datadog_lambda 8.125.0__tar.gz → 8.127.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 (32) hide show
  1. {datadog_lambda-8.125.0 → datadog_lambda-8.127.0}/PKG-INFO +1 -1
  2. {datadog_lambda-8.125.0 → datadog_lambda-8.127.0}/datadog_lambda/durable.py +62 -5
  3. {datadog_lambda-8.125.0 → datadog_lambda-8.127.0}/datadog_lambda/tracing.py +83 -9
  4. {datadog_lambda-8.125.0 → datadog_lambda-8.127.0}/datadog_lambda/trigger.py +55 -7
  5. datadog_lambda-8.127.0/datadog_lambda/version.py +1 -0
  6. {datadog_lambda-8.125.0 → datadog_lambda-8.127.0}/datadog_lambda/wrapper.py +4 -4
  7. {datadog_lambda-8.125.0 → datadog_lambda-8.127.0}/pyproject.toml +1 -1
  8. datadog_lambda-8.125.0/datadog_lambda/version.py +0 -1
  9. {datadog_lambda-8.125.0 → datadog_lambda-8.127.0}/LICENSE +0 -0
  10. {datadog_lambda-8.125.0 → datadog_lambda-8.127.0}/LICENSE-3rdparty.csv +0 -0
  11. {datadog_lambda-8.125.0 → datadog_lambda-8.127.0}/NOTICE +0 -0
  12. {datadog_lambda-8.125.0 → datadog_lambda-8.127.0}/README.md +0 -0
  13. {datadog_lambda-8.125.0 → datadog_lambda-8.127.0}/datadog_lambda/__init__.py +0 -0
  14. {datadog_lambda-8.125.0 → datadog_lambda-8.127.0}/datadog_lambda/api.py +0 -0
  15. {datadog_lambda-8.125.0 → datadog_lambda-8.127.0}/datadog_lambda/asm.py +0 -0
  16. {datadog_lambda-8.125.0 → datadog_lambda-8.127.0}/datadog_lambda/cold_start.py +0 -0
  17. {datadog_lambda-8.125.0 → datadog_lambda-8.127.0}/datadog_lambda/config.py +0 -0
  18. {datadog_lambda-8.125.0 → datadog_lambda-8.127.0}/datadog_lambda/constants.py +0 -0
  19. {datadog_lambda-8.125.0 → datadog_lambda-8.127.0}/datadog_lambda/dogstatsd.py +0 -0
  20. {datadog_lambda-8.125.0 → datadog_lambda-8.127.0}/datadog_lambda/extension.py +0 -0
  21. {datadog_lambda-8.125.0 → datadog_lambda-8.127.0}/datadog_lambda/handler.py +0 -0
  22. {datadog_lambda-8.125.0 → datadog_lambda-8.127.0}/datadog_lambda/logger.py +0 -0
  23. {datadog_lambda-8.125.0 → datadog_lambda-8.127.0}/datadog_lambda/metric.py +0 -0
  24. {datadog_lambda-8.125.0 → datadog_lambda-8.127.0}/datadog_lambda/module_name.py +0 -0
  25. {datadog_lambda-8.125.0 → datadog_lambda-8.127.0}/datadog_lambda/patch.py +0 -0
  26. {datadog_lambda-8.125.0 → datadog_lambda-8.127.0}/datadog_lambda/span_pointers.py +0 -0
  27. {datadog_lambda-8.125.0 → datadog_lambda-8.127.0}/datadog_lambda/stats_writer.py +0 -0
  28. {datadog_lambda-8.125.0 → datadog_lambda-8.127.0}/datadog_lambda/statsd_writer.py +0 -0
  29. {datadog_lambda-8.125.0 → datadog_lambda-8.127.0}/datadog_lambda/tag_object.py +0 -0
  30. {datadog_lambda-8.125.0 → datadog_lambda-8.127.0}/datadog_lambda/tags.py +0 -0
  31. {datadog_lambda-8.125.0 → datadog_lambda-8.127.0}/datadog_lambda/thread_stats_writer.py +0 -0
  32. {datadog_lambda-8.125.0 → datadog_lambda-8.127.0}/datadog_lambda/xray.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: datadog_lambda
3
- Version: 8.125.0
3
+ Version: 8.127.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
@@ -4,9 +4,12 @@
4
4
  # Copyright 2019 Datadog, Inc.
