From ae4505f40b90b4636484229ff718321cbf66e1d1 Mon Sep 17 00:00:00 2001 From: John Chrostek Date: Wed, 18 Mar 2026 14:45:21 -0400 Subject: [PATCH 1/7] feat: add durable_function_execution_status tag to aws.lambda span MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When using AWS Lambda Durable Execution SDK, extract the execution status from the handler response and add it as a tag to the aws.lambda span. The tag captures the Status field from durable function responses: - SUCCEEDED: Execution completed successfully - FAILED: Execution failed - PENDING: Execution is still in progress The tag is only added when: 1. The event contains a DurableExecutionArn (indicates durable invocation) 2. The response is a dict with a valid Status field 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- datadog_lambda/durable.py | 27 +++++++++++++++++ datadog_lambda/wrapper.py | 12 +++++++- tests/test_durable.py | 63 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 101 insertions(+), 1 deletion(-) diff --git a/datadog_lambda/durable.py b/datadog_lambda/durable.py index e9443f92..d5e89682 100644 --- a/datadog_lambda/durable.py +++ b/datadog_lambda/durable.py @@ -47,3 +47,30 @@ def extract_durable_function_tags(event): "durable_function_execution_name": execution_name, "durable_function_execution_id": execution_id, } + + +VALID_DURABLE_STATUSES = {"SUCCEEDED", "FAILED", "PENDING"} + + +def extract_durable_function_status_tag(response, event): + """ + Extract durable function execution status from handler response. + + Returns a dict with the status tag if: + - The event indicates a durable function invocation (has DurableExecutionArn) + - The response is a dict with a valid Status field + + Returns empty dict otherwise. + """ + # Only add status tag for durable function invocations + if not isinstance(event, dict) or "DurableExecutionArn" not in event: + return {} + + if not isinstance(response, dict): + return {} + + status = response.get("Status") + if status not in VALID_DURABLE_STATUSES: + return {} + + return {"durable_function_execution_status": status} diff --git a/datadog_lambda/wrapper.py b/datadog_lambda/wrapper.py index d022988b..88cf0d86 100644 --- a/datadog_lambda/wrapper.py +++ b/datadog_lambda/wrapper.py @@ -42,7 +42,10 @@ tracer, propagator, ) -from datadog_lambda.durable import extract_durable_function_tags +from datadog_lambda.durable import ( + extract_durable_function_tags, + extract_durable_function_status_tag, +) from datadog_lambda.trigger import ( extract_trigger_tags, extract_http_status_code_tag, @@ -340,6 +343,13 @@ def _after(self, event, context): if status_code: self.span.set_tag("http.status_code", status_code) + # Extract and set durable function execution status + durable_status_tags = extract_durable_function_status_tag( + self.response, event + ) + for tag_name, tag_value in durable_status_tags.items(): + self.span.set_tag(tag_name, tag_value) + self.span.finish() if status_code: diff --git a/tests/test_durable.py b/tests/test_durable.py index 60914934..549bc45d 100644 --- a/tests/test_durable.py +++ b/tests/test_durable.py @@ -7,6 +7,7 @@ from datadog_lambda.durable import ( _parse_durable_execution_arn, extract_durable_function_tags, + extract_durable_function_status_tag, ) @@ -89,3 +90,65 @@ def test_returns_empty_dict_when_durable_execution_arn_cannot_be_parsed(self): def test_returns_empty_dict_when_event_is_empty(self): result = extract_durable_function_tags({}) self.assertEqual(result, {}) + + +class TestExtractDurableFunctionStatusTag(unittest.TestCase): + def test_succeeded_status(self): + event = {"DurableExecutionArn": "arn:aws:states:us-east-1:123456789012:..."} + response = {"Status": "SUCCEEDED", "Result": "some-result"} + result = extract_durable_function_status_tag(response, event) + self.assertEqual(result, {"durable_function_execution_status": "SUCCEEDED"}) + + def test_failed_status(self): + event = {"DurableExecutionArn": "arn:aws:states:us-east-1:123456789012:..."} + response = {"Status": "FAILED", "Error": "some-error"} + result = extract_durable_function_status_tag(response, event) + self.assertEqual(result, {"durable_function_execution_status": "FAILED"}) + + def test_pending_status(self): + event = {"DurableExecutionArn": "arn:aws:states:us-east-1:123456789012:..."} + response = {"Status": "PENDING"} + result = extract_durable_function_status_tag(response, event) + self.assertEqual(result, {"durable_function_execution_status": "PENDING"}) + + def test_non_durable_event_returns_empty(self): + event = {"key": "value"} # No DurableExecutionArn + response = {"Status": "SUCCEEDED"} + result = extract_durable_function_status_tag(response, event) + self.assertEqual(result, {}) + + def test_non_dict_response_returns_empty(self): + event = {"DurableExecutionArn": "arn:aws:states:us-east-1:123456789012:..."} + response = "string response" + result = extract_durable_function_status_tag(response, event) + self.assertEqual(result, {}) + + def test_missing_status_returns_empty(self): + event = {"DurableExecutionArn": "arn:aws:states:us-east-1:123456789012:..."} + response = {"Result": "some-result"} # No Status field + result = extract_durable_function_status_tag(response, event) + self.assertEqual(result, {}) + + def test_invalid_status_returns_empty(self): + event = {"DurableExecutionArn": "arn:aws:states:us-east-1:123456789012:..."} + response = {"Status": "INVALID"} + result = extract_durable_function_status_tag(response, event) + self.assertEqual(result, {}) + + def test_non_dict_event_returns_empty(self): + event = "not-a-dict" + response = {"Status": "SUCCEEDED"} + result = extract_durable_function_status_tag(response, event) + self.assertEqual(result, {}) + + def test_none_event_returns_empty(self): + event = None + response = {"Status": "SUCCEEDED"} + result = extract_durable_function_status_tag(response, event) + self.assertEqual(result, {}) + + def test_none_response_returns_empty(self): + event = {"DurableExecutionArn": "arn:aws:states:us-east-1:123456789012:..."} + response = None + result = extract_durable_function_status_tag(response, event) + self.assertEqual(result, {}) From 3ba3e763e96ff69c9e2e843c67cdac6e191bf07b Mon Sep 17 00:00:00 2001 From: John Chrostek Date: Thu, 19 Mar 2026 07:01:57 -0400 Subject: [PATCH 2/7] refactor: simplify durable status extraction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Return status string directly instead of wrapping in a dict. Remove redundant comments. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- datadog_lambda/durable.py | 21 ++++------- datadog_lambda/wrapper.py | 11 +++--- tests/test_durable.py | 76 ++++++++++++++++----------------------- 3 files changed, 41 insertions(+), 67 deletions(-) diff --git a/datadog_lambda/durable.py b/datadog_lambda/durable.py index d5e89682..1fe9139d 100644 --- a/datadog_lambda/durable.py +++ b/datadog_lambda/durable.py @@ -52,25 +52,16 @@ def extract_durable_function_tags(event): VALID_DURABLE_STATUSES = {"SUCCEEDED", "FAILED", "PENDING"} -def extract_durable_function_status_tag(response, event): +def extract_durable_execution_status(response, event): """ Extract durable function execution status from handler response. - - Returns a dict with the status tag if: - - The event indicates a durable function invocation (has DurableExecutionArn) - - The response is a dict with a valid Status field - - Returns empty dict otherwise. + Returns the status string if valid, None otherwise. """ - # Only add status tag for durable function invocations if not isinstance(event, dict) or "DurableExecutionArn" not in event: - return {} - + return None if not isinstance(response, dict): - return {} - + return None status = response.get("Status") if status not in VALID_DURABLE_STATUSES: - return {} - - return {"durable_function_execution_status": status} + return None + return status diff --git a/datadog_lambda/wrapper.py b/datadog_lambda/wrapper.py index 88cf0d86..6ccdd00f 100644 --- a/datadog_lambda/wrapper.py +++ b/datadog_lambda/wrapper.py @@ -44,7 +44,7 @@ ) from datadog_lambda.durable import ( extract_durable_function_tags, - extract_durable_function_status_tag, + extract_durable_execution_status, ) from datadog_lambda.trigger import ( extract_trigger_tags, @@ -343,12 +343,9 @@ def _after(self, event, context): if status_code: self.span.set_tag("http.status_code", status_code) - # Extract and set durable function execution status - durable_status_tags = extract_durable_function_status_tag( - self.response, event - ) - for tag_name, tag_value in durable_status_tags.items(): - self.span.set_tag(tag_name, tag_value) + durable_status = extract_durable_execution_status(self.response, event) + if durable_status: + self.span.set_tag("durable_function_execution_status", durable_status) self.span.finish() diff --git a/tests/test_durable.py b/tests/test_durable.py index 549bc45d..cb0bb6da 100644 --- a/tests/test_durable.py +++ b/tests/test_durable.py @@ -7,7 +7,7 @@ from datadog_lambda.durable import ( _parse_durable_execution_arn, extract_durable_function_tags, - extract_durable_function_status_tag, + extract_durable_execution_status, ) @@ -92,63 +92,49 @@ def test_returns_empty_dict_when_event_is_empty(self): self.assertEqual(result, {}) -class TestExtractDurableFunctionStatusTag(unittest.TestCase): - def test_succeeded_status(self): - event = {"DurableExecutionArn": "arn:aws:states:us-east-1:123456789012:..."} +class TestExtractDurableExecutionStatus(unittest.TestCase): + def test_returns_succeeded(self): + event = {"DurableExecutionArn": "arn:aws:lambda:us-east-1:123:function:f:1/durable-execution/n/id"} response = {"Status": "SUCCEEDED", "Result": "some-result"} - result = extract_durable_function_status_tag(response, event) - self.assertEqual(result, {"durable_function_execution_status": "SUCCEEDED"}) + self.assertEqual(extract_durable_execution_status(response, event), "SUCCEEDED") - def test_failed_status(self): - event = {"DurableExecutionArn": "arn:aws:states:us-east-1:123456789012:..."} + def test_returns_failed(self): + event = {"DurableExecutionArn": "arn:aws:lambda:us-east-1:123:function:f:1/durable-execution/n/id"} response = {"Status": "FAILED", "Error": "some-error"} - result = extract_durable_function_status_tag(response, event) - self.assertEqual(result, {"durable_function_execution_status": "FAILED"}) + self.assertEqual(extract_durable_execution_status(response, event), "FAILED") - def test_pending_status(self): - event = {"DurableExecutionArn": "arn:aws:states:us-east-1:123456789012:..."} + def test_returns_pending(self): + event = {"DurableExecutionArn": "arn:aws:lambda:us-east-1:123:function:f:1/durable-execution/n/id"} response = {"Status": "PENDING"} - result = extract_durable_function_status_tag(response, event) - self.assertEqual(result, {"durable_function_execution_status": "PENDING"}) + self.assertEqual(extract_durable_execution_status(response, event), "PENDING") - def test_non_durable_event_returns_empty(self): - event = {"key": "value"} # No DurableExecutionArn + def test_returns_none_for_non_durable_event(self): + event = {"key": "value"} response = {"Status": "SUCCEEDED"} - result = extract_durable_function_status_tag(response, event) - self.assertEqual(result, {}) + self.assertIsNone(extract_durable_execution_status(response, event)) - def test_non_dict_response_returns_empty(self): - event = {"DurableExecutionArn": "arn:aws:states:us-east-1:123456789012:..."} - response = "string response" - result = extract_durable_function_status_tag(response, event) - self.assertEqual(result, {}) + def test_returns_none_for_non_dict_response(self): + event = {"DurableExecutionArn": "arn:aws:lambda:us-east-1:123:function:f:1/durable-execution/n/id"} + self.assertIsNone(extract_durable_execution_status("string", event)) - def test_missing_status_returns_empty(self): - event = {"DurableExecutionArn": "arn:aws:states:us-east-1:123456789012:..."} - response = {"Result": "some-result"} # No Status field - result = extract_durable_function_status_tag(response, event) - self.assertEqual(result, {}) + def test_returns_none_for_missing_status(self): + event = {"DurableExecutionArn": "arn:aws:lambda:us-east-1:123:function:f:1/durable-execution/n/id"} + response = {"Result": "some-result"} + self.assertIsNone(extract_durable_execution_status(response, event)) - def test_invalid_status_returns_empty(self): - event = {"DurableExecutionArn": "arn:aws:states:us-east-1:123456789012:..."} + def test_returns_none_for_invalid_status(self): + event = {"DurableExecutionArn": "arn:aws:lambda:us-east-1:123:function:f:1/durable-execution/n/id"} response = {"Status": "INVALID"} - result = extract_durable_function_status_tag(response, event) - self.assertEqual(result, {}) + self.assertIsNone(extract_durable_execution_status(response, event)) - def test_non_dict_event_returns_empty(self): - event = "not-a-dict" + def test_returns_none_for_non_dict_event(self): response = {"Status": "SUCCEEDED"} - result = extract_durable_function_status_tag(response, event) - self.assertEqual(result, {}) + self.assertIsNone(extract_durable_execution_status(response, "not-a-dict")) - def test_none_event_returns_empty(self): - event = None + def test_returns_none_for_none_event(self): response = {"Status": "SUCCEEDED"} - result = extract_durable_function_status_tag(response, event) - self.assertEqual(result, {}) + self.assertIsNone(extract_durable_execution_status(response, None)) - def test_none_response_returns_empty(self): - event = {"DurableExecutionArn": "arn:aws:states:us-east-1:123456789012:..."} - response = None - result = extract_durable_function_status_tag(response, event) - self.assertEqual(result, {}) + def test_returns_none_for_none_response(self): + event = {"DurableExecutionArn": "arn:aws:lambda:us-east-1:123:function:f:1/durable-execution/n/id"} + self.assertIsNone(extract_durable_execution_status(None, event)) From 0d78f92617285a7984db61277cfb45946b6b6fba Mon Sep 17 00:00:00 2001 From: John Chrostek Date: Thu, 19 Mar 2026 07:07:30 -0400 Subject: [PATCH 3/7] remove redundant docstrings --- datadog_lambda/durable.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/datadog_lambda/durable.py b/datadog_lambda/durable.py index 1fe9139d..cc59cb12 100644 --- a/datadog_lambda/durable.py +++ b/datadog_lambda/durable.py @@ -25,11 +25,6 @@ def _parse_durable_execution_arn(arn): def extract_durable_function_tags(event): - """ - Extracts durable function tags from the Lambda event payload. - Returns a dict with durable function tags, or an empty dict if the event - is not a durable function invocation. - """ if not isinstance(event, dict): return {} @@ -53,10 +48,6 @@ def extract_durable_function_tags(event): def extract_durable_execution_status(response, event): - """ - Extract durable function execution status from handler response. - Returns the status string if valid, None otherwise. - """ if not isinstance(event, dict) or "DurableExecutionArn" not in event: return None if not isinstance(response, dict): From 9cd43fed7a624bcd614e2ed48646787f1efd412c Mon Sep 17 00:00:00 2001 From: John Chrostek Date: Thu, 19 Mar 2026 10:23:28 -0400 Subject: [PATCH 4/7] restore docstring for existing function --- datadog_lambda/durable.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/datadog_lambda/durable.py b/datadog_lambda/durable.py index cc59cb12..5dd07a55 100644 --- a/datadog_lambda/durable.py +++ b/datadog_lambda/durable.py @@ -25,6 +25,11 @@ def _parse_durable_execution_arn(arn): def extract_durable_function_tags(event): + """ + Extracts durable function tags from the Lambda event payload. + Returns a dict with durable function tags, or an empty dict if the event + is not a durable function invocation. + """ if not isinstance(event, dict): return {} From dded86ed0b1b6488f9ea374cb0cfafd7f4d8a46a Mon Sep 17 00:00:00 2001 From: John Chrostek Date: Thu, 19 Mar 2026 10:26:08 -0400 Subject: [PATCH 5/7] fix: black formatting --- datadog_lambda/wrapper.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/datadog_lambda/wrapper.py b/datadog_lambda/wrapper.py index 6ccdd00f..13383dc6 100644 --- a/datadog_lambda/wrapper.py +++ b/datadog_lambda/wrapper.py @@ -345,7 +345,9 @@ def _after(self, event, context): durable_status = extract_durable_execution_status(self.response, event) if durable_status: - self.span.set_tag("durable_function_execution_status", durable_status) + self.span.set_tag( + "durable_function_execution_status", durable_status + ) self.span.finish() From 993ffcfb23e22d8b5b285afb22397326ae97369d Mon Sep 17 00:00:00 2001 From: John Chrostek Date: Thu, 19 Mar 2026 10:31:19 -0400 Subject: [PATCH 6/7] fix: apply black formatting --- datadog_lambda/wrapper.py | 2 +- tests/test_durable.py | 28 +++++++++++++++++++++------- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/datadog_lambda/wrapper.py b/datadog_lambda/wrapper.py index 13383dc6..b5f3b7d5 100644 --- a/datadog_lambda/wrapper.py +++ b/datadog_lambda/wrapper.py @@ -156,7 +156,7 @@ def __init__(self, func): if config.trace_extractor: extractor_parts = config.trace_extractor.rsplit(".", 1) if len(extractor_parts) == 2: - (mod_name, extractor_name) = extractor_parts + mod_name, extractor_name = extractor_parts modified_extractor_name = modify_module_name(mod_name) extractor_module = import_module(modified_extractor_name) self.trace_extractor = getattr(extractor_module, extractor_name) diff --git a/tests/test_durable.py b/tests/test_durable.py index cb0bb6da..d676b2bc 100644 --- a/tests/test_durable.py +++ b/tests/test_durable.py @@ -94,17 +94,23 @@ def test_returns_empty_dict_when_event_is_empty(self): class TestExtractDurableExecutionStatus(unittest.TestCase): def test_returns_succeeded(self): - event = {"DurableExecutionArn": "arn:aws:lambda:us-east-1:123:function:f:1/durable-execution/n/id"} + event = { + "DurableExecutionArn": "arn:aws:lambda:us-east-1:123:function:f:1/durable-execution/n/id" + } response = {"Status": "SUCCEEDED", "Result": "some-result"} self.assertEqual(extract_durable_execution_status(response, event), "SUCCEEDED") def test_returns_failed(self): - event = {"DurableExecutionArn": "arn:aws:lambda:us-east-1:123:function:f:1/durable-execution/n/id"} + event = { + "DurableExecutionArn": "arn:aws:lambda:us-east-1:123:function:f:1/durable-execution/n/id" + } response = {"Status": "FAILED", "Error": "some-error"} self.assertEqual(extract_durable_execution_status(response, event), "FAILED") def test_returns_pending(self): - event = {"DurableExecutionArn": "arn:aws:lambda:us-east-1:123:function:f:1/durable-execution/n/id"} + event = { + "DurableExecutionArn": "arn:aws:lambda:us-east-1:123:function:f:1/durable-execution/n/id" + } response = {"Status": "PENDING"} self.assertEqual(extract_durable_execution_status(response, event), "PENDING") @@ -114,16 +120,22 @@ def test_returns_none_for_non_durable_event(self): self.assertIsNone(extract_durable_execution_status(response, event)) def test_returns_none_for_non_dict_response(self): - event = {"DurableExecutionArn": "arn:aws:lambda:us-east-1:123:function:f:1/durable-execution/n/id"} + event = { + "DurableExecutionArn": "arn:aws:lambda:us-east-1:123:function:f:1/durable-execution/n/id" + } self.assertIsNone(extract_durable_execution_status("string", event)) def test_returns_none_for_missing_status(self): - event = {"DurableExecutionArn": "arn:aws:lambda:us-east-1:123:function:f:1/durable-execution/n/id"} + event = { + "DurableExecutionArn": "arn:aws:lambda:us-east-1:123:function:f:1/durable-execution/n/id" + } response = {"Result": "some-result"} self.assertIsNone(extract_durable_execution_status(response, event)) def test_returns_none_for_invalid_status(self): - event = {"DurableExecutionArn": "arn:aws:lambda:us-east-1:123:function:f:1/durable-execution/n/id"} + event = { + "DurableExecutionArn": "arn:aws:lambda:us-east-1:123:function:f:1/durable-execution/n/id" + } response = {"Status": "INVALID"} self.assertIsNone(extract_durable_execution_status(response, event)) @@ -136,5 +148,7 @@ def test_returns_none_for_none_event(self): self.assertIsNone(extract_durable_execution_status(response, None)) def test_returns_none_for_none_response(self): - event = {"DurableExecutionArn": "arn:aws:lambda:us-east-1:123:function:f:1/durable-execution/n/id"} + event = { + "DurableExecutionArn": "arn:aws:lambda:us-east-1:123:function:f:1/durable-execution/n/id" + } self.assertIsNone(extract_durable_execution_status(None, event)) From f3a621c348fc28e2400a827120fbf395fb176c96 Mon Sep 17 00:00:00 2001 From: John Chrostek Date: Thu, 19 Mar 2026 10:45:45 -0400 Subject: [PATCH 7/7] fix: use correct durable execution status values (SUCCEEDED, FAILED, STOPPED, TIMED_OUT) --- datadog_lambda/durable.py | 2 +- tests/test_durable.py | 13 ++++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/datadog_lambda/durable.py b/datadog_lambda/durable.py index 5dd07a55..3174cd6c 100644 --- a/datadog_lambda/durable.py +++ b/datadog_lambda/durable.py @@ -49,7 +49,7 @@ def extract_durable_function_tags(event): } -VALID_DURABLE_STATUSES = {"SUCCEEDED", "FAILED", "PENDING"} +VALID_DURABLE_STATUSES = {"SUCCEEDED", "FAILED", "STOPPED", "TIMED_OUT"} def extract_durable_execution_status(response, event): diff --git a/tests/test_durable.py b/tests/test_durable.py index d676b2bc..e668b45f 100644 --- a/tests/test_durable.py +++ b/tests/test_durable.py @@ -107,12 +107,19 @@ def test_returns_failed(self): response = {"Status": "FAILED", "Error": "some-error"} self.assertEqual(extract_durable_execution_status(response, event), "FAILED") - def test_returns_pending(self): + def test_returns_stopped(self): event = { "DurableExecutionArn": "arn:aws:lambda:us-east-1:123:function:f:1/durable-execution/n/id" } - response = {"Status": "PENDING"} - self.assertEqual(extract_durable_execution_status(response, event), "PENDING") + response = {"Status": "STOPPED"} + self.assertEqual(extract_durable_execution_status(response, event), "STOPPED") + + def test_returns_timed_out(self): + event = { + "DurableExecutionArn": "arn:aws:lambda:us-east-1:123:function:f:1/durable-execution/n/id" + } + response = {"Status": "TIMED_OUT"} + self.assertEqual(extract_durable_execution_status(response, event), "TIMED_OUT") def test_returns_none_for_non_durable_event(self): event = {"key": "value"}