Skip to main content

portalis_core/
logging.rs

1//! Structured Logging Infrastructure
2//! Week 33 - Phase 4: Monitoring and Observability
3//!
4//! Provides:
5//! - Structured logging setup (tracing_subscriber)
6//! - JSON log format
7//! - Log levels configuration
8//! - Contextual logging
9//! - Error logging standards
10
11use serde::Serialize;
12use std::fmt;
13use tracing::{error, info, warn, Level};
14
15/// Log level configuration
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum LogLevel {
18    Trace,
19    Debug,
20    Info,
21    Warn,
22    Error,
23}
24
25impl LogLevel {
26    pub fn from_str(s: &str) -> Self {
27        match s.to_lowercase().as_str() {
28            "trace" => LogLevel::Trace,
29            "debug" => LogLevel::Debug,
30            "info" => LogLevel::Info,
31            "warn" | "warning" => LogLevel::Warn,
32            "error" => LogLevel::Error,
33            _ => LogLevel::Info,
34        }
35    }
36
37    pub fn to_tracing_level(&self) -> Level {
38        match self {
39            LogLevel::Trace => Level::TRACE,
40            LogLevel::Debug => Level::DEBUG,
41            LogLevel::Info => Level::INFO,
42            LogLevel::Warn => Level::WARN,
43            LogLevel::Error => Level::ERROR,
44        }
45    }
46}
47
48/// Logging configuration
49#[derive(Debug, Clone)]
50pub struct LoggingConfig {
51    /// Log level
52    pub level: LogLevel,
53
54    /// Enable JSON output
55    pub json_format: bool,
56
57    /// Enable file output
58    pub file_output: bool,
59
60    /// Log file path
61    pub file_path: Option<String>,
62
63    /// Include timestamps
64    pub include_timestamps: bool,
65
66    /// Include thread IDs
67    pub include_thread_ids: bool,
68
69    /// Include source location
70    pub include_source: bool,
71}
72
73impl Default for LoggingConfig {
74    fn default() -> Self {
75        Self {
76            level: LogLevel::Info,
77            json_format: std::env::var("LOG_JSON").unwrap_or_else(|_| "false".to_string()) == "true",
78            file_output: false,
79            file_path: None,
80            include_timestamps: true,
81            include_thread_ids: true,
82            include_source: true,
83        }
84    }
85}
86
87/// Structured log entry for JSON output
88#[derive(Debug, Clone, Serialize)]
89pub struct LogEntry {
90    pub timestamp: String,
91    pub level: String,
92    pub message: String,
93    pub target: String,
94    pub thread_id: Option<String>,
95    pub file: Option<String>,
96    pub line: Option<u32>,
97    pub fields: serde_json::Value,
98}
99
100/// Error context for detailed error logging
101#[derive(Debug, Clone, Serialize)]
102pub struct ErrorContext {
103    pub error_type: String,
104    pub error_message: String,
105    pub component: String,
106    pub severity: String,
107    pub trace_id: Option<String>,
108    pub additional_context: serde_json::Value,
109}
110
111impl ErrorContext {
112    pub fn new(
113        error_type: impl ToString,
114        error_message: impl ToString,
115        component: impl ToString,
116    ) -> Self {
117        Self {
118            error_type: error_type.to_string(),
119            error_message: error_message.to_string(),
120            component: component.to_string(),
121            severity: "error".to_string(),
122            trace_id: None,
123            additional_context: serde_json::json!({}),
124        }
125    }
126
127    pub fn with_severity(mut self, severity: impl ToString) -> Self {
128        self.severity = severity.to_string();
129        self
130    }
131
132    pub fn with_trace_id(mut self, trace_id: impl ToString) -> Self {
133        self.trace_id = Some(trace_id.to_string());
134        self
135    }
136
137    pub fn with_context(mut self, key: &str, value: serde_json::Value) -> Self {
138        if let Some(obj) = self.additional_context.as_object_mut() {
139            obj.insert(key.to_string(), value);
140        }
141        self
142    }
143
144    pub fn log(&self) {
145        error!(
146            error_type = %self.error_type,
147            error_message = %self.error_message,
148            component = %self.component,
149            severity = %self.severity,
150            trace_id = ?self.trace_id,
151            context = ?self.additional_context,
152            "Error occurred"
153        );
154    }
155}
156
157/// Agent logging helper
158pub struct AgentLogger {
159    agent_name: String,
160}
161
162impl AgentLogger {
163    pub fn new(agent_name: impl ToString) -> Self {
164        Self {
165            agent_name: agent_name.to_string(),
166        }
167    }
168
169    pub fn info(&self, message: &str) {
170        info!(agent = %self.agent_name, "{}", message);
171    }
172
173    pub fn warn(&self, message: &str) {
174        warn!(agent = %self.agent_name, "{}", message);
175    }
176
177    pub fn error(&self, message: &str, error_context: Option<&ErrorContext>) {
178        if let Some(ctx) = error_context {
179            error!(
180                agent = %self.agent_name,
181                error_type = %ctx.error_type,
182                error_message = %ctx.error_message,
183                severity = %ctx.severity,
184                "{}",
185                message
186            );
187        } else {
188            error!(agent = %self.agent_name, "{}", message);
189        }
190    }
191
192    pub fn debug(&self, message: &str) {
193        tracing::debug!(agent = %self.agent_name, "{}", message);
194    }
195
196    pub fn trace(&self, message: &str) {
197        tracing::trace!(agent = %self.agent_name, "{}", message);
198    }
199}
200
201/// Pipeline logging helper
202pub struct PipelineLogger {
203    pipeline_id: String,
204}
205
206impl PipelineLogger {
207    pub fn new(pipeline_id: impl ToString) -> Self {
208        Self {
209            pipeline_id: pipeline_id.to_string(),
210        }
211    }
212
213    pub fn phase_start(&self, phase_name: &str) {
214        info!(
215            pipeline_id = %self.pipeline_id,
216            phase = %phase_name,
217            "Starting pipeline phase"
218        );
219    }
220
221    pub fn phase_complete(&self, phase_name: &str, duration_ms: f64) {
222        info!(
223            pipeline_id = %self.pipeline_id,
224            phase = %phase_name,
225            duration_ms = duration_ms,
226            "Pipeline phase completed"
227        );
228    }
229
230    pub fn phase_error(&self, phase_name: &str, error: &str) {
231        error!(
232            pipeline_id = %self.pipeline_id,
233            phase = %phase_name,
234            error = %error,
235            "Pipeline phase failed"
236        );
237    }
238}
239
240/// Translation logging helper
241pub struct TranslationLogger {
242    translation_id: String,
243}
244
245impl TranslationLogger {
246    pub fn new(translation_id: impl ToString) -> Self {
247        Self {
248            translation_id: translation_id.to_string(),
249        }
250    }
251
252    pub fn start(&self, source_lang: &str, target_format: &str, lines: usize) {
253        info!(
254            translation_id = %self.translation_id,
255            source_language = %source_lang,
256            target_format = %target_format,
257            lines_of_code = lines,
258            "Starting translation"
259        );
260    }
261
262    pub fn progress(&self, step: &str, progress_percent: f64) {
263        info!(
264            translation_id = %self.translation_id,
265            step = %step,
266            progress_percent = progress_percent,
267            "Translation progress"
268        );
269    }
270
271    pub fn complete(&self, duration_ms: f64, output_size_bytes: usize) {
272        info!(
273            translation_id = %self.translation_id,
274            duration_ms = duration_ms,
275            output_size_bytes = output_size_bytes,
276            "Translation completed successfully"
277        );
278    }
279
280    pub fn failed(&self, error_category: &str, error_message: &str) {
281        error!(
282            translation_id = %self.translation_id,
283            error_category = %error_category,
284            error_message = %error_message,
285            "Translation failed"
286        );
287    }
288}
289
290/// Performance logging helper
291pub struct PerformanceLogger;
292
293impl PerformanceLogger {
294    pub fn log_latency(operation: &str, duration_ms: f64) {
295        info!(
296            operation = %operation,
297            duration_ms = duration_ms,
298            metric_type = "latency",
299            "Performance metric"
300        );
301    }
302
303    pub fn log_throughput(operation: &str, items_per_second: f64) {
304        info!(
305            operation = %operation,
306            items_per_second = items_per_second,
307            metric_type = "throughput",
308            "Performance metric"
309        );
310    }
311
312    pub fn log_resource_usage(component: &str, cpu_percent: f64, memory_mb: f64) {
313        info!(
314            component = %component,
315            cpu_percent = cpu_percent,
316            memory_mb = memory_mb,
317            metric_type = "resource_usage",
318            "Resource usage"
319        );
320    }
321}
322
323/// Audit logging for compliance
324pub struct AuditLogger;
325
326impl AuditLogger {
327    pub fn log_translation_request(user_id: &str, source_file: &str, ip_address: &str) {
328        info!(
329            user_id = %user_id,
330            source_file = %source_file,
331            ip_address = %ip_address,
332            event_type = "translation_request",
333            "Audit log"
334        );
335    }
336
337    pub fn log_translation_complete(user_id: &str, translation_id: &str, success: bool) {
338        info!(
339            user_id = %user_id,
340            translation_id = %translation_id,
341            success = success,
342            event_type = "translation_complete",
343            "Audit log"
344        );
345    }
346
347    pub fn log_error(user_id: &str, error_type: &str, details: &str) {
348        error!(
349            user_id = %user_id,
350            error_type = %error_type,
351            details = %details,
352            event_type = "error",
353            "Audit log"
354        );
355    }
356}
357
358#[cfg(test)]
359mod tests {
360    use super::*;
361
362    #[test]
363    fn test_log_level_from_str() {
364        assert_eq!(LogLevel::from_str("info"), LogLevel::Info);
365        assert_eq!(LogLevel::from_str("DEBUG"), LogLevel::Debug);
366        assert_eq!(LogLevel::from_str("error"), LogLevel::Error);
367        assert_eq!(LogLevel::from_str("invalid"), LogLevel::Info);
368    }
369
370    #[test]
371    fn test_logging_config_default() {
372        let config = LoggingConfig::default();
373        assert_eq!(config.level, LogLevel::Info);
374        assert!(config.include_timestamps);
375    }
376
377    #[test]
378    fn test_error_context_builder() {
379        let ctx = ErrorContext::new("ParseError", "Invalid syntax", "ingest-agent")
380            .with_severity("critical")
381            .with_trace_id("trace-123");
382
383        assert_eq!(ctx.error_type, "ParseError");
384        assert_eq!(ctx.severity, "critical");
385        assert_eq!(ctx.trace_id, Some("trace-123".to_string()));
386    }
387
388    #[test]
389    fn test_agent_logger() {
390        let logger = AgentLogger::new("test-agent");
391        assert_eq!(logger.agent_name, "test-agent");
392    }
393
394    #[test]
395    fn test_pipeline_logger() {
396        let logger = PipelineLogger::new("pipeline-123");
397        assert_eq!(logger.pipeline_id, "pipeline-123");
398    }
399
400    #[test]
401    fn test_translation_logger() {
402        let logger = TranslationLogger::new("trans-456");
403        assert_eq!(logger.translation_id, "trans-456");
404    }
405}