5
5
  import logging
6
6
  import re
7
+ import ujson as json
7
8
 
8
9
  logger = logging.getLogger(__name__)
9
10
 
11
+ _TRACE_CHECKPOINT_PREFIX = "_datadog_"
12
+
10
13
 
11
14
  def _parse_durable_execution_arn(arn):
12
15
  """
@@ -47,17 +50,71 @@ def extract_durable_function_tags(event):
47
50
  operations = event.get("InitialExecutionState", {}).get("Operations", [])
48
51
  is_first_invocation = len(operations) == 1
49
52
  return {
50
- "aws_lambda.durable_function.execution_name": execution_name,
51
- "aws_lambda.durable_function.execution_id": execution_id,
52
- "aws_lambda.durable_function.first_invocation": str(
53
- is_first_invocation
54
- ).lower(),
53
+ "aws.durable.execution_name": execution_name,
54
+ "aws.durable.execution_id": execution_id,
55
+ "aws.durable.first_invocation": str(is_first_invocation).lower(),
55
56
  }
56
57
 
57
58
 
58
59
  VALID_DURABLE_STATUSES = {"SUCCEEDED", "FAILED", "PENDING"}
59
60
 
60
61
 
62
+ def _extract_context_from_durable_checkpoint(operation):
63
+ # Checkpoint data is written by the dd-trace-py in Datadog style
64
+ # (x-datadog-* headers). Extraction goes through the standard
65
+ # propagator.extract path, which honors DD_TRACE_PROPAGATION_STYLE_EXTRACT.
66
+ # The default extract list (datadog, tracecontext, baggage) already
67
+ # includes datadog. Customers who override the extract list MUST keep
68
+ # datadog in it.
69
+ if not isinstance(operation, dict):
70
+ return None
71
+
72
+ step_details = operation.get("StepDetails")
73
+ if not isinstance(step_details, dict):
74
+ return None
75
+
76
+ result = step_details.get("Result")
77
+ if isinstance(result, str):
78
+ try:
79
+ result = json.loads(result)
80
+ except Exception:
81
+ return None
82
+
83
+ if not isinstance(result, dict):
84
+ return None
85
+
86
+ from datadog_lambda.tracing import propagator
87
+
88
+ return propagator.extract(result)
89
+
90
+
91
+ def extract_context_from_durable_execution(event):
92
+ operations = event.get("InitialExecutionState", {}).get("Operations")
93
+ if isinstance(operations, dict):
94
+ operations = list(operations.values())
95
+ if not isinstance(operations, list) or not operations:
96
+ return None
97
+
98
+ highest = -1
99
+ best_operation = None
100
+ for operation in operations:
101
+ if not isinstance(operation, dict):
102
+ continue
103
+ name = operation.get("Name")
104
+ if not isinstance(name, str) or not name.startswith(_TRACE_CHECKPOINT_PREFIX):
105
+ continue
106
+ suffix = name[len(_TRACE_CHECKPOINT_PREFIX) :]
107
+ try:
108
+ number = int(suffix)
109
+ except (TypeError, ValueError):
110
+ continue
111
+ if number > highest:
112
+ highest = number
113
+ best_operation = operation
114
+
115
+ return _extract_context_from_durable_checkpoint(best_operation)
116
+
117
+
61
118
  def extract_durable_execution_status(response, event):
62
119
  if not isinstance(event, dict) or "DurableExecutionArn" not in event:
63
120
  return None
@@ -43,7 +43,9 @@ from datadog_lambda.trigger import (
43
43
  is_step_function_event,
44
44
  EventTypes,
45
45
  EventSubtypes,
46
+ resolve_multivalue_headers,
46
47
  )
48
+ from datadog_lambda.durable import extract_context_from_durable_execution
47
49
 
48
50
  if config.otel_enabled:
49
51
  from opentelemetry.trace import set_tracer_provider
@@ -195,7 +197,7 @@ def extract_context_from_http_event_or_context(
195
197
  if _is_context_complete(context):
196
198
  return context
197
199
 
198
- headers = event.get("headers")
200
+ headers = resolve_multivalue_headers(event)
199
201
  context = propagator.extract(headers)
200
202
 
201
203
  if not _is_context_complete(context):
@@ -481,6 +483,21 @@ def _generate_sfn_parent_id(context: dict) -> int:
481
483
  )
482
484
 
483
485
 
486
+ def _extract_dd_trace_id_from_dd_data(dd_data, meta):
487
+ """
488
+ Read native Datadog trace headers from a `_datadog` dict.
489
+ Returns the 64-bit trace_id and populates meta["_dd.p.tid"] when present.
490
+ Returns None if no usable trace id is found.
491
+ """
492
+ if not dd_data or "x-datadog-trace-id" not in dd_data:
493
+ return None
494
+ trace_id = int(dd_data.get("x-datadog-trace-id"))
495
+ high_64_bit_trace_id = _parse_high_64_bits(dd_data.get("x-datadog-tags"))
496
+ if high_64_bit_trace_id:
497
+ meta["_dd.p.tid"] = high_64_bit_trace_id
498
+ return trace_id
499
+
500
+
484
501
  def _generate_sfn_trace_id(execution_id: str, part: str):
485
502
  """
