@prisma/streams-server 0.1.1 → 0.1.3
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/CONTRIBUTING.md +8 -0
- package/package.json +2 -1
- package/src/app.ts +290 -17
- package/src/app_core.ts +1833 -698
- package/src/app_local.ts +144 -4
- package/src/auto_tune.ts +62 -0
- package/src/bootstrap.ts +159 -1
- package/src/concurrency_gate.ts +108 -0
- package/src/config.ts +116 -14
- package/src/db/db.ts +1201 -131
- package/src/db/schema.ts +308 -8
- package/src/foreground_activity.ts +55 -0
- package/src/index/indexer.ts +254 -124
- package/src/index/lexicon_file_cache.ts +261 -0
- package/src/index/lexicon_format.ts +93 -0
- package/src/index/lexicon_indexer.ts +789 -0
- package/src/index/secondary_indexer.ts +824 -0
- package/src/index/secondary_schema.ts +105 -0
- package/src/ingest.ts +10 -12
- package/src/manifest.ts +143 -8
- package/src/memory.ts +183 -8
- package/src/metrics.ts +15 -29
- package/src/metrics_emitter.ts +26 -3
- package/src/notifier.ts +121 -5
- package/src/objectstore/accounting.ts +92 -0
- package/src/objectstore/mock_r2.ts +1 -1
- package/src/objectstore/r2.ts +17 -1
- package/src/profiles/evlog/schema.ts +234 -0
- package/src/profiles/evlog.ts +299 -0
- package/src/profiles/generic.ts +47 -0
- package/src/profiles/index.ts +205 -0
- package/src/profiles/metrics/block_format.ts +109 -0
- package/src/profiles/metrics/normalize.ts +366 -0
- package/src/profiles/metrics/schema.ts +319 -0
- package/src/profiles/metrics.ts +85 -0
- package/src/profiles/profile.ts +225 -0
- package/src/{touch/engine.ts → profiles/stateProtocol/changes.ts} +3 -20
- package/src/profiles/stateProtocol/routes.ts +389 -0
- package/src/profiles/stateProtocol/types.ts +6 -0
- package/src/profiles/stateProtocol/validation.ts +51 -0
- package/src/profiles/stateProtocol.ts +100 -0
- package/src/read_filter.ts +468 -0
- package/src/reader.ts +2151 -164
- package/src/runtime/host_runtime.ts +5 -0
- package/src/runtime_memory.ts +200 -0
- package/src/runtime_memory_sampler.ts +235 -0
- package/src/schema/read_json.ts +43 -0
- package/src/schema/registry.ts +563 -59
- package/src/search/agg_format.ts +638 -0
- package/src/search/aggregate.ts +389 -0
- package/src/search/binary/codec.ts +162 -0
- package/src/search/binary/docset.ts +67 -0
- package/src/search/binary/restart_strings.ts +181 -0
- package/src/search/binary/varint.ts +34 -0
- package/src/search/bitset.ts +19 -0
- package/src/search/col_format.ts +382 -0
- package/src/search/col_runtime.ts +59 -0
- package/src/search/column_encoding.ts +43 -0
- package/src/search/companion_file_cache.ts +319 -0
- package/src/search/companion_format.ts +313 -0
- package/src/search/companion_manager.ts +1086 -0
- package/src/search/companion_plan.ts +218 -0
- package/src/search/fts_format.ts +423 -0
- package/src/search/fts_runtime.ts +333 -0
- package/src/search/query.ts +875 -0
- package/src/search/schema.ts +245 -0
- package/src/segment/cache.ts +93 -2
- package/src/segment/cached_segment.ts +89 -0
- package/src/segment/format.ts +108 -36
- package/src/segment/segmenter.ts +79 -5
- package/src/segment/segmenter_worker.ts +35 -6
- package/src/segment/segmenter_workers.ts +42 -12
- package/src/server.ts +150 -36
- package/src/sqlite/adapter.ts +185 -14
- package/src/sqlite/runtime_stats.ts +163 -0
- package/src/stats.ts +3 -3
- package/src/stream_size_reconciler.ts +100 -0
- package/src/touch/canonical_change.ts +7 -0
- package/src/touch/live_metrics.ts +94 -64
- package/src/touch/live_templates.ts +15 -1
- package/src/touch/manager.ts +166 -88
- package/src/touch/{interpreter_worker.ts → processor_worker.ts} +19 -14
- package/src/touch/spec.ts +95 -92
- package/src/touch/touch_journal.ts +4 -0
- package/src/touch/worker_pool.ts +8 -14
- package/src/touch/worker_protocol.ts +3 -3
- package/src/uploader.ts +77 -6
- package/src/util/bloom256.ts +2 -2
- package/src/util/byte_lru.ts +73 -0
- package/src/util/lru.ts +8 -0
- package/src/util/stream_paths.ts +19 -0
package/src/metrics.ts
CHANGED
|
@@ -1,26 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
import { buildInternalMetricsRecord } from "./profiles/metrics/normalize";
|
|
2
2
|
|
|
3
|
-
type
|
|
4
|
-
apiVersion: "durable.streams/metrics/v1";
|
|
5
|
-
kind: "interval";
|
|
6
|
-
metric: string;
|
|
7
|
-
unit: "ns" | "bytes" | "count";
|
|
8
|
-
windowStart: number;
|
|
9
|
-
windowEnd: number;
|
|
10
|
-
intervalMs: number;
|
|
11
|
-
instance: string;
|
|
12
|
-
stream?: string;
|
|
13
|
-
tags?: Tags;
|
|
14
|
-
count: number;
|
|
15
|
-
sum: number;
|
|
16
|
-
min: number;
|
|
17
|
-
max: number;
|
|
18
|
-
avg: number;
|
|
19
|
-
p50: number;
|
|
20
|
-
p95: number;
|
|
21
|
-
p99: number;
|
|
22
|
-
buckets: Record<string, number>;
|
|
23
|
-
};
|
|
3
|
+
type Tags = Record<string, string>;
|
|
24
4
|
|
|
25
5
|
class Histogram {
|
|
26
6
|
private readonly maxSamples: number;
|
|
@@ -41,7 +21,7 @@ class Histogram {
|
|
|
41
21
|
if (value < this.min) this.min = value;
|
|
42
22
|
if (value > this.max) this.max = value;
|
|
43
23
|
const bucket = Math.floor(Math.log2(Math.max(1, value)));
|
|
44
|
-
const key = String(
|
|
24
|
+
const key = String(2 ** bucket);
|
|
45
25
|
this.buckets[key] = (this.buckets[key] ?? 0) + 1;
|
|
46
26
|
if (this.samples.length < this.maxSamples) {
|
|
47
27
|
this.samples.push(value);
|
|
@@ -75,6 +55,10 @@ class Histogram {
|
|
|
75
55
|
|
|
76
56
|
type SeriesKey = string;
|
|
77
57
|
|
|
58
|
+
export type MetricsMemoryStats = {
|
|
59
|
+
seriesCount: number;
|
|
60
|
+
};
|
|
61
|
+
|
|
78
62
|
class MetricSeries {
|
|
79
63
|
readonly metric: string;
|
|
80
64
|
readonly unit: "ns" | "bytes" | "count";
|
|
@@ -134,16 +118,18 @@ export class Metrics {
|
|
|
134
118
|
};
|
|
135
119
|
}
|
|
136
120
|
|
|
137
|
-
|
|
121
|
+
getMemoryStats(): MetricsMemoryStats {
|
|
122
|
+
return { seriesCount: this.series.size };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
flushInterval(): Record<string, unknown>[] {
|
|
138
126
|
const windowEnd = Date.now();
|
|
139
127
|
const intervalMs = windowEnd - this.windowStartMs;
|
|
140
|
-
const events:
|
|
128
|
+
const events: Record<string, unknown>[] = [];
|
|
141
129
|
for (const s of this.series.values()) {
|
|
142
130
|
const snap = s.hist.snapshotAndReset();
|
|
143
131
|
if (snap.count === 0) continue;
|
|
144
|
-
events.push({
|
|
145
|
-
apiVersion: "durable.streams/metrics/v1",
|
|
146
|
-
kind: "interval",
|
|
132
|
+
events.push(buildInternalMetricsRecord({
|
|
147
133
|
metric: s.metric,
|
|
148
134
|
unit: s.unit,
|
|
149
135
|
windowStart: this.windowStartMs,
|
|
@@ -153,7 +139,7 @@ export class Metrics {
|
|
|
153
139
|
stream: s.stream,
|
|
154
140
|
tags: s.tags,
|
|
155
141
|
...snap,
|
|
156
|
-
});
|
|
142
|
+
}));
|
|
157
143
|
}
|
|
158
144
|
this.windowStartMs = windowEnd;
|
|
159
145
|
return events;
|
package/src/metrics_emitter.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Result } from "better-result";
|
|
1
2
|
import type { IngestQueue } from "./ingest";
|
|
2
3
|
import type { Metrics } from "./metrics";
|
|
3
4
|
|
|
@@ -5,12 +6,27 @@ export class MetricsEmitter {
|
|
|
5
6
|
private readonly metrics: Metrics;
|
|
6
7
|
private readonly ingest: IngestQueue;
|
|
7
8
|
private readonly intervalMs: number;
|
|
9
|
+
private readonly onAppended?: (args: {
|
|
10
|
+
lastOffset: bigint;
|
|
11
|
+
stream: string;
|
|
12
|
+
}) => void;
|
|
13
|
+
private readonly collectRuntimeMetrics?: () => void;
|
|
8
14
|
private timer: any | null = null;
|
|
9
15
|
|
|
10
|
-
constructor(
|
|
16
|
+
constructor(
|
|
17
|
+
metrics: Metrics,
|
|
18
|
+
ingest: IngestQueue,
|
|
19
|
+
intervalMs: number,
|
|
20
|
+
opts?: {
|
|
21
|
+
onAppended?: (args: { lastOffset: bigint; stream: string }) => void;
|
|
22
|
+
collectRuntimeMetrics?: () => void;
|
|
23
|
+
},
|
|
24
|
+
) {
|
|
11
25
|
this.metrics = metrics;
|
|
12
26
|
this.ingest = ingest;
|
|
13
27
|
this.intervalMs = intervalMs;
|
|
28
|
+
this.onAppended = opts?.onAppended;
|
|
29
|
+
this.collectRuntimeMetrics = opts?.collectRuntimeMetrics;
|
|
14
30
|
}
|
|
15
31
|
|
|
16
32
|
start(): void {
|
|
@@ -29,20 +45,27 @@ export class MetricsEmitter {
|
|
|
29
45
|
const queue = this.ingest.getQueueStats();
|
|
30
46
|
this.metrics.record("tieredstore.ingest.queue.bytes", queue.bytes, "bytes");
|
|
31
47
|
this.metrics.record("tieredstore.ingest.queue.requests", queue.requests, "count");
|
|
48
|
+
this.collectRuntimeMetrics?.();
|
|
32
49
|
const events = this.metrics.flushInterval();
|
|
33
50
|
if (events.length === 0) return;
|
|
34
51
|
const rows = events.map((e) => ({
|
|
35
|
-
routingKey: e.
|
|
52
|
+
routingKey: typeof e.seriesKey === "string" ? new TextEncoder().encode(e.seriesKey) : null,
|
|
36
53
|
contentType: "application/json",
|
|
37
54
|
payload: new TextEncoder().encode(JSON.stringify(e)),
|
|
38
55
|
}));
|
|
39
56
|
try {
|
|
40
|
-
await this.ingest.appendInternal({
|
|
57
|
+
const appendRes = await this.ingest.appendInternal({
|
|
41
58
|
stream: "__stream_metrics__",
|
|
42
59
|
baseAppendMs: BigInt(Date.now()),
|
|
43
60
|
rows,
|
|
44
61
|
contentType: "application/json",
|
|
45
62
|
});
|
|
63
|
+
if (!Result.isError(appendRes)) {
|
|
64
|
+
this.onAppended?.({
|
|
65
|
+
lastOffset: appendRes.value.lastOffset,
|
|
66
|
+
stream: "__stream_metrics__",
|
|
67
|
+
});
|
|
68
|
+
}
|
|
46
69
|
} catch {
|
|
47
70
|
// best-effort; drop on failure
|
|
48
71
|
}
|
package/src/notifier.ts
CHANGED
|
@@ -1,8 +1,27 @@
|
|
|
1
1
|
type Waiter = { afterSeq: bigint; resolve: () => void };
|
|
2
|
+
type DetailsWaiter = { afterVersion: bigint; resolve: () => void };
|
|
3
|
+
|
|
4
|
+
export type StreamNotifierMemoryStats = {
|
|
5
|
+
waiterStreams: number;
|
|
6
|
+
waiters: number;
|
|
7
|
+
latestSeqStreams: number;
|
|
8
|
+
detailsWaiterStreams: number;
|
|
9
|
+
detailsWaiters: number;
|
|
10
|
+
detailsVersionStreams: number;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type StreamNotifierTopStreamEntry = {
|
|
14
|
+
stream: string;
|
|
15
|
+
waiters: number;
|
|
16
|
+
details_waiters: number;
|
|
17
|
+
total_waiters: number;
|
|
18
|
+
};
|
|
2
19
|
|
|
3
20
|
export class StreamNotifier {
|
|
4
21
|
private readonly waiters = new Map<string, Set<Waiter>>();
|
|
5
22
|
private readonly latestSeq = new Map<string, bigint>();
|
|
23
|
+
private readonly detailsWaiters = new Map<string, Set<DetailsWaiter>>();
|
|
24
|
+
private readonly detailsVersion = new Map<string, bigint>();
|
|
6
25
|
|
|
7
26
|
notify(stream: string, newEndSeq: bigint): void {
|
|
8
27
|
this.latestSeq.set(stream, newEndSeq);
|
|
@@ -52,13 +71,110 @@ export class StreamNotifier {
|
|
|
52
71
|
});
|
|
53
72
|
}
|
|
54
73
|
|
|
55
|
-
|
|
56
|
-
|
|
74
|
+
currentDetailsVersion(stream: string): bigint {
|
|
75
|
+
return this.detailsVersion.get(stream) ?? 0n;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
notifyDetailsChanged(stream: string): void {
|
|
79
|
+
const nextVersion = (this.detailsVersion.get(stream) ?? 0n) + 1n;
|
|
80
|
+
this.detailsVersion.set(stream, nextVersion);
|
|
81
|
+
const set = this.detailsWaiters.get(stream);
|
|
57
82
|
if (!set || set.size === 0) return;
|
|
58
83
|
for (const w of Array.from(set)) {
|
|
59
|
-
|
|
60
|
-
|
|
84
|
+
if (nextVersion > w.afterVersion) {
|
|
85
|
+
set.delete(w);
|
|
86
|
+
w.resolve();
|
|
87
|
+
}
|
|
61
88
|
}
|
|
62
|
-
if (set.size === 0) this.
|
|
89
|
+
if (set.size === 0) this.detailsWaiters.delete(stream);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
waitForDetailsChange(stream: string, afterVersion: bigint, timeoutMs: number, signal?: AbortSignal): Promise<void> {
|
|
93
|
+
if (signal?.aborted) return Promise.resolve();
|
|
94
|
+
const latest = this.detailsVersion.get(stream);
|
|
95
|
+
if (latest != null && latest > afterVersion) return Promise.resolve();
|
|
96
|
+
return new Promise((resolve) => {
|
|
97
|
+
let done = false;
|
|
98
|
+
const set = this.detailsWaiters.get(stream) ?? new Set();
|
|
99
|
+
const cleanup = () => {
|
|
100
|
+
if (done) return;
|
|
101
|
+
done = true;
|
|
102
|
+
const s = this.detailsWaiters.get(stream);
|
|
103
|
+
if (s) {
|
|
104
|
+
s.delete(waiter);
|
|
105
|
+
if (s.size === 0) this.detailsWaiters.delete(stream);
|
|
106
|
+
}
|
|
107
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
108
|
+
if (signal) signal.removeEventListener("abort", onAbort);
|
|
109
|
+
resolve();
|
|
110
|
+
};
|
|
111
|
+
const waiter: DetailsWaiter = { afterVersion, resolve: cleanup };
|
|
112
|
+
set.add(waiter);
|
|
113
|
+
this.detailsWaiters.set(stream, set);
|
|
114
|
+
|
|
115
|
+
const onAbort = () => cleanup();
|
|
116
|
+
if (signal) signal.addEventListener("abort", onAbort, { once: true });
|
|
117
|
+
|
|
118
|
+
let timeoutId: any | null = null;
|
|
119
|
+
if (timeoutMs > 0) {
|
|
120
|
+
timeoutId = setTimeout(() => {
|
|
121
|
+
cleanup();
|
|
122
|
+
}, timeoutMs);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
notifyClose(stream: string): void {
|
|
128
|
+
const set = this.waiters.get(stream);
|
|
129
|
+
if (set && set.size > 0) {
|
|
130
|
+
for (const w of Array.from(set)) {
|
|
131
|
+
set.delete(w);
|
|
132
|
+
w.resolve();
|
|
133
|
+
}
|
|
134
|
+
if (set.size === 0) this.waiters.delete(stream);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const detailsSet = this.detailsWaiters.get(stream);
|
|
138
|
+
if (detailsSet && detailsSet.size > 0) {
|
|
139
|
+
for (const w of Array.from(detailsSet)) {
|
|
140
|
+
detailsSet.delete(w);
|
|
141
|
+
w.resolve();
|
|
142
|
+
}
|
|
143
|
+
if (detailsSet.size === 0) this.detailsWaiters.delete(stream);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
getMemoryStats(): StreamNotifierMemoryStats {
|
|
148
|
+
let waiters = 0;
|
|
149
|
+
for (const set of this.waiters.values()) waiters += set.size;
|
|
150
|
+
let detailsWaiters = 0;
|
|
151
|
+
for (const set of this.detailsWaiters.values()) detailsWaiters += set.size;
|
|
152
|
+
return {
|
|
153
|
+
waiterStreams: this.waiters.size,
|
|
154
|
+
waiters,
|
|
155
|
+
latestSeqStreams: this.latestSeq.size,
|
|
156
|
+
detailsWaiterStreams: this.detailsWaiters.size,
|
|
157
|
+
detailsWaiters,
|
|
158
|
+
detailsVersionStreams: this.detailsVersion.size,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
getTopStreams(limit = 5): StreamNotifierTopStreamEntry[] {
|
|
163
|
+
const totals = new Map<string, StreamNotifierTopStreamEntry>();
|
|
164
|
+
for (const [stream, waiters] of this.waiters) {
|
|
165
|
+
const row = totals.get(stream) ?? { stream, waiters: 0, details_waiters: 0, total_waiters: 0 };
|
|
166
|
+
row.waiters = waiters.size;
|
|
167
|
+
row.total_waiters = row.waiters + row.details_waiters;
|
|
168
|
+
totals.set(stream, row);
|
|
169
|
+
}
|
|
170
|
+
for (const [stream, detailsWaiters] of this.detailsWaiters) {
|
|
171
|
+
const row = totals.get(stream) ?? { stream, waiters: 0, details_waiters: 0, total_waiters: 0 };
|
|
172
|
+
row.details_waiters = detailsWaiters.size;
|
|
173
|
+
row.total_waiters = row.waiters + row.details_waiters;
|
|
174
|
+
totals.set(stream, row);
|
|
175
|
+
}
|
|
176
|
+
return Array.from(totals.values())
|
|
177
|
+
.sort((a, b) => b.total_waiters - a.total_waiters || a.stream.localeCompare(b.stream))
|
|
178
|
+
.slice(0, Math.max(0, Math.floor(limit)));
|
|
63
179
|
}
|
|
64
180
|
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { SqliteDurableStore } from "../db/db";
|
|
2
|
+
import type { GetOptions, ObjectStore, PutResult } from "./interface";
|
|
3
|
+
|
|
4
|
+
type ClassifiedRequest = {
|
|
5
|
+
streamHash: string;
|
|
6
|
+
artifact: string;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
function classifyKey(key: string): ClassifiedRequest | null {
|
|
10
|
+
const match = /^streams\/([0-9a-f]{32})\/(.+)$/.exec(key);
|
|
11
|
+
if (!match) return null;
|
|
12
|
+
const [, streamHash, rest] = match;
|
|
13
|
+
if (rest === "manifest.json") return { streamHash, artifact: "manifest" };
|
|
14
|
+
if (rest === "schema-registry.json") return { streamHash, artifact: "schema_registry" };
|
|
15
|
+
if (rest.startsWith("index/")) return { streamHash, artifact: "routing_index" };
|
|
16
|
+
if (rest.startsWith("lexicon/")) return { streamHash, artifact: "routing_key_lexicon" };
|
|
17
|
+
if (rest.startsWith("secondary-index/")) return { streamHash, artifact: "exact_index" };
|
|
18
|
+
if (rest.startsWith("segments/") && rest.endsWith(".bin")) return { streamHash, artifact: "segment" };
|
|
19
|
+
if (rest.startsWith("segments/") && rest.endsWith(".cix")) return { streamHash, artifact: "bundled_companion" };
|
|
20
|
+
return { streamHash, artifact: "meta" };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function classifyListPrefix(prefix: string): ClassifiedRequest | null {
|
|
24
|
+
const exact = classifyKey(prefix.replace(/\/+$/, ""));
|
|
25
|
+
if (exact) return exact;
|
|
26
|
+
const match = /^streams\/([0-9a-f]{32})(?:\/(.+))?\/?$/.exec(prefix);
|
|
27
|
+
if (!match) return null;
|
|
28
|
+
const [, streamHash, rest = ""] = match;
|
|
29
|
+
if (rest === "" || rest === "segments") return { streamHash, artifact: "segment" };
|
|
30
|
+
if (rest === "index") return { streamHash, artifact: "routing_index" };
|
|
31
|
+
if (rest.startsWith("lexicon")) return { streamHash, artifact: "routing_key_lexicon" };
|
|
32
|
+
if (rest.startsWith("secondary-index")) return { streamHash, artifact: "exact_index" };
|
|
33
|
+
return { streamHash, artifact: "meta" };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export class AccountingObjectStore implements ObjectStore {
|
|
37
|
+
constructor(
|
|
38
|
+
private readonly inner: ObjectStore,
|
|
39
|
+
private readonly db: SqliteDurableStore
|
|
40
|
+
) {}
|
|
41
|
+
|
|
42
|
+
async put(key: string, data: Uint8Array, opts?: { contentType?: string; contentLength?: number }): Promise<PutResult> {
|
|
43
|
+
const res = await this.inner.put(key, data, opts);
|
|
44
|
+
const classified = classifyKey(key);
|
|
45
|
+
if (classified) this.db.recordObjectStoreRequestByHash(classified.streamHash, classified.artifact, "put", data.byteLength);
|
|
46
|
+
return res;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async putFile(key: string, path: string, size: number, opts?: { contentType?: string }): Promise<PutResult> {
|
|
50
|
+
if (!this.inner.putFile) {
|
|
51
|
+
const bytes = await Bun.file(path).bytes();
|
|
52
|
+
const res = await this.inner.put(key, bytes, {
|
|
53
|
+
contentType: opts?.contentType,
|
|
54
|
+
contentLength: size,
|
|
55
|
+
});
|
|
56
|
+
const classified = classifyKey(key);
|
|
57
|
+
if (classified) this.db.recordObjectStoreRequestByHash(classified.streamHash, classified.artifact, "put", size);
|
|
58
|
+
return res;
|
|
59
|
+
}
|
|
60
|
+
const res = await this.inner.putFile(key, path, size, opts);
|
|
61
|
+
const classified = classifyKey(key);
|
|
62
|
+
if (classified) this.db.recordObjectStoreRequestByHash(classified.streamHash, classified.artifact, "put", size);
|
|
63
|
+
return res;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async get(key: string, opts?: GetOptions): Promise<Uint8Array | null> {
|
|
67
|
+
const res = await this.inner.get(key, opts);
|
|
68
|
+
const classified = classifyKey(key);
|
|
69
|
+
if (classified) this.db.recordObjectStoreRequestByHash(classified.streamHash, classified.artifact, "get", res?.byteLength ?? 0);
|
|
70
|
+
return res;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async head(key: string): Promise<{ etag: string; size: number } | null> {
|
|
74
|
+
const res = await this.inner.head(key);
|
|
75
|
+
const classified = classifyKey(key);
|
|
76
|
+
if (classified) this.db.recordObjectStoreRequestByHash(classified.streamHash, classified.artifact, "head", res?.size ?? 0);
|
|
77
|
+
return res;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async delete(key: string): Promise<void> {
|
|
81
|
+
await this.inner.delete(key);
|
|
82
|
+
const classified = classifyKey(key);
|
|
83
|
+
if (classified) this.db.recordObjectStoreRequestByHash(classified.streamHash, classified.artifact, "delete", 0);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async list(prefix: string): Promise<string[]> {
|
|
87
|
+
const res = await this.inner.list(prefix);
|
|
88
|
+
const classified = classifyListPrefix(prefix);
|
|
89
|
+
if (classified) this.db.recordObjectStoreRequestByHash(classified.streamHash, classified.artifact, "list", 0);
|
|
90
|
+
return res;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -180,7 +180,7 @@ export class MockR2Store implements ObjectStore {
|
|
|
180
180
|
out = entry.bytes.slice(start, start + length);
|
|
181
181
|
} else if (entry.path) {
|
|
182
182
|
if (length === total) {
|
|
183
|
-
out =
|
|
183
|
+
out = readFileSync(entry.path);
|
|
184
184
|
} else {
|
|
185
185
|
const fd = openSync(entry.path, "r");
|
|
186
186
|
try {
|
package/src/objectstore/r2.ts
CHANGED
|
@@ -31,6 +31,22 @@ function stripQuotes(value: string | null): string {
|
|
|
31
31
|
return value.replace(/^\"|\"$/g, "");
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
function isMissingObjectError(err: unknown): boolean {
|
|
35
|
+
const record = err as Record<string, unknown> | null | undefined;
|
|
36
|
+
const status = record?.status;
|
|
37
|
+
const statusCode = record?.statusCode;
|
|
38
|
+
const code = String(record?.code ?? "");
|
|
39
|
+
const message = String(record?.message ?? err ?? "").toLowerCase();
|
|
40
|
+
if (status === 404 || statusCode === 404) return true;
|
|
41
|
+
if (code === "NoSuchKey" || code === "NotFound") return true;
|
|
42
|
+
return (
|
|
43
|
+
message.includes("not found") ||
|
|
44
|
+
message.includes("no such key") ||
|
|
45
|
+
message.includes("does not exist") ||
|
|
46
|
+
message === "missing"
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
34
50
|
export class R2ObjectStore implements ObjectStore {
|
|
35
51
|
private readonly client: Bun.S3Client;
|
|
36
52
|
|
|
@@ -76,13 +92,13 @@ export class R2ObjectStore implements ObjectStore {
|
|
|
76
92
|
async get(key: string, opts: GetOptions = {}): Promise<Uint8Array | null> {
|
|
77
93
|
try {
|
|
78
94
|
const file = this.file(key);
|
|
79
|
-
if (!(await file.exists())) return null;
|
|
80
95
|
const body =
|
|
81
96
|
opts.range == null
|
|
82
97
|
? file
|
|
83
98
|
: file.slice(opts.range.start, opts.range.end == null ? undefined : opts.range.end + 1);
|
|
84
99
|
return new Uint8Array(await body.arrayBuffer());
|
|
85
100
|
} catch (err) {
|
|
101
|
+
if (isMissingObjectError(err)) return null;
|
|
86
102
|
this.wrapError("GET", key, err);
|
|
87
103
|
}
|
|
88
104
|
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SCHEMA_REGISTRY_API_VERSION,
|
|
3
|
+
type SchemaRegistry,
|
|
4
|
+
type SearchConfig,
|
|
5
|
+
} from "../../schema/registry";
|
|
6
|
+
|
|
7
|
+
export const EVLOG_CANONICAL_SCHEMA = {
|
|
8
|
+
type: "object",
|
|
9
|
+
additionalProperties: false,
|
|
10
|
+
properties: {
|
|
11
|
+
timestamp: { type: "string" },
|
|
12
|
+
level: { type: "string", enum: ["debug", "info", "warn", "error"] },
|
|
13
|
+
service: { type: ["string", "null"] },
|
|
14
|
+
environment: { type: ["string", "null"] },
|
|
15
|
+
version: { type: ["string", "null"] },
|
|
16
|
+
region: { type: ["string", "null"] },
|
|
17
|
+
requestId: { type: ["string", "null"] },
|
|
18
|
+
traceId: { type: ["string", "null"] },
|
|
19
|
+
spanId: { type: ["string", "null"] },
|
|
20
|
+
method: { type: ["string", "null"] },
|
|
21
|
+
path: { type: ["string", "null"] },
|
|
22
|
+
status: { type: ["integer", "null"] },
|
|
23
|
+
duration: { type: ["number", "null"] },
|
|
24
|
+
message: { type: ["string", "null"] },
|
|
25
|
+
why: { type: ["string", "null"] },
|
|
26
|
+
fix: { type: ["string", "null"] },
|
|
27
|
+
link: { type: ["string", "null"] },
|
|
28
|
+
sampling: {
|
|
29
|
+
type: ["object", "null"],
|
|
30
|
+
additionalProperties: true,
|
|
31
|
+
},
|
|
32
|
+
redaction: {
|
|
33
|
+
type: "object",
|
|
34
|
+
additionalProperties: false,
|
|
35
|
+
properties: {
|
|
36
|
+
keys: {
|
|
37
|
+
type: "array",
|
|
38
|
+
items: { type: "string" },
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
required: ["keys"],
|
|
42
|
+
},
|
|
43
|
+
context: {
|
|
44
|
+
type: "object",
|
|
45
|
+
additionalProperties: true,
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
required: [
|
|
49
|
+
"timestamp",
|
|
50
|
+
"level",
|
|
51
|
+
"service",
|
|
52
|
+
"environment",
|
|
53
|
+
"version",
|
|
54
|
+
"region",
|
|
55
|
+
"requestId",
|
|
56
|
+
"traceId",
|
|
57
|
+
"spanId",
|
|
58
|
+
"method",
|
|
59
|
+
"path",
|
|
60
|
+
"status",
|
|
61
|
+
"duration",
|
|
62
|
+
"message",
|
|
63
|
+
"why",
|
|
64
|
+
"fix",
|
|
65
|
+
"link",
|
|
66
|
+
"sampling",
|
|
67
|
+
"redaction",
|
|
68
|
+
"context",
|
|
69
|
+
],
|
|
70
|
+
} as const;
|
|
71
|
+
|
|
72
|
+
export const EVLOG_DEFAULT_SEARCH_CONFIG: SearchConfig = {
|
|
73
|
+
profile: "evlog",
|
|
74
|
+
primaryTimestampField: "timestamp",
|
|
75
|
+
aliases: {
|
|
76
|
+
env: "environment",
|
|
77
|
+
msg: "message",
|
|
78
|
+
req: "requestId",
|
|
79
|
+
span: "spanId",
|
|
80
|
+
time: "timestamp",
|
|
81
|
+
trace: "traceId",
|
|
82
|
+
ts: "timestamp",
|
|
83
|
+
},
|
|
84
|
+
defaultFields: [
|
|
85
|
+
{ field: "message", boost: 2 },
|
|
86
|
+
{ field: "why", boost: 1.5 },
|
|
87
|
+
{ field: "fix", boost: 1.25 },
|
|
88
|
+
{ field: "error.message", boost: 2 },
|
|
89
|
+
],
|
|
90
|
+
fields: {
|
|
91
|
+
timestamp: {
|
|
92
|
+
kind: "date",
|
|
93
|
+
bindings: [{ version: 1, jsonPointer: "/timestamp" }],
|
|
94
|
+
exact: true,
|
|
95
|
+
column: true,
|
|
96
|
+
exists: true,
|
|
97
|
+
sortable: true,
|
|
98
|
+
aggregatable: true,
|
|
99
|
+
},
|
|
100
|
+
level: {
|
|
101
|
+
kind: "keyword",
|
|
102
|
+
bindings: [{ version: 1, jsonPointer: "/level" }],
|
|
103
|
+
normalizer: "lowercase_v1",
|
|
104
|
+
exact: true,
|
|
105
|
+
prefix: true,
|
|
106
|
+
exists: true,
|
|
107
|
+
sortable: true,
|
|
108
|
+
aggregatable: true,
|
|
109
|
+
},
|
|
110
|
+
service: {
|
|
111
|
+
kind: "keyword",
|
|
112
|
+
bindings: [{ version: 1, jsonPointer: "/service" }],
|
|
113
|
+
normalizer: "lowercase_v1",
|
|
114
|
+
exact: true,
|
|
115
|
+
prefix: true,
|
|
116
|
+
exists: true,
|
|
117
|
+
sortable: true,
|
|
118
|
+
aggregatable: true,
|
|
119
|
+
},
|
|
120
|
+
environment: {
|
|
121
|
+
kind: "keyword",
|
|
122
|
+
bindings: [{ version: 1, jsonPointer: "/environment" }],
|
|
123
|
+
normalizer: "lowercase_v1",
|
|
124
|
+
exact: true,
|
|
125
|
+
prefix: true,
|
|
126
|
+
exists: true,
|
|
127
|
+
sortable: true,
|
|
128
|
+
aggregatable: true,
|
|
129
|
+
},
|
|
130
|
+
requestId: {
|
|
131
|
+
kind: "keyword",
|
|
132
|
+
bindings: [{ version: 1, jsonPointer: "/requestId" }],
|
|
133
|
+
exact: true,
|
|
134
|
+
prefix: true,
|
|
135
|
+
exists: true,
|
|
136
|
+
sortable: true,
|
|
137
|
+
},
|
|
138
|
+
traceId: {
|
|
139
|
+
kind: "keyword",
|
|
140
|
+
bindings: [{ version: 1, jsonPointer: "/traceId" }],
|
|
141
|
+
exact: true,
|
|
142
|
+
prefix: true,
|
|
143
|
+
exists: true,
|
|
144
|
+
sortable: true,
|
|
145
|
+
},
|
|
146
|
+
spanId: {
|
|
147
|
+
kind: "keyword",
|
|
148
|
+
bindings: [{ version: 1, jsonPointer: "/spanId" }],
|
|
149
|
+
exact: true,
|
|
150
|
+
prefix: true,
|
|
151
|
+
exists: true,
|
|
152
|
+
sortable: true,
|
|
153
|
+
},
|
|
154
|
+
path: {
|
|
155
|
+
kind: "keyword",
|
|
156
|
+
bindings: [{ version: 1, jsonPointer: "/path" }],
|
|
157
|
+
exact: true,
|
|
158
|
+
prefix: true,
|
|
159
|
+
exists: true,
|
|
160
|
+
sortable: true,
|
|
161
|
+
aggregatable: true,
|
|
162
|
+
},
|
|
163
|
+
method: {
|
|
164
|
+
kind: "keyword",
|
|
165
|
+
bindings: [{ version: 1, jsonPointer: "/method" }],
|
|
166
|
+
normalizer: "lowercase_v1",
|
|
167
|
+
exact: true,
|
|
168
|
+
prefix: true,
|
|
169
|
+
exists: true,
|
|
170
|
+
sortable: true,
|
|
171
|
+
aggregatable: true,
|
|
172
|
+
},
|
|
173
|
+
status: {
|
|
174
|
+
kind: "integer",
|
|
175
|
+
bindings: [{ version: 1, jsonPointer: "/status" }],
|
|
176
|
+
exact: true,
|
|
177
|
+
column: true,
|
|
178
|
+
exists: true,
|
|
179
|
+
sortable: true,
|
|
180
|
+
aggregatable: true,
|
|
181
|
+
},
|
|
182
|
+
duration: {
|
|
183
|
+
kind: "float",
|
|
184
|
+
bindings: [{ version: 1, jsonPointer: "/duration" }],
|
|
185
|
+
exact: true,
|
|
186
|
+
column: true,
|
|
187
|
+
exists: true,
|
|
188
|
+
sortable: true,
|
|
189
|
+
aggregatable: true,
|
|
190
|
+
},
|
|
191
|
+
message: {
|
|
192
|
+
kind: "text",
|
|
193
|
+
bindings: [{ version: 1, jsonPointer: "/message" }],
|
|
194
|
+
analyzer: "unicode_word_v1",
|
|
195
|
+
exists: true,
|
|
196
|
+
positions: true,
|
|
197
|
+
},
|
|
198
|
+
why: {
|
|
199
|
+
kind: "text",
|
|
200
|
+
bindings: [{ version: 1, jsonPointer: "/why" }],
|
|
201
|
+
analyzer: "unicode_word_v1",
|
|
202
|
+
exists: true,
|
|
203
|
+
positions: true,
|
|
204
|
+
},
|
|
205
|
+
fix: {
|
|
206
|
+
kind: "text",
|
|
207
|
+
bindings: [{ version: 1, jsonPointer: "/fix" }],
|
|
208
|
+
analyzer: "unicode_word_v1",
|
|
209
|
+
exists: true,
|
|
210
|
+
positions: true,
|
|
211
|
+
},
|
|
212
|
+
"error.message": {
|
|
213
|
+
kind: "text",
|
|
214
|
+
bindings: [{ version: 1, jsonPointer: "/context/error/message" }],
|
|
215
|
+
analyzer: "unicode_word_v1",
|
|
216
|
+
exists: true,
|
|
217
|
+
positions: true,
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
export function buildEvlogDefaultRegistry(stream: string): SchemaRegistry {
|
|
223
|
+
return {
|
|
224
|
+
apiVersion: SCHEMA_REGISTRY_API_VERSION,
|
|
225
|
+
schema: stream,
|
|
226
|
+
currentVersion: 1,
|
|
227
|
+
search: structuredClone(EVLOG_DEFAULT_SEARCH_CONFIG),
|
|
228
|
+
boundaries: [{ offset: 0, version: 1 }],
|
|
229
|
+
schemas: {
|
|
230
|
+
"1": structuredClone(EVLOG_CANONICAL_SCHEMA),
|
|
231
|
+
},
|
|
232
|
+
lenses: {},
|
|
233
|
+
};
|
|
234
|
+
}
|