Skip to main content

observability_core/
noop.rs

1//! Zero-cost no-op implementations for observability when features are disabled
2
3use crate::error::ObservabilityResult;
4#[cfg(feature = "structured-logging")]
5use crate::traits::StructuredLogger;
6use crate::traits::{LogLevel, MetricsCollector, ObservabilityPlugin, SpanGuard, SpanStatus};
7use std::collections::HashMap;
8use std::sync::Arc;
9use web_time::Duration;
10
11#[cfg(feature = "structured-logging")]
12use serde_json::Value as JsonValue;
13
14/// No-op observability plugin that does nothing
15///
16/// This implementation provides zero runtime cost when observability is disabled.
17/// All methods are inlined and compile to nothing in release builds.
18pub struct NoOpObservabilityPlugin;
19
20impl NoOpObservabilityPlugin {
21    /// Create a new no-op plugin
22    #[inline]
23    pub fn new() -> Self {
24        Self
25    }
26
27    /// Create an Arc'd no-op plugin for sharing
28    #[inline]
29    pub fn shared() -> Arc<Self> {
30        Arc::new(Self)
31    }
32}
33
34impl Default for NoOpObservabilityPlugin {
35    #[inline]
36    fn default() -> Self {
37        Self::new()
38    }
39}
40
41impl ObservabilityPlugin for NoOpObservabilityPlugin {
42    #[inline]
43    fn start_span(&self, _name: &str, _attributes: &[(&str, &str)]) -> SpanGuard {
44        SpanGuard::no_op()
45    }
46
47    #[inline]
48    fn end_span(&self, _span_id: &str) {
49        // No-op
50    }
51
52    #[inline]
53    fn add_span_attribute(&self, _span_id: &str, _key: &str, _value: &str) {
54        // No-op
55    }
56
57    #[inline]
58    fn set_span_status(&self, _span_id: &str, _status: SpanStatus) {
59        // No-op
60    }
61
62    #[inline]
63    fn record_metric(&self, _name: &str, _value: f64, _labels: &[(&str, &str)]) {
64        // No-op
65    }
66
67    #[cfg(feature = "structured-logging")]
68    #[inline]
69    fn log_structured(&self, _level: LogLevel, _message: &str, _fields: &JsonValue) {
70        // No-op
71    }
72
73    #[inline]
74    fn write_log(&self, _message: &str) {
75        // No-op - could optionally write to stdout in debug builds
76        #[cfg(debug_assertions)]
77        {
78            // Uncomment for debug logging in development
79            // println!("{}", _message);
80        }
81    }
82
83    #[inline]
84    fn flush(&self) -> ObservabilityResult<()> {
85        Ok(())
86    }
87
88    #[inline]
89    fn is_enabled(&self) -> bool {
90        false
91    }
92
93    #[inline]
94    fn plugin_type(&self) -> &'static str {
95        "noop"
96    }
97}
98
99/// No-op metrics collector
100pub struct NoOpMetricsCollector;
101
102impl NoOpMetricsCollector {
103    #[inline]
104    pub fn new() -> Self {
105        Self
106    }
107}
108
109impl Default for NoOpMetricsCollector {
110    #[inline]
111    fn default() -> Self {
112        Self::new()
113    }
114}
115
116impl MetricsCollector for NoOpMetricsCollector {
117    #[inline]
118    fn register_counter(
119        &mut self,
120        _name: &str,
121        _description: &str,
122        _labels: &[&str],
123    ) -> ObservabilityResult<()> {
124        Ok(())
125    }
126
127    #[inline]
128    fn register_histogram(
129        &mut self,
130        _name: &str,
131        _description: &str,
132        _buckets: &[f64],
133        _labels: &[&str],
134    ) -> ObservabilityResult<()> {
135        Ok(())
136    }
137
138    #[inline]
139    fn register_gauge(
140        &mut self,
141        _name: &str,
142        _description: &str,
143        _labels: &[&str],
144    ) -> ObservabilityResult<()> {
145        Ok(())
146    }
147
148    #[inline]
149    fn record_counter(
150        &self,
151        _name: &str,
152        _value: f64,
153        _labels: &HashMap<String, String>,
154    ) -> ObservabilityResult<()> {
155        Ok(())
156    }
157
158    #[inline]
159    fn record_histogram(
160        &self,
161        _name: &str,
162        _value: f64,
163        _labels: &HashMap<String, String>,
164    ) -> ObservabilityResult<()> {
165        Ok(())
166    }
167
168    #[inline]
169    fn set_gauge(
170        &self,
171        _name: &str,
172        _value: f64,
173        _labels: &HashMap<String, String>,
174    ) -> ObservabilityResult<()> {
175        Ok(())
176    }
177
178    #[inline]
179    fn get_metrics(&self) -> HashMap<String, f64> {
180        HashMap::new()
181    }
182
183    #[inline]
184    fn clear(&mut self) {
185        // No-op
186    }
187}
188
189/// No-op structured logger
190#[cfg(feature = "structured-logging")]
191pub struct NoOpStructuredLogger {
192    level: LogLevel,
193}
194
195#[cfg(feature = "structured-logging")]
196impl NoOpStructuredLogger {
197    #[inline]
198    pub fn new() -> Self {
199        Self {
200            level: LogLevel::Info,
201        }
202    }
203
204    #[inline]
205    pub fn with_level(level: LogLevel) -> Self {
206        Self { level }
207    }
208}
209
210#[cfg(feature = "structured-logging")]
211impl Default for NoOpStructuredLogger {
212    #[inline]
213    fn default() -> Self {
214        Self::new()
215    }
216}
217
218#[cfg(feature = "structured-logging")]
219impl StructuredLogger for NoOpStructuredLogger {
220    #[inline]
221    fn log_with_trace(
222        &self,
223        _level: LogLevel,
224        _message: &str,
225        _fields: &JsonValue,
226        _trace_id: Option<&str>,
227        _span_id: Option<&str>,
228    ) {
229        // No-op
230    }
231
232    #[inline]
233    fn log_performance(
234        &self,
235        _operation: &str,
236        _duration: Duration,
237        _success: bool,
238        _additional_fields: &JsonValue,
239    ) {
240        // No-op
241    }
242
243    #[inline]
244    fn log_error(&self, _error: &dyn std::error::Error, _context: &JsonValue) {
245        // No-op
246    }
247
248    #[inline]
249    fn set_level(&mut self, level: LogLevel) {
250        self.level = level;
251    }
252
253    #[inline]
254    fn is_level_enabled(&self, level: LogLevel) -> bool {
255        level <= self.level
256    }
257}
258
259/// Convenience function to create a no-op plugin when observability is disabled
260#[inline]
261pub fn create_noop_plugin() -> Box<dyn ObservabilityPlugin> {
262    Box::new(NoOpObservabilityPlugin::new())
263}
264
265/// Convenience function to create a shared no-op plugin
266#[inline]
267pub fn create_shared_noop_plugin() -> Arc<dyn ObservabilityPlugin> {
268    Arc::new(NoOpObservabilityPlugin::new())
269}
270
271/// Macro to conditionally create observability plugin based on features
272#[macro_export]
273macro_rules! observability_plugin {
274    ($plugin_expr:expr_2021) => {
275        #[cfg(feature = "observability")]
276        {
277            $plugin_expr
278        }
279        #[cfg(not(feature = "observability"))]
280        {
281            $crate::noop::create_noop_plugin()
282        }
283    };
284}
285
286/// Macro to conditionally create shared observability plugin based on features
287#[macro_export]
288macro_rules! shared_observability_plugin {
289    ($plugin_expr:expr_2021) => {
290        #[cfg(feature = "observability")]
291        {
292            $plugin_expr
293        }
294        #[cfg(not(feature = "observability"))]
295        {
296            $crate::noop::create_shared_noop_plugin()
297        }
298    };
299}
300
301/// Conditional span creation macro
302#[macro_export]
303macro_rules! observability_span {
304    ($plugin:expr_2021, $name:expr_2021, $($attr_key:expr_2021 => $attr_val:expr_2021),*) => {
305        {
306            #[cfg(feature = "observability")]
307            {
308                $plugin.start_span($name, &[$(($attr_key, $attr_val)),*])
309            }
310            #[cfg(not(feature = "observability"))]
311            {
312                $crate::SpanGuard::no_op()
313            }
314        }
315    };
316    ($plugin:expr_2021, $name:expr_2021) => {
317        {
318            #[cfg(feature = "observability")]
319            {
320                $plugin.start_span($name, &[])
321            }
322            #[cfg(not(feature = "observability"))]
323            {
324                $crate::SpanGuard::no_op()
325            }
326        }
327    };
328}
329
330/// Conditional metric recording macro
331#[macro_export]
332macro_rules! observability_metric {
333    ($plugin:expr_2021, $name:expr_2021, $value:expr_2021, $($label_key:expr_2021 => $label_val:expr_2021),*) => {
334        #[cfg(feature = "observability")]
335        {
336            $plugin.record_metric($name, $value, &[$(($label_key, $label_val)),*]);
337        }
338        #[cfg(not(feature = "observability"))]
339        {
340            // Compile to nothing
341        }
342    };
343    ($plugin:expr_2021, $name:expr_2021, $value:expr_2021) => {
344        #[cfg(feature = "observability")]
345        {
346            $plugin.record_metric($name, $value, &[]);
347        }
348        #[cfg(not(feature = "observability"))]
349        {
350            // Compile to nothing
351        }
352    };
353}
354
355/// Conditional logging macro
356#[macro_export]
357macro_rules! observability_log {
358    ($plugin:expr_2021, $level:expr_2021, $message:expr_2021) => {
359        #[cfg(feature = "observability")]
360        {
361            $plugin.log($level, $message);
362        }
363        #[cfg(not(feature = "observability"))]
364        {
365            // Compile to nothing in release, optionally log in debug
366            #[cfg(debug_assertions)]
367            {
368                println!("[{}] {}", $level.as_str(), $message);
369            }
370        }
371    };
372}
373
374#[cfg(test)]
375mod tests {
376    use super::*;
377
378    #[test]
379    fn test_noop_plugin_zero_cost() {
380        let plugin = NoOpObservabilityPlugin::new();
381
382        // These should all compile to no-ops
383        let _span = plugin.start_span("test", &[("key", "value")]);
384        plugin.record_metric("test_metric", 1.0, &[("label", "value")]);
385        plugin.log(LogLevel::Info, "test message");
386
387        assert!(!plugin.is_enabled());
388        assert_eq!(plugin.plugin_type(), "noop");
389        assert!(plugin.flush().is_ok());
390    }
391
392    #[test]
393    fn test_noop_metrics_collector() {
394        let mut collector = NoOpMetricsCollector::new();
395
396        // All operations should succeed but do nothing
397        assert!(
398            collector
399                .register_counter("test", "description", &["label"])
400                .is_ok()
401        );
402        assert!(
403            collector
404                .record_counter("test", 1.0, &HashMap::new())
405                .is_ok()
406        );
407        assert!(collector.get_metrics().is_empty());
408
409        collector.clear(); // Should not panic
410    }
411
412    #[cfg(feature = "structured-logging")]
413    #[test]
414    fn test_noop_structured_logger() {
415        let mut logger = NoOpStructuredLogger::new();
416
417        // All operations should succeed but do nothing
418        use serde_json::json;
419        logger.log_with_trace(LogLevel::Info, "test", &json!({}), None, None);
420        logger.log_performance("test_op", Duration::from_millis(100), true, &json!({}));
421
422        logger.set_level(LogLevel::Debug);
423        assert!(logger.is_level_enabled(LogLevel::Info));
424        assert!(!logger.is_level_enabled(LogLevel::Trace));
425    }
426
427    #[test]
428    fn test_macros_compile() {
429        let _plugin = create_noop_plugin();
430
431        // Test that macros compile without errors
432        let _span = observability_span!(_plugin, "test_span", "key" => "value");
433        observability_metric!(_plugin, "test_metric", 1.0, "label" => "value");
434        observability_log!(_plugin, LogLevel::Info, "test message");
435    }
436}