79 lines
4.6 KiB
TypeScript
79 lines
4.6 KiB
TypeScript
export interface LogEntry {
|
|
event_id: string; timestamp: string; app: string; level: string; message: string;
|
|
kind: string; cause_ids: string[]; probable_cause_ids?: string[];
|
|
entity?: string; values?: Record<string, unknown>;
|
|
source?: { path: string; line: number };
|
|
status?: string;
|
|
group_id?: string; group_kind?: string;
|
|
file?: string; raw?: string; time: number; trace_id?: string;
|
|
[key: string]: unknown;
|
|
}
|
|
const timeCache = new Map<string, number>();
|
|
const formatters = new Map<string, Intl.DateTimeFormat>();
|
|
export function rawTime(date: string, zone: string): number {
|
|
const cacheKey = zone + ':' + date;
|
|
const cached = timeCache.get(cacheKey);
|
|
if (cached !== undefined) { return cached; }
|
|
const m = date.match(/^(?:(\d{4})-(\d\d)-(\d\d)|(\d\d)-(\d\d)-(\d\d)) (\d\d):(\d\d):(\d\d)/);
|
|
if (!m) { return NaN; }
|
|
const year = Number(m[1] || (2000 + Number(m[6])));
|
|
const month = Number(m[2] || m[5]);
|
|
const day = Number(m[3] || m[4]);
|
|
const hour = Number(m[7]);
|
|
const minute = Number(m[8]);
|
|
const second = Number(m[9]);
|
|
const utc = Date.UTC(year, month - 1, day, hour, minute, second);
|
|
try {
|
|
let formatter = formatters.get(zone);
|
|
if (!formatter) { formatter = new Intl.DateTimeFormat('en-GB', { timeZone: zone, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hourCycle: 'h23' }); formatters.set(zone, formatter); }
|
|
const parts = formatter.formatToParts(utc);
|
|
const p = Object.fromEntries(parts.map(part => [part.type, part.value]));
|
|
const time = utc - (Date.UTC(+p.year, +p.month - 1, +p.day, +p.hour, +p.minute, +p.second) - utc);
|
|
timeCache.set(cacheKey, time);
|
|
if (timeCache.size > 4096) { timeCache.delete(timeCache.keys().next().value!); }
|
|
return time;
|
|
} catch { return utc; }
|
|
}
|
|
export function parseLine(line: string, file: string, offset: number, zone = 'Europe/Berlin'): LogEntry | undefined {
|
|
if (line.startsWith('{')) {
|
|
try {
|
|
const e = JSON.parse(line);
|
|
if (e.version !== 1 || typeof e.event_id !== 'string' || typeof e.message !== 'string' || typeof e.app !== 'string' || typeof e.timestamp !== 'string' || !Array.isArray(e.cause_ids) || !e.cause_ids.every((id: unknown) => typeof id === 'string')) { return undefined; }
|
|
return { ...e, level: String(e.level || 'INFO'), kind: String(e.kind || 'log'), file, raw: line, time: Date.parse(e.timestamp) };
|
|
} catch { return undefined; }
|
|
}
|
|
const m = line.match(/^((?:\d{2}-\d{2}-\d{2}|\d{4}-\d{2}-\d{2}) \d{2}:\d{2}:\d{2}(?:\.\d+)?)\s+(DEBUG|INFO|WARNING|ERROR|CRITICAL)\s+([^:]+):\s?(.*)$/);
|
|
if (!m) { return undefined; }
|
|
const group = m[4].match(/\[group=([^\]\r\n]+):([a-f0-9]{12})\]/i);
|
|
const id = m[4].match(/\[event=([\w-]+) trace=([\w-]+)\]/);
|
|
const groupKind = group?.[1]?.trim();
|
|
return { event_id: id?.[1] || `${file}:${offset}`, trace_id: id?.[2], group_id: group?.[2], group_kind: groupKind, timestamp: m[1], time: rawTime(m[1], zone), app: m[3].trim(), level: m[2], message: m[4], kind: 'log', cause_ids: [], file, raw: line };
|
|
}
|
|
|
|
/** Preserve valid UTF-8 spans, including files containing mixed legacy bytes. */
|
|
export function decodeLogLine(bytes: Buffer, encoding = 'auto'): string {
|
|
if (encoding === 'utf8') { return bytes.toString('utf8'); }
|
|
const legacy = new TextDecoder('windows-1252');
|
|
if (encoding === 'windows-1252') { return legacy.decode(bytes); }
|
|
try { return new TextDecoder('utf-8', { fatal: true }).decode(bytes); } catch { /* Mixed legacy log. */ }
|
|
const output: string[] = [];
|
|
let start = 0;
|
|
for (let i = 0; i < bytes.length;) {
|
|
const first = bytes[i];
|
|
const width = first < 0x80 ? 1 : first >= 0xc2 && first <= 0xdf ? 2 : first >= 0xe0 && first <= 0xef ? 3 : first >= 0xf0 && first <= 0xf4 ? 4 : 0;
|
|
let valid = width > 0 && i + width <= bytes.length;
|
|
for (let j = 1; valid && j < width; j++) { valid = bytes[i + j] >= 0x80 && bytes[i + j] <= 0xbf; }
|
|
if (valid && width > 1) {
|
|
const second = bytes[i + 1];
|
|
valid = !(first === 0xe0 && second < 0xa0 || first === 0xed && second > 0x9f || first === 0xf0 && second < 0x90 || first === 0xf4 && second > 0x8f);
|
|
}
|
|
if (valid) { i += width; } else {
|
|
if (i > start) { output.push(bytes.subarray(start, i).toString('utf8')); }
|
|
output.push(legacy.decode(bytes.subarray(i, i + 1)));
|
|
start = ++i;
|
|
}
|
|
}
|
|
if (start < bytes.length) { output.push(bytes.subarray(start).toString('utf8')); }
|
|
return output.join('');
|
|
}
|