fuju-trace 0.1.9__tar.gz
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.
- fuju_trace-0.1.9/.gitignore +41 -0
- fuju_trace-0.1.9/PKG-INFO +81 -0
- fuju_trace-0.1.9/README.md +55 -0
- fuju_trace-0.1.9/fuju_trace/__init__.py +45 -0
- fuju_trace-0.1.9/fuju_trace/_snowflake.py +55 -0
- fuju_trace-0.1.9/fuju_trace/cli.py +48 -0
- fuju_trace-0.1.9/fuju_trace/client.py +61 -0
- fuju_trace-0.1.9/fuju_trace/event.py +104 -0
- fuju_trace-0.1.9/fuju_trace/exporter.py +595 -0
- fuju_trace-0.1.9/fuju_trace/service.py +261 -0
- fuju_trace-0.1.9/fuju_trace/tracer.py +311 -0
- fuju_trace-0.1.9/pyproject.toml +41 -0
- fuju_trace-0.1.9/tests/test_sdk.py +1025 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Rust 构建产物(巨大,绝不入库)
|
|
2
|
+
target/
|
|
3
|
+
**/target/
|
|
4
|
+
|
|
5
|
+
# Python
|
|
6
|
+
__pycache__/
|
|
7
|
+
*.py[cod]
|
|
8
|
+
*.egg-info/
|
|
9
|
+
.venv/
|
|
10
|
+
venv/
|
|
11
|
+
.pytest_cache/
|
|
12
|
+
|
|
13
|
+
# Node / TypeScript
|
|
14
|
+
node_modules/
|
|
15
|
+
dist/
|
|
16
|
+
*.tsbuildinfo
|
|
17
|
+
fuju-trace-console/.test-dist/
|
|
18
|
+
fuju-trace-node/artifacts/
|
|
19
|
+
|
|
20
|
+
# C / 扩展构建产物(tracevault-extension)
|
|
21
|
+
*.o
|
|
22
|
+
*.so
|
|
23
|
+
*.a
|
|
24
|
+
*.node
|
|
25
|
+
|
|
26
|
+
# 运行时/测试落盘产物(WAL / 段 / manifest / 向量文件,正常写在 temp_dir,这里兜底)
|
|
27
|
+
*.wal
|
|
28
|
+
*.vortex
|
|
29
|
+
manifest.dat
|
|
30
|
+
vectors.dat
|
|
31
|
+
|
|
32
|
+
# 编辑器 / 操作系统
|
|
33
|
+
.DS_Store
|
|
34
|
+
*.swp
|
|
35
|
+
*~
|
|
36
|
+
.idea/
|
|
37
|
+
.vscode/
|
|
38
|
+
.gstack/
|
|
39
|
+
|
|
40
|
+
# 控制台前端构建产物(vite build 后拷入,编译期内嵌)
|
|
41
|
+
fuju-trace-engine/crates/fuju-trace-engine/console_dist/
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: fuju-trace
|
|
3
|
+
Version: 0.1.9
|
|
4
|
+
Summary: Fuju Trace instrumentation SDK — emit trace events with deterministic, engine-matching event_id
|
|
5
|
+
Project-URL: Homepage, https://github.com/vibeinging/fuju-trace
|
|
6
|
+
Project-URL: Repository, https://github.com/vibeinging/fuju-trace
|
|
7
|
+
Project-URL: Issues, https://github.com/vibeinging/fuju-trace/issues
|
|
8
|
+
Author: Fuju Trace
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
Keywords: agent,llm,observability,opentelemetry,trace,tracing
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Classifier: Topic :: System :: Monitoring
|
|
22
|
+
Requires-Python: >=3.8
|
|
23
|
+
Provides-Extra: vexdb
|
|
24
|
+
Requires-Dist: fuju-trace-vexdb[driver]==0.1.9; extra == 'vexdb'
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# Fuju Trace Python SDK
|
|
28
|
+
|
|
29
|
+
Python 3.8+ SDK for Agent traces. It emits deterministic events and writes them through an Exporter. The base package uses only the Python standard library.
|
|
30
|
+
|
|
31
|
+
## VexDB
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install 'fuju-trace[vexdb]==0.1.9'
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
import os
|
|
39
|
+
from fuju_trace import DbExporter, Tracer, connect
|
|
40
|
+
|
|
41
|
+
with connect(vexdb_dsn=os.environ["VEXDB_DSN"], tenant_id=1,
|
|
42
|
+
vector_dim=3, initialize=True) as db:
|
|
43
|
+
tracer = Tracer(exporter=DbExporter(db, tenant_id=1), node_id=1)
|
|
44
|
+
with tracer.trace("risk review", tenant_id=1) as trace:
|
|
45
|
+
with trace.span("investigate") as span:
|
|
46
|
+
span.log("suspicious transaction")
|
|
47
|
+
tracer.close()
|
|
48
|
+
print(db.search(text="transaction", k=10))
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The VexDB adapter also accepts `vexdb_params={...}`. `vector_dim` must match the embedding model; text search works without supplying vectors. For batching, use `BufferedDbExporter` and call `flush()` when reads need to see all queued writes. See [the adapter guide](../../fuju-trace-vexdb/README.md).
|
|
52
|
+
|
|
53
|
+
## Local embedded DB
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
python -m pip install -e ./fuju-trace-sdk/python -e ./fuju-trace-db-python
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Use `connect(path="./trace-data", tenant_id=1)` with the same `DbExporter` and `Tracer` code. The separate `fuju-trace-db` wheel contains the Rust engine.
|
|
60
|
+
|
|
61
|
+
## Logging and custom sinks
|
|
62
|
+
|
|
63
|
+
`ConsoleExporter` prints JSON events for local debugging. `CollectingExporter` stores events in memory for tests. `BatchExporter` wraps a sink and forwards full batches. Implement `Exporter.export_batch()` to integrate another database.
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
from fuju_trace import ConsoleExporter, Tracer
|
|
67
|
+
|
|
68
|
+
tracer = Tracer(exporter=ConsoleExporter(), node_id=1, agent_name="planner")
|
|
69
|
+
with tracer.trace("request", tenant_id=1) as trace:
|
|
70
|
+
with trace.span("planner.route", display_name="Plan next step") as span:
|
|
71
|
+
span.log("ready")
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
`name` becomes the stored `span_name`; `display_name` is optional. `agent_name` identifies the actor. `event_id` is derived from `ext_span_id`, `seq`, and `event_type` and matches the Rust and TypeScript implementations.
|
|
75
|
+
|
|
76
|
+
## Tests
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
python tests/test_sdk.py
|
|
80
|
+
python -m unittest discover -s tests
|
|
81
|
+
```
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Fuju Trace Python SDK
|
|
2
|
+
|
|
3
|
+
Python 3.8+ SDK for Agent traces. It emits deterministic events and writes them through an Exporter. The base package uses only the Python standard library.
|
|
4
|
+
|
|
5
|
+
## VexDB
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install 'fuju-trace[vexdb]==0.1.9'
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
```python
|
|
12
|
+
import os
|
|
13
|
+
from fuju_trace import DbExporter, Tracer, connect
|
|
14
|
+
|
|
15
|
+
with connect(vexdb_dsn=os.environ["VEXDB_DSN"], tenant_id=1,
|
|
16
|
+
vector_dim=3, initialize=True) as db:
|
|
17
|
+
tracer = Tracer(exporter=DbExporter(db, tenant_id=1), node_id=1)
|
|
18
|
+
with tracer.trace("risk review", tenant_id=1) as trace:
|
|
19
|
+
with trace.span("investigate") as span:
|
|
20
|
+
span.log("suspicious transaction")
|
|
21
|
+
tracer.close()
|
|
22
|
+
print(db.search(text="transaction", k=10))
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The VexDB adapter also accepts `vexdb_params={...}`. `vector_dim` must match the embedding model; text search works without supplying vectors. For batching, use `BufferedDbExporter` and call `flush()` when reads need to see all queued writes. See [the adapter guide](../../fuju-trace-vexdb/README.md).
|
|
26
|
+
|
|
27
|
+
## Local embedded DB
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
python -m pip install -e ./fuju-trace-sdk/python -e ./fuju-trace-db-python
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Use `connect(path="./trace-data", tenant_id=1)` with the same `DbExporter` and `Tracer` code. The separate `fuju-trace-db` wheel contains the Rust engine.
|
|
34
|
+
|
|
35
|
+
## Logging and custom sinks
|
|
36
|
+
|
|
37
|
+
`ConsoleExporter` prints JSON events for local debugging. `CollectingExporter` stores events in memory for tests. `BatchExporter` wraps a sink and forwards full batches. Implement `Exporter.export_batch()` to integrate another database.
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from fuju_trace import ConsoleExporter, Tracer
|
|
41
|
+
|
|
42
|
+
tracer = Tracer(exporter=ConsoleExporter(), node_id=1, agent_name="planner")
|
|
43
|
+
with tracer.trace("request", tenant_id=1) as trace:
|
|
44
|
+
with trace.span("planner.route", display_name="Plan next step") as span:
|
|
45
|
+
span.log("ready")
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`name` becomes the stored `span_name`; `display_name` is optional. `agent_name` identifies the actor. `event_id` is derived from `ext_span_id`, `seq`, and `event_type` and matches the Rust and TypeScript implementations.
|
|
49
|
+
|
|
50
|
+
## Tests
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
python tests/test_sdk.py
|
|
54
|
+
python -m unittest discover -s tests
|
|
55
|
+
```
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""fuju_trace SDK —— 给 Agent 打点,产出与 Fuju Trace 引擎一致的 trace 事件。
|
|
2
|
+
|
|
3
|
+
核心保证:event_id 与引擎逐字节一致(同一套 FNV 哈希),所以 SDK 产生的事件灌进引擎后,
|
|
4
|
+
去重、崩溃重放幂等全都对得上。
|
|
5
|
+
"""
|
|
6
|
+
from ._snowflake import Snowflake
|
|
7
|
+
from .client import connect
|
|
8
|
+
from .event import EventType, SpanEvent, event_id
|
|
9
|
+
from .exporter import (
|
|
10
|
+
BatchExporter,
|
|
11
|
+
BufferedDbExporter,
|
|
12
|
+
CollectingExporter,
|
|
13
|
+
ConsoleExporter,
|
|
14
|
+
DbExporter,
|
|
15
|
+
Exporter,
|
|
16
|
+
NoopExporter,
|
|
17
|
+
SpoolConsumer,
|
|
18
|
+
SpoolDbExporter,
|
|
19
|
+
)
|
|
20
|
+
from .service import FujuTraceRuntime, get_fuju_trace_runtime, init_fuju_trace, shutdown_fuju_trace
|
|
21
|
+
from .tracer import Span, Trace, Tracer
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"Snowflake",
|
|
25
|
+
"connect",
|
|
26
|
+
"EventType",
|
|
27
|
+
"SpanEvent",
|
|
28
|
+
"event_id",
|
|
29
|
+
"Exporter",
|
|
30
|
+
"ConsoleExporter",
|
|
31
|
+
"CollectingExporter",
|
|
32
|
+
"DbExporter",
|
|
33
|
+
"BufferedDbExporter",
|
|
34
|
+
"SpoolDbExporter",
|
|
35
|
+
"SpoolConsumer",
|
|
36
|
+
"BatchExporter",
|
|
37
|
+
"NoopExporter",
|
|
38
|
+
"FujuTraceRuntime",
|
|
39
|
+
"init_fuju_trace",
|
|
40
|
+
"shutdown_fuju_trace",
|
|
41
|
+
"get_fuju_trace_runtime",
|
|
42
|
+
"Tracer",
|
|
43
|
+
"Trace",
|
|
44
|
+
"Span",
|
|
45
|
+
]
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""提交单调的雪花 ID 生成器。
|
|
2
|
+
|
|
3
|
+
id = 41 位毫秒时间戳 | 10 位节点 | 12 位序列 → 单调、可排序、跨进程不撞。
|
|
4
|
+
调度层正确性硬前置:event_id 必须真单调(不能用 SEQUENCE CACHE),这里满足。
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import threading
|
|
10
|
+
import time
|
|
11
|
+
|
|
12
|
+
_EPOCH_MS = 1_577_836_800_000 # 2020-01-01Z,缩短数值
|
|
13
|
+
_nodes_lock = threading.Lock()
|
|
14
|
+
_nodes: dict[int, _NodeState] = {}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class _NodeState:
|
|
18
|
+
"""同一进程中,同一个 node_id 的所有 Tracer 共用计数器。"""
|
|
19
|
+
|
|
20
|
+
def __init__(self) -> None:
|
|
21
|
+
self.lock = threading.Lock()
|
|
22
|
+
self.last_ms = -1
|
|
23
|
+
self.seq = 0
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _state_for(node: int) -> _NodeState:
|
|
27
|
+
with _nodes_lock:
|
|
28
|
+
if node not in _nodes:
|
|
29
|
+
_nodes[node] = _NodeState()
|
|
30
|
+
return _nodes[node]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Snowflake:
|
|
34
|
+
def __init__(self, node_id: int | None = None):
|
|
35
|
+
if node_id is None:
|
|
36
|
+
# 默认用 PID 低 10 位;多机部署应显式配不同 node_id
|
|
37
|
+
node_id = os.getpid() & 0x3FF
|
|
38
|
+
if isinstance(node_id, bool) or not isinstance(node_id, int) or not 0 <= node_id <= 0x3FF:
|
|
39
|
+
raise ValueError("node_id must be an integer from 0 to 1023")
|
|
40
|
+
self.node = node_id
|
|
41
|
+
self._state = _state_for(node_id)
|
|
42
|
+
|
|
43
|
+
def next(self) -> int:
|
|
44
|
+
state = self._state
|
|
45
|
+
with state.lock:
|
|
46
|
+
ms = max(int(time.time() * 1000), state.last_ms)
|
|
47
|
+
if ms == state.last_ms:
|
|
48
|
+
state.seq = (state.seq + 1) & 0xFFF
|
|
49
|
+
if state.seq == 0: # 同毫秒序列耗尽,自旋到下一毫秒
|
|
50
|
+
while ms <= state.last_ms:
|
|
51
|
+
ms = int(time.time() * 1000)
|
|
52
|
+
else:
|
|
53
|
+
state.seq = 0
|
|
54
|
+
state.last_ms = ms
|
|
55
|
+
return ((ms - _EPOCH_MS) << 22) | (self.node << 12) | state.seq
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Fuju Trace SDK command line helpers."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import time
|
|
6
|
+
from typing import Sequence
|
|
7
|
+
|
|
8
|
+
from .client import connect
|
|
9
|
+
from .exporter import SpoolConsumer
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
13
|
+
parser = argparse.ArgumentParser(prog="fuju-trace")
|
|
14
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
15
|
+
|
|
16
|
+
consume = sub.add_parser("consume-spool", help="consume local spool files into an embedded Fuju Trace DB")
|
|
17
|
+
consume.add_argument("--data-dir", required=True, help="embedded Fuju Trace data directory")
|
|
18
|
+
consume.add_argument("--spool-dir", required=True, help="spool directory written by SpoolDbExporter")
|
|
19
|
+
consume.add_argument("--tenant-id", help="default tenant id when spool files do not carry one")
|
|
20
|
+
consume.add_argument("--interval", type=float, default=0.5, help="sleep seconds between polling rounds")
|
|
21
|
+
consume.add_argument("--limit", type=int, help="max events to consume per polling round")
|
|
22
|
+
consume.add_argument("--once", action="store_true", help="consume one round then exit")
|
|
23
|
+
consume.add_argument("--keep-done", action="store_true", help="move consumed files to done/ instead of deleting them")
|
|
24
|
+
return parser
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
28
|
+
parser = build_parser()
|
|
29
|
+
args = parser.parse_args(argv)
|
|
30
|
+
if args.command == "consume-spool":
|
|
31
|
+
db = connect(path=args.data_dir, tenant_id=args.tenant_id)
|
|
32
|
+
try:
|
|
33
|
+
consumer = SpoolConsumer(db, args.spool_dir, tenant_id=args.tenant_id, keep_done=args.keep_done)
|
|
34
|
+
while True:
|
|
35
|
+
consumed = consumer.consume_once(limit=args.limit)
|
|
36
|
+
if args.once:
|
|
37
|
+
print(f"consumed={consumed}")
|
|
38
|
+
return 0
|
|
39
|
+
if consumed == 0:
|
|
40
|
+
time.sleep(args.interval)
|
|
41
|
+
finally:
|
|
42
|
+
db.close()
|
|
43
|
+
parser.error("unknown command")
|
|
44
|
+
return 2
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
if __name__ == "__main__": # pragma: no cover
|
|
48
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Open an embedded Fuju Trace store or VexDB adapter."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, Mapping
|
|
6
|
+
|
|
7
|
+
TenantId = str | int
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def connect(
|
|
11
|
+
target: str | Path | None = None,
|
|
12
|
+
*,
|
|
13
|
+
path: str | Path | None = None,
|
|
14
|
+
data_dir: str | Path | None = None,
|
|
15
|
+
vexdb_dsn: str | None = None,
|
|
16
|
+
vexdb_params: Mapping[str, Any] | None = None,
|
|
17
|
+
tenant_id: TenantId | None = None,
|
|
18
|
+
**options: Any,
|
|
19
|
+
) -> Any:
|
|
20
|
+
"""Open a Fuju Trace connection.
|
|
21
|
+
|
|
22
|
+
Use ``connect(path="./data")`` for an embedded DB when ``fuju-trace-db`` is
|
|
23
|
+
installed, or ``connect(vexdb_params={...}, tenant_id=..., vector_dim=...)``
|
|
24
|
+
when the optional ``fuju-trace-vexdb`` package is installed. A VexDB DSN
|
|
25
|
+
can also be supplied with ``vexdb_dsn``.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
if vexdb_dsn is not None or vexdb_params is not None:
|
|
29
|
+
if target is not None or path is not None or data_dir is not None:
|
|
30
|
+
raise ValueError("connect accepts VexDB or path, not a combination")
|
|
31
|
+
if vexdb_dsn is not None and vexdb_params is not None:
|
|
32
|
+
raise ValueError("connect accepts vexdb_dsn or vexdb_params, not both")
|
|
33
|
+
if tenant_id is None or "vector_dim" not in options:
|
|
34
|
+
raise ValueError("connect(VexDB) requires tenant_id and vector_dim")
|
|
35
|
+
try:
|
|
36
|
+
from fuju_trace_vexdb import VexDBTraceStore
|
|
37
|
+
except ImportError as err:
|
|
38
|
+
raise RuntimeError("connect(VexDB) requires fuju-trace-vexdb; install with pip install 'fuju-trace[vexdb]'") from err
|
|
39
|
+
return VexDBTraceStore.open(vexdb_dsn, tenant_id=tenant_id,
|
|
40
|
+
connection_params=vexdb_params, **options)
|
|
41
|
+
|
|
42
|
+
if target is not None:
|
|
43
|
+
if path is not None or data_dir is not None:
|
|
44
|
+
raise ValueError("connect received both target and path/data_dir")
|
|
45
|
+
path = target
|
|
46
|
+
if str(path).startswith(("http://", "https://")):
|
|
47
|
+
raise ValueError("HTTP connections are no longer supported; use path or VexDB")
|
|
48
|
+
local_path = data_dir if data_dir is not None else path
|
|
49
|
+
if local_path is None:
|
|
50
|
+
raise ValueError("connect requires path or VexDB connection parameters")
|
|
51
|
+
try:
|
|
52
|
+
from fuju_trace_db import FujuTraceDB
|
|
53
|
+
except ImportError as err:
|
|
54
|
+
raise RuntimeError(
|
|
55
|
+
"connect(path=...) requires the embedded DB package. "
|
|
56
|
+
"Install it with: pip install fuju-trace-db. "
|
|
57
|
+
) from err
|
|
58
|
+
return FujuTraceDB.open(local_path, tenant_id=tenant_id, **options)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
__all__ = ["connect"]
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""事件模型 + 确定性 event_id。
|
|
2
|
+
|
|
3
|
+
关键:event_id 与 Rust 引擎 `fuju-trace-core::event` **逐字节一致**(同一套 FNV-1a 哈希、同样的字段顺序)。
|
|
4
|
+
这样 SDK 这边算出的 event_id 和引擎那边算出的相同 —— 同一条 span 事件无论重传几次、跨 SDK/引擎,
|
|
5
|
+
去重都对得上,崩溃重放也幂等。基准值见引擎 `cargo run -p fuju-trace-core --example print_event_id`。
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import enum
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
|
|
12
|
+
_MASK = 0xFFFFFFFFFFFFFFFF
|
|
13
|
+
_FNV_OFFSET = 0xCBF29CE484222325
|
|
14
|
+
_FNV_PRIME = 0x100000001B3
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class EventType(enum.Enum):
|
|
18
|
+
"""事件类型。tag 进 event_id 哈希,必须与引擎 `EventType::tag()` 完全一致、永不改。"""
|
|
19
|
+
|
|
20
|
+
SPAN_START = 1
|
|
21
|
+
SPAN_END = 2
|
|
22
|
+
ATTR = 3
|
|
23
|
+
LOG = 4
|
|
24
|
+
ERROR = 5
|
|
25
|
+
|
|
26
|
+
def tag(self) -> int:
|
|
27
|
+
return self.value # 1..5,与 Rust 对齐
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _fnv1a64(data: bytes) -> int:
|
|
31
|
+
h = _FNV_OFFSET
|
|
32
|
+
for b in data:
|
|
33
|
+
h ^= b
|
|
34
|
+
h = (h * _FNV_PRIME) & _MASK
|
|
35
|
+
return h
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def event_id(ext_span_id: str, seq: int, event_type: EventType) -> int:
|
|
39
|
+
"""= fnv1a64(ext_span_id(utf-8) ++ seq(8字节小端) ++ [type_tag])。与引擎逐字节一致。"""
|
|
40
|
+
data = ext_span_id.encode("utf-8") + (seq & _MASK).to_bytes(8, "little") + bytes([event_type.tag()])
|
|
41
|
+
return _fnv1a64(data)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class SpanEvent:
|
|
46
|
+
"""一个 span 事件 = 引擎 WalRecord 的 SDK 侧对应物。"""
|
|
47
|
+
|
|
48
|
+
trace_id: int
|
|
49
|
+
span_id: int
|
|
50
|
+
ts: int # 纳秒
|
|
51
|
+
seq: int # 上报序:客户端给,原样进引擎,绝不被引擎重补
|
|
52
|
+
event_type: EventType
|
|
53
|
+
ext_span_id: str # 跨进程稳定的 span 身份(进 event_id)
|
|
54
|
+
parent_span_id: int | None = None # 父 span(trace 是棵树)
|
|
55
|
+
status: int | None = None
|
|
56
|
+
duration_ns: int | None = None
|
|
57
|
+
input_tokens: int | None = None # LLM 输入 token(成本核心)
|
|
58
|
+
output_tokens: int | None = None
|
|
59
|
+
cache_read_tokens: int | None = None # input_tokens 中由供应商缓存读取的部分
|
|
60
|
+
cache_write_tokens: int | None = None # input_tokens 中新写入供应商缓存的部分
|
|
61
|
+
session_id: int | None = None # 会话 id(多轮对话/agent 会话,串起多条 trace)
|
|
62
|
+
tenant_id: int | None = None # 租户 id(逻辑隔离维度;多租户共享索引、查询强制按 tenant 过滤)
|
|
63
|
+
span_name: str | None = None # SDK 的 span(name),技术操作名;只在 SPAN_START 上报
|
|
64
|
+
display_name: str | None = None # 可选前端展示名;只在 SPAN_START 上报
|
|
65
|
+
agent_name: str | None = None # agent 名(成本/可观测按 agent 下钻)
|
|
66
|
+
tool_name: str | None = None # 工具名(tool/function call span)
|
|
67
|
+
model: str | None = None # 模型名(成本按模型归因)
|
|
68
|
+
input_text: str | None = None # LLM 输入文本(prompt)—— eval 的评测上文
|
|
69
|
+
output_text: str | None = None # LLM 输出文本(答案)—— eval 打分对象
|
|
70
|
+
logs: list[str] = field(default_factory=list)
|
|
71
|
+
attrs: dict[str, str | int | float | bool] = field(default_factory=dict)
|
|
72
|
+
|
|
73
|
+
def event_id(self) -> int:
|
|
74
|
+
return event_id(self.ext_span_id, self.seq, self.event_type)
|
|
75
|
+
|
|
76
|
+
def to_wire(self) -> dict:
|
|
77
|
+
"""灌进引擎摄入端的 JSON 载荷(字段对齐引擎 WalRecord)。"""
|
|
78
|
+
return {
|
|
79
|
+
"trace_id": self.trace_id,
|
|
80
|
+
"span_id": self.span_id,
|
|
81
|
+
"ts": self.ts,
|
|
82
|
+
"seq": self.seq,
|
|
83
|
+
"event_type": self.event_type.value,
|
|
84
|
+
"ext_span_id": self.ext_span_id,
|
|
85
|
+
"parent_span_id": self.parent_span_id,
|
|
86
|
+
"event_id": self.event_id(),
|
|
87
|
+
"status": self.status,
|
|
88
|
+
"duration_ns": self.duration_ns,
|
|
89
|
+
"input_tokens": self.input_tokens,
|
|
90
|
+
"output_tokens": self.output_tokens,
|
|
91
|
+
"cache_read_tokens": self.cache_read_tokens,
|
|
92
|
+
"cache_write_tokens": self.cache_write_tokens,
|
|
93
|
+
"session_id": self.session_id,
|
|
94
|
+
"tenant_id": self.tenant_id,
|
|
95
|
+
"span_name": self.span_name,
|
|
96
|
+
"display_name": self.display_name,
|
|
97
|
+
"agent_name": self.agent_name,
|
|
98
|
+
"tool_name": self.tool_name,
|
|
99
|
+
"model": self.model,
|
|
100
|
+
"input_text": self.input_text,
|
|
101
|
+
"output_text": self.output_text,
|
|
102
|
+
"logs": list(self.logs),
|
|
103
|
+
"attrs": dict(self.attrs),
|
|
104
|
+
}
|