diff --git a/docs/my-website/docs/proxy/config_settings.md b/docs/my-website/docs/proxy/config_settings.md index 03406c9bfa4..0c226ffca64 100644 --- a/docs/my-website/docs/proxy/config_settings.md +++ b/docs/my-website/docs/proxy/config_settings.md @@ -433,6 +433,7 @@ router_settings: | GALILEO_PASSWORD | Password for Galileo authentication | GALILEO_PROJECT_ID | Project ID for Galileo usage | GALILEO_USERNAME | Username for Galileo authentication +| GOOGLE_SECRET_MANAGER_PROJECT_ID | Project ID for Google Secret Manager | GCS_BUCKET_NAME | Name of the Google Cloud Storage bucket | GCS_PATH_SERVICE_ACCOUNT | Path to the Google Cloud service account JSON file | GCS_FLUSH_INTERVAL | Flush interval for GCS logging (in seconds). Specify how often you want a log to be sent to GCS. **Default is 20 seconds** diff --git a/ui/litellm-dashboard/package-lock.json b/ui/litellm-dashboard/package-lock.json index e49ad6bf2d3..88a66c9c585 100644 --- a/ui/litellm-dashboard/package-lock.json +++ b/ui/litellm-dashboard/package-lock.json @@ -22,6 +22,7 @@ "fs": "^0.0.1-security", "jsonwebtoken": "^9.0.2", "jwt-decode": "^4.0.0", + "lucide-react": "^0.513.0", "moment": "^2.30.1", "next": "^14.2.26", "openai": "^4.93.0", @@ -557,7 +558,6 @@ "cpu": [ "ia32" ], - "license": "MIT", "optional": true, "os": [ "win32" @@ -4352,6 +4352,14 @@ "node": "14 || >=16.14" } }, + "node_modules/lucide-react": { + "version": "0.513.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.513.0.tgz", + "integrity": "sha512-CJZKq2g8Y8yN4Aq002GahSXbG2JpFv9kXwyiOAMvUBv7pxeOFHUWKB0mO7MiY4ZVFCV4aNjv2BJFq/z3DgKPQg==", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/mdast-util-from-markdown": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.0.tgz", @@ -7993,21 +8001,6 @@ "type": "github", "url": "https://github.com/sponsors/wooorm" } - }, - "node_modules/@next/swc-win32-ia32-msvc": { - "version": "14.2.26", - "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.26.tgz", - "integrity": "sha512-GQWg/Vbz9zUGi9X80lOeGsz1rMH/MtFO/XqigDznhhhTfDlDoynCM6982mPCbSlxJ/aveZcKtTlwfAjwhyxDpg==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } } } } diff --git a/ui/litellm-dashboard/package.json b/ui/litellm-dashboard/package.json index 356f3bd0e0a..0406d55fda8 100644 --- a/ui/litellm-dashboard/package.json +++ b/ui/litellm-dashboard/package.json @@ -23,6 +23,7 @@ "fs": "^0.0.1-security", "jsonwebtoken": "^9.0.2", "jwt-decode": "^4.0.0", + "lucide-react": "^0.513.0", "moment": "^2.30.1", "next": "^14.2.26", "openai": "^4.93.0", diff --git a/ui/litellm-dashboard/src/app/page.tsx b/ui/litellm-dashboard/src/app/page.tsx index 66f36b0348b..987504538ca 100644 --- a/ui/litellm-dashboard/src/app/page.tsx +++ b/ui/litellm-dashboard/src/app/page.tsx @@ -405,7 +405,8 @@ export default function CreateKeyPage() { userRole={userRole} token={token} accessToken={accessToken} - allTeams={(teams as Team[]) ?? []} + allTeams={teams as Team[] ?? []} + premiumUser={premiumUser} /> ) : page == "mcp-servers" ? ( {userRole && rolesWithWriteAccess.includes(userRole) && (
- + + + + + +
+ + + +
+ {/* Custom Action Filter Dropdown */} +
+ + + {actionFilterOpen && ( +
+
+ {[ + { label: "All Actions", value: "all" }, + { label: "Created", value: "created" }, + { label: "Updated", value: "updated" }, + { label: "Deleted", value: "deleted" }, + { label: "Rotated", value: "rotated" }, + ].map((option) => ( + + ))} +
+
+ )} +
+ + {/* Custom Table Filter Dropdown */} +
+ + + {tableFilterOpen && ( +
+
+ {[ + { label: "All Tables", value: "all" }, + { label: "Keys", value: "keys" }, + { label: "Teams", value: "teams" }, + { label: "Users", value: "users" }, + ].map((option) => ( + + ))} +
+
+ )} +
+ + + Showing{" "} + {allLogsQuery.isLoading + ? "..." + : currentDisplayItemsStart}{" "} + -{" "} + {allLogsQuery.isLoading + ? "..." + : currentDisplayItemsEnd}{" "} + of{" "} + {allLogsQuery.isLoading + ? "..." + : totalFilteredItems}{" "} + results + +
+ + Page {allLogsQuery.isLoading ? "..." : clientCurrentPage} of{" "} + {allLogsQuery.isLoading + ? "..." + : totalFilteredPages} + + + +
+
+ + + true} + /> + + + ); +} diff --git a/ui/litellm-dashboard/src/components/view_logs/columns.tsx b/ui/litellm-dashboard/src/components/view_logs/columns.tsx index 54670bbab5c..95a38853fad 100644 --- a/ui/litellm-dashboard/src/components/view_logs/columns.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/columns.tsx @@ -1,12 +1,10 @@ import type { ColumnDef } from "@tanstack/react-table"; -import { getCountryFromIP } from "./ip_lookup"; -import moment from "moment"; -import React from "react"; -import { CountryCell } from "./country_cell"; +import React, { useState } from "react"; import { getProviderLogoAndName } from "../provider_info_helpers"; import { Tooltip } from "antd"; import { TimeCell } from "./time_cell"; -import { Button } from "@tremor/react"; +import { Button, Badge } from "@tremor/react"; +import { Eye, EyeOff} from "lucide-react" // Helper to get the appropriate logo URL const getLogoUrl = ( @@ -361,3 +359,189 @@ export const RequestResponsePanel = ({ request, response }: { request: any; resp ); }; + +// New component for collapsible JSON display +const CollapsibleJsonCell = ({ jsonData }: { jsonData: any }) => { + const [isExpanded, setIsExpanded] = React.useState(false); + const jsonString = JSON.stringify(jsonData, null, 2); + + if (!jsonData || Object.keys(jsonData).length === 0) { + return -; + } + + return ( +
+ + {isExpanded && ( +
+          {jsonString}
+        
+ )} +
+ ); +}; + +export type AuditLogEntry = { + id: string; + updated_at: string; + changed_by: string; + changed_by_api_key: string; + action: string; + table_name: string; + object_id: string; + before_value: Record; + updated_values: Record; +} + + +const getActionBadge = (action: string) => { + const actionColors = { + created: "blue", + updated: "slate", + deleted: "red", + rotated: "amber", + } as const; + + return ( + + {action} + + + ) +} + +export const auditLogColumns: ColumnDef[] = [ + { + id: "expander", + header: () => null, + cell: ({ row }) => { + const ExpanderCell = () => { + const [localExpanded, setLocalExpanded] = React.useState(row.getIsExpanded()); + + const toggleHandler = React.useCallback(() => { + setLocalExpanded((prev) => !prev); + row.getToggleExpandedHandler()(); + }, [row]); + + return row.getCanExpand() ? ( + + ) : ( + + ); + }; + return ; + }, + }, + { + header: "Created At", + accessorKey: "before_value.created_at", + cell: (info: any) => { + const createdAt = info.row.original.before_value?.created_at; + return createdAt ? : "-"; + }, + }, + { + header: "Updated At", + accessorKey: "updated_at", + cell: (info: any) => , + }, + { + header: "Action", + accessorKey: "action", + cell: (info: any) => {getActionBadge(info.getValue())}, + }, + { + header: "Changed By", + accessorKey: "changed_by", + cell: (info: any) => { + const changedBy = info.row.original.changed_by; + const apiKey = info.row.original.changed_by_api_key; + return ( +
+
{changedBy}
+ {apiKey && ( // Only show API key if it exists + +
{/* Apply max-width and truncate */} + {apiKey} +
+
+ )} +
+ ); + }, + }, + { + header: "Table Name", + accessorKey: "table_name", + cell: (info: any) => { + const tableName = info.getValue(); + let displayValue = tableName; + switch (tableName) { + case "LiteLLM_VerificationToken": + displayValue = "Keys"; + break; + case "LiteLLM_TeamTable": + displayValue = "Teams"; + break; + case "LiteLLM_OrganizationTable": + displayValue = "Organizations"; + break; + case "LiteLLM_UserTable": + displayValue = "Users"; + break; + default: + displayValue = tableName; + } + return {displayValue}; + }, + }, + { + header: "Affected Item ID", + accessorKey: "object_id", + cell: (props) => { + const ObjectIdDisplay = () => { + const objectId = props.getValue(); + const [copied, setCopied] = useState(false); + + if (!objectId) return <>-; + + const handleCopy = async () => { + try { + await navigator.clipboard.writeText(String(objectId)); + setCopied(true); + setTimeout(() => setCopied(false), 1500); + } catch (err) { + console.error("Failed to copy object ID: ", err); + } + }; + + return ( + + + {String(objectId)} + + + ); + }; + return ; + }, + }, +] \ No newline at end of file diff --git a/ui/litellm-dashboard/src/components/view_logs/index.tsx b/ui/litellm-dashboard/src/components/view_logs/index.tsx index b6be331aba5..bb0d2a223a7 100644 --- a/ui/litellm-dashboard/src/components/view_logs/index.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/index.tsx @@ -22,6 +22,16 @@ import FilterComponent from "../common_components/filter"; import { FilterOption } from "../common_components/filter"; import { useLogFilterLogic } from "./log_filter_logic"; import { fetchAllKeyAliases } from "../key_team_helpers/filter_helpers"; +import { + Tab, + TabGroup, + TabList, + TabPanels, + TabPanel, + Text, +} from "@tremor/react"; +import AuditLogs from "./audit_logs"; +import { getTimeRangeDisplay } from "./logs_utils"; interface SpendLogsTableProps { accessToken: string | null; @@ -29,6 +39,7 @@ interface SpendLogsTableProps { userRole: string | null; userID: string | null; allTeams: Team[]; + premiumUser: boolean; } export interface PaginatedResponse { @@ -50,6 +61,7 @@ export default function SpendLogsTable({ userRole, userID, allTeams, + premiumUser, }: SpendLogsTableProps) { const [searchTerm, setSearchTerm] = useState(""); const [showFilters, setShowFilters] = useState(false); @@ -82,6 +94,7 @@ export default function SpendLogsTable({ const [filterByCurrentUser, setFilterByCurrentUser] = useState( userRole && internalUserRoles.includes(userRole) ); + const [activeTab, setActiveTab] = useState("request logs"); const [expandedRequestId, setExpandedRequestId] = useState(null); const [selectedSessionId, setSelectedSessionId] = useState(null); @@ -208,7 +221,7 @@ export default function SpendLogsTable({ return response; }, - enabled: !!accessToken && !!token && !!userRole && !!userID, + enabled: !!accessToken && !!token && !!userRole && !!userID && activeTab === "request logs", refetchInterval: 5000, refetchIntervalInBackground: true, }); @@ -354,25 +367,7 @@ export default function SpendLogsTable({ logs.refetch(); }; - // Add this function to format the time range display - const getTimeRangeDisplay = () => { - if (isCustomDate) { - return `${moment(startTime).format('MMM D, h:mm A')} - ${moment(endTime).format('MMM D, h:mm A')}`; - } - - const now = moment(); - const start = moment(startTime); - const diffMinutes = now.diff(start, 'minutes'); - - if (diffMinutes <= 15) return 'Last 15 Minutes'; - if (diffMinutes <= 60) return 'Last Hour'; - - const diffHours = now.diff(start, 'hours'); - if (diffHours <= 4) return 'Last 4 Hours'; - if (diffHours <= 24) return 'Last 24 Hours'; - if (diffHours <= 168) return 'Last 7 Days'; - return `${start.format('MMM D')} - ${now.format('MMM D')}`; - }; + const handleRowExpand = (requestId: string | null) => { setExpandedRequestId(requestId); @@ -447,252 +442,276 @@ export default function SpendLogsTable({ return (
- -
-

- {selectedSessionId ? ( - <> - Session: {selectedSessionId} - - - ) : ( - "Request Logs" - )} -

-
- {selectedKeyInfo && selectedKeyIdInfoView && selectedKeyInfo.api_key === selectedKeyIdInfoView ? ( - setSelectedKeyIdInfoView(null)} /> - ) : selectedSessionId ? ( -
- true} - // Optionally: add session-specific row expansion state - /> -
- ) : ( - <> - -
-
-
-
-
- setSearchTerm(e.target.value)} - /> - - - -
- -
-
+ setActiveTab(index === 0 ? "request logs" : "audit logs")}> + + + Request Logs + + + Audit Logs + + + + +
+

+ {selectedSessionId ? ( + <> + Session: {selectedSessionId} - - {quickSelectOpen && ( -
-
- {[ - { label: "Last 15 Minutes", value: 15, unit: "minutes" }, - { label: "Last Hour", value: 1, unit: "hours" }, - { label: "Last 4 Hours", value: 4, unit: "hours" }, - { label: "Last 24 Hours", value: 24, unit: "hours" }, - { label: "Last 7 Days", value: 7, unit: "days" }, - ].map((option) => ( - - ))} -
- -
-
- )} -
- - -

- - {isCustomDate && ( -
-
- { - setStartTime(e.target.value); - setCurrentPage(1); - }} - className="px-3 py-2 border rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" - /> -
- to -
- { - setEndTime(e.target.value); - setCurrentPage(1); - }} - className="px-3 py-2 border rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" - /> -
-
+ + ) : ( + "Request Logs" )} -
- -
- - Showing{" "} - {logs.isLoading - ? "..." - : filteredLogs - ? (currentPage - 1) * pageSize + 1 - : 0}{" "} - -{" "} - {logs.isLoading - ? "..." - : filteredLogs - ? Math.min(currentPage * pageSize, filteredLogs.total) - : 0}{" "} - of{" "} - {logs.isLoading - ? "..." - : filteredLogs - ? filteredLogs.total - : 0}{" "} - results - -
- - Page {logs.isLoading ? "..." : currentPage} of{" "} - {logs.isLoading - ? "..." - : filteredLogs - ? filteredLogs.total_pages - : 1} - - - -
-
+
-
- true} - /> -
- - )} + {selectedKeyInfo && selectedKeyIdInfoView && selectedKeyInfo.api_key === selectedKeyIdInfoView ? ( + setSelectedKeyIdInfoView(null)} /> + ) : selectedSessionId ? ( +
+ true} + // Optionally: add session-specific row expansion state + /> +
+ ) : ( + <> + +
+
+
+
+
+ setSearchTerm(e.target.value)} + /> + + + +
+ +
+
+ + + {quickSelectOpen && ( +
+
+ {[ + { label: "Last 15 Minutes", value: 15, unit: "minutes" }, + { label: "Last Hour", value: 1, unit: "hours" }, + { label: "Last 4 Hours", value: 4, unit: "hours" }, + { label: "Last 24 Hours", value: 24, unit: "hours" }, + { label: "Last 7 Days", value: 7, unit: "days" }, + ].map((option) => ( + + ))} +
+ +
+
+ )} +
+ + +
+ + {isCustomDate && ( +
+
+ { + setStartTime(e.target.value); + setCurrentPage(1); + }} + className="px-3 py-2 border rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" + /> +
+ to +
+ { + setEndTime(e.target.value); + setCurrentPage(1); + }} + className="px-3 py-2 border rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" + /> +
+
+ )} +
+ +
+ + Showing{" "} + {logs.isLoading + ? "..." + : filteredLogs + ? (currentPage - 1) * pageSize + 1 + : 0}{" "} + -{" "} + {logs.isLoading + ? "..." + : filteredLogs + ? Math.min(currentPage * pageSize, filteredLogs.total) + : 0}{" "} + of{" "} + {logs.isLoading + ? "..." + : filteredLogs + ? filteredLogs.total + : 0}{" "} + results + +
+ + Page {logs.isLoading ? "..." : currentPage} of{" "} + {logs.isLoading + ? "..." + : filteredLogs + ? filteredLogs.total_pages + : 1} + + + +
+
+
+
+ true} + /> +
+ + )} + + + + + +
); } diff --git a/ui/litellm-dashboard/src/components/view_logs/logs_utils.tsx b/ui/litellm-dashboard/src/components/view_logs/logs_utils.tsx new file mode 100644 index 00000000000..b19a5bbead3 --- /dev/null +++ b/ui/litellm-dashboard/src/components/view_logs/logs_utils.tsx @@ -0,0 +1,21 @@ +import moment from "moment"; + +// Add this function to format the time range display +export const getTimeRangeDisplay = (isCustomDate: boolean, startTime: string, endTime: string) => { + if (isCustomDate) { + return `${moment(startTime).format('MMM D, h:mm A')} - ${moment(endTime).format('MMM D, h:mm A')}`; + } + + const now = moment(); + const start = moment(startTime); + const diffMinutes = now.diff(start, 'minutes'); + + if (diffMinutes <= 15) return 'Last 15 Minutes'; + if (diffMinutes <= 60) return 'Last Hour'; + + const diffHours = now.diff(start, 'hours'); + if (diffHours <= 4) return 'Last 4 Hours'; + if (diffHours <= 24) return 'Last 24 Hours'; + if (diffHours <= 168) return 'Last 7 Days'; + return `${start.format('MMM D')} - ${now.format('MMM D')}`; +}; \ No newline at end of file