use std::borrow::Cow;
use std::collections::HashMap;
use std::sync::{Arc, LazyLock, RwLock};
use opentelemetry::metrics::{
AsyncInstrumentBuilder, Counter, Gauge, Histogram, HistogramBuilder, InstrumentBuilder, Meter,
ObservableCounter, ObservableGauge, ObservableUpDownCounter, UpDownCounter,
};
static METER: LazyLock<Meter> = LazyLock::new(|| opentelemetry::global::meter("logfire"));
type HistogramName = Cow<'static, str>;
pub static EXPONENTIAL_HISTOGRAMS: LazyLock<RwLock<HashMap<HistogramName, i8>>> =
LazyLock::new(|| RwLock::new(HashMap::new()));
pub struct ExponentialHistogram<T> {
inner: Arc<Inner<T>>,
}
struct Inner<T> {
name: HistogramName,
histogram: Histogram<T>,
}
impl<T> Drop for Inner<T> {
fn drop(&mut self) {
let mut histograms = EXPONENTIAL_HISTOGRAMS
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner);
histograms.remove(&self.name);
}
}
impl<T> ExponentialHistogram<T> {
pub fn record(&self, value: T, attributes: &[opentelemetry::KeyValue]) {
self.inner.histogram.record(value, attributes);
}
}
pub struct ExponentialHistogramBuilder<'a, T> {
name: HistogramName,
scale: i8,
inner: HistogramBuilder<'a, Histogram<T>>,
}
impl<'a, T> ExponentialHistogramBuilder<'a, T> {
fn new(name: HistogramName, scale: i8, inner: HistogramBuilder<'a, Histogram<T>>) -> Self {
Self { name, scale, inner }
}
#[must_use]
pub fn with_description<S: Into<Cow<'static, str>>>(mut self, description: S) -> Self {
self.inner = self.inner.with_description(description);
self
}
#[must_use]
pub fn with_unit<S: Into<Cow<'static, str>>>(mut self, unit: S) -> Self {
self.inner = self.inner.with_unit(unit);
self
}
}
impl ExponentialHistogramBuilder<'_, u64> {
#[must_use]
pub fn build(self) -> ExponentialHistogram<u64> {
{
let mut histograms = EXPONENTIAL_HISTOGRAMS
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner);
histograms.insert(self.name.clone(), self.scale);
}
let histogram = self.inner.build();
ExponentialHistogram {
inner: Arc::new(Inner {
name: self.name,
histogram,
}),
}
}
}
impl ExponentialHistogramBuilder<'_, f64> {
#[must_use]
pub fn build(self) -> ExponentialHistogram<f64> {
{
let mut histograms = EXPONENTIAL_HISTOGRAMS
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner);
histograms.insert(self.name.clone(), self.scale);
}
let histogram = self.inner.build();
ExponentialHistogram {
inner: Arc::new(Inner {
name: self.name,
histogram,
}),
}
}
}
#[rustfmt::skip]
macro_rules! metric_doc_header {
(histogram, $method:ident) => {
concat!("Wrapper for [`Meter::", stringify!($method), "`] using Pydantic Logfire's global meter.")
};
(exponential_histogram, $method:ident) => {
concat!("Wrapper for [`Histogram`] using Base2ExponentialHistogram aggregation and Pydantic Logfire's global meter.")
};
}
macro_rules! metric_doc_create_metric {
(histogram, $method:ident, $ty:ty, $var_name:literal) => {
concat!(
"static ",
$var_name,
": LazyLock<opentelemetry::metrics::",
stringify!($ty),
"> = LazyLock::new(|| {
logfire::",
stringify!($method),
"(\"my_counter\")
.with_description(\"Just an example\")
.with_unit(\"s\")
.build()
});"
)
};
(exponential_histogram, $method:ident, $ty:ty, $var_name:literal) => {
concat!(
"static ",
$var_name,
": LazyLock<logfire::",
stringify!($ty),
"> = LazyLock::new(|| {
logfire::",
stringify!($method),
"(\"latency\", 20)
.with_description(\"Just an example\")
.with_unit(\"ms\")
.build()
});"
)
};
}
#[rustfmt::skip]
macro_rules! make_metric_doc {
($method: ident, $ty:ty, $var_name:literal, $usage:literal, $histogram_type: ident) => {
concat!(
metric_doc_header!($histogram_type, $method),
"
# Examples
We recommend using this as a static variable, like so:
```rust
use std::sync::LazyLock;
use opentelemetry::metrics::AsyncInstrument;
",
metric_doc_create_metric!($histogram_type, $method, $ty, $var_name),
"
fn main() -> Result<(), Box<dyn std::error::Error>> {
// ensure Pydantic Logfire is configured before accessing the metric for the first time
let shutdown_handler = logfire::configure()
# .send_to_logfire(logfire::config::SendToLogfire::IfTokenPresent)
.finish()?;
// send a value to the metric
", $usage, ";
shutdown_handler.shutdown()?;
Ok(())
}
```
"
)
};
}
macro_rules! wrap_method {
($method:ident, $ty:ty, $var_name:literal, $usage:literal) => {
#[doc = make_metric_doc!($method, $ty, $var_name, $usage, histogram)]
pub fn $method(name: impl Into<Cow<'static, str>>) -> InstrumentBuilder<'static, $ty> {
METER.$method(name)
}
};
}
macro_rules! wrap_histogram_method {
($method:ident, $ty:ty, $var_name:literal, $usage:literal) => {
#[doc = make_metric_doc!($method, $ty, $var_name, $usage, histogram)]
pub fn $method(name: impl Into<Cow<'static, str>>) -> HistogramBuilder<'static, $ty> {
METER.$method(name)
}
};
}
wrap_method!(
f64_counter,
Counter<f64>,
"COUNTER",
"COUNTER.add(1.0, &[])"
);
wrap_method!(f64_gauge, Gauge<f64>, "GAUGE", "GAUGE.record(1.0, &[])");
wrap_method!(
f64_up_down_counter,
UpDownCounter<f64>,
"UP_DOWN_COUNTER",
"UP_DOWN_COUNTER.add(1.0, &[])"
);
wrap_method!(i64_gauge, Gauge<i64>, "GAUGE", "GAUGE.record(1, &[])");
wrap_method!(
i64_up_down_counter,
UpDownCounter<i64>,
"UP_DOWN_COUNTER",
"UP_DOWN_COUNTER.add(1, &[])"
);
wrap_method!(u64_counter, Counter<u64>, "COUNTER", "COUNTER.add(1, &[])");
wrap_method!(u64_gauge, Gauge<u64>, "GAUGE", "GAUGE.record(1, &[])");
wrap_histogram_method!(
f64_histogram,
Histogram<f64>,
"HISTOGRAM",
"HISTOGRAM.record(1.0, &[])"
);
wrap_histogram_method!(
u64_histogram,
Histogram<u64>,
"HISTOGRAM",
"HISTOGRAM.record(1, &[])"
);
#[doc = make_metric_doc!(f64_exponential_histogram, ExponentialHistogram<f64>, "HISTOGRAM", "HISTOGRAM.record(1.0, &[])", exponential_histogram)]
pub fn f64_exponential_histogram(
name: impl Into<Cow<'static, str>>,
scale: i8,
) -> ExponentialHistogramBuilder<'static, f64> {
let name = name.into();
ExponentialHistogramBuilder::new(name.clone(), scale, f64_histogram(name))
}
#[doc = make_metric_doc!(u64_exponential_histogram, ExponentialHistogram<u64>, "HISTOGRAM", "HISTOGRAM.record(1, &[])", exponential_histogram)]
pub fn u64_exponential_histogram(
name: impl Into<Cow<'static, str>>,
scale: i8,
) -> ExponentialHistogramBuilder<'static, u64> {
let name = name.into();
ExponentialHistogramBuilder::new(name.clone(), scale, u64_histogram(name))
}
#[rustfmt::skip]
macro_rules! make_observable_metric_doc {
($method: ident, $ty:ty, $var_name:literal, $callback:literal) => {
concat!(
"Wrapper for [`Meter::", stringify!($method), "`] using logfire's global meter.
# Examples
We recommend using this as a static variable, like so:
```rust
use std::sync::LazyLock;
static ", $var_name, ": LazyLock<opentelemetry::metrics::", stringify!($ty), "> = LazyLock::new(|| {
logfire::", stringify!($method), "(\"my_counter\")
.with_description(\"Just an example\")
.with_unit(\"s\")
.with_callback(", $callback, ")
.build()
});
fn main() -> Result<(), Box<dyn std::error::Error>> {
// ensure logfire is configured before accessing the metric for the first time
let shutdown_handler = logfire::configure()
# .send_to_logfire(logfire::config::SendToLogfire::IfTokenPresent)
.finish()?;
// initialize the metric
LazyLock::force(&", $var_name, ");
// allow some time for the metric to sample
std::thread::sleep(std::time::Duration::from_secs(1));
shutdown_handler.shutdown()?;
Ok(())
}
```
"
)
};
}
macro_rules! wrap_observable_method {
($method:ident, $ty:ty, $unit:ident, $var_name:literal, $usage:literal) => {
#[doc = make_observable_metric_doc!($method, $ty, $var_name, $usage)]
pub fn $method(
name: impl Into<Cow<'static, str>>,
) -> AsyncInstrumentBuilder<'static, $ty, $unit> {
METER.$method(name)
}
};
}
wrap_observable_method!(
f64_observable_counter,
ObservableCounter<f64>,
f64,
"COUNTER",
"|counter| counter.observe(1.0, &[])"
);
wrap_observable_method!(
f64_observable_gauge,
ObservableGauge<f64>,
f64,
"GAUGE",
"|gauge| gauge.observe(1.0, &[])"
);
wrap_observable_method!(
f64_observable_up_down_counter,
ObservableUpDownCounter<f64>,
f64,
"UP_DOWN_COUNTER",
"|counter| counter.observe(1.0, &[])"
);
wrap_observable_method!(
i64_observable_gauge,
ObservableGauge<i64>,
i64,
"GAUGE",
"|gauge| gauge.observe(1, &[])"
);
wrap_observable_method!(
i64_observable_up_down_counter,
ObservableUpDownCounter<i64>,
i64,
"UP_DOWN_COUNTER",
"|counter| counter.observe(1, &[])"
);
wrap_observable_method!(
u64_observable_counter,
ObservableCounter<u64>,
u64,
"COUNTER",
"|counter| counter.observe(1, &[])"
);
wrap_observable_method!(
u64_observable_gauge,
ObservableGauge<u64>,
u64,
"GAUGE",
"|gauge| gauge.observe(1, &[])"
);
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn exponential_histograms_are_registered_and_removed_on_drop() {
let a = f64_exponential_histogram("f64_exp", 10).build();
let b = u64_exponential_histogram("u64_exp", 20).build();
{
let histograms = EXPONENTIAL_HISTOGRAMS.read().unwrap();
assert!(histograms.contains_key("f64_exp"));
assert!(histograms.contains_key("u64_exp"));
}
drop(a);
drop(b);
{
let histograms = EXPONENTIAL_HISTOGRAMS.read().unwrap();
assert!(!histograms.contains_key("f64_exp"));
assert!(!histograms.contains_key("u64_exp"));
}
}
}