use std::sync::{Arc, Mutex};
use opentelemetry::metrics::{Counter, Histogram, Meter};
use subms::{ObservationCtx, SubMsBenchSummary, SubMsObserver, SubMsStageKind};
use crate::bridge::{attributes_from_ctx, attributes_from_summary, histogram_for_stage};
use crate::drift::ReferenceDivergenceRecorder;
#[cfg(feature = "exemplars")]
use crate::exemplars::ExemplarReservoir;
use crate::{EXEMPLARS_KEPT_COUNTER_NAME, OPS_TOTAL_COUNTER_NAME};
pub struct OtelObserver {
meter: Meter,
cache: Mutex<Vec<CachedStage>>,
ops_counter: Counter<u64>,
exemplars_kept: Counter<u64>,
drift: ReferenceDivergenceRecorder,
#[cfg(feature = "exemplars")]
exemplars: Option<Arc<ExemplarReservoir>>,
}
struct CachedStage {
name: String,
kind: SubMsStageKind,
histogram: Histogram<f64>,
}
impl OtelObserver {
pub fn new(meter: Meter) -> Self {
let ops_counter = meter
.u64_counter(OPS_TOTAL_COUNTER_NAME)
.with_description("Per-stage operation counter; one increment per on_record call")
.build();
let exemplars_kept = meter
.u64_counter(EXEMPLARS_KEPT_COUNTER_NAME)
.with_description("Count of samples retained by an attached exemplar reservoir")
.build();
let drift = ReferenceDivergenceRecorder::new(meter.clone());
Self {
meter,
cache: Mutex::new(Vec::new()),
ops_counter,
exemplars_kept,
drift,
#[cfg(feature = "exemplars")]
exemplars: None,
}
}
#[cfg(feature = "exemplars")]
pub fn with_exemplar_reservoir(mut self, reservoir: Arc<ExemplarReservoir>) -> Self {
self.exemplars = Some(reservoir);
self
}
#[cfg(feature = "exemplars")]
pub fn exemplar_reservoir(&self) -> Option<Arc<ExemplarReservoir>> {
self.exemplars.clone()
}
pub fn record_reference_divergence(
&self,
stage: &str,
kind: SubMsStageKind,
reference_kind: &str,
expected: &str,
observed: &str,
) {
self.drift
.record(stage, kind, reference_kind, expected, observed);
}
#[cfg(feature = "exemplars")]
fn offer_exemplar(&self, ctx: &ObservationCtx<'_>, ns: u64, attrs: &[opentelemetry::KeyValue]) {
let Some(reservoir) = self.exemplars.as_ref() else {
return;
};
if reservoir.offer(ctx, ns) {
self.exemplars_kept.add(1, attrs);
}
}
fn histogram_for(&self, stage: &str, kind: SubMsStageKind) -> Histogram<f64> {
let mut cache = self.cache.lock().expect("observer cache poisoned");
if let Some(entry) = cache.iter().find(|c| c.name == stage && c.kind == kind) {
return entry.histogram.clone();
}
let histogram = histogram_for_stage(&self.meter, kind);
cache.push(CachedStage {
name: stage.to_string(),
kind,
histogram: histogram.clone(),
});
histogram
}
}
impl SubMsObserver for OtelObserver {
fn on_record(&self, ctx: &ObservationCtx, ns: u64) {
let histogram = self.histogram_for(ctx.stage, ctx.stage_kind);
let attrs = attributes_from_ctx(ctx);
histogram.record(ns as f64 / 1e9, &attrs);
self.ops_counter.add(1, &attrs);
#[cfg(feature = "exemplars")]
{
self.offer_exemplar(ctx, ns, &attrs);
}
}
fn on_summarize(&self, summary: &SubMsBenchSummary) {
for stage in &summary.stages {
let kind = {
let cache = self.cache.lock().expect("observer cache poisoned");
cache
.iter()
.find(|c| c.name == stage.name)
.map(|c| c.kind)
.unwrap_or(SubMsStageKind::Unspecified)
};
let histogram = self.histogram_for(&stage.name, kind);
let attrs = attributes_from_summary(summary, stage, kind);
histogram.record(stage.p50_ns as f64 / 1e9, &attrs);
histogram.record(stage.p99_ns as f64 / 1e9, &attrs);
histogram.record(stage.p999_ns as f64 / 1e9, &attrs);
histogram.record(stage.max_ns as f64 / 1e9, &attrs);
histogram.record(stage.mean_ns as f64 / 1e9, &attrs);
if let Some(samples) = &stage.samples_ns {
for ns in samples {
histogram.record(*ns as f64 / 1e9, &attrs);
}
}
}
#[cfg(feature = "exemplars")]
if let Some(reservoir) = &self.exemplars {
reservoir.publish(&self.meter);
}
}
}