提交qiming-mcp-proxy
This commit is contained in:
997
qiming-mcp-proxy/document-parser/src/utils/alerting.rs
Normal file
997
qiming-mcp-proxy/document-parser/src/utils/alerting.rs
Normal file
@@ -0,0 +1,997 @@
|
||||
use crate::utils::health_check::{HealthCheckResult, HealthStatus, SystemHealthStatus};
|
||||
use crate::utils::logging::{LogLevel, StructuredLogger};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant, SystemTime};
|
||||
use tokio::sync::{RwLock, mpsc};
|
||||
use uuid::Uuid;
|
||||
|
||||
/// 告警级别
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
|
||||
pub enum AlertLevel {
|
||||
Info,
|
||||
Warning,
|
||||
Critical,
|
||||
Emergency,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for AlertLevel {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
AlertLevel::Info => write!(f, "INFO"),
|
||||
AlertLevel::Warning => write!(f, "WARNING"),
|
||||
AlertLevel::Critical => write!(f, "CRITICAL"),
|
||||
AlertLevel::Emergency => write!(f, "EMERGENCY"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<HealthStatus> for AlertLevel {
|
||||
fn from(status: HealthStatus) -> Self {
|
||||
match status {
|
||||
HealthStatus::Healthy => AlertLevel::Info,
|
||||
HealthStatus::Degraded => AlertLevel::Warning,
|
||||
HealthStatus::Unhealthy => AlertLevel::Critical,
|
||||
HealthStatus::Unknown => AlertLevel::Warning,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 告警规则
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AlertRule {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub component: Option<String>, // None表示适用于所有组件
|
||||
pub condition: AlertCondition,
|
||||
pub level: AlertLevel,
|
||||
pub cooldown: Duration,
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
impl AlertRule {
|
||||
pub fn new(
|
||||
id: String,
|
||||
name: String,
|
||||
description: String,
|
||||
condition: AlertCondition,
|
||||
level: AlertLevel,
|
||||
) -> Self {
|
||||
Self {
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
component: None,
|
||||
condition,
|
||||
level,
|
||||
cooldown: Duration::from_secs(300), // 默认5分钟冷却
|
||||
enabled: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_component(mut self, component: String) -> Self {
|
||||
self.component = Some(component);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_cooldown(mut self, cooldown: Duration) -> Self {
|
||||
self.cooldown = cooldown;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn disabled(mut self) -> Self {
|
||||
self.enabled = false;
|
||||
self
|
||||
}
|
||||
|
||||
/// 检查规则是否匹配给定的健康检查结果
|
||||
pub fn matches(&self, result: &HealthCheckResult) -> bool {
|
||||
if !self.enabled {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查组件匹配
|
||||
if let Some(ref component) = self.component {
|
||||
if &result.component != component {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 检查条件匹配
|
||||
self.condition.evaluate(result)
|
||||
}
|
||||
|
||||
/// 检查规则是否匹配系统健康状态
|
||||
pub fn matches_system(&self, status: &SystemHealthStatus) -> bool {
|
||||
if !self.enabled {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 如果指定了组件,检查该组件
|
||||
if let Some(ref component) = self.component {
|
||||
if let Some(component_result) = status.get_component_status(component) {
|
||||
return self.condition.evaluate(component_result);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 否则检查整体状态
|
||||
self.condition.evaluate_system(status)
|
||||
}
|
||||
}
|
||||
|
||||
/// 告警条件
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum AlertCondition {
|
||||
/// 健康状态等于指定状态
|
||||
HealthStatusEquals(HealthStatus),
|
||||
/// 健康状态不等于指定状态
|
||||
HealthStatusNotEquals(HealthStatus),
|
||||
/// 响应时间超过阈值(毫秒)
|
||||
ResponseTimeExceeds(u64),
|
||||
/// 连续失败次数超过阈值
|
||||
ConsecutiveFailuresExceeds(u32),
|
||||
/// 错误率超过阈值(百分比)
|
||||
ErrorRateExceeds(f64),
|
||||
/// 自定义条件(使用详细信息中的键值对)
|
||||
DetailContains(String, String),
|
||||
/// 详细信息中的数值超过阈值
|
||||
DetailValueExceeds(String, f64),
|
||||
/// 组合条件(AND)
|
||||
And(Vec<AlertCondition>),
|
||||
/// 组合条件(OR)
|
||||
Or(Vec<AlertCondition>),
|
||||
}
|
||||
|
||||
impl AlertCondition {
|
||||
/// 评估条件是否满足
|
||||
pub fn evaluate(&self, result: &HealthCheckResult) -> bool {
|
||||
match self {
|
||||
AlertCondition::HealthStatusEquals(status) => result.status == *status,
|
||||
AlertCondition::HealthStatusNotEquals(status) => result.status != *status,
|
||||
AlertCondition::ResponseTimeExceeds(threshold) => result.response_time_ms > *threshold,
|
||||
AlertCondition::DetailContains(key, value) => result.details.get(key) == Some(value),
|
||||
AlertCondition::DetailValueExceeds(key, threshold) => result
|
||||
.details
|
||||
.get(key)
|
||||
.and_then(|v| v.parse::<f64>().ok())
|
||||
.is_some_and(|v| v > *threshold),
|
||||
AlertCondition::And(conditions) => conditions.iter().all(|c| c.evaluate(result)),
|
||||
AlertCondition::Or(conditions) => conditions.iter().any(|c| c.evaluate(result)),
|
||||
// 这些条件需要历史数据,暂时返回false
|
||||
AlertCondition::ConsecutiveFailuresExceeds(_) => false,
|
||||
AlertCondition::ErrorRateExceeds(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// 评估系统级条件
|
||||
pub fn evaluate_system(&self, status: &SystemHealthStatus) -> bool {
|
||||
match self {
|
||||
AlertCondition::HealthStatusEquals(health_status) => {
|
||||
status.overall_status == *health_status
|
||||
}
|
||||
AlertCondition::HealthStatusNotEquals(health_status) => {
|
||||
status.overall_status != *health_status
|
||||
}
|
||||
AlertCondition::ErrorRateExceeds(threshold) => {
|
||||
let total = status.components.len() as f64;
|
||||
if total == 0.0 {
|
||||
return false;
|
||||
}
|
||||
let error_rate = (status.unhealthy_count as f64 / total) * 100.0;
|
||||
error_rate > *threshold
|
||||
}
|
||||
AlertCondition::And(conditions) => conditions.iter().all(|c| c.evaluate_system(status)),
|
||||
AlertCondition::Or(conditions) => conditions.iter().any(|c| c.evaluate_system(status)),
|
||||
_ => false, // 其他条件不适用于系统级评估
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 告警事件
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AlertEvent {
|
||||
pub id: String,
|
||||
pub rule_id: String,
|
||||
pub rule_name: String,
|
||||
pub level: AlertLevel,
|
||||
pub component: Option<String>,
|
||||
pub message: String,
|
||||
pub details: HashMap<String, String>,
|
||||
pub timestamp: u64,
|
||||
pub resolved: bool,
|
||||
pub resolved_at: Option<u64>,
|
||||
}
|
||||
|
||||
impl AlertEvent {
|
||||
pub fn new(
|
||||
rule: &AlertRule,
|
||||
component: Option<String>,
|
||||
message: String,
|
||||
details: HashMap<String, String>,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: Uuid::new_v4().to_string(),
|
||||
rule_id: rule.id.clone(),
|
||||
rule_name: rule.name.clone(),
|
||||
level: rule.level.clone(),
|
||||
component,
|
||||
message,
|
||||
details,
|
||||
timestamp: SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs(),
|
||||
resolved: false,
|
||||
resolved_at: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolve(&mut self) {
|
||||
self.resolved = true;
|
||||
self.resolved_at = Some(
|
||||
SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs(),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn is_active(&self) -> bool {
|
||||
!self.resolved
|
||||
}
|
||||
|
||||
pub fn duration(&self) -> Option<Duration> {
|
||||
if let Some(resolved_at) = self.resolved_at {
|
||||
Some(Duration::from_secs(resolved_at - self.timestamp))
|
||||
} else {
|
||||
Some(Duration::from_secs(
|
||||
SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs()
|
||||
- self.timestamp,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 告警通知器trait
|
||||
#[async_trait::async_trait]
|
||||
pub trait AlertNotifier: Send + Sync {
|
||||
async fn send_alert(
|
||||
&self,
|
||||
event: &AlertEvent,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>>;
|
||||
async fn send_resolution(
|
||||
&self,
|
||||
event: &AlertEvent,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>>;
|
||||
fn name(&self) -> &str;
|
||||
}
|
||||
|
||||
/// 日志告警通知器
|
||||
pub struct LogAlertNotifier {
|
||||
logger: Arc<StructuredLogger>,
|
||||
}
|
||||
|
||||
impl LogAlertNotifier {
|
||||
pub fn new(logger: Arc<StructuredLogger>) -> Self {
|
||||
Self { logger }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl AlertNotifier for LogAlertNotifier {
|
||||
async fn send_alert(
|
||||
&self,
|
||||
event: &AlertEvent,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let log_level = match event.level {
|
||||
AlertLevel::Info => LogLevel::Info,
|
||||
AlertLevel::Warning => LogLevel::Warn,
|
||||
AlertLevel::Critical => LogLevel::Error,
|
||||
AlertLevel::Emergency => LogLevel::Error,
|
||||
};
|
||||
|
||||
let mut fields = HashMap::new();
|
||||
fields.insert("alert_id".to_string(), event.id.clone());
|
||||
fields.insert("rule_id".to_string(), event.rule_id.clone());
|
||||
fields.insert("rule_name".to_string(), event.rule_name.clone());
|
||||
fields.insert("alert_level".to_string(), event.level.to_string());
|
||||
|
||||
if let Some(ref component) = event.component {
|
||||
fields.insert("component".to_string(), component.clone());
|
||||
}
|
||||
|
||||
for (k, v) in &event.details {
|
||||
fields.insert(format!("detail_{k}"), v.clone());
|
||||
}
|
||||
|
||||
let entry = crate::utils::logging::LogEntry::new(
|
||||
log_level,
|
||||
format!("ALERT: {}", event.message),
|
||||
"alerting".to_string(),
|
||||
)
|
||||
.with_field("alert_id", &event.id)
|
||||
.with_field("rule_id", &event.rule_id)
|
||||
.with_field("rule_name", &event.rule_name)
|
||||
.with_field("alert_level", event.level.to_string());
|
||||
|
||||
let mut final_entry = entry;
|
||||
for (k, v) in &fields {
|
||||
final_entry = final_entry.with_field(k, v);
|
||||
}
|
||||
|
||||
self.logger.log(final_entry).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_resolution(
|
||||
&self,
|
||||
event: &AlertEvent,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let mut fields = HashMap::new();
|
||||
fields.insert("alert_id".to_string(), event.id.clone());
|
||||
fields.insert("rule_id".to_string(), event.rule_id.clone());
|
||||
fields.insert("rule_name".to_string(), event.rule_name.clone());
|
||||
fields.insert("alert_level".to_string(), event.level.to_string());
|
||||
|
||||
if let Some(duration) = event.duration() {
|
||||
fields.insert(
|
||||
"duration_seconds".to_string(),
|
||||
duration.as_secs().to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(ref component) = event.component {
|
||||
fields.insert("component".to_string(), component.clone());
|
||||
}
|
||||
|
||||
let entry = crate::utils::logging::LogEntry::new(
|
||||
LogLevel::Info,
|
||||
format!("ALERT RESOLVED: {}", event.message),
|
||||
"alerting".to_string(),
|
||||
)
|
||||
.with_field("alert_id", &event.id)
|
||||
.with_field("rule_id", &event.rule_id)
|
||||
.with_field("rule_name", &event.rule_name)
|
||||
.with_field("alert_level", event.level.to_string());
|
||||
|
||||
let mut final_entry = entry;
|
||||
for (k, v) in &fields {
|
||||
final_entry = final_entry.with_field(k, v);
|
||||
}
|
||||
|
||||
self.logger.log(final_entry).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn name(&self) -> &str {
|
||||
"log"
|
||||
}
|
||||
}
|
||||
|
||||
/// 控制台告警通知器
|
||||
#[derive(Debug)]
|
||||
pub struct ConsoleAlertNotifier;
|
||||
|
||||
impl Default for ConsoleAlertNotifier {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl ConsoleAlertNotifier {
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl AlertNotifier for ConsoleAlertNotifier {
|
||||
async fn send_alert(
|
||||
&self,
|
||||
event: &AlertEvent,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let color_code = match event.level {
|
||||
AlertLevel::Info => "\x1b[36m", // 青色
|
||||
AlertLevel::Warning => "\x1b[33m", // 黄色
|
||||
AlertLevel::Critical => "\x1b[31m", // 红色
|
||||
AlertLevel::Emergency => "\x1b[35m", // 紫色
|
||||
};
|
||||
let reset_code = "\x1b[0m";
|
||||
|
||||
println!(
|
||||
"{}🚨 ALERT [{}] {}: {}{}\n Rule: {} ({})\n Component: {}\n Time: {}",
|
||||
color_code,
|
||||
event.level,
|
||||
event.component.as_deref().unwrap_or("SYSTEM"),
|
||||
event.message,
|
||||
reset_code,
|
||||
event.rule_name,
|
||||
event.rule_id,
|
||||
event.component.as_deref().unwrap_or("N/A"),
|
||||
chrono::DateTime::from_timestamp(event.timestamp as i64, 0)
|
||||
.map(|dt| dt.format("%Y-%m-%d %H:%M:%S UTC").to_string())
|
||||
.unwrap_or_else(|| "Unknown".to_string())
|
||||
);
|
||||
|
||||
if !event.details.is_empty() {
|
||||
println!(" Details:");
|
||||
for (key, value) in &event.details {
|
||||
println!(" {key}: {value}");
|
||||
}
|
||||
}
|
||||
println!();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_resolution(
|
||||
&self,
|
||||
event: &AlertEvent,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let duration_str = event
|
||||
.duration()
|
||||
.map(|d| format!("{:.1}s", d.as_secs_f64()))
|
||||
.unwrap_or_else(|| "Unknown".to_string());
|
||||
|
||||
println!(
|
||||
"\x1b[32m✅ ALERT RESOLVED [{}] {}: {}\x1b[0m\n Rule: {} ({})\n Duration: {}\n",
|
||||
event.level,
|
||||
event.component.as_deref().unwrap_or("SYSTEM"),
|
||||
event.message,
|
||||
event.rule_name,
|
||||
event.rule_id,
|
||||
duration_str
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn name(&self) -> &str {
|
||||
"console"
|
||||
}
|
||||
}
|
||||
|
||||
/// Webhook告警通知器
|
||||
#[derive(Debug)]
|
||||
pub struct WebhookAlertNotifier {
|
||||
webhook_url: String,
|
||||
client: reqwest::Client,
|
||||
}
|
||||
|
||||
impl WebhookAlertNotifier {
|
||||
pub fn new(webhook_url: String) -> Self {
|
||||
Self {
|
||||
webhook_url,
|
||||
client: reqwest::Client::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl AlertNotifier for WebhookAlertNotifier {
|
||||
async fn send_alert(
|
||||
&self,
|
||||
event: &AlertEvent,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let payload = serde_json::json!({
|
||||
"type": "alert",
|
||||
"event": event
|
||||
});
|
||||
|
||||
let response = self
|
||||
.client
|
||||
.post(&self.webhook_url)
|
||||
.json(&payload)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(
|
||||
format!("Webhook request failed with status: {}", response.status()).into(),
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_resolution(
|
||||
&self,
|
||||
event: &AlertEvent,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let payload = serde_json::json!({
|
||||
"type": "resolution",
|
||||
"event": event
|
||||
});
|
||||
|
||||
let response = self
|
||||
.client
|
||||
.post(&self.webhook_url)
|
||||
.json(&payload)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(
|
||||
format!("Webhook request failed with status: {}", response.status()).into(),
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn name(&self) -> &str {
|
||||
"webhook"
|
||||
}
|
||||
}
|
||||
|
||||
/// 告警历史记录
|
||||
#[derive(Debug)]
|
||||
struct AlertHistory {
|
||||
_rule_id: String,
|
||||
last_triggered: Option<Instant>,
|
||||
consecutive_failures: u32,
|
||||
total_triggers: u64,
|
||||
}
|
||||
|
||||
impl AlertHistory {
|
||||
fn new(rule_id: String) -> Self {
|
||||
Self {
|
||||
_rule_id: rule_id,
|
||||
last_triggered: None,
|
||||
consecutive_failures: 0,
|
||||
total_triggers: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn can_trigger(&self, cooldown: Duration) -> bool {
|
||||
match self.last_triggered {
|
||||
Some(last) => last.elapsed() >= cooldown,
|
||||
None => true,
|
||||
}
|
||||
}
|
||||
|
||||
fn record_trigger(&mut self) {
|
||||
self.last_triggered = Some(Instant::now());
|
||||
self.consecutive_failures += 1;
|
||||
self.total_triggers += 1;
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn reset_failures(&mut self) {
|
||||
self.consecutive_failures = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// 告警管理器
|
||||
pub struct AlertManager {
|
||||
rules: Arc<RwLock<HashMap<String, AlertRule>>>,
|
||||
notifiers: Arc<RwLock<Vec<Arc<dyn AlertNotifier>>>>,
|
||||
active_alerts: Arc<RwLock<HashMap<String, AlertEvent>>>,
|
||||
alert_history: Arc<RwLock<HashMap<String, AlertHistory>>>,
|
||||
event_sender: mpsc::UnboundedSender<AlertEvent>,
|
||||
event_receiver: Arc<RwLock<Option<mpsc::UnboundedReceiver<AlertEvent>>>>,
|
||||
}
|
||||
|
||||
impl AlertManager {
|
||||
pub fn new() -> Self {
|
||||
let (event_sender, event_receiver) = mpsc::unbounded_channel();
|
||||
|
||||
Self {
|
||||
rules: Arc::new(RwLock::new(HashMap::new())),
|
||||
notifiers: Arc::new(RwLock::new(Vec::new())),
|
||||
active_alerts: Arc::new(RwLock::new(HashMap::new())),
|
||||
alert_history: Arc::new(RwLock::new(HashMap::new())),
|
||||
event_sender,
|
||||
event_receiver: Arc::new(RwLock::new(Some(event_receiver))),
|
||||
}
|
||||
}
|
||||
|
||||
/// 添加告警规则
|
||||
pub async fn add_rule(&self, rule: AlertRule) {
|
||||
let mut rules = self.rules.write().await;
|
||||
rules.insert(rule.id.clone(), rule);
|
||||
}
|
||||
|
||||
/// 移除告警规则
|
||||
pub async fn remove_rule(&self, rule_id: &str) -> bool {
|
||||
let mut rules = self.rules.write().await;
|
||||
rules.remove(rule_id).is_some()
|
||||
}
|
||||
|
||||
/// 获取告警规则
|
||||
pub async fn get_rule(&self, rule_id: &str) -> Option<AlertRule> {
|
||||
let rules = self.rules.read().await;
|
||||
rules.get(rule_id).cloned()
|
||||
}
|
||||
|
||||
/// 获取所有告警规则
|
||||
pub async fn get_all_rules(&self) -> Vec<AlertRule> {
|
||||
let rules = self.rules.read().await;
|
||||
rules.values().cloned().collect()
|
||||
}
|
||||
|
||||
/// 添加通知器
|
||||
pub async fn add_notifier(&self, notifier: Arc<dyn AlertNotifier>) {
|
||||
let mut notifiers = self.notifiers.write().await;
|
||||
notifiers.push(notifier);
|
||||
}
|
||||
|
||||
/// 处理健康检查结果
|
||||
pub async fn process_health_check(&self, result: &HealthCheckResult) {
|
||||
let rules = self.rules.read().await;
|
||||
|
||||
for rule in rules.values() {
|
||||
if rule.matches(result) {
|
||||
self.trigger_alert(
|
||||
rule,
|
||||
Some(result.component.clone()),
|
||||
&result.message,
|
||||
result.details.clone(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 处理系统健康状态
|
||||
pub async fn process_system_health(&self, status: &SystemHealthStatus) {
|
||||
let rules = self.rules.read().await;
|
||||
|
||||
for rule in rules.values() {
|
||||
if rule.matches_system(status) {
|
||||
let message = format!(
|
||||
"System health issue: {} healthy, {} degraded, {} unhealthy",
|
||||
status.healthy_count, status.degraded_count, status.unhealthy_count
|
||||
);
|
||||
|
||||
let mut details = HashMap::new();
|
||||
details.insert(
|
||||
"overall_status".to_string(),
|
||||
status.overall_status.to_string(),
|
||||
);
|
||||
details.insert(
|
||||
"healthy_count".to_string(),
|
||||
status.healthy_count.to_string(),
|
||||
);
|
||||
details.insert(
|
||||
"degraded_count".to_string(),
|
||||
status.degraded_count.to_string(),
|
||||
);
|
||||
details.insert(
|
||||
"unhealthy_count".to_string(),
|
||||
status.unhealthy_count.to_string(),
|
||||
);
|
||||
details.insert(
|
||||
"total_response_time_ms".to_string(),
|
||||
status.total_response_time_ms.to_string(),
|
||||
);
|
||||
|
||||
self.trigger_alert(rule, None, &message, details).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 触发告警
|
||||
async fn trigger_alert(
|
||||
&self,
|
||||
rule: &AlertRule,
|
||||
component: Option<String>,
|
||||
message: &str,
|
||||
details: HashMap<String, String>,
|
||||
) {
|
||||
// 检查冷却时间
|
||||
{
|
||||
let mut history = self.alert_history.write().await;
|
||||
let alert_history = history
|
||||
.entry(rule.id.clone())
|
||||
.or_insert_with(|| AlertHistory::new(rule.id.clone()));
|
||||
|
||||
if !alert_history.can_trigger(rule.cooldown) {
|
||||
return;
|
||||
}
|
||||
|
||||
alert_history.record_trigger();
|
||||
}
|
||||
|
||||
// 创建告警事件
|
||||
let event = AlertEvent::new(rule, component, message.to_string(), details);
|
||||
|
||||
// 存储活跃告警
|
||||
{
|
||||
let mut active_alerts = self.active_alerts.write().await;
|
||||
active_alerts.insert(event.id.clone(), event.clone());
|
||||
}
|
||||
|
||||
// 发送事件
|
||||
if let Err(e) = self.event_sender.send(event) {
|
||||
eprintln!("Failed to send alert event: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
/// 解决告警
|
||||
pub async fn resolve_alert(&self, alert_id: &str) -> bool {
|
||||
let mut active_alerts = self.active_alerts.write().await;
|
||||
|
||||
if let Some(mut event) = active_alerts.remove(alert_id) {
|
||||
event.resolve();
|
||||
|
||||
// 发送解决事件
|
||||
if let Err(e) = self.event_sender.send(event) {
|
||||
eprintln!("Failed to send alert resolution event: {e}");
|
||||
}
|
||||
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取活跃告警
|
||||
pub async fn get_active_alerts(&self) -> Vec<AlertEvent> {
|
||||
let active_alerts = self.active_alerts.read().await;
|
||||
active_alerts.values().cloned().collect()
|
||||
}
|
||||
|
||||
/// 获取告警统计
|
||||
pub async fn get_alert_stats(&self) -> AlertStats {
|
||||
let active_alerts = self.active_alerts.read().await;
|
||||
let history = self.alert_history.read().await;
|
||||
|
||||
let total_active = active_alerts.len();
|
||||
let critical_count = active_alerts
|
||||
.values()
|
||||
.filter(|a| matches!(a.level, AlertLevel::Critical | AlertLevel::Emergency))
|
||||
.count();
|
||||
|
||||
let total_triggered = history.values().map(|h| h.total_triggers).sum();
|
||||
|
||||
AlertStats {
|
||||
total_active,
|
||||
critical_count,
|
||||
total_triggered,
|
||||
rules_count: self.rules.read().await.len(),
|
||||
notifiers_count: self.notifiers.read().await.len(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 启动事件处理器
|
||||
pub async fn start_event_processor(&self) {
|
||||
let notifiers = self.notifiers.clone();
|
||||
let receiver = {
|
||||
let mut event_receiver = self.event_receiver.write().await;
|
||||
event_receiver.take()
|
||||
};
|
||||
|
||||
if let Some(mut receiver) = receiver {
|
||||
tokio::spawn(async move {
|
||||
while let Some(event) = receiver.recv().await {
|
||||
let notifiers_guard = notifiers.read().await;
|
||||
|
||||
for notifier in notifiers_guard.iter() {
|
||||
let result = if event.resolved {
|
||||
notifier.send_resolution(&event).await
|
||||
} else {
|
||||
notifier.send_alert(&event).await
|
||||
};
|
||||
|
||||
if let Err(e) = result {
|
||||
eprintln!("Failed to send notification via {}: {}", notifier.name(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// 创建默认规则
|
||||
pub async fn create_default_rules(&self) {
|
||||
// 组件不健康规则
|
||||
let unhealthy_rule = AlertRule::new(
|
||||
"component_unhealthy".to_string(),
|
||||
"Component Unhealthy".to_string(),
|
||||
"Triggered when a component becomes unhealthy".to_string(),
|
||||
AlertCondition::HealthStatusEquals(HealthStatus::Unhealthy),
|
||||
AlertLevel::Critical,
|
||||
);
|
||||
self.add_rule(unhealthy_rule).await;
|
||||
|
||||
// 组件降级规则
|
||||
let degraded_rule = AlertRule::new(
|
||||
"component_degraded".to_string(),
|
||||
"Component Degraded".to_string(),
|
||||
"Triggered when a component becomes degraded".to_string(),
|
||||
AlertCondition::HealthStatusEquals(HealthStatus::Degraded),
|
||||
AlertLevel::Warning,
|
||||
);
|
||||
self.add_rule(degraded_rule).await;
|
||||
|
||||
// 响应时间过长规则
|
||||
let slow_response_rule = AlertRule::new(
|
||||
"slow_response".to_string(),
|
||||
"Slow Response Time".to_string(),
|
||||
"Triggered when response time exceeds 30 seconds".to_string(),
|
||||
AlertCondition::ResponseTimeExceeds(30000),
|
||||
AlertLevel::Warning,
|
||||
);
|
||||
self.add_rule(slow_response_rule).await;
|
||||
|
||||
// 系统错误率过高规则
|
||||
let high_error_rate_rule = AlertRule::new(
|
||||
"high_error_rate".to_string(),
|
||||
"High Error Rate".to_string(),
|
||||
"Triggered when error rate exceeds 50%".to_string(),
|
||||
AlertCondition::ErrorRateExceeds(50.0),
|
||||
AlertLevel::Critical,
|
||||
);
|
||||
self.add_rule(high_error_rate_rule).await;
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for AlertManager {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// 告警统计信息
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AlertStats {
|
||||
pub total_active: usize,
|
||||
pub critical_count: usize,
|
||||
pub total_triggered: u64,
|
||||
pub rules_count: usize,
|
||||
pub notifiers_count: usize,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::utils::health_check::HealthCheckResult;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
struct MockNotifier {
|
||||
name: String,
|
||||
alert_count: Arc<AtomicUsize>,
|
||||
resolution_count: Arc<AtomicUsize>,
|
||||
}
|
||||
|
||||
impl MockNotifier {
|
||||
fn new(name: String) -> Self {
|
||||
Self {
|
||||
name,
|
||||
alert_count: Arc::new(AtomicUsize::new(0)),
|
||||
resolution_count: Arc::new(AtomicUsize::new(0)),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_alert_count(&self) -> usize {
|
||||
self.alert_count.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
fn get_resolution_count(&self) -> usize {
|
||||
self.resolution_count.load(Ordering::Relaxed)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl AlertNotifier for MockNotifier {
|
||||
async fn send_alert(
|
||||
&self,
|
||||
_event: &AlertEvent,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
self.alert_count.fetch_add(1, Ordering::Relaxed);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_resolution(
|
||||
&self,
|
||||
_event: &AlertEvent,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
self.resolution_count.fetch_add(1, Ordering::Relaxed);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_alert_rule_matching() {
|
||||
let rule = AlertRule::new(
|
||||
"test_rule".to_string(),
|
||||
"Test Rule".to_string(),
|
||||
"Test description".to_string(),
|
||||
AlertCondition::HealthStatusEquals(HealthStatus::Unhealthy),
|
||||
AlertLevel::Critical,
|
||||
);
|
||||
|
||||
let unhealthy_result = HealthCheckResult::new(
|
||||
"test_component".to_string(),
|
||||
HealthStatus::Unhealthy,
|
||||
"Test message".to_string(),
|
||||
);
|
||||
|
||||
let healthy_result = HealthCheckResult::new(
|
||||
"test_component".to_string(),
|
||||
HealthStatus::Healthy,
|
||||
"Test message".to_string(),
|
||||
);
|
||||
|
||||
assert!(rule.matches(&unhealthy_result));
|
||||
assert!(!rule.matches(&healthy_result));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_alert_condition_evaluation() {
|
||||
let condition = AlertCondition::And(vec![
|
||||
AlertCondition::HealthStatusEquals(HealthStatus::Unhealthy),
|
||||
AlertCondition::ResponseTimeExceeds(1000),
|
||||
]);
|
||||
|
||||
let mut result = HealthCheckResult::new(
|
||||
"test_component".to_string(),
|
||||
HealthStatus::Unhealthy,
|
||||
"Test message".to_string(),
|
||||
);
|
||||
result.response_time_ms = 2000;
|
||||
|
||||
assert!(condition.evaluate(&result));
|
||||
|
||||
result.response_time_ms = 500;
|
||||
assert!(!condition.evaluate(&result));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_alert_manager() {
|
||||
let manager = AlertManager::new();
|
||||
let notifier = Arc::new(MockNotifier::new("test".to_string()));
|
||||
|
||||
manager.add_notifier(notifier.clone()).await;
|
||||
manager.start_event_processor().await;
|
||||
|
||||
let rule = AlertRule::new(
|
||||
"test_rule".to_string(),
|
||||
"Test Rule".to_string(),
|
||||
"Test description".to_string(),
|
||||
AlertCondition::HealthStatusEquals(HealthStatus::Unhealthy),
|
||||
AlertLevel::Critical,
|
||||
)
|
||||
.with_cooldown(Duration::from_millis(100));
|
||||
|
||||
manager.add_rule(rule).await;
|
||||
|
||||
let unhealthy_result = HealthCheckResult::new(
|
||||
"test_component".to_string(),
|
||||
HealthStatus::Unhealthy,
|
||||
"Test message".to_string(),
|
||||
);
|
||||
|
||||
manager.process_health_check(&unhealthy_result).await;
|
||||
|
||||
// 等待事件处理
|
||||
tokio::time::sleep(Duration::from_millis(50)).await;
|
||||
|
||||
assert_eq!(notifier.get_alert_count(), 1);
|
||||
assert_eq!(manager.get_active_alerts().await.len(), 1);
|
||||
|
||||
// 测试冷却时间
|
||||
manager.process_health_check(&unhealthy_result).await;
|
||||
tokio::time::sleep(Duration::from_millis(50)).await;
|
||||
assert_eq!(notifier.get_alert_count(), 1); // 应该还是1,因为冷却时间
|
||||
|
||||
// 等待冷却时间过去
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
manager.process_health_check(&unhealthy_result).await;
|
||||
tokio::time::sleep(Duration::from_millis(50)).await;
|
||||
assert_eq!(notifier.get_alert_count(), 2); // 现在应该是2
|
||||
}
|
||||
}
|
||||
4921
qiming-mcp-proxy/document-parser/src/utils/environment_manager.rs
Normal file
4921
qiming-mcp-proxy/document-parser/src/utils/environment_manager.rs
Normal file
File diff suppressed because it is too large
Load Diff
68
qiming-mcp-proxy/document-parser/src/utils/file_utils.rs
Normal file
68
qiming-mcp-proxy/document-parser/src/utils/file_utils.rs
Normal file
@@ -0,0 +1,68 @@
|
||||
use crate::error::AppError;
|
||||
use regex::Regex;
|
||||
use std::path::Path;
|
||||
|
||||
/// 检查文件是否存在
|
||||
pub fn file_exists(file_path: &str) -> bool {
|
||||
Path::new(file_path).exists()
|
||||
}
|
||||
|
||||
/// 获取文件大小
|
||||
pub fn get_file_size(file_path: &str) -> Result<u64, AppError> {
|
||||
let metadata = std::fs::metadata(file_path)
|
||||
.map_err(|e| AppError::File(format!("无法获取文件元数据: {e}")))?;
|
||||
Ok(metadata.len())
|
||||
}
|
||||
|
||||
/// 获取文件扩展名
|
||||
pub fn get_file_extension(file_path: &str) -> Option<String> {
|
||||
Path::new(file_path)
|
||||
.extension()
|
||||
.and_then(|ext| ext.to_str())
|
||||
.map(|s| s.to_lowercase())
|
||||
}
|
||||
|
||||
/// 创建临时目录
|
||||
pub fn create_temp_dir(dir_path: &str) -> Result<(), AppError> {
|
||||
if !Path::new(dir_path).exists() {
|
||||
std::fs::create_dir_all(dir_path)
|
||||
.map_err(|e| AppError::File(format!("无法创建临时目录: {e}")))?
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 验证文件大小
|
||||
pub fn validate_file_size(file_size: u64, max_size: u64) -> Result<(), AppError> {
|
||||
if file_size > max_size {
|
||||
return Err(AppError::Validation(format!(
|
||||
"文件大小超过限制: {file_size} > {max_size} 字节"
|
||||
)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 清理文件名,移除不安全字符
|
||||
pub fn sanitize_filename(filename: &str) -> Result<String, AppError> {
|
||||
if filename.is_empty() {
|
||||
return Err(AppError::Validation("文件名不能为空".to_string()));
|
||||
}
|
||||
|
||||
// 移除路径分隔符和其他不安全字符
|
||||
let unsafe_chars = Regex::new(r#"[<>:"/\\|?*\x00-\x1f]"#).unwrap();
|
||||
let sanitized = unsafe_chars.replace_all(filename, "_").to_string();
|
||||
|
||||
// 移除开头和结尾的点和空格
|
||||
let sanitized = sanitized.trim_matches(|c| c == '.' || c == ' ').to_string();
|
||||
|
||||
if sanitized.is_empty() {
|
||||
return Err(AppError::Validation("清理后的文件名为空".to_string()));
|
||||
}
|
||||
|
||||
// 限制文件名长度
|
||||
if sanitized.len() > 255 {
|
||||
let truncated = sanitized.chars().take(255).collect::<String>();
|
||||
Ok(truncated)
|
||||
} else {
|
||||
Ok(sanitized)
|
||||
}
|
||||
}
|
||||
36
qiming-mcp-proxy/document-parser/src/utils/format_utils.rs
Normal file
36
qiming-mcp-proxy/document-parser/src/utils/format_utils.rs
Normal file
@@ -0,0 +1,36 @@
|
||||
use crate::error::AppError;
|
||||
use crate::models::DocumentFormat;
|
||||
|
||||
/// 从文件路径检测文档格式
|
||||
pub fn detect_format_from_path(file_path: &str) -> Result<DocumentFormat, AppError> {
|
||||
let extension = super::file_utils::get_file_extension(file_path)
|
||||
.ok_or_else(|| AppError::UnsupportedFormat("无法识别文件扩展名".to_string()))?;
|
||||
|
||||
Ok(DocumentFormat::from_extension(&extension))
|
||||
}
|
||||
|
||||
/// 从MIME类型检测文档格式
|
||||
pub fn detect_format_from_mime(mime_type: &str) -> DocumentFormat {
|
||||
DocumentFormat::from_mime_type(mime_type)
|
||||
}
|
||||
|
||||
/// 检查格式是否支持
|
||||
pub fn is_format_supported(format: &DocumentFormat) -> bool {
|
||||
format.is_supported()
|
||||
}
|
||||
|
||||
/// 获取支持格式列表
|
||||
pub fn get_supported_formats() -> Vec<DocumentFormat> {
|
||||
vec![
|
||||
DocumentFormat::PDF,
|
||||
DocumentFormat::Word,
|
||||
DocumentFormat::Excel,
|
||||
DocumentFormat::PowerPoint,
|
||||
DocumentFormat::Image,
|
||||
DocumentFormat::Audio,
|
||||
DocumentFormat::HTML,
|
||||
DocumentFormat::Text,
|
||||
DocumentFormat::Txt,
|
||||
DocumentFormat::Md,
|
||||
]
|
||||
}
|
||||
1193
qiming-mcp-proxy/document-parser/src/utils/health_check.rs
Normal file
1193
qiming-mcp-proxy/document-parser/src/utils/health_check.rs
Normal file
File diff suppressed because it is too large
Load Diff
861
qiming-mcp-proxy/document-parser/src/utils/logging.rs
Normal file
861
qiming-mcp-proxy/document-parser/src/utils/logging.rs
Normal file
@@ -0,0 +1,861 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::time::SystemTime;
|
||||
use tokio::sync::RwLock;
|
||||
use tracing::{info, instrument, warn};
|
||||
use tracing_subscriber::EnvFilter;
|
||||
use uuid::Uuid;
|
||||
|
||||
/// 日志级别
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
|
||||
pub enum LogLevel {
|
||||
Trace = 0,
|
||||
Debug = 1,
|
||||
Info = 2,
|
||||
Warn = 3,
|
||||
Error = 4,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for LogLevel {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
LogLevel::Trace => write!(f, "TRACE"),
|
||||
LogLevel::Debug => write!(f, "DEBUG"),
|
||||
LogLevel::Info => write!(f, "INFO"),
|
||||
LogLevel::Warn => write!(f, "WARN"),
|
||||
LogLevel::Error => write!(f, "ERROR"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for LogLevel {
|
||||
fn from(s: &str) -> Self {
|
||||
match s.to_uppercase().as_str() {
|
||||
"TRACE" => LogLevel::Trace,
|
||||
"DEBUG" => LogLevel::Debug,
|
||||
"INFO" => LogLevel::Info,
|
||||
"WARN" => LogLevel::Warn,
|
||||
"ERROR" => LogLevel::Error,
|
||||
_ => LogLevel::Info,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 关联ID管理器
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct CorrelationContext {
|
||||
pub request_id: Option<String>,
|
||||
pub task_id: Option<String>,
|
||||
pub user_id: Option<String>,
|
||||
pub session_id: Option<String>,
|
||||
pub trace_id: Option<String>,
|
||||
pub span_id: Option<String>,
|
||||
}
|
||||
|
||||
impl CorrelationContext {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn with_request_id(mut self, request_id: String) -> Self {
|
||||
self.request_id = Some(request_id);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_task_id(mut self, task_id: String) -> Self {
|
||||
self.task_id = Some(task_id);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_user_id(mut self, user_id: String) -> Self {
|
||||
self.user_id = Some(user_id);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_session_id(mut self, session_id: String) -> Self {
|
||||
self.session_id = Some(session_id);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_trace_id(mut self, trace_id: String) -> Self {
|
||||
self.trace_id = Some(trace_id);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_span_id(mut self, span_id: String) -> Self {
|
||||
self.span_id = Some(span_id);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn generate_request_id(&mut self) -> String {
|
||||
let id = Uuid::new_v4().to_string();
|
||||
self.request_id = Some(id.clone());
|
||||
id
|
||||
}
|
||||
|
||||
pub fn generate_trace_id(&mut self) -> String {
|
||||
let id = Uuid::new_v4().to_string();
|
||||
self.trace_id = Some(id.clone());
|
||||
id
|
||||
}
|
||||
|
||||
pub fn to_fields(&self) -> HashMap<String, String> {
|
||||
let mut fields = HashMap::new();
|
||||
|
||||
if let Some(ref request_id) = self.request_id {
|
||||
fields.insert("request_id".to_string(), request_id.clone());
|
||||
}
|
||||
if let Some(ref task_id) = self.task_id {
|
||||
fields.insert("task_id".to_string(), task_id.clone());
|
||||
}
|
||||
if let Some(ref user_id) = self.user_id {
|
||||
fields.insert("user_id".to_string(), user_id.clone());
|
||||
}
|
||||
if let Some(ref session_id) = self.session_id {
|
||||
fields.insert("session_id".to_string(), session_id.clone());
|
||||
}
|
||||
if let Some(ref trace_id) = self.trace_id {
|
||||
fields.insert("trace_id".to_string(), trace_id.clone());
|
||||
}
|
||||
if let Some(ref span_id) = self.span_id {
|
||||
fields.insert("span_id".to_string(), span_id.clone());
|
||||
}
|
||||
|
||||
fields
|
||||
}
|
||||
}
|
||||
|
||||
/// 结构化日志条目
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct LogEntry {
|
||||
pub id: String,
|
||||
pub timestamp: SystemTime,
|
||||
pub level: LogLevel,
|
||||
pub message: String,
|
||||
pub module: Option<String>,
|
||||
pub file: Option<String>,
|
||||
pub line: Option<u32>,
|
||||
pub target: String,
|
||||
pub fields: HashMap<String, serde_json::Value>,
|
||||
pub correlation: CorrelationContext,
|
||||
pub service_name: String,
|
||||
pub service_version: String,
|
||||
pub environment: String,
|
||||
}
|
||||
|
||||
impl LogEntry {
|
||||
pub fn new(level: LogLevel, message: String, target: String) -> Self {
|
||||
Self {
|
||||
id: Uuid::new_v4().to_string(),
|
||||
timestamp: SystemTime::now(),
|
||||
level,
|
||||
message,
|
||||
module: None,
|
||||
file: None,
|
||||
line: None,
|
||||
target,
|
||||
fields: HashMap::new(),
|
||||
correlation: CorrelationContext::default(),
|
||||
service_name: "document-parser".to_string(),
|
||||
service_version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
environment: std::env::var("ENVIRONMENT").unwrap_or_else(|_| "development".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// 添加字段
|
||||
pub fn with_field<T: Serialize>(mut self, key: &str, value: T) -> Self {
|
||||
if let Ok(json_value) = serde_json::to_value(value) {
|
||||
self.fields.insert(key.to_string(), json_value);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// 设置关联上下文
|
||||
pub fn with_correlation(mut self, correlation: CorrelationContext) -> Self {
|
||||
self.correlation = correlation;
|
||||
self
|
||||
}
|
||||
|
||||
/// 设置服务信息
|
||||
pub fn with_service_info(mut self, name: String, version: String, environment: String) -> Self {
|
||||
self.service_name = name;
|
||||
self.service_version = version;
|
||||
self.environment = environment;
|
||||
self
|
||||
}
|
||||
|
||||
/// 设置源码位置
|
||||
pub fn with_location(mut self, module: String, file: String, line: u32) -> Self {
|
||||
self.module = Some(module);
|
||||
self.file = Some(file);
|
||||
self.line = Some(line);
|
||||
self
|
||||
}
|
||||
|
||||
/// 脱敏处理
|
||||
pub fn sanitize(&mut self) {
|
||||
// 脱敏消息中的敏感信息
|
||||
self.message = self.sanitize_string(&self.message);
|
||||
|
||||
// 脱敏字段中的敏感信息
|
||||
let mut sanitized_fields = HashMap::new();
|
||||
for (key, value) in &self.fields {
|
||||
let sanitized_value = match value {
|
||||
serde_json::Value::String(s) => serde_json::Value::String(self.sanitize_string(s)),
|
||||
_ => value.clone(),
|
||||
};
|
||||
sanitized_fields.insert(key.clone(), sanitized_value);
|
||||
}
|
||||
self.fields = sanitized_fields;
|
||||
}
|
||||
|
||||
/// 脱敏字符串
|
||||
fn sanitize_string(&self, input: &str) -> String {
|
||||
let mut result = input.to_string();
|
||||
|
||||
// 脱敏常见的敏感信息模式
|
||||
let patterns = vec![
|
||||
(r"password[\s]*[:=][\s]*[\S]+", "password: ***"),
|
||||
(r"token[\s]*[:=][\s]*[\S]+", "token: ***"),
|
||||
(r"key[\s]*[:=][\s]*[\S]+", "key: ***"),
|
||||
(r"secret[\s]*[:=][\s]*[\S]+", "secret: ***"),
|
||||
(
|
||||
r"\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b",
|
||||
"****-****-****-****",
|
||||
), // 信用卡号
|
||||
(r"\b\d{3}-\d{2}-\d{4}\b", "***-**-****"), // SSN
|
||||
];
|
||||
|
||||
for (pattern, replacement) in patterns {
|
||||
if let Ok(re) = regex::Regex::new(pattern) {
|
||||
result = re.replace_all(&result, replacement).to_string();
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// 格式化为JSON
|
||||
pub fn to_json(&self) -> Result<String, serde_json::Error> {
|
||||
serde_json::to_string(self)
|
||||
}
|
||||
|
||||
/// 格式化为人类可读格式
|
||||
pub fn to_human_readable(&self) -> String {
|
||||
let timestamp = self
|
||||
.timestamp
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs();
|
||||
|
||||
let location = if let (Some(file), Some(line)) = (&self.file, self.line) {
|
||||
format!(" [{file}:{line}]")
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
let context = if !self.fields.is_empty() {
|
||||
format!(
|
||||
" {}",
|
||||
serde_json::to_string(&self.fields).unwrap_or_default()
|
||||
)
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
format!(
|
||||
"{} [{}] {}{}: {}{}",
|
||||
timestamp, self.level, self.target, location, self.message, context
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// 日志输出器trait
|
||||
#[async_trait::async_trait]
|
||||
pub trait LogOutput: Send + Sync {
|
||||
async fn write_log(
|
||||
&self,
|
||||
entry: &LogEntry,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>>;
|
||||
async fn flush(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>>;
|
||||
}
|
||||
|
||||
/// 控制台日志输出器
|
||||
pub struct ConsoleOutput {
|
||||
use_json: bool,
|
||||
}
|
||||
|
||||
impl ConsoleOutput {
|
||||
pub fn new(use_json: bool) -> Self {
|
||||
Self { use_json }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl LogOutput for ConsoleOutput {
|
||||
async fn write_log(
|
||||
&self,
|
||||
entry: &LogEntry,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let output = if self.use_json {
|
||||
entry.to_json()?
|
||||
} else {
|
||||
entry.to_human_readable()
|
||||
};
|
||||
|
||||
println!("{}", output);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn flush(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
use std::io::{self, Write};
|
||||
io::stdout().flush()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// 文件日志输出器
|
||||
pub struct FileOutput {
|
||||
file_path: String,
|
||||
use_json: bool,
|
||||
max_file_size: u64,
|
||||
max_files: usize,
|
||||
}
|
||||
|
||||
impl FileOutput {
|
||||
pub fn new(file_path: String, use_json: bool, max_file_size: u64, max_files: usize) -> Self {
|
||||
Self {
|
||||
file_path,
|
||||
use_json,
|
||||
max_file_size,
|
||||
max_files,
|
||||
}
|
||||
}
|
||||
|
||||
/// 检查并轮转日志文件
|
||||
async fn rotate_if_needed(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
use std::fs;
|
||||
|
||||
if let Ok(metadata) = fs::metadata(&self.file_path) {
|
||||
if metadata.len() > self.max_file_size {
|
||||
// 轮转日志文件
|
||||
for i in (1..self.max_files).rev() {
|
||||
let old_file = format!("{}.{}", self.file_path, i);
|
||||
let new_file = format!("{}.{}", self.file_path, i + 1);
|
||||
|
||||
if fs::metadata(&old_file).is_ok() {
|
||||
fs::rename(&old_file, &new_file)?;
|
||||
}
|
||||
}
|
||||
|
||||
// 移动当前文件
|
||||
let backup_file = format!("{}.1", self.file_path);
|
||||
fs::rename(&self.file_path, &backup_file)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl LogOutput for FileOutput {
|
||||
async fn write_log(
|
||||
&self,
|
||||
entry: &LogEntry,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::Write;
|
||||
|
||||
self.rotate_if_needed().await?;
|
||||
|
||||
let output = if self.use_json {
|
||||
format!("{}\n", entry.to_json()?)
|
||||
} else {
|
||||
format!("{}\n", entry.to_human_readable())
|
||||
};
|
||||
|
||||
let mut file = OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(&self.file_path)?;
|
||||
|
||||
file.write_all(output.as_bytes())?;
|
||||
file.flush()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn flush(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
// 文件输出器在每次写入时都会刷新
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// 日志配置
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LoggingConfig {
|
||||
pub level: String,
|
||||
pub format: LogFormat,
|
||||
pub output: LogOutputTarget,
|
||||
pub file_path: Option<String>,
|
||||
pub max_file_size: u64,
|
||||
pub max_files: usize,
|
||||
pub enable_console: bool,
|
||||
pub enable_json: bool,
|
||||
pub enable_correlation: bool,
|
||||
pub service_name: String,
|
||||
pub service_version: String,
|
||||
pub environment: String,
|
||||
}
|
||||
|
||||
impl Default for LoggingConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
level: "info".to_string(),
|
||||
format: LogFormat::Human,
|
||||
output: LogOutputTarget::Console,
|
||||
file_path: None,
|
||||
max_file_size: 100 * 1024 * 1024, // 100MB
|
||||
max_files: 10,
|
||||
enable_console: true,
|
||||
enable_json: false,
|
||||
enable_correlation: true,
|
||||
service_name: "document-parser".to_string(),
|
||||
service_version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
environment: std::env::var("ENVIRONMENT").unwrap_or_else(|_| "development".to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 日志格式
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum LogFormat {
|
||||
Human,
|
||||
Json,
|
||||
Compact,
|
||||
}
|
||||
|
||||
/// 日志输出目标
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum LogOutputTarget {
|
||||
Console,
|
||||
File,
|
||||
Both,
|
||||
}
|
||||
|
||||
/// 增强的日志系统
|
||||
pub struct EnhancedLoggingSystem {
|
||||
config: LoggingConfig,
|
||||
correlation_context: Arc<RwLock<CorrelationContext>>,
|
||||
_guards: Vec<tracing_appender::non_blocking::WorkerGuard>,
|
||||
}
|
||||
|
||||
impl EnhancedLoggingSystem {
|
||||
/// 初始化日志系统
|
||||
#[instrument(skip(config))]
|
||||
pub fn init(config: LoggingConfig) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let guards = Vec::new();
|
||||
|
||||
// 设置环境过滤器
|
||||
let env_filter = EnvFilter::try_from_default_env()
|
||||
.or_else(|_| EnvFilter::try_new(&config.level))
|
||||
.unwrap_or_else(|_| EnvFilter::new("info"));
|
||||
|
||||
// 控制台输出层
|
||||
if config.enable_console
|
||||
&& (config.output == LogOutputTarget::Console || config.output == LogOutputTarget::Both)
|
||||
{
|
||||
// 简化的控制台层配置
|
||||
// 在实际实现中,这里需要更复杂的配置
|
||||
}
|
||||
|
||||
// 文件输出层
|
||||
if let Some(ref _file_path) = config.file_path {
|
||||
if config.output == LogOutputTarget::File || config.output == LogOutputTarget::Both {
|
||||
// 简化的文件层配置
|
||||
// 在实际实现中,这里需要更复杂的配置
|
||||
}
|
||||
}
|
||||
|
||||
// 简化的订阅者初始化
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(env_filter)
|
||||
.with_target(true)
|
||||
.with_thread_ids(true)
|
||||
.with_file(true)
|
||||
.with_line_number(true)
|
||||
.init();
|
||||
|
||||
info!(
|
||||
service_name = %config.service_name,
|
||||
service_version = %config.service_version,
|
||||
environment = %config.environment,
|
||||
log_level = %config.level,
|
||||
"Logging system initialized"
|
||||
);
|
||||
|
||||
Ok(Self {
|
||||
config,
|
||||
correlation_context: Arc::new(RwLock::new(CorrelationContext::default())),
|
||||
_guards: guards,
|
||||
})
|
||||
}
|
||||
|
||||
/// 设置关联上下文
|
||||
pub async fn set_correlation_context(&self, context: CorrelationContext) {
|
||||
let mut correlation = self.correlation_context.write().await;
|
||||
*correlation = context;
|
||||
}
|
||||
|
||||
/// 获取关联上下文
|
||||
pub async fn get_correlation_context(&self) -> CorrelationContext {
|
||||
let correlation = self.correlation_context.read().await;
|
||||
correlation.clone()
|
||||
}
|
||||
|
||||
/// 生成新的请求ID
|
||||
pub async fn generate_request_id(&self) -> String {
|
||||
let mut correlation = self.correlation_context.write().await;
|
||||
correlation.generate_request_id()
|
||||
}
|
||||
|
||||
/// 生成新的跟踪ID
|
||||
pub async fn generate_trace_id(&self) -> String {
|
||||
let mut correlation = self.correlation_context.write().await;
|
||||
correlation.generate_trace_id()
|
||||
}
|
||||
|
||||
/// 创建带有关联上下文的span
|
||||
pub async fn create_span(&self, name: &str) -> tracing::Span {
|
||||
let correlation = self.correlation_context.read().await;
|
||||
let fields = correlation.to_fields();
|
||||
|
||||
let span = tracing::info_span!(
|
||||
"custom_span",
|
||||
name = name,
|
||||
service_name = %self.config.service_name,
|
||||
service_version = %self.config.service_version,
|
||||
environment = %self.config.environment,
|
||||
);
|
||||
|
||||
// 添加关联字段
|
||||
for (key, value) in fields {
|
||||
span.record(key.as_str(), tracing::field::display(&value));
|
||||
}
|
||||
|
||||
span
|
||||
}
|
||||
|
||||
/// 获取配置
|
||||
pub fn config(&self) -> &LoggingConfig {
|
||||
&self.config
|
||||
}
|
||||
}
|
||||
|
||||
/// 结构化日志记录器(保持向后兼容)
|
||||
pub struct StructuredLogger {
|
||||
min_level: LogLevel,
|
||||
outputs: Vec<Arc<dyn LogOutput>>,
|
||||
context: Arc<RwLock<HashMap<String, serde_json::Value>>>,
|
||||
}
|
||||
|
||||
impl StructuredLogger {
|
||||
/// 创建新的结构化日志器
|
||||
pub fn new(min_level: LogLevel) -> Self {
|
||||
Self {
|
||||
min_level,
|
||||
outputs: Vec::new(),
|
||||
context: Arc::new(RwLock::new(HashMap::new())),
|
||||
}
|
||||
}
|
||||
|
||||
/// 添加输出器
|
||||
pub fn add_output(mut self, output: Arc<dyn LogOutput>) -> Self {
|
||||
self.outputs.push(output);
|
||||
self
|
||||
}
|
||||
|
||||
/// 设置全局上下文
|
||||
pub async fn set_context<T: Serialize>(&self, key: &str, value: T) {
|
||||
if let Ok(json_value) = serde_json::to_value(value) {
|
||||
let mut context = self.context.write().await;
|
||||
context.insert(key.to_string(), json_value);
|
||||
}
|
||||
}
|
||||
|
||||
/// 移除全局上下文
|
||||
pub async fn remove_context(&self, key: &str) {
|
||||
let mut context = self.context.write().await;
|
||||
context.remove(key);
|
||||
}
|
||||
|
||||
/// 记录日志
|
||||
pub async fn log(&self, mut entry: LogEntry) {
|
||||
if entry.level < self.min_level {
|
||||
return;
|
||||
}
|
||||
|
||||
// 添加全局上下文
|
||||
{
|
||||
let context = self.context.read().await;
|
||||
for (key, value) in context.iter() {
|
||||
entry.fields.insert(key.clone(), value.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// 脱敏处理
|
||||
entry.sanitize();
|
||||
|
||||
// 输出到所有输出器
|
||||
for output in &self.outputs {
|
||||
if let Err(e) = output.write_log(&entry).await {
|
||||
eprintln!("Log output error: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 刷新所有输出器
|
||||
pub async fn flush(&self) {
|
||||
for output in &self.outputs {
|
||||
if let Err(e) = output.flush().await {
|
||||
eprintln!("Log refresh error: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 便捷方法:记录trace级别日志
|
||||
pub async fn trace(&self, message: &str, target: &str) {
|
||||
let entry = LogEntry::new(LogLevel::Trace, message.to_string(), target.to_string());
|
||||
self.log(entry).await;
|
||||
}
|
||||
|
||||
/// 便捷方法:记录debug级别日志
|
||||
pub async fn debug(&self, message: &str, target: &str) {
|
||||
let entry = LogEntry::new(LogLevel::Debug, message.to_string(), target.to_string());
|
||||
self.log(entry).await;
|
||||
}
|
||||
|
||||
/// 便捷方法:记录info级别日志
|
||||
pub async fn info(&self, message: &str, target: &str) {
|
||||
let entry = LogEntry::new(LogLevel::Info, message.to_string(), target.to_string());
|
||||
self.log(entry).await;
|
||||
}
|
||||
|
||||
/// 便捷方法:记录warn级别日志
|
||||
pub async fn warn(&self, message: &str, target: &str) {
|
||||
let entry = LogEntry::new(LogLevel::Warn, message.to_string(), target.to_string());
|
||||
self.log(entry).await;
|
||||
}
|
||||
|
||||
/// 便捷方法:记录error级别日志
|
||||
pub async fn error(&self, message: &str, target: &str) {
|
||||
let entry = LogEntry::new(LogLevel::Error, message.to_string(), target.to_string());
|
||||
self.log(entry).await;
|
||||
}
|
||||
}
|
||||
|
||||
/// 日志宏
|
||||
#[macro_export]
|
||||
macro_rules! structured_log {
|
||||
($logger:expr, $level:expr, $message:expr) => {
|
||||
{
|
||||
let entry = $crate::utils::logging::LogEntry::new(
|
||||
$level,
|
||||
$message.to_string(),
|
||||
module_path!().to_string(),
|
||||
).with_location(
|
||||
module_path!().to_string(),
|
||||
file!().to_string(),
|
||||
line!(),
|
||||
);
|
||||
$logger.log(entry).await;
|
||||
}
|
||||
};
|
||||
|
||||
($logger:expr, $level:expr, $message:expr, $($key:expr => $value:expr),+) => {
|
||||
{
|
||||
let mut entry = $crate::utils::logging::LogEntry::new(
|
||||
$level,
|
||||
$message.to_string(),
|
||||
module_path!().to_string(),
|
||||
).with_location(
|
||||
module_path!().to_string(),
|
||||
file!().to_string(),
|
||||
line!(),
|
||||
);
|
||||
|
||||
$(
|
||||
entry = entry.with_field($key, $value);
|
||||
)+
|
||||
|
||||
$logger.log(entry).await;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// 便捷宏
|
||||
#[macro_export]
|
||||
macro_rules! log_trace {
|
||||
($logger:expr, $message:expr) => {
|
||||
structured_log!($logger, $crate::utils::logging::LogLevel::Trace, $message)
|
||||
};
|
||||
($logger:expr, $message:expr, $($key:expr => $value:expr),+) => {
|
||||
structured_log!($logger, $crate::utils::logging::LogLevel::Trace, $message, $($key => $value),+)
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! log_debug {
|
||||
($logger:expr, $message:expr) => {
|
||||
structured_log!($logger, $crate::utils::logging::LogLevel::Debug, $message)
|
||||
};
|
||||
($logger:expr, $message:expr, $($key:expr => $value:expr),+) => {
|
||||
structured_log!($logger, $crate::utils::logging::LogLevel::Debug, $message, $($key => $value),+)
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! log_info {
|
||||
($logger:expr, $message:expr) => {
|
||||
structured_log!($logger, $crate::utils::logging::LogLevel::Info, $message)
|
||||
};
|
||||
($logger:expr, $message:expr, $($key:expr => $value:expr),+) => {
|
||||
structured_log!($logger, $crate::utils::logging::LogLevel::Info, $message, $($key => $value),+)
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! log_warn {
|
||||
($logger:expr, $message:expr) => {
|
||||
structured_log!($logger, $crate::utils::logging::LogLevel::Warn, $message)
|
||||
};
|
||||
($logger:expr, $message:expr, $($key:expr => $value:expr),+) => {
|
||||
structured_log!($logger, $crate::utils::logging::LogLevel::Warn, $message, $($key => $value),+)
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! log_error {
|
||||
($logger:expr, $message:expr) => {
|
||||
structured_log!($logger, $crate::utils::logging::LogLevel::Error, $message)
|
||||
};
|
||||
($logger:expr, $message:expr, $($key:expr => $value:expr),+) => {
|
||||
structured_log!($logger, $crate::utils::logging::LogLevel::Error, $message, $($key => $value),+)
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::sync::Mutex;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
struct TestOutput {
|
||||
entries: Arc<Mutex<Vec<LogEntry>>>,
|
||||
write_count: Arc<AtomicUsize>,
|
||||
}
|
||||
|
||||
impl TestOutput {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
entries: Arc::new(Mutex::new(Vec::new())),
|
||||
write_count: Arc::new(AtomicUsize::new(0)),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_entries(&self) -> Vec<LogEntry> {
|
||||
self.entries.lock().unwrap().clone()
|
||||
}
|
||||
|
||||
fn get_write_count(&self) -> usize {
|
||||
self.write_count.load(Ordering::SeqCst)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl LogOutput for TestOutput {
|
||||
async fn write_log(
|
||||
&self,
|
||||
entry: &LogEntry,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
self.entries.lock().unwrap().push(entry.clone());
|
||||
self.write_count.fetch_add(1, Ordering::SeqCst);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn flush(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_structured_logger() {
|
||||
let test_output = Arc::new(TestOutput::new());
|
||||
let logger = StructuredLogger::new(LogLevel::Debug).add_output(test_output.clone());
|
||||
|
||||
// 测试基本日志记录
|
||||
logger.info("测试消息", "test_module").await;
|
||||
|
||||
let entries = test_output.get_entries();
|
||||
assert_eq!(entries.len(), 1);
|
||||
assert_eq!(entries[0].level, LogLevel::Info);
|
||||
assert_eq!(entries[0].message, "测试消息");
|
||||
assert_eq!(entries[0].target, "test_module");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_log_level_filtering() {
|
||||
let test_output = Arc::new(TestOutput::new());
|
||||
let logger = StructuredLogger::new(LogLevel::Warn).add_output(test_output.clone());
|
||||
|
||||
// 这些日志应该被过滤掉
|
||||
logger.debug("debug消息", "test").await;
|
||||
logger.info("info消息", "test").await;
|
||||
|
||||
// 这些日志应该被记录
|
||||
logger.warn("warn消息", "test").await;
|
||||
logger.error("error消息", "test").await;
|
||||
|
||||
let entries = test_output.get_entries();
|
||||
assert_eq!(entries.len(), 2);
|
||||
assert_eq!(entries[0].level, LogLevel::Warn);
|
||||
assert_eq!(entries[1].level, LogLevel::Error);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_context() {
|
||||
let test_output = Arc::new(TestOutput::new());
|
||||
let logger = StructuredLogger::new(LogLevel::Debug).add_output(test_output.clone());
|
||||
|
||||
// 设置全局上下文
|
||||
logger.set_context("service", "document-parser").await;
|
||||
logger.set_context("version", "1.0.0").await;
|
||||
|
||||
logger.info("测试消息", "test").await;
|
||||
|
||||
let entries = test_output.get_entries();
|
||||
assert_eq!(entries.len(), 1);
|
||||
|
||||
let entry = &entries[0];
|
||||
assert!(entry.fields.contains_key("service"));
|
||||
assert!(entry.fields.contains_key("version"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_log_entry_sanitization() {
|
||||
let mut entry = LogEntry::new(
|
||||
LogLevel::Info,
|
||||
"用户登录: password=secret123 token=abc123".to_string(),
|
||||
"auth".to_string(),
|
||||
);
|
||||
|
||||
entry.sanitize();
|
||||
|
||||
assert!(entry.message.contains("password: ***"));
|
||||
assert!(entry.message.contains("token: ***"));
|
||||
assert!(!entry.message.contains("secret123"));
|
||||
assert!(!entry.message.contains("abc123"));
|
||||
}
|
||||
}
|
||||
1274
qiming-mcp-proxy/document-parser/src/utils/metrics.rs
Normal file
1274
qiming-mcp-proxy/document-parser/src/utils/metrics.rs
Normal file
File diff suppressed because it is too large
Load Diff
17
qiming-mcp-proxy/document-parser/src/utils/mod.rs
Normal file
17
qiming-mcp-proxy/document-parser/src/utils/mod.rs
Normal file
@@ -0,0 +1,17 @@
|
||||
// 工具模块
|
||||
// TODO: 实现具体的工具函数
|
||||
pub mod alerting;
|
||||
pub mod environment_manager;
|
||||
pub mod file_utils;
|
||||
pub mod format_utils;
|
||||
pub mod health_check;
|
||||
pub mod logging;
|
||||
pub mod metrics;
|
||||
|
||||
pub use environment_manager::{EnvironmentManager, EnvironmentStatus, InstallStage};
|
||||
|
||||
pub use alerting::*;
|
||||
pub use file_utils::*;
|
||||
pub use format_utils::*;
|
||||
pub use health_check::*;
|
||||
pub use metrics::*;
|
||||
Reference in New Issue
Block a user