systemprompt-cli 0.1.22

systemprompt.io OS - CLI for agent orchestration, AI operations, and system management
Documentation
use anyhow::{Result, anyhow};
use clap::Args;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use systemprompt_logging::{CliService, LogEntry, TraceQueryService};

use crate::CliConfig;
use crate::shared::{CommandResult, render_result};
use systemprompt_models::text::truncate_with_ellipsis;

#[derive(Debug, Args)]
pub struct ShowArgs {
    #[arg(help = "Log entry ID or trace ID (can be partial)")]
    pub id: String,

    #[arg(long, help = "Output as JSON")]
    pub json: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct LogShowOutput {
    pub id: String,
    pub trace_id: String,
    pub timestamp: String,
    pub level: String,
    pub module: String,
    pub message: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub metadata: Option<serde_json::Value>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub user_id: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub session_id: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub task_id: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub context_id: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct TraceLogsOutput {
    pub trace_id: String,
    pub total: u64,
    pub logs: Vec<LogShowOutput>,
}

crate::define_pool_command!(ShowArgs => (), with_config);

async fn execute_with_pool_inner(
    args: ShowArgs,
    pool: &Arc<sqlx::PgPool>,
    config: &CliConfig,
) -> Result<()> {
    let service = TraceQueryService::new(Arc::clone(pool));

    if let Some(log) = service.find_log_by_id(&args.id).await? {
        display_single_log(&log, config, args.json);
        return Ok(());
    }

    let logs = service.find_logs_by_trace_id(&args.id).await?;
    if !logs.is_empty() {
        display_trace_logs(&logs, config, args.json);
        return Ok(());
    }

    if let Some(log) = service.find_log_by_partial_id(&args.id).await? {
        display_single_log(&log, config, args.json);
        return Ok(());
    }

    Err(anyhow!(
        "No log entries found for ID: {}. Try 'logs view' to see recent logs.",
        args.id
    ))
}

fn entry_to_output(entry: &LogEntry) -> LogShowOutput {
    LogShowOutput {
        id: entry.id.to_string(),
        trace_id: entry.trace_id.to_string(),
        timestamp: entry.timestamp.format("%Y-%m-%d %H:%M:%S%.3f").to_string(),
        level: entry.level.to_string().to_uppercase(),
        module: entry.module.clone(),
        message: entry.message.clone(),
        metadata: entry.metadata.clone(),
        user_id: Some(entry.user_id.to_string()),
        session_id: Some(entry.session_id.to_string()),
        task_id: entry.task_id.as_ref().map(ToString::to_string),
        context_id: entry.context_id.as_ref().map(ToString::to_string),
    }
}

fn display_single_log(log: &LogEntry, config: &CliConfig, json: bool) {
    let output = entry_to_output(log);

    if config.is_json_output() || json {
        let result = CommandResult::table(output).with_title("Log Entry Details");
        render_result(&result);
        return;
    }

    CliService::section("Log Entry Details");
    CliService::key_value("ID", &output.id);
    CliService::key_value("Trace ID", &output.trace_id);
    CliService::key_value("Timestamp", &output.timestamp);
    CliService::key_value("Level", &output.level);
    CliService::key_value("Module", &output.module);
    CliService::key_value("Message", &output.message);

    if let Some(user_id) = &output.user_id {
        CliService::key_value("User ID", user_id);
    }
    if let Some(session_id) = &output.session_id {
        CliService::key_value("Session ID", session_id);
    }
    if let Some(task_id) = &output.task_id {
        CliService::key_value("Task ID", task_id);
    }
    if let Some(context_id) = &output.context_id {
        CliService::key_value("Context ID", context_id);
    }

    if let Some(metadata) = &output.metadata {
        CliService::subsection("Metadata");
        if let Some(obj) = metadata.as_object() {
            for (key, value) in obj {
                let formatted = format!("{}", value).trim_matches('"').to_string();
                CliService::key_value(key, &formatted);
            }
        } else {
            CliService::info(&format!("{}", metadata));
        }
    }

    CliService::info("");
    CliService::info(&format!(
        "Tip: Use 'logs trace show {}' for full execution trace",
        truncate_with_ellipsis(&output.trace_id, 12)
    ));
}

fn display_trace_logs(logs: &[LogEntry], config: &CliConfig, json: bool) {
    let Some(first_log) = logs.first() else {
        return;
    };
    let trace_id = first_log.trace_id.to_string();
    let outputs: Vec<LogShowOutput> = logs.iter().map(entry_to_output).collect();

    let output = TraceLogsOutput {
        trace_id: trace_id.clone(),
        total: outputs.len() as u64,
        logs: outputs,
    };

    if config.is_json_output() || json {
        let result = CommandResult::table(output).with_title("Logs for Trace");
        render_result(&result);
        return;
    }

    CliService::section(&format!(
        "Logs for Trace: {}",
        truncate_with_ellipsis(&trace_id, 12)
    ));
    CliService::info(&format!("Found {} log entries", logs.len()));
    CliService::info("");

    for log in logs {
        let time_part = log.timestamp.format("%H:%M:%S%.3f").to_string();
        let level_str = log.level.to_string().to_uppercase();
        let line = format!(
            "{} {} [{}] {}",
            time_part, level_str, log.module, log.message
        );

        match level_str.as_str() {
            "ERROR" => CliService::error(&line),
            "WARN" => CliService::warning(&line),
            _ => CliService::info(&line),
        }
    }

    CliService::info("");
    CliService::info(&format!(
        "Tip: Use 'logs trace show {}' for full trace with AI/MCP details",
        truncate_with_ellipsis(&trace_id, 12)
    ));
}