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.
Files changed (31) hide show
  1. {datadog_lambda-6.106.0 → datadog_lambda-6.108.0}/PKG-INFO +3 -3
  2. datadog_lambda-6.108.0/datadog_lambda/api.py +145 -0
  3. {datadog_lambda-6.106.0 → datadog_lambda-6.108.0}/datadog_lambda/dogstatsd.py +17 -10
  4. datadog_lambda-6.108.0/datadog_lambda/fips.py +19 -0
  5. {datadog_lambda-6.106.0 → datadog_lambda-6.108.0}/datadog_lambda/handler.py +0 -1
  6. {datadog_lambda-6.106.0 → datadog_lambda-6.108.0}/datadog_lambda/metric.py +98 -65
  7. {datadog_lambda-6.106.0 → datadog_lambda-6.108.0}/datadog_lambda/span_pointers.py +4 -0
  8. {datadog_lambda-6.106.0 → datadog_lambda-6.108.0}/datadog_lambda/stats_writer.py +1 -1
  9. {datadog_lambda-6.106.0 → datadog_lambda-6.108.0}/datadog_lambda/statsd_writer.py +3 -3
  10. {datadog_lambda-6.106.0 → datadog_lambda-6.108.0}/datadog_lambda/thread_stats_writer.py +2 -1
  11. {datadog_lambda-6.106.0 → datadog_lambda-6.108.0}/datadog_lambda/tracing.py +64 -27
  12. {datadog_lambda-6.106.0 → datadog_lambda-6.108.0}/datadog_lambda/trigger.py +29 -4
  13. datadog_lambda-6.108.0/datadog_lambda/version.py +1 -0
  14. {datadog_lambda-6.106.0 → datadog_lambda-6.108.0}/datadog_lambda/wrapper.py +16 -11
  15. {datadog_lambda-6.106.0 → datadog_lambda-6.108.0}/pyproject.toml +4 -4
  16. datadog_lambda-6.106.0/datadog_lambda/api.py +0 -89
  17. datadog_lambda-6.106.0/datadog_lambda/version.py +0 -1
  18. {datadog_lambda-6.106.0 → datadog_lambda-6.108.0}/LICENSE +0 -0
  19. {datadog_lambda-6.106.0 → datadog_lambda-6.108.0}/LICENSE-3rdparty.csv +0 -0
  20. {datadog_lambda-6.106.0 → datadog_lambda-6.108.0}/NOTICE +0 -0
  21. {datadog_lambda-6.106.0 → datadog_lambda-6.108.0}/README.md +0 -0
  22. {datadog_lambda-6.106.0 → datadog_lambda-6.108.0}/datadog_lambda/__init__.py +0 -0
  23. {datadog_lambda-6.106.0 → datadog_lambda-6.108.0}/datadog_lambda/cold_start.py +0 -0
  24. {datadog_lambda-6.106.0 → datadog_lambda-6.108.0}/datadog_lambda/constants.py +0 -0
  25. {datadog_lambda-6.106.0 → datadog_lambda-6.108.0}/datadog_lambda/extension.py +0 -0
  26. {datadog_lambda-6.106.0 → datadog_lambda-6.108.0}/datadog_lambda/logger.py +0 -0
  27. {datadog_lambda-6.106.0 → datadog_lambda-6.108.0}/datadog_lambda/module_name.py +0 -0
  28. {datadog_lambda-6.106.0 → datadog_lambda-6.108.0}/datadog_lambda/patch.py +0 -0
  29. {datadog_lambda-6.106.0 → datadog_lambda-6.108.0}/datadog_lambda/tag_object.py +0 -0
  30. {datadog_lambda-6.106.0 → datadog_lambda-6.108.0}/datadog_lambda/tags.py +0 -0
  31. {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.106.0
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: boto3 (>=1.34.0,<2.0.0) ; extra == "dev"
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,7 +3,6 @@
3
3
  # This product includes software developed at Datadog (https://www.datadoghq.com/).
4
4
  # Copyright 2020 Datadog, Inc.
5
5
 
6
- from __future__ import absolute_import
7
6
  from importlib import import_module
8
7
 
9
8
  import os
@@ -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.tags import get_enhanced_metrics_tags, dd_lambda_layer_tag
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
- flush_in_thread = os.environ.get("DD_FLUSH_IN_THREAD", "").lower() == "true"
21
+ class MetricsHandler(enum.Enum):
22
+ EXTENSION = "extension"
23
+ FORWARDER = "forwarder"
24
+ DATADOG_API = "datadog_api"
25
+ NO_METRICS = "no_metrics"
21
26
 
22
- if should_use_extension:
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
- else:
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
- When DD_FLUSH_TO_LOG is True, write metric to log, and
48
- wait for the Datadog Log Forwarder Lambda function to submit
49
- the metrics asynchronously.
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 value
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 should_use_extension and timestamp is not None:
79
- # The extension does not support timestamps for distributions so we create a
80
- # a thread stats writer to submit metrics with timestamps to the API
81
- timestamp_ceiling = int(
82
- (datetime.now() - timedelta(hours=4)).timestamp()
83
- ) # 4 hours ago
84
- if isinstance(timestamp, datetime):
85
- timestamp = int(timestamp.timestamp())
86
- if timestamp_ceiling > timestamp:
87
- logger.warning(
88
- "Timestamp %s is older than 4 hours, not submitting metric %s",
89
- timestamp,
90
- metric_name,
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
- if flush_to_logs or force_async:
113
- write_metric_point_to_stdout(
114
- metric_name, value, timestamp=timestamp, tags=tags
115
- )
116
- else:
117
- lambda_stats.distribution(
118
- metric_name, value, tags=tags, timestamp=timestamp
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.flush()
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
1
  class StatsWriter:
2
- def distribution(self, metric_name, value, tags=[], timestamp=None):
2
+ def distribution(self, metric_name, value, tags=None, timestamp=None):
3
3
  raise NotImplementedError()
4
4
 
5
5
  def flush(self):
@@ -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=[], timestamp=None):
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=[], timestamp=None):
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 dd_data and dd_data.get("serverless-version") == "v1":
442
- if "x-datadog-trace-id" in dd_data: # lambda root
443
- trace_id = int(dd_data.get("x-datadog-trace-id"))
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 = dd_data.get("RootExecutionId")
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(dd_data)
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
- dt_format = "%Y-%m-%dT%H:%M:%SZ"
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
- submit_errors_metric(context)
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
- submit_invocations_metric(context)
282
- if is_legacy_lambda_step_function(event):
283
- event = event["Payload"]
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.106.0"
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
- boto3 = { version = "^1.34.0", optional = true }
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
- "boto3",
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"