Skip to main content

agentlib_logger/
lib.rs

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        // Scope filter
133        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        // AFTER scopes
189        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}