langextract_rust/
logging.rs

1//! Logging and progress reporting system for LangExtract.
2//!
3//! This module provides a unified system for logging and progress reporting
4//! that can be controlled by library users and CLI applications.
5
6use std::sync::Arc;
7
8/// Progress event types for different stages of processing
9#[derive(Debug, Clone)]
10pub enum ProgressEvent {
11    /// Text processing started
12    ProcessingStarted {
13        text_length: usize,
14        model: String,
15        provider: String,
16    },
17    /// Text is being chunked
18    ChunkingStarted {
19        total_chars: usize,
20        chunk_count: usize,
21        strategy: String,
22    },
23    /// Batch processing progress
24    BatchProgress {
25        batch_number: usize,
26        total_batches: usize,
27        chunks_processed: usize,
28        total_chunks: usize,
29    },
30    /// Language model call in progress
31    ModelCall {
32        provider: String,
33        model: String,
34        input_length: usize,
35    },
36    /// Model response received
37    ModelResponse {
38        success: bool,
39        output_length: Option<usize>,
40    },
41    /// Extraction validation and parsing
42    ValidationStarted {
43        raw_output_length: usize,
44    },
45    /// Validation completed
46    ValidationCompleted {
47        extractions_found: usize,
48        aligned_count: usize,
49        errors: usize,
50        warnings: usize,
51    },
52    /// Results aggregation
53    AggregationStarted {
54        chunk_count: usize,
55    },
56    /// Processing completed
57    ProcessingCompleted {
58        total_extractions: usize,
59        processing_time_ms: u64,
60    },
61    /// Retry attempt
62    RetryAttempt {
63        operation: String,
64        attempt: usize,
65        max_attempts: usize,
66        delay_seconds: u64,
67    },
68    /// Error occurred
69    Error {
70        operation: String,
71        error: String,
72    },
73    /// Debug information
74    Debug {
75        operation: String,
76        details: String,
77    },
78}
79
80/// Trait for handling progress events
81pub trait ProgressHandler: Send + Sync {
82    /// Handle a progress event
83    fn handle_progress(&self, event: ProgressEvent);
84}
85
86/// Console progress handler that outputs to stdout
87pub struct ConsoleProgressHandler {
88    /// Whether to show progress messages
89    pub show_progress: bool,
90    /// Whether to show debug information
91    pub show_debug: bool,
92    /// Whether to use emoji and colors
93    pub use_styling: bool,
94}
95
96impl ConsoleProgressHandler {
97    /// Create a new console handler with default settings
98    pub fn new() -> Self {
99        Self {
100            show_progress: true,
101            show_debug: false,
102            use_styling: true,
103        }
104    }
105
106    /// Create a quiet console handler (only errors)
107    pub fn quiet() -> Self {
108        Self {
109            show_progress: false,
110            show_debug: false,
111            use_styling: true,
112        }
113    }
114
115    /// Create a verbose console handler (everything)
116    pub fn verbose() -> Self {
117        Self {
118            show_progress: true,
119            show_debug: true,
120            use_styling: true,
121        }
122    }
123
124    /// Create a machine-readable handler (no styling)
125    pub fn machine_readable() -> Self {
126        Self {
127            show_progress: true,
128            show_debug: false,
129            use_styling: false,
130        }
131    }
132
133    fn format_message(&self, emoji: &str, message: &str) -> String {
134        if self.use_styling {
135            format!("{} {}", emoji, message)
136        } else {
137            format!("[{}] {}", 
138                match emoji {
139                    "🤖" => "MODEL",
140                    "📄" => "CHUNK",
141                    "🔄" => "PROGRESS",
142                    "✅" => "SUCCESS",
143                    "❌" => "ERROR",
144                    "⏳" => "WAIT",
145                    "🎯" => "COMPLETE",
146                    "🔧" => "DEBUG",
147                    "📥" => "RESPONSE",
148                    "📡" => "REQUEST",
149                    "🎉" => "SUCCESS",
150                    _ => "INFO",
151                },
152                message
153            )
154        }
155    }
156}
157
158impl Default for ConsoleProgressHandler {
159    fn default() -> Self {
160        Self::new()
161    }
162}
163
164impl ProgressHandler for ConsoleProgressHandler {
165    fn handle_progress(&self, event: ProgressEvent) {
166        match event {
167            ProgressEvent::ProcessingStarted { text_length, model, provider } => {
168                if self.show_progress {
169                    let msg = format!("Calling {} model: {} ({} chars input)", provider, model, text_length);
170                    println!("{}", self.format_message("🤖", &msg));
171                }
172            }
173            ProgressEvent::ChunkingStarted { total_chars, chunk_count, strategy } => {
174                if self.show_progress {
175                    let msg = format!("Processing document with {} {} chunks ({} chars total)", 
176                        chunk_count, strategy, total_chars);
177                    println!("{}", self.format_message("📄", &msg));
178                }
179            }
180            ProgressEvent::BatchProgress { batch_number, total_batches: _, chunks_processed, total_chunks } => {
181                if self.show_progress {
182                    let msg = format!("Processing batch {} ({}/{} chunks processed)", 
183                        batch_number, chunks_processed, total_chunks);
184                    println!("{}", self.format_message("🔄", &msg));
185                }
186            }
187            ProgressEvent::ModelCall { provider, model: _, input_length } => {
188                if self.show_progress {
189                    let msg = format!("Starting {} API call with retry logic for input size {}", provider, input_length);
190                    println!("{}", self.format_message("🔄", &msg));
191                }
192            }
193            ProgressEvent::ModelResponse { success, output_length: _ } => {
194                if self.show_progress {
195                    let msg = if success {
196                        "Received response from language model".to_string()
197                    } else {
198                        "Failed to receive response from language model".to_string()
199                    };
200                    println!("{}", self.format_message("📥", &msg));
201                }
202            }
203            ProgressEvent::AggregationStarted { chunk_count } => {
204                if self.show_progress {
205                    let msg = format!("Aggregating results from {} chunks...", chunk_count);
206                    println!("{}", self.format_message("🔄", &msg));
207                }
208            }
209            ProgressEvent::ProcessingCompleted { total_extractions, processing_time_ms: _ } => {
210                if self.show_progress {
211                    let msg = format!("Extraction complete! Found {} total extractions", total_extractions);
212                    println!("{}", self.format_message("🎯", &msg));
213                }
214            }
215            ProgressEvent::RetryAttempt { operation, attempt, max_attempts, delay_seconds } => {
216                if self.show_progress {
217                    println!("{}", self.format_message("❌", 
218                        &format!("{} failed (attempt {}/{})", operation, attempt, max_attempts)));
219                    println!("{}", self.format_message("⏳", 
220                        &format!("Retrying in {} seconds...", delay_seconds)));
221                }
222            }
223            ProgressEvent::Error { operation, error } => {
224                // Always show errors
225                println!("{}", self.format_message("❌", &format!("{}: {}", operation, error)));
226            }
227            ProgressEvent::Debug { operation, details } => {
228                if self.show_debug {
229                    println!("{}", self.format_message("🔧", &format!("DEBUG: {}: {}", operation, details)));
230                }
231            }
232            ProgressEvent::ValidationStarted { raw_output_length: _ } => {
233                // Internal event, no output needed
234            }
235            ProgressEvent::ValidationCompleted { extractions_found, aligned_count, errors, warnings } => {
236                if self.show_debug {
237                    let msg = format!("Validation result: {} extractions ({} aligned), {} errors, {} warnings", 
238                        extractions_found, aligned_count, errors, warnings);
239                    println!("{}", self.format_message("🔧", &format!("DEBUG: {}", msg)));
240                }
241            }
242        }
243    }
244}
245
246/// Silent progress handler that does nothing
247pub struct SilentProgressHandler;
248
249impl ProgressHandler for SilentProgressHandler {
250    fn handle_progress(&self, _event: ProgressEvent) {
251        // Do nothing
252    }
253}
254
255/// Logger that integrates with the standard log crate
256pub struct LogProgressHandler;
257
258impl ProgressHandler for LogProgressHandler {
259    fn handle_progress(&self, event: ProgressEvent) {
260        match event {
261            ProgressEvent::ProcessingStarted { text_length, model, provider } => {
262                log::info!("Starting extraction with {} model {} ({} chars)", provider, model, text_length);
263            }
264            ProgressEvent::ChunkingStarted { total_chars, chunk_count, strategy } => {
265                log::info!("Chunking document: {} {} chunks ({} chars)", chunk_count, strategy, total_chars);
266            }
267            ProgressEvent::BatchProgress { batch_number, total_batches: _, chunks_processed, total_chunks } => {
268                log::debug!("Processing batch {}: {}/{} chunks", batch_number, chunks_processed, total_chunks);
269            }
270            ProgressEvent::ModelCall { provider, model, input_length } => {
271                log::debug!("Calling {} model {} with {} chars input", provider, model, input_length);
272            }
273            ProgressEvent::ModelResponse { success, output_length } => {
274                if success {
275                    log::debug!("Received response: {} chars", output_length.unwrap_or(0));
276                } else {
277                    log::warn!("Failed to receive model response");
278                }
279            }
280            ProgressEvent::ValidationCompleted { extractions_found, aligned_count, errors, warnings } => {
281                log::debug!("Validation: {} extractions ({} aligned), {} errors, {} warnings", 
282                    extractions_found, aligned_count, errors, warnings);
283            }
284            ProgressEvent::AggregationStarted { chunk_count } => {
285                log::debug!("Aggregating {} chunks", chunk_count);
286            }
287            ProgressEvent::ProcessingCompleted { total_extractions, processing_time_ms } => {
288                log::info!("Extraction completed: {} extractions in {}ms", total_extractions, processing_time_ms);
289            }
290            ProgressEvent::RetryAttempt { operation, attempt, max_attempts, delay_seconds } => {
291                log::warn!("Retry {}/{} for {}, waiting {}s", attempt, max_attempts, operation, delay_seconds);
292            }
293            ProgressEvent::Error { operation, error } => {
294                log::error!("{}: {}", operation, error);
295            }
296            ProgressEvent::Debug { operation, details } => {
297                log::debug!("{}: {}", operation, details);
298            }
299            ProgressEvent::ValidationStarted { .. } => {
300                log::trace!("Starting validation");
301            }
302        }
303    }
304}
305
306/// Global progress handler
307static mut PROGRESS_HANDLER: Option<Arc<dyn ProgressHandler>> = None;
308static INIT: std::sync::Once = std::sync::Once::new();
309
310/// Initialize the global progress handler
311pub fn init_progress_handler(handler: Arc<dyn ProgressHandler>) {
312    unsafe {
313        PROGRESS_HANDLER = Some(handler);
314    }
315}
316
317/// Get the current progress handler, or create a default one
318fn get_progress_handler() -> Arc<dyn ProgressHandler> {
319    unsafe {
320        PROGRESS_HANDLER.clone().unwrap_or_else(|| {
321            INIT.call_once(|| {
322                PROGRESS_HANDLER = Some(Arc::new(ConsoleProgressHandler::new()));
323            });
324            PROGRESS_HANDLER.clone().unwrap()
325        })
326    }
327}
328
329/// Report a progress event
330pub fn report_progress(event: ProgressEvent) {
331    let handler = get_progress_handler();
332    handler.handle_progress(event);
333}
334
335/// Convenience macros for common progress events
336#[macro_export]
337macro_rules! progress_info {
338    ($($arg:tt)*) => {
339        $crate::logging::report_progress($crate::logging::ProgressEvent::Debug {
340            operation: "info".to_string(),
341            details: format!($($arg)*),
342        });
343    };
344}
345
346#[macro_export]
347macro_rules! progress_debug {
348    ($operation:expr, $($arg:tt)*) => {
349        $crate::logging::report_progress($crate::logging::ProgressEvent::Debug {
350            operation: $operation.to_string(),
351            details: format!($($arg)*),
352        });
353    };
354}
355
356#[macro_export]
357macro_rules! progress_error {
358    ($operation:expr, $($arg:tt)*) => {
359        $crate::logging::report_progress($crate::logging::ProgressEvent::Error {
360            operation: $operation.to_string(),
361            error: format!($($arg)*),
362        });
363    };
364}
365
366#[cfg(test)]
367mod tests {
368    use super::*;
369
370    #[test]
371    fn test_console_handler_formatting() {
372        let handler = ConsoleProgressHandler::new();
373        let message = handler.format_message("🤖", "Test message");
374        assert!(message.contains("🤖"));
375        assert!(message.contains("Test message"));
376
377        let machine_handler = ConsoleProgressHandler::machine_readable();
378        let machine_message = machine_handler.format_message("🤖", "Test message");
379        assert!(machine_message.contains("[MODEL]"));
380        assert!(machine_message.contains("Test message"));
381    }
382
383    #[test]
384    fn test_progress_events() {
385        let handler = ConsoleProgressHandler::quiet();
386        
387        // Should not panic
388        handler.handle_progress(ProgressEvent::ProcessingStarted {
389            text_length: 1000,
390            model: "test-model".to_string(),
391            provider: "test-provider".to_string(),
392        });
393    }
394}