mirror of
https://github.com/fosrl/pangolin.git
synced 2026-05-26 02:32:36 +00:00
Transititioning the hc table and firing the alerts
This commit is contained in:
@@ -186,9 +186,13 @@ export const targets = pgTable("targets", {
|
|||||||
|
|
||||||
export const targetHealthCheck = pgTable("targetHealthCheck", {
|
export const targetHealthCheck = pgTable("targetHealthCheck", {
|
||||||
targetHealthCheckId: serial("targetHealthCheckId").primaryKey(),
|
targetHealthCheckId: serial("targetHealthCheckId").primaryKey(),
|
||||||
targetId: integer("targetId")
|
targetId: integer("targetId").references(() => targets.targetId, {
|
||||||
.notNull()
|
onDelete: "cascade"
|
||||||
.references(() => targets.targetId, { onDelete: "cascade" }),
|
}),
|
||||||
|
orgId: varchar("orgId").references(() => orgs.orgId, {
|
||||||
|
onDelete: "cascade"
|
||||||
|
}),
|
||||||
|
name: varchar("name"),
|
||||||
hcEnabled: boolean("hcEnabled").notNull().default(false),
|
hcEnabled: boolean("hcEnabled").notNull().default(false),
|
||||||
hcPath: varchar("hcPath"),
|
hcPath: varchar("hcPath"),
|
||||||
hcScheme: varchar("hcScheme"),
|
hcScheme: varchar("hcScheme"),
|
||||||
|
|||||||
@@ -210,8 +210,11 @@ export const targetHealthCheck = sqliteTable("targetHealthCheck", {
|
|||||||
autoIncrement: true
|
autoIncrement: true
|
||||||
}),
|
}),
|
||||||
targetId: integer("targetId")
|
targetId: integer("targetId")
|
||||||
.notNull()
|
|
||||||
.references(() => targets.targetId, { onDelete: "cascade" }),
|
.references(() => targets.targetId, { onDelete: "cascade" }),
|
||||||
|
orgId: text("orgId").references(() => orgs.orgId, {
|
||||||
|
onDelete: "cascade"
|
||||||
|
}),
|
||||||
|
name: text("name").notNull(),
|
||||||
hcEnabled: integer("hcEnabled", { mode: "boolean" })
|
hcEnabled: integer("hcEnabled", { mode: "boolean" })
|
||||||
.notNull()
|
.notNull()
|
||||||
.default(false),
|
.default(false),
|
||||||
|
|||||||
@@ -201,6 +201,7 @@ export async function buildTargetConfigurationForNewtClient(siteId: number) {
|
|||||||
internalPort: targets.internalPort,
|
internalPort: targets.internalPort,
|
||||||
enabled: targets.enabled,
|
enabled: targets.enabled,
|
||||||
protocol: resources.protocol,
|
protocol: resources.protocol,
|
||||||
|
hcId: targetHealthCheck.targetHealthCheckId,
|
||||||
hcEnabled: targetHealthCheck.hcEnabled,
|
hcEnabled: targetHealthCheck.hcEnabled,
|
||||||
hcPath: targetHealthCheck.hcPath,
|
hcPath: targetHealthCheck.hcPath,
|
||||||
hcScheme: targetHealthCheck.hcScheme,
|
hcScheme: targetHealthCheck.hcScheme,
|
||||||
@@ -272,6 +273,7 @@ export async function buildTargetConfigurationForNewtClient(siteId: number) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
id: target.targetId,
|
id: target.targetId,
|
||||||
|
hcId: target.hcId,
|
||||||
hcEnabled: target.hcEnabled,
|
hcEnabled: target.hcEnabled,
|
||||||
hcPath: target.hcPath,
|
hcPath: target.hcPath,
|
||||||
hcScheme: target.hcScheme,
|
hcScheme: target.hcScheme,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
} from "#dynamic/routers/ws";
|
} from "#dynamic/routers/ws";
|
||||||
import { eq, lt, isNull, and, or, ne, not } from "drizzle-orm";
|
import { eq, lt, isNull, and, or, ne, not } from "drizzle-orm";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { fireSiteOfflineAlert } from "#dynamic/lib/alerts";
|
import { fireSiteOfflineAlert, fireSiteOnlineAlert } from "#dynamic/lib/alerts";
|
||||||
|
|
||||||
// Track if the offline checker interval is running
|
// Track if the offline checker interval is running
|
||||||
let offlineCheckerInterval: NodeJS.Timeout | null = null;
|
let offlineCheckerInterval: NodeJS.Timeout | null = null;
|
||||||
@@ -101,6 +101,8 @@ export const startNewtOfflineChecker = (): void => {
|
|||||||
.targetHealthCheckId
|
.targetHealthCheckId
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// TODO: should we be firing an alert here when the health check goes to unknown?
|
||||||
}
|
}
|
||||||
|
|
||||||
await fireSiteOfflineAlert(staleSite.orgId, staleSite.siteId, staleSite.name);
|
await fireSiteOfflineAlert(staleSite.orgId, staleSite.siteId, staleSite.name);
|
||||||
@@ -111,6 +113,8 @@ export const startNewtOfflineChecker = (): void => {
|
|||||||
const allWireguardSites = await db
|
const allWireguardSites = await db
|
||||||
.select({
|
.select({
|
||||||
siteId: sites.siteId,
|
siteId: sites.siteId,
|
||||||
|
orgId: sites.orgId,
|
||||||
|
name: sites.name,
|
||||||
online: sites.online,
|
online: sites.online,
|
||||||
lastBandwidthUpdate: sites.lastBandwidthUpdate
|
lastBandwidthUpdate: sites.lastBandwidthUpdate
|
||||||
})
|
})
|
||||||
@@ -142,6 +146,8 @@ export const startNewtOfflineChecker = (): void => {
|
|||||||
.update(sites)
|
.update(sites)
|
||||||
.set({ online: false })
|
.set({ online: false })
|
||||||
.where(eq(sites.siteId, site.siteId));
|
.where(eq(sites.siteId, site.siteId));
|
||||||
|
|
||||||
|
await fireSiteOfflineAlert(site.orgId, site.siteId, site.name);
|
||||||
} else if (
|
} else if (
|
||||||
lastBandwidthUpdate >= wireguardOfflineThreshold &&
|
lastBandwidthUpdate >= wireguardOfflineThreshold &&
|
||||||
!site.online
|
!site.online
|
||||||
@@ -154,6 +160,8 @@ export const startNewtOfflineChecker = (): void => {
|
|||||||
.update(sites)
|
.update(sites)
|
||||||
.set({ online: true })
|
.set({ online: true })
|
||||||
.where(eq(sites.siteId, site.siteId));
|
.where(eq(sites.siteId, site.siteId));
|
||||||
|
|
||||||
|
await fireSiteOnlineAlert(site.orgId, site.siteId, site.name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { db } from "@server/db";
|
import { db } from "@server/db";
|
||||||
import { sites, clients, olms } from "@server/db";
|
import { sites, clients, olms } from "@server/db";
|
||||||
import { inArray } from "drizzle-orm";
|
import { and, eq, inArray } from "drizzle-orm";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
|
import { fireSiteOnlineAlert } from "#dynamic/lib/alerts";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ping Accumulator
|
* Ping Accumulator
|
||||||
@@ -110,15 +111,44 @@ async function flushSitePingsToDb(): Promise<void> {
|
|||||||
const siteIds = batch.map(([id]) => id);
|
const siteIds = batch.map(([id]) => id);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await withRetry(async () => {
|
const newlyOnlineSites = await withRetry(async () => {
|
||||||
await db
|
// Only update sites that were offline — these are the
|
||||||
|
// offline→online transitions. .returning() gives us exactly
|
||||||
|
// the site IDs that changed state.
|
||||||
|
const transitioned = await db
|
||||||
.update(sites)
|
.update(sites)
|
||||||
.set({
|
.set({
|
||||||
online: true,
|
online: true,
|
||||||
lastPing: maxTimestamp
|
lastPing: maxTimestamp
|
||||||
})
|
})
|
||||||
.where(inArray(sites.siteId, siteIds));
|
.where(
|
||||||
|
and(
|
||||||
|
inArray(sites.siteId, siteIds),
|
||||||
|
eq(sites.online, false)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.returning({ siteId: sites.siteId, orgId: sites.orgId, name: sites.name });
|
||||||
|
|
||||||
|
// Update lastPing for sites that were already online.
|
||||||
|
// After the update above, the newly-online sites now have
|
||||||
|
// online = true, so this catches all remaining sites in the
|
||||||
|
// batch and keeps lastPing current for them too.
|
||||||
|
await db
|
||||||
|
.update(sites)
|
||||||
|
.set({ lastPing: maxTimestamp })
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
inArray(sites.siteId, siteIds),
|
||||||
|
eq(sites.online, true)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return transitioned;
|
||||||
}, "flushSitePingsToDb");
|
}, "flushSitePingsToDb");
|
||||||
|
|
||||||
|
for (const site of newlyOnlineSites) {
|
||||||
|
await fireSiteOnlineAlert(site.orgId, site.siteId, site.name);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(
|
logger.error(
|
||||||
`Failed to flush site ping batch (${batch.length} sites), re-queuing for next cycle`,
|
`Failed to flush site ping batch (${batch.length} sites), re-queuing for next cycle`,
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { Target, TargetHealthCheck, db, targetHealthCheck } from "@server/db";
|
import { Target, TargetHealthCheck } from "@server/db";
|
||||||
import { sendToClient } from "#dynamic/routers/ws";
|
import { sendToClient } from "#dynamic/routers/ws";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { eq, inArray } from "drizzle-orm";
|
|
||||||
import { canCompress } from "@server/lib/clientVersionChecks";
|
import { canCompress } from "@server/lib/clientVersionChecks";
|
||||||
|
|
||||||
export async function addTargets(
|
export async function addTargets(
|
||||||
@@ -18,17 +17,23 @@ export async function addTargets(
|
|||||||
}:${target.port}`;
|
}:${target.port}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
await sendToClient(newtId, {
|
await sendToClient(
|
||||||
type: `newt/${protocol}/add`,
|
newtId,
|
||||||
data: {
|
{
|
||||||
targets: payloadTargets
|
type: `newt/${protocol}/add`,
|
||||||
}
|
data: {
|
||||||
}, { incrementConfigVersion: true, compress: canCompress(version, "newt") });
|
targets: payloadTargets
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ incrementConfigVersion: true, compress: canCompress(version, "newt") }
|
||||||
|
);
|
||||||
|
|
||||||
// Create a map for quick lookup
|
// Create a map for quick lookup
|
||||||
const healthCheckMap = new Map<number, TargetHealthCheck>();
|
const healthCheckMap = new Map<number, TargetHealthCheck>();
|
||||||
healthCheckData.forEach((hc) => {
|
healthCheckData.forEach((hc) => {
|
||||||
healthCheckMap.set(hc.targetId, hc);
|
if (hc.targetId !== null) {
|
||||||
|
healthCheckMap.set(hc.targetId, hc);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const healthCheckTargets = targets.map((target) => {
|
const healthCheckTargets = targets.map((target) => {
|
||||||
@@ -79,6 +84,7 @@ export async function addTargets(
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
id: target.targetId,
|
id: target.targetId,
|
||||||
|
hcId: hc.targetHealthCheckId,
|
||||||
hcEnabled: hc.hcEnabled,
|
hcEnabled: hc.hcEnabled,
|
||||||
hcPath: hc.hcPath,
|
hcPath: hc.hcPath,
|
||||||
hcScheme: hc.hcScheme,
|
hcScheme: hc.hcScheme,
|
||||||
@@ -102,12 +108,16 @@ export async function addTargets(
|
|||||||
(target) => target !== null
|
(target) => target !== null
|
||||||
);
|
);
|
||||||
|
|
||||||
await sendToClient(newtId, {
|
await sendToClient(
|
||||||
type: `newt/healthcheck/add`,
|
newtId,
|
||||||
data: {
|
{
|
||||||
targets: validHealthCheckTargets
|
type: `newt/healthcheck/add`,
|
||||||
}
|
data: {
|
||||||
}, { incrementConfigVersion: true, compress: canCompress(version, "newt") });
|
targets: validHealthCheckTargets
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ incrementConfigVersion: true, compress: canCompress(version, "newt") }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function removeTargets(
|
export async function removeTargets(
|
||||||
@@ -123,21 +133,29 @@ export async function removeTargets(
|
|||||||
}:${target.port}`;
|
}:${target.port}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
await sendToClient(newtId, {
|
await sendToClient(
|
||||||
type: `newt/${protocol}/remove`,
|
newtId,
|
||||||
data: {
|
{
|
||||||
targets: payloadTargets
|
type: `newt/${protocol}/remove`,
|
||||||
}
|
data: {
|
||||||
}, { incrementConfigVersion: true });
|
targets: payloadTargets
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ incrementConfigVersion: true }
|
||||||
|
);
|
||||||
|
|
||||||
const healthCheckTargets = targets.map((target) => {
|
const healthCheckTargets = targets.map((target) => {
|
||||||
return target.targetId;
|
return target.targetId;
|
||||||
});
|
});
|
||||||
|
|
||||||
await sendToClient(newtId, {
|
await sendToClient(
|
||||||
type: `newt/healthcheck/remove`,
|
newtId,
|
||||||
data: {
|
{
|
||||||
ids: healthCheckTargets
|
type: `newt/healthcheck/remove`,
|
||||||
}
|
data: {
|
||||||
}, { incrementConfigVersion: true, compress: canCompress(version, "newt") });
|
ids: healthCheckTargets
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ incrementConfigVersion: true, compress: canCompress(version, "newt") }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -275,8 +275,8 @@ export async function createTarget(
|
|||||||
|
|
||||||
return response<CreateTargetResponse>(res, {
|
return response<CreateTargetResponse>(res, {
|
||||||
data: {
|
data: {
|
||||||
...newTarget[0],
|
...healthCheck[0],
|
||||||
...healthCheck[0]
|
...newTarget[0]
|
||||||
},
|
},
|
||||||
success: true,
|
success: true,
|
||||||
error: false,
|
error: false,
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ const getTargetSchema = z.strictObject({
|
|||||||
});
|
});
|
||||||
|
|
||||||
type GetTargetResponse = Target &
|
type GetTargetResponse = Target &
|
||||||
Omit<TargetHealthCheck, "hcHeaders"> & {
|
Partial<Omit<TargetHealthCheck, "hcHeaders" | "targetId">> & {
|
||||||
hcHeaders: { name: string; value: string }[] | null;
|
hcHeaders: { name: string; value: string }[] | null | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
registry.registerPath({
|
registry.registerPath({
|
||||||
@@ -70,20 +70,19 @@ export async function getTarget(
|
|||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
// Parse hcHeaders from JSON string back to array
|
// Parse hcHeaders from JSON string back to array
|
||||||
let parsedHcHeaders = null;
|
let parsedHcHeaders: { name: string; value: string }[] | null = null;
|
||||||
if (targetHc?.hcHeaders) {
|
if (targetHc?.hcHeaders) {
|
||||||
try {
|
try {
|
||||||
parsedHcHeaders = JSON.parse(targetHc.hcHeaders);
|
parsedHcHeaders = JSON.parse(targetHc.hcHeaders);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// If parsing fails, keep as string for backward compatibility
|
// If parsing fails, keep as null for safety
|
||||||
parsedHcHeaders = targetHc.hcHeaders;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return response<GetTargetResponse>(res, {
|
return response<GetTargetResponse>(res, {
|
||||||
data: {
|
data: {
|
||||||
...target[0],
|
|
||||||
...targetHc,
|
...targetHc,
|
||||||
|
...target[0],
|
||||||
hcHeaders: parsedHcHeaders
|
hcHeaders: parsedHcHeaders
|
||||||
},
|
},
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
@@ -3,7 +3,10 @@ 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 } from "drizzle-orm";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { unknown } from "zod";
|
import {
|
||||||
|
fireHealthCheckHealthyAlert,
|
||||||
|
fireHealthCheckNotHealthyAlert
|
||||||
|
} from "#dynamic/lib/alerts";
|
||||||
|
|
||||||
interface TargetHealthStatus {
|
interface TargetHealthStatus {
|
||||||
status: string;
|
status: string;
|
||||||
@@ -11,7 +14,7 @@ interface TargetHealthStatus {
|
|||||||
checkCount: number;
|
checkCount: number;
|
||||||
lastError?: string;
|
lastError?: string;
|
||||||
config: {
|
config: {
|
||||||
id: string;
|
id: string; // this could be the hc id or the target id, depending on the version of newt
|
||||||
hcEnabled: boolean;
|
hcEnabled: boolean;
|
||||||
hcPath?: string;
|
hcPath?: string;
|
||||||
hcScheme?: string;
|
hcScheme?: string;
|
||||||
@@ -23,6 +26,9 @@ interface TargetHealthStatus {
|
|||||||
hcTimeout?: number;
|
hcTimeout?: number;
|
||||||
hcHeaders?: any;
|
hcHeaders?: any;
|
||||||
hcMethod?: string;
|
hcMethod?: string;
|
||||||
|
hcTlsServerName?: string;
|
||||||
|
hcHealthyThreshold?: number;
|
||||||
|
hcUnhealthyThreshold?: number;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,6 +84,10 @@ export const handleHealthcheckStatusMessage: MessageHandler = async (
|
|||||||
.select({
|
.select({
|
||||||
targetId: targets.targetId,
|
targetId: targets.targetId,
|
||||||
siteId: targets.siteId,
|
siteId: targets.siteId,
|
||||||
|
orgId: targetHealthCheck.orgId,
|
||||||
|
targetHealthCheckId: targetHealthCheck.targetHealthCheckId,
|
||||||
|
resourceOrgId: resources.orgId,
|
||||||
|
name: targetHealthCheck.name,
|
||||||
hcStatus: targetHealthCheck.hcHealth
|
hcStatus: targetHealthCheck.hcHealth
|
||||||
})
|
})
|
||||||
.from(targets)
|
.from(targets)
|
||||||
@@ -86,7 +96,10 @@ export const handleHealthcheckStatusMessage: MessageHandler = async (
|
|||||||
eq(targets.resourceId, resources.resourceId)
|
eq(targets.resourceId, resources.resourceId)
|
||||||
)
|
)
|
||||||
.innerJoin(sites, eq(targets.siteId, sites.siteId))
|
.innerJoin(sites, eq(targets.siteId, sites.siteId))
|
||||||
.innerJoin(targetHealthCheck, eq(targets.targetId, targetHealthCheck.targetId))
|
.innerJoin(
|
||||||
|
targetHealthCheck,
|
||||||
|
eq(targets.targetId, targetHealthCheck.targetId)
|
||||||
|
)
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
eq(targets.targetId, targetIdNum),
|
eq(targets.targetId, targetIdNum),
|
||||||
@@ -123,6 +136,21 @@ export const handleHealthcheckStatusMessage: MessageHandler = async (
|
|||||||
.where(eq(targetHealthCheck.targetId, targetIdNum))
|
.where(eq(targetHealthCheck.targetId, targetIdNum))
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
|
// 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(
|
||||||
|
targetCheck.orgId || targetCheck.resourceOrgId, // for backwards compatibility, check both orgId fields because the target health checks dont have the orgId
|
||||||
|
targetCheck.targetHealthCheckId,
|
||||||
|
targetCheck.name
|
||||||
|
);
|
||||||
|
} else if (healthStatus.status === "healthy") {
|
||||||
|
await fireHealthCheckNotHealthyAlert(
|
||||||
|
targetCheck.orgId || targetCheck.resourceOrgId, // for backwards compatibility, check both orgId fields because the target health checks dont have the orgId
|
||||||
|
targetCheck.targetHealthCheckId,
|
||||||
|
targetCheck.name
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`Updated health status for target ${targetId} to ${healthStatus.status}`
|
`Updated health status for target ${targetId} to ${healthStatus.status}`
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user