@camstack/system 1.2.456 → 1.2.458

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 (123) hide show
  1. package/dist/addon-runner.js +259 -37
  2. package/dist/addon-runner.mjs +289 -67
  3. package/dist/{agent-https-fm4AR8XL.mjs → agent-https-CQA3W-3e.mjs} +2 -2
  4. package/dist/{agent-https-yjxdfJDA.js → agent-https-DOELEug8.js} +2 -2
  5. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  6. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  7. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  8. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  9. package/dist/builtins/alerts/alerts.addon.js +2 -2
  10. package/dist/builtins/alerts/alerts.addon.mjs +2 -2
  11. package/dist/builtins/autotrack/index.js +1 -1
  12. package/dist/builtins/autotrack/index.mjs +1 -1
  13. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +2 -2
  14. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +2 -2
  15. package/dist/builtins/calendars/calendars.addon.js +1 -1
  16. package/dist/builtins/calendars/calendars.addon.mjs +1 -1
  17. package/dist/builtins/camera-grid/index.js +1 -1
  18. package/dist/builtins/camera-grid/index.mjs +1 -1
  19. package/dist/builtins/composer/composer.addon.js +2 -2
  20. package/dist/builtins/composer/composer.addon.mjs +2 -2
  21. package/dist/builtins/console-logging/console-destination.d.ts +1 -2
  22. package/dist/builtins/console-logging/index.js +1 -4
  23. package/dist/builtins/console-logging/index.mjs +1 -4
  24. package/dist/builtins/core-blocks/core-blocks.addon.js +2 -2
  25. package/dist/builtins/core-blocks/core-blocks.addon.mjs +2 -2
  26. package/dist/builtins/device-history/capture/capture-engine.d.ts +6 -0
  27. package/dist/builtins/device-history/device-history.addon.d.ts +13 -0
  28. package/dist/builtins/device-history/device-history.addon.js +736 -315
  29. package/dist/builtins/device-history/device-history.addon.mjs +736 -315
  30. package/dist/builtins/device-history/ingest/producer-ingest.d.ts +11 -1
  31. package/dist/builtins/device-history/query/history-query.d.ts +35 -9
  32. package/dist/builtins/device-history/query/point-shape.d.ts +33 -0
  33. package/dist/builtins/device-history/retention/retention.d.ts +1 -1
  34. package/dist/builtins/device-history/rollup/rollup-engine.d.ts +2 -0
  35. package/dist/builtins/device-history/rollup/rollup-math.d.ts +6 -0
  36. package/dist/builtins/device-history/store/history-store.d.ts +7 -0
  37. package/dist/builtins/device-history/store/series-registry.d.ts +26 -1
  38. package/dist/builtins/device-manager/device-manager.addon.js +1 -1
  39. package/dist/builtins/device-manager/device-manager.addon.mjs +1 -1
  40. package/dist/builtins/hub-forwarder/hub-forwarder-destination.d.ts +1 -2
  41. package/dist/builtins/hub-forwarder/index.js +1 -4
  42. package/dist/builtins/hub-forwarder/index.mjs +1 -4
  43. package/dist/builtins/keychain/index.js +4 -4
  44. package/dist/builtins/keychain/index.mjs +4 -4
  45. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  46. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  47. package/dist/builtins/local-auth/local-auth.addon.js +3 -3
  48. package/dist/builtins/local-auth/local-auth.addon.mjs +3 -3
  49. package/dist/builtins/local-network/local-network.addon.js +2 -2
  50. package/dist/builtins/local-network/local-network.addon.mjs +2 -2
  51. package/dist/builtins/loki-logging/index.js +323 -7
  52. package/dist/builtins/loki-logging/index.mjs +323 -7
  53. package/dist/builtins/loki-logging/loki-destination.d.ts +20 -3
  54. package/dist/builtins/loki-logging/loki-read.d.ts +51 -0
  55. package/dist/builtins/native-metrics/native-metrics.addon.js +290 -112
  56. package/dist/builtins/native-metrics/native-metrics.addon.mjs +290 -112
  57. package/dist/builtins/native-metrics/node-metrics-history-recorder.d.ts +106 -35
  58. package/dist/builtins/platform-probe/coral-accelerators.d.ts +26 -9
  59. package/dist/builtins/platform-probe/index.js +11 -68
  60. package/dist/builtins/platform-probe/index.mjs +11 -68
  61. package/dist/builtins/record-entries/index.js +1 -1
  62. package/dist/builtins/record-entries/index.mjs +1 -1
  63. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  64. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  65. package/dist/builtins/snapshot/index.js +1 -1
  66. package/dist/builtins/snapshot/index.mjs +1 -1
  67. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  68. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  69. package/dist/builtins/sqlite-storage/index.js +1 -1
  70. package/dist/builtins/sqlite-storage/index.mjs +1 -1
  71. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +2 -2
  72. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +2 -2
  73. package/dist/builtins/storage-orchestrator/location-history-recorder.d.ts +76 -0
  74. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.d.ts +9 -0
  75. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +144 -1
  76. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +144 -1
  77. package/dist/builtins/system-config/system-config.addon.js +3 -3
  78. package/dist/builtins/system-config/system-config.addon.mjs +3 -3
  79. package/dist/builtins/winston-logging/index.js +165 -3
  80. package/dist/builtins/winston-logging/index.mjs +165 -3
  81. package/dist/builtins/winston-logging/log-file-cursor.d.ts +12 -0
  82. package/dist/builtins/winston-logging/log-file-index-store.d.ts +11 -0
  83. package/dist/builtins/winston-logging/log-file-index.d.ts +77 -0
  84. package/dist/builtins/winston-logging/log-file-read-client.d.ts +44 -0
  85. package/dist/builtins/winston-logging/log-file-read-protocol.d.ts +41 -0
  86. package/dist/builtins/winston-logging/log-file-read-worker.d.ts +1 -0
  87. package/dist/builtins/winston-logging/log-file-reader.d.ts +51 -0
  88. package/dist/builtins/winston-logging/winston-destination.d.ts +20 -2
  89. package/dist/{composition-sources-Ds0Vr6eD.mjs → composition-sources-BVguN7VV.mjs} +1 -1
  90. package/dist/{composition-sources-gB4coDJR.js → composition-sources-DURQNDJ4.js} +1 -1
  91. package/dist/{constants-i5oD_gLB.mjs → constants-FGjnsE2x.mjs} +1 -1
  92. package/dist/{constants-CavyKfHZ.js → constants-Njka8Cv6.js} +1 -1
  93. package/dist/coral-accelerators-Bi_c3Rft.js +175 -0
  94. package/dist/coral-accelerators-DcnZvctM.mjs +162 -0
  95. package/dist/{device-ca-bpoEr0zG.js → device-ca-BrVK_5s3.js} +1 -1
  96. package/dist/{device-ca-qz1Pmeu1.mjs → device-ca-lEaAQ9Xi.mjs} +1 -1
  97. package/dist/{dist-BpqA2CT0.js → dist-BZQjLPiz.js} +526 -57
  98. package/dist/{dist-BSF_C8W2.mjs → dist-B_GRPhnY.mjs} +509 -58
  99. package/dist/file-host-worker.js +1 -1
  100. package/dist/file-host-worker.mjs +1 -1
  101. package/dist/{hub-mdns-ciao-CHaGDzyz.mjs → hub-mdns-ciao-Bg2HfTwP.mjs} +2 -2
  102. package/dist/{hub-mdns-ciao-CZlZRxxA.js → hub-mdns-ciao-nce3TXs_.js} +2 -2
  103. package/dist/index.d.ts +1 -0
  104. package/dist/index.js +140 -48
  105. package/dist/index.mjs +134 -49
  106. package/dist/kernel/config-schema.d.ts +5 -0
  107. package/dist/kernel/hwaccel/hwaccel-resolver.d.ts +7 -3
  108. package/dist/kernel/moleculer/accessory-prune-gate.d.ts +55 -0
  109. package/dist/log-file-read-protocol-B7M3X3bT.mjs +785 -0
  110. package/dist/log-file-read-protocol-D0WcLbIV.js +817 -0
  111. package/dist/log-file-read-worker.js +64 -0
  112. package/dist/log-file-read-worker.mjs +64 -0
  113. package/dist/log-read-filter-Bi066jwU.js +134 -0
  114. package/dist/log-read-filter-D7Ev3zm-.mjs +93 -0
  115. package/dist/logging/log-manager.d.ts +10 -6
  116. package/dist/logging/log-read-filter.d.ts +43 -0
  117. package/dist/{phase6-declare-gate-n-IZRklX.js → phase6-declare-gate-Dag1_WR2.js} +1 -1
  118. package/dist/{phase6-declare-gate-B4yfsf2n.mjs → phase6-declare-gate-Lb-mrVvz.mjs} +1 -1
  119. package/dist/{restore-archive-BGUM4FKf.mjs → restore-archive-C7kLDI_0.mjs} +1 -1
  120. package/dist/{restore-archive-B4QG7XmX.js → restore-archive-FyfZ8YMC.js} +1 -1
  121. package/dist/{wal-maintenance-CNEs_I94.js → wal-maintenance-DfSIOj24.js} +0 -0
  122. package/dist/{wal-maintenance-AerF4XE6.mjs → wal-maintenance-Duejh7Lx.mjs} +0 -0
  123. package/package.json +1 -1
