GraphQL的可观测性比REST复杂。REST的监控很简单:每个端点是一个URL,日志按URL聚合,延迟按URL统计。但GraphQL只有一个端点 /graphql,所有查询都发到这里。你不能用URL来区分不同的操作。
GraphQL的可观测性需要在请求级别添加上下文:操作名(operation name)、查询变量、解析器级别的耗时。
操作名是GraphQL请求最基本的标识。每个查询和Mutation都应该有明确的操作名:
query GetUserProfile { ... } # 操作名: GetUserProfile mutation CreateNewPost { ... } # 操作名: CreateNewPost subscription OnCommentAdded { ... } # 操作名: OnCommentAdded
没有操作名的查询在生产环境中是不可接受的——你无法在日志和监控中区分不同的请求。
GraphQL的核心价值在于解析器模型,但这也意味着性能瓶颈可能隐藏在任何解析器中。解析器级别的追踪是找到性能问题的关键手段。
Apollo Server内置了追踪支持,可以通过插件获取每个解析器的执行时间:
import { ApolloServerPluginLandingPageLocalDefault } from '@apollo/server/plugin/landingPage/local'; import { ApolloServerPluginTracing } from '@apollo/server/plugin/tracing'; const server = new ApolloServer({ typeDefs, resolvers, plugins: [ ApolloServerPluginLandingPageLocalDefault(), // 每个解析器的执行时间记录在extensions.tracing中 { async requestDidStart(requestContext) { const start = Date.now(); const resolverTimings: Record<string, number[]> = {}; return { async executionDidStart() { return { async willResolveField({ info }) { const fieldStart = Date.now(); return (error) => { const duration = Date.now() - fieldStart; const key = `${info.parentType.name}.${info.fieldName}`; if (!resolverTimings[key]) resolverTimings[key] = []; resolverTimings[key].push(duration); }; } }; }, async didEncounterErrors(errors) { // 错误日志 }, async willSendResponse() { const totalDuration = Date.now() - start; console.log(JSON.stringify({ operation: requestContext.request.operationName, duration: totalDuration, resolvers: resolverTimings, variables: requestContext.request.variables, })); } }; } } ] });
这段代码追踪了每个解析器的执行时间,在请求结束时输出一份详细的性能报告。
GraphQL的日志需要记录以下信息:
// 结构化日志示例 const logRequest = (context) => { logger.info('graphql_request', { operation: context.operationName, variables: sanitize(context.request.variables), // 脱敏:隐藏token等敏感信息 duration: context.totalDuration, status: context.response.statusCode, queryHash: hashQuery(context.request.query), // 用于聚合统计 }); };
脱敏是关键。变量中可能包含密码、token、个人信息。日志中不应该记录这些敏感数据。
如果你使用Apollo Cloud,Apollo Studio提供了开箱即用的监控面板:
这些面板的数据来源是客户端上报的 report-protocol——Apollo Client自动收集每个查询的执行信息(操作名、变量摘要、缓存命中情况、错误信息),定期上报到Apollo Studio。
// Apollo Client上报配置 const client = new ApolloClient({ link: authLink.concat(httpLink), cache: new InMemoryCache(), // Apollo Studio上报 connectToDevTools: process.env.NODE_ENV === 'development', defaultOptions: { watchQuery: { // 在上报中包含错误详情 fetchPolicy: 'cache-and-network', } } });
对于自建的监控系统,可以用Prometheus格式的指标:
import { Counter, Histogram } from 'prom-client'; const graphqlRequestTotal = new Counter({ name: 'graphql_request_total', labelNames: ['operation', 'status'], help: 'GraphQL请求总数', }); const graphqlRequestDuration = new Histogram({ name: 'graphql_request_duration_seconds', labelNames: ['operation'], buckets: [0.01, 0.05, 0.1, 0.25, 0.5, 1, 2.5], help: 'GraphQL请求延迟', }); const graphqlResolverDuration = new Histogram({ name: 'graphql_resolver_duration_seconds', labelNames: ['type', 'field'], buckets: [0.001, 0.005, 0.01, 0.05, 0.1, 0.5], help: 'GraphQL解析器延迟', });
然后在插件中收集这些指标,暴露Prometheus endpoint,用Grafana可视化。
在微服务架构中,GraphQL服务可能调用多个后端服务。分布式追踪(如OpenTelemetry、Jaeger)让你看到一个GraphQL请求经过了哪些服务、每个服务的耗时。
import { ApolloServerPluginLandingPageDisabled } from '@apollo/server/plugin/disabled'; import { ApolloServerPluginOpenTelemetry } from '@apollo/server/plugin/openTelemetry'; const server = new ApolloServer({ typeDefs, resolvers, plugins: [ ApolloServerPluginOpenTelemetry({ // OpenTelemetry自动为每个解析器创建span }), ] });
这样在Jaeger中,你可以看到一个GraphQL请求的追踪链:Gateway → 用户服务(查用户)→ 帖子服务(查帖子)→ 评论服务(查评论),每个环节的耗时一目了然。
可观测性的目标是:在出问题时,能在几分钟内定位到是哪个解析器、调用了哪个数据源、耗时多少、错误是什么。没有可观测性的GraphQL服务,在生产环境中就是黑箱。
