Filter logs on status and model (#10670)

* added status filtering on logs

* added model filter

* fix linter

* fix model filtering

* modified status filter to use status column from LiteLLM_SpendLogs

* remove json import
This commit is contained in:
tanjiro
2025-05-08 13:50:17 -07:00
committed by GitHub
parent fcd51a8f4b
commit d4f7403cb9
5 changed files with 169 additions and 82 deletions
@@ -1653,6 +1653,14 @@ async def ui_view_spend_logs( # noqa: PLR0915
page_size: int = fastapi.Query(
default=50, description="Number of items per page", ge=1, le=100
),
status_filter: Optional[str] = fastapi.Query(
default=None,
description="Filter logs by status (e.g., success, failure)"
),
model: Optional[str] = fastapi.Query( # Add this new parameter
default=None,
description="Filter logs by model name"
),
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
):
"""
@@ -1684,7 +1692,6 @@ async def ui_view_spend_logs( # noqa: PLR0915
param="None",
code=status.HTTP_400_BAD_REQUEST,
)
try:
# Convert the date strings to datetime objects
start_date_obj = datetime.strptime(start_date, "%Y-%m-%d %H:%M:%S").replace(
@@ -1699,28 +1706,24 @@ async def ui_view_spend_logs( # noqa: PLR0915
end_date_iso = end_date_obj.isoformat() # Already in UTC, no need to add Z
# Build where conditions
where_conditions: dict[str, Any] = {
"startTime": {"gte": start_date_iso, "lte": end_date_iso},
where_conditions: Dict[str, Any] = {
"startTime": {"gte": start_date_iso, "lte": end_date_iso} # Ensure date range is always applied
}
if team_id is not None:
where_conditions["team_id"] = team_id
if api_key is not None:
if api_key:
where_conditions["api_key"] = api_key
if user_id is not None:
where_conditions["user"] = user_id
if request_id is not None:
if user_id:
where_conditions["user"] = user_id
if request_id:
where_conditions["request_id"] = request_id
if team_id:
where_conditions["team_id"] = team_id
if model:
where_conditions["model"] = model
if min_spend is not None:
where_conditions.setdefault("spend", {}).update({"gte": min_spend})
if max_spend is not None:
where_conditions.setdefault("spend", {}).update({"lte": max_spend})
if min_spend is not None or max_spend is not None:
where_conditions["spend"] = {}
if min_spend is not None:
where_conditions["spend"]["gte"] = min_spend
if max_spend is not None:
where_conditions["spend"]["lte"] = max_spend
# Calculate skip value for pagination
skip = (page - 1) * page_size
@@ -8,6 +8,7 @@ export interface FilterOption {
label?: string;
isSearchable?: boolean;
searchFn?: (searchText: string) => Promise<Array<{ label: string; value: string }>>;
options?: Array<{ label: string; value: string }>;
}
interface FilterValues {
@@ -74,12 +75,13 @@ const FilterComponent: React.FC<FilterComponentProps> = ({
// Define the order of filters
const orderedFilters = [
'Team ID',
'Status',
'Model',
'Organization ID',
'Key Alias',
'User ID',
'Key Hash'
];
return (
<div className="w-full">
<div className="flex items-center gap-2 mb-6">
@@ -122,6 +124,20 @@ const FilterComponent: React.FC<FilterComponentProps> = ({
options={searchOptionsMap[option.name] || []}
allowClear
/>
) : option.options ? (
<Select
className="w-full"
placeholder={`Select ${option.label || option.name}...`}
value={tempValues[option.name] || undefined}
onChange={(value) => handleFilterChange(option.name, value)}
allowClear
>
{option.options.map(opt => (
<Select.Option key={opt.value} value={opt.value}>
{opt.label}
</Select.Option>
))}
</Select>
) : (
<Input
className="w-full"
@@ -2147,6 +2147,8 @@ export const uiSpendLogsCall = async (
page?: number,
page_size?: number,
user_id?: string,
status_filter?: string,
model?: string,
) => {
try {
// Construct base URL
@@ -2162,6 +2164,8 @@ export const uiSpendLogsCall = async (
if (page) queryParams.append('page', page.toString());
if (page_size) queryParams.append('page_size', page_size.toString());
if (user_id) queryParams.append('user_id', user_id);
if (status_filter) queryParams.append('status_filter', status_filter);
if (model) queryParams.append('model', model);
// Append query parameters to URL if any exist
const queryString = queryParams.toString();
@@ -74,16 +74,20 @@ export default function SpendLogsTable({
allOrganizations,
allUsers,
allKeyAliases,
allModels,
handleFilterChange,
handleFilterReset,
isLoading,
pagination,
setCurrentPage,
handleRefresh,
} = useLogFilterLogic({
accessToken,
startTime,
endTime,
pageSize,
userID,
userRole,
});
const sessionLogs = useQuery<{data: LogEntry[]}>({
@@ -169,10 +173,6 @@ export default function SpendLogsTable({
onSessionClick: (sessionId: string) => {},
})) || [];
const handleRefresh = () => {
queryClient.invalidateQueries({ queryKey: ['logs', 'table', accessToken, startTime, endTime, filters] });
};
const getTimeRangeDisplay = () => {
if (isCustomDate) {
return `${moment(startTime).format('MMM D, h:mm A')} - ${moment(endTime).format('MMM D, h:mm A')}`;
@@ -204,7 +204,6 @@ export default function SpendLogsTable({
searchFn: async (searchText: string) => {
if (!hookAllTeams || hookAllTeams.length === 0) return [];
const filtered = hookAllTeams.filter((team: Team) =>{
console.log("team", searchText)
return team.team_id.toLowerCase().includes(searchText.toLowerCase()) ||
(team.team_alias && team.team_alias.toLowerCase().includes(searchText.toLowerCase()))
});
@@ -214,55 +213,32 @@ export default function SpendLogsTable({
}));
}
},
// {
// name: 'Key Alias',
// label: 'Key Alias',
// isSearchable: true,
// searchFn: async (searchText: string) => {
// const filteredKeyAliases = allKeyAliases.filter(key => {
// console.log("key", searchText);
// return key.toLowerCase().includes(searchText.toLowerCase())
// });
// return filteredKeyAliases.map((key) => {
// return {
// label: key,
// value: key
// }
// });
// }
// },
// {
// name: 'Key Hash',
// label: 'Key Hash',
// isSearchable: false,
// },
// {
// name: 'Request ID',
// label: 'Request ID',
// isSearchable: false,
// },
// {
// name: 'Model',
// label: 'Model',
// isSearchable: false,
// },
// {
// name: 'User',
// label: 'User',
// isSearchable: true,
// searchFn: async (searchText: string) => {
// if (!allUsers || allUsers.length === 0) return [];
// const filtered = allUsers.filter((user: UserInfo) =>
// (user.user_id && user.user_id.toLowerCase().includes(searchText.toLowerCase())) ||
// (user.user_email && user.user_email.toLowerCase().includes(searchText.toLowerCase()))
// );
// return filtered.map((user: UserInfo) => ({
// label: `${user.user_email || user.user_id} (${user.user_id})`,
// value: user.user_id
// }));
// }
// },
{
name: 'Status',
label: 'Status',
isSearchable: false,
options: [
{ label: 'All', value: '' },
{ label: 'Success', value: 'success' },
{ label: 'Failure', value: 'failure' },
],
},
{
name: 'Model',
label: 'Model',
isSearchable: true,
searchFn: async (searchText: string) => {
// Get unique models from the current logs
const uniqueModels = new Set(filteredLogs.map((log: LogEntry) => log.model));
const filteredModels = Array.from(uniqueModels).filter(model =>
model.toLowerCase().includes(searchText.toLowerCase())
);
return filteredModels.map(model => ({
label: model,
value: model
}));
}
},
];
if (selectedSessionId && sessionLogs.data) {
@@ -501,7 +477,7 @@ export default function SpendLogsTable({
}
return (
<div className="w-full p-6">
<div className="w-full p-6 overflow-hidden">
<div className="flex items-center justify-between mb-4">
<h1 className="text-xl font-semibold">
Request Logs
@@ -625,6 +601,62 @@ export default function SpendLogsTable({
</svg>
<span>Refresh</span>
</button>
<div className="flex items-center space-x-4">
<span className="text-sm text-gray-700">
Showing{" "}
{isLoading
? "..."
: pagination.totalCount > 0
? (pagination.currentPage - 1) * pagination.pageSize + 1
: 0}{" "}
-{" "}
{isLoading
? "..."
: pagination.totalCount > 0
? Math.min(pagination.currentPage * pagination.pageSize, pagination.totalCount)
: 0}{" "}
of{" "}
{isLoading
? "..."
: pagination.totalCount ?? 0}{" "}
results
</span>
<div className="flex items-center space-x-2">
<span className="text-sm text-gray-700">
Page {isLoading ? "..." : pagination.currentPage} of{" "}
{isLoading
? "..."
: pagination.totalPages ?? 1}
</span>
<button
onClick={() =>
setCurrentPage((p) => Math.max(1, p - 1))
}
disabled={isLoading || pagination.currentPage === 1}
className="px-3 py-1 text-sm border rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
Previous
</button>
<button
onClick={() =>
setCurrentPage((p) =>
Math.min(
pagination.totalPages || 1,
p + 1,
),
)
}
disabled={
isLoading ||
pagination.currentPage === (pagination.totalPages || 1)
}
className="px-3 py-1 text-sm border rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
Next
</button>
</div>
</div>
</div>
{isCustomDate && (
@@ -1,6 +1,6 @@
import { useCallback, useEffect, useState, useRef, useMemo } from "react";
import { LogEntry } from "./columns";
import { uiSpendLogsCall, Organization, Team, UserInfo, teamListCall, userListCall, keyListCall as fetchAllKeysCall } from "../networking";
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 { Setter } from "@/types";
@@ -37,7 +37,9 @@ export function useLogFilterLogic({
endTime, // Receive from SpendLogsTable
pageSize = defaultPageSize,
initialPage = 1,
initialFilters = {}
initialFilters = {},
userID,
userRole
}: {
accessToken: string | null;
startTime: string;
@@ -45,6 +47,8 @@ export function useLogFilterLogic({
pageSize?: number;
initialPage?: number;
initialFilters?: Partial<LogFilterState>;
userID: string | null;
userRole: string | null;
}) {
const defaultFilters: LogFilterState = {
'Team ID': '',
@@ -109,6 +113,24 @@ export function useLogFilterLogic({
},
enabled: !!accessToken,
});
const { data: allModels = [] } = useQuery<string[], Error>({
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
);
return response.data.map((model: { id: string }) => model.id);
},
enabled: !!accessToken && !!userID && !!userRole,
});
// Debounced API call
const debouncedSearch = useCallback(
@@ -134,7 +156,8 @@ export function useLogFilterLogic({
const teamIdParam = currentFilters['Team ID'] || undefined;
const requestIdParam = currentFilters['Request ID'] || undefined;
const userIdParam = currentFilters['User'] || undefined;
// const modelParam = currentFilters['Model'] || undefined; // Prepared if uiSpendLogsCall is updated
const statusParam = currentFilters['Status'] || undefined;
const modelParam = currentFilters['Model'] || undefined;
const response = await uiSpendLogsCall(
accessToken,
@@ -145,9 +168,9 @@ export function useLogFilterLogic({
formattedEndTime,
pageToFetch,
pageSize,
userIdParam
// If uiSpendLogsCall is updated to accept more params (e.g., model), pass them here:
// modelParam,
userIdParam,
statusParam,
modelParam,
);
if (currentTimestamp === lastSearchTimestamp.current) {
@@ -226,6 +249,13 @@ export function useLogFilterLogic({
pageSize: paginationDetails.pageSize,
};
const handleRefresh = () => {
// Reset to first page
setCurrentPage(1);
// The useEffect in useLogFilterLogic will automatically trigger a refetch
// when currentPage changes
};
return {
filters,
filteredLogs: logEntries,
@@ -233,11 +263,13 @@ export function useLogFilterLogic({
allTeams: allTeams || [],
allUsers: allUsers || [],
allOrganizations: [], // Placeholder for now, can be fetched if needed
allModels: allModels || [],
handleFilterChange,
handleFilterReset,
isLoading: isLoadingLogs,
pagination,
setCurrentPage,
error: logsError
error: logsError,
handleRefresh
};
}