Node.js'te OpenTelemetry GenAI ile agent izlemek — spec oturmadanInstrumenting agents with OpenTelemetry GenAI in Node.js — before the spec settles
GenAI semantic convention'ları gerçek, faydalı ve hâlâ Development statüsünde. Kod tabanını o isimlere doğrudan bağlarsanız her rename bir migration; beklerseniz agent'larınızın en az güvenilir dönemini kör uçarsınız. Çıkış yolu eski bir tavsiye: kendi facade'ınıza enstrümante edin, onların sözlüğünü kenarda üretin.
The GenAI semantic conventions are real, useful, and still in Development status. Bind your codebase to those names directly and every rename is a migration; wait, and you fly blind through your agents' least trustworthy period. The way out is old advice: instrument against your own facade, emit their vocabulary at the edge.
Naylalabs · MühendislikEngineeringgen_ai.* eşlemesi semantic-conventions-genai reposunun bu tarihteki haline göre — statü: Development, stable hiçbir şey yok. Kod, trace ve ölçümler: agent-native-ops-demo (tag v0.3.0; ham trace JSON'u repo'da).
Snapshot: Last verified August 1, 2026. The gen_ai.* mapping follows semantic-conventions-genai as of that date — status: Development, nothing stable. Code, trace and measurements: agent-native-ops-demo (tag v0.3.0; the raw trace JSON is in the repo).
Bu yıl agent'larına "observability ekleyelim" diyen her ekibi bekleyen bir tuzak var ve şekli bir dokümantasyon sayfası. OTel GenAI convention'ları model çağrılarını, tool çalıştırmayı ve agent span'larını kapsıyor; MCP 2026-07-28 de trace context'i _meta içinde resmileştirdi. Ama neredeyse her şey Development işaretli: attribute isimleri daha önce değişti, tekrar değişmelerini engelleyen hiçbir şey yok.
There is a trap waiting for every team that decides to "add observability" to their agents this year, and it is shaped like a documentation page. The OTel GenAI conventions cover model calls, tool execution and agent spans; MCP 2026-07-28 also formalized trace context inside _meta. But nearly everything is marked Development: attribute names have been renamed before, and nothing prevents it happening again.
Bu yazının iddiası dar ve pratik: tek dosyalık bir sözlük katmanı, stabilite sorusunu büyük ölçüde anlamsızlaştırır — ve agent debug'ının asıl hazinesi olan şeyi hemen verir: retry'ların iki denemesinin kardeş kaldığı tek yer olan trace'i. Hepsini demo repo'da çalıştırıp ölçtük.
This post's claim is narrow and practical: a one-file vocabulary layer makes the stability question mostly irrelevant — and immediately buys the real treasure of agent debugging: the trace, the only place where a retry's two attempts remain siblings. We ran and measured all of it in the demo repo.
01Tek-dosya kuralı: rename size migration'a değil, diff'e mal olmalıThe one-file rule: renames should cost you a diff, not a migration
Bütün savunma tasarımı, tek bir modül sınırı. Uygulama kodu sizin domain dilinizi konuşur; semconv'u yalnızca bir eşleme dosyası konuşur:
The entire defensive design is a single module boundary. Application code speaks your domain language; only one mapping file speaks semconv:
// Kod tabanında gen_ai.* isimlerini anmasına izin verilen TEK dosya.// The ONLY file in the codebase allowed to mention gen_ai.* names. export const mapToolCall = (op: ToolCallInfo): Attributes => ({ 'gen_ai.operation.name': 'execute_tool', 'gen_ai.tool.name': op.toolName, 'mcp.method': op.mcpMethod, ...(op.runId ? { 'run.id': op.runId } : {}), }); export const mapAgentRun = (op: AgentRunInfo): Attributes => ({ 'gen_ai.operation.name': 'invoke_agent', 'agent.id': op.agentId, 'run.id': op.runId, 'on_behalf_of': op.onBehalfOf, });
Convention'lar bir şeyi yeniden adlandırdığında — Development statüsü "eğer" değil "ne zaman" demek — değişiklik bu dosyada bir diff artı bir dashboard sorgusu güncellemesi. Yüz çağrı noktanız hiçbir şey olmadığını sanmaya devam eder. Sözü, yazacağınız en ucuz testle mühürleyin: üretilen attribute isimlerini CI'da snapshot'layın ve snapshot'ı yalnızca başlığı "semconv upgrade" olan bir PR'da güncelleyin. Bu, serinin tool contract'larına uyguladığı disiplinin aynısı: telemetri sözlüğünüz, gelecekteki dashboard'larınızla yapılmış bir sözleşmedir.
When the conventions rename something — and Development status means when, not if — the change is a diff in this file plus a dashboard query update. Your hundred call sites don't know anything happened. Seal the promise with the cheapest test you will ever write: snapshot the emitted attribute names in CI, and bump the snapshot only in a PR titled "semconv upgrade". It is the same discipline this series applies to tool contracts: your telemetry vocabulary is a contract with your future dashboards.
02MCP sınırını geçmek: trace context artık protokolün içindeCrossing the MCP boundary: trace context now travels inside the protocol
2026-07-28, traceparent/tracestate/baggage anahtarlarını _meta içinde resmileştirdi (SEP-414). Client, aktif span'ın context'ini isteğe enjekte eder; sunucu tarafında MCP-Nest bunu ctx.getTraceContext() ile verir ve facade çağıranın trace'ini devam ettirir:
2026-07-28 formalized the traceparent/tracestate/baggage keys inside _meta (SEP-414). The client injects the active span's context into the request; on the server, MCP-Nest exposes it via ctx.getTraceContext() and the facade continues the caller's trace:
async toolCall<T>(mcpCtx: McpContext, info: ToolCallInfo, fn: (span: Span) => Promise<T>) { const carrier = mcpCtx.getTraceContext(); // _meta'dan (SEP-414) const parentCtx = propagation.extract(context.active(), carrier ?? {}); return this.tracer.startActiveSpan( `execute_tool ${info.toolName}`, { attributes: mapToolCall(info) }, parentCtx, async (span) => { /* …çalıştır, status'u işle, span.end() *//* …run it, handle status, span.end() */ }, ); }
Neden hayati: retry'lar protokol seviyesinde yeni request-id üretir — loglar denemeleri birbirine bağlayamaz. İki denemenin kardeş kaldığı tek yer trace'tir. Ve her span'a run.id damgalamak, audit logunuzla tek pivotta buluşmayı sağlar: audit olduğunu söyler, trace nedenini — v0.3.0'dan itibaren demo repo'nun audit satırları trace_id alanını da taşıyor.
Why this is vital: retries produce new request IDs at the protocol level — logs cannot tie attempts together. The trace is the only place the two attempts remain siblings. And stamping run.id on every span lets you meet your audit log at a single pivot: audit says that it happened, the trace says why — and as of v0.3.0 the demo repo's audit lines carry a trace_id field too.
03Gerçek koşu, gerçek ağaçA real run, a real tree
Aşağıdaki her sayı, deterministik bir senaryo agent'ının (scripts/scenario-agent.mjs — LLM yok, plan sabit; amaç trace'in kendisi) tek koşusundan: invoke_agent kök span'ını script açar, her MCP isteğine traceparent enjekte eder ve bir create_invoice çağrısında 300 ms'de stream'i bilerek koparıp aynı idempotency_key ile retry eder. Jaeger'dan dışa aktarılmış ham trace (results/trace-scenario.json, traceId e07570d1…) repo'da:
Every number below comes from a single run of a deterministic scenario agent (scripts/scenario-agent.mjs — no LLM, fixed plan; the trace itself is the point): the script opens the invoke_agent root span, injects traceparent into every MCP request, and on one create_invoice call deliberately breaks the stream at 300 ms and retries with the same idempotency_key. The raw trace exported from Jaeger (results/trace-scenario.json, traceId e07570d1…) is in the repo:
create_invoice denemesi üst üste biniyor: client 1. denemeyi terk edip retry'ı atmışken sunucu hâlâ ilk isteği işliyordu — logların asla gösteremeyeceği, yalnızca trace'in görünür kıldığı bir gerçek.
Figure 1. The bars are the real offsets/durations of the trace exported from Jaeger. The two create_invoice attempts overlap: while the client had abandoned attempt 1 and fired the retry, the server was still processing the first request — a fact logs can never show and only the trace makes visible.
Waterfall'u muhasebeci gibi okuyun: aynı idempotency_key'i taşıyan iki kardeş span, retry sözleşmenizin çalıştığının kanıtı — 1 204 ms'lik ilk deneme işi bir kez yaptı, 2 ms'lik ikincisi sadece sonucu replay etti. Bu iki barın üst üste binmesi ayrıca şunu da söylüyor: "client gitti" sunucu için "iş bitti" demek değildir; kapasite planınız terk edilmiş isteklerin kuyruğunu da taşır.
Read the waterfall like an accountant: two sibling spans carrying the same idempotency_key are proof your retry contract works — the 1,204 ms first attempt did the work once, the 2 ms second one merely replayed the result. The overlap of those two bars also says something else: "the client left" does not mean "the work stopped"; your capacity plan carries the tail of abandoned requests too.
04Content capture: telemetri bayrağı kılığında bir veri-yönetişimi kararıContent capture: a data-governance decision wearing a telemetry flag
Convention'ların gen_ai.input.messages ailesi, her prompt'u ve completion'ı tracing backend'inize seve seve kaydeder. Kaydetmeli mi sorusu observability sorusu değil: prompt'lar, kullanıcı ne yazdıysa onu içerir ve tracing backend'iniz artık PII tutuyordur — kendi retention politikasıyla. Production'da varsayılan kapalı; örneklenmiş debug oturumları için istek-başına açın (MCP'nin _meta içindeki istek-başına logLevel mekanizması doğal çengel). Orta yol, audit tasarımının aynası: digest ve boyutlar her zaman, tam içerik yalnızca kendi retention'ı olan explicit bir bayrak altında.
The conventions' gen_ai.input.messages family will happily record every prompt and completion into your tracing backend. Whether it should is not an observability question: prompts contain whatever users typed, and your tracing backend now holds PII — with a tracing backend's retention policy. Default off in production; enable per-request for sampled debug sessions (MCP's per-request logLevel in _meta is the natural hook). The middle path mirrors the audit design: digests and sizes always, full content only under an explicit flag with its own retention.
05Model çağrıları ve token'lar: aynı facade, bir metod dahaModel calls and tokens: same facade, one more method
Dürüst kapsam notu: demo repo'nun senaryo agent'ı deterministik — model çağrısı yapmıyor; bu yazının ölçtüğü şey tool-katmanı. Model çağrıları girdiğinde deseni değiştirmezsiniz, facade'a bir metod eklersiniz: modelCall(), gen_ai.request.model + gen_ai.usage.input_tokens/output_tokens + finish_reasons'ı aynı eşleme dosyasından üretir. Token'lar, önceden paraya çevrilmiş gelen tek metriktir — çarpma işlemiyle faturaya dönerler. Rollup eksenleriniz de hazır: koşu, agent, principal — kotaların ölçtüğü üçlünün aynısı. İnsanları masaya eğilten toplam, çağrı başına değil tamamlanan iş başına maliyettir; onu da run.id zaten mümkün kılıyor.
An honest scope note: the demo repo's scenario agent is deterministic — it makes no model calls; what this post measures is the tool layer. When model calls arrive you don't change the pattern, you add one method to the facade: modelCall() emits gen_ai.request.model + gen_ai.usage.input_tokens/output_tokens + finish_reasons from the same mapping file. Tokens are the only metric that arrives pre-monetized — they convert to invoices by multiplication. Your rollup axes are ready too: run, agent, principal — the same trio quotas meter. The total that makes people lean in is cost per completed task, not per call; run.id already makes that possible.
06Instrumentasyonun kendisi ne kadara mal oluyorWhat the instrumentation itself costs
Kendi overhead'ini söylemeyen observability tavsiyesi pazarlamadır. Enstrümante edilmiş bir tool'a (search_start) aynı yükü iki konfigürasyonda verdik — OTEL_ENABLED=0 ve =1 (BatchSpanProcessor → OTLP → lokal Jaeger); k6, 200 istek/sn, 45 sn, lokal makine:
Observability advice that omits its own overhead is marketing. We drove identical load at an instrumented tool (search_start) in two configurations — OTEL_ENABLED=0 and =1 (BatchSpanProcessor → OTLP → local Jaeger); k6, 200 req/s, 45 s, local machine:
| KonfigürasyonConfiguration | p50 | p95 | p99 |
|---|---|---|---|
| Telemetri kapalıTelemetry off | 2.19 ms | 4.72 ms | 9.08 ms |
| Telemetri açık (batch + OTLP)Telemetry on (batch + OTLP) | 2.15 ms | 4.12 ms | 6.54 ms |
Dürüst okuma: bu yükte fark ölçüm gürültüsünün içinde — "açık" kolun nominal olarak daha hızlı çıkması da tam olarak bunun kanıtı; overhead iddia edilecek kadar bile sinyal yok. Batching'in işi bu. Node özelinde iki kural gene de her şeyden önemli: BatchSpanProcessor kullanın (simple processor'ın span-başına export'u yük altında event loop'u tıkar) ve sampling'e irtifa başına karar verin — mecbursanız model span'larını örnekleyin ama execute_tool span'larını %100'de tutun: side effect'ler orada yaşar ve örnekleme kurbanı olmuş bir side effect, açıklanamayan bir incident'tır.
The honest reading: at this load the difference is inside measurement noise — the "on" leg coming out nominally faster is precisely the proof; there is not even enough signal to claim an overhead. That is batching doing its job. Two Node-specific rules still matter more than everything else: use the BatchSpanProcessor (the simple processor's per-span export stalls the event loop under load), and decide sampling per altitude — sample model spans if you must, but keep execute_tool spans at 100%: side effects live there, and a sampled-away side effect is an unexplainable incident.
07Bir sonraki semconv sürümünden sağ çıkmakSurviving the next semconv release
- Pinle ve söyle. Bu yazının tepesindeki snapshot satırı süs değil; dashboard'larınız da aynı pini taşımalı.
- Pin and say so. The snapshot line at the top of this post is not decoration; your dashboards should carry the same pin.
- Tek eşleme dosyası — snapshot-testli, yalnızca bilinçli PR'larla güncellenen.
- One mapping file — snapshot-tested, upgraded only in deliberate PRs.
- Dashboard'lar view'lardan sorgulasın, ham attribute isimlerinden değil — rename'i arkeolojiden bakıma çeviren bir dolaylama daha.
- Query through views, not raw attribute names — one more indirection that turns renames into maintenance instead of archaeology.
- Blog yazılarını değil, convention repo'sunun release'lerini izleyin. Bu yazının changelog'u, neyin değiştiğini itiraf edeceği yer.
- Watch the conventions repo's releases, not blog posts about them. This post's changelog is where it will admit what changed.
Bunların hiçbiri egzotik değil: anti-corruption layer deseninin telemetriye uygulanmışı. Alternatifin — Development statüsünde bir sözlükle evlenmiş kod tabanının — ufukta bir boşanması var.
None of this is exotic: it is the anti-corruption layer pattern applied to telemetry. The alternative — a codebase married to a Development-status vocabulary — has a divorce coming.
08Sık sorulanlarFAQ
Convention'ların oturmasını beklemeli miyim?
Hayır. Facade bir öğleden sonraya mal olur ve stabilite sorusunu anlamsızlaştırır. Retry görünürlüğüne şimdi ihtiyaç var — agent'lar yanlış davranmak için spec'in oturmasını beklemiyor.
Should I wait for the conventions to stabilize?
No. The facade costs an afternoon and makes the stability question irrelevant. You need retry visibility now — agents do not wait for specs to settle before misbehaving.
Ham OTel yerine bir LLM-observability vendor'ı kullanamaz mıyım?
Çoğu OTLP yutuyor ve giderek aynı convention'ları konuşuyor; facade sizi vendor'lara girerken de çıkarken de taşınabilir kılar. Önemli karar facade; backend bir tercih.
Can I use an LLM-observability vendor instead of raw OTel?
Most ingest OTLP and increasingly speak these same conventions; the facade keeps you portable into and out of vendors. The facade is the decision that matters; the backend is a preference.
Model sağlayıcısının tarafını da trace edebilir miyim?
Edemezsiniz — kendi sınırınızı trace edersiniz: istek attribute'ları, token kullanımı, latency, finish reason'lar. "Bu koşu neden yavaştı/pahalıydı" sorusuna bu yeter; sağlayıcının içi onların telemetrisi.
Can I trace the model provider's side too?
You cannot — you trace your boundary to them: request attributes, token usage, latency, finish reasons. That answers "why was this run slow/expensive"; what happens inside the provider is their telemetry.
Eval'lar nereye oturuyor?
Farklı katman: tracing ne yaptığını söyler, eval ne yapmış olması gerektiğini. run.id'de buluşurlar — o kimliğin var olması gerekmesinin bir sebebi daha.
Where do evals fit?
A different layer: tracing says what a run did, evals judge what it should have done. They meet at run.id — one more reason that identifier needs to exist.
09Changelog
- 1 Ağustos 2026 — İlk yayın; semconv eşlemesi 1 Ağustos 2026 tarihli repo haline, kod demo repo
v0.3.0'a göre doğrulandı. - August 1, 2026 — Initial publication; semconv mapping verified against the repo as of Aug 1, 2026, code against demo repo
v0.3.0.