ddtrace 3.11.0rc1__cp311-cp311-macosx_12_0_arm64.whl → 3.11.0rc3__cp311-cp311-macosx_12_0_arm64.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 (172) 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/_ast/iastpatch.cpython-311-darwin.so +0 -0
  11. ddtrace/appsec/_iast/_listener.py +12 -2
  12. ddtrace/appsec/_iast/_stacktrace.cpython-311-darwin.so +0 -0
  13. ddtrace/appsec/_processor.py +1 -1
  14. ddtrace/contrib/integration_registry/registry.yaml +10 -0
  15. ddtrace/contrib/internal/aiobotocore/patch.py +8 -0
  16. ddtrace/contrib/internal/avro/__init__.py +17 -0
  17. ddtrace/contrib/internal/azure_functions/patch.py +23 -12
  18. ddtrace/contrib/internal/azure_functions/utils.py +14 -0
  19. ddtrace/contrib/internal/boto/patch.py +14 -0
  20. ddtrace/contrib/internal/botocore/__init__.py +153 -0
  21. ddtrace/contrib/internal/botocore/services/bedrock.py +3 -27
  22. ddtrace/contrib/internal/django/patch.py +31 -8
  23. ddtrace/contrib/{_freezegun.py → internal/freezegun/__init__.py} +1 -1
  24. ddtrace/contrib/internal/google_genai/_utils.py +2 -2
  25. ddtrace/contrib/internal/google_genai/patch.py +7 -7
  26. ddtrace/contrib/internal/google_generativeai/patch.py +7 -5
  27. ddtrace/contrib/internal/langchain/patch.py +11 -443
  28. ddtrace/contrib/internal/langchain/utils.py +0 -26
  29. ddtrace/contrib/internal/logbook/patch.py +1 -2
  30. ddtrace/contrib/internal/logging/patch.py +4 -7
  31. ddtrace/contrib/internal/loguru/patch.py +1 -3
  32. ddtrace/contrib/internal/openai_agents/patch.py +44 -1
  33. ddtrace/contrib/internal/protobuf/__init__.py +17 -0
  34. ddtrace/contrib/internal/pytest/__init__.py +62 -0
  35. ddtrace/contrib/internal/pytest/_plugin_v2.py +13 -4
  36. ddtrace/contrib/internal/pytest_bdd/__init__.py +23 -0
  37. ddtrace/contrib/internal/pytest_benchmark/__init__.py +3 -0
  38. ddtrace/contrib/internal/structlog/patch.py +2 -4
  39. ddtrace/contrib/internal/unittest/__init__.py +36 -0
  40. ddtrace/contrib/internal/vertexai/patch.py +7 -5
  41. ddtrace/ext/ci.py +20 -0
  42. ddtrace/ext/git.py +66 -11
  43. ddtrace/internal/_encoding.cpython-311-darwin.so +0 -0
  44. ddtrace/internal/_encoding.pyi +1 -1
  45. ddtrace/internal/_rand.cpython-311-darwin.so +0 -0
  46. ddtrace/internal/_tagset.cpython-311-darwin.so +0 -0
  47. ddtrace/internal/_threads.cpython-311-darwin.so +0 -0
  48. ddtrace/internal/ci_visibility/encoder.py +126 -49
  49. ddtrace/internal/ci_visibility/utils.py +4 -4
  50. ddtrace/internal/core/__init__.py +5 -2
  51. ddtrace/internal/endpoints.py +76 -0
  52. ddtrace/internal/schema/processor.py +6 -2
  53. ddtrace/internal/telemetry/metrics_namespaces.cpython-311-darwin.so +0 -0
  54. ddtrace/internal/telemetry/writer.py +18 -0
  55. ddtrace/internal/test_visibility/coverage_lines.py +4 -4
  56. ddtrace/internal/writer/writer.py +24 -11
  57. ddtrace/llmobs/_constants.py +3 -0
  58. ddtrace/llmobs/_experiment.py +75 -10
  59. ddtrace/llmobs/_integrations/bedrock.py +4 -0
  60. ddtrace/llmobs/_integrations/bedrock_agents.py +5 -1
  61. ddtrace/llmobs/_integrations/crewai.py +52 -3
  62. ddtrace/llmobs/_integrations/gemini.py +7 -7
  63. ddtrace/llmobs/_integrations/google_genai.py +10 -10
  64. ddtrace/llmobs/_integrations/{google_genai_utils.py → google_utils.py} +103 -7
  65. ddtrace/llmobs/_integrations/langchain.py +29 -20
  66. ddtrace/llmobs/_integrations/openai_agents.py +145 -0
  67. ddtrace/llmobs/_integrations/pydantic_ai.py +67 -26
  68. ddtrace/llmobs/_integrations/utils.py +68 -158
  69. ddtrace/llmobs/_integrations/vertexai.py +8 -8
  70. ddtrace/llmobs/_llmobs.py +83 -14
  71. ddtrace/llmobs/_telemetry.py +20 -5
  72. ddtrace/llmobs/_utils.py +27 -0
  73. ddtrace/profiling/_threading.cpython-311-darwin.so +0 -0
  74. ddtrace/profiling/collector/_memalloc.cpython-311-darwin.so +0 -0
  75. ddtrace/profiling/collector/_task.cpython-311-darwin.so +0 -0
  76. ddtrace/profiling/collector/_traceback.cpython-311-darwin.so +0 -0
  77. ddtrace/profiling/collector/stack.cpython-311-darwin.so +0 -0
  78. ddtrace/settings/_config.py +1 -2
  79. ddtrace/settings/asm.py +9 -2
  80. ddtrace/settings/profiling.py +0 -9
  81. ddtrace/vendor/psutil/_psutil_osx.cpython-311-darwin.so +0 -0
  82. ddtrace/vendor/psutil/_psutil_posix.cpython-311-darwin.so +0 -0
  83. {ddtrace-3.11.0rc1.dist-info → ddtrace-3.11.0rc3.dist-info}/METADATA +1 -1
  84. {ddtrace-3.11.0rc1.dist-info → ddtrace-3.11.0rc3.dist-info}/RECORD +165 -171
  85. ddtrace/contrib/_avro.py +0 -17
  86. ddtrace/contrib/_botocore.py +0 -153
  87. ddtrace/contrib/_protobuf.py +0 -17
  88. ddtrace/contrib/_pytest.py +0 -62
  89. ddtrace/contrib/_pytest_bdd.py +0 -23
  90. ddtrace/contrib/_pytest_benchmark.py +0 -3
  91. ddtrace/contrib/_unittest.py +0 -36
  92. /ddtrace/contrib/{_aiobotocore.py → internal/aiobotocore/__init__.py} +0 -0
  93. /ddtrace/contrib/{_aiohttp_jinja2.py → internal/aiohttp_jinja2/__init__.py} +0 -0
  94. /ddtrace/contrib/{_aiomysql.py → internal/aiomysql/__init__.py} +0 -0
  95. /ddtrace/contrib/{_aiopg.py → internal/aiopg/__init__.py} +0 -0
  96. /ddtrace/contrib/{_aioredis.py → internal/aioredis/__init__.py} +0 -0
  97. /ddtrace/contrib/{_algoliasearch.py → internal/algoliasearch/__init__.py} +0 -0
  98. /ddtrace/contrib/{_anthropic.py → internal/anthropic/__init__.py} +0 -0
  99. /ddtrace/contrib/{_aredis.py → internal/aredis/__init__.py} +0 -0
  100. /ddtrace/contrib/{_asyncio.py → internal/asyncio/__init__.py} +0 -0
  101. /ddtrace/contrib/{_asyncpg.py → internal/asyncpg/__init__.py} +0 -0
  102. /ddtrace/contrib/{_aws_lambda.py → internal/aws_lambda/__init__.py} +0 -0
  103. /ddtrace/contrib/{_azure_functions.py → internal/azure_functions/__init__.py} +0 -0
  104. /ddtrace/contrib/{_azure_servicebus.py → internal/azure_servicebus/__init__.py} +0 -0
  105. /ddtrace/contrib/{_boto.py → internal/boto/__init__.py} +0 -0
  106. /ddtrace/contrib/{_cassandra.py → internal/cassandra/__init__.py} +0 -0
  107. /ddtrace/contrib/{_consul.py → internal/consul/__init__.py} +0 -0
  108. /ddtrace/contrib/{_coverage.py → internal/coverage/__init__.py} +0 -0
  109. /ddtrace/contrib/{_crewai.py → internal/crewai/__init__.py} +0 -0
  110. /ddtrace/contrib/{_django.py → internal/django/__init__.py} +0 -0
  111. /ddtrace/contrib/{_dogpile_cache.py → internal/dogpile_cache/__init__.py} +0 -0
  112. /ddtrace/contrib/{_dramatiq.py → internal/dramatiq/__init__.py} +0 -0
  113. /ddtrace/contrib/{_elasticsearch.py → internal/elasticsearch/__init__.py} +0 -0
  114. /ddtrace/contrib/{_fastapi.py → internal/fastapi/__init__.py} +0 -0
  115. /ddtrace/contrib/{_flask.py → internal/flask/__init__.py} +0 -0
  116. /ddtrace/contrib/{_futures.py → internal/futures/__init__.py} +0 -0
  117. /ddtrace/contrib/{_gevent.py → internal/gevent/__init__.py} +0 -0
  118. /ddtrace/contrib/{_google_genai.py → internal/google_genai/__init__.py} +0 -0
  119. /ddtrace/contrib/{_google_generativeai.py → internal/google_generativeai/__init__.py} +0 -0
  120. /ddtrace/contrib/{_graphql.py → internal/graphql/__init__.py} +0 -0
  121. /ddtrace/contrib/{_grpc.py → internal/grpc/__init__.py} +0 -0
  122. /ddtrace/contrib/{_gunicorn.py → internal/gunicorn/__init__.py} +0 -0
  123. /ddtrace/contrib/{_httplib.py → internal/httplib/__init__.py} +0 -0
  124. /ddtrace/contrib/{_httpx.py → internal/httpx/__init__.py} +0 -0
  125. /ddtrace/contrib/{_jinja2.py → internal/jinja2/__init__.py} +0 -0
  126. /ddtrace/contrib/{_kafka.py → internal/kafka/__init__.py} +0 -0
  127. /ddtrace/contrib/{_kombu.py → internal/kombu/__init__.py} +0 -0
  128. /ddtrace/contrib/{_langchain.py → internal/langchain/__init__.py} +0 -0
  129. /ddtrace/contrib/{_langgraph.py → internal/langgraph/__init__.py} +0 -0
  130. /ddtrace/contrib/{_litellm.py → internal/litellm/__init__.py} +0 -0
  131. /ddtrace/contrib/{_logbook.py → internal/logbook/__init__.py} +0 -0
  132. /ddtrace/contrib/{_logging.py → internal/logging/__init__.py} +0 -0
  133. /ddtrace/contrib/{_loguru.py → internal/loguru/__init__.py} +0 -0
  134. /ddtrace/contrib/{_mako.py → internal/mako/__init__.py} +0 -0
  135. /ddtrace/contrib/{_mariadb.py → internal/mariadb/__init__.py} +0 -0
  136. /ddtrace/contrib/{_mcp.py → internal/mcp/__init__.py} +0 -0
  137. /ddtrace/contrib/{_molten.py → internal/molten/__init__.py} +0 -0
  138. /ddtrace/contrib/{_mongoengine.py → internal/mongoengine/__init__.py} +0 -0
  139. /ddtrace/contrib/{_mysql.py → internal/mysql/__init__.py} +0 -0
  140. /ddtrace/contrib/{_mysqldb.py → internal/mysqldb/__init__.py} +0 -0
  141. /ddtrace/contrib/{_openai.py → internal/openai/__init__.py} +0 -0
  142. /ddtrace/contrib/{_openai_agents.py → internal/openai_agents/__init__.py} +0 -0
  143. /ddtrace/contrib/{_psycopg.py → internal/psycopg/__init__.py} +0 -0
  144. /ddtrace/contrib/{_pydantic_ai.py → internal/pydantic_ai/__init__.py} +0 -0
  145. /ddtrace/contrib/{_pymemcache.py → internal/pymemcache/__init__.py} +0 -0
  146. /ddtrace/contrib/{_pymongo.py → internal/pymongo/__init__.py} +0 -0
  147. /ddtrace/contrib/{_pymysql.py → internal/pymysql/__init__.py} +0 -0
  148. /ddtrace/contrib/{_pynamodb.py → internal/pynamodb/__init__.py} +0 -0
  149. /ddtrace/contrib/{_pyodbc.py → internal/pyodbc/__init__.py} +0 -0
  150. /ddtrace/contrib/{_redis.py → internal/redis/__init__.py} +0 -0
  151. /ddtrace/contrib/{_rediscluster.py → internal/rediscluster/__init__.py} +0 -0
  152. /ddtrace/contrib/{_rq.py → internal/rq/__init__.py} +0 -0
  153. /ddtrace/contrib/{_sanic.py → internal/sanic/__init__.py} +0 -0
  154. /ddtrace/contrib/{_selenium.py → internal/selenium/__init__.py} +0 -0
  155. /ddtrace/contrib/{_snowflake.py → internal/snowflake/__init__.py} +0 -0
  156. /ddtrace/contrib/{_sqlite3.py → internal/sqlite3/__init__.py} +0 -0
  157. /ddtrace/contrib/{_starlette.py → internal/starlette/__init__.py} +0 -0
  158. /ddtrace/contrib/{_structlog.py → internal/structlog/__init__.py} +0 -0
  159. /ddtrace/contrib/{_subprocess.py → internal/subprocess/__init__.py} +0 -0
  160. /ddtrace/contrib/{_urllib.py → internal/urllib/__init__.py} +0 -0
  161. /ddtrace/contrib/{_urllib3.py → internal/urllib3/__init__.py} +0 -0
  162. /ddtrace/contrib/{_vertexai.py → internal/vertexai/__init__.py} +0 -0
  163. /ddtrace/contrib/{_vertica.py → internal/vertica/__init__.py} +0 -0
  164. /ddtrace/contrib/{_webbrowser.py → internal/webbrowser/__init__.py} +0 -0
  165. /ddtrace/contrib/{_yaaredis.py → internal/yaaredis/__init__.py} +0 -0
  166. {ddtrace-3.11.0rc1.dist-info → ddtrace-3.11.0rc3.dist-info}/WHEEL +0 -0
  167. {ddtrace-3.11.0rc1.dist-info → ddtrace-3.11.0rc3.dist-info}/entry_points.txt +0 -0
  168. {ddtrace-3.11.0rc1.dist-info → ddtrace-3.11.0rc3.dist-info}/licenses/LICENSE +0 -0
  169. {ddtrace-3.11.0rc1.dist-info → ddtrace-3.11.0rc3.dist-info}/licenses/LICENSE.Apache +0 -0
  170. {ddtrace-3.11.0rc1.dist-info → ddtrace-3.11.0rc3.dist-info}/licenses/LICENSE.BSD3 +0 -0
  171. {ddtrace-3.11.0rc1.dist-info → ddtrace-3.11.0rc3.dist-info}/licenses/NOTICE +0 -0
  172. {ddtrace-3.11.0rc1.dist-info → ddtrace-3.11.0rc3.dist-info}/top_level.txt +0 -0
