ddtrace 3.11.0rc2__cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl → 3.11.0rc3__cp311-cp311-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.

Potentially problematic release.


This version of ddtrace might be problematic. Click here for more details.

Files changed (45) hide show
  1. ddtrace/_trace/sampling_rule.py +25 -33
  2. ddtrace/_trace/trace_handlers.py +9 -49
  3. ddtrace/_trace/utils_botocore/span_tags.py +48 -0
  4. ddtrace/_version.py +2 -2
  5. ddtrace/appsec/_constants.py +7 -0
  6. ddtrace/appsec/_handlers.py +11 -0
  7. ddtrace/appsec/_processor.py +1 -1
  8. ddtrace/contrib/internal/aiobotocore/patch.py +8 -0
  9. ddtrace/contrib/internal/boto/patch.py +14 -0
  10. ddtrace/contrib/internal/botocore/services/bedrock.py +3 -27
  11. ddtrace/contrib/internal/django/patch.py +31 -8
  12. ddtrace/contrib/internal/google_genai/_utils.py +2 -2
  13. ddtrace/contrib/internal/google_genai/patch.py +7 -7
  14. ddtrace/contrib/internal/google_generativeai/patch.py +7 -5
  15. ddtrace/contrib/internal/openai_agents/patch.py +44 -1
  16. ddtrace/contrib/internal/pytest/_plugin_v2.py +1 -1
  17. ddtrace/contrib/internal/vertexai/patch.py +7 -5
  18. ddtrace/ext/ci.py +20 -0
  19. ddtrace/ext/git.py +66 -11
  20. ddtrace/internal/ci_visibility/encoder.py +126 -55
  21. ddtrace/internal/endpoints.py +76 -0
  22. ddtrace/internal/schema/processor.py +6 -2
  23. ddtrace/internal/telemetry/writer.py +18 -0
  24. ddtrace/llmobs/_constants.py +1 -0
  25. ddtrace/llmobs/_experiment.py +6 -0
  26. ddtrace/llmobs/_integrations/crewai.py +52 -3
  27. ddtrace/llmobs/_integrations/gemini.py +7 -7
  28. ddtrace/llmobs/_integrations/google_genai.py +10 -10
  29. ddtrace/llmobs/_integrations/{google_genai_utils.py → google_utils.py} +103 -7
  30. ddtrace/llmobs/_integrations/openai_agents.py +145 -0
  31. ddtrace/llmobs/_integrations/pydantic_ai.py +67 -26
  32. ddtrace/llmobs/_integrations/utils.py +68 -158
  33. ddtrace/llmobs/_integrations/vertexai.py +8 -8
  34. ddtrace/llmobs/_llmobs.py +5 -1
  35. ddtrace/llmobs/_utils.py +21 -0
  36. ddtrace/settings/asm.py +9 -2
  37. {ddtrace-3.11.0rc2.dist-info → ddtrace-3.11.0rc3.dist-info}/METADATA +1 -1
  38. {ddtrace-3.11.0rc2.dist-info → ddtrace-3.11.0rc3.dist-info}/RECORD +45 -44
  39. {ddtrace-3.11.0rc2.dist-info → ddtrace-3.11.0rc3.dist-info}/WHEEL +0 -0
  40. {ddtrace-3.11.0rc2.dist-info → ddtrace-3.11.0rc3.dist-info}/entry_points.txt +0 -0
  41. {ddtrace-3.11.0rc2.dist-info → ddtrace-3.11.0rc3.dist-info}/licenses/LICENSE +0 -0
  42. {ddtrace-3.11.0rc2.dist-info → ddtrace-3.11.0rc3.dist-info}/licenses/LICENSE.Apache +0 -0
  43. {ddtrace-3.11.0rc2.dist-info → ddtrace-3.11.0rc3.dist-info}/licenses/LICENSE.BSD3 +0 -0
  44. {ddtrace-3.11.0rc2.dist-info → ddtrace-3.11.0rc3.dist-info}/licenses/NOTICE +0 -0
  45. {ddtrace-3.11.0rc2.dist-info → ddtrace-3.11.0rc3.dist-info}/top_level.txt +0 -0
