公司内部使用APM进行各个项目的分布式追踪,我们组使用Nuxt3有一段时间了,在生产环节的错误追踪环节一直有所欠缺,记录一下此次通过OpenTelemetry接入公司APM。
OpenTelemetry有Node环境对应的SDK,通过此SDK进行接入。
依赖安装
pnpm install @opentelemetry/sdk-node \ @opentelemetry/resources \ @opentelemetry/auto-instrumentations-node \ @opentelemetry/semantic-conventions \ @opentelemetry/exporter-trace-otlp-grpc \ @grpc/grpc-js --save-dev
|
新建追踪js
const { NodeSDK } = require('@opentelemetry/sdk-node'); const { Resource } = require('@opentelemetry/resources'); const { getNodeAutoInstrumentations, } = require('@opentelemetry/auto-instrumentations-node'); const { SemanticResourceAttributes, } = require('@opentelemetry/semantic-conventions'); const { OTLPTraceExporter, } = require('@opentelemetry/exporter-trace-otlp-grpc'); const os = require('os'); const grpc = require('@grpc/grpc-js');
var getCollectorAddress = function () { var collectorAddress = '根据不同环境,配置你的收集器地址。' console.log('收集器地址:', collectorAddress); return collectorAddress; };
const sdk = new NodeSDK({ traceExporter: new OTLPTraceExporter({ credentials: grpc.credentials.createInsecure(), url: getCollectorAddress(), }), instrumentations: [getNodeAutoInstrumentations()], resource: new Resource({ [SemanticResourceAttributes.SERVICE_NAME]: '应用标识', [SemanticResourceAttributes.SERVICE_INSTANCE_ID]: '服务器的ID', [SemanticResourceAttributes.HOST_NAME]: os.hostname(), }), });
sdk.start();
|
这个文件将会完成以下工作:
自动埋点采集
• 通过 getNodeAutoInstrumentations()
自动收集:
• HTTP 请求(Express/Fastify 等框架)
• gRPC 调用
• 数据库操作(MongoDB/Redis/MySQL 等)
• 定时任务
• 事件循环延迟
• 自动生成父子 span 关系,追踪完整的请求链路
资源标识体系
• 通过 Resource
对象标记服务身份:
{ service.name: '应用标识', service.instance.id: '服务器ID', host.name: '主机名' }
|
• 这些元数据会附加到每个追踪数据中
数据导出管道
• 使用 OTLP 协议通过 gRPC 发送数据:
new OTLPTraceExporter({ url: getCollectorAddress(), credentials: createInsecure() })
|
• 支持批量发送和断点续传(内置队列缓冲)
上下文传播机制
• 自动处理 W3C Trace Context 标头
• 跨服务调用时携带 traceId/spanId
• 支持异步操作上下文追踪(AsyncLocalStorage)
因为我司已经有了对应的采集服务器,所以这些配置本次不涉及。
通过这个配置建立了完整的可观测性基础设施,相当于给你的 Node.js 应用装上了全链路追踪的黑匣子。
尚未解决的问题
Nuxt底层的实现估计不是采用明确定义路由的方式,Node的SDK会无法出现无法采集到真实路径的情况。
该Issue目前尚未得到解决:fix wrong rpcMetadata.route value when route.use() handle nested router
写入日志文件
建一个服务端的plugin,将console输出到日志文件
import { createConsola } from 'consola'; import { trace, context } from '@opentelemetry/api';
function getCurrentTrace() { var span = trace.getSpan(context.active()); if (!span) { return null; } var spanContext = span.spanContext(); const t = { trace_id: spanContext.traceId, span_id: spanContext.spanId, }; return t; }
export default defineNuxtPlugin(() => { const consola = createConsola({ reporters: [ { log: logObj => { const logLevel = logObj.level; const levelMap = ['FATAL', 'WARN', 'INFO', 'INFO', 'DEBUG', 'TRACE']; const message = logObj.args .map(arg => { if (typeof arg === 'string') { return arg; } try { return JSON.stringify(arg); } catch (error) { return arg; } }) .join(' '); const t = getCurrentTrace(); const apmId = t ? `$apmTxId:${t.trace_id}@@${t.span_id}$ ` : ''; process.stdout.write(`${levelMap[logLevel]}${apmId}${message}\n`); }, }, ], });
consola.wrapConsole(); });
|
通过在页面Get请求中增加对应的trace,能够让我们更方便的排查和定位问题。
import { trace, context } from '@opentelemetry/api';
function getCurrentTrace() { var span = trace.getSpan(context.active()); if (!span) { return ''; } var spanContext = span.spanContext(); return spanContext.traceId; }
export default defineEventHandler(event => { try { const traceId = getCurrentTrace(); if (traceId) { event.node.res.setHeader('trace', traceId); } } catch (error) { console.error('apmTrace error', error); } });
|
启动服务
我们通过node的-r命令,在启动实际服务器,先预加载分布式追踪服务。
node -r ./instrumentation.cjs .output/server/index.mjs
|
PM2 启动
pm2通过node_args增加启动参数
{ "apps": [ { "name": "app", "script": ".output/server/index.mjs", "node_args": "-r ./instrumentation.cjs", "error_file": "/data/logs/error.log", "out_file": "/data/logs/out.log", "cwd": "./", "time": false, "log_date_format": "YYYY-MM-DD HH:mm:ss.SSS" } ] }
|
这样就可以在不需要修改太多额外代码的情况下,接入分布式追踪。