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_config::paths::AppPaths;
18use systemprompt_loader::ServicesRootBootstrap;
19
20#[derive(Debug, Clone, Copy, ValueEnum, PartialEq, Eq)]
21pub enum LogLevel {
22    Debug,
23    Info,
24    Warn,
25    Error,
26}
27
28impl LogLevel {
29    pub fn matches(&self, level_str: &str) -> bool {
30        let level_upper = level_str.to_uppercase();
31        match self {
32            Self::Debug => true,
33            Self::Info => !level_upper.contains("DEBUG"),
34            Self::Warn => level_upper.contains("WARN") || level_upper.contains("ERROR"),
35            Self::Error => level_upper.contains("ERROR"),
36        }
37    }
38}
39
40#[derive(Debug, Args)]
41pub struct LogsArgs {
42    #[arg(help = "MCP server name (optional - shows all MCP logs if not specified)")]
43    pub server: Option<String>,
44
45    #[arg(
46        long,
47        short = 'n',
48        visible_alias = "tail",
49        default_value = "50",
50        help = "Number of lines to show"
51    )]
52    pub lines: usize,
53
54    #[arg(long, short, help = "Follow log output continuously (disk only)")]
55    pub follow: bool,
56
57    #[arg(long, help = "Force reading from disk files instead of database")]
58    pub disk: bool,
59
60    #[arg(long, help = "Custom logs directory path")]
61    pub logs_dir: Option<String>,
62
63    #[arg(
64        long,
65        value_enum,
66        help = "Filter by log level: debug, info, warn, error"
67    )]
68    pub level: Option<LogLevel>,
69}
70
71fn get_default_logs_dir() -> PathBuf {
72    ProfileBootstrap::get()
73        .ok()
74        .and_then(|p| {
75            AppPaths::from_profile(
76                &p.paths,
77                p.path_resolution(),
78                ServicesRootBootstrap::get().map(|r| r.path.as_path()),
79            )
80            .ok()
81        })
82        .map_or_else(|| PathBuf::from("/var/log"), |paths| paths.system().logs())
83}
84
85pub(super) async fn execute(
86    args: LogsArgs,
87    prompter: &dyn Prompter,
88    config: &CliConfig,
89) -> Result<CommandOutput> {
90    let logs_path = args
91        .logs_dir
92        .as_ref()
93        .map_or_else(get_default_logs_dir, PathBuf::from);
94
95    if args.follow {
96        return execute_follow_mode(&args, prompter, config, &logs_path);
97    }
98
99    if args.disk {
100        return execute_disk_mode(&args, prompter, config, &logs_path);
101    }
102
103    match execute_db_mode(&args, config).await {
104        Ok(result) => Ok(result),
105        Err(e) => {
106            tracing::debug!(error = %e, "DB log query failed, falling back to disk");
107            execute_disk_mode(&args, prompter, config, &logs_path)
108        },
109    }
110}