lopata 0.0.1
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/README.md +15 -0
- package/package.json +51 -0
- package/runtime/bindings/ai.ts +132 -0
- package/runtime/bindings/analytics-engine.ts +96 -0
- package/runtime/bindings/browser.ts +64 -0
- package/runtime/bindings/cache.ts +179 -0
- package/runtime/bindings/cf-streams.ts +56 -0
- package/runtime/bindings/container-docker.ts +225 -0
- package/runtime/bindings/container.ts +662 -0
- package/runtime/bindings/crypto-extras.ts +89 -0
- package/runtime/bindings/d1.ts +315 -0
- package/runtime/bindings/do-executor-inprocess.ts +140 -0
- package/runtime/bindings/do-executor-worker.ts +368 -0
- package/runtime/bindings/do-executor.ts +45 -0
- package/runtime/bindings/do-websocket-bridge.ts +70 -0
- package/runtime/bindings/do-worker-entry.ts +220 -0
- package/runtime/bindings/do-worker-env.ts +74 -0
- package/runtime/bindings/durable-object.ts +992 -0
- package/runtime/bindings/email.ts +180 -0
- package/runtime/bindings/html-rewriter.ts +84 -0
- package/runtime/bindings/hyperdrive.ts +130 -0
- package/runtime/bindings/images.ts +381 -0
- package/runtime/bindings/kv.ts +359 -0
- package/runtime/bindings/queue.ts +507 -0
- package/runtime/bindings/r2.ts +759 -0
- package/runtime/bindings/rpc-stub.ts +267 -0
- package/runtime/bindings/scheduled.ts +172 -0
- package/runtime/bindings/service-binding.ts +217 -0
- package/runtime/bindings/static-assets.ts +481 -0
- package/runtime/bindings/websocket-pair.ts +182 -0
- package/runtime/bindings/workflow.ts +858 -0
- package/runtime/bunflare-config.ts +56 -0
- package/runtime/cli/cache.ts +39 -0
- package/runtime/cli/context.ts +105 -0
- package/runtime/cli/d1.ts +163 -0
- package/runtime/cli/dev.ts +392 -0
- package/runtime/cli/kv.ts +84 -0
- package/runtime/cli/queues.ts +109 -0
- package/runtime/cli/r2.ts +140 -0
- package/runtime/cli/traces.ts +251 -0
- package/runtime/cli.ts +102 -0
- package/runtime/config.ts +148 -0
- package/runtime/d1-migrate.ts +37 -0
- package/runtime/dashboard/api.ts +174 -0
- package/runtime/dashboard/app.tsx +220 -0
- package/runtime/dashboard/components/breadcrumb.tsx +16 -0
- package/runtime/dashboard/components/buttons.tsx +13 -0
- package/runtime/dashboard/components/code-block.tsx +5 -0
- package/runtime/dashboard/components/detail-field.tsx +8 -0
- package/runtime/dashboard/components/empty-state.tsx +8 -0
- package/runtime/dashboard/components/filter-input.tsx +11 -0
- package/runtime/dashboard/components/index.ts +16 -0
- package/runtime/dashboard/components/key-value-table.tsx +23 -0
- package/runtime/dashboard/components/modal.tsx +23 -0
- package/runtime/dashboard/components/page-header.tsx +11 -0
- package/runtime/dashboard/components/pill-button.tsx +14 -0
- package/runtime/dashboard/components/refresh-button.tsx +7 -0
- package/runtime/dashboard/components/service-info.tsx +45 -0
- package/runtime/dashboard/components/status-badge.tsx +7 -0
- package/runtime/dashboard/components/table-link.tsx +5 -0
- package/runtime/dashboard/components/table.tsx +26 -0
- package/runtime/dashboard/components.tsx +19 -0
- package/runtime/dashboard/index.html +23 -0
- package/runtime/dashboard/lib.ts +45 -0
- package/runtime/dashboard/rpc/client.ts +20 -0
- package/runtime/dashboard/rpc/handlers/ai.ts +71 -0
- package/runtime/dashboard/rpc/handlers/analytics-engine.ts +53 -0
- package/runtime/dashboard/rpc/handlers/cache.ts +24 -0
- package/runtime/dashboard/rpc/handlers/config.ts +137 -0
- package/runtime/dashboard/rpc/handlers/containers.ts +194 -0
- package/runtime/dashboard/rpc/handlers/d1.ts +84 -0
- package/runtime/dashboard/rpc/handlers/do.ts +117 -0
- package/runtime/dashboard/rpc/handlers/email.ts +82 -0
- package/runtime/dashboard/rpc/handlers/errors.ts +32 -0
- package/runtime/dashboard/rpc/handlers/generations.ts +60 -0
- package/runtime/dashboard/rpc/handlers/kv.ts +76 -0
- package/runtime/dashboard/rpc/handlers/overview.ts +94 -0
- package/runtime/dashboard/rpc/handlers/queue.ts +79 -0
- package/runtime/dashboard/rpc/handlers/r2.ts +72 -0
- package/runtime/dashboard/rpc/handlers/scheduled.ts +91 -0
- package/runtime/dashboard/rpc/handlers/traces.ts +64 -0
- package/runtime/dashboard/rpc/handlers/workers.ts +65 -0
- package/runtime/dashboard/rpc/handlers/workflows.ts +171 -0
- package/runtime/dashboard/rpc/hooks.ts +132 -0
- package/runtime/dashboard/rpc/server.ts +70 -0
- package/runtime/dashboard/rpc/types.ts +396 -0
- package/runtime/dashboard/sql-browser/data-browser-tab.tsx +122 -0
- package/runtime/dashboard/sql-browser/editable-cell.tsx +117 -0
- package/runtime/dashboard/sql-browser/filter-row.tsx +99 -0
- package/runtime/dashboard/sql-browser/history-panels.tsx +110 -0
- package/runtime/dashboard/sql-browser/hooks.ts +137 -0
- package/runtime/dashboard/sql-browser/index.ts +4 -0
- package/runtime/dashboard/sql-browser/insert-row-form.tsx +85 -0
- package/runtime/dashboard/sql-browser/modals.tsx +116 -0
- package/runtime/dashboard/sql-browser/schema-browser-tab.tsx +67 -0
- package/runtime/dashboard/sql-browser/sql-browser.tsx +52 -0
- package/runtime/dashboard/sql-browser/sql-console-tab.tsx +124 -0
- package/runtime/dashboard/sql-browser/table-data-view.tsx +566 -0
- package/runtime/dashboard/sql-browser/table-sidebar.tsx +38 -0
- package/runtime/dashboard/sql-browser/types.ts +61 -0
- package/runtime/dashboard/sql-browser/utils.ts +167 -0
- package/runtime/dashboard/style.css +177 -0
- package/runtime/dashboard/views/ai.tsx +152 -0
- package/runtime/dashboard/views/analytics-engine.tsx +169 -0
- package/runtime/dashboard/views/cache.tsx +93 -0
- package/runtime/dashboard/views/containers.tsx +197 -0
- package/runtime/dashboard/views/d1.tsx +81 -0
- package/runtime/dashboard/views/do.tsx +168 -0
- package/runtime/dashboard/views/email.tsx +235 -0
- package/runtime/dashboard/views/errors.tsx +558 -0
- package/runtime/dashboard/views/home.tsx +287 -0
- package/runtime/dashboard/views/kv.tsx +273 -0
- package/runtime/dashboard/views/queue.tsx +193 -0
- package/runtime/dashboard/views/r2.tsx +202 -0
- package/runtime/dashboard/views/scheduled.tsx +89 -0
- package/runtime/dashboard/views/trace-waterfall.tsx +410 -0
- package/runtime/dashboard/views/traces.tsx +768 -0
- package/runtime/dashboard/views/workers.tsx +55 -0
- package/runtime/dashboard/views/workflows.tsx +473 -0
- package/runtime/db.ts +258 -0
- package/runtime/env.ts +362 -0
- package/runtime/error-page/app.tsx +394 -0
- package/runtime/error-page/build.ts +269 -0
- package/runtime/error-page/index.html +16 -0
- package/runtime/error-page/style.css +31 -0
- package/runtime/execution-context.ts +18 -0
- package/runtime/file-watcher.ts +57 -0
- package/runtime/generation-manager.ts +230 -0
- package/runtime/generation.ts +411 -0
- package/runtime/plugin.ts +292 -0
- package/runtime/request-cf.ts +28 -0
- package/runtime/rpc-validate.ts +154 -0
- package/runtime/tracing/context.ts +40 -0
- package/runtime/tracing/db.ts +73 -0
- package/runtime/tracing/frames.ts +75 -0
- package/runtime/tracing/instrument.ts +186 -0
- package/runtime/tracing/span.ts +138 -0
- package/runtime/tracing/store.ts +499 -0
- package/runtime/tracing/types.ts +47 -0
- package/runtime/vite-plugin/config-plugin.ts +68 -0
- package/runtime/vite-plugin/dev-server-plugin.ts +493 -0
- package/runtime/vite-plugin/dist/index.mjs +52333 -0
- package/runtime/vite-plugin/globals-plugin.ts +94 -0
- package/runtime/vite-plugin/index.ts +43 -0
- package/runtime/vite-plugin/modules-plugin.ts +88 -0
- package/runtime/vite-plugin/react-router-plugin.ts +95 -0
- package/runtime/worker-registry.ts +52 -0
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite";
|
|
2
|
+
import { getTracingDatabase } from "./db";
|
|
3
|
+
import type { SpanData, SpanEventData, TraceEvent, TraceSummary, TraceDetail } from "./types";
|
|
4
|
+
|
|
5
|
+
type Listener = (event: TraceEvent) => void;
|
|
6
|
+
|
|
7
|
+
const TRACE_CAP = 10_000;
|
|
8
|
+
const PRUNE_BATCH = 100;
|
|
9
|
+
const STALE_SPAN_TTL_MS = 10 * 60 * 1000; // 10 minutes
|
|
10
|
+
const STALE_CLEANUP_INTERVAL_MS = 60 * 1000; // run every minute
|
|
11
|
+
|
|
12
|
+
export class TraceStore {
|
|
13
|
+
private db: Database;
|
|
14
|
+
private listeners = new Set<Listener>();
|
|
15
|
+
private startTimeCache = new Map<string, number>();
|
|
16
|
+
private rootSpanCount: number;
|
|
17
|
+
|
|
18
|
+
private staleCleanupTimer: ReturnType<typeof setInterval>;
|
|
19
|
+
private insertSpanStmt;
|
|
20
|
+
private endSpanStmt;
|
|
21
|
+
private getSpanStmt;
|
|
22
|
+
private insertEventStmt;
|
|
23
|
+
private updateAttributesStmt;
|
|
24
|
+
|
|
25
|
+
constructor(db?: Database) {
|
|
26
|
+
this.db = db ?? getTracingDatabase();
|
|
27
|
+
|
|
28
|
+
this.insertSpanStmt = this.db.prepare<void, [string, string, string | null, string, string, string, string | null, number, number | null, number | null, string, string | null]>(
|
|
29
|
+
`INSERT INTO spans (span_id, trace_id, parent_span_id, name, kind, status, status_message, start_time, end_time, duration_ms, attributes, worker_name)
|
|
30
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
this.endSpanStmt = this.db.prepare<void, [number, number, string, string | null, string]>(
|
|
34
|
+
`UPDATE spans SET end_time = ?, duration_ms = ?, status = ?, status_message = ? WHERE span_id = ?`
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
this.getSpanStmt = this.db.prepare<Record<string, unknown>, [string]>(
|
|
38
|
+
`SELECT * FROM spans WHERE span_id = ?`
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
this.insertEventStmt = this.db.prepare<void, [string, string, number, string, string | null, string | null, string]>(
|
|
42
|
+
`INSERT INTO span_events (span_id, trace_id, timestamp, name, level, message, attributes)
|
|
43
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
this.updateAttributesStmt = this.db.prepare<void, [string, string]>(
|
|
47
|
+
`UPDATE spans SET attributes = json_patch(COALESCE(attributes, '{}'), ?) WHERE span_id = ?`
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
// Init root span counter from DB
|
|
51
|
+
this.rootSpanCount = this.db.prepare<{ cnt: number }, []>(
|
|
52
|
+
"SELECT COUNT(*) as cnt FROM spans WHERE parent_span_id IS NULL"
|
|
53
|
+
).get()?.cnt ?? 0;
|
|
54
|
+
|
|
55
|
+
// Periodically evict stale entries from startTimeCache (spans that never ended)
|
|
56
|
+
this.staleCleanupTimer = setInterval(() => this.evictStaleSpans(), STALE_CLEANUP_INTERVAL_MS);
|
|
57
|
+
this.staleCleanupTimer.unref();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
insertSpan(span: SpanData): void {
|
|
61
|
+
this.startTimeCache.set(span.spanId, span.startTime);
|
|
62
|
+
this.insertSpanStmt.run(
|
|
63
|
+
span.spanId, span.traceId, span.parentSpanId, span.name, span.kind,
|
|
64
|
+
span.status, span.statusMessage, span.startTime, span.endTime,
|
|
65
|
+
span.durationMs, JSON.stringify(span.attributes), span.workerName,
|
|
66
|
+
);
|
|
67
|
+
this.broadcast({ type: "span.start", span });
|
|
68
|
+
if (!span.parentSpanId) {
|
|
69
|
+
this.rootSpanCount++;
|
|
70
|
+
this.enforceTraceCap();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
endSpan(spanId: string, endTime: number, status: "ok" | "error", statusMessage?: string): void {
|
|
75
|
+
const startTime = this.startTimeCache.get(spanId);
|
|
76
|
+
if (startTime === undefined) return;
|
|
77
|
+
|
|
78
|
+
const durationMs = endTime - startTime;
|
|
79
|
+
this.endSpanStmt.run(endTime, durationMs, status, statusMessage ?? null, spanId);
|
|
80
|
+
this.startTimeCache.delete(spanId);
|
|
81
|
+
|
|
82
|
+
const span = this.rowToSpan(this.getSpanStmt.get(spanId));
|
|
83
|
+
if (span) {
|
|
84
|
+
this.broadcast({ type: "span.end", span });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
getSpanStatus(spanId: string): string | null {
|
|
89
|
+
return this.db.prepare<{ status: string }, [string]>("SELECT status FROM spans WHERE span_id = ?").get(spanId)?.status ?? null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
setSpanStatus(spanId: string, status: "ok" | "error", statusMessage: string | null): void {
|
|
93
|
+
this.db.prepare("UPDATE spans SET status = ?, status_message = ? WHERE span_id = ?").run(status, statusMessage, spanId);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
updateAttributes(spanId: string, attrs: Record<string, unknown>): void {
|
|
97
|
+
this.updateAttributesStmt.run(JSON.stringify(attrs), spanId);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
addEvent(event: Omit<SpanEventData, "id">): void {
|
|
101
|
+
this.insertEventStmt.run(
|
|
102
|
+
event.spanId, event.traceId, event.timestamp,
|
|
103
|
+
event.name, event.level, event.message, JSON.stringify(event.attributes),
|
|
104
|
+
);
|
|
105
|
+
const id = this.db.prepare<{ id: number }, []>("SELECT last_insert_rowid() as id").get()?.id;
|
|
106
|
+
this.broadcast({ type: "span.event", event: { ...event, id } });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
listTraces(opts: { limit?: number; cursor?: string }): { items: TraceSummary[]; cursor: string | null } {
|
|
110
|
+
const limit = opts.limit ?? 50;
|
|
111
|
+
const { time: cursorTime, id: cursorId } = parseCursor(opts.cursor);
|
|
112
|
+
|
|
113
|
+
const rows = this.db.prepare<Record<string, unknown>, [number, number, string, number]>(`
|
|
114
|
+
SELECT
|
|
115
|
+
s.trace_id,
|
|
116
|
+
s.name as root_span_name,
|
|
117
|
+
s.worker_name,
|
|
118
|
+
s.status,
|
|
119
|
+
s.status_message,
|
|
120
|
+
s.start_time,
|
|
121
|
+
s.duration_ms,
|
|
122
|
+
COUNT(c.span_id) as span_count,
|
|
123
|
+
SUM(CASE WHEN c.status = 'error' THEN 1 ELSE 0 END) as error_count
|
|
124
|
+
FROM spans s
|
|
125
|
+
LEFT JOIN spans c ON c.trace_id = s.trace_id
|
|
126
|
+
WHERE s.parent_span_id IS NULL AND (s.start_time < ? OR (s.start_time = ? AND s.trace_id < ?))
|
|
127
|
+
GROUP BY s.trace_id
|
|
128
|
+
ORDER BY s.start_time DESC, s.trace_id DESC
|
|
129
|
+
LIMIT ?
|
|
130
|
+
`).all(cursorTime, cursorTime, cursorId, limit + 1);
|
|
131
|
+
|
|
132
|
+
const hasMore = rows.length > limit;
|
|
133
|
+
const items = (hasMore ? rows.slice(0, limit) : rows).map(r => ({
|
|
134
|
+
traceId: r.trace_id as string,
|
|
135
|
+
rootSpanName: r.root_span_name as string,
|
|
136
|
+
workerName: r.worker_name as string | null,
|
|
137
|
+
status: r.status as "ok" | "error" | "unset",
|
|
138
|
+
statusMessage: r.status_message ? String(r.status_message).slice(0, 80) : null,
|
|
139
|
+
startTime: r.start_time as number,
|
|
140
|
+
durationMs: r.duration_ms as number | null,
|
|
141
|
+
spanCount: r.span_count as number,
|
|
142
|
+
errorCount: r.error_count as number,
|
|
143
|
+
}));
|
|
144
|
+
|
|
145
|
+
const last = items[items.length - 1];
|
|
146
|
+
const cursor = hasMore && last ? buildCursor(last.startTime, last.traceId) : null;
|
|
147
|
+
return { items, cursor };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
getTrace(traceId: string): TraceDetail {
|
|
151
|
+
const spanRows = this.db.prepare<Record<string, unknown>, [string]>(
|
|
152
|
+
"SELECT * FROM spans WHERE trace_id = ? ORDER BY start_time ASC"
|
|
153
|
+
).all(traceId);
|
|
154
|
+
|
|
155
|
+
const eventRows = this.db.prepare<Record<string, unknown>, [string]>(
|
|
156
|
+
"SELECT * FROM span_events WHERE trace_id = ? ORDER BY timestamp ASC"
|
|
157
|
+
).all(traceId);
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
spans: spanRows.map(r => this.rowToSpan(r)!),
|
|
161
|
+
events: eventRows.map(r => ({
|
|
162
|
+
id: r.id as number,
|
|
163
|
+
spanId: r.span_id as string,
|
|
164
|
+
traceId: r.trace_id as string,
|
|
165
|
+
timestamp: r.timestamp as number,
|
|
166
|
+
name: r.name as string,
|
|
167
|
+
level: r.level as string | null,
|
|
168
|
+
message: r.message as string | null,
|
|
169
|
+
attributes: r.attributes ? JSON.parse(r.attributes as string) : {},
|
|
170
|
+
})),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
getRecentTraces(since: number, limit: number): TraceSummary[] {
|
|
175
|
+
const rows = this.db.prepare<Record<string, unknown>, [number, number]>(`
|
|
176
|
+
SELECT
|
|
177
|
+
s.trace_id,
|
|
178
|
+
s.name as root_span_name,
|
|
179
|
+
s.worker_name,
|
|
180
|
+
s.status,
|
|
181
|
+
s.status_message,
|
|
182
|
+
s.start_time,
|
|
183
|
+
s.duration_ms,
|
|
184
|
+
COUNT(c.span_id) as span_count,
|
|
185
|
+
SUM(CASE WHEN c.status = 'error' THEN 1 ELSE 0 END) as error_count
|
|
186
|
+
FROM spans s
|
|
187
|
+
LEFT JOIN spans c ON c.trace_id = s.trace_id
|
|
188
|
+
WHERE s.parent_span_id IS NULL AND s.start_time >= ?
|
|
189
|
+
GROUP BY s.trace_id
|
|
190
|
+
ORDER BY s.start_time DESC
|
|
191
|
+
LIMIT ?
|
|
192
|
+
`).all(since, limit);
|
|
193
|
+
|
|
194
|
+
return rows.map(r => ({
|
|
195
|
+
traceId: r.trace_id as string,
|
|
196
|
+
rootSpanName: r.root_span_name as string,
|
|
197
|
+
workerName: r.worker_name as string | null,
|
|
198
|
+
status: r.status as "ok" | "error" | "unset",
|
|
199
|
+
statusMessage: r.status_message ? String(r.status_message).slice(0, 80) : null,
|
|
200
|
+
startTime: r.start_time as number,
|
|
201
|
+
durationMs: r.duration_ms as number | null,
|
|
202
|
+
spanCount: r.span_count as number,
|
|
203
|
+
errorCount: r.error_count as number,
|
|
204
|
+
}));
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
searchTraces(query: string, limit: number = 50): { items: TraceSummary[]; cursor: string | null } {
|
|
208
|
+
const like = `%${query}%`;
|
|
209
|
+
const rows = this.db.prepare<Record<string, unknown>, [string, string, string, string, number]>(`
|
|
210
|
+
SELECT DISTINCT
|
|
211
|
+
s.trace_id,
|
|
212
|
+
s.name as root_span_name,
|
|
213
|
+
s.worker_name,
|
|
214
|
+
s.status,
|
|
215
|
+
s.status_message,
|
|
216
|
+
s.start_time,
|
|
217
|
+
s.duration_ms,
|
|
218
|
+
(SELECT COUNT(*) FROM spans WHERE trace_id = s.trace_id) as span_count,
|
|
219
|
+
(SELECT COUNT(*) FROM spans WHERE trace_id = s.trace_id AND status = 'error') as error_count
|
|
220
|
+
FROM spans s
|
|
221
|
+
LEFT JOIN span_events ev ON ev.trace_id = s.trace_id
|
|
222
|
+
WHERE s.parent_span_id IS NULL
|
|
223
|
+
AND (s.name LIKE ? OR s.attributes LIKE ? OR s.status_message LIKE ? OR ev.message LIKE ?)
|
|
224
|
+
ORDER BY s.start_time DESC
|
|
225
|
+
LIMIT ?
|
|
226
|
+
`).all(like, like, like, like, limit);
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
items: rows.map(r => ({
|
|
230
|
+
traceId: r.trace_id as string,
|
|
231
|
+
rootSpanName: r.root_span_name as string,
|
|
232
|
+
workerName: r.worker_name as string | null,
|
|
233
|
+
status: r.status as "ok" | "error" | "unset",
|
|
234
|
+
statusMessage: r.status_message ? String(r.status_message).slice(0, 80) : null,
|
|
235
|
+
startTime: r.start_time as number,
|
|
236
|
+
durationMs: r.duration_ms as number | null,
|
|
237
|
+
spanCount: r.span_count as number,
|
|
238
|
+
errorCount: r.error_count as number,
|
|
239
|
+
})),
|
|
240
|
+
cursor: null,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
listAllSpans(opts: { limit?: number; cursor?: string }): { items: Array<{ spanId: string; traceId: string; name: string; status: string; durationMs: number | null; startTime: number; workerName: string | null }>; cursor: string | null } {
|
|
245
|
+
const limit = opts.limit ?? 50;
|
|
246
|
+
const { time: cursorTime, id: cursorId } = parseCursor(opts.cursor);
|
|
247
|
+
|
|
248
|
+
const rows = this.db.prepare<Record<string, unknown>, [number, number, string, number]>(`
|
|
249
|
+
SELECT span_id, trace_id, name, status, duration_ms, start_time, worker_name
|
|
250
|
+
FROM spans
|
|
251
|
+
WHERE start_time < ? OR (start_time = ? AND span_id < ?)
|
|
252
|
+
ORDER BY start_time DESC, span_id DESC
|
|
253
|
+
LIMIT ?
|
|
254
|
+
`).all(cursorTime, cursorTime, cursorId, limit + 1);
|
|
255
|
+
|
|
256
|
+
const hasMore = rows.length > limit;
|
|
257
|
+
const items = (hasMore ? rows.slice(0, limit) : rows).map(r => ({
|
|
258
|
+
spanId: r.span_id as string,
|
|
259
|
+
traceId: r.trace_id as string,
|
|
260
|
+
name: r.name as string,
|
|
261
|
+
status: r.status as string,
|
|
262
|
+
durationMs: r.duration_ms as number | null,
|
|
263
|
+
startTime: r.start_time as number,
|
|
264
|
+
workerName: r.worker_name as string | null,
|
|
265
|
+
}));
|
|
266
|
+
|
|
267
|
+
const last = items[items.length - 1];
|
|
268
|
+
const cursor = hasMore && last ? buildCursor(last.startTime, last.spanId) : null;
|
|
269
|
+
return { items, cursor };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
listAllLogs(opts: { limit?: number; cursor?: string }): { items: Array<{ id: number; spanId: string; traceId: string; timestamp: number; name: string; level: string | null; message: string | null }>; cursor: string | null } {
|
|
273
|
+
const limit = opts.limit ?? 50;
|
|
274
|
+
const cursorId = opts.cursor ? parseInt(opts.cursor, 10) : Number.MAX_SAFE_INTEGER;
|
|
275
|
+
|
|
276
|
+
const rows = this.db.prepare<Record<string, unknown>, [number, number]>(`
|
|
277
|
+
SELECT id, span_id, trace_id, timestamp, name, level, message
|
|
278
|
+
FROM span_events
|
|
279
|
+
WHERE id < ?
|
|
280
|
+
ORDER BY id DESC
|
|
281
|
+
LIMIT ?
|
|
282
|
+
`).all(cursorId, limit + 1);
|
|
283
|
+
|
|
284
|
+
const hasMore = rows.length > limit;
|
|
285
|
+
const items = (hasMore ? rows.slice(0, limit) : rows).map(r => ({
|
|
286
|
+
id: r.id as number,
|
|
287
|
+
spanId: r.span_id as string,
|
|
288
|
+
traceId: r.trace_id as string,
|
|
289
|
+
timestamp: r.timestamp as number,
|
|
290
|
+
name: r.name as string,
|
|
291
|
+
level: r.level as string | null,
|
|
292
|
+
message: r.message as string | null,
|
|
293
|
+
}));
|
|
294
|
+
|
|
295
|
+
const cursor = hasMore && items.length > 0 ? String(items[items.length - 1]!.id) : null;
|
|
296
|
+
return { items, cursor };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
clearTraces(): void {
|
|
300
|
+
this.db.run("DELETE FROM span_events");
|
|
301
|
+
this.db.run("DELETE FROM spans");
|
|
302
|
+
this.rootSpanCount = 0;
|
|
303
|
+
this.startTimeCache.clear();
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// ─── Error persistence ──────────────────────────────────────────────
|
|
307
|
+
|
|
308
|
+
insertError(opts: {
|
|
309
|
+
id: string;
|
|
310
|
+
timestamp: number;
|
|
311
|
+
errorName: string;
|
|
312
|
+
errorMessage: string;
|
|
313
|
+
requestMethod?: string | null;
|
|
314
|
+
requestUrl?: string | null;
|
|
315
|
+
workerName?: string | null;
|
|
316
|
+
traceId?: string | null;
|
|
317
|
+
spanId?: string | null;
|
|
318
|
+
source?: string | null;
|
|
319
|
+
data: string;
|
|
320
|
+
}): void {
|
|
321
|
+
this.db.prepare(
|
|
322
|
+
`INSERT INTO errors (id, timestamp, error_name, error_message, request_method, request_url, worker_name, trace_id, span_id, source, data) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
323
|
+
).run(
|
|
324
|
+
opts.id, opts.timestamp, opts.errorName, opts.errorMessage,
|
|
325
|
+
opts.requestMethod ?? null, opts.requestUrl ?? null, opts.workerName ?? null,
|
|
326
|
+
opts.traceId ?? null, opts.spanId ?? null, opts.source ?? null, opts.data,
|
|
327
|
+
);
|
|
328
|
+
this.enforceErrorCap();
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
listErrors(opts: { limit?: number; cursor?: string }): { items: Array<{ id: string; timestamp: number; errorName: string; errorMessage: string; requestMethod: string | null; requestUrl: string | null; workerName: string | null; traceId: string | null; spanId: string | null; source: string | null }>; cursor: string | null } {
|
|
332
|
+
const limit = opts.limit ?? 50;
|
|
333
|
+
const { time: cursorTime, id: cursorId } = parseCursor(opts.cursor);
|
|
334
|
+
|
|
335
|
+
const rows = this.db.prepare<Record<string, unknown>, [number, number, string, number]>(`
|
|
336
|
+
SELECT id, timestamp, error_name, error_message, request_method, request_url, worker_name, trace_id, span_id, source
|
|
337
|
+
FROM errors
|
|
338
|
+
WHERE timestamp < ? OR (timestamp = ? AND id < ?)
|
|
339
|
+
ORDER BY timestamp DESC, id DESC
|
|
340
|
+
LIMIT ?
|
|
341
|
+
`).all(cursorTime, cursorTime, cursorId, limit + 1);
|
|
342
|
+
|
|
343
|
+
const hasMore = rows.length > limit;
|
|
344
|
+
const items = (hasMore ? rows.slice(0, limit) : rows).map(r => ({
|
|
345
|
+
id: r.id as string,
|
|
346
|
+
timestamp: r.timestamp as number,
|
|
347
|
+
errorName: r.error_name as string,
|
|
348
|
+
errorMessage: r.error_message as string,
|
|
349
|
+
requestMethod: r.request_method as string | null,
|
|
350
|
+
requestUrl: r.request_url as string | null,
|
|
351
|
+
workerName: r.worker_name as string | null,
|
|
352
|
+
traceId: r.trace_id as string | null,
|
|
353
|
+
spanId: r.span_id as string | null,
|
|
354
|
+
source: r.source as string | null,
|
|
355
|
+
}));
|
|
356
|
+
|
|
357
|
+
const last = items[items.length - 1];
|
|
358
|
+
const cursor = hasMore && last ? buildCursor(last.timestamp, last.id) : null;
|
|
359
|
+
return { items, cursor };
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
getError(id: string): { id: string; timestamp: number; traceId: string | null; spanId: string | null; source: string | null; data: unknown } | null {
|
|
363
|
+
const row = this.db.prepare<Record<string, unknown>, [string]>(
|
|
364
|
+
"SELECT id, timestamp, trace_id, span_id, source, data FROM errors WHERE id = ?"
|
|
365
|
+
).get(id);
|
|
366
|
+
if (!row) return null;
|
|
367
|
+
return {
|
|
368
|
+
id: row.id as string,
|
|
369
|
+
timestamp: row.timestamp as number,
|
|
370
|
+
traceId: row.trace_id as string | null,
|
|
371
|
+
spanId: row.span_id as string | null,
|
|
372
|
+
source: row.source as string | null,
|
|
373
|
+
data: JSON.parse(row.data as string),
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
getErrorsForTrace(traceId: string): Array<{ id: string; timestamp: number; errorName: string; errorMessage: string; source: string | null; data: unknown }> {
|
|
378
|
+
return this.db.prepare<Record<string, unknown>, [string]>(
|
|
379
|
+
"SELECT id, timestamp, error_name, error_message, source, data FROM errors WHERE trace_id = ? ORDER BY timestamp ASC"
|
|
380
|
+
).all(traceId).map(r => ({
|
|
381
|
+
id: r.id as string,
|
|
382
|
+
timestamp: r.timestamp as number,
|
|
383
|
+
errorName: r.error_name as string,
|
|
384
|
+
errorMessage: r.error_message as string,
|
|
385
|
+
source: r.source as string | null,
|
|
386
|
+
data: r.data ? JSON.parse(r.data as string) : null,
|
|
387
|
+
}));
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
deleteError(id: string): void {
|
|
391
|
+
this.db.prepare("DELETE FROM errors WHERE id = ?").run(id);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
clearErrors(): void {
|
|
395
|
+
this.db.run("DELETE FROM errors");
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
getErrorCount(): number {
|
|
399
|
+
return this.db.prepare<{ cnt: number }, []>("SELECT COUNT(*) as cnt FROM errors").get()?.cnt ?? 0;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
getErrorCountsByWorker(): Record<string, number> {
|
|
403
|
+
const rows = this.db.prepare<{ worker_name: string; cnt: number }, []>(
|
|
404
|
+
"SELECT COALESCE(worker_name, '') as worker_name, COUNT(*) as cnt FROM errors GROUP BY worker_name"
|
|
405
|
+
).all();
|
|
406
|
+
const map: Record<string, number> = {};
|
|
407
|
+
for (const r of rows) map[r.worker_name] = r.cnt;
|
|
408
|
+
return map;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
private enforceErrorCap(): void {
|
|
412
|
+
const count = this.db.prepare<{ cnt: number }, []>("SELECT COUNT(*) as cnt FROM errors").get()?.cnt ?? 0;
|
|
413
|
+
if (count <= 1000) return;
|
|
414
|
+
this.db.prepare(
|
|
415
|
+
"DELETE FROM errors WHERE id IN (SELECT id FROM errors ORDER BY timestamp ASC LIMIT 50)"
|
|
416
|
+
).run();
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
subscribe(listener: Listener): () => void {
|
|
420
|
+
this.listeners.add(listener);
|
|
421
|
+
return () => { this.listeners.delete(listener); };
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
private evictStaleSpans(): void {
|
|
425
|
+
const cutoff = Date.now() - STALE_SPAN_TTL_MS;
|
|
426
|
+
for (const [spanId, startTime] of this.startTimeCache) {
|
|
427
|
+
if (startTime < cutoff) {
|
|
428
|
+
this.startTimeCache.delete(spanId);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
private broadcast(event: TraceEvent): void {
|
|
434
|
+
for (const listener of this.listeners) {
|
|
435
|
+
try { listener(event); } catch {}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
private enforceTraceCap(): void {
|
|
440
|
+
if (this.rootSpanCount <= TRACE_CAP) return;
|
|
441
|
+
|
|
442
|
+
const oldest = this.db.prepare<{ trace_id: string }, [number]>(
|
|
443
|
+
"SELECT trace_id FROM spans WHERE parent_span_id IS NULL ORDER BY start_time ASC LIMIT ?"
|
|
444
|
+
).all(PRUNE_BATCH);
|
|
445
|
+
|
|
446
|
+
if (oldest.length === 0) return;
|
|
447
|
+
|
|
448
|
+
const ids = oldest.map(r => r.trace_id);
|
|
449
|
+
const placeholders = ids.map(() => "?").join(",");
|
|
450
|
+
|
|
451
|
+
this.db.transaction(() => {
|
|
452
|
+
this.db.prepare(`DELETE FROM span_events WHERE trace_id IN (${placeholders})`).run(...ids);
|
|
453
|
+
this.db.prepare(`DELETE FROM spans WHERE trace_id IN (${placeholders})`).run(...ids);
|
|
454
|
+
})();
|
|
455
|
+
|
|
456
|
+
this.rootSpanCount -= oldest.length;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
private rowToSpan(row: Record<string, unknown> | null): SpanData | null {
|
|
460
|
+
if (!row) return null;
|
|
461
|
+
return {
|
|
462
|
+
spanId: row.span_id as string,
|
|
463
|
+
traceId: row.trace_id as string,
|
|
464
|
+
parentSpanId: row.parent_span_id as string | null,
|
|
465
|
+
name: row.name as string,
|
|
466
|
+
kind: row.kind as SpanData["kind"],
|
|
467
|
+
status: row.status as SpanData["status"],
|
|
468
|
+
statusMessage: row.status_message as string | null,
|
|
469
|
+
startTime: row.start_time as number,
|
|
470
|
+
endTime: row.end_time as number | null,
|
|
471
|
+
durationMs: row.duration_ms as number | null,
|
|
472
|
+
attributes: row.attributes ? JSON.parse(row.attributes as string) : {},
|
|
473
|
+
workerName: row.worker_name as string | null,
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function buildCursor(time: number, id: string): string {
|
|
479
|
+
return `${time}:${id}`;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function parseCursor(cursor?: string): { time: number; id: string } {
|
|
483
|
+
if (!cursor) return { time: Number.MAX_SAFE_INTEGER, id: "\uffff" };
|
|
484
|
+
const sep = cursor.indexOf(":");
|
|
485
|
+
if (sep === -1) {
|
|
486
|
+
// Backwards-compatible: old numeric-only cursors
|
|
487
|
+
return { time: parseInt(cursor, 10), id: "\uffff" };
|
|
488
|
+
}
|
|
489
|
+
return { time: parseInt(cursor.substring(0, sep), 10), id: cursor.substring(sep + 1) };
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
let defaultStore: TraceStore | null = null;
|
|
493
|
+
|
|
494
|
+
export function getTraceStore(): TraceStore {
|
|
495
|
+
if (!defaultStore) {
|
|
496
|
+
defaultStore = new TraceStore();
|
|
497
|
+
}
|
|
498
|
+
return defaultStore;
|
|
499
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export interface SpanData {
|
|
2
|
+
spanId: string;
|
|
3
|
+
traceId: string;
|
|
4
|
+
parentSpanId: string | null;
|
|
5
|
+
name: string;
|
|
6
|
+
kind: "server" | "internal" | "client";
|
|
7
|
+
status: "ok" | "error" | "unset";
|
|
8
|
+
statusMessage: string | null;
|
|
9
|
+
startTime: number;
|
|
10
|
+
endTime: number | null;
|
|
11
|
+
durationMs: number | null;
|
|
12
|
+
attributes: Record<string, unknown>;
|
|
13
|
+
workerName: string | null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface SpanEventData {
|
|
17
|
+
id?: number;
|
|
18
|
+
spanId: string;
|
|
19
|
+
traceId: string;
|
|
20
|
+
timestamp: number;
|
|
21
|
+
name: string;
|
|
22
|
+
level: string | null;
|
|
23
|
+
message: string | null;
|
|
24
|
+
attributes: Record<string, unknown>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type TraceEvent =
|
|
28
|
+
| { type: "span.start"; span: SpanData }
|
|
29
|
+
| { type: "span.end"; span: SpanData }
|
|
30
|
+
| { type: "span.event"; event: SpanEventData };
|
|
31
|
+
|
|
32
|
+
export interface TraceSummary {
|
|
33
|
+
traceId: string;
|
|
34
|
+
rootSpanName: string;
|
|
35
|
+
workerName: string | null;
|
|
36
|
+
status: "ok" | "error" | "unset";
|
|
37
|
+
statusMessage: string | null;
|
|
38
|
+
startTime: number;
|
|
39
|
+
durationMs: number | null;
|
|
40
|
+
spanCount: number;
|
|
41
|
+
errorCount: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface TraceDetail {
|
|
45
|
+
spans: SpanData[];
|
|
46
|
+
events: SpanEventData[];
|
|
47
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { DevEnvironment, createServerModuleRunner, createServerHotChannel, type Plugin } from "vite";
|
|
2
|
+
import type { ModuleRunner } from "vite/module-runner";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Custom DevEnvironment that is NOT a RunnableDevEnvironment.
|
|
6
|
+
*
|
|
7
|
+
* This is critical for React Router integration: React Router's dev middleware
|
|
8
|
+
* checks `isRunnableDevEnvironment(ssrEnvironment)` — if true, it tries to
|
|
9
|
+
* handle SSR requests itself (loading virtual:react-router/server-build).
|
|
10
|
+
* By extending DevEnvironment directly (not RunnableDevEnvironment), the
|
|
11
|
+
* instanceof check returns false, React Router calls next(), and Bunflare's
|
|
12
|
+
* middleware handles the request through the worker's fetch() handler.
|
|
13
|
+
*
|
|
14
|
+
* We still provide a `runner` getter for Bunflare's own middleware to import
|
|
15
|
+
* modules through Vite's transform pipeline (JSX, HMR, etc.).
|
|
16
|
+
*/
|
|
17
|
+
class BunflareDevEnvironment extends DevEnvironment {
|
|
18
|
+
private _runner: ModuleRunner | undefined;
|
|
19
|
+
|
|
20
|
+
get runner(): ModuleRunner {
|
|
21
|
+
if (!this._runner) {
|
|
22
|
+
this._runner = createServerModuleRunner(this);
|
|
23
|
+
}
|
|
24
|
+
return this._runner;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
override async close() {
|
|
28
|
+
if (this._runner) {
|
|
29
|
+
await this._runner.close();
|
|
30
|
+
}
|
|
31
|
+
await super.close();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Sets SSR environment resolve conditions for Cloudflare Workers compatibility.
|
|
37
|
+
* Creates a BunflareDevEnvironment (non-runnable) so framework plugins (React Router)
|
|
38
|
+
* delegate SSR handling to Bunflare's middleware.
|
|
39
|
+
*/
|
|
40
|
+
export function configPlugin(envName: string): Plugin {
|
|
41
|
+
return {
|
|
42
|
+
name: "bunflare:config",
|
|
43
|
+
config() {
|
|
44
|
+
return {
|
|
45
|
+
server: {
|
|
46
|
+
watch: {
|
|
47
|
+
ignored: ["**/.bunflare/**"],
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
environments: {
|
|
51
|
+
[envName]: {
|
|
52
|
+
resolve: {
|
|
53
|
+
externalConditions: ["workerd", "worker"],
|
|
54
|
+
},
|
|
55
|
+
dev: {
|
|
56
|
+
createEnvironment(name, config) {
|
|
57
|
+
return new BunflareDevEnvironment(name, config, {
|
|
58
|
+
hot: true,
|
|
59
|
+
transport: createServerHotChannel(),
|
|
60
|
+
});
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|