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 boot;
43pub mod export_backoff;
44pub mod instrumented_port;
45pub mod log_bridge;
46pub mod span_enrichment;
47pub mod spanned;
48
49pub use instrumented_port::{Instrumented, InstrumentedArc};
50pub use log_bridge::{
51 PROPAGATED_SPAN_FIELDS, SpanLogAttrs, record_span_log_attr, record_span_log_attr_on,
52};
53pub use spanned::{Spanned, in_span};
54
55use opentelemetry::KeyValue;
56use opentelemetry::propagation::TextMapCompositePropagator;
57use opentelemetry_otlp::WithExportConfig;
58use opentelemetry_sdk::{
59 Resource,
60 logs::SdkLoggerProvider,
61 metrics::{MeterProviderBuilder, PeriodicReader, SdkMeterProvider},
62 propagation::{BaggagePropagator, TraceContextPropagator},
63 trace::{BatchConfigBuilder, BatchSpanProcessor, Sampler, SdkTracer, SdkTracerProvider},
64};
65use opentelemetry_semantic_conventions::attribute::{
66 DEPLOYMENT_ENVIRONMENT_NAME, HOST_NAME, PROCESS_PID, SERVICE_VERSION,
67};
68use std::error::Error;
69use std::time::Duration;
70use tracing_subscriber::layer::SubscriberExt;
71use tracing_subscriber::util::SubscriberInitExt;
72
73fn tracing_bridge_tracer(provider: &SdkTracerProvider) -> SdkTracer {
74 use opentelemetry::trace::TracerProvider as _;
75
76 provider.tracer(env!("CARGO_PKG_NAME"))
77}
78
79#[derive(Debug, Clone)]
94pub enum TraceSampler {
95 AlwaysOn,
97 AlwaysOff,
99 TraceIdRatio(f64),
101 ParentBased(Box<TraceSampler>),
104}
105
106#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
108pub enum LogFormat {
109 #[default]
111 Pretty,
112 Json,
114}
115
116impl TraceSampler {
117 fn into_sdk_sampler(self) -> Sampler {
119 match self {
120 TraceSampler::AlwaysOn => Sampler::AlwaysOn,
121 TraceSampler::AlwaysOff => Sampler::AlwaysOff,
122 TraceSampler::TraceIdRatio(r) => Sampler::TraceIdRatioBased(r),
123 TraceSampler::ParentBased(inner) => {
124 Sampler::ParentBased(Box::new(inner.into_sdk_sampler()))
125 }
126 }
127 }
128}
129
130fn sampler_from_env() -> Result<Option<TraceSampler>, Box<dyn Error>> {
138 let name = match std::env::var("OTEL_TRACES_SAMPLER") {
139 Ok(v) => v,
140 Err(_) => return Ok(None),
141 };
142 let arg = std::env::var("OTEL_TRACES_SAMPLER_ARG").ok();
143 let sampler = match name.as_str() {
144 "always_on" => TraceSampler::AlwaysOn,
145 "always_off" => TraceSampler::AlwaysOff,
146 "traceidratio" => {
147 let ratio = arg
148 .as_deref()
149 .unwrap_or("1.0")
150 .parse::<f64>()
151 .unwrap_or(1.0);
152 TraceSampler::TraceIdRatio(ratio)
153 }
154 "parentbased_always_on" => TraceSampler::ParentBased(Box::new(TraceSampler::AlwaysOn)),
155 "parentbased_always_off" => TraceSampler::ParentBased(Box::new(TraceSampler::AlwaysOff)),
156 "parentbased_traceidratio" => {
157 let ratio = arg
158 .as_deref()
159 .unwrap_or("1.0")
160 .parse::<f64>()
161 .unwrap_or(1.0);
162 TraceSampler::ParentBased(Box::new(TraceSampler::TraceIdRatio(ratio)))
163 }
164 unknown => {
165 return Err(format!(
166 "OTEL_TRACES_SAMPLER: unrecognised sampler name '{unknown}'. \
167 Valid values: always_on, always_off, traceidratio, \
168 parentbased_always_on, parentbased_always_off, parentbased_traceidratio"
169 )
170 .into());
171 }
172 };
173 Ok(Some(sampler))
174}
175
176const DEFAULT_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5);
178
179pub struct TelemetryHandles {
200 pub tracer_provider: SdkTracerProvider,
201 pub meter_provider: Option<SdkMeterProvider>,
202 pub logger_provider: Option<SdkLoggerProvider>,
203 shutdown_timeout: Duration,
204 boot_owner: bool,
207 #[cfg(feature = "profiling")]
208 pub profiling_handle: Option<profiling::ProfilingHandle>,
209}
210
211impl TelemetryHandles {
212 pub fn shutdown(&self) -> Result<(), Box<dyn Error>> {
243 self.close_boot();
244 if let Err(e) = self.tracer_provider.shutdown() {
245 tracing::warn!("tracer provider shutdown error: {e}");
246 }
247 if let Some(mp) = &self.meter_provider
248 && let Err(e) = mp.shutdown()
249 {
250 tracing::warn!("meter provider shutdown error: {e}");
251 }
252 if let Some(lp) = &self.logger_provider
253 && let Err(e) = lp.shutdown()
254 {
255 tracing::warn!("logger provider shutdown error: {e}");
256 }
257 Ok(())
258 }
259
260 fn close_boot(&self) {
262 if self.boot_owner {
263 boot::Timeline::global().close();
264 }
265 }
266}
267
268impl Drop for TelemetryHandles {
269 fn drop(&mut self) {
270 self.close_boot();
271 let tracer_provider = self.tracer_provider.clone();
272 let meter_provider = self.meter_provider.clone();
273 let logger_provider = self.logger_provider.clone();
274 let timeout = self.shutdown_timeout;
275
276 let (tx, rx) = std::sync::mpsc::channel();
277 std::thread::spawn(move || {
278 if let Err(e) = tracer_provider.shutdown() {
279 tracing::warn!("tracer provider shutdown error: {e}");
280 }
281 if let Some(mp) = meter_provider
282 && let Err(e) = mp.shutdown()
283 {
284 tracing::warn!("meter provider shutdown error: {e}");
285 }
286 if let Some(lp) = logger_provider
287 && let Err(e) = lp.shutdown()
288 {
289 tracing::warn!("logger provider shutdown error: {e}");
290 }
291 let _ = tx.send(());
292 });
293
294 if rx.recv_timeout(timeout).is_err() {
295 tracing::warn!(
296 "telemetry shutdown did not complete within {timeout:?}; \
297 some spans/metrics may not have been exported"
298 );
299 }
300 }
301}
302
303#[derive(Debug, Clone, Copy, PartialEq, Eq)]
326pub enum ExportProtocol {
327 #[cfg(feature = "grpc")]
329 Grpc,
330 #[cfg(feature = "http")]
332 HttpProtobuf,
333}
334
335#[cfg(feature = "grpc-mtls")]
345#[derive(Clone)]
346pub struct MtlsMaterial {
347 pub client_cert_chain_pem: Vec<u8>,
349 pub client_key_pem: Vec<u8>,
351 pub trust_bundle_pem: Vec<u8>,
353}
354
355#[cfg(feature = "grpc-mtls")]
356impl std::fmt::Debug for MtlsMaterial {
357 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
358 f.debug_struct("MtlsMaterial")
359 .field("client_cert_chain_pem", &"<redacted>")
360 .field("client_key_pem", &"<redacted>")
361 .field("trust_bundle_pem", &"<redacted>")
362 .finish()
363 }
364}
365
366fn protocol_from_env() -> Option<ExportProtocol> {
368 let val = std::env::var("OTEL_EXPORTER_OTLP_PROTOCOL").ok()?;
369 match val.trim() {
370 #[cfg(feature = "grpc")]
371 "grpc" => Some(ExportProtocol::Grpc),
372 #[cfg(feature = "http")]
373 "http/protobuf" => Some(ExportProtocol::HttpProtobuf),
374 _ => None,
375 }
376}
377
378pub struct Telemetry;
394
395impl Telemetry {
396 pub fn builder(service_name: &str) -> TelemetryBuilder {
400 TelemetryBuilder {
401 service_name: Some(service_name.to_string()),
402 service_version: None,
403 deployment_environment: None,
404 sampler: None,
405 metrics: true,
406 logs: false,
407 protocol: None,
408 default_endpoint: None,
409 max_export_batch_size: None,
410 metric_export_interval: None,
411 export_timeout: None,
412 shutdown_timeout: DEFAULT_SHUTDOWN_TIMEOUT,
413 log_filter: None,
414 log_format: LogFormat::default(),
415 extra_layers: Vec::new(),
416 extra_metric_readers: Vec::new(),
417 runtime_metrics: true,
418 #[cfg(feature = "grpc-mtls")]
419 mtls: None,
420 propagated_span_fields: crate::log_bridge::PROPAGATED_SPAN_FIELDS,
421 #[cfg(feature = "profiling")]
422 pyroscope_endpoint: None,
423 }
424 }
425
426 pub fn from_env() -> TelemetryBuilder {
436 TelemetryBuilder {
437 service_name: None,
438 service_version: None,
439 deployment_environment: None,
440 sampler: None,
441 metrics: true,
442 logs: false,
443 protocol: None,
444 default_endpoint: None,
445 max_export_batch_size: None,
446 metric_export_interval: None,
447 export_timeout: None,
448 shutdown_timeout: DEFAULT_SHUTDOWN_TIMEOUT,
449 log_filter: None,
450 log_format: LogFormat::default(),
451 extra_layers: Vec::new(),
452 extra_metric_readers: Vec::new(),
453 runtime_metrics: true,
454 #[cfg(feature = "grpc-mtls")]
455 mtls: None,
456 propagated_span_fields: crate::log_bridge::PROPAGATED_SPAN_FIELDS,
457 #[cfg(feature = "profiling")]
458 pyroscope_endpoint: None,
459 }
460 }
461}
462
463#[must_use = "a TelemetryBuilder does nothing until .init() is called"]
481pub struct TelemetryBuilder {
482 service_name: Option<String>,
483 service_version: Option<String>,
484 deployment_environment: Option<String>,
485 sampler: Option<TraceSampler>,
486 metrics: bool,
487 logs: bool,
488 protocol: Option<ExportProtocol>,
489 max_export_batch_size: Option<usize>,
490 metric_export_interval: Option<Duration>,
491 export_timeout: Option<Duration>,
492 shutdown_timeout: Duration,
493 log_filter: Option<String>,
494 log_format: LogFormat,
495 extra_layers: Vec<
496 Box<dyn tracing_subscriber::Layer<tracing_subscriber::Registry> + Send + Sync + 'static>,
497 >,
498 extra_metric_readers: Vec<MeterProviderInstaller>,
499 runtime_metrics: bool,
500 #[cfg(feature = "grpc-mtls")]
501 mtls: Option<MtlsMaterial>,
502 propagated_span_fields: &'static [&'static str],
503 #[cfg(feature = "profiling")]
504 pyroscope_endpoint: Option<String>,
505 default_endpoint: Option<String>,
506}
507
508type MeterProviderInstaller =
513 Box<dyn FnOnce(MeterProviderBuilder) -> MeterProviderBuilder + Send + Sync>;
514
515impl TelemetryBuilder {
516 pub fn with_log_filter(mut self, directive: impl Into<String>) -> Self {
521 self.log_filter = Some(directive.into());
522 self
523 }
524
525 pub fn with_log_format(mut self, format: LogFormat) -> Self {
527 self.log_format = format;
528 self
529 }
530
531 pub fn with_version(mut self, version: &str) -> Self {
533 self.service_version = Some(version.to_string());
534 self
535 }
536
537 pub fn with_environment(mut self, environment: &str) -> Self {
539 self.deployment_environment = Some(environment.to_string());
540 self
541 }
542
543 #[cfg(feature = "grpc-mtls")]
568 pub fn with_mtls(mut self, material: MtlsMaterial) -> Self {
569 self.mtls = Some(material);
570 self.protocol = Some(ExportProtocol::Grpc);
571 self
572 }
573
574 pub fn with_sampler(mut self, sampler: TraceSampler) -> Self {
577 self.sampler = Some(sampler);
578 self
579 }
580
581 pub fn with_metrics(mut self, enabled: bool) -> Self {
583 self.metrics = enabled;
584 self
585 }
586
587 pub fn with_runtime_metrics(mut self, enabled: bool) -> Self {
603 self.runtime_metrics = enabled;
604 self
605 }
606
607 pub fn with_default_endpoint(mut self, endpoint: impl Into<String>) -> Self {
612 self.default_endpoint = Some(endpoint.into());
613 self
614 }
615
616 pub fn with_protocol(mut self, protocol: ExportProtocol) -> Self {
620 self.protocol = Some(protocol);
621 self
622 }
623
624 pub fn with_max_export_batch_size(mut self, size: usize) -> Self {
629 self.max_export_batch_size = Some(size);
630 self
631 }
632
633 pub fn with_metric_export_interval(mut self, interval: Duration) -> Self {
638 self.metric_export_interval = Some(interval);
639 self
640 }
641
642 pub fn with_logs(mut self, enabled: bool) -> Self {
649 self.logs = enabled;
650 self
651 }
652
653 pub fn with_propagated_span_fields(mut self, fields: &'static [&'static str]) -> Self {
666 self.propagated_span_fields = fields;
667 self
668 }
669
670 pub fn with_export_timeout(mut self, timeout: Duration) -> Self {
674 self.export_timeout = Some(timeout);
675 self
676 }
677
678 pub fn with_shutdown_timeout(mut self, timeout: Duration) -> Self {
685 self.shutdown_timeout = timeout;
686 self
687 }
688
689 #[cfg(feature = "profiling")]
707 pub fn with_profiling(mut self, endpoint: &str) -> Self {
708 self.pyroscope_endpoint = Some(endpoint.to_string());
709 self
710 }
711
712 pub fn with_meter_provider_setup<F>(mut self, setup: F) -> Self
769 where
770 F: FnOnce(MeterProviderBuilder) -> MeterProviderBuilder + Send + Sync + 'static,
771 {
772 self.extra_metric_readers.push(Box::new(setup));
773 self
774 }
775
776 pub fn with_layer<L>(mut self, layer: L) -> Self
777 where
778 L: tracing_subscriber::Layer<tracing_subscriber::Registry> + Send + Sync + 'static,
779 {
780 self.extra_layers.push(Box::new(layer));
781 self
782 }
783
784 pub fn init(self) -> Result<TelemetryHandles, Box<dyn Error>> {
799 let log_filter = match self.log_filter.as_deref() {
800 Some(directive) => tracing_subscriber::EnvFilter::try_new(directive)?,
801 None => tracing_subscriber::EnvFilter::from_default_env(),
802 };
803
804 if let Some(interval) = self.metric_export_interval
805 && interval.is_zero()
806 {
807 return Err("metric_export_interval must be greater than zero".into());
808 }
809
810 let protocol = self.protocol.or_else(protocol_from_env).unwrap_or({
811 #[cfg(feature = "grpc")]
812 {
813 ExportProtocol::Grpc
814 }
815 #[cfg(all(not(feature = "grpc"), feature = "http"))]
816 {
817 ExportProtocol::HttpProtobuf
818 }
819 });
820
821 let default_endpoint = match protocol {
822 #[cfg(feature = "grpc")]
823 ExportProtocol::Grpc => "http://localhost:4317",
824 #[cfg(feature = "http")]
825 ExportProtocol::HttpProtobuf => "http://localhost:4318",
826 };
827 let endpoint = resolve_endpoint(
828 std::env::var("OTEL_EXPORTER_OTLP_ENDPOINT").ok(),
829 self.default_endpoint.as_deref(),
830 default_endpoint,
831 );
832
833 let export_timeout = self.export_timeout.or_else(timeout_from_env);
835
836 let service_name = self.service_name.unwrap_or_else(|| {
838 std::env::var("OTEL_SERVICE_NAME").unwrap_or_else(|_| "unknown_service".to_string())
839 });
840
841 let resource = build_resource(
842 &service_name,
843 self.service_version.as_deref(),
844 self.deployment_environment.as_deref(),
845 );
846
847 let sampler = match self.sampler {
848 Some(s) => s,
849 None => sampler_from_env()?.unwrap_or(TraceSampler::AlwaysOn),
850 };
851
852 let trace_exporter = build_span_exporter(
854 protocol,
855 &endpoint,
856 export_timeout,
857 #[cfg(feature = "grpc-mtls")]
858 self.mtls.as_ref(),
859 )?;
860
861 let batch_processor = if let Some(size) = self.max_export_batch_size {
862 BatchSpanProcessor::builder(trace_exporter)
863 .with_batch_config(
864 BatchConfigBuilder::default()
865 .with_max_export_batch_size(size)
866 .build(),
867 )
868 .build()
869 } else {
870 BatchSpanProcessor::builder(trace_exporter).build()
871 };
872
873 let tracer_provider = SdkTracerProvider::builder()
874 .with_resource(resource.clone())
875 .with_sampler(sampler.into_sdk_sampler())
876 .with_span_processor(batch_processor)
877 .build();
878
879 opentelemetry::global::set_tracer_provider(tracer_provider.clone());
880
881 let propagator = TextMapCompositePropagator::new(vec![
883 Box::new(TraceContextPropagator::new()),
884 Box::new(BaggagePropagator::new()),
885 ]);
886 opentelemetry::global::set_text_map_propagator(propagator);
887
888 let meter_provider = if self.metrics {
890 let metric_exporter = build_metric_exporter(
891 protocol,
892 &endpoint,
893 export_timeout,
894 #[cfg(feature = "grpc-mtls")]
895 self.mtls.as_ref(),
896 )?;
897
898 let periodic_reader = if let Some(interval) = self.metric_export_interval {
899 PeriodicReader::builder(metric_exporter)
900 .with_interval(interval)
901 .build()
902 } else {
903 PeriodicReader::builder(metric_exporter).build()
904 };
905
906 let mut mp_builder = SdkMeterProvider::builder()
907 .with_resource(resource.clone())
908 .with_reader(periodic_reader);
909 for installer in self.extra_metric_readers {
910 mp_builder = installer(mp_builder);
911 }
912 let mp = mp_builder.build();
913
914 opentelemetry::global::set_meter_provider(mp.clone());
915
916 if self.runtime_metrics {
920 crate::runtime_metrics::install();
921 }
922
923 Some(mp)
924 } else {
925 None
926 };
927
928 let logger_provider = if self.logs {
930 let log_exporter = build_log_exporter(
931 protocol,
932 &endpoint,
933 export_timeout,
934 #[cfg(feature = "grpc-mtls")]
935 self.mtls.as_ref(),
936 )?;
937
938 let lp = SdkLoggerProvider::builder()
939 .with_resource(resource)
940 .with_batch_exporter(log_exporter)
941 .build();
942
943 Some(lp)
944 } else {
945 None
946 };
947
948 #[cfg(feature = "profiling")]
950 let profiling_handle = if let Some(ref endpoint) = self.pyroscope_endpoint {
951 let identity = profiling::ProfilingIdentity {
956 host_name: hostname::get()
957 .ok()
958 .and_then(|h| h.into_string().ok())
959 .filter(|h| !h.is_empty()),
960 deployment_environment: self.deployment_environment.clone(),
961 service_version: self.service_version.clone(),
962 };
963 profiling::start_pyroscope_bridge(&service_name, endpoint, &identity)?
964 } else {
965 None
966 };
967 #[cfg(not(feature = "profiling"))]
968 let _profiling_handle: Option<()> = None;
969
970 let extra = if self.extra_layers.is_empty() {
976 None
977 } else {
978 Some(self.extra_layers)
979 };
980
981 macro_rules! install_subscriber {
982 ($fmt_layer:expr) => {{
983 let otel_layer = tracing_opentelemetry::layer()
987 .with_tracer(tracing_bridge_tracer(&tracer_provider));
988 let registry = tracing_subscriber::registry()
991 .with(extra)
992 .with(crate::export_backoff::ExportFailureBackoff::default())
993 .with(log_filter)
994 .with($fmt_layer)
995 .with(otel_layer);
996
997 #[cfg(feature = "profiling-bridge-pyroscope-rs")]
1000 #[allow(deprecated)]
1001 let registry = registry.with(crate::profiling::ProfilingTagLayer);
1002
1003 if let Some(lp) = &logger_provider {
1004 if let Err(e) = registry
1005 .with(crate::log_bridge::SpanAwareLogBridge::new(
1006 lp,
1007 self.propagated_span_fields,
1008 ))
1009 .try_init()
1010 {
1011 eprintln!(
1012 "otel-bootstrap: global tracing subscriber already installed — \
1013 OTLP log records will NOT be exported to the collector: {e}"
1014 );
1015 }
1016 } else if let Err(e) = registry.try_init() {
1017 eprintln!(
1018 "otel-bootstrap: global tracing subscriber already installed — \
1019 OTLP telemetry will NOT be exported to the collector: {e}"
1020 );
1021 }
1022 }};
1023 }
1024
1025 match self.log_format {
1026 LogFormat::Pretty => install_subscriber!(tracing_subscriber::fmt::layer()),
1027 LogFormat::Json => install_subscriber!(tracing_subscriber::fmt::layer().json()),
1028 }
1029
1030 let boot_owner = boot::Timeline::global()
1032 .attach(&tracer_provider, &service_name)
1033 .is_some();
1034
1035 Ok(TelemetryHandles {
1036 tracer_provider,
1037 meter_provider,
1038 logger_provider,
1039 shutdown_timeout: self.shutdown_timeout,
1040 boot_owner,
1041 #[cfg(feature = "profiling")]
1042 profiling_handle,
1043 })
1044 }
1045}
1046
1047pub fn init_telemetry(service_name: &str) -> Result<TelemetryHandles, Box<dyn Error>> {
1061 Telemetry::builder(service_name).init()
1062}
1063
1064pub fn init_telemetry_with_sampler(
1081 service_name: &str,
1082 sampler: Option<TraceSampler>,
1083) -> Result<TelemetryHandles, Box<dyn Error>> {
1084 let builder = Telemetry::builder(service_name);
1085 match sampler {
1086 Some(s) => builder.with_sampler(s),
1087 None => builder, }
1089 .init()
1090}
1091
1092fn timeout_from_env() -> Option<Duration> {
1094 let ms = std::env::var("OTEL_EXPORTER_OTLP_TIMEOUT").ok()?;
1095 let ms: u64 = ms.trim().parse().ok()?;
1096 Some(Duration::from_millis(ms))
1097}
1098
1099#[cfg(feature = "grpc-mtls")]
1106fn build_tls_config(material: &MtlsMaterial) -> tonic::transport::ClientTlsConfig {
1107 use tonic::transport::{Certificate, ClientTlsConfig, Identity};
1108 ClientTlsConfig::new()
1109 .ca_certificate(Certificate::from_pem(&material.trust_bundle_pem))
1110 .identity(Identity::from_pem(
1111 &material.client_cert_chain_pem,
1112 &material.client_key_pem,
1113 ))
1114}
1115
1116fn resolve_endpoint(
1119 configured: Option<String>,
1120 runtime_default: Option<&str>,
1121 fallback: &str,
1122) -> String {
1123 configured
1124 .or_else(|| runtime_default.map(str::to_owned))
1125 .unwrap_or_else(|| fallback.to_owned())
1126}
1127
1128fn build_span_exporter(
1129 protocol: ExportProtocol,
1130 endpoint: &str,
1131 timeout: Option<Duration>,
1132 #[cfg(feature = "grpc-mtls")] mtls: Option<&MtlsMaterial>,
1133) -> Result<opentelemetry_otlp::SpanExporter, Box<dyn Error>> {
1134 match protocol {
1135 #[cfg(feature = "grpc")]
1136 ExportProtocol::Grpc => {
1137 let mut b = opentelemetry_otlp::SpanExporter::builder()
1138 .with_tonic()
1139 .with_endpoint(endpoint);
1140 if let Some(t) = timeout {
1141 b = b.with_timeout(t);
1142 }
1143 #[cfg(feature = "grpc-mtls")]
1144 if let Some(m) = mtls {
1145 use opentelemetry_otlp::WithTonicConfig as _;
1146 b = b.with_tls_config(build_tls_config(m));
1147 }
1148 Ok(b.build()?)
1149 }
1150 #[cfg(feature = "http")]
1151 ExportProtocol::HttpProtobuf => {
1152 let mut b = opentelemetry_otlp::SpanExporter::builder()
1153 .with_http()
1154 .with_endpoint(endpoint);
1155 if let Some(t) = timeout {
1156 b = b.with_timeout(t);
1157 }
1158 Ok(b.build()?)
1159 }
1160 }
1161}
1162
1163fn build_metric_exporter(
1164 protocol: ExportProtocol,
1165 endpoint: &str,
1166 timeout: Option<Duration>,
1167 #[cfg(feature = "grpc-mtls")] mtls: Option<&MtlsMaterial>,
1168) -> Result<opentelemetry_otlp::MetricExporter, Box<dyn Error>> {
1169 match protocol {
1170 #[cfg(feature = "grpc")]
1171 ExportProtocol::Grpc => {
1172 let mut b = opentelemetry_otlp::MetricExporter::builder()
1173 .with_tonic()
1174 .with_endpoint(endpoint);
1175 if let Some(t) = timeout {
1176 b = b.with_timeout(t);
1177 }
1178 #[cfg(feature = "grpc-mtls")]
1179 if let Some(m) = mtls {
1180 use opentelemetry_otlp::WithTonicConfig as _;
1181 b = b.with_tls_config(build_tls_config(m));
1182 }
1183 Ok(b.build()?)
1184 }
1185 #[cfg(feature = "http")]
1186 ExportProtocol::HttpProtobuf => {
1187 let mut b = opentelemetry_otlp::MetricExporter::builder()
1188 .with_http()
1189 .with_endpoint(endpoint);
1190 if let Some(t) = timeout {
1191 b = b.with_timeout(t);
1192 }
1193 Ok(b.build()?)
1194 }
1195 }
1196}
1197
1198fn build_log_exporter(
1199 protocol: ExportProtocol,
1200 endpoint: &str,
1201 timeout: Option<Duration>,
1202 #[cfg(feature = "grpc-mtls")] mtls: Option<&MtlsMaterial>,
1203) -> Result<opentelemetry_otlp::LogExporter, Box<dyn Error>> {
1204 match protocol {
1205 #[cfg(feature = "grpc")]
1206 ExportProtocol::Grpc => {
1207 let mut b = opentelemetry_otlp::LogExporter::builder()
1208 .with_tonic()
1209 .with_endpoint(endpoint);
1210 if let Some(t) = timeout {
1211 b = b.with_timeout(t);
1212 }
1213 #[cfg(feature = "grpc-mtls")]
1214 if let Some(m) = mtls {
1215 use opentelemetry_otlp::WithTonicConfig as _;
1216 b = b.with_tls_config(build_tls_config(m));
1217 }
1218 Ok(b.build()?)
1219 }
1220 #[cfg(feature = "http")]
1221 ExportProtocol::HttpProtobuf => {
1222 let mut b = opentelemetry_otlp::LogExporter::builder()
1223 .with_http()
1224 .with_endpoint(endpoint);
1225 if let Some(t) = timeout {
1226 b = b.with_timeout(t);
1227 }
1228 Ok(b.build()?)
1229 }
1230 }
1231}
1232
1233pub fn build_resource(
1248 service_name: &str,
1249 service_version: Option<&str>,
1250 deployment_environment: Option<&str>,
1251) -> Resource {
1252 let hostname = hostname::get()
1253 .ok()
1254 .and_then(|h| h.into_string().ok())
1255 .unwrap_or_default();
1256
1257 let mut builder = Resource::builder()
1258 .with_service_name(service_name.to_string())
1259 .with_attributes([
1260 KeyValue::new(HOST_NAME, hostname),
1261 KeyValue::new(PROCESS_PID, std::process::id() as i64),
1262 ]);
1263
1264 if let Some(version) = service_version {
1265 builder = builder.with_attribute(KeyValue::new(SERVICE_VERSION, version.to_string()));
1266 }
1267
1268 if let Some(env) = deployment_environment {
1269 builder =
1270 builder.with_attribute(KeyValue::new(DEPLOYMENT_ENVIRONMENT_NAME, env.to_string()));
1271 }
1272
1273 builder.build()
1274}
1275
1276#[cfg(feature = "axum")]
1294pub fn axum_layer() -> axum_middleware::OtelTraceLayer {
1295 axum_middleware::OtelTraceLayer
1296}
1297
1298#[cfg(feature = "axum")]
1329pub fn span_enricher_layer<T>() -> axum_middleware::SpanEnricherLayer<T>
1330where
1331 T: span_enrichment::EnrichSpan + Clone + Send + Sync + 'static,
1332{
1333 axum_middleware::SpanEnricherLayer::default()
1334}
1335
1336#[cfg(feature = "tonic-tracing")]
1358pub fn grpc_client_layer() -> grpc_middleware::GrpcClientTraceLayer {
1359 grpc_middleware::GrpcClientTraceLayer
1360}
1361
1362#[cfg(feature = "tonic-tracing")]
1377pub fn grpc_server_layer() -> grpc_middleware::GrpcServerTraceLayer {
1378 grpc_middleware::GrpcServerTraceLayer
1379}
1380
1381#[cfg(test)]
1382mod tests {
1383 use super::*;
1384
1385 #[test]
1387 fn runtime_metrics_can_be_disabled() {
1388 assert!(
1389 Telemetry::builder("rm-default").runtime_metrics,
1390 "runtime metrics are on by default"
1391 );
1392 assert!(
1393 !Telemetry::builder("rm-off")
1394 .with_runtime_metrics(false)
1395 .runtime_metrics
1396 );
1397 }
1398
1399 #[tokio::test]
1407 async fn shutdown_absorbs_provider_errors() {
1408 let handles = TelemetryHandles {
1409 tracer_provider: SdkTracerProvider::builder().build(),
1410 meter_provider: Some(SdkMeterProvider::builder().build()),
1411 logger_provider: Some(SdkLoggerProvider::builder().build()),
1412 shutdown_timeout: DEFAULT_SHUTDOWN_TIMEOUT,
1413 boot_owner: false,
1414 #[cfg(feature = "profiling")]
1415 profiling_handle: None,
1416 };
1417
1418 handles.shutdown().expect("first shutdown succeeds");
1419 handles
1420 .shutdown()
1421 .expect("second shutdown absorbs the already-shut-down errors");
1422 }
1423 use opentelemetry::trace::{Span as _, Tracer as _};
1424 use std::sync::Mutex;
1425
1426 static ENV_LOCK: Mutex<()> = Mutex::new(());
1427
1428 #[test]
1429 fn tracing_bridge_uses_sdk_tracer() {
1430 let provider = SdkTracerProvider::builder().build();
1431 let tracer = tracing_bridge_tracer(&provider);
1432 let span = tracer.start("bridge-regression");
1433
1434 assert!(span.span_context().is_valid());
1435
1436 provider.shutdown().expect("provider shutdown");
1437 }
1438
1439 #[test]
1440 fn resource_contains_all_attributes_when_provided() {
1441 let resource = build_resource("test-svc", Some("1.2.3"), Some("staging"));
1442
1443 assert_eq!(
1444 resource.get(&opentelemetry::Key::new("service.name")),
1445 Some(opentelemetry::Value::from("test-svc")),
1446 );
1447 assert_eq!(
1448 resource.get(&opentelemetry::Key::new(SERVICE_VERSION)),
1449 Some(opentelemetry::Value::from("1.2.3")),
1450 );
1451 assert_eq!(
1452 resource.get(&opentelemetry::Key::new(DEPLOYMENT_ENVIRONMENT_NAME)),
1453 Some(opentelemetry::Value::from("staging")),
1454 );
1455 assert!(resource.get(&opentelemetry::Key::new(HOST_NAME)).is_some());
1456 assert!(
1457 resource
1458 .get(&opentelemetry::Key::new(PROCESS_PID))
1459 .is_some()
1460 );
1461 }
1462
1463 #[test]
1464 fn resource_graceful_when_optional_values_omitted() {
1465 let resource = build_resource("test-svc", None, None);
1466
1467 assert_eq!(
1468 resource.get(&opentelemetry::Key::new("service.name")),
1469 Some(opentelemetry::Value::from("test-svc")),
1470 );
1471 assert!(
1472 resource
1473 .get(&opentelemetry::Key::new(SERVICE_VERSION))
1474 .is_none()
1475 );
1476 assert!(
1477 resource
1478 .get(&opentelemetry::Key::new(DEPLOYMENT_ENVIRONMENT_NAME))
1479 .is_none()
1480 );
1481 assert!(resource.get(&opentelemetry::Key::new(HOST_NAME)).is_some());
1483 assert!(
1484 resource
1485 .get(&opentelemetry::Key::new(PROCESS_PID))
1486 .is_some()
1487 );
1488 }
1489
1490 #[test]
1491 fn trace_sampler_ratio_converts_to_sdk() {
1492 let sampler = TraceSampler::TraceIdRatio(0.5);
1493 let sdk = sampler.into_sdk_sampler();
1494 assert_eq!(format!("{sdk:?}"), "TraceIdRatioBased(0.5)");
1495 }
1496
1497 #[test]
1498 fn trace_sampler_parent_based_converts_to_sdk() {
1499 let sampler = TraceSampler::ParentBased(Box::new(TraceSampler::TraceIdRatio(0.25)));
1500 let sdk = sampler.into_sdk_sampler();
1501 let debug = format!("{sdk:?}");
1502 assert!(debug.contains("ParentBased"));
1503 assert!(debug.contains("0.25"));
1504 }
1505
1506 unsafe fn set_env(key: &str, val: &str) {
1508 unsafe {
1509 std::env::set_var(key, val);
1510 }
1511 }
1512
1513 unsafe fn remove_env(key: &str) {
1514 unsafe {
1515 std::env::remove_var(key);
1516 }
1517 }
1518
1519 #[test]
1520 fn sampler_from_env_reads_traceidratio() {
1521 let _lock = ENV_LOCK.lock().unwrap();
1522 unsafe {
1523 set_env("OTEL_TRACES_SAMPLER", "traceidratio");
1524 set_env("OTEL_TRACES_SAMPLER_ARG", "0.42");
1525 }
1526
1527 let sampler = sampler_from_env()
1528 .expect("should not error")
1529 .expect("should return Some");
1530 assert!(
1531 matches!(sampler, TraceSampler::TraceIdRatio(r) if (r - 0.42).abs() < f64::EPSILON)
1532 );
1533
1534 unsafe {
1535 remove_env("OTEL_TRACES_SAMPLER");
1536 remove_env("OTEL_TRACES_SAMPLER_ARG");
1537 }
1538 }
1539
1540 #[test]
1541 fn sampler_from_env_returns_none_when_unset() {
1542 let _lock = ENV_LOCK.lock().unwrap();
1543 unsafe {
1544 remove_env("OTEL_TRACES_SAMPLER");
1545 }
1546 assert!(sampler_from_env().expect("should not error").is_none());
1547 }
1548
1549 #[test]
1550 fn sampler_from_env_reads_parentbased_traceidratio() {
1551 let _lock = ENV_LOCK.lock().unwrap();
1552 unsafe {
1553 set_env("OTEL_TRACES_SAMPLER", "parentbased_traceidratio");
1554 set_env("OTEL_TRACES_SAMPLER_ARG", "0.1");
1555 }
1556
1557 let sampler = sampler_from_env()
1558 .expect("should not error")
1559 .expect("should return Some");
1560 assert!(
1561 matches!(sampler, TraceSampler::ParentBased(inner) if matches!(*inner, TraceSampler::TraceIdRatio(r) if (r - 0.1).abs() < f64::EPSILON))
1562 );
1563
1564 unsafe {
1565 remove_env("OTEL_TRACES_SAMPLER");
1566 remove_env("OTEL_TRACES_SAMPLER_ARG");
1567 }
1568 }
1569
1570 #[test]
1571 fn sampler_from_env_parentbased_always_on() {
1572 let _lock = ENV_LOCK.lock().unwrap();
1573 unsafe {
1574 set_env("OTEL_TRACES_SAMPLER", "parentbased_always_on");
1575 }
1576 let sampler = sampler_from_env()
1577 .expect("should not error")
1578 .expect("should return Some");
1579 assert!(
1580 matches!(sampler, TraceSampler::ParentBased(inner) if matches!(*inner, TraceSampler::AlwaysOn))
1581 );
1582 unsafe {
1583 remove_env("OTEL_TRACES_SAMPLER");
1584 }
1585 }
1586
1587 #[test]
1588 fn sampler_from_env_parentbased_always_off() {
1589 let _lock = ENV_LOCK.lock().unwrap();
1590 unsafe {
1591 set_env("OTEL_TRACES_SAMPLER", "parentbased_always_off");
1592 }
1593 let sampler = sampler_from_env()
1594 .expect("should not error")
1595 .expect("should return Some");
1596 assert!(
1597 matches!(sampler, TraceSampler::ParentBased(inner) if matches!(*inner, TraceSampler::AlwaysOff))
1598 );
1599 unsafe {
1600 remove_env("OTEL_TRACES_SAMPLER");
1601 }
1602 }
1603
1604 #[test]
1605 fn sampler_from_env_always_on() {
1606 let _lock = ENV_LOCK.lock().unwrap();
1607 unsafe {
1608 set_env("OTEL_TRACES_SAMPLER", "always_on");
1609 }
1610 let sampler = sampler_from_env()
1611 .expect("should not error")
1612 .expect("should return Some");
1613 assert!(matches!(sampler, TraceSampler::AlwaysOn));
1614 unsafe {
1615 remove_env("OTEL_TRACES_SAMPLER");
1616 }
1617 }
1618
1619 #[test]
1620 fn sampler_from_env_always_off() {
1621 let _lock = ENV_LOCK.lock().unwrap();
1622 unsafe {
1623 set_env("OTEL_TRACES_SAMPLER", "always_off");
1624 }
1625 let sampler = sampler_from_env()
1626 .expect("should not error")
1627 .expect("should return Some");
1628 assert!(matches!(sampler, TraceSampler::AlwaysOff));
1629 unsafe {
1630 remove_env("OTEL_TRACES_SAMPLER");
1631 }
1632 }
1633
1634 #[test]
1635 fn sampler_from_env_unknown_returns_error() {
1636 let _lock = ENV_LOCK.lock().unwrap();
1637 unsafe {
1638 set_env("OTEL_TRACES_SAMPLER", "unknown_sampler");
1639 }
1640 let err = sampler_from_env().expect_err("unknown sampler should produce an error");
1641 assert!(
1642 err.to_string().contains("unknown_sampler"),
1643 "error message should include the unknown name, got: {err}"
1644 );
1645 unsafe {
1646 remove_env("OTEL_TRACES_SAMPLER");
1647 }
1648 }
1649
1650 #[test]
1651 fn trace_sampler_always_on_converts_to_sdk() {
1652 let sdk = TraceSampler::AlwaysOn.into_sdk_sampler();
1653 assert_eq!(format!("{sdk:?}"), "AlwaysOn");
1654 }
1655
1656 #[test]
1657 fn trace_sampler_always_off_converts_to_sdk() {
1658 let sdk = TraceSampler::AlwaysOff.into_sdk_sampler();
1659 assert_eq!(format!("{sdk:?}"), "AlwaysOff");
1660 }
1661
1662 #[test]
1663 fn builder_has_sensible_defaults() {
1664 let builder = Telemetry::builder("test-svc");
1665 assert_eq!(builder.service_name.as_deref(), Some("test-svc"));
1666 assert!(builder.service_version.is_none());
1667 assert!(builder.deployment_environment.is_none());
1668 assert!(builder.sampler.is_none());
1669 assert!(builder.metrics);
1670 assert!(!builder.logs);
1671 assert!(builder.protocol.is_none());
1672 assert!(builder.max_export_batch_size.is_none());
1673 assert!(builder.metric_export_interval.is_none());
1674 assert!(builder.export_timeout.is_none());
1675 }
1676
1677 #[test]
1678 fn from_env_builder_has_no_service_name() {
1679 let builder = Telemetry::from_env();
1680 assert!(builder.service_name.is_none());
1681 }
1682
1683 #[test]
1684 fn with_export_timeout_stores_value() {
1685 let timeout = Duration::from_secs(5);
1686 let builder = Telemetry::builder("test-svc").with_export_timeout(timeout);
1687 assert_eq!(builder.export_timeout, Some(timeout));
1688 }
1689
1690 #[test]
1691 fn timeout_from_env_reads_milliseconds() {
1692 let _lock = ENV_LOCK.lock().unwrap();
1693 unsafe {
1694 set_env("OTEL_EXPORTER_OTLP_TIMEOUT", "5000");
1695 }
1696 let t = timeout_from_env();
1697 assert_eq!(t, Some(Duration::from_millis(5000)));
1698 unsafe {
1699 remove_env("OTEL_EXPORTER_OTLP_TIMEOUT");
1700 }
1701 }
1702
1703 #[test]
1704 fn timeout_from_env_returns_none_when_unset() {
1705 let _lock = ENV_LOCK.lock().unwrap();
1706 unsafe {
1707 remove_env("OTEL_EXPORTER_OTLP_TIMEOUT");
1708 }
1709 assert_eq!(timeout_from_env(), None);
1710 }
1711
1712 #[test]
1713 fn service_name_from_env_used_when_none_given() {
1714 let builder = Telemetry::from_env();
1715 assert!(builder.service_name.is_none());
1716 }
1717
1718 #[test]
1719 fn explicit_service_name_overrides_env_var() {
1720 let builder = Telemetry::builder("explicit-svc");
1721 assert_eq!(builder.service_name.as_deref(), Some("explicit-svc"));
1722 }
1723
1724 #[test]
1725 fn from_env_builder_service_name_is_none() {
1726 let builder = Telemetry::from_env();
1727 assert!(builder.service_name.is_none());
1728 }
1729
1730 #[test]
1731 fn init_returns_error_for_unknown_otel_traces_sampler() {
1732 let _lock = ENV_LOCK.lock().unwrap();
1733 unsafe {
1734 set_env("OTEL_TRACES_SAMPLER", "not_a_real_sampler");
1735 }
1736 let result = Telemetry::builder("test-svc").with_metrics(false).init();
1737 let err = result
1738 .err()
1739 .expect("unknown sampler env var should cause init to fail");
1740 assert!(
1741 err.to_string().contains("not_a_real_sampler"),
1742 "error should name the unknown sampler, got: {err}"
1743 );
1744 unsafe {
1745 remove_env("OTEL_TRACES_SAMPLER");
1746 }
1747 }
1748
1749 #[test]
1750 fn with_max_export_batch_size_stores_value() {
1751 let builder = Telemetry::builder("test-svc").with_max_export_batch_size(1024);
1752 assert_eq!(builder.max_export_batch_size, Some(1024));
1753 }
1754
1755 #[test]
1756 fn with_metric_export_interval_stores_value() {
1757 let interval = Duration::from_secs(30);
1758 let builder = Telemetry::builder("test-svc").with_metric_export_interval(interval);
1759 assert_eq!(builder.metric_export_interval, Some(interval));
1760 }
1761
1762 #[test]
1763 fn init_rejects_zero_metric_export_interval() {
1764 let err = Telemetry::builder("test-svc")
1765 .with_metric_export_interval(Duration::ZERO)
1766 .with_metrics(false)
1767 .init()
1768 .err()
1769 .expect("expected error for zero interval");
1770 assert!(
1771 err.to_string().contains("metric_export_interval"),
1772 "error message should mention metric_export_interval, got: {err}"
1773 );
1774 }
1775
1776 #[test]
1777 fn builder_with_custom_values() {
1778 let builder = Telemetry::builder("test-svc")
1779 .with_version("2.0.0")
1780 .with_environment("production")
1781 .with_sampler(TraceSampler::TraceIdRatio(0.5))
1782 .with_metrics(false);
1783
1784 assert_eq!(builder.service_name.as_deref(), Some("test-svc"));
1785 assert_eq!(builder.service_version.as_deref(), Some("2.0.0"));
1786 assert_eq!(
1787 builder.deployment_environment.as_deref(),
1788 Some("production")
1789 );
1790 assert!(
1791 matches!(builder.sampler, Some(TraceSampler::TraceIdRatio(r)) if (r - 0.5).abs() < f64::EPSILON)
1792 );
1793 assert!(!builder.metrics);
1794 }
1795
1796 #[test]
1797 fn builder_stores_programmatic_log_configuration() {
1798 let builder = Telemetry::builder("test-svc")
1799 .with_log_filter("info,opentelemetry_sdk=warn")
1800 .with_log_format(LogFormat::Json);
1801
1802 assert_eq!(
1803 builder.log_filter.as_deref(),
1804 Some("info,opentelemetry_sdk=warn")
1805 );
1806 assert_eq!(builder.log_format, LogFormat::Json);
1807 }
1808
1809 #[test]
1810 fn init_rejects_invalid_programmatic_log_filter_before_provider_setup() {
1811 let setup_ran = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
1812 let setup_ran_in_closure = std::sync::Arc::clone(&setup_ran);
1813
1814 let error = Telemetry::builder("test-svc")
1815 .with_log_filter("[")
1816 .with_meter_provider_setup(move |builder| {
1817 setup_ran_in_closure.store(true, std::sync::atomic::Ordering::SeqCst);
1818 builder
1819 })
1820 .init()
1821 .err()
1822 .expect("invalid filter must fail initialization");
1823
1824 assert!(error.to_string().contains("invalid filter directive"));
1825 assert!(!setup_ran.load(std::sync::atomic::Ordering::SeqCst));
1826 }
1827
1828 #[test]
1829 fn builder_with_default_endpoint() {
1830 let builder = Telemetry::builder("svc").with_default_endpoint("http://otel-collector:4317");
1831 assert_eq!(
1832 builder.default_endpoint.as_deref(),
1833 Some("http://otel-collector:4317")
1834 );
1835 }
1836
1837 #[test]
1838 fn a_configured_endpoint_wins_over_the_runtime_default() {
1839 assert_eq!(
1840 resolve_endpoint(
1841 Some("http://c:4317".into()),
1842 Some("http://d:4317"),
1843 "http://localhost:4317"
1844 ),
1845 "http://c:4317"
1846 );
1847 assert_eq!(
1848 resolve_endpoint(None, Some("http://d:4317"), "http://localhost:4317"),
1849 "http://d:4317"
1850 );
1851 assert_eq!(
1852 resolve_endpoint(None, None, "http://localhost:4317"),
1853 "http://localhost:4317"
1854 );
1855 }
1856
1857 #[test]
1858 #[cfg(feature = "grpc")]
1859 fn builder_with_protocol_grpc() {
1860 let builder = Telemetry::builder("test-svc").with_protocol(ExportProtocol::Grpc);
1861 assert_eq!(builder.protocol, Some(ExportProtocol::Grpc));
1862 }
1863
1864 #[test]
1865 #[cfg(feature = "http")]
1866 fn builder_with_protocol_http() {
1867 let builder = Telemetry::builder("test-svc").with_protocol(ExportProtocol::HttpProtobuf);
1868 assert_eq!(builder.protocol, Some(ExportProtocol::HttpProtobuf));
1869 }
1870
1871 #[test]
1872 #[cfg(feature = "grpc")]
1873 fn protocol_from_env_reads_grpc() {
1874 let _lock = ENV_LOCK.lock().unwrap();
1875 unsafe {
1876 set_env("OTEL_EXPORTER_OTLP_PROTOCOL", "grpc");
1877 }
1878 assert_eq!(protocol_from_env(), Some(ExportProtocol::Grpc));
1879 unsafe {
1880 remove_env("OTEL_EXPORTER_OTLP_PROTOCOL");
1881 }
1882 }
1883
1884 #[test]
1885 #[cfg(feature = "http")]
1886 fn protocol_from_env_reads_http_protobuf() {
1887 let _lock = ENV_LOCK.lock().unwrap();
1888 unsafe {
1889 set_env("OTEL_EXPORTER_OTLP_PROTOCOL", "http/protobuf");
1890 }
1891 assert_eq!(protocol_from_env(), Some(ExportProtocol::HttpProtobuf));
1892 unsafe {
1893 remove_env("OTEL_EXPORTER_OTLP_PROTOCOL");
1894 }
1895 }
1896
1897 #[test]
1898 fn protocol_from_env_returns_none_when_unset() {
1899 let _lock = ENV_LOCK.lock().unwrap();
1900 unsafe {
1901 remove_env("OTEL_EXPORTER_OTLP_PROTOCOL");
1902 }
1903 assert_eq!(protocol_from_env(), None);
1904 }
1905
1906 #[test]
1907 fn protocol_from_env_returns_none_for_unknown() {
1908 let _lock = ENV_LOCK.lock().unwrap();
1909 unsafe {
1910 set_env("OTEL_EXPORTER_OTLP_PROTOCOL", "websocket");
1911 }
1912 assert_eq!(protocol_from_env(), None);
1913 unsafe {
1914 remove_env("OTEL_EXPORTER_OTLP_PROTOCOL");
1915 }
1916 }
1917
1918 #[test]
1919 fn builder_is_send_and_sync() {
1920 fn assert_send_sync<T: Send + Sync>() {}
1921 assert_send_sync::<TelemetryBuilder>();
1922 }
1923
1924 #[test]
1925 fn with_shutdown_timeout_stores_value() {
1926 let timeout = Duration::from_secs(10);
1927 let builder = Telemetry::builder("test-svc").with_shutdown_timeout(timeout);
1928 assert_eq!(builder.shutdown_timeout, timeout);
1929 }
1930
1931 #[test]
1932 fn default_shutdown_timeout_is_five_seconds() {
1933 let builder = Telemetry::builder("test-svc");
1934 assert_eq!(builder.shutdown_timeout, Duration::from_secs(5));
1935 }
1936
1937 #[cfg(feature = "testing")]
1945 #[test]
1946 fn drop_completes_within_shutdown_timeout() {
1947 let mut handles = crate::Telemetry::testing("drop-timeout-test");
1949 handles.shutdown_timeout = Duration::from_millis(100);
1951
1952 let start = std::time::Instant::now();
1953 drop(handles);
1954 let elapsed = start.elapsed();
1955
1956 assert!(
1958 elapsed < Duration::from_millis(500),
1959 "drop took {elapsed:?}, expected < 500 ms"
1960 );
1961 }
1962}