Skip to main content

mcpr_integrations/store/query/
logs.rs

1//! Query: `mcpr proxy logs <proxy>` — recent request log with filtering.
2
3use rusqlite::{Row, params};
4use serde::Serialize;
5
6use super::QueryEngine;
7
8/// Filter parameters for the logs query.
9pub struct LogsParams {
10    /// Proxy name to filter by (None = all proxies).
11    pub proxy: Option<String>,
12    /// Only rows newer than this unix ms timestamp.
13    pub since_ts: i64,
14    /// Maximum number of rows to return.
15    pub limit: i64,
16    /// Filter to a specific tool name.
17    pub tool: Option<String>,
18    /// Filter by MCP method (e.g., "tools/call", "resources/read").
19    pub method: Option<String>,
20    /// Filter by session ID.
21    pub session: Option<String>,
22    /// Filter by status ("ok", "error", "timeout").
23    pub status: Option<String>,
24    /// Filter by JSON-RPC error code (e.g., "-32601").
25    pub error_code: Option<String>,
26}
27
28/// A single row from the logs/slow query.
29#[derive(Debug, Clone, Serialize)]
30pub struct LogRow {
31    pub request_id: String,
32    pub ts: i64,
33    pub method: String,
34    pub tool: Option<String>,
35    pub latency_us: i64,
36    pub status: String,
37    pub error_code: Option<String>,
38    pub error_msg: Option<String>,
39    pub session_id: Option<String>,
40    pub bytes_in: Option<i64>,
41    pub bytes_out: Option<i64>,
42}
43
44/// Shared row mapper — used by logs, logs_since, slow, slow_since to avoid
45/// duplicating the 11-column mapping closure.
46pub(crate) fn map_log_row(row: &Row<'_>) -> rusqlite::Result<LogRow> {
47    Ok(LogRow {
48        request_id: row.get(0)?,
49        ts: row.get(1)?,
50        method: row.get(2)?,
51        tool: row.get(3)?,
52        latency_us: row.get(4)?,
53        status: row.get(5)?,
54        error_code: row.get(6)?,
55        error_msg: row.get(7)?,
56        session_id: row.get(8)?,
57        bytes_in: row.get(9)?,
58        bytes_out: row.get(10)?,
59    })
60}
61
62/// The 11 columns selected in all log/slow queries.
63pub(crate) const LOG_COLUMNS: &str = "request_id, ts, method, tool, latency_us, status, error_code, error_msg, session_id, bytes_in, bytes_out";
64
65impl QueryEngine {
66    /// Fetch recent request logs, newest first.
67    pub fn logs(&self, params: &LogsParams) -> Result<Vec<LogRow>, rusqlite::Error> {
68        let sql = format!(
69            "SELECT {LOG_COLUMNS}
70            FROM requests
71            WHERE (?1 IS NULL OR proxy = ?1)
72              AND (?2 IS NULL OR tool = ?2)
73              AND (?3 IS NULL OR status = ?3)
74              AND (?4 IS NULL OR method = ?4)
75              AND (?5 IS NULL OR session_id LIKE ?5 || '%')
76              AND (?6 IS NULL OR error_code = ?6)
77              AND ts >= ?7
78            ORDER BY ts DESC
79            LIMIT ?8"
80        );
81
82        let mut stmt = self.conn().prepare(&sql)?;
83        let rows = stmt.query_map(
84            params![
85                params.proxy,
86                params.tool,
87                params.status,
88                params.method,
89                params.session,
90                params.error_code,
91                params.since_ts,
92                params.limit,
93            ],
94            map_log_row,
95        )?;
96
97        rows.collect()
98    }
99
100    /// Fetch logs newer than a given timestamp, oldest first.
101    ///
102    /// Used for `--follow` mode: poll every 500ms with the last seen timestamp.
103    pub fn logs_since(
104        &self,
105        params: &LogsParams,
106        after_ts: i64,
107    ) -> Result<Vec<LogRow>, rusqlite::Error> {
108        let sql = format!(
109            "SELECT {LOG_COLUMNS}
110            FROM requests
111            WHERE (?1 IS NULL OR proxy = ?1)
112              AND (?2 IS NULL OR tool = ?2)
113              AND (?3 IS NULL OR status = ?3)
114              AND (?4 IS NULL OR method = ?4)
115              AND (?5 IS NULL OR session_id LIKE ?5 || '%')
116              AND (?6 IS NULL OR error_code = ?6)
117              AND ts > ?7
118            ORDER BY ts ASC"
119        );
120
121        let mut stmt = self.conn().prepare(&sql)?;
122        let rows = stmt.query_map(
123            params![
124                params.proxy,
125                params.tool,
126                params.status,
127                params.method,
128                params.session,
129                params.error_code,
130                after_ts
131            ],
132            map_log_row,
133        )?;
134
135        rows.collect()
136    }
137}