use crate::admission::AdmissionDimension;
use crate::context::PipelineContext;
use otel_arrow_dfe_telemetry::error::Error as TelemetryError;
use otel_arrow_dfe_telemetry::instrument::Counter;
use otel_arrow_dfe_telemetry::metrics::{MeasurementMetricSet, MetricSetSnapshot};
use otel_arrow_dfe_telemetry::reporter::MetricsReporter;
use otel_arrow_dfe_telemetry_macros::{AttributeEnum, attribute_set, metric_set};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
#[derive(Debug, Clone, Copy, PartialEq, Eq, AttributeEnum)]
pub enum AdmissionRefusal {
WouldThrottle,
Throttle,
Oversized,
}
impl AdmissionRefusal {
const fn index(self) -> usize {
match self {
Self::WouldThrottle => 0,
Self::Throttle => 1,
Self::Oversized => 2,
}
}
const ALL: [Self; REFUSAL_KINDS] = [Self::WouldThrottle, Self::Throttle, Self::Oversized];
}
const REFUSAL_KINDS: usize = 3;
#[attribute_set(item, measurement)]
#[derive(Debug, Clone, Copy)]
pub struct AdmissionRefusalAttributes {
pub dimension: AdmissionDimension,
pub refusal: AdmissionRefusal,
}
#[metric_set(
name = "admission.rate_limiter",
measurement_attributes = AdmissionRefusalAttributes
)]
#[derive(Debug, Default, Clone)]
pub struct RateAdmissionMetrics {
#[metric(unit = "{decision}")]
pub refusals: Counter<u64>,
}
#[derive(Debug, Default)]
pub struct RefusalCounters {
counts: [AtomicU64; REFUSAL_KINDS],
}
impl RefusalCounters {
#[inline]
pub fn record(&self, refusal: AdmissionRefusal) {
let _ = self.counts[refusal.index()].fetch_add(1, Ordering::Relaxed);
}
pub(crate) fn drain(&self) -> [u64; REFUSAL_KINDS] {
let mut drained = [0_u64; REFUSAL_KINDS];
for (slot, counter) in drained.iter_mut().zip(self.counts.iter()) {
*slot = counter.swap(0, Ordering::Relaxed);
}
drained
}
}
#[derive(Clone)]
pub(crate) struct AdmissionMetricsHandle {
dimension: AdmissionDimension,
counters: Arc<RefusalCounters>,
metrics: Arc<Mutex<MeasurementMetricSet<RateAdmissionMetrics>>>,
}
impl AdmissionMetricsHandle {
pub(crate) fn new(
pipeline_ctx: &PipelineContext,
dimension: AdmissionDimension,
counters: Arc<RefusalCounters>,
) -> Self {
Self {
dimension,
counters,
metrics: Arc::new(Mutex::new(RateAdmissionMetrics::register(pipeline_ctx))),
}
}
pub(crate) fn report(
&self,
metrics_reporter: &mut MetricsReporter,
) -> Result<(), TelemetryError> {
let Ok(mut metrics) = self.metrics.try_lock() else {
return Ok(());
};
self.apply_drain(&mut metrics);
metrics_reporter.report_measurement(&mut metrics)
}
pub(crate) fn terminal_snapshots(&self) -> Vec<MetricSetSnapshot> {
let mut metrics = self
.metrics
.lock()
.unwrap_or_else(|error| error.into_inner());
self.apply_drain(&mut metrics);
metrics.terminal_snapshots()
}
fn apply_drain(&self, metrics: &mut MeasurementMetricSet<RateAdmissionMetrics>) {
let drained = self.counters.drain();
for (refusal, count) in AdmissionRefusal::ALL.into_iter().zip(drained) {
if count == 0 {
continue;
}
metrics
.with(AdmissionRefusalAttributes {
dimension: self.dimension,
refusal,
})
.refusals
.add(count);
}
}
}
#[derive(Default)]
pub(crate) struct AdmissionMetricsRegistry {
handles: Vec<AdmissionMetricsHandle>,
}
impl AdmissionMetricsRegistry {
pub(crate) fn register_if_enabled<F>(
&mut self,
basic_runtime_metrics_enabled: bool,
make_handle: F,
) where
F: FnOnce() -> Option<AdmissionMetricsHandle>,
{
if basic_runtime_metrics_enabled && let Some(handle) = make_handle() {
self.handles.push(handle);
}
}
pub(crate) fn into_handles(self) -> Vec<AdmissionMetricsHandle> {
self.handles
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn draining_returns_staged_counts_once() {
let counters = RefusalCounters::default();
counters.record(AdmissionRefusal::WouldThrottle);
counters.record(AdmissionRefusal::WouldThrottle);
counters.record(AdmissionRefusal::Throttle);
counters.record(AdmissionRefusal::Oversized);
assert_eq!(counters.drain(), [2, 1, 1]);
assert_eq!(counters.drain(), [0, 0, 0]);
}
#[test]
fn concurrent_staging_loses_no_increments() {
let counters = Arc::new(RefusalCounters::default());
std::thread::scope(|scope| {
for _ in 0..8 {
let counters = Arc::clone(&counters);
let _ = scope.spawn(move || {
for _ in 0..1_000 {
counters.record(AdmissionRefusal::Throttle);
}
});
}
});
assert_eq!(counters.drain(), [0, 8_000, 0]);
}
#[test]
fn refusal_indices_are_distinct_and_in_bounds() {
let indices: Vec<usize> = AdmissionRefusal::ALL
.into_iter()
.map(AdmissionRefusal::index)
.collect();
assert_eq!(indices, vec![0, 1, 2]);
assert_eq!(indices.len(), REFUSAL_KINDS);
}
#[test]
fn terminal_snapshot_uses_common_bounded_vocabulary_once() {
let (pipeline_ctx, _registry) = crate::testing::test_pipeline_ctx();
let counters = Arc::new(RefusalCounters::default());
let handle = AdmissionMetricsHandle::new(
&pipeline_ctx,
AdmissionDimension::Messages,
Arc::clone(&counters),
);
counters.record(AdmissionRefusal::Throttle);
let snapshots = handle.terminal_snapshots();
assert_eq!(snapshots.len(), 1);
assert_eq!(snapshots[0].descriptor().name, "admission.rate_limiter");
assert_eq!(
snapshots[0].measurement_attribute_value("dimension"),
Some("messages")
);
assert_eq!(
snapshots[0].measurement_attribute_value("refusal"),
Some("throttle")
);
assert!(handle.terminal_snapshots().is_empty());
}
#[test]
fn registration_respects_runtime_metrics_gate() {
let (pipeline_ctx, _registry) = crate::testing::test_pipeline_ctx();
let counters = Arc::new(RefusalCounters::default());
let disabled_handle_was_requested = std::cell::Cell::new(false);
let mut disabled = AdmissionMetricsRegistry::default();
disabled.register_if_enabled(false, || {
disabled_handle_was_requested.set(true);
Some(AdmissionMetricsHandle::new(
&pipeline_ctx,
AdmissionDimension::Messages,
Arc::clone(&counters),
))
});
assert!(!disabled_handle_was_requested.get());
assert!(disabled.into_handles().is_empty());
let mut enabled = AdmissionMetricsRegistry::default();
enabled.register_if_enabled(true, || {
Some(AdmissionMetricsHandle::new(
&pipeline_ctx,
AdmissionDimension::Messages,
counters,
))
});
assert_eq!(enabled.into_handles().len(), 1);
}
}