(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:
Ishaan Jaff 2025-02-10 20:38:55 -08:00 committed by GitHub
parent 64a4229606
commit 00c596a852
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 706 additions and 201 deletions

View file

@ -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;
}
};

View file

@ -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>
);
}

View 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);
});
}
});
};

View 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>
);
}