ddtrace 3.11.0rc1__cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl → 3.11.0rc3__cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl

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 (159) hide show
  1. ddtrace/_logger.py +5 -6
  2. ddtrace/_trace/product.py +1 -1
  3. ddtrace/_trace/sampling_rule.py +25 -33
  4. ddtrace/_trace/trace_handlers.py +12 -50
  5. ddtrace/_trace/utils_botocore/span_tags.py +48 -0
  6. ddtrace/_version.py +2 -2
  7. ddtrace/appsec/_asm_request_context.py +3 -1
  8. ddtrace/appsec/_constants.py +7 -0
  9. ddtrace/appsec/_handlers.py +11 -0
  10. ddtrace/appsec/_iast/_listener.py +12 -2
  11. ddtrace/appsec/_processor.py +1 -1
  12. ddtrace/contrib/integration_registry/registry.yaml +10 -0
  13. ddtrace/contrib/internal/aiobotocore/patch.py +8 -0
  14. ddtrace/contrib/internal/avro/__init__.py +17 -0
  15. ddtrace/contrib/internal/azure_functions/patch.py +23 -12
  16. ddtrace/contrib/internal/azure_functions/utils.py +14 -0
  17. ddtrace/contrib/internal/boto/patch.py +14 -0
  18. ddtrace/contrib/internal/botocore/__init__.py +153 -0
  19. ddtrace/contrib/internal/botocore/services/bedrock.py +3 -27
  20. ddtrace/contrib/internal/django/patch.py +31 -8
  21. ddtrace/contrib/{_freezegun.py → internal/freezegun/__init__.py} +1 -1
  22. ddtrace/contrib/internal/google_genai/_utils.py +2 -2
  23. ddtrace/contrib/internal/google_genai/patch.py +7 -7
  24. ddtrace/contrib/internal/google_generativeai/patch.py +7 -5
  25. ddtrace/contrib/internal/langchain/patch.py +11 -443
  26. ddtrace/contrib/internal/langchain/utils.py +0 -26
  27. ddtrace/contrib/internal/logbook/patch.py +1 -2
  28. ddtrace/contrib/internal/logging/patch.py +4 -7
  29. ddtrace/contrib/internal/loguru/patch.py +1 -3
  30. ddtrace/contrib/internal/openai_agents/patch.py +44 -1
  31. ddtrace/contrib/internal/protobuf/__init__.py +17 -0
  32. ddtrace/contrib/internal/pytest/__init__.py +62 -0
  33. ddtrace/contrib/internal/pytest/_plugin_v2.py +13 -4
  34. ddtrace/contrib/internal/pytest_bdd/__init__.py +23 -0
  35. ddtrace/contrib/internal/pytest_benchmark/__init__.py +3 -0
  36. ddtrace/contrib/internal/structlog/patch.py +2 -4
  37. ddtrace/contrib/internal/unittest/__init__.py +36 -0
  38. ddtrace/contrib/internal/vertexai/patch.py +7 -5
  39. ddtrace/ext/ci.py +20 -0
  40. ddtrace/ext/git.py +66 -11
  41. ddtrace/internal/_encoding.cpython-313-i386-linux-gnu.so +0 -0
  42. ddtrace/internal/_encoding.pyi +1 -1
  43. ddtrace/internal/ci_visibility/encoder.py +126 -49
  44. ddtrace/internal/ci_visibility/utils.py +4 -4
  45. ddtrace/internal/core/__init__.py +5 -2
  46. ddtrace/internal/endpoints.py +76 -0
  47. ddtrace/internal/schema/processor.py +6 -2
  48. ddtrace/internal/telemetry/writer.py +18 -0
  49. ddtrace/internal/test_visibility/coverage_lines.py +4 -4
  50. ddtrace/internal/writer/writer.py +24 -11
  51. ddtrace/llmobs/_constants.py +3 -0
  52. ddtrace/llmobs/_experiment.py +75 -10
  53. ddtrace/llmobs/_integrations/bedrock.py +4 -0
  54. ddtrace/llmobs/_integrations/bedrock_agents.py +5 -1
  55. ddtrace/llmobs/_integrations/crewai.py +52 -3
  56. ddtrace/llmobs/_integrations/gemini.py +7 -7
  57. ddtrace/llmobs/_integrations/google_genai.py +10 -10
  58. ddtrace/llmobs/_integrations/{google_genai_utils.py → google_utils.py} +103 -7
  59. ddtrace/llmobs/_integrations/langchain.py +29 -20
  60. ddtrace/llmobs/_integrations/openai_agents.py +145 -0
  61. ddtrace/llmobs/_integrations/pydantic_ai.py +67 -26
  62. ddtrace/llmobs/_integrations/utils.py +68 -158
  63. ddtrace/llmobs/_integrations/vertexai.py +8 -8
  64. ddtrace/llmobs/_llmobs.py +83 -14
  65. ddtrace/llmobs/_telemetry.py +20 -5
  66. ddtrace/llmobs/_utils.py +27 -0
  67. ddtrace/settings/_config.py +1 -2
  68. ddtrace/settings/asm.py +9 -2
  69. ddtrace/settings/profiling.py +0 -9
  70. {ddtrace-3.11.0rc1.dist-info → ddtrace-3.11.0rc3.dist-info}/METADATA +1 -1
  71. {ddtrace-3.11.0rc1.dist-info → ddtrace-3.11.0rc3.dist-info}/RECORD +154 -160
  72. ddtrace/contrib/_avro.py +0 -17
  73. ddtrace/contrib/_botocore.py +0 -153
  74. ddtrace/contrib/_protobuf.py +0 -17
  75. ddtrace/contrib/_pytest.py +0 -62
  76. ddtrace/contrib/_pytest_bdd.py +0 -23
  77. ddtrace/contrib/_pytest_benchmark.py +0 -3
  78. ddtrace/contrib/_unittest.py +0 -36
  79. /ddtrace/contrib/{_aiobotocore.py → internal/aiobotocore/__init__.py} +0 -0
  80. /ddtrace/contrib/{_aiohttp_jinja2.py → internal/aiohttp_jinja2/__init__.py} +0 -0
  81. /ddtrace/contrib/{_aiomysql.py → internal/aiomysql/__init__.py} +0 -0
  82. /ddtrace/contrib/{_aiopg.py → internal/aiopg/__init__.py} +0 -0
  83. /ddtrace/contrib/{_aioredis.py → internal/aioredis/__init__.py} +0 -0
  84. /ddtrace/contrib/{_algoliasearch.py → internal/algoliasearch/__init__.py} +0 -0
  85. /ddtrace/contrib/{_anthropic.py → internal/anthropic/__init__.py} +0 -0
  86. /ddtrace/contrib/{_aredis.py → internal/aredis/__init__.py} +0 -0
  87. /ddtrace/contrib/{_asyncio.py → internal/asyncio/__init__.py} +0 -0
  88. /ddtrace/contrib/{_asyncpg.py → internal/asyncpg/__init__.py} +0 -0
  89. /ddtrace/contrib/{_aws_lambda.py → internal/aws_lambda/__init__.py} +0 -0
  90. /ddtrace/contrib/{_azure_functions.py → internal/azure_functions/__init__.py} +0 -0
  91. /ddtrace/contrib/{_azure_servicebus.py → internal/azure_servicebus/__init__.py} +0 -0
  92. /ddtrace/contrib/{_boto.py → internal/boto/__init__.py} +0 -0
  93. /ddtrace/contrib/{_cassandra.py → internal/cassandra/__init__.py} +0 -0
  94. /ddtrace/contrib/{_consul.py → internal/consul/__init__.py} +0 -0
  95. /ddtrace/contrib/{_coverage.py → internal/coverage/__init__.py} +0 -0
  96. /ddtrace/contrib/{_crewai.py → internal/crewai/__init__.py} +0 -0
  97. /ddtrace/contrib/{_django.py → internal/django/__init__.py} +0 -0
  98. /ddtrace/contrib/{_dogpile_cache.py → internal/dogpile_cache/__init__.py} +0 -0
  99. /ddtrace/contrib/{_dramatiq.py → internal/dramatiq/__init__.py} +0 -0
  100. /ddtrace/contrib/{_elasticsearch.py → internal/elasticsearch/__init__.py} +0 -0
  101. /ddtrace/contrib/{_fastapi.py → internal/fastapi/__init__.py} +0 -0
  102. /ddtrace/contrib/{_flask.py → internal/flask/__init__.py} +0 -0
  103. /ddtrace/contrib/{_futures.py → internal/futures/__init__.py} +0 -0
  104. /ddtrace/contrib/{_gevent.py → internal/gevent/__init__.py} +0 -0
  105. /ddtrace/contrib/{_google_genai.py → internal/google_genai/__init__.py} +0 -0
  106. /ddtrace/contrib/{_google_generativeai.py → internal/google_generativeai/__init__.py} +0 -0
  107. /ddtrace/contrib/{_graphql.py → internal/graphql/__init__.py} +0 -0
  108. /ddtrace/contrib/{_grpc.py → internal/grpc/__init__.py} +0 -0
  109. /ddtrace/contrib/{_gunicorn.py → internal/gunicorn/__init__.py} +0 -0
  110. /ddtrace/contrib/{_httplib.py → internal/httplib/__init__.py} +0 -0
  111. /ddtrace/contrib/{_httpx.py → internal/httpx/__init__.py} +0 -0
  112. /ddtrace/contrib/{_jinja2.py → internal/jinja2/__init__.py} +0 -0
  113. /ddtrace/contrib/{_kafka.py → internal/kafka/__init__.py} +0 -0
  114. /ddtrace/contrib/{_kombu.py → internal/kombu/__init__.py} +0 -0
  115. /ddtrace/contrib/{_langchain.py → internal/langchain/__init__.py} +0 -0
  116. /ddtrace/contrib/{_langgraph.py → internal/langgraph/__init__.py} +0 -0
  117. /ddtrace/contrib/{_litellm.py → internal/litellm/__init__.py} +0 -0
  118. /ddtrace/contrib/{_logbook.py → internal/logbook/__init__.py} +0 -0
  119. /ddtrace/contrib/{_logging.py → internal/logging/__init__.py} +0 -0
  120. /ddtrace/contrib/{_loguru.py → internal/loguru/__init__.py} +0 -0
  121. /ddtrace/contrib/{_mako.py → internal/mako/__init__.py} +0 -0
  122. /ddtrace/contrib/{_mariadb.py → internal/mariadb/__init__.py} +0 -0
  123. /ddtrace/contrib/{_mcp.py → internal/mcp/__init__.py} +0 -0
  124. /ddtrace/contrib/{_molten.py → internal/molten/__init__.py} +0 -0
  125. /ddtrace/contrib/{_mongoengine.py → internal/mongoengine/__init__.py} +0 -0
  126. /ddtrace/contrib/{_mysql.py → internal/mysql/__init__.py} +0 -0
  127. /ddtrace/contrib/{_mysqldb.py → internal/mysqldb/__init__.py} +0 -0
  128. /ddtrace/contrib/{_openai.py → internal/openai/__init__.py} +0 -0
  129. /ddtrace/contrib/{_openai_agents.py → internal/openai_agents/__init__.py} +0 -0
  130. /ddtrace/contrib/{_psycopg.py → internal/psycopg/__init__.py} +0 -0
  131. /ddtrace/contrib/{_pydantic_ai.py → internal/pydantic_ai/__init__.py} +0 -0
  132. /ddtrace/contrib/{_pymemcache.py → internal/pymemcache/__init__.py} +0 -0
  133. /ddtrace/contrib/{_pymongo.py → internal/pymongo/__init__.py} +0 -0
  134. /ddtrace/contrib/{_pymysql.py → internal/pymysql/__init__.py} +0 -0
  135. /ddtrace/contrib/{_pynamodb.py → internal/pynamodb/__init__.py} +0 -0
  136. /ddtrace/contrib/{_pyodbc.py → internal/pyodbc/__init__.py} +0 -0
  137. /ddtrace/contrib/{_redis.py → internal/redis/__init__.py} +0 -0
  138. /ddtrace/contrib/{_rediscluster.py → internal/rediscluster/__init__.py} +0 -0
  139. /ddtrace/contrib/{_rq.py → internal/rq/__init__.py} +0 -0
  140. /ddtrace/contrib/{_sanic.py → internal/sanic/__init__.py} +0 -0
  141. /ddtrace/contrib/{_selenium.py → internal/selenium/__init__.py} +0 -0
  142. /ddtrace/contrib/{_snowflake.py → internal/snowflake/__init__.py} +0 -0
  143. /ddtrace/contrib/{_sqlite3.py → internal/sqlite3/__init__.py} +0 -0
  144. /ddtrace/contrib/{_starlette.py → internal/starlette/__init__.py} +0 -0
  145. /ddtrace/contrib/{_structlog.py → internal/structlog/__init__.py} +0 -0
  146. /ddtrace/contrib/{_subprocess.py → internal/subprocess/__init__.py} +0 -0
  147. /ddtrace/contrib/{_urllib.py → internal/urllib/__init__.py} +0 -0
  148. /ddtrace/contrib/{_urllib3.py → internal/urllib3/__init__.py} +0 -0
  149. /ddtrace/contrib/{_vertexai.py → internal/vertexai/__init__.py} +0 -0
  150. /ddtrace/contrib/{_vertica.py → internal/vertica/__init__.py} +0 -0
  151. /ddtrace/contrib/{_webbrowser.py → internal/webbrowser/__init__.py} +0 -0
  152. /ddtrace/contrib/{_yaaredis.py → internal/yaaredis/__init__.py} +0 -0
  153. {ddtrace-3.11.0rc1.dist-info → ddtrace-3.11.0rc3.dist-info}/WHEEL +0 -0
  154. {ddtrace-3.11.0rc1.dist-info → ddtrace-3.11.0rc3.dist-info}/entry_points.txt +0 -0
  155. {ddtrace-3.11.0rc1.dist-info → ddtrace-3.11.0rc3.dist-info}/licenses/LICENSE +0 -0
  156. {ddtrace-3.11.0rc1.dist-info → ddtrace-3.11.0rc3.dist-info}/licenses/LICENSE.Apache +0 -0
  157. {ddtrace-3.11.0rc1.dist-info → ddtrace-3.11.0rc3.dist-info}/licenses/LICENSE.BSD3 +0 -0
  158. {ddtrace-3.11.0rc1.dist-info → ddtrace-3.11.0rc3.dist-info}/licenses/NOTICE +0 -0
  159. {ddtrace-3.11.0rc1.dist-info → ddtrace-3.11.0rc3.dist-info}/top_level.txt +0 -0