@@ -1,4 +1,6 @@
1
- import { $r as BaseAddon, In as logDestinationCapability } from "../../dist-BSF_C8W2.mjs";
1
+ import { ni as BaseAddon, zn as logDestinationCapability } from "../../dist-B_GRPhnY.mjs";
2
+ import { n as compileLogFilter, o as lineMatches } from "../../log-read-filter-D7Ev3zm-.mjs";
3
+ import { z } from "zod";
2
4
  //#region src/builtins/loki-logging/loki-payload.ts
3
5
  /**
4
6
  * Loki rejects a label name that is not a valid Prometheus label
@@ -36,6 +38,7 @@ function toUnixNano(timestamp) {
36
38
  */
37
39
  function formatLokiLine(entry) {
38
40
  const line = { message: entry.message };
41
+ if (entry.id !== void 0) line["id"] = entry.id;
39
42
  if (entry.scope !== void 0 && entry.scope.length > 0) line["scope"] = entry.scope;
40
43
  if (entry.tags !== void 0 && Object.keys(entry.tags).length > 0) line["tags"] = entry.tags;
41
44
  if (entry.meta !== void 0 && Object.keys(entry.meta).length > 0) line["meta"] = entry.meta;
@@ -86,6 +89,231 @@ function buildPushBody(entries, labels) {
86
89
  return { streams };
87
90
  }
88
91
  //#endregion
92
+ //#region src/builtins/loki-logging/loki-read.ts
93
+ /**
94
+ * Reading history back from Loki (D898): `GET /loki/api/v1/query_range`,
95
+ * newest first.
96
+ *
97
+ * The label selector narrows by what IS a label (`job`, `node`, `level` —
98
+ * see `loki-payload.ts` for why nothing else is); line filters (`|=`, `|~`)
99
+ * narrow by device, addon and text on the JSON body; the shared
100
+ * `lineMatches` then decides exactly, so a line filter only has to be a
101
+ * superset.
102
+ *
103
+ * The cursor is the nanosecond timestamp of the oldest line returned plus a
104
+ * short hash of each line returned AT that nanosecond: entries share
105
+ * timestamps (they are millisecond-precise), so "older than t" would drop the
106
+ * siblings a page cut through, and "t or older" would repeat them. The next
107
+ * page asks for `end = t + 1` (Loki's end is exclusive) and skips the hashes.
108
+ */
109
+ var LEVELS = [
110
+ "debug",
111
+ "info",
112
+ "warn",
113
+ "error"
114
+ ];
115
+ /** One query_range window. A busy stream fills `limit` long before this. */
116
+ var LOKI_WINDOW_MS = 360 * 60 * 1e3;
117
+ var MS_NS = 1000000n;
118
+ var CURSOR_PREFIX = "l1.";
119
+ var LokiCursorSchema = z.object({
120
+ /** Nanosecond epoch, decimal string. */
121
+ n: z.string().regex(/^\d{1,22}$/),
122
+ /** Hashes of the lines already returned at exactly `n`. */
123
+ h: z.array(z.string().max(16)).max(1e3)
124
+ });
125
+ function encodeLokiCursor(cursor) {
126
+ return `${CURSOR_PREFIX}${Buffer.from(JSON.stringify(cursor)).toString("base64url")}`;
127
+ }
128
+ function decodeLokiCursor(raw) {
129
+ if (!raw.startsWith(CURSOR_PREFIX)) return null;
130
+ try {
131
+ const parsed = LokiCursorSchema.safeParse(JSON.parse(Buffer.from(raw.slice(3), "base64url").toString("utf8")));
132
+ return parsed.success ? parsed.data : null;
133
+ } catch {
134
+ return null;
135
+ }
136
+ }
137
+ /** FNV-1a, 32 bit, hex — a dedupe key, not a security property. */
138
+ function lineHash(line) {
139
+ let h = 2166136261;
140
+ for (let i = 0; i < line.length; i += 1) {
141
+ h ^= line.charCodeAt(i);
142
+ h = Math.imul(h, 16777619);
143
+ }
144
+ return (h >>> 0).toString(16);
145
+ }
146
+ /** A LogQL double-quoted string literal. */
147
+ function quote(value) {
148
+ return `"${value.replaceAll("\\", "\\\\").replaceAll("\"", "\\\"")}"`;
149
+ }
150
+ function escapeRegex(value) {
151
+ return value.replaceAll(/[\\.+*?()|[\]{}^$]/g, (c) => `\\${c}`);
152
+ }
153
+ /** The LogQL for a read. Pure. */
154
+ function buildLogQl(query, labels) {
155
+ const selectors = [`job=${quote(labels.job)}`];
156
+ if (query.fromNodeId !== void 0) selectors.push(`node=${quote(query.fromNodeId)}`);
157
+ if (query.level !== void 0 && query.level !== "debug") {
158
+ const floor = LEVELS.indexOf(query.level);
159
+ selectors.push(`level=~${quote(LEVELS.slice(floor).join("|"))}`);
160
+ }
161
+ let logql = `{${selectors.join(",")}}`;
162
+ if (query.deviceId !== void 0) logql += ` |= ${quote(`"deviceId":${String(query.deviceId)}`)}`;
163
+ if (query.fromAddonId !== void 0) {
164
+ const bare = query.fromAddonId.split("@")[0] ?? query.fromAddonId;
165
+ logql += ` |= ${quote(`"addonId":"${bare}`)}`;
166
+ }
167
+ const text = query.text?.trim();
168
+ if (text !== void 0 && text.length > 0) logql += ` |~ ${quote(`(?i)${escapeRegex(text)}`)}`;
169
+ return logql;
170
+ }
171
+ var LokiResponseSchema = z.object({ data: z.object({ result: z.array(z.object({
172
+ stream: z.record(z.string(), z.string()),
173
+ values: z.array(z.tuple([z.string(), z.string()]))
174
+ })) }) });
175
+ var LokiLineSchema = z.object({
176
+ id: z.string().optional(),
177
+ message: z.string(),
178
+ scope: z.string().optional(),
179
+ tags: z.record(z.string(), z.unknown()).optional(),
180
+ meta: z.record(z.string(), z.unknown()).optional()
181
+ });
182
+ function levelOf(label) {
183
+ return label === "debug" || label === "warn" || label === "error" ? label : "info";
184
+ }
185
+ /** A query_range body → rows, newest first. Lines that are not ours are dropped. */
186
+ function parseQueryRange(body) {
187
+ const parsed = LokiResponseSchema.safeParse(body);
188
+ if (!parsed.success) throw new Error("Loki answered with an unexpected query_range shape");
189
+ const rows = [];
190
+ for (const stream of parsed.data.data.result) {
191
+ const level = levelOf(stream.stream["level"]);
192
+ for (const [nsText, text] of stream.values) {
193
+ let json;
194
+ try {
195
+ json = JSON.parse(text);
196
+ } catch {
197
+ continue;
198
+ }
199
+ const line = LokiLineSchema.safeParse(json);
200
+ if (!line.success) continue;
201
+ const ns = BigInt(nsText);
202
+ const timestamp = Number(ns / MS_NS);
203
+ const tags = {};
204
+ for (const [k, v] of Object.entries(line.data.tags ?? {})) if (typeof v === "string" || typeof v === "number") tags[k] = v;
205
+ const hasTags = Object.keys(tags).length > 0;
206
+ const entry = {
207
+ ...line.data.id !== void 0 ? { id: line.data.id } : {},
208
+ timestamp,
209
+ level,
210
+ ...line.data.scope !== void 0 ? { scope: line.data.scope } : {},
211
+ message: line.data.message,
212
+ ...hasTags ? { tags } : {},
213
+ ...line.data.meta !== void 0 ? { meta: line.data.meta } : {}
214
+ };
215
+ rows.push({
216
+ ns,
217
+ hash: lineHash(text),
218
+ entry,
219
+ line: {
220
+ timestamp,
221
+ level,
222
+ message: line.data.message,
223
+ scope: line.data.scope,
224
+ tags: hasTags ? tags : void 0,
225
+ meta: line.data.meta
226
+ }
227
+ });
228
+ }
229
+ }
230
+ return rows.toSorted((a, b) => a.ns < b.ns ? 1 : a.ns > b.ns ? -1 : 0);
231
+ }
232
+ /**
233
+ * One backward slice over Loki: windows of {@link LOKI_WINDOW_MS}, newest
234
+ * first, until `limit` matches, the horizon, the budget or a cancel.
235
+ */
236
+ async function readLokiBackward(args) {
237
+ const { query } = args;
238
+ const now = args.now ?? Date.now;
239
+ const filter = compileLogFilter(query);
240
+ const limit = query.limit ?? 200;
241
+ const deadline = now() + Math.min(query.budgetMs ?? 2e3, 1e4);
242
+ const logql = buildLogQl(query, args.labels);
243
+ const horizonMs = query.since ?? now() - 30 * 864e5;
244
+ const horizonNs = BigInt(Math.trunc(horizonMs)) * MS_NS;
245
+ const cursor = query.before !== void 0 ? decodeLokiCursor(query.before) : null;
246
+ const untilNs = query.until !== void 0 ? BigInt(Math.trunc(query.until) + 1) * MS_NS : null;
247
+ let endNs = cursor !== null ? BigInt(cursor.n) + 1n : untilNs ?? BigInt(now() + 1) * MS_NS;
248
+ let seenAtBoundary = new Set(cursor?.h ?? []);
249
+ let boundaryNs = cursor !== null ? BigInt(cursor.n) : null;
250
+ const entries = [];
251
+ let bytes = 0;
252
+ while (endNs > horizonNs) {
253
+ if (args.isCancelled()) return {
254
+ entries,
255
+ nextBefore: encodeLokiCursor({
256
+ n: String(endNs - 1n),
257
+ h: [...seenAtBoundary]
258
+ }),
259
+ nextAfter: null,
260
+ scannedBytes: bytes,
261
+ done: false,
262
+ cancelled: true
263
+ };
264
+ const windowStart = endNs - BigInt(LOKI_WINDOW_MS) * MS_NS;
265
+ const startNs = windowStart > horizonNs ? windowStart : horizonNs;
266
+ const want = limit - entries.length + seenAtBoundary.size;
267
+ const { rows, bytes: got } = await args.range({
268
+ logql,
269
+ startNs,
270
+ endNs,
271
+ limit: want
272
+ });
273
+ bytes += got;
274
+ let oldest = null;
275
+ for (const row of rows) {
276
+ if (boundaryNs !== null && row.ns === boundaryNs && seenAtBoundary.has(row.hash)) continue;
277
+ if (oldest === null || row.ns < oldest) {
278
+ if (oldest !== null && row.ns < oldest) seenAtBoundary = /* @__PURE__ */ new Set();
279
+ oldest = row.ns;
280
+ }
281
+ if (row.ns === oldest) seenAtBoundary.add(row.hash);
282
+ boundaryNs = oldest;
283
+ if (!lineMatches(row.line, filter)) continue;
284
+ entries.push(row.entry);
285
+ if (entries.length >= limit) return {
286
+ entries,
287
+ nextBefore: encodeLokiCursor({
288
+ n: String(row.ns),
289
+ h: [...seenAtBoundary]
290
+ }),
291
+ nextAfter: null,
292
+ scannedBytes: bytes,
293
+ done: false
294
+ };
295
+ }
296
+ if (rows.length >= want && oldest !== null) endNs = oldest + 1n;
297
+ else {
298
+ endNs = startNs;
299
+ seenAtBoundary = /* @__PURE__ */ new Set();
300
+ boundaryNs = null;
301
+ }
302
+ if (now() >= deadline) break;
303
+ }
304
+ const done = endNs <= horizonNs;
305
+ return {
306
+ entries,
307
+ nextBefore: done ? null : encodeLokiCursor({
308
+ n: String(endNs - 1n),
309
+ h: [...seenAtBoundary]
310
+ }),
311
+ nextAfter: null,
312
+ scannedBytes: bytes,
313
+ done
314
+ };
315
+ }
316
+ //#endregion
89
317
  //#region src/builtins/loki-logging/loki-destination.ts
90
318
  var LEVEL_ORDER = {
91
319
  debug: 0,
@@ -109,6 +337,8 @@ var LokiDestination = class {
109
337
  failures = 0;
110
338
  lastError = null;
111
339
  lastPushAtMs = null;
340
+ /** In-flight history reads, by requestId, so `cancelRead` can abort the fetch. */
341
+ reads = /* @__PURE__ */ new Map();
112
342
  fetchFn;
113
343
  setIntervalFn;
114
344
  clearIntervalFn;
@@ -116,7 +346,7 @@ var LokiDestination = class {
116
346
  this.fetchFn = deps?.fetchFn ?? ((url, init) => globalThis.fetch(url, {
117
347
  method: init.method,
118
348
  headers: { ...init.headers },
119
- body: init.body,
349
+ ...init.body !== void 0 ? { body: init.body } : {},
120
350
  ...init.signal ? { signal: init.signal } : {}
121
351
  }));
122
352
  this.setIntervalFn = deps?.setIntervalFn ?? setInterval;
@@ -208,9 +438,10 @@ var LokiDestination = class {
208
438
  }
209
439
  async push(body, config) {
210
440
  const endpoint = `${config.url.replace(/\/+$/, "")}/loki/api/v1/push`;
211
- const headers = { "content-type": "application/json" };
212
- if (config.tenantId.trim().length > 0) headers["X-Scope-OrgID"] = config.tenantId.trim();
213
- if (config.username.length > 0) headers["authorization"] = `Basic ${Buffer.from(`${config.username}:${config.password}`).toString("base64")}`;
441
+ const headers = {
442
+ "content-type": "application/json",
443
+ ...this.authHeaders(config)
444
+ };
214
445
  const controller = new AbortController();
215
446
  const timeout = setTimeout(() => controller.abort(), Math.max(1e3, config.timeoutMs));
216
447
  try {
@@ -278,8 +509,93 @@ var LokiDestination = class {
278
509
  };
279
510
  }
280
511
  }
281
- async query(_filter) {
282
- return [];
512
+ /** Headers every call to this Loki carries: tenant + basic auth. */
513
+ authHeaders(config) {
514
+ const headers = {};
515
+ if (config.tenantId.trim().length > 0) headers["X-Scope-OrgID"] = config.tenantId.trim();
516
+ if (config.username.length > 0) headers["authorization"] = `Basic ${Buffer.from(`${config.username}:${config.password}`).toString("base64")}`;
517
+ return headers;
518
+ }
519
+ /**
520
+ * Readable when it is configured (D898): the same URL and credentials the
521
+ * push uses answer `query_range`. Labels are `job`/`node`/`level`, so those
522
+ * filter at the index; device, addon and text are line filters.
523
+ */
524
+ async describeRead() {
525
+ const configured = this.isActive();
526
+ return {
527
+ readable: configured,
528
+ reason: configured ? null : "Loki is not enabled or has no URL",
529
+ filters: {
530
+ deviceId: true,
531
+ addonId: true,
532
+ nodeId: true,
533
+ tags: true,
534
+ level: true,
535
+ text: true,
536
+ timeRange: true
537
+ },
538
+ retentionDays: null
539
+ };
540
+ }
541
+ /** One backward slice from `query_range`, newest first. Never logs. */
542
+ async read(query) {
543
+ const config = this.config;
544
+ if (config === null || !this.isActive()) throw new Error("Loki is not enabled or has no URL");
545
+ const controller = new AbortController();
546
+ const requestId = query.requestId;
547
+ if (requestId !== void 0) this.reads.set(requestId, controller);
548
+ const base = config.url.replace(/\/+$/, "");
549
+ const range = async ({ logql, startNs, endNs, limit }) => {
550
+ const params = new URLSearchParams({
551
+ query: logql,
552
+ start: String(startNs),
553
+ end: String(endNs),
554
+ limit: String(limit),
555
+ direction: "backward"
556
+ });
557
+ const timeout = setTimeout(() => controller.abort(), Math.max(1e3, config.timeoutMs));
558
+ try {
559
+ const response = await this.fetchFn(`${base}/loki/api/v1/query_range?${params.toString()}`, {
560
+ method: "GET",
561
+ headers: this.authHeaders(config),
562
+ signal: controller.signal
563
+ });
564
+ const text = await response.text();
565
+ if (!response.ok) throw new Error(`Loki query failed: ${String(response.status)}${text.length > 0 ? ` — ${text.slice(0, 200)}` : ""}`);
566
+ return {
567
+ rows: parseQueryRange(JSON.parse(text)),
568
+ bytes: Buffer.byteLength(text)
569
+ };
570
+ } finally {
571
+ clearTimeout(timeout);
572
+ }
573
+ };
574
+ try {
575
+ return await readLokiBackward({
576
+ query,
577
+ labels: { job: config.job },
578
+ range,
579
+ isCancelled: () => controller.signal.aborted
580
+ });
581
+ } catch (error) {
582
+ if (controller.signal.aborted) return {
583
+ entries: [],
584
+ nextBefore: query.before ?? null,
585
+ nextAfter: null,
586
+ scannedBytes: 0,
587
+ done: false,
588
+ cancelled: true
589
+ };
590
+ throw error;
591
+ } finally {
592
+ if (requestId !== void 0) this.reads.delete(requestId);
593
+ }
594
+ }
595
+ async cancelRead(input) {
596
+ const controller = this.reads.get(input.requestId);
597
+ controller?.abort();
598
+ return { cancelled: controller !== void 0 };
283
599
  }
284
600
  async shutdown() {
285
601
  this.stopTimer();
@@ -24,7 +24,7 @@
24
24
  * `MAX_QUEUE` and drops OLDEST-first when full (a burst during an incident is
25
25
  * exactly when the newest lines matter most). Drops are counted and surfaced.
26
26
  */
27
- import type { ILogDestination, LogEntry, LogFilter, LogLevel } from "@camstack/types";
27
+ import type { ILogDestination, LogEntry, LogLevel, LogReadCapabilities, LogReadQuery, LogReadResult } from "@camstack/types";
28
28
  export interface LokiConfig {
29
29
  readonly enabled: boolean;
30
30
  /** Base URL of the Loki instance, e.g. `http://loki:3100`. */
@@ -59,7 +59,8 @@ export interface LokiStatus {
59
59
  export type LokiFetch = (url: string, init: {
60
60
  readonly method: string;
61
61
  readonly headers: Readonly<Record<string, string>>;
62
- readonly body: string;
62
+ /** Absent on a GET (the history read). */
63
+ readonly body?: string;
63
64
  readonly signal?: AbortSignal;
64
65
  }) => Promise<{
65
66
  readonly ok: boolean;
@@ -81,6 +82,8 @@ export declare class LokiDestination implements ILogDestination {
81
82
  private failures;
82
83
  private lastError;
83
84
  private lastPushAtMs;
85
+ /** In-flight history reads, by requestId, so `cancelRead` can abort the fetch. */
86
+ private readonly reads;
84
87
  private readonly fetchFn;
85
88
  private readonly setIntervalFn;
86
89
  private readonly clearIntervalFn;
@@ -120,6 +123,20 @@ export declare class LokiDestination implements ILogDestination {
120
123
  ok: boolean;
121
124
  error: string | null;
122
125
  }>;
123
- query(_filter: LogFilter): Promise<readonly LogEntry[]>;
126
+ /** Headers every call to this Loki carries: tenant + basic auth. */
127
+ private authHeaders;
128
+ /**
129
+ * Readable when it is configured (D898): the same URL and credentials the
130
+ * push uses answer `query_range`. Labels are `job`/`node`/`level`, so those
131
+ * filter at the index; device, addon and text are line filters.
132
+ */
133
+ describeRead(): Promise<LogReadCapabilities>;
134
+ /** One backward slice from `query_range`, newest first. Never logs. */
135
+ read(query: LogReadQuery): Promise<LogReadResult>;
136
+ cancelRead(input: {
137
+ readonly requestId: string;
138
+ }): Promise<{
139
+ readonly cancelled: boolean;
140
+ }>;
124
141
  shutdown(): Promise<void>;
125
142
  }
@@ -0,0 +1,51 @@
1
+ import type { LogReadEntry, LogReadQuery, LogReadResult } from "@camstack/types";
2
+ import { type ReadableLine } from "../../logging/log-read-filter.js";
3
+ /** How far back a read may go when the query sets no `since`. */
4
+ export declare const LOKI_READ_HORIZON_DAYS = 30;
5
+ /** One query_range window. A busy stream fills `limit` long before this. */
6
+ export declare const LOKI_WINDOW_MS: number;
7
+ /** Where a Loki page stopped. */
8
+ export interface LokiCursor {
9
+ /** Nanosecond epoch, decimal string. */
10
+ readonly n: string;
11
+ /** Hashes of the lines already returned at exactly `n`. */
12
+ readonly h: string[];
13
+ }
14
+ export declare function encodeLokiCursor(cursor: LokiCursor): string;
15
+ export declare function decodeLokiCursor(raw: string): LokiCursor | null;
16
+ /** FNV-1a, 32 bit, hex — a dedupe key, not a security property. */
17
+ export declare function lineHash(line: string): string;
18
+ export interface LokiQueryLabels {
19
+ readonly job: string;
20
+ }
21
+ /** The LogQL for a read. Pure. */
22
+ export declare function buildLogQl(query: LogReadQuery, labels: LokiQueryLabels): string;
23
+ export interface LokiRow {
24
+ readonly ns: bigint;
25
+ readonly hash: string;
26
+ readonly entry: LogReadEntry;
27
+ readonly line: ReadableLine;
28
+ }
29
+ /** A query_range body → rows, newest first. Lines that are not ours are dropped. */
30
+ export declare function parseQueryRange(body: unknown): LokiRow[];
31
+ /** One query_range call: newest-first rows in [startNs, endNs). */
32
+ export type LokiRangeQuery = (args: {
33
+ readonly logql: string;
34
+ readonly startNs: bigint;
35
+ readonly endNs: bigint;
36
+ readonly limit: number;
37
+ }) => Promise<{
38
+ readonly rows: LokiRow[];
39
+ readonly bytes: number;
40
+ }>;
41
+ /**
42
+ * One backward slice over Loki: windows of {@link LOKI_WINDOW_MS}, newest
43
+ * first, until `limit` matches, the horizon, the budget or a cancel.
44
+ */
45
+ export declare function readLokiBackward(args: {
46
+ readonly query: LogReadQuery;
47
+ readonly labels: LokiQueryLabels;
48
+ readonly range: LokiRangeQuery;
49
+ readonly isCancelled: () => boolean;
50
+ readonly now?: () => number;
51
+ }): Promise<LogReadResult>;