Files
mytheclipse/crates/mytheclipse/src/auto_metrics_service.rs
T
asepharyana 1dfc6d6865 fix(ci): restore full CI green — test-matrix, clippy, rustfmt, and rustdoc gates
Root cause of the failing CI run was that examples/tests referencing
feature-gated items were auto-detected (no required-features), so
`--all-targets` compiled them under feature combinations where those
modules didn't exist. Fixes:

- mytheclipse Cargo.toml: declare the `high_level` example and
  `race_stress` integration test with required-features = ["full"];
  `cargo build --all-targets` now skips them when full is off. This
  clears the whole test-matrix (workspace default, all-features, and
  every single-feature config) which all failed on E0432/E0433.
- lib.rs: auto_metrics_service depends on service_builder, so regate it
  behind all(observability, resiliency) instead of observability alone
  (observability-only build compiled the module without resiliency).
- mytheclipse-tracing: gate `pub mod fmt` behind any
  tracing-subscriber-providing feature so --no-default-features compiles.
- mytheclipse-queue: gate `pub mod worker` behind in-memory (worker.rs
  requires tokio, only provided by in-memory).
- clippy -D warnings fixes: deprecated base64 0.22 free fns -> Engine
  (paseto), unused key field, needless mut (service_builder), unused
  import/dead var/missing is_empty (bg_join), dead is_expired (dlock),
  while-let-iterator->for (parallel_map), type_complexity (shutdown_guard),
  MutexGuard held across await (middleware, now clones Arc'd layers),
  if-let-Err->is_err (queue), unused CliBuilder fields now wired into clap.
- rustdoc -D warnings: resolve retry/MetricsBridge/CircuitBreaker/KeyRing
  intra-doc links and fix the unparseable lifecycle.rs code fence.
- cargo fmt --all to satisfy the Rustfmt gate.
2026-08-30 00:33:21 +07:00

163 lines
5.4 KiB
Rust

//! Auto-metrics service builder (feature `observability`).
//!
//! [`AutoMetricsServiceBuilder`] composes a [`crate::ServiceBuilder`] with a
//! [`crate::metrics::MetricsCollector`] so that every call recorded through
//! `.run()` automatically:
//!
//! - increments a `mytheclipse_service_calls_total` counter (labelled by
//! outcome `ok` / `err` / `timeout` / `circuit_open` / `rate_limited`),
//! - observes a `mytheclipse_service_duration_seconds` histogram,
//! - forwards the result to a [`crate::metrics_bridge::MetricsBridge`] when
//! one is attached (e.g. for OpenTelemetry export).
//!
//! This removes the need for callers to hand-wire tracing/metering at every
//! call site.
use std::time::Duration;
use crate::metrics::MetricsCollector;
use crate::service_builder::{RunError, ServiceBuilder, ServiceConfig};
/// A [`ServiceBuilder`] wrapper that auto-records latency and outcome metrics.
///
/// ```
/// use std::time::Duration;
/// use mytheclipse::auto_metrics_service::AutoMetricsServiceBuilder;
/// use mytheclipse::service_builder::ServiceConfig;
///
/// let cfg = ServiceConfig {
/// max_attempts: 2,
/// timeout: Duration::from_millis(1),
/// ..ServiceConfig::default()
/// };
/// let svc = AutoMetricsServiceBuilder::new("checkout", cfg);
/// // Every `.run()` call now auto-records outcome + latency on the shared
/// // MetricsCollector (retrievable via `collector()`).
/// let _ = svc.collector();
/// ```
pub struct AutoMetricsServiceBuilder {
inner: ServiceBuilder,
metrics: MetricsCollector,
#[cfg(feature = "resiliency")]
bridge: Option<crate::metrics_bridge::MetricsBridge>,
service_name: String,
}
impl AutoMetricsServiceBuilder {
/// Creates a new auto-metrics builder around a base [`ServiceConfig`].
pub fn new(service_name: impl Into<String>, config: ServiceConfig) -> Self {
Self {
inner: ServiceBuilder::new(config),
metrics: MetricsCollector::new(),
#[cfg(feature = "resiliency")]
bridge: None,
service_name: service_name.into(),
}
}
/// Sets the underlying [`ServiceBuilder`] (e.g. to attach a circuit
/// breaker) and returns a fresh [`AutoMetricsServiceBuilder`].
pub fn with_builders(self, inner: ServiceBuilder) -> Self {
Self {
inner,
metrics: self.metrics,
#[cfg(feature = "resiliency")]
bridge: self.bridge,
service_name: self.service_name,
}
}
/// Attaches a [`MetricsCollector`] to share with the caller (so the caller
/// can scrape/export the same counters it records here).
pub fn with_collector(mut self, m: MetricsCollector) -> Self {
self.metrics = m;
self
}
/// Attaches a [`crate::metrics_bridge::MetricsBridge`] to forward snapshots
/// downstream (requires the `resiliency` feature which pulls in the bridge).
#[cfg(feature = "resiliency")]
pub fn with_bridge(mut self, bridge: crate::metrics_bridge::MetricsBridge) -> Self {
self.bridge = Some(bridge);
self
}
/// Returns a shared [`MetricsCollector`] handle.
pub fn collector(&self) -> MetricsCollector {
self.metrics.clone()
}
/// Runs a service call, auto-recording metrics around the outcome.
pub async fn run<F, T, E>(&self, f: F) -> Result<T, RunError<E>>
where
F: FnMut() -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<T, E>> + Send>>,
E: std::fmt::Debug,
{
let start = std::time::Instant::now();
let result = self.inner.run(f).await;
let dur: Duration = start.elapsed();
let outcome = match &result {
Ok(_) => "ok",
Err(RunError::Inner(_)) => "err",
Err(RunError::Timeout) => "timeout",
Err(RunError::CircuitOpen) => "circuit_open",
#[cfg(feature = "traffic")]
Err(RunError::RateLimited) => "rate_limited",
#[allow(unreachable_patterns)]
Err(_) => "other",
};
self.metrics.inc_counter(
&format!(
"mytheclipse_service_calls_total{{service=\"{}\",outcome=\"{}\"}}",
self.service_name, outcome
),
1,
);
self.metrics
.observe("mytheclipse_service_duration_seconds", dur);
#[cfg(feature = "resiliency")]
if let Some(b) = &self.bridge {
b.emit_now();
}
result
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::Arc;
#[tokio::test]
async fn auto_metrics_records_call() {
let mut cfg = ServiceConfig::default();
cfg.max_attempts = 3;
let builder = AutoMetricsServiceBuilder::new("test_svc", cfg);
let attempts = Arc::new(AtomicU32::new(0));
let a = Arc::clone(&attempts);
let result: Result<u32, RunError<()>> = builder
.run(|| {
let a = Arc::clone(&a);
Box::pin(async move {
let n = a.fetch_add(1, Ordering::SeqCst);
if n < 2 {
Err(())
} else {
Ok(42u32)
}
})
})
.await;
assert_eq!(result.unwrap(), 42);
assert_eq!(attempts.load(Ordering::SeqCst), 3);
let snap = builder.collector().snapshot();
assert!(snap.counters.len() >= 1);
}
}