construct/observability/
mod.rs1pub 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
27pub 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}