From b6ba332c011bb8626b4e9e3c150e15433f3d09cd Mon Sep 17 00:00:00 2001 From: HatemMn <19950216+HatemMn@users.noreply.github.com> Date: Fri, 29 May 2026 13:25:48 +0200 Subject: [PATCH 01/28] feat: db metrics fix: fix some useless test vector generator --- CHANGELOG/feat_richOTELmetrics.md | 16 + crate/server/src/core/kms/mod.rs | 12 +- crate/server/src/core/otel_metrics.rs | 53 +++- .../src/core/database_objects.rs | 287 +++++++++++++----- .../src/core/database_permissions.rs | 71 ++++- crate/server_database/src/core/db_metrics.rs | 39 +++ crate/server_database/src/core/mod.rs | 30 +- .../src/core/unwrapped_cache.rs | 2 +- crate/server_database/src/lib.rs | 3 +- 9 files changed, 407 insertions(+), 106 deletions(-) create mode 100644 CHANGELOG/feat_richOTELmetrics.md create mode 100644 crate/server_database/src/core/db_metrics.rs diff --git a/CHANGELOG/feat_richOTELmetrics.md b/CHANGELOG/feat_richOTELmetrics.md new file mode 100644 index 0000000000..16625f25aa --- /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 30200962b4..f5901ca88b 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 19c2f36f19..a7d3ae007f 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 1f9c82e0f1..90765cca7a 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 1c877b7373..af3031d125 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 0000000000..03294d449f --- /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 b4c1b7a705..0047655031 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 cc22a5bfee..fcd04946b0 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 9b271fdccf..085c54597a 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; From 0d33889663edf32be17900b43b3385b7bdc36dbe Mon Sep 17 00:00:00 2001 From: HatemMn <19950216+HatemMn@users.noreply.github.com> Date: Fri, 29 May 2026 15:54:50 +0200 Subject: [PATCH 02/28] feat: http middleware feat: first try with object count --- CHANGELOG/feat_richOTELmetrics.md | 44 ++- crate/interfaces/src/stores/objects_store.rs | 23 ++ crate/server/src/core/kms/mod.rs | 22 ++ crate/server/src/core/otel_metrics.rs | 49 ++++ crate/server/src/cron.rs | 34 ++- crate/server/src/middlewares/mod.rs | 3 + .../src/middlewares/otel_http_middleware.rs | 261 ++++++++++++++++++ crate/server/src/start_kms_server.rs | 3 +- .../src/core/database_objects.rs | 161 ++++++++--- crate/server_database/src/core/db_metrics.rs | 16 ++ .../src/stores/redis/redis_with_findex.rs | 12 + crate/server_database/src/stores/sql/mysql.rs | 24 ++ crate/server_database/src/stores/sql/pgsql.rs | 22 ++ .../server_database/src/stores/sql/query.sql | 10 + .../src/stores/sql/query_mysql.sql | 10 + .../server_database/src/stores/sql/sqlite.rs | 22 ++ 16 files changed, 679 insertions(+), 37 deletions(-) create mode 100644 crate/server/src/middlewares/otel_http_middleware.rs diff --git a/CHANGELOG/feat_richOTELmetrics.md b/CHANGELOG/feat_richOTELmetrics.md index 16625f25aa..1578db83cf 100644 --- a/CHANGELOG/feat_richOTELmetrics.md +++ b/CHANGELOG/feat_richOTELmetrics.md @@ -13,4 +13,46 @@ - `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 + (`"sqlite"`, `"postgresql"`, `"mysql"`, `"redis"`). + +### HTTP Metrics Wiring (Step 2) + +- Add `OtelHttpMetrics` Actix-web middleware (`crate/server/src/middlewares/otel_http_middleware.rs`) + that records `kms.http.requests.total` (counter with `method`, `path`, `status` + attributes), `kms.http.request.duration` (histogram with `method`, `path`), and + `kms.active.connections` (in-flight up-down counter) for every HTTP request. +- Middleware is installed as the outermost `App`-level wrap to measure true + client-perceived latency including all inner middleware. +- `normalize_path()` maps raw request paths to low-cardinality labels + (e.g. `/ui/assets/index-Ab3Cd.js` → `/ui/{static}`) to prevent cardinality + explosion from hashed asset filenames and per-UID path segments. +- Zero overhead when OTLP is not configured: `Option>` is `None` + and no allocation occurs per request. + +### Object Count Metric — `kms.objects.total` (Step 3) + +- Add `ObjectsStore::count_all_non_destroyed(&self) -> InterfaceResult` default + method to the `ObjectsStore` trait. The default returns `Ok(0)` so existing backends + compile without change; concrete implementations override it. +- Implement `count_all_non_destroyed` for SQLite, PostgreSQL, and MySQL via named SQL + queries (`count-non-destroyed-objects`) that scan the full `objects` table without + user/permission filters — a privileged metrics-only operation. +- Add `TODO(redis)` comment in `RedisWithFindex`; Redis remains on the default `Ok(0)` + pending a `SCAN do::*`-based implementation. +- Add `DbMetricsRecorder::record_object_delta(delta: i64)` method to the recorder trait, + allowing the database facade to push incremental object-count changes to OTEL without + a crate-cycle dependency. +- Wire `record_object_delta` in the `Database` facade: + - `+1` on successful `create()` + - `-1` on successful `delete()` + - Pre-computed delta on `atomic()` with Upsert pre-read to distinguish insert vs update; + TOCTOU drift is corrected by the periodic absolute sync. +- Add `OtelMetrics::objects_total_mirror: Arc>` and + `update_objects_total(absolute: i64)` using the mirror pattern (same as + `update_active_keys_count`) to simulate a gauge on `UpDownCounter`. +- Seed `kms.objects.total` at server startup from `count_all_non_destroyed_objects()` + so the gauge is correct from second 0, not after the first cron tick. +- Add 30-second periodic absolute sync in `cron.rs` alongside the active-keys refresh; + corrects any drift from TOCTOU races or Redis backends. +- Add `Database::count_all_non_destroyed_objects()` facade that sums counts across all + registered stores with `saturating_add`, tolerating partial failures. \ No newline at end of file diff --git a/crate/interfaces/src/stores/objects_store.rs b/crate/interfaces/src/stores/objects_store.rs index 7f27d27353..7dd70b1132 100644 --- a/crate/interfaces/src/stores/objects_store.rs +++ b/crate/interfaces/src/stores/objects_store.rs @@ -102,4 +102,27 @@ pub trait ObjectsStore { user_must_be_owner: bool, vendor_id: &str, ) -> InterfaceResult>; + + /// Count all objects that are **not** in a terminal (destroyed) state. + /// + /// # Purpose — metrics only + /// + /// This method is called exclusively by the OTEL metrics layer to feed the + /// `kms.objects.total` gauge. It deliberately skips all user/permission + /// filters so the result reflects the true server-wide object inventory, + /// not just the subset visible to a particular caller. + /// + /// **Never expose the result to client requests** — it bypasses access control. + /// + /// # Why a default of `Ok(0)`? + /// + /// Adding a required method to this trait would force every backend + /// (SQL, Redis, HSM stubs) to implement it in the same commit. The default + /// lets backends compile immediately; each one should replace it with a + /// real implementation when ready. A `TODO` comment is added at each + /// call site that still uses the default. + async fn count_all_non_destroyed(&self) -> InterfaceResult { + // TODO: implement for this backend — currently returns 0 (safe fallback) + Ok(0) + } } diff --git a/crate/server/src/core/kms/mod.rs b/crate/server/src/core/kms/mod.rs index f5901ca88b..c1922217f7 100644 --- a/crate/server/src/core/kms/mod.rs +++ b/crate/server/src/core/kms/mod.rs @@ -155,6 +155,28 @@ impl KMS { ) .await?; + // Seed the kms.objects.total gauge from the real DB count on startup. + // + // This ensures the metric starts at the correct absolute value rather + // than 0. Without this seed, the gauge would only reach the right count + // after the first periodic cron sync (up to 30 s later), giving a + // misleading reading immediately after server restart. + if let Some(ref m) = metrics { + match database.count_all_non_destroyed_objects().await { + Ok(count) => { + m.update_objects_total( + i64::try_from(count).unwrap_or(i64::MAX), + ); + } + Err(e) => { + // Non-fatal: the cron will correct the value within 30 s. + cosmian_logger::debug!( + "[kms-init] Failed to seed kms.objects.total: {e}" + ); + } + } + } + Ok(Self { params: server_params.clone(), database, diff --git a/crate/server/src/core/otel_metrics.rs b/crate/server/src/core/otel_metrics.rs index a7d3ae007f..17843d5bef 100644 --- a/crate/server/src/core/otel_metrics.rs +++ b/crate/server/src/core/otel_metrics.rs @@ -87,6 +87,15 @@ pub struct OtelMetrics { /// Mirror of `active_keys_count` for tracking the last set value active_keys_count_value: Arc>, + /// Mirror of `kms_objects_total` for tracking the last emitted absolute count. + /// + /// Because `kms_objects_total` is an `UpDownCounter` (delta-based), we cannot + /// "set" it to an absolute value. When a periodic sync delivers a fresh + /// absolute count, we compute `delta = new_count - last_known` and add that + /// delta. `objects_total_mirror` holds the last known value so subsequent + /// sync calls produce the correct incremental delta. + objects_total_mirror: Arc>, + /// Cache hit/miss statistics pub cache_operations_total: Counter, @@ -277,6 +286,7 @@ impl OtelMetrics { kms_objects_total, active_keys_count, active_keys_count_value: Arc::new(RwLock::new(0)), + objects_total_mirror: Arc::new(RwLock::new(0)), cache_operations_total, hsm_operations_total, }) @@ -456,6 +466,28 @@ impl OtelMetrics { } } + /// Set the current `kms.objects.total` gauge from an absolute object count. + /// + /// `kms_objects_total` is an `UpDownCounter`, which only accepts incremental + /// `add(delta)` calls. To simulate a gauge "set", we keep a mirror of the + /// last emitted absolute value in `objects_total_mirror` and emit only the + /// delta between the new count and that mirror on each call. + /// + /// This method is called: + /// - Once at server startup with the result of `count_all_non_destroyed_objects` + /// to seed the gauge from the real DB state. + /// - Every 30 s by the metrics cron task to correct drift that may have + /// accumulated from the fast-path delta tracking. + pub fn update_objects_total(&self, absolute_count: i64) { + if let Ok(mut last) = self.objects_total_mirror.write() { + let delta = absolute_count - *last; + if delta != 0 { + self.kms_objects_total.add(delta, &[]); + *last = absolute_count; + } + } + } + /// Record cache operation pub fn record_cache_operation(&self, operation: &str, result: &str) { self.cache_operations_total.add( @@ -500,6 +532,23 @@ impl DbMetricsRecorder for OtelMetrics { ) { self.record_database_operation(operation, backend, outcome, duration_seconds); } + + /// Add a signed delta to `kms.objects.total`. + /// + /// This is the fast path called synchronously after each mutating DB + /// operation (+1 for create, -1 for delete, pre-computed delta for + /// atomic). Calls with `delta == 0` are silently ignored to avoid + /// emitting no-op measurements to the OTLP backend. + /// + /// Note: this does **not** update `objects_total_mirror`. The mirror is + /// only updated by the absolute-sync path (`update_objects_total`) so that + /// the periodic cron can always correct accumulated drift from this fast + /// path without the two paths interfering with each other. + fn record_object_delta(&self, delta: i64) { + if delta != 0 { + self.kms_objects_total.add(delta, &[]); + } + } } #[cfg(test)] diff --git a/crate/server/src/cron.rs b/crate/server/src/cron.rs index e6134814d0..d1ddf05756 100644 --- a/crate/server/src/cron.rs +++ b/crate/server/src/cron.rs @@ -36,9 +36,15 @@ pub fn spawn_metrics_cron(kms: Arc) -> oneshot::Sender<()> { let mut uptime_interval = tokio::time::interval(std::time::Duration::from_secs(1)); let mut shutdown_rx = shutdown_rx; loop { + // TODO: this should better be deleted/updated because we are changing the approach tokio::select! { _ = interval.tick() => { - // Refresh Active Keys via KMIP Locate filtered on Active state + // ── Active-keys refresh (via KMIP Locate) ───────────────────────── + // NOTE: this uses a KMIP Locate filtered to Active state, which is + // subject to the caller's ACL. In multi-admin deployments this may + // undercount if the cron user does not own all keys. A future task + // should replace this with a privileged SQL count, similar to the + // objects.total path below. let request = Locate { attributes: Attributes { state: Some(State::Active), @@ -61,6 +67,32 @@ pub fn spawn_metrics_cron(kms: Arc) -> oneshot::Sender<()> { debug!("[metrics-cron] Failed to refresh active keys count: {}", e); } } + + // ── Objects-total absolute sync ──────────────────────────────────── + // The fast-path delta tracking (+1/-1 per operation) can accumulate + // drift over time because of the TOCTOU race in the Upsert pre-read + // and because the Redis backend always emits 0 from the fast path. + // Every 30 s we re-read the authoritative SQL count and correct the + // UpDownCounter via the mirror pattern (see OtelMetrics::update_objects_total). + if let Some(ref metrics) = kms.metrics { + match kms.database.count_all_non_destroyed_objects().await { + Ok(count) => { + debug!( + "[metrics-cron] kms.objects.total synced to {}", + count + ); + metrics.update_objects_total( + i64::try_from(count).unwrap_or(i64::MAX), + ); + } + Err(e) => { + debug!( + "[metrics-cron] Failed to sync kms.objects.total: {}", + e + ); + } + } + } } _ = uptime_interval.tick() => { if let Some(ref metrics) = kms.metrics { diff --git a/crate/server/src/middlewares/mod.rs b/crate/server/src/middlewares/mod.rs index 587f9a2ea6..fe1b848961 100644 --- a/crate/server/src/middlewares/mod.rs +++ b/crate/server/src/middlewares/mod.rs @@ -13,6 +13,9 @@ pub(crate) use jwt::{JwksManager, JwtAuth, JwtConfig, JwtTokenHeaders, UserClaim mod rate_limiter; pub(crate) use rate_limiter::{RateLimiterConfig, RateLimiterMiddleware}; +mod otel_http_middleware; +pub(crate) use otel_http_middleware::OtelHttpMetrics; + /// Represents an authenticated user /// /// This struct is stored in the request extensions after successful diff --git a/crate/server/src/middlewares/otel_http_middleware.rs b/crate/server/src/middlewares/otel_http_middleware.rs new file mode 100644 index 0000000000..9eedabbbc3 --- /dev/null +++ b/crate/server/src/middlewares/otel_http_middleware.rs @@ -0,0 +1,261 @@ +//! OpenTelemetry HTTP metrics middleware. +//! +//! Records `kms.http.requests.total`, `kms.http.request.duration`, and +//! `kms.active.connections` for every request that reaches the server. +//! +//! Must be placed as the **outermost** `App`-level wrap to correctly measure total latency. + +use crate::core::OtelMetrics; +use actix_web::{ + Error, + dev::{Service, ServiceRequest, ServiceResponse, Transform}, +}; +use futures::future::{Ready, ok}; +use std::{ + future::Future, + pin::Pin, + rc::Rc, + sync::Arc, + task::{Context, Poll}, + time::Instant, +}; + +/// App-level middleware that records HTTP request metrics via OTLP. +/// +/// When `metrics` is `None` (OTLP not configured) the middleware is just a +/// zero-overhead pass-through. +#[derive(Clone)] +pub(crate) struct OtelHttpMetrics { + metrics: Option>, +} + +impl OtelHttpMetrics { + /// Creates a new `OtelHttpMetrics` middleware. + /// + /// Pass `None` to install the middleware as a no-op (metrics disabled). + #[must_use] + pub(crate) const fn new(metrics: Option>) -> Self { + Self { metrics } + } +} + +impl Transform for OtelHttpMetrics +where + S: Service, Error = Error> + 'static, + S::Future: 'static, +{ + type Error = Error; + type Future = Ready>; + type InitError = (); + type Response = ServiceResponse; + type Transform = OtelHttpMetricsService; + + fn new_transform(&self, service: S) -> Self::Future { + ok(OtelHttpMetricsService { + service: Rc::new(service), + metrics: self.metrics.clone(), + }) + } +} + +/// The per-request service produced by [`OtelHttpMetrics`]. +pub(crate) struct OtelHttpMetricsService { + service: Rc, + metrics: Option>, +} + +impl Service for OtelHttpMetricsService +where + S: Service, Error = Error> + 'static, + S::Future: 'static, +{ + type Error = Error; + type Future = Pin>>>; + type Response = ServiceResponse; + + fn poll_ready(&self, ctx: &mut Context) -> Poll> { + self.service.poll_ready(ctx) + } + + fn call(&self, req: ServiceRequest) -> Self::Future { + let service = self.service.clone(); + let metrics = self.metrics.clone(); + + // Snapshot method and normalised path before consuming the request. + let method = req.method().to_string(); + let path = normalize_path(req.path()).to_owned(); + + Box::pin(async move { + // Increment in-flight counter before calling inner service. + if let Some(ref m) = metrics { + m.increment_active_connections(); + } + + let start = Instant::now(); + let result = service.call(req).await; + + // Decrement in-flight counter in both success and error paths. + if let Some(ref m) = metrics { + m.decrement_active_connections(); + + let duration = start.elapsed().as_secs_f64(); + let status = result.as_ref().map_or_else( + |_| "500".to_owned(), + |resp| resp.status().as_str().to_owned(), + ); + + m.record_http_request(&method, &path, &status); + m.record_http_request_duration(&method, &path, duration); + } + + result + }) + } +} + +/// Maps a raw request path to a low-cardinality label for OTEL metrics. +/// +/// Without normalisation, hashed UI asset filenames (`/ui/assets/index-Ab3Cd.js`) +/// and per-key UID segments in REST paths would explode the metric cardinality. +/// +/// The KMS route set is small and stable; static prefix matching is sufficient +/// and avoids any dependency on the actix-web pattern matcher at this layer. +fn normalize_path(path: &str) -> &'static str { + // Exact matches for high-traffic single-path endpoints. + match path { + "/kmip/2_1" | "/kmip" => return "/kmip/2_1", + "/version" => return "/version", + "/health" => return "/health", + _ => {} + } + + // Prefix-based grouping for scoped route families. + if path.starts_with("/access") { + return "/access/{...}"; + } + if path.starts_with("/google_cse") { + return "/google_cse/{...}"; + } + if path.starts_with("/ms_dke") { + return "/ms_dke/{...}"; + } + if path.starts_with("/aws") { + return "/aws/{...}"; + } + if path.starts_with("/azure") { + return "/azure/{...}"; + } + if path.starts_with("/v1/crypto") { + return "/v1/crypto/{...}"; + } + if path.starts_with("/ui") { + return "/ui/{static}"; + } + if path.starts_with("/swagger") || path.starts_with("/openapi") { + return "/swagger/{...}"; + } + if path.starts_with("/download-cli") { + return "/download-cli"; + } + + "/other" +} + +#[cfg(test)] +#[allow(clippy::expect_used)] +mod tests { + use super::{OtelHttpMetrics, normalize_path}; + use crate::core::OtelMetrics; + use actix_web::{App, web}; + use opentelemetry_sdk::metrics::SdkMeterProvider; + use std::sync::Arc; + + #[test] + fn test_normalize_exact_kmip() { + assert_eq!(normalize_path("/kmip/2_1"), "/kmip/2_1"); + assert_eq!(normalize_path("/kmip"), "/kmip/2_1"); + } + + #[test] + fn test_normalize_exact_endpoints() { + assert_eq!(normalize_path("/version"), "/version"); + assert_eq!(normalize_path("/health"), "/health"); + assert_eq!(normalize_path("/download-cli"), "/download-cli"); + } + + #[test] + fn test_normalize_prefix_groups() { + assert_eq!(normalize_path("/access/owned"), "/access/{...}"); + assert_eq!(normalize_path("/access/accesses/abc123"), "/access/{...}"); + assert_eq!(normalize_path("/google_cse/rewrap"), "/google_cse/{...}"); + assert_eq!(normalize_path("/ms_dke/keys/mykey"), "/ms_dke/{...}"); + assert_eq!( + normalize_path("/aws/kms/xks/v1/keys/abc/decrypt"), + "/aws/{...}" + ); + assert_eq!(normalize_path("/azure/keys/abc"), "/azure/{...}"); + assert_eq!(normalize_path("/v1/crypto/encrypt"), "/v1/crypto/{...}"); + } + + #[test] + fn test_normalize_ui_assets() { + assert_eq!(normalize_path("/ui/assets/index-Ab3Cd.js"), "/ui/{static}"); + assert_eq!(normalize_path("/ui/"), "/ui/{static}"); + } + + #[test] + fn test_normalize_swagger() { + assert_eq!(normalize_path("/swagger/ui"), "/swagger/{...}"); + assert_eq!(normalize_path("/openapi/kms.yaml"), "/swagger/{...}"); + } + + #[test] + fn test_normalize_unknown_falls_to_other() { + assert_eq!(normalize_path("/unknown/deep/path"), "/other"); + assert_eq!(normalize_path("/"), "/other"); + } + + // middleware smoke tests + + fn make_test_metrics() -> Arc { + // No-op provider: instruments accept calls but do not export anything. + // Same approach used by the existing tests in core/otel_metrics.rs. + let provider = SdkMeterProvider::builder().build(); + Arc::new(OtelMetrics::new(provider).expect("Failed to create OtelMetrics")) + } + + /// Verifies that the middleware passes requests through without error when + /// a real `OtelMetrics` instance is configured (OTLP recording path is exercised). + #[actix_web::test] + async fn test_middleware_with_otel_metrics_passes_requests() { + let app = actix_web::test::init_service( + App::new() + .wrap(OtelHttpMetrics::new(Some(make_test_metrics()))) + .service(web::resource("/kmip/2_1").to(|| async { "ok" })), + ) + .await; + + let req = actix_web::test::TestRequest::post() + .uri("/kmip/2_1") + .to_request(); + let resp = actix_web::test::call_service(&app, req).await; + assert!(resp.status().is_success()); + } + + /// Verifies that `metrics = None` is a true no-op (no panic, request succeeds). + #[actix_web::test] + async fn test_middleware_noop_when_metrics_none() { + let app = actix_web::test::init_service( + App::new() + .wrap(OtelHttpMetrics::new(None)) + .service(web::resource("/health").to(|| async { "ok" })), + ) + .await; + + let req = actix_web::test::TestRequest::get() + .uri("/health") + .to_request(); + let resp = actix_web::test::call_service(&app, req).await; + assert!(resp.status().is_success()); + } +} diff --git a/crate/server/src/start_kms_server.rs b/crate/server/src/start_kms_server.rs index dc1d67a3bd..e950d2e21a 100644 --- a/crate/server/src/start_kms_server.rs +++ b/crate/server/src/start_kms_server.rs @@ -52,7 +52,7 @@ use crate::{ cron, error::KmsError, middlewares::{ - ApiTokenAuth, EnsureAuth, JwksManager, JwtAuth, JwtConfig, TlsAuth, + ApiTokenAuth, EnsureAuth, JwksManager, JwtAuth, JwtConfig, OtelHttpMetrics, TlsAuth, extract_peer_certificate, }, result::{KResult, KResultHelper}, @@ -775,6 +775,7 @@ pub async fn prepare_kms_server(kms_server: Arc) -> KResult, ) -> DbResult { - 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 + let result = 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; + // Emit +1 on success: a net-new object was just added. + if result.is_ok() { + if let Some(ref rec) = self.recorder { + rec.record_object_delta(1); + } + } + result } /// Retrieve objects from the database. @@ -321,13 +329,21 @@ impl Database { /// Delete an object from the database. pub async fn delete(&self, uid: &str) -> DbResult<()> { - 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 + let result = 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; + // Emit -1 on success: the object has been permanently removed. + if result.is_ok() { + if let Some(ref rec) = self.recorder { + rec.record_object_delta(-1); + } + } + result } /// Test if an object identified by its `uid` is currently owned by `owner` @@ -409,29 +425,102 @@ impl Database { if operations.is_empty() { return Ok(vec![]); } - 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?; + + // Pre-compute the net object-count delta for the `kms.objects.total` metric. + // + // We walk the operation list BEFORE executing the transaction to determine + // how many net-new objects will be created vs destroyed: + // + // - `Create` always adds one object → delta += 1 + // - `Delete` always removes one object → delta -= 1 + // - `Upsert` is ambiguous: it is an INSERT…ON CONFLICT UPDATE, so it adds + // an object only if the UID did not previously exist. We do a lightweight + // `retrieve` pre-read to decide: if the object is absent → delta += 1, + // if it already exists → delta += 0 (it will be overwritten in-place). + // - `UpdateObject` / `UpdateState` leave the count unchanged → delta += 0 + // + // **TOCTOU note**: there is a narrow race window between the pre-read and the + // transaction commit. A concurrent create or delete could change the real + // count by ±1 before our transaction lands. We accept this because: + // a) the periodic 30-second cron sync will correct any accumulated drift, and + // b) the alternative (reading inside the transaction) is impractical across + // the heterogeneous backend abstraction. + let mut delta: i64 = 0; + for op in operations { + match op { + AtomicOperation::Create(_) => delta += 1, + AtomicOperation::Delete(_) => delta -= 1, + AtomicOperation::Upsert((uid, _, _, _, _)) => { + // Upsert is net +1 only when the object does not yet exist. + // `retrieve_object` goes through the recorder ("retrieve" label), + // which is fine — a pre-read is a legitimate DB operation. + let exists = self.retrieve_object(uid).await?.is_some(); + if !exists { + delta += 1; } - AtomicOperation::Delete(uid) => { - self.unwrapped_cache.clear_cache(uid).await; + } + AtomicOperation::UpdateObject(_) | AtomicOperation::UpdateState(_) => {} + } + } + + let result = 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::UpdateState(_) => {} } + Ok(ids) + }) + .await; + + // Emit the pre-computed delta only on success and only when non-zero. + // On failure the transaction was rolled back, so no objects changed. + if result.is_ok() && delta != 0 { + if let Some(ref rec) = self.recorder { + rec.record_object_delta(delta); } - Ok(ids) - }) - .await + } + result + } + + /// Count all live (non-destroyed) objects across every registered object store. + /// + /// This is a **metrics-only** operation that bypasses user/permission filters. + /// It is called: + /// 1. Once at server startup to seed the `kms.objects.total` gauge. + /// 2. Every 30 s by the metrics cron task to correct any drift from the + /// delta-based fast path (see `record_object_delta` in `database_objects.rs`). + /// + /// Because several stores may be registered simultaneously (e.g. one SQL store + /// plus one or more HSM stores), the results are summed. Backends that have not + /// yet implemented `count_all_non_destroyed` return `0` via the trait default, + /// which is acceptable — the sum will still be a valid lower bound. + pub async fn count_all_non_destroyed_objects(&self) -> DbResult { + let map = self.objects.read().await; + let mut total: u64 = 0; + for store in map.values() { + let n = store + .count_all_non_destroyed() + .await + .unwrap_or(0); // A single backend failure must not block the aggregate + total = total.saturating_add(n); + } + Ok(total) } } @@ -502,6 +591,10 @@ mod tests { outcome.to_owned(), )); } + + fn record_object_delta(&self, _delta: i64) { + // no-op in tests; delta tracking is covered by unit tests in otel_metrics + } } let tmp = TempDir::new().expect("Failed to create temp dir"); diff --git a/crate/server_database/src/core/db_metrics.rs b/crate/server_database/src/core/db_metrics.rs index 03294d449f..4c6b176291 100644 --- a/crate/server_database/src/core/db_metrics.rs +++ b/crate/server_database/src/core/db_metrics.rs @@ -36,4 +36,20 @@ pub trait DbMetricsRecorder: Send + Sync { outcome: &str, duration_seconds: f64, ); + + /// Record a signed delta for the `kms.objects.total` gauge. + /// + /// # Why a delta rather than an absolute value? + /// + /// The underlying OTLP instrument is an `UpDownCounter`, which only + /// accepts incremental `add(delta)` calls. There is no "set" operation. + /// The delta path is a low-overhead fast path: +1 on `create`, -1 on + /// `delete`. For `atomic` operations the delta is pre-computed before the + /// transaction runs (see `database_objects.rs` for the rationale around + /// TOCTOU and Upsert pre-reads). + /// + /// A periodic absolute sync via `count_all_non_destroyed_objects` corrects + /// any drift that accumulates from the TOCTOU race window or from backends + /// (e.g. Redis) that always emit `0` from the fast path. + fn record_object_delta(&self, delta: i64); } diff --git a/crate/server_database/src/stores/redis/redis_with_findex.rs b/crate/server_database/src/stores/redis/redis_with_findex.rs index 5350fe45d5..9763c8dd56 100644 --- a/crate/server_database/src/stores/redis/redis_with_findex.rs +++ b/crate/server_database/src/stores/redis/redis_with_findex.rs @@ -622,6 +622,18 @@ impl ObjectsStore for RedisWithFindex { }) .collect()) } + + // TODO(redis): implement count_all_non_destroyed properly. + // + // Redis-findex stores objects as individual keys with prefix `do::`. + // A correct implementation would use `SCAN do::*` and then retrieve and + // filter each object's state field. This is deferred due to the high cost + // of a full Redis key scan on large datasets and because Redis-findex is + // a non-FIPS backend with lower CI priority. + // + // For now, the default `Ok(0)` from the trait is used, which means + // `kms.objects.total` will read `0` for Redis-backed deployments. + // async fn count_all_non_destroyed(&self) -> InterfaceResult { ... } } #[async_trait(?Send)] diff --git a/crate/server_database/src/stores/sql/mysql.rs b/crate/server_database/src/stores/sql/mysql.rs index 105f99ea72..00a4a5b73c 100644 --- a/crate/server_database/src/stores/sql/mysql.rs +++ b/crate/server_database/src/stores/sql/mysql.rs @@ -625,6 +625,30 @@ impl ObjectsStore for MySqlPool { ) .await?) } + + /// Returns the total count of live (non-destroyed) objects in this `MySQL` store. + /// + /// This is a **metrics-only** privileged query: it scans the full `objects` table + /// without any user or permission filter, so the result always reflects the true + /// server-wide inventory. It must never be used to answer client requests. + /// + /// The state strings `'Destroyed'` and `'Destroyed_Compromised'` are the Rust + /// enum variant names as serialised to the DB by `strum::Display`. + async fn count_all_non_destroyed(&self) -> InterfaceResult { + let sql = get_mysql_query!("count-non-destroyed-objects"); + let mut conn = self + .get_configured_conn() + .await + .map_err(InterfaceError::from)?; + // MySQL returns COUNT(*) as u64 via the mysql_async FromValue impl. + let count: u64 = conn + .exec_first(sql, ()) + .await + .map_err(DbError::from) + .map_err(InterfaceError::from)? + .unwrap_or(0); + Ok(count) + } } #[async_trait(?Send)] diff --git a/crate/server_database/src/stores/sql/pgsql.rs b/crate/server_database/src/stores/sql/pgsql.rs index 723662c047..82925a1626 100644 --- a/crate/server_database/src/stores/sql/pgsql.rs +++ b/crate/server_database/src/stores/sql/pgsql.rs @@ -790,6 +790,28 @@ impl ObjectsStore for PgPool { Ok(out) }) } + + /// Returns the total count of live (non-destroyed) objects in this `PostgreSQL` store. + /// + /// This is a **metrics-only** privileged query: it scans the full `objects` table + /// without any user or permission filter, so the result always reflects the true + /// server-wide inventory. It must never be used to answer client requests. + /// + /// The state strings `'Destroyed'` and `'Destroyed_Compromised'` are the Rust + /// enum variant names as serialised to the DB by `strum::Display`. + async fn count_all_non_destroyed(&self) -> InterfaceResult { + let sql = get_pgsql_query!("count-non-destroyed-objects"); + let client = pg_get_client(&self.pool) + .await + .map_err(InterfaceError::from)?; + let row = client + .query_one(sql, &[]) + .await + .map_err(DbError::from) + .map_err(InterfaceError::from)?; + let count: i64 = row.get(0); + Ok(u64::try_from(count).unwrap_or(0)) + } } #[async_trait(?Send)] diff --git a/crate/server_database/src/stores/sql/query.sql b/crate/server_database/src/stores/sql/query.sql index a3faf14737..9c09c3fa05 100644 --- a/crate/server_database/src/stores/sql/query.sql +++ b/crate/server_database/src/stores/sql/query.sql @@ -78,6 +78,16 @@ INSERT INTO objects (id, object, attributes, state, owner) VALUES ($1, $2, $3, $ DO UPDATE SET object=$2, attributes=$3, state=$4, owner=$5 WHERE objects.owner=$5; +-- name: count-non-destroyed-objects +-- Privileged metrics-only query: counts ALL objects regardless of owner. +-- Called exclusively by the OTEL metrics layer for kms.objects.total. +-- State strings correspond to Rust enum variant names via strum::Display: +-- Destroyed = the object was explicitly destroyed +-- Destroyed_Compromised = the object was destroyed after being compromised +-- All other states (PreActive, Active, Deactivated, Compromised) are live objects. +SELECT COUNT(*) FROM objects +WHERE state NOT IN ('Destroyed', 'Destroyed_Compromised'); + -- name: select-user-accesses-for-object SELECT permissions FROM read_access diff --git a/crate/server_database/src/stores/sql/query_mysql.sql b/crate/server_database/src/stores/sql/query_mysql.sql index ff41c6ddbb..5f6b14f63d 100644 --- a/crate/server_database/src/stores/sql/query_mysql.sql +++ b/crate/server_database/src/stores/sql/query_mysql.sql @@ -68,6 +68,16 @@ DELETE FROM tags; +-- name: count-non-destroyed-objects +-- Privileged metrics-only query: counts ALL objects regardless of owner. +-- Called exclusively by the OTEL metrics layer for kms.objects.total. +-- State strings correspond to Rust enum variant names via strum::Display: +-- Destroyed = the object was explicitly destroyed +-- Destroyed_Compromised = the object was destroyed after being compromised +-- All other states (PreActive, Active, Deactivated, Compromised) are live objects. +SELECT COUNT(*) FROM objects +WHERE state NOT IN ('Destroyed', 'Destroyed_Compromised'); + -- name: insert-objects INSERT INTO objects (id, object, attributes, state, owner) VALUES (?, ?, ?, ?, ?); diff --git a/crate/server_database/src/stores/sql/sqlite.rs b/crate/server_database/src/stores/sql/sqlite.rs index e7d79b3c77..21fab1d079 100644 --- a/crate/server_database/src/stores/sql/sqlite.rs +++ b/crate/server_database/src/stores/sql/sqlite.rs @@ -534,6 +534,28 @@ impl ObjectsStore for SqlitePool { .map_err(DbError::from)?; Ok(rows) } + + /// Returns the total count of live (non-destroyed) objects in this `SQLite` store. + /// + /// This is a **metrics-only** privileged query: it scans the full `objects` table + /// without any user or permission filter, so the result always reflects the true + /// server-wide inventory. It must never be used to answer client requests. + /// + /// The state strings `'Destroyed'` and `'Destroyed_Compromised'` are the Rust + /// enum variant names as serialised to the DB by `strum::Display`. + async fn count_all_non_destroyed(&self) -> InterfaceResult { + // No $N placeholders — no need for replace_dollars_with_qn. + let sql = get_sqlite_query!("count-non-destroyed-objects").to_string(); + let count: i64 = self + .reader() + .call(move |c: &mut rusqlite::Connection| { + let mut stmt = c.prepare(&sql)?; + stmt.query_row([], |r| r.get(0)) + }) + .await + .map_err(DbError::from)?; + Ok(u64::try_from(count).unwrap_or(0)) + } } #[async_trait(?Send)] From 84ae0f99843bb7f0608c9265747aff2c7c997edf Mon Sep 17 00:00:00 2001 From: HatemMn <19950216+HatemMn@users.noreply.github.com> Date: Tue, 2 Jun 2026 18:49:47 +0200 Subject: [PATCH 03/28] feat: `kms.objects.total` (Step 3) --- CHANGELOG/feat_richOTELmetrics.md | 31 ++-- crate/interfaces/src/hsm/hsm_store.rs | 7 + crate/interfaces/src/stores/objects_store.rs | 7 +- crate/server/src/core/otel_metrics.rs | 105 +++---------- .../src/core/database_objects.rs | 143 +++++------------- crate/server_database/src/core/db_metrics.rs | 16 -- .../src/stores/redis/redis_with_findex.rs | 9 +- 7 files changed, 86 insertions(+), 232 deletions(-) diff --git a/CHANGELOG/feat_richOTELmetrics.md b/CHANGELOG/feat_richOTELmetrics.md index 1578db83cf..cb3f93a682 100644 --- a/CHANGELOG/feat_richOTELmetrics.md +++ b/CHANGELOG/feat_richOTELmetrics.md @@ -32,27 +32,24 @@ ### Object Count Metric — `kms.objects.total` (Step 3) - Add `ObjectsStore::count_all_non_destroyed(&self) -> InterfaceResult` default - method to the `ObjectsStore` trait. The default returns `Ok(0)` so existing backends - compile without change; concrete implementations override it. + method to the `ObjectsStore` trait. The default logs a `warn!` and returns `Ok(0)`, + making it visible when a new backend forgets to implement it. +- Add explicit silent `Ok(0)` overrides to `RedisWithFindex` and `HsmStore` to suppress + the warning for intentionally deferred implementations. - Implement `count_all_non_destroyed` for SQLite, PostgreSQL, and MySQL via named SQL queries (`count-non-destroyed-objects`) that scan the full `objects` table without user/permission filters — a privileged metrics-only operation. -- Add `TODO(redis)` comment in `RedisWithFindex`; Redis remains on the default `Ok(0)` - pending a `SCAN do::*`-based implementation. -- Add `DbMetricsRecorder::record_object_delta(delta: i64)` method to the recorder trait, - allowing the database facade to push incremental object-count changes to OTEL without - a crate-cycle dependency. -- Wire `record_object_delta` in the `Database` facade: - - `+1` on successful `create()` - - `-1` on successful `delete()` - - Pre-computed delta on `atomic()` with Upsert pre-read to distinguish insert vs update; - TOCTOU drift is corrected by the periodic absolute sync. -- Add `OtelMetrics::objects_total_mirror: Arc>` and - `update_objects_total(absolute: i64)` using the mirror pattern (same as - `update_active_keys_count`) to simulate a gauge on `UpDownCounter`. +- Change `kms_objects_total` and `active_keys_count` from `UpDownCounter` to + `Gauge` — the semantically correct OTEL instrument for an absolute current value. + This eliminates the `objects_total_mirror` and `active_keys_count_value` `Arc` + mirror fields; `update_objects_total` and `update_active_keys_count` now call + `gauge.record(absolute, &[])` directly. +- Remove `DbMetricsRecorder::record_object_delta` and all per-operation delta wiring + (`+1` on create, `-1` on delete, Upsert pre-read in atomic) — the gauge is updated + exclusively by the startup seed and the 30-second cron absolute sync, which is + sufficient accuracy for a metrics gauge. - Seed `kms.objects.total` at server startup from `count_all_non_destroyed_objects()` so the gauge is correct from second 0, not after the first cron tick. -- Add 30-second periodic absolute sync in `cron.rs` alongside the active-keys refresh; - corrects any drift from TOCTOU races or Redis backends. +- Add 30-second periodic absolute sync in `cron.rs` alongside the active-keys refresh. - Add `Database::count_all_non_destroyed_objects()` facade that sums counts across all registered stores with `saturating_add`, tolerating partial failures. \ No newline at end of file diff --git a/crate/interfaces/src/hsm/hsm_store.rs b/crate/interfaces/src/hsm/hsm_store.rs index e03ffced4a..f4032b92dc 100644 --- a/crate/interfaces/src/hsm/hsm_store.rs +++ b/crate/interfaces/src/hsm/hsm_store.rs @@ -377,6 +377,13 @@ impl ObjectsStore for HsmStore { Ok(uids) } + + /// HSM object counting is not implemented — HSMs do not expose a key-count + /// API in the PKCS#11 interface. Override the trait default to suppress + /// the warning that would otherwise fire every 30 s from the metrics cron. + async fn count_all_non_destroyed(&self) -> InterfaceResult { + Ok(0) + } } // ────────────────────────────────────────────────────────────────────────────── diff --git a/crate/interfaces/src/stores/objects_store.rs b/crate/interfaces/src/stores/objects_store.rs index 7dd70b1132..7d3ac05327 100644 --- a/crate/interfaces/src/stores/objects_store.rs +++ b/crate/interfaces/src/stores/objects_store.rs @@ -6,6 +6,8 @@ use cosmian_kmip::{ kmip_2_1::{kmip_attributes::Attributes, kmip_objects::Object}, }; +use cosmian_logger::warn; + use crate::{InterfaceResult, ObjectWithMetadata}; /// An atomic operation on the objects database @@ -122,7 +124,10 @@ pub trait ObjectsStore { /// real implementation when ready. A `TODO` comment is added at each /// call site that still uses the default. async fn count_all_non_destroyed(&self) -> InterfaceResult { - // TODO: implement for this backend — currently returns 0 (safe fallback) + warn!( + "count_all_non_destroyed not implemented for this ObjectsStore backend — \ + kms.objects.total will read 0 until a real implementation is provided" + ); Ok(0) } } diff --git a/crate/server/src/core/otel_metrics.rs b/crate/server/src/core/otel_metrics.rs index 17843d5bef..23ee2a5003 100644 --- a/crate/server/src/core/otel_metrics.rs +++ b/crate/server/src/core/otel_metrics.rs @@ -19,7 +19,7 @@ use std::{ use cosmian_kms_server_database::{DbMetricsRecorder, MainDbKind}; use opentelemetry::{ KeyValue, - metrics::{Counter, Histogram, Meter, MeterProvider, UpDownCounter}, + metrics::{Counter, Gauge, Histogram, Meter, MeterProvider, UpDownCounter}, }; use opentelemetry_sdk::metrics::SdkMeterProvider; @@ -78,23 +78,11 @@ pub struct OtelMetrics { /// Current number of active connections pub active_connections: UpDownCounter, - /// Total number of objects in the KMS - pub kms_objects_total: UpDownCounter, + /// Total number of objects in the KMS (gauge — records absolute count directly) + pub kms_objects_total: Gauge, - /// Current number of active keys (absolute count from Locate responses) - pub active_keys_count: UpDownCounter, - - /// Mirror of `active_keys_count` for tracking the last set value - active_keys_count_value: Arc>, - - /// Mirror of `kms_objects_total` for tracking the last emitted absolute count. - /// - /// Because `kms_objects_total` is an `UpDownCounter` (delta-based), we cannot - /// "set" it to an absolute value. When a periodic sync delivers a fresh - /// absolute count, we compute `delta = new_count - last_known` and add that - /// delta. `objects_total_mirror` holds the last known value so subsequent - /// sync calls produce the correct incremental delta. - objects_total_mirror: Arc>, + /// Current number of active keys in Active state (gauge — records absolute count directly) + pub active_keys_count: Gauge, /// Cache hit/miss statistics pub cache_operations_total: Counter, @@ -234,22 +222,21 @@ impl OtelMetrics { .with_unit("{connection}") .build(); - // KMS objects + // KMS objects — gauge records the current absolute count directly let kms_objects_total = meter - .i64_up_down_counter("kms.objects.total") + .i64_gauge("kms.objects.total") .with_description("Total number of objects in the KMS") .with_unit("{object}") .build(); - // Active Keys count (absolute number of keys in Active state) + // Active keys count — gauge records the current absolute count directly let active_keys_count = meter - .i64_up_down_counter("kms.keys.active.count") + .i64_gauge("kms.keys.active.count") .with_description("Number of keys in Active state (absolute count based on Locate)") .with_unit("{key}") .build(); - // Force the time series to exist even when the count is 0. - // Without at least one measurement, some backends won't expose the metric at all. - active_keys_count.add(0, &[]); + // Seed the time series so it is visible in the backend from server start. + active_keys_count.record(0, &[]); // Cache operations let cache_operations_total = meter @@ -285,8 +272,6 @@ impl OtelMetrics { active_connections, kms_objects_total, active_keys_count, - active_keys_count_value: Arc::new(RwLock::new(0)), - objects_total_mirror: Arc::new(RwLock::new(0)), cache_operations_total, hsm_operations_total, }) @@ -436,56 +421,17 @@ impl OtelMetrics { self.active_connections.add(-1, &[]); } - /// Update object count for a specific type - pub fn update_object_count(&self, object_type: &str, count: f64) { - // For UpDownCounter, we need to track the delta - // This is a simplified implementation - in production you might want to track previous values - // Round the f64 to avoid truncation issues - #[allow(clippy::cast_possible_wrap)] - #[allow(clippy::cast_possible_truncation)] - #[allow(clippy::as_conversions)] - let count_i64 = count.round() as i64; - self.kms_objects_total.add( - count_i64, - &[KeyValue::new("object_type", object_type.to_owned())], - ); - } - - /// Set the current active keys count from an absolute Locate response - /// - /// OTLP instrument is an `UpDownCounter`, so we compute the delta from - /// the previously observed value and add it. The last value is mirrored - /// internally for subsequent updates and optional inspection. + /// Set the current active keys count from an absolute Locate response. pub fn update_active_keys_count(&self, absolute_count: i64) { - if let Ok(mut last) = self.active_keys_count_value.write() { - let delta = absolute_count - *last; - if delta != 0 { - self.active_keys_count.add(delta, &[]); - *last = absolute_count; - } - } + self.active_keys_count.record(absolute_count, &[]); } - /// Set the current `kms.objects.total` gauge from an absolute object count. + /// Set `kms.objects.total` to the current absolute object count. /// - /// `kms_objects_total` is an `UpDownCounter`, which only accepts incremental - /// `add(delta)` calls. To simulate a gauge "set", we keep a mirror of the - /// last emitted absolute value in `objects_total_mirror` and emit only the - /// delta between the new count and that mirror on each call. - /// - /// This method is called: - /// - Once at server startup with the result of `count_all_non_destroyed_objects` - /// to seed the gauge from the real DB state. - /// - Every 30 s by the metrics cron task to correct drift that may have - /// accumulated from the fast-path delta tracking. + /// Called once at server startup (seeding from the real DB count) and + /// every 30 s by the metrics cron task. pub fn update_objects_total(&self, absolute_count: i64) { - if let Ok(mut last) = self.objects_total_mirror.write() { - let delta = absolute_count - *last; - if delta != 0 { - self.kms_objects_total.add(delta, &[]); - *last = absolute_count; - } - } + self.kms_objects_total.record(absolute_count, &[]); } /// Record cache operation @@ -532,23 +478,6 @@ impl DbMetricsRecorder for OtelMetrics { ) { self.record_database_operation(operation, backend, outcome, duration_seconds); } - - /// Add a signed delta to `kms.objects.total`. - /// - /// This is the fast path called synchronously after each mutating DB - /// operation (+1 for create, -1 for delete, pre-computed delta for - /// atomic). Calls with `delta == 0` are silently ignored to avoid - /// emitting no-op measurements to the OTLP backend. - /// - /// Note: this does **not** update `objects_total_mirror`. The mirror is - /// only updated by the absolute-sync path (`update_objects_total`) so that - /// the periodic cron can always correct accumulated drift from this fast - /// path without the two paths interfering with each other. - fn record_object_delta(&self, delta: i64) { - if delta != 0 { - self.kms_objects_total.add(delta, &[]); - } - } } #[cfg(test)] diff --git a/crate/server_database/src/core/database_objects.rs b/crate/server_database/src/core/database_objects.rs index 7270882ef8..56603d9cea 100644 --- a/crate/server_database/src/core/database_objects.rs +++ b/crate/server_database/src/core/database_objects.rs @@ -182,22 +182,14 @@ impl Database { attributes: &Attributes, tags: &HashSet, ) -> DbResult { - let result = 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; - // Emit +1 on success: a net-new object was just added. - if result.is_ok() { - if let Some(ref rec) = self.recorder { - rec.record_object_delta(1); - } - } - result + 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. @@ -329,21 +321,13 @@ impl Database { /// Delete an object from the database. pub async fn delete(&self, uid: &str) -> DbResult<()> { - let result = 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; - // Emit -1 on success: the object has been permanently removed. - if result.is_ok() { - if let Some(ref rec) = self.recorder { - rec.record_object_delta(-1); - } - } - result + 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` @@ -426,76 +410,29 @@ impl Database { return Ok(vec![]); } - // Pre-compute the net object-count delta for the `kms.objects.total` metric. - // - // We walk the operation list BEFORE executing the transaction to determine - // how many net-new objects will be created vs destroyed: - // - // - `Create` always adds one object → delta += 1 - // - `Delete` always removes one object → delta -= 1 - // - `Upsert` is ambiguous: it is an INSERT…ON CONFLICT UPDATE, so it adds - // an object only if the UID did not previously exist. We do a lightweight - // `retrieve` pre-read to decide: if the object is absent → delta += 1, - // if it already exists → delta += 0 (it will be overwritten in-place). - // - `UpdateObject` / `UpdateState` leave the count unchanged → delta += 0 - // - // **TOCTOU note**: there is a narrow race window between the pre-read and the - // transaction commit. A concurrent create or delete could change the real - // count by ±1 before our transaction lands. We accept this because: - // a) the periodic 30-second cron sync will correct any accumulated drift, and - // b) the alternative (reading inside the transaction) is impractical across - // the heterogeneous backend abstraction. - let mut delta: i64 = 0; - for op in operations { - match op { - AtomicOperation::Create(_) => delta += 1, - AtomicOperation::Delete(_) => delta -= 1, - AtomicOperation::Upsert((uid, _, _, _, _)) => { - // Upsert is net +1 only when the object does not yet exist. - // `retrieve_object` goes through the recorder ("retrieve" label), - // which is fine — a pre-read is a legitimate DB operation. - let exists = self.retrieve_object(uid).await?.is_some(); - if !exists { - delta += 1; + 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::UpdateObject(_) | AtomicOperation::UpdateState(_) => {} - } - } - - let result = 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) - }) - .await; - - // Emit the pre-computed delta only on success and only when non-zero. - // On failure the transaction was rolled back, so no objects changed. - if result.is_ok() && delta != 0 { - if let Some(ref rec) = self.recorder { - rec.record_object_delta(delta); } - } - result + Ok(ids) + }) + .await } /// Count all live (non-destroyed) objects across every registered object store. @@ -503,8 +440,7 @@ impl Database { /// This is a **metrics-only** operation that bypasses user/permission filters. /// It is called: /// 1. Once at server startup to seed the `kms.objects.total` gauge. - /// 2. Every 30 s by the metrics cron task to correct any drift from the - /// delta-based fast path (see `record_object_delta` in `database_objects.rs`). + /// 2. Every 30 s by the metrics cron task. /// /// Because several stores may be registered simultaneously (e.g. one SQL store /// plus one or more HSM stores), the results are summed. Backends that have not @@ -514,10 +450,7 @@ impl Database { let map = self.objects.read().await; let mut total: u64 = 0; for store in map.values() { - let n = store - .count_all_non_destroyed() - .await - .unwrap_or(0); // A single backend failure must not block the aggregate + let n = store.count_all_non_destroyed().await.unwrap_or(0); // A single backend failure must not block the aggregate total = total.saturating_add(n); } Ok(total) @@ -591,10 +524,6 @@ mod tests { outcome.to_owned(), )); } - - fn record_object_delta(&self, _delta: i64) { - // no-op in tests; delta tracking is covered by unit tests in otel_metrics - } } let tmp = TempDir::new().expect("Failed to create temp dir"); diff --git a/crate/server_database/src/core/db_metrics.rs b/crate/server_database/src/core/db_metrics.rs index 4c6b176291..03294d449f 100644 --- a/crate/server_database/src/core/db_metrics.rs +++ b/crate/server_database/src/core/db_metrics.rs @@ -36,20 +36,4 @@ pub trait DbMetricsRecorder: Send + Sync { outcome: &str, duration_seconds: f64, ); - - /// Record a signed delta for the `kms.objects.total` gauge. - /// - /// # Why a delta rather than an absolute value? - /// - /// The underlying OTLP instrument is an `UpDownCounter`, which only - /// accepts incremental `add(delta)` calls. There is no "set" operation. - /// The delta path is a low-overhead fast path: +1 on `create`, -1 on - /// `delete`. For `atomic` operations the delta is pre-computed before the - /// transaction runs (see `database_objects.rs` for the rationale around - /// TOCTOU and Upsert pre-reads). - /// - /// A periodic absolute sync via `count_all_non_destroyed_objects` corrects - /// any drift that accumulates from the TOCTOU race window or from backends - /// (e.g. Redis) that always emit `0` from the fast path. - fn record_object_delta(&self, delta: i64); } diff --git a/crate/server_database/src/stores/redis/redis_with_findex.rs b/crate/server_database/src/stores/redis/redis_with_findex.rs index 9763c8dd56..8eff9823e5 100644 --- a/crate/server_database/src/stores/redis/redis_with_findex.rs +++ b/crate/server_database/src/stores/redis/redis_with_findex.rs @@ -631,9 +631,12 @@ impl ObjectsStore for RedisWithFindex { // of a full Redis key scan on large datasets and because Redis-findex is // a non-FIPS backend with lower CI priority. // - // For now, the default `Ok(0)` from the trait is used, which means - // `kms.objects.total` will read `0` for Redis-backed deployments. - // async fn count_all_non_destroyed(&self) -> InterfaceResult { ... } + // Override the trait default (which logs a warning) with a silent `Ok(0)` + // because Redis intentionally defers this implementation — the warning + // would be misleading and noisy (fired every 30 s from the metrics cron). + async fn count_all_non_destroyed(&self) -> InterfaceResult { + Ok(0) + } } #[async_trait(?Send)] From 3ebad40c70490868bfcc854575efaeb365bc854e Mon Sep 17 00:00:00 2001 From: HatemMn <19950216+HatemMn@users.noreply.github.com> Date: Tue, 2 Jun 2026 21:19:59 +0200 Subject: [PATCH 04/28] feat: `kms.objects.total`, Redis-findex backend (Step 3 bis OK) --- CHANGELOG/feat_richOTELmetrics.md | 22 ++- crate/server/src/core/kms/mod.rs | 8 +- .../redis/additional_redis_findex_tests.rs | 121 +++++++++++++++- .../src/stores/redis/objects_db.rs | 129 +++++++++++++++++ .../src/stores/redis/redis_with_findex.rs | 130 +++++++++++++++--- crate/server_database/src/tests/mod.rs | 3 +- 6 files changed, 381 insertions(+), 32 deletions(-) diff --git a/CHANGELOG/feat_richOTELmetrics.md b/CHANGELOG/feat_richOTELmetrics.md index cb3f93a682..02fe9fa4cd 100644 --- a/CHANGELOG/feat_richOTELmetrics.md +++ b/CHANGELOG/feat_richOTELmetrics.md @@ -52,4 +52,24 @@ so the gauge is correct from second 0, not after the first cron tick. - Add 30-second periodic absolute sync in `cron.rs` alongside the active-keys refresh. - Add `Database::count_all_non_destroyed_objects()` facade that sums counts across all - registered stores with `saturating_add`, tolerating partial failures. \ No newline at end of file + registered stores with `saturating_add`, tolerating partial failures. + +### Object Count Metric — Redis-findex backend (Step 3, continued) + +- Implement `count_all_non_destroyed` for the `RedisWithFindex` backend using an + O(1) counter key (`kms::metrics::live_object_count`) instead of a full key scan. +- **`ObjectsDB`**: add 4 new methods — `adjust_live_count(delta)` (`INCRBY`), + `get_live_count()` (`GET`, returns `None` when key absent), `set_live_count(count)` + (`SET`, bootstrap only), and `scan_count_non_destroyed()` (one-time SCAN+decrypt + to establish the baseline on first boot or after `FLUSHDB`). +- **`RedisWithFindex`**: add `is_live(state) -> bool` helper; wire `adjust_live_count` + into `create` (+1), `update_state` (±1 on boundary cross), `delete` (-1 for live + objects only), and `atomic` (accumulate `live_delta` for the batch, emit one + `INCRBY` after the transaction succeeds). +- **`count_all_non_destroyed`**: fast path reads the counter key (O(1), no + decryption); if absent it falls back to `scan_count_non_destroyed`, persists the + result, then returns it. After the first call the fast path is always taken. +- **Test** (`test_live_count_counter`): 6-step integration test covering create → + destroy → delete-live → delete-destroyed → fast-path count → bootstrap-SCAN count. + Registered in `test_db_redis_with_findex`. + Tagged `#[ignore = "Requires a running Redis instance"]`. \ 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 c1922217f7..834bed7015 100644 --- a/crate/server/src/core/kms/mod.rs +++ b/crate/server/src/core/kms/mod.rs @@ -164,15 +164,11 @@ impl KMS { if let Some(ref m) = metrics { match database.count_all_non_destroyed_objects().await { Ok(count) => { - m.update_objects_total( - i64::try_from(count).unwrap_or(i64::MAX), - ); + m.update_objects_total(i64::try_from(count).unwrap_or(i64::MAX)); } Err(e) => { // Non-fatal: the cron will correct the value within 30 s. - cosmian_logger::debug!( - "[kms-init] Failed to seed kms.objects.total: {e}" - ); + cosmian_logger::debug!("[kms-init] Failed to seed kms.objects.total: {e}"); } } } diff --git a/crate/server_database/src/stores/redis/additional_redis_findex_tests.rs b/crate/server_database/src/stores/redis/additional_redis_findex_tests.rs index 80bdec5b7e..8b6e8b21f5 100644 --- a/crate/server_database/src/stores/redis/additional_redis_findex_tests.rs +++ b/crate/server_database/src/stores/redis/additional_redis_findex_tests.rs @@ -14,15 +14,19 @@ use cosmian_kms_crypto::reexport::cosmian_crypto_core::{ CsRng, RandomFixedSizeCBytes, Secret, SymmetricKey, reexport::rand_core::{RngCore, SeedableRng}, }; +use cosmian_kms_interfaces::ObjectsStore as _; use cosmian_logger::trace; -use redis::aio::ConnectionManager; +use redis::{AsyncCommands, aio::ConnectionManager}; use crate::{ error::DbResult, - stores::redis::{ - init_findex_redis, - objects_db::{ObjectsDB, RedisDbObject}, - permissions::{ObjectUid, PermissionDB, UserId}, + stores::{ + REDIS_WITH_FINDEX_MASTER_KEY_LENGTH, RedisWithFindex, + redis::{ + init_findex_redis, + objects_db::{LIVE_COUNT_KEY, ObjectsDB, RedisDbObject}, + permissions::{ObjectUid, PermissionDB, UserId}, + }, }, tests::get_redis_url, }; @@ -371,3 +375,110 @@ pub(crate) async fn test_corner_case() -> DbResult<()> { Ok(()) } + +/// Verify that the live-object counter key (`kms::metrics::live_object_count`) is +/// kept accurate across `create` / `update_state` / `delete` operations and that the +/// bootstrap SCAN path correctly reconstructs the counter when the key is absent. +/// +/// **Requires a running Redis instance.** +pub(crate) async fn test_live_count_counter() -> DbResult<()> { + cosmian_logger::log_init(option_env!("RUST_LOG")); + + let mut rng = CsRng::from_entropy(); + let redis_url = get_redis_url(); + // `clear_database: true` issues a FLUSHDB so each run starts clean. + let master_key = Secret::::random(&mut rng); + let db = RedisWithFindex::instantiate(&redis_url, master_key, true).await?; + + // ── Step 1: create 3 objects ───────────────────────────────────────────── + // All newly created objects are PreActive (live). + let mut key_bytes = vec![0; 32]; + rng.fill_bytes(&mut key_bytes); + let key1 = create_symmetric_key_kmip_object( + VENDOR_ID_COSMIAN, + &key_bytes, + &Attributes { + cryptographic_algorithm: Some(CryptographicAlgorithm::AES), + ..Default::default() + }, + )?; + rng.fill_bytes(&mut key_bytes); + let key2 = create_symmetric_key_kmip_object( + VENDOR_ID_COSMIAN, + &key_bytes, + &Attributes { + cryptographic_algorithm: Some(CryptographicAlgorithm::AES), + ..Default::default() + }, + )?; + rng.fill_bytes(&mut key_bytes); + let key3 = create_symmetric_key_kmip_object( + VENDOR_ID_COSMIAN, + &key_bytes, + &Attributes { + cryptographic_algorithm: Some(CryptographicAlgorithm::AES), + ..Default::default() + }, + )?; + + let uid1 = db + .create(None, "owner", &key1, key1.attributes()?, &HashSet::new()) + .await?; + let uid2 = db + .create(None, "owner", &key2, key2.attributes()?, &HashSet::new()) + .await?; + let uid3 = db + .create(None, "owner", &key3, key3.attributes()?, &HashSet::new()) + .await?; + + let raw: Option = db.mgr.clone().get(LIVE_COUNT_KEY).await?; + assert_eq!(raw, Some(3), "counter should be 3 after 3 creates"); + + // ── Step 2: destroy uid1 (live → destroyed) ─ counter must drop to 2 ─── + db.update_state(&uid1, State::Destroyed).await?; + let raw: Option = db.mgr.clone().get(LIVE_COUNT_KEY).await?; + assert_eq!(raw, Some(2), "counter should be 2 after destroying uid1"); + + // ── Step 3: delete live uid2 ─ counter must drop to 1 ─────────────────── + db.delete(&uid2).await?; + let raw: Option = db.mgr.clone().get(LIVE_COUNT_KEY).await?; + assert_eq!(raw, Some(1), "counter should be 1 after deleting live uid2"); + + // ── Step 4: delete destroyed uid1 ─ counter must remain at 1 ──────────── + db.delete(&uid1).await?; + let raw: Option = db.mgr.clone().get(LIVE_COUNT_KEY).await?; + assert_eq!( + raw, + Some(1), + "deleting an already-destroyed object must not change the counter" + ); + + // ── Step 5: fast-path count_all_non_destroyed ──────────────────────────── + // Counter key exists → one O(1) GET, no SCAN. + let n = db.count_all_non_destroyed().await?; + assert_eq!(n, 1, "count_all_non_destroyed (fast path) should return 1"); + + // ── Step 6: bootstrap SCAN path ────────────────────────────────────────── + // Delete the counter key to simulate a first-boot / FLUSHDB situation. + redis::cmd("DEL") + .arg(LIVE_COUNT_KEY) + .query_async::<()>(&mut db.mgr.clone()) + .await?; + + // count_all_non_destroyed must fall back to SCAN, count the one live + // object (uid3), write the counter key, and return 1. + let n = db.count_all_non_destroyed().await?; + assert_eq!( + n, 1, + "count_all_non_destroyed (bootstrap SCAN) should return 1" + ); + + // Bootstrap must have persisted the counter so the next call is fast. + let raw: Option = db.mgr.clone().get(LIVE_COUNT_KEY).await?; + assert_eq!(raw, Some(1), "bootstrap must persist the counter to Redis"); + + // ── Teardown ───────────────────────────────────────────────────────────── + db.delete(&uid3).await?; + + Ok(()) +} diff --git a/crate/server_database/src/stores/redis/objects_db.rs b/crate/server_database/src/stores/redis/objects_db.rs index 754da64f19..acd14ee7cb 100644 --- a/crate/server_database/src/stores/redis/objects_db.rs +++ b/crate/server_database/src/stores/redis/objects_db.rs @@ -15,6 +15,7 @@ use cosmian_kms_crypto::reexport::cosmian_crypto_core::{ Aes256Gcm, CsRng, Dem, Instantiable, Nonce, RandomFixedSizeCBytes, SymmetricKey, reexport::rand_core::SeedableRng, }; +use cosmian_logger::debug; use redis::{AsyncCommands, aio::ConnectionManager, pipe}; use serde::{Deserialize, Serialize}; @@ -319,3 +320,131 @@ pub(crate) enum RedisOperation { Upsert(String, RedisDbObject), Delete(String), } + +// ── Live-object counter key ────────────────────────────────────────────────── +// +// A single Redis key `kms::metrics::live_object_count` holds the number of +// objects that are NOT in a terminal (Destroyed / Destroyed_Compromised) state. +// +// Reads → one `GET` — O(1), no decryption. +// Writes → one `INCRBY delta` piggybacked on every mutating operation. +// Bootstrap → first call when the key is absent runs a one-time SCAN+decrypt to +// set the initial value; all subsequent calls are O(1). +// +// The counter lives next to the `ObjectsDB` implementation because: +// - it uses the same `ConnectionManager` and the same `do::*` key namespace; +// - the bootstrap scan reuses `decrypt_object`, which requires `&self`; +// - keeping it here avoids threading the counter through the higher-level +// `RedisWithFindex` layer with extra `Arc` indirection. + +/// Redis key that stores the count of live (non-destroyed) objects. +pub(crate) const LIVE_COUNT_KEY: &str = "kms::metrics::live_object_count"; + +/// SCAN batch hint passed to Redis. Redis may return more or fewer keys per +/// batch; `200` is a pragmatic balance between round-trips and command latency. +const SCAN_BATCH_HINT: u64 = 200; + +impl ObjectsDB { + /// Atomically adjust the live-object counter by `delta`. + /// + /// Uses `INCRBY` (positive) or `DECRBY` (negative). Redis creates the key + /// with value `0` before applying the increment if it does not exist, so + /// calling this before the bootstrap is safe — the counter will start from + /// `delta` rather than the true absolute count. The cron-driven + /// `count_all_non_destroyed` will correct the value on its next tick. + pub(crate) async fn adjust_live_count(&self, delta: i64) -> DbResult<()> { + if delta == 0 { + return Ok(()); + } + self.mgr + .clone() + .incr::<_, i64, i64>(LIVE_COUNT_KEY, delta) + .await?; + Ok(()) + } + + /// Return the current live-object count, or `None` if the key has never + /// been set (i.e. the server has not yet bootstrapped the counter). + pub(crate) async fn get_live_count(&self) -> DbResult> { + let raw: Option = self.mgr.clone().get(LIVE_COUNT_KEY).await?; + Ok(raw.map(|n| u64::try_from(n.max(0)).unwrap_or(0))) + } + + /// Overwrite the live-object counter with an absolute value. + /// + /// Called once during bootstrap (when `get_live_count` returns `None`) and + /// **never** again during normal operation. + pub(crate) async fn set_live_count(&self, count: u64) -> DbResult<()> { + self.mgr + .clone() + .set::<_, _, ()>(LIVE_COUNT_KEY, count) + .await?; + Ok(()) + } + + /// One-time bootstrap: scan every `do::*` key, decrypt each blob, and count + /// objects whose `state` is not `Destroyed` or `Destroyed_Compromised`. + /// + /// This is O(N) over the keyspace and decrypts every object — it is + /// expensive by design and must only be called once (when the counter key is + /// absent). After this call the incremental counter path takes over. + /// + /// # Decryption errors + /// + /// A single corrupt or foreign blob does not abort the scan: it is skipped + /// with a `debug!` log. The 30-second cron sync will re-run bootstrap if + /// the counter key is ever lost (e.g. after `FLUSHDB` in tests). + pub(crate) async fn scan_count_non_destroyed(&self) -> DbResult { + let mut count: u64 = 0; + let mut cursor: u64 = 0; + loop { + // SCAN cursor MATCH do::* COUNT hint + let (next_cursor, keys): (u64, Vec) = redis::cmd("SCAN") + .arg(cursor) + .arg("MATCH") + .arg("do::*") + .arg("COUNT") + .arg(SCAN_BATCH_HINT) + .query_async(&mut self.mgr.clone()) + .await?; + + if !keys.is_empty() { + // Pipeline-GET all values in this batch (one round-trip). + let mut pipeline = pipe(); + for key in &keys { + pipeline.get(key); + } + let values: Vec> = pipeline.query_async(&mut self.mgr.clone()).await?; + + for (key, ciphertext) in keys.iter().zip(values) { + if ciphertext.is_empty() { + // Key disappeared between SCAN and GET — harmless. + continue; + } + // Strip the "do::" prefix to recover the raw UID used as + // AEAD additional data during encryption. + let uid = key.strip_prefix("do::").unwrap_or(key.as_str()); + match self.decrypt_object(uid, &ciphertext) { + Ok(obj) => { + if !matches!(obj.state, State::Destroyed | State::Destroyed_Compromised) + { + count += 1; + } + } + Err(e) => { + // Skip corrupted / foreign blobs rather than + // aborting the entire count. + debug!("[redis-bootstrap] skipping key {key}: {e}"); + } + } + } + } + + cursor = next_cursor; + if cursor == 0 { + break; + } + } + Ok(count) + } +} diff --git a/crate/server_database/src/stores/redis/redis_with_findex.rs b/crate/server_database/src/stores/redis/redis_with_findex.rs index 8eff9823e5..aff003b5cc 100644 --- a/crate/server_database/src/stores/redis/redis_with_findex.rs +++ b/crate/server_database/src/stores/redis/redis_with_findex.rs @@ -23,7 +23,9 @@ use uuid::Uuid; use super::{ FINDEX_KEY_LENGTH, - objects_db::{DB_KEY_LENGTH, ObjectsDB, RedisDbObject, keywords_from_attributes}, + objects_db::{ + DB_KEY_LENGTH, LIVE_COUNT_KEY, ObjectsDB, RedisDbObject, keywords_from_attributes, + }, permissions::PermissionDB, }; use crate::{ @@ -43,6 +45,15 @@ use crate::{ const REDIS_WITH_FINDEX_MASTER_KEY_DERIVATION_SALT: &[u8; 16] = b"rediswithfindex_"; const REDIS_WITH_FINDEX_MASTER_DB_KEY_DERIVATION_SALT: &[u8; 2] = b"db"; +/// Returns `true` when an object's state counts toward the live-object total. +/// +/// `Destroyed` and `Destroyed_Compromised` are terminal states — the object is +/// no longer usable and should not appear in the `kms.objects.total` gauge. +#[inline] +const fn is_live(state: State) -> bool { + !matches!(state, State::Destroyed | State::Destroyed_Compromised) +} + /// Derive a Redis Master Key from a password pub fn redis_master_key_from_password( master_password: &str, @@ -329,8 +340,9 @@ impl ObjectsStore for RedisWithFindex { .prepare_object_for_create(uid, owner, object, attributes, tags) .await?; - // create the object self.objects_db.object_create(&uid, &db_object).await?; + // New objects are always PreActive (live) — increment unconditionally. + self.objects_db.adjust_live_count(1).await?; Ok(uid) } @@ -381,10 +393,26 @@ impl ObjectsStore for RedisWithFindex { } async fn update_state(&self, uid: &str, state: State) -> InterfaceResult<()> { + // Read the object once here so we can: + // 1. Capture the old state for the counter delta. + // 2. Pass it as `existing` to avoid a second object_get inside + // prepare_object_for_state_update. + let existing = self.objects_db.object_get(uid).await?; + let old_state = existing.as_ref().map(|o| o.state); + let db_object = self - .prepare_object_for_state_update(uid, state, None) + .prepare_object_for_state_update(uid, state, existing) .await?; self.objects_db.object_upsert(uid, &db_object).await?; + + // Adjust counter only when the liveness crosses a boundary: + // live → destroyed → -1 + // destroyed → live → +1 + // no boundary cross → 0 + if let Some(old) = old_state { + let delta = i64::from(is_live(state)) - i64::from(is_live(old)); + self.objects_db.adjust_live_count(delta).await?; + } Ok(()) } @@ -393,6 +421,11 @@ impl ObjectsStore for RedisWithFindex { self.delete_findex_keywords(uid, &db_object.keywords()) .await?; self.objects_db.object_delete(uid).await?; + // Only decrement for live objects — destroying an already-destroyed + // object must not double-decrement the counter. + if is_live(db_object.state) { + self.objects_db.adjust_live_count(-1).await?; + } } Ok(()) } @@ -408,9 +441,26 @@ impl ObjectsStore for RedisWithFindex { // would clobber previous modifications for the same UID. let mut pending: HashMap = HashMap::new(); let mut redis_operations: Vec = Vec::with_capacity(operations.len()); + // Accumulate the net live-object delta for the entire batch. We emit a + // single INCRBY at the end rather than one per operation to keep the + // counter update close to the data write. + let mut live_delta: i64 = 0; + for operation in operations { match operation { AtomicOperation::Upsert((uid, object, attributes, tags, state)) => { + // Determine whether this Upsert is an insert (+1 if live) + // or an update (±1 on liveness boundary). + // Check pending first (already processed in this batch), then Redis. + let old_state = if let Some(p) = pending.get(uid.as_str()) { + Some(p.state) + } else { + self.objects_db.object_get(uid).await?.map(|o| o.state) + }; + let new_live = i64::from(is_live(*state)); + live_delta += + old_state.map_or(new_live, |old| new_live - i64::from(is_live(old))); + // TODO: this operation contains a non atomic retrieve_tags. It will be hard to make this whole method atomic let db_object = self .prepare_object_for_insert( @@ -426,6 +476,9 @@ impl ObjectsStore for RedisWithFindex { redis_operations.push(RedisOperation::Upsert(uid.clone(), db_object)); } AtomicOperation::Create((uid, object, attributes, tags)) => { + // New objects are always live. + live_delta += 1; + let (uid, db_object) = self .prepare_object_for_create( Some(uid.clone()), @@ -439,7 +492,9 @@ impl ObjectsStore for RedisWithFindex { redis_operations.push(RedisOperation::Create(uid, db_object)); } AtomicOperation::Delete(uid) => { - // Clean up Findex entries before deleting the object + // The existing object is read below (for Findex keyword + // cleanup); we capture its state for the counter at the same + // time — zero extra round trips. let existing = pending.remove(uid); let db_object = match existing { Some(obj) => obj, @@ -452,11 +507,15 @@ impl ObjectsStore for RedisWithFindex { } } }; + if is_live(db_object.state) { + live_delta -= 1; + } self.delete_findex_keywords(uid, &db_object.keywords()) .await?; redis_operations.push(RedisOperation::Delete(uid.clone())); } AtomicOperation::UpdateObject((uid, object, attributes, tags)) => { + // State is unchanged by UpdateObject — no counter adjustment. // TODO: this operation contains a non atomic retrieve_object. It will be hard to make this whole method atomic let existing = pending.remove(uid); let db_object = self @@ -466,17 +525,36 @@ impl ObjectsStore for RedisWithFindex { redis_operations.push(RedisOperation::Upsert(uid.clone(), db_object)); } AtomicOperation::UpdateState((uid, state)) => { - // TODO: this operation contains a non atomic retrieve_object. It will be hard to make this whole method atomic - let existing = pending.remove(uid); + // Fetch once: either from in-flight pending map or Redis. + // Pass it as `existing` to avoid a second object_get inside + // prepare_object_for_state_update. + let existing = match pending.remove(uid) { + Some(obj) => Some(obj), + None => self.objects_db.object_get(uid).await?, + }; + let old_state = existing.as_ref().map(|o| o.state); + let db_object = self .prepare_object_for_state_update(uid, *state, existing) .await?; pending.insert(uid.clone(), db_object.clone()); redis_operations.push(RedisOperation::Upsert(uid.clone(), db_object)); + + if let Some(old) = old_state { + live_delta += i64::from(is_live(*state)) - i64::from(is_live(old)); + } } } } - Ok(self.objects_db.atomic(&redis_operations).await?) + + let result = self.objects_db.atomic(&redis_operations).await?; + + // Emit a single counter adjustment for the whole batch after the data + // write succeeds. On failure the Redis transaction is rolled back and + // the counter should not move. + self.objects_db.adjust_live_count(live_delta).await?; + + Ok(result) } /// Test if an object identified by its `uid` is currently owned by `owner` @@ -623,19 +701,33 @@ impl ObjectsStore for RedisWithFindex { .collect()) } - // TODO(redis): implement count_all_non_destroyed properly. - // - // Redis-findex stores objects as individual keys with prefix `do::`. - // A correct implementation would use `SCAN do::*` and then retrieve and - // filter each object's state field. This is deferred due to the high cost - // of a full Redis key scan on large datasets and because Redis-findex is - // a non-FIPS backend with lower CI priority. - // - // Override the trait default (which logs a warning) with a silent `Ok(0)` - // because Redis intentionally defers this implementation — the warning - // would be misleading and noisy (fired every 30 s from the metrics cron). + /// Return the count of live (non-destroyed) objects. + /// + /// # Fast path (steady state) + /// + /// Reads a single counter key `kms::metrics::live_object_count` that is + /// incremented/decremented in sync with every mutating operation in this + /// backend. Cost: one O(1) `GET`, no decryption. + /// + /// # Bootstrap (first boot or after `FLUSHDB`) + /// + /// When the counter key is absent (counter has never been initialised or + /// was manually deleted), a one-time SCAN of all `do::*` keys is performed, + /// each blob is decrypted, and the result is written to the counter key. + /// Subsequent calls return the fast path immediately. async fn count_all_non_destroyed(&self) -> InterfaceResult { - Ok(0) + // Fast path: counter key already exists. + if let Some(count) = self.objects_db.get_live_count().await? { + return Ok(count); + } + // Bootstrap: scan + decrypt once to establish the baseline. + let count = self.objects_db.scan_count_non_destroyed().await?; + self.objects_db.set_live_count(count).await?; + debug!( + "[redis-metrics] bootstrapped {} live object(s) into `{}`", + count, LIVE_COUNT_KEY + ); + Ok(count) } } diff --git a/crate/server_database/src/tests/mod.rs b/crate/server_database/src/tests/mod.rs index 323336a482..ca8b7db278 100644 --- a/crate/server_database/src/tests/mod.rs +++ b/crate/server_database/src/tests/mod.rs @@ -21,7 +21,7 @@ use self::{ use crate::stores::RedisWithFindex; #[cfg(feature = "non-fips")] use crate::stores::additional_redis_findex_tests::{ - test_corner_case, test_objects_db, test_permissions_db, + test_corner_case, test_live_count_counter, test_objects_db, test_permissions_db, }; use crate::{ error::DbResult, @@ -97,6 +97,7 @@ pub(crate) async fn test_db_redis_with_findex() -> DbResult<()> { test_objects_db().await?; test_permissions_db().await?; test_corner_case().await?; + test_live_count_counter().await?; Box::pin(json_access(&get_redis_with_findex().await?)).await?; find_attributes(&get_redis_with_findex().await?).await?; owner(&get_redis_with_findex().await?).await?; From 008aff571f7cd958e0ad85ab52473865e1656f19 Mon Sep 17 00:00:00 2001 From: HatemMn <19950216+HatemMn@users.noreply.github.com> Date: Fri, 5 Jun 2026 10:31:30 +0200 Subject: [PATCH 05/28] feat: step 3.bis OK --- CHANGELOG/feat_richOTELmetrics.md | 39 ++++++ crate/interfaces/src/hsm/hsm_store.rs | 31 +++++ crate/interfaces/src/stores/objects_store.rs | 32 +++++ crate/server/src/core/kms/mod.rs | 10 ++ crate/server/src/core/otel_metrics.rs | 6 +- crate/server/src/cron.rs | 65 ++++------ .../src/core/database_objects.rs | 33 +++++ .../redis/additional_redis_findex_tests.rs | 120 ++++++++++++++++- .../src/stores/redis/objects_db.rs | 101 +++++++++++++++ .../src/stores/redis/redis_with_findex.rs | 122 +++++++++++++++++- crate/server_database/src/stores/sql/mysql.rs | 15 +++ crate/server_database/src/stores/sql/pgsql.rs | 14 ++ .../server_database/src/stores/sql/query.sql | 20 +++ .../src/stores/sql/query_mysql.sql | 10 ++ .../server_database/src/stores/sql/sqlite.rs | 14 ++ crate/server_database/src/tests/mod.rs | 4 +- 16 files changed, 588 insertions(+), 48 deletions(-) diff --git a/CHANGELOG/feat_richOTELmetrics.md b/CHANGELOG/feat_richOTELmetrics.md index 02fe9fa4cd..27d79a43a7 100644 --- a/CHANGELOG/feat_richOTELmetrics.md +++ b/CHANGELOG/feat_richOTELmetrics.md @@ -72,4 +72,43 @@ - **Test** (`test_live_count_counter`): 6-step integration test covering create → destroy → delete-live → delete-destroyed → fast-path count → bootstrap-SCAN count. Registered in `test_db_redis_with_findex`. + Tagged `#[ignore = "Requires a running Redis instance"]`. + +### Active Key Count Metric — `kms.keys.active.count` V2 (Step 3.bis) + +Replace the previous Locate-based implementation with a privileged backend count +that returns the number of non-destroyed key objects (`SymmetricKey`, `PrivateKey`, +`PublicKey`, `SplitKey`) across all backends. "Non-destroyed" means the object +state is not `Destroyed` or `Destroyed_Compromised`. + +- **`ObjectsStore` trait** (`crate/interfaces`): add two new default methods — + `count_non_destroyed_keys()` (default: `warn!` + `Ok(0)`) and `reconcile_counts()` + (default: no-op `Ok(())`), keeping all existing backends compilable without changes. +- **SQL backends** (SQLite / PostgreSQL / MySQL): add named SQL queries + (`count-non-destroyed-keys-sqlite`, `count-non-destroyed-keys-pg`, + `count-non-destroyed-keys`) that filter `objects` by `ObjectType IN (...)` and + `state NOT IN ('Destroyed', 'Destroyed_Compromised')` using backend-specific + JSON extraction syntax. +- **HSM store**: implement `count_non_destroyed_keys` by iterating all available + slots, calling `hsm.find(slot_id, HsmObjectFilter::Any)`, and summing slot lengths. + All HSM objects are considered non-destroyed active keys. +- **Redis-findex — `ObjectsDB`**: add O(1) counter key `kms::metrics::active_key_count` + with helpers `adjust_active_key_count(delta)`, `get_active_key_count()`, + `set_active_key_count(count)`, and `scan_count_non_destroyed_keys()` (bootstrap SCAN). +- **Redis-findex — `RedisWithFindex`**: add `is_key_type(ObjectType) -> bool` helper; + wire `adjust_active_key_count` into `create`, `update_state`, `delete`, and `atomic` + mirroring the existing `live_delta` pattern. Implement `count_non_destroyed_keys` + (O(1) fast path / bootstrap SCAN fallback) and `reconcile_counts` (scans both + `live_object_count` and `active_key_count` counters atomically). +- **Database facade** (`crate/server_database`): add `count_non_destroyed_key_objects()` + and `reconcile_all_object_counts()` aggregating across all registered stores. +- **Cron** (`crate/server/src/cron.rs`): remove dead Locate-based implementation; + replace with `database.count_non_destroyed_key_objects()` on the 30-second tick and + a 5-minute `reconcile_all_object_counts()` tick for counter drift correction. +- **Startup seed** (`crate/server/src/core/kms/mod.rs`): seed `kms.keys.active.count` + at startup from `count_non_destroyed_key_objects()` (non-fatal, same pattern as + `kms.objects.total`). +- **Test** (`test_active_key_count_counter`): 6-step integration test covering + create 2 keys, create non-key object (OpaqueObject), deactivate, destroy, delete, + and bootstrap-SCAN reconcile. Registered in `test_db_redis_with_findex`. Tagged `#[ignore = "Requires a running Redis instance"]`. \ No newline at end of file diff --git a/crate/interfaces/src/hsm/hsm_store.rs b/crate/interfaces/src/hsm/hsm_store.rs index f4032b92dc..746991f7d2 100644 --- a/crate/interfaces/src/hsm/hsm_store.rs +++ b/crate/interfaces/src/hsm/hsm_store.rs @@ -384,6 +384,37 @@ impl ObjectsStore for HsmStore { async fn count_all_non_destroyed(&self) -> InterfaceResult { Ok(0) } + + /// Count non-destroyed key objects across all HSM slots. + /// + /// All objects present in an HSM are cryptographic key material and are by + /// definition non-destroyed (deleted keys are removed from the HSM). + /// Each PKCS#11 slot is queried via `find(slot, HsmObjectFilter::Any)` and + /// the returned key IDs are counted. Slot errors are non-fatal: a failed + /// slot contributes 0 and a warning is logged, so a single unavailable slot + /// does not block the aggregate count. + async fn count_non_destroyed_keys(&self) -> InterfaceResult { + let slot_ids = self + .hsm + .get_available_slot_list() + .await + .unwrap_or_else(|e| { + warn!("HSM count_non_destroyed_keys: failed to list slots: {e}"); + vec![] + }); + let mut total: u64 = 0; + for slot_id in slot_ids { + match self.hsm.find(slot_id, HsmObjectFilter::Any).await { + Ok(keys) => { + total = total.saturating_add(u64::try_from(keys.len()).unwrap_or(u64::MAX)); + } + Err(e) => { + debug!("HSM count_non_destroyed_keys: slot {slot_id} query failed: {e}"); + } + } + } + Ok(total) + } } // ────────────────────────────────────────────────────────────────────────────── diff --git a/crate/interfaces/src/stores/objects_store.rs b/crate/interfaces/src/stores/objects_store.rs index 7d3ac05327..b3b9430da6 100644 --- a/crate/interfaces/src/stores/objects_store.rs +++ b/crate/interfaces/src/stores/objects_store.rs @@ -130,4 +130,36 @@ pub trait ObjectsStore { ); Ok(0) } + + /// Returns the count of non-destroyed key objects (`SymmetricKey`, `PrivateKey`, + /// `PublicKey`, `SplitKey`) across this store. + /// + /// "Non-destroyed" means state ∉ {`Destroyed`, `Destroyed_Compromised`}. + /// This covers `PreActive`, `Active`, `Deactivated`, and `Compromised` keys — + /// all states in which the key material is still present. + /// + /// Backends should override this with a real implementation. The default + /// logs a warning and returns 0 so that the gauge shows a valid lower-bound + /// until a proper implementation is provided. + async fn count_non_destroyed_keys(&self) -> InterfaceResult { + warn!( + "count_non_destroyed_keys not implemented for this ObjectsStore backend — \ + kms.keys.active.count will read 0 until a real implementation is provided" + ); + Ok(0) + } + + /// Perform an authoritative reconciliation of any cached object-count + /// counters maintained by this store. + /// + /// For in-memory counters (e.g. Redis `INCRBY` counters) this should + /// recompute the true count from the authoritative data source and overwrite + /// the cached value. For SQL backends this is a no-op because every COUNT(*) + /// query is already authoritative. + /// + /// Called by the slow-path cron loop (every 5 minutes) to prevent counter + /// drift from accumulating due to partial failures. + async fn reconcile_counts(&self) -> InterfaceResult<()> { + Ok(()) + } } diff --git a/crate/server/src/core/kms/mod.rs b/crate/server/src/core/kms/mod.rs index 834bed7015..820869ef26 100644 --- a/crate/server/src/core/kms/mod.rs +++ b/crate/server/src/core/kms/mod.rs @@ -171,6 +171,16 @@ impl KMS { cosmian_logger::debug!("[kms-init] Failed to seed kms.objects.total: {e}"); } } + // Seed kms.keys.active.count from the real DB count on startup. + match database.count_non_destroyed_key_objects().await { + Ok(count) => { + m.update_active_keys_count(i64::try_from(count).unwrap_or(i64::MAX)); + } + Err(e) => { + // Non-fatal: the cron will correct the value within 30 s. + cosmian_logger::debug!("[kms-init] Failed to seed kms.keys.active.count: {e}"); + } + } } Ok(Self { diff --git a/crate/server/src/core/otel_metrics.rs b/crate/server/src/core/otel_metrics.rs index 23ee2a5003..f961410a0d 100644 --- a/crate/server/src/core/otel_metrics.rs +++ b/crate/server/src/core/otel_metrics.rs @@ -232,7 +232,11 @@ impl OtelMetrics { // Active keys count — gauge records the current absolute count directly let active_keys_count = meter .i64_gauge("kms.keys.active.count") - .with_description("Number of keys in Active state (absolute count based on Locate)") + .with_description( + "Number of non-destroyed key objects (SymmetricKey, PrivateKey, PublicKey, \ + SplitKey) across all backends. Counts keys in all non-terminal states: \ + PreActive, Active, Deactivated, Compromised.", + ) .with_unit("{key}") .build(); // Seed the time series so it is visible in the backend from server start. diff --git a/crate/server/src/cron.rs b/crate/server/src/cron.rs index d1ddf05756..e416e37ffa 100644 --- a/crate/server/src/cron.rs +++ b/crate/server/src/cron.rs @@ -1,9 +1,5 @@ use std::sync::Arc; -use cosmian_kms_server_database::reexport::cosmian_kmip::{ - kmip_0::kmip_types::State, - kmip_2_1::{kmip_attributes::Attributes, kmip_operations::Locate}, -}; use cosmian_logger::debug; use tokio::sync::oneshot; @@ -34,47 +30,33 @@ pub fn spawn_metrics_cron(kms: Arc) -> oneshot::Sender<()> { rt.block_on(async move { let mut interval = tokio::time::interval(std::time::Duration::from_secs(30)); let mut uptime_interval = tokio::time::interval(std::time::Duration::from_secs(1)); + let mut reconcile_interval = tokio::time::interval(std::time::Duration::from_secs(300)); let mut shutdown_rx = shutdown_rx; loop { - // TODO: this should better be deleted/updated because we are changing the approach tokio::select! { _ = interval.tick() => { - // ── Active-keys refresh (via KMIP Locate) ───────────────────────── - // NOTE: this uses a KMIP Locate filtered to Active state, which is - // subject to the caller's ACL. In multi-admin deployments this may - // undercount if the cron user does not own all keys. A future task - // should replace this with a privileged SQL count, similar to the - // objects.total path below. - let request = Locate { - attributes: Attributes { - state: Some(State::Active), - ..Default::default() - }, - ..Default::default() - }; - let user = kms.params.hsm_instances.first() - .and_then(|inst| inst.admin.iter().find(|a| a.as_str() != "*").cloned()) - .unwrap_or_else(|| kms.params.default_username.clone()); - match kms.locate(request, &user,).await { - Ok(resp) => { - let count = resp.located_items.unwrap_or(0); - debug!("[metrics-cron] Active keys count refreshed to {}", count); - if let Some(ref metrics) = kms.metrics { - metrics.update_active_keys_count(i64::from(count)); + if let Some(ref metrics) = kms.metrics { + // ── Non-destroyed key objects count ──────────────────────────────── + // Privileged backend count: no ACL filtering, covers all backends. + match kms.database.count_non_destroyed_key_objects().await { + Ok(count) => { + debug!( + "[metrics-cron] kms.keys.active.count synced to {}", + count + ); + metrics.update_active_keys_count( + i64::try_from(count).unwrap_or(i64::MAX), + ); + } + Err(e) => { + debug!( + "[metrics-cron] Failed to sync kms.keys.active.count: {}", + e + ); } } - Err(e) => { - debug!("[metrics-cron] Failed to refresh active keys count: {}", e); - } - } - // ── Objects-total absolute sync ──────────────────────────────────── - // The fast-path delta tracking (+1/-1 per operation) can accumulate - // drift over time because of the TOCTOU race in the Upsert pre-read - // and because the Redis backend always emits 0 from the fast path. - // Every 30 s we re-read the authoritative SQL count and correct the - // UpDownCounter via the mirror pattern (see OtelMetrics::update_objects_total). - if let Some(ref metrics) = kms.metrics { + // ── Objects-total absolute sync ──────────────────────────────────── match kms.database.count_all_non_destroyed_objects().await { Ok(count) => { debug!( @@ -94,6 +76,13 @@ pub fn spawn_metrics_cron(kms: Arc) -> oneshot::Sender<()> { } } } + _ = reconcile_interval.tick() => { + // Authoritative reconcile of Redis O(1) counters (no-op for SQL). + // Prevents permanent counter drift from partial failures. + if let Err(e) = kms.database.reconcile_all_object_counts().await { + debug!("[metrics-cron] reconcile_all_object_counts failed: {}", e); + } + } _ = uptime_interval.tick() => { if let Some(ref metrics) = kms.metrics { metrics.update_uptime(); diff --git a/crate/server_database/src/core/database_objects.rs b/crate/server_database/src/core/database_objects.rs index 56603d9cea..20623df885 100644 --- a/crate/server_database/src/core/database_objects.rs +++ b/crate/server_database/src/core/database_objects.rs @@ -455,6 +455,39 @@ impl Database { } Ok(total) } + + /// Return the total count of non-destroyed key objects (`SymmetricKey`, `PrivateKey`, + /// `PublicKey`, `SplitKey`) across all registered stores. + /// + /// Aggregates results from every registered backend (SQL stores, HSM stores, etc.). + /// Backends that have not yet implemented `count_non_destroyed_keys` return `0` via + /// the trait default — the sum remains a valid lower bound. + pub async fn count_non_destroyed_key_objects(&self) -> DbResult { + let map = self.objects.read().await; + let mut total: u64 = 0; + for store in map.values() { + let n = store.count_non_destroyed_keys().await.unwrap_or(0); // A single backend failure must not block the aggregate + total = total.saturating_add(n); + } + Ok(total) + } + + /// Perform an authoritative reconciliation of cached object-count counters + /// across all registered stores. + /// + /// SQL backends are no-ops (every COUNT query is authoritative). + /// Redis backends recompute counts from a full SCAN and overwrite cached keys. + /// Called by the slow-path cron loop (every 5 minutes) to prevent counter drift. + pub async fn reconcile_all_object_counts(&self) -> DbResult<()> { + let map = self.objects.read().await; + for store in map.values() { + if let Err(e) = store.reconcile_counts().await { + // Non-fatal: log and continue so one failing backend does not block others. + cosmian_logger::warn!("[database] reconcile_counts failed for a store: {e}"); + } + } + Ok(()) + } } #[cfg(test)] diff --git a/crate/server_database/src/stores/redis/additional_redis_findex_tests.rs b/crate/server_database/src/stores/redis/additional_redis_findex_tests.rs index 8b6e8b21f5..dbd6e04d4f 100644 --- a/crate/server_database/src/stores/redis/additional_redis_findex_tests.rs +++ b/crate/server_database/src/stores/redis/additional_redis_findex_tests.rs @@ -6,8 +6,12 @@ use cosmian_kmip::{ KmipResultHelper, kmip_0::kmip_types::State, kmip_2_1::{ - KmipOperation, extra::tagging::VENDOR_ID_COSMIAN, kmip_attributes::Attributes, - kmip_types::CryptographicAlgorithm, requests::create_symmetric_key_kmip_object, + KmipOperation, + extra::tagging::VENDOR_ID_COSMIAN, + kmip_attributes::Attributes, + kmip_objects::{Object, OpaqueObject}, + kmip_types::{CryptographicAlgorithm, OpaqueDataType}, + requests::create_symmetric_key_kmip_object, }, }; use cosmian_kms_crypto::reexport::cosmian_crypto_core::{ @@ -24,7 +28,7 @@ use crate::{ REDIS_WITH_FINDEX_MASTER_KEY_LENGTH, RedisWithFindex, redis::{ init_findex_redis, - objects_db::{LIVE_COUNT_KEY, ObjectsDB, RedisDbObject}, + objects_db::{ACTIVE_KEY_COUNT_KEY, LIVE_COUNT_KEY, ObjectsDB, RedisDbObject}, permissions::{ObjectUid, PermissionDB, UserId}, }, }, @@ -482,3 +486,113 @@ pub(crate) async fn test_live_count_counter() -> DbResult<()> { Ok(()) } + +/// Verify that `ACTIVE_KEY_COUNT_KEY` is maintained correctly across the +/// full lifecycle of key and non-key objects. +/// +/// Steps: +/// 1. Create 2 `SymmetricKey` objects → counter = 2. +/// 2. Create an `OpaqueObject` → counter must stay at 2 (not a key type). +/// 3. `update_state` key1 → Deactivated → counter stays at 2 (still non-destroyed). +/// 4. `update_state` key1 → Destroyed → counter drops to 1. +/// 5. `delete` key2 (live) → counter drops to 0. +/// 6. Bootstrap SCAN path: delete the counter key, call `count_non_destroyed_keys`, +/// expect it to return 0 and persist the counter. +pub(crate) async fn test_active_key_count_counter() -> DbResult<()> { + cosmian_logger::log_init(option_env!("RUST_LOG")); + + let mut rng = CsRng::from_entropy(); + let redis_url = get_redis_url(); + let master_key = Secret::::random(&mut rng); + let db = RedisWithFindex::instantiate(&redis_url, master_key, true).await?; + + // ── Step 1: create 2 SymmetricKey objects ──────────────────────────────── + let mut key_bytes = vec![0_u8; 32]; + rng.fill_bytes(&mut key_bytes); + let key1 = create_symmetric_key_kmip_object( + VENDOR_ID_COSMIAN, + &key_bytes, + &Attributes { + cryptographic_algorithm: Some(CryptographicAlgorithm::AES), + ..Default::default() + }, + )?; + rng.fill_bytes(&mut key_bytes); + let key2 = create_symmetric_key_kmip_object( + VENDOR_ID_COSMIAN, + &key_bytes, + &Attributes { + cryptographic_algorithm: Some(CryptographicAlgorithm::AES), + ..Default::default() + }, + )?; + + let uid_key1 = db + .create(None, "owner", &key1, key1.attributes()?, &HashSet::new()) + .await?; + let uid_key2 = db + .create(None, "owner", &key2, key2.attributes()?, &HashSet::new()) + .await?; + + let raw: Option = db.mgr.clone().get(ACTIVE_KEY_COUNT_KEY).await?; + assert_eq!(raw, Some(2), "counter should be 2 after creating 2 keys"); + + // ── Step 2: create an OpaqueObject → counter must NOT increment ────────── + let opaque = Object::OpaqueObject(OpaqueObject { + opaque_data_type: OpaqueDataType::Unknown, + opaque_data_value: b"test-opaque".to_vec(), + }); + let _uid_opaque = db + .create( + None, + "owner", + &opaque, + &Attributes::default(), + &HashSet::new(), + ) + .await?; + let raw: Option = db.mgr.clone().get(ACTIVE_KEY_COUNT_KEY).await?; + assert_eq!( + raw, + Some(2), + "creating an OpaqueObject must not increment the key counter" + ); + + // ── Step 3: Deactivate key1 → counter stays at 2 (still non-destroyed) ─── + db.update_state(&uid_key1, State::Deactivated).await?; + let raw: Option = db.mgr.clone().get(ACTIVE_KEY_COUNT_KEY).await?; + assert_eq!( + raw, + Some(2), + "Deactivated key is non-destroyed and must still be counted" + ); + + // ── Step 4: Destroy key1 → counter drops to 1 ─────────────────────────── + db.update_state(&uid_key1, State::Destroyed).await?; + let raw: Option = db.mgr.clone().get(ACTIVE_KEY_COUNT_KEY).await?; + assert_eq!(raw, Some(1), "counter should be 1 after destroying key1"); + + // ── Step 5: delete key2 (live) → counter drops to 0 ───────────────────── + db.delete(&uid_key2).await?; + let raw: Option = db.mgr.clone().get(ACTIVE_KEY_COUNT_KEY).await?; + assert_eq!(raw, Some(0), "counter should be 0 after deleting key2"); + + // ── Step 6: bootstrap SCAN path ───────────────────────────────────────── + // Delete the counter key to simulate a first-boot / FLUSHDB situation. + redis::cmd("DEL") + .arg(ACTIVE_KEY_COUNT_KEY) + .query_async::<()>(&mut db.mgr.clone()) + .await?; + + // count_non_destroyed_keys must fall back to SCAN, count 0 non-destroyed + // key objects, write the counter key, and return 0. + let n = db.count_non_destroyed_keys().await?; + assert_eq!( + n, 0, + "count_non_destroyed_keys (bootstrap SCAN) should return 0" + ); + let raw: Option = db.mgr.clone().get(ACTIVE_KEY_COUNT_KEY).await?; + assert_eq!(raw, Some(0), "bootstrap must persist the counter to Redis"); + + Ok(()) +} diff --git a/crate/server_database/src/stores/redis/objects_db.rs b/crate/server_database/src/stores/redis/objects_db.rs index acd14ee7cb..d68ad87edf 100644 --- a/crate/server_database/src/stores/redis/objects_db.rs +++ b/crate/server_database/src/stores/redis/objects_db.rs @@ -340,6 +340,10 @@ pub(crate) enum RedisOperation { /// Redis key that stores the count of live (non-destroyed) objects. pub(crate) const LIVE_COUNT_KEY: &str = "kms::metrics::live_object_count"; +/// Redis key that stores the count of non-destroyed key objects +/// (`SymmetricKey`, `PrivateKey`, `PublicKey`, `SplitKey`). +pub(crate) const ACTIVE_KEY_COUNT_KEY: &str = "kms::metrics::active_key_count"; + /// SCAN batch hint passed to Redis. Redis may return more or fewer keys per /// batch; `200` is a pragmatic balance between round-trips and command latency. const SCAN_BATCH_HINT: u64 = 200; @@ -447,4 +451,101 @@ impl ObjectsDB { } Ok(count) } + + /// Atomically adjust the active-key counter by `delta`. + /// + /// "Active key" means a non-destroyed key object (`ObjectType` ∈ {`SymmetricKey`, + /// `PrivateKey`, `PublicKey`, `SplitKey`}, state ∉ {`Destroyed`, `Destroyed_Compromised`}). + /// Uses `INCRBY`; the key is auto-created at `delta` if absent — the cron + /// reconcile will correct it on the next tick. + pub(crate) async fn adjust_active_key_count(&self, delta: i64) -> DbResult<()> { + if delta == 0 { + return Ok(()); + } + self.mgr + .clone() + .incr::<_, i64, i64>(ACTIVE_KEY_COUNT_KEY, delta) + .await?; + Ok(()) + } + + /// Return the current active-key count, or `None` if the key has never + /// been set (i.e. the bootstrap scan has not yet run). + pub(crate) async fn get_active_key_count(&self) -> DbResult> { + let raw: Option = self.mgr.clone().get(ACTIVE_KEY_COUNT_KEY).await?; + Ok(raw.map(|n| u64::try_from(n.max(0)).unwrap_or(0))) + } + + /// Overwrite the active-key counter with an absolute value. + /// + /// Called once during bootstrap and by the reconcile path. + pub(crate) async fn set_active_key_count(&self, count: u64) -> DbResult<()> { + self.mgr + .clone() + .set::<_, _, ()>(ACTIVE_KEY_COUNT_KEY, count) + .await?; + Ok(()) + } + + /// One-time bootstrap: scan every `do::*` key, decrypt each blob, and count + /// objects that are both a key type (`SymmetricKey`, `PrivateKey`, `PublicKey`, + /// `SplitKey`) **and** non-destroyed. + /// + /// Same cost and error-handling semantics as `scan_count_non_destroyed`. + pub(crate) async fn scan_count_non_destroyed_keys(&self) -> DbResult { + let mut count: u64 = 0; + let mut cursor: u64 = 0; + loop { + let (next_cursor, keys): (u64, Vec) = redis::cmd("SCAN") + .arg(cursor) + .arg("MATCH") + .arg("do::*") + .arg("COUNT") + .arg(SCAN_BATCH_HINT) + .query_async(&mut self.mgr.clone()) + .await?; + + if !keys.is_empty() { + let mut pipeline = pipe(); + for key in &keys { + pipeline.get(key); + } + let values: Vec> = pipeline.query_async(&mut self.mgr.clone()).await?; + + for (key, ciphertext) in keys.iter().zip(values) { + if ciphertext.is_empty() { + continue; + } + let uid = key.strip_prefix("do::").unwrap_or(key.as_str()); + match self.decrypt_object(uid, &ciphertext) { + Ok(obj) => { + let is_key = matches!( + obj.object_type, + ObjectType::SymmetricKey + | ObjectType::PrivateKey + | ObjectType::PublicKey + | ObjectType::SplitKey + ); + let is_non_destroyed = !matches!( + obj.state, + State::Destroyed | State::Destroyed_Compromised + ); + if is_key && is_non_destroyed { + count += 1; + } + } + Err(e) => { + debug!("[redis-bootstrap] skipping key {key}: {e}"); + } + } + } + } + + cursor = next_cursor; + if cursor == 0 { + break; + } + } + Ok(count) + } } diff --git a/crate/server_database/src/stores/redis/redis_with_findex.rs b/crate/server_database/src/stores/redis/redis_with_findex.rs index aff003b5cc..16cb7530c7 100644 --- a/crate/server_database/src/stores/redis/redis_with_findex.rs +++ b/crate/server_database/src/stores/redis/redis_with_findex.rs @@ -7,7 +7,11 @@ use async_trait::async_trait; use cosmian_findex::{Findex, IndexADT, MemoryEncryptionLayer, generic_decode, generic_encode}; use cosmian_kmip::{ kmip_0::kmip_types::State, - kmip_2_1::{KmipOperation, kmip_attributes::Attributes, kmip_objects::Object}, + kmip_2_1::{ + KmipOperation, + kmip_attributes::Attributes, + kmip_objects::{Object, ObjectType}, + }, }; use cosmian_kms_crypto::{ crypto::password_derivation::derive_key_from_password, @@ -24,7 +28,8 @@ use uuid::Uuid; use super::{ FINDEX_KEY_LENGTH, objects_db::{ - DB_KEY_LENGTH, LIVE_COUNT_KEY, ObjectsDB, RedisDbObject, keywords_from_attributes, + ACTIVE_KEY_COUNT_KEY, DB_KEY_LENGTH, LIVE_COUNT_KEY, ObjectsDB, RedisDbObject, + keywords_from_attributes, }, permissions::PermissionDB, }; @@ -54,6 +59,21 @@ const fn is_live(state: State) -> bool { !matches!(state, State::Destroyed | State::Destroyed_Compromised) } +/// Returns `true` when `object_type` is a key type counted by `kms.keys.active.count`. +/// +/// Key types: `SymmetricKey`, `PrivateKey`, `PublicKey`, `SplitKey`. +/// Excluded: `Certificate`, `SecretData`, `OpaqueObject`, `PGPKey`, `CertificateRequest`. +#[inline] +const fn is_key_type(object_type: ObjectType) -> bool { + matches!( + object_type, + ObjectType::SymmetricKey + | ObjectType::PrivateKey + | ObjectType::PublicKey + | ObjectType::SplitKey + ) +} + /// Derive a Redis Master Key from a password pub fn redis_master_key_from_password( master_password: &str, @@ -343,6 +363,10 @@ impl ObjectsStore for RedisWithFindex { self.objects_db.object_create(&uid, &db_object).await?; // New objects are always PreActive (live) — increment unconditionally. self.objects_db.adjust_live_count(1).await?; + // New key objects are non-destroyed by definition — increment the key counter. + if is_key_type(db_object.object_type) { + self.objects_db.adjust_active_key_count(1).await?; + } Ok(uid) } @@ -412,6 +436,17 @@ impl ObjectsStore for RedisWithFindex { if let Some(old) = old_state { let delta = i64::from(is_live(state)) - i64::from(is_live(old)); self.objects_db.adjust_live_count(delta).await?; + // Mirror the same boundary check for the key counter. + if is_key_type(db_object.object_type) { + let key_delta = i64::from(!matches!( + state, + State::Destroyed | State::Destroyed_Compromised + )) - i64::from(!matches!( + old, + State::Destroyed | State::Destroyed_Compromised + )); + self.objects_db.adjust_active_key_count(key_delta).await?; + } } Ok(()) } @@ -426,6 +461,10 @@ impl ObjectsStore for RedisWithFindex { if is_live(db_object.state) { self.objects_db.adjust_live_count(-1).await?; } + // Decrement the key counter if this was a non-destroyed key object. + if is_key_type(db_object.object_type) && is_live(db_object.state) { + self.objects_db.adjust_active_key_count(-1).await?; + } } Ok(()) } @@ -445,6 +484,8 @@ impl ObjectsStore for RedisWithFindex { // single INCRBY at the end rather than one per operation to keep the // counter update close to the data write. let mut live_delta: i64 = 0; + // Accumulate the net active-key delta (non-destroyed key objects) for the batch. + let mut active_key_delta: i64 = 0; for operation in operations { match operation { @@ -452,11 +493,13 @@ impl ObjectsStore for RedisWithFindex { // Determine whether this Upsert is an insert (+1 if live) // or an update (±1 on liveness boundary). // Check pending first (already processed in this batch), then Redis. - let old_state = if let Some(p) = pending.get(uid.as_str()) { - Some(p.state) + let old_obj = if let Some(p) = pending.get(uid.as_str()) { + Some(p.clone()) } else { - self.objects_db.object_get(uid).await?.map(|o| o.state) + self.objects_db.object_get(uid).await? }; + let old_state = old_obj.as_ref().map(|o| o.state); + let old_object_type = old_obj.as_ref().map(|o| o.object_type); let new_live = i64::from(is_live(*state)); live_delta += old_state.map_or(new_live, |old| new_live - i64::from(is_live(old))); @@ -472,6 +515,19 @@ impl ObjectsStore for RedisWithFindex { *state, ) .await?; + // Accumulate key-counter delta. Use the resolved object_type + // from the newly built db_object (covers both insert and update). + let obj_type = db_object.object_type; + if is_key_type(obj_type) { + let new_key_live = i64::from(is_live(*state)); + // For an existing object we compare old vs new liveness. + // For a new insert (no old state) we use new_key_live directly. + let old_key_live = old_object_type + .filter(|ot| is_key_type(*ot)) + .and(old_state) + .map_or(0, |old| i64::from(is_live(old))); + active_key_delta += new_key_live - old_key_live; + } pending.insert(uid.clone(), db_object.clone()); redis_operations.push(RedisOperation::Upsert(uid.clone(), db_object)); } @@ -488,6 +544,10 @@ impl ObjectsStore for RedisWithFindex { tags, ) .await?; + // New key objects are always non-destroyed. + if is_key_type(db_object.object_type) { + active_key_delta += 1; + } pending.insert(uid.clone(), db_object.clone()); redis_operations.push(RedisOperation::Create(uid, db_object)); } @@ -510,6 +570,9 @@ impl ObjectsStore for RedisWithFindex { if is_live(db_object.state) { live_delta -= 1; } + if is_key_type(db_object.object_type) && is_live(db_object.state) { + active_key_delta -= 1; + } self.delete_findex_keywords(uid, &db_object.keywords()) .await?; redis_operations.push(RedisOperation::Delete(uid.clone())); @@ -533,6 +596,7 @@ impl ObjectsStore for RedisWithFindex { None => self.objects_db.object_get(uid).await?, }; let old_state = existing.as_ref().map(|o| o.state); + let object_type = existing.as_ref().map(|o| o.object_type); let db_object = self .prepare_object_for_state_update(uid, *state, existing) @@ -542,6 +606,12 @@ impl ObjectsStore for RedisWithFindex { if let Some(old) = old_state { live_delta += i64::from(is_live(*state)) - i64::from(is_live(old)); + if let Some(ot) = object_type { + if is_key_type(ot) { + active_key_delta += + i64::from(is_live(*state)) - i64::from(is_live(old)); + } + } } } } @@ -553,6 +623,9 @@ impl ObjectsStore for RedisWithFindex { // write succeeds. On failure the Redis transaction is rolled back and // the counter should not move. self.objects_db.adjust_live_count(live_delta).await?; + self.objects_db + .adjust_active_key_count(active_key_delta) + .await?; Ok(result) } @@ -729,6 +802,45 @@ impl ObjectsStore for RedisWithFindex { ); Ok(count) } + + /// Count non-destroyed key objects (`SymmetricKey`, `PrivateKey`, `PublicKey`, `SplitKey`). + /// + /// # Fast path + /// When `ACTIVE_KEY_COUNT_KEY` exists: one O(1) `GET`, no decryption. + /// + /// # Bootstrap + /// When the key is absent (first boot or after `FLUSHDB`): scans all `do::*` + /// keys, decrypts each, filters by key type and non-destroyed state, writes + /// the result to `ACTIVE_KEY_COUNT_KEY`, and returns the count. + async fn count_non_destroyed_keys(&self) -> InterfaceResult { + if let Some(count) = self.objects_db.get_active_key_count().await? { + return Ok(count); + } + let count = self.objects_db.scan_count_non_destroyed_keys().await?; + self.objects_db.set_active_key_count(count).await?; + debug!( + "[redis-metrics] bootstrapped {} non-destroyed key(s) into `{}`", + count, ACTIVE_KEY_COUNT_KEY + ); + Ok(count) + } + + /// Authoritative reconcile: recompute both Redis counter keys from a full + /// SCAN and overwrite the cached values. + /// + /// Called by the slow-path cron loop (every 5 minutes) to prevent counter + /// drift from accumulating due to partial failures. The O(N) scan cost is + /// acceptable at that frequency. + async fn reconcile_counts(&self) -> InterfaceResult<()> { + let live_count = self.objects_db.scan_count_non_destroyed().await?; + self.objects_db.set_live_count(live_count).await?; + let key_count = self.objects_db.scan_count_non_destroyed_keys().await?; + self.objects_db.set_active_key_count(key_count).await?; + debug!( + "[redis-metrics] reconcile: live_objects={live_count}, non_destroyed_keys={key_count}" + ); + Ok(()) + } } #[async_trait(?Send)] diff --git a/crate/server_database/src/stores/sql/mysql.rs b/crate/server_database/src/stores/sql/mysql.rs index 00a4a5b73c..9cea9cfc84 100644 --- a/crate/server_database/src/stores/sql/mysql.rs +++ b/crate/server_database/src/stores/sql/mysql.rs @@ -649,6 +649,21 @@ impl ObjectsStore for MySqlPool { .unwrap_or(0); Ok(count) } + + async fn count_non_destroyed_keys(&self) -> InterfaceResult { + let sql = get_mysql_query!("count-non-destroyed-keys"); + let mut conn = self + .get_configured_conn() + .await + .map_err(InterfaceError::from)?; + let count: u64 = conn + .exec_first(sql, ()) + .await + .map_err(DbError::from) + .map_err(InterfaceError::from)? + .unwrap_or(0); + Ok(count) + } } #[async_trait(?Send)] diff --git a/crate/server_database/src/stores/sql/pgsql.rs b/crate/server_database/src/stores/sql/pgsql.rs index 82925a1626..02ff643033 100644 --- a/crate/server_database/src/stores/sql/pgsql.rs +++ b/crate/server_database/src/stores/sql/pgsql.rs @@ -812,6 +812,20 @@ impl ObjectsStore for PgPool { let count: i64 = row.get(0); Ok(u64::try_from(count).unwrap_or(0)) } + + async fn count_non_destroyed_keys(&self) -> InterfaceResult { + let sql = get_pgsql_query!("count-non-destroyed-keys-pg"); + let client = pg_get_client(&self.pool) + .await + .map_err(InterfaceError::from)?; + let row = client + .query_one(sql, &[]) + .await + .map_err(DbError::from) + .map_err(InterfaceError::from)?; + let count: i64 = row.get(0); + Ok(u64::try_from(count).unwrap_or(0)) + } } #[async_trait(?Send)] diff --git a/crate/server_database/src/stores/sql/query.sql b/crate/server_database/src/stores/sql/query.sql index 9c09c3fa05..72e1ef1d52 100644 --- a/crate/server_database/src/stores/sql/query.sql +++ b/crate/server_database/src/stores/sql/query.sql @@ -88,6 +88,26 @@ INSERT INTO objects (id, object, attributes, state, owner) VALUES ($1, $2, $3, $ SELECT COUNT(*) FROM objects WHERE state NOT IN ('Destroyed', 'Destroyed_Compromised'); +-- name: count-non-destroyed-keys-sqlite +-- Privileged metrics-only query: counts non-destroyed key objects (SQLite). +-- ObjectType is stored as a JSON field inside the 'attributes' column +-- (serialised via serde with rename_all = "PascalCase"). +-- Key object types: SymmetricKey, PrivateKey, PublicKey, SplitKey. +-- All states except Destroyed / Destroyed_Compromised are counted. +SELECT COUNT(*) FROM objects +WHERE state NOT IN ('Destroyed', 'Destroyed_Compromised') +AND json_extract(attributes, '$.ObjectType') IN ('SymmetricKey', 'PrivateKey', 'PublicKey', 'SplitKey'); + +-- name: count-non-destroyed-keys-pg +-- Privileged metrics-only query: counts non-destroyed key objects (PostgreSQL). +-- ObjectType is stored as a JSONB field inside the 'attributes' column +-- (serialised via serde with rename_all = "PascalCase"). +-- Key object types: SymmetricKey, PrivateKey, PublicKey, SplitKey. +-- All states except Destroyed / Destroyed_Compromised are counted. +SELECT COUNT(*) FROM objects +WHERE state NOT IN ('Destroyed', 'Destroyed_Compromised') +AND attributes->>'ObjectType' IN ('SymmetricKey', 'PrivateKey', 'PublicKey', 'SplitKey'); + -- name: select-user-accesses-for-object SELECT permissions FROM read_access diff --git a/crate/server_database/src/stores/sql/query_mysql.sql b/crate/server_database/src/stores/sql/query_mysql.sql index 5f6b14f63d..f0e1f18bf9 100644 --- a/crate/server_database/src/stores/sql/query_mysql.sql +++ b/crate/server_database/src/stores/sql/query_mysql.sql @@ -78,6 +78,16 @@ FROM tags; SELECT COUNT(*) FROM objects WHERE state NOT IN ('Destroyed', 'Destroyed_Compromised'); +-- name: count-non-destroyed-keys +-- Privileged metrics-only query: counts non-destroyed key objects (MySQL). +-- ObjectType is stored as a JSON field inside the 'attributes' column +-- (serialised via serde with rename_all = "PascalCase"). +-- Key object types: SymmetricKey, PrivateKey, PublicKey, SplitKey. +-- All states except Destroyed / Destroyed_Compromised are counted. +SELECT COUNT(*) FROM objects +WHERE state NOT IN ('Destroyed', 'Destroyed_Compromised') +AND JSON_UNQUOTE(JSON_EXTRACT(attributes, '$.ObjectType')) IN ('SymmetricKey', 'PrivateKey', 'PublicKey', 'SplitKey'); + -- name: insert-objects INSERT INTO objects (id, object, attributes, state, owner) VALUES (?, ?, ?, ?, ?); diff --git a/crate/server_database/src/stores/sql/sqlite.rs b/crate/server_database/src/stores/sql/sqlite.rs index 21fab1d079..a298b2e638 100644 --- a/crate/server_database/src/stores/sql/sqlite.rs +++ b/crate/server_database/src/stores/sql/sqlite.rs @@ -556,6 +556,20 @@ impl ObjectsStore for SqlitePool { .map_err(DbError::from)?; Ok(u64::try_from(count).unwrap_or(0)) } + + async fn count_non_destroyed_keys(&self) -> InterfaceResult { + // No $N placeholders — no need for replace_dollars_with_qn. + let sql = get_sqlite_query!("count-non-destroyed-keys-sqlite").to_string(); + let count: i64 = self + .reader() + .call(move |c: &mut rusqlite::Connection| { + let mut stmt = c.prepare(&sql)?; + stmt.query_row([], |r| r.get(0)) + }) + .await + .map_err(DbError::from)?; + Ok(u64::try_from(count).unwrap_or(0)) + } } #[async_trait(?Send)] diff --git a/crate/server_database/src/tests/mod.rs b/crate/server_database/src/tests/mod.rs index ca8b7db278..d899d49749 100644 --- a/crate/server_database/src/tests/mod.rs +++ b/crate/server_database/src/tests/mod.rs @@ -21,7 +21,8 @@ use self::{ use crate::stores::RedisWithFindex; #[cfg(feature = "non-fips")] use crate::stores::additional_redis_findex_tests::{ - test_corner_case, test_live_count_counter, test_objects_db, test_permissions_db, + test_active_key_count_counter, test_corner_case, test_live_count_counter, test_objects_db, + test_permissions_db, }; use crate::{ error::DbResult, @@ -98,6 +99,7 @@ pub(crate) async fn test_db_redis_with_findex() -> DbResult<()> { test_permissions_db().await?; test_corner_case().await?; test_live_count_counter().await?; + test_active_key_count_counter().await?; Box::pin(json_access(&get_redis_with_findex().await?)).await?; find_attributes(&get_redis_with_findex().await?).await?; owner(&get_redis_with_findex().await?).await?; From 3127c02ae0003e27147abdf4462709b90c62a6db Mon Sep 17 00:00:00 2001 From: HatemMn <19950216+HatemMn@users.noreply.github.com> Date: Fri, 5 Jun 2026 12:58:49 +0200 Subject: [PATCH 06/28] feat: step 3Bis improvement fixes --- CHANGELOG/feat_richOTELmetrics.md | 40 +++++-- Cargo.lock | 61 +++++++++++ crate/interfaces/Cargo.toml | 4 + crate/interfaces/src/hsm/hsm_store.rs | 145 +++++++++++++++++++++++++- 4 files changed, 238 insertions(+), 12 deletions(-) diff --git a/CHANGELOG/feat_richOTELmetrics.md b/CHANGELOG/feat_richOTELmetrics.md index 27d79a43a7..9c51d97ff8 100644 --- a/CHANGELOG/feat_richOTELmetrics.md +++ b/CHANGELOG/feat_richOTELmetrics.md @@ -89,9 +89,14 @@ state is not `Destroyed` or `Destroyed_Compromised`. `count-non-destroyed-keys`) that filter `objects` by `ObjectType IN (...)` and `state NOT IN ('Destroyed', 'Destroyed_Compromised')` using backend-specific JSON extraction syntax. -- **HSM store**: implement `count_non_destroyed_keys` by iterating all available - slots, calling `hsm.find(slot_id, HsmObjectFilter::Any)`, and summing slot lengths. - All HSM objects are considered non-destroyed active keys. +- **HSM store — `count_non_destroyed_keys`**: implemented by iterating all available + slots via `hsm.find(slot_id, HsmObjectFilter::Any)` and summing results. All HSM + objects are non-destroyed active keys by definition (deleted keys are removed from + the device, not marked Destroyed). +- **HSM store — `count_all_non_destroyed` bug fix**: the previous `Ok(0)` stub + (incorrect comment: "HSMs do not expose a key-count API") is replaced with a + delegation to `count_non_destroyed_keys()`. PKCS#11 `C_FindObjects` is sufficient + to count objects. `kms.objects.total` now correctly includes HSM-backed keys. - **Redis-findex — `ObjectsDB`**: add O(1) counter key `kms::metrics::active_key_count` with helpers `adjust_active_key_count(delta)`, `get_active_key_count()`, `set_active_key_count(count)`, and `scan_count_non_destroyed_keys()` (bootstrap SCAN). @@ -108,7 +113,28 @@ state is not `Destroyed` or `Destroyed_Compromised`. - **Startup seed** (`crate/server/src/core/kms/mod.rs`): seed `kms.keys.active.count` at startup from `count_non_destroyed_key_objects()` (non-fatal, same pattern as `kms.objects.total`). -- **Test** (`test_active_key_count_counter`): 6-step integration test covering - create 2 keys, create non-key object (OpaqueObject), deactivate, destroy, delete, - and bootstrap-SCAN reconcile. Registered in `test_db_redis_with_findex`. - Tagged `#[ignore = "Requires a running Redis instance"]`. \ No newline at end of file +- **Test** (`test_active_key_count_counter`): 6-step integration test for the Redis + counter lifecycle — create 2 keys, create non-key object, deactivate, destroy, + delete, bootstrap-SCAN reconcile. Tagged `#[ignore = "Requires a running Redis instance"]`. +- **Test** (`test_count_all_non_destroyed_delegates_to_count_non_destroyed_keys`): + pure in-process unit test using a `MockHsm` (2 slots, 3+2 keys) that verifies + `count_all_non_destroyed` returns 5 and equals `count_non_destroyed_keys`. + Runs without any hardware. Added `tokio` as a dev-dependency to + `cosmian_kms_interfaces`. + +## Testing + +### Mockall test infrastructure + +- Add `mockall = "0.13"` as a dev-dependency of `cosmian_kms_interfaces`. +- Replace the 130-line hand-rolled `MockHsm` struct (with full `impl HSM`) in + `crate/interfaces/src/hsm/hsm_store.rs` with a `mockall::mock!`-generated + `MockHsm` (~20 lines of method signatures). The macro generates expectation + machinery for all 14 async methods; `hsm_lib` is a concrete no-op `{ None }` + to sidestep mockall's limitation with `&self`-bound reference return types. +- The `test_count_all_non_destroyed_delegates_to_count_non_destroyed_keys` test + now uses `mock.expect_get_available_slot_list()` / `mock.expect_find()` with + `returning(...)` closures instead of a bespoke `HashMap`-based data structure. +- `MockRecorder` in `crate/server_database` is retained as-is: it is a stateful + recorder (collects `(op, backend, outcome)` triples in a `Vec`) where mockall + would add complexity without reducing code. diff --git a/Cargo.lock b/Cargo.lock index d5e9fc4a1d..e3d6cb6efb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1310,9 +1310,11 @@ dependencies = [ "async-trait", "cosmian_kmip", "cosmian_logger", + "mockall", "num-bigint-dig", "serde_json", "thiserror 2.0.17", + "tokio", "zeroize", ] @@ -1956,6 +1958,12 @@ version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" +[[package]] +name = "downcast" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" + [[package]] name = "ecdsa" version = "0.16.9" @@ -2177,6 +2185,15 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fragile" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8878864ba14bb86e818a412bfd6f18f9eabd4ec0f008a28e8f7eb61db532fcf9" +dependencies = [ + "futures-core", +] + [[package]] name = "futures" version = "0.3.32" @@ -3261,6 +3278,50 @@ dependencies = [ "zeroize", ] +[[package]] +name = "mockall" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39a6bfcc6c8c7eed5ee98b9c3e33adc726054389233e201c95dab2d41a3839d2" +dependencies = [ + "cfg-if", + "downcast", + "fragile", + "mockall_derive", + "predicates", + "predicates-tree", +] + +[[package]] +name = "mockall_derive" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ca3004c2efe9011bd4e461bd8256445052b9615405b4f7ea43fc8ca5c20898" +dependencies = [ + "cfg-if", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "mysql-common-derive" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66f62cad7623a9cb6f8f64037f0c4f69c8db8e82914334a83c9788201c2c1bfa" +dependencies = [ + "darling", + "heck", + "num-bigint", + "proc-macro-crate", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn", + "termcolor", + "thiserror 2.0.17", +] + [[package]] name = "mysql_async" version = "0.37.0" diff --git a/crate/interfaces/Cargo.toml b/crate/interfaces/Cargo.toml index 2543324ce0..1e7b81a5c8 100644 --- a/crate/interfaces/Cargo.toml +++ b/crate/interfaces/Cargo.toml @@ -24,3 +24,7 @@ num-bigint-dig = { workspace = true, features = ["std", "rand", "serde", "zeroiz serde_json = { workspace = true } thiserror = { workspace = true } zeroize = { workspace = true, default-features = true } + +[dev-dependencies] +mockall = "0.13" +tokio = { workspace = true, features = ["rt", "macros"] } diff --git a/crate/interfaces/src/hsm/hsm_store.rs b/crate/interfaces/src/hsm/hsm_store.rs index 746991f7d2..03c9cbfde0 100644 --- a/crate/interfaces/src/hsm/hsm_store.rs +++ b/crate/interfaces/src/hsm/hsm_store.rs @@ -378,11 +378,14 @@ impl ObjectsStore for HsmStore { Ok(uids) } - /// HSM object counting is not implemented — HSMs do not expose a key-count - /// API in the PKCS#11 interface. Override the trait default to suppress - /// the warning that would otherwise fire every 30 s from the metrics cron. + /// Count all non-destroyed objects on this HSM. + /// + /// On an HSM every object present in a slot is by definition non-destroyed: + /// deleted keys are physically removed from the device rather than being + /// transitioned to a `Destroyed` state. All HSM objects are also key + /// material, so this delegates directly to [`Self::count_non_destroyed_keys`]. async fn count_all_non_destroyed(&self) -> InterfaceResult { - Ok(0) + self.count_non_destroyed_keys().await } /// Count non-destroyed key objects across all HSM slots. @@ -1136,13 +1139,108 @@ fn to_object_with_metadata( #[cfg(test)] mod tests { + use std::sync::Arc; + + use async_trait::async_trait; use cosmian_kmip::kmip_2_1::{ kmip_attributes::Attributes, kmip_types::{Name, NameType}, }; + use zeroize::Zeroizing; use super::check_basic_compatibility; - use crate::InterfaceError; + use crate::{ + CryptoAlgorithm, HSM, HsmKeyAlgorithm, HsmKeypairAlgorithm, HsmObject, HsmObjectFilter, + InterfaceError, InterfaceResult, KeyMetadata, KeyType, ObjectsStore, SigningAlgorithm, + crypto_oracle::EncryptedContent, hsm::HsmStore, + }; + + // ── mockall-generated test double for HSM ───────────────────────────────── + + mockall::mock! { + /// Auto-generated test double for the `HSM` trait. + /// All async methods get expectation machinery; `hsm_lib` is a concrete + /// no-op implementation that always returns `None` (avoids mockall's + /// limitations with `&self`-bounded reference return types). + pub Hsm {} + + #[async_trait] + impl HSM for Hsm { + async fn get_available_slot_list(&self) -> InterfaceResult>; + async fn find( + &self, + slot_id: usize, + object_filter: HsmObjectFilter, + ) -> InterfaceResult>>; + async fn get_supported_algorithms( + &self, + slot_id: usize, + ) -> InterfaceResult>; + async fn create_key( + &self, + slot_id: usize, + id: &[u8], + algorithm: HsmKeyAlgorithm, + key_length_in_bits: usize, + sensitive: bool, + ) -> InterfaceResult<()>; + async fn create_keypair( + &self, + slot_id: usize, + sk_id: &[u8], + pk_id: &[u8], + algorithm: HsmKeypairAlgorithm, + key_length_in_bits: usize, + sensitive: bool, + ) -> InterfaceResult<()>; + async fn export( + &self, + slot_id: usize, + object_id: &[u8], + ) -> InterfaceResult>; + async fn delete(&self, slot_id: usize, object_id: &[u8]) -> InterfaceResult<()>; + async fn encrypt( + &self, + slot_id: usize, + key_id: &[u8], + algorithm: CryptoAlgorithm, + data: &[u8], + ) -> InterfaceResult; + async fn decrypt( + &self, + slot_id: usize, + key_id: &[u8], + algorithm: CryptoAlgorithm, + data: &[u8], + ) -> InterfaceResult>>; + async fn get_key_type( + &self, + slot_id: usize, + key_id: &[u8], + ) -> InterfaceResult>; + async fn get_key_metadata( + &self, + slot_id: usize, + key_id: &[u8], + ) -> InterfaceResult>; + async fn sign( + &self, + slot_id: usize, + key_id: &[u8], + algorithm: SigningAlgorithm, + data: &[u8], + ) -> InterfaceResult>; + async fn generate_random( + &self, + slot_id: usize, + len: usize, + ) -> InterfaceResult>; + async fn seed_random(&self, slot_id: usize, seed: &[u8]) -> InterfaceResult<()>; + fn hsm_lib(&self) -> Option<&'static dyn std::any::Any> { None } + } + } + + // ── Tests ───────────────────────────────────────────────────────────────── /// Locate with a Name filter must not match any HSM key (issue #935): /// HSM keys have no KMIP Name, so the filter should yield empty results @@ -1179,4 +1277,41 @@ mod tests { "Expected ObjectType-only filter to be compatible with HSM, got: {result:?}" ); } + + /// `count_all_non_destroyed` must equal `count_non_destroyed_keys` for an `HsmStore` + /// because all objects present on an HSM are non-destroyed key material by definition. + /// + /// Uses a `MockHsm` with two slots (3 + 2 keys) to verify that both methods return 5 + /// and that `count_all_non_destroyed` is not hard-coded to 0. + #[tokio::test] + async fn test_count_all_non_destroyed_delegates_to_count_non_destroyed_keys() + -> InterfaceResult<()> { + let mut mock = MockHsm::new(); + mock.expect_get_available_slot_list() + .returning(|| Ok(vec![0, 1])); + mock.expect_find() + .returning(|slot_id, _filter| match slot_id { + 0 => Ok(vec![vec![0], vec![1], vec![2]]), // 3 keys in slot 0 + 1 => Ok(vec![vec![0], vec![1]]), // 2 keys in slot 1 + _ => Ok(vec![]), + }); + + let store = HsmStore::new(Arc::new(mock), &["admin".to_owned()], "cosmian", "hsm"); + + let via_all = store.count_all_non_destroyed().await?; + let via_keys = store.count_non_destroyed_keys().await?; + + if via_all != 5 { + return Err(InterfaceError::Default(format!( + "count_all_non_destroyed should return 5 (3+2), got {via_all}" + ))); + } + if via_all != via_keys { + return Err(InterfaceError::Default(format!( + "count_all_non_destroyed ({via_all}) must equal count_non_destroyed_keys \ + ({via_keys}) for HsmStore" + ))); + } + Ok(()) + } } From c40228fb9f8ec45520c4fdf25cf27ab37f7ce8c1 Mon Sep 17 00:00:00 2001 From: HatemMn <19950216+HatemMn@users.noreply.github.com> Date: Mon, 8 Jun 2026 13:36:58 +0200 Subject: [PATCH 07/28] feat: wire `kms.cache.operations.total`, `kms.hsm.operations.total` --- CHANGELOG/feat_richOTELmetrics.md | 36 ++++++++- Cargo.lock | 18 ----- crate/server/src/config/mod.rs | 4 +- crate/server/src/config/params/mod.rs | 2 +- .../server/src/core/kms/other_kms_methods.rs | 9 +++ .../src/core/operations/key_ops/crypto_op.rs | 10 ++- crate/server/src/core/otel_metrics.rs | 2 +- crate/server/src/core/uid_utils.rs | 74 +++++++++++++++++++ crate/server/src/core/wrapping/unwrap.rs | 5 ++ crate/server/src/core/wrapping/wrap.rs | 5 ++ 10 files changed, 140 insertions(+), 25 deletions(-) diff --git a/CHANGELOG/feat_richOTELmetrics.md b/CHANGELOG/feat_richOTELmetrics.md index 9c51d97ff8..ed92979555 100644 --- a/CHANGELOG/feat_richOTELmetrics.md +++ b/CHANGELOG/feat_richOTELmetrics.md @@ -2,7 +2,7 @@ ## Features -### Database Metrics Wiring (Step 1) +### Database Metrics Wiring — `kms.database.operations.total`, `kms.database.operation.duration` (Step 1) - Wire `kms.database.operations.total` (counter) and `kms.database.operation.duration` (histogram) at the `Database` facade layer with `operation`, `backend`, and `outcome` @@ -15,7 +15,7 @@ - `MainDbKind::as_str()` provides canonical backend labels (`"sqlite"`, `"postgresql"`, `"mysql"`, `"redis"`). -### HTTP Metrics Wiring (Step 2) +### HTTP Metrics Wiring — `kms.http.requests.total`, `kms.http.request.duration`, `kms.active.connections` (Step 2) - Add `OtelHttpMetrics` Actix-web middleware (`crate/server/src/middlewares/otel_http_middleware.rs`) that records `kms.http.requests.total` (counter with `method`, `path`, `status` @@ -54,7 +54,7 @@ - Add `Database::count_all_non_destroyed_objects()` facade that sums counts across all registered stores with `saturating_add`, tolerating partial failures. -### Object Count Metric — Redis-findex backend (Step 3, continued) +### Object Count Metric — `kms.objects.total`, Redis-findex backend (Step 3, continued) - Implement `count_all_non_destroyed` for the `RedisWithFindex` backend using an O(1) counter key (`kms::metrics::live_object_count`) instead of a full key scan. @@ -122,6 +122,36 @@ state is not `Destroyed` or `Destroyed_Compromised`. Runs without any hardware. Added `tokio` as a dev-dependency to `cosmian_kms_interfaces`. + +### Cache and HSM Operation Metrics — `kms.cache.operations.total`, `kms.hsm.operations.total` (Step 4) + + +- Add `hsm_model_from_prefix(hsm_instances, prefix) -> &str` to + `crate/server/src/core/uid_utils.rs`. Looks up the human-readable HSM model + label (e.g. `"softhsm2"`, `"utimaco"`) from the configured `hsm_instances` + slice and falls back to the prefix string itself when no matching instance is + found, ensuring the metric label is always non-empty. +- Export `HsmInstanceParams` from `crate/server/src/config/mod.rs` (re-exported + through `params/mod.rs`) so that `uid_utils` and future callers can reference + it without reaching into private sub-modules. +- Wire `kms.cache.operations.total` in `get_unwrapped()` + (`crate/server/src/core/kms/other_kms_methods.rs`): emit `record_cache_operation("get", "hit")` + on a cache hit, `record_cache_operation("get", "miss")` on a miss, and + `record_cache_operation("insert", "ok")` after a successful cache population. +- Wire `kms.hsm.operations.total` at three dispatch points: + - `perform_crypto_operation()` in `key_ops/crypto_op.rs` — covers all six + oracle-routed operations (Encrypt, Decrypt, Sign, SignatureVerify, MAC, + MACVerify) via the `ResolvedKey::Oracle` arm. Uses `Op::OP_NAME` as the + operation label and `hsm_model_from_prefix` for the model label. + - `wrap_using_crypto_oracle()` in `core/wrapping/wrap.rs` — emits + `record_hsm_operation("Wrap", model)` after the oracle `encrypt` call. + - `unwrap_using_crypto_oracle()` in `core/wrapping/unwrap.rs` — emits + `record_hsm_operation("Unwrap", model)` after the oracle `decrypt` call. +- Add 4 unit tests in `core::uid_utils::tests` covering legacy `"hsm"` prefix, + new `"hsm::softhsm2"` format, multi-instance selection, and unknown-prefix + fallback. All tests are sync, require no async runtime, and pass without + hardware. + ## Testing ### Mockall test infrastructure diff --git a/Cargo.lock b/Cargo.lock index e3d6cb6efb..473c253045 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3304,24 +3304,6 @@ dependencies = [ "syn", ] -[[package]] -name = "mysql-common-derive" -version = "0.32.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66f62cad7623a9cb6f8f64037f0c4f69c8db8e82914334a83c9788201c2c1bfa" -dependencies = [ - "darling", - "heck", - "num-bigint", - "proc-macro-crate", - "proc-macro-error2", - "proc-macro2", - "quote", - "syn", - "termcolor", - "thiserror 2.0.17", -] - [[package]] name = "mysql_async" version = "0.37.0" diff --git a/crate/server/src/config/mod.rs b/crate/server/src/config/mod.rs index d20e58f87c..d36dbcb275 100644 --- a/crate/server/src/config/mod.rs +++ b/crate/server/src/config/mod.rs @@ -3,7 +3,9 @@ mod params; pub mod wizard; pub use command_line::*; -pub use params::{KmipPolicyParams, OpenTelemetryConfig, ProxyParams, ServerParams, TlsParams}; +pub use params::{ + HsmInstanceParams, KmipPolicyParams, OpenTelemetryConfig, ProxyParams, ServerParams, TlsParams, +}; #[derive(Debug, Clone)] pub struct IdpConfig { diff --git a/crate/server/src/config/params/mod.rs b/crate/server/src/config/params/mod.rs index 99e6cb0226..b6cca01b34 100644 --- a/crate/server/src/config/params/mod.rs +++ b/crate/server/src/config/params/mod.rs @@ -7,5 +7,5 @@ mod tls_params; pub use kmip_policy_params::KmipPolicyParams; pub use open_telemetry_params::OpenTelemetryConfig; pub use proxy_params::ProxyParams; -pub use server_params::ServerParams; +pub use server_params::{HsmInstanceParams, ServerParams}; pub use tls_params::TlsParams; diff --git a/crate/server/src/core/kms/other_kms_methods.rs b/crate/server/src/core/kms/other_kms_methods.rs index e8f27ddce5..fcd33da41d 100644 --- a/crate/server/src/core/kms/other_kms_methods.rs +++ b/crate/server/src/core/kms/other_kms_methods.rs @@ -62,11 +62,17 @@ impl KMS { // check if we have it in the cache if let Some(u) = self.database.unwrapped_cache().peek(uid, object).await? { debug!("Unwrapped cache hit"); + if let Some(ref metrics) = self.metrics { + metrics.record_cache_operation("get", "hit"); + } return Ok(u); } // cache miss, try to unwrap debug!("Unwrapped cache miss. Calling unwrap"); + if let Some(ref metrics) = self.metrics { + metrics.record_cache_operation("get", "miss"); + } let unwrapped_object = { let mut unwrapped_object = object.clone(); unwrap_object(&mut unwrapped_object, self, user).await?; @@ -78,6 +84,9 @@ impl KMS { .unwrapped_cache() .insert(uid.to_owned(), object, unwrapped_object.clone()) .await?; + if let Some(ref metrics) = self.metrics { + metrics.record_cache_operation("insert", "ok"); + } Ok(unwrapped_object) } diff --git a/crate/server/src/core/operations/key_ops/crypto_op.rs b/crate/server/src/core/operations/key_ops/crypto_op.rs index d3a85270b3..ae68c48234 100644 --- a/crate/server/src/core/operations/key_ops/crypto_op.rs +++ b/crate/server/src/core/operations/key_ops/crypto_op.rs @@ -163,7 +163,15 @@ pub(crate) async fn perform_crypto_operation( match resolve_key_for_operation::(unique_identifier, kms, user).await? { ResolvedKey::Oracle { uid, prefix } => { - Op::execute_oracle(kms, &request, &uid, &prefix).await + let result = Op::execute_oracle(kms, &request, &uid, &prefix).await; + if let Some(ref metrics) = kms.metrics { + let model = crate::core::uid_utils::hsm_model_from_prefix( + &kms.params.hsm_instances, + &prefix, + ); + metrics.record_hsm_operation(Op::OP_NAME, model); + } + result } ResolvedKey::Local(owm) => { let mut owm = *owm; diff --git a/crate/server/src/core/otel_metrics.rs b/crate/server/src/core/otel_metrics.rs index f961410a0d..5a7a569db3 100644 --- a/crate/server/src/core/otel_metrics.rs +++ b/crate/server/src/core/otel_metrics.rs @@ -454,8 +454,8 @@ impl OtelMetrics { self.hsm_operations_total.add( 1, &[ - KeyValue::new("operation", operation.to_owned()), KeyValue::new("hsm_model", hsm_model.to_owned()), + KeyValue::new("operation", operation.to_owned()), ], ); } diff --git a/crate/server/src/core/uid_utils.rs b/crate/server/src/core/uid_utils.rs index 71c7ef9859..8ad5bd5b32 100644 --- a/crate/server/src/core/uid_utils.rs +++ b/crate/server/src/core/uid_utils.rs @@ -3,6 +3,7 @@ use std::collections::HashSet; use cosmian_kms_server_database::reexport::cosmian_kmip::kmip_2_1::kmip_types::UniqueIdentifier; use crate::{ + config::HsmInstanceParams, core::KMS, result::{KResult, KResultHelper}, }; @@ -31,6 +32,26 @@ pub(crate) fn has_prefix(uid: &str) -> Option<&str> { None } +/// Resolve a human-readable HSM model label from a routing prefix. +/// +/// Searches the configured `hsm_instances` for an entry whose `prefix` matches +/// the given prefix and returns its `model` field (e.g. `"softhsm2"`). +/// Falls back to the prefix string itself when no matching instance is found, +/// ensuring call sites always get a usable label without panicking. +/// +/// # Arguments +/// * `hsm_instances` — the slice from `ServerParams::hsm_instances` +/// * `prefix` — a routing prefix as returned by [`has_prefix`] +pub(crate) fn hsm_model_from_prefix<'a>( + hsm_instances: &'a [HsmInstanceParams], + prefix: &'a str, +) -> &'a str { + hsm_instances + .iter() + .find(|i| i.prefix == prefix) + .map_or(prefix, |i| i.model.as_str()) +} + /// Determine the list of possible UIDs from a Unique Identifier, /// that may contain tags. /// # Arguments @@ -52,3 +73,56 @@ pub(super) async fn uids_from_unique_identifier( } Ok(HashSet::from([uid_or_tags.to_owned()])) } + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + + use super::*; + + fn make_instance(prefix: &str, model: &str) -> HsmInstanceParams { + HsmInstanceParams { + model: model.to_owned(), + admin: vec![], + slot_passwords: HashMap::new(), + prefix: prefix.to_owned(), + } + } + + #[test] + fn test_hsm_model_legacy_prefix() { + // Legacy format: UID "hsm::::" → prefix == "hsm" + let instances = vec![make_instance("hsm", "softhsm2")]; + assert_eq!(hsm_model_from_prefix(&instances, "hsm"), "softhsm2"); + } + + #[test] + fn test_hsm_model_new_format_prefix() { + // New format: UID "hsm::softhsm2::::" → prefix == "hsm::softhsm2" + let instances = vec![make_instance("hsm::softhsm2", "softhsm2")]; + assert_eq!( + hsm_model_from_prefix(&instances, "hsm::softhsm2"), + "softhsm2" + ); + } + + #[test] + fn test_hsm_model_second_instance() { + // Multiple instances; prefix selects the correct one. + let instances = vec![ + make_instance("hsm::softhsm2", "softhsm2"), + make_instance("hsm::utimaco", "utimaco"), + ]; + assert_eq!(hsm_model_from_prefix(&instances, "hsm::utimaco"), "utimaco"); + } + + #[test] + fn test_hsm_model_fallback_to_prefix_when_unknown() { + // Unknown prefix with empty instance list → falls back to the prefix itself. + let instances: Vec = vec![]; + assert_eq!( + hsm_model_from_prefix(&instances, "hsm::unknown"), + "hsm::unknown" + ); + } +} diff --git a/crate/server/src/core/wrapping/unwrap.rs b/crate/server/src/core/wrapping/unwrap.rs index 1ca32cff60..cb8a2b7c43 100644 --- a/crate/server/src/core/wrapping/unwrap.rs +++ b/crate/server/src/core/wrapping/unwrap.rs @@ -203,6 +203,11 @@ async fn unwrap_using_crypto_oracle( let plaintext = crypto_oracle .decrypt(&unwrapping_key_uid, wrapped_key, None, None) .await?; + if let Some(ref metrics) = kms.metrics { + let model = + crate::core::uid_utils::hsm_model_from_prefix(&kms.params.hsm_instances, prefix); + metrics.record_hsm_operation("Unwrap", model); + } // decode the unwrapped key let key_value = decode_unwrapped_key( diff --git a/crate/server/src/core/wrapping/wrap.rs b/crate/server/src/core/wrapping/wrap.rs index 04de4b4870..d8be34a717 100644 --- a/crate/server/src/core/wrapping/wrap.rs +++ b/crate/server/src/core/wrapping/wrap.rs @@ -362,6 +362,11 @@ async fn wrap_using_crypto_oracle( let encrypted_content = crypto_oracle .encrypt(wrapping_key_uid, data_to_wrap.as_slice(), None, None) .await?; + if let Some(ref metrics) = kms.metrics { + let model = + crate::core::uid_utils::hsm_model_from_prefix(&kms.params.hsm_instances, prefix); + metrics.record_hsm_operation("Wrap", model); + } let wrapped_key = [ encrypted_content.iv.clone().unwrap_or_default(), From 4f1165ae035ae15c846449df6c8659c4396a8f03 Mon Sep 17 00:00:00 2001 From: HatemMn <19950216+HatemMn@users.noreply.github.com> Date: Mon, 8 Jun 2026 17:30:00 +0200 Subject: [PATCH 08/28] fix: budgfix --- crate/server/src/core/otel_metrics.rs | 63 +++++++++++++------ .../src/core/database_objects.rs | 21 +++++-- 2 files changed, 59 insertions(+), 25 deletions(-) diff --git a/crate/server/src/core/otel_metrics.rs b/crate/server/src/core/otel_metrics.rs index 5a7a569db3..d9f1b7c605 100644 --- a/crate/server/src/core/otel_metrics.rs +++ b/crate/server/src/core/otel_metrics.rs @@ -19,7 +19,7 @@ use std::{ use cosmian_kms_server_database::{DbMetricsRecorder, MainDbKind}; use opentelemetry::{ KeyValue, - metrics::{Counter, Gauge, Histogram, Meter, MeterProvider, UpDownCounter}, + metrics::{Counter, Histogram, Meter, MeterProvider, UpDownCounter}, }; use opentelemetry_sdk::metrics::SdkMeterProvider; @@ -78,11 +78,17 @@ pub struct OtelMetrics { /// Current number of active connections pub active_connections: UpDownCounter, - /// Total number of objects in the KMS (gauge — records absolute count directly) - pub kms_objects_total: Gauge, + /// Total number of objects in the KMS + pub kms_objects_total: UpDownCounter, - /// Current number of active keys in Active state (gauge — records absolute count directly) - pub active_keys_count: Gauge, + /// Mirror of `kms_objects_total` for tracking the last set value + objects_total_value: Arc>, + + /// Current number of active keys (absolute count from DB) + pub active_keys_count: UpDownCounter, + + /// Mirror of `active_keys_count` for tracking the last set value + active_keys_count_value: Arc>, /// Cache hit/miss statistics pub cache_operations_total: Counter, @@ -222,25 +228,27 @@ impl OtelMetrics { .with_unit("{connection}") .build(); - // KMS objects — gauge records the current absolute count directly + // KMS objects total (UpDownCounter with delta tracking) let kms_objects_total = meter - .i64_gauge("kms.objects.total") + .i64_up_down_counter("kms.objects.total") .with_description("Total number of objects in the KMS") .with_unit("{object}") .build(); + // Force the time series to exist even when the count is 0. + kms_objects_total.add(0, &[]); - // Active keys count — gauge records the current absolute count directly + // Active keys count (UpDownCounter with delta tracking) let active_keys_count = meter - .i64_gauge("kms.keys.active.count") + .i64_up_down_counter("kms.keys.active.count") .with_description( "Number of non-destroyed key objects (SymmetricKey, PrivateKey, PublicKey, \ - SplitKey) across all backends. Counts keys in all non-terminal states: \ - PreActive, Active, Deactivated, Compromised.", + SplitKey) across all backends. Counts keys in all non-terminal \ + states: PreActive, Active, Deactivated, Compromised.", ) .with_unit("{key}") .build(); - // Seed the time series so it is visible in the backend from server start. - active_keys_count.record(0, &[]); + // Force the time series to exist even when the count is 0. + active_keys_count.add(0, &[]); // Cache operations let cache_operations_total = meter @@ -275,7 +283,9 @@ impl OtelMetrics { errors_total, active_connections, kms_objects_total, + objects_total_value: Arc::new(RwLock::new(0)), active_keys_count, + active_keys_count_value: Arc::new(RwLock::new(0)), cache_operations_total, hsm_operations_total, }) @@ -425,17 +435,32 @@ impl OtelMetrics { self.active_connections.add(-1, &[]); } - /// Set the current active keys count from an absolute Locate response. + /// Set the current active keys count from an absolute DB count. + /// + /// Uses delta encoding so the `UpDownCounter` always reflects the + /// correct absolute value without resetting to zero. pub fn update_active_keys_count(&self, absolute_count: i64) { - self.active_keys_count.record(absolute_count, &[]); + if let Ok(mut last) = self.active_keys_count_value.write() { + let delta = absolute_count - *last; + if delta != 0 { + self.active_keys_count.add(delta, &[]); + *last = absolute_count; + } + } } /// Set `kms.objects.total` to the current absolute object count. /// - /// Called once at server startup (seeding from the real DB count) and - /// every 30 s by the metrics cron task. + /// Uses delta encoding so the `UpDownCounter` always reflects the + /// correct absolute value without resetting to zero. pub fn update_objects_total(&self, absolute_count: i64) { - self.kms_objects_total.record(absolute_count, &[]); + if let Ok(mut last) = self.objects_total_value.write() { + let delta = absolute_count - *last; + if delta != 0 { + self.kms_objects_total.add(delta, &[]); + *last = absolute_count; + } + } } /// Record cache operation @@ -454,8 +479,8 @@ impl OtelMetrics { self.hsm_operations_total.add( 1, &[ - KeyValue::new("hsm_model", hsm_model.to_owned()), KeyValue::new("operation", operation.to_owned()), + KeyValue::new("hsm_model", hsm_model.to_owned()), ], ); } diff --git a/crate/server_database/src/core/database_objects.rs b/crate/server_database/src/core/database_objects.rs index 20623df885..7fbb7b6a13 100644 --- a/crate/server_database/src/core/database_objects.rs +++ b/crate/server_database/src/core/database_objects.rs @@ -447,9 +447,12 @@ impl Database { /// yet implemented `count_all_non_destroyed` return `0` via the trait default, /// which is acceptable — the sum will still be a valid lower bound. pub async fn count_all_non_destroyed_objects(&self) -> DbResult { - let map = self.objects.read().await; + let stores: Vec> = { + let map = self.objects.read().await; + map.values().cloned().collect() + }; // read guard dropped before any async I/O let mut total: u64 = 0; - for store in map.values() { + for store in &stores { let n = store.count_all_non_destroyed().await.unwrap_or(0); // A single backend failure must not block the aggregate total = total.saturating_add(n); } @@ -463,9 +466,12 @@ impl Database { /// Backends that have not yet implemented `count_non_destroyed_keys` return `0` via /// the trait default — the sum remains a valid lower bound. pub async fn count_non_destroyed_key_objects(&self) -> DbResult { - let map = self.objects.read().await; + let stores: Vec> = { + let map = self.objects.read().await; + map.values().cloned().collect() + }; // read guard dropped before any async I/O let mut total: u64 = 0; - for store in map.values() { + for store in &stores { let n = store.count_non_destroyed_keys().await.unwrap_or(0); // A single backend failure must not block the aggregate total = total.saturating_add(n); } @@ -479,8 +485,11 @@ impl Database { /// Redis backends recompute counts from a full SCAN and overwrite cached keys. /// Called by the slow-path cron loop (every 5 minutes) to prevent counter drift. pub async fn reconcile_all_object_counts(&self) -> DbResult<()> { - let map = self.objects.read().await; - for store in map.values() { + let stores: Vec> = { + let map = self.objects.read().await; + map.values().cloned().collect() + }; // read guard dropped before any async I/O + for store in &stores { if let Err(e) = store.reconcile_counts().await { // Non-fatal: log and continue so one failing backend does not block others. cosmian_logger::warn!("[database] reconcile_counts failed for a store: {e}"); From d215423fe9697cc9c7417fac28fd404378d658d0 Mon Sep 17 00:00:00 2001 From: HatemMn <19950216+HatemMn@users.noreply.github.com> Date: Mon, 8 Jun 2026 18:55:04 +0200 Subject: [PATCH 09/28] fix: bug fixes --- .../server_database/src/stores/sql/query.sql | 16 ---- .../server_database/src/stores/sql/sqlite.rs | 76 +++++++++++++++++++ 2 files changed, 76 insertions(+), 16 deletions(-) diff --git a/crate/server_database/src/stores/sql/query.sql b/crate/server_database/src/stores/sql/query.sql index 72e1ef1d52..542be8db5e 100644 --- a/crate/server_database/src/stores/sql/query.sql +++ b/crate/server_database/src/stores/sql/query.sql @@ -79,31 +79,15 @@ INSERT INTO objects (id, object, attributes, state, owner) VALUES ($1, $2, $3, $ WHERE objects.owner=$5; -- name: count-non-destroyed-objects --- Privileged metrics-only query: counts ALL objects regardless of owner. --- Called exclusively by the OTEL metrics layer for kms.objects.total. --- State strings correspond to Rust enum variant names via strum::Display: --- Destroyed = the object was explicitly destroyed --- Destroyed_Compromised = the object was destroyed after being compromised --- All other states (PreActive, Active, Deactivated, Compromised) are live objects. SELECT COUNT(*) FROM objects WHERE state NOT IN ('Destroyed', 'Destroyed_Compromised'); -- name: count-non-destroyed-keys-sqlite --- Privileged metrics-only query: counts non-destroyed key objects (SQLite). --- ObjectType is stored as a JSON field inside the 'attributes' column --- (serialised via serde with rename_all = "PascalCase"). --- Key object types: SymmetricKey, PrivateKey, PublicKey, SplitKey. --- All states except Destroyed / Destroyed_Compromised are counted. SELECT COUNT(*) FROM objects WHERE state NOT IN ('Destroyed', 'Destroyed_Compromised') AND json_extract(attributes, '$.ObjectType') IN ('SymmetricKey', 'PrivateKey', 'PublicKey', 'SplitKey'); -- name: count-non-destroyed-keys-pg --- Privileged metrics-only query: counts non-destroyed key objects (PostgreSQL). --- ObjectType is stored as a JSONB field inside the 'attributes' column --- (serialised via serde with rename_all = "PascalCase"). --- Key object types: SymmetricKey, PrivateKey, PublicKey, SplitKey. --- All states except Destroyed / Destroyed_Compromised are counted. SELECT COUNT(*) FROM objects WHERE state NOT IN ('Destroyed', 'Destroyed_Compromised') AND attributes->>'ObjectType' IN ('SymmetricKey', 'PrivateKey', 'PublicKey', 'SplitKey'); diff --git a/crate/server_database/src/stores/sql/sqlite.rs b/crate/server_database/src/stores/sql/sqlite.rs index a298b2e638..a394cdbbf0 100644 --- a/crate/server_database/src/stores/sql/sqlite.rs +++ b/crate/server_database/src/stores/sql/sqlite.rs @@ -1033,3 +1033,79 @@ fn apply_owned_ops( } Ok(uids) } + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::TempDir; + + /// Verify that `count-non-destroyed-objects` and `count-non-destroyed-keys-sqlite` + /// are present in the parsed query map. + /// + /// Regression guard: `rawsql` treats any `--` line containing the substring + /// `"name"` as a new named-query tag, silently overwriting the current query + /// accumulation. Intermediate comment lines that contained "names" or "rename" + /// previously caused these keys to be absent from the map, making every call + /// to `count_all_non_destroyed` / `count_non_destroyed_keys` return 0 via the + /// `unwrap_or(0)` in `database_objects.rs`. + #[test] + fn test_count_query_keys_present_in_loader() { + assert!( + SQLITE_QUERIES.get("count-non-destroyed-objects").is_some(), + "count-non-destroyed-objects not found – rawsql comment stripping bug recurred" + ); + assert!( + SQLITE_QUERIES + .get("count-non-destroyed-keys-sqlite") + .is_some(), + "count-non-destroyed-keys-sqlite not found – rawsql comment stripping bug recurred" + ); + } + + /// End-to-end: insert rows directly via SQL and verify both count methods + /// return the expected value. Uses raw SQL to avoid pulling in the full KMIP + /// object-construction machinery. + #[tokio::test] + async fn test_count_non_destroyed_returns_correct_value() -> Result<(), Box> { + let dir = TempDir::new()?; + let db_path = dir.path().join("test.db"); + let pool = SqlitePool::instantiate(&db_path, true, None).await?; + + // Initially empty. + assert_eq!(pool.count_all_non_destroyed().await?, 0); + assert_eq!(pool.count_non_destroyed_keys().await?, 0); + + // Insert one Active SymmetricKey row directly. + let attrs_json = r#"{"ObjectType":"SymmetricKey","State":"Active"}"#.to_owned(); + pool.writer + .call(move |c: &mut rusqlite::Connection| { + c.execute( + "INSERT INTO objects (id, object, attributes, state, owner) \ + VALUES ('uid-1', '{}', ?1, 'Active', 'owner')", + rusqlite::params![attrs_json], + ) + }) + .await?; + + assert_eq!(pool.count_all_non_destroyed().await?, 1); + assert_eq!(pool.count_non_destroyed_keys().await?, 1); + + // Insert one Destroyed Certificate row — should not be counted. + let attrs2 = r#"{"ObjectType":"Certificate","State":"Destroyed"}"#.to_owned(); + pool.writer + .call(move |c: &mut rusqlite::Connection| { + c.execute( + "INSERT INTO objects (id, object, attributes, state, owner) \ + VALUES ('uid-2', '{}', ?1, 'Destroyed', 'owner')", + rusqlite::params![attrs2], + ) + }) + .await?; + + // Total non-destroyed stays 1; keys also stays 1. + assert_eq!(pool.count_all_non_destroyed().await?, 1); + assert_eq!(pool.count_non_destroyed_keys().await?, 1); + + Ok(()) + } +} From dde3fc0aec69f0187e617e7d5e7c9cbf069db7a5 Mon Sep 17 00:00:00 2001 From: HatemMn <19950216+HatemMn@users.noreply.github.com> Date: Tue, 9 Jun 2026 10:57:45 +0200 Subject: [PATCH 10/28] fix: lots of inconsistencies in the code + new tests --- crate/server/src/core/otel_metrics.rs | 80 ++++++--------- .../src/middlewares/otel_http_middleware.rs | 2 +- .../src/core/database_objects.rs | 12 ++- .../src/core/database_permissions.rs | 98 ++++++------------- .../server_database/src/stores/sql/sqlite.rs | 10 +- 5 files changed, 80 insertions(+), 122 deletions(-) diff --git a/crate/server/src/core/otel_metrics.rs b/crate/server/src/core/otel_metrics.rs index d9f1b7c605..18cec94c55 100644 --- a/crate/server/src/core/otel_metrics.rs +++ b/crate/server/src/core/otel_metrics.rs @@ -19,7 +19,7 @@ use std::{ use cosmian_kms_server_database::{DbMetricsRecorder, MainDbKind}; use opentelemetry::{ KeyValue, - metrics::{Counter, Histogram, Meter, MeterProvider, UpDownCounter}, + metrics::{Counter, Gauge, Histogram, Meter, MeterProvider, UpDownCounter}, }; use opentelemetry_sdk::metrics::SdkMeterProvider; @@ -78,17 +78,11 @@ pub struct OtelMetrics { /// Current number of active connections pub active_connections: UpDownCounter, - /// Total number of objects in the KMS - pub kms_objects_total: UpDownCounter, + /// Total number of objects in the KMS (gauge — records absolute count directly) + pub kms_objects_total: Gauge, - /// Mirror of `kms_objects_total` for tracking the last set value - objects_total_value: Arc>, - - /// Current number of active keys (absolute count from DB) - pub active_keys_count: UpDownCounter, - - /// Mirror of `active_keys_count` for tracking the last set value - active_keys_count_value: Arc>, + /// Current number of active keys in Active state (gauge — records absolute count directly) + pub active_keys_count: Gauge, /// Cache hit/miss statistics pub cache_operations_total: Counter, @@ -228,27 +222,25 @@ impl OtelMetrics { .with_unit("{connection}") .build(); - // KMS objects total (UpDownCounter with delta tracking) + // KMS objects — gauge records the current absolute count directly let kms_objects_total = meter - .i64_up_down_counter("kms.objects.total") + .i64_gauge("kms.objects.total") .with_description("Total number of objects in the KMS") .with_unit("{object}") .build(); - // Force the time series to exist even when the count is 0. - kms_objects_total.add(0, &[]); - // Active keys count (UpDownCounter with delta tracking) + // Active keys count — gauge records the current absolute count directly let active_keys_count = meter - .i64_up_down_counter("kms.keys.active.count") + .i64_gauge("kms.keys.active.count") .with_description( "Number of non-destroyed key objects (SymmetricKey, PrivateKey, PublicKey, \ - SplitKey) across all backends. Counts keys in all non-terminal \ - states: PreActive, Active, Deactivated, Compromised.", + SplitKey) across all backends. Counts keys in all non-terminal states: \ + PreActive, Active, Deactivated, Compromised.", ) .with_unit("{key}") .build(); - // Force the time series to exist even when the count is 0. - active_keys_count.add(0, &[]); + // Seed the time series so it is visible in the backend from server start. + active_keys_count.record(0, &[]); // Cache operations let cache_operations_total = meter @@ -283,9 +275,7 @@ impl OtelMetrics { errors_total, active_connections, kms_objects_total, - objects_total_value: Arc::new(RwLock::new(0)), active_keys_count, - active_keys_count_value: Arc::new(RwLock::new(0)), cache_operations_total, hsm_operations_total, }) @@ -435,52 +425,44 @@ impl OtelMetrics { self.active_connections.add(-1, &[]); } - /// Set the current active keys count from an absolute DB count. - /// - /// Uses delta encoding so the `UpDownCounter` always reflects the - /// correct absolute value without resetting to zero. + /// Set the current active keys count from an absolute Locate response. pub fn update_active_keys_count(&self, absolute_count: i64) { - if let Ok(mut last) = self.active_keys_count_value.write() { - let delta = absolute_count - *last; - if delta != 0 { - self.active_keys_count.add(delta, &[]); - *last = absolute_count; - } - } + self.active_keys_count.record(absolute_count, &[]); } /// Set `kms.objects.total` to the current absolute object count. /// - /// Uses delta encoding so the `UpDownCounter` always reflects the - /// correct absolute value without resetting to zero. + /// Called once at server startup (seeding from the real DB count) and + /// every 30 s by the metrics cron task. pub fn update_objects_total(&self, absolute_count: i64) { - if let Ok(mut last) = self.objects_total_value.write() { - let delta = absolute_count - *last; - if delta != 0 { - self.kms_objects_total.add(delta, &[]); - *last = absolute_count; - } - } + self.kms_objects_total.record(absolute_count, &[]); } /// Record cache operation - pub fn record_cache_operation(&self, operation: &str, result: &str) { + /// + /// Both `operation` and `result` must be `'static` string literals (e.g. `"get"`, `"hit"`). + /// Using `&'static str` avoids a `String` allocation on every call since + /// all current call sites already use compile-time constants. + pub fn record_cache_operation(&self, operation: &'static str, result: &'static str) { self.cache_operations_total.add( 1, &[ - KeyValue::new("operation", operation.to_owned()), - KeyValue::new("result", result.to_owned()), + KeyValue::new("operation", operation), + KeyValue::new("result", result), ], ); } - /// Record HSM operation - pub fn record_hsm_operation(&self, operation: &str, hsm_model: &str) { + /// Record HSM operation. + /// + /// `operation` must be a `'static` literal (e.g. `Op::OP_NAME`, `"Wrap"`, `"Unwrap"`). + /// `hsm_model` is a runtime label from `hsm_model_from_prefix` and still requires allocation. + pub fn record_hsm_operation(&self, operation: &'static str, hsm_model: &str) { self.hsm_operations_total.add( 1, &[ - KeyValue::new("operation", operation.to_owned()), KeyValue::new("hsm_model", hsm_model.to_owned()), + KeyValue::new("operation", operation), ], ); } diff --git a/crate/server/src/middlewares/otel_http_middleware.rs b/crate/server/src/middlewares/otel_http_middleware.rs index 9eedabbbc3..070943c4ae 100644 --- a/crate/server/src/middlewares/otel_http_middleware.rs +++ b/crate/server/src/middlewares/otel_http_middleware.rs @@ -100,7 +100,7 @@ where let duration = start.elapsed().as_secs_f64(); let status = result.as_ref().map_or_else( - |_| "500".to_owned(), + |err| err.as_response_error().status_code().as_str().to_owned(), |resp| resp.status().as_str().to_owned(), ); diff --git a/crate/server_database/src/core/database_objects.rs b/crate/server_database/src/core/database_objects.rs index 7fbb7b6a13..98be802067 100644 --- a/crate/server_database/src/core/database_objects.rs +++ b/crate/server_database/src/core/database_objects.rs @@ -137,7 +137,7 @@ impl Database { /// /// 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( + pub(crate) async fn record( &self, operation: &str, fut: impl Future>, @@ -453,7 +453,10 @@ impl Database { }; // read guard dropped before any async I/O let mut total: u64 = 0; for store in &stores { - let n = store.count_all_non_destroyed().await.unwrap_or(0); // A single backend failure must not block the aggregate + let n = store.count_all_non_destroyed().await.unwrap_or_else(|e| { + cosmian_logger::warn!("[database] count_all_non_destroyed failed: {e}"); + 0 + }); total = total.saturating_add(n); } Ok(total) @@ -472,7 +475,10 @@ impl Database { }; // read guard dropped before any async I/O let mut total: u64 = 0; for store in &stores { - let n = store.count_non_destroyed_keys().await.unwrap_or(0); // A single backend failure must not block the aggregate + let n = store.count_non_destroyed_keys().await.unwrap_or_else(|e| { + cosmian_logger::warn!("[database] count_non_destroyed_keys failed: {e}"); + 0 + }); total = total.saturating_add(n); } Ok(total) diff --git a/crate/server_database/src/core/database_permissions.rs b/crate/server_database/src/core/database_permissions.rs index af3031d125..19bd416946 100644 --- a/crate/server_database/src/core/database_permissions.rs +++ b/crate/server_database/src/core/database_permissions.rs @@ -1,7 +1,4 @@ -use std::{ - collections::{HashMap, HashSet}, - time::Instant, -}; +use std::collections::{HashMap, HashSet}; use cosmian_kmip::{kmip_0::kmip_types::State, kmip_2_1::KmipOperation}; @@ -19,17 +16,10 @@ impl Database { &self, user: &str, ) -> DbResult)>> { - 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?) + self.record("list_user_ops_granted", async move { + Ok(self.permissions.list_user_operations_granted(user).await?) + }) + .await } /// List all the KMIP operations granted per `user` on the given object @@ -38,17 +28,10 @@ impl Database { &self, uid: &str, ) -> DbResult>> { - 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?) + self.record("list_object_ops_granted", async move { + Ok(self.permissions.list_object_operations_granted(uid).await?) + }) + .await } /// Grant the ability to `user` to perform the KMIP `operations` @@ -59,20 +42,13 @@ impl Database { user: &str, operations: HashSet, ) -> DbResult<()> { - let start = Instant::now(); - let result = self - .permissions - .grant_operations(uid, user, operations) - .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?) + self.record("grant_ops", async move { + Ok(self + .permissions + .grant_operations(uid, user, operations) + .await?) + }) + .await } /// Remove the ability to `user` to perform the `operations` @@ -83,20 +59,13 @@ impl Database { user: &str, operations: HashSet, ) -> DbResult<()> { - let start = Instant::now(); - let result = self - .permissions - .remove_operations(uid, user, operations) - .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?) + self.record("remove_ops", async move { + Ok(self + .permissions + .remove_operations(uid, user, operations) + .await?) + }) + .await } /// List all the operations that have been granted to a user on an object @@ -109,19 +78,12 @@ impl Database { user: &str, no_inherited_access: bool, ) -> DbResult> { - let start = Instant::now(); - let result = self - .permissions - .list_user_operations_on_object(uid, user, no_inherited_access) - .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?) + self.record("list_user_ops_on_object", async move { + Ok(self + .permissions + .list_user_operations_on_object(uid, user, no_inherited_access) + .await?) + }) + .await } } diff --git a/crate/server_database/src/stores/sql/sqlite.rs b/crate/server_database/src/stores/sql/sqlite.rs index a394cdbbf0..cd1ec15afe 100644 --- a/crate/server_database/src/stores/sql/sqlite.rs +++ b/crate/server_database/src/stores/sql/sqlite.rs @@ -1065,8 +1065,16 @@ mod tests { /// End-to-end: insert rows directly via SQL and verify both count methods /// return the expected value. Uses raw SQL to avoid pulling in the full KMIP /// object-construction machinery. + /// + /// `assert_eq!` is the appropriate tool for test assertions; `Result` return + /// is required to propagate setup errors via `?`. The combination is intentional. #[tokio::test] - async fn test_count_non_destroyed_returns_correct_value() -> Result<(), Box> { + #[expect( + clippy::panic_in_result_fn, + reason = "assertions are the test mechanism; Result return propagates async setup errors via ?" + )] + async fn test_count_non_destroyed_returns_correct_value() + -> Result<(), Box> { let dir = TempDir::new()?; let db_path = dir.path().join("test.db"); let pool = SqlitePool::instantiate(&db_path, true, None).await?; From 50efcb34e4346163ba8149ae70db9efb10e13a26 Mon Sep 17 00:00:00 2001 From: HatemMn <19950216+HatemMn@users.noreply.github.com> Date: Wed, 10 Jun 2026 13:40:17 +0200 Subject: [PATCH 11/28] fix: nix hash --- .github/reusable_scripts | 2 +- nix/expected-hashes/server.vendor.static.sha256 | 4 ++++ test_data | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/reusable_scripts b/.github/reusable_scripts index 27958a96a0..5216e05f11 160000 --- a/.github/reusable_scripts +++ b/.github/reusable_scripts @@ -1 +1 @@ -Subproject commit 27958a96a092ebb9d5340fddd5b5f72095a8e009 +Subproject commit 5216e05f11e37c472d75dac40818ea9e02c857dc diff --git a/nix/expected-hashes/server.vendor.static.sha256 b/nix/expected-hashes/server.vendor.static.sha256 index 986451d83f..af99df38a3 100644 --- a/nix/expected-hashes/server.vendor.static.sha256 +++ b/nix/expected-hashes/server.vendor.static.sha256 @@ -1 +1,5 @@ +<<<<<<< HEAD sha256-ZzuWLgecB3Hpz1TyDw0/ew6dihrrkdNP3iq+Ts8WEHw= +======= +sha256-npzFql9tG0Lx7Ij7c/hzKtIPIYvoWFpAE2/pl6qlIRQ= +>>>>>>> 3efb77ba (fix: nix hash) diff --git a/test_data b/test_data index 47c9a06b99..41871788ac 160000 --- a/test_data +++ b/test_data @@ -1 +1 @@ -Subproject commit 47c9a06b99f25439c70111d9bec7b67b2377527c +Subproject commit 41871788ac8b8ebbb990747828f25960863538c4 From cb982d97a09ce58d4752d075d6235e813d6cad93 Mon Sep 17 00:00:00 2001 From: HatemMn <19950216+HatemMn@users.noreply.github.com> Date: Wed, 10 Jun 2026 17:14:00 +0200 Subject: [PATCH 12/28] test: first test --- .github/scripts/test/test_otel_export.sh | 32 ++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/.github/scripts/test/test_otel_export.sh b/.github/scripts/test/test_otel_export.sh index d5ffa82afa..4ae9f416db 100755 --- a/.github/scripts/test/test_otel_export.sh +++ b/.github/scripts/test/test_otel_export.sh @@ -557,10 +557,42 @@ EOF # This is a hard/blocking requirement (script fails on mismatch/timeout). wait_for_metric_eq "kms_keys_active_count" 10 180 + # ── Step 3: kms.objects.total ──────────────────────────────────────────── + # Gauge; 10 creates on a virgin SQLite DB → exactly 10 live objects. + # Single label set so wait_for_metric_eq is safe here. + wait_for_metric_eq "kms_objects_total" 10 60 + + # ── Step 2: kms.http.requests.total ───────────────────────────────────── + # Counter split by status label ({status="200"} and {status="422"}). + # wait_for_metric_eq exits on the first matching Prometheus line and label + # ordering is non-deterministic, so == 20 would be fragile. + # wait_for_metric_gt scans ALL matching lines: the status=200 line carries + # value 20 which satisfies > 19. + wait_for_metric_gt "kms_http_requests_total" 19 60 + + # ── Step 1: kms.database.operations.total ─────────────────────────────── + # Counter split by operation/backend/outcome labels. Exact per-combo + # totals vary by internal code path; existence check is sufficient to + # confirm the metric is wired. + wait_for_metric_gt "kms_database_operations_total" 0 60 + + # ── Step 4: kms.cache.operations.total ────────────────────────────────── + # Counter; the AES Activate path exercises the unwrap-cache (miss then + # insert), so at least one observation is guaranteed. + wait_for_metric_gt "kms_cache_operations_total" 0 60 + # Echo what we observed to help diagnose CI flakiness. body=$(collector_metrics_body) observed_active_keys=$(metric_value_from_body "kms_keys_active_count" "${body}") + observed_objects=$(metric_value_from_body "kms_objects_total" "${body}") + observed_http=$(metric_value_from_body "kms_http_requests_total" "${body}") + observed_db=$(metric_value_from_body "kms_database_operations_total" "${body}") + observed_cache=$(metric_value_from_body "kms_cache_operations_total" "${body}") echo "Observed kms_keys_active_count=${observed_active_keys:-}" + echo "Observed kms_objects_total=${observed_objects:-}" + echo "Observed kms_http_requests_total=${observed_http:-}" + echo "Observed kms_database_operations_total=${observed_db:-}" + echo "Observed kms_cache_operations_total=${observed_cache:-}" echo "OTEL export integration script completed successfully." } From 64449f283de15011814c2983f7a68d56584ce0a3 Mon Sep 17 00:00:00 2001 From: HatemMn <19950216+HatemMn@users.noreply.github.com> Date: Thu, 11 Jun 2026 18:58:50 +0200 Subject: [PATCH 13/28] test: increase coverage --- .github/scripts/test/test_otel_export.sh | 58 +++- Cargo.lock | 274 ++++++++++++++++++ crate/server/Cargo.toml | 1 + crate/server/src/core/otel_metrics.rs | 206 +++++++++++-- .../src/core/database_objects.rs | 35 ++- 5 files changed, 537 insertions(+), 37 deletions(-) diff --git a/.github/scripts/test/test_otel_export.sh b/.github/scripts/test/test_otel_export.sh index 4ae9f416db..5bbaf182db 100755 --- a/.github/scripts/test/test_otel_export.sh +++ b/.github/scripts/test/test_otel_export.sh @@ -277,6 +277,24 @@ extract_uid() { perl -0777 -ne 'if (m/"tag"\s*:\s*"UniqueIdentifier".*?"value"\s*:\s*"([^"]+)"/s) { print "$1\n"; }' } +encrypt_with_key() { + local uid="$1" + # Encrypt a short plaintext with the given AES-256 key. + # This forces the server to call get_unwrapped_object, which records + # cache hits/misses in kms.cache.operations.total. + kmip_post "{\"tag\":\"Encrypt\",\"type\":\"Structure\",\"value\":[{\"tag\":\"UniqueIdentifier\",\"type\":\"TextString\",\"value\":\"${uid}\"},{\"tag\":\"Data\",\"type\":\"ByteString\",\"value\":\"48656c6c6f20576f726c6421\"}]}" +} + +# ── Wrapped-key helper (for kms.cache.operations.total) ────────────────────── +# Creates an AES-256 key that the server stores wrapped by kek_uid via the +# cosmian vendor attribute "wrapping_key_id". The key material is stored +# encrypted in the DB, so every Encrypt call triggers get_unwrapped() → +# cache miss/hit → kms.cache.operations.total increments. +create_aes_key_wrapped_by() { + local kek_uid="$1" + kmip_post "{\"tag\":\"Create\",\"type\":\"Structure\",\"value\":[{\"tag\":\"ObjectType\",\"type\":\"Enumeration\",\"value\":\"SymmetricKey\"},{\"tag\":\"Attributes\",\"value\":[{\"tag\":\"CryptographicAlgorithm\",\"type\":\"Enumeration\",\"value\":\"AES\"},{\"tag\":\"CryptographicLength\",\"type\":\"Integer\",\"value\":256},{\"tag\":\"CryptographicUsageMask\",\"type\":\"Integer\",\"value\":12},{\"tag\":\"KeyFormatType\",\"type\":\"Enumeration\",\"value\":\"TransparentSymmetricKey\"},{\"tag\":\"ObjectType\",\"type\":\"Enumeration\",\"value\":\"SymmetricKey\"},{\"tag\":\"Attribute\",\"value\":[{\"tag\":\"VendorIdentification\",\"type\":\"TextString\",\"value\":\"cosmian\"},{\"tag\":\"AttributeName\",\"type\":\"TextString\",\"value\":\"wrapping_key_id\"},{\"tag\":\"AttributeValue\",\"type\":\"TextString\",\"value\":\"${kek_uid}\"}]}]}]}" +} + wait_for_metric_gt() { local metric_name="$1" local min="$2" @@ -545,6 +563,15 @@ EOF activate_key "${uid}" >/dev/null done + # Trigger unwrap-cache path: first Encrypt → cache miss + insert, + # second Encrypt on same key → cache hit. + # NOTE: plain transparent keys skip get_unwrapped() (is_wrapped()==false), + # so these calls do NOT produce cache metrics. The actual cache coverage + # is in Step 4 below using a key stored with wrapped material. + echo "Triggering two Encrypt calls on key ${uid}..." + encrypt_with_key "${uid}" >/dev/null + encrypt_with_key "${uid}" >/dev/null + echo "Waiting for exported metrics (uptime + active keys)..." # Prefer an explicit uptime metric if present, otherwise fall back @@ -577,8 +604,35 @@ EOF wait_for_metric_gt "kms_database_operations_total" 0 60 # ── Step 4: kms.cache.operations.total ────────────────────────────────── - # Counter; the AES Activate path exercises the unwrap-cache (miss then - # insert), so at least one observation is guaranteed. + # Plain transparent keys bypass get_unwrapped() (is_wrapped() == false). + # Create a key stored wrapped by a KEK via cosmian vendor attribute + # "wrapping_key_id": the server encrypts its material at rest with the KEK, + # so every subsequent Encrypt call triggers get_unwrapped() → cache metrics. + echo "Building wrapped-key scenario for kms.cache.operations.total..." + cache_kek_uid=$(create_aes_key | extract_uid) + if [ -z "${cache_kek_uid}" ]; then + echo "ERROR: failed to create KEK for cache scenario" >&2 + exit 1 + fi + activate_key "${cache_kek_uid}" >/dev/null + cache_data_uid=$(create_aes_key_wrapped_by "${cache_kek_uid}" | extract_uid) + if [ -z "${cache_data_uid}" ]; then + echo "ERROR: failed to create wrapped data key for cache scenario" >&2 + exit 1 + fi + activate_key "${cache_data_uid}" >/dev/null + # First Encrypt: get_unwrapped() → cache miss + insert. + enc1_resp=$(encrypt_with_key "${cache_data_uid}") + if ! printf '%s' "${enc1_resp}" | grep -q '"tag"[[:space:]]*:[[:space:]]*"EncryptResponse"'; then + echo "ERROR: first Encrypt on wrapped key failed: ${enc1_resp}" >&2 + exit 1 + fi + # Second Encrypt: get_unwrapped() → cache hit. + enc2_resp=$(encrypt_with_key "${cache_data_uid}") + if ! printf '%s' "${enc2_resp}" | grep -q '"tag"[[:space:]]*:[[:space:]]*"EncryptResponse"'; then + echo "ERROR: second Encrypt on wrapped key failed: ${enc2_resp}" >&2 + exit 1 + fi wait_for_metric_gt "kms_cache_operations_total" 0 60 # Echo what we observed to help diagnose CI flakiness. diff --git a/Cargo.lock b/Cargo.lock index 473c253045..0f87296212 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -474,6 +474,105 @@ dependencies = [ "wait-timeout", ] +[[package]] +name = "async-channel" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" +dependencies = [ + "concurrent-queue", + "event-listener 2.5.3", + "futures-core", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-global-executor" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" +dependencies = [ + "async-channel 2.5.0", + "async-executor", + "async-io", + "async-lock", + "blocking", + "futures-lite", + "once_cell", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener 5.4.1", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel 2.5.0", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener 5.4.1", + "futures-lite", + "rustix", +] + [[package]] name = "async-recursion" version = "1.1.1" @@ -485,6 +584,51 @@ dependencies = [ "syn", ] +[[package]] +name = "async-signal" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-std" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c8e079a4ab67ae52b7403632e4618815d6db36d2a010cfe41b02c1b1578f93b" +dependencies = [ + "async-channel 1.9.0", + "async-global-executor", + "async-io", + "async-lock", + "async-process", + "crossbeam-utils", + "futures-channel", + "futures-core", + "futures-io", + "futures-lite", + "gloo-timers", + "kv-log-macro", + "log", + "memchr", + "once_cell", + "pin-project-lite", + "pin-utils", + "slab", + "wasm-bindgen-futures", +] + [[package]] name = "async-stream" version = "0.3.6" @@ -507,6 +651,12 @@ dependencies = [ "syn", ] +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + [[package]] name = "async-trait" version = "0.1.89" @@ -656,12 +806,25 @@ dependencies = [ ] [[package]] +<<<<<<< HEAD name = "block-buffer" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" dependencies = [ "hybrid-array 0.4.12", +======= +name = "blocking" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel 2.5.0", + "async-task", + "futures-io", + "futures-lite", + "piper", +>>>>>>> a3b57845 (test: increase coverage) ] [[package]] @@ -924,6 +1087,15 @@ dependencies = [ "tokio-util", ] +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "console" version = "0.15.11" @@ -2071,6 +2243,33 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener 5.4.1", + "pin-project-lite", +] + [[package]] name = "fallible-iterator" version = "0.2.0" @@ -2242,6 +2441,19 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + [[package]] name = "futures-macro" version = "0.3.32" @@ -2377,6 +2589,18 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" +[[package]] +name = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + [[package]] name = "governor" version = "0.10.4" @@ -3043,6 +3267,15 @@ dependencies = [ "syn", ] +[[package]] +name = "kv-log-macro" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" +dependencies = [ + "log", +] + [[package]] name = "language-tags" version = "0.3.2" @@ -3155,6 +3388,9 @@ name = "log" version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +dependencies = [ + "value-bag", +] [[package]] name = "lru" @@ -3727,6 +3963,7 @@ version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "231e9d6ceef9b0b2546ddf52335785ce41252bc7474ee8ba05bfad277be13ab8" dependencies = [ + "async-std", "async-trait", "futures-channel", "futures-executor", @@ -3794,6 +4031,12 @@ dependencies = [ "sha2 0.10.9", ] +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "parking_lot" version = "0.12.5" @@ -3904,6 +4147,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "piper" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + [[package]] name = "pkcs1" version = "0.7.5" @@ -3937,6 +4191,20 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "windows-sys 0.61.2", +] + [[package]] name = "poly1305" version = "0.8.0" @@ -5890,6 +6158,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +[[package]] +name = "value-bag" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ba6f5989077681266825251a52748b8c1d8a4ad098cc37e440103d0ea717fc0" + [[package]] name = "vcpkg" version = "0.2.15" diff --git a/crate/server/Cargo.toml b/crate/server/Cargo.toml index c3242f2a3f..25e1d8c070 100644 --- a/crate/server/Cargo.toml +++ b/crate/server/Cargo.toml @@ -125,6 +125,7 @@ actix-http = { workspace = true } cosmian_kms_client_utils = { path = "../clients/client_utils", version = "5.23.0" } cosmian_kms_interfaces = { path = "../interfaces", version = "5.23.0" } native-tls = { workspace = true } +opentelemetry_sdk = { workspace = true, features = ["testing"] } pem = { workspace = true } tempfile = { workspace = true } diff --git a/crate/server/src/core/otel_metrics.rs b/crate/server/src/core/otel_metrics.rs index 18cec94c55..62e7df1c8c 100644 --- a/crate/server/src/core/otel_metrics.rs +++ b/crate/server/src/core/otel_metrics.rs @@ -500,50 +500,98 @@ impl DbMetricsRecorder for OtelMetrics { )] mod tests { use super::*; + use opentelemetry_sdk::{ + metrics::{PeriodicReader, data::Gauge as GaugeData, data::Sum}, + runtime, + testing::metrics::InMemoryMetricExporter, + }; + + // ── No-op provider — cheap, used only where value assertions aren't needed ── fn create_test_meter_provider() -> SdkMeterProvider { - // Create a simple no-op meter provider for testing - // We don't need to actually export metrics in tests - opentelemetry_sdk::metrics::SdkMeterProvider::builder().build() + SdkMeterProvider::builder().build() } - #[test] - fn test_metrics_creation() { - let meter_provider = create_test_meter_provider(); - let _metrics = OtelMetrics::new(meter_provider).expect("Failed to create metrics"); + // ── Observing setup: real exporter, values assertable after force_flush() ── + + fn setup_observing_metrics() -> (OtelMetrics, SdkMeterProvider, InMemoryMetricExporter) { + let exporter = InMemoryMetricExporter::default(); + let reader = PeriodicReader::builder(exporter.clone(), runtime::Tokio).build(); + let provider = SdkMeterProvider::builder().with_reader(reader).build(); + let provider_ref = provider.clone(); + let metrics = OtelMetrics::new(provider).expect("metrics init"); + (metrics, provider_ref, exporter) } - #[test] - fn test_kmip_operation_recording() { - let meter_provider = create_test_meter_provider(); - let metrics = OtelMetrics::new(meter_provider).expect("Failed to create metrics"); + // ── Value-reading helpers ───────────────────────────────────────────────── + + /// Sum of all data-point values for a u64 counter metric in the last exported batch. + fn last_counter_u64(exporter: &InMemoryMetricExporter, name: &str) -> u64 { + let batches = exporter.get_finished_metrics().unwrap_or_default(); + let Some(last) = batches.last() else { + return 0; + }; + for sm in &last.scope_metrics { + for metric in &sm.metrics { + if metric.name.as_ref() == name { + if let Some(sum) = metric.data.as_any().downcast_ref::>() { + return sum.data_points.iter().map(|dp| dp.value).sum(); + } + } + } + } + 0 + } - metrics.record_kmip_operation("Create", "user1"); - metrics.record_kmip_operation("Get", "user1"); - metrics.record_kmip_operation("Create", "user2"); + /// Net value of an i64 `UpDownCounter` (`Sum`) in the last exported batch. + fn last_updown_i64(exporter: &InMemoryMetricExporter, name: &str) -> i64 { + let batches = exporter.get_finished_metrics().unwrap_or_default(); + let Some(last) = batches.last() else { + return 0; + }; + for sm in &last.scope_metrics { + for metric in &sm.metrics { + if metric.name.as_ref() == name { + if let Some(sum) = metric.data.as_any().downcast_ref::>() { + return sum.data_points.iter().map(|dp| dp.value).sum(); + } + } + } + } + 0 + } - // Metrics are recorded, actual verification would require checking the exporter + /// Last recorded value of an i64 Gauge in the last exported batch. + fn last_gauge_i64(exporter: &InMemoryMetricExporter, name: &str) -> i64 { + let batches = exporter.get_finished_metrics().unwrap_or_default(); + let Some(last) = batches.last() else { + return 0; + }; + for sm in &last.scope_metrics { + for metric in &sm.metrics { + if metric.name.as_ref() == name { + if let Some(g) = metric.data.as_any().downcast_ref::>() { + return g.data_points.last().map_or(0, |dp| dp.value); + } + } + } + } + 0 } - #[test] - fn test_permission_recording() { - let meter_provider = create_test_meter_provider(); - let metrics = OtelMetrics::new(meter_provider).expect("Failed to create metrics"); + // ── Smoke tests (construction + no-panic; no value assertions needed) ───── - metrics.record_permission_grant("user1", "read"); - metrics.record_permission_grant("user1", "write"); - metrics.record_permission_grant("user2", "read"); + #[test] + fn test_metrics_creation() { + let _metrics = OtelMetrics::new(create_test_meter_provider()).expect("creation"); } #[test] fn test_active_users_tracking() { - let meter_provider = create_test_meter_provider(); - let metrics = OtelMetrics::new(meter_provider).expect("Failed to create metrics"); - + let metrics = OtelMetrics::new(create_test_meter_provider()).expect("creation"); metrics.update_active_user("user1"); metrics.update_active_user("user2"); metrics.update_active_user("user3"); - assert_eq!( metrics .active_users_tracker @@ -554,12 +602,110 @@ mod tests { ); } - #[test] - fn test_operation_duration() { - let meter_provider = create_test_meter_provider(); - let metrics = OtelMetrics::new(meter_provider).expect("Failed to create metrics"); + // ── Tests with value assertions ─────────────────────────────────────────── + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_kmip_operation_recording() { + let (metrics, provider, exporter) = setup_observing_metrics(); + metrics.record_kmip_operation("Create", "user1"); + metrics.record_kmip_operation("Get", "user1"); + metrics.record_kmip_operation("Create", "user2"); + provider.force_flush().expect("flush"); + assert_eq!(last_counter_u64(&exporter, "kms.kmip.operations.total"), 3); + } + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_permission_recording() { + let (metrics, provider, exporter) = setup_observing_metrics(); + metrics.record_permission_grant("user1", "read"); + metrics.record_permission_grant("user1", "write"); + metrics.record_permission_grant("user2", "read"); + provider.force_flush().expect("flush"); + assert_eq!( + last_counter_u64(&exporter, "kms.permissions.granted.total"), + 3 + ); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_operation_duration_exports_histogram_names() { + let (metrics, provider, exporter) = setup_observing_metrics(); metrics.record_kmip_operation_duration("Create", 0.123); metrics.record_database_operation("insert", MainDbKind::Sqlite, "success", 0.045); + provider.force_flush().expect("flush"); + let batches = exporter.get_finished_metrics().unwrap_or_default(); + let names: Vec<&str> = batches.last().map_or(vec![], |rm| { + rm.scope_metrics + .iter() + .flat_map(|sm| &sm.metrics) + .map(|m| m.name.as_ref()) + .collect() + }); + assert!( + names.contains(&"kms.kmip.operation.duration"), + "kmip histogram not exported" + ); + assert!( + names.contains(&"kms.database.operation.duration"), + "db histogram not exported" + ); + } + + // ── New tests for previously-untested methods ───────────────────────────── + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_record_http_request_increments_counter() { + let (metrics, provider, exporter) = setup_observing_metrics(); + metrics.record_http_request("POST", "/kmip/2_1", "200"); + metrics.record_http_request("GET", "/health", "200"); + metrics.record_http_request("POST", "/kmip/2_1", "422"); + provider.force_flush().expect("flush"); + assert_eq!(last_counter_u64(&exporter, "kms.http.requests.total"), 3); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_record_cache_operation_increments_counter() { + let (metrics, provider, exporter) = setup_observing_metrics(); + metrics.record_cache_operation("get", "miss"); + metrics.record_cache_operation("insert", "ok"); + metrics.record_cache_operation("get", "hit"); + provider.force_flush().expect("flush"); + assert_eq!(last_counter_u64(&exporter, "kms.cache.operations.total"), 3); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_record_hsm_operation_increments_counter() { + let (metrics, provider, exporter) = setup_observing_metrics(); + metrics.record_hsm_operation("Encrypt", "softhsm2"); + metrics.record_hsm_operation("Decrypt", "softhsm2"); + provider.force_flush().expect("flush"); + assert_eq!(last_counter_u64(&exporter, "kms.hsm.operations.total"), 2); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_update_objects_total_sets_gauge() { + let (metrics, provider, exporter) = setup_observing_metrics(); + metrics.update_objects_total(42); + provider.force_flush().expect("flush"); + assert_eq!(last_gauge_i64(&exporter, "kms.objects.total"), 42); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_active_connections_up_down() { + let (metrics, provider, exporter) = setup_observing_metrics(); + metrics.increment_active_connections(); + metrics.increment_active_connections(); + metrics.decrement_active_connections(); + provider.force_flush().expect("flush"); + assert_eq!(last_updown_i64(&exporter, "kms.active.connections"), 1); + } + + // ── MainDbKind::as_str correctness ──────────────────────────────────────── + + #[test] + fn test_main_db_kind_as_str() { + assert_eq!(MainDbKind::Sqlite.as_str(), "sqlite"); + assert_eq!(MainDbKind::Postgres.as_str(), "postgresql"); + assert_eq!(MainDbKind::Mysql.as_str(), "mysql"); } } diff --git a/crate/server_database/src/core/database_objects.rs b/crate/server_database/src/core/database_objects.rs index 98be802067..a02dbab8fe 100644 --- a/crate/server_database/src/core/database_objects.rs +++ b/crate/server_database/src/core/database_objects.rs @@ -592,17 +592,42 @@ mod tests { // list_user_operations_granted: exercises the permissions facade path. drop(db.list_user_operations_granted("test_user").await); + // retrieve_object on a non-existent uid → Ok(None) → outcome "success" + drop(db.retrieve_object("non-existent-uid-xyz").await); + + // find with no filters → Ok([]) → outcome "success" + drop(db.find(None, None, "test_user", false, "").await); + let recorded = calls.lock().expect("mutex poisoned").clone(); + + // At least 3 calls recorded (one per method above) assert!( - !recorded.is_empty(), - "Expected the mock recorder to be called but got zero calls" + recorded.len() >= 3, + "Expected ≥ 3 recorded calls, got {}", + recorded.len() ); - // Every recorded call must use "sqlite" as the backend. - for (op, backend, _outcome) in &recorded { + + // All outcomes must be "success" — these operations cannot fail on an empty DB. + for (op, backend, outcome) in &recorded { assert_eq!( backend, "sqlite", - "Expected backend 'sqlite' for operation '{op}', got '{backend}'" + "Expected backend 'sqlite' for op '{op}', got '{backend}'" + ); + assert_eq!( + outcome, "success", + "Expected outcome 'success' for op '{op}', got '{outcome}'" ); } + + // Operation names present in the recorded set. + let op_names: Vec<&str> = recorded.iter().map(|(op, _, _)| op.as_str()).collect(); + assert!( + op_names.contains(&"retrieve"), + "recorder missing 'retrieve' op; got: {op_names:?}" + ); + assert!( + op_names.contains(&"find"), + "recorder missing 'find' op; got: {op_names:?}" + ); } } From 439363028e0134e299e2ccab9e4436667bf88d84 Mon Sep 17 00:00:00 2001 From: HatemMn <19950216+HatemMn@users.noreply.github.com> Date: Thu, 11 Jun 2026 20:36:21 +0200 Subject: [PATCH 14/28] feat: add proper docs --- documentation/docs/configuration/logging.md | 3 + .../docs/configuration/otlp-metrics.md | 76 +++++++++++++++++++ documentation/mkdocs.yml | 4 +- monitoring/OTLP_METRICS.md | 67 ++++++++++------ 4 files changed, 127 insertions(+), 23 deletions(-) create mode 100644 documentation/docs/configuration/otlp-metrics.md diff --git a/documentation/docs/configuration/logging.md b/documentation/docs/configuration/logging.md index 4443820ec0..e34b8d022a 100644 --- a/documentation/docs/configuration/logging.md +++ b/documentation/docs/configuration/logging.md @@ -139,6 +139,9 @@ Enable the feature with: - the `--enable-metering` command line argument, - or the equivalent TOML key in the `[logging]` section. +For the full list of emitted metrics, their types, and label sets, see the +[Metrics reference](./otlp-metrics.md). + --- ## Observability stack (OTel Collector + VictoriaMetrics + Grafana) diff --git a/documentation/docs/configuration/otlp-metrics.md b/documentation/docs/configuration/otlp-metrics.md new file mode 100644 index 0000000000..e5c8988484 --- /dev/null +++ b/documentation/docs/configuration/otlp-metrics.md @@ -0,0 +1,76 @@ +# OTLP Metrics Reference + +The KMS server pushes metrics to any [OpenTelemetry](https://opentelemetry.io/) collector via +**OTLP/gRPC every 30 seconds**. No HTTP `/metrics` endpoint is exposed — metrics are always +pushed, never scraped. + +For deployment instructions and Grafana setup, see [Monitoring Setup](./monitoring-setup.md). +To enable the feature, see [Telemetry & Observability](./logging.md). + +## KMIP Operations + +| Metric | Type | Description | Labels | +|--------|------|-------------|--------| +| `kms.kmip.operations.total` | counter | Total KMIP operations executed | `operation` | +| `kms.kmip.operations.per_user.total` | counter | Total KMIP operations per user | `operation`, `user` | +| `kms.kmip.operation.duration` | histogram (s) | Duration of each KMIP operation | `operation` | + +## Users & Permissions + +| Metric | Type | Description | Labels | +|--------|------|-------------|--------| +| `kms.active.users` | up-down counter | Unique users who issued at least one request | — | +| `kms.permissions.granted.per_user.total` | counter | Access rights granted, broken down by user | `user`, `permission_type` | +| `kms.permissions.granted.total` | counter | Total access rights granted | — | + +## Database + +| Metric | Type | Description | Labels | +|--------|------|-------------|--------| +| `kms.database.operations.total` | counter | DB operations by type and result | `operation`, `backend`, `outcome` | +| `kms.database.operation.duration` | histogram (s) | Wall-clock time of each DB call | `operation`, `backend`, `outcome` | + +**Label values:** + +- `backend`: `sqlite` · `postgresql` · `mysql` · `redis` +- `outcome`: `success` · `error` + +## HTTP + +| Metric | Type | Description | Labels | +|--------|------|-------------|--------| +| `kms.http.requests.total` | counter | Incoming HTTP requests | `method`, `path`, `status` | +| `kms.http.request.duration` | histogram (s) | HTTP request latency | `method`, `path` | + +`path` is normalised (e.g. `/kmip/2_1`, `/google_cse/...`) to avoid high cardinality from +object identifiers. + +## Server Health + +| Metric | Type | Description | Labels | +|--------|------|-------------|--------| +| `kms.server.uptime` | counter (monotonic, s) | Seconds elapsed since server start | — | +| `kms.server.start_time` | up-down counter | Server start time as Unix timestamp (s) | — | +| `kms.active.connections` | up-down counter | Current open HTTP connections | — | +| `kms.errors.total` | counter | Errors categorised by type | `error_type` | + +## Objects & Keys + +| Metric | Type | Description | Labels | +|--------|------|-------------|--------| +| `kms.objects.total` | up-down counter | Total non-destroyed objects in the KMS | — | +| `kms.keys.active.count` | up-down counter | Non-destroyed key objects (SymmetricKey, PrivateKey, PublicKey, SplitKey) across all states: PreActive, Active, Deactivated, Compromised | — | + +Both metrics are refreshed every 30 s by the metrics cron task and seeded at server startup. + +## Cache + +| Metric | Type | Description | Labels | +|--------|------|-------------|--------| +| `kms.cache.operations.total` | counter | Unwrap-cache lookups | `operation`, `result` | + +## HSM + +| Metric | Type | Description | Labels | +|--------|------|-------------|--------| +| `kms.hsm.operations.total` | counter | HSM operations by type and model | `operation`, `hsm_model` | diff --git a/documentation/mkdocs.yml b/documentation/mkdocs.yml index 1d2dd43f5f..74dc3aa1b6 100644 --- a/documentation/mkdocs.yml +++ b/documentation/mkdocs.yml @@ -140,7 +140,9 @@ nav: - Authorizing users with access rights: configuration/authorization.md - Enabling TLS: configuration/tls.md - Logging and telemetry: configuration/logging.md - - Monitoring: configuration/monitoring-setup.md + - Monitoring: + - Setup: configuration/monitoring-setup.md + - Metrics reference: configuration/otlp-metrics.md - User interface: configuration/ui.md - UI branding: configuration/ui_branding.md - Custom OpenSSL build: configuration/openssl_override.md diff --git a/monitoring/OTLP_METRICS.md b/monitoring/OTLP_METRICS.md index e0a393cfe0..9f3ac3e8a2 100644 --- a/monitoring/OTLP_METRICS.md +++ b/monitoring/OTLP_METRICS.md @@ -84,45 +84,66 @@ The server exposes the following instruments via OTLP, as implemented in `crate/ ### KMIP Operations -- `kms.kmip.operations.total` — Total KMIP operations executed (counter) -- `kms.kmip.operations.per_user.total` — Total KMIP operations per user (counter) -- `kms.kmip.operation.duration` — Duration of KMIP operations in seconds (histogram) +| Metric | Type | Labels | +|--------|------|--------| +| `kms.kmip.operations.total` | counter | `operation` | +| `kms.kmip.operations.per_user.total` | counter | `operation`, `user` | +| `kms.kmip.operation.duration` | histogram (s) | `operation` | ### Users & Permissions -- `kms.active.users` — Number of unique active users (up-down counter) -- `kms.permissions.granted.per_user.total` — Permissions granted per user (counter) -- `kms.permissions.granted.total` — Total permissions granted (counter) +| Metric | Type | Labels | +|--------|------|--------| +| `kms.active.users` | up-down counter | — | +| `kms.permissions.granted.per_user.total` | counter | `user`, `permission_type` | +| `kms.permissions.granted.total` | counter | — | ### Database Metrics -- `kms.database.operations.total` — Total database operations (counter) -- `kms.database.operation.duration` — Database operation duration in seconds (histogram) +| Metric | Type | Labels | +|--------|------|--------| +| `kms.database.operations.total` | counter | `operation`, `backend`, `outcome` | +| `kms.database.operation.duration` | histogram (s) | `operation`, `backend`, `outcome` | + +`backend`: `sqlite` · `postgresql` · `mysql` · `redis` — `outcome`: `success` · `error` ### HTTP Metrics -- `kms.http.requests.total` — Total HTTP requests (counter) -- `kms.http.request.duration` — HTTP request duration in seconds (histogram) +| Metric | Type | Labels | +|--------|------|--------| +| `kms.http.requests.total` | counter | `method`, `path`, `status` | +| `kms.http.request.duration` | histogram (s) | `method`, `path` | ### Server Health -- `kms.server.uptime` — Server uptime in seconds (counter) -- `kms.server.start_time` — Server start time as Unix timestamp (up-down counter) -- `kms.active.connections` — Current number of active connections (up-down counter) -- `kms.errors.total` — Total number of errors by type (counter) +| Metric | Type | Labels | +|--------|------|--------| +| `kms.server.uptime` | counter (monotonic, s) | — | +| `kms.server.start_time` | up-down counter | — | +| `kms.active.connections` | up-down counter | — | +| `kms.errors.total` | counter | `error_type` | ### Objects & Keys -- `kms.objects.total` — Total number of objects (up-down counter) -- `kms.keys.active.count` — Number of keys in Active state (up-down counter; absolute count applied via delta) +| Metric | Type | Labels | +|--------|------|--------| +| `kms.objects.total` | up-down counter | — | +| `kms.keys.active.count` | up-down counter | — | + +`kms.keys.active.count` counts all **non-destroyed** key objects (SymmetricKey, PrivateKey, +PublicKey, SplitKey) across all non-terminal states: PreActive, Active, Deactivated, Compromised. ### Cache -- `kms.cache.operations.total` — Total cache operations (counter) +| Metric | Type | Labels | +|--------|------|--------| +| `kms.cache.operations.total` | counter | `operation`, `result` | ### HSM -- `kms.hsm.operations.total` — Total HSM operations (counter) +| Metric | Type | Labels | +|--------|------|--------| +| `kms.hsm.operations.total` | counter | `operation`, `hsm_model` | ## OTLP Collector Configuration @@ -269,7 +290,9 @@ docker compose --profile otel-test logs -f otel-collector ## Additional Resources -- [OpenTelemetry Documentation](https://opentelemetry.io/docs/) -- [OTLP Specification](https://opentelemetry.io/docs/specs/otlp/) -- [Jaeger Documentation](https://www.jaegertracing.io/docs/) -- [Grafana Documentation](https://grafana.com/docs/) +- [OpenTelemetry documentation](https://opentelemetry.io/docs/) +- [OTLP specification](https://opentelemetry.io/docs/specs/otlp/) +- [Jaeger documentation](https://www.jaegertracing.io/docs/) +- [Grafana documentation](https://grafana.com/docs/) +- [Cosmian KMS metrics reference](https://docs.cosmian.com/key_management_system/configuration/otlp-metrics/) +- [Cosmian KMS monitoring documentation](https://docs.cosmian.com/key_management_system/configuration/monitoring-setup/) From 510ffd09f0ee6a989ea9f94419918a12c99e204a Mon Sep 17 00:00:00 2001 From: HatemMn <19950216+HatemMn@users.noreply.github.com> Date: Fri, 12 Jun 2026 13:37:45 +0200 Subject: [PATCH 15/28] test: multiple updates --- .github/scripts/test/test_otel_export.sh | 21 + Cargo.lock | 446 +------------------- Cargo.toml | 6 +- crate/server/src/core/kms/mod.rs | 9 +- crate/server/src/core/otel_metrics.rs | 8 +- nix/expected-hashes/cli.vendor.linux.sha256 | 4 + 6 files changed, 57 insertions(+), 437 deletions(-) diff --git a/.github/scripts/test/test_otel_export.sh b/.github/scripts/test/test_otel_export.sh index 5bbaf182db..b660601a90 100755 --- a/.github/scripts/test/test_otel_export.sh +++ b/.github/scripts/test/test_otel_export.sh @@ -603,6 +603,23 @@ EOF # confirm the metric is wired. wait_for_metric_gt "kms_database_operations_total" 0 60 + # ── Step 1b: kms.database.operation.duration (histogram) ──────────────── + # OTel appends the unit suffix: kms.database.operation.duration[seconds] → + # kms_database_operation_duration_seconds_{bucket,count,sum}. + wait_for_metric_gt "kms_database_operation_duration_seconds_count" 0 60 + + # ── Step 2b: kms.http.request.duration (histogram) ────────────────────── + wait_for_metric_gt "kms_http_request_duration_seconds_count" 0 60 + + # ── Step 2c: label correctness ────────────────────────────────────────── + # Spot-check that at least one DB operation was recorded with backend="sqlite". + body=$(collector_metrics_body) + if ! printf '%s' "${body}" | grep -q 'kms_database_operations_total{.*backend="sqlite"'; then + echo "ERROR: kms_database_operations_total missing backend=\"sqlite\" label" >&2 + printf '%s\n' "${body}" | grep "kms_database" >&2 + exit 1 + fi + # ── Step 4: kms.cache.operations.total ────────────────────────────────── # Plain transparent keys bypass get_unwrapped() (is_wrapped() == false). # Create a key stored wrapped by a KEK via cosmian vendor attribute @@ -640,12 +657,16 @@ EOF observed_active_keys=$(metric_value_from_body "kms_keys_active_count" "${body}") observed_objects=$(metric_value_from_body "kms_objects_total" "${body}") observed_http=$(metric_value_from_body "kms_http_requests_total" "${body}") + observed_http_dur=$(metric_value_from_body "kms_http_request_duration_seconds_count" "${body}") observed_db=$(metric_value_from_body "kms_database_operations_total" "${body}") + observed_db_dur=$(metric_value_from_body "kms_database_operation_duration_seconds_count" "${body}") observed_cache=$(metric_value_from_body "kms_cache_operations_total" "${body}") echo "Observed kms_keys_active_count=${observed_active_keys:-}" echo "Observed kms_objects_total=${observed_objects:-}" echo "Observed kms_http_requests_total=${observed_http:-}" + echo "Observed kms_http_request_duration_seconds_count=${observed_http_dur:-}" echo "Observed kms_database_operations_total=${observed_db:-}" + echo "Observed kms_database_operation_duration_seconds_count=${observed_db_dur:-}" echo "Observed kms_cache_operations_total=${observed_cache:-}" echo "OTEL export integration script completed successfully." diff --git a/Cargo.lock b/Cargo.lock index 0f87296212..9a3993df4e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -474,105 +474,6 @@ dependencies = [ "wait-timeout", ] -[[package]] -name = "async-channel" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" -dependencies = [ - "concurrent-queue", - "event-listener 2.5.3", - "futures-core", -] - -[[package]] -name = "async-channel" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" -dependencies = [ - "concurrent-queue", - "event-listener-strategy", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-executor" -version = "1.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" -dependencies = [ - "async-task", - "concurrent-queue", - "fastrand", - "futures-lite", - "pin-project-lite", - "slab", -] - -[[package]] -name = "async-global-executor" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" -dependencies = [ - "async-channel 2.5.0", - "async-executor", - "async-io", - "async-lock", - "blocking", - "futures-lite", - "once_cell", -] - -[[package]] -name = "async-io" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" -dependencies = [ - "autocfg", - "cfg-if", - "concurrent-queue", - "futures-io", - "futures-lite", - "parking", - "polling", - "rustix", - "slab", - "windows-sys 0.61.2", -] - -[[package]] -name = "async-lock" -version = "3.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" -dependencies = [ - "event-listener 5.4.1", - "event-listener-strategy", - "pin-project-lite", -] - -[[package]] -name = "async-process" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" -dependencies = [ - "async-channel 2.5.0", - "async-io", - "async-lock", - "async-signal", - "async-task", - "blocking", - "cfg-if", - "event-listener 5.4.1", - "futures-lite", - "rustix", -] - [[package]] name = "async-recursion" version = "1.1.1" @@ -584,79 +485,6 @@ dependencies = [ "syn", ] -[[package]] -name = "async-signal" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" -dependencies = [ - "async-io", - "async-lock", - "atomic-waker", - "cfg-if", - "futures-core", - "futures-io", - "rustix", - "signal-hook-registry", - "slab", - "windows-sys 0.61.2", -] - -[[package]] -name = "async-std" -version = "1.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c8e079a4ab67ae52b7403632e4618815d6db36d2a010cfe41b02c1b1578f93b" -dependencies = [ - "async-channel 1.9.0", - "async-global-executor", - "async-io", - "async-lock", - "async-process", - "crossbeam-utils", - "futures-channel", - "futures-core", - "futures-io", - "futures-lite", - "gloo-timers", - "kv-log-macro", - "log", - "memchr", - "once_cell", - "pin-project-lite", - "pin-utils", - "slab", - "wasm-bindgen-futures", -] - -[[package]] -name = "async-stream" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" -dependencies = [ - "async-stream-impl", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-stream-impl" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "async-task" -version = "4.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" - [[package]] name = "async-trait" version = "0.1.89" @@ -680,53 +508,6 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" -[[package]] -name = "axum" -version = "0.7.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" -dependencies = [ - "async-trait", - "axum-core", - "bytes", - "futures-util", - "http 1.4.0", - "http-body", - "http-body-util", - "itoa", - "matchit", - "memchr", - "mime", - "percent-encoding", - "pin-project-lite", - "rustversion", - "serde", - "sync_wrapper", - "tower 0.5.2", - "tower-layer", - "tower-service", -] - -[[package]] -name = "axum-core" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" -dependencies = [ - "async-trait", - "bytes", - "futures-util", - "http 1.4.0", - "http-body", - "http-body-util", - "mime", - "pin-project-lite", - "rustversion", - "sync_wrapper", - "tower-layer", - "tower-service", -] - [[package]] name = "backon" version = "1.6.0" @@ -807,6 +588,7 @@ dependencies = [ [[package]] <<<<<<< HEAD +<<<<<<< HEAD name = "block-buffer" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -828,6 +610,8 @@ dependencies = [ ] [[package]] +======= +>>>>>>> 801f9218 (test: multiple updates) name = "bstr" version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1087,15 +871,6 @@ dependencies = [ "tokio-util", ] -[[package]] -name = "concurrent-queue" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" -dependencies = [ - "crossbeam-utils", -] - [[package]] name = "console" version = "0.15.11" @@ -1524,9 +1299,9 @@ dependencies = [ "num-bigint-dig", "openssl", "openssl-sys", - "opentelemetry 0.27.1", - "opentelemetry-otlp 0.27.0", - "opentelemetry_sdk 0.27.1", + "opentelemetry", + "opentelemetry-otlp", + "opentelemetry_sdk", "pem", "proteccio_pkcs11_loader", "reqwest", @@ -1590,11 +1365,11 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "84cd0fe474e8ad66df5dfc5d0dfa02379e0a80c76da53ec659f68cfdd377f6d8" dependencies = [ - "opentelemetry 0.29.1", - "opentelemetry-otlp 0.29.0", + "opentelemetry", + "opentelemetry-otlp", "opentelemetry-semantic-conventions", "opentelemetry-stdout", - "opentelemetry_sdk 0.29.0", + "opentelemetry_sdk", "syslog-tracing", "thiserror 2.0.17", "tracing", @@ -2243,33 +2018,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "event-listener" -version = "2.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" - -[[package]] -name = "event-listener" -version = "5.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" -dependencies = [ - "concurrent-queue", - "parking", - "pin-project-lite", -] - -[[package]] -name = "event-listener-strategy" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" -dependencies = [ - "event-listener 5.4.1", - "pin-project-lite", -] - [[package]] name = "fallible-iterator" version = "0.2.0" @@ -2441,19 +2189,6 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" -[[package]] -name = "futures-lite" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" -dependencies = [ - "fastrand", - "futures-core", - "futures-io", - "parking", - "pin-project-lite", -] - [[package]] name = "futures-macro" version = "0.3.32" @@ -2589,18 +2324,6 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" -[[package]] -name = "gloo-timers" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" -dependencies = [ - "futures-channel", - "futures-core", - "js-sys", - "wasm-bindgen", -] - [[package]] name = "governor" version = "0.10.4" @@ -2887,7 +2610,6 @@ dependencies = [ "http 1.4.0", "http-body", "httparse", - "httpdate", "itoa", "pin-project-lite", "pin-utils", @@ -3267,15 +2989,6 @@ dependencies = [ "syn", ] -[[package]] -name = "kv-log-macro" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" -dependencies = [ - "log", -] - [[package]] name = "language-tags" version = "0.3.2" @@ -3388,9 +3101,6 @@ name = "log" version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" -dependencies = [ - "value-bag", -] [[package]] name = "lru" @@ -3425,12 +3135,6 @@ dependencies = [ "regex-automata", ] -[[package]] -name = "matchit" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" - [[package]] name = "md-5" version = "0.11.0" @@ -3834,20 +3538,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "opentelemetry" -version = "0.27.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab70038c28ed37b97d8ed414b6429d343a8bbf44c9f79ec854f3a643029ba6d7" -dependencies = [ - "futures-core", - "futures-sink", - "js-sys", - "pin-project-lite", - "thiserror 1.0.69", - "tracing", -] - [[package]] name = "opentelemetry" version = "0.29.1" @@ -3871,30 +3561,11 @@ dependencies = [ "async-trait", "bytes", "http 1.4.0", - "opentelemetry 0.29.1", + "opentelemetry", "reqwest", "tracing", ] -[[package]] -name = "opentelemetry-otlp" -version = "0.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91cf61a1868dacc576bf2b2a1c3e9ab150af7272909e80085c3173384fe11f76" -dependencies = [ - "async-trait", - "futures-core", - "http 1.4.0", - "opentelemetry 0.27.1", - "opentelemetry-proto 0.27.0", - "opentelemetry_sdk 0.27.1", - "prost", - "thiserror 1.0.69", - "tokio", - "tonic", - "tracing", -] - [[package]] name = "opentelemetry-otlp" version = "0.29.0" @@ -3903,10 +3574,10 @@ checksum = "d899720fe06916ccba71c01d04ecd77312734e2de3467fd30d9d580c8ce85656" dependencies = [ "futures-core", "http 1.4.0", - "opentelemetry 0.29.1", + "opentelemetry", "opentelemetry-http", - "opentelemetry-proto 0.29.0", - "opentelemetry_sdk 0.29.0", + "opentelemetry-proto", + "opentelemetry_sdk", "prost", "reqwest", "thiserror 2.0.17", @@ -3915,26 +3586,14 @@ dependencies = [ "tracing", ] -[[package]] -name = "opentelemetry-proto" -version = "0.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6e05acbfada5ec79023c85368af14abd0b307c015e9064d249b2a950ef459a6" -dependencies = [ - "opentelemetry 0.27.1", - "opentelemetry_sdk 0.27.1", - "prost", - "tonic", -] - [[package]] name = "opentelemetry-proto" version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c40da242381435e18570d5b9d50aca2a4f4f4d8e146231adb4e7768023309b3" dependencies = [ - "opentelemetry 0.29.1", - "opentelemetry_sdk 0.29.0", + "opentelemetry", + "opentelemetry_sdk", "prost", "tonic", ] @@ -3953,30 +3612,8 @@ checksum = "a7e27d446dabd68610ef0b77d07b102ecde827a4596ea9c01a4d3811e945b286" dependencies = [ "chrono", "futures-util", - "opentelemetry 0.29.1", - "opentelemetry_sdk 0.29.0", -] - -[[package]] -name = "opentelemetry_sdk" -version = "0.27.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "231e9d6ceef9b0b2546ddf52335785ce41252bc7474ee8ba05bfad277be13ab8" -dependencies = [ - "async-std", - "async-trait", - "futures-channel", - "futures-executor", - "futures-util", - "glob", - "opentelemetry 0.27.1", - "percent-encoding", - "rand 0.8.6", - "serde_json", - "thiserror 1.0.69", - "tokio", - "tokio-stream", - "tracing", + "opentelemetry", + "opentelemetry_sdk", ] [[package]] @@ -3989,11 +3626,13 @@ dependencies = [ "futures-executor", "futures-util", "glob", - "opentelemetry 0.29.1", + "opentelemetry", "percent-encoding", "rand 0.9.4", "serde_json", "thiserror 2.0.17", + "tokio", + "tokio-stream", "tracing", ] @@ -4031,12 +3670,6 @@ dependencies = [ "sha2 0.10.9", ] -[[package]] -name = "parking" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" - [[package]] name = "parking_lot" version = "0.12.5" @@ -4147,17 +3780,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" -[[package]] -name = "piper" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" -dependencies = [ - "atomic-waker", - "fastrand", - "futures-io", -] - [[package]] name = "pkcs1" version = "0.7.5" @@ -4191,20 +3813,6 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" -[[package]] -name = "polling" -version = "3.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" -dependencies = [ - "cfg-if", - "concurrent-queue", - "hermit-abi", - "pin-project-lite", - "rustix", - "windows-sys 0.61.2", -] - [[package]] name = "poly1305" version = "0.8.0" @@ -5826,12 +5434,9 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877c5b330756d856ffcc4553ab34a5684481ade925ecc54bcd1bf02b1d0d4d52" dependencies = [ - "async-stream", "async-trait", - "axum", "base64 0.22.1", "bytes", - "h2 0.4.12", "http 1.4.0", "http-body", "http-body-util", @@ -5841,7 +5446,6 @@ dependencies = [ "percent-encoding", "pin-project", "prost", - "socket2 0.5.10", "tokio", "tokio-stream", "tower 0.4.13", @@ -5989,8 +5593,8 @@ checksum = "fd8e764bd6f5813fd8bebc3117875190c5b0415be8f7f8059bffb6ecd979c444" dependencies = [ "js-sys", "once_cell", - "opentelemetry 0.29.1", - "opentelemetry_sdk 0.29.0", + "opentelemetry", + "opentelemetry_sdk", "smallvec", "tracing", "tracing-core", @@ -6158,12 +5762,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" -[[package]] -name = "value-bag" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ba6f5989077681266825251a52748b8c1d8a4ad098cc37e440103d0ea717fc0" - [[package]] name = "vcpkg" version = "0.2.15" diff --git a/Cargo.toml b/Cargo.toml index 2dcbc9d9c4..18acfde162 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -185,9 +185,9 @@ num-bigint = { version = "0.4", default-features = false } num-traits = "0.2" num-bigint-dig = { version = "0.8", default-features = false } openssl = { version = "0.10", default-features = false } -opentelemetry = "0.27" -opentelemetry-otlp = { version = "0.27", features = ["metrics", "grpc-tonic"] } -opentelemetry_sdk = { version = "0.27", features = ["metrics", "rt-tokio"] } +opentelemetry = "0.29" +opentelemetry-otlp = { version = "0.29", features = ["metrics", "grpc-tonic"] } +opentelemetry_sdk = { version = "0.29", features = ["metrics", "rt-tokio"] } pem = "3.0" pkcs11-sys = "0.2" rand = "0.10" diff --git a/crate/server/src/core/kms/mod.rs b/crate/server/src/core/kms/mod.rs index 820869ef26..6dd457eb57 100644 --- a/crate/server/src/core/kms/mod.rs +++ b/crate/server/src/core/kms/mod.rs @@ -235,10 +235,11 @@ impl KMS { KmsError::ServerError(format!("Failed to create OTLP metrics exporter: {e}")) })?; - // Create periodic reader that sends metrics every 30 seconds - let reader = PeriodicReader::builder(exporter, opentelemetry_sdk::runtime::Tokio) + // Create periodic reader that sends metrics every 30 seconds. + // otel_sdk 0.29: builder takes only the exporter (no runtime arg); + // export timeout is configured on the exporter, not the reader. + let reader = PeriodicReader::builder(exporter) .with_interval(std::time::Duration::from_secs(30)) - .with_timeout(std::time::Duration::from_secs(10)) .build(); // Create meter provider @@ -261,7 +262,7 @@ impl KMS { } let meter_provider = opentelemetry_sdk::metrics::SdkMeterProvider::builder() - .with_resource(Resource::new(resource_kvs)) + .with_resource(Resource::builder().with_attributes(resource_kvs).build()) .with_reader(reader) .build(); diff --git a/crate/server/src/core/otel_metrics.rs b/crate/server/src/core/otel_metrics.rs index 62e7df1c8c..aba02fdb33 100644 --- a/crate/server/src/core/otel_metrics.rs +++ b/crate/server/src/core/otel_metrics.rs @@ -500,11 +500,7 @@ impl DbMetricsRecorder for OtelMetrics { )] mod tests { use super::*; - use opentelemetry_sdk::{ - metrics::{PeriodicReader, data::Gauge as GaugeData, data::Sum}, - runtime, - testing::metrics::InMemoryMetricExporter, - }; + use opentelemetry_sdk::metrics::{InMemoryMetricExporter, PeriodicReader, data::Gauge as GaugeData, data::Sum}; // ── No-op provider — cheap, used only where value assertions aren't needed ── @@ -516,7 +512,7 @@ mod tests { fn setup_observing_metrics() -> (OtelMetrics, SdkMeterProvider, InMemoryMetricExporter) { let exporter = InMemoryMetricExporter::default(); - let reader = PeriodicReader::builder(exporter.clone(), runtime::Tokio).build(); + let reader = PeriodicReader::builder(exporter.clone()).build(); let provider = SdkMeterProvider::builder().with_reader(reader).build(); let provider_ref = provider.clone(); let metrics = OtelMetrics::new(provider).expect("metrics init"); diff --git a/nix/expected-hashes/cli.vendor.linux.sha256 b/nix/expected-hashes/cli.vendor.linux.sha256 index 2017701c61..eb1b6c7b42 100644 --- a/nix/expected-hashes/cli.vendor.linux.sha256 +++ b/nix/expected-hashes/cli.vendor.linux.sha256 @@ -1 +1,5 @@ +<<<<<<< HEAD sha256-BLYlBznRLu5RBetai9ZGsdb8C7cpm5PLCIf3/MJMOnM= +======= +sha256-lnIKTuxfQ10VVsBjSSU7gD7zkJA1XvdGCvxJzYVhMAc= +>>>>>>> 801f9218 (test: multiple updates) From 485ee7553290a94bfb88f9121d4512279a27cbe2 Mon Sep 17 00:00:00 2001 From: HatemMn <19950216+HatemMn@users.noreply.github.com> Date: Fri, 12 Jun 2026 14:55:34 +0200 Subject: [PATCH 16/28] fix: fixes --- nix/expected-hashes/server.vendor.static.sha256 | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/nix/expected-hashes/server.vendor.static.sha256 b/nix/expected-hashes/server.vendor.static.sha256 index af99df38a3..660bb8b718 100644 --- a/nix/expected-hashes/server.vendor.static.sha256 +++ b/nix/expected-hashes/server.vendor.static.sha256 @@ -1,5 +1,9 @@ <<<<<<< HEAD +<<<<<<< HEAD sha256-ZzuWLgecB3Hpz1TyDw0/ew6dihrrkdNP3iq+Ts8WEHw= ======= sha256-npzFql9tG0Lx7Ij7c/hzKtIPIYvoWFpAE2/pl6qlIRQ= >>>>>>> 3efb77ba (fix: nix hash) +======= +sha256-0UbqfLFumMDHXHDk0y7gZt62wJqxbHiJBFUhfZuX7Ro= +>>>>>>> e2d57f30 (fix: fixes) From 92ba71bb91ca6681868cdb99617815bbb10be482 Mon Sep 17 00:00:00 2001 From: HatemMn <19950216+HatemMn@users.noreply.github.com> Date: Fri, 12 Jun 2026 16:49:01 +0200 Subject: [PATCH 17/28] test: others --- crate/server/src/core/otel_metrics.rs | 79 +++++++++++++++---- .../src/middlewares/otel_http_middleware.rs | 12 +++ monitoring/OTLP_METRICS.md | 2 + 3 files changed, 76 insertions(+), 17 deletions(-) diff --git a/crate/server/src/core/otel_metrics.rs b/crate/server/src/core/otel_metrics.rs index aba02fdb33..e794532ee5 100644 --- a/crate/server/src/core/otel_metrics.rs +++ b/crate/server/src/core/otel_metrics.rs @@ -25,6 +25,21 @@ use opentelemetry_sdk::metrics::SdkMeterProvider; use crate::{error::KmsError, result::KResult}; +/// Maximum number of distinct user identities tracked in the active-users window. +/// +/// If this limit is reached, new users are not inserted into the tracker and the +/// per-user metric label for the current operation is substituted with +/// `"__overflow__"`. This limits Prometheus timeseries cardinality for the +/// `kms.kmip.operations.per_user.total` and +/// `kms.permissions.granted.per_user.total` metrics. +/// +/// Raise this constant if your deployment legitimately has more than 10_000 +/// distinct users active within any 1-hour window. +pub(crate) const MAX_TRACKED_CARDINALITY: usize = 10_000; + +/// Sentinel label value emitted when the cardinality cap is reached. +const OVERFLOW_USER_LABEL: &str = "__overflow__"; + /// OpenTelemetry metrics for KMS operations pub struct OtelMetrics { /// The meter used to create instruments @@ -285,10 +300,11 @@ impl OtelMetrics { pub fn record_kmip_operation(&self, operation: &str, user: &str) { self.kmip_operations_total .add(1, &[KeyValue::new("operation", operation.to_owned())]); + let effective_user = self.bounded_user_label(user); self.kmip_operations_per_user.add( 1, &[ - KeyValue::new("user", user.to_owned()), + KeyValue::new("user", effective_user), KeyValue::new("operation", operation.to_owned()), ], ); @@ -305,39 +321,66 @@ impl OtelMetrics { /// Record a permission grant pub fn record_permission_grant(&self, user: &str, permission_type: &str) { + let effective_user = self.bounded_user_label(user); self.permissions_granted_per_user.add( 1, &[ - KeyValue::new("user", user.to_owned()), + KeyValue::new("user", effective_user), KeyValue::new("permission_type", permission_type.to_owned()), ], ); self.permissions_granted_total.add(1, &[]); } - /// Update active user tracking + /// Returns the user label to use for per-user metrics. + /// + /// Returns the real user string if the cardinality cap has not been reached, + /// or `"__overflow__"` when it has. + fn bounded_user_label(&self, user: &str) -> String { + let tracker = self + .active_users_tracker + .read() + .unwrap_or_else(std::sync::PoisonError::into_inner); + if tracker.contains_key(user) || tracker.len() < MAX_TRACKED_CARDINALITY { + user.to_owned() + } else { + OVERFLOW_USER_LABEL.to_owned() + } + } + + /// Update active user tracking. + /// + /// Metric recording is best-effort: if the lock is poisoned (a previous + /// writer panicked), the poisoned value is recovered and tracking continues + /// rather than propagating a panic into the KMIP hot path. /// /// # Panics /// - /// Panics if system time is before `UNIX_EPOCH` or lock is poisoned - #[allow( - clippy::cast_possible_wrap, - clippy::expect_used, - clippy::as_conversions - )] + /// Panics if system time is before `UNIX_EPOCH` (only possible on systems + /// with a misconfigured clock; safe to treat as unrecoverable). + #[allow(clippy::expect_used)] pub fn update_active_user(&self, user: &str) { - #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)] - let now = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .expect("System time before UNIX_EPOCH") - .as_secs() as i64; + let now = i64::try_from( + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("System time before UNIX_EPOCH") + .as_secs(), + ) + .unwrap_or(i64::MAX); let mut tracker = self .active_users_tracker .write() - .expect("Active users tracker lock poisoned"); + // SAFETY: recover the inner value on lock poisoning so that a + // previous writer panic does not permanently break metric recording. + .unwrap_or_else(std::sync::PoisonError::into_inner); + + // Enforce cardinality cap: do not track new users beyond the limit. + if !tracker.contains_key(user) && tracker.len() >= MAX_TRACKED_CARDINALITY { + return; + } - let previous_len = tracker.len() as i64; + let previous_len = i64::try_from(tracker.len()).unwrap_or(i64::MAX); tracker.insert(user.to_owned(), now); // Clean up users inactive for more than 1 hour @@ -345,7 +388,7 @@ impl OtelMetrics { tracker.retain(|_, &mut last_seen| last_seen > cutoff); // Update gauge - calculate the delta - let current_len = tracker.len() as i64; + let current_len = i64::try_from(tracker.len()).unwrap_or(i64::MAX); let delta = current_len - previous_len; if delta != 0 { self.active_users.add(delta, &[]); @@ -703,5 +746,7 @@ mod tests { assert_eq!(MainDbKind::Sqlite.as_str(), "sqlite"); assert_eq!(MainDbKind::Postgres.as_str(), "postgresql"); assert_eq!(MainDbKind::Mysql.as_str(), "mysql"); + #[cfg(feature = "non-fips")] + assert_eq!(MainDbKind::RedisFindex.as_str(), "redis"); } } diff --git a/crate/server/src/middlewares/otel_http_middleware.rs b/crate/server/src/middlewares/otel_http_middleware.rs index 070943c4ae..b38e020244 100644 --- a/crate/server/src/middlewares/otel_http_middleware.rs +++ b/crate/server/src/middlewares/otel_http_middleware.rs @@ -157,6 +157,11 @@ fn normalize_path(path: &str) -> &'static str { if path.starts_with("/download-cli") { return "/download-cli"; } + // Some older KMIP clients use dot-notation (/kmip/2.1) instead of underscore. + // Map to the same label so metrics are not silently bucketed as /other. + if path.starts_with("/kmip/2.1") || path.starts_with("/kmip/1.") { + return "/kmip/2_1"; + } "/other" } @@ -209,6 +214,13 @@ mod tests { assert_eq!(normalize_path("/openapi/kms.yaml"), "/swagger/{...}"); } + #[test] + fn test_normalize_kmip_dot_notation() { + // Older clients may use /kmip/2.1 (dot) instead of /kmip/2_1 (underscore). + assert_eq!(normalize_path("/kmip/2.1"), "/kmip/2_1"); + assert_eq!(normalize_path("/kmip/1.4"), "/kmip/2_1"); + } + #[test] fn test_normalize_unknown_falls_to_other() { assert_eq!(normalize_path("/unknown/deep/path"), "/other"); diff --git a/monitoring/OTLP_METRICS.md b/monitoring/OTLP_METRICS.md index 9f3ac3e8a2..7a753e7d85 100644 --- a/monitoring/OTLP_METRICS.md +++ b/monitoring/OTLP_METRICS.md @@ -90,6 +90,8 @@ The server exposes the following instruments via OTLP, as implemented in `crate/ | `kms.kmip.operations.per_user.total` | counter | `operation`, `user` | | `kms.kmip.operation.duration` | histogram (s) | `operation` | +> **Note:** The `user` label in `kms.kmip.operations.per_user.total` and `kms.permissions.granted.per_user.total` carries whatever string the authentication middleware extracts (e.g. an OAuth subject, email address, or service-account identifier); operators connecting these metrics to a cloud OTLP backend should be aware that this value will be stored in the backend. + ### Users & Permissions | Metric | Type | Labels | From 0fb53976a9b386e5a303104ca315be0197572698 Mon Sep 17 00:00:00 2001 From: HatemMn <19950216+HatemMn@users.noreply.github.com> Date: Fri, 12 Jun 2026 16:52:59 +0200 Subject: [PATCH 18/28] fix: hash --- nix/expected-hashes/server.vendor.static.sha256 | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/nix/expected-hashes/server.vendor.static.sha256 b/nix/expected-hashes/server.vendor.static.sha256 index 660bb8b718..3e2d40969c 100644 --- a/nix/expected-hashes/server.vendor.static.sha256 +++ b/nix/expected-hashes/server.vendor.static.sha256 @@ -1,5 +1,6 @@ <<<<<<< HEAD <<<<<<< HEAD +<<<<<<< HEAD sha256-ZzuWLgecB3Hpz1TyDw0/ew6dihrrkdNP3iq+Ts8WEHw= ======= sha256-npzFql9tG0Lx7Ij7c/hzKtIPIYvoWFpAE2/pl6qlIRQ= @@ -7,3 +8,6 @@ sha256-npzFql9tG0Lx7Ij7c/hzKtIPIYvoWFpAE2/pl6qlIRQ= ======= sha256-0UbqfLFumMDHXHDk0y7gZt62wJqxbHiJBFUhfZuX7Ro= >>>>>>> e2d57f30 (fix: fixes) +======= +sha256-o1iIUkI+0xV8aPLmqUilZJwbNk8IvIqsWwW97pOCe+E= +>>>>>>> c4db7311 (fix: hash) From 62cb7d1dc7f476ab3d365268be59d78172e144f5 Mon Sep 17 00:00:00 2001 From: HatemMn <19950216+HatemMn@users.noreply.github.com> Date: Fri, 12 Jun 2026 16:54:37 +0200 Subject: [PATCH 19/28] fix: update nix vendor hashes --- nix/expected-hashes/cli.vendor.linux.sha256 | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/nix/expected-hashes/cli.vendor.linux.sha256 b/nix/expected-hashes/cli.vendor.linux.sha256 index eb1b6c7b42..41c073d5f5 100644 --- a/nix/expected-hashes/cli.vendor.linux.sha256 +++ b/nix/expected-hashes/cli.vendor.linux.sha256 @@ -1,5 +1,9 @@ <<<<<<< HEAD +<<<<<<< HEAD sha256-BLYlBznRLu5RBetai9ZGsdb8C7cpm5PLCIf3/MJMOnM= ======= sha256-lnIKTuxfQ10VVsBjSSU7gD7zkJA1XvdGCvxJzYVhMAc= >>>>>>> 801f9218 (test: multiple updates) +======= +sha256-rHfD9R90SalVpvoaBuxSXkpEIBlnHTPVYZTvkaz6ZCE= +>>>>>>> c16b82b2 (fix: update nix vendor hashes) From a46cb3fef537c26622203f53365c1dfa21fe4fd6 Mon Sep 17 00:00:00 2001 From: HatemMn <19950216+HatemMn@users.noreply.github.com> Date: Mon, 15 Jun 2026 09:41:33 +0200 Subject: [PATCH 20/28] fix: ci --- crate/server/src/core/otel_metrics.rs | 6 ++++-- nix/expected-hashes/ui.vendor.fips.sha256 | 4 ++++ nix/expected-hashes/ui.vendor.non-fips.sha256 | 4 ++++ 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/crate/server/src/core/otel_metrics.rs b/crate/server/src/core/otel_metrics.rs index e794532ee5..5baf5d97ab 100644 --- a/crate/server/src/core/otel_metrics.rs +++ b/crate/server/src/core/otel_metrics.rs @@ -33,7 +33,7 @@ use crate::{error::KmsError, result::KResult}; /// `kms.kmip.operations.per_user.total` and /// `kms.permissions.granted.per_user.total` metrics. /// -/// Raise this constant if your deployment legitimately has more than 10_000 +/// Raise this constant if your deployment legitimately has more than `10_000` /// distinct users active within any 1-hour window. pub(crate) const MAX_TRACKED_CARDINALITY: usize = 10_000; @@ -543,7 +543,9 @@ impl DbMetricsRecorder for OtelMetrics { )] mod tests { use super::*; - use opentelemetry_sdk::metrics::{InMemoryMetricExporter, PeriodicReader, data::Gauge as GaugeData, data::Sum}; + use opentelemetry_sdk::metrics::{ + InMemoryMetricExporter, PeriodicReader, data::Gauge as GaugeData, data::Sum, + }; // ── No-op provider — cheap, used only where value assertions aren't needed ── diff --git a/nix/expected-hashes/ui.vendor.fips.sha256 b/nix/expected-hashes/ui.vendor.fips.sha256 index d1b16e798a..7fa2ade936 100644 --- a/nix/expected-hashes/ui.vendor.fips.sha256 +++ b/nix/expected-hashes/ui.vendor.fips.sha256 @@ -1 +1,5 @@ +<<<<<<< HEAD sha256-UUy49PEGv1Xe60YAxUD8rbPnIO0B08CpiHL9vClfXjY= +======= +sha256-BuvRZXYokFcMCVaVei/yn57fxxuEuymo5SypHSnlFNE= +>>>>>>> 9ff46bb8 (fix: ci) diff --git a/nix/expected-hashes/ui.vendor.non-fips.sha256 b/nix/expected-hashes/ui.vendor.non-fips.sha256 index 81b379abb6..72403f5eed 100644 --- a/nix/expected-hashes/ui.vendor.non-fips.sha256 +++ b/nix/expected-hashes/ui.vendor.non-fips.sha256 @@ -1 +1,5 @@ +<<<<<<< HEAD sha256-/C3v8txjB8P8mRbYkcT8hbBRapzB4cdbTBdmZFsphlo= +======= +sha256-NlAidjG7Z1x6aw8y7rkPA5/o14bNIIx3fmKeG0zT0Ec= +>>>>>>> 9ff46bb8 (fix: ci) From c0069d10f66fd6fa2d94c80c16283d9bcddae3c1 Mon Sep 17 00:00:00 2001 From: HatemMn <19950216+HatemMn@users.noreply.github.com> Date: Mon, 15 Jun 2026 10:22:05 +0200 Subject: [PATCH 21/28] fix: ci2 --- Cargo.lock | 855 +++++++++--------- nix/expected-hashes/ui.vendor.fips.sha256 | 4 - nix/expected-hashes/ui.vendor.non-fips.sha256 | 4 - 3 files changed, 419 insertions(+), 444 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9a3993df4e..25cdfc48f5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -46,7 +46,7 @@ dependencies = [ "actix-web", "bitflags", "bytes", - "derive_more 2.1.0", + "derive_more 2.1.1", "futures-core", "http-range", "log", @@ -71,7 +71,7 @@ dependencies = [ "bitflags", "bytes", "bytestring", - "derive_more 2.1.0", + "derive_more 2.1.1", "encoding_rs", "foldhash 0.1.5", "futures-core", @@ -234,7 +234,7 @@ dependencies = [ "bytestring", "cfg-if", "cookie", - "derive_more 2.1.0", + "derive_more 2.1.1", "encoding_rs", "foldhash 0.1.5", "futures-core", @@ -251,7 +251,7 @@ dependencies = [ "serde_json", "serde_urlencoded", "smallvec", - "socket2 0.6.1", + "socket2 0.6.4", "time", "tracing", "url", @@ -344,7 +344,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41ac571010bd60765c56085a4f1d412012a9be2663b1a2f2b19b49318653fd0d" dependencies = [ "aes 0.9.1", - "const-oid 0.10.1", + "const-oid 0.10.2", ] [[package]] @@ -392,21 +392,24 @@ checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] name = "anstyle" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anyhow" -version = "1.0.100" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "arc-swap" -version = "1.7.1" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" +checksum = "6a3a1fd6f75306b68087b831f025c712524bcb19aad54e557b1129cfa0a2b207" +dependencies = [ + "rustversion", +] [[package]] name = "argon2" @@ -422,9 +425,9 @@ dependencies = [ [[package]] name = "asn1-rs" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56624a96882bb8c26d61312ae18cb45868e5a9992ea73c58e45c3101e56a1e60" +checksum = "b7f43a50ac4fdca5df8e885c21b835997f0a1cdee65494a6847694a98652d9d8" dependencies = [ "asn1-rs-derive", "asn1-rs-impl", @@ -432,7 +435,7 @@ dependencies = [ "nom", "num-traits", "rusticata-macros", - "thiserror 2.0.17", + "thiserror 2.0.18", "time", ] @@ -461,9 +464,9 @@ dependencies = [ [[package]] name = "assert_cmd" -version = "2.1.1" +version = "2.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcbb6924530aa9e0432442af08bbcafdad182db80d2e560da42a6d442535bf85" +checksum = "2aa3a22042e45de04255c7bf3626e239f450200fd0493c1e382263544b20aea6" dependencies = [ "anstyle", "bstr", @@ -504,9 +507,9 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "backon" @@ -543,9 +546,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" -version = "1.8.0" +version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" [[package]] name = "bit-set" @@ -564,9 +567,9 @@ checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" [[package]] name = "bitflags" -version = "2.11.0" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" [[package]] name = "blake2" @@ -587,31 +590,15 @@ dependencies = [ ] [[package]] -<<<<<<< HEAD -<<<<<<< HEAD name = "block-buffer" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" dependencies = [ "hybrid-array 0.4.12", -======= -name = "blocking" -version = "1.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" -dependencies = [ - "async-channel 2.5.0", - "async-task", - "futures-io", - "futures-lite", - "piper", ->>>>>>> a3b57845 (test: increase coverage) ] [[package]] -======= ->>>>>>> 801f9218 (test: multiple updates) name = "bstr" version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -633,9 +620,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.19.0" +version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "byteorder" @@ -651,9 +638,9 @@ checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "bytestring" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "113b4343b5f6617e7ad401ced8de3cc8b012e73a594347c307b90db3e9271289" +checksum = "86566c496f2f47d9b8147a4c8b02ffdb69c919fe0c2b2e7195d22cbba0e635c9" dependencies = [ "bytes", ] @@ -666,9 +653,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.48" +version = "1.2.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c481bdbf0ed3b892f6f806287d72acd515b352a4ec27a208489b8c1bc839633a" +checksum = "dad887fd958be91b5098c0248def011f4523ab786cd411be668777e55063501f" dependencies = [ "find-msvc-tools", "shlex", @@ -723,9 +710,9 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.42" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" dependencies = [ "iana-time-zone", "js-sys", @@ -806,7 +793,7 @@ dependencies = [ "strum", "tempfile", "test_kms_server", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "url", "uuid", @@ -815,9 +802,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.53" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" dependencies = [ "clap_builder", "clap_derive", @@ -825,9 +812,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.53" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstyle", "clap_lex", @@ -835,9 +822,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.49" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" dependencies = [ "heck", "proc-macro2", @@ -847,9 +834,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.6" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "cmov" @@ -902,9 +889,9 @@ checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" [[package]] name = "const-oid" -version = "0.10.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dabb6555f92fb9ee4140454eb5dcd14c7960e1225c6d1a6cc361f032947713e" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" [[package]] name = "const-random" @@ -921,7 +908,7 @@ version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", "once_cell", "tiny-keccak", ] @@ -961,9 +948,9 @@ dependencies = [ [[package]] name = "core-foundation" -version = "0.9.4" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" dependencies = [ "core-foundation-sys", "libc", @@ -985,7 +972,7 @@ dependencies = [ "cosmian_logger", "serial_test", "test_kms_server", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "uuid", "windows-sys 0.59.0", @@ -1001,7 +988,7 @@ dependencies = [ "base64 0.21.7", "serde", "serde_json", - "thiserror 2.0.17", + "thiserror 2.0.18", "toml 0.8.23", "tracing", "url", @@ -1037,7 +1024,7 @@ dependencies = [ "curve25519-dalek", "ed25519-dalek", "gensym", - "getrandom 0.2.16", + "getrandom 0.2.17", "leb128", "rand_chacha 0.3.1", "rand_core 0.6.4", @@ -1079,7 +1066,7 @@ dependencies = [ "serde_json", "strum", "strum_macros", - "thiserror 2.0.17", + "thiserror 2.0.18", "time", "tracing", "uuid", @@ -1103,10 +1090,10 @@ dependencies = [ "cosmian_logger", "futures", "libloading", - "lru 0.16.3", + "lru 0.16.4", "pkcs11-sys", "rand 0.10.1", - "thiserror 2.0.17", + "thiserror 2.0.18", "uuid", "zeroize", ] @@ -1142,7 +1129,7 @@ dependencies = [ "strum", "tempfile", "test_kms_server", - "thiserror 2.0.17", + "thiserror 2.0.18", "time", "tokio", "url", @@ -1166,9 +1153,9 @@ dependencies = [ "reqwest", "serde", "serde_json", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", - "toml 0.9.8", + "toml 0.9.12+spec-1.1.0", "tracing", "url", ] @@ -1187,7 +1174,7 @@ dependencies = [ "serde", "serde_json", "strum", - "thiserror 2.0.17", + "thiserror 2.0.18", "time", "zeroize", ] @@ -1199,7 +1186,7 @@ dependencies = [ "base64 0.22.1", "console_error_panic_hook", "cosmian_kms_client_utils", - "getrandom 0.2.16", + "getrandom 0.2.17", "js-sys", "pem", "serde", @@ -1243,7 +1230,7 @@ dependencies = [ "serde_json", "sha2 0.10.9", "tempfile", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "uuid", "x509-parser", @@ -1260,7 +1247,7 @@ dependencies = [ "mockall", "num-bigint-dig", "serde_json", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "zeroize", ] @@ -1293,7 +1280,7 @@ dependencies = [ "futures", "governor", "hex", - "http 1.4.0", + "http 1.4.2", "jsonwebtoken", "native-tls", "num-bigint-dig", @@ -1314,10 +1301,10 @@ dependencies = [ "strum", "subtle", "tempfile", - "thiserror 2.0.17", + "thiserror 2.0.18", "time", "tokio", - "toml 0.9.8", + "toml 0.9.12+spec-1.1.0", "tracing", "url", "utimaco_pkcs11_loader", @@ -1339,7 +1326,7 @@ dependencies = [ "cosmian_logger", "cosmian_sse_memories", "deadpool-postgres", - "lru 0.16.3", + "lru 0.16.4", "mysql_async", "num_cpus", "openssl", @@ -1351,7 +1338,7 @@ dependencies = [ "serde_json", "strum", "tempfile", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tokio-postgres", "tokio-rusqlite", @@ -1371,7 +1358,7 @@ dependencies = [ "opentelemetry-stdout", "opentelemetry_sdk", "syslog-tracing", - "thiserror 2.0.17", + "thiserror 2.0.18", "tracing", "tracing-appender", "tracing-opentelemetry", @@ -1406,7 +1393,7 @@ dependencies = [ "serial_test", "sha3", "test_kms_server", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tracing-error", "tracing-subscriber", @@ -1429,7 +1416,7 @@ dependencies = [ "rand 0.10.1", "serial_test", "strum_macros", - "thiserror 2.0.17", + "thiserror 2.0.18", "tracing", "zeroize", ] @@ -1698,9 +1685,9 @@ dependencies = [ [[package]] name = "dashmap" -version = "6.1.0" +version = "6.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +checksum = "e6361d5c062261c78a176addb82d4c821ae42bed6089de0e12603cd25de2059c" dependencies = [ "cfg-if", "crossbeam-utils", @@ -1712,9 +1699,9 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.9.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" +checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" [[package]] name = "deadpool" @@ -1736,7 +1723,7 @@ checksum = "3d697d376cbfa018c23eb4caab1fd1883dd9c906a8c034e8d9a3cb06a7e0bef9" dependencies = [ "async-trait", "deadpool", - "getrandom 0.2.16", + "getrandom 0.2.17", "tokio", "tokio-postgres", "tracing", @@ -1791,11 +1778,10 @@ dependencies = [ [[package]] name = "deranged" -version = "0.5.5" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ - "powerfmt", "serde_core", ] @@ -1814,18 +1800,18 @@ dependencies = [ [[package]] name = "derive_more" -version = "2.1.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10b768e943bed7bf2cab53df09f4bc34bfd217cdb57d971e769874c9a6710618" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" dependencies = [ "derive_more-impl", ] [[package]] name = "derive_more-impl" -version = "2.1.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d286bfdaf75e988b4a78e013ecd79c581e06399ab53fbacd2d916c2f904f30b" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" dependencies = [ "convert_case 0.10.0", "proc-macro2", @@ -1874,16 +1860,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" dependencies = [ "block-buffer 0.12.1", - "const-oid 0.10.1", + "const-oid 0.10.2", "crypto-common 0.2.2", "ctutils", ] [[package]] name = "displaydoc" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" dependencies = [ "proc-macro2", "quote", @@ -1951,9 +1937,9 @@ dependencies = [ [[package]] name = "either" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" [[package]] name = "elliptic-curve" @@ -2038,9 +2024,9 @@ checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" [[package]] name = "fastrand" -version = "2.3.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" [[package]] name = "ff" @@ -2060,9 +2046,9 @@ checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] name = "find-msvc-tools" -version = "0.1.5" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "flagset" @@ -2072,9 +2058,9 @@ checksum = "b7ac824320a75a52197e8f2d787f6a38b6718bb6897a35142d749af3c0e8f4fe" [[package]] name = "flate2" -version = "1.1.5" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ "crc32fast", "libz-sys", @@ -2214,9 +2200,9 @@ checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-timer" -version = "3.0.3" +version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" +checksum = "af43fadb8a98512d547e37b4e92e0ced13e205c061b87b4623eff01d918d6968" [[package]] name = "futures-util" @@ -2269,9 +2255,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", "js-sys", @@ -2370,7 +2356,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.12", - "indexmap 2.12.1", + "indexmap 2.14.0", "slab", "tokio", "tokio-util", @@ -2379,17 +2365,17 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.12" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733" dependencies = [ "atomic-waker", "bytes", "fnv", "futures-core", "futures-sink", - "http 1.4.0", - "indexmap 2.12.1", + "http 1.4.2", + "indexmap 2.14.0", "slab", "tokio", "tokio-util", @@ -2529,9 +2515,9 @@ dependencies = [ [[package]] name = "http" -version = "1.4.0" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" dependencies = [ "bytes", "itoa", @@ -2544,7 +2530,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http 1.4.0", + "http 1.4.2", ] [[package]] @@ -2555,7 +2541,7 @@ checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", "futures-core", - "http 1.4.0", + "http 1.4.2", "http-body", "pin-project-lite", ] @@ -2598,21 +2584,20 @@ dependencies = [ [[package]] name = "hyper" -version = "1.8.1" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" dependencies = [ "atomic-waker", "bytes", "futures-channel", "futures-core", - "h2 0.4.12", - "http 1.4.0", + "h2 0.4.14", + "http 1.4.2", "http-body", "httparse", "itoa", "pin-project-lite", - "pin-utils", "smallvec", "tokio", "want", @@ -2620,15 +2605,14 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.7" +version = "0.27.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" dependencies = [ - "http 1.4.0", + "http 1.4.2", "hyper", "hyper-util", "rustls", - "rustls-pki-types", "tokio", "tokio-rustls", "tower-service", @@ -2666,23 +2650,22 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.19" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ "base64 0.22.1", "bytes", "futures-channel", - "futures-core", "futures-util", - "http 1.4.0", + "http 1.4.2", "http-body", "hyper", "ipnet", "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.1", + "socket2 0.6.4", "tokio", "tower-service", "tracing", @@ -2690,9 +2673,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.64" +version = "0.1.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -2714,12 +2697,13 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" dependencies = [ "displaydoc", "potential_utf", + "utf8_iter", "yoke", "zerofrom", "zerovec", @@ -2727,9 +2711,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" dependencies = [ "displaydoc", "litemap", @@ -2740,9 +2724,9 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" dependencies = [ "icu_collections", "icu_normalizer_data", @@ -2754,15 +2738,15 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" [[package]] name = "icu_properties" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" dependencies = [ "icu_collections", "icu_locale_core", @@ -2774,15 +2758,15 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" [[package]] name = "icu_provider" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" dependencies = [ "displaydoc", "icu_locale_core", @@ -2812,9 +2796,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" dependencies = [ "icu_normalizer", "icu_properties", @@ -2838,12 +2822,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.12.1" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.16.1", + "hashbrown 0.17.1", "serde", "serde_core", ] @@ -2868,43 +2852,42 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.11.0" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] -name = "iri-string" -version = "0.7.9" +name = "itertools" +version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" dependencies = [ - "memchr", - "serde", + "either", ] [[package]] name = "itertools" -version = "0.10.5" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" dependencies = [ "either", ] [[package]] name = "itertools" -version = "0.13.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" dependencies = [ "either", ] [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "js-sys" @@ -2918,13 +2901,13 @@ dependencies = [ [[package]] name = "jsonwebtoken" -version = "10.3.0" +version = "10.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0529410abe238729a60b108898784df8984c87f6054c9c4fcacc47e4803c1ce1" +checksum = "eba32bfb4ffdeaca3e34431072faf01745c9b26d25504aa7a6cf5684334fc4fc" dependencies = [ "base64 0.22.1", "ed25519-dalek", - "getrandom 0.2.16", + "getrandom 0.2.17", "hmac 0.12.1", "js-sys", "p256", @@ -2937,6 +2920,7 @@ dependencies = [ "sha2 0.10.9", "signature", "simple_asn1", + "zeroize", ] [[package]] @@ -2978,7 +2962,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ee7893dab2e44ae5f9d0173f26ff4aa327c10b01b06a72b52dd9405b628640d" dependencies = [ - "indexmap 2.12.1", + "indexmap 2.14.0", ] [[package]] @@ -3006,9 +2990,9 @@ dependencies = [ [[package]] name = "leb128" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" +checksum = "6cc46bac87ef8093eed6f272babb833b6443374399985ac8ed28471ee0918545" [[package]] name = "leb128fmt" @@ -3034,9 +3018,9 @@ dependencies = [ [[package]] name = "libm" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libredox" @@ -3060,9 +3044,9 @@ dependencies = [ [[package]] name = "libz-sys" -version = "1.1.23" +version = "1.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15d118bbf3771060e7311cc7bb0545b01d08a8b4a7de949198dec1fa0ca1c0f7" +checksum = "85bc9657773828b90eeb625adff10eeac83cc21bbfd8e23a03eaa8a33c9e28d9" dependencies = [ "cc", "pkg-config", @@ -3071,15 +3055,15 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" [[package]] name = "local-waker" @@ -3098,15 +3082,15 @@ dependencies = [ [[package]] name = "log" -version = "0.4.29" +version = "0.4.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" [[package]] name = "lru" -version = "0.16.3" +version = "0.16.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593" +checksum = "7f66e8d5d03f609abc3a39e6f08e4164ebf1447a732906d39eb9b99b7919ef39" dependencies = [ "hashbrown 0.16.1", ] @@ -3147,9 +3131,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.6" +version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" [[package]] name = "mime" @@ -3195,9 +3179,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.1.1" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" dependencies = [ "libc", "log", @@ -3207,9 +3191,9 @@ dependencies = [ [[package]] name = "ml-kem" -version = "0.2.1" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97befee0c869cb56f3118f49d0f9bb68c9e3f380dec23c1100aedc4ec3ba239a" +checksum = "8de49b3df74c35498c0232031bb7e85f9389f913e2796169c8ab47a53993a18f" dependencies = [ "hybrid-array 0.2.3", "kem", @@ -3265,8 +3249,8 @@ dependencies = [ "percent-encoding", "rand 0.10.1", "serde", - "socket2 0.6.1", - "thiserror 2.0.17", + "socket2 0.6.4", + "thiserror 2.0.18", "tokio", "tokio-native-tls", "tokio-util", @@ -3296,15 +3280,15 @@ dependencies = [ "serde_json", "sha1", "sha2 0.10.9", - "thiserror 2.0.17", + "thiserror 2.0.18", "uuid", ] [[package]] name = "native-tls" -version = "0.2.14" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" dependencies = [ "libc", "log", @@ -3377,9 +3361,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.2.0" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" [[package]] name = "num-integer" @@ -3438,8 +3422,8 @@ checksum = "51e219e79014df21a225b1860a479e2dcd7cbd9130f4defd4bd0e191ea31d67d" dependencies = [ "base64 0.22.1", "chrono", - "getrandom 0.2.16", - "http 1.4.0", + "getrandom 0.2.17", + "http 1.4.2", "rand 0.8.6", "reqwest", "serde", @@ -3479,9 +3463,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "oorandom" @@ -3522,9 +3506,9 @@ dependencies = [ [[package]] name = "openssl-probe" -version = "0.1.6" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] name = "openssl-sys" @@ -3548,7 +3532,7 @@ dependencies = [ "futures-sink", "js-sys", "pin-project-lite", - "thiserror 2.0.17", + "thiserror 2.0.18", "tracing", ] @@ -3560,7 +3544,7 @@ checksum = "46d7ab32b827b5b495bd90fa95a6cb65ccc293555dcc3199ae2937d2d237c8ed" dependencies = [ "async-trait", "bytes", - "http 1.4.0", + "http 1.4.2", "opentelemetry", "reqwest", "tracing", @@ -3573,14 +3557,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d899720fe06916ccba71c01d04ecd77312734e2de3467fd30d9d580c8ce85656" dependencies = [ "futures-core", - "http 1.4.0", + "http 1.4.2", "opentelemetry", "opentelemetry-http", "opentelemetry-proto", "opentelemetry_sdk", "prost", "reqwest", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tonic", "tracing", @@ -3630,7 +3614,7 @@ dependencies = [ "percent-encoding", "rand 0.9.4", "serde_json", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tokio-stream", "tracing", @@ -3750,18 +3734,18 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.1.10" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.10" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" dependencies = [ "proc-macro2", "quote", @@ -3770,15 +3754,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" - -[[package]] -name = "pin-utils" -version = "0.1.0" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pkcs1" @@ -3809,9 +3787,9 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] name = "poly1305" @@ -3844,9 +3822,9 @@ checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "postgres-openssl" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f86f073ad570f76e9e278ce6f05775fc723eed7daa6b4f9c2aa078080a564a0" +checksum = "06743eefaa1a5c0ef2ccb6d9abf6528790a229eabd62ddcabf9b2a3aeff09fa4" dependencies = [ "openssl", "tokio", @@ -3888,9 +3866,9 @@ dependencies = [ [[package]] name = "potential_utf" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" dependencies = [ "zerovec", ] @@ -3912,9 +3890,9 @@ dependencies = [ [[package]] name = "predicates" -version = "3.1.3" +version = "3.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573" +checksum = "ada8f2932f28a27ee7b70dd6c1c39ea0675c55a36879ab92f3a715eaa1e63cfe" dependencies = [ "anstyle", "difflib", @@ -3926,15 +3904,15 @@ dependencies = [ [[package]] name = "predicates-core" -version = "1.0.9" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa" +checksum = "cad38746f3166b4031b1a0d39ad9f954dd291e7854fcc0eed52ee41a0b50d144" [[package]] name = "predicates-tree" -version = "1.0.12" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c" +checksum = "d0de1b847b39c8131db0467e9df1ff60e6d0562ab8e9a16e568ad0fdb372e2f2" dependencies = [ "predicates-core", "termtree", @@ -3961,9 +3939,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.103" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] @@ -4004,7 +3982,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" dependencies = [ "anyhow", - "itertools 0.13.0", + "itertools 0.14.0", "proc-macro2", "quote", "syn", @@ -4061,8 +4039,8 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls", - "socket2 0.6.1", - "thiserror 2.0.17", + "socket2 0.6.4", + "thiserror 2.0.18", "tokio", "tracing", "web-time", @@ -4083,7 +4061,7 @@ dependencies = [ "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.17", + "thiserror 2.0.18", "tinyvec", "tracing", "web-time", @@ -4098,16 +4076,16 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.1", + "socket2 0.6.4", "tracing", "windows-sys 0.60.2", ] [[package]] name = "quote" -version = "1.0.42" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -4142,7 +4120,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" dependencies = [ "rand_chacha 0.9.0", - "rand_core 0.9.3", + "rand_core 0.9.5", ] [[package]] @@ -4173,7 +4151,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core 0.9.3", + "rand_core 0.9.5", ] [[package]] @@ -4192,14 +4170,14 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", ] [[package]] name = "rand_core" -version = "0.9.3" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" dependencies = [ "getrandom 0.3.4", ] @@ -4226,7 +4204,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" dependencies = [ - "rand_core 0.9.3", + "rand_core 0.9.5", ] [[package]] @@ -4264,7 +4242,7 @@ dependencies = [ "pin-project-lite", "ryu", "sha1_smol", - "socket2 0.6.1", + "socket2 0.6.4", "tokio", "tokio-util", "url", @@ -4281,9 +4259,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.12.2" +version = "1.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" dependencies = [ "aho-corasick", "memchr", @@ -4293,9 +4271,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -4304,29 +4282,29 @@ dependencies = [ [[package]] name = "regex-lite" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d942b98df5e658f56f20d592c7f868833fe38115e65c33003d8cd224b0155da" +checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" [[package]] name = "regex-syntax" -version = "0.8.8" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "reqwest" -version = "0.12.24" +version = "0.12.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ "base64 0.22.1", "bytes", "futures-channel", "futures-core", "futures-util", - "h2 0.4.12", - "http 1.4.0", + "h2 0.4.14", + "http 1.4.2", "http-body", "http-body-util", "hyper", @@ -4348,7 +4326,7 @@ dependencies = [ "tokio", "tokio-native-tls", "tokio-rustls", - "tower 0.5.2", + "tower 0.5.3", "tower-http", "tower-service", "url", @@ -4391,7 +4369,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.16", + "getrandom 0.2.17", "libc", "untrusted 0.9.0", "windows-sys 0.52.0", @@ -4468,9 +4446,9 @@ dependencies = [ [[package]] name = "rustix" -version = "1.1.2" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ "bitflags", "errno", @@ -4481,9 +4459,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.35" +version = "0.23.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" dependencies = [ "once_cell", "ring 0.17.14", @@ -4495,9 +4473,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.13.1" +version = "1.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "708c0f9d5f54ba0272468c1d306a52c495b31fa155e91bc25371e6df7996908c" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" dependencies = [ "web-time", "zeroize", @@ -4534,9 +4512,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.20" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "salsa20" @@ -4562,20 +4540,11 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ece8e78b2f38ec51c51f5d475df0a7187ba5111b2a28bdc761ee05b075d40a71" -[[package]] -name = "scc" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46e6f046b7fef48e2660c57ed794263155d713de679057f2d0c169bfc6e756cc" -dependencies = [ - "sdd", -] - [[package]] name = "schannel" -version = "0.1.28" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" dependencies = [ "windows-sys 0.61.2", ] @@ -4604,7 +4573,7 @@ dependencies = [ "async-trait", "chrono", "hex", - "http 1.4.0", + "http 1.4.2", "lazy_static", "log", "regex", @@ -4614,12 +4583,6 @@ dependencies = [ "tower 0.4.13", ] -[[package]] -name = "sdd" -version = "3.0.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "490dcfcbfef26be6800d11870ff2df8774fa6e86d047e3e8c8a76b25655e41ca" - [[package]] name = "sec1" version = "0.7.3" @@ -4636,9 +4599,9 @@ dependencies = [ [[package]] name = "security-framework" -version = "2.11.1" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ "bitflags", "core-foundation", @@ -4649,9 +4612,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.16.0" +version = "2.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "321c8673b092a9a42605034a9879d73cb79101ed5fd117bc9a597b89b4e9e61a" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" dependencies = [ "core-foundation-sys", "libc", @@ -4659,9 +4622,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.27" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "serde" @@ -4706,16 +4669,16 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.145" +version = "1.0.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" dependencies = [ - "indexmap 2.12.1", + "indexmap 2.14.0", "itoa", "memchr", - "ryu", "serde", "serde_core", + "zmij", ] [[package]] @@ -4740,9 +4703,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "1.0.4" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" dependencies = [ "serde_core", ] @@ -4761,23 +4724,23 @@ dependencies = [ [[package]] name = "serial_test" -version = "3.2.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b258109f244e1d6891bf1053a55d63a5cd4f8f4c30cf9a1280989f80e7a1fa9" +checksum = "699f4197115b8a7e7ff19c9a315a4bd6fffec26cc4626ef45ecaea389e081c6d" dependencies = [ - "futures", + "futures-executor", + "futures-util", "log", "once_cell", "parking_lot", - "scc", "serial_test_derive", ] [[package]] name = "serial_test_derive" -version = "3.2.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d69265a08751de7844521fd15003ae0a888e035773ba05695c5c759a6f89eef" +checksum = "94e153fc76e1c6a068703d6d29c508a0b15c061c4b7e43da59cc097bc342673c" dependencies = [ "proc-macro2", "quote", @@ -4825,9 +4788,9 @@ dependencies = [ [[package]] name = "sha3" -version = "0.10.8" +version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" +checksum = "77fd7028345d415a4034cf8777cd4f8ab1851274233b45f84e3d955502d93874" dependencies = [ "digest 0.10.7", "keccak", @@ -4850,16 +4813,17 @@ checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77" [[package]] name = "shlex" -version = "1.3.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" [[package]] name = "signal-hook-registry" -version = "1.4.7" +version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ + "errno", "libc", ] @@ -4875,39 +4839,39 @@ dependencies = [ [[package]] name = "simd-adler32" -version = "0.3.7" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" [[package]] name = "simple_asn1" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" +checksum = "0d585997b0ac10be3c5ee635f1bab02d512760d14b7c468801ac8a01d9ae5f1d" dependencies = [ "num-bigint", "num-traits", - "thiserror 2.0.17", + "thiserror 2.0.18", "time", ] [[package]] name = "siphasher" -version = "1.0.1" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" [[package]] name = "slab" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.15.1" +version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" [[package]] name = "smartcardhsm_pkcs11_loader" @@ -4930,12 +4894,12 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.1" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -5022,11 +4986,17 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "symlink" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7973cce6668464ea31f176d85b13c7ab3bba2cb3b77a2ed26abd7801688010a" + [[package]] name = "syn" -version = "2.0.111" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -5066,12 +5036,12 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.23.0" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.2", "once_cell", "rustix", "windows-sys 0.61.2", @@ -5098,7 +5068,7 @@ dependencies = [ "serde_json", "time", "tokio", - "toml 0.9.8", + "toml 0.9.12+spec-1.1.0", "zeroize", ] @@ -5113,11 +5083,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 2.0.17", + "thiserror-impl 2.0.18", ] [[package]] @@ -5133,9 +5103,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", @@ -5153,12 +5123,11 @@ dependencies = [ [[package]] name = "time" -version = "0.3.47" +version = "0.3.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +checksum = "711a53c2d47bbd818258c498c8dbfe186a2526c631495cfe7e078567f86b8469" dependencies = [ "deranged", - "itoa", "libc", "num-conv", "num_threads", @@ -5170,15 +5139,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" [[package]] name = "time-macros" -version = "0.2.27" +version = "0.2.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +checksum = "71c652a3727a9cbb9a02f707f530b618ce00d0ccd762009c8c23bd191df3c17d" dependencies = [ "num-conv", "time-core", @@ -5195,9 +5164,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" dependencies = [ "displaydoc", "zerovec", @@ -5215,9 +5184,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" dependencies = [ "tinyvec_macros", ] @@ -5230,9 +5199,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.48.0" +version = "1.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" dependencies = [ "bytes", "libc", @@ -5240,16 +5209,16 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.6.1", + "socket2 0.6.4", "tokio-macros", "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", @@ -5297,7 +5266,7 @@ dependencies = [ "postgres-protocol", "postgres-types", "rand 0.10.1", - "socket2 0.6.1", + "socket2 0.6.4", "tokio", "tokio-util", "whoami", @@ -5326,9 +5295,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.17" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" dependencies = [ "futures-core", "pin-project-lite", @@ -5337,9 +5306,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.17" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" dependencies = [ "bytes", "futures-core", @@ -5362,17 +5331,17 @@ dependencies = [ [[package]] name = "toml" -version = "0.9.8" +version = "0.9.12+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" dependencies = [ - "indexmap 2.12.1", + "indexmap 2.14.0", "serde_core", - "serde_spanned 1.0.4", - "toml_datetime 0.7.3", + "serde_spanned 1.1.1", + "toml_datetime 0.7.5+spec-1.1.0", "toml_parser", "toml_writer", - "winnow", + "winnow 0.7.15", ] [[package]] @@ -5386,9 +5355,9 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.7.3" +version = "0.7.5+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" dependencies = [ "serde_core", ] @@ -5399,21 +5368,21 @@ version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ - "indexmap 2.12.1", + "indexmap 2.14.0", "serde", "serde_spanned 0.6.9", "toml_datetime 0.6.11", "toml_write", - "winnow", + "winnow 0.7.15", ] [[package]] name = "toml_parser" -version = "1.0.4" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ - "winnow", + "winnow 1.0.3", ] [[package]] @@ -5424,9 +5393,9 @@ checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" [[package]] name = "toml_writer" -version = "1.0.6+spec-1.1.0" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" [[package]] name = "tonic" @@ -5437,7 +5406,7 @@ dependencies = [ "async-trait", "base64 0.22.1", "bytes", - "http 1.4.0", + "http 1.4.2", "http-body", "http-body-util", "hyper", @@ -5476,9 +5445,9 @@ dependencies = [ [[package]] name = "tower" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ "futures-core", "futures-util", @@ -5491,20 +5460,20 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.7" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf146f99d442e8e68e585f5d798ccd3cad9a7835b917e09728880a862706456" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ "bitflags", "bytes", "futures-util", - "http 1.4.0", + "http 1.4.2", "http-body", - "iri-string", "pin-project-lite", - "tower 0.5.2", + "tower 0.5.3", "tower-layer", "tower-service", + "url", ] [[package]] @@ -5521,9 +5490,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.43" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "log", "pin-project-lite", @@ -5533,12 +5502,13 @@ dependencies = [ [[package]] name = "tracing-appender" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "786d480bce6247ab75f005b14ae1624ad978d3029d9113f0a22fa1ac773faeaf" +checksum = "050686193eb999b4bb3bc2acfa891a13da00f79734704c4b8b4ef1a10b368a3c" dependencies = [ "crossbeam-channel", - "thiserror 2.0.17", + "symlink", + "thiserror 2.0.18", "time", "tracing-subscriber", ] @@ -5556,9 +5526,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.35" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", "valuable", @@ -5605,9 +5575,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.22" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" dependencies = [ "matchers", "nu-ansi-term", @@ -5645,9 +5615,9 @@ checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" [[package]] name = "unicase" -version = "2.8.1" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" [[package]] name = "unicode-bidi" @@ -5657,9 +5627,9 @@ checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" [[package]] name = "unicode-ident" -version = "1.0.22" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-normalization" @@ -5678,9 +5648,9 @@ checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" [[package]] name = "unicode-segmentation" -version = "1.12.0" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" [[package]] name = "unicode-width" @@ -5718,14 +5688,15 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.7" +version = "2.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" dependencies = [ "form_urlencoded", "idna", "percent-encoding", "serde", + "serde_derive", ] [[package]] @@ -5747,7 +5718,7 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b913a3b5fe84142e269d63cc62b64319ccaf89b748fc31fe025177f767a756c4" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", ] [[package]] @@ -5819,11 +5790,11 @@ dependencies = [ [[package]] name = "wasip2" -version = "1.0.1+wasi-0.2.4" +version = "1.0.4+wasi-0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" dependencies = [ - "wit-bindgen 0.46.0", + "wit-bindgen 0.57.1", ] [[package]] @@ -5959,7 +5930,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" dependencies = [ "anyhow", - "indexmap 2.12.1", + "indexmap 2.14.0", "wasm-encoder", "wasmparser", ] @@ -5972,7 +5943,7 @@ checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ "bitflags", "hashbrown 0.15.5", - "indexmap 2.12.1", + "indexmap 2.14.0", "semver", ] @@ -6358,18 +6329,18 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" -version = "0.7.14" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" dependencies = [ "memchr", ] [[package]] -name = "wit-bindgen" -version = "0.46.0" +name = "winnow" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" [[package]] name = "wit-bindgen" @@ -6380,6 +6351,12 @@ dependencies = [ "wit-bindgen-rust-macro", ] +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + [[package]] name = "wit-bindgen-core" version = "0.51.0" @@ -6399,7 +6376,7 @@ checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" dependencies = [ "anyhow", "heck", - "indexmap 2.12.1", + "indexmap 2.14.0", "prettyplease", "syn", "wasm-metadata", @@ -6430,7 +6407,7 @@ checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", "bitflags", - "indexmap 2.12.1", + "indexmap 2.14.0", "log", "serde", "serde_derive", @@ -6449,7 +6426,7 @@ checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" dependencies = [ "anyhow", "id-arena", - "indexmap 2.12.1", + "indexmap 2.14.0", "log", "semver", "serde", @@ -6461,9 +6438,9 @@ dependencies = [ [[package]] name = "writeable" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" [[package]] name = "x509-cert" @@ -6490,7 +6467,7 @@ dependencies = [ "oid-registry", "ring 0.17.14", "rusticata-macros", - "thiserror 2.0.17", + "thiserror 2.0.18", "time", ] @@ -6506,9 +6483,9 @@ dependencies = [ [[package]] name = "yoke" -version = "0.8.1" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" dependencies = [ "stable_deref_trait", "yoke-derive", @@ -6517,9 +6494,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", @@ -6529,18 +6506,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.31" +version = "0.8.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" +checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.31" +version = "0.8.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" +checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" dependencies = [ "proc-macro2", "quote", @@ -6549,18 +6526,18 @@ dependencies = [ [[package]] name = "zerofrom" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", @@ -6570,9 +6547,9 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.8.2" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" dependencies = [ "serde", "zeroize_derive", @@ -6580,9 +6557,9 @@ dependencies = [ [[package]] name = "zeroize_derive" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" dependencies = [ "proc-macro2", "quote", @@ -6591,9 +6568,9 @@ dependencies = [ [[package]] name = "zerotrie" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" dependencies = [ "displaydoc", "yoke", @@ -6602,9 +6579,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.5" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" dependencies = [ "yoke", "zerofrom", @@ -6613,11 +6590,17 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", "syn", ] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/nix/expected-hashes/ui.vendor.fips.sha256 b/nix/expected-hashes/ui.vendor.fips.sha256 index 7fa2ade936..fc05522249 100644 --- a/nix/expected-hashes/ui.vendor.fips.sha256 +++ b/nix/expected-hashes/ui.vendor.fips.sha256 @@ -1,5 +1 @@ -<<<<<<< HEAD -sha256-UUy49PEGv1Xe60YAxUD8rbPnIO0B08CpiHL9vClfXjY= -======= sha256-BuvRZXYokFcMCVaVei/yn57fxxuEuymo5SypHSnlFNE= ->>>>>>> 9ff46bb8 (fix: ci) diff --git a/nix/expected-hashes/ui.vendor.non-fips.sha256 b/nix/expected-hashes/ui.vendor.non-fips.sha256 index 72403f5eed..d842e6a920 100644 --- a/nix/expected-hashes/ui.vendor.non-fips.sha256 +++ b/nix/expected-hashes/ui.vendor.non-fips.sha256 @@ -1,5 +1 @@ -<<<<<<< HEAD -sha256-/C3v8txjB8P8mRbYkcT8hbBRapzB4cdbTBdmZFsphlo= -======= sha256-NlAidjG7Z1x6aw8y7rkPA5/o14bNIIx3fmKeG0zT0Ec= ->>>>>>> 9ff46bb8 (fix: ci) From 86a492b9634cfa6bbccf55411cb59dd65a083756 Mon Sep 17 00:00:00 2001 From: HatemMn <19950216+HatemMn@users.noreply.github.com> Date: Mon, 15 Jun 2026 13:30:39 +0200 Subject: [PATCH 22/28] fix: ci3 --- crate/clients/client_utils/src/attributes_utils.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/crate/clients/client_utils/src/attributes_utils.rs b/crate/clients/client_utils/src/attributes_utils.rs index bacfe8ab14..c5b5a7aca9 100644 --- a/crate/clients/client_utils/src/attributes_utils.rs +++ b/crate/clients/client_utils/src/attributes_utils.rs @@ -10,7 +10,7 @@ use cosmian_kmip::kmip_2_1::{ }; use serde_json::Value; use strum::{EnumIter, EnumString, IntoEnumIterator}; -use time::{OffsetDateTime, format_description::parse}; +use time::{OffsetDateTime, format_description}; use crate::{ error::UtilsError, @@ -461,8 +461,10 @@ pub fn build_selected_attribute( ) -> Result { let attribute = match attribute_name { "activation_date" => { - let format = parse("[year]-[month]-[day]T[hour]:[minute]:[second]Z") - .map_err(|e| UtilsError::Default(e.to_string()))?; + let format = format_description::parse_borrowed::<2>( + "[year]-[month]-[day]T[hour]:[minute]:[second]Z", + ) + .map_err(|e| UtilsError::Default(e.to_string()))?; let activation_date = OffsetDateTime::parse(&attribute_value, &format) .map_err(|e| UtilsError::Default(e.to_string()))?; Attribute::ActivationDate(activation_date) From 75e07edbb68225ed1d7c3eed4e474d76828d87c8 Mon Sep 17 00:00:00 2001 From: HatemMn <19950216+HatemMn@users.noreply.github.com> Date: Mon, 15 Jun 2026 13:50:07 +0200 Subject: [PATCH 23/28] fix: ci4 --- crate/server/src/core/otel_metrics.rs | 9 +- .../src/middlewares/otel_http_middleware.rs | 2 +- .../docs/configuration/otlp-metrics.md | 6 +- monitoring/OTLP_METRICS.md | 6 +- nix/README.md | 166 ------------------ nix/expected-hashes/cli.vendor.linux.sha256 | 8 - .../server.vendor.static.sha256 | 12 -- 7 files changed, 15 insertions(+), 194 deletions(-) diff --git a/crate/server/src/core/otel_metrics.rs b/crate/server/src/core/otel_metrics.rs index 5baf5d97ab..80befec42b 100644 --- a/crate/server/src/core/otel_metrics.rs +++ b/crate/server/src/core/otel_metrics.rs @@ -442,12 +442,19 @@ impl OtelMetrics { } /// Record HTTP request duration - pub fn record_http_request_duration(&self, method: &str, path: &str, duration_seconds: f64) { + pub fn record_http_request_duration( + &self, + method: &str, + path: &str, + status: &str, + duration_seconds: f64, + ) { self.http_request_duration.record( duration_seconds, &[ KeyValue::new("method", method.to_owned()), KeyValue::new("path", path.to_owned()), + KeyValue::new("status", status.to_owned()), ], ); } diff --git a/crate/server/src/middlewares/otel_http_middleware.rs b/crate/server/src/middlewares/otel_http_middleware.rs index b38e020244..b719bf8641 100644 --- a/crate/server/src/middlewares/otel_http_middleware.rs +++ b/crate/server/src/middlewares/otel_http_middleware.rs @@ -105,7 +105,7 @@ where ); m.record_http_request(&method, &path, &status); - m.record_http_request_duration(&method, &path, duration); + m.record_http_request_duration(&method, &path, &status, duration); } result diff --git a/documentation/docs/configuration/otlp-metrics.md b/documentation/docs/configuration/otlp-metrics.md index e5c8988484..76882cb5dc 100644 --- a/documentation/docs/configuration/otlp-metrics.md +++ b/documentation/docs/configuration/otlp-metrics.md @@ -40,7 +40,7 @@ To enable the feature, see [Telemetry & Observability](./logging.md). | Metric | Type | Description | Labels | |--------|------|-------------|--------| | `kms.http.requests.total` | counter | Incoming HTTP requests | `method`, `path`, `status` | -| `kms.http.request.duration` | histogram (s) | HTTP request latency | `method`, `path` | +| `kms.http.request.duration` | histogram (s) | HTTP request latency | `method`, `path`, `status` | `path` is normalised (e.g. `/kmip/2_1`, `/google_cse/...`) to avoid high cardinality from object identifiers. @@ -58,8 +58,8 @@ object identifiers. | Metric | Type | Description | Labels | |--------|------|-------------|--------| -| `kms.objects.total` | up-down counter | Total non-destroyed objects in the KMS | — | -| `kms.keys.active.count` | up-down counter | Non-destroyed key objects (SymmetricKey, PrivateKey, PublicKey, SplitKey) across all states: PreActive, Active, Deactivated, Compromised | — | +| `kms.objects.total` | gauge | Total non-destroyed objects in the KMS | — | +| `kms.keys.active.count` | gauge | Non-destroyed key objects (SymmetricKey, PrivateKey, PublicKey, SplitKey) across all states: PreActive, Active, Deactivated, Compromised | — | Both metrics are refreshed every 30 s by the metrics cron task and seeded at server startup. diff --git a/monitoring/OTLP_METRICS.md b/monitoring/OTLP_METRICS.md index 7a753e7d85..c551132a13 100644 --- a/monitoring/OTLP_METRICS.md +++ b/monitoring/OTLP_METRICS.md @@ -114,7 +114,7 @@ The server exposes the following instruments via OTLP, as implemented in `crate/ | Metric | Type | Labels | |--------|------|--------| | `kms.http.requests.total` | counter | `method`, `path`, `status` | -| `kms.http.request.duration` | histogram (s) | `method`, `path` | +| `kms.http.request.duration` | histogram (s) | `method`, `path`, `status` | ### Server Health @@ -129,8 +129,8 @@ The server exposes the following instruments via OTLP, as implemented in `crate/ | Metric | Type | Labels | |--------|------|--------| -| `kms.objects.total` | up-down counter | — | -| `kms.keys.active.count` | up-down counter | — | +| `kms.objects.total` | gauge | — | +| `kms.keys.active.count` | gauge | — | `kms.keys.active.count` counts all **non-destroyed** key objects (SymmetricKey, PrivateKey, PublicKey, SplitKey) across all non-terminal states: PreActive, Active, Deactivated, Compromised. diff --git a/nix/README.md b/nix/README.md index 9fe85b9ae3..6093ae2bbf 100644 --- a/nix/README.md +++ b/nix/README.md @@ -442,171 +442,6 @@ bash .github/scripts/nix.sh package deb # Reuses binary; no compilation ### Offline Build Visual Flow -<<<<<<< Updated upstream - -```text -┌─────────────────────────────────────────────────────────────────────────┐ -│ Expected Hash Update Workflow (CI-driven) │ -└─────────────────────────────────────────────────────────────────────────┘ - -Trigger: CI packaging job fails with a fixed-output derivation hash mismatch - │ - ▼ - ┌──────────────────────────┐ - │ bash .github/scripts/ │ - │ nix.sh update-hashes │ - │ [RUN_ID] │ - └──────────┬───────────────┘ - │ - ▼ - ┌──────────────────────────┐ - │ update_hashes.sh │ - │ • requires `gh` │ - │ • downloads job logs │ - │ • parses specified/got │ - └──────────┬───────────────┘ - │ - ▼ - ┌──────────────────────────┐ - │ Updates files in │ - │ nix/expected-hashes/ │ - │ • ui.vendor.*.sha256 │ - │ • server.vendor.{static,dynamic}.sha256 │ - │ • cli.vendor.linux.sha256 │ - │ • cli.vendor.{static,dynamic}.darwin.sha256 │ - └──────────────────────────┘ - -Note: deterministic *binary* hash enforcement is optional in Nix derivations; -when enabled, builds emit a `cosmian-kms-server.*.sha256` file with copy instructions. - │ - ▼ - ┌──────────────────────────┐ - │ Cache Result Symlinks │ - │ │ - │ • result-server-fips │ - │ • result-server-non-fips│ - │ • result-rust-1_91 │ - │ • result-cargo-deb │ - │ • result-cargo-rpm │ - └──────────┬───────────────┘ - │ - ▼ - ┌────────────────────────────────┐ - │ Prewarm Complete ✅ │ - │ All dependencies cached │ - │ Ready for offline builds │ - └────────────────────────────────┘ - - -PHASE 2: OFFLINE BUILD (No Network - Repeatable) -═══════════════════════════════════════════════════════════════════════════ - -┌─────────────────────────────────────────────────────────────────────────┐ -│ 🚫 Network Disconnected / Air-Gapped Environment │ -└─────────────────────────────────────────────────────────────────────────┘ - │ - ▼ - ┌──────────────────────────┐ - │ export NO_PREWARM=1 │ - │ export CARGO_NET_OFFLINE│ - └──────────┬───────────────┘ - │ - ▼ - ┌──────────────────────────┐ - │ bash nix.sh package │ - │ deb/rpm/dmg │ - └──────────┬───────────────┘ - │ - ▼ - ┌────────────────────────────────┐ - │ Check for Existing Build │ - │ result-server- │ - └────────┬─────────────────┬─────┘ - │ │ - Found │ │ Not Found - │ │ - ▼ ▼ - ┌────────────────┐ ┌──────────────────┐ - │ Reuse Binary │ │ Build from Nix │ - │ (No rebuild) │ │ Store Cache │ - └────────┬───────┘ └──────┬───────────┘ - │ │ - └─────────┬───────┘ - │ - ▼ - ┌──────────────────────────┐ - │ Load Tools from Cache │ - │ │ - │ • cargo-deb (DEB) │ - │ • cargo-generate-rpm │ - │ • DMG tools (macOS) │ - │ │ - │ All from /nix/store │ - │ (no network needed) │ - └──────────┬───────────────┘ - │ - ▼ - ┌──────────────────────────┐ - │ Package Binary │ - │ (using cached tools) │ - └──────────┬───────────────┘ - │ - ▼ - ┌──────────────────────────┐ - │ Smoke Test │ - │ (extract + run --info) │ - └──────────┬───────────────┘ - │ - ▼ - ┌──────────────────────────┐ - │ Generate Checksum │ - │ (.sha256 file) │ - └──────────┬───────────────┘ - │ - ▼ - ┌────────────────────────────────┐ - │ Offline Build Complete ✅ │ - │ │ - │ Output: │ - │ • Package file │ - │ • .sha256 checksum │ - │ • .asc signature (if GPG) │ - └────────────────────────────────┘ - - -CACHE DEPENDENCY GRAPH -═══════════════════════════════════════════════════════════════════════════ - -┌─────────────────────────────────────────────────────────────────────────┐ -│ What's Stored Where (for offline use) │ -└─────────────────────────────────────────────────────────────────────────┘ - - /nix/store/ target/ resources/ - ├─ -nixpkgs ├─ cargo-offline-home/ └─ tarballs/ - │ └─ All system packages │ ├─ registry/ └─ openssl-3.1.2.tar.gz - │ │ │ ├─ index/ - ├─ -openssl-3.1.2 │ │ ├─ cache/ - │ └─ Built OpenSSL lib │ │ └─ src/ - │ │ └─ git/db/ - ├─ -rust-1.91.0 │ - │ └─ Rust toolchain ├─ release/ - │ │ └─ cosmian_kms (binary) - ├─ -cargo-deb │ - │ └─ DEB packaging tool └─ debug/ - │ └─ cosmian_kms (binary) - ├─ -cargo-generate-rpm - │ └─ RPM packaging tool - │ - └─ -cosmian-kms-server - └─ Hash-verified binary - - Symlinks in project root: - ├─ result-server-fips → /nix/store/-cosmian-kms-server - ├─ result-server-non-fips → /nix/store/-cosmian-kms-server - ├─ result-rust-1_91 → /nix/store/-rust-minimal-1.91.0 - ├─ result-cargo-deb → /nix/store/-cargo-deb - └─ result-cargo-rpm → /nix/store/-cargo-generate-rpm -======= ```mermaid flowchart TB subgraph update["Expected Hash Update Workflow (CI-driven)"] @@ -633,7 +468,6 @@ flowchart TB check_cache -- no --> nix_build --> load_tools load_tools --> package --> smoke --> gen_sha --> done end ->>>>>>> Stashed changes ``` ### Step 1: Prewarm all dependencies (first-time setup) diff --git a/nix/expected-hashes/cli.vendor.linux.sha256 b/nix/expected-hashes/cli.vendor.linux.sha256 index 41c073d5f5..642d9ca965 100644 --- a/nix/expected-hashes/cli.vendor.linux.sha256 +++ b/nix/expected-hashes/cli.vendor.linux.sha256 @@ -1,9 +1 @@ -<<<<<<< HEAD -<<<<<<< HEAD -sha256-BLYlBznRLu5RBetai9ZGsdb8C7cpm5PLCIf3/MJMOnM= -======= -sha256-lnIKTuxfQ10VVsBjSSU7gD7zkJA1XvdGCvxJzYVhMAc= ->>>>>>> 801f9218 (test: multiple updates) -======= sha256-rHfD9R90SalVpvoaBuxSXkpEIBlnHTPVYZTvkaz6ZCE= ->>>>>>> c16b82b2 (fix: update nix vendor hashes) diff --git a/nix/expected-hashes/server.vendor.static.sha256 b/nix/expected-hashes/server.vendor.static.sha256 index 3e2d40969c..9d69ce8320 100644 --- a/nix/expected-hashes/server.vendor.static.sha256 +++ b/nix/expected-hashes/server.vendor.static.sha256 @@ -1,13 +1 @@ -<<<<<<< HEAD -<<<<<<< HEAD -<<<<<<< HEAD -sha256-ZzuWLgecB3Hpz1TyDw0/ew6dihrrkdNP3iq+Ts8WEHw= -======= -sha256-npzFql9tG0Lx7Ij7c/hzKtIPIYvoWFpAE2/pl6qlIRQ= ->>>>>>> 3efb77ba (fix: nix hash) -======= -sha256-0UbqfLFumMDHXHDk0y7gZt62wJqxbHiJBFUhfZuX7Ro= ->>>>>>> e2d57f30 (fix: fixes) -======= sha256-o1iIUkI+0xV8aPLmqUilZJwbNk8IvIqsWwW97pOCe+E= ->>>>>>> c4db7311 (fix: hash) From 8a3dec050b5448230ccf24690c2fbf53288c9706 Mon Sep 17 00:00:00 2001 From: HatemMn <19950216+HatemMn@users.noreply.github.com> Date: Mon, 15 Jun 2026 16:33:46 +0200 Subject: [PATCH 24/28] fix: ciC --- CHANGELOG/feat_richOTELmetrics.md | 2 +- crate/test_kms_server/src/vector_runner.rs | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG/feat_richOTELmetrics.md b/CHANGELOG/feat_richOTELmetrics.md index ed92979555..faa1024ad5 100644 --- a/CHANGELOG/feat_richOTELmetrics.md +++ b/CHANGELOG/feat_richOTELmetrics.md @@ -19,7 +19,7 @@ - Add `OtelHttpMetrics` Actix-web middleware (`crate/server/src/middlewares/otel_http_middleware.rs`) that records `kms.http.requests.total` (counter with `method`, `path`, `status` - attributes), `kms.http.request.duration` (histogram with `method`, `path`), and + attributes), `kms.http.request.duration` (histogram with `method`, `path`, `status`), and `kms.active.connections` (in-flight up-down counter) for every HTTP request. - Middleware is installed as the outermost `App`-level wrap to measure true client-perceived latency including all inner middleware. diff --git a/crate/test_kms_server/src/vector_runner.rs b/crate/test_kms_server/src/vector_runner.rs index 217fbb206b..516f6e727e 100644 --- a/crate/test_kms_server/src/vector_runner.rs +++ b/crate/test_kms_server/src/vector_runner.rs @@ -3309,6 +3309,9 @@ ObjectType = "SymmetricKey" } // ── Negative tests: ReCertify ─────────────────────────────────────── + // ReCertify is not yet implemented (KMIP 1.4 only); these tests verify the + // server correctly rejects the operation. Enable positive recertify tests + // above once the operation is dispatched. #[tokio::test] async fn test_neg_recertify_missing_uid() -> Result<(), KmsClientError> { From 1387e1f47a853b5e47bc2a8fe64488ea8c4494ff Mon Sep 17 00:00:00 2001 From: HatemMn <19950216+HatemMn@users.noreply.github.com> Date: Mon, 15 Jun 2026 16:37:30 +0200 Subject: [PATCH 25/28] fix: manifest test data --- test_data | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test_data b/test_data index 41871788ac..107e3c7154 160000 --- a/test_data +++ b/test_data @@ -1 +1 @@ -Subproject commit 41871788ac8b8ebbb990747828f25960863538c4 +Subproject commit 107e3c7154bd81c5320cdd88c4ff36b010af4260 From ef9757ad14fc65d0cd689b8ab0331ba86807f8e0 Mon Sep 17 00:00:00 2001 From: HatemMn <19950216+HatemMn@users.noreply.github.com> Date: Mon, 15 Jun 2026 16:49:28 +0200 Subject: [PATCH 26/28] fix: ci_9 --- nix/expected-hashes/cli.vendor.linux.sha256 | 2 +- nix/expected-hashes/server.vendor.static.sha256 | 2 +- nix/expected-hashes/ui.vendor.non-fips.sha256 | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/nix/expected-hashes/cli.vendor.linux.sha256 b/nix/expected-hashes/cli.vendor.linux.sha256 index 642d9ca965..f6410ab554 100644 --- a/nix/expected-hashes/cli.vendor.linux.sha256 +++ b/nix/expected-hashes/cli.vendor.linux.sha256 @@ -1 +1 @@ -sha256-rHfD9R90SalVpvoaBuxSXkpEIBlnHTPVYZTvkaz6ZCE= +sha256-pWvIUdTFSO566iS8AaPmEWfz8tuE6k3QxS/iGK4Y2gA= diff --git a/nix/expected-hashes/server.vendor.static.sha256 b/nix/expected-hashes/server.vendor.static.sha256 index 9d69ce8320..5e3f3e963c 100644 --- a/nix/expected-hashes/server.vendor.static.sha256 +++ b/nix/expected-hashes/server.vendor.static.sha256 @@ -1 +1 @@ -sha256-o1iIUkI+0xV8aPLmqUilZJwbNk8IvIqsWwW97pOCe+E= +sha256-eQdmSdDu5ehy20dKbSHmD8vQ7fR2tjB58A8afZKCASQ= diff --git a/nix/expected-hashes/ui.vendor.non-fips.sha256 b/nix/expected-hashes/ui.vendor.non-fips.sha256 index d842e6a920..0b08340ba6 100644 --- a/nix/expected-hashes/ui.vendor.non-fips.sha256 +++ b/nix/expected-hashes/ui.vendor.non-fips.sha256 @@ -1 +1 @@ -sha256-NlAidjG7Z1x6aw8y7rkPA5/o14bNIIx3fmKeG0zT0Ec= +sha256-Ik9Gga/IrZ96WiHPEUsd7gGTA6lm0gvWZO03zfNVrwc= From c65db2abafa4bd9d3107c5d21377d75fa9cbdda2 Mon Sep 17 00:00:00 2001 From: HatemMn <19950216+HatemMn@users.noreply.github.com> Date: Mon, 15 Jun 2026 17:52:07 +0200 Subject: [PATCH 27/28] fix: blue lock --- Cargo.lock | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 25cdfc48f5..75b9c04861 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1990,7 +1990,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -3329,7 +3329,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -4454,7 +4454,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -4899,7 +4899,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -4986,12 +4986,6 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" -[[package]] -name = "symlink" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7973cce6668464ea31f176d85b13c7ab3bba2cb3b77a2ed26abd7801688010a" - [[package]] name = "syn" version = "2.0.117" @@ -5044,7 +5038,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -5502,12 +5496,11 @@ dependencies = [ [[package]] name = "tracing-appender" -version = "0.2.5" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "050686193eb999b4bb3bc2acfa891a13da00f79734704c4b8b4ef1a10b368a3c" +checksum = "786d480bce6247ab75f005b14ae1624ad978d3029d9113f0a22fa1ac773faeaf" dependencies = [ "crossbeam-channel", - "symlink", "thiserror 2.0.18", "time", "tracing-subscriber", @@ -6017,7 +6010,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.48.0", ] [[package]] From 7bd470d2a1e451a71fbbb53f557f79caf5df655c Mon Sep 17 00:00:00 2001 From: HatemMn <19950216+HatemMn@users.noreply.github.com> Date: Mon, 15 Jun 2026 18:12:50 +0200 Subject: [PATCH 28/28] fix: nix --- nix/expected-hashes/cli.vendor.linux.sha256 | 2 +- nix/expected-hashes/server.vendor.static.sha256 | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/nix/expected-hashes/cli.vendor.linux.sha256 b/nix/expected-hashes/cli.vendor.linux.sha256 index f6410ab554..8e6523ee49 100644 --- a/nix/expected-hashes/cli.vendor.linux.sha256 +++ b/nix/expected-hashes/cli.vendor.linux.sha256 @@ -1 +1 @@ -sha256-pWvIUdTFSO566iS8AaPmEWfz8tuE6k3QxS/iGK4Y2gA= +sha256-6supaSDfTlswq+dF78tbSZfvFGL+1VGCxTOFjZG8rGo= diff --git a/nix/expected-hashes/server.vendor.static.sha256 b/nix/expected-hashes/server.vendor.static.sha256 index 5e3f3e963c..7e6a53a7ca 100644 --- a/nix/expected-hashes/server.vendor.static.sha256 +++ b/nix/expected-hashes/server.vendor.static.sha256 @@ -1 +1 @@ -sha256-eQdmSdDu5ehy20dKbSHmD8vQ7fR2tjB58A8afZKCASQ= +sha256-jJT4Rc2nLF9AMmNlgAi8wd1KFa3gO0JL5ob+VLvZ9o0=