use async_trait::async_trait;
use chrono::{DateTime, Utc};
use parking_lot::Mutex;
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, VecDeque};
use std::sync::Arc;
use crate::context::Context;
use crate::errors::ModuleError;
use crate::middleware::base::Middleware;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ErrorEntry {
pub module_id: String,
pub error_code: String,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub ai_guidance: Option<String>,
pub timestamp: DateTime<Utc>,
pub count: u64,
pub first_occurred: DateTime<Utc>,
pub last_occurred: DateTime<Utc>,
}
#[derive(Debug, Clone)]
pub struct ErrorHistory {
entries: Arc<Mutex<HashMap<String, VecDeque<ErrorEntry>>>>,
max_entries_per_module: usize,
max_total_entries: usize,
}
impl ErrorHistory {
#[must_use]
pub fn new(max_entries_per_module: usize) -> Self {
Self {
entries: Arc::new(Mutex::new(HashMap::new())),
max_entries_per_module,
max_total_entries: max_entries_per_module * 100,
}
}
#[must_use]
pub fn with_limits(max_entries_per_module: usize, max_total_entries: usize) -> Self {
Self {
entries: Arc::new(Mutex::new(HashMap::new())),
max_entries_per_module,
max_total_entries,
}
}
pub fn record(&self, module_id: &str, error: &ModuleError) {
let mut map = self.entries.lock();
let error_code = format!("{:?}", error.code);
let now = Utc::now();
let module_entries = map.entry(module_id.to_string()).or_default();
let existing = module_entries
.iter_mut()
.find(|e| e.error_code == error_code && e.message == error.message);
if let Some(entry) = existing {
entry.count += 1;
entry.last_occurred = now;
entry.timestamp = now;
} else {
let entry = ErrorEntry {
module_id: module_id.to_string(),
error_code,
message: error.message.clone(),
ai_guidance: error.ai_guidance.clone(),
timestamp: now,
count: 1,
first_occurred: now,
last_occurred: now,
};
module_entries.push_back(entry);
while module_entries.len() > self.max_entries_per_module {
module_entries.pop_front();
}
}
let mut total: usize = map.values().map(std::collections::VecDeque::len).sum();
while total > self.max_total_entries {
let mut oldest_module = None;
let mut oldest_time = None;
for (mid, entries) in map.iter() {
if let Some(front) = entries.front() {
if oldest_time.is_none() || front.first_occurred < oldest_time.unwrap() {
oldest_time = Some(front.first_occurred);
oldest_module = Some(mid.clone());
}
}
}
if let Some(mid) = oldest_module {
if let Some(entries) = map.get_mut(&mid) {
entries.pop_front();
if entries.is_empty() {
map.remove(&mid);
}
}
total -= 1;
} else {
break;
}
}
}
#[must_use]
pub fn get(&self, module_id: &str, limit: Option<usize>) -> Vec<ErrorEntry> {
let map = self.entries.lock();
match map.get(module_id) {
Some(entries) => {
let mut result: Vec<ErrorEntry> = entries.iter().cloned().collect();
result.sort_by_key(|b| std::cmp::Reverse(b.last_occurred));
if let Some(lim) = limit {
result.truncate(lim);
}
result
}
None => Vec::new(),
}
}
#[must_use]
pub fn get_all(&self, limit: Option<usize>) -> Vec<ErrorEntry> {
let map = self.entries.lock();
let mut all: Vec<ErrorEntry> = map
.values()
.flat_map(|entries| entries.iter().cloned())
.collect();
all.sort_by_key(|b| std::cmp::Reverse(b.last_occurred));
if let Some(lim) = limit {
all.truncate(lim);
}
all
}
pub fn clear(&self, module_id: Option<&str>) {
let mut map = self.entries.lock();
match module_id {
Some(id) => {
map.remove(id);
}
None => map.clear(),
}
}
}
#[derive(Debug)]
pub struct ErrorHistoryMiddleware {
history: ErrorHistory,
}
impl ErrorHistoryMiddleware {
#[must_use]
pub fn new(history: ErrorHistory) -> Self {
Self { history }
}
#[must_use]
pub fn history(&self) -> &ErrorHistory {
&self.history
}
}
#[async_trait]
impl Middleware for ErrorHistoryMiddleware {
fn name(&self) -> &'static str {
"error_history"
}
async fn before(
&self,
_module_id: &str,
_inputs: serde_json::Value,
_ctx: &Context<serde_json::Value>,
) -> Result<Option<serde_json::Value>, ModuleError> {
Ok(None)
}
async fn after(
&self,
_module_id: &str,
_inputs: serde_json::Value,
_output: serde_json::Value,
_ctx: &Context<serde_json::Value>,
) -> Result<Option<serde_json::Value>, ModuleError> {
Ok(None)
}
async fn on_error(
&self,
module_id: &str,
_inputs: serde_json::Value,
error: &ModuleError,
_ctx: &Context<serde_json::Value>,
) -> Result<Option<serde_json::Value>, ModuleError> {
self.history.record(module_id, error);
Ok(None)
}
}