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 (
-
-
-
+