import { db } from '@/lib/db' import { getEnv } from '@/lib/env' import { pingClamav } from '@/lib/health/clamav' export type ComponentState = 'up' | 'degraded' | 'down' export type HealthReport = { status: ComponentState checkedAt: string components: Record<'database' | 'discordGateway' | 'clamav', { status: ComponentState detail?: string }> } /** Der Worker gilt als abgehaengt, wenn sein Lebenszeichen aelter ist. */ const HEARTBEAT_MAX_AGE_MS = 3 * 60_000 async function checkDatabase(): Promise<{ status: ComponentState; detail?: string }> { try { await db.$queryRaw`SELECT 1` return { status: 'up' } } catch (error) { return { status: 'down', detail: error instanceof Error ? error.message : 'unbekannt' } } } async function checkDiscordGateway(): Promise<{ status: ComponentState; detail?: string }> { try { const beat = await db.serviceHeartbeat.findUnique({ where: { service: 'worker' } }) if (!beat) return { status: 'down', detail: 'kein Lebenszeichen' } const ageMs = Date.now() - beat.at.getTime() if (ageMs > HEARTBEAT_MAX_AGE_MS) { return { status: 'down', detail: `Lebenszeichen ${Math.round(ageMs / 1000)} s alt` } } if (beat.status !== 'up') { return { status: beat.status === 'degraded' ? 'degraded' : 'down', detail: beat.detail ?? undefined } } return { status: 'up' } } catch { return { status: 'down', detail: 'nicht abfragbar' } } } async function checkClamav(): Promise<{ status: ComponentState; detail?: string }> { const ok = await pingClamav(getEnv().CLAMAV_SOCKET) return ok ? { status: 'up' } : { status: 'down', detail: 'clamd antwortet nicht' } } /** * Sammelt den Zustand der drei Abhaengigkeiten. Die Gesamtbewertung faellt * bewusst nur auf `down`, wenn die Datenbank weg ist: Worker, clamav und pdf * haben eigene Healthchecks im Stack, und ein ausgefallener Virenscanner soll * nicht den ganzen Web-Container als tot markieren. Sichtbar bleibt der Ausfall * trotzdem, in dieser Antwort und in Dockge am jeweiligen Dienst. */ export async function collectHealth(): Promise { const [database, discordGateway, clamav] = await Promise.all([ checkDatabase(), checkDiscordGateway(), checkClamav(), ]) const status: ComponentState = database.status === 'down' ? 'down' : discordGateway.status === 'up' && clamav.status === 'up' ? 'up' : 'degraded' return { status, checkedAt: new Date().toISOString(), components: { database, discordGateway, clamav }, } }