Improve live log viewer — fix level filters, faster updates, add search
- Fix level filter showing nothing: now uses heuristic classification for print() output (error/traceback/failed→ERROR, warn→WARNING, etc.) in addition to exact logger format matching - Speed up WebSocket updates from 2s to 0.5s polling - Add search box with 300ms debounce — filters both initial load and live - Use DocumentFragment for batch DOM appends (performance) - Increase line cap from 1000 to 2000 - Backend search parameter support in /api/logs/tail
This commit is contained in:
parent
8b0e619fa1
commit
3404812a1e
4 changed files with 85 additions and 22 deletions
|
|
@ -6282,21 +6282,38 @@ def get_log_tail():
|
|||
}
|
||||
log_path = log_map.get(log_source, log_map['app'])
|
||||
|
||||
search = request.args.get('search', '').lower()
|
||||
|
||||
def _classify_log_level(line):
|
||||
"""Classify a log line's level. Returns DEBUG/INFO/WARNING/ERROR or empty for unclassified."""
|
||||
if ' - DEBUG - ' in line: return 'DEBUG'
|
||||
if ' - INFO - ' in line: return 'INFO'
|
||||
if ' - WARNING - ' in line: return 'WARNING'
|
||||
if ' - ERROR - ' in line or ' - CRITICAL - ' in line: return 'ERROR'
|
||||
# Heuristic for print() output and non-logger lines
|
||||
ll = line.lower()
|
||||
if 'error' in ll or 'traceback' in ll or 'exception' in ll or 'failed' in ll: return 'ERROR'
|
||||
if 'warning' in ll or 'warn' in ll: return 'WARNING'
|
||||
if 'debug' in ll: return 'DEBUG'
|
||||
return 'INFO' # Default unclassified lines to INFO
|
||||
|
||||
result_lines = []
|
||||
if os.path.exists(log_path):
|
||||
try:
|
||||
with open(log_path, 'r', encoding='utf-8', errors='replace') as f:
|
||||
all_lines = f.readlines()
|
||||
# Read more lines than requested so filtering has enough to work with
|
||||
tail = all_lines[-(lines * 3):] if level_filter else all_lines[-lines:]
|
||||
pool_size = lines * 5 if (level_filter or search) else lines
|
||||
tail = all_lines[-pool_size:]
|
||||
for line in tail:
|
||||
stripped = line.rstrip()
|
||||
if not stripped:
|
||||
continue
|
||||
if level_filter and level_filter in ('DEBUG', 'INFO', 'WARNING', 'ERROR'):
|
||||
# Match lines like "2026-04-18 12:00:00 - name - INFO - message"
|
||||
if f' - {level_filter} - ' not in stripped:
|
||||
if _classify_log_level(stripped) != level_filter:
|
||||
continue
|
||||
if search and search not in stripped.lower():
|
||||
continue
|
||||
result_lines.append(stripped)
|
||||
# Trim to requested count after filtering
|
||||
result_lines = result_lines[-lines:]
|
||||
|
|
@ -54470,7 +54487,7 @@ def _emit_live_log_loop():
|
|||
'source_reuse': os.path.join('logs', 'source_reuse.log'),
|
||||
}
|
||||
while not globals().get('IS_SHUTTING_DOWN', False):
|
||||
socketio.sleep(2)
|
||||
socketio.sleep(0.5)
|
||||
try:
|
||||
# Read which source clients want (stored by subscribe handler)
|
||||
source = getattr(_emit_live_log_loop, '_source', 'app')
|
||||
|
|
|
|||
|
|
@ -5867,6 +5867,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="log-viewer-actions">
|
||||
<input type="text" id="log-viewer-search" class="log-viewer-search" placeholder="Search logs..." oninput="_logViewerOnSearch(this)">
|
||||
<button class="log-action-btn" onclick="_logViewerCopy()" title="Copy visible logs">Copy</button>
|
||||
<button class="log-action-btn" onclick="_logViewerClear()" title="Clear display">Clear</button>
|
||||
<label class="log-autoscroll-label">
|
||||
|
|
|
|||
|
|
@ -6984,7 +6984,26 @@ function _collectGenreWhitelist() {
|
|||
let _logViewerActive = false;
|
||||
let _logViewerFilter = '';
|
||||
let _logViewerSource = 'app';
|
||||
const _LOG_MAX_LINES = 1000;
|
||||
let _logViewerSearch = '';
|
||||
const _LOG_MAX_LINES = 2000;
|
||||
|
||||
function _logClassify(line) {
|
||||
// Exact logger format first
|
||||
if (line.includes(' - DEBUG - ')) return 'DEBUG';
|
||||
if (line.includes(' - INFO - ')) return 'INFO';
|
||||
if (line.includes(' - WARNING - ')) return 'WARNING';
|
||||
if (line.includes(' - ERROR - ') || line.includes(' - CRITICAL - ')) return 'ERROR';
|
||||
// Heuristic for print() output
|
||||
const ll = line.toLowerCase();
|
||||
if (ll.includes('error') || ll.includes('traceback') || ll.includes('exception') || ll.includes('failed')) return 'ERROR';
|
||||
if (ll.includes('warning') || ll.includes('warn')) return 'WARNING';
|
||||
if (ll.includes('debug')) return 'DEBUG';
|
||||
return 'INFO';
|
||||
}
|
||||
|
||||
function _logClassToCSS(level) {
|
||||
return { DEBUG: 'log-debug', INFO: 'log-info', WARNING: 'log-warning', ERROR: 'log-error' }[level] || 'log-plain';
|
||||
}
|
||||
|
||||
async function _logViewerInit() {
|
||||
if (_logViewerActive) return;
|
||||
|
|
@ -6993,8 +7012,9 @@ async function _logViewerInit() {
|
|||
|
||||
// Fetch initial tail
|
||||
try {
|
||||
const params = new URLSearchParams({ source: _logViewerSource, lines: 200 });
|
||||
const params = new URLSearchParams({ source: _logViewerSource, lines: 300 });
|
||||
if (_logViewerFilter) params.set('level', _logViewerFilter);
|
||||
if (_logViewerSearch) params.set('search', _logViewerSearch);
|
||||
const resp = await fetch(`/api/logs/tail?${params}`);
|
||||
const data = await resp.json();
|
||||
if (data.lines) {
|
||||
|
|
@ -7027,10 +7047,17 @@ function _logViewerStop() {
|
|||
function _logViewerOnLive(data) {
|
||||
if (!_logViewerActive || !data.lines) return;
|
||||
if (data.source !== _logViewerSource) return;
|
||||
const filtered = _logViewerFilter
|
||||
? data.lines.filter(l => l.includes(` - ${_logViewerFilter} - `))
|
||||
: data.lines;
|
||||
if (filtered.length > 0) _logViewerAppendLines(filtered);
|
||||
let lines = data.lines;
|
||||
// Apply level filter client-side for live lines
|
||||
if (_logViewerFilter) {
|
||||
lines = lines.filter(l => _logClassify(l) === _logViewerFilter);
|
||||
}
|
||||
// Apply search filter
|
||||
if (_logViewerSearch) {
|
||||
const s = _logViewerSearch.toLowerCase();
|
||||
lines = lines.filter(l => l.toLowerCase().includes(s));
|
||||
}
|
||||
if (lines.length > 0) _logViewerAppendLines(lines);
|
||||
}
|
||||
|
||||
function _logViewerAppendLines(lines) {
|
||||
|
|
@ -7039,12 +7066,14 @@ function _logViewerAppendLines(lines) {
|
|||
const autoScroll = document.getElementById('log-viewer-autoscroll')?.checked;
|
||||
const terminal = document.getElementById('log-viewer-terminal');
|
||||
|
||||
const frag = document.createDocumentFragment();
|
||||
for (const line of lines) {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'log-line ' + _logViewerGetClass(line);
|
||||
div.className = 'log-line ' + _logClassToCSS(_logClassify(line));
|
||||
div.textContent = line;
|
||||
container.appendChild(div);
|
||||
frag.appendChild(div);
|
||||
}
|
||||
container.appendChild(frag);
|
||||
|
||||
// Trim old lines
|
||||
while (container.children.length > _LOG_MAX_LINES) {
|
||||
|
|
@ -7061,14 +7090,6 @@ function _logViewerAppendLines(lines) {
|
|||
}
|
||||
}
|
||||
|
||||
function _logViewerGetClass(line) {
|
||||
if (line.includes(' - DEBUG - ')) return 'log-debug';
|
||||
if (line.includes(' - INFO - ')) return 'log-info';
|
||||
if (line.includes(' - WARNING - ')) return 'log-warning';
|
||||
if (line.includes(' - ERROR - ') || line.includes(' - CRITICAL - ')) return 'log-error';
|
||||
return 'log-plain';
|
||||
}
|
||||
|
||||
async function _logViewerChangeSource() {
|
||||
_logViewerStop();
|
||||
_logViewerSource = document.getElementById('log-viewer-source')?.value || 'app';
|
||||
|
|
@ -7081,10 +7102,22 @@ function _logViewerFilterLevel(btn) {
|
|||
document.querySelectorAll('.log-filter-btn').forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
_logViewerFilter = btn.dataset.level || '';
|
||||
// Reload with filter
|
||||
_logViewerReload();
|
||||
}
|
||||
|
||||
let _logSearchDebounce = null;
|
||||
function _logViewerOnSearch(input) {
|
||||
clearTimeout(_logSearchDebounce);
|
||||
_logSearchDebounce = setTimeout(() => {
|
||||
_logViewerSearch = (input.value || '').trim();
|
||||
_logViewerReload();
|
||||
}, 300);
|
||||
}
|
||||
|
||||
function _logViewerReload() {
|
||||
_logViewerStop();
|
||||
const container = document.getElementById('log-viewer-lines');
|
||||
if (container) container.innerHTML = '';
|
||||
if (container) container.innerHTML = '<div class="log-line log-info">Loading...</div>';
|
||||
_logViewerInit();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17282,6 +17282,18 @@ body.helper-mode-active #dashboard-activity-feed:hover {
|
|||
|
||||
.log-action-btn:hover { background: rgba(255, 255, 255, 0.08); color: #fff; }
|
||||
|
||||
.log-viewer-search {
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
border-radius: 4px;
|
||||
color: #e0e0e0;
|
||||
padding: 4px 10px;
|
||||
font-size: 12px;
|
||||
width: 160px;
|
||||
outline: none;
|
||||
}
|
||||
.log-viewer-search:focus { border-color: rgba(255, 255, 255, 0.3); }
|
||||
|
||||
.log-autoscroll-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
|
|||
Loading…
Reference in a new issue