mirror of
https://github.com/fosrl/pangolin.git
synced 2026-05-31 04:56:43 +00:00
Making the alerts work
This commit is contained in:
@@ -1469,6 +1469,8 @@
|
|||||||
"alertingConfigureTrigger": "Configure Trigger",
|
"alertingConfigureTrigger": "Configure Trigger",
|
||||||
"alertingConfigureActions": "Configure Actions",
|
"alertingConfigureActions": "Configure Actions",
|
||||||
"alertingBackToRules": "Back to Rules",
|
"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",
|
"alertingDraftBadge": "Draft - save to store this rule",
|
||||||
"alertingSidebarHint": "Click a step on the canvas to edit it here.",
|
"alertingSidebarHint": "Click a step on the canvas to edit it here.",
|
||||||
"alertingGraphCanvasTitle": "Rule Flow",
|
"alertingGraphCanvasTitle": "Rule Flow",
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export async function createNextServer() {
|
|||||||
// const app = next({ dev });
|
// const app = next({ dev });
|
||||||
const app = next({
|
const app = next({
|
||||||
dev: process.env.ENVIRONMENT !== "prod",
|
dev: process.env.ENVIRONMENT !== "prod",
|
||||||
turbopack: true
|
turbopack: false
|
||||||
});
|
});
|
||||||
const handle = app.getRequestHandler();
|
const handle = app.getRequestHandler();
|
||||||
|
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ export async function fireHealthCheckHealthyAlert(
|
|||||||
* @param healthCheckName - Human-readable name shown in notifications (optional).
|
* @param healthCheckName - Human-readable name shown in notifications (optional).
|
||||||
* @param extra - Any additional key/value pairs to include in the payload.
|
* @param extra - Any additional key/value pairs to include in the payload.
|
||||||
*/
|
*/
|
||||||
export async function fireHealthCheckNotHealthyAlert(
|
export async function fireHealthCheckUnhealthyAlert(
|
||||||
orgId: string,
|
orgId: string,
|
||||||
healthCheckId: number,
|
healthCheckId: number,
|
||||||
healthCheckName?: string | null,
|
healthCheckName?: string | null,
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import { fromError } from "zod-validation-error";
|
|||||||
import { eq, and } from "drizzle-orm";
|
import { eq, and } from "drizzle-orm";
|
||||||
import {
|
import {
|
||||||
fireHealthCheckHealthyAlert,
|
fireHealthCheckHealthyAlert,
|
||||||
fireHealthCheckNotHealthyAlert
|
fireHealthCheckUnhealthyAlert
|
||||||
} from "#private/lib/alerts/events/healthCheckEvents";
|
} from "#private/lib/alerts/events/healthCheckEvents";
|
||||||
|
|
||||||
const paramsSchema = z.strictObject({
|
const paramsSchema = z.strictObject({
|
||||||
@@ -106,7 +106,7 @@ export async function triggerHealthCheckAlert(
|
|||||||
healthCheck.name ?? undefined
|
healthCheck.name ?? undefined
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
await fireHealthCheckNotHealthyAlert(
|
await fireHealthCheckUnhealthyAlert(
|
||||||
orgId,
|
orgId,
|
||||||
healthCheckId,
|
healthCheckId,
|
||||||
healthCheck.name ?? undefined
|
healthCheck.name ?? undefined
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ const bodySchema = z
|
|||||||
...RESOURCE_EVENT_TYPES
|
...RESOURCE_EVENT_TYPES
|
||||||
]),
|
]),
|
||||||
enabled: z.boolean().optional().default(true),
|
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
|
// Source join tables - which is required depends on eventType
|
||||||
siteIds: z.array(z.number().int().positive()).optional().default([]),
|
siteIds: z.array(z.number().int().positive()).optional().default([]),
|
||||||
allSites: z.boolean().optional().default(false),
|
allSites: z.boolean().optional().default(false),
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ const bodySchema = z.strictObject({
|
|||||||
hcMethod: z.string().default("GET"),
|
hcMethod: z.string().default("GET"),
|
||||||
hcInterval: z.number().int().positive().default(30),
|
hcInterval: z.number().int().positive().default(30),
|
||||||
hcUnhealthyInterval: 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(),
|
hcHeaders: z.string().optional().nullable(),
|
||||||
hcFollowRedirects: z.boolean().default(true),
|
hcFollowRedirects: z.boolean().default(true),
|
||||||
hcStatus: z.number().int().optional().nullable(),
|
hcStatus: z.number().int().optional().nullable(),
|
||||||
|
|||||||
@@ -31,8 +31,8 @@ const createTargetSchema = z.strictObject({
|
|||||||
hcMode: z.string().optional().nullable(),
|
hcMode: z.string().optional().nullable(),
|
||||||
hcHostname: z.string().optional().nullable(),
|
hcHostname: z.string().optional().nullable(),
|
||||||
hcPort: z.int().positive().optional().nullable(),
|
hcPort: z.int().positive().optional().nullable(),
|
||||||
hcInterval: z.int().positive().min(5).optional().nullable(),
|
hcInterval: z.int().positive().min(1).optional().nullable(),
|
||||||
hcUnhealthyInterval: z.int().positive().min(5).optional().nullable(),
|
hcUnhealthyInterval: z.int().positive().min(1).optional().nullable(),
|
||||||
hcTimeout: z.int().positive().min(1).optional().nullable(),
|
hcTimeout: z.int().positive().min(1).optional().nullable(),
|
||||||
hcHeaders: z
|
hcHeaders: z
|
||||||
.array(z.strictObject({ name: z.string(), value: z.string() }))
|
.array(z.strictObject({ name: z.string(), value: z.string() }))
|
||||||
|
|||||||
@@ -8,12 +8,13 @@ import {
|
|||||||
} from "@server/db";
|
} from "@server/db";
|
||||||
import { MessageHandler } from "@server/routers/ws";
|
import { MessageHandler } from "@server/routers/ws";
|
||||||
import { Newt } from "@server/db";
|
import { Newt } from "@server/db";
|
||||||
import { eq, and } from "drizzle-orm";
|
import { eq, and, ne } from "drizzle-orm";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import {
|
import {
|
||||||
fireHealthCheckHealthyAlert,
|
fireHealthCheckHealthyAlert,
|
||||||
fireHealthCheckNotHealthyAlert
|
fireHealthCheckUnhealthyAlert
|
||||||
} from "#dynamic/lib/alerts";
|
} from "#dynamic/lib/alerts";
|
||||||
|
import { fireResourceHealthyAlert, fireResourceUnhealthyAlert } from "@server/private/lib/alerts/events/resourceEvents";
|
||||||
|
|
||||||
interface TargetHealthStatus {
|
interface TargetHealthStatus {
|
||||||
status: string;
|
status: string;
|
||||||
@@ -96,10 +97,12 @@ export const handleHealthcheckStatusMessage: MessageHandler = async (
|
|||||||
targetHealthCheckId: targetHealthCheck.targetHealthCheckId,
|
targetHealthCheckId: targetHealthCheck.targetHealthCheckId,
|
||||||
resourceOrgId: resources.orgId,
|
resourceOrgId: resources.orgId,
|
||||||
resourceId: resources.resourceId,
|
resourceId: resources.resourceId,
|
||||||
|
resourceName: resources.name,
|
||||||
name: targetHealthCheck.name,
|
name: targetHealthCheck.name,
|
||||||
hcStatus: targetHealthCheck.hcHealth
|
hcHealth: targetHealthCheck.hcHealth
|
||||||
})
|
})
|
||||||
.from(targetHealthCheck)
|
.from(targetHealthCheck)
|
||||||
|
.innerJoin(sites, eq(targetHealthCheck.siteId, sites.siteId))
|
||||||
.innerJoin(
|
.innerJoin(
|
||||||
targets,
|
targets,
|
||||||
eq(targetHealthCheck.targetId, targets.targetId)
|
eq(targetHealthCheck.targetId, targets.targetId)
|
||||||
@@ -108,7 +111,6 @@ export const handleHealthcheckStatusMessage: MessageHandler = async (
|
|||||||
resources,
|
resources,
|
||||||
eq(targets.resourceId, resources.resourceId)
|
eq(targets.resourceId, resources.resourceId)
|
||||||
)
|
)
|
||||||
.innerJoin(sites, eq(targets.siteId, sites.siteId))
|
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
eq(targetHealthCheck.targetHealthCheckId, targetIdNum),
|
eq(targetHealthCheck.targetHealthCheckId, targetIdNum),
|
||||||
@@ -126,7 +128,7 @@ export const handleHealthcheckStatusMessage: MessageHandler = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
// check if the status has changed
|
// check if the status has changed
|
||||||
if (targetCheck.hcStatus === healthStatus.status) {
|
if (targetCheck.hcHealth === healthStatus.status) {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`Health status for target ${targetId} is already ${healthStatus.status}, skipping update`
|
`Health status for target ${targetId} is already ${healthStatus.status}, skipping update`
|
||||||
);
|
);
|
||||||
@@ -178,7 +180,7 @@ export const handleHealthcheckStatusMessage: MessageHandler = async (
|
|||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
eq(targets.resourceId, targetCheck.resourceId),
|
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,
|
status: status,
|
||||||
timestamp: Math.floor(Date.now() / 1000)
|
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
|
// because we are checking above if there was a change we can fire the alert here because it changed
|
||||||
if (healthStatus.status === "unhealthy") {
|
if (healthStatus.status === "unhealthy") {
|
||||||
await fireHealthCheckHealthyAlert(
|
await fireHealthCheckUnhealthyAlert(
|
||||||
orgId,
|
orgId,
|
||||||
targetCheck.targetHealthCheckId,
|
targetCheck.targetHealthCheckId,
|
||||||
targetCheck.name
|
targetCheck.name
|
||||||
);
|
);
|
||||||
} else if (healthStatus.status === "healthy") {
|
} else if (healthStatus.status === "healthy") {
|
||||||
await fireHealthCheckNotHealthyAlert(
|
await fireHealthCheckHealthyAlert(
|
||||||
orgId,
|
orgId,
|
||||||
targetCheck.targetHealthCheckId,
|
targetCheck.targetHealthCheckId,
|
||||||
targetCheck.name
|
targetCheck.name
|
||||||
|
|||||||
@@ -32,8 +32,8 @@ const updateTargetBodySchema = z
|
|||||||
hcMode: z.string().optional().nullable(),
|
hcMode: z.string().optional().nullable(),
|
||||||
hcHostname: z.string().optional().nullable(),
|
hcHostname: z.string().optional().nullable(),
|
||||||
hcPort: z.int().positive().optional().nullable(),
|
hcPort: z.int().positive().optional().nullable(),
|
||||||
hcInterval: z.int().positive().min(5).optional().nullable(),
|
hcInterval: z.int().positive().min(1).optional().nullable(),
|
||||||
hcUnhealthyInterval: z.int().positive().min(5).optional().nullable(),
|
hcUnhealthyInterval: z.int().positive().min(1).optional().nullable(),
|
||||||
hcTimeout: z.int().positive().min(1).optional().nullable(),
|
hcTimeout: z.int().positive().min(1).optional().nullable(),
|
||||||
hcHeaders: z
|
hcHeaders: z
|
||||||
.array(z.strictObject({ name: z.string(), value: z.string() }))
|
.array(z.strictObject({ name: z.string(), value: z.string() }))
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { Card, CardContent } from "@app/components/ui/card";
|
|||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
FormField,
|
FormField,
|
||||||
FormItem,
|
FormItem,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
@@ -205,6 +206,38 @@ export default function AlertRuleGraphEditor({
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="cooldownSeconds"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
{t("alertingRuleCooldown")}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min={0}
|
||||||
|
step={1}
|
||||||
|
{...field}
|
||||||
|
value={field.value}
|
||||||
|
onChange={(e) =>
|
||||||
|
field.onChange(
|
||||||
|
Number(
|
||||||
|
e.target
|
||||||
|
.value
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
{t("alertingRuleCooldownDescription")}
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="enabled"
|
name="enabled"
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ export type AlertRuleFormAction =
|
|||||||
export type AlertRuleFormValues = {
|
export type AlertRuleFormValues = {
|
||||||
name: string;
|
name: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
|
cooldownSeconds: number;
|
||||||
sourceType: "site" | "health_check" | "resource";
|
sourceType: "site" | "health_check" | "resource";
|
||||||
allSites: boolean;
|
allSites: boolean;
|
||||||
siteIds: number[];
|
siteIds: number[];
|
||||||
@@ -66,6 +67,7 @@ export type AlertRuleFormValues = {
|
|||||||
|
|
||||||
export type AlertRuleApiPayload = {
|
export type AlertRuleApiPayload = {
|
||||||
name: string;
|
name: string;
|
||||||
|
cooldownSeconds: number;
|
||||||
eventType:
|
eventType:
|
||||||
| "site_online"
|
| "site_online"
|
||||||
| "site_offline"
|
| "site_offline"
|
||||||
@@ -141,6 +143,7 @@ export function buildFormSchema(t: (k: string) => string) {
|
|||||||
.string()
|
.string()
|
||||||
.min(1, { message: t("alertingErrorNameRequired") }),
|
.min(1, { message: t("alertingErrorNameRequired") }),
|
||||||
enabled: z.boolean(),
|
enabled: z.boolean(),
|
||||||
|
cooldownSeconds: z.number().int().nonnegative().default(0),
|
||||||
sourceType: z.enum(["site", "health_check", "resource"]),
|
sourceType: z.enum(["site", "health_check", "resource"]),
|
||||||
allSites: z.boolean().default(true),
|
allSites: z.boolean().default(true),
|
||||||
siteIds: z.array(z.number()).default([]),
|
siteIds: z.array(z.number()).default([]),
|
||||||
@@ -309,6 +312,7 @@ export function defaultFormValues(): AlertRuleFormValues {
|
|||||||
return {
|
return {
|
||||||
name: "",
|
name: "",
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
cooldownSeconds: 0,
|
||||||
sourceType: "site",
|
sourceType: "site",
|
||||||
allSites: true,
|
allSites: true,
|
||||||
siteIds: [],
|
siteIds: [],
|
||||||
@@ -422,6 +426,7 @@ export function apiResponseToFormValues(
|
|||||||
return {
|
return {
|
||||||
name: rule.name,
|
name: rule.name,
|
||||||
enabled: rule.enabled,
|
enabled: rule.enabled,
|
||||||
|
cooldownSeconds: rule.cooldownSeconds ?? 0,
|
||||||
sourceType,
|
sourceType,
|
||||||
allSites,
|
allSites,
|
||||||
siteIds: rule.siteIds,
|
siteIds: rule.siteIds,
|
||||||
@@ -483,6 +488,7 @@ export function formValuesToApiPayload(
|
|||||||
name: values.name.trim(),
|
name: values.name.trim(),
|
||||||
eventType,
|
eventType,
|
||||||
enabled: values.enabled,
|
enabled: values.enabled,
|
||||||
|
cooldownSeconds: values.cooldownSeconds,
|
||||||
allSites: values.allSites,
|
allSites: values.allSites,
|
||||||
siteIds: values.allSites ? [] : values.siteIds,
|
siteIds: values.allSites ? [] : values.siteIds,
|
||||||
allHealthChecks: values.allHealthChecks,
|
allHealthChecks: values.allHealthChecks,
|
||||||
|
|||||||
Reference in New Issue
Block a user