datadog_lambda 6.105.0__tar.gz → 6.107.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. {datadog_lambda-6.105.0 → datadog_lambda-6.107.0}/PKG-INFO +3 -3
  2. datadog_lambda-6.107.0/datadog_lambda/api.py +139 -0
  3. {datadog_lambda-6.105.0 → datadog_lambda-6.107.0}/datadog_lambda/handler.py +0 -1
  4. {datadog_lambda-6.105.0 → datadog_lambda-6.107.0}/datadog_lambda/metric.py +30 -0
  5. {datadog_lambda-6.105.0 → datadog_lambda-6.107.0}/datadog_lambda/span_pointers.py +4 -0
  6. {datadog_lambda-6.105.0 → datadog_lambda-6.107.0}/datadog_lambda/tracing.py +75 -34
  7. {datadog_lambda-6.105.0 → datadog_lambda-6.107.0}/datadog_lambda/trigger.py +29 -4
  8. datadog_lambda-6.107.0/datadog_lambda/version.py +1 -0
  9. {datadog_lambda-6.105.0 → datadog_lambda-6.107.0}/datadog_lambda/wrapper.py +24 -12
  10. {datadog_lambda-6.105.0 → datadog_lambda-6.107.0}/pyproject.toml +4 -4
  11. datadog_lambda-6.105.0/datadog_lambda/api.py +0 -89
  12. datadog_lambda-6.105.0/datadog_lambda/version.py +0 -1
  13. {datadog_lambda-6.105.0 → datadog_lambda-6.107.0}/LICENSE +0 -0
  14. {datadog_lambda-6.105.0 → datadog_lambda-6.107.0}/LICENSE-3rdparty.csv +0 -0
  15. {datadog_lambda-6.105.0 → datadog_lambda-6.107.0}/NOTICE +0 -0
  16. {datadog_lambda-6.105.0 → datadog_lambda-6.107.0}/README.md +0 -0
  17. {datadog_lambda-6.105.0 → datadog_lambda-6.107.0}/datadog_lambda/__init__.py +0 -0
  18. {datadog_lambda-6.105.0 → datadog_lambda-6.107.0}/datadog_lambda/cold_start.py +0 -0
  19. {datadog_lambda-6.105.0 → datadog_lambda-6.107.0}/datadog_lambda/constants.py +0 -0
  20. {datadog_lambda-6.105.0 → datadog_lambda-6.107.0}/datadog_lambda/dogstatsd.py +0 -0
  21. {datadog_lambda-6.105.0 → datadog_lambda-6.107.0}/datadog_lambda/extension.py +0 -0
  22. {datadog_lambda-6.105.0 → datadog_lambda-6.107.0}/datadog_lambda/logger.py +0 -0
  23. {datadog_lambda-6.105.0 → datadog_lambda-6.107.0}/datadog_lambda/module_name.py +0 -0
  24. {datadog_lambda-6.105.0 → datadog_lambda-6.107.0}/datadog_lambda/patch.py +0 -0
  25. {datadog_lambda-6.105.0 → datadog_lambda-6.107.0}/datadog_lambda/stats_writer.py +0 -0
  26. {datadog_lambda-6.105.0 → datadog_lambda-6.107.0}/datadog_lambda/statsd_writer.py +0 -0
  27. {datadog_lambda-6.105.0 → datadog_lambda-6.107.0}/datadog_lambda/tag_object.py +0 -0
  28. {datadog_lambda-6.105.0 → datadog_lambda-6.107.0}/datadog_lambda/tags.py +0 -0
  29. {datadog_lambda-6.105.0 → datadog_lambda-6.107.0}/datadog_lambda/thread_stats_writer.py +0 -0
  30. {datadog_lambda-6.105.0 → datadog_lambda-6.107.0}/datadog_lambda/xray.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: datadog_lambda
