| <!DOCTYPE html> | |
| <!-- SPDX-License-Identifier: LGPL-2.1-or-later --> | |
| <html> | |
| <head> | |
| <title>Journal</title> | |
| <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> | |
| <style type="text/css"> | |
| div#divlogs, div#diventry { | |
| font-family: monospace; | |
| font-size: 7pt; | |
| background-color: #ffffff; | |
| padding: 1em; | |
| margin: 2em 0em; | |
| border-radius: 10px 10px 10px 10px; | |
| border: 1px solid threedshadow; | |
| white-space: nowrap; | |
| overflow-x: scroll; | |
| } | |
| div#diventry { | |
| display: none; | |
| } | |
| div#divlogs { | |
| display: block; | |
| } | |
| body { | |
| background-color: #ededed; | |
| color: #313739; | |
| font: message-box; | |
| margin: 3em; | |
| } | |
| td.timestamp { | |
| text-align: right; | |
| border-right: 1px dotted lightgrey; | |
| padding-right: 5px; | |
| } | |
| td.process { | |
| border-right: 1px dotted lightgrey; | |
| padding-left: 5px; | |
| padding-right: 5px; | |
| } | |
| td.message { | |
| padding-left: 5px; | |
| } | |
| td.message > a:link, td.message > a:visited { | |
| text-decoration: none; | |
| color: #313739; | |
| } | |
| td.message-error { | |
| padding-left: 5px; | |
| color: red; | |
| font-weight: bold; | |
| } | |
| td.message-error > a:link, td.message-error > a:visited { | |
| text-decoration: none; | |
| color: red; | |
| } | |
| td.message-highlight { | |
| padding-left: 5px; | |
| font-weight: bold; | |
| } | |
| td.message-highlight > a:link, td.message-highlight > a:visited { | |
| text-decoration: none; | |
| color: #313739; | |
| } | |
| td > a:hover, td > a:active { | |
| text-decoration: underline; | |
| color: #c13739; | |
| } | |
| table#tablelogs, table#tableentry { | |
| border-collapse: collapse; | |
| } | |
| td.field { | |
| text-align: right; | |
| border-right: 1px dotted lightgrey; | |
| padding-right: 5px; | |
| } | |
| td.data { | |
| padding-left: 5px; | |
| } | |
| div#keynav { | |
| text-align: center; | |
| font-size: 7pt; | |
| color: #818789; | |
| padding-top: 2em; | |
| } | |
| span.key { | |
| font-weight: bold; | |
| color: #313739; | |
| } | |
| div#buttonnav { | |
| text-align: center; | |
| } | |
| button { | |
| font-size: 18pt; | |
| font-weight: bold; | |
| width: 2em; | |
| height: 2em; | |
| } | |
| div#filternav { | |
| text-align: center; | |
| } | |
| select { | |
| width: 50em; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- TODO: | |
| - live display | |
| - show red lines for reboots --> | |
| <h1 id="title"></h1> | |
| <div id="os"></div> | |
| <div id="virtualization"></div> | |
| <div id="cutoff"></div> | |
| <div id="machine"></div> | |
| <div id="usage"></div> | |
| <div id="showing"></div> | |
| <div id="filternav"> | |
| <select id="filter" onchange="onFilterChange(this);" onfocus="onFilterFocus(this);"> | |
| <option>No filter</option> | |
| </select> | |
| | |
| <input id="boot" type="checkbox" onchange="onBootChange(this);">Only current boot</input> | |
| </div> | |
| <div id="divlogs"><table id="tablelogs"></table></div> | |
| <a name="entry"></a> | |
| <div id="diventry"><table id="tableentry"></table></div> | |
| <div id="buttonnav"> | |
| <button id="head" onclick="entriesLoadHead();" title="First Page">⇤</button> | |
| <button id="previous" type="button" onclick="entriesLoadPrevious();" title="Previous Page"/>←</button> | |
| <button id="next" type="button" onclick="entriesLoadNext();" title="Next Page"/>→</button> | |
| <button id="tail" type="button" onclick="entriesLoadTail();" title="Last Page"/>⇥</button> | |
| | |
| <button id="more" type="button" onclick="entriesMore();" title="More Entries"/>+</button> | |
| <button id="less" type="button" onclick="entriesLess();" title="Fewer Entries"/>-</button> | |
| </div> | |
| <div id="keynav"> | |
| <span class="key">g</span>: First Page | |
| <span class="key">←, k, BACKSPACE</span>: Previous Page | |
| <span class="key">→, j, SPACE</span>: Next Page | |
| <span class="key">G</span>: Last Page | |
| <span class="key">+</span>: More entries | |
| <span class="key">-</span>: Fewer entries | |
| </div> | |
| <script type="text/javascript"> | |
| var first_cursor = null; | |
| var last_cursor = null; | |
| function getNEntries() { | |
| var n; | |
| n = localStorage["n_entries"]; | |
| if (n == null) | |
| return 50; | |
| n = parseInt(n); | |
| if (n < 10) | |
| return 10; | |
| if (n > 1000) | |
| return 1000; | |
| return n; | |
| } | |
| function showNEntries(n) { | |
| var showing = document.getElementById("showing"); | |
| showing.innerHTML = "Showing <b>" + n.toString() + "</b> entries."; | |
| } | |
| function setNEntries(n) { | |
| if (n < 10) | |
| return 10; | |
| if (n > 1000) | |
| return 1000; | |
| localStorage["n_entries"] = n.toString(); | |
| showNEntries(n); | |
| } | |
| function machineLoad() { | |
| var request = new XMLHttpRequest(); | |
| request.open("GET", "machine"); | |
| request.onreadystatechange = machineOnResult; | |
| request.setRequestHeader("Accept", "application/json"); | |
| request.send(null); | |
| } | |
| function formatBytes(u) { | |
| if (u >= 1024*1024*1024*1024) | |
| return (u/1024/1024/1024/1024).toFixed(1) + " TiB"; | |
| else if (u >= 1024*1024*1024) | |
| return (u/1024/1024/1024).toFixed(1) + " GiB"; | |
| else if (u >= 1024*1024) | |
| return (u/1024/1024).toFixed(1) + " MiB"; | |
| else if (u >= 1024) | |
| return (u/1024).toFixed(1) + " KiB"; | |
| else | |
| return u.toString() + " B"; | |
| } | |
| function escapeHTML(s) { | |
| return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">"); | |
| } | |
| function machineOnResult(event) { | |
| if ((event.currentTarget.readyState != 4) || | |
| (event.currentTarget.status != 200 && event.currentTarget.status != 0)) | |
| return; | |
| var d = JSON.parse(event.currentTarget.responseText); | |
| var title = document.getElementById("title"); | |
| title.innerHTML = 'Journal of ' + escapeHTML(d.hostname); | |
| document.title = 'Journal of ' + escapeHTML(d.hostname); | |
| var machine = document.getElementById("machine"); | |
| machine.innerHTML = 'Machine ID is <b>' + d.machine_id + '</b>, current boot ID is <b>' + d.boot_id + '</b>.'; | |
| var cutoff = document.getElementById("cutoff"); | |
| var from = new Date(parseInt(d.cutoff_from_realtime) / 1000); | |
| var to = new Date(parseInt(d.cutoff_to_realtime) / 1000); | |
| cutoff.innerHTML = 'Journal begins at <b>' + from.toLocaleString() + '</b> and ends at <b>' + to.toLocaleString() + '</b>.'; | |
| var usage = document.getElementById("usage"); | |
| usage.innerHTML = 'Disk usage is <b>' + formatBytes(parseInt(d.usage)) + '</b>.'; | |
| var os = document.getElementById("os"); | |
| os.innerHTML = 'Operating system is <b>' + escapeHTML(d.os_pretty_name) + '</b>.'; | |
| var virtualization = document.getElementById("virtualization"); | |
| virtualization.innerHTML = d.virtualization == "bare" ? "Running on <b>bare metal</b>." : "Running on virtualization <b>" + escapeHTML(d.virtualization) + "</b>."; | |
| } | |
| function entriesLoad(range) { | |
| if (range == null) { | |
| if (localStorage["cursor"] != null && localStorage["cursor"] != "") | |
| range = localStorage["cursor"] + ":0"; | |
| else | |
| range = ""; | |
| } | |
| var url = "entries"; | |
| if (localStorage["filter"] != "" && localStorage["filter"] != null) { | |
| url += "?_SYSTEMD_UNIT=" + escape(localStorage["filter"]); | |
| if (localStorage["boot"] == "1") | |
| url += "&boot"; | |
| } else { | |
| if (localStorage["boot"] == "1") | |
| url += "?boot"; | |
| } | |
| var request = new XMLHttpRequest(); | |
| request.open("GET", url); | |
| request.onreadystatechange = entriesOnResult; | |
| request.setRequestHeader("Accept", "application/json"); | |
| request.setRequestHeader("Range", "entries=" + range + ":" + getNEntries().toString()); | |
| request.send(null); | |
| } | |
| function entriesLoadNext() { | |
| if (last_cursor == null) | |
| entriesLoad(""); | |
| else | |
| entriesLoad(last_cursor + ":1"); | |
| } | |
| function entriesLoadPrevious() { | |
| if (first_cursor == null) | |
| entriesLoad(""); | |
| else | |
| entriesLoad(first_cursor + ":-" + getNEntries().toString()); | |
| } | |
| function entriesLoadHead() { | |
| entriesLoad(""); | |
| } | |
| function entriesLoadTail() { | |
| entriesLoad(":-" + getNEntries().toString()); | |
| } | |
| function entriesOnResult(event) { | |
| if ((event.currentTarget.readyState != 4) || | |
| (event.currentTarget.status != 200 && event.currentTarget.status != 0)) | |
| return; | |
| var logs = document.getElementById("tablelogs"); | |
| var lc = null; | |
| var fc = null; | |
| var i, l = event.currentTarget.responseText.split('\n'); | |
| if (l.length <= 1) { | |
| logs.innerHTML = '<tbody><tr><td colspan="3"><i>No further entries...</i></td></tr></tbody>'; | |
| return; | |
| } | |
| var buf = ''; | |
| for (i in l) { | |
| if (l[i] == '') | |
| continue; | |
| var d = JSON.parse(l[i]); | |
| if (d.MESSAGE == undefined || d.__CURSOR == undefined) | |
| continue; | |
| if (fc == null) | |
| fc = d.__CURSOR; | |
| lc = d.__CURSOR; | |
| var priority; | |
| if (d.PRIORITY != undefined) | |
| priority = parseInt(d.PRIORITY); | |
| else | |
| priority = 6; | |
| var clazz; | |
| if (priority <= 3) | |
| clazz = "message-error"; | |
| else if (priority <= 5) | |
| clazz = "message-highlight"; | |
| else | |
| clazz = "message"; | |
| buf += '<tr><td class="timestamp">'; | |
| if (d.__REALTIME_TIMESTAMP != undefined) { | |
| var timestamp = new Date(parseInt(d.__REALTIME_TIMESTAMP) / 1000); | |
| buf += timestamp.toLocaleString(); | |
| } | |
| buf += '</td><td class="process">'; | |
| if (d.SYSLOG_IDENTIFIER != undefined) | |
| buf += escapeHTML(d.SYSLOG_IDENTIFIER); | |
| else if (d._COMM != undefined) | |
| buf += escapeHTML(d._COMM); | |
| if (d._PID != undefined) | |
| buf += "[" + escapeHTML(d._PID) + "]"; | |
| else if (d.SYSLOG_PID != undefined) | |
| buf += "[" + escapeHTML(d.SYSLOG_PID) + "]"; | |
| buf += '</td><td class="' + clazz + '"><a href="#entry" onclick="onMessageClick(\'' + d.__CURSOR + '\');">'; | |
| if (d.MESSAGE == null) | |
| buf += "[blob data]"; | |
| else if (d.MESSAGE instanceof Array) | |
| buf += "[" + formatBytes(d.MESSAGE.length) + " blob data]"; | |
| else | |
| buf += escapeHTML(d.MESSAGE); | |
| buf += '</a></td></tr>'; | |
| } | |
| logs.innerHTML = '<tbody>' + buf + '</tbody>'; | |
| if (fc != null) { | |
| first_cursor = fc; | |
| localStorage["cursor"] = fc; | |
| } | |
| if (lc != null) | |
| last_cursor = lc; | |
| } | |
| function entriesMore() { | |
| setNEntries(getNEntries() + 10); | |
| entriesLoad(first_cursor); | |
| } | |
| function entriesLess() { | |
| setNEntries(getNEntries() - 10); | |
| entriesLoad(first_cursor); | |
| } | |
| function onResultMessageClick(event) { | |
| if ((event.currentTarget.readyState != 4) || | |
| (event.currentTarget.status != 200 && event.currentTarget.status != 0)) | |
| return; | |
| var d = JSON.parse(event.currentTarget.responseText); | |
| document.getElementById("diventry").style.display = "block"; | |
| var entry = document.getElementById("tableentry"); | |
| var buf = ""; | |
| for (var key in d) { | |
| var data = d[key]; | |
| if (data == null) | |
| data = "[blob data]"; | |
| else if (data instanceof Array) | |
| data = "[" + formatBytes(data.length) + " blob data]"; | |
| else | |
| data = escapeHTML(data); | |
| buf += '<tr><td class="field">' + key + '</td><td class="data">' + data + '</td></tr>'; | |
| } | |
| entry.innerHTML = '<tbody>' + buf + '</tbody>'; | |
| } | |
| function onMessageClick(t) { | |
| var request = new XMLHttpRequest(); | |
| request.open("GET", "entries?discrete"); | |
| request.onreadystatechange = onResultMessageClick; | |
| request.setRequestHeader("Accept", "application/json"); | |
| request.setRequestHeader("Range", "entries=" + t + ":0:1"); | |
| request.send(null); | |
| } | |
| function onKeyUp(event) { | |
| switch (event.keyCode) { | |
| case 8: | |
| case 37: | |
| case 75: | |
| entriesLoadPrevious(); | |
| break; | |
| case 32: | |
| case 39: | |
| case 74: | |
| entriesLoadNext(); | |
| break; | |
| case 71: | |
| if (event.shiftKey) | |
| entriesLoadTail(); | |
| else | |
| entriesLoadHead(); | |
| break; | |
| case 171: | |
| entriesMore(); | |
| break; | |
| case 173: | |
| entriesLess(); | |
| break; | |
| } | |
| } | |
| function onMouseWheel(event) { | |
| if (event.detail < 0 || event.wheelDelta > 0) | |
| entriesLoadPrevious(); | |
| else | |
| entriesLoadNext(); | |
| } | |
| function onResultFilterFocus(event) { | |
| if ((event.currentTarget.readyState != 4) || | |
| (event.currentTarget.status != 200 && event.currentTarget.status != 0)) | |
| return; | |
| var f = document.getElementById("filter"); | |
| var l = event.currentTarget.responseText.split('\n'); | |
| var buf = '<option>No filter</option>'; | |
| var j = -1; | |
| for (i in l) { | |
| if (l[i] == '') | |
| continue; | |
| var d = JSON.parse(l[i]); | |
| if (d._SYSTEMD_UNIT == undefined) | |
| continue; | |
| buf += '<option value="' + escape(d._SYSTEMD_UNIT) + '">' + escapeHTML(d._SYSTEMD_UNIT) + '</option>'; | |
| if (d._SYSTEMD_UNIT == localStorage["filter"]) | |
| j = i; | |
| } | |
| if (j < 0) { | |
| if (localStorage["filter"] != null && localStorage["filter"] != "") { | |
| buf += '<option value="' + escape(localStorage["filter"]) + '">' + escapeHTML(localStorage["filter"]) + '</option>'; | |
| j = i + 1; | |
| } else | |
| j = 0; | |
| } | |
| f.innerHTML = buf; | |
| f.selectedIndex = j; | |
| } | |
| function onFilterFocus(w) { | |
| var request = new XMLHttpRequest(); | |
| request.open("GET", "fields/_SYSTEMD_UNIT"); | |
| request.onreadystatechange = onResultFilterFocus; | |
| request.setRequestHeader("Accept", "application/json"); | |
| request.send(null); | |
| } | |
| function onFilterChange(w) { | |
| if (w.selectedIndex <= 0) | |
| localStorage["filter"] = ""; | |
| else | |
| localStorage["filter"] = unescape(w.options[w.selectedIndex].value); | |
| entriesLoadHead(); | |
| } | |
| function onBootChange(w) { | |
| localStorage["boot"] = w.checked ? "1" : "0"; | |
| entriesLoadHead(); | |
| } | |
| function initFilter() { | |
| var f = document.getElementById("filter"); | |
| var buf = '<option>No filter</option>'; | |
| var filter = localStorage["filter"]; | |
| var j; | |
| if (filter != null && filter != "") { | |
| buf += '<option value="' + escape(filter) + '">' + escapeHTML(filter) + '</option>'; | |
| j = 1; | |
| } else | |
| j = 0; | |
| f.innerHTML = buf; | |
| f.selectedIndex = j; | |
| } | |
| function installHandlers() { | |
| document.onkeyup = onKeyUp; | |
| var logs = document.getElementById("divlogs"); | |
| logs.addEventListener("mousewheel", onMouseWheel, false); | |
| logs.addEventListener("DOMMouseScroll", onMouseWheel, false); | |
| } | |
| machineLoad(); | |
| entriesLoad(null); | |
| showNEntries(getNEntries()); | |
| initFilter(); | |
| installHandlers(); | |
| </script> | |
| </body> | |
| </html> |