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 resource_uri: Option<String>,
36    pub prompt_name: Option<String>,
37    pub latency_us: i64,
38    pub status: String,
39    pub error_code: Option<String>,
40    pub error_msg: Option<String>,
41    pub session_id: Option<String>,
42    pub bytes_in: Option<i64>,
43    pub bytes_out: Option<i64>,
44}
45
46/// Shared row mapper — used by logs, logs_since, slow, slow_since to avoid
47/// duplicating the column mapping closure.
48pub(crate) fn map_log_row(row: &Row<'_>) -> rusqlite::Result<LogRow> {
49    Ok(LogRow {
50        request_id: row.get(0)?,
51        ts: row.get(1)?,
52        method: row.get(2)?,
53        tool: row.get(3)?,
54        resource_uri: row.get(4)?,
55        prompt_name: row.get(5)?,
56        latency_us: row.get(6)?,
57        status: row.get(7)?,
58        error_code: row.get(8)?,
59        error_msg: row.get(9)?,
60        session_id: row.get(10)?,
61        bytes_in: row.get(11)?,
62        bytes_out: row.get(12)?,
63    })
64}
65
66/// Columns selected in all log/slow queries — must align with `map_log_row`.
67pub(crate) const LOG_COLUMNS: &str = "request_id, ts, method, tool, resource_uri, prompt_name, latency_us, status, error_code, error_msg, session_id, bytes_in, bytes_out";
68
69impl QueryEngine {
70    /// Fetch recent request logs, newest first.
71    pub fn logs(&self, params: &LogsParams) -> Result<Vec<LogRow>, rusqlite::Error> {
72        let sql = format!(
73            "SELECT {LOG_COLUMNS}
74            FROM requests
75            WHERE (?1 IS NULL OR proxy = ?1)
76              AND (?2 IS NULL OR tool = ?2)
77              AND (?3 IS NULL OR status = ?3)
78              AND (?4 IS NULL OR method = ?4)
79              AND (?5 IS NULL OR session_id LIKE ?5 || '%')
80              AND (?6 IS NULL OR error_code = ?6)
81              AND ts >= ?7
82            ORDER BY ts DESC
83            LIMIT ?8"
84        );
85
86        let mut stmt = self.conn().prepare(&sql)?;
87        let rows = stmt.query_map(
88            params![
89                params.proxy,
90                params.tool,
91                params.status,
92                params.method,
93                params.session,
94                params.error_code,
95                params.since_ts,
96                params.limit,
97            ],
98            map_log_row,
99        )?;
100
101        rows.collect()
102    }
103
104    /// Fetch logs newer than a given timestamp, oldest first.
105    ///
106    /// Used for `--follow` mode: poll every 500ms with the last seen timestamp.
107    pub fn logs_since(
108        &self,
109        params: &LogsParams,
110        after_ts: i64,
111    ) -> Result<Vec<LogRow>, rusqlite::Error> {
112        let sql = format!(
113            "SELECT {LOG_COLUMNS}
114            FROM requests
115            WHERE (?1 IS NULL OR proxy = ?1)
116              AND (?2 IS NULL OR tool = ?2)
117              AND (?3 IS NULL OR status = ?3)
118              AND (?4 IS NULL OR method = ?4)
119              AND (?5 IS NULL OR session_id LIKE ?5 || '%')
120              AND (?6 IS NULL OR error_code = ?6)
121              AND ts > ?7
122            ORDER BY ts ASC"
123        );
124
125        let mut stmt = self.conn().prepare(&sql)?;
126        let rows = stmt.query_map(
127            params![
128                params.proxy,
129                params.tool,
130                params.status,
131                params.method,
132                params.session,
133                params.error_code,
134                after_ts
135            ],
136            map_log_row,
137        )?;
138
139        rows.collect()
140    }
141}