//! # Postlogd 日志服务组件解析器
//!
//! Postlogd 是 Postfix 的内部日志服务组件,负责:
//! - 集中处理 Postfix 各组件的日志输出
//! - 提供统一的日志格式和时间戳
//! - 监控配置文件变更和覆盖警告
//! - 管理日志的路由和存储
//!
//! ## 核心功能
//!
//! - **日志集中化**: 收集和转发其他组件的日志
//! - **配置监控**: 检测配置文件参数覆盖和冲突
//! - **时间戳统一**: 为所有日志提供一致的时间戳格式
//! - **日志路由**: 根据级别和类型路由日志到不同目标
//!
//! ## 支持的事件类型
//!
//! - **配置覆盖警告**: 配置文件中重复参数定义的警告
//!
//! ## 示例日志格式
//!
//! ```text
//! # 配置覆盖警告
//! warning: /etc/postfix/main.cf, line 820: overriding earlier entry: smtpd_recipient_restrictions=...
//! warning: /etc/postfix/main.cf, line 806: overriding earlier entry: smtpd_client_message_rate_limit=0
//! ```
use crate::events::postlogd::{PostlogdEvent, PostlogdEventType};
use regex::Regex;
use std::sync::LazyLock;
/// POSTLOGD组件解析器
/// 处理Postfix内部日志服务器相关事件
pub struct PostlogdParser;
/// 配置覆盖警告正则表达式
/// 格式: warning: /etc/postfix/main.cf, line 820: overriding earlier entry: smtpd_recipient_restrictions=...
static CONFIG_OVERRIDE_WARNING_RE: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"^((?:\d{4}\s+)?\S+ \d+ \d+:\d+:\d+(?:\.\d+)?) \S+ postfix/postlogd\[(\d+)\]: warning: (.+?), line (\d+): overriding earlier entry: (.+?)=(.+)$")
.expect("Invalid CONFIG_OVERRIDE_WARNING_RE regex")
});
impl PostlogdParser {
/// 创建新的POSTLOGD解析器实例
pub fn new() -> Self {
Self
}
/// 解析POSTLOGD日志行
pub fn parse_log_line(&self, line: &str) -> Result<PostlogdEvent, String> {
// 处理配置覆盖警告
if let Some(caps) = CONFIG_OVERRIDE_WARNING_RE.captures(line) {
let timestamp = caps.get(1).unwrap().as_str().to_string();
let process_id = caps.get(2).unwrap().as_str().to_string();
let file_path = caps.get(3).unwrap().as_str().to_string();
let line_number = caps
.get(4)
.unwrap()
.as_str()
.parse::<u32>()
.map_err(|_| "Failed to parse line number")?;
let parameter = caps.get(5).unwrap().as_str().to_string();
let value = caps.get(6).unwrap().as_str().to_string();
return Ok(PostlogdEvent {
timestamp,
process_id,
event_type: PostlogdEventType::ConfigOverrideWarning {
file_path,
line_number,
parameter,
value,
},
});
}
Err(format!("Failed to parse postlogd log line: {}", line))
}
/// 获取支持的事件类型数量
pub fn supported_event_types(&self) -> usize {
1 // ConfigOverrideWarning
}
/// 检查日志行是否属于POSTLOGD组件
pub fn matches_component(&self, line: &str) -> bool {
line.contains("postfix/postlogd[")
}
/// 解析配置覆盖警告(用于ComponentParser接口)
/// 输入是已移除"warning:"前缀的消息内容
fn parse_config_override_warning(&self, message: &str) -> Option<PostlogdEvent> {
use regex::Regex;
use std::sync::LazyLock;
// 配置覆盖警告正则表达式:/path/to/file, line N: overriding earlier entry: param=value
static CONFIG_WARNING_RE: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"^(.+?), line (\d+): overriding earlier entry: (.+?)=(.+)$")
.expect("Invalid CONFIG_WARNING_RE regex")
});
if let Some(caps) = CONFIG_WARNING_RE.captures(message.trim()) {
let file_path = caps.get(1)?.as_str().to_string();
let line_number = caps.get(2)?.as_str().parse::<u32>().ok()?;
let parameter = caps.get(3)?.as_str().to_string();
let value = caps.get(4)?.as_str().to_string();
return Some(PostlogdEvent {
timestamp: chrono::Utc::now().format("%b %d %H:%M:%S").to_string(),
process_id: "0".to_string(), // 会在主解析器中正确设置
event_type: PostlogdEventType::ConfigOverrideWarning {
file_path,
line_number,
parameter,
value,
},
});
}
None
}
}
impl Default for PostlogdParser {
fn default() -> Self {
Self::new()
}
}
impl crate::components::ComponentParser for PostlogdParser {
fn parse(
&self,
message: &str,
) -> Result<crate::events::base::ComponentEvent, crate::error::ParseError> {
let clean_message = message.trim();
// 尝试解析配置覆盖警告
if let Some(event) = self.parse_config_override_warning(clean_message) {
return Ok(crate::events::base::ComponentEvent::Postlogd(event));
}
Err(crate::error::ParseError::ComponentParseError {
component: "postlogd".to_string(),
reason: format!("Unable to parse message: {}", message),
})
}
fn component_name(&self) -> &'static str {
"postlogd"
}
fn can_parse(&self, message: &str) -> bool {
message.contains("overriding earlier entry:") && message.contains("main.cf")
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::components::ComponentParser;
fn create_parser() -> PostlogdParser {
PostlogdParser::new()
}
#[test]
fn test_config_override_warning_parsing() {
let parser = create_parser();
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";
let result = parser.parse_log_line(log_line);
assert!(result.is_ok());
let event = result.unwrap();
assert_eq!(event.timestamp, "Apr 08 17:54:30");
assert_eq!(event.process_id, "78");
let PostlogdEventType::ConfigOverrideWarning {
file_path,
line_number,
parameter,
value,
} = event.event_type;
assert_eq!(file_path, "/etc/postfix/main.cf");
assert_eq!(line_number, 820);
assert_eq!(parameter, "smtpd_recipient_restrictions");
assert!(value.contains("check_client_access"));
assert!(value.contains("permit_sasl_authenticated"));
}
#[test]
fn test_client_message_rate_limit_parsing() {
let parser = create_parser();
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";
let result = parser.parse_log_line(log_line);
assert!(result.is_ok());
let event = result.unwrap();
assert_eq!(event.timestamp, "Apr 10 11:17:10");
assert_eq!(event.process_id, "78");
let PostlogdEventType::ConfigOverrideWarning {
file_path,
line_number,
parameter,
value,
} = event.event_type;
assert_eq!(file_path, "/etc/postfix/main.cf");
assert_eq!(line_number, 806);
assert_eq!(parameter, "smtpd_client_message_rate_limit");
assert_eq!(value, "0");
}
#[test]
fn test_discard_ehlo_keywords_parsing() {
let parser = create_parser();
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";
let result = parser.parse_log_line(log_line);
assert!(result.is_ok());
let event = result.unwrap();
let PostlogdEventType::ConfigOverrideWarning {
parameter, value, ..
} = event.event_type;
assert_eq!(parameter, "smtpd_discard_ehlo_keywords");
assert_eq!(value, "silent-discard,dsn,etrn");
}
#[test]
fn test_different_line_numbers() {
let parser = create_parser();
let test_cases = vec![
("line 806", 806),
("line 818", 818),
("line 819", 819),
("line 820", 820),
("line 822", 822),
("line 826", 826),
];
for (line_text, expected_line) in test_cases {
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);
let result = parser.parse_log_line(&log_line);
assert!(result.is_ok());
let event = result.unwrap();
let PostlogdEventType::ConfigOverrideWarning { line_number, .. } = event.event_type;
assert_eq!(line_number, expected_line);
}
}
#[test]
fn test_different_process_ids() {
let parser = create_parser();
let test_cases = vec!["78", "83"];
for pid in test_cases {
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);
let result = parser.parse_log_line(&log_line);
assert!(result.is_ok());
let event = result.unwrap();
assert_eq!(event.process_id, pid);
}
}
#[test]
fn test_complex_parameter_values() {
let parser = create_parser();
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";
let result = parser.parse_log_line(log_line);
assert!(result.is_ok());
let event = result.unwrap();
let PostlogdEventType::ConfigOverrideWarning { value, .. } = event.event_type;
assert!(value.contains("check_client_access pcre:/etc/postfix/filter_trusted"));
assert!(value.contains("check_client_access hash:/etc/postfix/access"));
assert!(value.contains("check_recipient_access hash:/etc/postfix/recipient_access"));
}
#[test]
fn test_component_matching() {
let parser = create_parser();
// 应该匹配的行
let matching_lines = vec![
"Apr 08 17:54:30 m01 postfix/postlogd[78]: warning: /etc/postfix/main.cf, line 820: overriding earlier entry: test=value",
"Apr 10 11:17:10 m01 postfix/postlogd[83]: warning: /etc/postfix/main.cf, line 806: overriding earlier entry: another=param",
];
for line in matching_lines {
assert!(parser.matches_component(line), "Should match: {}", line);
}
// 不应该匹配的行
let non_matching_lines = vec![
"Apr 08 17:54:30 m01 postfix/qmgr[78]: info: statistics",
"Apr 08 17:54:30 m01 postfix/smtpd[78]: connect from localhost",
"Apr 08 17:54:30 m01 postfix/cleanup[78]: message-id=<test@example.com>",
];
for line in non_matching_lines {
assert!(
!parser.matches_component(line),
"Should not match: {}",
line
);
}
}
#[test]
fn test_invalid_log_lines() {
let parser = create_parser();
let invalid_lines = vec![
"Invalid log line",
"Apr 08 17:54:30 m01 postfix/qmgr[78]: info: statistics",
"Apr 08 17:54:30 m01 postfix/postlogd[78]: some other message",
"incomplete line",
];
for line in invalid_lines {
let result = parser.parse_log_line(line);
assert!(result.is_err(), "Should fail to parse: {}", line);
}
}
#[test]
fn test_supported_event_types() {
let parser = create_parser();
assert_eq!(parser.supported_event_types(), 1);
}
#[test]
fn test_parser_default() {
let parser = PostlogdParser::default();
assert_eq!(parser.supported_event_types(), 1);
}
#[test]
fn test_component_parser_parse() {
let parser = PostlogdParser::new();
// 测试配置覆盖警告
let message = "/etc/postfix/main.cf, line 820: overriding earlier entry: smtpd_recipient_restrictions=check_client_access pcre:/etc/postfix/filter_trusted";
let result = parser.parse(message);
assert!(result.is_ok());
match result.unwrap() {
crate::events::base::ComponentEvent::Postlogd(event) => {
assert_eq!(event.process_id, "0"); // 临时进程ID
let PostlogdEventType::ConfigOverrideWarning {
file_path,
line_number,
parameter,
..
} = event.event_type;
assert_eq!(file_path, "/etc/postfix/main.cf");
assert_eq!(line_number, 820);
assert_eq!(parameter, "smtpd_recipient_restrictions");
}
_ => panic!("Expected Postlogd ComponentEvent"),
}
}
#[test]
fn test_component_parser_invalid() {
let parser = PostlogdParser::new();
let message = "some invalid message";
let result = parser.parse(message);
assert!(result.is_err());
match result.unwrap_err() {
crate::error::ParseError::ComponentParseError { component, .. } => {
assert_eq!(component, "postlogd");
}
_ => panic!("Expected ComponentParseError"),
}
}
#[test]
fn test_component_name() {
let parser = PostlogdParser::new();
assert_eq!(parser.component_name(), "postlogd");
}
#[test]
fn test_can_parse() {
let parser = PostlogdParser::new();
// 应该能解析的消息
assert!(parser
.can_parse("/etc/postfix/main.cf, line 820: overriding earlier entry: test=value"));
// 不应该解析的消息
assert!(!parser.can_parse("some random message"));
assert!(!parser.can_parse("overriding earlier entry: test=value")); // 缺少main.cf
assert!(!parser.can_parse("/etc/postfix/main.cf, line 820: some other message"));
// 缺少overriding
}
}