feat(infra): p6 hardening - observability and deploy compose

- 5 NestJS services add /metrics endpoint via app.getHttpAdapter()
- prometheus.yml scales to 8 services with rule_files and alertmanager
- monitoring compose replaces blackbox with Loki+Promtail
- Grafana datasource adds Loki
- docker-compose.deploy.yml scales to 11 services
- deploy.env.example completes Neo4j/ES/ClickHouse/LLM vars
- teacher-bff adds health.controller
- CI removes continue-on-error on lint step
- teacher-portal lint script changed to eslint src
This commit is contained in:
SpecialX
2026-07-09 10:21:06 +08:00
parent 3ca654619f
commit 566060fade
21 changed files with 554 additions and 59 deletions

View File

@@ -1,31 +1,39 @@
import 'reflect-metadata';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module.js';
import { GlobalErrorFilter } from './shared/errors/global-error.filter.js';
import { initTracer, shutdownTracer } from './shared/observability/tracer.js';
import { env } from './config/env.js';
import { logger } from './shared/observability/logger.js';
import "reflect-metadata";
import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module.js";
import { GlobalErrorFilter } from "./shared/errors/global-error.filter.js";
import { initTracer, shutdownTracer } from "./shared/observability/tracer.js";
import { env } from "./config/env.js";
import { logger } from "./shared/observability/logger.js";
import { metricsRegistry } from "./shared/observability/metrics.js";
async function bootstrap(): Promise<void> {
initTracer();
const app = await NestFactory.create(AppModule, {
logger: ['log', 'error', 'warn'],
logger: ["log", "error", "warn"],
});
app.useGlobalFilters(new GlobalErrorFilter());
app.enableShutdownHooks();
await app.listen(env.PORT);
logger.info({ port: env.PORT }, 'Classes service started');
// Prometheus 指标端点:不鉴权,供 Prometheus 抓取。
// 返回 register.metrics()Promise<string>,含 Content-Type text/plain; version=0.0.4; charset=utf-8
app.getHttpAdapter().get("/metrics", async (req, res) => {
res.set("Content-Type", metricsRegistry.contentType);
res.end(await metricsRegistry.metrics());
});
process.on('SIGTERM', async () => {
await app.listen(env.PORT);
logger.info({ port: env.PORT }, "Classes service started");
process.on("SIGTERM", async () => {
await app.close();
await shutdownTracer();
});
}
bootstrap().catch((err: unknown) => {
logger.error({ err }, 'Failed to start classes service');
logger.error({ err }, "Failed to start classes service");
process.exit(1);
});

View File

@@ -7,6 +7,7 @@ import { env } from "./config/env.js";
import { closeDb } from "./config/database.js";
import { closeNeo4j } from "./config/neo4j.js";
import { logger } from "./shared/observability/logger.js";
import { metricsRegistry } from "./shared/observability/metrics.js";
async function bootstrap(): Promise<void> {
initTracer();
@@ -18,6 +19,13 @@ async function bootstrap(): Promise<void> {
app.useGlobalFilters(new GlobalErrorFilter());
app.enableShutdownHooks();
// Prometheus 指标端点:不鉴权,供 Prometheus 抓取。
// 返回 register.metrics()Promise<string>,含 Content-Type text/plain; version=0.0.4; charset=utf-8
app.getHttpAdapter().get("/metrics", async (req, res) => {
res.set("Content-Type", metricsRegistry.contentType);
res.end(await metricsRegistry.metrics());
});
await app.listen(env.PORT);
logger.info({ port: env.PORT }, "Content service started");

View File

@@ -6,6 +6,7 @@ import { outboxPublisher } from "./shared/outbox/outbox.publisher.js";
import { GlobalErrorFilter } from "./shared/errors/global-error.filter.js";
import { initTracer, shutdownTracer } from "./shared/observability/tracer.js";
import { logger } from "./shared/observability/logger.js";
import { registry } from "./shared/observability/metrics.js";
async function bootstrap(): Promise<void> {
initTracer();
@@ -14,6 +15,13 @@ async function bootstrap(): Promise<void> {
app.useGlobalFilters(new GlobalErrorFilter());
app.enableShutdownHooks();
// Prometheus 指标端点:不鉴权,供 Prometheus 抓取。
// 返回 register.metrics()Promise<string>,含 Content-Type text/plain; version=0.0.4; charset=utf-8
app.getHttpAdapter().get("/metrics", async (req, res) => {
res.set("Content-Type", registry.contentType);
res.end(await registry.metrics());
});
// Connect Kafka producer/consumer before starting the outbox publisher.
// Non-blocking: if Kafka is unavailable, service still starts; outbox
// publisher will retry sends and messages stay pending until Kafka recovers.

View File

@@ -1,31 +1,39 @@
import 'reflect-metadata';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module.js';
import { GlobalErrorFilter } from './shared/errors/global-error.filter.js';
import { initTracer, shutdownTracer } from './shared/observability/tracer.js';
import { env } from './config/env.js';
import { logger } from './shared/observability/logger.js';
import "reflect-metadata";
import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module.js";
import { GlobalErrorFilter } from "./shared/errors/global-error.filter.js";
import { initTracer, shutdownTracer } from "./shared/observability/tracer.js";
import { env } from "./config/env.js";
import { logger } from "./shared/observability/logger.js";
import { metricsRegistry } from "./shared/observability/metrics.js";
async function bootstrap(): Promise<void> {
initTracer();
const app = await NestFactory.create(AppModule, {
logger: ['log', 'error', 'warn'],
logger: ["log", "error", "warn"],
});
app.useGlobalFilters(new GlobalErrorFilter());
app.enableShutdownHooks();
await app.listen(env.PORT);
logger.info({ port: env.PORT }, 'IAM service started');
// Prometheus 指标端点:不鉴权,供 Prometheus 抓取。
// 返回 register.metrics()Promise<string>,含 Content-Type text/plain; version=0.0.4; charset=utf-8
app.getHttpAdapter().get("/metrics", async (req, res) => {
res.set("Content-Type", metricsRegistry.contentType);
res.end(await metricsRegistry.metrics());
});
process.on('SIGTERM', async () => {
await app.listen(env.PORT);
logger.info({ port: env.PORT }, "IAM service started");
process.on("SIGTERM", async () => {
await app.close();
await shutdownTracer();
});
}
bootstrap().catch((err: unknown) => {
logger.error({ err }, 'Failed to start IAM service');
logger.error({ err }, "Failed to start IAM service");
process.exit(1);
});

View File

@@ -7,6 +7,7 @@ import { env } from "./config/env.js";
import { logger } from "./shared/observability/logger.js";
import { checkEsConnection, closeEs } from "./config/elasticsearch.js";
import { closeDb } from "./config/database.js";
import { metricsRegistry } from "./shared/observability/metrics.js";
async function bootstrap(): Promise<void> {
initTracer();
@@ -22,6 +23,13 @@ async function bootstrap(): Promise<void> {
app.useGlobalFilters(new GlobalErrorFilter());
app.enableShutdownHooks();
// Prometheus 指标端点:不鉴权,供 Prometheus 抓取。
// 返回 register.metrics()Promise<string>,含 Content-Type text/plain; version=0.0.4; charset=utf-8
app.getHttpAdapter().get("/metrics", async (req, res) => {
res.set("Content-Type", metricsRegistry.contentType);
res.end(await metricsRegistry.metrics());
});
await app.listen(env.PORT);
logger.info({ port: env.PORT }, "Msg service started");

View File

@@ -1,7 +1,9 @@
import { Module } from '@nestjs/common';
import { TeacherModule } from './teacher/teacher.module.js';
import { Module } from "@nestjs/common";
import { TeacherModule } from "./teacher/teacher.module.js";
import { HealthController } from "./health.controller.js";
@Module({
imports: [TeacherModule],
controllers: [HealthController],
})
export class AppModule {}

View File

@@ -0,0 +1,30 @@
import { Controller, Get } from "@nestjs/common";
/**
* 健康检查端点。
*
* - GET /healthzliveness仅返回进程存活
* - GET /readyzreadiness无外部依赖可直接返回 ok
*
* 不需要鉴权,必须在路由白名单中放行。
*/
@Controller()
export class HealthController {
@Get("healthz")
liveness(): { status: string; service: string; timestamp: string } {
return {
status: "ok",
service: "teacher-bff",
timestamp: new Date().toISOString(),
};
}
@Get("readyz")
readiness(): { status: string; service: string; timestamp: string } {
return {
status: "ok",
service: "teacher-bff",
timestamp: new Date().toISOString(),
};
}
}