Skip to main content

systemprompt_cli/commands/plugins/mcp/
logs.rs

1//! `plugins mcp logs` command dispatch.
2//!
3//! Copyright (c) systemprompt.io — Business Source License 1.1.
4//! See <https://systemprompt.io> for licensing details.
5
6use std::path::PathBuf;
7
8use anyhow::Result;
9use clap::{Args, ValueEnum};
10
11use super::logs_db::execute_db_mode;
12use super::logs_disk::{execute_disk_mode, execute_follow_mode};
13use crate::CliConfig;
14use crate::interactive::Prompter;
15use crate::shared::CommandOutput;
16use systemprompt_config::ProfileBootstrap;
17use systemprompt_models::AppPaths;
18
19#[derive(Debug, Clone, Copy, ValueEnum, PartialEq, Eq)]
20pub enum LogLevel {
21    Debug,
22    Info,
23    Warn,
24    Error,
25}
26
27impl LogLevel {
28    pub fn matches(&self, level_str: &str) -> bool {
29        let level_upper = level_str.to_uppercase();
30        match self {
31            Self::Debug => true,
32            Self::Info => !level_upper.contains("DEBUG"),
33            Self::Warn => level_upper.contains("WARN") || level_upper.contains("ERROR"),
34            Self::Error => level_upper.contains("ERROR"),
35        }
36    }
37}
38
39#[derive(Debug, Args)]
40pub struct LogsArgs {
41    #[arg(help = "MCP server name (optional - shows all MCP logs if not specified)")]
42    pub server: Option<String>,
43
44    #[arg(
45        long,
46        short = 'n',
47        visible_alias = "tail",
48        default_value = "50",
49        help = "Number of lines to show"
50    )]
51    pub lines: usize,
52
53    #[arg(long, short, help = "Follow log output continuously (disk only)")]
54    pub follow: bool,
55
56    #[arg(long, help = "Force reading from disk files instead of database")]
57    pub disk: bool,
58
59    #[arg(long, help = "Custom logs directory path")]
60    pub logs_dir: Option<String>,
61
62    #[arg(
63        long,
64        value_enum,
65        help = "Filter by log level: debug, info, warn, error"
66    )]
67    pub level: Option<LogLevel>,
68}
69
70fn get_default_logs_dir() -> PathBuf {
71    ProfileBootstrap::get()
72        .ok()
73        .and_then(|p| AppPaths::from_profile(&p.paths).ok())
74        .map_or_else(|| PathBuf::from("/var/log"), |paths| paths.system().logs())
75}
76
77pub(super) async fn execute(
78    args: LogsArgs,
79    prompter: &dyn Prompter,
80    config: &CliConfig,
81) -> Result<CommandOutput> {
82    let logs_path = args
83        .logs_dir
84        .as_ref()
85        .map_or_else(get_default_logs_dir, PathBuf::from);
86
87    if args.follow {
88        return execute_follow_mode(&args, prompter, config, &logs_path);
89    }
90
91    if args.disk {
92        return execute_disk_mode(&args, prompter, config, &logs_path);
93    }
94
95    match execute_db_mode(&args, config).await {
96        Ok(result) => Ok(result),
97        Err(e) => {
98            tracing::debug!(error = %e, "DB log query failed, falling back to disk");
99            execute_disk_mode(&args, prompter, config, &logs_path)
100        },
101    }
102}