All files / src/components/Logging useLogs.ts

0% Statements 0/57
0% Branches 0/24
0% Functions 0/12
0% Lines 0/52

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213                                                                                                                                                                                                                                                                                                                                                                                                                                         
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};
}