X-Ray Service Maps Lie: Fix Missing Spans in Node Trace Edges

X-Ray Service Maps Lie: Fix Missing Spans in Node Trace Edges

X-Ray service maps that show a lonely Lambda with no downstream edges are usually instrumentation gaps—not “the network vanished.” The unfair advantage is closing Node gaps: AWS SDK v3 middleware, undici/fetch, and async context loss so edges match reality during sev-1 triage.

⚡ TL;DR: Use ADOT or Powertools with SDK v3 capture; instrument undici explicitly; propagate X-Amzn-Trace-Id / W3C traceparent across HTTP; never trust a map that lacks edges you know exist—verify with raw traces. Pair with Lambda ADOT vs Powertools and OpenTelemetry for LLMs.

Common Node edge killers

Gap Symptom on map Fix
SDK v3 without middleware No DynamoDB/S3 edges @aws-sdk/middleware-instrumentation / ADOT auto
Global fetch / undici No HTTP peers OTel undici instrumentation or wrap dispatcher
Fire-and-forget void promise Truncated subsegments Await or attach to parent context
Lambda response streaming Partial traces Ensure segment closed on finally
Manual HTTP without header Broken trace ID Inject trace header
// ✅ AWS SDK v3 — ensure X-Ray / OTel middleware is registered once
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { NodeHttpHandler } from "@smithy/node-http-handler";

const ddb = new DynamoDBClient({
  requestHandler: new NodeHttpHandler({ connectionTimeout: 2000, requestTimeout: 5000 }),
});
// With ADOT Lambda layer + env AWS_OTEL / instrumentations enabled, edges appear.
// ✅ undici: instrument Agent used by fetch
import { Agent, fetch as undiciFetch } from "undici";
import { diag } from "@opentelemetry/api";

const agent = new Agent({ connect: { timeout: 2000 } });
// Register @opentelemetry/instrumentation-undici against this Agent in tracing bootstrap

export async function callPeer(url: string, parentHeaders: HeadersInit) {
  const res = await undiciFetch(url, { dispatcher: agent, headers: parentHeaders });
  // ❌ bare fetch() without instrumentation → invisible edge
  return res;
}

AsyncLocalStorage / context loss

// ❌ Loses parent subsegment
setImmediate(() => ddb.send(cmd));

// ✅ Bind context (OTel context.with / X-Ray captureAsyncFunc)
import { context, trace } from "@opentelemetry/api";
const ctx = context.active();
setImmediate(() => context.with(ctx, () => ddb.send(cmd)));

Triage habit when maps lie

  1. Open a single trace ID from the failing request (ALB/Lambda log).
  2. Confirm subsegments exist in raw JSON even if map collapsed them.
  3. Diff instrumented vs missing clients (search for new Agent, raw http.request).
  4. Fix library bootstrap—not the map UI.

Closing checklist

✅ Dos
– ✅ Boot tracing before importing app routes
– ✅ Instrument SDK v3 and undici explicitly
– ✅ Propagate trace headers on every egress
– ✅ Sample higher during incidents (X-Ray Sampling)
– ✅ Canary a trace that must include DDB+HTTP edges in CI

❌ Don’ts
– ❌ Don’t trust empty service maps over logs
– ❌ Don’t create clients lazily after disabling auto-instrumentation
– ❌ Don’t swallow errors inside fire-and-forget tasks
– ❌ Don’t mix X-Ray and OTel exporters without a clear gateway story

Related reading

Last updated on September 11, 2026


Discover more from CheatCoders

Subscribe to get the latest posts sent to your email.

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply