Prüfung / Lint, Typen, Tests (push) Canceled after 0s
Prüfung / Images bauen (pdf) (push) Canceled after 0s
Prüfung / Images bauen (web) (push) Canceled after 0s
Prüfung / Images bauen (worker) (push) Canceled after 0s
Images / bauen (pdf, docker/pdf.Dockerfile) (push) Canceled after 0s
Images / bauen (web, docker/web.Dockerfile) (push) Canceled after 0s
Images / bauen (worker, docker/worker.Dockerfile) (push) Canceled after 0s
Die Adresse zeigte auf /var/run/clamav/clamd.sock in einem geteilten Volume. Das offizielle clamav-Image legt seinen Unix-Socket aber unter /tmp im eigenen Container an - nachgemessen, das Verzeichnis im Volume bleibt leer. Jeder Verbindungsversuch lief damit in ENOENT, und die Statusseite meldete die Virenpruefung dauerhaft als gestoert. Folgenreich war das nicht nur fuer die Anzeige: ohne erreichbaren Scanner bleiben Uploads auf "Pruefung unvollstaendig" liegen und brauchen die Freigabe von Hand. Die Kette hat also gehalten, aber niemand kam durch. Dasselbe Image oeffnet ab Werk TCPSocket 3310. Darueber laeuft die Anbindung jetzt; der Port ist im Stack nicht veroeffentlicht und nur im internen Netz erreichbar. CLAMAV_ADDRESS nimmt weiterhin einen Pfad an, falls irgendwo doch ein Unix-Socket erreichbar ist. Das Socket-Volume faellt weg, es enthielt nie etwas. Der Scanner sieht ausserdem die Dateiablage nicht mehr: uebertragen wird per INSTREAM durch die Verbindung, gelesen hat er dort nie etwas. Geprueft gegen ein laufendes clamd, einschliesslich EICAR. Diese Pruefung bleibt als abschaltbarer Teil der Suite stehen: CLAMAV_LIVE=clamav:3310 pnpm --filter @afis/web test pruefkette Die uebrigen neuen Pruefungen decken das Zerlegen der Adresse ab. Sie haetten den Fehler nicht gefunden - eine Adresse, die auf nichts zeigt, ist syntaktisch tadellos. Nur eine echte Verbindung beantwortet das.
78 lines
2.5 KiB
TypeScript
78 lines
2.5 KiB
TypeScript
import { db } from '@/lib/db'
|
|
import { getEnv } from '@/lib/env'
|
|
import { pingClamav } from '@/lib/files/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_ADDRESS)
|
|
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<HealthReport> {
|
|
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 },
|
|
}
|
|
}
|