spanforge 1.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.
Files changed (174) hide show
  1. spanforge/__init__.py +815 -0
  2. spanforge/_ansi.py +93 -0
  3. spanforge/_batch_exporter.py +409 -0
  4. spanforge/_cli.py +2094 -0
  5. spanforge/_cli_audit.py +639 -0
  6. spanforge/_cli_compliance.py +711 -0
  7. spanforge/_cli_cost.py +243 -0
  8. spanforge/_cli_ops.py +791 -0
  9. spanforge/_cli_phase11.py +356 -0
  10. spanforge/_hooks.py +337 -0
  11. spanforge/_server.py +1708 -0
  12. spanforge/_span.py +1036 -0
  13. spanforge/_store.py +288 -0
  14. spanforge/_stream.py +664 -0
  15. spanforge/_trace.py +335 -0
  16. spanforge/_tracer.py +254 -0
  17. spanforge/actor.py +141 -0
  18. spanforge/alerts.py +469 -0
  19. spanforge/auto.py +464 -0
  20. spanforge/baseline.py +335 -0
  21. spanforge/cache.py +635 -0
  22. spanforge/compliance.py +325 -0
  23. spanforge/config.py +532 -0
  24. spanforge/consent.py +228 -0
  25. spanforge/consumer.py +377 -0
  26. spanforge/core/__init__.py +5 -0
  27. spanforge/core/compliance_mapping.py +1254 -0
  28. spanforge/cost.py +600 -0
  29. spanforge/debug.py +548 -0
  30. spanforge/deprecations.py +205 -0
  31. spanforge/drift.py +482 -0
  32. spanforge/egress.py +58 -0
  33. spanforge/eval.py +648 -0
  34. spanforge/event.py +1064 -0
  35. spanforge/exceptions.py +240 -0
  36. spanforge/explain.py +178 -0
  37. spanforge/export/__init__.py +69 -0
  38. spanforge/export/append_only.py +337 -0
  39. spanforge/export/cloud.py +357 -0
  40. spanforge/export/datadog.py +497 -0
  41. spanforge/export/grafana.py +320 -0
  42. spanforge/export/jsonl.py +195 -0
  43. spanforge/export/openinference.py +158 -0
  44. spanforge/export/otel_bridge.py +294 -0
  45. spanforge/export/otlp.py +811 -0
  46. spanforge/export/otlp_bridge.py +233 -0
  47. spanforge/export/redis_backend.py +282 -0
  48. spanforge/export/siem_schema.py +98 -0
  49. spanforge/export/siem_splunk.py +264 -0
  50. spanforge/export/siem_syslog.py +212 -0
  51. spanforge/export/webhook.py +299 -0
  52. spanforge/exporters/__init__.py +30 -0
  53. spanforge/exporters/console.py +271 -0
  54. spanforge/exporters/jsonl.py +144 -0
  55. spanforge/exporters/sqlite.py +142 -0
  56. spanforge/gate.py +1150 -0
  57. spanforge/governance.py +181 -0
  58. spanforge/hitl.py +295 -0
  59. spanforge/http.py +187 -0
  60. spanforge/inspect.py +427 -0
  61. spanforge/integrations/__init__.py +45 -0
  62. spanforge/integrations/_pricing.py +280 -0
  63. spanforge/integrations/anthropic.py +388 -0
  64. spanforge/integrations/azure_openai.py +133 -0
  65. spanforge/integrations/bedrock.py +292 -0
  66. spanforge/integrations/crewai.py +251 -0
  67. spanforge/integrations/gemini.py +351 -0
  68. spanforge/integrations/groq.py +442 -0
  69. spanforge/integrations/langchain.py +349 -0
  70. spanforge/integrations/langgraph.py +306 -0
  71. spanforge/integrations/llamaindex.py +373 -0
  72. spanforge/integrations/ollama.py +287 -0
  73. spanforge/integrations/openai.py +368 -0
  74. spanforge/integrations/together.py +483 -0
  75. spanforge/io.py +214 -0
  76. spanforge/lint.py +322 -0
  77. spanforge/metrics.py +417 -0
  78. spanforge/metrics_export.py +343 -0
  79. spanforge/migrate.py +402 -0
  80. spanforge/model_registry.py +278 -0
  81. spanforge/models.py +389 -0
  82. spanforge/namespaces/__init__.py +254 -0
  83. spanforge/namespaces/audit.py +256 -0
  84. spanforge/namespaces/cache.py +237 -0
  85. spanforge/namespaces/chain.py +77 -0
  86. spanforge/namespaces/confidence.py +72 -0
  87. spanforge/namespaces/consent.py +92 -0
  88. spanforge/namespaces/cost.py +179 -0
  89. spanforge/namespaces/decision.py +143 -0
  90. spanforge/namespaces/diff.py +157 -0
  91. spanforge/namespaces/drift.py +80 -0
  92. spanforge/namespaces/eval_.py +251 -0
  93. spanforge/namespaces/feedback.py +241 -0
  94. spanforge/namespaces/fence.py +193 -0
  95. spanforge/namespaces/guard.py +105 -0
  96. spanforge/namespaces/hitl.py +91 -0
  97. spanforge/namespaces/latency.py +72 -0
  98. spanforge/namespaces/prompt.py +190 -0
  99. spanforge/namespaces/redact.py +173 -0
  100. spanforge/namespaces/retrieval.py +379 -0
  101. spanforge/namespaces/runtime_governance.py +494 -0
  102. spanforge/namespaces/template.py +208 -0
  103. spanforge/namespaces/tool_call.py +77 -0
  104. spanforge/namespaces/trace.py +1029 -0
  105. spanforge/normalizer.py +171 -0
  106. spanforge/plugins.py +82 -0
  107. spanforge/presidio_backend.py +349 -0
  108. spanforge/processor.py +258 -0
  109. spanforge/prompt_registry.py +418 -0
  110. spanforge/py.typed +0 -0
  111. spanforge/redact.py +914 -0
  112. spanforge/regression.py +192 -0
  113. spanforge/runtime_policy.py +159 -0
  114. spanforge/sampling.py +511 -0
  115. spanforge/schema.py +183 -0
  116. spanforge/schemas/v1.0/schema.json +170 -0
  117. spanforge/schemas/v2.0/schema.json +536 -0
  118. spanforge/sdk/__init__.py +625 -0
  119. spanforge/sdk/_base.py +584 -0
  120. spanforge/sdk/_base.pyi +71 -0
  121. spanforge/sdk/_exceptions.py +1096 -0
  122. spanforge/sdk/_types.py +2184 -0
  123. spanforge/sdk/alert.py +1514 -0
  124. spanforge/sdk/alert.pyi +56 -0
  125. spanforge/sdk/audit.py +1196 -0
  126. spanforge/sdk/audit.pyi +67 -0
  127. spanforge/sdk/cec.py +1215 -0
  128. spanforge/sdk/cec.pyi +37 -0
  129. spanforge/sdk/config.py +641 -0
  130. spanforge/sdk/config.pyi +55 -0
  131. spanforge/sdk/enterprise.py +714 -0
  132. spanforge/sdk/enterprise.pyi +79 -0
  133. spanforge/sdk/explain.py +170 -0
  134. spanforge/sdk/fallback.py +432 -0
  135. spanforge/sdk/feedback.py +351 -0
  136. spanforge/sdk/gate.py +874 -0
  137. spanforge/sdk/gate.pyi +51 -0
  138. spanforge/sdk/identity.py +2114 -0
  139. spanforge/sdk/identity.pyi +47 -0
  140. spanforge/sdk/lineage.py +175 -0
  141. spanforge/sdk/observe.py +1065 -0
  142. spanforge/sdk/observe.pyi +50 -0
  143. spanforge/sdk/operator.py +338 -0
  144. spanforge/sdk/pii.py +1473 -0
  145. spanforge/sdk/pii.pyi +119 -0
  146. spanforge/sdk/pipelines.py +458 -0
  147. spanforge/sdk/pipelines.pyi +39 -0
  148. spanforge/sdk/policy.py +930 -0
  149. spanforge/sdk/rag.py +594 -0
  150. spanforge/sdk/rbac.py +280 -0
  151. spanforge/sdk/registry.py +430 -0
  152. spanforge/sdk/registry.pyi +46 -0
  153. spanforge/sdk/scope.py +279 -0
  154. spanforge/sdk/secrets.py +293 -0
  155. spanforge/sdk/secrets.pyi +25 -0
  156. spanforge/sdk/security.py +560 -0
  157. spanforge/sdk/security.pyi +57 -0
  158. spanforge/sdk/trust.py +472 -0
  159. spanforge/sdk/trust.pyi +41 -0
  160. spanforge/secrets.py +799 -0
  161. spanforge/signing.py +1179 -0
  162. spanforge/stats.py +100 -0
  163. spanforge/stream.py +560 -0
  164. spanforge/testing.py +378 -0
  165. spanforge/testing_mocks.py +1052 -0
  166. spanforge/trace.py +199 -0
  167. spanforge/types.py +696 -0
  168. spanforge/ulid.py +300 -0
  169. spanforge/validate.py +379 -0
  170. spanforge-1.0.0.dist-info/METADATA +1509 -0
  171. spanforge-1.0.0.dist-info/RECORD +174 -0
  172. spanforge-1.0.0.dist-info/WHEEL +4 -0
  173. spanforge-1.0.0.dist-info/entry_points.txt +5 -0
  174. spanforge-1.0.0.dist-info/licenses/LICENSE +128 -0