ddtrace/_logger.py CHANGED
@@ -20,6 +20,7 @@ class LogInjectionState(object):
20
20
  # Log injection is enabled, but not yet configured
21
21
  ENABLED = "true"
22
22
  # Log injection is enabled and configured for structured logging
23
+ # This value is deprecated, but kept for backwards compatibility
23
24
  STRUCTURED = "structured"
24
25
 
25
26
 
@@ -108,17 +109,15 @@ def set_log_formatting():
108
109
  handler.setFormatter(logging.Formatter(DD_LOG_FORMAT))
109
110
 
110
111
 
111
- def get_log_injection_state(raw_config: Optional[str]) -> str:
112
+ def get_log_injection_state(raw_config: Optional[str]) -> bool:
112
113
  """Returns the current log injection state."""
113
114
  if raw_config:
114
115
  normalized = raw_config.lower().strip()
115
- if normalized == LogInjectionState.STRUCTURED:
116
- return LogInjectionState.STRUCTURED
117
- elif normalized in ("true", "1"):
118
- return LogInjectionState.ENABLED
116
+ if normalized == LogInjectionState.STRUCTURED or normalized in ("true", "1"):
117
+ return True
119
118
  elif normalized not in ("false", "0"):
120
119
  logging.warning(
121
120
  "Invalid log injection state '%s'. Expected 'true', 'false', or 'structured'. Defaulting to 'false'.",
122
121
  normalized,
123
122
  )
