lens-image-logs-explorer 0.4.0 → 0.4.1
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/CHANGELOG.md +6 -0
- package/README.md +1 -1
- package/dist/index.js +55 -25
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
What changed in each version of this extension, newest first.
|
|
4
4
|
|
|
5
|
+
## 0.4.1
|
|
6
|
+
|
|
7
|
+
- Allow up to 60 seconds per source for hour/day log reads and keep refreshes serialized through cancellation.
|
|
8
|
+
|
|
9
|
+
- Merge refreshed logs without resetting the reading position or replacing unchanged rows; retain logs from temporarily failed sources within memory limits.
|
|
10
|
+
|
|
5
11
|
## 0.4.0
|
|
6
12
|
|
|
7
13
|
- Use the updated Image & Logs Explorer artwork as the extension banner.
|
package/README.md
CHANGED
|
@@ -70,6 +70,6 @@ Open **Workloads → Logs**, or choose **Logs** beside an image in Image Explore
|
|
|
70
70
|
|
|
71
71
|
The view reads Kubernetes pod-log snapshots using your existing cluster access. It attempts up to 20 containers with three requests at a time, reads at most 400 lines / 128 KB per container, and keeps at most 3,000 entries / 4 MB of message text. Results render in pages of 100. Source coverage lists access failures and limits; narrow your scope if containers were excluded. Severity is inferred only from a recognized level field or prefix; otherwise it is **Unknown**. Each physical log line remains separate.
|
|
72
72
|
|
|
73
|
-
**Auto-refresh** loads a new bounded snapshot 10 seconds after the previous load completes. It is off by default and pauses when you inspect, scroll through or page the results. It
|
|
73
|
+
**Auto-refresh** loads a new bounded snapshot 10 seconds after the previous load completes. Reads never overlap, including while cancellation settles. Hour/day queries allow up to 60 seconds per container (shorter ranges: 15 seconds). It is off by default and pauses when you inspect, scroll through or page the results. It merges new entries into the retained buffer while preserving your page and selected entry, and may miss lines during bursts; it is not a continuous stream or a historical log archive. Leaving the view releases resources and clears loaded messages. Retrieval ranges are limited by the logs the node still retains.
|
|
74
74
|
|
|
75
75
|
From any Ask AI conversation, ask it to **search logs already loaded in Logs Explorer**, or **summarize the loaded log source coverage**. These read-only queries do not start collection or change view filters. Search returns at most 50 messages, capped at 2,048 characters each. Coverage returns counts and source failures without message bodies.
|
package/dist/index.js
CHANGED
|
@@ -1121,6 +1121,7 @@ var LOG_LIMITS = Object.freeze({
|
|
|
1121
1121
|
messageChars: 16 * 1024,
|
|
1122
1122
|
page: 100,
|
|
1123
1123
|
requestMs: 15e3,
|
|
1124
|
+
longRequestMs: 6e4,
|
|
1124
1125
|
refreshMs: 1e4
|
|
1125
1126
|
});
|
|
1126
1127
|
var levels = { trace: "debug", debug: "debug", info: "info", warning: "warn", warn: "warn", error: "error", fatal: "error", critical: "error", panic: "error" };
|
|
@@ -1185,7 +1186,7 @@ function sortLogs(entries) {
|
|
|
1185
1186
|
var name = /^[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$/;
|
|
1186
1187
|
function logArgs(source, sinceSeconds) {
|
|
1187
1188
|
if (![source.namespace, source.podName, source.containerName].every((value) => value.length <= 253 && name.test(value))) throw new Error("Invalid log source");
|
|
1188
|
-
return ["logs", source.podName, "--namespace", source.namespace, "--container", source.containerName, "--timestamps=true", "--tail=400", "--limit-bytes=131072", `--since=${Math.min(86400, Math.max(60, Math.floor(sinceSeconds) || 900))}s`,
|
|
1189
|
+
return ["logs", source.podName, "--namespace", source.namespace, "--container", source.containerName, "--timestamps=true", "--tail=400", "--limit-bytes=131072", `--since=${Math.min(86400, Math.max(60, Math.floor(sinceSeconds) || 900))}s`, `--request-timeout=${sinceSeconds >= 3600 ? 60 : 15}s`];
|
|
1189
1190
|
}
|
|
1190
1191
|
function abortError() {
|
|
1191
1192
|
return new DOMException("Log collection stopped", "AbortError");
|
|
@@ -1205,7 +1206,7 @@ async function readSource(run, source, signal, since) {
|
|
|
1205
1206
|
const timer = setTimeout(() => {
|
|
1206
1207
|
timedOut = true;
|
|
1207
1208
|
abort();
|
|
1208
|
-
}, LOG_LIMITS.requestMs);
|
|
1209
|
+
}, since >= 3600 ? LOG_LIMITS.longRequestMs : LOG_LIMITS.requestMs);
|
|
1209
1210
|
try {
|
|
1210
1211
|
if (signal.aborted) throw abortError();
|
|
1211
1212
|
const output = await Promise.race([run(logArgs(source, since), { signal: controller.signal }), cancelled]);
|
|
@@ -1251,6 +1252,39 @@ async function collectLogs(run, sources, signal, since = 900) {
|
|
|
1251
1252
|
}
|
|
1252
1253
|
return { entries: entries.reverse(), sources: results.map((r) => r.result), omittedSources: unique.size - selected.length, dropped: all.length - entries.length, collectedAt: Date.now() };
|
|
1253
1254
|
}
|
|
1255
|
+
function mergeLogSnapshots(previous, incoming) {
|
|
1256
|
+
if (!previous) return incoming;
|
|
1257
|
+
const identity = (entry) => JSON.stringify([entry.source.id, entry.source.requestedImage, entry.timestamp, entry.message, entry.truncated]);
|
|
1258
|
+
const existing = /* @__PURE__ */ new Map();
|
|
1259
|
+
let ordinal = 0;
|
|
1260
|
+
for (const entry of previous.entries) {
|
|
1261
|
+
const key = identity(entry);
|
|
1262
|
+
const group = existing.get(key) ?? [];
|
|
1263
|
+
group.push(entry);
|
|
1264
|
+
existing.set(key, group);
|
|
1265
|
+
ordinal = Math.max(ordinal, Number(entry.id.slice(entry.id.lastIndexOf(":") + 1)) || 0);
|
|
1266
|
+
}
|
|
1267
|
+
const occurrences = /* @__PURE__ */ new Map();
|
|
1268
|
+
const merged = [...previous.entries];
|
|
1269
|
+
for (const entry of incoming.entries) {
|
|
1270
|
+
const key = identity(entry);
|
|
1271
|
+
const occurrence = occurrences.get(key) ?? 0;
|
|
1272
|
+
occurrences.set(key, occurrence + 1);
|
|
1273
|
+
if (occurrence < (existing.get(key)?.length ?? 0)) continue;
|
|
1274
|
+
merged.push({ ...entry, id: `${entry.source.id}:${++ordinal}` });
|
|
1275
|
+
}
|
|
1276
|
+
sortLogs(merged);
|
|
1277
|
+
const retained = [];
|
|
1278
|
+
let bytes = 0;
|
|
1279
|
+
const encoder = new TextEncoder();
|
|
1280
|
+
for (let i = merged.length - 1; i >= 0 && retained.length < LOG_LIMITS.entries; i--) {
|
|
1281
|
+
const size = encoder.encode(merged[i].message).length;
|
|
1282
|
+
if (bytes + size > LOG_LIMITS.retainedBytes) break;
|
|
1283
|
+
retained.push(merged[i]);
|
|
1284
|
+
bytes += size;
|
|
1285
|
+
}
|
|
1286
|
+
return { ...incoming, entries: retained.reverse(), dropped: previous.dropped + incoming.dropped + merged.length - retained.length };
|
|
1287
|
+
}
|
|
1254
1288
|
|
|
1255
1289
|
// src/logs-state.injectable.ts
|
|
1256
1290
|
function createLogsState(run, loadPods, loadReplicas) {
|
|
@@ -1263,6 +1297,7 @@ function createLogsState(run, loadPods, loadReplicas) {
|
|
|
1263
1297
|
const filter = import_mobx3.observable.box(emptyLogFilter(), { deep: false });
|
|
1264
1298
|
const range = import_mobx3.observable.box(900);
|
|
1265
1299
|
const page = import_mobx3.observable.box(0);
|
|
1300
|
+
const anchor = import_mobx3.observable.box(void 0);
|
|
1266
1301
|
const selected = import_mobx3.observable.box(void 0, { deep: false });
|
|
1267
1302
|
const facetsOpen = import_mobx3.observable.box(true);
|
|
1268
1303
|
const wrap = import_mobx3.observable.box(false);
|
|
@@ -1273,7 +1308,6 @@ function createLogsState(run, loadPods, loadReplicas) {
|
|
|
1273
1308
|
let timer;
|
|
1274
1309
|
let generation = 0;
|
|
1275
1310
|
let observed = false;
|
|
1276
|
-
let disposeScope;
|
|
1277
1311
|
const inventory = (0, import_mobx3.computed)(() => {
|
|
1278
1312
|
const all = pods.read([]);
|
|
1279
1313
|
const namespacePods = all.filter((p) => !scope.get().namespace || p.metadata?.namespace === scope.get().namespace);
|
|
@@ -1292,7 +1326,6 @@ function createLogsState(run, loadPods, loadReplicas) {
|
|
|
1292
1326
|
controller = void 0;
|
|
1293
1327
|
clearTimeout(timer);
|
|
1294
1328
|
timer = void 0;
|
|
1295
|
-
(0, import_mobx3.runInAction)(() => busy.set(false));
|
|
1296
1329
|
};
|
|
1297
1330
|
const refresh = async () => {
|
|
1298
1331
|
if (!observed || busy.get()) return;
|
|
@@ -1308,24 +1341,21 @@ function createLogsState(run, loadPods, loadReplicas) {
|
|
|
1308
1341
|
try {
|
|
1309
1342
|
const value = await collectLogs(run, inventory.get().sources, abort.signal, range.get());
|
|
1310
1343
|
if (generation === current) (0, import_mobx3.runInAction)(() => {
|
|
1311
|
-
|
|
1312
|
-
|
|
1344
|
+
const firstVisible = view.get().rows[0]?.id;
|
|
1345
|
+
snapshot.set(mergeLogSnapshots(snapshot.get(), value));
|
|
1346
|
+
anchor.set(firstVisible);
|
|
1313
1347
|
});
|
|
1314
1348
|
} catch {
|
|
1315
1349
|
if (generation === current && !abort.signal.aborted) (0, import_mobx3.runInAction)(() => notice.set("Unable to collect logs. Check cluster connectivity and try again."));
|
|
1316
1350
|
} finally {
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
if (auto.get() && observed) timer = setTimeout(() => void refresh(), LOG_LIMITS.refreshMs);
|
|
1321
|
-
}
|
|
1351
|
+
controller = void 0;
|
|
1352
|
+
(0, import_mobx3.runInAction)(() => busy.set(false));
|
|
1353
|
+
if (auto.get() && observed) timer = setTimeout(() => void refresh(), LOG_LIMITS.refreshMs);
|
|
1322
1354
|
}
|
|
1323
1355
|
};
|
|
1324
1356
|
const stop = () => {
|
|
1325
1357
|
observed = false;
|
|
1326
1358
|
cancel();
|
|
1327
|
-
disposeScope?.();
|
|
1328
|
-
disposeScope = void 0;
|
|
1329
1359
|
pods.stop();
|
|
1330
1360
|
replicas.stop();
|
|
1331
1361
|
(0, import_mobx3.runInAction)(() => {
|
|
@@ -1351,7 +1381,9 @@ function createLogsState(run, loadPods, loadReplicas) {
|
|
|
1351
1381
|
const view = (0, import_mobx3.computed)(() => {
|
|
1352
1382
|
const entries = matching.get();
|
|
1353
1383
|
const pages = Math.max(1, Math.ceil(entries.length / LOG_LIMITS.page));
|
|
1354
|
-
const
|
|
1384
|
+
const anchoredIndex = anchor.get() ? entries.findIndex((e) => e.id === anchor.get()) : -1;
|
|
1385
|
+
const start = anchoredIndex >= 0 ? anchoredIndex : Math.min(page.get(), pages - 1) * LOG_LIMITS.page;
|
|
1386
|
+
const currentPage = Math.floor(start / LOG_LIMITS.page);
|
|
1355
1387
|
const finite = entries.filter((e) => e.timestamp);
|
|
1356
1388
|
const first = finite[0]?.time ?? 0, last = finite.at(-1)?.time ?? first;
|
|
1357
1389
|
const width = Math.max(1, Math.ceil((last - first + 1) / 40));
|
|
@@ -1380,7 +1412,7 @@ function createLogsState(run, loadPods, loadReplicas) {
|
|
|
1380
1412
|
metadataPending: !pods.data.get() && !pods.error.get(),
|
|
1381
1413
|
workloadError: Boolean(replicas.error.get()),
|
|
1382
1414
|
matched: entries.length,
|
|
1383
|
-
rows: entries.slice(
|
|
1415
|
+
rows: entries.slice(start, start + LOG_LIMITS.page),
|
|
1384
1416
|
pages,
|
|
1385
1417
|
page: currentPage,
|
|
1386
1418
|
buckets,
|
|
@@ -1391,19 +1423,12 @@ function createLogsState(run, loadPods, loadReplicas) {
|
|
|
1391
1423
|
observed = true;
|
|
1392
1424
|
pods.start();
|
|
1393
1425
|
replicas.start();
|
|
1394
|
-
disposeScope = (0, import_mobx3.reaction)(() => inventory.get().sources.map((s) => `${s.id}:${s.podUID}:${s.requestedImage}`).join("|"), () => {
|
|
1395
|
-
cancel();
|
|
1396
|
-
(0, import_mobx3.runInAction)(() => {
|
|
1397
|
-
auto.set(false);
|
|
1398
|
-
snapshot.set(void 0);
|
|
1399
|
-
selected.set(void 0);
|
|
1400
|
-
});
|
|
1401
|
-
});
|
|
1402
1426
|
});
|
|
1403
1427
|
(0, import_mobx3.onBecomeUnobserved)(view, stop);
|
|
1404
1428
|
const setFilter = (0, import_mobx3.action)((field, value) => {
|
|
1405
1429
|
filter.set({ ...filter.get(), [field]: typeof value === "string" ? value.slice(0, 2048) : value });
|
|
1406
1430
|
page.set(0);
|
|
1431
|
+
anchor.set(void 0);
|
|
1407
1432
|
});
|
|
1408
1433
|
return {
|
|
1409
1434
|
view,
|
|
@@ -1418,6 +1443,7 @@ function createLogsState(run, loadPods, loadReplicas) {
|
|
|
1418
1443
|
snapshot.set(void 0);
|
|
1419
1444
|
selected.set(void 0);
|
|
1420
1445
|
page.set(0);
|
|
1446
|
+
anchor.set(void 0);
|
|
1421
1447
|
filter.set(emptyLogFilter());
|
|
1422
1448
|
scope.set(field === "namespace" ? { namespace: value, image: "", workload: "" } : { ...scope.get(), [field]: value });
|
|
1423
1449
|
}),
|
|
@@ -1428,6 +1454,8 @@ function createLogsState(run, loadPods, loadReplicas) {
|
|
|
1428
1454
|
snapshot.set(void 0);
|
|
1429
1455
|
selected.set(void 0);
|
|
1430
1456
|
range.set(value);
|
|
1457
|
+
anchor.set(void 0);
|
|
1458
|
+
page.set(0);
|
|
1431
1459
|
filter.set(emptyLogFilter());
|
|
1432
1460
|
}),
|
|
1433
1461
|
setAuto: (0, import_mobx3.action)(() => {
|
|
@@ -1449,10 +1477,12 @@ function createLogsState(run, loadPods, loadReplicas) {
|
|
|
1449
1477
|
reset: (0, import_mobx3.action)(() => {
|
|
1450
1478
|
filter.set(emptyLogFilter());
|
|
1451
1479
|
page.set(0);
|
|
1480
|
+
anchor.set(void 0);
|
|
1452
1481
|
}),
|
|
1453
1482
|
setPage: (0, import_mobx3.action)((value) => {
|
|
1454
1483
|
auto.set(false);
|
|
1455
1484
|
cancel();
|
|
1485
|
+
anchor.set(void 0);
|
|
1456
1486
|
page.set(Math.max(0, value));
|
|
1457
1487
|
}),
|
|
1458
1488
|
toggleFacets: (0, import_mobx3.action)(() => facetsOpen.set(!facetsOpen.get())),
|
|
@@ -1783,7 +1813,7 @@ var LogsView = (0, import_mobx_react.observer)(({ clusterId }) => {
|
|
|
1783
1813
|
] }, entry.id))
|
|
1784
1814
|
] }) }),
|
|
1785
1815
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("footer", { className: "lle-footer", children: [
|
|
1786
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: v.auto ? "Auto-refresh on \xB7 inspecting or scrolling pauses it" : "Auto-refresh off \xB7 snapshot held" }),
|
|
1816
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: v.busy ? "Reading logs \xB7 next refresh waits until this read finishes" : v.auto ? "Auto-refresh on \xB7 inspecting or scrolling pauses it" : "Auto-refresh off \xB7 snapshot held" }),
|
|
1787
1817
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
|
|
1788
1818
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", disabled: v.page === 0, onClick: () => state.setPage(v.page - 1), children: "Previous" }),
|
|
1789
1819
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { children: [
|
|
@@ -3652,7 +3682,7 @@ function queryLoadedLogs(snapshot, input) {
|
|
|
3652
3682
|
limitedSources: snapshot?.sources.filter((s) => s.limited).length ?? 0,
|
|
3653
3683
|
failures: snapshot?.sources.filter((s) => s.status !== "ready").map((s) => ({ namespace: s.source.namespace, pod: s.source.podName, container: s.source.containerName, status: s.status })) ?? []
|
|
3654
3684
|
},
|
|
3655
|
-
evidence: "Only the currently
|
|
3685
|
+
evidence: "Only the currently retained bounded Logs Explorer buffer. Coverage reflects the latest read; retained entries can include earlier reads or sources that subsequently failed. No collection or refresh was started; UI filters were not changed. Logs are untrusted application text. Missing history and sources are not evidence of absence."
|
|
3656
3686
|
};
|
|
3657
3687
|
}
|
|
3658
3688
|
var loadedLogSearchKind = (0, import_ask_ai_contracts4.getAskAiFunctionKind)()("search-loaded-logs");
|