mirror of
https://github.com/BerriAI/litellm.git
synced 2025-04-27 03:34:10 +00:00
(Feat) - Allow viewing Request/Response Logs stored in GCS Bucket (#8449)
* BaseRequestResponseFetchFromCustomLogger * get_active_base_request_response_fetch_from_custom_logger * get_request_response_payload * ui_view_request_response_for_request_id * fix uiSpendLogDetailsCall * fix get_request_response_payload * ui fix RequestViewer * use 1 class AdditionalLoggingUtils * ui_view_request_response_for_request_id * cache the prefetch logs details * refactor prefetch * test view request/resp logs * fix code quality * fix get_request_response_payload * uninstall posthog prevent it from being added in ci/cd * fix posthog * fix traceloop test * fix linting error
This commit is contained in:
parent
64a4229606
commit
00c596a852
13 changed files with 706 additions and 201 deletions
|
@ -3221,3 +3221,41 @@ export const getGuardrailsList = async (accessToken: String) => {
|
|||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
export const uiSpendLogDetailsCall = async (
|
||||
accessToken: string,
|
||||
logId: string,
|
||||
start_date: string
|
||||
) => {
|
||||
try {
|
||||
// Construct base URL
|
||||
let url = proxyBaseUrl
|
||||
? `${proxyBaseUrl}/spend/logs/ui/${logId}?start_date=${encodeURIComponent(start_date)}`
|
||||
: `/spend/logs/ui/${logId}?start_date=${encodeURIComponent(start_date)}`;
|
||||
|
||||
console.log("Fetching log details from:", url);
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.text();
|
||||
handleError(errorData);
|
||||
throw new Error("Network response was not ok");
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log("Fetched log details:", data);
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch log details:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import moment from "moment";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useQuery, useQueries, useQueryClient } from "@tanstack/react-query";
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
|
||||
import { uiSpendLogsCall } from "../networking";
|
||||
import { uiSpendLogsCall, uiSpendLogDetailsCall } from "../networking";
|
||||
import { DataTable } from "./table";
|
||||
import { columns, LogEntry } from "./columns";
|
||||
import { Row } from "@tanstack/react-table";
|
||||
import { RequestViewer } from "./request_viewer";
|
||||
import { prefetchLogDetails } from "./prefetch";
|
||||
|
||||
interface SpendLogsTableProps {
|
||||
accessToken: string | null;
|
||||
|
@ -54,6 +55,8 @@ export default function SpendLogsTable({
|
|||
const [selectedKeyHash, setSelectedKeyHash] = useState("");
|
||||
const [selectedFilter, setSelectedFilter] = useState("Team ID");
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
// Close dropdown when clicking outside
|
||||
useEffect(() => {
|
||||
function handleClickOutside(event: MouseEvent) {
|
||||
|
@ -82,6 +85,7 @@ export default function SpendLogsTable({
|
|||
document.removeEventListener("mousedown", handleClickOutside);
|
||||
}, []);
|
||||
|
||||
// Update the logs query to use the imported prefetchLogDetails
|
||||
const logs = useQuery<PaginatedResponse>({
|
||||
queryKey: [
|
||||
"logs",
|
||||
|
@ -105,13 +109,21 @@ export default function SpendLogsTable({
|
|||
};
|
||||
}
|
||||
|
||||
// Convert times to UTC before formatting
|
||||
console.log("Fetching logs with params:", {
|
||||
startTime,
|
||||
endTime,
|
||||
selectedTeamId,
|
||||
selectedKeyHash,
|
||||
currentPage,
|
||||
pageSize
|
||||
});
|
||||
|
||||
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");
|
||||
|
||||
return await uiSpendLogsCall(
|
||||
const response = await uiSpendLogsCall(
|
||||
accessToken,
|
||||
selectedKeyHash || undefined,
|
||||
selectedTeamId || undefined,
|
||||
|
@ -121,30 +133,62 @@ export default function SpendLogsTable({
|
|||
currentPage,
|
||||
pageSize
|
||||
);
|
||||
|
||||
console.log("Received logs response:", response);
|
||||
|
||||
// Update prefetchLogDetails call with new parameters
|
||||
prefetchLogDetails(response.data, formattedStartTime, accessToken, queryClient);
|
||||
|
||||
return response;
|
||||
},
|
||||
enabled: !!accessToken && !!token && !!userRole && !!userID,
|
||||
refetchInterval: 5000,
|
||||
refetchIntervalInBackground: true,
|
||||
});
|
||||
|
||||
// Move useQueries before the early return
|
||||
const logDetailsQueries = useQueries({
|
||||
queries: logs.data?.data?.map((log) => ({
|
||||
queryKey: ["logDetails", log.request_id, moment(startTime).utc().format("YYYY-MM-DD HH:mm:ss")],
|
||||
queryFn: () => uiSpendLogDetailsCall(accessToken!, log.request_id, moment(startTime).utc().format("YYYY-MM-DD HH:mm:ss")),
|
||||
staleTime: 10 * 60 * 1000,
|
||||
cacheTime: 10 * 60 * 1000,
|
||||
enabled: !!log.request_id,
|
||||
})) || []
|
||||
});
|
||||
|
||||
if (!accessToken || !token || !userRole || !userID) {
|
||||
console.log(
|
||||
"got None values for one of accessToken, token, userRole, userID",
|
||||
);
|
||||
console.log("got None values for one of accessToken, token, userRole, userID");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Consolidate log details from queries
|
||||
const logDetails: Record<string, any> = {};
|
||||
logDetailsQueries.forEach((q, index) => {
|
||||
const log = logs.data?.data[index];
|
||||
if (log && q.data) {
|
||||
logDetails[log.request_id] = q.data;
|
||||
}
|
||||
});
|
||||
|
||||
// Modify the filtered data to include log details
|
||||
const filteredData =
|
||||
logs.data?.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;
|
||||
}) || [];
|
||||
logs.data?.data
|
||||
?.filter((log) => {
|
||||
const matchesSearch =
|
||||
!searchTerm ||
|
||||
log.request_id.includes(searchTerm) ||
|
||||
log.model.includes(searchTerm) ||
|
||||
(log.user && log.user.includes(searchTerm));
|
||||
|
||||
return matchesSearch;
|
||||
})
|
||||
.map(log => ({
|
||||
...log,
|
||||
// Include messages/response from cached details
|
||||
messages: logDetails[log.request_id]?.messages || [],
|
||||
response: logDetails[log.request_id]?.response || {},
|
||||
})) || [];
|
||||
|
||||
// Add this function to handle manual refresh
|
||||
const handleRefresh = () => {
|
||||
|
@ -529,155 +573,3 @@ export default function SpendLogsTable({
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function RequestViewer({ row }: { row: Row<LogEntry> }) {
|
||||
const formatData = (input: any) => {
|
||||
if (typeof input === "string") {
|
||||
try {
|
||||
return JSON.parse(input);
|
||||
} catch {
|
||||
return input;
|
||||
}
|
||||
}
|
||||
return input;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6 bg-gray-50 space-y-6">
|
||||
{/* Combined Info Card */}
|
||||
<div className="bg-white rounded-lg shadow">
|
||||
<div className="p-4 border-b">
|
||||
<h3 className="text-lg font-medium ">Request Details</h3>
|
||||
</div>
|
||||
<div className="space-y-2 p-4 ">
|
||||
<div className="flex">
|
||||
<span className="font-medium w-1/3">Request ID:</span>
|
||||
<span>{row.original.request_id}</span>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<span className="font-medium w-1/3">Api Key:</span>
|
||||
<span>{row.original.api_key}</span>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<span className="font-medium w-1/3">Team ID:</span>
|
||||
<span>{row.original.team_id}</span>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<span className="font-medium w-1/3">Model:</span>
|
||||
<span>{row.original.model}</span>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<span className="font-medium w-1/3">Api Base:</span>
|
||||
<span>{row.original.api_base}</span>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<span className="font-medium w-1/3">Call Type:</span>
|
||||
<span>{row.original.call_type}</span>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<span className="font-medium w-1/3">Spend:</span>
|
||||
<span>{row.original.spend}</span>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<span className="font-medium w-1/3">Total Tokens:</span>
|
||||
<span>{row.original.total_tokens}</span>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<span className="font-medium w-1/3">Prompt Tokens:</span>
|
||||
<span>{row.original.prompt_tokens}</span>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<span className="font-medium w-1/3">Completion Tokens:</span>
|
||||
<span>{row.original.completion_tokens}</span>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<span className="font-medium w-1/3">Start Time:</span>
|
||||
<span>{row.original.startTime}</span>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<span className="font-medium w-1/3">End Time:</span>
|
||||
<span>{row.original.endTime}</span>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<span className="font-medium w-1/3">Cache Hit:</span>
|
||||
<span>{row.original.cache_hit}</span>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<span className="font-medium w-1/3">Cache Key:</span>
|
||||
<span>{row.original.cache_key}</span>
|
||||
</div>
|
||||
{row?.original?.requester_ip_address && (
|
||||
<div className="flex">
|
||||
<span className="font-medium w-1/3">Request IP Address:</span>
|
||||
<span>{row?.original?.requester_ip_address}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Request Card */}
|
||||
<div className="bg-white rounded-lg shadow">
|
||||
<div className="flex justify-between items-center p-4 border-b">
|
||||
<h3 className="text-lg font-medium">Request Tags</h3>
|
||||
</div>
|
||||
<pre className="p-4 text-wrap overflow-auto text-sm">
|
||||
{JSON.stringify(formatData(row.original.request_tags), null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
{/* Request Card */}
|
||||
<div className="bg-white rounded-lg shadow">
|
||||
<div className="flex justify-between items-center p-4 border-b">
|
||||
<h3 className="text-lg font-medium">Request</h3>
|
||||
{/* <div>
|
||||
<button className="mr-2 px-3 py-1 text-sm border rounded hover:bg-gray-50">
|
||||
Expand
|
||||
</button>
|
||||
<button className="px-3 py-1 text-sm border rounded hover:bg-gray-50">
|
||||
JSON
|
||||
</button>
|
||||
</div> */}
|
||||
</div>
|
||||
<pre className="p-4 text-wrap overflow-auto text-sm">
|
||||
{JSON.stringify(formatData(row.original.messages), null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
{/* Response Card */}
|
||||
<div className="bg-white rounded-lg shadow">
|
||||
<div className="flex justify-between items-center p-4 border-b">
|
||||
<h3 className="text-lg font-medium">Response</h3>
|
||||
<div>
|
||||
{/* <button className="mr-2 px-3 py-1 text-sm border rounded hover:bg-gray-50">
|
||||
Expand
|
||||
</button>
|
||||
<button className="px-3 py-1 text-sm border rounded hover:bg-gray-50">
|
||||
JSON
|
||||
</button> */}
|
||||
</div>
|
||||
</div>
|
||||
<pre className="p-4 text-wrap overflow-auto text-sm">
|
||||
{JSON.stringify(formatData(row.original.response), null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
{/* Metadata Card */}
|
||||
{row.original.metadata &&
|
||||
Object.keys(row.original.metadata).length > 0 && (
|
||||
<div className="bg-white rounded-lg shadow">
|
||||
<div className="flex justify-between items-center p-4 border-b">
|
||||
<h3 className="text-lg font-medium">Metadata</h3>
|
||||
{/* <div>
|
||||
<button className="px-3 py-1 text-sm border rounded hover:bg-gray-50">
|
||||
JSON
|
||||
</button>
|
||||
</div> */}
|
||||
</div>
|
||||
<pre className="p-4 text-wrap overflow-auto text-sm ">
|
||||
{JSON.stringify(row.original.metadata, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
23
ui/litellm-dashboard/src/components/view_logs/prefetch.ts
Normal file
23
ui/litellm-dashboard/src/components/view_logs/prefetch.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { QueryClient } from "@tanstack/react-query";
|
||||
import { uiSpendLogDetailsCall } from "../networking";
|
||||
import { LogEntry } from "./columns";
|
||||
|
||||
export const prefetchLogDetails = (
|
||||
logs: LogEntry[],
|
||||
formattedStartTime: string,
|
||||
accessToken: string,
|
||||
queryClient: QueryClient
|
||||
) => {
|
||||
logs.forEach((log) => {
|
||||
if (log.request_id) {
|
||||
queryClient.prefetchQuery({
|
||||
queryKey: ["logDetails", log.request_id, formattedStartTime],
|
||||
queryFn: () => uiSpendLogDetailsCall(accessToken, log.request_id, formattedStartTime),
|
||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||
gcTime: 10 * 60 * 1000, // 10 minutes
|
||||
}).catch((error) => {
|
||||
console.error(`Failed to prefetch details for log: ${log.request_id}`, error);
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
155
ui/litellm-dashboard/src/components/view_logs/request_viewer.tsx
Normal file
155
ui/litellm-dashboard/src/components/view_logs/request_viewer.tsx
Normal file
|
@ -0,0 +1,155 @@
|
|||
import { Row } from "@tanstack/react-table";
|
||||
import { LogEntry } from "./columns";
|
||||
|
||||
export function RequestViewer({ row }: { row: Row<LogEntry> }) {
|
||||
const formatData = (input: any) => {
|
||||
if (typeof input === "string") {
|
||||
try {
|
||||
return JSON.parse(input);
|
||||
} catch {
|
||||
return input;
|
||||
}
|
||||
}
|
||||
return input;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6 bg-gray-50 space-y-6">
|
||||
{/* Combined Info Card */}
|
||||
<div className="bg-white rounded-lg shadow">
|
||||
<div className="p-4 border-b">
|
||||
<h3 className="text-lg font-medium ">Request Details</h3>
|
||||
</div>
|
||||
<div className="space-y-2 p-4 ">
|
||||
<div className="flex">
|
||||
<span className="font-medium w-1/3">Request ID:</span>
|
||||
<span>{row.original.request_id}</span>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<span className="font-medium w-1/3">Api Key:</span>
|
||||
<span>{row.original.api_key}</span>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<span className="font-medium w-1/3">Team ID:</span>
|
||||
<span>{row.original.team_id}</span>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<span className="font-medium w-1/3">Model:</span>
|
||||
<span>{row.original.model}</span>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<span className="font-medium w-1/3">Api Base:</span>
|
||||
<span>{row.original.api_base}</span>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<span className="font-medium w-1/3">Call Type:</span>
|
||||
<span>{row.original.call_type}</span>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<span className="font-medium w-1/3">Spend:</span>
|
||||
<span>{row.original.spend}</span>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<span className="font-medium w-1/3">Total Tokens:</span>
|
||||
<span>{row.original.total_tokens}</span>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<span className="font-medium w-1/3">Prompt Tokens:</span>
|
||||
<span>{row.original.prompt_tokens}</span>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<span className="font-medium w-1/3">Completion Tokens:</span>
|
||||
<span>{row.original.completion_tokens}</span>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<span className="font-medium w-1/3">Start Time:</span>
|
||||
<span>{row.original.startTime}</span>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<span className="font-medium w-1/3">End Time:</span>
|
||||
<span>{row.original.endTime}</span>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<span className="font-medium w-1/3">Cache Hit:</span>
|
||||
<span>{row.original.cache_hit}</span>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<span className="font-medium w-1/3">Cache Key:</span>
|
||||
<span>{row.original.cache_key}</span>
|
||||
</div>
|
||||
{row?.original?.requester_ip_address && (
|
||||
<div className="flex">
|
||||
<span className="font-medium w-1/3">Request IP Address:</span>
|
||||
<span>{row?.original?.requester_ip_address}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Request Card */}
|
||||
<div className="bg-white rounded-lg shadow">
|
||||
<div className="flex justify-between items-center p-4 border-b">
|
||||
<h3 className="text-lg font-medium">Request Tags</h3>
|
||||
</div>
|
||||
<pre className="p-4 text-wrap overflow-auto text-sm">
|
||||
{JSON.stringify(formatData(row.original.request_tags), null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
{/* Request Card */}
|
||||
<div className="bg-white rounded-lg shadow">
|
||||
<div className="flex justify-between items-center p-4 border-b">
|
||||
<h3 className="text-lg font-medium">Request</h3>
|
||||
{/* <div>
|
||||
<button className="mr-2 px-3 py-1 text-sm border rounded hover:bg-gray-50">
|
||||
Expand
|
||||
</button>
|
||||
<button className="px-3 py-1 text-sm border rounded hover:bg-gray-50">
|
||||
JSON
|
||||
</button>
|
||||
</div> */}
|
||||
</div>
|
||||
<pre className="p-4 text-wrap overflow-auto text-sm">
|
||||
{JSON.stringify(formatData(row.original.messages), null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
{/* Response Card */}
|
||||
<div className="bg-white rounded-lg shadow">
|
||||
<div className="flex justify-between items-center p-4 border-b">
|
||||
<h3 className="text-lg font-medium">Response</h3>
|
||||
<div>
|
||||
{/* <button className="mr-2 px-3 py-1 text-sm border rounded hover:bg-gray-50">
|
||||
Expand
|
||||
</button>
|
||||
<button className="px-3 py-1 text-sm border rounded hover:bg-gray-50">
|
||||
JSON
|
||||
</button> */}
|
||||
</div>
|
||||
</div>
|
||||
<pre className="p-4 text-wrap overflow-auto text-sm">
|
||||
{JSON.stringify(formatData(row.original.response), null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
{/* Metadata Card */}
|
||||
{row.original.metadata &&
|
||||
Object.keys(row.original.metadata).length > 0 && (
|
||||
<div className="bg-white rounded-lg shadow">
|
||||
<div className="flex justify-between items-center p-4 border-b">
|
||||
<h3 className="text-lg font-medium">Metadata</h3>
|
||||
{/* <div>
|
||||
<button className="px-3 py-1 text-sm border rounded hover:bg-gray-50">
|
||||
JSON
|
||||
</button>
|
||||
</div> */}
|
||||
</div>
|
||||
<pre className="p-4 text-wrap overflow-auto text-sm ">
|
||||
{JSON.stringify(row.original.metadata, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
Loading…
Add table
Add a link
Reference in a new issue