shared/lib/alert/health-checker.ts

98 lines
3.7 KiB
TypeScript

// shared/lib/alert/health-checker.ts — 健康检查引擎
import type { CheckType, HealthCheckConfig, CheckResult } from './types'
export interface Checker {
check(config: HealthCheckConfig): Promise<CheckResult>
}
export class HealthChecker {
private checkers = new Map<CheckType, Checker>()
registerChecker(type: CheckType, handler: Checker): void {
this.checkers.set(type, handler)
}
// 并发检查所有维度
async check(checks: HealthCheckConfig[], timeoutMs: number): Promise<CheckResult[]> {
const results = await Promise.allSettled(
checks.map(config => this.checkOne(config, timeoutMs))
)
return results.map(r => (r.status === 'fulfilled' ? r.value : {
success: false,
status: 'unknown' as const,
error: 'Checker execution failed',
latency: timeoutMs,
checkType: 'http' as const,
}))
}
private async checkOne(config: HealthCheckConfig, timeoutMs: number): Promise<CheckResult> {
const checker = this.checkers.get(config.type)
if (!checker) {
return { success: false, status: 'unknown', error: `Unknown check type: ${config.type}`, latency: 0, checkType: config.type }
}
return checker.check(config)
}
}
// 内置 HTTP 检查器
export class HttpChecker implements Checker {
async check(config: HealthCheckConfig): Promise<CheckResult> {
const start = Date.now()
try {
const res = await fetch(config.url!, {
signal: AbortSignal.timeout(5000),
headers: { Accept: 'text/html,application/json' },
})
const latency = Date.now() - start
if (config.expectedStatus && res.status !== config.expectedStatus) {
return { success: false, status: 'abnormal', error: `Expected ${config.expectedStatus}, got ${res.status}`, latency, checkType: 'http' }
}
if (config.expectedBody) {
const body = await res.text()
if (!body.includes(config.expectedBody)) {
return { success: false, status: 'abnormal', error: `Body does not contain "${config.expectedBody}"`, latency, checkType: 'http' }
}
}
return { success: true, status: 'normal', latency, checkType: 'http' }
} catch (err) {
return { success: false, status: 'abnormal', error: String(err), latency: Date.now() - start, checkType: 'http' }
}
}
}
// 内置 Docker 检查器(同机部署,需要 docker.sock 只读挂载)
export class DockerChecker implements Checker {
async check(config: HealthCheckConfig): Promise<CheckResult> {
const start = Date.now()
try {
const { execFileSync } = await import('child_process')
const containerName = config.containerName!
const stdout = execFileSync('docker', ['ps', '--filter', `name=${containerName}`, '--format', '{{.Status}}'], {
encoding: 'utf-8',
timeout: 5000,
}).trim()
const latency = Date.now() - start
if (!stdout) {
return { success: false, status: 'abnormal', error: `Container ${containerName} not found or stopped`, latency, checkType: 'docker' }
}
if (stdout.startsWith('Up')) {
return { success: true, status: 'normal', latency, checkType: 'docker' }
}
return { success: false, status: 'abnormal', error: `Container ${containerName} status: ${stdout}`, latency, checkType: 'docker' }
} catch (err) {
const latency = Date.now() - start
// 区分 daemon 不可达 vs 其他错误
const errStr = String(err)
if (errStr.includes('Cannot connect') || errStr.includes('Is the docker daemon')) {
return { success: false, status: 'unknown', error: 'Docker daemon unreachable', latency, checkType: 'docker' }
}
return { success: false, status: 'abnormal', error: errStr, latency, checkType: 'docker' }
}
}
}