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