ci / 质量门禁 (push) Canceled after 0s
- 建立 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 实施报告与本地开发说明
95 lines
3.1 KiB
TypeScript
95 lines
3.1 KiB
TypeScript
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 : '未知错误',
|
||
};
|
||
}
|
||
}
|
||
}
|