use chrono::{DateTime, Utc};
use systemprompt_identifiers::{LogId, McpServerId, McpToolName, TraceId};
use systemprompt_logging::LogLevel;
use super::{Result, TraceRepository};
use crate::trace::models::{LogSearchItem, ToolExecutionItem};
struct LogRow {
id: LogId,
trace_id: TraceId,
timestamp: DateTime<Utc>,
level: String,
module: String,
message: String,
metadata: Option<String>,
}
impl TraceRepository {
pub async fn search_logs(
&self,
pattern: &str,
since: Option<DateTime<Utc>>,
level: Option<LogLevel>,
limit: i64,
) -> Result<Vec<LogSearchItem>> {
let rows = sqlx::query_as!(
LogRow,
r#"
SELECT
id as "id!: LogId",
trace_id as "trace_id!: TraceId",
timestamp as "timestamp!",
level as "level!",
module as "module!",
message as "message!",
metadata
FROM logs
WHERE message ILIKE $1
AND ($2::timestamptz IS NULL OR timestamp >= $2)
AND ($3::text IS NULL OR UPPER(level) = $3)
ORDER BY timestamp DESC
LIMIT $4
"#,
pattern,
since,
level.map(LogLevel::as_str),
limit
)
.fetch_all(&*self.pool)
.await?;
rows.into_iter()
.map(|r| -> Result<LogSearchItem> {
Ok(LogSearchItem {
id: r.id,
trace_id: r.trace_id,
timestamp: r.timestamp,
level: r.level.parse()?,
module: r.module,
message: r.message,
metadata: r.metadata,
})
})
.collect()
}
pub async fn search_tool_executions(
&self,
pattern: &str,
since: Option<DateTime<Utc>>,
limit: i64,
) -> Result<Vec<ToolExecutionItem>> {
let rows = sqlx::query!(
r#"
SELECT
started_at as "timestamp!",
trace_id as "trace_id!: TraceId",
tool_name as "tool_name!",
server_name,
status as "status!",
execution_time_ms
FROM mcp_tool_executions
WHERE (tool_name ILIKE $1 OR server_name ILIKE $1)
AND ($2::timestamptz IS NULL OR started_at >= $2)
ORDER BY started_at DESC
LIMIT $3
"#,
pattern,
since,
limit
)
.fetch_all(&*self.pool)
.await?;
Ok(rows
.into_iter()
.map(|r| ToolExecutionItem {
timestamp: r.timestamp,
trace_id: r.trace_id,
tool_name: McpToolName::new(r.tool_name),
server_name: Some(McpServerId::new(r.server_name)),
status: r.status,
execution_time_ms: r.execution_time_ms,
})
.collect())
}
}