1<!DOCTYPE html> 2<!-- SPDX-License-Identifier: LGPL-2.1-or-later --> 3<html> 4<head> 5 <title>Journal</title> 6 <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> 7 <style type="text/css"> 8 div#divlogs, div#diventry { 9 font-family: monospace; 10 font-size: 7pt; 11 background-color: #ffffff; 12 padding: 1em; 13 margin: 2em 0em; 14 border-radius: 10px 10px 10px 10px; 15 border: 1px solid threedshadow; 16 white-space: nowrap; 17 overflow-x: scroll; 18 } 19 div#diventry { 20 display: none; 21 } 22 div#divlogs { 23 display: block; 24 } 25 body { 26 background-color: #ededed; 27 color: #313739; 28 font: message-box; 29 margin: 3em; 30 } 31 td.timestamp { 32 text-align: right; 33 border-right: 1px dotted lightgrey; 34 padding-right: 5px; 35 } 36 td.process { 37 border-right: 1px dotted lightgrey; 38 padding-left: 5px; 39 padding-right: 5px; 40 } 41 td.message { 42 padding-left: 5px; 43 } 44 td.message > a:link, td.message > a:visited { 45 text-decoration: none; 46 color: #313739; 47 } 48 td.message-error { 49 padding-left: 5px; 50 color: red; 51 font-weight: bold; 52 } 53 td.message-error > a:link, td.message-error > a:visited { 54 text-decoration: none; 55 color: red; 56 } 57 td.message-highlight { 58 padding-left: 5px; 59 font-weight: bold; 60 } 61 td.message-highlight > a:link, td.message-highlight > a:visited { 62 text-decoration: none; 63 color: #313739; 64 } 65 td > a:hover, td > a:active { 66 text-decoration: underline; 67 color: #c13739; 68 } 69 table#tablelogs, table#tableentry { 70 border-collapse: collapse; 71 } 72 td.field { 73 text-align: right; 74 border-right: 1px dotted lightgrey; 75 padding-right: 5px; 76 } 77 td.data { 78 padding-left: 5px; 79 } 80 div#keynav { 81 text-align: center; 82 font-size: 7pt; 83 color: #818789; 84 padding-top: 2em; 85 } 86 span.key { 87 font-weight: bold; 88 color: #313739; 89 } 90 div#buttonnav { 91 text-align: center; 92 } 93 button { 94 font-size: 18pt; 95 font-weight: bold; 96 width: 2em; 97 height: 2em; 98 } 99 div#filternav { 100 text-align: center; 101 } 102 select { 103 width: 50em; 104 } 105 </style> 106</head> 107 108<body> 109 <!-- TODO: 110 - live display 111 - show red lines for reboots --> 112 113 <h1 id="title"></h1> 114 115 <div id="os"></div> 116 <div id="virtualization"></div> 117 <div id="cutoff"></div> 118 <div id="machine"></div> 119 <div id="usage"></div> 120 <div id="showing"></div> 121 122 <div id="filternav"> 123 <select id="filter" onchange="onFilterChange(this);" onfocus="onFilterFocus(this);"> 124 <option>No filter</option> 125 </select> 126 127 <input id="boot" type="checkbox" onchange="onBootChange(this);">Only current boot</input> 128 </div> 129 130 <div id="divlogs"><table id="tablelogs"></table></div> 131 <a name="entry"></a> 132 <div id="diventry"><table id="tableentry"></table></div> 133 134 <div id="buttonnav"> 135 <button id="head" onclick="entriesLoadHead();" title="First Page">⇤</button> 136 <button id="previous" type="button" onclick="entriesLoadPrevious();" title="Previous Page"/>←</button> 137 <button id="next" type="button" onclick="entriesLoadNext();" title="Next Page"/>→</button> 138 <button id="tail" type="button" onclick="entriesLoadTail();" title="Last Page"/>⇥</button> 139 140 <button id="more" type="button" onclick="entriesMore();" title="More Entries"/>+</button> 141 <button id="less" type="button" onclick="entriesLess();" title="Fewer Entries"/>-</button> 142 </div> 143 144 <div id="keynav"> 145 <span class="key">g</span>: First Page 146 <span class="key">←, k, BACKSPACE</span>: Previous Page 147 <span class="key">→, j, SPACE</span>: Next Page 148 <span class="key">G</span>: Last Page 149 <span class="key">+</span>: More entries 150 <span class="key">-</span>: Fewer entries 151 </div> 152 153 <script type="text/javascript"> 154 var first_cursor = null; 155 var last_cursor = null; 156 157 function getNEntries() { 158 var n; 159 n = localStorage["n_entries"]; 160 if (n == null) 161 return 50; 162 n = parseInt(n); 163 if (n < 10) 164 return 10; 165 if (n > 1000) 166 return 1000; 167 return n; 168 } 169 170 function showNEntries(n) { 171 var showing = document.getElementById("showing"); 172 showing.innerHTML = "Showing <b>" + n.toString() + "</b> entries."; 173 } 174 175 function setNEntries(n) { 176 if (n < 10) 177 return 10; 178 if (n > 1000) 179 return 1000; 180 localStorage["n_entries"] = n.toString(); 181 showNEntries(n); 182 } 183 184 function machineLoad() { 185 var request = new XMLHttpRequest(); 186 request.open("GET", "machine"); 187 request.onreadystatechange = machineOnResult; 188 request.setRequestHeader("Accept", "application/json"); 189 request.send(null); 190 } 191 192 function formatBytes(u) { 193 if (u >= 1024*1024*1024*1024) 194 return (u/1024/1024/1024/1024).toFixed(1) + " TiB"; 195 else if (u >= 1024*1024*1024) 196 return (u/1024/1024/1024).toFixed(1) + " GiB"; 197 else if (u >= 1024*1024) 198 return (u/1024/1024).toFixed(1) + " MiB"; 199 else if (u >= 1024) 200 return (u/1024).toFixed(1) + " KiB"; 201 else 202 return u.toString() + " B"; 203 } 204 205 function escapeHTML(s) { 206 return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">"); 207 } 208 209 function machineOnResult(event) { 210 if ((event.currentTarget.readyState != 4) || 211 (event.currentTarget.status != 200 && event.currentTarget.status != 0)) 212 return; 213 214 var d = JSON.parse(event.currentTarget.responseText); 215 216 var title = document.getElementById("title"); 217 title.innerHTML = 'Journal of ' + escapeHTML(d.hostname); 218 document.title = 'Journal of ' + escapeHTML(d.hostname); 219 220 var machine = document.getElementById("machine"); 221 machine.innerHTML = 'Machine ID is <b>' + d.machine_id + '</b>, current boot ID is <b>' + d.boot_id + '</b>.'; 222 223 var cutoff = document.getElementById("cutoff"); 224 var from = new Date(parseInt(d.cutoff_from_realtime) / 1000); 225 var to = new Date(parseInt(d.cutoff_to_realtime) / 1000); 226 cutoff.innerHTML = 'Journal begins at <b>' + from.toLocaleString() + '</b> and ends at <b>' + to.toLocaleString() + '</b>.'; 227 228 var usage = document.getElementById("usage"); 229 usage.innerHTML = 'Disk usage is <b>' + formatBytes(parseInt(d.usage)) + '</b>.'; 230 231 var os = document.getElementById("os"); 232 os.innerHTML = 'Operating system is <b>' + escapeHTML(d.os_pretty_name) + '</b>.'; 233 234 var virtualization = document.getElementById("virtualization"); 235 virtualization.innerHTML = d.virtualization == "bare" ? "Running on <b>bare metal</b>." : "Running on virtualization <b>" + escapeHTML(d.virtualization) + "</b>."; 236 } 237 238 function entriesLoad(range) { 239 240 if (range == null) { 241 if (localStorage["cursor"] != null && localStorage["cursor"] != "") 242 range = localStorage["cursor"] + ":0"; 243 else 244 range = ""; 245 } 246 247 var url = "entries"; 248 249 if (localStorage["filter"] != "" && localStorage["filter"] != null) { 250 url += "?_SYSTEMD_UNIT=" + escape(localStorage["filter"]); 251 252 if (localStorage["boot"] == "1") 253 url += "&boot"; 254 } else { 255 if (localStorage["boot"] == "1") 256 url += "?boot"; 257 } 258 259 var request = new XMLHttpRequest(); 260 request.open("GET", url); 261 request.onreadystatechange = entriesOnResult; 262 request.setRequestHeader("Accept", "application/json"); 263 request.setRequestHeader("Range", "entries=" + range + ":" + getNEntries().toString()); 264 request.send(null); 265 } 266 267 function entriesLoadNext() { 268 if (last_cursor == null) 269 entriesLoad(""); 270 else 271 entriesLoad(last_cursor + ":1"); 272 } 273 274 function entriesLoadPrevious() { 275 if (first_cursor == null) 276 entriesLoad(""); 277 else 278 entriesLoad(first_cursor + ":-" + getNEntries().toString()); 279 } 280 281 function entriesLoadHead() { 282 entriesLoad(""); 283 } 284 285 function entriesLoadTail() { 286 entriesLoad(":-" + getNEntries().toString()); 287 } 288 289 function entriesOnResult(event) { 290 291 if ((event.currentTarget.readyState != 4) || 292 (event.currentTarget.status != 200 && event.currentTarget.status != 0)) 293 return; 294 295 var logs = document.getElementById("tablelogs"); 296 297 var lc = null; 298 var fc = null; 299 300 var i, l = event.currentTarget.responseText.split('\n'); 301 302 if (l.length <= 1) { 303 logs.innerHTML = '<tbody><tr><td colspan="3"><i>No further entries...</i></td></tr></tbody>'; 304 return; 305 } 306 307 var buf = ''; 308 309 for (i in l) { 310 if (l[i] == '') 311 continue; 312 313 var d = JSON.parse(l[i]); 314 if (d.MESSAGE == undefined || d.__CURSOR == undefined) 315 continue; 316 317 if (fc == null) 318 fc = d.__CURSOR; 319 lc = d.__CURSOR; 320 321 var priority; 322 if (d.PRIORITY != undefined) 323 priority = parseInt(d.PRIORITY); 324 else 325 priority = 6; 326 327 var clazz; 328 if (priority <= 3) 329 clazz = "message-error"; 330 else if (priority <= 5) 331 clazz = "message-highlight"; 332 else 333 clazz = "message"; 334 335 buf += '<tr><td class="timestamp">'; 336 337 if (d.__REALTIME_TIMESTAMP != undefined) { 338 var timestamp = new Date(parseInt(d.__REALTIME_TIMESTAMP) / 1000); 339 buf += timestamp.toLocaleString(); 340 } 341 342 buf += '</td><td class="process">'; 343 344 if (d.SYSLOG_IDENTIFIER != undefined) 345 buf += escapeHTML(d.SYSLOG_IDENTIFIER); 346 else if (d._COMM != undefined) 347 buf += escapeHTML(d._COMM); 348 349 if (d._PID != undefined) 350 buf += "[" + escapeHTML(d._PID) + "]"; 351 else if (d.SYSLOG_PID != undefined) 352 buf += "[" + escapeHTML(d.SYSLOG_PID) + "]"; 353 354 buf += '</td><td class="' + clazz + '"><a href="#entry" onclick="onMessageClick(\'' + d.__CURSOR + '\');">'; 355 356 if (d.MESSAGE == null) 357 buf += "[blob data]"; 358 else if (d.MESSAGE instanceof Array) 359 buf += "[" + formatBytes(d.MESSAGE.length) + " blob data]"; 360 else 361 buf += escapeHTML(d.MESSAGE); 362 363 buf += '</a></td></tr>'; 364 } 365 366 logs.innerHTML = '<tbody>' + buf + '</tbody>'; 367 368 if (fc != null) { 369 first_cursor = fc; 370 localStorage["cursor"] = fc; 371 } 372 if (lc != null) 373 last_cursor = lc; 374 } 375 376 function entriesMore() { 377 setNEntries(getNEntries() + 10); 378 entriesLoad(first_cursor); 379 } 380 381 function entriesLess() { 382 setNEntries(getNEntries() - 10); 383 entriesLoad(first_cursor); 384 } 385 386 function onResultMessageClick(event) { 387 if ((event.currentTarget.readyState != 4) || 388 (event.currentTarget.status != 200 && event.currentTarget.status != 0)) 389 return; 390 391 var d = JSON.parse(event.currentTarget.responseText); 392 393 document.getElementById("diventry").style.display = "block"; 394 var entry = document.getElementById("tableentry"); 395 396 var buf = ""; 397 for (var key in d) { 398 var data = d[key]; 399 400 if (data == null) 401 data = "[blob data]"; 402 else if (data instanceof Array) 403 data = "[" + formatBytes(data.length) + " blob data]"; 404 else 405 data = escapeHTML(data); 406 407 buf += '<tr><td class="field">' + key + '</td><td class="data">' + data + '</td></tr>'; 408 } 409 entry.innerHTML = '<tbody>' + buf + '</tbody>'; 410 } 411 412 function onMessageClick(t) { 413 var request = new XMLHttpRequest(); 414 request.open("GET", "entries?discrete"); 415 request.onreadystatechange = onResultMessageClick; 416 request.setRequestHeader("Accept", "application/json"); 417 request.setRequestHeader("Range", "entries=" + t + ":0:1"); 418 request.send(null); 419 } 420 421 function onKeyUp(event) { 422 switch (event.keyCode) { 423 case 8: 424 case 37: 425 case 75: 426 entriesLoadPrevious(); 427 break; 428 case 32: 429 case 39: 430 case 74: 431 entriesLoadNext(); 432 break; 433 434 case 71: 435 if (event.shiftKey) 436 entriesLoadTail(); 437 else 438 entriesLoadHead(); 439 break; 440 case 171: 441 entriesMore(); 442 break; 443 case 173: 444 entriesLess(); 445 break; 446 } 447 } 448 449 function onMouseWheel(event) { 450 if (event.detail < 0 || event.wheelDelta > 0) 451 entriesLoadPrevious(); 452 else 453 entriesLoadNext(); 454 } 455 456 function onResultFilterFocus(event) { 457 if ((event.currentTarget.readyState != 4) || 458 (event.currentTarget.status != 200 && event.currentTarget.status != 0)) 459 return; 460 461 var f = document.getElementById("filter"); 462 463 var l = event.currentTarget.responseText.split('\n'); 464 var buf = '<option>No filter</option>'; 465 var j = -1; 466 467 for (i in l) { 468 469 if (l[i] == '') 470 continue; 471 472 var d = JSON.parse(l[i]); 473 if (d._SYSTEMD_UNIT == undefined) 474 continue; 475 476 buf += '<option value="' + escape(d._SYSTEMD_UNIT) + '">' + escapeHTML(d._SYSTEMD_UNIT) + '</option>'; 477 478 if (d._SYSTEMD_UNIT == localStorage["filter"]) 479 j = i; 480 } 481 482 if (j < 0) { 483 if (localStorage["filter"] != null && localStorage["filter"] != "") { 484 buf += '<option value="' + escape(localStorage["filter"]) + '">' + escapeHTML(localStorage["filter"]) + '</option>'; 485 j = i + 1; 486 } else 487 j = 0; 488 } 489 490 f.innerHTML = buf; 491 f.selectedIndex = j; 492 } 493 494 function onFilterFocus(w) { 495 var request = new XMLHttpRequest(); 496 request.open("GET", "fields/_SYSTEMD_UNIT"); 497 request.onreadystatechange = onResultFilterFocus; 498 request.setRequestHeader("Accept", "application/json"); 499 request.send(null); 500 } 501 502 function onFilterChange(w) { 503 if (w.selectedIndex <= 0) 504 localStorage["filter"] = ""; 505 else 506 localStorage["filter"] = unescape(w.options[w.selectedIndex].value); 507 508 entriesLoadHead(); 509 } 510 511 function onBootChange(w) { 512 localStorage["boot"] = w.checked ? "1" : "0"; 513 entriesLoadHead(); 514 } 515 516 function initFilter() { 517 var f = document.getElementById("filter"); 518 519 var buf = '<option>No filter</option>'; 520 521 var filter = localStorage["filter"]; 522 var j; 523 if (filter != null && filter != "") { 524 buf += '<option value="' + escape(filter) + '">' + escapeHTML(filter) + '</option>'; 525 j = 1; 526 } else 527 j = 0; 528 529 f.innerHTML = buf; 530 f.selectedIndex = j; 531 } 532 533 function installHandlers() { 534 document.onkeyup = onKeyUp; 535 536 var logs = document.getElementById("divlogs"); 537 logs.addEventListener("mousewheel", onMouseWheel, false); 538 logs.addEventListener("DOMMouseScroll", onMouseWheel, false); 539 } 540 541 machineLoad(); 542 entriesLoad(null); 543 showNEntries(getNEntries()); 544 initFilter(); 545 installHandlers(); 546 </script> 547</body> 548</html> 549