Skip to main content

systemprompt_cli/commands/infrastructure/logs/
audit.rs

1//! `infra logs audit` command rendering the audit trail.
2//!
3//! Copyright (c) systemprompt.io — Business Source License 1.1.
4//! See <https://systemprompt.io> for licensing details.
5
6use std::sync::Arc;
7
8use anyhow::Result;
9use clap::Args;
10use schemars::JsonSchema;
11use serde::{Deserialize, Serialize};
12use systemprompt_identifiers::{AiRequestId, TaskId, TraceId};
13use systemprompt_logging::TraceQueryService;
14
15use super::types::MessageRow;
16use crate::CliConfig;
17use crate::shared::{CommandOutput, render_result};
18
19#[derive(Debug, Args)]
20pub struct AuditArgs {
21    #[arg(help = "AI request ID, task ID, or trace ID")]
22    pub id: String,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
26pub struct AuditOutput {
27    pub request_id: AiRequestId,
28    pub provider: String,
29    pub model: String,
30    pub requested_model: Option<String>,
31    pub input_tokens: i32,
32    pub output_tokens: i32,
33    pub cost_dollars: f64,
34    pub latency_ms: i64,
35    pub task_id: Option<TaskId>,
36    pub trace_id: Option<TraceId>,
37    pub messages: Vec<MessageRow>,
38    pub tool_calls: Vec<AuditToolCall>,
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
42pub struct AuditToolCall {
43    pub tool_name: String,
44    pub tool_input: String,
45    pub sequence: i32,
46}
47
48crate::define_pool_command!(AuditArgs => (), with_config);
49
50async fn execute_with_pool_inner(
51    args: AuditArgs,
52    pool: &Arc<sqlx::PgPool>,
53    config: &CliConfig,
54) -> Result<()> {
55    let service = TraceQueryService::new(Arc::clone(pool));
56
57    let row = service.find_ai_request_for_audit(&args.id).await?;
58
59    let Some(row) = row else {
60        render_result(&not_found_output(&args.id), config);
61        return Ok(());
62    };
63
64    let request_id = row.id;
65    let (messages, tool_calls) = tokio::try_join!(
66        service.list_audit_messages(&request_id),
67        service.list_audit_tool_calls(&request_id),
68    )?;
69
70    let output = AuditOutput {
71        request_id,
72        provider: row.provider,
73        model: row.model,
74        requested_model: row.requested_model,
75        input_tokens: row.input_tokens.unwrap_or(0),
76        output_tokens: row.output_tokens.unwrap_or(0),
77        cost_dollars: row.cost_microdollars as f64 / 1_000_000.0,
78        latency_ms: i64::from(row.latency_ms.unwrap_or(0)),
79        task_id: row.task_id,
80        trace_id: row.trace_id.map(TraceId::new),
81        messages: messages
82            .into_iter()
83            .map(|m| MessageRow {
84                sequence: m.sequence_number,
85                role: m.role,
86                content: m.content,
87            })
88            .collect(),
89        tool_calls: tool_calls
90            .into_iter()
91            .map(|t| AuditToolCall {
92                tool_name: t.tool_name,
93                tool_input: t.tool_input,
94                sequence: t.sequence_number,
95            })
96            .collect(),
97    };
98
99    render_result(&build_audit(&output), config);
100
101    Ok(())
102}
103
104#[must_use]
105pub fn build_audit(output: &AuditOutput) -> CommandOutput {
106    CommandOutput::card_value("AI Request Audit", output)
107}
108
109#[must_use]
110pub fn not_found_output(id: &str) -> CommandOutput {
111    use systemprompt_models::artifacts::NoticeLine;
112    CommandOutput::message(vec![
113        NoticeLine::new("warning", format!("No AI request found for: {id}")),
114        NoticeLine::new(
115            "info",
116            "Tip: Use 'systemprompt infra logs request list' to see recent requests",
117        ),
118        NoticeLine::new(
119            "info",
120            "Use 'systemprompt infra logs trace list' to see recent traces",
121        ),
122    ])
123}