diff --git a/messages/en-US.json b/messages/en-US.json index e4d55e4b9..5c0ca434b 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -1469,6 +1469,8 @@ "alertingConfigureTrigger": "Configure Trigger", "alertingConfigureActions": "Configure Actions", "alertingBackToRules": "Back to Rules", + "alertingRuleCooldown": "Cooldown (seconds)", + "alertingRuleCooldownDescription": "Minimum time between repeated alerts for the same rule. Set to 0 to fire every time.", "alertingDraftBadge": "Draft - save to store this rule", "alertingSidebarHint": "Click a step on the canvas to edit it here.", "alertingGraphCanvasTitle": "Rule Flow", diff --git a/server/nextServer.ts b/server/nextServer.ts index b862a699c..deb74d309 100644 --- a/server/nextServer.ts +++ b/server/nextServer.ts @@ -11,7 +11,7 @@ export async function createNextServer() { // const app = next({ dev }); const app = next({ dev: process.env.ENVIRONMENT !== "prod", - turbopack: true + turbopack: false }); const handle = app.getRequestHandler(); diff --git a/server/private/lib/alerts/events/healthCheckEvents.ts b/server/private/lib/alerts/events/healthCheckEvents.ts index 2e1f591a2..cd9f3f1c3 100644 --- a/server/private/lib/alerts/events/healthCheckEvents.ts +++ b/server/private/lib/alerts/events/healthCheckEvents.ts @@ -74,7 +74,7 @@ export async function fireHealthCheckHealthyAlert( * @param healthCheckName - Human-readable name shown in notifications (optional). * @param extra - Any additional key/value pairs to include in the payload. */ -export async function fireHealthCheckNotHealthyAlert( +export async function fireHealthCheckUnhealthyAlert( orgId: string, healthCheckId: number, healthCheckName?: string | null, diff --git a/server/private/routers/alertEvents/triggerHealthCheckAlert.ts b/server/private/routers/alertEvents/triggerHealthCheckAlert.ts index 246de8cd0..94202b0b2 100644 --- a/server/private/routers/alertEvents/triggerHealthCheckAlert.ts +++ b/server/private/routers/alertEvents/triggerHealthCheckAlert.ts @@ -23,7 +23,7 @@ import { fromError } from "zod-validation-error"; import { eq, and } from "drizzle-orm"; import { fireHealthCheckHealthyAlert, - fireHealthCheckNotHealthyAlert + fireHealthCheckUnhealthyAlert } from "#private/lib/alerts/events/healthCheckEvents"; const paramsSchema = z.strictObject({ @@ -106,7 +106,7 @@ export async function triggerHealthCheckAlert( healthCheck.name ?? undefined ); } else { - await fireHealthCheckNotHealthyAlert( + await fireHealthCheckUnhealthyAlert( orgId, healthCheckId, healthCheck.name ?? undefined @@ -126,4 +126,4 @@ export async function triggerHealthCheckAlert( createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") ); } -} \ No newline at end of file +} diff --git a/server/private/routers/alertRule/createAlertRule.ts b/server/private/routers/alertRule/createAlertRule.ts index 8a31327ab..b9d17d35d 100644 --- a/server/private/routers/alertRule/createAlertRule.ts +++ b/server/private/routers/alertRule/createAlertRule.ts @@ -63,7 +63,7 @@ const bodySchema = z ...RESOURCE_EVENT_TYPES ]), enabled: z.boolean().optional().default(true), - cooldownSeconds: z.number().int().nonnegative().optional().default(300), + cooldownSeconds: z.number().int().nonnegative().optional().default(0), // Source join tables - which is required depends on eventType siteIds: z.array(z.number().int().positive()).optional().default([]), allSites: z.boolean().optional().default(false), diff --git a/server/private/routers/healthChecks/createHealthCheck.ts b/server/private/routers/healthChecks/createHealthCheck.ts index ff5495e55..374ec4ba4 100644 --- a/server/private/routers/healthChecks/createHealthCheck.ts +++ b/server/private/routers/healthChecks/createHealthCheck.ts @@ -39,7 +39,7 @@ const bodySchema = z.strictObject({ hcMethod: z.string().default("GET"), hcInterval: z.number().int().positive().default(30), hcUnhealthyInterval: z.number().int().positive().default(30), - hcTimeout: z.number().int().positive().default(5), + hcTimeout: z.number().int().positive().default(1), hcHeaders: z.string().optional().nullable(), hcFollowRedirects: z.boolean().default(true), hcStatus: z.number().int().optional().nullable(), diff --git a/server/routers/target/createTarget.ts b/server/routers/target/createTarget.ts index ea7512b9c..e5c1f246e 100644 --- a/server/routers/target/createTarget.ts +++ b/server/routers/target/createTarget.ts @@ -31,8 +31,8 @@ const createTargetSchema = z.strictObject({ hcMode: z.string().optional().nullable(), hcHostname: z.string().optional().nullable(), hcPort: z.int().positive().optional().nullable(), - hcInterval: z.int().positive().min(5).optional().nullable(), - hcUnhealthyInterval: z.int().positive().min(5).optional().nullable(), + hcInterval: z.int().positive().min(1).optional().nullable(), + hcUnhealthyInterval: z.int().positive().min(1).optional().nullable(), hcTimeout: z.int().positive().min(1).optional().nullable(), hcHeaders: z .array(z.strictObject({ name: z.string(), value: z.string() })) diff --git a/server/routers/target/handleHealthcheckStatusMessage.ts b/server/routers/target/handleHealthcheckStatusMessage.ts index 55834d926..b5ac7f79f 100644 --- a/server/routers/target/handleHealthcheckStatusMessage.ts +++ b/server/routers/target/handleHealthcheckStatusMessage.ts @@ -8,12 +8,13 @@ import { } from "@server/db"; import { MessageHandler } from "@server/routers/ws"; import { Newt } from "@server/db"; -import { eq, and } from "drizzle-orm"; +import { eq, and, ne } from "drizzle-orm"; import logger from "@server/logger"; import { fireHealthCheckHealthyAlert, - fireHealthCheckNotHealthyAlert + fireHealthCheckUnhealthyAlert } from "#dynamic/lib/alerts"; +import { fireResourceHealthyAlert, fireResourceUnhealthyAlert } from "@server/private/lib/alerts/events/resourceEvents"; interface TargetHealthStatus { status: string; @@ -96,10 +97,12 @@ export const handleHealthcheckStatusMessage: MessageHandler = async ( targetHealthCheckId: targetHealthCheck.targetHealthCheckId, resourceOrgId: resources.orgId, resourceId: resources.resourceId, + resourceName: resources.name, name: targetHealthCheck.name, - hcStatus: targetHealthCheck.hcHealth + hcHealth: targetHealthCheck.hcHealth }) .from(targetHealthCheck) + .innerJoin(sites, eq(targetHealthCheck.siteId, sites.siteId)) .innerJoin( targets, eq(targetHealthCheck.targetId, targets.targetId) @@ -108,7 +111,6 @@ export const handleHealthcheckStatusMessage: MessageHandler = async ( resources, eq(targets.resourceId, resources.resourceId) ) - .innerJoin(sites, eq(targets.siteId, sites.siteId)) .where( and( eq(targetHealthCheck.targetHealthCheckId, targetIdNum), @@ -126,7 +128,7 @@ export const handleHealthcheckStatusMessage: MessageHandler = async ( } // check if the status has changed - if (targetCheck.hcStatus === healthStatus.status) { + if (targetCheck.hcHealth === healthStatus.status) { logger.debug( `Health status for target ${targetId} is already ${healthStatus.status}, skipping update` ); @@ -178,7 +180,7 @@ export const handleHealthcheckStatusMessage: MessageHandler = async ( .where( and( eq(targets.resourceId, targetCheck.resourceId), - eq(targets.targetId, targetCheck.targetId) // only check the other targets, not the one we just updated + ne(targets.targetId, targetCheck.targetId) // only check the other targets, not the one we just updated ) ); @@ -200,17 +202,31 @@ export const handleHealthcheckStatusMessage: MessageHandler = async ( status: status, timestamp: Math.floor(Date.now() / 1000) }); + + if (status === "unhealthy") { + await fireResourceUnhealthyAlert( + orgId, + targetCheck.resourceId, + targetCheck.resourceName + ); + } else if (status === "healthy") { + await fireResourceHealthyAlert( + orgId, + targetCheck.resourceId, + targetCheck.resourceName + ); + } } // because we are checking above if there was a change we can fire the alert here because it changed if (healthStatus.status === "unhealthy") { - await fireHealthCheckHealthyAlert( + await fireHealthCheckUnhealthyAlert( orgId, targetCheck.targetHealthCheckId, targetCheck.name ); } else if (healthStatus.status === "healthy") { - await fireHealthCheckNotHealthyAlert( + await fireHealthCheckHealthyAlert( orgId, targetCheck.targetHealthCheckId, targetCheck.name diff --git a/server/routers/target/updateTarget.ts b/server/routers/target/updateTarget.ts index 52759bfc8..a633deb4d 100644 --- a/server/routers/target/updateTarget.ts +++ b/server/routers/target/updateTarget.ts @@ -32,8 +32,8 @@ const updateTargetBodySchema = z hcMode: z.string().optional().nullable(), hcHostname: z.string().optional().nullable(), hcPort: z.int().positive().optional().nullable(), - hcInterval: z.int().positive().min(5).optional().nullable(), - hcUnhealthyInterval: z.int().positive().min(5).optional().nullable(), + hcInterval: z.int().positive().min(1).optional().nullable(), + hcUnhealthyInterval: z.int().positive().min(1).optional().nullable(), hcTimeout: z.int().positive().min(1).optional().nullable(), hcHeaders: z .array(z.strictObject({ name: z.string(), value: z.string() })) diff --git a/src/components/alert-rule-editor/AlertRuleGraphEditor.tsx b/src/components/alert-rule-editor/AlertRuleGraphEditor.tsx index 524f17482..85dd61c5a 100644 --- a/src/components/alert-rule-editor/AlertRuleGraphEditor.tsx +++ b/src/components/alert-rule-editor/AlertRuleGraphEditor.tsx @@ -12,6 +12,7 @@ import { Card, CardContent } from "@app/components/ui/card"; import { Form, FormControl, + FormDescription, FormField, FormItem, FormLabel, @@ -205,6 +206,38 @@ export default function AlertRuleGraphEditor({ )} /> + ( + + + {t("alertingRuleCooldown")} + + + + field.onChange( + Number( + e.target + .value + ) + ) + } + /> + + + {t("alertingRuleCooldownDescription")} + + + + )} + /> string) { .string() .min(1, { message: t("alertingErrorNameRequired") }), enabled: z.boolean(), + cooldownSeconds: z.number().int().nonnegative().default(0), sourceType: z.enum(["site", "health_check", "resource"]), allSites: z.boolean().default(true), siteIds: z.array(z.number()).default([]), @@ -309,6 +312,7 @@ export function defaultFormValues(): AlertRuleFormValues { return { name: "", enabled: true, + cooldownSeconds: 0, sourceType: "site", allSites: true, siteIds: [], @@ -422,6 +426,7 @@ export function apiResponseToFormValues( return { name: rule.name, enabled: rule.enabled, + cooldownSeconds: rule.cooldownSeconds ?? 0, sourceType, allSites, siteIds: rule.siteIds, @@ -483,6 +488,7 @@ export function formValuesToApiPayload( name: values.name.trim(), eventType, enabled: values.enabled, + cooldownSeconds: values.cooldownSeconds, allSites: values.allSites, siteIds: values.allSites ? [] : values.siteIds, allHealthChecks: values.allHealthChecks,