datadog_lambda 6.106.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.106.0 → datadog_lambda-6.107.0}/PKG-INFO +3 -3
- datadog_lambda-6.107.0/datadog_lambda/api.py +139 -0
- {datadog_lambda-6.106.0 → datadog_lambda-6.107.0}/datadog_lambda/handler.py +0 -1
- {datadog_lambda-6.106.0 → datadog_lambda-6.107.0}/datadog_lambda/metric.py +14 -0
- {datadog_lambda-6.106.0 → datadog_lambda-6.107.0}/datadog_lambda/span_pointers.py +4 -0
- {datadog_lambda-6.106.0 → datadog_lambda-6.107.0}/datadog_lambda/tracing.py +64 -27
- {datadog_lambda-6.106.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.106.0 → datadog_lambda-6.107.0}/datadog_lambda/wrapper.py +24 -12
- {datadog_lambda-6.106.0 → datadog_lambda-6.107.0}/pyproject.toml +4 -4
- datadog_lambda-6.106.0/datadog_lambda/api.py +0 -89
- datadog_lambda-6.106.0/datadog_lambda/version.py +0 -1
- {datadog_lambda-6.106.0 → datadog_lambda-6.107.0}/LICENSE +0 -0
- {datadog_lambda-6.106.0 → datadog_lambda-6.107.0}/LICENSE-3rdparty.csv +0 -0
- {datadog_lambda-6.106.0 → datadog_lambda-6.107.0}/NOTICE +0 -0
- {datadog_lambda-6.106.0 → datadog_lambda-6.107.0}/README.md +0 -0
- {datadog_lambda-6.106.0 → datadog_lambda-6.107.0}/datadog_lambda/__init__.py +0 -0
- {datadog_lambda-6.106.0 → datadog_lambda-6.107.0}/datadog_lambda/cold_start.py +0 -0
- {datadog_lambda-6.106.0 → datadog_lambda-6.107.0}/datadog_lambda/constants.py +0 -0
- {datadog_lambda-6.106.0 → datadog_lambda-6.107.0}/datadog_lambda/dogstatsd.py +0 -0
- {datadog_lambda-6.106.0 → datadog_lambda-6.107.0}/datadog_lambda/extension.py +0 -0
- {datadog_lambda-6.106.0 → datadog_lambda-6.107.0}/datadog_lambda/logger.py +0 -0
- {datadog_lambda-6.106.0 → datadog_lambda-6.107.0}/datadog_lambda/module_name.py +0 -0
- {datadog_lambda-6.106.0 → datadog_lambda-6.107.0}/datadog_lambda/patch.py +0 -0
- {datadog_lambda-6.106.0 → datadog_lambda-6.107.0}/datadog_lambda/stats_writer.py +0 -0
- {datadog_lambda-6.106.0 → datadog_lambda-6.107.0}/datadog_lambda/statsd_writer.py +0 -0
- {datadog_lambda-6.106.0 → datadog_lambda-6.107.0}/datadog_lambda/tag_object.py +0 -0
- {datadog_lambda-6.106.0 → datadog_lambda-6.107.0}/datadog_lambda/tags.py +0 -0
- {datadog_lambda-6.106.0 → datadog_lambda-6.107.0}/datadog_lambda/thread_stats_writer.py +0 -0
- {datadog_lambda-6.106.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)
|
|
@@ -188,3 +188,17 @@ def submit_errors_metric(lambda_context):
|
|
|
188
188
|
lambda_context (object): Lambda context dict passed to the function by AWS
|
|
189
189
|
"""
|
|
190
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)
|
|
@@ -424,7 +458,7 @@ def _generate_sfn_trace_id(execution_id: str, part: str):
|
|
|
424
458
|
def extract_context_from_step_functions(event, lambda_context):
|
|
425
459
|
"""
|
|
426
460
|
Only extract datadog trace context when Step Functions Context Object is injected
|
|
427
|
-
into lambda's event dict.
|
|
461
|
+
into lambda's event dict. Unwrap "Payload" if it exists to handle Legacy Lambda cases.
|
|
428
462
|
|
|
429
463
|
If '_datadog' header is present, we have two cases:
|
|
430
464
|
1. Root is a Lambda and we use its traceID
|
|
@@ -435,25 +469,25 @@ def extract_context_from_step_functions(event, lambda_context):
|
|
|
435
469
|
object.
|
|
436
470
|
"""
|
|
437
471
|
try:
|
|
472
|
+
event = event.get("Payload", event)
|
|
473
|
+
event = event.get("_datadog", event)
|
|
474
|
+
|
|
438
475
|
meta = {}
|
|
439
|
-
dd_data = event.get("_datadog")
|
|
440
476
|
|
|
441
|
-
if
|
|
442
|
-
if "x-datadog-trace-id" in
|
|
443
|
-
trace_id = int(
|
|
444
|
-
high_64_bit_trace_id = _parse_high_64_bits(
|
|
445
|
-
dd_data.get("x-datadog-tags")
|
|
446
|
-
)
|
|
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"))
|
|
447
481
|
if high_64_bit_trace_id:
|
|
448
482
|
meta["_dd.p.tid"] = high_64_bit_trace_id
|
|
449
483
|
else: # sfn root
|
|
450
|
-
root_execution_id =
|
|
484
|
+
root_execution_id = event.get("RootExecutionId")
|
|
451
485
|
trace_id = _generate_sfn_trace_id(root_execution_id, LOWER_64_BITS)
|
|
452
486
|
meta["_dd.p.tid"] = _generate_sfn_trace_id(
|
|
453
487
|
root_execution_id, HIGHER_64_BITS
|
|
454
488
|
)
|
|
455
489
|
|
|
456
|
-
parent_id = _generate_sfn_parent_id(
|
|
490
|
+
parent_id = _generate_sfn_parent_id(event)
|
|
457
491
|
else:
|
|
458
492
|
execution_id = event.get("Execution").get("Id")
|
|
459
493
|
trace_id = _generate_sfn_trace_id(execution_id, LOWER_64_BITS)
|
|
@@ -472,20 +506,6 @@ def extract_context_from_step_functions(event, lambda_context):
|
|
|
472
506
|
return extract_context_from_lambda_context(lambda_context)
|
|
473
507
|
|
|
474
508
|
|
|
475
|
-
def is_legacy_lambda_step_function(event):
|
|
476
|
-
"""
|
|
477
|
-
Check if the event is a step function that called a legacy lambda
|
|
478
|
-
"""
|
|
479
|
-
if not isinstance(event, dict) or "Payload" not in event:
|
|
480
|
-
return False
|
|
481
|
-
|
|
482
|
-
event = event.get("Payload")
|
|
483
|
-
return isinstance(event, dict) and (
|
|
484
|
-
"_datadog" in event
|
|
485
|
-
or ("Execution" in event and "StateMachine" in event and "State" in event)
|
|
486
|
-
)
|
|
487
|
-
|
|
488
|
-
|
|
489
509
|
def extract_context_custom_extractor(extractor, event, lambda_context):
|
|
490
510
|
"""
|
|
491
511
|
Extract Datadog trace context using a custom trace extractor function
|
|
@@ -535,6 +555,8 @@ def get_injected_authorizer_data(event, is_http_api) -> dict:
|
|
|
535
555
|
if not dd_data_raw:
|
|
536
556
|
return None
|
|
537
557
|
|
|
558
|
+
import base64
|
|
559
|
+
|
|
538
560
|
injected_data = json.loads(base64.b64decode(dd_data_raw))
|
|
539
561
|
|
|
540
562
|
# Lambda authorizer's results can be cached. But the payload will still have the injected
|
|
@@ -1309,8 +1331,18 @@ def create_inferred_span_from_eventbridge_event(event, context):
|
|
|
1309
1331
|
synchronicity="async",
|
|
1310
1332
|
tag_source="self",
|
|
1311
1333
|
)
|
|
1312
|
-
|
|
1334
|
+
|
|
1313
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
|
+
|
|
1314
1346
|
dt = datetime.strptime(timestamp, dt_format)
|
|
1315
1347
|
|
|
1316
1348
|
tracer.set_tags(_dd_origin)
|
|
@@ -1320,6 +1352,11 @@ def create_inferred_span_from_eventbridge_event(event, context):
|
|
|
1320
1352
|
if span:
|
|
1321
1353
|
span.set_tags(tags)
|
|
1322
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
|
+
|
|
1323
1360
|
return span
|
|
1324
1361
|
|
|
1325
1362
|
|
|
@@ -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.106.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
|