@blogic-cz/agent-tools 0.8.2 → 0.8.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/package.json +1 -1
- package/src/shared/index.ts +2 -0
- package/src/shared/log-transform.ts +240 -0
package/package.json
CHANGED
package/src/shared/index.ts
CHANGED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { deduplicateLines } from "./transform";
|
|
2
|
+
|
|
3
|
+
type LogLevel = "ERROR" | "WARN" | "INFO" | "DEBUG";
|
|
4
|
+
|
|
5
|
+
type ParsedLogLine = {
|
|
6
|
+
level: LogLevel;
|
|
7
|
+
timestamp?: string;
|
|
8
|
+
message: string;
|
|
9
|
+
originalIndex: number;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const TIMESTAMP_FIELDS = ["timestamp", "ts", "time", "@timestamp", "datetime"] as const;
|
|
13
|
+
const LEVEL_FIELDS = ["level", "severity", "lvl", "log.level"] as const;
|
|
14
|
+
const MESSAGE_FIELDS = ["message", "msg", "log", "text"] as const;
|
|
15
|
+
const ERROR_FIELDS = ["error", "err", "stack", "exception", "error.message"] as const;
|
|
16
|
+
|
|
17
|
+
const LEVEL_ORDER: ReadonlyArray<LogLevel> = ["ERROR", "WARN", "INFO", "DEBUG"];
|
|
18
|
+
|
|
19
|
+
function getByPath(obj: unknown, path: string): unknown {
|
|
20
|
+
if (typeof obj !== "object" || obj === null) {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (!path.includes(".")) {
|
|
25
|
+
return (obj as Record<string, unknown>)[path];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const parts = path.split(".");
|
|
29
|
+
let current: unknown = obj;
|
|
30
|
+
|
|
31
|
+
for (const part of parts) {
|
|
32
|
+
if (typeof current !== "object" || current === null) {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
current = (current as Record<string, unknown>)[part];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return current;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function toText(value: unknown): string | undefined {
|
|
42
|
+
if (typeof value === "string") {
|
|
43
|
+
const trimmed = value.trim();
|
|
44
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
48
|
+
return String(value);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (value && typeof value === "object") {
|
|
52
|
+
if ("message" in value) {
|
|
53
|
+
const nestedMessage = toText((value as Record<string, unknown>).message);
|
|
54
|
+
if (nestedMessage) {
|
|
55
|
+
return nestedMessage;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
return JSON.stringify(value);
|
|
61
|
+
} catch {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function formatTimestamp(value: unknown): string | undefined {
|
|
70
|
+
if (typeof value === "number" && value > 1_000_000_000_000) {
|
|
71
|
+
return new Date(value).toISOString();
|
|
72
|
+
}
|
|
73
|
+
if (typeof value === "number" && value > 1_000_000_000) {
|
|
74
|
+
return new Date(value * 1000).toISOString();
|
|
75
|
+
}
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function firstMappedField(
|
|
80
|
+
obj: unknown,
|
|
81
|
+
fields: ReadonlyArray<string>,
|
|
82
|
+
isTimestamp = false,
|
|
83
|
+
): string | undefined {
|
|
84
|
+
for (const field of fields) {
|
|
85
|
+
const raw = getByPath(obj, field);
|
|
86
|
+
if (isTimestamp) {
|
|
87
|
+
const formatted = formatTimestamp(raw);
|
|
88
|
+
if (formatted) return formatted;
|
|
89
|
+
}
|
|
90
|
+
const value = toText(raw);
|
|
91
|
+
if (value) {
|
|
92
|
+
return value;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Pino numeric levels: 10=trace, 20=debug, 30=info, 40=warn, 50=error, 60=fatal
|
|
100
|
+
function normalizeLevel(rawLevel: string | undefined): LogLevel {
|
|
101
|
+
const normalized = rawLevel?.trim().toLowerCase();
|
|
102
|
+
|
|
103
|
+
if (!normalized) {
|
|
104
|
+
return "INFO";
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const numericLevel = Number(normalized);
|
|
108
|
+
if (Number.isFinite(numericLevel)) {
|
|
109
|
+
if (numericLevel >= 50) return "ERROR";
|
|
110
|
+
if (numericLevel >= 40) return "WARN";
|
|
111
|
+
if (numericLevel >= 30) return "INFO";
|
|
112
|
+
return "DEBUG";
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (normalized === "error" || normalized === "err" || normalized === "fatal") {
|
|
116
|
+
return "ERROR";
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (normalized === "warn" || normalized === "warning") {
|
|
120
|
+
return "WARN";
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (normalized === "info") {
|
|
124
|
+
return "INFO";
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (normalized === "debug" || normalized === "trace") {
|
|
128
|
+
return "DEBUG";
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return "INFO";
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function formatLine({ level, timestamp, message }: ParsedLogLine): string {
|
|
135
|
+
if (timestamp) {
|
|
136
|
+
return `[${timestamp}] [${level}] ${message}`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return `[${level}] ${message}`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function parseJsonLogLine(line: string, originalIndex: number): ParsedLogLine {
|
|
143
|
+
try {
|
|
144
|
+
const parsed = JSON.parse(line) as unknown;
|
|
145
|
+
const timestamp = firstMappedField(parsed, TIMESTAMP_FIELDS, true);
|
|
146
|
+
const level = normalizeLevel(firstMappedField(parsed, LEVEL_FIELDS));
|
|
147
|
+
const message =
|
|
148
|
+
firstMappedField(parsed, MESSAGE_FIELDS) ??
|
|
149
|
+
firstMappedField(parsed, ERROR_FIELDS) ??
|
|
150
|
+
line.trim();
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
level,
|
|
154
|
+
timestamp,
|
|
155
|
+
message,
|
|
156
|
+
originalIndex,
|
|
157
|
+
};
|
|
158
|
+
} catch {
|
|
159
|
+
return {
|
|
160
|
+
level: "INFO",
|
|
161
|
+
message: line.trim(),
|
|
162
|
+
originalIndex,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function isJsonMode(lines: ReadonlyArray<string>): boolean {
|
|
168
|
+
const firstNonEmpty = lines.find((line) => line.trim().length > 0);
|
|
169
|
+
if (!firstNonEmpty) {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
const parsed = JSON.parse(firstNonEmpty) as unknown;
|
|
175
|
+
return typeof parsed === "object" && parsed !== null;
|
|
176
|
+
} catch {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function groupHeader(level: LogLevel, count: number): string {
|
|
182
|
+
switch (level) {
|
|
183
|
+
case "ERROR":
|
|
184
|
+
return `--- errors (${count}) ---`;
|
|
185
|
+
case "WARN":
|
|
186
|
+
return `--- warnings (${count}) ---`;
|
|
187
|
+
case "INFO":
|
|
188
|
+
return `--- info (${count}) ---`;
|
|
189
|
+
case "DEBUG":
|
|
190
|
+
return `--- debug (${count}) ---`;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function transformJsonLines(lines: ReadonlyArray<string>): string {
|
|
195
|
+
const parsedLines = lines
|
|
196
|
+
.map((line, originalIndex) => ({ line, originalIndex }))
|
|
197
|
+
.filter(({ line }) => line.trim().length > 0)
|
|
198
|
+
.map(({ line, originalIndex }) => parseJsonLogLine(line, originalIndex));
|
|
199
|
+
|
|
200
|
+
if (parsedLines.length === 0) {
|
|
201
|
+
return "";
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const ordered = LEVEL_ORDER.flatMap((level) =>
|
|
205
|
+
parsedLines
|
|
206
|
+
.filter((entry) => entry.level === level)
|
|
207
|
+
.sort((a, b) => a.originalIndex - b.originalIndex),
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
const output: Array<string> = [];
|
|
211
|
+
|
|
212
|
+
for (const level of LEVEL_ORDER) {
|
|
213
|
+
const group = ordered.filter((entry) => entry.level === level);
|
|
214
|
+
if (group.length === 0) {
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
output.push(groupHeader(level, group.length));
|
|
219
|
+
output.push(...group.map(formatLine));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return deduplicateLines(output.join("\n"));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function transformLogOutput(rawOutput: string): string {
|
|
226
|
+
if (rawOutput.length === 0) {
|
|
227
|
+
return "";
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const lines = rawOutput.split(/\r?\n/);
|
|
231
|
+
|
|
232
|
+
if (isJsonMode(lines)) {
|
|
233
|
+
return transformJsonLines(lines);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return deduplicateLines(rawOutput, {
|
|
237
|
+
normalizeTimestamps: true,
|
|
238
|
+
normalizeUUIDs: true,
|
|
239
|
+
});
|
|
240
|
+
}
|