Skip to main content

boost/tools/
logs.rs

1//! `read-log-entries` and `last-error` — tail the in-memory log capture buffer.
2
3use async_trait::async_trait;
4use serde_json::{json, Value};
5
6use crate::protocol::CallToolResult;
7use crate::tool::{Context, Tool};
8
9pub struct ReadLogEntries;
10
11#[async_trait]
12impl Tool for ReadLogEntries {
13    fn name(&self) -> &'static str {
14        "read-log-entries"
15    }
16    fn description(&self) -> &'static str {
17        "Tail the most recent log entries captured by the tracing layer. Defaults to the last 50 lines."
18    }
19    fn input_schema(&self) -> Value {
20        json!({
21            "type": "object",
22            "properties": {
23                "lines": { "type": "integer", "description": "Number of entries to return.", "default": 50, "minimum": 1, "maximum": 5000 },
24                "level": { "type": "string", "description": "Filter: TRACE/DEBUG/INFO/WARN/ERROR." }
25            }
26        })
27    }
28
29    async fn call(&self, ctx: &Context, args: Value) -> CallToolResult {
30        let n = args.get("lines").and_then(|v| v.as_u64()).unwrap_or(50) as usize;
31        let level = args
32            .get("level")
33            .and_then(|v| v.as_str())
34            .map(str::to_ascii_uppercase);
35        let entries = ctx.log_buffer.tail(n.min(5000));
36        let filtered: Vec<_> = match level {
37            Some(lvl) => entries
38                .into_iter()
39                .filter(|e| e.level.eq_ignore_ascii_case(&lvl))
40                .collect(),
41            None => entries,
42        };
43        CallToolResult::json(&json!({
44            "count": filtered.len(),
45            "entries": filtered,
46        }))
47    }
48}
49
50pub struct LastError;
51
52#[async_trait]
53impl Tool for LastError {
54    fn name(&self) -> &'static str {
55        "last-error"
56    }
57    fn description(&self) -> &'static str {
58        "Fetch the most recent ERROR-level log entry, if any. Use this after an action fails to grab the stack/context."
59    }
60
61    async fn call(&self, ctx: &Context, _args: Value) -> CallToolResult {
62        match ctx.log_buffer.last_error() {
63            Some(entry) => {
64                CallToolResult::json(&serde_json::to_value(entry).unwrap_or(Value::Null))
65            }
66            None => CallToolResult::json(
67                &json!({ "error": null, "note": "no error-level entries captured since startup" }),
68            ),
69        }
70    }
71}