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;
40mod runtime_metrics;
41
42pub mod instrumented_port;
43pub mod log_bridge;
44pub mod span_enrichment;
45pub mod spanned;
46
47pub use instrumented_port::{Instrumented, InstrumentedArc};
48pub use log_bridge::{
49 PROPAGATED_SPAN_FIELDS, SpanLogAttrs, record_span_log_attr, record_span_log_attr_on,
50};
51pub use spanned::{Spanned, in_span};
52
53use opentelemetry::KeyValue;
54use opentelemetry::propagation::TextMapCompositePropagator;
55use opentelemetry_otlp::WithExportConfig;
56use opentelemetry_sdk::{
57 Resource,
58 logs::SdkLoggerProvider,
59 metrics::{MeterProviderBuilder, PeriodicReader, SdkMeterProvider},
60 propagation::{BaggagePropagator, TraceContextPropagator},
61 trace::{BatchConfigBuilder, BatchSpanProcessor, Sampler, SdkTracer, SdkTracerProvider},
62};
63use opentelemetry_semantic_conventions::attribute::{
64 DEPLOYMENT_ENVIRONMENT_NAME, HOST_NAME, PROCESS_PID, SERVICE_VERSION,
65};
66use std::error::Error;
67use std::time::Duration;
68use tracing_subscriber::layer::SubscriberExt;
69use tracing_subscriber::util::SubscriberInitExt;
70
71fn tracing_bridge_tracer(provider: &SdkTracerProvider) -> SdkTracer {
72 use opentelemetry::trace::TracerProvider as _;
73
74 provider.tracer(env!("CARGO_PKG_NAME"))
75}
76
77#[derive(Debug, Clone)]
92pub enum TraceSampler {
93 AlwaysOn,
95 AlwaysOff,
97 TraceIdRatio(f64),
99 ParentBased(Box<TraceSampler>),
102}
103
104#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
106pub enum LogFormat {
107 #[default]
109 Pretty,
110 Json,
112}
113
114impl TraceSampler {
115 fn into_sdk_sampler(self) -> Sampler {
117 match self {
118 TraceSampler::AlwaysOn => Sampler::AlwaysOn,
119 TraceSampler::AlwaysOff => Sampler::AlwaysOff,
120 TraceSampler::TraceIdRatio(r) => Sampler::TraceIdRatioBased(r),
121 TraceSampler::ParentBased(inner) => {
122 Sampler::ParentBased(Box::new(inner.into_sdk_sampler()))
123 }
124 }
125 }
126}
127
128fn sampler_from_env() -> Result<Option<TraceSampler>, Box<dyn Error>> {
136 let name = match std::env::var("OTEL_TRACES_SAMPLER") {
137 Ok(v) => v,
138 Err(_) => return Ok(None),
139 };
140 let arg = std::env::var("OTEL_TRACES_SAMPLER_ARG").ok();
141 let sampler = match name.as_str() {
142 "always_on" => TraceSampler::AlwaysOn,
143 "always_off" => TraceSampler::AlwaysOff,
144 "traceidratio" => {
145 let ratio = arg
146 .as_deref()
147 .unwrap_or("1.0")
148 .parse::<f64>()
149 .unwrap_or(1.0);
150 TraceSampler::TraceIdRatio(ratio)
151 }
152 "parentbased_always_on" => TraceSampler::ParentBased(Box::new(TraceSampler::AlwaysOn)),
153 "parentbased_always_off" => TraceSampler::ParentBased(Box::new(TraceSampler::AlwaysOff)),
154 "parentbased_traceidratio" => {
155 let ratio = arg
156 .as_deref()
157 .unwrap_or("1.0")
158 .parse::<f64>()
159 .unwrap_or(1.0);
160 TraceSampler::ParentBased(Box::new(TraceSampler::TraceIdRatio(ratio)))
161 }
162 unknown => {
163 return Err(format!(
164 "OTEL_TRACES_SAMPLER: unrecognised sampler name '{unknown}'. \
165 Valid values: always_on, always_off, traceidratio, \
166 parentbased_always_on, parentbased_always_off, parentbased_traceidratio"
167 )
168 .into());
169 }
170 };
171 Ok(Some(sampler))
172}
173
174const DEFAULT_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5);
176
177pub struct TelemetryHandles {
198 pub tracer_provider: SdkTracerProvider,
199 pub meter_provider: Option<SdkMeterProvider>,
200 pub logger_provider: Option<SdkLoggerProvider>,
201 shutdown_timeout: Duration,
202 #[cfg(feature = "profiling")]
203 pub profiling_handle: Option<profiling::ProfilingHandle>,
204}
205
206impl TelemetryHandles {
207 pub fn shutdown(&self) -> Result<(), Box<dyn Error>> {
238 if let Err(e) = self.tracer_provider.shutdown() {
239 tracing::warn!("tracer provider shutdown error: {e}");
240 }
241 if let Some(mp) = &self.meter_provider
242 && let Err(e) = mp.shutdown()
243 {
244 tracing::warn!("meter provider shutdown error: {e}");
245 }
246 if let Some(lp) = &self.logger_provider
247 && let Err(e) = lp.shutdown()
248 {
249 tracing::warn!("logger provider shutdown error: {e}");
250 }
251 Ok(())
252 }
253}
254
255impl Drop for TelemetryHandles {
256 fn drop(&mut self) {
257 let tracer_provider = self.tracer_provider.clone();
258 let meter_provider = self.meter_provider.clone();
259 let logger_provider = self.logger_provider.clone();
260 let timeout = self.shutdown_timeout;
261
262 let (tx, rx) = std::sync::mpsc::channel();
263 std::thread::spawn(move || {
264 if let Err(e) = tracer_provider.shutdown() {
265 tracing::warn!("tracer provider shutdown error: {e}");
266 }
267 if let Some(mp) = meter_provider
268 && let Err(e) = mp.shutdown()
269 {
270 tracing::warn!("meter provider shutdown error: {e}");
271 }
272 if let Some(lp) = logger_provider
273 && let Err(e) = lp.shutdown()
274 {
275 tracing::warn!("logger provider shutdown error: {e}");
276 }
277 let _ = tx.send(());
278 });
279
280 if rx.recv_timeout(timeout).is_err() {
281 tracing::warn!(
282 "telemetry shutdown did not complete within {timeout:?}; \
283 some spans/metrics may not have been exported"
284 );
285 }
286 }
287}
288
289#[derive(Debug, Clone, Copy, PartialEq, Eq)]
312pub enum ExportProtocol {
313 #[cfg(feature = "grpc")]
315 Grpc,
316 #[cfg(feature = "http")]
318 HttpProtobuf,
319}
320
321#[cfg(feature = "grpc-mtls")]
331#[derive(Clone)]
332pub struct MtlsMaterial {
333 pub client_cert_chain_pem: Vec<u8>,
335 pub client_key_pem: Vec<u8>,
337 pub trust_bundle_pem: Vec<u8>,
339}
340
341#[cfg(feature = "grpc-mtls")]
342impl std::fmt::Debug for MtlsMaterial {
343 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
344 f.debug_struct("MtlsMaterial")
345 .field("client_cert_chain_pem", &"<redacted>")
346 .field("client_key_pem", &"<redacted>")
347 .field("trust_bundle_pem", &"<redacted>")
348 .finish()
349 }
350}
351
352fn protocol_from_env() -> Option<ExportProtocol> {
354 let val = std::env::var("OTEL_EXPORTER_OTLP_PROTOCOL").ok()?;
355 match val.trim() {
356 #[cfg(feature = "grpc")]
357 "grpc" => Some(ExportProtocol::Grpc),
358 #[cfg(feature = "http")]
359 "http/protobuf" => Some(ExportProtocol::HttpProtobuf),
360 _ => None,
361 }
362}
363
364pub struct Telemetry;
380
381impl Telemetry {
382 pub fn builder(service_name: &str) -> TelemetryBuilder {
386 TelemetryBuilder {
387 service_name: Some(service_name.to_string()),
388 service_version: None,
389 deployment_environment: None,
390 sampler: None,
391 metrics: true,
392 logs: false,
393 protocol: None,
394 max_export_batch_size: None,
395 metric_export_interval: None,
396 export_timeout: None,
397 shutdown_timeout: DEFAULT_SHUTDOWN_TIMEOUT,
398 log_filter: None,
399 log_format: LogFormat::default(),
400 extra_layers: Vec::new(),
401 extra_metric_readers: Vec::new(),
402 runtime_metrics: true,
403 #[cfg(feature = "grpc-mtls")]
404 mtls: None,
405 propagated_span_fields: crate::log_bridge::PROPAGATED_SPAN_FIELDS,
406 #[cfg(feature = "profiling")]
407 pyroscope_endpoint: None,
408 }
409 }
410
411 pub fn from_env() -> TelemetryBuilder {
421 TelemetryBuilder {
422 service_name: None,
423 service_version: None,
424 deployment_environment: None,
425 sampler: None,
426 metrics: true,
427 logs: false,
428 protocol: None,
429 max_export_batch_size: None,
430 metric_export_interval: None,
431 export_timeout: None,
432 shutdown_timeout: DEFAULT_SHUTDOWN_TIMEOUT,
433 log_filter: None,
434 log_format: LogFormat::default(),
435 extra_layers: Vec::new(),
436 extra_metric_readers: Vec::new(),
437 runtime_metrics: true,
438 #[cfg(feature = "grpc-mtls")]
439 mtls: None,
440 propagated_span_fields: crate::log_bridge::PROPAGATED_SPAN_FIELDS,
441 #[cfg(feature = "profiling")]
442 pyroscope_endpoint: None,
443 }
444 }
445}
446
447#[must_use = "a TelemetryBuilder does nothing until .init() is called"]
465pub struct TelemetryBuilder {
466 service_name: Option<String>,
467 service_version: Option<String>,
468 deployment_environment: Option<String>,
469 sampler: Option<TraceSampler>,
470 metrics: bool,
471 logs: bool,
472 protocol: Option<ExportProtocol>,
473 max_export_batch_size: Option<usize>,
474 metric_export_interval: Option<Duration>,
475 export_timeout: Option<Duration>,
476 shutdown_timeout: Duration,
477 log_filter: Option<String>,
478 log_format: LogFormat,
479 extra_layers: Vec<
480 Box<dyn tracing_subscriber::Layer<tracing_subscriber::Registry> + Send + Sync + 'static>,
481 >,
482 extra_metric_readers: Vec<MeterProviderInstaller>,
483 runtime_metrics: bool,
484 #[cfg(feature = "grpc-mtls")]
485 mtls: Option<MtlsMaterial>,
486 propagated_span_fields: &'static [&'static str],
487 #[cfg(feature = "profiling")]
488 pyroscope_endpoint: Option<String>,
489}
490
491type MeterProviderInstaller =
496 Box<dyn FnOnce(MeterProviderBuilder) -> MeterProviderBuilder + Send + Sync>;
497
498impl TelemetryBuilder {
499 pub fn with_log_filter(mut self, directive: impl Into<String>) -> Self {
504 self.log_filter = Some(directive.into());
505 self
506 }
507
508 pub fn with_log_format(mut self, format: LogFormat) -> Self {
510 self.log_format = format;
511 self
512 }
513
514 pub fn with_version(mut self, version: &str) -> Self {
516 self.service_version = Some(version.to_string());
517 self
518 }
519
520 pub fn with_environment(mut self, environment: &str) -> Self {
522 self.deployment_environment = Some(environment.to_string());
523 self
524 }
525
526 #[cfg(feature = "grpc-mtls")]
551 pub fn with_mtls(mut self, material: MtlsMaterial) -> Self {
552 self.mtls = Some(material);
553 self.protocol = Some(ExportProtocol::Grpc);
554 self
555 }
556
557 pub fn with_sampler(mut self, sampler: TraceSampler) -> Self {
560 self.sampler = Some(sampler);
561 self
562 }
563
564 pub fn with_metrics(mut self, enabled: bool) -> Self {
566 self.metrics = enabled;
567 self
568 }
569
570 pub fn with_runtime_metrics(mut self, enabled: bool) -> Self {
586 self.runtime_metrics = enabled;
587 self
588 }
589
590 pub fn with_protocol(mut self, protocol: ExportProtocol) -> Self {
594 self.protocol = Some(protocol);
595 self
596 }
597
598 pub fn with_max_export_batch_size(mut self, size: usize) -> Self {
603 self.max_export_batch_size = Some(size);
604 self
605 }
606
607 pub fn with_metric_export_interval(mut self, interval: Duration) -> Self {
612 self.metric_export_interval = Some(interval);
613 self
614 }
615
616 pub fn with_logs(mut self, enabled: bool) -> Self {
623 self.logs = enabled;
624 self
625 }
626
627 pub fn with_propagated_span_fields(mut self, fields: &'static [&'static str]) -> Self {
640 self.propagated_span_fields = fields;
641 self
642 }
643
644 pub fn with_export_timeout(mut self, timeout: Duration) -> Self {
648 self.export_timeout = Some(timeout);
649 self
650 }
651
652 pub fn with_shutdown_timeout(mut self, timeout: Duration) -> Self {
659 self.shutdown_timeout = timeout;
660 self
661 }
662
663 #[cfg(feature = "profiling")]
681 pub fn with_profiling(mut self, endpoint: &str) -> Self {
682 self.pyroscope_endpoint = Some(endpoint.to_string());
683 self
684 }
685
686 pub fn with_meter_provider_setup<F>(mut self, setup: F) -> Self
743 where
744 F: FnOnce(MeterProviderBuilder) -> MeterProviderBuilder + Send + Sync + 'static,
745 {
746 self.extra_metric_readers.push(Box::new(setup));
747 self
748 }
749
750 pub fn with_layer<L>(mut self, layer: L) -> Self
751 where
752 L: tracing_subscriber::Layer<tracing_subscriber::Registry> + Send + Sync + 'static,
753 {
754 self.extra_layers.push(Box::new(layer));
755 self
756 }
757
758 pub fn init(self) -> Result<TelemetryHandles, Box<dyn Error>> {
773 let log_filter = match self.log_filter.as_deref() {
774 Some(directive) => tracing_subscriber::EnvFilter::try_new(directive)?,
775 None => tracing_subscriber::EnvFilter::from_default_env(),
776 };
777
778 if let Some(interval) = self.metric_export_interval
779 && interval.is_zero()
780 {
781 return Err("metric_export_interval must be greater than zero".into());
782 }
783
784 let protocol = self.protocol.or_else(protocol_from_env).unwrap_or({
785 #[cfg(feature = "grpc")]
786 {
787 ExportProtocol::Grpc
788 }
789 #[cfg(all(not(feature = "grpc"), feature = "http"))]
790 {
791 ExportProtocol::HttpProtobuf
792 }
793 });
794
795 let default_endpoint = match protocol {
796 #[cfg(feature = "grpc")]
797 ExportProtocol::Grpc => "http://localhost:4317",
798 #[cfg(feature = "http")]
799 ExportProtocol::HttpProtobuf => "http://localhost:4318",
800 };
801 let endpoint = std::env::var("OTEL_EXPORTER_OTLP_ENDPOINT")
802 .unwrap_or_else(|_| default_endpoint.to_string());
803
804 let export_timeout = self.export_timeout.or_else(timeout_from_env);
806
807 let service_name = self.service_name.unwrap_or_else(|| {
809 std::env::var("OTEL_SERVICE_NAME").unwrap_or_else(|_| "unknown_service".to_string())
810 });
811
812 let resource = build_resource(
813 &service_name,
814 self.service_version.as_deref(),
815 self.deployment_environment.as_deref(),
816 );
817
818 let sampler = match self.sampler {
819 Some(s) => s,
820 None => sampler_from_env()?.unwrap_or(TraceSampler::AlwaysOn),
821 };
822
823 let trace_exporter = build_span_exporter(
825 protocol,
826 &endpoint,
827 export_timeout,
828 #[cfg(feature = "grpc-mtls")]
829 self.mtls.as_ref(),
830 )?;
831
832 let batch_processor = if let Some(size) = self.max_export_batch_size {
833 BatchSpanProcessor::builder(trace_exporter)
834 .with_batch_config(
835 BatchConfigBuilder::default()
836 .with_max_export_batch_size(size)
837 .build(),
838 )
839 .build()
840 } else {
841 BatchSpanProcessor::builder(trace_exporter).build()
842 };
843
844 let tracer_provider = SdkTracerProvider::builder()
845 .with_resource(resource.clone())
846 .with_sampler(sampler.into_sdk_sampler())
847 .with_span_processor(batch_processor)
848 .build();
849
850 opentelemetry::global::set_tracer_provider(tracer_provider.clone());
851
852 let propagator = TextMapCompositePropagator::new(vec![
854 Box::new(TraceContextPropagator::new()),
855 Box::new(BaggagePropagator::new()),
856 ]);
857 opentelemetry::global::set_text_map_propagator(propagator);
858
859 let meter_provider = if self.metrics {
861 let metric_exporter = build_metric_exporter(
862 protocol,
863 &endpoint,
864 export_timeout,
865 #[cfg(feature = "grpc-mtls")]
866 self.mtls.as_ref(),
867 )?;
868
869 let periodic_reader = if let Some(interval) = self.metric_export_interval {
870 PeriodicReader::builder(metric_exporter)
871 .with_interval(interval)
872 .build()
873 } else {
874 PeriodicReader::builder(metric_exporter).build()
875 };
876
877 let mut mp_builder = SdkMeterProvider::builder()
878 .with_resource(resource.clone())
879 .with_reader(periodic_reader);
880 for installer in self.extra_metric_readers {
881 mp_builder = installer(mp_builder);
882 }
883 let mp = mp_builder.build();
884
885 opentelemetry::global::set_meter_provider(mp.clone());
886
887 if self.runtime_metrics {
891 crate::runtime_metrics::install();
892 }
893
894 Some(mp)
895 } else {
896 None
897 };
898
899 let logger_provider = if self.logs {
901 let log_exporter = build_log_exporter(
902 protocol,
903 &endpoint,
904 export_timeout,
905 #[cfg(feature = "grpc-mtls")]
906 self.mtls.as_ref(),
907 )?;
908
909 let lp = SdkLoggerProvider::builder()
910 .with_resource(resource)
911 .with_batch_exporter(log_exporter)
912 .build();
913
914 Some(lp)
915 } else {
916 None
917 };
918
919 #[cfg(feature = "profiling")]
921 let profiling_handle = if let Some(ref endpoint) = self.pyroscope_endpoint {
922 let identity = profiling::ProfilingIdentity {
927 host_name: hostname::get()
928 .ok()
929 .and_then(|h| h.into_string().ok())
930 .filter(|h| !h.is_empty()),
931 deployment_environment: self.deployment_environment.clone(),
932 service_version: self.service_version.clone(),
933 };
934 profiling::start_pyroscope_bridge(&service_name, endpoint, &identity)?
935 } else {
936 None
937 };
938 #[cfg(not(feature = "profiling"))]
939 let _profiling_handle: Option<()> = None;
940
941 let extra = if self.extra_layers.is_empty() {
947 None
948 } else {
949 Some(self.extra_layers)
950 };
951
952 macro_rules! install_subscriber {
953 ($fmt_layer:expr) => {{
954 let otel_layer = tracing_opentelemetry::layer()
958 .with_tracer(tracing_bridge_tracer(&tracer_provider));
959 let registry = tracing_subscriber::registry()
960 .with(extra)
961 .with(log_filter)
962 .with($fmt_layer)
963 .with(otel_layer);
964
965 #[cfg(feature = "profiling-bridge-pyroscope-rs")]
968 #[allow(deprecated)]
969 let registry = registry.with(crate::profiling::ProfilingTagLayer);
970
971 if let Some(lp) = &logger_provider {
972 if let Err(e) = registry
973 .with(crate::log_bridge::SpanAwareLogBridge::new(
974 lp,
975 self.propagated_span_fields,
976 ))
977 .try_init()
978 {
979 eprintln!(
980 "otel-bootstrap: global tracing subscriber already installed — \
981 OTLP log records will NOT be exported to the collector: {e}"
982 );
983 }
984 } else if let Err(e) = registry.try_init() {
985 eprintln!(
986 "otel-bootstrap: global tracing subscriber already installed — \
987 OTLP telemetry will NOT be exported to the collector: {e}"
988 );
989 }
990 }};
991 }
992
993 match self.log_format {
994 LogFormat::Pretty => install_subscriber!(tracing_subscriber::fmt::layer()),
995 LogFormat::Json => install_subscriber!(tracing_subscriber::fmt::layer().json()),
996 }
997
998 Ok(TelemetryHandles {
999 tracer_provider,
1000 meter_provider,
1001 logger_provider,
1002 shutdown_timeout: self.shutdown_timeout,
1003 #[cfg(feature = "profiling")]
1004 profiling_handle,
1005 })
1006 }
1007}
1008
1009pub fn init_telemetry(service_name: &str) -> Result<TelemetryHandles, Box<dyn Error>> {
1023 Telemetry::builder(service_name).init()
1024}
1025
1026pub fn init_telemetry_with_sampler(
1043 service_name: &str,
1044 sampler: Option<TraceSampler>,
1045) -> Result<TelemetryHandles, Box<dyn Error>> {
1046 let builder = Telemetry::builder(service_name);
1047 match sampler {
1048 Some(s) => builder.with_sampler(s),
1049 None => builder, }
1051 .init()
1052}
1053
1054fn timeout_from_env() -> Option<Duration> {
1056 let ms = std::env::var("OTEL_EXPORTER_OTLP_TIMEOUT").ok()?;
1057 let ms: u64 = ms.trim().parse().ok()?;
1058 Some(Duration::from_millis(ms))
1059}
1060
1061#[cfg(feature = "grpc-mtls")]
1068fn build_tls_config(material: &MtlsMaterial) -> tonic::transport::ClientTlsConfig {
1069 use tonic::transport::{Certificate, ClientTlsConfig, Identity};
1070 ClientTlsConfig::new()
1071 .ca_certificate(Certificate::from_pem(&material.trust_bundle_pem))
1072 .identity(Identity::from_pem(
1073 &material.client_cert_chain_pem,
1074 &material.client_key_pem,
1075 ))
1076}
1077
1078fn build_span_exporter(
1079 protocol: ExportProtocol,
1080 endpoint: &str,
1081 timeout: Option<Duration>,
1082 #[cfg(feature = "grpc-mtls")] mtls: Option<&MtlsMaterial>,
1083) -> Result<opentelemetry_otlp::SpanExporter, Box<dyn Error>> {
1084 match protocol {
1085 #[cfg(feature = "grpc")]
1086 ExportProtocol::Grpc => {
1087 let mut b = opentelemetry_otlp::SpanExporter::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::SpanExporter::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
1113fn build_metric_exporter(
1114 protocol: ExportProtocol,
1115 endpoint: &str,
1116 timeout: Option<Duration>,
1117 #[cfg(feature = "grpc-mtls")] mtls: Option<&MtlsMaterial>,
1118) -> Result<opentelemetry_otlp::MetricExporter, Box<dyn Error>> {
1119 match protocol {
1120 #[cfg(feature = "grpc")]
1121 ExportProtocol::Grpc => {
1122 let mut b = opentelemetry_otlp::MetricExporter::builder()
1123 .with_tonic()
1124 .with_endpoint(endpoint);
1125 if let Some(t) = timeout {
1126 b = b.with_timeout(t);
1127 }
1128 #[cfg(feature = "grpc-mtls")]
1129 if let Some(m) = mtls {
1130 use opentelemetry_otlp::WithTonicConfig as _;
1131 b = b.with_tls_config(build_tls_config(m));
1132 }
1133 Ok(b.build()?)
1134 }
1135 #[cfg(feature = "http")]
1136 ExportProtocol::HttpProtobuf => {
1137 let mut b = opentelemetry_otlp::MetricExporter::builder()
1138 .with_http()
1139 .with_endpoint(endpoint);
1140 if let Some(t) = timeout {
1141 b = b.with_timeout(t);
1142 }
1143 Ok(b.build()?)
1144 }
1145 }
1146}
1147
1148fn build_log_exporter(
1149 protocol: ExportProtocol,
1150 endpoint: &str,
1151 timeout: Option<Duration>,
1152 #[cfg(feature = "grpc-mtls")] mtls: Option<&MtlsMaterial>,
1153) -> Result<opentelemetry_otlp::LogExporter, Box<dyn Error>> {
1154 match protocol {
1155 #[cfg(feature = "grpc")]
1156 ExportProtocol::Grpc => {
1157 let mut b = opentelemetry_otlp::LogExporter::builder()
1158 .with_tonic()
1159 .with_endpoint(endpoint);
1160 if let Some(t) = timeout {
1161 b = b.with_timeout(t);
1162 }
1163 #[cfg(feature = "grpc-mtls")]
1164 if let Some(m) = mtls {
1165 use opentelemetry_otlp::WithTonicConfig as _;
1166 b = b.with_tls_config(build_tls_config(m));
1167 }
1168 Ok(b.build()?)
1169 }
1170 #[cfg(feature = "http")]
1171 ExportProtocol::HttpProtobuf => {
1172 let mut b = opentelemetry_otlp::LogExporter::builder()
1173 .with_http()
1174 .with_endpoint(endpoint);
1175 if let Some(t) = timeout {
1176 b = b.with_timeout(t);
1177 }
1178 Ok(b.build()?)
1179 }
1180 }
1181}
1182
1183pub fn build_resource(
1198 service_name: &str,
1199 service_version: Option<&str>,
1200 deployment_environment: Option<&str>,
1201) -> Resource {
1202 let hostname = hostname::get()
1203 .ok()
1204 .and_then(|h| h.into_string().ok())
1205 .unwrap_or_default();
1206
1207 let mut builder = Resource::builder()
1208 .with_service_name(service_name.to_string())
1209 .with_attributes([
1210 KeyValue::new(HOST_NAME, hostname),
1211 KeyValue::new(PROCESS_PID, std::process::id() as i64),
1212 ]);
1213
1214 if let Some(version) = service_version {
1215 builder = builder.with_attribute(KeyValue::new(SERVICE_VERSION, version.to_string()));
1216 }
1217
1218 if let Some(env) = deployment_environment {
1219 builder =
1220 builder.with_attribute(KeyValue::new(DEPLOYMENT_ENVIRONMENT_NAME, env.to_string()));
1221 }
1222
1223 builder.build()
1224}
1225
1226#[cfg(feature = "axum")]
1244pub fn axum_layer() -> axum_middleware::OtelTraceLayer {
1245 axum_middleware::OtelTraceLayer
1246}
1247
1248#[cfg(feature = "axum")]
1279pub fn span_enricher_layer<T>() -> axum_middleware::SpanEnricherLayer<T>
1280where
1281 T: span_enrichment::EnrichSpan + Clone + Send + Sync + 'static,
1282{
1283 axum_middleware::SpanEnricherLayer::default()
1284}
1285
1286#[cfg(feature = "tonic-tracing")]
1308pub fn grpc_client_layer() -> grpc_middleware::GrpcClientTraceLayer {
1309 grpc_middleware::GrpcClientTraceLayer
1310}
1311
1312#[cfg(feature = "tonic-tracing")]
1327pub fn grpc_server_layer() -> grpc_middleware::GrpcServerTraceLayer {
1328 grpc_middleware::GrpcServerTraceLayer
1329}
1330
1331#[cfg(test)]
1332mod tests {
1333 use super::*;
1334
1335 #[test]
1337 fn runtime_metrics_can_be_disabled() {
1338 assert!(
1339 Telemetry::builder("rm-default").runtime_metrics,
1340 "runtime metrics are on by default"
1341 );
1342 assert!(
1343 !Telemetry::builder("rm-off")
1344 .with_runtime_metrics(false)
1345 .runtime_metrics
1346 );
1347 }
1348
1349 #[tokio::test]
1357 async fn shutdown_absorbs_provider_errors() {
1358 let handles = TelemetryHandles {
1359 tracer_provider: SdkTracerProvider::builder().build(),
1360 meter_provider: Some(SdkMeterProvider::builder().build()),
1361 logger_provider: Some(SdkLoggerProvider::builder().build()),
1362 shutdown_timeout: DEFAULT_SHUTDOWN_TIMEOUT,
1363 #[cfg(feature = "profiling")]
1364 profiling_handle: None,
1365 };
1366
1367 handles.shutdown().expect("first shutdown succeeds");
1368 handles
1369 .shutdown()
1370 .expect("second shutdown absorbs the already-shut-down errors");
1371 }
1372 use opentelemetry::trace::{Span as _, Tracer as _};
1373 use std::sync::Mutex;
1374
1375 static ENV_LOCK: Mutex<()> = Mutex::new(());
1376
1377 #[test]
1378 fn tracing_bridge_uses_sdk_tracer() {
1379 let provider = SdkTracerProvider::builder().build();
1380 let tracer = tracing_bridge_tracer(&provider);
1381 let span = tracer.start("bridge-regression");
1382
1383 assert!(span.span_context().is_valid());
1384
1385 provider.shutdown().expect("provider shutdown");
1386 }
1387
1388 #[test]
1389 fn resource_contains_all_attributes_when_provided() {
1390 let resource = build_resource("test-svc", Some("1.2.3"), Some("staging"));
1391
1392 assert_eq!(
1393 resource.get(&opentelemetry::Key::new("service.name")),
1394 Some(opentelemetry::Value::from("test-svc")),
1395 );
1396 assert_eq!(
1397 resource.get(&opentelemetry::Key::new(SERVICE_VERSION)),
1398 Some(opentelemetry::Value::from("1.2.3")),
1399 );
1400 assert_eq!(
1401 resource.get(&opentelemetry::Key::new(DEPLOYMENT_ENVIRONMENT_NAME)),
1402 Some(opentelemetry::Value::from("staging")),
1403 );
1404 assert!(resource.get(&opentelemetry::Key::new(HOST_NAME)).is_some());
1405 assert!(
1406 resource
1407 .get(&opentelemetry::Key::new(PROCESS_PID))
1408 .is_some()
1409 );
1410 }
1411
1412 #[test]
1413 fn resource_graceful_when_optional_values_omitted() {
1414 let resource = build_resource("test-svc", None, None);
1415
1416 assert_eq!(
1417 resource.get(&opentelemetry::Key::new("service.name")),
1418 Some(opentelemetry::Value::from("test-svc")),
1419 );
1420 assert!(
1421 resource
1422 .get(&opentelemetry::Key::new(SERVICE_VERSION))
1423 .is_none()
1424 );
1425 assert!(
1426 resource
1427 .get(&opentelemetry::Key::new(DEPLOYMENT_ENVIRONMENT_NAME))
1428 .is_none()
1429 );
1430 assert!(resource.get(&opentelemetry::Key::new(HOST_NAME)).is_some());
1432 assert!(
1433 resource
1434 .get(&opentelemetry::Key::new(PROCESS_PID))
1435 .is_some()
1436 );
1437 }
1438
1439 #[test]
1440 fn trace_sampler_ratio_converts_to_sdk() {
1441 let sampler = TraceSampler::TraceIdRatio(0.5);
1442 let sdk = sampler.into_sdk_sampler();
1443 assert_eq!(format!("{sdk:?}"), "TraceIdRatioBased(0.5)");
1444 }
1445
1446 #[test]
1447 fn trace_sampler_parent_based_converts_to_sdk() {
1448 let sampler = TraceSampler::ParentBased(Box::new(TraceSampler::TraceIdRatio(0.25)));
1449 let sdk = sampler.into_sdk_sampler();
1450 let debug = format!("{sdk:?}");
1451 assert!(debug.contains("ParentBased"));
1452 assert!(debug.contains("0.25"));
1453 }
1454
1455 unsafe fn set_env(key: &str, val: &str) {
1457 unsafe {
1458 std::env::set_var(key, val);
1459 }
1460 }
1461
1462 unsafe fn remove_env(key: &str) {
1463 unsafe {
1464 std::env::remove_var(key);
1465 }
1466 }
1467
1468 #[test]
1469 fn sampler_from_env_reads_traceidratio() {
1470 let _lock = ENV_LOCK.lock().unwrap();
1471 unsafe {
1472 set_env("OTEL_TRACES_SAMPLER", "traceidratio");
1473 set_env("OTEL_TRACES_SAMPLER_ARG", "0.42");
1474 }
1475
1476 let sampler = sampler_from_env()
1477 .expect("should not error")
1478 .expect("should return Some");
1479 assert!(
1480 matches!(sampler, TraceSampler::TraceIdRatio(r) if (r - 0.42).abs() < f64::EPSILON)
1481 );
1482
1483 unsafe {
1484 remove_env("OTEL_TRACES_SAMPLER");
1485 remove_env("OTEL_TRACES_SAMPLER_ARG");
1486 }
1487 }
1488
1489 #[test]
1490 fn sampler_from_env_returns_none_when_unset() {
1491 let _lock = ENV_LOCK.lock().unwrap();
1492 unsafe {
1493 remove_env("OTEL_TRACES_SAMPLER");
1494 }
1495 assert!(sampler_from_env().expect("should not error").is_none());
1496 }
1497
1498 #[test]
1499 fn sampler_from_env_reads_parentbased_traceidratio() {
1500 let _lock = ENV_LOCK.lock().unwrap();
1501 unsafe {
1502 set_env("OTEL_TRACES_SAMPLER", "parentbased_traceidratio");
1503 set_env("OTEL_TRACES_SAMPLER_ARG", "0.1");
1504 }
1505
1506 let sampler = sampler_from_env()
1507 .expect("should not error")
1508 .expect("should return Some");
1509 assert!(
1510 matches!(sampler, TraceSampler::ParentBased(inner) if matches!(*inner, TraceSampler::TraceIdRatio(r) if (r - 0.1).abs() < f64::EPSILON))
1511 );
1512
1513 unsafe {
1514 remove_env("OTEL_TRACES_SAMPLER");
1515 remove_env("OTEL_TRACES_SAMPLER_ARG");
1516 }
1517 }
1518
1519 #[test]
1520 fn sampler_from_env_parentbased_always_on() {
1521 let _lock = ENV_LOCK.lock().unwrap();
1522 unsafe {
1523 set_env("OTEL_TRACES_SAMPLER", "parentbased_always_on");
1524 }
1525 let sampler = sampler_from_env()
1526 .expect("should not error")
1527 .expect("should return Some");
1528 assert!(
1529 matches!(sampler, TraceSampler::ParentBased(inner) if matches!(*inner, TraceSampler::AlwaysOn))
1530 );
1531 unsafe {
1532 remove_env("OTEL_TRACES_SAMPLER");
1533 }
1534 }
1535
1536 #[test]
1537 fn sampler_from_env_parentbased_always_off() {
1538 let _lock = ENV_LOCK.lock().unwrap();
1539 unsafe {
1540 set_env("OTEL_TRACES_SAMPLER", "parentbased_always_off");
1541 }
1542 let sampler = sampler_from_env()
1543 .expect("should not error")
1544 .expect("should return Some");
1545 assert!(
1546 matches!(sampler, TraceSampler::ParentBased(inner) if matches!(*inner, TraceSampler::AlwaysOff))
1547 );
1548 unsafe {
1549 remove_env("OTEL_TRACES_SAMPLER");
1550 }
1551 }
1552
1553 #[test]
1554 fn sampler_from_env_always_on() {
1555 let _lock = ENV_LOCK.lock().unwrap();
1556 unsafe {
1557 set_env("OTEL_TRACES_SAMPLER", "always_on");
1558 }
1559 let sampler = sampler_from_env()
1560 .expect("should not error")
1561 .expect("should return Some");
1562 assert!(matches!(sampler, TraceSampler::AlwaysOn));
1563 unsafe {
1564 remove_env("OTEL_TRACES_SAMPLER");
1565 }
1566 }
1567
1568 #[test]
1569 fn sampler_from_env_always_off() {
1570 let _lock = ENV_LOCK.lock().unwrap();
1571 unsafe {
1572 set_env("OTEL_TRACES_SAMPLER", "always_off");
1573 }
1574 let sampler = sampler_from_env()
1575 .expect("should not error")
1576 .expect("should return Some");
1577 assert!(matches!(sampler, TraceSampler::AlwaysOff));
1578 unsafe {
1579 remove_env("OTEL_TRACES_SAMPLER");
1580 }
1581 }
1582
1583 #[test]
1584 fn sampler_from_env_unknown_returns_error() {
1585 let _lock = ENV_LOCK.lock().unwrap();
1586 unsafe {
1587 set_env("OTEL_TRACES_SAMPLER", "unknown_sampler");
1588 }
1589 let err = sampler_from_env().expect_err("unknown sampler should produce an error");
1590 assert!(
1591 err.to_string().contains("unknown_sampler"),
1592 "error message should include the unknown name, got: {err}"
1593 );
1594 unsafe {
1595 remove_env("OTEL_TRACES_SAMPLER");
1596 }
1597 }
1598
1599 #[test]
1600 fn trace_sampler_always_on_converts_to_sdk() {
1601 let sdk = TraceSampler::AlwaysOn.into_sdk_sampler();
1602 assert_eq!(format!("{sdk:?}"), "AlwaysOn");
1603 }
1604
1605 #[test]
1606 fn trace_sampler_always_off_converts_to_sdk() {
1607 let sdk = TraceSampler::AlwaysOff.into_sdk_sampler();
1608 assert_eq!(format!("{sdk:?}"), "AlwaysOff");
1609 }
1610
1611 #[test]
1612 fn builder_has_sensible_defaults() {
1613 let builder = Telemetry::builder("test-svc");
1614 assert_eq!(builder.service_name.as_deref(), Some("test-svc"));
1615 assert!(builder.service_version.is_none());
1616 assert!(builder.deployment_environment.is_none());
1617 assert!(builder.sampler.is_none());
1618 assert!(builder.metrics);
1619 assert!(!builder.logs);
1620 assert!(builder.protocol.is_none());
1621 assert!(builder.max_export_batch_size.is_none());
1622 assert!(builder.metric_export_interval.is_none());
1623 assert!(builder.export_timeout.is_none());
1624 }
1625
1626 #[test]
1627 fn from_env_builder_has_no_service_name() {
1628 let builder = Telemetry::from_env();
1629 assert!(builder.service_name.is_none());
1630 }
1631
1632 #[test]
1633 fn with_export_timeout_stores_value() {
1634 let timeout = Duration::from_secs(5);
1635 let builder = Telemetry::builder("test-svc").with_export_timeout(timeout);
1636 assert_eq!(builder.export_timeout, Some(timeout));
1637 }
1638
1639 #[test]
1640 fn timeout_from_env_reads_milliseconds() {
1641 let _lock = ENV_LOCK.lock().unwrap();
1642 unsafe {
1643 set_env("OTEL_EXPORTER_OTLP_TIMEOUT", "5000");
1644 }
1645 let t = timeout_from_env();
1646 assert_eq!(t, Some(Duration::from_millis(5000)));
1647 unsafe {
1648 remove_env("OTEL_EXPORTER_OTLP_TIMEOUT");
1649 }
1650 }
1651
1652 #[test]
1653 fn timeout_from_env_returns_none_when_unset() {
1654 let _lock = ENV_LOCK.lock().unwrap();
1655 unsafe {
1656 remove_env("OTEL_EXPORTER_OTLP_TIMEOUT");
1657 }
1658 assert_eq!(timeout_from_env(), None);
1659 }
1660
1661 #[test]
1662 fn service_name_from_env_used_when_none_given() {
1663 let builder = Telemetry::from_env();
1664 assert!(builder.service_name.is_none());
1665 }
1666
1667 #[test]
1668 fn explicit_service_name_overrides_env_var() {
1669 let builder = Telemetry::builder("explicit-svc");
1670 assert_eq!(builder.service_name.as_deref(), Some("explicit-svc"));
1671 }
1672
1673 #[test]
1674 fn from_env_builder_service_name_is_none() {
1675 let builder = Telemetry::from_env();
1676 assert!(builder.service_name.is_none());
1677 }
1678
1679 #[test]
1680 fn init_returns_error_for_unknown_otel_traces_sampler() {
1681 let _lock = ENV_LOCK.lock().unwrap();
1682 unsafe {
1683 set_env("OTEL_TRACES_SAMPLER", "not_a_real_sampler");
1684 }
1685 let result = Telemetry::builder("test-svc").with_metrics(false).init();
1686 let err = result
1687 .err()
1688 .expect("unknown sampler env var should cause init to fail");
1689 assert!(
1690 err.to_string().contains("not_a_real_sampler"),
1691 "error should name the unknown sampler, got: {err}"
1692 );
1693 unsafe {
1694 remove_env("OTEL_TRACES_SAMPLER");
1695 }
1696 }
1697
1698 #[test]
1699 fn with_max_export_batch_size_stores_value() {
1700 let builder = Telemetry::builder("test-svc").with_max_export_batch_size(1024);
1701 assert_eq!(builder.max_export_batch_size, Some(1024));
1702 }
1703
1704 #[test]
1705 fn with_metric_export_interval_stores_value() {
1706 let interval = Duration::from_secs(30);
1707 let builder = Telemetry::builder("test-svc").with_metric_export_interval(interval);
1708 assert_eq!(builder.metric_export_interval, Some(interval));
1709 }
1710
1711 #[test]
1712 fn init_rejects_zero_metric_export_interval() {
1713 let err = Telemetry::builder("test-svc")
1714 .with_metric_export_interval(Duration::ZERO)
1715 .with_metrics(false)
1716 .init()
1717 .err()
1718 .expect("expected error for zero interval");
1719 assert!(
1720 err.to_string().contains("metric_export_interval"),
1721 "error message should mention metric_export_interval, got: {err}"
1722 );
1723 }
1724
1725 #[test]
1726 fn builder_with_custom_values() {
1727 let builder = Telemetry::builder("test-svc")
1728 .with_version("2.0.0")
1729 .with_environment("production")
1730 .with_sampler(TraceSampler::TraceIdRatio(0.5))
1731 .with_metrics(false);
1732
1733 assert_eq!(builder.service_name.as_deref(), Some("test-svc"));
1734 assert_eq!(builder.service_version.as_deref(), Some("2.0.0"));
1735 assert_eq!(
1736 builder.deployment_environment.as_deref(),
1737 Some("production")
1738 );
1739 assert!(
1740 matches!(builder.sampler, Some(TraceSampler::TraceIdRatio(r)) if (r - 0.5).abs() < f64::EPSILON)
1741 );
1742 assert!(!builder.metrics);
1743 }
1744
1745 #[test]
1746 fn builder_stores_programmatic_log_configuration() {
1747 let builder = Telemetry::builder("test-svc")
1748 .with_log_filter("info,opentelemetry_sdk=warn")
1749 .with_log_format(LogFormat::Json);
1750
1751 assert_eq!(
1752 builder.log_filter.as_deref(),
1753 Some("info,opentelemetry_sdk=warn")
1754 );
1755 assert_eq!(builder.log_format, LogFormat::Json);
1756 }
1757
1758 #[test]
1759 fn init_rejects_invalid_programmatic_log_filter_before_provider_setup() {
1760 let setup_ran = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
1761 let setup_ran_in_closure = std::sync::Arc::clone(&setup_ran);
1762
1763 let error = Telemetry::builder("test-svc")
1764 .with_log_filter("[")
1765 .with_meter_provider_setup(move |builder| {
1766 setup_ran_in_closure.store(true, std::sync::atomic::Ordering::SeqCst);
1767 builder
1768 })
1769 .init()
1770 .err()
1771 .expect("invalid filter must fail initialization");
1772
1773 assert!(error.to_string().contains("invalid filter directive"));
1774 assert!(!setup_ran.load(std::sync::atomic::Ordering::SeqCst));
1775 }
1776
1777 #[test]
1778 #[cfg(feature = "grpc")]
1779 fn builder_with_protocol_grpc() {
1780 let builder = Telemetry::builder("test-svc").with_protocol(ExportProtocol::Grpc);
1781 assert_eq!(builder.protocol, Some(ExportProtocol::Grpc));
1782 }
1783
1784 #[test]
1785 #[cfg(feature = "http")]
1786 fn builder_with_protocol_http() {
1787 let builder = Telemetry::builder("test-svc").with_protocol(ExportProtocol::HttpProtobuf);
1788 assert_eq!(builder.protocol, Some(ExportProtocol::HttpProtobuf));
1789 }
1790
1791 #[test]
1792 #[cfg(feature = "grpc")]
1793 fn protocol_from_env_reads_grpc() {
1794 let _lock = ENV_LOCK.lock().unwrap();
1795 unsafe {
1796 set_env("OTEL_EXPORTER_OTLP_PROTOCOL", "grpc");
1797 }
1798 assert_eq!(protocol_from_env(), Some(ExportProtocol::Grpc));
1799 unsafe {
1800 remove_env("OTEL_EXPORTER_OTLP_PROTOCOL");
1801 }
1802 }
1803
1804 #[test]
1805 #[cfg(feature = "http")]
1806 fn protocol_from_env_reads_http_protobuf() {
1807 let _lock = ENV_LOCK.lock().unwrap();
1808 unsafe {
1809 set_env("OTEL_EXPORTER_OTLP_PROTOCOL", "http/protobuf");
1810 }
1811 assert_eq!(protocol_from_env(), Some(ExportProtocol::HttpProtobuf));
1812 unsafe {
1813 remove_env("OTEL_EXPORTER_OTLP_PROTOCOL");
1814 }
1815 }
1816
1817 #[test]
1818 fn protocol_from_env_returns_none_when_unset() {
1819 let _lock = ENV_LOCK.lock().unwrap();
1820 unsafe {
1821 remove_env("OTEL_EXPORTER_OTLP_PROTOCOL");
1822 }
1823 assert_eq!(protocol_from_env(), None);
1824 }
1825
1826 #[test]
1827 fn protocol_from_env_returns_none_for_unknown() {
1828 let _lock = ENV_LOCK.lock().unwrap();
1829 unsafe {
1830 set_env("OTEL_EXPORTER_OTLP_PROTOCOL", "websocket");
1831 }
1832 assert_eq!(protocol_from_env(), None);
1833 unsafe {
1834 remove_env("OTEL_EXPORTER_OTLP_PROTOCOL");
1835 }
1836 }
1837
1838 #[test]
1839 fn builder_is_send_and_sync() {
1840 fn assert_send_sync<T: Send + Sync>() {}
1841 assert_send_sync::<TelemetryBuilder>();
1842 }
1843
1844 #[test]
1845 fn with_shutdown_timeout_stores_value() {
1846 let timeout = Duration::from_secs(10);
1847 let builder = Telemetry::builder("test-svc").with_shutdown_timeout(timeout);
1848 assert_eq!(builder.shutdown_timeout, timeout);
1849 }
1850
1851 #[test]
1852 fn default_shutdown_timeout_is_five_seconds() {
1853 let builder = Telemetry::builder("test-svc");
1854 assert_eq!(builder.shutdown_timeout, Duration::from_secs(5));
1855 }
1856
1857 #[cfg(feature = "testing")]
1865 #[test]
1866 fn drop_completes_within_shutdown_timeout() {
1867 let mut handles = crate::Telemetry::testing("drop-timeout-test");
1869 handles.shutdown_timeout = Duration::from_millis(100);
1871
1872 let start = std::time::Instant::now();
1873 drop(handles);
1874 let elapsed = start.elapsed();
1875
1876 assert!(
1878 elapsed < Duration::from_millis(500),
1879 "drop took {elapsed:?}, expected < 500 ms"
1880 );
1881 }
1882}