@@ -1,7 +1,14 @@
1
+ from __future__ import annotations
2
+
1
3
  import json
2
4
  import os
3
5
  import threading
4
6
  from typing import TYPE_CHECKING # noqa:F401
7
+ from typing import Any # noqa:F401
8
+ from typing import Dict # noqa:F401
9
+ from typing import List # noqa:F401
10
+ from typing import Optional # noqa:F401
11
+ from typing import Tuple # noqa:F401
5
12
  from uuid import uuid4
6
13
 
7
14
  from ddtrace.ext import SpanTypes
@@ -28,11 +35,6 @@ from ddtrace.internal.writer.writer import NoEncodableSpansError
28
35
  log = get_logger(__name__)
29
36
 
30
37
  if TYPE_CHECKING: # pragma: no cover
31
- from typing import Any # noqa:F401
32
- from typing import Dict # noqa:F401
33
- from typing import List # noqa:F401
34
- from typing import Optional # noqa:F401
35
-
36
38
  from ddtrace._trace.span import Span # noqa:F401
37
39
 
38
40
 
@@ -42,74 +44,153 @@ class CIVisibilityEncoderV01(BufferedEncoder):
42
44
  TEST_SUITE_EVENT_VERSION = 1
43
45
  TEST_EVENT_VERSION = 2
