forked from Vero-protocol/vero-core-engine
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathalert-channel.ts
More file actions
119 lines (100 loc) · 3.29 KB
/
Copy pathalert-channel.ts
File metadata and controls
119 lines (100 loc) · 3.29 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
import { logger } from "./logger";
export interface Alert {
id: string;
severity: "INFO" | "WARNING" | "CRITICAL";
title: string;
message: string;
timestamp: string;
metadata?: Record<string, unknown>;
}
export interface AlertChannel {
send(alert: Alert): Promise<boolean>;
}
export interface AlertChannelServiceOptions {
channels?: AlertChannel[];
}
export class AlertChannelService {
private channels: AlertChannel[];
constructor(options: AlertChannelServiceOptions = {}) {
this.channels = options.channels ?? [new ConsoleAlertChannel()];
}
addChannel(channel: AlertChannel): void {
this.channels.push(channel);
}
async send(alert: Alert): Promise<void> {
const results = await Promise.allSettled(
this.channels.map(ch => ch.send(alert))
);
for (let i = 0; i < results.length; i++) {
const result = results[i];
if (result.status === "rejected") {
logger.error(`[AlertChannelService] Channel ${i} failed:`, result.reason);
} else if (!result.value) {
logger.warn(`[AlertChannelService] Channel ${i} returned false`);
}
}
}
}
export class ConsoleAlertChannel implements AlertChannel {
async send(alert: Alert): Promise<boolean> {
const prefix = alert.severity === "CRITICAL" ? "[ALERT CRITICAL]" :
alert.severity === "WARNING" ? "[ALERT WARNING]" :
"[ALERT INFO]";
const meta = alert.metadata ? ` ${JSON.stringify(alert.metadata)}` : "";
if (alert.severity === "CRITICAL") {
logger.error(`${prefix} ${alert.title} — ${alert.message}${meta}`, { alert });
} else if (alert.severity === "WARNING") {
logger.warn(`${prefix} ${alert.title} — ${alert.message}${meta}`, { alert });
} else {
logger.info(`${prefix} ${alert.title} — ${alert.message}${meta}`, { alert });
}
return true;
}
}
export interface WebhookAlertChannelOptions {
url: string;
headers?: Record<string, string>;
timeoutMs?: number;
}
export class WebhookAlertChannel implements AlertChannel {
private readonly url: string;
private readonly headers: Record<string, string>;
private readonly timeoutMs: number;
constructor(options: WebhookAlertChannelOptions) {
this.url = options.url;
this.headers = {
"Content-Type": "application/json",
...options.headers,
};
this.timeoutMs = options.timeoutMs ?? 10_000;
}
async send(alert: Alert): Promise<boolean> {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
try {
const response = await fetch(this.url, {
method: "POST",
headers: this.headers,
body: JSON.stringify({
id: alert.id,
severity: alert.severity,
title: alert.title,
message: alert.message,
timestamp: alert.timestamp,
metadata: alert.metadata,
}),
signal: controller.signal,
});
if (!response.ok) {
logger.warn(`[WebhookAlertChannel] HTTP ${response.status} for alert ${alert.id}`);
return false;
}
return true;
} catch (err) {
logger.error(`[WebhookAlertChannel] Request failed for alert ${alert.id}:`, err);
return false;
} finally {
clearTimeout(timer);
}
}
}