Add experiment logs to the monitoring page
This commit is contained in:
31
src/components/Logging/Definitions.ts
Normal file
31
src/components/Logging/Definitions.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import type {Cell} from "../../utils/cellStore.ts";
|
||||
import type {LogRecord} from "./useLogs.ts";
|
||||
|
||||
/**
|
||||
* Zustand store definition for managing user preferences related to logging.
|
||||
*
|
||||
* Includes flags for toggling relative timestamps and automatic scroll behavior.
|
||||
*/
|
||||
export type LoggingSettings = {
|
||||
/** Whether to display log timestamps as relative (e.g., "2m 15s ago") instead of absolute. */
|
||||
showRelativeTime: boolean;
|
||||
/** Updates the `showRelativeTime` setting. */
|
||||
setShowRelativeTime: (showRelativeTime: boolean) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Props for any component that renders a single log message entry.
|
||||
*
|
||||
* @param recordCell - A reactive `Cell` containing a single `LogRecord`.
|
||||
* @param onUpdate - Optional callback triggered when the log entry updates.
|
||||
*/
|
||||
export type MessageComponentProps = {
|
||||
recordCell: Cell<LogRecord>,
|
||||
onUpdate?: () => void,
|
||||
};
|
||||
|
||||
/**
|
||||
* Key used for the experiment filter predicate in the filter map, to exclude experiment logs from the developer logs.
|
||||
*/
|
||||
export const EXPERIMENT_FILTER_KEY = "experiment_filter";
|
||||
export const EXPERIMENT_LOGGER_NAME = "experiment";
|
||||
@@ -13,9 +13,8 @@ type Setter<T> = (value: T | ((prev: T) => T)) => void;
|
||||
* Mapping of log level names to their corresponding numeric severity.
|
||||
* Used for comparison in log filtering predicates.
|
||||
*/
|
||||
const optionMapping = new Map([
|
||||
const optionMapping: Map<string, number> = new Map([
|
||||
["ALL", 0],
|
||||
["LLM", 9],
|
||||
["DEBUG", 10],
|
||||
["INFO", 20],
|
||||
["WARNING", 30],
|
||||
@@ -93,7 +92,7 @@ function GlobalLevelFilter({
|
||||
filterPredicates: Map<string, LogFilterPredicate>;
|
||||
setFilterPredicates: Setter<Map<string, LogFilterPredicate>>;
|
||||
}) {
|
||||
const selected = filterPredicates.get(GLOBAL_LOG_LEVEL_PREDICATE_KEY)?.value || "ALL";
|
||||
const selected = filterPredicates.get(GLOBAL_LOG_LEVEL_PREDICATE_KEY)?.value ?? "ALL";
|
||||
const setSelected = (selected: string | null) => {
|
||||
if (!selected || !optionMapping.has(selected)) return;
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
flex-shrink: 0;
|
||||
|
||||
box-shadow: 0 0 1rem black;
|
||||
padding: 1rem 1rem 0 1rem;
|
||||
}
|
||||
|
||||
.no-numbers {
|
||||
@@ -15,8 +14,6 @@
|
||||
}
|
||||
|
||||
.log-container {
|
||||
margin-bottom: .5rem;
|
||||
|
||||
.accented-0, .accented-10 {
|
||||
background-color: color-mix(in oklab, canvas, rgb(159, 159, 159) 35%)
|
||||
}
|
||||
@@ -32,7 +29,7 @@
|
||||
}
|
||||
|
||||
.floating-button {
|
||||
position: fixed;
|
||||
position: absolute;
|
||||
bottom: 1rem;
|
||||
right: 1rem;
|
||||
box-shadow: 0 0 1rem rgba(0, 0, 0, 0.5);
|
||||
|
||||
@@ -1,38 +1,23 @@
|
||||
import {useEffect, useRef, useState} from "react";
|
||||
import {create} from "zustand";
|
||||
|
||||
import {type ComponentType, useEffect, useRef, useState} from "react";
|
||||
import formatDuration from "../../utils/formatDuration.ts";
|
||||
import {type LogFilterPredicate, type LogRecord, useLogs} from "./useLogs.ts";
|
||||
import Filters from "./Filters.tsx";
|
||||
import {type Cell, useCell} from "../../utils/cellStore.ts";
|
||||
|
||||
import styles from "./Logging.module.css";
|
||||
|
||||
import {
|
||||
EXPERIMENT_FILTER_KEY,
|
||||
EXPERIMENT_LOGGER_NAME,
|
||||
type LoggingSettings,
|
||||
type MessageComponentProps
|
||||
} from "./Definitions.ts";
|
||||
import {create} from "zustand";
|
||||
|
||||
/**
|
||||
* Zustand store definition for managing user preferences related to logging.
|
||||
*
|
||||
* Includes flags for toggling relative timestamps and automatic scroll behavior.
|
||||
*/
|
||||
type LoggingSettings = {
|
||||
/** Whether to display log timestamps as relative (e.g., "2m 15s ago") instead of absolute. */
|
||||
showRelativeTime: boolean;
|
||||
/** Updates the `showRelativeTime` setting. */
|
||||
setShowRelativeTime: (showRelativeTime: boolean) => void;
|
||||
/** Whether the log view should automatically scroll to the newest entry. */
|
||||
scrollToBottom: boolean;
|
||||
/** Updates the `scrollToBottom` setting. */
|
||||
setScrollToBottom: (scrollToBottom: boolean) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Global Zustand store for logging UI preferences.
|
||||
* Local Zustand store for logging UI preferences.
|
||||
*/
|
||||
const useLoggingSettings = create<LoggingSettings>((set) => ({
|
||||
showRelativeTime: false,
|
||||
setShowRelativeTime: (showRelativeTime: boolean) => set({ showRelativeTime }),
|
||||
scrollToBottom: true,
|
||||
setScrollToBottom: (scrollToBottom: boolean) => set({ scrollToBottom }),
|
||||
}));
|
||||
|
||||
/**
|
||||
@@ -45,13 +30,7 @@ const useLoggingSettings = create<LoggingSettings>((set) => ({
|
||||
* @param onUpdate - Optional callback triggered when the log entry updates.
|
||||
* @returns A JSX element displaying a formatted log message.
|
||||
*/
|
||||
function LogMessage({
|
||||
recordCell,
|
||||
onUpdate,
|
||||
}: {
|
||||
recordCell: Cell<LogRecord>,
|
||||
onUpdate?: () => void,
|
||||
}) {
|
||||
function LogMessage({ recordCell, onUpdate }: MessageComponentProps) {
|
||||
const { showRelativeTime, setShowRelativeTime } = useLoggingSettings();
|
||||
const record = useCell(recordCell);
|
||||
|
||||
@@ -69,7 +48,7 @@ function LogMessage({
|
||||
/** Simplifies the logger name by showing only the last path segment. */
|
||||
const normalizedName = record.name.split(".").pop() || record.name;
|
||||
|
||||
// Notify parent component (e.g. for scroll updates) when this record changes.
|
||||
// Notify the parent component (e.g., for scroll updates) when this record changes.
|
||||
useEffect(() => {
|
||||
if (onUpdate) onUpdate();
|
||||
}, [record, onUpdate]);
|
||||
@@ -77,11 +56,10 @@ function LogMessage({
|
||||
return <div className={`${styles.logContainer} round-md border-lg flex-row gap-md`}>
|
||||
<div className={`${styles[`accented${normalizedLevelNo}`]} flex-col padding-sm justify-between`}>
|
||||
<span className={"mono bold"}>{record.levelname}</span>
|
||||
<span className={"mono clickable font-small"}
|
||||
onClick={() => setShowRelativeTime(!showRelativeTime)}
|
||||
>{showRelativeTime
|
||||
? formatDuration(record.relativeCreated)
|
||||
: new Date(record.created * 1000).toLocaleTimeString()
|
||||
<span className={"mono clickable font-small"} onClick={() => setShowRelativeTime(!showRelativeTime)}>{
|
||||
showRelativeTime
|
||||
? formatDuration(record.relativeCreated)
|
||||
: new Date(record.created * 1000).toLocaleTimeString()
|
||||
}</span>
|
||||
</div>
|
||||
<div className={"flex-col flex-1 padding-sm"}>
|
||||
@@ -100,12 +78,18 @@ function LogMessage({
|
||||
* - A floating "Scroll to bottom" button when not at the bottom.
|
||||
*
|
||||
* @param recordCells - Array of reactive log records to display.
|
||||
* @param MessageComponent - A component to use to render each log message entry.
|
||||
* @returns A scrollable log list component.
|
||||
*/
|
||||
function LogMessages({ recordCells }: { recordCells: Cell<LogRecord>[] }) {
|
||||
export function LogMessages({
|
||||
recordCells,
|
||||
MessageComponent,
|
||||
}: {
|
||||
recordCells: Cell<LogRecord>[],
|
||||
MessageComponent: ComponentType<MessageComponentProps>,
|
||||
}) {
|
||||
const scrollableRef = useRef<HTMLDivElement>(null);
|
||||
const lastElementRef = useRef<HTMLLIElement>(null)
|
||||
const { scrollToBottom, setScrollToBottom } = useLoggingSettings();
|
||||
const [scrollToBottom, setScrollToBottom] = useState(true);
|
||||
|
||||
// Disable auto-scroll if the user manually scrolls.
|
||||
useEffect(() => {
|
||||
@@ -124,30 +108,28 @@ function LogMessages({ recordCells }: { recordCells: Cell<LogRecord>[] }) {
|
||||
}, [scrollableRef, setScrollToBottom]);
|
||||
|
||||
/**
|
||||
* Scrolls the last log message into view if auto-scroll is enabled,
|
||||
* or if forced (e.g., user clicks "Scroll to bottom").
|
||||
* Scrolls the log messages to the bottom, making the latest messages visible.
|
||||
*
|
||||
* @param force - If true, forces scrolling even if `scrollToBottom` is false.
|
||||
*/
|
||||
function scrollLastElementIntoView(force = false) {
|
||||
if ((!scrollToBottom && !force) || !lastElementRef.current) return;
|
||||
lastElementRef.current.scrollIntoView({ behavior: "smooth" });
|
||||
function showBottom(force = false) {
|
||||
if ((!scrollToBottom && !force) || !scrollableRef.current) return;
|
||||
scrollableRef.current.scrollTo({top: scrollableRef.current.scrollHeight, left: 0, behavior: "smooth"});
|
||||
}
|
||||
|
||||
return <div ref={scrollableRef} className={"min-height-0 scroll-y padding-b-md"}>
|
||||
return <div ref={scrollableRef} className={"min-height-0 scroll-y padding-h-lg padding-b-lg flex-1"}>
|
||||
<ol className={`${styles.noNumbers} margin-0 flex-col gap-md`}>
|
||||
{recordCells.map((recordCell, i) => (
|
||||
<li key={`${i}_${recordCell.get().firstRelativeCreated}`}>
|
||||
<LogMessage recordCell={recordCell} onUpdate={scrollLastElementIntoView} />
|
||||
<MessageComponent recordCell={recordCell} onUpdate={showBottom} />
|
||||
</li>
|
||||
))}
|
||||
<li ref={lastElementRef}></li>
|
||||
</ol>
|
||||
{!scrollToBottom && <button
|
||||
className={styles.floatingButton}
|
||||
onClick={() => {
|
||||
setScrollToBottom(true);
|
||||
scrollLastElementIntoView(true);
|
||||
showBottom(true);
|
||||
}}
|
||||
>
|
||||
Scroll to bottom
|
||||
@@ -164,16 +146,27 @@ function LogMessages({ recordCells }: { recordCells: Cell<LogRecord>[] }) {
|
||||
* - Zustand-managed UI settings (auto-scroll, timestamp display).
|
||||
*
|
||||
* This component uses the `useLogs` hook to fetch and filter logs based on
|
||||
* active predicates, and re-renders automatically as new logs arrive.
|
||||
* active predicates and re-renders automatically as new logs arrive.
|
||||
*
|
||||
* @returns The complete logging UI as a React element.
|
||||
*/
|
||||
export default function Logging() {
|
||||
const [filterPredicates, setFilterPredicates] = useState(new Map<string, LogFilterPredicate>());
|
||||
// By default, filter experiment logs from this debug logger
|
||||
const [filterPredicates, setFilterPredicates] = useState(new Map<string, LogFilterPredicate>([
|
||||
[
|
||||
EXPERIMENT_FILTER_KEY,
|
||||
{
|
||||
predicate: (r) => r.name == EXPERIMENT_LOGGER_NAME ? false : null,
|
||||
priority: 999,
|
||||
value: null,
|
||||
} as LogFilterPredicate,
|
||||
],
|
||||
]));
|
||||
const { filteredLogs, distinctNames } = useLogs(filterPredicates)
|
||||
distinctNames.delete(EXPERIMENT_LOGGER_NAME);
|
||||
|
||||
return <div className={`flex-col gap-lg min-height-0 ${styles.loggingContainer}`}>
|
||||
<div className={"flex-row gap-lg justify-between align-center"}>
|
||||
return <div className={`flex-col min-height-0 relative ${styles.loggingContainer}`}>
|
||||
<div className={"flex-row gap-lg justify-between align-center padding-lg"}>
|
||||
<h2 className={"margin-0"}>Logs</h2>
|
||||
<Filters
|
||||
filterPredicates={filterPredicates}
|
||||
@@ -181,6 +174,6 @@ export default function Logging() {
|
||||
agentNames={distinctNames}
|
||||
/>
|
||||
</div>
|
||||
<LogMessages recordCells={filteredLogs} />
|
||||
<LogMessages recordCells={filteredLogs} MessageComponent={LogMessage} />
|
||||
</div>;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,21 @@ import {useCallback, useEffect, useRef, useState} from "react";
|
||||
import {applyPriorityPredicates, type PriorityFilterPredicate} from "../../utils/priorityFiltering.ts";
|
||||
import {cell, type Cell} from "../../utils/cellStore.ts";
|
||||
|
||||
type ExtraLevelName = 'LLM' | 'OBSERVATION' | 'ACTION' | 'CHAT';
|
||||
|
||||
export type LevelName = ExtraLevelName | 'DEBUG' | 'INFO' | 'WARNING' | 'ERROR' | 'CRITICAL' | string;
|
||||
|
||||
/**
|
||||
* Extra fields that are added to log records in the backend but are not part of the standard `LogRecord` type.
|
||||
*
|
||||
* @property reference - (Optional) A reference identifier linking related log messages.
|
||||
* @property role - (Optional) For chat log messages, the role of the agent that generated the message.
|
||||
*/
|
||||
type ExtraLogRecordFields = {
|
||||
reference?: string;
|
||||
role?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a single log record emitted by the backend logging system.
|
||||
*
|
||||
@@ -12,21 +27,19 @@ import {cell, type Cell} from "../../utils/cellStore.ts";
|
||||
* @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;
|
||||
levelname: LevelName;
|
||||
levelno: number;
|
||||
created: number;
|
||||
relativeCreated: number;
|
||||
reference?: string;
|
||||
firstCreated: number;
|
||||
firstRelativeCreated: number;
|
||||
};
|
||||
} & ExtraLogRecordFields;
|
||||
|
||||
/**
|
||||
* A log filter predicate with priority support, used to determine whether
|
||||
@@ -37,7 +50,7 @@ export type LogRecord = {
|
||||
*
|
||||
* @template T - The type of record being filtered (here, `LogRecord`).
|
||||
*/
|
||||
export type LogFilterPredicate = PriorityFilterPredicate<LogRecord> & {
|
||||
export type LogFilterPredicate = PriorityFilterPredicate<LogRecord> & {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
value: any };
|
||||
|
||||
|
||||
Reference in New Issue
Block a user