From cc1c90c5bd115f90d8c8f10fed18f59f89944041 Mon Sep 17 00:00:00 2001 From: MichaelUnkey <148160799+MichaelUnkey@users.noreply.github.com> Date: Thu, 12 Dec 2024 11:53:30 -0500 Subject: [PATCH 1/3] Changed return to match shape of error. (#2744) --- packages/api/src/client.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/api/src/client.ts b/packages/api/src/client.ts index 01fa6a2177..4b11aef86b 100644 --- a/packages/api/src/client.ts +++ b/packages/api/src/client.ts @@ -195,11 +195,9 @@ export class Unkey { } if (res) { - const { code, message, docs, requestId } = (await res.json()) as ErrorResponse["error"]; - return { - error: { code, message, docs, requestId }, - }; + return (await res.json()) as ErrorResponse; } + return { error: { // @ts-ignore From bd8e2b465bc6f31aa69185a1f41858b0702b98c2 Mon Sep 17 00:00:00 2001 From: MichaelUnkey <148160799+MichaelUnkey@users.noreply.github.com> Date: Thu, 12 Dec 2024 12:30:35 -0500 Subject: [PATCH 2/3] added test to verify error (#2746) --- packages/api/src/client.test.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/packages/api/src/client.test.ts b/packages/api/src/client.test.ts index 2d682f5cda..20ef60f88e 100644 --- a/packages/api/src/client.test.ts +++ b/packages/api/src/client.test.ts @@ -11,4 +11,19 @@ describe("client", () => { }); }).not.toThrow(); }); + + test("errors are correctly passed through to the caller", async () => { + const unkey = new Unkey({ rootKey: "wrong key" }); + const res = await unkey.keys.create({ + apiId: "", + }); + + expect(res.error).toBeDefined(); + expect(res.error!.code).toEqual("UNAUTHORIZED"); + expect(res.error!.docs).toEqual( + "https://unkey.dev/docs/api-reference/errors/code/UNAUTHORIZED", + ); + expect(res.error!.message).toEqual("key not found"); + expect(res.error!.requestId).toBeDefined(); + }); }); From 19548f8d18f489f3ebeff8176cc77b6f63f2ecb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?O=C4=9Fuzhan=20Olguncu?= <21091016+ogzhanolguncu@users.noreply.github.com> Date: Thu, 12 Dec 2024 20:37:50 +0300 Subject: [PATCH 3/3] feat: add more granular fetch options for timeseries (#2736) * feat: add fetch for initial chart data * [autofix.ci] apply automated fixes * refactor: fetch charts data from different rpc * chore: format * chore: run formatter * refactor: turn logs table into generic one * fix: aligment issues of buttons * [autofix.ci] apply automated fixes * chore: run formatter * fix: selected bg style * chore: coderabbit fix * refactor: remove redudant memo * chore: run formatter * chore: fix imports --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- .../app/(app)/@breadcrumb/logs/page.tsx | 3 + .../app/(app)/logs/components/chart.tsx | 142 ----------- .../app/(app)/logs/components/charts/hooks.ts | 61 +++++ .../(app)/logs/components/charts/index.tsx | 131 ++++++++++ .../search-combobox/search-combobox.tsx | 60 ++--- .../(app)/logs/components/filters/index.tsx | 13 +- .../app/(app)/logs/components/logs-table.tsx | 205 --------------- .../app/(app)/logs/components/table/hooks.ts | 91 +++++++ .../log-details/components/log-footer.tsx | 6 +- .../log-details/components/log-header.tsx | 2 +- .../log-details/components/log-meta.tsx | 0 .../log-details/components/log-section.tsx | 0 .../components/request-response-details.tsx | 0 .../{ => table}/log-details/index.tsx | 6 +- .../log-details/resizable-panel.tsx | 2 +- .../{ => table}/logs-table-loading-row.tsx | 0 .../logs/components/table/logs-table.tsx | 92 +++++++ apps/dashboard/app/(app)/logs/logs-page.tsx | 78 +----- apps/dashboard/app/(app)/logs/page.tsx | 62 ++++- apps/dashboard/app/(app)/logs/query-state.ts | 17 +- apps/dashboard/app/(app)/logs/types.ts | 16 -- apps/dashboard/app/(app)/logs/utils.ts | 54 +++- apps/dashboard/components/ui/calendar.tsx | 6 +- apps/dashboard/components/virtual-table.tsx | 240 ++++++++++++++++++ apps/dashboard/lib/trpc/routers/index.ts | 2 + .../lib/trpc/routers/logs/query-log.ts | 3 +- .../lib/trpc/routers/logs/query-timeseries.ts | 51 ++++ apps/dashboard/tailwind.config.js | 5 +- internal/clickhouse/src/logs.ts | 17 ++ 29 files changed, 857 insertions(+), 508 deletions(-) create mode 100644 apps/dashboard/app/(app)/@breadcrumb/logs/page.tsx delete mode 100644 apps/dashboard/app/(app)/logs/components/chart.tsx create mode 100644 apps/dashboard/app/(app)/logs/components/charts/hooks.ts create mode 100644 apps/dashboard/app/(app)/logs/components/charts/index.tsx delete mode 100644 apps/dashboard/app/(app)/logs/components/logs-table.tsx create mode 100644 apps/dashboard/app/(app)/logs/components/table/hooks.ts rename apps/dashboard/app/(app)/logs/components/{ => table}/log-details/components/log-footer.tsx (96%) rename apps/dashboard/app/(app)/logs/components/{ => table}/log-details/components/log-header.tsx (95%) rename apps/dashboard/app/(app)/logs/components/{ => table}/log-details/components/log-meta.tsx (100%) rename apps/dashboard/app/(app)/logs/components/{ => table}/log-details/components/log-section.tsx (100%) rename apps/dashboard/app/(app)/logs/components/{ => table}/log-details/components/request-response-details.tsx (100%) rename apps/dashboard/app/(app)/logs/components/{ => table}/log-details/index.tsx (92%) rename apps/dashboard/app/(app)/logs/components/{ => table}/log-details/resizable-panel.tsx (99%) rename apps/dashboard/app/(app)/logs/components/{ => table}/logs-table-loading-row.tsx (100%) create mode 100644 apps/dashboard/app/(app)/logs/components/table/logs-table.tsx create mode 100644 apps/dashboard/components/virtual-table.tsx create mode 100644 apps/dashboard/lib/trpc/routers/logs/query-timeseries.ts diff --git a/apps/dashboard/app/(app)/@breadcrumb/logs/page.tsx b/apps/dashboard/app/(app)/@breadcrumb/logs/page.tsx new file mode 100644 index 0000000000..382fddc8c0 --- /dev/null +++ b/apps/dashboard/app/(app)/@breadcrumb/logs/page.tsx @@ -0,0 +1,3 @@ +export default function NoBreadcrumb() { + return null; +} diff --git a/apps/dashboard/app/(app)/logs/components/chart.tsx b/apps/dashboard/app/(app)/logs/components/chart.tsx deleted file mode 100644 index 305ec04aca..0000000000 --- a/apps/dashboard/app/(app)/logs/components/chart.tsx +++ /dev/null @@ -1,142 +0,0 @@ -"use client"; - -import { - type ChartConfig, - ChartContainer, - ChartTooltip, - ChartTooltipContent, -} from "@/components/ui/chart"; -import { format } from "date-fns"; -import { Bar, BarChart, CartesianGrid, XAxis } from "recharts"; -import { useLogSearchParams } from "../query-state"; - -export type Log = { - request_id: string; - time: number; - workspace_id: string; - host: string; - method: string; - path: string; - request_headers: string[]; - request_body: string; - response_status: number; - response_headers: string[]; - response_body: string; - error: string; - service_latency: number; -}; - -const chartConfig = { - success: { - label: "Success", - color: "hsl(var(--chart-3))", - }, - warning: { - label: "Warning", - color: "hsl(var(--chart-4))", - }, - error: { - label: "Error", - color: "hsl(var(--chart-1))", - }, -} satisfies ChartConfig; - -export function LogsChart({ logs }: { logs: Log[] }) { - const { searchParams } = useLogSearchParams(); - const data = aggregateData(logs, searchParams.startTime, searchParams.endTime ?? Date.now()); - - return ( - - - { - const date = new Date(value); - return date.toLocaleDateString("en-US", { - month: "short", - day: "2-digit", - hour: "2-digit", - minute: "2-digit", - hour12: true, - }); - }} - /> - - { - return new Date(value).toLocaleDateString("en-US", { - month: "short", - day: "numeric", - year: "numeric", - hour: "2-digit", - minute: "2-digit", - hour12: true, - }); - }} - /> - } - /> - - - - - - ); -} - -function aggregateData(data: Log[], startTime: number, endTime: number) { - const aggregatedData: { - date: string; - success: number; - warning: number; - error: number; - }[] = []; - - const intervalMs = 60 * 1000 * 10; // 10 minutes - - if (data.length === 0) { - return aggregatedData; - } - - const buckets = new Map(); - - // Create a bucket for each 10 minute interval - for (let timestamp = startTime; timestamp < endTime; timestamp += intervalMs) { - buckets.set(timestamp, { - date: format(timestamp, "yyyy-MM-dd'T'HH:mm:ss"), - success: 0, - warning: 0, - error: 0, - }); - } - - // For each log, find its bucket then increment the appropriate counter - for (const log of data) { - const bucketIndex = Math.floor((log.time - startTime) / intervalMs); - const bucket = buckets.get(startTime + bucketIndex * intervalMs); - - if (bucket) { - const status = log.response_status; - if (status >= 200 && status < 300) { - bucket.success++; - } else if (status >= 400 && status < 500) { - bucket.warning++; - } else if (status >= 500) { - bucket.error++; - } - } - } - - return Array.from(buckets.values()); -} diff --git a/apps/dashboard/app/(app)/logs/components/charts/hooks.ts b/apps/dashboard/app/(app)/logs/components/charts/hooks.ts new file mode 100644 index 0000000000..953864d3ac --- /dev/null +++ b/apps/dashboard/app/(app)/logs/components/charts/hooks.ts @@ -0,0 +1,61 @@ +import { trpc } from "@/lib/trpc/client"; +import type { LogsTimeseriesDataPoint } from "@unkey/clickhouse/src/logs"; +import { addMinutes, format } from "date-fns"; +import { useLogSearchParams } from "../../query-state"; +import { type TimeseriesGranularity, getTimeseriesGranularity } from "../../utils"; + +const roundToSecond = (timestamp: number) => Math.floor(timestamp / 1000) * 1000; + +const formatTimestamp = (value: string | number, granularity: TimeseriesGranularity) => { + const date = new Date(value); + const offset = new Date().getTimezoneOffset() * -1; + const localDate = addMinutes(date, offset); + + switch (granularity) { + case "perMinute": + return format(localDate, "HH:mm:ss"); + case "perHour": + return format(localDate, "MMM d, HH:mm"); + case "perDay": + return format(localDate, "MMM d"); + default: + return format(localDate, "Pp"); + } +}; + +export const useFetchTimeseries = (initialTimeseries: LogsTimeseriesDataPoint[]) => { + const { searchParams } = useLogSearchParams(); + + const filters = { + host: searchParams.host, + path: searchParams.path, + method: searchParams.method, + responseStatus: searchParams.responseStatus, + }; + + const { + startTime: rawStartTime, + endTime: rawEndTime, + granularity, + } = getTimeseriesGranularity(searchParams.startTime, searchParams.endTime); + + const { data, isLoading } = trpc.logs.queryTimeseries.useQuery( + { + startTime: roundToSecond(rawStartTime), + endTime: roundToSecond(rawEndTime), + ...filters, + }, + { + refetchInterval: searchParams.endTime ? false : 10_000, + initialData: initialTimeseries, + }, + ); + + const timeseries = data.map((data) => ({ + displayX: formatTimestamp(data.x, granularity), + originalTimestamp: data.x, + ...data.y, + })); + + return { timeseries, isLoading }; +}; diff --git a/apps/dashboard/app/(app)/logs/components/charts/index.tsx b/apps/dashboard/app/(app)/logs/components/charts/index.tsx new file mode 100644 index 0000000000..7c3ae5f8e0 --- /dev/null +++ b/apps/dashboard/app/(app)/logs/components/charts/index.tsx @@ -0,0 +1,131 @@ +"use client"; +import { + type ChartConfig, + ChartContainer, + ChartTooltip, + ChartTooltipContent, +} from "@/components/ui/chart"; +import type { LogsTimeseriesDataPoint } from "@unkey/clickhouse/src/logs"; +import { addMinutes, format } from "date-fns"; +import { useEffect, useState } from "react"; +import { Bar, BarChart, CartesianGrid, ResponsiveContainer, XAxis, YAxis } from "recharts"; +import { useFetchTimeseries } from "./hooks"; + +const chartConfig = { + success: { + label: "Success", + color: "hsl(var(--chart-3))", + }, + warning: { + label: "Warning", + color: "hsl(var(--chart-4))", + }, + error: { + label: "Error", + color: "hsl(var(--chart-1))", + }, +} satisfies ChartConfig; + +const formatTimestampTooltip = (value: string | number) => { + const date = new Date(value); + const offset = new Date().getTimezoneOffset() * -1; + const localDate = addMinutes(date, offset); + return format(localDate, "dd MMM HH:mm:ss"); +}; + +const calculateTickInterval = (dataLength: number, containerWidth: number) => { + const pixelsPerTick = 80; // Adjust this value to control density + const suggestedInterval = Math.ceil((dataLength * pixelsPerTick) / containerWidth); + + const intervals = [1, 2, 5, 10, 15, 30, 60]; + return intervals.find((i) => i >= suggestedInterval) || intervals[intervals.length - 1]; +}; + +export function LogsChart({ + initialTimeseries, +}: { + initialTimeseries: LogsTimeseriesDataPoint[]; +}) { + const { timeseries } = useFetchTimeseries(initialTimeseries); + const [tickInterval, setTickInterval] = useState(5); + const [containerWidth, setContainerWidth] = useState(0); + + useEffect(() => { + if (containerWidth > 0 && timeseries.length > 0) { + const newInterval = calculateTickInterval(timeseries.length, containerWidth); + setTickInterval(newInterval); + } + }, [timeseries.length, containerWidth]); + + const maxValue = Math.max( + ...timeseries.map((item) => (item.success || 0) + (item.warning || 0) + (item.error || 0)), + ); + const yAxisMax = Math.ceil(maxValue * 1.1); + + return ( +
+ setContainerWidth(width)} + > + + + + + { + const originalTimestamp = payload[0]?.payload?.originalTimestamp; + return originalTimestamp ? ( + + {formatTimestampTooltip(originalTimestamp)} + + ) : ( + "" + ); + }} + /> + } + /> + + {["success", "warning", "error"].map((key, index) => ( + + ))} + + + +
+ ); +} diff --git a/apps/dashboard/app/(app)/logs/components/filters/components/search-combobox/search-combobox.tsx b/apps/dashboard/app/(app)/logs/components/filters/components/search-combobox/search-combobox.tsx index b17af52aa1..cdf47951bb 100644 --- a/apps/dashboard/app/(app)/logs/components/filters/components/search-combobox/search-combobox.tsx +++ b/apps/dashboard/app/(app)/logs/components/filters/components/search-combobox/search-combobox.tsx @@ -88,35 +88,37 @@ export function SearchCombobox() { return ( - +
+ +
{/* Forces popover content to strech relative to its parent */} { const { setSearchParams } = useLogSearchParams(); const handleRefresh = () => { - const now = Date.now(); - const startTime = now - ONE_DAY_MS; - const endTime = Date.now(); - setSearchParams({ - endTime: endTime, + endTime: null, host: null, method: null, path: null, requestId: null, responseStatus: [], - startTime: startTime, + startTime: null, }); }; return (
-
- -
+