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 log_bridge;
36pub mod span_enrichment;
37
38pub use log_bridge::{
39 PROPAGATED_SPAN_FIELDS, SpanLogAttrs, record_span_log_attr, record_span_log_attr_on,
40};
41
42use opentelemetry::KeyValue;
43use opentelemetry::propagation::TextMapCompositePropagator;
44use opentelemetry_otlp::WithExportConfig;
45use opentelemetry_sdk::{
46 Resource,
47 logs::SdkLoggerProvider,
48 metrics::{MeterProviderBuilder, PeriodicReader, SdkMeterProvider},
49 propagation::{BaggagePropagator, TraceContextPropagator},
50 trace::{BatchConfigBuilder, BatchSpanProcessor, Sampler, SdkTracerProvider},
51};
52use opentelemetry_semantic_conventions::attribute::{
53 DEPLOYMENT_ENVIRONMENT_NAME, HOST_NAME, PROCESS_PID, SERVICE_VERSION,
54};
55use std::error::Error;
56use std::time::Duration;
57use tracing_subscriber::layer::SubscriberExt;
58use tracing_subscriber::util::SubscriberInitExt;
59
60#[derive(Debug, Clone)]
75pub enum TraceSampler {
76 AlwaysOn,
78 AlwaysOff,
80 TraceIdRatio(f64),
82 ParentBased(Box<TraceSampler>),
85}
86
87impl TraceSampler {
88 fn into_sdk_sampler(self) -> Sampler {
90 match self {
91 TraceSampler::AlwaysOn => Sampler::AlwaysOn,
92 TraceSampler::AlwaysOff => Sampler::AlwaysOff,
93 TraceSampler::TraceIdRatio(r) => Sampler::TraceIdRatioBased(r),
94 TraceSampler::ParentBased(inner) => {
95 Sampler::ParentBased(Box::new(inner.into_sdk_sampler()))
96 }
97 }
98 }
99}
100
101fn sampler_from_env() -> Result<Option<TraceSampler>, Box<dyn Error>> {
109 let name = match std::env::var("OTEL_TRACES_SAMPLER") {
110 Ok(v) => v,
111 Err(_) => return Ok(None),
112 };
113 let arg = std::env::var("OTEL_TRACES_SAMPLER_ARG").ok();
114 let sampler = match name.as_str() {
115 "always_on" => TraceSampler::AlwaysOn,
116 "always_off" => TraceSampler::AlwaysOff,
117 "traceidratio" => {
118 let ratio = arg
119 .as_deref()
120 .unwrap_or("1.0")
121 .parse::<f64>()
122 .unwrap_or(1.0);
123 TraceSampler::TraceIdRatio(ratio)
124 }
125 "parentbased_always_on" => TraceSampler::ParentBased(Box::new(TraceSampler::AlwaysOn)),
126 "parentbased_always_off" => TraceSampler::ParentBased(Box::new(TraceSampler::AlwaysOff)),
127 "parentbased_traceidratio" => {
128 let ratio = arg
129 .as_deref()
130 .unwrap_or("1.0")
131 .parse::<f64>()
132 .unwrap_or(1.0);
133 TraceSampler::ParentBased(Box::new(TraceSampler::TraceIdRatio(ratio)))
134 }
135 unknown => {
136 return Err(format!(
137 "OTEL_TRACES_SAMPLER: unrecognised sampler name '{unknown}'. \
138 Valid values: always_on, always_off, traceidratio, \
139 parentbased_always_on, parentbased_always_off, parentbased_traceidratio"
140 )
141 .into());
142 }
143 };
144 Ok(Some(sampler))
145}
146
147const DEFAULT_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5);
149
150pub struct TelemetryHandles {
171 pub tracer_provider: SdkTracerProvider,
172 pub meter_provider: Option<SdkMeterProvider>,
173 pub logger_provider: Option<SdkLoggerProvider>,
174 shutdown_timeout: Duration,
175}
176
177impl TelemetryHandles {
178 pub fn shutdown(&self) -> Result<(), Box<dyn Error>> {
191 self.tracer_provider.shutdown()?;
192 if let Some(mp) = &self.meter_provider {
193 mp.shutdown()?;
194 }
195 if let Some(lp) = &self.logger_provider {
196 lp.shutdown()?;
197 }
198 Ok(())
199 }
200}
201
202impl Drop for TelemetryHandles {
203 fn drop(&mut self) {
204 let tracer_provider = self.tracer_provider.clone();
205 let meter_provider = self.meter_provider.clone();
206 let logger_provider = self.logger_provider.clone();
207 let timeout = self.shutdown_timeout;
208
209 let (tx, rx) = std::sync::mpsc::channel();
210 std::thread::spawn(move || {
211 if let Err(e) = tracer_provider.shutdown() {
212 tracing::warn!("tracer provider shutdown error: {e}");
213 }
214 if let Some(mp) = meter_provider
215 && let Err(e) = mp.shutdown()
216 {
217 tracing::warn!("meter provider shutdown error: {e}");
218 }
219 if let Some(lp) = logger_provider
220 && let Err(e) = lp.shutdown()
221 {
222 tracing::warn!("logger provider shutdown error: {e}");
223 }
224 let _ = tx.send(());
225 });
226
227 if rx.recv_timeout(timeout).is_err() {
228 tracing::warn!(
229 "telemetry shutdown did not complete within {timeout:?}; \
230 some spans/metrics may not have been exported"
231 );
232 }
233 }
234}
235
236#[derive(Debug, Clone, Copy, PartialEq, Eq)]
259pub enum ExportProtocol {
260 #[cfg(feature = "grpc")]
262 Grpc,
263 #[cfg(feature = "http")]
265 HttpProtobuf,
266}
267
268#[cfg(feature = "grpc-mtls")]
278#[derive(Clone)]
279pub struct MtlsMaterial {
280 pub client_cert_chain_pem: Vec<u8>,
282 pub client_key_pem: Vec<u8>,
284 pub trust_bundle_pem: Vec<u8>,
286}
287
288#[cfg(feature = "grpc-mtls")]
289impl std::fmt::Debug for MtlsMaterial {
290 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
291 f.debug_struct("MtlsMaterial")
292 .field("client_cert_chain_pem", &"<redacted>")
293 .field("client_key_pem", &"<redacted>")
294 .field("trust_bundle_pem", &"<redacted>")
295 .finish()
296 }
297}
298
299fn protocol_from_env() -> Option<ExportProtocol> {
301 let val = std::env::var("OTEL_EXPORTER_OTLP_PROTOCOL").ok()?;
302 match val.trim() {
303 #[cfg(feature = "grpc")]
304 "grpc" => Some(ExportProtocol::Grpc),
305 #[cfg(feature = "http")]
306 "http/protobuf" => Some(ExportProtocol::HttpProtobuf),
307 _ => None,
308 }
309}
310
311pub struct Telemetry;
327
328impl Telemetry {
329 pub fn builder(service_name: &str) -> TelemetryBuilder {
333 TelemetryBuilder {
334 service_name: Some(service_name.to_string()),
335 service_version: None,
336 deployment_environment: None,
337 sampler: None,
338 metrics: true,
339 logs: false,
340 protocol: None,
341 max_export_batch_size: None,
342 metric_export_interval: None,
343 export_timeout: None,
344 shutdown_timeout: DEFAULT_SHUTDOWN_TIMEOUT,
345 extra_layers: Vec::new(),
346 extra_metric_readers: Vec::new(),
347 #[cfg(feature = "grpc-mtls")]
348 mtls: None,
349 propagated_span_fields: crate::log_bridge::PROPAGATED_SPAN_FIELDS,
350 }
351 }
352
353 pub fn from_env() -> TelemetryBuilder {
363 TelemetryBuilder {
364 service_name: None,
365 service_version: None,
366 deployment_environment: None,
367 sampler: None,
368 metrics: true,
369 logs: false,
370 protocol: None,
371 max_export_batch_size: None,
372 metric_export_interval: None,
373 export_timeout: None,
374 shutdown_timeout: DEFAULT_SHUTDOWN_TIMEOUT,
375 extra_layers: Vec::new(),
376 extra_metric_readers: Vec::new(),
377 #[cfg(feature = "grpc-mtls")]
378 mtls: None,
379 propagated_span_fields: crate::log_bridge::PROPAGATED_SPAN_FIELDS,
380 }
381 }
382}
383
384#[must_use = "a TelemetryBuilder does nothing until .init() is called"]
402pub struct TelemetryBuilder {
403 service_name: Option<String>,
404 service_version: Option<String>,
405 deployment_environment: Option<String>,
406 sampler: Option<TraceSampler>,
407 metrics: bool,
408 logs: bool,
409 protocol: Option<ExportProtocol>,
410 max_export_batch_size: Option<usize>,
411 metric_export_interval: Option<Duration>,
412 export_timeout: Option<Duration>,
413 shutdown_timeout: Duration,
414 extra_layers: Vec<
415 Box<dyn tracing_subscriber::Layer<tracing_subscriber::Registry> + Send + Sync + 'static>,
416 >,
417 extra_metric_readers: Vec<MeterProviderInstaller>,
418 #[cfg(feature = "grpc-mtls")]
419 mtls: Option<MtlsMaterial>,
420 propagated_span_fields: &'static [&'static str],
421}
422
423type MeterProviderInstaller =
428 Box<dyn FnOnce(MeterProviderBuilder) -> MeterProviderBuilder + Send + Sync>;
429
430impl TelemetryBuilder {
431 pub fn with_version(mut self, version: &str) -> Self {
433 self.service_version = Some(version.to_string());
434 self
435 }
436
437 pub fn with_environment(mut self, environment: &str) -> Self {
439 self.deployment_environment = Some(environment.to_string());
440 self
441 }
442
443 #[cfg(feature = "grpc-mtls")]
468 pub fn with_mtls(mut self, material: MtlsMaterial) -> Self {
469 self.mtls = Some(material);
470 self.protocol = Some(ExportProtocol::Grpc);
471 self
472 }
473
474 pub fn with_sampler(mut self, sampler: TraceSampler) -> Self {
477 self.sampler = Some(sampler);
478 self
479 }
480
481 pub fn with_metrics(mut self, enabled: bool) -> Self {
483 self.metrics = enabled;
484 self
485 }
486
487 pub fn with_protocol(mut self, protocol: ExportProtocol) -> Self {
491 self.protocol = Some(protocol);
492 self
493 }
494
495 pub fn with_max_export_batch_size(mut self, size: usize) -> Self {
500 self.max_export_batch_size = Some(size);
501 self
502 }
503
504 pub fn with_metric_export_interval(mut self, interval: Duration) -> Self {
509 self.metric_export_interval = Some(interval);
510 self
511 }
512
513 pub fn with_logs(mut self, enabled: bool) -> Self {
520 self.logs = enabled;
521 self
522 }
523
524 pub fn with_propagated_span_fields(mut self, fields: &'static [&'static str]) -> Self {
537 self.propagated_span_fields = fields;
538 self
539 }
540
541 pub fn with_export_timeout(mut self, timeout: Duration) -> Self {
545 self.export_timeout = Some(timeout);
546 self
547 }
548
549 pub fn with_shutdown_timeout(mut self, timeout: Duration) -> Self {
556 self.shutdown_timeout = timeout;
557 self
558 }
559
560 pub fn with_meter_provider_setup<F>(mut self, setup: F) -> Self
617 where
618 F: FnOnce(MeterProviderBuilder) -> MeterProviderBuilder + Send + Sync + 'static,
619 {
620 self.extra_metric_readers.push(Box::new(setup));
621 self
622 }
623
624 pub fn with_layer<L>(mut self, layer: L) -> Self
625 where
626 L: tracing_subscriber::Layer<tracing_subscriber::Registry> + Send + Sync + 'static,
627 {
628 self.extra_layers.push(Box::new(layer));
629 self
630 }
631
632 pub fn init(self) -> Result<TelemetryHandles, Box<dyn Error>> {
647 if let Some(interval) = self.metric_export_interval
648 && interval.is_zero()
649 {
650 return Err("metric_export_interval must be greater than zero".into());
651 }
652
653 let protocol = self.protocol.or_else(protocol_from_env).unwrap_or({
654 #[cfg(feature = "grpc")]
655 {
656 ExportProtocol::Grpc
657 }
658 #[cfg(all(not(feature = "grpc"), feature = "http"))]
659 {
660 ExportProtocol::HttpProtobuf
661 }
662 });
663
664 let default_endpoint = match protocol {
665 #[cfg(feature = "grpc")]
666 ExportProtocol::Grpc => "http://localhost:4317",
667 #[cfg(feature = "http")]
668 ExportProtocol::HttpProtobuf => "http://localhost:4318",
669 };
670 let endpoint = std::env::var("OTEL_EXPORTER_OTLP_ENDPOINT")
671 .unwrap_or_else(|_| default_endpoint.to_string());
672
673 let export_timeout = self.export_timeout.or_else(timeout_from_env);
675
676 let service_name = self.service_name.unwrap_or_else(|| {
678 std::env::var("OTEL_SERVICE_NAME").unwrap_or_else(|_| "unknown_service".to_string())
679 });
680
681 let resource = build_resource(
682 &service_name,
683 self.service_version.as_deref(),
684 self.deployment_environment.as_deref(),
685 );
686
687 let sampler = match self.sampler {
688 Some(s) => s,
689 None => sampler_from_env()?.unwrap_or(TraceSampler::AlwaysOn),
690 };
691
692 let trace_exporter = build_span_exporter(
694 protocol,
695 &endpoint,
696 export_timeout,
697 #[cfg(feature = "grpc-mtls")]
698 self.mtls.as_ref(),
699 )?;
700
701 let batch_processor = if let Some(size) = self.max_export_batch_size {
702 BatchSpanProcessor::builder(trace_exporter)
703 .with_batch_config(
704 BatchConfigBuilder::default()
705 .with_max_export_batch_size(size)
706 .build(),
707 )
708 .build()
709 } else {
710 BatchSpanProcessor::builder(trace_exporter).build()
711 };
712
713 let tracer_provider = SdkTracerProvider::builder()
714 .with_resource(resource.clone())
715 .with_sampler(sampler.into_sdk_sampler())
716 .with_span_processor(batch_processor)
717 .build();
718
719 opentelemetry::global::set_tracer_provider(tracer_provider.clone());
720
721 let propagator = TextMapCompositePropagator::new(vec![
723 Box::new(TraceContextPropagator::new()),
724 Box::new(BaggagePropagator::new()),
725 ]);
726 opentelemetry::global::set_text_map_propagator(propagator);
727
728 let meter_provider = if self.metrics {
730 let metric_exporter = build_metric_exporter(
731 protocol,
732 &endpoint,
733 export_timeout,
734 #[cfg(feature = "grpc-mtls")]
735 self.mtls.as_ref(),
736 )?;
737
738 let periodic_reader = if let Some(interval) = self.metric_export_interval {
739 PeriodicReader::builder(metric_exporter)
740 .with_interval(interval)
741 .build()
742 } else {
743 PeriodicReader::builder(metric_exporter).build()
744 };
745
746 let mut mp_builder = SdkMeterProvider::builder()
747 .with_resource(resource.clone())
748 .with_reader(periodic_reader);
749 for installer in self.extra_metric_readers {
750 mp_builder = installer(mp_builder);
751 }
752 let mp = mp_builder.build();
753
754 opentelemetry::global::set_meter_provider(mp.clone());
755
756 Some(mp)
757 } else {
758 None
759 };
760
761 let logger_provider = if self.logs {
763 let log_exporter = build_log_exporter(
764 protocol,
765 &endpoint,
766 export_timeout,
767 #[cfg(feature = "grpc-mtls")]
768 self.mtls.as_ref(),
769 )?;
770
771 let lp = SdkLoggerProvider::builder()
772 .with_resource(resource)
773 .with_batch_exporter(log_exporter)
774 .build();
775
776 Some(lp)
777 } else {
778 None
779 };
780
781 let otel_layer = tracing_opentelemetry::layer();
783
784 let extra = if self.extra_layers.is_empty() {
789 None
790 } else {
791 Some(self.extra_layers)
792 };
793
794 let registry = tracing_subscriber::registry()
795 .with(extra)
796 .with(tracing_subscriber::EnvFilter::from_default_env())
797 .with(tracing_subscriber::fmt::layer())
798 .with(otel_layer);
799
800 if let Some(lp) = &logger_provider {
801 if let Err(e) = registry
802 .with(crate::log_bridge::SpanAwareLogBridge::new(
803 lp,
804 self.propagated_span_fields,
805 ))
806 .try_init()
807 {
808 eprintln!(
809 "otel-bootstrap: global tracing subscriber already installed — \
810 OTLP log records will NOT be exported to the collector: {e}"
811 );
812 }
813 } else if let Err(e) = registry.try_init() {
814 eprintln!(
815 "otel-bootstrap: global tracing subscriber already installed — \
816 OTLP telemetry will NOT be exported to the collector: {e}"
817 );
818 }
819
820 Ok(TelemetryHandles {
821 tracer_provider,
822 meter_provider,
823 logger_provider,
824 shutdown_timeout: self.shutdown_timeout,
825 })
826 }
827}
828
829pub fn init_telemetry(service_name: &str) -> Result<TelemetryHandles, Box<dyn Error>> {
843 Telemetry::builder(service_name).init()
844}
845
846pub fn init_telemetry_with_sampler(
863 service_name: &str,
864 sampler: Option<TraceSampler>,
865) -> Result<TelemetryHandles, Box<dyn Error>> {
866 let builder = Telemetry::builder(service_name);
867 match sampler {
868 Some(s) => builder.with_sampler(s),
869 None => builder, }
871 .init()
872}
873
874fn timeout_from_env() -> Option<Duration> {
876 let ms = std::env::var("OTEL_EXPORTER_OTLP_TIMEOUT").ok()?;
877 let ms: u64 = ms.trim().parse().ok()?;
878 Some(Duration::from_millis(ms))
879}
880
881#[cfg(feature = "grpc-mtls")]
888fn build_tls_config(material: &MtlsMaterial) -> tonic::transport::ClientTlsConfig {
889 use tonic::transport::{Certificate, ClientTlsConfig, Identity};
890 ClientTlsConfig::new()
891 .ca_certificate(Certificate::from_pem(&material.trust_bundle_pem))
892 .identity(Identity::from_pem(
893 &material.client_cert_chain_pem,
894 &material.client_key_pem,
895 ))
896}
897
898fn build_span_exporter(
899 protocol: ExportProtocol,
900 endpoint: &str,
901 timeout: Option<Duration>,
902 #[cfg(feature = "grpc-mtls")] mtls: Option<&MtlsMaterial>,
903) -> Result<opentelemetry_otlp::SpanExporter, Box<dyn Error>> {
904 match protocol {
905 #[cfg(feature = "grpc")]
906 ExportProtocol::Grpc => {
907 let mut b = opentelemetry_otlp::SpanExporter::builder()
908 .with_tonic()
909 .with_endpoint(endpoint);
910 if let Some(t) = timeout {
911 b = b.with_timeout(t);
912 }
913 #[cfg(feature = "grpc-mtls")]
914 if let Some(m) = mtls {
915 use opentelemetry_otlp::WithTonicConfig as _;
916 b = b.with_tls_config(build_tls_config(m));
917 }
918 Ok(b.build()?)
919 }
920 #[cfg(feature = "http")]
921 ExportProtocol::HttpProtobuf => {
922 let mut b = opentelemetry_otlp::SpanExporter::builder()
923 .with_http()
924 .with_endpoint(endpoint);
925 if let Some(t) = timeout {
926 b = b.with_timeout(t);
927 }
928 Ok(b.build()?)
929 }
930 }
931}
932
933fn build_metric_exporter(
934 protocol: ExportProtocol,
935 endpoint: &str,
936 timeout: Option<Duration>,
937 #[cfg(feature = "grpc-mtls")] mtls: Option<&MtlsMaterial>,
938) -> Result<opentelemetry_otlp::MetricExporter, Box<dyn Error>> {
939 match protocol {
940 #[cfg(feature = "grpc")]
941 ExportProtocol::Grpc => {
942 let mut b = opentelemetry_otlp::MetricExporter::builder()
943 .with_tonic()
944 .with_endpoint(endpoint);
945 if let Some(t) = timeout {
946 b = b.with_timeout(t);
947 }
948 #[cfg(feature = "grpc-mtls")]
949 if let Some(m) = mtls {
950 use opentelemetry_otlp::WithTonicConfig as _;
951 b = b.with_tls_config(build_tls_config(m));
952 }
953 Ok(b.build()?)
954 }
955 #[cfg(feature = "http")]
956 ExportProtocol::HttpProtobuf => {
957 let mut b = opentelemetry_otlp::MetricExporter::builder()
958 .with_http()
959 .with_endpoint(endpoint);
960 if let Some(t) = timeout {
961 b = b.with_timeout(t);
962 }
963 Ok(b.build()?)
964 }
965 }
966}
967
968fn build_log_exporter(
969 protocol: ExportProtocol,
970 endpoint: &str,
971 timeout: Option<Duration>,
972 #[cfg(feature = "grpc-mtls")] mtls: Option<&MtlsMaterial>,
973) -> Result<opentelemetry_otlp::LogExporter, Box<dyn Error>> {
974 match protocol {
975 #[cfg(feature = "grpc")]
976 ExportProtocol::Grpc => {
977 let mut b = opentelemetry_otlp::LogExporter::builder()
978 .with_tonic()
979 .with_endpoint(endpoint);
980 if let Some(t) = timeout {
981 b = b.with_timeout(t);
982 }
983 #[cfg(feature = "grpc-mtls")]
984 if let Some(m) = mtls {
985 use opentelemetry_otlp::WithTonicConfig as _;
986 b = b.with_tls_config(build_tls_config(m));
987 }
988 Ok(b.build()?)
989 }
990 #[cfg(feature = "http")]
991 ExportProtocol::HttpProtobuf => {
992 let mut b = opentelemetry_otlp::LogExporter::builder()
993 .with_http()
994 .with_endpoint(endpoint);
995 if let Some(t) = timeout {
996 b = b.with_timeout(t);
997 }
998 Ok(b.build()?)
999 }
1000 }
1001}
1002
1003pub fn build_resource(
1018 service_name: &str,
1019 service_version: Option<&str>,
1020 deployment_environment: Option<&str>,
1021) -> Resource {
1022 let hostname = hostname::get()
1023 .ok()
1024 .and_then(|h| h.into_string().ok())
1025 .unwrap_or_default();
1026
1027 let mut builder = Resource::builder()
1028 .with_service_name(service_name.to_string())
1029 .with_attributes([
1030 KeyValue::new(HOST_NAME, hostname),
1031 KeyValue::new(PROCESS_PID, std::process::id() as i64),
1032 ]);
1033
1034 if let Some(version) = service_version {
1035 builder = builder.with_attribute(KeyValue::new(SERVICE_VERSION, version.to_string()));
1036 }
1037
1038 if let Some(env) = deployment_environment {
1039 builder =
1040 builder.with_attribute(KeyValue::new(DEPLOYMENT_ENVIRONMENT_NAME, env.to_string()));
1041 }
1042
1043 builder.build()
1044}
1045
1046#[cfg(feature = "axum")]
1064pub fn axum_layer() -> axum_middleware::OtelTraceLayer {
1065 axum_middleware::OtelTraceLayer
1066}
1067
1068#[cfg(feature = "axum")]
1099pub fn span_enricher_layer<T>() -> axum_middleware::SpanEnricherLayer<T>
1100where
1101 T: span_enrichment::EnrichSpan + Clone + Send + Sync + 'static,
1102{
1103 axum_middleware::SpanEnricherLayer::default()
1104}
1105
1106#[cfg(test)]
1107mod tests {
1108 use super::*;
1109 use std::sync::Mutex;
1110
1111 static ENV_LOCK: Mutex<()> = Mutex::new(());
1112
1113 #[test]
1114 fn resource_contains_all_attributes_when_provided() {
1115 let resource = build_resource("test-svc", Some("1.2.3"), Some("staging"));
1116
1117 assert_eq!(
1118 resource.get(&opentelemetry::Key::new("service.name")),
1119 Some(opentelemetry::Value::from("test-svc")),
1120 );
1121 assert_eq!(
1122 resource.get(&opentelemetry::Key::new(SERVICE_VERSION)),
1123 Some(opentelemetry::Value::from("1.2.3")),
1124 );
1125 assert_eq!(
1126 resource.get(&opentelemetry::Key::new(DEPLOYMENT_ENVIRONMENT_NAME)),
1127 Some(opentelemetry::Value::from("staging")),
1128 );
1129 assert!(resource.get(&opentelemetry::Key::new(HOST_NAME)).is_some());
1130 assert!(
1131 resource
1132 .get(&opentelemetry::Key::new(PROCESS_PID))
1133 .is_some()
1134 );
1135 }
1136
1137 #[test]
1138 fn resource_graceful_when_optional_values_omitted() {
1139 let resource = build_resource("test-svc", None, None);
1140
1141 assert_eq!(
1142 resource.get(&opentelemetry::Key::new("service.name")),
1143 Some(opentelemetry::Value::from("test-svc")),
1144 );
1145 assert!(
1146 resource
1147 .get(&opentelemetry::Key::new(SERVICE_VERSION))
1148 .is_none()
1149 );
1150 assert!(
1151 resource
1152 .get(&opentelemetry::Key::new(DEPLOYMENT_ENVIRONMENT_NAME))
1153 .is_none()
1154 );
1155 assert!(resource.get(&opentelemetry::Key::new(HOST_NAME)).is_some());
1157 assert!(
1158 resource
1159 .get(&opentelemetry::Key::new(PROCESS_PID))
1160 .is_some()
1161 );
1162 }
1163
1164 #[test]
1165 fn trace_sampler_ratio_converts_to_sdk() {
1166 let sampler = TraceSampler::TraceIdRatio(0.5);
1167 let sdk = sampler.into_sdk_sampler();
1168 assert_eq!(format!("{sdk:?}"), "TraceIdRatioBased(0.5)");
1169 }
1170
1171 #[test]
1172 fn trace_sampler_parent_based_converts_to_sdk() {
1173 let sampler = TraceSampler::ParentBased(Box::new(TraceSampler::TraceIdRatio(0.25)));
1174 let sdk = sampler.into_sdk_sampler();
1175 let debug = format!("{sdk:?}");
1176 assert!(debug.contains("ParentBased"));
1177 assert!(debug.contains("0.25"));
1178 }
1179
1180 unsafe fn set_env(key: &str, val: &str) {
1182 unsafe {
1183 std::env::set_var(key, val);
1184 }
1185 }
1186
1187 unsafe fn remove_env(key: &str) {
1188 unsafe {
1189 std::env::remove_var(key);
1190 }
1191 }
1192
1193 #[test]
1194 fn sampler_from_env_reads_traceidratio() {
1195 let _lock = ENV_LOCK.lock().unwrap();
1196 unsafe {
1197 set_env("OTEL_TRACES_SAMPLER", "traceidratio");
1198 set_env("OTEL_TRACES_SAMPLER_ARG", "0.42");
1199 }
1200
1201 let sampler = sampler_from_env()
1202 .expect("should not error")
1203 .expect("should return Some");
1204 assert!(
1205 matches!(sampler, TraceSampler::TraceIdRatio(r) if (r - 0.42).abs() < f64::EPSILON)
1206 );
1207
1208 unsafe {
1209 remove_env("OTEL_TRACES_SAMPLER");
1210 remove_env("OTEL_TRACES_SAMPLER_ARG");
1211 }
1212 }
1213
1214 #[test]
1215 fn sampler_from_env_returns_none_when_unset() {
1216 let _lock = ENV_LOCK.lock().unwrap();
1217 unsafe {
1218 remove_env("OTEL_TRACES_SAMPLER");
1219 }
1220 assert!(sampler_from_env().expect("should not error").is_none());
1221 }
1222
1223 #[test]
1224 fn sampler_from_env_reads_parentbased_traceidratio() {
1225 let _lock = ENV_LOCK.lock().unwrap();
1226 unsafe {
1227 set_env("OTEL_TRACES_SAMPLER", "parentbased_traceidratio");
1228 set_env("OTEL_TRACES_SAMPLER_ARG", "0.1");
1229 }
1230
1231 let sampler = sampler_from_env()
1232 .expect("should not error")
1233 .expect("should return Some");
1234 assert!(
1235 matches!(sampler, TraceSampler::ParentBased(inner) if matches!(*inner, TraceSampler::TraceIdRatio(r) if (r - 0.1).abs() < f64::EPSILON))
1236 );
1237
1238 unsafe {
1239 remove_env("OTEL_TRACES_SAMPLER");
1240 remove_env("OTEL_TRACES_SAMPLER_ARG");
1241 }
1242 }
1243
1244 #[test]
1245 fn sampler_from_env_parentbased_always_on() {
1246 let _lock = ENV_LOCK.lock().unwrap();
1247 unsafe {
1248 set_env("OTEL_TRACES_SAMPLER", "parentbased_always_on");
1249 }
1250 let sampler = sampler_from_env()
1251 .expect("should not error")
1252 .expect("should return Some");
1253 assert!(
1254 matches!(sampler, TraceSampler::ParentBased(inner) if matches!(*inner, TraceSampler::AlwaysOn))
1255 );
1256 unsafe {
1257 remove_env("OTEL_TRACES_SAMPLER");
1258 }
1259 }
1260
1261 #[test]
1262 fn sampler_from_env_parentbased_always_off() {
1263 let _lock = ENV_LOCK.lock().unwrap();
1264 unsafe {
1265 set_env("OTEL_TRACES_SAMPLER", "parentbased_always_off");
1266 }
1267 let sampler = sampler_from_env()
1268 .expect("should not error")
1269 .expect("should return Some");
1270 assert!(
1271 matches!(sampler, TraceSampler::ParentBased(inner) if matches!(*inner, TraceSampler::AlwaysOff))
1272 );
1273 unsafe {
1274 remove_env("OTEL_TRACES_SAMPLER");
1275 }
1276 }
1277
1278 #[test]
1279 fn sampler_from_env_always_on() {
1280 let _lock = ENV_LOCK.lock().unwrap();
1281 unsafe {
1282 set_env("OTEL_TRACES_SAMPLER", "always_on");
1283 }
1284 let sampler = sampler_from_env()
1285 .expect("should not error")
1286 .expect("should return Some");
1287 assert!(matches!(sampler, TraceSampler::AlwaysOn));
1288 unsafe {
1289 remove_env("OTEL_TRACES_SAMPLER");
1290 }
1291 }
1292
1293 #[test]
1294 fn sampler_from_env_always_off() {
1295 let _lock = ENV_LOCK.lock().unwrap();
1296 unsafe {
1297 set_env("OTEL_TRACES_SAMPLER", "always_off");
1298 }
1299 let sampler = sampler_from_env()
1300 .expect("should not error")
1301 .expect("should return Some");
1302 assert!(matches!(sampler, TraceSampler::AlwaysOff));
1303 unsafe {
1304 remove_env("OTEL_TRACES_SAMPLER");
1305 }
1306 }
1307
1308 #[test]
1309 fn sampler_from_env_unknown_returns_error() {
1310 let _lock = ENV_LOCK.lock().unwrap();
1311 unsafe {
1312 set_env("OTEL_TRACES_SAMPLER", "unknown_sampler");
1313 }
1314 let err = sampler_from_env().expect_err("unknown sampler should produce an error");
1315 assert!(
1316 err.to_string().contains("unknown_sampler"),
1317 "error message should include the unknown name, got: {err}"
1318 );
1319 unsafe {
1320 remove_env("OTEL_TRACES_SAMPLER");
1321 }
1322 }
1323
1324 #[test]
1325 fn trace_sampler_always_on_converts_to_sdk() {
1326 let sdk = TraceSampler::AlwaysOn.into_sdk_sampler();
1327 assert_eq!(format!("{sdk:?}"), "AlwaysOn");
1328 }
1329
1330 #[test]
1331 fn trace_sampler_always_off_converts_to_sdk() {
1332 let sdk = TraceSampler::AlwaysOff.into_sdk_sampler();
1333 assert_eq!(format!("{sdk:?}"), "AlwaysOff");
1334 }
1335
1336 #[test]
1337 fn builder_has_sensible_defaults() {
1338 let builder = Telemetry::builder("test-svc");
1339 assert_eq!(builder.service_name.as_deref(), Some("test-svc"));
1340 assert!(builder.service_version.is_none());
1341 assert!(builder.deployment_environment.is_none());
1342 assert!(builder.sampler.is_none());
1343 assert!(builder.metrics);
1344 assert!(!builder.logs);
1345 assert!(builder.protocol.is_none());
1346 assert!(builder.max_export_batch_size.is_none());
1347 assert!(builder.metric_export_interval.is_none());
1348 assert!(builder.export_timeout.is_none());
1349 }
1350
1351 #[test]
1352 fn from_env_builder_has_no_service_name() {
1353 let builder = Telemetry::from_env();
1354 assert!(builder.service_name.is_none());
1355 }
1356
1357 #[test]
1358 fn with_export_timeout_stores_value() {
1359 let timeout = Duration::from_secs(5);
1360 let builder = Telemetry::builder("test-svc").with_export_timeout(timeout);
1361 assert_eq!(builder.export_timeout, Some(timeout));
1362 }
1363
1364 #[test]
1365 fn timeout_from_env_reads_milliseconds() {
1366 let _lock = ENV_LOCK.lock().unwrap();
1367 unsafe {
1368 set_env("OTEL_EXPORTER_OTLP_TIMEOUT", "5000");
1369 }
1370 let t = timeout_from_env();
1371 assert_eq!(t, Some(Duration::from_millis(5000)));
1372 unsafe {
1373 remove_env("OTEL_EXPORTER_OTLP_TIMEOUT");
1374 }
1375 }
1376
1377 #[test]
1378 fn timeout_from_env_returns_none_when_unset() {
1379 let _lock = ENV_LOCK.lock().unwrap();
1380 unsafe {
1381 remove_env("OTEL_EXPORTER_OTLP_TIMEOUT");
1382 }
1383 assert_eq!(timeout_from_env(), None);
1384 }
1385
1386 #[test]
1387 fn service_name_from_env_used_when_none_given() {
1388 let builder = Telemetry::from_env();
1389 assert!(builder.service_name.is_none());
1390 }
1391
1392 #[test]
1393 fn explicit_service_name_overrides_env_var() {
1394 let builder = Telemetry::builder("explicit-svc");
1395 assert_eq!(builder.service_name.as_deref(), Some("explicit-svc"));
1396 }
1397
1398 #[test]
1399 fn from_env_builder_service_name_is_none() {
1400 let builder = Telemetry::from_env();
1401 assert!(builder.service_name.is_none());
1402 }
1403
1404 #[test]
1405 fn init_returns_error_for_unknown_otel_traces_sampler() {
1406 let _lock = ENV_LOCK.lock().unwrap();
1407 unsafe {
1408 set_env("OTEL_TRACES_SAMPLER", "not_a_real_sampler");
1409 }
1410 let result = Telemetry::builder("test-svc").with_metrics(false).init();
1411 let err = result
1412 .err()
1413 .expect("unknown sampler env var should cause init to fail");
1414 assert!(
1415 err.to_string().contains("not_a_real_sampler"),
1416 "error should name the unknown sampler, got: {err}"
1417 );
1418 unsafe {
1419 remove_env("OTEL_TRACES_SAMPLER");
1420 }
1421 }
1422
1423 #[test]
1424 fn with_max_export_batch_size_stores_value() {
1425 let builder = Telemetry::builder("test-svc").with_max_export_batch_size(1024);
1426 assert_eq!(builder.max_export_batch_size, Some(1024));
1427 }
1428
1429 #[test]
1430 fn with_metric_export_interval_stores_value() {
1431 let interval = Duration::from_secs(30);
1432 let builder = Telemetry::builder("test-svc").with_metric_export_interval(interval);
1433 assert_eq!(builder.metric_export_interval, Some(interval));
1434 }
1435
1436 #[test]
1437 fn init_rejects_zero_metric_export_interval() {
1438 let err = Telemetry::builder("test-svc")
1439 .with_metric_export_interval(Duration::ZERO)
1440 .with_metrics(false)
1441 .init()
1442 .err()
1443 .expect("expected error for zero interval");
1444 assert!(
1445 err.to_string().contains("metric_export_interval"),
1446 "error message should mention metric_export_interval, got: {err}"
1447 );
1448 }
1449
1450 #[test]
1451 fn builder_with_custom_values() {
1452 let builder = Telemetry::builder("test-svc")
1453 .with_version("2.0.0")
1454 .with_environment("production")
1455 .with_sampler(TraceSampler::TraceIdRatio(0.5))
1456 .with_metrics(false);
1457
1458 assert_eq!(builder.service_name.as_deref(), Some("test-svc"));
1459 assert_eq!(builder.service_version.as_deref(), Some("2.0.0"));
1460 assert_eq!(
1461 builder.deployment_environment.as_deref(),
1462 Some("production")
1463 );
1464 assert!(
1465 matches!(builder.sampler, Some(TraceSampler::TraceIdRatio(r)) if (r - 0.5).abs() < f64::EPSILON)
1466 );
1467 assert!(!builder.metrics);
1468 }
1469
1470 #[test]
1471 #[cfg(feature = "grpc")]
1472 fn builder_with_protocol_grpc() {
1473 let builder = Telemetry::builder("test-svc").with_protocol(ExportProtocol::Grpc);
1474 assert_eq!(builder.protocol, Some(ExportProtocol::Grpc));
1475 }
1476
1477 #[test]
1478 #[cfg(feature = "http")]
1479 fn builder_with_protocol_http() {
1480 let builder = Telemetry::builder("test-svc").with_protocol(ExportProtocol::HttpProtobuf);
1481 assert_eq!(builder.protocol, Some(ExportProtocol::HttpProtobuf));
1482 }
1483
1484 #[test]
1485 #[cfg(feature = "grpc")]
1486 fn protocol_from_env_reads_grpc() {
1487 let _lock = ENV_LOCK.lock().unwrap();
1488 unsafe {
1489 set_env("OTEL_EXPORTER_OTLP_PROTOCOL", "grpc");
1490 }
1491 assert_eq!(protocol_from_env(), Some(ExportProtocol::Grpc));
1492 unsafe {
1493 remove_env("OTEL_EXPORTER_OTLP_PROTOCOL");
1494 }
1495 }
1496
1497 #[test]
1498 #[cfg(feature = "http")]
1499 fn protocol_from_env_reads_http_protobuf() {
1500 let _lock = ENV_LOCK.lock().unwrap();
1501 unsafe {
1502 set_env("OTEL_EXPORTER_OTLP_PROTOCOL", "http/protobuf");
1503 }
1504 assert_eq!(protocol_from_env(), Some(ExportProtocol::HttpProtobuf));
1505 unsafe {
1506 remove_env("OTEL_EXPORTER_OTLP_PROTOCOL");
1507 }
1508 }
1509
1510 #[test]
1511 fn protocol_from_env_returns_none_when_unset() {
1512 let _lock = ENV_LOCK.lock().unwrap();
1513 unsafe {
1514 remove_env("OTEL_EXPORTER_OTLP_PROTOCOL");
1515 }
1516 assert_eq!(protocol_from_env(), None);
1517 }
1518
1519 #[test]
1520 fn protocol_from_env_returns_none_for_unknown() {
1521 let _lock = ENV_LOCK.lock().unwrap();
1522 unsafe {
1523 set_env("OTEL_EXPORTER_OTLP_PROTOCOL", "websocket");
1524 }
1525 assert_eq!(protocol_from_env(), None);
1526 unsafe {
1527 remove_env("OTEL_EXPORTER_OTLP_PROTOCOL");
1528 }
1529 }
1530
1531 #[test]
1532 fn builder_is_send_and_sync() {
1533 fn assert_send_sync<T: Send + Sync>() {}
1534 assert_send_sync::<TelemetryBuilder>();
1535 }
1536
1537 #[test]
1538 fn with_shutdown_timeout_stores_value() {
1539 let timeout = Duration::from_secs(10);
1540 let builder = Telemetry::builder("test-svc").with_shutdown_timeout(timeout);
1541 assert_eq!(builder.shutdown_timeout, timeout);
1542 }
1543
1544 #[test]
1545 fn default_shutdown_timeout_is_five_seconds() {
1546 let builder = Telemetry::builder("test-svc");
1547 assert_eq!(builder.shutdown_timeout, Duration::from_secs(5));
1548 }
1549
1550 #[cfg(feature = "testing")]
1558 #[test]
1559 fn drop_completes_within_shutdown_timeout() {
1560 let mut handles = crate::Telemetry::testing("drop-timeout-test");
1562 handles.shutdown_timeout = Duration::from_millis(100);
1564
1565 let start = std::time::Instant::now();
1566 drop(handles);
1567 let elapsed = start.elapsed();
1568
1569 assert!(
1571 elapsed < Duration::from_millis(500),
1572 "drop took {elapsed:?}, expected < 500 ms"
1573 );
1574 }
1575}