Skip to main content

systemprompt_runtime/trace/repository/
list.rs

1//! Trace listing over the stored `trace_facts` projection, one row per trace,
2//! with filter-driven pagination.
3//!
4//! Copyright (c) systemprompt.io — Business Source License 1.1.
5//! See <https://systemprompt.io> for licensing details.
6
7use chrono::{DateTime, Utc};
8
9use systemprompt_identifiers::TraceId;
10
11use super::{Result, TraceRepository};
12use crate::trace::models::{TraceListFilter, TraceListItem};
13
14struct TraceRow {
15    trace_id: TraceId,
16    first_timestamp: DateTime<Utc>,
17    last_timestamp: DateTime<Utc>,
18    agent: Option<String>,
19    status: String,
20    ai_requests: i64,
21    mcp_calls: i64,
22}
23
24impl TraceRepository {
25    pub async fn list_traces(&self, filter: &TraceListFilter) -> Result<Vec<TraceListItem>> {
26        let rows = self.fetch_trace_rows(filter).await?;
27
28        Ok(rows
29            .into_iter()
30            .map(|r| TraceListItem {
31                trace_id: r.trace_id,
32                first_timestamp: r.first_timestamp,
33                last_timestamp: r.last_timestamp,
34                agent: r.agent,
35                status: r.status,
36                ai_requests: r.ai_requests,
37                mcp_calls: r.mcp_calls,
38            })
39            .collect())
40    }
41
42    async fn fetch_trace_rows(&self, filter: &TraceListFilter) -> Result<Vec<TraceRow>> {
43        let tool_pattern = filter.tool.as_deref();
44        let agent_pattern = filter.agent.as_ref().map(|a| format!("%{a}%"));
45        let agent_pat = agent_pattern.as_deref();
46        let status_lower = filter.status.as_ref().map(|s| s.to_lowercase());
47        let status_val = status_lower.as_deref();
48        let exclude_system = !filter.include_system;
49
50        sqlx::query_as!(
51            TraceRow,
52            r#"
53            SELECT trace_id AS "trace_id!: TraceId", first_at AS "first_timestamp!",
54                   last_at AS "last_timestamp!", agent_name AS "agent", status AS "status!",
55                   ai_count AS "ai_requests!", mcp_count AS "mcp_calls!"
56            FROM trace_facts
57            WHERE ($2::timestamptz IS NULL OR last_at >= $2)
58              AND ($1::text IS NULL OR EXISTS (SELECT 1 FROM unnest(tool_names) AS t(name) WHERE t.name ILIKE $1))
59              AND (NOT $3::boolean OR trace_id <> 'system')
60              AND (NOT $3::boolean OR ai_count > 0 OR mcp_count > 0 OR agent_name IS NOT NULL)
61              AND ($4::text IS NULL OR agent_name ILIKE $4)
62              AND ($5::text IS NULL OR LOWER(status) = $5)
63              AND ($6::boolean IS NOT TRUE OR mcp_count > 0)
64            ORDER BY first_at DESC
65            LIMIT $7
66            "#,
67            tool_pattern,
68            filter.since,
69            exclude_system,
70            agent_pat,
71            status_val,
72            Some(filter.has_mcp),
73            filter.limit
74        )
75        .fetch_all(&*self.pool)
76        .await
77        .map_err(Into::into)
78    }
79}