124
- return LogInjectionState.DISABLED
123
+ return False
ddtrace/_trace/product.py CHANGED
@@ -215,7 +215,7 @@ def apm_tracing_rc(lib_config, dd_config):
215
215
  new_rc_configs["_trace_sampling_rules"] = trace_sampling_rules
216
216
 
217
217
  if "log_injection_enabled" in lib_config:
218
- new_rc_configs["_logs_injection"] = str(lib_config["log_injection_enabled"]).lower()
218
+ new_rc_configs["_logs_injection"] = lib_config["log_injection_enabled"]
219
219
 
220
220
  if "tracing_tags" in lib_config:
221
221
  tags = lib_config["tracing_tags"]
@@ -60,7 +60,7 @@ class SamplingRule(object):
60
60
  number of characters, and "?" meaning any one character. If all tags specified in a SamplingRule are
61
61
  matches with a given span, that span is considered to have matching tags with the rule.
62
62
  """
63
- self.sample_rate = float(min(1, max(0, sample_rate)))
63
+ self.sample_rate = min(1.0, max(0.0, float(sample_rate)))
64
64
  # since span.py converts None to 'None' for tags, and does not accept 'None' for metrics
65
65
  # we can just create a GlobMatcher for 'None' and it will match properly
66
66
  self._tag_value_matchers = (
@@ -111,46 +111,38 @@ class SamplingRule(object):
111
111
  :returns: Whether this span matches or not
112
112
  :rtype: :obj:`bool`
113
113
  """
