From 7aba6e23619a4e1b367a2dc1751ce52e9bb1420e Mon Sep 17 00:00:00 2001 From: Priyanshu-u07 Date: Mon, 26 Oct 2026 18:38:09 +0530 Subject: [PATCH] Show and search request IDs in the Insights logs Signed-off-by: Priyanshu-u07 --- apps/dashboard/src/pages/Insights.test.tsx | 92 +++++++++++++++++++ apps/dashboard/src/pages/Insights.tsx | 67 ++++++++++---- .../dashboard/src/services/insightsService.ts | 2 + 3 files changed, 144 insertions(+), 17 deletions(-) create mode 100644 apps/dashboard/src/pages/Insights.test.tsx diff --git a/apps/dashboard/src/pages/Insights.test.tsx b/apps/dashboard/src/pages/Insights.test.tsx new file mode 100644 index 00000000..26cd1d4c --- /dev/null +++ b/apps/dashboard/src/pages/Insights.test.tsx @@ -0,0 +1,92 @@ +import { render, screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { MemoryRouter } from "react-router-dom"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +vi.mock("@/services/insightsService", () => ({ + insightsService: { + getSummary: vi.fn(), + getTimeseries: vi.fn(), + getLogs: vi.fn(), + getFilters: vi.fn(), + getTopIps: vi.fn(), + getTopModels: vi.fn(), + }, +})); + +// The charts are lazy and need a real layout; they are not under test here. +vi.mock("@/components/insights/InsightsCharts", () => ({ + RequestVolumeChart: () => null, + TokenUsageChart: () => null, + ModelDistributionChart: () => null, + TrafficByIpChart: () => null, +})); + +import { insightsService } from "@/services/insightsService"; +import Insights from "./Insights"; + +const service = insightsService as unknown as Record>; +const REQUEST_ID = "3f9a1c0e-7b2d-4a6f-8e1c-3b5a7d9f0e2c"; + +function log(requestId: string | null) { + return { + id: `log-${requestId}`, deployment_id: "dep-12345678", user_id: "apikey:k1", model: "qwen", + ip_address: "10.0.0.1", request_id: requestId, request_payload: null, latency_ms: 120, + ttft_ms: null, tokens_per_second: null, prompt_tokens: 10, completion_tokens: 5, + total_tokens: 15, status_code: 200, error_message: null, is_streaming: false, + applied_policies: null, created_at: "2026-10-26T10:00:00Z", + }; +} + +function renderPage(url = "/dashboard/insights") { + const client = new QueryClient({ defaultOptions: { queries: { retry: false } } }); + return render( + + + + + , + ); +} + +beforeEach(() => { + globalThis.ResizeObserver ??= class { observe() {} unobserve() {} disconnect() {} } as never; + const totals = { requests: 2, successful_requests: 2, failed_requests: 0, success_rate: 100, + prompt_tokens: 20, completion_tokens: 10, total_tokens: 30 }; + service.getSummary.mockResolvedValue({ totals, latency_ms: { avg: 120, samples: 2 }, + ttft_ms: { avg: 0, samples: 0 }, throughput: { requests_per_minute: 1, tokens_per_second: 1, avg_tokens_per_second: 1 } }); + service.getTimeseries.mockResolvedValue({ granularity: "day", buckets: [] }); + service.getFilters.mockResolvedValue({ deployments: [], models: [], ip_addresses: [], status_options: [], deployment_types: [] }); + service.getTopIps.mockResolvedValue({ items: [] }); + service.getTopModels.mockResolvedValue({ items: [] }); + service.getLogs.mockResolvedValue({ items: [log(REQUEST_ID), log(null)], pagination: { limit: 25, offset: 0, total: 2 } }); +}); + +afterEach(() => vi.clearAllMocks()); + +describe("Insights request ID", () => { + it("shows each log's request ID, shortened, with the full ID on hover", async () => { + renderPage(); + const cell = await screen.findByTitle(REQUEST_ID); + expect(cell).toHaveTextContent("3f9a1c0e-7b2…"); + expect(screen.getByRole("columnheader", { name: "Request ID" })).toBeInTheDocument(); + }); + + it("searches the logs by request ID, and only the logs", async () => { + renderPage(); + await screen.findByTitle(REQUEST_ID); + await userEvent.type(screen.getByLabelText("Find a request by its ID"), "trace-42"); + await waitFor(() => + expect(service.getLogs).toHaveBeenLastCalledWith(expect.objectContaining({ request_id: "trace-42", offset: 0 }))); + for (const call of service.getSummary.mock.calls) expect(call[0]).not.toHaveProperty("request_id"); + }); + + it("opens with the search from the link, and says when nothing matches", async () => { + service.getLogs.mockResolvedValue({ items: [], pagination: { limit: 25, offset: 0, total: 0 } }); + renderPage("/dashboard/insights?requestId=trace-404"); + expect(await screen.findByText("No log with this request ID in the selected time range.")).toBeInTheDocument(); + expect(screen.getByLabelText("Find a request by its ID")).toHaveValue("trace-404"); + expect(service.getLogs).toHaveBeenCalledWith(expect.objectContaining({ request_id: "trace-404" })); + }); +}); diff --git a/apps/dashboard/src/pages/Insights.tsx b/apps/dashboard/src/pages/Insights.tsx index afb064dd..1bed027c 100644 --- a/apps/dashboard/src/pages/Insights.tsx +++ b/apps/dashboard/src/pages/Insights.tsx @@ -72,6 +72,7 @@ type State = { customEnd: string; deploymentId: string; ipAddress: string; + requestId: string; status: InsightsStatus; page: number; pageSize: number; @@ -89,6 +90,7 @@ const createInitialState = (defaultStart: Date, now: Date): State => ({ customEnd: toLocalDateTimeInputValue(now), deploymentId: "", ipAddress: "", + requestId: "", status: "all", page: 1, pageSize: DEFAULT_PAGE_SIZE, @@ -119,6 +121,7 @@ function createInitialStateFromParams(searchParams: URLSearchParams, defaultStar customEnd: searchParams.get("customEnd") || baseState.customEnd, deploymentId: searchParams.get("deploymentId") || "", ipAddress: searchParams.get("ipAddress") || "", + requestId: searchParams.get("requestId") || "", status: status ? (status as InsightsStatus) : baseState.status, page, pageSize: PAGE_SIZE_OPTIONS.includes(parsedPageSize) ? parsedPageSize : DEFAULT_PAGE_SIZE, @@ -132,7 +135,7 @@ function reducer(state: State, action: Action): State { case 'SET_DEPLOYMENT_TYPE': return { ...state, deploymentType: action.payload, deploymentId: "", page: 1 }; case 'CLEAR_FILTERS': - return { ...state, deploymentId: "", ipAddress: "", status: "all", page: 1 }; + return { ...state, deploymentId: "", ipAddress: "", requestId: "", status: "all", page: 1 }; default: return state; } @@ -150,7 +153,7 @@ export default function Insights() { ); const { deploymentType, timePreset, customStart, customEnd, - deploymentId, ipAddress, status, page, pageSize + deploymentId, ipAddress, requestId, status, page, pageSize } = state; useEffect(() => { @@ -170,6 +173,7 @@ export default function Insights() { setOrDelete("customEnd", timePreset === "custom" ? customEnd : null); setOrDelete("deploymentId", deploymentId || null); setOrDelete("ipAddress", ipAddress.trim() || null); + setOrDelete("requestId", requestId.trim() || null); setOrDelete("status", status === "all" ? null : status); setOrDelete("page", page > 1 ? String(page) : null); setOrDelete("pageSize", pageSize !== DEFAULT_PAGE_SIZE ? String(pageSize) : null); @@ -177,9 +181,10 @@ export default function Insights() { if (next.toString() !== searchParams.toString()) { setSearchParams(next, { replace: true }); } - }, [deploymentType, timePreset, customStart, customEnd, deploymentId, ipAddress, status, page, pageSize, searchParams, setSearchParams]); + }, [deploymentType, timePreset, customStart, customEnd, deploymentId, ipAddress, requestId, status, page, pageSize, searchParams, setSearchParams]); const debouncedIpAddress = useDebouncedValue(ipAddress.trim(), IP_DEBOUNCE_DELAY); + const debouncedRequestId = useDebouncedValue(requestId.trim(), IP_DEBOUNCE_DELAY); const range = useMemo(() => { const end = new Date(); @@ -307,14 +312,17 @@ export default function Insights() { baseParams.end_time, deploymentId, debouncedIpAddress, + debouncedRequestId, status, page, pageSize, deploymentType, ], + // Only the logs list can be searched by request ID; the totals and charts cannot. queryFn: () => insightsService.getLogs({ ...baseParams, + request_id: debouncedRequestId || undefined, limit: pageSize, offset: (page - 1) * pageSize, }), @@ -395,7 +403,7 @@ export default function Insights() { logsQuery.isLoading; const hasNoData = (summary?.totals.requests ?? 0) === 0 && !isInitialLoading; - const hasActiveFilters = deploymentId !== "" || ipAddress !== "" || status !== "all"; + const hasActiveFilters = deploymentId !== "" || ipAddress !== "" || requestId !== "" || status !== "all"; const clearFilters = useCallback(() => { dispatch({ type: 'CLEAR_FILTERS' }); @@ -403,9 +411,10 @@ export default function Insights() { const exportLogsToCSV = useCallback(() => { if (!logs?.items.length) return; - const headers = ["Timestamp", "Deployment ID", "Model", "IP Address", "Prompt Tokens", "Completion Tokens", "Total Tokens", "Latency (ms)", "Status"]; + const headers = ["Timestamp", "Request ID", "Deployment ID", "Model", "IP Address", "Prompt Tokens", "Completion Tokens", "Total Tokens", "Latency (ms)", "Status"]; const rows = logs.items.map((log) => [ log.created_at, + log.request_id || "", log.deployment_id, log.model, log.ip_address || "", @@ -836,16 +845,33 @@ export default function Insights() { }

- {logs?.items.length ? ( - - ) : null} +
+ { + dispatch({ type: 'SET_FIELD', field: 'requestId', value: e.target.value }); + dispatch({ type: 'SET_FIELD', field: 'page', value: 1 }); + }} + placeholder="Find request ID" + maxLength={128} + autoComplete="off" + spellCheck={false} + className="h-8 w-56 rounded-md border bg-background px-3 text-xs font-mono" + /> + {logs?.items.length ? ( + + ) : null} +
@@ -853,6 +879,7 @@ export default function Insights() { Timestamp + Request ID Deployment Model IP @@ -867,6 +894,10 @@ export default function Insights() { {new Date(log.created_at).toLocaleString()} + + {!log.request_id ? "-" + : log.request_id.length > 14 ? `${log.request_id.slice(0, 12)}…` : log.request_id} + {String(log.deployment_id).slice(0, 8)}… @@ -899,8 +930,10 @@ export default function Insights() { ))} {!logsQuery.isLoading && (logs?.items.length || 0) === 0 && ( - - No logs found for the selected filters. + + {debouncedRequestId + ? "No log with this request ID in the selected time range." + : "No logs found for the selected filters."} )} diff --git a/apps/dashboard/src/services/insightsService.ts b/apps/dashboard/src/services/insightsService.ts index 0502c262..1fe7537f 100644 --- a/apps/dashboard/src/services/insightsService.ts +++ b/apps/dashboard/src/services/insightsService.ts @@ -66,6 +66,7 @@ export interface InferenceLogItem { user_id: string; model: string; ip_address: string | null; + request_id: string | null; request_payload: Record | null; latency_ms: number | null; ttft_ms: number | null; @@ -130,6 +131,7 @@ export interface InsightsTopModelsResponse { export interface InsightsLogsQueryParams extends InsightsQueryParams { limit?: number; offset?: number; + request_id?: string; } export interface InsightsTimeseriesQueryParams extends InsightsQueryParams {