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(feature = "otel")]
194pub(crate) fn build_headers_from_cfg_and_env(
195 exporter: Option<&crate::telemetry::config::Exporter>,
196) -> Option<std::collections::HashMap<String, String>> {
197 use std::collections::HashMap;
198 let mut out: HashMap<String, String> = HashMap::new();
199
200 if let Some(exp) = exporter
202 && let Some(hdrs) = &exp.headers
203 {
204 for (k, v) in hdrs {
205 out.insert(k.clone(), v.clone());
206 }
207 }
208
209 if let Ok(env_hdrs) = std::env::var("OTEL_EXPORTER_OTLP_HEADERS") {
211 for part in env_hdrs.split(',').map(str::trim).filter(|s| !s.is_empty()) {
212 if let Some((k, v)) = part.split_once('=') {
213 out.insert(k.trim().to_owned(), v.trim().to_owned());
214 }
215 }
216 }
217
218 if out.is_empty() { None } else { Some(out) }
219}
220
221#[cfg(feature = "otel")]
222pub(crate) fn extend_metadata_from_source<'a, I>(
223 md: &mut MetadataMap,
224 source: I,
225 context: &'static str,
226) where
227 I: Iterator<Item = (&'a str, &'a str)>,
228{
229 for (k, v) in source {
230 match MetadataKey::from_bytes(k.as_bytes()) {
231 Ok(key) => match MetadataValue::try_from(v) {
232 Ok(val) => {
233 md.insert(key, val);
234 }
235 Err(_) => {
236 tracing::warn!(header = %k, context, "Skipping invalid gRPC metadata value");
237 }
238 },
239 Err(_) => {
240 tracing::warn!(header = %k, context, "Skipping invalid gRPC metadata header name");
241 }
242 }
243 }
244}
245
246#[cfg(feature = "otel")]
247pub(crate) fn build_metadata_from_cfg_and_env(
248 exporter: Option<&crate::telemetry::config::Exporter>,
249) -> Option<MetadataMap> {
250 let mut md = MetadataMap::new();
251
252 if let Some(exp) = exporter
254 && let Some(hdrs) = &exp.headers
255 {
256 let iter = hdrs.iter().map(|(k, v)| (k.as_str(), v.as_str()));
257 extend_metadata_from_source(&mut md, iter, "config");
258 }
259
260 if let Ok(env_hdrs) = std::env::var("OTEL_EXPORTER_OTLP_HEADERS") {
262 let iter = env_hdrs.split(',').filter_map(|part| {
263 let part = part.trim();
264 if part.is_empty() {
265 None
266 } else {
267 part.split_once('=').map(|(k, v)| (k.trim(), v.trim()))
268 }
269 });
270 extend_metadata_from_source(&mut md, iter, "env");
271 }
272
273 if md.is_empty() { None } else { Some(md) }
274}
275
276#[cfg(feature = "otel")]
283pub fn shutdown_tracing() {
284 tracing::info!("Tracing shutdown: no-op (keep a provider handle to call `shutdown()`).");
285}
286
287#[cfg(not(feature = "otel"))]
288pub fn shutdown_tracing() {
289 tracing::info!("Tracing shutdown (no-op)");
290}
291
292#[cfg(feature = "otel")]
297pub fn shutdown_metrics() {
298 tracing::info!("Metrics shutdown: no-op (keep a provider handle to call `shutdown()`).");
299}
300
301#[cfg(not(feature = "otel"))]
302pub fn shutdown_metrics() {
303 tracing::info!("Metrics shutdown (no-op)");
304}
305
306#[cfg(feature = "otel")]
309static METRICS_INIT: std::sync::OnceLock<Result<(), String>> = std::sync::OnceLock::new();
310
311#[cfg(feature = "otel")]
328pub fn init_metrics_provider(otel_cfg: &OpenTelemetryConfig) -> anyhow::Result<()> {
329 if !otel_cfg.metrics.enabled {
330 tracing::info!(
333 "OpenTelemetry metrics disabled - global meter provider is \
334 the built-in NoopMeterProvider"
335 );
336 return Ok(());
337 }
338
339 METRICS_INIT
340 .get_or_init(|| do_init_metrics_provider(otel_cfg).map_err(|e| e.to_string()))
341 .clone()
342 .map_err(|e| anyhow::anyhow!("{e}"))
343}
344
345#[cfg(feature = "otel")]
346fn do_init_metrics_provider(otel_cfg: &OpenTelemetryConfig) -> anyhow::Result<()> {
347 let resolved_exporter = otel_cfg.metrics_exporter();
348
349 let (kind, endpoint, timeout) = extract_exporter_config(resolved_exporter);
350
351 let exporter = if matches!(kind, ExporterKind::OtlpHttp) {
353 let mut b = opentelemetry_otlp::MetricExporter::builder()
354 .with_http()
355 .with_protocol(Protocol::HttpBinary)
356 .with_endpoint(&endpoint);
357 if let Some(t) = timeout {
358 b = b.with_timeout(t);
359 }
360 if let Some(headers) = build_headers_from_cfg_and_env(resolved_exporter) {
361 b = b.with_headers(headers);
362 }
363 b.build().context("build OTLP HTTP metric exporter")?
364 } else {
365 let mut b = opentelemetry_otlp::MetricExporter::builder()
366 .with_tonic()
367 .with_endpoint(&endpoint);
368 if let Some(t) = timeout {
369 b = b.with_timeout(t);
370 }
371 if let Some(md) = build_metadata_from_cfg_and_env(resolved_exporter) {
372 b = b.with_metadata(md);
373 }
374 b.build().context("build OTLP gRPC metric exporter")?
375 };
376
377 let resource = build_resource(&otel_cfg.resource);
379
380 let mut builder = opentelemetry_sdk::metrics::SdkMeterProvider::builder()
382 .with_periodic_exporter(exporter)
383 .with_resource(resource);
384
385 if let Some(limit) = otel_cfg.metrics.cardinality_limit {
387 builder = builder.with_view(move |_: &opentelemetry_sdk::metrics::Instrument| {
388 opentelemetry_sdk::metrics::Stream::builder()
389 .with_cardinality_limit(limit)
390 .build()
391 .ok()
392 });
393 }
394
395 let provider = builder.build();
396
397 global::set_meter_provider(provider);
398 tracing::info!("OpenTelemetry metrics initialized successfully");
399
400 Ok(())
401}
402
403#[cfg(not(feature = "otel"))]
408pub fn init_metrics_provider(_otel_cfg: &super::config::OpenTelemetryConfig) -> anyhow::Result<()> {
409 Err(anyhow::anyhow!("otel feature is disabled"))
410}
411
412#[cfg(feature = "otel")]
420pub fn otel_connectivity_probe(otel_cfg: &OpenTelemetryConfig) -> anyhow::Result<()> {
421 use opentelemetry::trace::{Span, Tracer as _};
422
423 let resolved_exporter = otel_cfg.tracing_exporter();
424 let (kind, endpoint, timeout) = extract_exporter_config(resolved_exporter);
425
426 let resource = build_resource(&otel_cfg.resource);
428
429 let exporter = if matches!(kind, ExporterKind::OtlpHttp) {
431 let mut b = opentelemetry_otlp::SpanExporter::builder()
432 .with_http()
433 .with_protocol(Protocol::HttpBinary)
434 .with_endpoint(endpoint);
435 if let Some(t) = timeout {
436 b = b.with_timeout(t);
437 }
438 if let Some(h) = build_headers_from_cfg_and_env(resolved_exporter) {
439 b = b.with_headers(h);
440 }
441 b.build()
442 .map_err(|e| anyhow::anyhow!("otlp http exporter build failed: {e}"))?
443 } else {
444 let mut b = opentelemetry_otlp::SpanExporter::builder()
445 .with_tonic()
446 .with_endpoint(endpoint);
447 if let Some(t) = timeout {
448 b = b.with_timeout(t);
449 }
450 if let Some(md) = build_metadata_from_cfg_and_env(resolved_exporter) {
451 b = b.with_metadata(md);
452 }
453 b.build()
454 .map_err(|e| anyhow::anyhow!("otlp grpc exporter build failed: {e}"))?
455 };
456
457 let provider = SdkTracerProvider::builder()
459 .with_simple_exporter(exporter)
460 .with_resource(resource)
461 .build();
462
463 let tracer = provider.tracer("connectivity_probe");
465 let mut span = tracer.start("otel_connectivity_probe");
466 span.end();
467
468 if let Err(e) = provider.force_flush() {
470 tracing::warn!(error = %e, "force_flush failed during OTLP connectivity probe");
471 }
472
473 provider
474 .shutdown()
475 .map_err(|e| anyhow::anyhow!("shutdown failed: {e}"))?;
476
477 tracing::info!(kind = ?kind, "OTLP connectivity probe exported a test span");
478 Ok(())
479}
480
481#[cfg(not(feature = "otel"))]
486pub fn otel_connectivity_probe(_cfg: &super::config::OpenTelemetryConfig) -> anyhow::Result<()> {
487 tracing::info!("OTLP connectivity probe skipped (otel feature disabled)");
488 Ok(())
489}
490
491#[cfg(test)]
494#[cfg_attr(coverage_nightly, coverage(off))]
495mod tests {
496 use super::*;
497 use crate::telemetry::config::{
498 Exporter, ExporterKind, OpenTelemetryConfig, OpenTelemetryResource, Sampler, TracingConfig,
499 };
500 use std::collections::{BTreeMap, HashMap};
501
502 fn otel_with_tracing(tracing: TracingConfig) -> OpenTelemetryConfig {
504 OpenTelemetryConfig {
505 tracing,
506 ..Default::default()
507 }
508 }
509
510 #[test]
511 #[cfg(feature = "otel")]
512 fn test_init_tracing_disabled() {
513 let otel = otel_with_tracing(TracingConfig {
514 enabled: false,
515 ..Default::default()
516 });
517
518 let result = init_tracing(&otel);
519 assert!(result.is_err());
520 }
521
522 #[tokio::test]
523 #[cfg(feature = "otel")]
524 async fn test_init_tracing_enabled() {
525 let otel = otel_with_tracing(TracingConfig {
526 enabled: true,
527 ..Default::default()
528 });
529
530 let result = init_tracing(&otel);
531 assert!(result.is_ok());
532 }
533
534 #[test]
535 #[cfg(feature = "otel")]
536 fn test_init_tracing_with_resource_attributes() {
537 let rt = tokio::runtime::Runtime::new().unwrap();
538 let _guard = rt.enter();
539
540 let mut attrs = BTreeMap::new();
541 attrs.insert("service.version".to_owned(), "1.0.0".to_owned());
542 attrs.insert("deployment.environment".to_owned(), "test".to_owned());
543
544 let otel = OpenTelemetryConfig {
545 resource: OpenTelemetryResource {
546 service_name: "test-service".to_owned(),
547 attributes: attrs,
548 },
549 tracing: TracingConfig {
550 enabled: true,
551 ..Default::default()
552 },
553 ..Default::default()
554 };
555
556 let result = init_tracing(&otel);
557 assert!(result.is_ok());
558 }
559
560 #[test]
561 #[cfg(feature = "otel")]
562 fn test_init_tracing_with_always_on_sampler() {
563 let rt = tokio::runtime::Runtime::new().unwrap();
564 let _guard = rt.enter();
565
566 let otel = otel_with_tracing(TracingConfig {
567 enabled: true,
568 sampler: Some(Sampler::AlwaysOn {}),
569 ..Default::default()
570 });
571
572 let result = init_tracing(&otel);
573 assert!(result.is_ok());
574 }
575
576 #[test]
577 #[cfg(feature = "otel")]
578 fn test_init_tracing_with_always_off_sampler() {
579 let rt = tokio::runtime::Runtime::new().unwrap();
580 let _guard = rt.enter();
581
582 let otel = otel_with_tracing(TracingConfig {
583 enabled: true,
584 sampler: Some(Sampler::AlwaysOff {}),
585 ..Default::default()
586 });
587
588 let result = init_tracing(&otel);
589 assert!(result.is_ok());
590 }
591
592 #[test]
593 #[cfg(feature = "otel")]
594 fn test_init_tracing_with_ratio_sampler() {
595 let rt = tokio::runtime::Runtime::new().unwrap();
596 let _guard = rt.enter();
597
598 let otel = otel_with_tracing(TracingConfig {
599 enabled: true,
600 sampler: Some(Sampler::ParentBasedRatio { ratio: Some(0.5) }),
601 ..Default::default()
602 });
603
604 let result = init_tracing(&otel);
605 assert!(result.is_ok());
606 }
607
608 #[test]
609 #[cfg(feature = "otel")]
610 fn test_init_tracing_with_http_exporter() {
611 let _rt = tokio::runtime::Runtime::new().unwrap();
612
613 let otel = otel_with_tracing(TracingConfig {
614 enabled: true,
615 exporter: Some(Exporter {
616 kind: ExporterKind::OtlpHttp,
617 endpoint: Some("http://localhost:4318".to_owned()),
618 headers: None,
619 timeout_ms: Some(5000),
620 }),
621 ..Default::default()
622 });
623
624 let result = init_tracing(&otel);
625 assert!(result.is_ok());
626 }
627
628 #[test]
629 #[cfg(feature = "otel")]
630 fn test_init_tracing_with_grpc_exporter() {
631 let rt = tokio::runtime::Runtime::new().unwrap();
632 let _guard = rt.enter();
633
634 let otel = otel_with_tracing(TracingConfig {
635 enabled: true,
636 exporter: Some(Exporter {
637 kind: ExporterKind::OtlpGrpc,
638 endpoint: Some("http://localhost:4317".to_owned()),
639 headers: None,
640 timeout_ms: Some(5000),
641 }),
642 ..Default::default()
643 });
644
645 let result = init_tracing(&otel);
646 assert!(result.is_ok());
647 }
648
649 #[test]
650 #[cfg(feature = "otel")]
651 fn test_build_headers_from_cfg_empty() {
652 temp_env::with_var_unset("OTEL_EXPORTER_OTLP_HEADERS", || {
653 let cfg = TracingConfig {
654 enabled: true,
655 ..Default::default()
656 };
657
658 let result = build_headers_from_cfg_and_env(cfg.exporter.as_ref());
659 assert!(
660 result.is_none(),
661 "expected None when no headers configured and no env"
662 );
663 });
664 }
665
666 #[test]
667 #[cfg(feature = "otel")]
668 fn test_build_headers_from_cfg_with_headers() {
669 let mut headers = HashMap::new();
670 headers.insert("authorization".to_owned(), "Bearer token".to_owned());
671
672 let cfg = TracingConfig {
673 enabled: true,
674 exporter: Some(Exporter {
675 kind: ExporterKind::OtlpHttp,
676 endpoint: Some("http://localhost:4318".to_owned()),
677 headers: Some(headers.clone()),
678 timeout_ms: None,
679 }),
680 ..Default::default()
681 };
682
683 let result = build_headers_from_cfg_and_env(cfg.exporter.as_ref());
684 assert!(result.is_some());
685 let result_headers = result.unwrap();
686 assert_eq!(
687 result_headers.get("authorization"),
688 Some(&"Bearer token".to_owned())
689 );
690 }
691
692 #[test]
693 #[cfg(feature = "otel")]
694 fn test_build_metadata_from_cfg_empty() {
695 temp_env::with_var_unset("OTEL_EXPORTER_OTLP_HEADERS", || {
696 let cfg = TracingConfig {
697 enabled: true,
698 ..Default::default()
699 };
700
701 let result = build_metadata_from_cfg_and_env(cfg.exporter.as_ref());
702 assert!(
703 result.is_none(),
704 "expected None when no headers configured and no env"
705 );
706 });
707 }
708
709 #[test]
710 #[cfg(feature = "otel")]
711 fn test_build_metadata_from_cfg_with_headers() {
712 let mut headers = HashMap::new();
713 headers.insert("authorization".to_owned(), "Bearer token".to_owned());
714
715 let cfg = TracingConfig {
716 enabled: true,
717 exporter: Some(Exporter {
718 kind: ExporterKind::OtlpGrpc,
719 endpoint: Some("http://localhost:4317".to_owned()),
720 headers: Some(headers.clone()),
721 timeout_ms: None,
722 }),
723 ..Default::default()
724 };
725
726 let result = build_metadata_from_cfg_and_env(cfg.exporter.as_ref());
727 assert!(result.is_some());
728 let metadata = result.unwrap();
729 assert!(!metadata.is_empty());
730 }
731
732 #[test]
733 #[cfg(feature = "otel")]
734 fn test_build_metadata_multiple_headers() {
735 let mut headers = HashMap::new();
736 headers.insert("authorization".to_owned(), "Bearer token".to_owned());
737 headers.insert("x-custom-header".to_owned(), "custom-value".to_owned());
738
739 let cfg = TracingConfig {
740 enabled: true,
741 exporter: Some(Exporter {
742 kind: ExporterKind::OtlpGrpc,
743 endpoint: Some("http://localhost:4317".to_owned()),
744 headers: Some(headers.clone()),
745 timeout_ms: None,
746 }),
747 ..Default::default()
748 };
749
750 let result = build_metadata_from_cfg_and_env(cfg.exporter.as_ref());
751 assert!(result.is_some());
752 let metadata = result.unwrap();
753 assert_eq!(metadata.len(), 2);
754 }
755
756 #[test]
757 #[cfg(feature = "otel")]
758 fn test_build_metadata_invalid_header_name_skipped() {
759 let mut headers = HashMap::new();
760 headers.insert("valid-header".to_owned(), "value1".to_owned());
761 headers.insert("invalid header with spaces".to_owned(), "value2".to_owned());
762
763 let cfg = TracingConfig {
764 enabled: true,
765 exporter: Some(Exporter {
766 kind: ExporterKind::OtlpGrpc,
767 endpoint: Some("http://localhost:4317".to_owned()),
768 headers: Some(headers.clone()),
769 timeout_ms: None,
770 }),
771 ..Default::default()
772 };
773
774 let result = build_metadata_from_cfg_and_env(cfg.exporter.as_ref());
775 assert!(result.is_some());
776 let metadata = result.unwrap();
777 assert_eq!(metadata.len(), 1);
779 }
780
781 #[test]
782 fn test_shutdown_tracing_does_not_panic() {
783 shutdown_tracing();
785 }
786
787 #[test]
788 #[cfg(feature = "otel")]
789 fn test_init_metrics_provider_disabled() {
790 let otel = OpenTelemetryConfig {
791 metrics: crate::telemetry::config::MetricsConfig {
792 enabled: false,
793 ..Default::default()
794 },
795 ..Default::default()
796 };
797 let result = init_metrics_provider(&otel);
799 assert!(result.is_ok());
800 }
801}