add deny missing docs

This commit is contained in:
Chris Beck
2025-12-05 11:51:15 -07:00
parent b3a0894221
commit 5b98e74f92
4 changed files with 91 additions and 9 deletions
+26 -2
View File
@@ -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 <https://prometheus.io/docs/alerting/latest/configuration/#webhook_config>
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::BTreeMap; use std::collections::BTreeMap;
use url::Url; use url::Url;
/// Alert status indicating whether an alert is firing or resolved.
#[derive(Clone, Copy, Debug, Deserialize, Serialize)] #[derive(Clone, Copy, Debug, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum Status { pub enum Status {
/// The alert condition is no longer true.
Resolved, Resolved,
/// The alert condition is currently true.
Firing, Firing,
} }
impl Status { impl Status {
/// Returns an emoji symbol representing the status.
pub fn symbol(&self) -> char { pub fn symbol(&self) -> char {
match self { match self {
Status::Firing => '🔴', Status::Firing => '🔴',
@@ -21,41 +27,59 @@ impl Status {
} }
} }
/// The top-level webhook payload from Alertmanager.
#[derive(Clone, Debug, Deserialize, Serialize)] #[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct AlertPost { pub struct AlertPost {
// should be 4.0 /// Alertmanager API version (should be "4").
pub version: String, pub version: String,
/// Key used to group alerts together.
pub group_key: String, pub group_key: String,
/// Overall status of the alert group.
pub status: Status, pub status: Status,
/// Name of the receiver that triggered this webhook.
pub receiver: String, pub receiver: String,
/// Labels used to group the alerts.
#[serde(default)] #[serde(default)]
pub group_labels: BTreeMap<String, String>, pub group_labels: BTreeMap<String, String>,
/// Labels common to all alerts in this group.
#[serde(default)] #[serde(default)]
pub common_labels: BTreeMap<String, String>, pub common_labels: BTreeMap<String, String>,
/// Annotations common to all alerts in this group.
#[serde(default)] #[serde(default)]
pub common_annotations: BTreeMap<String, String>, pub common_annotations: BTreeMap<String, String>,
/// URL of the Alertmanager instance.
#[serde(alias = "externalURL")] #[serde(alias = "externalURL")]
pub external_url: String, pub external_url: String,
/// List of alerts in this notification.
pub alerts: Vec<Alert>, pub alerts: Vec<Alert>,
} }
/// An individual alert from Alertmanager.
#[derive(Clone, Debug, Deserialize, Serialize)] #[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct Alert { pub struct Alert {
/// Status of this specific alert.
pub status: Status, pub status: Status,
/// Labels identifying the alert.
#[serde(default)] #[serde(default)]
pub labels: BTreeMap<String, String>, pub labels: BTreeMap<String, String>,
/// Annotations providing additional information.
#[serde(default)] #[serde(default)]
pub annotations: BTreeMap<String, String>, pub annotations: BTreeMap<String, String>,
/// Time when the alert started firing.
pub starts_at: DateTime<Utc>, pub starts_at: DateTime<Utc>,
/// Time when the alert was resolved (zero value if still firing).
pub ends_at: DateTime<Utc>, pub ends_at: DateTime<Utc>,
/// URL to the Prometheus graph for this alert's expression.
#[serde(alias = "generatorURL")] #[serde(alias = "generatorURL")]
pub generator_url: String, pub generator_url: String,
/// Unique identifier for this alert.
pub fingerprint: String, pub fingerprint: String,
} }
impl Alert { impl Alert {
/// Parse the PromQL expression from the generator URL.
pub fn parse_expr_from_generator_url(&self) -> Result<String, String> { pub fn parse_expr_from_generator_url(&self) -> Result<String, String> {
let url = Url::parse(&self.generator_url).map_err(|err| err.to_string())?; let url = Url::parse(&self.generator_url).map_err(|err| err.to_string())?;
+14 -5
View File
@@ -1,3 +1,5 @@
//! Gateway for bridging alerts and logs with Signal messenger.
#[cfg(unix)] #[cfg(unix)]
use crate::jsonrpc::connect_ipc; use crate::jsonrpc::connect_ipc;
use crate::{ use crate::{
@@ -38,28 +40,32 @@ use log_handler::{LogHandler, LogHandlerConfig};
mod rate_limiter; mod rate_limiter;
use rate_limiter::{MultiRateLimiter, RateThreshold, SourceLocationRateLimiter}; use rate_limiter::{MultiRateLimiter, RateThreshold, SourceLocationRateLimiter};
/// Configuration for the gateway.
#[derive(Conf, Debug)] #[derive(Conf, Debug)]
#[cfg_attr(unix, conf(one_of_fields(signal_cli_tcp_addr, signal_cli_socket_path)))] #[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)))] #[cfg_attr(not(unix), conf(one_of_fields(signal_cli_tcp_addr)))]
pub struct GatewayConfig { pub struct GatewayConfig {
/// TCP address of signal-cli JSON-RPC server /// TCP address of signal-cli JSON-RPC server.
#[conf(long, env)] #[conf(long, env)]
pub signal_cli_tcp_addr: Option<SocketAddr>, pub signal_cli_tcp_addr: Option<SocketAddr>,
/// Unix socket path of signal-cli JSON-RPC server /// Unix socket path of signal-cli JSON-RPC server.
#[cfg(unix)] #[cfg(unix)]
#[conf(long, env)] #[conf(long, env)]
pub signal_cli_socket_path: Option<PathBuf>, pub signal_cli_socket_path: Option<PathBuf>,
/// The phone number or UUID of the Signal account to use.
#[conf(long, env)] #[conf(long, env)]
pub signal_account: String, pub signal_account: String,
/// Admin UUIDs mapped to their safety numbers (can be empty). /// 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)] #[conf(long, env, value_parser = serde_json::from_str)]
pub admin_safety_numbers: HashMap<String, Vec<String>>, pub admin_safety_numbers: HashMap<String, Vec<String>>,
/// 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)] #[conf(long, env)]
pub alert_group_id: Option<String>, pub alert_group_id: Option<String>,
/// Prometheus server configuration for querying metrics.
#[conf(flatten)] #[conf(flatten)]
pub prometheus: Option<PrometheusConfig>, pub prometheus: Option<PrometheusConfig>,
/// Log handler configuration for processing log messages.
#[conf(flatten)] #[conf(flatten)]
pub log_handler: LogHandlerConfig, pub log_handler: LogHandlerConfig,
} }
@@ -187,6 +193,7 @@ pub struct Gateway {
} }
impl Gateway { impl Gateway {
/// Create a new gateway with the given configuration.
pub async fn new( pub async fn new(
config: GatewayConfig, config: GatewayConfig,
token: CancellationToken, token: CancellationToken,
@@ -212,6 +219,7 @@ impl Gateway {
} }
} }
/// Run the gateway main loop, reconnecting to signal-cli on errors.
pub async fn run(&self) { pub async fn run(&self) {
loop { loop {
if self.token.is_cancelled() { 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<B>(&self, req: Request<B>) -> Result<Response<String>, String> pub async fn handle_http_request<B>(&self, req: Request<B>) -> Result<Response<String>, String>
where where
B: Body + Send, B: Body + Send,
@@ -812,6 +820,7 @@ impl Gateway {
Ok(text) 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<LogMessage>) { pub async fn handle_log_message(&self, log_msg: impl Into<LogMessage>) {
let log_msg = log_msg.into(); let log_msg = log_msg.into();
let origin = Origin::from(&log_msg); let origin = Origin::from(&log_msg);
+7
View File
@@ -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 alertmanager;
pub mod gateway; pub mod gateway;
pub mod message_handler; pub mod message_handler;
+44 -2
View File
@@ -1,18 +1,31 @@
//! Log message schema used by this crate //! Log message schema and types.
use serde::Deserialize; 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] #[non_exhaustive]
#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)] #[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
pub enum Level { pub enum Level {
/// System is unusable.
EMERGENCY = 0, EMERGENCY = 0,
/// Action must be taken immediately.
ALERT = 1, ALERT = 1,
/// Critical conditions.
CRITICAL = 2, CRITICAL = 2,
/// Error conditions.
ERROR = 3, ERROR = 3,
/// Warning conditions.
WARNING = 4, WARNING = 4,
/// Normal but significant condition.
NOTICE = 5, NOTICE = 5,
/// Informational messages.
INFO = 6, INFO = 6,
/// Debug-level messages.
DEBUG = 7, DEBUG = 7,
/// Trace-level messages (more verbose than debug).
TRACE = 8, TRACE = 8,
} }
@@ -33,21 +46,32 @@ impl Level {
} }
} }
/// A structured log message.
#[non_exhaustive] #[non_exhaustive]
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct LogMessage { pub struct LogMessage {
/// Severity level of the message.
pub level: Level, pub level: Level,
/// Unix timestamp in seconds.
pub timestamp: Option<i64>, pub timestamp: Option<i64>,
/// Nanosecond component of the timestamp.
pub timestamp_nanos: u32, pub timestamp_nanos: u32,
/// Hostname where the log originated.
pub hostname: Option<Box<str>>, pub hostname: Option<Box<str>>,
/// Application name that generated the log.
pub appname: Option<Box<str>>, pub appname: Option<Box<str>>,
/// The log message text.
pub msg: Box<str>, pub msg: Box<str>,
/// Module path (e.g., `myapp::server::handler`).
pub module_path: Option<Box<str>>, pub module_path: Option<Box<str>>,
/// Source file path.
pub file: Option<Box<str>>, pub file: Option<Box<str>>,
/// Line number in the source file.
pub line: Option<Box<str>>, pub line: Option<Box<str>>,
} }
impl LogMessage { impl LogMessage {
/// Create a builder for constructing a log message.
pub fn builder(level: Level, msg: impl Into<Box<str>>) -> LogMessageBuilder { pub fn builder(level: Level, msg: impl Into<Box<str>>) -> LogMessageBuilder {
LogMessageBuilder { LogMessageBuilder {
level, level,
@@ -63,6 +87,7 @@ impl LogMessage {
} }
} }
/// Builder for constructing [`LogMessage`] instances.
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct LogMessageBuilder { pub struct LogMessageBuilder {
level: Level, level: Level,
@@ -77,41 +102,49 @@ pub struct LogMessageBuilder {
} }
impl LogMessageBuilder { impl LogMessageBuilder {
/// Set the Unix timestamp in seconds.
pub fn timestamp(mut self, ts: i64) -> Self { pub fn timestamp(mut self, ts: i64) -> Self {
self.timestamp = Some(ts); self.timestamp = Some(ts);
self self
} }
/// Set the nanosecond component of the timestamp.
pub fn timestamp_nanos(mut self, nanos: u32) -> Self { pub fn timestamp_nanos(mut self, nanos: u32) -> Self {
self.timestamp_nanos = nanos; self.timestamp_nanos = nanos;
self self
} }
/// Set the hostname.
pub fn hostname(mut self, hostname: impl Into<Box<str>>) -> Self { pub fn hostname(mut self, hostname: impl Into<Box<str>>) -> Self {
self.hostname = Some(hostname.into()); self.hostname = Some(hostname.into());
self self
} }
/// Set the application name.
pub fn appname(mut self, appname: impl Into<Box<str>>) -> Self { pub fn appname(mut self, appname: impl Into<Box<str>>) -> Self {
self.appname = Some(appname.into()); self.appname = Some(appname.into());
self self
} }
/// Set the module path.
pub fn module_path(mut self, module_path: impl Into<Box<str>>) -> Self { pub fn module_path(mut self, module_path: impl Into<Box<str>>) -> Self {
self.module_path = Some(module_path.into()); self.module_path = Some(module_path.into());
self self
} }
/// Set the source file path.
pub fn file(mut self, file: impl Into<Box<str>>) -> Self { pub fn file(mut self, file: impl Into<Box<str>>) -> Self {
self.file = Some(file.into()); self.file = Some(file.into());
self self
} }
/// Set the line number.
pub fn line(mut self, line: impl Into<Box<str>>) -> Self { pub fn line(mut self, line: impl Into<Box<str>>) -> Self {
self.line = Some(line.into()); self.line = Some(line.into());
self self
} }
/// Build the log message.
pub fn build(self) -> LogMessage { pub fn build(self) -> LogMessage {
LogMessage { LogMessage {
level: self.level, level: self.level,
@@ -134,10 +167,13 @@ impl From<LogMessageBuilder> for LogMessage {
} }
/// Identifies the source of log messages (app name + host). /// Identifies the source of log messages (app name + host).
///
/// Used to separate log buffers and rate limiters per source. /// Used to separate log buffers and rate limiters per source.
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)] #[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
pub struct Origin { pub struct Origin {
/// Application name.
pub app: Box<str>, pub app: Box<str>,
/// Hostname.
pub host: Box<str>, pub host: Box<str>,
} }
@@ -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)] #[derive(Clone, Debug, Default, Deserialize)]
pub struct LogFilter { pub struct LogFilter {
/// If non-empty, the message must contain this substring.
#[serde(default)] #[serde(default)]
pub msg_contains: String, pub msg_contains: String,
/// If non-empty, the module path must equal this value exactly.
#[serde(default)] #[serde(default)]
pub module_equals: String, pub module_equals: String,
/// If non-empty, the file path must equal this value exactly.
#[serde(default)] #[serde(default)]
pub file_equals: String, pub file_equals: String,
/// If non-empty, the line number must equal this value exactly.
#[serde(default)] #[serde(default)]
pub line_equals: String, pub line_equals: String,
} }