@kinetica/admin-agent 0.2.2 → 0.2.3

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/README.md CHANGED
@@ -332,14 +332,14 @@ The `--bundle` flag points the agent at an **extracted** support-bundle director
332
332
 
333
333
  Available against an extracted `gpudb_sysinfo` support bundle (see [Offline Bundle Mode](#offline-bundle-mode)). All read-only; the search/timeline tools stream and bound their output so a large rank log (tens of MB, hundreds of thousands of lines) never blows up the context.
334
334
 
335
- | Tool | Description |
336
- | ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
337
- | `kinetica_load_bundle` | Attach an extracted bundle directory; without a path it opens a directory picker (a model-supplied path needs operator confirmation) |
338
- | `kinetica_bundle_list_files` | Inventory: detected version, ranks + services present, file counts/sizes by kind, plus a layout-match verdict + per-file confidence for off-shape bundles — call this first |
339
- | `kinetica_bundle_log_timeline` | Per-time-bucket severity counts across ranks (the incident shape) — call before searching |
340
- | `kinetica_bundle_search_logs` | Bounded log search by regex, min-severity, time window, and rank / host-manager / component (reads both rolling and Loki-export logs) |
341
- | `kinetica_bundle_read_config` | Read the bundle's real on-disk `gpudb.conf`, with optional section/key filter |
342
- | `kinetica_bundle_read_sysinfo` | OS/process/version diagnostic files (memory, CPU, disk, GPU, network, process args) |
335
+ | Tool | Description |
336
+ | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
337
+ | `kinetica_load_bundle` | Attach an extracted bundle directory; without a path it opens a directory picker (a model-supplied path needs operator confirmation) |
338
+ | `kinetica_bundle_list_files` | Inventory: detected version, ranks + services present, file counts/sizes by kind, plus a layout-match verdict + per-file confidence for off-shape bundles — call this first |
339
+ | `kinetica_bundle_log_timeline` | Per-time-bucket severity counts across ranks (the incident shape) — call before searching |
340
+ | `kinetica_bundle_search_logs` | Bounded log search by regex, min-severity, time window, and rank / host-manager / component (reads both rolling and Loki-export logs); `include_multiline` stitches a multi-line record — e.g. a full `Executing SQL:` query whose embedded newlines span many lines — back onto each match |
341
+ | `kinetica_bundle_read_config` | Read the bundle's real on-disk `gpudb.conf`, with optional section/key filter |
342
+ | `kinetica_bundle_read_sysinfo` | OS/process/version diagnostic files (memory, CPU, disk, GPU, network, process args) |
343
343
 
344
344
  ### Reporting
345
345
 
@@ -4034,6 +4034,8 @@ function parseLogLine(line) {
4034
4034
 
4035
4035
  // src/bundle/log-search.ts
4036
4036
  var DEFAULT_MAX_MATCHES = 200;
4037
+ var MULTILINE_MAX_LINES = 300;
4038
+ var MULTILINE_MAX_CHARS = 2e4;
4037
4039
  var REGEX_SCAN_MAX = 8192;
4038
4040
  var GRANULARITY_LEN = {
4039
4041
  day: 10,
@@ -4075,6 +4077,23 @@ function matchesFilters(parsed, query3, regex, minRank) {
4075
4077
  return false;
4076
4078
  return true;
4077
4079
  }
4080
+ function buildMatch(lineNumber, parsed) {
4081
+ return {
4082
+ lineNumber,
4083
+ ...parsed.timestamp !== void 0 ? { timestamp: parsed.timestamp } : {},
4084
+ ...parsed.severity !== void 0 ? { severity: parsed.severity } : {},
4085
+ ...parsed.rank !== void 0 ? { rank: parsed.rank } : {},
4086
+ message: parsed.message,
4087
+ raw: parsed.raw
4088
+ };
4089
+ }
4090
+ function finalizeMultiline(pending) {
4091
+ if (pending.extra.length === 0) return pending.base;
4092
+ const joined = pending.extra.join("\n");
4093
+ const suffix = pending.truncated ? "\n\u2026 [continuation truncated]" : "";
4094
+ return { ...pending.base, message: `${pending.base.message}
4095
+ ${joined}${suffix}` };
4096
+ }
4078
4097
  async function searchLogFile(filePath, query3) {
4079
4098
  const maxMatches = query3.maxMatches ?? DEFAULT_MAX_MATCHES;
4080
4099
  const minRank = query3.minSeverity !== void 0 ? severityRank(query3.minSeverity) : -Infinity;
@@ -4096,9 +4115,17 @@ async function searchLogFile(filePath, query3) {
4096
4115
  ...query3.fromTs !== void 0 ? { fromTs: floorTimestamp(query3.fromTs) } : {},
4097
4116
  ...query3.toTs !== void 0 ? { toTs: ceilTimestamp(query3.toTs) } : {}
4098
4117
  };
4118
+ const coalesce = query3.coalesceMultiline === true;
4099
4119
  const matches = [];
4100
4120
  let totalMatched = 0;
4101
4121
  let linesScanned = 0;
4122
+ let pending;
4123
+ const flushPending = () => {
4124
+ if (pending) {
4125
+ matches.push(finalizeMultiline(pending));
4126
+ pending = void 0;
4127
+ }
4128
+ };
4102
4129
  try {
4103
4130
  const rl = (0, import_node_readline.createInterface)({
4104
4131
  input: (0, import_node_fs4.createReadStream)(filePath, { encoding: "utf-8" }),
@@ -4107,20 +4134,29 @@ async function searchLogFile(filePath, query3) {
4107
4134
  for await (const line of rl) {
4108
4135
  linesScanned++;
4109
4136
  const parsed = parseLogLine(line);
4137
+ if (pending) {
4138
+ if (parsed.timestamp === void 0) {
4139
+ if (!pending.truncated && pending.extra.length < MULTILINE_MAX_LINES && pending.chars + line.length + 1 <= MULTILINE_MAX_CHARS) {
4140
+ pending.extra.push(line);
4141
+ pending.chars += line.length + 1;
4142
+ } else {
4143
+ pending.truncated = true;
4144
+ }
4145
+ continue;
4146
+ }
4147
+ flushPending();
4148
+ }
4110
4149
  if (!matchesFilters(parsed, boundedQuery, regex, minRank)) continue;
4111
4150
  totalMatched++;
4112
4151
  if (matches.length < maxMatches) {
4113
- matches.push({
4114
- lineNumber: linesScanned,
4115
- ...parsed.timestamp !== void 0 ? { timestamp: parsed.timestamp } : {},
4116
- ...parsed.severity !== void 0 ? { severity: parsed.severity } : {},
4117
- ...parsed.rank !== void 0 ? { rank: parsed.rank } : {},
4118
- message: parsed.message,
4119
- raw: parsed.raw
4120
- });
4152
+ const base = buildMatch(linesScanned, parsed);
4153
+ if (coalesce) pending = { base, extra: [], chars: 0, truncated: false };
4154
+ else matches.push(base);
4121
4155
  }
4122
4156
  }
4157
+ flushPending();
4123
4158
  } catch (err) {
4159
+ flushPending();
4124
4160
  const message = err instanceof Error ? err.message : String(err);
4125
4161
  return {
4126
4162
  matches,
@@ -4508,7 +4544,8 @@ function toLineQuery(q) {
4508
4544
  ...q.minSeverity !== void 0 ? { minSeverity: q.minSeverity } : {},
4509
4545
  ...q.fromTs !== void 0 ? { fromTs: q.fromTs } : {},
4510
4546
  ...q.toTs !== void 0 ? { toTs: q.toTs } : {},
4511
- ...q.maxMatches !== void 0 ? { maxMatches: q.maxMatches } : {}
4547
+ ...q.maxMatches !== void 0 ? { maxMatches: q.maxMatches } : {},
4548
+ ...q.coalesceMultiline !== void 0 ? { coalesceMultiline: q.coalesceMultiline } : {}
4512
4549
  };
4513
4550
  }
4514
4551
  function toTimelineLineQuery(q) {
@@ -4847,6 +4884,9 @@ var BundleSearchLogsSchema = import_zod20.z.object({
4847
4884
  host_manager: import_zod20.z.boolean().describe("Search the host-manager (hm) log \u2014 a singleton service, not a rank.").optional(),
4848
4885
  component: import_zod20.z.string().optional(),
4849
4886
  include_components: import_zod20.z.boolean().optional(),
4887
+ include_multiline: import_zod20.z.boolean().describe(
4888
+ "Reconstruct multi-line log records: append continuation lines (those with no timestamp) to each match. Use this to capture a full SQL statement on an 'Executing SQL:' line \u2014 the query often spans many lines because the SQL has embedded newlines, and a plain match shows only its first line. Works on the rolling core logs (logs-local/); Loki per-rank tails (logs/rankN.log) keep only the statement's first line, so there are no continuation lines to stitch there."
4889
+ ).optional(),
4850
4890
  max_matches: import_zod20.z.number().int().min(1).max(1e3).optional()
4851
4891
  });
4852
4892
  async function bundleSearchLogs(source, args = {}) {
@@ -4859,6 +4899,7 @@ async function bundleSearchLogs(source, args = {}) {
4859
4899
  ...args.host_manager !== void 0 ? { hostManager: args.host_manager } : {},
4860
4900
  ...args.component !== void 0 ? { component: args.component } : {},
4861
4901
  ...args.include_components !== void 0 ? { includeComponents: args.include_components } : {},
4902
+ ...args.include_multiline !== void 0 ? { coalesceMultiline: args.include_multiline } : {},
4862
4903
  ...args.max_matches !== void 0 ? { maxMatches: args.max_matches } : {}
4863
4904
  };
4864
4905
  const result = await source.searchLogs(query3);
@@ -20,6 +20,7 @@ Severity order for filtering is `WARN < UERR < ERROR < FATAL`, so `min_severity=
20
20
 
21
21
  - The logs are large (a rank log can exceed 100k lines). NEVER ask for a whole file. Use `kinetica_bundle_log_timeline` to localize, then `kinetica_bundle_search_logs` with a tight time window + severity to extract only relevant lines. The match cap is shared across files — if you see "capped", narrow the query rather than asking for more.
22
22
  - You can pass a timeline bucket label straight into `from_ts`/`to_ts` (e.g. `2026-06-11 15` searches that whole hour) — partial timestamps are widened to cover the full period.
23
+ - A single log record can span multiple physical lines when a logged value (notably a SQL statement) contains embedded newlines — the continuation lines have no timestamp. A plain search returns only the first line. Pass `include_multiline: true` to stitch the continuation lines back onto each match and recover the whole record. See "Finding a crash's triggering SQL".
23
24
  - Timestamps are plain local strings without a timezone; compare them lexically and treat cross-rank timing cautiously.
24
25
  - **Ranks vs. the host manager:** `rank` selects a numeric rank (`r0`, `r1`, …) only. The host manager (`core-gpudb-rolling-hm.log`) is a singleton service, NOT a rank — search or timeline it with `host_manager: true`, never `rank: "hm"`. By default both `log_timeline` and `search_logs` already cover the host manager along with the numeric ranks; `kinetica_bundle_list_files` lists it under `services_present`.
25
26
 
@@ -29,11 +30,13 @@ When a worker rank segfaults mid-query, that rank's log holds the **backtrace**
29
30
 
30
31
  Workflow, given a `JobId` from a worker's crash stack:
31
32
 
32
- 1. `kinetica_bundle_search_logs` with `rank: "r0"` and `regex` = the JobId. r0 logs the `/execute/sql` receipt (submitting user), the `Sql/SqlDriver.cpp … Executing SQL:` line, and per-operation endpoint lines.
33
- 2. The per-operation lines (`Endpoint_aggregate_group_by.cpp`, filter/join endpoints) carry `table:`, `column_names:`/`aliases:` (the SELECT list), and `expr:` (the full WHERE predicate) reconstruct the query from these.
34
- 3. **Quirk:** if `Found plan for the SQL in cache` precedes it, the `Executing SQL:` line is truncated to just `SELECT`. Use the per-operation endpoint lines (step 2) their predicate survives a cache hit. A `datetime()`/timestamp filter showing up here often _is_ the input that triggered a parser segfault.
33
+ 1. `kinetica_bundle_search_logs` with `rank: "r0"`, `regex` = the JobId, **and `include_multiline: true`**. r0 logs the `/execute/sql` receipt (submitting user), the `Sql/SqlDriver.cpp … Executing SQL:` line, and per-operation endpoint lines.
34
+ 2. **Read the full statement straight from the `Executing SQL:` line — do not reconstruct it.** Kinetica logs the SQL verbatim, so a real query spans MANY physical lines: `FROM …`, `JOIN …`, `WHERE …` each land on their own line with no timestamp prefix. Those continuation lines belong to the same log record; `include_multiline: true` stitches them back onto the match so you see the WHOLE query. WITHOUT it, a match is only the first physical line (e.g. `… Executing SQL: SELECT c."circuitId", c."circuitRef"`) and you would wrongly report the query as "truncated." Quote the statement verbatim from this single (now multi-line) match.
35
+ 3. **Cache-hit fallback only:** if `Found plan for the SQL in cache` precedes the job, Kinetica logs `Executing SQL:` as just the statement keyword (e.g. a bare `SELECT` or `EXECUTE PROCEDURE …`) with no continuation lines to stitch. ONLY then fall back to the per-operation endpoint lines (`Endpoint_aggregate_group_by.cpp`, filter/join endpoints): they carry `table:`, `column_names:`/`aliases:` (the SELECT list), and `expr:` (the full WHERE predicate), whose values survive a cache hit. A `datetime()`/timestamp filter showing up here often _is_ the input that triggered a parser segfault.
35
36
 
36
- See `rank-architecture.md` (Where queries are logged) for why this locality holds.
37
+ **Where the full multi-line query actually lives:** `include_multiline` recovers the whole statement only from the **rolling core logs** (`logs-local/core-gpudb-rolling-r0.log`), where the SQL's embedded newlines are preserved as continuation lines. The **Loki per-rank tail** (`logs/rank0.log`) keeps only the statement's first physical line — promtail captures each line as its own record, so the `FROM`/`JOIN`/`WHERE` lines are simply not in that export, and nothing can stitch them. So this workflow depends on r0 being present under `logs-local/`. If r0 exists only as a Loki tail (rare for the coordinator, but possible for a Loki-only bundle), the complete query may not be in the bundle at all — say so rather than reporting the first line as the whole query, and fall back to step 3's endpoint lines.
38
+
39
+ See `rank-architecture.md` (Where queries are logged) for why this locality holds, and "Two log families" below for rolling-vs-Loki precedence.
37
40
 
38
41
  ### Files of interest
39
42
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kinetica/admin-agent",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "Autonomous diagnostic agent for Kinetica databases",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Kinetica",