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.
Files changed (44) hide show
  1. encore/__init__.py +43 -0
  2. encore/cron/__init__.py +50 -0
  3. encore/internal/__init__.py +1 -0
  4. encore/internal/_loader.py +79 -0
  5. encore/internal/_native.pyi +734 -0
  6. encore/internal/lazy.py +35 -0
  7. encore/internal/runtime.py +133 -0
  8. encore/internal/startup.py +24 -0
  9. encore/logs/__init__.py +19 -0
  10. encore/logs/_console.py +155 -0
  11. encore/logs/_runtime.py +56 -0
  12. encore/meta/__init__.py +228 -0
  13. encore/metrics/__init__.py +390 -0
  14. encore/pubsub/__init__.py +425 -0
  15. encore/py.typed +0 -0
  16. encore/secrets/__init__.py +139 -0
  17. encore/shutdown/__init__.py +164 -0
  18. encore/storage/__init__.py +5 -0
  19. encore/storage/objects/__init__.py +396 -0
  20. encore/storage/postgres/__init__.py +737 -0
  21. encore/storage/redis/__init__.py +200 -0
  22. encore/storage/redis/_base.py +42 -0
  23. encore/storage/redis/_responses.py +150 -0
  24. encore/storage/redis/_values.py +189 -0
  25. encore/storage/redis/bitmap.py +142 -0
  26. encore/storage/redis/generic.py +367 -0
  27. encore/storage/redis/geo.py +508 -0
  28. encore/storage/redis/hash.py +504 -0
  29. encore/storage/redis/json.py +614 -0
  30. encore/storage/redis/list.py +511 -0
  31. encore/storage/redis/pubsub.py +276 -0
  32. encore/storage/redis/set.py +570 -0
  33. encore/storage/redis/sorted_set.py +753 -0
  34. encore/storage/redis/stream.py +773 -0
  35. encore/storage/redis/string.py +654 -0
  36. encore/tracing/__init__.py +19 -0
  37. encore/tracing/_context.py +27 -0
  38. encore/tracing/_encode.py +351 -0
  39. encore/tracing/_provider.py +62 -0
  40. encore/tracing/_span.py +226 -0
  41. encore/tracing/_tracer.py +93 -0
  42. encore_dev-2.0.0a7.dist-info/METADATA +791 -0
  43. encore_dev-2.0.0a7.dist-info/RECORD +44 -0
  44. 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())
@@ -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()})"