Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

156 linesCodeBlameRaw
1import { Link } from "react-router";
2
3import type { Route } from "./+types/observability-traces";
4import { DurationBar, FilterBar, ObservabilityHeader, StatusBadge, useLoadingView } from "../../components/observability";
5import { ButtonLink, EmptyState } from "../../components/ui";
6import { Badge } from "../../components/ui/badge";
7import { Card } from "../../components/ui/card";
8import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../../components/ui/table";
9import { cn } from "../../lib/cn";
10import { page } from "../../lib/meta";
11import { clock, duration, viewHref } from "../../lib/observability";
12import { observed, soft } from "../../lib/observability.server";
13
14export function meta({ params, ...args }: Route.MetaArgs) {
15 return page(args, { title: `Traces · ${params.owner}/${params.repo} · g1t` });
16}
17
18export async function loader({ params, context, request }: Route.LoaderArgs) {
19 const { projectId, service, view, filter } = await observed(context, params, request);
20 const search = new URL(request.url).searchParams;
21 const errorsOnly = search.get("errors") === "1";
22 const slow = Number(search.get("slow")) || null;
23 const cursor = search.get("cursor");
24 const [list, facets] = await Promise.all([
25 soft(service.traces({ project_id: projectId, ...filter, text: view.q || null, errors_only: errorsOnly, min_duration_ms: slow, cursor, limit: 50 }), "traces"),
26 soft(service.facets({ project_id: projectId }), "facets"),
27 ]);
28 return { view, list, facets, errorsOnly, slow, cursor };
29}
30
31const SLOW = [100, 500, 1000];
32
33export default function Traces({ loaderData, params }: Route.ComponentProps) {
34 const { view, list, facets, errorsOnly, slow, cursor } = loaderData;
35 const base = `/${params.owner}/${params.repo}/observability`;
36 const path = `${base}/traces`;
37 const loading = useLoadingView();
38 const extra = { errors: errorsOnly ? "1" : null, slow: slow ? String(slow) : null };
39 const href = (change: Record<string, string | null>) => viewHref(path, view, { ...extra, ...change });
40 const max = Math.max(1, ...(list?.items.map((t) => t.duration_us / 1000) ?? []));
41
42 return (
43 <div className="mx-auto max-w-6xl">
44 <ObservabilityHeader
45 title="Traces"
46 about="Each request followed through the software it touched: the request on g1t.page, the spans its own code and every other service sent, and the logs written along the way."
47 />
48 <FilterBar
49 path={path}
50 view={view}
51 environments={facets?.environments ?? []}
52 services={facets?.services ?? []}
53 search={{ placeholder: "Search span names service:api http.route=/users/:id", hint: "Words in span names, a field such as service:api, or attribute=value" }}
54 >
55 <nav className="flex rounded-md border border-line p-0.5 text-sm" aria-label="Slower than">
56 {SLOW.map((ms) => (
57 <Link
58 key={ms}
59 to={href({ slow: slow === ms ? null : String(ms) })}
60 preventScrollReset
61 aria-pressed={slow === ms}
62 className={cn("rounded px-2 py-0.5 text-xs tabular-nums", slow === ms ? "bg-raised text-fg" : "text-muted hover:text-fg")}
63 >
64 &gt;{duration(ms)}
65 </Link>
66 ))}
67 </nav>
68 <Link
69 to={href({ errors: errorsOnly ? null : "1" })}
70 preventScrollReset
71 aria-pressed={errorsOnly}
72 className={cn("inline-flex h-8 items-center rounded-md border px-2.5 text-xs", errorsOnly ? "border-danger/40 bg-danger/10 text-danger" : "border-line text-muted hover:text-fg")}
73 >
74 With errors
75 </Link>
76 </FilterBar>
77
78 <div className={cn("mt-4 transition-opacity", loading && "opacity-60")}>
79 {!list ? (
80 <Card className="p-6 text-sm text-muted">Traces could not be loaded just now. Reload the page to try again.</Card>
81 ) : list.items.length === 0 ? (
82 <EmptyState title={cursor ? "No older traces" : "No traces in this period"}>
83 Traces come from OpenTelemetry spans. An app on g1t.page sends them as soon as it uses an OpenTelemetry SDK: its endpoint and token
84 are set for it. Software elsewhere sends them with an ingest token.{" "}
85 <a href="https://docs.g1t.sh/guides/observability/#traces" className="text-accent hover:underline">
86 How
87 </a>
88 </EmptyState>
89 ) : (
90 <Card className="min-w-0 overflow-hidden">
91 <Table frameProps={{ className: "rounded-none border-0" }}>
92 <TableHeader>
93 <TableRow>
94 <TableHead className="w-36">Time (UTC)</TableHead>
95 <TableHead>Trace</TableHead>
96 <TableHead className="w-48">Services</TableHead>
97 <TableHead className="w-20 text-right">Spans</TableHead>
98 <TableHead className="w-44">Duration</TableHead>
99 </TableRow>
100 </TableHeader>
101 <TableBody>
102 {list.items.map((t) => (
103 <TableRow key={t.trace_id} className="group">
104 <TableCell className="font-mono text-xs text-muted tabular-nums">{clock(t.start_us)}</TableCell>
105 <TableCell className="max-w-0">
106 <Link to={`${path}/${t.trace_id}`} className="flex min-w-0 items-center gap-2 group-hover:text-accent">
107 {t.status !== null && <StatusBadge status={t.status} />}
108 <span className="truncate font-mono text-xs">{t.name || t.trace_id}</span>
109 </Link>
110 </TableCell>
111 <TableCell>
112 <div className="flex flex-wrap gap-1">
113 {t.services.slice(0, 3).map((s) => (
114 <Badge key={s} tone="neutral" className="font-mono">
115 {s}
116 </Badge>
117 ))}
118 {t.services.length > 3 && <span className="text-xs text-faint">+{t.services.length - 3}</span>}
119 </div>
120 </TableCell>
121 <TableCell className="text-right text-xs tabular-nums">
122 {t.span_count}
123 {t.error_count > 0 && <span className="ml-1.5 text-danger">{t.error_count} failed</span>}
124 </TableCell>
125 <TableCell>
126 <div className="flex items-center gap-2">
127 <span className="w-14 shrink-0 text-right font-mono text-xs tabular-nums">{duration(t.duration_us / 1000)}</span>
128 <DurationBar ms={t.duration_us / 1000} max={max} failed={t.error_count > 0} />
129 </div>
130 </TableCell>
131 </TableRow>
132 ))}
133 </TableBody>
134 </Table>
135 </Card>
136 )}
137 {(list?.next || cursor) && (
138 <div className="mt-4 flex justify-between">
139 {cursor ? (
140 <ButtonLink to={href({ cursor: null })} variant="outline" size="sm">
141 Newest
142 </ButtonLink>
143 ) : (
144 <span />
145 )}
146 {list?.next && (
147 <ButtonLink to={href({ cursor: list.next })} variant="outline" size="sm">
148 Older
149 </ButtonLink>
150 )}
151 </div>
152 )}
153 </div>
154 </div>
155 );
156}