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, OnceLock};
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")]
142static TRACER_PROVIDER: OnceLock<SdkTracerProvider> = OnceLock::new();
143
144#[cfg(feature = "otel")]
147static METER_PROVIDER: OnceLock<opentelemetry_sdk::metrics::SdkMeterProvider> = OnceLock::new();
148
149#[cfg(feature = "otel")]
155pub fn init_tracing(
156 otel_cfg: &OpenTelemetryConfig,
157) -> anyhow::Result<
158 tracing_opentelemetry::OpenTelemetryLayer<
159 tracing_subscriber::Registry,
160 opentelemetry_sdk::trace::Tracer,
161 >,
162> {
163 let cfg = &otel_cfg.tracing;
164 if !cfg.enabled {
165 return Err(anyhow::anyhow!("tracing is disabled"));
166 }
167
168 global::set_text_map_propagator(TraceContextPropagator::new());
170
171 let resource = build_resource(&otel_cfg.resource);
173 let sampler = build_sampler(cfg);
174 let resolved_exporter = otel_cfg.tracing_exporter();
175 let (kind, endpoint, timeout) = extract_exporter_config(resolved_exporter);
176
177 tracing::info!(kind = ?kind, %endpoint, "OTLP exporter config");
178
179 let exporter = if matches!(kind, ExporterKind::OtlpHttp) {
181 build_http_exporter(resolved_exporter, endpoint, timeout)
182 } else {
183 build_grpc_exporter(resolved_exporter, endpoint, timeout)
184 }?;
185
186 let provider = SdkTracerProvider::builder()
188 .with_batch_exporter(exporter)
189 .with_sampler(sampler)
190 .with_resource(resource)
191 .build();
192
193 let service_name = otel_cfg.resource.service_name.clone();
195 let tracer = provider.tracer(service_name);
196 let otel_layer = tracing_opentelemetry::OpenTelemetryLayer::new(tracer);
197
198 INIT_TRACING.call_once(|| {
202 if TRACER_PROVIDER.set(provider.clone()).is_err() {
203 tracing::debug!("tracer provider handle already stored");
204 }
205 global::set_tracer_provider(provider);
206 });
207
208 tracing::info!("OpenTelemetry layer created successfully");
209 Ok(otel_layer)
210}
211
212#[cfg(feature = "otel")]
213pub(crate) fn build_headers_from_cfg_and_env(
214 exporter: Option<&crate::telemetry::config::Exporter>,
215) -> Option<std::collections::HashMap<String, String>> {
216 use std::collections::HashMap;
217 let mut out: HashMap<String, String> = HashMap::new();
218
219 if let Some(exp) = exporter
221 && let Some(hdrs) = &exp.headers
222 {
223 for (k, v) in hdrs {
224 out.insert(k.clone(), v.clone());
225 }
226 }
227
228 if let Ok(env_hdrs) = std::env::var("OTEL_EXPORTER_OTLP_HEADERS") {
230 for part in env_hdrs.split(',').map(str::trim).filter(|s| !s.is_empty()) {
231 if let Some((k, v)) = part.split_once('=') {
232 out.insert(k.trim().to_owned(), v.trim().to_owned());
233 }
234 }
235 }
236
237 if out.is_empty() { None } else { Some(out) }
238}
239
240#[cfg(feature = "otel")]
241pub(crate) fn extend_metadata_from_source<'a, I>(
242 md: &mut MetadataMap,
243 source: I,
244 context: &'static str,
245) where
246 I: Iterator<Item = (&'a str, &'a str)>,
247{
248 for (k, v) in source {
249 match MetadataKey::from_bytes(k.as_bytes()) {
250 Ok(key) => match MetadataValue::try_from(v) {
251 Ok(val) => {
252 md.insert(key, val);
253 }
254 Err(_) => {
255 tracing::warn!(header = %k, context, "Skipping invalid gRPC metadata value");
256 }
257 },
258 Err(_) => {
259 tracing::warn!(header = %k, context, "Skipping invalid gRPC metadata header name");
260 }
261 }
262 }
263}
264
265#[cfg(feature = "otel")]
266pub(crate) fn build_metadata_from_cfg_and_env(
267 exporter: Option<&crate::telemetry::config::Exporter>,
268) -> Option<MetadataMap> {
269 let mut md = MetadataMap::new();
270
271 if let Some(exp) = exporter
273 && let Some(hdrs) = &exp.headers
274 {
275 let iter = hdrs.iter().map(|(k, v)| (k.as_str(), v.as_str()));
276 extend_metadata_from_source(&mut md, iter, "config");
277 }
278
279 if let Ok(env_hdrs) = std::env::var("OTEL_EXPORTER_OTLP_HEADERS") {
281 let iter = env_hdrs.split(',').filter_map(|part| {
282 let part = part.trim();
283 if part.is_empty() {
284 None
285 } else {
286 part.split_once('=').map(|(k, v)| (k.trim(), v.trim()))
287 }
288 });
289 extend_metadata_from_source(&mut md, iter, "env");
290 }
291
292 if md.is_empty() { None } else { Some(md) }
293}
294
295#[cfg(feature = "otel")]
307pub fn shutdown_tracing() {
308 let Some(provider) = TRACER_PROVIDER.get() else {
309 tracing::debug!("Tracing shutdown: no provider installed, nothing to flush");
310 return;
311 };
312 match provider.shutdown() {
313 Ok(()) => tracing::info!("OpenTelemetry tracing flushed and shut down"),
314 Err(e) => tracing::warn!(error = %e, "OpenTelemetry tracing shutdown failed"),
315 }
316}
317
318#[cfg(not(feature = "otel"))]
319pub fn shutdown_tracing() {
320 tracing::info!("Tracing shutdown (no-op)");
321}
322
323#[cfg(feature = "otel")]
328pub fn shutdown_metrics() {
329 let Some(provider) = METER_PROVIDER.get() else {
330 tracing::debug!("Metrics shutdown: no provider installed, nothing to flush");
331 return;
332 };
333 match provider.shutdown() {
334 Ok(()) => tracing::info!("OpenTelemetry metrics flushed and shut down"),
335 Err(e) => tracing::warn!(error = %e, "OpenTelemetry metrics shutdown failed"),
336 }
337}
338
339#[cfg(not(feature = "otel"))]
340pub fn shutdown_metrics() {
341 tracing::info!("Metrics shutdown (no-op)");
342}
343
344#[cfg(feature = "otel")]
347static METRICS_INIT: std::sync::OnceLock<()> = std::sync::OnceLock::new();
348
349#[cfg(feature = "otel")]
367pub fn init_metrics_provider(otel_cfg: &OpenTelemetryConfig) -> anyhow::Result<()> {
368 if !otel_cfg.metrics.enabled {
369 tracing::info!(
372 "OpenTelemetry metrics disabled - global meter provider is \
373 the built-in NoopMeterProvider"
374 );
375 return Ok(());
376 }
377
378 if METRICS_INIT.get().is_some() {
379 return Ok(());
380 }
381
382 do_init_metrics_provider(otel_cfg)?;
383 let _set_result = METRICS_INIT.set(());
387 Ok(())
388}
389
390#[cfg(feature = "otel")]
391fn do_init_metrics_provider(otel_cfg: &OpenTelemetryConfig) -> anyhow::Result<()> {
392 let resolved_exporter = otel_cfg.metrics_exporter();
393
394 let (kind, endpoint, timeout) = extract_exporter_config(resolved_exporter);
395
396 let exporter = if matches!(kind, ExporterKind::OtlpHttp) {
398 let mut b = opentelemetry_otlp::MetricExporter::builder()
399 .with_http()
400 .with_protocol(Protocol::HttpBinary)
401 .with_endpoint(&endpoint);
402 if let Some(t) = timeout {
403 b = b.with_timeout(t);
404 }
405 if let Some(headers) = build_headers_from_cfg_and_env(resolved_exporter) {
406 b = b.with_headers(headers);
407 }
408 b.build().context("build OTLP HTTP metric exporter")?
409 } else {
410 let mut b = opentelemetry_otlp::MetricExporter::builder()
411 .with_tonic()
412 .with_endpoint(&endpoint);
413 if let Some(t) = timeout {
414 b = b.with_timeout(t);
415 }
416 if let Some(md) = build_metadata_from_cfg_and_env(resolved_exporter) {
417 b = b.with_metadata(md);
418 }
419 b.build().context("build OTLP gRPC metric exporter")?
420 };
421
422 let resource = build_resource(&otel_cfg.resource);
424
425 let mut builder = opentelemetry_sdk::metrics::SdkMeterProvider::builder()
427 .with_periodic_exporter(exporter)
428 .with_resource(resource);
429
430 if let Some(limit) = otel_cfg.metrics.cardinality_limit {
432 builder = builder.with_view(move |_: &opentelemetry_sdk::metrics::Instrument| {
433 opentelemetry_sdk::metrics::Stream::builder()
434 .with_cardinality_limit(limit)
435 .build()
436 .ok()
437 });
438 }
439
440 let provider = builder.build();
441
442 let installed = METER_PROVIDER.get_or_init(|| provider);
450 global::set_meter_provider(installed.clone());
451 tracing::info!("OpenTelemetry metrics initialized successfully");
452
453 Ok(())
454}
455
456#[cfg(not(feature = "otel"))]
461pub fn init_metrics_provider(_otel_cfg: &super::config::OpenTelemetryConfig) -> anyhow::Result<()> {
462 Err(anyhow::anyhow!("otel feature is disabled"))
463}
464
465#[cfg(feature = "otel")]
473pub fn otel_connectivity_probe(otel_cfg: &OpenTelemetryConfig) -> anyhow::Result<()> {
474 use opentelemetry::trace::{Span, Tracer as _};
475
476 let resolved_exporter = otel_cfg.tracing_exporter();
477 let (kind, endpoint, timeout) = extract_exporter_config(resolved_exporter);
478
479 let resource = build_resource(&otel_cfg.resource);
481
482 let exporter = if matches!(kind, ExporterKind::OtlpHttp) {
484 let mut b = opentelemetry_otlp::SpanExporter::builder()
485 .with_http()
486 .with_protocol(Protocol::HttpBinary)
487 .with_endpoint(endpoint);
488 if let Some(t) = timeout {
489 b = b.with_timeout(t);
490 }
491 if let Some(h) = build_headers_from_cfg_and_env(resolved_exporter) {
492 b = b.with_headers(h);
493 }
494 b.build()
495 .map_err(|e| anyhow::anyhow!("otlp http exporter build failed: {e}"))?
496 } else {
497 let mut b = opentelemetry_otlp::SpanExporter::builder()
498 .with_tonic()
499 .with_endpoint(endpoint);
500 if let Some(t) = timeout {
501 b = b.with_timeout(t);
502 }
503 if let Some(md) = build_metadata_from_cfg_and_env(resolved_exporter) {
504 b = b.with_metadata(md);
505 }
506 b.build()
507 .map_err(|e| anyhow::anyhow!("otlp grpc exporter build failed: {e}"))?
508 };
509
510 let provider = SdkTracerProvider::builder()
512 .with_simple_exporter(exporter)
513 .with_resource(resource)
514 .build();
515
516 let tracer = provider.tracer("connectivity_probe");
518 let mut span = tracer.start("otel_connectivity_probe");
519 span.end();
520
521 if let Err(e) = provider.force_flush() {
523 tracing::warn!(error = %e, "force_flush failed during OTLP connectivity probe");
524 }
525
526 provider
527 .shutdown()
528 .map_err(|e| anyhow::anyhow!("shutdown failed: {e}"))?;
529
530 tracing::info!(kind = ?kind, "OTLP connectivity probe exported a test span");
531 Ok(())
532}
533
534#[cfg(not(feature = "otel"))]
539pub fn otel_connectivity_probe(_cfg: &super::config::OpenTelemetryConfig) -> anyhow::Result<()> {
540 tracing::info!("OTLP connectivity probe skipped (otel feature disabled)");
541 Ok(())
542}
543
544#[cfg(test)]
547#[cfg_attr(coverage_nightly, coverage(off))]
548mod tests {
549 use super::*;
550 #[cfg(feature = "otel")]
551 use crate::telemetry::config::{
552 Exporter, ExporterKind, OpenTelemetryConfig, OpenTelemetryResource, Sampler, TracingConfig,
553 };
554 #[cfg(feature = "otel")]
555 use std::collections::{BTreeMap, HashMap};
556
557 #[cfg(feature = "otel")]
559 fn otel_with_tracing(tracing: TracingConfig) -> OpenTelemetryConfig {
560 OpenTelemetryConfig {
561 tracing,
562 ..Default::default()
563 }
564 }
565
566 #[test]
567 #[cfg(feature = "otel")]
568 fn test_init_tracing_disabled() {
569 let otel = otel_with_tracing(TracingConfig {
570 enabled: false,
571 ..Default::default()
572 });
573
574 let result = init_tracing(&otel);
575 assert!(result.is_err());
576 }
577
578 #[tokio::test]
579 #[cfg(feature = "otel")]
580 async fn test_init_tracing_enabled() {
581 let otel = otel_with_tracing(TracingConfig {
582 enabled: true,
583 ..Default::default()
584 });
585
586 let result = init_tracing(&otel);
587 assert!(result.is_ok());
588 }
589
590 #[test]
591 #[cfg(feature = "otel")]
592 fn test_init_tracing_with_resource_attributes() {
593 let rt = tokio::runtime::Runtime::new().unwrap();
594 let _guard = rt.enter();
595
596 let mut attrs = BTreeMap::new();
597 attrs.insert("service.version".to_owned(), "1.0.0".to_owned());
598 attrs.insert("deployment.environment".to_owned(), "test".to_owned());
599
600 let otel = OpenTelemetryConfig {
601 resource: OpenTelemetryResource {
602 service_name: "test-service".to_owned(),
603 attributes: attrs,
604 },
605 tracing: TracingConfig {
606 enabled: true,
607 ..Default::default()
608 },
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_always_on_sampler() {
619 let rt = tokio::runtime::Runtime::new().unwrap();
620 let _guard = rt.enter();
621
622 let otel = otel_with_tracing(TracingConfig {
623 enabled: true,
624 sampler: Some(Sampler::AlwaysOn {}),
625 ..Default::default()
626 });
627
628 let result = init_tracing(&otel);
629 assert!(result.is_ok());
630 }
631
632 #[test]
633 #[cfg(feature = "otel")]
634 fn test_init_tracing_with_always_off_sampler() {
635 let rt = tokio::runtime::Runtime::new().unwrap();
636 let _guard = rt.enter();
637
638 let otel = otel_with_tracing(TracingConfig {
639 enabled: true,
640 sampler: Some(Sampler::AlwaysOff {}),
641 ..Default::default()
642 });
643
644 let result = init_tracing(&otel);
645 assert!(result.is_ok());
646 }
647
648 #[test]
649 #[cfg(feature = "otel")]
650 fn test_init_tracing_with_ratio_sampler() {
651 let rt = tokio::runtime::Runtime::new().unwrap();
652 let _guard = rt.enter();
653
654 let otel = otel_with_tracing(TracingConfig {
655 enabled: true,
656 sampler: Some(Sampler::ParentBasedRatio { ratio: Some(0.5) }),
657 ..Default::default()
658 });
659
660 let result = init_tracing(&otel);
661 assert!(result.is_ok());
662 }
663
664 #[test]
665 #[cfg(feature = "otel")]
666 fn test_init_tracing_with_http_exporter() {
667 let _rt = tokio::runtime::Runtime::new().unwrap();
668
669 let otel = otel_with_tracing(TracingConfig {
670 enabled: true,
671 exporter: Some(Exporter {
672 kind: ExporterKind::OtlpHttp,
673 endpoint: Some("http://localhost:4318".to_owned()),
674 headers: None,
675 timeout_ms: Some(5000),
676 }),
677 ..Default::default()
678 });
679
680 let result = init_tracing(&otel);
681 assert!(result.is_ok());
682 }
683
684 #[test]
685 #[cfg(feature = "otel")]
686 fn test_init_tracing_with_grpc_exporter() {
687 let rt = tokio::runtime::Runtime::new().unwrap();
688 let _guard = rt.enter();
689
690 let otel = otel_with_tracing(TracingConfig {
691 enabled: true,
692 exporter: Some(Exporter {
693 kind: ExporterKind::OtlpGrpc,
694 endpoint: Some("http://localhost:4317".to_owned()),
695 headers: None,
696 timeout_ms: Some(5000),
697 }),
698 ..Default::default()
699 });
700
701 let result = init_tracing(&otel);
702 assert!(result.is_ok());
703 }
704
705 #[test]
706 #[cfg(feature = "otel")]
707 fn test_build_headers_from_cfg_empty() {
708 temp_env::with_var_unset("OTEL_EXPORTER_OTLP_HEADERS", || {
709 let cfg = TracingConfig {
710 enabled: true,
711 ..Default::default()
712 };
713
714 let result = build_headers_from_cfg_and_env(cfg.exporter.as_ref());
715 assert!(
716 result.is_none(),
717 "expected None when no headers configured and no env"
718 );
719 });
720 }
721
722 #[test]
723 #[cfg(feature = "otel")]
724 fn test_build_headers_from_cfg_with_headers() {
725 let mut headers = HashMap::new();
726 headers.insert("authorization".to_owned(), "Bearer token".to_owned());
727
728 let cfg = TracingConfig {
729 enabled: true,
730 exporter: Some(Exporter {
731 kind: ExporterKind::OtlpHttp,
732 endpoint: Some("http://localhost:4318".to_owned()),
733 headers: Some(headers.clone()),
734 timeout_ms: None,
735 }),
736 ..Default::default()
737 };
738
739 let result = build_headers_from_cfg_and_env(cfg.exporter.as_ref());
740 assert!(result.is_some());
741 let result_headers = result.unwrap();
742 assert_eq!(
743 result_headers.get("authorization"),
744 Some(&"Bearer token".to_owned())
745 );
746 }
747
748 #[test]
749 #[cfg(feature = "otel")]
750 fn test_build_metadata_from_cfg_empty() {
751 temp_env::with_var_unset("OTEL_EXPORTER_OTLP_HEADERS", || {
752 let cfg = TracingConfig {
753 enabled: true,
754 ..Default::default()
755 };
756
757 let result = build_metadata_from_cfg_and_env(cfg.exporter.as_ref());
758 assert!(
759 result.is_none(),
760 "expected None when no headers configured and no env"
761 );
762 });
763 }
764
765 #[test]
766 #[cfg(feature = "otel")]
767 fn test_build_metadata_from_cfg_with_headers() {
768 let mut headers = HashMap::new();
769 headers.insert("authorization".to_owned(), "Bearer token".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!(!metadata.is_empty());
786 }
787
788 #[test]
789 #[cfg(feature = "otel")]
790 fn test_build_metadata_multiple_headers() {
791 let mut headers = HashMap::new();
792 headers.insert("authorization".to_owned(), "Bearer token".to_owned());
793 headers.insert("x-custom-header".to_owned(), "custom-value".to_owned());
794
795 let cfg = TracingConfig {
796 enabled: true,
797 exporter: Some(Exporter {
798 kind: ExporterKind::OtlpGrpc,
799 endpoint: Some("http://localhost:4317".to_owned()),
800 headers: Some(headers.clone()),
801 timeout_ms: None,
802 }),
803 ..Default::default()
804 };
805
806 let result = build_metadata_from_cfg_and_env(cfg.exporter.as_ref());
807 assert!(result.is_some());
808 let metadata = result.unwrap();
809 assert_eq!(metadata.len(), 2);
810 }
811
812 #[test]
813 #[cfg(feature = "otel")]
814 fn test_build_metadata_invalid_header_name_skipped() {
815 let mut headers = HashMap::new();
816 headers.insert("valid-header".to_owned(), "value1".to_owned());
817 headers.insert("invalid header with spaces".to_owned(), "value2".to_owned());
818
819 let cfg = TracingConfig {
820 enabled: true,
821 exporter: Some(Exporter {
822 kind: ExporterKind::OtlpGrpc,
823 endpoint: Some("http://localhost:4317".to_owned()),
824 headers: Some(headers.clone()),
825 timeout_ms: None,
826 }),
827 ..Default::default()
828 };
829
830 let result = build_metadata_from_cfg_and_env(cfg.exporter.as_ref());
831 assert!(result.is_some());
832 let metadata = result.unwrap();
833 assert_eq!(metadata.len(), 1);
835 }
836
837 #[test]
838 fn test_shutdown_tracing_does_not_panic() {
839 shutdown_tracing();
841 }
842
843 #[test]
844 #[cfg(feature = "otel")]
845 fn test_init_metrics_provider_disabled() {
846 let otel = OpenTelemetryConfig {
847 metrics: crate::telemetry::config::MetricsConfig {
848 enabled: false,
849 ..Default::default()
850 },
851 ..Default::default()
852 };
853 let result = init_metrics_provider(&otel);
855 assert!(result.is_ok());
856 }
857}