@townco/debugger 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -0
- package/package.json +38 -0
- package/src/App.tsx +103 -0
- package/src/components/AttributeViewer.tsx +26 -0
- package/src/components/LogList.tsx +72 -0
- package/src/components/SessionTraceList.tsx +116 -0
- package/src/components/SpanTree.tsx +205 -0
- package/src/components/TraceDetailContent.tsx +131 -0
- package/src/components/ui/button.tsx +60 -0
- package/src/components/ui/card.tsx +92 -0
- package/src/components/ui/input.tsx +21 -0
- package/src/components/ui/label.tsx +24 -0
- package/src/components/ui/select.tsx +187 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/db.ts +60 -0
- package/src/env.d.ts +9 -0
- package/src/frontend.tsx +26 -0
- package/src/index.css +11 -0
- package/src/index.html +13 -0
- package/src/index.ts +20 -0
- package/src/lib/utils.ts +6 -0
- package/src/logo.svg +1 -0
- package/src/pages/SessionView.tsx +88 -0
- package/src/pages/TraceDetail.tsx +19 -0
- package/src/pages/TraceList.tsx +153 -0
- package/src/server.ts +82 -0
- package/src/types.ts +48 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
Card,
|
|
4
|
+
CardContent,
|
|
5
|
+
CardDescription,
|
|
6
|
+
CardHeader,
|
|
7
|
+
CardTitle,
|
|
8
|
+
} from "@/components/ui/card";
|
|
9
|
+
import type { TraceDetail as TraceDetailType } from "../types";
|
|
10
|
+
import { LogList } from "./LogList";
|
|
11
|
+
import { SpanTree } from "./SpanTree";
|
|
12
|
+
|
|
13
|
+
function formatDuration(startNano: number, endNano: number): string {
|
|
14
|
+
const ms = (endNano - startNano) / 1_000_000;
|
|
15
|
+
if (ms < 1000) return `${ms.toFixed(2)}ms`;
|
|
16
|
+
return `${(ms / 1000).toFixed(2)}s`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function formatTimestamp(nanoseconds: number): string {
|
|
20
|
+
return new Date(nanoseconds / 1_000_000).toLocaleString();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface TraceDetailContentProps {
|
|
24
|
+
traceId: string;
|
|
25
|
+
compact?: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function TraceDetailContent({
|
|
29
|
+
traceId,
|
|
30
|
+
compact = false,
|
|
31
|
+
}: TraceDetailContentProps) {
|
|
32
|
+
const [data, setData] = useState<TraceDetailType | null>(null);
|
|
33
|
+
const [loading, setLoading] = useState(true);
|
|
34
|
+
const [error, setError] = useState<string | null>(null);
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
setLoading(true);
|
|
38
|
+
setError(null);
|
|
39
|
+
fetch(`/api/traces/${traceId}`)
|
|
40
|
+
.then((res) => {
|
|
41
|
+
if (!res.ok) {
|
|
42
|
+
if (res.status === 404) throw new Error("Trace not found");
|
|
43
|
+
throw new Error("Failed to fetch trace");
|
|
44
|
+
}
|
|
45
|
+
return res.json();
|
|
46
|
+
})
|
|
47
|
+
.then((data) => {
|
|
48
|
+
setData(data);
|
|
49
|
+
setLoading(false);
|
|
50
|
+
})
|
|
51
|
+
.catch((err) => {
|
|
52
|
+
setError(err.message);
|
|
53
|
+
setLoading(false);
|
|
54
|
+
});
|
|
55
|
+
}, [traceId]);
|
|
56
|
+
|
|
57
|
+
if (loading) {
|
|
58
|
+
return (
|
|
59
|
+
<div className={compact ? "p-4" : "p-8"}>
|
|
60
|
+
<div className="text-muted-foreground">Loading trace...</div>
|
|
61
|
+
</div>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (error || !data?.trace) {
|
|
66
|
+
return (
|
|
67
|
+
<div className={compact ? "p-4" : "p-8"}>
|
|
68
|
+
<div className="text-red-500">Error: {error || "Trace not found"}</div>
|
|
69
|
+
</div>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const { trace, spans, logs } = data;
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<div className={compact ? "p-4" : "p-8"}>
|
|
77
|
+
<Card className="mb-6">
|
|
78
|
+
<CardHeader>
|
|
79
|
+
<CardTitle>{trace.first_span_name || "Unknown"}</CardTitle>
|
|
80
|
+
<CardDescription className="space-y-1">
|
|
81
|
+
<div>{trace.service_name || "Unknown service"}</div>
|
|
82
|
+
<div className="font-mono text-xs">{trace.trace_id}</div>
|
|
83
|
+
</CardDescription>
|
|
84
|
+
</CardHeader>
|
|
85
|
+
<CardContent>
|
|
86
|
+
<div className="flex gap-6 text-sm">
|
|
87
|
+
<div>
|
|
88
|
+
<span className="text-muted-foreground">Duration: </span>
|
|
89
|
+
{formatDuration(
|
|
90
|
+
trace.start_time_unix_nano,
|
|
91
|
+
trace.end_time_unix_nano,
|
|
92
|
+
)}
|
|
93
|
+
</div>
|
|
94
|
+
<div>
|
|
95
|
+
<span className="text-muted-foreground">Spans: </span>
|
|
96
|
+
{spans.length}
|
|
97
|
+
</div>
|
|
98
|
+
<div>
|
|
99
|
+
<span className="text-muted-foreground">Logs: </span>
|
|
100
|
+
{logs.length}
|
|
101
|
+
</div>
|
|
102
|
+
<div>
|
|
103
|
+
<span className="text-muted-foreground">Started: </span>
|
|
104
|
+
{formatTimestamp(trace.start_time_unix_nano)}
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
</CardContent>
|
|
108
|
+
</Card>
|
|
109
|
+
|
|
110
|
+
<div className="grid gap-6 lg:grid-cols-2">
|
|
111
|
+
<Card>
|
|
112
|
+
<CardHeader>
|
|
113
|
+
<CardTitle className="text-lg">Spans ({spans.length})</CardTitle>
|
|
114
|
+
</CardHeader>
|
|
115
|
+
<CardContent>
|
|
116
|
+
<SpanTree spans={spans} />
|
|
117
|
+
</CardContent>
|
|
118
|
+
</Card>
|
|
119
|
+
|
|
120
|
+
<Card>
|
|
121
|
+
<CardHeader>
|
|
122
|
+
<CardTitle className="text-lg">Logs ({logs.length})</CardTitle>
|
|
123
|
+
</CardHeader>
|
|
124
|
+
<CardContent>
|
|
125
|
+
<LogList logs={logs} />
|
|
126
|
+
</CardContent>
|
|
127
|
+
</Card>
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { Slot } from "@radix-ui/react-slot";
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
|
|
5
|
+
import { cn } from "@/lib/utils";
|
|
6
|
+
|
|
7
|
+
const buttonVariants = cva(
|
|
8
|
+
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
|
9
|
+
{
|
|
10
|
+
variants: {
|
|
11
|
+
variant: {
|
|
12
|
+
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
|
13
|
+
destructive:
|
|
14
|
+
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
|
15
|
+
outline:
|
|
16
|
+
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
|
17
|
+
secondary:
|
|
18
|
+
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
|
19
|
+
ghost:
|
|
20
|
+
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
|
21
|
+
link: "text-primary underline-offset-4 hover:underline",
|
|
22
|
+
},
|
|
23
|
+
size: {
|
|
24
|
+
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
|
25
|
+
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
|
26
|
+
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
|
27
|
+
icon: "size-9",
|
|
28
|
+
"icon-sm": "size-8",
|
|
29
|
+
"icon-lg": "size-10",
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
defaultVariants: {
|
|
33
|
+
variant: "default",
|
|
34
|
+
size: "default",
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
function Button({
|
|
40
|
+
className,
|
|
41
|
+
variant,
|
|
42
|
+
size,
|
|
43
|
+
asChild = false,
|
|
44
|
+
...props
|
|
45
|
+
}: React.ComponentProps<"button"> &
|
|
46
|
+
VariantProps<typeof buttonVariants> & {
|
|
47
|
+
asChild?: boolean;
|
|
48
|
+
}) {
|
|
49
|
+
const Comp = asChild ? Slot : "button";
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<Comp
|
|
53
|
+
data-slot="button"
|
|
54
|
+
className={cn(buttonVariants({ variant, size, className }))}
|
|
55
|
+
{...props}
|
|
56
|
+
/>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export { Button, buttonVariants };
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
import { cn } from "@/lib/utils";
|
|
4
|
+
|
|
5
|
+
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
|
6
|
+
return (
|
|
7
|
+
<div
|
|
8
|
+
data-slot="card"
|
|
9
|
+
className={cn(
|
|
10
|
+
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
|
11
|
+
className,
|
|
12
|
+
)}
|
|
13
|
+
{...props}
|
|
14
|
+
/>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|
19
|
+
return (
|
|
20
|
+
<div
|
|
21
|
+
data-slot="card-header"
|
|
22
|
+
className={cn(
|
|
23
|
+
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
|
24
|
+
className,
|
|
25
|
+
)}
|
|
26
|
+
{...props}
|
|
27
|
+
/>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
|
32
|
+
return (
|
|
33
|
+
<div
|
|
34
|
+
data-slot="card-title"
|
|
35
|
+
className={cn("leading-none font-semibold", className)}
|
|
36
|
+
{...props}
|
|
37
|
+
/>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
|
42
|
+
return (
|
|
43
|
+
<div
|
|
44
|
+
data-slot="card-description"
|
|
45
|
+
className={cn("text-muted-foreground text-sm", className)}
|
|
46
|
+
{...props}
|
|
47
|
+
/>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
|
52
|
+
return (
|
|
53
|
+
<div
|
|
54
|
+
data-slot="card-action"
|
|
55
|
+
className={cn(
|
|
56
|
+
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
|
57
|
+
className,
|
|
58
|
+
)}
|
|
59
|
+
{...props}
|
|
60
|
+
/>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
|
65
|
+
return (
|
|
66
|
+
<div
|
|
67
|
+
data-slot="card-content"
|
|
68
|
+
className={cn("px-6", className)}
|
|
69
|
+
{...props}
|
|
70
|
+
/>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|
75
|
+
return (
|
|
76
|
+
<div
|
|
77
|
+
data-slot="card-footer"
|
|
78
|
+
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
|
|
79
|
+
{...props}
|
|
80
|
+
/>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export {
|
|
85
|
+
Card,
|
|
86
|
+
CardAction,
|
|
87
|
+
CardContent,
|
|
88
|
+
CardDescription,
|
|
89
|
+
CardFooter,
|
|
90
|
+
CardHeader,
|
|
91
|
+
CardTitle,
|
|
92
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
import { cn } from "@/lib/utils";
|
|
4
|
+
|
|
5
|
+
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
|
6
|
+
return (
|
|
7
|
+
<input
|
|
8
|
+
type={type}
|
|
9
|
+
data-slot="input"
|
|
10
|
+
className={cn(
|
|
11
|
+
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
|
12
|
+
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
|
13
|
+
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
|
14
|
+
className,
|
|
15
|
+
)}
|
|
16
|
+
{...props}
|
|
17
|
+
/>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export { Input };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as LabelPrimitive from "@radix-ui/react-label";
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
|
|
6
|
+
import { cn } from "@/lib/utils";
|
|
7
|
+
|
|
8
|
+
function Label({
|
|
9
|
+
className,
|
|
10
|
+
...props
|
|
11
|
+
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
|
12
|
+
return (
|
|
13
|
+
<LabelPrimitive.Root
|
|
14
|
+
data-slot="label"
|
|
15
|
+
className={cn(
|
|
16
|
+
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
|
17
|
+
className,
|
|
18
|
+
)}
|
|
19
|
+
{...props}
|
|
20
|
+
/>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export { Label };
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as SelectPrimitive from "@radix-ui/react-select";
|
|
4
|
+
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react";
|
|
5
|
+
import * as React from "react";
|
|
6
|
+
|
|
7
|
+
import { cn } from "@/lib/utils";
|
|
8
|
+
|
|
9
|
+
function Select({
|
|
10
|
+
...props
|
|
11
|
+
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
|
|
12
|
+
return <SelectPrimitive.Root data-slot="select" {...props} />;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function SelectGroup({
|
|
16
|
+
...props
|
|
17
|
+
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
|
|
18
|
+
return <SelectPrimitive.Group data-slot="select-group" {...props} />;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function SelectValue({
|
|
22
|
+
...props
|
|
23
|
+
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
|
|
24
|
+
return <SelectPrimitive.Value data-slot="select-value" {...props} />;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function SelectTrigger({
|
|
28
|
+
className,
|
|
29
|
+
size = "default",
|
|
30
|
+
children,
|
|
31
|
+
...props
|
|
32
|
+
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
|
|
33
|
+
size?: "sm" | "default";
|
|
34
|
+
}) {
|
|
35
|
+
return (
|
|
36
|
+
<SelectPrimitive.Trigger
|
|
37
|
+
data-slot="select-trigger"
|
|
38
|
+
data-size={size}
|
|
39
|
+
className={cn(
|
|
40
|
+
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
41
|
+
className,
|
|
42
|
+
)}
|
|
43
|
+
{...props}
|
|
44
|
+
>
|
|
45
|
+
{children}
|
|
46
|
+
<SelectPrimitive.Icon asChild>
|
|
47
|
+
<ChevronDownIcon className="size-4 opacity-50" />
|
|
48
|
+
</SelectPrimitive.Icon>
|
|
49
|
+
</SelectPrimitive.Trigger>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function SelectContent({
|
|
54
|
+
className,
|
|
55
|
+
children,
|
|
56
|
+
position = "popper",
|
|
57
|
+
align = "center",
|
|
58
|
+
...props
|
|
59
|
+
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
|
|
60
|
+
return (
|
|
61
|
+
<SelectPrimitive.Portal>
|
|
62
|
+
<SelectPrimitive.Content
|
|
63
|
+
data-slot="select-content"
|
|
64
|
+
className={cn(
|
|
65
|
+
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
|
|
66
|
+
position === "popper" &&
|
|
67
|
+
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
|
68
|
+
className,
|
|
69
|
+
)}
|
|
70
|
+
position={position}
|
|
71
|
+
align={align}
|
|
72
|
+
{...props}
|
|
73
|
+
>
|
|
74
|
+
<SelectScrollUpButton />
|
|
75
|
+
<SelectPrimitive.Viewport
|
|
76
|
+
className={cn(
|
|
77
|
+
"p-1",
|
|
78
|
+
position === "popper" &&
|
|
79
|
+
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1",
|
|
80
|
+
)}
|
|
81
|
+
>
|
|
82
|
+
{children}
|
|
83
|
+
</SelectPrimitive.Viewport>
|
|
84
|
+
<SelectScrollDownButton />
|
|
85
|
+
</SelectPrimitive.Content>
|
|
86
|
+
</SelectPrimitive.Portal>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function SelectLabel({
|
|
91
|
+
className,
|
|
92
|
+
...props
|
|
93
|
+
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
|
|
94
|
+
return (
|
|
95
|
+
<SelectPrimitive.Label
|
|
96
|
+
data-slot="select-label"
|
|
97
|
+
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
|
|
98
|
+
{...props}
|
|
99
|
+
/>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function SelectItem({
|
|
104
|
+
className,
|
|
105
|
+
children,
|
|
106
|
+
...props
|
|
107
|
+
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
|
|
108
|
+
return (
|
|
109
|
+
<SelectPrimitive.Item
|
|
110
|
+
data-slot="select-item"
|
|
111
|
+
className={cn(
|
|
112
|
+
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
|
113
|
+
className,
|
|
114
|
+
)}
|
|
115
|
+
{...props}
|
|
116
|
+
>
|
|
117
|
+
<span className="absolute right-2 flex size-3.5 items-center justify-center">
|
|
118
|
+
<SelectPrimitive.ItemIndicator>
|
|
119
|
+
<CheckIcon className="size-4" />
|
|
120
|
+
</SelectPrimitive.ItemIndicator>
|
|
121
|
+
</span>
|
|
122
|
+
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
|
123
|
+
</SelectPrimitive.Item>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function SelectSeparator({
|
|
128
|
+
className,
|
|
129
|
+
...props
|
|
130
|
+
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
|
|
131
|
+
return (
|
|
132
|
+
<SelectPrimitive.Separator
|
|
133
|
+
data-slot="select-separator"
|
|
134
|
+
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
|
|
135
|
+
{...props}
|
|
136
|
+
/>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function SelectScrollUpButton({
|
|
141
|
+
className,
|
|
142
|
+
...props
|
|
143
|
+
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
|
|
144
|
+
return (
|
|
145
|
+
<SelectPrimitive.ScrollUpButton
|
|
146
|
+
data-slot="select-scroll-up-button"
|
|
147
|
+
className={cn(
|
|
148
|
+
"flex cursor-default items-center justify-center py-1",
|
|
149
|
+
className,
|
|
150
|
+
)}
|
|
151
|
+
{...props}
|
|
152
|
+
>
|
|
153
|
+
<ChevronUpIcon className="size-4" />
|
|
154
|
+
</SelectPrimitive.ScrollUpButton>
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function SelectScrollDownButton({
|
|
159
|
+
className,
|
|
160
|
+
...props
|
|
161
|
+
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
|
|
162
|
+
return (
|
|
163
|
+
<SelectPrimitive.ScrollDownButton
|
|
164
|
+
data-slot="select-scroll-down-button"
|
|
165
|
+
className={cn(
|
|
166
|
+
"flex cursor-default items-center justify-center py-1",
|
|
167
|
+
className,
|
|
168
|
+
)}
|
|
169
|
+
{...props}
|
|
170
|
+
>
|
|
171
|
+
<ChevronDownIcon className="size-4" />
|
|
172
|
+
</SelectPrimitive.ScrollDownButton>
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export {
|
|
177
|
+
Select,
|
|
178
|
+
SelectContent,
|
|
179
|
+
SelectGroup,
|
|
180
|
+
SelectItem,
|
|
181
|
+
SelectLabel,
|
|
182
|
+
SelectScrollDownButton,
|
|
183
|
+
SelectScrollUpButton,
|
|
184
|
+
SelectSeparator,
|
|
185
|
+
SelectTrigger,
|
|
186
|
+
SelectValue,
|
|
187
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
import { cn } from "@/lib/utils";
|
|
4
|
+
|
|
5
|
+
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
|
6
|
+
return (
|
|
7
|
+
<textarea
|
|
8
|
+
data-slot="textarea"
|
|
9
|
+
className={cn(
|
|
10
|
+
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
|
11
|
+
className,
|
|
12
|
+
)}
|
|
13
|
+
{...props}
|
|
14
|
+
/>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export { Textarea };
|
package/src/db.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
import type { Log, Span, Trace, TraceDetail } from "./types";
|
|
3
|
+
|
|
4
|
+
export class DebuggerDb {
|
|
5
|
+
private db: Database;
|
|
6
|
+
|
|
7
|
+
constructor(dbPath: string) {
|
|
8
|
+
this.db = new Database(dbPath, { readonly: true });
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
listTraces(limit = 50, offset = 0, sessionId?: string): Trace[] {
|
|
12
|
+
if (sessionId) {
|
|
13
|
+
return this.db
|
|
14
|
+
.query<Trace, [string, number, number]>(
|
|
15
|
+
`
|
|
16
|
+
SELECT DISTINCT t.trace_id, t.service_name, t.first_span_name,
|
|
17
|
+
t.start_time_unix_nano, t.end_time_unix_nano, t.span_count, t.created_at
|
|
18
|
+
FROM traces t
|
|
19
|
+
INNER JOIN spans s ON s.trace_id = t.trace_id
|
|
20
|
+
WHERE json_extract(s.attributes, '$."agent.session_id"') = ?
|
|
21
|
+
ORDER BY t.start_time_unix_nano DESC
|
|
22
|
+
LIMIT ? OFFSET ?
|
|
23
|
+
`,
|
|
24
|
+
)
|
|
25
|
+
.all(sessionId, limit, offset);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return this.db
|
|
29
|
+
.query<Trace, [number, number]>(
|
|
30
|
+
`
|
|
31
|
+
SELECT trace_id, service_name, first_span_name,
|
|
32
|
+
start_time_unix_nano, end_time_unix_nano, span_count, created_at
|
|
33
|
+
FROM traces
|
|
34
|
+
ORDER BY start_time_unix_nano DESC
|
|
35
|
+
LIMIT ? OFFSET ?
|
|
36
|
+
`,
|
|
37
|
+
)
|
|
38
|
+
.all(limit, offset);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
getTraceById(traceId: string): TraceDetail {
|
|
42
|
+
const trace = this.db
|
|
43
|
+
.query<Trace, [string]>(`SELECT * FROM traces WHERE trace_id = ?`)
|
|
44
|
+
.get(traceId);
|
|
45
|
+
|
|
46
|
+
const spans = this.db
|
|
47
|
+
.query<Span, [string]>(
|
|
48
|
+
`SELECT * FROM spans WHERE trace_id = ? ORDER BY start_time_unix_nano`,
|
|
49
|
+
)
|
|
50
|
+
.all(traceId);
|
|
51
|
+
|
|
52
|
+
const logs = this.db
|
|
53
|
+
.query<Log, [string]>(
|
|
54
|
+
`SELECT * FROM logs WHERE trace_id = ? ORDER BY timestamp_unix_nano`,
|
|
55
|
+
)
|
|
56
|
+
.all(traceId);
|
|
57
|
+
|
|
58
|
+
return { trace: trace ?? null, spans, logs };
|
|
59
|
+
}
|
|
60
|
+
}
|
package/src/env.d.ts
ADDED
package/src/frontend.tsx
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is the entry point for the React app, it sets up the root
|
|
3
|
+
* element and renders the App component to the DOM.
|
|
4
|
+
*
|
|
5
|
+
* It is included in `src/index.html`.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { StrictMode } from "react";
|
|
9
|
+
import { createRoot } from "react-dom/client";
|
|
10
|
+
import { App } from "./App";
|
|
11
|
+
|
|
12
|
+
const elem = document.getElementById("root")!;
|
|
13
|
+
const app = (
|
|
14
|
+
<StrictMode>
|
|
15
|
+
<App />
|
|
16
|
+
</StrictMode>
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
if (import.meta.hot) {
|
|
20
|
+
// With hot module reloading, `import.meta.hot.data` is persisted.
|
|
21
|
+
const root = (import.meta.hot.data.root ??= createRoot(elem));
|
|
22
|
+
root.render(app);
|
|
23
|
+
} else {
|
|
24
|
+
// The hot module reloading API is not available in production.
|
|
25
|
+
createRoot(elem).render(app);
|
|
26
|
+
}
|
package/src/index.css
ADDED
package/src/index.html
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<link rel="icon" type="image/svg+xml" href="./logo.svg" />
|
|
7
|
+
<title>Town Exterminator</title>
|
|
8
|
+
<script type="module" src="./frontend.tsx" async></script>
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<div id="root"></div>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_DEBUGGER_PORT,
|
|
3
|
+
DEFAULT_OTLP_PORT,
|
|
4
|
+
startDebuggerServer,
|
|
5
|
+
} from "./server";
|
|
6
|
+
|
|
7
|
+
const port = Number.parseInt(
|
|
8
|
+
process.env.PORT ?? String(DEFAULT_DEBUGGER_PORT),
|
|
9
|
+
10,
|
|
10
|
+
);
|
|
11
|
+
const otlpPort = Number.parseInt(
|
|
12
|
+
process.env.OTLP_PORT ?? String(DEFAULT_OTLP_PORT),
|
|
13
|
+
10,
|
|
14
|
+
);
|
|
15
|
+
const dbPath = process.env.DB_PATH ?? "./traces.db";
|
|
16
|
+
|
|
17
|
+
const { server, otlpServer } = startDebuggerServer({ port, otlpPort, dbPath });
|
|
18
|
+
|
|
19
|
+
console.log(`OTLP server running at ${otlpServer.url}`);
|
|
20
|
+
console.log(`Debugger running at ${server.url}`);
|