mirror of
https://github.com/tiennm99/litellm.git
synced 2026-07-10 15:03:36 +00:00
Fix log table bugs (after filtering logic was added) (#10712)
* fix status filter * fix refresh button * minor * fix refresh and auto refresh
This commit is contained in:
@@ -1727,11 +1727,6 @@ async def ui_view_spend_logs( # noqa: PLR0915
|
||||
# Calculate skip value for pagination
|
||||
skip = (page - 1) * page_size
|
||||
|
||||
# Get total count of records
|
||||
total_records = await prisma_client.db.litellm_spendlogs.count(
|
||||
where=where_conditions,
|
||||
)
|
||||
|
||||
# Get paginated data
|
||||
data = await prisma_client.db.litellm_spendlogs.find_many(
|
||||
where=where_conditions,
|
||||
@@ -1742,6 +1737,24 @@ async def ui_view_spend_logs( # noqa: PLR0915
|
||||
take=page_size,
|
||||
)
|
||||
|
||||
if status_filter:
|
||||
status_lower = status_filter.strip().lower()
|
||||
|
||||
def status_filter_fn(row):
|
||||
metadata = getattr(row, "metadata", {}) or {}
|
||||
status_val = metadata.get("status") if isinstance(metadata, dict) else None
|
||||
if status_lower == "failure":
|
||||
return status_val == "failure"
|
||||
elif status_lower == "success":
|
||||
return status_val != "failure"
|
||||
return True
|
||||
|
||||
data = list(filter(status_filter_fn, data))
|
||||
|
||||
# Get total count of records
|
||||
total_records = await prisma_client.db.litellm_spendlogs.count(
|
||||
where=where_conditions,
|
||||
)
|
||||
# Calculate total pages
|
||||
total_pages = (total_records + page_size - 1) // page_size
|
||||
|
||||
|
||||
@@ -88,6 +88,7 @@ export default function SpendLogsTable({
|
||||
pageSize,
|
||||
userID,
|
||||
userRole,
|
||||
autoRefreshInterval: 5000,
|
||||
});
|
||||
|
||||
const sessionLogs = useQuery<{data: LogEntry[]}>({
|
||||
|
||||
@@ -2,9 +2,9 @@ import { useCallback, useEffect, useState, useRef, useMemo } from "react";
|
||||
import { LogEntry } from "./columns";
|
||||
import { uiSpendLogsCall, Organization, Team, UserInfo, teamListCall, userListCall, keyListCall as fetchAllKeysCall, modelAvailableCall } from "../networking";
|
||||
import { KeyResponse } from "../key_team_helpers/key_list";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useQuery, useQueryClient, QueryKey } from "@tanstack/react-query";
|
||||
import { Setter } from "@/types";
|
||||
import { debounce } from "lodash";
|
||||
// import { debounce } from "lodash"; // No longer needed
|
||||
import { defaultPageSize } from "../constants";
|
||||
import moment from "moment";
|
||||
import { fetchAllKeyAliases, fetchAllTeams } from "../key_team_helpers/filter_helpers"; // Import fetchAllTeams
|
||||
@@ -31,6 +31,16 @@ export interface PaginationState {
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
// Define the expected response structure from uiSpendLogsCall for type safety
|
||||
interface SpendLogsResponse {
|
||||
data: LogEntry[];
|
||||
page: number;
|
||||
total_pages: number;
|
||||
total: number;
|
||||
page_size: number;
|
||||
}
|
||||
|
||||
|
||||
export function useLogFilterLogic({
|
||||
accessToken,
|
||||
startTime, // Receive from SpendLogsTable
|
||||
@@ -39,7 +49,8 @@ export function useLogFilterLogic({
|
||||
initialPage = 1,
|
||||
initialFilters = {},
|
||||
userID,
|
||||
userRole
|
||||
userRole,
|
||||
autoRefreshInterval // Added for auto-refresh
|
||||
}: {
|
||||
accessToken: string | null;
|
||||
startTime: string;
|
||||
@@ -49,6 +60,7 @@ export function useLogFilterLogic({
|
||||
initialFilters?: Partial<LogFilterState>;
|
||||
userID: string | null;
|
||||
userRole: string | null;
|
||||
autoRefreshInterval?: number; // Added for auto-refresh
|
||||
}) {
|
||||
const defaultFilters: LogFilterState = {
|
||||
'Team ID': '',
|
||||
@@ -67,38 +79,22 @@ export function useLogFilterLogic({
|
||||
const [currentPage, setCurrentPage] = useState<number>(initialPage);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
// States for manually managed data, loading, and error
|
||||
const [logEntries, setLogEntries] = useState<LogEntry[]>([]);
|
||||
const [paginationDetails, setPaginationDetails] = useState<PaginationState>({
|
||||
currentPage: initialPage,
|
||||
totalPages: 0,
|
||||
totalCount: 0,
|
||||
pageSize
|
||||
});
|
||||
const [isLoadingLogs, setIsLoadingLogs] = useState<boolean>(false);
|
||||
const [logsError, setLogsError] = useState<Error | null>(null);
|
||||
const lastSearchTimestamp = useRef(0);
|
||||
|
||||
const queryAllKeysQuery = useQuery({
|
||||
queryKey: ['allKeysForLogFilters', accessToken], // Ensure unique queryKey
|
||||
const queryAllKeysQuery = useQuery<string[], Error>({
|
||||
queryKey: ['allKeysForLogFilters', accessToken],
|
||||
queryFn: async () => {
|
||||
if (!accessToken) throw new Error('Access token required');
|
||||
return await fetchAllKeyAliases(accessToken);
|
||||
},
|
||||
enabled: !!accessToken
|
||||
});
|
||||
const allKeyAliases = queryAllKeysQuery.data || []
|
||||
const allKeyAliases = queryAllKeysQuery.data || [];
|
||||
|
||||
|
||||
// Fetch all teams and users for potential filter dropdowns (optional, can be adapted)
|
||||
const { data: allTeams } = useQuery<Team[], Error>({
|
||||
queryKey: ["allTeamsForLogFilters", accessToken],
|
||||
queryFn: async () => {
|
||||
if (!accessToken) return [];
|
||||
// Use fetchAllTeams helper function for consistency and abstraction
|
||||
// Assuming fetchAllTeams returns Team[] directly
|
||||
const teamsData = await fetchAllTeams(accessToken);
|
||||
return teamsData || []; // Ensure it returns an array
|
||||
return teamsData || [];
|
||||
},
|
||||
enabled: !!accessToken,
|
||||
});
|
||||
@@ -107,8 +103,7 @@ export function useLogFilterLogic({
|
||||
queryKey: ["allUsersForLogFilters", accessToken],
|
||||
queryFn: async () => {
|
||||
if (!accessToken) return [];
|
||||
// Assuming userListCall fetches all users. Adapt if pagination or specific params are needed.
|
||||
const response = await userListCall(accessToken, null, 1, 1000); // Fetch a large number for dropdown
|
||||
const response = await userListCall(accessToken, null, 1, 100);
|
||||
return response.users || [];
|
||||
},
|
||||
enabled: !!accessToken,
|
||||
@@ -118,113 +113,67 @@ export function useLogFilterLogic({
|
||||
queryKey: ['allModels', accessToken, userID, userRole],
|
||||
queryFn: async () => {
|
||||
if (!accessToken || !userID || !userRole) return [];
|
||||
|
||||
const response = await modelAvailableCall(
|
||||
accessToken,
|
||||
userID,
|
||||
userRole,
|
||||
false, // return_wildcard_routes
|
||||
null // teamID
|
||||
false,
|
||||
null
|
||||
);
|
||||
|
||||
return response.data.map((model: { id: string }) => model.id);
|
||||
},
|
||||
enabled: !!accessToken && !!userID && !!userRole,
|
||||
});
|
||||
|
||||
// Debounced API call
|
||||
const debouncedSearch = useCallback(
|
||||
debounce(async (currentFilters: LogFilterState, pageToFetch: number) => {
|
||||
if (!accessToken) {
|
||||
setLogEntries([]);
|
||||
setPaginationDetails({ currentPage: pageToFetch, totalPages: 0, totalCount: 0, pageSize });
|
||||
setIsLoadingLogs(false);
|
||||
setLogsError(null);
|
||||
return;
|
||||
}
|
||||
const logsQueryKey: QueryKey = ['spendLogs', accessToken, filters, currentPage, startTime, endTime, pageSize, userID, userRole];
|
||||
|
||||
const currentTimestamp = Date.now();
|
||||
lastSearchTimestamp.current = currentTimestamp;
|
||||
setIsLoadingLogs(true);
|
||||
setLogsError(null);
|
||||
const logsQueryFn = async (): Promise<SpendLogsResponse> => {
|
||||
const formattedStartTime = moment(startTime).utc().format("YYYY-MM-DD HH:mm:ss");
|
||||
const formattedEndTime = moment(endTime).utc().format("YYYY-MM-DD HH:mm:ss");
|
||||
|
||||
try {
|
||||
const formattedStartTime = moment(startTime).utc().format("YYYY-MM-DD HH:mm:ss");
|
||||
const formattedEndTime = moment(endTime).utc().format("YYYY-MM-DD HH:mm:ss");
|
||||
const apiKeyParam = filters['Key Hash'] || undefined;
|
||||
const teamIdParam = filters['Team ID'] || undefined;
|
||||
const requestIdParam = filters['Request ID'] || undefined;
|
||||
const userIdParamFilter = filters['User'] || undefined;
|
||||
const statusParam = filters['Status'] || undefined;
|
||||
const modelParam = filters['Model'] || undefined;
|
||||
|
||||
const apiKeyParam = currentFilters['Key Hash'] || undefined;
|
||||
const teamIdParam = currentFilters['Team ID'] || undefined;
|
||||
const requestIdParam = currentFilters['Request ID'] || undefined;
|
||||
const userIdParam = currentFilters['User'] || undefined;
|
||||
const statusParam = currentFilters['Status'] || undefined;
|
||||
const modelParam = currentFilters['Model'] || undefined;
|
||||
|
||||
const response = await uiSpendLogsCall(
|
||||
accessToken,
|
||||
apiKeyParam,
|
||||
teamIdParam,
|
||||
requestIdParam,
|
||||
formattedStartTime,
|
||||
formattedEndTime,
|
||||
pageToFetch,
|
||||
pageSize,
|
||||
userIdParam,
|
||||
statusParam,
|
||||
modelParam,
|
||||
);
|
||||
|
||||
if (currentTimestamp === lastSearchTimestamp.current) {
|
||||
if (response && response.data) {
|
||||
setLogEntries(response.data);
|
||||
setPaginationDetails({
|
||||
currentPage: response.page,
|
||||
totalPages: response.total_pages,
|
||||
totalCount: response.total,
|
||||
pageSize: response.page_size || pageSize,
|
||||
});
|
||||
} else {
|
||||
setLogEntries([]);
|
||||
setPaginationDetails({ currentPage: pageToFetch, totalPages: 0, totalCount: 0, pageSize });
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (currentTimestamp === lastSearchTimestamp.current) {
|
||||
console.error("Error fetching logs:", error);
|
||||
setLogsError(error);
|
||||
setLogEntries([]);
|
||||
setPaginationDetails({ currentPage: pageToFetch, totalPages: 0, totalCount: 0, pageSize });
|
||||
}
|
||||
} finally {
|
||||
if (currentTimestamp === lastSearchTimestamp.current) {
|
||||
setIsLoadingLogs(false);
|
||||
}
|
||||
}
|
||||
}, 300),
|
||||
[accessToken, startTime, endTime, pageSize, setLogEntries, setPaginationDetails, setIsLoadingLogs, setLogsError]
|
||||
);
|
||||
|
||||
// useEffect to trigger debouncedSearch when dependencies change
|
||||
useEffect(() => {
|
||||
if (accessToken) {
|
||||
debouncedSearch(filters, currentPage);
|
||||
} else {
|
||||
// Clear data if accessToken is lost or not present
|
||||
setLogEntries([]);
|
||||
setPaginationDetails(prev => ({ ...prev, currentPage, totalPages: 0, totalCount: 0 }));
|
||||
setIsLoadingLogs(false);
|
||||
setLogsError(null);
|
||||
if (!accessToken) {
|
||||
// This case should ideally be prevented by the `enabled` option
|
||||
// Returning a structure that matches SpendLogsResponse to satisfy Promise type
|
||||
return { data: [], page: 1, total_pages: 0, total: 0, page_size: pageSize };
|
||||
}
|
||||
// Cleanup function for debounce
|
||||
return () => {
|
||||
debouncedSearch.cancel();
|
||||
};
|
||||
}, [accessToken, filters, currentPage, startTime, endTime, pageSize, debouncedSearch]);
|
||||
|
||||
const response = await uiSpendLogsCall(
|
||||
accessToken,
|
||||
apiKeyParam,
|
||||
teamIdParam,
|
||||
requestIdParam,
|
||||
formattedStartTime,
|
||||
formattedEndTime,
|
||||
currentPage,
|
||||
pageSize,
|
||||
userIdParamFilter,
|
||||
statusParam,
|
||||
modelParam,
|
||||
);
|
||||
// Assuming uiSpendLogsCall returns a type compatible with SpendLogsResponse
|
||||
return response as SpendLogsResponse;
|
||||
};
|
||||
|
||||
const logsQuery = useQuery<SpendLogsResponse, Error, SpendLogsResponse, QueryKey>({
|
||||
queryKey: logsQueryKey,
|
||||
queryFn: logsQueryFn,
|
||||
enabled: !!accessToken && !!userID && !!userRole,
|
||||
refetchInterval: autoRefreshInterval && autoRefreshInterval > 0 ? autoRefreshInterval : undefined,
|
||||
refetchIntervalInBackground: true,
|
||||
placeholderData: (previousData) => previousData,
|
||||
notifyOnChangeProps: ['data', 'error'],
|
||||
});
|
||||
|
||||
const handleFilterChange = (newFilters: Partial<LogFilterState>) => {
|
||||
setFilters(prev => {
|
||||
const updatedFilters = { ...prev, ...newFilters }; // Simpler update
|
||||
// Ensure all keys in LogFilterState are present, defaulting to '' if not in newFilters
|
||||
const updatedFilters = { ...prev, ...newFilters };
|
||||
for (const key of Object.keys(defaultFilters) as Array<keyof LogFilterState>) {
|
||||
if (!(key in updatedFilters)) {
|
||||
updatedFilters[key] = defaultFilters[key];
|
||||
@@ -232,44 +181,58 @@ export function useLogFilterLogic({
|
||||
}
|
||||
return updatedFilters as LogFilterState;
|
||||
});
|
||||
setCurrentPage(1); // Reset to first page when filters change
|
||||
// debouncedSearch will be called by the useEffect hook
|
||||
setCurrentPage(1);
|
||||
};
|
||||
|
||||
const handleFilterReset = () => {
|
||||
setFilters(defaultFilters);
|
||||
setCurrentPage(1);
|
||||
// debouncedSearch will be called by the useEffect hook
|
||||
};
|
||||
|
||||
const pagination: PaginationState = useMemo(() => {
|
||||
const data: SpendLogsResponse | undefined = logsQuery.data;
|
||||
return {
|
||||
currentPage: data?.page ?? currentPage,
|
||||
totalPages: data?.total_pages ?? 0,
|
||||
totalCount: data?.total ?? 0,
|
||||
pageSize: data?.page_size ?? pageSize,
|
||||
};
|
||||
}, [logsQuery.data, currentPage, pageSize]);
|
||||
|
||||
const pagination: PaginationState = {
|
||||
currentPage: paginationDetails.currentPage,
|
||||
totalPages: paginationDetails.totalPages,
|
||||
totalCount: paginationDetails.totalCount,
|
||||
pageSize: paginationDetails.pageSize,
|
||||
};
|
||||
const [isManualRefreshing, setIsManualRefreshing] = useState(false);
|
||||
|
||||
const handleRefresh = () => {
|
||||
// Reset to first page
|
||||
setCurrentPage(1);
|
||||
// The useEffect in useLogFilterLogic will automatically trigger a refetch
|
||||
// when currentPage changes
|
||||
setIsManualRefreshing(true); // <-- ADD THIS LINE
|
||||
// First invalidate all the filter queries
|
||||
queryClient.invalidateQueries({ queryKey: ['allKeysForLogFilters'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['allTeamsForLogFilters'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['allUsersForLogFilters'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['allModels'] });
|
||||
|
||||
// Force an immediate refetch of the logs query
|
||||
queryClient.refetchQueries({
|
||||
queryKey: logsQueryKey,
|
||||
exact: true,
|
||||
type: 'active'
|
||||
}).then(() => {
|
||||
setIsManualRefreshing(false); // Reset loading state when refresh completes
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
filters,
|
||||
filteredLogs: logEntries,
|
||||
filteredLogs: logsQuery.data?.data || [],
|
||||
allKeyAliases,
|
||||
allTeams: allTeams || [],
|
||||
allUsers: allUsers || [],
|
||||
allOrganizations: [], // Placeholder for now, can be fetched if needed
|
||||
allOrganizations: [],
|
||||
allModels: allModels || [],
|
||||
handleFilterChange,
|
||||
handleFilterReset,
|
||||
isLoading: isLoadingLogs,
|
||||
isLoading: logsQuery.isFetching || isManualRefreshing,
|
||||
pagination,
|
||||
setCurrentPage,
|
||||
error: logsError,
|
||||
setCurrentPage,
|
||||
error: logsQuery.error,
|
||||
handleRefresh
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user