postfix_log_parser/components/
postlogd.rs

1//! # Postlogd 日志服务组件解析器
2//!
3//! Postlogd 是 Postfix 的内部日志服务组件,负责:
4//! - 集中处理 Postfix 各组件的日志输出
5//! - 提供统一的日志格式和时间戳
6//! - 监控配置文件变更和覆盖警告
7//! - 管理日志的路由和存储
8//!
9//! ## 核心功能
10//!
11//! - **日志集中化**: 收集和转发其他组件的日志
12//! - **配置监控**: 检测配置文件参数覆盖和冲突
13//! - **时间戳统一**: 为所有日志提供一致的时间戳格式
14//! - **日志路由**: 根据级别和类型路由日志到不同目标
15//!
16//! ## 支持的事件类型
17//!
18//! - **配置覆盖警告**: 配置文件中重复参数定义的警告
19//!
20//! ## 示例日志格式
21//!
22//! ```text
23//! # 配置覆盖警告
24//! warning: /etc/postfix/main.cf, line 820: overriding earlier entry: smtpd_recipient_restrictions=...
25//! warning: /etc/postfix/main.cf, line 806: overriding earlier entry: smtpd_client_message_rate_limit=0
26//! ```
27
28use crate::events::postlogd::{PostlogdEvent, PostlogdEventType};
29use regex::Regex;
30use std::sync::LazyLock;
31
32/// POSTLOGD组件解析器
33/// 处理Postfix内部日志服务器相关事件
34pub struct PostlogdParser;
35
36/// 配置覆盖警告正则表达式
37/// 格式: warning: /etc/postfix/main.cf, line 820: overriding earlier entry: smtpd_recipient_restrictions=...
38static CONFIG_OVERRIDE_WARNING_RE: LazyLock<Regex> = LazyLock::new(|| {
39    Regex::new(r"^((?:\d{4}\s+)?\S+ \d+ \d+:\d+:\d+(?:\.\d+)?) \S+ postfix/postlogd\[(\d+)\]: warning: (.+?), line (\d+): overriding earlier entry: (.+?)=(.+)$")
40        .expect("Invalid CONFIG_OVERRIDE_WARNING_RE regex")
41});
42
43impl PostlogdParser {
44    /// 创建新的POSTLOGD解析器实例
45    pub fn new() -> Self {
46        Self
47    }
48
49    /// 解析POSTLOGD日志行
50    pub fn parse_log_line(&self, line: &str) -> Result<PostlogdEvent, String> {
51        // 处理配置覆盖警告
52        if let Some(caps) = CONFIG_OVERRIDE_WARNING_RE.captures(line) {
53            let timestamp = caps.get(1).unwrap().as_str().to_string();
54            let process_id = caps.get(2).unwrap().as_str().to_string();
55            let file_path = caps.get(3).unwrap().as_str().to_string();
56            let line_number = caps
57                .get(4)
58                .unwrap()
59                .as_str()
60                .parse::<u32>()
61                .map_err(|_| "Failed to parse line number")?;
62            let parameter = caps.get(5).unwrap().as_str().to_string();
63            let value = caps.get(6).unwrap().as_str().to_string();
64
65            return Ok(PostlogdEvent {
66                timestamp,
67                process_id,
68                event_type: PostlogdEventType::ConfigOverrideWarning {
69                    file_path,
70                    line_number,
71                    parameter,
72                    value,
73                },
74            });
75        }
76
77        Err(format!("Failed to parse postlogd log line: {}", line))
78    }
79
80    /// 获取支持的事件类型数量
81    pub fn supported_event_types(&self) -> usize {
82        1 // ConfigOverrideWarning
83    }
84
85    /// 检查日志行是否属于POSTLOGD组件
86    pub fn matches_component(&self, line: &str) -> bool {
87        line.contains("postfix/postlogd[")
88    }
89
90    /// 解析配置覆盖警告(用于ComponentParser接口)
91    /// 输入是已移除"warning:"前缀的消息内容
92    fn parse_config_override_warning(&self, message: &str) -> Option<PostlogdEvent> {
93        use regex::Regex;
94        use std::sync::LazyLock;
95
96        // 配置覆盖警告正则表达式:/path/to/file, line N: overriding earlier entry: param=value
97        static CONFIG_WARNING_RE: LazyLock<Regex> = LazyLock::new(|| {
98            Regex::new(r"^(.+?), line (\d+): overriding earlier entry: (.+?)=(.+)$")
99                .expect("Invalid CONFIG_WARNING_RE regex")
100        });
101
102        if let Some(caps) = CONFIG_WARNING_RE.captures(message.trim()) {
103            let file_path = caps.get(1)?.as_str().to_string();
104            let line_number = caps.get(2)?.as_str().parse::<u32>().ok()?;
105            let parameter = caps.get(3)?.as_str().to_string();
106            let value = caps.get(4)?.as_str().to_string();
107
108            return Some(PostlogdEvent {
109                timestamp: chrono::Utc::now().format("%b %d %H:%M:%S").to_string(),
110                process_id: "0".to_string(), // 会在主解析器中正确设置
111                event_type: PostlogdEventType::ConfigOverrideWarning {
112                    file_path,
113                    line_number,
114                    parameter,
115                    value,
116                },
117            });
118        }
119        None
120    }
121}
122
123impl Default for PostlogdParser {
124    fn default() -> Self {
125        Self::new()
126    }
127}
128
129impl crate::components::ComponentParser for PostlogdParser {
130    fn parse(
131        &self,
132        message: &str,
133    ) -> Result<crate::events::base::ComponentEvent, crate::error::ParseError> {
134        let clean_message = message.trim();
135
136        // 尝试解析配置覆盖警告
137        if let Some(event) = self.parse_config_override_warning(clean_message) {
138            return Ok(crate::events::base::ComponentEvent::Postlogd(event));
139        }
140
141        Err(crate::error::ParseError::ComponentParseError {
142            component: "postlogd".to_string(),
143            reason: format!("Unable to parse message: {}", message),
144        })
145    }
146
147    fn component_name(&self) -> &'static str {
148        "postlogd"
149    }
150
151    fn can_parse(&self, message: &str) -> bool {
152        message.contains("overriding earlier entry:") && message.contains("main.cf")
153    }
154}
155
156#[cfg(test)]
157mod tests {
158    use super::*;
159    use crate::components::ComponentParser;
160
161    fn create_parser() -> PostlogdParser {
162        PostlogdParser::new()
163    }
164
165    #[test]
166    fn test_config_override_warning_parsing() {
167        let parser = create_parser();
168        let log_line = "Apr 08 17:54:30 m01 postfix/postlogd[78]: warning: /etc/postfix/main.cf, line 820: overriding earlier entry: smtpd_recipient_restrictions=check_client_access pcre:/etc/postfix/filter_trusted,                                                                permit_sasl_authenticated,                                                                permit_mynetworks,                                                                reject_unauth_destination,                                                                pcre:/etc/postfix/filter_default";
169
170        let result = parser.parse_log_line(log_line);
171        assert!(result.is_ok());
172
173        let event = result.unwrap();
174        assert_eq!(event.timestamp, "Apr 08 17:54:30");
175        assert_eq!(event.process_id, "78");
176
177        let PostlogdEventType::ConfigOverrideWarning {
178            file_path,
179            line_number,
180            parameter,
181            value,
182        } = event.event_type;
183
184        assert_eq!(file_path, "/etc/postfix/main.cf");
185        assert_eq!(line_number, 820);
186        assert_eq!(parameter, "smtpd_recipient_restrictions");
187        assert!(value.contains("check_client_access"));
188        assert!(value.contains("permit_sasl_authenticated"));
189    }
190
191    #[test]
192    fn test_client_message_rate_limit_parsing() {
193        let parser = create_parser();
194        let log_line = "Apr 10 11:17:10 m01 postfix/postlogd[78]: warning: /etc/postfix/main.cf, line 806: overriding earlier entry: smtpd_client_message_rate_limit=0";
195
196        let result = parser.parse_log_line(log_line);
197        assert!(result.is_ok());
198
199        let event = result.unwrap();
200        assert_eq!(event.timestamp, "Apr 10 11:17:10");
201        assert_eq!(event.process_id, "78");
202
203        let PostlogdEventType::ConfigOverrideWarning {
204            file_path,
205            line_number,
206            parameter,
207            value,
208        } = event.event_type;
209
210        assert_eq!(file_path, "/etc/postfix/main.cf");
211        assert_eq!(line_number, 806);
212        assert_eq!(parameter, "smtpd_client_message_rate_limit");
213        assert_eq!(value, "0");
214    }
215
216    #[test]
217    fn test_discard_ehlo_keywords_parsing() {
218        let parser = create_parser();
219        let log_line = "Apr 10 11:17:10 m01 postfix/postlogd[78]: warning: /etc/postfix/main.cf, line 826: overriding earlier entry: smtpd_discard_ehlo_keywords=silent-discard,dsn,etrn";
220
221        let result = parser.parse_log_line(log_line);
222        assert!(result.is_ok());
223
224        let event = result.unwrap();
225        let PostlogdEventType::ConfigOverrideWarning {
226            parameter, value, ..
227        } = event.event_type;
228
229        assert_eq!(parameter, "smtpd_discard_ehlo_keywords");
230        assert_eq!(value, "silent-discard,dsn,etrn");
231    }
232
233    #[test]
234    fn test_different_line_numbers() {
235        let parser = create_parser();
236        let test_cases = vec![
237            ("line 806", 806),
238            ("line 818", 818),
239            ("line 819", 819),
240            ("line 820", 820),
241            ("line 822", 822),
242            ("line 826", 826),
243        ];
244
245        for (line_text, expected_line) in test_cases {
246            let log_line = format!("Apr 10 11:19:32 m01 postfix/postlogd[78]: warning: /etc/postfix/main.cf, {}: overriding earlier entry: test_param=test_value", line_text);
247
248            let result = parser.parse_log_line(&log_line);
249            assert!(result.is_ok());
250
251            let event = result.unwrap();
252            let PostlogdEventType::ConfigOverrideWarning { line_number, .. } = event.event_type;
253            assert_eq!(line_number, expected_line);
254        }
255    }
256
257    #[test]
258    fn test_different_process_ids() {
259        let parser = create_parser();
260        let test_cases = vec!["78", "83"];
261
262        for pid in test_cases {
263            let log_line = format!("Apr 08 17:58:29 m01 postfix/postlogd[{}]: warning: /etc/postfix/main.cf, line 820: overriding earlier entry: test_param=test_value", pid);
264
265            let result = parser.parse_log_line(&log_line);
266            assert!(result.is_ok());
267
268            let event = result.unwrap();
269            assert_eq!(event.process_id, pid);
270        }
271    }
272
273    #[test]
274    fn test_complex_parameter_values() {
275        let parser = create_parser();
276        let log_line = "Apr 10 11:45:49 m01 postfix/postlogd[78]: warning: /etc/postfix/main.cf, line 815: overriding earlier entry: smtpd_recipient_restrictions=check_client_access pcre:/etc/postfix/filter_trusted,                                                                permit_sasl_authenticated,                                                                permit_mynetworks,                                                                reject_unauth_destination,                                                                pcre:/etc/postfix/filter_default,                                                                check_client_access hash:/etc/postfix/access,                                                                check_recipient_access hash:/etc/postfix/recipient_access";
277
278        let result = parser.parse_log_line(log_line);
279        assert!(result.is_ok());
280
281        let event = result.unwrap();
282        let PostlogdEventType::ConfigOverrideWarning { value, .. } = event.event_type;
283        assert!(value.contains("check_client_access pcre:/etc/postfix/filter_trusted"));
284        assert!(value.contains("check_client_access hash:/etc/postfix/access"));
285        assert!(value.contains("check_recipient_access hash:/etc/postfix/recipient_access"));
286    }
287
288    #[test]
289    fn test_component_matching() {
290        let parser = create_parser();
291
292        // 应该匹配的行
293        let matching_lines = vec![
294            "Apr 08 17:54:30 m01 postfix/postlogd[78]: warning: /etc/postfix/main.cf, line 820: overriding earlier entry: test=value",
295            "Apr 10 11:17:10 m01 postfix/postlogd[83]: warning: /etc/postfix/main.cf, line 806: overriding earlier entry: another=param",
296        ];
297
298        for line in matching_lines {
299            assert!(parser.matches_component(line), "Should match: {}", line);
300        }
301
302        // 不应该匹配的行
303        let non_matching_lines = vec![
304            "Apr 08 17:54:30 m01 postfix/qmgr[78]: info: statistics",
305            "Apr 08 17:54:30 m01 postfix/smtpd[78]: connect from localhost",
306            "Apr 08 17:54:30 m01 postfix/cleanup[78]: message-id=<test@example.com>",
307        ];
308
309        for line in non_matching_lines {
310            assert!(
311                !parser.matches_component(line),
312                "Should not match: {}",
313                line
314            );
315        }
316    }
317
318    #[test]
319    fn test_invalid_log_lines() {
320        let parser = create_parser();
321
322        let invalid_lines = vec![
323            "Invalid log line",
324            "Apr 08 17:54:30 m01 postfix/qmgr[78]: info: statistics",
325            "Apr 08 17:54:30 m01 postfix/postlogd[78]: some other message",
326            "incomplete line",
327        ];
328
329        for line in invalid_lines {
330            let result = parser.parse_log_line(line);
331            assert!(result.is_err(), "Should fail to parse: {}", line);
332        }
333    }
334
335    #[test]
336    fn test_supported_event_types() {
337        let parser = create_parser();
338        assert_eq!(parser.supported_event_types(), 1);
339    }
340
341    #[test]
342    fn test_parser_default() {
343        let parser = PostlogdParser::default();
344        assert_eq!(parser.supported_event_types(), 1);
345    }
346
347    #[test]
348    fn test_component_parser_parse() {
349        let parser = PostlogdParser::new();
350
351        // 测试配置覆盖警告
352        let message = "/etc/postfix/main.cf, line 820: overriding earlier entry: smtpd_recipient_restrictions=check_client_access pcre:/etc/postfix/filter_trusted";
353        let result = parser.parse(message);
354
355        assert!(result.is_ok());
356        match result.unwrap() {
357            crate::events::base::ComponentEvent::Postlogd(event) => {
358                assert_eq!(event.process_id, "0"); // 临时进程ID
359                let PostlogdEventType::ConfigOverrideWarning {
360                    file_path,
361                    line_number,
362                    parameter,
363                    ..
364                } = event.event_type;
365
366                assert_eq!(file_path, "/etc/postfix/main.cf");
367                assert_eq!(line_number, 820);
368                assert_eq!(parameter, "smtpd_recipient_restrictions");
369            }
370            _ => panic!("Expected Postlogd ComponentEvent"),
371        }
372    }
373
374    #[test]
375    fn test_component_parser_invalid() {
376        let parser = PostlogdParser::new();
377
378        let message = "some invalid message";
379        let result = parser.parse(message);
380
381        assert!(result.is_err());
382        match result.unwrap_err() {
383            crate::error::ParseError::ComponentParseError { component, .. } => {
384                assert_eq!(component, "postlogd");
385            }
386            _ => panic!("Expected ComponentParseError"),
387        }
388    }
389
390    #[test]
391    fn test_component_name() {
392        let parser = PostlogdParser::new();
393        assert_eq!(parser.component_name(), "postlogd");
394    }
395
396    #[test]
397    fn test_can_parse() {
398        let parser = PostlogdParser::new();
399
400        // 应该能解析的消息
401        assert!(parser
402            .can_parse("/etc/postfix/main.cf, line 820: overriding earlier entry: test=value"));
403
404        // 不应该解析的消息
405        assert!(!parser.can_parse("some random message"));
406        assert!(!parser.can_parse("overriding earlier entry: test=value")); // 缺少main.cf
407        assert!(!parser.can_parse("/etc/postfix/main.cf, line 820: some other message"));
408        // 缺少overriding
409    }
410}