Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -404,9 +404,14 @@ If the prompt adds a new security feature, hardens an existing one, or fixes a s

### Test vectors

For every feature added or bug fixed, a test vector **must** be added to `test_data/vectors/`.
This is not optional — it is enforced by the mandatory checklist above.

For every feature added or bug fixed, a test vector **must** be added to `test_data/vectors/` **unless** one the following conditions is met:
- The behavior being tested is not observable in a KMIP HTTP response (e.g., metric emission, in-process state changes).
- The new vector would exercise the same KMIP operations already covered by existing vectors,
just with a new name to "document" a feature, without catching a regression that no
other vector would catch.
- The vector will always pass regardless of weather the new feature works.

If none of these apply, the vector **must** be added as follows :
1. Model the vector on existing examples in `test_data/vectors/`.
2. Register the corresponding test function in `crate/test_kms_server/src/vector_runner.rs`.
3. Run the test and confirm it passes: `cargo test -p test_kms_server <fn_name>`.
Expand Down
16 changes: 16 additions & 0 deletions CHANGELOG/feat_richOTELmetrics.md
Original file line number Diff line number Diff line change
@@ -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"`).
12 changes: 10 additions & 2 deletions crate/server/src/core/kms/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<Arc<dyn DbMetricsRecorder>> =
metrics.as_ref().map(|m| -> Arc<dyn DbMetricsRecorder> {
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?;

Expand All @@ -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,
})
}

Expand Down
53 changes: 43 additions & 10 deletions crate/server/src/core/otel_metrics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down Expand Up @@ -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()),
],
);
}

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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);
}
}
Loading
Loading