New feature when visualizing logs with multiple nodes to view them all in multiple tables Changelog-None
451 lines
10 KiB
HTML
451 lines
10 KiB
HTML
<html>
|
|
<head>
|
|
<script>
|
|
function cssClean(str)
|
|
{
|
|
return str.replaceAll(/[^A-Za-z0-9]/g, "_")
|
|
}
|
|
function firstPubkey(str)
|
|
{
|
|
if (!str)
|
|
return null;
|
|
|
|
var res = str.match(/0[23][0-9a-fA-F]{64}/)
|
|
|
|
return res ? res.at(0) : null;
|
|
}
|
|
function parseDaemon(daemon)
|
|
{
|
|
var res = {};
|
|
|
|
var items = daemon.split('-');
|
|
|
|
if (daemon == "plugin-manager")
|
|
items = ["plugin manager"];
|
|
|
|
res.str = daemon;
|
|
res.name = items.pop();
|
|
res.peer = firstPubkey(items[0]);
|
|
res.short_peer = null;
|
|
|
|
if (res.peer) {
|
|
items.shift();
|
|
res.short_peer = res.peer.substring(0, 4);
|
|
}
|
|
|
|
res.parents = items;
|
|
|
|
if(res.name.startsWith("chan#"))
|
|
if(res.parents.length)
|
|
res.name = res.parents.pop() + " " + res.name;
|
|
else
|
|
res.name = "lightningd " + res.name;
|
|
|
|
return res;
|
|
}
|
|
function parseLogLine(line)
|
|
{
|
|
var res = {};
|
|
|
|
function eat_upto(re)
|
|
{
|
|
details = line.match(re);
|
|
if(!details)
|
|
return null;
|
|
value = line.substring(0, details.index);
|
|
line = line.substring(details.index + details[0].length);
|
|
return value;
|
|
}
|
|
|
|
res.date = new Date(eat_upto(/\s+/));
|
|
res.type = eat_upto(/\s+/);
|
|
res.daemon = parseDaemon(eat_upto(/: /));
|
|
res.msg = line;
|
|
|
|
return res;
|
|
}
|
|
function parseUs(lineInfo)
|
|
{
|
|
return {
|
|
pubkey: firstPubkey(lineInfo.msg),
|
|
alias: (lineInfo.msg.match(/, alias (.*) \(color /) || []).at(1),
|
|
color: (lineInfo.msg.match(/ \(color ([#][0-9a-fA-F]{6})\)/) || []).at(1),
|
|
lightngind: (lineInfo.msg.match(/\) and lightningd ([0-9a-fA-F]+)/) || []).at(1),
|
|
}
|
|
}
|
|
function toggle_type()
|
|
{
|
|
var sheet = document.getElementById('logStyleSheet').sheet;
|
|
var rule = "";
|
|
let selector = ".type-" + cssClean(this.type_str);
|
|
if(this.className == 'type_on') {
|
|
this.className = 'type_off';
|
|
rule = "display:none";
|
|
}
|
|
else {
|
|
this.className = 'type_on';
|
|
}
|
|
|
|
for(let i = 0; i < sheet.cssRules.length; i++) {
|
|
if (sheet.cssRules[i].selectorText != selector)
|
|
continue;
|
|
sheet.deleteRule(i);
|
|
sheet.insertRule(selector + " {" + rule + "}", i);
|
|
break;
|
|
}
|
|
}
|
|
function togggle_all_type()
|
|
{
|
|
for (b of this.buttons)
|
|
if(b.className == this.className)
|
|
b.click();
|
|
|
|
if(this.className == 'type_on')
|
|
this.className = 'type_off';
|
|
else
|
|
this.className = 'type_on';
|
|
}
|
|
function toggle_daemon()
|
|
{
|
|
var sheet = document.getElementById('logStyleSheet').sheet;
|
|
var rule = "";
|
|
let selector = ".daemon-" + cssClean(this.daemon.name);
|
|
if(this.className == 'daemon_on') {
|
|
this.className = 'daemon_off';
|
|
rule = "display:none";
|
|
}
|
|
else {
|
|
this.className = 'daemon_on';
|
|
}
|
|
|
|
for(let i = 0; i < sheet.cssRules.length; i++) {
|
|
if (sheet.cssRules[i].selectorText != selector)
|
|
continue;
|
|
sheet.deleteRule(i);
|
|
sheet.insertRule(selector + " {" + rule + "}", i);
|
|
break;
|
|
}
|
|
}
|
|
function togggle_all_daemon()
|
|
{
|
|
for (b of this.buttons)
|
|
if(b.className == this.className)
|
|
b.click();
|
|
|
|
if(this.className == 'daemon_on')
|
|
this.className = 'daemon_off';
|
|
else
|
|
this.className = 'daemon_on';
|
|
}
|
|
function filter_messages()
|
|
{
|
|
lines = document.getElementsByClassName("logLine");
|
|
|
|
try {
|
|
for(line of lines) {
|
|
if(line.info.msg.match(this.value))
|
|
line.style.display = "";
|
|
else
|
|
line.style.display = "none";
|
|
}
|
|
document.getElementById('filter_error').innerText = "";
|
|
}
|
|
catch (error) {
|
|
document.getElementById('filter_error').innerText = error.message;
|
|
}
|
|
}
|
|
function tab_option(type_str, regex_prefix, regex_postfix, nodes, logs)
|
|
{
|
|
str = type_str + " log with multiple nodes detected.\n\n";
|
|
str += "Would you like to open them all with new tabs?\n\n";
|
|
str += "Nodes detected:\n" + nodes.join("\n");
|
|
|
|
function make_regex(key) {
|
|
return new RegExp(regex_prefix + key + regex_postfix, "g");
|
|
}
|
|
|
|
if(!confirm(str))
|
|
return null;
|
|
|
|
function load_next_tab() {
|
|
if (!nodes.length)
|
|
return;
|
|
window.addEventListener('message', on_recv_msg);
|
|
w = window.open(window.location.href, '_blank');
|
|
w.blur();
|
|
window.focus();
|
|
}
|
|
function on_recv_msg(msg) {
|
|
if(msg.data == "v0.1-ready") {
|
|
msg.source.postMessage({"version": "v0.1-data", "logs": logs, "prefix": make_regex(nodes.shift())}, "*");
|
|
window.removeEventListener('message', on_recv_msg);
|
|
load_next_tab();
|
|
}
|
|
}
|
|
var result = make_regex(nodes.shift());
|
|
load_next_tab();
|
|
return result;
|
|
}
|
|
function single_option(type_str, regex_prefix, regex_postfix, nodes)
|
|
{
|
|
str = type_str + " log with multiple nodes detected.\n\n";
|
|
str += "Which would you like rendered?\n\n";
|
|
str += "Nodes detected:\n" + nodes.join("\n");
|
|
|
|
function make_regex(key) {
|
|
return new RegExp(regex_prefix + key + regex_postfix, "g");
|
|
}
|
|
|
|
var result = prompt(str, nodes[0]);
|
|
|
|
return result ? make_regex(result.trim()) : null;
|
|
}
|
|
function detect_ci_logs(logs)
|
|
{
|
|
nodes = new Set()
|
|
Array.from(logs.matchAll(/[0-9\-T:.Z]+ (lightningd-[0-9]+) /g)).forEach(match => {
|
|
nodes.add(match[1]);
|
|
});
|
|
var keys = [...nodes];
|
|
|
|
if (keys.length > 1)
|
|
return tab_option("Continous Integration", "[0-9\-T:.Z]+ ", " ", keys, logs)
|
|
|| single_option("Continous Integration", "[0-9\-T:.Z]+ ", " ", keys);
|
|
|
|
return keys.at(0);
|
|
}
|
|
function detect_pytest_logs(logs)
|
|
{
|
|
nodes = new Set()
|
|
Array.from(logs.matchAll(/(lightningd-[0-9]+) /g)).forEach(match => {
|
|
nodes.add(match[1]);
|
|
});
|
|
var keys = [...nodes];
|
|
|
|
if (keys.length > 1)
|
|
return tab_option("Python Test", "", " ", keys, logs)
|
|
|| single_option("Python Test", "", " ", keys);
|
|
|
|
return keys.at(0);
|
|
}
|
|
window.onload = function() {
|
|
if (window.opener) {
|
|
function receive_data_message(msg) {
|
|
if (!msg.data || msg.data.version != "v0.1-data") {
|
|
console.log("Unrecognized data message");
|
|
console.log(msg);
|
|
}
|
|
else {
|
|
do_render(msg.data.logs, document.getElementById('area'), msg.data.prefix);
|
|
window.removeEventListener('message', receive_data_message);
|
|
}
|
|
}
|
|
window.addEventListener('message', receive_data_message);
|
|
console.log(window.opener);
|
|
window.opener.postMessage("v0.1-ready", "*");
|
|
}
|
|
}
|
|
function detect_log_prefix(logs)
|
|
{
|
|
var ci = detect_ci_logs(logs)
|
|
if (ci)
|
|
return ci;
|
|
var pytest = detect_pytest_logs(logs)
|
|
if (pytest)
|
|
return pytest;
|
|
return null;
|
|
}
|
|
function do_render(logs, area, prefix)
|
|
{
|
|
var d = document;
|
|
var sheet = d.getElementById('logStyleSheet').sheet;
|
|
var types = {};
|
|
var daemons = {};
|
|
var us = null;
|
|
|
|
while(area.firstChild)
|
|
area.removeChild(area.firstChild);
|
|
|
|
while(sheet.cssRules.length)
|
|
sheet.deleteRule(0);
|
|
|
|
if(!prefix)
|
|
prefix = detect_log_prefix(logs);
|
|
|
|
for(line of logs.split("\n")) {
|
|
line = line.trim()
|
|
if(!line.length)
|
|
continue;
|
|
|
|
/* Detect and eat node prefix. If no prefix match, ignore line */
|
|
if (prefix) {
|
|
prefix_match = line.match(prefix);
|
|
if(!prefix_match)
|
|
continue;
|
|
line = line.slice(prefix_match[0].length);
|
|
}
|
|
|
|
info = parseLogLine(line);
|
|
|
|
if(info.msg.startsWith('Server started with public key'))
|
|
us = parseUs(info);
|
|
|
|
p = d.createElement('p');
|
|
p.info = info;
|
|
p.className = "logLine type-" + cssClean(info.type) + " daemon-" + cssClean(info.daemon.name);
|
|
|
|
if (info.daemon.peer)
|
|
p.className += " peer-" + info.daemon.peer;
|
|
|
|
types[info.type] = (types[info.type] || 0) + 1
|
|
daemons[info.daemon.str] = (daemons[info.daemon.str] || 0) + 1
|
|
|
|
var s = d.createElement('span');
|
|
s.className = "daemon"
|
|
s.title = info.daemon.parents;
|
|
s.innerText = info.daemon.name;
|
|
if (info.daemon.short_peer)
|
|
s.innerText += " " + info.daemon.short_peer;
|
|
s.innerText += " ";
|
|
p.appendChild(s);
|
|
|
|
var s = d.createElement('span');
|
|
s.className = "logMsg";
|
|
s.title = info.date;
|
|
s.innerText = info.msg;
|
|
p.appendChild(s);
|
|
|
|
area.appendChild(p);
|
|
}
|
|
|
|
types = Object.fromEntries(
|
|
Object.entries(types).sort(([,a],[,b]) => b-a)
|
|
);
|
|
|
|
daemons = Object.fromEntries(
|
|
Object.entries(daemons).sort(([,a],[,b]) => b-a)
|
|
);
|
|
|
|
var controls = d.createElement('p');
|
|
controls.className = "controls";
|
|
var type_buttons = [];
|
|
var daemon_buttons = [];
|
|
|
|
for(type in types) {
|
|
var b = d.createElement('button');
|
|
b.className = 'type_on';
|
|
b.type_str = type;
|
|
b.innerText = type + " " + types[type];
|
|
b.onclick = toggle_type;
|
|
type_buttons.push(b);
|
|
controls.appendChild(b);
|
|
|
|
sheet.insertRule(".type-" + cssClean(type) + "{}", 0);
|
|
}
|
|
|
|
var b = d.createElement('button');
|
|
b.className = 'type_on';
|
|
b.innerText = "All"
|
|
b.onclick = togggle_all_type;
|
|
b.buttons = type_buttons;
|
|
controls.appendChild(b);
|
|
|
|
controls.appendChild(d.createElement('hr'));
|
|
|
|
for(daemon_str in daemons) {
|
|
var daemon = parseDaemon(daemon_str)
|
|
var b = d.createElement('button');
|
|
b.className = 'daemon_on';
|
|
b.daemon = daemon;
|
|
b.innerText = (daemon.short_peer || "") + " " + daemon.name + " " + daemons[daemon_str];
|
|
b.onclick = toggle_daemon;
|
|
daemon_buttons.push(b);
|
|
controls.appendChild(b);
|
|
|
|
sheet.insertRule(".daemon-" + cssClean(daemon.name) + "{}", 0);
|
|
}
|
|
|
|
var b = d.createElement('button');
|
|
b.className = 'daemon_on';
|
|
b.innerText = "All"
|
|
b.onclick = togggle_all_daemon;
|
|
b.buttons = daemon_buttons;
|
|
controls.appendChild(b);
|
|
|
|
controls.appendChild(d.createElement('hr'));
|
|
|
|
|
|
var t = d.createElement('input');
|
|
t.type = 'text';
|
|
t.placeholder = 'message filter regex';
|
|
t.onchange = filter_messages;
|
|
t.onkeyup = filter_messages;
|
|
|
|
controls.appendChild(t);
|
|
controls.appendChild(document.createTextNode(" "));
|
|
|
|
var s = d.createElement('span');
|
|
s.id = 'filter_error';
|
|
controls.appendChild(s);
|
|
|
|
area.prepend(controls);
|
|
}
|
|
</script>
|
|
<style id="logStyleSheet">
|
|
</style>
|
|
<style>
|
|
.logLine {
|
|
font-family: Cascadia, Hack, monospace;
|
|
}
|
|
.type_on:after, .daemon_on:after {
|
|
content: " (on)";
|
|
}
|
|
.type_off:after, .daemon_off:after {
|
|
content: " (off)";
|
|
}
|
|
.type_on, .daemon_on {
|
|
}
|
|
.type_off, .daemon_off {
|
|
background-color: #FF6961;
|
|
}
|
|
.controls {
|
|
position: sticky;
|
|
top: 0;
|
|
background-color: #aaa9;
|
|
padding: 1em;
|
|
}
|
|
.logLine .daemon {
|
|
color: #AEC6CF;
|
|
}
|
|
.type-IO:before {
|
|
content: "io ";
|
|
}
|
|
.type-TRACE:before {
|
|
content: "trace ";
|
|
}
|
|
.type-DEBUG:before {
|
|
content: "debug ";
|
|
color: #Ffd1dc;
|
|
}
|
|
.type-INFO:before {
|
|
content: "info ";
|
|
color: #AEC6CF;
|
|
}
|
|
.type-UNUSUAL:before {
|
|
content: "unusual ";
|
|
color: #E9D502;
|
|
}
|
|
.type-BROKEN:before {
|
|
content: "broken ";
|
|
color: #FF6961;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="area">
|
|
<h3>Enter Logs:</h3>
|
|
<textarea id="logs"></textarea>
|
|
<button onclick="do_render(document.getElementById('logs').value, document.getElementById('area'))">Render</button>
|
|
</div>
|
|
</body>
|
|
</html> |