systemprompt_runtime/trace/repository/
list.rs1use chrono::{DateTime, Utc};
7
8use systemprompt_identifiers::TraceId;
9
10use super::{Result, TraceRepository};
11use crate::trace::models::{TraceListFilter, TraceListItem};
12
13struct TraceRow {
14 trace_id: TraceId,
15 first_timestamp: DateTime<Utc>,
16 last_timestamp: DateTime<Utc>,
17 agent: Option<String>,
18 status: String,
19 ai_requests: Option<i64>,
20 mcp_calls: Option<i64>,
21}
22
23impl TraceRepository {
24 pub async fn list_traces(&self, filter: &TraceListFilter) -> Result<Vec<TraceListItem>> {
25 let rows = self.fetch_trace_rows(filter).await?;
26
27 Ok(rows
28 .into_iter()
29 .map(|r| TraceListItem {
30 trace_id: r.trace_id,
31 first_timestamp: r.first_timestamp,
32 last_timestamp: r.last_timestamp,
33 agent: r.agent,
34 status: r.status,
35 ai_requests: r.ai_requests.unwrap_or(0),
36 mcp_calls: r.mcp_calls.unwrap_or(0),
37 })
38 .collect())
39 }
40
41 async fn fetch_trace_rows(&self, filter: &TraceListFilter) -> Result<Vec<TraceRow>> {
42 let tool_pattern = filter.tool.as_deref();
43 let agent_pattern = filter.agent.as_ref().map(|a| format!("%{a}%"));
44 let agent_pat = agent_pattern.as_deref();
45 let status_lower = filter.status.as_ref().map(|s| s.to_lowercase());
46 let status_val = status_lower.as_deref();
47 let exclude_system = (!filter.include_system).then_some("1");
48 let require_tracked: Option<&str> = None;
49
50 sqlx::query_as!(
51 TraceRow,
52 r#"
53 WITH tool_traces AS (
54 SELECT DISTINCT trace_id FROM mcp_tool_executions
55 WHERE ($1::text IS NULL OR tool_name ILIKE $1) AND ($2::timestamptz IS NULL OR started_at >= $2)
56 ),
57 all_traces AS (
58 SELECT trace_id, timestamp as ts FROM logs
59 WHERE trace_id IS NOT NULL AND ($2::timestamptz IS NULL OR timestamp >= $2) AND ($1::text IS NULL OR trace_id IN (SELECT trace_id FROM tool_traces))
60 UNION ALL
61 SELECT trace_id, created_at as ts FROM ai_requests
62 WHERE trace_id IS NOT NULL AND ($2::timestamptz IS NULL OR created_at >= $2) AND ($1::text IS NULL OR trace_id IN (SELECT trace_id FROM tool_traces))
63 UNION ALL
64 SELECT trace_id, started_at as ts FROM mcp_tool_executions
65 WHERE trace_id IS NOT NULL AND ($2::timestamptz IS NULL OR started_at >= $2) AND ($1::text IS NULL OR trace_id IN (SELECT trace_id FROM tool_traces))
66 UNION ALL
67 SELECT trace_id, created_at as ts FROM agent_tasks
68 WHERE trace_id IS NOT NULL AND trace_id <> '' AND ($2::timestamptz IS NULL OR created_at >= $2) AND ($1::text IS NULL OR trace_id IN (SELECT trace_id FROM tool_traces))
69 ),
70 grouped AS (
71 SELECT
72 t.trace_id,
73 MIN(t.ts) as first_ts,
74 MAX(t.ts) as last_ts,
75 (SELECT at.agent_name FROM agent_tasks at WHERE at.trace_id = t.trace_id AND at.trace_id <> '' ORDER BY at.updated_at DESC LIMIT 1) as agent,
76 COALESCE(
77 CASE WHEN EXISTS (SELECT 1 FROM agent_tasks at WHERE at.trace_id = t.trace_id AND at.trace_id <> '' AND at.status IN ('TASK_STATE_PENDING','TASK_STATE_SUBMITTED','TASK_STATE_WORKING','TASK_STATE_INPUT_REQUIRED','TASK_STATE_AUTH_REQUIRED'))
78 OR EXISTS (SELECT 1 FROM ai_requests ar WHERE ar.trace_id = t.trace_id AND ar.status = 'pending')
79 OR EXISTS (SELECT 1 FROM mcp_tool_executions mte WHERE mte.trace_id = t.trace_id AND mte.status = 'pending') THEN 'running' END,
80 CASE WHEN EXISTS (SELECT 1 FROM agent_tasks at WHERE at.trace_id = t.trace_id AND at.trace_id <> '' AND at.status IN ('TASK_STATE_FAILED','TASK_STATE_REJECTED'))
81 OR EXISTS (SELECT 1 FROM ai_requests ar WHERE ar.trace_id = t.trace_id AND ar.status = 'failed')
82 OR EXISTS (SELECT 1 FROM mcp_tool_executions mte WHERE mte.trace_id = t.trace_id AND mte.status IN ('failed','timeout'))
83 OR (NOT EXISTS (SELECT 1 FROM agent_tasks at WHERE at.trace_id = t.trace_id AND at.trace_id <> '') AND NOT EXISTS (SELECT 1 FROM ai_requests ar WHERE ar.trace_id = t.trace_id) AND NOT EXISTS (SELECT 1 FROM mcp_tool_executions mte WHERE mte.trace_id = t.trace_id) AND EXISTS (SELECT 1 FROM logs l WHERE l.trace_id = t.trace_id AND l.level = 'ERROR')) THEN 'failed' END,
84 CASE WHEN EXISTS (SELECT 1 FROM agent_tasks at WHERE at.trace_id = t.trace_id AND at.trace_id <> '' AND at.status = 'TASK_STATE_CANCELED') THEN 'canceled' END,
85 'completed'
86 ) as status,
87 (SELECT COUNT(*) FROM ai_requests ar WHERE ar.trace_id = t.trace_id) as ai_requests,
88 (SELECT COUNT(*) FROM mcp_tool_executions mte WHERE mte.trace_id = t.trace_id) as mcp_calls
89 FROM all_traces t
90 GROUP BY t.trace_id
91 )
92 SELECT trace_id as "trace_id!: TraceId", first_ts as "first_timestamp!", last_ts as "last_timestamp!",
93 agent as "agent", status as "status!", ai_requests as "ai_requests", mcp_calls as "mcp_calls"
94 FROM grouped
95 WHERE ($3::text IS NULL OR trace_id != 'system') AND ($4::text IS NULL OR agent ILIKE $4)
96 AND ($5::text IS NULL OR LOWER(status) = $5) AND ($6::boolean IS NOT TRUE OR mcp_calls > 0)
97 AND ($7::text IS NULL OR status IS NOT NULL)
98 AND ($3::text IS NULL OR ai_requests > 0 OR mcp_calls > 0 OR agent IS NOT NULL)
99 ORDER BY first_ts DESC
100 LIMIT $8
101 "#,
102 tool_pattern,
103 filter.since,
104 exclude_system,
105 agent_pat,
106 status_val,
107 Some(filter.has_mcp),
108 require_tracked,
109 filter.limit
110 )
111 .fetch_all(&*self.pool)
112 .await
113 .map_err(Into::into)
114 }
115}