use std::time::Instant;
use prometheus::{exponential_buckets, register_histogram_vec_with_registry, Registry};
use tracing::{span, Subscriber};
pub struct PrometheusSpanLatencyLayer {
span_latencies: prometheus::HistogramVec,
}
#[derive(Debug)]
pub enum PrometheusSpanError {
ZeroOrNegativeNumBuckets,
PromError(prometheus::Error),
}
impl From<prometheus::Error> for PrometheusSpanError {
fn from(err: prometheus::Error) -> Self {
Self::PromError(err)
}
}
const TOP_LATENCY_IN_NS: f64 = 300.0 * 1.0e9;
const LOWEST_LATENCY_IN_NS: f64 = 500.0;
impl PrometheusSpanLatencyLayer {
pub fn try_new(registry: &Registry, num_buckets: usize) -> Result<Self, PrometheusSpanError> {
if num_buckets < 1 {
return Err(PrometheusSpanError::ZeroOrNegativeNumBuckets);
}
let factor = (TOP_LATENCY_IN_NS / LOWEST_LATENCY_IN_NS).powf(1.0 / (num_buckets as f64));
let buckets = exponential_buckets(LOWEST_LATENCY_IN_NS, factor, num_buckets)?;
let span_latencies = register_histogram_vec_with_registry!(
"tracing_span_latencies",
"Latencies from tokio-tracing spans",
&["span_name"],
buckets,
registry
)?;
Ok(Self { span_latencies })
}
}
struct PromSpanTimestamp(Instant);
impl<S> tracing_subscriber::Layer<S> for PrometheusSpanLatencyLayer
where
S: Subscriber + for<'span> tracing_subscriber::registry::LookupSpan<'span>,
{
fn on_new_span(
&self,
_attrs: &span::Attributes,
id: &span::Id,
ctx: tracing_subscriber::layer::Context<S>,
) {
let span = ctx.span(id).unwrap();
span.extensions_mut()
.insert(PromSpanTimestamp(Instant::now()));
}
fn on_close(&self, id: span::Id, ctx: tracing_subscriber::layer::Context<'_, S>) {
let span = ctx.span(&id).unwrap();
let start_time = span
.extensions()
.get::<PromSpanTimestamp>()
.expect("Could not find saved timestamp on span")
.0;
let elapsed_ns = start_time.elapsed().as_nanos() as u64;
self.span_latencies
.with_label_values(&[span.name()])
.observe(elapsed_ns as f64);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_prom_span_latency_init() {
let registry = prometheus::Registry::new();
let res = PrometheusSpanLatencyLayer::try_new(®istry, 0);
assert!(matches!(
res,
Err(PrometheusSpanError::ZeroOrNegativeNumBuckets)
));
}
}