ddtrace 3.11.0rc1__cp313-cp313-win32.whl → 3.11.0rc3__cp313-cp313-win32.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.
- ddtrace/_logger.py +5 -6
- ddtrace/_trace/product.py +1 -1
- ddtrace/_trace/sampling_rule.py +25 -33
- ddtrace/_trace/trace_handlers.py +12 -50
- ddtrace/_trace/utils_botocore/span_tags.py +48 -0
- ddtrace/_version.py +2 -2
- ddtrace/appsec/_asm_request_context.py +3 -1
- ddtrace/appsec/_constants.py +7 -0
- ddtrace/appsec/_handlers.py +11 -0
- ddtrace/appsec/_iast/_listener.py +12 -2
- ddtrace/appsec/_processor.py +1 -1
- ddtrace/contrib/integration_registry/registry.yaml +10 -0
- ddtrace/contrib/internal/aiobotocore/patch.py +8 -0
- ddtrace/contrib/internal/avro/__init__.py +17 -0
- ddtrace/contrib/internal/azure_functions/patch.py +23 -12
- ddtrace/contrib/internal/azure_functions/utils.py +14 -0
- ddtrace/contrib/internal/boto/patch.py +14 -0
- ddtrace/contrib/internal/botocore/__init__.py +153 -0
- ddtrace/contrib/internal/botocore/services/bedrock.py +3 -27
- ddtrace/contrib/internal/django/patch.py +31 -8
- ddtrace/contrib/{_freezegun.py → internal/freezegun/__init__.py} +1 -1
- ddtrace/contrib/internal/google_genai/_utils.py +2 -2
- ddtrace/contrib/internal/google_genai/patch.py +7 -7
- ddtrace/contrib/internal/google_generativeai/patch.py +7 -5
- ddtrace/contrib/internal/langchain/patch.py +11 -443
- ddtrace/contrib/internal/langchain/utils.py +0 -26
- ddtrace/contrib/internal/logbook/patch.py +1 -2
- ddtrace/contrib/internal/logging/patch.py +4 -7
- ddtrace/contrib/internal/loguru/patch.py +1 -3
- ddtrace/contrib/internal/openai_agents/patch.py +44 -1
- ddtrace/contrib/internal/protobuf/__init__.py +17 -0
- ddtrace/contrib/internal/pytest/__init__.py +62 -0
- ddtrace/contrib/internal/pytest/_plugin_v2.py +13 -4
- ddtrace/contrib/internal/pytest_bdd/__init__.py +23 -0
- ddtrace/contrib/internal/pytest_benchmark/__init__.py +3 -0
- ddtrace/contrib/internal/structlog/patch.py +2 -4
- ddtrace/contrib/internal/unittest/__init__.py +36 -0
- ddtrace/contrib/internal/vertexai/patch.py +7 -5
- ddtrace/ext/ci.py +20 -0
- ddtrace/ext/git.py +66 -11
- ddtrace/internal/_encoding.cp313-win32.pyd +0 -0
- ddtrace/internal/_encoding.pyi +1 -1
- ddtrace/internal/_rand.cp313-win32.pyd +0 -0
- ddtrace/internal/_tagset.cp313-win32.pyd +0 -0
- ddtrace/internal/_threads.cp313-win32.pyd +0 -0
- ddtrace/internal/ci_visibility/encoder.py +126 -49
- ddtrace/internal/ci_visibility/utils.py +4 -4
- ddtrace/internal/core/__init__.py +5 -2
- ddtrace/internal/datadog/profiling/dd_wrapper-unknown-amd64.dll +0 -0
- ddtrace/internal/datadog/profiling/dd_wrapper-unknown-amd64.lib +0 -0
- ddtrace/internal/datadog/profiling/ddup/_ddup.cp313-win32.pyd +0 -0
- ddtrace/internal/datadog/profiling/ddup/_ddup.cp313-win32.pyd.lib +0 -0
- ddtrace/internal/datadog/profiling/ddup/dd_wrapper-unknown-amd64.dll +0 -0
- ddtrace/internal/datadog/profiling/ddup/dd_wrapper-unknown-amd64.lib +0 -0
- ddtrace/internal/endpoints.py +76 -0
- ddtrace/internal/native/_native.cp313-win32.pyd +0 -0
- ddtrace/internal/schema/processor.py +6 -2
- ddtrace/internal/telemetry/metrics_namespaces.cp313-win32.pyd +0 -0
- ddtrace/internal/telemetry/writer.py +18 -0
- ddtrace/internal/test_visibility/coverage_lines.py +4 -4
- ddtrace/internal/writer/writer.py +24 -11
- ddtrace/llmobs/_constants.py +3 -0
- ddtrace/llmobs/_experiment.py +75 -10
- ddtrace/llmobs/_integrations/bedrock.py +4 -0
- ddtrace/llmobs/_integrations/bedrock_agents.py +5 -1
- ddtrace/llmobs/_integrations/crewai.py +52 -3
- ddtrace/llmobs/_integrations/gemini.py +7 -7
- ddtrace/llmobs/_integrations/google_genai.py +10 -10
- ddtrace/llmobs/_integrations/{google_genai_utils.py → google_utils.py} +103 -7
- ddtrace/llmobs/_integrations/langchain.py +29 -20
- ddtrace/llmobs/_integrations/openai_agents.py +145 -0
- ddtrace/llmobs/_integrations/pydantic_ai.py +67 -26
- ddtrace/llmobs/_integrations/utils.py +68 -158
- ddtrace/llmobs/_integrations/vertexai.py +8 -8
- ddtrace/llmobs/_llmobs.py +83 -14
- ddtrace/llmobs/_telemetry.py +20 -5
- ddtrace/llmobs/_utils.py +27 -0
- ddtrace/profiling/_threading.cp313-win32.pyd +0 -0
- ddtrace/profiling/collector/_memalloc.cp313-win32.pyd +0 -0
- ddtrace/profiling/collector/_task.cp313-win32.pyd +0 -0
- ddtrace/profiling/collector/_traceback.cp313-win32.pyd +0 -0
- ddtrace/profiling/collector/stack.cp313-win32.pyd +0 -0
- ddtrace/settings/_config.py +1 -2
- ddtrace/settings/asm.py +9 -2
- ddtrace/settings/profiling.py +0 -9
- ddtrace/vendor/psutil/_psutil_windows.cp313-win32.pyd +0 -0
- {ddtrace-3.11.0rc1.dist-info → ddtrace-3.11.0rc3.dist-info}/METADATA +1 -1
- {ddtrace-3.11.0rc1.dist-info → ddtrace-3.11.0rc3.dist-info}/RECORD +171 -177
- ddtrace/contrib/_avro.py +0 -17
- ddtrace/contrib/_botocore.py +0 -153
- ddtrace/contrib/_protobuf.py +0 -17
- ddtrace/contrib/_pytest.py +0 -62
- ddtrace/contrib/_pytest_bdd.py +0 -23
- ddtrace/contrib/_pytest_benchmark.py +0 -3
- ddtrace/contrib/_unittest.py +0 -36
- /ddtrace/contrib/{_aiobotocore.py → internal/aiobotocore/__init__.py} +0 -0
- /ddtrace/contrib/{_aiohttp_jinja2.py → internal/aiohttp_jinja2/__init__.py} +0 -0
- /ddtrace/contrib/{_aiomysql.py → internal/aiomysql/__init__.py} +0 -0
- /ddtrace/contrib/{_aiopg.py → internal/aiopg/__init__.py} +0 -0
- /ddtrace/contrib/{_aioredis.py → internal/aioredis/__init__.py} +0 -0
- /ddtrace/contrib/{_algoliasearch.py → internal/algoliasearch/__init__.py} +0 -0
- /ddtrace/contrib/{_anthropic.py → internal/anthropic/__init__.py} +0 -0
- /ddtrace/contrib/{_aredis.py → internal/aredis/__init__.py} +0 -0
- /ddtrace/contrib/{_asyncio.py → internal/asyncio/__init__.py} +0 -0
- /ddtrace/contrib/{_asyncpg.py → internal/asyncpg/__init__.py} +0 -0
- /ddtrace/contrib/{_aws_lambda.py → internal/aws_lambda/__init__.py} +0 -0
- /ddtrace/contrib/{_azure_functions.py → internal/azure_functions/__init__.py} +0 -0
- /ddtrace/contrib/{_azure_servicebus.py → internal/azure_servicebus/__init__.py} +0 -0
- /ddtrace/contrib/{_boto.py → internal/boto/__init__.py} +0 -0
- /ddtrace/contrib/{_cassandra.py → internal/cassandra/__init__.py} +0 -0
- /ddtrace/contrib/{_consul.py → internal/consul/__init__.py} +0 -0
- /ddtrace/contrib/{_coverage.py → internal/coverage/__init__.py} +0 -0
- /ddtrace/contrib/{_crewai.py → internal/crewai/__init__.py} +0 -0
- /ddtrace/contrib/{_django.py → internal/django/__init__.py} +0 -0
- /ddtrace/contrib/{_dogpile_cache.py → internal/dogpile_cache/__init__.py} +0 -0
- /ddtrace/contrib/{_dramatiq.py → internal/dramatiq/__init__.py} +0 -0
- /ddtrace/contrib/{_elasticsearch.py → internal/elasticsearch/__init__.py} +0 -0
- /ddtrace/contrib/{_fastapi.py → internal/fastapi/__init__.py} +0 -0
- /ddtrace/contrib/{_flask.py → internal/flask/__init__.py} +0 -0
- /ddtrace/contrib/{_futures.py → internal/futures/__init__.py} +0 -0
- /ddtrace/contrib/{_gevent.py → internal/gevent/__init__.py} +0 -0
- /ddtrace/contrib/{_google_genai.py → internal/google_genai/__init__.py} +0 -0
- /ddtrace/contrib/{_google_generativeai.py → internal/google_generativeai/__init__.py} +0 -0
- /ddtrace/contrib/{_graphql.py → internal/graphql/__init__.py} +0 -0
- /ddtrace/contrib/{_grpc.py → internal/grpc/__init__.py} +0 -0
- /ddtrace/contrib/{_gunicorn.py → internal/gunicorn/__init__.py} +0 -0
- /ddtrace/contrib/{_httplib.py → internal/httplib/__init__.py} +0 -0
- /ddtrace/contrib/{_httpx.py → internal/httpx/__init__.py} +0 -0
- /ddtrace/contrib/{_jinja2.py → internal/jinja2/__init__.py} +0 -0
- /ddtrace/contrib/{_kafka.py → internal/kafka/__init__.py} +0 -0
- /ddtrace/contrib/{_kombu.py → internal/kombu/__init__.py} +0 -0
- /ddtrace/contrib/{_langchain.py → internal/langchain/__init__.py} +0 -0
- /ddtrace/contrib/{_langgraph.py → internal/langgraph/__init__.py} +0 -0
- /ddtrace/contrib/{_litellm.py → internal/litellm/__init__.py} +0 -0
- /ddtrace/contrib/{_logbook.py → internal/logbook/__init__.py} +0 -0
- /ddtrace/contrib/{_logging.py → internal/logging/__init__.py} +0 -0
- /ddtrace/contrib/{_loguru.py → internal/loguru/__init__.py} +0 -0
- /ddtrace/contrib/{_mako.py → internal/mako/__init__.py} +0 -0
- /ddtrace/contrib/{_mariadb.py → internal/mariadb/__init__.py} +0 -0
- /ddtrace/contrib/{_mcp.py → internal/mcp/__init__.py} +0 -0
- /ddtrace/contrib/{_molten.py → internal/molten/__init__.py} +0 -0
- /ddtrace/contrib/{_mongoengine.py → internal/mongoengine/__init__.py} +0 -0
- /ddtrace/contrib/{_mysql.py → internal/mysql/__init__.py} +0 -0
- /ddtrace/contrib/{_mysqldb.py → internal/mysqldb/__init__.py} +0 -0
- /ddtrace/contrib/{_openai.py → internal/openai/__init__.py} +0 -0
- /ddtrace/contrib/{_openai_agents.py → internal/openai_agents/__init__.py} +0 -0
- /ddtrace/contrib/{_psycopg.py → internal/psycopg/__init__.py} +0 -0
- /ddtrace/contrib/{_pydantic_ai.py → internal/pydantic_ai/__init__.py} +0 -0
- /ddtrace/contrib/{_pymemcache.py → internal/pymemcache/__init__.py} +0 -0
- /ddtrace/contrib/{_pymongo.py → internal/pymongo/__init__.py} +0 -0
- /ddtrace/contrib/{_pymysql.py → internal/pymysql/__init__.py} +0 -0
- /ddtrace/contrib/{_pynamodb.py → internal/pynamodb/__init__.py} +0 -0
- /ddtrace/contrib/{_pyodbc.py → internal/pyodbc/__init__.py} +0 -0
- /ddtrace/contrib/{_redis.py → internal/redis/__init__.py} +0 -0
- /ddtrace/contrib/{_rediscluster.py → internal/rediscluster/__init__.py} +0 -0
- /ddtrace/contrib/{_rq.py → internal/rq/__init__.py} +0 -0
- /ddtrace/contrib/{_sanic.py → internal/sanic/__init__.py} +0 -0
- /ddtrace/contrib/{_selenium.py → internal/selenium/__init__.py} +0 -0
- /ddtrace/contrib/{_snowflake.py → internal/snowflake/__init__.py} +0 -0
- /ddtrace/contrib/{_sqlite3.py → internal/sqlite3/__init__.py} +0 -0
- /ddtrace/contrib/{_starlette.py → internal/starlette/__init__.py} +0 -0
- /ddtrace/contrib/{_structlog.py → internal/structlog/__init__.py} +0 -0
- /ddtrace/contrib/{_subprocess.py → internal/subprocess/__init__.py} +0 -0
- /ddtrace/contrib/{_urllib.py → internal/urllib/__init__.py} +0 -0
- /ddtrace/contrib/{_urllib3.py → internal/urllib3/__init__.py} +0 -0
- /ddtrace/contrib/{_vertexai.py → internal/vertexai/__init__.py} +0 -0
- /ddtrace/contrib/{_vertica.py → internal/vertica/__init__.py} +0 -0
- /ddtrace/contrib/{_webbrowser.py → internal/webbrowser/__init__.py} +0 -0
- /ddtrace/contrib/{_yaaredis.py → internal/yaaredis/__init__.py} +0 -0
- {ddtrace-3.11.0rc1.dist-info → ddtrace-3.11.0rc3.dist-info}/WHEEL +0 -0
- {ddtrace-3.11.0rc1.dist-info → ddtrace-3.11.0rc3.dist-info}/entry_points.txt +0 -0
- {ddtrace-3.11.0rc1.dist-info → ddtrace-3.11.0rc3.dist-info}/licenses/LICENSE +0 -0
- {ddtrace-3.11.0rc1.dist-info → ddtrace-3.11.0rc3.dist-info}/licenses/LICENSE.Apache +0 -0
- {ddtrace-3.11.0rc1.dist-info → ddtrace-3.11.0rc3.dist-info}/licenses/LICENSE.BSD3 +0 -0
- {ddtrace-3.11.0rc1.dist-info → ddtrace-3.11.0rc3.dist-info}/licenses/NOTICE +0 -0
- {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.
|
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,
|
69
|
+
def put(self, item):
|
67
70
|
with self._lock:
|
68
|
-
self.buffer.append(
|
71
|
+
self.buffer.append(item)
|
69
72
|
|
70
73
|
def encode_traces(self, traces):
|
71
|
-
|
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
|
-
|
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
|
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
|
-
|
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
|
-
|
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
|
-
{
|
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(
|
112
|
-
|
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,
|
261
|
+
def put(self, item):
|
181
262
|
spans_with_coverage = [
|
182
263
|
span
|
183
|
-
for span in
|
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
|
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(
|
240
|
-
|
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:
|
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
|
-
|
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
|
-
|
140
|
-
|
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,
|
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:
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
@@ -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
|
Binary file
|
@@ -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
|
-
|
14
|
-
|
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,
|
Binary file
|
@@ -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 >=
|
45
|
-
self._lines.extend(bytearray(lines_byte -
|
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
|
66
|
-
self._lines.extend(bytearray(
|
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
|
-
|
389
|
-
|
390
|
-
if encoded is None:
|
388
|
+
if not (encoded_traces := client.encoder.encode()):
|
391
389
|
return
|
392
390
|
|
393
|
-
|
394
|
-
if
|
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
|
-
|
404
|
-
|
405
|
-
|
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)
|
ddtrace/llmobs/_constants.py
CHANGED
@@ -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"
|