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
35pub mod span_enrichment;
36
37use opentelemetry::KeyValue;
38use opentelemetry::propagation::TextMapCompositePropagator;
39use opentelemetry_otlp::WithExportConfig;
40use opentelemetry_sdk::{
41 Resource,
42 logs::SdkLoggerProvider,
43 metrics::{MeterProviderBuilder, PeriodicReader, SdkMeterProvider},
44 propagation::{BaggagePropagator, TraceContextPropagator},
45 trace::{BatchConfigBuilder, BatchSpanProcessor, Sampler, SdkTracerProvider},
46};
47use opentelemetry_semantic_conventions::attribute::{
48 DEPLOYMENT_ENVIRONMENT_NAME, HOST_NAME, PROCESS_PID, SERVICE_VERSION,
49};
50use std::error::Error;
51use std::time::Duration;
52use tracing_subscriber::layer::SubscriberExt;
53use tracing_subscriber::util::SubscriberInitExt;
54
55#[derive(Debug, Clone)]
70pub enum TraceSampler {
71 AlwaysOn,
73 AlwaysOff,
75 TraceIdRatio(f64),
77 ParentBased(Box<TraceSampler>),
80}
81
82impl TraceSampler {
83 fn into_sdk_sampler(self) -> Sampler {
85 match self {
86 TraceSampler::AlwaysOn => Sampler::AlwaysOn,
87 TraceSampler::AlwaysOff => Sampler::AlwaysOff,
88 TraceSampler::TraceIdRatio(r) => Sampler::TraceIdRatioBased(r),
89 TraceSampler::ParentBased(inner) => {
90 Sampler::ParentBased(Box::new(inner.into_sdk_sampler()))
91 }
92 }
93 }
94}
95
96fn sampler_from_env() -> Result<Option<TraceSampler>, Box<dyn Error>> {
104 let name = match std::env::var("OTEL_TRACES_SAMPLER") {
105 Ok(v) => v,
106 Err(_) => return Ok(None),
107 };
108 let arg = std::env::var("OTEL_TRACES_SAMPLER_ARG").ok();
109 let sampler = match name.as_str() {
110 "always_on" => TraceSampler::AlwaysOn,
111 "always_off" => TraceSampler::AlwaysOff,
112 "traceidratio" => {
113 let ratio = arg
114 .as_deref()
115 .unwrap_or("1.0")
116 .parse::<f64>()
117 .unwrap_or(1.0);
118 TraceSampler::TraceIdRatio(ratio)
119 }
120 "parentbased_always_on" => TraceSampler::ParentBased(Box::new(TraceSampler::AlwaysOn)),
121 "parentbased_always_off" => TraceSampler::ParentBased(Box::new(TraceSampler::AlwaysOff)),
122 "parentbased_traceidratio" => {
123 let ratio = arg
124 .as_deref()
125 .unwrap_or("1.0")
126 .parse::<f64>()
127 .unwrap_or(1.0);
128 TraceSampler::ParentBased(Box::new(TraceSampler::TraceIdRatio(ratio)))
129 }
130 unknown => {
131 return Err(format!(
132 "OTEL_TRACES_SAMPLER: unrecognised sampler name '{unknown}'. \
133 Valid values: always_on, always_off, traceidratio, \
134 parentbased_always_on, parentbased_always_off, parentbased_traceidratio"
135 )
136 .into());
137 }
138 };
139 Ok(Some(sampler))
140}
141
142const DEFAULT_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5);
144
145pub struct TelemetryHandles {
166 pub tracer_provider: SdkTracerProvider,
167 pub meter_provider: Option<SdkMeterProvider>,
168 pub logger_provider: Option<SdkLoggerProvider>,
169 shutdown_timeout: Duration,
170}
171
172impl TelemetryHandles {
173 pub fn shutdown(&self) -> Result<(), Box<dyn Error>> {
186 self.tracer_provider.shutdown()?;
187 if let Some(mp) = &self.meter_provider {
188 mp.shutdown()?;
189 }
190 if let Some(lp) = &self.logger_provider {
191 lp.shutdown()?;
192 }
193 Ok(())
194 }
195}
196
197impl Drop for TelemetryHandles {
198 fn drop(&mut self) {
199 let tracer_provider = self.tracer_provider.clone();
200 let meter_provider = self.meter_provider.clone();
201 let logger_provider = self.logger_provider.clone();
202 let timeout = self.shutdown_timeout;
203
204 let (tx, rx) = std::sync::mpsc::channel();
205 std::thread::spawn(move || {
206 if let Err(e) = tracer_provider.shutdown() {
207 tracing::warn!("tracer provider shutdown error: {e}");
208 }
209 if let Some(mp) = meter_provider
210 && let Err(e) = mp.shutdown()
211 {
212 tracing::warn!("meter provider shutdown error: {e}");
213 }
214 if let Some(lp) = logger_provider
215 && let Err(e) = lp.shutdown()
216 {
217 tracing::warn!("logger provider shutdown error: {e}");
218 }
219 let _ = tx.send(());
220 });
221
222 if rx.recv_timeout(timeout).is_err() {
223 tracing::warn!(
224 "telemetry shutdown did not complete within {timeout:?}; \
225 some spans/metrics may not have been exported"
226 );
227 }
228 }
229}
230
231#[derive(Debug, Clone, Copy, PartialEq, Eq)]
254pub enum ExportProtocol {
255 #[cfg(feature = "grpc")]
257 Grpc,
258 #[cfg(feature = "http")]
260 HttpProtobuf,
261}
262
263#[cfg(feature = "grpc-mtls")]
273#[derive(Clone)]
274pub struct MtlsMaterial {
275 pub client_cert_chain_pem: Vec<u8>,
277 pub client_key_pem: Vec<u8>,
279 pub trust_bundle_pem: Vec<u8>,
281}
282
283#[cfg(feature = "grpc-mtls")]
284impl std::fmt::Debug for MtlsMaterial {
285 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
286 f.debug_struct("MtlsMaterial")
287 .field("client_cert_chain_pem", &"<redacted>")
288 .field("client_key_pem", &"<redacted>")
289 .field("trust_bundle_pem", &"<redacted>")
290 .finish()
291 }
292}
293
294fn protocol_from_env() -> Option<ExportProtocol> {
296 let val = std::env::var("OTEL_EXPORTER_OTLP_PROTOCOL").ok()?;
297 match val.trim() {
298 #[cfg(feature = "grpc")]
299 "grpc" => Some(ExportProtocol::Grpc),
300 #[cfg(feature = "http")]
301 "http/protobuf" => Some(ExportProtocol::HttpProtobuf),
302 _ => None,
303 }
304}
305
306pub struct Telemetry;
322
323impl Telemetry {
324 pub fn builder(service_name: &str) -> TelemetryBuilder {
328 TelemetryBuilder {
329 service_name: Some(service_name.to_string()),
330 service_version: None,
331 deployment_environment: None,
332 sampler: None,
333 metrics: true,
334 logs: false,
335 protocol: None,
336 max_export_batch_size: None,
337 metric_export_interval: None,
338 export_timeout: None,
339 shutdown_timeout: DEFAULT_SHUTDOWN_TIMEOUT,
340 extra_layers: Vec::new(),
341 extra_metric_readers: Vec::new(),
342 #[cfg(feature = "grpc-mtls")]
343 mtls: None,
344 }
345 }
346
347 pub fn from_env() -> TelemetryBuilder {
357 TelemetryBuilder {
358 service_name: None,
359 service_version: None,
360 deployment_environment: None,
361 sampler: None,
362 metrics: true,
363 logs: false,
364 protocol: None,
365 max_export_batch_size: None,
366 metric_export_interval: None,
367 export_timeout: None,
368 shutdown_timeout: DEFAULT_SHUTDOWN_TIMEOUT,
369 extra_layers: Vec::new(),
370 extra_metric_readers: Vec::new(),
371 #[cfg(feature = "grpc-mtls")]
372 mtls: None,
373 }
374 }
375}
376
377#[must_use = "a TelemetryBuilder does nothing until .init() is called"]
395pub struct TelemetryBuilder {
396 service_name: Option<String>,
397 service_version: Option<String>,
398 deployment_environment: Option<String>,
399 sampler: Option<TraceSampler>,
400 metrics: bool,
401 logs: bool,
402 protocol: Option<ExportProtocol>,
403 max_export_batch_size: Option<usize>,
404 metric_export_interval: Option<Duration>,
405 export_timeout: Option<Duration>,
406 shutdown_timeout: Duration,
407 extra_layers: Vec<
408 Box<dyn tracing_subscriber::Layer<tracing_subscriber::Registry> + Send + Sync + 'static>,
409 >,
410 extra_metric_readers: Vec<MeterProviderInstaller>,
411 #[cfg(feature = "grpc-mtls")]
412 mtls: Option<MtlsMaterial>,
413}
414
415type MeterProviderInstaller =
420 Box<dyn FnOnce(MeterProviderBuilder) -> MeterProviderBuilder + Send + Sync>;
421
422impl TelemetryBuilder {
423 pub fn with_version(mut self, version: &str) -> Self {
425 self.service_version = Some(version.to_string());
426 self
427 }
428
429 pub fn with_environment(mut self, environment: &str) -> Self {
431 self.deployment_environment = Some(environment.to_string());
432 self
433 }
434
435 #[cfg(feature = "grpc-mtls")]
460 pub fn with_mtls(mut self, material: MtlsMaterial) -> Self {
461 self.mtls = Some(material);
462 self.protocol = Some(ExportProtocol::Grpc);
463 self
464 }
465
466 pub fn with_sampler(mut self, sampler: TraceSampler) -> Self {
469 self.sampler = Some(sampler);
470 self
471 }
472
473 pub fn with_metrics(mut self, enabled: bool) -> Self {
475 self.metrics = enabled;
476 self
477 }
478
479 pub fn with_protocol(mut self, protocol: ExportProtocol) -> Self {
483 self.protocol = Some(protocol);
484 self
485 }
486
487 pub fn with_max_export_batch_size(mut self, size: usize) -> Self {
492 self.max_export_batch_size = Some(size);
493 self
494 }
495
496 pub fn with_metric_export_interval(mut self, interval: Duration) -> Self {
501 self.metric_export_interval = Some(interval);
502 self
503 }
504
505 pub fn with_logs(mut self, enabled: bool) -> Self {
512 self.logs = enabled;
513 self
514 }
515
516 pub fn with_export_timeout(mut self, timeout: Duration) -> Self {
520 self.export_timeout = Some(timeout);
521 self
522 }
523
524 pub fn with_shutdown_timeout(mut self, timeout: Duration) -> Self {
531 self.shutdown_timeout = timeout;
532 self
533 }
534
535 pub fn with_meter_provider_setup<F>(mut self, setup: F) -> Self
592 where
593 F: FnOnce(MeterProviderBuilder) -> MeterProviderBuilder + Send + Sync + 'static,
594 {
595 self.extra_metric_readers.push(Box::new(setup));
596 self
597 }
598
599 pub fn with_layer<L>(mut self, layer: L) -> Self
600 where
601 L: tracing_subscriber::Layer<tracing_subscriber::Registry> + Send + Sync + 'static,
602 {
603 self.extra_layers.push(Box::new(layer));
604 self
605 }
606
607 pub fn init(self) -> Result<TelemetryHandles, Box<dyn Error>> {
622 if let Some(interval) = self.metric_export_interval
623 && interval.is_zero()
624 {
625 return Err("metric_export_interval must be greater than zero".into());
626 }
627
628 let protocol = self.protocol.or_else(protocol_from_env).unwrap_or({
629 #[cfg(feature = "grpc")]
630 {
631 ExportProtocol::Grpc
632 }
633 #[cfg(all(not(feature = "grpc"), feature = "http"))]
634 {
635 ExportProtocol::HttpProtobuf
636 }
637 });
638
639 let default_endpoint = match protocol {
640 #[cfg(feature = "grpc")]
641 ExportProtocol::Grpc => "http://localhost:4317",
642 #[cfg(feature = "http")]
643 ExportProtocol::HttpProtobuf => "http://localhost:4318",
644 };
645 let endpoint = std::env::var("OTEL_EXPORTER_OTLP_ENDPOINT")
646 .unwrap_or_else(|_| default_endpoint.to_string());
647
648 let export_timeout = self.export_timeout.or_else(timeout_from_env);
650
651 let service_name = self.service_name.unwrap_or_else(|| {
653 std::env::var("OTEL_SERVICE_NAME").unwrap_or_else(|_| "unknown_service".to_string())
654 });
655
656 let resource = build_resource(
657 &service_name,
658 self.service_version.as_deref(),
659 self.deployment_environment.as_deref(),
660 );
661
662 let sampler = match self.sampler {
663 Some(s) => s,
664 None => sampler_from_env()?.unwrap_or(TraceSampler::AlwaysOn),
665 };
666
667 let trace_exporter = build_span_exporter(
669 protocol,
670 &endpoint,
671 export_timeout,
672 #[cfg(feature = "grpc-mtls")]
673 self.mtls.as_ref(),
674 )?;
675
676 let batch_processor = if let Some(size) = self.max_export_batch_size {
677 BatchSpanProcessor::builder(trace_exporter)
678 .with_batch_config(
679 BatchConfigBuilder::default()
680 .with_max_export_batch_size(size)
681 .build(),
682 )
683 .build()
684 } else {
685 BatchSpanProcessor::builder(trace_exporter).build()
686 };
687
688 let tracer_provider = SdkTracerProvider::builder()
689 .with_resource(resource.clone())
690 .with_sampler(sampler.into_sdk_sampler())
691 .with_span_processor(batch_processor)
692 .build();
693
694 opentelemetry::global::set_tracer_provider(tracer_provider.clone());
695
696 let propagator = TextMapCompositePropagator::new(vec![
698 Box::new(TraceContextPropagator::new()),
699 Box::new(BaggagePropagator::new()),
700 ]);
701 opentelemetry::global::set_text_map_propagator(propagator);
702
703 let meter_provider = if self.metrics {
705 let metric_exporter = build_metric_exporter(
706 protocol,
707 &endpoint,
708 export_timeout,
709 #[cfg(feature = "grpc-mtls")]
710 self.mtls.as_ref(),
711 )?;
712
713 let periodic_reader = if let Some(interval) = self.metric_export_interval {
714 PeriodicReader::builder(metric_exporter)
715 .with_interval(interval)
716 .build()
717 } else {
718 PeriodicReader::builder(metric_exporter).build()
719 };
720
721 let mut mp_builder = SdkMeterProvider::builder()
722 .with_resource(resource.clone())
723 .with_reader(periodic_reader);
724 for installer in self.extra_metric_readers {
725 mp_builder = installer(mp_builder);
726 }
727 let mp = mp_builder.build();
728
729 opentelemetry::global::set_meter_provider(mp.clone());
730
731 Some(mp)
732 } else {
733 None
734 };
735
736 let logger_provider = if self.logs {
738 let log_exporter = build_log_exporter(
739 protocol,
740 &endpoint,
741 export_timeout,
742 #[cfg(feature = "grpc-mtls")]
743 self.mtls.as_ref(),
744 )?;
745
746 let lp = SdkLoggerProvider::builder()
747 .with_resource(resource)
748 .with_batch_exporter(log_exporter)
749 .build();
750
751 Some(lp)
752 } else {
753 None
754 };
755
756 let otel_layer = tracing_opentelemetry::layer();
758
759 let extra = if self.extra_layers.is_empty() {
764 None
765 } else {
766 Some(self.extra_layers)
767 };
768
769 let registry = tracing_subscriber::registry()
770 .with(extra)
771 .with(tracing_subscriber::EnvFilter::from_default_env())
772 .with(tracing_subscriber::fmt::layer())
773 .with(otel_layer);
774
775 if let Some(lp) = &logger_provider {
776 if let Err(e) = registry
777 .with(opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge::new(lp))
778 .try_init()
779 {
780 eprintln!(
781 "otel-bootstrap: global tracing subscriber already installed — \
782 OTLP log records will NOT be exported to the collector: {e}"
783 );
784 }
785 } else if let Err(e) = registry.try_init() {
786 eprintln!(
787 "otel-bootstrap: global tracing subscriber already installed — \
788 OTLP telemetry will NOT be exported to the collector: {e}"
789 );
790 }
791
792 Ok(TelemetryHandles {
793 tracer_provider,
794 meter_provider,
795 logger_provider,
796 shutdown_timeout: self.shutdown_timeout,
797 })
798 }
799}
800
801pub fn init_telemetry(service_name: &str) -> Result<TelemetryHandles, Box<dyn Error>> {
815 Telemetry::builder(service_name).init()
816}
817
818pub fn init_telemetry_with_sampler(
835 service_name: &str,
836 sampler: Option<TraceSampler>,
837) -> Result<TelemetryHandles, Box<dyn Error>> {
838 let builder = Telemetry::builder(service_name);
839 match sampler {
840 Some(s) => builder.with_sampler(s),
841 None => builder, }
843 .init()
844}
845
846fn timeout_from_env() -> Option<Duration> {
848 let ms = std::env::var("OTEL_EXPORTER_OTLP_TIMEOUT").ok()?;
849 let ms: u64 = ms.trim().parse().ok()?;
850 Some(Duration::from_millis(ms))
851}
852
853#[cfg(feature = "grpc-mtls")]
860fn build_tls_config(material: &MtlsMaterial) -> tonic::transport::ClientTlsConfig {
861 use tonic::transport::{Certificate, ClientTlsConfig, Identity};
862 ClientTlsConfig::new()
863 .ca_certificate(Certificate::from_pem(&material.trust_bundle_pem))
864 .identity(Identity::from_pem(
865 &material.client_cert_chain_pem,
866 &material.client_key_pem,
867 ))
868}
869
870fn build_span_exporter(
871 protocol: ExportProtocol,
872 endpoint: &str,
873 timeout: Option<Duration>,
874 #[cfg(feature = "grpc-mtls")] mtls: Option<&MtlsMaterial>,
875) -> Result<opentelemetry_otlp::SpanExporter, Box<dyn Error>> {
876 match protocol {
877 #[cfg(feature = "grpc")]
878 ExportProtocol::Grpc => {
879 let mut b = opentelemetry_otlp::SpanExporter::builder()
880 .with_tonic()
881 .with_endpoint(endpoint);
882 if let Some(t) = timeout {
883 b = b.with_timeout(t);
884 }
885 #[cfg(feature = "grpc-mtls")]
886 if let Some(m) = mtls {
887 use opentelemetry_otlp::WithTonicConfig as _;
888 b = b.with_tls_config(build_tls_config(m));
889 }
890 Ok(b.build()?)
891 }
892 #[cfg(feature = "http")]
893 ExportProtocol::HttpProtobuf => {
894 let mut b = opentelemetry_otlp::SpanExporter::builder()
895 .with_http()
896 .with_endpoint(endpoint);
897 if let Some(t) = timeout {
898 b = b.with_timeout(t);
899 }
900 Ok(b.build()?)
901 }
902 }
903}
904
905fn build_metric_exporter(
906 protocol: ExportProtocol,
907 endpoint: &str,
908 timeout: Option<Duration>,
909 #[cfg(feature = "grpc-mtls")] mtls: Option<&MtlsMaterial>,
910) -> Result<opentelemetry_otlp::MetricExporter, Box<dyn Error>> {
911 match protocol {
912 #[cfg(feature = "grpc")]
913 ExportProtocol::Grpc => {
914 let mut b = opentelemetry_otlp::MetricExporter::builder()
915 .with_tonic()
916 .with_endpoint(endpoint);
917 if let Some(t) = timeout {
918 b = b.with_timeout(t);
919 }
920 #[cfg(feature = "grpc-mtls")]
921 if let Some(m) = mtls {
922 use opentelemetry_otlp::WithTonicConfig as _;
923 b = b.with_tls_config(build_tls_config(m));
924 }
925 Ok(b.build()?)
926 }
927 #[cfg(feature = "http")]
928 ExportProtocol::HttpProtobuf => {
929 let mut b = opentelemetry_otlp::MetricExporter::builder()
930 .with_http()
931 .with_endpoint(endpoint);
932 if let Some(t) = timeout {
933 b = b.with_timeout(t);
934 }
935 Ok(b.build()?)
936 }
937 }
938}
939
940fn build_log_exporter(
941 protocol: ExportProtocol,
942 endpoint: &str,
943 timeout: Option<Duration>,
944 #[cfg(feature = "grpc-mtls")] mtls: Option<&MtlsMaterial>,
945) -> Result<opentelemetry_otlp::LogExporter, Box<dyn Error>> {
946 match protocol {
947 #[cfg(feature = "grpc")]
948 ExportProtocol::Grpc => {
949 let mut b = opentelemetry_otlp::LogExporter::builder()
950 .with_tonic()
951 .with_endpoint(endpoint);
952 if let Some(t) = timeout {
953 b = b.with_timeout(t);
954 }
955 #[cfg(feature = "grpc-mtls")]
956 if let Some(m) = mtls {
957 use opentelemetry_otlp::WithTonicConfig as _;
958 b = b.with_tls_config(build_tls_config(m));
959 }
960 Ok(b.build()?)
961 }
962 #[cfg(feature = "http")]
963 ExportProtocol::HttpProtobuf => {
964 let mut b = opentelemetry_otlp::LogExporter::builder()
965 .with_http()
966 .with_endpoint(endpoint);
967 if let Some(t) = timeout {
968 b = b.with_timeout(t);
969 }
970 Ok(b.build()?)
971 }
972 }
973}
974
975pub fn build_resource(
990 service_name: &str,
991 service_version: Option<&str>,
992 deployment_environment: Option<&str>,
993) -> Resource {
994 let hostname = hostname::get()
995 .ok()
996 .and_then(|h| h.into_string().ok())
997 .unwrap_or_default();
998
999 let mut builder = Resource::builder()
1000 .with_service_name(service_name.to_string())
1001 .with_attributes([
1002 KeyValue::new(HOST_NAME, hostname),
1003 KeyValue::new(PROCESS_PID, std::process::id() as i64),
1004 ]);
1005
1006 if let Some(version) = service_version {
1007 builder = builder.with_attribute(KeyValue::new(SERVICE_VERSION, version.to_string()));
1008 }
1009
1010 if let Some(env) = deployment_environment {
1011 builder =
1012 builder.with_attribute(KeyValue::new(DEPLOYMENT_ENVIRONMENT_NAME, env.to_string()));
1013 }
1014
1015 builder.build()
1016}
1017
1018#[cfg(feature = "axum")]
1036pub fn axum_layer() -> axum_middleware::OtelTraceLayer {
1037 axum_middleware::OtelTraceLayer
1038}
1039
1040#[cfg(feature = "axum")]
1071pub fn span_enricher_layer<T>() -> axum_middleware::SpanEnricherLayer<T>
1072where
1073 T: span_enrichment::EnrichSpan + Clone + Send + Sync + 'static,
1074{
1075 axum_middleware::SpanEnricherLayer::default()
1076}
1077
1078#[cfg(test)]
1079mod tests {
1080 use super::*;
1081 use std::sync::Mutex;
1082
1083 static ENV_LOCK: Mutex<()> = Mutex::new(());
1084
1085 #[test]
1086 fn resource_contains_all_attributes_when_provided() {
1087 let resource = build_resource("test-svc", Some("1.2.3"), Some("staging"));
1088
1089 assert_eq!(
1090 resource.get(&opentelemetry::Key::new("service.name")),
1091 Some(opentelemetry::Value::from("test-svc")),
1092 );
1093 assert_eq!(
1094 resource.get(&opentelemetry::Key::new(SERVICE_VERSION)),
1095 Some(opentelemetry::Value::from("1.2.3")),
1096 );
1097 assert_eq!(
1098 resource.get(&opentelemetry::Key::new(DEPLOYMENT_ENVIRONMENT_NAME)),
1099 Some(opentelemetry::Value::from("staging")),
1100 );
1101 assert!(resource.get(&opentelemetry::Key::new(HOST_NAME)).is_some());
1102 assert!(
1103 resource
1104 .get(&opentelemetry::Key::new(PROCESS_PID))
1105 .is_some()
1106 );
1107 }
1108
1109 #[test]
1110 fn resource_graceful_when_optional_values_omitted() {
1111 let resource = build_resource("test-svc", None, None);
1112
1113 assert_eq!(
1114 resource.get(&opentelemetry::Key::new("service.name")),
1115 Some(opentelemetry::Value::from("test-svc")),
1116 );
1117 assert!(
1118 resource
1119 .get(&opentelemetry::Key::new(SERVICE_VERSION))
1120 .is_none()
1121 );
1122 assert!(
1123 resource
1124 .get(&opentelemetry::Key::new(DEPLOYMENT_ENVIRONMENT_NAME))
1125 .is_none()
1126 );
1127 assert!(resource.get(&opentelemetry::Key::new(HOST_NAME)).is_some());
1129 assert!(
1130 resource
1131 .get(&opentelemetry::Key::new(PROCESS_PID))
1132 .is_some()
1133 );
1134 }
1135
1136 #[test]
1137 fn trace_sampler_ratio_converts_to_sdk() {
1138 let sampler = TraceSampler::TraceIdRatio(0.5);
1139 let sdk = sampler.into_sdk_sampler();
1140 assert_eq!(format!("{sdk:?}"), "TraceIdRatioBased(0.5)");
1141 }
1142
1143 #[test]
1144 fn trace_sampler_parent_based_converts_to_sdk() {
1145 let sampler = TraceSampler::ParentBased(Box::new(TraceSampler::TraceIdRatio(0.25)));
1146 let sdk = sampler.into_sdk_sampler();
1147 let debug = format!("{sdk:?}");
1148 assert!(debug.contains("ParentBased"));
1149 assert!(debug.contains("0.25"));
1150 }
1151
1152 unsafe fn set_env(key: &str, val: &str) {
1154 unsafe {
1155 std::env::set_var(key, val);
1156 }
1157 }
1158
1159 unsafe fn remove_env(key: &str) {
1160 unsafe {
1161 std::env::remove_var(key);
1162 }
1163 }
1164
1165 #[test]
1166 fn sampler_from_env_reads_traceidratio() {
1167 let _lock = ENV_LOCK.lock().unwrap();
1168 unsafe {
1169 set_env("OTEL_TRACES_SAMPLER", "traceidratio");
1170 set_env("OTEL_TRACES_SAMPLER_ARG", "0.42");
1171 }
1172
1173 let sampler = sampler_from_env()
1174 .expect("should not error")
1175 .expect("should return Some");
1176 assert!(
1177 matches!(sampler, TraceSampler::TraceIdRatio(r) if (r - 0.42).abs() < f64::EPSILON)
1178 );
1179
1180 unsafe {
1181 remove_env("OTEL_TRACES_SAMPLER");
1182 remove_env("OTEL_TRACES_SAMPLER_ARG");
1183 }
1184 }
1185
1186 #[test]
1187 fn sampler_from_env_returns_none_when_unset() {
1188 let _lock = ENV_LOCK.lock().unwrap();
1189 unsafe {
1190 remove_env("OTEL_TRACES_SAMPLER");
1191 }
1192 assert!(sampler_from_env().expect("should not error").is_none());
1193 }
1194
1195 #[test]
1196 fn sampler_from_env_reads_parentbased_traceidratio() {
1197 let _lock = ENV_LOCK.lock().unwrap();
1198 unsafe {
1199 set_env("OTEL_TRACES_SAMPLER", "parentbased_traceidratio");
1200 set_env("OTEL_TRACES_SAMPLER_ARG", "0.1");
1201 }
1202
1203 let sampler = sampler_from_env()
1204 .expect("should not error")
1205 .expect("should return Some");
1206 assert!(
1207 matches!(sampler, TraceSampler::ParentBased(inner) if matches!(*inner, TraceSampler::TraceIdRatio(r) if (r - 0.1).abs() < f64::EPSILON))
1208 );
1209
1210 unsafe {
1211 remove_env("OTEL_TRACES_SAMPLER");
1212 remove_env("OTEL_TRACES_SAMPLER_ARG");
1213 }
1214 }
1215
1216 #[test]
1217 fn sampler_from_env_parentbased_always_on() {
1218 let _lock = ENV_LOCK.lock().unwrap();
1219 unsafe {
1220 set_env("OTEL_TRACES_SAMPLER", "parentbased_always_on");
1221 }
1222 let sampler = sampler_from_env()
1223 .expect("should not error")
1224 .expect("should return Some");
1225 assert!(
1226 matches!(sampler, TraceSampler::ParentBased(inner) if matches!(*inner, TraceSampler::AlwaysOn))
1227 );
1228 unsafe {
1229 remove_env("OTEL_TRACES_SAMPLER");
1230 }
1231 }
1232
1233 #[test]
1234 fn sampler_from_env_parentbased_always_off() {
1235 let _lock = ENV_LOCK.lock().unwrap();
1236 unsafe {
1237 set_env("OTEL_TRACES_SAMPLER", "parentbased_always_off");
1238 }
1239 let sampler = sampler_from_env()
1240 .expect("should not error")
1241 .expect("should return Some");
1242 assert!(
1243 matches!(sampler, TraceSampler::ParentBased(inner) if matches!(*inner, TraceSampler::AlwaysOff))
1244 );
1245 unsafe {
1246 remove_env("OTEL_TRACES_SAMPLER");
1247 }
1248 }
1249
1250 #[test]
1251 fn sampler_from_env_always_on() {
1252 let _lock = ENV_LOCK.lock().unwrap();
1253 unsafe {
1254 set_env("OTEL_TRACES_SAMPLER", "always_on");
1255 }
1256 let sampler = sampler_from_env()
1257 .expect("should not error")
1258 .expect("should return Some");
1259 assert!(matches!(sampler, TraceSampler::AlwaysOn));
1260 unsafe {
1261 remove_env("OTEL_TRACES_SAMPLER");
1262 }
1263 }
1264
1265 #[test]
1266 fn sampler_from_env_always_off() {
1267 let _lock = ENV_LOCK.lock().unwrap();
1268 unsafe {
1269 set_env("OTEL_TRACES_SAMPLER", "always_off");
1270 }
1271 let sampler = sampler_from_env()
1272 .expect("should not error")
1273 .expect("should return Some");
1274 assert!(matches!(sampler, TraceSampler::AlwaysOff));
1275 unsafe {
1276 remove_env("OTEL_TRACES_SAMPLER");
1277 }
1278 }
1279
1280 #[test]
1281 fn sampler_from_env_unknown_returns_error() {
1282 let _lock = ENV_LOCK.lock().unwrap();
1283 unsafe {
1284 set_env("OTEL_TRACES_SAMPLER", "unknown_sampler");
1285 }
1286 let err = sampler_from_env().expect_err("unknown sampler should produce an error");
1287 assert!(
1288 err.to_string().contains("unknown_sampler"),
1289 "error message should include the unknown name, got: {err}"
1290 );
1291 unsafe {
1292 remove_env("OTEL_TRACES_SAMPLER");
1293 }
1294 }
1295
1296 #[test]
1297 fn trace_sampler_always_on_converts_to_sdk() {
1298 let sdk = TraceSampler::AlwaysOn.into_sdk_sampler();
1299 assert_eq!(format!("{sdk:?}"), "AlwaysOn");
1300 }
1301
1302 #[test]
1303 fn trace_sampler_always_off_converts_to_sdk() {
1304 let sdk = TraceSampler::AlwaysOff.into_sdk_sampler();
1305 assert_eq!(format!("{sdk:?}"), "AlwaysOff");
1306 }
1307
1308 #[test]
1309 fn builder_has_sensible_defaults() {
1310 let builder = Telemetry::builder("test-svc");
1311 assert_eq!(builder.service_name.as_deref(), Some("test-svc"));
1312 assert!(builder.service_version.is_none());
1313 assert!(builder.deployment_environment.is_none());
1314 assert!(builder.sampler.is_none());
1315 assert!(builder.metrics);
1316 assert!(!builder.logs);
1317 assert!(builder.protocol.is_none());
1318 assert!(builder.max_export_batch_size.is_none());
1319 assert!(builder.metric_export_interval.is_none());
1320 assert!(builder.export_timeout.is_none());
1321 }
1322
1323 #[test]
1324 fn from_env_builder_has_no_service_name() {
1325 let builder = Telemetry::from_env();
1326 assert!(builder.service_name.is_none());
1327 }
1328
1329 #[test]
1330 fn with_export_timeout_stores_value() {
1331 let timeout = Duration::from_secs(5);
1332 let builder = Telemetry::builder("test-svc").with_export_timeout(timeout);
1333 assert_eq!(builder.export_timeout, Some(timeout));
1334 }
1335
1336 #[test]
1337 fn timeout_from_env_reads_milliseconds() {
1338 let _lock = ENV_LOCK.lock().unwrap();
1339 unsafe {
1340 set_env("OTEL_EXPORTER_OTLP_TIMEOUT", "5000");
1341 }
1342 let t = timeout_from_env();
1343 assert_eq!(t, Some(Duration::from_millis(5000)));
1344 unsafe {
1345 remove_env("OTEL_EXPORTER_OTLP_TIMEOUT");
1346 }
1347 }
1348
1349 #[test]
1350 fn timeout_from_env_returns_none_when_unset() {
1351 let _lock = ENV_LOCK.lock().unwrap();
1352 unsafe {
1353 remove_env("OTEL_EXPORTER_OTLP_TIMEOUT");
1354 }
1355 assert_eq!(timeout_from_env(), None);
1356 }
1357
1358 #[test]
1359 fn service_name_from_env_used_when_none_given() {
1360 let builder = Telemetry::from_env();
1361 assert!(builder.service_name.is_none());
1362 }
1363
1364 #[test]
1365 fn explicit_service_name_overrides_env_var() {
1366 let builder = Telemetry::builder("explicit-svc");
1367 assert_eq!(builder.service_name.as_deref(), Some("explicit-svc"));
1368 }
1369
1370 #[test]
1371 fn from_env_builder_service_name_is_none() {
1372 let builder = Telemetry::from_env();
1373 assert!(builder.service_name.is_none());
1374 }
1375
1376 #[test]
1377 fn init_returns_error_for_unknown_otel_traces_sampler() {
1378 let _lock = ENV_LOCK.lock().unwrap();
1379 unsafe {
1380 set_env("OTEL_TRACES_SAMPLER", "not_a_real_sampler");
1381 }
1382 let result = Telemetry::builder("test-svc").with_metrics(false).init();
1383 let err = result
1384 .err()
1385 .expect("unknown sampler env var should cause init to fail");
1386 assert!(
1387 err.to_string().contains("not_a_real_sampler"),
1388 "error should name the unknown sampler, got: {err}"
1389 );
1390 unsafe {
1391 remove_env("OTEL_TRACES_SAMPLER");
1392 }
1393 }
1394
1395 #[test]
1396 fn with_max_export_batch_size_stores_value() {
1397 let builder = Telemetry::builder("test-svc").with_max_export_batch_size(1024);
1398 assert_eq!(builder.max_export_batch_size, Some(1024));
1399 }
1400
1401 #[test]
1402 fn with_metric_export_interval_stores_value() {
1403 let interval = Duration::from_secs(30);
1404 let builder = Telemetry::builder("test-svc").with_metric_export_interval(interval);
1405 assert_eq!(builder.metric_export_interval, Some(interval));
1406 }
1407
1408 #[test]
1409 fn init_rejects_zero_metric_export_interval() {
1410 let err = Telemetry::builder("test-svc")
1411 .with_metric_export_interval(Duration::ZERO)
1412 .with_metrics(false)
1413 .init()
1414 .err()
1415 .expect("expected error for zero interval");
1416 assert!(
1417 err.to_string().contains("metric_export_interval"),
1418 "error message should mention metric_export_interval, got: {err}"
1419 );
1420 }
1421
1422 #[test]
1423 fn builder_with_custom_values() {
1424 let builder = Telemetry::builder("test-svc")
1425 .with_version("2.0.0")
1426 .with_environment("production")
1427 .with_sampler(TraceSampler::TraceIdRatio(0.5))
1428 .with_metrics(false);
1429
1430 assert_eq!(builder.service_name.as_deref(), Some("test-svc"));
1431 assert_eq!(builder.service_version.as_deref(), Some("2.0.0"));
1432 assert_eq!(
1433 builder.deployment_environment.as_deref(),
1434 Some("production")
1435 );
1436 assert!(
1437 matches!(builder.sampler, Some(TraceSampler::TraceIdRatio(r)) if (r - 0.5).abs() < f64::EPSILON)
1438 );
1439 assert!(!builder.metrics);
1440 }
1441
1442 #[test]
1443 #[cfg(feature = "grpc")]
1444 fn builder_with_protocol_grpc() {
1445 let builder = Telemetry::builder("test-svc").with_protocol(ExportProtocol::Grpc);
1446 assert_eq!(builder.protocol, Some(ExportProtocol::Grpc));
1447 }
1448
1449 #[test]
1450 #[cfg(feature = "http")]
1451 fn builder_with_protocol_http() {
1452 let builder = Telemetry::builder("test-svc").with_protocol(ExportProtocol::HttpProtobuf);
1453 assert_eq!(builder.protocol, Some(ExportProtocol::HttpProtobuf));
1454 }
1455
1456 #[test]
1457 #[cfg(feature = "grpc")]
1458 fn protocol_from_env_reads_grpc() {
1459 let _lock = ENV_LOCK.lock().unwrap();
1460 unsafe {
1461 set_env("OTEL_EXPORTER_OTLP_PROTOCOL", "grpc");
1462 }
1463 assert_eq!(protocol_from_env(), Some(ExportProtocol::Grpc));
1464 unsafe {
1465 remove_env("OTEL_EXPORTER_OTLP_PROTOCOL");
1466 }
1467 }
1468
1469 #[test]
1470 #[cfg(feature = "http")]
1471 fn protocol_from_env_reads_http_protobuf() {
1472 let _lock = ENV_LOCK.lock().unwrap();
1473 unsafe {
1474 set_env("OTEL_EXPORTER_OTLP_PROTOCOL", "http/protobuf");
1475 }
1476 assert_eq!(protocol_from_env(), Some(ExportProtocol::HttpProtobuf));
1477 unsafe {
1478 remove_env("OTEL_EXPORTER_OTLP_PROTOCOL");
1479 }
1480 }
1481
1482 #[test]
1483 fn protocol_from_env_returns_none_when_unset() {
1484 let _lock = ENV_LOCK.lock().unwrap();
1485 unsafe {
1486 remove_env("OTEL_EXPORTER_OTLP_PROTOCOL");
1487 }
1488 assert_eq!(protocol_from_env(), None);
1489 }
1490
1491 #[test]
1492 fn protocol_from_env_returns_none_for_unknown() {
1493 let _lock = ENV_LOCK.lock().unwrap();
1494 unsafe {
1495 set_env("OTEL_EXPORTER_OTLP_PROTOCOL", "websocket");
1496 }
1497 assert_eq!(protocol_from_env(), None);
1498 unsafe {
1499 remove_env("OTEL_EXPORTER_OTLP_PROTOCOL");
1500 }
1501 }
1502
1503 #[test]
1504 fn builder_is_send_and_sync() {
1505 fn assert_send_sync<T: Send + Sync>() {}
1506 assert_send_sync::<TelemetryBuilder>();
1507 }
1508
1509 #[test]
1510 fn with_shutdown_timeout_stores_value() {
1511 let timeout = Duration::from_secs(10);
1512 let builder = Telemetry::builder("test-svc").with_shutdown_timeout(timeout);
1513 assert_eq!(builder.shutdown_timeout, timeout);
1514 }
1515
1516 #[test]
1517 fn default_shutdown_timeout_is_five_seconds() {
1518 let builder = Telemetry::builder("test-svc");
1519 assert_eq!(builder.shutdown_timeout, Duration::from_secs(5));
1520 }
1521
1522 #[cfg(feature = "testing")]
1530 #[test]
1531 fn drop_completes_within_shutdown_timeout() {
1532 let mut handles = crate::Telemetry::testing("drop-timeout-test");
1534 handles.shutdown_timeout = Duration::from_millis(100);
1536
1537 let start = std::time::Instant::now();
1538 drop(handles);
1539 let elapsed = start.elapsed();
1540
1541 assert!(
1543 elapsed < Duration::from_millis(500),
1544 "drop took {elapsed:?}, expected < 500 ms"
1545 );
1546 }
1547}