import { DataTable } from "./table"; import moment from "moment"; import { useRef, useState, useEffect, useCallback, useMemo } from "react"; import { useQuery } from "@tanstack/react-query"; import { uiAuditLogsCall, keyListCall } from "../networking"; import { AuditLogEntry, auditLogColumns } from "./columns"; import { Text } from "@tremor/react"; import { Team } from "../key_team_helpers/key_list"; interface AuditLogsProps { accessToken: string | null; token: string | null; userRole: string | null; userID: string | null; isActive: boolean; premiumUser: boolean; allTeams: Team[]; } export default function AuditLogs({ userID, userRole, token, accessToken, isActive, premiumUser, allTeams, }: AuditLogsProps) { const [startTime, setStartTime] = useState( moment().subtract(24, "hours").format("YYYY-MM-DDTHH:mm") ); const actionFilterRef = useRef(null); const tableFilterRef = useRef(null); const [clientCurrentPage, setClientCurrentPage] = useState(1); const [pageSize] = useState(50); const [filters, setFilters] = useState>({}); const [selectedTeamId, setSelectedTeamId] = useState(""); const [selectedKeyHash, setSelectedKeyHash] = useState(""); const [objectIdSearch, setObjectIdSearch] = useState(""); const [selectedActionFilter, setSelectedActionFilter] = useState("all"); const [selectedTableFilter, setSelectedTableFilter] = useState("all"); const [actionFilterOpen, setActionFilterOpen] = useState(false); const [tableFilterOpen, setTableFilterOpen] = useState(false); const allLogsQuery = useQuery({ queryKey: [ "all_audit_logs", accessToken, token, userRole, userID, startTime, ], queryFn: async () => { if (!accessToken || !token || !userRole || !userID) { return []; } const formattedStartTimeStr = moment(startTime).utc().format("YYYY-MM-DD HH:mm:ss"); const formattedEndTimeStr = moment().utc().format("YYYY-MM-DD HH:mm:ss"); let accumulatedLogs: AuditLogEntry[] = []; let currentPageToFetch = 1; let totalPagesFromBackend = 1; const backendPageSize = 50; do { const response = await uiAuditLogsCall( accessToken, formattedStartTimeStr, formattedEndTimeStr, currentPageToFetch, backendPageSize ); accumulatedLogs = accumulatedLogs.concat(response.audit_logs); totalPagesFromBackend = response.total_pages; currentPageToFetch++; } while (currentPageToFetch <= totalPagesFromBackend); return accumulatedLogs; }, enabled: !!accessToken && !!token && !!userRole && !!userID && isActive, refetchInterval: 5000, refetchIntervalInBackground: true, }); const handleRefresh = () => { allLogsQuery.refetch(); }; const handleFilterChange = (newFilters: Record) => { setFilters(newFilters); }; const handleFilterReset = () => { setFilters({}); setSelectedTeamId(""); setSelectedKeyHash(""); setObjectIdSearch(""); setSelectedActionFilter("all"); setSelectedTableFilter("all"); setClientCurrentPage(1); }; const fetchKeyHashForAlias = useCallback(async (keyAlias: string) => { if (!accessToken) return; try { const response = await keyListCall( accessToken, null, null, keyAlias, null, null, 1, 10 ); const selectedKey = response.keys.find( (key: any) => key.key_alias === keyAlias ); if (selectedKey) { setSelectedKeyHash(selectedKey.token); } else { setSelectedKeyHash(""); } } catch (error) { console.error("Error fetching key hash for alias:", error); setSelectedKeyHash(""); } }, [accessToken]); useEffect(() => { if(!accessToken) return; let teamIdChanged = false; let keyHashChanged = false; if (filters['Team ID']) { if (selectedTeamId !== filters['Team ID']) { setSelectedTeamId(filters['Team ID']); teamIdChanged = true; } } else { if (selectedTeamId !== "") { setSelectedTeamId(""); teamIdChanged = true; } } if (filters['Key Hash']) { if (selectedKeyHash !== filters['Key Hash']) { setSelectedKeyHash(filters['Key Hash']); keyHashChanged = true; } } else if (filters['Key Alias']) { fetchKeyHashForAlias(filters['Key Alias']); } else { if (selectedKeyHash !== "") { setSelectedKeyHash(""); keyHashChanged = true; } } if (teamIdChanged || keyHashChanged) { setClientCurrentPage(1); } }, [filters, accessToken, fetchKeyHashForAlias, selectedTeamId, selectedKeyHash]); useEffect(() => { setClientCurrentPage(1); }, [selectedTeamId, selectedKeyHash, startTime, objectIdSearch, selectedActionFilter, selectedTableFilter]); useEffect(() => { function handleClickOutside(event: MouseEvent) { if ( actionFilterRef.current && !actionFilterRef.current.contains(event.target as Node) ) { setActionFilterOpen(false); } if ( tableFilterRef.current && !tableFilterRef.current.contains(event.target as Node) ) { setTableFilterOpen(false); } } document.addEventListener("mousedown", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside); }, []); const completeFilteredLogs = useMemo(() => { if (!allLogsQuery.data) return []; return allLogsQuery.data.filter(log => { let matchesTeam = true; let matchesKey = true; let matchesObjectId = true; let matchesAction = true; let matchesTable = true; if (selectedTeamId) { const beforeTeamId = typeof log.before_value === 'string' ? JSON.parse(log.before_value)?.team_id : log.before_value?.team_id; const updatedTeamId = typeof log.updated_values === 'string' ? JSON.parse(log.updated_values)?.team_id : log.updated_values?.team_id; matchesTeam = beforeTeamId === selectedTeamId || updatedTeamId === selectedTeamId; } if (selectedKeyHash) { try { const beforeBody = typeof log.before_value === 'string' ? JSON.parse(log.before_value) : log.before_value; const updatedBody = typeof log.updated_values === 'string' ? JSON.parse(log.updated_values) : log.updated_values; const beforeKey = beforeBody?.token; const updatedKey = updatedBody?.token; matchesKey = (typeof beforeKey === 'string' && beforeKey.includes(selectedKeyHash)) || (typeof updatedKey === 'string' && updatedKey.includes(selectedKeyHash)); } catch (e) { matchesKey = false; } } if (objectIdSearch) { matchesObjectId = log.object_id?.toLowerCase().includes(objectIdSearch.toLowerCase()); } if (selectedActionFilter !== "all") { matchesAction = log.action?.toLowerCase() === selectedActionFilter.toLowerCase(); } if (selectedTableFilter !== "all") { let tableMatchName = ""; switch(selectedTableFilter) { case "keys": tableMatchName = "litellm_verificationtoken"; break; case "teams": tableMatchName = "litellm_teamtable"; break; case "users": tableMatchName = "litellm_usertable"; break; // Add other direct table names if needed, or rely on a more generic match default: tableMatchName = selectedTableFilter; // Should not happen with current UI options } matchesTable = log.table_name?.toLowerCase() === tableMatchName; } return matchesTeam && matchesKey && matchesObjectId && matchesAction && matchesTable; }); }, [allLogsQuery.data, selectedTeamId, selectedKeyHash, objectIdSearch, selectedActionFilter, selectedTableFilter]); const totalFilteredItems = completeFilteredLogs.length; const totalFilteredPages = Math.ceil(totalFilteredItems / pageSize) || 1; const paginatedViewOfFilteredLogs = useMemo(() => { const start = (clientCurrentPage - 1) * pageSize; const end = start + pageSize; return completeFilteredLogs.slice(start, end); }, [completeFilteredLogs, clientCurrentPage, pageSize]); // Check if audit logs are empty (not loading and no data) const showAuditLogsInfo = (!allLogsQuery.data || allLogsQuery.data.length === 0); // Custom AuditLogsInfoMessage component const AuditLogsInfoMessage = ({ show }: { show: boolean }) => { if (!show) return null; return (

Audit Logs Not Available

To enable audit logging, add the following configuration to your LiteLLM proxy configuration file:

{`litellm_settings:
  store_audit_logs: true`}
          

Note: This will only affect new requests after the configuration change and proxy restart.

); }; const renderSubComponent = useCallback(({ row }: { row: any }) => { const AuditLogRowExpansionPanel = ({ rowData }: { rowData: AuditLogEntry }) => { const { before_value, updated_values, table_name, action } = rowData; const renderValue = (value: Record, isKeyTable: boolean) => { if (!value || Object.keys(value).length === 0) return N/A; if (isKeyTable) { const changedKeys = Object.keys(value); const knownKeyFields = ['token', 'spend', 'max_budget']; const onlyKnownFieldsChanged = changedKeys.every(key => knownKeyFields.includes(key)); if (onlyKnownFieldsChanged && changedKeys.length > 0) { return (
{changedKeys.includes('token') &&

Token: {value.token || 'N/A'}

} {changedKeys.includes('spend') &&

Spend: {value.spend !== undefined ? `$${Number(value.spend).toFixed(6)}` : 'N/A'}

} {changedKeys.includes('max_budget') &&

Max Budget: {value.max_budget !== undefined ? `$${Number(value.max_budget).toFixed(6)}` : 'N/A'}

}
); } else { if (value["No differing fields detected in 'before' state"] || value["No differing fields detected in 'updated' state"] || value["No fields changed"]) { return {value[Object.keys(value)[0]]} // Display the N/A message string } return
{JSON.stringify(value, null, 2)}
; } } return
{JSON.stringify(value, null, 2)}
; }; let displayBeforeValue = before_value; let displayUpdatedValue = updated_values; if ((action === "updated" || action === "rotated") && before_value && updated_values) { if (table_name === "LiteLLM_TeamTable" || table_name === "LiteLLM_UserTable" || table_name === "LiteLLM_VerificationToken") { const changedBefore: Record = {}; const changedUpdated: Record = {}; const allKeys = new Set([...Object.keys(before_value), ...Object.keys(updated_values)]); allKeys.forEach(key => { const beforeValStr = JSON.stringify(before_value[key]); const updatedValStr = JSON.stringify(updated_values[key]); if (beforeValStr !== updatedValStr) { if (before_value.hasOwnProperty(key)) { changedBefore[key] = before_value[key]; } if (updated_values.hasOwnProperty(key)) { changedUpdated[key] = updated_values[key]; } } }); Object.keys(before_value).forEach(key => { if (!updated_values.hasOwnProperty(key) && !changedBefore.hasOwnProperty(key)) { changedBefore[key] = before_value[key]; changedUpdated[key] = undefined; } }); Object.keys(updated_values).forEach(key => { if (!before_value.hasOwnProperty(key) && !changedUpdated.hasOwnProperty(key)) { changedUpdated[key] = updated_values[key]; changedBefore[key] = undefined; } }); displayBeforeValue = Object.keys(changedBefore).length > 0 ? changedBefore : { "No differing fields detected in 'before' state": "N/A" }; displayUpdatedValue = Object.keys(changedUpdated).length > 0 ? changedUpdated : { "No differing fields detected in 'updated' state": "N/A" }; if (Object.keys(changedBefore).length === 0 && Object.keys(changedUpdated).length === 0) { displayBeforeValue = {"No fields changed": "N/A"}; displayUpdatedValue = {"No fields changed": "N/A"}; } } } return (

Before Value:

{renderValue(displayBeforeValue, table_name === "LiteLLM_VerificationToken")}

Updated Value:

{renderValue(displayUpdatedValue, table_name === "LiteLLM_VerificationToken")}
); }; return ; }, []); if (!premiumUser) { return (
This is a LiteLLM Enterprise feature, and requires a valid key to use. Get a trial key here.
); } const currentDisplayItemsStart = totalFilteredItems > 0 ? (clientCurrentPage - 1) * pageSize + 1 : 0; const currentDisplayItemsEnd = Math.min(clientCurrentPage * pageSize, totalFilteredItems); return ( <>
{/* */}

Audit Logs

{/* Show Audit Logs Info Message when no data */}
setObjectIdSearch(e.target.value)} className="px-3 py-2 border rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" />
{/* Custom Action Filter Dropdown */}
{actionFilterOpen && (
{[ { label: "All Actions", value: "all" }, { label: "Created", value: "created" }, { label: "Updated", value: "updated" }, { label: "Deleted", value: "deleted" }, { label: "Rotated", value: "rotated" }, ].map((option) => ( ))}
)}
{/* Custom Table Filter Dropdown */}
{tableFilterOpen && (
{[ { label: "All Tables", value: "all" }, { label: "Keys", value: "keys" }, { label: "Teams", value: "teams" }, { label: "Users", value: "users" }, ].map((option) => ( ))}
)}
Showing{" "} {allLogsQuery.isLoading ? "..." : currentDisplayItemsStart}{" "} -{" "} {allLogsQuery.isLoading ? "..." : currentDisplayItemsEnd}{" "} of{" "} {allLogsQuery.isLoading ? "..." : totalFilteredItems}{" "} results
Page {allLogsQuery.isLoading ? "..." : clientCurrentPage} of{" "} {allLogsQuery.isLoading ? "..." : totalFilteredPages}
true} />
); }