Skip to content

Commit 781646a

Browse files
lym953claude
andcommitted
Add durable_function_first_invocation tag to aws.lambda spans
Extends extract_durable_function_tags() to set durable_function_first_invocation ("true"/"false") by checking len(InitialExecutionState.Operations) <= 1, mirroring the SDK's own replay detection logic (ReplayStatus.REPLAY when len > 1). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent c64baa8 commit 781646a

3 files changed

Lines changed: 43 additions & 29 deletions

File tree

datadog_lambda/durable.py

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ def _parse_durable_execution_arn(arn):
2424
return execution_name, execution_id
2525

2626

27-
def extract_durable_function_tags(event, state=None):
27+
def extract_durable_function_tags(event):
2828
"""
2929
Extracts durable function tags from the Lambda event payload.
3030
Returns a dict with durable function tags, or an empty dict if the event
@@ -43,15 +43,10 @@ def extract_durable_function_tags(event, state=None):
4343
return {}
4444

4545
execution_name, execution_id = parsed
46-
try:
47-
first_invocation = str(not state.is_replaying()).lower()
48-
except Exception:
49-
logger.debug("Failed to compute durable_function_first_invocation")
50-
first_invocation = None
51-
tags = {
46+
operations = event.get("InitialExecutionState", {}).get("Operations", [])
47+
is_first_invocation = len(operations) <= 1
48+
return {
5249
"durable_function_execution_name": execution_name,
5350
"durable_function_execution_id": execution_id,
51+
"durable_function_first_invocation": str(is_first_invocation).lower(),
5452
}
55-
if first_invocation is not None:
56-
tags["durable_function_first_invocation"] = first_invocation
57-
return tags

datadog_lambda/wrapper.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ def __init__(self, func):
153153
if config.trace_extractor:
154154
extractor_parts = config.trace_extractor.rsplit(".", 1)
155155
if len(extractor_parts) == 2:
156-
mod_name, extractor_name = extractor_parts
156+
(mod_name, extractor_name) = extractor_parts
157157
modified_extractor_name = modify_module_name(mod_name)
158158
extractor_module = import_module(modified_extractor_name)
159159
self.trace_extractor = getattr(extractor_module, extractor_name)
@@ -244,9 +244,7 @@ def _before(self, event, context):
244244
submit_invocations_metric(context)
245245

246246
self.trigger_tags = extract_trigger_tags(event, context)
247-
self.durable_function_tags = extract_durable_function_tags(
248-
event, context.state
249-
)
247+
self.durable_function_tags = extract_durable_function_tags(event)
250248
# Extract Datadog trace context and source from incoming requests
251249
dd_context, trace_context_source, event_source = extract_dd_trace_context(
252250
event,

tests/test_durable.py

Lines changed: 36 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
# This product includes software developed at Datadog (https://www.datadoghq.com/).
44
# Copyright 2019 Datadog, Inc.
55
import unittest
6-
from unittest.mock import MagicMock
76

87
from datadog_lambda.durable import (
98
_parse_durable_execution_arn,
@@ -46,31 +45,31 @@ def test_works_with_numeric_version_qualifier(self):
4645

4746

4847
class TestExtractDurableFunctionTags(unittest.TestCase):
49-
def test_extracts_tags_from_event_with_durable_execution_arn(self):
48+
def test_sets_first_invocation_true_when_only_execution_operation(self):
49+
# One operation (the EXECUTION op itself) → not replaying → first invocation
5050
event = {
5151
"DurableExecutionArn": "arn:aws:lambda:us-east-1:123456789012:function:my-func:1/durable-execution/my-execution/550e8400-e29b-41d4-a716-446655440004",
5252
"CheckpointToken": "some-token",
53-
"InitialExecutionState": {"Operations": []},
53+
"InitialExecutionState": {"Operations": [{"OperationType": "EXECUTION"}]},
5454
}
55-
state = MagicMock()
56-
state.is_replaying.return_value = True
57-
result = extract_durable_function_tags(event, state)
55+
result = extract_durable_function_tags(event)
5856
self.assertEqual(
5957
result,
6058
{
6159
"durable_function_execution_name": "my-execution",
6260
"durable_function_execution_id": "550e8400-e29b-41d4-a716-446655440004",
63-
"durable_function_first_invocation": "false",
61+
"durable_function_first_invocation": "true",
6462
},
6563
)
6664

67-
def test_sets_first_invocation_true_when_not_replaying(self):
65+
def test_sets_first_invocation_true_when_no_operations(self):
66+
# Empty operations list → not replaying → first invocation
6867
event = {
6968
"DurableExecutionArn": "arn:aws:lambda:us-east-1:123456789012:function:my-func:1/durable-execution/my-execution/550e8400-e29b-41d4-a716-446655440004",
69+
"CheckpointToken": "some-token",
70+
"InitialExecutionState": {"Operations": []},
7071
}
71-
state = MagicMock()
72-
state.is_replaying.return_value = False
73-
result = extract_durable_function_tags(event, state)
72+
result = extract_durable_function_tags(event)
7473
self.assertEqual(
7574
result,
7675
{
@@ -80,13 +79,19 @@ def test_sets_first_invocation_true_when_not_replaying(self):
8079
},
8180
)
8281

83-
def test_sets_first_invocation_false_when_replaying(self):
82+
def test_sets_first_invocation_false_when_multiple_operations(self):
83+
# More than one operation → replaying → not first invocation
8484
event = {
8585
"DurableExecutionArn": "arn:aws:lambda:us-east-1:123456789012:function:my-func:1/durable-execution/my-execution/550e8400-e29b-41d4-a716-446655440004",
86+
"CheckpointToken": "some-token",
87+
"InitialExecutionState": {
88+
"Operations": [
89+
{"OperationType": "EXECUTION"},
90+
{"OperationType": "STEP"},
91+
]
92+
},
8693
}
87-
state = MagicMock()
88-
state.is_replaying.return_value = True
89-
result = extract_durable_function_tags(event, state)
94+
result = extract_durable_function_tags(event)
9095
self.assertEqual(
9196
result,
9297
{
@@ -96,6 +101,22 @@ def test_sets_first_invocation_false_when_replaying(self):
96101
},
97102
)
98103

104+
def test_sets_first_invocation_true_when_initial_execution_state_absent(self):
105+
# No InitialExecutionState key → treated as empty → first invocation
106+
event = {
107+
"DurableExecutionArn": "arn:aws:lambda:us-east-1:123456789012:function:my-func:1/durable-execution/my-execution/550e8400-e29b-41d4-a716-446655440004",
108+
"CheckpointToken": "some-token",
109+
}
110+
result = extract_durable_function_tags(event)
111+
self.assertEqual(
112+
result,
113+
{
114+
"durable_function_execution_name": "my-execution",
115+
"durable_function_execution_id": "550e8400-e29b-41d4-a716-446655440004",
116+
"durable_function_first_invocation": "true",
117+
},
118+
)
119+
99120
def test_returns_empty_dict_for_regular_lambda_event(self):
100121
event = {
101122
"body": '{"key": "value"}',

0 commit comments

Comments
 (0)