/** * student-bff 启动入口. * * 仲裁依据: * - coord-final-decisions §1 G1 (Dockerfile 多阶段构建, EXPOSE 3009) * - coord-final-decisions §1 G3 (/healthz liveness) * - coord-final-decisions §1 G5 (/metrics 端点) * - coord-final-decisions §1 G6 (OTel tracer) * - coord-final-decisions §1 G8 (GlobalErrorFilter) * - coord-final-decisions §1 G9 (优雅关闭 SIGTERM) * - coord-final-decisions §2 B1 (GraphQL Yoga endpoint: POST /graphql) * - port-allocation.md (HTTP 3009, 无 gRPC 对外) * * 启动流程: * 1. initTracer (OTel) * 2. NestFactory.create(AppModule) * 3. app.useGlobalFilters(GlobalErrorFilter) * 4. 挂载 GraphQL Yoga middleware (POST /graphql) * 5. 注册 /metrics 端点 * 6. app.listen(3009) * 7. SIGTERM → app.close() → 关闭 Redis/gRPC/Tracer */ import "reflect-metadata"; import { NestFactory } from "@nestjs/core"; import type { NestExpressApplication } from "@nestjs/platform-express"; import { AppModule } from "./app.module.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 { metricsRegistry } from "./shared/observability/metrics.js"; import { env } from "./config/env.js"; import { GRAPHQL_YOGA } from "./student/student.module.js"; import { REDIS_CLIENT } from "./shared/cache/cache.module.js"; import { DOWNSTREAM_CLIENT } from "./shared/downstream/downstream.module.js"; import { CircuitBreakerService } from "./shared/circuit-breaker/circuit-breaker.service.js"; import type { Redis } from "ioredis"; import type { DownstreamClient } from "@edu/shared-ts/bff"; import type { YogaServerInstance } from "graphql-yoga"; import type { Request, Response, NextFunction } from "express"; async function bootstrap(): Promise { initTracer(); const app = await NestFactory.create(AppModule, { logger: ["log", "error", "warn"], }); // G8 全局错误过滤器 app.useGlobalFilters(new GlobalErrorFilter()); app.enableShutdownHooks(); // B1 GraphQL Yoga endpoint 挂载 const yoga = (await app.resolve(GRAPHQL_YOGA)) as YogaServerInstance< { req: Request; res: Response }, Record >; const httpAdapter = app.getHttpAdapter().getInstance(); httpAdapter.use( "/graphql", (req: Request, res: Response, next: NextFunction) => { yoga.handle(req, res).catch((err: unknown) => { logger.error({ err }, "GraphQL Yoga handle error"); next(err); }); }, ); // G5 Prometheus 指标端点 (不鉴权) httpAdapter.get("/metrics", async (_req: Request, res: Response) => { res.set("Content-Type", metricsRegistry.contentType); res.end(await metricsRegistry.metrics()); }); await app.listen(env.PORT); logger.info( { port: env.PORT, graphql: "/graphql", playground: env.GRAPHQL_PLAYGROUND && env.NODE_ENV === "development", mockUpstream: env.MOCK_UPSTREAM, devMode: env.DEV_MODE, }, "Student BFF started", ); // G9 优雅关闭: SIGTERM → 关闭 HTTP → Redis → gRPC → Tracer process.on("SIGTERM", async () => { logger.info("SIGTERM received, shutting down gracefully"); try { await app.close(); const redis = app.get(REDIS_CLIENT); await redis.quit(); const downstream = app.get(DOWNSTREAM_CLIENT); await downstream.close(); const circuitBreaker = app.get( CircuitBreakerService, ); await circuitBreaker.shutdown(); await shutdownTracer(); logger.info("Graceful shutdown complete"); } catch (err) { logger.error({ err }, "Error during graceful shutdown"); process.exit(1); } }); } bootstrap().catch((err: unknown) => { logger.error({ err }, "Failed to start Student BFF"); process.exit(1); });