// Pure journal query construction and parsing for system problem history. // No I/O. Dual-mode: QML import and CommonJS require. var COREDUMP_MESSAGE_ID = "fc2e22bc6ee647b6b90729ab34a250b1"; var WARNING_ICON = ""; var MAX_ISSUES = 100; var MAX_DETAIL = 320; function journalCommand() { return [ "journalctl", "--boot=0", "--since=-24h", "--lines=100", "--output=json", "--no-pager", "--truncate-newline", "--output-fields=MESSAGE,MESSAGE_ID,_TRANSPORT,_COMM,_EXE,_PID,_UID,_SYSTEMD_UNIT,_SYSTEMD_USER_UNIT,COREDUMP_PID,COREDUMP_UID,COREDUMP_COMM,COREDUMP_EXE,COREDUMP_SIGNAL,COREDUMP_SIGNAL_NAME,COREDUMP_UNIT,COREDUMP_USER_UNIT", "--grep=^(?:[^\\r\\n]*dumped core\\.?|(?:Out of memory|Memory cgroup out of memory): Killed process [1-9][0-9]* \\([^()\\r\\n]+\\)(?: |$)|Killed /[^\\r\\n]*)", "MESSAGE_ID=" + COREDUMP_MESSAGE_ID, "+", "_TRANSPORT=kernel", "+", "_COMM=systemd-oomd", "_SYSTEMD_UNIT=systemd-oomd.service" ]; } function stringField(entry, name) { return typeof entry[name] === "string" ? entry[name] : ""; } function clean(value, limit) { var text = String(value == null ? "" : value) .replace(/[\x00-\x1f\x7f-\x9f]+/g, " ") .replace(/\s+/g, " ") .replace(/^\s+|\s+$/g, ""); return text.length > limit ? text.slice(0, limit) : text; } function numericMetadata(value) { var text = clean(value, 20); return /^[0-9]{1,20}$/.test(text) ? text : ""; } function signalMetadata(name, number) { if (/^SIG[A-Z0-9]{1,21}$/.test(name)) return name; return numericMetadata(number); } function commonMetadata(entry) { var cursor = stringField(entry, "__CURSOR"); var bootId = stringField(entry, "_BOOT_ID"); var micros = stringField(entry, "__REALTIME_TIMESTAMP"); if (!/^[\x21-\x7e]{1,512}$/.test(cursor) || !/^[0-9a-fA-F]{32}$/.test(bootId) || !/^[0-9]{1,20}$/.test(micros)) { return null; } var millis = Math.floor(Number(micros) / 1000); var date = new Date(millis); if (!isFinite(millis) || isNaN(date.getTime())) return null; var timestamp; try { timestamp = date.toISOString(); } catch (error) { return null; } return { cursor: cursor, bootId: bootId.toLowerCase(), timestamp: timestamp }; } function classify(entry) { // Journal-owned unit attribution cannot be supplied by a client, unlike // MESSAGE_ID or a process name chosen to resemble a system daemon. if (stringField(entry, "MESSAGE_ID") === COREDUMP_MESSAGE_ID && /^systemd-coredump@[^/\r\n]{1,160}\.service$/.test(stringField(entry, "_SYSTEMD_UNIT"))) { return { source: "crash", match: null }; } var message = stringField(entry, "MESSAGE"); if (stringField(entry, "_TRANSPORT") === "kernel") { var kernel = message.match(/^(?:Out of memory|Memory cgroup out of memory): Killed process ([1-9][0-9]*) \(([^()\r\n]{1,128})\)(?:\s|$)/); if (kernel) return { source: "kernel-oom", match: kernel }; } if (stringField(entry, "_COMM") === "systemd-oomd" && stringField(entry, "_SYSTEMD_UNIT") === "systemd-oomd.service") { var oomd = message.match(/^Killed (\/[\s\S]{1,512}?)(?: due to[\s\S]*)?\.?$/); if (oomd) return { source: "oomd", match: oomd }; } return null; } function eventMetadata(entry, classification) { var metadata; if (classification.source === "crash") { metadata = { executable: clean(stringField(entry, "COREDUMP_EXE"), 240), comm: clean(stringField(entry, "COREDUMP_COMM"), 160), pid: numericMetadata(stringField(entry, "COREDUMP_PID")), signal: signalMetadata(stringField(entry, "COREDUMP_SIGNAL_NAME"), stringField(entry, "COREDUMP_SIGNAL")), unit: clean(stringField(entry, "COREDUMP_UNIT"), 160), userUnit: clean(stringField(entry, "COREDUMP_USER_UNIT"), 160), uid: numericMetadata(stringField(entry, "COREDUMP_UID")) }; } else if (classification.source === "kernel-oom") { metadata = { executable: "", comm: clean(classification.match[2], 128), pid: numericMetadata(classification.match[1]), signal: "SIGKILL", unit: "", userUnit: "", uid: "" }; } else { metadata = { executable: "", comm: "", pid: "", signal: "SIGKILL", unit: "", userUnit: "", uid: "", target: clean(classification.match[1] || classification.match[2], 200) }; } return metadata; } // Only complete, lossless identity may join occurrences. Display cleanup or // truncation must never make two different applications look like one. function crashGroupId(entry) { var executable = stringField(entry, "COREDUMP_EXE"); var uid = stringField(entry, "COREDUMP_UID"); if (executable[0] !== "/" || executable !== clean(executable, 240) || !uid || uid !== numericMetadata(uid)) return ""; return "crashes:" + JSON.stringify([uid, executable]); } // Derived presentation only: retain the journal records as the source of truth. function groupIncidents(issues) { var groups = {}; var result = []; issues.slice().sort(function (a, b) { return Date.parse(b.context.timestamp) - Date.parse(a.context.timestamp); }).forEach(function (issue) { var key = issue.context.source === "crash" ? issue.groupId : ""; if (!key) { result.push(issue); return; } if (!Object.prototype.hasOwnProperty.call(groups, key)) { var group = Object.assign({}, issue, { id: key, events: [] }); groups[key] = group; result.push(group); } groups[key].events.push(issue.context); }); return result; } function issueLabel(source) { if (source === "crash") return "process crashed"; if (source === "kernel-oom") return "process killed by kernel oom"; return "process killed by systemd-oomd"; } function issueDetail(timestamp, metadata) { var parts = [timestamp]; var process = metadata.comm || metadata.executable || metadata.target; if (process) parts.push(process); if (metadata.pid) parts.push("pid " + metadata.pid); if (metadata.signal) parts.push(metadata.signal); if (metadata.userUnit || metadata.unit) parts.push(metadata.userUnit || metadata.unit); if (metadata.uid) parts.push("uid " + metadata.uid); return clean(parts.join(" · "), MAX_DETAIL); } function parseJournal(text) { var sourceText = String(text == null ? "" : text); if (!sourceText.trim()) return { ok: true, issues: [] }; var lines = sourceText.split(/\r?\n/); var issues = []; var seen = {}; var ok = true; for (var i = 0; i < lines.length; i++) { if (!lines[i].trim()) continue; var entry; try { entry = JSON.parse(lines[i]); } catch (error) { ok = false; continue; } if (!entry || typeof entry !== "object" || Object.prototype.toString.call(entry) === "[object Array]") { ok = false; continue; } var classification = classify(entry); if (!classification) continue; var common = commonMetadata(entry); if (!common) { ok = false; continue; } var seenKey = "$" + common.cursor; if (Object.prototype.hasOwnProperty.call(seen, seenKey)) continue; seen[seenKey] = true; if (issues.length < MAX_ISSUES) { var metadata = eventMetadata(entry, classification); issues.push({ id: "journal:" + common.cursor, groupId: classification.source === "crash" ? crashGroupId(entry) : "", severity: "warning", icon: WARNING_ICON, label: issueLabel(classification.source), detail: issueDetail(common.timestamp, metadata), action: { kind: "journal-event", bootId: common.bootId, timestamp: common.timestamp, cursor: common.cursor }, context: { source: classification.source, bootId: common.bootId, timestamp: common.timestamp, cursor: common.cursor, metadata: metadata } }); } } issues.sort(function (a, b) { return Date.parse(b.context.timestamp) - Date.parse(a.context.timestamp); }); return { ok: ok, issues: issues }; } if (typeof module !== "undefined" && module.exports) { module.exports = { journalCommand: journalCommand, parseJournal: parseJournal, groupIncidents: groupIncidents }; }