@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.
Files changed (91) hide show
  1. package/CONTRIBUTING.md +8 -0
  2. package/package.json +2 -1
  3. package/src/app.ts +290 -17
  4. package/src/app_core.ts +1833 -698
  5. package/src/app_local.ts +144 -4
  6. package/src/auto_tune.ts +62 -0
  7. package/src/bootstrap.ts +159 -1
  8. package/src/concurrency_gate.ts +108 -0
  9. package/src/config.ts +116 -14
  10. package/src/db/db.ts +1201 -131
  11. package/src/db/schema.ts +308 -8
  12. package/src/foreground_activity.ts +55 -0
  13. package/src/index/indexer.ts +254 -124
  14. package/src/index/lexicon_file_cache.ts +261 -0
  15. package/src/index/lexicon_format.ts +93 -0
  16. package/src/index/lexicon_indexer.ts +789 -0
  17. package/src/index/secondary_indexer.ts +824 -0
  18. package/src/index/secondary_schema.ts +105 -0
  19. package/src/ingest.ts +10 -12
  20. package/src/manifest.ts +143 -8
  21. package/src/memory.ts +183 -8
  22. package/src/metrics.ts +15 -29
  23. package/src/metrics_emitter.ts +26 -3
  24. package/src/notifier.ts +121 -5
  25. package/src/objectstore/accounting.ts +92 -0
  26. package/src/objectstore/mock_r2.ts +1 -1
  27. package/src/objectstore/r2.ts +17 -1
  28. package/src/profiles/evlog/schema.ts +234 -0
  29. package/src/profiles/evlog.ts +299 -0
  30. package/src/profiles/generic.ts +47 -0
  31. package/src/profiles/index.ts +205 -0
  32. package/src/profiles/metrics/block_format.ts +109 -0
  33. package/src/profiles/metrics/normalize.ts +366 -0
  34. package/src/profiles/metrics/schema.ts +319 -0
  35. package/src/profiles/metrics.ts +85 -0
  36. package/src/profiles/profile.ts +225 -0
  37. package/src/{touch/engine.ts → profiles/stateProtocol/changes.ts} +3 -20
  38. package/src/profiles/stateProtocol/routes.ts +389 -0
  39. package/src/profiles/stateProtocol/types.ts +6 -0
  40. package/src/profiles/stateProtocol/validation.ts +51 -0
  41. package/src/profiles/stateProtocol.ts +100 -0
  42. package/src/read_filter.ts +468 -0
  43. package/src/reader.ts +2151 -164
  44. package/src/runtime/host_runtime.ts +5 -0
  45. package/src/runtime_memory.ts +200 -0
  46. package/src/runtime_memory_sampler.ts +235 -0
  47. package/src/schema/read_json.ts +43 -0
  48. package/src/schema/registry.ts +563 -59
  49. package/src/search/agg_format.ts +638 -0
  50. package/src/search/aggregate.ts +389 -0
  51. package/src/search/binary/codec.ts +162 -0
  52. package/src/search/binary/docset.ts +67 -0
  53. package/src/search/binary/restart_strings.ts +181 -0
  54. package/src/search/binary/varint.ts +34 -0
  55. package/src/search/bitset.ts +19 -0
  56. package/src/search/col_format.ts +382 -0
  57. package/src/search/col_runtime.ts +59 -0
  58. package/src/search/column_encoding.ts +43 -0
  59. package/src/search/companion_file_cache.ts +319 -0
  60. package/src/search/companion_format.ts +313 -0
  61. package/src/search/companion_manager.ts +1086 -0
  62. package/src/search/companion_plan.ts +218 -0
  63. package/src/search/fts_format.ts +423 -0
  64. package/src/search/fts_runtime.ts +333 -0
  65. package/src/search/query.ts +875 -0
  66. package/src/search/schema.ts +245 -0
  67. package/src/segment/cache.ts +93 -2
  68. package/src/segment/cached_segment.ts +89 -0
  69. package/src/segment/format.ts +108 -36
  70. package/src/segment/segmenter.ts +79 -5
  71. package/src/segment/segmenter_worker.ts +35 -6
  72. package/src/segment/segmenter_workers.ts +42 -12
  73. package/src/server.ts +150 -36
  74. package/src/sqlite/adapter.ts +185 -14
  75. package/src/sqlite/runtime_stats.ts +163 -0
  76. package/src/stats.ts +3 -3
  77. package/src/stream_size_reconciler.ts +100 -0
  78. package/src/touch/canonical_change.ts +7 -0
  79. package/src/touch/live_metrics.ts +94 -64
  80. package/src/touch/live_templates.ts +15 -1
  81. package/src/touch/manager.ts +166 -88
  82. package/src/touch/{interpreter_worker.ts → processor_worker.ts} +19 -14
  83. package/src/touch/spec.ts +95 -92
  84. package/src/touch/touch_journal.ts +4 -0
  85. package/src/touch/worker_pool.ts +8 -14
  86. package/src/touch/worker_protocol.ts +3 -3
  87. package/src/uploader.ts +77 -6
  88. package/src/util/bloom256.ts +2 -2
  89. package/src/util/byte_lru.ts +73 -0
  90. package/src/util/lru.ts +8 -0
  91. package/src/util/stream_paths.ts +19 -0
package/src/metrics.ts CHANGED
@@ -1,26 +1,6 @@
1
- type Tags = Record<string, string>;
1
+ import { buildInternalMetricsRecord } from "./profiles/metrics/normalize";
2
2
 
3
- type MetricEvent = {
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(1 << bucket);
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
- flushInterval(): MetricEvent[] {
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: MetricEvent[] = [];
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;
@@ -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(metrics: Metrics, ingest: IngestQueue, intervalMs: number) {
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.stream ? new TextEncoder().encode(e.stream) : null,
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
- notifyClose(stream: string): void {
56
- const set = this.waiters.get(stream);
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
- set.delete(w);
60
- w.resolve();
84
+ if (nextVersion > w.afterVersion) {
85
+ set.delete(w);
86
+ w.resolve();
87
+ }
61
88
  }
62
- if (set.size === 0) this.waiters.delete(stream);
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 = new Uint8Array(readFileSync(entry.path));
183
+ out = readFileSync(entry.path);
184
184
  } else {
185
185
  const fd = openSync(entry.path, "r");
186
186
  try {
@@ -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
+ }