Files
tracer-cloud--opensre/integrations/telegram/alarms.py
T
wehub-resource-sync 4b6817381b
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
CI / coverage-report (push) Has been cancelled
CI / test-kubernetes (push) Has been cancelled
CI / should-run-thorough (push) Has been cancelled
CI / test-thorough (cloudwatch-demo) (push) Has been cancelled
CI / test-thorough (flink-ecs) (push) Has been cancelled
CI / test-thorough (upstream-lambda) (push) Has been cancelled
CI / test-thorough (prefect-ecs-fargate) (push) Has been cancelled
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Has been cancelled
Benchmark image — build + push to ECR (any adapter) / build + push (push) Has been cancelled
CI / quality (ubuntu-latest) (push) Has been cancelled
CI / test (tools-runtime) (push) Has been cancelled
CI / test (e2e-general) (push) Has been cancelled
CI / test (cli-runtime) (push) Has been cancelled
CI / test (e2e-provider-and-openclaw) (push) Has been cancelled
CI / test (integrations-and-misc) (push) Has been cancelled
Release / verify (push) Has been cancelled
Release / build-python-dist (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Has been cancelled
Release / publish-release (push) Has been cancelled
Release / publish-main-release (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Has been cancelled
Release / prepare (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:10:45 +08:00

110 lines
3.8 KiB
Python

"""Telegram alarm dispatcher with per-key cooldown.
Shared by features that need throttled Telegram alerts (watchdog thresholds,
Hermes incident sinks, the Telegram send-message tool). The dispatcher takes
a string key (e.g. a threshold name or incident fingerprint) and suppresses
repeat deliveries for the same key within the cooldown window.
Credential resolution lives in
:mod:`integrations.telegram.credentials`; raw transport in
:mod:`integrations.telegram.delivery`. This module owns only the
throttling + dispatch policy.
"""
from __future__ import annotations
import logging
import threading
import time
from integrations.telegram.credentials import TelegramCredentials
from integrations.telegram.delivery import (
post_telegram_message,
truncate_for_telegram_html,
)
from platform.common.truncation import truncate
from platform.notifications.limits import MAX_MESSAGE_SIZE
logger = logging.getLogger(__name__)
_DEFAULT_COOLDOWN_SECONDS = 300.0
_TELEGRAM_MESSAGE_LIMIT = MAX_MESSAGE_SIZE
class AlarmDispatcher:
"""Dispatch Telegram alarms with per-key cooldown."""
def __init__(
self,
creds: TelegramCredentials,
*,
cooldown_seconds: float = _DEFAULT_COOLDOWN_SECONDS,
parse_mode: str = "",
) -> None:
self._creds = creds
self._cooldown_seconds = cooldown_seconds
self._parse_mode = parse_mode
self._last_dispatched: dict[str, float] = {}
self._lock = threading.Lock()
def dispatch(self, threshold_name: str, message: str) -> bool:
"""Send to Telegram unless this threshold is in cooldown."""
now = self._now()
# Reserve the cooldown slot under the lock BEFORE the network call so
# a concurrent dispatch on the same threshold sees the reservation and
# is suppressed. Without this, two threads could both pass the check
# (state of last_dispatched at "check" time != "use" time, classic
# TOCTOU) and both send.
with self._lock:
last = self._last_dispatched.get(threshold_name)
if last is not None and (now - last) < self._cooldown_seconds:
logger.debug(
"alarm suppressed by cooldown: name=%s remaining=%.1fs",
threshold_name,
self._cooldown_seconds - (now - last),
)
return False
self._last_dispatched[threshold_name] = now
if self._parse_mode.upper() == "HTML":
text = truncate_for_telegram_html(message, _TELEGRAM_MESSAGE_LIMIT, suffix="…")
else:
text = truncate(message, _TELEGRAM_MESSAGE_LIMIT, suffix="…")
# The cooldown slot was reserved before this network call (see lock
# block above). If ``post_telegram_message`` returns ``ok=False`` OR
# raises, the slot stays armed for the cooldown window and the next
# caller for the same key is silently suppressed — emit the same
# warning in both paths so operators see the original failure
# instead of only the suppression debug line.
try:
ok, error, _ = post_telegram_message(
chat_id=self._creds.chat_id,
text=text,
bot_token=self._creds.bot_token,
parse_mode=self._parse_mode,
)
except Exception as exc:
logger.warning(
"alarm delivery raised and cooldown remains armed: name=%s error=%s",
threshold_name,
exc,
exc_info=True,
)
return False
if ok:
return True
logger.warning(
"alarm delivery failed and cooldown remains armed: name=%s error=%s",
threshold_name,
error,
)
return False
@staticmethod
def _now() -> float:
return time.monotonic()