endpoint_libs/libs/log/
otel.rs1use 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#[derive(Debug, Clone, Default)]
32pub struct OtelConfig {
33 pub enabled: bool,
35 pub service_name: Option<String>,
37 pub endpoint: Option<String>,
39 pub headers: HashMap<String, String>,
41}
42
43pub struct OtelGuards {
45 pub tracer_provider: SdkTracerProvider,
47 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
57pub struct OtelLayerResult {
59 pub guards: Option<OtelGuards>,
61 pub tracer: Option<Tracer>,
63}
64
65pub 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}