Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions development/tools/sam-cop/channels.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import abc
from typing import List

import httpx


class Channel(abc.ABC):
"""Delivery backend. Subclasses self-register; declare required_env and build_channels() picks them up."""

name = "channel"
required_env: tuple = ()
registry: list = []

def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
Channel.registry.append(cls)

@classmethod
def from_env(cls, env):
# __init__ parameters must line up with required_env order.
return cls(*(env[var] for var in cls.required_env))

@abc.abstractmethod
async def send(self, message: str) -> None: ...


class SlackChannel(Channel):
name = "slack"
required_env = ("SLACK_WEBHOOK_URL",)

def __init__(self, webhook_url: str):
self.webhook_url = webhook_url

async def send(self, message: str) -> None:
async with httpx.AsyncClient() as http_client:
response = await http_client.post(self.webhook_url, json={"text": message}, timeout=10.0)
response.raise_for_status()
Comment on lines +31 to +37

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Creating a new httpx.AsyncClient on every single alert delivery is inefficient as it incurs connection establishment and TLS handshake overhead for each message. We should instantiate a single httpx.AsyncClient in the channel's __init__ method and reuse it across all send calls.

Suggested change
def __init__(self, webhook_url: str):
self.webhook_url = webhook_url
async def send(self, message: str) -> None:
async with httpx.AsyncClient() as http_client:
response = await http_client.post(self.webhook_url, json={"text": message}, timeout=10.0)
response.raise_for_status()
def __init__(self, webhook_url: str):
self.webhook_url = webhook_url
self.http_client = httpx.AsyncClient()
async def send(self, message: str) -> None:
response = await self.http_client.post(self.webhook_url, json={"text": message}, timeout=10.0)
response.raise_for_status()



class TelegramChannel(Channel):
name = "telegram"
required_env = ("TELEGRAM_BOT_TOKEN", "TELEGRAM_CHAT_ID")

def __init__(self, bot_token: str, chat_id: str):
self.bot_token = bot_token
self.chat_id = chat_id

async def send(self, message: str) -> None:
url = f"https://api.telegram.org/bot{self.bot_token}/sendMessage"
async with httpx.AsyncClient() as http_client:
response = await http_client.post(url, json={"chat_id": self.chat_id, "text": message}, timeout=10.0)
response.raise_for_status()
Comment on lines +44 to +52

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Creating a new httpx.AsyncClient on every single alert delivery is inefficient as it incurs connection establishment and TLS handshake overhead for each message. We should instantiate a single httpx.AsyncClient in the channel's __init__ method and reuse it across all send calls.

Suggested change
def __init__(self, bot_token: str, chat_id: str):
self.bot_token = bot_token
self.chat_id = chat_id
async def send(self, message: str) -> None:
url = f"https://api.telegram.org/bot{self.bot_token}/sendMessage"
async with httpx.AsyncClient() as http_client:
response = await http_client.post(url, json={"chat_id": self.chat_id, "text": message}, timeout=10.0)
response.raise_for_status()
def __init__(self, bot_token: str, chat_id: str):
self.bot_token = bot_token
self.chat_id = chat_id
self.http_client = httpx.AsyncClient()
async def send(self, message: str) -> None:
url = f"https://api.telegram.org/bot{self.bot_token}/sendMessage"
response = await self.http_client.post(url, json={"chat_id": self.chat_id, "text": message}, timeout=10.0)
response.raise_for_status()



class StdoutChannel(Channel):
name = "stdout"

async def send(self, message: str) -> None:
print(f"[ALERT] {message}", flush=True)


def build_channels(env) -> List[Channel]:
# Stdout is always on so the log shows every alert, delivered or not.
channels = [cls.from_env(env) for cls in Channel.registry
if cls.required_env and all(env.get(var) for var in cls.required_env)]
channels.append(StdoutChannel())
return channels
Loading
Loading