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.
- spanforge/__init__.py +815 -0
- spanforge/_ansi.py +93 -0
- spanforge/_batch_exporter.py +409 -0
- spanforge/_cli.py +2094 -0
- spanforge/_cli_audit.py +639 -0
- spanforge/_cli_compliance.py +711 -0
- spanforge/_cli_cost.py +243 -0
- spanforge/_cli_ops.py +791 -0
- spanforge/_cli_phase11.py +356 -0
- spanforge/_hooks.py +337 -0
- spanforge/_server.py +1708 -0
- spanforge/_span.py +1036 -0
- spanforge/_store.py +288 -0
- spanforge/_stream.py +664 -0
- spanforge/_trace.py +335 -0
- spanforge/_tracer.py +254 -0
- spanforge/actor.py +141 -0
- spanforge/alerts.py +469 -0
- spanforge/auto.py +464 -0
- spanforge/baseline.py +335 -0
- spanforge/cache.py +635 -0
- spanforge/compliance.py +325 -0
- spanforge/config.py +532 -0
- spanforge/consent.py +228 -0
- spanforge/consumer.py +377 -0
- spanforge/core/__init__.py +5 -0
- spanforge/core/compliance_mapping.py +1254 -0
- spanforge/cost.py +600 -0
- spanforge/debug.py +548 -0
- spanforge/deprecations.py +205 -0
- spanforge/drift.py +482 -0
- spanforge/egress.py +58 -0
- spanforge/eval.py +648 -0
- spanforge/event.py +1064 -0
- spanforge/exceptions.py +240 -0
- spanforge/explain.py +178 -0
- spanforge/export/__init__.py +69 -0
- spanforge/export/append_only.py +337 -0
- spanforge/export/cloud.py +357 -0
- spanforge/export/datadog.py +497 -0
- spanforge/export/grafana.py +320 -0
- spanforge/export/jsonl.py +195 -0
- spanforge/export/openinference.py +158 -0
- spanforge/export/otel_bridge.py +294 -0
- spanforge/export/otlp.py +811 -0
- spanforge/export/otlp_bridge.py +233 -0
- spanforge/export/redis_backend.py +282 -0
- spanforge/export/siem_schema.py +98 -0
- spanforge/export/siem_splunk.py +264 -0
- spanforge/export/siem_syslog.py +212 -0
- spanforge/export/webhook.py +299 -0
- spanforge/exporters/__init__.py +30 -0
- spanforge/exporters/console.py +271 -0
- spanforge/exporters/jsonl.py +144 -0
- spanforge/exporters/sqlite.py +142 -0
- spanforge/gate.py +1150 -0
- spanforge/governance.py +181 -0
- spanforge/hitl.py +295 -0
- spanforge/http.py +187 -0
- spanforge/inspect.py +427 -0
- spanforge/integrations/__init__.py +45 -0
- spanforge/integrations/_pricing.py +280 -0
- spanforge/integrations/anthropic.py +388 -0
- spanforge/integrations/azure_openai.py +133 -0
- spanforge/integrations/bedrock.py +292 -0
- spanforge/integrations/crewai.py +251 -0
- spanforge/integrations/gemini.py +351 -0
- spanforge/integrations/groq.py +442 -0
- spanforge/integrations/langchain.py +349 -0
- spanforge/integrations/langgraph.py +306 -0
- spanforge/integrations/llamaindex.py +373 -0
- spanforge/integrations/ollama.py +287 -0
- spanforge/integrations/openai.py +368 -0
- spanforge/integrations/together.py +483 -0
- spanforge/io.py +214 -0
- spanforge/lint.py +322 -0
- spanforge/metrics.py +417 -0
- spanforge/metrics_export.py +343 -0
- spanforge/migrate.py +402 -0
- spanforge/model_registry.py +278 -0
- spanforge/models.py +389 -0
- spanforge/namespaces/__init__.py +254 -0
- spanforge/namespaces/audit.py +256 -0
- spanforge/namespaces/cache.py +237 -0
- spanforge/namespaces/chain.py +77 -0
- spanforge/namespaces/confidence.py +72 -0
- spanforge/namespaces/consent.py +92 -0
- spanforge/namespaces/cost.py +179 -0
- spanforge/namespaces/decision.py +143 -0
- spanforge/namespaces/diff.py +157 -0
- spanforge/namespaces/drift.py +80 -0
- spanforge/namespaces/eval_.py +251 -0
- spanforge/namespaces/feedback.py +241 -0
- spanforge/namespaces/fence.py +193 -0
- spanforge/namespaces/guard.py +105 -0
- spanforge/namespaces/hitl.py +91 -0
- spanforge/namespaces/latency.py +72 -0
- spanforge/namespaces/prompt.py +190 -0
- spanforge/namespaces/redact.py +173 -0
- spanforge/namespaces/retrieval.py +379 -0
- spanforge/namespaces/runtime_governance.py +494 -0
- spanforge/namespaces/template.py +208 -0
- spanforge/namespaces/tool_call.py +77 -0
- spanforge/namespaces/trace.py +1029 -0
- spanforge/normalizer.py +171 -0
- spanforge/plugins.py +82 -0
- spanforge/presidio_backend.py +349 -0
- spanforge/processor.py +258 -0
- spanforge/prompt_registry.py +418 -0
- spanforge/py.typed +0 -0
- spanforge/redact.py +914 -0
- spanforge/regression.py +192 -0
- spanforge/runtime_policy.py +159 -0
- spanforge/sampling.py +511 -0
- spanforge/schema.py +183 -0
- spanforge/schemas/v1.0/schema.json +170 -0
- spanforge/schemas/v2.0/schema.json +536 -0
- spanforge/sdk/__init__.py +625 -0
- spanforge/sdk/_base.py +584 -0
- spanforge/sdk/_base.pyi +71 -0
- spanforge/sdk/_exceptions.py +1096 -0
- spanforge/sdk/_types.py +2184 -0
- spanforge/sdk/alert.py +1514 -0
- spanforge/sdk/alert.pyi +56 -0
- spanforge/sdk/audit.py +1196 -0
- spanforge/sdk/audit.pyi +67 -0
- spanforge/sdk/cec.py +1215 -0
- spanforge/sdk/cec.pyi +37 -0
- spanforge/sdk/config.py +641 -0
- spanforge/sdk/config.pyi +55 -0
- spanforge/sdk/enterprise.py +714 -0
- spanforge/sdk/enterprise.pyi +79 -0
- spanforge/sdk/explain.py +170 -0
- spanforge/sdk/fallback.py +432 -0
- spanforge/sdk/feedback.py +351 -0
- spanforge/sdk/gate.py +874 -0
- spanforge/sdk/gate.pyi +51 -0
- spanforge/sdk/identity.py +2114 -0
- spanforge/sdk/identity.pyi +47 -0
- spanforge/sdk/lineage.py +175 -0
- spanforge/sdk/observe.py +1065 -0
- spanforge/sdk/observe.pyi +50 -0
- spanforge/sdk/operator.py +338 -0
- spanforge/sdk/pii.py +1473 -0
- spanforge/sdk/pii.pyi +119 -0
- spanforge/sdk/pipelines.py +458 -0
- spanforge/sdk/pipelines.pyi +39 -0
- spanforge/sdk/policy.py +930 -0
- spanforge/sdk/rag.py +594 -0
- spanforge/sdk/rbac.py +280 -0
- spanforge/sdk/registry.py +430 -0
- spanforge/sdk/registry.pyi +46 -0
- spanforge/sdk/scope.py +279 -0
- spanforge/sdk/secrets.py +293 -0
- spanforge/sdk/secrets.pyi +25 -0
- spanforge/sdk/security.py +560 -0
- spanforge/sdk/security.pyi +57 -0
- spanforge/sdk/trust.py +472 -0
- spanforge/sdk/trust.pyi +41 -0
- spanforge/secrets.py +799 -0
- spanforge/signing.py +1179 -0
- spanforge/stats.py +100 -0
- spanforge/stream.py +560 -0
- spanforge/testing.py +378 -0
- spanforge/testing_mocks.py +1052 -0
- spanforge/trace.py +199 -0
- spanforge/types.py +696 -0
- spanforge/ulid.py +300 -0
- spanforge/validate.py +379 -0
- spanforge-1.0.0.dist-info/METADATA +1509 -0
- spanforge-1.0.0.dist-info/RECORD +174 -0
- spanforge-1.0.0.dist-info/WHEEL +4 -0
- spanforge-1.0.0.dist-info/entry_points.txt +5 -0
- spanforge-1.0.0.dist-info/licenses/LICENSE +128 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""spanforge.exporters.jsonl — Synchronous JSONL file exporter.
|
|
2
|
+
|
|
3
|
+
Appends one canonical JSON line per event to a file on disk. Zero external
|
|
4
|
+
dependencies (stdlib only).
|
|
5
|
+
|
|
6
|
+
Usage::
|
|
7
|
+
|
|
8
|
+
from spanforge import configure
|
|
9
|
+
configure(exporter="jsonl", endpoint="./events.jsonl")
|
|
10
|
+
|
|
11
|
+
# Now all tracer.span() / agent_run() / agent_step() calls write to
|
|
12
|
+
# events.jsonl automatically.
|
|
13
|
+
|
|
14
|
+
You can also instantiate directly for testing::
|
|
15
|
+
|
|
16
|
+
from spanforge.exporters.jsonl import SyncJSONLExporter
|
|
17
|
+
from spanforge.event import Event, EventType, Tags
|
|
18
|
+
|
|
19
|
+
exporter = SyncJSONLExporter("/tmp/test.jsonl") # noqa: S108 # NOSONAR
|
|
20
|
+
exporter.export(my_event)
|
|
21
|
+
exporter.close()
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import sys
|
|
27
|
+
import threading
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
from typing import IO, TYPE_CHECKING, Literal, Union
|
|
30
|
+
|
|
31
|
+
if TYPE_CHECKING:
|
|
32
|
+
from spanforge.event import Event
|
|
33
|
+
|
|
34
|
+
__all__ = ["SyncJSONLExporter"]
|
|
35
|
+
|
|
36
|
+
_PathLike = Union[str, Path]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class SyncJSONLExporter:
|
|
40
|
+
"""Synchronous exporter that appends events as newline-delimited JSON.
|
|
41
|
+
|
|
42
|
+
Thread-safe: a :class:`threading.Lock` serialises concurrent writes so
|
|
43
|
+
the output file is never corrupted when multiple threads share one
|
|
44
|
+
instance.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
path: File path, :class:`pathlib.Path`, or ``"-"`` for stdout.
|
|
48
|
+
mode: File open mode — ``"a"`` (append, default) or ``"w"``
|
|
49
|
+
(overwrite / truncate on first write).
|
|
50
|
+
encoding: File encoding (default ``"utf-8"``).
|
|
51
|
+
|
|
52
|
+
Raises:
|
|
53
|
+
OSError: If the file cannot be opened or written.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
def __init__(
|
|
57
|
+
self,
|
|
58
|
+
path: _PathLike | str = "spanforge_events.jsonl",
|
|
59
|
+
mode: str = "a",
|
|
60
|
+
encoding: str = "utf-8",
|
|
61
|
+
) -> None:
|
|
62
|
+
if mode not in ("a", "w"):
|
|
63
|
+
raise ValueError("mode must be 'a' or 'w'")
|
|
64
|
+
self._path_str = str(path)
|
|
65
|
+
self._mode = mode
|
|
66
|
+
self._encoding = encoding
|
|
67
|
+
self._file: IO[str] | None = None
|
|
68
|
+
self._lock = threading.Lock()
|
|
69
|
+
self._closed = False
|
|
70
|
+
|
|
71
|
+
# ------------------------------------------------------------------
|
|
72
|
+
# Internal file management
|
|
73
|
+
# ------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
def _ensure_open(self) -> IO[str]:
|
|
76
|
+
"""Open the file lazily on first write."""
|
|
77
|
+
if self._file is not None and not self._file.closed:
|
|
78
|
+
return self._file
|
|
79
|
+
if self._path_str == "-":
|
|
80
|
+
self._file = sys.stdout
|
|
81
|
+
return self._file
|
|
82
|
+
# Create parent directories if they don't exist.
|
|
83
|
+
p = Path(self._path_str)
|
|
84
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
85
|
+
self._file = p.open(self._mode, encoding=self._encoding)
|
|
86
|
+
# After first open, always append.
|
|
87
|
+
self._mode = "a"
|
|
88
|
+
return self._file
|
|
89
|
+
|
|
90
|
+
# ------------------------------------------------------------------
|
|
91
|
+
# Public interface
|
|
92
|
+
# ------------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
def export(self, event: Event) -> None:
|
|
95
|
+
"""Write *event* as a single JSON line.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
event: A fully-formed :class:`~spanforge.event.Event` instance.
|
|
99
|
+
|
|
100
|
+
Raises:
|
|
101
|
+
RuntimeError: If :meth:`close` has already been called.
|
|
102
|
+
OSError: If the file write fails.
|
|
103
|
+
"""
|
|
104
|
+
if self._closed:
|
|
105
|
+
raise RuntimeError("SyncJSONLExporter is closed")
|
|
106
|
+
line = event.to_json() + "\n"
|
|
107
|
+
with self._lock:
|
|
108
|
+
fh = self._ensure_open()
|
|
109
|
+
fh.write(line)
|
|
110
|
+
fh.flush()
|
|
111
|
+
|
|
112
|
+
def flush(self) -> None:
|
|
113
|
+
"""Flush any buffered data to disk."""
|
|
114
|
+
with self._lock:
|
|
115
|
+
if self._file is not None and not self._file.closed:
|
|
116
|
+
self._file.flush()
|
|
117
|
+
|
|
118
|
+
def close(self) -> None:
|
|
119
|
+
"""Flush and close the output file. Safe to call multiple times."""
|
|
120
|
+
with self._lock:
|
|
121
|
+
if not self._closed:
|
|
122
|
+
if self._file is not None and self._file is not sys.stdout:
|
|
123
|
+
try:
|
|
124
|
+
self._file.flush()
|
|
125
|
+
self._file.close()
|
|
126
|
+
except OSError:
|
|
127
|
+
pass
|
|
128
|
+
self._file = None
|
|
129
|
+
self._closed = True
|
|
130
|
+
|
|
131
|
+
# ------------------------------------------------------------------
|
|
132
|
+
# Context manager
|
|
133
|
+
# ------------------------------------------------------------------
|
|
134
|
+
|
|
135
|
+
def __enter__(self) -> SyncJSONLExporter:
|
|
136
|
+
return self
|
|
137
|
+
|
|
138
|
+
def __exit__(self, *_: object) -> Literal[False]:
|
|
139
|
+
self.close()
|
|
140
|
+
return False
|
|
141
|
+
|
|
142
|
+
def __repr__(self) -> str:
|
|
143
|
+
state = "closed" if self._closed else "open"
|
|
144
|
+
return f"SyncJSONLExporter(path={self._path_str!r}, state={state})"
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""spanforge.exporters.sqlite — Synchronous SQLite exporter.
|
|
2
|
+
|
|
3
|
+
Persists events to a local SQLite database. Zero external dependencies
|
|
4
|
+
(stdlib ``sqlite3`` only). Suitable for development, staging, and solo
|
|
5
|
+
deployments where durable single-file storage is needed without standing up
|
|
6
|
+
Redis, Kafka, or a cloud collector.
|
|
7
|
+
|
|
8
|
+
Usage::
|
|
9
|
+
|
|
10
|
+
from spanforge import configure
|
|
11
|
+
configure(exporter="sqlite", endpoint="./spanforge.db")
|
|
12
|
+
|
|
13
|
+
# Events are now durable across process restarts.
|
|
14
|
+
# Query them with any SQLite client:
|
|
15
|
+
# sqlite3 spanforge.db "SELECT event_type, source, ts FROM events ORDER BY ts DESC LIMIT 10;"
|
|
16
|
+
|
|
17
|
+
You can also instantiate directly::
|
|
18
|
+
|
|
19
|
+
from spanforge.exporters.sqlite import SyncSQLiteExporter
|
|
20
|
+
exporter = SyncSQLiteExporter("./spanforge.db")
|
|
21
|
+
exporter.export(my_event)
|
|
22
|
+
exporter.close()
|
|
23
|
+
|
|
24
|
+
Schema
|
|
25
|
+
------
|
|
26
|
+
Table ``events``:
|
|
27
|
+
|
|
28
|
+
* ``id`` INTEGER PRIMARY KEY AUTOINCREMENT
|
|
29
|
+
* ``event_id`` TEXT NOT NULL — ULID from :attr:`~spanforge.event.Event.event_id`
|
|
30
|
+
* ``event_type`` TEXT NOT NULL — e.g. ``"trace.span.completed"``
|
|
31
|
+
* ``source`` TEXT NOT NULL
|
|
32
|
+
* ``org_id`` TEXT
|
|
33
|
+
* ``trace_id`` TEXT
|
|
34
|
+
* ``span_id`` TEXT
|
|
35
|
+
* ``ts`` TEXT NOT NULL — ISO-8601 UTC timestamp
|
|
36
|
+
* ``payload`` TEXT NOT NULL — full canonical JSON of the event
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
from __future__ import annotations
|
|
40
|
+
|
|
41
|
+
import sqlite3
|
|
42
|
+
import threading
|
|
43
|
+
from typing import TYPE_CHECKING
|
|
44
|
+
|
|
45
|
+
if TYPE_CHECKING:
|
|
46
|
+
from pathlib import Path
|
|
47
|
+
|
|
48
|
+
from spanforge.event import Event
|
|
49
|
+
|
|
50
|
+
__all__ = ["SyncSQLiteExporter"]
|
|
51
|
+
|
|
52
|
+
_CREATE_TABLE = """
|
|
53
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
54
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
55
|
+
event_id TEXT NOT NULL,
|
|
56
|
+
event_type TEXT NOT NULL,
|
|
57
|
+
source TEXT NOT NULL,
|
|
58
|
+
org_id TEXT,
|
|
59
|
+
trace_id TEXT,
|
|
60
|
+
span_id TEXT,
|
|
61
|
+
ts TEXT NOT NULL,
|
|
62
|
+
payload TEXT NOT NULL
|
|
63
|
+
);
|
|
64
|
+
CREATE INDEX IF NOT EXISTS idx_events_trace_id ON events (trace_id);
|
|
65
|
+
CREATE INDEX IF NOT EXISTS idx_events_event_type ON events (event_type);
|
|
66
|
+
CREATE INDEX IF NOT EXISTS idx_events_ts ON events (ts);
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
_INSERT = """
|
|
70
|
+
INSERT INTO events (event_id, event_type, source, org_id, trace_id, span_id, ts, payload)
|
|
71
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class SyncSQLiteExporter:
|
|
76
|
+
"""Synchronous exporter that writes events to a SQLite database.
|
|
77
|
+
|
|
78
|
+
Thread-safe: a :class:`threading.Lock` serialises all writes.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
path: Filesystem path for the SQLite database file. Defaults to
|
|
82
|
+
``"spanforge_events.db"``. Use ``":memory:"`` for tests.
|
|
83
|
+
|
|
84
|
+
Raises:
|
|
85
|
+
sqlite3.Error: If the database cannot be opened or the schema
|
|
86
|
+
cannot be initialised.
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
def __init__(self, path: str | Path = "spanforge_events.db") -> None:
|
|
90
|
+
self._path = str(path)
|
|
91
|
+
self._lock = threading.Lock()
|
|
92
|
+
self._closed = False
|
|
93
|
+
self._conn: sqlite3.Connection = sqlite3.connect(
|
|
94
|
+
self._path,
|
|
95
|
+
check_same_thread=False,
|
|
96
|
+
)
|
|
97
|
+
self._conn.executescript(_CREATE_TABLE)
|
|
98
|
+
self._conn.commit()
|
|
99
|
+
|
|
100
|
+
# ------------------------------------------------------------------
|
|
101
|
+
# Public interface
|
|
102
|
+
# ------------------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
def export(self, event: Event) -> None:
|
|
105
|
+
"""Persist *event* to the SQLite database.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
event: A fully-formed :class:`~spanforge.event.Event` instance.
|
|
109
|
+
|
|
110
|
+
Raises:
|
|
111
|
+
RuntimeError: If :meth:`close` has already been called.
|
|
112
|
+
sqlite3.Error: If the INSERT fails.
|
|
113
|
+
"""
|
|
114
|
+
if self._closed:
|
|
115
|
+
raise RuntimeError("SyncSQLiteExporter is closed")
|
|
116
|
+
ts = str(getattr(event, "timestamp", "") or "")
|
|
117
|
+
row = (
|
|
118
|
+
str(event.event_id),
|
|
119
|
+
str(event.event_type.value if hasattr(event.event_type, "value") else event.event_type),
|
|
120
|
+
str(getattr(event, "source", "") or ""),
|
|
121
|
+
str(getattr(event, "org_id", "") or "") or None,
|
|
122
|
+
str(getattr(event, "trace_id", "") or "") or None,
|
|
123
|
+
str(getattr(event, "span_id", "") or "") or None,
|
|
124
|
+
ts,
|
|
125
|
+
event.to_json(),
|
|
126
|
+
)
|
|
127
|
+
with self._lock:
|
|
128
|
+
self._conn.execute(_INSERT, row)
|
|
129
|
+
self._conn.commit()
|
|
130
|
+
|
|
131
|
+
def flush(self) -> None:
|
|
132
|
+
"""Flush — commits are immediate per-write, so this is a no-op."""
|
|
133
|
+
|
|
134
|
+
def close(self) -> None:
|
|
135
|
+
"""Close the database connection. Safe to call multiple times."""
|
|
136
|
+
with self._lock:
|
|
137
|
+
if not self._closed:
|
|
138
|
+
self._closed = True
|
|
139
|
+
try:
|
|
140
|
+
self._conn.close()
|
|
141
|
+
except sqlite3.Error:
|
|
142
|
+
pass
|