remotesysmonitor/main.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208
//! # Remote System Monitoring Application
//!
//! `RemoteSysMonitor` is a comprehensive tool designed for monitoring remote servers. It executes specified commands on remote servers via SSH and can forward the results to Slack for notifications. The application supports a variety of checks, such as ping, system load, temperature readings, and the execution of custom scripts. Configuration is managed through a YAML file, allowing for easy setup and customization.
//!
//! ## Usage
//!
//! To utilize `RemoteSysMonitor`, follow these steps:
//! 1. Prepare a `config.yaml` file according to your monitoring requirements, detailing the servers to be monitored along with the specific checks for each.
//! 2. Set the `SLACK_HOOK_URL` environment variable to your Slack webhook URL to enable Slack notifications.
//! 3. Launch the application, providing the path to your configuration file as the argument.
//!
//! Example command to run the application:
//! ```no_run
//! cargo run -- /path/to/your/config.yaml
//! ```
//!
//! ## Key Features
//!
//! - **Server Monitoring**: Facilitates monitoring of multiple servers through SSH.
//! - **Diverse Checks**: Supports various checks, including ping, system load, temperature readings, and execution of custom scripts.
//! - **Slack Integration**: Enables direct reporting of monitoring results to a specified Slack channel for real-time alerts.
//!
//! ## Configuration Guide
//!
//! `RemoteSysMonitor` relies on a YAML file for configuration, allowing you to specify the servers to monitor and the checks to perform on each. Below is an example of how the configuration file might look:
//!
//! ```yaml
//! servers:
//! - name: "Server 1"
//! host: "192.168.1.1"
//! user: "user"
//! private_key: "/path/to/private/key"
//! checks:
//! ping: ["example.com", "google.com"]
//! load: 5
//! temperature: "/sys/class/thermal/thermal_zone0/temp"
//! custom_command: "custom_script.sh"
//! ```
//!
//! ## Contributing to RemoteSysMonitor
//!
//! Contributions to `RemoteSysMonitor` are highly encouraged and appreciated. Whether it's through submitting pull requests with code enhancements, bug fixes, or feature additions, or by reporting issues and suggesting improvements, your input helps make `RemoteSysMonitor` better for everyone.
//!
//! Please feel free to submit pull requests or open issues on the project's GitHub repository for any bugs you encounter or enhancements you believe are worth adding.
pub mod checks;
pub mod config;
pub mod slack;
pub mod ssh;
pub mod utils;
use crate::config::Check;
use clap::Parser;
use log::info;
use std::{env, vec};
#[derive(Parser)]
#[command(author, version, about)]
struct Args {
config: String,
#[clap(short, long)]
/// Post a check to Slack even if there is no ❌ in the checks
full: bool,
#[clap(short, long)]
/// Print the output of the checks in stdout
print: bool,
}
/// Entry point of the monitoring application.
///
/// This function performs the following steps:
/// 1. Reads the configuration file path from the command line arguments.
/// 2. Loads the configuration from the specified path.
/// 3. Retrieves the Slack webhook URL from an environment variable.
/// 4. Iterates over each server defined in the configuration, creating SSH sessions and executing specified checks.
/// 5. Collects the results of all checks into a payload.
/// 6. Posts the payload to a Slack channel using the webhook URL.
///
/// # Command Line Arguments
///
/// The application expects a single command line argument specifying the path to the configuration file.
///
/// # Environment Variables
///
/// - `SLACK_HOOK_URL`: The webhook URL for posting messages to Slack. This must be set before running the application.
///
/// # Errors
///
/// This function returns an error if:
/// - The configuration file path is not provided as a command line argument.
/// - The configuration file cannot be loaded.
/// - The `SLACK_HOOK_URL` environment variable is not set.
/// - An SSH session cannot be created for any of the servers.
/// - An unknown check type is encountered in the configuration.
///
/// # Exit Codes
///
/// The application exits with code 1 if:
/// - The configuration file path is not provided.
/// - The `SLACK_HOOK_URL` environment variable is not set.
///
/// # Examples
///
/// Run the application by specifying the path to the configuration file:
/// ```sh
/// cargo run -- /path/to/config.yaml
/// ```
///
/// Ensure the `SLACK_HOOK_URL` environment variable is set before running:
/// ```sh
/// export SLACK_HOOK_URL='https://hooks.slack.com/services/...'
/// ```
///
/// # Note
///
/// The function aggregates all check results into a single message payload, which is then posted to Slack.
/// It sorts checks for each server alphabetically by their names before execution, ensuring a consistent
/// order in the Slack message. Each check's result is separated by new lines in the final Slack message.
fn main() -> Result<(), Box<dyn std::error::Error>> {
env_logger::init();
let cli = Args::parse();
info!("Loading configuration from {}", cli.config.as_str());
let config = config::load_config(cli.config.as_str())?;
let slack_hook_url = match env::var("SLACK_HOOK_URL") {
Ok(url) => url,
Err(_) => {
eprintln!("SLACK_HOOK_URL environment variable not set");
std::process::exit(1);
}
};
let mut payload: Vec<String> = vec![];
for server in config.servers {
// Add the server name to the payload
payload.push(format!("🖥️ {} (`{}`)", server.name, server.host));
let sess = match ssh::create_session(
server.host.as_str(),
server.port,
server.user.as_str(),
server.private_key.as_str(),
) {
Ok(sess) => sess,
Err(e) => {
eprintln!("Failed to create SSH session for {}: {}", server.name, e);
let error_msg = format!("❌ could not start SSH session with {}", server.name);
payload.push(error_msg);
continue;
}
};
if let Some(checks) = server.checks {
let mut sorted_checks: Vec<(&String, &Check)> = checks.iter().collect();
sorted_checks.sort_by(|a, b| a.0.cmp(b.0));
for (_check_name, check_details) in sorted_checks {
let result = match check_details {
Check::Ping { url } => {
checks::ping(&("https://".to_owned() + server.host.as_str()), url)
}
Check::Temperature { sensor } => checks::temperature(&sess, sensor.as_str()),
Check::Load {
interval,
warning_cutoff,
} => checks::load(&sess, server.name.as_str(), *interval, *warning_cutoff),
Check::NumberOfSubfolders { path, max_folders } => {
checks::number_of_folders(&sess, server.name.as_str(), path, max_folders)
}
Check::CustomCommand { command } => checks::custom_command(&sess, command),
Check::ListOldDirectories { loc, cutoff } => {
checks::list_old_directories(&sess, loc, *cutoff)
}
_ => return Err("Unknown check".into()),
};
payload.push(result);
}
}
// Add a separator between servers, if it has been defined
match config.general {
Some(ref general) => {
payload.push(general.separator.repeat(10));
}
None => payload.push("".to_string()),
}
}
let flatten: Vec<String> = payload
.iter()
.flat_map(|p| p.split('\n').map(|s| s.to_string()))
.collect();
if cli.print {
println!("{}", flatten.join("\n"));
}
if cli.full || flatten.iter().any(|s| s.contains('❌')) {
slack::post_to_slack(slack_hook_url.as_str(), flatten.join("\n").as_str());
} else {
println!("No ❌ found in checks, not posting to Slack. Use --full to post anyway and --help for more options.");
}
Ok(())
}