feat(teacher-bff): 完整实现 teacher-bff GraphQL 聚合层
包含 clients/graphql/middleware、health probes、shared-ts contracts 等
This commit is contained in:
159
services/teacher-bff/src/clients/base.client.ts
Normal file
159
services/teacher-bff/src/clients/base.client.ts
Normal file
@@ -0,0 +1,159 @@
|
||||
// DownstreamClient 抽象基类(B8 裁决:3 个 BFF 统一使用)
|
||||
// 提供统一的错误映射 + 结构化日志 + metrics 记录
|
||||
import type { Logger } from "pino";
|
||||
import { logger } from "../shared/observability/logger.js";
|
||||
import {
|
||||
BadGatewayError,
|
||||
AggregationFailedError,
|
||||
} from "../shared/errors/application-error.js";
|
||||
import type {
|
||||
CallContext,
|
||||
DownstreamError,
|
||||
DownstreamResult,
|
||||
DownstreamServiceName,
|
||||
GrpcMetadata,
|
||||
} from "./types.js";
|
||||
|
||||
/** gRPC 状态码 → HTTP 语义映射(@grpc/grpc-js status codes) */
|
||||
const GRPC_STATUS = {
|
||||
OK: 0,
|
||||
CANCELLED: 1,
|
||||
UNKNOWN: 2,
|
||||
INVALID_ARGUMENT: 3,
|
||||
DEADLINE_EXCEEDED: 4,
|
||||
NOT_FOUND: 5,
|
||||
ALREADY_EXISTS: 6,
|
||||
PERMISSION_DENIED: 7,
|
||||
RESOURCE_EXHAUSTED: 8,
|
||||
FAILED_PRECONDITION: 9,
|
||||
ABORTED: 10,
|
||||
OUT_OF_RANGE: 11,
|
||||
UNIMPLEMENTED: 12,
|
||||
INTERNAL: 13,
|
||||
UNAVAILABLE: 14,
|
||||
DATA_LOSS: 15,
|
||||
UNAUTHENTICATED: 16,
|
||||
} as const;
|
||||
|
||||
export abstract class BaseDownstreamClient {
|
||||
protected log: Logger;
|
||||
abstract readonly serviceName: DownstreamServiceName;
|
||||
|
||||
constructor() {
|
||||
// abstract property 在子类构造后才可用,延迟初始化 logger
|
||||
this.log = logger.child({ downstream: "downstream" });
|
||||
}
|
||||
|
||||
/** 子类在构造函数中调用以初始化 logger(带正确 serviceName) */
|
||||
protected initLogger(): void {
|
||||
this.log = logger.child({ downstream: this.serviceName });
|
||||
}
|
||||
|
||||
/** 构建 gRPC metadata(注入 x-user-id + x-request-id) */
|
||||
protected buildMetadata(ctx: CallContext): GrpcMetadata {
|
||||
const meta: GrpcMetadata = { "x-user-id": ctx.userId };
|
||||
if (ctx.traceId) {
|
||||
meta["x-request-id"] = ctx.traceId;
|
||||
}
|
||||
return meta;
|
||||
}
|
||||
|
||||
/** 将 gRPC 错误映射为 BadGatewayError(502) */
|
||||
protected mapGrpcError(err: unknown, rpc: string): BadGatewayError {
|
||||
const grpcErr = err as {
|
||||
code?: number;
|
||||
message?: string;
|
||||
details?: string;
|
||||
};
|
||||
const code = grpcErr.code ?? GRPC_STATUS.UNKNOWN;
|
||||
const message = grpcErr.message ?? "Unknown gRPC error";
|
||||
|
||||
this.log.warn(
|
||||
{ rpc, grpcCode: code, message, err },
|
||||
"Downstream gRPC call failed",
|
||||
);
|
||||
|
||||
// UNAVAILABLE → 502 Bad Gateway
|
||||
if (code === GRPC_STATUS.UNAVAILABLE) {
|
||||
return new BadGatewayError(
|
||||
`Downstream ${this.serviceName}.${rpc} unavailable`,
|
||||
{ service: this.serviceName, rpc, grpcCode: code },
|
||||
);
|
||||
}
|
||||
|
||||
// UNAUTHENTICATED → 映射为 BadGateway(BFF 自身做身份校验,下游不应返回 UNAUTHENTICATED)
|
||||
if (code === GRPC_STATUS.UNAUTHENTICATED) {
|
||||
return new BadGatewayError(
|
||||
`Downstream ${this.serviceName}.${rpc} returned UNAUTHENTICATED`,
|
||||
{ service: this.serviceName, rpc, grpcCode: code },
|
||||
);
|
||||
}
|
||||
|
||||
// 其他错误统一映射为 BadGateway
|
||||
return new BadGatewayError(
|
||||
`Downstream ${this.serviceName}.${rpc} failed: ${message}`,
|
||||
{
|
||||
service: this.serviceName,
|
||||
rpc,
|
||||
grpcCode: code,
|
||||
details: grpcErr.details,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/** 执行 gRPC 调用并统一错误处理 + 超时控制 */
|
||||
protected async callGrpc<T>(
|
||||
rpc: string,
|
||||
fn: () => Promise<T>,
|
||||
timeoutMs = 3000,
|
||||
): Promise<T> {
|
||||
const start = Date.now();
|
||||
try {
|
||||
const result = await Promise.race([
|
||||
fn(),
|
||||
this.createTimeout(timeoutMs, rpc),
|
||||
]);
|
||||
const duration = Date.now() - start;
|
||||
this.log.debug(
|
||||
{ rpc, durationMs: duration },
|
||||
"Downstream gRPC call succeeded",
|
||||
);
|
||||
return result;
|
||||
} catch (err) {
|
||||
throw this.mapGrpcError(err, rpc);
|
||||
}
|
||||
}
|
||||
|
||||
private createTimeout(ms: number, rpc: string): Promise<never> {
|
||||
return new Promise((_, reject) => {
|
||||
setTimeout(() => {
|
||||
reject(
|
||||
new BadGatewayError(
|
||||
`Downstream ${this.serviceName}.${rpc} timed out after ${ms}ms`,
|
||||
{ service: this.serviceName, rpc, timeoutMs: ms },
|
||||
),
|
||||
);
|
||||
}, ms);
|
||||
});
|
||||
}
|
||||
|
||||
/** 降级模式 B:部分失败时返回 success=true + degraded=true + warning(president §2.6) */
|
||||
protected degraded<T>(
|
||||
data: T | null,
|
||||
warning: string,
|
||||
rpc: string,
|
||||
): DownstreamResult<T> {
|
||||
this.log.warn({ rpc, warning }, "Downstream call degraded");
|
||||
return { success: true, data, degraded: true, warning };
|
||||
}
|
||||
|
||||
/** 聚合失败(多下游部分失败且无降级数据,president §2.6) */
|
||||
protected aggregationFailed(
|
||||
errors: DownstreamError[],
|
||||
): AggregationFailedError {
|
||||
return new AggregationFailedError(
|
||||
`Aggregation failed for ${this.serviceName}: ${errors.length} downstream(s) failed`,
|
||||
{ service: this.serviceName, errors },
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user