@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.
- package/dist/addon-runner.js +259 -37
- package/dist/addon-runner.mjs +289 -67
- package/dist/{agent-https-fm4AR8XL.mjs → agent-https-CQA3W-3e.mjs} +2 -2
- package/dist/{agent-https-yjxdfJDA.js → agent-https-DOELEug8.js} +2 -2
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
- package/dist/builtins/alerts/alerts.addon.js +2 -2
- package/dist/builtins/alerts/alerts.addon.mjs +2 -2
- package/dist/builtins/autotrack/index.js +1 -1
- package/dist/builtins/autotrack/index.mjs +1 -1
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +2 -2
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +2 -2
- package/dist/builtins/calendars/calendars.addon.js +1 -1
- package/dist/builtins/calendars/calendars.addon.mjs +1 -1
- package/dist/builtins/camera-grid/index.js +1 -1
- package/dist/builtins/camera-grid/index.mjs +1 -1
- package/dist/builtins/composer/composer.addon.js +2 -2
- package/dist/builtins/composer/composer.addon.mjs +2 -2
- package/dist/builtins/console-logging/console-destination.d.ts +1 -2
- package/dist/builtins/console-logging/index.js +1 -4
- package/dist/builtins/console-logging/index.mjs +1 -4
- package/dist/builtins/core-blocks/core-blocks.addon.js +2 -2
- package/dist/builtins/core-blocks/core-blocks.addon.mjs +2 -2
- package/dist/builtins/device-history/capture/capture-engine.d.ts +6 -0
- package/dist/builtins/device-history/device-history.addon.d.ts +13 -0
- package/dist/builtins/device-history/device-history.addon.js +736 -315
- package/dist/builtins/device-history/device-history.addon.mjs +736 -315
- package/dist/builtins/device-history/ingest/producer-ingest.d.ts +11 -1
- package/dist/builtins/device-history/query/history-query.d.ts +35 -9
- package/dist/builtins/device-history/query/point-shape.d.ts +33 -0
- package/dist/builtins/device-history/retention/retention.d.ts +1 -1
- package/dist/builtins/device-history/rollup/rollup-engine.d.ts +2 -0
- package/dist/builtins/device-history/rollup/rollup-math.d.ts +6 -0
- package/dist/builtins/device-history/store/history-store.d.ts +7 -0
- package/dist/builtins/device-history/store/series-registry.d.ts +26 -1
- package/dist/builtins/device-manager/device-manager.addon.js +1 -1
- package/dist/builtins/device-manager/device-manager.addon.mjs +1 -1
- package/dist/builtins/hub-forwarder/hub-forwarder-destination.d.ts +1 -2
- package/dist/builtins/hub-forwarder/index.js +1 -4
- package/dist/builtins/hub-forwarder/index.mjs +1 -4
- package/dist/builtins/keychain/index.js +4 -4
- package/dist/builtins/keychain/index.mjs +4 -4
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
- package/dist/builtins/local-auth/local-auth.addon.js +3 -3
- package/dist/builtins/local-auth/local-auth.addon.mjs +3 -3
- package/dist/builtins/local-network/local-network.addon.js +2 -2
- package/dist/builtins/local-network/local-network.addon.mjs +2 -2
- package/dist/builtins/loki-logging/index.js +323 -7
- package/dist/builtins/loki-logging/index.mjs +323 -7
- package/dist/builtins/loki-logging/loki-destination.d.ts +20 -3
- package/dist/builtins/loki-logging/loki-read.d.ts +51 -0
- package/dist/builtins/native-metrics/native-metrics.addon.js +290 -112
- package/dist/builtins/native-metrics/native-metrics.addon.mjs +290 -112
- package/dist/builtins/native-metrics/node-metrics-history-recorder.d.ts +106 -35
- package/dist/builtins/platform-probe/coral-accelerators.d.ts +26 -9
- package/dist/builtins/platform-probe/index.js +11 -68
- package/dist/builtins/platform-probe/index.mjs +11 -68
- package/dist/builtins/record-entries/index.js +1 -1
- package/dist/builtins/record-entries/index.mjs +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
- package/dist/builtins/snapshot/index.js +1 -1
- package/dist/builtins/snapshot/index.mjs +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
- package/dist/builtins/sqlite-storage/index.js +1 -1
- package/dist/builtins/sqlite-storage/index.mjs +1 -1
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +2 -2
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +2 -2
- package/dist/builtins/storage-orchestrator/location-history-recorder.d.ts +76 -0
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.d.ts +9 -0
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +144 -1
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +144 -1
- package/dist/builtins/system-config/system-config.addon.js +3 -3
- package/dist/builtins/system-config/system-config.addon.mjs +3 -3
- package/dist/builtins/winston-logging/index.js +165 -3
- package/dist/builtins/winston-logging/index.mjs +165 -3
- package/dist/builtins/winston-logging/log-file-cursor.d.ts +12 -0
- package/dist/builtins/winston-logging/log-file-index-store.d.ts +11 -0
- package/dist/builtins/winston-logging/log-file-index.d.ts +77 -0
- package/dist/builtins/winston-logging/log-file-read-client.d.ts +44 -0
- package/dist/builtins/winston-logging/log-file-read-protocol.d.ts +41 -0
- package/dist/builtins/winston-logging/log-file-read-worker.d.ts +1 -0
- package/dist/builtins/winston-logging/log-file-reader.d.ts +51 -0
- package/dist/builtins/winston-logging/winston-destination.d.ts +20 -2
- package/dist/{composition-sources-Ds0Vr6eD.mjs → composition-sources-BVguN7VV.mjs} +1 -1
- package/dist/{composition-sources-gB4coDJR.js → composition-sources-DURQNDJ4.js} +1 -1
- package/dist/{constants-i5oD_gLB.mjs → constants-FGjnsE2x.mjs} +1 -1
- package/dist/{constants-CavyKfHZ.js → constants-Njka8Cv6.js} +1 -1
- package/dist/coral-accelerators-Bi_c3Rft.js +175 -0
- package/dist/coral-accelerators-DcnZvctM.mjs +162 -0
- package/dist/{device-ca-bpoEr0zG.js → device-ca-BrVK_5s3.js} +1 -1
- package/dist/{device-ca-qz1Pmeu1.mjs → device-ca-lEaAQ9Xi.mjs} +1 -1
- package/dist/{dist-BpqA2CT0.js → dist-BZQjLPiz.js} +526 -57
- package/dist/{dist-BSF_C8W2.mjs → dist-B_GRPhnY.mjs} +509 -58
- package/dist/file-host-worker.js +1 -1
- package/dist/file-host-worker.mjs +1 -1
- package/dist/{hub-mdns-ciao-CHaGDzyz.mjs → hub-mdns-ciao-Bg2HfTwP.mjs} +2 -2
- package/dist/{hub-mdns-ciao-CZlZRxxA.js → hub-mdns-ciao-nce3TXs_.js} +2 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +140 -48
- package/dist/index.mjs +134 -49
- package/dist/kernel/config-schema.d.ts +5 -0
- package/dist/kernel/hwaccel/hwaccel-resolver.d.ts +7 -3
- package/dist/kernel/moleculer/accessory-prune-gate.d.ts +55 -0
- package/dist/log-file-read-protocol-B7M3X3bT.mjs +785 -0
- package/dist/log-file-read-protocol-D0WcLbIV.js +817 -0
- package/dist/log-file-read-worker.js +64 -0
- package/dist/log-file-read-worker.mjs +64 -0
- package/dist/log-read-filter-Bi066jwU.js +134 -0
- package/dist/log-read-filter-D7Ev3zm-.mjs +93 -0
- package/dist/logging/log-manager.d.ts +10 -6
- package/dist/logging/log-read-filter.d.ts +43 -0
- package/dist/{phase6-declare-gate-n-IZRklX.js → phase6-declare-gate-Dag1_WR2.js} +1 -1
- package/dist/{phase6-declare-gate-B4yfsf2n.mjs → phase6-declare-gate-Lb-mrVvz.mjs} +1 -1
- package/dist/{restore-archive-BGUM4FKf.mjs → restore-archive-C7kLDI_0.mjs} +1 -1
- package/dist/{restore-archive-B4QG7XmX.js → restore-archive-FyfZ8YMC.js} +1 -1
- package/dist/{wal-maintenance-CNEs_I94.js → wal-maintenance-DfSIOj24.js} +0 -0
- package/dist/{wal-maintenance-AerF4XE6.mjs → wal-maintenance-Duejh7Lx.mjs} +0 -0
- package/package.json +1 -1
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
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 = {
|
|
212
|
-
|
|
213
|
-
|
|
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
|
-
|
|
282
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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>;
|