use std::time::Duration; use opentelemetry::{KeyValue, global, metrics::Counter}; use crate::SpacetimeClientError; // SpacetimeDB procedure 指标只使用 procedure / status_class 等低基数字段,避免把用户或作品 ID 写进指标标签。 pub(crate) struct ProcedureMetricsGuard { procedure: &'static str, started_at: std::time::Instant, } pub(crate) struct ReadMetricsGuard { read: &'static str, started_at: std::time::Instant, } pub(crate) fn begin_procedure(procedure: &'static str) -> ProcedureMetricsGuard { ProcedureMetricsGuard { procedure, started_at: std::time::Instant::now(), } } pub(crate) fn begin_read(read: &'static str) -> ReadMetricsGuard { ReadMetricsGuard { read, started_at: std::time::Instant::now(), } } impl ProcedureMetricsGuard { pub(crate) fn finish(&self, result: &Result) { let duration = self.started_at.elapsed(); record_procedure(self.procedure, duration, result.is_err()); } } impl ReadMetricsGuard { pub(crate) fn finish(&self, result: &Result) { let duration = self.started_at.elapsed(); record_read(self.read, duration, result.is_err()); } } struct SpacetimeMetrics { calls: Counter, errors: Counter, duration_ms: opentelemetry::metrics::Histogram, read_calls: Counter, read_errors: Counter, read_duration_ms: opentelemetry::metrics::Histogram, } fn spacetime_metrics() -> &'static SpacetimeMetrics { static METRICS: std::sync::OnceLock = std::sync::OnceLock::new(); METRICS.get_or_init(|| { let meter = global::meter("genarrative-spacetime-client"); SpacetimeMetrics { calls: meter .u64_counter("genarrative.spacetime.procedure.calls") .with_description("SpacetimeDB procedure call count") .build(), errors: meter .u64_counter("genarrative.spacetime.procedure.errors") .with_description("SpacetimeDB procedure error count") .build(), duration_ms: meter .f64_histogram("genarrative.spacetime.procedure.duration_ms") .with_unit("ms") .with_description("SpacetimeDB procedure duration in milliseconds") .build(), read_calls: meter .u64_counter("genarrative.spacetime.read.calls") .with_description("SpacetimeDB local subscription cache read count") .build(), read_errors: meter .u64_counter("genarrative.spacetime.read.errors") .with_description("SpacetimeDB local subscription cache read error count") .build(), read_duration_ms: meter .f64_histogram("genarrative.spacetime.read.duration_ms") .with_unit("ms") .with_description("SpacetimeDB local subscription cache read duration in milliseconds") .build(), } }) } fn record_procedure(procedure: &'static str, duration: Duration, failed: bool) { let labels = vec![ KeyValue::new("procedure", procedure), KeyValue::new("status_class", if failed { "error" } else { "ok" }), ]; let metrics = spacetime_metrics(); metrics.calls.add(1, &labels); metrics .duration_ms .record(duration.as_secs_f64() * 1000.0, &labels); if failed { metrics.errors.add(1, &labels); } } fn record_read(read: &'static str, duration: Duration, failed: bool) { let labels = vec![ KeyValue::new("read", read), KeyValue::new("status_class", if failed { "error" } else { "ok" }), ]; let metrics = spacetime_metrics(); metrics.read_calls.add(1, &labels); metrics .read_duration_ms .record(duration.as_secs_f64() * 1000.0, &labels); if failed { metrics.read_errors.add(1, &labels); } }