Skip to content

Commit 5220fe0

Browse files
committed
Add webhook notification provider
1 parent c1ff208 commit 5220fe0

File tree

13 files changed

+299
-29
lines changed

13 files changed

+299
-29
lines changed

README.md

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ A high-performance uptime monitoring system built with Bun and ClickHouse. Recei
77
- **Pulse-Based Monitoring** - Services send heartbeats; missing pulses trigger alerts
88
- **Hierarchical Groups** - Organize monitors with flexible health strategies (any-up, all-up, percentage)
99
- **Custom Metrics** - Track up to 3 numeric values per monitor (player count, connections, etc.)
10-
- **Multi-Channel Notifications** - Discord, Email, Ntfy and Telegram support with per-monitor control
10+
- **Multi-Channel Notifications** - Discord, Email, Ntfy, Telegram and Webhook support with per-monitor control
1111
- **Dependency-Based Notification Suppression** - Define dependencies between monitors/groups to avoid notification storms when infrastructure fails
1212
- **Real-Time Status Pages** - WebSocket-powered live updates
1313
- **Self-Healing** - Automatic backfill when the monitor itself recovers from downtime
@@ -78,18 +78,18 @@ curl http://localhost:3000/v1/status/:slug
7878

7979
## Documentation
8080

81-
| Document | Description |
82-
| ---------------------------------------------------------------------- | ------------------------------------------------- |
83-
| [Pulses](docs/pulses.md) | How pulses work, timing, and best practices |
84-
| [Configuration Guide](docs/configuration.md) | Complete config.toml reference |
85-
| [API Reference](docs/api.md) | All endpoints and WebSocket events |
86-
| [Admin API Reference](docs/admin-api.md) | CRUD endpoints for managing configuration via API |
87-
| [Notifications](docs/notifications.md) | Setting up Discord, Email, Ntfy, Telegram |
88-
| [Groups & Strategies](docs/groups.md) | Organizing monitors hierarchically |
89-
| [Dependencies](docs/dependencies.md) | Notification suppression via dependencies |
90-
| [Custom Metrics](docs/custom-metrics.md) | Tracking additional data points |
91-
| [PulseMonitor Integration](docs/pulsemonitor.md) | Automated monitoring from multiple regions |
92-
| [Visual Configuration Editor](https://uptime-monitor.org/configurator) | Web-based UI for configuring monitors |
81+
| Document | Description |
82+
| ---------------------------------------------------------------------- | -------------------------------------------------- |
83+
| [Pulses](docs/pulses.md) | How pulses work, timing, and best practices |
84+
| [Configuration Guide](docs/configuration.md) | Complete config.toml reference |
85+
| [API Reference](docs/api.md) | All endpoints and WebSocket events |
86+
| [Admin API Reference](docs/admin-api.md) | CRUD endpoints for managing configuration via API |
87+
| [Notifications](docs/notifications.md) | Setting up Discord, Email, Ntfy, Telegram, Webhook |
88+
| [Groups & Strategies](docs/groups.md) | Organizing monitors hierarchically |
89+
| [Dependencies](docs/dependencies.md) | Notification suppression via dependencies |
90+
| [Custom Metrics](docs/custom-metrics.md) | Tracking additional data points |
91+
| [PulseMonitor Integration](docs/pulsemonitor.md) | Automated monitoring from multiple regions |
92+
| [Visual Configuration Editor](https://uptime-monitor.org/configurator) | Web-based UI for configuring monitors |
9393

9494
## Related Projects
9595

bun.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

config.example.toml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -294,4 +294,13 @@ chatId = "-1001234567890"
294294
# Optional: send to a specific forum topic
295295
# topicId = 123
296296
# Optional: send silently
297-
# disableNotification = false
297+
# disableNotification = false
298+
299+
# Webhook configuration for critical alerts
300+
[notifications.channels.critical.webhook]
301+
enabled = false
302+
url = "https://example.com/webhook"
303+
# Optional: Custom headers for authentication
304+
#[notifications.channels.critical.webhook.headers]
305+
#Authorization = "Bearer your-token"
306+
#X-Api-Key = "your-api-key"

docs/admin-api.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -596,6 +596,7 @@ curl -X POST -H "Authorization: Bearer <token>" \
596596
| `email` | object | Email/SMTP configuration |
597597
| `ntfy` | object | Ntfy push notification config |
598598
| `telegram` | object | Telegram bot configuration |
599+
| `webhook` | object | Webhook configuration |
599600

600601
**Discord configuration:**
601602

@@ -653,6 +654,18 @@ curl -X POST -H "Authorization: Bearer <token>" \
653654
}
654655
```
655656

657+
**Webhook configuration:**
658+
659+
```json
660+
{
661+
"enabled": true,
662+
"url": "https://example.com/webhook",
663+
"headers": {
664+
"Authorization": "Bearer your-token"
665+
}
666+
}
667+
```
668+
656669
**Success Response (201):**
657670

658671
```json

docs/notifications.md

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,74 @@ chatId = "-1001234567890"
224224
topicId = 42
225225
```
226226

227+
## Webhook
228+
229+
Send notifications to any HTTP endpoint as a JSON POST request.
230+
231+
```toml
232+
[notifications.channels.critical.webhook]
233+
enabled = true
234+
url = "https://example.com/webhook"
235+
236+
[notifications.channels.critical.webhook.headers]
237+
Authorization = "Bearer your-token"
238+
```
239+
240+
| Field | Required | Description |
241+
| --------- | -------- | ----------------------------------- |
242+
| `enabled` | Yes | Enable webhook notifications |
243+
| `url` | Yes | Target URL to POST notifications to |
244+
| `headers` | No | Custom HTTP headers (e.g. for auth) |
245+
246+
### Payload Format
247+
248+
All webhook notifications are sent as `POST` requests with `Content-Type: application/json`. The payload has a fixed structure:
249+
250+
```json
251+
{
252+
"type": "down",
253+
"monitorId": "api-prod",
254+
"monitorName": "Production API",
255+
"sourceType": "monitor",
256+
"timestamp": "2025-01-15T10:30:00.000Z",
257+
"formattedTime": "2025-01-15 10:30:00",
258+
"interval": 60,
259+
"downtime": 120000,
260+
"downtimeDuration": "2m 0s",
261+
"consecutiveDownCount": 2
262+
}
263+
```
264+
265+
| Field | Type | Always Present | Description |
266+
| ------------------------------ | ------ | -------------- | ---------------------------------------------------- |
267+
| `type` | string | Yes | `down`, `still-down`, or `recovered` |
268+
| `monitorId` | string | Yes | Monitor or group ID |
269+
| `monitorName` | string | Yes | Monitor or group display name |
270+
| `sourceType` | string | Yes | `monitor` or `group` |
271+
| `timestamp` | string | Yes | ISO 8601 timestamp |
272+
| `formattedTime` | string | Yes | Human-readable local time |
273+
| `interval` | number | Yes | Check interval in seconds |
274+
| `downtime` | number | No | Downtime duration in milliseconds |
275+
| `downtimeDuration` | string | No | Human-readable downtime (e.g. "2m 30s") |
276+
| `consecutiveDownCount` | number | No | Number of consecutive down checks |
277+
| `previousConsecutiveDownCount` | number | No | Previous consecutive down count |
278+
| `groupInfo` | object | No | Present only for groups (strategy, childrenUp, etc.) |
279+
280+
### Group Info Object
281+
282+
When `sourceType` is `group`, the payload includes:
283+
284+
```json
285+
{
286+
"groupInfo": {
287+
"strategy": "percentage",
288+
"childrenUp": 2,
289+
"totalChildren": 5,
290+
"upPercentage": 40
291+
}
292+
}
293+
```
294+
227295
## Assigning Channels to Monitors
228296

229297
```toml

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
{
22
"name": "uptimemonitor-server",
33
"module": "src/index.ts",
4-
"version": "0.2.20",
4+
"version": "0.2.21",
55
"type": "module",
66
"private": true,
77
"scripts": {
88
"start": "bun src/index.ts"
99
},
1010
"devDependencies": {
1111
"@types/bun": "^1.3.9",
12-
"@types/nodemailer": "^7.0.9"
12+
"@types/nodemailer": "^7.0.10"
1313
},
1414
"peerDependencies": {
1515
"typescript": "^5.9.3"

src/admin/notifications.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ function serialize(input: any): Record<string, unknown> {
132132
if (input.email) r.email = input.email;
133133
if (input.ntfy) r.ntfy = input.ntfy;
134134
if (input.telegram) r.telegram = input.telegram;
135+
if (input.webhook) r.webhook = input.webhook;
135136
return r;
136137
}
137138

@@ -211,5 +212,22 @@ function validate(input: any, isUpdate: boolean): string[] {
211212
}
212213
}
213214

215+
// Webhook
216+
if (input.webhook !== undefined && input.webhook !== null) {
217+
if (typeof input.webhook !== "object" || Array.isArray(input.webhook)) {
218+
e.push("webhook must be an object");
219+
} else {
220+
if (typeof input.webhook.enabled !== "boolean") e.push("webhook.enabled must be a boolean");
221+
if (input.webhook.enabled) {
222+
if (!input.webhook.url || typeof input.webhook.url !== "string") e.push("webhook.url is required when enabled");
223+
if (input.webhook.headers !== undefined) {
224+
if (typeof input.webhook.headers !== "object" || Array.isArray(input.webhook.headers)) {
225+
e.push("webhook.headers must be an object");
226+
}
227+
}
228+
}
229+
}
230+
}
231+
214232
return e;
215233
}

src/config.ts

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1239,6 +1239,48 @@ function validateTelegramConfig(config: unknown, channelId: string): any {
12391239
return result;
12401240
}
12411241

1242+
function validateWebhookConfig(config: unknown, channelId: string): any {
1243+
const errors: string[] = [];
1244+
const cfg = config as Record<string, unknown>;
1245+
1246+
if (!isBoolean(cfg.enabled)) {
1247+
errors.push(`notifications.channels.${channelId}.webhook.enabled must be a boolean`);
1248+
}
1249+
1250+
if (!cfg.enabled) {
1251+
return { enabled: false };
1252+
}
1253+
1254+
if (!isString(cfg.url) || cfg.url.trim().length === 0) {
1255+
errors.push(`notifications.channels.${channelId}.webhook.url must be a non-empty string`);
1256+
}
1257+
1258+
if (cfg.headers !== undefined) {
1259+
if (!isObject(cfg.headers)) {
1260+
errors.push(`notifications.channels.${channelId}.webhook.headers must be an object`);
1261+
} else {
1262+
for (const [key, value] of Object.entries(cfg.headers)) {
1263+
if (!isString(value)) {
1264+
errors.push(`notifications.channels.${channelId}.webhook.headers.${key} must be a string`);
1265+
}
1266+
}
1267+
}
1268+
}
1269+
1270+
if (errors.length > 0) {
1271+
throw new ConfigValidationError(errors);
1272+
}
1273+
1274+
const result: any = {
1275+
enabled: cfg.enabled,
1276+
url: cfg.url,
1277+
};
1278+
1279+
if (cfg.headers) result.headers = cfg.headers;
1280+
1281+
return result;
1282+
}
1283+
12421284
function validateNotificationChannel(channel: unknown, channelId: string): NotificationChannel {
12431285
const errors: string[] = [];
12441286

@@ -1318,6 +1360,16 @@ function validateNotificationChannel(channel: unknown, channelId: string): Notif
13181360
}
13191361
}
13201362

