Skip to content

Commit c9b398b

Browse files
lym953claude
andauthored
[SVLS-8583] Add execution_status tag to aws.lambda span for durable functions (#764)
* feat: [SVLS-8583] add execution_status tag to aws.lambda span for durable functions Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: fix formatting Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: [SVLS-8583] correct valid execution statuses to match InvocationStatus enum Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: [SVLS-8583] address code review comments - Use typeof string guard in extractDurableExecutionStatus for consistency - Move execution_status tagging outside durableFunctionContext block so it applies even when ARN parsing fails - Add listener.spec.ts tests for execution_status tag being set and not set Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 1f8a5e7 commit c9b398b

File tree

4 files changed

+110
-2
lines changed

4 files changed

+110
-2
lines changed

src/trace/durable-function-context.spec.ts

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import { parseDurableExecutionArn, extractDurableFunctionContext } from "./durable-function-context";
1+
import {
2+
parseDurableExecutionArn,
3+
extractDurableFunctionContext,
4+
extractDurableExecutionStatus,
5+
} from "./durable-function-context";
26

37
describe("durable-function-context", () => {
48
describe("parseDurableExecutionArn", () => {
@@ -132,4 +136,41 @@ describe("durable-function-context", () => {
132136
expect(result).toBeUndefined();
133137
});
134138
});
139+
140+
describe("extractDurableExecutionStatus", () => {
141+
const durableEvent = {
142+
DurableExecutionArn:
143+
"arn:aws:lambda:us-east-1:123456789012:function:my-func:1/durable-execution/my-execution/550e8400-e29b-41d4-a716-446655440004",
144+
};
145+
146+
it.each(["SUCCEEDED", "FAILED", "PENDING"])("returns %s when result.Status is %s", (status) => {
147+
const result = extractDurableExecutionStatus(durableEvent, { Status: status });
148+
expect(result).toBe(status);
149+
});
150+
151+
it("returns undefined when result.Status is not a valid status", () => {
152+
const result = extractDurableExecutionStatus(durableEvent, { Status: "UNKNOWN" });
153+
expect(result).toBeUndefined();
154+
});
155+
156+
it("returns undefined when result has no Status field", () => {
157+
const result = extractDurableExecutionStatus(durableEvent, {});
158+
expect(result).toBeUndefined();
159+
});
160+
161+
it("returns undefined when result is null", () => {
162+
const result = extractDurableExecutionStatus(durableEvent, null);
163+
expect(result).toBeUndefined();
164+
});
165+
166+
it("returns undefined when event has no DurableExecutionArn", () => {
167+
const result = extractDurableExecutionStatus({ body: "{}" }, { Status: "SUCCEEDED" });
168+
expect(result).toBeUndefined();
169+
});
170+
171+
it("returns undefined when event is null", () => {
172+
const result = extractDurableExecutionStatus(null, { Status: "SUCCEEDED" });
173+
expect(result).toBeUndefined();
174+
});
175+
});
135176
});

src/trace/durable-function-context.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ export interface DurableFunctionContext {
66
"aws_lambda.durable_function.first_invocation"?: string;
77
}
88

9+
const VALID_DURABLE_EXECUTION_STATUSES = new Set(["SUCCEEDED", "FAILED", "PENDING"]);
10+
911
export function extractDurableFunctionContext(event: any): DurableFunctionContext | undefined {
1012
const durableExecutionArn = event?.DurableExecutionArn;
1113

@@ -33,6 +35,23 @@ export function extractDurableFunctionContext(event: any): DurableFunctionContex
3335
return context;
3436
}
3537

38+
/**
39+
* Extracts the durable function execution status from the handler result.
40+
* Only applies when the event contains a DurableExecutionArn.
41+
*/
42+
export function extractDurableExecutionStatus(event: any, result: any): string | undefined {
43+
if (typeof event?.DurableExecutionArn !== "string") {
44+
return undefined;
45+
}
46+
47+
const status = result?.Status;
48+
if (typeof status !== "string" || !VALID_DURABLE_EXECUTION_STATUSES.has(status)) {
49+
return undefined;
50+
}
51+
52+
return status;
53+
}
54+
3655
/**
3756
* Parses a DurableExecutionArn to extract execution name and ID.
3857
* ARN format: arn:aws:lambda:{region}:{account}:function:{func}:{version}/durable-execution/{name}/{id}

src/trace/listener.spec.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -568,4 +568,44 @@ describe("TraceListener", () => {
568568
currentSpanSpy.mockRestore();
569569
}
570570
});
571+
572+
it("sets execution_status tag on the aws.lambda span when result.Status is valid", async () => {
573+
const mockSetTag = jest.fn();
574+
const mockSpan = { setTag: mockSetTag };
575+
const currentSpanSpy = jest.spyOn(TracerWrapper.prototype, "currentSpan", "get").mockReturnValue(mockSpan);
576+
577+
try {
578+
const listener = new TraceListener(defaultConfig);
579+
const durableEvent = {
580+
DurableExecutionArn:
581+
"arn:aws:lambda:us-east-1:123456789012:function:my-func:1/durable-execution/my-execution/550e8400-e29b-41d4-a716-446655440004",
582+
};
583+
await listener.onStartInvocation(durableEvent, context as any);
584+
listener.onEndingInvocation(durableEvent, { Status: "SUCCEEDED" }, false);
585+
586+
expect(mockSetTag).toHaveBeenCalledWith("aws_lambda.durable_function.execution_status", "SUCCEEDED");
587+
} finally {
588+
currentSpanSpy.mockRestore();
589+
}
590+
});
591+
592+
it("does not set execution_status tag when result.Status is invalid", async () => {
593+
const mockSetTag = jest.fn();
594+
const mockSpan = { setTag: mockSetTag };
595+
const currentSpanSpy = jest.spyOn(TracerWrapper.prototype, "currentSpan", "get").mockReturnValue(mockSpan);
596+
597+
try {
598+
const listener = new TraceListener(defaultConfig);
599+
const durableEvent = {
600+
DurableExecutionArn:
601+
"arn:aws:lambda:us-east-1:123456789012:function:my-func:1/durable-execution/my-execution/550e8400-e29b-41d4-a716-446655440004",
602+
};
603+
await listener.onStartInvocation(durableEvent, context as any);
604+
listener.onEndingInvocation(durableEvent, { Status: "UNKNOWN" }, false);
605+
606+
expect(mockSetTag).not.toHaveBeenCalledWith("aws_lambda.durable_function.execution_status", expect.anything());
607+
} finally {
608+
currentSpanSpy.mockRestore();
609+
}
610+
});
571611
});

src/trace/listener.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,11 @@ import { SpanWrapper } from "./span-wrapper";
2020
import { getTraceTree, clearTraceTree } from "../runtime/index";
2121
import { TraceContext, TraceContextService, TraceSource } from "./trace-context-service";
2222
import { StepFunctionContext, StepFunctionContextService } from "./step-function-service";
23-
import { DurableFunctionContext, extractDurableFunctionContext } from "./durable-function-context";
23+
import {
24+
DurableFunctionContext,
25+
extractDurableFunctionContext,
26+
extractDurableExecutionStatus,
27+
} from "./durable-function-context";
2428
import { XrayService } from "./xray-service";
2529
import { AUTHORIZING_REQUEST_ID_HEADER } from "./context/extractors/http";
2630
import { getSpanPointerAttributes, SpanPointerAttributes } from "../utils/span-pointers";
@@ -234,6 +238,10 @@ export class TraceListener {
234238
}
235239
}
236240
}
241+
const executionStatus = extractDurableExecutionStatus(event, result);
242+
if (executionStatus !== undefined) {
243+
this.tracerWrapper.currentSpan.setTag("aws_lambda.durable_function.execution_status", executionStatus);
244+
}
237245

238246
let rootSpan = this.inferredSpan;
239247
if (!rootSpan) {

0 commit comments

Comments
 (0)