Skip to main content

moduvex_observe/
lib.rs

1//! # moduvex-observe
2//!
3//! Observability for the Moduvex framework: structured logging, distributed
4//! tracing, metrics collection, and health checks — all built on
5//! `moduvex-runtime` with zero external async runtime dependencies.
6//!
7//! ## Quick Start
8//!
9//! ```rust,ignore
10//! use moduvex_observe::prelude::*;
11//!
12//! // Install the default subscriber (reads MODUVEX_LOG / MODUVEX_LOG_FORMAT).
13//! moduvex_observe::init_logging();
14//!
15//! // Structured logging
16//! info!("request handled", status = 200, path = "/users");
17//!
18//! // Metrics
19//! let counter = Counter::new("http_requests_total", "Total HTTP requests");
20//! counter.inc();
21//! ```
22
23// ── Modules ──
24
25pub mod export;
26pub mod health;
27pub mod log;
28pub mod metrics;
29pub mod trace;
30
31// ── Re-exports: Log ──
32
33pub use log::format::{JsonFormatter, PrettyFormatter};
34pub use log::subscriber::{
35    set_global_subscriber, set_min_level, LogFormat, LogSubscriber, Subscriber,
36};
37pub use log::{Event, Level, Value};
38
39// ── Re-exports: Trace ──
40
41pub use trace::context::SpanContext;
42pub use trace::span::{Span, SpanGuard};
43pub use trace::{SpanId, TraceId};
44
45// ── Re-exports: Metrics ──
46
47pub use metrics::counter::Counter;
48pub use metrics::gauge::Gauge;
49pub use metrics::histogram::Histogram;
50pub use metrics::registry::MetricsRegistry;
51
52// ── Re-exports: Health ──
53
54pub use health::{AsyncHealthCheck, HealthCheck, HealthRegistry, HealthStatus};
55
56// ── Re-exports: Export ──
57
58pub use export::prometheus::PrometheusExporter;
59pub use export::stdout::StdoutExporter;
60pub use export::Exporter;
61
62// ── Prelude ──
63
64pub mod prelude {
65    pub use crate::{
66        Counter, Event, Gauge, HealthCheck, HealthRegistry, HealthStatus, Histogram, Level,
67        MetricsRegistry, Span, SpanContext, SpanGuard, SpanId, Subscriber, TraceId, Value,
68    };
69}
70
71// ── Logging init ──────────────────────────────────────────────────────────────
72
73/// Install the default structured-log subscriber.
74///
75/// Reads environment variables at call time:
76/// - `MODUVEX_LOG` — minimum level (`trace`, `debug`, `info`, `warn`, `error`).
77///   Defaults to `info`.
78/// - `MODUVEX_LOG_FORMAT` — output format (`json` for JSON lines, otherwise
79///   human-readable pretty format).
80///
81/// Also sets the global min-level atomic so that the `log_event!` macro can
82/// short-circuit before allocating an `Event` for filtered-out events.
83///
84/// Calling this more than once is safe: subsequent calls are silently ignored
85/// because `OnceLock` only accepts the first value.
86pub fn init_logging() {
87    let sub = LogSubscriber::from_env();
88    // Raise the global filter to match the subscriber's min level, enabling
89    // zero-cost filtering in the macros.
90    set_min_level(sub.min_level);
91    // Ignore the error — it just means a subscriber was already installed.
92    let _ = set_global_subscriber(sub);
93}
94
95// ── Convenience macros ────────────────────────────────────────────────────────
96
97/// Emit a log event at an explicit level.
98///
99/// The level check against the global minimum happens **before** any `Event`
100/// allocation, making filtered-out events truly zero-cost.
101#[macro_export]
102macro_rules! log_event {
103    ($level:expr, $msg:expr $(, $key:ident = $val:expr)* $(,)?) => {{
104        // Zero-cost gate: compare numeric repr of levels.
105        if ($level as u8) >= $crate::log::subscriber::min_level() as u8 {
106            let event = $crate::Event::now($level, $msg)
107                $(.field(stringify!($key), $val))*;
108            $crate::log::subscriber::dispatch(&event);
109        }
110    }};
111}
112
113/// Emit a TRACE-level log event.
114#[macro_export]
115macro_rules! trace {
116    ($msg:expr $(, $key:ident = $val:expr)* $(,)?) => {
117        $crate::log_event!($crate::Level::Trace, $msg $(, $key = $val)*)
118    };
119}
120
121/// Emit a DEBUG-level log event.
122#[macro_export]
123macro_rules! debug {
124    ($msg:expr $(, $key:ident = $val:expr)* $(,)?) => {
125        $crate::log_event!($crate::Level::Debug, $msg $(, $key = $val)*)
126    };
127}
128
129/// Emit an INFO-level log event.
130#[macro_export]
131macro_rules! info {
132    ($msg:expr $(, $key:ident = $val:expr)* $(,)?) => {
133        $crate::log_event!($crate::Level::Info, $msg $(, $key = $val)*)
134    };
135}
136
137/// Emit a WARN-level log event.
138#[macro_export]
139macro_rules! warn {
140    ($msg:expr $(, $key:ident = $val:expr)* $(,)?) => {
141        $crate::log_event!($crate::Level::Warn, $msg $(, $key = $val)*)
142    };
143}
144
145/// Emit an ERROR-level log event.
146#[macro_export]
147macro_rules! error {
148    ($msg:expr $(, $key:ident = $val:expr)* $(,)?) => {
149        $crate::log_event!($crate::Level::Error, $msg $(, $key = $val)*)
150    };
151}
152
153/// Emit a TRACE-level log event (alias kept for backward compatibility).
154#[macro_export]
155macro_rules! trace_event {
156    ($msg:expr $(, $key:ident = $val:expr)* $(,)?) => {
157        $crate::log_event!($crate::Level::Trace, $msg $(, $key = $val)*)
158    };
159}
160
161// ── Tests ─────────────────────────────────────────────────────────────────────
162
163#[cfg(test)]
164mod tests {
165    use crate::log::subscriber::{min_level, set_min_level, LogFormat, LogSubscriber};
166    use crate::log::{Event, Level};
167
168    // Helper: run a closure with a temporarily overridden min level, then
169    // restore the previous value.  This avoids cross-test pollution.
170    fn with_level<F: FnOnce()>(level: Level, f: F) {
171        let prev = min_level();
172        set_min_level(level);
173        f();
174        set_min_level(prev);
175    }
176
177    #[test]
178    fn log_event_macro_passes_at_or_above_min_level() {
179        // The macro itself calls dispatch() which is a no-op without a subscriber.
180        // We just verify it compiles and doesn't panic.
181        with_level(Level::Debug, || {
182            crate::log_event!(Level::Debug, "debug msg");
183            crate::log_event!(Level::Info, "info msg");
184        });
185    }
186
187    #[test]
188    fn trace_macro_compiles() {
189        with_level(Level::Trace, || {
190            crate::trace!("trace message");
191            crate::trace!("trace with field", key = "value");
192        });
193    }
194
195    #[test]
196    fn debug_macro_compiles() {
197        with_level(Level::Debug, || {
198            crate::debug!("debug message");
199            crate::debug!("debug with field", count = 42_i32);
200        });
201    }
202
203    #[test]
204    fn info_macro_compiles() {
205        with_level(Level::Info, || {
206            crate::info!("info message");
207            crate::info!("info with fields", status = 200_i32, path = "/health");
208        });
209    }
210
211    #[test]
212    fn warn_macro_compiles() {
213        with_level(Level::Warn, || {
214            crate::warn!("warn message");
215        });
216    }
217
218    #[test]
219    fn error_macro_compiles() {
220        with_level(Level::Error, || {
221            crate::error!("error message");
222            crate::error!("error with field", code = 500_i32);
223        });
224    }
225
226    #[test]
227    fn trace_event_alias_compiles() {
228        with_level(Level::Trace, || {
229            crate::trace_event!("trace alias");
230        });
231    }
232
233    #[test]
234    fn log_event_macro_filtered_when_below_min() {
235        // Set min to Error — Debug and Info should be no-ops.
236        with_level(Level::Error, || {
237            // These calls should be filtered without panic.
238            crate::log_event!(Level::Debug, "should be filtered");
239            crate::log_event!(Level::Info, "should be filtered");
240            crate::log_event!(Level::Warn, "should be filtered");
241            // This should pass the filter (but dispatch is a no-op without subscriber).
242            crate::log_event!(Level::Error, "should pass filter");
243        });
244    }
245
246    #[test]
247    fn log_subscriber_new_fields_accessible() {
248        let sub = LogSubscriber::new(Level::Debug, LogFormat::Json);
249        assert_eq!(sub.min_level, Level::Debug);
250        assert_eq!(sub.format, LogFormat::Json);
251    }
252
253    #[test]
254    fn init_logging_is_idempotent() {
255        // Second call should not panic even though OnceLock is already set.
256        // (In practice only the first call succeeds; subsequent ones are ignored.)
257        crate::init_logging();
258        crate::init_logging();
259    }
260
261    #[test]
262    fn json_format_output_is_valid_structure() {
263        let event = Event::now(Level::Info, "test")
264            .field("key", "val")
265            .field("n", 42_i32);
266        let mut buf = Vec::new();
267        crate::JsonFormatter::format(&event, &mut buf).unwrap();
268        let s = String::from_utf8(buf).unwrap();
269        // Must start with `{` and end with `}\n`.
270        assert!(s.starts_with('{'));
271        assert!(s.trim_end().ends_with('}'));
272        assert!(s.contains("\"level\":\"INFO\""));
273        assert!(s.contains("\"msg\":\"test\""));
274        assert!(s.contains("\"key\":\"val\""));
275        assert!(s.contains("\"n\":42"));
276    }
277
278    #[test]
279    fn set_min_level_affects_macro_gate() {
280        // With Error level, the u8 comparison in log_event! skips lower levels.
281        with_level(Level::Error, || {
282            assert_eq!(min_level(), Level::Error);
283            // Trace (0) < Error (4) — filtered.
284            assert!((Level::Trace as u8) < (min_level() as u8));
285        });
286    }
287}