#[cfg(feature = "otel-metrics")]
use crate::DASHBOARD_METER_SCOPE;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DashboardStorageBackend {
File,
Sqlite,
MySql,
Postgres,
}
#[cfg(feature = "otel-metrics")]
impl DashboardStorageBackend {
const fn as_str(self) -> &'static str {
match self {
Self::File => "file",
Self::Sqlite => "sqlite",
Self::MySql => "mysql",
Self::Postgres => "postgres",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DashboardStorageResult {
Available,
Degraded,
Unavailable,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DashboardStorageOperation {
Status,
PersistedMutation,
HistoryCollection,
HistoryRetention,
SessionAuditCleanup,
}
#[cfg(feature = "otel-metrics")]
impl DashboardStorageOperation {
const fn as_str(self) -> &'static str {
match self {
Self::Status => "status",
Self::PersistedMutation => "persisted_mutation",
Self::HistoryCollection => "history_collection",
Self::HistoryRetention => "history_retention",
Self::SessionAuditCleanup => "session_audit_cleanup",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DashboardStorageOperationResult {
Success,
Failure,
}
#[cfg(feature = "otel-metrics")]
impl DashboardStorageOperationResult {
const fn as_str(self) -> &'static str {
match self {
Self::Success => "success",
Self::Failure => "failure",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DashboardStorageFailureLabel {
Capacity,
Connection,
Conflict,
Timeout,
Other,
}
#[cfg(feature = "otel-metrics")]
impl DashboardStorageFailureLabel {
const fn as_str(self) -> &'static str {
match self {
Self::Capacity => "capacity",
Self::Connection => "connection",
Self::Conflict => "conflict",
Self::Timeout => "timeout",
Self::Other => "other",
}
}
}
#[cfg(feature = "otel-metrics")]
impl DashboardStorageResult {
const fn as_str(self) -> &'static str {
match self {
Self::Available => "available",
Self::Degraded => "degraded",
Self::Unavailable => "unavailable",
}
}
}
#[derive(Clone)]
pub struct DashboardStorageMetricsRecorder {
#[cfg(feature = "otel-metrics")]
telemetry: crate::TelemetryRecorder,
#[cfg(feature = "otel-metrics")]
metrics: Option<DashboardStorageMetrics>,
}
impl DashboardStorageMetricsRecorder {
#[must_use]
pub fn noop() -> Self {
Self::from_handle(&crate::TelemetryHandle::noop())
}
#[must_use]
pub fn from_handle(handle: &crate::TelemetryHandle) -> Self {
#[cfg(feature = "otel-metrics")]
{
let telemetry = handle.child(DASHBOARD_METER_SCOPE);
let metrics = telemetry.meter().map(|meter| DashboardStorageMetrics::new(&meter));
Self { telemetry, metrics }
}
#[cfg(not(feature = "otel-metrics"))]
{
let _ = handle;
Self {}
}
}
pub fn record_status(&self, backend: DashboardStorageBackend, result: DashboardStorageResult) {
#[cfg(feature = "otel-metrics")]
if self.telemetry.is_active() {
if let Some(metrics) = &self.metrics {
metrics.record_status(backend, result);
}
}
#[cfg(not(feature = "otel-metrics"))]
let _ = (backend, result);
}
pub fn record_operation(
&self,
backend: DashboardStorageBackend,
operation: DashboardStorageOperation,
result: DashboardStorageOperationResult,
failure: Option<DashboardStorageFailureLabel>,
elapsed: std::time::Duration,
) {
#[cfg(feature = "otel-metrics")]
if self.telemetry.is_active() {
if let Some(metrics) = &self.metrics {
metrics.record_operation(backend, operation, result, failure, elapsed);
}
}
#[cfg(not(feature = "otel-metrics"))]
let _ = (backend, operation, result, failure, elapsed);
}
pub fn record_state(
&self,
backend: DashboardStorageBackend,
available_bytes: Option<u64>,
pool_size: Option<u32>,
idle_connections: Option<usize>,
) {
#[cfg(feature = "otel-metrics")]
if self.telemetry.is_active() {
if let Some(metrics) = &self.metrics {
metrics.record_state(backend, available_bytes, pool_size, idle_connections);
}
}
#[cfg(not(feature = "otel-metrics"))]
let _ = (backend, available_bytes, pool_size, idle_connections);
}
}
#[cfg(feature = "otel-metrics")]
#[derive(Clone)]
struct DashboardStorageMetrics {
operations_total: opentelemetry::metrics::Counter<u64>,
operation_duration_milliseconds: opentelemetry::metrics::Histogram<u64>,
operation_errors_total: opentelemetry::metrics::Counter<u64>,
capacity_bytes: opentelemetry::metrics::Gauge<u64>,
pool_connections: opentelemetry::metrics::Gauge<u64>,
}
#[cfg(feature = "otel-metrics")]
impl DashboardStorageMetrics {
fn new(meter: &opentelemetry::metrics::Meter) -> Self {
Self {
operations_total: meter
.u64_counter(crate::semantic::metrics::DASHBOARD_STORAGE_OPERATIONS_TOTAL)
.with_description("Dashboard storage status and operation events")
.build(),
operation_duration_milliseconds: meter
.u64_histogram(crate::semantic::metrics::DASHBOARD_STORAGE_OPERATION_DURATION_MILLISECONDS)
.with_unit("ms")
.with_description("Dashboard storage operation duration")
.build(),
operation_errors_total: meter
.u64_counter(crate::semantic::metrics::DASHBOARD_STORAGE_OPERATION_ERRORS_TOTAL)
.with_description("Dashboard storage operation failures")
.build(),
capacity_bytes: meter
.u64_gauge(crate::semantic::metrics::DASHBOARD_STORAGE_CAPACITY_BYTES)
.with_unit("By")
.with_description("Dashboard File or SQLite safe available capacity")
.build(),
pool_connections: meter
.u64_gauge(crate::semantic::metrics::DASHBOARD_STORAGE_POOL_CONNECTIONS)
.with_description("Dashboard SQL connection pool state")
.build(),
}
}
fn record_status(&self, backend: DashboardStorageBackend, result: DashboardStorageResult) {
self.operations_total.add(
1,
&[
opentelemetry::KeyValue::new("backend", backend.as_str()),
opentelemetry::KeyValue::new("operation", "status"),
opentelemetry::KeyValue::new("result", result.as_str()),
],
);
}
fn record_operation(
&self,
backend: DashboardStorageBackend,
operation: DashboardStorageOperation,
result: DashboardStorageOperationResult,
failure: Option<DashboardStorageFailureLabel>,
elapsed: std::time::Duration,
) {
let labels = [
opentelemetry::KeyValue::new("backend", backend.as_str()),
opentelemetry::KeyValue::new("operation", operation.as_str()),
opentelemetry::KeyValue::new("result", result.as_str()),
];
self.operations_total.add(1, &labels);
let elapsed_milliseconds = u64::try_from(elapsed.as_millis()).unwrap_or(u64::MAX);
self.operation_duration_milliseconds
.record(elapsed_milliseconds, &labels);
if let Some(failure) = failure {
self.operation_errors_total.add(
1,
&[
opentelemetry::KeyValue::new("backend", backend.as_str()),
opentelemetry::KeyValue::new("operation", operation.as_str()),
opentelemetry::KeyValue::new("error_kind", failure.as_str()),
],
);
}
}
fn record_state(
&self,
backend: DashboardStorageBackend,
available_bytes: Option<u64>,
pool_size: Option<u32>,
idle_connections: Option<usize>,
) {
let backend_label = [opentelemetry::KeyValue::new("backend", backend.as_str())];
if let Some(available_bytes) = available_bytes {
self.capacity_bytes.record(available_bytes, &backend_label);
}
if let Some(pool_size) = pool_size {
self.pool_connections.record(
u64::from(pool_size),
&[
opentelemetry::KeyValue::new("backend", backend.as_str()),
opentelemetry::KeyValue::new("state", "total"),
],
);
}
if let Some(idle_connections) = idle_connections {
self.pool_connections.record(
u64::try_from(idle_connections).unwrap_or(u64::MAX),
&[
opentelemetry::KeyValue::new("backend", backend.as_str()),
opentelemetry::KeyValue::new("state", "idle"),
],
);
}
}
}
#[cfg(test)]
mod tests {
use super::DashboardStorageBackend;
use super::DashboardStorageFailureLabel;
use super::DashboardStorageMetricsRecorder;
use super::DashboardStorageOperation;
use super::DashboardStorageOperationResult;
use super::DashboardStorageResult;
#[test]
fn noop_recorder_accepts_only_typed_low_cardinality_values() {
let recorder = DashboardStorageMetricsRecorder::noop();
recorder.record_status(DashboardStorageBackend::File, DashboardStorageResult::Available);
recorder.record_status(DashboardStorageBackend::Postgres, DashboardStorageResult::Unavailable);
recorder.record_operation(
DashboardStorageBackend::Sqlite,
DashboardStorageOperation::HistoryCollection,
DashboardStorageOperationResult::Failure,
Some(DashboardStorageFailureLabel::Timeout),
std::time::Duration::from_millis(1),
);
recorder.record_state(DashboardStorageBackend::File, Some(1), None, None);
}
}