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, SdkTracer, 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
70fn tracing_bridge_tracer(provider: &SdkTracerProvider) -> SdkTracer {
71 use opentelemetry::trace::TracerProvider as _;
72
73 provider.tracer(env!("CARGO_PKG_NAME"))
74}
75
76#[derive(Debug, Clone)]
91pub enum TraceSampler {
92 AlwaysOn,
94 AlwaysOff,
96 TraceIdRatio(f64),
98 ParentBased(Box<TraceSampler>),
101}
102
103impl TraceSampler {
104 fn into_sdk_sampler(self) -> Sampler {
106 match self {
107 TraceSampler::AlwaysOn => Sampler::AlwaysOn,
108 TraceSampler::AlwaysOff => Sampler::AlwaysOff,
109 TraceSampler::TraceIdRatio(r) => Sampler::TraceIdRatioBased(r),
110 TraceSampler::ParentBased(inner) => {
111 Sampler::ParentBased(Box::new(inner.into_sdk_sampler()))
112 }
113 }
114 }
115}
116
117fn sampler_from_env() -> Result<Option<TraceSampler>, Box<dyn Error>> {
125 let name = match std::env::var("OTEL_TRACES_SAMPLER") {
126 Ok(v) => v,
127 Err(_) => return Ok(None),
128 };
129 let arg = std::env::var("OTEL_TRACES_SAMPLER_ARG").ok();
130 let sampler = match name.as_str() {
131 "always_on" => TraceSampler::AlwaysOn,
132 "always_off" => TraceSampler::AlwaysOff,
133 "traceidratio" => {
134 let ratio = arg
135 .as_deref()
136 .unwrap_or("1.0")
137 .parse::<f64>()
138 .unwrap_or(1.0);
139 TraceSampler::TraceIdRatio(ratio)
140 }
141 "parentbased_always_on" => TraceSampler::ParentBased(Box::new(TraceSampler::AlwaysOn)),
142 "parentbased_always_off" => TraceSampler::ParentBased(Box::new(TraceSampler::AlwaysOff)),
143 "parentbased_traceidratio" => {
144 let ratio = arg
145 .as_deref()
146 .unwrap_or("1.0")
147 .parse::<f64>()
148 .unwrap_or(1.0);
149 TraceSampler::ParentBased(Box::new(TraceSampler::TraceIdRatio(ratio)))
150 }
151 unknown => {
152 return Err(format!(
153 "OTEL_TRACES_SAMPLER: unrecognised sampler name '{unknown}'. \
154 Valid values: always_on, always_off, traceidratio, \
155 parentbased_always_on, parentbased_always_off, parentbased_traceidratio"
156 )
157 .into());
158 }
159 };
160 Ok(Some(sampler))
161}
162
163const DEFAULT_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5);
165
166pub struct TelemetryHandles {
187 pub tracer_provider: SdkTracerProvider,
188 pub meter_provider: Option<SdkMeterProvider>,
189 pub logger_provider: Option<SdkLoggerProvider>,
190 shutdown_timeout: Duration,
191 #[cfg(feature = "profiling")]
192 pub profiling_handle: Option<profiling::ProfilingHandle>,
193}
194
195impl TelemetryHandles {
196 pub fn shutdown(&self) -> Result<(), Box<dyn Error>> {
209 self.tracer_provider.shutdown()?;
210 if let Some(mp) = &self.meter_provider {
211 mp.shutdown()?;
212 }
213 if let Some(lp) = &self.logger_provider {
214 lp.shutdown()?;
215 }
216 Ok(())
217 }
218}
219
220impl Drop for TelemetryHandles {
221 fn drop(&mut self) {
222 let tracer_provider = self.tracer_provider.clone();
223 let meter_provider = self.meter_provider.clone();
224 let logger_provider = self.logger_provider.clone();
225 let timeout = self.shutdown_timeout;
226
227 let (tx, rx) = std::sync::mpsc::channel();
228 std::thread::spawn(move || {
229 if let Err(e) = tracer_provider.shutdown() {
230 tracing::warn!("tracer provider shutdown error: {e}");
231 }
232 if let Some(mp) = meter_provider
233 && let Err(e) = mp.shutdown()
234 {
235 tracing::warn!("meter provider shutdown error: {e}");
236 }
237 if let Some(lp) = logger_provider
238 && let Err(e) = lp.shutdown()
239 {
240 tracing::warn!("logger provider shutdown error: {e}");
241 }
242 let _ = tx.send(());
243 });
244
245 if rx.recv_timeout(timeout).is_err() {
246 tracing::warn!(
247 "telemetry shutdown did not complete within {timeout:?}; \
248 some spans/metrics may not have been exported"
249 );
250 }
251 }
252}
253
254#[derive(Debug, Clone, Copy, PartialEq, Eq)]
277pub enum ExportProtocol {
278 #[cfg(feature = "grpc")]
280 Grpc,
281 #[cfg(feature = "http")]
283 HttpProtobuf,
284}
285
286#[cfg(feature = "grpc-mtls")]
296#[derive(Clone)]
297pub struct MtlsMaterial {
298 pub client_cert_chain_pem: Vec<u8>,
300 pub client_key_pem: Vec<u8>,
302 pub trust_bundle_pem: Vec<u8>,
304}
305
306#[cfg(feature = "grpc-mtls")]
307impl std::fmt::Debug for MtlsMaterial {
308 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
309 f.debug_struct("MtlsMaterial")
310 .field("client_cert_chain_pem", &"<redacted>")
311 .field("client_key_pem", &"<redacted>")
312 .field("trust_bundle_pem", &"<redacted>")
313 .finish()
314 }
315}
316
317fn protocol_from_env() -> Option<ExportProtocol> {
319 let val = std::env::var("OTEL_EXPORTER_OTLP_PROTOCOL").ok()?;
320 match val.trim() {
321 #[cfg(feature = "grpc")]
322 "grpc" => Some(ExportProtocol::Grpc),
323 #[cfg(feature = "http")]
324 "http/protobuf" => Some(ExportProtocol::HttpProtobuf),
325 _ => None,
326 }
327}
328
329pub struct Telemetry;
345
346impl Telemetry {
347 pub fn builder(service_name: &str) -> TelemetryBuilder {
351 TelemetryBuilder {
352 service_name: Some(service_name.to_string()),
353 service_version: None,
354 deployment_environment: None,
355 sampler: None,
356 metrics: true,
357 logs: false,
358 protocol: None,
359 max_export_batch_size: None,
360 metric_export_interval: None,
361 export_timeout: None,
362 shutdown_timeout: DEFAULT_SHUTDOWN_TIMEOUT,
363 extra_layers: Vec::new(),
364 extra_metric_readers: Vec::new(),
365 #[cfg(feature = "grpc-mtls")]
366 mtls: None,
367 propagated_span_fields: crate::log_bridge::PROPAGATED_SPAN_FIELDS,
368 #[cfg(feature = "profiling")]
369 pyroscope_endpoint: None,
370 }
371 }
372
373 pub fn from_env() -> TelemetryBuilder {
383 TelemetryBuilder {
384 service_name: None,
385 service_version: None,
386 deployment_environment: None,
387 sampler: None,
388 metrics: true,
389 logs: false,
390 protocol: None,
391 max_export_batch_size: None,
392 metric_export_interval: None,
393 export_timeout: None,
394 shutdown_timeout: DEFAULT_SHUTDOWN_TIMEOUT,
395 extra_layers: Vec::new(),
396 extra_metric_readers: Vec::new(),
397 #[cfg(feature = "grpc-mtls")]
398 mtls: None,
399 propagated_span_fields: crate::log_bridge::PROPAGATED_SPAN_FIELDS,
400 #[cfg(feature = "profiling")]
401 pyroscope_endpoint: None,
402 }
403 }
404}
405
406#[must_use = "a TelemetryBuilder does nothing until .init() is called"]
424pub struct TelemetryBuilder {
425 service_name: Option<String>,
426 service_version: Option<String>,
427 deployment_environment: Option<String>,
428 sampler: Option<TraceSampler>,
429 metrics: bool,
430 logs: bool,
431 protocol: Option<ExportProtocol>,
432 max_export_batch_size: Option<usize>,
433 metric_export_interval: Option<Duration>,
434 export_timeout: Option<Duration>,
435 shutdown_timeout: Duration,
436 extra_layers: Vec<
437 Box<dyn tracing_subscriber::Layer<tracing_subscriber::Registry> + Send + Sync + 'static>,
438 >,
439 extra_metric_readers: Vec<MeterProviderInstaller>,
440 #[cfg(feature = "grpc-mtls")]
441 mtls: Option<MtlsMaterial>,
442 propagated_span_fields: &'static [&'static str],
443 #[cfg(feature = "profiling")]
444 pyroscope_endpoint: Option<String>,
445}
446
447type MeterProviderInstaller =
452 Box<dyn FnOnce(MeterProviderBuilder) -> MeterProviderBuilder + Send + Sync>;
453
454impl TelemetryBuilder {
455 pub fn with_version(mut self, version: &str) -> Self {
457 self.service_version = Some(version.to_string());
458 self
459 }
460
461 pub fn with_environment(mut self, environment: &str) -> Self {
463 self.deployment_environment = Some(environment.to_string());
464 self
465 }
466
467 #[cfg(feature = "grpc-mtls")]
492 pub fn with_mtls(mut self, material: MtlsMaterial) -> Self {
493 self.mtls = Some(material);
494 self.protocol = Some(ExportProtocol::Grpc);
495 self
496 }
497
498 pub fn with_sampler(mut self, sampler: TraceSampler) -> Self {
501 self.sampler = Some(sampler);
502 self
503 }
504
505 pub fn with_metrics(mut self, enabled: bool) -> Self {
507 self.metrics = enabled;
508 self
509 }
510
511 pub fn with_protocol(mut self, protocol: ExportProtocol) -> Self {
515 self.protocol = Some(protocol);
516 self
517 }
518
519 pub fn with_max_export_batch_size(mut self, size: usize) -> Self {
524 self.max_export_batch_size = Some(size);
525 self
526 }
527
528 pub fn with_metric_export_interval(mut self, interval: Duration) -> Self {
533 self.metric_export_interval = Some(interval);
534 self
535 }
536
537 pub fn with_logs(mut self, enabled: bool) -> Self {
544 self.logs = enabled;
545 self
546 }
547
548 pub fn with_propagated_span_fields(mut self, fields: &'static [&'static str]) -> Self {
561 self.propagated_span_fields = fields;
562 self
563 }
564
565 pub fn with_export_timeout(mut self, timeout: Duration) -> Self {
569 self.export_timeout = Some(timeout);
570 self
571 }
572
573 pub fn with_shutdown_timeout(mut self, timeout: Duration) -> Self {
580 self.shutdown_timeout = timeout;
581 self
582 }
583
584 #[cfg(feature = "profiling")]
602 pub fn with_profiling(mut self, endpoint: &str) -> Self {
603 self.pyroscope_endpoint = Some(endpoint.to_string());
604 self
605 }
606
607 pub fn with_meter_provider_setup<F>(mut self, setup: F) -> Self
664 where
665 F: FnOnce(MeterProviderBuilder) -> MeterProviderBuilder + Send + Sync + 'static,
666 {
667 self.extra_metric_readers.push(Box::new(setup));
668 self
669 }
670
671 pub fn with_layer<L>(mut self, layer: L) -> Self
672 where
673 L: tracing_subscriber::Layer<tracing_subscriber::Registry> + Send + Sync + 'static,
674 {
675 self.extra_layers.push(Box::new(layer));
676 self
677 }
678
679 pub fn init(self) -> Result<TelemetryHandles, Box<dyn Error>> {
694 if let Some(interval) = self.metric_export_interval
695 && interval.is_zero()
696 {
697 return Err("metric_export_interval must be greater than zero".into());
698 }
699
700 let protocol = self.protocol.or_else(protocol_from_env).unwrap_or({
701 #[cfg(feature = "grpc")]
702 {
703 ExportProtocol::Grpc
704 }
705 #[cfg(all(not(feature = "grpc"), feature = "http"))]
706 {
707 ExportProtocol::HttpProtobuf
708 }
709 });
710
711 let default_endpoint = match protocol {
712 #[cfg(feature = "grpc")]
713 ExportProtocol::Grpc => "http://localhost:4317",
714 #[cfg(feature = "http")]
715 ExportProtocol::HttpProtobuf => "http://localhost:4318",
716 };
717 let endpoint = std::env::var("OTEL_EXPORTER_OTLP_ENDPOINT")
718 .unwrap_or_else(|_| default_endpoint.to_string());
719
720 let export_timeout = self.export_timeout.or_else(timeout_from_env);
722
723 let service_name = self.service_name.unwrap_or_else(|| {
725 std::env::var("OTEL_SERVICE_NAME").unwrap_or_else(|_| "unknown_service".to_string())
726 });
727
728 let resource = build_resource(
729 &service_name,
730 self.service_version.as_deref(),
731 self.deployment_environment.as_deref(),
732 );
733
734 let sampler = match self.sampler {
735 Some(s) => s,
736 None => sampler_from_env()?.unwrap_or(TraceSampler::AlwaysOn),
737 };
738
739 let trace_exporter = build_span_exporter(
741 protocol,
742 &endpoint,
743 export_timeout,
744 #[cfg(feature = "grpc-mtls")]
745 self.mtls.as_ref(),
746 )?;
747
748 let batch_processor = if let Some(size) = self.max_export_batch_size {
749 BatchSpanProcessor::builder(trace_exporter)
750 .with_batch_config(
751 BatchConfigBuilder::default()
752 .with_max_export_batch_size(size)
753 .build(),
754 )
755 .build()
756 } else {
757 BatchSpanProcessor::builder(trace_exporter).build()
758 };
759
760 let tracer_provider = SdkTracerProvider::builder()
761 .with_resource(resource.clone())
762 .with_sampler(sampler.into_sdk_sampler())
763 .with_span_processor(batch_processor)
764 .build();
765
766 opentelemetry::global::set_tracer_provider(tracer_provider.clone());
767
768 let propagator = TextMapCompositePropagator::new(vec![
770 Box::new(TraceContextPropagator::new()),
771 Box::new(BaggagePropagator::new()),
772 ]);
773 opentelemetry::global::set_text_map_propagator(propagator);
774
775 let meter_provider = if self.metrics {
777 let metric_exporter = build_metric_exporter(
778 protocol,
779 &endpoint,
780 export_timeout,
781 #[cfg(feature = "grpc-mtls")]
782 self.mtls.as_ref(),
783 )?;
784
785 let periodic_reader = if let Some(interval) = self.metric_export_interval {
786 PeriodicReader::builder(metric_exporter)
787 .with_interval(interval)
788 .build()
789 } else {
790 PeriodicReader::builder(metric_exporter).build()
791 };
792
793 let mut mp_builder = SdkMeterProvider::builder()
794 .with_resource(resource.clone())
795 .with_reader(periodic_reader);
796 for installer in self.extra_metric_readers {
797 mp_builder = installer(mp_builder);
798 }
799 let mp = mp_builder.build();
800
801 opentelemetry::global::set_meter_provider(mp.clone());
802
803 Some(mp)
804 } else {
805 None
806 };
807
808 let logger_provider = if self.logs {
810 let log_exporter = build_log_exporter(
811 protocol,
812 &endpoint,
813 export_timeout,
814 #[cfg(feature = "grpc-mtls")]
815 self.mtls.as_ref(),
816 )?;
817
818 let lp = SdkLoggerProvider::builder()
819 .with_resource(resource)
820 .with_batch_exporter(log_exporter)
821 .build();
822
823 Some(lp)
824 } else {
825 None
826 };
827
828 #[cfg(feature = "profiling")]
830 let profiling_handle = if let Some(ref endpoint) = self.pyroscope_endpoint {
831 profiling::start_pyroscope_bridge(&service_name, endpoint)?
832 } else {
833 None
834 };
835 #[cfg(not(feature = "profiling"))]
836 let _profiling_handle: Option<()> = None;
837
838 let otel_layer =
843 tracing_opentelemetry::layer().with_tracer(tracing_bridge_tracer(&tracer_provider));
844
845 let extra = if self.extra_layers.is_empty() {
850 None
851 } else {
852 Some(self.extra_layers)
853 };
854
855 let registry = tracing_subscriber::registry()
856 .with(extra)
857 .with(tracing_subscriber::EnvFilter::from_default_env())
858 .with(tracing_subscriber::fmt::layer())
859 .with(otel_layer);
860
861 #[cfg(feature = "profiling-bridge-pyroscope-rs")]
862 let registry = registry.with(crate::profiling::ProfilingTagLayer);
863
864 if let Some(lp) = &logger_provider {
865 if let Err(e) = registry
866 .with(crate::log_bridge::SpanAwareLogBridge::new(
867 lp,
868 self.propagated_span_fields,
869 ))
870 .try_init()
871 {
872 eprintln!(
873 "otel-bootstrap: global tracing subscriber already installed — \
874 OTLP log records will NOT be exported to the collector: {e}"
875 );
876 }
877 } else if let Err(e) = registry.try_init() {
878 eprintln!(
879 "otel-bootstrap: global tracing subscriber already installed — \
880 OTLP telemetry will NOT be exported to the collector: {e}"
881 );
882 }
883
884 Ok(TelemetryHandles {
885 tracer_provider,
886 meter_provider,
887 logger_provider,
888 shutdown_timeout: self.shutdown_timeout,
889 #[cfg(feature = "profiling")]
890 profiling_handle,
891 })
892 }
893}
894
895pub fn init_telemetry(service_name: &str) -> Result<TelemetryHandles, Box<dyn Error>> {
909 Telemetry::builder(service_name).init()
910}
911
912pub fn init_telemetry_with_sampler(
929 service_name: &str,
930 sampler: Option<TraceSampler>,
931) -> Result<TelemetryHandles, Box<dyn Error>> {
932 let builder = Telemetry::builder(service_name);
933 match sampler {
934 Some(s) => builder.with_sampler(s),
935 None => builder, }
937 .init()
938}
939
940fn timeout_from_env() -> Option<Duration> {
942 let ms = std::env::var("OTEL_EXPORTER_OTLP_TIMEOUT").ok()?;
943 let ms: u64 = ms.trim().parse().ok()?;
944 Some(Duration::from_millis(ms))
945}
946
947#[cfg(feature = "grpc-mtls")]
954fn build_tls_config(material: &MtlsMaterial) -> tonic::transport::ClientTlsConfig {
955 use tonic::transport::{Certificate, ClientTlsConfig, Identity};
956 ClientTlsConfig::new()
957 .ca_certificate(Certificate::from_pem(&material.trust_bundle_pem))
958 .identity(Identity::from_pem(
959 &material.client_cert_chain_pem,
960 &material.client_key_pem,
961 ))
962}
963
964fn build_span_exporter(
965 protocol: ExportProtocol,
966 endpoint: &str,
967 timeout: Option<Duration>,
968 #[cfg(feature = "grpc-mtls")] mtls: Option<&MtlsMaterial>,
969) -> Result<opentelemetry_otlp::SpanExporter, Box<dyn Error>> {
970 match protocol {
971 #[cfg(feature = "grpc")]
972 ExportProtocol::Grpc => {
973 let mut b = opentelemetry_otlp::SpanExporter::builder()
974 .with_tonic()
975 .with_endpoint(endpoint);
976 if let Some(t) = timeout {
977 b = b.with_timeout(t);
978 }
979 #[cfg(feature = "grpc-mtls")]
980 if let Some(m) = mtls {
981 use opentelemetry_otlp::WithTonicConfig as _;
982 b = b.with_tls_config(build_tls_config(m));
983 }
984 Ok(b.build()?)
985 }
986 #[cfg(feature = "http")]
987 ExportProtocol::HttpProtobuf => {
988 let mut b = opentelemetry_otlp::SpanExporter::builder()
989 .with_http()
990 .with_endpoint(endpoint);
991 if let Some(t) = timeout {
992 b = b.with_timeout(t);
993 }
994 Ok(b.build()?)
995 }
996 }
997}
998
999fn build_metric_exporter(
1000 protocol: ExportProtocol,
1001 endpoint: &str,
1002 timeout: Option<Duration>,
1003 #[cfg(feature = "grpc-mtls")] mtls: Option<&MtlsMaterial>,
1004) -> Result<opentelemetry_otlp::MetricExporter, Box<dyn Error>> {
1005 match protocol {
1006 #[cfg(feature = "grpc")]
1007 ExportProtocol::Grpc => {
1008 let mut b = opentelemetry_otlp::MetricExporter::builder()
1009 .with_tonic()
1010 .with_endpoint(endpoint);
1011 if let Some(t) = timeout {
1012 b = b.with_timeout(t);
1013 }
1014 #[cfg(feature = "grpc-mtls")]
1015 if let Some(m) = mtls {
1016 use opentelemetry_otlp::WithTonicConfig as _;
1017 b = b.with_tls_config(build_tls_config(m));
1018 }
1019 Ok(b.build()?)
1020 }
1021 #[cfg(feature = "http")]
1022 ExportProtocol::HttpProtobuf => {
1023 let mut b = opentelemetry_otlp::MetricExporter::builder()
1024 .with_http()
1025 .with_endpoint(endpoint);
1026 if let Some(t) = timeout {
1027 b = b.with_timeout(t);
1028 }
1029 Ok(b.build()?)
1030 }
1031 }
1032}
1033
1034fn build_log_exporter(
1035 protocol: ExportProtocol,
1036 endpoint: &str,
1037 timeout: Option<Duration>,
1038 #[cfg(feature = "grpc-mtls")] mtls: Option<&MtlsMaterial>,
1039) -> Result<opentelemetry_otlp::LogExporter, Box<dyn Error>> {
1040 match protocol {
1041 #[cfg(feature = "grpc")]
1042 ExportProtocol::Grpc => {
1043 let mut b = opentelemetry_otlp::LogExporter::builder()
1044 .with_tonic()
1045 .with_endpoint(endpoint);
1046 if let Some(t) = timeout {
1047 b = b.with_timeout(t);
1048 }
1049 #[cfg(feature = "grpc-mtls")]
1050 if let Some(m) = mtls {
1051 use opentelemetry_otlp::WithTonicConfig as _;
1052 b = b.with_tls_config(build_tls_config(m));
1053 }
1054 Ok(b.build()?)
1055 }
1056 #[cfg(feature = "http")]
1057 ExportProtocol::HttpProtobuf => {
1058 let mut b = opentelemetry_otlp::LogExporter::builder()
1059 .with_http()
1060 .with_endpoint(endpoint);
1061 if let Some(t) = timeout {
1062 b = b.with_timeout(t);
1063 }
1064 Ok(b.build()?)
1065 }
1066 }
1067}
1068
1069pub fn build_resource(
1084 service_name: &str,
1085 service_version: Option<&str>,
1086 deployment_environment: Option<&str>,
1087) -> Resource {
1088 let hostname = hostname::get()
1089 .ok()
1090 .and_then(|h| h.into_string().ok())
1091 .unwrap_or_default();
1092
1093 let mut builder = Resource::builder()
1094 .with_service_name(service_name.to_string())
1095 .with_attributes([
1096 KeyValue::new(HOST_NAME, hostname),
1097 KeyValue::new(PROCESS_PID, std::process::id() as i64),
1098 ]);
1099
1100 if let Some(version) = service_version {
1101 builder = builder.with_attribute(KeyValue::new(SERVICE_VERSION, version.to_string()));
1102 }
1103
1104 if let Some(env) = deployment_environment {
1105 builder =
1106 builder.with_attribute(KeyValue::new(DEPLOYMENT_ENVIRONMENT_NAME, env.to_string()));
1107 }
1108
1109 builder.build()
1110}
1111
1112#[cfg(feature = "axum")]
1130pub fn axum_layer() -> axum_middleware::OtelTraceLayer {
1131 axum_middleware::OtelTraceLayer
1132}
1133
1134#[cfg(feature = "axum")]
1165pub fn span_enricher_layer<T>() -> axum_middleware::SpanEnricherLayer<T>
1166where
1167 T: span_enrichment::EnrichSpan + Clone + Send + Sync + 'static,
1168{
1169 axum_middleware::SpanEnricherLayer::default()
1170}
1171
1172#[cfg(feature = "tonic-tracing")]
1194pub fn grpc_client_layer() -> grpc_middleware::GrpcClientTraceLayer {
1195 grpc_middleware::GrpcClientTraceLayer
1196}
1197
1198#[cfg(feature = "tonic-tracing")]
1213pub fn grpc_server_layer() -> grpc_middleware::GrpcServerTraceLayer {
1214 grpc_middleware::GrpcServerTraceLayer
1215}
1216
1217#[cfg(test)]
1218mod tests {
1219 use super::*;
1220 use opentelemetry::trace::{Span as _, Tracer as _};
1221 use std::sync::Mutex;
1222
1223 static ENV_LOCK: Mutex<()> = Mutex::new(());
1224
1225 #[test]
1226 fn tracing_bridge_uses_sdk_tracer() {
1227 let provider = SdkTracerProvider::builder().build();
1228 let tracer = tracing_bridge_tracer(&provider);
1229 let span = tracer.start("bridge-regression");
1230
1231 assert!(span.span_context().is_valid());
1232
1233 provider.shutdown().expect("provider shutdown");
1234 }
1235
1236 #[test]
1237 fn resource_contains_all_attributes_when_provided() {
1238 let resource = build_resource("test-svc", Some("1.2.3"), Some("staging"));
1239
1240 assert_eq!(
1241 resource.get(&opentelemetry::Key::new("service.name")),
1242 Some(opentelemetry::Value::from("test-svc")),
1243 );
1244 assert_eq!(
1245 resource.get(&opentelemetry::Key::new(SERVICE_VERSION)),
1246 Some(opentelemetry::Value::from("1.2.3")),
1247 );
1248 assert_eq!(
1249 resource.get(&opentelemetry::Key::new(DEPLOYMENT_ENVIRONMENT_NAME)),
1250 Some(opentelemetry::Value::from("staging")),
1251 );
1252 assert!(resource.get(&opentelemetry::Key::new(HOST_NAME)).is_some());
1253 assert!(
1254 resource
1255 .get(&opentelemetry::Key::new(PROCESS_PID))
1256 .is_some()
1257 );
1258 }
1259
1260 #[test]
1261 fn resource_graceful_when_optional_values_omitted() {
1262 let resource = build_resource("test-svc", None, None);
1263
1264 assert_eq!(
1265 resource.get(&opentelemetry::Key::new("service.name")),
1266 Some(opentelemetry::Value::from("test-svc")),
1267 );
1268 assert!(
1269 resource
1270 .get(&opentelemetry::Key::new(SERVICE_VERSION))
1271 .is_none()
1272 );
1273 assert!(
1274 resource
1275 .get(&opentelemetry::Key::new(DEPLOYMENT_ENVIRONMENT_NAME))
1276 .is_none()
1277 );
1278 assert!(resource.get(&opentelemetry::Key::new(HOST_NAME)).is_some());
1280 assert!(
1281 resource
1282 .get(&opentelemetry::Key::new(PROCESS_PID))
1283 .is_some()
1284 );
1285 }
1286
1287 #[test]
1288 fn trace_sampler_ratio_converts_to_sdk() {
1289 let sampler = TraceSampler::TraceIdRatio(0.5);
1290 let sdk = sampler.into_sdk_sampler();
1291 assert_eq!(format!("{sdk:?}"), "TraceIdRatioBased(0.5)");
1292 }
1293
1294 #[test]
1295 fn trace_sampler_parent_based_converts_to_sdk() {
1296 let sampler = TraceSampler::ParentBased(Box::new(TraceSampler::TraceIdRatio(0.25)));
1297 let sdk = sampler.into_sdk_sampler();
1298 let debug = format!("{sdk:?}");
1299 assert!(debug.contains("ParentBased"));
1300 assert!(debug.contains("0.25"));
1301 }
1302
1303 unsafe fn set_env(key: &str, val: &str) {
1305 unsafe {
1306 std::env::set_var(key, val);
1307 }
1308 }
1309
1310 unsafe fn remove_env(key: &str) {
1311 unsafe {
1312 std::env::remove_var(key);
1313 }
1314 }
1315
1316 #[test]
1317 fn sampler_from_env_reads_traceidratio() {
1318 let _lock = ENV_LOCK.lock().unwrap();
1319 unsafe {
1320 set_env("OTEL_TRACES_SAMPLER", "traceidratio");
1321 set_env("OTEL_TRACES_SAMPLER_ARG", "0.42");
1322 }
1323
1324 let sampler = sampler_from_env()
1325 .expect("should not error")
1326 .expect("should return Some");
1327 assert!(
1328 matches!(sampler, TraceSampler::TraceIdRatio(r) if (r - 0.42).abs() < f64::EPSILON)
1329 );
1330
1331 unsafe {
1332 remove_env("OTEL_TRACES_SAMPLER");
1333 remove_env("OTEL_TRACES_SAMPLER_ARG");
1334 }
1335 }
1336
1337 #[test]
1338 fn sampler_from_env_returns_none_when_unset() {
1339 let _lock = ENV_LOCK.lock().unwrap();
1340 unsafe {
1341 remove_env("OTEL_TRACES_SAMPLER");
1342 }
1343 assert!(sampler_from_env().expect("should not error").is_none());
1344 }
1345
1346 #[test]
1347 fn sampler_from_env_reads_parentbased_traceidratio() {
1348 let _lock = ENV_LOCK.lock().unwrap();
1349 unsafe {
1350 set_env("OTEL_TRACES_SAMPLER", "parentbased_traceidratio");
1351 set_env("OTEL_TRACES_SAMPLER_ARG", "0.1");
1352 }
1353
1354 let sampler = sampler_from_env()
1355 .expect("should not error")
1356 .expect("should return Some");
1357 assert!(
1358 matches!(sampler, TraceSampler::ParentBased(inner) if matches!(*inner, TraceSampler::TraceIdRatio(r) if (r - 0.1).abs() < f64::EPSILON))
1359 );
1360
1361 unsafe {
1362 remove_env("OTEL_TRACES_SAMPLER");
1363 remove_env("OTEL_TRACES_SAMPLER_ARG");
1364 }
1365 }
1366
1367 #[test]
1368 fn sampler_from_env_parentbased_always_on() {
1369 let _lock = ENV_LOCK.lock().unwrap();
1370 unsafe {
1371 set_env("OTEL_TRACES_SAMPLER", "parentbased_always_on");
1372 }
1373 let sampler = sampler_from_env()
1374 .expect("should not error")
1375 .expect("should return Some");
1376 assert!(
1377 matches!(sampler, TraceSampler::ParentBased(inner) if matches!(*inner, TraceSampler::AlwaysOn))
1378 );
1379 unsafe {
1380 remove_env("OTEL_TRACES_SAMPLER");
1381 }
1382 }
1383
1384 #[test]
1385 fn sampler_from_env_parentbased_always_off() {
1386 let _lock = ENV_LOCK.lock().unwrap();
1387 unsafe {
1388 set_env("OTEL_TRACES_SAMPLER", "parentbased_always_off");
1389 }
1390 let sampler = sampler_from_env()
1391 .expect("should not error")
1392 .expect("should return Some");
1393 assert!(
1394 matches!(sampler, TraceSampler::ParentBased(inner) if matches!(*inner, TraceSampler::AlwaysOff))
1395 );
1396 unsafe {
1397 remove_env("OTEL_TRACES_SAMPLER");
1398 }
1399 }
1400
1401 #[test]
1402 fn sampler_from_env_always_on() {
1403 let _lock = ENV_LOCK.lock().unwrap();
1404 unsafe {
1405 set_env("OTEL_TRACES_SAMPLER", "always_on");
1406 }
1407 let sampler = sampler_from_env()
1408 .expect("should not error")
1409 .expect("should return Some");
1410 assert!(matches!(sampler, TraceSampler::AlwaysOn));
1411 unsafe {
1412 remove_env("OTEL_TRACES_SAMPLER");
1413 }
1414 }
1415
1416 #[test]
1417 fn sampler_from_env_always_off() {
1418 let _lock = ENV_LOCK.lock().unwrap();
1419 unsafe {
1420 set_env("OTEL_TRACES_SAMPLER", "always_off");
1421 }
1422 let sampler = sampler_from_env()
1423 .expect("should not error")
1424 .expect("should return Some");
1425 assert!(matches!(sampler, TraceSampler::AlwaysOff));
1426 unsafe {
1427 remove_env("OTEL_TRACES_SAMPLER");
1428 }
1429 }
1430
1431 #[test]
1432 fn sampler_from_env_unknown_returns_error() {
1433 let _lock = ENV_LOCK.lock().unwrap();
1434 unsafe {
1435 set_env("OTEL_TRACES_SAMPLER", "unknown_sampler");
1436 }
1437 let err = sampler_from_env().expect_err("unknown sampler should produce an error");
1438 assert!(
1439 err.to_string().contains("unknown_sampler"),
1440 "error message should include the unknown name, got: {err}"
1441 );
1442 unsafe {
1443 remove_env("OTEL_TRACES_SAMPLER");
1444 }
1445 }
1446
1447 #[test]
1448 fn trace_sampler_always_on_converts_to_sdk() {
1449 let sdk = TraceSampler::AlwaysOn.into_sdk_sampler();
1450 assert_eq!(format!("{sdk:?}"), "AlwaysOn");
1451 }
1452
1453 #[test]
1454 fn trace_sampler_always_off_converts_to_sdk() {
1455 let sdk = TraceSampler::AlwaysOff.into_sdk_sampler();
1456 assert_eq!(format!("{sdk:?}"), "AlwaysOff");
1457 }
1458
1459 #[test]
1460 fn builder_has_sensible_defaults() {
1461 let builder = Telemetry::builder("test-svc");
1462 assert_eq!(builder.service_name.as_deref(), Some("test-svc"));
1463 assert!(builder.service_version.is_none());
1464 assert!(builder.deployment_environment.is_none());
1465 assert!(builder.sampler.is_none());
1466 assert!(builder.metrics);
1467 assert!(!builder.logs);
1468 assert!(builder.protocol.is_none());
1469 assert!(builder.max_export_batch_size.is_none());
1470 assert!(builder.metric_export_interval.is_none());
1471 assert!(builder.export_timeout.is_none());
1472 }
1473
1474 #[test]
1475 fn from_env_builder_has_no_service_name() {
1476 let builder = Telemetry::from_env();
1477 assert!(builder.service_name.is_none());
1478 }
1479
1480 #[test]
1481 fn with_export_timeout_stores_value() {
1482 let timeout = Duration::from_secs(5);
1483 let builder = Telemetry::builder("test-svc").with_export_timeout(timeout);
1484 assert_eq!(builder.export_timeout, Some(timeout));
1485 }
1486
1487 #[test]
1488 fn timeout_from_env_reads_milliseconds() {
1489 let _lock = ENV_LOCK.lock().unwrap();
1490 unsafe {
1491 set_env("OTEL_EXPORTER_OTLP_TIMEOUT", "5000");
1492 }
1493 let t = timeout_from_env();
1494 assert_eq!(t, Some(Duration::from_millis(5000)));
1495 unsafe {
1496 remove_env("OTEL_EXPORTER_OTLP_TIMEOUT");
1497 }
1498 }
1499
1500 #[test]
1501 fn timeout_from_env_returns_none_when_unset() {
1502 let _lock = ENV_LOCK.lock().unwrap();
1503 unsafe {
1504 remove_env("OTEL_EXPORTER_OTLP_TIMEOUT");
1505 }
1506 assert_eq!(timeout_from_env(), None);
1507 }
1508
1509 #[test]
1510 fn service_name_from_env_used_when_none_given() {
1511 let builder = Telemetry::from_env();
1512 assert!(builder.service_name.is_none());
1513 }
1514
1515 #[test]
1516 fn explicit_service_name_overrides_env_var() {
1517 let builder = Telemetry::builder("explicit-svc");
1518 assert_eq!(builder.service_name.as_deref(), Some("explicit-svc"));
1519 }
1520
1521 #[test]
1522 fn from_env_builder_service_name_is_none() {
1523 let builder = Telemetry::from_env();
1524 assert!(builder.service_name.is_none());
1525 }
1526
1527 #[test]
1528 fn init_returns_error_for_unknown_otel_traces_sampler() {
1529 let _lock = ENV_LOCK.lock().unwrap();
1530 unsafe {
1531 set_env("OTEL_TRACES_SAMPLER", "not_a_real_sampler");
1532 }
1533 let result = Telemetry::builder("test-svc").with_metrics(false).init();
1534 let err = result
1535 .err()
1536 .expect("unknown sampler env var should cause init to fail");
1537 assert!(
1538 err.to_string().contains("not_a_real_sampler"),
1539 "error should name the unknown sampler, got: {err}"
1540 );
1541 unsafe {
1542 remove_env("OTEL_TRACES_SAMPLER");
1543 }
1544 }
1545
1546 #[test]
1547 fn with_max_export_batch_size_stores_value() {
1548 let builder = Telemetry::builder("test-svc").with_max_export_batch_size(1024);
1549 assert_eq!(builder.max_export_batch_size, Some(1024));
1550 }
1551
1552 #[test]
1553 fn with_metric_export_interval_stores_value() {
1554 let interval = Duration::from_secs(30);
1555 let builder = Telemetry::builder("test-svc").with_metric_export_interval(interval);
1556 assert_eq!(builder.metric_export_interval, Some(interval));
1557 }
1558
1559 #[test]
1560 fn init_rejects_zero_metric_export_interval() {
1561 let err = Telemetry::builder("test-svc")
1562 .with_metric_export_interval(Duration::ZERO)
1563 .with_metrics(false)
1564 .init()
1565 .err()
1566 .expect("expected error for zero interval");
1567 assert!(
1568 err.to_string().contains("metric_export_interval"),
1569 "error message should mention metric_export_interval, got: {err}"
1570 );
1571 }
1572
1573 #[test]
1574 fn builder_with_custom_values() {
1575 let builder = Telemetry::builder("test-svc")
1576 .with_version("2.0.0")
1577 .with_environment("production")
1578 .with_sampler(TraceSampler::TraceIdRatio(0.5))
1579 .with_metrics(false);
1580
1581 assert_eq!(builder.service_name.as_deref(), Some("test-svc"));
1582 assert_eq!(builder.service_version.as_deref(), Some("2.0.0"));
1583 assert_eq!(
1584 builder.deployment_environment.as_deref(),
1585 Some("production")
1586 );
1587 assert!(
1588 matches!(builder.sampler, Some(TraceSampler::TraceIdRatio(r)) if (r - 0.5).abs() < f64::EPSILON)
1589 );
1590 assert!(!builder.metrics);
1591 }
1592
1593 #[test]
1594 #[cfg(feature = "grpc")]
1595 fn builder_with_protocol_grpc() {
1596 let builder = Telemetry::builder("test-svc").with_protocol(ExportProtocol::Grpc);
1597 assert_eq!(builder.protocol, Some(ExportProtocol::Grpc));
1598 }
1599
1600 #[test]
1601 #[cfg(feature = "http")]
1602 fn builder_with_protocol_http() {
1603 let builder = Telemetry::builder("test-svc").with_protocol(ExportProtocol::HttpProtobuf);
1604 assert_eq!(builder.protocol, Some(ExportProtocol::HttpProtobuf));
1605 }
1606
1607 #[test]
1608 #[cfg(feature = "grpc")]
1609 fn protocol_from_env_reads_grpc() {
1610 let _lock = ENV_LOCK.lock().unwrap();
1611 unsafe {
1612 set_env("OTEL_EXPORTER_OTLP_PROTOCOL", "grpc");
1613 }
1614 assert_eq!(protocol_from_env(), Some(ExportProtocol::Grpc));
1615 unsafe {
1616 remove_env("OTEL_EXPORTER_OTLP_PROTOCOL");
1617 }
1618 }
1619
1620 #[test]
1621 #[cfg(feature = "http")]
1622 fn protocol_from_env_reads_http_protobuf() {
1623 let _lock = ENV_LOCK.lock().unwrap();
1624 unsafe {
1625 set_env("OTEL_EXPORTER_OTLP_PROTOCOL", "http/protobuf");
1626 }
1627 assert_eq!(protocol_from_env(), Some(ExportProtocol::HttpProtobuf));
1628 unsafe {
1629 remove_env("OTEL_EXPORTER_OTLP_PROTOCOL");
1630 }
1631 }
1632
1633 #[test]
1634 fn protocol_from_env_returns_none_when_unset() {
1635 let _lock = ENV_LOCK.lock().unwrap();
1636 unsafe {
1637 remove_env("OTEL_EXPORTER_OTLP_PROTOCOL");
1638 }
1639 assert_eq!(protocol_from_env(), None);
1640 }
1641
1642 #[test]
1643 fn protocol_from_env_returns_none_for_unknown() {
1644 let _lock = ENV_LOCK.lock().unwrap();
1645 unsafe {
1646 set_env("OTEL_EXPORTER_OTLP_PROTOCOL", "websocket");
1647 }
1648 assert_eq!(protocol_from_env(), None);
1649 unsafe {
1650 remove_env("OTEL_EXPORTER_OTLP_PROTOCOL");
1651 }
1652 }
1653
1654 #[test]
1655 fn builder_is_send_and_sync() {
1656 fn assert_send_sync<T: Send + Sync>() {}
1657 assert_send_sync::<TelemetryBuilder>();
1658 }
1659
1660 #[test]
1661 fn with_shutdown_timeout_stores_value() {
1662 let timeout = Duration::from_secs(10);
1663 let builder = Telemetry::builder("test-svc").with_shutdown_timeout(timeout);
1664 assert_eq!(builder.shutdown_timeout, timeout);
1665 }
1666
1667 #[test]
1668 fn default_shutdown_timeout_is_five_seconds() {
1669 let builder = Telemetry::builder("test-svc");
1670 assert_eq!(builder.shutdown_timeout, Duration::from_secs(5));
1671 }
1672
1673 #[cfg(feature = "testing")]
1681 #[test]
1682 fn drop_completes_within_shutdown_timeout() {
1683 let mut handles = crate::Telemetry::testing("drop-timeout-test");
1685 handles.shutdown_timeout = Duration::from_millis(100);
1687
1688 let start = std::time::Instant::now();
1689 drop(handles);
1690 let elapsed = start.elapsed();
1691
1692 assert!(
1694 elapsed < Duration::from_millis(500),
1695 "drop took {elapsed:?}, expected < 500 ms"
1696 );
1697 }
1698}