HBB's Blog

Ordinary road, record every bit

Nuxt3 + OpenTelemetry 进行分布式追踪

公司内部使用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

/*instrumentation.cjs*/
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();

这个文件将会完成以下工作:

  1. 自动埋点采集
    • 通过 getNodeAutoInstrumentations() 自动收集:
    • HTTP 请求(Express/Fastify 等框架)
    • gRPC 调用
    • 数据库操作(MongoDB/Redis/MySQL 等)
    • 定时任务
    • 事件循环延迟
    • 自动生成父子 span 关系,追踪完整的请求链路

  2. 资源标识体系
    • 通过 Resource 对象标记服务身份:

    {
    service.name: '应用标识', // 服务名称(微服务架构中的唯一标识)
    service.instance.id: '服务器ID', // 实例标识(便于横向扩展时区分实例)
    host.name: '主机名' // 物理主机标识
    }

    • 这些元数据会附加到每个追踪数据中

  3. 数据导出管道
    • 使用 OTLP 协议通过 gRPC 发送数据:

    new OTLPTraceExporter({
    url: getCollectorAddress(), // 收集器地址(如 Jaeger/OpenTelemetry Collector)
    credentials: createInsecure() // 开发环境使用,生产环境应配置 TLS
    })

    • 支持批量发送和断点续传(内置队列缓冲)

  4. 上下文传播机制
    • 自动处理 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输出到日志文件

/** src/plugins/logs.server.ts */
import { createConsola } from 'consola';
import { trace, context } from '@opentelemetry/api';
// 获取当前的trace id
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}$ ` : '';
// 向终端输出日志,配合PM2将日志写入到文件
process.stdout.write(`${levelMap[logLevel]}${apmId}${message}\n`);
},
},
],
});

consola.wrapConsole();
});

增加ResponseTraceHeader

通过在页面Get请求中增加对应的trace,能够让我们更方便的排查和定位问题。

/** server/middleware/trace.ts */
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) {
// 增加一个名为 trace 的响应头
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"
}
]
}

这样就可以在不需要修改太多额外代码的情况下,接入分布式追踪。