Skip to main content

subms_otel/
drift.rs

1//! Reference-impl divergence counter. Recipes that cross-check their behaviour
2//! against a reference library call [`record_reference_divergence`] when the
3//! reference output differs from theirs; that fires a single Counter
4//! `subms.reference.divergence` with the attribute set the bridge already
5//! emits, plus `subms.reference.kind`, `subms.reference.expected`, and
6//! `subms.reference.observed`.
7
8use std::sync::Mutex;
9
10use opentelemetry::KeyValue;
11use opentelemetry::metrics::{Counter, Meter};
12
13use subms::SubMsStageKind;
14
15/// Counter name surfaced to OTEL. Stable across versions.
16pub const REFERENCE_DIVERGENCE_COUNTER_NAME: &str = "subms.reference.divergence";
17
18/// Attribute key for the reference-impl identity (e.g. `"set_membership"`,
19/// `"count_estimate"`).
20pub const REFERENCE_KIND_ATTR: &str = "subms.reference.kind";
21
22/// Attribute key for the reference's expected value, stringified.
23pub const REFERENCE_EXPECTED_ATTR: &str = "subms.reference.expected";
24
25/// Attribute key for the recipe's observed value, stringified.
26pub const REFERENCE_OBSERVED_ATTR: &str = "subms.reference.observed";
27
28/// Build the attribute set carried by every divergence Counter emission.
29pub fn divergence_attributes(
30    stage: &str,
31    kind: SubMsStageKind,
32    reference_kind: &str,
33    expected: &str,
34    observed: &str,
35) -> Vec<KeyValue> {
36    vec![
37        KeyValue::new("subms.stage", stage.to_string()),
38        KeyValue::new("subms.stage.kind", kind.as_str()),
39        KeyValue::new(REFERENCE_KIND_ATTR, reference_kind.to_string()),
40        KeyValue::new(REFERENCE_EXPECTED_ATTR, expected.to_string()),
41        KeyValue::new(REFERENCE_OBSERVED_ATTR, observed.to_string()),
42    ]
43}
44
45/// One-shot helper for recipes that don't want to hold a [`ReferenceDivergenceRecorder`].
46/// Builds the counter on the fly; cheap enough for the cross-check call site
47/// but allocates a fresh instrument every call - if you call this in a tight
48/// loop, build a [`ReferenceDivergenceRecorder`] once and reuse it.
49pub fn record_reference_divergence(
50    meter: &Meter,
51    stage: &str,
52    kind: SubMsStageKind,
53    reference_kind: &str,
54    expected: &str,
55    observed: &str,
56) {
57    let counter: Counter<u64> = meter
58        .u64_counter(REFERENCE_DIVERGENCE_COUNTER_NAME)
59        .with_description("Count of detected divergences between a recipe and its reference impl")
60        .build();
61    let attrs = divergence_attributes(stage, kind, reference_kind, expected, observed);
62    counter.add(1, &attrs);
63}
64
65/// Hold a single Counter instrument so a recipe wiring repeated cross-checks
66/// doesn't pay for a fresh `u64_counter` build per divergence. Internally
67/// cached; cloning is cheap (the counter is reference-counted in OTEL).
68pub struct ReferenceDivergenceRecorder {
69    counter: Mutex<Option<Counter<u64>>>,
70    meter: Meter,
71}
72
73impl ReferenceDivergenceRecorder {
74    /// Build bound to the given meter. Counter is built lazily on first record.
75    pub fn new(meter: Meter) -> Self {
76        Self {
77            counter: Mutex::new(None),
78            meter,
79        }
80    }
81
82    /// Record a divergence event. Stage + kind identify the bench surface,
83    /// reference_kind names the reference algorithm class, expected + observed
84    /// are stringified scalar deltas.
85    pub fn record(
86        &self,
87        stage: &str,
88        kind: SubMsStageKind,
89        reference_kind: &str,
90        expected: &str,
91        observed: &str,
92    ) {
93        let mut slot = self.counter.lock().expect("divergence counter poisoned");
94        if slot.is_none() {
95            *slot = Some(
96                self.meter
97                    .u64_counter(REFERENCE_DIVERGENCE_COUNTER_NAME)
98                    .with_description(
99                        "Count of detected divergences between a recipe and its reference impl",
100                    )
101                    .build(),
102            );
103        }
104        let counter = slot.as_ref().expect("counter just built");
105        let attrs = divergence_attributes(stage, kind, reference_kind, expected, observed);
106        counter.add(1, &attrs);
107    }
108}