import moment from "moment"; import { useQuery } from "@tanstack/react-query"; import { useState, useRef, useEffect, useCallback } from "react"; import { useQueryClient } from "@tanstack/react-query"; import { uiSpendLogsCall, keyInfoV1Call, sessionSpendLogsCall, keyListCall } from "../networking"; import { DataTable } from "./table"; import { columns, LogEntry } from "./columns"; import { Row } from "@tanstack/react-table"; import { prefetchLogDetails } from "./prefetch"; import { RequestResponsePanel } from './RequestResponsePanel'; import { ErrorViewer } from './ErrorViewer'; import { internalUserRoles } from "../../utils/roles"; import { ConfigInfoMessage } from './ConfigInfoMessage'; import { Tooltip } from "antd"; import { KeyResponse, Team } from "../key_team_helpers/key_list"; import KeyInfoView from "../key_info_view"; import { SessionView } from './SessionView'; import { VectorStoreViewer } from './VectorStoreViewer'; import { GuardrailViewer } from './GuardrailViewer'; import FilterComponent from "../common_components/filter"; import { FilterOption } from "../common_components/filter"; import { useLogFilterLogic } from "./log_filter_logic"; import { fetchAllKeyAliases } from "../key_team_helpers/filter_helpers"; import { Tab, TabGroup, TabList, TabPanels, TabPanel, Text, } from "@tremor/react"; import AuditLogs from "./audit_logs"; import { getTimeRangeDisplay } from "./logs_utils"; interface SpendLogsTableProps { accessToken: string | null; token: string | null; userRole: string | null; userID: string | null; allTeams: Team[]; premiumUser: boolean; } export interface PaginatedResponse { data: LogEntry[]; total: number; page: number; page_size: number; total_pages: number; } interface PrefetchedLog { messages: any[]; response: any; } export default function SpendLogsTable({ accessToken, token, userRole, userID, allTeams, premiumUser, }: SpendLogsTableProps) { const [searchTerm, setSearchTerm] = useState(""); const [showFilters, setShowFilters] = useState(false); const [showColumnDropdown, setShowColumnDropdown] = useState(false); const [currentPage, setCurrentPage] = useState(1); const [pageSize] = useState(50); const dropdownRef = useRef(null); const filtersRef = useRef(null); const quickSelectRef = useRef(null); // New state variables for Start and End Time const [startTime, setStartTime] = useState( moment().subtract(24, "hours").format("YYYY-MM-DDTHH:mm") ); const [endTime, setEndTime] = useState( moment().format("YYYY-MM-DDTHH:mm") ); // Add these new state variables at the top with other useState declarations const [isCustomDate, setIsCustomDate] = useState(false); const [quickSelectOpen, setQuickSelectOpen] = useState(false); const [tempTeamId, setTempTeamId] = useState(""); const [tempKeyHash, setTempKeyHash] = useState(""); const [selectedTeamId, setSelectedTeamId] = useState(""); const [selectedKeyHash, setSelectedKeyHash] = useState(""); const [selectedModel, setSelectedModel] = useState(""); const [selectedKeyInfo, setSelectedKeyInfo] = useState(null); const [selectedKeyIdInfoView, setSelectedKeyIdInfoView] = useState(null); const [selectedStatus, setSelectedStatus] = useState(""); const [filterByCurrentUser, setFilterByCurrentUser] = useState( userRole && internalUserRoles.includes(userRole) ); const [activeTab, setActiveTab] = useState("request logs"); const [expandedRequestId, setExpandedRequestId] = useState(null); const [selectedSessionId, setSelectedSessionId] = useState(null); const queryClient = useQueryClient(); useEffect(() => { const fetchKeyInfo = async () => { if (selectedKeyIdInfoView && accessToken) { const keyData = await keyInfoV1Call(accessToken, selectedKeyIdInfoView); const keyResponse: KeyResponse = { ...keyData["info"], "token": selectedKeyIdInfoView, "api_key": selectedKeyIdInfoView, }; setSelectedKeyInfo(keyResponse); } }; fetchKeyInfo(); }, [selectedKeyIdInfoView, accessToken]); // Close dropdown when clicking outside useEffect(() => { function handleClickOutside(event: MouseEvent) { if ( dropdownRef.current && !dropdownRef.current.contains(event.target as Node) ) { setShowColumnDropdown(false); } if ( filtersRef.current && !filtersRef.current.contains(event.target as Node) ) { setShowFilters(false); } if ( quickSelectRef.current && !quickSelectRef.current.contains(event.target as Node) ) { setQuickSelectOpen(false); } } document.addEventListener("mousedown", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside); }, []); useEffect(() => { if (userRole && internalUserRoles.includes(userRole)) { setFilterByCurrentUser(true); } }, [userRole]); const logs = useQuery({ queryKey: [ "logs", "table", currentPage, pageSize, startTime, endTime, selectedTeamId, selectedKeyHash, filterByCurrentUser ? userID : null, selectedStatus, selectedModel ], queryFn: async () => { if (!accessToken || !token || !userRole || !userID) { return { data: [], total: 0, page: 1, page_size: pageSize, total_pages: 0, }; } const formattedStartTime = moment(startTime).utc().format("YYYY-MM-DD HH:mm:ss"); const formattedEndTime = isCustomDate ? moment(endTime).utc().format("YYYY-MM-DD HH:mm:ss") : moment().utc().format("YYYY-MM-DD HH:mm:ss"); // Get base response from API const response = await uiSpendLogsCall( accessToken, selectedKeyHash || undefined, selectedTeamId || undefined, undefined, formattedStartTime, formattedEndTime, currentPage, pageSize, filterByCurrentUser ? userID : undefined, selectedStatus, selectedModel ); // Trigger prefetch for all logs await prefetchLogDetails( response.data, formattedStartTime, accessToken, queryClient ); // Update logs with prefetched data if available response.data = response.data.map((log: LogEntry) => { const prefetchedData = queryClient.getQueryData( ["logDetails", log.request_id, formattedStartTime] ); if (prefetchedData?.messages && prefetchedData?.response) { log.messages = prefetchedData.messages; log.response = prefetchedData.response; return log; } return log; }); return response; }, enabled: !!accessToken && !!token && !!userRole && !!userID && activeTab === "request logs", refetchInterval: 5000, refetchIntervalInBackground: true, }); const logsData = logs.data || { data: [], total: 0, page: 1, page_size: pageSize || 10, total_pages: 1 }; const { filters, filteredLogs, allTeams: hookAllTeams, allKeyAliases, handleFilterChange, handleFilterReset } = useLogFilterLogic({ logs: logsData, accessToken, startTime, endTime, pageSize, isCustomDate, setCurrentPage, userID, userRole }) const fetchKeyHashForAlias = useCallback(async (keyAlias: string) => { if (!accessToken) return; try { const response = await keyListCall( accessToken, null, null, keyAlias, null, null, currentPage, pageSize ); const selectedKey = response.keys.find( (key: any) => key.key_alias === keyAlias ); if (selectedKey) { setSelectedKeyHash(selectedKey.token); } } catch (error) { console.error("Error fetching key hash for alias:", error); } }, [accessToken, currentPage, pageSize]); // Add this effect to update selected filters when filter changes useEffect(() => { if(!accessToken) return; if (filters['Team ID']) { setSelectedTeamId(filters['Team ID']); } else { setSelectedTeamId(""); } setSelectedStatus(filters['Status'] || ""); setSelectedModel(filters['Model'] || ""); if (filters['Key Hash']) { setSelectedKeyHash(filters['Key Hash']); } else if (filters['Key Alias']) { fetchKeyHashForAlias(filters['Key Alias']); } else { setSelectedKeyHash(""); } }, [filters, accessToken, fetchKeyHashForAlias]); // Fetch logs for a session if selected const sessionLogs = useQuery({ queryKey: ["sessionLogs", selectedSessionId], queryFn: async () => { if (!accessToken || !selectedSessionId) return { data: [], total: 0, page: 1, page_size: 50, total_pages: 1 }; const response = await sessionSpendLogsCall(accessToken, selectedSessionId); // If the API returns an array, wrap it in the same shape as PaginatedResponse return { data: response.data || response || [], total: (response.data || response || []).length, page: 1, page_size: 1000, total_pages: 1, }; }, enabled: !!accessToken && !!selectedSessionId, }); // Add this effect to preserve expanded state when data refreshes useEffect(() => { if (logs.data?.data && expandedRequestId) { // Check if the expanded request ID still exists in the new data const stillExists = logs.data.data.some(log => log.request_id === expandedRequestId); if (!stillExists) { // If the request ID no longer exists in the data, clear the expanded state setExpandedRequestId(null); } } }, [logs.data?.data, expandedRequestId]); if (!accessToken || !token || !userRole || !userID) { return null; } const filteredData = filteredLogs.data.filter((log) => { const matchesSearch = !searchTerm || log.request_id.includes(searchTerm) || log.model.includes(searchTerm) || (log.user && log.user.includes(searchTerm)); // No need for additional filtering since we're now handling this in the API call return matchesSearch; }).map(log => ({ ...log, onKeyHashClick: (keyHash: string) => setSelectedKeyIdInfoView(keyHash), onSessionClick: (sessionId: string) => { if (sessionId) setSelectedSessionId(sessionId); }, })) || []; // For session logs, add onKeyHashClick/onSessionClick as well const sessionData = sessionLogs.data?.data?.map(log => ({ ...log, onKeyHashClick: (keyHash: string) => setSelectedKeyIdInfoView(keyHash), onSessionClick: (sessionId: string) => {}, })) || []; // Add this function to handle manual refresh const handleRefresh = () => { logs.refetch(); }; const handleRowExpand = (requestId: string | null) => { setExpandedRequestId(requestId); }; const logFilterOptions: FilterOption[] = [ { name: 'Team ID', label: 'Team ID', isSearchable: true, searchFn: async (searchText: string) => { if (!allTeams || allTeams.length === 0) return []; const filtered = allTeams.filter((team: Team) =>{ return team.team_id.toLowerCase().includes(searchText.toLowerCase()) || (team.team_alias && team.team_alias.toLowerCase().includes(searchText.toLowerCase())) }); return filtered.map((team: Team) => ({ label: `${team.team_alias || team.team_id} (${team.team_id})`, value: team.team_id })); } }, { name:'Status', label:'Status', isSearchable: false, options: [ { label: 'Success', value: 'success' }, { label: 'Failure', value: 'failure' } ] }, { name: 'Model', label: 'Model', isSearchable: false, }, { name: 'Key Alias', label: 'Key Alias', isSearchable: true, searchFn: async (searchText: string) => { if (!accessToken) return []; const keyAliases = await fetchAllKeyAliases(accessToken); const filtered = keyAliases.filter(alias => alias.toLowerCase().includes(searchText.toLowerCase()) ); return filtered.map(alias => ({ label: alias, value: alias })); } }, { name: 'Key Hash', label: 'Key Hash', isSearchable: false, }, ] // When a session is selected, render the SessionView component if (selectedSessionId && sessionLogs.data) { return (
setSelectedSessionId(null)} />
); } return (
setActiveTab(index === 0 ? "request logs" : "audit logs")}> Request Logs Audit Logs

