1#[cfg(feature = "otel")]
7use anyhow::Context;
8#[cfg(feature = "otel")]
9use opentelemetry::{KeyValue, global, trace::TracerProvider as _};
10#[cfg(feature = "otel")]
11use std::sync::Once;
12
13#[cfg(feature = "otel")]
14use opentelemetry_otlp::{Protocol, WithExportConfig};
15#[cfg(feature = "otel")]
17use opentelemetry_otlp::{WithHttpConfig, WithTonicConfig};
18
19#[cfg(feature = "otel")]
20use opentelemetry_sdk::{
21 Resource,
22 propagation::TraceContextPropagator,
23 trace::{Sampler, SdkTracerProvider},
24};
25
26#[cfg(feature = "otel")]
27use super::config::{OpenTelemetryConfig, OpenTelemetryResource, TracingConfig};
28#[cfg(feature = "otel")]
29use crate::telemetry::config::ExporterKind;
30#[cfg(feature = "otel")]
31use tonic::metadata::{MetadataKey, MetadataMap, MetadataValue};
32
33#[cfg(feature = "otel")]
37pub(crate) fn build_resource(cfg: &OpenTelemetryResource) -> Resource {
38 tracing::debug!(
39 "Building OpenTelemetry resource for service: {}",
40 cfg.service_name
41 );
42 let mut attrs = vec![KeyValue::new("service.name", cfg.service_name.clone())];
43
44 for (k, v) in &cfg.attributes {
45 if k == "service.name" {
49 continue;
50 }
51 attrs.push(KeyValue::new(k.clone(), v.clone()));
52 }
53
54 Resource::builder_empty().with_attributes(attrs).build()
55}
56
57#[cfg(feature = "otel")]
59fn build_sampler(cfg: &TracingConfig) -> Sampler {
60 match cfg.sampler.as_ref() {
61 Some(crate::telemetry::config::Sampler::AlwaysOff { .. }) => Sampler::AlwaysOff,
62 Some(crate::telemetry::config::Sampler::AlwaysOn { .. }) => Sampler::AlwaysOn,
63 Some(crate::telemetry::config::Sampler::ParentBasedAlwaysOn { .. }) => {
64 Sampler::ParentBased(Box::new(Sampler::AlwaysOn))
65 }
66 Some(crate::telemetry::config::Sampler::ParentBasedRatio { ratio }) => {
67 let ratio = ratio.unwrap_or(0.1);
68 Sampler::ParentBased(Box::new(Sampler::TraceIdRatioBased(ratio)))
69 }
70 None => Sampler::ParentBased(Box::new(Sampler::AlwaysOn)),
71 }
72}
73
74#[cfg(feature = "otel")]
76pub(crate) fn extract_exporter_config(
77 exporter: Option<&crate::telemetry::config::Exporter>,
78) -> (ExporterKind, String, Option<std::time::Duration>) {
79 let kind = exporter.map_or(ExporterKind::OtlpGrpc, |e| e.kind);
80 let default_endpoint = match kind {
81 ExporterKind::OtlpHttp => "http://127.0.0.1:4318",
82 ExporterKind::OtlpGrpc => "http://127.0.0.1:4317",
83 };
84 let endpoint = exporter
85 .and_then(|e| e.endpoint.clone())
86 .unwrap_or_else(|| default_endpoint.into());
87
88 let timeout = exporter
89 .and_then(|e| e.timeout_ms)
90 .map(std::time::Duration::from_millis);
91
92 (kind, endpoint, timeout)
93}
94
95#[cfg(feature = "otel")]
97fn build_http_exporter(
98 exporter: Option<&crate::telemetry::config::Exporter>,
99 endpoint: String,
100 timeout: Option<std::time::Duration>,
101) -> anyhow::Result<opentelemetry_otlp::SpanExporter> {
102 let mut b = opentelemetry_otlp::SpanExporter::builder()
103 .with_http()
104 .with_protocol(Protocol::HttpBinary)
105 .with_endpoint(endpoint);
106 if let Some(t) = timeout {
107 b = b.with_timeout(t);
108 }
109 if let Some(hmap) = build_headers_from_cfg_and_env(exporter) {
110 b = b.with_headers(hmap);
111 }
112 b.build().context("build OTLP HTTP exporter")
113}
114
115#[cfg(feature = "otel")]
117fn build_grpc_exporter(
118 exporter: Option<&crate::telemetry::config::Exporter>,
119 endpoint: String,
120 timeout: Option<std::time::Duration>,
121) -> anyhow::Result<opentelemetry_otlp::SpanExporter> {
122 let mut b = opentelemetry_otlp::SpanExporter::builder()
123 .with_tonic()
124 .with_endpoint(endpoint);
125 if let Some(t) = timeout {
126 b = b.with_timeout(t);
127 }
128 if let Some(md) = build_metadata_from_cfg_and_env(exporter) {
129 b = b.with_metadata(md);
130 }
131 b.build().context("build OTLP gRPC exporter")
132}
133
134#[cfg(feature = "otel")]
135static INIT_TRACING: Once = Once::new();
136
137#[cfg(feature = "otel")]
143pub fn init_tracing(
144 otel_cfg: &OpenTelemetryConfig,
145) -> anyhow::Result<
146 tracing_opentelemetry::OpenTelemetryLayer<
147 tracing_subscriber::Registry,
148 opentelemetry_sdk::trace::Tracer,
149 >,
150> {
151 let cfg = &otel_cfg.tracing;
152 if !cfg.enabled {
153 return Err(anyhow::anyhow!("tracing is disabled"));
154 }
155
156 global::set_text_map_propagator(TraceContextPropagator::new());
158
159 let resource = build_resource(&otel_cfg.resource);
161 let sampler = build_sampler(cfg);
162 let resolved_exporter = otel_cfg.tracing_exporter();
163 let (kind, endpoint, timeout) = extract_exporter_config(resolved_exporter);
164
165 tracing::info!(kind = ?kind, %endpoint, "OTLP exporter config");
166
167 let exporter = if matches!(kind, ExporterKind::OtlpHttp) {
169 build_http_exporter(resolved_exporter, endpoint, timeout)
170 } else {
171 build_grpc_exporter(resolved_exporter, endpoint, timeout)
172 }?;
173
174 let provider = SdkTracerProvider::builder()
176 .with_batch_exporter(exporter)
177 .with_sampler(sampler)
178 .with_resource(resource)
179 .build();
180
181 let service_name = otel_cfg.resource.service_name.clone();
183 let tracer = provider.tracer(service_name);
184 let otel_layer = tracing_opentelemetry::OpenTelemetryLayer::new(tracer);
185
186 INIT_TRACING.call_once(|| {
188 global::set_tracer_provider(provider);
189 });
190
191 tracing::info!("OpenTelemetry layer created successfully");
192 Ok(otel_layer)
193}
194
195#[cfg(feature = "otel")]
196pub(crate) fn build_headers_from_cfg_and_env(
197 exporter: Option<&crate::telemetry::config::Exporter>,
198) -> Option<std::collections::HashMap<String, String>> {
199 use std::collections::HashMap;
200 let mut out: HashMap<String, String> = HashMap::new();
201
202 if let Some(exp) = exporter
204 && let Some(hdrs) = &exp.headers
205 {
206 for (k, v) in hdrs {
207 out.insert(k.clone(), v.clone());
208 }
209 }
210
211 if let Ok(env_hdrs) = std::env::var("OTEL_EXPORTER_OTLP_HEADERS") {
213 for part in env_hdrs.split(',').map(str::trim).filter(|s| !s.is_empty()) {
214 if let Some((k, v)) = part.split_once('=') {
215 out.insert(k.trim().to_owned(), v.trim().to_owned());
216 }
217 }
218 }
219
220 if out.is_empty() { None } else { Some(out) }
221}
222
223#[cfg(feature = "otel")]
224pub(crate) fn extend_metadata_from_source<'a, I>(
225 md: &mut MetadataMap,
226 source: I,
227 context: &'static str,
228) where
229 I: Iterator<Item = (&'a str, &'a str)>,
230{
231 for (k, v) in source {
232 match MetadataKey::from_bytes(k.as_bytes()) {
233 Ok(key) => match MetadataValue::try_from(v) {
234 Ok(val) => {
235 md.insert(key, val);
236 }
237 Err(_) => {
238 tracing::warn!(header = %k, context, "Skipping invalid gRPC metadata value");
239 }
240 },
241 Err(_) => {
242 tracing::warn!(header = %k, context, "Skipping invalid gRPC metadata header name");
243 }
244 }
245 }
246}
247
248#[cfg(feature = "otel")]
249pub(crate) fn build_metadata_from_cfg_and_env(
250 exporter: Option<&crate::telemetry::config::Exporter>,
251) -> Option<MetadataMap> {
252 let mut md = MetadataMap::new();
253
254 if let Some(exp) = exporter
256 && let Some(hdrs) = &exp.headers
257 {
258 let iter = hdrs.iter().map(|(k, v)| (k.as_str(), v.as_str()));
259 extend_metadata_from_source(&mut md, iter, "config");
260 }
261
262 if let Ok(env_hdrs) = std::env::var("OTEL_EXPORTER_OTLP_HEADERS") {
264 let iter = env_hdrs.split(',').filter_map(|part| {
265 let part = part.trim();
266 if part.is_empty() {
267 None
268 } else {
269 part.split_once('=').map(|(k, v)| (k.trim(), v.trim()))
270 }
271 });
272 extend_metadata_from_source(&mut md, iter, "env");
273 }
274
275 if md.is_empty() { None } else { Some(md) }
276}
277
278#[cfg(feature = "otel")]
285pub fn shutdown_tracing() {
286 tracing::info!("Tracing shutdown: no-op (keep a provider handle to call `shutdown()`).");
287}
288
289#[cfg(not(feature = "otel"))]
290pub fn shutdown_tracing() {
291 tracing::info!("Tracing shutdown (no-op)");
292}
293
294#[cfg(feature = "otel")]
299pub fn shutdown_metrics() {
300 tracing::info!("Metrics shutdown: no-op (keep a provider handle to call `shutdown()`).");
301}
302
303#[cfg(not(feature = "otel"))]
304pub fn shutdown_metrics() {
305 tracing::info!("Metrics shutdown (no-op)");
306}
307
308#[cfg(feature = "otel")]
311static METRICS_INIT: std::sync::OnceLock<()> = std::sync::OnceLock::new();
312
313#[cfg(feature = "otel")]
331pub fn init_metrics_provider(otel_cfg: &OpenTelemetryConfig) -> anyhow::Result<()> {
332 if !otel_cfg.metrics.enabled {
333 tracing::info!(
336 "OpenTelemetry metrics disabled - global meter provider is \
337 the built-in NoopMeterProvider"
338 );
339 return Ok(());
340 }
341
342 if METRICS_INIT.get().is_some() {
343 return Ok(());
344 }
345
346 do_init_metrics_provider(otel_cfg)?;
347 let _set_result = METRICS_INIT.set(());
351 Ok(())
352}
353
354#[cfg(feature = "otel")]
355fn do_init_metrics_provider(otel_cfg: &OpenTelemetryConfig) -> anyhow::Result<()> {
356 let resolved_exporter = otel_cfg.metrics_exporter();
357
358 let (kind, endpoint, timeout) = extract_exporter_config(resolved_exporter);
359
360 let exporter = if matches!(kind, ExporterKind::OtlpHttp) {
362 let mut b = opentelemetry_otlp::MetricExporter::builder()
363 .with_http()
364 .with_protocol(Protocol::HttpBinary)
365 .with_endpoint(&endpoint);
366 if let Some(t) = timeout {
367 b = b.with_timeout(t);
368 }
369 if let Some(headers) = build_headers_from_cfg_and_env(resolved_exporter) {
370 b = b.with_headers(headers);
371 }
372 b.build().context("build OTLP HTTP metric exporter")?
373 } else {
374 let mut b = opentelemetry_otlp::MetricExporter::builder()
375 .with_tonic()
376 .with_endpoint(&endpoint);
377 if let Some(t) = timeout {
378 b = b.with_timeout(t);
379 }
380 if let Some(md) = build_metadata_from_cfg_and_env(resolved_exporter) {
381 b = b.with_metadata(md);
382 }
383 b.build().context("build OTLP gRPC metric exporter")?
384 };
385
386 let resource = build_resource(&otel_cfg.resource);
388
389 let mut builder = opentelemetry_sdk::metrics::SdkMeterProvider::builder()
391 .with_periodic_exporter(exporter)
392 .with_resource(resource);
393
394 if let Some(limit) = otel_cfg.metrics.cardinality_limit {
396 builder = builder.with_view(move |_: &opentelemetry_sdk::metrics::Instrument| {
397 opentelemetry_sdk::metrics::Stream::builder()
398 .with_cardinality_limit(limit)
399 .build()
400 .ok()
401 });
402 }
403
404 let provider = builder.build();
405
406 global::set_meter_provider(provider);
407 tracing::info!("OpenTelemetry metrics initialized successfully");
408
409 Ok(())
410}
411
412#[cfg(not(feature = "otel"))]
417pub fn init_metrics_provider(_otel_cfg: &super::config::OpenTelemetryConfig) -> anyhow::Result<()> {
418 Err(anyhow::anyhow!("otel feature is disabled"))
419}
420
421#[cfg(feature = "otel")]
429pub fn otel_connectivity_probe(otel_cfg: &OpenTelemetryConfig) -> anyhow::Result<()> {
430 use opentelemetry::trace::{Span, Tracer as _};
431
432 let resolved_exporter = otel_cfg.tracing_exporter();
433 let (kind, endpoint, timeout) = extract_exporter_config(resolved_exporter);
434
435 let resource = build_resource(&otel_cfg.resource);
437
438 let exporter = if matches!(kind, ExporterKind::OtlpHttp) {
440 let mut b = opentelemetry_otlp::SpanExporter::builder()
441 .with_http()
442 .with_protocol(Protocol::HttpBinary)
443 .with_endpoint(endpoint);
444 if let Some(t) = timeout {
445 b = b.with_timeout(t);
446 }
447 if let Some(h) = build_headers_from_cfg_and_env(resolved_exporter) {
448 b = b.with_headers(h);
449 }
450 b.build()
451 .map_err(|e| anyhow::anyhow!("otlp http exporter build failed: {e}"))?
452 } else {
453 let mut b = opentelemetry_otlp::SpanExporter::builder()
454 .with_tonic()
455 .with_endpoint(endpoint);
456 if let Some(t) = timeout {
457 b = b.with_timeout(t);
458 }
459 if let Some(md) = build_metadata_from_cfg_and_env(resolved_exporter) {
460 b = b.with_metadata(md);
461 }
462 b.build()
463 .map_err(|e| anyhow::anyhow!("otlp grpc exporter build failed: {e}"))?
464 };
465
466 let provider = SdkTracerProvider::builder()
468 .with_simple_exporter(exporter)
469 .with_resource(resource)
470 .build();
471
472 let tracer = provider.tracer("connectivity_probe");
474 let mut span = tracer.start("otel_connectivity_probe");
475 span.end();
476
477 if let Err(e) = provider.force_flush() {
479 tracing::warn!(error = %e, "force_flush failed during OTLP connectivity probe");
480 }
481
482 provider
483 .shutdown()
484 .map_err(|e| anyhow::anyhow!("shutdown failed: {e}"))?;
485
486 tracing::info!(kind = ?kind, "OTLP connectivity probe exported a test span");
487 Ok(())
488}
489
490#[cfg(not(feature = "otel"))]
495pub fn otel_connectivity_probe(_cfg: &super::config::OpenTelemetryConfig) -> anyhow::Result<()> {
496 tracing::info!("OTLP connectivity probe skipped (otel feature disabled)");
497 Ok(())
498}
499
500#[cfg(test)]
503#[cfg_attr(coverage_nightly, coverage(off))]
504mod tests {
505 use super::*;
506 #[cfg(feature = "otel")]
507 use crate::telemetry::config::{
508 Exporter, ExporterKind, OpenTelemetryConfig, OpenTelemetryResource, Sampler, TracingConfig,
509 };
510 #[cfg(feature = "otel")]
511 use std::collections::{BTreeMap, HashMap};
512
513 #[cfg(feature = "otel")]
515 fn otel_with_tracing(tracing: TracingConfig) -> OpenTelemetryConfig {
516 OpenTelemetryConfig {
517 tracing,
518 ..Default::default()
519 }
520 }
521
522 #[test]
523 #[cfg(feature = "otel")]
524 fn test_init_tracing_disabled() {
525 let otel = otel_with_tracing(TracingConfig {
526 enabled: false,
527 ..Default::default()
528 });
529
530 let result = init_tracing(&otel);
531 assert!(result.is_err());
532 }
533
534 #[tokio::test]
535 #[cfg(feature = "otel")]
536 async fn test_init_tracing_enabled() {
537 let otel = otel_with_tracing(TracingConfig {
538 enabled: true,
539 ..Default::default()
540 });
541
542 let result = init_tracing(&otel);
543 assert!(result.is_ok());
544 }
545
546 #[test]
547 #[cfg(feature = "otel")]
548 fn test_init_tracing_with_resource_attributes() {
549 let rt = tokio::runtime::Runtime::new().unwrap();
550 let _guard = rt.enter();
551
552 let mut attrs = BTreeMap::new();
553 attrs.insert("service.version".to_owned(), "1.0.0".to_owned());
554 attrs.insert("deployment.environment".to_owned(), "test".to_owned());
555
556 let otel = OpenTelemetryConfig {
557 resource: OpenTelemetryResource {
558 service_name: "test-service".to_owned(),
559 attributes: attrs,
560 },
561 tracing: TracingConfig {
562 enabled: true,
563 ..Default::default()
564 },
565 ..Default::default()
566 };
567
568 let result = init_tracing(&otel);
569 assert!(result.is_ok());
570 }
571
572 #[test]
573 #[cfg(feature = "otel")]
574 fn test_init_tracing_with_always_on_sampler() {
575 let rt = tokio::runtime::Runtime::new().unwrap();
576 let _guard = rt.enter();
577
578 let otel = otel_with_tracing(TracingConfig {
579 enabled: true,
580 sampler: Some(Sampler::AlwaysOn {}),
581 ..Default::default()
582 });
583
584 let result = init_tracing(&otel);
585 assert!(result.is_ok());
586 }
587
588 #[test]
589 #[cfg(feature = "otel")]
590 fn test_init_tracing_with_always_off_sampler() {
591 let rt = tokio::runtime::Runtime::new().unwrap();
592 let _guard = rt.enter();
593
594 let otel = otel_with_tracing(TracingConfig {
595 enabled: true,
596 sampler: Some(Sampler::AlwaysOff {}),
597 ..Default::default()
598 });
599
600 let result = init_tracing(&otel);
601 assert!(result.is_ok());
602 }
603
604 #[test]
605 #[cfg(feature = "otel")]
606 fn test_init_tracing_with_ratio_sampler() {
607 let rt = tokio::runtime::Runtime::new().unwrap();
608 let _guard = rt.enter();
609
610 let otel = otel_with_tracing(TracingConfig {
611 enabled: true,
612 sampler: Some(Sampler::ParentBasedRatio { ratio: Some(0.5) }),
613 ..Default::default()
614 });
615
616 let result = init_tracing(&otel);
617 assert!(result.is_ok());
618 }
619
620 #[test]
621 #[cfg(feature = "otel")]
622 fn test_init_tracing_with_http_exporter() {
623 let _rt = tokio::runtime::Runtime::new().unwrap();
624
625 let otel = otel_with_tracing(TracingConfig {
626 enabled: true,
627 exporter: Some(Exporter {
628 kind: ExporterKind::OtlpHttp,
629 endpoint: Some("http://localhost:4318".to_owned()),
630 headers: None,
631 timeout_ms: Some(5000),
632 }),
633 ..Default::default()
634 });
635
636 let result = init_tracing(&otel);
637 assert!(result.is_ok());
638 }
639
640 #[test]
641 #[cfg(feature = "otel")]
642 fn test_init_tracing_with_grpc_exporter() {
643 let rt = tokio::runtime::Runtime::new().unwrap();
644 let _guard = rt.enter();
645
646 let otel = otel_with_tracing(TracingConfig {
647 enabled: true,
648 exporter: Some(Exporter {
649 kind: ExporterKind::OtlpGrpc,
650 endpoint: Some("http://localhost:4317".to_owned()),
651 headers: None,
652 timeout_ms: Some(5000),
653 }),
654 ..Default::default()
655 });
656
657 let result = init_tracing(&otel);
658 assert!(result.is_ok());
659 }
660
661 #[test]
662 #[cfg(feature = "otel")]
663 fn test_build_headers_from_cfg_empty() {
664 temp_env::with_var_unset("OTEL_EXPORTER_OTLP_HEADERS", || {
665 let cfg = TracingConfig {
666 enabled: true,
667 ..Default::default()
668 };
669
670 let result = build_headers_from_cfg_and_env(cfg.exporter.as_ref());
671 assert!(
672 result.is_none(),
673 "expected None when no headers configured and no env"
674 );
675 });
676 }
677
678 #[test]
679 #[cfg(feature = "otel")]
680 fn test_build_headers_from_cfg_with_headers() {
681 let mut headers = HashMap::new();
682 headers.insert("authorization".to_owned(), "Bearer token".to_owned());
683
684 let cfg = TracingConfig {
685 enabled: true,
686 exporter: Some(Exporter {
687 kind: ExporterKind::OtlpHttp,
688 endpoint: Some("http://localhost:4318".to_owned()),
689 headers: Some(headers.clone()),
690 timeout_ms: None,
691 }),
692 ..Default::default()
693 };
694
695 let result = build_headers_from_cfg_and_env(cfg.exporter.as_ref());
696 assert!(result.is_some());
697 let result_headers = result.unwrap();
698 assert_eq!(
699 result_headers.get("authorization"),
700 Some(&"Bearer token".to_owned())
701 );
702 }
703
704 #[test]
705 #[cfg(feature = "otel")]
706 fn test_build_metadata_from_cfg_empty() {
707 temp_env::with_var_unset("OTEL_EXPORTER_OTLP_HEADERS", || {
708 let cfg = TracingConfig {
709 enabled: true,
710 ..Default::default()
711 };
712
713 let result = build_metadata_from_cfg_and_env(cfg.exporter.as_ref());
714 assert!(
715 result.is_none(),
716 "expected None when no headers configured and no env"
717 );
718 });
719 }
720
721 #[test]
722 #[cfg(feature = "otel")]
723 fn test_build_metadata_from_cfg_with_headers() {
724 let mut headers = HashMap::new();
725 headers.insert("authorization".to_owned(), "Bearer token".to_owned());
726
727 let cfg = TracingConfig {
728 enabled: true,
729 exporter: Some(Exporter {
730 kind: ExporterKind::OtlpGrpc,
731 endpoint: Some("http://localhost:4317".to_owned()),
732 headers: Some(headers.clone()),
733 timeout_ms: None,
734 }),
735 ..Default::default()
736 };
737
738 let result = build_metadata_from_cfg_and_env(cfg.exporter.as_ref());
739 assert!(result.is_some());
740 let metadata = result.unwrap();
741 assert!(!metadata.is_empty());
742 }
743
744 #[test]
745 #[cfg(feature = "otel")]
746 fn test_build_metadata_multiple_headers() {
747 let mut headers = HashMap::new();
748 headers.insert("authorization".to_owned(), "Bearer token".to_owned());
749 headers.insert("x-custom-header".to_owned(), "custom-value".to_owned());
750
751 let cfg = TracingConfig {
752 enabled: true,
753 exporter: Some(Exporter {
754 kind: ExporterKind::OtlpGrpc,
755 endpoint: Some("http://localhost:4317".to_owned()),
756 headers: Some(headers.clone()),
757 timeout_ms: None,
758 }),
759 ..Default::default()
760 };
761
762 let result = build_metadata_from_cfg_and_env(cfg.exporter.as_ref());
763 assert!(result.is_some());
764 let metadata = result.unwrap();
765 assert_eq!(metadata.len(), 2);
766 }
767
768 #[test]
769 #[cfg(feature = "otel")]
770 fn test_build_metadata_invalid_header_name_skipped() {
771 let mut headers = HashMap::new();
772 headers.insert("valid-header".to_owned(), "value1".to_owned());
773 headers.insert("invalid header with spaces".to_owned(), "value2".to_owned());
774
775 let cfg = TracingConfig {
776 enabled: true,
777 exporter: Some(Exporter {
778 kind: ExporterKind::OtlpGrpc,
779 endpoint: Some("http://localhost:4317".to_owned()),
780 headers: Some(headers.clone()),
781 timeout_ms: None,
782 }),
783 ..Default::default()
784 };
785
786 let result = build_metadata_from_cfg_and_env(cfg.exporter.as_ref());
787 assert!(result.is_some());
788 let metadata = result.unwrap();
789 assert_eq!(metadata.len(), 1);
791 }
792
793 #[test]
794 fn test_shutdown_tracing_does_not_panic() {
795 shutdown_tracing();
797 }
798
799 #[test]
800 #[cfg(feature = "otel")]
801 fn test_init_metrics_provider_disabled() {
802 let otel = OpenTelemetryConfig {
803 metrics: crate::telemetry::config::MetricsConfig {
804 enabled: false,
805 ..Default::default()
806 },
807 ..Default::default()
808 };
809 let result = init_metrics_provider(&otel);
811 assert!(result.is_ok());
812 }
813}