rust_telemetry/
lib.rs

1// SPDX-FileCopyrightText: 2025 Famedly GmbH (info@famedly.com)
2//
3// SPDX-License-Identifier: Apache-2.0
4
5//! OpenTelemetry initialization
6//!
7//! Lib containing the definitions and initializations of the OpenTelemetry
8//! tools
9#![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
37/// Crates a resource for the Otel providers
38fn 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
54/// Setup a Otel exporter and a provider for traces
55fn 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
70/// Setup a Otel exporter and a provider for metrics
71fn 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
90/// Setup a Otel exporter and a provider for logs
91fn 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/// Initializes the OpenTelemetry
101///
102/// example
103/// ```rust
104/// use rust_telemetry::{config::OtelConfig, init_otel};
105///
106/// #[tokio::main]
107/// async fn main() {
108/// 	let _guard = init_otel!(&OtelConfig::default());
109///
110/// 	// ...
111/// }
112/// ```
113#[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/// Initializes the OpenTelemetry
126///
127/// example
128/// ```rust
129/// use rust_telemetry::config;
130///
131/// #[tokio::main]
132/// async fn main() {
133/// 	let _guard = rust_telemetry::init_otel(
134/// 		&config::OtelConfig::default(),
135/// 		env!("CARGO_CRATE_NAME"),
136/// 		env!("CARGO_PKG_NAME"),
137/// 		env!("CARGO_PKG_VERSION"),
138/// 	);
139///
140/// 	// ...
141/// }
142/// ```
143#[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				// Create a new OpenTelemetryTracingBridge using the above LoggerProvider.
183				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	// Initialize the tracing subscriber with the stdout layer and
222	// layers for exporting over OpenTelemetry the logs, traces and metrics.
223	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/// Guarding object to make sure the providers are properly shutdown
238#[derive(Debug)]
239pub struct ProvidersGuard {
240	/// Logger provider
241	logger_provider: Option<SdkLoggerProvider>,
242	/// Tracer provider
243	tracer_provider: Option<SdkTracerProvider>,
244	/// Meter provider
245	meter_provider: Option<SdkMeterProvider>,
246}
247
248// Necessary to call TracerProvider::shutdown() on exit
249// due to a bug with flushing on global shutdown:
250// https://github.com/open-telemetry/opentelemetry-rust/issues/1961
251impl Drop for ProvidersGuard {
252	fn drop(&mut self) {
253		// This causes a hang in testing.
254		// Some relevant information:
255		// https://github.com/open-telemetry/opentelemetry-rust/issues/536
256		#[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/// OpenTelemetry setup errors
278#[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	// There seems to be a problem when testing the scenario when the meter
322	// provider is enable. The tests hangs when calling the shutdown from the
323	// PeriodicReader. For now we won't test this scenarios
324	//https://github.com/open-telemetry/opentelemetry-rust/issues/2056
325
326	#[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}