44
46
  ENDPOINT_TYPE = ENDPOINT.TEST_CYCLE
47
+ _MAX_PAYLOAD_SIZE = 5 * 1024 * 1024 # 5MB
45
48
 
46
49
  def __init__(self, *args):
47
50
  # DEV: args are not used here, but are used by BufferedEncoder's __cinit__() method,
48
51
  # which is called implicitly by Cython.
49
52
  super(CIVisibilityEncoderV01, self).__init__()
53
+ self._metadata: Dict[str, Dict[str, str]] = {}
50
54
  self._lock = threading.RLock()
51
- self._metadata = {}
55
+ self._is_xdist_worker = os.getenv("PYTEST_XDIST_WORKER") is not None
52
56
  self._init_buffer()
53
57
 
54
58
  def __len__(self):
55
59
  with self._lock:
56
60
  return len(self.buffer)
57
61
 
58
- def set_metadata(self, event_type, metadata):
59
- # type: (str, Dict[str, str]) -> None
62
+ def set_metadata(self, event_type: str, metadata: Dict[str, str]):
60
63
  self._metadata.setdefault(event_type, {}).update(metadata)
61
64
 
62
65
  def _init_buffer(self):
63
66
  with self._lock:
64
67
  self.buffer = []
65
68
 
66
- def put(self, spans):
69
+ def put(self, item):
67
70
  with self._lock:
