use anyhow::{Result, anyhow};
use rch_telemetry::metrics::Metrics;
use rch_telemetry::otlp::{OtelMetrics, OtlpConfig};
use std::sync::{LazyLock, RwLock};
use std::time::Instant;
static PROMETHEUS_METRICS: LazyLock<Result<Metrics>> = LazyLock::new(|| {
let metrics = Metrics::new()?;
metrics.register(&super::REGISTRY)?;
Ok(metrics)
});
static REQUEST_METRICS: RwLock<Option<Metrics>> = RwLock::new(None);
pub fn init_otel() -> Result<OtelGuard> {
let mut config = OtlpConfig::from_env();
if std::env::var("OTEL_SERVICE_NAME")
.ok()
.is_none_or(|name| name.trim().is_empty())
{
config.service_name = "rchd".to_string();
}
init_with_config(&config)
}
pub(crate) fn init_with_config(config: &OtlpConfig) -> Result<OtelGuard> {
let mut current = REQUEST_METRICS
.write()
.map_err(|_| anyhow!("daemon request metrics lock poisoned"))?;
if current.is_some() {
return Err(anyhow!("daemon request metrics already initialized"));
}
let metrics = PROMETHEUS_METRICS
.as_ref()
.map_err(|error| anyhow!("registering daemon request metrics: {error}"))?;
let (metrics, exporter) = configured_metrics(config, metrics.clone())?;
*current = Some(metrics);
Ok(OtelGuard { exporter })
}
fn configured_metrics(
config: &OtlpConfig,
metrics: Metrics,
) -> Result<(Metrics, Option<OtelMetrics>)> {
let exporter = OtelMetrics::from_config(config)?;
Ok((metrics.with_otel(exporter.clone()), exporter))
}
pub fn request_metrics() -> Option<Metrics> {
REQUEST_METRICS
.read()
.ok()
.and_then(|current| current.clone())
}
pub struct OtelGuard {
exporter: Option<OtelMetrics>,
}
impl OtelGuard {
pub fn otel_enabled(&self) -> bool {
self.exporter.is_some()
}
pub async fn shutdown(self) {
if let Ok(mut current) = REQUEST_METRICS.write() {
*current = None;
}
if let Some(exporter) = self.exporter
&& let Err(error) = tokio::task::spawn_blocking(move || exporter.shutdown()).await
{
tracing::warn!(%error, "OTLP metrics shutdown task failed");
}
}
}
pub struct RequestDuration {
metrics: Option<Metrics>,
started: Instant,
}
impl RequestDuration {
pub fn start(metrics: Option<Metrics>) -> Self {
Self {
metrics,
started: Instant::now(),
}
}
}
impl Drop for RequestDuration {
fn drop(&mut self) {
if let Some(metrics) = &self.metrics {
metrics.observe_request_duration("rchd_api", self.started.elapsed().as_secs_f64());
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use prometheus::Encoder;
use rch_common::test_guard;
#[test]
fn explicitly_disabled_exporter_preserves_prometheus_recording() {
let _guard = test_guard!();
let config = OtlpConfig {
enabled: false,
endpoint: Some("http://127.0.0.1:4317".to_string()),
service_name: "rchd".to_string(),
..OtlpConfig::default()
};
let registry = prometheus::Registry::new();
let metrics = Metrics::new().expect("metrics");
metrics.register(®istry).expect("register metrics");
let (metrics, exporter) = configured_metrics(&config, metrics).expect("disabled config");
assert!(
exporter.is_none(),
"disabled even with a configured endpoint"
);
assert!(!metrics.otel_enabled());
drop(RequestDuration::start(Some(metrics)));
let mut text = Vec::new();
prometheus::TextEncoder::new()
.encode(®istry.gather(), &mut text)
.expect("encode registered metrics");
assert!(
String::from_utf8(text)
.unwrap()
.contains("rch_request_duration_seconds_count{entrypoint=\"rchd_api\"} 1\n")
);
}
}