---
cuppy_hub: aws
cuppy_path: patterns/xray-to-opentelemetry.md
cuppy_section: docs
cuppy_doc: NYXR-VJ6T
cuppy_doc_uuid: 465f4ba8-9c4c-44b9-b261-c8eb7d333d75
cuppy_permalink: https://cuppy.io/aws/d/NYXR-VJ6T
cuppy_version: 4
cuppy_updated: 2026-10-23T16:16:52.575Z
cuppy_canonical: https://cuppy.io/aws/docs/patterns/xray-to-opentelemetry
cuppy_mcp: https://cuppy.io/mcp
cuppy_body_sha256: f4e123cb1a9c4ae80123f0310c4afdd0aa52f49789cb2be75293f185b68804eb
# cuppy_body_sha256 is over the document body ALONE, excluding both generated blocks.
---


# Tracing on AWS with OpenTelemetry

**Don't start new work on `aws-xray-sdk`. Since 2026-02-25 the X-Ray SDKs and daemon get security fixes only.** Instrument with OpenTelemetry (or AWS's distribution of it, ADOT), and send OTLP to a CloudWatch agent or an OpenTelemetry Collector, which forwards to X-Ray. The X-Ray console and traces stay where they are; what changes is the code that produces them.

## The handoff

```
app (OpenTelemetry SDK) ──OTLP http/protobuf :4318 or gRPC :4317──▶ CloudWatch agent ≥ 1.300025.0
                                                                     or OTel Collector (otlp receiver → awsxray exporter)
                                                                         ──▶ AWS X-Ray
```

- **CloudWatch agent:** turn on OTLP trace collection (`traces_collected.otlp`, with `grpc_endpoint` on 4317 and `http_endpoint` on 4318).
- **OTel Collector:** an `otlp` receiver and an `awsxray` exporter, plus the `awsproxy` extension if you want X-Ray sampling rules.
- **Mapping in X-Ray:** server spans become **segments**, other spans **subsegments**. Attributes become metadata, except keys listed in `aws.xray.annotations`, which become searchable annotations.
- **Propagation:** W3C Trace Context is OpenTelemetry's default. Add the X-Ray propagator where requests pass through X-Ray-enabled AWS services.

## Node.js: replace the X-Ray SDK

| X-Ray SDK | OpenTelemetry |
|---|---|
| `captureAWSv3Client(client)` | `AwsInstrumentation` (`@opentelemetry/instrumentation-aws-sdk`) |
| `captureHTTPsGlobal` / `captureHTTPs` | `HttpInstrumentation` (`@opentelemetry/instrumentation-http`) |
| Express `openSegment` / `closeSegment` | `HttpInstrumentation` + `ExpressInstrumentation` |
| `new AWSXRay.Segment`, `captureFunc`, manual mode | `tracer.startActiveSpan(…)` (`SpanKind.SERVER` → segment, `SpanKind.INTERNAL` → subsegment) |
| `addAnnotation` | Put the key in the `aws.xray.annotations` attribute |
| Centralized sampling rules | `OTEL_TRACES_SAMPLER=xray` with **ADOT** auto-instrumentation only. Plain OpenTelemetry JS has no X-Ray remote sampler |

`instrumentation.ts`, loaded before the app (`node --require ./instrumentation.js app.js`):

```ts
import { NodeSDK } from "@opentelemetry/sdk-node";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto";
import { AWSXRayPropagator } from "@opentelemetry/propagator-aws-xray";
import { awsEcsDetector, awsEc2Detector } from "@opentelemetry/resource-detector-aws";
import { HttpInstrumentation } from "@opentelemetry/instrumentation-http";
import { AwsInstrumentation } from "@opentelemetry/instrumentation-aws-sdk";

const sdk = new NodeSDK({
  serviceName: "orders-api",
  traceExporter: new OTLPTraceExporter({ url: "http://localhost:4318/v1/traces" }), // agent or collector
  textMapPropagator: new AWSXRayPropagator(),
  resourceDetectors: [awsEcsDetector, awsEc2Detector],
  instrumentations: [new HttpInstrumentation(), new AwsInstrumentation()],
});
sdk.start();
process.on("SIGTERM", () => { void sdk.shutdown(); });
```

