Skip to content

Commit 7e28e29

Browse files
committed
wip
1 parent 8e76feb commit 7e28e29

10 files changed

Lines changed: 242 additions & 8 deletions

File tree

.gitlab/scripts/publish_layers.sh

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,12 @@ if [[ ! ${STAGES[@]} =~ $STAGE ]]; then
8787
fi
8888

8989
layer="${LAYERS[$index]}"
90+
if [ -z "$LAYER_NAME_SUFFIX" ]; then
91+
echo "No layer name suffix"
92+
else
93+
layer="${layer}-${LAYER_NAME_SUFFIX}"
94+
fi
95+
echo "layer name: $layer"
9096

9197
if [[ "$STAGE" =~ ^(staging|sandbox)$ ]]; then
9298
# Deploy latest version

src/trace/context/extractor-utils.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import { XrayService } from "../xray-service";
1212
* Attempts to extract trace context from headers, falling back to Step Function context if needed
1313
* @param headers The headers object to extract from
1414
* @param tracerWrapper The tracer wrapper instance
15-
* @param eventType The type of event (for logging)
1615
* @returns SpanContextWrapper or null
1716
*/
1817
export function extractTraceContext(headers: any, tracerWrapper: TracerWrapper): SpanContextWrapper | null {

src/trace/context/extractor.spec.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -694,6 +694,54 @@ describe("TraceContextExtractor", () => {
694694
expect(traceContext?.sampleMode()).toBe("1");
695695
expect(traceContext?.source).toBe("event");
696696
});
697+
698+
it("extracts and applies deterministic trace ID from StepFunction event end-to-end", async () => {
699+
// This test verifies the complete flow from Step Function event to trace ID assignment
700+
const event = {
701+
Execution: {
702+
Id: "arn:aws:states:us-east-1:123456789012:execution:MyStateMachine:test-execution-id",
703+
Name: "test-execution-id",
704+
StartTime: "2024-01-01T00:00:00.000Z",
705+
},
706+
State: {
707+
Name: "ProcessData",
708+
EnteredTime: "2024-01-01T00:00:01.000Z",
709+
RetryCount: 0,
710+
},
711+
StateMachine: {
712+
Id: "arn:aws:states:us-east-1:123456789012:stateMachine:MyStateMachine",
713+
Name: "MyStateMachine",
714+
},
715+
};
716+
717+
const tracerWrapper = new TracerWrapper();
718+
const extractor = new TraceContextExtractor(tracerWrapper, {} as TraceConfig);
719+
720+
// Extract trace context through the full pipeline
721+
const traceContext = await extractor.extract(event, {} as Context);
722+
expect(traceContext).not.toBeNull();
723+
724+
// Verify the trace context was extracted from Step Function
725+
expect(traceContext?.source).toBe("event");
726+
727+
// Verify the trace ID is deterministic based on execution ID
728+
// The trace ID should be generated from SHA256 hash of the execution ID
729+
const traceId = traceContext?.toTraceId();
730+
expect(traceId).toBeDefined();
731+
expect(traceId).not.toBe("0"); // Should not be zero
732+
expect(traceId).toMatch(/^\d+$/); // Should be numeric string
733+
734+
// Verify the span ID is deterministic based on execution ID, state name, and entered time
735+
const spanId = traceContext?.toSpanId();
736+
expect(spanId).toBeDefined();
737+
expect(spanId).not.toBe("0"); // Should not be zero
738+
expect(spanId).toMatch(/^\d+$/); // Should be numeric string
739+
740+
// Verify that extracting the same event produces the same trace IDs (deterministic)
741+
const traceContext2 = await extractor.extract(event, {} as Context);
742+
expect(traceContext2?.toTraceId()).toBe(traceId);
743+
expect(traceContext2?.toSpanId()).toBe(spanId);
744+
});
697745
});
698746