1363+
if (channel.webhook) {
1364+
try {
1365+
result.webhook = validateWebhookConfig(channel.webhook, channelId);
1366+
} catch (error) {
1367+
if (error instanceof ConfigValidationError) {
1368+
throw error;
1369+
}
1370+
}
1371+
}
1372+
13211373
return result;
13221374
}
13231375

@@ -1657,8 +1709,9 @@ function validateNotificationChannelProviders(config: Config): void {
16571709
const hasDiscord = channel.discord?.enabled;
16581710
const hasNtfy = channel.ntfy?.enabled;
16591711
const hasTelegram = channel.telegram?.enabled;
1712+
const hasWebhook = channel.webhook?.enabled;
16601713

1661-
if (!hasEmail && !hasDiscord && !hasNtfy && !hasTelegram) {
1714+
if (!hasEmail && !hasDiscord && !hasNtfy && !hasTelegram && !hasWebhook) {
16621715
errors.push(`Notification channel '${channelId}' is enabled but has no providers configured`);
16631716
}
16641717
}

src/notifications/channel-manager.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { EmailProvider } from "./providers/email";
22
import { DiscordProvider } from "./providers/discord";
33
import { NtfyProvider } from "./providers/ntfy";
44
import { TelegramProvider } from "./providers/telegram";
5+
import { WebhookProvider } from "./providers/webhook";
56
import { Logger } from "../logger";
67
import type { NotificationChannel, NotificationEvent, NotificationProvider } from "../types";
78

@@ -38,6 +39,10 @@ export class NotificationChannelManager {
3839
channelProviders.push(new TelegramProvider(channel.telegram));
3940
}
4041

42+
if (channel.webhook?.enabled) {
43+
channelProviders.push(new WebhookProvider(channel.webhook));
44+
}
45+
4146
if (channelProviders.length > 0) {
4247
this.providers.set(channelId, channelProviders);
4348
Logger.info("Notification channel initialized", {

src/notifications/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export { NotificationManager } from "./manager";
22
export { EmailProvider } from "./providers/email";
33
export { DiscordProvider } from "./providers/discord";
4-
export type { EmailConfig, NotificationEvent, DiscordConfig, TelegramConfig } from "../types";
4+
export { WebhookProvider } from "./providers/webhook";
5+
export type { EmailConfig, NotificationEvent, DiscordConfig, TelegramConfig, WebhookConfig } from "../types";

0 commit comments

Comments
 (0)