diff --git a/src/admin/debug/commands.rs b/src/admin/debug/commands.rs index 53f4cf641278ba35bea30260f1575fc282739015..014ba79b56e80282ef48518e207244c2c76b60e3 100644 --- a/src/admin/debug/commands.rs +++ b/src/admin/debug/commands.rs @@ -828,3 +828,24 @@ pub(super) async fn list_dependencies(&self, names: bool) -> Result<RoomMessageE Ok(RoomMessageEventContent::notice_markdown(out)) } + +#[admin_command] +pub(super) async fn database_stats( + &self, property: Option<String>, map: Option<String>, +) -> Result<RoomMessageEventContent> { + let property = property.unwrap_or_else(|| "rocksdb.stats".to_owned()); + let map_name = map.as_ref().map_or(utils::string::EMPTY, String::as_str); + + let mut out = String::new(); + for (name, map) in self.services.db.iter_maps() { + if !map_name.is_empty() && *map_name != *name { + continue; + } + + let res = map.property(&property)?; + let res = res.trim(); + writeln!(out, "##### {name}:\n```\n{res}\n```")?; + } + + Ok(RoomMessageEventContent::notice_markdown(out)) +} diff --git a/src/admin/debug/mod.rs b/src/admin/debug/mod.rs index fbe6fd264180a115ee67b9205b20cbce824eb5b3..1f51a35e4f08a7dbf6178952c2094f067d1a60ff 100644 --- a/src/admin/debug/mod.rs +++ b/src/admin/debug/mod.rs @@ -184,6 +184,14 @@ pub(super) enum DebugCommand { names: bool, }, + /// - Get database statistics + DatabaseStats { + property: Option<String>, + + #[arg(short, long, alias("column"))] + map: Option<String>, + }, + /// - Developer test stubs #[command(subcommand)] #[allow(non_snake_case)] diff --git a/src/database/engine.rs b/src/database/engine.rs index bf172551f21753d4d5759f294f05c4bf867ad016..3975d3d9a7dac7966b72c0623231d0fefa97e01a 100644 --- a/src/database/engine.rs +++ b/src/database/engine.rs @@ -1,5 +1,6 @@ use std::{ collections::{BTreeSet, HashMap}, + ffi::CStr, fmt::Write, path::PathBuf, sync::{atomic::AtomicU32, Arc, Mutex, RwLock}, @@ -9,7 +10,8 @@ use rocksdb::{ backup::{BackupEngine, BackupEngineOptions}, perf::get_memory_usage_stats, - BoundColumnFamily, Cache, ColumnFamilyDescriptor, DBCommon, DBWithThreadMode, Env, MultiThreaded, Options, + AsColumnFamilyRef, BoundColumnFamily, Cache, ColumnFamilyDescriptor, DBCommon, DBWithThreadMode, Env, + MultiThreaded, Options, }; use crate::{ @@ -240,6 +242,20 @@ pub fn file_list(&self) -> Result<String> { }, } } + + /// Query for database property by null-terminated name which is expected to + /// have a result with an integer representation. This is intended for + /// low-overhead programmatic use. + pub(crate) fn property_integer(&self, cf: &impl AsColumnFamilyRef, name: &CStr) -> Result<u64> { + result(self.db.property_int_value_cf(cf, name)) + .and_then(|val| val.map_or_else(|| Err!("Property {name:?} not found."), Ok)) + } + + /// Query for database property by name receiving the result in a string. + pub(crate) fn property(&self, cf: &impl AsColumnFamilyRef, name: &str) -> Result<String> { + result(self.db.property_value_cf(cf, name)) + .and_then(|val| val.map_or_else(|| Err!("Property {name:?} not found."), Ok)) + } } pub(crate) fn repair(db_opts: &Options, path: &PathBuf) -> Result<()> { diff --git a/src/database/map.rs b/src/database/map.rs index 1b35a72aa04c99e19de3a21dd7952e6327dd1faa..ddae8c8136f60cb3b0c0e9d02262c22cd240c092 100644 --- a/src/database/map.rs +++ b/src/database/map.rs @@ -1,4 +1,4 @@ -use std::{future::Future, mem::size_of, pin::Pin, sync::Arc}; +use std::{ffi::CStr, future::Future, mem::size_of, pin::Pin, sync::Arc}; use conduit::{utils, Result}; use rocksdb::{ @@ -189,6 +189,10 @@ pub fn watch_prefix<'a>(&'a self, prefix: &Key) -> Pin<Box<dyn Future<Output = ( self.watchers.watch(prefix) } + pub fn property_integer(&self, name: &CStr) -> Result<u64> { self.db.property_integer(&self.cf(), name) } + + pub fn property(&self, name: &str) -> Result<String> { self.db.property(&self.cf(), name) } + #[inline] pub fn name(&self) -> &str { &self.name }