telemetry-dev 0.1.0__tar.gz → 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {telemetry_dev-0.1.0 → telemetry_dev-0.2.0}/PKG-INFO +1 -1
- {telemetry_dev-0.1.0 → telemetry_dev-0.2.0}/pyproject.toml +1 -1
- {telemetry_dev-0.1.0 → telemetry_dev-0.2.0}/src/telemetry_dev/__init__.py +2 -0
- telemetry_dev-0.2.0/src/telemetry_dev/_capture.py +177 -0
- {telemetry_dev-0.1.0 → telemetry_dev-0.2.0}/README.md +0 -0
- {telemetry_dev-0.1.0 → telemetry_dev-0.2.0}/src/telemetry_dev/_client.py +0 -0
- {telemetry_dev-0.1.0 → telemetry_dev-0.2.0}/src/telemetry_dev/_config.py +0 -0
- {telemetry_dev-0.1.0 → telemetry_dev-0.2.0}/src/telemetry_dev/_context.py +0 -0
- {telemetry_dev-0.1.0 → telemetry_dev-0.2.0}/src/telemetry_dev/_logs.py +0 -0
- {telemetry_dev-0.1.0 → telemetry_dev-0.2.0}/src/telemetry_dev/_metrics.py +0 -0
- {telemetry_dev-0.1.0 → telemetry_dev-0.2.0}/src/telemetry_dev/_observe.py +0 -0
- {telemetry_dev-0.1.0 → telemetry_dev-0.2.0}/src/telemetry_dev/_processor.py +0 -0
- {telemetry_dev-0.1.0 → telemetry_dev-0.2.0}/src/telemetry_dev/_semconv.py +0 -0
- {telemetry_dev-0.1.0 → telemetry_dev-0.2.0}/src/telemetry_dev/_serialize.py +0 -0
- {telemetry_dev-0.1.0 → telemetry_dev-0.2.0}/src/telemetry_dev/_spans.py +0 -0
- {telemetry_dev-0.1.0 → telemetry_dev-0.2.0}/src/telemetry_dev/otel.py +0 -0
- {telemetry_dev-0.1.0 → telemetry_dev-0.2.0}/src/telemetry_dev/py.typed +0 -0
|
@@ -12,6 +12,7 @@ Quickstart::
|
|
|
12
12
|
span.update(output=completion, usage={"input_tokens": 11, "output_tokens": 7})
|
|
13
13
|
"""
|
|
14
14
|
|
|
15
|
+
from ._capture import CaptureBudget
|
|
15
16
|
from ._client import Client, flush, get_client, init, shutdown
|
|
16
17
|
from ._config import SDK_VERSION as __version__
|
|
17
18
|
from ._config import LogLevelOption
|
|
@@ -32,6 +33,7 @@ from .otel import TelemetrySpanProcessor
|
|
|
32
33
|
__all__ = [
|
|
33
34
|
"NOT_GIVEN",
|
|
34
35
|
"AttributeValue",
|
|
36
|
+
"CaptureBudget",
|
|
35
37
|
"Client",
|
|
36
38
|
"LogLevel",
|
|
37
39
|
"LogLevelOption",
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""Bounded retention for provider streaming instrumentation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterable
|
|
6
|
+
from typing import Final, cast
|
|
7
|
+
|
|
8
|
+
_DEFAULT_MAX_BYTES: Final = 64 * 1024
|
|
9
|
+
_DEFAULT_MAX_ITEMS: Final = 1024
|
|
10
|
+
_MAX_DEPTH: Final = 32
|
|
11
|
+
_BASE_ITEM_BYTES: Final = 16
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CaptureBudget:
|
|
15
|
+
"""Track a hard byte and item budget before retaining streamed provider data."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, max_bytes: int = _DEFAULT_MAX_BYTES, max_items: int = _DEFAULT_MAX_ITEMS):
|
|
18
|
+
self.max_bytes: int = max(0, min(max_bytes, _DEFAULT_MAX_BYTES))
|
|
19
|
+
self.max_items: int = max(0, min(max_items, _DEFAULT_MAX_ITEMS))
|
|
20
|
+
self.bytes_used: int = 0
|
|
21
|
+
self.items_used: int = 0
|
|
22
|
+
self.truncated: bool = False
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def from_client(cls) -> CaptureBudget:
|
|
26
|
+
"""Use the active client's attribute limit without exceeding the SDK ceiling."""
|
|
27
|
+
from ._client import get_client
|
|
28
|
+
|
|
29
|
+
client = get_client()
|
|
30
|
+
return cls(
|
|
31
|
+
max_bytes=client.max_attribute_length if client is not None else _DEFAULT_MAX_BYTES
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def remaining_bytes(self) -> int:
|
|
36
|
+
"""Return bytes still available for retained capture state."""
|
|
37
|
+
return self.max_bytes - self.bytes_used
|
|
38
|
+
|
|
39
|
+
def accept(self, value: object) -> bool:
|
|
40
|
+
"""Reserve space for a value, returning false without retaining it when over budget."""
|
|
41
|
+
if self.truncated:
|
|
42
|
+
return False
|
|
43
|
+
measured = self._measure(
|
|
44
|
+
value,
|
|
45
|
+
remaining_bytes=self.remaining_bytes,
|
|
46
|
+
remaining_items=self.max_items - self.items_used,
|
|
47
|
+
depth=0,
|
|
48
|
+
seen=set(),
|
|
49
|
+
)
|
|
50
|
+
if measured is None:
|
|
51
|
+
self.truncated = True
|
|
52
|
+
return False
|
|
53
|
+
byte_count, item_count = measured
|
|
54
|
+
self.bytes_used += byte_count
|
|
55
|
+
self.items_used += item_count
|
|
56
|
+
return True
|
|
57
|
+
|
|
58
|
+
def capture_bytes(self, value: bytes | bytearray | memoryview) -> bytes:
|
|
59
|
+
"""Retain at most the remaining byte prefix while marking a partial capture."""
|
|
60
|
+
if self.truncated:
|
|
61
|
+
return b""
|
|
62
|
+
view = memoryview(value)
|
|
63
|
+
try:
|
|
64
|
+
byte_view = view.cast("B")
|
|
65
|
+
except TypeError:
|
|
66
|
+
byte_view = memoryview(bytes(view))
|
|
67
|
+
byte_length = len(byte_view)
|
|
68
|
+
if byte_length == 0:
|
|
69
|
+
return b""
|
|
70
|
+
if self.items_used >= self.max_items or self.remaining_bytes <= 0:
|
|
71
|
+
self.truncated = True
|
|
72
|
+
return b""
|
|
73
|
+
retained_length = min(byte_length, self.remaining_bytes)
|
|
74
|
+
self.items_used += 1
|
|
75
|
+
self.bytes_used += retained_length
|
|
76
|
+
if retained_length < byte_length:
|
|
77
|
+
self.truncated = True
|
|
78
|
+
if isinstance(value, bytes) and retained_length == byte_length:
|
|
79
|
+
return value
|
|
80
|
+
return bytes(byte_view[:retained_length])
|
|
81
|
+
|
|
82
|
+
@classmethod
|
|
83
|
+
def _measure(
|
|
84
|
+
cls,
|
|
85
|
+
value: object,
|
|
86
|
+
*,
|
|
87
|
+
remaining_bytes: int,
|
|
88
|
+
remaining_items: int,
|
|
89
|
+
depth: int,
|
|
90
|
+
seen: set[int],
|
|
91
|
+
) -> tuple[int, int] | None:
|
|
92
|
+
if remaining_items <= 0 or remaining_bytes < _BASE_ITEM_BYTES or depth > _MAX_DEPTH:
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
byte_count = _BASE_ITEM_BYTES
|
|
96
|
+
item_count = 1
|
|
97
|
+
if value is None or isinstance(value, bool | int | float):
|
|
98
|
+
return byte_count, item_count
|
|
99
|
+
if isinstance(value, str):
|
|
100
|
+
for character in value:
|
|
101
|
+
codepoint = ord(character)
|
|
102
|
+
byte_count += (
|
|
103
|
+
1
|
|
104
|
+
if codepoint <= 0x7F
|
|
105
|
+
else 2
|
|
106
|
+
if codepoint <= 0x7FF
|
|
107
|
+
else 3
|
|
108
|
+
if codepoint <= 0xFFFF
|
|
109
|
+
else 4
|
|
110
|
+
)
|
|
111
|
+
if byte_count > remaining_bytes:
|
|
112
|
+
return None
|
|
113
|
+
return byte_count, item_count
|
|
114
|
+
if isinstance(value, bytes | bytearray | memoryview):
|
|
115
|
+
length = value.nbytes if isinstance(value, memoryview) else len(value)
|
|
116
|
+
# Structured attributes serialize binary values as base64 text.
|
|
117
|
+
byte_count += 4 * ((length + 2) // 3)
|
|
118
|
+
return (byte_count, item_count) if byte_count <= remaining_bytes else None
|
|
119
|
+
|
|
120
|
+
value_id = id(value)
|
|
121
|
+
if value_id in seen:
|
|
122
|
+
return byte_count, item_count
|
|
123
|
+
seen.add(value_id)
|
|
124
|
+
mapping: dict[object, object] | None = None
|
|
125
|
+
children: Iterable[object] = ()
|
|
126
|
+
if isinstance(value, dict):
|
|
127
|
+
mapping = cast("dict[object, object]", value)
|
|
128
|
+
elif isinstance(value, list | tuple | set | frozenset):
|
|
129
|
+
children = cast("Iterable[object]", value)
|
|
130
|
+
else:
|
|
131
|
+
attributes = getattr(value, "__dict__", None)
|
|
132
|
+
if isinstance(attributes, dict):
|
|
133
|
+
mapping = cast("dict[object, object]", attributes)
|
|
134
|
+
|
|
135
|
+
if mapping is not None:
|
|
136
|
+
for key, child in mapping.items():
|
|
137
|
+
measured = cls._measure(
|
|
138
|
+
key,
|
|
139
|
+
remaining_bytes=remaining_bytes - byte_count,
|
|
140
|
+
remaining_items=remaining_items - item_count,
|
|
141
|
+
depth=depth + 1,
|
|
142
|
+
seen=seen,
|
|
143
|
+
)
|
|
144
|
+
if measured is None:
|
|
145
|
+
return None
|
|
146
|
+
child_bytes, child_items = measured
|
|
147
|
+
byte_count += child_bytes
|
|
148
|
+
item_count += child_items
|
|
149
|
+
|
|
150
|
+
measured = cls._measure(
|
|
151
|
+
child,
|
|
152
|
+
remaining_bytes=remaining_bytes - byte_count,
|
|
153
|
+
remaining_items=remaining_items - item_count,
|
|
154
|
+
depth=depth + 1,
|
|
155
|
+
seen=seen,
|
|
156
|
+
)
|
|
157
|
+
if measured is None:
|
|
158
|
+
return None
|
|
159
|
+
child_bytes, child_items = measured
|
|
160
|
+
byte_count += child_bytes
|
|
161
|
+
item_count += child_items
|
|
162
|
+
return byte_count, item_count
|
|
163
|
+
|
|
164
|
+
for child in children:
|
|
165
|
+
measured = cls._measure(
|
|
166
|
+
child,
|
|
167
|
+
remaining_bytes=remaining_bytes - byte_count,
|
|
168
|
+
remaining_items=remaining_items - item_count,
|
|
169
|
+
depth=depth + 1,
|
|
170
|
+
seen=seen,
|
|
171
|
+
)
|
|
172
|
+
if measured is None:
|
|
173
|
+
return None
|
|
174
|
+
child_bytes, child_items = measured
|
|
175
|
+
byte_count += child_bytes
|
|
176
|
+
item_count += child_items
|
|
177
|
+
return byte_count, item_count
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|