Skip to content

Commit fc9db19

Browse files
pescnclaude
andauthored
feat: add Grafana integration and alert system (#67)
* feat: add Grafana integration and alert system Add a complete alert system with notification channels (webhook, email, Feishu), configurable alert rules (budget, error rate, latency, quota), and alert history tracking. Add Grafana integration for syncing alert rules as Prometheus-based Grafana alerts via the Provisioning API. When Grafana is connected, the built-in alert engine defers to Grafana for evaluation. Restructure frontend navigation: model configuration moves to /models (Providers + Registry sub-nav), system settings moves to /settings (Alerts + Grafana sub-nav) as separate sidebar items. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: address PR review comments - Fix PromQL label selector syntax (no leading/trailing commas) - Add fetch timeouts (AbortSignal.timeout) to all external HTTP calls - Add ON DELETE CASCADE to alert_history FK referencing alert_rules - Deduplicate grafanaConnectionQueryOptions (reuse from use-settings hook) - Add default case to dispatchToChannel switch statement - Fix duplicate listApiKeys() call in evaluateQuota - Add missing i18n key pages.settings.grafana.Testing - Fix wrong i18n key reference in grafana-settings-page.tsx - Clear datasourceUid on connection test failure Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: escape HTML in email alerts and add apiKeyId filter to quota PromQL - Add escapeHtml() helper and apply to all dynamic fields in email template - Add apiKeyId label selector to quota PromQL when a specific key is configured Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(alerts): correct Feishu webhook signature algorithm Per Feishu docs, the signature must use `timestamp\nsecret` as the HMAC key and sign an empty string, then Base64-encode the result. The previous implementation incorrectly used `secret` as key, `timestamp\nsecret` as data, and hex-encoded the output. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: use Buffer for hex encoding and add graceful shutdown - Replace manual Array.from hex encoding with Buffer.from().toString("hex") - Add SIGINT/SIGTERM handlers to stop alert engine and server on shutdown Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent f823a58 commit fc9db19

37 files changed

Lines changed: 8644 additions & 77 deletions
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
CREATE TYPE "public"."alert_channel_type" AS ENUM('webhook', 'email', 'feishu');--> statement-breakpoint
2+
CREATE TYPE "public"."alert_history_status" AS ENUM('sent', 'failed', 'suppressed');--> statement-breakpoint
3+
CREATE TYPE "public"."alert_rule_type" AS ENUM('budget', 'error_rate', 'latency', 'quota');--> statement-breakpoint
4+
CREATE TABLE "alert_channels" (
5+
"id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "alert_channels_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1),
6+
"name" varchar(100) NOT NULL,
7+
"type" "alert_channel_type" NOT NULL,
8+
"config" jsonb NOT NULL,
9+
"enabled" boolean DEFAULT true NOT NULL,
10+
"created_at" timestamp DEFAULT now() NOT NULL,
11+
"updated_at" timestamp DEFAULT now() NOT NULL
12+
);
13+
--> statement-breakpoint
14+
CREATE TABLE "alert_history" (
15+
"id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "alert_history_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1),
16+
"rule_id" integer NOT NULL,
17+
"triggered_at" timestamp DEFAULT now() NOT NULL,
18+
"resolved_at" timestamp,
19+
"payload" jsonb NOT NULL,
20+
"status" "alert_history_status" NOT NULL
21+
);
22+
--> statement-breakpoint
23+
CREATE TABLE "alert_rules" (
24+
"id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "alert_rules_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1),
25+
"name" varchar(100) NOT NULL,
26+
"type" "alert_rule_type" NOT NULL,
27+
"condition" jsonb NOT NULL,
28+
"channel_ids" integer[] NOT NULL,
29+
"cooldown_minutes" integer DEFAULT 60 NOT NULL,
30+
"enabled" boolean DEFAULT true NOT NULL,
31+
"created_at" timestamp DEFAULT now() NOT NULL,
32+
"updated_at" timestamp DEFAULT now() NOT NULL
33+
);
34+
--> statement-breakpoint
35+
ALTER TABLE "alert_history" ADD CONSTRAINT "alert_history_rule_id_alert_rules_id_fk" FOREIGN KEY ("rule_id") REFERENCES "public"."alert_rules"("id") ON DELETE no action ON UPDATE no action;
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
ALTER TABLE "alert_channels" ADD COLUMN "grafana_uid" varchar(127);--> statement-breakpoint
2+
ALTER TABLE "alert_channels" ADD COLUMN "grafana_synced_at" timestamp;--> statement-breakpoint
3+
ALTER TABLE "alert_channels" ADD COLUMN "grafana_sync_error" varchar(500);--> statement-breakpoint
4+
ALTER TABLE "alert_rules" ADD COLUMN "grafana_uid" varchar(127);--> statement-breakpoint
5+
ALTER TABLE "alert_rules" ADD COLUMN "grafana_synced_at" timestamp;--> statement-breakpoint
6+
ALTER TABLE "alert_rules" ADD COLUMN "grafana_sync_error" varchar(500);
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
ALTER TABLE "alert_history" DROP CONSTRAINT "alert_history_rule_id_alert_rules_id_fk";
2+
--> statement-breakpoint
3+
ALTER TABLE "alert_history" ADD CONSTRAINT "alert_history_rule_id_alert_rules_id_fk" FOREIGN KEY ("rule_id") REFERENCES "public"."alert_rules"("id") ON DELETE cascade ON UPDATE no action;

0 commit comments

Comments
 (0)