1#![cfg_attr(all(doc, not(doctest)), feature(doc_auto_cfg))]
10use std::{collections::BTreeMap as Map, str::FromStr as _};
11
12use config::{OtelConfig, OtelUrl, StdoutLogsConfig};
13use opentelemetry::{KeyValue, trace::TracerProvider as _};
14use opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge;
15use opentelemetry_otlp::{ExporterBuildError, LogExporter, SpanExporter, WithExportConfig as _};
16use opentelemetry_resource_detectors::{K8sResourceDetector, ProcessResourceDetector};
17use opentelemetry_sdk::{
18 Resource,
19 logs::SdkLoggerProvider,
20 metrics::{MeterProviderBuilder, PeriodicReader, SdkMeterProvider},
21 propagation::TraceContextPropagator,
22 trace::{RandomIdGenerator, SdkTracerProvider},
23};
24use opentelemetry_semantic_conventions::resource::SERVICE_VERSION;
25use tracing_opentelemetry::{MetricsLayer, OpenTelemetryLayer};
26use tracing_subscriber::{
27 EnvFilter, Layer, layer::SubscriberExt as _, util::SubscriberInitExt as _,
28};
29
30#[cfg(feature = "axum")]
31pub mod axum;
32pub mod config;
33pub mod reexport;
34#[cfg(feature = "reqwest-middleware")]
35pub mod reqwest_middleware;
36
37fn mk_resource(
39 service_name: &'static str,
40 version: &'static str,
41 resource_metadata: Map<String, String>,
42) -> Resource {
43 Resource::builder()
44 .with_attributes(
45 resource_metadata.into_iter().map(|(key, value)| KeyValue::new(key, value)),
46 )
47 .with_detector(Box::new(K8sResourceDetector {}))
48 .with_detector(Box::new(ProcessResourceDetector {}))
49 .with_attribute(KeyValue::new(SERVICE_VERSION, version))
50 .with_service_name(service_name)
51 .build()
52}
53
54fn init_traces(
56 endpoint: OtelUrl,
57 resource: Resource,
58) -> Result<SdkTracerProvider, ExporterBuildError> {
59 let exporter = SpanExporter::builder().with_tonic().with_endpoint(endpoint.url).build()?;
60 let tracer_provider = SdkTracerProvider::builder()
61 .with_id_generator(RandomIdGenerator::default())
62 .with_resource(resource)
63 .with_batch_exporter(exporter)
64 .build();
65
66 opentelemetry::global::set_tracer_provider(tracer_provider.clone());
67 Ok(tracer_provider)
68}
69
70fn init_metrics(
72 endpoint: OtelUrl,
73 resource: Resource,
74) -> Result<SdkMeterProvider, ExporterBuildError> {
75 let exporter = opentelemetry_otlp::MetricExporter::builder()
76 .with_tonic()
77 .with_endpoint(endpoint.url)
78 .with_temporality(opentelemetry_sdk::metrics::Temporality::default())
79 .build()?;
80
81 let reader = PeriodicReader::builder(exporter).build();
82
83 let meter_provider =
84 MeterProviderBuilder::default().with_resource(resource).with_reader(reader).build();
85
86 opentelemetry::global::set_meter_provider(meter_provider.clone());
87 Ok(meter_provider)
88}
89
90fn init_logs(
92 endpoint: OtelUrl,
93 resource: Resource,
94) -> Result<SdkLoggerProvider, ExporterBuildError> {
95 let exporter = LogExporter::builder().with_tonic().with_endpoint(endpoint.url).build()?;
96
97 Ok(SdkLoggerProvider::builder().with_resource(resource).with_batch_exporter(exporter).build())
98}
99
100#[macro_export]
114macro_rules! init_otel {
115 ($config:expr) => {
116 $crate::init_otel(
117 $config,
118 env!("CARGO_CRATE_NAME"),
119 env!("CARGO_PKG_NAME"),
120 env!("CARGO_PKG_VERSION"),
121 )
122 };
123}
124
125#[must_use = "The return is a guard for the providers and it need to be kept to properly shutdown them"]
144pub fn init_otel(
145 config: &OtelConfig,
146 main_crate: &'static str,
147 service_name: &'static str,
148 pkg_version: &'static str,
149) -> Result<ProvidersGuard, OtelInitError> {
150 opentelemetry::global::set_text_map_propagator(TraceContextPropagator::default());
151
152 let stdout_layer = config
153 .stdout
154 .as_ref()
155 .or(Some(&StdoutLogsConfig::default()))
156 .and_then(|stdout| stdout.enabled.then_some(stdout))
157 .map(|logger_config| {
158 let filter_fmt = EnvFilter::from_str(&logger_config.get_filter(main_crate))?;
159 let stdout_layer = tracing_subscriber::fmt::layer().with_thread_names(true);
160 Ok::<_, OtelInitError>(
161 if logger_config.json_output {
162 Box::new(stdout_layer.json()) as Box<dyn Layer<_> + Send + Sync>
163 } else {
164 Box::new(stdout_layer)
165 }
166 .with_filter(filter_fmt),
167 )
168 })
169 .transpose()?;
170
171 let exporter_with_resource = config.exporter.as_ref().map(|exporter| {
172 (exporter, mk_resource(service_name, pkg_version, exporter.resource_metadata.clone()))
173 });
174
175 let (logger_provider, logs_layer) = exporter_with_resource
176 .as_ref()
177 .and_then(|(exporter, resource)| {
178 exporter.logs.as_ref().and_then(|c| c.enabled.then_some(c)).map(|logger_config| {
179 let filter_otel = EnvFilter::from_str(&logger_config.get_filter(main_crate))?;
180 let logger_provider = init_logs(exporter.endpoint.clone(), resource.clone())?;
181
182 let logs_layer =
184 OpenTelemetryTracingBridge::new(&logger_provider).with_filter(filter_otel);
185
186 Ok::<_, OtelInitError>((Some(logger_provider), Some(logs_layer)))
187 })
188 })
189 .transpose()?
190 .unwrap_or((None, None));
191
192 let (tracer_provider, tracer_layer) = exporter_with_resource
193 .as_ref()
194 .and_then(|(exporter, resource)| {
195 exporter.traces.as_ref().and_then(|c| c.enabled.then_some(c)).map(|tracer_config| {
196 let trace_filter = EnvFilter::from_str(&tracer_config.get_filter(main_crate))?;
197 let tracer_provider = init_traces(exporter.endpoint.clone(), resource.clone())?;
198 let tracer = tracer_provider.tracer(service_name);
199 let tracer_layer = OpenTelemetryLayer::new(tracer).with_filter(trace_filter);
200 Ok::<_, OtelInitError>((Some(tracer_provider), Some(tracer_layer)))
201 })
202 })
203 .transpose()?
204 .unwrap_or((None, None));
205
206 let (meter_provider, meter_layer) = exporter_with_resource
207 .as_ref()
208 .and_then(|(exporter, resource)| {
209 exporter.metrics.as_ref().and_then(|c| c.enabled.then_some(c)).map(|meter_config| {
210 let metrics_filter = EnvFilter::from_str(&meter_config.get_filter(main_crate))?;
211 let meter_provider = init_metrics(exporter.endpoint.clone(), resource.clone())?;
212 let meter_layer =
213 MetricsLayer::new(meter_provider.clone()).with_filter(metrics_filter);
214
215 Ok::<_, OtelInitError>((Some(meter_provider), Some(meter_layer)))
216 })
217 })
218 .transpose()?
219 .unwrap_or((None, None));
220
221 let subscriber = tracing_subscriber::registry()
224 .with(logs_layer)
225 .with(stdout_layer)
226 .with(meter_layer)
227 .with(tracer_layer);
228
229 #[cfg(feature = "tracing-error")]
230 let subscriber = subscriber.with(tracing_error::ErrorLayer::default());
231
232 subscriber.init();
233
234 Ok(ProvidersGuard { logger_provider, tracer_provider, meter_provider })
235}
236
237#[derive(Debug)]
239pub struct ProvidersGuard {
240 logger_provider: Option<SdkLoggerProvider>,
242 tracer_provider: Option<SdkTracerProvider>,
244 meter_provider: Option<SdkMeterProvider>,
246}
247
248impl Drop for ProvidersGuard {
252 fn drop(&mut self) {
253 #[cfg(not(test))]
257 {
258 if let Some(logger_provider) = self.logger_provider.as_ref() {
259 let _ = logger_provider.shutdown().inspect_err(|err| {
260 tracing::error!("Could not shutdown LoggerProvider: {err}");
261 });
262 }
263 if let Some(tracer_provider) = self.tracer_provider.as_ref() {
264 let _ = tracer_provider.shutdown().inspect_err(|err| {
265 tracing::error!("Could not shutdown TracerProvider: {err}");
266 });
267 }
268 if let Some(meter_provider) = self.meter_provider.as_ref() {
269 let _ = meter_provider.shutdown().inspect_err(|err| {
270 tracing::error!("Could not shutdown MeterProvider: {err}");
271 });
272 }
273 }
274 }
275}
276
277#[allow(missing_docs)]
279#[derive(Debug, thiserror::Error)]
280pub enum OtelInitError {
281 #[error("Error building the exporter: {0}")]
282 BuildExporterError(#[from] ExporterBuildError),
283 #[error("Parsing EnvFilter directives error: {0}")]
284 EnvFilterError(#[from] tracing_subscriber::filter::ParseError),
285}
286
287#[cfg(test)]
288mod tests {
289 #![allow(clippy::expect_used)]
290 use super::{
291 config::{ExporterConfig, OtelConfig, ProviderConfig},
292 init_otel,
293 };
294 use crate::config::StdoutLogsConfig;
295
296 #[tokio::test]
297 async fn test_tracer_provider_enabled() {
298 let config = OtelConfig {
299 stdout: None,
300 exporter: Some(ExporterConfig {
301 traces: Some(ProviderConfig { enabled: true, ..Default::default() }),
302 ..Default::default()
303 }),
304 };
305 let guard = init_otel!(&config).expect("Error initializing Otel");
306 assert!(guard.tracer_provider.is_some());
307 }
308 #[tokio::test]
309 async fn test_tracer_provider_disabled() {
310 let config_enabled_false = OtelConfig {
311 stdout: None,
312 exporter: Some(ExporterConfig {
313 traces: Some(ProviderConfig { enabled: false, ..Default::default() }),
314 ..Default::default()
315 }),
316 };
317 let guard = init_otel!(&config_enabled_false).expect("Error initializing Otel");
318 assert!(guard.tracer_provider.is_none());
319 }
320
321 #[tokio::test]
327 async fn test_meter_provider_disabled() {
328 let config_enabled_false = OtelConfig {
329 stdout: None,
330 exporter: Some(ExporterConfig {
331 metrics: Some(ProviderConfig { enabled: false, ..Default::default() }),
332 ..Default::default()
333 }),
334 };
335 let guard = init_otel!(&config_enabled_false).expect("Error initializing Otel");
336 assert!(guard.meter_provider.is_none());
337 }
338 #[tokio::test]
339 async fn test_logger_provider_enabled() {
340 let config = OtelConfig {
341 stdout: None,
342 exporter: Some(ExporterConfig {
343 logs: Some(ProviderConfig { enabled: true, ..Default::default() }),
344 ..Default::default()
345 }),
346 };
347 let guard = init_otel!(&config).expect("Error initializing Otel");
348 assert!(guard.logger_provider.is_some());
349 }
350 #[tokio::test]
351 async fn test_logger_provider_disabled() {
352 let config_enabled_false = OtelConfig {
353 stdout: None,
354 exporter: Some(ExporterConfig {
355 logs: Some(ProviderConfig { enabled: false, ..Default::default() }),
356 ..Default::default()
357 }),
358 };
359 let guard = init_otel!(&config_enabled_false).expect("Error initializing Otel");
360 assert!(guard.logger_provider.is_none());
361 }
362
363 #[tokio::test]
364 async fn test_exporter_config_none() {
365 let config_none = OtelConfig {
366 stdout: Some(StdoutLogsConfig { enabled: true, ..Default::default() }),
367 exporter: Some(ExporterConfig::default()),
368 };
369 let guard = init_otel!(&config_none).expect("Error initializing Otel");
370 assert!(guard.meter_provider.is_none());
371 assert!(guard.tracer_provider.is_none());
372 assert!(guard.logger_provider.is_none());
373 }
374}