Skip to content

Commit ce53280

Browse files
authored
Lambda and Step Functions Span Linking (#320)
1 parent ad8140a commit ce53280

File tree

3 files changed

+68
-5
lines changed

3 files changed

+68
-5
lines changed

datadog_lambda/tracing.py

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
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-
5+
import hashlib
66
import logging
77
import os
88
import json
@@ -328,6 +328,39 @@ def extract_context_from_kinesis_event(event, lambda_context):
328328
return extract_context_from_lambda_context(lambda_context)
329329

330330

331+
def _deterministic_md5_hash(s: str) -> str:
332+
"""MD5 here is to generate trace_id, not for any encryption."""
333+
hex_number = hashlib.md5(s.encode("ascii")).hexdigest()
334+
binary = bin(int(hex_number, 16))
335+
binary_str = str(binary)
336+
binary_str_remove_0b = binary_str[2:].rjust(128, "0")
337+
most_significant_64_bits_without_leading_1 = "0" + binary_str_remove_0b[1:-64]
338+
result = str(int(most_significant_64_bits_without_leading_1, 2))
339+
if result == "0" * 64:
340+
return "1"
341+
return result
342+
343+
344+
def extract_context_from_step_functions(event, lambda_context):
345+
"""
346+
Only extract datadog trace context when Step Functions Context Object is injected
347+
into lambda's event dict.
348+
"""
349+
try:
350+
execution_id = event.get("Execution").get("Id")
351+
state_name = event.get("State").get("Name")
352+
state_entered_time = event.get("State").get("EnteredTime")
353+
trace_id = _deterministic_md5_hash(execution_id)
354+
parent_id = _deterministic_md5_hash(
355+
execution_id + "#" + state_name + "#" + state_entered_time
356+
)
357+
sampling_priority = SamplingPriority.AUTO_KEEP
358+
return trace_id, parent_id, sampling_priority
359+
except Exception as e:
360+
logger.debug("The Step Functions trace extractor returned with error %s", e)
361+
return extract_context_from_lambda_context(lambda_context)
362+
363+
331364
def extract_context_custom_extractor(extractor, event, lambda_context):
332365
"""
333366
Extract Datadog trace context using a custom trace extractor function
@@ -440,6 +473,12 @@ def extract_dd_trace_context(
440473
parent_id,
441474
sampling_priority,
442475
) = extract_context_from_kinesis_event(event, lambda_context)
476+
elif event_source.equals(EventTypes.STEPFUNCTIONS):
477+
(
478+
trace_id,
479+
parent_id,
480+
sampling_priority,
481+
) = extract_context_from_step_functions(event, lambda_context)
443482
else:
444483
trace_id, parent_id, sampling_priority = extract_context_from_lambda_context(
445484
lambda_context

datadog_lambda/trigger.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,13 @@ class EventTypes(_stringTypedEnum):
3434
CLOUDWATCH_EVENTS = "cloudwatch-events"
3535
CLOUDFRONT = "cloudfront"
3636
DYNAMODB = "dynamodb"
37+
EVENTBRIDGE = "eventbridge"
3738
KINESIS = "kinesis"
3839
LAMBDA_FUNCTION_URL = "lambda-function-url"
3940
S3 = "s3"
4041
SNS = "sns"
4142
SQS = "sqs"
42-
EVENTBRIDGE = "eventbridge"
43+
STEPFUNCTIONS = "states"
4344

4445

4546
class EventSubtypes(_stringTypedEnum):
@@ -145,6 +146,9 @@ def parse_event_source(event: dict) -> _EventSource:
145146
if event.get("source") == "aws.events" or has_event_categories:
146147
event_source = _EventSource(EventTypes.CLOUDWATCH_EVENTS)
147148

149+
if "Execution" in event and "StateMachine" in event and "State" in event:
150+
event_source = _EventSource(EventTypes.STEPFUNCTIONS)
151+
148152
event_record = get_first_record(event)
149153
if event_record:
150154
aws_event_source = event_record.get(

tests/test_tracing.py

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from unittest.mock import MagicMock, Mock, patch, call
66

77
import ddtrace
8-
from ddtrace.constants import ERROR_MSG, ERROR_TYPE
8+
99
from ddtrace import tracer
1010
from ddtrace.context import Context
1111

@@ -15,6 +15,7 @@
1515
XraySubsegment,
1616
)
1717
from datadog_lambda.tracing import (
18+
_deterministic_md5_hash,
1819
create_inferred_span,
1920
extract_dd_trace_context,
2021
create_dd_dummy_metadata_subsegment,
@@ -1334,9 +1335,7 @@ def test_create_inferred_span_from_api_gateway_event_no_apiid(self):
13341335
event = json.load(event)
13351336
ctx = get_mock_context()
13361337
ctx.aws_request_id = "123"
1337-
print(event)
13381338
span = create_inferred_span(event, ctx)
1339-
print(span)
13401339
self.assertEqual(span.get_tag("operation_name"), "aws.apigateway.rest")
13411340
self.assertEqual(
13421341
span.service,
@@ -1389,3 +1388,24 @@ def test_no_error_with_nonetype_headers(self):
13891388
lambda_ctx,
13901389
)
13911390
self.assertEqual(ctx, None)
1391+
1392+
1393+
class TestStepFunctionsTraceContext(unittest.TestCase):
1394+
def test_deterministic_m5_hash(self):
1395+
result = _deterministic_md5_hash("some_testing_random_string")
1396+
self.assertEqual("2251275791555400689", result)
1397+
1398+
def test_deterministic_m5_hash__result_the_same_as_backend(self):
1399+
result = _deterministic_md5_hash(
1400+
"arn:aws:states:sa-east-1:601427271234:express:DatadogStateMachine:acaf1a67-336a-e854-1599-2a627eb2dd8a"
1401+
":c8baf081-31f1-464d-971f-70cb17d01111#step-one#2022-12-08T21:08:19.224Z"
1402+
)
1403+
self.assertEqual("8034507082463708833", result)
1404+
1405+
def test_deterministic_m5_hash__always_leading_with_zero(self):
1406+
for i in range(100):
1407+
result = _deterministic_md5_hash(str(i))
1408+
result_in_binary = bin(int(result))
1409+
# Leading zeros will be omitted, so only test for full 64 bits present
1410+
if len(result_in_binary) == 66: # "0b" + 64 bits.
1411+
self.assertTrue(result_in_binary.startswith("0b0"))

0 commit comments

Comments
 (0)