Skip to main content

otel_init/
otel.rs

1//! OpenTelemetry integration for tracing.
2//!
3//! This module provides utilities for initializing and configuring OpenTelemetry
4//! tracing and metrics in your application. It includes functions for:
5//!
6//! - Configuring resource attributes
7//! - Initializing tracer and meter providers
8use crate::macros::build_exporter;
9use anyhow::Result;
10use opentelemetry::global;
11use opentelemetry_sdk::{
12    Resource,
13    logs::SdkLoggerProvider,
14    metrics::{MeterProviderBuilder, PeriodicReader, SdkMeterProvider, Temporality},
15    propagation::TraceContextPropagator,
16    trace::{RandomIdGenerator, Sampler, SdkTracerProvider},
17};
18use std::{env::var, time::Duration};
19
20/// Global OTLP endpoint environment variable.
21const OTEL_EXPORTER_OTLP_ENDPOINT: &str = "OTEL_EXPORTER_OTLP_ENDPOINT";
22/// Environment variable for signal-specific traces protocol override.
23const OTEL_EXPORTER_OTLP_TRACES_PROTOCOL: &str = "OTEL_EXPORTER_OTLP_TRACES_PROTOCOL";
24/// Environment variable for signal-specific traces endpoint override.
25const OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: &str = "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT";
26/// Environment variable for signal-specific metrics protocol override.
27const OTEL_EXPORTER_OTLP_METRICS_PROTOCOL: &str = "OTEL_EXPORTER_OTLP_METRICS_PROTOCOL";
28/// Environment variable for signal-specific metrics endpoint override.
29const OTEL_EXPORTER_OTLP_METRICS_ENDPOINT: &str = "OTEL_EXPORTER_OTLP_METRICS_ENDPOINT";
30/// Environment variable for signal-specific logs protocol override.
31const OTEL_EXPORTER_OTLP_LOGS_PROTOCOL: &str = "OTEL_EXPORTER_OTLP_LOGS_PROTOCOL";
32/// Environment variable for signal-specific logs endpoint override.
33const OTEL_EXPORTER_OTLP_LOGS_ENDPOINT: &str = "OTEL_EXPORTER_OTLP_LOGS_ENDPOINT";
34
35/// Check whether an OTLP exporter should be enabled for a signal.
36///
37/// A signal-specific endpoint enables only that signal, while
38/// `OTEL_EXPORTER_OTLP_ENDPOINT` enables all signals. Empty or whitespace-only
39/// endpoint values are treated as unset.
40///
41/// # Arguments
42///
43/// * `endpoint_env` - The signal-specific endpoint environment variable to
44///   check.
45///
46/// # Returns
47///
48/// `true` if either the signal-specific endpoint or the global OTLP endpoint is
49/// configured with a non-empty value.
50fn exporter_enabled(endpoint_env: &str) -> bool {
51    endpoint_configured(endpoint_env) || endpoint_configured(OTEL_EXPORTER_OTLP_ENDPOINT)
52}
53
54/// Check whether an endpoint environment variable has a usable value.
55///
56/// Empty or whitespace-only values are treated as unset so templated
57/// deployments can leave endpoint variables empty without accidentally enabling
58/// OTLP exporters.
59///
60/// # Arguments
61///
62/// * `endpoint_env` - The endpoint environment variable to check.
63///
64/// # Returns
65///
66/// `true` if the environment variable exists and contains a non-empty value.
67fn endpoint_configured(endpoint_env: &str) -> bool {
68    var(endpoint_env)
69        .ok()
70        .is_some_and(|value| !value.trim().is_empty())
71}
72
73/// Build the span exporter based on the configured protocol.
74///
75/// # Environment
76///
77/// Resolution order:
78/// 1. `OTEL_EXPORTER_OTLP_TRACES_PROTOCOL`
79/// 2. `OTEL_EXPORTER_OTLP_PROTOCOL`
80/// 3. Falls back to gRPC
81///
82/// # Errors
83///
84/// Returns an error if the exporter cannot be built.
85fn build_span_exporter() -> Result<opentelemetry_otlp::SpanExporter> {
86    build_exporter!(
87        opentelemetry_otlp::SpanExporter::builder(),
88        OTEL_EXPORTER_OTLP_TRACES_PROTOCOL,
89        "Failed to build OTLP span exporter"
90    )
91}
92
93/// Build the metric exporter based on the configured protocol.
94///
95/// # Environment
96///
97/// Resolution order:
98/// 1. `OTEL_EXPORTER_OTLP_METRICS_PROTOCOL`
99/// 2. `OTEL_EXPORTER_OTLP_PROTOCOL`
100/// 3. Falls back to gRPC
101///
102/// # Errors
103///
104/// Returns an error if the exporter cannot be built.
105fn build_metric_exporter() -> Result<opentelemetry_otlp::MetricExporter> {
106    build_exporter!(
107        opentelemetry_otlp::MetricExporter::builder(),
108        OTEL_EXPORTER_OTLP_METRICS_PROTOCOL,
109        "Failed to build OTLP metric exporter",
110        |b| b.with_temporality(Temporality::default())
111    )
112}
113
114/// Build the log exporter based on the configured protocol.
115///
116/// # Environment
117///
118/// Resolution order:
119/// 1. `OTEL_EXPORTER_OTLP_LOGS_PROTOCOL`
120/// 2. `OTEL_EXPORTER_OTLP_PROTOCOL`
121/// 3. Falls back to gRPC
122///
123/// # Errors
124///
125/// Returns an error if the exporter cannot be built.
126fn build_log_exporter() -> Result<opentelemetry_otlp::LogExporter> {
127    build_exporter!(
128        opentelemetry_otlp::LogExporter::builder(),
129        OTEL_EXPORTER_OTLP_LOGS_PROTOCOL,
130        "Failed to build OTLP log exporter"
131    )
132}
133
134/// Initialize a tracer provider for OpenTelemetry tracing.
135///
136/// # Arguments
137///
138/// * `resource` - The OpenTelemetry resource to use.
139/// * `sample_ratio` - The ratio of traces to sample (0.0 to 1.0).
140///
141/// # Errors
142///
143/// Returns an error if the span exporter cannot be built. When no OTLP endpoint
144/// is configured, or the endpoint env var is empty, the provider is still
145/// initialized without an exporter.
146///
147/// # Examples
148///
149/// ```rust
150/// use otel_init::{get_resource, init_tracer_provider};
151/// use opentelemetry::KeyValue;
152///
153/// #[tokio::main]
154/// async fn main() -> anyhow::Result<()> {
155///     let resource = get_resource("my-service", &[]);
156///     let tracer_provider = init_tracer_provider(&resource, 1.0)?;
157///     Ok(())
158/// }
159/// ```
160pub fn init_tracer_provider(resource: &Resource, sample_ratio: f64) -> Result<SdkTracerProvider> {
161    global::set_text_map_propagator(TraceContextPropagator::new());
162
163    let builder = SdkTracerProvider::builder()
164        .with_sampler(Sampler::ParentBased(Box::new(Sampler::TraceIdRatioBased(
165            sample_ratio,
166        ))))
167        .with_id_generator(RandomIdGenerator::default())
168        .with_resource(resource.clone());
169
170    let tracer_provider = if exporter_enabled(OTEL_EXPORTER_OTLP_TRACES_ENDPOINT) {
171        let exporter = build_span_exporter()?;
172        builder.with_batch_exporter(exporter).build()
173    } else {
174        builder.build()
175    };
176
177    global::set_tracer_provider(tracer_provider.clone());
178
179    Ok(tracer_provider)
180}
181
182/// Initialize a meter provider for OpenTelemetry metrics.
183///
184/// # Arguments
185///
186/// * `resource` - The OpenTelemetry resource to use.
187/// * `metrics_interval_secs` - The interval in seconds between metric collections.
188///
189/// # Errors
190///
191/// Returns an error if the metric exporter cannot be built. When no OTLP
192/// endpoint is configured, or the endpoint env var is empty, the provider is
193/// still initialized without a periodic exporter.
194///
195/// # Examples
196///
197/// ```rust
198/// use otel_init::{get_resource, init_meter_provider};
199/// use opentelemetry::KeyValue;
200///
201/// #[tokio::main]
202/// async fn main() -> anyhow::Result<()> {
203///     let resource = get_resource("my-service", &[]);
204///     let meter_provider = init_meter_provider(&resource, 30)?;
205///     Ok(())
206/// }
207/// ```
208pub fn init_meter_provider(
209    resource: &Resource,
210    metrics_interval_secs: u64,
211) -> Result<SdkMeterProvider> {
212    let builder = MeterProviderBuilder::default().with_resource(resource.clone());
213
214    let meter_provider = if exporter_enabled(OTEL_EXPORTER_OTLP_METRICS_ENDPOINT) {
215        let exporter = build_metric_exporter()?;
216        let reader = PeriodicReader::builder(exporter)
217            .with_interval(Duration::from_secs(metrics_interval_secs))
218            .build();
219
220        builder.with_reader(reader).build()
221    } else {
222        builder.build()
223    };
224
225    global::set_meter_provider(meter_provider.clone());
226
227    Ok(meter_provider)
228}
229
230/// Initialize a logger provider for OpenTelemetry logs.
231///
232/// # Arguments
233///
234/// * `resource` - The OpenTelemetry resource to use.
235///
236/// # Errors
237///
238/// Returns an error if the log exporter cannot be built. When no OTLP endpoint
239/// is configured, or the endpoint env var is empty, the provider is still
240/// initialized without an exporter.
241///
242/// # Examples
243///
244/// ```rust
245/// use otel_init::{get_resource, init_logger_provider};
246/// use opentelemetry::KeyValue;
247///
248/// #[tokio::main]
249/// async fn main() -> anyhow::Result<()> {
250///     let resource = get_resource("my-service", &[]);
251///     let logger_provider = init_logger_provider(&resource)?;
252///     Ok(())
253/// }
254/// ```
255pub fn init_logger_provider(resource: &Resource) -> Result<SdkLoggerProvider> {
256    let builder = SdkLoggerProvider::builder().with_resource(resource.clone());
257
258    let logger_provider = if exporter_enabled(OTEL_EXPORTER_OTLP_LOGS_ENDPOINT) {
259        let exporter = build_log_exporter()?;
260        builder.with_batch_exporter(exporter).build()
261    } else {
262        builder.build()
263    };
264
265    Ok(logger_provider)
266}
267
268#[cfg(test)]
269mod tests {
270    use super::{
271        OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_EXPORTER_OTLP_LOGS_ENDPOINT,
272        OTEL_EXPORTER_OTLP_METRICS_ENDPOINT, OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, exporter_enabled,
273    };
274    use std::sync::Mutex;
275
276    static ENV_LOCK: Mutex<()> = Mutex::new(());
277
278    fn clear_endpoint_envs() {
279        unsafe {
280            std::env::remove_var(OTEL_EXPORTER_OTLP_ENDPOINT);
281            std::env::remove_var(OTEL_EXPORTER_OTLP_TRACES_ENDPOINT);
282            std::env::remove_var(OTEL_EXPORTER_OTLP_METRICS_ENDPOINT);
283            std::env::remove_var(OTEL_EXPORTER_OTLP_LOGS_ENDPOINT);
284        }
285    }
286
287    #[test]
288    fn exporter_is_disabled_without_global_or_signal_endpoint() {
289        let _guard = ENV_LOCK
290            .lock()
291            .expect("environment lock should not be poisoned");
292        clear_endpoint_envs();
293
294        assert!(!exporter_enabled(OTEL_EXPORTER_OTLP_TRACES_ENDPOINT));
295        assert!(!exporter_enabled(OTEL_EXPORTER_OTLP_METRICS_ENDPOINT));
296        assert!(!exporter_enabled(OTEL_EXPORTER_OTLP_LOGS_ENDPOINT));
297    }
298
299    #[test]
300    fn exporter_is_disabled_with_empty_endpoint() {
301        let _guard = ENV_LOCK
302            .lock()
303            .expect("environment lock should not be poisoned");
304        clear_endpoint_envs();
305        unsafe {
306            std::env::set_var(OTEL_EXPORTER_OTLP_ENDPOINT, " ");
307            std::env::set_var(OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, "");
308        }
309
310        assert!(!exporter_enabled(OTEL_EXPORTER_OTLP_TRACES_ENDPOINT));
311
312        clear_endpoint_envs();
313    }
314
315    #[test]
316    fn exporter_is_enabled_with_global_endpoint() {
317        let _guard = ENV_LOCK
318            .lock()
319            .expect("environment lock should not be poisoned");
320        clear_endpoint_envs();
321        unsafe {
322            std::env::set_var(OTEL_EXPORTER_OTLP_ENDPOINT, "http://localhost:4317");
323        }
324
325        assert!(exporter_enabled(OTEL_EXPORTER_OTLP_TRACES_ENDPOINT));
326        assert!(exporter_enabled(OTEL_EXPORTER_OTLP_METRICS_ENDPOINT));
327        assert!(exporter_enabled(OTEL_EXPORTER_OTLP_LOGS_ENDPOINT));
328
329        clear_endpoint_envs();
330    }
331
332    #[test]
333    fn exporter_is_enabled_with_signal_endpoint() {
334        let _guard = ENV_LOCK
335            .lock()
336            .expect("environment lock should not be poisoned");
337        clear_endpoint_envs();
338        unsafe {
339            std::env::set_var(
340                OTEL_EXPORTER_OTLP_TRACES_ENDPOINT,
341                "http://localhost:4318/v1/traces",
342            );
343        }
344
345        assert!(exporter_enabled(OTEL_EXPORTER_OTLP_TRACES_ENDPOINT));
346        assert!(!exporter_enabled(OTEL_EXPORTER_OTLP_METRICS_ENDPOINT));
347        assert!(!exporter_enabled(OTEL_EXPORTER_OTLP_LOGS_ENDPOINT));
348
349        clear_endpoint_envs();
350    }
351}