mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-07-20 19:46:09 +00:00
df0e52cda8
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.
120 lines
4.1 KiB
TypeScript
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 };
|
|
}
|