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
38#[cfg(feature = "profiling")]
39pub mod profiling;
40
41pub mod instrumented_port;
42pub mod log_bridge;
43pub mod span_enrichment;
44
45pub use instrumented_port::{Instrumented, InstrumentedArc};
46pub use log_bridge::{
47 PROPAGATED_SPAN_FIELDS, SpanLogAttrs, record_span_log_attr, record_span_log_attr_on,
48};
49
50use opentelemetry::KeyValue;
51use opentelemetry::propagation::TextMapCompositePropagator;
52use opentelemetry_otlp::WithExportConfig;
53use opentelemetry_sdk::{
54 Resource,
55 logs::SdkLoggerProvider,
56 metrics::{MeterProviderBuilder, PeriodicReader, SdkMeterProvider},
57 propagation::{BaggagePropagator, TraceContextPropagator},
58 trace::{BatchConfigBuilder, BatchSpanProcessor, Sampler, SdkTracerProvider},
59};
60use opentelemetry_semantic_conventions::attribute::{
61 DEPLOYMENT_ENVIRONMENT_NAME, HOST_NAME, PROCESS_PID, SERVICE_VERSION,
62};
63use std::error::Error;
64use std::time::Duration;
65use tracing_subscriber::layer::SubscriberExt;
66use tracing_subscriber::util::SubscriberInitExt;
67
68#[derive(Debug, Clone)]
83pub enum TraceSampler {
84 AlwaysOn,
86 AlwaysOff,
88 TraceIdRatio(f64),
90 ParentBased(Box<TraceSampler>),
93}
94
95impl TraceSampler {
96 fn into_sdk_sampler(self) -> Sampler {
98 match self {
99 TraceSampler::AlwaysOn => Sampler::AlwaysOn,
100 TraceSampler::AlwaysOff => Sampler::AlwaysOff,
101 TraceSampler::TraceIdRatio(r) => Sampler::TraceIdRatioBased(r),
102 TraceSampler::ParentBased(inner) => {
103 Sampler::ParentBased(Box::new(inner.into_sdk_sampler()))
104 }
105 }
106 }
107}
108
109fn sampler_from_env() -> Result<Option<TraceSampler>, Box<dyn Error>> {
117 let name = match std::env::var("OTEL_TRACES_SAMPLER") {
118 Ok(v) => v,
119 Err(_) => return Ok(None),
120 };
121 let arg = std::env::var("OTEL_TRACES_SAMPLER_ARG").ok();
122 let sampler = match name.as_str() {
123 "always_on" => TraceSampler::AlwaysOn,
124 "always_off" => TraceSampler::AlwaysOff,
125 "traceidratio" => {
126 let ratio = arg
127 .as_deref()
128 .unwrap_or("1.0")
129 .parse::<f64>()
130 .unwrap_or(1.0);
131 TraceSampler::TraceIdRatio(ratio)
132 }
133 "parentbased_always_on" => TraceSampler::ParentBased(Box::new(TraceSampler::AlwaysOn)),
134 "parentbased_always_off" => TraceSampler::ParentBased(Box::new(TraceSampler::AlwaysOff)),
135 "parentbased_traceidratio" => {
136 let ratio = arg
137 .as_deref()
138 .unwrap_or("1.0")
139 .parse::<f64>()
140 .unwrap_or(1.0);
141 TraceSampler::ParentBased(Box::new(TraceSampler::TraceIdRatio(ratio)))
142 }
143 unknown => {
144 return Err(format!(
145 "OTEL_TRACES_SAMPLER: unrecognised sampler name '{unknown}'. \
146 Valid values: always_on, always_off, traceidratio, \
147 parentbased_always_on, parentbased_always_off, parentbased_traceidratio"
148 )
149 .into());
150 }
151 };
152 Ok(Some(sampler))
153}
154
155const DEFAULT_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5);
157
158pub struct TelemetryHandles {
179 pub tracer_provider: SdkTracerProvider,
180 pub meter_provider: Option<SdkMeterProvider>,
181 pub logger_provider: Option<SdkLoggerProvider>,
182 shutdown_timeout: Duration,
183 #[cfg(feature = "profiling")]
184 pub profiling_handle: Option<profiling::ProfilingHandle>,
185}
186
187impl TelemetryHandles {
188 pub fn shutdown(&self) -> Result<(), Box<dyn Error>> {
201 self.tracer_provider.shutdown()?;
202 if let Some(mp) = &self.meter_provider {
203 mp.shutdown()?;
204 }
205 if let Some(lp) = &self.logger_provider {
206 lp.shutdown()?;
207 }
208 Ok(())
209 }
210}
211
212impl Drop for TelemetryHandles {
213 fn drop(&mut self) {
214 let tracer_provider = self.tracer_provider.clone();
215 let meter_provider = self.meter_provider.clone();
216 let logger_provider = self.logger_provider.clone();
217 let timeout = self.shutdown_timeout;
218
219 let (tx, rx) = std::sync::mpsc::channel();
220 std::thread::spawn(move || {
221 if let Err(e) = tracer_provider.shutdown() {
222 tracing::warn!("tracer provider shutdown error: {e}");
223 }
224 if let Some(mp) = meter_provider
225 && let Err(e) = mp.shutdown()
226 {
227 tracing::warn!("meter provider shutdown error: {e}");
228 }
229 if let Some(lp) = logger_provider
230 && let Err(e) = lp.shutdown()
231 {
232 tracing::warn!("logger provider shutdown error: {e}");
233 }
234 let _ = tx.send(());
235 });
236
237 if rx.recv_timeout(timeout).is_err() {
238 tracing::warn!(
239 "telemetry shutdown did not complete within {timeout:?}; \
240 some spans/metrics may not have been exported"
241 );
242 }
243 }
244}
245
246#[derive(Debug, Clone, Copy, PartialEq, Eq)]
269pub enum ExportProtocol {
270 #[cfg(feature = "grpc")]
272 Grpc,
273 #[cfg(feature = "http")]
275 HttpProtobuf,
276}
277
278#[cfg(feature = "grpc-mtls")]
288#[derive(Clone)]
289pub struct MtlsMaterial {
290 pub client_cert_chain_pem: Vec<u8>,
292 pub client_key_pem: Vec<u8>,
294 pub trust_bundle_pem: Vec<u8>,
296}
297
298#[cfg(feature = "grpc-mtls")]
299impl std::fmt::Debug for MtlsMaterial {
300 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
301 f.debug_struct("MtlsMaterial")
302 .field("client_cert_chain_pem", &"<redacted>")
303 .field("client_key_pem", &"<redacted>")
304 .field("trust_bundle_pem", &"<redacted>")
305 .finish()
306 }
307}
308
309fn protocol_from_env() -> Option<ExportProtocol> {
311 let val = std::env::var("OTEL_EXPORTER_OTLP_PROTOCOL").ok()?;
312 match val.trim() {
313 #[cfg(feature = "grpc")]
314 "grpc" => Some(ExportProtocol::Grpc),
315 #[cfg(feature = "http")]
316 "http/protobuf" => Some(ExportProtocol::HttpProtobuf),
317 _ => None,
318 }
319}
320
321pub struct Telemetry;
337
338impl Telemetry {
339 pub fn builder(service_name: &str) -> TelemetryBuilder {
343 TelemetryBuilder {
344 service_name: Some(service_name.to_string()),
345 service_version: None,
346 deployment_environment: None,
347 sampler: None,
348 metrics: true,
349 logs: false,
350 protocol: None,
351 max_export_batch_size: None,
352 metric_export_interval: None,
353 export_timeout: None,
354 shutdown_timeout: DEFAULT_SHUTDOWN_TIMEOUT,
355 extra_layers: Vec::new(),
356 extra_metric_readers: Vec::new(),
357 #[cfg(feature = "grpc-mtls")]
358 mtls: None,
359 propagated_span_fields: crate::log_bridge::PROPAGATED_SPAN_FIELDS,
360 #[cfg(feature = "profiling")]
361 pyroscope_endpoint: None,
362 }
363 }
364
365 pub fn from_env() -> TelemetryBuilder {
375 TelemetryBuilder {
376 service_name: None,
377 service_version: None,
378 deployment_environment: None,
379 sampler: None,
380 metrics: true,
381 logs: false,
382 protocol: None,
383 max_export_batch_size: None,
384 metric_export_interval: None,
385 export_timeout: None,
386 shutdown_timeout: DEFAULT_SHUTDOWN_TIMEOUT,
387 extra_layers: Vec::new(),
388 extra_metric_readers: Vec::new(),
389 #[cfg(feature = "grpc-mtls")]
390 mtls: None,
391 propagated_span_fields: crate::log_bridge::PROPAGATED_SPAN_FIELDS,
392 #[cfg(feature = "profiling")]
393 pyroscope_endpoint: None,
394 }
395 }
396}
397
398#[must_use = "a TelemetryBuilder does nothing until .init() is called"]
416pub struct TelemetryBuilder {
417 service_name: Option<String>,
418 service_version: Option<String>,
419 deployment_environment: Option<String>,
420 sampler: Option<TraceSampler>,
421 metrics: bool,
422 logs: bool,
423 protocol: Option<ExportProtocol>,
424 max_export_batch_size: Option<usize>,
425 metric_export_interval: Option<Duration>,
426 export_timeout: Option<Duration>,
427 shutdown_timeout: Duration,
428 extra_layers: Vec<
429 Box<dyn tracing_subscriber::Layer<tracing_subscriber::Registry> + Send + Sync + 'static>,
430 >,
431 extra_metric_readers: Vec<MeterProviderInstaller>,
432 #[cfg(feature = "grpc-mtls")]
433 mtls: Option<MtlsMaterial>,
434 propagated_span_fields: &'static [&'static str],
435 #[cfg(feature = "profiling")]
436 pyroscope_endpoint: Option<String>,
437}
438
439type MeterProviderInstaller =
444 Box<dyn FnOnce(MeterProviderBuilder) -> MeterProviderBuilder + Send + Sync>;
445
446impl TelemetryBuilder {
447 pub fn with_version(mut self, version: &str) -> Self {
449 self.service_version = Some(version.to_string());
450 self
451 }
452
453 pub fn with_environment(mut self, environment: &str) -> Self {
455 self.deployment_environment = Some(environment.to_string());
456 self
457 }
458
459 #[cfg(feature = "grpc-mtls")]
484 pub fn with_mtls(mut self, material: MtlsMaterial) -> Self {
485 self.mtls = Some(material);
486 self.protocol = Some(ExportProtocol::Grpc);
487 self
488 }
489
490 pub fn with_sampler(mut self, sampler: TraceSampler) -> Self {
493 self.sampler = Some(sampler);
494 self
495 }
496
497 pub fn with_metrics(mut self, enabled: bool) -> Self {
499 self.metrics = enabled;
500 self
501 }
502
503 pub fn with_protocol(mut self, protocol: ExportProtocol) -> Self {
507 self.protocol = Some(protocol);
508 self
509 }
510
511 pub fn with_max_export_batch_size(mut self, size: usize) -> Self {
516 self.max_export_batch_size = Some(size);
517 self
518 }
519
520 pub fn with_metric_export_interval(mut self, interval: Duration) -> Self {
525 self.metric_export_interval = Some(interval);
526 self
527 }
528
529 pub fn with_logs(mut self, enabled: bool) -> Self {
536 self.logs = enabled;
537 self
538 }
539
540 pub fn with_propagated_span_fields(mut self, fields: &'static [&'static str]) -> Self {
553 self.propagated_span_fields = fields;
554 self
555 }
556
557 pub fn with_export_timeout(mut self, timeout: Duration) -> Self {
561 self.export_timeout = Some(timeout);
562 self
563 }
564
565 pub fn with_shutdown_timeout(mut self, timeout: Duration) -> Self {
572 self.shutdown_timeout = timeout;
573 self
574 }
575
576 #[cfg(feature = "profiling")]
594 pub fn with_profiling(mut self, endpoint: &str) -> Self {
595 self.pyroscope_endpoint = Some(endpoint.to_string());
596 self
597 }
598
599 pub fn with_meter_provider_setup<F>(mut self, setup: F) -> Self
656 where
657 F: FnOnce(MeterProviderBuilder) -> MeterProviderBuilder + Send + Sync + 'static,
658 {
659 self.extra_metric_readers.push(Box::new(setup));
660 self
661 }
662
663 pub fn with_layer<L>(mut self, layer: L) -> Self
664 where
665 L: tracing_subscriber::Layer<tracing_subscriber::Registry> + Send + Sync + 'static,
666 {
667 self.extra_layers.push(Box::new(layer));
668 self
669 }
670
671 pub fn init(self) -> Result<TelemetryHandles, Box<dyn Error>> {
686 if let Some(interval) = self.metric_export_interval
687 && interval.is_zero()
688 {
689 return Err("metric_export_interval must be greater than zero".into());
690 }
691
692 let protocol = self.protocol.or_else(protocol_from_env).unwrap_or({
693 #[cfg(feature = "grpc")]
694 {
695 ExportProtocol::Grpc
696 }
697 #[cfg(all(not(feature = "grpc"), feature = "http"))]
698 {
699 ExportProtocol::HttpProtobuf
700 }
701 });
702
703 let default_endpoint = match protocol {
704 #[cfg(feature = "grpc")]
705 ExportProtocol::Grpc => "http://localhost:4317",
706 #[cfg(feature = "http")]
707 ExportProtocol::HttpProtobuf => "http://localhost:4318",
708 };
709 let endpoint = std::env::var("OTEL_EXPORTER_OTLP_ENDPOINT")
710 .unwrap_or_else(|_| default_endpoint.to_string());
711
712 let export_timeout = self.export_timeout.or_else(timeout_from_env);
714
715 let service_name = self.service_name.unwrap_or_else(|| {
717 std::env::var("OTEL_SERVICE_NAME").unwrap_or_else(|_| "unknown_service".to_string())
718 });
719
720 let resource = build_resource(
721 &service_name,
722 self.service_version.as_deref(),
723 self.deployment_environment.as_deref(),
724 );
725
726 let sampler = match self.sampler {
727 Some(s) => s,
728 None => sampler_from_env()?.unwrap_or(TraceSampler::AlwaysOn),
729 };
730
731 let trace_exporter = build_span_exporter(
733 protocol,
734 &endpoint,
735 export_timeout,
736 #[cfg(feature = "grpc-mtls")]
737 self.mtls.as_ref(),
738 )?;
739
740 let batch_processor = if let Some(size) = self.max_export_batch_size {
741 BatchSpanProcessor::builder(trace_exporter)
742 .with_batch_config(
743 BatchConfigBuilder::default()
744 .with_max_export_batch_size(size)
745 .build(),
746 )
747 .build()
748 } else {
749 BatchSpanProcessor::builder(trace_exporter).build()
750 };
751
752 let tracer_provider = SdkTracerProvider::builder()
753 .with_resource(resource.clone())
754 .with_sampler(sampler.into_sdk_sampler())
755 .with_span_processor(batch_processor)
756 .build();
757
758 opentelemetry::global::set_tracer_provider(tracer_provider.clone());
759
760 let propagator = TextMapCompositePropagator::new(vec![
762 Box::new(TraceContextPropagator::new()),
763 Box::new(BaggagePropagator::new()),
764 ]);
765 opentelemetry::global::set_text_map_propagator(propagator);
766
767 let meter_provider = if self.metrics {
769 let metric_exporter = build_metric_exporter(
770 protocol,
771 &endpoint,
772 export_timeout,
773 #[cfg(feature = "grpc-mtls")]
774 self.mtls.as_ref(),
775 )?;
776
777 let periodic_reader = if let Some(interval) = self.metric_export_interval {
778 PeriodicReader::builder(metric_exporter)
779 .with_interval(interval)
780 .build()
781 } else {
782 PeriodicReader::builder(metric_exporter).build()
783 };
784
785 let mut mp_builder = SdkMeterProvider::builder()
786 .with_resource(resource.clone())
787 .with_reader(periodic_reader);
788 for installer in self.extra_metric_readers {
789 mp_builder = installer(mp_builder);
790 }
791 let mp = mp_builder.build();
792
793 opentelemetry::global::set_meter_provider(mp.clone());
794
795 Some(mp)
796 } else {
797 None
798 };
799
800 let logger_provider = if self.logs {
802 let log_exporter = build_log_exporter(
803 protocol,
804 &endpoint,
805 export_timeout,
806 #[cfg(feature = "grpc-mtls")]
807 self.mtls.as_ref(),
808 )?;
809
810 let lp = SdkLoggerProvider::builder()
811 .with_resource(resource)
812 .with_batch_exporter(log_exporter)
813 .build();
814
815 Some(lp)
816 } else {
817 None
818 };
819
820 #[cfg(feature = "profiling")]
822 let profiling_handle = if let Some(ref endpoint) = self.pyroscope_endpoint {
823 profiling::start_pyroscope_bridge(&service_name, endpoint)?
824 } else {
825 None
826 };
827 #[cfg(not(feature = "profiling"))]
828 let _profiling_handle: Option<()> = None;
829
830 let otel_layer = tracing_opentelemetry::layer();
832
833 let extra = if self.extra_layers.is_empty() {
838 None
839 } else {
840 Some(self.extra_layers)
841 };
842
843 let registry = tracing_subscriber::registry()
844 .with(extra)
845 .with(tracing_subscriber::EnvFilter::from_default_env())
846 .with(tracing_subscriber::fmt::layer())
847 .with(otel_layer);
848
849 #[cfg(feature = "profiling-bridge-pyroscope-rs")]
850 let registry = registry.with(crate::profiling::ProfilingTagLayer);
851
852 if let Some(lp) = &logger_provider {
853 if let Err(e) = registry
854 .with(crate::log_bridge::SpanAwareLogBridge::new(
855 lp,
856 self.propagated_span_fields,
857 ))
858 .try_init()
859 {
860 eprintln!(
861 "otel-bootstrap: global tracing subscriber already installed — \
862 OTLP log records will NOT be exported to the collector: {e}"
863 );
864 }
865 } else if let Err(e) = registry.try_init() {
866 eprintln!(
867 "otel-bootstrap: global tracing subscriber already installed — \
868 OTLP telemetry will NOT be exported to the collector: {e}"
869 );
870 }
871
872 Ok(TelemetryHandles {
873 tracer_provider,
874 meter_provider,
875 logger_provider,
876 shutdown_timeout: self.shutdown_timeout,
877 #[cfg(feature = "profiling")]
878 profiling_handle,
879 })
880 }
881}
882
883pub fn init_telemetry(service_name: &str) -> Result<TelemetryHandles, Box<dyn Error>> {
897 Telemetry::builder(service_name).init()
898}
899
900pub fn init_telemetry_with_sampler(
917 service_name: &str,
918 sampler: Option<TraceSampler>,
919) -> Result<TelemetryHandles, Box<dyn Error>> {
920 let builder = Telemetry::builder(service_name);
921 match sampler {
922 Some(s) => builder.with_sampler(s),
923 None => builder, }
925 .init()
926}
927
928fn timeout_from_env() -> Option<Duration> {
930 let ms = std::env::var("OTEL_EXPORTER_OTLP_TIMEOUT").ok()?;
931 let ms: u64 = ms.trim().parse().ok()?;
932 Some(Duration::from_millis(ms))
933}
934
935#[cfg(feature = "grpc-mtls")]
942fn build_tls_config(material: &MtlsMaterial) -> tonic::transport::ClientTlsConfig {
943 use tonic::transport::{Certificate, ClientTlsConfig, Identity};
944 ClientTlsConfig::new()
945 .ca_certificate(Certificate::from_pem(&material.trust_bundle_pem))
946 .identity(Identity::from_pem(
947 &material.client_cert_chain_pem,
948 &material.client_key_pem,
949 ))
950}
951
952fn build_span_exporter(
953 protocol: ExportProtocol,
954 endpoint: &str,
955 timeout: Option<Duration>,
956 #[cfg(feature = "grpc-mtls")] mtls: Option<&MtlsMaterial>,
957) -> Result<opentelemetry_otlp::SpanExporter, Box<dyn Error>> {
958 match protocol {
959 #[cfg(feature = "grpc")]
960 ExportProtocol::Grpc => {
961 let mut b = opentelemetry_otlp::SpanExporter::builder()
962 .with_tonic()
963 .with_endpoint(endpoint);
964 if let Some(t) = timeout {
965 b = b.with_timeout(t);
966 }
967 #[cfg(feature = "grpc-mtls")]
968 if let Some(m) = mtls {
969 use opentelemetry_otlp::WithTonicConfig as _;
970 b = b.with_tls_config(build_tls_config(m));
971 }
972 Ok(b.build()?)
973 }
974 #[cfg(feature = "http")]
975 ExportProtocol::HttpProtobuf => {
976 let mut b = opentelemetry_otlp::SpanExporter::builder()
977 .with_http()
978 .with_endpoint(endpoint);
979 if let Some(t) = timeout {
980 b = b.with_timeout(t);
981 }
982 Ok(b.build()?)
983 }
984 }
985}
986
987fn build_metric_exporter(
988 protocol: ExportProtocol,
989 endpoint: &str,
990 timeout: Option<Duration>,
991 #[cfg(feature = "grpc-mtls")] mtls: Option<&MtlsMaterial>,
992) -> Result<opentelemetry_otlp::MetricExporter, Box<dyn Error>> {
993 match protocol {
994 #[cfg(feature = "grpc")]
995 ExportProtocol::Grpc => {
996 let mut b = opentelemetry_otlp::MetricExporter::builder()
997 .with_tonic()
998 .with_endpoint(endpoint);
999 if let Some(t) = timeout {
1000 b = b.with_timeout(t);
1001 }
1002 #[cfg(feature = "grpc-mtls")]
1003 if let Some(m) = mtls {
1004 use opentelemetry_otlp::WithTonicConfig as _;
1005 b = b.with_tls_config(build_tls_config(m));
1006 }
1007 Ok(b.build()?)
1008 }
1009 #[cfg(feature = "http")]
1010 ExportProtocol::HttpProtobuf => {
1011 let mut b = opentelemetry_otlp::MetricExporter::builder()
1012 .with_http()
1013 .with_endpoint(endpoint);
1014 if let Some(t) = timeout {
1015 b = b.with_timeout(t);
1016 }
1017 Ok(b.build()?)
1018 }
1019 }
1020}
1021
1022fn build_log_exporter(
1023 protocol: ExportProtocol,
1024 endpoint: &str,
1025 timeout: Option<Duration>,
1026 #[cfg(feature = "grpc-mtls")] mtls: Option<&MtlsMaterial>,
1027) -> Result<opentelemetry_otlp::LogExporter, Box<dyn Error>> {
1028 match protocol {
1029 #[cfg(feature = "grpc")]
1030 ExportProtocol::Grpc => {
1031 let mut b = opentelemetry_otlp::LogExporter::builder()
1032 .with_tonic()
1033 .with_endpoint(endpoint);
1034 if let Some(t) = timeout {
1035 b = b.with_timeout(t);
1036 }
1037 #[cfg(feature = "grpc-mtls")]
1038 if let Some(m) = mtls {
1039 use opentelemetry_otlp::WithTonicConfig as _;
1040 b = b.with_tls_config(build_tls_config(m));
1041 }
1042 Ok(b.build()?)
1043 }
1044 #[cfg(feature = "http")]
1045 ExportProtocol::HttpProtobuf => {
1046 let mut b = opentelemetry_otlp::LogExporter::builder()
1047 .with_http()
1048 .with_endpoint(endpoint);
1049 if let Some(t) = timeout {
1050 b = b.with_timeout(t);
1051 }
1052 Ok(b.build()?)
1053 }
1054 }
1055}
1056
1057pub fn build_resource(
1072 service_name: &str,
1073 service_version: Option<&str>,
1074 deployment_environment: Option<&str>,
1075) -> Resource {
1076 let hostname = hostname::get()
1077 .ok()
1078 .and_then(|h| h.into_string().ok())
1079 .unwrap_or_default();
1080
1081 let mut builder = Resource::builder()
1082 .with_service_name(service_name.to_string())
1083 .with_attributes([
1084 KeyValue::new(HOST_NAME, hostname),
1085 KeyValue::new(PROCESS_PID, std::process::id() as i64),
1086 ]);
1087
1088 if let Some(version) = service_version {
1089 builder = builder.with_attribute(KeyValue::new(SERVICE_VERSION, version.to_string()));
1090 }
1091
1092 if let Some(env) = deployment_environment {
1093 builder =
1094 builder.with_attribute(KeyValue::new(DEPLOYMENT_ENVIRONMENT_NAME, env.to_string()));
1095 }
1096
1097 builder.build()
1098}
1099
1100#[cfg(feature = "axum")]
1118pub fn axum_layer() -> axum_middleware::OtelTraceLayer {
1119 axum_middleware::OtelTraceLayer
1120}
1121
1122#[cfg(feature = "axum")]
1153pub fn span_enricher_layer<T>() -> axum_middleware::SpanEnricherLayer<T>
1154where
1155 T: span_enrichment::EnrichSpan + Clone + Send + Sync + 'static,
1156{
1157 axum_middleware::SpanEnricherLayer::default()
1158}
1159
1160#[cfg(feature = "tonic-tracing")]
1182pub fn grpc_client_layer() -> grpc_middleware::GrpcClientTraceLayer {
1183 grpc_middleware::GrpcClientTraceLayer
1184}
1185
1186#[cfg(feature = "tonic-tracing")]
1201pub fn grpc_server_layer() -> grpc_middleware::GrpcServerTraceLayer {
1202 grpc_middleware::GrpcServerTraceLayer
1203}
1204
1205#[cfg(test)]
1206mod tests {
1207 use super::*;
1208 use std::sync::Mutex;
1209
1210 static ENV_LOCK: Mutex<()> = Mutex::new(());
1211
1212 #[test]
1213 fn resource_contains_all_attributes_when_provided() {
1214 let resource = build_resource("test-svc", Some("1.2.3"), Some("staging"));
1215
1216 assert_eq!(
1217 resource.get(&opentelemetry::Key::new("service.name")),
1218 Some(opentelemetry::Value::from("test-svc")),
1219 );
1220 assert_eq!(
1221 resource.get(&opentelemetry::Key::new(SERVICE_VERSION)),
1222 Some(opentelemetry::Value::from("1.2.3")),
1223 );
1224 assert_eq!(
1225 resource.get(&opentelemetry::Key::new(DEPLOYMENT_ENVIRONMENT_NAME)),
1226 Some(opentelemetry::Value::from("staging")),
1227 );
1228 assert!(resource.get(&opentelemetry::Key::new(HOST_NAME)).is_some());
1229 assert!(
1230 resource
1231 .get(&opentelemetry::Key::new(PROCESS_PID))
1232 .is_some()
1233 );
1234 }
1235
1236 #[test]
1237 fn resource_graceful_when_optional_values_omitted() {
1238 let resource = build_resource("test-svc", None, None);
1239
1240 assert_eq!(
1241 resource.get(&opentelemetry::Key::new("service.name")),
1242 Some(opentelemetry::Value::from("test-svc")),
1243 );
1244 assert!(
1245 resource
1246 .get(&opentelemetry::Key::new(SERVICE_VERSION))
1247 .is_none()
1248 );
1249 assert!(
1250 resource
1251 .get(&opentelemetry::Key::new(DEPLOYMENT_ENVIRONMENT_NAME))
1252 .is_none()
1253 );
1254 assert!(resource.get(&opentelemetry::Key::new(HOST_NAME)).is_some());
1256 assert!(
1257 resource
1258 .get(&opentelemetry::Key::new(PROCESS_PID))
1259 .is_some()
1260 );
1261 }
1262
1263 #[test]
1264 fn trace_sampler_ratio_converts_to_sdk() {
1265 let sampler = TraceSampler::TraceIdRatio(0.5);
1266 let sdk = sampler.into_sdk_sampler();
1267 assert_eq!(format!("{sdk:?}"), "TraceIdRatioBased(0.5)");
1268 }
1269
1270 #[test]
1271 fn trace_sampler_parent_based_converts_to_sdk() {
1272 let sampler = TraceSampler::ParentBased(Box::new(TraceSampler::TraceIdRatio(0.25)));
1273 let sdk = sampler.into_sdk_sampler();
1274 let debug = format!("{sdk:?}");
1275 assert!(debug.contains("ParentBased"));
1276 assert!(debug.contains("0.25"));
1277 }
1278
1279 unsafe fn set_env(key: &str, val: &str) {
1281 unsafe {
1282 std::env::set_var(key, val);
1283 }
1284 }
1285
1286 unsafe fn remove_env(key: &str) {
1287 unsafe {
1288 std::env::remove_var(key);
1289 }
1290 }
1291
1292 #[test]
1293 fn sampler_from_env_reads_traceidratio() {
1294 let _lock = ENV_LOCK.lock().unwrap();
1295 unsafe {
1296 set_env("OTEL_TRACES_SAMPLER", "traceidratio");
1297 set_env("OTEL_TRACES_SAMPLER_ARG", "0.42");
1298 }
1299
1300 let sampler = sampler_from_env()
1301 .expect("should not error")
1302 .expect("should return Some");
1303 assert!(
1304 matches!(sampler, TraceSampler::TraceIdRatio(r) if (r - 0.42).abs() < f64::EPSILON)
1305 );
1306
1307 unsafe {
1308 remove_env("OTEL_TRACES_SAMPLER");
1309 remove_env("OTEL_TRACES_SAMPLER_ARG");
1310 }
1311 }
1312
1313 #[test]
1314 fn sampler_from_env_returns_none_when_unset() {
1315 let _lock = ENV_LOCK.lock().unwrap();
1316 unsafe {
1317 remove_env("OTEL_TRACES_SAMPLER");
1318 }
1319 assert!(sampler_from_env().expect("should not error").is_none());
1320 }
1321
1322 #[test]
1323 fn sampler_from_env_reads_parentbased_traceidratio() {
1324 let _lock = ENV_LOCK.lock().unwrap();
1325 unsafe {
1326 set_env("OTEL_TRACES_SAMPLER", "parentbased_traceidratio");
1327 set_env("OTEL_TRACES_SAMPLER_ARG", "0.1");
1328 }
1329
1330 let sampler = sampler_from_env()
1331 .expect("should not error")
1332 .expect("should return Some");
1333 assert!(
1334 matches!(sampler, TraceSampler::ParentBased(inner) if matches!(*inner, TraceSampler::TraceIdRatio(r) if (r - 0.1).abs() < f64::EPSILON))
1335 );
1336
1337 unsafe {
1338 remove_env("OTEL_TRACES_SAMPLER");
1339 remove_env("OTEL_TRACES_SAMPLER_ARG");
1340 }
1341 }
1342
1343 #[test]
1344 fn sampler_from_env_parentbased_always_on() {
1345 let _lock = ENV_LOCK.lock().unwrap();
1346 unsafe {
1347 set_env("OTEL_TRACES_SAMPLER", "parentbased_always_on");
1348 }
1349 let sampler = sampler_from_env()
1350 .expect("should not error")
1351 .expect("should return Some");
1352 assert!(
1353 matches!(sampler, TraceSampler::ParentBased(inner) if matches!(*inner, TraceSampler::AlwaysOn))
1354 );
1355 unsafe {
1356 remove_env("OTEL_TRACES_SAMPLER");
1357 }
1358 }
1359
1360 #[test]
1361 fn sampler_from_env_parentbased_always_off() {
1362 let _lock = ENV_LOCK.lock().unwrap();
1363 unsafe {
1364 set_env("OTEL_TRACES_SAMPLER", "parentbased_always_off");
1365 }
1366 let sampler = sampler_from_env()
1367 .expect("should not error")
1368 .expect("should return Some");
1369 assert!(
1370 matches!(sampler, TraceSampler::ParentBased(inner) if matches!(*inner, TraceSampler::AlwaysOff))
1371 );
1372 unsafe {
1373 remove_env("OTEL_TRACES_SAMPLER");
1374 }
1375 }
1376
1377 #[test]
1378 fn sampler_from_env_always_on() {
1379 let _lock = ENV_LOCK.lock().unwrap();
1380 unsafe {
1381 set_env("OTEL_TRACES_SAMPLER", "always_on");
1382 }
1383 let sampler = sampler_from_env()
1384 .expect("should not error")
1385 .expect("should return Some");
1386 assert!(matches!(sampler, TraceSampler::AlwaysOn));
1387 unsafe {
1388 remove_env("OTEL_TRACES_SAMPLER");
1389 }
1390 }
1391
1392 #[test]
1393 fn sampler_from_env_always_off() {
1394 let _lock = ENV_LOCK.lock().unwrap();
1395 unsafe {
1396 set_env("OTEL_TRACES_SAMPLER", "always_off");
1397 }
1398 let sampler = sampler_from_env()
1399 .expect("should not error")
1400 .expect("should return Some");
1401 assert!(matches!(sampler, TraceSampler::AlwaysOff));
1402 unsafe {
1403 remove_env("OTEL_TRACES_SAMPLER");
1404 }
1405 }
1406
1407 #[test]
1408 fn sampler_from_env_unknown_returns_error() {
1409 let _lock = ENV_LOCK.lock().unwrap();
1410 unsafe {
1411 set_env("OTEL_TRACES_SAMPLER", "unknown_sampler");
1412 }
1413 let err = sampler_from_env().expect_err("unknown sampler should produce an error");
1414 assert!(
1415 err.to_string().contains("unknown_sampler"),
1416 "error message should include the unknown name, got: {err}"
1417 );
1418 unsafe {
1419 remove_env("OTEL_TRACES_SAMPLER");
1420 }
1421 }
1422
1423 #[test]
1424 fn trace_sampler_always_on_converts_to_sdk() {
1425 let sdk = TraceSampler::AlwaysOn.into_sdk_sampler();
1426 assert_eq!(format!("{sdk:?}"), "AlwaysOn");
1427 }
1428
1429 #[test]
1430 fn trace_sampler_always_off_converts_to_sdk() {
1431 let sdk = TraceSampler::AlwaysOff.into_sdk_sampler();
1432 assert_eq!(format!("{sdk:?}"), "AlwaysOff");
1433 }
1434
1435 #[test]
1436 fn builder_has_sensible_defaults() {
1437 let builder = Telemetry::builder("test-svc");
1438 assert_eq!(builder.service_name.as_deref(), Some("test-svc"));
1439 assert!(builder.service_version.is_none());
1440 assert!(builder.deployment_environment.is_none());
1441 assert!(builder.sampler.is_none());
1442 assert!(builder.metrics);
1443 assert!(!builder.logs);
1444 assert!(builder.protocol.is_none());
1445 assert!(builder.max_export_batch_size.is_none());
1446 assert!(builder.metric_export_interval.is_none());
1447 assert!(builder.export_timeout.is_none());
1448 }
1449
1450 #[test]
1451 fn from_env_builder_has_no_service_name() {
1452 let builder = Telemetry::from_env();
1453 assert!(builder.service_name.is_none());
1454 }
1455
1456 #[test]
1457 fn with_export_timeout_stores_value() {
1458 let timeout = Duration::from_secs(5);
1459 let builder = Telemetry::builder("test-svc").with_export_timeout(timeout);
1460 assert_eq!(builder.export_timeout, Some(timeout));
1461 }
1462
1463 #[test]
1464 fn timeout_from_env_reads_milliseconds() {
1465 let _lock = ENV_LOCK.lock().unwrap();
1466 unsafe {
1467 set_env("OTEL_EXPORTER_OTLP_TIMEOUT", "5000");
1468 }
1469 let t = timeout_from_env();
1470 assert_eq!(t, Some(Duration::from_millis(5000)));
1471 unsafe {
1472 remove_env("OTEL_EXPORTER_OTLP_TIMEOUT");
1473 }
1474 }
1475
1476 #[test]
1477 fn timeout_from_env_returns_none_when_unset() {
1478 let _lock = ENV_LOCK.lock().unwrap();
1479 unsafe {
1480 remove_env("OTEL_EXPORTER_OTLP_TIMEOUT");
1481 }
1482 assert_eq!(timeout_from_env(), None);
1483 }
1484
1485 #[test]
1486 fn service_name_from_env_used_when_none_given() {
1487 let builder = Telemetry::from_env();
1488 assert!(builder.service_name.is_none());
1489 }
1490
1491 #[test]
1492 fn explicit_service_name_overrides_env_var() {
1493 let builder = Telemetry::builder("explicit-svc");
1494 assert_eq!(builder.service_name.as_deref(), Some("explicit-svc"));
1495 }
1496
1497 #[test]
1498 fn from_env_builder_service_name_is_none() {
1499 let builder = Telemetry::from_env();
1500 assert!(builder.service_name.is_none());
1501 }
1502
1503 #[test]
1504 fn init_returns_error_for_unknown_otel_traces_sampler() {
1505 let _lock = ENV_LOCK.lock().unwrap();
1506 unsafe {
1507 set_env("OTEL_TRACES_SAMPLER", "not_a_real_sampler");
1508 }
1509 let result = Telemetry::builder("test-svc").with_metrics(false).init();
1510 let err = result
1511 .err()
1512 .expect("unknown sampler env var should cause init to fail");
1513 assert!(
1514 err.to_string().contains("not_a_real_sampler"),
1515 "error should name the unknown sampler, got: {err}"
1516 );
1517 unsafe {
1518 remove_env("OTEL_TRACES_SAMPLER");
1519 }
1520 }
1521
1522 #[test]
1523 fn with_max_export_batch_size_stores_value() {
1524 let builder = Telemetry::builder("test-svc").with_max_export_batch_size(1024);
1525 assert_eq!(builder.max_export_batch_size, Some(1024));
1526 }
1527
1528 #[test]
1529 fn with_metric_export_interval_stores_value() {
1530 let interval = Duration::from_secs(30);
1531 let builder = Telemetry::builder("test-svc").with_metric_export_interval(interval);
1532 assert_eq!(builder.metric_export_interval, Some(interval));
1533 }
1534
1535 #[test]
1536 fn init_rejects_zero_metric_export_interval() {
1537 let err = Telemetry::builder("test-svc")
1538 .with_metric_export_interval(Duration::ZERO)
1539 .with_metrics(false)
1540 .init()
1541 .err()
1542 .expect("expected error for zero interval");
1543 assert!(
1544 err.to_string().contains("metric_export_interval"),
1545 "error message should mention metric_export_interval, got: {err}"
1546 );
1547 }
1548
1549 #[test]
1550 fn builder_with_custom_values() {
1551 let builder = Telemetry::builder("test-svc")
1552 .with_version("2.0.0")
1553 .with_environment("production")
1554 .with_sampler(TraceSampler::TraceIdRatio(0.5))
1555 .with_metrics(false);
1556
1557 assert_eq!(builder.service_name.as_deref(), Some("test-svc"));
1558 assert_eq!(builder.service_version.as_deref(), Some("2.0.0"));
1559 assert_eq!(
1560 builder.deployment_environment.as_deref(),
1561 Some("production")
1562 );
1563 assert!(
1564 matches!(builder.sampler, Some(TraceSampler::TraceIdRatio(r)) if (r - 0.5).abs() < f64::EPSILON)
1565 );
1566 assert!(!builder.metrics);
1567 }
1568
1569 #[test]
1570 #[cfg(feature = "grpc")]
1571 fn builder_with_protocol_grpc() {
1572 let builder = Telemetry::builder("test-svc").with_protocol(ExportProtocol::Grpc);
1573 assert_eq!(builder.protocol, Some(ExportProtocol::Grpc));
1574 }
1575
1576 #[test]
1577 #[cfg(feature = "http")]
1578 fn builder_with_protocol_http() {
1579 let builder = Telemetry::builder("test-svc").with_protocol(ExportProtocol::HttpProtobuf);
1580 assert_eq!(builder.protocol, Some(ExportProtocol::HttpProtobuf));
1581 }
1582
1583 #[test]
1584 #[cfg(feature = "grpc")]
1585 fn protocol_from_env_reads_grpc() {
1586 let _lock = ENV_LOCK.lock().unwrap();
1587 unsafe {
1588 set_env("OTEL_EXPORTER_OTLP_PROTOCOL", "grpc");
1589 }
1590 assert_eq!(protocol_from_env(), Some(ExportProtocol::Grpc));
1591 unsafe {
1592 remove_env("OTEL_EXPORTER_OTLP_PROTOCOL");
1593 }
1594 }
1595
1596 #[test]
1597 #[cfg(feature = "http")]
1598 fn protocol_from_env_reads_http_protobuf() {
1599 let _lock = ENV_LOCK.lock().unwrap();
1600 unsafe {
1601 set_env("OTEL_EXPORTER_OTLP_PROTOCOL", "http/protobuf");
1602 }
1603 assert_eq!(protocol_from_env(), Some(ExportProtocol::HttpProtobuf));
1604 unsafe {
1605 remove_env("OTEL_EXPORTER_OTLP_PROTOCOL");
1606 }
1607 }
1608
1609 #[test]
1610 fn protocol_from_env_returns_none_when_unset() {
1611 let _lock = ENV_LOCK.lock().unwrap();
1612 unsafe {
1613 remove_env("OTEL_EXPORTER_OTLP_PROTOCOL");
1614 }
1615 assert_eq!(protocol_from_env(), None);
1616 }
1617
1618 #[test]
1619 fn protocol_from_env_returns_none_for_unknown() {
1620 let _lock = ENV_LOCK.lock().unwrap();
1621 unsafe {
1622 set_env("OTEL_EXPORTER_OTLP_PROTOCOL", "websocket");
1623 }
1624 assert_eq!(protocol_from_env(), None);
1625 unsafe {
1626 remove_env("OTEL_EXPORTER_OTLP_PROTOCOL");
1627 }
1628 }
1629
1630 #[test]
1631 fn builder_is_send_and_sync() {
1632 fn assert_send_sync<T: Send + Sync>() {}
1633 assert_send_sync::<TelemetryBuilder>();
1634 }
1635
1636 #[test]
1637 fn with_shutdown_timeout_stores_value() {
1638 let timeout = Duration::from_secs(10);
1639 let builder = Telemetry::builder("test-svc").with_shutdown_timeout(timeout);
1640 assert_eq!(builder.shutdown_timeout, timeout);
1641 }
1642
1643 #[test]
1644 fn default_shutdown_timeout_is_five_seconds() {
1645 let builder = Telemetry::builder("test-svc");
1646 assert_eq!(builder.shutdown_timeout, Duration::from_secs(5));
1647 }
1648
1649 #[cfg(feature = "testing")]
1657 #[test]
1658 fn drop_completes_within_shutdown_timeout() {
1659 let mut handles = crate::Telemetry::testing("drop-timeout-test");
1661 handles.shutdown_timeout = Duration::from_millis(100);
1663
1664 let start = std::time::Instant::now();
1665 drop(handles);
1666 let elapsed = start.elapsed();
1667
1668 assert!(
1670 elapsed < Duration::from_millis(500),
1671 "drop took {elapsed:?}, expected < 500 ms"
1672 );
1673 }
1674}