prepare config for healthchecks integration

This commit is contained in:
Alex Tau 2025-08-13 16:59:23 +03:00
parent 4558cf9e6f
commit c01ab8303c
8 changed files with 89 additions and 34 deletions

View file

@ -31,9 +31,15 @@ package:
serviceConfigFile = json.generate "config.json" { serviceConfigFile = json.generate "config.json" {
enabled_check_sets = cfg.enabledCheckSets; enabled_check_sets = cfg.enabledCheckSets;
log_level = cfg.logLevel; log_level = cfg.logLevel;
telegram = with cfg.telegram; { alert_channels = {
telegram = with cfg.alertChannels.telegram; if enable then
{
creds_secret_path = credsSecretPath; creds_secret_path = credsSecretPath;
room_id = roomId; room_id = roomId;
} else null;
healthchecks = with cfg.alertChannels.healthchecks; if enable then {
pinging_keys_secret_path = pingingKeysSecretPath;
} else null;
}; };
checks = { checks = {
temp.sensors = lib.mapAttrs (_: sensorCfg: { temp.sensors = lib.mapAttrs (_: sensorCfg: {

View file

@ -50,16 +50,34 @@ in
* vulnix -- periodically scans system for known CVEs, alerts if any are found (NixOS only)''; * vulnix -- periodically scans system for known CVEs, alerts if any are found (NixOS only)'';
}; };
alertChannels = {
telegram = { telegram = {
enable = lib.mkEnableOption "Telegram notification channel";
credsSecretPath = lib.mkOption { credsSecretPath = lib.mkOption {
type = lib.types.str; type = lib.types.str;
default = "";
description = "Path to a file containing Telegram api_id, api_hash, and bot token, separated by the `,` character."; description = "Path to a file containing Telegram api_id, api_hash, and bot token, separated by the `,` character.";
}; };
roomId = lib.mkOption { roomId = lib.mkOption {
type = lib.types.int; type = lib.types.int;
default = 0;
description = "ID of chat where to send alerts."; 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.'';
};
};
};
checks = { checks = {
temp = { temp = {

View file

@ -49,11 +49,6 @@ async def async_main():
logging.basicConfig(level=config.log_level) 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 check_sets = config_enums.CheckSet
checker_sets: dict[config_enums.CheckSet, list[Coroutine | BaseChecker]] = { 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] checker_sets[check_sets.REMIND][0].check_args = [checkers]
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) command_manager = CommandHandlerManager(checkers)
await command_manager.attach_handlers(tg_client) 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) signal.signal(signal.SIGTERM, stop_gracefully)

View file

@ -13,7 +13,7 @@ from .sender import format_message
def admin_chat_only( def admin_chat_only(
handler: Callable[[events.NewMessage.Event], Awaitable[None]], handler: Callable[[events.NewMessage.Event], Awaitable[None]],
) -> 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: async def safe_handler(event: events.NewMessage.Event) -> None:
if event.chat_id == admin_room_id: if event.chat_id == admin_room_id:

View file

@ -1,5 +1,3 @@
from dataclasses import dataclass
from telethon import TelegramClient from telethon import TelegramClient
from telethon.sessions import MemorySession from telethon.sessions import MemorySession
@ -10,7 +8,7 @@ from .enum import SEVERITY_TO_EMOJI, AlertType, Severity
async def get_client() -> TelegramClient: async def get_client() -> TelegramClient:
config = cvars.config.get() 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 = await TelegramClient(MemorySession(), api_id, api_hash, connection_retries=None).start(bot_token=bot_token)
client.parse_mode = "html" client.parse_mode = "html"
return client return client
@ -38,13 +36,17 @@ async def send_alert(alert: Alert, note: str = "") -> None:
raise NotImplementedError # TODO raise NotImplementedError # TODO
else: else:
... # temp_client = False ... # temp_client = False
room_id = cvars.config.get().telegram.room_id if client is not None:
room_id = cvars.config.get().alert_channels.telegram.room_id
message = format_message(alert, note) message = format_message(alert, note)
await client.send_message(entity=room_id, message=message) await client.send_message(entity=room_id, message=message)
# if temp_client: # if temp_client:
# await client.close() # 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: async def send_start_alert() -> None:
config = cvars.config.get() config = cvars.config.get()
await send_alert( await send_alert(

View file

@ -5,6 +5,7 @@ from typing import Optional
from alt_utils import NestedDeserializableDataclass from alt_utils import NestedDeserializableDataclass
from . import enums from . import enums
from .alert_channels import AlertChannelsConfig
from .checks.cpu import CpuCheckConfig from .checks.cpu import CpuCheckConfig
from .checks.net import NetCheckConfig from .checks.net import NetCheckConfig
from .checks.ram import RamCheckConfig from .checks.ram import RamCheckConfig
@ -21,16 +22,10 @@ class ChecksConfig(NestedDeserializableDataclass):
net: NetCheckConfig = field(default_factory=NetCheckConfig) net: NetCheckConfig = field(default_factory=NetCheckConfig)
@dataclass
class TelegramConfig:
creds: str
room_id: int
@dataclass @dataclass
class Config(NestedDeserializableDataclass): class Config(NestedDeserializableDataclass):
checks: ChecksConfig checks: ChecksConfig
telegram: TelegramConfig alert_channels: AlertChannelsConfig
enabled_check_sets: list[enums.CheckSet] = field(default_factory=list) enabled_check_sets: list[enums.CheckSet] = field(default_factory=list)
log_level: enums.LogLevelName = enums.LogLevelName.INFO log_level: enums.LogLevelName = enums.LogLevelName.INFO

View file

@ -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

View file

@ -1,4 +1,5 @@
from contextvars import ContextVar from contextvars import ContextVar
from typing import Optional
from telethon import TelegramClient from telethon import TelegramClient
@ -7,5 +8,5 @@ from lego_monitoring.alerting.current import CurrentAlerts
from ..config import Config from ..config import Config
config: ContextVar[Config] = ContextVar("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=[]) current_alerts: ContextVar[list[CurrentAlerts]] = ContextVar("current_alerts", default=[])