fix(infra): resolve NestJS dist build and Prometheus target issues
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:
@@ -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 {}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"emitDecoratorMetadata": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"incremental": false,
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"emitDecoratorMetadata": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"incremental": false,
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"emitDecoratorMetadata": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"incremental": false,
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"emitDecoratorMetadata": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"incremental": false,
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"emitDecoratorMetadata": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"incremental": false,
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
|
||||
26
services/teacher-bff/src/shared/observability/metrics.ts
Normal file
26
services/teacher-bff/src/shared/observability/metrics.ts
Normal 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 };
|
||||
@@ -8,6 +8,7 @@
|
||||
"emitDecoratorMetadata": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"incremental": false,
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
|
||||
Reference in New Issue
Block a user