spanforge 2.0.0__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.
- spanforge/__init__.py +695 -0
- spanforge/_batch_exporter.py +322 -0
- spanforge/_cli.py +3081 -0
- spanforge/_hooks.py +340 -0
- spanforge/_server.py +953 -0
- spanforge/_span.py +1015 -0
- spanforge/_store.py +287 -0
- spanforge/_stream.py +654 -0
- spanforge/_trace.py +334 -0
- spanforge/_tracer.py +253 -0
- spanforge/actor.py +141 -0
- spanforge/alerts.py +464 -0
- spanforge/auto.py +181 -0
- spanforge/baseline.py +336 -0
- spanforge/config.py +460 -0
- spanforge/consent.py +227 -0
- spanforge/consumer.py +379 -0
- spanforge/core/__init__.py +5 -0
- spanforge/core/compliance_mapping.py +1060 -0
- spanforge/cost.py +597 -0
- spanforge/debug.py +514 -0
- spanforge/drift.py +488 -0
- spanforge/egress.py +63 -0
- spanforge/eval.py +575 -0
- spanforge/event.py +1052 -0
- spanforge/exceptions.py +246 -0
- spanforge/explain.py +181 -0
- spanforge/export/__init__.py +50 -0
- spanforge/export/append_only.py +342 -0
- spanforge/export/cloud.py +349 -0
- spanforge/export/datadog.py +495 -0
- spanforge/export/grafana.py +331 -0
- spanforge/export/jsonl.py +198 -0
- spanforge/export/otel_bridge.py +291 -0
- spanforge/export/otlp.py +817 -0
- spanforge/export/otlp_bridge.py +231 -0
- spanforge/export/redis_backend.py +282 -0
- spanforge/export/webhook.py +302 -0
- spanforge/exporters/__init__.py +29 -0
- spanforge/exporters/console.py +271 -0
- spanforge/exporters/jsonl.py +144 -0
- spanforge/hitl.py +297 -0
- spanforge/inspect.py +429 -0
- spanforge/integrations/__init__.py +39 -0
- spanforge/integrations/_pricing.py +277 -0
- spanforge/integrations/anthropic.py +388 -0
- spanforge/integrations/bedrock.py +306 -0
- spanforge/integrations/crewai.py +251 -0
- spanforge/integrations/gemini.py +349 -0
- spanforge/integrations/groq.py +444 -0
- spanforge/integrations/langchain.py +349 -0
- spanforge/integrations/llamaindex.py +370 -0
- spanforge/integrations/ollama.py +286 -0
- spanforge/integrations/openai.py +370 -0
- spanforge/integrations/together.py +485 -0
- spanforge/metrics.py +393 -0
- spanforge/metrics_export.py +342 -0
- spanforge/migrate.py +278 -0
- spanforge/model_registry.py +282 -0
- spanforge/models.py +407 -0
- spanforge/namespaces/__init__.py +215 -0
- spanforge/namespaces/audit.py +253 -0
- spanforge/namespaces/cache.py +209 -0
- spanforge/namespaces/chain.py +74 -0
- spanforge/namespaces/confidence.py +69 -0
- spanforge/namespaces/consent.py +85 -0
- spanforge/namespaces/cost.py +175 -0
- spanforge/namespaces/decision.py +135 -0
- spanforge/namespaces/diff.py +146 -0
- spanforge/namespaces/drift.py +79 -0
- spanforge/namespaces/eval_.py +232 -0
- spanforge/namespaces/fence.py +180 -0
- spanforge/namespaces/guard.py +104 -0
- spanforge/namespaces/hitl.py +92 -0
- spanforge/namespaces/latency.py +69 -0
- spanforge/namespaces/prompt.py +185 -0
- spanforge/namespaces/redact.py +172 -0
- spanforge/namespaces/template.py +197 -0
- spanforge/namespaces/tool_call.py +76 -0
- spanforge/namespaces/trace.py +1006 -0
- spanforge/normalizer.py +183 -0
- spanforge/presidio_backend.py +149 -0
- spanforge/processor.py +258 -0
- spanforge/prompt_registry.py +415 -0
- spanforge/py.typed +0 -0
- spanforge/redact.py +780 -0
- spanforge/sampling.py +500 -0
- spanforge/schemas/v1.0/schema.json +170 -0
- spanforge/schemas/v2.0/schema.json +536 -0
- spanforge/signing.py +1152 -0
- spanforge/stream.py +559 -0
- spanforge/testing.py +376 -0
- spanforge/trace.py +199 -0
- spanforge/types.py +696 -0
- spanforge/ulid.py +304 -0
- spanforge/validate.py +383 -0
- spanforge-2.0.0.dist-info/METADATA +1777 -0
- spanforge-2.0.0.dist-info/RECORD +101 -0
- spanforge-2.0.0.dist-info/WHEEL +4 -0
- spanforge-2.0.0.dist-info/entry_points.txt +5 -0
- spanforge-2.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
"""OpenTelemetry SDK bridge for spanforge.
|
|
2
|
+
|
|
3
|
+
When the ``opentelemetry-sdk`` package is installed (``pip install
|
|
4
|
+
"spanforge[otel]"``) this module provides a first-class integration
|
|
5
|
+
that converts :class:`~spanforge.event.Event` objects into **real**
|
|
6
|
+
OpenTelemetry spans via the OTel Python SDK — rather than serialising the OTLP
|
|
7
|
+
wire format by hand.
|
|
8
|
+
|
|
9
|
+
This means:
|
|
10
|
+
* Spans flow through any configured ``TracerProvider`` (Jaeger, Zipkin, OTLP,
|
|
11
|
+
console, etc.) without a dedicated ``OTLPExporter`` endpoint.
|
|
12
|
+
* ``gen_ai.*`` semantic convention attributes are applied automatically.
|
|
13
|
+
* W3C ``traceparent`` / ``tracestate`` context propagation is hooked in via the
|
|
14
|
+
OTel SDK's standard :mod:`opentelemetry.propagate` interface.
|
|
15
|
+
* The bridge implements the :class:`~spanforge.stream.Exporter`
|
|
16
|
+
protocol so it is a drop-in replacement for
|
|
17
|
+
:class:`~spanforge.export.otlp.OTLPExporter` in any
|
|
18
|
+
:class:`~spanforge.stream.EventStream` pipeline.
|
|
19
|
+
|
|
20
|
+
Usage
|
|
21
|
+
-----
|
|
22
|
+
::
|
|
23
|
+
|
|
24
|
+
from opentelemetry import trace
|
|
25
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
26
|
+
from opentelemetry.sdk.trace.export import ConsoleSpanExporter, BatchSpanProcessor
|
|
27
|
+
|
|
28
|
+
# Configure the OTel SDK as usual.
|
|
29
|
+
provider = TracerProvider()
|
|
30
|
+
provider.add_span_processor(BatchSpanProcessor(ConsoleSpanExporter()))
|
|
31
|
+
trace.set_tracer_provider(provider)
|
|
32
|
+
|
|
33
|
+
# Create the bridge and use it like any other exporter.
|
|
34
|
+
from spanforge.export.otel_bridge import OTelBridgeExporter
|
|
35
|
+
|
|
36
|
+
bridge = OTelBridgeExporter()
|
|
37
|
+
await bridge.export(event)
|
|
38
|
+
|
|
39
|
+
Requirements
|
|
40
|
+
------------
|
|
41
|
+
``pip install "spanforge[otel]"`` — installs ``opentelemetry-sdk>=1.24``.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
from __future__ import annotations
|
|
45
|
+
|
|
46
|
+
from typing import TYPE_CHECKING, Any
|
|
47
|
+
|
|
48
|
+
from spanforge.export.otlp import _gen_ai_attributes
|
|
49
|
+
|
|
50
|
+
if TYPE_CHECKING:
|
|
51
|
+
from collections.abc import Sequence
|
|
52
|
+
|
|
53
|
+
from spanforge.event import Event
|
|
54
|
+
|
|
55
|
+
__all__ = ["OTelBridgeExporter"]
|
|
56
|
+
|
|
57
|
+
# ---------------------------------------------------------------------------
|
|
58
|
+
# Lazy OTel SDK import guard
|
|
59
|
+
# ---------------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _require_otel() -> Any: # noqa: ANN401
|
|
63
|
+
"""Import the OTel SDK or raise a clear ImportError."""
|
|
64
|
+
try:
|
|
65
|
+
import opentelemetry # noqa: PLC0415
|
|
66
|
+
except ImportError as exc:
|
|
67
|
+
raise ImportError(
|
|
68
|
+
"opentelemetry-sdk is required for OTelBridgeExporter. "
|
|
69
|
+
"Install it: pip install \"spanforge[otel]\""
|
|
70
|
+
) from exc
|
|
71
|
+
else:
|
|
72
|
+
return opentelemetry
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# ---------------------------------------------------------------------------
|
|
76
|
+
# SpanKind constants (mirrored to avoid importing SDK at module load time)
|
|
77
|
+
# ---------------------------------------------------------------------------
|
|
78
|
+
|
|
79
|
+
_SPAN_KIND_INTERNAL = 0
|
|
80
|
+
_SPAN_KIND_CLIENT = 3
|
|
81
|
+
|
|
82
|
+
# ---------------------------------------------------------------------------
|
|
83
|
+
# OTelBridgeExporter
|
|
84
|
+
# ---------------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class OTelBridgeExporter:
|
|
88
|
+
"""Exporter that emits events as real OpenTelemetry SDK spans.
|
|
89
|
+
|
|
90
|
+
Converts each :class:`~spanforge.event.Event` into a completed
|
|
91
|
+
OTel span using the globally-configured ``TracerProvider``. All ``gen_ai.*``
|
|
92
|
+
semantic convention attributes are applied alongside the ``llm.*`` namespace
|
|
93
|
+
attributes so the events are visible in both OTel-native tooling (Grafana,
|
|
94
|
+
Honeycomb, Jaeger) and custom ``llm.*``-aware consumers.
|
|
95
|
+
|
|
96
|
+
Implements the :class:`~spanforge.stream.Exporter` protocol —
|
|
97
|
+
can be used anywhere an ``OTLPExporter`` is accepted.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
tracer_name: Instrumentation scope name embedded in every span.
|
|
101
|
+
Defaults to ``"spanforge"``.
|
|
102
|
+
tracer_version: Instrumentation scope version. Defaults to ``"1.0"``.
|
|
103
|
+
|
|
104
|
+
Example::
|
|
105
|
+
|
|
106
|
+
bridge = OTelBridgeExporter()
|
|
107
|
+
await bridge.export(event)
|
|
108
|
+
|
|
109
|
+
# Or use in an EventStream pipeline:
|
|
110
|
+
await stream.drain(bridge)
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
def __init__(
|
|
114
|
+
self,
|
|
115
|
+
tracer_name: str = "spanforge",
|
|
116
|
+
tracer_version: str = "1.0",
|
|
117
|
+
) -> None:
|
|
118
|
+
_require_otel()
|
|
119
|
+
self._tracer_name = tracer_name
|
|
120
|
+
self._tracer_version = tracer_version
|
|
121
|
+
|
|
122
|
+
# ------------------------------------------------------------------
|
|
123
|
+
# Internal helpers
|
|
124
|
+
# ------------------------------------------------------------------
|
|
125
|
+
|
|
126
|
+
def _get_tracer(self) -> Any: # noqa: ANN401
|
|
127
|
+
from opentelemetry import trace # noqa: PLC0415
|
|
128
|
+
return trace.get_tracer(self._tracer_name, self._tracer_version)
|
|
129
|
+
|
|
130
|
+
@staticmethod
|
|
131
|
+
def _apply_identity_fields(event: "Event", attrs: dict[str, Any]) -> None:
|
|
132
|
+
"""Add optional identity fields to attrs if present on the event."""
|
|
133
|
+
if event.org_id is not None:
|
|
134
|
+
attrs["llm.org_id"] = event.org_id
|
|
135
|
+
if event.team_id is not None:
|
|
136
|
+
attrs["llm.team_id"] = event.team_id
|
|
137
|
+
if event.actor_id is not None:
|
|
138
|
+
attrs["llm.actor_id"] = event.actor_id
|
|
139
|
+
if event.session_id is not None:
|
|
140
|
+
attrs["llm.session_id"] = event.session_id
|
|
141
|
+
if event.tags is not None:
|
|
142
|
+
for tag_key, tag_val in event.tags.items():
|
|
143
|
+
attrs[f"llm.tag.{tag_key}"] = tag_val
|
|
144
|
+
if event.checksum is not None:
|
|
145
|
+
attrs["llm.checksum"] = event.checksum
|
|
146
|
+
|
|
147
|
+
@staticmethod
|
|
148
|
+
def _apply_payload_fields(event: "Event", attrs: dict[str, Any]) -> None:
|
|
149
|
+
"""Flatten event payload (one level deep) into attrs."""
|
|
150
|
+
for k, v in event.payload.items():
|
|
151
|
+
if isinstance(v, (str, int, float, bool)):
|
|
152
|
+
attrs[f"llm.payload.{k}"] = v
|
|
153
|
+
elif isinstance(v, dict):
|
|
154
|
+
for sub_k, sub_v in v.items():
|
|
155
|
+
if isinstance(sub_v, (str, int, float, bool)):
|
|
156
|
+
attrs[f"llm.payload.{k}.{sub_k}"] = sub_v
|
|
157
|
+
|
|
158
|
+
@staticmethod
|
|
159
|
+
def _apply_gen_ai_fields(event: "Event", attrs: dict[str, Any]) -> None:
|
|
160
|
+
"""Extract gen_ai.* semantic conventions from OTLP _kv dicts."""
|
|
161
|
+
for kv in _gen_ai_attributes(event):
|
|
162
|
+
key = kv["key"]
|
|
163
|
+
val_wrapper = kv["value"]
|
|
164
|
+
for type_key in ("stringValue", "intValue", "doubleValue", "boolValue"):
|
|
165
|
+
if type_key in val_wrapper:
|
|
166
|
+
raw = val_wrapper[type_key]
|
|
167
|
+
attrs[key] = int(raw) if type_key == "intValue" else raw
|
|
168
|
+
break
|
|
169
|
+
|
|
170
|
+
@staticmethod
|
|
171
|
+
def _build_otel_attributes(event: "Event") -> dict[str, Any]: # noqa: PLR0912
|
|
172
|
+
"""Build a flat attribute dict suitable for the OTel SDK ``span.set_attributes()``."""
|
|
173
|
+
attrs: dict[str, Any] = {
|
|
174
|
+
"llm.schema_version": event.schema_version,
|
|
175
|
+
"llm.event_id": event.event_id,
|
|
176
|
+
"llm.event_type": event.event_type,
|
|
177
|
+
"llm.source": event.source,
|
|
178
|
+
}
|
|
179
|
+
OTelBridgeExporter._apply_identity_fields(event, attrs)
|
|
180
|
+
OTelBridgeExporter._apply_payload_fields(event, attrs)
|
|
181
|
+
OTelBridgeExporter._apply_gen_ai_fields(event, attrs)
|
|
182
|
+
return attrs
|
|
183
|
+
|
|
184
|
+
@staticmethod
|
|
185
|
+
def _resolve_span_context(event: Event) -> Any | None: # noqa: ANN401
|
|
186
|
+
"""Build an OTel ``SpanContext`` from the event's trace/parent fields."""
|
|
187
|
+
if event.trace_id is None:
|
|
188
|
+
return None
|
|
189
|
+
try:
|
|
190
|
+
from opentelemetry.trace import ( # noqa: PLC0415
|
|
191
|
+
NonRecordingSpan,
|
|
192
|
+
SpanContext,
|
|
193
|
+
TraceFlags,
|
|
194
|
+
)
|
|
195
|
+
except ImportError:
|
|
196
|
+
return None
|
|
197
|
+
|
|
198
|
+
try:
|
|
199
|
+
trace_id_int = int(event.trace_id, 16)
|
|
200
|
+
parent_span_id = event.parent_span_id or event.span_id
|
|
201
|
+
if not parent_span_id:
|
|
202
|
+
return None
|
|
203
|
+
span_id_int = int(parent_span_id, 16)
|
|
204
|
+
except (ValueError, TypeError):
|
|
205
|
+
return None
|
|
206
|
+
|
|
207
|
+
return NonRecordingSpan(
|
|
208
|
+
SpanContext(
|
|
209
|
+
trace_id=trace_id_int,
|
|
210
|
+
span_id=span_id_int,
|
|
211
|
+
is_remote=True,
|
|
212
|
+
trace_flags=TraceFlags(TraceFlags.SAMPLED),
|
|
213
|
+
)
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
# ------------------------------------------------------------------
|
|
217
|
+
# Single-event export
|
|
218
|
+
# ------------------------------------------------------------------
|
|
219
|
+
|
|
220
|
+
async def export(self, event: Event) -> None: # NOSONAR
|
|
221
|
+
"""Export a single event as a completed OTel span.
|
|
222
|
+
|
|
223
|
+
The span is started and immediately ended using the event's
|
|
224
|
+
``timestamp`` and ``duration_ms`` (if available) to reconstruct
|
|
225
|
+
the correct wall-clock times.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
event: The event to emit as an OTel span.
|
|
229
|
+
"""
|
|
230
|
+
from opentelemetry import context as otel_context # noqa: PLC0415
|
|
231
|
+
from opentelemetry import trace # noqa: PLC0415
|
|
232
|
+
from opentelemetry.trace import SpanKind, use_span # noqa: PLC0415
|
|
233
|
+
|
|
234
|
+
tracer = self._get_tracer()
|
|
235
|
+
attributes = self._build_otel_attributes(event)
|
|
236
|
+
|
|
237
|
+
# Attach the parent span context if the event carries trace linkage.
|
|
238
|
+
parent_span = self._resolve_span_context(event)
|
|
239
|
+
ctx = otel_context.get_current()
|
|
240
|
+
if parent_span is not None:
|
|
241
|
+
ctx = trace.set_span_in_context(parent_span, ctx)
|
|
242
|
+
|
|
243
|
+
# Determine SpanKind: LLM calls are CLIENT; internal ops are INTERNAL.
|
|
244
|
+
span_kind = SpanKind.CLIENT
|
|
245
|
+
|
|
246
|
+
span = tracer.start_span(
|
|
247
|
+
name=event.event_type,
|
|
248
|
+
context=ctx,
|
|
249
|
+
kind=span_kind,
|
|
250
|
+
attributes=attributes,
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
# Record error if the payload indicates failure.
|
|
254
|
+
status_val = event.payload.get("status", "ok")
|
|
255
|
+
error_msg = event.payload.get("error")
|
|
256
|
+
|
|
257
|
+
with use_span(span, record_exception=False, end_on_exit=False):
|
|
258
|
+
if status_val in ("error", "timeout"):
|
|
259
|
+
from opentelemetry.trace import StatusCode # noqa: PLC0415
|
|
260
|
+
message = error_msg or ("Operation timed out" if status_val == "timeout" else "")
|
|
261
|
+
span.set_status(StatusCode.ERROR, message)
|
|
262
|
+
else:
|
|
263
|
+
from opentelemetry.trace import StatusCode # noqa: PLC0415
|
|
264
|
+
span.set_status(StatusCode.OK)
|
|
265
|
+
|
|
266
|
+
span.end()
|
|
267
|
+
|
|
268
|
+
# ------------------------------------------------------------------
|
|
269
|
+
# Batch export (Exporter protocol)
|
|
270
|
+
# ------------------------------------------------------------------
|
|
271
|
+
|
|
272
|
+
async def export_batch(self, events: Sequence[Event]) -> None:
|
|
273
|
+
"""Export a sequence of events as OTel spans.
|
|
274
|
+
|
|
275
|
+
Implements the :class:`~spanforge.stream.Exporter` protocol.
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
events: Sequence of events to export.
|
|
279
|
+
"""
|
|
280
|
+
for event in events:
|
|
281
|
+
await self.export(event)
|
|
282
|
+
|
|
283
|
+
# ------------------------------------------------------------------
|
|
284
|
+
# Repr
|
|
285
|
+
# ------------------------------------------------------------------
|
|
286
|
+
|
|
287
|
+
def __repr__(self) -> str:
|
|
288
|
+
return (
|
|
289
|
+
f"OTelBridgeExporter(tracer_name={self._tracer_name!r}, "
|
|
290
|
+
f"tracer_version={self._tracer_version!r})"
|
|
291
|
+
)
|