Files
cloudflare-domain-manager/apps/api/src/app.ts
T
Denozordec 9783974949
Build and Push CFDM Docker Image / build-and-push (push) Successful in 2m0s
Build and Push CFDM Docker Image / create-release (push) Skipped
Build and Push CFDM Docker Image / update-wiki (push) Successful in 6s
feat(health-check): introduce health probe gap configuration and enhance health check logic
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.
2026-07-20 03:57:37 +07:00

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;
}