Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
4e1078c
initial working version
joeyzhao2018 Apr 29, 2026
965710e
some updates and fixes
joeyzhao2018 Apr 30, 2026
ad4a2c4
remove debugging ENV
joeyzhao2018 May 1, 2026
e149fa6
update comments
joeyzhao2018 May 1, 2026
4f0e77f
refactor away the AI's complex extractor
joeyzhao2018 May 1, 2026
0d5ab4a
clean up AI complicated stuff...
joeyzhao2018 May 1, 2026
c625b24
rearrange and enhancements
joeyzhao2018 May 1, 2026
4401d6a
build: support dd-trace package override and filter publish layer check
joeyzhao2018 May 8, 2026
1652613
revert some unrelated changes
joeyzhao2018 May 15, 2026
dfa5e4b
revert irrelavant changes
joeyzhao2018 May 15, 2026
8a19b64
lint
joeyzhao2018 May 15, 2026
e54a187
revert dockerfile
joeyzhao2018 May 15, 2026
3cef4f5
revert dockerfile
joeyzhao2018 May 15, 2026
08bf22e
refactor(durable-execution): drop root span, force datadog-only check…
joeyzhao2018 May 15, 2026
1f4e7ea
refactor(durable-execution): drop upstream-headers extraction path
joeyzhao2018 May 16, 2026
e6a7434
simplify AI code
joeyzhao2018 May 20, 2026
6bdf796
remove isDurableExecutionReplay
joeyzhao2018 May 20, 2026
bf4a282
further cleanup
joeyzhao2018 May 20, 2026
03c9bee
simplify the extractor part
joeyzhao2018 May 20, 2026
735cff1
further simplify
joeyzhao2018 May 20, 2026
7628513
stype consistency
joeyzhao2018 May 20, 2026
20b2339
lint
joeyzhao2018 May 20, 2026
a08d146
lint
joeyzhao2018 May 20, 2026
c730d1f
Merge branch 'main' into joey/cross-invocation-tracecontext-propagation
joeyzhao2018 Jun 16, 2026
52a971c
Merge branch 'main' into joey/cross-invocation-tracecontext-propagation
joeyzhao2018 Jun 17, 2026
40d0c41
add DD_DURABLE_CROSS_INVOCATION_TRACING_ENABLED env explanation in RE…
joeyzhao2018 Jun 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ Besides the environment variables supported by dd-trace-js, the datadog-lambda-j
| DD_COLD_START_TRACE_SKIP_LIB | optionally skip creating Cold Start Spans for a comma-separated list of libraries. Useful to limit depth or skip known libraries. | `./opentracing/tracer` |
| DD_CAPTURE_LAMBDA_PAYLOAD | [Captures incoming and outgoing AWS Lambda payloads][1] in the Datadog APM spans for Lambda invocations. | `false` |
| DD_CAPTURE_LAMBDA_PAYLOAD_MAX_DEPTH | Determines the level of detail captured from AWS Lambda payloads, which are then assigned as tags for the `aws.lambda` span. It specifies the nesting depth of the JSON payload structure to process. Once the specified maximum depth is reached, the tag's value is set to the stringified value of any nested elements beyond this level. <br> For example, given the input payload: <pre>{<br> "lv1" : {<br> "lv2": {<br> "lv3": "val"<br> }<br> }<br>}</pre> If the depth is set to `2`, the resulting tag's key is set to `function.request.lv1.lv2` and the value is `{\"lv3\": \"val\"}`. <br> If the depth is set to `0`, the resulting tag's key is set to `function.request` and value is `{\"lv1\":{\"lv2\":{\"lv3\": \"val\"}}}` | `10` |
| DD_DURABLE_CROSS_INVOCATION_TRACING_ENABLED | For AWS Durable functions, the tracer creates extra checkpoints named `_datadog_{N}` to propagate trace context across function invocations, keeping spans from multiple invocations in one intact trace for each durable execution. | `true` |


