1use agentlib_core::{Middleware, MiddlewareContext, MiddlewareScope, NextMiddleware, async_trait};
2use anyhow::Result;
3use chrono::Utc;
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
8#[serde(rename_all = "lowercase")]
9pub enum LogLevel {
10 Debug,
11 Info,
12 Warn,
13 Error,
14 Silent,
15}
16
17impl LogLevel {
18 fn rank(&self) -> i32 {
19 match self {
20 LogLevel::Debug => 0,
21 LogLevel::Info => 1,
22 LogLevel::Warn => 2,
23 LogLevel::Error => 3,
24 LogLevel::Silent => 99,
25 }
26 }
27}
28
29#[derive(Debug, Clone, Serialize)]
30pub struct LogEntry {
31 pub level: LogLevel,
32 pub scope: MiddlewareScope,
33 pub agent_input: Option<String>,
34 pub tool: Option<String>,
35 pub timestamp: String,
36 pub duration_ms: Option<u128>,
37 pub meta: Option<serde_json::Value>,
38}
39
40pub type LogTransport = Box<dyn Fn(LogEntry) + Send + Sync>;
41
42pub struct LoggerConfig {
43 pub level: LogLevel,
44 pub scopes: Option<Vec<MiddlewareScope>>,
45 pub transport: Option<LogTransport>,
46 pub timing: bool,
47 pub prefix: String,
48}
49
50impl Default for LoggerConfig {
51 fn default() -> Self {
52 Self {
53 level: LogLevel::Info,
54 scopes: None,
55 transport: None,
56 timing: true,
57 prefix: "[agentlib]".to_string(),
58 }
59 }
60}
61
62pub struct Logger {
63 config: LoggerConfig,
64 timers: std::sync::Arc<std::sync::Mutex<HashMap<String, std::time::Instant>>>,
65}
66
67impl Logger {
68 pub fn new(config: LoggerConfig) -> Self {
69 Self {
70 config,
71 timers: std::sync::Arc::new(std::sync::Mutex::new(HashMap::new())),
72 }
73 }
74
75 fn should_log(&self, level: LogLevel) -> bool {
76 level.rank() >= self.config.level.rank()
77 }
78
79 fn scope_to_level(scope: MiddlewareScope) -> LogLevel {
80 match scope {
81 MiddlewareScope::RunBefore | MiddlewareScope::RunAfter => LogLevel::Info,
82 MiddlewareScope::StepBefore | MiddlewareScope::StepAfter => LogLevel::Debug,
83 MiddlewareScope::ToolBefore | MiddlewareScope::ToolAfter => LogLevel::Debug,
84 }
85 }
86
87 fn default_transport(prefix: &str, entry: LogEntry) {
88 let mut parts = vec![
89 prefix.to_string(),
90 format!("[{}]", entry.timestamp),
91 format!("[{:?}]", entry.level).to_uppercase(),
92 format!("scope={:?}", entry.scope),
93 ];
94
95 if let Some(tool) = entry.tool {
96 parts.push(format!("tool={}", tool));
97 }
98
99 if let Some(duration) = entry.duration_ms {
100 parts.push(format!("duration={}ms", duration));
101 }
102
103 if let Some(meta) = entry.meta {
104 parts.push(meta.to_string());
105 }
106
107 let line = parts.join(" ");
108
109 match entry.level {
110 LogLevel::Debug => println!("{}", line),
111 LogLevel::Info => println!("{}", line),
112 LogLevel::Warn => eprintln!("{}", line),
113 LogLevel::Error => eprintln!("{}", line),
114 LogLevel::Silent => {}
115 }
116 }
117}
118
119#[async_trait]
120impl Middleware for Logger {
121 fn name(&self) -> &str {
122 "logger"
123 }
124
125 async fn run(
126 &self,
127 m_ctx: &mut MiddlewareContext<'_>,
128 next: &mut dyn NextMiddleware,
129 ) -> Result<()> {
130 let scope = m_ctx.scope;
131
132 if let Some(scopes) = &self.config.scopes {
134 if !scopes.contains(&scope) {
135 return next.run(m_ctx).await;
136 }
137 }
138
139 let level = Self::scope_to_level(scope);
140 if !self.should_log(level) {
141 return next.run(m_ctx).await;
142 }
143
144 let timestamp = Utc::now().to_rfc3339();
145 let tool_name = m_ctx.tool.as_ref().map(|t| t.name.clone());
146 let timer_key = format!(
147 "{:?}-{}-{}",
148 scope,
149 m_ctx.ctx.input.chars().take(20).collect::<String>(),
150 tool_name.as_deref().unwrap_or("")
151 );
152
153 let is_before = match scope {
154 MiddlewareScope::RunBefore
155 | MiddlewareScope::StepBefore
156 | MiddlewareScope::ToolBefore => true,
157 _ => false,
158 };
159
160 if is_before {
161 if self.config.timing {
162 let mut timers = self.timers.lock().unwrap();
163 timers.insert(timer_key.clone(), std::time::Instant::now());
164 }
165
166 let entry = LogEntry {
167 level,
168 scope,
169 timestamp,
170 agent_input: Some(m_ctx.ctx.input.clone()),
171 tool: tool_name.clone(),
172 duration_ms: None,
173 meta: m_ctx
174 .tool
175 .as_ref()
176 .map(|t| serde_json::json!({ "args": t.args })),
177 };
178
179 if let Some(transport) = &self.config.transport {
180 transport(entry);
181 } else {
182 Self::default_transport(&self.config.prefix, entry);
183 }
184
185 return next.run(m_ctx).await;
186 }
187
188 let start = if self.config.timing {
190 let before_scope = match scope {
191 MiddlewareScope::RunAfter => MiddlewareScope::RunBefore,
192 MiddlewareScope::StepAfter => MiddlewareScope::StepBefore,
193 MiddlewareScope::ToolAfter => MiddlewareScope::ToolBefore,
194 _ => unreachable!(),
195 };
196 let before_key = format!(
197 "{:?}-{}-{}",
198 before_scope,
199 m_ctx.ctx.input.chars().take(20).collect::<String>(),
200 tool_name.as_deref().unwrap_or("")
201 );
202 let mut timers = self.timers.lock().unwrap();
203 timers.remove(&before_key)
204 } else {
205 None
206 };
207
208 let res = next.run(m_ctx).await;
209
210 let duration_ms = start.map(|s| s.elapsed().as_millis());
211 let entry = LogEntry {
212 level,
213 scope,
214 timestamp: Utc::now().to_rfc3339(),
215 agent_input: Some(m_ctx.ctx.input.clone()),
216 tool: tool_name,
217 duration_ms,
218 meta: m_ctx.tool.as_ref().and_then(|t| {
219 t.result
220 .as_ref()
221 .map(|r| serde_json::json!({ "result": r }))
222 }),
223 };
224
225 if let Some(transport) = &self.config.transport {
226 transport(entry);
227 } else {
228 Self::default_transport(&self.config.prefix, entry);
229 }
230
231 res
232 }
233}
234
235pub fn create_logger(config: LoggerConfig) -> Box<dyn Middleware> {
236 Box::new(Logger::new(config))
237}