postfix_log_parser/components/
discard.rs1use regex::Regex;
33
34use crate::error::ParseError;
35use crate::events::discard::{DelayBreakdown, DiscardConfigType, DiscardEvent};
36use crate::events::{base::BaseEvent, ComponentEvent};
37use crate::utils::common_fields::CommonFieldsParser;
38
39use super::ComponentParser;
40
41pub struct DiscardParser {
50 message_discard_regex: Regex,
53
54 config_regex: Regex,
57}
58
59impl DiscardParser {
60 pub fn new() -> Self {
61 Self {
62 message_discard_regex: Regex::new(
64 r"^([A-F0-9]+):\s+to=<([^>]+)>,\s+relay=([^,]+),\s+delay=([0-9.]+),\s+delays=([0-9./]+),\s+dsn=([0-9.]+),\s+status=(\w+)\s+\(([^)]+)\)$"
65 ).expect("DISCARD消息丢弃正则表达式编译失败"),
66
67 config_regex: Regex::new(
69 r"^(starting|stopping|warning|configuration|transport).*$"
70 ).expect("DISCARD配置正则表达式编译失败"),
71 }
72 }
73
74 pub fn parse_line(&self, line: &str, base_event: BaseEvent) -> Option<DiscardEvent> {
76 if let Some(captures) = self.message_discard_regex.captures(line) {
78 return self.parse_message_discard(captures, base_event);
79 }
80
81 if let Some(captures) = self.config_regex.captures(line) {
83 return self.parse_config_event(captures, base_event);
84 }
85
86 None
87 }
88
89 fn parse_message_discard(
91 &self,
92 captures: regex::Captures,
93 base_event: BaseEvent,
94 ) -> Option<DiscardEvent> {
95 let full_message = base_event.raw_message.as_str();
96 let queue_id = captures.get(1)?.as_str().to_string();
97
98 let recipient = CommonFieldsParser::extract_to_email(full_message)
100 .map(|email| email.address)
101 .unwrap_or_else(|| captures.get(2).map_or_else(String::new, |m| m.as_str().to_string()));
102
103 let relay_info = CommonFieldsParser::extract_relay_info(full_message);
104 let relay = relay_info.as_ref()
105 .map(|r| r.hostname.clone())
106 .unwrap_or_else(|| captures.get(3).map_or_else(String::new, |m| m.as_str().to_string()));
107
108 let delay_info = CommonFieldsParser::extract_delay_info(full_message);
109 let delay = delay_info.as_ref()
110 .map(|d| d.total)
111 .unwrap_or_else(|| {
112 captures.get(4)
113 .and_then(|m| m.as_str().parse().ok())
114 .unwrap_or(0.0)
115 });
116
117 let delays = delay_info.as_ref()
119 .and_then(|d| d.breakdown.as_ref())
120 .and_then(|breakdown| {
121 DelayBreakdown::from_delays_string(&format!("{}/{}/{}/{}",
122 breakdown[0], breakdown[1], breakdown[2], breakdown[3]))
123 })
124 .or_else(|| {
125 captures.get(5)
126 .and_then(|m| DelayBreakdown::from_delays_string(m.as_str()))
127 })?;
128
129 let status_info = CommonFieldsParser::extract_status_info(full_message);
130 let dsn = status_info.as_ref()
131 .and_then(|s| s.dsn.clone())
132 .unwrap_or_else(|| captures.get(6).map_or_else(String::new, |m| m.as_str().to_string()));
133
134 let status = status_info.as_ref()
135 .map(|s| s.status.clone())
136 .unwrap_or_else(|| captures.get(7).map_or_else(String::new, |m| m.as_str().to_string()));
137
138 let discard_reason = status_info.as_ref()
139 .and_then(|s| s.description.clone())
140 .unwrap_or_else(|| captures.get(8).map_or_else(String::new, |m| m.as_str().to_string()));
141
142 Some(DiscardEvent::MessageDiscard {
143 base: base_event,
144 queue_id,
145 recipient,
146 relay,
147 delay,
148 delays,
149 dsn,
150 status,
151 discard_reason,
152 })
153 }
154
155 fn parse_config_event(
158 &self,
159 captures: regex::Captures,
160 base_event: BaseEvent,
161 ) -> Option<DiscardEvent> {
162 let message = captures.get(0)?.as_str();
163
164 let config_type = if message.contains("starting") || message.contains("stopping") {
167 DiscardConfigType::ServiceStartup
168 } else if message.contains("transport") {
169 DiscardConfigType::TransportMapping
170 } else if message.contains("discard") || message.contains("rule") {
171 DiscardConfigType::DiscardRules
172 } else {
173 DiscardConfigType::Other
174 };
175
176 Some(DiscardEvent::Configuration {
177 base: base_event,
178 config_type,
179 details: message.to_string(),
180 })
181 }
182}
183
184impl ComponentParser for DiscardParser {
185 fn parse(&self, message: &str) -> Result<ComponentEvent, ParseError> {
186 let base_event = BaseEvent {
189 timestamp: chrono::Utc::now(),
190 hostname: "temp".to_string(),
191 component: "discard".to_string(),
192 process_id: 0,
193 log_level: crate::events::base::PostfixLogLevel::Info,
194 raw_message: message.to_string(),
195 };
196
197 if let Some(discard_event) = self.parse_line(message, base_event) {
198 Ok(ComponentEvent::Discard(discard_event))
199 } else {
200 Err(ParseError::ComponentParseError {
201 component: "discard".to_string(),
202 reason: "无法识别的discard日志格式".to_string(),
203 })
204 }
205 }
206
207 fn component_name(&self) -> &'static str {
208 "discard"
209 }
210
211 fn can_parse(&self, message: &str) -> bool {
212 self.message_discard_regex.is_match(message) || self.config_regex.is_match(message)
214 }
215}
216
217impl Default for DiscardParser {
218 fn default() -> Self {
219 Self::new()
220 }
221}
222
223#[cfg(test)]
224mod tests {
225 use super::*;
226 use crate::events::base::BaseEvent;
227 use chrono::{DateTime, Utc};
228
229 fn create_test_base_event() -> BaseEvent {
230 BaseEvent {
231 timestamp: DateTime::parse_from_rfc3339("2024-04-07T10:51:05+00:00")
232 .unwrap()
233 .with_timezone(&Utc),
234 hostname: "m01".to_string(),
235 component: "discard".to_string(),
236 process_id: 85,
237 log_level: crate::events::base::PostfixLogLevel::Info,
238 raw_message: "test message".to_string(),
239 }
240 }
241
242 #[test]
243 fn test_parse_message_discard() {
244 let parser = DiscardParser::new();
245 let base_event = create_test_base_event();
246
247 let message = "5A4DF1C801B0: to=<six@nextcloud.games>, relay=none, delay=0.05, delays=0.04/0/0/0, dsn=2.0.0, status=sent (nextcloud.games)";
248
249 let result = parser.parse_line(message, base_event);
250 assert!(result.is_some());
251
252 if let Some(DiscardEvent::MessageDiscard {
253 queue_id,
254 recipient,
255 relay,
256 delay,
257 dsn,
258 status,
259 discard_reason,
260 ..
261 }) = result {
262 assert_eq!(queue_id, "5A4DF1C801B0");
263 assert_eq!(recipient, "six@nextcloud.games");
264 assert_eq!(relay, "none");
265 assert_eq!(delay, 0.05);
266 assert_eq!(dsn, "2.0.0");
267 assert_eq!(status, "sent");
268 assert_eq!(discard_reason, "nextcloud.games");
269 } else {
270 panic!("解析结果类型不正确");
271 }
272 }
273
274 #[test]
275 fn test_parse_various_delays() {
276 let parser = DiscardParser::new();
277 let base_event = create_test_base_event();
278
279 let test_cases = vec![
281 ("delays=0.04/0/0/0", 0.04),
282 ("delays=0/0/0/0", 0.0),
283 ("delays=0.01/0.02/0/0", 0.03),
284 ];
285
286 for (delays_part, expected_total) in test_cases {
287 let message = format!("5A4DF1C801B0: to=<test@example.com>, relay=none, delay=0.05, {}, dsn=2.0.0, status=sent (example.com)", delays_part);
288
289 let result = parser.parse_line(&message, base_event.clone());
290 assert!(result.is_some());
291
292 if let Some(DiscardEvent::MessageDiscard { delays, .. }) = result {
293 assert!((delays.total_delay() - expected_total).abs() < 0.001);
294 }
295 }
296 }
297
298 #[test]
299 fn test_parse_config_event() {
300 let parser = DiscardParser::new();
301 let base_event = create_test_base_event();
302
303 let message = "starting mail discard service";
304
305 let result = parser.parse_line(message, base_event);
306 assert!(result.is_some());
307
308 if let Some(DiscardEvent::Configuration { config_type, details, .. }) = result {
309 assert!(matches!(config_type, DiscardConfigType::ServiceStartup));
310 assert_eq!(details, "starting mail discard service");
311 } else {
312 panic!("解析结果类型不正确");
313 }
314 }
315
316 #[test]
317 fn test_delay_breakdown_parsing() {
318 let delay_breakdown = DelayBreakdown::from_delays_string("0.04/0/0/0").unwrap();
320 assert_eq!(delay_breakdown.queue_wait, 0.04);
321 assert_eq!(delay_breakdown.connection_setup, 0.0);
322 assert_eq!(delay_breakdown.connection_time, 0.0);
323 assert_eq!(delay_breakdown.transmission_time, 0.0);
324 assert_eq!(delay_breakdown.total_delay(), 0.04);
325 assert!(delay_breakdown.is_fast_discard());
326
327 let delay_breakdown = DelayBreakdown::from_delays_string("0.01/0.02/0.03/0.04").unwrap();
328 assert_eq!(delay_breakdown.total_delay(), 0.10);
329 assert!(!delay_breakdown.is_fast_discard());
330 }
331}