stackpatrol 0.2.5

Single-binary Rust CLI that monitors a server and reports to the StackPatrol control plane.
mod buffer;
mod client;
mod commands;
mod config;
mod daemon;
mod probe_docker;
mod probe_ports;
mod probe_resources;
mod probe_systemd;
mod style;

use std::path::PathBuf;

use clap::{Parser, Subcommand};

#[derive(Parser)]
#[command(
    name = "stackpatrol",
    version,
    about = "Server monitoring CLI for StackPatrol"
)]
struct Cli {
    #[command(subcommand)]
    command: Command,
}

#[derive(Subcommand)]
enum Command {
    /// Interactive setup — prompts for the server token from @StackPatrolBot and writes the config.
    Init,
    /// Long-running agent — emits a heartbeat every `heartbeat_interval_secs` and probe events.
    Daemon,
    /// One-shot snapshot of current server state.
    Status,
    /// Live tail of events being reported to the control plane.
    Watch,
    /// Send a test alert to verify token + connectivity.
    AlertTest,
    /// Manage what this server reports on (docker projects, systemd units, TCP ports).
    #[command(subcommand)]
    Probe(ProbeAction),
}

#[derive(Subcommand)]
enum ProbeAction {
    /// List configured probes.
    Ls,
    /// Add a probe target.
    #[command(subcommand)]
    Add(ProbeTarget),
    /// Remove a probe target.
    #[command(subcommand)]
    Rm(ProbeTarget),
}

#[derive(Subcommand)]
enum ProbeTarget {
    /// A docker compose project — directory containing compose.yaml / docker-compose.yml.
    Docker {
        /// Path to the compose project directory.
        path: PathBuf,
    },
    /// A systemd unit (e.g. nginx.service — `.service` is appended if no suffix is given).
    Systemd {
        /// Unit name.
        unit: String,
    },
    /// A TCP target (e.g. localhost:5432, 1.1.1.1:443).
    Port {
        /// host:port to connect-probe each tick.
        target: String,
    },
}

fn main() -> anyhow::Result<()> {
    tracing_subscriber::fmt()
        .with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
        .init();

    let cli = Cli::parse();
    match cli.command {
        Command::Init => commands::init(),
        Command::Daemon => runtime()?.block_on(daemon::run()),
        Command::AlertTest => runtime()?.block_on(commands::alert_test()),
        Command::Status => runtime()?.block_on(commands::status()),
        Command::Watch => todo!("M5: tail events being reported"),
        Command::Probe(ProbeAction::Ls) => commands::probe_ls(),
        Command::Probe(ProbeAction::Add(target)) => match target {
            ProbeTarget::Docker { path } => commands::probe_add_docker(path),
            ProbeTarget::Systemd { unit } => commands::probe_add_systemd(unit),
            ProbeTarget::Port { target } => commands::probe_add_port(target),
        },
        Command::Probe(ProbeAction::Rm(target)) => match target {
            ProbeTarget::Docker { path } => commands::probe_rm_docker(path),
            ProbeTarget::Systemd { unit } => commands::probe_rm_systemd(unit),
            ProbeTarget::Port { target } => commands::probe_rm_port(target),
        },
    }
}

fn runtime() -> anyhow::Result<tokio::runtime::Runtime> {
    Ok(tokio::runtime::Builder::new_current_thread()
        .enable_all()
        .build()?)
}