diff --git a/signal-gateway/src/alertmanager.rs b/signal-gateway/src/alertmanager.rs index e82eb9e..c0ef2a8 100644 --- a/signal-gateway/src/alertmanager.rs +++ b/signal-gateway/src/alertmanager.rs @@ -1,18 +1,24 @@ -//! Schema for the alertmanager http POST requests that are sent to us +//! Schema for the Alertmanager HTTP POST webhook requests. +//! +//! See use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; use url::Url; +/// Alert status indicating whether an alert is firing or resolved. #[derive(Clone, Copy, Debug, Deserialize, Serialize)] #[serde(rename_all = "snake_case")] pub enum Status { + /// The alert condition is no longer true. Resolved, + /// The alert condition is currently true. Firing, } impl Status { + /// Returns an emoji symbol representing the status. pub fn symbol(&self) -> char { match self { Status::Firing => '🔴', @@ -21,41 +27,59 @@ impl Status { } } +/// The top-level webhook payload from Alertmanager. #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct AlertPost { - // should be 4.0 + /// Alertmanager API version (should be "4"). pub version: String, + /// Key used to group alerts together. pub group_key: String, + /// Overall status of the alert group. pub status: Status, + /// Name of the receiver that triggered this webhook. pub receiver: String, + /// Labels used to group the alerts. #[serde(default)] pub group_labels: BTreeMap, + /// Labels common to all alerts in this group. #[serde(default)] pub common_labels: BTreeMap, + /// Annotations common to all alerts in this group. #[serde(default)] pub common_annotations: BTreeMap, + /// URL of the Alertmanager instance. #[serde(alias = "externalURL")] pub external_url: String, + /// List of alerts in this notification. pub alerts: Vec, } +/// An individual alert from Alertmanager. #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct Alert { + /// Status of this specific alert. pub status: Status, + /// Labels identifying the alert. #[serde(default)] pub labels: BTreeMap, + /// Annotations providing additional information. #[serde(default)] pub annotations: BTreeMap, + /// Time when the alert started firing. pub starts_at: DateTime, + /// Time when the alert was resolved (zero value if still firing). pub ends_at: DateTime, + /// URL to the Prometheus graph for this alert's expression. #[serde(alias = "generatorURL")] pub generator_url: String, + /// Unique identifier for this alert. pub fingerprint: String, } impl Alert { + /// Parse the PromQL expression from the generator URL. pub fn parse_expr_from_generator_url(&self) -> Result { let url = Url::parse(&self.generator_url).map_err(|err| err.to_string())?; diff --git a/signal-gateway/src/gateway/mod.rs b/signal-gateway/src/gateway/mod.rs index a7b3c5c..a2f3f4b 100644 --- a/signal-gateway/src/gateway/mod.rs +++ b/signal-gateway/src/gateway/mod.rs @@ -1,3 +1,5 @@ +//! Gateway for bridging alerts and logs with Signal messenger. + #[cfg(unix)] use crate::jsonrpc::connect_ipc; use crate::{ @@ -38,28 +40,32 @@ use log_handler::{LogHandler, LogHandlerConfig}; mod rate_limiter; use rate_limiter::{MultiRateLimiter, RateThreshold, SourceLocationRateLimiter}; +/// Configuration for the gateway. #[derive(Conf, Debug)] #[cfg_attr(unix, conf(one_of_fields(signal_cli_tcp_addr, signal_cli_socket_path)))] #[cfg_attr(not(unix), conf(one_of_fields(signal_cli_tcp_addr)))] pub struct GatewayConfig { - /// TCP address of signal-cli JSON-RPC server + /// TCP address of signal-cli JSON-RPC server. #[conf(long, env)] pub signal_cli_tcp_addr: Option, - /// Unix socket path of signal-cli JSON-RPC server + /// Unix socket path of signal-cli JSON-RPC server. #[cfg(unix)] #[conf(long, env)] pub signal_cli_socket_path: Option, + /// The phone number or UUID of the Signal account to use. #[conf(long, env)] pub signal_account: String, /// Admin UUIDs mapped to their safety numbers (can be empty). - /// Example: {"uuid1": ["12345...", "67890..."], "uuid2": []} + /// Example: `{"uuid1": ["12345...", "67890..."], "uuid2": []}` #[conf(long, env, value_parser = serde_json::from_str)] pub admin_safety_numbers: HashMap>, - /// If set, alerts are sent to this group instead of individual admins + /// If set, alerts are sent to this group instead of individual admins. #[conf(long, env)] pub alert_group_id: Option, + /// Prometheus server configuration for querying metrics. #[conf(flatten)] pub prometheus: Option, + /// Log handler configuration for processing log messages. #[conf(flatten)] pub log_handler: LogHandlerConfig, } @@ -187,6 +193,7 @@ pub struct Gateway { } impl Gateway { + /// Create a new gateway with the given configuration. pub async fn new( config: GatewayConfig, token: CancellationToken, @@ -212,6 +219,7 @@ impl Gateway { } } + /// Run the gateway main loop, reconnecting to signal-cli on errors. pub async fn run(&self) { loop { if self.token.is_cancelled() { @@ -483,7 +491,7 @@ impl Gateway { } } - // Handler function that processes incoming http requests (push's from alertmanager expected) + /// Handle an incoming HTTP request (e.g., webhooks from Alertmanager). pub async fn handle_http_request(&self, req: Request) -> Result, String> where B: Body + Send, @@ -812,6 +820,7 @@ impl Gateway { Ok(text) } + /// Process an incoming log message, buffering it and potentially triggering an alert. pub async fn handle_log_message(&self, log_msg: impl Into) { let log_msg = log_msg.into(); let origin = Origin::from(&log_msg); diff --git a/signal-gateway/src/lib.rs b/signal-gateway/src/lib.rs index 92d83a3..0c4e6b2 100644 --- a/signal-gateway/src/lib.rs +++ b/signal-gateway/src/lib.rs @@ -1,3 +1,10 @@ +//! Signal Gateway library for bridging alertmanager and logging with Signal messenger. +//! +//! This crate provides the core functionality for receiving alerts and log messages +//! and forwarding them to Signal messenger via signal-cli. + +#![deny(missing_docs)] + pub mod alertmanager; pub mod gateway; pub mod message_handler; diff --git a/signal-gateway/src/log_message.rs b/signal-gateway/src/log_message.rs index 97667fc..f3f3345 100644 --- a/signal-gateway/src/log_message.rs +++ b/signal-gateway/src/log_message.rs @@ -1,18 +1,31 @@ -//! Log message schema used by this crate +//! Log message schema and types. use serde::Deserialize; +/// Log severity level, following syslog conventions. +/// +/// Lower values indicate higher severity. The ordering allows comparisons +/// like `level <= Level::ERROR` to match ERROR, CRITICAL, ALERT, and EMERGENCY. #[non_exhaustive] #[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)] pub enum Level { + /// System is unusable. EMERGENCY = 0, + /// Action must be taken immediately. ALERT = 1, + /// Critical conditions. CRITICAL = 2, + /// Error conditions. ERROR = 3, + /// Warning conditions. WARNING = 4, + /// Normal but significant condition. NOTICE = 5, + /// Informational messages. INFO = 6, + /// Debug-level messages. DEBUG = 7, + /// Trace-level messages (more verbose than debug). TRACE = 8, } @@ -33,21 +46,32 @@ impl Level { } } +/// A structured log message. #[non_exhaustive] #[derive(Clone, Debug)] pub struct LogMessage { + /// Severity level of the message. pub level: Level, + /// Unix timestamp in seconds. pub timestamp: Option, + /// Nanosecond component of the timestamp. pub timestamp_nanos: u32, + /// Hostname where the log originated. pub hostname: Option>, + /// Application name that generated the log. pub appname: Option>, + /// The log message text. pub msg: Box, + /// Module path (e.g., `myapp::server::handler`). pub module_path: Option>, + /// Source file path. pub file: Option>, + /// Line number in the source file. pub line: Option>, } impl LogMessage { + /// Create a builder for constructing a log message. pub fn builder(level: Level, msg: impl Into>) -> LogMessageBuilder { LogMessageBuilder { level, @@ -63,6 +87,7 @@ impl LogMessage { } } +/// Builder for constructing [`LogMessage`] instances. #[derive(Clone, Debug)] pub struct LogMessageBuilder { level: Level, @@ -77,41 +102,49 @@ pub struct LogMessageBuilder { } impl LogMessageBuilder { + /// Set the Unix timestamp in seconds. pub fn timestamp(mut self, ts: i64) -> Self { self.timestamp = Some(ts); self } + /// Set the nanosecond component of the timestamp. pub fn timestamp_nanos(mut self, nanos: u32) -> Self { self.timestamp_nanos = nanos; self } + /// Set the hostname. pub fn hostname(mut self, hostname: impl Into>) -> Self { self.hostname = Some(hostname.into()); self } + /// Set the application name. pub fn appname(mut self, appname: impl Into>) -> Self { self.appname = Some(appname.into()); self } + /// Set the module path. pub fn module_path(mut self, module_path: impl Into>) -> Self { self.module_path = Some(module_path.into()); self } + /// Set the source file path. pub fn file(mut self, file: impl Into>) -> Self { self.file = Some(file.into()); self } + /// Set the line number. pub fn line(mut self, line: impl Into>) -> Self { self.line = Some(line.into()); self } + /// Build the log message. pub fn build(self) -> LogMessage { LogMessage { level: self.level, @@ -134,10 +167,13 @@ impl From for LogMessage { } /// Identifies the source of log messages (app name + host). +/// /// Used to separate log buffers and rate limiters per source. #[derive(Clone, Debug, Default, PartialEq, Eq, Hash)] pub struct Origin { + /// Application name. pub app: Box, + /// Hostname. pub host: Box, } @@ -174,15 +210,21 @@ impl Origin { } } -/// Filter criteria for matching log messages +/// Filter criteria for matching log messages. +/// +/// All non-empty fields must match for the filter to pass. #[derive(Clone, Debug, Default, Deserialize)] pub struct LogFilter { + /// If non-empty, the message must contain this substring. #[serde(default)] pub msg_contains: String, + /// If non-empty, the module path must equal this value exactly. #[serde(default)] pub module_equals: String, + /// If non-empty, the file path must equal this value exactly. #[serde(default)] pub file_equals: String, + /// If non-empty, the line number must equal this value exactly. #[serde(default)] pub line_equals: String, }