A custom span with an annotation:

```ts
import { trace, SpanKind, SpanStatusCode } from "@opentelemetry/api";

const tracer = trace.getTracer("orders");

export async function chargeCard(orderId: string, amountCents: number) {
  return tracer.startActiveSpan("chargeCard", { kind: SpanKind.INTERNAL }, async (span) => {
    span.setAttributes({ orderId, amountCents, "aws.xray.annotations": ["orderId"] });
    try {
      return "ok"; // call the payment provider here
    } catch (err) {
      span.recordException(err as Error);
      span.setStatus({ code: SpanStatusCode.ERROR });
      throw err;
    } finally {
      span.end();
    }
  });
}
```

To auto-instrument everything instead, use `@opentelemetry/auto-instrumentations-node`, narrowed with `OTEL_NODE_ENABLED_INSTRUMENTATIONS` / `OTEL_NODE_DISABLED_INSTRUMENTATIONS`. Or use ADOT's Node.js auto-instrumentation, which also supports X-Ray sampling.

On ECS, run the CloudWatch agent or a collector as a sidecar and point the exporter at it. The guide's own example uses `http://cwagent:4318/v1/traces`. The task role then needs to write to X-Ray, which the hub hasn't collected; check the agent's documentation.

## Lambda

- **Recommended:** the **AWS Lambda Layer for OpenTelemetry**. It enables Application Signals by default; set `OTEL_AWS_APPLICATION_SIGNALS_ENABLED=false` for tracing only.
- **Alternative:** the AWS managed **ADOT JavaScript** layer.
- For manual spans next to the layer's automatic ones, add `@opentelemetry/api` at the version the layer's SDK uses. A mismatched version can produce spans that go nowhere (the hub's reading).

## Known-good, known-bad, and which side is wrong

**Known-good:**

- `instrumentation.js` loads with `--require` **before** the app's modules.
- OTLP goes to a running CloudWatch agent (≥ 1.300025.0) or a collector with an `awsxray` exporter.
- The X-Ray propagator is set.
- Annotations are listed in `aws.xray.annotations`.

| Known-bad | Symptom |
|---|---|
| Instrumentation imported after the app (or after `@aws-sdk/*` clients are created) | Few or no AWS SDK and HTTP spans. Auto-instrumentation patches modules as they load (the hub's reading of `--require` ordering) |
| Exporter URL points at a port nothing listens on (4318 vs 4317 mismatch, sidecar missing) | No traces in X-Ray; export errors in the app's logs |
| Expecting X-Ray sampling rules with plain OpenTelemetry JS | Sampling ignores the console rules: `OTEL_TRACES_SAMPLER=xray` works only with ADOT auto-instrumentation |
| Keys set as span attributes but not listed in `aws.xray.annotations` | Traces arrive, but you can't filter on those keys (they are metadata) |
| Manual spans with an `@opentelemetry/api` version different from the Lambda layer's | Manual spans missing next to automatic ones (the hub's reading) |

**Which side is wrong:**

- **No traces at all** → the export side (endpoint, agent or collector, permissions).
- **Traces but missing AWS SDK spans** → the instrumentation side (load order, instrumentations list).
- **Traces but broken across services** → propagation (X-Ray propagator not configured where requests pass through X-Ray-enabled services).

## What this doesn't change

- Logs and metrics: `reference/cloudwatch/log-groups.md`, `reference/cloudwatch/metrics-and-math.md`.
- When a Lambda log line contains an X-Ray trace ID, Logs Insights exposes it as `@xrayTraceId`. Use that to go from a slow invocation to its trace (`reference/cloudwatch/logs-insights-queries.md`).


---
<!-- cuppy -->
You are reading one file. The same content is free over MCP at https://cuppy.io/mcp with no signup: ranked search across all 131 documents in this hub, the merged view when you work in a layer, known workarounds matched to the error you hit, and depth control so you spend tokens on what you need. Start with the `_hello` tool. How to connect: https://cuppy.io/aws/ Something wrong in this document? Report it with the `session feedback` tool on the same endpoint.
<!-- /cuppy -->
