Skip to content

Commit 0d40bd1

Browse files
lym953claude
andcommitted
feat: add durable function execution tags to Lambda spans
Extract DurableExecutionArn from the Lambda event payload and add durable_function_execution_name and durable_function_execution_id as span tags, matching the equivalent feature in datadog-lambda-js#730. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 8cfa94f commit 0d40bd1

File tree

4 files changed

+145
-0
lines changed

4 files changed

+145
-0
lines changed

datadog_lambda/durable.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# Unless explicitly stated otherwise all files in this repository are licensed
2+
# under the Apache License Version 2.0.
3+
# This product includes software developed at Datadog (https://www.datadoghq.com/).
4+
# Copyright 2019 Datadog, Inc.
5+
import logging
6+
import re
7+
8+
logger = logging.getLogger(__name__)
9+
10+
11+
def _parse_durable_execution_arn(arn):
12+
"""
13+
Parses a DurableExecutionArn to extract execution name and ID.
14+
ARN format: arn:aws:lambda:{region}:{account}:function:{func}:{version}/durable-execution/{name}/{id}
15+
Returns (execution_name, execution_id) or None if parsing fails.
16+
"""
17+
match = re.search(r"/durable-execution/([^/]+)/([^/]+)$", arn)
18+
if not match:
19+
return None
20+
execution_name, execution_id = match.group(1), match.group(2)
21+
if not execution_name or not execution_id:
22+
return None
23+
return execution_name, execution_id
24+
25+
26+
def extract_durable_function_tags(event):
27+
"""
28+
Extracts durable function tags from the Lambda event payload.
29+
Returns a dict with durable function tags, or an empty dict if the event
30+
is not a durable function invocation.
31+
"""
32+
if not isinstance(event, dict):
33+
return {}
34+
35+
durable_execution_arn = event.get("DurableExecutionArn")
36+
if not isinstance(durable_execution_arn, str):
37+
return {}
38+
39+
parsed = _parse_durable_execution_arn(durable_execution_arn)
40+
if not parsed:
41+
logger.debug(
42+
"Failed to parse DurableExecutionArn: %s", durable_execution_arn
43+
)
44+
return {}
45+
46+
execution_name, execution_id = parsed
47+
return {
48+
"durable_function_execution_name": execution_name,
49+
"durable_function_execution_id": execution_id,
50+
}

