做后端开发这些年,我一直觉得Node.js应用的监控是个挺折磨人的事。单线程事件循环、海量异步回调、第三方依赖一拉一大把,线上出问题的时候日志翻半天也拼不出完整链路。后来在几个生产项目里把OpenTelemetry真正落地到了Node.js服务上,从trace、metric到日志,一条链路完整打通,Prometheus、Grafana、Jaeger想接谁接谁,才算彻底解决了监控选型焦虑。这篇文章会把我从技术选型到SDK初始化、自动埋点、手动埋点、指标暴露、图表告警,再到各种坑的记录全部过一遍,给你一套可以直接抄作业的Node.js监控方案。
需要提前说清楚的是,OpenTelemetry(下文统一叫OTel)不是一个开箱即用的监控平台,它是一套标准框架和工具集合。它负责从应用里采集、处理、导出遥测数据,至于数据最后落到哪个后端,由导出器决定。这个特性让它在监控体系里的位置非常特殊:你只需要埋一次点,后面换存储、换展示层,都不需要再动业务代码。
1. 为什么我把监控方案定为OpenTelemetry
1.1 Node.js监控的旧困境
早期我给Node.js服务做监控,试过好几条路,每条都有让人头疼的短板。
最原始的是日志方案。业务代码里console.log到处打,日志汇到ELK,然后靠关键字搜索定位问题。这套方案在小规模服务上还行,服务一多就崩:日志格式全凭个人习惯,有的打JSON有的打字符串,跨服务调用链条根本连不起来,一个请求经过了A服务、B服务、C服务,每个服务只留下一段孤立日志,排障全靠脑补。
后来上商业APM。接入确实快,几行配置就能看到调用拓扑和慢请求,但是成本不低,而且数据格式是厂商私有的,今天用的这家,明天想换成另一家,所有埋点都要重来。更难受的是,自定义业务埋点能力往往受限,你想给某个核心业务方法单独加一条链路追踪,文档翻半天还不一定支持。
也试过让应用直接暴露Prometheus指标。这个方案对指标很好用,请求量、延迟、错误率都能看到,但它覆盖不了链路追踪。线上一个慢请求,你能看到接口P99现在有多高,却看不到慢在哪一环,是数据库查询慢还是下游HTTP调用慢,还是一段同步代码阻塞了事件循环。对Node.js这种异步密集型应用来说,缺了trace,监控等于少了一条腿。
OTel解决的正是这几个问题:埋点一次、标准统一、后端自由。它把trace和metric两套原本割裂的体系收纳进同一个SDK,让你能在一套框架下同时处理好"这个接口多慢"和"慢在哪一层"两个问题。
1.2 三个核心概念先弄明白
在动手之前,得先把OTel的几个基础概念捋清楚,不然看文档会一头雾水。
Trace和Span。Trace是一条完整请求链路,从用户请求进来,到应用处理,到数据库查询、外部API调用,再到响应返回,整条路径就是一个trace。Span是trace里的最小工作单元,相当于整条链路上的一个环节。我习惯这么类比:trace是一张快递运单,span是运单上每个环节的扫描记录——揽收、中转、派送,每个环节有自己耗时和状态。一个HTTP请求进入Node.js服务,会先形成一个根span,然后Express路由处理、数据库查询、下游HTTP调用,都会在根span下面挂上子span,形成一棵嵌套树。
Signal。OTel把遥测数据分成三类信号:Traces(链路追踪)、Metrics(指标)、Logs(日志)。这三类各有分工,Traces看细节,Metrics看趋势,Logs看具体错误内容。真正完整的监控体系应该三件套都上,我在生产环境里是trace和metric一起用,日志仍然走原来的日志平台,但把traceId写进日志,方便从日志跳到链路。
Instrumentation(插桩)。这是OTel接入应用的方式,分自动插桩和手动插桩。自动插桩通过拦截HTTP库、Express框架、数据库客户端等底层模块的调用,自动生成span,业务代码不用改。手动插桩则是你在业务代码里用OTel API主动创建span和记录指标,适合埋自动插桩覆盖不到的、有业务语义的节点。
1.3 我用下来的选型对比
拿我最近接的一个订单服务举例,监控方案摆在一起对比,优劣势就非常清楚:
| 方案 | 接入成本 | 链路追踪 | 指标能力 | 后端绑定 | 适合场景 |
|---|---|---|---|---|---|
| 自研日志+ELK | 高 | 弱 | 弱 | 无 | 小团队临时方案 |
| 商业APM | 低 | 强 | 强 | 绑定厂商 | 预算足、不想折腾 |
| Prometheus直出 | 中 | 无 | 强 | 绑定Prometheus生态 | 只看指标的场景 |
| OpenTelemetry | 中偏高 | 强 | 强 | 基本不绑定 | 长期治理、链路和指标都要 |
OTel的缺点也要说,就是它需要你自己组装的东西比较多。它不是拿走即用的全家桶,SDK要初始化、导出器要配、Collector要不要上要评估、看板还得自己搭。但这些东西都是一次性投入,换来的是标准统一和数据主权。我最后选择OTel,核心原因就是不想再被任何一家厂商绑死,同时又要一个能把trace和metric统一管理的方案。
2. 环境准备与SDK初始化:从零到Hello World
2.1 前置条件:Node.js版本与工程结构
建议Node.js版本不低于18。原因很简单:OTel的异步上下文管理依赖AsyncLocalStorage,Node 18的运行时表现稳定,而且当前版本的好几个自动插桩包也默认按现代Node特性做兼容。我生产环境现在跑的是18.20.4 LTS,官方下载页直接装LTS版就行,别用最新偶数版追求版本号,生产追求的是稳定。如果还没装Node,建议先装好再往下走,这里假设你已经有了一个能跑的Node.js项目。
工程结构上,我的做法是新建一个独立的tracing.js文件专门写SDK初始化逻辑,然后在启动入口用--require提前加载它。不要把SDK初始化代码混进业务入口文件里,否则业务代码和监控代码耦合在一起,后面想单独关监控、替换配置,就会碍手碍脚。
2.2 安装哪些依赖
先初始化项目,再安装OTel相关包:
npm init -y npm install @opentelemetry/sdk-node @opentelemetry/api @opentelemetry/auto-instrumentations-node npm install @opentelemetry/exporter-trace-otlp-http @opentelemetry/exporter-metrics-otlp-http这里几个包的分工要说一下。@opentelemetry/api是业务代码里手动埋点用到的API库,它只定义接口不包含实现;@opentelemetry/sdk-node才是真正干活的SDK,把trace、metric、资源属性、采样器都组装起来;@opentelemetry/auto-instrumentations-node是一个大杂烩包,一次性集成了Node.js生态常见的自动插桩器,包括HTTP、Express、MongoDB、MySQL、Redis等。
auto-instrumentations-node这个包虽然省事,但它是all-in-one的,会把一堆你可能用不到的插桩器全装上。我到后期把它拆掉了,改成只装实际用到的@opentelemetry/instrumentation-http和@opentelemetry/instrumentation-express。依赖越少,版本冲突的风险越低,上线时的体积也越小。初期图省事可以用auto包,稳定后建议按需裁剪。
2.3 最简tracing.js
下面这个是我最初用的一个最小可运行版本:
// tracing.js const { NodeSDK } = require('@opentelemetry/sdk-node'); const { Resource } = require('@opentelemetry/resources'); const { SemanticResourceAttributes } = require('@opentelemetry/semantic-conventions'); const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-http'); const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node'); const sdk = new NodeSDK({ resource: new Resource({ [SemanticResourceAttributes.SERVICE_NAME]: 'order-service', [SemanticResourceAttributes.SERVICE_VERSION]: '1.0.0', }), traceExporter: new OTLPTraceExporter({ url: 'http://localhost:4318/v1/traces', }), instrumentations: [getNodeAutoInstrumentations()], }); sdk.start();启动方式:
node --require ./tracing.js src/server.js--require ./tracing.js必须在启动时就加载,目的是赶在HTTP模块被业务代码require之前完成插桩。如果你在server.js里第三行才引入tracing.js,那么前面被加载的http模块缓存就没被patch到,HTTPSpan就出不来。这一点是新手最容易踩的坑。
本地调试阶段,我建议先不接OTLP导出器,把OTLPTraceExporter换成ConsoleSpanExporter,在控制台直接看span结构,确认链路数据确实产生之后,再切到OTLP。这种先控制台、再后端的调法能帮你快速判断问题出在埋点侧还是导出侧。
3. 核心配置细节:导出器、资源、采样
3.1 导出器怎么选:OTLP和Collector
导出器是OTel连接后端的桥梁。官方生态里常见的导出器有这么几类:Consoler是调试用的;OTLP是OTel自己的标准协议,用来发给OTel Collector或者支持OTLP的后端;Jaeger、Zipkin、Prometheus这类直接导出器则绕过Collector直连某一后端。
如果你只是单机测试,应用直连Jaeger或Prometheus没问题。但上了生产,我强烈建议在应用和后端之间加一层OTel Collector。Collector扮演的角色类似消息队列里的代理,应用只把OTLP数据发给Collector,由Collector做数据接收、预处理、批量转发、失败重试,再分发给不同后端。这样做的好处有三个:一是应用侧的导出压力小,不用自己处理重试和积压;二是可以同时往Jaeger发trace、往Prometheus发metrics,一鱼多吃;三是切换后端的时候,只改Collector配置,应用完全不用动。
我用的Collector配置简化后长这样:
receivers: otlp: protocols: http: exporters: jaeger: endpoint: http://jaeger:14250 prometheus: endpoint: 0.0.0.0:9464 service: pipelines: traces: receivers: [otlp] exporters: [jaeger] metrics: receivers: [otlp] exporters: [prometheus]3.2 资源属性:给监控数据打上身份标签
Resource这个概念容易被忽略,但它非常关键。Resource是附在trace和metric上的一组键值对,用来描述数据的来源:服务叫什么、部署在哪个环境、跑在哪台机器上。如果没有这些属性,多个服务的数据混在同一个Jaeger或者Prometheus里,你根本分不清哪条链路是订单服务的、哪条是支付服务的。
SERVICE_NAME是最重要的一个属性,它相当于服务在监控系统里的身份证。所有看板、告警、搜索,基本都依赖这个名字来过滤。我建议不要把服务名硬编码在代码里,而是通过环境变量OTEL_SERVICE_NAME注入,同一份代码在测试环境和生产环境可以显示不同的服务名而不需要改代码。
我给生产环境配的资源属性通常还有这三个:
deployment.environment:dev、staging、prod,区分环境。service.version:当前部署版本,出问题能直接定位到代码版本。host.name:主机名,容器化环境下用Pod名,方便定位实例。
3.3 采样率怎么定:不要一股脑全采
OTel默认的采样行为对全链路Span全量采集。在低流量环境没问题,但生产环境流量一大,全量采集会带来两个问题:一是存储成本爆炸,二是导出器在批量发送时产生不小的CPU和内存开销。
我自己的经验是,90%以上的服务根本不需要100%采样。一个高频接口一天几百万次调用,你采样10%也足够看出延迟趋势和错误率。就算某个极端问题恰好没被采到,你还可以靠日志里的traceId去手动重放或追加采集。
采样器我推荐用ParentBasedSampler结合TraceIdRatioBasedSampler:
const { ParentBasedSampler, TraceIdRatioBasedSampler } = require('@opentelemetry/core'); const sdk = new NodeSDK({ sampler: new ParentBasedSampler({ root: new TraceIdRatioBasedSampler(0.1), }), // 其他配置略 });ParentBasedSampler的逻辑是:如果当前请求已经有父span(比如上游服务已经决定采样),那这个span也跟随采样;只有根span才按比例采样。这样可以保证一条链路上的span要么全有要么全没有,不会出现"请求被采样了但内部子span全丢了"的断链情况。
采样率的设置我建议按服务重要程度分档:核心交易链路5%到10%足够;排查专用的调试环境可以100%;低频的内部管理后台全采也没问题,成本低。
4. 从自动埋点到手动埋点:实战记录
4.1 自动埋点能覆盖的范围
getNodeAutoInstrumentations()能自动插桩的模块比我预想的多很多,包括HTTP/HTTPS、Express、Fastify、MongoDB、MySQL、PostgreSQL、Redis、AWS SDK、gRPC、GraphQL等。只要你的项目依赖这些模块,SDK启动后就会自动为它们创建span,不需要在每个请求处理器里写任何埋点代码。
这些自动span的质量相当不错。以HTTP请求为例,一个进来的请求会生成一个根span,记录HTTP方法、路由、状态码、客户端地址;如果是Express处理,还会在根span下挂一个Express中间件处理的子span;如果处理过程中查了MongoDB,又会往下挂一个MongoDB查询span。刚开始接入的时候,你不需要写任何业务埋点,就能在Jaeger里看到一条完整的调用链,这对存量项目的监控救急特别有价值。
有一点要注意:fs这类文件系统操作也在自动插桩范围内。如果你的服务本来就有高频的日志写入或临时文件读写,这些操作会产生比HTTP请求多得多的span,很容易刷屏干扰排查。我后来在自动插桩配置里把fs类的插桩关掉了,只在需要的时候手动埋。
4.2 手动埋点:把业务逻辑串成span
自动插桩能覆盖框架层,但覆盖不了业务语义。比如"创建订单"这个操作,在自动插桩视角下只是一堆数据库插入语句和HTTP调用,它看不出这是一次下单行为。所以核心业务方法我习惯手动加一层业务span。
我常用的写法是startActiveSpan,它能自动把新span挂到当前异步上下文里:
const { trace, SpanStatusCode } = require('@opentelemetry/api'); const tracer = trace.getTracer('order-service'); async function createOrder(orderData) { const newSpan = tracer.startActiveSpan('order.create', async (span) => { span.setAttribute('order.id', orderData.id); span.setAttribute('order.userId', orderData.userId); try { const saved = await orderRepository.save(orderData); await callPaymentGateway(orderData); span.setStatus({ code: SpanStatusCode.OK }); return saved; } catch (err) { span.recordException(err); span.setStatus({ code: SpanStatusCode.ERROR }); throw err; } finally { span.end(); } }); return await newSpan; }这里有几个细节值得注意。第一,order.id、order.userId这类业务属性一定要打上,没有这些属性,后面查链路的时候你根本不知道这根span对应的是哪一单。第二,异常发生时除了设置ERROR状态,还应该记录异常对象,recordException会把异常堆栈附在span上。第三,span.end()必须放在finally里,保证即使抛异常,span也会正常结束,不会泄漏一个永远挂着的span。
4.3 自定义业务指标:计数器与直方图
Trace能告诉你一次请求慢在哪,但你要回答"今天订单量比昨天涨了多少"这类问题,得靠Metrics。OTel的Metrics API写起来也很直接。
我举两个最常用的例子。计数器用来统计业务事件总量:
const { metrics } = require('@opentelemetry/api'); const meter = metrics.getMeter('order-service'); const orderCounter = meter.createCounter('orders.created.total', { description: '累计创建订单数', }); // 下单成功后调用 orderCounter.add(1, { paymentMethod: 'wechat' });直方图用来记录耗时分布,适合统计第三方调用的延迟:
const httpClientDuration = meter.createHistogram('payment.gateway.duration', { description: '支付网关调用耗时', unit: 'ms', }); const start = Date.now(); try { await callPaymentGateway(orderData); } finally { httpClientDuration.record(Date.now() - start, { result: 'success' }); }自定义指标要想起作用,SDK里得配置metric导出器。只配了traceExporter,指标数据是发不出去的。
4.4 跨服务调用追踪怎么连起来
微服务架构下,A服务调用B服务,监控数据要能串成一条完整链路。OTel对这一块的处理是自动的:HTTP插桩在发出请求时,会自动往Header里注入traceparent、tracestate、baggage这几个HTTP头,下游服务收到请求后,会从Header里解析出父上下文,把自己的span接到上游的span下。
所以只要两个服务都接了OTel,且都开了HTTP插桩,跨服务链路就能自动串起来,不需要你手动传递任何东西。我在实际项目里验证过,A服务发HTTP请求调B服务,在Jaeger里看到的就是一条从上到下的完整trace,中间没有任何断点。
baggage这个Header值得一提。它是用来传业务键值对数据的,比如把userId、orderId放进baggage,下游服务就能在span里看到这些业务信息。但baggage会随每个请求头发送,别往里塞大对象,只放轻量的标识数据。
5. 数据可视化与告警:Prometheus + Grafana + Jaeger
5.1 指标如何暴露给Prometheus
OTel的Metrics要接到Prometheus,常见有两种方式:应用直接暴露Prometheus格式的metrics端点,或者通过Collector把OTLP指标转成Prometheus格式。我前期测试用的第一种,部署简单,直接在应用里挂一个Prometheus导出器:
const { PrometheusExporter } = require('@opentelemetry/exporter-prometheus'); const prometheusExporter = new PrometheusExporter({ port: 9464 }); const sdk = new NodeSDK({ metricReader: prometheusExporter, // 其他配置略 }); sdk.start();启动后,应用会在9464端口暴露一个metrics端点。Prometheus只需要把这个端口加到抓取配置里:
scrape_configs: - job_name: 'node-app' static_configs: - targets: ['localhost:9464']注意,这是直连模式。生产环境我仍然推荐走Collector,因为Prometheus每次来抓取,应用侧都要实时渲染所有样本,高并发服务在Prometheus抓取周期内会有瞬时CPU毛刺。Collector可以先把指标聚合成非时序的样本,缓解这个压力。
5.2 Grafana看板怎么搭
Grafana是数据展示层,数据源指向Prometheus,然后开始拼图表。我常用的Node.js服务监控面板通常包含这几个图:
请求量用rate函数:
sum(rate(http_server_duration_count[5m])) by (service_name)错误率用状态码过滤:
sum(rate(http_server_duration_count{status_code=~"5.."}[5m])) by (service_name) / sum(rate(http_server_duration_count[5m])) by (service_name)P95延迟用直方图聚合:
histogram_quantile(0.95, sum(rate(http_server_duration_bucket[5m])) by (le))这些指标名并不是我起的,而是OTel HTTP自动插桩按照语义约定输出的标准指标。http_server_duration是HTTP服务端请求耗时的直方图,属性里带status_code和route,所以上面的PromQL可以直接用。如果你发现指标名对不上,多半是版本差异,可以用sum({__name__=~"http.*"})在Prometheus里先搜一下实际指标名。
看板建议按服务维度做一行摘要视图,一眼看到每个服务的请求量、错误率、P95延迟,然后再往下钻去看单个服务的详细链路。JetBrains式的所有指标堆一块的大面板,在排查问题时反而不好用。
5.3 告警规则怎么配
监控的最终目的是告警。Prometheus的告警规则我用rules文件统一管理,下面这个是我给订单服务配的“错误率超5%”告警:
groups: - name: order-service rules: - alert: OrderServiceErrorRateHigh expr: > sum(rate(http_server_duration_count{status_code=~"5.."}[5m])) / sum(rate(http_server_duration_count[5m])) > 0.05 for: 5m labels: severity: warning annotations: summary: "订单服务5分钟错误率超过5%"for: 5m这个参数很重要,它要求条件持续5分钟才触发告警,可以过滤掉瞬时的抖动和发布期间的短时波动。告警发到Alertmanager之后,再接入钉钉、企微或飞书机器人,这些都属于常规操作,网上有大量现成配置,我就不展开了。
只提醒一点:告警规则一定要配上可以定位的信息,比如服务名、命名的链接。没有上下文信息的告警,等告警真的响起来,你还要花时间查这是哪个服务的哪个指标,告警效率会大打折扣。
6. 踩坑实录与常见问题排查
接入OTel的过程中,我把各个版本的坑踩了个遍。下面这些问题是我在实际项目中真实遇到过的,每条都附了排查思路和解法。
6.1 依赖版本不一致导致数据全空
OTel生态的包版本号非常敏感。核心包是1.x版本,插桩器包是0.x版本,而且不同的minor版本之间可能会有协议或API变动。最典型的症状是:SDK启动不报错,业务代码也不报错,但Jaeger里就是一片空白。
我唯一一次遇到全空的情况,就是@opentelemetry/sdk-node取了0.4x版本,插桩器取了0.5x版本,核心包和插桩器的兼容性出了问题。排查方式很笨但有效:
npm ls @opentelemetry/core @opentelemetry/api @opentelemetry/sdk-node把所有@opentelemetry/*包列出来,把版本号对齐到同一个版本线。尤其是@opentelemetry/api,它和SDK的版本不匹配,会导致SDK内部拿到的API实现和业务代码里用的不是同一个实例,手动埋点全部失效。
6.2 异步回调里拿不到span
Node.js是异步模型,OTel的上下文是靠AsyncLocalStorage传播的,但AsyncLocalStorage只能传播当前上下文范围内的异步资源。像setTimeout、setInterval、事件监听器这类场景,回调默认不会自动继承上下文,导致你在回调里创建的span成了一个孤儿span。
我的解决办法是显式把上下文传进回调:
const { context, trace } = require('@opentelemetry/api'); const currentCtx = context.active(); setTimeout(() => { context.with(currentCtx, () => { const span = trace.getTracer('worker').startSpan('order.timeout.handle'); // 业务逻辑 span.end(); }); }, 1000);写异步任务框架的时候尤其注意,比如BullMQ、RabbitMQ消费者这类基于事件的场景,消息回调的时候原请求上下文已经结束,必须自己决定要不要新建根span,还是用上游传递的上下文续接链路。这块设计要提前想清楚,否则生产环境数据会断链断得很诡异。
6.3 ESM项目装上自动埋点却看不到span
纯ESM项目接入OTel,比CommonJS麻烦不少。原因在于ESM的模块加载是异步的,而且模块在import之后就被缓存,插桩器如果没在最开始就patch到模块,业务代码后续就再也patch不上了。
我的建议分两种情况。如果是老项目,最稳的方案是保留一个CommonJS格式的入口文件,用node --require ./tracing.js的方式启动,业务代码继续ESM也不影响。如果是纯ESM项目,就得用Node的loader机制在模块解析阶段完成插桩,具体配置在不同Node版本下还有差异。奉劝一句:不要在项目改造期同时搞ESM迁移和OTel接入,两件事叠加起来,排查问题的难度会指数级上升。
6.4 健康检查接口刷屏
Kubernetes部署后,/healthz和/metrics这类探活接口每几秒就会被kubelet打一次,如果这些请求也生成span,链路数据会被大量健康检查请求淹没。我在自动插桩配置里直接把这些路径过滤掉:
const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node'); const sdk = new NodeSDK({ instrumentations: [getNodeAutoInstrumentations({ '@opentelemetry/instrumentation-http': { ignoreIncomingPaths: [/\/healthz/, /\/metrics/], }, })], });这个配置只影响插桩,不影响业务对请求的正常处理。加了之后Jaeger里的数据瞬间清净了很多。
6.5 进程退出时丢span
Node.js进程退出时,如果还有批量未导出的span,就可能在内存里直接消失。批量导出器是攒够一批或者到时间才发送的,进程瞬间退出,最后一两批数据很容易丢。
解决办法是监听退出信号,优雅关闭SDK:
['SIGTERM', 'SIGINT'].forEach((signal) => { process.on(signal, async () => { await sdk.shutdown(); process.exit(0); }); });shutdown()方法会强制把当前缓冲的span和metrics全部flush出去。这个处理尤其在发布扩容、滚动重启频繁的场景下重要,否则你会看到每次发布前后那几分钟的监控数据总是有缺口。
6.6 常见问题速查表
| 症状 | 可能原因 | 解决办法 |
|---|---|---|
| Jaeger看不到任何trace | 版本不兼容 | 统一所有@opentelemetry/*包版本 |
| 只有HTTP span,没有Express子span | Express插桩未生效 | 确认express插桩器已加载,且版本支持当前Express |
| 手动埋点不生效 | @opentelemetry/api和SDK实例不一致 | 检查api包版本,确保只有一个副本 |
| 启动报patch错误 | 插桩器和被插桩库版本不兼容 | 升级被插桩库到符合要求的版本 |
| 异步回调span断链 | 上下文未显式传入回调 | 用context.with()包裹回调 |
| 监控数据导出延迟高 | 批量导出积累等待 | 调整BatchSpanProcessor的批量导出间隔 |
| 进程退出丢数据 | 未优雅关闭SDK | 监听退出信号并调用sdk.shutdown() |
7. 生产落地建议:从试点到全量
如果团队是从零开始接OTel,我不建议上来就在核心业务全量铺开。我的落地节奏一般分三步。
第一步,选一个流量不大、逻辑不复杂的服务做试点。先用Console导出器验证链路能出来,然后接上OTLP和Jaeger,跑一天看看数据量级,建立基本的看板。这一步的目的是让团队熟悉OTel的工作机制。
第二步,在试点服务上补手动埋点,把核心业务链路串起来,同时把自定义指标加上,接到Prometheus和Grafana。这一步要让团队掌握埋点的写法,也验证监控数据到告警的完整闭环。
第三步,把积累下来的配置、埋点规范、坑位清单沉淀成团队文档,再批量复制到其他服务。这一步最忌讳每个团队各自为战,你埋一套他埋一套,最后数据格式五花八门,等于放弃了OTel最大的优势:标准统一。
另外,环境变量配置从第一天就要规范。OTEL_SERVICE_NAME、OTEL_EXPORTER_OTLP_ENDPOINT、采样率这些都要走配置中心或环境变量,别hardcode在代码里。我看到过不少项目,SDK初始化时把服务名写死在代码里,后面换环境部署,监控数据全串了,排查起来极其痛苦。
最后说句个人体会。OTel部署初期我犯的最大错误是想一口吃成胖子,把Redis、MongoDB、外部调用、文件操作全埋上,结果排查问题时光过滤噪声就花了半天。后来我把埋点分阶段上:先HTTP加Express让主干链路可见,再补数据库和关键业务span,最后才上自定义指标。这套顺序两周内就能让一个中等规模服务完成全量接入,而且不影响线上稳定。建议你也按这个节奏来,先让监控跑起来,再慢慢变细。