@earendil-works/chord 1.3.0 → 1.4.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 (43) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +55 -1
  3. package/dist/context/index.d.ts +8 -0
  4. package/dist/context/index.d.ts.map +1 -1
  5. package/dist/context/index.js +15 -0
  6. package/dist/context/index.js.map +1 -1
  7. package/dist/output.d.ts +8 -2
  8. package/dist/output.d.ts.map +1 -1
  9. package/dist/output.js +17 -7
  10. package/dist/output.js.map +1 -1
  11. package/dist/sqlite/cloudflare.d.ts +52 -0
  12. package/dist/sqlite/cloudflare.d.ts.map +1 -0
  13. package/dist/sqlite/cloudflare.js +134 -0
  14. package/dist/sqlite/cloudflare.js.map +1 -0
  15. package/dist/sqlite/database.d.ts +37 -0
  16. package/dist/sqlite/database.d.ts.map +1 -0
  17. package/dist/sqlite/database.js +2 -0
  18. package/dist/sqlite/database.js.map +1 -0
  19. package/dist/sqlite/index.d.ts +4 -0
  20. package/dist/sqlite/index.d.ts.map +1 -0
  21. package/dist/sqlite/index.js +3 -0
  22. package/dist/sqlite/index.js.map +1 -0
  23. package/dist/sqlite/migrations.d.ts +10 -0
  24. package/dist/sqlite/migrations.d.ts.map +1 -0
  25. package/dist/sqlite/migrations.js +33 -0
  26. package/dist/sqlite/migrations.js.map +1 -0
  27. package/dist/sqlite/node.d.ts +38 -0
  28. package/dist/sqlite/node.d.ts.map +1 -0
  29. package/dist/sqlite/node.js +174 -0
  30. package/dist/sqlite/node.js.map +1 -0
  31. package/dist/sqlite/schema.d.ts +3 -0
  32. package/dist/sqlite/schema.d.ts.map +1 -0
  33. package/dist/sqlite/schema.js +10 -0
  34. package/dist/sqlite/schema.js.map +1 -0
  35. package/dist/telemetry-sqlite/index.d.ts +24 -0
  36. package/dist/telemetry-sqlite/index.d.ts.map +1 -0
  37. package/dist/telemetry-sqlite/index.js +273 -0
  38. package/dist/telemetry-sqlite/index.js.map +1 -0
  39. package/dist/telemetry-sqlite/store.d.ts +9 -0
  40. package/dist/telemetry-sqlite/store.d.ts.map +1 -0
  41. package/dist/telemetry-sqlite/store.js +110 -0
  42. package/dist/telemetry-sqlite/store.js.map +1 -0
  43. package/package.json +22 -1