486
503
  Take the SHA-256 hash of the execution_id to calculate the trace ID. If the high 64 bits are
@@ -516,10 +533,7 @@ def extract_context_from_step_functions(event, lambda_context):
516
533
 
517
534
  if event.get("serverless-version") == "v1":
518
535
  if "x-datadog-trace-id" in event: # lambda root
519
- trace_id = int(event.get("x-datadog-trace-id"))
520
- high_64_bit_trace_id = _parse_high_64_bits(event.get("x-datadog-tags"))
521
- if high_64_bit_trace_id:
522
- meta["_dd.p.tid"] = high_64_bit_trace_id
536
+ trace_id = _extract_dd_trace_id_from_dd_data(event, meta)
523
537
  else: # sfn root
524
538
  root_execution_id = event.get("RootExecutionId")
525
539
  trace_id = _generate_sfn_trace_id(root_execution_id, LOWER_64_BITS)
@@ -529,9 +543,13 @@ def extract_context_from_step_functions(event, lambda_context):
529
543
 
530
544
  parent_id = _generate_sfn_parent_id(event)
531
545
  else:
532
- execution_id = event.get("Execution").get("Id")
533
- trace_id = _generate_sfn_trace_id(execution_id, LOWER_64_BITS)
534
- meta["_dd.p.tid"] = _generate_sfn_trace_id(execution_id, HIGHER_64_BITS)
546
+ execution = event.get("Execution", {})
547
+ dd_input = execution.get("Input", {}).get("_datadog")
548
+ trace_id = _extract_dd_trace_id_from_dd_data(dd_input, meta)
549
+ if trace_id is None:
550
+ execution_id = execution.get("Id")
551
+ trace_id = _generate_sfn_trace_id(execution_id, LOWER_64_BITS)
552
+ meta["_dd.p.tid"] = _generate_sfn_trace_id(execution_id, HIGHER_64_BITS)
535
553
  parent_id = _generate_sfn_parent_id(event)
536
554
 
537
555
  sampling_priority = SamplingPriority.AUTO_KEEP
