1use serde::{Deserialize, Serialize};
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
32#[serde(rename_all = "lowercase")]
33pub enum LogLevel {
34 Trace,
36 Debug,
38 #[default]
40 Info,
41 Warn,
43 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#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct LogConfig {
89 pub level: LogLevel,
91 pub structured: bool,
93 pub include_timestamps: bool,
95 pub include_location: bool,
97 pub include_thread_ids: bool,
99 pub domain_levels: std::collections::HashMap<String, LogLevel>,
101 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 pub fn development() -> Self {
122 Self {
123 level: LogLevel::Debug,
124 structured: false,
125 include_location: true,
126 ..Default::default()
127 }
128 }
129
130 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 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 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#[derive(Debug, Clone, Default, Serialize, Deserialize)]
179#[serde(rename_all = "lowercase")]
180pub enum LogOutput {
181 #[default]
183 Stdout,
184 Stderr,
186 File(String),
188}
189
190pub 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 pub fn trace() -> Self {
205 Self::new(LogLevel::Trace)
206 }
207
208 pub fn debug() -> Self {
210 Self::new(LogLevel::Debug)
211 }
212
213 pub fn info() -> Self {
215 Self::new(LogLevel::Info)
216 }
217
218 pub fn warn() -> Self {
220 Self::new(LogLevel::Warn)
221 }
222
223 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 pub fn message(mut self, msg: impl Into<String>) -> Self {
243 self.message = Some(msg.into());
244 self
245 }
246
247 pub fn kernel(mut self, id: impl Into<String>) -> Self {
249 self.kernel_id = Some(id.into());
250 self
251 }
252
253 pub fn domain(mut self, domain: impl Into<String>) -> Self {
255 self.domain = Some(domain.into());
256 self
257 }
258
259 pub fn tenant(mut self, tenant: impl Into<String>) -> Self {
261 self.tenant_id = Some(tenant.into());
262 self
263 }
264
265 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 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 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#[derive(Debug, Clone, Serialize)]
340pub struct AuditLog {
341 pub timestamp: chrono::DateTime<chrono::Utc>,
343 pub event_type: AuditEventType,
345 pub actor: String,
347 pub resource: String,
349 pub action: String,
351 pub result: AuditResult,
353 pub details: Option<serde_json::Value>,
355 pub tenant_id: Option<String>,
357 pub request_id: Option<String>,
359}
360
361impl AuditLog {
362 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 pub fn with_result(mut self, result: AuditResult) -> Self {
384 self.result = result;
385 self
386 }
387
388 pub fn with_details(mut self, details: impl Serialize) -> Self {
390 self.details = serde_json::to_value(details).ok();
391 self
392 }
393
394 pub fn with_tenant(mut self, tenant: impl Into<String>) -> Self {
396 self.tenant_id = Some(tenant.into());
397 self
398 }
399
400 pub fn with_request_id(mut self, id: impl Into<String>) -> Self {
402 self.request_id = Some(id.into());
403 self
404 }
405
406 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#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
424#[serde(rename_all = "snake_case")]
425pub enum AuditEventType {
426 Authentication,
428 Authorization,
430 KernelAccess,
432 ConfigChange,
434 DataAccess,
436 AdminAction,
438}
439
440#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
442#[serde(rename_all = "snake_case")]
443pub enum AuditResult {
444 Success,
446 Failure,
448 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 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}