全栈 AI 排障:让前端、API 与模型共享 Trace
新技术可以大胆试,但接口状态、权限和回滚条件要写得足够保守。这篇只讨论一个问题:怎样让 GraphQL 网关和 Python 推理服务共享同一个 Trace 上下文。
写作边界:文中的请求和耗时用于说明采集方式,不代表特定线上系统。日志示例不得记录令牌、完整 Prompt、个人信息或未经筛选的业务 Payload。
有效证据的标准:可观测性三位一体链条
在复杂的全栈体系中,一次有效的排障证据链必须能够全路径还原现场:
一份合格的“现场证据”需要具备三个特征:
面向生产环境的可观测性基础设施
下面的工程代码示范了如何在 Node.js Apollo GraphQL 网关中集成 OpenTelemetry 链路追踪,打印结构化 JSON 日志,并把 Trace 上下文自动注入到对 Python 后端 API 的 HTTP 请求中。
示例场景:1. Node.js 网关层:OpenTelemetry 与日志打印插件
// gateway/logger.ts
import winston from 'winston';
import { trace, context } from '@opentelemetry/api';
export const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
defaultMeta: { service: 'graphql-gateway' },
transports: [new winston.transports.Console()],
});
// 提取当前 OpenTelemetry 链路上下文
export function getTraceContext() {
const currentSpan = trace.getSpan(context.active());
if (!currentSpan) return { traceId: 'none', spanId: 'none' };
const spanContext = currentSpan.spanContext();
return {
traceId: spanContext.traceId,
spanId: spanContext.spanId,
};
}
Apollo GraphQL 可观测性插件,拦截 Query 运行全生命周期:
// gateway/opentelemetryPlugin.ts
import { ApolloServerPlugin, GraphQLRequestListener } from '@apollo/server';
import { logger, getTraceContext } from './logger';
import axios from 'axios';
import { trace } from '@opentelemetry/api';
export const GraphQLObservabilityPlugin: ApolloServerPlugin = {
async requestDidStart(requestContext): Promise<GraphQLRequestListener<any>> {
const startTime = Date.now();
const { traceId, spanId } = getTraceContext();
const operationName = requestContext.request.operationName || 'AnonymousOperation';
logger.info(`[GraphQL Request Started] ${operationName}`, {
traceId,
spanId,
operationName,
query: requestContext.request.query,
variables: requestContext.request.variables,
});
return {
async didEncounterErrors(ctx) {
for (const err of ctx.errors) {
logger.error(`[GraphQL Execution Error] ${err.message}`, {
traceId,
spanId,
operationName,
path: err.path,
stack: err.stack,
});
}
},
async willSendResponse(ctx) {
const durationMs = Date.now() – startTime;
logger.info(`[GraphQL Request Completed] ${operationName}`, {
traceId,
spanId,
operationName,
durationMs,
});
},
};
},
};
// 带有 Trace 传播的 Axios 客户端,用于调用 Python 服务
export async function callPythonBackend(endpoint: string, data: any) {
const tracer = trace.getTracer('gateway-tracer');
const span = tracer.startSpan(`HTTP POST ${endpoint}`);
try {
const spanContext = span.spanContext();
// 按照 W3C 规范构造 traceparent 请求头
const traceparent = `00-${spanContext.traceId}-${spanContext.spanId}-01`;
const response = await axios.post(endpoint, data, {
headers: {
'traceparent': traceparent,
'Content-Type': 'application/json',
},
timeout: 3000,
});
return response.data;
} catch (error: any) {
span.recordException(error);
throw error;
} finally {
span.end();
}
}
示例场景:2. Python 算力层:解析 TraceParent 并绑定本地日志
在 Python 后端(FastAPI),使用 OpenTelemetry 中间件自动接管从 Node.js 传过来的 traceparent:
# python_backend/server.py
import logging
from fastapi import FastAPI, Request
from opentelemetry import trace
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
logger = logging.getLogger("python-backend")
app = FastAPI()
FastAPIInstrumentor.instrument_app(app)
@app.post("/predict")
async def predict(request: Request) -> dict[str, str]:
span = trace.get_current_span()
context = span.get_span_context()
trace_id = format(context.trace_id, "032x") if context.is_valid else "none"
span_id = format(context.span_id, "016x") if context.is_valid else "none"
# 只记录诊断字段,不把原始 Prompt、令牌或个人信息写入日志。
logger.info(
"prediction request",
extra={
"trace_id": trace_id,
"span_id": span_id,
"content_length": request.headers.get("content-length", "unknown"),
},
)
return {"status": "accepted", "trace_id": trace_id}
线上排障现场复盘套路
这套可观测性环境建起来后,当线上产生告警时,排障人员的操作步骤应该规范为:
保留清晰的 Trace 与 JSON 日志,排障时便能依据实际调用链和事件记录定位问题。
