Skip to main content

barbacane_telemetry/
logging.rs

1//! Structured logging with JSON output and trace correlation.
2//!
3//! Implements 12-factor app logging: structured JSON to stdout.
4
5use crate::{LogFormat, TelemetryConfig, TelemetryError};
6use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Layer};
7
8/// Initialize the logging subsystem.
9///
10/// Sets up tracing-subscriber with either JSON or pretty format,
11/// respecting the configured log level.
12pub fn init_logging(config: &TelemetryConfig) -> Result<(), TelemetryError> {
13    // Build the env filter from config or RUST_LOG
14    let filter =
15        EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(&config.log_level));
16
17    match config.log_format {
18        LogFormat::Json => init_json_logging(filter),
19        LogFormat::Pretty => init_pretty_logging(filter),
20    }
21}
22
23/// Initialize JSON logging for production.
24fn init_json_logging(filter: EnvFilter) -> Result<(), TelemetryError> {
25    let json_layer = fmt::layer()
26        .json()
27        .with_target(true)
28        .with_current_span(true)
29        .with_span_list(false)
30        .with_file(false)
31        .with_line_number(false)
32        .flatten_event(true)
33        .with_filter(filter);
34
35    tracing_subscriber::registry()
36        .with(json_layer)
37        .try_init()
38        .map_err(|e: tracing_subscriber::util::TryInitError| {
39            TelemetryError::LoggingInit(e.to_string())
40        })
41}
42
43/// Initialize pretty logging for development.
44fn init_pretty_logging(filter: EnvFilter) -> Result<(), TelemetryError> {
45    let pretty_layer = fmt::layer()
46        .pretty()
47        .with_target(true)
48        .with_file(true)
49        .with_line_number(true)
50        .with_filter(filter);
51
52    tracing_subscriber::registry()
53        .with(pretty_layer)
54        .try_init()
55        .map_err(|e: tracing_subscriber::util::TryInitError| {
56            TelemetryError::LoggingInit(e.to_string())
57        })
58}
59
60/// Standard log event names per ADR-0010.
61pub mod events {
62    /// Gateway is starting up.
63    pub const STARTUP: &str = "startup";
64
65    /// Gateway is shutting down.
66    pub const SHUTDOWN: &str = "shutdown";
67
68    /// Artifact has been loaded.
69    pub const ARTIFACT_LOADED: &str = "artifact_loaded";
70
71    /// Plugin has been initialized.
72    pub const PLUGIN_INITIALIZED: &str = "plugin_initialized";
73
74    /// Gateway is listening on a port.
75    pub const LISTENING: &str = "listening";
76
77    /// Request has been completed.
78    pub const REQUEST_COMPLETED: &str = "request_completed";
79
80    /// Request validation failed.
81    pub const VALIDATION_FAILURE: &str = "validation_failure";
82
83    /// Middleware short-circuited the request.
84    pub const MIDDLEWARE_SHORT_CIRCUIT: &str = "middleware_short_circuit";
85
86    /// Dispatch to upstream failed.
87    pub const DISPATCH_ERROR: &str = "dispatch_error";
88
89    /// WASM plugin trapped (panic/error).
90    pub const WASM_TRAP: &str = "wasm_trap";
91
92    /// Secret refresh failed.
93    pub const SECRET_REFRESH_FAILED: &str = "secret_refresh_failed";
94
95    /// OTLP export failed.
96    pub const OTLP_EXPORT_FAILED: &str = "otlp_export_failed";
97}
98
99/// Helper macros for structured logging with standard fields.
100///
101/// These wrap the tracing macros to ensure consistent field naming.
102#[macro_export]
103macro_rules! log_startup {
104    ($($field:tt)*) => {
105        tracing::info!(
106            event = $crate::logging::events::STARTUP,
107            $($field)*
108        )
109    };
110}
111
112#[macro_export]
113macro_rules! log_shutdown {
114    ($($field:tt)*) => {
115        tracing::info!(
116            event = $crate::logging::events::SHUTDOWN,
117            $($field)*
118        )
119    };
120}
121
122#[macro_export]
123macro_rules! log_artifact_loaded {
124    ($($field:tt)*) => {
125        tracing::info!(
126            event = $crate::logging::events::ARTIFACT_LOADED,
127            $($field)*
128        )
129    };
130}
131
132#[macro_export]
133macro_rules! log_plugin_initialized {
134    ($($field:tt)*) => {
135        tracing::info!(
136            event = $crate::logging::events::PLUGIN_INITIALIZED,
137            $($field)*
138        )
139    };
140}
141
142#[macro_export]
143macro_rules! log_listening {
144    ($($field:tt)*) => {
145        tracing::info!(
146            event = $crate::logging::events::LISTENING,
147            $($field)*
148        )
149    };
150}
151
152#[macro_export]
153macro_rules! log_request_completed {
154    ($($field:tt)*) => {
155        tracing::info!(
156            event = $crate::logging::events::REQUEST_COMPLETED,
157            $($field)*
158        )
159    };
160}
161
162#[macro_export]
163macro_rules! log_validation_failure {
164    ($($field:tt)*) => {
165        tracing::warn!(
166            event = $crate::logging::events::VALIDATION_FAILURE,
167            $($field)*
168        )
169    };
170}
171
172#[macro_export]
173macro_rules! log_middleware_short_circuit {
174    ($($field:tt)*) => {
175        tracing::info!(
176            event = $crate::logging::events::MIDDLEWARE_SHORT_CIRCUIT,
177            $($field)*
178        )
179    };
180}
181
182#[macro_export]
183macro_rules! log_dispatch_error {
184    ($($field:tt)*) => {
185        tracing::error!(
186            event = $crate::logging::events::DISPATCH_ERROR,
187            $($field)*
188        )
189    };
190}
191
192#[macro_export]
193macro_rules! log_wasm_trap {
194    ($($field:tt)*) => {
195        tracing::error!(
196            event = $crate::logging::events::WASM_TRAP,
197            $($field)*
198        )
199    };
200}
201
202#[cfg(test)]
203mod tests {
204    use super::*;
205
206    // Note: We can't easily test logging initialization multiple times
207    // in the same test process due to global subscriber state.
208    // These tests verify the configuration logic.
209
210    #[test]
211    fn test_log_format_parse() {
212        assert_eq!(LogFormat::parse("json"), Some(LogFormat::Json));
213        assert_eq!(LogFormat::parse("JSON"), Some(LogFormat::Json));
214        assert_eq!(LogFormat::parse("pretty"), Some(LogFormat::Pretty));
215        assert_eq!(LogFormat::parse("PRETTY"), Some(LogFormat::Pretty));
216        assert_eq!(LogFormat::parse("invalid"), None);
217    }
218}