| <!DOCTYPE html> |
| <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) |
| range = localStorage["cursor"] + ":0"; |
| if (range == null) |
| 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; |
| |
| 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"; |
| 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; |
| |
| 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() { |
| f = document.getElementById("filter"); |
| |
| var buf = '<option>No filter</option>'; |
| |
| var filter = localStorage["filter"]; |
| 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; |
| |
| logs = document.getElementById("divlogs"); |
| logs.addEventListener("mousewheel", onMouseWheel, false); |
| logs.addEventListener("DOMMouseScroll", onMouseWheel, false); |
| } |
| |
| machineLoad(); |
| entriesLoad(null); |
| showNEntries(getNEntries()); |
| initFilter(); |
| installHandlers(); |
| </script> |
| </body> |
| </html> |