Files
lingxi/apps/api/src/modules/platform/health/health.service.ts
T
Stev_Wang 04f49690f4
ci / 质量门禁 (push) Canceled after 0s
feat(a0): 完成工程骨架与契约基线
- 建立 pnpm workspace + Turborepo 单仓、ESLint 9/Prettier/commitlint/husky 门禁
- NestJS + Fastify API:统一错误体、trace_id、Zod 管道、/health 真实依赖探活、OpenAPI 3.1 契约
- Prisma 基线:outbox_events/idempotency_keys/system_configs 与首版迁移、幂等种子
- shared 契约包:错误码、权限点、ULID、游标分页、命名/时间工具
- ui 包与 Web 壳层:Semi + Remix 图标统一出口、Cloud Blue 视觉、登录/工作台/错误页
- worker 骨架、Playwright 冒烟、Gitea CI、OpenAPI 快照与 oasdiff 回退脚本
- 补充 A0 实施报告与本地开发说明
2026-11-02 17:01:50 +08:00

95 lines
3.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { ErrorCode, nowIso } from '@lingxi/shared';
import { Inject, Injectable } from '@nestjs/common';
import { AppException } from '../../../common/errors/app-exception';
import { APP_CONFIG } from '../../../config/config.module';
import type { AppConfig } from '../../../config/env';
import type { PrismaService } from '../../../infra/prisma/prisma.service';
import type { RedisService } from '../../../infra/redis/redis.service';
import { PRISMA } from '../../../infra/tokens';
import { REDIS } from '../../../infra/tokens';
import type { ComponentHealth, HealthReport } from './health.types';
const API_VERSION = '0.1.0';
const CHECK_TIMEOUT_MS = 2000;
function withTimeout<T>(task: Promise<T>, timeoutMs: number): Promise<T> {
return new Promise<T>((resolve, reject) => {
const timer = setTimeout(() => {
reject(new Error(`检查超时(${String(timeoutMs)}ms)`));
}, timeoutMs);
void task.then(
(value) => {
clearTimeout(timer);
resolve(value);
},
(error: unknown) => {
clearTimeout(timer);
reject(error instanceof Error ? error : new Error(String(error)));
},
);
});
}
/**
* 健康检查:真实检查数据库(SELECT 1)与 Redis(PING),不只看端口(技术架构 §4.1.1)。
* 任一依赖不可用时返回 503 + 统一错误体,details 里带完整检查快照。
*/
@Injectable()
export class HealthService {
constructor(
@Inject(PRISMA) private readonly prisma: PrismaService,
@Inject(REDIS) private readonly redis: RedisService,
@Inject(APP_CONFIG) private readonly config: AppConfig,
) {}
async check(): Promise<HealthReport> {
const [database, redis] = await Promise.all([this.checkDatabase(), this.checkRedis()]);
const report: HealthReport = {
status: database.status === 'up' && redis.status === 'up' ? 'ok' : 'degraded',
env: this.config.nodeEnv,
version: API_VERSION,
uptimeSeconds: Math.round(process.uptime()),
checks: { database, redis },
timestamp: nowIso(),
};
if (report.status === 'degraded') {
throw new AppException({
code: ErrorCode.DEPENDENCY_UNAVAILABLE,
message: '依赖服务不可用',
details: report.checks,
});
}
return report;
}
private async checkDatabase(): Promise<ComponentHealth> {
const startedAt = Date.now();
try {
await withTimeout(this.prisma.$queryRaw`SELECT 1`, CHECK_TIMEOUT_MS);
return { status: 'up', latencyMs: Date.now() - startedAt };
} catch (error) {
return {
status: 'down',
latencyMs: Date.now() - startedAt,
error: error instanceof Error ? error.message : '未知错误',
};
}
}
private async checkRedis(): Promise<ComponentHealth> {
const startedAt = Date.now();
try {
await withTimeout(this.redis.ping(), CHECK_TIMEOUT_MS);
return { status: 'up', latencyMs: Date.now() - startedAt };
} catch (error) {
return {
status: 'down',
latencyMs: Date.now() - startedAt,
error: error instanceof Error ? error.message : '未知错误',
};
}
}
}