mirror of
https://github.com/tiennm99/litellm.git
synced 2026-08-21 16:26:51 +00:00
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:
@@ -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
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user