{selectedSessionId ? ( <> Session: {selectedSessionId} ) : ( "Request Logs" )}

{selectedKeyInfo && selectedKeyIdInfoView && selectedKeyInfo.api_key === selectedKeyIdInfoView ? ( setSelectedKeyIdInfoView(null)} premiumUser={premiumUser}/> ) : selectedSessionId ? (
true} // Optionally: add session-specific row expansion state />
) : ( <>
setSearchTerm(e.target.value)} />
{quickSelectOpen && (
{[ { label: "Last 15 Minutes", value: 15, unit: "minutes" }, { label: "Last Hour", value: 1, unit: "hours" }, { label: "Last 4 Hours", value: 4, unit: "hours" }, { label: "Last 24 Hours", value: 24, unit: "hours" }, { label: "Last 7 Days", value: 7, unit: "days" }, ].map((option) => ( ))}
)}
{isCustomDate && (
{ setStartTime(e.target.value); setCurrentPage(1); }} className="px-3 py-2 border rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" />
to
{ setEndTime(e.target.value); setCurrentPage(1); }} className="px-3 py-2 border rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" />
)}
Showing{" "} {logs.isLoading ? "..." : filteredLogs ? (currentPage - 1) * pageSize + 1 : 0}{" "} -{" "} {logs.isLoading ? "..." : filteredLogs ? Math.min(currentPage * pageSize, filteredLogs.total) : 0}{" "} of{" "} {logs.isLoading ? "..." : filteredLogs ? filteredLogs.total : 0}{" "} results
Page {logs.isLoading ? "..." : currentPage} of{" "} {logs.isLoading ? "..." : filteredLogs ? filteredLogs.total_pages : 1}
true} />
)}
); } export function RequestViewer({ row }: { row: Row }) { // Helper function to clean metadata by removing specific fields const formatData = (input: any) => { if (typeof input === "string") { try { return JSON.parse(input); } catch { return input; } } return input; }; // New helper function to get raw request const getRawRequest = () => { // First check if proxy_server_request exists in metadata if (row.original?.proxy_server_request) { return formatData(row.original.proxy_server_request); } // Fall back to messages if proxy_server_request is empty return formatData(row.original.messages); }; // Extract error information from metadata if available const metadata = row.original.metadata || {}; const hasError = metadata.status === "failure"; const errorInfo = hasError ? metadata.error_information : null; // Check if request/response data is missing const hasMessages = row.original.messages && (Array.isArray(row.original.messages) ? row.original.messages.length > 0 : Object.keys(row.original.messages).length > 0); const hasResponse = row.original.response && Object.keys(formatData(row.original.response)).length > 0; const missingData = !hasMessages && !hasResponse; // Format the response with error details if present const formattedResponse = () => { if (hasError && errorInfo) { return { error: { message: errorInfo.error_message || "An error occurred", type: errorInfo.error_class || "error", code: errorInfo.error_code || "unknown", param: null } }; } return formatData(row.original.response); }; // Extract vector store request metadata if available const hasVectorStoreData = metadata.vector_store_request_metadata && Array.isArray(metadata.vector_store_request_metadata) && metadata.vector_store_request_metadata.length > 0; // Extract guardrail information from metadata if available const hasGuardrailData = row.original.metadata && row.original.metadata.guardrail_information; // Calculate total masked entities if guardrail data exists const getTotalMaskedEntities = (): number => { if (!hasGuardrailData || !row.original.metadata?.guardrail_information.masked_entity_count) { return 0; } return Object.values(row.original.metadata.guardrail_information.masked_entity_count) .reduce((sum: number, count: any) => sum + (typeof count === 'number' ? count : 0), 0); }; const totalMaskedEntities = getTotalMaskedEntities(); return (
{/* Combined Info Card */}

Request Details

Request ID: {row.original.request_id}
Model: {row.original.model}
Model ID: {row.original.model_id}
Call Type: {row.original.call_type}
Provider: {row.original.custom_llm_provider || "-"}
API Base: {row.original.api_base || "-"}
{row?.original?.requester_ip_address && (
IP Address: {row?.original?.requester_ip_address}
)} {hasGuardrailData && (
Guardrail:
{row.original.metadata!.guardrail_information.guardrail_name} {totalMaskedEntities > 0 && ( {totalMaskedEntities} masked )}
)}
Tokens: {row.original.total_tokens} ({row.original.prompt_tokens}+{row.original.completion_tokens})
Cost: ${Number(row.original.spend || 0).toFixed(6)}
Cache Hit: {row.original.cache_hit}
Status: {(row.original.metadata?.status || "Success").toLowerCase() !== "failure" ? "Success" : "Failure"}
Start Time: {row.original.startTime}
End Time: {row.original.endTime}
{/* Configuration Info Message - Show when data is missing */} {/* Request/Response Panel */} {/* Guardrail Data - Show only if present */} {hasGuardrailData && ( )} {/* Vector Store Request Data - Show only if present */} {hasVectorStoreData && ( )} {/* Error Card - Only show for failures */} {hasError && errorInfo && } {/* Tags Card - Only show if there are tags */} {row.original.request_tags && Object.keys(row.original.request_tags).length > 0 && (

Request Tags

{Object.entries(row.original.request_tags).map(([key, value]) => ( {key}: {String(value)} ))}
)} {/* Metadata Card - Only show if there's metadata */} {row.original.metadata && Object.keys(row.original.metadata).length > 0 && (

Metadata

              {JSON.stringify(row.original.metadata, null, 2)}
            
)}
); }