@@ -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(
@@ -931,7 +892,6 @@ def listen():
931
892
  core.on("botocore.client_context.update_messages", _on_botocore_update_messages)
932
893
  core.on("botocore.patched_bedrock_api_call.started", _on_botocore_patched_bedrock_api_call_started)
933
894
  core.on("botocore.patched_bedrock_api_call.exception", _on_botocore_patched_bedrock_api_call_exception)
934
- core.on("botocore.patched_bedrock_api_call.success", _on_botocore_patched_bedrock_api_call_success)
935
895
  core.on("botocore.bedrock.process_response", _on_botocore_bedrock_process_response)
936
896
  core.on("botocore.bedrock.process_response_converse", _on_botocore_bedrock_process_response_converse)
937
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.0rc2'
21
- __version_tuple__ = version_tuple = (3, 11, 0, 'rc2')
20
+ __version__ = version = '3.11.0rc3'
21
+ __version_tuple__ = version_tuple = (3, 11, 0, 'rc3')
@@ -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)
@@ -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
@@ -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,
@@ -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
@@ -48,12 +48,9 @@ class TracedBotocoreStreamingBody(wrapt.ObjectProxy):
48
48
  self._body.append(json.loads(body))
49
49
  if self.__wrapped__.tell() == int(self.__wrapped__._content_length):
50
50
  formatted_response = _extract_text_and_response_reason(self._execution_ctx, self._body[0])
51
- model_provider = self._execution_ctx["model_provider"]
52
- model_name = self._execution_ctx["model_name"]
53
- should_set_choice_ids = model_provider == _COHERE and "embed" not in model_name
54
51
  core.dispatch(
55
52
  "botocore.bedrock.process_response",
56
- [self._execution_ctx, formatted_response, None, self._body[0], should_set_choice_ids],
53
+ [self._execution_ctx, formatted_response],
57
54
  )
58
55
  return body
59
56
  except Exception:
@@ -67,12 +64,9 @@ class TracedBotocoreStreamingBody(wrapt.ObjectProxy):
67
64
  for line in lines:
68
65
  self._body.append(json.loads(line))
69
66
  formatted_response = _extract_text_and_response_reason(self._execution_ctx, self._body[0])
70
- model_provider = self._execution_ctx["model_provider"]
71
- model_name = self._execution_ctx["model_name"]
72
- should_set_choice_ids = model_provider == _COHERE and "embed" not in model_name
73
67
  core.dispatch(
74
68
  "botocore.bedrock.process_response",
75
- [self._execution_ctx, formatted_response, None, self._body[0], should_set_choice_ids],
69
+ [self._execution_ctx, formatted_response],
76
70
  )
77
71
  return lines
78
72
  except Exception:
@@ -93,16 +87,10 @@ class TracedBotocoreStreamingBody(wrapt.ObjectProxy):
93
87
  finally:
94
88
  if exception_raised:
95
89
  return
96
- metadata = _extract_streamed_response_metadata(self._execution_ctx, self._body)
97
90
  formatted_response = _extract_streamed_response(self._execution_ctx, self._body)
98
- model_provider = self._execution_ctx["model_provider"]
99
- model_name = self._execution_ctx["model_name"]
100
- should_set_choice_ids = (
101
- model_provider == _COHERE and "is_finished" not in self._body[0] and "embed" not in model_name
102
- )
103
91
  core.dispatch(
104
92
  "botocore.bedrock.process_response",
105
- [self._execution_ctx, formatted_response, metadata, self._body, should_set_choice_ids],
93
+ [self._execution_ctx, formatted_response],
106
94
  )
107
95
 
108
96
 
@@ -443,18 +431,6 @@ def handle_bedrock_response(
443
431
  safe_token_count(cache_write_tokens),
444
432
  )
445
433
 
