datadog_lambda 6.105.0__tar.gz → 6.107.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {datadog_lambda-6.105.0 → datadog_lambda-6.107.0}/PKG-INFO +3 -3
- datadog_lambda-6.107.0/datadog_lambda/api.py +139 -0
- {datadog_lambda-6.105.0 → datadog_lambda-6.107.0}/datadog_lambda/handler.py +0 -1
- {datadog_lambda-6.105.0 → datadog_lambda-6.107.0}/datadog_lambda/metric.py +30 -0
- {datadog_lambda-6.105.0 → datadog_lambda-6.107.0}/datadog_lambda/span_pointers.py +4 -0
- {datadog_lambda-6.105.0 → datadog_lambda-6.107.0}/datadog_lambda/tracing.py +75 -34
- {datadog_lambda-6.105.0 → datadog_lambda-6.107.0}/datadog_lambda/trigger.py +29 -4
- datadog_lambda-6.107.0/datadog_lambda/version.py +1 -0
- {datadog_lambda-6.105.0 → datadog_lambda-6.107.0}/datadog_lambda/wrapper.py +24 -12
- {datadog_lambda-6.105.0 → datadog_lambda-6.107.0}/pyproject.toml +4 -4
- datadog_lambda-6.105.0/datadog_lambda/api.py +0 -89
- datadog_lambda-6.105.0/datadog_lambda/version.py +0 -1
- {datadog_lambda-6.105.0 → datadog_lambda-6.107.0}/LICENSE +0 -0
- {datadog_lambda-6.105.0 → datadog_lambda-6.107.0}/LICENSE-3rdparty.csv +0 -0
- {datadog_lambda-6.105.0 → datadog_lambda-6.107.0}/NOTICE +0 -0
- {datadog_lambda-6.105.0 → datadog_lambda-6.107.0}/README.md +0 -0
- {datadog_lambda-6.105.0 → datadog_lambda-6.107.0}/datadog_lambda/__init__.py +0 -0
- {datadog_lambda-6.105.0 → datadog_lambda-6.107.0}/datadog_lambda/cold_start.py +0 -0
- {datadog_lambda-6.105.0 → datadog_lambda-6.107.0}/datadog_lambda/constants.py +0 -0
- {datadog_lambda-6.105.0 → datadog_lambda-6.107.0}/datadog_lambda/dogstatsd.py +0 -0
- {datadog_lambda-6.105.0 → datadog_lambda-6.107.0}/datadog_lambda/extension.py +0 -0
- {datadog_lambda-6.105.0 → datadog_lambda-6.107.0}/datadog_lambda/logger.py +0 -0
- {datadog_lambda-6.105.0 → datadog_lambda-6.107.0}/datadog_lambda/module_name.py +0 -0
- {datadog_lambda-6.105.0 → datadog_lambda-6.107.0}/datadog_lambda/patch.py +0 -0
- {datadog_lambda-6.105.0 → datadog_lambda-6.107.0}/datadog_lambda/stats_writer.py +0 -0
- {datadog_lambda-6.105.0 → datadog_lambda-6.107.0}/datadog_lambda/statsd_writer.py +0 -0
- {datadog_lambda-6.105.0 → datadog_lambda-6.107.0}/datadog_lambda/tag_object.py +0 -0
- {datadog_lambda-6.105.0 → datadog_lambda-6.107.0}/datadog_lambda/tags.py +0 -0
- {datadog_lambda-6.105.0 → datadog_lambda-6.107.0}/datadog_lambda/thread_stats_writer.py +0 -0
- {datadog_lambda-6.105.0 → datadog_lambda-6.107.0}/datadog_lambda/xray.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: datadog_lambda
|
|
3
|
-
Version: 6.
|
|
3
|
+
Version: 6.107.0
|
|
4
4
|
Summary: The Datadog AWS Lambda Library
|
|
5
5
|
Home-page: https://github.com/DataDog/datadog-lambda-python
|
|
6
6
|
License: Apache-2.0
|
|
@@ -17,9 +17,9 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
17
17
|
Classifier: Programming Language :: Python :: 3.12
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.13
|
|
19
19
|
Provides-Extra: dev
|
|
20
|
-
Requires-Dist:
|
|
20
|
+
Requires-Dist: botocore (>=1.34.0,<2.0.0) ; extra == "dev"
|
|
21
21
|
Requires-Dist: datadog (>=0.51.0,<1.0.0)
|
|
22
|
-
Requires-Dist: ddtrace (>=2.20.0)
|
|
22
|
+
Requires-Dist: ddtrace (>=2.20.0,<4)
|
|
23
23
|
Requires-Dist: flake8 (>=5.0.4,<6.0.0) ; extra == "dev"
|
|
24
24
|
Requires-Dist: pytest (>=8.0.0,<9.0.0) ; extra == "dev"
|
|
25
25
|
Requires-Dist: pytest-benchmark (>=4.0,<5.0) ; extra == "dev"
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import logging
|
|
3
|
+
|
|
4
|
+
logger = logging.getLogger(__name__)
|
|
5
|
+
KMS_ENCRYPTION_CONTEXT_KEY = "LambdaFunctionName"
|
|
6
|
+
api_key = None
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def decrypt_kms_api_key(kms_client, ciphertext):
|
|
10
|
+
from botocore.exceptions import ClientError
|
|
11
|
+
import base64
|
|
12
|
+
|
|
13
|
+
"""
|
|
14
|
+
Decodes and deciphers the base64-encoded ciphertext given as a parameter using KMS.
|
|
15
|
+
For this to work properly, the Lambda function must have the appropriate IAM permissions.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
kms_client: The KMS client to use for decryption
|
|
19
|
+
ciphertext (string): The base64-encoded ciphertext to decrypt
|
|
20
|
+
"""
|
|
21
|
+
decoded_bytes = base64.b64decode(ciphertext)
|
|
22
|
+
|
|
23
|
+
"""
|
|
24
|
+
When the API key is encrypted using the AWS console, the function name is added as an
|
|
25
|
+
encryption context. When the API key is encrypted using the AWS CLI, no encryption context
|
|
26
|
+
is added. We need to try decrypting the API key both with and without the encryption context.
|
|
27
|
+
"""
|
|
28
|
+
# Try without encryption context, in case API key was encrypted using the AWS CLI
|
|
29
|
+
function_name = os.environ.get("AWS_LAMBDA_FUNCTION_NAME")
|
|
30
|
+
try:
|
|
31
|
+
plaintext = kms_client.decrypt(CiphertextBlob=decoded_bytes)[
|
|
32
|
+
"Plaintext"
|
|
33
|
+
].decode("utf-8")
|
|
34
|
+
except ClientError:
|
|
35
|
+
logger.debug(
|
|
36
|
+
"Failed to decrypt ciphertext without encryption context, \
|
|
37
|
+
retrying with encryption context"
|
|
38
|
+
)
|
|
39
|
+
# Try with encryption context, in case API key was encrypted using the AWS Console
|
|
40
|
+
plaintext = kms_client.decrypt(
|
|
41
|
+
CiphertextBlob=decoded_bytes,
|
|
42
|
+
EncryptionContext={
|
|
43
|
+
KMS_ENCRYPTION_CONTEXT_KEY: function_name,
|
|
44
|
+
},
|
|
45
|
+
)["Plaintext"].decode("utf-8")
|
|
46
|
+
|
|
47
|
+
return plaintext
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def get_api_key() -> str:
|
|
51
|
+
"""
|
|
52
|
+
Gets the Datadog API key from the environment variables or secrets manager.
|
|
53
|
+
Extracts the result to a global value to avoid repeated calls to the
|
|
54
|
+
secrets manager from different products.
|
|
55
|
+
"""
|
|
56
|
+
global api_key
|
|
57
|
+
if api_key:
|
|
58
|
+
return api_key
|
|
59
|
+
|
|
60
|
+
DD_API_KEY_SECRET_ARN = os.environ.get("DD_API_KEY_SECRET_ARN", "")
|
|
61
|
+
DD_API_KEY_SSM_NAME = os.environ.get("DD_API_KEY_SSM_NAME", "")
|
|
62
|
+
DD_KMS_API_KEY = os.environ.get("DD_KMS_API_KEY", "")
|
|
63
|
+
DD_API_KEY = os.environ.get("DD_API_KEY", os.environ.get("DATADOG_API_KEY", ""))
|
|
64
|
+
|
|
65
|
+
LAMBDA_REGION = os.environ.get("AWS_REGION", "")
|
|
66
|
+
is_gov_region = LAMBDA_REGION.startswith("us-gov-")
|
|
67
|
+
if is_gov_region:
|
|
68
|
+
logger.debug(
|
|
69
|
+
"Govcloud region detected. Using FIPs endpoints for secrets management."
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
if DD_API_KEY_SECRET_ARN:
|
|
73
|
+
# Secrets manager endpoints: https://docs.aws.amazon.com/general/latest/gr/asm.html
|
|
74
|
+
try:
|
|
75
|
+
secrets_region = DD_API_KEY_SECRET_ARN.split(":")[3]
|
|
76
|
+
except Exception:
|
|
77
|
+
logger.debug(
|
|
78
|
+
"Invalid secret arn in DD_API_KEY_SECRET_ARN. Unable to get API key."
|
|
79
|
+
)
|
|
80
|
+
return ""
|
|
81
|
+
endpoint_url = (
|
|
82
|
+
f"https://secretsmanager-fips.{secrets_region}.amazonaws.com"
|
|
83
|
+
if is_gov_region
|
|
84
|
+
else None
|
|
85
|
+
)
|
|
86
|
+
secrets_manager_client = _boto3_client(
|
|
87
|
+
"secretsmanager", endpoint_url=endpoint_url, region_name=secrets_region
|
|
88
|
+
)
|
|
89
|
+
api_key = secrets_manager_client.get_secret_value(
|
|
90
|
+
SecretId=DD_API_KEY_SECRET_ARN
|
|
91
|
+
)["SecretString"]
|
|
92
|
+
elif DD_API_KEY_SSM_NAME:
|
|
93
|
+
# SSM endpoints: https://docs.aws.amazon.com/general/latest/gr/ssm.html
|
|
94
|
+
fips_endpoint = (
|
|
95
|
+
f"https://ssm-fips.{LAMBDA_REGION}.amazonaws.com" if is_gov_region else None
|
|
96
|
+
)
|
|
97
|
+
ssm_client = _boto3_client("ssm", endpoint_url=fips_endpoint)
|
|
98
|
+
api_key = ssm_client.get_parameter(
|
|
99
|
+
Name=DD_API_KEY_SSM_NAME, WithDecryption=True
|
|
100
|
+
)["Parameter"]["Value"]
|
|
101
|
+
elif DD_KMS_API_KEY:
|
|
102
|
+
# KMS endpoints: https://docs.aws.amazon.com/general/latest/gr/kms.html
|
|
103
|
+
fips_endpoint = (
|
|
104
|
+
f"https://kms-fips.{LAMBDA_REGION}.amazonaws.com" if is_gov_region else None
|
|
105
|
+
)
|
|
106
|
+
kms_client = _boto3_client("kms", endpoint_url=fips_endpoint)
|
|
107
|
+
api_key = decrypt_kms_api_key(kms_client, DD_KMS_API_KEY)
|
|
108
|
+
else:
|
|
109
|
+
api_key = DD_API_KEY
|
|
110
|
+
|
|
111
|
+
return api_key
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def init_api():
|
|
115
|
+
if not os.environ.get("DD_FLUSH_TO_LOG", "").lower() == "true":
|
|
116
|
+
# Make sure that this package would always be lazy-loaded/outside from the critical path
|
|
117
|
+
# since underlying packages are quite heavy to load
|
|
118
|
+
# and useless with the extension unless sending metrics with timestamps
|
|
119
|
+
from datadog import api
|
|
120
|
+
|
|
121
|
+
if not api._api_key:
|
|
122
|
+
api._api_key = get_api_key()
|
|
123
|
+
|
|
124
|
+
logger.debug("Setting DATADOG_API_KEY of length %d", len(api._api_key))
|
|
125
|
+
|
|
126
|
+
# Set DATADOG_HOST, to send data to a non-default Datadog datacenter
|
|
127
|
+
api._api_host = os.environ.get(
|
|
128
|
+
"DATADOG_HOST", "https://api." + os.environ.get("DD_SITE", "datadoghq.com")
|
|
129
|
+
)
|
|
130
|
+
logger.debug("Setting DATADOG_HOST to %s", api._api_host)
|
|
131
|
+
|
|
132
|
+
# Unmute exceptions from datadog api client, so we can catch and handle them
|
|
133
|
+
api._mute = False
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _boto3_client(*args, **kwargs):
|
|
137
|
+
import botocore.session
|
|
138
|
+
|
|
139
|
+
return botocore.session.get_session().create_client(*args, **kwargs)
|
|
@@ -55,6 +55,22 @@ def lambda_metric(metric_name, value, timestamp=None, tags=None, force_async=Fal
|
|
|
55
55
|
Note that if the extension is present, it will override the DD_FLUSH_TO_LOG value
|
|
56
56
|
and always use the layer to send metrics to the extension
|
|
57
57
|
"""
|
|
58
|
+
if not metric_name or not isinstance(metric_name, str):
|
|
59
|
+
logger.warning(
|
|
60
|
+
"Ignoring metric submission. Invalid metric name: %s", metric_name
|
|
61
|
+
)
|
|
62
|
+
return
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
float(value)
|
|
66
|
+
except (ValueError, TypeError):
|
|
67
|
+
logger.warning(
|
|
68
|
+
"Ignoring metric submission for metric '%s' because the value is not numeric: %r",
|
|
69
|
+
metric_name,
|
|
70
|
+
value,
|
|
71
|
+
)
|
|
72
|
+
return
|
|
73
|
+
|
|
58
74
|
flush_to_logs = os.environ.get("DD_FLUSH_TO_LOG", "").lower() == "true"
|
|
59
75
|
tags = [] if tags is None else list(tags)
|
|
60
76
|
tags.append(dd_lambda_layer_tag)
|
|
@@ -172,3 +188,17 @@ def submit_errors_metric(lambda_context):
|
|
|
172
188
|
lambda_context (object): Lambda context dict passed to the function by AWS
|
|
173
189
|
"""
|
|
174
190
|
submit_enhanced_metric("errors", lambda_context)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def submit_dynamodb_stream_type_metric(event):
|
|
194
|
+
stream_view_type = (
|
|
195
|
+
event.get("Records", [{}])[0].get("dynamodb", {}).get("StreamViewType")
|
|
196
|
+
)
|
|
197
|
+
if stream_view_type:
|
|
198
|
+
lambda_metric(
|
|
199
|
+
"datadog.serverless.dynamodb.stream.type",
|
|
200
|
+
1,
|
|
201
|
+
timestamp=None,
|
|
202
|
+
tags=[f"streamtype:{stream_view_type}"],
|
|
203
|
+
force_async=True,
|
|
204
|
+
)
|
|
@@ -6,6 +6,8 @@ from typing import Optional
|
|
|
6
6
|
|
|
7
7
|
from ddtrace._trace._span_pointer import _SpanPointerDirection
|
|
8
8
|
from ddtrace._trace._span_pointer import _SpanPointerDescription
|
|
9
|
+
|
|
10
|
+
from datadog_lambda.metric import submit_dynamodb_stream_type_metric
|
|
9
11
|
from datadog_lambda.trigger import EventTypes
|
|
10
12
|
|
|
11
13
|
|
|
@@ -28,6 +30,8 @@ def calculate_span_pointers(
|
|
|
28
30
|
return _calculate_s3_span_pointers_for_event(event)
|
|
29
31
|
|
|
30
32
|
elif event_source.equals(EventTypes.DYNAMODB):
|
|
33
|
+
# Temporary metric. TODO eventually remove(@nhulston)
|
|
34
|
+
submit_dynamodb_stream_type_metric(event)
|
|
31
35
|
return _calculate_dynamodb_span_pointers_for_event(event)
|
|
32
36
|
|
|
33
37
|
except Exception as e:
|
|
@@ -2,10 +2,8 @@
|
|
|
2
2
|
# under the Apache License Version 2.0.
|
|
3
3
|
# This product includes software developed at Datadog (https://www.datadoghq.com/).
|
|
4
4
|
# Copyright 2019 Datadog, Inc.
|
|
5
|
-
import hashlib
|
|
6
5
|
import logging
|
|
7
6
|
import os
|
|
8
|
-
import base64
|
|
9
7
|
import traceback
|
|
10
8
|
import ujson as json
|
|
11
9
|
from datetime import datetime, timezone
|
|
@@ -39,6 +37,7 @@ from datadog_lambda.trigger import (
|
|
|
39
37
|
_EventSource,
|
|
40
38
|
parse_event_source,
|
|
41
39
|
get_first_record,
|
|
40
|
+
is_step_function_event,
|
|
42
41
|
EventTypes,
|
|
43
42
|
EventSubtypes,
|
|
44
43
|
)
|
|
@@ -258,6 +257,8 @@ def extract_context_from_sqs_or_sns_event_or_context(event, lambda_context):
|
|
|
258
257
|
dd_json_data = None
|
|
259
258
|
dd_json_data_type = dd_payload.get("Type") or dd_payload.get("dataType")
|
|
260
259
|
if dd_json_data_type == "Binary":
|
|
260
|
+
import base64
|
|
261
|
+
|
|
261
262
|
dd_json_data = dd_payload.get("binaryValue") or dd_payload.get("Value")
|
|
262
263
|
if dd_json_data:
|
|
263
264
|
dd_json_data = base64.b64decode(dd_json_data)
|
|
@@ -271,6 +272,15 @@ def extract_context_from_sqs_or_sns_event_or_context(event, lambda_context):
|
|
|
271
272
|
|
|
272
273
|
if dd_json_data:
|
|
273
274
|
dd_data = json.loads(dd_json_data)
|
|
275
|
+
|
|
276
|
+
if is_step_function_event(dd_data):
|
|
277
|
+
try:
|
|
278
|
+
return extract_context_from_step_functions(dd_data, None)
|
|
279
|
+
except Exception:
|
|
280
|
+
logger.debug(
|
|
281
|
+
"Failed to extract Step Functions context from SQS/SNS event."
|
|
282
|
+
)
|
|
283
|
+
|
|
274
284
|
return propagator.extract(dd_data)
|
|
275
285
|
else:
|
|
276
286
|
# Handle case where trace context is injected into attributes.AWSTraceHeader
|
|
@@ -313,6 +323,15 @@ def _extract_context_from_eventbridge_sqs_event(event):
|
|
|
313
323
|
body = json.loads(body_str)
|
|
314
324
|
detail = body.get("detail")
|
|
315
325
|
dd_context = detail.get("_datadog")
|
|
326
|
+
|
|
327
|
+
if is_step_function_event(dd_context):
|
|
328
|
+
try:
|
|
329
|
+
return extract_context_from_step_functions(dd_context, None)
|
|
330
|
+
except Exception:
|
|
331
|
+
logger.debug(
|
|
332
|
+
"Failed to extract Step Functions context from EventBridge to SQS event."
|
|
333
|
+
)
|
|
334
|
+
|
|
316
335
|
return propagator.extract(dd_context)
|
|
317
336
|
|
|
318
337
|
|
|
@@ -320,12 +339,23 @@ def extract_context_from_eventbridge_event(event, lambda_context):
|
|
|
320
339
|
"""
|
|
321
340
|
Extract datadog trace context from an EventBridge message's Details.
|
|
322
341
|
This is only possible if Details is a JSON string.
|
|
342
|
+
|
|
343
|
+
If we find a Step Function context, try to extract the trace context from
|
|
344
|
+
that header.
|
|
323
345
|
"""
|
|
324
346
|
try:
|
|
325
347
|
detail = event.get("detail")
|
|
326
348
|
dd_context = detail.get("_datadog")
|
|
327
349
|
if not dd_context:
|
|
328
350
|
return extract_context_from_lambda_context(lambda_context)
|
|
351
|
+
|
|
352
|
+
try:
|
|
353
|
+
return extract_context_from_step_functions(dd_context, None)
|
|
354
|
+
except Exception:
|
|
355
|
+
logger.debug(
|
|
356
|
+
"Failed to extract Step Functions context from EventBridge event."
|
|
357
|
+
)
|
|
358
|
+
|
|
329
359
|
return propagator.extract(dd_context)
|
|
330
360
|
except Exception as e:
|
|
331
361
|
logger.debug("The trace extractor returned with error %s", e)
|
|
@@ -343,6 +373,8 @@ def extract_context_from_kinesis_event(event, lambda_context):
|
|
|
343
373
|
return extract_context_from_lambda_context(lambda_context)
|
|
344
374
|
data = kinesis.get("data")
|
|
345
375
|
if data:
|
|
376
|
+
import base64
|
|
377
|
+
|
|
346
378
|
b64_bytes = data.encode("ascii")
|
|
347
379
|
str_bytes = base64.b64decode(b64_bytes)
|
|
348
380
|
data_str = str_bytes.decode("ascii")
|
|
@@ -357,6 +389,8 @@ def extract_context_from_kinesis_event(event, lambda_context):
|
|
|
357
389
|
|
|
358
390
|
|
|
359
391
|
def _deterministic_sha256_hash(s: str, part: str) -> int:
|
|
392
|
+
import hashlib
|
|
393
|
+
|
|
360
394
|
sha256_hash = hashlib.sha256(s.encode()).hexdigest()
|
|
361
395
|
# First two chars is '0b'. zfill to ensure 256 bits, but we only care about the first 128 bits
|
|
362
396
|
binary_hash = bin(int(sha256_hash, 16))[2:].zfill(256)
|
|
@@ -385,21 +419,24 @@ def _parse_high_64_bits(trace_tags: str) -> str:
|
|
|
385
419
|
|
|
386
420
|
def _generate_sfn_parent_id(context: dict) -> int:
|
|
387
421
|
"""
|
|
388
|
-
|
|
389
|
-
|
|
422
|
+
Generates a stable parent span ID for a downstream Lambda invoked by a Step Function. The
|
|
423
|
+
upstream Step Function execution context is used to infer the parent's span ID, ensuring trace
|
|
424
|
+
continuity.
|
|
390
425
|
|
|
391
|
-
|
|
392
|
-
|
|
426
|
+
`RetryCount` and `RedriveCount` are appended only when both are nonzero to maintain
|
|
427
|
+
compatibility with older Lambda layers that did not include these fields.
|
|
393
428
|
"""
|
|
394
429
|
execution_id = context.get("Execution").get("Id")
|
|
395
430
|
redrive_count = context.get("Execution").get("RedriveCount", 0)
|
|
396
431
|
state_name = context.get("State").get("Name")
|
|
397
432
|
state_entered_time = context.get("State").get("EnteredTime")
|
|
433
|
+
retry_count = context.get("State").get("RetryCount", 0)
|
|
398
434
|
|
|
399
|
-
|
|
435
|
+
include_counts = not (retry_count == 0 and redrive_count == 0)
|
|
436
|
+
counts_suffix = f"#{retry_count}#{redrive_count}" if include_counts else ""
|
|
400
437
|
|
|
401
438
|
return _deterministic_sha256_hash(
|
|
402
|
-
f"{execution_id}#{state_name}#{state_entered_time}{
|
|
439
|
+
f"{execution_id}#{state_name}#{state_entered_time}{counts_suffix}",
|
|
403
440
|
HIGHER_64_BITS,
|
|
404
441
|
)
|
|
405
442
|
|
|
@@ -421,7 +458,7 @@ def _generate_sfn_trace_id(execution_id: str, part: str):
|
|
|
421
458
|
def extract_context_from_step_functions(event, lambda_context):
|
|
422
459
|
"""
|
|
423
460
|
Only extract datadog trace context when Step Functions Context Object is injected
|
|
424
|
-
into lambda's event dict.
|
|
461
|
+
into lambda's event dict. Unwrap "Payload" if it exists to handle Legacy Lambda cases.
|
|
425
462
|
|
|
426
463
|
If '_datadog' header is present, we have two cases:
|
|
427
464
|
1. Root is a Lambda and we use its traceID
|
|
@@ -432,25 +469,25 @@ def extract_context_from_step_functions(event, lambda_context):
|
|
|
432
469
|
object.
|
|
433
470
|
"""
|
|
434
471
|
try:
|
|
472
|
+
event = event.get("Payload", event)
|
|
473
|
+
event = event.get("_datadog", event)
|
|
474
|
+
|
|
435
475
|
meta = {}
|
|
436
|
-
dd_data = event.get("_datadog")
|
|
437
476
|
|
|
438
|
-
if
|
|
439
|
-
if "x-datadog-trace-id" in
|
|
440
|
-
trace_id = int(
|
|
441
|
-
high_64_bit_trace_id = _parse_high_64_bits(
|
|
442
|
-
dd_data.get("x-datadog-tags")
|
|
443
|
-
)
|
|
477
|
+
if event.get("serverless-version") == "v1":
|
|
478
|
+
if "x-datadog-trace-id" in event: # lambda root
|
|
479
|
+
trace_id = int(event.get("x-datadog-trace-id"))
|
|
480
|
+
high_64_bit_trace_id = _parse_high_64_bits(event.get("x-datadog-tags"))
|
|
444
481
|
if high_64_bit_trace_id:
|
|
445
482
|
meta["_dd.p.tid"] = high_64_bit_trace_id
|
|
446
483
|
else: # sfn root
|
|
447
|
-
root_execution_id =
|
|
484
|
+
root_execution_id = event.get("RootExecutionId")
|
|
448
485
|
trace_id = _generate_sfn_trace_id(root_execution_id, LOWER_64_BITS)
|
|
449
486
|
meta["_dd.p.tid"] = _generate_sfn_trace_id(
|
|
450
487
|
root_execution_id, HIGHER_64_BITS
|
|
451
488
|
)
|
|
452
489
|
|
|
453
|
-
parent_id = _generate_sfn_parent_id(
|
|
490
|
+
parent_id = _generate_sfn_parent_id(event)
|
|
454
491
|
else:
|
|
455
492
|
execution_id = event.get("Execution").get("Id")
|
|
456
493
|
trace_id = _generate_sfn_trace_id(execution_id, LOWER_64_BITS)
|
|
@@ -469,20 +506,6 @@ def extract_context_from_step_functions(event, lambda_context):
|
|
|
469
506
|
return extract_context_from_lambda_context(lambda_context)
|
|
470
507
|
|
|
471
508
|
|
|
472
|
-
def is_legacy_lambda_step_function(event):
|
|
473
|
-
"""
|
|
474
|
-
Check if the event is a step function that called a legacy lambda
|
|
475
|
-
"""
|
|
476
|
-
if not isinstance(event, dict) or "Payload" not in event:
|
|
477
|
-
return False
|
|
478
|
-
|
|
479
|
-
event = event.get("Payload")
|
|
480
|
-
return isinstance(event, dict) and (
|
|
481
|
-
"_datadog" in event
|
|
482
|
-
or ("Execution" in event and "StateMachine" in event and "State" in event)
|
|
483
|
-
)
|
|
484
|
-
|
|
485
|
-
|
|
486
509
|
def extract_context_custom_extractor(extractor, event, lambda_context):
|
|
487
510
|
"""
|
|
488
511
|
Extract Datadog trace context using a custom trace extractor function
|
|
@@ -532,6 +555,8 @@ def get_injected_authorizer_data(event, is_http_api) -> dict:
|
|
|
532
555
|
if not dd_data_raw:
|
|
533
556
|
return None
|
|
534
557
|
|
|
558
|
+
import base64
|
|
559
|
+
|
|
535
560
|
injected_data = json.loads(base64.b64decode(dd_data_raw))
|
|
536
561
|
|
|
537
562
|
# Lambda authorizer's results can be cached. But the payload will still have the injected
|
|
@@ -1306,8 +1331,18 @@ def create_inferred_span_from_eventbridge_event(event, context):
|
|
|
1306
1331
|
synchronicity="async",
|
|
1307
1332
|
tag_source="self",
|
|
1308
1333
|
)
|
|
1309
|
-
|
|
1334
|
+
|
|
1310
1335
|
timestamp = event.get("time")
|
|
1336
|
+
dt_format = "%Y-%m-%dT%H:%M:%SZ"
|
|
1337
|
+
|
|
1338
|
+
# Use more granular timestamp from upstream Step Function if possible
|
|
1339
|
+
try:
|
|
1340
|
+
if is_step_function_event(event.get("detail")):
|
|
1341
|
+
timestamp = event["detail"]["_datadog"]["State"]["EnteredTime"]
|
|
1342
|
+
dt_format = "%Y-%m-%dT%H:%M:%S.%fZ"
|
|
1343
|
+
except (TypeError, KeyError, AttributeError):
|
|
1344
|
+
logger.debug("Error parsing timestamp from Step Functions event")
|
|
1345
|
+
|
|
1311
1346
|
dt = datetime.strptime(timestamp, dt_format)
|
|
1312
1347
|
|
|
1313
1348
|
tracer.set_tags(_dd_origin)
|
|
@@ -1317,6 +1352,11 @@ def create_inferred_span_from_eventbridge_event(event, context):
|
|
|
1317
1352
|
if span:
|
|
1318
1353
|
span.set_tags(tags)
|
|
1319
1354
|
span.start = dt.replace(tzinfo=timezone.utc).timestamp()
|
|
1355
|
+
|
|
1356
|
+
# Since inferred span will later parent Lambda, preserve Lambda's current parent
|
|
1357
|
+
if dd_trace_context.span_id:
|
|
1358
|
+
span.parent_id = dd_trace_context.span_id
|
|
1359
|
+
|
|
1320
1360
|
return span
|
|
1321
1361
|
|
|
1322
1362
|
|
|
@@ -1368,8 +1408,9 @@ def create_function_execution_span(
|
|
|
1368
1408
|
if parent_span:
|
|
1369
1409
|
span.parent_id = parent_span.span_id
|
|
1370
1410
|
if span_pointers:
|
|
1411
|
+
root_span = parent_span if parent_span else span
|
|
1371
1412
|
for span_pointer_description in span_pointers:
|
|
1372
|
-
|
|
1413
|
+
root_span._add_span_pointer(
|
|
1373
1414
|
pointer_kind=span_pointer_description.pointer_kind,
|
|
1374
1415
|
pointer_direction=span_pointer_description.pointer_direction,
|
|
1375
1416
|
pointer_hash=span_pointer_description.pointer_hash,
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
# This product includes software developed at Datadog (https://www.datadoghq.com/).
|
|
4
4
|
# Copyright 2019 Datadog, Inc.
|
|
5
5
|
|
|
6
|
-
import base64
|
|
7
6
|
import gzip
|
|
8
7
|
import ujson as json
|
|
9
8
|
from io import BytesIO, BufferedReader
|
|
@@ -146,9 +145,7 @@ def parse_event_source(event: dict) -> _EventSource:
|
|
|
146
145
|
if event.get("source") == "aws.events" or has_event_categories:
|
|
147
146
|
event_source = _EventSource(EventTypes.CLOUDWATCH_EVENTS)
|
|
148
147
|
|
|
149
|
-
if (
|
|
150
|
-
"_datadog" in event and event.get("_datadog").get("serverless-version") == "v1"
|
|
151
|
-
) or ("Execution" in event and "StateMachine" in event and "State" in event):
|
|
148
|
+
if is_step_function_event(event):
|
|
152
149
|
event_source = _EventSource(EventTypes.STEPFUNCTIONS)
|
|
153
150
|
|
|
154
151
|
event_record = get_first_record(event)
|
|
@@ -244,6 +241,8 @@ def parse_event_source_arn(source: _EventSource, event: dict, context: Any) -> s
|
|
|
244
241
|
|
|
245
242
|
# e.g. arn:aws:logs:us-west-1:123456789012:log-group:/my-log-group-xyz
|
|
246
243
|
if source.event_type == EventTypes.CLOUDWATCH_LOGS:
|
|
244
|
+
import base64
|
|
245
|
+
|
|
247
246
|
with gzip.GzipFile(
|
|
248
247
|
fileobj=BytesIO(base64.b64decode(event.get("awslogs", {}).get("data")))
|
|
249
248
|
) as decompress_stream:
|
|
@@ -369,3 +368,29 @@ def extract_http_status_code_tag(trigger_tags, response):
|
|
|
369
368
|
status_code = response.status_code
|
|
370
369
|
|
|
371
370
|
return str(status_code)
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def is_step_function_event(event):
|
|
374
|
+
"""
|
|
375
|
+
Check if the event is a step function that invoked the current lambda.
|
|
376
|
+
|
|
377
|
+
The whole event can be wrapped in "Payload" in Legacy Lambda cases. There may also be a
|
|
378
|
+
"_datadog" for JSONata style context propagation.
|
|
379
|
+
|
|
380
|
+
The actual event must contain "Execution", "StateMachine", and "State" fields.
|
|
381
|
+
"""
|
|
382
|
+
event = event.get("Payload", event)
|
|
383
|
+
|
|
384
|
+
# JSONPath style
|
|
385
|
+
if "Execution" in event and "StateMachine" in event and "State" in event:
|
|
386
|
+
return True
|
|
387
|
+
|
|
388
|
+
# JSONata style
|
|
389
|
+
dd_context = event.get("_datadog")
|
|
390
|
+
return (
|
|
391
|
+
dd_context
|
|
392
|
+
and "Execution" in dd_context
|
|
393
|
+
and "StateMachine" in dd_context
|
|
394
|
+
and "State" in dd_context
|
|
395
|
+
and "serverless-version" in dd_context
|
|
396
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "6.107.0"
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
# under the Apache License Version 2.0.
|
|
3
3
|
# This product includes software developed at Datadog (https://www.datadoghq.com/).
|
|
4
4
|
# Copyright 2019 Datadog, Inc.
|
|
5
|
-
import base64
|
|
6
5
|
import os
|
|
7
6
|
import logging
|
|
8
7
|
import traceback
|
|
@@ -23,11 +22,6 @@ from datadog_lambda.constants import (
|
|
|
23
22
|
XraySubsegment,
|
|
24
23
|
Headers,
|
|
25
24
|
)
|
|
26
|
-
from datadog_lambda.metric import (
|
|
27
|
-
flush_stats,
|
|
28
|
-
submit_invocations_metric,
|
|
29
|
-
submit_errors_metric,
|
|
30
|
-
)
|
|
31
25
|
from datadog_lambda.module_name import modify_module_name
|
|
32
26
|
from datadog_lambda.patch import patch_all
|
|
33
27
|
from datadog_lambda.span_pointers import calculate_span_pointers
|
|
@@ -45,7 +39,6 @@ from datadog_lambda.tracing import (
|
|
|
45
39
|
is_authorizer_response,
|
|
46
40
|
tracer,
|
|
47
41
|
propagator,
|
|
48
|
-
is_legacy_lambda_step_function,
|
|
49
42
|
)
|
|
50
43
|
from datadog_lambda.trigger import (
|
|
51
44
|
extract_trigger_tags,
|
|
@@ -56,10 +49,14 @@ profiling_env_var = os.environ.get("DD_PROFILING_ENABLED", "false").lower() == "
|
|
|
56
49
|
if profiling_env_var:
|
|
57
50
|
from ddtrace.profiling import profiler
|
|
58
51
|
|
|
52
|
+
llmobs_api_key = None
|
|
59
53
|
llmobs_env_var = os.environ.get("DD_LLMOBS_ENABLED", "false").lower() in ("true", "1")
|
|
60
54
|
if llmobs_env_var:
|
|
55
|
+
from datadog_lambda.api import get_api_key
|
|
61
56
|
from ddtrace.llmobs import LLMObs
|
|
62
57
|
|
|
58
|
+
llmobs_api_key = get_api_key()
|
|
59
|
+
|
|
63
60
|
logger = logging.getLogger(__name__)
|
|
64
61
|
|
|
65
62
|
DD_FLUSH_TO_LOG = "DD_FLUSH_TO_LOG"
|
|
@@ -229,7 +226,10 @@ class _LambdaDecorator(object):
|
|
|
229
226
|
|
|
230
227
|
# Enable LLM Observability
|
|
231
228
|
if llmobs_env_var:
|
|
232
|
-
LLMObs.enable(
|
|
229
|
+
LLMObs.enable(
|
|
230
|
+
agentless_enabled=True,
|
|
231
|
+
api_key=llmobs_api_key,
|
|
232
|
+
)
|
|
233
233
|
|
|
234
234
|
logger.debug("datadog_lambda_wrapper initialized")
|
|
235
235
|
except Exception as e:
|
|
@@ -242,7 +242,11 @@ class _LambdaDecorator(object):
|
|
|
242
242
|
self.response = self.func(event, context, **kwargs)
|
|
243
243
|
return self.response
|
|
244
244
|
except Exception:
|
|
245
|
-
|
|
245
|
+
if not should_use_extension:
|
|
246
|
+
from datadog_lambda.metric import submit_errors_metric
|
|
247
|
+
|
|
248
|
+
submit_errors_metric(context)
|
|
249
|
+
|
|
246
250
|
if self.span:
|
|
247
251
|
self.span.set_traceback()
|
|
248
252
|
raise
|
|
@@ -268,6 +272,9 @@ class _LambdaDecorator(object):
|
|
|
268
272
|
injected_headers[Headers.Parent_Span_Finish_Time] = finish_time_ns
|
|
269
273
|
if request_id is not None:
|
|
270
274
|
injected_headers[Headers.Authorizing_Request_Id] = request_id
|
|
275
|
+
|
|
276
|
+
import base64
|
|
277
|
+
|
|
271
278
|
datadog_data = base64.b64encode(
|
|
272
279
|
json.dumps(injected_headers, escape_forward_slashes=False).encode()
|
|
273
280
|
).decode()
|
|
@@ -278,9 +285,12 @@ class _LambdaDecorator(object):
|
|
|
278
285
|
try:
|
|
279
286
|
self.response = None
|
|
280
287
|
set_cold_start(init_timestamp_ns)
|
|
281
|
-
|
|
282
|
-
if
|
|
283
|
-
|
|
288
|
+
|
|
289
|
+
if not should_use_extension:
|
|
290
|
+
from datadog_lambda.metric import submit_invocations_metric
|
|
291
|
+
|
|
292
|
+
submit_invocations_metric(context)
|
|
293
|
+
|
|
284
294
|
self.trigger_tags = extract_trigger_tags(event, context)
|
|
285
295
|
# Extract Datadog trace context and source from incoming requests
|
|
286
296
|
dd_context, trace_context_source, event_source = extract_dd_trace_context(
|
|
@@ -379,6 +389,8 @@ class _LambdaDecorator(object):
|
|
|
379
389
|
logger.debug("Failed to create cold start spans. %s", e)
|
|
380
390
|
|
|
381
391
|
if not self.flush_to_log or should_use_extension:
|
|
392
|
+
from datadog_lambda.metric import flush_stats
|
|
393
|
+
|
|
382
394
|
flush_stats(context)
|
|
383
395
|
if should_use_extension and self.local_testing_mode:
|
|
384
396
|
# when testing locally, the extension does not know when an
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "datadog_lambda"
|
|
3
|
-
version = "6.
|
|
3
|
+
version = "6.107.0"
|
|
4
4
|
description = "The Datadog AWS Lambda Library"
|
|
5
5
|
authors = ["Datadog, Inc. <dev@datadoghq.com>"]
|
|
6
6
|
license = "Apache-2.0"
|
|
@@ -28,9 +28,9 @@ classifiers = [
|
|
|
28
28
|
python = ">=3.8.0,<4"
|
|
29
29
|
datadog = ">=0.51.0,<1.0.0"
|
|
30
30
|
wrapt = "^1.11.2"
|
|
31
|
-
ddtrace = ">=2.20.0"
|
|
31
|
+
ddtrace = ">=2.20.0,<4"
|
|
32
32
|
ujson = ">=5.9.0"
|
|
33
|
-
|
|
33
|
+
botocore = { version = "^1.34.0", optional = true }
|
|
34
34
|
requests = { version ="^2.22.0", optional = true }
|
|
35
35
|
pytest = { version= "^8.0.0", optional = true }
|
|
36
36
|
pytest-benchmark = { version = "^4.0", optional = true }
|
|
@@ -38,7 +38,7 @@ flake8 = { version = "^5.0.4", optional = true }
|
|
|
38
38
|
|
|
39
39
|
[tool.poetry.extras]
|
|
40
40
|
dev = [
|
|
41
|
-
"
|
|
41
|
+
"botocore",
|
|
42
42
|
"flake8",
|
|
43
43
|
"pytest",
|
|
44
44
|
"pytest-benchmark",
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import logging
|
|
3
|
-
import base64
|
|
4
|
-
|
|
5
|
-
logger = logging.getLogger(__name__)
|
|
6
|
-
KMS_ENCRYPTION_CONTEXT_KEY = "LambdaFunctionName"
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def decrypt_kms_api_key(kms_client, ciphertext):
|
|
10
|
-
from botocore.exceptions import ClientError
|
|
11
|
-
|
|
12
|
-
"""
|
|
13
|
-
Decodes and deciphers the base64-encoded ciphertext given as a parameter using KMS.
|
|
14
|
-
For this to work properly, the Lambda function must have the appropriate IAM permissions.
|
|
15
|
-
|
|
16
|
-
Args:
|
|
17
|
-
kms_client: The KMS client to use for decryption
|
|
18
|
-
ciphertext (string): The base64-encoded ciphertext to decrypt
|
|
19
|
-
"""
|
|
20
|
-
decoded_bytes = base64.b64decode(ciphertext)
|
|
21
|
-
|
|
22
|
-
"""
|
|
23
|
-
When the API key is encrypted using the AWS console, the function name is added as an
|
|
24
|
-
encryption context. When the API key is encrypted using the AWS CLI, no encryption context
|
|
25
|
-
is added. We need to try decrypting the API key both with and without the encryption context.
|
|
26
|
-
"""
|
|
27
|
-
# Try without encryption context, in case API key was encrypted using the AWS CLI
|
|
28
|
-
function_name = os.environ.get("AWS_LAMBDA_FUNCTION_NAME")
|
|
29
|
-
try:
|
|
30
|
-
plaintext = kms_client.decrypt(CiphertextBlob=decoded_bytes)[
|
|
31
|
-
"Plaintext"
|
|
32
|
-
].decode("utf-8")
|
|
33
|
-
except ClientError:
|
|
34
|
-
logger.debug(
|
|
35
|
-
"Failed to decrypt ciphertext without encryption context, \
|
|
36
|
-
retrying with encryption context"
|
|
37
|
-
)
|
|
38
|
-
# Try with encryption context, in case API key was encrypted using the AWS Console
|
|
39
|
-
plaintext = kms_client.decrypt(
|
|
40
|
-
CiphertextBlob=decoded_bytes,
|
|
41
|
-
EncryptionContext={
|
|
42
|
-
KMS_ENCRYPTION_CONTEXT_KEY: function_name,
|
|
43
|
-
},
|
|
44
|
-
)["Plaintext"].decode("utf-8")
|
|
45
|
-
|
|
46
|
-
return plaintext
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
def init_api():
|
|
50
|
-
if not os.environ.get("DD_FLUSH_TO_LOG", "").lower() == "true":
|
|
51
|
-
# Make sure that this package would always be lazy-loaded/outside from the critical path
|
|
52
|
-
# since underlying packages are quite heavy to load
|
|
53
|
-
# and useless with the extension unless sending metrics with timestamps
|
|
54
|
-
from datadog import api
|
|
55
|
-
|
|
56
|
-
if not api._api_key:
|
|
57
|
-
import boto3
|
|
58
|
-
|
|
59
|
-
DD_API_KEY_SECRET_ARN = os.environ.get("DD_API_KEY_SECRET_ARN", "")
|
|
60
|
-
DD_API_KEY_SSM_NAME = os.environ.get("DD_API_KEY_SSM_NAME", "")
|
|
61
|
-
DD_KMS_API_KEY = os.environ.get("DD_KMS_API_KEY", "")
|
|
62
|
-
DD_API_KEY = os.environ.get(
|
|
63
|
-
"DD_API_KEY", os.environ.get("DATADOG_API_KEY", "")
|
|
64
|
-
)
|
|
65
|
-
|
|
66
|
-
if DD_API_KEY_SECRET_ARN:
|
|
67
|
-
api._api_key = boto3.client("secretsmanager").get_secret_value(
|
|
68
|
-
SecretId=DD_API_KEY_SECRET_ARN
|
|
69
|
-
)["SecretString"]
|
|
70
|
-
elif DD_API_KEY_SSM_NAME:
|
|
71
|
-
api._api_key = boto3.client("ssm").get_parameter(
|
|
72
|
-
Name=DD_API_KEY_SSM_NAME, WithDecryption=True
|
|
73
|
-
)["Parameter"]["Value"]
|
|
74
|
-
elif DD_KMS_API_KEY:
|
|
75
|
-
kms_client = boto3.client("kms")
|
|
76
|
-
api._api_key = decrypt_kms_api_key(kms_client, DD_KMS_API_KEY)
|
|
77
|
-
else:
|
|
78
|
-
api._api_key = DD_API_KEY
|
|
79
|
-
|
|
80
|
-
logger.debug("Setting DATADOG_API_KEY of length %d", len(api._api_key))
|
|
81
|
-
|
|
82
|
-
# Set DATADOG_HOST, to send data to a non-default Datadog datacenter
|
|
83
|
-
api._api_host = os.environ.get(
|
|
84
|
-
"DATADOG_HOST", "https://api." + os.environ.get("DD_SITE", "datadoghq.com")
|
|
85
|
-
)
|
|
86
|
-
logger.debug("Setting DATADOG_HOST to %s", api._api_host)
|
|
87
|
-
|
|
88
|
-
# Unmute exceptions from datadog api client, so we can catch and handle them
|
|
89
|
-
api._mute = False
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "6.105.0"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|