@@ -0,0 +1,294 @@
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:
63
+ """Import the OTel SDK or raise a clear ImportError."""
64
+ try:
65
+ import opentelemetry
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:
127
+ from opentelemetry import trace
128
+
129
+ return trace.get_tracer(self._tracer_name, self._tracer_version)
130
+
131
+ @staticmethod
132
+ def _apply_identity_fields(event: Event, attrs: dict[str, Any]) -> None:
133
+ """Add optional identity fields to attrs if present on the event."""
134
+ if event.org_id is not None:
135
+ attrs["llm.org_id"] = event.org_id
136
+ if event.team_id is not None:
137
+ attrs["llm.team_id"] = event.team_id
138
+ if event.actor_id is not None:
139
+ attrs["llm.actor_id"] = event.actor_id
140
+ if event.session_id is not None:
141
+ attrs["llm.session_id"] = event.session_id
142
+ if event.tags is not None:
143
+ for tag_key, tag_val in event.tags.items():
144
+ attrs[f"llm.tag.{tag_key}"] = tag_val
145
+ if event.checksum is not None:
146
+ attrs["llm.checksum"] = event.checksum
147
+
148
+ @staticmethod
149
+ def _apply_payload_fields(event: Event, attrs: dict[str, Any]) -> None:
150
+ """Flatten event payload (one level deep) into attrs."""
151
+ for k, v in event.payload.items():
152
+ if isinstance(v, (str, int, float, bool)):
153
+ attrs[f"llm.payload.{k}"] = v
154
+ elif isinstance(v, dict):
155
+ for sub_k, sub_v in v.items():
156
+ if isinstance(sub_v, (str, int, float, bool)):
157
+ attrs[f"llm.payload.{k}.{sub_k}"] = sub_v
158
+
159
+ @staticmethod
160
+ def _apply_gen_ai_fields(event: Event, attrs: dict[str, Any]) -> None:
161
+ """Extract gen_ai.* semantic conventions from OTLP _kv dicts."""
162
+ for kv in _gen_ai_attributes(event):
163
+ key = kv["key"]
164
+ val_wrapper = kv["value"]
165
+ for type_key in ("stringValue", "intValue", "doubleValue", "boolValue"):
166
+ if type_key in val_wrapper:
167
+ raw = val_wrapper[type_key]
168
+ attrs[key] = int(raw) if type_key == "intValue" else raw
169
+ break
170
+
171
+ @staticmethod
172
+ def _build_otel_attributes(event: Event) -> dict[str, Any]:
173
+ """Build a flat attribute dict suitable for the OTel SDK ``span.set_attributes()``."""
174
+ attrs: dict[str, Any] = {
175
+ "llm.schema_version": event.schema_version,
176
+ "llm.event_id": event.event_id,
177
+ "llm.event_type": event.event_type,
178
+ "llm.source": event.source,
179
+ }
180
+ OTelBridgeExporter._apply_identity_fields(event, attrs)
181
+ OTelBridgeExporter._apply_payload_fields(event, attrs)
182
+ OTelBridgeExporter._apply_gen_ai_fields(event, attrs)
183
+ return attrs
184
+
185
+ @staticmethod
186
+ def _resolve_span_context(event: Event) -> Any | None:
187
+ """Build an OTel ``SpanContext`` from the event's trace/parent fields."""
188
+ if event.trace_id is None:
189
+ return None
190
+ try:
191
+ from opentelemetry.trace import (
192
+ NonRecordingSpan,
193
+ SpanContext,
194
+ TraceFlags,
195
+ )
196
+ except ImportError:
197
+ return None
198
+
199
+ try:
200
+ trace_id_int = int(event.trace_id, 16)
201
+ parent_span_id = event.parent_span_id or event.span_id
202
+ if not parent_span_id:
203
+ return None
204
+ span_id_int = int(parent_span_id, 16)
205
+ except (ValueError, TypeError):
206
+ return None
207
+
208
+ return NonRecordingSpan(
209
+ SpanContext(
210
+ trace_id=trace_id_int,
211
+ span_id=span_id_int,
212
+ is_remote=True,
213
+ trace_flags=TraceFlags(TraceFlags.SAMPLED),
214
+ )
215
+ )
216
+
217
+ # ------------------------------------------------------------------
218
+ # Single-event export
219
+ # ------------------------------------------------------------------
220
+
221
+ async def export(self, event: Event) -> None: # NOSONAR
222
+ """Export a single event as a completed OTel span.
223
+
224
+ The span is started and immediately ended using the event's
225
+ ``timestamp`` and ``duration_ms`` (if available) to reconstruct
226
+ the correct wall-clock times.
227
+
228
+ Args:
229
+ event: The event to emit as an OTel span.
230
+ """
231
+ from opentelemetry import context as otel_context
232
+ from opentelemetry import trace
233
+ from opentelemetry.trace import SpanKind, use_span
234
+
235
+ tracer = self._get_tracer()
236
+ attributes = self._build_otel_attributes(event)
237
+
238
+ # Attach the parent span context if the event carries trace linkage.
239
+ parent_span = self._resolve_span_context(event)
240
+ ctx = otel_context.get_current()
241
+ if parent_span is not None:
242
+ ctx = trace.set_span_in_context(parent_span, ctx)
243
+
244
+ # Determine SpanKind: LLM calls are CLIENT; internal ops are INTERNAL.
245
+ span_kind = SpanKind.CLIENT
246
+
247
+ span = tracer.start_span(
248
+ name=event.event_type,
249
+ context=ctx,
250
+ kind=span_kind,
251
+ attributes=attributes,
252
+ )
253
+
254
+ # Record error if the payload indicates failure.
255
+ status_val = event.payload.get("status", "ok")
256
+ error_msg = event.payload.get("error")
257
+
258
+ with use_span(span, record_exception=False, end_on_exit=False):
259
+ if status_val in ("error", "timeout"):
260
+ from opentelemetry.trace import StatusCode
261
+
262
+ message = error_msg or ("Operation timed out" if status_val == "timeout" else "")
263
+ span.set_status(StatusCode.ERROR, message)
264
+ else:
265
+ from opentelemetry.trace import StatusCode
266
+
267
+ span.set_status(StatusCode.OK)
268
+
269
+ span.end()
270
+
271
+ # ------------------------------------------------------------------
272
+ # Batch export (Exporter protocol)
273
+ # ------------------------------------------------------------------
274
+
275
+ async def export_batch(self, events: Sequence[Event]) -> None:
276
+ """Export a sequence of events as OTel spans.
277
+
278
+ Implements the :class:`~spanforge.stream.Exporter` protocol.
279
+
280
+ Args:
281
+ events: Sequence of events to export.
282
+ """
283
+ for event in events:
284
+ await self.export(event)
285
+
286
+ # ------------------------------------------------------------------
287
+ # Repr
288
+ # ------------------------------------------------------------------
289
+
290
+ def __repr__(self) -> str:
291
+ return (
292
+ f"OTelBridgeExporter(tracer_name={self._tracer_name!r}, "
293
+ f"tracer_version={self._tracer_version!r})"
294
+ )