446
- # for both converse & invoke, dispatch success event to store basic metrics
447
- core.dispatch(
448
- "botocore.patched_bedrock_api_call.success",
449
- [
450
- ctx,
451
- str(metadata.get("RequestId", "")),
452
- request_latency,
453
- str(input_tokens),
454
- str(output_tokens),
455
- ],
456
- )
457
-
458
434
  if ctx["resource"] == "Converse":
459
435
  core.dispatch("botocore.bedrock.process_response_converse", [ctx, result])
460
436
  return result
@@ -49,6 +49,7 @@ from ddtrace.internal.utils.formats import asbool
49
49
  from ddtrace.internal.utils.importlib import func_name
50
50
  from ddtrace.propagation._database_monitoring import _DBM_Propagator
51
51
  from ddtrace.settings.asm import config as asm_config
52
+ from ddtrace.settings.asm import endpoint_collection
52
53
  from ddtrace.settings.integration import IntegrationConfig
53
54
  from ddtrace.trace import Pin
54
55
  from ddtrace.vendor.packaging.version import parse as parse_version
@@ -81,6 +82,7 @@ config._add(
81
82
  use_legacy_resource_format=asbool(os.getenv("DD_DJANGO_USE_LEGACY_RESOURCE_FORMAT", default=False)),
82
83
  _trace_asgi_websocket=os.getenv("DD_ASGI_TRACE_WEBSOCKET", default=False),
83
84
  obfuscate_404_resource=os.getenv("DD_ASGI_OBFUSCATE_404_RESOURCE", default=False),
85
+ views={},
84
86
  ),
85
87
  )
86
88
 
@@ -598,7 +600,7 @@ def traced_template_render(django, pin, wrapped, instance, args, kwargs):
598
600
  return wrapped(*args, **kwargs)
599
601
 
600
602
 
