Simplify the log grouping logic

This commit is contained in:
Pierre committed 2026-10-04 14:29:14 +02:00
1 parent f57e23d76a
commit 3aa0120e3d
10 files changed
+82 -84

No files matched your search

+1 -1
View File
@@ -21,7 +21,7 @@ The explorer groups lines by the app name in the standard AppDaemon log format.
**AD Logs** turns red when the loaded logs contain an ERROR or CRITICAL entry. **Clear logs** empties the configured log files and clears the current explorer history.
The standard AppDaemon text logs do not contain causal links or reliable initialization boundaries. The details panel will say so for ordinary entries. Existing messages that already contain trace or initialization metadata can still be grouped or linked; the explorer does not create that metadata. No AppDaemon Python changes are needed for this raw-log view.
Lines carrying the same `[group=<name>:<id>]` marker are grouped together, and the supplied name is shown in the explorer. Ordinary AppDaemon lines without a group marker remain individual log entries. Trace metadata can still be used for causal links where present.
## Settings
+14 -16
View File
@@ -26,7 +26,7 @@
&& (level === 'all' || normalLevel(e) === level)
&& `${e.app} ${e.entity || ''} ${e.message}`.toLowerCase().includes(q));
}
const cleanMessage = e => e.message.replace(/ \[event=[\w-]+ trace=[\w-]+\]/g, '').replace(/ \[init=[\w-]+\]/g, '').replace(/ \[group=(?:initialization|smartcondition|value_selector):[a-f0-9]{12}\]/gi, '');
const cleanMessage = e => e.message.replace(/ \[event=[\w-]+ trace=[\w-]+\]/g, '').replace(/ \[init=[^\]]+\]/g, '').replace(/ \[group=[^\]]+:[a-f0-9]{12}\]/gi, '');
const formatTime = (entry, includeYear = false) => {
if (!Number.isFinite(entry.time)) return entry.timestamp;
try {
@@ -68,13 +68,12 @@
});
return container;
}
function blockTitle(items, groupKey) {
function blockTitle(items) {
const first = items[0];
const kind = groupKey?.startsWith('initialization:') || first.initialization_id ? 'initialization' : items.find(entry => entry.group_kind)?.group_kind;
const names = { initialization: 'Initialization', smartcondition: 'Smart condition', value_selector: 'Value selector' };
const name = names[kind] || (items.length > 1 || first.trace_id ? 'Event' : 'Log');
const label = kind === 'initialization' ? first.app : items.map(entry => entry.group_label || cleanMessage(entry).match(/^\[([^\]]+)\]/)?.[1]).find(Boolean) || first.entity || first.app;
return node('div', `${name} · ${label}`, 'block-title');
const kind = items.find(entry => entry.group_kind)?.group_kind;
const name = kind || (items.length > 1 || first.trace_id ? 'Event' : 'Log');
const label = items.map(entry => entry.group_label).find(Boolean) || first.entity || first.app;
return node('div', kind ? name : `${name} · ${label}`, 'block-title');
}
function row(e) {
const item = node('div', undefined, `row${selected === e.event_id ? ' selected' : ''}${recentEvents.has(e.event_id) ? ' recent-log' : ''}`);
@@ -188,25 +187,24 @@
detail.querySelector('.rows')?.remove();
if (!detail.open) return;
const rows = node('div', undefined, 'rows');
const units = [], initGroups = new Map();
const units = [], groupUnits = new Map();
logs.forEach(e => {
if (e.initialization_id) {
if (!initGroups.has(e.initialization_id)) { const unit = { id: e.initialization_id, entries: [] }; initGroups.set(e.initialization_id, unit); units.push(unit); }
initGroups.get(e.initialization_id).entries.push(e);
if (e.group_id) {
const key = `group:${e.group_id}`;
if (!groupUnits.has(key)) { const unit = { key, entries: [] }; groupUnits.set(key, unit); units.push(unit); }
groupUnits.get(key).entries.push(e);
} else {
const key = e.group_id ? `group:${e.group_id}` : e.kind === 'log' && e.trace_id ? `trace:${e.trace_id}` : undefined;
const key = e.kind === 'log' && e.trace_id ? `trace:${e.trace_id}` : undefined;
const previous = units[units.length - 1];
if (key && previous && !previous.id && previous.key === key) previous.entries.push(e);
else units.push({ key, entries: [e] });
}
});
const startTime = unit => unit.id
? (entries.find(e => e.app === app && e.initialization_id === unit.id && e.kind === 'initialization_started') || unit.entries[0]).time
: unit.entries[0].time;
const startTime = unit => unit.entries[0].time;
units.sort((a, b) => startTime(a) - startTime(b));
if (units.length > 150) rows.append(node('div', `${units.length - 150} older rows/groups: narrow the time range or search.`, 'more'));
units.slice(-150).forEach(unit => {
appendEventRows(rows, unit.entries, unit.id ? `initialization:${unit.id}` : undefined);
appendEventRows(rows, unit.entries, unit.key);
});
detail.append(rows);
const position = logScrollPositions.get(app);
+1 -28
View File
@@ -223,14 +223,7 @@ export class LogExplorer implements vscode.Disposable {
}
private publish() {
const publication = ++this.publication;
const entries = [...this.entries.values()].sort((a, b) => {
const time = a.time - b.time;
if (time) { return time; }
// Include raw app.log() lines written in the same second as initialization markers.
const priority = (entry: LogEntry) => /Initialization (?:started|completed|failed)/.test(entry.message)
? (entry.message.includes('started') ? -1 : 1) : 0;
return priority(a) - priority(b);
});
const entries = [...this.entries.values()].sort((a, b) => a.time - b.time);
// Legacy errors duplicated across main/error logs are merged only on exact match.
const seen = new Set<string>();
const unique = entries.filter(e => {
@@ -238,26 +231,6 @@ export class LogExplorer implements vscode.Disposable {
if (seen.has(key)) { return false; } seen.add(key); return true;
});
this.onErrorCountChanged?.(unique.filter(e => (e.kind === 'log' || e.kind.startsWith('initialization_')) && (e.level === 'ERROR' || e.level === 'CRITICAL')).length);
// Direct app.log() calls have no LoggerInterface tag. Infer their initialization
// group while an explicit per-app initialize() scope is open.
const activeInitialization = new Map<string, string>();
for (const entry of unique) {
const marker = entry.message.match(/Initialization (started|completed|failed) \[group=initialization:([a-f0-9]{12})\]/i);
if (marker?.[1] === 'started') { activeInitialization.set(entry.app, marker[2]); }
const activeId = activeInitialization.get(entry.app);
if (activeId && !entry.group_id) {
entry.group_id = activeId;
entry.group_kind = 'initialization';
entry.initialization_id = activeId;
entry.phase = 'initialization';
entry.group_inferred = true;
}
if (entry.group_kind === 'initialization') {
entry.initialization_id = entry.initialization_id || entry.group_id;
entry.phase = 'initialization';
}
if (marker && (marker[1] === 'completed' || marker[1] === 'failed')) { activeInitialization.delete(entry.app); }
}
const zone = vscode.workspace.getConfiguration('appdaemon').get<string>('logTimezone', 'Europe/Berlin');
this.activeWebview?.postMessage({ type: 'entries', entries: unique, warnings: [...this.warnings], limit: 10000, timezone: zone });
void this.resolveSources(unique, publication);
+5 -7
View File
@@ -3,8 +3,8 @@ export interface LogEntry {
kind: string; cause_ids: string[]; probable_cause_ids?: string[];
entity?: string; values?: Record<string, unknown>;
source?: { path: string; line: number };
initialization_id?: string; phase?: string; status?: string;
group_id?: string; group_kind?: string; group_inferred?: boolean;
status?: string;
group_id?: string; group_kind?: string;
file?: string; raw?: string; time: number; trace_id?: string;
[key: string]: unknown;
}
@@ -38,12 +38,10 @@ export function parseLine(line: string, file: string, offset: number, zone = 'Eu
}
const m = line.match(/^(\d{2}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(?:\.\d+)?)\s+(DEBUG|INFO|WARNING|ERROR|CRITICAL)\s+([^:]+):\s?(.*)$/);
if (!m) { return undefined; }
const init = m[4].match(/\[init=([\w-]+)\]/);
const group = m[4].match(/\[group=(initialization|smartcondition|value_selector):([a-f0-9]{12})\]/i);
const group = m[4].match(/\[group=([^\]\r\n]+):([a-f0-9]{12})\]/i);
const id = m[4].match(/\[event=([\w-]+) trace=([\w-]+)\]/);
const groupKind = group?.[1]?.toLowerCase();
const initializationId = groupKind === 'initialization' ? group?.[2] : init?.[1];
return { event_id: id?.[1] || `${file}:${offset}`, trace_id: id?.[2], initialization_id: initializationId, phase: initializationId ? 'initialization' : undefined, group_id: group?.[2], group_kind: groupKind, timestamp: m[1], time: rawTime(m[1], zone), app: m[3].trim(), level: m[2], message: m[4], kind: 'log', cause_ids: [], file, raw: line };
const groupKind = group?.[1]?.trim();
return { event_id: id?.[1] || `${file}:${offset}`, trace_id: id?.[2], group_id: group?.[2], group_kind: groupKind, timestamp: m[1], time: rawTime(m[1], zone), app: m[3].trim(), level: m[2], message: m[4], kind: 'log', cause_ids: [], file, raw: line };
}
/** Preserve valid UTF-8 spans, including files containing mixed legacy bytes. */
Binary file not shown.