@tungthedev/streams-server 0.2.0

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 (183) hide show
  1. package/CODE_OF_CONDUCT.md +45 -0
  2. package/CONTRIBUTING.md +76 -0
  3. package/LICENSE +201 -0
  4. package/README.md +58 -0
  5. package/SECURITY.md +42 -0
  6. package/bin/prisma-streams-server +2 -0
  7. package/package.json +46 -0
  8. package/src/app.ts +583 -0
  9. package/src/app_core.ts +3144 -0
  10. package/src/app_local.ts +206 -0
  11. package/src/auth.ts +124 -0
  12. package/src/auto_tune.ts +69 -0
  13. package/src/backpressure.ts +66 -0
  14. package/src/bootstrap.ts +613 -0
  15. package/src/compute/demo_entry.ts +415 -0
  16. package/src/compute/demo_site.ts +1242 -0
  17. package/src/compute/entry.ts +19 -0
  18. package/src/compute/package_entry.ts +4 -0
  19. package/src/compute/virtual-modules.d.ts +15 -0
  20. package/src/compute/worker_module_url.ts +9 -0
  21. package/src/concurrency_gate.ts +108 -0
  22. package/src/config.ts +402 -0
  23. package/src/db/bootstrap_store.ts +9 -0
  24. package/src/db/db.ts +2424 -0
  25. package/src/db/schema.ts +925 -0
  26. package/src/db/sqlite_manifest_snapshot.ts +81 -0
  27. package/src/db/sqlite_touch_store.ts +491 -0
  28. package/src/db/sqlite_wal_store.ts +472 -0
  29. package/src/details/full_mode_details.ts +568 -0
  30. package/src/expiry_sweeper.ts +47 -0
  31. package/src/foreground_activity.ts +55 -0
  32. package/src/hist.ts +169 -0
  33. package/src/index/binary_fuse.ts +379 -0
  34. package/src/index/indexer.ts +947 -0
  35. package/src/index/lexicon_file_cache.ts +261 -0
  36. package/src/index/lexicon_format.ts +93 -0
  37. package/src/index/lexicon_indexer.ts +863 -0
  38. package/src/index/run_cache.ts +84 -0
  39. package/src/index/run_format.ts +213 -0
  40. package/src/index/schedule.ts +28 -0
  41. package/src/index/secondary_indexer.ts +901 -0
  42. package/src/index/secondary_schema.ts +105 -0
  43. package/src/ingest.ts +309 -0
  44. package/src/lens/lens.ts +501 -0
  45. package/src/manifest.ts +249 -0
  46. package/src/memory.ts +334 -0
  47. package/src/metrics.ts +147 -0
  48. package/src/metrics_emitter.ts +83 -0
  49. package/src/notifier.ts +180 -0
  50. package/src/objectstore/accounting.ts +151 -0
  51. package/src/objectstore/interface.ts +13 -0
  52. package/src/objectstore/mock_r2.ts +269 -0
  53. package/src/objectstore/null.ts +32 -0
  54. package/src/objectstore/r2.ts +318 -0
  55. package/src/observe/pairing.ts +61 -0
  56. package/src/observe/request.ts +772 -0
  57. package/src/offset.ts +70 -0
  58. package/src/postgres/bootstrap.ts +269 -0
  59. package/src/postgres/companions.ts +197 -0
  60. package/src/postgres/control_restore.ts +109 -0
  61. package/src/postgres/details.ts +189 -0
  62. package/src/postgres/lexicon_index.ts +260 -0
  63. package/src/postgres/routing_index.ts +189 -0
  64. package/src/postgres/rows.ts +132 -0
  65. package/src/postgres/schema.ts +355 -0
  66. package/src/postgres/secondary_index.ts +238 -0
  67. package/src/postgres/segments.ts +900 -0
  68. package/src/postgres/stats.ts +103 -0
  69. package/src/postgres/store.ts +947 -0
  70. package/src/postgres/touch.ts +591 -0
  71. package/src/postgres/types.ts +32 -0
  72. package/src/profiles/evlog/schema.ts +234 -0
  73. package/src/profiles/evlog.ts +473 -0
  74. package/src/profiles/generic.ts +51 -0
  75. package/src/profiles/index.ts +237 -0
  76. package/src/profiles/metrics/block_format.ts +109 -0
  77. package/src/profiles/metrics/normalize.ts +366 -0
  78. package/src/profiles/metrics/schema.ts +319 -0
  79. package/src/profiles/metrics.ts +83 -0
  80. package/src/profiles/otelTraces/normalize.ts +955 -0
  81. package/src/profiles/otelTraces/otlp.ts +1002 -0
  82. package/src/profiles/otelTraces/schema.ts +408 -0
  83. package/src/profiles/otelTraces.ts +390 -0
  84. package/src/profiles/profile.ts +284 -0
  85. package/src/profiles/stateProtocol/change_event_conformance.typecheck.ts +35 -0
  86. package/src/profiles/stateProtocol/changes.ts +24 -0
  87. package/src/profiles/stateProtocol/ingest.ts +115 -0
  88. package/src/profiles/stateProtocol/routes.ts +511 -0
  89. package/src/profiles/stateProtocol/types.ts +6 -0
  90. package/src/profiles/stateProtocol/validation.ts +51 -0
  91. package/src/profiles/stateProtocol.ts +107 -0
  92. package/src/read_filter.ts +468 -0
  93. package/src/reader.ts +2986 -0
  94. package/src/runtime/hash.ts +156 -0
  95. package/src/runtime/hash_vendor/LICENSE.hash-wasm +38 -0
  96. package/src/runtime/hash_vendor/NOTICE.md +8 -0
  97. package/src/runtime/hash_vendor/xxhash3.umd.min.cjs +7 -0
  98. package/src/runtime/hash_vendor/xxhash32.umd.min.cjs +7 -0
  99. package/src/runtime/hash_vendor/xxhash64.umd.min.cjs +7 -0
  100. package/src/runtime/host_runtime.ts +5 -0
  101. package/src/runtime_memory.ts +200 -0
  102. package/src/runtime_memory_sampler.ts +237 -0
  103. package/src/schema/lens_schema.ts +290 -0
  104. package/src/schema/proof.ts +547 -0
  105. package/src/schema/read_json.ts +51 -0
  106. package/src/schema/registry.ts +966 -0
  107. package/src/search/agg_format.ts +638 -0
  108. package/src/search/aggregate.ts +409 -0
  109. package/src/search/binary/codec.ts +162 -0
  110. package/src/search/binary/docset.ts +67 -0
  111. package/src/search/binary/restart_strings.ts +181 -0
  112. package/src/search/binary/varint.ts +34 -0
  113. package/src/search/bitset.ts +19 -0
  114. package/src/search/col_format.ts +382 -0
  115. package/src/search/col_runtime.ts +59 -0
  116. package/src/search/column_encoding.ts +43 -0
  117. package/src/search/companion_file_cache.ts +319 -0
  118. package/src/search/companion_format.ts +327 -0
  119. package/src/search/companion_manager.ts +1305 -0
  120. package/src/search/companion_plan.ts +229 -0
  121. package/src/search/exact_format.ts +281 -0
  122. package/src/search/exact_runtime.ts +55 -0
  123. package/src/search/fts_format.ts +423 -0
  124. package/src/search/fts_runtime.ts +333 -0
  125. package/src/search/query.ts +875 -0
  126. package/src/search/schema.ts +245 -0
  127. package/src/segment/cache.ts +270 -0
  128. package/src/segment/cached_segment.ts +89 -0
  129. package/src/segment/format.ts +403 -0
  130. package/src/segment/segmenter.ts +412 -0
  131. package/src/segment/segmenter_worker.ts +72 -0
  132. package/src/segment/segmenter_workers.ts +130 -0
  133. package/src/server.ts +264 -0
  134. package/src/server_auto_tune.ts +158 -0
  135. package/src/sqlite/adapter.ts +335 -0
  136. package/src/sqlite/runtime_stats.ts +163 -0
  137. package/src/stats.ts +205 -0
  138. package/src/store/append.ts +50 -0
  139. package/src/store/bootstrap_restore_store.ts +71 -0
  140. package/src/store/capabilities.ts +86 -0
  141. package/src/store/full_mode_details_store.ts +71 -0
  142. package/src/store/index_store.ts +104 -0
  143. package/src/store/profile_touch_store.ts +1 -0
  144. package/src/store/rows.ts +144 -0
  145. package/src/store/schema_profile_store.ts +73 -0
  146. package/src/store/schema_publication.ts +6 -0
  147. package/src/store/segment_manifest_store.ts +129 -0
  148. package/src/store/segment_read_store.ts +22 -0
  149. package/src/store/stats_accounting_store.ts +83 -0
  150. package/src/store/touch_store.ts +98 -0
  151. package/src/store/wal_store.ts +21 -0
  152. package/src/stream_size_reconciler.ts +100 -0
  153. package/src/touch/canonical_change.ts +7 -0
  154. package/src/touch/live_keys.ts +158 -0
  155. package/src/touch/live_metrics.ts +841 -0
  156. package/src/touch/live_templates.ts +449 -0
  157. package/src/touch/manager.ts +1292 -0
  158. package/src/touch/process_batch.ts +576 -0
  159. package/src/touch/processor_worker.ts +85 -0
  160. package/src/touch/spec.ts +459 -0
  161. package/src/touch/touch_journal.ts +771 -0
  162. package/src/touch/touch_key_id.ts +20 -0
  163. package/src/touch/worker_pool.ts +191 -0
  164. package/src/touch/worker_protocol.ts +57 -0
  165. package/src/types/proper-lockfile.d.ts +1 -0
  166. package/src/uploader.ts +358 -0
  167. package/src/util/base32_crockford.ts +81 -0
  168. package/src/util/bloom256.ts +67 -0
  169. package/src/util/byte_lru.ts +73 -0
  170. package/src/util/cleanup.ts +22 -0
  171. package/src/util/crc32c.ts +29 -0
  172. package/src/util/ds_error.ts +15 -0
  173. package/src/util/duration.ts +17 -0
  174. package/src/util/endian.ts +53 -0
  175. package/src/util/json_pointer.ts +148 -0
  176. package/src/util/log.ts +25 -0
  177. package/src/util/lru.ts +53 -0
  178. package/src/util/retry.ts +35 -0
  179. package/src/util/siphash.ts +71 -0
  180. package/src/util/stream_paths.ts +50 -0
  181. package/src/util/time.ts +14 -0
  182. package/src/util/yield.ts +3 -0
  183. package/src/util/zstd.ts +24 -0
