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