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 status: String,
29    #[serde(skip_serializing_if = "Option::is_none")]
30    pub error_message: Option<String>,
31    pub provider: Option<String>,
32    pub model: Option<String>,
33    pub requested_model: Option<String>,
34    pub input_tokens: i32,
35    pub output_tokens: i32,
36    pub cache_read_tokens: i32,
37    pub cache_creation_tokens: i32,
38    pub cost_dollars: f64,
39    pub latency_ms: i64,
40    pub task_id: Option<TaskId>,
41    pub trace_id: Option<TraceId>,
42    pub messages: Vec<MessageRow>,
43    pub tool_calls: Vec<AuditToolCall>,
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
47pub struct AuditToolCall {
48    pub tool_name: String,
49    pub tool_input: String,
50    pub sequence: i32,
51}
52
53crate::define_pool_command!(AuditArgs => (), with_config);
54
55async fn execute_with_pool_inner(
56    args: AuditArgs,
57    pool: &Arc<sqlx::PgPool>,
58    config: &CliConfig,
59) -> Result<()> {
60    let service = TraceQueryService::new(Arc::clone(pool));
61
62    let row = service.find_ai_request_for_audit(&args.id).await?;
63
64    let Some(row) = row else {
65        render_result(&not_found_output(&args.id), config);
66        return Ok(());
67    };
68
69    let request_id = row.id;
70    let (messages, tool_calls) = tokio::try_join!(
71        service.list_audit_messages(&request_id),
72        service.list_audit_tool_calls(&request_id),
73    )?;
74
75    let output = AuditOutput {
76        request_id,
77        status: row.status,
78        error_message: row.error_message,
79        provider: row.provider,
80        model: row.model,
81        requested_model: row.requested_model,
82        input_tokens: row.input_tokens.unwrap_or(0),
83        output_tokens: row.output_tokens.unwrap_or(0),
84        cache_read_tokens: row.cache_read_tokens.unwrap_or(0),
85        cache_creation_tokens: row.cache_creation_tokens.unwrap_or(0),
86        cost_dollars: row.cost_microdollars as f64 / 1_000_000.0,
87        latency_ms: i64::from(row.latency_ms.unwrap_or(0)),
88        task_id: row.task_id,
89        trace_id: row.trace_id.map(TraceId::new),
90        messages: messages
91            .into_iter()
92            .map(|m| MessageRow {
93                sequence: m.sequence_number,
94                role: m.role,
95                content: m.content,
96            })
97            .collect(),
98        tool_calls: tool_calls
99            .into_iter()
100            .map(|t| AuditToolCall {
101                tool_name: t.tool_name,
102                tool_input: t.tool_input,
103                sequence: t.sequence_number,
104            })
105            .collect(),
106    };
107
108    render_result(&build_audit(&output), config);
109
110    Ok(())
111}
112
113#[must_use]
114pub fn build_audit(output: &AuditOutput) -> CommandOutput {
115    // Why: audit is the richest single view; a failed request must announce
116    // itself in the title, not hide behind zero token counts.
117    let title = if output.status == "completed" {
118        "AI Request Audit".to_owned()
119    } else {
120        format!("AI Request Audit — {}", output.status.to_uppercase())
121    };
122    CommandOutput::card_value(title, output)
123}
124
125#[must_use]
126pub fn not_found_output(id: &str) -> CommandOutput {
127    use systemprompt_models::artifacts::NoticeLine;
128    CommandOutput::message(vec![
129        NoticeLine::new("warning", format!("No AI request found for: {id}")),
130        NoticeLine::new(
131            "info",
132            "Tip: Use 'systemprompt infra logs request list' to see recent requests",
133        ),
134        NoticeLine::new(
135            "info",
136            "Use 'systemprompt infra logs trace list' to see recent traces",
137        ),
138    ])
139}