@blogic-cz/agent-tools 0.7.0 → 0.8.0
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/package.json +1 -1
- package/src/az-tool/build.ts +7 -3
- package/src/az-tool/service.ts +9 -1
- package/src/az-tool/transformers.ts +127 -0
- package/src/credential-guard/index.ts +8 -11
- package/src/db-tool/service.ts +16 -4
- package/src/db-tool/transformers.ts +38 -0
- package/src/db-tool/types.ts +2 -0
- package/src/k8s-tool/index.ts +44 -12
- package/src/k8s-tool/transformers.ts +508 -0
- package/src/k8s-tool/types.ts +1 -1
- package/src/logs-tool/service.ts +13 -10
- package/src/logs-tool/transformers.ts +212 -0
- package/src/shared/index.ts +9 -0
- package/src/shared/transform.ts +320 -0
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
import { aggregateByField, deduplicateLines, formatCountSummary, parseTextTable } from "#shared";
|
|
2
|
+
|
|
3
|
+
type PodIssueByOwner = {
|
|
4
|
+
owner: string;
|
|
5
|
+
status: string;
|
|
6
|
+
count: number;
|
|
7
|
+
restarts: number;
|
|
8
|
+
pods: string[];
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
type PodIssueIndividual = {
|
|
12
|
+
name: string;
|
|
13
|
+
status: string;
|
|
14
|
+
restarts: number;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type PodSummary = {
|
|
18
|
+
summary: string;
|
|
19
|
+
healthy: number;
|
|
20
|
+
issues: Array<PodIssueByOwner | PodIssueIndividual>;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type ParsedPod = {
|
|
24
|
+
name: string;
|
|
25
|
+
namespace: string;
|
|
26
|
+
status: string;
|
|
27
|
+
restarts: number;
|
|
28
|
+
ready: string;
|
|
29
|
+
owner?: string;
|
|
30
|
+
reason?: string;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const HEALTHY_POD_STATUSES = new Set(["Running", "Completed", "Succeeded"]);
|
|
34
|
+
const STANDARD_VOLUME_TYPES = ["emptydir", "configmap", "secret"];
|
|
35
|
+
|
|
36
|
+
function normalizeReplicaSetOwner(ownerName: string): string {
|
|
37
|
+
return ownerName.replace(/-[a-z0-9]{5,10}$/i, "");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function getStatusFromContainerState(containerState: unknown): string | undefined {
|
|
41
|
+
if (!containerState || typeof containerState !== "object") {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const stateRecord = containerState as Record<string, unknown>;
|
|
46
|
+
|
|
47
|
+
const waiting = stateRecord.waiting;
|
|
48
|
+
if (waiting && typeof waiting === "object") {
|
|
49
|
+
const reason = (waiting as Record<string, unknown>).reason;
|
|
50
|
+
if (typeof reason === "string" && reason.length > 0) {
|
|
51
|
+
return reason;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const terminated = stateRecord.terminated;
|
|
56
|
+
if (terminated && typeof terminated === "object") {
|
|
57
|
+
const reason = (terminated as Record<string, unknown>).reason;
|
|
58
|
+
if (typeof reason === "string" && reason.length > 0) {
|
|
59
|
+
return reason;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function getPodReason(
|
|
67
|
+
status: Record<string, unknown>,
|
|
68
|
+
firstContainer: Record<string, unknown> | undefined,
|
|
69
|
+
): string | undefined {
|
|
70
|
+
const containerState = firstContainer?.state;
|
|
71
|
+
|
|
72
|
+
if (containerState && typeof containerState === "object") {
|
|
73
|
+
const waiting = (containerState as Record<string, unknown>).waiting;
|
|
74
|
+
if (waiting && typeof waiting === "object") {
|
|
75
|
+
const waitingReason = (waiting as Record<string, unknown>).reason;
|
|
76
|
+
if (typeof waitingReason === "string" && waitingReason.length > 0) {
|
|
77
|
+
return waitingReason;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const statusReason = status.reason;
|
|
83
|
+
if (typeof statusReason === "string" && statusReason.length > 0) {
|
|
84
|
+
return statusReason;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function parseCpuToMillicores(cpu: string): number {
|
|
91
|
+
const trimmed = cpu.trim();
|
|
92
|
+
if (trimmed.length === 0) return 0;
|
|
93
|
+
if (trimmed.endsWith("m")) {
|
|
94
|
+
const value = Number(trimmed.slice(0, -1));
|
|
95
|
+
return Number.isFinite(value) ? Math.round(value) : 0;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const cores = Number(trimmed);
|
|
99
|
+
if (!Number.isFinite(cores)) return 0;
|
|
100
|
+
return Math.round(cores * 1000);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function parseMemoryToMi(memory: string): number {
|
|
104
|
+
const trimmed = memory.trim();
|
|
105
|
+
if (trimmed.length === 0) return 0;
|
|
106
|
+
|
|
107
|
+
const match = trimmed.match(/^([0-9]+(?:\.[0-9]+)?)([A-Za-z]+)?$/);
|
|
108
|
+
if (!match) return 0;
|
|
109
|
+
|
|
110
|
+
const value = Number(match[1]);
|
|
111
|
+
const unit = (match[2] ?? "Mi").toLowerCase();
|
|
112
|
+
if (!Number.isFinite(value)) return 0;
|
|
113
|
+
|
|
114
|
+
switch (unit) {
|
|
115
|
+
case "ki":
|
|
116
|
+
return Math.round(value / 1024);
|
|
117
|
+
case "mi":
|
|
118
|
+
return Math.round(value);
|
|
119
|
+
case "gi":
|
|
120
|
+
return Math.round(value * 1024);
|
|
121
|
+
case "ti":
|
|
122
|
+
return Math.round(value * 1024 * 1024);
|
|
123
|
+
case "k":
|
|
124
|
+
return Math.round(value / 1000 / 1024);
|
|
125
|
+
case "m":
|
|
126
|
+
return Math.round(value / 1000 / 1000 / 1024);
|
|
127
|
+
case "g":
|
|
128
|
+
return Math.round(value / 1024);
|
|
129
|
+
default:
|
|
130
|
+
return Math.round(value);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function parseKeyValueBlock(lines: string[], initialValue: string): Array<[string, string]> {
|
|
135
|
+
const collected = [initialValue, ...lines]
|
|
136
|
+
.map((line) => line.trim())
|
|
137
|
+
.filter((line) => line.length > 0 && line !== "<none>");
|
|
138
|
+
|
|
139
|
+
const entries: Array<[string, string]> = [];
|
|
140
|
+
|
|
141
|
+
for (const line of collected) {
|
|
142
|
+
const separator = line.includes(":") ? ":" : "=";
|
|
143
|
+
const parts = line.split(separator);
|
|
144
|
+
if (parts.length < 2) continue;
|
|
145
|
+
|
|
146
|
+
const key = parts[0]?.trim() ?? "";
|
|
147
|
+
const value = parts.slice(1).join(separator).trim();
|
|
148
|
+
|
|
149
|
+
if (key.length > 0) {
|
|
150
|
+
entries.push([key, value]);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return entries;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function parseDescribeSections(
|
|
158
|
+
textOutput: string,
|
|
159
|
+
): Map<string, { initial: string; lines: string[] }> {
|
|
160
|
+
const sections = new Map<string, { initial: string; lines: string[] }>();
|
|
161
|
+
const lines = textOutput.split(/\r?\n/);
|
|
162
|
+
const headerPattern = /^([A-Za-z][A-Za-z0-9 _\-()/.]*):\s*(.*)$/;
|
|
163
|
+
|
|
164
|
+
let currentSection: string | undefined;
|
|
165
|
+
|
|
166
|
+
for (const line of lines) {
|
|
167
|
+
const headerMatch = line.match(headerPattern);
|
|
168
|
+
if (headerMatch && !line.startsWith(" ") && !line.startsWith("\t")) {
|
|
169
|
+
const sectionName = headerMatch[1].trim();
|
|
170
|
+
const initial = headerMatch[2] ?? "";
|
|
171
|
+
sections.set(sectionName, { initial, lines: [] });
|
|
172
|
+
currentSection = sectionName;
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (currentSection) {
|
|
177
|
+
const section = sections.get(currentSection);
|
|
178
|
+
if (section) {
|
|
179
|
+
section.lines.push(line);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return sections;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function stripLowValueColumns(rows: Record<string, string>[]): Record<string, string>[] {
|
|
188
|
+
if (rows.length === 0) {
|
|
189
|
+
return rows;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const keys = Object.keys(rows[0] ?? {});
|
|
193
|
+
const keysToRemove = new Set<string>();
|
|
194
|
+
|
|
195
|
+
for (const key of keys) {
|
|
196
|
+
const allNone = rows.every((row) => {
|
|
197
|
+
const value = (row[key] ?? "").trim().toLowerCase();
|
|
198
|
+
return value === "<none>";
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
if (allNone) {
|
|
202
|
+
keysToRemove.add(key);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (keysToRemove.size === 0) {
|
|
207
|
+
return rows;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return rows.map((row) => {
|
|
211
|
+
const filtered: Record<string, string> = {};
|
|
212
|
+
for (const [key, value] of Object.entries(row)) {
|
|
213
|
+
if (!keysToRemove.has(key)) {
|
|
214
|
+
filtered[key] = value;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return filtered;
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function transformPods(jsonOutput: string): PodSummary | string {
|
|
222
|
+
try {
|
|
223
|
+
const parsed = JSON.parse(jsonOutput) as { items?: unknown[] };
|
|
224
|
+
if (!Array.isArray(parsed.items)) {
|
|
225
|
+
return jsonOutput;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const pods: ParsedPod[] = parsed.items.map((item) => {
|
|
229
|
+
const pod = (item ?? {}) as Record<string, unknown>;
|
|
230
|
+
const metadata = (pod.metadata ?? {}) as Record<string, unknown>;
|
|
231
|
+
const status = (pod.status ?? {}) as Record<string, unknown>;
|
|
232
|
+
const containerStatuses = Array.isArray(status.containerStatuses)
|
|
233
|
+
? (status.containerStatuses as unknown[])
|
|
234
|
+
: [];
|
|
235
|
+
|
|
236
|
+
const firstContainer =
|
|
237
|
+
containerStatuses.length > 0 && typeof containerStatuses[0] === "object"
|
|
238
|
+
? (containerStatuses[0] as Record<string, unknown>)
|
|
239
|
+
: undefined;
|
|
240
|
+
const stateReason = getStatusFromContainerState(firstContainer?.state);
|
|
241
|
+
|
|
242
|
+
const restarts = containerStatuses.reduce<number>((sum, entry) => {
|
|
243
|
+
if (!entry || typeof entry !== "object") {
|
|
244
|
+
return sum;
|
|
245
|
+
}
|
|
246
|
+
const restartCount = Number((entry as Record<string, unknown>).restartCount);
|
|
247
|
+
return sum + (Number.isFinite(restartCount) ? restartCount : 0);
|
|
248
|
+
}, 0);
|
|
249
|
+
|
|
250
|
+
const readyCount = containerStatuses.reduce<number>((sum, entry) => {
|
|
251
|
+
if (!entry || typeof entry !== "object") {
|
|
252
|
+
return sum;
|
|
253
|
+
}
|
|
254
|
+
return sum + ((entry as Record<string, unknown>).ready === true ? 1 : 0);
|
|
255
|
+
}, 0);
|
|
256
|
+
|
|
257
|
+
const ownerReferences = Array.isArray(metadata.ownerReferences)
|
|
258
|
+
? (metadata.ownerReferences as unknown[])
|
|
259
|
+
: [];
|
|
260
|
+
const ownerRef =
|
|
261
|
+
ownerReferences.length > 0 && typeof ownerReferences[0] === "object"
|
|
262
|
+
? (ownerReferences[0] as Record<string, unknown>)
|
|
263
|
+
: undefined;
|
|
264
|
+
const ownerNameRaw = typeof ownerRef?.name === "string" ? ownerRef.name : undefined;
|
|
265
|
+
|
|
266
|
+
const podReason = getPodReason(status, firstContainer);
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
name: typeof metadata.name === "string" ? metadata.name : "unknown",
|
|
270
|
+
namespace: typeof metadata.namespace === "string" ? metadata.namespace : "default",
|
|
271
|
+
status:
|
|
272
|
+
stateReason ??
|
|
273
|
+
(typeof status.phase === "string" && status.phase.length > 0 ? status.phase : "Unknown"),
|
|
274
|
+
restarts,
|
|
275
|
+
ready: `${readyCount}/${containerStatuses.length}`,
|
|
276
|
+
owner: ownerNameRaw ? normalizeReplicaSetOwner(ownerNameRaw) : undefined,
|
|
277
|
+
reason: podReason,
|
|
278
|
+
};
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
const countsByStatus = aggregateByField(pods, "status");
|
|
282
|
+
const summary = formatCountSummary(countsByStatus, pods.length, "pods");
|
|
283
|
+
const healthyPods = pods.filter((pod) => HEALTHY_POD_STATUSES.has(pod.status));
|
|
284
|
+
const issuePods = pods.filter((pod) => !HEALTHY_POD_STATUSES.has(pod.status));
|
|
285
|
+
|
|
286
|
+
const groupedIssues = new Map<string, PodIssueByOwner>();
|
|
287
|
+
const individualIssues: PodIssueIndividual[] = [];
|
|
288
|
+
|
|
289
|
+
for (const pod of issuePods) {
|
|
290
|
+
if (pod.owner) {
|
|
291
|
+
const key = `${pod.owner}|${pod.status}`;
|
|
292
|
+
const existing = groupedIssues.get(key);
|
|
293
|
+
|
|
294
|
+
if (existing) {
|
|
295
|
+
existing.count += 1;
|
|
296
|
+
existing.restarts += pod.restarts;
|
|
297
|
+
existing.pods.push(pod.name);
|
|
298
|
+
} else {
|
|
299
|
+
groupedIssues.set(key, {
|
|
300
|
+
owner: pod.owner,
|
|
301
|
+
status: pod.status,
|
|
302
|
+
count: 1,
|
|
303
|
+
restarts: pod.restarts,
|
|
304
|
+
pods: [pod.name],
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
} else {
|
|
308
|
+
individualIssues.push({
|
|
309
|
+
name: pod.name,
|
|
310
|
+
status: pod.status,
|
|
311
|
+
restarts: pod.restarts,
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return {
|
|
317
|
+
summary,
|
|
318
|
+
healthy: healthyPods.length,
|
|
319
|
+
issues: [...groupedIssues.values(), ...individualIssues],
|
|
320
|
+
};
|
|
321
|
+
} catch {
|
|
322
|
+
return jsonOutput;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export function transformTop(textOutput: string): Record<string, unknown> {
|
|
327
|
+
const table = parseTextTable(textOutput);
|
|
328
|
+
const rows = table.rows;
|
|
329
|
+
|
|
330
|
+
const pods = rows.map((row) => {
|
|
331
|
+
const name = row.NAME ?? row.POD ?? row.PODS ?? "";
|
|
332
|
+
const cpu = row["CPU(cores)"] ?? row.CPU ?? row.CPU_CORES ?? "0m";
|
|
333
|
+
const memory = row["MEMORY(bytes)"] ?? row.MEMORY ?? row.MEMORY_BYTES ?? "0Mi";
|
|
334
|
+
|
|
335
|
+
return { name, cpu, memory };
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
const totalCpuMillicores = pods.reduce((sum, pod) => sum + parseCpuToMillicores(pod.cpu), 0);
|
|
339
|
+
const totalMemoryMi = pods.reduce((sum, pod) => sum + parseMemoryToMi(pod.memory), 0);
|
|
340
|
+
|
|
341
|
+
return {
|
|
342
|
+
summary: `${pods.length} pods: total CPU ${totalCpuMillicores}m, Memory ${totalMemoryMi}Mi`,
|
|
343
|
+
pods,
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export function transformDescribe(textOutput: string): string {
|
|
348
|
+
const sections = parseDescribeSections(textOutput);
|
|
349
|
+
if (sections.size === 0) {
|
|
350
|
+
return textOutput;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const outputLines: string[] = [];
|
|
354
|
+
|
|
355
|
+
const simpleFields = ["Name", "Namespace", "Status", "Type"];
|
|
356
|
+
for (const field of simpleFields) {
|
|
357
|
+
const section = sections.get(field);
|
|
358
|
+
if (!section) continue;
|
|
359
|
+
const value = section.initial.trim();
|
|
360
|
+
if (value.length > 0) {
|
|
361
|
+
outputLines.push(`${field}: ${value}`);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const labelsSection = sections.get("Labels");
|
|
366
|
+
if (labelsSection) {
|
|
367
|
+
const labels = parseKeyValueBlock(labelsSection.lines, labelsSection.initial);
|
|
368
|
+
if (labels.length > 0) {
|
|
369
|
+
outputLines.push("Labels:");
|
|
370
|
+
for (const [key, value] of labels) {
|
|
371
|
+
outputLines.push(` ${key}=${value}`);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const annotationsSection = sections.get("Annotations");
|
|
377
|
+
if (annotationsSection) {
|
|
378
|
+
const annotations = parseKeyValueBlock(annotationsSection.lines, annotationsSection.initial);
|
|
379
|
+
if (annotations.length > 0) {
|
|
380
|
+
outputLines.push("Annotations:");
|
|
381
|
+
for (const [key, value] of annotations) {
|
|
382
|
+
const compact = value.length > 100 ? `${value.slice(0, 100)}…` : value;
|
|
383
|
+
outputLines.push(` ${key}: ${compact}`);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const volumesSection = sections.get("Volumes");
|
|
389
|
+
if (volumesSection) {
|
|
390
|
+
const volumeTypes = volumesSection.lines
|
|
391
|
+
.map((line) => line.trim())
|
|
392
|
+
.filter((line) => line.startsWith("Type:"))
|
|
393
|
+
.map((line) => line.slice("Type:".length).trim().toLowerCase());
|
|
394
|
+
|
|
395
|
+
if (volumeTypes.length > 0) {
|
|
396
|
+
const allStandard = volumeTypes.every((type) =>
|
|
397
|
+
STANDARD_VOLUME_TYPES.some((standard) => type.includes(standard)),
|
|
398
|
+
);
|
|
399
|
+
|
|
400
|
+
if (allStandard) {
|
|
401
|
+
outputLines.push(
|
|
402
|
+
`Volumes: ${volumeTypes.length} standard volumes (emptyDir/configMap/secret)`,
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const conditionsSection = sections.get("Conditions");
|
|
409
|
+
if (conditionsSection) {
|
|
410
|
+
const conditionLines = conditionsSection.lines
|
|
411
|
+
.map((line) => line.trim())
|
|
412
|
+
.filter((line) => line.length > 0)
|
|
413
|
+
.filter((line) => !/^type\s+status/i.test(line));
|
|
414
|
+
|
|
415
|
+
const parsedConditions = conditionLines
|
|
416
|
+
.map((line) => line.split(/\s{2,}/).filter((part) => part.length > 0))
|
|
417
|
+
.filter((parts) => parts.length >= 2)
|
|
418
|
+
.map((parts) => ({
|
|
419
|
+
type: parts[0] ?? "",
|
|
420
|
+
status: parts[1] ?? "",
|
|
421
|
+
reason: parts[2] ?? "",
|
|
422
|
+
message: parts.slice(3).join(" "),
|
|
423
|
+
}));
|
|
424
|
+
|
|
425
|
+
if (parsedConditions.length > 0) {
|
|
426
|
+
const problematic = parsedConditions.filter((condition) => condition.status !== "True");
|
|
427
|
+
|
|
428
|
+
if (problematic.length === 0) {
|
|
429
|
+
outputLines.push(`Conditions: All ${parsedConditions.length} conditions True`);
|
|
430
|
+
} else {
|
|
431
|
+
outputLines.push("Conditions:");
|
|
432
|
+
for (const condition of problematic) {
|
|
433
|
+
const reasonPart = condition.reason.length > 0 ? ` reason=${condition.reason}` : "";
|
|
434
|
+
const messagePart = condition.message.length > 0 ? ` message=${condition.message}` : "";
|
|
435
|
+
outputLines.push(` ${condition.type}: ${condition.status}${reasonPart}${messagePart}`);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const eventsSection = sections.get("Events");
|
|
442
|
+
if (eventsSection) {
|
|
443
|
+
const eventLines = eventsSection.lines
|
|
444
|
+
.map((line) => line.trimEnd())
|
|
445
|
+
.filter((line) => line.trim().length > 0)
|
|
446
|
+
.filter((line) => !line.trimStart().toLowerCase().startsWith("type"));
|
|
447
|
+
|
|
448
|
+
if (eventLines.length > 0) {
|
|
449
|
+
outputLines.push("Events:");
|
|
450
|
+
for (const line of eventLines.slice(-10)) {
|
|
451
|
+
outputLines.push(` ${line.trim()}`);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return outputLines.join("\n").trim();
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
export function transformLogs(textOutput: string): string {
|
|
460
|
+
return deduplicateLines(textOutput, {
|
|
461
|
+
normalizeTimestamps: true,
|
|
462
|
+
normalizeUUIDs: true,
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
export function transformGenericKubectl(
|
|
467
|
+
textOutput: string,
|
|
468
|
+
_command: string,
|
|
469
|
+
): string | Record<string, unknown> {
|
|
470
|
+
const trimmed = textOutput.trim();
|
|
471
|
+
|
|
472
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
473
|
+
try {
|
|
474
|
+
const parsed = JSON.parse(trimmed);
|
|
475
|
+
if (Array.isArray(parsed)) {
|
|
476
|
+
return { items: parsed };
|
|
477
|
+
}
|
|
478
|
+
if (parsed && typeof parsed === "object") {
|
|
479
|
+
return parsed as Record<string, unknown>;
|
|
480
|
+
}
|
|
481
|
+
} catch (error) {
|
|
482
|
+
const ignored = error;
|
|
483
|
+
void ignored;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const lines = textOutput.split(/\r?\n/).filter((line) => line.trim().length > 0);
|
|
488
|
+
const parsedTable = parseTextTable(textOutput);
|
|
489
|
+
const looksLikeTable =
|
|
490
|
+
lines.length >= 2 &&
|
|
491
|
+
parsedTable.headers.length >= 2 &&
|
|
492
|
+
parsedTable.rows.length >= 1 &&
|
|
493
|
+
parsedTable.headers.every((header) => /^[A-Z0-9_()\-/]+$/.test(header));
|
|
494
|
+
|
|
495
|
+
if (looksLikeTable) {
|
|
496
|
+
const rows = stripLowValueColumns(parsedTable.rows);
|
|
497
|
+
const headers = parsedTable.headers.filter((headerName) =>
|
|
498
|
+
rows.length === 0 ? true : rows.some((row) => headerName in row),
|
|
499
|
+
);
|
|
500
|
+
return { headers, rows };
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
if (lines.length > 50) {
|
|
504
|
+
return deduplicateLines(textOutput);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
return textOutput;
|
|
508
|
+
}
|
package/src/k8s-tool/types.ts
CHANGED
package/src/logs-tool/service.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { K8sService, K8sServiceLayer } from "#k8s/service";
|
|
|
8
8
|
import { ConfigService, ConfigServiceLayer, getToolConfig } from "#config/loader";
|
|
9
9
|
import type { LogsConfig } from "#config/types";
|
|
10
10
|
import { LogsNotFoundError, LogsReadError, type LogsError } from "./errors";
|
|
11
|
+
import { transformLogOutput } from "./transformers";
|
|
11
12
|
|
|
12
13
|
export const parseLogFiles = (output: string): LogFile[] => {
|
|
13
14
|
const lines = output.trim().split("\n").slice(1);
|
|
@@ -44,6 +45,8 @@ export const formatPrettyOutput = (output: string): string => {
|
|
|
44
45
|
*/
|
|
45
46
|
export const sanitizeShellArg = (input: string): string => `'${input.replace(/'/g, "'\\''")}'`;
|
|
46
47
|
|
|
48
|
+
const readCommandOutput = (output: unknown): string => (typeof output === "string" ? output : "");
|
|
49
|
+
|
|
47
50
|
export class LogsService extends ServiceMap.Service<
|
|
48
51
|
LogsService,
|
|
49
52
|
{
|
|
@@ -138,7 +141,7 @@ export class LogsService extends ServiceMap.Service<
|
|
|
138
141
|
),
|
|
139
142
|
);
|
|
140
143
|
|
|
141
|
-
const pod = (podResult.output
|
|
144
|
+
const pod = readCommandOutput(podResult.output).replace(/'/g, "");
|
|
142
145
|
|
|
143
146
|
const listResult = yield* k8s.runKubectl(`exec ${pod} -- ls -la ${remotePath}`, false).pipe(
|
|
144
147
|
Effect.mapError(
|
|
@@ -150,7 +153,7 @@ export class LogsService extends ServiceMap.Service<
|
|
|
150
153
|
),
|
|
151
154
|
);
|
|
152
155
|
|
|
153
|
-
const files = parseLogFiles(listResult.output
|
|
156
|
+
const files = parseLogFiles(readCommandOutput(listResult.output));
|
|
154
157
|
if (files.length === 0) {
|
|
155
158
|
return yield* new LogsNotFoundError({
|
|
156
159
|
message: "No log files found",
|
|
@@ -210,11 +213,11 @@ export class LogsService extends ServiceMap.Service<
|
|
|
210
213
|
}
|
|
211
214
|
|
|
212
215
|
const output = result.stdout.trim();
|
|
213
|
-
if (
|
|
214
|
-
return
|
|
216
|
+
if (!output) {
|
|
217
|
+
return "(no matching lines)";
|
|
215
218
|
}
|
|
216
219
|
|
|
217
|
-
return output
|
|
220
|
+
return transformLogOutput(output);
|
|
218
221
|
});
|
|
219
222
|
|
|
220
223
|
const readRemoteLogs = Effect.fn("LogsService.readRemoteLogs")(function* (
|
|
@@ -236,7 +239,7 @@ export class LogsService extends ServiceMap.Service<
|
|
|
236
239
|
),
|
|
237
240
|
);
|
|
238
241
|
|
|
239
|
-
const pod = (podResult.output
|
|
242
|
+
const pod = readCommandOutput(podResult.output).replace(/'/g, "");
|
|
240
243
|
const logFile = options.file ?? "app.log";
|
|
241
244
|
const logPath = `${remotePath}/${logFile}`;
|
|
242
245
|
let command = `tail -${options.tail} ${sanitizeShellArg(logPath)}`;
|
|
@@ -263,12 +266,12 @@ export class LogsService extends ServiceMap.Service<
|
|
|
263
266
|
);
|
|
264
267
|
},
|
|
265
268
|
onSuccess: (result) => {
|
|
266
|
-
const trimmed = (result.output
|
|
267
|
-
if (
|
|
268
|
-
return Effect.succeed(
|
|
269
|
+
const trimmed = readCommandOutput(result.output).trim();
|
|
270
|
+
if (!trimmed) {
|
|
271
|
+
return Effect.succeed("(no matching lines)");
|
|
269
272
|
}
|
|
270
273
|
|
|
271
|
-
return Effect.succeed(trimmed
|
|
274
|
+
return Effect.succeed(transformLogOutput(trimmed));
|
|
272
275
|
},
|
|
273
276
|
});
|
|
274
277
|
});
|