601
- def instrument_view(django, view):
603
+ def instrument_view(django, view, path=None):
602
604
  """
603
605
  Helper to wrap Django views.
604
606
 
@@ -608,10 +610,24 @@ def instrument_view(django, view):
608
610
  for cls in reversed(getmro(view)):
609
611
  _instrument_view(django, cls)
610
612
 
611
- return _instrument_view(django, view)
613
+ return _instrument_view(django, view, path=path)
612
614
 
613
615
 
614
- def _instrument_view(django, view):
616
+ def extract_request_method_list(view):
617
+ try:
618
+ while "view_func" in view.__code__.co_freevars:
619
+ view = view.__closure__[view.__code__.co_freevars.index("view_func")].cell_contents
620
+ if "request_method_list" in view.__code__.co_freevars:
621
+ return view.__closure__[view.__code__.co_freevars.index("request_method_list")].cell_contents
622
+ return []
623
+ except Exception:
624
+ return []
625
+
626
+
627
+ _DEFAULT_METHODS = ("get", "delete", "post", "options", "head")
628
+
629
+
630
+ def _instrument_view(django, view, path=None):
615
631
  """Helper to wrap Django views."""
616
632
  from . import utils
617
633
 
@@ -620,9 +636,14 @@ def _instrument_view(django, view):
620
636
  return view
621
637
 
622
638
  # Patch view HTTP methods and lifecycle methods
623
- http_method_names = getattr(view, "http_method_names", ("get", "delete", "post", "options", "head"))
639
+
640
+ http_method_names = getattr(view, "http_method_names", ())
641
+ request_method_list = extract_request_method_list(view) or http_method_names
642
+ if path is not None:
643
+ for method in request_method_list or ["*"]:
644
+ endpoint_collection.add_endpoint(method, path, operation_name="django.request")
624
645
  lifecycle_methods = ("setup", "dispatch", "http_method_not_allowed")
625
- for name in list(http_method_names) + list(lifecycle_methods):
646
+ for name in list(request_method_list or _DEFAULT_METHODS) + list(lifecycle_methods):
626
647
  try:
627
648
  func = getattr(view, name, None)
628
649
  if not func or isinstance(func, wrapt.ObjectProxy):
@@ -665,18 +686,20 @@ def traced_urls_path(django, pin, wrapped, instance, args, kwargs):
665
686
  try:
666
687
  from_args = False
667
688
  view = kwargs.pop("view", None)
689
+ path = kwargs.pop("path", None)
668
690
  if view is None:
669
691
  view = args[1]
670
692
  from_args = True
693
+ if path is None:
694
+ path = args[0]
671
695
 
672
696
  core.dispatch("service_entrypoint.patch", (unwrap(view),))
673
-
674
697
  if from_args:
675
698
  args = list(args)
676
- args[1] = instrument_view(django, view)
699
+ args[1] = instrument_view(django, view, path=path)
677
700
  args = tuple(args)
678
701
  else:
679
- kwargs["view"] = instrument_view(django, view)
702
+ kwargs["view"] = instrument_view(django, view, path=path)
680
703
  except Exception:
681
704
  log.debug("Failed to instrument Django url path %r %r", args, kwargs, exc_info=True)
682
705
  return wrapped(*args, **kwargs)
@@ -6,7 +6,7 @@ from typing import Optional
6
6
 
7
7
  import wrapt
8
8
 
9
- from ddtrace.llmobs._integrations.google_genai_utils import DEFAULT_MODEL_ROLE
9
+ from ddtrace.llmobs._integrations.google_utils import GOOGLE_GENAI_DEFAULT_MODEL_ROLE
10
10
  from ddtrace.llmobs._utils import _get_attr
11
11
 
12
12
 
@@ -32,7 +32,7 @@ def _join_chunks(chunks: List[Any]) -> Optional[Dict[str, Any]]:
32
32
  continue
33
33
 
34
34
  if role is None:
35
- role = _get_attr(content, "role", DEFAULT_MODEL_ROLE)
35
+ role = _get_attr(content, "role", GOOGLE_GENAI_DEFAULT_MODEL_ROLE)
36
36
 
37
37
  parts = _get_attr(content, "parts", [])
38
38
  for part in parts:
@@ -9,7 +9,7 @@ from ddtrace.contrib.internal.trace_utils import unwrap
9
9
  from ddtrace.contrib.internal.trace_utils import with_traced_module
10
10
  from ddtrace.contrib.internal.trace_utils import wrap
11
11
  from ddtrace.llmobs._integrations import GoogleGenAIIntegration
12
- from ddtrace.llmobs._integrations.google_genai_utils import extract_provider_and_model_name
12
+ from ddtrace.llmobs._integrations.google_utils import extract_provider_and_model_name
13
13
  from ddtrace.trace import Pin
14
14
 
15
15
 
@@ -27,7 +27,7 @@ def get_version() -> str:
27
27
  @with_traced_module
28
28
  def traced_generate(genai, pin, func, instance, args, kwargs):
29
29
  integration = genai._datadog_integration
30
- provider_name, model_name = extract_provider_and_model_name(kwargs)
30
+ provider_name, model_name = extract_provider_and_model_name(kwargs=kwargs)
31
31
  with integration.trace(
32
32
  pin,
33
33
  "%s.%s" % (instance.__class__.__name__, func.__name__),
@@ -46,7 +46,7 @@ def traced_generate(genai, pin, func, instance, args, kwargs):
46
46
  @with_traced_module
47
47
  async def traced_async_generate(genai, pin, func, instance, args, kwargs):
48
48
  integration = genai._datadog_integration
49
- provider_name, model_name = extract_provider_and_model_name(kwargs)
49
+ provider_name, model_name = extract_provider_and_model_name(kwargs=kwargs)
50
50
  with integration.trace(
51
51
  pin,
52
52
  "%s.%s" % (instance.__class__.__name__, func.__name__),
@@ -65,7 +65,7 @@ async def traced_async_generate(genai, pin, func, instance, args, kwargs):
65
65
  @with_traced_module
66
66
  def traced_generate_stream(genai, pin, func, instance, args, kwargs):
67
67
  integration = genai._datadog_integration
68
- provider_name, model_name = extract_provider_and_model_name(kwargs)
68
+ provider_name, model_name = extract_provider_and_model_name(kwargs=kwargs)
69
69
  span = integration.trace(
70
70
  pin,
71
71
  "%s.%s" % (instance.__class__.__name__, func.__name__),
@@ -86,7 +86,7 @@ def traced_generate_stream(genai, pin, func, instance, args, kwargs):
86
86
  @with_traced_module
87
87
  async def traced_async_generate_stream(genai, pin, func, instance, args, kwargs):
88
88
  integration = genai._datadog_integration
89
- provider_name, model_name = extract_provider_and_model_name(kwargs)
89
+ provider_name, model_name = extract_provider_and_model_name(kwargs=kwargs)
90
90
  span = integration.trace(
91
91
  pin,
92
92
  "%s.%s" % (instance.__class__.__name__, func.__name__),
@@ -107,7 +107,7 @@ async def traced_async_generate_stream(genai, pin, func, instance, args, kwargs)
107
107
  @with_traced_module
108
108
  def traced_embed_content(genai, pin, func, instance, args, kwargs):
109
109
  integration = genai._datadog_integration
110
- provider_name, model_name = extract_provider_and_model_name(kwargs)
110
+ provider_name, model_name = extract_provider_and_model_name(kwargs=kwargs)
111
111
  with integration.trace(
112
112
  pin,
113
113
  "%s.%s" % (instance.__class__.__name__, func.__name__),
@@ -126,7 +126,7 @@ def traced_embed_content(genai, pin, func, instance, args, kwargs):
126
126
  @with_traced_module
127
127
  async def traced_async_embed_content(genai, pin, func, instance, args, kwargs):
128
128
  integration = genai._datadog_integration
129
- provider_name, model_name = extract_provider_and_model_name(kwargs)
129
+ provider_name, model_name = extract_provider_and_model_name(kwargs=kwargs)
130
130
  with integration.trace(
131
131
  pin,
132
132
  "%s.%s" % (instance.__class__.__name__, func.__name__),
@@ -11,7 +11,7 @@ from ddtrace.contrib.internal.trace_utils import unwrap
11
11
  from ddtrace.contrib.internal.trace_utils import with_traced_module
12
12
  from ddtrace.contrib.internal.trace_utils import wrap
13
13
  from ddtrace.llmobs._integrations import GeminiIntegration
14
- from ddtrace.llmobs._integrations.utils import extract_model_name_google
14
+ from ddtrace.llmobs._integrations.google_utils import extract_provider_and_model_name
15
15
  from ddtrace.trace import Pin
16
16
 
17
17
 
@@ -40,11 +40,12 @@ def traced_generate(genai, pin, func, instance, args, kwargs):
40
40
  integration = genai._datadog_integration
41
41
  stream = kwargs.get("stream", False)
42
42
  generations = None
43
+ provider_name, model_name = extract_provider_and_model_name(instance=instance, model_name_attr="model_name")
43
44
  span = integration.trace(
44
45
  pin,
45
46
  "%s.%s" % (instance.__class__.__name__, func.__name__),
46
- provider="google",
47
- model=extract_model_name_google(instance, "model_name"),
47
+ provider=provider_name,
48
+ model=model_name,
48
49
  submit_to_llmobs=True,
49
50
  )
50
51
  try:
@@ -68,11 +69,12 @@ async def traced_agenerate(genai, pin, func, instance, args, kwargs):
68
69
  integration = genai._datadog_integration
69
70
  stream = kwargs.get("stream", False)
70
71
  generations = None
72
+ provider_name, model_name = extract_provider_and_model_name(instance=instance, model_name_attr="model_name")
71
73
  span = integration.trace(
72
74
  pin,
73
75
  "%s.%s" % (instance.__class__.__name__, func.__name__),
74
- provider="google",
75
- model=extract_model_name_google(instance, "model_name"),
76
+ provider=provider_name,
77
+ model=model_name,
76
78
  submit_to_llmobs=True,
77
79
  )
78
80
  try: