postfix_log_parser/components/
discard.rs1use 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
9pub struct DiscardParser {
18 message_discard_regex: Regex,
21
22 config_regex: Regex,
25}
26
27impl DiscardParser {
28 pub fn new() -> Self {
29 Self {
30 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 config_regex: Regex::new(
37 r"^(starting|stopping|warning|configuration|transport).*$"
38 ).expect("DISCARD配置正则表达式编译失败"),
39 }
40 }
41
42 pub fn parse_line(&self, line: &str, base_event: BaseEvent) -> Option<DiscardEvent> {
44 if let Some(captures) = self.message_discard_regex.captures(line) {
46 return self.parse_message_discard(captures, base_event);
47 }
48
49 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 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 let delay: f64 = delay_str.parse().ok()?;
75
76 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 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 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 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 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 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 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}