encore-dev 2.0.0a7__py3-none-any.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.
- encore/__init__.py +43 -0
- encore/cron/__init__.py +50 -0
- encore/internal/__init__.py +1 -0
- encore/internal/_loader.py +79 -0
- encore/internal/_native.pyi +734 -0
- encore/internal/lazy.py +35 -0
- encore/internal/runtime.py +133 -0
- encore/internal/startup.py +24 -0
- encore/logs/__init__.py +19 -0
- encore/logs/_console.py +155 -0
- encore/logs/_runtime.py +56 -0
- encore/meta/__init__.py +228 -0
- encore/metrics/__init__.py +390 -0
- encore/pubsub/__init__.py +425 -0
- encore/py.typed +0 -0
- encore/secrets/__init__.py +139 -0
- encore/shutdown/__init__.py +164 -0
- encore/storage/__init__.py +5 -0
- encore/storage/objects/__init__.py +396 -0
- encore/storage/postgres/__init__.py +737 -0
- encore/storage/redis/__init__.py +200 -0
- encore/storage/redis/_base.py +42 -0
- encore/storage/redis/_responses.py +150 -0
- encore/storage/redis/_values.py +189 -0
- encore/storage/redis/bitmap.py +142 -0
- encore/storage/redis/generic.py +367 -0
- encore/storage/redis/geo.py +508 -0
- encore/storage/redis/hash.py +504 -0
- encore/storage/redis/json.py +614 -0
- encore/storage/redis/list.py +511 -0
- encore/storage/redis/pubsub.py +276 -0
- encore/storage/redis/set.py +570 -0
- encore/storage/redis/sorted_set.py +753 -0
- encore/storage/redis/stream.py +773 -0
- encore/storage/redis/string.py +654 -0
- encore/tracing/__init__.py +19 -0
- encore/tracing/_context.py +27 -0
- encore/tracing/_encode.py +351 -0
- encore/tracing/_provider.py +62 -0
- encore/tracing/_span.py +226 -0
- encore/tracing/_tracer.py +93 -0
- encore_dev-2.0.0a7.dist-info/METADATA +791 -0
- encore_dev-2.0.0a7.dist-info/RECORD +44 -0
- encore_dev-2.0.0a7.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
"""Encoding spans as packed buffers.
|
|
2
|
+
|
|
3
|
+
The decoder documents the layouts in crates/encore_telemetry/src/packed.rs.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import struct
|
|
9
|
+
from typing import Any, Mapping, NamedTuple, Optional, Sequence
|
|
10
|
+
|
|
11
|
+
from opentelemetry.trace import Link, SpanContext
|
|
12
|
+
|
|
13
|
+
VERSION = 1
|
|
14
|
+
HEADER_LEN = 120
|
|
15
|
+
ATTR_LEN = 24
|
|
16
|
+
ITEM_LEN = 8
|
|
17
|
+
EVENT_LEN = 24
|
|
18
|
+
LINK_LEN = 48
|
|
19
|
+
|
|
20
|
+
#: Length of a span context buffer: trace id, span id, flags.
|
|
21
|
+
CTX_LEN = 25
|
|
22
|
+
|
|
23
|
+
#: Bounds the buffer; the runtime enforces the real limits.
|
|
24
|
+
MAX_COUNT = 128
|
|
25
|
+
|
|
26
|
+
TYPE_STR = 0
|
|
27
|
+
TYPE_BOOL = 1
|
|
28
|
+
TYPE_I64 = 2
|
|
29
|
+
TYPE_F64 = 3
|
|
30
|
+
TYPE_STR_ARRAY = 4
|
|
31
|
+
TYPE_BOOL_ARRAY = 5
|
|
32
|
+
TYPE_I64_ARRAY = 6
|
|
33
|
+
TYPE_F64_ARRAY = 7
|
|
34
|
+
|
|
35
|
+
_I64_MAX = (1 << 63) - 1
|
|
36
|
+
_I64_MIN = -(1 << 63)
|
|
37
|
+
|
|
38
|
+
_U32 = struct.Struct("<I")
|
|
39
|
+
_U64 = struct.Struct("<Q")
|
|
40
|
+
_I64 = struct.Struct("<q")
|
|
41
|
+
_F64 = struct.Struct("<d")
|
|
42
|
+
|
|
43
|
+
Attributes = Optional[Mapping[str, Any]]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class Scope(NamedTuple):
|
|
47
|
+
"""An instrumentation scope; an empty version is none."""
|
|
48
|
+
|
|
49
|
+
name: str
|
|
50
|
+
version: str = ""
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class Value(NamedTuple):
|
|
54
|
+
t: int
|
|
55
|
+
v: Any
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _clamp(v: int) -> int:
|
|
59
|
+
return _I64_MAX if v > _I64_MAX else _I64_MIN if v < _I64_MIN else v
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def to_value(v: object) -> Value | None:
|
|
63
|
+
"""An attribute value as a packed value; ``None`` when unsupported."""
|
|
64
|
+
if isinstance(v, str):
|
|
65
|
+
return Value(TYPE_STR, v)
|
|
66
|
+
if isinstance(v, bool):
|
|
67
|
+
return Value(TYPE_BOOL, v)
|
|
68
|
+
if isinstance(v, int):
|
|
69
|
+
return Value(TYPE_I64, _clamp(v))
|
|
70
|
+
if isinstance(v, float):
|
|
71
|
+
return Value(TYPE_F64, v)
|
|
72
|
+
if isinstance(v, (list, tuple)):
|
|
73
|
+
return _array_value(v)
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _array_value(arr: Sequence[object]) -> Value | None:
|
|
78
|
+
"""Typed by the first non-None item; items of other types are dropped."""
|
|
79
|
+
first = next((x for x in arr if x is not None), None)
|
|
80
|
+
if first is None or isinstance(first, str):
|
|
81
|
+
return Value(TYPE_STR_ARRAY, [x for x in arr if isinstance(x, str)])
|
|
82
|
+
if isinstance(first, bool):
|
|
83
|
+
return Value(TYPE_BOOL_ARRAY, [x for x in arr if isinstance(x, bool)])
|
|
84
|
+
if isinstance(first, (int, float)):
|
|
85
|
+
nums = [
|
|
86
|
+
x for x in arr if isinstance(x, (int, float)) and not isinstance(x, bool)
|
|
87
|
+
]
|
|
88
|
+
if all(isinstance(x, int) for x in nums):
|
|
89
|
+
return Value(TYPE_I64_ARRAY, [_clamp(int(x)) for x in nums])
|
|
90
|
+
return Value(TYPE_F64_ARRAY, [float(x) for x in nums])
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class _Event(NamedTuple):
|
|
95
|
+
name: str
|
|
96
|
+
time_unix_nanos: int
|
|
97
|
+
first: int
|
|
98
|
+
count: int
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class _Link(NamedTuple):
|
|
102
|
+
ctx: bytes
|
|
103
|
+
tracestate: str
|
|
104
|
+
first: int
|
|
105
|
+
count: int
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class PackedWriter:
|
|
109
|
+
"""A builder for one packed span buffer.
|
|
110
|
+
|
|
111
|
+
Write span attributes first, then events and links in any order, and
|
|
112
|
+
finally call finish().
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
def __init__(self) -> None:
|
|
116
|
+
self._attrs: list[tuple[str, Value]] = []
|
|
117
|
+
self._span_attr_count = 0
|
|
118
|
+
self._events: list[_Event] = []
|
|
119
|
+
self._links: list[_Link] = []
|
|
120
|
+
|
|
121
|
+
def span_attrs(self, attrs: Attributes) -> PackedWriter:
|
|
122
|
+
if self._attrs or self._events or self._links:
|
|
123
|
+
raise ValueError("span attributes must be written first")
|
|
124
|
+
self._push_attrs(attrs)
|
|
125
|
+
self._span_attr_count = len(self._attrs)
|
|
126
|
+
return self
|
|
127
|
+
|
|
128
|
+
def event(
|
|
129
|
+
self, name: str, time_unix_nanos: int, attrs: Attributes = None
|
|
130
|
+
) -> PackedWriter:
|
|
131
|
+
if len(self._events) >= MAX_COUNT:
|
|
132
|
+
return self
|
|
133
|
+
first, count = self._push_attrs(attrs)
|
|
134
|
+
self._events.append(_Event(name, time_unix_nanos, first, count))
|
|
135
|
+
return self
|
|
136
|
+
|
|
137
|
+
def link(
|
|
138
|
+
self, ctx: bytes, tracestate: str, attrs: Attributes = None
|
|
139
|
+
) -> PackedWriter:
|
|
140
|
+
"""``ctx``: 25 bytes."""
|
|
141
|
+
if len(self._links) >= MAX_COUNT:
|
|
142
|
+
return self
|
|
143
|
+
first, count = self._push_attrs(attrs)
|
|
144
|
+
self._links.append(_Link(ctx, tracestate, first, count))
|
|
145
|
+
return self
|
|
146
|
+
|
|
147
|
+
def _push_attrs(self, attrs: Attributes) -> tuple[int, int]:
|
|
148
|
+
first = len(self._attrs)
|
|
149
|
+
if attrs:
|
|
150
|
+
n = 0
|
|
151
|
+
for key, raw in attrs.items():
|
|
152
|
+
if n >= MAX_COUNT:
|
|
153
|
+
break
|
|
154
|
+
value = to_value(raw)
|
|
155
|
+
if value is None:
|
|
156
|
+
continue
|
|
157
|
+
self._attrs.append((key, value))
|
|
158
|
+
n += 1
|
|
159
|
+
return first, len(self._attrs) - first
|
|
160
|
+
|
|
161
|
+
def finish(
|
|
162
|
+
self,
|
|
163
|
+
*,
|
|
164
|
+
scope: Scope,
|
|
165
|
+
name: str,
|
|
166
|
+
kind: int,
|
|
167
|
+
status_code: int,
|
|
168
|
+
status_message: str = "",
|
|
169
|
+
tracestate: str = "",
|
|
170
|
+
ctx: bytes | None = None,
|
|
171
|
+
parent_span_id: bytes | None = None,
|
|
172
|
+
start_unix_nanos: int = 0,
|
|
173
|
+
start_mono_nanos: int = 0,
|
|
174
|
+
end_unix_nanos: int = 0,
|
|
175
|
+
) -> bytes:
|
|
176
|
+
attrs = self._attrs
|
|
177
|
+
attrs_off = HEADER_LEN
|
|
178
|
+
items_off = attrs_off + len(attrs) * ATTR_LEN
|
|
179
|
+
events_off = items_off + _item_count(attrs) * ITEM_LEN
|
|
180
|
+
links_off = events_off + len(self._events) * EVENT_LEN
|
|
181
|
+
t = _Buffer(links_off + len(self._links) * LINK_LEN)
|
|
182
|
+
fixed = t.fixed
|
|
183
|
+
|
|
184
|
+
fixed[0] = VERSION
|
|
185
|
+
fixed[1] = kind
|
|
186
|
+
fixed[2] = status_code
|
|
187
|
+
fixed[3] = ctx[24] if ctx else 0
|
|
188
|
+
_U32.pack_into(fixed, 4, self._span_attr_count)
|
|
189
|
+
_U32.pack_into(fixed, 8, len(attrs))
|
|
190
|
+
_U32.pack_into(fixed, 12, _item_count(attrs))
|
|
191
|
+
_U32.pack_into(fixed, 16, len(self._events))
|
|
192
|
+
_U32.pack_into(fixed, 20, len(self._links))
|
|
193
|
+
t.ref(24, name)
|
|
194
|
+
t.ref(32, status_message)
|
|
195
|
+
t.ref(40, tracestate)
|
|
196
|
+
t.ref(104, scope.name)
|
|
197
|
+
t.ref(112, scope.version)
|
|
198
|
+
if ctx:
|
|
199
|
+
fixed[48:72] = ctx[:24]
|
|
200
|
+
if parent_span_id:
|
|
201
|
+
fixed[72:80] = parent_span_id[:8]
|
|
202
|
+
_U64.pack_into(fixed, 80, start_unix_nanos)
|
|
203
|
+
_U64.pack_into(fixed, 88, start_mono_nanos)
|
|
204
|
+
_U64.pack_into(fixed, 96, end_unix_nanos)
|
|
205
|
+
|
|
206
|
+
t.attrs(attrs, attrs_off, items_off)
|
|
207
|
+
|
|
208
|
+
for i, e in enumerate(self._events):
|
|
209
|
+
off = events_off + i * EVENT_LEN
|
|
210
|
+
t.ref(off, e.name)
|
|
211
|
+
_U64.pack_into(fixed, off + 8, e.time_unix_nanos)
|
|
212
|
+
_U32.pack_into(fixed, off + 16, e.first)
|
|
213
|
+
_U32.pack_into(fixed, off + 20, e.count)
|
|
214
|
+
|
|
215
|
+
for i, link in enumerate(self._links):
|
|
216
|
+
off = links_off + i * LINK_LEN
|
|
217
|
+
fixed[off : off + CTX_LEN] = link.ctx[:CTX_LEN]
|
|
218
|
+
t.ref(off + 28, link.tracestate)
|
|
219
|
+
_U32.pack_into(fixed, off + 36, link.first)
|
|
220
|
+
_U32.pack_into(fixed, off + 40, link.count)
|
|
221
|
+
|
|
222
|
+
return t.finish()
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _item_count(attrs: Sequence[tuple[str, Value]]) -> int:
|
|
226
|
+
return sum(len(v.v) for _, v in attrs if v.t >= TYPE_STR_ARRAY)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
class _Buffer:
|
|
230
|
+
"""The fixed sections of a buffer, then its data table."""
|
|
231
|
+
|
|
232
|
+
def __init__(self, fixed_len: int) -> None:
|
|
233
|
+
self.fixed = bytearray(fixed_len)
|
|
234
|
+
self._table: list[bytes] = []
|
|
235
|
+
self._table_len = 0
|
|
236
|
+
|
|
237
|
+
def ref(self, off: int, data: str) -> None:
|
|
238
|
+
"""Write a string reference and append its data to the table."""
|
|
239
|
+
b = data.encode("utf-8")
|
|
240
|
+
_U32.pack_into(self.fixed, off, self._table_len)
|
|
241
|
+
_U32.pack_into(self.fixed, off + 4, len(b))
|
|
242
|
+
if b:
|
|
243
|
+
self._table.append(b)
|
|
244
|
+
self._table_len += len(b)
|
|
245
|
+
|
|
246
|
+
def attrs(
|
|
247
|
+
self, attrs: Sequence[tuple[str, Value]], attrs_off: int, items_off: int
|
|
248
|
+
) -> None:
|
|
249
|
+
fixed = self.fixed
|
|
250
|
+
item = 0
|
|
251
|
+
for i, (key, value) in enumerate(attrs):
|
|
252
|
+
off = attrs_off + i * ATTR_LEN
|
|
253
|
+
self.ref(off, key)
|
|
254
|
+
fixed[off + 8] = value.t
|
|
255
|
+
v = off + 16
|
|
256
|
+
t = value.t
|
|
257
|
+
if t == TYPE_STR:
|
|
258
|
+
self.ref(v, value.v)
|
|
259
|
+
elif t == TYPE_BOOL:
|
|
260
|
+
fixed[v] = 1 if value.v else 0
|
|
261
|
+
elif t == TYPE_I64:
|
|
262
|
+
_I64.pack_into(fixed, v, value.v)
|
|
263
|
+
elif t == TYPE_F64:
|
|
264
|
+
_F64.pack_into(fixed, v, value.v)
|
|
265
|
+
else:
|
|
266
|
+
_U32.pack_into(fixed, v, item)
|
|
267
|
+
_U32.pack_into(fixed, v + 4, len(value.v))
|
|
268
|
+
for x in value.v:
|
|
269
|
+
o = items_off + item * ITEM_LEN
|
|
270
|
+
item += 1
|
|
271
|
+
if t == TYPE_STR_ARRAY:
|
|
272
|
+
self.ref(o, x)
|
|
273
|
+
elif t == TYPE_BOOL_ARRAY:
|
|
274
|
+
fixed[o] = 1 if x else 0
|
|
275
|
+
elif t == TYPE_I64_ARRAY:
|
|
276
|
+
_I64.pack_into(fixed, o, x)
|
|
277
|
+
else:
|
|
278
|
+
_F64.pack_into(fixed, o, x)
|
|
279
|
+
|
|
280
|
+
def finish(self) -> bytes:
|
|
281
|
+
return bytes(self.fixed) + b"".join(self._table)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def ctx_bytes(sc: SpanContext) -> bytes:
|
|
285
|
+
"""The 25-byte buffer of a span context."""
|
|
286
|
+
return (
|
|
287
|
+
sc.trace_id.to_bytes(16, "big")
|
|
288
|
+
+ sc.span_id.to_bytes(8, "big")
|
|
289
|
+
+ bytes([sc.trace_flags & 0xFF])
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _write_links(w: PackedWriter, links: Sequence[Link] | None) -> None:
|
|
294
|
+
for link in links or ():
|
|
295
|
+
w.link(
|
|
296
|
+
ctx_bytes(link.context),
|
|
297
|
+
link.context.trace_state.to_header(),
|
|
298
|
+
link.attributes,
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def encode_start(
|
|
303
|
+
scope: Scope, attrs: Attributes, links: Sequence[Link] | None
|
|
304
|
+
) -> bytes:
|
|
305
|
+
"""The start buffer: scope, attributes, and links; other fields zero."""
|
|
306
|
+
w = PackedWriter().span_attrs(attrs)
|
|
307
|
+
_write_links(w, links)
|
|
308
|
+
return w.finish(scope=scope, name="", kind=0, status_code=0)
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
class Event(NamedTuple):
|
|
312
|
+
name: str
|
|
313
|
+
time_unix_nanos: int
|
|
314
|
+
attributes: Attributes = None
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def encode_end(
|
|
318
|
+
*,
|
|
319
|
+
scope: Scope,
|
|
320
|
+
name: str,
|
|
321
|
+
kind: int,
|
|
322
|
+
status_code: int,
|
|
323
|
+
status_message: str,
|
|
324
|
+
tracestate: str,
|
|
325
|
+
ctx: bytes,
|
|
326
|
+
parent_span_id: bytes | None,
|
|
327
|
+
start_unix_nanos: int,
|
|
328
|
+
start_mono_nanos: int,
|
|
329
|
+
end_unix_nanos: int,
|
|
330
|
+
attributes: Attributes,
|
|
331
|
+
events: Sequence[Event],
|
|
332
|
+
links: Sequence[Link],
|
|
333
|
+
) -> bytes:
|
|
334
|
+
"""The end buffer of a finished span."""
|
|
335
|
+
w = PackedWriter().span_attrs(attributes)
|
|
336
|
+
for e in events:
|
|
337
|
+
w.event(e.name, e.time_unix_nanos, e.attributes)
|
|
338
|
+
_write_links(w, links)
|
|
339
|
+
return w.finish(
|
|
340
|
+
scope=scope,
|
|
341
|
+
name=name,
|
|
342
|
+
kind=kind,
|
|
343
|
+
status_code=status_code,
|
|
344
|
+
status_message=status_message,
|
|
345
|
+
tracestate=tracestate,
|
|
346
|
+
ctx=ctx,
|
|
347
|
+
parent_span_id=parent_span_id,
|
|
348
|
+
start_unix_nanos=start_unix_nanos,
|
|
349
|
+
start_mono_nanos=start_mono_nanos,
|
|
350
|
+
end_unix_nanos=end_unix_nanos,
|
|
351
|
+
)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""The ``opentelemetry.trace.TracerProvider`` the runtime installs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
from opentelemetry import propagate, trace
|
|
10
|
+
from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator
|
|
11
|
+
from opentelemetry.util import types
|
|
12
|
+
|
|
13
|
+
from ._encode import Scope
|
|
14
|
+
from ._span import Backend
|
|
15
|
+
from ._tracer import Tracer
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TracerProvider(trace.TracerProvider):
|
|
21
|
+
"""One tracer per instrumentation scope."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, backend: Backend) -> None:
|
|
24
|
+
self._backend = backend
|
|
25
|
+
self._tracers: dict[Scope, Tracer] = {}
|
|
26
|
+
|
|
27
|
+
def get_tracer(
|
|
28
|
+
self,
|
|
29
|
+
instrumenting_module_name: str,
|
|
30
|
+
instrumenting_library_version: Optional[str] = None,
|
|
31
|
+
schema_url: Optional[str] = None,
|
|
32
|
+
attributes: types.Attributes = None,
|
|
33
|
+
) -> Tracer:
|
|
34
|
+
scope = Scope(instrumenting_module_name, instrumenting_library_version or "")
|
|
35
|
+
tracer = self._tracers.get(scope)
|
|
36
|
+
if tracer is None:
|
|
37
|
+
tracer = self._tracers.setdefault(scope, Tracer(self._backend, scope))
|
|
38
|
+
return tracer
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
_installed = False
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def install(backend: Backend) -> None:
|
|
45
|
+
"""Register the tracer provider and W3C propagator globally.
|
|
46
|
+
|
|
47
|
+
Repeated calls leave the installed OpenTelemetry globals unchanged.
|
|
48
|
+
"""
|
|
49
|
+
global _installed
|
|
50
|
+
if _installed:
|
|
51
|
+
return
|
|
52
|
+
_installed = True
|
|
53
|
+
# Instrumentations read this at import; Encore records stable semconv (ADR 0004).
|
|
54
|
+
os.environ.setdefault("OTEL_SEMCONV_STABILITY_OPT_IN", "http")
|
|
55
|
+
provider = TracerProvider(backend)
|
|
56
|
+
trace.set_tracer_provider(provider)
|
|
57
|
+
if trace.get_tracer_provider() is not provider:
|
|
58
|
+
logger.error(
|
|
59
|
+
"encore: a tracer provider was already registered; "
|
|
60
|
+
"spans from opentelemetry-api are not recorded"
|
|
61
|
+
)
|
|
62
|
+
propagate.set_global_textmap(TraceContextTextMapPropagator())
|
encore/tracing/_span.py
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"""A recording span with Python-owned state.
|
|
2
|
+
|
|
3
|
+
Ending a span crosses to the native runtime once.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import threading
|
|
10
|
+
import time
|
|
11
|
+
import traceback
|
|
12
|
+
from typing import Any, Mapping, Optional, Protocol, Sequence
|
|
13
|
+
|
|
14
|
+
from opentelemetry import trace
|
|
15
|
+
from opentelemetry.trace import (
|
|
16
|
+
Link,
|
|
17
|
+
SpanContext,
|
|
18
|
+
SpanKind,
|
|
19
|
+
Status,
|
|
20
|
+
StatusCode,
|
|
21
|
+
TraceFlags,
|
|
22
|
+
TraceState,
|
|
23
|
+
)
|
|
24
|
+
from opentelemetry.util import types
|
|
25
|
+
|
|
26
|
+
from ._encode import MAX_COUNT, Event, Scope, encode_end
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Backend(Protocol):
|
|
32
|
+
"""The runtime's span lifecycle calls (the native ``Pipeline``)."""
|
|
33
|
+
|
|
34
|
+
def start_span(
|
|
35
|
+
self,
|
|
36
|
+
name: str,
|
|
37
|
+
kind: int,
|
|
38
|
+
parent: Optional[bytes],
|
|
39
|
+
tracestate: Optional[str],
|
|
40
|
+
start_unix_nanos: Optional[int],
|
|
41
|
+
packed: bytes,
|
|
42
|
+
) -> tuple[bytes, str, int, int]: ...
|
|
43
|
+
|
|
44
|
+
def end_span(self, packed: bytes) -> None: ...
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class Span(trace.Span):
|
|
48
|
+
"""A recording span: identity plus accumulated data."""
|
|
49
|
+
|
|
50
|
+
def __init__(
|
|
51
|
+
self,
|
|
52
|
+
*,
|
|
53
|
+
backend: Backend,
|
|
54
|
+
scope: Scope,
|
|
55
|
+
ctx_bytes: bytes,
|
|
56
|
+
tracestate: str,
|
|
57
|
+
name: str,
|
|
58
|
+
kind: SpanKind,
|
|
59
|
+
parent_span_id: bytes | None,
|
|
60
|
+
start_unix_nanos: int,
|
|
61
|
+
start_mono_nanos: int,
|
|
62
|
+
attributes: types.Attributes = None,
|
|
63
|
+
links: Sequence[Link] | None = None,
|
|
64
|
+
) -> None:
|
|
65
|
+
self.ctx_bytes = ctx_bytes
|
|
66
|
+
self._backend = backend
|
|
67
|
+
self._scope = scope
|
|
68
|
+
self._tracestate = tracestate
|
|
69
|
+
self._name = name
|
|
70
|
+
self._kind = kind
|
|
71
|
+
self._parent_span_id = parent_span_id
|
|
72
|
+
self._start_unix_nanos = start_unix_nanos
|
|
73
|
+
self._start_mono_nanos = start_mono_nanos
|
|
74
|
+
self._attrs: dict[str, Any] = {}
|
|
75
|
+
self._links: list[Link] = list(links or ())[:MAX_COUNT]
|
|
76
|
+
self._events: list[Event] = []
|
|
77
|
+
self._status = Status(StatusCode.UNSET)
|
|
78
|
+
self._ended = False
|
|
79
|
+
self._lock = threading.Lock()
|
|
80
|
+
self._ctx: SpanContext | None = None
|
|
81
|
+
self._sampled = bool(ctx_bytes[24] & TraceFlags.SAMPLED)
|
|
82
|
+
if attributes:
|
|
83
|
+
self.set_attributes(attributes)
|
|
84
|
+
|
|
85
|
+
# `name` and `kind` mirror the SDK's `ReadableSpan`; instrumentations read them.
|
|
86
|
+
@property
|
|
87
|
+
def name(self) -> str:
|
|
88
|
+
return self._name
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def kind(self) -> SpanKind:
|
|
92
|
+
return self._kind
|
|
93
|
+
|
|
94
|
+
def get_span_context(self) -> SpanContext:
|
|
95
|
+
if self._ctx is None:
|
|
96
|
+
b = self.ctx_bytes
|
|
97
|
+
self._ctx = SpanContext(
|
|
98
|
+
trace_id=int.from_bytes(b[0:16], "big"),
|
|
99
|
+
span_id=int.from_bytes(b[16:24], "big"),
|
|
100
|
+
is_remote=False,
|
|
101
|
+
trace_flags=TraceFlags(b[24]),
|
|
102
|
+
trace_state=TraceState.from_header([self._tracestate])
|
|
103
|
+
if self._tracestate
|
|
104
|
+
else TraceState(),
|
|
105
|
+
)
|
|
106
|
+
return self._ctx
|
|
107
|
+
|
|
108
|
+
def is_recording(self) -> bool:
|
|
109
|
+
"""Return whether the span is sampled and has not ended."""
|
|
110
|
+
return self._sampled and not self._ended
|
|
111
|
+
|
|
112
|
+
def _mutable(self, op: str) -> bool:
|
|
113
|
+
if self._ended:
|
|
114
|
+
logger.warning("encore: %s on ended span %s", op, self._name)
|
|
115
|
+
return False
|
|
116
|
+
return True
|
|
117
|
+
|
|
118
|
+
def set_attribute(self, key: str, value: types.AttributeValue) -> None:
|
|
119
|
+
if not self._mutable("set_attribute"):
|
|
120
|
+
return
|
|
121
|
+
if not key or value is None:
|
|
122
|
+
return
|
|
123
|
+
if key not in self._attrs and len(self._attrs) >= MAX_COUNT:
|
|
124
|
+
return
|
|
125
|
+
self._attrs[key] = value
|
|
126
|
+
|
|
127
|
+
def set_attributes(self, attributes: Mapping[str, types.AttributeValue]) -> None:
|
|
128
|
+
for k, v in attributes.items():
|
|
129
|
+
self.set_attribute(k, v)
|
|
130
|
+
|
|
131
|
+
def add_event(
|
|
132
|
+
self,
|
|
133
|
+
name: str,
|
|
134
|
+
attributes: types.Attributes = None,
|
|
135
|
+
timestamp: int | None = None,
|
|
136
|
+
) -> None:
|
|
137
|
+
if not self._mutable("add_event"):
|
|
138
|
+
return
|
|
139
|
+
if len(self._events) >= MAX_COUNT:
|
|
140
|
+
return
|
|
141
|
+
self._events.append(
|
|
142
|
+
Event(name, time.time_ns() if timestamp is None else timestamp, attributes)
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
def add_link(
|
|
146
|
+
self, context: SpanContext, attributes: types.Attributes = None
|
|
147
|
+
) -> None:
|
|
148
|
+
if not self._mutable("add_link"):
|
|
149
|
+
return
|
|
150
|
+
if not context.is_valid:
|
|
151
|
+
return
|
|
152
|
+
if len(self._links) < MAX_COUNT:
|
|
153
|
+
self._links.append(Link(context, attributes))
|
|
154
|
+
|
|
155
|
+
def update_name(self, name: str) -> None:
|
|
156
|
+
if not self._mutable("update_name"):
|
|
157
|
+
return
|
|
158
|
+
self._name = name
|
|
159
|
+
|
|
160
|
+
def set_status(
|
|
161
|
+
self, status: Status | StatusCode, description: str | None = None
|
|
162
|
+
) -> None:
|
|
163
|
+
if not self._mutable("set_status"):
|
|
164
|
+
return
|
|
165
|
+
if isinstance(status, StatusCode):
|
|
166
|
+
status = Status(status, description)
|
|
167
|
+
# OK is final; UNSET never overrides.
|
|
168
|
+
if status.status_code is StatusCode.UNSET:
|
|
169
|
+
return
|
|
170
|
+
if self._status.status_code is StatusCode.OK:
|
|
171
|
+
return
|
|
172
|
+
self._status = status
|
|
173
|
+
|
|
174
|
+
def record_exception(
|
|
175
|
+
self,
|
|
176
|
+
exception: BaseException,
|
|
177
|
+
attributes: types.Attributes = None,
|
|
178
|
+
timestamp: int | None = None,
|
|
179
|
+
escaped: bool = False,
|
|
180
|
+
) -> None:
|
|
181
|
+
attrs: dict[str, Any] = {
|
|
182
|
+
"exception.type": type(exception).__qualname__,
|
|
183
|
+
"exception.message": str(exception),
|
|
184
|
+
"exception.stacktrace": "".join(
|
|
185
|
+
traceback.format_exception(
|
|
186
|
+
type(exception), exception, exception.__traceback__
|
|
187
|
+
)
|
|
188
|
+
),
|
|
189
|
+
}
|
|
190
|
+
if attributes:
|
|
191
|
+
attrs.update(attributes)
|
|
192
|
+
self.add_event("exception", attrs, timestamp)
|
|
193
|
+
|
|
194
|
+
def end(self, end_time: int | None = None) -> None:
|
|
195
|
+
with self._lock:
|
|
196
|
+
if self._ended:
|
|
197
|
+
logger.warning("encore: end called twice on span %s", self._name)
|
|
198
|
+
return
|
|
199
|
+
self._ended = True
|
|
200
|
+
status = self._status
|
|
201
|
+
try:
|
|
202
|
+
self._backend.end_span(
|
|
203
|
+
encode_end(
|
|
204
|
+
scope=self._scope,
|
|
205
|
+
name=self._name,
|
|
206
|
+
kind=self._kind.value,
|
|
207
|
+
status_code=status.status_code.value,
|
|
208
|
+
status_message=(status.description or "")
|
|
209
|
+
if status.status_code is StatusCode.ERROR
|
|
210
|
+
else "",
|
|
211
|
+
tracestate=self._tracestate,
|
|
212
|
+
ctx=self.ctx_bytes,
|
|
213
|
+
parent_span_id=self._parent_span_id,
|
|
214
|
+
start_unix_nanos=self._start_unix_nanos,
|
|
215
|
+
start_mono_nanos=self._start_mono_nanos,
|
|
216
|
+
end_unix_nanos=0 if end_time is None else end_time,
|
|
217
|
+
attributes=self._attrs,
|
|
218
|
+
events=self._events,
|
|
219
|
+
links=self._links,
|
|
220
|
+
)
|
|
221
|
+
)
|
|
222
|
+
except Exception:
|
|
223
|
+
logger.exception("encore: failed to end span %s", self._name)
|
|
224
|
+
|
|
225
|
+
def __repr__(self) -> str:
|
|
226
|
+
return f"Span(name={self._name!r}, ctx={self.ctx_bytes.hex()})"
|