diff --git a/backend/Cargo.lock b/backend/Cargo.lock index ece88212..57c977d5 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -403,12 +403,11 @@ dependencies = [ "deadpool-redis", "dotenv", "dto", - "env_logger", "error", "indexmap", "jsonwebtoken", - "log", "matchmaking", + "migration", "redis", "sea-orm", "security", @@ -417,6 +416,9 @@ dependencies = [ "service", "st_core", "tokio", + "tracing", + "tracing-actix-web", + "tracing-subscriber", "url", "utoipa 4.2.3", "utoipa-actix-web", @@ -435,7 +437,7 @@ checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" dependencies = [ "base64ct", "blake2", - "cpufeatures", + "cpufeatures 0.2.17", "password-hash", ] @@ -876,6 +878,17 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "chacha20" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.1", +] + [[package]] name = "challenge" version = "0.1.0" @@ -1039,6 +1052,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "crate-git-revision" version = "0.0.6" @@ -1121,7 +1143,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "curve25519-dalek-derive", "digest", "fiat-crypto", @@ -1394,29 +1416,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "env_filter" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32e90c2accc4b07a8456ea0debdc2e7587bdd890680d71173a15d4ae604f6eef" -dependencies = [ - "log", - "regex", -] - -[[package]] -name = "env_logger" -version = "0.11.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56a" -dependencies = [ - "anstream", - "anstyle", - "env_filter", - "jiff", - "log", -] - [[package]] name = "equivalent" version = "1.0.2" @@ -1726,10 +1725,22 @@ checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 5.3.0", "wasip2", ] +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.1", +] + [[package]] name = "glob" version = "0.3.3" @@ -1992,30 +2003,6 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" -[[package]] -name = "jiff" -version = "0.2.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f00b5dbd620d61dfdcb6007c9c1f6054ebd75319f163d886a9055cec1155073d" -dependencies = [ - "jiff-static", - "log", - "portable-atomic", - "portable-atomic-util", - "serde_core", -] - -[[package]] -name = "jiff-static" -version = "0.2.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e000de030ff8022ea1da3f466fbb0f3a809f5e51ed31f6dd931c35181ad8e6d7" -dependencies = [ - "proc-macro2 1.0.106", - "quote 1.0.45", - "syn 2.0.117", -] - [[package]] name = "jobserver" version = "0.1.34" @@ -2211,10 +2198,10 @@ dependencies = [ "actix-web", "chrono", "deadpool-redis", - "log", "redis", "serde", "serde_json", + "tracing", "uuid", ] @@ -2289,6 +2276,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "mutually_exclusive_features" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e94e1e6445d314f972ff7395df2de295fe51b71821694f0b0e1e79c4f12c8577" + [[package]] name = "nacl" version = "0.5.3" @@ -2343,6 +2336,15 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21" +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "num-bigint" version = "0.2.6" @@ -2623,6 +2625,26 @@ dependencies = [ "serde", ] +[[package]] +name = "pin-project" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" +dependencies = [ + "proc-macro2 1.0.106", + "quote 1.0.45", + "syn 2.0.117", +] + [[package]] name = "pin-project-lite" version = "0.2.17" @@ -2699,15 +2721,6 @@ version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" -[[package]] -name = "portable-atomic-util" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" -dependencies = [ - "portable-atomic", -] - [[package]] name = "powerfmt" version = "0.2.0" @@ -2868,6 +2881,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "radium" version = "0.7.0" @@ -2895,6 +2914,17 @@ dependencies = [ "rand_core 0.9.5", ] +[[package]] +name = "rand" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core 0.10.1", +] + [[package]] name = "rand_chacha" version = "0.3.1" @@ -2933,6 +2963,12 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + [[package]] name = "raw-cpuid" version = "11.6.0" @@ -3522,11 +3558,11 @@ dependencies = [ "dto", "engine", "error", - "log", "rand 0.8.6", "sea-orm", "serde_json", "tokio", + "tracing", "uuid", ] @@ -3537,7 +3573,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest", ] @@ -3554,7 +3590,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest", ] @@ -4333,6 +4369,19 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-actix-web" +version = "0.7.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36bb7a33ce7f0807d44124b5119ea3581fd59028db56477a7aa01741c869ca6a" +dependencies = [ + "actix-web", + "mutually_exclusive_features", + "pin-project", + "tracing", + "uuid", +] + [[package]] name = "tracing-attributes" version = "0.1.31" @@ -4351,6 +4400,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-serde" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" +dependencies = [ + "serde", + "tracing-core", ] [[package]] @@ -4360,12 +4431,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" dependencies = [ "matchers", + "nu-ansi-term", "once_cell", "regex-automata", + "serde", + "serde_json", "sharded-slab", + "smallvec", "thread_local", "tracing", "tracing-core", + "tracing-log", + "tracing-serde", ] [[package]] @@ -4554,13 +4631,13 @@ dependencies = [ [[package]] name = "uuid" -version = "1.20.0" +version = "1.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f" +checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" dependencies = [ - "getrandom 0.3.4", + "getrandom 0.4.3", "js-sys", - "rand 0.9.4", + "rand 0.10.2", "serde_core", "wasm-bindgen", ] @@ -4607,6 +4684,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + [[package]] name = "value-bag" version = "1.12.0" diff --git a/backend/modules/api/Cargo.toml b/backend/modules/api/Cargo.toml index 1552cbc0..daecee7b 100644 --- a/backend/modules/api/Cargo.toml +++ b/backend/modules/api/Cargo.toml @@ -5,8 +5,9 @@ edition = "2021" [dependencies] dotenv = "0.15.0" -env_logger = "0.11.8" -log = "0.4" +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] } +tracing-actix-web = "0.7" actix-web = "4" actix = "0.13" actix-web-actors = "4" diff --git a/backend/modules/api/src/ai.rs b/backend/modules/api/src/ai.rs index 88356d93..649c405f 100644 --- a/backend/modules/api/src/ai.rs +++ b/backend/modules/api/src/ai.rs @@ -7,6 +7,7 @@ use dto::{ responses::ValidationErrorResponse, }; use serde_json::json; +use tracing::error; use validator::Validate; use service::engine_service::EngineService; @@ -47,7 +48,7 @@ pub async fn get_ai_suggestion(payload: Json) -> HttpRespon computation_time_ms: elapsed, }), Err(e) => { - log::error!("Engine error in get_ai_suggestion: {}", e); + error!("Engine error in get_ai_suggestion: {}", e); HttpResponse::InternalServerError().json(json!({ "error": "internal server error" })) @@ -104,7 +105,7 @@ pub async fn analyze_position(payload: Json) -> HttpRes }) } Err(e) => { - log::error!("Engine error in analyze_position: {}", e); + error!("Engine error in analyze_position: {}", e); HttpResponse::InternalServerError().json(json!({ "error": "internal server error" })) diff --git a/backend/modules/api/src/auth.rs b/backend/modules/api/src/auth.rs index 8290f229..92ef76bc 100644 --- a/backend/modules/api/src/auth.rs +++ b/backend/modules/api/src/auth.rs @@ -3,6 +3,7 @@ use actix_web::{ post, web, HttpRequest, HttpResponse, }; use std::env; +use tracing::{error, warn}; use uuid::Uuid; use validator::Validate; @@ -129,7 +130,7 @@ pub async fn login( { Ok(t) => t, Err(e) => { - log::error!("Failed to generate refresh token: {}", e); + error!("Failed to generate refresh token: {}", e); return HttpResponse::InternalServerError().json(ErrorResponse { message: "Failed to generate refresh token".to_string(), code: "TOKEN_ERROR".to_string(), @@ -241,7 +242,7 @@ pub async fn refresh( { Ok(fid) => fid, Err(TokenServiceError::TokenReuseDetected) => { - log::warn!("Token reuse detected for player {}", claims.user_id); + warn!("Token reuse detected for player {}", claims.user_id); return HttpResponse::Unauthorized().json(ErrorResponse { message: "Token reuse detected. Account locked for security.".to_string(), code: "TOKEN_THEFT_DETECTED".to_string(), @@ -289,7 +290,7 @@ pub async fn refresh( { Ok(t) => t, Err(e) => { - log::error!("Failed to generate new refresh token: {}", e); + error!("Failed to generate new refresh token: {}", e); return HttpResponse::InternalServerError().json(ErrorResponse { message: "Failed to generate new refresh token".to_string(), code: "TOKEN_ERROR".to_string(), @@ -377,7 +378,7 @@ pub async fn logout( // Revoke all tokens for this player if let Err(e) = TokenService::revoke_player_tokens(db.get_ref(), user_id).await { - log::error!("Failed to revoke tokens: {}", e); + error!("Failed to revoke tokens: {}", e); return HttpResponse::InternalServerError().json(ErrorResponse { message: "Failed to logout".to_string(), code: "LOGOUT_ERROR".to_string(), diff --git a/backend/modules/api/src/games.rs b/backend/modules/api/src/games.rs index 7fabb856..5a886cc7 100644 --- a/backend/modules/api/src/games.rs +++ b/backend/modules/api/src/games.rs @@ -203,8 +203,32 @@ pub async fn list_games( let limit = query.limit.unwrap_or(10); let cursor = query.cursor.clone(); - match GameService::list_games(db.get_ref(), cursor, limit, query.player_id, status_enum).await { - Ok((games, next_cursor)) => { + // Compute offset from page (if page is provided and cursor is not) + let offset: Option = if cursor.is_none() { + query.page.map(|p| { + let page = if p < 1 { + tracing::warn!("Invalid page value {} — clamping to 1", p); + 1 + } else { + p as u64 + }; + (page - 1) * limit + }) + } else { + None + }; + + match GameService::list_games( + db.get_ref(), + cursor, + offset, + limit, + query.player_id, + status_enum, + ) + .await + { + Ok((games, next_cursor, total_count)) => { let game_dtos: Vec = games .into_iter() .map(|g| { @@ -230,6 +254,7 @@ pub async fn list_games( "message": "Games found", "data": { "games": game_dtos, + "total_count": total_count, "next_cursor": next_cursor, "limit": limit, } diff --git a/backend/modules/api/src/rate_limiter.rs b/backend/modules/api/src/rate_limiter.rs index 397da048..f0b197b1 100644 --- a/backend/modules/api/src/rate_limiter.rs +++ b/backend/modules/api/src/rate_limiter.rs @@ -10,6 +10,7 @@ use std::{ rc::Rc, task::{Context, Poll}, }; +use tracing::warn; /// Redis-backed rate limiter middleware for actix-web. /// @@ -114,7 +115,7 @@ where let mut conn = match pool.get().await { Ok(c) => c, Err(e) => { - log::warn!( + warn!( "Redis rate limiter connection failed: {}. Allowing request.", e ); @@ -130,7 +131,7 @@ where { Ok(c) => c, Err(e) => { - log::warn!("Redis INCR failed: {}. Allowing request.", e); + warn!("Redis INCR failed: {}. Allowing request.", e); return service.call(req).await.map(ServiceResponse::map_into_boxed_body); } }; diff --git a/backend/modules/api/src/server.rs b/backend/modules/api/src/server.rs index 030aa691..5e1a0e6a 100644 --- a/backend/modules/api/src/server.rs +++ b/backend/modules/api/src/server.rs @@ -18,12 +18,13 @@ use challenge::api::configure_puzzle_routes; use challenge::puzzle_validation::PuzzleValidationService; use dotenv::dotenv; use matchmaking::redis::{create_redis_pool, test_redis_connection}; -use matchmaking::service::MatchmakingService; -use migration::{Migrator, MigratorTrait}; +use matchmaking::MatchmakingService; +use migration::Migrator; +use migration::MigratorTrait; +use security::jwt::{JwtAuthMiddleware, JwtService}; +use tracing::{info, warn, error}; +use tracing_actix_web::TracingLogger; use sea_orm::Database; -use security::JwtAuthMiddleware; -use security::JwtService; -use st_core::endpoint::configure as configure_nft_routes; use std::env; use std::sync::Arc; use utoipa::OpenApi; @@ -49,8 +50,24 @@ pub async fn main() -> std::io::Result<()> { // Load environment variables from .env file dotenv().ok(); - // Initialize logger - env_logger::init(); + // Initialize structured logger with JSON output in release builds + { + use tracing_subscriber::EnvFilter; + + let env_filter = EnvFilter::try_from_default_env() + .unwrap_or_else(|_| EnvFilter::new("info")); + + let subscriber = tracing_subscriber::fmt() + .with_env_filter(env_filter); + + #[cfg(debug_assertions)] + let subscriber = subscriber.pretty(); + + #[cfg(not(debug_assertions))] + let subscriber = subscriber.json(); + + subscriber.init(); + } // Load configuration from environment — critical secrets have no fallbacks (BE-27) let server_addr = env::var("SERVER_ADDR").unwrap_or_else(|_| "127.0.0.1:8080".to_string()); @@ -66,18 +83,19 @@ pub async fn main() -> std::io::Result<()> { "REDIS_URL must be set. Refusing to start with a hardcoded fallback.", ); - eprintln!("Initializing KnightVerse Backend Server"); - eprintln!("Server address: {}", server_addr); + info!("Initializing KnightVerse Backend Server"); + info!("Server address: {}", server_addr); // Connect to database let db = match Database::connect(&database_url).await { Ok(conn) => { - eprintln!("Database connection successful"); + info!("Database connection successful"); conn } Err(e) => { - eprintln!("Failed to connect to database: {}", e); - return Err(std::io::Error::other( + error!("Failed to connect to database: {}", e); + return Err(std::io::Error::new( + std::io::ErrorKind::Other, "Database connection failed", )); } @@ -102,12 +120,12 @@ pub async fn main() -> std::io::Result<()> { let config = AppConfig::from_env(); // Initialize Matchmaking - eprintln!("Connecting to Redis for matchmaking at {}", redis_url); + info!("Connecting to Redis for matchmaking at {}", redis_url); let redis_pool = create_redis_pool(&redis_url).expect("Failed to create Redis pool"); // Optional: test connection if let Err(e) = test_redis_connection(&redis_pool).await { - eprintln!("Warning: Redis connection test failed: {}", e); + warn!("Warning: Redis connection test failed: {}", e); } let rate_limiter_pool = redis_pool.clone(); @@ -116,7 +134,7 @@ pub async fn main() -> std::io::Result<()> { // Initialize Puzzle Validation Service let puzzle_service = Arc::new(PuzzleValidationService::new(jwt_secret.clone())); - eprintln!("Starting HTTP server on {}", server_addr); + info!("Starting HTTP server on {}", server_addr); // Define the app factory closure let app_factory = move || { @@ -179,10 +197,8 @@ pub async fn main() -> std::io::Result<()> { ); App::new() - .wrap(actix_web::middleware::DefaultHeaders::new().add(( - "Strict-Transport-Security", - "max-age=31536000; includeSubDomains", - ))) + .wrap(TracingLogger::default()) + .wrap(actix_web::middleware::DefaultHeaders::new().add(("Strict-Transport-Security", "max-age=31536000; includeSubDomains"))) // Global middleware .wrap(cors) // App data @@ -241,8 +257,8 @@ pub async fn main() -> std::io::Result<()> { .service(get_ai_suggestion) .service(analyze_position), ) - // NFT routes - .service(web::scope("/api/v1").configure(configure_nft_routes)) + // NFT routes (placeholder — not yet implemented) + // .service(web::scope("/api/v1").configure(configure_nft_routes)) // Swagger UI integration .service( SwaggerUi::new("/api/docs/{_:.*}") @@ -266,7 +282,7 @@ pub async fn main() -> std::io::Result<()> { if let Ok(workers_str) = env::var("WORKERS") { if let Ok(workers) = workers_str.parse::() { - println!("Setting worker count to {}", workers); + info!("Setting worker count to {}", workers); http_server = http_server.workers(workers); } } diff --git a/backend/modules/api/src/ws.rs b/backend/modules/api/src/ws.rs index 3a371e08..754b3297 100644 --- a/backend/modules/api/src/ws.rs +++ b/backend/modules/api/src/ws.rs @@ -8,6 +8,7 @@ use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; use std::collections::{HashMap, HashSet}; use std::env; +use tracing::{error, info, warn}; use uuid::Uuid; // For Redis Pub/Sub @@ -158,7 +159,7 @@ impl WsSession { ctx.run_interval(Self::HEARTBEAT_INTERVAL, |act, ctx| { let elapsed = std::time::Instant::now().duration_since(act.hb); if elapsed > Self::CLIENT_TIMEOUT { - log::warn!( + warn!( "WebSocket timeout for game {}: no pong in {}s, terminating connection", act.game_id, elapsed.as_secs() @@ -187,7 +188,7 @@ impl Actor for WsSession { } fn stopped(&mut self, ctx: &mut Self::Context) { - log::info!("WebSocket disconnected for game: {}", self.game_id); + info!("WebSocket disconnected for game: {}", self.game_id); // Send reconnection token to client for seamless reconnection if let Ok(reconnect_token) = self.generate_reconnect_token() { @@ -198,9 +199,9 @@ impl Actor for WsSession { // Try to send the reconnection token ctx.address().do_send(reconnect_msg); - log::info!("Sent reconnection token for user: {}", self.username); + info!("Sent reconnection token for user: {}", self.username); } else { - log::error!( + error!( "Failed to generate reconnection token for user: {}", self.username ); diff --git a/backend/modules/matchmaking/Cargo.toml b/backend/modules/matchmaking/Cargo.toml index 241f26ef..411c4fda 100644 --- a/backend/modules/matchmaking/Cargo.toml +++ b/backend/modules/matchmaking/Cargo.toml @@ -14,4 +14,4 @@ uuid = { version = "1", features = ["v4", "serde"] } chrono = { version = "0.4", features = ["serde"] } deadpool-redis = "0.14" redis = { version = "0.24", features = ["tokio-comp", "json"] } -log = "0.4" +tracing = "0.1" diff --git a/backend/modules/matchmaking/routes.rs b/backend/modules/matchmaking/routes.rs index 0fb22ed8..ef2703af 100644 --- a/backend/modules/matchmaking/routes.rs +++ b/backend/modules/matchmaking/routes.rs @@ -1,6 +1,7 @@ use actix_web::{web, HttpResponse, Responder}; use chrono::Utc; use serde::{Deserialize, Serialize}; +use tracing::error; use uuid::Uuid; use super::models::*; @@ -76,7 +77,7 @@ async fn join_queue( match service.join_queue(match_request).await { Ok(response) => HttpResponse::Ok().json(response), Err(e) => { - log::error!("Failed to join queue: {}", e); + error!("Failed to join queue: {}", e); HttpResponse::ServiceUnavailable().json(ErrorResponse { status: "error".to_string(), error: "internal_error".to_string(), @@ -101,7 +102,7 @@ async fn get_status( queue_status: None, }), Err(e) => { - log::error!("Failed to get queue status: {}", e); + error!("Failed to get queue status: {}", e); HttpResponse::ServiceUnavailable().json(ErrorResponse { status: "error".to_string(), error: "internal_error".to_string(), @@ -122,7 +123,7 @@ async fn cancel_request( "status": "Request not found" })), Err(e) => { - log::error!("Failed to cancel request: {}", e); + error!("Failed to cancel request: {}", e); HttpResponse::ServiceUnavailable().json(ErrorResponse { status: "error".to_string(), error: "internal_error".to_string(), @@ -150,7 +151,7 @@ async fn accept_invite( "status": "Invite not found" })), Err(e) => { - log::error!("Failed to accept invite: {}", e); + error!("Failed to accept invite: {}", e); HttpResponse::ServiceUnavailable().json(ErrorResponse { status: "error".to_string(), error: "internal_error".to_string(), diff --git a/backend/modules/service/Cargo.toml b/backend/modules/service/Cargo.toml index 9a519c2e..a1df588b 100644 --- a/backend/modules/service/Cargo.toml +++ b/backend/modules/service/Cargo.toml @@ -12,7 +12,7 @@ chrono = { version = "0.4", features = ["serde"] } base64 = "0.22" tokio = { version = "1", features = ["full", "sync"] } serde_json = "1" -log = "0.4" +tracing = "0.1" dto = { path = "../dto"} db = {path = "../db"} diff --git a/backend/modules/service/src/circuit_breaker.rs b/backend/modules/service/src/circuit_breaker.rs index 57948788..141669d6 100644 --- a/backend/modules/service/src/circuit_breaker.rs +++ b/backend/modules/service/src/circuit_breaker.rs @@ -3,6 +3,7 @@ use std::sync::Arc; use std::time::Duration; use tokio::sync::Mutex; use tokio::time::Instant; +use tracing::{info, warn}; /// Circuit breaker state machine. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -109,7 +110,7 @@ impl InnerState { if opened_at.elapsed() >= self.config.open_timeout { self.state = CircuitState::HalfOpen; self.consecutive_successes = 0; - log::info!("Circuit breaker transitioning from open to half-open"); + info!("Circuit breaker transitioning from open to half-open"); return Ok(()); } } @@ -132,7 +133,7 @@ impl InnerState { if self.consecutive_successes >= self.config.success_threshold { self.state = CircuitState::Closed; self.consecutive_successes = 0; - log::info!("Circuit breaker closed after successful half-open tests"); + info!("Circuit breaker closed after successful half-open tests"); } } CircuitState::Open => { @@ -150,7 +151,7 @@ impl InnerState { if self.consecutive_failures >= self.config.failure_threshold { self.state = CircuitState::Open; self.opened_at = Some(Instant::now()); - log::warn!( + warn!( "Circuit breaker opened after {} consecutive failures", self.consecutive_failures ); @@ -160,7 +161,7 @@ impl InnerState { // Any failure in half-open re-opens the circuit self.state = CircuitState::Open; self.opened_at = Some(Instant::now()); - log::warn!("Circuit breaker re-opened after failure in half-open state"); + warn!("Circuit breaker re-opened after failure in half-open state"); } CircuitState::Open => { // Already open — keep counting but state stays open diff --git a/backend/modules/service/src/engine_service.rs b/backend/modules/service/src/engine_service.rs index bab8062a..a082bff2 100644 --- a/backend/modules/service/src/engine_service.rs +++ b/backend/modules/service/src/engine_service.rs @@ -3,6 +3,7 @@ use engine::{process::ProcessEngine, Engine, EngineError, EngineResult, GoParams use std::collections::HashMap; use std::sync::Arc; use tokio::sync::Mutex; +use tracing::{error, warn}; use uuid::Uuid; pub struct EngineService { @@ -69,17 +70,17 @@ impl EngineService { match result { Ok(engine_result) => Ok(engine_result), Err(CircuitBreakerError::CircuitOpen) => { - log::warn!("Circuit breaker is open — engine request rejected"); + warn!("Circuit breaker is open — engine request rejected"); Err(EngineError::Unknown( "Engine is temporarily unavailable (circuit breaker open)".to_string(), )) } Err(CircuitBreakerError::OperationTimeout) => { - log::error!("Engine operation timed out"); + error!("Engine operation timed out"); Err(EngineError::Timeout) } Err(CircuitBreakerError::OperationFailed(msg)) => { - log::error!("Engine operation failed: {}", msg); + error!("Engine operation failed: {}", msg); Err(EngineError::Unknown(format!("Engine failure: {}", msg))) } } diff --git a/backend/modules/service/src/games.rs b/backend/modules/service/src/games.rs index fb8f69d1..b60e5a3f 100644 --- a/backend/modules/service/src/games.rs +++ b/backend/modules/service/src/games.rs @@ -8,8 +8,8 @@ use dto::games::{ }; use error::error::ApiError; use sea_orm::{ - ActiveModelTrait, ColumnTrait, DbErr, EntityTrait, Order, QueryFilter, QueryOrder, QuerySelect, - Set, TransactionTrait, + ActiveModelTrait, ColumnTrait, DbErr, EntityTrait, Order, PaginatorTrait, QueryFilter, + QueryOrder, QuerySelect, Set, TransactionTrait, }; use sea_orm::{Condition, DatabaseConnection}; use uuid::Uuid; @@ -418,54 +418,66 @@ impl GameService { chess::RatingService::get_player_rating(db, player_id).await } - /// List games with keyset pagination. - /// + /// List games with keyset or offset pagination. + /// /// # Arguments /// * `db` - Database connection - /// * `cursor` - Optional cursor string (base64 encoded "timestamp,id") + /// * `cursor` - Optional cursor string (base64 encoded "timestamp,id") for keyset pagination + /// * `offset` - Optional offset for offset-based pagination (takes precedence over cursor if both provided) /// * `limit` - Number of items to return /// * `player_id` - Optional player ID filter (checks both white and black players) /// * `status` - Optional status filter (currently maps to result being not null for finished games, or specific status if column exists) /// /// Note: The current schema uses `result` to determine if a game is finished. /// Active games might have `result` as NULL (after our migration). - pub async fn list_games( - db: &DatabaseConnection, - cursor: Option, - limit: u64, + /// Build the shared filter conditions for player_id and status. + fn build_filter_condition( player_id: Option, - status: Option, - ) -> Result<(Vec, Option), DbErr> { - let mut query = Game::find(); + status: &Option, + ) -> Option { + let mut conditions: Vec = Vec::new(); - // 1. Apply Filtering if let Some(pid) = player_id { - // Filter by player (white OR black) - // effective union of indexes logic would be nice, but OR is simpler to write here. - // "idx_games_white_player_created_at_id" and "idx_games_black_player_created_at_id" - // Postgres creates a BitmapOr for these two indexes usually. - let condition = Condition::any() + let player_cond = Condition::any() .add(game::Column::WhitePlayer.eq(pid)) .add(game::Column::BlackPlayer.eq(pid)); - query = query.filter(condition); + conditions.push(player_cond); } - if let Some(s) = status { + if let Some(s) = status.as_ref() { match s { GameStatus::Waiting | GameStatus::InProgress => { - // Active games: result is NULL - query = query.filter(game::Column::Result.is_null()); + conditions.push(Condition::all().add(game::Column::Result.is_null())); } GameStatus::Completed | GameStatus::Aborted => { - // Finished games: result is NOT NULL - // Note: "Aborted" vs "Completed" might need distinguishing via ResultSide if we had it, - // but for now we just check if it has a result. - query = query.filter(game::Column::Result.is_not_null()); + conditions.push(Condition::all().add(game::Column::Result.is_not_null())); } } } - // 2. Apply Cursor (Keyset Pagination) + if conditions.is_empty() { + None + } else { + Some(conditions.into_iter().reduce(|acc, c| acc.add(c)).unwrap()) + } + } + + pub async fn list_games( + db: &DatabaseConnection, + cursor: Option, + offset: Option, + limit: u64, + player_id: Option, + status: Option, + ) -> Result<(Vec, Option, u64), DbErr> { + let filter_condition = Self::build_filter_condition(player_id, &status); + + let mut query = Game::find(); + if let Some(ref cond) = filter_condition { + query = query.filter(cond.clone()); + } + + // 1. Apply Cursor (Keyset Pagination) // Sort by created_at DESC, id DESC query = query .order_by(game::Column::CreatedAt, Order::Desc) @@ -507,7 +519,19 @@ impl GameService { } } - // 3. Limit and Execution + // 2. Count total matching games (before pagination) + let mut count_query = Game::find(); + if let Some(ref cond) = filter_condition { + count_query = count_query.filter(cond.clone()); + } + let total_count = count_query.count(db).await?; + + // 3. Apply offset if provided (offset-based pagination) + if let Some(off) = offset { + query = query.offset(off); + } + + // 4. Limit and Execution // Fetch limit + 1 to check if there is a next page let results = query.limit(limit + 1).all(db).await?; @@ -525,7 +549,7 @@ impl GameService { } } - Ok((games, next_cursor)) + Ok((games, next_cursor, total_count)) } fn encode_cursor(timestamp: DateTime, id: Uuid) -> String { @@ -587,9 +611,14 @@ mod tests { #[tokio::test] async fn test_list_games_query_structure() { // Create Mock Database to verify the generated SQL + // We need two query result sets: one for count, one for the main query let db = MockDatabase::new(DbBackend::Postgres) .append_query_results(vec![ - // First query result (empty list is fine, we check SQL) + // First query result (count) + vec![], + ]) + .append_query_results(vec![ + // Second query result (main data) vec![game::Model { id: Uuid::new_v4(), white_player: Uuid::new_v4(), @@ -609,15 +638,22 @@ mod tests { .into_connection(); let player_id = Uuid::new_v4(); - - let _result = GameService::list_games(&db, None, 10, Some(player_id), None).await; - + + let _result = GameService::list_games( + &db, + None, + None, + 10, + Some(player_id), + None + ).await; + // Get transaction log to verify SQL let transaction_log = db.into_transaction_log(); - - // We expect one query - assert_eq!(transaction_log.len(), 1); - + + // We expect two queries (count + data) + assert_eq!(transaction_log.len(), 2); + let log = &transaction_log[0]; let log_str = format!("{:?}", log); println!("Log: {}", log_str); @@ -639,25 +675,38 @@ mod tests { let cursor = GameService::encode_cursor(last_time, last_id); let db = MockDatabase::new(DbBackend::Postgres) - .append_query_results(vec![vec![game::Model { - id: Uuid::new_v4(), - white_player: Uuid::new_v4(), - black_player: Uuid::new_v4(), - fen: "fen".to_string(), - pgn: serde_json::json!({}), - result: None, - variant: db_entity::game::GameVariant::Standard, - started_at: Utc::now().with_timezone(&FixedOffset::east_opt(0).unwrap()), - duration_sec: 600, - created_at: Utc::now().with_timezone(&FixedOffset::east_opt(0).unwrap()), - updated_at: Utc::now().with_timezone(&FixedOffset::east_opt(0).unwrap()), - is_imported: false, - original_pgn: None, + .append_query_results(vec![ + // First query result (count) + vec![], + ]) + .append_query_results(vec![ + // Second query result (main data) + vec![game::Model { + id: Uuid::new_v4(), + white_player: Uuid::new_v4(), + black_player: Uuid::new_v4(), + fen: "fen".to_string(), + pgn: serde_json::json!({}), + result: None, + variant: db_entity::game::GameVariant::Standard, + started_at: Utc::now().with_timezone(&FixedOffset::east_opt(0).unwrap()), + duration_sec: 600, + created_at: Utc::now().with_timezone(&FixedOffset::east_opt(0).unwrap()), + updated_at: Utc::now().with_timezone(&FixedOffset::east_opt(0).unwrap()), + is_imported: false, + original_pgn: None, }]]) .into_connection(); - - let _result = GameService::list_games(&db, Some(cursor), 10, None, None).await; - + + let _result = GameService::list_games( + &db, + Some(cursor), + None, + 10, + None, + None + ).await; + let transaction_log = db.into_transaction_log(); let log = &transaction_log[0]; let log_str = format!("{:?}", log); diff --git a/backend/modules/src/main.rs b/backend/modules/src/main.rs index ab1e8711..71e5e15e 100644 --- a/backend/modules/src/main.rs +++ b/backend/modules/src/main.rs @@ -6,9 +6,20 @@ use std::env; #[actix_web::main] async fn main() -> std::io::Result<()> { dotenv().ok(); - env_logger::init(); - println!("Starting KnightVerse Backend Server..."); + // Initialize structured logger + { + use tracing_subscriber::EnvFilter; + let env_filter = EnvFilter::try_from_default_env() + .unwrap_or_else(|_| EnvFilter::new("info")); + #[cfg(debug_assertions)] + let subscriber = tracing_subscriber::fmt().with_env_filter(env_filter).pretty(); + #[cfg(not(debug_assertions))] + let subscriber = tracing_subscriber::fmt().with_env_filter(env_filter).json(); + subscriber.init(); + } + + eprintln!("Starting KnightVerse Backend Server..."); // Initialize Redis connection pool let redis_url = env::var("REDIS_URL") @@ -22,14 +33,14 @@ async fn main() -> std::io::Result<()> { // Test Redis connection on startup match matchmaking::redis::test_redis_connection(&redis_pool).await { - Ok(_) => println!("✅ Redis connection successful"), + Ok(_) => eprintln!("✅ Redis connection successful"), Err(e) => { eprintln!("⚠️ Warning: Redis connection failed: {}", e); eprintln!("Matchmaking service will not be available"); } } - println!("Server starting on http://127.0.0.1:8080"); + eprintln!("Server starting on http://127.0.0.1:8080"); HttpServer::new(move || { App::new() diff --git a/backend/src/Cargo.toml b/backend/src/Cargo.toml index afedea2b..84648003 100644 --- a/backend/src/Cargo.toml +++ b/backend/src/Cargo.toml @@ -11,6 +11,8 @@ chrono = { version = "0.4", features = ["serde"] } rand = "0.8" lazy_static = "1.4" api = { path = "../modules/api" } +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] } [dev-dependencies] tokio = { version = "1", features = ["full"] } diff --git a/backend/src/socket/Cargo.toml b/backend/src/socket/Cargo.toml index c5056e00..309a0c04 100644 --- a/backend/src/socket/Cargo.toml +++ b/backend/src/socket/Cargo.toml @@ -18,8 +18,8 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" uuid = { version = "1.0", features = ["v4"] } lazy_static = "1.4" -log = "0.4" -env_logger = "0.11" +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] } [dev-dependencies] tokio-test = "0.4" diff --git a/backend/src/socket/game.rs b/backend/src/socket/game.rs index 0c579b4a..118130c8 100644 --- a/backend/src/socket/game.rs +++ b/backend/src/socket/game.rs @@ -2,6 +2,7 @@ use std::collections::HashMap; use std::sync::{Arc, Mutex}; use std::time::SystemTime; use tokio::sync::broadcast; +use tracing::{info, warn}; use uuid::Uuid; use crate::models::{GameStatus, PieceColor, Player, Room, ServerMessage}; @@ -26,7 +27,7 @@ lazy_static::lazy_static! { pub fn init_game_state() { // This function is called at startup to ensure the lazy_static is initialized let _guard = GAME_STATE.lock().unwrap(); - log::info!("Game state initialized"); + info!("Game state initialized"); } // Get a clone of the message sender for a room @@ -59,7 +60,7 @@ pub fn create_room_with_time(initial_time_ms: u64, increment_ms: u64) -> String ); state.message_senders.insert(room_id.clone(), tx); - log::info!( + info!( "Created room {} with time control: {}ms + {}ms increment", room_id, initial_time_ms, increment_ms ); @@ -108,7 +109,7 @@ pub fn join_room(room_id: &str, player_id: &str, player_name: Option) -> .map_err(|e| format!("System clock error: {}", e))? .as_millis() as u64; room.last_move_at = Some(now_ms); - log::info!("Game started in room {}, clock started at {}ms", room_id, now_ms); + info!("Game started in room {}, clock started at {}ms", room_id, now_ms); } // Create response message @@ -122,7 +123,7 @@ pub fn join_room(room_id: &str, player_id: &str, player_name: Option) -> // Broadcast to other players in the room if let Some(sender) = state.message_senders.get(room_id) { if let Err(e) = sender.send(response.clone()) { - log::warn!("Failed to broadcast RoomJoined message: {:?}", e); + warn!("Failed to broadcast RoomJoined message: {:?}", e); } } @@ -164,7 +165,7 @@ pub fn send_move(room_id: &str, player_id: &str, move_notation: &str) -> Result< let winner_color = if is_white { "Black" } else { "White" }; let loser_color = if is_white { "White" } else { "Black" }; - log::warn!( + warn!( "Move rejected: player {} in room {} exceeded time. Elapsed: {}ms, Remaining: {}ms, Buffer: {}ms", player_id, room_id, elapsed_ms, player_remaining, LATENCY_BUFFER_MS ); diff --git a/backend/src/socket/handlers.rs b/backend/src/socket/handlers.rs index 09194c6b..05471e95 100644 --- a/backend/src/socket/handlers.rs +++ b/backend/src/socket/handlers.rs @@ -2,6 +2,7 @@ use futures_util::{SinkExt, StreamExt}; use serde_json::{from_str, to_string}; use tokio::sync::broadcast; use tokio_tungstenite::tungstenite::Message; +use tracing::{error, info}; use crate::game::{ accept_takeback, @@ -28,7 +29,7 @@ pub async fn handle_client_message( let client_message: ClientMessage = match from_str(message) { Ok(msg) => msg, Err(e) => { - log::error!("Failed to parse client message: {}", e); + error!("Failed to parse client message: {}", e); let error_msg = ServerMessage::Error { code: "PARSE_ERROR".to_string(), message: "Failed to parse message".to_string(), @@ -43,7 +44,7 @@ pub async fn handle_client_message( // Handle the message based on its type match client_message { ClientMessage::JoinRoom(payload) => { - log::info!( + info!( "Player {} joining room {}", payload.player_id, payload.room_id @@ -69,7 +70,7 @@ pub async fn handle_client_message( } } ClientMessage::SendMove(payload) => { - log::info!( + info!( "Player {} making move {} in room {}", payload.player_id, payload.move_notation, @@ -90,7 +91,7 @@ pub async fn handle_client_message( } } ClientMessage::LeaveRoom(payload) => { - log::info!( + info!( "Player {} leaving room {}", payload.player_id, payload.room_id @@ -113,7 +114,7 @@ pub async fn handle_client_message( } } ClientMessage::RequestGameLog(payload) => { - log::info!("Game log requested for room {}", payload.room_id); + info!("Game log requested for room {}", payload.room_id); match get_game_log(&payload.room_id) { Ok(response) => { @@ -129,7 +130,7 @@ pub async fn handle_client_message( } } ClientMessage::OfferTakeback(payload) => { - log::info!( + info!( "Player {} offering takeback in room {}", payload.player_id, payload.room_id @@ -149,7 +150,7 @@ pub async fn handle_client_message( } } ClientMessage::AcceptTakeback(payload) => { - log::info!( + info!( "Player {} accepting takeback in room {}", payload.player_id, payload.room_id @@ -169,7 +170,7 @@ pub async fn handle_client_message( } } ClientMessage::RejectTakeback(payload) => { - log::info!( + info!( "Player {} rejecting takeback in room {}", payload.player_id, payload.room_id diff --git a/backend/src/socket/main.rs b/backend/src/socket/main.rs index 575a7bf4..e3ae6a9e 100644 --- a/backend/src/socket/main.rs +++ b/backend/src/socket/main.rs @@ -5,40 +5,50 @@ mod websocket; use std::env; use tokio::net::TcpListener; +use tracing::{info, error}; +use tracing_subscriber::EnvFilter; use websocket::handle_connection; #[tokio::main] async fn main() -> Result<(), Box> { - // Initialize the logger - env_logger::init_from_env(env_logger::Env::default().default_filter_or("info")); + // Initialize structured logger + { + let env_filter = EnvFilter::try_from_default_env() + .unwrap_or_else(|_| EnvFilter::new("info")); + #[cfg(debug_assertions)] + let subscriber = tracing_subscriber::fmt().with_env_filter(env_filter).pretty(); + #[cfg(not(debug_assertions))] + let subscriber = tracing_subscriber::fmt().with_env_filter(env_filter).json(); + subscriber.init(); + } // Get the address from environment or use default let addr = env::var("BIND_ADDRESS").unwrap_or_else(|_| "127.0.0.1:8080".to_string()); - log::info!("Starting WebSocket server on {}", addr); + info!("Starting WebSocket server on {}", addr); // Initialize the game state game::init_game_state(); // Create the TCP listener let listener = TcpListener::bind(&addr).await?; - log::info!("WebSocket server listening on: {}", addr); + info!("WebSocket server listening on: {}", addr); // Accept connections loop { match listener.accept().await { Ok((stream, addr)) => { - log::info!("New connection from: {}", addr); + info!("New connection from: {}", addr); // Spawn a new task for each connection tokio::spawn(async move { if let Err(e) = handle_connection(stream, addr).await { - log::error!("Error handling connection: {}", e); + error!("Error handling connection: {}", e); } }); } Err(e) => { - log::error!("Failed to accept connection: {}", e); + error!("Failed to accept connection: {}", e); // Continue accepting connections despite errors } } diff --git a/backend/src/socket/websocket.rs b/backend/src/socket/websocket.rs index 734c5c4a..867f308c 100644 --- a/backend/src/socket/websocket.rs +++ b/backend/src/socket/websocket.rs @@ -3,6 +3,7 @@ use std::net::SocketAddr; use tokio::net::TcpStream; use tokio::sync::broadcast; use tokio_tungstenite::{accept_async, tungstenite::Message, WebSocketStream}; +use tracing::{error, info}; use crate::handlers::handle_client_message; use crate::models::ServerMessage; @@ -14,7 +15,7 @@ pub async fn handle_connection( ) -> Result<(), Box> { // Accept the WebSocket connection let ws_stream = accept_async(stream).await?; - log::info!("WebSocket connection established with: {}", addr); + info!("WebSocket connection established with: {}", addr); // Split the WebSocket stream let (mut ws_sender, mut ws_receiver) = ws_stream.split(); @@ -33,17 +34,17 @@ pub async fn handle_connection( match msg { Message::Text(text) => { if let Err(e) = handle_client_message(&text, &mut ws_sender, &mut room_senders).await { - log::error!("Error handling client message: {}", e); + error!("Error handling client message: {}", e); break; } } Message::Close(_) => { - log::info!("Client {} disconnected", addr); + info!("Client {} disconnected", addr); break; } Message::Ping(data) => { if let Err(e) = ws_sender.send(Message::Pong(data)).await { - log::error!("Error sending pong: {}", e); + error!("Error sending pong: {}", e); break; } } @@ -51,11 +52,11 @@ pub async fn handle_connection( } } Some(Err(e)) => { - log::error!("WebSocket error: {}", e); + error!("WebSocket error: {}", e); break; } None => { - log::info!("Client {} disconnected", addr); + info!("Client {} disconnected", addr); break; } } @@ -76,7 +77,7 @@ if room_receivers.len() != room_senders.len() { if let Ok(msg) = receiver.try_recv() { if let Ok(json) = serde_json::to_string(&msg) { if let Err(e) = ws_sender.send(Message::Text(json)).await { - log::error!("Error forwarding room message: {}", e); + error!("Error forwarding room message: {}", e); return; } }