datadog_lambda 6.106.0__tar.gz → 6.108.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.108.0}/PKG-INFO +3 -3
- datadog_lambda-6.108.0/datadog_lambda/api.py +145 -0
- {datadog_lambda-6.106.0 → datadog_lambda-6.108.0}/datadog_lambda/dogstatsd.py +17 -10
- datadog_lambda-6.108.0/datadog_lambda/fips.py +19 -0
- {datadog_lambda-6.106.0 → datadog_lambda-6.108.0}/datadog_lambda/handler.py +0 -1
- {datadog_lambda-6.106.0 → datadog_lambda-6.108.0}/datadog_lambda/metric.py +98 -65
- {datadog_lambda-6.106.0 → datadog_lambda-6.108.0}/datadog_lambda/span_pointers.py +4 -0
- {datadog_lambda-6.106.0 → datadog_lambda-6.108.0}/datadog_lambda/stats_writer.py +1 -1
- {datadog_lambda-6.106.0 → datadog_lambda-6.108.0}/datadog_lambda/statsd_writer.py +3 -3
- {datadog_lambda-6.106.0 → datadog_lambda-6.108.0}/datadog_lambda/thread_stats_writer.py +2 -1
- {datadog_lambda-6.106.0 → datadog_lambda-6.108.0}/datadog_lambda/tracing.py +64 -27
- {datadog_lambda-6.106.0 → datadog_lambda-6.108.0}/datadog_lambda/trigger.py +29 -4
- datadog_lambda-6.108.0/datadog_lambda/version.py +1 -0
- {datadog_lambda-6.106.0 → datadog_lambda-6.108.0}/datadog_lambda/wrapper.py +16 -11
- {datadog_lambda-6.106.0 → datadog_lambda-6.108.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.108.0}/LICENSE +0 -0
- {datadog_lambda-6.106.0 → datadog_lambda-6.108.0}/LICENSE-3rdparty.csv +0 -0
- {datadog_lambda-6.106.0 → datadog_lambda-6.108.0}/NOTICE +0 -0
- {datadog_lambda-6.106.0 → datadog_lambda-6.108.0}/README.md +0 -0
- {datadog_lambda-6.106.0 → datadog_lambda-6.108.0}/datadog_lambda/__init__.py +0 -0
- {datadog_lambda-6.106.0 → datadog_lambda-6.108.0}/datadog_lambda/cold_start.py +0 -0
- {datadog_lambda-6.106.0 → datadog_lambda-6.108.0}/datadog_lambda/constants.py +0 -0
- {datadog_lambda-6.106.0 → datadog_lambda-6.108.0}/datadog_lambda/extension.py +0 -0
- {datadog_lambda-6.106.0 → datadog_lambda-6.108.0}/datadog_lambda/logger.py +0 -0
- {datadog_lambda-6.106.0 → datadog_lambda-6.108.0}/datadog_lambda/module_name.py +0 -0
- {datadog_lambda-6.106.0 → datadog_lambda-6.108.0}/datadog_lambda/patch.py +0 -0
- {datadog_lambda-6.106.0 → datadog_lambda-6.108.0}/datadog_lambda/tag_object.py +0 -0
- {datadog_lambda-6.106.0 → datadog_lambda-6.108.0}/datadog_lambda/tags.py +0 -0
- {datadog_lambda-6.106.0 → datadog_lambda-6.108.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.108.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,145 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
from datadog_lambda.fips import fips_mode_enabled
|
|
5
|
+
|
|
6
|
+
logger = logging.getLogger(__name__)
|
|
7
|
+
KMS_ENCRYPTION_CONTEXT_KEY = "LambdaFunctionName"
|
|
8
|
+
api_key = None
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def decrypt_kms_api_key(kms_client, ciphertext):
|
|
12
|
+
import base64
|
|
13
|
+
|
|
14
|
+
from botocore.exceptions import ClientError
|
|
15
|
+
|
|
16
|
+
"""
|
|
17
|
+
Decodes and deciphers the base64-encoded ciphertext given as a parameter using KMS.
|
|
18
|
+
For this to work properly, the Lambda function must have the appropriate IAM permissions.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
kms_client: The KMS client to use for decryption
|
|
22
|
+
ciphertext (string): The base64-encoded ciphertext to decrypt
|
|
23
|
+
"""
|
|
24
|
+
decoded_bytes = base64.b64decode(ciphertext)
|
|
25
|
+
|
|
26
|
+
"""
|
|
27
|
+
When the API key is encrypted using the AWS console, the function name is added as an
|
|
28
|
+
encryption context. When the API key is encrypted using the AWS CLI, no encryption context
|
|
29
|
+
is added. We need to try decrypting the API key both with and without the encryption context.
|
|
30
|
+
"""
|
|
31
|
+
# Try without encryption context, in case API key was encrypted using the AWS CLI
|
|
32
|
+
function_name = os.environ.get("AWS_LAMBDA_FUNCTION_NAME")
|
|
33
|
+
try:
|
|
34
|
+
plaintext = kms_client.decrypt(CiphertextBlob=decoded_bytes)[
|
|
35
|
+
"Plaintext"
|
|
36
|
+
].decode("utf-8")
|
|
37
|
+
except ClientError:
|
|
38
|
+
logger.debug(
|
|
39
|
+
"Failed to decrypt ciphertext without encryption context, \
|
|
40
|
+
retrying with encryption context"
|
|
41
|
+
)
|
|
42
|
+
# Try with encryption context, in case API key was encrypted using the AWS Console
|
|
43
|
+
plaintext = kms_client.decrypt(
|
|
44
|
+
CiphertextBlob=decoded_bytes,
|
|
45
|
+
EncryptionContext={
|
|
46
|
+
KMS_ENCRYPTION_CONTEXT_KEY: function_name,
|
|
47
|
+
},
|
|
48
|
+
)["Plaintext"].decode("utf-8")
|
|
49
|
+
|
|
50
|
+
return plaintext
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def get_api_key() -> str:
|
|
54
|
+
"""
|
|
55
|
+
Gets the Datadog API key from the environment variables or secrets manager.
|
|
56
|
+
Extracts the result to a global value to avoid repeated calls to the
|
|
57
|
+
secrets manager from different products.
|
|
58
|
+
"""
|
|
59
|
+
global api_key
|
|
60
|
+
if api_key:
|
|
61
|
+
return api_key
|
|
62
|
+
|
|
63
|
+
DD_API_KEY_SECRET_ARN = os.environ.get("DD_API_KEY_SECRET_ARN", "")
|
|
64
|
+
DD_API_KEY_SSM_NAME = os.environ.get("DD_API_KEY_SSM_NAME", "")
|
|
65
|
+
DD_KMS_API_KEY = os.environ.get("DD_KMS_API_KEY", "")
|
|
66
|
+
DD_API_KEY = os.environ.get("DD_API_KEY", os.environ.get("DATADOG_API_KEY", ""))
|
|
67
|
+
|
|
68
|
+
LAMBDA_REGION = os.environ.get("AWS_REGION", "")
|
|
69
|
+
if fips_mode_enabled:
|
|
70
|
+
logger.debug(
|
|
71
|
+
"FIPS mode is enabled, using FIPS endpoints for secrets management."
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
if DD_API_KEY_SECRET_ARN:
|
|
75
|
+
# Secrets manager endpoints: https://docs.aws.amazon.com/general/latest/gr/asm.html
|
|
76
|
+
try:
|
|
77
|
+
secrets_region = DD_API_KEY_SECRET_ARN.split(":")[3]
|
|
78
|
+
except Exception:
|
|
79
|
+
logger.debug(
|
|
80
|
+
"Invalid secret arn in DD_API_KEY_SECRET_ARN. Unable to get API key."
|
|
81
|
+
)
|
|
82
|
+
return ""
|
|
83
|
+
endpoint_url = (
|
|
84
|
+
f"https://secretsmanager-fips.{secrets_region}.amazonaws.com"
|
|
85
|
+
if fips_mode_enabled
|
|
86
|
+
else None
|
|
87
|
+
)
|
|
88
|
+
secrets_manager_client = _boto3_client(
|
|
89
|
+
"secretsmanager", endpoint_url=endpoint_url, region_name=secrets_region
|
|
90
|
+
)
|
|
91
|
+
api_key = secrets_manager_client.get_secret_value(
|
|
92
|
+
SecretId=DD_API_KEY_SECRET_ARN
|
|
93
|
+
)["SecretString"]
|
|
94
|
+
elif DD_API_KEY_SSM_NAME:
|
|
95
|
+
# SSM endpoints: https://docs.aws.amazon.com/general/latest/gr/ssm.html
|
|
96
|
+
fips_endpoint = (
|
|
97
|
+
f"https://ssm-fips.{LAMBDA_REGION}.amazonaws.com"
|
|
98
|
+
if fips_mode_enabled
|
|
99
|
+
else None
|
|
100
|
+
)
|
|
101
|
+
ssm_client = _boto3_client("ssm", endpoint_url=fips_endpoint)
|
|
102
|
+
api_key = ssm_client.get_parameter(
|
|
103
|
+
Name=DD_API_KEY_SSM_NAME, WithDecryption=True
|
|
104
|
+
)["Parameter"]["Value"]
|
|
105
|
+
elif DD_KMS_API_KEY:
|
|
106
|
+
# KMS endpoints: https://docs.aws.amazon.com/general/latest/gr/kms.html
|
|
107
|
+
fips_endpoint = (
|
|
108
|
+
f"https://kms-fips.{LAMBDA_REGION}.amazonaws.com"
|
|
109
|
+
if fips_mode_enabled
|
|
110
|
+
else None
|
|
111
|
+
)
|
|
112
|
+
kms_client = _boto3_client("kms", endpoint_url=fips_endpoint)
|
|
113
|
+
api_key = decrypt_kms_api_key(kms_client, DD_KMS_API_KEY)
|
|
114
|
+
else:
|
|
115
|
+
api_key = DD_API_KEY
|
|
116
|
+
|
|
117
|
+
return api_key
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def init_api():
|
|
121
|
+
if not os.environ.get("DD_FLUSH_TO_LOG", "").lower() == "true":
|
|
122
|
+
# Make sure that this package would always be lazy-loaded/outside from the critical path
|
|
123
|
+
# since underlying packages are quite heavy to load
|
|
124
|
+
# and useless with the extension unless sending metrics with timestamps
|
|
125
|
+
from datadog import api
|
|
126
|
+
|
|
127
|
+
if not api._api_key:
|
|
128
|
+
api._api_key = get_api_key()
|
|
129
|
+
|
|
130
|
+
logger.debug("Setting DATADOG_API_KEY of length %d", len(api._api_key))
|
|
131
|
+
|
|
132
|
+
# Set DATADOG_HOST, to send data to a non-default Datadog datacenter
|
|
133
|
+
api._api_host = os.environ.get(
|
|
134
|
+
"DATADOG_HOST", "https://api." + os.environ.get("DD_SITE", "datadoghq.com")
|
|
135
|
+
)
|
|
136
|
+
logger.debug("Setting DATADOG_HOST to %s", api._api_host)
|
|
137
|
+
|
|
138
|
+
# Unmute exceptions from datadog api client, so we can catch and handle them
|
|
139
|
+
api._mute = False
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _boto3_client(*args, **kwargs):
|
|
143
|
+
import botocore.session
|
|
144
|
+
|
|
145
|
+
return botocore.session.get_session().create_client(*args, **kwargs)
|
|
@@ -1,11 +1,10 @@
|
|
|
1
|
+
import errno
|
|
1
2
|
import logging
|
|
2
3
|
import os
|
|
3
|
-
import socket
|
|
4
|
-
import errno
|
|
5
4
|
import re
|
|
5
|
+
import socket
|
|
6
6
|
from threading import Lock
|
|
7
7
|
|
|
8
|
-
|
|
9
8
|
MIN_SEND_BUFFER_SIZE = 32 * 1024
|
|
10
9
|
log = logging.getLogger("datadog_lambda.dogstatsd")
|
|
11
10
|
|
|
@@ -55,14 +54,21 @@ class DogStatsd(object):
|
|
|
55
54
|
|
|
56
55
|
return sock
|
|
57
56
|
|
|
58
|
-
def distribution(self, metric, value, tags=None):
|
|
57
|
+
def distribution(self, metric, value, tags=None, timestamp=None):
|
|
59
58
|
"""
|
|
60
|
-
Send a global distribution value, optionally setting tags.
|
|
59
|
+
Send a global distribution value, optionally setting tags. The optional
|
|
60
|
+
timestamp should be an integer representing seconds since the epoch
|
|
61
|
+
(January 1, 1970, 00:00:00 UTC).
|
|
61
62
|
|
|
62
63
|
>>> statsd.distribution("uploaded.file.size", 1445)
|
|
63
64
|
>>> statsd.distribution("album.photo.count", 26, tags=["gender:female"])
|
|
65
|
+
>>> statsd.distribution(
|
|
66
|
+
>>> "historic.file.count",
|
|
67
|
+
>>> 5,
|
|
68
|
+
>>> timestamp=int(datetime(2020, 2, 14, 12, 0, 0).timestamp()),
|
|
69
|
+
>>> )
|
|
64
70
|
"""
|
|
65
|
-
self._report(metric, "d", value, tags)
|
|
71
|
+
self._report(metric, "d", value, tags, timestamp)
|
|
66
72
|
|
|
67
73
|
def close_socket(self):
|
|
68
74
|
"""
|
|
@@ -84,20 +90,21 @@ class DogStatsd(object):
|
|
|
84
90
|
for tag in tag_list
|
|
85
91
|
]
|
|
86
92
|
|
|
87
|
-
def _serialize_metric(self, metric, metric_type, value, tags):
|
|
93
|
+
def _serialize_metric(self, metric, metric_type, value, tags, timestamp):
|
|
88
94
|
# Create/format the metric packet
|
|
89
|
-
return "%s:%s|%s%s" % (
|
|
95
|
+
return "%s:%s|%s%s%s" % (
|
|
90
96
|
metric,
|
|
91
97
|
value,
|
|
92
98
|
metric_type,
|
|
93
99
|
("|#" + ",".join(self.normalize_tags(tags))) if tags else "",
|
|
100
|
+
("|T" + str(timestamp)) if timestamp is not None else "",
|
|
94
101
|
)
|
|
95
102
|
|
|
96
|
-
def _report(self, metric, metric_type, value, tags):
|
|
103
|
+
def _report(self, metric, metric_type, value, tags, timestamp):
|
|
97
104
|
if value is None:
|
|
98
105
|
return
|
|
99
106
|
|
|
100
|
-
payload = self._serialize_metric(metric, metric_type, value, tags)
|
|
107
|
+
payload = self._serialize_metric(metric, metric_type, value, tags, timestamp)
|
|
101
108
|
|
|
102
109
|
# Send it
|
|
103
110
|
self._send_to_server(payload)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
is_gov_region = os.environ.get("AWS_REGION", "").startswith("us-gov-")
|
|
5
|
+
|
|
6
|
+
fips_mode_enabled = (
|
|
7
|
+
os.environ.get(
|
|
8
|
+
"DD_LAMBDA_FIPS_MODE",
|
|
9
|
+
"true" if is_gov_region else "false",
|
|
10
|
+
).lower()
|
|
11
|
+
== "true"
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
if is_gov_region or fips_mode_enabled:
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
logger.debug(
|
|
17
|
+
"Python Lambda Layer FIPS mode is %s.",
|
|
18
|
+
"enabled" if fips_mode_enabled else "not enabled",
|
|
19
|
+
)
|
|
@@ -3,37 +3,66 @@
|
|
|
3
3
|
# This product includes software developed at Datadog (https://www.datadoghq.com/).
|
|
4
4
|
# Copyright 2019 Datadog, Inc.
|
|
5
5
|
|
|
6
|
+
import enum
|
|
7
|
+
import logging
|
|
6
8
|
import os
|
|
7
9
|
import time
|
|
8
|
-
import logging
|
|
9
|
-
import ujson as json
|
|
10
10
|
from datetime import datetime, timedelta
|
|
11
11
|
|
|
12
|
+
import ujson as json
|
|
13
|
+
|
|
12
14
|
from datadog_lambda.extension import should_use_extension
|
|
13
|
-
from datadog_lambda.
|
|
15
|
+
from datadog_lambda.fips import fips_mode_enabled
|
|
16
|
+
from datadog_lambda.tags import dd_lambda_layer_tag, get_enhanced_metrics_tags
|
|
14
17
|
|
|
15
18
|
logger = logging.getLogger(__name__)
|
|
16
19
|
|
|
17
|
-
lambda_stats = None
|
|
18
|
-
extension_thread_stats = None
|
|
19
20
|
|
|
20
|
-
|
|
21
|
+
class MetricsHandler(enum.Enum):
|
|
22
|
+
EXTENSION = "extension"
|
|
23
|
+
FORWARDER = "forwarder"
|
|
24
|
+
DATADOG_API = "datadog_api"
|
|
25
|
+
NO_METRICS = "no_metrics"
|
|
21
26
|
|
|
22
|
-
|
|
27
|
+
|
|
28
|
+
def _select_metrics_handler():
|
|
29
|
+
if should_use_extension:
|
|
30
|
+
return MetricsHandler.EXTENSION
|
|
31
|
+
if os.environ.get("DD_FLUSH_TO_LOG", "").lower() == "true":
|
|
32
|
+
return MetricsHandler.FORWARDER
|
|
33
|
+
|
|
34
|
+
if fips_mode_enabled:
|
|
35
|
+
logger.debug(
|
|
36
|
+
"With FIPS mode enabled, the Datadog API metrics handler is unavailable."
|
|
37
|
+
)
|
|
38
|
+
return MetricsHandler.NO_METRICS
|
|
39
|
+
|
|
40
|
+
return MetricsHandler.DATADOG_API
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
metrics_handler = _select_metrics_handler()
|
|
44
|
+
logger.debug("identified primary metrics handler as %s", metrics_handler)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
lambda_stats = None
|
|
48
|
+
if metrics_handler == MetricsHandler.EXTENSION:
|
|
23
49
|
from datadog_lambda.statsd_writer import StatsDWriter
|
|
24
50
|
|
|
25
51
|
lambda_stats = StatsDWriter()
|
|
26
|
-
|
|
52
|
+
|
|
53
|
+
elif metrics_handler == MetricsHandler.DATADOG_API:
|
|
27
54
|
# Periodical flushing in a background thread is NOT guaranteed to succeed
|
|
28
55
|
# and leads to data loss. When disabled, metrics are only flushed at the
|
|
29
56
|
# end of invocation. To make metrics submitted from a long-running Lambda
|
|
30
57
|
# function available sooner, consider using the Datadog Lambda extension.
|
|
31
|
-
from datadog_lambda.thread_stats_writer import ThreadStatsWriter
|
|
32
58
|
from datadog_lambda.api import init_api
|
|
59
|
+
from datadog_lambda.thread_stats_writer import ThreadStatsWriter
|
|
33
60
|
|
|
61
|
+
flush_in_thread = os.environ.get("DD_FLUSH_IN_THREAD", "").lower() == "true"
|
|
34
62
|
init_api()
|
|
35
63
|
lambda_stats = ThreadStatsWriter(flush_in_thread)
|
|
36
64
|
|
|
65
|
+
|
|
37
66
|
enhanced_metrics_enabled = (
|
|
38
67
|
os.environ.get("DD_ENHANCED_METRICS", "true").lower() == "true"
|
|
39
68
|
)
|
|
@@ -44,16 +73,19 @@ def lambda_metric(metric_name, value, timestamp=None, tags=None, force_async=Fal
|
|
|
44
73
|
Submit a data point to Datadog distribution metrics.
|
|
45
74
|
https://docs.datadoghq.com/graphing/metrics/distributions/
|
|
46
75
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
76
|
+
If the Datadog Lambda Extension is present, metrics are submitted to its
|
|
77
|
+
dogstatsd endpoint.
|
|
78
|
+
|
|
79
|
+
When DD_FLUSH_TO_LOG is True or force_async is True, write metric to log,
|
|
80
|
+
and wait for the Datadog Log Forwarder Lambda function to submit the
|
|
81
|
+
metrics asynchronously.
|
|
50
82
|
|
|
51
83
|
Otherwise, the metrics will be submitted to the Datadog API
|
|
52
84
|
periodically and at the end of the function execution in a
|
|
53
85
|
background thread.
|
|
54
86
|
|
|
55
|
-
Note that if the extension is present, it will override the DD_FLUSH_TO_LOG
|
|
56
|
-
and always use the layer to send metrics to the extension
|
|
87
|
+
Note that if the extension is present, it will override the DD_FLUSH_TO_LOG
|
|
88
|
+
value and always use the layer to send metrics to the extension
|
|
57
89
|
"""
|
|
58
90
|
if not metric_name or not isinstance(metric_name, str):
|
|
59
91
|
logger.warning(
|
|
@@ -71,56 +103,54 @@ def lambda_metric(metric_name, value, timestamp=None, tags=None, force_async=Fal
|
|
|
71
103
|
)
|
|
72
104
|
return
|
|
73
105
|
|
|
74
|
-
flush_to_logs = os.environ.get("DD_FLUSH_TO_LOG", "").lower() == "true"
|
|
75
106
|
tags = [] if tags is None else list(tags)
|
|
76
107
|
tags.append(dd_lambda_layer_tag)
|
|
77
108
|
|
|
78
|
-
if
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
)
|
|
92
|
-
return
|
|
93
|
-
global extension_thread_stats
|
|
94
|
-
if extension_thread_stats is None:
|
|
95
|
-
from datadog_lambda.thread_stats_writer import ThreadStatsWriter
|
|
96
|
-
from datadog_lambda.api import init_api
|
|
97
|
-
|
|
98
|
-
init_api()
|
|
99
|
-
extension_thread_stats = ThreadStatsWriter(flush_in_thread)
|
|
100
|
-
|
|
101
|
-
extension_thread_stats.distribution(
|
|
102
|
-
metric_name, value, tags=tags, timestamp=timestamp
|
|
103
|
-
)
|
|
104
|
-
return
|
|
109
|
+
if metrics_handler == MetricsHandler.EXTENSION:
|
|
110
|
+
if timestamp is not None:
|
|
111
|
+
if isinstance(timestamp, datetime):
|
|
112
|
+
timestamp = int(timestamp.timestamp())
|
|
113
|
+
|
|
114
|
+
timestamp_floor = int((datetime.now() - timedelta(hours=4)).timestamp())
|
|
115
|
+
if timestamp < timestamp_floor:
|
|
116
|
+
logger.warning(
|
|
117
|
+
"Timestamp %s is older than 4 hours, not submitting metric %s",
|
|
118
|
+
timestamp,
|
|
119
|
+
metric_name,
|
|
120
|
+
)
|
|
121
|
+
return
|
|
105
122
|
|
|
106
|
-
if should_use_extension:
|
|
107
123
|
logger.debug(
|
|
108
124
|
"Sending metric %s value %s to Datadog via extension", metric_name, value
|
|
109
125
|
)
|
|
110
126
|
lambda_stats.distribution(metric_name, value, tags=tags, timestamp=timestamp)
|
|
127
|
+
|
|
128
|
+
elif force_async or (metrics_handler == MetricsHandler.FORWARDER):
|
|
129
|
+
write_metric_point_to_stdout(metric_name, value, timestamp=timestamp, tags=tags)
|
|
130
|
+
|
|
131
|
+
elif metrics_handler == MetricsHandler.DATADOG_API:
|
|
132
|
+
lambda_stats.distribution(metric_name, value, tags=tags, timestamp=timestamp)
|
|
133
|
+
|
|
134
|
+
elif metrics_handler == MetricsHandler.NO_METRICS:
|
|
135
|
+
logger.debug(
|
|
136
|
+
"Metric %s cannot be submitted because the metrics handler is disabled",
|
|
137
|
+
metric_name,
|
|
138
|
+
),
|
|
139
|
+
|
|
111
140
|
else:
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
)
|
|
141
|
+
# This should be qutie impossible, but let's at least log a message if
|
|
142
|
+
# it somehow happens.
|
|
143
|
+
logger.debug(
|
|
144
|
+
"Metric %s cannot be submitted because the metrics handler is not configured: %s",
|
|
145
|
+
metric_name,
|
|
146
|
+
metrics_handler,
|
|
147
|
+
)
|
|
120
148
|
|
|
121
149
|
|
|
122
|
-
def write_metric_point_to_stdout(metric_name, value, timestamp=None, tags=
|
|
150
|
+
def write_metric_point_to_stdout(metric_name, value, timestamp=None, tags=None):
|
|
123
151
|
"""Writes the specified metric point to standard output"""
|
|
152
|
+
tags = tags or []
|
|
153
|
+
|
|
124
154
|
logger.debug(
|
|
125
155
|
"Sending metric %s value %s to Datadog via log forwarder", metric_name, value
|
|
126
156
|
)
|
|
@@ -138,19 +168,8 @@ def write_metric_point_to_stdout(metric_name, value, timestamp=None, tags=[]):
|
|
|
138
168
|
|
|
139
169
|
|
|
140
170
|
def flush_stats(lambda_context=None):
|
|
141
|
-
lambda_stats
|
|
142
|
-
|
|
143
|
-
if extension_thread_stats is not None:
|
|
144
|
-
tags = None
|
|
145
|
-
if lambda_context is not None:
|
|
146
|
-
tags = get_enhanced_metrics_tags(lambda_context)
|
|
147
|
-
split_arn = lambda_context.invoked_function_arn.split(":")
|
|
148
|
-
if len(split_arn) > 7:
|
|
149
|
-
# Get rid of the alias
|
|
150
|
-
split_arn.pop()
|
|
151
|
-
arn = ":".join(split_arn)
|
|
152
|
-
tags.append("function_arn:" + arn)
|
|
153
|
-
extension_thread_stats.flush(tags)
|
|
171
|
+
if lambda_stats is not None:
|
|
172
|
+
lambda_stats.flush()
|
|
154
173
|
|
|
155
174
|
|
|
156
175
|
def submit_enhanced_metric(metric_name, lambda_context):
|
|
@@ -188,3 +207,17 @@ def submit_errors_metric(lambda_context):
|
|
|
188
207
|
lambda_context (object): Lambda context dict passed to the function by AWS
|
|
189
208
|
"""
|
|
190
209
|
submit_enhanced_metric("errors", lambda_context)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def submit_dynamodb_stream_type_metric(event):
|
|
213
|
+
stream_view_type = (
|
|
214
|
+
event.get("Records", [{}])[0].get("dynamodb", {}).get("StreamViewType")
|
|
215
|
+
)
|
|
216
|
+
if stream_view_type:
|
|
217
|
+
lambda_metric(
|
|
218
|
+
"datadog.serverless.dynamodb.stream.type",
|
|
219
|
+
1,
|
|
220
|
+
timestamp=None,
|
|
221
|
+
tags=[f"streamtype:{stream_view_type}"],
|
|
222
|
+
force_async=True,
|
|
223
|
+
)
|
|
@@ -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:
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
from datadog_lambda.stats_writer import StatsWriter
|
|
2
1
|
from datadog_lambda.dogstatsd import statsd
|
|
2
|
+
from datadog_lambda.stats_writer import StatsWriter
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
class StatsDWriter(StatsWriter):
|
|
@@ -7,8 +7,8 @@ class StatsDWriter(StatsWriter):
|
|
|
7
7
|
Writes distribution metrics using StatsD protocol
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
|
-
def distribution(self, metric_name, value, tags=
|
|
11
|
-
statsd.distribution(metric_name, value, tags=tags)
|
|
10
|
+
def distribution(self, metric_name, value, tags=None, timestamp=None):
|
|
11
|
+
statsd.distribution(metric_name, value, tags=tags, timestamp=timestamp)
|
|
12
12
|
|
|
13
13
|
def flush(self):
|
|
14
14
|
pass
|
|
@@ -3,6 +3,7 @@ import logging
|
|
|
3
3
|
# Make sure that this package would always be lazy-loaded/outside from the critical path
|
|
4
4
|
# since underlying packages are quite heavy to load and useless when the extension is present
|
|
5
5
|
from datadog.threadstats import ThreadStats
|
|
6
|
+
|
|
6
7
|
from datadog_lambda.stats_writer import StatsWriter
|
|
7
8
|
|
|
8
9
|
logger = logging.getLogger(__name__)
|
|
@@ -17,7 +18,7 @@ class ThreadStatsWriter(StatsWriter):
|
|
|
17
18
|
self.thread_stats = ThreadStats(compress_payload=True)
|
|
18
19
|
self.thread_stats.start(flush_in_thread=flush_in_thread)
|
|
19
20
|
|
|
20
|
-
def distribution(self, metric_name, value, tags=
|
|
21
|
+
def distribution(self, metric_name, value, tags=None, timestamp=None):
|
|
21
22
|
self.thread_stats.distribution(
|
|
22
23
|
metric_name, value, tags=tags, timestamp=timestamp
|
|
23
24
|
)
|
|
@@ -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.108.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,
|
|
@@ -242,7 +235,11 @@ class _LambdaDecorator(object):
|
|
|
242
235
|
self.response = self.func(event, context, **kwargs)
|
|
243
236
|
return self.response
|
|
244
237
|
except Exception:
|
|
245
|
-
|
|
238
|
+
if not should_use_extension:
|
|
239
|
+
from datadog_lambda.metric import submit_errors_metric
|
|
240
|
+
|
|
241
|
+
submit_errors_metric(context)
|
|
242
|
+
|
|
246
243
|
if self.span:
|
|
247
244
|
self.span.set_traceback()
|
|
248
245
|
raise
|
|
@@ -268,6 +265,9 @@ class _LambdaDecorator(object):
|
|
|
268
265
|
injected_headers[Headers.Parent_Span_Finish_Time] = finish_time_ns
|
|
269
266
|
if request_id is not None:
|
|
270
267
|
injected_headers[Headers.Authorizing_Request_Id] = request_id
|
|
268
|
+
|
|
269
|
+
import base64
|
|
270
|
+
|
|
271
271
|
datadog_data = base64.b64encode(
|
|
272
272
|
json.dumps(injected_headers, escape_forward_slashes=False).encode()
|
|
273
273
|
).decode()
|
|
@@ -278,9 +278,12 @@ class _LambdaDecorator(object):
|
|
|
278
278
|
try:
|
|
279
279
|
self.response = None
|
|
280
280
|
set_cold_start(init_timestamp_ns)
|
|
281
|
-
|
|
282
|
-
if
|
|
283
|
-
|
|
281
|
+
|
|
282
|
+
if not should_use_extension:
|
|
283
|
+
from datadog_lambda.metric import submit_invocations_metric
|
|
284
|
+
|
|
285
|
+
submit_invocations_metric(context)
|
|
286
|
+
|
|
284
287
|
self.trigger_tags = extract_trigger_tags(event, context)
|
|
285
288
|
# Extract Datadog trace context and source from incoming requests
|
|
286
289
|
dd_context, trace_context_source, event_source = extract_dd_trace_context(
|
|
@@ -379,6 +382,8 @@ class _LambdaDecorator(object):
|
|
|
379
382
|
logger.debug("Failed to create cold start spans. %s", e)
|
|
380
383
|
|
|
381
384
|
if not self.flush_to_log or should_use_extension:
|
|
385
|
+
from datadog_lambda.metric import flush_stats
|
|
386
|
+
|
|
382
387
|
flush_stats(context)
|
|
383
388
|
if should_use_extension and self.local_testing_mode:
|
|
384
389
|
# 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.108.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
|