Add logging with filters

This commit is contained in:
Twirre
2025-11-12 14:35:38 +00:00
committed by Gerla, J. (Justin)
parent b7eb0cb5ec
commit 231d7a5ba1
22 changed files with 1899 additions and 68 deletions

View File

@@ -0,0 +1,146 @@
import {useCallback, useEffect, useRef, useState} from "react";
import {applyPriorityPredicates, type PriorityFilterPredicate} from "../../utils/priorityFiltering.ts";
import {cell, type Cell} from "../../utils/cellStore.ts";
export type LogRecord = {
name: string;
message: string;
levelname: 'DEBUG' | 'INFO' | 'WARNING' | 'ERROR' | 'CRITICAL' | string;
levelno: number;
created: number;
relativeCreated: number;
reference?: string;
firstCreated: number;
firstRelativeCreated: number;
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type LogFilterPredicate = PriorityFilterPredicate<LogRecord> & { value: any };
export function useLogs(filterPredicates: Map<string, LogFilterPredicate>) {
const [distinctNames, setDistinctNames] = useState<Set<string>>(new Set());
const [filtered, setFiltered] = useState<Cell<LogRecord>[]>([]);
const sseRef = useRef<EventSource | null>(null);
const filtersRef = useRef(filterPredicates);
const logsRef = useRef<LogRecord[]>([]);
/** Map to store the first message for each reference, instance can be updated to change contents. */
const firstByRefRef = useRef<Map<string, Cell<LogRecord>>>(new Map());
/**
* Apply the filter predicates to a log record.
* @param log The log record to apply the filters to.
* @returns `true` if the record passes.
*/
const applyFilters = useCallback((log: LogRecord) =>
applyPriorityPredicates(log, [...filtersRef.current.values()]), []);
/** Recomputes the entire filtered list. Use when filter predicates change. */
const recomputeFiltered = useCallback(() => {
const newFiltered: Cell<LogRecord>[] = [];
firstByRefRef.current = new Map();
for (const message of logsRef.current) {
const messageCell = cell<LogRecord>({
...message,
firstCreated: message.created,
firstRelativeCreated: message.relativeCreated,
});
if (message.reference) {
const first = firstByRefRef.current.get(message.reference);
if (first) {
// Update the first's contents
first.set((prev) => ({
...message,
firstCreated: prev.firstCreated ?? prev.created,
firstRelativeCreated: prev.firstRelativeCreated ?? prev.relativeCreated,
}));
// Don't add it to the list again
continue;
} else {
// Add the first message with this reference to the registry
firstByRefRef.current.set(message.reference, messageCell);
}
}
if (applyFilters(message)) {
newFiltered.push(messageCell);
}
}
setFiltered(newFiltered);
}, [applyFilters, setFiltered]);
// Reapply filters to all logs, only when filters change
useEffect(() => {
filtersRef.current = filterPredicates;
recomputeFiltered();
}, [filterPredicates, recomputeFiltered]);
/**
* Handle a new log message. Updates the filtered list and to the full history.
* @param message The new log message.
*/
const handleNewMessage = useCallback((message: LogRecord) => {
// Add to the full history for re-filtering on filter changes
logsRef.current.push(message);
setDistinctNames((prev) => {
if (prev.has(message.name)) return prev;
const newSet = new Set(prev);
newSet.add(message.name);
return newSet;
});
const messageCell = cell<LogRecord>({
...message,
firstCreated: message.created,
firstRelativeCreated: message.relativeCreated,
});
if (message.reference) {
const first = firstByRefRef.current.get(message.reference);
if (first) {
// Update the first's contents
first.set((prev) => ({
...message,
firstCreated: prev.firstCreated ?? prev.created,
firstRelativeCreated: prev.firstRelativeCreated ?? prev.relativeCreated,
}));
// Don't add it to the list again
return;
} else {
// Add the first message with this reference to the registry
firstByRefRef.current.set(message.reference, messageCell);
}
}
if (applyFilters(message)) {
setFiltered((curr) => [...curr, messageCell]);
}
}, [applyFilters, setFiltered]);
useEffect(() => {
if (sseRef.current) return;
const es = new EventSource("http://localhost:8000/logs/stream");
sseRef.current = es;
es.onmessage = (event) => {
const data: LogRecord = JSON.parse(event.data);
handleNewMessage(data);
};
return () => {
es.close();
sseRef.current = null;
};
}, [handleNewMessage]);
return {filteredLogs: filtered, distinctNames};
}