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