699747
describe("lambda context", () => {

src/trace/context/extractor.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,6 @@ export class TraceContextExtractor {
4343
}
4444

4545
async extract(event: any, context: Context): Promise<SpanContextWrapper | null> {
46-
this.stepFunctionContextService = StepFunctionContextService.instance(event);
47-
4846
let spanContext: SpanContextWrapper | null = null;
4947
if (this.config.traceExtractor) {
5048
const customExtractor = new CustomTraceExtractor(this.config.traceExtractor);
@@ -57,7 +55,15 @@ export class TraceContextExtractor {
5755
spanContext = eventExtractor.extract(event);
5856
}
5957
}
60-
58+
/*
59+
if (spanContext === null) {
60+
this.stepFunctionContextService = StepFunctionContextService.instance(event)
61+
if (this.stepFunctionContextService?.context) {
62+
const extractor = new StepFunctionEventTraceExtractor();
63+
spanContext = extractor?.extract(event);
64+
}
65+
}
66+
*/
6167
if (spanContext === null) {
6268
const contextExtractor = new LambdaContextTraceExtractor(this.tracerWrapper);
6369
spanContext = contextExtractor.extract(context);
@@ -88,6 +94,7 @@ export class TraceContextExtractor {
8894
if (EventValidator.isKinesisStreamEvent(event)) return new KinesisEventTraceExtractor(this.tracerWrapper);
8995
if (EventValidator.isEventBridgeEvent(event)) return new EventBridgeEventTraceExtractor(this.tracerWrapper);
9096

97+
this.stepFunctionContextService = StepFunctionContextService.instance(event);
9198
if (this.stepFunctionContextService?.context) return new StepFunctionEventTraceExtractor();
9299

93100
return;

src/trace/context/extractors/sns-sqs.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export class SNSSQSEventTraceExtractor implements EventTraceExtractor {
99
constructor(private tracerWrapper: TracerWrapper) {}
1010

1111
extract(event: SQSEvent): SpanContextWrapper | null {
12+
logDebug("SNS-SQS Extractor Being Used");
1213
try {
1314
// Try to extract trace context from SNS wrapped in SQS
1415
const body = event?.Records?.[0]?.body;

src/trace/context/extractors/sqs.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export class SQSEventTraceExtractor implements EventTraceExtractor {
99
constructor(private tracerWrapper: TracerWrapper) {}
1010

1111
extract(event: SQSEvent): SpanContextWrapper | null {
12+
logDebug("SQS Extractor Being Used");
1213
try {
1314
// First try to extract trace context from message attributes
1415
let headers = event?.Records?.[0]?.messageAttributes?._datadog?.stringValue;
@@ -25,6 +26,7 @@ export class SQSEventTraceExtractor implements EventTraceExtractor {
2526

2627
if (headers) {
2728
const parsedHeaders = JSON.parse(headers);
29+
2830
const traceContext = extractTraceContext(parsedHeaders, this.tracerWrapper);
2931
if (traceContext) {
3032
return traceContext;

src/trace/context/extractors/step-function.spec.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { StepFunctionContextService } from "../../step-function-service";
22
import { StepFunctionEventTraceExtractor } from "./step-function";
3+
import { TracerWrapper } from "../../tracer-wrapper";
34

45
describe("StepFunctionEventTraceExtractor", () => {
56
beforeEach(() => {
@@ -112,5 +113,77 @@ describe("StepFunctionEventTraceExtractor", () => {
112113
const traceContext = extractor.extract({});
113114
expect(traceContext).toBeNull();
114115
});
116+
117+
it("extracts trace context end-to-end with deterministic IDs for v1 nested context", () => {
118+
const nestedPayload = {
119+
_datadog: {
120+
Execution: {
121+
Id: "arn:aws:states:us-east-1:123456789012:execution:parent-machine:parent-execution-id",
122+
Name: "parent-execution-id",
123+
StartTime: "2024-01-01T00:00:00.000Z",
124+
},
125+
State: {
126+
Name: "InvokeNestedStateMachine",
127+
EnteredTime: "2024-01-01T00:00:05.000Z",
128+
RetryCount: 0,
129+
},
130+
"serverless-version": "v1",
131+
RootExecutionId: "arn:aws:states:us-east-1:123456789012:execution:root-machine:root-execution-id",
132+
},
133+
};
134+
135+
const extractor = new StepFunctionEventTraceExtractor();
136+
const traceContext = extractor.extract(nestedPayload);
137+
138+
expect(traceContext).not.toBeNull();
139+
expect(traceContext?.source).toBe("event");
140+
141+
// Verify trace ID is based on root execution ID (deterministic)
142+
const traceId = traceContext?.toTraceId();
143+
expect(traceId).toBeDefined();
144+
expect(traceId).not.toBe("0");
145+
146+
// Extract again to verify deterministic behavior
147+
const traceContext2 = extractor.extract(nestedPayload);
148+
expect(traceContext2?.toTraceId()).toBe(traceId);
149+
});
150+
151+
it("extracts trace context end-to-end with propagated IDs for v1 lambda root context", () => {
152+
const lambdaRootPayload = {
153+
_datadog: {
154+
Execution: {
155+
Id: "arn:aws:states:us-east-1:123456789012:execution:machine:execution-id",
156+
Name: "execution-id",
157+
StartTime: "2024-01-01T00:00:00.000Z",
158+
},
159+
State: {
160+
Name: "ProcessLambda",
161+
EnteredTime: "2024-01-01T00:00:02.000Z",
162+
RetryCount: 0,
163+
},
164+
"serverless-version": "v1",
165+
"x-datadog-trace-id": "1234567890123456789",
166+
"x-datadog-tags": "_dd.p.tid=fedcba9876543210,_dd.p.dm=-0",
167+
},
168+
};
169+
170+
const extractor = new StepFunctionEventTraceExtractor();
171+
const traceContext = extractor.extract(lambdaRootPayload);
172+
173+
expect(traceContext).not.toBeNull();
174+
expect(traceContext?.source).toBe("event");
175+
176+
// Verify trace ID comes from propagated headers, not deterministic hash
177+
expect(traceContext?.toTraceId()).toBe("1234567890123456789");
178+
179+
// Verify deterministic span ID based on execution details
180+
const spanId = traceContext?.toSpanId();
181+
expect(spanId).toBeDefined();
182+
expect(spanId).not.toBe("0");
183+
184+
// Extract again to verify deterministic span ID
185+
const traceContext2 = extractor.extract(lambdaRootPayload);
186+
expect(traceContext2?.toSpanId()).toBe(spanId);
187+
});
115188
});
116189
});

src/trace/context/extractors/step-function.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,16 @@ import { EventTraceExtractor } from "../extractor";
55
export class StepFunctionEventTraceExtractor implements EventTraceExtractor {
66
extract(event: any): SpanContextWrapper | null {
77
// Probably StepFunctionContextService hasn't been called
8-
const instance = StepFunctionContextService.instance(event);
9-
const context = instance.context;
8+
const stepFunctionInstance = StepFunctionContextService.instance(event);
9+
const stepFunctionContext = stepFunctionInstance.context;
1010

11-
if (context === undefined) return null;
11+
if (stepFunctionContext !== undefined) {
12+
const spanContext = stepFunctionInstance.spanContext;
13+
if (spanContext !== null) {
14+
return spanContext;
15+
}
16+
}
1217

13-
return instance.spanContext;
18+
return null;
1419
}
1520
}

src/trace/listener.spec.ts

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,15 @@ jest.mock("./tracer-wrapper", () => {
3333
return mockExtract(event);
3434
}
3535

36+
startSpan(name: any, options: any): any {
37+
return {
38+
toSpanId: () => "mockSpanId",
39+
toTraceId: () => "mockTraceId",
40+
finish: jest.fn(),
41+
setTag: jest.fn(),
42+
};
43+
}
44+
3645
injectSpan(span: any): any {
3746
return {
3847
[DATADOG_PARENT_ID_HEADER]: span.toSpanId(),
@@ -304,6 +313,88 @@ describe("TraceListener", () => {
304313
);
305314
});
306315

316+
it("wraps dd-trace span around invocation with Step Function context", async () => {
317+
const listener = new TraceListener(defaultConfig);
318+
mockTraceSource = TraceSource.Event;
319+
320+
// Mock Step Function context with deterministic trace IDs
321+
mockSpanContext = {
322+
toTraceId: () => "512d06a10e5e34cb", // Hex converted to decimal would be different
323+
toSpanId: () => "7069a031ef9ad2cc",
324+
_sampling: {
325+
priority: "1",
326+
},
327+
};
328+
mockSpanContextWrapper = {
329+
spanContext: mockSpanContext,
330+
};
331+
332+
const stepFunctionSQSEvent = {
333+
Records: [
334+
{
335+
messageId: "4ead33f3-51c8-4094-87bd-5325dc143cbd",
336+
receiptHandle:
337+
"AQEBrGtLZCUS1POUEZtdZRoB0zXgT14OQC48A4Xk4Qbnv/v4d0ib5rFI1wEah823t2hE9haPm6nNN1aGsJmYkqa9Y8qaBQscp9f7HKJyybT5hpdKEn07fY0VRv/Of63u1RN1YdFdY5uhI8XGWRc4w7t62lQwMMFY5Ahy7XLVwnav81KRjGFdgxzITrtx3YKxmISNvXzPiiHNKb7jT+ClfXi91bEYHi3Od3ji5xGajAofgYrj2VBDULyohsfMkwlvAanD2wfj2x++wL5LSpFEtMFnvThzt7Dh5FEZChVMzWV+fRFpljivHX58ZeuGv4yIIjLVuuDGn5uAY5ES4CsdINrBAru6K5gDSPUajRzE3TktNgAq5Niqfky1x0srLRAJjTDdmZK8/CXU0sRT/MCT99vkCHa0bC17S/9au5bCbrB4k/T9J8W39AA6kIYhebkq3IQr",
338+
body: '{"testData":"Hello from Step Functions to SQS"}',
339+
attributes: {
340+
ApproximateReceiveCount: "1",
341+
SentTimestamp: "1752594520503",
342+
SenderId: "AROAWGCM4HXU73A4V34AJ:EcGTcmgJbwwOwXPbloVwgSaDOmwhYBLH",
343+
ApproximateFirstReceiveTimestamp: "1752594520516",
344+
},
345+
messageAttributes: {
346+
_datadog: {
347+
stringValue:
348+
'{"Execution":{"Id":"arn:aws:states:sa-east-1:123456123456:execution:rstrat-sfn-sqs-demo-dev-state-machine:a4912895-93a3-4803-a712-69fecb55c025","StartTime":"2025-07-15T15:48:40.302Z","Name":"a4912895-93a3-4803-a712-69fecb55c025","RoleArn":"arn:aws:iam::123456123456:role/rstrat-sfn-sqs-demo-dev-StepFunctionsExecutionRole-s6ozc2dVrvLH","RedriveCount":0},"StateMachine":{"Id":"arn:aws:states:sa-east-1:123456123456:stateMachine:rstrat-sfn-sqs-demo-dev-state-machine","Name":"rstrat-sfn-sqs-demo-dev-state-machine"},"State":{"Name":"SendToSQS","EnteredTime":"2025-07-15T15:48:40.333Z","RetryCount":0},"RootExecutionId":"arn:aws:states:sa-east-1:123456123456:execution:rstrat-sfn-sqs-demo-dev-state-machine:a4912895-93a3-4803-a712-69fecb55c025","serverless-version":"v1"}',
349+
stringListValues: [],
350+
binaryListValues: [],
351+
dataType: "String",
352+
},
353+
},
354+
md5OfMessageAttributes: "5469b8f90bb6ab27e95816c1fa178680",
355+
md5OfBody: "f0c0ddb2ed09a09e8791013f142e8d7e",
356+
eventSource: "aws:sqs",
357+
eventSourceARN: "arn:aws:sqs:sa-east-1:123456123456:rstrat-sfn-sqs-demo-dev-process-event-queue",
358+
awsRegion: "sa-east-1",
359+
},
360+
],
361+
};
362+
363+
await listener.onStartInvocation(stepFunctionSQSEvent, context as any);
364+
const unwrappedFunc = () => {};
365+
const wrappedFunc = listener.onWrap(unwrappedFunc);
366+
wrappedFunc();
367+
await listener.onCompleteInvocation();
368+
369+
expect(mockWrap).toHaveBeenCalledWith(
370+
"aws.lambda",
371+
{
372+
resource: "my-Lambda",
373+
service: "my-Lambda",
374+
tags: {
375+
cold_start: true,
376+
function_arn: "arn:aws:lambda:us-east-1:123456789101:function:my-lambda",
377+
function_version: "$LATEST",
378+
request_id: "1234",
379+
resource_names: "my-Lambda",
380+
functionname: "my-lambda",
381+
"_dd.parent_source": "event",
382+
"function_trigger.event_source": "sqs",
383+
"function_trigger.event_source_arn":
384+
"arn:aws:sqs:sa-east-1:123456123456:rstrat-sfn-sqs-demo-dev-process-event-queue",
385+
datadog_lambda: datadogLambdaVersion,
386+
dd_trace: ddtraceVersion,
387+
},
388+
type: "serverless",
389+
childOf: expect.objectContaining({
390+
toSpanId: expect.any(Function),
391+
toTraceId: expect.any(Function),
392+
}),
393+
},
394+
unwrappedFunc,
395+
);
396+
});
397+
307398
it("injects authorizer context if it exists", async () => {
308399
const listener = new TraceListener(defaultConfig);
309400
mockTraceSource = TraceSource.Event;

src/trace/tracer-wrapper.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,9 @@ export class TracerWrapper {
7575
}
7676

7777
public startSpan<T = (...args: any[]) => any>(name: string, options: TraceOptions): T | null {
78+
logDebug("Starting Span named:", { name });
7879
if (!this.isTracerAvailable) {
80+
logDebug("No Tracer available, cannot start span");
7981
return null;
8082
}
8183
return this.tracer.startSpan(name, options);

0 commit comments

Comments
 (0)