## Lambda Profiling Beta
Expand Down
4 changes: 4 additions & 0 deletions src/trace/context/extractor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { XrayService } from "../xray-service";
import {
AppSyncEventTraceExtractor,
CustomTraceExtractor,
DurableExecutionEventTraceExtractor,
EventBridgeEventTraceExtractor,
EventBridgeSQSEventTraceExtractor,
HTTPEventTraceExtractor,
Expand Down Expand Up @@ -81,6 +82,9 @@ export class TraceContextExtractor {
private getTraceEventExtractor(event: any): EventTraceExtractor | undefined {
if (!event || typeof event !== "object") return;

if (EventValidator.isDurableExecutionEvent(event))
return new DurableExecutionEventTraceExtractor(this.tracerWrapper);

const headers = event.headers ?? event.multiValueHeaders;
if (headers !== null && typeof headers === "object") {
return new HTTPEventTraceExtractor(this.tracerWrapper, this.config.decodeAuthorizerContext);
Expand Down
65 changes: 65 additions & 0 deletions src/trace/context/extractors/durable-execution.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { DurableExecutionEventTraceExtractor } from "./durable-execution";
import { TracerWrapper } from "../../tracer-wrapper";

function makeTracerWrapper(extractReturn: any = null): TracerWrapper {
return {
extract: jest.fn().mockReturnValue(extractReturn),
} as unknown as TracerWrapper;
}

describe("DurableExecutionEventTraceExtractor", () => {
beforeEach(() => {
jest.clearAllMocks();
});

it("extracts checkpoint headers via the standard propagator", () => {
const executionArn = "arn:aws:lambda:us-east-2:123456789012:function:demo:$LATEST/durable-execution/demo/abc";

const checkpointHeaders = {
"x-datadog-trace-id": "149750110124521191",
"x-datadog-parent-id": "987654321012345678",
"x-datadog-sampling-priority": "1",
};

const event = {
DurableExecutionArn: executionArn,
CheckpointToken: "t-1",
InitialExecutionState: {
Operations: [
{
Id: "op-1",
Name: "_datadog_0",
Status: "SUCCEEDED",
StepDetails: {
Result: JSON.stringify(checkpointHeaders),
},
},
],
},
};

const sentinelContext = { sentinel: true };
const tracerWrapper = makeTracerWrapper(sentinelContext);
const extractor = new DurableExecutionEventTraceExtractor(tracerWrapper);
const context = extractor.extract(event);

// Checkpoint headers are Datadog-style; the default extract list includes
// `datadog`, so the standard extract path picks them up.
expect(tracerWrapper.extract).toHaveBeenCalledWith(checkpointHeaders);
expect(context).toBe(sentinelContext);
});

it("returns null when no checkpoint exists", () => {
const tracerWrapper = makeTracerWrapper();
const extractor = new DurableExecutionEventTraceExtractor(tracerWrapper);

const context = extractor.extract({
DurableExecutionArn: "arn:aws:lambda:us-east-2:123:function:demo",
CheckpointToken: "t-empty",
InitialExecutionState: { Operations: [] },
});

expect(context).toBeNull();
expect(tracerWrapper.extract).not.toHaveBeenCalled();
});
});
105 changes: 105 additions & 0 deletions src/trace/context/extractors/durable-execution.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/**
* Durable Execution Trace Extractor — Checkpoint Approach
*
* Strategy:
* 1. Look for trace context in the latest `_datadog_{N}` checkpoint.
* 2. If no trace checkpoint exists, return null and let the default extraction
* path create the context.
*
* The extracted context becomes the parent of the `aws.lambda` span (and any
* downstream spans created by dd-trace-js, including `aws.durable.execute`).
* Therefore all `aws.lambda` spans will be anchored to the first
* `aws.durable.execute` span for a durable execution.
*
* Checkpoint data will be written by the dd-trace-js plugin in Datadog style
* (`x-datadog-*`). Extraction goes through the standard `TracerWrapper.extract`
* path, which honors `DD_TRACE_PROPAGATION_STYLE_EXTRACT`. The default extract
* list (`datadog, tracecontext, baggage`) already includes `datadog`. Customers
* who override the extract list MUST keep `datadog` in it.
*/

import { logDebug } from "../../../utils";
import { SpanContextWrapper } from "../../span-context-wrapper";
import { TracerWrapper } from "../../tracer-wrapper";
import { EventTraceExtractor } from "../extractor";

const TRACE_CHECKPOINT_NAME_PREFIX = "_datadog_";

interface CheckpointOperation {
Name?: string;
Payload?: string;
StepDetails?: { Result?: string };
}

interface DurableExecutionEventShape {
DurableExecutionArn?: string;
InitialExecutionState?: { Operations?: CheckpointOperation[] };
}

function parseTraceCheckpointNumber(name: unknown): number | null {
if (typeof name !== "string") return null;

if (!name.startsWith(TRACE_CHECKPOINT_NAME_PREFIX)) return null;
const suffix = name.slice(TRACE_CHECKPOINT_NAME_PREFIX.length);
const n = Number.parseInt(suffix, 10);
if (Number.isNaN(n) || String(n) !== suffix) return null;
return n;
}

/**
* Find the highest-numbered `_datadog_{N}` checkpoint in the event and return
* its parsed header dict.
*
* Each invocation that changes trace context saves a new checkpoint with N+1;
* the one with the highest N is the most recent. Headers are written by the
* dd-trace-js plugin via `tracer.inject(span, 'http_headers', headers)` so the
* payload is a standard HTTP-style header dict.
*
*/
function findLatestCheckpointHeaders(event: DurableExecutionEventShape): Record<string, string> | null {
const operations = event.InitialExecutionState?.Operations;
if (!operations || operations.length === 0) return null;

let best: { number: number; op: CheckpointOperation } | null = null;
for (const op of operations) {
const n = parseTraceCheckpointNumber(op?.Name);
if (n === null) continue;
if (best === null || n > best.number) {
best = { number: n, op };
}
}
if (best === null) return null;

const raw = best.op.Payload ?? best.op.StepDetails?.Result;
if (!raw || typeof raw !== "string") return null;
Comment thread
joeyzhao2018 marked this conversation as resolved.
try {
const parsed = JSON.parse(raw);
if (parsed && typeof parsed === "object") {
return parsed as Record<string, string>;
}
} catch (e) {
logDebug(`Failed to parse trace checkpoint payload: ${e}`);
}
return null;
}

export class DurableExecutionEventTraceExtractor implements EventTraceExtractor {
constructor(private tracerWrapper: TracerWrapper) {}

extract(event: unknown): SpanContextWrapper | null {
const e = event as DurableExecutionEventShape | undefined;
if (!e?.DurableExecutionArn) {
logDebug("No DurableExecutionArn in event");
return null;
}

const checkpointHeaders = findLatestCheckpointHeaders(e);
if (checkpointHeaders) {
logDebug("Extracting trace context from durable checkpoint");
return this.tracerWrapper.extract(checkpointHeaders);
}

logDebug("No durable trace context found; deferring to default extraction");
return null;
}
}
1 change: 1 addition & 0 deletions src/trace/context/extractors/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ export { SNSSQSEventTraceExtractor } from "./sns-sqs";
export { StepFunctionEventTraceExtractor } from "./step-function";
export { LambdaContextTraceExtractor } from "./lambda-context";
export { CustomTraceExtractor } from "./custom";
export { DurableExecutionEventTraceExtractor } from "./durable-execution";
4 changes: 4 additions & 0 deletions src/utils/event-validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,8 @@ export class EventValidator {
static isKinesisStreamEvent(event: any): event is KinesisStreamEvent {
return Array.isArray(event.Records) && event.Records.length > 0 && event.Records[0].kinesis !== undefined;
}

static isDurableExecutionEvent(event: any): boolean {
return typeof event.DurableExecutionArn === "string";
}
}
Loading