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.

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