use regex::Regex;
use crate::error::ParseError;
use crate::events::discard::{DelayBreakdown, DiscardConfigType, DiscardEvent};
use crate::events::{base::BaseEvent, ComponentEvent};
use crate::utils::common_fields::CommonFieldsParser;
use super::ComponentParser;
pub struct DiscardParser {
message_discard_regex: Regex,
config_regex: Regex,
}
impl DiscardParser {
pub fn new() -> Self {
Self {
message_discard_regex: Regex::new(
r"^([A-F0-9]+):\s+to=<([^>]+)>,\s+relay=([^,]+),\s+delay=([0-9.]+),\s+delays=([0-9./]+),\s+dsn=([0-9.]+),\s+status=(\w+)\s+\(([^)]+)\)$"
).expect("DISCARD消息丢弃正则表达式编译失败"),
config_regex: Regex::new(
r"^(starting|stopping|warning|configuration|transport).*$"
).expect("DISCARD配置正则表达式编译失败"),
}
}
pub fn parse_line(&self, line: &str, base_event: BaseEvent) -> Option<DiscardEvent> {
if let Some(captures) = self.message_discard_regex.captures(line) {
return self.parse_message_discard(captures, base_event);
}
if let Some(captures) = self.config_regex.captures(line) {
return self.parse_config_event(captures, base_event);
}
None
}
fn parse_message_discard(
&self,
captures: regex::Captures,
base_event: BaseEvent,
) -> Option<DiscardEvent> {
let full_message = base_event.raw_message.as_str();
let queue_id = captures.get(1)?.as_str().to_string();
let recipient = CommonFieldsParser::extract_to_email(full_message)
.map(|email| email.address)
.unwrap_or_else(|| captures.get(2).map_or_else(String::new, |m| m.as_str().to_string()));
let relay_info = CommonFieldsParser::extract_relay_info(full_message);
let relay = relay_info.as_ref()
.map(|r| r.hostname.clone())
.unwrap_or_else(|| captures.get(3).map_or_else(String::new, |m| m.as_str().to_string()));
let delay_info = CommonFieldsParser::extract_delay_info(full_message);
let delay = delay_info.as_ref()
.map(|d| d.total)
.unwrap_or_else(|| {
captures.get(4)
.and_then(|m| m.as_str().parse().ok())
.unwrap_or(0.0)
});
let delays = delay_info.as_ref()
.and_then(|d| d.breakdown.as_ref())
.and_then(|breakdown| {
DelayBreakdown::from_delays_string(&format!("{}/{}/{}/{}",
breakdown[0], breakdown[1], breakdown[2], breakdown[3]))
})
.or_else(|| {
captures.get(5)
.and_then(|m| DelayBreakdown::from_delays_string(m.as_str()))
})?;
let status_info = CommonFieldsParser::extract_status_info(full_message);
let dsn = status_info.as_ref()
.and_then(|s| s.dsn.clone())
.unwrap_or_else(|| captures.get(6).map_or_else(String::new, |m| m.as_str().to_string()));
let status = status_info.as_ref()
.map(|s| s.status.clone())
.unwrap_or_else(|| captures.get(7).map_or_else(String::new, |m| m.as_str().to_string()));
let discard_reason = status_info.as_ref()
.and_then(|s| s.description.clone())
.unwrap_or_else(|| captures.get(8).map_or_else(String::new, |m| m.as_str().to_string()));
Some(DiscardEvent::MessageDiscard {
base: base_event,
queue_id,
recipient,
relay,
delay,
delays,
dsn,
status,
discard_reason,
})
}
fn parse_config_event(
&self,
captures: regex::Captures,
base_event: BaseEvent,
) -> Option<DiscardEvent> {
let message = captures.get(0)?.as_str();
let config_type = if message.contains("starting") || message.contains("stopping") {
DiscardConfigType::ServiceStartup
} else if message.contains("transport") {
DiscardConfigType::TransportMapping
} else if message.contains("discard") || message.contains("rule") {
DiscardConfigType::DiscardRules
} else {
DiscardConfigType::Other
};
Some(DiscardEvent::Configuration {
base: base_event,
config_type,
details: message.to_string(),
})
}
}
impl ComponentParser for DiscardParser {
fn parse(&self, message: &str) -> Result<ComponentEvent, ParseError> {
let base_event = BaseEvent {
timestamp: chrono::Utc::now(),
hostname: "temp".to_string(),
component: "discard".to_string(),
process_id: 0,
log_level: crate::events::base::PostfixLogLevel::Info,
raw_message: message.to_string(),
};
if let Some(discard_event) = self.parse_line(message, base_event) {
Ok(ComponentEvent::Discard(discard_event))
} else {
Err(ParseError::ComponentParseError {
component: "discard".to_string(),
reason: "无法识别的discard日志格式".to_string(),
})
}
}
fn component_name(&self) -> &'static str {
"discard"
}
fn can_parse(&self, message: &str) -> bool {
self.message_discard_regex.is_match(message) || self.config_regex.is_match(message)
}
}
impl Default for DiscardParser {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::events::base::BaseEvent;
use chrono::{DateTime, Utc};
fn create_test_base_event() -> BaseEvent {
BaseEvent {
timestamp: DateTime::parse_from_rfc3339("2024-04-07T10:51:05+00:00")
.unwrap()
.with_timezone(&Utc),
hostname: "m01".to_string(),
component: "discard".to_string(),
process_id: 85,
log_level: crate::events::base::PostfixLogLevel::Info,
raw_message: "test message".to_string(),
}
}
#[test]
fn test_parse_message_discard() {
let parser = DiscardParser::new();
let base_event = create_test_base_event();
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)";
let result = parser.parse_line(message, base_event);
assert!(result.is_some());
if let Some(DiscardEvent::MessageDiscard {
queue_id,
recipient,
relay,
delay,
dsn,
status,
discard_reason,
..
}) = result {
assert_eq!(queue_id, "5A4DF1C801B0");
assert_eq!(recipient, "six@nextcloud.games");
assert_eq!(relay, "none");
assert_eq!(delay, 0.05);
assert_eq!(dsn, "2.0.0");
assert_eq!(status, "sent");
assert_eq!(discard_reason, "nextcloud.games");
} else {
panic!("解析结果类型不正确");
}
}
#[test]
fn test_parse_various_delays() {
let parser = DiscardParser::new();
let base_event = create_test_base_event();
let test_cases = vec![
("delays=0.04/0/0/0", 0.04),
("delays=0/0/0/0", 0.0),
("delays=0.01/0.02/0/0", 0.03),
];
for (delays_part, expected_total) in test_cases {
let message = format!("5A4DF1C801B0: to=<test@example.com>, relay=none, delay=0.05, {}, dsn=2.0.0, status=sent (example.com)", delays_part);
let result = parser.parse_line(&message, base_event.clone());
assert!(result.is_some());
if let Some(DiscardEvent::MessageDiscard { delays, .. }) = result {
assert!((delays.total_delay() - expected_total).abs() < 0.001);
}
}
}
#[test]
fn test_parse_config_event() {
let parser = DiscardParser::new();
let base_event = create_test_base_event();
let message = "starting mail discard service";
let result = parser.parse_line(message, base_event);
assert!(result.is_some());
if let Some(DiscardEvent::Configuration { config_type, details, .. }) = result {
assert!(matches!(config_type, DiscardConfigType::ServiceStartup));
assert_eq!(details, "starting mail discard service");
} else {
panic!("解析结果类型不正确");
}
}
#[test]
fn test_delay_breakdown_parsing() {
let delay_breakdown = DelayBreakdown::from_delays_string("0.04/0/0/0").unwrap();
assert_eq!(delay_breakdown.queue_wait, 0.04);
assert_eq!(delay_breakdown.connection_setup, 0.0);
assert_eq!(delay_breakdown.connection_time, 0.0);
assert_eq!(delay_breakdown.transmission_time, 0.0);
assert_eq!(delay_breakdown.total_delay(), 0.04);
assert!(delay_breakdown.is_fast_discard());
let delay_breakdown = DelayBreakdown::from_delays_string("0.01/0.02/0.03/0.04").unwrap();
assert_eq!(delay_breakdown.total_delay(), 0.10);
assert!(!delay_breakdown.is_fast_discard());
}
}