Skip to content

Commit 1fafd5e

Browse files
lym953claude
andauthored
feat: add durable function execution tags to Lambda spans (#728)
* 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> * Change log level from debug to error * Format * fmt * Fix long line --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 1031435 commit 1fafd5e

File tree

4 files changed

+146
-0
lines changed

4 files changed

+146
-0
lines changed

datadog_lambda/durable.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
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:
15+
arn:aws:lambda:{region}:{account}:function:{func}:{version}/durable-execution/{name}/{id}
16+
Returns (execution_name, execution_id) or None if parsing fails.
17+
"""
18+
match = re.search(r"/durable-execution/([^/]+)/([^/]+)$", arn)
19+
if not match:
20+
return None
21+
execution_name, execution_id = match.group(1), match.group(2)
22+
if not execution_name or not execution_id:
23+
return None
24+
return execution_name, execution_id
25+
26+
27+
def extract_durable_function_tags(event):
28+
"""
29+
Extracts durable function tags from the Lambda event payload.
30+
Returns a dict with durable function tags, or an empty dict if the event
31+
is not a durable function invocation.
32+
"""
33+
if not isinstance(event, dict):
34+
return {}
35+
36+
durable_execution_arn = event.get("DurableExecutionArn")
37+
if not isinstance(durable_execution_arn, str):
38+
return {}
39+
40+
parsed = _parse_durable_execution_arn(durable_execution_arn)
41+
if not parsed:
42+
logger.error("Failed to parse DurableExecutionArn: %s", durable_execution_arn)
43+
return {}
44+
45+
execution_name, execution_id = parsed
46+
return {
47+
"durable_function_execution_name": execution_name,
48+
"durable_function_execution_id": execution_id,
49+
}

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: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
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(
43+
result, ("my-execution", "550e8400-e29b-41d4-a716-446655440004")
44+
)
45+
46+
47+
class TestExtractDurableFunctionTags(unittest.TestCase):
48+
def test_extracts_tags_from_event_with_durable_execution_arn(self):
49+
event = {
50+
"DurableExecutionArn": "arn:aws:lambda:us-east-1:123456789012:function:my-func:1/durable-execution/my-execution/550e8400-e29b-41d4-a716-446655440004",
51+
"CheckpointToken": "some-token",
52+
"InitialExecutionState": {"Operations": []},
53+
}
54+
result = extract_durable_function_tags(event)
55+
self.assertEqual(
56+
result,
57+
{
58+
"durable_function_execution_name": "my-execution",
59+
"durable_function_execution_id": "550e8400-e29b-41d4-a716-446655440004",
60+
},
61+
)
62+
63+
def test_returns_empty_dict_for_regular_lambda_event(self):
64+
event = {
65+
"body": '{"key": "value"}',
66+
"headers": {"Content-Type": "application/json"},
67+
}
68+
result = extract_durable_function_tags(event)
69+
self.assertEqual(result, {})
70+
71+
def test_returns_empty_dict_when_event_is_none(self):
72+
result = extract_durable_function_tags(None)
73+
self.assertEqual(result, {})
74+
75+
def test_returns_empty_dict_when_event_is_not_a_dict(self):
76+
result = extract_durable_function_tags("not-a-dict")
77+
self.assertEqual(result, {})
78+
79+
def test_returns_empty_dict_when_durable_execution_arn_is_not_a_string(self):
80+
event = {"DurableExecutionArn": 12345}
81+
result = extract_durable_function_tags(event)
82+
self.assertEqual(result, {})
83+
84+
def test_returns_empty_dict_when_durable_execution_arn_cannot_be_parsed(self):
85+
event = {"DurableExecutionArn": "invalid-arn-without-durable-execution-marker"}
86+
result = extract_durable_function_tags(event)
87+
self.assertEqual(result, {})
88+
89+
def test_returns_empty_dict_when_event_is_empty(self):
90+
result = extract_durable_function_tags({})
91+
self.assertEqual(result, {})

0 commit comments

Comments
 (0)