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 = "org-context")]
36pub mod span_enrichment;
37
38use opentelemetry::KeyValue;
39use opentelemetry::propagation::TextMapCompositePropagator;
40use opentelemetry_otlp::WithExportConfig;
41use opentelemetry_sdk::{
42 Resource,
43 logs::SdkLoggerProvider,
44 metrics::{MeterProviderBuilder, PeriodicReader, SdkMeterProvider},
45 propagation::{BaggagePropagator, TraceContextPropagator},
46 trace::{BatchConfigBuilder, BatchSpanProcessor, Sampler, SdkTracerProvider},
47};
48use opentelemetry_semantic_conventions::attribute::{
49 DEPLOYMENT_ENVIRONMENT_NAME, HOST_NAME, PROCESS_PID, SERVICE_VERSION,
50};
51use std::error::Error;
52use std::time::Duration;
53use tracing_subscriber::layer::SubscriberExt;
54use tracing_subscriber::util::SubscriberInitExt;
55
56#[derive(Debug, Clone)]
71pub enum TraceSampler {
72 AlwaysOn,
74 AlwaysOff,
76 TraceIdRatio(f64),
78 ParentBased(Box<TraceSampler>),
81}
82
83impl TraceSampler {
84 fn into_sdk_sampler(self) -> Sampler {
86 match self {
87 TraceSampler::AlwaysOn => Sampler::AlwaysOn,
88 TraceSampler::AlwaysOff => Sampler::AlwaysOff,
89 TraceSampler::TraceIdRatio(r) => Sampler::TraceIdRatioBased(r),
90 TraceSampler::ParentBased(inner) => {
91 Sampler::ParentBased(Box::new(inner.into_sdk_sampler()))
92 }
93 }
94 }
95}
96
97fn sampler_from_env() -> Result<Option<TraceSampler>, Box<dyn Error>> {
105 let name = match std::env::var("OTEL_TRACES_SAMPLER") {
106 Ok(v) => v,
107 Err(_) => return Ok(None),
108 };
109 let arg = std::env::var("OTEL_TRACES_SAMPLER_ARG").ok();
110 let sampler = match name.as_str() {
111 "always_on" => TraceSampler::AlwaysOn,
112 "always_off" => TraceSampler::AlwaysOff,
113 "traceidratio" => {
114 let ratio = arg
115 .as_deref()
116 .unwrap_or("1.0")
117 .parse::<f64>()
118 .unwrap_or(1.0);
119 TraceSampler::TraceIdRatio(ratio)
120 }
121 "parentbased_always_on" => TraceSampler::ParentBased(Box::new(TraceSampler::AlwaysOn)),
122 "parentbased_always_off" => TraceSampler::ParentBased(Box::new(TraceSampler::AlwaysOff)),
123 "parentbased_traceidratio" => {
124 let ratio = arg
125 .as_deref()
126 .unwrap_or("1.0")
127 .parse::<f64>()
128 .unwrap_or(1.0);
129 TraceSampler::ParentBased(Box::new(TraceSampler::TraceIdRatio(ratio)))
130 }
131 unknown => {
132 return Err(format!(
133 "OTEL_TRACES_SAMPLER: unrecognised sampler name '{unknown}'. \
134 Valid values: always_on, always_off, traceidratio, \
135 parentbased_always_on, parentbased_always_off, parentbased_traceidratio"
136 )
137 .into());
138 }
139 };
140 Ok(Some(sampler))
141}
142
143const DEFAULT_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5);
145
146pub struct TelemetryHandles {
167 pub tracer_provider: SdkTracerProvider,
168 pub meter_provider: Option<SdkMeterProvider>,
169 pub logger_provider: Option<SdkLoggerProvider>,
170 shutdown_timeout: Duration,
171}
172
173impl TelemetryHandles {
174 pub fn shutdown(&self) -> Result<(), Box<dyn Error>> {
187 self.tracer_provider.shutdown()?;
188 if let Some(mp) = &self.meter_provider {
189 mp.shutdown()?;
190 }
191 if let Some(lp) = &self.logger_provider {
192 lp.shutdown()?;
193 }
194 Ok(())
195 }
196}
197
198impl Drop for TelemetryHandles {
199 fn drop(&mut self) {
200 let tracer_provider = self.tracer_provider.clone();
201 let meter_provider = self.meter_provider.clone();
202 let logger_provider = self.logger_provider.clone();
203 let timeout = self.shutdown_timeout;
204
205 let (tx, rx) = std::sync::mpsc::channel();
206 std::thread::spawn(move || {
207 if let Err(e) = tracer_provider.shutdown() {
208 tracing::warn!("tracer provider shutdown error: {e}");
209 }
210 if let Some(mp) = meter_provider
211 && let Err(e) = mp.shutdown()
212 {
213 tracing::warn!("meter provider shutdown error: {e}");
214 }
215 if let Some(lp) = logger_provider
216 && let Err(e) = lp.shutdown()
217 {
218 tracing::warn!("logger provider shutdown error: {e}");
219 }
220 let _ = tx.send(());
221 });
222
223 if rx.recv_timeout(timeout).is_err() {
224 tracing::warn!(
225 "telemetry shutdown did not complete within {timeout:?}; \
226 some spans/metrics may not have been exported"
227 );
228 }
229 }
230}
231
232#[derive(Debug, Clone, Copy, PartialEq, Eq)]
255pub enum ExportProtocol {
256 #[cfg(feature = "grpc")]
258 Grpc,
259 #[cfg(feature = "http")]
261 HttpProtobuf,
262}
263
264fn protocol_from_env() -> Option<ExportProtocol> {
266 let val = std::env::var("OTEL_EXPORTER_OTLP_PROTOCOL").ok()?;
267 match val.trim() {
268 #[cfg(feature = "grpc")]
269 "grpc" => Some(ExportProtocol::Grpc),
270 #[cfg(feature = "http")]
271 "http/protobuf" => Some(ExportProtocol::HttpProtobuf),
272 _ => None,
273 }
274}
275
276pub struct Telemetry;
292
293impl Telemetry {
294 pub fn builder(service_name: &str) -> TelemetryBuilder {
298 TelemetryBuilder {
299 service_name: Some(service_name.to_string()),
300 service_version: None,
301 deployment_environment: None,
302 sampler: None,
303 metrics: true,
304 logs: false,
305 protocol: None,
306 max_export_batch_size: None,
307 metric_export_interval: None,
308 export_timeout: None,
309 shutdown_timeout: DEFAULT_SHUTDOWN_TIMEOUT,
310 extra_layers: Vec::new(),
311 extra_metric_readers: Vec::new(),
312 }
313 }
314
315 pub fn from_env() -> TelemetryBuilder {
325 TelemetryBuilder {
326 service_name: None,
327 service_version: None,
328 deployment_environment: None,
329 sampler: None,
330 metrics: true,
331 logs: false,
332 protocol: None,
333 max_export_batch_size: None,
334 metric_export_interval: None,
335 export_timeout: None,
336 shutdown_timeout: DEFAULT_SHUTDOWN_TIMEOUT,
337 extra_layers: Vec::new(),
338 extra_metric_readers: Vec::new(),
339 }
340 }
341}
342
343#[must_use = "a TelemetryBuilder does nothing until .init() is called"]
361pub struct TelemetryBuilder {
362 service_name: Option<String>,
363 service_version: Option<String>,
364 deployment_environment: Option<String>,
365 sampler: Option<TraceSampler>,
366 metrics: bool,
367 logs: bool,
368 protocol: Option<ExportProtocol>,
369 max_export_batch_size: Option<usize>,
370 metric_export_interval: Option<Duration>,
371 export_timeout: Option<Duration>,
372 shutdown_timeout: Duration,
373 extra_layers: Vec<
374 Box<dyn tracing_subscriber::Layer<tracing_subscriber::Registry> + Send + Sync + 'static>,
375 >,
376 extra_metric_readers: Vec<MeterProviderInstaller>,
377}
378
379type MeterProviderInstaller =
384 Box<dyn FnOnce(MeterProviderBuilder) -> MeterProviderBuilder + Send + Sync>;
385
386impl TelemetryBuilder {
387 pub fn with_version(mut self, version: &str) -> Self {
389 self.service_version = Some(version.to_string());
390 self
391 }
392
393 pub fn with_environment(mut self, environment: &str) -> Self {
395 self.deployment_environment = Some(environment.to_string());
396 self
397 }
398
399 pub fn with_sampler(mut self, sampler: TraceSampler) -> Self {
402 self.sampler = Some(sampler);
403 self
404 }
405
406 pub fn with_metrics(mut self, enabled: bool) -> Self {
408 self.metrics = enabled;
409 self
410 }
411
412 pub fn with_protocol(mut self, protocol: ExportProtocol) -> Self {
416 self.protocol = Some(protocol);
417 self
418 }
419
420 pub fn with_max_export_batch_size(mut self, size: usize) -> Self {
425 self.max_export_batch_size = Some(size);
426 self
427 }
428
429 pub fn with_metric_export_interval(mut self, interval: Duration) -> Self {
434 self.metric_export_interval = Some(interval);
435 self
436 }
437
438 pub fn with_logs(mut self, enabled: bool) -> Self {
445 self.logs = enabled;
446 self
447 }
448
449 pub fn with_export_timeout(mut self, timeout: Duration) -> Self {
453 self.export_timeout = Some(timeout);
454 self
455 }
456
457 pub fn with_shutdown_timeout(mut self, timeout: Duration) -> Self {
464 self.shutdown_timeout = timeout;
465 self
466 }
467
468 pub fn with_meter_provider_setup<F>(mut self, setup: F) -> Self
525 where
526 F: FnOnce(MeterProviderBuilder) -> MeterProviderBuilder + Send + Sync + 'static,
527 {
528 self.extra_metric_readers.push(Box::new(setup));
529 self
530 }
531
532 pub fn with_layer<L>(mut self, layer: L) -> Self
533 where
534 L: tracing_subscriber::Layer<tracing_subscriber::Registry> + Send + Sync + 'static,
535 {
536 self.extra_layers.push(Box::new(layer));
537 self
538 }
539
540 pub fn init(self) -> Result<TelemetryHandles, Box<dyn Error>> {
555 if let Some(interval) = self.metric_export_interval
556 && interval.is_zero()
557 {
558 return Err("metric_export_interval must be greater than zero".into());
559 }
560
561 let protocol = self.protocol.or_else(protocol_from_env).unwrap_or({
562 #[cfg(feature = "grpc")]
563 {
564 ExportProtocol::Grpc
565 }
566 #[cfg(all(not(feature = "grpc"), feature = "http"))]
567 {
568 ExportProtocol::HttpProtobuf
569 }
570 });
571
572 let default_endpoint = match protocol {
573 #[cfg(feature = "grpc")]
574 ExportProtocol::Grpc => "http://localhost:4317",
575 #[cfg(feature = "http")]
576 ExportProtocol::HttpProtobuf => "http://localhost:4318",
577 };
578 let endpoint = std::env::var("OTEL_EXPORTER_OTLP_ENDPOINT")
579 .unwrap_or_else(|_| default_endpoint.to_string());
580
581 let export_timeout = self.export_timeout.or_else(timeout_from_env);
583
584 let service_name = self.service_name.unwrap_or_else(|| {
586 std::env::var("OTEL_SERVICE_NAME").unwrap_or_else(|_| "unknown_service".to_string())
587 });
588
589 let resource = build_resource(
590 &service_name,
591 self.service_version.as_deref(),
592 self.deployment_environment.as_deref(),
593 );
594
595 let sampler = match self.sampler {
596 Some(s) => s,
597 None => sampler_from_env()?.unwrap_or(TraceSampler::AlwaysOn),
598 };
599
600 let trace_exporter = build_span_exporter(protocol, &endpoint, export_timeout)?;
602
603 let batch_processor = if let Some(size) = self.max_export_batch_size {
604 BatchSpanProcessor::builder(trace_exporter)
605 .with_batch_config(
606 BatchConfigBuilder::default()
607 .with_max_export_batch_size(size)
608 .build(),
609 )
610 .build()
611 } else {
612 BatchSpanProcessor::builder(trace_exporter).build()
613 };
614
615 let tracer_provider = SdkTracerProvider::builder()
616 .with_resource(resource.clone())
617 .with_sampler(sampler.into_sdk_sampler())
618 .with_span_processor(batch_processor)
619 .build();
620
621 opentelemetry::global::set_tracer_provider(tracer_provider.clone());
622
623 let propagator = TextMapCompositePropagator::new(vec![
625 Box::new(TraceContextPropagator::new()),
626 Box::new(BaggagePropagator::new()),
627 ]);
628 opentelemetry::global::set_text_map_propagator(propagator);
629
630 let meter_provider = if self.metrics {
632 let metric_exporter = build_metric_exporter(protocol, &endpoint, export_timeout)?;
633
634 let periodic_reader = if let Some(interval) = self.metric_export_interval {
635 PeriodicReader::builder(metric_exporter)
636 .with_interval(interval)
637 .build()
638 } else {
639 PeriodicReader::builder(metric_exporter).build()
640 };
641
642 let mut mp_builder = SdkMeterProvider::builder()
643 .with_resource(resource.clone())
644 .with_reader(periodic_reader);
645 for installer in self.extra_metric_readers {
646 mp_builder = installer(mp_builder);
647 }
648 let mp = mp_builder.build();
649
650 opentelemetry::global::set_meter_provider(mp.clone());
651
652 Some(mp)
653 } else {
654 None
655 };
656
657 let logger_provider = if self.logs {
659 let log_exporter = build_log_exporter(protocol, &endpoint, export_timeout)?;
660
661 let lp = SdkLoggerProvider::builder()
662 .with_resource(resource)
663 .with_batch_exporter(log_exporter)
664 .build();
665
666 Some(lp)
667 } else {
668 None
669 };
670
671 let otel_layer = tracing_opentelemetry::layer();
673
674 let registry = tracing_subscriber::registry()
675 .with(self.extra_layers)
676 .with(tracing_subscriber::EnvFilter::from_default_env())
677 .with(tracing_subscriber::fmt::layer())
678 .with(otel_layer);
679
680 if let Some(lp) = &logger_provider {
681 registry
682 .with(opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge::new(lp))
683 .try_init()
684 .ok();
685 } else {
686 registry.try_init().ok();
687 }
688
689 Ok(TelemetryHandles {
690 tracer_provider,
691 meter_provider,
692 logger_provider,
693 shutdown_timeout: self.shutdown_timeout,
694 })
695 }
696}
697
698pub fn init_telemetry(service_name: &str) -> Result<TelemetryHandles, Box<dyn Error>> {
712 Telemetry::builder(service_name).init()
713}
714
715pub fn init_telemetry_with_sampler(
732 service_name: &str,
733 sampler: Option<TraceSampler>,
734) -> Result<TelemetryHandles, Box<dyn Error>> {
735 let builder = Telemetry::builder(service_name);
736 match sampler {
737 Some(s) => builder.with_sampler(s),
738 None => builder, }
740 .init()
741}
742
743fn timeout_from_env() -> Option<Duration> {
745 let ms = std::env::var("OTEL_EXPORTER_OTLP_TIMEOUT").ok()?;
746 let ms: u64 = ms.trim().parse().ok()?;
747 Some(Duration::from_millis(ms))
748}
749
750fn build_span_exporter(
751 protocol: ExportProtocol,
752 endpoint: &str,
753 timeout: Option<Duration>,
754) -> Result<opentelemetry_otlp::SpanExporter, Box<dyn Error>> {
755 match protocol {
756 #[cfg(feature = "grpc")]
757 ExportProtocol::Grpc => {
758 let mut b = opentelemetry_otlp::SpanExporter::builder()
759 .with_tonic()
760 .with_endpoint(endpoint);
761 if let Some(t) = timeout {
762 b = b.with_timeout(t);
763 }
764 Ok(b.build()?)
765 }
766 #[cfg(feature = "http")]
767 ExportProtocol::HttpProtobuf => {
768 let mut b = opentelemetry_otlp::SpanExporter::builder()
769 .with_http()
770 .with_endpoint(endpoint);
771 if let Some(t) = timeout {
772 b = b.with_timeout(t);
773 }
774 Ok(b.build()?)
775 }
776 }
777}
778
779fn build_metric_exporter(
780 protocol: ExportProtocol,
781 endpoint: &str,
782 timeout: Option<Duration>,
783) -> Result<opentelemetry_otlp::MetricExporter, Box<dyn Error>> {
784 match protocol {
785 #[cfg(feature = "grpc")]
786 ExportProtocol::Grpc => {
787 let mut b = opentelemetry_otlp::MetricExporter::builder()
788 .with_tonic()
789 .with_endpoint(endpoint);
790 if let Some(t) = timeout {
791 b = b.with_timeout(t);
792 }
793 Ok(b.build()?)
794 }
795 #[cfg(feature = "http")]
796 ExportProtocol::HttpProtobuf => {
797 let mut b = opentelemetry_otlp::MetricExporter::builder()
798 .with_http()
799 .with_endpoint(endpoint);
800 if let Some(t) = timeout {
801 b = b.with_timeout(t);
802 }
803 Ok(b.build()?)
804 }
805 }
806}
807
808fn build_log_exporter(
809 protocol: ExportProtocol,
810 endpoint: &str,
811 timeout: Option<Duration>,
812) -> Result<opentelemetry_otlp::LogExporter, Box<dyn Error>> {
813 match protocol {
814 #[cfg(feature = "grpc")]
815 ExportProtocol::Grpc => {
816 let mut b = opentelemetry_otlp::LogExporter::builder()
817 .with_tonic()
818 .with_endpoint(endpoint);
819 if let Some(t) = timeout {
820 b = b.with_timeout(t);
821 }
822 Ok(b.build()?)
823 }
824 #[cfg(feature = "http")]
825 ExportProtocol::HttpProtobuf => {
826 let mut b = opentelemetry_otlp::LogExporter::builder()
827 .with_http()
828 .with_endpoint(endpoint);
829 if let Some(t) = timeout {
830 b = b.with_timeout(t);
831 }
832 Ok(b.build()?)
833 }
834 }
835}
836
837pub fn build_resource(
852 service_name: &str,
853 service_version: Option<&str>,
854 deployment_environment: Option<&str>,
855) -> Resource {
856 let hostname = hostname::get()
857 .ok()
858 .and_then(|h| h.into_string().ok())
859 .unwrap_or_default();
860
861 let mut builder = Resource::builder()
862 .with_service_name(service_name.to_string())
863 .with_attributes([
864 KeyValue::new(HOST_NAME, hostname),
865 KeyValue::new(PROCESS_PID, std::process::id() as i64),
866 ]);
867
868 if let Some(version) = service_version {
869 builder = builder.with_attribute(KeyValue::new(SERVICE_VERSION, version.to_string()));
870 }
871
872 if let Some(env) = deployment_environment {
873 builder =
874 builder.with_attribute(KeyValue::new(DEPLOYMENT_ENVIRONMENT_NAME, env.to_string()));
875 }
876
877 builder.build()
878}
879
880#[cfg(feature = "axum")]
898pub fn axum_layer() -> axum_middleware::OtelTraceLayer {
899 axum_middleware::OtelTraceLayer
900}
901
902#[cfg(all(feature = "axum", feature = "org-context"))]
932pub fn org_context_span_enricher_layer() -> axum_middleware::OrgContextSpanEnricher {
933 axum_middleware::OrgContextSpanEnricher
934}
935
936#[cfg(test)]
937mod tests {
938 use super::*;
939
940 #[test]
941 fn resource_contains_all_attributes_when_provided() {
942 let resource = build_resource("test-svc", Some("1.2.3"), Some("staging"));
943
944 assert_eq!(
945 resource.get(&opentelemetry::Key::new("service.name")),
946 Some(opentelemetry::Value::from("test-svc")),
947 );
948 assert_eq!(
949 resource.get(&opentelemetry::Key::new(SERVICE_VERSION)),
950 Some(opentelemetry::Value::from("1.2.3")),
951 );
952 assert_eq!(
953 resource.get(&opentelemetry::Key::new(DEPLOYMENT_ENVIRONMENT_NAME)),
954 Some(opentelemetry::Value::from("staging")),
955 );
956 assert!(resource.get(&opentelemetry::Key::new(HOST_NAME)).is_some());
957 assert!(
958 resource
959 .get(&opentelemetry::Key::new(PROCESS_PID))
960 .is_some()
961 );
962 }
963
964 #[test]
965 fn resource_graceful_when_optional_values_omitted() {
966 let resource = build_resource("test-svc", None, None);
967
968 assert_eq!(
969 resource.get(&opentelemetry::Key::new("service.name")),
970 Some(opentelemetry::Value::from("test-svc")),
971 );
972 assert!(
973 resource
974 .get(&opentelemetry::Key::new(SERVICE_VERSION))
975 .is_none()
976 );
977 assert!(
978 resource
979 .get(&opentelemetry::Key::new(DEPLOYMENT_ENVIRONMENT_NAME))
980 .is_none()
981 );
982 assert!(resource.get(&opentelemetry::Key::new(HOST_NAME)).is_some());
984 assert!(
985 resource
986 .get(&opentelemetry::Key::new(PROCESS_PID))
987 .is_some()
988 );
989 }
990
991 #[test]
992 fn trace_sampler_ratio_converts_to_sdk() {
993 let sampler = TraceSampler::TraceIdRatio(0.5);
994 let sdk = sampler.into_sdk_sampler();
995 assert_eq!(format!("{sdk:?}"), "TraceIdRatioBased(0.5)");
996 }
997
998 #[test]
999 fn trace_sampler_parent_based_converts_to_sdk() {
1000 let sampler = TraceSampler::ParentBased(Box::new(TraceSampler::TraceIdRatio(0.25)));
1001 let sdk = sampler.into_sdk_sampler();
1002 let debug = format!("{sdk:?}");
1003 assert!(debug.contains("ParentBased"));
1004 assert!(debug.contains("0.25"));
1005 }
1006
1007 unsafe fn set_env(key: &str, val: &str) {
1009 unsafe {
1010 std::env::set_var(key, val);
1011 }
1012 }
1013
1014 unsafe fn remove_env(key: &str) {
1015 unsafe {
1016 std::env::remove_var(key);
1017 }
1018 }
1019
1020 #[test]
1021 fn sampler_from_env_reads_traceidratio() {
1022 unsafe {
1023 set_env("OTEL_TRACES_SAMPLER", "traceidratio");
1024 set_env("OTEL_TRACES_SAMPLER_ARG", "0.42");
1025 }
1026
1027 let sampler = sampler_from_env()
1028 .expect("should not error")
1029 .expect("should return Some");
1030 assert!(
1031 matches!(sampler, TraceSampler::TraceIdRatio(r) if (r - 0.42).abs() < f64::EPSILON)
1032 );
1033
1034 unsafe {
1035 remove_env("OTEL_TRACES_SAMPLER");
1036 remove_env("OTEL_TRACES_SAMPLER_ARG");
1037 }
1038 }
1039
1040 #[test]
1041 fn sampler_from_env_returns_none_when_unset() {
1042 unsafe {
1043 remove_env("OTEL_TRACES_SAMPLER");
1044 }
1045 assert!(sampler_from_env().expect("should not error").is_none());
1046 }
1047
1048 #[test]
1049 fn sampler_from_env_reads_parentbased_traceidratio() {
1050 unsafe {
1051 set_env("OTEL_TRACES_SAMPLER", "parentbased_traceidratio");
1052 set_env("OTEL_TRACES_SAMPLER_ARG", "0.1");
1053 }
1054
1055 let sampler = sampler_from_env()
1056 .expect("should not error")
1057 .expect("should return Some");
1058 assert!(
1059 matches!(sampler, TraceSampler::ParentBased(inner) if matches!(*inner, TraceSampler::TraceIdRatio(r) if (r - 0.1).abs() < f64::EPSILON))
1060 );
1061
1062 unsafe {
1063 remove_env("OTEL_TRACES_SAMPLER");
1064 remove_env("OTEL_TRACES_SAMPLER_ARG");
1065 }
1066 }
1067
1068 #[test]
1069 fn sampler_from_env_parentbased_always_on() {
1070 unsafe {
1071 set_env("OTEL_TRACES_SAMPLER", "parentbased_always_on");
1072 }
1073 let sampler = sampler_from_env()
1074 .expect("should not error")
1075 .expect("should return Some");
1076 assert!(
1077 matches!(sampler, TraceSampler::ParentBased(inner) if matches!(*inner, TraceSampler::AlwaysOn))
1078 );
1079 unsafe {
1080 remove_env("OTEL_TRACES_SAMPLER");
1081 }
1082 }
1083
1084 #[test]
1085 fn sampler_from_env_parentbased_always_off() {
1086 unsafe {
1087 set_env("OTEL_TRACES_SAMPLER", "parentbased_always_off");
1088 }
1089 let sampler = sampler_from_env()
1090 .expect("should not error")
1091 .expect("should return Some");
1092 assert!(
1093 matches!(sampler, TraceSampler::ParentBased(inner) if matches!(*inner, TraceSampler::AlwaysOff))
1094 );
1095 unsafe {
1096 remove_env("OTEL_TRACES_SAMPLER");
1097 }
1098 }
1099
1100 #[test]
1101 fn sampler_from_env_always_on() {
1102 unsafe {
1103 set_env("OTEL_TRACES_SAMPLER", "always_on");
1104 }
1105 let sampler = sampler_from_env()
1106 .expect("should not error")
1107 .expect("should return Some");
1108 assert!(matches!(sampler, TraceSampler::AlwaysOn));
1109 unsafe {
1110 remove_env("OTEL_TRACES_SAMPLER");
1111 }
1112 }
1113
1114 #[test]
1115 fn sampler_from_env_always_off() {
1116 unsafe {
1117 set_env("OTEL_TRACES_SAMPLER", "always_off");
1118 }
1119 let sampler = sampler_from_env()
1120 .expect("should not error")
1121 .expect("should return Some");
1122 assert!(matches!(sampler, TraceSampler::AlwaysOff));
1123 unsafe {
1124 remove_env("OTEL_TRACES_SAMPLER");
1125 }
1126 }
1127
1128 #[test]
1129 fn sampler_from_env_unknown_returns_error() {
1130 unsafe {
1131 set_env("OTEL_TRACES_SAMPLER", "unknown_sampler");
1132 }
1133 let err = sampler_from_env().expect_err("unknown sampler should produce an error");
1134 assert!(
1135 err.to_string().contains("unknown_sampler"),
1136 "error message should include the unknown name, got: {err}"
1137 );
1138 unsafe {
1139 remove_env("OTEL_TRACES_SAMPLER");
1140 }
1141 }
1142
1143 #[test]
1144 fn trace_sampler_always_on_converts_to_sdk() {
1145 let sdk = TraceSampler::AlwaysOn.into_sdk_sampler();
1146 assert_eq!(format!("{sdk:?}"), "AlwaysOn");
1147 }
1148
1149 #[test]
1150 fn trace_sampler_always_off_converts_to_sdk() {
1151 let sdk = TraceSampler::AlwaysOff.into_sdk_sampler();
1152 assert_eq!(format!("{sdk:?}"), "AlwaysOff");
1153 }
1154
1155 #[test]
1156 fn builder_has_sensible_defaults() {
1157 let builder = Telemetry::builder("test-svc");
1158 assert_eq!(builder.service_name.as_deref(), Some("test-svc"));
1159 assert!(builder.service_version.is_none());
1160 assert!(builder.deployment_environment.is_none());
1161 assert!(builder.sampler.is_none());
1162 assert!(builder.metrics);
1163 assert!(!builder.logs);
1164 assert!(builder.protocol.is_none());
1165 assert!(builder.max_export_batch_size.is_none());
1166 assert!(builder.metric_export_interval.is_none());
1167 assert!(builder.export_timeout.is_none());
1168 }
1169
1170 #[test]
1171 fn from_env_builder_has_no_service_name() {
1172 let builder = Telemetry::from_env();
1173 assert!(builder.service_name.is_none());
1174 }
1175
1176 #[test]
1177 fn with_export_timeout_stores_value() {
1178 let timeout = Duration::from_secs(5);
1179 let builder = Telemetry::builder("test-svc").with_export_timeout(timeout);
1180 assert_eq!(builder.export_timeout, Some(timeout));
1181 }
1182
1183 #[test]
1184 fn timeout_from_env_reads_milliseconds() {
1185 unsafe {
1186 set_env("OTEL_EXPORTER_OTLP_TIMEOUT", "5000");
1187 }
1188 let t = timeout_from_env();
1189 assert_eq!(t, Some(Duration::from_millis(5000)));
1190 unsafe {
1191 remove_env("OTEL_EXPORTER_OTLP_TIMEOUT");
1192 }
1193 }
1194
1195 #[test]
1196 fn timeout_from_env_returns_none_when_unset() {
1197 unsafe {
1198 remove_env("OTEL_EXPORTER_OTLP_TIMEOUT");
1199 }
1200 assert_eq!(timeout_from_env(), None);
1201 }
1202
1203 #[test]
1204 fn service_name_from_env_used_when_none_given() {
1205 let builder = Telemetry::from_env();
1206 assert!(builder.service_name.is_none());
1207 }
1208
1209 #[test]
1210 fn explicit_service_name_overrides_env_var() {
1211 let builder = Telemetry::builder("explicit-svc");
1212 assert_eq!(builder.service_name.as_deref(), Some("explicit-svc"));
1213 }
1214
1215 #[test]
1216 fn from_env_builder_service_name_is_none() {
1217 let builder = Telemetry::from_env();
1218 assert!(builder.service_name.is_none());
1219 }
1220
1221 #[test]
1222 fn init_returns_error_for_unknown_otel_traces_sampler() {
1223 unsafe {
1224 set_env("OTEL_TRACES_SAMPLER", "not_a_real_sampler");
1225 }
1226 let result = Telemetry::builder("test-svc").with_metrics(false).init();
1227 let err = result
1228 .err()
1229 .expect("unknown sampler env var should cause init to fail");
1230 assert!(
1231 err.to_string().contains("not_a_real_sampler"),
1232 "error should name the unknown sampler, got: {err}"
1233 );
1234 unsafe {
1235 remove_env("OTEL_TRACES_SAMPLER");
1236 }
1237 }
1238
1239 #[test]
1240 fn with_max_export_batch_size_stores_value() {
1241 let builder = Telemetry::builder("test-svc").with_max_export_batch_size(1024);
1242 assert_eq!(builder.max_export_batch_size, Some(1024));
1243 }
1244
1245 #[test]
1246 fn with_metric_export_interval_stores_value() {
1247 let interval = Duration::from_secs(30);
1248 let builder = Telemetry::builder("test-svc").with_metric_export_interval(interval);
1249 assert_eq!(builder.metric_export_interval, Some(interval));
1250 }
1251
1252 #[test]
1253 fn init_rejects_zero_metric_export_interval() {
1254 let err = Telemetry::builder("test-svc")
1255 .with_metric_export_interval(Duration::ZERO)
1256 .with_metrics(false)
1257 .init()
1258 .err()
1259 .expect("expected error for zero interval");
1260 assert!(
1261 err.to_string().contains("metric_export_interval"),
1262 "error message should mention metric_export_interval, got: {err}"
1263 );
1264 }
1265
1266 #[test]
1267 fn builder_with_custom_values() {
1268 let builder = Telemetry::builder("test-svc")
1269 .with_version("2.0.0")
1270 .with_environment("production")
1271 .with_sampler(TraceSampler::TraceIdRatio(0.5))
1272 .with_metrics(false);
1273
1274 assert_eq!(builder.service_name.as_deref(), Some("test-svc"));
1275 assert_eq!(builder.service_version.as_deref(), Some("2.0.0"));
1276 assert_eq!(
1277 builder.deployment_environment.as_deref(),
1278 Some("production")
1279 );
1280 assert!(
1281 matches!(builder.sampler, Some(TraceSampler::TraceIdRatio(r)) if (r - 0.5).abs() < f64::EPSILON)
1282 );
1283 assert!(!builder.metrics);
1284 }
1285
1286 #[test]
1287 #[cfg(feature = "grpc")]
1288 fn builder_with_protocol_grpc() {
1289 let builder = Telemetry::builder("test-svc").with_protocol(ExportProtocol::Grpc);
1290 assert_eq!(builder.protocol, Some(ExportProtocol::Grpc));
1291 }
1292
1293 #[test]
1294 #[cfg(feature = "http")]
1295 fn builder_with_protocol_http() {
1296 let builder = Telemetry::builder("test-svc").with_protocol(ExportProtocol::HttpProtobuf);
1297 assert_eq!(builder.protocol, Some(ExportProtocol::HttpProtobuf));
1298 }
1299
1300 #[test]
1301 #[cfg(feature = "grpc")]
1302 fn protocol_from_env_reads_grpc() {
1303 unsafe {
1304 set_env("OTEL_EXPORTER_OTLP_PROTOCOL", "grpc");
1305 }
1306 assert_eq!(protocol_from_env(), Some(ExportProtocol::Grpc));
1307 unsafe {
1308 remove_env("OTEL_EXPORTER_OTLP_PROTOCOL");
1309 }
1310 }
1311
1312 #[test]
1313 #[cfg(feature = "http")]
1314 fn protocol_from_env_reads_http_protobuf() {
1315 unsafe {
1316 set_env("OTEL_EXPORTER_OTLP_PROTOCOL", "http/protobuf");
1317 }
1318 assert_eq!(protocol_from_env(), Some(ExportProtocol::HttpProtobuf));
1319 unsafe {
1320 remove_env("OTEL_EXPORTER_OTLP_PROTOCOL");
1321 }
1322 }
1323
1324 #[test]
1325 fn protocol_from_env_returns_none_when_unset() {
1326 unsafe {
1327 remove_env("OTEL_EXPORTER_OTLP_PROTOCOL");
1328 }
1329 assert_eq!(protocol_from_env(), None);
1330 }
1331
1332 #[test]
1333 fn protocol_from_env_returns_none_for_unknown() {
1334 unsafe {
1335 set_env("OTEL_EXPORTER_OTLP_PROTOCOL", "websocket");
1336 }
1337 assert_eq!(protocol_from_env(), None);
1338 unsafe {
1339 remove_env("OTEL_EXPORTER_OTLP_PROTOCOL");
1340 }
1341 }
1342
1343 #[test]
1344 fn builder_is_send_and_sync() {
1345 fn assert_send_sync<T: Send + Sync>() {}
1346 assert_send_sync::<TelemetryBuilder>();
1347 }
1348
1349 #[test]
1350 fn with_shutdown_timeout_stores_value() {
1351 let timeout = Duration::from_secs(10);
1352 let builder = Telemetry::builder("test-svc").with_shutdown_timeout(timeout);
1353 assert_eq!(builder.shutdown_timeout, timeout);
1354 }
1355
1356 #[test]
1357 fn default_shutdown_timeout_is_five_seconds() {
1358 let builder = Telemetry::builder("test-svc");
1359 assert_eq!(builder.shutdown_timeout, Duration::from_secs(5));
1360 }
1361
1362 #[cfg(feature = "testing")]
1370 #[test]
1371 fn drop_completes_within_shutdown_timeout() {
1372 let mut handles = crate::Telemetry::testing("drop-timeout-test");
1374 handles.shutdown_timeout = Duration::from_millis(100);
1376
1377 let start = std::time::Instant::now();
1378 drop(handles);
1379 let elapsed = start.elapsed();
1380
1381 assert!(
1383 elapsed < Duration::from_millis(500),
1384 "drop took {elapsed:?}, expected < 500 ms"
1385 );
1386 }
1387}