postfix_log_parser/components/
discard.rs

1use regex::Regex;
2
3use crate::error::ParseError;
4use crate::events::discard::{DelayBreakdown, DiscardConfigType, DiscardEvent};
5use crate::events::{base::BaseEvent, ComponentEvent};
6
7use super::ComponentParser;
8
9/// DISCARD组件解析器
10/// 
11/// 基于Postfix DISCARD邮件丢弃代理的真实日志格式开发
12/// DISCARD代理的特点:
13/// - 假装投递但实际丢弃邮件
14/// - 总是报告relay=nonediscard
15/// - 状态总是为sent,但DSN为2.0.0表示成功丢弃
16/// - 不进行实际网络投递,延迟时间通常很短
17pub struct DiscardParser {
18    /// 邮件丢弃事件解析 - 主要模式(95%+频率)
19    /// 格式: "queue_id: to=<recipient>, relay=none, delay=X, delays=a/b/c/d, dsn=X.X.X, status=sent (reason)"
20    message_discard_regex: Regex,
21    
22    /// 服务启动/配置事件解析
23    /// 格式: 各种配置相关消息
24    config_regex: Regex,
25}
26
27impl DiscardParser {
28    pub fn new() -> Self {
29        Self {
30            // 主要的邮件丢弃事件模式
31            message_discard_regex: Regex::new(
32                r"^([A-F0-9]+):\s+to=<([^>]+)>,\s+relay=([^,]+),\s+delay=([0-9.]+),\s+delays=([0-9./]+),\s+dsn=([0-9.]+),\s+status=(\w+)\s+\(([^)]+)\)$"
33            ).expect("DISCARD消息丢弃正则表达式编译失败"),
34            
35            // 配置和启动事件模式
36            config_regex: Regex::new(
37                r"^(starting|stopping|warning|configuration|transport).*$"
38            ).expect("DISCARD配置正则表达式编译失败"),
39        }
40    }
41
42    /// 解析DISCARD日志行
43    pub fn parse_line(&self, line: &str, base_event: BaseEvent) -> Option<DiscardEvent> {
44        // 尝试解析邮件丢弃事件(主要模式)
45        if let Some(captures) = self.message_discard_regex.captures(line) {
46            return self.parse_message_discard(captures, base_event);
47        }
48        
49        // 尝试解析配置事件
50        if let Some(captures) = self.config_regex.captures(line) {
51            return self.parse_config_event(captures, base_event);
52        }
53        
54        None
55    }
56
57    /// 解析邮件丢弃事件
58    /// 示例: "5A4DF1C801B0: to=<piggy@nextcloud.games>, relay=none, delay=0.05, delays=0.04/0/0/0, dsn=2.0.0, status=sent (nextcloud.games)"
59    fn parse_message_discard(
60        &self,
61        captures: regex::Captures,
62        base_event: BaseEvent,
63    ) -> Option<DiscardEvent> {
64        let queue_id = captures.get(1)?.as_str().to_string();
65        let recipient = captures.get(2)?.as_str().to_string();
66        let relay = captures.get(3)?.as_str().to_string();
67        let delay_str = captures.get(4)?.as_str();
68        let delays_str = captures.get(5)?.as_str();
69        let dsn = captures.get(6)?.as_str().to_string();
70        let status = captures.get(7)?.as_str().to_string();
71        let discard_reason = captures.get(8)?.as_str().to_string();
72        
73        // 解析延迟时间
74        let delay: f64 = delay_str.parse().ok()?;
75        
76        // 解析延迟细分
77        let delays = DelayBreakdown::from_delays_string(delays_str)?;
78        
79        Some(DiscardEvent::MessageDiscard {
80            base: base_event,
81            queue_id,
82            recipient,
83            relay,
84            delay,
85            delays,
86            dsn,
87            status,
88            discard_reason,
89        })
90    }
91    
92    /// 解析配置事件
93    /// 处理DISCARD服务的配置、启动、停止等事件
94    fn parse_config_event(
95        &self,
96        captures: regex::Captures,
97        base_event: BaseEvent,
98    ) -> Option<DiscardEvent> {
99        let message = captures.get(0)?.as_str();
100        
101        // 根据消息内容确定配置类型
102        // 注意:优先级很重要!更具体的匹配应该放在前面
103        let config_type = if message.contains("starting") || message.contains("stopping") {
104            DiscardConfigType::ServiceStartup
105        } else if message.contains("transport") {
106            DiscardConfigType::TransportMapping
107        } else if message.contains("discard") || message.contains("rule") {
108            DiscardConfigType::DiscardRules
109        } else {
110            DiscardConfigType::Other
111        };
112        
113        Some(DiscardEvent::Configuration {
114            base: base_event,
115            config_type,
116            details: message.to_string(),
117        })
118    }
119}
120
121impl ComponentParser for DiscardParser {
122    fn parse(&self, message: &str) -> Result<ComponentEvent, ParseError> {
123        // 创建一个临时的BaseEvent,用于解析
124        // 在实际使用中,这些字段会被MasterParser正确填充
125        let base_event = BaseEvent {
126            timestamp: chrono::Utc::now(),
127            hostname: "temp".to_string(),
128            component: "discard".to_string(),
129            process_id: 0,
130            log_level: crate::events::base::PostfixLogLevel::Info,
131            raw_message: message.to_string(),
132        };
133
134        if let Some(discard_event) = self.parse_line(message, base_event) {
135            Ok(ComponentEvent::Discard(discard_event))
136        } else {
137            Err(ParseError::ComponentParseError {
138                component: "discard".to_string(),
139                reason: "无法识别的discard日志格式".to_string(),
140            })
141        }
142    }
143    
144    fn component_name(&self) -> &'static str {
145        "discard"
146    }
147
148    fn can_parse(&self, message: &str) -> bool {
149        // 检查是否包含discard特征
150        self.message_discard_regex.is_match(message) || self.config_regex.is_match(message)
151    }
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157    use crate::events::base::BaseEvent;
158    use chrono::{DateTime, Utc};
159    
160    fn create_test_base_event() -> BaseEvent {
161        BaseEvent {
162            timestamp: DateTime::parse_from_rfc3339("2024-04-07T10:51:05+00:00")
163                .unwrap()
164                .with_timezone(&Utc),
165            hostname: "m01".to_string(),
166            component: "discard".to_string(),
167            process_id: 85,
168            log_level: crate::events::base::PostfixLogLevel::Info,
169            raw_message: "test message".to_string(),
170        }
171    }
172    
173    #[test]
174    fn test_parse_message_discard() {
175        let parser = DiscardParser::new();
176        let base_event = create_test_base_event();
177        
178        let message = "5A4DF1C801B0: to=<piggy@nextcloud.games>, relay=none, delay=0.05, delays=0.04/0/0/0, dsn=2.0.0, status=sent (nextcloud.games)";
179        
180        let result = parser.parse_line(message, base_event);
181        assert!(result.is_some());
182        
183        if let Some(DiscardEvent::MessageDiscard { 
184            queue_id, 
185            recipient, 
186            relay,
187            delay,
188            dsn,
189            status,
190            discard_reason,
191            .. 
192        }) = result {
193            assert_eq!(queue_id, "5A4DF1C801B0");
194            assert_eq!(recipient, "piggy@nextcloud.games");
195            assert_eq!(relay, "none");
196            assert_eq!(delay, 0.05);
197            assert_eq!(dsn, "2.0.0");
198            assert_eq!(status, "sent");
199            assert_eq!(discard_reason, "nextcloud.games");
200        } else {
201            panic!("解析结果类型不正确");
202        }
203    }
204    
205    #[test]
206    fn test_parse_various_delays() {
207        let parser = DiscardParser::new();
208        let base_event = create_test_base_event();
209        
210        // 测试不同的延迟格式
211        let test_cases = vec![
212            ("delays=0.04/0/0/0", 0.04),
213            ("delays=0/0/0/0", 0.0),
214            ("delays=0.01/0.02/0/0", 0.03),
215        ];
216        
217        for (delays_part, expected_total) in test_cases {
218            let message = format!("5A4DF1C801B0: to=<test@example.com>, relay=none, delay=0.05, {}, dsn=2.0.0, status=sent (example.com)", delays_part);
219            
220            let result = parser.parse_line(&message, base_event.clone());
221            assert!(result.is_some());
222            
223            if let Some(DiscardEvent::MessageDiscard { delays, .. }) = result {
224                assert!((delays.total_delay() - expected_total).abs() < 0.001);
225            }
226        }
227    }
228    
229    #[test]
230    fn test_parse_config_event() {
231        let parser = DiscardParser::new();
232        let base_event = create_test_base_event();
233        
234        let message = "starting mail discard service";
235        
236        let result = parser.parse_line(message, base_event);
237        assert!(result.is_some());
238        
239        if let Some(DiscardEvent::Configuration { config_type, details, .. }) = result {
240            assert!(matches!(config_type, DiscardConfigType::ServiceStartup));
241            assert_eq!(details, "starting mail discard service");
242        } else {
243            panic!("解析结果类型不正确");
244        }
245    }
246    
247    #[test]
248    fn test_delay_breakdown_parsing() {
249        // 测试DelayBreakdown的解析功能
250        let delay_breakdown = DelayBreakdown::from_delays_string("0.04/0/0/0").unwrap();
251        assert_eq!(delay_breakdown.queue_wait, 0.04);
252        assert_eq!(delay_breakdown.connection_setup, 0.0);
253        assert_eq!(delay_breakdown.connection_time, 0.0);
254        assert_eq!(delay_breakdown.transmission_time, 0.0);
255        assert_eq!(delay_breakdown.total_delay(), 0.04);
256        assert!(delay_breakdown.is_fast_discard());
257        
258        let delay_breakdown = DelayBreakdown::from_delays_string("0.01/0.02/0.03/0.04").unwrap();
259        assert_eq!(delay_breakdown.total_delay(), 0.10);
260        assert!(!delay_breakdown.is_fast_discard());
261    }
262}