@nebutra/trace-store 0.1.2 → 0.1.4
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.
- package/package.json +16 -6
- package/.turbo/turbo-build.log +0 -22
- package/.turbo/turbo-test.log +0 -14
- package/.turbo/turbo-typecheck.log +0 -4
- package/examples/batch-flush.ts +0 -11
- package/examples/llm-span.ts +0 -9
- package/examples/tool-span.ts +0 -9
- package/src/cli.ts +0 -16
- package/src/index.test.ts +0 -62
- package/src/index.ts +0 -174
- package/tsconfig.json +0 -12
- package/tsup.config.ts +0 -11
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nebutra/trace-store",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Async trace emit layer for agent, tool, and LLM spans",
|
|
5
5
|
"private": false,
|
|
6
6
|
"license": "MIT",
|
|
@@ -10,15 +10,19 @@
|
|
|
10
10
|
"category": "platform",
|
|
11
11
|
"summary": "Batched trace emitter over the shared telemetry layer"
|
|
12
12
|
},
|
|
13
|
-
"main": "./
|
|
14
|
-
"types": "./
|
|
13
|
+
"main": "./dist/index.js",
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
15
|
"exports": {
|
|
16
|
-
".":
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"import": "./dist/index.js",
|
|
19
|
+
"default": "./dist/index.js"
|
|
20
|
+
}
|
|
17
21
|
},
|
|
18
22
|
"dependencies": {
|
|
19
23
|
"p-limit": "^7.3.0",
|
|
20
|
-
"@nebutra/
|
|
21
|
-
"@nebutra/
|
|
24
|
+
"@nebutra/errors": "0.1.2",
|
|
25
|
+
"@nebutra/logger": "0.1.2"
|
|
22
26
|
},
|
|
23
27
|
"devDependencies": {
|
|
24
28
|
"@types/node": "^25.9.1",
|
|
@@ -38,6 +42,12 @@
|
|
|
38
42
|
"publishConfig": {
|
|
39
43
|
"access": "public"
|
|
40
44
|
},
|
|
45
|
+
"files": [
|
|
46
|
+
"dist",
|
|
47
|
+
"README.md",
|
|
48
|
+
"LICENSE",
|
|
49
|
+
"CHANGELOG.md"
|
|
50
|
+
],
|
|
41
51
|
"scripts": {
|
|
42
52
|
"build": "tsup",
|
|
43
53
|
"test": "vitest run",
|
package/.turbo/turbo-build.log
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
> @nebutra/trace-store@0.1.2 build /Users/tseka_luk/Documents/Nebutra-SaaS-Lab/Nebutra-Sailor/packages/platform/trace-store
|
|
3
|
-
> tsup
|
|
4
|
-
|
|
5
|
-
CLI Building entry: src/cli.ts, src/index.ts
|
|
6
|
-
CLI Using tsconfig: tsconfig.json
|
|
7
|
-
CLI tsup v8.5.1
|
|
8
|
-
CLI Using tsup config: /Users/tseka_luk/Documents/Nebutra-SaaS-Lab/Nebutra-Sailor/packages/platform/trace-store/tsup.config.ts
|
|
9
|
-
CLI Target: es2022
|
|
10
|
-
CLI Cleaning output folder
|
|
11
|
-
ESM Build start
|
|
12
|
-
ESM dist/index.js 304.00 B
|
|
13
|
-
ESM dist/chunk-WWNVYJKZ.js 4.10 KB
|
|
14
|
-
ESM dist/cli.js 579.00 B
|
|
15
|
-
ESM dist/index.js.map 71.00 B
|
|
16
|
-
ESM dist/cli.js.map 1.01 KB
|
|
17
|
-
ESM dist/chunk-WWNVYJKZ.js.map 8.46 KB
|
|
18
|
-
ESM ⚡️ Build success in 24ms
|
|
19
|
-
DTS Build start
|
|
20
|
-
DTS ⚡️ Build success in 2354ms
|
|
21
|
-
DTS dist/cli.d.ts 13.00 B
|
|
22
|
-
DTS dist/index.d.ts 1.56 KB
|
package/.turbo/turbo-test.log
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
> @nebutra/trace-store@0.1.1 test /Users/tseka_luk/Documents/Nebutra-SaaS-Lab/Nebutra-Sailor/packages/platform/trace-store
|
|
3
|
-
> vitest run
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
RUN v4.1.4 /Users/tseka_luk/Documents/Nebutra-SaaS-Lab/Nebutra-Sailor/packages/platform/trace-store
|
|
7
|
-
|
|
8
|
-
✓ src/index.test.ts (3 tests) 18ms
|
|
9
|
-
|
|
10
|
-
Test Files 1 passed (1)
|
|
11
|
-
Tests 3 passed (3)
|
|
12
|
-
Start at 09:38:36
|
|
13
|
-
Duration 391ms (transform 71ms, setup 0ms, import 92ms, tests 18ms, environment 0ms)
|
|
14
|
-
|
package/examples/batch-flush.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import { TraceStore } from "../src/index";
|
|
2
|
-
|
|
3
|
-
const store = TraceStore.default();
|
|
4
|
-
const first = store.start("agent", "plan", { traceId: "example_trace" });
|
|
5
|
-
const second = store.start("tool", "write_file", { traceId: "example_trace" });
|
|
6
|
-
|
|
7
|
-
first.end({ outcome: "ok" });
|
|
8
|
-
second.end({ path: "hello.md" });
|
|
9
|
-
await store.flush();
|
|
10
|
-
|
|
11
|
-
process.stdout.write(`${JSON.stringify(store.doctor(), null, 2)}\n`);
|
package/examples/llm-span.ts
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import { TraceStore } from "../src/index";
|
|
2
|
-
|
|
3
|
-
const store = TraceStore.default();
|
|
4
|
-
const span = store.start("llm", "completion", { traceId: "example_trace", model: "local" });
|
|
5
|
-
|
|
6
|
-
span.end({ inputTokens: 4, outputTokens: 3 });
|
|
7
|
-
await store.flush();
|
|
8
|
-
|
|
9
|
-
process.stdout.write("llm span flushed\n");
|
package/examples/tool-span.ts
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import { TraceStore } from "../src/index";
|
|
2
|
-
|
|
3
|
-
const store = TraceStore.default();
|
|
4
|
-
const span = store.start("tool", "sandbox_echo", { traceId: "example_trace", token: "hidden" });
|
|
5
|
-
|
|
6
|
-
span.end({ status: "ok" });
|
|
7
|
-
await store.flush();
|
|
8
|
-
|
|
9
|
-
process.stdout.write("tool span flushed\n");
|
package/src/cli.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { readTraceDebug, TraceStore } from "./index";
|
|
2
|
-
|
|
3
|
-
const command = process.argv[2] ?? "doctor";
|
|
4
|
-
|
|
5
|
-
if (command === "doctor") {
|
|
6
|
-
process.stdout.write(
|
|
7
|
-
`${JSON.stringify({ capability: "trace-store", ...TraceStore.default().doctor() }, null, 2)}\n`,
|
|
8
|
-
);
|
|
9
|
-
} else if (command === "debug") {
|
|
10
|
-
process.stdout.write(
|
|
11
|
-
`${JSON.stringify({ capability: "trace-store", entries: await readTraceDebug() }, null, 2)}\n`,
|
|
12
|
-
);
|
|
13
|
-
} else {
|
|
14
|
-
process.stderr.write(`Unknown trace-store command: ${command}\n`);
|
|
15
|
-
process.exitCode = 1;
|
|
16
|
-
}
|
package/src/index.test.ts
DELETED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
-
import { redactTracePayload, TraceStore } from "./index";
|
|
3
|
-
|
|
4
|
-
describe("TraceStore", () => {
|
|
5
|
-
afterEach(() => {
|
|
6
|
-
vi.doUnmock("node:fs/promises");
|
|
7
|
-
vi.resetModules();
|
|
8
|
-
vi.restoreAllMocks();
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
it("batches redacted spans asynchronously", async () => {
|
|
12
|
-
const emitted: unknown[] = [];
|
|
13
|
-
const trace = new TraceStore({
|
|
14
|
-
exporter: async (batch) => {
|
|
15
|
-
emitted.push(...batch);
|
|
16
|
-
},
|
|
17
|
-
flushIntervalMs: 1,
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
const span = trace.start("llm", "thread_1", { prompt: "hi", apiKey: "secret" });
|
|
21
|
-
span.end({ output: "ok" });
|
|
22
|
-
await trace.flush();
|
|
23
|
-
|
|
24
|
-
expect(emitted).toHaveLength(1);
|
|
25
|
-
expect(emitted[0]).toMatchObject({ kind: "llm", name: "thread_1" });
|
|
26
|
-
expect(JSON.stringify(emitted[0])).not.toContain("secret");
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it("redacts PII-shaped fields", () => {
|
|
30
|
-
expect(redactTracePayload({ password: "x", nested: { token: "y" } })).toEqual({
|
|
31
|
-
password: "[redacted]",
|
|
32
|
-
nested: { token: "[redacted]" },
|
|
33
|
-
});
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it("caps default debug-file writes during burst flushes", async () => {
|
|
37
|
-
let activeWrites = 0;
|
|
38
|
-
let maxActiveWrites = 0;
|
|
39
|
-
|
|
40
|
-
vi.doMock("node:fs/promises", () => ({
|
|
41
|
-
mkdir: vi.fn().mockResolvedValue(undefined),
|
|
42
|
-
readFile: vi.fn(),
|
|
43
|
-
writeFile: vi.fn(async () => {
|
|
44
|
-
activeWrites += 1;
|
|
45
|
-
maxActiveWrites = Math.max(maxActiveWrites, activeWrites);
|
|
46
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
47
|
-
activeWrites -= 1;
|
|
48
|
-
}),
|
|
49
|
-
}));
|
|
50
|
-
|
|
51
|
-
const { TraceStore: MockedTraceStore } = await import("./index");
|
|
52
|
-
const trace = new MockedTraceStore({ flushIntervalMs: 60_000 });
|
|
53
|
-
|
|
54
|
-
for (let index = 0; index < 12; index += 1) {
|
|
55
|
-
trace.start("tool", `tool_${index}`).end();
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
await trace.flush();
|
|
59
|
-
|
|
60
|
-
expect(maxActiveWrites).toBeLessThanOrEqual(4);
|
|
61
|
-
});
|
|
62
|
-
});
|
package/src/index.ts
DELETED
|
@@ -1,174 +0,0 @@
|
|
|
1
|
-
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
-
import { dirname, join } from "node:path";
|
|
3
|
-
import { CapabilityError } from "@nebutra/errors";
|
|
4
|
-
import pLimit from "p-limit";
|
|
5
|
-
|
|
6
|
-
export type NebuSpanKind = "agent" | "tool" | "llm";
|
|
7
|
-
|
|
8
|
-
export interface NebuSpanRecord {
|
|
9
|
-
readonly id: string;
|
|
10
|
-
readonly traceId: string;
|
|
11
|
-
readonly kind: NebuSpanKind;
|
|
12
|
-
readonly name: string;
|
|
13
|
-
readonly startedAt: string;
|
|
14
|
-
readonly endedAt?: string;
|
|
15
|
-
readonly durationMs?: number;
|
|
16
|
-
readonly attributes?: Record<string, unknown>;
|
|
17
|
-
readonly status: "started" | "ended" | "failed";
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export type TraceExporter = (batch: readonly NebuSpanRecord[]) => Promise<void>;
|
|
21
|
-
|
|
22
|
-
export interface TraceStoreOptions {
|
|
23
|
-
readonly exporter?: TraceExporter;
|
|
24
|
-
readonly flushIntervalMs?: number;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export async function initTraceStoreTelemetry(serviceName = "trace-store"): Promise<void> {
|
|
28
|
-
const { initGlobalOtel } = await import("@nebutra/logger/otel-bootstrap");
|
|
29
|
-
initGlobalOtel({ serviceName });
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const REDACT_KEYS = /api[_-]?key|token|secret|password|authorization|cookie|email/i;
|
|
33
|
-
const TRACE_DEBUG_WRITE_CONCURRENCY = 4;
|
|
34
|
-
|
|
35
|
-
export function redactTracePayload(value: unknown): unknown {
|
|
36
|
-
if (Array.isArray(value)) return value.map(redactTracePayload);
|
|
37
|
-
if (!value || typeof value !== "object") return value;
|
|
38
|
-
const out: Record<string, unknown> = {};
|
|
39
|
-
for (const [key, inner] of Object.entries(value)) {
|
|
40
|
-
out[key] = REDACT_KEYS.test(key) ? "[redacted]" : redactTracePayload(inner);
|
|
41
|
-
}
|
|
42
|
-
return out;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export function traceDebugPath(): string {
|
|
46
|
-
return join(process.cwd(), ".nebutra", "debug", "trace-store.jsonl");
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
async function appendTraceDebug(record: NebuSpanRecord): Promise<void> {
|
|
50
|
-
const path = traceDebugPath();
|
|
51
|
-
await mkdir(dirname(path), { recursive: true });
|
|
52
|
-
await writeFile(path, `${JSON.stringify(record)}\n`, { flag: "a" });
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function createTraceDebugExporter(): TraceExporter {
|
|
56
|
-
const limit = pLimit(TRACE_DEBUG_WRITE_CONCURRENCY);
|
|
57
|
-
return async (batch) => {
|
|
58
|
-
await Promise.all(batch.map((record) => limit(() => appendTraceDebug(record))));
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export class NebuSpan {
|
|
63
|
-
readonly #store: TraceStore;
|
|
64
|
-
readonly #record: NebuSpanRecord;
|
|
65
|
-
readonly #start = Date.now();
|
|
66
|
-
|
|
67
|
-
constructor(store: TraceStore, record: NebuSpanRecord) {
|
|
68
|
-
this.#store = store;
|
|
69
|
-
this.#record = record;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
end(attributes: Record<string, unknown> = {}): void {
|
|
73
|
-
this.#store.enqueue({
|
|
74
|
-
...this.#record,
|
|
75
|
-
endedAt: new Date().toISOString(),
|
|
76
|
-
durationMs: Date.now() - this.#start,
|
|
77
|
-
status: "ended",
|
|
78
|
-
attributes: {
|
|
79
|
-
...(this.#record.attributes ?? {}),
|
|
80
|
-
...(redactTracePayload(attributes) as Record<string, unknown>),
|
|
81
|
-
},
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
fail(error: unknown): void {
|
|
86
|
-
this.#store.enqueue({
|
|
87
|
-
...this.#record,
|
|
88
|
-
endedAt: new Date().toISOString(),
|
|
89
|
-
durationMs: Date.now() - this.#start,
|
|
90
|
-
status: "failed",
|
|
91
|
-
attributes: {
|
|
92
|
-
...(this.#record.attributes ?? {}),
|
|
93
|
-
error: error instanceof Error ? error.message : String(error),
|
|
94
|
-
},
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
export class TraceStore {
|
|
100
|
-
readonly #exporter: TraceExporter;
|
|
101
|
-
readonly #queue: NebuSpanRecord[] = [];
|
|
102
|
-
readonly #flushIntervalMs: number;
|
|
103
|
-
#timer: NodeJS.Timeout | undefined;
|
|
104
|
-
#lastSpanAt: string | undefined;
|
|
105
|
-
|
|
106
|
-
constructor(options: TraceStoreOptions = {}) {
|
|
107
|
-
this.#exporter = options.exporter ?? createTraceDebugExporter();
|
|
108
|
-
this.#flushIntervalMs = options.flushIntervalMs ?? 250;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
static default(): TraceStore {
|
|
112
|
-
return new TraceStore();
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
start(kind: NebuSpanKind, name: string, attributes: Record<string, unknown> = {}): NebuSpan {
|
|
116
|
-
const now = new Date().toISOString();
|
|
117
|
-
return new NebuSpan(this, {
|
|
118
|
-
id: `span_${Date.now()}_${Math.random().toString(16).slice(2)}`,
|
|
119
|
-
traceId: String(attributes.traceId ?? name),
|
|
120
|
-
kind,
|
|
121
|
-
name,
|
|
122
|
-
startedAt: now,
|
|
123
|
-
status: "started",
|
|
124
|
-
attributes: redactTracePayload(attributes) as Record<string, unknown>,
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
enqueue(record: NebuSpanRecord): void {
|
|
129
|
-
this.#queue.push(record);
|
|
130
|
-
this.#lastSpanAt = record.endedAt ?? record.startedAt;
|
|
131
|
-
this.#timer ??= setTimeout(() => {
|
|
132
|
-
this.flush().catch(() => undefined);
|
|
133
|
-
}, this.#flushIntervalMs);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
async flush(): Promise<void> {
|
|
137
|
-
if (this.#timer) {
|
|
138
|
-
clearTimeout(this.#timer);
|
|
139
|
-
this.#timer = undefined;
|
|
140
|
-
}
|
|
141
|
-
const batch = this.#queue.splice(0);
|
|
142
|
-
if (batch.length === 0) return;
|
|
143
|
-
await this.#exporter(batch);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
doctor(): { ok: boolean; lastSpanAt?: string; suggestion?: string } {
|
|
147
|
-
return this.#lastSpanAt
|
|
148
|
-
? { ok: true, lastSpanAt: this.#lastSpanAt }
|
|
149
|
-
: {
|
|
150
|
-
ok: false,
|
|
151
|
-
suggestion: "Emit a span with TraceStore.start(...).end(...) or run the Layer 0 demo.",
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
export async function readTraceDebug(limit = 10): Promise<unknown[]> {
|
|
157
|
-
try {
|
|
158
|
-
const raw = await readFile(traceDebugPath(), "utf8");
|
|
159
|
-
return raw
|
|
160
|
-
.trim()
|
|
161
|
-
.split("\n")
|
|
162
|
-
.filter(Boolean)
|
|
163
|
-
.slice(-limit)
|
|
164
|
-
.map((line) => JSON.parse(line) as unknown);
|
|
165
|
-
} catch (cause) {
|
|
166
|
-
const options = {
|
|
167
|
-
suggestion: "Run `pnpm trace:doctor` after executing a traced workflow.",
|
|
168
|
-
...(cause instanceof Error && { cause }),
|
|
169
|
-
};
|
|
170
|
-
throw new CapabilityError("trace-store", "No trace debug log found", {
|
|
171
|
-
...options,
|
|
172
|
-
});
|
|
173
|
-
}
|
|
174
|
-
}
|
package/tsconfig.json
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"extends": "../../../tsconfig.base.json",
|
|
3
|
-
"compilerOptions": {
|
|
4
|
-
"module": "ESNext",
|
|
5
|
-
"moduleResolution": "bundler",
|
|
6
|
-
"target": "esnext",
|
|
7
|
-
"types": ["node"],
|
|
8
|
-
"incremental": false
|
|
9
|
-
},
|
|
10
|
-
"include": ["src", "examples"],
|
|
11
|
-
"exclude": ["node_modules", "dist"]
|
|
12
|
-
}
|
package/tsup.config.ts
DELETED