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.
@@ -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
+ }