- 后端
- API设计
【免费下载链接】graphql-yoga
🧘 Rewrite of a fully-featured GraphQL Server with focus on easy setup, performance & great developer experience. The core of Yoga implements WHATWG Fetch API and can run/deploy on any JS environment.
本文以 @envelop/newrelic 插件文档 为核心,介绍如何在基于 Envelop / GraphQL Yoga 的 GraphQL 应用中接入 New Relic Node.js Agent,通过分布式追踪监控操作(operation)与解析器(resolver)的性能和错误。读完本文,你将掌握插件的安装配置、全部选项的语义与默认值、基于正则表达式的变量/参数白名单黑名单过滤,以及 newrelic.js 与环境变量两种 Agent 配置方式,并能从源码与测试层面理解其工作原理。
说明:
@envelop/newrelic属于本仓库packages/envelop/plugins/newrelic目录,是 Envelop 插件体系的一员,可直接与 @envelop/core 组合使用;GraphQL Yoga 同样基于 Envelop 构建,因此该插件也适用于 Yoga 服务。
插件能做什么
@envelop/newrelic为你的 GraphQL 应用提供 New Relic 上报能力,核心价值在于:
- 分布式追踪(Distributed Tracing):将 GraphQL 操作接入 New Relic 的跨服务追踪链路,定位性能瓶颈与错误根因;
- 操作级监控:以 GraphQL 操作(operation)为单位记录事务(transaction),可携带操作名、操作类型、请求文档、变量与执行结果;
- Resolver 级监控:把每个解析器的调用记录为 segment,展示根字段(root-field)与子字段(sub-field)的逐级耗时;
- 错误追踪:将执行结果中的
GraphQLError上报给 New Relic Agent,并支持自定义跳过规则。
插件依赖 New Relic 官方 Node.js Agent(newrelicnpm 包),自身只负责把 GraphQL 的语义信息桥接到 Agent 的 API 上,最终的上报、采样、展示均由 Agent 与 New Relic 平台完成。
快速开始
按官方文档,接入分三步:安装依赖 → 配置 Agent → 注册插件。
安装
yarn add newrelic @envelop/newrelic从 package.json 可以看到本插件的版本要求:
- Node.js
>=18.0.0; - 作为
peerDependency的@envelop/core; graphql支持^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0;newrelic支持>=7 <12(当前开发环境使用newrelic@11.0.0)。
基本用法
在创建 Envelop 实例时,将插件加入 plugins 数组:
import { execute, parse, specifiedRules, subscribe, validate } from 'graphql' import { envelop, useEngine } from '@envelop/core' import { useNewRelic } from '@envelop/newrelic' const getEnveloped = envelop({ plugins: [ useEngine({ parse, validate, specifiedRules, execute, subscribe }), // ... other plugins ... useNewRelic({ includeOperationDocument: true, // 默认 false。为 true 时,把定义操作与片段的 GraphQL 文档作为属性上报 includeExecuteVariables: false, // 默认 false。为 true 时,把操作变量及值全部上报 includeRawResult: false, // 默认 false。为 true 时,把执行结果上报 trackResolvers: true, // 默认 false。为 true 时,把 resolver 记录为 segment 以监控性能 includeResolverArgs: false, // 默认 false。为 true 时,把传给 resolver 的参数及值全部上报 rootFieldsNaming: true, // 默认 false。为 true 时,把操作根字段名追加到事务名中 skipError: error => { return true // 允许你决定某个错误是否上报给 NewRelic。默认情况下自定义的 EnvelopError 会被跳过 }, extractOperationName: context => context.request.body.customOperationName // 从 context 中提取自定义操作名,用于事务名与属性 }) ] })所有选项在源码 src/index.ts 中都有明确的默认值:
const DEFAULT_OPTIONS: UseNewRelicOptions = { includeOperationDocument: false, includeExecuteVariables: false, includeRawResult: false, trackResolvers: false, includeResolverArgs: false, rootFieldsNaming: false, skipError: () => false, };其中skipError的默认行为需要留意:文档与源码注释指出,插件默认会跳过EnvelopError(Envelop 自定义错误类型)的上报,避免把业务校验错误当作系统故障;而对普通Error则按默认规则上报(源码中skipError默认实现为恒返回false,即不跳过)。
重要提示:事务(transaction)与 segment/span 的计时可能受其他插件影响。为了获得更准确的追踪数据,官方建议把 New Relic 插件放在插件列表的最后。
选项详解与底层实现
结合 src/index.ts,我们逐项说明各选项的语义及其在源码中的落点。
事务命名与操作级属性
插件在onExecute阶段执行核心逻辑(src/index.ts#L141-L241):
- 通过
getOperationAST(args.document, args.operationName)解析出根操作,拿不到则直接放弃记录; - 确定操作名,优先级为:
extractOperationName(context)→args.operationName→rootOperation.name?.value→ 匿名占位符<anonymous>(源码中的AttributeName.ANONYMOUS_OPERATION); - 如果开启了
rootFieldsNaming,会从 selectionSet 中收集所有根字段名(只统计Kind.FIELD节点); - 事务名被设置为
operationType + delimiter + operationName (+ delimiter + rootFields.join('&')),例如query/Greetings/hello(分隔符来自 Agent 的transactionNameState.delimiter,测试见 tests/newrelic.spec.ts); - 通过
getSpanContext()向 span 写入自定义属性。
插件写入的属性名集中在AttributeName枚举中(src/index.ts#L8-L21):
| 枚举值 | 属性名 | 含义 |
|---|---|---|
COMPONENT_NAME | Envelop_NewRelic_Plugin | 组件标识,同时用于注册Supportability/ExternalModules/Envelop_NewRelic_Plugin指标 |
EXECUTION_OPERATION_NAME | graphql.execute.operationName | 操作名 |
EXECUTION_OPERATION_TYPE | graphql.execute.operationType | 操作类型(query / mutation / subscription) |
EXECUTION_OPERATION_DOCUMENT | graphql.execute.document | GraphQL 文档字符串(开启includeOperationDocument时写入) |
EXECUTION_VARIABLES | graphql.execute.variables | 操作变量 JSON(开启includeExecuteVariables时写入) |
EXECUTION_RESULT | graphql.execute.result | 执行结果 JSON(开启includeRawResult且结果含data时写入) |
RESOLVER_FIELD_PATH | graphql.resolver.fieldPath | resolver 字段路径,如country/name |
RESOLVER_TYPE_NAME | graphql.resolver.typeName | 所属类型名,如Query |
RESOLVER_RESULT_TYPE | graphql.resolver.resultType | 返回类型,如Country、String! |
RESOLVER_RESULT | graphql.resolver.result | resolver 返回结果 JSON(开启includeRawResult时写入) |
RESOLVER_ARGS | graphql.resolver.args | resolver 参数 JSON(开启includeResolverArgs时写入) |
includeOperationDocument:把定义操作与片段的完整 GraphQL 文档写入graphql.execute.document。文档字符串通过@envelop/core的getDocumentString(args.document, print)获取。includeExecuteVariables:把操作变量 JSON 写入graphql.execute.variables;变量取自args.variableValues。includeRawResult:操作成功后把执行结果写入graphql.execute.result;同时每个被追踪的 resolver 会把返回值写入graphql.resolver.result(仅当includeRawResult开启)。extractOperationName:接收 Envelop 的DefaultContext,返回自定义操作名,同时用于事务名与graphql.execute.operationName属性。该能力在 4.0.0 版本由operationNameProperty改为函数式 API(见 CHANGELOG.md),好处是你可以读取 context 中的嵌套属性或来自其他 Envelop 插件的上下文扩展。
错误处理与 skipError
在onExecuteDone中,插件对执行结果逐条检查错误(src/index.ts#L204-L239):
if (singularResult.errors && singularResult.errors.length > 0) { const agent = instrumentationApi.agent; const transaction = instrumentationApi.tracer.getTransaction(); if (transaction) { for (const error of singularResult.errors) { if (options.skipError?.(error)) continue; agent.errors.add(transaction, JSON.stringify(error)); } } }skipError接收每个GraphQLError,返回true表示跳过该错误的上报;- 测试用例验证了这一点:当
skipError: e => e.message === 'Ignore me!'时,抛错 resolver 所在事务的hasError为false(tests/newrelic.spec.ts#L177-L198)。
流式结果支持
对于订阅(subscription)等异步可迭代结果,插件通过isAsyncIterable判断,并使用onNext逐条上报、onEnd关闭 operation segment(src/index.ts#L226-L235)。这一能力在 3.2.0 版本加入("support async iterable results"),保证订阅类操作的错误与数据同样被记录。
Resolver 级追踪的工作原理
开启trackResolvers: true后,插件会在onPluginInit阶段动态注入基于@envelop/on-resolve的钩子(src/index.ts#L81-L139):
useOnResolve在onSchemaChange阶段遍历 schema 中所有对象类型的字段,把每个字段的 resolver 包上一层拦截逻辑(packages/envelop/plugins/on-resolve/src/index.ts),默认跳过 introspection 查询;- 在 resolver 调用前,插件通过
instrumentationApi.getActiveSegment()拿到当前活动 segment,为其创建名为resolver/<路径>的子 segment(例如resolver/country、resolver/country/name); - 字段路径由
flattenPath递归拼接info.path得到,数字类型索引(如列表项下标)会被跳过(src/index.ts#L245-L257); - 为 segment 写入
graphql.resolver.fieldPath、graphql.resolver.typeName、graphql.resolver.resultType; - resolver 返回后(包括异步 Promise 场景),若开启
includeRawResult则写入返回结果 JSON,并结束 segment(回调形式({ result }) => { ...; resolverSegment.end(); }); - 若当前没有活动事务或活动 segment(例如 Agent 未初始化),插件会通过 logger 以
trace级别记录原因并跳过,不会影响业务执行。
另外,插件在初始化时会先检查 Agent 是否可用(rawOptions?.shim || newRelic?.shim以及instrumentationApi?.agent)。若 Agent 不可用(未正确安装newrelic、配置缺失或被禁用),会打印警告并返回空插件,避免应用崩溃(src/index.ts#L65-L72)。注册成功后还会上报一个Supportability/ExternalModules/Envelop_NewRelic_Plugin指标,测试中对此有断言(tests/newrelic.spec.ts#L79-L83)。
高级用法:正则过滤变量与参数
除了布尔值,includeExecuteVariables和includeResolverArgs还接受RegExp,用于对追踪内容做白名单/黑名单过滤。这在防止泄露用户数据(如 PII)的同时保留调试所需的字段非常有用。
useNewRelic({ includeExecuteVariables: /client|application/i, // 白名单:只追踪名称含 "client" 或 "application" 的变量(如 clientName、applicationId、xApplicationId) trackResolvers: true, // 追踪 resolver,因为同时想追踪 resolver 参数 includeResolverArgs: /^(?!name|email|password).*/i, // 黑名单:追踪所有名称不等于 name、email、password 的参数 }),实现上,插件在初始化时用instanceof RegExp判断是否为正则(options.isExecuteVariablesRegex/options.isResolverArgsRegex),随后通过filterPropertiesByRegex遍历对象键、用pattern.test(property)逐键筛选(src/index.ts#L259-L267)。
测试用例提供了直观的验证:
includeExecuteVariables: true+ 变量{ name: 'Laurin' }→ 上报{"name":"Laurin"}(tests/newrelic.spec.ts#L86-L114);includeExecuteVariables: /verb/+ 变量{ verb: 'Hi', name: 'Dotan' }→ 只上报{"verb":"Hi"}(tests/newrelic.spec.ts#L115-L150)。
性能提醒:过滤变量和参数的方式是循环遍历,因此会产生 O(n) 的开销,其中n为操作变量个数(追踪执行变量时)或传给 resolver 的参数个数(追踪 resolver 参数时)。在生产环境请权衡过滤粒度与开销,尤其是高 QPS 的服务。
Agent 配置:newrelic.js 与环境变量
插件本身只负责桥接,真正的 Agent 配置需要按 New Relic 官方文档完成。官方推荐两种配置方式:
- newrelic.js 文件:放在应用根目录。New Relic 官方仓库提供了该文件的基础示例(对应
node-newrelic的newrelic.js模板); - 环境变量:与 newrelic.js 中的配置项一一对应,只需全部大写、以
NEW_RELIC_前缀开头,并在应用启动前确保变量已就绪。
两个必填项:
| 描述 | newrelic.js | 环境变量 |
|---|---|---|
| 应用名 | app_name: ['MyAppName'] | NEW_RELIC_APP_NAME=MyAppName |
| 许可证密钥 | license_key: '40HexadecimalCharacters' | NEW_RELIC_LICENSE_KEY=40HexadecimalCharacters |
常用配置项:
| 描述 | newrelic.js | 环境变量 |
|---|---|---|
| 开启分布式追踪 | distributed_tracing: { enabled: true } | NEW_RELIC_DISTRIBUTED_TRACING_ENABLED=true |
| 日志级别 | logging: { level: 'info' } | NEW_RELIC_LOG_LEVEL=info |
| 捕获所有请求头 | allow_all_headers: true | NEW_RELIC_ALLOW_ALL_HEADERS=true |
| 开启错误收集 | error_collector: { enabled: true } | NEW_RELIC_ERROR_COLLECTOR_ENABLED=true |
更多可配置项可参考 New Relic Node.js Agent 的官方配置文档及其lib/config/default.js(该文件列出了 newrelic.js 中可包含的全部配置变量,均可转换为对应的环境变量)。注意:分布式追踪是本文场景(跨服务定位 GraphQL 请求根因)的基础,建议显式开启。
监控效果一览
以下截图来自插件 README,展示的是所有插件选项均为true时的 New Relic 界面效果。
错误追踪与操作/Resolver 视图:
成功操作追踪——操作、根字段与子字段 resolver 视图:
从截图可以看到:操作名为myCustomQuery的查询在 New Relic 中被记录为一次分布式追踪,右侧 Attributes 面板展示graphql.execute.operationName、graphql.execute.operationType、请求文档、变量与结果;span 列表中resolver/country、resolver/language等顶级 resolver 与resolver/country/name等子字段 resolver 被逐级展开,错误场景下还会标注具体的GraphQLError错误类与出错 resolver(如resolver/country)及其入参({"code":"GB"})、字段路径、结果类型等信息,帮助快速定位究竟是哪个字段、哪个入参导致耗时异常或失败。
小结
@envelop/newrelic以很小的接入成本为 GraphQL 服务补齐了 APM 能力:操作级事务命名与属性、resolver 级 segment、错误上报、基于正则的敏感数据过滤一应俱全,且对query/mutation/subscription三类操作均做了处理。其实现完全建立在 Envelop 的onExecute/onExecuteDone钩子与@envelop/on-resolve的 resolver 拦截机制之上,不侵入业务代码;需要留意的是 Agent 未初始化时插件会静默降级(打印警告、不记录),以及按官方建议将插件置于 plugins 数组末尾以获得更准确的计时。
- 后端
- API设计
【免费下载链接】graphql-yoga
🧘 Rewrite of a fully-featured GraphQL Server with focus on easy setup, performance & great developer experience. The core of Yoga implements WHATWG Fetch API and can run/deploy on any JS environment.
相关推荐
使用 redisotel 为 go-redis 接入 OpenTelemetry 分布式追踪与指标监控
使用 redisotel 为 go redis 接入 OpenTelemetry 分布式追踪与指标监控 导读 redisotel 是 go redis( git
云原生存储GraphQL Yoga 与 Envelop 集成 OpenTelemetry 追踪:从零接入到 Jaeger 全流程实战
GraphQL Yoga 与 Envelop 集成 OpenTelemetry 追踪:从零接入到 Jaeger 全流程实战 导读:本文以开源仓库 graphql
后端API设计Quansheng UV-K5硬件逆向工程:从PCB到射频设计的完整技术解析
Quansheng UV K5硬件逆向工程:从PCB到射频设计的完整技术解析 在开源硬件与业余无线电技术快速融合的今天,逆向工程已成为理解复杂射频系统设计的重要
硬件开发逆向工程嵌入式智能硬件
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考