Skip to main content

server_watchdog/application/handler/
command.rs

1mod alarm;
2
3use std::error::Error;
4use anyhow::anyhow;
5use async_trait::async_trait;
6use log::{debug, trace};
7use crate::application::handler::command::alarm::AlarmCommand;
8use crate::application::handler::command::Command::{Alarm, EventList, HealthCheck, HealthCheckAll, Help, Logs, Nothing};
9use crate::application::handler::GeneralHandler;
10use crate::domain::client::Message;
11
12pub struct CommandMeta {
13    pub name: &'static str,
14    pub description: &'static str,
15    pub usage: &'static str,
16    pub examples: &'static [&'static str],
17}
18
19impl CommandMeta {
20    pub const fn new(
21        name: &'static str,
22        description: &'static str,
23        usage: &'static str,
24        examples: &'static [&'static str],
25    ) -> Self {
26        Self { name, description, usage, examples }
27    }
28}
29
30#[async_trait]
31pub trait Run: Send + Sync {
32    async fn run(&self, handler: &mut GeneralHandler, id: String, message: &Message) -> Result<String, Box<dyn Error + Send + Sync>>;
33}
34
35#[derive(Debug)]
36pub enum Command {
37    Logs(String, i32),
38    HealthCheckAll,
39    HealthCheck(String),
40    Nothing,
41    Alarm(AlarmCommand),
42    EventList,
43    Help(Option<String>),
44}
45
46impl Command {
47    const HELP: CommandMeta = CommandMeta::new(
48        "/help",
49        "커맨드 사용법을 표시합니다",
50        "/help [command]",
51        &["/help", "/help logs"],
52    );
53    const LOGS: CommandMeta = CommandMeta::new(
54        "/logs",
55        "서버의 최근 로그를 가져옵니다",
56        "/logs <server_name> <lines>",
57        &["/logs main 100", "/logs api 50"],
58    );
59    const HEALTH: CommandMeta = CommandMeta::new(
60        "/health",
61        "서버 헬스체크 결과를 확인합니다",
62        "/health [server_name]",
63        &["/health", "/health main"],
64    );
65    const ALARM: CommandMeta = CommandMeta::new(
66        "/alarm",
67        "이벤트 알람을 구독 또는 해제합니다",
68        "/alarm <add|remove|list> [event_name]",
69        &["/alarm list", "/alarm add cpu-high", "/alarm remove cpu-high"],
70    );
71    const EVENT: CommandMeta = CommandMeta::new(
72        "/event",
73        "설정된 이벤트 목록을 표시합니다",
74        "/event [list]",
75        &["/event", "/event list"],
76    );
77
78    pub fn meta(&self) -> Option<&'static CommandMeta> {
79        match self {
80            Help(_)                            => Some(&Self::HELP),
81            Logs(_, _)                         => Some(&Self::LOGS),
82            HealthCheck(_) | HealthCheckAll    => Some(&Self::HEALTH),
83            Alarm(_)                           => Some(&Self::ALARM),
84            EventList                          => Some(&Self::EVENT),
85            Nothing                            => None,
86        }
87    }
88
89    pub fn all_docs() -> &'static [&'static CommandMeta] {
90        &[&Self::HELP, &Self::LOGS, &Self::HEALTH, &Self::ALARM, &Self::EVENT]
91    }
92
93    pub fn render_help_all() -> String {
94        let list = Self::all_docs()
95            .iter()
96            .map(|m| format!("{} — {}", m.name, m.description))
97            .collect::<Vec<_>>()
98            .join("\n");
99        format!("사용 가능한 커맨드:\n\n{list}\n\n자세한 사용법: /help <command>\n예시: /help logs")
100    }
101
102    pub fn render_help_one(name: &str) -> Option<String> {
103        let meta = Self::all_docs()
104            .iter()
105            .find(|m| m.name.trim_start_matches('/') == name)?;
106        let examples = meta.examples
107            .iter()
108            .map(|e| format!("  {e}"))
109            .collect::<Vec<_>>()
110            .join("\n");
111        Some(format!(
112            "{} — {}\n\n사용법:\n  {}\n\n예시:\n{examples}",
113            meta.name, meta.description, meta.usage
114        ))
115    }
116}
117
118#[async_trait]
119impl Run for Command {
120    async fn run(&self, handler: &mut GeneralHandler, id: String, message: &Message) -> Result<String, Box<dyn Error + Send + Sync>> {
121        match self {
122            Command::Logs(name, n) => {
123                handler.server_manager.logs(name.as_str(), *n).await
124                    .ok_or_else(|| anyhow!("Logs are not available."))
125                    .map_err(Into::into)
126            },
127            Command::HealthCheck(name) => {
128                let health = handler.server_manager.healthcheck(name.as_str()).await;
129                let response = format!("===\nServer: {name}\n Health: {health}");
130                Ok(response)
131            },
132            Command::HealthCheckAll => {
133                let response = handler.server_manager.healthcheck_all()
134                    .await
135                    .iter().map(|result|{format!("===\nServer: {}\nHealth: {}", result.0, result.1)})
136                    .collect::<Vec<String>>()
137                    .join("\n");
138                Ok(response)
139            },
140            Command::Alarm(command) => {
141                command.run(handler, id, message).await
142            },
143            Command::EventList => {
144                let events = handler.event_config_use_case.list_event().await?;
145                let event_names = events.iter().map(|e| e.name.clone()).collect::<Vec<String>>().join("\n");
146                Ok(format!("Available events:\n{}", event_names))
147            },
148            Command::Help(name) => match name {
149                None => Ok(Command::render_help_all()),
150                Some(name) => Command::render_help_one(name)
151                    .ok_or_else(|| anyhow!("알 수 없는 커맨드: /{name}\n\n{}", Command::render_help_all()).into()),
152            },
153            Command::Nothing => Ok(Command::render_help_all()),
154        }
155    }
156}
157
158impl Command {
159    pub fn parse(text: &str) -> Self {
160        trace!("Command::parse(text: {})", &text);
161        let command = match text.split_whitespace().collect::<Vec<_>>()[..] {
162            ["/help"] => Help(None),
163            ["/help", name] => Help(Some(name.trim_start_matches('/').to_string())),
164            ["/health", name] => HealthCheck(name.to_string()),
165            ["/health"] => HealthCheckAll,
166            ["/logs", name, n] => {
167                match n.parse() {
168                    Ok(n) => Logs(name.to_string(), n),
169                    Err(_) => Nothing
170                }
171            },
172            ["/alarm", "add", name] => {
173                Alarm(AlarmCommand::Add(String::from(name)))
174            },
175            ["/alarm", "remove", name] => {
176                Alarm(AlarmCommand::Remove(String::from(name)))
177            },
178            ["/alarm", "list"] => {
179                Alarm(AlarmCommand::List)
180            },
181            ["/alarm"] => {
182                Alarm(AlarmCommand::List)
183            },
184            ["/event", "list"] => EventList,
185            ["/event"] => EventList,
186            _ => Nothing
187        };
188        debug!("parsed command: {:?}", &command);
189        command
190    }
191}