datadog_lambda/tracing.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1449,6 +1449,7 @@ def create_function_execution_span(
14491449
trace_context_source,
14501450
merge_xray_traces,
14511451
trigger_tags,
1452+
durable_function_tags=None,
14521453
parent_span=None,
14531454
span_pointers=None,
14541455
):
@@ -1477,6 +1478,8 @@ def create_function_execution_span(
14771478
if trace_context_source == TraceContextSource.XRAY and merge_xray_traces:
14781479
tags["_dd.parent_source"] = trace_context_source
14791480
tags.update(trigger_tags)
1481+
if durable_function_tags:
1482+
tags.update(durable_function_tags)
14801483
tracer.set_tags(_dd_origin)
14811484
# Determine service name based on config and env var
14821485
if config.service:

datadog_lambda/wrapper.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
tracer,
4343
propagator,
4444
)
45+
from datadog_lambda.durable import extract_durable_function_tags
4546
from datadog_lambda.trigger import (
4647
extract_trigger_tags,
4748
extract_http_status_code_tag,
@@ -243,6 +244,7 @@ def _before(self, event, context):
243244
submit_invocations_metric(context)
244245

245246
self.trigger_tags = extract_trigger_tags(event, context)
247+
self.durable_function_tags = extract_durable_function_tags(event)
246248
# Extract Datadog trace context and source from incoming requests
247249
dd_context, trace_context_source, event_source = extract_dd_trace_context(
248250
event,
@@ -280,6 +282,7 @@ def _before(self, event, context):
280282
trace_context_source=trace_context_source,
281283
merge_xray_traces=config.merge_xray_traces,
282284
trigger_tags=self.trigger_tags,
285+
durable_function_tags=self.durable_function_tags,
283286
parent_span=self.inferred_span,
284287
span_pointers=calculate_span_pointers(event_source, event),
285288
)

tests/test_durable.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# Unless explicitly stated otherwise all files in this repository are licensed
2+
# under the Apache License Version 2.0.
3+
# This product includes software developed at Datadog (https://www.datadoghq.com/).
4+
# Copyright 2019 Datadog, Inc.
5+
import unittest
6+
7+
from datadog_lambda.durable import (
8+
_parse_durable_execution_arn,
9+
extract_durable_function_tags,
10+
)
11+
12+
13+
class TestParseDurableExecutionArn(unittest.TestCase):
14+
def test_returns_name_and_id_for_valid_arn(self):
15+
arn = "arn:aws:lambda:us-east-1:123456789012:function:my-func:$LATEST/durable-execution/order-123/550e8400-e29b-41d4-a716-446655440001"
16+
result = _parse_durable_execution_arn(arn)
17+
self.assertEqual(result, ("order-123", "550e8400-e29b-41d4-a716-446655440001"))
18+
19+
def test_returns_none_for_arn_without_durable_execution_marker(self):
20+
arn = "arn:aws:lambda:us-east-1:123456789012:function:my-func:$LATEST"
21+
result = _parse_durable_execution_arn(arn)
22+
self.assertIsNone(result)
23+
24+
def test_returns_none_for_malformed_arn_with_only_execution_name(self):
25+
arn = "arn:aws:lambda:us-east-1:123456789012:function:my-func:$LATEST/durable-execution/order-123"
26+
result = _parse_durable_execution_arn(arn)
27+
self.assertIsNone(result)
28+
29+
def test_returns_none_for_malformed_arn_with_empty_execution_name(self):
30+
arn = "arn:aws:lambda:us-east-1:123456789012:function:my-func:$LATEST/durable-execution//550e8400-e29b-41d4-a716-446655440002"
31+
result = _parse_durable_execution_arn(arn)
32+
self.assertIsNone(result)
33+
34+
def test_returns_none_for_malformed_arn_with_empty_execution_id(self):
35+
arn = "arn:aws:lambda:us-east-1:123456789012:function:my-func:$LATEST/durable-execution/order-123/"
36+
result = _parse_durable_execution_arn(arn)
37+
self.assertIsNone(result)
38+
39+
def test_works_with_numeric_version_qualifier(self):
40+
arn = "arn:aws:lambda:us-east-1:123456789012:function:my-func:1/durable-execution/my-execution/550e8400-e29b-41d4-a716-446655440004"
41+
result = _parse_durable_execution_arn(arn)
42+
self.assertEqual(result, ("my-execution", "550e8400-e29b-41d4-a716-446655440004"))
43+
44+
45+
class TestExtractDurableFunctionTags(unittest.TestCase):
46+
def test_extracts_tags_from_event_with_durable_execution_arn(self):
47+
event = {
48+
"DurableExecutionArn": "arn:aws:lambda:us-east-1:123456789012:function:my-func:1/durable-execution/my-execution/550e8400-e29b-41d4-a716-446655440004",
49+
"CheckpointToken": "some-token",
50+
"InitialExecutionState": {"Operations": []},
51+
}
52+
result = extract_durable_function_tags(event)
53+
self.assertEqual(
54+
result,
55+
{
56+
"durable_function_execution_name": "my-execution",
57+
"durable_function_execution_id": "550e8400-e29b-41d4-a716-446655440004",
58+
},
59+
)
60+
61+
def test_returns_empty_dict_for_regular_lambda_event(self):
62+
event = {
63+
"body": '{"key": "value"}',
64+
"headers": {"Content-Type": "application/json"},
65+
}
66+
result = extract_durable_function_tags(event)
67+
self.assertEqual(result, {})
68+
69+
def test_returns_empty_dict_when_event_is_none(self):
70+
result = extract_durable_function_tags(None)
71+
self.assertEqual(result, {})
72+
73+
def test_returns_empty_dict_when_event_is_not_a_dict(self):
74+
result = extract_durable_function_tags("not-a-dict")
75+
self.assertEqual(result, {})
76+
77+
def test_returns_empty_dict_when_durable_execution_arn_is_not_a_string(self):
78+
event = {"DurableExecutionArn": 12345}
79+
result = extract_durable_function_tags(event)
80+
self.assertEqual(result, {})
81+
82+
def test_returns_empty_dict_when_durable_execution_arn_cannot_be_parsed(self):
83+
event = {"DurableExecutionArn": "invalid-arn-without-durable-execution-marker"}
84+
result = extract_durable_function_tags(event)
85+
self.assertEqual(result, {})
86+
87+
def test_returns_empty_dict_when_event_is_empty(self):
88+
result = extract_durable_function_tags({})
89+
self.assertEqual(result, {})

0 commit comments

Comments
 (0)