Files
3x-ui/frontend/src/pages/index/logParse.ts
T
MHSanaei df0e52cda8 fix(logs): render plain log notices verbatim instead of mangling them as timestamps
A plain message with no timestamp/level (e.g. the Windows 'Syslog is not
supported' notice) was parsed by the app-log branch, which took the first
three words as date/time/level and dropped the rest. Match the strict
'YYYY/MM/DD LEVEL - body' shape only, keep other lines whole, and drop the
leading separator when there is no stamp or level.
2026-06-25 23:59:49 +02:00

120 lines
4.1 KiB
TypeScript

// Parser for the panel log viewer. Logs reach the UI in two shapes:
//
// - App log (SysLog off): the in-memory buffer, formatted as
// "2006/01/02 15:04:05 LEVEL - message"
// - SysLog (journalctl -o short): every entry is prefixed with
// "Mon DD HH:MM:SS host ident[pid]: " before the real message, and the
// message itself is one of several shapes depending on which subsystem
// emitted it:
// "INFO - mtproto: ..." go-logging (x-ui + xray)
// "2026/06/08 19:22:22 http: ..." Go std log (net/http, runtime)
// "[Mon Jun 8 23:56:52 UTC 2026] ERROR ..." telego bot
// "Stopping x-ui.service - ..." systemd
//
// parseLogLine normalises all of these into a stamp + level + service + body so
// the viewer renders a readable line instead of a bare timestamp.
export interface ParsedLog {
date: string;
time: string;
stamp: string;
levelText: string;
levelClass: string;
service: string;
body: string;
}
export const LEVELS = ['DEBUG', 'INFO', 'NOTICE', 'WARNING', 'ERROR'];
export const LEVEL_CLASSES = [
'level-debug',
'level-info',
'level-notice',
'level-warning',
'level-error',
];
// "Mon DD HH:MM:SS host ident[pid]: <message>" — captures the journal date,
// time, and the message that follows the syslog identifier.
const SYSLOG_PREFIX = /^([A-Za-z]{3}\s+\d{1,2})\s+(\d{2}:\d{2}:\d{2})\s+\S+\s+\S+?:\s+(.*)$/;
// Redundant Go std-log date prefix ("2006/01/02 15:04:05 ") to strip — the
// journal already carries the timestamp.
const GO_LOG_DATE = /^\d{4}\/\d{2}\/\d{2}\s+\d{2}:\d{2}:\d{2}\s+/;
// telego's own line prefix: "[Mon Jan _2 15:04:05 MST 2006] LEVEL rest".
const TELEGO = /^\[[^\]]+\]\s+([A-Z]+)\s+(.*)$/;
// App-log format emitted by the in-memory buffer:
// "2006/01/02 15:04:05 LEVEL - message". Only a line matching this exact shape
// carries a structured timestamp/level; anything else (e.g. a plain notice such
// as the Windows "Syslog is not supported" message) is kept whole as the body.
const APP_LOG = /^(\d{4}\/\d{2}\/\d{2})\s+(\d{2}:\d{2}:\d{2})\s+(\S+)\s+-\s+([\s\S]*)$/;
// splitLevelDash pulls a leading "LEVEL - " off a message, returning the level
// and the remainder. Returns null when the message does not start with a level.
function splitLevelDash(message: string): { level: string; rest: string } | null {
const dash = message.indexOf(' - ');
if (dash < 0) return null;
const level = message.slice(0, dash).trim();
if (LEVELS.indexOf(level) < 0) return null;
return { level, rest: message.slice(dash + 3) };
}
export function parseLogLine(line: string): ParsedLog {
const raw = (line || '').trim();
let date = '';
let time = '';
let levelText = '';
let body: string;
const sys = raw.match(SYSLOG_PREFIX);
if (sys) {
date = sys[1];
time = sys[2];
let message = sys[3];
const ld = splitLevelDash(message);
if (ld) {
// go-logging: "LEVEL - message"
levelText = ld.level;
body = ld.rest;
} else {
// Strip the redundant Go std-log date, then try to lift a level out of a
// telego "[timestamp] LEVEL ..." line; otherwise keep the message as-is.
message = message.replace(GO_LOG_DATE, '');
const tg = message.match(TELEGO);
if (tg && LEVELS.indexOf(tg[1]) >= 0) {
levelText = tg[1];
body = tg[2];
} else {
body = message;
}
}
} else {
const app = raw.match(APP_LOG);
if (app) {
// App-log format: "2006/01/02 15:04:05 LEVEL - body"
date = app[1];
time = app[2];
levelText = app[3];
body = app[4];
} else {
// Plain message with no timestamp/level — show it verbatim.
body = raw;
}
}
const li = LEVELS.indexOf(levelText);
const levelClass = li >= 0 ? LEVEL_CLASSES[li] : 'level-unknown';
let service = '';
if (body.startsWith('XRAY:')) {
service = 'XRAY:';
body = body.slice('XRAY:'.length).trimStart();
} else if (body) {
service = 'X-UI:';
}
const stamp = [date, time].filter(Boolean).join(' ');
return { date, time, stamp, levelText, levelClass, service, body };
}