use std::os::raw::c_void;
use std::sync::OnceLock;
use abi_stable::std_types::{RSlice, RStr};
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MetricKind {
Counter = 0,
Gauge = 1,
Histogram = 2,
}
pub type MetricLabel<'a> = (RStr<'a>, RStr<'a>);
pub type RecordMetricFn = extern "C" fn(
ctx: *const c_void,
name: RStr<'_>,
labels: RSlice<'_, MetricLabel<'_>>,
value: f64,
kind: MetricKind,
);
pub struct MetricRecorder {
inner: OnceLock<RecorderInner>,
}
struct RecorderInner {
record_fn: RecordMetricFn,
ctx: *const c_void,
}
unsafe impl Send for RecorderInner {}
unsafe impl Sync for RecorderInner {}
impl MetricRecorder {
#[must_use]
pub const fn new() -> Self {
Self {
inner: OnceLock::new(),
}
}
#[doc(hidden)]
pub fn install(&self, record_fn: RecordMetricFn, ctx: *const c_void) {
let _ = self.inner.set(RecorderInner { record_fn, ctx });
}
pub fn counter(&self, name: &str, labels: &[(&str, &str)], increment: u64) {
#[allow(clippy::cast_precision_loss)]
self.emit(name, labels, increment as f64, MetricKind::Counter);
}
pub fn gauge(&self, name: &str, labels: &[(&str, &str)], value: f64) {
self.emit(name, labels, value, MetricKind::Gauge);
}
pub fn histogram(&self, name: &str, labels: &[(&str, &str)], value: f64) {
self.emit(name, labels, value, MetricKind::Histogram);
}
fn emit(&self, name: &str, labels: &[(&str, &str)], value: f64, kind: MetricKind) {
let Some(inner) = self.inner.get() else {
return;
};
let ffi_labels: Vec<MetricLabel<'_>> = labels
.iter()
.map(|(k, v)| (RStr::from(*k), RStr::from(*v)))
.collect();
(inner.record_fn)(
inner.ctx,
RStr::from(name),
RSlice::from(ffi_labels.as_slice()),
value,
kind,
);
}
}
impl Default for MetricRecorder {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Debug for MetricRecorder {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MetricRecorder")
.field("installed", &self.inner.get().is_some())
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::ptr;
use std::sync::atomic::{AtomicU64, Ordering};
static EMITTED_COUNT: AtomicU64 = AtomicU64::new(0);
extern "C" fn test_recorder(
_ctx: *const c_void,
_name: RStr<'_>,
_labels: RSlice<'_, MetricLabel<'_>>,
_value: f64,
_kind: MetricKind,
) {
EMITTED_COUNT.fetch_add(1, Ordering::Relaxed);
}
#[test]
fn emit_without_install_is_silent_noop() {
let r = MetricRecorder::new();
r.counter("test", &[], 1);
r.gauge("test", &[], 1.0);
r.histogram("test", &[], 1.0);
}
#[test]
fn install_then_emit_routes_to_recorder() {
let before = EMITTED_COUNT.load(Ordering::Relaxed);
let r = MetricRecorder::new();
r.install(test_recorder, ptr::null());
r.counter("dispatches_total", &[("kind", "ok")], 1);
r.gauge("in_flight", &[], 7.0);
r.histogram("latency_seconds", &[("path", "/x")], 0.012);
assert_eq!(EMITTED_COUNT.load(Ordering::Relaxed) - before, 3);
}
#[test]
fn install_is_idempotent() {
let r = MetricRecorder::new();
r.install(test_recorder, ptr::null());
r.install(test_recorder, 0xdead_beef as *const c_void);
}
}