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 registry = tracing_subscriber::registry()
760 .with(self.extra_layers)
761 .with(tracing_subscriber::EnvFilter::from_default_env())
762 .with(tracing_subscriber::fmt::layer())
763 .with(otel_layer);
764
765 if let Some(lp) = &logger_provider {
766 registry
767 .with(opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge::new(lp))
768 .try_init()
769 .ok();
770 } else {
771 registry.try_init().ok();
772 }
773
774 Ok(TelemetryHandles {
775 tracer_provider,
776 meter_provider,
777 logger_provider,
778 shutdown_timeout: self.shutdown_timeout,
779 })
780 }
781}
782
783pub fn init_telemetry(service_name: &str) -> Result<TelemetryHandles, Box<dyn Error>> {
797 Telemetry::builder(service_name).init()
798}
799
800pub fn init_telemetry_with_sampler(
817 service_name: &str,
818 sampler: Option<TraceSampler>,
819) -> Result<TelemetryHandles, Box<dyn Error>> {
820 let builder = Telemetry::builder(service_name);
821 match sampler {
822 Some(s) => builder.with_sampler(s),
823 None => builder, }
825 .init()
826}
827
828fn timeout_from_env() -> Option<Duration> {
830 let ms = std::env::var("OTEL_EXPORTER_OTLP_TIMEOUT").ok()?;
831 let ms: u64 = ms.trim().parse().ok()?;
832 Some(Duration::from_millis(ms))
833}
834
835#[cfg(feature = "grpc-mtls")]
842fn build_tls_config(material: &MtlsMaterial) -> tonic::transport::ClientTlsConfig {
843 use tonic::transport::{Certificate, ClientTlsConfig, Identity};
844 ClientTlsConfig::new()
845 .ca_certificate(Certificate::from_pem(&material.trust_bundle_pem))
846 .identity(Identity::from_pem(
847 &material.client_cert_chain_pem,
848 &material.client_key_pem,
849 ))
850}
851
852fn build_span_exporter(
853 protocol: ExportProtocol,
854 endpoint: &str,
855 timeout: Option<Duration>,
856 #[cfg(feature = "grpc-mtls")] mtls: Option<&MtlsMaterial>,
857) -> Result<opentelemetry_otlp::SpanExporter, Box<dyn Error>> {
858 match protocol {
859 #[cfg(feature = "grpc")]
860 ExportProtocol::Grpc => {
861 let mut b = opentelemetry_otlp::SpanExporter::builder()
862 .with_tonic()
863 .with_endpoint(endpoint);
864 if let Some(t) = timeout {
865 b = b.with_timeout(t);
866 }
867 #[cfg(feature = "grpc-mtls")]
868 if let Some(m) = mtls {
869 use opentelemetry_otlp::WithTonicConfig as _;
870 b = b.with_tls_config(build_tls_config(m));
871 }
872 Ok(b.build()?)
873 }
874 #[cfg(feature = "http")]
875 ExportProtocol::HttpProtobuf => {
876 let mut b = opentelemetry_otlp::SpanExporter::builder()
877 .with_http()
878 .with_endpoint(endpoint);
879 if let Some(t) = timeout {
880 b = b.with_timeout(t);
881 }
882 Ok(b.build()?)
883 }
884 }
885}
886
887fn build_metric_exporter(
888 protocol: ExportProtocol,
889 endpoint: &str,
890 timeout: Option<Duration>,
891 #[cfg(feature = "grpc-mtls")] mtls: Option<&MtlsMaterial>,
892) -> Result<opentelemetry_otlp::MetricExporter, Box<dyn Error>> {
893 match protocol {
894 #[cfg(feature = "grpc")]
895 ExportProtocol::Grpc => {
896 let mut b = opentelemetry_otlp::MetricExporter::builder()
897 .with_tonic()
898 .with_endpoint(endpoint);
899 if let Some(t) = timeout {
900 b = b.with_timeout(t);
901 }
902 #[cfg(feature = "grpc-mtls")]
903 if let Some(m) = mtls {
904 use opentelemetry_otlp::WithTonicConfig as _;
905 b = b.with_tls_config(build_tls_config(m));
906 }
907 Ok(b.build()?)
908 }
909 #[cfg(feature = "http")]
910 ExportProtocol::HttpProtobuf => {
911 let mut b = opentelemetry_otlp::MetricExporter::builder()
912 .with_http()
913 .with_endpoint(endpoint);
914 if let Some(t) = timeout {
915 b = b.with_timeout(t);
916 }
917 Ok(b.build()?)
918 }
919 }
920}
921
922fn build_log_exporter(
923 protocol: ExportProtocol,
924 endpoint: &str,
925 timeout: Option<Duration>,
926 #[cfg(feature = "grpc-mtls")] mtls: Option<&MtlsMaterial>,
927) -> Result<opentelemetry_otlp::LogExporter, Box<dyn Error>> {
928 match protocol {
929 #[cfg(feature = "grpc")]
930 ExportProtocol::Grpc => {
931 let mut b = opentelemetry_otlp::LogExporter::builder()
932 .with_tonic()
933 .with_endpoint(endpoint);
934 if let Some(t) = timeout {
935 b = b.with_timeout(t);
936 }
937 #[cfg(feature = "grpc-mtls")]
938 if let Some(m) = mtls {
939 use opentelemetry_otlp::WithTonicConfig as _;
940 b = b.with_tls_config(build_tls_config(m));
941 }
942 Ok(b.build()?)
943 }
944 #[cfg(feature = "http")]
945 ExportProtocol::HttpProtobuf => {
946 let mut b = opentelemetry_otlp::LogExporter::builder()
947 .with_http()
948 .with_endpoint(endpoint);
949 if let Some(t) = timeout {
950 b = b.with_timeout(t);
951 }
952 Ok(b.build()?)
953 }
954 }
955}
956
957pub fn build_resource(
972 service_name: &str,
973 service_version: Option<&str>,
974 deployment_environment: Option<&str>,
975) -> Resource {
976 let hostname = hostname::get()
977 .ok()
978 .and_then(|h| h.into_string().ok())
979 .unwrap_or_default();
980
981 let mut builder = Resource::builder()
982 .with_service_name(service_name.to_string())
983 .with_attributes([
984 KeyValue::new(HOST_NAME, hostname),
985 KeyValue::new(PROCESS_PID, std::process::id() as i64),
986 ]);
987
988 if let Some(version) = service_version {
989 builder = builder.with_attribute(KeyValue::new(SERVICE_VERSION, version.to_string()));
990 }
991
992 if let Some(env) = deployment_environment {
993 builder =
994 builder.with_attribute(KeyValue::new(DEPLOYMENT_ENVIRONMENT_NAME, env.to_string()));
995 }
996
997 builder.build()
998}
999
1000#[cfg(feature = "axum")]
1018pub fn axum_layer() -> axum_middleware::OtelTraceLayer {
1019 axum_middleware::OtelTraceLayer
1020}
1021
1022#[cfg(feature = "axum")]
1053pub fn span_enricher_layer<T>() -> axum_middleware::SpanEnricherLayer<T>
1054where
1055 T: span_enrichment::EnrichSpan + Clone + Send + Sync + 'static,
1056{
1057 axum_middleware::SpanEnricherLayer::default()
1058}
1059
1060#[cfg(test)]
1061mod tests {
1062 use super::*;
1063 use std::sync::Mutex;
1064
1065 static ENV_LOCK: Mutex<()> = Mutex::new(());
1066
1067 #[test]
1068 fn resource_contains_all_attributes_when_provided() {
1069 let resource = build_resource("test-svc", Some("1.2.3"), Some("staging"));
1070
1071 assert_eq!(
1072 resource.get(&opentelemetry::Key::new("service.name")),
1073 Some(opentelemetry::Value::from("test-svc")),
1074 );
1075 assert_eq!(
1076 resource.get(&opentelemetry::Key::new(SERVICE_VERSION)),
1077 Some(opentelemetry::Value::from("1.2.3")),
1078 );
1079 assert_eq!(
1080 resource.get(&opentelemetry::Key::new(DEPLOYMENT_ENVIRONMENT_NAME)),
1081 Some(opentelemetry::Value::from("staging")),
1082 );
1083 assert!(resource.get(&opentelemetry::Key::new(HOST_NAME)).is_some());
1084 assert!(
1085 resource
1086 .get(&opentelemetry::Key::new(PROCESS_PID))
1087 .is_some()
1088 );
1089 }
1090
1091 #[test]
1092 fn resource_graceful_when_optional_values_omitted() {
1093 let resource = build_resource("test-svc", None, None);
1094
1095 assert_eq!(
1096 resource.get(&opentelemetry::Key::new("service.name")),
1097 Some(opentelemetry::Value::from("test-svc")),
1098 );
1099 assert!(
1100 resource
1101 .get(&opentelemetry::Key::new(SERVICE_VERSION))
1102 .is_none()
1103 );
1104 assert!(
1105 resource
1106 .get(&opentelemetry::Key::new(DEPLOYMENT_ENVIRONMENT_NAME))
1107 .is_none()
1108 );
1109 assert!(resource.get(&opentelemetry::Key::new(HOST_NAME)).is_some());
1111 assert!(
1112 resource
1113 .get(&opentelemetry::Key::new(PROCESS_PID))
1114 .is_some()
1115 );
1116 }
1117
1118 #[test]
1119 fn trace_sampler_ratio_converts_to_sdk() {
1120 let sampler = TraceSampler::TraceIdRatio(0.5);
1121 let sdk = sampler.into_sdk_sampler();
1122 assert_eq!(format!("{sdk:?}"), "TraceIdRatioBased(0.5)");
1123 }
1124
1125 #[test]
1126 fn trace_sampler_parent_based_converts_to_sdk() {
1127 let sampler = TraceSampler::ParentBased(Box::new(TraceSampler::TraceIdRatio(0.25)));
1128 let sdk = sampler.into_sdk_sampler();
1129 let debug = format!("{sdk:?}");
1130 assert!(debug.contains("ParentBased"));
1131 assert!(debug.contains("0.25"));
1132 }
1133
1134 unsafe fn set_env(key: &str, val: &str) {
1136 unsafe {
1137 std::env::set_var(key, val);
1138 }
1139 }
1140
1141 unsafe fn remove_env(key: &str) {
1142 unsafe {
1143 std::env::remove_var(key);
1144 }
1145 }
1146
1147 #[test]
1148 fn sampler_from_env_reads_traceidratio() {
1149 let _lock = ENV_LOCK.lock().unwrap();
1150 unsafe {
1151 set_env("OTEL_TRACES_SAMPLER", "traceidratio");
1152 set_env("OTEL_TRACES_SAMPLER_ARG", "0.42");
1153 }
1154
1155 let sampler = sampler_from_env()
1156 .expect("should not error")
1157 .expect("should return Some");
1158 assert!(
1159 matches!(sampler, TraceSampler::TraceIdRatio(r) if (r - 0.42).abs() < f64::EPSILON)
1160 );
1161
1162 unsafe {
1163 remove_env("OTEL_TRACES_SAMPLER");
1164 remove_env("OTEL_TRACES_SAMPLER_ARG");
1165 }
1166 }
1167
1168 #[test]
1169 fn sampler_from_env_returns_none_when_unset() {
1170 let _lock = ENV_LOCK.lock().unwrap();
1171 unsafe {
1172 remove_env("OTEL_TRACES_SAMPLER");
1173 }
1174 assert!(sampler_from_env().expect("should not error").is_none());
1175 }
1176
1177 #[test]
1178 fn sampler_from_env_reads_parentbased_traceidratio() {
1179 let _lock = ENV_LOCK.lock().unwrap();
1180 unsafe {
1181 set_env("OTEL_TRACES_SAMPLER", "parentbased_traceidratio");
1182 set_env("OTEL_TRACES_SAMPLER_ARG", "0.1");
1183 }
1184
1185 let sampler = sampler_from_env()
1186 .expect("should not error")
1187 .expect("should return Some");
1188 assert!(
1189 matches!(sampler, TraceSampler::ParentBased(inner) if matches!(*inner, TraceSampler::TraceIdRatio(r) if (r - 0.1).abs() < f64::EPSILON))
1190 );
1191
1192 unsafe {
1193 remove_env("OTEL_TRACES_SAMPLER");
1194 remove_env("OTEL_TRACES_SAMPLER_ARG");
1195 }
1196 }
1197
1198 #[test]
1199 fn sampler_from_env_parentbased_always_on() {
1200 let _lock = ENV_LOCK.lock().unwrap();
1201 unsafe {
1202 set_env("OTEL_TRACES_SAMPLER", "parentbased_always_on");
1203 }
1204 let sampler = sampler_from_env()
1205 .expect("should not error")
1206 .expect("should return Some");
1207 assert!(
1208 matches!(sampler, TraceSampler::ParentBased(inner) if matches!(*inner, TraceSampler::AlwaysOn))
1209 );
1210 unsafe {
1211 remove_env("OTEL_TRACES_SAMPLER");
1212 }
1213 }
1214
1215 #[test]
1216 fn sampler_from_env_parentbased_always_off() {
1217 let _lock = ENV_LOCK.lock().unwrap();
1218 unsafe {
1219 set_env("OTEL_TRACES_SAMPLER", "parentbased_always_off");
1220 }
1221 let sampler = sampler_from_env()
1222 .expect("should not error")
1223 .expect("should return Some");
1224 assert!(
1225 matches!(sampler, TraceSampler::ParentBased(inner) if matches!(*inner, TraceSampler::AlwaysOff))
1226 );
1227 unsafe {
1228 remove_env("OTEL_TRACES_SAMPLER");
1229 }
1230 }
1231
1232 #[test]
1233 fn sampler_from_env_always_on() {
1234 let _lock = ENV_LOCK.lock().unwrap();
1235 unsafe {
1236 set_env("OTEL_TRACES_SAMPLER", "always_on");
1237 }
1238 let sampler = sampler_from_env()
1239 .expect("should not error")
1240 .expect("should return Some");
1241 assert!(matches!(sampler, TraceSampler::AlwaysOn));
1242 unsafe {
1243 remove_env("OTEL_TRACES_SAMPLER");
1244 }
1245 }
1246
1247 #[test]
1248 fn sampler_from_env_always_off() {
1249 let _lock = ENV_LOCK.lock().unwrap();
1250 unsafe {
1251 set_env("OTEL_TRACES_SAMPLER", "always_off");
1252 }
1253 let sampler = sampler_from_env()
1254 .expect("should not error")
1255 .expect("should return Some");
1256 assert!(matches!(sampler, TraceSampler::AlwaysOff));
1257 unsafe {
1258 remove_env("OTEL_TRACES_SAMPLER");
1259 }
1260 }
1261
1262 #[test]
1263 fn sampler_from_env_unknown_returns_error() {
1264 let _lock = ENV_LOCK.lock().unwrap();
1265 unsafe {
1266 set_env("OTEL_TRACES_SAMPLER", "unknown_sampler");
1267 }
1268 let err = sampler_from_env().expect_err("unknown sampler should produce an error");
1269 assert!(
1270 err.to_string().contains("unknown_sampler"),
1271 "error message should include the unknown name, got: {err}"
1272 );
1273 unsafe {
1274 remove_env("OTEL_TRACES_SAMPLER");
1275 }
1276 }
1277
1278 #[test]
1279 fn trace_sampler_always_on_converts_to_sdk() {
1280 let sdk = TraceSampler::AlwaysOn.into_sdk_sampler();
1281 assert_eq!(format!("{sdk:?}"), "AlwaysOn");
1282 }
1283
1284 #[test]
1285 fn trace_sampler_always_off_converts_to_sdk() {
1286 let sdk = TraceSampler::AlwaysOff.into_sdk_sampler();
1287 assert_eq!(format!("{sdk:?}"), "AlwaysOff");
1288 }
1289
1290 #[test]
1291 fn builder_has_sensible_defaults() {
1292 let builder = Telemetry::builder("test-svc");
1293 assert_eq!(builder.service_name.as_deref(), Some("test-svc"));
1294 assert!(builder.service_version.is_none());
1295 assert!(builder.deployment_environment.is_none());
1296 assert!(builder.sampler.is_none());
1297 assert!(builder.metrics);
1298 assert!(!builder.logs);
1299 assert!(builder.protocol.is_none());
1300 assert!(builder.max_export_batch_size.is_none());
1301 assert!(builder.metric_export_interval.is_none());
1302 assert!(builder.export_timeout.is_none());
1303 }
1304
1305 #[test]
1306 fn from_env_builder_has_no_service_name() {
1307 let builder = Telemetry::from_env();
1308 assert!(builder.service_name.is_none());
1309 }
1310
1311 #[test]
1312 fn with_export_timeout_stores_value() {
1313 let timeout = Duration::from_secs(5);
1314 let builder = Telemetry::builder("test-svc").with_export_timeout(timeout);
1315 assert_eq!(builder.export_timeout, Some(timeout));
1316 }
1317
1318 #[test]
1319 fn timeout_from_env_reads_milliseconds() {
1320 let _lock = ENV_LOCK.lock().unwrap();
1321 unsafe {
1322 set_env("OTEL_EXPORTER_OTLP_TIMEOUT", "5000");
1323 }
1324 let t = timeout_from_env();
1325 assert_eq!(t, Some(Duration::from_millis(5000)));
1326 unsafe {
1327 remove_env("OTEL_EXPORTER_OTLP_TIMEOUT");
1328 }
1329 }
1330
1331 #[test]
1332 fn timeout_from_env_returns_none_when_unset() {
1333 let _lock = ENV_LOCK.lock().unwrap();
1334 unsafe {
1335 remove_env("OTEL_EXPORTER_OTLP_TIMEOUT");
1336 }
1337 assert_eq!(timeout_from_env(), None);
1338 }
1339
1340 #[test]
1341 fn service_name_from_env_used_when_none_given() {
1342 let builder = Telemetry::from_env();
1343 assert!(builder.service_name.is_none());
1344 }
1345
1346 #[test]
1347 fn explicit_service_name_overrides_env_var() {
1348 let builder = Telemetry::builder("explicit-svc");
1349 assert_eq!(builder.service_name.as_deref(), Some("explicit-svc"));
1350 }
1351
1352 #[test]
1353 fn from_env_builder_service_name_is_none() {
1354 let builder = Telemetry::from_env();
1355 assert!(builder.service_name.is_none());
1356 }
1357
1358 #[test]
1359 fn init_returns_error_for_unknown_otel_traces_sampler() {
1360 let _lock = ENV_LOCK.lock().unwrap();
1361 unsafe {
1362 set_env("OTEL_TRACES_SAMPLER", "not_a_real_sampler");
1363 }
1364 let result = Telemetry::builder("test-svc").with_metrics(false).init();
1365 let err = result
1366 .err()
1367 .expect("unknown sampler env var should cause init to fail");
1368 assert!(
1369 err.to_string().contains("not_a_real_sampler"),
1370 "error should name the unknown sampler, got: {err}"
1371 );
1372 unsafe {
1373 remove_env("OTEL_TRACES_SAMPLER");
1374 }
1375 }
1376
1377 #[test]
1378 fn with_max_export_batch_size_stores_value() {
1379 let builder = Telemetry::builder("test-svc").with_max_export_batch_size(1024);
1380 assert_eq!(builder.max_export_batch_size, Some(1024));
1381 }
1382
1383 #[test]
1384 fn with_metric_export_interval_stores_value() {
1385 let interval = Duration::from_secs(30);
1386 let builder = Telemetry::builder("test-svc").with_metric_export_interval(interval);
1387 assert_eq!(builder.metric_export_interval, Some(interval));
1388 }
1389
1390 #[test]
1391 fn init_rejects_zero_metric_export_interval() {
1392 let err = Telemetry::builder("test-svc")
1393 .with_metric_export_interval(Duration::ZERO)
1394 .with_metrics(false)
1395 .init()
1396 .err()
1397 .expect("expected error for zero interval");
1398 assert!(
1399 err.to_string().contains("metric_export_interval"),
1400 "error message should mention metric_export_interval, got: {err}"
1401 );
1402 }
1403
1404 #[test]
1405 fn builder_with_custom_values() {
1406 let builder = Telemetry::builder("test-svc")
1407 .with_version("2.0.0")
1408 .with_environment("production")
1409 .with_sampler(TraceSampler::TraceIdRatio(0.5))
1410 .with_metrics(false);
1411
1412 assert_eq!(builder.service_name.as_deref(), Some("test-svc"));
1413 assert_eq!(builder.service_version.as_deref(), Some("2.0.0"));
1414 assert_eq!(
1415 builder.deployment_environment.as_deref(),
1416 Some("production")
1417 );
1418 assert!(
1419 matches!(builder.sampler, Some(TraceSampler::TraceIdRatio(r)) if (r - 0.5).abs() < f64::EPSILON)
1420 );
1421 assert!(!builder.metrics);
1422 }
1423
1424 #[test]
1425 #[cfg(feature = "grpc")]
1426 fn builder_with_protocol_grpc() {
1427 let builder = Telemetry::builder("test-svc").with_protocol(ExportProtocol::Grpc);
1428 assert_eq!(builder.protocol, Some(ExportProtocol::Grpc));
1429 }
1430
1431 #[test]
1432 #[cfg(feature = "http")]
1433 fn builder_with_protocol_http() {
1434 let builder = Telemetry::builder("test-svc").with_protocol(ExportProtocol::HttpProtobuf);
1435 assert_eq!(builder.protocol, Some(ExportProtocol::HttpProtobuf));
1436 }
1437
1438 #[test]
1439 #[cfg(feature = "grpc")]
1440 fn protocol_from_env_reads_grpc() {
1441 let _lock = ENV_LOCK.lock().unwrap();
1442 unsafe {
1443 set_env("OTEL_EXPORTER_OTLP_PROTOCOL", "grpc");
1444 }
1445 assert_eq!(protocol_from_env(), Some(ExportProtocol::Grpc));
1446 unsafe {
1447 remove_env("OTEL_EXPORTER_OTLP_PROTOCOL");
1448 }
1449 }
1450
1451 #[test]
1452 #[cfg(feature = "http")]
1453 fn protocol_from_env_reads_http_protobuf() {
1454 let _lock = ENV_LOCK.lock().unwrap();
1455 unsafe {
1456 set_env("OTEL_EXPORTER_OTLP_PROTOCOL", "http/protobuf");
1457 }
1458 assert_eq!(protocol_from_env(), Some(ExportProtocol::HttpProtobuf));
1459 unsafe {
1460 remove_env("OTEL_EXPORTER_OTLP_PROTOCOL");
1461 }
1462 }
1463
1464 #[test]
1465 fn protocol_from_env_returns_none_when_unset() {
1466 let _lock = ENV_LOCK.lock().unwrap();
1467 unsafe {
1468 remove_env("OTEL_EXPORTER_OTLP_PROTOCOL");
1469 }
1470 assert_eq!(protocol_from_env(), None);
1471 }
1472
1473 #[test]
1474 fn protocol_from_env_returns_none_for_unknown() {
1475 let _lock = ENV_LOCK.lock().unwrap();
1476 unsafe {
1477 set_env("OTEL_EXPORTER_OTLP_PROTOCOL", "websocket");
1478 }
1479 assert_eq!(protocol_from_env(), None);
1480 unsafe {
1481 remove_env("OTEL_EXPORTER_OTLP_PROTOCOL");
1482 }
1483 }
1484
1485 #[test]
1486 fn builder_is_send_and_sync() {
1487 fn assert_send_sync<T: Send + Sync>() {}
1488 assert_send_sync::<TelemetryBuilder>();
1489 }
1490
1491 #[test]
1492 fn with_shutdown_timeout_stores_value() {
1493 let timeout = Duration::from_secs(10);
1494 let builder = Telemetry::builder("test-svc").with_shutdown_timeout(timeout);
1495 assert_eq!(builder.shutdown_timeout, timeout);
1496 }
1497
1498 #[test]
1499 fn default_shutdown_timeout_is_five_seconds() {
1500 let builder = Telemetry::builder("test-svc");
1501 assert_eq!(builder.shutdown_timeout, Duration::from_secs(5));
1502 }
1503
1504 #[cfg(feature = "testing")]
1512 #[test]
1513 fn drop_completes_within_shutdown_timeout() {
1514 let mut handles = crate::Telemetry::testing("drop-timeout-test");
1516 handles.shutdown_timeout = Duration::from_millis(100);
1518
1519 let start = std::time::Instant::now();
1520 drop(handles);
1521 let elapsed = start.elapsed();
1522
1523 assert!(
1525 elapsed < Duration::from_millis(500),
1526 "drop took {elapsed:?}, expected < 500 ms"
1527 );
1528 }
1529}