@@ -633,6 +651,7 @@ def extract_dd_trace_context(
633
651
  global dd_trace_context
634
652
  trace_context_source = None
635
653
  event_source = parse_event_source(event)
654
+ context = None
636
655
 
637
656
  if extractor is not None:
638
657
  context = extract_context_custom_extractor(extractor, event, lambda_context)
@@ -640,7 +659,9 @@ def extract_dd_trace_context(
640
659
  context = extract_context_from_request_header_or_context(
641
660
  event, lambda_context, event_source
642
661
  )
643
- elif isinstance(event, (set, dict)) and "headers" in event:
662
+ elif isinstance(event, (set, dict)) and (
663
+ "headers" in event or "multiValueHeaders" in event
664
+ ):
644
665
  context = extract_context_from_http_event_or_context(
645
666
  event, lambda_context, event_source, decode_authorizer_context
646
667
  )
@@ -654,6 +675,8 @@ def extract_dd_trace_context(
654
675
  context = extract_context_from_kinesis_event(event, lambda_context)
655
676
  elif event_source.equals(EventTypes.STEPFUNCTIONS):
656
677
  context = extract_context_from_step_functions(event, lambda_context)
678
+ elif isinstance(event, dict) and "DurableExecutionArn" in event:
679
+ context = extract_context_from_durable_execution(event)
657
680
  else:
658
681
  context = extract_context_from_lambda_context(lambda_context)
659
682
 
@@ -817,6 +840,9 @@ def create_inferred_span(
817
840
  elif event_source.equals(EventTypes.LAMBDA_FUNCTION_URL):
818
841
  logger.debug("Function URL event detected. Inferring a span")
819
842
  return create_inferred_span_from_lambda_function_url_event(event, context)
843
+ elif event_source.event_type == EventTypes.ALB:
844
+ logger.debug("ALB event detected. Inferring a span")
845
+ return create_inferred_span_from_alb_event(event, context)
820
846
  elif event_source.equals(
821
847
  EventTypes.API_GATEWAY, subtype=EventSubtypes.HTTP_API
822
848
  ):
@@ -932,6 +958,54 @@ def create_inferred_span_from_lambda_function_url_event(event, context):
932
958
  return span
933
959
 
934
960
 
961
+ def create_inferred_span_from_alb_event(event, context):
962
+ request_context = event.get("requestContext") or {}
963
+ elb = request_context.get("elb") or {}
964
+ target_group_arn = elb.get("targetGroupArn")
965
+
966
+ headers = resolve_multivalue_headers(event)
967
+ host = headers.get("host")
968
+ method = event.get("httpMethod")
969
+ path = event.get("path")
970
+ proto = headers.get("x-forwarded-proto", "http")
971
+
972
+ # ALB has no api id; key the service mapping off the load-balancer host and
973
+ # fall back to it when DD_TRACE_AWS_SERVICE_REPRESENTATION_ENABLED is on.
974
+ service_name = determine_service_name(service_mapping, host, "lambda_alb", host)
975
+
976
+ http_url = "%s://%s%s" % (proto, host, path) if host and path is not None else None
977
+ if method and path is not None:
978
+ resource = f"{method} {path}"
979
+ else:
980
+ resource = method or path
981
+
982
+ tags = {
983
+ "operation_name": "aws.alb",
984
+ "span.kind": "server",
985
+ "http.method": method,
986
+ "http.url": http_url,
987
+ "http.useragent": headers.get("user-agent"),
988
+ "endpoint": path,
989
+ "resource_names": resource,
990
+ "request_id": context.aws_request_id,
991
+ "target_group_arn": target_group_arn,
992
+ }
993
+ # Drop tags we couldn't derive so the span never carries malformed values.
994
+ tags = {key: value for key, value in tags.items() if value is not None}
995
+
996
+ tracer.set_tags(_dd_origin)
997
+ # ALB events carry no request timestamp (unlike API GW requestTimeEpoch /
998
+ # Function URL timeEpoch), so the span starts at handler time.
999
+ span = tracer.trace(
1000
+ "aws.alb", service=service_name, resource=resource, span_type="http"
1001
+ )
1002
+ InferredSpanInfo.set_tags(tags, tag_source="self", synchronicity="sync")
1003
+ if span:
1004
+ span.set_tags(tags)
1005
+ span.set_metric(InferredSpanInfo.METRIC, 1.0)
1006
+ return span
1007
+
1008
+
935
1009
  def is_api_gateway_invocation_async(event):
936
1010
  hdrs = event.get("headers")
937
1011
  if not hdrs:
@@ -293,6 +293,39 @@ def get_event_source_arn(source: _EventSource, event: dict, context: Any) -> str
293
293
  return event_source_arn
294
294
 
295
295
 
296
+ def resolve_multivalue_headers(event):
297
+ """
298
+ Resolve request headers from single-value ``headers`` or ``multiValueHeaders``
299
+ (first value per key, matching datadog-lambda-js).
300
+ """
301
+ headers = event.get("headers")
302
+ if isinstance(headers, dict) and headers:
303
+ return headers
304
+
305
+ multi_value = event.get("multiValueHeaders")
306
+ if not isinstance(multi_value, dict):
307
+ return {}
308
+
309
+ resolved = {}
310
+ for key, value in multi_value.items():
311
+ if isinstance(value, list):
312
+ if value:
313
+ resolved[key] = value[0]
314
+ elif isinstance(value, str):
315
+ resolved[key] = value
316
+ return resolved
317
+
318
+
319
+ def _get_header_case_insensitive(headers, name):
320
+ if not isinstance(headers, dict):
321
+ return None
322
+ name_lower = name.lower()
323
+ for key, value in headers.items():
324
+ if isinstance(key, str) and key.lower() == name_lower and value:
325
+ return value
326
+ return None
327
+
328
+
296
329
  def extract_http_tags(event):
297
330
  """
298
331
  Extracts HTTP facet tags from the triggering event
@@ -324,19 +357,32 @@ def extract_http_tags(event):
324
357
  path = apigateway_v2_http.get("path")
325
358
  method = apigateway_v2_http.get("method")
326
359
 
360
+ elif request_context and request_context.get("elb"):
361
+ # ALB events have no requestContext.stage; derive the URL from the
362
+ # forwarded host/proto headers and the top-level path.
363
+ alb_headers = resolve_multivalue_headers(event)
364
+ host = alb_headers.get("host")
365
+ if host:
366
+ proto = alb_headers.get("x-forwarded-proto", "http")
367
+ http_tags["http.url"] = proto + "://" + host
368
+
369
+ user_agent = alb_headers.get("user-agent")
370
+ if user_agent:
371
+ http_tags["http.useragent"] = user_agent
372
+
373
+ # ALB carries no route template, so use the request path as the route.
374
+ if path:
375
+ http_tags["http.route"] = path
376
+
327
377
  if path:
328
378
  if http_tags.get("http.url"):
329
379
  http_tags["http.url"] += path
330
380
  if method:
331
381
  http_tags["http.method"] = method
332
382
 
333
- # Safely get headers
334
- headers = event.get("headers", {})
335
- if not isinstance(headers, dict):
336
- headers = {}
337
-
338
- if headers and headers.get("Referer"):
339
- http_tags["http.referer"] = headers.get("Referer")
383
+ referer = _get_header_case_insensitive(resolve_multivalue_headers(event), "referer")
384
+ if referer:
385
+ http_tags["http.referer"] = referer
340
386
 
341
387
  # Try to get `routeKey` from API GW v2; otherwise try to get `resource` from API GW v1
342
388
  route = event.get("routeKey") or event.get("resource")
@@ -415,6 +461,8 @@ def is_step_function_event(event):
415
461
  The actual event must contain "Execution", "StateMachine", and "State" fields.
416
462
  """
417
463
  event = event.get("Payload", event)
464
+ if not isinstance(event, dict):
465
+ return False
418
466
 
419
467
  # JSONPath style
420
468
  if "Execution" in event and "StateMachine" in event and "State" in event:
@@ -0,0 +1 @@
1
+ __version__ = "8.127.0"
@@ -9,7 +9,6 @@ import ujson as json
9
9
  from importlib import import_module
10
10
  from time import time_ns
11
11
 
12
- from ddtrace.internal._exceptions import BlockingException
13
12
  from datadog_lambda.extension import should_use_extension, flush_extension
14
13
  from datadog_lambda.cold_start import (
15
14
  set_cold_start,
@@ -191,8 +190,6 @@ class _LambdaDecorator(object):
191
190
  if self.blocking_response:
192
191
  return self.blocking_response
193
192
  self.response = self.func(event, context, **kwargs)
194
- except BlockingException:
195
- self.blocking_response = get_asm_blocked_response(self.event_source)
196
193
  except Exception:
197
194
  from datadog_lambda.metric import submit_errors_metric
198
195
 
@@ -201,6 +198,9 @@ class _LambdaDecorator(object):
201
198
  if self.span:
202
199
  self.span.set_traceback()
203
200
  raise
201
+ except BaseException as e:
202
+ if "BlockingException" in type(e).__name__:
203
+ self.blocking_response = get_asm_blocked_response(self.event_source)
204
204
  finally:
205
205
  self._after(event, context)
206
206
  if self.blocking_response:
@@ -346,7 +346,7 @@ class _LambdaDecorator(object):
346
346
  durable_status = extract_durable_execution_status(self.response, event)
347
347
  if durable_status:
348
348
  self.span.set_tag(
349
- "aws_lambda.durable_function.execution_status",
349
+ "aws.durable.execution_status",
350
350
  durable_status,
351
351
  )
352
352
 
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "datadog_lambda"
3
- version = "8.125.0"
3
+ version = "8.127.0"
4
4
  description = "The Datadog AWS Lambda Library"
5
5
  authors = ["Datadog, Inc. <dev@datadoghq.com>"]
6
6
  license = "Apache-2.0"
@@ -1 +0,0 @@
1
- __version__ = "8.125.0"