114
- tags_match = self.tags_match(span)
115
- return tags_match and self._matches((span.service, span.name, span.resource))
114
+ return self.tags_match(span) and self._matches((span.service, span.name, span.resource))
116
115
 
117
116
  def tags_match(self, span: Span) -> bool:
118
- tag_match = True
119
- if self._tag_value_matchers:
120
- tag_match = self.check_tags(span.get_tags(), span.get_metrics())
121
- return tag_match
117
+ if not self._tag_value_matchers:
118
+ return True
122
119
 
123
- def check_tags(self, meta, metrics):
124
- if meta is None and metrics is None:
120
+ meta = span._meta or {}
121
+ metrics = span._metrics or {}
122
+ if not meta and not metrics:
125
123
  return False
126
124
 
127
- tag_match = False
128
- for tag_key in self._tag_value_matchers.keys():
129
- value = meta.get(tag_key)
130
- # it's because we're not checking metrics first before continuing
125
+ for tag_key, pattern in self._tag_value_matchers.items():
126
+ value = meta.get(tag_key, metrics.get(tag_key))
131
127
  if value is None:
132
- value = metrics.get(tag_key)
133
- if value is None:
134
- continue
135
- # Floats: Matching floating point values with a non-zero decimal part is not supported.
136
- # For floating point values with a non-zero decimal part, any all * pattern always returns true.
137
- # Other patterns always return false.
138
- if isinstance(value, float):
139
- if not value.is_integer():
140
- if all(c == "*" for c in self._tag_value_matchers[tag_key].pattern):
141
- tag_match = True
142
- continue
143
- else:
144
- return False
145
- else:
146
- value = int(value)
147
-
148
- tag_match = self._tag_value_matchers[tag_key].match(str(value))
149
- # if we don't match with all specified tags for a rule, it's not a match
150
- if tag_match is False:
128
+ # If the tag is not present, we failed the match
129
+ # (Metrics and meta do not support the value None)
130
+ return False
131
+
132
+ if isinstance(value, float):
133
+ # Floats: Convert floats that represent integers to int for matching. This is because
134
+ # SamplingRules only support integers for matfching or glob patterns.
135
+ if value.is_integer():
136
+ value = int(value)
137
+ elif set(pattern.pattern) - {"?", "*"}:
138
+ # Only match floats to patterns that only contain wildcards (ex: * or ?*)
139
+ # This is because we do not want to match floats to patterns like `23.*`.
140
+ return False
141
+
142
+ if not pattern.match(str(value)):
151
143
  return False
152
144
 
153
- return tag_match
145
+ return True
154
146
 
155
147
  def sample(self, span):
