feat: add healthchecks (#1187)

This commit is contained in:
Manuel
2024-09-28 23:02:28 +02:00
committed by GitHub
parent 8aaa4f5856
commit 08c6303fa6
7 changed files with 161 additions and 56 deletions

View File

@@ -0,0 +1,91 @@
import { performance } from "perf_hooks";
import { db } from "@homarr/db";
import { logger } from "@homarr/log";
import { handshakeAsync } from "@homarr/redis";
export async function GET() {
const timeBeforeHealthCheck = performance.now();
const response = await executeAndAggregateAllHealthChecksAsync();
logger.info(`Completed healthcheck after ${performance.now() - timeBeforeHealthCheck}ms`);
if (response.status === "healthy") {
return new Response(JSON.stringify(response), {
status: 200,
});
}
return new Response(JSON.stringify(response), {
status: 500,
});
}
const executeAndAggregateAllHealthChecksAsync = async (): Promise<{
healthChecks: Record<string, object>;
status: "healthy" | "unhealthy";
}> => {
const healthChecks = [
executeHealthCheckSafelyAsync("database", async () => {
// sqlite driver does not support raw query execution. this is for a heartbeat check only - it doesn't matter if data is returned or not
await db.query.serverSettings.findFirst();
return {};
}),
executeHealthCheckSafelyAsync("redis", async () => {
await handshakeAsync();
return {};
}),
];
const healthCheckResults = await Promise.all(healthChecks);
const anyUnhealthy = healthCheckResults.some((healthCheck) => healthCheck.status === "unhealthy");
const healthCheckValues = healthCheckResults.reduce(
(acc, healthCheck) => {
acc[healthCheck.name] = {
status: healthCheck.status,
...healthCheck.values,
};
return acc;
},
{} as Record<string, object>,
);
return {
status: anyUnhealthy ? "unhealthy" : "healthy",
healthChecks: healthCheckValues,
};
};
const executeHealthCheckSafelyAsync = async (
name: string,
callback: () => Promise<object>,
): Promise<HealthCheckResult> => {
try {
const currentTimeBeforeCallback = performance.now();
const values = await callback();
return {
name,
status: "healthy",
values: {
...values,
latency: performance.now() - currentTimeBeforeCallback,
},
};
} catch (error) {
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
logger.error(`Healthcheck '${name}' has failed: ${error}`);
return {
status: "unhealthy",
values: {
error,
},
name,
};
}
};
interface HealthCheckResult {
status: "healthy" | "unhealthy";
name: string;
values: object;
}

View File

@@ -0,0 +1,5 @@
export function GET() {
return new Response(undefined, {
status: 200,
});
}