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
263fn protocol_from_env() -> Option<ExportProtocol> {
265 let val = std::env::var("OTEL_EXPORTER_OTLP_PROTOCOL").ok()?;
266 match val.trim() {
267 #[cfg(feature = "grpc")]
268 "grpc" => Some(ExportProtocol::Grpc),
269 #[cfg(feature = "http")]
270 "http/protobuf" => Some(ExportProtocol::HttpProtobuf),
271 _ => None,
272 }
273}
274
275pub struct Telemetry;
291
292impl Telemetry {
293 pub fn builder(service_name: &str) -> TelemetryBuilder {
297 TelemetryBuilder {
298 service_name: Some(service_name.to_string()),
299 service_version: None,
300 deployment_environment: None,
301 sampler: None,
302 metrics: true,
303 logs: false,
304 protocol: None,
305 max_export_batch_size: None,
306 metric_export_interval: None,
307 export_timeout: None,
308 shutdown_timeout: DEFAULT_SHUTDOWN_TIMEOUT,
309 extra_layers: Vec::new(),
310 extra_metric_readers: Vec::new(),
311 }
312 }
313
314 pub fn from_env() -> TelemetryBuilder {
324 TelemetryBuilder {
325 service_name: None,
326 service_version: None,
327 deployment_environment: None,
328 sampler: None,
329 metrics: true,
330 logs: false,
331 protocol: None,
332 max_export_batch_size: None,
333 metric_export_interval: None,
334 export_timeout: None,
335 shutdown_timeout: DEFAULT_SHUTDOWN_TIMEOUT,
336 extra_layers: Vec::new(),
337 extra_metric_readers: Vec::new(),
338 }
339 }
340}
341
342#[must_use = "a TelemetryBuilder does nothing until .init() is called"]
360pub struct TelemetryBuilder {
361 service_name: Option<String>,
362 service_version: Option<String>,
363 deployment_environment: Option<String>,
364 sampler: Option<TraceSampler>,
365 metrics: bool,
366 logs: bool,
367 protocol: Option<ExportProtocol>,
368 max_export_batch_size: Option<usize>,
369 metric_export_interval: Option<Duration>,
370 export_timeout: Option<Duration>,
371 shutdown_timeout: Duration,
372 extra_layers: Vec<
373 Box<dyn tracing_subscriber::Layer<tracing_subscriber::Registry> + Send + Sync + 'static>,
374 >,
375 extra_metric_readers: Vec<MeterProviderInstaller>,
376}
377
378type MeterProviderInstaller =
383 Box<dyn FnOnce(MeterProviderBuilder) -> MeterProviderBuilder + Send + Sync>;
384
385impl TelemetryBuilder {
386 pub fn with_version(mut self, version: &str) -> Self {
388 self.service_version = Some(version.to_string());
389 self
390 }
391
392 pub fn with_environment(mut self, environment: &str) -> Self {
394 self.deployment_environment = Some(environment.to_string());
395 self
396 }
397
398 pub fn with_sampler(mut self, sampler: TraceSampler) -> Self {
401 self.sampler = Some(sampler);
402 self
403 }
404
405 pub fn with_metrics(mut self, enabled: bool) -> Self {
407 self.metrics = enabled;
408 self
409 }
410
411 pub fn with_protocol(mut self, protocol: ExportProtocol) -> Self {
415 self.protocol = Some(protocol);
416 self
417 }
418
419 pub fn with_max_export_batch_size(mut self, size: usize) -> Self {
424 self.max_export_batch_size = Some(size);
425 self
426 }
427
428 pub fn with_metric_export_interval(mut self, interval: Duration) -> Self {
433 self.metric_export_interval = Some(interval);
434 self
435 }
436
437 pub fn with_logs(mut self, enabled: bool) -> Self {
444 self.logs = enabled;
445 self
446 }
447
448 pub fn with_export_timeout(mut self, timeout: Duration) -> Self {
452 self.export_timeout = Some(timeout);
453 self
454 }
455
456 pub fn with_shutdown_timeout(mut self, timeout: Duration) -> Self {
463 self.shutdown_timeout = timeout;
464 self
465 }
466
467 pub fn with_meter_provider_setup<F>(mut self, setup: F) -> Self
524 where
525 F: FnOnce(MeterProviderBuilder) -> MeterProviderBuilder + Send + Sync + 'static,
526 {
527 self.extra_metric_readers.push(Box::new(setup));
528 self
529 }
530
531 pub fn with_layer<L>(mut self, layer: L) -> Self
532 where
533 L: tracing_subscriber::Layer<tracing_subscriber::Registry> + Send + Sync + 'static,
534 {
535 self.extra_layers.push(Box::new(layer));
536 self
537 }
538
539 pub fn init(self) -> Result<TelemetryHandles, Box<dyn Error>> {
554 if let Some(interval) = self.metric_export_interval
555 && interval.is_zero()
556 {
557 return Err("metric_export_interval must be greater than zero".into());
558 }
559
560 let protocol = self.protocol.or_else(protocol_from_env).unwrap_or({
561 #[cfg(feature = "grpc")]
562 {
563 ExportProtocol::Grpc
564 }
565 #[cfg(all(not(feature = "grpc"), feature = "http"))]
566 {
567 ExportProtocol::HttpProtobuf
568 }
569 });
570
571 let default_endpoint = match protocol {
572 #[cfg(feature = "grpc")]
573 ExportProtocol::Grpc => "http://localhost:4317",
574 #[cfg(feature = "http")]
575 ExportProtocol::HttpProtobuf => "http://localhost:4318",
576 };
577 let endpoint = std::env::var("OTEL_EXPORTER_OTLP_ENDPOINT")
578 .unwrap_or_else(|_| default_endpoint.to_string());
579
580 let export_timeout = self.export_timeout.or_else(timeout_from_env);
582
583 let service_name = self.service_name.unwrap_or_else(|| {
585 std::env::var("OTEL_SERVICE_NAME").unwrap_or_else(|_| "unknown_service".to_string())
586 });
587
588 let resource = build_resource(
589 &service_name,
590 self.service_version.as_deref(),
591 self.deployment_environment.as_deref(),
592 );
593
594 let sampler = match self.sampler {
595 Some(s) => s,
596 None => sampler_from_env()?.unwrap_or(TraceSampler::AlwaysOn),
597 };
598
599 let trace_exporter = build_span_exporter(protocol, &endpoint, export_timeout)?;
601
602 let batch_processor = if let Some(size) = self.max_export_batch_size {
603 BatchSpanProcessor::builder(trace_exporter)
604 .with_batch_config(
605 BatchConfigBuilder::default()
606 .with_max_export_batch_size(size)
607 .build(),
608 )
609 .build()
610 } else {
611 BatchSpanProcessor::builder(trace_exporter).build()
612 };
613
614 let tracer_provider = SdkTracerProvider::builder()
615 .with_resource(resource.clone())
616 .with_sampler(sampler.into_sdk_sampler())
617 .with_span_processor(batch_processor)
618 .build();
619
620 opentelemetry::global::set_tracer_provider(tracer_provider.clone());
621
622 let propagator = TextMapCompositePropagator::new(vec![
624 Box::new(TraceContextPropagator::new()),
625 Box::new(BaggagePropagator::new()),
626 ]);
627 opentelemetry::global::set_text_map_propagator(propagator);
628
629 let meter_provider = if self.metrics {
631 let metric_exporter = build_metric_exporter(protocol, &endpoint, export_timeout)?;
632
633 let periodic_reader = if let Some(interval) = self.metric_export_interval {
634 PeriodicReader::builder(metric_exporter)
635 .with_interval(interval)
636 .build()
637 } else {
638 PeriodicReader::builder(metric_exporter).build()
639 };
640
641 let mut mp_builder = SdkMeterProvider::builder()
642 .with_resource(resource.clone())
643 .with_reader(periodic_reader);
644 for installer in self.extra_metric_readers {
645 mp_builder = installer(mp_builder);
646 }
647 let mp = mp_builder.build();
648
649 opentelemetry::global::set_meter_provider(mp.clone());
650
651 Some(mp)
652 } else {
653 None
654 };
655
656 let logger_provider = if self.logs {
658 let log_exporter = build_log_exporter(protocol, &endpoint, export_timeout)?;
659
660 let lp = SdkLoggerProvider::builder()
661 .with_resource(resource)
662 .with_batch_exporter(log_exporter)
663 .build();
664
665 Some(lp)
666 } else {
667 None
668 };
669
670 let otel_layer = tracing_opentelemetry::layer();
672
673 let registry = tracing_subscriber::registry()
674 .with(self.extra_layers)
675 .with(tracing_subscriber::EnvFilter::from_default_env())
676 .with(tracing_subscriber::fmt::layer())
677 .with(otel_layer);
678
679 if let Some(lp) = &logger_provider {
680 registry
681 .with(opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge::new(lp))
682 .try_init()
683 .ok();
684 } else {
685 registry.try_init().ok();
686 }
687
688 Ok(TelemetryHandles {
689 tracer_provider,
690 meter_provider,
691 logger_provider,
692 shutdown_timeout: self.shutdown_timeout,
693 })
694 }
695}
696
697pub fn init_telemetry(service_name: &str) -> Result<TelemetryHandles, Box<dyn Error>> {
711 Telemetry::builder(service_name).init()
712}
713
714pub fn init_telemetry_with_sampler(
731 service_name: &str,
732 sampler: Option<TraceSampler>,
733) -> Result<TelemetryHandles, Box<dyn Error>> {
734 let builder = Telemetry::builder(service_name);
735 match sampler {
736 Some(s) => builder.with_sampler(s),
737 None => builder, }
739 .init()
740}
741
742fn timeout_from_env() -> Option<Duration> {
744 let ms = std::env::var("OTEL_EXPORTER_OTLP_TIMEOUT").ok()?;
745 let ms: u64 = ms.trim().parse().ok()?;
746 Some(Duration::from_millis(ms))
747}
748
749fn build_span_exporter(
750 protocol: ExportProtocol,
751 endpoint: &str,
752 timeout: Option<Duration>,
753) -> Result<opentelemetry_otlp::SpanExporter, Box<dyn Error>> {
754 match protocol {
755 #[cfg(feature = "grpc")]
756 ExportProtocol::Grpc => {
757 let mut b = opentelemetry_otlp::SpanExporter::builder()
758 .with_tonic()
759 .with_endpoint(endpoint);
760 if let Some(t) = timeout {
761 b = b.with_timeout(t);
762 }
763 Ok(b.build()?)
764 }
765 #[cfg(feature = "http")]
766 ExportProtocol::HttpProtobuf => {
767 let mut b = opentelemetry_otlp::SpanExporter::builder()
768 .with_http()
769 .with_endpoint(endpoint);
770 if let Some(t) = timeout {
771 b = b.with_timeout(t);
772 }
773 Ok(b.build()?)
774 }
775 }
776}
777
778fn build_metric_exporter(
779 protocol: ExportProtocol,
780 endpoint: &str,
781 timeout: Option<Duration>,
782) -> Result<opentelemetry_otlp::MetricExporter, Box<dyn Error>> {
783 match protocol {
784 #[cfg(feature = "grpc")]
785 ExportProtocol::Grpc => {
786 let mut b = opentelemetry_otlp::MetricExporter::builder()
787 .with_tonic()
788 .with_endpoint(endpoint);
789 if let Some(t) = timeout {
790 b = b.with_timeout(t);
791 }
792 Ok(b.build()?)
793 }
794 #[cfg(feature = "http")]
795 ExportProtocol::HttpProtobuf => {
796 let mut b = opentelemetry_otlp::MetricExporter::builder()
797 .with_http()
798 .with_endpoint(endpoint);
799 if let Some(t) = timeout {
800 b = b.with_timeout(t);
801 }
802 Ok(b.build()?)
803 }
804 }
805}
806
807fn build_log_exporter(
808 protocol: ExportProtocol,
809 endpoint: &str,
810 timeout: Option<Duration>,
811) -> Result<opentelemetry_otlp::LogExporter, Box<dyn Error>> {
812 match protocol {
813 #[cfg(feature = "grpc")]
814 ExportProtocol::Grpc => {
815 let mut b = opentelemetry_otlp::LogExporter::builder()
816 .with_tonic()
817 .with_endpoint(endpoint);
818 if let Some(t) = timeout {
819 b = b.with_timeout(t);
820 }
821 Ok(b.build()?)
822 }
823 #[cfg(feature = "http")]
824 ExportProtocol::HttpProtobuf => {
825 let mut b = opentelemetry_otlp::LogExporter::builder()
826 .with_http()
827 .with_endpoint(endpoint);
828 if let Some(t) = timeout {
829 b = b.with_timeout(t);
830 }
831 Ok(b.build()?)
832 }
833 }
834}
835
836pub fn build_resource(
851 service_name: &str,
852 service_version: Option<&str>,
853 deployment_environment: Option<&str>,
854) -> Resource {
855 let hostname = hostname::get()
856 .ok()
857 .and_then(|h| h.into_string().ok())
858 .unwrap_or_default();
859
860 let mut builder = Resource::builder()
861 .with_service_name(service_name.to_string())
862 .with_attributes([
863 KeyValue::new(HOST_NAME, hostname),
864 KeyValue::new(PROCESS_PID, std::process::id() as i64),
865 ]);
866
867 if let Some(version) = service_version {
868 builder = builder.with_attribute(KeyValue::new(SERVICE_VERSION, version.to_string()));
869 }
870
871 if let Some(env) = deployment_environment {
872 builder =
873 builder.with_attribute(KeyValue::new(DEPLOYMENT_ENVIRONMENT_NAME, env.to_string()));
874 }
875
876 builder.build()
877}
878
879#[cfg(feature = "axum")]
897pub fn axum_layer() -> axum_middleware::OtelTraceLayer {
898 axum_middleware::OtelTraceLayer
899}
900
901#[cfg(feature = "axum")]
932pub fn span_enricher_layer<T>() -> axum_middleware::SpanEnricherLayer<T>
933where
934 T: span_enrichment::EnrichSpan + Clone + Send + Sync + 'static,
935{
936 axum_middleware::SpanEnricherLayer::default()
937}
938
939#[cfg(test)]
940mod tests {
941 use super::*;
942 use std::sync::Mutex;
943
944 static ENV_LOCK: Mutex<()> = Mutex::new(());
945
946 #[test]
947 fn resource_contains_all_attributes_when_provided() {
948 let resource = build_resource("test-svc", Some("1.2.3"), Some("staging"));
949
950 assert_eq!(
951 resource.get(&opentelemetry::Key::new("service.name")),
952 Some(opentelemetry::Value::from("test-svc")),
953 );
954 assert_eq!(
955 resource.get(&opentelemetry::Key::new(SERVICE_VERSION)),
956 Some(opentelemetry::Value::from("1.2.3")),
957 );
958 assert_eq!(
959 resource.get(&opentelemetry::Key::new(DEPLOYMENT_ENVIRONMENT_NAME)),
960 Some(opentelemetry::Value::from("staging")),
961 );
962 assert!(resource.get(&opentelemetry::Key::new(HOST_NAME)).is_some());
963 assert!(
964 resource
965 .get(&opentelemetry::Key::new(PROCESS_PID))
966 .is_some()
967 );
968 }
969
970 #[test]
971 fn resource_graceful_when_optional_values_omitted() {
972 let resource = build_resource("test-svc", None, None);
973
974 assert_eq!(
975 resource.get(&opentelemetry::Key::new("service.name")),
976 Some(opentelemetry::Value::from("test-svc")),
977 );
978 assert!(
979 resource
980 .get(&opentelemetry::Key::new(SERVICE_VERSION))
981 .is_none()
982 );
983 assert!(
984 resource
985 .get(&opentelemetry::Key::new(DEPLOYMENT_ENVIRONMENT_NAME))
986 .is_none()
987 );
988 assert!(resource.get(&opentelemetry::Key::new(HOST_NAME)).is_some());
990 assert!(
991 resource
992 .get(&opentelemetry::Key::new(PROCESS_PID))
993 .is_some()
994 );
995 }
996
997 #[test]
998 fn trace_sampler_ratio_converts_to_sdk() {
999 let sampler = TraceSampler::TraceIdRatio(0.5);
1000 let sdk = sampler.into_sdk_sampler();
1001 assert_eq!(format!("{sdk:?}"), "TraceIdRatioBased(0.5)");
1002 }
1003
1004 #[test]
1005 fn trace_sampler_parent_based_converts_to_sdk() {
1006 let sampler = TraceSampler::ParentBased(Box::new(TraceSampler::TraceIdRatio(0.25)));
1007 let sdk = sampler.into_sdk_sampler();
1008 let debug = format!("{sdk:?}");
1009 assert!(debug.contains("ParentBased"));
1010 assert!(debug.contains("0.25"));
1011 }
1012
1013 unsafe fn set_env(key: &str, val: &str) {
1015 unsafe {
1016 std::env::set_var(key, val);
1017 }
1018 }
1019
1020 unsafe fn remove_env(key: &str) {
1021 unsafe {
1022 std::env::remove_var(key);
1023 }
1024 }
1025
1026 #[test]
1027 fn sampler_from_env_reads_traceidratio() {
1028 let _lock = ENV_LOCK.lock().unwrap();
1029 unsafe {
1030 set_env("OTEL_TRACES_SAMPLER", "traceidratio");
1031 set_env("OTEL_TRACES_SAMPLER_ARG", "0.42");
1032 }
1033
1034 let sampler = sampler_from_env()
1035 .expect("should not error")
1036 .expect("should return Some");
1037 assert!(
1038 matches!(sampler, TraceSampler::TraceIdRatio(r) if (r - 0.42).abs() < f64::EPSILON)
1039 );
1040
1041 unsafe {
1042 remove_env("OTEL_TRACES_SAMPLER");
1043 remove_env("OTEL_TRACES_SAMPLER_ARG");
1044 }
1045 }
1046
1047 #[test]
1048 fn sampler_from_env_returns_none_when_unset() {
1049 let _lock = ENV_LOCK.lock().unwrap();
1050 unsafe {
1051 remove_env("OTEL_TRACES_SAMPLER");
1052 }
1053 assert!(sampler_from_env().expect("should not error").is_none());
1054 }
1055
1056 #[test]
1057 fn sampler_from_env_reads_parentbased_traceidratio() {
1058 let _lock = ENV_LOCK.lock().unwrap();
1059 unsafe {
1060 set_env("OTEL_TRACES_SAMPLER", "parentbased_traceidratio");
1061 set_env("OTEL_TRACES_SAMPLER_ARG", "0.1");
1062 }
1063
1064 let sampler = sampler_from_env()
1065 .expect("should not error")
1066 .expect("should return Some");
1067 assert!(
1068 matches!(sampler, TraceSampler::ParentBased(inner) if matches!(*inner, TraceSampler::TraceIdRatio(r) if (r - 0.1).abs() < f64::EPSILON))
1069 );
1070
1071 unsafe {
1072 remove_env("OTEL_TRACES_SAMPLER");
1073 remove_env("OTEL_TRACES_SAMPLER_ARG");
1074 }
1075 }
1076
1077 #[test]
1078 fn sampler_from_env_parentbased_always_on() {
1079 let _lock = ENV_LOCK.lock().unwrap();
1080 unsafe {
1081 set_env("OTEL_TRACES_SAMPLER", "parentbased_always_on");
1082 }
1083 let sampler = sampler_from_env()
1084 .expect("should not error")
1085 .expect("should return Some");
1086 assert!(
1087 matches!(sampler, TraceSampler::ParentBased(inner) if matches!(*inner, TraceSampler::AlwaysOn))
1088 );
1089 unsafe {
1090 remove_env("OTEL_TRACES_SAMPLER");
1091 }
1092 }
1093
1094 #[test]
1095 fn sampler_from_env_parentbased_always_off() {
1096 let _lock = ENV_LOCK.lock().unwrap();
1097 unsafe {
1098 set_env("OTEL_TRACES_SAMPLER", "parentbased_always_off");
1099 }
1100 let sampler = sampler_from_env()
1101 .expect("should not error")
1102 .expect("should return Some");
1103 assert!(
1104 matches!(sampler, TraceSampler::ParentBased(inner) if matches!(*inner, TraceSampler::AlwaysOff))
1105 );
1106 unsafe {
1107 remove_env("OTEL_TRACES_SAMPLER");
1108 }
1109 }
1110
1111 #[test]
1112 fn sampler_from_env_always_on() {
1113 let _lock = ENV_LOCK.lock().unwrap();
1114 unsafe {
1115 set_env("OTEL_TRACES_SAMPLER", "always_on");
1116 }
1117 let sampler = sampler_from_env()
1118 .expect("should not error")
1119 .expect("should return Some");
1120 assert!(matches!(sampler, TraceSampler::AlwaysOn));
1121 unsafe {
1122 remove_env("OTEL_TRACES_SAMPLER");
1123 }
1124 }
1125
1126 #[test]
1127 fn sampler_from_env_always_off() {
1128 let _lock = ENV_LOCK.lock().unwrap();
1129 unsafe {
1130 set_env("OTEL_TRACES_SAMPLER", "always_off");
1131 }
1132 let sampler = sampler_from_env()
1133 .expect("should not error")
1134 .expect("should return Some");
1135 assert!(matches!(sampler, TraceSampler::AlwaysOff));
1136 unsafe {
1137 remove_env("OTEL_TRACES_SAMPLER");
1138 }
1139 }
1140
1141 #[test]
1142 fn sampler_from_env_unknown_returns_error() {
1143 let _lock = ENV_LOCK.lock().unwrap();
1144 unsafe {
1145 set_env("OTEL_TRACES_SAMPLER", "unknown_sampler");
1146 }
1147 let err = sampler_from_env().expect_err("unknown sampler should produce an error");
1148 assert!(
1149 err.to_string().contains("unknown_sampler"),
1150 "error message should include the unknown name, got: {err}"
1151 );
1152 unsafe {
1153 remove_env("OTEL_TRACES_SAMPLER");
1154 }
1155 }
1156
1157 #[test]
1158 fn trace_sampler_always_on_converts_to_sdk() {
1159 let sdk = TraceSampler::AlwaysOn.into_sdk_sampler();
1160 assert_eq!(format!("{sdk:?}"), "AlwaysOn");
1161 }
1162
1163 #[test]
1164 fn trace_sampler_always_off_converts_to_sdk() {
1165 let sdk = TraceSampler::AlwaysOff.into_sdk_sampler();
1166 assert_eq!(format!("{sdk:?}"), "AlwaysOff");
1167 }
1168
1169 #[test]
1170 fn builder_has_sensible_defaults() {
1171 let builder = Telemetry::builder("test-svc");
1172 assert_eq!(builder.service_name.as_deref(), Some("test-svc"));
1173 assert!(builder.service_version.is_none());
1174 assert!(builder.deployment_environment.is_none());
1175 assert!(builder.sampler.is_none());
1176 assert!(builder.metrics);
1177 assert!(!builder.logs);
1178 assert!(builder.protocol.is_none());
1179 assert!(builder.max_export_batch_size.is_none());
1180 assert!(builder.metric_export_interval.is_none());
1181 assert!(builder.export_timeout.is_none());
1182 }
1183
1184 #[test]
1185 fn from_env_builder_has_no_service_name() {
1186 let builder = Telemetry::from_env();
1187 assert!(builder.service_name.is_none());
1188 }
1189
1190 #[test]
1191 fn with_export_timeout_stores_value() {
1192 let timeout = Duration::from_secs(5);
1193 let builder = Telemetry::builder("test-svc").with_export_timeout(timeout);
1194 assert_eq!(builder.export_timeout, Some(timeout));
1195 }
1196
1197 #[test]
1198 fn timeout_from_env_reads_milliseconds() {
1199 let _lock = ENV_LOCK.lock().unwrap();
1200 unsafe {
1201 set_env("OTEL_EXPORTER_OTLP_TIMEOUT", "5000");
1202 }
1203 let t = timeout_from_env();
1204 assert_eq!(t, Some(Duration::from_millis(5000)));
1205 unsafe {
1206 remove_env("OTEL_EXPORTER_OTLP_TIMEOUT");
1207 }
1208 }
1209
1210 #[test]
1211 fn timeout_from_env_returns_none_when_unset() {
1212 let _lock = ENV_LOCK.lock().unwrap();
1213 unsafe {
1214 remove_env("OTEL_EXPORTER_OTLP_TIMEOUT");
1215 }
1216 assert_eq!(timeout_from_env(), None);
1217 }
1218
1219 #[test]
1220 fn service_name_from_env_used_when_none_given() {
1221 let builder = Telemetry::from_env();
1222 assert!(builder.service_name.is_none());
1223 }
1224
1225 #[test]
1226 fn explicit_service_name_overrides_env_var() {
1227 let builder = Telemetry::builder("explicit-svc");
1228 assert_eq!(builder.service_name.as_deref(), Some("explicit-svc"));
1229 }
1230
1231 #[test]
1232 fn from_env_builder_service_name_is_none() {
1233 let builder = Telemetry::from_env();
1234 assert!(builder.service_name.is_none());
1235 }
1236
1237 #[test]
1238 fn init_returns_error_for_unknown_otel_traces_sampler() {
1239 let _lock = ENV_LOCK.lock().unwrap();
1240 unsafe {
1241 set_env("OTEL_TRACES_SAMPLER", "not_a_real_sampler");
1242 }
1243 let result = Telemetry::builder("test-svc").with_metrics(false).init();
1244 let err = result
1245 .err()
1246 .expect("unknown sampler env var should cause init to fail");
1247 assert!(
1248 err.to_string().contains("not_a_real_sampler"),
1249 "error should name the unknown sampler, got: {err}"
1250 );
1251 unsafe {
1252 remove_env("OTEL_TRACES_SAMPLER");
1253 }
1254 }
1255
1256 #[test]
1257 fn with_max_export_batch_size_stores_value() {
1258 let builder = Telemetry::builder("test-svc").with_max_export_batch_size(1024);
1259 assert_eq!(builder.max_export_batch_size, Some(1024));
1260 }
1261
1262 #[test]
1263 fn with_metric_export_interval_stores_value() {
1264 let interval = Duration::from_secs(30);
1265 let builder = Telemetry::builder("test-svc").with_metric_export_interval(interval);
1266 assert_eq!(builder.metric_export_interval, Some(interval));
1267 }
1268
1269 #[test]
1270 fn init_rejects_zero_metric_export_interval() {
1271 let err = Telemetry::builder("test-svc")
1272 .with_metric_export_interval(Duration::ZERO)
1273 .with_metrics(false)
1274 .init()
1275 .err()
1276 .expect("expected error for zero interval");
1277 assert!(
1278 err.to_string().contains("metric_export_interval"),
1279 "error message should mention metric_export_interval, got: {err}"
1280 );
1281 }
1282
1283 #[test]
1284 fn builder_with_custom_values() {
1285 let builder = Telemetry::builder("test-svc")
1286 .with_version("2.0.0")
1287 .with_environment("production")
1288 .with_sampler(TraceSampler::TraceIdRatio(0.5))
1289 .with_metrics(false);
1290
1291 assert_eq!(builder.service_name.as_deref(), Some("test-svc"));
1292 assert_eq!(builder.service_version.as_deref(), Some("2.0.0"));
1293 assert_eq!(
1294 builder.deployment_environment.as_deref(),
1295 Some("production")
1296 );
1297 assert!(
1298 matches!(builder.sampler, Some(TraceSampler::TraceIdRatio(r)) if (r - 0.5).abs() < f64::EPSILON)
1299 );
1300 assert!(!builder.metrics);
1301 }
1302
1303 #[test]
1304 #[cfg(feature = "grpc")]
1305 fn builder_with_protocol_grpc() {
1306 let builder = Telemetry::builder("test-svc").with_protocol(ExportProtocol::Grpc);
1307 assert_eq!(builder.protocol, Some(ExportProtocol::Grpc));
1308 }
1309
1310 #[test]
1311 #[cfg(feature = "http")]
1312 fn builder_with_protocol_http() {
1313 let builder = Telemetry::builder("test-svc").with_protocol(ExportProtocol::HttpProtobuf);
1314 assert_eq!(builder.protocol, Some(ExportProtocol::HttpProtobuf));
1315 }
1316
1317 #[test]
1318 #[cfg(feature = "grpc")]
1319 fn protocol_from_env_reads_grpc() {
1320 let _lock = ENV_LOCK.lock().unwrap();
1321 unsafe {
1322 set_env("OTEL_EXPORTER_OTLP_PROTOCOL", "grpc");
1323 }
1324 assert_eq!(protocol_from_env(), Some(ExportProtocol::Grpc));
1325 unsafe {
1326 remove_env("OTEL_EXPORTER_OTLP_PROTOCOL");
1327 }
1328 }
1329
1330 #[test]
1331 #[cfg(feature = "http")]
1332 fn protocol_from_env_reads_http_protobuf() {
1333 let _lock = ENV_LOCK.lock().unwrap();
1334 unsafe {
1335 set_env("OTEL_EXPORTER_OTLP_PROTOCOL", "http/protobuf");
1336 }
1337 assert_eq!(protocol_from_env(), Some(ExportProtocol::HttpProtobuf));
1338 unsafe {
1339 remove_env("OTEL_EXPORTER_OTLP_PROTOCOL");
1340 }
1341 }
1342
1343 #[test]
1344 fn protocol_from_env_returns_none_when_unset() {
1345 let _lock = ENV_LOCK.lock().unwrap();
1346 unsafe {
1347 remove_env("OTEL_EXPORTER_OTLP_PROTOCOL");
1348 }
1349 assert_eq!(protocol_from_env(), None);
1350 }
1351
1352 #[test]
1353 fn protocol_from_env_returns_none_for_unknown() {
1354 let _lock = ENV_LOCK.lock().unwrap();
1355 unsafe {
1356 set_env("OTEL_EXPORTER_OTLP_PROTOCOL", "websocket");
1357 }
1358 assert_eq!(protocol_from_env(), None);
1359 unsafe {
1360 remove_env("OTEL_EXPORTER_OTLP_PROTOCOL");
1361 }
1362 }
1363
1364 #[test]
1365 fn builder_is_send_and_sync() {
1366 fn assert_send_sync<T: Send + Sync>() {}
1367 assert_send_sync::<TelemetryBuilder>();
1368 }
1369
1370 #[test]
1371 fn with_shutdown_timeout_stores_value() {
1372 let timeout = Duration::from_secs(10);
1373 let builder = Telemetry::builder("test-svc").with_shutdown_timeout(timeout);
1374 assert_eq!(builder.shutdown_timeout, timeout);
1375 }
1376
1377 #[test]
1378 fn default_shutdown_timeout_is_five_seconds() {
1379 let builder = Telemetry::builder("test-svc");
1380 assert_eq!(builder.shutdown_timeout, Duration::from_secs(5));
1381 }
1382
1383 #[cfg(feature = "testing")]
1391 #[test]
1392 fn drop_completes_within_shutdown_timeout() {
1393 let mut handles = crate::Telemetry::testing("drop-timeout-test");
1395 handles.shutdown_timeout = Duration::from_millis(100);
1397
1398 let start = std::time::Instant::now();
1399 drop(handles);
1400 let elapsed = start.elapsed();
1401
1402 assert!(
1404 elapsed < Duration::from_millis(500),
1405 "drop took {elapsed:?}, expected < 500 ms"
1406 );
1407 }
1408}