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