postfix_log_parser/components/
master.rs

1//! Master daemon component parser
2//!
3//! Parses log entries for the master component, which handles
4//! process management and daemon control operations.
5
6use regex::Regex;
7use crate::components::ComponentParser;
8use crate::events::ComponentEvent;
9use crate::events::base::{BaseEvent, PostfixLogLevel};
10use crate::events::master::{
11    MasterEvent, MasterLifecycleEvent, MasterProcessWarning, 
12    MasterServiceLimit, MasterConfigWarning
13};
14use crate::error::ParseError;
15use chrono::{DateTime, Utc};
16
17pub struct MasterParser {
18    // Daemon lifecycle
19    daemon_started_regex: Regex,
20    daemon_reload_regex: Regex,
21    
22    // Process warnings
23    process_killed_regex: Regex,
24    bad_command_startup_regex: Regex,
25    
26    // Service limits
27    service_limit_regex: Regex,
28    
29    // Configuration warnings
30    process_count_suggestion_regex: Regex,
31    stress_config_reference_regex: Regex,
32}
33
34impl MasterParser {
35    pub fn new() -> Self {
36        MasterParser {
37            // Daemon lifecycle
38            daemon_started_regex: Regex::new(r"^daemon started -- version ([^,]+), configuration (.+)$").unwrap(),
39            daemon_reload_regex: Regex::new(r"^reload -- version ([^,]+), configuration (.+)$").unwrap(),
40            
41            // Process warnings
42            process_killed_regex: Regex::new(r"^warning: process (/[^/]+/[^/]+/[^/]+/([^/\s]+)) pid (\d+) killed by signal (\d+)$").unwrap(),
43            bad_command_startup_regex: Regex::new(r"^warning: (/[^/]+/[^/]+/[^/]+/[^:]+): bad command startup -- throttling$").unwrap(),
44            
45            // Service limits
46            service_limit_regex: Regex::new(r#"^warning: service "([^"]+)" \(([^)]+)\) has reached its process limit "(\d+)": new clients may experience noticeable delays$"#).unwrap(),
47            
48            // Configuration warnings
49            process_count_suggestion_regex: Regex::new(r"^warning: to avoid this condition, increase the process count in master\.cf or reduce the service time per client$").unwrap(),
50            stress_config_reference_regex: Regex::new(r"^warning: see (https?://[^\s]+) for examples of stress-adapting configuration settings$").unwrap(),
51        }
52    }
53
54    fn create_base_event(&self, message: &str) -> BaseEvent {
55        BaseEvent {
56            timestamp: DateTime::parse_from_rfc3339("2024-04-27T16:20:48Z")
57                .unwrap()
58                .with_timezone(&Utc),
59            hostname: "unknown".to_string(),
60            component: "master".to_string(),
61            process_id: 0,
62            log_level: PostfixLogLevel::Info,
63            raw_message: message.to_string(),
64        }
65    }
66
67    fn parse_daemon_lifecycle(&self, message: &str) -> Option<MasterEvent> {
68        let base = self.create_base_event(message);
69        
70        if let Some(caps) = self.daemon_started_regex.captures(message) {
71            let version = caps.get(1).map(|m| m.as_str().to_string())
72                .unwrap_or_else(|| "unknown".to_string());
73            let configuration = caps.get(2).map(|m| m.as_str().to_string())
74                .unwrap_or_else(|| "unknown".to_string());
75            return Some(MasterEvent::DaemonLifecycle {
76                base,
77                lifecycle: MasterLifecycleEvent::Started { version, configuration },
78            });
79        }
80
81        if let Some(caps) = self.daemon_reload_regex.captures(message) {
82            let version = caps.get(1).map(|m| m.as_str().to_string())
83                .unwrap_or_else(|| "unknown".to_string());
84            let configuration = caps.get(2).map(|m| m.as_str().to_string())
85                .unwrap_or_else(|| "unknown".to_string());
86            return Some(MasterEvent::DaemonLifecycle {
87                base,
88                lifecycle: MasterLifecycleEvent::Reload { version, configuration },
89            });
90        }
91
92        None
93    }
94
95    fn parse_process_warning(&self, message: &str) -> Option<MasterEvent> {
96        let mut base = self.create_base_event(message);
97        base.log_level = PostfixLogLevel::Warning;
98        
99        if let Some(caps) = self.process_killed_regex.captures(message) {
100            let process_path = caps.get(1).map(|m| m.as_str().to_string())
101                .unwrap_or_else(|| "unknown".to_string());
102            let service = caps.get(2).map(|m| m.as_str().to_string())
103                .unwrap_or_else(|| "unknown".to_string());
104            let pid = caps.get(3).and_then(|m| m.as_str().parse::<u32>().ok())
105                .unwrap_or(0);
106            let signal = caps.get(4).and_then(|m| m.as_str().parse::<u8>().ok())
107                .unwrap_or(0);
108            return Some(MasterEvent::ProcessWarning {
109                base,
110                warning: MasterProcessWarning::ProcessKilled {
111                    service,
112                    process_path,
113                    pid,
114                    signal,
115                },
116            });
117        }
118
119        if let Some(caps) = self.bad_command_startup_regex.captures(message) {
120            let full_path = caps.get(1).map(|m| m.as_str().to_string())
121                .unwrap_or_else(|| "unknown".to_string());
122            // Extract service name from path like "/usr/libexec/postfix/smtpd"
123            let service = full_path.split('/').last().unwrap_or("unknown").to_string();
124            return Some(MasterEvent::ProcessWarning {
125                base,
126                warning: MasterProcessWarning::BadCommandStartup { service },
127            });
128        }
129
130        None
131    }
132
133    fn parse_service_limit(&self, message: &str) -> Option<MasterEvent> {
134        let mut base = self.create_base_event(message);
135        base.log_level = PostfixLogLevel::Warning;
136        
137        if let Some(caps) = self.service_limit_regex.captures(message) {
138            let service = caps.get(1).map(|m| m.as_str().to_string())
139                .unwrap_or_else(|| "unknown".to_string());
140            let service_address = caps.get(2).map(|m| m.as_str().to_string())
141                .unwrap_or_else(|| "unknown".to_string());
142            let limit = caps.get(3).and_then(|m| m.as_str().parse::<u32>().ok())
143                .unwrap_or(0);
144            return Some(MasterEvent::ServiceLimit {
145                base,
146                limit: MasterServiceLimit::ProcessLimitReached {
147                    service,
148                    service_address,
149                    limit,
150                },
151            });
152        }
153
154        None
155    }
156
157    fn parse_configuration_warning(&self, message: &str) -> Option<MasterEvent> {
158        let mut base = self.create_base_event(message);
159        base.log_level = PostfixLogLevel::Warning;
160        
161        if self.process_count_suggestion_regex.is_match(message) {
162            return Some(MasterEvent::ConfigurationWarning {
163                base,
164                warning: MasterConfigWarning::ProcessCountSuggestion,
165            });
166        }
167
168        if let Some(caps) = self.stress_config_reference_regex.captures(message) {
169            let url = caps.get(1).map(|m| m.as_str().to_string())
170                .unwrap_or_else(|| "unknown".to_string());
171            return Some(MasterEvent::ConfigurationWarning {
172                base,
173                warning: MasterConfigWarning::StressConfigReference { url },
174            });
175        }
176
177        None
178    }
179}
180
181impl ComponentParser for MasterParser {
182    fn parse(&self, message: &str) -> Result<ComponentEvent, ParseError> {
183        // Try daemon lifecycle events first
184        if let Some(event) = self.parse_daemon_lifecycle(message) {
185            return Ok(ComponentEvent::Master(event));
186        }
187        
188        // Try warnings (most common)
189        if message.contains("warning:") {
190            if let Some(event) = self.parse_process_warning(message) {
191                return Ok(ComponentEvent::Master(event));
192            }
193            
194            if let Some(event) = self.parse_service_limit(message) {
195                return Ok(ComponentEvent::Master(event));
196            }
197            
198            if let Some(event) = self.parse_configuration_warning(message) {
199                return Ok(ComponentEvent::Master(event));
200            }
201        }
202
203        Err(ParseError::ComponentParseError {
204            component: "master".to_string(),
205            reason: format!("Unsupported message format: {}", message),
206        })
207    }
208
209    fn component_name(&self) -> &'static str {
210        "master"
211    }
212
213    fn can_parse(&self, component: &str) -> bool {
214        component == "master"
215    }
216}