@@ -0,0 +1,273 @@
1
+ import { NOOP_TELEMETRY_CONTEXT, } from "@earendil-works/pi-telemetry";
2
+ import { applySqliteMigrations } from "../sqlite/index.js";
3
+ import { findMetrics, findTraces, getTrace } from "./store.js";
4
+ const identities = new WeakMap();
5
+ const carrierKey = "pi-sqlite-span";
6
+ const traceKey = "pi-sqlite-trace";
7
+ const intervalMs = 10_000;
8
+ function copyAttributes(attributes) {
9
+ return Object.fromEntries(Object.entries(attributes ?? {})
10
+ .filter(([, value]) => value !== undefined)
11
+ .map(([key, value]) => [key, Array.isArray(value) ? [...value] : value]));
12
+ }
13
+ function copyLink(link) {
14
+ const identity = identities.get(link.context);
15
+ return identity
16
+ ? { trace_id: identity.traceId, span_id: identity.id, attributes: copyAttributes(link.attributes) }
17
+ : undefined;
18
+ }
19
+ class Recorder {
20
+ propagation = this;
21
+ metrics = {
22
+ counter: (name, options) => ({ add: this.measurement("counter", name, options) }),
23
+ upDownCounter: (name, options) => ({ add: this.measurement("upDownCounter", name, options) }),
24
+ histogram: (name, options) => ({ record: this.measurement("histogram", name, options) }),
25
+ };
26
+ droppedSpans = 0;
27
+ droppedMeasurements = 0;
28
+ db;
29
+ retention;
30
+ capacity;
31
+ scope = crypto.randomUUID();
32
+ sequence = 0;
33
+ spans = [];
34
+ measurements = new Map();
35
+ inFlightMeasurements = 0;
36
+ timer;
37
+ stopped = false;
38
+ failed = false;
39
+ failure;
40
+ writing;
41
+ closing;
42
+ constructor(db, retention, capacity) {
43
+ this.db = db;
44
+ this.retention = retention;
45
+ this.capacity = capacity;
46
+ this.timer = setInterval(() => {
47
+ if (!this.writing)
48
+ void this.flush().catch(() => { });
49
+ }, 1000);
50
+ this.timer.unref?.();
51
+ }
52
+ startSpan(options, callback) {
53
+ return this.start(undefined, options, callback);
54
+ }
55
+ inject(context) {
56
+ const identity = identities.get(context);
57
+ return identity ? { [carrierKey]: identity.id, [traceKey]: identity.traceId } : undefined;
58
+ }
59
+ extract(carrier) {
60
+ const id = carrier?.[carrierKey];
61
+ const traceId = carrier?.[traceKey];
62
+ if (typeof id !== "string" || typeof traceId !== "string")
63
+ return this;
64
+ const identity = { id, traceId };
65
+ const parent = {
66
+ metrics: this.metrics,
67
+ propagation: this,
68
+ startSpan: (options, callback) => this.start(identity, options, callback),
69
+ };
70
+ identities.set(parent, identity);
71
+ return parent;
72
+ }
73
+ inert(options, callback) {
74
+ return NOOP_TELEMETRY_CONTEXT.startSpan(options, (noop) => callback({
75
+ ...noop,
76
+ metrics: this.metrics,
77
+ propagation: this,
78
+ startSpan: (options, callback) => this.inert(options, callback),
79
+ }));
80
+ }
81
+ async start(parent, options, callback) {
82
+ if (this.stopped)
83
+ return this.inert(options, callback);
84
+ const name = options.name;
85
+ let attributes = copyAttributes(options.attributes);
86
+ const links = [];
87
+ for (const link of options.links ?? []) {
88
+ const copied = copyLink(link);
89
+ if (copied)
90
+ links.push(copied);
91
+ }
92
+ const id = `${this.scope}:${++this.sequence}`;
93
+ const identity = { id, traceId: parent?.traceId ?? id };
94
+ const started = Date.now();
95
+ const clock = performance.now();
96
+ const events = [];
97
+ let status = { status: "ok" };
98
+ let explicit = false;
99
+ let settled = false;
100
+ const record = (action) => {
101
+ if (!settled && !this.stopped)
102
+ action();
103
+ };
104
+ const span = {
105
+ metrics: this.metrics,
106
+ propagation: this,
107
+ startSpan: (options, callback) => settled ? this.inert(options, callback) : this.start(identity, options, callback),
108
+ addLink: (link) => record(() => {
109
+ const copied = copyLink(link);
110
+ if (copied)
111
+ links.push(copied);
112
+ }),
113
+ addEvent: (name, attrs) => record(() => {
114
+ events.push({ name, time_ms: performance.now() - clock, attributes: copyAttributes(attrs) });
115
+ }),
116
+ setAttributes: (attrs) => record(() => {
117
+ attributes = { ...attributes, ...copyAttributes(attrs) };
118
+ }),
119
+ setStatus: (value) => record(() => {
120
+ status =
121
+ value.status === "error" && value.error
122
+ ? { status: "error", error: { ...value.error } }
123
+ : { status: value.status };
124
+ explicit = true;
125
+ }),
126
+ };
127
+ identities.set(span, identity);
128
+ try {
129
+ return await callback(span);
130
+ }
131
+ catch (error) {
132
+ if (!explicit)
133
+ status =
134
+ error instanceof Error
135
+ ? { status: "error", error: { name: error.name, message: error.message } }
136
+ : { status: "error" };
137
+ throw error;
138
+ }
139
+ finally {
140
+ settled = true;
141
+ if (!this.stopped) {
142
+ if (this.spans.length >= this.capacity)
143
+ this.droppedSpans++;
144
+ else
145
+ this.spans.push([
146
+ id,
147
+ identity.traceId,
148
+ parent?.id ?? null,
149
+ name,
150
+ started,
151
+ started + performance.now() - clock,
152
+ JSON.stringify(attributes),
153
+ JSON.stringify(events),
154
+ JSON.stringify(links),
155
+ JSON.stringify(status),
156
+ ]);
157
+ }
158
+ }
159
+ }
160
+ measurement(kind, name, options) {
161
+ const unit = options?.unit ?? null;
162
+ return (value, attributes) => {
163
+ if (this.stopped)
164
+ return;
165
+ const copied = copyAttributes(attributes);
166
+ const encoded = JSON.stringify(Object.fromEntries(Object.keys(copied)
167
+ .sort()
168
+ .map((key) => [key, copied[key]])));
169
+ const key = JSON.stringify([name, kind, unit, encoded]);
170
+ const interval = Math.floor(Date.now() / intervalMs) * intervalMs;
171
+ const pendingKey = `${interval}:${key}`;
172
+ const previous = this.measurements.get(pendingKey);
173
+ if (!previous && this.measurements.size + this.inFlightMeasurements >= this.capacity) {
174
+ this.droppedMeasurements++;
175
+ return;
176
+ }
177
+ this.measurements.set(pendingKey, {
178
+ interval,
179
+ key,
180
+ name,
181
+ kind,
182
+ unit,
183
+ attributes: encoded,
184
+ count: (previous?.count ?? 0) + 1,
185
+ sum: (previous?.sum ?? 0) + value,
186
+ min: Math.min(previous?.min ?? value, value),
187
+ max: Math.max(previous?.max ?? value, value),
188
+ });
189
+ };
190
+ }
191
+ /** Readers use committed rows only; flush first to include newly settled spans. */
192
+ getTrace = (traceId) => getTrace(this.db, traceId);
193
+ findTraces = (query) => findTraces(this.db, query);
194
+ findMetrics = (query) => findMetrics(this.db, query);
195
+ flush() {
196
+ return this.closing ?? this.drain(false);
197
+ }
198
+ close() {
199
+ if (!this.closing) {
200
+ this.stopped = true;
201
+ clearInterval(this.timer);
202
+ this.closing = this.drain(true);
203
+ }
204
+ return this.closing;
205
+ }
206
+ async drain(partial) {
207
+ while (this.writing)
208
+ await this.writing;
209
+ if (this.failed)
210
+ throw this.failure;
211
+ const spans = this.spans.slice();
212
+ const metrics = [];
213
+ const completed = Math.floor(Date.now() / intervalMs) * intervalMs;
214
+ for (const [key, metric] of this.measurements) {
215
+ if (!partial && metric.interval >= completed)
216
+ continue;
217
+ metrics.push(metric);
218
+ this.measurements.delete(key);
219
+ }
220
+ this.inFlightMeasurements = metrics.length;
221
+ this.writing = Promise.resolve()
222
+ .then(() => this.db.transaction(async (tx) => {
223
+ for (const span of spans)
224
+ await tx.run("INSERT INTO telemetry_spans VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", ...span);
225
+ for (const metric of metrics)
226
+ await tx.run(`INSERT INTO telemetry_metrics VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
227
+ ON CONFLICT(interval_ms, series_key) DO UPDATE SET count = count + excluded.count,
228
+ sum = sum + excluded.sum, min = min(min, excluded.min), max = max(max, excluded.max)`, metric.interval, metric.key, metric.name, metric.kind, metric.unit, metric.attributes, metric.count, metric.sum, metric.min, metric.max);
229
+ const cutoff = Date.now() - this.retention;
230
+ await tx.run("DELETE FROM telemetry_spans WHERE ended_ms < ?", cutoff);
231
+ await tx.run("DELETE FROM telemetry_metrics WHERE interval_ms < ?", cutoff);
232
+ }))
233
+ .then(() => {
234
+ this.spans.splice(0, spans.length);
235
+ })
236
+ .catch((error) => {
237
+ this.failed = this.stopped = true;
238
+ this.failure = error;
239
+ this.spans.length = 0;
240
+ this.measurements.clear();
241
+ clearInterval(this.timer);
242
+ throw error;
243
+ })
244
+ .finally(() => {
245
+ this.writing = undefined;
246
+ this.inFlightMeasurements = 0;
247
+ });
248
+ await this.writing;
249
+ }
250
+ }
251
+ /** Portable, buffered SQLite recorder. Recording never performs SQL; flush runs every second.
252
+ * Only settled spans are stored. Metrics aggregate count/sum/min/max per ten-second interval.
253
+ * A write failure permanently stops recording and is reported by flush and close. */
254
+ export async function createSqliteTelemetry(db, options = {}) {
255
+ const retention = options.retentionMs ?? 7 * 24 * 60 * 60 * 1000;
256
+ const capacity = options.maxQueuedSpans ?? 10_000;
257
+ await applySqliteMigrations(db, [
258
+ {
259
+ version: 1,
260
+ statements: [
261
+ `CREATE TABLE telemetry_spans (id TEXT PRIMARY KEY, trace_id TEXT NOT NULL, parent_id TEXT, name TEXT, started_ms REAL,
262
+ ended_ms REAL NOT NULL, attributes TEXT, events TEXT, links TEXT, status TEXT)`,
263
+ "CREATE INDEX telemetry_spans_ended_ms ON telemetry_spans(ended_ms)",
264
+ "CREATE INDEX telemetry_spans_trace_started ON telemetry_spans(trace_id, started_ms)",
265
+ "CREATE INDEX telemetry_spans_started_ms ON telemetry_spans(started_ms)",
266
+ `CREATE TABLE telemetry_metrics (interval_ms INTEGER, series_key TEXT, name TEXT, kind TEXT,
267
+ unit TEXT, attributes TEXT, count INTEGER, sum REAL, min REAL, max REAL, PRIMARY KEY(interval_ms, series_key))`,
268
+ ],
269
+ },
270
+ ], { table: "telemetry_schema" });
271
+ return new Recorder(db, retention, capacity);
272
+ }
273
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/telemetry-sqlite/index.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n\ttype InstrumentOptions,\n\tNOOP_TELEMETRY_CONTEXT,\n\ttype SpanAttributes,\n\ttype SpanLink,\n\ttype SpanOptions,\n\ttype SpanStatus,\n\ttype TelemetryCarrier,\n\ttype TelemetryContext,\n\ttype TelemetryMetrics,\n\ttype TelemetryPropagation,\n\ttype TelemetrySpan,\n\ttype TelemetryStore,\n} from \"@earendil-works/pi-telemetry\";\nimport { applySqliteMigrations, type SqliteDatabase, type SqliteValue } from \"../sqlite/index.ts\";\nimport { findMetrics, findTraces, getTrace } from \"./store.ts\";\n\nexport interface SqliteTelemetryOptions {\n\t/** Retain seven days by default. Cleanup runs in the flush transaction. */\n\treadonly retentionMs?: number;\n\t/** Settled spans awaiting commit, including the in-flight batch. Defaults to 10,000.\n\t * Also bounds pending metric interval/series pairs independently. */\n\treadonly maxQueuedSpans?: number;\n}\n\nexport interface SqliteTelemetry extends TelemetryContext, TelemetryStore {\n\treadonly metrics: TelemetryMetrics;\n\treadonly propagation: TelemetryPropagation;\n\treadonly droppedSpans: number;\n\treadonly droppedMeasurements: number;\n\t/** Persist settled spans and completed ten-second metric intervals. */\n\tflush(): Promise<void>;\n\t/** Stop recording and drain, including partial metric intervals. Does not close the database. */\n\tclose(): Promise<void>;\n}\n\ntype Identity = { id: string; traceId: string };\ntype Link = { trace_id: string; span_id: string; attributes?: SpanAttributes };\ntype Metric = {\n\tinterval: number;\n\tkey: string;\n\tname: string;\n\tkind: \"counter\" | \"upDownCounter\" | \"histogram\";\n\tunit: string | null;\n\tattributes: string;\n\tcount: number;\n\tsum: number;\n\tmin: number;\n\tmax: number;\n};\nconst identities = new WeakMap<TelemetryContext, Identity>();\nconst carrierKey = \"pi-sqlite-span\";\nconst traceKey = \"pi-sqlite-trace\";\nconst intervalMs = 10_000;\n\nfunction copyAttributes(attributes?: SpanAttributes): SpanAttributes {\n\treturn Object.fromEntries(\n\t\tObject.entries(attributes ?? {})\n\t\t\t.filter(([, value]) => value !== undefined)\n\t\t\t.map(([key, value]) => [key, Array.isArray(value) ? [...value] : value]),\n\t);\n}\n\nfunction copyLink(link: SpanLink): Link | undefined {\n\tconst identity = identities.get(link.context);\n\treturn identity\n\t\t? { trace_id: identity.traceId, span_id: identity.id, attributes: copyAttributes(link.attributes) }\n\t\t: undefined;\n}\n\nclass Recorder implements SqliteTelemetry, TelemetryPropagation {\n\treadonly propagation: TelemetryPropagation = this;\n\treadonly metrics: TelemetryMetrics = {\n\t\tcounter: (name, options) => ({ add: this.measurement(\"counter\", name, options) }),\n\t\tupDownCounter: (name, options) => ({ add: this.measurement(\"upDownCounter\", name, options) }),\n\t\thistogram: (name, options) => ({ record: this.measurement(\"histogram\", name, options) }),\n\t};\n\tdroppedSpans = 0;\n\tdroppedMeasurements = 0;\n\tprivate readonly db: SqliteDatabase;\n\tprivate readonly retention: number;\n\tprivate readonly capacity: number;\n\tprivate readonly scope = crypto.randomUUID();\n\tprivate sequence = 0;\n\tprivate readonly spans: SqliteValue[][] = [];\n\tprivate readonly measurements = new Map<string, Metric>();\n\tprivate inFlightMeasurements = 0;\n\tprivate readonly timer: ReturnType<typeof setInterval>;\n\tprivate stopped = false;\n\tprivate failed = false;\n\tprivate failure: unknown;\n\tprivate writing?: Promise<void>;\n\tprivate closing?: Promise<void>;\n\n\tconstructor(db: SqliteDatabase, retention: number, capacity: number) {\n\t\tthis.db = db;\n\t\tthis.retention = retention;\n\t\tthis.capacity = capacity;\n\t\tthis.timer = setInterval(() => {\n\t\t\tif (!this.writing) void this.flush().catch(() => {});\n\t\t}, 1000);\n\t\tthis.timer.unref?.();\n\t}\n\n\tstartSpan<T>(options: SpanOptions, callback: (span: TelemetrySpan) => T | Promise<T>): Promise<T> {\n\t\treturn this.start(undefined, options, callback);\n\t}\n\n\tinject(context: TelemetryContext): TelemetryCarrier | undefined {\n\t\tconst identity = identities.get(context);\n\t\treturn identity ? { [carrierKey]: identity.id, [traceKey]: identity.traceId } : undefined;\n\t}\n\n\textract(carrier: TelemetryCarrier | undefined): TelemetryContext {\n\t\tconst id = carrier?.[carrierKey];\n\t\tconst traceId = carrier?.[traceKey];\n\t\tif (typeof id !== \"string\" || typeof traceId !== \"string\") return this;\n\t\tconst identity = { id, traceId };\n\t\tconst parent: TelemetryContext = {\n\t\t\tmetrics: this.metrics,\n\t\t\tpropagation: this,\n\t\t\tstartSpan: (options, callback) => this.start(identity, options, callback),\n\t\t};\n\t\tidentities.set(parent, identity);\n\t\treturn parent;\n\t}\n\n\tprivate inert<T>(options: SpanOptions, callback: (span: TelemetrySpan) => T | Promise<T>): Promise<T> {\n\t\treturn NOOP_TELEMETRY_CONTEXT.startSpan(options, (noop) =>\n\t\t\tcallback({\n\t\t\t\t...noop,\n\t\t\t\tmetrics: this.metrics,\n\t\t\t\tpropagation: this,\n\t\t\t\tstartSpan: (options, callback) => this.inert(options, callback),\n\t\t\t}),\n\t\t);\n\t}\n\n\tprivate async start<T>(\n\t\tparent: Identity | undefined,\n\t\toptions: SpanOptions,\n\t\tcallback: (span: TelemetrySpan) => T | Promise<T>,\n\t): Promise<T> {\n\t\tif (this.stopped) return this.inert(options, callback);\n\t\tconst name = options.name;\n\t\tlet attributes = copyAttributes(options.attributes);\n\t\tconst links: Link[] = [];\n\t\tfor (const link of options.links ?? []) {\n\t\t\tconst copied = copyLink(link);\n\t\t\tif (copied) links.push(copied);\n\t\t}\n\t\tconst id = `${this.scope}:${++this.sequence}`;\n\t\tconst identity = { id, traceId: parent?.traceId ?? id };\n\t\tconst started = Date.now();\n\t\tconst clock = performance.now();\n\t\tconst events: { name: string; time_ms: number; attributes: SpanAttributes }[] = [];\n\t\tlet status: SpanStatus = { status: \"ok\" };\n\t\tlet explicit = false;\n\t\tlet settled = false;\n\t\tconst record = (action: () => void): void => {\n\t\t\tif (!settled && !this.stopped) action();\n\t\t};\n\t\tconst span: TelemetrySpan = {\n\t\t\tmetrics: this.metrics,\n\t\t\tpropagation: this,\n\t\t\tstartSpan: (options, callback) =>\n\t\t\t\tsettled ? this.inert(options, callback) : this.start(identity, options, callback),\n\t\t\taddLink: (link) =>\n\t\t\t\trecord(() => {\n\t\t\t\t\tconst copied = copyLink(link);\n\t\t\t\t\tif (copied) links.push(copied);\n\t\t\t\t}),\n\t\t\taddEvent: (name, attrs) =>\n\t\t\t\trecord(() => {\n\t\t\t\t\tevents.push({ name, time_ms: performance.now() - clock, attributes: copyAttributes(attrs) });\n\t\t\t\t}),\n\t\t\tsetAttributes: (attrs) =>\n\t\t\t\trecord(() => {\n\t\t\t\t\tattributes = { ...attributes, ...copyAttributes(attrs) };\n\t\t\t\t}),\n\t\t\tsetStatus: (value) =>\n\t\t\t\trecord(() => {\n\t\t\t\t\tstatus =\n\t\t\t\t\t\tvalue.status === \"error\" && value.error\n\t\t\t\t\t\t\t? { status: \"error\", error: { ...value.error } }\n\t\t\t\t\t\t\t: { status: value.status };\n\t\t\t\t\texplicit = true;\n\t\t\t\t}),\n\t\t};\n\t\tidentities.set(span, identity);\n\t\ttry {\n\t\t\treturn await callback(span);\n\t\t} catch (error) {\n\t\t\tif (!explicit)\n\t\t\t\tstatus =\n\t\t\t\t\terror instanceof Error\n\t\t\t\t\t\t? { status: \"error\", error: { name: error.name, message: error.message } }\n\t\t\t\t\t\t: { status: \"error\" };\n\t\t\tthrow error;\n\t\t} finally {\n\t\t\tsettled = true;\n\t\t\tif (!this.stopped) {\n\t\t\t\tif (this.spans.length >= this.capacity) this.droppedSpans++;\n\t\t\t\telse\n\t\t\t\t\tthis.spans.push([\n\t\t\t\t\t\tid,\n\t\t\t\t\t\tidentity.traceId,\n\t\t\t\t\t\tparent?.id ?? null,\n\t\t\t\t\t\tname,\n\t\t\t\t\t\tstarted,\n\t\t\t\t\t\tstarted + performance.now() - clock,\n\t\t\t\t\t\tJSON.stringify(attributes),\n\t\t\t\t\t\tJSON.stringify(events),\n\t\t\t\t\t\tJSON.stringify(links),\n\t\t\t\t\t\tJSON.stringify(status),\n\t\t\t\t\t]);\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate measurement(\n\t\tkind: Metric[\"kind\"],\n\t\tname: string,\n\t\toptions?: InstrumentOptions,\n\t): (value: number, attributes?: SpanAttributes) => void {\n\t\tconst unit = options?.unit ?? null;\n\t\treturn (value, attributes) => {\n\t\t\tif (this.stopped) return;\n\t\t\tconst copied = copyAttributes(attributes);\n\t\t\tconst encoded = JSON.stringify(\n\t\t\t\tObject.fromEntries(\n\t\t\t\t\tObject.keys(copied)\n\t\t\t\t\t\t.sort()\n\t\t\t\t\t\t.map((key) => [key, copied[key]]),\n\t\t\t\t),\n\t\t\t);\n\t\t\tconst key = JSON.stringify([name, kind, unit, encoded]);\n\t\t\tconst interval = Math.floor(Date.now() / intervalMs) * intervalMs;\n\t\t\tconst pendingKey = `${interval}:${key}`;\n\t\t\tconst previous = this.measurements.get(pendingKey);\n\t\t\tif (!previous && this.measurements.size + this.inFlightMeasurements >= this.capacity) {\n\t\t\t\tthis.droppedMeasurements++;\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tthis.measurements.set(pendingKey, {\n\t\t\t\tinterval,\n\t\t\t\tkey,\n\t\t\t\tname,\n\t\t\t\tkind,\n\t\t\t\tunit,\n\t\t\t\tattributes: encoded,\n\t\t\t\tcount: (previous?.count ?? 0) + 1,\n\t\t\t\tsum: (previous?.sum ?? 0) + value,\n\t\t\t\tmin: Math.min(previous?.min ?? value, value),\n\t\t\t\tmax: Math.max(previous?.max ?? value, value),\n\t\t\t});\n\t\t};\n\t}\n\n\t/** Readers use committed rows only; flush first to include newly settled spans. */\n\treadonly getTrace: TelemetryStore[\"getTrace\"] = (traceId) => getTrace(this.db, traceId);\n\treadonly findTraces: TelemetryStore[\"findTraces\"] = (query) => findTraces(this.db, query);\n\treadonly findMetrics: TelemetryStore[\"findMetrics\"] = (query) => findMetrics(this.db, query);\n\n\tflush(): Promise<void> {\n\t\treturn this.closing ?? this.drain(false);\n\t}\n\n\tclose(): Promise<void> {\n\t\tif (!this.closing) {\n\t\t\tthis.stopped = true;\n\t\t\tclearInterval(this.timer);\n\t\t\tthis.closing = this.drain(true);\n\t\t}\n\t\treturn this.closing;\n\t}\n\n\tprivate async drain(partial: boolean): Promise<void> {\n\t\twhile (this.writing) await this.writing;\n\t\tif (this.failed) throw this.failure;\n\t\tconst spans = this.spans.slice();\n\t\tconst metrics: Metric[] = [];\n\t\tconst completed = Math.floor(Date.now() / intervalMs) * intervalMs;\n\t\tfor (const [key, metric] of this.measurements) {\n\t\t\tif (!partial && metric.interval >= completed) continue;\n\t\t\tmetrics.push(metric);\n\t\t\tthis.measurements.delete(key);\n\t\t}\n\t\tthis.inFlightMeasurements = metrics.length;\n\t\tthis.writing = Promise.resolve()\n\t\t\t.then(() =>\n\t\t\t\tthis.db.transaction(async (tx) => {\n\t\t\t\t\tfor (const span of spans)\n\t\t\t\t\t\tawait tx.run(\"INSERT INTO telemetry_spans VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\", ...span);\n\t\t\t\t\tfor (const metric of metrics)\n\t\t\t\t\t\tawait tx.run(\n\t\t\t\t\t\t\t`INSERT INTO telemetry_metrics VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n\t\t\t\tON CONFLICT(interval_ms, series_key) DO UPDATE SET count = count + excluded.count,\n\t\t\t\tsum = sum + excluded.sum, min = min(min, excluded.min), max = max(max, excluded.max)`,\n\t\t\t\t\t\t\tmetric.interval,\n\t\t\t\t\t\t\tmetric.key,\n\t\t\t\t\t\t\tmetric.name,\n\t\t\t\t\t\t\tmetric.kind,\n\t\t\t\t\t\t\tmetric.unit,\n\t\t\t\t\t\t\tmetric.attributes,\n\t\t\t\t\t\t\tmetric.count,\n\t\t\t\t\t\t\tmetric.sum,\n\t\t\t\t\t\t\tmetric.min,\n\t\t\t\t\t\t\tmetric.max,\n\t\t\t\t\t\t);\n\t\t\t\t\tconst cutoff = Date.now() - this.retention;\n\t\t\t\t\tawait tx.run(\"DELETE FROM telemetry_spans WHERE ended_ms < ?\", cutoff);\n\t\t\t\t\tawait tx.run(\"DELETE FROM telemetry_metrics WHERE interval_ms < ?\", cutoff);\n\t\t\t\t}),\n\t\t\t)\n\t\t\t.then(() => {\n\t\t\t\tthis.spans.splice(0, spans.length);\n\t\t\t})\n\t\t\t.catch((error: unknown) => {\n\t\t\t\tthis.failed = this.stopped = true;\n\t\t\t\tthis.failure = error;\n\t\t\t\tthis.spans.length = 0;\n\t\t\t\tthis.measurements.clear();\n\t\t\t\tclearInterval(this.timer);\n\t\t\t\tthrow error;\n\t\t\t})\n\t\t\t.finally(() => {\n\t\t\t\tthis.writing = undefined;\n\t\t\t\tthis.inFlightMeasurements = 0;\n\t\t\t});\n\t\tawait this.writing;\n\t}\n}\n\n/** Portable, buffered SQLite recorder. Recording never performs SQL; flush runs every second.\n * Only settled spans are stored. Metrics aggregate count/sum/min/max per ten-second interval.\n * A write failure permanently stops recording and is reported by flush and close. */\nexport async function createSqliteTelemetry(\n\tdb: SqliteDatabase,\n\toptions: SqliteTelemetryOptions = {},\n): Promise<SqliteTelemetry> {\n\tconst retention = options.retentionMs ?? 7 * 24 * 60 * 60 * 1000;\n\tconst capacity = options.maxQueuedSpans ?? 10_000;\n\tawait applySqliteMigrations(\n\t\tdb,\n\t\t[\n\t\t\t{\n\t\t\t\tversion: 1,\n\t\t\t\tstatements: [\n\t\t\t\t\t`CREATE TABLE telemetry_spans (id TEXT PRIMARY KEY, trace_id TEXT NOT NULL, parent_id TEXT, name TEXT, started_ms REAL,\n\t\t\tended_ms REAL NOT NULL, attributes TEXT, events TEXT, links TEXT, status TEXT)`,\n\t\t\t\t\t\"CREATE INDEX telemetry_spans_ended_ms ON telemetry_spans(ended_ms)\",\n\t\t\t\t\t\"CREATE INDEX telemetry_spans_trace_started ON telemetry_spans(trace_id, started_ms)\",\n\t\t\t\t\t\"CREATE INDEX telemetry_spans_started_ms ON telemetry_spans(started_ms)\",\n\t\t\t\t\t`CREATE TABLE telemetry_metrics (interval_ms INTEGER, series_key TEXT, name TEXT, kind TEXT,\n\t\t\tunit TEXT, attributes TEXT, count INTEGER, sum REAL, min REAL, max REAL, PRIMARY KEY(interval_ms, series_key))`,\n\t\t\t\t],\n\t\t\t},\n\t\t],\n\t\t{ table: \"telemetry_schema\" },\n\t);\n\treturn new Recorder(db, retention, capacity);\n}\n"]}
@@ -0,0 +1,9 @@
1
+ import type { MetricQuery, StoredMetric, StoredTrace, TraceQuery } from "@earendil-works/pi-telemetry";
2
+ import type { SqliteDatabase } from "../sqlite/index.ts";
3
+ export declare function getTrace(db: SqliteDatabase, traceId: string): Promise<StoredTrace | undefined>;
4
+ export declare function findTraces(db: SqliteDatabase, query?: TraceQuery): Promise<{
5
+ traces: readonly StoredTrace[];
6
+ cursor?: string;
7
+ }>;
8
+ export declare function findMetrics(db: SqliteDatabase, query: MetricQuery): Promise<readonly StoredMetric[]>;
9
+ //# sourceMappingURL=store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/telemetry-sqlite/store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACX,WAAW,EAGX,YAAY,EAEZ,WAAW,EACX,UAAU,EACV,MAAM,8BAA8B,CAAC;AACtC,OAAO,KAAK,EAAE,cAAc,EAAe,MAAM,oBAAoB,CAAC;AAwDtE,wBAAsB,QAAQ,CAAC,EAAE,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,SAAS,CAAC,CAMpG;AAED,wBAAsB,UAAU,CAC/B,EAAE,EAAE,cAAc,EAClB,KAAK,GAAE,UAAe,GACpB,OAAO,CAAC;IAAE,MAAM,EAAE,SAAS,WAAW,EAAE,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CA0D9D;AAED,wBAAsB,WAAW,CAAC,EAAE,EAAE,cAAc,EAAE,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,SAAS,YAAY,EAAE,CAAC,CAkC1G"}
@@ -0,0 +1,110 @@
1
+ function storedSpan(row) {
2
+ return {
3
+ id: row.id,
4
+ traceId: row.trace_id,
5
+ ...(row.parent_id === null ? {} : { parentId: row.parent_id }),
6
+ name: row.name,
7
+ startedMs: row.started_ms,
8
+ endedMs: row.ended_ms,
9
+ attributes: JSON.parse(row.attributes),
10
+ events: JSON.parse(row.events).map((event) => ({ name: event.name, timeMs: event.time_ms, attributes: event.attributes })),
11
+ links: JSON.parse(row.links).map((link) => ({
12
+ traceId: link.trace_id,
13
+ spanId: link.span_id,
14
+ ...(link.attributes === undefined ? {} : { attributes: link.attributes }),
15
+ })),
16
+ status: JSON.parse(row.status),
17
+ };
18
+ }
19
+ // JSON equality must distinguish boolean true from numeric 1. json_each also handles arbitrary attribute keys.
20
+ function attributesSql(attributes, column, params) {
21
+ return (Object.entries(attributes ?? {})
22
+ .map(([key, value]) => {
23
+ const type = typeof value === "number"
24
+ ? "entry.type IN ('integer', 'real')"
25
+ : typeof value === "boolean"
26
+ ? `entry.type = '${value ? "true" : "false"}'`
27
+ : "entry.type = 'text'";
28
+ params.push(key, typeof value === "boolean" ? Number(value) : value);
29
+ return `EXISTS (SELECT 1 FROM json_each(${column}) entry WHERE entry.key = ? AND ${type} AND entry.value = ?)`;
30
+ })
31
+ .join(" AND ") || "1");
32
+ }
33
+ export async function getTrace(db, traceId) {
34
+ const rows = await db.all("SELECT * FROM telemetry_spans WHERE trace_id = ? ORDER BY started_ms, id", traceId);
35
+ return rows.length ? { traceId, spans: rows.map(storedSpan) } : undefined;
36
+ }
37
+ export async function findTraces(db, query = {}) {
38
+ const limit = query.limit ?? 20;
39
+ const conditions = [];
40
+ const params = [];
41
+ if (query.name !== undefined) {
42
+ conditions.push("(SELECT name FROM telemetry_spans WHERE trace_id = heads.trace_id ORDER BY started_ms, id LIMIT 1) = ?");
43
+ params.push(query.name);
44
+ }
45
+ if (query.startMin !== undefined) {
46
+ conditions.push("started_ms >= ?");
47
+ params.push(query.startMin);
48
+ }
49
+ if (query.startMax !== undefined) {
50
+ conditions.push("started_ms <= ?");
51
+ params.push(query.startMax);
52
+ }
53
+ if (query.errors)
54
+ conditions.push("EXISTS (SELECT 1 FROM telemetry_spans WHERE trace_id = heads.trace_id AND json_extract(status, '$.status') = 'error')");
55
+ if (query.attributes)
56
+ conditions.push(`EXISTS (SELECT 1 FROM telemetry_spans candidate WHERE candidate.trace_id = heads.trace_id AND ${attributesSql(query.attributes, "candidate.attributes", params)})`);
57
+ if (query.cursor !== undefined) {
58
+ const cursor = JSON.parse(query.cursor);
59
+ conditions.push("(started_ms < ? OR (started_ms = ? AND trace_id > ?))");
60
+ params.push(cursor[0], cursor[0], cursor[1]);
61
+ }
62
+ return db.transaction(async (tx) => {
63
+ const heads = await tx.all(`WITH heads AS (
64
+ SELECT trace_id, min(started_ms) started_ms FROM telemetry_spans GROUP BY trace_id
65
+ ) SELECT * FROM heads WHERE ${conditions.join(" AND ") || "1"} ORDER BY started_ms DESC, trace_id LIMIT ?`, ...params, limit + 1);
66
+ const more = heads.length > limit;
67
+ if (more)
68
+ heads.pop();
69
+ if (!heads.length)
70
+ return { traces: [] };
71
+ const rows = await tx.all("SELECT * FROM telemetry_spans WHERE trace_id IN (SELECT value FROM json_each(?)) ORDER BY started_ms, id", JSON.stringify(heads.map((head) => head.trace_id)));
72
+ const grouped = new Map();
73
+ for (const row of rows) {
74
+ const spans = grouped.get(row.trace_id) ?? [];
75
+ spans.push(storedSpan(row));
76
+ grouped.set(row.trace_id, spans);
77
+ }
78
+ const last = heads.at(-1);
79
+ return {
80
+ traces: heads.map((head) => ({ traceId: head.trace_id, spans: grouped.get(head.trace_id) })),
81
+ ...(more ? { cursor: JSON.stringify([last.started_ms, last.trace_id]) } : {}),
82
+ };
83
+ });
84
+ }
85
+ export async function findMetrics(db, query) {
86
+ const params = [query.name];
87
+ const conditions = ["name = ?"];
88
+ if (query.startMin !== undefined) {
89
+ conditions.push("interval_ms >= ?");
90
+ params.push(query.startMin);
91
+ }
92
+ if (query.startMax !== undefined) {
93
+ conditions.push("interval_ms <= ?");
94
+ params.push(query.startMax);
95
+ }
96
+ conditions.push(attributesSql(query.attributes, "attributes", params));
97
+ const rows = await db.all(`SELECT * FROM telemetry_metrics WHERE ${conditions.join(" AND ")} ORDER BY interval_ms, series_key`, ...params);
98
+ return rows.map((row) => ({
99
+ intervalMs: row.interval_ms,
100
+ name: row.name,
101
+ kind: row.kind,
102
+ ...(row.unit === null ? {} : { unit: row.unit }),
103
+ attributes: JSON.parse(row.attributes),
104
+ count: row.count,
105
+ sum: row.sum,
106
+ min: row.min,
107
+ max: row.max,
108
+ }));
109
+ }
110
+ //# sourceMappingURL=store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.js","sourceRoot":"","sources":["../../src/telemetry-sqlite/store.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n\tMetricQuery,\n\tSpanAttributes,\n\tSpanStatus,\n\tStoredMetric,\n\tStoredSpan,\n\tStoredTrace,\n\tTraceQuery,\n} from \"@earendil-works/pi-telemetry\";\nimport type { SqliteDatabase, SqliteValue } from \"../sqlite/index.ts\";\n\ntype SpanRow = {\n\tid: string;\n\ttrace_id: string;\n\tparent_id: string | null;\n\tname: string;\n\tstarted_ms: number;\n\tended_ms: number;\n\tattributes: string;\n\tevents: string;\n\tlinks: string;\n\tstatus: string;\n};\n\nfunction storedSpan(row: SpanRow): StoredSpan {\n\treturn {\n\t\tid: row.id,\n\t\ttraceId: row.trace_id,\n\t\t...(row.parent_id === null ? {} : { parentId: row.parent_id }),\n\t\tname: row.name,\n\t\tstartedMs: row.started_ms,\n\t\tendedMs: row.ended_ms,\n\t\tattributes: JSON.parse(row.attributes) as SpanAttributes,\n\t\tevents: (JSON.parse(row.events) as { name: string; time_ms: number; attributes: SpanAttributes }[]).map(\n\t\t\t(event) => ({ name: event.name, timeMs: event.time_ms, attributes: event.attributes }),\n\t\t),\n\t\tlinks: (JSON.parse(row.links) as { trace_id: string; span_id: string; attributes?: SpanAttributes }[]).map(\n\t\t\t(link) => ({\n\t\t\t\ttraceId: link.trace_id,\n\t\t\t\tspanId: link.span_id,\n\t\t\t\t...(link.attributes === undefined ? {} : { attributes: link.attributes }),\n\t\t\t}),\n\t\t),\n\t\tstatus: JSON.parse(row.status) as SpanStatus,\n\t};\n}\n\n// JSON equality must distinguish boolean true from numeric 1. json_each also handles arbitrary attribute keys.\nfunction attributesSql(attributes: TraceQuery[\"attributes\"], column: string, params: SqliteValue[]): string {\n\treturn (\n\t\tObject.entries(attributes ?? {})\n\t\t\t.map(([key, value]) => {\n\t\t\t\tconst type =\n\t\t\t\t\ttypeof value === \"number\"\n\t\t\t\t\t\t? \"entry.type IN ('integer', 'real')\"\n\t\t\t\t\t\t: typeof value === \"boolean\"\n\t\t\t\t\t\t\t? `entry.type = '${value ? \"true\" : \"false\"}'`\n\t\t\t\t\t\t\t: \"entry.type = 'text'\";\n\t\t\t\tparams.push(key, typeof value === \"boolean\" ? Number(value) : value);\n\t\t\t\treturn `EXISTS (SELECT 1 FROM json_each(${column}) entry WHERE entry.key = ? AND ${type} AND entry.value = ?)`;\n\t\t\t})\n\t\t\t.join(\" AND \") || \"1\"\n\t);\n}\n\nexport async function getTrace(db: SqliteDatabase, traceId: string): Promise<StoredTrace | undefined> {\n\tconst rows = await db.all<SpanRow>(\n\t\t\"SELECT * FROM telemetry_spans WHERE trace_id = ? ORDER BY started_ms, id\",\n\t\ttraceId,\n\t);\n\treturn rows.length ? { traceId, spans: rows.map(storedSpan) } : undefined;\n}\n\nexport async function findTraces(\n\tdb: SqliteDatabase,\n\tquery: TraceQuery = {},\n): Promise<{ traces: readonly StoredTrace[]; cursor?: string }> {\n\tconst limit = query.limit ?? 20;\n\tconst conditions: string[] = [];\n\tconst params: SqliteValue[] = [];\n\tif (query.name !== undefined) {\n\t\tconditions.push(\n\t\t\t\"(SELECT name FROM telemetry_spans WHERE trace_id = heads.trace_id ORDER BY started_ms, id LIMIT 1) = ?\",\n\t\t);\n\t\tparams.push(query.name);\n\t}\n\tif (query.startMin !== undefined) {\n\t\tconditions.push(\"started_ms >= ?\");\n\t\tparams.push(query.startMin);\n\t}\n\tif (query.startMax !== undefined) {\n\t\tconditions.push(\"started_ms <= ?\");\n\t\tparams.push(query.startMax);\n\t}\n\tif (query.errors)\n\t\tconditions.push(\n\t\t\t\"EXISTS (SELECT 1 FROM telemetry_spans WHERE trace_id = heads.trace_id AND json_extract(status, '$.status') = 'error')\",\n\t\t);\n\tif (query.attributes)\n\t\tconditions.push(\n\t\t\t`EXISTS (SELECT 1 FROM telemetry_spans candidate WHERE candidate.trace_id = heads.trace_id AND ${attributesSql(query.attributes, \"candidate.attributes\", params)})`,\n\t\t);\n\tif (query.cursor !== undefined) {\n\t\tconst cursor = JSON.parse(query.cursor) as [number, string];\n\t\tconditions.push(\"(started_ms < ? OR (started_ms = ? AND trace_id > ?))\");\n\t\tparams.push(cursor[0], cursor[0], cursor[1]);\n\t}\n\treturn db.transaction(async (tx) => {\n\t\tconst heads = await tx.all<{ trace_id: string; started_ms: number }>(\n\t\t\t`WITH heads AS (\n\t\t\tSELECT trace_id, min(started_ms) started_ms FROM telemetry_spans GROUP BY trace_id\n\t\t) SELECT * FROM heads WHERE ${conditions.join(\" AND \") || \"1\"} ORDER BY started_ms DESC, trace_id LIMIT ?`,\n\t\t\t...params,\n\t\t\tlimit + 1,\n\t\t);\n\t\tconst more = heads.length > limit;\n\t\tif (more) heads.pop();\n\t\tif (!heads.length) return { traces: [] };\n\t\tconst rows = await tx.all<SpanRow>(\n\t\t\t\"SELECT * FROM telemetry_spans WHERE trace_id IN (SELECT value FROM json_each(?)) ORDER BY started_ms, id\",\n\t\t\tJSON.stringify(heads.map((head) => head.trace_id)),\n\t\t);\n\t\tconst grouped = new Map<string, StoredSpan[]>();\n\t\tfor (const row of rows) {\n\t\t\tconst spans = grouped.get(row.trace_id) ?? [];\n\t\t\tspans.push(storedSpan(row));\n\t\t\tgrouped.set(row.trace_id, spans);\n\t\t}\n\t\tconst last = heads.at(-1)!;\n\t\treturn {\n\t\t\ttraces: heads.map((head) => ({ traceId: head.trace_id, spans: grouped.get(head.trace_id)! })),\n\t\t\t...(more ? { cursor: JSON.stringify([last.started_ms, last.trace_id]) } : {}),\n\t\t};\n\t});\n}\n\nexport async function findMetrics(db: SqliteDatabase, query: MetricQuery): Promise<readonly StoredMetric[]> {\n\tconst params: SqliteValue[] = [query.name];\n\tconst conditions = [\"name = ?\"];\n\tif (query.startMin !== undefined) {\n\t\tconditions.push(\"interval_ms >= ?\");\n\t\tparams.push(query.startMin);\n\t}\n\tif (query.startMax !== undefined) {\n\t\tconditions.push(\"interval_ms <= ?\");\n\t\tparams.push(query.startMax);\n\t}\n\tconditions.push(attributesSql(query.attributes, \"attributes\", params));\n\tconst rows = await db.all<{\n\t\tinterval_ms: number;\n\t\tname: string;\n\t\tkind: StoredMetric[\"kind\"];\n\t\tunit: string | null;\n\t\tattributes: string;\n\t\tcount: number;\n\t\tsum: number;\n\t\tmin: number;\n\t\tmax: number;\n\t}>(`SELECT * FROM telemetry_metrics WHERE ${conditions.join(\" AND \")} ORDER BY interval_ms, series_key`, ...params);\n\treturn rows.map((row) => ({\n\t\tintervalMs: row.interval_ms,\n\t\tname: row.name,\n\t\tkind: row.kind,\n\t\t...(row.unit === null ? {} : { unit: row.unit }),\n\t\tattributes: JSON.parse(row.attributes) as SpanAttributes,\n\t\tcount: row.count,\n\t\tsum: row.sum,\n\t\tmin: row.min,\n\t\tmax: row.max,\n\t}));\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@earendil-works/chord",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "Typed host runtime for plugins, contracts, replicated state, and RPC over JSON links",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -31,6 +31,26 @@
31
31
  "types": "./dist/node.d.ts",
