Skip to main content

beep_telemetry/
telemetry.rs

1//! OpenTelemetry setup for tracing and metrics used by the workspace.
2//!
3//! Public API
4//! - [`init`] — initialize telemetry and return an [`OtelGuard`]
5//! - [`OtelGuard::shutdown`] — gracefully shutdown providers and flush
6//!   buffered telemetry.
7//!
8
9use opentelemetry::{global, trace::TracerProvider as _, KeyValue};
10use opentelemetry_sdk::{
11    metrics::{MeterProviderBuilder, PeriodicReader, SdkMeterProvider},
12    trace::{RandomIdGenerator, Sampler, SdkTracerProvider},
13    Resource,
14};
15use opentelemetry_semantic_conventions::{
16    attribute::{DEPLOYMENT_ENVIRONMENT_NAME, SERVICE_VERSION},
17    SCHEMA_URL,
18};
19use tracing_core::Level;
20use tracing_opentelemetry::{MetricsLayer, OpenTelemetryLayer};
21use tracing_subscriber::prelude::__tracing_subscriber_SubscriberExt;
22use tracing_subscriber::util::SubscriberInitExt;
23
24use crate::domain::models::errors::TelemetryError;
25use crate::domain::models::config::Config;
26
27/// Build an OpenTelemetry `Resource` describing this service.
28fn resource() -> Resource {
29    Resource::builder()
30        .with_service_name(env!("CARGO_PKG_NAME"))
31        .with_schema_url(
32            [
33                KeyValue::new(SERVICE_VERSION, env!("CARGO_PKG_VERSION")),
34                KeyValue::new(DEPLOYMENT_ENVIRONMENT_NAME, "develop"),
35            ],
36            SCHEMA_URL,
37        )
38        .build()
39}
40
41/// Initialize and register a meter provider.
42fn init_meter_provider() -> Result<SdkMeterProvider, TelemetryError> {
43    let exporter = opentelemetry_otlp::MetricExporter::builder()
44        .with_tonic()
45        .with_temporality(opentelemetry_sdk::metrics::Temporality::default())
46        .build()
47        .map_err(|e| TelemetryError::OpenTelemetry(format!("failed to build OTLP metric exporter: {}", e)))?;
48
49    let reader = PeriodicReader::builder(exporter)
50        .with_interval(std::time::Duration::from_secs(30))
51        .build();
52
53    let stdout_reader =
54        PeriodicReader::builder(opentelemetry_stdout::MetricExporter::default()).build();
55
56    let meter_provider = MeterProviderBuilder::default()
57        .with_resource(resource())
58        .with_reader(reader)
59        .with_reader(stdout_reader)
60        .build();
61
62    global::set_meter_provider(meter_provider.clone());
63
64    Ok(meter_provider)
65}
66
67/// Initialize a tracer provider configured to export spans via OTLP.
68fn init_tracer_provider() -> Result<SdkTracerProvider, TelemetryError> {
69    let exporter = opentelemetry_otlp::SpanExporter::builder()
70        .with_tonic()
71        .build()
72        .map_err(|e| TelemetryError::OpenTelemetry(format!("failed to build OTLP span exporter: {}", e)))?;
73    Ok(SdkTracerProvider::builder()
74        .with_sampler(Sampler::ParentBased(Box::new(Sampler::TraceIdRatioBased(
75            1.0,
76        ))))
77        .with_id_generator(RandomIdGenerator::default())
78        .with_resource(resource())
79        .with_batch_exporter(exporter)
80        .build())
81}
82
83/// `tracing` subscriber init to forward traces and metrics to OpenTelemetry (OTLP) and logs to stdout.
84///
85/// - Initializes and configures an OpenTelemetry tracer provider (OTLP span exporter).
86/// - Initializes and configures an OpenTelemetry meter provider (OTLP metric exporter and
87///   a stdout metrics reader).
88/// - Builds a `tracing` subscriber registry
89///
90/// Return value
91/// - Success :[`OtelGuard`] owns the tracer and meter providers.
92///   Before shutting down the application call [`OtelGuard::shutdown`].
93///
94/// Parameters
95/// - void
96///
97/// Example
98/// ```rust
99/// # beep_telemetry::telemetry::{init_tracing_subscriber, OtelGuard};
100/// # beep_telemetry::domain::models::config::Config;
101/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
102/// let guard: OtelGuard = init_tracing_subscriber()?;
103///
104/// // Use `tracing` in the application:
105/// tracing::info!("application started");
106///
107/// // On shutdown, flush and shutdown the providers. Should be awaited.
108/// guard.shutdown().await;
109/// # Ok(())
110/// # }
111/// ```
112///
113fn init_tracing_subscriber() -> Result<OtelGuard, TelemetryError> {
114    let tracer_provider = init_tracer_provider()?;
115    let meter_provider = init_meter_provider()?;
116
117    let tracer = tracer_provider.tracer("tracing-otel-subscriber");
118
119    tracing_subscriber::registry()
120        .with(tracing_subscriber::filter::LevelFilter::from_level(
121            Level::INFO,
122        ))
123        .with(tracing_subscriber::fmt::layer())
124        .with(MetricsLayer::new(meter_provider.clone()))
125        .with(OpenTelemetryLayer::new(tracer))
126        .init();
127
128    Ok(OtelGuard {
129        tracer_provider,
130        meter_provider,
131    })
132}
133
134pub struct OtelGuard {
135    tracer_provider: SdkTracerProvider,
136    meter_provider: SdkMeterProvider,
137}
138
139impl OtelGuard {
140    /// Shutdown telemetry providers and flush any buffered telemetry.
141    pub async fn shutdown(self) {
142        let tracer_provider = self.tracer_provider;
143        let meter_provider = self.meter_provider;
144
145        let _ = tokio::task::spawn_blocking(move || {
146            if let Err(err) = tracer_provider.shutdown() {
147                eprintln!("tracer shutdown error: {err:?}");
148            }
149            if let Err(err) = meter_provider.shutdown() {
150                eprintln!("meter shutdown error: {err:?}");
151            }
152        })
153        .await;
154    }
155}
156
157/// Initialize telemetry for the application using the provided
158/// [`Config`].
159pub fn init(_config: &Config) -> Result<OtelGuard, TelemetryError> {
160    let guard = init_tracing_subscriber()?;
161
162    Ok(guard)
163}