Skip to content

Commit

Permalink
feat: introduce basic error page (fixes #4)
Browse files Browse the repository at this point in the history
  • Loading branch information
tschoffelen committed Sep 7, 2024
1 parent 0663c6b commit 1727ce4
Show file tree
Hide file tree
Showing 12 changed files with 287 additions and 16 deletions.
3 changes: 2 additions & 1 deletion packages/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
"date-fns": "^3.6.0",
"dynamodb-toolbox": "^1.3.8",
"hono": "^4.5.10",
"p-limit": "^6.1.0"
"p-limit": "^6.1.0",
"slug": "^9.1.0"
},
"devDependencies": {
"esbuild": "^0.20.1",
Expand Down
1 change: 1 addition & 0 deletions packages/api/serverless.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ provider:
runtime: nodejs20.x
timeout: 25
region: eu-west-1
architecture: arm64
deploymentMethod: direct
versionFunctions: false
stage: ${opt:stage, 'dev'}
Expand Down
10 changes: 10 additions & 0 deletions packages/api/src/infrastructure/database/table.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ Resources:
AttributeType: S
- AttributeName: type
AttributeType: S
- AttributeName: lastSeen
AttributeType: S
BillingMode: PAY_PER_REQUEST
KeySchema:
- AttributeName: pk
Expand All @@ -28,3 +30,11 @@ Resources:
KeyType: RANGE
Projection:
ProjectionType: ALL
- IndexName: type-lastSeen
KeySchema:
- AttributeName: type
KeyType: HASH
- AttributeName: lastSeen
KeyType: RANGE
Projection:
ProjectionType: ALL
38 changes: 36 additions & 2 deletions packages/api/src/routes/collector/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { Hono } from "hono";
import slug from "slug";

import { getExpiryTime, put, update } from "../../lib/database";
import { saveHourlyStat } from "../../lib/stats";

Expand All @@ -8,8 +10,6 @@ app.post("/", async (c) => {
const body = await c.req.json();

for (const span of body) {
console.log(span);

if (process.env.TRACER_TOKEN && span.token !== process.env.TRACER_TOKEN) {
console.log(`Invalid token: ${span.token}`);
continue;
Expand Down Expand Up @@ -58,6 +58,40 @@ app.post("/", async (c) => {
true,
);

// save error
if (span.error) {
const errorKey = slug(
`${span.error.type} ${span.error.message}`.trim() || "unknown",
);
await update({
Key: {
pk: `function#${span.region}#${span.name}`,
sk: `error#${errorKey}`,
},
UpdateExpression: `SET #error = :error, #lastInvocation = :lastInvocation, #lastSeen = :lastSeen, #expires = :expires, #type = :type, #name = :name, #region = :region`,
ExpressionAttributeValues: {
":error": span.error,
":lastInvocation": `${span.started}/${span.id}`,
":lastSeen": new Date(span.ended).toISOString(),
":expires": getExpiryTime(),
":type": "error",
":name": span.name,
":region": span.region,
},
ExpressionAttributeNames: {
"#error": "error",
"#lastInvocation": "lastInvocation",
"#lastSeen": "lastSeen",
"#expires": "_expires",
"#type": "type",
"#name": "name",
"#region": "region",
},
});
await saveHourlyStat(span.region, span.name + ".error." + errorKey, 1);
await saveHourlyStat(span.region, "error." + errorKey, 1);
}

// save function meta data
try {
await update({
Expand Down
31 changes: 31 additions & 0 deletions packages/api/src/routes/explore/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,37 @@ app.get("/functions/:region/:name/invocations/:ts/:id", async (c) => {
return c.json(Items?.[0]);
});

app.get("/errors", async (c) => {
const [start, end] = getDates(c);
const startTs = start.toISOString();
const endTs = end.toISOString();

const startKey = c.req.query("startKey");

const { Items, LastEvaluatedKey } = await query({
KeyConditionExpression:
"#type = :type AND #lastSeen BETWEEN :skStart AND :skEnd",
ExclusiveStartKey: startKey ? JSON.parse(startKey) : undefined,
ExpressionAttributeNames: {
"#type": "type",
"#lastSeen": "lastSeen",
},
ExpressionAttributeValues: {
":type": "error",
":skStart": startTs,
":skEnd": endTs,
},
IndexName: "type-lastSeen",
Limit: 50,
ScanIndexForward: false,
});

return c.json({
errors: Items,
nextStartKey: LastEvaluatedKey ? JSON.stringify(LastEvaluatedKey) : false,
});
});

app.get("/transactions/:id", async (c) => {
const items = await queryAll({
KeyConditionExpression: "#pk = :pk",
Expand Down
16 changes: 16 additions & 0 deletions packages/dashboard/src/components/layout/menu-link.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { cn } from "@/lib/utils";
import { NavLink } from "react-router-dom";

export const MenuLink = ({ to, children }) => (
<NavLink
to={to}
className={({ isActive }) =>
cn(
`text-sm font-medium transition-colors hover:text-primary`,
!isActive && "text-muted-foreground",
)
}
>
{children}
</NavLink>
);
7 changes: 5 additions & 2 deletions packages/dashboard/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ import Root from "@/routes/root";
import Functions from "@/routes/functions/page";
import Invocations from "@/routes/invocations/page";
import InvocationDetails from "@/routes/invocation-details/page";
import Errors from "@/routes/errors/page";

import { ThemeProvider } from "@/components/layout/theme-provider";
import { dataLoader } from "@/lib/api";
import { DateRangeProvider } from "@/components/layout/date-picker";

const router = createBrowserRouter([
Expand All @@ -23,7 +23,10 @@ const router = createBrowserRouter([
{
path: "/functions",
element: <Functions />,
loader: dataLoader("functions"),
},
{
path: "/errors",
element: <Errors />,
},
{
path: "/functions/:region/:name/invocations",
Expand Down
97 changes: 97 additions & 0 deletions packages/dashboard/src/routes/errors/columns.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
"use client";

import { MiniFunctionSummary } from "@/components/stats/function-summary";
import { MiniStatsChart } from "@/components/stats/mini-stats-chart";
import { Badge } from "@/components/ui/badge";
import { Tooltipped } from "@/components/ui/tooltipped";
import { ColumnDef, Table } from "@tanstack/react-table";
import { formatRelative } from "date-fns";
import { CheckCircle2, CirclePause, CircleX } from "lucide-react";
import { Link } from "react-router-dom";

export type FunctionItem = {
name: string;
amount: number;
status: "pending" | "processing" | "success" | "failed";
email: string;
};

export const columns: ColumnDef<FunctionItem>[] = [
{
accessorKey: "error",
header: "Error",
cell: ({ row }) => {
const error = row.getValue("error");
return (
<span>
<b>{error.type || "Invocation failed"}</b>
<br />
{error.message.toString().substring(0, 250)}
</span>
);
},
},
{
accessorKey: "name",
header: "Function",
cell: ({ row }) => {
const name = row.getValue("name") as string;
return (
<Link
to={`/functions/${row.original.region}/${name}/invocations`}
className="block text-primary"
>
<span className="font-semibold block">{name}</span>
</Link>
);
},
filterFn: (row, id, value) =>
row.getValue("name")?.toLowerCase().includes(value.toLowerCase()),
},
{
accessorKey: "errors",
header: "Occurrences",
enableSorting: false,
cell: ({ row }) => {
return (
<div className="lg:mr-10">
<MiniStatsChart
title="Occurrences"
color="var(--chart-2)"
region={row.original.region}
name={
row.original.name +
"." +
row.original.sk.replace("error#", "error.")
}
/>
</div>
);
},
},
{
accessorKey: "lastSeen",
header: "Last seen",
cell: ({ row }) => {
const lastSeen = row.getValue("lastSeen");
if (!lastSeen) return <span>-</span>;
return <span>{formatRelative(new Date(lastSeen), new Date())}</span>;
},
},
{
accessorKey: "lastInvocation",
header: "Latest trace",
cell: ({ row }) => {
return (
<Link
to={`/functions/${row.original.region}/${row.original.name}/invocations/${row.original.lastInvocation}`}
className="block text-primary"
>
<span className="text-xs block font-mono">
{row.original.lastInvocation.split("/").pop()}
</span>
</Link>
);
},
},
];
69 changes: 69 additions & 0 deletions packages/dashboard/src/routes/errors/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { DataTable } from "@/components/tables/data-table";
import { StatsChart } from "@/components/stats/stats-chart";

import { columns } from "./columns";
import { useData } from "@/lib/api";
import { DataTableFilter } from "@/components/tables/data-table-filter";
import { useMemo, useState } from "react";
import { Button } from "@/components/ui/button";

const Errors = () => {
const [startKey, setStartKey] = useState("");
const [previousKeys, setPreviousKeys] = useState<string[]>([]);

const {
data: { errors, nextStartKey },
} = useData(`errors?startKey=${encodeURIComponent(startKey)}`, {
suspense: true,
});

const goBack = () => {
setStartKey(previousKeys.pop());
setPreviousKeys(previousKeys);
};
const goNext = () => {
setPreviousKeys([...previousKeys, startKey]);
setStartKey(nextStartKey);
};

return (
<div>
<h1 className="text-2xl font-bold">Errors</h1>
<p className="prose prose-sm mb-4">
Errors are collected from all traced functions.
</p>
<DataTable
id="errors"
defaultSorting={[{ id: "lastSeen", desc: true }]}
pageSize={50}
columns={columns}
data={errors}
/>
<div className="flex items-center justify-between">
<div className="text-xs text-muted-foreground">
Page {previousKeys.length + 1} ({errors.length} items)
</div>
<div className="flex items-center justify-end space-x-2 py-4">
<Button
variant="outline"
size="sm"
onClick={() => goBack()}
disabled={!previousKeys.length}
>
Previous
</Button>
<Button
variant="outline"
size="sm"
onClick={() => goNext()}
disabled={!nextStartKey}
>
Next
</Button>
</div>
</div>
</div>
);
};

export default Errors;
2 changes: 1 addition & 1 deletion packages/dashboard/src/routes/functions/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const Functions = () => {
.map(([tag, value]) => {
return `${tag}: ${value}`;
}),
}));
})) || [];
}, [functions]);

return (
Expand Down
19 changes: 9 additions & 10 deletions packages/dashboard/src/routes/root.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import DatePicker from "@/components/layout/date-picker";
import { MenuLink } from "@/components/layout/menu-link";
import { ModeToggle } from "@/components/layout/mode-toggle";
import { Suspense } from "react";
import { Link, Outlet } from "react-router-dom";
Expand Down Expand Up @@ -33,19 +34,17 @@ export default function Root() {
</svg>
</Link>

<Link
to="/"
className="text-sm font-medium transition-colors hover:text-primary"
<MenuLink
to="/functions"
>
Functions
</Link>
{/* <Link
to="/issues"
className="text-sm font-medium text-muted-foreground transition-colors hover:text-primary"
</MenuLink>
<MenuLink
to="/errors"
>
Issues
</Link>
<Link
Errors
</MenuLink>
{/* <Link
to="/traces"
className="text-sm font-medium text-muted-foreground transition-colors hover:text-primary"
>
Expand Down
Loading

0 comments on commit 1727ce4

Please sign in to comment.