diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index de2330407..ed41aa737 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -404,9 +404,14 @@ If the prompt adds a new security feature, hardens an existing one, or fixes a s ### Test vectors -For every feature added or bug fixed, a test vector **must** be added to `test_data/vectors/`. -This is not optional — it is enforced by the mandatory checklist above. - +For every feature added or bug fixed, a test vector **must** be added to `test_data/vectors/` **unless** one the following conditions is met: +- The behavior being tested is not observable in a KMIP HTTP response (e.g., metric emission, in-process state changes). +- The new vector would exercise the same KMIP operations already covered by existing vectors, + just with a new name to "document" a feature, without catching a regression that no + other vector would catch. +- The vector will always pass regardless of weather the new feature works. + +If none of these apply, the vector **must** be added as follows : 1. Model the vector on existing examples in `test_data/vectors/`. 2. Register the corresponding test function in `crate/test_kms_server/src/vector_runner.rs`. 3. Run the test and confirm it passes: `cargo test -p test_kms_server `. diff --git a/CHANGELOG/feat_richOTELmetrics.md b/CHANGELOG/feat_richOTELmetrics.md new file mode 100644 index 000000000..16625f25a --- /dev/null +++ b/CHANGELOG/feat_richOTELmetrics.md @@ -0,0 +1,16 @@ +# Changelog — feat/richOTELmetrics + +## Features + +### Database Metrics Wiring (Step 1) + +- Wire `kms.database.operations.total` (counter) and `kms.database.operation.duration` + (histogram) at the `Database` facade layer with `operation`, `backend`, and `outcome` + attributes, covering all 11 object-store and 5 permission-store methods. +- Introduce `DbMetricsRecorder` trait in `cosmian_kms_server_database` for + dependency-inversion: the `server_database` crate emits metrics via a trait object + without depending on `cosmian_kms_server` (avoids crate cycle). +- `OtelMetrics` in `cosmian_kms_server` implements `DbMetricsRecorder`; the recorder + `Arc` is injected into `Database::instantiate` at KMS startup. +- `MainDbKind::as_str()` provides canonical backend labels + (`"sqlite"`, `"postgresql"`, `"mysql"`, `"redis"`). \ No newline at end of file diff --git a/crate/server/src/core/kms/mod.rs b/crate/server/src/core/kms/mod.rs index 30200962b..f5901ca88 100644 --- a/crate/server/src/core/kms/mod.rs +++ b/crate/server/src/core/kms/mod.rs @@ -7,7 +7,7 @@ mod permissions; use std::{collections::HashMap, sync::Arc}; use cosmian_kms_server_database::{ - Database, + Database, DbMetricsRecorder, reexport::cosmian_kms_interfaces::{CryptoOracle, HSM, HsmStore, ObjectsStore}, }; use cosmian_logger::trace; @@ -139,11 +139,19 @@ impl KMS { let main_db_params = server_params.main_db_params.as_ref().ok_or_else(|| { KmsError::InvalidRequest("The main database parameters are not specified".to_owned()) })?; + + let metrics = Self::create_otel_metrics(&server_params)?; + let db_otel_recorder: Option> = + metrics.as_ref().map(|m| -> Arc { + m.clone() // Arc clones are cheap + }); + let database = Database::instantiate( main_db_params, server_params.clear_db_on_start, object_stores, server_params.unwrapped_cache_max_age, + db_otel_recorder, ) .await?; @@ -153,7 +161,7 @@ impl KMS { crypto_oracles: RwLock::new(crypto_oracles), // Keep a reference to the first HSM for PKCS#11 C_Initialize / C_GetInfo operations. hsm: hsm_instances.into_iter().next(), - metrics: Self::create_otel_metrics(&server_params)?, + metrics, }) } diff --git a/crate/server/src/core/otel_metrics.rs b/crate/server/src/core/otel_metrics.rs index 19c2f36f1..a7d3ae007 100644 --- a/crate/server/src/core/otel_metrics.rs +++ b/crate/server/src/core/otel_metrics.rs @@ -16,6 +16,7 @@ use std::{ sync::{Arc, RwLock}, }; +use cosmian_kms_server_database::{DbMetricsRecorder, MainDbKind}; use opentelemetry::{ KeyValue, metrics::{Counter, Histogram, Meter, MeterProvider, UpDownCounter}, @@ -352,17 +353,37 @@ impl OtelMetrics { } } - /// Record a database operation - pub fn record_database_operation(&self, operation: &str) { - self.database_operations_total - .add(1, &[KeyValue::new("operation", operation.to_owned())]); - } - - /// Record database operation duration - pub fn record_database_operation_duration(&self, operation: &str, duration_seconds: f64) { + /// Record a database operation (count + duration in one call). + /// + /// # Arguments + /// * `operation` – low-cardinality label (`"create"`, `"retrieve"`, …) + /// * `backend` – typed database backend; `as_str()` is called here so + /// no free-form string can sneak in through this method. + /// * `outcome` – `"success"` or `"error"` + /// * `duration_seconds` – wall-clock duration of the operation + pub fn record_database_operation( + &self, + operation: &str, + backend: MainDbKind, + outcome: &str, + duration_seconds: f64, + ) { + let backend_str = backend.as_str(); + self.database_operations_total.add( + 1, + &[ + KeyValue::new("operation", operation.to_owned()), + KeyValue::new("backend", backend_str), + KeyValue::new("outcome", outcome.to_owned()), + ], + ); self.database_operation_duration.record( duration_seconds, - &[KeyValue::new("operation", operation.to_owned())], + &[ + KeyValue::new("operation", operation.to_owned()), + KeyValue::new("backend", backend_str), + KeyValue::new("outcome", outcome.to_owned()), + ], ); } @@ -469,6 +490,18 @@ impl OtelMetrics { } } +impl DbMetricsRecorder for OtelMetrics { + fn record_operation( + &self, + operation: &str, + backend: MainDbKind, + outcome: &str, + duration_seconds: f64, + ) { + self.record_database_operation(operation, backend, outcome, duration_seconds); + } +} + #[cfg(test)] #[allow( clippy::expect_used, @@ -538,6 +571,6 @@ mod tests { let metrics = OtelMetrics::new(meter_provider).expect("Failed to create metrics"); metrics.record_kmip_operation_duration("Create", 0.123); - metrics.record_database_operation_duration("insert", 0.045); + metrics.record_database_operation("insert", MainDbKind::Sqlite, "success", 0.045); } } diff --git a/crate/server_database/src/core/database_objects.rs b/crate/server_database/src/core/database_objects.rs index 1f9c82e0f..90765cca7 100644 --- a/crate/server_database/src/core/database_objects.rs +++ b/crate/server_database/src/core/database_objects.rs @@ -1,6 +1,8 @@ use std::{ collections::{HashMap, HashSet}, + future::Future, sync::Arc, + time::Instant, }; use cosmian_kmip::{ @@ -124,7 +126,34 @@ impl Database { .ok_or_else(|| DbError::InvalidRequest("No default object store available".to_owned())) .map(Arc::clone) } - + /// Centralises metrics instrumentation boilerplate so that public methods + /// stay focused on their core logic. + /// + /// Accepts a future representing the database operation (not yet polled), + /// awaits it, then records the operation name, backend, outcome, and elapsed + /// duration to the injected [`DbMetricsRecorder`] (if any). + /// + /// # Important + /// + /// Every new operation added to the `Database` facade must be wrapped with + /// this method to be accounted for by the metrics recorder. + async fn record( + &self, + operation: &str, + fut: impl Future>, + ) -> DbResult { + let start = Instant::now(); + let result = fut.await; + if let Some(ref rec) = self.recorder { + rec.record_operation( + operation, + self.kind, + if result.is_ok() { "success" } else { "error" }, + start.elapsed().as_secs_f64(), + ); + } + result + } /// Create the given Object in the database. /// A new UUID will be created if none is supplier. /// This method will fail if an ` uid ` is supplied @@ -153,12 +182,14 @@ impl Database { attributes: &Attributes, tags: &HashSet, ) -> DbResult { - let db = self - .get_object_store(uid.as_deref().unwrap_or_default()) - .await?; - let uid = db.create(uid, owner, object, attributes, tags).await?; - // New objects never have a cache entry; nothing to invalidate. - Ok(uid) + self.record("create", async move { + let db = self + .get_object_store(uid.as_deref().unwrap_or_default()) + .await?; + // New objects never have a cache entry; nothing to invalidate. + Ok(db.create(uid, owner, object, attributes, tags).await?) + }) + .await } /// Retrieve objects from the database. @@ -188,21 +219,24 @@ impl Database { &self, uid_or_tags: &str, ) -> DbResult> { - let uids = if uid_or_tags.starts_with('[') { - // tags - let tags: HashSet = serde_json::from_str(uid_or_tags)?; - self.list_uids_for_tags(&tags).await? - } else { - HashSet::from([uid_or_tags.to_owned()]) - }; - let mut results: HashMap = HashMap::new(); - for uid in &uids { - let owm = self.retrieve_object(uid).await?; - if let Some(owm) = owm { - results.insert(uid.to_owned(), owm); + self.record("retrieve_objects", async move { + let uids = if uid_or_tags.starts_with('[') { + // tags + let tags: HashSet = serde_json::from_str(uid_or_tags)?; + self.list_uids_for_tags(&tags).await? + } else { + HashSet::from([uid_or_tags.to_owned()]) + }; + let mut results: HashMap = HashMap::new(); + for uid in &uids { + let owm = self.retrieve_object(uid).await?; + if let Some(owm) = owm { + results.insert(uid.to_owned(), owm); + } } - } - Ok(results) + Ok(results) + }) + .await } /// Retrieve a single object from the database. @@ -224,15 +258,20 @@ impl Database { /// If the object is found and passes the filters, it is returned wrapped in `Some`. /// If the object is not found or does not pass the filters, `None` is returned. pub async fn retrieve_object(&self, uid: &str) -> DbResult> { - // retrieve the object - let db = self.get_object_store(uid).await?; - Ok(db.retrieve(uid).await?) + self.record("retrieve", async move { + let db = self.get_object_store(uid).await?; + Ok(db.retrieve(uid).await?) + }) + .await } /// Retrieve the tags of the object with the given `uid` pub async fn retrieve_tags(&self, uid: &str) -> DbResult> { - let db = self.get_object_store(uid).await?; - Ok(db.retrieve_tags(uid).await?) + self.record("retrieve_tags", async move { + let db = self.get_object_store(uid).await?; + Ok(db.retrieve_tags(uid).await?) + }) + .await } /// This method updates the specified object identified by its `uid` in the database. @@ -261,40 +300,55 @@ impl Database { attributes: &Attributes, tags: Option<&HashSet>, ) -> DbResult<()> { - let db = self.get_object_store(uid).await?; - db.update_object(uid, object, attributes, tags).await?; - // Key material is immutable; only attributes change via update_object. - // The GC clears stale unwrap-cache entries; no eager invalidation needed here. - Ok(()) + self.record("update_object", async move { + let db = self.get_object_store(uid).await?; + db.update_object(uid, object, attributes, tags).await?; + // Key material is immutable; only attributes change via update_object. + // The GC clears stale unwrap-cache entries; no eager invalidation needed here. + Ok(()) + }) + .await } /// Update the state of an object in the database. pub async fn update_state(&self, uid: &str, state: State) -> DbResult<()> { - let db = self.get_object_store(uid).await?; - Ok(db.update_state(uid, state).await?) + self.record("update_state", async move { + let db = self.get_object_store(uid).await?; + Ok(db.update_state(uid, state).await?) + }) + .await } /// Delete an object from the database. pub async fn delete(&self, uid: &str) -> DbResult<()> { - let db = self.get_object_store(uid).await?; - db.delete(uid).await?; - self.unwrapped_cache.clear_cache(uid).await; - Ok(()) + self.record("delete", async move { + let db = self.get_object_store(uid).await?; + db.delete(uid).await?; + self.unwrapped_cache.clear_cache(uid).await; + Ok(()) + }) + .await } /// Test if an object identified by its `uid` is currently owned by `owner` pub async fn is_object_owned_by(&self, uid: &str, owner: &str) -> DbResult { - let db = self.get_object_store(uid).await?; - Ok(db.is_object_owned_by(uid, owner).await?) + self.record("is_object_owned_by", async move { + let db = self.get_object_store(uid).await?; + Ok(db.is_object_owned_by(uid, owner).await?) + }) + .await } pub async fn list_uids_for_tags(&self, tags: &HashSet) -> DbResult> { - let db_map = self.objects.read().await; - let mut results = HashSet::new(); - for db in db_map.values() { - results.extend(db.list_uids_for_tags(tags).await?); - } - Ok(results) + self.record("list_uids_for_tags", async move { + let db_map = self.objects.read().await; + let mut results = HashSet::new(); + for db in db_map.values() { + results.extend(db.list_uids_for_tags(tags).await?); + } + Ok(results) + }) + .await } /// Return uid, state and attributes of the object identified by its owner, @@ -307,22 +361,25 @@ impl Database { user_must_be_owner: bool, vendor_id: &str, ) -> DbResult> { - let map = self.objects.read().await; - let mut results: Vec<(String, State, Attributes)> = Vec::new(); - for db in map.values() { - results.extend( - db.find( - researched_attributes, - state, - user, - user_must_be_owner, - vendor_id, - ) - .await - .unwrap_or(vec![]), - ); - } - Ok(results) + self.record("find", async move { + let map = self.objects.read().await; + let mut results: Vec<(String, State, Attributes)> = Vec::new(); + for db in map.values() { + results.extend( + db.find( + researched_attributes, + state, + user, + user_must_be_owner, + vendor_id, + ) + .await + .unwrap_or(vec![]), + ); + } + Ok(results) + }) + .await } /// Perform an atomic set of operations on the database. @@ -352,38 +409,45 @@ impl Database { if operations.is_empty() { return Ok(vec![]); } - #[expect(clippy::indexing_slicing)] - let first_op = &operations[0]; - let first_uid = first_op.get_object_uid(); - let db = self.get_object_store(first_uid).await?; - let ids = db.atomic(user, operations).await?; - // invalidate of clear cache for all operations - for op in operations { - match op { - AtomicOperation::Create((uid, object, ..)) - | AtomicOperation::UpdateObject((uid, object, ..)) - | AtomicOperation::Upsert((uid, object, ..)) => { - self.unwrapped_cache.validate_cache(uid, object).await?; + self.record("atomic", async move { + #[expect(clippy::indexing_slicing)] + let first_op = &operations[0]; + let first_uid = first_op.get_object_uid(); + let db = self.get_object_store(first_uid).await?; + let ids = db.atomic(user, operations).await?; + // invalidate or clear cache for all operations + for op in operations { + match op { + AtomicOperation::Create((uid, object, ..)) + | AtomicOperation::UpdateObject((uid, object, ..)) + | AtomicOperation::Upsert((uid, object, ..)) => { + self.unwrapped_cache.validate_cache(uid, object).await?; + } + AtomicOperation::Delete(uid) => { + self.unwrapped_cache.clear_cache(uid).await; + } + AtomicOperation::UpdateState(_) => {} } - AtomicOperation::Delete(uid) => { - self.unwrapped_cache.clear_cache(uid).await; - } - AtomicOperation::UpdateState(_) => {} } - } - Ok(ids) + Ok(ids) + }) + .await } } #[cfg(test)] #[expect(clippy::expect_used, clippy::panic)] mod tests { - use std::{collections::HashMap, time::Duration}; + use std::{ + collections::HashMap, + sync::{Arc, Mutex}, + time::Duration, + }; use tempfile::TempDir; use super::Database; - use crate::core::MainDbParams; + use crate::core::{DbMetricsRecorder, MainDbKind, MainDbParams}; /// Verify that a UID with an HSM prefix is rejected when no HSM store is registered. #[tokio::test] @@ -394,6 +458,7 @@ mod tests { false, HashMap::new(), // no HSM stores registered Duration::from_secs(1), + None, ) .await .expect("Failed to instantiate in-memory database"); @@ -410,4 +475,64 @@ mod tests { } } } + + /// Verify that the `DbMetricsRecorder` injected into `Database` is called when + /// DB facade methods are invoked. + /// + /// Uses a thread-safe mock recorder that collects every `(operation, backend, outcome)` + /// triple so the test can assert that the instrumentation fires as expected. + #[tokio::test] + async fn test_db_recorder_called_on_operations() { + /// Minimal mock that records every call in a shared Vec. + #[derive(Clone, Default)] + struct MockRecorder { + calls: Arc>>, + } + impl DbMetricsRecorder for MockRecorder { + fn record_operation( + &self, + operation: &str, + backend: MainDbKind, + outcome: &str, + _duration_seconds: f64, + ) { + self.calls.lock().expect("mutex poisoned").push(( + operation.to_owned(), + backend.as_str().to_owned(), + outcome.to_owned(), + )); + } + } + + let tmp = TempDir::new().expect("Failed to create temp dir"); + let recorder = MockRecorder::default(); + let calls = Arc::clone(&recorder.calls); + let recorder_arc: Arc = Arc::new(recorder); + + let db = Database::instantiate( + &MainDbParams::Sqlite(tmp.path().to_path_buf(), None), + false, + HashMap::new(), + Duration::from_secs(1), + Some(recorder_arc), + ) + .await + .expect("Failed to instantiate database with mock recorder"); + + // list_user_operations_granted: exercises the permissions facade path. + drop(db.list_user_operations_granted("test_user").await); + + let recorded = calls.lock().expect("mutex poisoned").clone(); + assert!( + !recorded.is_empty(), + "Expected the mock recorder to be called but got zero calls" + ); + // Every recorded call must use "sqlite" as the backend. + for (op, backend, _outcome) in &recorded { + assert_eq!( + backend, "sqlite", + "Expected backend 'sqlite' for operation '{op}', got '{backend}'" + ); + } + } } diff --git a/crate/server_database/src/core/database_permissions.rs b/crate/server_database/src/core/database_permissions.rs index 1c877b737..af3031d12 100644 --- a/crate/server_database/src/core/database_permissions.rs +++ b/crate/server_database/src/core/database_permissions.rs @@ -1,4 +1,7 @@ -use std::collections::{HashMap, HashSet}; +use std::{ + collections::{HashMap, HashSet}, + time::Instant, +}; use cosmian_kmip::{kmip_0::kmip_types::State, kmip_2_1::KmipOperation}; @@ -16,7 +19,17 @@ impl Database { &self, user: &str, ) -> DbResult)>> { - Ok(self.permissions.list_user_operations_granted(user).await?) + let start = Instant::now(); + let result = self.permissions.list_user_operations_granted(user).await; + if let Some(ref rec) = self.recorder { + rec.record_operation( + "list_user_ops_granted", + self.kind, + if result.is_ok() { "success" } else { "error" }, + start.elapsed().as_secs_f64(), + ); + } + Ok(result?) } /// List all the KMIP operations granted per `user` on the given object @@ -25,7 +38,17 @@ impl Database { &self, uid: &str, ) -> DbResult>> { - Ok(self.permissions.list_object_operations_granted(uid).await?) + let start = Instant::now(); + let result = self.permissions.list_object_operations_granted(uid).await; + if let Some(ref rec) = self.recorder { + rec.record_operation( + "list_object_ops_granted", + self.kind, + if result.is_ok() { "success" } else { "error" }, + start.elapsed().as_secs_f64(), + ); + } + Ok(result?) } /// Grant the ability to `user` to perform the KMIP `operations` @@ -36,10 +59,20 @@ impl Database { user: &str, operations: HashSet, ) -> DbResult<()> { - Ok(self + let start = Instant::now(); + let result = self .permissions .grant_operations(uid, user, operations) - .await?) + .await; + if let Some(ref rec) = self.recorder { + rec.record_operation( + "grant_ops", + self.kind, + if result.is_ok() { "success" } else { "error" }, + start.elapsed().as_secs_f64(), + ); + } + Ok(result?) } /// Remove the ability to `user` to perform the `operations` @@ -50,10 +83,20 @@ impl Database { user: &str, operations: HashSet, ) -> DbResult<()> { - Ok(self + let start = Instant::now(); + let result = self .permissions .remove_operations(uid, user, operations) - .await?) + .await; + if let Some(ref rec) = self.recorder { + rec.record_operation( + "remove_ops", + self.kind, + if result.is_ok() { "success" } else { "error" }, + start.elapsed().as_secs_f64(), + ); + } + Ok(result?) } /// List all the operations that have been granted to a user on an object @@ -66,9 +109,19 @@ impl Database { user: &str, no_inherited_access: bool, ) -> DbResult> { - Ok(self + let start = Instant::now(); + let result = self .permissions .list_user_operations_on_object(uid, user, no_inherited_access) - .await?) + .await; + if let Some(ref rec) = self.recorder { + rec.record_operation( + "list_user_ops_on_object", + self.kind, + if result.is_ok() { "success" } else { "error" }, + start.elapsed().as_secs_f64(), + ); + } + Ok(result?) } } diff --git a/crate/server_database/src/core/db_metrics.rs b/crate/server_database/src/core/db_metrics.rs new file mode 100644 index 000000000..03294d449 --- /dev/null +++ b/crate/server_database/src/core/db_metrics.rs @@ -0,0 +1,39 @@ +//! Recorder trait for database operation metrics. +//! +//! This module defines [`DbMetricsRecorder`], the only interface between +//! `cosmian_kms_server_database` and the OTEL layer. The trait is +//! implemented by the server crate's `OtelMetrics` type and injected into +//! [`super::Database`] at construction time, keeping the dependency arrow +//! strictly `server → server_database → interfaces` with no reverse edge. + +use super::MainDbKind; + +/// Observer interface for recording database operation metrics. +/// +/// Implementors are expected to be cheap to clone (typically wrapping an +/// `Arc`) and to forward measurements to an OpenTelemetry counter and +/// histogram. The call happens **after** each facade method returns, so +/// the `outcome` is always known at the call site. +pub trait DbMetricsRecorder: Send + Sync { + /// Record a completed database operation. + /// + /// # Arguments + /// * `operation` – low-cardinality label: `"create"`, `"retrieve"`, + /// `"retrieve_tags"`, `"update_object"`, `"update_state"`, `"delete"`, + /// `"find"`, `"atomic"`, `"list_uids_for_tags"`, `"is_object_owned_by"`, + /// `"list_user_ops_granted"`, `"list_object_ops_granted"`, + /// `"grant_ops"`, `"remove_ops"`, `"list_user_ops_on_object"` + /// * `backend` – typed backend identifier; `as_str()` is called + /// inside the implementor so callers cannot pass an arbitrary string. + /// Adding a new [`MainDbKind`] variant is a compile error unless + /// `MainDbKind::as_str` is updated, which keeps the label set in sync. + /// * `outcome` – `"success"` or `"error"` + /// * `duration_seconds` – wall-clock duration of the operation in seconds + fn record_operation( + &self, + operation: &str, + backend: MainDbKind, + outcome: &str, + duration_seconds: f64, + ); +} diff --git a/crate/server_database/src/core/mod.rs b/crate/server_database/src/core/mod.rs index b4c1b7a70..004765503 100644 --- a/crate/server_database/src/core/mod.rs +++ b/crate/server_database/src/core/mod.rs @@ -2,6 +2,8 @@ //! permission checks, and caching mechanisms for unwrapped keys. mod database_objects; mod database_permissions; +mod db_metrics; +pub use db_metrics::DbMetricsRecorder; use std::{collections::HashMap, sync::Arc, time::Duration}; @@ -43,6 +45,13 @@ pub struct Database { /// /// This enables server-side `/health` checks without exposing internal store types. health: Arc, + + /// Optional OTEL metrics recorder injected at construction time. + /// + /// When `None`, all metric recording is skipped without any overhead. + /// The concrete implementation lives in the `server` crate to avoid a + /// dependency cycle. + recorder: Option>, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -54,6 +63,19 @@ pub enum MainDbKind { RedisFindex, } +impl MainDbKind { + #[must_use] + pub const fn as_str(self) -> &'static str { + match self { + Self::Sqlite => "sqlite", + Self::Postgres => "postgresql", + Self::Mysql => "mysql", + #[cfg(feature = "non-fips")] + Self::RedisFindex => "redis", + } + } +} + #[async_trait] trait DatabaseHealth { async fn check(&self) -> Result<(), String>; @@ -75,10 +97,13 @@ impl Database { clear_db_on_start: bool, object_stores: HashMap>, cache_max_age: Duration, + recorder: Option>, ) -> DbResult { // main/default database - let db = Self::instantiate_main_database(main_db_params, clear_db_on_start, cache_max_age) - .await?; + let mut db = + Self::instantiate_main_database(main_db_params, clear_db_on_start, cache_max_age) + .await?; + db.recorder = recorder; for (prefix, store) in object_stores { db.register_objects_store(&prefix, store).await; } @@ -190,6 +215,7 @@ impl Database { unwrapped_cache: UnwrappedCache::new(cache_max_age), kind, health, + recorder: None, } } diff --git a/crate/server_database/src/core/unwrapped_cache.rs b/crate/server_database/src/core/unwrapped_cache.rs index cc22a5bfe..fcd04946b 100644 --- a/crate/server_database/src/core/unwrapped_cache.rs +++ b/crate/server_database/src/core/unwrapped_cache.rs @@ -301,7 +301,6 @@ mod tests { #[tokio::test] async fn test_lru_cache() -> DbResult<()> { - // log_init(Some("debug")); log_init(option_env!("RUST_LOG")); let dir = TempDir::new()?; @@ -312,6 +311,7 @@ mod tests { true, HashMap::new(), Duration::from_millis(100), + None, ) .await?; diff --git a/crate/server_database/src/lib.rs b/crate/server_database/src/lib.rs index 9b271fdcc..085c54597 100644 --- a/crate/server_database/src/lib.rs +++ b/crate/server_database/src/lib.rs @@ -36,7 +36,8 @@ mod core; pub use core::{ - AdditionalObjectStoresParams, CachedObject, Database, MainDbKind, MainDbParams, UnwrappedCache, + AdditionalObjectStoresParams, CachedObject, Database, DbMetricsRecorder, MainDbKind, + MainDbParams, UnwrappedCache, }; mod error; pub use error::DbError;