Skip to content

Commit 23be005

Browse files
lym953claude
andcommitted
Add durable_function_first_invocation tag to aws.lambda spans
Introduces a durable_execution() decorator that sets the durable_function_first_invocation tag ("true"/"false") on the active span based on whether the orchestration context is replaying history. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 8fe789e commit 23be005

File tree

2 files changed

+102
-0
lines changed

2 files changed

+102
-0
lines changed

datadog_lambda/durable.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@
22
# under the Apache License Version 2.0.
33
# This product includes software developed at Datadog (https://www.datadoghq.com/).
44
# Copyright 2019 Datadog, Inc.
5+
import functools
56
import logging
67
import re
78

9+
from ddtrace import tracer
10+
811
logger = logging.getLogger(__name__)
912

1013

@@ -47,3 +50,27 @@ def extract_durable_function_tags(event):
4750
"durable_function_execution_name": execution_name,
4851
"durable_function_execution_id": execution_id,
4952
}
53+
54+
55+
def durable_execution(func):
56+
"""
57+
Decorator for AWS Lambda durable execution orchestration functions.
58+
Sets the durable_function_first_invocation tag on the current span
59+
based on whether this is the first invocation (not replaying history).
60+
"""
61+
62+
@functools.wraps(func)
63+
def wrapper(context, *args, **kwargs):
64+
try:
65+
is_first_invocation = not context.state.is_replaying()
66+
span = tracer.current_span()
67+
if span:
68+
span.set_tag(
69+
"durable_function_first_invocation",
70+
str(is_first_invocation).lower(),
71+
)
72+
except Exception:
73+
logger.debug("Failed to set durable_function_first_invocation tag")
74+
return func(context, *args, **kwargs)
75+
76+
return wrapper

tests/test_durable.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
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, patch
67

78
from datadog_lambda.durable import (
89
_parse_durable_execution_arn,
10+
durable_execution,
911
extract_durable_function_tags,
1012
)
1113

@@ -89,3 +91,76 @@ def test_returns_empty_dict_when_durable_execution_arn_cannot_be_parsed(self):
8991
def test_returns_empty_dict_when_event_is_empty(self):
9092
result = extract_durable_function_tags({})
9193
self.assertEqual(result, {})
94+
95+
96+
class TestDurableExecution(unittest.TestCase):
97+
def _make_durable_context(self, is_replaying):
98+
ctx = MagicMock()
99+
ctx.state.is_replaying.return_value = is_replaying
100+
return ctx
101+
102+
def test_sets_first_invocation_true_when_not_replaying(self):
103+
mock_span = MagicMock()
104+
with patch("datadog_lambda.durable.tracer") as mock_tracer:
105+
mock_tracer.current_span.return_value = mock_span
106+
ctx = self._make_durable_context(is_replaying=False)
107+
108+
@durable_execution
109+
def handler(context):
110+
return "result"
111+
112+
result = handler(ctx)
113+
114+
self.assertEqual(result, "result")
115+
mock_span.set_tag.assert_called_once_with(
116+
"durable_function_first_invocation", "true"
117+
)
118+
119+
def test_sets_first_invocation_false_when_replaying(self):
120+
mock_span = MagicMock()
121+
with patch("datadog_lambda.durable.tracer") as mock_tracer:
122+
mock_tracer.current_span.return_value = mock_span
123+
ctx = self._make_durable_context(is_replaying=True)
124+
125+
@durable_execution
126+
def handler(context):
127+
return "result"
128+
129+
result = handler(ctx)
130+
131+
self.assertEqual(result, "result")
132+
mock_span.set_tag.assert_called_once_with(
133+
"durable_function_first_invocation", "false"
134+
)
135+
136+
def test_does_not_set_tag_when_no_active_span(self):
137+
with patch("datadog_lambda.durable.tracer") as mock_tracer:
138+
mock_tracer.current_span.return_value = None
139+
ctx = self._make_durable_context(is_replaying=False)
140+
141+
@durable_execution
142+
def handler(context):
143+
return "result"
144+
145+
result = handler(ctx)
146+
147+
self.assertEqual(result, "result")
148+
149+
def test_does_not_raise_when_context_has_no_state(self):
150+
ctx = MagicMock(spec=[]) # no attributes
151+
with patch("datadog_lambda.durable.tracer"):
152+
153+
@durable_execution
154+
def handler(context):
155+
return "result"
156+
157+
result = handler(ctx)
158+
159+
self.assertEqual(result, "result")
160+
161+
def test_preserves_function_name(self):
162+
@durable_execution
163+
def my_handler(context):
164+
pass
165+
166+
self.assertEqual(my_handler.__name__, "my_handler")

0 commit comments

Comments
 (0)