@noego/telemetry 0.0.5 → 0.1.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 (79) hide show
  1. package/README.md +6 -0
  2. package/dist/browser/index.d.cts +6 -5
  3. package/dist/browser/index.d.ts +6 -5
  4. package/dist/{browser_telemetry_client-BSAMZ8Qb.d.cts → browser_telemetry_client-ClZl3awO.d.cts} +2 -2
  5. package/dist/{browser_telemetry_client-DPBU6NGw.d.ts → browser_telemetry_client-t4dPp8Ku.d.ts} +2 -2
  6. package/dist/cli/telemetry_bin.cjs +393 -35
  7. package/dist/cli/telemetry_bin.cjs.map +1 -1
  8. package/dist/cli/telemetry_bin.js +378 -20
  9. package/dist/cli/telemetry_bin.js.map +1 -1
  10. package/dist/cli/telemetry_cli.cjs +393 -35
  11. package/dist/cli/telemetry_cli.cjs.map +1 -1
  12. package/dist/cli/telemetry_cli.d.cts +35 -5
  13. package/dist/cli/telemetry_cli.d.ts +35 -5
  14. package/dist/cli/telemetry_cli.js +378 -20
  15. package/dist/cli/telemetry_cli.js.map +1 -1
  16. package/dist/{client_defaults-ShVYQYmn.d.ts → client_defaults-CZ8PysZX.d.ts} +2 -2
  17. package/dist/{client_defaults-Dk0MMzvx.d.cts → client_defaults-lDbWYWWK.d.cts} +2 -2
  18. package/dist/core/index.cjs +5 -1
  19. package/dist/core/index.cjs.map +1 -1
  20. package/dist/core/index.d.cts +15 -12
  21. package/dist/core/index.d.ts +15 -12
  22. package/dist/core/index.js +5 -1
  23. package/dist/core/index.js.map +1 -1
  24. package/dist/electron/index.d.cts +5 -4
  25. package/dist/electron/index.d.ts +5 -4
  26. package/dist/{envelope.types-rOLpWXRC.d.cts → envelope.types-DIvZD3xH.d.ts} +2 -6
  27. package/dist/{envelope.types-rOLpWXRC.d.ts → envelope.types-DyrlRG49.d.cts} +2 -6
  28. package/dist/{in_memory_batch_transport-DKW9EZnU.d.cts → in_memory_batch_transport-3T6gElXP.d.cts} +1 -1
  29. package/dist/{in_memory_batch_transport-oOwNxXnA.d.ts → in_memory_batch_transport-DpQU6JqM.d.ts} +1 -1
  30. package/dist/index.cjs +5 -1
  31. package/dist/index.cjs.map +1 -1
  32. package/dist/index.d.cts +14 -12
  33. package/dist/index.d.ts +14 -12
  34. package/dist/index.js +5 -1
  35. package/dist/index.js.map +1 -1
  36. package/dist/json.types-BqS77fcN.d.cts +7 -0
  37. package/dist/json.types-BqS77fcN.d.ts +7 -0
  38. package/dist/{maintenance_coordinator.types-C4ULi4A4.d.cts → maintenance_coordinator.types-Crhx9i9k.d.cts} +3 -3
  39. package/dist/{maintenance_coordinator.types-ikUrRXmO.d.ts → maintenance_coordinator.types-DprmgpVn.d.ts} +3 -3
  40. package/dist/metric.types-D8aOAnUm.d.ts +67 -0
  41. package/dist/metric.types-Dg4eZq6u.d.cts +67 -0
  42. package/dist/{metric_definition_contract-BYtnODhu.d.ts → metric_definition_contract-C6MERhJs.d.ts} +1 -1
  43. package/dist/{metric_definition_contract-DfhhmTZ1.d.cts → metric_definition_contract-DoAk7NOw.d.cts} +1 -1
  44. package/dist/node/index.cjs +5 -1
  45. package/dist/node/index.cjs.map +1 -1
  46. package/dist/node/index.d.cts +11 -9
  47. package/dist/node/index.d.ts +11 -9
  48. package/dist/node/index.js +5 -1
  49. package/dist/node/index.js.map +1 -1
  50. package/dist/node/producer/index.d.cts +5 -4
  51. package/dist/node/producer/index.d.ts +5 -4
  52. package/dist/projector/index.cjs +1370 -0
  53. package/dist/projector/index.cjs.map +1 -0
  54. package/dist/projector/index.d.cts +246 -0
  55. package/dist/projector/index.d.ts +246 -0
  56. package/dist/projector/index.js +1328 -0
  57. package/dist/projector/index.js.map +1 -0
  58. package/dist/{query.types-DU5v7vM0.d.ts → query.types-CRqAsGTX.d.cts} +4 -66
  59. package/dist/{query.types-DuYWbFFe.d.cts → query.types-DnOMYXpC.d.ts} +4 -66
  60. package/dist/sqlite/index.cjs +5 -1
  61. package/dist/sqlite/index.cjs.map +1 -1
  62. package/dist/sqlite/index.d.cts +7 -5
  63. package/dist/sqlite/index.d.ts +7 -5
  64. package/dist/sqlite/index.js +5 -1
  65. package/dist/sqlite/index.js.map +1 -1
  66. package/dist/{storage.types-k7oElEB3.d.ts → storage.types-BKWoIOHp.d.ts} +3 -2
  67. package/dist/{storage.types-Bji_pWqr.d.cts → storage.types-DHbAm3j_.d.cts} +3 -2
  68. package/dist/{telemetry_client-BO5Pjp41.d.cts → telemetry_client-Bg5tbj5E.d.cts} +3 -2
  69. package/dist/{telemetry_client-D8uZ-mTL.d.ts → telemetry_client-DWKgufmc.d.ts} +3 -2
  70. package/dist/{telemetry_configuration_parser-BZ8Nvnle.d.ts → telemetry_configuration_parser-6BR3_dD_.d.ts} +2 -2
  71. package/dist/{telemetry_configuration_parser-CgSN7R6A.d.cts → telemetry_configuration_parser-ybeweUPE.d.cts} +2 -2
  72. package/dist/testing/index.d.cts +7 -5
  73. package/dist/testing/index.d.ts +7 -5
  74. package/dist/trace-adapter/index.d.cts +4 -3
  75. package/dist/trace-adapter/index.d.ts +4 -3
  76. package/dist/{transport.types-BAl_TRtU.d.cts → transport.types-C2dP-eed.d.cts} +1 -1
  77. package/dist/{transport.types-DDnZoKWi.d.ts → transport.types-SA8KpLiK.d.ts} +1 -1
  78. package/native/noego_telemetry_native.node +0 -0
  79. package/package.json +11 -4