32
32
  "import": "./dist/node.js"
33
33
  },
34
+ "./telemetry-sqlite": {
35
+ "source": "./src/telemetry-sqlite/index.ts",
36
+ "types": "./dist/telemetry-sqlite/index.d.ts",
37
+ "import": "./dist/telemetry-sqlite/index.js"
38
+ },
39
+ "./sqlite": {
40
+ "source": "./src/sqlite/index.ts",
41
+ "types": "./dist/sqlite/index.d.ts",
42
+ "import": "./dist/sqlite/index.js"
43
+ },
44
+ "./sqlite/node": {
45
+ "source": "./src/sqlite/node.ts",
46
+ "types": "./dist/sqlite/node.d.ts",
47
+ "import": "./dist/sqlite/node.js"
48
+ },
49
+ "./sqlite/cloudflare": {
50
+ "source": "./src/sqlite/cloudflare.ts",
51
+ "types": "./dist/sqlite/cloudflare.d.ts",
52
+ "import": "./dist/sqlite/cloudflare.js"
53
+ },
34
54
  "./package.json": "./package.json"
35
55
  },
36
56
  "sideEffects": false,
@@ -65,6 +85,7 @@
65
85
  "node": ">=22.19.0"
66
86
  },
67
87
  "dependencies": {
88
+ "@earendil-works/pi-telemetry": "1.4.0",
68
89
  "typebox": "1.3.27"
69
90
  },
70
91
  "devDependencies": {