server-watchdog 0.1.2

A server monitoring and remote control tool via messenger.
Documentation
mod alarm;

use std::error::Error;
use anyhow::anyhow;
use async_trait::async_trait;
use log::{debug, trace};
use crate::application::handler::command::alarm::AlarmCommand;
use crate::application::handler::command::Command::{Alarm, EventList, HealthCheck, HealthCheckAll, Help, Logs, Nothing};
use crate::application::handler::GeneralHandler;
use crate::domain::client::Message;

pub struct CommandMeta {
    pub name: &'static str,
    pub description: &'static str,
    pub usage: &'static str,
    pub examples: &'static [&'static str],
}

impl CommandMeta {
    pub const fn new(
        name: &'static str,
        description: &'static str,
        usage: &'static str,
        examples: &'static [&'static str],
    ) -> Self {
        Self { name, description, usage, examples }
    }
}

#[async_trait]
pub trait Run: Send + Sync {
    async fn run(&self, handler: &mut GeneralHandler, id: String, message: &Message) -> Result<String, Box<dyn Error + Send + Sync>>;
}

#[derive(Debug)]
pub enum Command {
    Logs(String, i32),
    HealthCheckAll,
    HealthCheck(String),
    Nothing,
    Alarm(AlarmCommand),
    EventList,
    Help(Option<String>),
}

impl Command {
    const HELP: CommandMeta = CommandMeta::new(
        "/help",
        "커맨드 사용법을 표시합니다",
        "/help [command]",
        &["/help", "/help logs"],
    );
    const LOGS: CommandMeta = CommandMeta::new(
        "/logs",
        "서버의 최근 로그를 가져옵니다",
        "/logs <server_name> <lines>",
        &["/logs main 100", "/logs api 50"],
    );
    const HEALTH: CommandMeta = CommandMeta::new(
        "/health",
        "서버 헬스체크 결과를 확인합니다",
        "/health [server_name]",
        &["/health", "/health main"],
    );
    const ALARM: CommandMeta = CommandMeta::new(
        "/alarm",
        "이벤트 알람을 구독 또는 해제합니다",
        "/alarm <add|remove|list> [event_name]",
        &["/alarm list", "/alarm add cpu-high", "/alarm remove cpu-high"],
    );
    const EVENT: CommandMeta = CommandMeta::new(
        "/event",
        "설정된 이벤트 목록을 표시합니다",
        "/event [list]",
        &["/event", "/event list"],
    );

    pub fn meta(&self) -> Option<&'static CommandMeta> {
        match self {
            Help(_)                            => Some(&Self::HELP),
            Logs(_, _)                         => Some(&Self::LOGS),
            HealthCheck(_) | HealthCheckAll    => Some(&Self::HEALTH),
            Alarm(_)                           => Some(&Self::ALARM),
            EventList                          => Some(&Self::EVENT),
            Nothing                            => None,
        }
    }

    pub fn all_docs() -> &'static [&'static CommandMeta] {
        &[&Self::HELP, &Self::LOGS, &Self::HEALTH, &Self::ALARM, &Self::EVENT]
    }

    pub fn render_help_all() -> String {
        let list = Self::all_docs()
            .iter()
            .map(|m| format!("{}{}", m.name, m.description))
            .collect::<Vec<_>>()
            .join("\n");
        format!("사용 가능한 커맨드:\n\n{list}\n\n자세한 사용법: /help <command>\n예시: /help logs")
    }

    pub fn render_help_one(name: &str) -> Option<String> {
        let meta = Self::all_docs()
            .iter()
            .find(|m| m.name.trim_start_matches('/') == name)?;
        let examples = meta.examples
            .iter()
            .map(|e| format!("  {e}"))
            .collect::<Vec<_>>()
            .join("\n");
        Some(format!(
            "{}{}\n\n사용법:\n  {}\n\n예시:\n{examples}",
            meta.name, meta.description, meta.usage
        ))
    }
}

#[async_trait]
impl Run for Command {
    async fn run(&self, handler: &mut GeneralHandler, id: String, message: &Message) -> Result<String, Box<dyn Error + Send + Sync>> {
        match self {
            Command::Logs(name, n) => {
                handler.server_manager.logs(name.as_str(), *n).await
                    .ok_or_else(|| anyhow!("Logs are not available."))
                    .map_err(Into::into)
            },
            Command::HealthCheck(name) => {
                let health = handler.server_manager.healthcheck(name.as_str()).await;
                let response = format!("===\nServer: {name}\n Health: {health}");
                Ok(response)
            },
            Command::HealthCheckAll => {
                let response = handler.server_manager.healthcheck_all()
                    .await
                    .iter().map(|result|{format!("===\nServer: {}\nHealth: {}", result.0, result.1)})
                    .collect::<Vec<String>>()
                    .join("\n");
                Ok(response)
            },
            Command::Alarm(command) => {
                command.run(handler, id, message).await
            },
            Command::EventList => {
                let events = handler.event_config_use_case.list_event().await?;
                let event_names = events.iter().map(|e| e.name.clone()).collect::<Vec<String>>().join("\n");
                Ok(format!("Available events:\n{}", event_names))
            },
            Command::Help(name) => match name {
                None => Ok(Command::render_help_all()),
                Some(name) => Command::render_help_one(name)
                    .ok_or_else(|| anyhow!("알 수 없는 커맨드: /{name}\n\n{}", Command::render_help_all()).into()),
            },
            Command::Nothing => Ok(Command::render_help_all()),
        }
    }
}

impl Command {
    pub fn parse(text: &str) -> Self {
        trace!("Command::parse(text: {})", &text);
        let command = match text.split_whitespace().collect::<Vec<_>>()[..] {
            ["/help"] => Help(None),
            ["/help", name] => Help(Some(name.trim_start_matches('/').to_string())),
            ["/health", name] => HealthCheck(name.to_string()),
            ["/health"] => HealthCheckAll,
            ["/logs", name, n] => {
                match n.parse() {
                    Ok(n) => Logs(name.to_string(), n),
                    Err(_) => Nothing
                }
            },
            ["/alarm", "add", name] => {
                Alarm(AlarmCommand::Add(String::from(name)))
            },
            ["/alarm", "remove", name] => {
                Alarm(AlarmCommand::Remove(String::from(name)))
            },
            ["/alarm", "list"] => {
                Alarm(AlarmCommand::List)
            },
            ["/alarm"] => {
                Alarm(AlarmCommand::List)
            },
            ["/event", "list"] => EventList,
            ["/event"] => EventList,
            _ => Nothing
        };
        debug!("parsed command: {:?}", &command);
        command
    }
}