68
- self.buffer.append(spans)
71
+ self.buffer.append(item)
69
72
 
70
73
  def encode_traces(self, traces):
71
- return self._build_payload(traces=traces)
74
+ """
75
+ Only used for LogWriter, not called for CI Visibility currently
76
+ """
77
+ raise NotImplementedError()
72
78
 
73
- def encode(self):
79
+ def encode(self) -> List[Tuple[Optional[bytes], int]]:
74
80
  with self._lock:
81
+ if not self.buffer:
82
+ return []
83
+ payloads = []
75
84
  with StopWatch() as sw:
76
- payload = self._build_payload(self.buffer)
85
+ payloads = self._build_payload(self.buffer)
77
86
  record_endpoint_payload_events_serialization_time(endpoint=self.ENDPOINT_TYPE, seconds=sw.elapsed())
78
- buffer_size = len(self.buffer)
79
87
  self._init_buffer()
80
- return payload, buffer_size
88
+ return payloads
81
89
 
82
- def _get_parent_session(self, traces):
90
+ def _get_parent_session(self, traces: List[List[Span]]) -> int:
83
91
  for trace in traces:
84
92
  for span in trace:
85
93
  if span.get_tag(EVENT_TYPE) == SESSION_TYPE and span.parent_id is not None and span.parent_id != 0:
86
94
  return span.parent_id
87
95
  return 0
88
96
 
89
- def _build_payload(self, traces):
97
+ def _build_payload(self, traces: List[List[Span]]) -> List[Tuple[Optional[bytes], int]]:
98
+ """
99
+ Build multiple payloads from traces, splitting when necessary to stay under size limits.
100
+ Uses index-based recursive approach to avoid copying slices.
101
+
102
+ Returns a list of (payload_bytes, trace_count) tuples, where each payload contains
103
+ as many traces as possible without exceeding _MAX_PAYLOAD_SIZE.
104
+ """
105
+ if not traces:
106
+ return []
107
+
90
108
  new_parent_session_span_id = self._get_parent_session(traces)
91
- is_not_xdist_worker = os.getenv("PYTEST_XDIST_WORKER") is None
92
- normalized_spans = [
93
- self._convert_span(span, trace[0].context.dd_origin, new_parent_session_span_id)
94
- for trace in traces
95
- for span in trace
96
- if (is_not_xdist_worker or span.get_tag(EVENT_TYPE) != SESSION_TYPE)
97
- ]
98
- if not normalized_spans:
99
- return None
100
- record_endpoint_payload_events_count(endpoint=ENDPOINT.TEST_CYCLE, count=len(normalized_spans))
109
+ return self._build_payloads_recursive(traces, 0, len(traces), new_parent_session_span_id)
101
110
 
