fix(infra): resolve NestJS dist build and Prometheus target issues
Some checks failed
CI / quality-go (push) Failing after 5s
CI / quality-proto (push) Failing after 3s
CI / deploy (push) Has been skipped
CI / quality-ts (push) Failing after 50s

NestJS: disable incremental in 6 services tsconfig.json to fix dist
not emitted when nest-cli deleteOutDir conflicts with tsc tsbuildinfo.
classes/iam: import HealthModule in AppModule to fix /healthz 404.
classes: rewrite HealthController to Drizzle getDb from TypeORM DI.
teacher-bff: add /metrics endpoint for Prometheus scraping.
infra: add node/mysql/redis exporters to observability profile.
mysql-exporter v0.15.1 uses command-line flags not DATA_SOURCE_NAME.
prometheus: enable web.enable-lifecycle for hot reload.
This commit is contained in:
SpecialX
2026-07-09 15:12:15 +08:00
parent b72c8d81d4
commit f1e466a772
16 changed files with 267 additions and 55 deletions

View File

@@ -1,7 +1,8 @@
import { Module } from '@nestjs/common';
import { ClassesModule } from './classes/classes.module.js';
import { Module } from "@nestjs/common";
import { ClassesModule } from "./classes/classes.module.js";
import { HealthModule } from "./shared/health/health.module.js";
@Module({
imports: [ClassesModule],
imports: [ClassesModule, HealthModule],
})
export class AppModule {}

View File

@@ -1,7 +1,8 @@
import { Controller, Get, HttpException, HttpStatus } from '@nestjs/common';
import { DataSource } from 'typeorm';
import { Controller, Get, HttpException, HttpStatus } from "@nestjs/common";
import { sql } from "drizzle-orm";
import { getDb } from "../../config/database.js";
const SERVICE_NAME = 'classes';
const SERVICE_NAME = "classes";
/**
* 健康检查端点。
@@ -14,33 +15,37 @@ const SERVICE_NAME = 'classes';
*/
@Controller()
export class HealthController {
constructor(private readonly dataSource: DataSource) {}
@Get('healthz')
@Get("healthz")
liveness(): { status: string; service: string; timestamp: string } {
return {
status: 'ok',
status: "ok",
service: SERVICE_NAME,
timestamp: new Date().toISOString(),
};
}
@Get('readyz')
async readiness(): Promise<{ status: string; service: string; timestamp: string }> {
@Get("readyz")
async readiness(): Promise<{
status: string;
service: string;
timestamp: string;
}> {
try {
await this.dataSource.query('SELECT 1');
const db = getDb();
await db.execute(sql`SELECT 1`);
return {
status: 'ok',
status: "ok",
service: SERVICE_NAME,
timestamp: new Date().toISOString(),
};
} catch (error) {
throw new HttpException(
{
status: 'error',
status: "error",
service: SERVICE_NAME,
timestamp: new Date().toISOString(),
error: error instanceof Error ? error.message : 'database unreachable',
error:
error instanceof Error ? error.message : "database unreachable",
},
HttpStatus.SERVICE_UNAVAILABLE,
);

View File

@@ -8,6 +8,7 @@
"emitDecoratorMetadata": true,
"outDir": "./dist",
"rootDir": "./src",
"incremental": false,
"types": ["node"]
},
"include": ["src/**/*"],

View File

@@ -8,6 +8,7 @@
"emitDecoratorMetadata": true,
"outDir": "./dist",
"rootDir": "./src",
"incremental": false,
"types": ["node"]
},
"include": ["src/**/*"],

View File

@@ -8,6 +8,7 @@
"emitDecoratorMetadata": true,
"outDir": "./dist",
"rootDir": "./src",
"incremental": false,
"types": ["node"]
},
"include": ["src/**/*"],

View File

@@ -1,7 +1,8 @@
import { Module } from '@nestjs/common';
import { IamModule } from './iam/iam.module.js';
import { Module } from "@nestjs/common";
import { IamModule } from "./iam/iam.module.js";
import { HealthModule } from "./shared/health/health.module.js";
@Module({
imports: [IamModule],
imports: [IamModule, HealthModule],
})
export class AppModule {}

View File

@@ -8,6 +8,7 @@
"emitDecoratorMetadata": true,
"outDir": "./dist",
"rootDir": "./src",
"incremental": false,
"types": ["node"]
},
"include": ["src/**/*"],

View File

@@ -8,6 +8,7 @@
"emitDecoratorMetadata": true,
"outDir": "./dist",
"rootDir": "./src",
"incremental": false,
"types": ["node"]
},
"include": ["src/**/*"],

View File

@@ -3,6 +3,7 @@ import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module.js";
import { env } from "./config/env.js";
import { initTracer, shutdownTracer } from "./shared/observability/tracer.js";
import { metricsRegistry } from "./shared/observability/metrics.js";
async function bootstrap(): Promise<void> {
initTracer();
@@ -12,6 +13,12 @@ async function bootstrap(): Promise<void> {
app.enableShutdownHooks();
// Prometheus 指标端点:不鉴权,供 Prometheus 抓取。
app.getHttpAdapter().get("/metrics", async (_req, res) => {
res.set("Content-Type", metricsRegistry.contentType);
res.end(await metricsRegistry.metrics());
});
await app.listen(env.PORT);
console.log(`Teacher BFF started on port ${env.PORT}`);

View File

@@ -0,0 +1,26 @@
import promClient from "prom-client";
const registry = new promClient.Registry();
registry.setDefaultLabels({ service: "teacher-bff" });
registry.registerMetric(
new promClient.Counter({
name: "teacher_bff_requests_total",
help: "Total number of teacher-bff requests",
labelNames: ["method", "endpoint", "status"],
}),
);
registry.registerMetric(
new promClient.Histogram({
name: "teacher_bff_request_duration_seconds",
help: "Teacher-bff request duration in seconds",
labelNames: ["method", "endpoint"],
buckets: [0.01, 0.05, 0.1, 0.3, 0.5, 1, 3, 5],
}),
);
// 自动收集 Node.js 进程级指标CPU/内存/事件循环/GC等
promClient.collectDefaultMetrics({ register: registry });
export { registry as metricsRegistry };

View File

@@ -8,6 +8,7 @@
"emitDecoratorMetadata": true,
"outDir": "./dist",
"rootDir": "./src",
"incremental": false,
"types": ["node"]
},
"include": ["src/**/*"],