Skip to main content

construct/hooks/builtin/
command_logger.rs

1use async_trait::async_trait;
2use std::sync::{Arc, Mutex};
3use std::time::Duration;
4
5use crate::hooks::traits::HookHandler;
6use crate::tools::traits::ToolResult;
7
8/// Logs tool calls for auditing.
9pub struct CommandLoggerHook {
10    log: Arc<Mutex<Vec<String>>>,
11}
12
13impl CommandLoggerHook {
14    pub fn new() -> Self {
15        Self {
16            log: Arc::new(Mutex::new(Vec::new())),
17        }
18    }
19
20    #[cfg(test)]
21    pub fn entries(&self) -> Vec<String> {
22        self.log.lock().unwrap().clone()
23    }
24}
25
26#[async_trait]
27impl HookHandler for CommandLoggerHook {
28    fn name(&self) -> &str {
29        "command-logger"
30    }
31
32    fn priority(&self) -> i32 {
33        -50
34    }
35
36    async fn on_after_tool_call(&self, tool: &str, result: &ToolResult, duration: Duration) {
37        let entry = format!(
38            "[{}] {} ({}ms) success={}",
39            chrono::Utc::now().format("%H:%M:%S"),
40            tool,
41            duration.as_millis(),
42            result.success,
43        );
44        tracing::info!(hook = "command-logger", "{}", entry);
45        self.log.lock().unwrap().push(entry);
46    }
47}
48
49#[cfg(test)]
50mod tests {
51    use super::*;
52
53    #[tokio::test]
54    async fn logs_tool_calls() {
55        let hook = CommandLoggerHook::new();
56        let result = ToolResult {
57            success: true,
58            output: "ok".into(),
59            error: None,
60        };
61        hook.on_after_tool_call("shell", &result, Duration::from_millis(42))
62            .await;
63        let entries = hook.entries();
64        assert_eq!(entries.len(), 1);
65        assert!(entries[0].contains("shell"));
66        assert!(entries[0].contains("42ms"));
67        assert!(entries[0].contains("success=true"));
68    }
69}