From 31bb1a5bd42f4ea5c461447e045567a03d240cf4 Mon Sep 17 00:00:00 2001 From: Srujan Gurram Date: Fri, 10 Apr 2026 21:59:27 +0530 Subject: [PATCH 1/2] docs: add design spec for Slack and Telegram adapters Covers adapter-owned routing (Approach A), message formatting, rate limits, breaking changes to RoutingConfig/FormattedAlert, and migration path from global routing to per-adapter config. Co-Authored-By: Claude Opus 4.6 (1M context) --- ...26-04-10-slack-telegram-adapters-design.md | 274 ++++++++++++++++++ 1 file changed, 274 insertions(+) create mode 100644 docs/specs/2026-04-10-slack-telegram-adapters-design.md diff --git a/docs/specs/2026-04-10-slack-telegram-adapters-design.md b/docs/specs/2026-04-10-slack-telegram-adapters-design.md new file mode 100644 index 0000000..b1d4044 --- /dev/null +++ b/docs/specs/2026-04-10-slack-telegram-adapters-design.md @@ -0,0 +1,274 @@ +# Slack & Telegram Adapters Design + +## Goal + +Add Slack and Telegram adapters to `@iqai/alert-logger`, following the same patterns as the existing Discord adapter. Each adapter owns its own routing (per-level channels/topics, per-tag overrides, mentions). + +## Breaking Changes + +This design moves routing from the global `AlertLoggerConfig.routing` into individual adapter constructors. The global `RoutingConfig` type, the `Router` class, and the `webhookUrl`/`pings` fields on `FormattedAlert` are removed. + +**Migration path:** Move `routing.channels`, `routing.tags`, and `routing.pings` into `DiscordAdapterOptions`. + +Before: +```ts +AlertLogger.init({ + adapters: [new DiscordAdapter({ webhookUrl: '...' })], + routing: { + channels: { critical: 'https://discord.com/.../critical' }, + pings: { critical: ['<@&role>'] }, + }, +}) +``` + +After: +```ts +AlertLogger.init({ + adapters: [ + new DiscordAdapter({ + webhookUrl: '...', + channels: { critical: 'https://discord.com/.../critical' }, + mentions: { critical: ['<@&role>'] }, + }), + ], +}) +``` + +## Adapter Configs + +### SlackAdapter + +```ts +interface SlackAdapterOptions { + /** Default Incoming Webhook URL */ + webhookUrl: string + /** Override webhook URL per alert level */ + channels?: Partial> + /** Override webhook URL per tag */ + tags?: Record + /** Slack user/group mentions per level, e.g. ["<@U0123>", ""] */ + mentions?: Partial> +} +``` + +- Uses Slack Incoming Webhooks (no bot token, no OAuth). +- `channels` and `tags` values are webhook URLs (each Slack webhook maps to one channel). +- Rate limit: 1 request/sec per webhook. + +### TelegramAdapter + +```ts +interface TelegramAdapterOptions { + /** Telegram Bot API token */ + botToken: string + /** Target chat ID (group or channel) */ + chatId: string + /** Map alert level to a forum topic (message_thread_id) */ + topics?: Partial> + /** Map tag to a forum topic */ + tags?: Record + /** Telegram @username mentions per level */ + mentions?: Partial> +} +``` + +- Uses the Telegram Bot HTTP API (`sendMessage` endpoint). +- Single group chat with forum topics for per-level routing. +- Rate limit: 20 messages/60s per chat. + +### DiscordAdapter (updated) + +```ts +interface DiscordAdapterOptions { + /** Default webhook URL */ + webhookUrl: string + /** Override webhook URL per alert level */ + channels?: Partial> + /** Override webhook URL per tag */ + tags?: Record + /** Discord user/role mentions per level, e.g. ["<@123>", "<@&456>"] */ + mentions?: Partial> +} +``` + +- Gains `channels`, `tags`, `mentions` (previously in global `routing` config). +- Existing `webhookUrl` remains the default destination. + +## Internal Routing + +Each adapter implements a private `resolve(level, tags?)` method that returns the destination + mentions: + +- **Discord/Slack:** Returns `{ url: string; mentions: string[] }` — checks tags first, then level, then falls back to the default webhook URL. +- **Telegram:** Returns `{ topicId?: number; mentions: string[] }` — checks tags first, then level. No topic = posts to general chat. + +This replaces the current `Router` class and the `webhookUrl`/`pings` fields on `FormattedAlert`. + +## Message Formatting + +Each adapter has its own `formatter.ts` that handles the 4 aggregation phases (onset, ramp, sustained, resolution). + +### Slack Formatter + +Uses Block Kit with attachments for color coding: + +- **Color bar:** `attachment.color` hex — blue `#3498db` (info), yellow `#f39c12` (warning), red `#e74c3c` (critical), green `#2ecc71` (resolution). +- **Title:** Header block — `[PROD] [CRITICAL] Alert title`. +- **Body:** Section block with `mrkdwn` — alert message, stack traces in triple-backtick code blocks. +- **Fields:** Section fields as `mrkdwn` key/value pairs with `inline: true` equivalent (short fields). +- **Footer:** Context block — service name + timestamp. +- **Mentions:** Plain text block above the attachment (like Discord's `content` field). + +Phase-specific formatting mirrors the Discord formatter (onset shows full detail, ramp/sustained show counts, resolution shows totals). + +### Telegram Formatter + +Uses HTML parse mode (`parse_mode: "HTML"`): + +- **Severity indicator:** Emoji prefix — blue circle (info), warning triangle (warning), red circle (critical), green checkmark (resolution). +- **Title:** `[PROD] [CRITICAL] Alert title`. +- **Body:** Alert message as plain text. Stack traces in `` blocks. +- **Fields:** Key-value list — `key: value`. +- **Footer:** `Service: name | timestamp`. +- **Mentions:** `@username` inline in the message. +- **Limit:** 4096 characters per message — truncate with ellipsis. + +## Rate Limits & Retry + +| Adapter | maxPerWindow | windowMs | Retry Strategy | +|----------|-------------|----------|---------------------------------------------| +| Discord | 30 | 60000 | Retry on 429, `Retry-After` header (secs) | +| Slack | 1 | 1000 | Retry on 429, `Retry-After` header (secs) | +| Telegram | 20 | 60000 | Retry on 429, `retry_after` in JSON body | + +All three retry up to 2 times on 429 responses, reading the service-specific retry-after value. + +## Core Type Changes + +### FormattedAlert + +Remove `webhookUrl` and `pings`: + +```ts +interface FormattedAlert extends Alert { + aggregation: AggregationMeta + environmentBadge: string + // webhookUrl and pings removed — adapters resolve these internally +} +``` + +### AlertLoggerConfig + +Remove `routing`: + +```ts +interface AlertLoggerConfig { + adapters: AlertAdapter[] + serviceName?: string + environment?: string + aggregation?: Partial + // routing removed — each adapter owns its routing + environments?: Record + queue?: Partial + health?: Partial + fingerprint?: Partial +} +``` + +### EnvironmentConfig + +Remove `pings` field (was per-environment ping overrides — now handled by adapter config): + +```ts +interface EnvironmentConfig { + levels?: AlertLevel[] + aggregation?: Partial + // pings removed +} +``` + +### Router class + +Removed entirely. Each adapter resolves destinations internally. + +### ResolvedConfig + +Remove `routing` and `pings` fields. + +## File Structure + +``` +src/adapters/ + console/ + console-adapter.ts (unchanged) + discord/ + discord-adapter.ts (add routing, remove webhookUrl dependency) + discord-adapter.test.ts (update tests) + formatter.ts (unchanged) + formatter.test.ts (unchanged) + slack/ + slack-adapter.ts (new) + slack-adapter.test.ts (new) + formatter.ts (new) + formatter.test.ts (new) + telegram/ + telegram-adapter.ts (new) + telegram-adapter.test.ts (new) + formatter.ts (new) + formatter.test.ts (new) +``` + +## Exports + +Add to `src/index.ts`: + +```ts +export { SlackAdapter } from './adapters/slack/slack-adapter.js' +export type { SlackAdapterOptions } from './adapters/slack/slack-adapter.js' +export { TelegramAdapter } from './adapters/telegram/telegram-adapter.js' +export type { TelegramAdapterOptions } from './adapters/telegram/telegram-adapter.js' +``` + +No changes to `tsup.config.ts` — adapters are part of the main entry point. + +## Testing + +Same pattern as `discord-adapter.test.ts`: + +- Mock `fetch` globally with `vi.stubGlobal`. +- Test `send()` posts correct payload to correct URL/endpoint. +- Test routing: level-based, tag-based, default fallback. +- Test mentions appear in correct location. +- Test 429 retry logic with service-specific retry-after parsing. +- Test non-429 error throws. +- Test `rateLimits()` returns expected values. +- Formatter tests cover all 4 phases, truncation, and sanitization. + +## Usage Example + +```ts +import { AlertLogger, DiscordAdapter, SlackAdapter, TelegramAdapter } from '@iqai/alert-logger' + +const logger = AlertLogger.init({ + serviceName: 'my-api', + adapters: [ + new DiscordAdapter({ + webhookUrl: 'https://discord.com/api/webhooks/default', + channels: { critical: 'https://discord.com/api/webhooks/critical' }, + mentions: { critical: ['<@&oncall-role>'] }, + }), + new SlackAdapter({ + webhookUrl: 'https://hooks.slack.com/services/T.../B.../default', + channels: { critical: 'https://hooks.slack.com/services/T.../B.../critical' }, + mentions: { critical: ['<@U0123ONCALL>'] }, + }), + new TelegramAdapter({ + botToken: '123456:ABC-DEF...', + chatId: '-1001234567890', + topics: { critical: 42, warning: 43, info: 44 }, + mentions: { critical: ['@oncall_dev'] }, + }), + ], +}) + +logger.error('Database connection lost', new Error('ECONNREFUSED')) +``` From feebe2bed65fe1b1606b8354e56dea50f6f9afce Mon Sep 17 00:00:00 2001 From: Srujan Gurram Date: Fri, 10 Apr 2026 23:54:28 +0530 Subject: [PATCH 2/2] docs: update README for Slack/Telegram adapters and adapter-owned routing - Quick Start shows all three adapters (Discord, Slack, Telegram) - Multi-Channel Routing updated to adapter-owned config pattern - Per-Environment Config removes deprecated pings field - Full Configuration removes global routing section - Adapters table marks Slack and Telegram as built-in Co-Authored-By: Claude Opus 4.6 (1M context) --- README.md | 111 ++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 75 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index b5b2b28..4831f21 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Stop drowning in alert storms. `@iqai/alert-logger` groups repeated errors using - **Resolution detection** — get a "resolved" message when an error stops occurring - **Error fingerprinting** — same bug from different requests groups automatically (strips IDs, timestamps, UUIDs) - **Multi-channel routing** — route by severity level or custom tags to different channels -- **Adapter architecture** — Discord and Console built-in; Sentry, Slack, Telegram as separate packages; or build your own +- **Adapter architecture** — Discord, Slack, Telegram, and Console built-in; or build your own - **NestJS integration** — drop-in `@Global()` module with automatic exception filter - **NextJS integration** — `instrumentation.ts` hook with automatic `onRequestError` handler - **Per-environment config** — different suppression thresholds, levels, and ping rules for prod/staging/dev @@ -35,16 +35,29 @@ yarn add @iqai/alert-logger ### Standalone (any Node.js project) ```ts -import { AlertLogger, DiscordAdapter } from '@iqai/alert-logger' +import { AlertLogger, DiscordAdapter, SlackAdapter, TelegramAdapter } from '@iqai/alert-logger' const logger = AlertLogger.init({ adapters: [ - new DiscordAdapter({ webhookUrl: process.env.DISCORD_WEBHOOK_URL }), + new DiscordAdapter({ + webhookUrl: process.env.DISCORD_WEBHOOK_URL, + channels: { critical: process.env.DISCORD_ONCALL_WEBHOOK }, + mentions: { critical: ['<@&oncall-role>'] }, + }), + new SlackAdapter({ + webhookUrl: process.env.SLACK_WEBHOOK_URL, + mentions: { critical: ['<@U0123ONCALL>'] }, + }), + new TelegramAdapter({ + botToken: process.env.TELEGRAM_BOT_TOKEN, + chatId: process.env.TELEGRAM_CHAT_ID, + topics: { critical: 42, warning: 43, info: 44 }, + }), ], serviceName: 'my-service', }) -// Simple error — goes to Discord with full context +// Simple error — goes to all adapters with full context logger.error('Payment failed', error) // With metadata @@ -156,17 +169,14 @@ AlertLogger.init({ environments: { production: { levels: ['warning', 'critical'], - pings: { critical: ['@here'] }, aggregation: { digestIntervalMs: 5 * 60_000 }, }, staging: { levels: ['critical'], // only errors, no warnings - pings: {}, // never ping anyone aggregation: { digestIntervalMs: 15 * 60_000 }, }, development: { levels: ['critical'], - pings: {}, aggregation: { rampThreshold: 8, digestIntervalMs: 30 * 60_000 }, }, }, @@ -177,25 +187,54 @@ Every alert is prefixed with an environment badge (`[PROD]`, `[STG]`, `[DEV]`) s ## 📡 Multi-Channel Routing -Route alerts to different webhooks by severity or tags: +Each adapter owns its routing. Route alerts to different channels/topics by severity or tags: ```ts AlertLogger.init({ - adapters: [new DiscordAdapter({ webhookUrl: DEFAULT_WEBHOOK })], - routing: { - channels: { - info: process.env.DISCORD_INFO_WEBHOOK, - warning: process.env.DISCORD_WARNINGS_WEBHOOK, - critical: process.env.DISCORD_ONCALL_WEBHOOK, - }, - tags: { - indexer: process.env.DISCORD_INDEXER_WEBHOOK, - relayer: process.env.DISCORD_RELAYER_WEBHOOK, - }, - pings: { - critical: ['@here'], - }, - }, + adapters: [ + // Discord: route by level to different webhook URLs + new DiscordAdapter({ + webhookUrl: process.env.DISCORD_DEFAULT_WEBHOOK, + channels: { + critical: process.env.DISCORD_ONCALL_WEBHOOK, + warning: process.env.DISCORD_WARNINGS_WEBHOOK, + }, + tags: { + indexer: process.env.DISCORD_INDEXER_WEBHOOK, + }, + mentions: { + critical: ['<@&oncall-role>'], + }, + }), + + // Slack: same pattern with Incoming Webhook URLs + new SlackAdapter({ + webhookUrl: process.env.SLACK_DEFAULT_WEBHOOK, + channels: { + critical: process.env.SLACK_ONCALL_WEBHOOK, + }, + mentions: { + critical: ['<@U0123ONCALL>'], + }, + }), + + // Telegram: route by level to forum topics + new TelegramAdapter({ + botToken: process.env.TELEGRAM_BOT_TOKEN, + chatId: process.env.TELEGRAM_CHAT_ID, + topics: { + critical: 42, + warning: 43, + info: 44, + }, + tags: { + indexer: 99, + }, + mentions: { + critical: ['@oncall_dev'], + }, + }), + ], }) ``` @@ -224,8 +263,15 @@ class PagerDutyAdapter implements AlertAdapter { ```ts AlertLogger.init({ - // Required - adapters: [new DiscordAdapter({ webhookUrl: '...' })], + // Required — each adapter configures its own routing + adapters: [ + new DiscordAdapter({ + webhookUrl: '...', + channels: {}, // level → webhook URL + tags: {}, // tag → webhook URL + mentions: {}, // level → mention strings + }), + ], // Identity serviceName: 'backend', // defaults to hostname @@ -238,17 +284,10 @@ AlertLogger.init({ resolutionCooldownMs: 2 * 60_000, // silence before "resolved" }, - // Routing - routing: { - channels: {}, // level → webhook URL - tags: {}, // tag → webhook URL - pings: {}, // level → mention strings - }, - // Per-environment overrides environments: { - production: { levels: ['warning', 'critical'], pings: { critical: ['@here'] } }, - staging: { levels: ['critical'], pings: {} }, + production: { levels: ['warning', 'critical'] }, + staging: { levels: ['critical'] }, development: { levels: ['critical'], aggregation: { rampThreshold: 8 } }, }, @@ -271,10 +310,10 @@ AlertLogger.init({ | Adapter | Package | Status | |---------|---------|--------| | Discord | `@iqai/alert-logger` (built-in) | Available | +| Slack | `@iqai/alert-logger` (built-in) | Available | +| Telegram | `@iqai/alert-logger` (built-in) | Available | | Console | `@iqai/alert-logger` (built-in) | Available | | Sentry | `@iqai/alert-logger-sentry` | Planned | -| Slack | `@iqai/alert-logger-slack` | Planned | -| Telegram | `@iqai/alert-logger-telegram` | Planned | ## 🤝 Contributing