102
- # TODO: Split the events in several payloads as needed to avoid hitting the intake's maximum payload size.
111
+ def _build_payloads_recursive(
112
+ self, traces: List[List[Span]], start_idx: int, end_idx: int, new_parent_session_span_id: int
113
+ ) -> List[Tuple[Optional[bytes], int]]:
114
+ """
115
+ Recursively build payloads using start/end indexes to avoid slice copying.
116
+
117
+ Args:
118
+ traces: Full list of traces
119
+ start_idx: Start index (inclusive)
120
+ end_idx: End index (exclusive)
121
+ new_parent_session_span_id: Parent session span ID
122
+
123
+ Returns:
124
+ List of (payload_bytes, trace_count) tuples
125
+ """
126
+ if start_idx >= end_idx:
127
+ return []
128
+
129
+ trace_count = end_idx - start_idx
130
+
131
+ # Convert traces to spans with filtering (using indexes)
132
+ all_spans_with_trace_info = self._convert_traces_to_spans_indexed(
133
+ traces, start_idx, end_idx, new_parent_session_span_id
134
+ )
135
+
136
+ # Get all spans (flattened)
137
+ all_spans = [span for _, trace_spans in all_spans_with_trace_info for span in trace_spans]
138
+
139
+ if not all_spans:
140
+ log.debug("No spans to encode after filtering, skipping chunk")
141
+ return []
142
+
143
+ # Try to create payload from all spans
144
+ payload = self._create_payload_from_spans(all_spans)
145
+
146
+ if len(payload) <= self._MAX_PAYLOAD_SIZE or trace_count == 1:
147
+ # Payload fits or we can't split further (single trace)
148
+ record_endpoint_payload_events_count(endpoint=self.ENDPOINT_TYPE, count=len(all_spans))
149
+ return [(payload, trace_count)]
150
+ else:
151
+ # Payload is too large, split in half recursively
152
+ mid_idx = start_idx + (trace_count + 1) // 2
153
+
154
+ # Process both halves recursively
155
+ left_payloads = self._build_payloads_recursive(traces, start_idx, mid_idx, new_parent_session_span_id)
156
+ right_payloads = self._build_payloads_recursive(traces, mid_idx, end_idx, new_parent_session_span_id)
157
+
158
+ # Combine results
159
+ return left_payloads + right_payloads
160
+
161
+ def _convert_traces_to_spans_indexed(
162
+ self, traces: List[List[Span]], start_idx: int, end_idx: int, new_parent_session_span_id: int
163
+ ) -> List[Tuple[int, List[Dict[str, Any]]]]:
164
+ """Convert traces to spans with xdist filtering applied, using indexes to avoid slicing."""
165
+ all_spans_with_trace_info = []
166
+ for trace_idx in range(start_idx, end_idx):
167
+ trace = traces[trace_idx]
168
+ trace_spans = [
169
+ self._convert_span(span, trace[0].context.dd_origin, new_parent_session_span_id)
170
+ for span in trace
171
+ if (not self._is_xdist_worker) or (span.get_tag(EVENT_TYPE) != SESSION_TYPE)
172
+ ]
173
+ all_spans_with_trace_info.append((trace_idx, trace_spans))
174
+
175
+ return all_spans_with_trace_info
176
+
177
+ def _create_payload_from_spans(self, spans: List[Dict[str, Any]]) -> bytes:
178
+ """Create a payload from the given spans."""
103
179
  return CIVisibilityEncoderV01._pack_payload(
104
- {"version": self.PAYLOAD_FORMAT_VERSION, "metadata": self._metadata, "events": normalized_spans}
180
+ {
181
+ "version": self.PAYLOAD_FORMAT_VERSION,
182
+ "metadata": self._metadata,
183
+ "events": spans,
184
+ }
105
185
  )
106
186
 
107
187
  @staticmethod
108
188
  def _pack_payload(payload):
109
189
  return msgpack_packb(payload)
110
190
 
111
- def _convert_span(self, span, dd_origin, new_parent_session_span_id=0):
112
- # type: (Span, str, Optional[int]) -> Dict[str, Any]
191
+ def _convert_span(
192
+ self, span: Span, dd_origin: Optional[str] = None, new_parent_session_span_id: int = 0
193
+ ) -> Dict[str, Any]:
113
194
  sp = JSONEncoderV2._span_to_dict(span)
114
195
  sp = JSONEncoderV2._normalize_span(sp)
115
196
  sp["type"] = span.get_tag(EVENT_TYPE) or span.span_type
@@ -177,18 +258,17 @@ class CIVisibilityCoverageEncoderV02(CIVisibilityEncoderV01):
177
258
  def _set_itr_suite_skipping_mode(self, new_value):
178
259
  self.itr_suite_skipping_mode = new_value
179
260
 
180
- def put(self, spans):
261
+ def put(self, item):
181
262
  spans_with_coverage = [
182
263
  span
183
- for span in spans
264
+ for span in item
184
265
  if COVERAGE_TAG_NAME in span.get_tags() or span.get_struct_tag(COVERAGE_TAG_NAME) is not None
185
266
  ]
186
267
  if not spans_with_coverage:
187
268
  raise NoEncodableSpansError()
188
269
  return super(CIVisibilityCoverageEncoderV02, self).put(spans_with_coverage)
189
270
 
190
- def _build_coverage_attachment(self, data):
191
- # type: (bytes) -> List[bytes]
271
+ def _build_coverage_attachment(self, data: bytes) -> List[bytes]:
192
272
  return [
193
273
  b"--%s" % self.boundary.encode("utf-8"),
194
274
  b'Content-Disposition: form-data; name="coverage1"; filename="coverage1.msgpack"',
@@ -197,8 +277,7 @@ class CIVisibilityCoverageEncoderV02(CIVisibilityEncoderV01):
197
277
  data,
198
278
  ]
199
279
 
