From c01ab8303ce8ce027d03764af48dee23b83fa8a9 Mon Sep 17 00:00:00 2001 From: Alex Tau Date: Wed, 13 Aug 2025 16:59:23 +0300 Subject: [PATCH] prepare config for healthchecks integration --- modules/default.nix | 12 ++++++-- modules/options.nix | 32 +++++++++++++++----- src/lego_monitoring/__init__.py | 19 +++++++----- src/lego_monitoring/alerting/commands.py | 2 +- src/lego_monitoring/alerting/sender.py | 18 ++++++----- src/lego_monitoring/config/__init__.py | 9 ++---- src/lego_monitoring/config/alert_channels.py | 28 +++++++++++++++++ src/lego_monitoring/core/cvars.py | 3 +- 8 files changed, 89 insertions(+), 34 deletions(-) create mode 100644 src/lego_monitoring/config/alert_channels.py diff --git a/modules/default.nix b/modules/default.nix index 4dd838d..28124c1 100644 --- a/modules/default.nix +++ b/modules/default.nix @@ -31,9 +31,15 @@ package: serviceConfigFile = json.generate "config.json" { enabled_check_sets = cfg.enabledCheckSets; log_level = cfg.logLevel; - telegram = with cfg.telegram; { - creds_secret_path = credsSecretPath; - room_id = roomId; + alert_channels = { + telegram = with cfg.alertChannels.telegram; if enable then + { + creds_secret_path = credsSecretPath; + room_id = roomId; + } else null; + healthchecks = with cfg.alertChannels.healthchecks; if enable then { + pinging_keys_secret_path = pingingKeysSecretPath; + } else null; }; checks = { temp.sensors = lib.mapAttrs (_: sensorCfg: { diff --git a/modules/options.nix b/modules/options.nix index ad92929..63aa610 100644 --- a/modules/options.nix +++ b/modules/options.nix @@ -50,14 +50,32 @@ in * vulnix -- periodically scans system for known CVEs, alerts if any are found (NixOS only)''; }; - telegram = { - credsSecretPath = lib.mkOption { - type = lib.types.str; - description = "Path to a file containing Telegram api_id, api_hash, and bot token, separated by the `,` character."; + alertChannels = { + telegram = { + enable = lib.mkEnableOption "Telegram notification channel"; + credsSecretPath = lib.mkOption { + type = lib.types.str; + default = ""; + description = "Path to a file containing Telegram api_id, api_hash, and bot token, separated by the `,` character."; + }; + roomId = lib.mkOption { + type = lib.types.int; + default = 0; + description = "ID of chat where to send alerts."; + }; }; - roomId = lib.mkOption { - type = lib.types.int; - description = "ID of chat where to send alerts."; + healthchecks = { + enable = lib.mkEnableOption "[Healthchecks](https://healthchecks.io) notification channel"; + pingingKeysSecretPath = lib.mkOption { + type = lib.types.str; + default = ""; + description = '' + Path to a file containing the pinging keys in a `slug:key` format, one on each line (ex: `lego-cpu:aaaaaaaaaaaaaaaaaaaaaa`). + Specify `default` as the slug to use this key for check types that don't have a key explicitly assigned to them. + + If you are unsure of the exact slug a check will generate, it is recommended to try it out with the default key first, before + assigning a specific one.''; + }; }; }; diff --git a/src/lego_monitoring/__init__.py b/src/lego_monitoring/__init__.py index 655e102..ba33d0b 100644 --- a/src/lego_monitoring/__init__.py +++ b/src/lego_monitoring/__init__.py @@ -49,11 +49,6 @@ async def async_main(): logging.basicConfig(level=config.log_level) - tg_client = await sender.get_client() - - cvars.tg_client.set(tg_client) - my_username = (await tg_client.get_me()).username - logging.info(f"Logged in as @{my_username}") check_sets = config_enums.CheckSet checker_sets: dict[config_enums.CheckSet, list[Coroutine | BaseChecker]] = { @@ -94,8 +89,18 @@ async def async_main(): checker_sets[check_sets.REMIND][0].check_args = [checkers] - command_manager = CommandHandlerManager(checkers) - await command_manager.attach_handlers(tg_client) + if config.alert_channels.telegram is not None: + tg_client = await sender.get_client() + my_username = (await tg_client.get_me()).username + logging.info(f"Logged in as @{my_username}") + + command_manager = CommandHandlerManager(checkers) + await command_manager.attach_handlers(tg_client) + else: + logging.info("Telegram integration is disabled") + tg_client = None + + cvars.tg_client.set(tg_client) signal.signal(signal.SIGTERM, stop_gracefully) diff --git a/src/lego_monitoring/alerting/commands.py b/src/lego_monitoring/alerting/commands.py index a07bb34..96491a0 100644 --- a/src/lego_monitoring/alerting/commands.py +++ b/src/lego_monitoring/alerting/commands.py @@ -13,7 +13,7 @@ from .sender import format_message def admin_chat_only( handler: Callable[[events.NewMessage.Event], Awaitable[None]], ) -> Callable[[events.NewMessage.Event], Awaitable[None]]: - admin_room_id = cvars.config.get().telegram.room_id + admin_room_id = cvars.config.get().alert_channels.telegram.room_id async def safe_handler(event: events.NewMessage.Event) -> None: if event.chat_id == admin_room_id: diff --git a/src/lego_monitoring/alerting/sender.py b/src/lego_monitoring/alerting/sender.py index 53f09ed..2ab4933 100644 --- a/src/lego_monitoring/alerting/sender.py +++ b/src/lego_monitoring/alerting/sender.py @@ -1,5 +1,3 @@ -from dataclasses import dataclass - from telethon import TelegramClient from telethon.sessions import MemorySession @@ -10,7 +8,7 @@ from .enum import SEVERITY_TO_EMOJI, AlertType, Severity async def get_client() -> TelegramClient: config = cvars.config.get() - api_id, api_hash, bot_token = config.telegram.creds.split(",") + api_id, api_hash, bot_token = config.alert_channels.telegram.creds.split(",") client = await TelegramClient(MemorySession(), api_id, api_hash, connection_retries=None).start(bot_token=bot_token) client.parse_mode = "html" return client @@ -38,13 +36,17 @@ async def send_alert(alert: Alert, note: str = "") -> None: raise NotImplementedError # TODO else: ... # temp_client = False - room_id = cvars.config.get().telegram.room_id - message = format_message(alert, note) - await client.send_message(entity=room_id, message=message) - # if temp_client: - # await client.close() + if client is not None: + room_id = cvars.config.get().alert_channels.telegram.room_id + message = format_message(alert, note) + await client.send_message(entity=room_id, message=message) + # if temp_client: + # await client.close() + # TODO ping healthchecks if enabled +# TODO service itself has to be monitored like everything else - with regular pinging - if we're +# using healthchecks async def send_start_alert() -> None: config = cvars.config.get() await send_alert( diff --git a/src/lego_monitoring/config/__init__.py b/src/lego_monitoring/config/__init__.py index 4354df8..11e4c70 100644 --- a/src/lego_monitoring/config/__init__.py +++ b/src/lego_monitoring/config/__init__.py @@ -5,6 +5,7 @@ from typing import Optional from alt_utils import NestedDeserializableDataclass from . import enums +from .alert_channels import AlertChannelsConfig from .checks.cpu import CpuCheckConfig from .checks.net import NetCheckConfig from .checks.ram import RamCheckConfig @@ -21,16 +22,10 @@ class ChecksConfig(NestedDeserializableDataclass): net: NetCheckConfig = field(default_factory=NetCheckConfig) -@dataclass -class TelegramConfig: - creds: str - room_id: int - - @dataclass class Config(NestedDeserializableDataclass): checks: ChecksConfig - telegram: TelegramConfig + alert_channels: AlertChannelsConfig enabled_check_sets: list[enums.CheckSet] = field(default_factory=list) log_level: enums.LogLevelName = enums.LogLevelName.INFO diff --git a/src/lego_monitoring/config/alert_channels.py b/src/lego_monitoring/config/alert_channels.py new file mode 100644 index 0000000..22e99de --- /dev/null +++ b/src/lego_monitoring/config/alert_channels.py @@ -0,0 +1,28 @@ +from dataclasses import dataclass, field +from typing import Optional + +from alt_utils import NestedDeserializableDataclass + + +@dataclass +class TelegramConfig: + creds: str + room_id: int + + +@dataclass +class HealthchecksConfig: + pinging_keys: str | dict[str, str] + + def __post_init__(self): + lines = self.pinging_keys.split() + self.pinging_keys = {} + for l in lines: + slug, key = l.split(":") + self.pinging_keys[slug] = key + + +@dataclass +class AlertChannelsConfig(NestedDeserializableDataclass): + telegram: Optional[TelegramConfig] = None + healthchecks: Optional[HealthchecksConfig] = None diff --git a/src/lego_monitoring/core/cvars.py b/src/lego_monitoring/core/cvars.py index 185514f..89c68f4 100644 --- a/src/lego_monitoring/core/cvars.py +++ b/src/lego_monitoring/core/cvars.py @@ -1,4 +1,5 @@ from contextvars import ContextVar +from typing import Optional from telethon import TelegramClient @@ -7,5 +8,5 @@ from lego_monitoring.alerting.current import CurrentAlerts from ..config import Config config: ContextVar[Config] = ContextVar("config") -tg_client: ContextVar[TelegramClient] = ContextVar("tg_client") +tg_client: ContextVar[Optional[TelegramClient]] = ContextVar("tg_client") current_alerts: ContextVar[list[CurrentAlerts]] = ContextVar("current_alerts", default=[])