Skip to main content

construct/observability/
mod.rs

1pub mod log;
2pub mod multi;
3pub mod noop;
4#[cfg(feature = "observability-otel")]
5pub mod otel;
6#[cfg(feature = "observability-prometheus")]
7pub mod prometheus;
8pub mod runtime_trace;
9pub mod traits;
10pub mod verbose;
11
12#[allow(unused_imports)]
13pub use self::log::LogObserver;
14#[allow(unused_imports)]
15pub use self::multi::MultiObserver;
16pub use noop::NoopObserver;
17#[cfg(feature = "observability-otel")]
18pub use otel::OtelObserver;
19#[cfg(feature = "observability-prometheus")]
20pub use prometheus::PrometheusObserver;
21pub use traits::{Observer, ObserverEvent};
22#[allow(unused_imports)]
23pub use verbose::VerboseObserver;
24
25use crate::config::ObservabilityConfig;
26
27/// Factory: create the right observer from config
28pub fn create_observer(config: &ObservabilityConfig) -> Box<dyn Observer> {
29    match config.backend.as_str() {
30        "log" => Box::new(LogObserver::new()),
31        "verbose" => Box::new(VerboseObserver::new()),
32        "prometheus" => {
33            #[cfg(feature = "observability-prometheus")]
34            {
35                Box::new(PrometheusObserver::new())
36            }
37            #[cfg(not(feature = "observability-prometheus"))]
38            {
39                tracing::warn!(
40                    "Prometheus backend requested but this build was compiled without `observability-prometheus`; falling back to noop."
41                );
42                Box::new(NoopObserver)
43            }
44        }
45        "otel" | "opentelemetry" | "otlp" => {
46            #[cfg(feature = "observability-otel")]
47            match OtelObserver::new(
48                config.otel_endpoint.as_deref(),
49                config.otel_service_name.as_deref(),
50            ) {
51                Ok(obs) => {
52                    tracing::info!(
53                        endpoint = config
54                            .otel_endpoint
55                            .as_deref()
56                            .unwrap_or("http://localhost:4318"),
57                        "OpenTelemetry observer initialized"
58                    );
59                    Box::new(obs)
60                }
61                Err(e) => {
62                    tracing::error!("Failed to create OTel observer: {e}. Falling back to noop.");
63                    Box::new(NoopObserver)
64                }
65            }
66            #[cfg(not(feature = "observability-otel"))]
67            {
68                tracing::warn!(
69                    "OpenTelemetry backend requested but this build was compiled without `observability-otel`; falling back to noop."
70                );
71                Box::new(NoopObserver)
72            }
73        }
74        "none" | "noop" => Box::new(NoopObserver),
75        _ => {
76            tracing::warn!(
77                "Unknown observability backend '{}', falling back to noop",
78                config.backend
79            );
80            Box::new(NoopObserver)
81        }
82    }
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88
89    #[test]
90    fn factory_none_returns_noop() {
91        let cfg = ObservabilityConfig {
92            backend: "none".into(),
93            ..ObservabilityConfig::default()
94        };
95        assert_eq!(create_observer(&cfg).name(), "noop");
96    }
97
98    #[test]
99    fn factory_noop_returns_noop() {
100        let cfg = ObservabilityConfig {
101            backend: "noop".into(),
102            ..ObservabilityConfig::default()
103        };
104        assert_eq!(create_observer(&cfg).name(), "noop");
105    }
106
107    #[test]
108    fn factory_log_returns_log() {
109        let cfg = ObservabilityConfig {
110            backend: "log".into(),
111            ..ObservabilityConfig::default()
112        };
113        assert_eq!(create_observer(&cfg).name(), "log");
114    }
115
116    #[test]
117    fn factory_verbose_returns_verbose() {
118        let cfg = ObservabilityConfig {
119            backend: "verbose".into(),
120            ..ObservabilityConfig::default()
121        };
122        assert_eq!(create_observer(&cfg).name(), "verbose");
123    }
124
125    #[test]
126    fn factory_prometheus_returns_prometheus() {
127        let cfg = ObservabilityConfig {
128            backend: "prometheus".into(),
129            ..ObservabilityConfig::default()
130        };
131        let expected = if cfg!(feature = "observability-prometheus") {
132            "prometheus"
133        } else {
134            "noop"
135        };
136        assert_eq!(create_observer(&cfg).name(), expected);
137    }
138
139    #[test]
140    fn factory_otel_returns_otel() {
141        let cfg = ObservabilityConfig {
142            backend: "otel".into(),
143            otel_endpoint: Some("http://127.0.0.1:19999".into()),
144            otel_service_name: Some("test".into()),
145            ..ObservabilityConfig::default()
146        };
147        let expected = if cfg!(feature = "observability-otel") {
148            "otel"
149        } else {
150            "noop"
151        };
152        assert_eq!(create_observer(&cfg).name(), expected);
153    }
154
155    #[test]
156    fn factory_opentelemetry_alias() {
157        let cfg = ObservabilityConfig {
158            backend: "opentelemetry".into(),
159            otel_endpoint: Some("http://127.0.0.1:19999".into()),
160            otel_service_name: Some("test".into()),
161            ..ObservabilityConfig::default()
162        };
163        let expected = if cfg!(feature = "observability-otel") {
164            "otel"
165        } else {
166            "noop"
167        };
168        assert_eq!(create_observer(&cfg).name(), expected);
169    }
170
171    #[test]
172    fn factory_otlp_alias() {
173        let cfg = ObservabilityConfig {
174            backend: "otlp".into(),
175            otel_endpoint: Some("http://127.0.0.1:19999".into()),
176            otel_service_name: Some("test".into()),
177            ..ObservabilityConfig::default()
178        };
179        let expected = if cfg!(feature = "observability-otel") {
180            "otel"
181        } else {
182            "noop"
183        };
184        assert_eq!(create_observer(&cfg).name(), expected);
185    }
186
187    #[test]
188    fn factory_unknown_falls_back_to_noop() {
189        let cfg = ObservabilityConfig {
190            backend: "xyzzy_unknown".into(),
191            ..ObservabilityConfig::default()
192        };
193        assert_eq!(create_observer(&cfg).name(), "noop");
194    }
195
196    #[test]
197    fn factory_empty_string_falls_back_to_noop() {
198        let cfg = ObservabilityConfig {
199            backend: String::new(),
200            ..ObservabilityConfig::default()
201        };
202        assert_eq!(create_observer(&cfg).name(), "noop");
203    }
204
205    #[test]
206    fn factory_garbage_falls_back_to_noop() {
207        let cfg = ObservabilityConfig {
208            backend: "xyzzy_garbage_123".into(),
209            ..ObservabilityConfig::default()
210        };
211        assert_eq!(create_observer(&cfg).name(), "noop");
212    }
213}