200
- def _build_event_json_attachment(self):
201
- # type: () -> List[bytes]
280
+ def _build_event_json_attachment(self) -> List[bytes]:
202
281
  return [
203
282
  b"--%s" % self.boundary.encode("utf-8"),
204
283
  b'Content-Disposition: form-data; name="event"; filename="event.json"',
@@ -207,18 +286,16 @@ class CIVisibilityCoverageEncoderV02(CIVisibilityEncoderV01):
207
286
  b'{"dummy":true}',
208
287
  ]
209
288
 
210
- def _build_body(self, data):
211
- # type: (bytes) -> List[bytes]
289
+ def _build_body(self, data: bytes) -> List[bytes]:
212
290
  return (
213
291
  self._build_coverage_attachment(data)
214
292
  + self._build_event_json_attachment()
215
293
  + [b"--%s--" % self.boundary.encode("utf-8")]
216
294
  )
217
295
 
218
- def _build_data(self, traces):
219
- # type: (List[List[Span]]) -> Optional[bytes]
296
+ def _build_data(self, traces: List[List[Span]]) -> Optional[bytes]:
220
297
  normalized_covs = [
221
- self._convert_span(span, "")
298
+ self._convert_span(span)
222
299
  for trace in traces
223
300
  for span in trace
224
301
  if (COVERAGE_TAG_NAME in span.get_tags() or span.get_struct_tag(COVERAGE_TAG_NAME) is not None)
@@ -229,17 +306,17 @@ class CIVisibilityCoverageEncoderV02(CIVisibilityEncoderV01):
229
306
  # TODO: Split the events in several payloads as needed to avoid hitting the intake's maximum payload size.
230
307
  return msgpack_packb({"version": self.PAYLOAD_FORMAT_VERSION, "coverages": normalized_covs})
231
308
 
232
- def _build_payload(self, traces):
233
- # type: (List[List[Span]]) -> Optional[bytes]
309
+ def _build_payload(self, traces: List[List[Span]]) -> List[Tuple[Optional[bytes], int]]:
234
310
  data = self._build_data(traces)
235
311
  if not data:
236
- return None
237
- return b"\r\n".join(self._build_body(data))
312
+ return []
313
+ return [(b"\r\n".join(self._build_body(data)), len(data))]
238
314
 
239
- def _convert_span(self, span, dd_origin, new_parent_session_span_id=0):
240
- # type: (Span, str, Optional[int]) -> Dict[str, Any]
315
+ def _convert_span(
316
+ self, span: Span, dd_origin: Optional[str] = None, new_parent_session_span_id: int = 0
317
+ ) -> Dict[str, Any]:
241
318
  # DEV: new_parent_session_span_id is unused here, but it is used in super class
242
- files: Dict[str, Any] = {}
319
+ files: dict[str, Any] = {}
243
320
 
244
321
  files_struct_tag_value = span.get_struct_tag(COVERAGE_TAG_NAME)
245
322
  if files_struct_tag_value is not None and "files" in files_struct_tag_value:
@@ -112,11 +112,10 @@ def take_over_logger_stream_handler(remove_ddtrace_stream_handlers=True):
112
112
  log.debug("CIVisibility not taking over ddtrace logger because level is set to: %s", level)
113
113
  return
114
114
 
115
- root_logger = logging.getLogger()
115
+ ddtrace_logger = logging.getLogger("ddtrace")
116
116
 
117
117
  if remove_ddtrace_stream_handlers:
118
118
  log.debug("CIVisibility removing DDTrace logger handler")
119
- ddtrace_logger = logging.getLogger("ddtrace")
120
119
  for handler in list(ddtrace_logger.handlers):
121
120
  ddtrace_logger.removeHandler(handler)
122
121
  else:
@@ -136,8 +135,9 @@ def take_over_logger_stream_handler(remove_ddtrace_stream_handlers=True):
136
135
  log.warning("Invalid log level: %s", level)
137
136
  return
138
137
 
139
- root_logger.addHandler(ci_visibility_handler)
140
- root_logger.setLevel(min(root_logger.level, ci_visibility_handler.level))
138
+ ddtrace_logger.addHandler(ci_visibility_handler)
139
+ ddtrace_logger.setLevel(min(ddtrace_logger.level, ci_visibility_handler.level))
140
+ ddtrace_logger.propagate = False
141
141
 
142
142
  log.debug("logger setup complete")
143
143
 
@@ -197,9 +197,12 @@ class ExecutionContext(object):
197
197
  self._parent = value
198
198
 
199
199
  def __exit__(
200
- self, exc_type: Optional[type], exc_value: Optional[BaseException], traceback: Optional[types.TracebackType]
200
+ self,
201
+ exc_type: Optional[type],
202
+ exc_value: Optional[BaseException],
203
+ traceback: Optional[types.TracebackType],
201
204
  ) -> bool:
202
- dispatch("context.ended.%s" % self.identifier, (self,))
205
+ dispatch("context.ended.%s" % self.identifier, (self, (exc_type, exc_value, traceback)))
203
206
  if self._span is None:
204
207
  try:
205
208
  if self._token is not None:
@@ -0,0 +1,76 @@
1
+ import dataclasses
2
+ from time import monotonic
3
+ from typing import List
4
+
5
+
6
+ @dataclasses.dataclass(frozen=True)
7
+ class HttpEndPoint:
8
+ method: str
9
+ path: str
10
+ resource_name: str = dataclasses.field(default="")
11
+ operation_name: str = dataclasses.field(default="http.request")
12
+
13
+ def __post_init__(self) -> None:
14
+ super().__setattr__("method", self.method.upper())
15
+ if not self.resource_name:
16
+ super().__setattr__("resource_name", f"{self.method} {self.path}")
17
+
18
+
19
+ @dataclasses.dataclass()
20
+ class HttpEndPointsCollection:
21
+ """A collection of HTTP endpoints that can be modified and flushed to a telemetry payload.
22
+
23
+ The collection collects HTTP endpoints at startup and can be flushed to a telemetry payload.
24
+ It maintains a maximum size and drops endpoints after a certain time period in case of a hot reload of the server.
25
+ """
26
+
27
+ endpoints: List[HttpEndPoint] = dataclasses.field(default_factory=list, init=False)
28
+ is_first: bool = dataclasses.field(default=True, init=False)
29
+ drop_time_seconds: float = dataclasses.field(default=90.0, init=False)
30
+ last_modification_time: float = dataclasses.field(default_factory=monotonic, init=False)
31
+ max_size_length: int = dataclasses.field(default=900, init=False)
32
+
33
+ def reset(self) -> None:
34
+ """Reset the collection to its initial state."""
35
+ self.endpoints.clear()
36
+ self.is_first = True
37
+ self.last_modification_time = monotonic()
38
+
39
+ def add_endpoint(
40
+ self, method: str, path: str, resource_name: str = "", operation_name: str = "http.request"
41
+ ) -> None:
42
+ """
43
+ Add an endpoint to the collection.
44
+ """
45
+ current_time = monotonic()
46
+ if current_time - self.last_modification_time > self.drop_time_seconds:
47
+ self.reset()
48
+ self.endpoints.append(
49
+ HttpEndPoint(method=method, path=path, resource_name=resource_name, operation_name=operation_name)
50
+ )
51
+ elif len(self.endpoints) < self.max_size_length:
52
+ self.last_modification_time = current_time
53
+ self.endpoints.append(
54
+ HttpEndPoint(method=method, path=path, resource_name=resource_name, operation_name=operation_name)
55
+ )
56
+
57
+ def flush(self, max_length: int) -> dict:
58
+ """
59
+ Flush the endpoints to a payload, returning the first `max` endpoints.
60
+ """
61
+ if max_length >= len(self.endpoints):
62
+ res = {
63
+ "is_first": self.is_first,
64
+ "endpoints": [dataclasses.asdict(ep) for ep in self.endpoints],
65
+ }
66
+ self.reset()
67
+ return res
68
+ else:
69
+ res = {
70
+ "is_first": self.is_first,
71
+ "endpoints": [dataclasses.asdict(ep) for ep in self.endpoints[:max_length]],
72
+ }
73
+ self.endpoints = self.endpoints[max_length:]
74
+ self.is_first = False
75
+ self.last_modification_time = monotonic()
76
+ return res
@@ -1,5 +1,6 @@
1
1
  from ddtrace._trace.processor import TraceProcessor
2
2
  from ddtrace.constants import _BASE_SERVICE_KEY
3
+ from ddtrace.internal.serverless import in_aws_lambda
3
4
  from ddtrace.settings._config import config
4
5
 
5
6
  from . import schematize_service_name
@@ -8,10 +9,13 @@ from . import schematize_service_name
8
9
  class BaseServiceProcessor(TraceProcessor):
9
10
  def __init__(self):
10
11
  self._global_service = schematize_service_name((config.service or "").lower())
12
+ self._in_aws_lambda = in_aws_lambda()
11
13
 
12
14
  def process_trace(self, trace):
13
- if not trace:
14
- return
15
+ # AWS Lambda spans receive unhelpful base_service value of runtime
16
+ # Remove base_service to prevent service overrides in Lambda spans
17
+ if not trace or self._in_aws_lambda:
18
+ return trace
15
19
 
16
20
  traces_to_process = filter(
17
21
  lambda x: x.service and x.service.lower() != self._global_service,
@@ -418,6 +418,23 @@ class TelemetryWriter(PeriodicService):
418
418
  payload = {"dependencies": packages}
419
419
  self.add_event(payload, "app-dependencies-loaded")
420
420
 
421
+ def _add_endpoints_event(self):
422
+ """Adds a Telemetry event which sends the list of HTTP endpoints found at startup to the agent"""
423
+ import ddtrace.settings.asm as asm_config_module
424
+
425
+ if not asm_config_module.config._api_security_endpoint_collection or not self._enabled:
426
+ return
427
+
428
+ if not asm_config_module.endpoint_collection.endpoints:
429
+ return
430
+
431
+ with self._service_lock:
432
+ payload = asm_config_module.endpoint_collection.flush(
433
+ asm_config_module.config._api_security_endpoint_collection_limit
434
+ )
435
+
436
+ self.add_event(payload, "app-endpoints")
437
+
421
438
  def _app_product_change(self):
422
439
  # type: () -> None
423
440
  """Adds a Telemetry event which reports the enablement of an APM product"""
@@ -660,6 +677,7 @@ class TelemetryWriter(PeriodicService):
660
677
  self._app_client_configuration_changed_event(configurations)
661
678
 
662
679
  self._app_dependencies_loaded_event()
680
+ self._add_endpoints_event()
663
681
 
664
682
  if shutting_down:
665
683
  self._app_closing_event()
@@ -41,8 +41,8 @@ class CoverageLines:
41
41
  def add(self, line_number: int):
42
42
  lines_byte = line_number // 8
43
43
 
44
- if lines_byte >= len(self._lines):
45
- self._lines.extend(bytearray(lines_byte - len(self._lines) + 1))
44
+ if lines_byte >= self._lines.__len__():
45
+ self._lines.extend(bytearray(lines_byte - self._lines.__len__() + 1))
46
46
 
47
47
  # DEV this fun bit allows us to trick ourselves into little-endianness, which is what the backend wants to see
48
48
  # in bytes
@@ -62,8 +62,8 @@ class CoverageLines:
62
62
 
63
63
  def update(self, other: "CoverageLines"):
64
64
  # Extend our lines if the other coverage has more lines
65
- if len(other._lines) > len(self._lines):
66
- self._lines.extend(bytearray(len(other._lines) - len(self._lines)))
65
+ if other._lines.__len__() > self._lines.__len__():
66
+ self._lines.extend(bytearray(other._lines.__len__() - self._lines.__len__()))
67
67
 
68
68
  for _byte_idx, _byte in enumerate(other._lines):
69
69
  self._lines[_byte_idx] |= _byte
@@ -385,13 +385,28 @@ class HTTPWriter(periodic.PeriodicService, TraceWriter):
385
385
  def _flush_queue_with_client(self, client: WriterClientBase, raise_exc: bool = False) -> None:
386
386
  n_traces = len(client.encoder)
387
387
  try:
388
- encoded, n_traces = client.encoder.encode()
389
-
390
- if encoded is None:
388
+ if not (encoded_traces := client.encoder.encode()):
391
389
  return
392
390
 
393
- # Should gzip the payload if intake accepts it
394
- if self._intake_accepts_gzip:
391
+ except Exception:
392
+ # FIXME(munir): if client.encoder raises an Exception n_traces may not be accurate due to race conditions
393
+ log.error("failed to encode trace with encoder %r", client.encoder, exc_info=True)
394
+ self._metrics_dist("encoder.dropped.traces", n_traces)
395
+ return
396
+
397
+ for payload in encoded_traces:
398
+ encoded_data, n_traces = payload
399
+ self._flush_single_payload(encoded_data, n_traces, client=client, raise_exc=raise_exc)
400
+
401
+ def _flush_single_payload(
402
+ self, encoded: Optional[bytes], n_traces: int, client: WriterClientBase, raise_exc: bool = False
403
+ ) -> None:
404
+ if encoded is None:
405
+ return
406
+
407
+ # Should gzip the payload if intake accepts it
408
+ if self._intake_accepts_gzip:
409
+ try:
395
410
  original_size = len(encoded)
396
411
  # Replace the value to send with the gzipped the value
397
412
  encoded = gzip.compress(encoded, compresslevel=6)
@@ -399,12 +414,10 @@ class HTTPWriter(periodic.PeriodicService, TraceWriter):
399
414
 
400
415
  # And add the header
401
416
  self._headers["Content-Encoding"] = "gzip"
402
-
403
- except Exception:
404
- # FIXME(munir): if client.encoder raises an Exception n_traces may not be accurate due to race conditions
405
- log.error("failed to encode trace with encoder %r", client.encoder, exc_info=True)
406
- self._metrics_dist("encoder.dropped.traces", n_traces)
407
- return
417
+ except Exception:
418
+ log.error("failed to compress traces with encoder %r", client.encoder, exc_info=True)
419
+ self._metrics_dist("encoder.dropped.traces", n_traces)
420
+ return
408
421
 
409
422
  try:
410
423
  self._send_payload_with_backoff(encoded, n_traces, client)
@@ -9,6 +9,7 @@ PARENT_ID_KEY = "_ml_obs.llmobs_parent_id"
9
9
  PROPAGATED_LLMOBS_TRACE_ID_KEY = "_dd.p.llmobs_trace_id"
10
10
  LLMOBS_TRACE_ID = "_ml_obs.llmobs_trace_id"
11
11
  TAGS = "_ml_obs.tags"
12
+ AGENT_MANIFEST = "_ml_obs.meta.agent_manifest"
12
13
 
13
14
  MODEL_NAME = "_ml_obs.meta.model_name"
14
15
  MODEL_PROVIDER = "_ml_obs.meta.model_provider"
@@ -56,6 +57,7 @@ AGENTLESS_EXP_BASE_URL = "https://{}".format(EXP_SUBDOMAIN_NAME)
56
57
  EVP_PAYLOAD_SIZE_LIMIT = 5 << 20 # 5MB (actual limit is 5.1MB)
57
58
  EVP_EVENT_SIZE_LIMIT = (1 << 20) - 1024 # 999KB (actual limit is 1MB)
58
59
 
60
+ EXPERIMENT_CSV_FIELD_MAX_SIZE = 10 * 1024 * 1024
59
61
 
60
62
  DROPPED_IO_COLLECTION_ERROR = "dropped_io"
61
63
  DROPPED_VALUE_TEXT = "[This value has been dropped because this span's size exceeds the 1MB size limit.]"
@@ -97,3 +99,4 @@ PROXY_REQUEST = "llmobs.proxy_request"
97
99
 
98
100
  EXPERIMENT_ID_KEY = "_ml_obs.experiment_id"
99
101
  EXPERIMENT_EXPECTED_OUTPUT = "_ml_obs.meta.input.expected_output"
102
+ DEFAULT_PROJECT_NAME = "default-project"