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
103#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
105pub enum LogFormat {
106 #[default]
108 Pretty,
109 Json,
111}
112
113impl TraceSampler {
114 fn into_sdk_sampler(self) -> Sampler {
116 match self {
117 TraceSampler::AlwaysOn => Sampler::AlwaysOn,
118 TraceSampler::AlwaysOff => Sampler::AlwaysOff,
119 TraceSampler::TraceIdRatio(r) => Sampler::TraceIdRatioBased(r),
120 TraceSampler::ParentBased(inner) => {
121 Sampler::ParentBased(Box::new(inner.into_sdk_sampler()))
122 }
123 }
124 }
125}
126
127fn sampler_from_env() -> Result<Option<TraceSampler>, Box<dyn Error>> {
135 let name = match std::env::var("OTEL_TRACES_SAMPLER") {
136 Ok(v) => v,
137 Err(_) => return Ok(None),
138 };
139 let arg = std::env::var("OTEL_TRACES_SAMPLER_ARG").ok();
140 let sampler = match name.as_str() {
141 "always_on" => TraceSampler::AlwaysOn,
142 "always_off" => TraceSampler::AlwaysOff,
143 "traceidratio" => {
144 let ratio = arg
145 .as_deref()
146 .unwrap_or("1.0")
147 .parse::<f64>()
148 .unwrap_or(1.0);
149 TraceSampler::TraceIdRatio(ratio)
150 }
151 "parentbased_always_on" => TraceSampler::ParentBased(Box::new(TraceSampler::AlwaysOn)),
152 "parentbased_always_off" => TraceSampler::ParentBased(Box::new(TraceSampler::AlwaysOff)),
153 "parentbased_traceidratio" => {
154 let ratio = arg
155 .as_deref()
156 .unwrap_or("1.0")
157 .parse::<f64>()
158 .unwrap_or(1.0);
159 TraceSampler::ParentBased(Box::new(TraceSampler::TraceIdRatio(ratio)))
160 }
161 unknown => {
162 return Err(format!(
163 "OTEL_TRACES_SAMPLER: unrecognised sampler name '{unknown}'. \
164 Valid values: always_on, always_off, traceidratio, \
165 parentbased_always_on, parentbased_always_off, parentbased_traceidratio"
166 )
167 .into());
168 }
169 };
170 Ok(Some(sampler))
171}
172
173const DEFAULT_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5);
175
176pub struct TelemetryHandles {
197 pub tracer_provider: SdkTracerProvider,
198 pub meter_provider: Option<SdkMeterProvider>,
199 pub logger_provider: Option<SdkLoggerProvider>,
200 shutdown_timeout: Duration,
201 #[cfg(feature = "profiling")]
202 pub profiling_handle: Option<profiling::ProfilingHandle>,
203}
204
205impl TelemetryHandles {
206 pub fn shutdown(&self) -> Result<(), Box<dyn Error>> {
219 self.tracer_provider.shutdown()?;
220 if let Some(mp) = &self.meter_provider {
221 mp.shutdown()?;
222 }
223 if let Some(lp) = &self.logger_provider {
224 lp.shutdown()?;
225 }
226 Ok(())
227 }
228}
229
230impl Drop for TelemetryHandles {
231 fn drop(&mut self) {
232 let tracer_provider = self.tracer_provider.clone();
233 let meter_provider = self.meter_provider.clone();
234 let logger_provider = self.logger_provider.clone();
235 let timeout = self.shutdown_timeout;
236
237 let (tx, rx) = std::sync::mpsc::channel();
238 std::thread::spawn(move || {
239 if let Err(e) = tracer_provider.shutdown() {
240 tracing::warn!("tracer provider shutdown error: {e}");
241 }
242 if let Some(mp) = meter_provider
243 && let Err(e) = mp.shutdown()
244 {
245 tracing::warn!("meter provider shutdown error: {e}");
246 }
247 if let Some(lp) = logger_provider
248 && let Err(e) = lp.shutdown()
249 {
250 tracing::warn!("logger provider shutdown error: {e}");
251 }
252 let _ = tx.send(());
253 });
254
255 if rx.recv_timeout(timeout).is_err() {
256 tracing::warn!(
257 "telemetry shutdown did not complete within {timeout:?}; \
258 some spans/metrics may not have been exported"
259 );
260 }
261 }
262}
263
264#[derive(Debug, Clone, Copy, PartialEq, Eq)]
287pub enum ExportProtocol {
288 #[cfg(feature = "grpc")]
290 Grpc,
291 #[cfg(feature = "http")]
293 HttpProtobuf,
294}
295
296#[cfg(feature = "grpc-mtls")]
306#[derive(Clone)]
307pub struct MtlsMaterial {
308 pub client_cert_chain_pem: Vec<u8>,
310 pub client_key_pem: Vec<u8>,
312 pub trust_bundle_pem: Vec<u8>,
314}
315
316#[cfg(feature = "grpc-mtls")]
317impl std::fmt::Debug for MtlsMaterial {
318 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
319 f.debug_struct("MtlsMaterial")
320 .field("client_cert_chain_pem", &"<redacted>")
321 .field("client_key_pem", &"<redacted>")
322 .field("trust_bundle_pem", &"<redacted>")
323 .finish()
324 }
325}
326
327fn protocol_from_env() -> Option<ExportProtocol> {
329 let val = std::env::var("OTEL_EXPORTER_OTLP_PROTOCOL").ok()?;
330 match val.trim() {
331 #[cfg(feature = "grpc")]
332 "grpc" => Some(ExportProtocol::Grpc),
333 #[cfg(feature = "http")]
334 "http/protobuf" => Some(ExportProtocol::HttpProtobuf),
335 _ => None,
336 }
337}
338
339pub struct Telemetry;
355
356impl Telemetry {
357 pub fn builder(service_name: &str) -> TelemetryBuilder {
361 TelemetryBuilder {
362 service_name: Some(service_name.to_string()),
363 service_version: None,
364 deployment_environment: None,
365 sampler: None,
366 metrics: true,
367 logs: false,
368 protocol: None,
369 max_export_batch_size: None,
370 metric_export_interval: None,
371 export_timeout: None,
372 shutdown_timeout: DEFAULT_SHUTDOWN_TIMEOUT,
373 log_filter: None,
374 log_format: LogFormat::default(),
375 extra_layers: Vec::new(),
376 extra_metric_readers: Vec::new(),
377 #[cfg(feature = "grpc-mtls")]
378 mtls: None,
379 propagated_span_fields: crate::log_bridge::PROPAGATED_SPAN_FIELDS,
380 #[cfg(feature = "profiling")]
381 pyroscope_endpoint: None,
382 }
383 }
384
385 pub fn from_env() -> TelemetryBuilder {
395 TelemetryBuilder {
396 service_name: None,
397 service_version: None,
398 deployment_environment: None,
399 sampler: None,
400 metrics: true,
401 logs: false,
402 protocol: None,
403 max_export_batch_size: None,
404 metric_export_interval: None,
405 export_timeout: None,
406 shutdown_timeout: DEFAULT_SHUTDOWN_TIMEOUT,
407 log_filter: None,
408 log_format: LogFormat::default(),
409 extra_layers: Vec::new(),
410 extra_metric_readers: Vec::new(),
411 #[cfg(feature = "grpc-mtls")]
412 mtls: None,
413 propagated_span_fields: crate::log_bridge::PROPAGATED_SPAN_FIELDS,
414 #[cfg(feature = "profiling")]
415 pyroscope_endpoint: None,
416 }
417 }
418}
419
420#[must_use = "a TelemetryBuilder does nothing until .init() is called"]
438pub struct TelemetryBuilder {
439 service_name: Option<String>,
440 service_version: Option<String>,
441 deployment_environment: Option<String>,
442 sampler: Option<TraceSampler>,
443 metrics: bool,
444 logs: bool,
445 protocol: Option<ExportProtocol>,
446 max_export_batch_size: Option<usize>,
447 metric_export_interval: Option<Duration>,
448 export_timeout: Option<Duration>,
449 shutdown_timeout: Duration,
450 log_filter: Option<String>,
451 log_format: LogFormat,
452 extra_layers: Vec<
453 Box<dyn tracing_subscriber::Layer<tracing_subscriber::Registry> + Send + Sync + 'static>,
454 >,
455 extra_metric_readers: Vec<MeterProviderInstaller>,
456 #[cfg(feature = "grpc-mtls")]
457 mtls: Option<MtlsMaterial>,
458 propagated_span_fields: &'static [&'static str],
459 #[cfg(feature = "profiling")]
460 pyroscope_endpoint: Option<String>,
461}
462
463type MeterProviderInstaller =
468 Box<dyn FnOnce(MeterProviderBuilder) -> MeterProviderBuilder + Send + Sync>;
469
470impl TelemetryBuilder {
471 pub fn with_log_filter(mut self, directive: impl Into<String>) -> Self {
476 self.log_filter = Some(directive.into());
477 self
478 }
479
480 pub fn with_log_format(mut self, format: LogFormat) -> Self {
482 self.log_format = format;
483 self
484 }
485
486 pub fn with_version(mut self, version: &str) -> Self {
488 self.service_version = Some(version.to_string());
489 self
490 }
491
492 pub fn with_environment(mut self, environment: &str) -> Self {
494 self.deployment_environment = Some(environment.to_string());
495 self
496 }
497
498 #[cfg(feature = "grpc-mtls")]
523 pub fn with_mtls(mut self, material: MtlsMaterial) -> Self {
524 self.mtls = Some(material);
525 self.protocol = Some(ExportProtocol::Grpc);
526 self
527 }
528
529 pub fn with_sampler(mut self, sampler: TraceSampler) -> Self {
532 self.sampler = Some(sampler);
533 self
534 }
535
536 pub fn with_metrics(mut self, enabled: bool) -> Self {
538 self.metrics = enabled;
539 self
540 }
541
542 pub fn with_protocol(mut self, protocol: ExportProtocol) -> Self {
546 self.protocol = Some(protocol);
547 self
548 }
549
550 pub fn with_max_export_batch_size(mut self, size: usize) -> Self {
555 self.max_export_batch_size = Some(size);
556 self
557 }
558
559 pub fn with_metric_export_interval(mut self, interval: Duration) -> Self {
564 self.metric_export_interval = Some(interval);
565 self
566 }
567
568 pub fn with_logs(mut self, enabled: bool) -> Self {
575 self.logs = enabled;
576 self
577 }
578
579 pub fn with_propagated_span_fields(mut self, fields: &'static [&'static str]) -> Self {
592 self.propagated_span_fields = fields;
593 self
594 }
595
596 pub fn with_export_timeout(mut self, timeout: Duration) -> Self {
600 self.export_timeout = Some(timeout);
601 self
602 }
603
604 pub fn with_shutdown_timeout(mut self, timeout: Duration) -> Self {
611 self.shutdown_timeout = timeout;
612 self
613 }
614
615 #[cfg(feature = "profiling")]
633 pub fn with_profiling(mut self, endpoint: &str) -> Self {
634 self.pyroscope_endpoint = Some(endpoint.to_string());
635 self
636 }
637
638 pub fn with_meter_provider_setup<F>(mut self, setup: F) -> Self
695 where
696 F: FnOnce(MeterProviderBuilder) -> MeterProviderBuilder + Send + Sync + 'static,
697 {
698 self.extra_metric_readers.push(Box::new(setup));
699 self
700 }
701
702 pub fn with_layer<L>(mut self, layer: L) -> Self
703 where
704 L: tracing_subscriber::Layer<tracing_subscriber::Registry> + Send + Sync + 'static,
705 {
706 self.extra_layers.push(Box::new(layer));
707 self
708 }
709
710 pub fn init(self) -> Result<TelemetryHandles, Box<dyn Error>> {
725 let log_filter = match self.log_filter.as_deref() {
726 Some(directive) => tracing_subscriber::EnvFilter::try_new(directive)?,
727 None => tracing_subscriber::EnvFilter::from_default_env(),
728 };
729
730 if let Some(interval) = self.metric_export_interval
731 && interval.is_zero()
732 {
733 return Err("metric_export_interval must be greater than zero".into());
734 }
735
736 let protocol = self.protocol.or_else(protocol_from_env).unwrap_or({
737 #[cfg(feature = "grpc")]
738 {
739 ExportProtocol::Grpc
740 }
741 #[cfg(all(not(feature = "grpc"), feature = "http"))]
742 {
743 ExportProtocol::HttpProtobuf
744 }
745 });
746
747 let default_endpoint = match protocol {
748 #[cfg(feature = "grpc")]
749 ExportProtocol::Grpc => "http://localhost:4317",
750 #[cfg(feature = "http")]
751 ExportProtocol::HttpProtobuf => "http://localhost:4318",
752 };
753 let endpoint = std::env::var("OTEL_EXPORTER_OTLP_ENDPOINT")
754 .unwrap_or_else(|_| default_endpoint.to_string());
755
756 let export_timeout = self.export_timeout.or_else(timeout_from_env);
758
759 let service_name = self.service_name.unwrap_or_else(|| {
761 std::env::var("OTEL_SERVICE_NAME").unwrap_or_else(|_| "unknown_service".to_string())
762 });
763
764 let resource = build_resource(
765 &service_name,
766 self.service_version.as_deref(),
767 self.deployment_environment.as_deref(),
768 );
769
770 let sampler = match self.sampler {
771 Some(s) => s,
772 None => sampler_from_env()?.unwrap_or(TraceSampler::AlwaysOn),
773 };
774
775 let trace_exporter = build_span_exporter(
777 protocol,
778 &endpoint,
779 export_timeout,
780 #[cfg(feature = "grpc-mtls")]
781 self.mtls.as_ref(),
782 )?;
783
784 let batch_processor = if let Some(size) = self.max_export_batch_size {
785 BatchSpanProcessor::builder(trace_exporter)
786 .with_batch_config(
787 BatchConfigBuilder::default()
788 .with_max_export_batch_size(size)
789 .build(),
790 )
791 .build()
792 } else {
793 BatchSpanProcessor::builder(trace_exporter).build()
794 };
795
796 let tracer_provider = SdkTracerProvider::builder()
797 .with_resource(resource.clone())
798 .with_sampler(sampler.into_sdk_sampler())
799 .with_span_processor(batch_processor)
800 .build();
801
802 opentelemetry::global::set_tracer_provider(tracer_provider.clone());
803
804 let propagator = TextMapCompositePropagator::new(vec![
806 Box::new(TraceContextPropagator::new()),
807 Box::new(BaggagePropagator::new()),
808 ]);
809 opentelemetry::global::set_text_map_propagator(propagator);
810
811 let meter_provider = if self.metrics {
813 let metric_exporter = build_metric_exporter(
814 protocol,
815 &endpoint,
816 export_timeout,
817 #[cfg(feature = "grpc-mtls")]
818 self.mtls.as_ref(),
819 )?;
820
821 let periodic_reader = if let Some(interval) = self.metric_export_interval {
822 PeriodicReader::builder(metric_exporter)
823 .with_interval(interval)
824 .build()
825 } else {
826 PeriodicReader::builder(metric_exporter).build()
827 };
828
829 let mut mp_builder = SdkMeterProvider::builder()
830 .with_resource(resource.clone())
831 .with_reader(periodic_reader);
832 for installer in self.extra_metric_readers {
833 mp_builder = installer(mp_builder);
834 }
835 let mp = mp_builder.build();
836
837 opentelemetry::global::set_meter_provider(mp.clone());
838
839 Some(mp)
840 } else {
841 None
842 };
843
844 let logger_provider = if self.logs {
846 let log_exporter = build_log_exporter(
847 protocol,
848 &endpoint,
849 export_timeout,
850 #[cfg(feature = "grpc-mtls")]
851 self.mtls.as_ref(),
852 )?;
853
854 let lp = SdkLoggerProvider::builder()
855 .with_resource(resource)
856 .with_batch_exporter(log_exporter)
857 .build();
858
859 Some(lp)
860 } else {
861 None
862 };
863
864 #[cfg(feature = "profiling")]
866 let profiling_handle = if let Some(ref endpoint) = self.pyroscope_endpoint {
867 profiling::start_pyroscope_bridge(&service_name, endpoint)?
868 } else {
869 None
870 };
871 #[cfg(not(feature = "profiling"))]
872 let _profiling_handle: Option<()> = None;
873
874 let extra = if self.extra_layers.is_empty() {
880 None
881 } else {
882 Some(self.extra_layers)
883 };
884
885 macro_rules! install_subscriber {
886 ($fmt_layer:expr) => {{
887 let otel_layer = tracing_opentelemetry::layer()
891 .with_tracer(tracing_bridge_tracer(&tracer_provider));
892 let registry = tracing_subscriber::registry()
893 .with(extra)
894 .with(log_filter)
895 .with($fmt_layer)
896 .with(otel_layer);
897
898 #[cfg(feature = "profiling-bridge-pyroscope-rs")]
899 let registry = registry.with(crate::profiling::ProfilingTagLayer);
900
901 if let Some(lp) = &logger_provider {
902 if let Err(e) = registry
903 .with(crate::log_bridge::SpanAwareLogBridge::new(
904 lp,
905 self.propagated_span_fields,
906 ))
907 .try_init()
908 {
909 eprintln!(
910 "otel-bootstrap: global tracing subscriber already installed — \
911 OTLP log records will NOT be exported to the collector: {e}"
912 );
913 }
914 } else if let Err(e) = registry.try_init() {
915 eprintln!(
916 "otel-bootstrap: global tracing subscriber already installed — \
917 OTLP telemetry will NOT be exported to the collector: {e}"
918 );
919 }
920 }};
921 }
922
923 match self.log_format {
924 LogFormat::Pretty => install_subscriber!(tracing_subscriber::fmt::layer()),
925 LogFormat::Json => install_subscriber!(tracing_subscriber::fmt::layer().json()),
926 }
927
928 Ok(TelemetryHandles {
929 tracer_provider,
930 meter_provider,
931 logger_provider,
932 shutdown_timeout: self.shutdown_timeout,
933 #[cfg(feature = "profiling")]
934 profiling_handle,
935 })
936 }
937}
938
939pub fn init_telemetry(service_name: &str) -> Result<TelemetryHandles, Box<dyn Error>> {
953 Telemetry::builder(service_name).init()
954}
955
956pub fn init_telemetry_with_sampler(
973 service_name: &str,
974 sampler: Option<TraceSampler>,
975) -> Result<TelemetryHandles, Box<dyn Error>> {
976 let builder = Telemetry::builder(service_name);
977 match sampler {
978 Some(s) => builder.with_sampler(s),
979 None => builder, }
981 .init()
982}
983
984fn timeout_from_env() -> Option<Duration> {
986 let ms = std::env::var("OTEL_EXPORTER_OTLP_TIMEOUT").ok()?;
987 let ms: u64 = ms.trim().parse().ok()?;
988 Some(Duration::from_millis(ms))
989}
990
991#[cfg(feature = "grpc-mtls")]
998fn build_tls_config(material: &MtlsMaterial) -> tonic::transport::ClientTlsConfig {
999 use tonic::transport::{Certificate, ClientTlsConfig, Identity};
1000 ClientTlsConfig::new()
1001 .ca_certificate(Certificate::from_pem(&material.trust_bundle_pem))
1002 .identity(Identity::from_pem(
1003 &material.client_cert_chain_pem,
1004 &material.client_key_pem,
1005 ))
1006}
1007
1008fn build_span_exporter(
1009 protocol: ExportProtocol,
1010 endpoint: &str,
1011 timeout: Option<Duration>,
1012 #[cfg(feature = "grpc-mtls")] mtls: Option<&MtlsMaterial>,
1013) -> Result<opentelemetry_otlp::SpanExporter, Box<dyn Error>> {
1014 match protocol {
1015 #[cfg(feature = "grpc")]
1016 ExportProtocol::Grpc => {
1017 let mut b = opentelemetry_otlp::SpanExporter::builder()
1018 .with_tonic()
1019 .with_endpoint(endpoint);
1020 if let Some(t) = timeout {
1021 b = b.with_timeout(t);
1022 }
1023 #[cfg(feature = "grpc-mtls")]
1024 if let Some(m) = mtls {
1025 use opentelemetry_otlp::WithTonicConfig as _;
1026 b = b.with_tls_config(build_tls_config(m));
1027 }
1028 Ok(b.build()?)
1029 }
1030 #[cfg(feature = "http")]
1031 ExportProtocol::HttpProtobuf => {
1032 let mut b = opentelemetry_otlp::SpanExporter::builder()
1033 .with_http()
1034 .with_endpoint(endpoint);
1035 if let Some(t) = timeout {
1036 b = b.with_timeout(t);
1037 }
1038 Ok(b.build()?)
1039 }
1040 }
1041}
1042
1043fn build_metric_exporter(
1044 protocol: ExportProtocol,
1045 endpoint: &str,
1046 timeout: Option<Duration>,
1047 #[cfg(feature = "grpc-mtls")] mtls: Option<&MtlsMaterial>,
1048) -> Result<opentelemetry_otlp::MetricExporter, Box<dyn Error>> {
1049 match protocol {
1050 #[cfg(feature = "grpc")]
1051 ExportProtocol::Grpc => {
1052 let mut b = opentelemetry_otlp::MetricExporter::builder()
1053 .with_tonic()
1054 .with_endpoint(endpoint);
1055 if let Some(t) = timeout {
1056 b = b.with_timeout(t);
1057 }
1058 #[cfg(feature = "grpc-mtls")]
1059 if let Some(m) = mtls {
1060 use opentelemetry_otlp::WithTonicConfig as _;
1061 b = b.with_tls_config(build_tls_config(m));
1062 }
1063 Ok(b.build()?)
1064 }
1065 #[cfg(feature = "http")]
1066 ExportProtocol::HttpProtobuf => {
1067 let mut b = opentelemetry_otlp::MetricExporter::builder()
1068 .with_http()
1069 .with_endpoint(endpoint);
1070 if let Some(t) = timeout {
1071 b = b.with_timeout(t);
1072 }
1073 Ok(b.build()?)
1074 }
1075 }
1076}
1077
1078fn build_log_exporter(
1079 protocol: ExportProtocol,
1080 endpoint: &str,
1081 timeout: Option<Duration>,
1082 #[cfg(feature = "grpc-mtls")] mtls: Option<&MtlsMaterial>,
1083) -> Result<opentelemetry_otlp::LogExporter, Box<dyn Error>> {
1084 match protocol {
1085 #[cfg(feature = "grpc")]
1086 ExportProtocol::Grpc => {
1087 let mut b = opentelemetry_otlp::LogExporter::builder()
1088 .with_tonic()
1089 .with_endpoint(endpoint);
1090 if let Some(t) = timeout {
1091 b = b.with_timeout(t);
1092 }
1093 #[cfg(feature = "grpc-mtls")]
1094 if let Some(m) = mtls {
1095 use opentelemetry_otlp::WithTonicConfig as _;
1096 b = b.with_tls_config(build_tls_config(m));
1097 }
1098 Ok(b.build()?)
1099 }
1100 #[cfg(feature = "http")]
1101 ExportProtocol::HttpProtobuf => {
1102 let mut b = opentelemetry_otlp::LogExporter::builder()
1103 .with_http()
1104 .with_endpoint(endpoint);
1105 if let Some(t) = timeout {
1106 b = b.with_timeout(t);
1107 }
1108 Ok(b.build()?)
1109 }
1110 }
1111}
1112
1113pub fn build_resource(
1128 service_name: &str,
1129 service_version: Option<&str>,
1130 deployment_environment: Option<&str>,
1131) -> Resource {
1132 let hostname = hostname::get()
1133 .ok()
1134 .and_then(|h| h.into_string().ok())
1135 .unwrap_or_default();
1136
1137 let mut builder = Resource::builder()
1138 .with_service_name(service_name.to_string())
1139 .with_attributes([
1140 KeyValue::new(HOST_NAME, hostname),
1141 KeyValue::new(PROCESS_PID, std::process::id() as i64),
1142 ]);
1143
1144 if let Some(version) = service_version {
1145 builder = builder.with_attribute(KeyValue::new(SERVICE_VERSION, version.to_string()));
1146 }
1147
1148 if let Some(env) = deployment_environment {
1149 builder =
1150 builder.with_attribute(KeyValue::new(DEPLOYMENT_ENVIRONMENT_NAME, env.to_string()));
1151 }
1152
1153 builder.build()
1154}
1155
1156#[cfg(feature = "axum")]
1174pub fn axum_layer() -> axum_middleware::OtelTraceLayer {
1175 axum_middleware::OtelTraceLayer
1176}
1177
1178#[cfg(feature = "axum")]
1209pub fn span_enricher_layer<T>() -> axum_middleware::SpanEnricherLayer<T>
1210where
1211 T: span_enrichment::EnrichSpan + Clone + Send + Sync + 'static,
1212{
1213 axum_middleware::SpanEnricherLayer::default()
1214}
1215
1216#[cfg(feature = "tonic-tracing")]
1238pub fn grpc_client_layer() -> grpc_middleware::GrpcClientTraceLayer {
1239 grpc_middleware::GrpcClientTraceLayer
1240}
1241
1242#[cfg(feature = "tonic-tracing")]
1257pub fn grpc_server_layer() -> grpc_middleware::GrpcServerTraceLayer {
1258 grpc_middleware::GrpcServerTraceLayer
1259}
1260
1261#[cfg(test)]
1262mod tests {
1263 use super::*;
1264 use opentelemetry::trace::{Span as _, Tracer as _};
1265 use std::sync::Mutex;
1266
1267 static ENV_LOCK: Mutex<()> = Mutex::new(());
1268
1269 #[test]
1270 fn tracing_bridge_uses_sdk_tracer() {
1271 let provider = SdkTracerProvider::builder().build();
1272 let tracer = tracing_bridge_tracer(&provider);
1273 let span = tracer.start("bridge-regression");
1274
1275 assert!(span.span_context().is_valid());
1276
1277 provider.shutdown().expect("provider shutdown");
1278 }
1279
1280 #[test]
1281 fn resource_contains_all_attributes_when_provided() {
1282 let resource = build_resource("test-svc", Some("1.2.3"), Some("staging"));
1283
1284 assert_eq!(
1285 resource.get(&opentelemetry::Key::new("service.name")),
1286 Some(opentelemetry::Value::from("test-svc")),
1287 );
1288 assert_eq!(
1289 resource.get(&opentelemetry::Key::new(SERVICE_VERSION)),
1290 Some(opentelemetry::Value::from("1.2.3")),
1291 );
1292 assert_eq!(
1293 resource.get(&opentelemetry::Key::new(DEPLOYMENT_ENVIRONMENT_NAME)),
1294 Some(opentelemetry::Value::from("staging")),
1295 );
1296 assert!(resource.get(&opentelemetry::Key::new(HOST_NAME)).is_some());
1297 assert!(
1298 resource
1299 .get(&opentelemetry::Key::new(PROCESS_PID))
1300 .is_some()
1301 );
1302 }
1303
1304 #[test]
1305 fn resource_graceful_when_optional_values_omitted() {
1306 let resource = build_resource("test-svc", None, None);
1307
1308 assert_eq!(
1309 resource.get(&opentelemetry::Key::new("service.name")),
1310 Some(opentelemetry::Value::from("test-svc")),
1311 );
1312 assert!(
1313 resource
1314 .get(&opentelemetry::Key::new(SERVICE_VERSION))
1315 .is_none()
1316 );
1317 assert!(
1318 resource
1319 .get(&opentelemetry::Key::new(DEPLOYMENT_ENVIRONMENT_NAME))
1320 .is_none()
1321 );
1322 assert!(resource.get(&opentelemetry::Key::new(HOST_NAME)).is_some());
1324 assert!(
1325 resource
1326 .get(&opentelemetry::Key::new(PROCESS_PID))
1327 .is_some()
1328 );
1329 }
1330
1331 #[test]
1332 fn trace_sampler_ratio_converts_to_sdk() {
1333 let sampler = TraceSampler::TraceIdRatio(0.5);
1334 let sdk = sampler.into_sdk_sampler();
1335 assert_eq!(format!("{sdk:?}"), "TraceIdRatioBased(0.5)");
1336 }
1337
1338 #[test]
1339 fn trace_sampler_parent_based_converts_to_sdk() {
1340 let sampler = TraceSampler::ParentBased(Box::new(TraceSampler::TraceIdRatio(0.25)));
1341 let sdk = sampler.into_sdk_sampler();
1342 let debug = format!("{sdk:?}");
1343 assert!(debug.contains("ParentBased"));
1344 assert!(debug.contains("0.25"));
1345 }
1346
1347 unsafe fn set_env(key: &str, val: &str) {
1349 unsafe {
1350 std::env::set_var(key, val);
1351 }
1352 }
1353
1354 unsafe fn remove_env(key: &str) {
1355 unsafe {
1356 std::env::remove_var(key);
1357 }
1358 }
1359
1360 #[test]
1361 fn sampler_from_env_reads_traceidratio() {
1362 let _lock = ENV_LOCK.lock().unwrap();
1363 unsafe {
1364 set_env("OTEL_TRACES_SAMPLER", "traceidratio");
1365 set_env("OTEL_TRACES_SAMPLER_ARG", "0.42");
1366 }
1367
1368 let sampler = sampler_from_env()
1369 .expect("should not error")
1370 .expect("should return Some");
1371 assert!(
1372 matches!(sampler, TraceSampler::TraceIdRatio(r) if (r - 0.42).abs() < f64::EPSILON)
1373 );
1374
1375 unsafe {
1376 remove_env("OTEL_TRACES_SAMPLER");
1377 remove_env("OTEL_TRACES_SAMPLER_ARG");
1378 }
1379 }
1380
1381 #[test]
1382 fn sampler_from_env_returns_none_when_unset() {
1383 let _lock = ENV_LOCK.lock().unwrap();
1384 unsafe {
1385 remove_env("OTEL_TRACES_SAMPLER");
1386 }
1387 assert!(sampler_from_env().expect("should not error").is_none());
1388 }
1389
1390 #[test]
1391 fn sampler_from_env_reads_parentbased_traceidratio() {
1392 let _lock = ENV_LOCK.lock().unwrap();
1393 unsafe {
1394 set_env("OTEL_TRACES_SAMPLER", "parentbased_traceidratio");
1395 set_env("OTEL_TRACES_SAMPLER_ARG", "0.1");
1396 }
1397
1398 let sampler = sampler_from_env()
1399 .expect("should not error")
1400 .expect("should return Some");
1401 assert!(
1402 matches!(sampler, TraceSampler::ParentBased(inner) if matches!(*inner, TraceSampler::TraceIdRatio(r) if (r - 0.1).abs() < f64::EPSILON))
1403 );
1404
1405 unsafe {
1406 remove_env("OTEL_TRACES_SAMPLER");
1407 remove_env("OTEL_TRACES_SAMPLER_ARG");
1408 }
1409 }
1410
1411 #[test]
1412 fn sampler_from_env_parentbased_always_on() {
1413 let _lock = ENV_LOCK.lock().unwrap();
1414 unsafe {
1415 set_env("OTEL_TRACES_SAMPLER", "parentbased_always_on");
1416 }
1417 let sampler = sampler_from_env()
1418 .expect("should not error")
1419 .expect("should return Some");
1420 assert!(
1421 matches!(sampler, TraceSampler::ParentBased(inner) if matches!(*inner, TraceSampler::AlwaysOn))
1422 );
1423 unsafe {
1424 remove_env("OTEL_TRACES_SAMPLER");
1425 }
1426 }
1427
1428 #[test]
1429 fn sampler_from_env_parentbased_always_off() {
1430 let _lock = ENV_LOCK.lock().unwrap();
1431 unsafe {
1432 set_env("OTEL_TRACES_SAMPLER", "parentbased_always_off");
1433 }
1434 let sampler = sampler_from_env()
1435 .expect("should not error")
1436 .expect("should return Some");
1437 assert!(
1438 matches!(sampler, TraceSampler::ParentBased(inner) if matches!(*inner, TraceSampler::AlwaysOff))
1439 );
1440 unsafe {
1441 remove_env("OTEL_TRACES_SAMPLER");
1442 }
1443 }
1444
1445 #[test]
1446 fn sampler_from_env_always_on() {
1447 let _lock = ENV_LOCK.lock().unwrap();
1448 unsafe {
1449 set_env("OTEL_TRACES_SAMPLER", "always_on");
1450 }
1451 let sampler = sampler_from_env()
1452 .expect("should not error")
1453 .expect("should return Some");
1454 assert!(matches!(sampler, TraceSampler::AlwaysOn));
1455 unsafe {
1456 remove_env("OTEL_TRACES_SAMPLER");
1457 }
1458 }
1459
1460 #[test]
1461 fn sampler_from_env_always_off() {
1462 let _lock = ENV_LOCK.lock().unwrap();
1463 unsafe {
1464 set_env("OTEL_TRACES_SAMPLER", "always_off");
1465 }
1466 let sampler = sampler_from_env()
1467 .expect("should not error")
1468 .expect("should return Some");
1469 assert!(matches!(sampler, TraceSampler::AlwaysOff));
1470 unsafe {
1471 remove_env("OTEL_TRACES_SAMPLER");
1472 }
1473 }
1474
1475 #[test]
1476 fn sampler_from_env_unknown_returns_error() {
1477 let _lock = ENV_LOCK.lock().unwrap();
1478 unsafe {
1479 set_env("OTEL_TRACES_SAMPLER", "unknown_sampler");
1480 }
1481 let err = sampler_from_env().expect_err("unknown sampler should produce an error");
1482 assert!(
1483 err.to_string().contains("unknown_sampler"),
1484 "error message should include the unknown name, got: {err}"
1485 );
1486 unsafe {
1487 remove_env("OTEL_TRACES_SAMPLER");
1488 }
1489 }
1490
1491 #[test]
1492 fn trace_sampler_always_on_converts_to_sdk() {
1493 let sdk = TraceSampler::AlwaysOn.into_sdk_sampler();
1494 assert_eq!(format!("{sdk:?}"), "AlwaysOn");
1495 }
1496
1497 #[test]
1498 fn trace_sampler_always_off_converts_to_sdk() {
1499 let sdk = TraceSampler::AlwaysOff.into_sdk_sampler();
1500 assert_eq!(format!("{sdk:?}"), "AlwaysOff");
1501 }
1502
1503 #[test]
1504 fn builder_has_sensible_defaults() {
1505 let builder = Telemetry::builder("test-svc");
1506 assert_eq!(builder.service_name.as_deref(), Some("test-svc"));
1507 assert!(builder.service_version.is_none());
1508 assert!(builder.deployment_environment.is_none());
1509 assert!(builder.sampler.is_none());
1510 assert!(builder.metrics);
1511 assert!(!builder.logs);
1512 assert!(builder.protocol.is_none());
1513 assert!(builder.max_export_batch_size.is_none());
1514 assert!(builder.metric_export_interval.is_none());
1515 assert!(builder.export_timeout.is_none());
1516 }
1517
1518 #[test]
1519 fn from_env_builder_has_no_service_name() {
1520 let builder = Telemetry::from_env();
1521 assert!(builder.service_name.is_none());
1522 }
1523
1524 #[test]
1525 fn with_export_timeout_stores_value() {
1526 let timeout = Duration::from_secs(5);
1527 let builder = Telemetry::builder("test-svc").with_export_timeout(timeout);
1528 assert_eq!(builder.export_timeout, Some(timeout));
1529 }
1530
1531 #[test]
1532 fn timeout_from_env_reads_milliseconds() {
1533 let _lock = ENV_LOCK.lock().unwrap();
1534 unsafe {
1535 set_env("OTEL_EXPORTER_OTLP_TIMEOUT", "5000");
1536 }
1537 let t = timeout_from_env();
1538 assert_eq!(t, Some(Duration::from_millis(5000)));
1539 unsafe {
1540 remove_env("OTEL_EXPORTER_OTLP_TIMEOUT");
1541 }
1542 }
1543
1544 #[test]
1545 fn timeout_from_env_returns_none_when_unset() {
1546 let _lock = ENV_LOCK.lock().unwrap();
1547 unsafe {
1548 remove_env("OTEL_EXPORTER_OTLP_TIMEOUT");
1549 }
1550 assert_eq!(timeout_from_env(), None);
1551 }
1552
1553 #[test]
1554 fn service_name_from_env_used_when_none_given() {
1555 let builder = Telemetry::from_env();
1556 assert!(builder.service_name.is_none());
1557 }
1558
1559 #[test]
1560 fn explicit_service_name_overrides_env_var() {
1561 let builder = Telemetry::builder("explicit-svc");
1562 assert_eq!(builder.service_name.as_deref(), Some("explicit-svc"));
1563 }
1564
1565 #[test]
1566 fn from_env_builder_service_name_is_none() {
1567 let builder = Telemetry::from_env();
1568 assert!(builder.service_name.is_none());
1569 }
1570
1571 #[test]
1572 fn init_returns_error_for_unknown_otel_traces_sampler() {
1573 let _lock = ENV_LOCK.lock().unwrap();
1574 unsafe {
1575 set_env("OTEL_TRACES_SAMPLER", "not_a_real_sampler");
1576 }
1577 let result = Telemetry::builder("test-svc").with_metrics(false).init();
1578 let err = result
1579 .err()
1580 .expect("unknown sampler env var should cause init to fail");
1581 assert!(
1582 err.to_string().contains("not_a_real_sampler"),
1583 "error should name the unknown sampler, got: {err}"
1584 );
1585 unsafe {
1586 remove_env("OTEL_TRACES_SAMPLER");
1587 }
1588 }
1589
1590 #[test]
1591 fn with_max_export_batch_size_stores_value() {
1592 let builder = Telemetry::builder("test-svc").with_max_export_batch_size(1024);
1593 assert_eq!(builder.max_export_batch_size, Some(1024));
1594 }
1595
1596 #[test]
1597 fn with_metric_export_interval_stores_value() {
1598 let interval = Duration::from_secs(30);
1599 let builder = Telemetry::builder("test-svc").with_metric_export_interval(interval);
1600 assert_eq!(builder.metric_export_interval, Some(interval));
1601 }
1602
1603 #[test]
1604 fn init_rejects_zero_metric_export_interval() {
1605 let err = Telemetry::builder("test-svc")
1606 .with_metric_export_interval(Duration::ZERO)
1607 .with_metrics(false)
1608 .init()
1609 .err()
1610 .expect("expected error for zero interval");
1611 assert!(
1612 err.to_string().contains("metric_export_interval"),
1613 "error message should mention metric_export_interval, got: {err}"
1614 );
1615 }
1616
1617 #[test]
1618 fn builder_with_custom_values() {
1619 let builder = Telemetry::builder("test-svc")
1620 .with_version("2.0.0")
1621 .with_environment("production")
1622 .with_sampler(TraceSampler::TraceIdRatio(0.5))
1623 .with_metrics(false);
1624
1625 assert_eq!(builder.service_name.as_deref(), Some("test-svc"));
1626 assert_eq!(builder.service_version.as_deref(), Some("2.0.0"));
1627 assert_eq!(
1628 builder.deployment_environment.as_deref(),
1629 Some("production")
1630 );
1631 assert!(
1632 matches!(builder.sampler, Some(TraceSampler::TraceIdRatio(r)) if (r - 0.5).abs() < f64::EPSILON)
1633 );
1634 assert!(!builder.metrics);
1635 }
1636
1637 #[test]
1638 fn builder_stores_programmatic_log_configuration() {
1639 let builder = Telemetry::builder("test-svc")
1640 .with_log_filter("info,opentelemetry_sdk=warn")
1641 .with_log_format(LogFormat::Json);
1642
1643 assert_eq!(
1644 builder.log_filter.as_deref(),
1645 Some("info,opentelemetry_sdk=warn")
1646 );
1647 assert_eq!(builder.log_format, LogFormat::Json);
1648 }
1649
1650 #[test]
1651 fn init_rejects_invalid_programmatic_log_filter_before_provider_setup() {
1652 let setup_ran = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
1653 let setup_ran_in_closure = std::sync::Arc::clone(&setup_ran);
1654
1655 let error = Telemetry::builder("test-svc")
1656 .with_log_filter("[")
1657 .with_meter_provider_setup(move |builder| {
1658 setup_ran_in_closure.store(true, std::sync::atomic::Ordering::SeqCst);
1659 builder
1660 })
1661 .init()
1662 .err()
1663 .expect("invalid filter must fail initialization");
1664
1665 assert!(error.to_string().contains("invalid filter directive"));
1666 assert!(!setup_ran.load(std::sync::atomic::Ordering::SeqCst));
1667 }
1668
1669 #[test]
1670 #[cfg(feature = "grpc")]
1671 fn builder_with_protocol_grpc() {
1672 let builder = Telemetry::builder("test-svc").with_protocol(ExportProtocol::Grpc);
1673 assert_eq!(builder.protocol, Some(ExportProtocol::Grpc));
1674 }
1675
1676 #[test]
1677 #[cfg(feature = "http")]
1678 fn builder_with_protocol_http() {
1679 let builder = Telemetry::builder("test-svc").with_protocol(ExportProtocol::HttpProtobuf);
1680 assert_eq!(builder.protocol, Some(ExportProtocol::HttpProtobuf));
1681 }
1682
1683 #[test]
1684 #[cfg(feature = "grpc")]
1685 fn protocol_from_env_reads_grpc() {
1686 let _lock = ENV_LOCK.lock().unwrap();
1687 unsafe {
1688 set_env("OTEL_EXPORTER_OTLP_PROTOCOL", "grpc");
1689 }
1690 assert_eq!(protocol_from_env(), Some(ExportProtocol::Grpc));
1691 unsafe {
1692 remove_env("OTEL_EXPORTER_OTLP_PROTOCOL");
1693 }
1694 }
1695
1696 #[test]
1697 #[cfg(feature = "http")]
1698 fn protocol_from_env_reads_http_protobuf() {
1699 let _lock = ENV_LOCK.lock().unwrap();
1700 unsafe {
1701 set_env("OTEL_EXPORTER_OTLP_PROTOCOL", "http/protobuf");
1702 }
1703 assert_eq!(protocol_from_env(), Some(ExportProtocol::HttpProtobuf));
1704 unsafe {
1705 remove_env("OTEL_EXPORTER_OTLP_PROTOCOL");
1706 }
1707 }
1708
1709 #[test]
1710 fn protocol_from_env_returns_none_when_unset() {
1711 let _lock = ENV_LOCK.lock().unwrap();
1712 unsafe {
1713 remove_env("OTEL_EXPORTER_OTLP_PROTOCOL");
1714 }
1715 assert_eq!(protocol_from_env(), None);
1716 }
1717
1718 #[test]
1719 fn protocol_from_env_returns_none_for_unknown() {
1720 let _lock = ENV_LOCK.lock().unwrap();
1721 unsafe {
1722 set_env("OTEL_EXPORTER_OTLP_PROTOCOL", "websocket");
1723 }
1724 assert_eq!(protocol_from_env(), None);
1725 unsafe {
1726 remove_env("OTEL_EXPORTER_OTLP_PROTOCOL");
1727 }
1728 }
1729
1730 #[test]
1731 fn builder_is_send_and_sync() {
1732 fn assert_send_sync<T: Send + Sync>() {}
1733 assert_send_sync::<TelemetryBuilder>();
1734 }
1735
1736 #[test]
1737 fn with_shutdown_timeout_stores_value() {
1738 let timeout = Duration::from_secs(10);
1739 let builder = Telemetry::builder("test-svc").with_shutdown_timeout(timeout);
1740 assert_eq!(builder.shutdown_timeout, timeout);
1741 }
1742
1743 #[test]
1744 fn default_shutdown_timeout_is_five_seconds() {
1745 let builder = Telemetry::builder("test-svc");
1746 assert_eq!(builder.shutdown_timeout, Duration::from_secs(5));
1747 }
1748
1749 #[cfg(feature = "testing")]
1757 #[test]
1758 fn drop_completes_within_shutdown_timeout() {
1759 let mut handles = crate::Telemetry::testing("drop-timeout-test");
1761 handles.shutdown_timeout = Duration::from_millis(100);
1763
1764 let start = std::time::Instant::now();
1765 drop(handles);
1766 let elapsed = start.elapsed();
1767
1768 assert!(
1770 elapsed < Duration::from_millis(500),
1771 "drop took {elapsed:?}, expected < 500 ms"
1772 );
1773 }
1774}