Skip to main content

endpoint_libs/libs/log/
otel.rs

1//! OpenTelemetry log and trace forwarding layer
2//!
3//! This module provides a separate tracing layer that forwards all log events and span lifecycle
4//! to an OpenTelemetry collector via OTLP. It operates independently from the existing stdout
5//! and file logging layers.
6//!
7//! # Configuration
8//! OTel can be configured via [`OtelConfig`] or through standard environment variables:
9//! - `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` - OTLP collector endpoint for traces
10//! - `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT` - OTLP collector endpoint for logs
11//! - `OTEL_SERVICE_NAME` - Service name for traces
12//! - `OTEL_EXPORTER_OTLP_HEADERS` - Additional headers (e.g., auth tokens)
13//!
14//! # Graceful Degradation
15//! If the OTel layer fails to initialize (e.g., invalid endpoint, network issues), a warning
16//! is logged and the application continues with stdout/file logging only.
17
18use std::collections::HashMap;
19
20use opentelemetry::propagation::{TextMapCompositePropagator, TextMapPropagator};
21use opentelemetry_otlp::WithHttpConfig;
22use opentelemetry_sdk::{
23    Resource,
24    logs::SdkLoggerProvider,
25    propagation::{BaggagePropagator, TraceContextPropagator},
26    trace::{SdkTracerProvider, Tracer},
27};
28use opentelemetry_semantic_conventions::resource::SERVICE_VERSION;
29
30/// Configuration for OpenTelemetry integration
31#[derive(Debug, Clone, Default)]
32pub struct OtelConfig {
33    /// Whether to enable OTel log/trace forwarding
34    pub enabled: bool,
35    /// Service name to identify this application
36    pub service_name: Option<String>,
37    /// OTLP collector endpoint (e.g., "http://localhost:4317")
38    pub endpoint: Option<String>,
39    /// Additional headers to include in OTLP requests (e.g., authentication)
40    pub headers: HashMap<String, String>,
41}
42
43/// Guards for OpenTelemetry providers to ensure traces and logs are flushed on drop
44pub struct OtelGuards {
45    /// The tracer provider guard
46    pub tracer_provider: SdkTracerProvider,
47    /// The logger provider guard
48    pub logger_provider: SdkLoggerProvider,
49}
50
51impl Drop for OtelGuards {
52    fn drop(&mut self) {
53        tracing::debug!(target: "otel::setup", "OTel layer shutting down - flushing pending traces and logs");
54    }
55}
56
57/// Result of building the OTel layer
58pub struct OtelLayerResult {
59    /// The guards that must be kept alive to ensure traces/logs are flushed on drop
60    pub guards: Option<OtelGuards>,
61    /// The tracer for OpenTelemetryLayer
62    pub tracer: Option<Tracer>,
63}
64
65/// Build OpenTelemetry tracer and logger providers for forwarding logs and spans
66pub fn build_otel_layer(config: &OtelConfig) -> OtelLayerResult {
67    if !config.enabled {
68        tracing::debug!(target: "otel::setup", "OTel layer disabled by config");
69        return OtelLayerResult {
70            guards: None,
71            tracer: None,
72        };
73    }
74
75    match build_otel_layer_inner(config) {
76        Ok(result) => {
77            tracing::debug!(
78                target: "otel::setup",
79                service_name = result.service_name,
80                endpoint = ?config.endpoint,
81                "OTel layer initialized successfully (Traces + Logs)"
82            );
83            tracing::debug!(
84                target: "otel::setup",
85                endpoint = config.endpoint.as_deref().unwrap_or("SDK default"),
86                header_keys = ?config.headers.keys().collect::<Vec<_>>(),
87                "OTel exporter config"
88            );
89            OtelLayerResult {
90                guards: Some(OtelGuards {
91                    tracer_provider: result.tracer_provider,
92                    logger_provider: result.logger_provider,
93                }),
94                tracer: Some(result.tracer),
95            }
96        }
97        Err(e) => {
98            tracing::warn!(
99                target: "otel::setup",
100                error = %e,
101                "Failed to initialize OTel layer - continuing without OTel forwarding"
102            );
103            OtelLayerResult {
104                guards: None,
105                tracer: None,
106            }
107        }
108    }
109}
110
111struct OtelLayerBuild {
112    tracer_provider: SdkTracerProvider,
113    logger_provider: SdkLoggerProvider,
114    tracer: Tracer,
115    service_name: String,
116}
117
118fn build_otel_layer_inner(
119    config: &OtelConfig,
120) -> Result<OtelLayerBuild, Box<dyn std::error::Error + Send + Sync>> {
121    let service_name = config
122        .service_name
123        .clone()
124        .or_else(|| std::env::var("OTEL_SERVICE_NAME").ok())
125        .unwrap_or_else(|| env!("CARGO_PKG_NAME").to_string());
126
127    let resource = Resource::builder()
128        .with_service_name(service_name.clone())
129        .with_attribute(opentelemetry::KeyValue::new(
130            SERVICE_VERSION,
131            env!("CARGO_PKG_VERSION"),
132        ))
133        .build();
134
135    let tracer_provider = build_tracer_provider(&resource, config)?;
136    let logger_provider = build_logger_provider(&resource, config)?;
137
138    init_propagator();
139
140    let tracer =
141        opentelemetry::trace::TracerProvider::tracer(&tracer_provider, service_name.clone());
142
143    Ok(OtelLayerBuild {
144        tracer_provider,
145        logger_provider,
146        tracer,
147        service_name,
148    })
149}
150
151fn build_tracer_provider(
152    resource: &Resource,
153    config: &OtelConfig,
154) -> Result<SdkTracerProvider, Box<dyn std::error::Error + Send + Sync>> {
155    use opentelemetry_otlp::{SpanExporter, WithExportConfig};
156
157    let endpoint = config
158        .endpoint
159        .clone()
160        .or_else(|| std::env::var("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT").ok());
161
162    tracing::debug!(
163        target: "otel::setup",
164        "Building traces exporter: endpoint={:?}, headers_count={}",
165        endpoint,
166        config.headers.len(),
167    );
168
169    let mut builder = SdkTracerProvider::builder().with_resource(resource.clone());
170
171    let mut exporter_builder = SpanExporter::builder().with_http();
172    if let Some(ref ep) = endpoint {
173        exporter_builder = exporter_builder.with_endpoint(ep);
174    }
175    if !config.headers.is_empty() {
176        exporter_builder = exporter_builder.with_headers(config.headers.clone());
177    }
178    builder = builder.with_batch_exporter(exporter_builder.build()?);
179
180    Ok(builder.build())
181}
182
183fn build_logger_provider(
184    resource: &Resource,
185    config: &OtelConfig,
186) -> Result<SdkLoggerProvider, Box<dyn std::error::Error + Send + Sync>> {
187    use opentelemetry_otlp::{LogExporter, WithExportConfig};
188
189    let endpoint = config
190        .endpoint
191        .clone()
192        .or_else(|| std::env::var("OTEL_EXPORTER_OTLP_LOGS_ENDPOINT").ok())
193        .or_else(|| config.endpoint.clone())
194        .or_else(|| std::env::var("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT").ok());
195
196    tracing::debug!(
197        target: "otel::setup",
198        "Building logs exporter: endpoint={:?}, headers_count={}",
199        endpoint,
200        config.headers.len(),
201    );
202
203    let mut builder = SdkLoggerProvider::builder().with_resource(resource.clone());
204
205    let mut exporter_builder = LogExporter::builder().with_http();
206    if let Some(ref ep) = endpoint {
207        exporter_builder = exporter_builder.with_endpoint(ep);
208    }
209    if !config.headers.is_empty() {
210        exporter_builder = exporter_builder.with_headers(config.headers.clone());
211    }
212    builder = builder.with_batch_exporter(exporter_builder.build()?);
213
214    Ok(builder.build())
215}
216
217fn init_propagator() {
218    let value =
219        std::env::var("OTEL_PROPAGATORS").unwrap_or_else(|_| "tracecontext,baggage".to_string());
220    let mut propagators: Vec<(Box<dyn TextMapPropagator + Send + Sync>, String)> = Vec::new();
221
222    for name in value.split(',').map(|s| s.trim().to_lowercase()) {
223        match name.as_str() {
224            "tracecontext" => propagators.push((Box::new(TraceContextPropagator::new()), name)),
225            "baggage" => propagators.push((Box::new(BaggagePropagator::new()), name)),
226            _ => {}
227        }
228    }
229
230    if !propagators.is_empty() {
231        let (propagators_impl, _): (Vec<_>, Vec<_>) = propagators.into_iter().unzip();
232        opentelemetry::global::set_text_map_propagator(TextMapCompositePropagator::new(
233            propagators_impl,
234        ));
235    }
236}