use std::{collections::HashMap, fmt, sync::RwLock};
use tracing::{field::Visit, Subscriber};
use tracing_core::Field;
use opentelemetry::{
metrics::{Counter, Histogram, Meter, MeterProvider, UpDownCounter},
sdk::metrics::controllers::BasicController,
Context as OtelContext,
};
use tracing_subscriber::{layer::Context, registry::LookupSpan, Layer};
const CARGO_PKG_VERSION: &str = env!("CARGO_PKG_VERSION");
const INSTRUMENTATION_LIBRARY_NAME: &str = "tracing/tracing-opentelemetry";
const METRIC_PREFIX_MONOTONIC_COUNTER: &str = "monotonic_counter.";
const METRIC_PREFIX_COUNTER: &str = "counter.";
const METRIC_PREFIX_HISTOGRAM: &str = "histogram.";
const I64_MAX: u64 = i64::MAX as u64;
#[derive(Default)]
pub(crate) struct Instruments {
u64_counter: MetricsMap<Counter<u64>>,
f64_counter: MetricsMap<Counter<f64>>,
i64_up_down_counter: MetricsMap<UpDownCounter<i64>>,
f64_up_down_counter: MetricsMap<UpDownCounter<f64>>,
u64_histogram: MetricsMap<Histogram<u64>>,
i64_histogram: MetricsMap<Histogram<i64>>,
f64_histogram: MetricsMap<Histogram<f64>>,
}
type MetricsMap<T> = RwLock<HashMap<&'static str, T>>;
#[derive(Copy, Clone, Debug)]
pub(crate) enum InstrumentType {
CounterU64(u64),
CounterF64(f64),
UpDownCounterI64(i64),
UpDownCounterF64(f64),
HistogramU64(u64),
HistogramI64(i64),
HistogramF64(f64),
}
impl Instruments {
pub(crate) fn update_metric(
&self,
cx: &OtelContext,
meter: &Meter,
instrument_type: InstrumentType,
metric_name: &'static str,
) {
fn update_or_insert<T>(
map: &MetricsMap<T>,
name: &'static str,
insert: impl FnOnce() -> T,
update: impl FnOnce(&T),
) {
{
let lock = map.read().unwrap();
if let Some(metric) = lock.get(name) {
update(metric);
return;
}
}
let mut lock = map.write().unwrap();
let metric = lock.entry(name).or_insert_with(insert);
update(metric)
}
match instrument_type {
InstrumentType::CounterU64(value) => {
update_or_insert(
&self.u64_counter,
metric_name,
|| meter.u64_counter(metric_name).init(),
|ctr| ctr.add(cx, value, &[]),
);
}
InstrumentType::CounterF64(value) => {
update_or_insert(
&self.f64_counter,
metric_name,
|| meter.f64_counter(metric_name).init(),
|ctr| ctr.add(cx, value, &[]),
);
}
InstrumentType::UpDownCounterI64(value) => {
update_or_insert(
&self.i64_up_down_counter,
metric_name,
|| meter.i64_up_down_counter(metric_name).init(),
|ctr| ctr.add(cx, value, &[]),
);
}
InstrumentType::UpDownCounterF64(value) => {
update_or_insert(
&self.f64_up_down_counter,
metric_name,
|| meter.f64_up_down_counter(metric_name).init(),
|ctr| ctr.add(cx, value, &[]),
);
}
InstrumentType::HistogramU64(value) => {
update_or_insert(
&self.u64_histogram,
metric_name,
|| meter.u64_histogram(metric_name).init(),
|rec| rec.record(cx, value, &[]),
);
}
InstrumentType::HistogramI64(value) => {
update_or_insert(
&self.i64_histogram,
metric_name,
|| meter.i64_histogram(metric_name).init(),
|rec| rec.record(cx, value, &[]),
);
}
InstrumentType::HistogramF64(value) => {
update_or_insert(
&self.f64_histogram,
metric_name,
|| meter.f64_histogram(metric_name).init(),
|rec| rec.record(cx, value, &[]),
);
}
};
}
}
pub(crate) struct MetricVisitor<'a> {
pub(crate) instruments: &'a Instruments,
pub(crate) meter: &'a Meter,
}
impl<'a> Visit for MetricVisitor<'a> {
fn record_debug(&mut self, _field: &Field, _value: &dyn fmt::Debug) {
}
fn record_u64(&mut self, field: &Field, value: u64) {
let cx = OtelContext::current();
if let Some(metric_name) = field.name().strip_prefix(METRIC_PREFIX_MONOTONIC_COUNTER) {
self.instruments.update_metric(
&cx,
self.meter,
InstrumentType::CounterU64(value),
metric_name,
);
} else if let Some(metric_name) = field.name().strip_prefix(METRIC_PREFIX_COUNTER) {
if value <= I64_MAX {
self.instruments.update_metric(
&cx,
self.meter,
InstrumentType::UpDownCounterI64(value as i64),
metric_name,
);
} else {
eprintln!(
"[tracing-opentelemetry]: Received Counter metric, but \
provided u64: {} is greater than i64::MAX. Ignoring \
this metric.",
value
);
}
} else if let Some(metric_name) = field.name().strip_prefix(METRIC_PREFIX_HISTOGRAM) {
self.instruments.update_metric(
&cx,
self.meter,
InstrumentType::HistogramU64(value),
metric_name,
);
}
}
fn record_f64(&mut self, field: &Field, value: f64) {
let cx = OtelContext::current();
if let Some(metric_name) = field.name().strip_prefix(METRIC_PREFIX_MONOTONIC_COUNTER) {
self.instruments.update_metric(
&cx,
self.meter,
InstrumentType::CounterF64(value),
metric_name,
);
} else if let Some(metric_name) = field.name().strip_prefix(METRIC_PREFIX_COUNTER) {
self.instruments.update_metric(
&cx,
self.meter,
InstrumentType::UpDownCounterF64(value),
metric_name,
);
} else if let Some(metric_name) = field.name().strip_prefix(METRIC_PREFIX_HISTOGRAM) {
self.instruments.update_metric(
&cx,
self.meter,
InstrumentType::HistogramF64(value),
metric_name,
);
}
}
fn record_i64(&mut self, field: &Field, value: i64) {
let cx = OtelContext::current();
if let Some(metric_name) = field.name().strip_prefix(METRIC_PREFIX_MONOTONIC_COUNTER) {
self.instruments.update_metric(
&cx,
self.meter,
InstrumentType::CounterU64(value as u64),
metric_name,
);
} else if let Some(metric_name) = field.name().strip_prefix(METRIC_PREFIX_COUNTER) {
self.instruments.update_metric(
&cx,
self.meter,
InstrumentType::UpDownCounterI64(value),
metric_name,
);
} else if let Some(metric_name) = field.name().strip_prefix(METRIC_PREFIX_HISTOGRAM) {
self.instruments.update_metric(
&cx,
self.meter,
InstrumentType::HistogramI64(value),
metric_name,
);
}
}
}
#[cfg_attr(docsrs, doc(cfg(feature = "metrics")))]
pub struct MetricsLayer {
meter: Meter,
instruments: Instruments,
}
impl MetricsLayer {
pub fn new(controller: BasicController) -> Self {
let meter =
controller.versioned_meter(INSTRUMENTATION_LIBRARY_NAME, Some(CARGO_PKG_VERSION), None);
MetricsLayer {
meter,
instruments: Default::default(),
}
}
}
impl<S> Layer<S> for MetricsLayer
where
S: Subscriber + for<'span> LookupSpan<'span>,
{
fn on_event(&self, event: &tracing::Event<'_>, _ctx: Context<'_, S>) {
let mut metric_visitor = MetricVisitor {
instruments: &self.instruments,
meter: &self.meter,
};
event.record(&mut metric_visitor);
}
}