3
- Version: 6.105.0
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: 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,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)
@@ -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
@@ -55,6 +55,22 @@ def lambda_metric(metric_name, value, timestamp=None, tags=None, force_async=Fal
55
55
  Note that if the extension is present, it will override the DD_FLUSH_TO_LOG value
56
56
  and always use the layer to send metrics to the extension
57
57
  """
58
+ if not metric_name or not isinstance(metric_name, str):
59
+ logger.warning(
60
+ "Ignoring metric submission. Invalid metric name: %s", metric_name
61
+ )
62
+ return
63
+
64
+ try:
65
+ float(value)
66
+ except (ValueError, TypeError):
67
+ logger.warning(
68
+ "Ignoring metric submission for metric '%s' because the value is not numeric: %r",
69
+ metric_name,
70
+ value,
71
+ )
72
+ return
73
+
58
74
  flush_to_logs = os.environ.get("DD_FLUSH_TO_LOG", "").lower() == "true"
59
75
  tags = [] if tags is None else list(tags)
60
76
  tags.append(dd_lambda_layer_tag)
@@ -172,3 +188,17 @@ def submit_errors_metric(lambda_context):
172
188
  lambda_context (object): Lambda context dict passed to the function by AWS
173
189
  """
174
190
  submit_enhanced_metric("errors", lambda_context)
191
+
192
+
193
+ def submit_dynamodb_stream_type_metric(event):
194
+ stream_view_type = (
195
+ event.get("Records", [{}])[0].get("dynamodb", {}).get("StreamViewType")
196
+ )
197
+ if stream_view_type:
198
+ lambda_metric(
199
+ "datadog.serverless.dynamodb.stream.type",
200
+ 1,
201
+ timestamp=None,
202
+ tags=[f"streamtype:{stream_view_type}"],
203
+ force_async=True,
204
+ )
@@ -6,6 +6,8 @@ from typing import Optional
6
6
 
7
7
  from ddtrace._trace._span_pointer import _SpanPointerDirection
8
8
  from ddtrace._trace._span_pointer import _SpanPointerDescription
9
+
10
+ from datadog_lambda.metric import submit_dynamodb_stream_type_metric
9
11
  from datadog_lambda.trigger import EventTypes
10
12
 
11
13
 
@@ -28,6 +30,8 @@ def calculate_span_pointers(
28
30
  return _calculate_s3_span_pointers_for_event(event)
29
31
 
30
32
  elif event_source.equals(EventTypes.DYNAMODB):
33
+ # Temporary metric. TODO eventually remove(@nhulston)
34
+ submit_dynamodb_stream_type_metric(event)
31
35
  return _calculate_dynamodb_span_pointers_for_event(event)
32
36
 
33
37
  except Exception as e:
@@ -2,10 +2,8 @@
2
2
  # under the Apache License Version 2.0.
3
3
  # This product includes software developed at Datadog (https://www.datadoghq.com/).
4
4
  # Copyright 2019 Datadog, Inc.
5
- import hashlib
6
5
  import logging
7
6
  import os
8
- import base64
9
7
  import traceback
10
8
  import ujson as json
11
9
  from datetime import datetime, timezone
@@ -39,6 +37,7 @@ from datadog_lambda.trigger import (
39
37
  _EventSource,
40
38
  parse_event_source,
41
39
  get_first_record,
40
+ is_step_function_event,
42
41
  EventTypes,
43
42
  EventSubtypes,
44
43
  )
@@ -258,6 +257,8 @@ def extract_context_from_sqs_or_sns_event_or_context(event, lambda_context):
258
257
  dd_json_data = None
259
258
  dd_json_data_type = dd_payload.get("Type") or dd_payload.get("dataType")
260
259
  if dd_json_data_type == "Binary":
260
+ import base64
261
+
261
262
  dd_json_data = dd_payload.get("binaryValue") or dd_payload.get("Value")
262
263
  if dd_json_data:
263
264
  dd_json_data = base64.b64decode(dd_json_data)
@@ -271,6 +272,15 @@ def extract_context_from_sqs_or_sns_event_or_context(event, lambda_context):
271
272
 
272
273
  if dd_json_data:
273
274
  dd_data = json.loads(dd_json_data)
275
+
276
+ if is_step_function_event(dd_data):
277
+ try:
278
+ return extract_context_from_step_functions(dd_data, None)
279
+ except Exception:
280
+ logger.debug(
281
+ "Failed to extract Step Functions context from SQS/SNS event."
282
+ )
283
+
274
284
  return propagator.extract(dd_data)
275
285
  else:
276
286
  # Handle case where trace context is injected into attributes.AWSTraceHeader
@@ -313,6 +323,15 @@ def _extract_context_from_eventbridge_sqs_event(event):
313
323
  body = json.loads(body_str)
314
324
  detail = body.get("detail")
315
325
  dd_context = detail.get("_datadog")
326
+
327
+ if is_step_function_event(dd_context):
328
+ try:
329
+ return extract_context_from_step_functions(dd_context, None)
330
+ except Exception:
331
+ logger.debug(
332
+ "Failed to extract Step Functions context from EventBridge to SQS event."
333
+ )
334
+
316
335
  return propagator.extract(dd_context)
317
336
 
318
337
 
@@ -320,12 +339,23 @@ def extract_context_from_eventbridge_event(event, lambda_context):
320
339
  """
321
340
  Extract datadog trace context from an EventBridge message's Details.
322
341
  This is only possible if Details is a JSON string.
342
+
343
+ If we find a Step Function context, try to extract the trace context from
344
+ that header.
323
345
  """
324
346
  try:
325
347
  detail = event.get("detail")
326
348
  dd_context = detail.get("_datadog")
327
349
  if not dd_context:
328
350
  return extract_context_from_lambda_context(lambda_context)
351
+
352
+ try:
353
+ return extract_context_from_step_functions(dd_context, None)
354
+ except Exception:
355
+ logger.debug(
356
+ "Failed to extract Step Functions context from EventBridge event."
357
+ )
358
+
329
359
  return propagator.extract(dd_context)
330
360
  except Exception as e:
331
361
  logger.debug("The trace extractor returned with error %s", e)
@@ -343,6 +373,8 @@ def extract_context_from_kinesis_event(event, lambda_context):
343
373
  return extract_context_from_lambda_context(lambda_context)
344
374
  data = kinesis.get("data")
345
375
  if data:
376
+ import base64
377
+
346
378
  b64_bytes = data.encode("ascii")
347
379
  str_bytes = base64.b64decode(b64_bytes)
348
380
  data_str = str_bytes.decode("ascii")
@@ -357,6 +389,8 @@ def extract_context_from_kinesis_event(event, lambda_context):
357
389
 
358
390
 
359
391
  def _deterministic_sha256_hash(s: str, part: str) -> int:
392
+ import hashlib
393
+
360
394
  sha256_hash = hashlib.sha256(s.encode()).hexdigest()
361
395
  # First two chars is '0b'. zfill to ensure 256 bits, but we only care about the first 128 bits
362
396
  binary_hash = bin(int(sha256_hash, 16))[2:].zfill(256)
@@ -385,21 +419,24 @@ def _parse_high_64_bits(trace_tags: str) -> str:
385
419
 
386
420
  def _generate_sfn_parent_id(context: dict) -> int:
387
421
  """
388
- The upstream Step Function can propagate its execution context to downstream Lambdas. The
389
- Lambda can use these details to share the same traceID and infer its parent's spanID.
422
+ Generates a stable parent span ID for a downstream Lambda invoked by a Step Function. The
423
+ upstream Step Function execution context is used to infer the parent's span ID, ensuring trace
424
+ continuity.
390
425
 
391
- Excluding redriveCount when its 0 to account for cases where customers are using an old
392
- version of the Lambda layer that doesn't use this value for its parentID generation.
426
+ `RetryCount` and `RedriveCount` are appended only when both are nonzero to maintain
427
+ compatibility with older Lambda layers that did not include these fields.
393
428
  """
394
429
  execution_id = context.get("Execution").get("Id")
395
430
  redrive_count = context.get("Execution").get("RedriveCount", 0)
396
431
  state_name = context.get("State").get("Name")
397
432
  state_entered_time = context.get("State").get("EnteredTime")
433
+ retry_count = context.get("State").get("RetryCount", 0)
398
434
 
399
- redrive_postfix = "" if redrive_count == 0 else f"#{redrive_count}"
435
+ include_counts = not (retry_count == 0 and redrive_count == 0)
436
+ counts_suffix = f"#{retry_count}#{redrive_count}" if include_counts else ""
400
437
 
401
438
  return _deterministic_sha256_hash(
402
- f"{execution_id}#{state_name}#{state_entered_time}{redrive_postfix}",
439
+ f"{execution_id}#{state_name}#{state_entered_time}{counts_suffix}",
403
440
  HIGHER_64_BITS,
404
441
  )
405
442
 
@@ -421,7 +458,7 @@ def _generate_sfn_trace_id(execution_id: str, part: str):
421
458
  def extract_context_from_step_functions(event, lambda_context):
422
459
  """
423
460
  Only extract datadog trace context when Step Functions Context Object is injected
424
- into lambda's event dict.
461
+ into lambda's event dict. Unwrap "Payload" if it exists to handle Legacy Lambda cases.
425
462
 
426
463
  If '_datadog' header is present, we have two cases:
427
464
  1. Root is a Lambda and we use its traceID
@@ -432,25 +469,25 @@ def extract_context_from_step_functions(event, lambda_context):
432
469
  object.
433
470
  """
434
471
  try:
472
+ event = event.get("Payload", event)
473
+ event = event.get("_datadog", event)
474
+
435
475
  meta = {}
436
- dd_data = event.get("_datadog")
437
476
 
438
- if dd_data and dd_data.get("serverless-version") == "v1":
439
- if "x-datadog-trace-id" in dd_data: # lambda root
440
- trace_id = int(dd_data.get("x-datadog-trace-id"))
441
- high_64_bit_trace_id = _parse_high_64_bits(
442
- dd_data.get("x-datadog-tags")
443
- )
477
+ if event.get("serverless-version") == "v1":
478
+ if "x-datadog-trace-id" in event: # lambda root
479
+ trace_id = int(event.get("x-datadog-trace-id"))
480
+ high_64_bit_trace_id = _parse_high_64_bits(event.get("x-datadog-tags"))
444
481
  if high_64_bit_trace_id:
445
482
  meta["_dd.p.tid"] = high_64_bit_trace_id
446
483
  else: # sfn root
447
- root_execution_id = dd_data.get("RootExecutionId")
484
+ root_execution_id = event.get("RootExecutionId")
448
485
  trace_id = _generate_sfn_trace_id(root_execution_id, LOWER_64_BITS)
449
486
  meta["_dd.p.tid"] = _generate_sfn_trace_id(
450
487
  root_execution_id, HIGHER_64_BITS
451
488
  )
452
489
 
453
- parent_id = _generate_sfn_parent_id(dd_data)
490
+ parent_id = _generate_sfn_parent_id(event)
454
491
  else:
455
492
  execution_id = event.get("Execution").get("Id")
456
493
  trace_id = _generate_sfn_trace_id(execution_id, LOWER_64_BITS)
@@ -469,20 +506,6 @@ def extract_context_from_step_functions(event, lambda_context):
469
506
  return extract_context_from_lambda_context(lambda_context)
470
507
 
471
508
 
472
- def is_legacy_lambda_step_function(event):
473
- """
474
- Check if the event is a step function that called a legacy lambda
475
- """
476
- if not isinstance(event, dict) or "Payload" not in event:
477
- return False
478
-
479
- event = event.get("Payload")
480
- return isinstance(event, dict) and (
481
- "_datadog" in event
482
- or ("Execution" in event and "StateMachine" in event and "State" in event)
483
- )
484
-
485
-
486
509
  def extract_context_custom_extractor(extractor, event, lambda_context):
487
510
  """
488
511
  Extract Datadog trace context using a custom trace extractor function
@@ -532,6 +555,8 @@ def get_injected_authorizer_data(event, is_http_api) -> dict:
532
555
  if not dd_data_raw:
533
556
  return None
534
557
 
558
+ import base64
559
+
535
560
  injected_data = json.loads(base64.b64decode(dd_data_raw))
536
561
 
537
562
  # Lambda authorizer's results can be cached. But the payload will still have the injected
@@ -1306,8 +1331,18 @@ def create_inferred_span_from_eventbridge_event(event, context):
1306
1331
  synchronicity="async",
1307
1332
  tag_source="self",
1308
1333
  )
1309
- dt_format = "%Y-%m-%dT%H:%M:%SZ"
1334
+
1310
1335
  timestamp = event.get("time")
1336
+ dt_format = "%Y-%m-%dT%H:%M:%SZ"
1337
+
1338
+ # Use more granular timestamp from upstream Step Function if possible
1339
+ try:
1340
+ if is_step_function_event(event.get("detail")):
1341
+ timestamp = event["detail"]["_datadog"]["State"]["EnteredTime"]
1342
+ dt_format = "%Y-%m-%dT%H:%M:%S.%fZ"
1343
+ except (TypeError, KeyError, AttributeError):
1344
+ logger.debug("Error parsing timestamp from Step Functions event")
1345
+
1311
1346
  dt = datetime.strptime(timestamp, dt_format)
1312
1347
 
1313
1348
  tracer.set_tags(_dd_origin)
@@ -1317,6 +1352,11 @@ def create_inferred_span_from_eventbridge_event(event, context):
1317
1352
  if span:
1318
1353
  span.set_tags(tags)
1319
1354
  span.start = dt.replace(tzinfo=timezone.utc).timestamp()
1355
+
1356
+ # Since inferred span will later parent Lambda, preserve Lambda's current parent
1357
+ if dd_trace_context.span_id:
1358
+ span.parent_id = dd_trace_context.span_id
1359
+
1320
1360
  return span
1321
1361
 
1322
1362
 
@@ -1368,8 +1408,9 @@ def create_function_execution_span(
1368
1408
  if parent_span:
1369
1409
  span.parent_id = parent_span.span_id
1370
1410
  if span_pointers:
1411
+ root_span = parent_span if parent_span else span
1371
1412
  for span_pointer_description in span_pointers:
1372
- span._add_span_pointer(
1413
+ root_span._add_span_pointer(
1373
1414
  pointer_kind=span_pointer_description.pointer_kind,
1374
1415
  pointer_direction=span_pointer_description.pointer_direction,
1375
1416
  pointer_hash=span_pointer_description.pointer_hash,
@@ -3,7 +3,6 @@
3
3
  # This product includes software developed at Datadog (https://www.datadoghq.com/).
4
4
  # Copyright 2019 Datadog, Inc.
5
5
 
6
- import base64
7
6
  import gzip
8
7
  import ujson as json
9
8
  from io import BytesIO, BufferedReader
@@ -146,9 +145,7 @@ def parse_event_source(event: dict) -> _EventSource:
146
145
  if event.get("source") == "aws.events" or has_event_categories:
147
146
  event_source = _EventSource(EventTypes.CLOUDWATCH_EVENTS)
148
147
 
149
- if (
150
- "_datadog" in event and event.get("_datadog").get("serverless-version") == "v1"
151
- ) or ("Execution" in event and "StateMachine" in event and "State" in event):
148
+ if is_step_function_event(event):
152
149
  event_source = _EventSource(EventTypes.STEPFUNCTIONS)
153
150
 
154
151
  event_record = get_first_record(event)
@@ -244,6 +241,8 @@ def parse_event_source_arn(source: _EventSource, event: dict, context: Any) -> s
244
241
 
245
242
  # e.g. arn:aws:logs:us-west-1:123456789012:log-group:/my-log-group-xyz
246
243
  if source.event_type == EventTypes.CLOUDWATCH_LOGS:
244
+ import base64
245
+
247
246
  with gzip.GzipFile(
248
247
  fileobj=BytesIO(base64.b64decode(event.get("awslogs", {}).get("data")))
249
248
  ) as decompress_stream:
@@ -369,3 +368,29 @@ def extract_http_status_code_tag(trigger_tags, response):
369
368
  status_code = response.status_code
370
369
 
371
370
  return str(status_code)
371
+
372
+
373
+ def is_step_function_event(event):
374
+ """
375
+ Check if the event is a step function that invoked the current lambda.
376
+
377
+ The whole event can be wrapped in "Payload" in Legacy Lambda cases. There may also be a
378
+ "_datadog" for JSONata style context propagation.
379
+
380
+ The actual event must contain "Execution", "StateMachine", and "State" fields.
381
+ """
382
+ event = event.get("Payload", event)
383
+
384
+ # JSONPath style
385
+ if "Execution" in event and "StateMachine" in event and "State" in event:
386
+ return True
387
+
388
+ # JSONata style
389
+ dd_context = event.get("_datadog")
390
+ return (
391
+ dd_context
392
+ and "Execution" in dd_context
393
+ and "StateMachine" in dd_context
394
+ and "State" in dd_context
395
+ and "serverless-version" in dd_context
396
+ )
@@ -0,0 +1 @@
1
+ __version__ = "6.107.0"
@@ -2,7 +2,6 @@
2
2
  # under the Apache License Version 2.0.
3
3
  # This product includes software developed at Datadog (https://www.datadoghq.com/).
4
4
  # Copyright 2019 Datadog, Inc.
5
- import base64
6
5
  import os
7
6
  import logging
8
7
  import traceback
@@ -23,11 +22,6 @@ from datadog_lambda.constants import (
23
22
  XraySubsegment,
24
23
  Headers,
25
24
  )
26
- from datadog_lambda.metric import (
27
- flush_stats,
28
- submit_invocations_metric,
29
- submit_errors_metric,
30
- )
31
25
  from datadog_lambda.module_name import modify_module_name
32
26
  from datadog_lambda.patch import patch_all
33
27
  from datadog_lambda.span_pointers import calculate_span_pointers
@@ -45,7 +39,6 @@ from datadog_lambda.tracing import (
45
39
  is_authorizer_response,
46
40
  tracer,
47
41
  propagator,
48
- is_legacy_lambda_step_function,
49
42
  )
50
43
  from datadog_lambda.trigger import (
51
44
  extract_trigger_tags,
@@ -56,10 +49,14 @@ profiling_env_var = os.environ.get("DD_PROFILING_ENABLED", "false").lower() == "
56
49
  if profiling_env_var:
57
50
  from ddtrace.profiling import profiler
58
51
 
52
+ llmobs_api_key = None
59
53
  llmobs_env_var = os.environ.get("DD_LLMOBS_ENABLED", "false").lower() in ("true", "1")
60
54
  if llmobs_env_var:
55
+ from datadog_lambda.api import get_api_key
61
56
  from ddtrace.llmobs import LLMObs
62
57
 
58
+ llmobs_api_key = get_api_key()
59
+
63
60
  logger = logging.getLogger(__name__)
64
61
 
65
62
  DD_FLUSH_TO_LOG = "DD_FLUSH_TO_LOG"
@@ -229,7 +226,10 @@ class _LambdaDecorator(object):
229
226
 
230
227
  # Enable LLM Observability
231
228
  if llmobs_env_var:
232
- LLMObs.enable()
229
+ LLMObs.enable(
230
+ agentless_enabled=True,
231
+ api_key=llmobs_api_key,
232
+ )
233
233
 
234
234
  logger.debug("datadog_lambda_wrapper initialized")
235
235
  except Exception as e:
@@ -242,7 +242,11 @@ class _LambdaDecorator(object):
242
242
  self.response = self.func(event, context, **kwargs)
243
243
  return self.response
244
244
  except Exception:
245
- submit_errors_metric(context)
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
- submit_invocations_metric(context)
282
- if is_legacy_lambda_step_function(event):
283
- event = event["Payload"]
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.105.0"
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
- 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.105.0"