1import Fastify from "fastify";
2
3interface AlertRule {
4 name: string;
5 query: string;
6 threshold: number;
7 comparison: "gt" | "lt";
8 duration: string;
9 channels: string[];
10}
11
12interface AlertState {
13 firing: boolean;
14 firedAt?: Date;
15 value?: number;
16}
17
18class AlertManager {
19 private rules: AlertRule[] = [];
20 private states: Map<string, AlertState> = new Map();
21 private prometheusUrl: string;
22
23 constructor(prometheusUrl: string) {
24 this.prometheusUrl = prometheusUrl;
25 }
26
27 addRule(rule: AlertRule): void {
28 this.rules.push(rule);
29 this.states.set(rule.name, { firing: false });
30 }
31
32 async evaluate(): Promise<void> {
33 for (const rule of this.rules) {
34 const value = await this.queryPrometheus(rule.query);
35 const shouldFire = rule.comparison === "gt" ? value > rule.threshold : value < rule.threshold;
36 const state = this.states.get(rule.name)!;
37
38 if (shouldFire && !state.firing) {
39 state.firing = true;
40 state.firedAt = new Date();
41 state.value = value;
42 await this.notify(rule, value);
43 } else if (!shouldFire && state.firing) {
44 state.firing = false;
45 await this.notifyResolved(rule);
46 }
47 }
48 }
49
50 private async queryPrometheus(query: string): Promise<number> {
51 const resp = await fetch(
52 `${this.prometheusUrl}/api/v1/query?query=${encodeURIComponent(query)}`
53 );
54 const data = await resp.json();
55 return parseFloat(data.data?.result?.[0]?.value?.[1] || "0");
56 }
57
58 private async notify(rule: AlertRule, value: number): Promise<void> {
59 for (const channel of rule.channels) {
60 if (channel.startsWith("slack:")) {
61 await this.sendSlackAlert(channel.replace("slack:", ""), rule, value);
62 } else if (channel.startsWith("pagerduty:")) {
63 await this.sendPagerDutyAlert(channel.replace("pagerduty:", ""), rule, value);
64 }
65 }
66 }
67
68 private async sendSlackAlert(webhook: string, rule: AlertRule, value: number): Promise<void> {
69 await fetch(webhook, {
70 method: "POST",
71 headers: { "Content-Type": "application/json" },
72 body: JSON.stringify({
73 text: `🚨 Alert: ${rule.name}\nValue: ${value} (threshold: ${rule.threshold})`,
74 }),
75 });
76 }
77
78 private async sendPagerDutyAlert(routingKey: string, rule: AlertRule, value: number): Promise<void> {
79 await fetch("https://events.pagerduty.com/v2/enqueue", {
80 method: "POST",
81 headers: { "Content-Type": "application/json" },
82 body: JSON.stringify({
83 routing_key: routingKey,
84 event_action: "trigger",
85 payload: {
86 summary: `${rule.name}: ${value} exceeds threshold ${rule.threshold}`,
87 severity: "critical",
88 source: "custom-alert-manager",
89 },
90 }),
91 });
92 }
93
94 private async notifyResolved(rule: AlertRule): Promise<void> {
95 for (const channel of rule.channels) {
96 if (channel.startsWith("slack:")) {
97 await fetch(channel.replace("slack:", ""), {
98 method: "POST",
99 headers: { "Content-Type": "application/json" },
100 body: JSON.stringify({ text: `✅ Resolved: ${rule.name}` }),
101 });
102 }
103 }
104 }
105}
106