@@ -0,0 +1,366 @@
1
+ import { Result } from "better-result";
2
+ import type { AggSummaryState } from "../../search/agg_format";
3
+ import type { NormalizedMetricsRecord } from "../profile";
4
+ import { expectPlainObjectResult, isPlainObject } from "../profile";
5
+
6
+ type PrimitiveAttribute = string | number | boolean | bigint;
7
+
8
+ type HistogramMap = Record<string, number>;
9
+
10
+ function normalizeString(value: unknown): string | null {
11
+ if (typeof value !== "string") return null;
12
+ const trimmed = value.trim();
13
+ return trimmed === "" ? null : trimmed;
14
+ }
15
+
16
+ function normalizeFiniteNumber(value: unknown): number | null {
17
+ if (typeof value === "number" && Number.isFinite(value)) return value;
18
+ if (typeof value === "bigint") return Number(value);
19
+ if (typeof value === "string" && value.trim() !== "") {
20
+ const parsed = Number(value);
21
+ if (Number.isFinite(parsed)) return parsed;
22
+ }
23
+ return null;
24
+ }
25
+
26
+ function normalizeInteger(value: unknown): number | null {
27
+ const numeric = normalizeFiniteNumber(value);
28
+ if (numeric == null) return null;
29
+ return Math.trunc(numeric);
30
+ }
31
+
32
+ function normalizeHistogram(value: unknown): HistogramMap | undefined {
33
+ if (!isPlainObject(value)) return undefined;
34
+ const out: HistogramMap = {};
35
+ for (const [bucket, raw] of Object.entries(value)) {
36
+ const count = normalizeFiniteNumber(raw);
37
+ if (count == null || count <= 0) continue;
38
+ out[String(bucket)] = (out[String(bucket)] ?? 0) + count;
39
+ }
40
+ return Object.keys(out).length > 0 ? out : undefined;
41
+ }
42
+
43
+ function histogramPercentile(histogram: HistogramMap | undefined, percentile: number): number | null {
44
+ if (!histogram) return null;
45
+ const entries = Object.entries(histogram)
46
+ .map(([bucket, count]) => ({ bucket: Number(bucket), count }))
47
+ .filter((entry) => Number.isFinite(entry.bucket) && Number.isFinite(entry.count) && entry.count > 0)
48
+ .sort((a, b) => a.bucket - b.bucket);
49
+ if (entries.length === 0) return null;
50
+ const total = entries.reduce((sum, entry) => sum + entry.count, 0);
51
+ if (total <= 0) return null;
52
+ const threshold = total * percentile;
53
+ let seen = 0;
54
+ for (const entry of entries) {
55
+ seen += entry.count;
56
+ if (seen >= threshold) return entry.bucket;
57
+ }
58
+ return entries[entries.length - 1]?.bucket ?? null;
59
+ }
60
+
61
+ function normalizeAttributes(value: unknown): Record<string, string> {
62
+ if (!isPlainObject(value)) return {};
63
+ const out: Record<string, string> = {};
64
+ for (const [key, raw] of Object.entries(value)) {
65
+ if (typeof raw === "string") out[key] = raw;
66
+ else if (typeof raw === "number" && Number.isFinite(raw)) out[key] = String(raw);
67
+ else if (typeof raw === "boolean") out[key] = raw ? "true" : "false";
68
+ else if (typeof raw === "bigint") out[key] = raw.toString();
69
+ }
70
+ return Object.fromEntries(Object.entries(out).sort((a, b) => a[0].localeCompare(b[0])));
71
+ }
72
+
73
+ function buildDimensionPairs(attributes: Record<string, string>): string[] {
74
+ return Object.entries(attributes)
75
+ .map(([key, value]) => `${key}=${value}`)
76
+ .sort((a, b) => a.localeCompare(b));
77
+ }
78
+
79
+ function buildSeriesKey(args: {
80
+ metricKind: string;
81
+ temporality: string;
82
+ metric: string;
83
+ unit: string;
84
+ stream: string | null;
85
+ instance: string | null;
86
+ dimensionKey: string | null;
87
+ }): string {
88
+ return [
89
+ args.metricKind,
90
+ args.temporality,
91
+ args.metric,
92
+ args.unit,
93
+ args.stream ?? "",
94
+ args.instance ?? "",
95
+ args.dimensionKey ?? "",
96
+ ].join("|");
97
+ }
98
+
99
+ function normalizeSummaryResult(value: Record<string, unknown>): Result<AggSummaryState, { message: string }> {
100
+ const summaryObject = isPlainObject(value.summary) ? value.summary : null;
101
+ const histogram =
102
+ normalizeHistogram(value.buckets) ??
103
+ normalizeHistogram(summaryObject?.histogram ?? undefined);
104
+ const count =
105
+ normalizeFiniteNumber(value.count) ??
106
+ normalizeFiniteNumber(summaryObject?.count);
107
+ const sum =
108
+ normalizeFiniteNumber(value.sum) ??
109
+ normalizeFiniteNumber(summaryObject?.sum);
110
+ const min =
111
+ normalizeFiniteNumber(value.min) ??
112
+ normalizeFiniteNumber(summaryObject?.min);
113
+ const max =
114
+ normalizeFiniteNumber(value.max) ??
115
+ normalizeFiniteNumber(summaryObject?.max);
116
+
117
+ if (count == null || count < 0) return Result.err({ message: "metrics interval requires count" });
118
+ if (sum == null) return Result.err({ message: "metrics interval requires sum" });
119
+
120
+ return Result.ok({
121
+ count,
122
+ sum,
123
+ min: count === 0 ? null : min ?? 0,
124
+ max: count === 0 ? null : max ?? 0,
125
+ histogram,
126
+ });
127
+ }
128
+
129
+ export function normalizeMetricsRecordResult(value: unknown): Result<NormalizedMetricsRecord, { message: string }> {
130
+ const objRes = expectPlainObjectResult(value, "metrics record");
131
+ if (Result.isError(objRes)) return objRes;
132
+ const input = objRes.value;
133
+
134
+ const kind = normalizeString(input.kind) ?? "interval";
135
+ if (kind !== "interval") return Result.err({ message: "metrics record.kind must be interval" });
136
+
137
+ const metric = normalizeString(input.metric);
138
+ if (!metric) return Result.err({ message: "metrics record.metric must be a non-empty string" });
139
+
140
+ const unit = normalizeString(input.unit);
141
+ if (!unit) return Result.err({ message: "metrics record.unit must be a non-empty string" });
142
+
143
+ const windowStart = normalizeInteger(input.windowStart);
144
+ const windowEnd = normalizeInteger(input.windowEnd);
145
+ if (windowStart == null || windowEnd == null) {
146
+ return Result.err({ message: "metrics record.windowStart and windowEnd must be integers" });
147
+ }
148
+ if (windowEnd < windowStart) return Result.err({ message: "metrics record.windowEnd must be >= windowStart" });
149
+
150
+ const intervalMs = normalizeInteger(input.intervalMs) ?? (windowEnd - windowStart);
151
+ if (intervalMs < 0) return Result.err({ message: "metrics record.intervalMs must be >= 0" });
152
+
153
+ const metricKind = normalizeString(input.metricKind) ?? "summary";
154
+ const temporality = normalizeString(input.temporality) ?? "delta";
155
+ const stream = normalizeString(input.stream);
156
+ const instance = normalizeString(input.instance);
157
+ const attributes = normalizeAttributes(input.attributes ?? input.tags);
158
+ const dimensionPairs = buildDimensionPairs(attributes);
159
+ const dimensionKey = dimensionPairs.length > 0 ? dimensionPairs.join("\u0000") : null;
160
+ const seriesKey = buildSeriesKey({
161
+ metricKind,
162
+ temporality,
163
+ metric,
164
+ unit,
165
+ stream,
166
+ instance,
167
+ dimensionKey,
168
+ });
169
+
170
+ const summaryRes = normalizeSummaryResult(input);
171
+ if (Result.isError(summaryRes)) return summaryRes;
172
+ const summary = summaryRes.value;
173
+ const avg = normalizeFiniteNumber(input.avg) ?? (summary.count > 0 ? summary.sum / summary.count : 0);
174
+ const p50 = normalizeFiniteNumber(input.p50) ?? histogramPercentile(summary.histogram, 0.5) ?? 0;
175
+ const p95 = normalizeFiniteNumber(input.p95) ?? histogramPercentile(summary.histogram, 0.95) ?? 0;
176
+ const p99 = normalizeFiniteNumber(input.p99) ?? histogramPercentile(summary.histogram, 0.99) ?? 0;
177
+
178
+ const normalizedValue: Record<string, unknown> = {
179
+ apiVersion: "durable.streams/metrics/v1",
180
+ kind: "interval",
181
+ metric,
182
+ unit,
183
+ metricKind,
184
+ temporality,
185
+ windowStart,
186
+ windowEnd,
187
+ intervalMs,
188
+ instance,
189
+ stream,
190
+ tags: attributes,
191
+ attributes,
192
+ dimensionPairs,
193
+ dimensionKey,
194
+ seriesKey,
195
+ count: summary.count,
196
+ sum: summary.sum,
197
+ min: summary.min,
198
+ max: summary.max,
199
+ avg,
200
+ p50,
201
+ p95,
202
+ p99,
203
+ buckets: summary.histogram ?? {},
204
+ summary: {
205
+ count: summary.count,
206
+ sum: summary.sum,
207
+ min: summary.min,
208
+ max: summary.max,
209
+ histogram: summary.histogram ?? {},
210
+ },
211
+ };
212
+
213
+ return Result.ok({
214
+ value: normalizedValue,
215
+ routingKey: seriesKey,
216
+ companion: {
217
+ metric,
218
+ unit,
219
+ metricKind,
220
+ temporality,
221
+ windowStartMs: windowStart,
222
+ windowEndMs: windowEnd,
223
+ intervalMs,
224
+ stream,
225
+ instance,
226
+ attributes,
227
+ dimensionPairs,
228
+ dimensionKey,
229
+ seriesKey,
230
+ summary,
231
+ },
232
+ });
233
+ }
234
+
235
+ export type MetricsBlockRecord = {
236
+ doc_id: number;
237
+ metric: string;
238
+ unit: string;
239
+ metricKind: string;
240
+ temporality: string;
241
+ windowStartMs: number;
242
+ windowEndMs: number;
243
+ intervalMs: number;
244
+ stream: string | null;
245
+ instance: string | null;
246
+ attributes: Record<string, string>;
247
+ dimensionPairs: string[];
248
+ dimensionKey: string | null;
249
+ seriesKey: string;
250
+ summary: AggSummaryState;
251
+ };
252
+
253
+ export function buildMetricsBlockRecord(docId: number, value: unknown): Result<MetricsBlockRecord, { message: string }> {
254
+ const normalizedRes = normalizeMetricsRecordResult(value);
255
+ if (Result.isError(normalizedRes)) return normalizedRes;
256
+ return Result.ok({
257
+ doc_id: docId,
258
+ ...normalizedRes.value.companion,
259
+ });
260
+ }
261
+
262
+ export function materializeMetricsBlockRecord(record: MetricsBlockRecord): Record<string, unknown> {
263
+ const histogram = record.summary.histogram ?? {};
264
+ const avg = record.summary.count > 0 ? record.summary.sum / record.summary.count : 0;
265
+ return {
266
+ apiVersion: "durable.streams/metrics/v1",
267
+ kind: "interval",
268
+ metric: record.metric,
269
+ unit: record.unit,
270
+ metricKind: record.metricKind,
271
+ temporality: record.temporality,
272
+ windowStart: record.windowStartMs,
273
+ windowEnd: record.windowEndMs,
274
+ intervalMs: record.intervalMs,
275
+ instance: record.instance,
276
+ stream: record.stream,
277
+ tags: { ...record.attributes },
278
+ attributes: { ...record.attributes },
279
+ dimensionPairs: [...record.dimensionPairs],
280
+ dimensionKey: record.dimensionKey,
281
+ seriesKey: record.seriesKey,
282
+ count: record.summary.count,
283
+ sum: record.summary.sum,
284
+ min: record.summary.min,
285
+ max: record.summary.max,
286
+ avg,
287
+ p50: histogramPercentile(histogram, 0.5) ?? 0,
288
+ p95: histogramPercentile(histogram, 0.95) ?? 0,
289
+ p99: histogramPercentile(histogram, 0.99) ?? 0,
290
+ buckets: { ...histogram },
291
+ summary: {
292
+ count: record.summary.count,
293
+ sum: record.summary.sum,
294
+ min: record.summary.min,
295
+ max: record.summary.max,
296
+ histogram: { ...histogram },
297
+ },
298
+ };
299
+ }
300
+
301
+ export function buildInternalMetricsRecord(args: {
302
+ metric: string;
303
+ unit: string;
304
+ windowStart: number;
305
+ windowEnd: number;
306
+ intervalMs: number;
307
+ instance: string;
308
+ stream?: string;
309
+ tags?: Record<string, PrimitiveAttribute>;
310
+ count: number;
311
+ sum: number;
312
+ min: number;
313
+ max: number;
314
+ avg: number;
315
+ p50: number;
316
+ p95: number;
317
+ p99: number;
318
+ buckets: Record<string, number>;
319
+ }): Record<string, unknown> {
320
+ const attributes = normalizeAttributes(args.tags);
321
+ const dimensionPairs = buildDimensionPairs(attributes);
322
+ const dimensionKey = dimensionPairs.length > 0 ? dimensionPairs.join("\u0000") : null;
323
+ const seriesKey = buildSeriesKey({
324
+ metricKind: "summary",
325
+ temporality: "delta",
326
+ metric: args.metric,
327
+ unit: args.unit,
328
+ stream: args.stream ?? null,
329
+ instance: args.instance,
330
+ dimensionKey,
331
+ });
332
+ return {
333
+ apiVersion: "durable.streams/metrics/v1",
334
+ kind: "interval",
335
+ metric: args.metric,
336
+ unit: args.unit,
337
+ metricKind: "summary",
338
+ temporality: "delta",
339
+ windowStart: args.windowStart,
340
+ windowEnd: args.windowEnd,
341
+ intervalMs: args.intervalMs,
342
+ instance: args.instance,
343
+ stream: args.stream ?? null,
344
+ tags: attributes,
345
+ attributes,
346
+ dimensionPairs,
347
+ dimensionKey,
348
+ seriesKey,
349
+ count: args.count,
350
+ sum: args.sum,
351
+ min: args.count === 0 ? null : args.min,
352
+ max: args.count === 0 ? null : args.max,
353
+ avg: args.avg,
354
+ p50: args.p50,
355
+ p95: args.p95,
356
+ p99: args.p99,
357
+ buckets: { ...args.buckets },
358
+ summary: {
359
+ count: args.count,
360
+ sum: args.sum,
361
+ min: args.count === 0 ? null : args.min,
362
+ max: args.count === 0 ? null : args.max,
363
+ histogram: { ...args.buckets },
364
+ },
365
+ };
366
+ }
@@ -0,0 +1,319 @@
1
+ import {
2
+ SCHEMA_REGISTRY_API_VERSION,
3
+ type SchemaRegistry,
4
+ type SearchConfig,
5
+ } from "../../schema/registry";
6
+
7
+ export const METRICS_CANONICAL_SCHEMA = {
8
+ type: "object",
9
+ additionalProperties: false,
10
+ properties: {
11
+ apiVersion: { type: "string", const: "durable.streams/metrics/v1" },
12
+ kind: { type: "string", const: "interval" },
13
+ metric: { type: "string" },
14
+ unit: { type: "string" },
15
+ metricKind: { type: "string" },
16
+ temporality: { type: "string" },
17
+ windowStart: { type: "integer" },
18
+ windowEnd: { type: "integer" },
19
+ intervalMs: { type: "integer" },
20
+ instance: { type: ["string", "null"] },
21
+ stream: { type: ["string", "null"] },
22
+ tags: {
23
+ type: "object",
24
+ additionalProperties: { type: "string" },
25
+ },
26
+ attributes: {
27
+ type: "object",
28
+ additionalProperties: { type: "string" },
29
+ },
30
+ dimensionPairs: {
31
+ type: "array",
32
+ items: { type: "string" },
33
+ },
34
+ dimensionKey: { type: ["string", "null"] },
35
+ seriesKey: { type: "string" },
36
+ count: { type: "number" },
37
+ sum: { type: "number" },
38
+ min: { type: ["number", "null"] },
39
+ max: { type: ["number", "null"] },
40
+ avg: { type: "number" },
41
+ p50: { type: "number" },
42
+ p95: { type: "number" },
43
+ p99: { type: "number" },
44
+ buckets: {
45
+ type: "object",
46
+ additionalProperties: { type: "number" },
47
+ },
48
+ summary: {
49
+ type: "object",
50
+ additionalProperties: false,
51
+ properties: {
52
+ count: { type: "number" },
53
+ sum: { type: "number" },
54
+ min: { type: ["number", "null"] },
55
+ max: { type: ["number", "null"] },
56
+ histogram: {
57
+ type: "object",
58
+ additionalProperties: { type: "number" },
59
+ },
60
+ },
61
+ required: ["count", "sum", "min", "max", "histogram"],
62
+ },
63
+ },
64
+ required: [
65
+ "apiVersion",
66
+ "kind",
67
+ "metric",
68
+ "unit",
69
+ "metricKind",
70
+ "temporality",
71
+ "windowStart",
72
+ "windowEnd",
73
+ "intervalMs",
74
+ "instance",
75
+ "stream",
76
+ "tags",
77
+ "attributes",
78
+ "dimensionPairs",
79
+ "dimensionKey",
80
+ "seriesKey",
81
+ "count",
82
+ "sum",
83
+ "min",
84
+ "max",
85
+ "avg",
86
+ "p50",
87
+ "p95",
88
+ "p99",
89
+ "buckets",
90
+ "summary",
91
+ ],
92
+ } as const;
93
+
94
+ export const METRICS_DEFAULT_SEARCH_CONFIG: SearchConfig = {
95
+ profile: "metrics",
96
+ primaryTimestampField: "windowStart",
97
+ aliases: {
98
+ ts: "windowStart",
99
+ time: "windowStart",
100
+ name: "metric",
101
+ dims: "dimensionKey",
102
+ series: "seriesKey",
103
+ },
104
+ defaultFields: [
105
+ { field: "metric", boost: 2 },
106
+ { field: "stream", boost: 1.25 },
107
+ { field: "dimensionPairs", boost: 1 },
108
+ ],
109
+ fields: {
110
+ windowStart: {
111
+ kind: "date",
112
+ bindings: [{ version: 1, jsonPointer: "/windowStart" }],
113
+ exact: true,
114
+ column: true,
115
+ exists: true,
116
+ sortable: true,
117
+ aggregatable: true,
118
+ },
119
+ windowEnd: {
120
+ kind: "date",
121
+ bindings: [{ version: 1, jsonPointer: "/windowEnd" }],
122
+ exact: true,
123
+ column: true,
124
+ exists: true,
125
+ sortable: true,
126
+ },
127
+ intervalMs: {
128
+ kind: "integer",
129
+ bindings: [{ version: 1, jsonPointer: "/intervalMs" }],
130
+ exact: true,
131
+ column: true,
132
+ exists: true,
133
+ sortable: true,
134
+ aggregatable: true,
135
+ },
136
+ metric: {
137
+ kind: "keyword",
138
+ bindings: [{ version: 1, jsonPointer: "/metric" }],
139
+ normalizer: "lowercase_v1",
140
+ exact: true,
141
+ prefix: true,
142
+ exists: true,
143
+ sortable: true,
144
+ aggregatable: true,
145
+ },
146
+ unit: {
147
+ kind: "keyword",
148
+ bindings: [{ version: 1, jsonPointer: "/unit" }],
149
+ normalizer: "lowercase_v1",
150
+ exact: true,
151
+ prefix: true,
152
+ exists: true,
153
+ aggregatable: true,
154
+ },
155
+ metricKind: {
156
+ kind: "keyword",
157
+ bindings: [{ version: 1, jsonPointer: "/metricKind" }],
158
+ normalizer: "lowercase_v1",
159
+ exact: true,
160
+ prefix: true,
161
+ exists: true,
162
+ aggregatable: true,
163
+ },
164
+ temporality: {
165
+ kind: "keyword",
166
+ bindings: [{ version: 1, jsonPointer: "/temporality" }],
167
+ normalizer: "lowercase_v1",
168
+ exact: true,
169
+ prefix: true,
170
+ exists: true,
171
+ aggregatable: true,
172
+ },
173
+ stream: {
174
+ kind: "keyword",
175
+ bindings: [{ version: 1, jsonPointer: "/stream" }],
176
+ normalizer: "lowercase_v1",
177
+ exact: true,
178
+ prefix: true,
179
+ exists: true,
180
+ sortable: true,
181
+ aggregatable: true,
182
+ },
183
+ instance: {
184
+ kind: "keyword",
185
+ bindings: [{ version: 1, jsonPointer: "/instance" }],
186
+ exact: true,
187
+ prefix: true,
188
+ exists: true,
189
+ sortable: true,
190
+ aggregatable: true,
191
+ },
192
+ seriesKey: {
193
+ kind: "keyword",
194
+ bindings: [{ version: 1, jsonPointer: "/seriesKey" }],
195
+ exact: true,
196
+ exists: true,
197
+ sortable: true,
198
+ },
199
+ dimensionKey: {
200
+ kind: "keyword",
201
+ bindings: [{ version: 1, jsonPointer: "/dimensionKey" }],
202
+ exact: true,
203
+ prefix: true,
204
+ exists: true,
205
+ sortable: true,
206
+ aggregatable: true,
207
+ },
208
+ dimensionPairs: {
209
+ kind: "keyword",
210
+ bindings: [{ version: 1, jsonPointer: "/dimensionPairs" }],
211
+ exact: true,
212
+ prefix: true,
213
+ exists: true,
214
+ },
215
+ count: {
216
+ kind: "integer",
217
+ bindings: [{ version: 1, jsonPointer: "/count" }],
218
+ exact: true,
219
+ column: true,
220
+ exists: true,
221
+ sortable: true,
222
+ aggregatable: true,
223
+ },
224
+ sum: {
225
+ kind: "float",
226
+ bindings: [{ version: 1, jsonPointer: "/sum" }],
227
+ exact: true,
228
+ column: true,
229
+ exists: true,
230
+ sortable: true,
231
+ aggregatable: true,
232
+ },
233
+ min: {
234
+ kind: "float",
235
+ bindings: [{ version: 1, jsonPointer: "/min" }],
236
+ exact: true,
237
+ column: true,
238
+ exists: true,
239
+ sortable: true,
240
+ },
241
+ max: {
242
+ kind: "float",
243
+ bindings: [{ version: 1, jsonPointer: "/max" }],
244
+ exact: true,
245
+ column: true,
246
+ exists: true,
247
+ sortable: true,
248
+ },
249
+ avg: {
250
+ kind: "float",
251
+ bindings: [{ version: 1, jsonPointer: "/avg" }],
252
+ exact: true,
253
+ column: true,
254
+ exists: true,
255
+ sortable: true,
256
+ },
257
+ p95: {
258
+ kind: "float",
259
+ bindings: [{ version: 1, jsonPointer: "/p95" }],
260
+ exact: true,
261
+ column: true,
262
+ exists: true,
263
+ sortable: true,
264
+ },
265
+ p99: {
266
+ kind: "float",
267
+ bindings: [{ version: 1, jsonPointer: "/p99" }],
268
+ exact: true,
269
+ column: true,
270
+ exists: true,
271
+ sortable: true,
272
+ },
273
+ },
274
+ rollups: {
275
+ metrics: {
276
+ timestampField: "windowStart",
277
+ dimensions: ["metric", "unit", "stream", "instance", "dimensionKey", "metricKind", "temporality"],
278
+ intervals: ["10s", "1m", "5m", "1h"],
279
+ measures: {
280
+ value: {
281
+ kind: "summary_parts",
282
+ countJsonPointer: "/count",
283
+ sumJsonPointer: "/sum",
284
+ minJsonPointer: "/min",
285
+ maxJsonPointer: "/max",
286
+ histogramJsonPointer: "/buckets",
287
+ },
288
+ },
289
+ },
290
+ },
291
+ };
292
+
293
+ export function buildMetricsDefaultRegistry(stream: string): SchemaRegistry {
294
+ return {
295
+ apiVersion: SCHEMA_REGISTRY_API_VERSION,
296
+ schema: stream,
297
+ currentVersion: 1,
298
+ routingKey: { jsonPointer: "/seriesKey", required: true },
299
+ search: structuredClone(METRICS_DEFAULT_SEARCH_CONFIG),
300
+ boundaries: [{ offset: 0, version: 1 }],
301
+ schemas: {
302
+ "1": structuredClone(METRICS_CANONICAL_SCHEMA),
303
+ },
304
+ lenses: {},
305
+ };
306
+ }
307
+
308
+ export function buildInternalMetricsRegistry(stream: string): SchemaRegistry {
309
+ return {
310
+ apiVersion: SCHEMA_REGISTRY_API_VERSION,
311
+ schema: stream,
312
+ currentVersion: 1,
313
+ boundaries: [{ offset: 0, version: 1 }],
314
+ schemas: {
315
+ "1": structuredClone(METRICS_CANONICAL_SCHEMA),
316
+ },
317
+ lenses: {},
318
+ };
319
+ }