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 & { // 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` 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) { /** Distinct logger names encountered across all logs. */ const [distinctNames, setDistinctNames] = useState>(new Set()); /** Filtered logs that pass all active predicates, stored as reactive cells. */ const [filtered, setFiltered] = useState[]>([]); /** Persistent reference to the active EventSource connection. */ const sseRef = useRef(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([]); /** Map to store the first message for each reference, instance can be updated to change contents. */ const firstByRefRef = useRef>>(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[] = []; firstByRefRef.current = new Map(); for (const message of logsRef.current) { const messageCell = cell({ ...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({ ...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}; }