diff --git a/Cargo.lock b/Cargo.lock index de65a25..86fa896 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2583,6 +2583,7 @@ name = "oj_rc_chat_room" version = "0.5.0" dependencies = [ "async-trait", + "chrono", "clap", "env_logger", "git-version", diff --git a/assets/robocraft/config.json b/assets/robocraft/config.json index 351af06..ce2c195 100644 --- a/assets/robocraft/config.json +++ b/assets/robocraft/config.json @@ -14491,6 +14491,14 @@ }, "permission": "Developer" }, + { + "regex": "\\?stats?", + "op": { + "type": "BuiltIn", + "built_in": "Stats" + }, + "permission": "Player" + }, { "regex": "\\?broadcast", "op": { diff --git a/rc_chat_room/Cargo.toml b/rc_chat_room/Cargo.toml index 959489e..b81a666 100644 --- a/rc_chat_room/Cargo.toml +++ b/rc_chat_room/Cargo.toml @@ -21,3 +21,4 @@ serde_json.workspace = true regex = "1" async-trait.workspace = true git-version.workspace = true +chrono.workspace = true diff --git a/rc_chat_room/src/main.rs b/rc_chat_room/src/main.rs index a9fd8d8..377734d 100644 --- a/rc_chat_room/src/main.rs +++ b/rc_chat_room/src/main.rs @@ -17,8 +17,13 @@ use polariton::operation::{OperationResponse, Typed}; pub type UserTy = oj_rc_core::UserState; +pub static START_TIMESTAMP_S: std::sync::atomic::AtomicI64 = std::sync::atomic::AtomicI64::new(0); +pub static READY_DURATION_NS: std::sync::atomic::AtomicI64 = std::sync::atomic::AtomicI64::new(0); + #[tokio::main] async fn main() -> std::io::Result<()> { + let start_time = chrono::Utc::now(); + START_TIMESTAMP_S.store(start_time.timestamp(), std::sync::atomic::Ordering::Relaxed); env_logger::init(); let args = cli::CliArgs::get(); log::debug!("Got cli args {:?}", args); @@ -34,6 +39,9 @@ async fn main() -> std::io::Result<()> { let listener = net::TcpListener::bind(std::net::SocketAddr::new(ip_addr, args.port)).await?; + let ready_dur = chrono::Utc::now() - start_time; + READY_DURATION_NS.store(ready_dur.num_nanoseconds().unwrap_or(-1), std::sync::atomic::Ordering::Relaxed); + log::info!("chat_room ready"); if args.once { log::warn!("Handling first connection and then exiting"); let (socket, address) = listener.accept().await?; diff --git a/rc_chat_room/src/state/chat/config.rs b/rc_chat_room/src/state/chat/config.rs index de2e376..0a244a4 100644 --- a/rc_chat_room/src/state/chat/config.rs +++ b/rc_chat_room/src/state/chat/config.rs @@ -117,6 +117,7 @@ enum BuiltIn { Intercom(Intercom), OnlineUsers, TotalUsers, + Stats, Version, Help, } @@ -127,6 +128,7 @@ impl BuiltIn { oj_rc_core::persist::BuiltInChatOperation::Intercom(com) => Self::Intercom(Intercom::from_persist(com)), oj_rc_core::persist::BuiltInChatOperation::OnlineUsers => Self::OnlineUsers, oj_rc_core::persist::BuiltInChatOperation::TotalUsers => Self::TotalUsers, + oj_rc_core::persist::BuiltInChatOperation::Stats => Self::Stats, oj_rc_core::persist::BuiltInChatOperation::Version => Self::Version, oj_rc_core::persist::BuiltInChatOperation::Help => Self::Help, } @@ -157,6 +159,53 @@ impl BuiltIn { Err(e) => e.error_msg().map(|x| x.to_owned()).unwrap_or_else(|| "Failed to retrieve registered users".to_owned()), } }, + Self::Stats => { + let mut stats = Vec::new(); + for variant in text.trim().split(' ').skip(1) { + match variant { + "db" | "database" => { + let db_stats = format!("(chat db) {}", ctx.user.db_metrics().await); + stats.push(db_stats); + }, + "perms" | "permission" | "permissions" => { + let com_stats = format!("(perms {}) mod:{} admin:{} dev:{} banned:{} royal:{}", + ctx.user.public_id(), + ctx.user.is_mod(), + ctx.user.is_admin(), + ctx.user.is_dev(), + ctx.user.is_banned(), + ctx.user.is_royal(), + ); + stats.push(com_stats); + }, + "up" | "uptime" => { + let now = chrono::Utc::now().timestamp(); + let startup_timestamp = crate::START_TIMESTAMP_S.load(std::sync::atomic::Ordering::Relaxed); + let uptime_delta = now - startup_timestamp; + let uptime_str = if uptime_delta <= 0 { + "0?".to_owned() + } else if uptime_delta < 60 { + format!("{}s", uptime_delta) + } else if uptime_delta < 24 * 60 * 60 { + format!("{}:{:02}", uptime_delta / (60 * 60), (uptime_delta % (60 * 60)) / 60) + } else { + format!("{} days {}:{:02}", uptime_delta / (24 * 60 * 60), (uptime_delta % (24 * 60 * 60)) / (60 * 60), ((uptime_delta % (24 * 60 * 60)) % (60 * 60)) / 60) + }; + let ready_ns = crate::READY_DURATION_NS.load(std::sync::atomic::Ordering::Relaxed); + let uptime_stats = format!("(uptime) {}, startup in {}ns", uptime_str, ready_ns); + stats.push(uptime_stats); + }, + idk => { + let stat = format!("(unknown stat) {}", idk); + stats.push(stat); + } + } + } + if stats.is_empty() { + stats.push(format!("TODO: general stats (try db)")); + } + stats.join("\n") + }, Self::Version => { let name = env!("CARGO_PKG_NAME"); let version = env!("CARGO_PKG_VERSION"); @@ -189,6 +238,7 @@ impl BuiltIn { Self::Intercom(i) => i.do_help(), Self::OnlineUsers => "Show total users online".to_owned(), Self::TotalUsers => "Show total users registered".to_owned(), + Self::Stats => "Show server metrics (db|perms)".to_owned(), Self::Version => "Show chat server version information".to_owned(), Self::Help => "Display this message".to_owned(), } diff --git a/rc_core/src/persist/chat.rs b/rc_core/src/persist/chat.rs index bf8c09a..7ffa3fd 100644 --- a/rc_core/src/persist/chat.rs +++ b/rc_core/src/persist/chat.rs @@ -117,6 +117,7 @@ pub enum BuiltInChatOperation { Intercom(IntercomChatOperation), OnlineUsers, TotalUsers, + Stats, Version, Help, } diff --git a/rc_core/src/persist/user/common.rs b/rc_core/src/persist/user/common.rs index 0fff64b..2351810 100644 --- a/rc_core/src/persist/user/common.rs +++ b/rc_core/src/persist/user/common.rs @@ -29,4 +29,8 @@ impl super::CommonUser for UserData { async fn resolve_config_vehicle(&self, vehicle: &crate::persist::config::VehicleInfo, factory: &dyn oj_rc_factory::VehicleFactoryAdapter, weapon_order: &crate::cubes::WeaponListParser, cpu_counter: &crate::cubes::CpuListParser) -> Result { self.resolve_vehicle(vehicle, factory, weapon_order, cpu_counter).await } + + async fn db_metrics(&self) -> oj_rc_database::DatabaseMetrics { + self.db.metrics().await + } } diff --git a/rc_core/src/persist/user/traits.rs b/rc_core/src/persist/user/traits.rs index 27c8224..fdb36b5 100644 --- a/rc_core/src/persist/user/traits.rs +++ b/rc_core/src/persist/user/traits.rs @@ -376,4 +376,5 @@ pub trait CommonUser: Send + Sync { fn is_dev(&self) -> bool; fn is_royal(&self) -> bool; fn is_banned(&self) -> bool; + async fn db_metrics(&self) -> oj_rc_database::DatabaseMetrics; } diff --git a/rc_database/src/lib.rs b/rc_database/src/lib.rs index df02b9c..d30f75c 100644 --- a/rc_database/src/lib.rs +++ b/rc_database/src/lib.rs @@ -6,4 +6,7 @@ pub mod schema; mod wrapper; pub use wrapper::Database; +mod metrics; +pub use metrics::DatabaseMetrics; + pub use sea_orm; diff --git a/rc_database/src/metrics.rs b/rc_database/src/metrics.rs new file mode 100644 index 0000000..1846dc0 --- /dev/null +++ b/rc_database/src/metrics.rs @@ -0,0 +1,91 @@ +pub(crate) struct MetricsState { + total_calls: u64, + total_duration_ns: u128, + total_failed: u64, + fastest: StatementInfo, + slowest: StatementInfo, +} + +#[derive(Debug)] +pub struct DatabaseMetrics { + pub avg_query_duration: std::time::Duration, + pub fail_queries: u64, + pub success_queries: u64, + pub slowest: StatementInfo, + pub fastest: StatementInfo, +} + +impl core::fmt::Display for DatabaseMetrics { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "avg: {}s success: {}, failed: {} worst: {} ({}s), best: {} ({}s)", + self.avg_query_duration.as_secs_f32(), + self.success_queries, + self.fail_queries, + self.slowest.statement, self.slowest.elapsed.as_secs_f32(), + self.fastest.statement, self.fastest.elapsed.as_secs_f32(), + ) + } +} + +#[derive(Debug, Clone)] +pub struct StatementInfo { + elapsed: std::time::Duration, + statement: String, +} + +impl StatementInfo { + fn fastest() -> Self { + Self { + elapsed: std::time::Duration::ZERO, + statement: "".to_owned(), + } + } + + fn slowest() -> Self { + Self { + elapsed: std::time::Duration::MAX, + statement: "".to_owned(), + } + } +} + +impl MetricsState { + pub(crate) fn new() -> Self { + Self { + total_calls: 0, + total_duration_ns: 0, + total_failed: 0, + slowest: StatementInfo::fastest(), + fastest: StatementInfo::slowest(), + } + } + + pub(crate) fn snapshot(&self) -> DatabaseMetrics { + DatabaseMetrics { + avg_query_duration: std::time::Duration::from_nanos((self.total_duration_ns / self.total_calls as u128) as u64), + fail_queries: self.total_failed, + success_queries: self.total_calls, + slowest: self.slowest.clone(), + fastest: self.fastest.clone(), + } + } +} + +pub(super) fn metrics_cb(state: std::sync::Arc>) -> impl Fn(&sea_orm::metric::Info) + Send + Sync + 'static { + move |info: &sea_orm::metric::Info| { + let mut lock = state.lock().unwrap(); + lock.total_calls += 1; + lock.total_duration_ns += info.elapsed.as_nanos(); + if info.failed { + lock.total_failed += 1; + } + if info.elapsed < lock.fastest.elapsed { + lock.fastest.elapsed = info.elapsed; + lock.fastest.statement = info.statement.to_string(); + } + if info.elapsed > lock.slowest.elapsed { + lock.slowest.elapsed = info.elapsed; + lock.slowest.statement = info.statement.to_string(); + } + } +} diff --git a/rc_database/src/wrapper.rs b/rc_database/src/wrapper.rs index 50c2a23..800297c 100644 --- a/rc_database/src/wrapper.rs +++ b/rc_database/src/wrapper.rs @@ -3,15 +3,19 @@ use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, PaginatorTrait, QueryF pub struct Database { orm: sea_orm::DatabaseConnection, + metrics: std::sync::Arc>, } impl Database { pub async fn init(uri: &str) -> Result{ - let db = sea_orm::Database::connect(uri).await?; + let mut db = sea_orm::Database::connect(uri).await?; + let metrics_data = std::sync::Arc::new(std::sync::Mutex::new(super::metrics::MetricsState::new())); + db.set_metric_callback(super::metrics::metrics_cb(metrics_data.clone())); //let schema_manager = SchemaManager::new(&db); super::Migrator::up(&db, None).await?; Ok(Self { orm: db, + metrics: metrics_data, }) } @@ -423,4 +427,8 @@ impl Database { pub async fn insert_game_event(&self, entity: crate::schema::game_event::ActiveModel) -> Result { entity.insert(&self.orm).await } + + pub async fn metrics(&self) -> super::DatabaseMetrics { + self.metrics.lock().unwrap().snapshot() + } }