luci-app: New log.

ruantiblock: Minor fixes.
This commit is contained in:
gSpot
2021-03-11 18:24:24 +03:00
parent a5fc9016be
commit b187ab5384
25 changed files with 736 additions and 244 deletions
@@ -0,0 +1,502 @@
'use strict';
'require ui';
return L.Class.extend({
view: L.view.extend({
viewName: null,
title: null,
logFacilities: [
'kern',
'user',
'mail',
'daemon',
'auth',
'syslog',
'lpr',
'news',
],
logLevels: {
'emerg': E('span', { 'class': 'zonebadge log-emerg' }, E('strong', _('Emergency'))),
'alert': E('span', { 'class': 'zonebadge log-alert' }, E('strong', _('Alert'))),
'crit': E('span', { 'class': 'zonebadge log-crit' }, E('strong', _('Critical'))),
'err': E('span', { 'class': 'zonebadge log-err' }, E('strong', _('Error'))),
'warn': E('span', { 'class': 'zonebadge log-warn' }, E('strong', _('Warning'))),
'notice': E('span', { 'class': 'zonebadge log-notice' }, E('strong', _('Notice'))),
'info': E('span', { 'class': 'zonebadge log-info' }, E('strong', _('Info'))),
'debug': E('span', { 'class': 'zonebadge log-debug' }, E('strong', _('Debug'))),
},
tailValue: 25,
logSortingValue: 'asc',
logLevelsStat: {},
logLevelsDropdown: null,
totalLogLines: 0,
htmlEntities: function(str) {
return String(str).replace(
/&/g, '&').replace(
/</g, '&#60;').replace(
/>/g, '&#62;').replace(
/"/g, '&#34;');
},
/**
*
* @param {number} tail
* @returns {string}
* Returns the raw content of the log
*
*/
getLogData: function(tail) {
throw new Error('getLogData needs to be reloaded in subclass');
},
/**
*
* @param {string} logdata
* @param {number} tail
* @returns {Array<number, string|null, string|null, string|null, string|null>}
* Returns an array of values: [ #, Timestamp, Level, Facility, Message ]
*
*/
parseLogData: function(logdata, tail) {
throw new Error('parseLogData needs to be reloaded in subclass');
},
makeLogArea: function(logdataArray) {
let lines = `<div class="tr"><div class="td center log-entry-empty">${_('No entries available...')}</div></div>`;
let logTable = E('div', { 'id': 'logTable', 'class': 'table' });
for(let level of Object.keys(this.logLevels)) {
this.logLevelsStat[level] = 0;
};
if(logdataArray.length > 0) {
lines = [];
logdataArray.forEach((e, i) => {
this.logLevelsStat[e[2]] = (this.logLevelsStat[e[2]] != undefined) ?
this.logLevelsStat[e[2]] + 1 : 1;
lines.push(
`<div class="tr log-${e[2] || 'empty'}"><div class="td left" data-title="#">${e[0]}</div>` +
((e[1]) ? `<div class="td left" data-title="${_('Timestamp')}">${e[1]}</div>` : '') +
((e[2]) ? `<div class="td left" data-title="${_('Level')}">${e[2]}</div>` : '') +
((e[3]) ? `<div class="td left" data-title="${_('Facility')}">${e[3]}</div>` : '') +
((e[4]) ? `<div class="td left" data-title="${_('Message')}">${e[4]}</div>` : '') +
`</div>`
);
});
lines = lines.join('');
logTable.append(
E('div', { 'class': 'tr table-titles' }, [
E('div', { 'class': 'th left log-entry-number' }, '#'),
(logdataArray[0][1]) ? E('div', { 'class': 'th left log-entry-time' }, _('Timestamp')) : '',
(logdataArray[0][2]) ? E('div', { 'class': 'th left log-entry-log-level' }, _('Level')) : '',
(logdataArray[0][3]) ? E('div', { 'class': 'th left log-entry-facility' }, _('Facility')) : '',
(logdataArray[0][4]) ? E('div', { 'class': 'th left log-entry-message' }, _('Message')) : '',
])
);
};
try {
logTable.insertAdjacentHTML('beforeend', lines);
} catch(err) {
if(err.name === 'SyntaxError') {
ui.addNotification(null,
E('p', {}, _('HTML/XML error') + ': ' + err.message), 'error');
};
throw err;
};
let levelsStatString = '';
if((Object.values(this.logLevelsStat).reduce((s,c) => s + c, 0)) > 0) {
Object.entries(this.logLevelsStat).forEach(e => {
if(e[1] > 0) {
levelsStatString += `<span class="log-entries-count-level log-${e[0]}" title="${e[0]}">${e[1]}</span>`;
};
});
};
return E([
E('div', { 'class': 'log-entries-count' },
`${_('Entries')}: ${logdataArray.length} / ${this.totalLogLines}${levelsStatString}`
),
logTable
]);
},
setLevelFilter: function(cArr) {
let logLevelsKeys = Object.keys(this.logLevels);
if(logLevelsKeys.length > 0) {
let selectedLevels = this.logLevelsDropdown.getValue();
if(logLevelsKeys.length === selectedLevels.length) {
return cArr;
};
return cArr.filter(s => selectedLevels.length === 0 || selectedLevels.includes(s[2]));
};
return cArr;
},
setRegexpFilter: function(cArr) {
let fPattern = document.getElementById('logFilter').value;
if(!fPattern) {
return cArr;
};
let fArr = [];
try {
let regExp = new RegExp(`(${fPattern})`, 'giu');
cArr.forEach((e, i) => {
if(regExp.test(e[4])) {
e[4] = e[4].replace(regExp, '<span class="log-highlight-item">$1</span>');
fArr.push(e);
};
});
} catch(err) {
if(err.name === 'SyntaxError') {
ui.addNotification(null,
E('p', {}, _('Invalid regular expression') + ': ' + err.message));
return cArr;
} else {
throw err;
};
};
return fArr;
},
downloadLog: function() {
let formElems = Array.from(document.forms.logForm.elements);
formElems.forEach(e => e.disabled = true);
return this.getLogData(0).then(logdata => {
logdata = logdata || '';
let link = E('a', {
'download': this.viewName + '.txt',
'href': URL.createObjectURL(
new Blob([ logdata ], { type: 'text/plain' })),
});
link.click();
URL.revokeObjectURL(link.href);
}).catch(() => {
ui.addNotification(null,
E('p', {}, _('Download error') + ': ' + err.message));
}).finally(() => {
formElems.forEach(e => e.disabled = false);
});
},
load: function() {
// Restoring settings from localStorage
let tailValueLocal = localStorage.getItem(`luci-app-${this.viewName}-tailValue`);
if(tailValueLocal) {
this.tailValue = Number(tailValueLocal);
};
let logSortingLocal = localStorage.getItem(`luci-app-${this.viewName}-logSorting`);
if(logSortingLocal) {
this.logSortingValue = logSortingLocal;
};
return this.getLogData(this.tailValue);
},
render: function(logdata) {
document.head.append(E('style', {'type': 'text/css'},
`
.log-entry-empty {
}
.log-entry-number {
min-width: 4em !important;
}
.log-entry-time {
min-width: 14em !important;
}
.log-entry-log-level {
max-width: 5em !important;
}
.log-entry-facility{
max-width: 7em !important;
}
.log-entry-message {
min-width: 25em !important;
}
.log-empty {
}
.log-emerg {
background-color: #a93734 !important;
color: #fff;
}
.log-alert {
background-color: #ff7968 !important;
color: #fff;
}
.log-crit {
background-color: #fcc3bf !important;
}
.log-err {
background-color: #ffe9e8 !important;
}
.log-warn {
background-color: #fff7e2 !important;
}
.log-notice {
background-color: #e3ffec !important;
}
.log-info {
}
.log-debug {
background-color: #ebf6ff !important;
}
.log-highlight-item {
background-color: #ffef00;
}
.log-entries-count {
margin: 0 0 5px 5px;
font-weight: bold;
opacity: 0.6;
}
.log-entries-count-level {
display: inline-block !important;
margin: 0 0 0 5px;
padding: 0 4px;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
border-radius: 3px;
border: 1px solid #ccc;
font-weight: normal;
}
`
));
let logWrapper = E('div', {
'id': 'logWrapper',
'style': 'width:100%; min-height:20em; padding: 0 0 0 45px; font-size:0.9em !important'
}, this.makeLogArea(this.parseLogData(logdata, this.tailValue)));
let tailInput = E('input', {
'id': 'tailInput',
'name': 'tailInput',
'type': 'text',
'form': 'logForm',
'class': 'cbi-input-text',
'style': 'width:4em !important; min-width:4em !important',
'maxlength': 5,
});
tailInput.value = (this.tailValue === 0) ? null : this.tailValue;
ui.addValidator(tailInput, 'uinteger', true);
let tailReset = E('input', {
'type': 'button',
'form': 'logForm',
'class': 'cbi-button btn cbi-button-reset',
'value': 'Χ',
'click': ev => {
tailInput.value = null;
logFormSubmitBtn.click();
ev.target.blur();
},
'style': 'max-width:4em !important',
});
let logLevelsDropdownElem = '';
let logLevelsKeys = Object.keys(this.logLevels);
if(logLevelsKeys.length > 0) {
this.logLevelsDropdown = new ui.Dropdown(
null,
this.logLevels,
{
id: 'logLevelsDropdown',
sort: logLevelsKeys,
multiple: true,
select_placeholder: _('All'),
display_items: 3,
dropdown_items: -1,
}
);
logLevelsDropdownElem = E(
'div', { 'class': 'cbi-value' }, [
E('label', {
'class': 'cbi-value-title',
'for': 'logLevelsDropdown',
}, _('Logging levels')),
E('div', { 'class': 'cbi-value-field' },
this.logLevelsDropdown.render()
),
]
);
};
let logFilter = E('input', {
'id': 'logFilter',
'name': 'logFilter',
'type': 'text',
'form': 'logForm',
'class': 'cbi-input-text',
'placeholder': _('Type an expression...'),
});
let logFormSubmitBtn = E('input', {
'type': 'submit',
'form': 'logForm',
'class': 'cbi-button btn cbi-button-action',
'value': _('Apply'),
'click': ev => ev.target.blur(),
'style': 'margin-right: 1em',
});
let logSorting = E('select', {
'id': 'logSorting',
'form': 'logForm',
'class': "cbi-input-select",
}, [
E('option', { 'value': 'asc' }, _('ascending')),
E('option', { 'value': 'desc' }, _('descending')),
]);
logSorting.value = this.logSortingValue;
let logDownloadBtn = E('button', {
'class': 'cbi-button btn',
'click': ui.createHandlerFn(this, this.downloadLog),
}, _('Download log'));
return E([
E('h2', { 'id': 'logTitle', 'class': 'fade-in' }, this.title),
E('div', { 'class': 'cbi-section-descr fade-in' }),
E('div', { 'class': 'cbi-section fade-in' },
E('div', { 'class': 'cbi-section-node' }, [
E('div', { 'class': 'cbi-value' }, [
E('label', {
'class': 'cbi-value-title',
'for': 'tailInput',
}, _('Last entries')),
E('div', { 'class': 'cbi-value-field' }, [
tailInput,
tailReset,
]),
]),
logLevelsDropdownElem,
E('div', { 'class': 'cbi-value' }, [
E('label', {
'class': 'cbi-value-title',
'for': 'logFilter',
}, _('Message filter')),
E('div', { 'class': 'cbi-value-field' }, logFilter),
]),
E('div', { 'class': 'cbi-value' }, [
E('label', {
'class': 'cbi-value-title',
'for': 'logSorting',
}, _('Sorting entries')),
E('div', { 'class': 'cbi-value-field' }, logSorting,),
]),
E('div', { 'class': 'cbi-value' }, [
E('label', {
'class': 'cbi-value-title',
'for': 'logFilter',
}),
E('div', { 'class': 'cbi-value-field' }, [
logFormSubmitBtn,
E('form', {
'id': 'logForm',
'name': 'logForm',
'style': 'display:inline-block',
'submit': ui.createHandlerFn(this, function(ev) {
ev.preventDefault();
let formElems = Array.from(document.forms.logForm.elements);
formElems.forEach(e => e.disabled = true);
logDownloadBtn.disabled = true;
// Saving settings to localStorage
if(this.tailValue != tailInput.value) {
this.tailValue = (/^[0-9]+$/.test(tailInput.value)) ? tailInput.value : 0;
localStorage.setItem(
`luci-app-${this.viewName}-tailValue`, String(this.tailValue));
};
if(this.logSortingValue != logSorting.value) {
this.logSortingValue = logSorting.value;
localStorage.setItem(
`luci-app-${this.viewName}-logSorting`, this.logSortingValue);
};
let tail = (tailInput.value && tailInput.value > 0) ? tailInput.value : 0
return this.getLogData(tail).then(logdata => {
logdata = logdata || '';
let loglines = this.makeLogArea(
this.setRegexpFilter(
this.setLevelFilter(
this.parseLogData(logdata, tail)
)
)
);
logWrapper.innerHTML = '';
logWrapper.append(loglines);
}).finally(() => {
formElems.forEach(e => e.disabled = false);
logDownloadBtn.disabled = false;
});
}),
}, E('span', {}, '&#160;')),
]),
]),
])
),
E('div', { 'class': 'cbi-section fade-in' },
E('div', { 'class': 'cbi-section-node' },
E('div', { 'class': 'cbi-value' }, [
E('div', { 'style': 'position:fixed; z-index:1 !important' }, [
E('button', {
'class': 'btn',
'style': 'position:relative; display:block; margin:0 !important; left:1px; top:1px',
'click': ev => {
document.getElementById('logTitle').scrollIntoView(true);
ev.target.blur();
},
}, '&#8593;'),
E('button', {
'class': 'btn',
'style': 'position:relative; display:block; margin:0 !important; margin-top:1px !important; left:1px; top:1px',
'click': ev => {
logWrapper.scrollIntoView(false);
ev.target.blur();
},
}, '&#8595;'),
]),
logWrapper,
])
)
),
E('div', { 'class': 'cbi-section fade-in' },
E('div', { 'class': 'cbi-section-node' },
E('div', { 'class': 'cbi-value' },
E('div', { 'style': 'width:100%; text-align:right !important' }, [
E('hr'),
logDownloadBtn,
])
)
)
),
]);
},
handleSaveApply: null,
handleSave: null,
handleReset: null,
}),
})
@@ -1,195 +1,61 @@
'use strict';
'require fs';
'require ui';
'require view.ruantiblock.baselog as baselog';
'require view.ruantiblock.tools as tools';
let log_regexp = new RegExp(`^.*(user\\.notice ${tools.app_name}).*$`, 'gm');
return baselog.view.extend({
viewName: 'ruantiblock-log',
return L.view.extend({
tail_default: 25,
title: _('Ruantiblock') + ' - ' + _('Log'),
parse_log_data: function(logdata) {
return logdata.trim().match(log_regexp);
},
getLogData: function(tail) {
return Promise.all([
L.resolveDefault(fs.stat('/sbin/logread'), null),
L.resolveDefault(fs.stat('/usr/sbin/logread'), null),
]).then(stat => {
let logger = (stat[0]) ? stat[0].path : (stat[1]) ? stat[1].path : null;
load: function() {
return Promise.all([
L.resolveDefault(fs.stat('/sbin/logread'), null),
L.resolveDefault(fs.stat('/usr/sbin/logread'), null),
]).then(stat => {
let logger = (stat[0]) ? stat[0].path : (stat[1]) ? stat[1].path : null;
if(logger) {
return fs.exec_direct(logger, [ '-e', tools.app_name ]).catch(e => {
ui.addNotification(null, E('p', _('Unable to execute or read contents')
+ ': %s<br />[ %s ]'.format(e.message, logger)
if(logger) {
return fs.exec_direct(logger, [ '-e', '^' + tools.app_name ]).catch(err => {
ui.addNotification(null, E('p', _('Unable to execute or read contents')
+ ': %s<br />[ %s ]'.format(err.message, logger)
));
return '';
});
};
});
},
return '';
});
};
});
},
render: function(logdata) {
let nav_btns_top = '1px';
let loglines = this.parse_log_data(logdata);
parseLogData: function(logdata, tail) {
if(!logdata) {
return [];
};
let log_textarea = E('textarea', {
'id': 'syslog',
'class': 'cbi-input-textarea',
'style': 'width:100% !important; resize:horizontal; padding: 0 0 0 45px; font-size:12px',
'readonly': 'readonly',
'wrap': 'off',
'rows': this.tail_default,
'spellcheck': 'false',
}, [ loglines.slice(-this.tail_default).join('\n') ]);
let strings = logdata.trim().split(/\n/);
let tail_value = E('input', {
'id': 'tail_value',
'name': 'tail_value',
'type': 'text',
'form': 'log_form',
'class': 'cbi-input-text',
'style': 'width:4em !important; min-width:4em !important; margin-bottom:0.3em !important',
'maxlength': 5,
});
tail_value.value = this.tail_default;
ui.addValidator(tail_value, 'uinteger', true);
if(tail && tail > 0 && strings) {
strings = strings.slice(-tail);
};
let log_filter = E('input', {
'id': 'log_filter',
'name': 'log_filter',
'type': 'text',
'form': 'log_form',
'class': 'cbi-input-text',
'style': 'min-width:16em !important; margin-right:1em !important; margin-bottom:0.3em !important',
'placeholder': _('Message filter'),
'data-tooltip': _('Filter messages with a regexp'),
});
this.totalLogLines = strings.length;
let log_form_submit_btn = E('input', {
'type': 'submit',
'form': 'log_form',
'class': 'cbi-button btn cbi-button-action',
'style': 'margin-right:1em !important; margin-bottom:0.3em !important;',
'value': _('Apply'),
'click': ev => ev.target.blur(),
});
let entriesArray = strings.map((e, i) => {
let strArray = e.split(/\s+/);
let logLevel = strArray[5].split('.');
function set_log_tail(c_arr) {
let tail_num_val = tail_value.value;
if(tail_num_val && tail_num_val > 0 && c_arr) {
return c_arr.slice(-tail_num_val);
};
return c_arr;
}
return [
i + 1, // # (Number)
strArray.slice(0, 5).join(' '), // Timestamp (String)
logLevel[1], // Level (String)
logLevel[0], // Facility (String)
this.htmlEntities(strArray.slice(6).join(' ')), // Message (String)
];
});
function set_log_filter(c_arr) {
let f_pattern = log_filter.value;
if(!f_pattern) {
return c_arr;
};
let f_arr = [];
try {
f_arr = c_arr.filter(s => new RegExp(f_pattern.toLowerCase()).test(s.toLowerCase()));
} catch(err) {
if(err.name === 'SyntaxError') {
ui.addNotification(null,
E('p', {}, _('Wrong regular expression') + ': ' + err.message));
return c_arr;
} else {
throw err;
};
};
if(f_arr.length === 0) {
f_arr.push(_('No matches...'));
};
return f_arr;
}
if(this.logSortingValue === 'desc') {
entriesArray.reverse();
};
return E([
E('h2', { 'id': 'log_title', 'class': 'fade-in' }, _('Ruantiblock') + ' - ' + _('Log')),
E('div', { 'class': 'cbi-section-descr fade-in' }),
E('div', { 'class': 'cbi-section fade-in' },
E('div', { 'class': 'cbi-section-node' },
E('div', { 'class': 'cbi-value' }, [
E('label', {
'class': 'cbi-value-title',
'for': 'tailValue',
'style': 'margin-bottom:0.3em !important',
}, _('Show only the last messages')),
E('div', { 'class': 'cbi-value-field' }, [
tail_value,
E('input', {
'type': 'button',
'form': 'log_form',
'class': 'cbi-button btn cbi-button-reset',
'value': 'Χ',
'click': ev => {
tail_value.value = null;
log_form_submit_btn.click();
ev.target.blur();
},
'style': 'margin-right:1em !important; margin-bottom:0.3em !important; max-width:4em !important',
}),
log_filter,
log_form_submit_btn,
E('form', {
'id': 'log_form',
'name': 'log_form',
'style': 'display:inline-block; margin-bottom:0.3em !important',
'submit': ui.createHandlerFn(this, function(ev) {
ev.preventDefault();
let form_elems = Array.from(document.forms.log_form.elements);
form_elems.forEach(e => e.disabled = true);
return this.load().then(logdata => {
let loglines = set_log_filter(set_log_tail(
this.parse_log_data(logdata)));
log_textarea.rows = (loglines.length < this.tail_default) ?
this.tail_default : loglines.length;
log_textarea.value = loglines.join('\n');
}).finally(() => {
form_elems.forEach(e => e.disabled = false);
});
}),
}, E('span', {}, '&#160;')),
]),
])
)
),
E('div', { 'class': 'cbi-section fade-in' },
E('div', { 'class': 'cbi-section-node' },
E('div', { 'class': 'cbi-value' }, [
E('div', { 'style': 'position:fixed' }, [
E('button', {
'class': 'btn',
'style': 'position:relative; display:block; margin:0 !important; left:1px; top:'
+ nav_btns_top,
'click': ev => {
document.getElementById('log_title').scrollIntoView(true);
ev.target.blur();
},
}, '&#8593;'),
E('button', {
'class': 'btn',
'style': 'position:relative; display:block; margin:0 !important; margin-top:1px !important; left:1px; top:'
+ nav_btns_top,
'click': ev => {
log_textarea.scrollIntoView(false);
ev.target.blur();
},
}, '&#8595;'),
]),
log_textarea,
])
)
),
]);
},
handleSaveApply: null,
handleSave: null,
handleReset: null,
return entriesArray;
},
});