98 lines
3.7 KiB
TypeScript
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' }
|
|
}
|
|
}
|
|
}
|