@@ -0,0 +1,1370 @@
1
+ /* @noego/telemetry */
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __export = (target, all) => {
10
+ for (var name in all)
11
+ __defProp(target, name, { get: all[name], enumerable: true });
12
+ };
13
+ var __copyProps = (to, from, except, desc) => {
14
+ if (from && typeof from === "object" || typeof from === "function") {
15
+ for (let key of __getOwnPropNames(from))
16
+ if (!__hasOwnProp.call(to, key) && key !== except)
17
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
18
+ }
19
+ return to;
20
+ };
21
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
22
+ // If the importer is in node compatibility mode or this is not an ESM
23
+ // file that has been converted to a CommonJS file using a Babel-
24
+ // compatible transform (i.e. "__esModule" has not been set), then set
25
+ // "default" to the CommonJS "module.exports" for node compatibility.
26
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
27
+ mod
28
+ ));
29
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
30
+
31
+ // src/projector/index.ts
32
+ var projector_exports = {};
33
+ __export(projector_exports, {
34
+ TraceProjector: () => TraceProjector,
35
+ createTraceProjectorProcess: () => createTraceProjectorProcess,
36
+ detectSourceReset: () => detectSourceReset,
37
+ eventSelectorMatches: () => eventSelectorMatches,
38
+ metricDefinitionToTraceProjection: () => metricDefinitionToTraceProjection,
39
+ metricDefinitionsToTraceProjections: () => metricDefinitionsToTraceProjections,
40
+ validateTraceProjection: () => validateTraceProjection
41
+ });
42
+ module.exports = __toCommonJS(projector_exports);
43
+
44
+ // src/projector/trace_projection.types.ts
45
+ var NAME_PATTERN = /^[a-z][a-z0-9_.-]*$/;
46
+ function validateTraceProjection(projection) {
47
+ if (!NAME_PATTERN.test(projection.name)) {
48
+ throw new Error(`TraceProjection: invalid name '${projection.name}'`);
49
+ }
50
+ if (!Number.isSafeInteger(projection.version) || projection.version < 1) {
51
+ throw new Error(`TraceProjection '${projection.name}': version must be a positive integer`);
52
+ }
53
+ if (typeof projection.event !== "string" || projection.event.trim().length === 0) {
54
+ throw new Error(`TraceProjection '${projection.name}': event selector is required`);
55
+ }
56
+ if (projection.resolutions.length === 0) {
57
+ throw new Error(`TraceProjection '${projection.name}': at least one resolution is required`);
58
+ }
59
+ for (const resolution of projection.resolutions) {
60
+ if (!Number.isSafeInteger(resolution) || resolution < 1) {
61
+ throw new Error(`TraceProjection '${projection.name}': resolutions must be positive integers`);
62
+ }
63
+ }
64
+ for (const measurement of projection.measurements) {
65
+ if (typeof measurement.field !== "string" || measurement.field.trim().length === 0) {
66
+ throw new Error(`TraceProjection '${projection.name}': measurement field is required`);
67
+ }
68
+ if (measurement.aggregates.length === 0) {
69
+ throw new Error(
70
+ `TraceProjection '${projection.name}': measurement '${measurement.field}' needs at least one aggregate`
71
+ );
72
+ }
73
+ }
74
+ const dimensions = projection.groupBy ?? [];
75
+ if (new Set(dimensions).size !== dimensions.length) {
76
+ throw new Error(`TraceProjection '${projection.name}': duplicate groupBy dimension`);
77
+ }
78
+ }
79
+
80
+ // src/projector/trace_projector.ts
81
+ var SCAN_PROGRESS_KEY = "__scan__";
82
+ var OVERFLOW_DIMENSIONS = Object.freeze({ __overflow__: "other" });
83
+ var TraceProjector = class {
84
+ source;
85
+ db;
86
+ projections;
87
+ pageSize;
88
+ now;
89
+ constructor(options) {
90
+ this.source = options.source;
91
+ this.db = options.destination;
92
+ this.projections = options.projections;
93
+ this.pageSize = Math.max(1, options.pageSize ?? 500);
94
+ this.now = options.now ?? Date.now;
95
+ for (const projection of this.projections) {
96
+ validateTraceProjection(projection);
97
+ }
98
+ this.initializeSchema();
99
+ this.registerDefinitions();
100
+ }
101
+ /**
102
+ * Drains all unprojected raw traces, page by page, committing progress per
103
+ * page.
104
+ *
105
+ * The durable cursor is a source rowid, so it is only meaningful against
106
+ * the store file it was taken from. When the source describes itself, a
107
+ * cursor whose epoch differs from the store's — or that lies beyond the
108
+ * store's newest row — belongs to a store that has since been recreated
109
+ * (boot reset, manual deletion) and is discarded, restarting the scan at
110
+ * row 0. 2026-09-03: the live projector sat at rowid 22.6M over a rebuilt
111
+ * store holding rows 1–440k, reported "caught up" every minute, and wrote
112
+ * no buckets for ten hours.
113
+ */
114
+ runOnce() {
115
+ const description = this.source.describe?.() ?? null;
116
+ const progress = this.loadProgress();
117
+ const sourceReset = detectSourceReset(progress, description);
118
+ let cursor = sourceReset === null ? progress : null;
119
+ const result = {
120
+ pages: 0,
121
+ eventsRead: 0,
122
+ selectorMatches: 0,
123
+ bucketsUpdated: 0,
124
+ failures: 0,
125
+ progress: "caught-up-no-new-source",
126
+ cursorRowId: cursor?.rowId ?? null,
127
+ sourceHighRowId: description?.highRowId ?? null,
128
+ sourceEpoch: description?.storeEpoch ?? null,
129
+ sourceReset
130
+ };
131
+ for (; ; ) {
132
+ const page = this.source.readAfter(cursor, { limit: this.pageSize });
133
+ if (page.events.length === 0) break;
134
+ const apply = this.db.transaction(() => {
135
+ for (const event of page.events) {
136
+ let matched = false;
137
+ for (const projection of this.projections) {
138
+ if (!this.matches(projection, event)) continue;
139
+ matched = true;
140
+ const outcome = this.applyEvent(projection, event);
141
+ result.bucketsUpdated += outcome.bucketsUpdated;
142
+ result.failures += outcome.failures;
143
+ }
144
+ if (matched) result.selectorMatches += 1;
145
+ }
146
+ this.saveProgress(page.cursor, description?.storeEpoch ?? progress?.storeEpoch ?? null);
147
+ });
148
+ apply();
149
+ cursor = page.cursor;
150
+ result.pages += 1;
151
+ result.eventsRead += page.events.length;
152
+ }
153
+ if (sourceReset !== null && result.pages === 0) {
154
+ this.saveProgress({ rowId: 0 }, description?.storeEpoch ?? null);
155
+ }
156
+ result.cursorRowId = cursor?.rowId ?? (sourceReset !== null ? 0 : null);
157
+ result.progress = classifyProgress(result);
158
+ return result;
159
+ }
160
+ /** Newest completed bucket start per projection at the given resolution — the freshness signal. */
161
+ newestBucketStartMs(resolutionMs) {
162
+ const row = this.db.prepare(
163
+ "SELECT MAX(bucket_start_ms) AS newest FROM trace_metric_buckets WHERE resolution_ms = ?"
164
+ ).get(resolutionMs);
165
+ return row.newest;
166
+ }
167
+ /**
168
+ * Rebuilds one projection's derived buckets for an occurrence-time range
169
+ * from retained raw traces (spec 04 §14–15): clears coverage for the range,
170
+ * then reprojects every retained matching event. Uses the same extraction
171
+ * code as forward projection; does not touch the forward scan cursor.
172
+ */
173
+ backfill(options) {
174
+ const projection = this.projections.find(
175
+ (candidate) => candidate.name === options.projection && candidate.version === options.version
176
+ );
177
+ if (!projection) {
178
+ throw new Error(`TraceProjector: unknown projection ${options.projection}@${options.version}`);
179
+ }
180
+ const result = { pages: 0, eventsRead: 0, bucketsUpdated: 0, failures: 0 };
181
+ const run = this.db.transaction(() => {
182
+ for (const resolution of projection.resolutions) {
183
+ const firstBucket = bucketStart(options.fromOccurredAtMs, resolution);
184
+ this.db.prepare(`
185
+ DELETE FROM trace_metric_buckets
186
+ WHERE projection_name = ? AND projection_version = ? AND resolution_ms = ?
187
+ AND bucket_start_ms >= ? AND bucket_start_ms <= ?
188
+ `).run(projection.name, projection.version, resolution, firstBucket, options.toOccurredAtMs);
189
+ }
190
+ let cursor = null;
191
+ for (; ; ) {
192
+ const page = this.source.readAfter(cursor, {
193
+ limit: this.pageSize,
194
+ toOccurredAtMs: options.toOccurredAtMs
195
+ });
196
+ if (page.events.length === 0) break;
197
+ for (const event of page.events) {
198
+ if (event.occurredAtMs < options.fromOccurredAtMs) continue;
199
+ if (!this.matches(projection, event)) continue;
200
+ const outcome = this.applyEvent(projection, event);
201
+ result.bucketsUpdated += outcome.bucketsUpdated;
202
+ result.failures += outcome.failures;
203
+ result.eventsRead += 1;
204
+ }
205
+ cursor = page.cursor;
206
+ result.pages += 1;
207
+ }
208
+ });
209
+ run();
210
+ return result;
211
+ }
212
+ /** Historical bucket query with explicit projection/version/resolution. */
213
+ query(query) {
214
+ const clauses = [
215
+ "projection_name = @projection",
216
+ "projection_version = @version",
217
+ "resolution_ms = @resolutionMs"
218
+ ];
219
+ const params = {
220
+ projection: query.projection,
221
+ version: query.version,
222
+ resolutionMs: query.resolutionMs
223
+ };
224
+ if (query.fromMs !== void 0) {
225
+ clauses.push("bucket_start_ms >= @fromMs");
226
+ params.fromMs = query.fromMs;
227
+ }
228
+ if (query.toMs !== void 0) {
229
+ clauses.push("bucket_start_ms <= @toMs");
230
+ params.toMs = query.toMs;
231
+ }
232
+ if (query.dimensions !== void 0) {
233
+ clauses.push("dimension_key = @dimensionKey");
234
+ params.dimensionKey = dimensionKey(query.dimensions);
235
+ }
236
+ const rows = this.db.prepare(`
237
+ SELECT * FROM trace_metric_buckets
238
+ WHERE ${clauses.join(" AND ")}
239
+ ORDER BY bucket_start_ms, dimension_key
240
+ `).all(params);
241
+ return rows.map((row) => {
242
+ const values = JSON.parse(row.values_json);
243
+ const averages = {};
244
+ for (const [field, state] of Object.entries(values)) {
245
+ if (state.count > 0) averages[field] = state.sum / state.count;
246
+ }
247
+ return {
248
+ projection: row.projection_name,
249
+ version: row.projection_version,
250
+ resolutionMs: row.resolution_ms,
251
+ bucketStartMs: row.bucket_start_ms,
252
+ dimensions: JSON.parse(row.dimensions_json),
253
+ count: row.count,
254
+ values,
255
+ averages
256
+ };
257
+ });
258
+ }
259
+ /** Bounded projection failures, newest first. */
260
+ failures(limit = 100) {
261
+ const rows = this.db.prepare(`
262
+ SELECT projection_name, projection_version, trace_id, reason
263
+ FROM trace_projection_failures ORDER BY recorded_at_ms DESC LIMIT ?
264
+ `).all(Math.max(1, limit));
265
+ return rows.map((row) => ({
266
+ projection: row.projection_name,
267
+ version: row.projection_version,
268
+ traceId: row.trace_id,
269
+ reason: row.reason
270
+ }));
271
+ }
272
+ matches(projection, event) {
273
+ if (!eventSelectorMatches(projection.event, event.event)) return false;
274
+ if (projection.source !== void 0 && event.source !== projection.source) return false;
275
+ return true;
276
+ }
277
+ applyEvent(projection, event) {
278
+ const context = event.context ?? {};
279
+ let dimensions = {};
280
+ for (const field of projection.groupBy ?? []) {
281
+ const value = context[field];
282
+ dimensions[field] = value === void 0 || value === null ? "" : String(value);
283
+ }
284
+ const measured = {};
285
+ for (const measurement of projection.measurements) {
286
+ const value = context[measurement.field];
287
+ if (typeof value !== "number" || !Number.isFinite(value)) {
288
+ if ((projection.missingField ?? "record-failure") === "skip") {
289
+ continue;
290
+ }
291
+ this.recordFailure(projection, event, `measurement field '${measurement.field}' is not a finite number`);
292
+ return { bucketsUpdated: 0, failures: 1 };
293
+ }
294
+ measured[measurement.field] = value;
295
+ }
296
+ if (!this.admitDimensions(projection, dimensions)) {
297
+ if ((projection.cardinality?.overflow ?? "reject") === "other") {
298
+ dimensions = { ...OVERFLOW_DIMENSIONS };
299
+ } else {
300
+ this.recordFailure(projection, event, "dimension cardinality limit exceeded");
301
+ return { bucketsUpdated: 0, failures: 1 };
302
+ }
303
+ }
304
+ let bucketsUpdated = 0;
305
+ for (const resolution of projection.resolutions) {
306
+ this.updateBucket(projection, resolution, bucketStart(event.occurredAtMs, resolution), dimensions, measured);
307
+ bucketsUpdated += 1;
308
+ }
309
+ return { bucketsUpdated, failures: 0 };
310
+ }
311
+ admitDimensions(projection, dimensions) {
312
+ const limit = projection.cardinality?.maxDimensionKeys;
313
+ if (limit === void 0) return true;
314
+ const key = dimensionKey(dimensions);
315
+ const known = this.db.prepare(`
316
+ SELECT 1 FROM trace_metric_buckets
317
+ WHERE projection_name = ? AND projection_version = ? AND dimension_key = ?
318
+ LIMIT 1
319
+ `).get(projection.name, projection.version, key);
320
+ if (known) return true;
321
+ const row = this.db.prepare(`
322
+ SELECT COUNT(DISTINCT dimension_key) AS n FROM trace_metric_buckets
323
+ WHERE projection_name = ? AND projection_version = ?
324
+ `).get(projection.name, projection.version);
325
+ return row.n < limit;
326
+ }
327
+ updateBucket(projection, resolutionMs, bucketStartMs, dimensions, measured) {
328
+ const key = dimensionKey(dimensions);
329
+ const existing = this.db.prepare(`
330
+ SELECT count, values_json FROM trace_metric_buckets
331
+ WHERE projection_name = ? AND projection_version = ? AND resolution_ms = ?
332
+ AND bucket_start_ms = ? AND dimension_key = ?
333
+ `).get(projection.name, projection.version, resolutionMs, bucketStartMs, key);
334
+ const values = existing ? JSON.parse(existing.values_json) : {};
335
+ for (const [field, value] of Object.entries(measured)) {
336
+ const state = values[field] ?? {
337
+ count: 0,
338
+ sum: 0,
339
+ min: Number.POSITIVE_INFINITY,
340
+ max: Number.NEGATIVE_INFINITY,
341
+ last: Number.NaN
342
+ };
343
+ state.count += 1;
344
+ state.sum += value;
345
+ state.min = Math.min(state.min, value);
346
+ state.max = Math.max(state.max, value);
347
+ state.last = value;
348
+ values[field] = state;
349
+ }
350
+ this.db.prepare(`
351
+ INSERT INTO trace_metric_buckets (
352
+ projection_name, projection_version, resolution_ms, bucket_start_ms,
353
+ dimension_key, dimensions_json, count, values_json, updated_at_ms
354
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
355
+ ON CONFLICT (projection_name, projection_version, resolution_ms, bucket_start_ms, dimension_key)
356
+ DO UPDATE SET count = excluded.count, values_json = excluded.values_json, updated_at_ms = excluded.updated_at_ms
357
+ `).run(
358
+ projection.name,
359
+ projection.version,
360
+ resolutionMs,
361
+ bucketStartMs,
362
+ key,
363
+ JSON.stringify(dimensions),
364
+ (existing?.count ?? 0) + 1,
365
+ JSON.stringify(values),
366
+ this.now()
367
+ );
368
+ }
369
+ recordFailure(projection, event, reason) {
370
+ this.db.prepare(`
371
+ INSERT INTO trace_projection_failures (
372
+ projection_name, projection_version, trace_id, reason, occurred_at_ms, recorded_at_ms
373
+ ) VALUES (?, ?, ?, ?, ?, ?)
374
+ `).run(projection.name, projection.version, event.id, reason, event.occurredAtMs, this.now());
375
+ }
376
+ loadProgress() {
377
+ const row = this.db.prepare(
378
+ "SELECT cursor_json FROM trace_projection_progress WHERE progress_key = ?"
379
+ ).get(SCAN_PROGRESS_KEY);
380
+ if (!row) return null;
381
+ const parsed = JSON.parse(row.cursor_json);
382
+ return { rowId: parsed.rowId, storeEpoch: parsed.storeEpoch ?? null };
383
+ }
384
+ saveProgress(cursor, storeEpoch) {
385
+ if (cursor === null) return;
386
+ const progress = { rowId: cursor.rowId, storeEpoch };
387
+ this.db.prepare(`
388
+ INSERT INTO trace_projection_progress (progress_key, cursor_json, updated_at_ms)
389
+ VALUES (?, ?, ?)
390
+ ON CONFLICT (progress_key) DO UPDATE SET cursor_json = excluded.cursor_json, updated_at_ms = excluded.updated_at_ms
391
+ `).run(SCAN_PROGRESS_KEY, JSON.stringify(progress), this.now());
392
+ }
393
+ registerDefinitions() {
394
+ for (const projection of this.projections) {
395
+ const definitionJson = JSON.stringify(projection);
396
+ const existing = this.db.prepare(`
397
+ SELECT definition_json FROM trace_projection_definitions
398
+ WHERE projection_name = ? AND projection_version = ?
399
+ `).get(projection.name, projection.version);
400
+ if (existing === void 0) {
401
+ this.db.prepare(`
402
+ INSERT INTO trace_projection_definitions (projection_name, projection_version, definition_json, registered_at_ms)
403
+ VALUES (?, ?, ?, ?)
404
+ `).run(projection.name, projection.version, definitionJson, this.now());
405
+ } else if (existing.definition_json !== definitionJson) {
406
+ throw new Error(
407
+ `TraceProjector: projection ${projection.name}@${projection.version} differs from its registered definition; bump the version`
408
+ );
409
+ }
410
+ }
411
+ }
412
+ initializeSchema() {
413
+ this.db.pragma("journal_mode = WAL");
414
+ this.db.exec(`
415
+ CREATE TABLE IF NOT EXISTS trace_projection_definitions (
416
+ projection_name TEXT NOT NULL,
417
+ projection_version INTEGER NOT NULL,
418
+ definition_json TEXT NOT NULL,
419
+ registered_at_ms INTEGER NOT NULL,
420
+ PRIMARY KEY (projection_name, projection_version)
421
+ );
422
+
423
+ CREATE TABLE IF NOT EXISTS trace_projection_progress (
424
+ progress_key TEXT NOT NULL PRIMARY KEY,
425
+ cursor_json TEXT NOT NULL,
426
+ updated_at_ms INTEGER NOT NULL
427
+ );
428
+
429
+ CREATE TABLE IF NOT EXISTS trace_metric_buckets (
430
+ projection_name TEXT NOT NULL,
431
+ projection_version INTEGER NOT NULL,
432
+ resolution_ms INTEGER NOT NULL,
433
+ bucket_start_ms INTEGER NOT NULL,
434
+ dimension_key TEXT NOT NULL,
435
+ dimensions_json TEXT NOT NULL,
436
+ count INTEGER NOT NULL,
437
+ values_json TEXT NOT NULL,
438
+ updated_at_ms INTEGER NOT NULL,
439
+ PRIMARY KEY (projection_name, projection_version, resolution_ms, bucket_start_ms, dimension_key)
440
+ );
441
+
442
+ CREATE INDEX IF NOT EXISTS trace_metric_buckets_time_idx
443
+ ON trace_metric_buckets (projection_name, projection_version, resolution_ms, bucket_start_ms);
444
+
445
+ CREATE TABLE IF NOT EXISTS trace_projection_failures (
446
+ projection_name TEXT NOT NULL,
447
+ projection_version INTEGER NOT NULL,
448
+ trace_id TEXT NOT NULL,
449
+ reason TEXT NOT NULL,
450
+ occurred_at_ms INTEGER NOT NULL,
451
+ recorded_at_ms INTEGER NOT NULL
452
+ );
453
+ `);
454
+ }
455
+ };
456
+ function eventSelectorMatches(selector, eventName) {
457
+ if (selector === eventName) return true;
458
+ if (!selector.includes("*")) return false;
459
+ const selectorParts = selector.split(".");
460
+ const eventParts = eventName.split(".");
461
+ for (let i = 0; i < selectorParts.length; i++) {
462
+ const part = selectorParts[i];
463
+ if (part === "**") {
464
+ return i === selectorParts.length - 1;
465
+ }
466
+ if (i >= eventParts.length) return false;
467
+ if (part !== "*" && part !== eventParts[i]) return false;
468
+ }
469
+ return selectorParts.length === eventParts.length;
470
+ }
471
+ function bucketStart(occurredAtMs, resolutionMs) {
472
+ return occurredAtMs - occurredAtMs % resolutionMs;
473
+ }
474
+ function detectSourceReset(progress, description) {
475
+ if (progress === null || description === null) return null;
476
+ if (progress.storeEpoch !== null && progress.storeEpoch !== description.storeEpoch) {
477
+ return "store-epoch-changed";
478
+ }
479
+ if (progress.rowId > description.highRowId) return "cursor-beyond-source";
480
+ return null;
481
+ }
482
+ function classifyProgress(result) {
483
+ if (result.bucketsUpdated > 0) return "buckets-updated";
484
+ if (result.eventsRead === 0) return "caught-up-no-new-source";
485
+ if (result.selectorMatches === 0) return "new-source-no-selector-match";
486
+ return "matching-source-no-bucket";
487
+ }
488
+ function dimensionKey(dimensions) {
489
+ const sorted = Object.keys(dimensions).sort().map((key) => [key, dimensions[key]]);
490
+ return JSON.stringify(sorted);
491
+ }
492
+
493
+ // src/projector/trace_projector_process.ts
494
+ var import_node_fs2 = require("fs");
495
+ var import_node_path2 = __toESM(require("path"), 1);
496
+ var import_better_sqlite3 = __toESM(require("better-sqlite3"), 1);
497
+ var import_sqlite = require("@noego/trace/sqlite");
498
+
499
+ // src/platform/node/telemetry_configuration_parser.ts
500
+ var import_node_path = require("path");
501
+ var import_node_fs = require("fs");
502
+ var import_js_yaml = require("js-yaml");
503
+
504
+ // src/core/metric_definition_contract.ts
505
+ var MetricDefinitionContract = class {
506
+ normalize(definition) {
507
+ const parsedSelector = this.parseSelector(definition.selector);
508
+ if (!parsedSelector.ok) {
509
+ return parsedSelector;
510
+ }
511
+ const bracketGroupBy = parsedSelector.value.groupBy;
512
+ const configuredGroupBy = definition.groupBy;
513
+ const bothSpecifyDifferentGrouping = bracketGroupBy.length > 0 && configuredGroupBy.length > 0 && bracketGroupBy.join(".") !== configuredGroupBy.join(".");
514
+ if (bothSpecifyDifferentGrouping) {
515
+ return { ok: false, error: `Metric ${definition.name} has conflicting selector and groupBy declarations` };
516
+ }
517
+ const groupBy = configuredGroupBy.length > 0 ? configuredGroupBy : bracketGroupBy;
518
+ const basicError = this.validateBasic(definition, parsedSelector.value.selector, groupBy);
519
+ if (basicError !== null) {
520
+ return { ok: false, error: basicError };
521
+ }
522
+ if (parsedSelector.value.wildcardUngrouped && definition.measurements.some((measurement) => measurement.kind !== "count")) {
523
+ return {
524
+ ok: false,
525
+ error: `Metric ${definition.name} uses [*] and may only declare count`
526
+ };
527
+ }
528
+ return {
529
+ ok: true,
530
+ value: Object.freeze({
531
+ ...definition,
532
+ selector: parsedSelector.value.selector,
533
+ groupBy: Object.freeze([...groupBy]),
534
+ resolutionsMs: Object.freeze([...definition.resolutionsMs].sort((left, right) => left - right)),
535
+ measurements: Object.freeze([...definition.measurements]),
536
+ targetIds: Object.freeze([...definition.targetIds]),
537
+ cardinality: Object.freeze({ ...definition.cardinality })
538
+ })
539
+ };
540
+ }
541
+ matches(selector, label) {
542
+ const selectorSegments = selector.split(".");
543
+ const labelSegments = label.split(".");
544
+ return this.matchSegments(selectorSegments, labelSegments, 0, 0);
545
+ }
546
+ parseSelector(selector) {
547
+ if (selector.startsWith("[")) {
548
+ return { ok: false, error: "A metric selector cannot omit its event label or glob" };
549
+ }
550
+ const bracketStart = selector.lastIndexOf("[");
551
+ if (bracketStart < 0) {
552
+ return { ok: true, value: { selector, groupBy: [], wildcardUngrouped: false } };
553
+ }
554
+ if (!selector.endsWith("]")) {
555
+ return { ok: false, error: `Invalid metric selector ${selector}` };
556
+ }
557
+ const eventSelector = selector.slice(0, bracketStart);
558
+ const bracket = selector.slice(bracketStart + 1, -1).trim();
559
+ if (!eventSelector || !bracket) {
560
+ return { ok: false, error: `Invalid metric selector ${selector}` };
561
+ }
562
+ const groupBy = bracket === "*" ? [] : bracket.split(",").map((field) => field.trim());
563
+ if (groupBy.some((field) => field.length === 0)) {
564
+ return { ok: false, error: `Invalid metric selector ${selector}` };
565
+ }
566
+ return {
567
+ ok: true,
568
+ value: {
569
+ selector: eventSelector,
570
+ groupBy,
571
+ wildcardUngrouped: bracket === "*"
572
+ }
573
+ };
574
+ }
575
+ validateBasic(definition, selector, groupBy) {
576
+ if (!/^[A-Za-z0-9_*][A-Za-z0-9_.:*\-]*$/.test(selector)) {
577
+ return `Metric ${definition.name} has an invalid selector`;
578
+ }
579
+ if (!/^[A-Za-z][A-Za-z0-9_]*$/.test(definition.name)) {
580
+ return `Metric name ${definition.name} is invalid`;
581
+ }
582
+ if (!Number.isInteger(definition.version) || definition.version <= 0) {
583
+ return `Metric ${definition.name} must have a positive integer version`;
584
+ }
585
+ if (definition.measurements.length === 0) {
586
+ return `Metric ${definition.name} must declare at least one measurement`;
587
+ }
588
+ const invalidResolution = definition.resolutionsMs.some(
589
+ (resolution) => !Number.isInteger(resolution) || resolution <= 0
590
+ );
591
+ const duplicateResolution = new Set(definition.resolutionsMs).size !== definition.resolutionsMs.length;
592
+ if (invalidResolution || duplicateResolution || definition.resolutionsMs.length === 0) {
593
+ return `Metric ${definition.name} has invalid or duplicate resolutions`;
594
+ }
595
+ const missingCardinality = groupBy.find((field) => definition.cardinality[field] === void 0);
596
+ if (missingCardinality) {
597
+ return `Metric ${definition.name} is missing cardinality policy for ${missingCardinality}`;
598
+ }
599
+ const invalidCardinality = Object.values(definition.cardinality).some(
600
+ (rule) => !Number.isInteger(rule.maxValuesPerBucket) || rule.maxValuesPerBucket <= 0
601
+ );
602
+ if (invalidCardinality) {
603
+ return `Metric ${definition.name} has an invalid cardinality limit`;
604
+ }
605
+ return null;
606
+ }
607
+ matchSegments(selector, label, selectorIndex, labelIndex) {
608
+ if (selectorIndex === selector.length) {
609
+ return labelIndex === label.length;
610
+ }
611
+ const segment = selector[selectorIndex];
612
+ if (segment === "**") {
613
+ if (selectorIndex === selector.length - 1) {
614
+ return true;
615
+ }
616
+ for (let next = labelIndex; next <= label.length; next += 1) {
617
+ if (this.matchSegments(selector, label, selectorIndex + 1, next)) {
618
+ return true;
619
+ }
620
+ }
621
+ return false;
622
+ }
623
+ if (labelIndex >= label.length) {
624
+ return false;
625
+ }
626
+ const segmentMatches = segment === "*" || segment === label[labelIndex];
627
+ return segmentMatches && this.matchSegments(selector, label, selectorIndex + 1, labelIndex + 1);
628
+ }
629
+ };
630
+
631
+ // src/core/helper/canonical_json.ts
632
+ var UTF8_ENCODER = new TextEncoder();
633
+ function compareCanonicalKeys(left, right) {
634
+ if (left === right) return 0;
635
+ if (/^[\x00-\x7f]*$/.test(left) && /^[\x00-\x7f]*$/.test(right)) {
636
+ return left < right ? -1 : 1;
637
+ }
638
+ const leftBytes = UTF8_ENCODER.encode(left);
639
+ const rightBytes = UTF8_ENCODER.encode(right);
640
+ const length = Math.min(leftBytes.length, rightBytes.length);
641
+ for (let index = 0; index < length; index += 1) {
642
+ const difference = (leftBytes[index] ?? 0) - (rightBytes[index] ?? 0);
643
+ if (difference !== 0) return difference;
644
+ }
645
+ return leftBytes.length - rightBytes.length;
646
+ }
647
+ function canonicalJson(value) {
648
+ if (value === null || typeof value === "string" || typeof value === "boolean") {
649
+ return JSON.stringify(value);
650
+ }
651
+ if (typeof value === "number") {
652
+ if (!Number.isFinite(value)) {
653
+ throw new Error("Canonical telemetry JSON cannot encode a non-finite number");
654
+ }
655
+ return JSON.stringify(value);
656
+ }
657
+ if (Array.isArray(value)) {
658
+ const items = value.map((item) => canonicalJson(item));
659
+ return `[${items.join(",")}]`;
660
+ }
661
+ if (typeof value !== "object") {
662
+ throw new Error(`Canonical telemetry JSON cannot encode ${typeof value}`);
663
+ }
664
+ const entries = Object.entries(value).sort(([left], [right]) => compareCanonicalKeys(left, right));
665
+ const properties = entries.map(([key, item]) => `${JSON.stringify(key)}:${canonicalJson(item)}`);
666
+ return `{${properties.join(",")}}`;
667
+ }
668
+ function telemetryChecksum(value) {
669
+ const bytes = new TextEncoder().encode(canonicalJson(value));
670
+ let hash = 0xcbf29ce484222325n;
671
+ const prime = 0x100000001b3n;
672
+ for (const byte of bytes) {
673
+ hash ^= BigInt(byte);
674
+ hash = BigInt.asUintN(64, hash * prime);
675
+ }
676
+ return hash.toString(16).padStart(16, "0");
677
+ }
678
+
679
+ // src/storage/helper/sqlite_table_names.ts
680
+ var SQLITE_LOGICAL_TABLE_NAMES = [
681
+ "rawEvents",
682
+ "batchAcknowledgements",
683
+ "ingestWatermarks",
684
+ "metricDefinitions",
685
+ "metricSamples",
686
+ "metricRollups",
687
+ "rollupCompletions",
688
+ "extractionFailures",
689
+ "maintenanceRuns",
690
+ "readerLeases",
691
+ "committedDropCounters",
692
+ "migrationHistory"
693
+ ];
694
+ var SQLITE_DEFAULT_TABLE_NAMES = Object.freeze({
695
+ rawEvents: "raw_events",
696
+ batchAcknowledgements: "batch_acknowledgements",
697
+ ingestWatermarks: "ingest_watermarks",
698
+ metricDefinitions: "metric_definitions",
699
+ metricSamples: "metric_samples",
700
+ metricRollups: "metric_rollups",
701
+ rollupCompletions: "rollup_completions",
702
+ extractionFailures: "extraction_failures",
703
+ maintenanceRuns: "maintenance_runs",
704
+ readerLeases: "reader_leases",
705
+ committedDropCounters: "committed_drop_counters",
706
+ migrationHistory: "migration_history"
707
+ });
708
+ var SQLITE_SCHEMA_MANIFEST_TABLE = "telemetry_schema_manifest";
709
+
710
+ // src/storage/telemetry_table_map.ts
711
+ var TelemetryTableMap = class _TelemetryTableMap {
712
+ constructor(mapping, checksum) {
713
+ this.mapping = mapping;
714
+ this.checksum = checksum;
715
+ this.tokenMap = Object.freeze({
716
+ ...mapping,
717
+ rawEventsOccurredIndex: this.indexName(mapping.rawEvents, "occurred"),
718
+ rawEventsConversationIndex: this.indexName(mapping.rawEvents, "conversation"),
719
+ rawEventsLabelIndex: this.indexName(mapping.rawEvents, "label"),
720
+ rawEventsLevelIndex: this.indexName(mapping.rawEvents, "level"),
721
+ metricSamplesTimeIndex: this.indexName(mapping.metricSamples, "time"),
722
+ metricSamplesEventIndex: this.indexName(mapping.metricSamples, "event"),
723
+ metricRollupsTimeIndex: this.indexName(mapping.metricRollups, "time")
724
+ });
725
+ }
726
+ mapping;
727
+ checksum;
728
+ tokenMap;
729
+ static create(configuration) {
730
+ const prefixError = _TelemetryTableMap.validateOptionalPrefix(configuration.tablePrefix);
731
+ if (prefixError !== null) {
732
+ return { ok: false, error: prefixError };
733
+ }
734
+ const entries = SQLITE_LOGICAL_TABLE_NAMES.map((logicalName) => {
735
+ const override = configuration.tables[logicalName];
736
+ const defaultName = SQLITE_DEFAULT_TABLE_NAMES[logicalName];
737
+ const physicalName = override ?? (configuration.tablePrefix === null ? defaultName : `${configuration.tablePrefix}${defaultName}`);
738
+ return [logicalName, physicalName];
739
+ });
740
+ for (const [logicalName, physicalName] of entries) {
741
+ const identifierError = _TelemetryTableMap.validateIdentifier(physicalName);
742
+ if (identifierError !== null) {
743
+ return { ok: false, error: `${logicalName}: ${identifierError}` };
744
+ }
745
+ if (physicalName.toLowerCase() === SQLITE_SCHEMA_MANIFEST_TABLE) {
746
+ return { ok: false, error: `${logicalName} cannot use the fixed schema manifest name` };
747
+ }
748
+ }
749
+ const foldedNames = entries.map(([, physicalName]) => physicalName.toLowerCase());
750
+ if (new Set(foldedNames).size !== foldedNames.length) {
751
+ return { ok: false, error: "Resolved telemetry table names must be unique after case folding" };
752
+ }
753
+ const mapping = Object.freeze(Object.fromEntries(entries));
754
+ const checksum = telemetryChecksum(mapping);
755
+ return { ok: true, value: new _TelemetryTableMap(mapping, checksum) };
756
+ }
757
+ physical(logicalName) {
758
+ return this.mapping[logicalName];
759
+ }
760
+ quoted(logicalName) {
761
+ return this.quote(this.mapping[logicalName]);
762
+ }
763
+ manifestMapping() {
764
+ return this.mapping;
765
+ }
766
+ substitute(sql) {
767
+ const unknownTokens = [];
768
+ const substituted = sql.replace(/\{\{([A-Za-z][A-Za-z0-9]*)\}\}/g, (_match, token) => {
769
+ const identifier = this.tokenMap[token];
770
+ if (identifier === void 0) {
771
+ unknownTokens.push(token);
772
+ return `{{${token}}}`;
773
+ }
774
+ return this.quote(identifier);
775
+ });
776
+ if (unknownTokens.length > 0 || substituted.includes("{{")) {
777
+ return {
778
+ ok: false,
779
+ error: `Unknown telemetry migration identifier token(s): ${unknownTokens.join(", ") || "malformed token"}`
780
+ };
781
+ }
782
+ return { ok: true, value: substituted };
783
+ }
784
+ quote(identifier) {
785
+ return `"${identifier.replaceAll('"', '""')}"`;
786
+ }
787
+ indexName(tableName, purpose) {
788
+ const raw = `${tableName}_${purpose}_idx`;
789
+ if (raw.length <= 63) {
790
+ return raw;
791
+ }
792
+ const suffix = telemetryChecksum(raw).slice(0, 8);
793
+ return `${raw.slice(0, 54)}_${suffix}`;
794
+ }
795
+ static validateOptionalPrefix(prefix) {
796
+ if (prefix === null) {
797
+ return null;
798
+ }
799
+ return this.validateIdentifier(prefix);
800
+ }
801
+ static validateIdentifier(identifier) {
802
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(identifier)) {
803
+ return `Invalid SQLite identifier ${identifier}`;
804
+ }
805
+ if (identifier.length > 63) {
806
+ return `SQLite identifier ${identifier} exceeds 63 characters`;
807
+ }
808
+ if (identifier.toLowerCase().startsWith("sqlite_")) {
809
+ return `SQLite identifier ${identifier} uses the reserved sqlite_ prefix`;
810
+ }
811
+ return null;
812
+ }
813
+ };
814
+
815
+ // src/platform/node/helper/telemetry_duration_parser.ts
816
+ var TelemetryDurationParser = class _TelemetryDurationParser {
817
+ static MULTIPLIERS = Object.freeze({
818
+ ms: 1,
819
+ s: 1e3,
820
+ m: 6e4,
821
+ h: 60 * 6e4,
822
+ d: 24 * 60 * 6e4,
823
+ w: 7 * 24 * 60 * 6e4,
824
+ y: 365 * 24 * 60 * 6e4
825
+ });
826
+ parse(value, field, allowZero = false) {
827
+ const match = /^(0|[1-9][0-9]*)(ms|s|m|h|d|w|y)$/.exec(value);
828
+ if (!match) {
829
+ throw new Error(`${field} must be an integer duration using ms, s, m, h, d, w, or y`);
830
+ }
831
+ const amount = Number(match[1]);
832
+ const unit = match[2];
833
+ const milliseconds = amount * _TelemetryDurationParser.MULTIPLIERS[unit];
834
+ if (!Number.isSafeInteger(milliseconds) || !allowZero && milliseconds <= 0) {
835
+ throw new Error(`${field} is outside the supported duration range`);
836
+ }
837
+ return milliseconds;
838
+ }
839
+ };
840
+
841
+ // src/platform/node/schema/telemetry_configuration_schema.ts
842
+ var import_zod = require("zod");
843
+ var DurationSchema = import_zod.z.string().min(1);
844
+ var NonEmptyStringSchema = import_zod.z.string().trim().min(1);
845
+ var StableIdSchema = import_zod.z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/);
846
+ var CardinalityRuleSchema = import_zod.z.object({
847
+ maxValuesPerBucket: import_zod.z.number().int().positive(),
848
+ overflow: import_zod.z.enum(["other", "drop", "reject"])
849
+ }).strict();
850
+ var MetricMeasurementSchema = import_zod.z.union([
851
+ import_zod.z.literal("count"),
852
+ import_zod.z.object({
853
+ field: import_zod.z.string().min(1),
854
+ aggregate: import_zod.z.array(import_zod.z.enum(["sum", "min", "max", "last"])).min(1)
855
+ }).strict()
856
+ ]);
857
+ var MetricDefinitionSchema = import_zod.z.object({
858
+ name: import_zod.z.string().min(1),
859
+ version: import_zod.z.number().int().positive().optional(),
860
+ selector: import_zod.z.string().min(1),
861
+ groupBy: import_zod.z.array(import_zod.z.string().min(1)).optional(),
862
+ measurements: import_zod.z.array(MetricMeasurementSchema).min(1),
863
+ cardinality: import_zod.z.record(import_zod.z.string(), CardinalityRuleSchema).optional(),
864
+ targets: import_zod.z.array(StableIdSchema).min(1).optional(),
865
+ resolutions: import_zod.z.array(DurationSchema).min(1).optional(),
866
+ missingField: import_zod.z.enum(["skip", "record-failure"]).optional()
867
+ }).strict();
868
+ var SqliteTablesSchema = import_zod.z.object({
869
+ rawEvents: NonEmptyStringSchema.optional(),
870
+ batchAcknowledgements: NonEmptyStringSchema.optional(),
871
+ ingestWatermarks: NonEmptyStringSchema.optional(),
872
+ metricDefinitions: NonEmptyStringSchema.optional(),
873
+ metricSamples: NonEmptyStringSchema.optional(),
874
+ metricRollups: NonEmptyStringSchema.optional(),
875
+ rollupCompletions: NonEmptyStringSchema.optional(),
876
+ extractionFailures: NonEmptyStringSchema.optional(),
877
+ maintenanceRuns: NonEmptyStringSchema.optional(),
878
+ readerLeases: NonEmptyStringSchema.optional(),
879
+ committedDropCounters: NonEmptyStringSchema.optional(),
880
+ migrationHistory: NonEmptyStringSchema.optional()
881
+ }).strict();
882
+ var SqliteTargetSchema = import_zod.z.object({
883
+ id: StableIdSchema,
884
+ transport: import_zod.z.literal("sqlite"),
885
+ delivery: import_zod.z.enum(["required", "best-effort"]),
886
+ path: import_zod.z.string().min(1),
887
+ schema: import_zod.z.object({
888
+ tablePrefix: NonEmptyStringSchema.nullable().optional(),
889
+ tables: SqliteTablesSchema.optional()
890
+ }).strict().optional(),
891
+ retention: import_zod.z.object({
892
+ raw: DurationSchema,
893
+ lateArrival: DurationSchema.optional(),
894
+ samples: DurationSchema.optional(),
895
+ levels: import_zod.z.record(import_zod.z.string().min(1), DurationSchema).optional(),
896
+ rollups: import_zod.z.array(
897
+ import_zod.z.object({
898
+ resolution: DurationSchema,
899
+ retain: DurationSchema
900
+ }).strict()
901
+ ).min(1)
902
+ }).strict(),
903
+ maintenance: import_zod.z.object({
904
+ interval: DurationSchema,
905
+ maxWorkMs: import_zod.z.number().int().positive(),
906
+ reclaim: import_zod.z.literal("incremental"),
907
+ maxReclaimPages: import_zod.z.number().int().positive().optional()
908
+ }).strict(),
909
+ leaseExpiry: DurationSchema.optional(),
910
+ query: import_zod.z.object({
911
+ maxRange: DurationSchema.optional(),
912
+ maxRows: import_zod.z.number().int().positive().optional(),
913
+ maxBuckets: import_zod.z.number().int().positive().optional(),
914
+ maxResponseBytes: import_zod.z.number().int().positive().optional()
915
+ }).strict().optional()
916
+ }).strict();
917
+ var StorageSchema = import_zod.z.object({
918
+ defaultTarget: StableIdSchema.nullable().optional(),
919
+ targets: import_zod.z.array(SqliteTargetSchema).min(1)
920
+ }).strict();
921
+ var TraceSchema = import_zod.z.object({
922
+ ingestionTransport: import_zod.z.string().min(1).optional(),
923
+ storage: StorageSchema.optional()
924
+ }).strict();
925
+ var TelemetryConfigurationDocumentSchema = import_zod.z.object({
926
+ telemetry: import_zod.z.object({
927
+ version: import_zod.z.literal(1).optional(),
928
+ role: import_zod.z.enum(["producer", "collector", "combined"]),
929
+ trace: TraceSchema,
930
+ metrics: import_zod.z.array(MetricDefinitionSchema).optional(),
931
+ logs: import_zod.z.object({
932
+ transport: import_zod.z.literal("console")
933
+ }).strict().optional()
934
+ }).strict()
935
+ }).strict();
936
+
937
+ // src/platform/node/telemetry_configuration_parser.ts
938
+ var DEFAULT_LEASE_EXPIRY = "30s";
939
+ var DEFAULT_QUERY_RANGE = "365d";
940
+ var DEFAULT_QUERY_ROWS = 1e4;
941
+ var DEFAULT_QUERY_BUCKETS = 1e4;
942
+ var DEFAULT_QUERY_BYTES = 5e6;
943
+ var DEFAULT_RECLAIM_PAGES = 100;
944
+ var TelemetryConfigurationError = class extends Error {
945
+ constructor(message, source) {
946
+ super(`${source}: ${message}`);
947
+ this.source = source;
948
+ this.name = "TelemetryConfigurationError";
949
+ }
950
+ source;
951
+ };
952
+ var NodeTelemetryConfigurationParser = class {
953
+ constructor(durations = new TelemetryDurationParser(), metricContract = new MetricDefinitionContract()) {
954
+ this.durations = durations;
955
+ this.metricContract = metricContract;
956
+ }
957
+ durations;
958
+ metricContract;
959
+ parseFile(configurationPath, options = {}) {
960
+ const absolutePath = (0, import_node_path.resolve)(configurationPath);
961
+ let source;
962
+ try {
963
+ source = (0, import_node_fs.readFileSync)(absolutePath, "utf8");
964
+ } catch (cause) {
965
+ throw new TelemetryConfigurationError(this.errorMessage(cause), absolutePath);
966
+ }
967
+ return this.parseYaml(source, {
968
+ sourceName: options.sourceName ?? absolutePath,
969
+ storageBaseDirectory: options.storageBaseDirectory ?? (0, import_node_path.dirname)(absolutePath)
970
+ });
971
+ }
972
+ parseYaml(source, options = {}) {
973
+ const sourceName = options.sourceName ?? "<telemetry-configuration>";
974
+ let document;
975
+ try {
976
+ document = (0, import_js_yaml.load)(source);
977
+ } catch (cause) {
978
+ throw new TelemetryConfigurationError(this.errorMessage(cause), sourceName);
979
+ }
980
+ const parsed = TelemetryConfigurationDocumentSchema.safeParse(document);
981
+ if (!parsed.success) {
982
+ throw new TelemetryConfigurationError(parsed.error.message, sourceName);
983
+ }
984
+ try {
985
+ const baseDirectory = options.storageBaseDirectory === void 0 ? (0, import_node_path.dirname)((0, import_node_path.resolve)(sourceName)) : (0, import_node_path.resolve)(options.storageBaseDirectory);
986
+ return this.resolveDocument(parsed.data, baseDirectory);
987
+ } catch (cause) {
988
+ if (cause instanceof TelemetryConfigurationError) {
989
+ throw cause;
990
+ }
991
+ throw new TelemetryConfigurationError(this.errorMessage(cause), sourceName);
992
+ }
993
+ }
994
+ resolveDocument(document, baseDirectory) {
995
+ const telemetry = document.telemetry;
996
+ this.validateRole(telemetry.role, telemetry.trace);
997
+ const serializedTargets = telemetry.trace.storage?.targets ?? [];
998
+ const targets = serializedTargets.map(
999
+ (target, index) => this.resolveTarget(target, baseDirectory, index)
1000
+ );
1001
+ this.validateTargets(telemetry.role, targets, telemetry.trace.storage?.defaultTarget ?? null);
1002
+ const metrics = (telemetry.metrics ?? []).map(
1003
+ (metric, index) => this.resolveMetric(metric, targets, index)
1004
+ );
1005
+ this.validateMetricIdentities(metrics);
1006
+ return Object.freeze({
1007
+ version: telemetry.version ?? 1,
1008
+ role: telemetry.role,
1009
+ ingestionTransport: telemetry.trace.ingestionTransport ?? null,
1010
+ logTransport: telemetry.logs?.transport ?? null,
1011
+ defaultTargetId: telemetry.trace.storage?.defaultTarget ?? null,
1012
+ sqliteTargets: Object.freeze(targets),
1013
+ metrics: Object.freeze(metrics)
1014
+ });
1015
+ }
1016
+ validateRole(role, trace) {
1017
+ const hasIngestion = trace.ingestionTransport !== void 0;
1018
+ const hasStorage = trace.storage !== void 0;
1019
+ if (role === "producer" && (!hasIngestion || hasStorage)) {
1020
+ throw new Error("Producer telemetry requires ingestionTransport and forbids storage");
1021
+ }
1022
+ if (role === "collector" && !hasStorage) {
1023
+ throw new Error("Collector telemetry requires storage targets");
1024
+ }
1025
+ if (role === "combined" && (!hasIngestion || !hasStorage)) {
1026
+ throw new Error("Combined telemetry requires ingestionTransport and storage targets");
1027
+ }
1028
+ }
1029
+ resolveTarget(target, baseDirectory, index) {
1030
+ const field = `telemetry.trace.storage.targets[${index}]`;
1031
+ const rawMs = this.durations.parse(target.retention.raw, `${field}.retention.raw`);
1032
+ const lateArrivalMs = this.durations.parse(
1033
+ target.retention.lateArrival ?? "0ms",
1034
+ `${field}.retention.lateArrival`,
1035
+ true
1036
+ );
1037
+ const samplesMs = target.retention.samples === void 0 ? rawMs + lateArrivalMs : this.durations.parse(target.retention.samples, `${field}.retention.samples`);
1038
+ const levelMs = this.resolveLevelRetention(target.retention.levels, field);
1039
+ const rollups = target.retention.rollups.map(
1040
+ (tier, tierIndex) => Object.freeze({
1041
+ resolutionMs: this.durations.parse(
1042
+ tier.resolution,
1043
+ `${field}.retention.rollups[${tierIndex}].resolution`
1044
+ ),
1045
+ retainMs: this.durations.parse(
1046
+ tier.retain,
1047
+ `${field}.retention.rollups[${tierIndex}].retain`
1048
+ )
1049
+ })
1050
+ );
1051
+ const tables = Object.freeze(
1052
+ Object.fromEntries(
1053
+ Object.entries(target.schema?.tables ?? {}).filter((entry) => entry[1] !== void 0)
1054
+ )
1055
+ );
1056
+ const schema = Object.freeze({
1057
+ tablePrefix: target.schema?.tablePrefix ?? null,
1058
+ tables
1059
+ });
1060
+ const tableMap = TelemetryTableMap.create(schema);
1061
+ if (!tableMap.ok) {
1062
+ throw new Error(`${field}.schema: ${tableMap.error}`);
1063
+ }
1064
+ const resolved = Object.freeze({
1065
+ targetId: target.id,
1066
+ path: (0, import_node_path.resolve)(baseDirectory, target.path),
1067
+ delivery: target.delivery,
1068
+ schema,
1069
+ retention: Object.freeze({
1070
+ rawMs,
1071
+ lateArrivalMs,
1072
+ samplesMs,
1073
+ levelMs,
1074
+ rollups: Object.freeze(rollups)
1075
+ }),
1076
+ maintenance: Object.freeze({
1077
+ intervalMs: this.durations.parse(
1078
+ target.maintenance.interval,
1079
+ `${field}.maintenance.interval`
1080
+ ),
1081
+ maxWorkMs: target.maintenance.maxWorkMs,
1082
+ maxReclaimPages: target.maintenance.maxReclaimPages ?? DEFAULT_RECLAIM_PAGES
1083
+ }),
1084
+ leaseExpiryMs: this.durations.parse(
1085
+ target.leaseExpiry ?? DEFAULT_LEASE_EXPIRY,
1086
+ `${field}.leaseExpiry`
1087
+ ),
1088
+ query: Object.freeze({
1089
+ maxRangeMs: this.durations.parse(
1090
+ target.query?.maxRange ?? DEFAULT_QUERY_RANGE,
1091
+ `${field}.query.maxRange`
1092
+ ),
1093
+ maxRows: target.query?.maxRows ?? DEFAULT_QUERY_ROWS,
1094
+ maxBuckets: target.query?.maxBuckets ?? DEFAULT_QUERY_BUCKETS,
1095
+ maxResponseBytes: target.query?.maxResponseBytes ?? DEFAULT_QUERY_BYTES
1096
+ })
1097
+ });
1098
+ this.validateRetention(resolved, field);
1099
+ if (resolved.leaseExpiryMs < 1e3) {
1100
+ throw new Error(`${field}.leaseExpiry must be at least 1s`);
1101
+ }
1102
+ return resolved;
1103
+ }
1104
+ resolveLevelRetention(levels, field) {
1105
+ if (levels === void 0) {
1106
+ return Object.freeze({});
1107
+ }
1108
+ const resolved = {};
1109
+ for (const [level, duration] of Object.entries(levels)) {
1110
+ const normalized = level.trim().toLowerCase();
1111
+ if (normalized.length === 0) {
1112
+ throw new Error(`${field}.retention.levels contains an empty level name`);
1113
+ }
1114
+ if (resolved[normalized] !== void 0) {
1115
+ throw new Error(`${field}.retention.levels declares "${normalized}" more than once`);
1116
+ }
1117
+ resolved[normalized] = this.durations.parse(
1118
+ duration,
1119
+ `${field}.retention.levels.${normalized}`
1120
+ );
1121
+ }
1122
+ return Object.freeze(resolved);
1123
+ }
1124
+ validateRetention(target, field) {
1125
+ const retention = target.retention;
1126
+ const requiredSamplesMs = retention.rawMs + retention.lateArrivalMs;
1127
+ if (!Number.isSafeInteger(requiredSamplesMs) || retention.samplesMs < requiredSamplesMs) {
1128
+ throw new Error(`${field}.retention.samples must cover raw plus lateArrival`);
1129
+ }
1130
+ for (let index = 0; index < retention.rollups.length; index += 1) {
1131
+ const tier = retention.rollups[index];
1132
+ const previous = retention.rollups[index - 1];
1133
+ if (!tier || tier.retainMs < retention.rawMs) {
1134
+ throw new Error(`${field}.retention rollup retention cannot be shorter than raw retention`);
1135
+ }
1136
+ if (previous && (tier.resolutionMs <= previous.resolutionMs || tier.retainMs <= previous.retainMs)) {
1137
+ throw new Error(
1138
+ `${field}.retention rollup tiers must increase by resolution and retention`
1139
+ );
1140
+ }
1141
+ }
1142
+ }
1143
+ validateTargets(role, targets, defaultTargetId) {
1144
+ if (role === "producer") {
1145
+ return;
1146
+ }
1147
+ const targetIds = targets.map((target) => target.targetId);
1148
+ if (new Set(targetIds).size !== targetIds.length) {
1149
+ throw new Error("Telemetry storage target ids must be unique");
1150
+ }
1151
+ if (!targets.some((target) => target.delivery === "required")) {
1152
+ throw new Error("Telemetry storage requires at least one required durable target");
1153
+ }
1154
+ if (defaultTargetId !== null && !targetIds.includes(defaultTargetId)) {
1155
+ throw new Error(`Unknown default telemetry target ${defaultTargetId}`);
1156
+ }
1157
+ }
1158
+ resolveMetric(metric, targets, index) {
1159
+ if (metric.selector.endsWith("[*]") && (metric.groupBy?.length ?? 0) > 0) {
1160
+ throw new Error(`telemetry.metrics[${index}] selector [*] cannot declare groupBy`);
1161
+ }
1162
+ const targetIds = metric.targets ?? targets.map((target) => target.targetId);
1163
+ if (new Set(targetIds).size !== targetIds.length) {
1164
+ throw new Error(`telemetry.metrics[${index}] has duplicate target ids`);
1165
+ }
1166
+ const unknownTarget = targetIds.find(
1167
+ (targetId) => !targets.some((target) => target.targetId === targetId)
1168
+ );
1169
+ if (unknownTarget !== void 0) {
1170
+ throw new Error(`telemetry.metrics[${index}] references unknown target ${unknownTarget}`);
1171
+ }
1172
+ const resolutionsMs = metric.resolutions?.map(
1173
+ (resolution, resolutionIndex) => this.durations.parse(
1174
+ resolution,
1175
+ `telemetry.metrics[${index}].resolutions[${resolutionIndex}]`
1176
+ )
1177
+ ) ?? this.metricResolutions(targets, targetIds);
1178
+ this.validateMetricResolutions(resolutionsMs, targets, targetIds, index);
1179
+ const cardinality = Object.freeze(
1180
+ Object.fromEntries(
1181
+ Object.entries(metric.cardinality ?? {}).map(([field, rule]) => [
1182
+ field,
1183
+ Object.freeze({ ...rule })
1184
+ ])
1185
+ )
1186
+ );
1187
+ const definition = {
1188
+ name: metric.name,
1189
+ version: metric.version ?? 1,
1190
+ selector: metric.selector,
1191
+ groupBy: Object.freeze([...metric.groupBy ?? []]),
1192
+ measurements: Object.freeze(
1193
+ metric.measurements.map((measurement) => this.resolveMeasurement(measurement))
1194
+ ),
1195
+ cardinality,
1196
+ targetIds: Object.freeze([...targetIds]),
1197
+ resolutionsMs: Object.freeze(resolutionsMs),
1198
+ missingField: metric.missingField ?? "record-failure"
1199
+ };
1200
+ const normalized = this.metricContract.normalize(definition);
1201
+ if (!normalized.ok) {
1202
+ throw new Error(`telemetry.metrics[${index}]: ${normalized.error}`);
1203
+ }
1204
+ this.validateNormalizedMetric(normalized.value, index);
1205
+ return normalized.value;
1206
+ }
1207
+ validateNormalizedMetric(definition, index) {
1208
+ const segments = definition.selector.split(".");
1209
+ const invalidSelector = segments.some(
1210
+ (segment) => segment !== "*" && segment !== "**" && !/^[A-Za-z0-9_:-][A-Za-z0-9_:\-]*$/.test(segment)
1211
+ );
1212
+ if (invalidSelector) {
1213
+ throw new Error(`telemetry.metrics[${index}] has an invalid dotted selector`);
1214
+ }
1215
+ const invalidGroupBy = definition.groupBy.find((field) => !this.isFieldPath(field));
1216
+ if (invalidGroupBy !== void 0 || new Set(definition.groupBy).size !== definition.groupBy.length) {
1217
+ throw new Error(`telemetry.metrics[${index}] has invalid or duplicate groupBy fields`);
1218
+ }
1219
+ const extraCardinality = Object.keys(definition.cardinality).find(
1220
+ (field) => !definition.groupBy.includes(field)
1221
+ );
1222
+ if (extraCardinality !== void 0) {
1223
+ throw new Error(
1224
+ `telemetry.metrics[${index}] cardinality policy ${extraCardinality} is not a declared dimension`
1225
+ );
1226
+ }
1227
+ const measurementKeys = definition.measurements.map(
1228
+ (measurement) => measurement.kind === "count" ? "count" : `field:${measurement.field}`
1229
+ );
1230
+ const invalidField = definition.measurements.find(
1231
+ (measurement) => measurement.kind === "field" && !this.isFieldPath(measurement.field)
1232
+ );
1233
+ if (invalidField !== void 0 || new Set(measurementKeys).size !== measurementKeys.length) {
1234
+ throw new Error(`telemetry.metrics[${index}] has invalid or duplicate measurements`);
1235
+ }
1236
+ }
1237
+ isFieldPath(value) {
1238
+ return value.split(".").every((segment) => /^[A-Za-z_][A-Za-z0-9_]*$/.test(segment));
1239
+ }
1240
+ resolveMeasurement(measurement) {
1241
+ if (measurement === "count") {
1242
+ return Object.freeze({ kind: "count" });
1243
+ }
1244
+ if (new Set(measurement.aggregate).size !== measurement.aggregate.length) {
1245
+ throw new Error(`Metric field ${measurement.field} has duplicate aggregates`);
1246
+ }
1247
+ return Object.freeze({
1248
+ kind: "field",
1249
+ field: measurement.field,
1250
+ aggregates: Object.freeze([...measurement.aggregate])
1251
+ });
1252
+ }
1253
+ metricResolutions(targets, targetIds) {
1254
+ const selected = targets.filter((target) => targetIds.includes(target.targetId));
1255
+ const first = selected[0]?.retention.rollups.map((tier) => tier.resolutionMs) ?? [];
1256
+ return first.filter(
1257
+ (resolution) => selected.every(
1258
+ (target) => target.retention.rollups.some((tier) => tier.resolutionMs === resolution)
1259
+ )
1260
+ ).sort((left, right) => left - right);
1261
+ }
1262
+ validateMetricResolutions(resolutions, targets, targetIds, metricIndex) {
1263
+ for (const targetId of targetIds) {
1264
+ const target = targets.find((candidate) => candidate.targetId === targetId);
1265
+ const unavailable = resolutions.find(
1266
+ (resolution) => !target?.retention.rollups.some((tier) => tier.resolutionMs === resolution)
1267
+ );
1268
+ if (unavailable !== void 0) {
1269
+ throw new Error(
1270
+ `telemetry.metrics[${metricIndex}] resolution ${unavailable} is not configured for target ${targetId}`
1271
+ );
1272
+ }
1273
+ }
1274
+ }
1275
+ validateMetricIdentities(metrics) {
1276
+ const identities = metrics.map((metric) => `${metric.name}:${metric.version}`);
1277
+ if (new Set(identities).size !== identities.length) {
1278
+ throw new Error("Metric name/version identities must be unique");
1279
+ }
1280
+ }
1281
+ errorMessage(cause) {
1282
+ return cause instanceof Error ? cause.message : String(cause);
1283
+ }
1284
+ };
1285
+
1286
+ // src/projector/telemetry_metrics_migration.ts
1287
+ var MAX_COMBINED_DIMENSION_KEYS = 1e4;
1288
+ function metricDefinitionToTraceProjection(metric, options = {}) {
1289
+ const measurements = [];
1290
+ for (const measurement of metric.measurements) {
1291
+ if (measurement.kind === "count") {
1292
+ continue;
1293
+ }
1294
+ measurements.push({
1295
+ field: measurement.field,
1296
+ aggregates: measurement.aggregates
1297
+ });
1298
+ }
1299
+ const resolutions = metric.resolutionsMs.length > 0 ? metric.resolutionsMs : options.fallbackResolutions ?? [6e4, 9e5, 36e5];
1300
+ const dimensionPolicies = Object.entries(metric.cardinality);
1301
+ const cardinality = dimensionPolicies.length > 0 ? {
1302
+ // Legacy policy is per-dimension per-bucket; the projection policy is
1303
+ // per-projection distinct dimension keys. The combined budget is the
1304
+ // product of the per-field limits (bounded by the spec's default
1305
+ // ceiling): `[routeKey(100), visibility(2)]` allows 200 pairs, not 2.
1306
+ // Taking the minimum, as this did until 2026-09-03, folded the third
1307
+ // route/visibility pair of frontend_performance_health@2 into `other`.
1308
+ maxDimensionKeys: Math.min(
1309
+ MAX_COMBINED_DIMENSION_KEYS,
1310
+ dimensionPolicies.reduce((product, [, policy]) => product * policy.maxValuesPerBucket, 1)
1311
+ ),
1312
+ overflow: dimensionPolicies.some(([, policy]) => policy.overflow === "other") ? "other" : "reject"
1313
+ } : void 0;
1314
+ return {
1315
+ name: metric.name,
1316
+ version: metric.version,
1317
+ event: metric.selector,
1318
+ ...metric.groupBy.length > 0 ? { groupBy: metric.groupBy } : {},
1319
+ measurements,
1320
+ resolutions,
1321
+ missingField: metric.missingField,
1322
+ ...cardinality !== void 0 ? { cardinality } : {}
1323
+ };
1324
+ }
1325
+ function metricDefinitionsToTraceProjections(metrics, options = {}) {
1326
+ return metrics.map((metric) => metricDefinitionToTraceProjection(metric, options));
1327
+ }
1328
+
1329
+ // src/projector/trace_projector_process.ts
1330
+ function createTraceProjectorProcess(options) {
1331
+ let projections = options.projections ?? [];
1332
+ if (options.configurationPath !== void 0) {
1333
+ const parser = new NodeTelemetryConfigurationParser();
1334
+ const configuration = parser.parseFile(options.configurationPath, {
1335
+ storageBaseDirectory: import_node_path2.default.dirname(options.destinationDbPath)
1336
+ });
1337
+ projections = [...projections, ...metricDefinitionsToTraceProjections(configuration.metrics)];
1338
+ }
1339
+ if (projections.length === 0) {
1340
+ throw new Error("TraceProjectorProcess: no projections configured");
1341
+ }
1342
+ const source = new import_sqlite.SqliteTraceStore({ connection: new import_better_sqlite3.default(options.tracesDbPath) });
1343
+ (0, import_node_fs2.mkdirSync)(import_node_path2.default.dirname(options.destinationDbPath), { recursive: true });
1344
+ const destination = new import_better_sqlite3.default(options.destinationDbPath);
1345
+ const projector = new TraceProjector({
1346
+ source,
1347
+ destination,
1348
+ projections,
1349
+ ...options.pageSize !== void 0 ? { pageSize: options.pageSize } : {}
1350
+ });
1351
+ return {
1352
+ projector,
1353
+ runOnce: () => projector.runOnce(),
1354
+ close: () => {
1355
+ source.close();
1356
+ destination.close();
1357
+ }
1358
+ };
1359
+ }
1360
+ // Annotate the CommonJS export names for ESM import in node:
1361
+ 0 && (module.exports = {
1362
+ TraceProjector,
1363
+ createTraceProjectorProcess,
1364
+ detectSourceReset,
1365
+ eventSelectorMatches,
1366
+ metricDefinitionToTraceProjection,
1367
+ metricDefinitionsToTraceProjections,
1368
+ validateTraceProjection
1369
+ });
1370
+ //# sourceMappingURL=index.cjs.map