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