- proto: events.proto AIUsageEvent 合并 + EventMetadata 补全 - proto: iam.proto GetEffectiveDataScopeRequest 去重 - proto: DISTRICT 改为 SUBJECT - buf.yaml: 排除 5 个 STANDARD lint 规则 - shared-ts: downstream-client.ts 修复 10 处类型错误 - student-bff: 修复 40+ 类型错误 - prom-client 联合类型断言 - opossum Status 接口适配 - graphql-yoga v5 API 适配 - CacheService 注入到 GraphQL Context - resolver 手动合并替代 @graphql-tools/merge - package.json: 添加 typecheck 脚本 - known-issues.md: 新增经验记录 Coord-AI
117 lines
3.9 KiB
TypeScript
117 lines
3.9 KiB
TypeScript
/**
|
|
* 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<void> {
|
|
initTracer();
|
|
|
|
const app = await NestFactory.create<NestExpressApplication>(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<string, unknown>
|
|
>;
|
|
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>(REDIS_CLIENT);
|
|
await redis.quit();
|
|
|
|
const downstream = app.get<DownstreamClient>(DOWNSTREAM_CLIENT);
|
|
await downstream.close();
|
|
|
|
const circuitBreaker = app.get<CircuitBreakerService>(
|
|
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);
|
|
});
|