Add logging with filters
This commit is contained in:
committed by
Gerla, J. (Justin)
parent
b7eb0cb5ec
commit
231d7a5ba1
146
src/components/Logging/useLogs.ts
Normal file
146
src/components/Logging/useLogs.ts
Normal 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};
|
||||
}
|
||||
Reference in New Issue
Block a user