rs_zero/observability/
otel.rs1use thiserror::Error;
2use tracing_subscriber::{EnvFilter, fmt, fmt::format::FmtSpan};
3
4use crate::observability::{OpenTelemetryConfig, TraceExporter, TraceShutdownHandle};
5
6pub type ObservabilityResult<T> = Result<T, ObservabilityError>;
8
9#[derive(Debug, Error, PartialEq, Eq)]
11pub enum ObservabilityError {
12 #[error("tracing subscriber is already initialized")]
14 SubscriberAlreadyInitialized,
15
16 #[error("otlp exporter endpoint is required")]
18 MissingOtlpEndpoint,
19
20 #[error("observability exporter error: {0}")]
22 ExporterInstall(String),
23}
24
25pub fn init_opentelemetry_tracing(config: OpenTelemetryConfig) -> ObservabilityResult<()> {
27 init_opentelemetry_tracing_with_handle(config).map(|_| ())
28}
29
30pub fn init_opentelemetry_tracing_with_handle(
32 config: OpenTelemetryConfig,
33) -> ObservabilityResult<TraceShutdownHandle> {
34 match config.exporter {
35 TraceExporter::Disabled => Ok(TraceShutdownHandle::disabled()),
36 TraceExporter::Stdout => {
37 let filter =
38 EnvFilter::try_new(config.filter).unwrap_or_else(|_| EnvFilter::new("info"));
39 fmt()
40 .with_env_filter(filter)
41 .with_span_events(FmtSpan::CLOSE)
42 .try_init()
43 .map_err(|_| ObservabilityError::SubscriberAlreadyInitialized)?;
44 Ok(TraceShutdownHandle::installed())
45 }
46 TraceExporter::Otlp { endpoint } => init_otlp(endpoint, config.filter, config.timeout),
47 }
48}
49
50#[cfg(feature = "otlp")]
51fn init_otlp(
52 endpoint: String,
53 filter: String,
54 timeout: std::time::Duration,
55) -> ObservabilityResult<TraceShutdownHandle> {
56 crate::observability::install_otlp_tracing(
57 crate::observability::OtlpTraceConfig {
58 endpoint,
59 timeout,
60 ..crate::observability::OtlpTraceConfig::default()
61 },
62 filter,
63 )
64}
65
66#[cfg(not(feature = "otlp"))]
67fn init_otlp(
68 endpoint: String,
69 _filter: String,
70 _timeout: std::time::Duration,
71) -> ObservabilityResult<TraceShutdownHandle> {
72 if endpoint.trim().is_empty() {
73 Err(ObservabilityError::MissingOtlpEndpoint)
74 } else {
75 Err(ObservabilityError::ExporterInstall(
76 "enable the `otlp` feature to install an OTLP exporter".to_string(),
77 ))
78 }
79}
80
81#[cfg(test)]
82mod tests {
83 use super::{ObservabilityError, init_opentelemetry_tracing};
84 use crate::observability::{OpenTelemetryConfig, TraceExporter};
85
86 #[test]
87 fn disabled_exporter_does_not_install_subscriber() {
88 init_opentelemetry_tracing(OpenTelemetryConfig::default()).expect("disabled");
89 }
90
91 #[test]
92 fn otlp_requires_endpoint() {
93 let error = init_opentelemetry_tracing(OpenTelemetryConfig {
94 exporter: TraceExporter::Otlp {
95 endpoint: String::new(),
96 },
97 ..OpenTelemetryConfig::default()
98 })
99 .expect_err("missing endpoint");
100 assert_eq!(error, ObservabilityError::MissingOtlpEndpoint);
101 }
102}