mirror of
https://github.com/fosrl/pangolin.git
synced 2026-08-12 15:30:53 +02:00
✨ test alert email works
This commit is contained in:
@@ -20,7 +20,10 @@ export async function processTestAlerts(context: TestAlertContext) {
|
||||
try {
|
||||
const recipients = await resolveEmailRecipients(action);
|
||||
if (recipients.length > 0) {
|
||||
await sendAlertEmail(recipients, context);
|
||||
await sendAlertEmail(recipients, {
|
||||
...context,
|
||||
isTest: true
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(`processTestAlerts: failed to send alert email`, err);
|
||||
|
||||
@@ -15,7 +15,24 @@ import { sendEmail } from "@server/emails";
|
||||
import AlertNotification from "@server/emails/templates/AlertNotification";
|
||||
import config from "@server/lib/config";
|
||||
import logger from "@server/logger";
|
||||
import { AlertContext } from "@server/routers/alertRule/types";
|
||||
import {
|
||||
AlertContext,
|
||||
type AlertEventType
|
||||
} from "@server/routers/alertRule/types";
|
||||
|
||||
type EmailAlertContext = {
|
||||
eventType: AlertEventType;
|
||||
orgId: string;
|
||||
/** Set for site_online / site_offline events */
|
||||
siteId?: number;
|
||||
/** Set for health_check_* events */
|
||||
healthCheckId?: number;
|
||||
/** Set for resource_* events */
|
||||
resourceId?: number;
|
||||
/** Human-readable context data included in emails and webhook payloads */
|
||||
data: Record<string, unknown>;
|
||||
isTest?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Sends an alert notification email to every address in `recipients`.
|
||||
@@ -27,7 +44,7 @@ import { AlertContext } from "@server/routers/alertRule/types";
|
||||
*/
|
||||
export async function sendAlertEmail(
|
||||
recipients: string[],
|
||||
context: AlertContext
|
||||
context: EmailAlertContext
|
||||
): Promise<void> {
|
||||
if (recipients.length === 0) {
|
||||
return;
|
||||
@@ -46,7 +63,8 @@ export async function sendAlertEmail(
|
||||
eventType: context.eventType,
|
||||
orgId: context.orgId,
|
||||
data: context.data,
|
||||
dashboardLink
|
||||
dashboardLink,
|
||||
isTestAlert: context.isTest
|
||||
}),
|
||||
{
|
||||
from,
|
||||
@@ -70,34 +88,35 @@ export async function sendAlertEmail(
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function buildSubject(context: AlertContext): string {
|
||||
function buildSubject(context: EmailAlertContext): string {
|
||||
const prefix = context.isTest ? "[Test Alert]" : "[Alert]";
|
||||
switch (context.eventType) {
|
||||
case "site_online":
|
||||
return "[Alert] Site Back Online";
|
||||
return `${prefix} Site Back Online`;
|
||||
case "site_offline":
|
||||
return "[Alert] Site Offline";
|
||||
return `${prefix} Site Offline`;
|
||||
case "site_toggle":
|
||||
return "[Alert] Site Status Changed";
|
||||
return `${prefix} Site Status Changed`;
|
||||
case "health_check_healthy":
|
||||
return "[Alert] Health Check Recovered";
|
||||
return `${prefix} Health Check Recovered`;
|
||||
case "health_check_unhealthy":
|
||||
return "[Alert] Health Check Failing";
|
||||
return `${prefix} Health Check Failing`;
|
||||
case "health_check_toggle":
|
||||
return "[Alert] Health Check Status Changed";
|
||||
return `${prefix} Health Check Status Changed`;
|
||||
case "resource_healthy":
|
||||
return "[Alert] Resource Healthy";
|
||||
return `${prefix} Resource Healthy`;
|
||||
case "resource_unhealthy":
|
||||
return "[Alert] Resource Unhealthy";
|
||||
return `${prefix} Resource Unhealthy`;
|
||||
case "resource_degraded":
|
||||
return "[Alert] Resource Degraded";
|
||||
return `${prefix} Resource Degraded`;
|
||||
case "resource_toggle":
|
||||
return "[Alert] Resource Status Changed";
|
||||
return `${prefix} Resource Status Changed`;
|
||||
default: {
|
||||
// Exhaustiveness fallback – should never be reached with a
|
||||
// well-typed caller, but keeps runtime behaviour predictable.
|
||||
const _exhaustive: never = context.eventType;
|
||||
void _exhaustive;
|
||||
return "[Alert] Event Notification";
|
||||
return `${prefix} Event Notification`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,13 @@ import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import { and, asc, desc, eq, inArray, like, or, sql } from "drizzle-orm";
|
||||
import { ListAlertRulesResponse } from "@server/routers/alertRule/types";
|
||||
import {
|
||||
ListAlertRulesResponse,
|
||||
type AlertAction,
|
||||
type EmailAlertAction
|
||||
} from "@server/routers/alertRule/types";
|
||||
import { processTestAlerts } from "@server/private/lib/alerts/processTestAlerts";
|
||||
import { getRandomItemInArray } from "@app/lib/getRandomItemInArray";
|
||||
|
||||
const paramsSchema = z.strictObject({
|
||||
orgId: z.string().nonempty()
|
||||
@@ -51,12 +57,12 @@ export const RESOURCE_EVENT_TYPES = [
|
||||
] as const;
|
||||
|
||||
const webhookActionSchema = z.strictObject({
|
||||
webhookUrl: z.string().url(),
|
||||
webhookUrl: z.url(),
|
||||
config: z.string().optional(),
|
||||
enabled: z.boolean().optional().default(true)
|
||||
});
|
||||
|
||||
const bodySchema = z.strictObject({
|
||||
const bodySchema = z.object({
|
||||
eventType: z.enum([
|
||||
...HC_EVENT_TYPES,
|
||||
...SITE_EVENT_TYPES,
|
||||
@@ -97,7 +103,106 @@ export async function testAlertRule(
|
||||
);
|
||||
}
|
||||
|
||||
const body = parsedBody.data;
|
||||
const collectedActions: AlertAction[] = [];
|
||||
if (
|
||||
body.emails.length > 0 ||
|
||||
body.roleIds.length > 0 ||
|
||||
body.userIds.length > 0
|
||||
) {
|
||||
collectedActions.push({
|
||||
type: "email",
|
||||
emails: body.emails,
|
||||
roleIds: body.roleIds,
|
||||
userIds: body.userIds
|
||||
});
|
||||
}
|
||||
|
||||
for (const action of body.webhookActions) {
|
||||
collectedActions.push({
|
||||
type: "webhook",
|
||||
...action
|
||||
});
|
||||
}
|
||||
|
||||
let data: Record<string, any> = {};
|
||||
switch (body.eventType) {
|
||||
case "site_toggle":
|
||||
data = {
|
||||
status: getRandomItemInArray(["online", "offline"]),
|
||||
siteName: "Test Site Alert"
|
||||
};
|
||||
break;
|
||||
case "site_offline":
|
||||
data = {
|
||||
status: "offline",
|
||||
siteName: "Test Site Alert"
|
||||
};
|
||||
break;
|
||||
case "site_online":
|
||||
data = {
|
||||
status: "online",
|
||||
siteName: "Test Site Alert"
|
||||
};
|
||||
break;
|
||||
case "resource_toggle":
|
||||
data = {
|
||||
status: getRandomItemInArray([
|
||||
"healthy",
|
||||
"unhealthy",
|
||||
"degraded"
|
||||
]),
|
||||
siteName: "Test Resource Alert"
|
||||
};
|
||||
break;
|
||||
case "resource_healthy":
|
||||
data = {
|
||||
status: "healthy",
|
||||
siteName: "Test Resource Alert"
|
||||
};
|
||||
break;
|
||||
case "resource_unhealthy":
|
||||
data = {
|
||||
status: "unhealthy",
|
||||
siteName: "Test Resource Alert"
|
||||
};
|
||||
break;
|
||||
case "resource_degraded":
|
||||
data = {
|
||||
status: "degraded",
|
||||
siteName: "Test Resource Alert"
|
||||
};
|
||||
break;
|
||||
case "health_check_toggle":
|
||||
data = {
|
||||
status: getRandomItemInArray(["healthy", "unhealthy"]),
|
||||
healthCheckName: "Test Health Check Alert"
|
||||
};
|
||||
break;
|
||||
case "health_check_healthy":
|
||||
data = {
|
||||
status: "healthy",
|
||||
healthCheckName: "Test Health Check Alert"
|
||||
};
|
||||
break;
|
||||
case "health_check_unhealthy":
|
||||
data = {
|
||||
status: "unhealthy",
|
||||
healthCheckName: "Test Health Check Alert"
|
||||
};
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// TODO: process alert rule
|
||||
await processTestAlerts({
|
||||
eventType: body.eventType,
|
||||
orgId,
|
||||
actions: collectedActions,
|
||||
data
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
|
||||
@@ -809,7 +809,7 @@ authenticated.get(
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
"/org/:orgId/alert-rule/test",
|
||||
"/org/:orgId/test-alert-rule",
|
||||
verifyValidLicense,
|
||||
verifyOrgAccess,
|
||||
verifyUserHasAction(ActionsEnum.testAlertRule),
|
||||
|
||||
Reference in New Issue
Block a user