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
spanforge/_store.py ADDED
@@ -0,0 +1,288 @@
1
+ """spanforge._store — In-process ring-buffer trace store.
2
+
3
+ Retains the last *N* traces in memory for programmatic querying via
4
+ :func:`~spanforge.get_trace`, :func:`~spanforge.get_last_agent_run`, etc.
5
+
6
+ The store is opt-in (disabled by default) to keep memory overhead zero for
7
+ users who do not need it. Enable via::
8
+
9
+ from spanforge import configure
10
+ configure(enable_trace_store=True, trace_store_size=200)
11
+
12
+ or environment variable ``SPANFORGE_ENABLE_TRACE_STORE=1``.
13
+
14
+ Security
15
+ --------
16
+ Events are stored **after** the redaction pass in :func:`~spanforge._stream._dispatch`.
17
+ When a :class:`~spanforge.redact.RedactionPolicy` is configured, all PII has
18
+ been masked before an event reaches :meth:`TraceStore.record`. The store
19
+ never bypasses redaction.
20
+
21
+ The ring buffer is bounded to ``trace_store_size`` *traces* (not individual
22
+ events). Once full, the oldest trace is evicted to make room for the new one.
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import contextlib
28
+ import threading
29
+ from collections import OrderedDict
30
+ from contextlib import contextmanager
31
+ from typing import TYPE_CHECKING
32
+
33
+ if TYPE_CHECKING:
34
+ from collections.abc import Generator
35
+
36
+ from spanforge.event import Event
37
+ from spanforge.namespaces.trace import SpanPayload
38
+
39
+ __all__ = ["TraceStore", "get_store", "trace_store"]
40
+
41
+ # ---------------------------------------------------------------------------
42
+ # EventType string constants (avoid circular import)
43
+ # ---------------------------------------------------------------------------
44
+
45
+ _SPAN_COMPLETED = "llm.trace.span.completed"
46
+ _SPAN_FAILED = "llm.trace.span.failed"
47
+ _AGENT_COMPLETED = "llm.trace.agent.completed"
48
+ _SPAN_EVENT_TYPES = frozenset({_SPAN_COMPLETED, _SPAN_FAILED})
49
+
50
+
51
+ def _event_type_str(event: Event) -> str:
52
+ et = event.event_type
53
+ return et.value if hasattr(et, "value") else str(et)
54
+
55
+
56
+ # ---------------------------------------------------------------------------
57
+ # TraceStore
58
+ # ---------------------------------------------------------------------------
59
+
60
+
61
+ class TraceStore:
62
+ """Thread-safe in-memory ring buffer storing the last *max_traces* traces.
63
+
64
+ Each trace is keyed by its ``trace_id``; events without a ``trace_id`` are
65
+ stored under the sentinel key ``"__no_trace_id__"``.
66
+
67
+ Args:
68
+ max_traces: Maximum number of distinct traces to retain. Oldest trace
69
+ is evicted when the buffer is full. Default: 100.
70
+ """
71
+
72
+ def __init__(self, max_traces: int = 100) -> None:
73
+ if max_traces < 1:
74
+ raise ValueError("TraceStore.max_traces must be >= 1")
75
+ self._max_traces = max_traces
76
+ # OrderedDict preserves insertion order; oldest = first.
77
+ self._traces: OrderedDict[str, list[Event]] = OrderedDict()
78
+ self._last_agent_trace_id: str | None = None
79
+ self._lock = threading.Lock()
80
+
81
+ # ------------------------------------------------------------------
82
+ # Internal helpers
83
+ # ------------------------------------------------------------------
84
+
85
+ def _resolve_trace_id(self, event: Event) -> str:
86
+ """Extract the trace_id from the event payload or envelope."""
87
+ tid = getattr(event, "trace_id", None) or event.payload.get("trace_id", "")
88
+ return str(tid) if tid else "__no_trace_id__"
89
+
90
+ # ------------------------------------------------------------------
91
+ # Public interface
92
+ # ------------------------------------------------------------------
93
+
94
+ def record(self, event: Event) -> None:
95
+ """Append *event* to the store.
96
+
97
+ Evicts the oldest trace when the buffer would exceed ``max_traces``.
98
+
99
+ Args:
100
+ event: A fully-formed (and already-redacted) :class:`~spanforge.event.Event`.
101
+ """
102
+ trace_id = self._resolve_trace_id(event)
103
+ with self._lock:
104
+ if trace_id not in self._traces:
105
+ # Evict oldest if full.
106
+ if len(self._traces) >= self._max_traces:
107
+ self._traces.popitem(last=False)
108
+ self._traces[trace_id] = []
109
+ else:
110
+ # Move to end (most recently active).
111
+ self._traces.move_to_end(trace_id)
112
+ self._traces[trace_id].append(event)
113
+
114
+ # Track the most recently completed agent run.
115
+ if _event_type_str(event) == _AGENT_COMPLETED:
116
+ self._last_agent_trace_id = trace_id
117
+
118
+ def get_trace(self, trace_id: str) -> list[Event] | None:
119
+ """Return all stored events for *trace_id*, or ``None`` if not found.
120
+
121
+ Args:
122
+ trace_id: The 32-character hex trace identifier.
123
+
124
+ Returns:
125
+ A copy of the event list so callers cannot mutate the store's
126
+ internal state.
127
+ """
128
+ with self._lock:
129
+ events = self._traces.get(trace_id)
130
+ return list(events) if events is not None else None
131
+
132
+ def get_last_agent_run(self) -> list[Event] | None:
133
+ """Return all events for the most recently completed agent-run trace.
134
+
135
+ Returns:
136
+ A copy of the event list, or ``None`` if no agent run has been
137
+ recorded yet.
138
+ """
139
+ with self._lock:
140
+ if self._last_agent_trace_id is None:
141
+ return None
142
+ events = self._traces.get(self._last_agent_trace_id)
143
+ return list(events) if events is not None else None
144
+
145
+ def list_tool_calls(self, trace_id: str) -> list[SpanPayload]:
146
+ """Return tool-call spans for *trace_id* as deserialized SpanPayload objects.
147
+
148
+ Args:
149
+ trace_id: The 32-character hex trace identifier.
150
+
151
+ Returns:
152
+ List of ``SpanPayload`` objects for tool-call spans, sorted by
153
+ ``start_time_unix_nano``. Returns an empty list if the trace is
154
+ not found or contains no tool-call spans.
155
+ """
156
+ return self._list_spans_by_operation(trace_id, "tool_call")
157
+
158
+ def list_llm_calls(self, trace_id: str) -> list[SpanPayload]:
159
+ """Return LLM-operation spans for *trace_id* as deserialized SpanPayload objects.
160
+
161
+ Args:
162
+ trace_id: The 32-character hex trace identifier.
163
+
164
+ Returns:
165
+ List of ``SpanPayload`` objects for LLM spans, sorted by
166
+ ``start_time_unix_nano``.
167
+ """
168
+ llm_ops = frozenset({"chat", "completion", "embedding", "chat_completion", "generate"})
169
+ return self._list_spans_by_operation(trace_id, *llm_ops)
170
+
171
+ def _list_spans_by_operation(self, trace_id: str, *operations: str) -> list[SpanPayload]:
172
+ """Shared implementation for list_tool_calls / list_llm_calls."""
173
+ from spanforge.namespaces.trace import SpanPayload
174
+
175
+ with self._lock:
176
+ events = self._traces.get(trace_id)
177
+ if not events:
178
+ return []
179
+ result: list[SpanPayload] = []
180
+ for event in events:
181
+ if _event_type_str(event) not in _SPAN_EVENT_TYPES:
182
+ continue
183
+ payload = event.payload
184
+ op = payload.get("operation", "")
185
+ if op in operations:
186
+ with contextlib.suppress(Exception):
187
+ result.append(
188
+ SpanPayload.from_dict(dict(payload))
189
+ ) # malformed span — skip without raising
190
+ result.sort(key=lambda s: s.start_time_unix_nano)
191
+ return result
192
+
193
+ def clear(self) -> None:
194
+ """Remove all stored traces and reset the last-agent-run pointer."""
195
+ with self._lock:
196
+ self._traces.clear()
197
+ self._last_agent_trace_id = None
198
+
199
+ def __len__(self) -> int:
200
+ with self._lock:
201
+ return len(self._traces)
202
+
203
+ def __repr__(self) -> str:
204
+ with self._lock:
205
+ return f"TraceStore(traces={len(self._traces)}, max={self._max_traces})"
206
+
207
+
208
+ # ---------------------------------------------------------------------------
209
+ # Module-level singleton
210
+ # ---------------------------------------------------------------------------
211
+
212
+ _store: TraceStore = TraceStore()
213
+
214
+
215
+ def get_store() -> TraceStore:
216
+ """Return the module-level :class:`TraceStore` singleton.
217
+
218
+ The singleton is recreated whenever
219
+ :func:`~spanforge._stream._reset_exporter` is called (e.g. after
220
+ ``configure(trace_store_size=…)``).
221
+ """
222
+ return _store
223
+
224
+
225
+ def _reset_store(max_traces: int = 100) -> None:
226
+ """Recreate the module-level store with a new *max_traces* limit.
227
+
228
+ Called by :func:`~spanforge._stream._reset_exporter` after ``configure()``
229
+ so that a changed ``trace_store_size`` takes effect immediately.
230
+ """
231
+ global _store
232
+ _store = TraceStore(max_traces=max_traces)
233
+
234
+
235
+ # ---------------------------------------------------------------------------
236
+ # Convenience module-level access functions (re-exported via __init__.py)
237
+ # ---------------------------------------------------------------------------
238
+
239
+
240
+ def get_trace(trace_id: str) -> list[Event] | None:
241
+ """Return all stored events for *trace_id*. See :meth:`TraceStore.get_trace`."""
242
+ return get_store().get_trace(trace_id)
243
+
244
+
245
+ def get_last_agent_run() -> list[Event] | None:
246
+ """Return events for the most recent agent-run trace. See :meth:`TraceStore.get_last_agent_run`."""
247
+ return get_store().get_last_agent_run()
248
+
249
+
250
+ def list_tool_calls(trace_id: str) -> list[SpanPayload]:
251
+ """Return tool-call spans for *trace_id*. See :meth:`TraceStore.list_tool_calls`."""
252
+ return get_store().list_tool_calls(trace_id)
253
+
254
+
255
+ def list_llm_calls(trace_id: str) -> list[SpanPayload]:
256
+ """Return LLM-call spans for *trace_id*. See :meth:`TraceStore.list_llm_calls`."""
257
+ return get_store().list_llm_calls(trace_id)
258
+
259
+
260
+ @contextmanager
261
+ def trace_store(max_traces: int = 100) -> Generator[TraceStore, None, None]:
262
+ """Context manager that installs a fresh, isolated :class:`TraceStore` for the duration of the block.
263
+
264
+ Useful in tests and interactive sessions where you want a clean store
265
+ without affecting the global singleton::
266
+
267
+ with spanforge.trace_store() as store:
268
+ # run code that emits events ...
269
+ events = store.get_trace(my_trace_id)
270
+
271
+ The previous global store is restored automatically on exit, even if an
272
+ exception is raised.
273
+
274
+ Args:
275
+ max_traces: Ring-buffer size for the temporary store. Default: 100.
276
+
277
+ Yields:
278
+ A fresh :class:`TraceStore` instance that is installed as the global
279
+ singleton for the duration of the block.
280
+ """
281
+ global _store
282
+ previous = _store
283
+ fresh = TraceStore(max_traces=max_traces)
284
+ _store = fresh
285
+ try:
286
+ yield fresh
287
+ finally:
288
+ _store = previous