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 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 replaces the previous snapshot 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.
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`, "--request-timeout=15s"];
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
- snapshot.set(value);
1312
- page.set(0);
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
- if (generation === current) {
1318
- controller = void 0;
1319
- (0, import_mobx3.runInAction)(() => busy.set(false));
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 currentPage = Math.min(page.get(), pages - 1);
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(currentPage * LOG_LIMITS.page, (currentPage + 1) * LOG_LIMITS.page),
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 loaded bounded Logs Explorer snapshot. 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."
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");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lens-image-logs-explorer",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Inspect container images and investigate bounded, consolidated pod logs in Lens.",
5
5
  "keywords": [
6
6
  "lens-extension"