construct/hooks/builtin/
command_logger.rs1use 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
8pub 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}