156
148
  """
@@ -661,13 +661,9 @@ def _on_botocore_patched_bedrock_api_call_started(ctx, request_params):
661
661
 
662
662
  span.set_tag_str("bedrock.request.model_provider", ctx["model_provider"])
663
663
  span.set_tag_str("bedrock.request.model", ctx["model_name"])
664
- for k, v in request_params.items():
665
- if k == "prompt":
666
- if integration.is_pc_sampled_span(span):
667
- v = integration.trunc(str(v))
668
- span.set_tag_str("bedrock.request.{}".format(k), str(v))
669
- if k == "n":
670
- ctx.set_item("num_generations", str(v))
664
+
665
+ if "n" in request_params:
666
+ ctx.set_item("num_generations", str(request_params["n"]))
671
667
 
672
668
 
673
669
  def _on_botocore_patched_bedrock_api_call_exception(ctx, exc_info):
@@ -680,16 +676,6 @@ def _on_botocore_patched_bedrock_api_call_exception(ctx, exc_info):
680
676
  span.finish()
681
677
 
682
678
 
683
- def _on_botocore_patched_bedrock_api_call_success(ctx, reqid, latency, input_token_count, output_token_count):
684
- span = ctx.span
685
- span.set_tag_str("bedrock.response.id", reqid)
686
- span.set_tag_str("bedrock.response.duration", latency)
687
- if input_token_count:
688
- span.set_metric("bedrock.response.usage.prompt_tokens", int(input_token_count))
689
- if output_token_count:
690
- span.set_metric("bedrock.response.usage.completion_tokens", int(output_token_count))
691
-
692
-
693
679
  def _propagate_context(ctx, headers):
694
680
  distributed_tracing_enabled = ctx["integration_config"].distributed_tracing_enabled
695
681
  span = ctx.span
@@ -731,38 +717,13 @@ def _on_botocore_bedrock_process_response_converse(
731
717
  def _on_botocore_bedrock_process_response(
732
718
  ctx: core.ExecutionContext,
733
719
  formatted_response: Dict[str, Any],
734
- metadata: Dict[str, Any],
735
- body: Dict[str, List[Dict]],
736
- should_set_choice_ids: bool,
737
720
  ) -> None:
738
- text = formatted_response["text"]
739
- span = ctx.span
740
- model_name = ctx["model_name"]
741
- if should_set_choice_ids:
742
- for i in range(len(text)):
743
- span.set_tag_str("bedrock.response.choices.{}.id".format(i), str(body["generations"][i]["id"]))
744
- integration = ctx["bedrock_integration"]
745
- if metadata is not None:
746
- for k, v in metadata.items():
747
- if k in ["usage.completion_tokens", "usage.prompt_tokens"] and v:
748
- span.set_metric("bedrock.response.{}".format(k), int(v))
749
- else:
750
- span.set_tag_str("bedrock.{}".format(k), str(v))
751
- if "embed" in model_name:
752
- span.set_metric("bedrock.response.embedding_length", len(formatted_response["text"][0]))
753
- span.finish()
754
- return
755
- for i in range(len(formatted_response["text"])):
756
- if integration.is_pc_sampled_span(span):
757
- span.set_tag_str(
758
- "bedrock.response.choices.{}.text".format(i),
759
- integration.trunc(str(formatted_response["text"][i])),
760
- )
761
- span.set_tag_str(
762
- "bedrock.response.choices.{}.finish_reason".format(i), str(formatted_response["finish_reason"][i])
763
- )
764
- integration.llmobs_set_tags(span, args=[ctx], kwargs={}, response=formatted_response)
765
- span.finish()
721
+ with ctx.span as span:
722
+ model_name = ctx["model_name"]
723
+ integration = ctx["bedrock_integration"]
724
+ if "embed" in model_name:
725
+ return
726
+ integration.llmobs_set_tags(span, args=[ctx], kwargs={}, response=formatted_response)
766
727
 
767
728
 
768
729
  def _on_botocore_sqs_recvmessage_post(
@@ -872,10 +833,12 @@ def _on_azure_functions_service_bus_trigger_span_modifier(
872
833
  span = ctx.span
873
834
  _set_azure_function_tags(span, azure_functions_config, function_name, trigger, span_kind)
874
835
  span.set_tag_str(MESSAGING_DESTINATION_NAME, entity_name)
875
- span.set_tag_str(MESSAGING_MESSAGE_ID, message_id)
876
836
  span.set_tag_str(MESSAGING_OPERATION, "receive")
877
837
  span.set_tag_str(MESSAGING_SYSTEM, azure_servicebusx.SERVICE)
878
838
 
839
+ if message_id is not None:
840
+ span.set_tag_str(MESSAGING_MESSAGE_ID, message_id)
841
+
879
842
 
880
843
  def _on_azure_servicebus_send_message_modifier(ctx, azure_servicebus_config, entity_name, fully_qualified_namespace):
881
844
  span = ctx.span
@@ -929,7 +892,6 @@ def listen():
929
892
  core.on("botocore.client_context.update_messages", _on_botocore_update_messages)
930
893
  core.on("botocore.patched_bedrock_api_call.started", _on_botocore_patched_bedrock_api_call_started)
931
894
  core.on("botocore.patched_bedrock_api_call.exception", _on_botocore_patched_bedrock_api_call_exception)
932
- core.on("botocore.patched_bedrock_api_call.success", _on_botocore_patched_bedrock_api_call_success)
933
895
  core.on("botocore.bedrock.process_response", _on_botocore_bedrock_process_response)
934
896
  core.on("botocore.bedrock.process_response_converse", _on_botocore_bedrock_process_response_converse)
935
897
  core.on("botocore.sqs.ReceiveMessage.post", _on_botocore_sqs_recvmessage_post)
@@ -12,11 +12,52 @@ from ddtrace.ext import SpanKind
12
12
  from ddtrace.ext import aws
13
13
  from ddtrace.ext import http
14
14
  from ddtrace.internal.constants import COMPONENT
15
+ from ddtrace.internal.serverless import in_aws_lambda
15
16
  from ddtrace.internal.utils.formats import deep_getattr
16
17
 
17
18
 
18
19
  _PAYLOAD_TAGGER = AWSPayloadTagging()
19
20
 
21
+ SERVICE_MAP = {
22
+ "eventbridge": "events",
23
+ "events": "events",
24
+ "sqs": "sqs",
25
+ "sns": "sns",
26
+ "kinesis": "kinesis",
27
+ "dynamodb": "dynamodb",
28
+ "dynamodbdocument": "dynamodb",
29
+ }
30
+
31
+
32
+ # Helper to build AWS hostname from service, region and parameters
33
+ def _derive_peer_hostname(service: str, region: str, params: Optional[Dict[str, Any]] = None) -> Optional[str]:
34
+ """Return hostname for given AWS service according to Datadog peer hostname rules.
35
+
36
+ Only returns hostnames for specific AWS services:
37
+ - eventbridge/events -> events.<region>.amazonaws.com
38
+ - sqs -> sqs.<region>.amazonaws.com
39
+ - sns -> sns.<region>.amazonaws.com
40
+ - kinesis -> kinesis.<region>.amazonaws.com
41
+ - dynamodb -> dynamodb.<region>.amazonaws.com
42
+ - s3 -> <bucket>.s3.<region>.amazonaws.com (if Bucket param present)
43
+ s3.<region>.amazonaws.com (otherwise)
44
+
45
+ Other services return ``None``.
46
+ """
47
+
48
+ if not region:
49
+ return None
50
+
51
+ aws_service = service.lower()
52
+
53
+ if aws_service == "s3":
54
+ bucket = params.get("Bucket") if params else None
55
+ return f"{bucket}.s3.{region}.amazonaws.com" if bucket else f"s3.{region}.amazonaws.com"
56
+
57
+ mapped = SERVICE_MAP.get(aws_service)
58
+
59
+ return f"{mapped}.{region}.amazonaws.com" if mapped else None
60
+
20
61
 
21
62
  def set_botocore_patched_api_call_span_tags(span: Span, instance, args, params, endpoint_name, operation):
22
63
  span.set_tag_str(COMPONENT, config.botocore.integration_name)
@@ -51,6 +92,13 @@ def set_botocore_patched_api_call_span_tags(span: Span, instance, args, params,
51
92
  span.set_tag_str("aws.region", region_name)
52
93
  span.set_tag_str("region", region_name)
53
94
 
95
+ # Derive peer hostname only in serverless environments to avoid
96
+ # unnecessary tag noise in traditional hosts/containers.
97
+ if in_aws_lambda():
98
+ hostname = _derive_peer_hostname(endpoint_name, region_name, params)
99
+ if hostname:
100
+ span.set_tag_str("peer.service", hostname)
101
+
54
102
 
55
103
  def set_botocore_response_metadata_tags(
56
104
  span: Span, result: Dict[str, Any], is_error_code_fn: Optional[Callable] = None
ddtrace/_version.py CHANGED
@@ -17,5 +17,5 @@ __version__: str
17
17
  __version_tuple__: VERSION_TUPLE
18
18
  version_tuple: VERSION_TUPLE
19
19
 
20
- __version__ = version = '3.11.0rc1'
21
- __version_tuple__ = version_tuple = (3, 11, 0, 'rc1')
20
+ __version__ = version = '3.11.0rc3'
21
+ __version_tuple__ = version_tuple = (3, 11, 0, 'rc3')
@@ -1,6 +1,7 @@
1
1
  import functools
2
2
  import json
3
3
  import re
4
+ from types import TracebackType
4
5
  from typing import Any
5
6
  from typing import Callable
6
7
  from typing import Dict
@@ -8,6 +9,7 @@ from typing import List
8
9
  from typing import Literal
9
10
  from typing import Optional
10
11
  from typing import Set
12
+ from typing import Tuple
11
13
  from typing import Union
12
14
  from urllib import parse
13
15
 
@@ -534,7 +536,7 @@ def end_context(span: Span):
534
536
  finalize_asm_env(env)
535
537
 
536
538
 
537
- def _on_context_ended(ctx):
539
+ def _on_context_ended(ctx, _exc_info: Tuple[Optional[type], Optional[BaseException], Optional[TracebackType]]):
538
540
  env = ctx.get_local_item(_ASM_CONTEXT)
539
541
  if env is not None:
540
542
  finalize_asm_env(env)
@@ -275,6 +275,12 @@ class API_SECURITY(metaclass=Constant_Class):
275
275
  SAMPLE_RATE: Literal["DD_API_SECURITY_REQUEST_SAMPLE_RATE"] = "DD_API_SECURITY_REQUEST_SAMPLE_RATE"
276
276
  SAMPLE_DELAY: Literal["DD_API_SECURITY_SAMPLE_DELAY"] = "DD_API_SECURITY_SAMPLE_DELAY"
277
277
  MAX_PAYLOAD_SIZE: Literal[0x1000000] = 0x1000000 # 16MB maximum size
278
+ ENDPOINT_COLLECTION: Literal[
279
+ "DD_API_SECURITY_ENDPOINT_COLLECTION_ENABLED"
280
+ ] = "DD_API_SECURITY_ENDPOINT_COLLECTION_ENABLED"
281
+ ENDPOINT_COLLECTION_LIMIT: Literal[
282
+ "DD_API_SECURITY_ENDPOINT_COLLECTION_MESSAGE_LIMIT"
283
+ ] = "DD_API_SECURITY_ENDPOINT_COLLECTION_MESSAGE_LIMIT"
278
284
 
279
285
 
280
286
  class WAF_CONTEXT_NAMES(metaclass=Constant_Class):
@@ -348,6 +354,7 @@ class DEFAULT(metaclass=Constant_Class):
348
354
  r"|ey[I-L][\w=-]+\.(ey[I-L][\w=-]+(?:\.[\w.+\/=-]+)?)|[\-]{5}BEGIN[a-z\s]+PRIVATE\sKEY[\-]{5}([^\-]+)[\-]"
349
355
  r"{5}END[a-z\s]+PRIVATE\sKEY|ssh-rsa\s*([a-z0-9\/\.+]{100,})"
350
356
  )
357
+ ENDPOINT_COLLECTION_LIMIT = 300
351
358
 
352
359
 
353
360
  class EXPLOIT_PREVENTION(metaclass=Constant_Class):
@@ -4,6 +4,7 @@ import json
4
4
  from typing import Any
5
5
  from typing import Dict
6
6
  from typing import Optional
7
+ from typing import Union
7
8
 
8
9
  import xmltodict
9
10
 
@@ -11,6 +12,7 @@ from ddtrace._trace.span import Span
11
12
  from ddtrace.appsec._asm_request_context import _call_waf
12
13
  from ddtrace.appsec._asm_request_context import _call_waf_first
13
14
  from ddtrace.appsec._asm_request_context import get_blocked
15
+ from ddtrace.appsec._asm_request_context import set_body_response
14
16
  from ddtrace.appsec._constants import SPAN_DATA_NAMES
15
17
  from ddtrace.appsec._http_utils import extract_cookies_from_headers
16
18
  from ddtrace.appsec._http_utils import normalize_headers
@@ -157,6 +159,14 @@ def _on_lambda_start_response(
157
159
  _call_waf(("aws_lambda",))
158
160
 
159
161
 
162
+ def _on_lambda_parse_body(
163
+ response_body: Optional[Union[str, Dict[str, Any]]],
164
+ ):
165
+ if asm_config._api_security_feature_active:
166
+ if response_body:
167
+ set_body_response(response_body)
168
+
169
+
160
170
  # ASGI
161
171
 
162
172
 
@@ -408,6 +418,7 @@ def listen():
408
418
 
409
419
  core.on("aws_lambda.start_request", _on_lambda_start_request)
410
420
  core.on("aws_lambda.start_response", _on_lambda_start_response)
421
+ core.on("aws_lambda.parse_body", _on_lambda_parse_body)
411
422
 
412
423
  core.on("grpc.server.response.message", _on_grpc_server_response)
413
424
  core.on("grpc.server.data", _on_grpc_server_data)
@@ -1,3 +1,7 @@
1
+ from types import TracebackType
2
+ from typing import Optional
3
+ from typing import Tuple
4
+
1
5
  from ddtrace.appsec._iast._handlers import _iast_on_wrapped_view
2
6
  from ddtrace.appsec._iast._handlers import _on_asgi_finalize_response
3
7
  from ddtrace.appsec._iast._handlers import _on_django_finalize_response_pre
@@ -19,6 +23,12 @@ from ddtrace.internal import core
19
23
 
20
24
 
21
25
  def iast_listen():
26
+ def _iast_context_end(
27
+ ctx: core.ExecutionContext,
28
+ _exc_info: Tuple[Optional[type], Optional[BaseException], Optional[TracebackType]],
29
+ ):
30
+ _iast_end_request(ctx)
31
+
22
32
  core.on("grpc.client.response.message", _on_grpc_response)
23
33
  core.on("grpc.server.response.message", _on_grpc_server_response)
24
34
 
@@ -37,8 +47,8 @@ def iast_listen():
37
47
  core.on("flask.finalize_request.post", _on_flask_finalize_request_post)
38
48
  core.on("werkzeug.render_debugger_html", _on_werkzeug_render_debugger_html)
39
49
 
40
- core.on("context.ended.wsgi.__call__", _iast_end_request)
41
- core.on("context.ended.asgi.__call__", _iast_end_request)
50
+ core.on("context.ended.wsgi.__call__", _iast_context_end)
51
+ core.on("context.ended.asgi.__call__", _iast_context_end)
42
52
 
43
53
  # Sink points
44
54
  core.on("db_query_check", _on_report_sqli)
@@ -189,7 +189,7 @@ class AppSecSpanProcessor(SpanProcessor):
189
189
  if skip_event:
190
190
  core.discard_item("appsec_skip_next_lambda_event")
191
191
  log.debug(
192
- "appsec: ignoring unsupported lamdba event",
192
+ "appsec: ignoring unsupported lambda event",
193
193
  )
194
194
  span.set_metric(APPSEC.UNSUPPORTED_EVENT_TYPE, 1.0)
195
195
  return
@@ -393,6 +393,16 @@ integrations:
393
393
  min: 20.12.1
394
394
  max: 24.11.1
395
395
 
396
+ - integration_name: gunicorn
397
+ is_external_package: true
398
+ is_tested: true
399
+ dependency_names:
400
+ - gunicorn
401
+ tested_versions_by_dependency:
402
+ gunicorn:
403
+ min: 20.0.4
404
+ max: 23.0.0
405
+
396
406
  - integration_name: google_genai
397
407
  is_external_package: true
398
408
  is_tested: true
@@ -5,6 +5,7 @@ import aiobotocore.client
5
5
  import wrapt
6
6
 
7
7
  from ddtrace import config
8
+ from ddtrace._trace.utils_botocore.span_tags import _derive_peer_hostname
8
9
  from ddtrace.constants import _SPAN_MEASURED_KEY
9
10
  from ddtrace.constants import SPAN_KIND
10
11
  from ddtrace.contrib.internal.trace_utils import ext_service
@@ -16,6 +17,7 @@ from ddtrace.ext import http
16
17
  from ddtrace.internal.constants import COMPONENT
17
18
  from ddtrace.internal.schema import schematize_cloud_api_operation
18
19
  from ddtrace.internal.schema import schematize_service_name
20
+ from ddtrace.internal.serverless import in_aws_lambda
19
21
  from ddtrace.internal.utils import ArgumentError
20
22
  from ddtrace.internal.utils import get_argument_value
21
23
  from ddtrace.internal.utils.formats import asbool
@@ -145,6 +147,12 @@ async def _wrapped_api_call(original_func, instance, args, kwargs):
145
147
 
146
148
  region_name = deep_getattr(instance, "meta.region_name")
147
149
 
150
+ if in_aws_lambda():
151
+ # Derive the peer hostname now that we have both service and region.
152
+ hostname = _derive_peer_hostname(endpoint_name, region_name, params)
153
+ if hostname:
154
+ span.set_tag_str("peer.service", hostname)
155
+
148
156
  meta = {
149
157
  "aws.agent": "aiobotocore",
150
158
  "aws.operation": operation,
@@ -0,0 +1,17 @@
1
+ """
2
+ The Avro integration will trace all Avro read / write calls made with the ``avro``
3
+ library. This integration is enabled by default.
4
+
5
+ Enabling
6
+ ~~~~~~~~
7
+
8
+ The avro integration is enabled by default. Use
9
+ :func:`patch()<ddtrace.patch>` to enable the integration::
10
+
11
+ from ddtrace import patch
12
+ patch(avro=True)
13
+
14
+ Configuration
15
+ ~~~~~~~~~~~~~
16
+
17
+ """
@@ -12,6 +12,7 @@ from ddtrace.internal.utils.formats import asbool
12
12
  from ddtrace.trace import Pin
13
13
 
14
14
  from .utils import create_context
15
+ from .utils import message_list_has_single_context
15
16
  from .utils import wrap_function_with_tracing
16
17
 
17
18
 
@@ -97,24 +98,34 @@ def _wrap_service_bus_trigger(pin, func, function_name, trigger_arg_name, trigge
97
98
  def context_factory(kwargs):
98
99
  resource_name = f"{trigger_type} {function_name}"
99
100
  msg = kwargs.get(trigger_arg_name)
100
- return create_context(
101
- "azure.functions.patched_service_bus", pin, resource_name, headers=msg.application_properties
102
- )
101
+
102
+ # Reparent trace if single message or list of messages all with same context
103
+ if isinstance(msg, azure_functions.ServiceBusMessage):
104
+ application_properties = msg.application_properties
105
+ elif (
106
+ isinstance(msg, list)
107
+ and msg
108
+ and isinstance(msg[0], azure_functions.ServiceBusMessage)
109
+ and message_list_has_single_context(msg)
110
+ ):
111
+ application_properties = msg[0].application_properties
112
+ else:
113
+ application_properties = None
114
+
115
+ return create_context("azure.functions.patched_service_bus", pin, resource_name, headers=application_properties)
103
116
 
104
117
  def pre_dispatch(ctx, kwargs):
105
118
  msg = kwargs.get(trigger_arg_name)
119
+
120
+ if isinstance(msg, azure_functions.ServiceBusMessage):
121
+ message_id = msg.message_id
122
+ else:
123
+ message_id = None
124
+
106
125
  entity_name = trigger_details.get("topicName") or trigger_details.get("queueName")
107
126
  return (
108
127
  "azure.functions.service_bus_trigger_modifier",
109
- (
110
- ctx,
111
- config.azure_functions,
112
- function_name,
113
- trigger_type,
114
- SpanKind.CONSUMER,
115
- entity_name,
116
- msg.message_id,
117
- ),
128
+ (ctx, config.azure_functions, function_name, trigger_type, SpanKind.CONSUMER, entity_name, message_id),
118
129
  )
119
130
 
120
131
  return wrap_function_with_tracing(func, context_factory, pre_dispatch=pre_dispatch)
@@ -1,11 +1,15 @@
1
1
  import functools
2
2
  import inspect
3
+ from typing import List
4
+
5
+ import azure.functions as azure_functions
3
6
 
4
7
  from ddtrace import config
5
8
  from ddtrace.contrib.internal.trace_utils import int_service
6
9
  from ddtrace.ext import SpanTypes
7
10
  from ddtrace.internal import core
8
11
  from ddtrace.internal.schema import schematize_cloud_faas_operation
12
+ from ddtrace.propagation.http import HTTPPropagator
9
13
 
10
14
 
11
15
  def create_context(context_name, pin, resource=None, headers=None):
@@ -59,3 +63,13 @@ def wrap_function_with_tracing(func, context_factory, pre_dispatch=None, post_di
59
63
  core.dispatch(*post_dispatch(ctx, res))
60
64
 
61
65
  return wrapper
66
+
67
+
68
+ def message_list_has_single_context(msg_list: List[azure_functions.ServiceBusMessage]):
69
+ first_context = HTTPPropagator.extract(msg_list[0].application_properties)
70
+ for message in msg_list[1:]:
71
+ context = HTTPPropagator.extract(message.application_properties)
72
+ if first_context != context:
73
+ return False
74
+
75
+ return True
@@ -7,6 +7,7 @@ import boto.connection
7
7
  import wrapt
8
8
 
9
9
  from ddtrace import config
10
+ from ddtrace._trace.utils_botocore.span_tags import _derive_peer_hostname
10
11
  from ddtrace.constants import _SPAN_MEASURED_KEY
11
12
  from ddtrace.constants import SPAN_KIND
12
13
  from ddtrace.ext import SpanKind
@@ -16,6 +17,7 @@ from ddtrace.ext import http
16
17
  from ddtrace.internal.constants import COMPONENT
17
18
  from ddtrace.internal.schema import schematize_cloud_api_operation
18
19
  from ddtrace.internal.schema import schematize_service_name
20
+ from ddtrace.internal.serverless import in_aws_lambda
19
21
  from ddtrace.internal.utils import get_argument_value
20
22
  from ddtrace.internal.utils.formats import asbool
21
23
  from ddtrace.internal.utils.wrappers import unwrap
@@ -122,6 +124,12 @@ def patched_query_request(original_func, instance, args, kwargs):
122
124
  meta[aws.REGION] = region_name
123
125
  meta[aws.AWSREGION] = region_name
124
126
 
127
+ if in_aws_lambda():
128
+ # Derive the peer hostname now that we have both service and region.
129
+ hostname = _derive_peer_hostname(endpoint_name, region_name, params)
130
+ if hostname:
131
+ meta["peer.service"] = hostname
132
+
125
133
  span.set_tags(meta)
126
134
 
127
135
  # Original func returns a boto.connection.HTTPResponse object
@@ -183,6 +191,12 @@ def patched_auth_request(original_func, instance, args, kwargs):
183
191
  meta[aws.REGION] = region_name
184
192
  meta[aws.AWSREGION] = region_name
185
193
 
194
+ if in_aws_lambda():
195
+ # Derive the peer hostname
196
+ hostname = _derive_peer_hostname(endpoint_name, region_name, None)
197
+ if hostname:
198
+ meta["peer.service"] = hostname
199
+
186
200
  span.set_tags(meta)
187
201
 
188
202
  # Original func returns a boto.connection.HTTPResponse object