unique_toolkit 2026.34.0.dev3__py3-none-any.whl → 2026.34.0.dev5__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.
- unique_toolkit/monitoring/__init__.py +17 -1
- unique_toolkit/monitoring/tracing.py +201 -0
- {unique_toolkit-2026.34.0.dev3.dist-info → unique_toolkit-2026.34.0.dev5.dist-info}/METADATA +5 -2
- {unique_toolkit-2026.34.0.dev3.dist-info → unique_toolkit-2026.34.0.dev5.dist-info}/RECORD +5 -4
- {unique_toolkit-2026.34.0.dev3.dist-info → unique_toolkit-2026.34.0.dev5.dist-info}/WHEEL +0 -0
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
from unique_toolkit.monitoring.tracing import (
|
|
2
|
+
TraceContextMiddleware,
|
|
3
|
+
TraceExporter,
|
|
4
|
+
TracingSettings,
|
|
5
|
+
configure_tracing,
|
|
6
|
+
inject_trace_headers,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"TraceContextMiddleware",
|
|
11
|
+
"TraceExporter",
|
|
12
|
+
"TracingSettings",
|
|
13
|
+
"configure_tracing",
|
|
14
|
+
"inject_trace_headers",
|
|
15
|
+
]
|
|
16
|
+
|
|
1
17
|
_MONITORING_AVAILABLE = False
|
|
2
18
|
try:
|
|
3
19
|
from prometheus_client import ( # pyright: ignore[reportMissingImports]
|
|
@@ -27,7 +43,7 @@ if _MONITORING_AVAILABLE:
|
|
|
27
43
|
track_execution,
|
|
28
44
|
)
|
|
29
45
|
|
|
30
|
-
__all__
|
|
46
|
+
__all__ += [
|
|
31
47
|
"REGISTRY",
|
|
32
48
|
"MetricNamespace",
|
|
33
49
|
"MetricsMiddleware",
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""OpenTelemetry tracing bootstrap for toolkit consumers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Awaitable, Callable, Iterable, MutableMapping
|
|
6
|
+
from enum import StrEnum
|
|
7
|
+
from typing import TYPE_CHECKING, ClassVar, Self, TypeAlias, cast
|
|
8
|
+
|
|
9
|
+
from pydantic import AliasChoices, Field
|
|
10
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from opentelemetry.trace.propagation.tracecontext import (
|
|
14
|
+
TraceContextTextMapPropagator,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
# Keep ASGI aliases local; tracing should not depend on asgiref solely for typing.
|
|
18
|
+
ASGIScope: TypeAlias = MutableMapping[str, object]
|
|
19
|
+
ASGIMessage: TypeAlias = MutableMapping[str, object]
|
|
20
|
+
ASGIReceive: TypeAlias = Callable[[], Awaitable[ASGIMessage]]
|
|
21
|
+
ASGISend: TypeAlias = Callable[[ASGIMessage], Awaitable[None]]
|
|
22
|
+
ASGIApp: TypeAlias = Callable[[ASGIScope, ASGIReceive, ASGISend], Awaitable[None]]
|
|
23
|
+
|
|
24
|
+
_OTEL_EXTRA_MESSAGE = (
|
|
25
|
+
"OpenTelemetry tracing requires the unique_toolkit[otel] extra. "
|
|
26
|
+
"Install it with: uv add 'unique-toolkit[otel]'"
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class TraceExporter(StrEnum):
|
|
31
|
+
"""Trace exporters supported by the toolkit bootstrap."""
|
|
32
|
+
|
|
33
|
+
NONE = "none"
|
|
34
|
+
CONSOLE = "console"
|
|
35
|
+
OTLP = "otlp"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class TracingSettings(BaseSettings):
|
|
39
|
+
"""Environment settings for optional OpenTelemetry tracing."""
|
|
40
|
+
|
|
41
|
+
model_config: ClassVar[SettingsConfigDict] = SettingsConfigDict(
|
|
42
|
+
env_prefix="OTEL_",
|
|
43
|
+
env_ignore_empty=True,
|
|
44
|
+
extra="ignore",
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
traces_exporter: TraceExporter | None = None
|
|
48
|
+
service_name: str | None = None
|
|
49
|
+
service_version: str | None = Field(
|
|
50
|
+
default=None,
|
|
51
|
+
validation_alias=AliasChoices("OTEL_SERVICE_VERSION", "VERSION"),
|
|
52
|
+
)
|
|
53
|
+
otlp_endpoint: str | None = Field(
|
|
54
|
+
default=None,
|
|
55
|
+
validation_alias=AliasChoices(
|
|
56
|
+
"OTEL_EXPORTER_OTLP_TRACES_ENDPOINT",
|
|
57
|
+
"OTEL_EXPORTER_OTLP_ENDPOINT",
|
|
58
|
+
),
|
|
59
|
+
)
|
|
60
|
+
span_processor: TraceExporter | None = None
|
|
61
|
+
enabled: bool | None = Field(
|
|
62
|
+
default=None,
|
|
63
|
+
validation_alias="ENABLE_OPENTELEMETRY",
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def exporter(self) -> TraceExporter | None:
|
|
68
|
+
"""Resolve standard OTel settings with Node service compatibility aliases."""
|
|
69
|
+
if self.traces_exporter:
|
|
70
|
+
return self.traces_exporter
|
|
71
|
+
if self.otlp_endpoint:
|
|
72
|
+
return TraceExporter.OTLP
|
|
73
|
+
if self.enabled is False:
|
|
74
|
+
return None
|
|
75
|
+
if self.enabled is True:
|
|
76
|
+
return self.span_processor or TraceExporter.OTLP
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
@classmethod
|
|
80
|
+
def try_load(cls) -> Self | None:
|
|
81
|
+
"""Load env settings, or ``None`` when tracing cannot be enabled."""
|
|
82
|
+
settings = cls()
|
|
83
|
+
if settings.exporter in {None, TraceExporter.NONE}:
|
|
84
|
+
return None
|
|
85
|
+
return settings
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _missing_otel_extra() -> ImportError:
|
|
89
|
+
"""Create a consistent error for an unavailable optional tracing dependency."""
|
|
90
|
+
return ImportError(_OTEL_EXTRA_MESSAGE)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _trace_context_propagator() -> TraceContextTextMapPropagator:
|
|
94
|
+
"""Return a W3C trace-context propagator without forwarding baggage."""
|
|
95
|
+
try:
|
|
96
|
+
from opentelemetry.trace.propagation.tracecontext import (
|
|
97
|
+
TraceContextTextMapPropagator,
|
|
98
|
+
)
|
|
99
|
+
except ImportError as error:
|
|
100
|
+
raise _missing_otel_extra() from error
|
|
101
|
+
return TraceContextTextMapPropagator()
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class TraceContextMiddleware:
|
|
105
|
+
"""ASGI middleware that continues W3C trace context with one server span."""
|
|
106
|
+
|
|
107
|
+
def __init__(self, app: ASGIApp, span_name: str = "HTTP request") -> None:
|
|
108
|
+
self.app: ASGIApp = app
|
|
109
|
+
self._span_name: str = span_name
|
|
110
|
+
|
|
111
|
+
async def __call__(
|
|
112
|
+
self, scope: ASGIScope, receive: ASGIReceive, send: ASGISend
|
|
113
|
+
) -> None:
|
|
114
|
+
if scope["type"] != "http":
|
|
115
|
+
return await self.app(scope, receive, send)
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
from opentelemetry import trace
|
|
119
|
+
from opentelemetry.trace import SpanKind
|
|
120
|
+
except ImportError as error:
|
|
121
|
+
raise _missing_otel_extra() from error
|
|
122
|
+
|
|
123
|
+
headers: dict[str, str] = {}
|
|
124
|
+
raw_headers = cast(Iterable[tuple[bytes, bytes]], scope.get("headers", ()))
|
|
125
|
+
for name, value in raw_headers:
|
|
126
|
+
key = name.decode("latin-1").lower()
|
|
127
|
+
if key not in {"traceparent", "tracestate"}:
|
|
128
|
+
continue
|
|
129
|
+
header_value = value.decode("latin-1")
|
|
130
|
+
if key == "tracestate" and key in headers:
|
|
131
|
+
headers[key] = f"{headers[key]},{header_value}"
|
|
132
|
+
elif key not in headers:
|
|
133
|
+
headers[key] = header_value
|
|
134
|
+
parent_context = _trace_context_propagator().extract(headers)
|
|
135
|
+
with trace.get_tracer(__name__).start_as_current_span(
|
|
136
|
+
self._span_name,
|
|
137
|
+
context=parent_context,
|
|
138
|
+
kind=SpanKind.SERVER,
|
|
139
|
+
):
|
|
140
|
+
return await self.app(scope, receive, send)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def inject_trace_headers(headers: MutableMapping[str, str]) -> None:
|
|
144
|
+
"""Inject the active W3C trace context into outgoing request headers."""
|
|
145
|
+
_trace_context_propagator().inject(headers)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def configure_tracing(
|
|
149
|
+
*,
|
|
150
|
+
service_name: str | None = None,
|
|
151
|
+
service_version: str | None = None,
|
|
152
|
+
) -> bool:
|
|
153
|
+
"""Install an OpenTelemetry trace provider from standard and Node-compatible env.
|
|
154
|
+
|
|
155
|
+
``OTEL_TRACES_EXPORTER`` takes precedence. When it is unset,
|
|
156
|
+
``ENABLE_OPENTELEMETRY`` and ``OTEL_SPAN_PROCESSOR`` mirror the Node service
|
|
157
|
+
tracing configuration. Prometheus metrics and application logging remain
|
|
158
|
+
separate, so ``OTEL_METRICS_READER`` and ``OTEL_LOGS_PROCESSOR`` are ignored.
|
|
159
|
+
"""
|
|
160
|
+
settings = TracingSettings.try_load()
|
|
161
|
+
if settings is None:
|
|
162
|
+
return False
|
|
163
|
+
exporter = settings.exporter
|
|
164
|
+
assert exporter is not None # try_load guarantees a concrete exporter
|
|
165
|
+
|
|
166
|
+
try:
|
|
167
|
+
from opentelemetry import trace
|
|
168
|
+
from opentelemetry.exporter.otlp.proto.http.trace_exporter import (
|
|
169
|
+
OTLPSpanExporter,
|
|
170
|
+
)
|
|
171
|
+
from opentelemetry.sdk.resources import Resource
|
|
172
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
173
|
+
from opentelemetry.sdk.trace.export import (
|
|
174
|
+
BatchSpanProcessor,
|
|
175
|
+
ConsoleSpanExporter,
|
|
176
|
+
SimpleSpanProcessor,
|
|
177
|
+
)
|
|
178
|
+
from opentelemetry.trace import ProxyTracerProvider
|
|
179
|
+
except ImportError as error:
|
|
180
|
+
raise _missing_otel_extra() from error
|
|
181
|
+
|
|
182
|
+
if not isinstance(trace.get_tracer_provider(), ProxyTracerProvider):
|
|
183
|
+
return True
|
|
184
|
+
|
|
185
|
+
name = service_name if service_name is not None else settings.service_name
|
|
186
|
+
version = (
|
|
187
|
+
service_version if service_version is not None else settings.service_version
|
|
188
|
+
)
|
|
189
|
+
attributes = {
|
|
190
|
+
key: value
|
|
191
|
+
for key, value in (("service.name", name), ("service.version", version))
|
|
192
|
+
if value is not None
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
provider = TracerProvider(resource=Resource.create(attributes))
|
|
196
|
+
if exporter is TraceExporter.CONSOLE:
|
|
197
|
+
provider.add_span_processor(SimpleSpanProcessor(ConsoleSpanExporter()))
|
|
198
|
+
else:
|
|
199
|
+
provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter()))
|
|
200
|
+
trace.set_tracer_provider(provider)
|
|
201
|
+
return True
|
{unique_toolkit-2026.34.0.dev3.dist-info → unique_toolkit-2026.34.0.dev5.dist-info}/METADATA
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: unique-toolkit
|
|
3
|
-
Version: 2026.34.0.
|
|
3
|
+
Version: 2026.34.0.dev5
|
|
4
4
|
Summary:
|
|
5
5
|
Author: Cedric Klinkert, Martin Fadler, Sadique Sheik, Fabian Schläpfer, Pascal Hauri, Ejiroghene Oki-Peter, Rami Azouz, Ricardo Visini, Sanzio Monti
|
|
6
6
|
Author-email: Cedric Klinkert <cedric.klinkert@unique.ch>, Martin Fadler <martin.fadler@unique.ch>, Sadique Sheik <sadique@unique.ch>, Fabian Schläpfer <fabian@unique.ch>, Pascal Hauri <pascal@unique.ch>, Ejiroghene Oki-Peter <oki-peter.ejiroghene@unique.ai>, Rami Azouz <rami.ext@unique.ch>, Ricardo Visini <ricardo.visini@unique.ch>, Sanzio Monti <sanzio.monti@unique.ai>
|
|
@@ -16,7 +16,7 @@ Requires-Dist: sseclient>=0.0.27,<0.0.28
|
|
|
16
16
|
Requires-Dist: openai>=1.105.0,<3
|
|
17
17
|
Requires-Dist: platformdirs>=4.0.0,<5
|
|
18
18
|
Requires-Dist: pillow>=12.1.1
|
|
19
|
-
Requires-Dist: unique-sdk>=2026.34.0.
|
|
19
|
+
Requires-Dist: unique-sdk>=2026.34.0.dev2,<2026.34.0rc0
|
|
20
20
|
Requires-Dist: jambo>=0.1.2,<0.2
|
|
21
21
|
Requires-Dist: jinja2>=3.1.6,<4
|
|
22
22
|
Requires-Dist: docxtpl>=0.20.1,<0.21
|
|
@@ -42,10 +42,13 @@ Requires-Dist: langchain>=1.0.0,<2 ; extra == 'langchain'
|
|
|
42
42
|
Requires-Dist: langchain-openai>=0.3.28 ; extra == 'langchain'
|
|
43
43
|
Requires-Dist: langchain-core>=1.0.0,<2 ; extra == 'langchain'
|
|
44
44
|
Requires-Dist: prometheus-client>=0.24.1,<1 ; extra == 'monitoring'
|
|
45
|
+
Requires-Dist: opentelemetry-sdk>=1.27,<2 ; extra == 'otel'
|
|
46
|
+
Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.27,<2 ; extra == 'otel'
|
|
45
47
|
Requires-Python: >=3.12
|
|
46
48
|
Provides-Extra: fastapi
|
|
47
49
|
Provides-Extra: langchain
|
|
48
50
|
Provides-Extra: monitoring
|
|
51
|
+
Provides-Extra: otel
|
|
49
52
|
Description-Content-Type: text/markdown
|
|
50
53
|
|
|
51
54
|
# Unique Toolkit
|
|
@@ -402,10 +402,11 @@ unique_toolkit/language_model/service.py,sha256=hDoJBBgG_wWs8WuC_c8tnmK8cNu8o5Eh
|
|
|
402
402
|
unique_toolkit/language_model/settings.py,sha256=7szohExI-qmTfUf2Zp5AB5TzrsZy1UekAGyXx_UaPMc,1524
|
|
403
403
|
unique_toolkit/language_model/stream_transform.py,sha256=iNNGpLY_7G6Bpp9Y4KtbIkQm3WB4yZt75BXcqx0DVkI,2775
|
|
404
404
|
unique_toolkit/language_model/utils.py,sha256=bPQ4l6_YO71w-zaIPanUUmtbXC1_hCvLK0tAFc3VCRc,1902
|
|
405
|
-
unique_toolkit/monitoring/__init__.py,sha256=
|
|
405
|
+
unique_toolkit/monitoring/__init__.py,sha256=auxvvOBwYrCD3TqEX0sIupDYx9nbCt2u4XN0Sy55qKI,1376
|
|
406
406
|
unique_toolkit/monitoring/gunicorn.py,sha256=urTFFS5zYHKoxb1TEQHcLqb2byi-aYgFRHGwgYRzYYI,844
|
|
407
407
|
unique_toolkit/monitoring/middleware.py,sha256=MNsUuKgt74mSnUwttXTB_AxX546Pz2zKoAP9OewVrwQ,4531
|
|
408
408
|
unique_toolkit/monitoring/registry.py,sha256=De2Bx5QBBRJQY2MM3xtntiJ2JU2FoKkVT7rNMG7ewgA,5874
|
|
409
|
+
unique_toolkit/monitoring/tracing.py,sha256=86yUIF8HrHVrkegOBOM7GqpbmVrYP07AZ4HtP-3X9QY,7128
|
|
409
410
|
unique_toolkit/protocols/support.py,sha256=x5edzbBmKAUuvm3oLFh52gkgQsqR5LU7M0t8RHK0DZk,5161
|
|
410
411
|
unique_toolkit/services/__init__.py,sha256=b9Wl7Wvz1JmZDbDexIZzBoXZ0wscXuczGAH8LoL-Qak,572
|
|
411
412
|
unique_toolkit/services/chat_service.py,sha256=ofhNHCW7kz6xXN4pUTBdDEYovZ7PXhUglPDgez8Qov0,99142
|
|
@@ -419,6 +420,6 @@ unique_toolkit/short_term_memory/service.py,sha256=2JMBEBn6GoyK-a0xvdcWpPft-3C74
|
|
|
419
420
|
unique_toolkit/smart_rules/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
420
421
|
unique_toolkit/smart_rules/compile.py,sha256=vRe0aMJWuVIK-VsqomgxQImzke7MDmwgSxxJfYCa7zw,1386
|
|
421
422
|
unique_toolkit/test_utilities/events.py,sha256=FLrhMh0A_mQutAUjoD0FdMcVQ4Gq31r35OVGzRjvLro,6760
|
|
422
|
-
unique_toolkit-2026.34.0.
|
|
423
|
-
unique_toolkit-2026.34.0.
|
|
424
|
-
unique_toolkit-2026.34.0.
|
|
423
|
+
unique_toolkit-2026.34.0.dev5.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
|
|
424
|
+
unique_toolkit-2026.34.0.dev5.dist-info/METADATA,sha256=vSBmpUAKzA9Om39LKg54TO4En9-N1RKACVGB02ekhEw,7656
|
|
425
|
+
unique_toolkit-2026.34.0.dev5.dist-info/RECORD,,
|
|
File without changes
|