rustkernel_core/observability/
logging.rs

1//! Structured Logging
2//!
3//! Provides structured logging with kernel context for production debugging.
4//!
5//! # Features
6//!
7//! - JSON structured output for log aggregation
8//! - Context propagation (trace IDs, tenant IDs)
9//! - Per-domain log levels
10//! - Audit logging for security events
11//!
12//! # Example
13//!
14//! ```rust,ignore
15//! use rustkernel_core::observability::logging::{LogConfig, StructuredLogger};
16//!
17//! let config = LogConfig::production();
18//! config.init()?;
19//!
20//! StructuredLogger::info()
21//!     .kernel("graph/pagerank")
22//!     .tenant("tenant-123")
23//!     .message("Kernel execution completed")
24//!     .field("latency_us", 150)
25//!     .log();
26//! ```
27
28use serde::{Deserialize, Serialize};
29
30/// Log level
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
32#[serde(rename_all = "lowercase")]
33pub enum LogLevel {
34    /// Trace level (most verbose)
35    Trace,
36    /// Debug level
37    Debug,
38    /// Info level
39    #[default]
40    Info,
41    /// Warning level
42    Warn,
43    /// Error level
44    Error,
45}
46
47impl std::fmt::Display for LogLevel {
48    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49        match self {
50            Self::Trace => write!(f, "trace"),
51            Self::Debug => write!(f, "debug"),
52            Self::Info => write!(f, "info"),
53            Self::Warn => write!(f, "warn"),
54            Self::Error => write!(f, "error"),
55        }
56    }
57}
58
59impl std::str::FromStr for LogLevel {
60    type Err = String;
61
62    fn from_str(s: &str) -> Result<Self, Self::Err> {
63        match s.to_lowercase().as_str() {
64            "trace" => Ok(Self::Trace),
65            "debug" => Ok(Self::Debug),
66            "info" => Ok(Self::Info),
67            "warn" | "warning" => Ok(Self::Warn),
68            "error" => Ok(Self::Error),
69            _ => Err(format!("Invalid log level: {}", s)),
70        }
71    }
72}
73
74impl From<LogLevel> for tracing::Level {
75    fn from(level: LogLevel) -> Self {
76        match level {
77            LogLevel::Trace => tracing::Level::TRACE,
78            LogLevel::Debug => tracing::Level::DEBUG,
79            LogLevel::Info => tracing::Level::INFO,
80            LogLevel::Warn => tracing::Level::WARN,
81            LogLevel::Error => tracing::Level::ERROR,
82        }
83    }
84}
85
86/// Logging configuration
87#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct LogConfig {
89    /// Default log level
90    pub level: LogLevel,
91    /// Enable structured JSON output
92    pub structured: bool,
93    /// Include timestamps
94    pub include_timestamps: bool,
95    /// Include caller location
96    pub include_location: bool,
97    /// Include thread IDs
98    pub include_thread_ids: bool,
99    /// Per-domain log levels
100    pub domain_levels: std::collections::HashMap<String, LogLevel>,
101    /// Output target (stdout, stderr, file path)
102    pub output: LogOutput,
103}
104
105impl Default for LogConfig {
106    fn default() -> Self {
107        Self {
108            level: LogLevel::Info,
109            structured: false,
110            include_timestamps: true,
111            include_location: false,
112            include_thread_ids: false,
113            domain_levels: std::collections::HashMap::new(),
114            output: LogOutput::Stdout,
115        }
116    }
117}
118
119impl LogConfig {
120    /// Development configuration
121    pub fn development() -> Self {
122        Self {
123            level: LogLevel::Debug,
124            structured: false,
125            include_location: true,
126            ..Default::default()
127        }
128    }
129
130    /// Production configuration
131    pub fn production() -> Self {
132        Self {
133            level: LogLevel::Info,
134            structured: true,
135            include_timestamps: true,
136            include_thread_ids: true,
137            ..Default::default()
138        }
139    }
140
141    /// Set log level for a specific domain
142    pub fn with_domain_level(mut self, domain: impl Into<String>, level: LogLevel) -> Self {
143        self.domain_levels.insert(domain.into(), level);
144        self
145    }
146
147    /// Initialize logging
148    pub fn init(&self) -> crate::error::Result<()> {
149        use tracing_subscriber::{EnvFilter, fmt, layer::SubscriberExt, util::SubscriberInitExt};
150
151        let filter = EnvFilter::try_from_default_env()
152            .unwrap_or_else(|_| EnvFilter::new(self.level.to_string()));
153
154        let subscriber = tracing_subscriber::registry().with(filter);
155
156        if self.structured {
157            let layer = fmt::layer()
158                .json()
159                .with_thread_ids(self.include_thread_ids)
160                .with_file(self.include_location)
161                .with_line_number(self.include_location);
162
163            subscriber.with(layer).try_init().ok();
164        } else {
165            let layer = fmt::layer()
166                .with_thread_ids(self.include_thread_ids)
167                .with_file(self.include_location)
168                .with_line_number(self.include_location);
169
170            subscriber.with(layer).try_init().ok();
171        }
172
173        Ok(())
174    }
175}
176
177/// Log output target
178#[derive(Debug, Clone, Default, Serialize, Deserialize)]
179#[serde(rename_all = "lowercase")]
180pub enum LogOutput {
181    /// Standard output
182    #[default]
183    Stdout,
184    /// Standard error
185    Stderr,
186    /// File path
187    File(String),
188}
189
190/// Structured logger builder
191pub struct StructuredLogger {
192    level: LogLevel,
193    message: Option<String>,
194    kernel_id: Option<String>,
195    domain: Option<String>,
196    tenant_id: Option<String>,
197    trace_id: Option<String>,
198    span_id: Option<String>,
199    fields: std::collections::HashMap<String, serde_json::Value>,
200}
201
202impl StructuredLogger {
203    /// Create a new logger at trace level
204    pub fn trace() -> Self {
205        Self::new(LogLevel::Trace)
206    }
207
208    /// Create a new logger at debug level
209    pub fn debug() -> Self {
210        Self::new(LogLevel::Debug)
211    }
212
213    /// Create a new logger at info level
214    pub fn info() -> Self {
215        Self::new(LogLevel::Info)
216    }
217
218    /// Create a new logger at warn level
219    pub fn warn() -> Self {
220        Self::new(LogLevel::Warn)
221    }
222
223    /// Create a new logger at error level
224    pub fn error() -> Self {
225        Self::new(LogLevel::Error)
226    }
227
228    fn new(level: LogLevel) -> Self {
229        Self {
230            level,
231            message: None,
232            kernel_id: None,
233            domain: None,
234            tenant_id: None,
235            trace_id: None,
236            span_id: None,
237            fields: std::collections::HashMap::new(),
238        }
239    }
240
241    /// Set the message
242    pub fn message(mut self, msg: impl Into<String>) -> Self {
243        self.message = Some(msg.into());
244        self
245    }
246
247    /// Set the kernel ID
248    pub fn kernel(mut self, id: impl Into<String>) -> Self {
249        self.kernel_id = Some(id.into());
250        self
251    }
252
253    /// Set the domain
254    pub fn domain(mut self, domain: impl Into<String>) -> Self {
255        self.domain = Some(domain.into());
256        self
257    }
258
259    /// Set the tenant ID
260    pub fn tenant(mut self, tenant: impl Into<String>) -> Self {
261        self.tenant_id = Some(tenant.into());
262        self
263    }
264
265    /// Set trace context
266    pub fn trace_context(
267        mut self,
268        trace_id: impl Into<String>,
269        span_id: impl Into<String>,
270    ) -> Self {
271        self.trace_id = Some(trace_id.into());
272        self.span_id = Some(span_id.into());
273        self
274    }
275
276    /// Add a field
277    pub fn field(mut self, key: impl Into<String>, value: impl Serialize) -> Self {
278        if let Ok(json_value) = serde_json::to_value(value) {
279            self.fields.insert(key.into(), json_value);
280        }
281        self
282    }
283
284    /// Emit the log
285    pub fn log(self) {
286        let msg = self.message.unwrap_or_default();
287
288        match self.level {
289            LogLevel::Trace => tracing::trace!(
290                kernel_id = ?self.kernel_id,
291                domain = ?self.domain,
292                tenant_id = ?self.tenant_id,
293                trace_id = ?self.trace_id,
294                span_id = ?self.span_id,
295                "{}",
296                msg
297            ),
298            LogLevel::Debug => tracing::debug!(
299                kernel_id = ?self.kernel_id,
300                domain = ?self.domain,
301                tenant_id = ?self.tenant_id,
302                trace_id = ?self.trace_id,
303                span_id = ?self.span_id,
304                "{}",
305                msg
306            ),
307            LogLevel::Info => tracing::info!(
308                kernel_id = ?self.kernel_id,
309                domain = ?self.domain,
310                tenant_id = ?self.tenant_id,
311                trace_id = ?self.trace_id,
312                span_id = ?self.span_id,
313                "{}",
314                msg
315            ),
316            LogLevel::Warn => tracing::warn!(
317                kernel_id = ?self.kernel_id,
318                domain = ?self.domain,
319                tenant_id = ?self.tenant_id,
320                trace_id = ?self.trace_id,
321                span_id = ?self.span_id,
322                "{}",
323                msg
324            ),
325            LogLevel::Error => tracing::error!(
326                kernel_id = ?self.kernel_id,
327                domain = ?self.domain,
328                tenant_id = ?self.tenant_id,
329                trace_id = ?self.trace_id,
330                span_id = ?self.span_id,
331                "{}",
332                msg
333            ),
334        }
335    }
336}
337
338/// Audit log entry for security-relevant events
339#[derive(Debug, Clone, Serialize)]
340pub struct AuditLog {
341    /// Event timestamp
342    pub timestamp: chrono::DateTime<chrono::Utc>,
343    /// Event type
344    pub event_type: AuditEventType,
345    /// Actor (user ID or system)
346    pub actor: String,
347    /// Resource being accessed
348    pub resource: String,
349    /// Action performed
350    pub action: String,
351    /// Result (success/failure)
352    pub result: AuditResult,
353    /// Additional details
354    pub details: Option<serde_json::Value>,
355    /// Tenant ID
356    pub tenant_id: Option<String>,
357    /// Request ID
358    pub request_id: Option<String>,
359}
360
361impl AuditLog {
362    /// Create a new audit log entry
363    pub fn new(
364        event_type: AuditEventType,
365        actor: impl Into<String>,
366        resource: impl Into<String>,
367        action: impl Into<String>,
368    ) -> Self {
369        Self {
370            timestamp: chrono::Utc::now(),
371            event_type,
372            actor: actor.into(),
373            resource: resource.into(),
374            action: action.into(),
375            result: AuditResult::Success,
376            details: None,
377            tenant_id: None,
378            request_id: None,
379        }
380    }
381
382    /// Set the result
383    pub fn with_result(mut self, result: AuditResult) -> Self {
384        self.result = result;
385        self
386    }
387
388    /// Set details
389    pub fn with_details(mut self, details: impl Serialize) -> Self {
390        self.details = serde_json::to_value(details).ok();
391        self
392    }
393
394    /// Set tenant
395    pub fn with_tenant(mut self, tenant: impl Into<String>) -> Self {
396        self.tenant_id = Some(tenant.into());
397        self
398    }
399
400    /// Set request ID
401    pub fn with_request_id(mut self, id: impl Into<String>) -> Self {
402        self.request_id = Some(id.into());
403        self
404    }
405
406    /// Emit the audit log
407    pub fn emit(self) {
408        tracing::info!(
409            target: "audit",
410            event_type = ?self.event_type,
411            actor = %self.actor,
412            resource = %self.resource,
413            action = %self.action,
414            result = ?self.result,
415            tenant_id = ?self.tenant_id,
416            request_id = ?self.request_id,
417            "AUDIT"
418        );
419    }
420}
421
422/// Audit event types
423#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
424#[serde(rename_all = "snake_case")]
425pub enum AuditEventType {
426    /// Authentication event
427    Authentication,
428    /// Authorization event
429    Authorization,
430    /// Kernel access
431    KernelAccess,
432    /// Configuration change
433    ConfigChange,
434    /// Data access
435    DataAccess,
436    /// Administrative action
437    AdminAction,
438}
439
440/// Audit result
441#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
442#[serde(rename_all = "snake_case")]
443pub enum AuditResult {
444    /// Action succeeded
445    Success,
446    /// Action failed
447    Failure,
448    /// Action denied
449    Denied,
450}
451
452#[cfg(test)]
453mod tests {
454    use super::*;
455
456    #[test]
457    fn test_log_level_parsing() {
458        assert_eq!("debug".parse::<LogLevel>().unwrap(), LogLevel::Debug);
459        assert_eq!("INFO".parse::<LogLevel>().unwrap(), LogLevel::Info);
460        assert_eq!("warning".parse::<LogLevel>().unwrap(), LogLevel::Warn);
461    }
462
463    #[test]
464    fn test_log_config() {
465        let config = LogConfig::production();
466        assert!(config.structured);
467        assert_eq!(config.level, LogLevel::Info);
468
469        let dev_config = LogConfig::development();
470        assert!(!dev_config.structured);
471        assert_eq!(dev_config.level, LogLevel::Debug);
472    }
473
474    #[test]
475    fn test_structured_logger() {
476        // Just test that it builds correctly
477        let logger = StructuredLogger::info()
478            .message("Test message")
479            .kernel("graph/pagerank")
480            .tenant("tenant-123")
481            .field("latency_us", 150);
482
483        assert!(logger.message.is_some());
484        assert!(logger.kernel_id.is_some());
485    }
486
487    #[test]
488    fn test_audit_log() {
489        let audit = AuditLog::new(
490            AuditEventType::KernelAccess,
491            "user-123",
492            "graph/pagerank",
493            "execute",
494        )
495        .with_result(AuditResult::Success)
496        .with_tenant("tenant-456");
497
498        assert_eq!(audit.actor, "user-123");
499        assert!(audit.tenant_id.is_some());
500    }
501}