1#[cfg(not(any(feature = "grpc", feature = "http")))]
27compile_error!("at least one transport feature must be enabled: `grpc` or `http`");
28
29#[cfg(feature = "testing")]
30pub mod testing;
31
32#[cfg(feature = "axum")]
33pub mod axum_middleware;
34
35#[cfg(feature = "tonic-tracing")]
36pub mod grpc_middleware;
37
38#[cfg(feature = "profiling")]
39pub mod profiling;
40mod runtime_metrics;
41
42pub mod export_backoff;
43pub mod instrumented_port;
44pub mod log_bridge;
45pub mod span_enrichment;
46pub mod spanned;
47
48pub use instrumented_port::{Instrumented, InstrumentedArc};
49pub use log_bridge::{
50 PROPAGATED_SPAN_FIELDS, SpanLogAttrs, record_span_log_attr, record_span_log_attr_on,
51};
52pub use spanned::{Spanned, in_span};
53
54use opentelemetry::KeyValue;
55use opentelemetry::propagation::TextMapCompositePropagator;
56use opentelemetry_otlp::WithExportConfig;
57use opentelemetry_sdk::{
58 Resource,
59 logs::SdkLoggerProvider,
60 metrics::{MeterProviderBuilder, PeriodicReader, SdkMeterProvider},
61 propagation::{BaggagePropagator, TraceContextPropagator},
62 trace::{BatchConfigBuilder, BatchSpanProcessor, Sampler, SdkTracer, SdkTracerProvider},
63};
64use opentelemetry_semantic_conventions::attribute::{
65 DEPLOYMENT_ENVIRONMENT_NAME, HOST_NAME, PROCESS_PID, SERVICE_VERSION,
66};
67use std::error::Error;
68use std::time::Duration;
69use tracing_subscriber::layer::SubscriberExt;
70use tracing_subscriber::util::SubscriberInitExt;
71
72fn tracing_bridge_tracer(provider: &SdkTracerProvider) -> SdkTracer {
73 use opentelemetry::trace::TracerProvider as _;
74
75 provider.tracer(env!("CARGO_PKG_NAME"))
76}
77
78#[derive(Debug, Clone)]
93pub enum TraceSampler {
94 AlwaysOn,
96 AlwaysOff,
98 TraceIdRatio(f64),
100 ParentBased(Box<TraceSampler>),
103}
104
105#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
107pub enum LogFormat {
108 #[default]
110 Pretty,
111 Json,
113}
114
115impl TraceSampler {
116 fn into_sdk_sampler(self) -> Sampler {
118 match self {
119 TraceSampler::AlwaysOn => Sampler::AlwaysOn,
120 TraceSampler::AlwaysOff => Sampler::AlwaysOff,
121 TraceSampler::TraceIdRatio(r) => Sampler::TraceIdRatioBased(r),
122 TraceSampler::ParentBased(inner) => {
123 Sampler::ParentBased(Box::new(inner.into_sdk_sampler()))
124 }
125 }
126 }
127}
128
129fn sampler_from_env() -> Result<Option<TraceSampler>, Box<dyn Error>> {
137 let name = match std::env::var("OTEL_TRACES_SAMPLER") {
138 Ok(v) => v,
139 Err(_) => return Ok(None),
140 };
141 let arg = std::env::var("OTEL_TRACES_SAMPLER_ARG").ok();
142 let sampler = match name.as_str() {
143 "always_on" => TraceSampler::AlwaysOn,
144 "always_off" => TraceSampler::AlwaysOff,
145 "traceidratio" => {
146 let ratio = arg
147 .as_deref()
148 .unwrap_or("1.0")
149 .parse::<f64>()
150 .unwrap_or(1.0);
151 TraceSampler::TraceIdRatio(ratio)
152 }
153 "parentbased_always_on" => TraceSampler::ParentBased(Box::new(TraceSampler::AlwaysOn)),
154 "parentbased_always_off" => TraceSampler::ParentBased(Box::new(TraceSampler::AlwaysOff)),
155 "parentbased_traceidratio" => {
156 let ratio = arg
157 .as_deref()
158 .unwrap_or("1.0")
159 .parse::<f64>()
160 .unwrap_or(1.0);
161 TraceSampler::ParentBased(Box::new(TraceSampler::TraceIdRatio(ratio)))
162 }
163 unknown => {
164 return Err(format!(
165 "OTEL_TRACES_SAMPLER: unrecognised sampler name '{unknown}'. \
166 Valid values: always_on, always_off, traceidratio, \
167 parentbased_always_on, parentbased_always_off, parentbased_traceidratio"
168 )
169 .into());
170 }
171 };
172 Ok(Some(sampler))
173}
174
175const DEFAULT_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5);
177
178pub struct TelemetryHandles {
199 pub tracer_provider: SdkTracerProvider,
200 pub meter_provider: Option<SdkMeterProvider>,
201 pub logger_provider: Option<SdkLoggerProvider>,
202 shutdown_timeout: Duration,
203 #[cfg(feature = "profiling")]
204 pub profiling_handle: Option<profiling::ProfilingHandle>,
205}
206
207impl TelemetryHandles {
208 pub fn shutdown(&self) -> Result<(), Box<dyn Error>> {
239 if let Err(e) = self.tracer_provider.shutdown() {
240 tracing::warn!("tracer provider shutdown error: {e}");
241 }
242 if let Some(mp) = &self.meter_provider
243 && let Err(e) = mp.shutdown()
244 {
245 tracing::warn!("meter provider shutdown error: {e}");
246 }
247 if let Some(lp) = &self.logger_provider
248 && let Err(e) = lp.shutdown()
249 {
250 tracing::warn!("logger provider shutdown error: {e}");
251 }
252 Ok(())
253 }
254}
255
256impl Drop for TelemetryHandles {
257 fn drop(&mut self) {
258 let tracer_provider = self.tracer_provider.clone();
259 let meter_provider = self.meter_provider.clone();
260 let logger_provider = self.logger_provider.clone();
261 let timeout = self.shutdown_timeout;
262
263 let (tx, rx) = std::sync::mpsc::channel();
264 std::thread::spawn(move || {
265 if let Err(e) = tracer_provider.shutdown() {
266 tracing::warn!("tracer provider shutdown error: {e}");
267 }
268 if let Some(mp) = meter_provider
269 && let Err(e) = mp.shutdown()
270 {
271 tracing::warn!("meter provider shutdown error: {e}");
272 }
273 if let Some(lp) = logger_provider
274 && let Err(e) = lp.shutdown()
275 {
276 tracing::warn!("logger provider shutdown error: {e}");
277 }
278 let _ = tx.send(());
279 });
280
281 if rx.recv_timeout(timeout).is_err() {
282 tracing::warn!(
283 "telemetry shutdown did not complete within {timeout:?}; \
284 some spans/metrics may not have been exported"
285 );
286 }
287 }
288}
289
290#[derive(Debug, Clone, Copy, PartialEq, Eq)]
313pub enum ExportProtocol {
314 #[cfg(feature = "grpc")]
316 Grpc,
317 #[cfg(feature = "http")]
319 HttpProtobuf,
320}
321
322#[cfg(feature = "grpc-mtls")]
332#[derive(Clone)]
333pub struct MtlsMaterial {
334 pub client_cert_chain_pem: Vec<u8>,
336 pub client_key_pem: Vec<u8>,
338 pub trust_bundle_pem: Vec<u8>,
340}
341
342#[cfg(feature = "grpc-mtls")]
343impl std::fmt::Debug for MtlsMaterial {
344 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
345 f.debug_struct("MtlsMaterial")
346 .field("client_cert_chain_pem", &"<redacted>")
347 .field("client_key_pem", &"<redacted>")
348 .field("trust_bundle_pem", &"<redacted>")
349 .finish()
350 }
351}
352
353fn protocol_from_env() -> Option<ExportProtocol> {
355 let val = std::env::var("OTEL_EXPORTER_OTLP_PROTOCOL").ok()?;
356 match val.trim() {
357 #[cfg(feature = "grpc")]
358 "grpc" => Some(ExportProtocol::Grpc),
359 #[cfg(feature = "http")]
360 "http/protobuf" => Some(ExportProtocol::HttpProtobuf),
361 _ => None,
362 }
363}
364
365pub struct Telemetry;
381
382impl Telemetry {
383 pub fn builder(service_name: &str) -> TelemetryBuilder {
387 TelemetryBuilder {
388 service_name: Some(service_name.to_string()),
389 service_version: None,
390 deployment_environment: None,
391 sampler: None,
392 metrics: true,
393 logs: false,
394 protocol: None,
395 default_endpoint: None,
396 max_export_batch_size: None,
397 metric_export_interval: None,
398 export_timeout: None,
399 shutdown_timeout: DEFAULT_SHUTDOWN_TIMEOUT,
400 log_filter: None,
401 log_format: LogFormat::default(),
402 extra_layers: Vec::new(),
403 extra_metric_readers: Vec::new(),
404 runtime_metrics: true,
405 #[cfg(feature = "grpc-mtls")]
406 mtls: None,
407 propagated_span_fields: crate::log_bridge::PROPAGATED_SPAN_FIELDS,
408 #[cfg(feature = "profiling")]
409 pyroscope_endpoint: None,
410 }
411 }
412
413 pub fn from_env() -> TelemetryBuilder {
423 TelemetryBuilder {
424 service_name: None,
425 service_version: None,
426 deployment_environment: None,
427 sampler: None,
428 metrics: true,
429 logs: false,
430 protocol: None,
431 default_endpoint: None,
432 max_export_batch_size: None,
433 metric_export_interval: None,
434 export_timeout: None,
435 shutdown_timeout: DEFAULT_SHUTDOWN_TIMEOUT,
436 log_filter: None,
437 log_format: LogFormat::default(),
438 extra_layers: Vec::new(),
439 extra_metric_readers: Vec::new(),
440 runtime_metrics: true,
441 #[cfg(feature = "grpc-mtls")]
442 mtls: None,
443 propagated_span_fields: crate::log_bridge::PROPAGATED_SPAN_FIELDS,
444 #[cfg(feature = "profiling")]
445 pyroscope_endpoint: None,
446 }
447 }
448}
449
450#[must_use = "a TelemetryBuilder does nothing until .init() is called"]
468pub struct TelemetryBuilder {
469 service_name: Option<String>,
470 service_version: Option<String>,
471 deployment_environment: Option<String>,
472 sampler: Option<TraceSampler>,
473 metrics: bool,
474 logs: bool,
475 protocol: Option<ExportProtocol>,
476 max_export_batch_size: Option<usize>,
477 metric_export_interval: Option<Duration>,
478 export_timeout: Option<Duration>,
479 shutdown_timeout: Duration,
480 log_filter: Option<String>,
481 log_format: LogFormat,
482 extra_layers: Vec<
483 Box<dyn tracing_subscriber::Layer<tracing_subscriber::Registry> + Send + Sync + 'static>,
484 >,
485 extra_metric_readers: Vec<MeterProviderInstaller>,
486 runtime_metrics: bool,
487 #[cfg(feature = "grpc-mtls")]
488 mtls: Option<MtlsMaterial>,
489 propagated_span_fields: &'static [&'static str],
490 #[cfg(feature = "profiling")]
491 pyroscope_endpoint: Option<String>,
492 default_endpoint: Option<String>,
493}
494
495type MeterProviderInstaller =
500 Box<dyn FnOnce(MeterProviderBuilder) -> MeterProviderBuilder + Send + Sync>;
501
502impl TelemetryBuilder {
503 pub fn with_log_filter(mut self, directive: impl Into<String>) -> Self {
508 self.log_filter = Some(directive.into());
509 self
510 }
511
512 pub fn with_log_format(mut self, format: LogFormat) -> Self {
514 self.log_format = format;
515 self
516 }
517
518 pub fn with_version(mut self, version: &str) -> Self {
520 self.service_version = Some(version.to_string());
521 self
522 }
523
524 pub fn with_environment(mut self, environment: &str) -> Self {
526 self.deployment_environment = Some(environment.to_string());
527 self
528 }
529
530 #[cfg(feature = "grpc-mtls")]
555 pub fn with_mtls(mut self, material: MtlsMaterial) -> Self {
556 self.mtls = Some(material);
557 self.protocol = Some(ExportProtocol::Grpc);
558 self
559 }
560
561 pub fn with_sampler(mut self, sampler: TraceSampler) -> Self {
564 self.sampler = Some(sampler);
565 self
566 }
567
568 pub fn with_metrics(mut self, enabled: bool) -> Self {
570 self.metrics = enabled;
571 self
572 }
573
574 pub fn with_runtime_metrics(mut self, enabled: bool) -> Self {
590 self.runtime_metrics = enabled;
591 self
592 }
593
594 pub fn with_default_endpoint(mut self, endpoint: impl Into<String>) -> Self {
599 self.default_endpoint = Some(endpoint.into());
600 self
601 }
602
603 pub fn with_protocol(mut self, protocol: ExportProtocol) -> Self {
607 self.protocol = Some(protocol);
608 self
609 }
610
611 pub fn with_max_export_batch_size(mut self, size: usize) -> Self {
616 self.max_export_batch_size = Some(size);
617 self
618 }
619
620 pub fn with_metric_export_interval(mut self, interval: Duration) -> Self {
625 self.metric_export_interval = Some(interval);
626 self
627 }
628
629 pub fn with_logs(mut self, enabled: bool) -> Self {
636 self.logs = enabled;
637 self
638 }
639
640 pub fn with_propagated_span_fields(mut self, fields: &'static [&'static str]) -> Self {
653 self.propagated_span_fields = fields;
654 self
655 }
656
657 pub fn with_export_timeout(mut self, timeout: Duration) -> Self {
661 self.export_timeout = Some(timeout);
662 self
663 }
664
665 pub fn with_shutdown_timeout(mut self, timeout: Duration) -> Self {
672 self.shutdown_timeout = timeout;
673 self
674 }
675
676 #[cfg(feature = "profiling")]
694 pub fn with_profiling(mut self, endpoint: &str) -> Self {
695 self.pyroscope_endpoint = Some(endpoint.to_string());
696 self
697 }
698
699 pub fn with_meter_provider_setup<F>(mut self, setup: F) -> Self
756 where
757 F: FnOnce(MeterProviderBuilder) -> MeterProviderBuilder + Send + Sync + 'static,
758 {
759 self.extra_metric_readers.push(Box::new(setup));
760 self
761 }
762
763 pub fn with_layer<L>(mut self, layer: L) -> Self
764 where
765 L: tracing_subscriber::Layer<tracing_subscriber::Registry> + Send + Sync + 'static,
766 {
767 self.extra_layers.push(Box::new(layer));
768 self
769 }
770
771 pub fn init(self) -> Result<TelemetryHandles, Box<dyn Error>> {
786 let log_filter = match self.log_filter.as_deref() {
787 Some(directive) => tracing_subscriber::EnvFilter::try_new(directive)?,
788 None => tracing_subscriber::EnvFilter::from_default_env(),
789 };
790
791 if let Some(interval) = self.metric_export_interval
792 && interval.is_zero()
793 {
794 return Err("metric_export_interval must be greater than zero".into());
795 }
796
797 let protocol = self.protocol.or_else(protocol_from_env).unwrap_or({
798 #[cfg(feature = "grpc")]
799 {
800 ExportProtocol::Grpc
801 }
802 #[cfg(all(not(feature = "grpc"), feature = "http"))]
803 {
804 ExportProtocol::HttpProtobuf
805 }
806 });
807
808 let default_endpoint = match protocol {
809 #[cfg(feature = "grpc")]
810 ExportProtocol::Grpc => "http://localhost:4317",
811 #[cfg(feature = "http")]
812 ExportProtocol::HttpProtobuf => "http://localhost:4318",
813 };
814 let endpoint = resolve_endpoint(
815 std::env::var("OTEL_EXPORTER_OTLP_ENDPOINT").ok(),
816 self.default_endpoint.as_deref(),
817 default_endpoint,
818 );
819
820 let export_timeout = self.export_timeout.or_else(timeout_from_env);
822
823 let service_name = self.service_name.unwrap_or_else(|| {
825 std::env::var("OTEL_SERVICE_NAME").unwrap_or_else(|_| "unknown_service".to_string())
826 });
827
828 let resource = build_resource(
829 &service_name,
830 self.service_version.as_deref(),
831 self.deployment_environment.as_deref(),
832 );
833
834 let sampler = match self.sampler {
835 Some(s) => s,
836 None => sampler_from_env()?.unwrap_or(TraceSampler::AlwaysOn),
837 };
838
839 let trace_exporter = build_span_exporter(
841 protocol,
842 &endpoint,
843 export_timeout,
844 #[cfg(feature = "grpc-mtls")]
845 self.mtls.as_ref(),
846 )?;
847
848 let batch_processor = if let Some(size) = self.max_export_batch_size {
849 BatchSpanProcessor::builder(trace_exporter)
850 .with_batch_config(
851 BatchConfigBuilder::default()
852 .with_max_export_batch_size(size)
853 .build(),
854 )
855 .build()
856 } else {
857 BatchSpanProcessor::builder(trace_exporter).build()
858 };
859
860 let tracer_provider = SdkTracerProvider::builder()
861 .with_resource(resource.clone())
862 .with_sampler(sampler.into_sdk_sampler())
863 .with_span_processor(batch_processor)
864 .build();
865
866 opentelemetry::global::set_tracer_provider(tracer_provider.clone());
867
868 let propagator = TextMapCompositePropagator::new(vec![
870 Box::new(TraceContextPropagator::new()),
871 Box::new(BaggagePropagator::new()),
872 ]);
873 opentelemetry::global::set_text_map_propagator(propagator);
874
875 let meter_provider = if self.metrics {
877 let metric_exporter = build_metric_exporter(
878 protocol,
879 &endpoint,
880 export_timeout,
881 #[cfg(feature = "grpc-mtls")]
882 self.mtls.as_ref(),
883 )?;
884
885 let periodic_reader = if let Some(interval) = self.metric_export_interval {
886 PeriodicReader::builder(metric_exporter)
887 .with_interval(interval)
888 .build()
889 } else {
890 PeriodicReader::builder(metric_exporter).build()
891 };
892
893 let mut mp_builder = SdkMeterProvider::builder()
894 .with_resource(resource.clone())
895 .with_reader(periodic_reader);
896 for installer in self.extra_metric_readers {
897 mp_builder = installer(mp_builder);
898 }
899 let mp = mp_builder.build();
900
901 opentelemetry::global::set_meter_provider(mp.clone());
902
903 if self.runtime_metrics {
907 crate::runtime_metrics::install();
908 }
909
910 Some(mp)
911 } else {
912 None
913 };
914
915 let logger_provider = if self.logs {
917 let log_exporter = build_log_exporter(
918 protocol,
919 &endpoint,
920 export_timeout,
921 #[cfg(feature = "grpc-mtls")]
922 self.mtls.as_ref(),
923 )?;
924
925 let lp = SdkLoggerProvider::builder()
926 .with_resource(resource)
927 .with_batch_exporter(log_exporter)
928 .build();
929
930 Some(lp)
931 } else {
932 None
933 };
934
935 #[cfg(feature = "profiling")]
937 let profiling_handle = if let Some(ref endpoint) = self.pyroscope_endpoint {
938 let identity = profiling::ProfilingIdentity {
943 host_name: hostname::get()
944 .ok()
945 .and_then(|h| h.into_string().ok())
946 .filter(|h| !h.is_empty()),
947 deployment_environment: self.deployment_environment.clone(),
948 service_version: self.service_version.clone(),
949 };
950 profiling::start_pyroscope_bridge(&service_name, endpoint, &identity)?
951 } else {
952 None
953 };
954 #[cfg(not(feature = "profiling"))]
955 let _profiling_handle: Option<()> = None;
956
957 let extra = if self.extra_layers.is_empty() {
963 None
964 } else {
965 Some(self.extra_layers)
966 };
967
968 macro_rules! install_subscriber {
969 ($fmt_layer:expr) => {{
970 let otel_layer = tracing_opentelemetry::layer()
974 .with_tracer(tracing_bridge_tracer(&tracer_provider));
975 let registry = tracing_subscriber::registry()
978 .with(extra)
979 .with(crate::export_backoff::ExportFailureBackoff::default())
980 .with(log_filter)
981 .with($fmt_layer)
982 .with(otel_layer);
983
984 #[cfg(feature = "profiling-bridge-pyroscope-rs")]
987 #[allow(deprecated)]
988 let registry = registry.with(crate::profiling::ProfilingTagLayer);
989
990 if let Some(lp) = &logger_provider {
991 if let Err(e) = registry
992 .with(crate::log_bridge::SpanAwareLogBridge::new(
993 lp,
994 self.propagated_span_fields,
995 ))
996 .try_init()
997 {
998 eprintln!(
999 "otel-bootstrap: global tracing subscriber already installed — \
1000 OTLP log records will NOT be exported to the collector: {e}"
1001 );
1002 }
1003 } else if let Err(e) = registry.try_init() {
1004 eprintln!(
1005 "otel-bootstrap: global tracing subscriber already installed — \
1006 OTLP telemetry will NOT be exported to the collector: {e}"
1007 );
1008 }
1009 }};
1010 }
1011
1012 match self.log_format {
1013 LogFormat::Pretty => install_subscriber!(tracing_subscriber::fmt::layer()),
1014 LogFormat::Json => install_subscriber!(tracing_subscriber::fmt::layer().json()),
1015 }
1016
1017 Ok(TelemetryHandles {
1018 tracer_provider,
1019 meter_provider,
1020 logger_provider,
1021 shutdown_timeout: self.shutdown_timeout,
1022 #[cfg(feature = "profiling")]
1023 profiling_handle,
1024 })
1025 }
1026}
1027
1028pub fn init_telemetry(service_name: &str) -> Result<TelemetryHandles, Box<dyn Error>> {
1042 Telemetry::builder(service_name).init()
1043}
1044
1045pub fn init_telemetry_with_sampler(
1062 service_name: &str,
1063 sampler: Option<TraceSampler>,
1064) -> Result<TelemetryHandles, Box<dyn Error>> {
1065 let builder = Telemetry::builder(service_name);
1066 match sampler {
1067 Some(s) => builder.with_sampler(s),
1068 None => builder, }
1070 .init()
1071}
1072
1073fn timeout_from_env() -> Option<Duration> {
1075 let ms = std::env::var("OTEL_EXPORTER_OTLP_TIMEOUT").ok()?;
1076 let ms: u64 = ms.trim().parse().ok()?;
1077 Some(Duration::from_millis(ms))
1078}
1079
1080#[cfg(feature = "grpc-mtls")]
1087fn build_tls_config(material: &MtlsMaterial) -> tonic::transport::ClientTlsConfig {
1088 use tonic::transport::{Certificate, ClientTlsConfig, Identity};
1089 ClientTlsConfig::new()
1090 .ca_certificate(Certificate::from_pem(&material.trust_bundle_pem))
1091 .identity(Identity::from_pem(
1092 &material.client_cert_chain_pem,
1093 &material.client_key_pem,
1094 ))
1095}
1096
1097fn resolve_endpoint(
1100 configured: Option<String>,
1101 runtime_default: Option<&str>,
1102 fallback: &str,
1103) -> String {
1104 configured
1105 .or_else(|| runtime_default.map(str::to_owned))
1106 .unwrap_or_else(|| fallback.to_owned())
1107}
1108
1109fn build_span_exporter(
1110 protocol: ExportProtocol,
1111 endpoint: &str,
1112 timeout: Option<Duration>,
1113 #[cfg(feature = "grpc-mtls")] mtls: Option<&MtlsMaterial>,
1114) -> Result<opentelemetry_otlp::SpanExporter, Box<dyn Error>> {
1115 match protocol {
1116 #[cfg(feature = "grpc")]
1117 ExportProtocol::Grpc => {
1118 let mut b = opentelemetry_otlp::SpanExporter::builder()
1119 .with_tonic()
1120 .with_endpoint(endpoint);
1121 if let Some(t) = timeout {
1122 b = b.with_timeout(t);
1123 }
1124 #[cfg(feature = "grpc-mtls")]
1125 if let Some(m) = mtls {
1126 use opentelemetry_otlp::WithTonicConfig as _;
1127 b = b.with_tls_config(build_tls_config(m));
1128 }
1129 Ok(b.build()?)
1130 }
1131 #[cfg(feature = "http")]
1132 ExportProtocol::HttpProtobuf => {
1133 let mut b = opentelemetry_otlp::SpanExporter::builder()
1134 .with_http()
1135 .with_endpoint(endpoint);
1136 if let Some(t) = timeout {
1137 b = b.with_timeout(t);
1138 }
1139 Ok(b.build()?)
1140 }
1141 }
1142}
1143
1144fn build_metric_exporter(
1145 protocol: ExportProtocol,
1146 endpoint: &str,
1147 timeout: Option<Duration>,
1148 #[cfg(feature = "grpc-mtls")] mtls: Option<&MtlsMaterial>,
1149) -> Result<opentelemetry_otlp::MetricExporter, Box<dyn Error>> {
1150 match protocol {
1151 #[cfg(feature = "grpc")]
1152 ExportProtocol::Grpc => {
1153 let mut b = opentelemetry_otlp::MetricExporter::builder()
1154 .with_tonic()
1155 .with_endpoint(endpoint);
1156 if let Some(t) = timeout {
1157 b = b.with_timeout(t);
1158 }
1159 #[cfg(feature = "grpc-mtls")]
1160 if let Some(m) = mtls {
1161 use opentelemetry_otlp::WithTonicConfig as _;
1162 b = b.with_tls_config(build_tls_config(m));
1163 }
1164 Ok(b.build()?)
1165 }
1166 #[cfg(feature = "http")]
1167 ExportProtocol::HttpProtobuf => {
1168 let mut b = opentelemetry_otlp::MetricExporter::builder()
1169 .with_http()
1170 .with_endpoint(endpoint);
1171 if let Some(t) = timeout {
1172 b = b.with_timeout(t);
1173 }
1174 Ok(b.build()?)
1175 }
1176 }
1177}
1178
1179fn build_log_exporter(
1180 protocol: ExportProtocol,
1181 endpoint: &str,
1182 timeout: Option<Duration>,
1183 #[cfg(feature = "grpc-mtls")] mtls: Option<&MtlsMaterial>,
1184) -> Result<opentelemetry_otlp::LogExporter, Box<dyn Error>> {
1185 match protocol {
1186 #[cfg(feature = "grpc")]
1187 ExportProtocol::Grpc => {
1188 let mut b = opentelemetry_otlp::LogExporter::builder()
1189 .with_tonic()
1190 .with_endpoint(endpoint);
1191 if let Some(t) = timeout {
1192 b = b.with_timeout(t);
1193 }
1194 #[cfg(feature = "grpc-mtls")]
1195 if let Some(m) = mtls {
1196 use opentelemetry_otlp::WithTonicConfig as _;
1197 b = b.with_tls_config(build_tls_config(m));
1198 }
1199 Ok(b.build()?)
1200 }
1201 #[cfg(feature = "http")]
1202 ExportProtocol::HttpProtobuf => {
1203 let mut b = opentelemetry_otlp::LogExporter::builder()
1204 .with_http()
1205 .with_endpoint(endpoint);
1206 if let Some(t) = timeout {
1207 b = b.with_timeout(t);
1208 }
1209 Ok(b.build()?)
1210 }
1211 }
1212}
1213
1214pub fn build_resource(
1229 service_name: &str,
1230 service_version: Option<&str>,
1231 deployment_environment: Option<&str>,
1232) -> Resource {
1233 let hostname = hostname::get()
1234 .ok()
1235 .and_then(|h| h.into_string().ok())
1236 .unwrap_or_default();
1237
1238 let mut builder = Resource::builder()
1239 .with_service_name(service_name.to_string())
1240 .with_attributes([
1241 KeyValue::new(HOST_NAME, hostname),
1242 KeyValue::new(PROCESS_PID, std::process::id() as i64),
1243 ]);
1244
1245 if let Some(version) = service_version {
1246 builder = builder.with_attribute(KeyValue::new(SERVICE_VERSION, version.to_string()));
1247 }
1248
1249 if let Some(env) = deployment_environment {
1250 builder =
1251 builder.with_attribute(KeyValue::new(DEPLOYMENT_ENVIRONMENT_NAME, env.to_string()));
1252 }
1253
1254 builder.build()
1255}
1256
1257#[cfg(feature = "axum")]
1275pub fn axum_layer() -> axum_middleware::OtelTraceLayer {
1276 axum_middleware::OtelTraceLayer
1277}
1278
1279#[cfg(feature = "axum")]
1310pub fn span_enricher_layer<T>() -> axum_middleware::SpanEnricherLayer<T>
1311where
1312 T: span_enrichment::EnrichSpan + Clone + Send + Sync + 'static,
1313{
1314 axum_middleware::SpanEnricherLayer::default()
1315}
1316
1317#[cfg(feature = "tonic-tracing")]
1339pub fn grpc_client_layer() -> grpc_middleware::GrpcClientTraceLayer {
1340 grpc_middleware::GrpcClientTraceLayer
1341}
1342
1343#[cfg(feature = "tonic-tracing")]
1358pub fn grpc_server_layer() -> grpc_middleware::GrpcServerTraceLayer {
1359 grpc_middleware::GrpcServerTraceLayer
1360}
1361
1362#[cfg(test)]
1363mod tests {
1364 use super::*;
1365
1366 #[test]
1368 fn runtime_metrics_can_be_disabled() {
1369 assert!(
1370 Telemetry::builder("rm-default").runtime_metrics,
1371 "runtime metrics are on by default"
1372 );
1373 assert!(
1374 !Telemetry::builder("rm-off")
1375 .with_runtime_metrics(false)
1376 .runtime_metrics
1377 );
1378 }
1379
1380 #[tokio::test]
1388 async fn shutdown_absorbs_provider_errors() {
1389 let handles = TelemetryHandles {
1390 tracer_provider: SdkTracerProvider::builder().build(),
1391 meter_provider: Some(SdkMeterProvider::builder().build()),
1392 logger_provider: Some(SdkLoggerProvider::builder().build()),
1393 shutdown_timeout: DEFAULT_SHUTDOWN_TIMEOUT,
1394 #[cfg(feature = "profiling")]
1395 profiling_handle: None,
1396 };
1397
1398 handles.shutdown().expect("first shutdown succeeds");
1399 handles
1400 .shutdown()
1401 .expect("second shutdown absorbs the already-shut-down errors");
1402 }
1403 use opentelemetry::trace::{Span as _, Tracer as _};
1404 use std::sync::Mutex;
1405
1406 static ENV_LOCK: Mutex<()> = Mutex::new(());
1407
1408 #[test]
1409 fn tracing_bridge_uses_sdk_tracer() {
1410 let provider = SdkTracerProvider::builder().build();
1411 let tracer = tracing_bridge_tracer(&provider);
1412 let span = tracer.start("bridge-regression");
1413
1414 assert!(span.span_context().is_valid());
1415
1416 provider.shutdown().expect("provider shutdown");
1417 }
1418
1419 #[test]
1420 fn resource_contains_all_attributes_when_provided() {
1421 let resource = build_resource("test-svc", Some("1.2.3"), Some("staging"));
1422
1423 assert_eq!(
1424 resource.get(&opentelemetry::Key::new("service.name")),
1425 Some(opentelemetry::Value::from("test-svc")),
1426 );
1427 assert_eq!(
1428 resource.get(&opentelemetry::Key::new(SERVICE_VERSION)),
1429 Some(opentelemetry::Value::from("1.2.3")),
1430 );
1431 assert_eq!(
1432 resource.get(&opentelemetry::Key::new(DEPLOYMENT_ENVIRONMENT_NAME)),
1433 Some(opentelemetry::Value::from("staging")),
1434 );
1435 assert!(resource.get(&opentelemetry::Key::new(HOST_NAME)).is_some());
1436 assert!(
1437 resource
1438 .get(&opentelemetry::Key::new(PROCESS_PID))
1439 .is_some()
1440 );
1441 }
1442
1443 #[test]
1444 fn resource_graceful_when_optional_values_omitted() {
1445 let resource = build_resource("test-svc", None, None);
1446
1447 assert_eq!(
1448 resource.get(&opentelemetry::Key::new("service.name")),
1449 Some(opentelemetry::Value::from("test-svc")),
1450 );
1451 assert!(
1452 resource
1453 .get(&opentelemetry::Key::new(SERVICE_VERSION))
1454 .is_none()
1455 );
1456 assert!(
1457 resource
1458 .get(&opentelemetry::Key::new(DEPLOYMENT_ENVIRONMENT_NAME))
1459 .is_none()
1460 );
1461 assert!(resource.get(&opentelemetry::Key::new(HOST_NAME)).is_some());
1463 assert!(
1464 resource
1465 .get(&opentelemetry::Key::new(PROCESS_PID))
1466 .is_some()
1467 );
1468 }
1469
1470 #[test]
1471 fn trace_sampler_ratio_converts_to_sdk() {
1472 let sampler = TraceSampler::TraceIdRatio(0.5);
1473 let sdk = sampler.into_sdk_sampler();
1474 assert_eq!(format!("{sdk:?}"), "TraceIdRatioBased(0.5)");
1475 }
1476
1477 #[test]
1478 fn trace_sampler_parent_based_converts_to_sdk() {
1479 let sampler = TraceSampler::ParentBased(Box::new(TraceSampler::TraceIdRatio(0.25)));
1480 let sdk = sampler.into_sdk_sampler();
1481 let debug = format!("{sdk:?}");
1482 assert!(debug.contains("ParentBased"));
1483 assert!(debug.contains("0.25"));
1484 }
1485
1486 unsafe fn set_env(key: &str, val: &str) {
1488 unsafe {
1489 std::env::set_var(key, val);
1490 }
1491 }
1492
1493 unsafe fn remove_env(key: &str) {
1494 unsafe {
1495 std::env::remove_var(key);
1496 }
1497 }
1498
1499 #[test]
1500 fn sampler_from_env_reads_traceidratio() {
1501 let _lock = ENV_LOCK.lock().unwrap();
1502 unsafe {
1503 set_env("OTEL_TRACES_SAMPLER", "traceidratio");
1504 set_env("OTEL_TRACES_SAMPLER_ARG", "0.42");
1505 }
1506
1507 let sampler = sampler_from_env()
1508 .expect("should not error")
1509 .expect("should return Some");
1510 assert!(
1511 matches!(sampler, TraceSampler::TraceIdRatio(r) if (r - 0.42).abs() < f64::EPSILON)
1512 );
1513
1514 unsafe {
1515 remove_env("OTEL_TRACES_SAMPLER");
1516 remove_env("OTEL_TRACES_SAMPLER_ARG");
1517 }
1518 }
1519
1520 #[test]
1521 fn sampler_from_env_returns_none_when_unset() {
1522 let _lock = ENV_LOCK.lock().unwrap();
1523 unsafe {
1524 remove_env("OTEL_TRACES_SAMPLER");
1525 }
1526 assert!(sampler_from_env().expect("should not error").is_none());
1527 }
1528
1529 #[test]
1530 fn sampler_from_env_reads_parentbased_traceidratio() {
1531 let _lock = ENV_LOCK.lock().unwrap();
1532 unsafe {
1533 set_env("OTEL_TRACES_SAMPLER", "parentbased_traceidratio");
1534 set_env("OTEL_TRACES_SAMPLER_ARG", "0.1");
1535 }
1536
1537 let sampler = sampler_from_env()
1538 .expect("should not error")
1539 .expect("should return Some");
1540 assert!(
1541 matches!(sampler, TraceSampler::ParentBased(inner) if matches!(*inner, TraceSampler::TraceIdRatio(r) if (r - 0.1).abs() < f64::EPSILON))
1542 );
1543
1544 unsafe {
1545 remove_env("OTEL_TRACES_SAMPLER");
1546 remove_env("OTEL_TRACES_SAMPLER_ARG");
1547 }
1548 }
1549
1550 #[test]
1551 fn sampler_from_env_parentbased_always_on() {
1552 let _lock = ENV_LOCK.lock().unwrap();
1553 unsafe {
1554 set_env("OTEL_TRACES_SAMPLER", "parentbased_always_on");
1555 }
1556 let sampler = sampler_from_env()
1557 .expect("should not error")
1558 .expect("should return Some");
1559 assert!(
1560 matches!(sampler, TraceSampler::ParentBased(inner) if matches!(*inner, TraceSampler::AlwaysOn))
1561 );
1562 unsafe {
1563 remove_env("OTEL_TRACES_SAMPLER");
1564 }
1565 }
1566
1567 #[test]
1568 fn sampler_from_env_parentbased_always_off() {
1569 let _lock = ENV_LOCK.lock().unwrap();
1570 unsafe {
1571 set_env("OTEL_TRACES_SAMPLER", "parentbased_always_off");
1572 }
1573 let sampler = sampler_from_env()
1574 .expect("should not error")
1575 .expect("should return Some");
1576 assert!(
1577 matches!(sampler, TraceSampler::ParentBased(inner) if matches!(*inner, TraceSampler::AlwaysOff))
1578 );
1579 unsafe {
1580 remove_env("OTEL_TRACES_SAMPLER");
1581 }
1582 }
1583
1584 #[test]
1585 fn sampler_from_env_always_on() {
1586 let _lock = ENV_LOCK.lock().unwrap();
1587 unsafe {
1588 set_env("OTEL_TRACES_SAMPLER", "always_on");
1589 }
1590 let sampler = sampler_from_env()
1591 .expect("should not error")
1592 .expect("should return Some");
1593 assert!(matches!(sampler, TraceSampler::AlwaysOn));
1594 unsafe {
1595 remove_env("OTEL_TRACES_SAMPLER");
1596 }
1597 }
1598
1599 #[test]
1600 fn sampler_from_env_always_off() {
1601 let _lock = ENV_LOCK.lock().unwrap();
1602 unsafe {
1603 set_env("OTEL_TRACES_SAMPLER", "always_off");
1604 }
1605 let sampler = sampler_from_env()
1606 .expect("should not error")
1607 .expect("should return Some");
1608 assert!(matches!(sampler, TraceSampler::AlwaysOff));
1609 unsafe {
1610 remove_env("OTEL_TRACES_SAMPLER");
1611 }
1612 }
1613
1614 #[test]
1615 fn sampler_from_env_unknown_returns_error() {
1616 let _lock = ENV_LOCK.lock().unwrap();
1617 unsafe {
1618 set_env("OTEL_TRACES_SAMPLER", "unknown_sampler");
1619 }
1620 let err = sampler_from_env().expect_err("unknown sampler should produce an error");
1621 assert!(
1622 err.to_string().contains("unknown_sampler"),
1623 "error message should include the unknown name, got: {err}"
1624 );
1625 unsafe {
1626 remove_env("OTEL_TRACES_SAMPLER");
1627 }
1628 }
1629
1630 #[test]
1631 fn trace_sampler_always_on_converts_to_sdk() {
1632 let sdk = TraceSampler::AlwaysOn.into_sdk_sampler();
1633 assert_eq!(format!("{sdk:?}"), "AlwaysOn");
1634 }
1635
1636 #[test]
1637 fn trace_sampler_always_off_converts_to_sdk() {
1638 let sdk = TraceSampler::AlwaysOff.into_sdk_sampler();
1639 assert_eq!(format!("{sdk:?}"), "AlwaysOff");
1640 }
1641
1642 #[test]
1643 fn builder_has_sensible_defaults() {
1644 let builder = Telemetry::builder("test-svc");
1645 assert_eq!(builder.service_name.as_deref(), Some("test-svc"));
1646 assert!(builder.service_version.is_none());
1647 assert!(builder.deployment_environment.is_none());
1648 assert!(builder.sampler.is_none());
1649 assert!(builder.metrics);
1650 assert!(!builder.logs);
1651 assert!(builder.protocol.is_none());
1652 assert!(builder.max_export_batch_size.is_none());
1653 assert!(builder.metric_export_interval.is_none());
1654 assert!(builder.export_timeout.is_none());
1655 }
1656
1657 #[test]
1658 fn from_env_builder_has_no_service_name() {
1659 let builder = Telemetry::from_env();
1660 assert!(builder.service_name.is_none());
1661 }
1662
1663 #[test]
1664 fn with_export_timeout_stores_value() {
1665 let timeout = Duration::from_secs(5);
1666 let builder = Telemetry::builder("test-svc").with_export_timeout(timeout);
1667 assert_eq!(builder.export_timeout, Some(timeout));
1668 }
1669
1670 #[test]
1671 fn timeout_from_env_reads_milliseconds() {
1672 let _lock = ENV_LOCK.lock().unwrap();
1673 unsafe {
1674 set_env("OTEL_EXPORTER_OTLP_TIMEOUT", "5000");
1675 }
1676 let t = timeout_from_env();
1677 assert_eq!(t, Some(Duration::from_millis(5000)));
1678 unsafe {
1679 remove_env("OTEL_EXPORTER_OTLP_TIMEOUT");
1680 }
1681 }
1682
1683 #[test]
1684 fn timeout_from_env_returns_none_when_unset() {
1685 let _lock = ENV_LOCK.lock().unwrap();
1686 unsafe {
1687 remove_env("OTEL_EXPORTER_OTLP_TIMEOUT");
1688 }
1689 assert_eq!(timeout_from_env(), None);
1690 }
1691
1692 #[test]
1693 fn service_name_from_env_used_when_none_given() {
1694 let builder = Telemetry::from_env();
1695 assert!(builder.service_name.is_none());
1696 }
1697
1698 #[test]
1699 fn explicit_service_name_overrides_env_var() {
1700 let builder = Telemetry::builder("explicit-svc");
1701 assert_eq!(builder.service_name.as_deref(), Some("explicit-svc"));
1702 }
1703
1704 #[test]
1705 fn from_env_builder_service_name_is_none() {
1706 let builder = Telemetry::from_env();
1707 assert!(builder.service_name.is_none());
1708 }
1709
1710 #[test]
1711 fn init_returns_error_for_unknown_otel_traces_sampler() {
1712 let _lock = ENV_LOCK.lock().unwrap();
1713 unsafe {
1714 set_env("OTEL_TRACES_SAMPLER", "not_a_real_sampler");
1715 }
1716 let result = Telemetry::builder("test-svc").with_metrics(false).init();
1717 let err = result
1718 .err()
1719 .expect("unknown sampler env var should cause init to fail");
1720 assert!(
1721 err.to_string().contains("not_a_real_sampler"),
1722 "error should name the unknown sampler, got: {err}"
1723 );
1724 unsafe {
1725 remove_env("OTEL_TRACES_SAMPLER");
1726 }
1727 }
1728
1729 #[test]
1730 fn with_max_export_batch_size_stores_value() {
1731 let builder = Telemetry::builder("test-svc").with_max_export_batch_size(1024);
1732 assert_eq!(builder.max_export_batch_size, Some(1024));
1733 }
1734
1735 #[test]
1736 fn with_metric_export_interval_stores_value() {
1737 let interval = Duration::from_secs(30);
1738 let builder = Telemetry::builder("test-svc").with_metric_export_interval(interval);
1739 assert_eq!(builder.metric_export_interval, Some(interval));
1740 }
1741
1742 #[test]
1743 fn init_rejects_zero_metric_export_interval() {
1744 let err = Telemetry::builder("test-svc")
1745 .with_metric_export_interval(Duration::ZERO)
1746 .with_metrics(false)
1747 .init()
1748 .err()
1749 .expect("expected error for zero interval");
1750 assert!(
1751 err.to_string().contains("metric_export_interval"),
1752 "error message should mention metric_export_interval, got: {err}"
1753 );
1754 }
1755
1756 #[test]
1757 fn builder_with_custom_values() {
1758 let builder = Telemetry::builder("test-svc")
1759 .with_version("2.0.0")
1760 .with_environment("production")
1761 .with_sampler(TraceSampler::TraceIdRatio(0.5))
1762 .with_metrics(false);
1763
1764 assert_eq!(builder.service_name.as_deref(), Some("test-svc"));
1765 assert_eq!(builder.service_version.as_deref(), Some("2.0.0"));
1766 assert_eq!(
1767 builder.deployment_environment.as_deref(),
1768 Some("production")
1769 );
1770 assert!(
1771 matches!(builder.sampler, Some(TraceSampler::TraceIdRatio(r)) if (r - 0.5).abs() < f64::EPSILON)
1772 );
1773 assert!(!builder.metrics);
1774 }
1775
1776 #[test]
1777 fn builder_stores_programmatic_log_configuration() {
1778 let builder = Telemetry::builder("test-svc")
1779 .with_log_filter("info,opentelemetry_sdk=warn")
1780 .with_log_format(LogFormat::Json);
1781
1782 assert_eq!(
1783 builder.log_filter.as_deref(),
1784 Some("info,opentelemetry_sdk=warn")
1785 );
1786 assert_eq!(builder.log_format, LogFormat::Json);
1787 }
1788
1789 #[test]
1790 fn init_rejects_invalid_programmatic_log_filter_before_provider_setup() {
1791 let setup_ran = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
1792 let setup_ran_in_closure = std::sync::Arc::clone(&setup_ran);
1793
1794 let error = Telemetry::builder("test-svc")
1795 .with_log_filter("[")
1796 .with_meter_provider_setup(move |builder| {
1797 setup_ran_in_closure.store(true, std::sync::atomic::Ordering::SeqCst);
1798 builder
1799 })
1800 .init()
1801 .err()
1802 .expect("invalid filter must fail initialization");
1803
1804 assert!(error.to_string().contains("invalid filter directive"));
1805 assert!(!setup_ran.load(std::sync::atomic::Ordering::SeqCst));
1806 }
1807
1808 #[test]
1809 fn builder_with_default_endpoint() {
1810 let builder = Telemetry::builder("svc").with_default_endpoint("http://otel-collector:4317");
1811 assert_eq!(
1812 builder.default_endpoint.as_deref(),
1813 Some("http://otel-collector:4317")
1814 );
1815 }
1816
1817 #[test]
1818 fn a_configured_endpoint_wins_over_the_runtime_default() {
1819 assert_eq!(
1820 resolve_endpoint(
1821 Some("http://c:4317".into()),
1822 Some("http://d:4317"),
1823 "http://localhost:4317"
1824 ),
1825 "http://c:4317"
1826 );
1827 assert_eq!(
1828 resolve_endpoint(None, Some("http://d:4317"), "http://localhost:4317"),
1829 "http://d:4317"
1830 );
1831 assert_eq!(
1832 resolve_endpoint(None, None, "http://localhost:4317"),
1833 "http://localhost:4317"
1834 );
1835 }
1836
1837 #[test]
1838 #[cfg(feature = "grpc")]
1839 fn builder_with_protocol_grpc() {
1840 let builder = Telemetry::builder("test-svc").with_protocol(ExportProtocol::Grpc);
1841 assert_eq!(builder.protocol, Some(ExportProtocol::Grpc));
1842 }
1843
1844 #[test]
1845 #[cfg(feature = "http")]
1846 fn builder_with_protocol_http() {
1847 let builder = Telemetry::builder("test-svc").with_protocol(ExportProtocol::HttpProtobuf);
1848 assert_eq!(builder.protocol, Some(ExportProtocol::HttpProtobuf));
1849 }
1850
1851 #[test]
1852 #[cfg(feature = "grpc")]
1853 fn protocol_from_env_reads_grpc() {
1854 let _lock = ENV_LOCK.lock().unwrap();
1855 unsafe {
1856 set_env("OTEL_EXPORTER_OTLP_PROTOCOL", "grpc");
1857 }
1858 assert_eq!(protocol_from_env(), Some(ExportProtocol::Grpc));
1859 unsafe {
1860 remove_env("OTEL_EXPORTER_OTLP_PROTOCOL");
1861 }
1862 }
1863
1864 #[test]
1865 #[cfg(feature = "http")]
1866 fn protocol_from_env_reads_http_protobuf() {
1867 let _lock = ENV_LOCK.lock().unwrap();
1868 unsafe {
1869 set_env("OTEL_EXPORTER_OTLP_PROTOCOL", "http/protobuf");
1870 }
1871 assert_eq!(protocol_from_env(), Some(ExportProtocol::HttpProtobuf));
1872 unsafe {
1873 remove_env("OTEL_EXPORTER_OTLP_PROTOCOL");
1874 }
1875 }
1876
1877 #[test]
1878 fn protocol_from_env_returns_none_when_unset() {
1879 let _lock = ENV_LOCK.lock().unwrap();
1880 unsafe {
1881 remove_env("OTEL_EXPORTER_OTLP_PROTOCOL");
1882 }
1883 assert_eq!(protocol_from_env(), None);
1884 }
1885
1886 #[test]
1887 fn protocol_from_env_returns_none_for_unknown() {
1888 let _lock = ENV_LOCK.lock().unwrap();
1889 unsafe {
1890 set_env("OTEL_EXPORTER_OTLP_PROTOCOL", "websocket");
1891 }
1892 assert_eq!(protocol_from_env(), None);
1893 unsafe {
1894 remove_env("OTEL_EXPORTER_OTLP_PROTOCOL");
1895 }
1896 }
1897
1898 #[test]
1899 fn builder_is_send_and_sync() {
1900 fn assert_send_sync<T: Send + Sync>() {}
1901 assert_send_sync::<TelemetryBuilder>();
1902 }
1903
1904 #[test]
1905 fn with_shutdown_timeout_stores_value() {
1906 let timeout = Duration::from_secs(10);
1907 let builder = Telemetry::builder("test-svc").with_shutdown_timeout(timeout);
1908 assert_eq!(builder.shutdown_timeout, timeout);
1909 }
1910
1911 #[test]
1912 fn default_shutdown_timeout_is_five_seconds() {
1913 let builder = Telemetry::builder("test-svc");
1914 assert_eq!(builder.shutdown_timeout, Duration::from_secs(5));
1915 }
1916
1917 #[cfg(feature = "testing")]
1925 #[test]
1926 fn drop_completes_within_shutdown_timeout() {
1927 let mut handles = crate::Telemetry::testing("drop-timeout-test");
1929 handles.shutdown_timeout = Duration::from_millis(100);
1931
1932 let start = std::time::Instant::now();
1933 drop(handles);
1934 let elapsed = start.elapsed();
1935
1936 assert!(
1938 elapsed < Duration::from_millis(500),
1939 "drop took {elapsed:?}, expected < 500 ms"
1940 );
1941 }
1942}