Added `healthProbeGapMs` configuration to control the minimum pause between probes to different physical targets. Updated health check service to utilize this configuration, ensuring efficient probing without overwhelming the targets. Enhanced the `runAllChecks` function to group probes by physical IP and implement the new gap logic. Updated related tests to validate the new functionality.
192 lines
6.3 KiB
TypeScript
192 lines
6.3 KiB
TypeScript
import { resolve } from "node:path";
|
|
import Fastify from "fastify";
|
|
import {
|
|
serializerCompiler,
|
|
validatorCompiler,
|
|
type ZodTypeProvider,
|
|
} from "@fastify/type-provider-zod";
|
|
import type { AppConfig } from "./config.js";
|
|
import { loadConfig } from "./config.js";
|
|
import { repos } from "@cfdm/db";
|
|
import authPlugin from "./plugins/auth.js";
|
|
import cfClientPlugin from "./plugins/cf-client.js";
|
|
import { requireAuth } from "./plugins/auth.js";
|
|
import corsPlugin from "./plugins/cors.js";
|
|
import dbPlugin from "./plugins/db.js";
|
|
import errorHandlerPlugin from "./plugins/error-handler.js";
|
|
import { authRoutes, healthRoutes } from "./routes/health.js";
|
|
import { groupRoutes } from "./routes/groups.js";
|
|
import { serviceRoutes } from "./routes/services.js";
|
|
import { serviceGroupRoutes } from "./routes/service-groups.js";
|
|
import { serviceBindingRoutes } from "./routes/service-bindings.js";
|
|
import { domainRoutes } from "./routes/domains.js";
|
|
import { dnsRoutes } from "./routes/dns.js";
|
|
import { subdomainRoutes } from "./routes/subdomains.js";
|
|
import { certificateRoutes } from "./routes/certificates.js";
|
|
import { syncRoutes } from "./routes/sync.js";
|
|
import { healthCheckRoutes } from "./routes/health-check.js";
|
|
import {
|
|
domainMonitorRoutes,
|
|
notificationRoutes,
|
|
} from "./routes/domain-monitors.js";
|
|
import { settingsRoutes } from "./routes/settings.js";
|
|
import { integrationsVpsTrackerRoutes } from "./routes/integrations-vps-tracker.js";
|
|
import * as certificateService from "./services/certificate-service.js";
|
|
import * as healthCheckService from "./services/health-check-service.js";
|
|
import * as serviceConfigService from "./services/service-config-service.js";
|
|
import { AsyncTask, CronJob } from "toad-scheduler";
|
|
|
|
export interface BuildAppOptions {
|
|
config?: AppConfig;
|
|
memory?: boolean;
|
|
}
|
|
|
|
export async function buildApp(opts: BuildAppOptions = {}) {
|
|
const config = opts.config ?? loadConfig();
|
|
|
|
const app = Fastify({
|
|
logger: { level: config.logLevel },
|
|
}).withTypeProvider<ZodTypeProvider>();
|
|
|
|
app.setValidatorCompiler(validatorCompiler);
|
|
app.setSerializerCompiler(serializerCompiler);
|
|
|
|
await app.register(import("@fastify/sensible"));
|
|
await app.register(import("@fastify/helmet"), { contentSecurityPolicy: false });
|
|
await app.register(import("@fastify/rate-limit"), {
|
|
max: 300,
|
|
timeWindow: "1 minute",
|
|
});
|
|
await app.register(corsPlugin);
|
|
await app.register(errorHandlerPlugin);
|
|
await app.register(dbPlugin, { config, memory: opts.memory });
|
|
await app.register(cfClientPlugin, { config });
|
|
await app.register(authPlugin, { config });
|
|
|
|
await app.register(healthRoutes);
|
|
await app.register(authRoutes, { prefix: "/api/v1" });
|
|
await app.register(integrationsVpsTrackerRoutes, { prefix: "/api/v1" });
|
|
|
|
await app.register(
|
|
async (protectedApi) => {
|
|
protectedApi.addHook("onRequest", requireAuth);
|
|
await protectedApi.register(groupRoutes);
|
|
await protectedApi.register(serviceRoutes);
|
|
await protectedApi.register(serviceGroupRoutes);
|
|
await protectedApi.register(serviceBindingRoutes);
|
|
await protectedApi.register(domainRoutes);
|
|
await protectedApi.register(dnsRoutes);
|
|
await protectedApi.register(subdomainRoutes);
|
|
await protectedApi.register(certificateRoutes);
|
|
await protectedApi.register(syncRoutes);
|
|
await protectedApi.register(healthCheckRoutes);
|
|
await protectedApi.register(domainMonitorRoutes);
|
|
await protectedApi.register(notificationRoutes);
|
|
await protectedApi.register(settingsRoutes);
|
|
},
|
|
{ prefix: "/api/v1" },
|
|
);
|
|
|
|
const staticDir = config.staticDir ?? resolve(process.cwd(), "static");
|
|
if (config.staticDir !== null) {
|
|
await app.register(import("@fastify/static"), {
|
|
root: staticDir,
|
|
wildcard: false,
|
|
});
|
|
|
|
app.setNotFoundHandler(async (_request, reply) => {
|
|
return reply.sendFile("index.html");
|
|
});
|
|
}
|
|
|
|
if (!opts.memory) {
|
|
await app.register(import("@fastify/schedule"));
|
|
|
|
const certTask = new AsyncTask(
|
|
"certificate-check",
|
|
async () => {
|
|
const n = await certificateService.runAllChecks(app.db);
|
|
app.log.info({ checked: n }, "certificate check completed");
|
|
},
|
|
(err) => {
|
|
app.log.warn({ err }, "certificate check failed");
|
|
},
|
|
);
|
|
|
|
app.scheduler.addCronJob(
|
|
new CronJob(
|
|
{ cronExpression: config.certCheckCron },
|
|
certTask,
|
|
{ preventOverrun: true },
|
|
),
|
|
);
|
|
|
|
const healthTask = new AsyncTask(
|
|
"health-check",
|
|
async () => {
|
|
const thresholds = {
|
|
degradedFailures: config.healthDegradedFailures,
|
|
downFailures: config.healthDownFailures,
|
|
latencyWarnMs: config.healthLatencyWarnMs,
|
|
};
|
|
const n = await healthCheckService.runAllChecks(app.db, {
|
|
thresholds,
|
|
probeGapMs: config.healthProbeGapMs,
|
|
onStatusChange: async (target, prev, next) => {
|
|
try {
|
|
const label =
|
|
next === "up"
|
|
? "OK"
|
|
: next === "degraded"
|
|
? "Slow"
|
|
: next === "down"
|
|
? "Down"
|
|
: "—";
|
|
repos.insertNotificationLog(
|
|
app.db,
|
|
"ip_health",
|
|
target.scope,
|
|
target.ref_id,
|
|
`${target.hostname || target.ip}: ${label}`,
|
|
`IP ${target.ip}: ${prev ?? "—"} → ${label}`,
|
|
);
|
|
await serviceConfigService.reconcileDnsForTarget(
|
|
app.db,
|
|
app.cf,
|
|
target.scope,
|
|
target.ref_id,
|
|
);
|
|
} catch (err) {
|
|
app.log.warn(
|
|
{ err, scope: target.scope, refId: target.ref_id },
|
|
"health-check reconcile failed",
|
|
);
|
|
}
|
|
},
|
|
});
|
|
const monitors = await healthCheckService.runDomainMonitors(
|
|
app.db,
|
|
thresholds,
|
|
);
|
|
app.log.info(
|
|
{ checked: n, monitors },
|
|
"health check completed",
|
|
);
|
|
},
|
|
(err) => {
|
|
app.log.warn({ err }, "health check failed");
|
|
},
|
|
);
|
|
|
|
app.scheduler.addCronJob(
|
|
new CronJob(
|
|
{ cronExpression: config.healthCheckCron },
|
|
healthTask,
|
|
{ preventOverrun: true },
|
|
),
|
|
);
|
|
}
|
|
|
|
return app;
|
|
}
|