213 lines
7.6 KiB
TypeScript
213 lines
7.6 KiB
TypeScript
import {useCallback, useEffect, useRef, useState} from "react";
|
|
|
|
import {applyPriorityPredicates, type PriorityFilterPredicate} from "../../utils/priorityFiltering.ts";
|
|
import {cell, type Cell} from "../../utils/cellStore.ts";
|
|
|
|
/**
|
|
* Represents a single log record emitted by the backend logging system.
|
|
*
|
|
* @property name - The name of the logger or source (e.g., `"agent.core"`).
|
|
* @property message - The message content of the log record.
|
|
* @property levelname - The human-readable severity level (e.g., `"INFO"`, `"ERROR"`).
|
|
* @property levelno - The numeric severity value corresponding to `levelname`.
|
|
* @property created - The UNIX timestamp (in seconds) when this record was created.
|
|
* @property relativeCreated - The time (in milliseconds) since the logging system started.
|
|
* @property reference - (Optional) A reference identifier linking related log messages.
|
|
* @property firstCreated - Timestamp of the first log in this reference group.
|
|
* @property firstRelativeCreated - Relative timestamp of the first log in this reference group.
|
|
*/
|
|
export type LogRecord = {
|
|
name: string;
|
|
message: string;
|
|
levelname: 'LLM' | 'DEBUG' | 'INFO' | 'WARNING' | 'ERROR' | 'CRITICAL' | string;
|
|
levelno: number;
|
|
created: number;
|
|
relativeCreated: number;
|
|
reference?: string;
|
|
firstCreated: number;
|
|
firstRelativeCreated: number;
|
|
};
|
|
|
|
/**
|
|
* A log filter predicate with priority support, used to determine whether
|
|
* a log record should be displayed.
|
|
*
|
|
* This extends a general `PriorityFilterPredicate` and includes an optional
|
|
* `value` field for UI metadata (e.g., selected log level or agent).
|
|
*
|
|
* @template T - The type of record being filtered (here, `LogRecord`).
|
|
*/
|
|
export type LogFilterPredicate = PriorityFilterPredicate<LogRecord> & {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
value: any };
|
|
|
|
/**
|
|
* React hook that manages the lifecycle of log records, including:
|
|
* - Receiving live log messages via Server-Sent Events (SSE),
|
|
* - Applying priority-based filtering rules,
|
|
* - Managing distinct logger names and reference-linked messages.
|
|
*
|
|
* Returns both the filtered logs (as reactive `Cell<LogRecord>` objects)
|
|
* and a set of distinct logger names for use in UI components (e.g., Filters).
|
|
*
|
|
* @param filterPredicates - A `Map` of log filter predicates, keyed by ID or type.
|
|
* @returns An object containing:
|
|
* - `filteredLogs`: The currently visible (filtered) log messages.
|
|
* - `distinctNames`: A set of all distinct logger names encountered.
|
|
*
|
|
* @example
|
|
* ```ts
|
|
* const { filteredLogs, distinctNames } = useLogs(activeFilters);
|
|
* ```
|
|
*/
|
|
export function useLogs(filterPredicates: Map<string, LogFilterPredicate>) {
|
|
/** Distinct logger names encountered across all logs. */
|
|
const [distinctNames, setDistinctNames] = useState<Set<string>>(new Set());
|
|
/** Filtered logs that pass all active predicates, stored as reactive cells. */
|
|
const [filtered, setFiltered] = useState<Cell<LogRecord>[]>([]);
|
|
|
|
/** Persistent reference to the active EventSource connection. */
|
|
const sseRef = useRef<EventSource | null>(null);
|
|
/** Keeps a stable reference to the current filter map (avoids re-renders). */
|
|
const filtersRef = useRef(filterPredicates);
|
|
/** Stores all received logs (the unfiltered full history). */
|
|
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 all active filter predicates to a log record.
|
|
* @param log The log record to apply the filters to.
|
|
* @returns `true` if the record passes all filters; otherwise `false`.
|
|
*/
|
|
const applyFilters = useCallback((log: LogRecord) =>
|
|
applyPriorityPredicates(log, [...filtersRef.current.values()]), []);
|
|
|
|
/**
|
|
* Fully recomputes the filtered log list based on the current
|
|
* filter predicates and historical logs.
|
|
*
|
|
* Should be invoked whenever the filter map changes.
|
|
*/
|
|
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,
|
|
});
|
|
|
|
// Handle reference grouping: update the first message in the group.
|
|
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,
|
|
}));
|
|
|
|
continue; // Don't add it to the list again (it's a duplicate).
|
|
} else {
|
|
// Add the first message with this reference to the registry
|
|
firstByRefRef.current.set(message.reference, messageCell);
|
|
}
|
|
}
|
|
|
|
// Include only if it passes current filters.
|
|
if (applyFilters(message)) {
|
|
newFiltered.push(messageCell);
|
|
}
|
|
}
|
|
|
|
setFiltered(newFiltered);
|
|
}, [applyFilters, setFiltered]);
|
|
|
|
// Re-filter all logs whenever filter predicates change.
|
|
useEffect(() => {
|
|
filtersRef.current = filterPredicates;
|
|
recomputeFiltered();
|
|
}, [filterPredicates, recomputeFiltered]);
|
|
|
|
/**
|
|
* Handles a newly received log record.
|
|
* Updates the full log history, distinct names set, and filtered log list.
|
|
*
|
|
* @param message - The new log record to process.
|
|
*/
|
|
const handleNewMessage = useCallback((message: LogRecord) => {
|
|
// Store in complete history for future refiltering.
|
|
logsRef.current.push(message);
|
|
|
|
// Track distinct logger names.
|
|
setDistinctNames((prev) => {
|
|
if (prev.has(message.name)) return prev;
|
|
const newSet = new Set(prev);
|
|
newSet.add(message.name);
|
|
return newSet;
|
|
});
|
|
|
|
// Wrap in a reactive cell for UI binding.
|
|
const messageCell = cell<LogRecord>({
|
|
...message,
|
|
firstCreated: message.created,
|
|
firstRelativeCreated: message.relativeCreated,
|
|
});
|
|
|
|
// Handle reference-linked updates.
|
|
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,
|
|
}));
|
|
|
|
return; // Do not duplicate reference group entries.
|
|
} else {
|
|
firstByRefRef.current.set(message.reference, messageCell);
|
|
}
|
|
}
|
|
|
|
// Only append if message passes filters.
|
|
if (applyFilters(message)) {
|
|
setFiltered((curr) => [...curr, messageCell]);
|
|
}
|
|
}, [applyFilters, setFiltered]);
|
|
|
|
/**
|
|
* Initializes the SSE (Server-Sent Events) stream for real-time logs.
|
|
*
|
|
* Subscribes to messages from the backend logging endpoint and
|
|
* dispatches each message to `handleNewMessage`.
|
|
*
|
|
* Cleans up the EventSource connection when the component unmounts.
|
|
*/
|
|
useEffect(() => {
|
|
// Only create one SSE connection for the lifetime of the hook.
|
|
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};
|
|
}
|