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.

g1t.sh
207 linesCodeBlameRaw
1import { ChevronLeft } from "lucide-react";
2import { useMemo, useState } from "react";
3import { Link, data } from "react-router";
4
5import type { TelemetrySpan } from "@g1t/contracts";
6
7import type { Route } from "./+types/observability-trace";
8import { LogLines } from "../../components/observability-logs";
9import { SourceBadge } from "../../components/observability";
10import { Badge } from "../../components/ui/badge";
11import { Card } from "../../components/ui/card";
12import { cn } from "../../lib/cn";
13import { page } from "../../lib/meta";
14import { clock, duration } from "../../lib/observability";
15import { observed } from "../../lib/observability.server";
16import { spanTree } from "../../lib/observability-trace";
17
18export function meta({ params, ...args }: Route.MetaArgs) {
19 return page(args, { title: `Trace ${params.trace.slice(0, 8)} · ${params.owner}/${params.repo} · g1t` });
20}
21
22export async function loader({ params, context, request }: Route.LoaderArgs) {
23 const { projectId, service } = await observed(context, params, request);
24 if (!/^[0-9a-f]{32}$/.test(params.trace)) throw data(null, { status: 404 });
25 const trace = await service.trace({ project_id: projectId, trace_id: params.trace });
26 if (!trace) throw data(null, { status: 404 });
27 return trace;
28}
29
30/** Each service keeps one of the site's categorical colors through the trace (lib/usage.ts). */
31const SERVICE_COLORS = ["#3987e5", "#d95926", "#199e70", "#c98500", "#d55181", "#008300", "#9085e9", "#e66767"];
32
33export default function Trace({ loaderData, params }: Route.ComponentProps) {
34 const { spans, logs, trace_id } = loaderData;
35 const base = `/${params.owner}/${params.repo}`;
36 const rows = useMemo(() => spanTree(spans), [spans]);
37 const start = Math.min(...spans.map((s) => s.start_us));
38 const end = Math.max(...spans.map((s) => s.start_us + s.duration_us));
39 const total = Math.max(1, end - start);
40 const services = [...new Set(spans.map((s) => s.service))];
41 const color = (service: string) => SERVICE_COLORS[services.indexOf(service) % SERVICE_COLORS.length]!;
42 const [selected, setSelected] = useState<string | null>(rows[0]?.span.span_id ?? null);
43 const chosen = spans.find((s) => s.span_id === selected) ?? null;
44 const errors = spans.filter((s) => s.status === "error").length;
45
46 return (
47 <div className="mx-auto max-w-6xl">
48 <Link to={`${base}/observability/traces`} className="inline-flex items-center gap-1 text-sm text-muted hover:text-fg">
49 <ChevronLeft size={14} />
50 Traces
51 </Link>
52 <header className="mt-3">
53 <h1 className="truncate font-mono text-lg">{rows[0]?.span.name ?? trace_id}</h1>
54 <p className="mt-1 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-muted">
55 <span className="font-mono">{trace_id}</span>
56 <span className="tabular-nums">{clock(start, true)} UTC</span>
57 <span className="tabular-nums">{duration(total / 1000)}</span>
58 <span>
59 {spans.length} span{spans.length === 1 ? "" : "s"}
60 </span>
61 {errors > 0 && <span className="text-danger">{errors} failed</span>}
62 </p>
63 <ul className="mt-3 flex flex-wrap gap-x-4 gap-y-1 text-xs text-muted" aria-label="Services">
64 {services.map((s) => (
65 <li key={s} className="flex items-center gap-1.5">
66 <span className="size-2.5 rounded-sm" style={{ background: color(s) }} />
67 <span className="font-mono">{s || "(unnamed)"}</span>
68 </li>
69 ))}
70 </ul>
71 </header>
72
73 <div className="mt-5 grid gap-4 lg:grid-cols-[minmax(0,1fr)_22rem]">
74 <Card className="min-w-0 overflow-hidden">
75 {/* The time ruler. */}
76 <div className="grid grid-cols-[minmax(10rem,40%)_1fr] border-b border-line px-3 py-1.5 text-[0.6875rem] text-faint tabular-nums">
77 <span>Span</span>
78 <div className="relative h-4">
79 {[0, 0.25, 0.5, 0.75, 1].map((f) => (
80 <span key={f} className={cn("absolute", f === 1 ? "right-0" : f === 0 ? "left-0" : "-translate-x-1/2")} style={f > 0 && f < 1 ? { left: `${f * 100}%` } : undefined}>
81 {duration((total * f) / 1000)}
82 </span>
83 ))}
84 </div>
85 </div>
86 <ol role="tree" aria-label="Spans">
87 {rows.map(({ span, depth }) => {
88 const left = ((span.start_us - start) / total) * 100;
89 const width = Math.max(0.4, (span.duration_us / total) * 100);
90 const on = span.span_id === selected;
91 return (
92 <li key={span.span_id} role="treeitem" aria-selected={on} aria-level={depth + 1}>
93 <button
94 type="button"
95 onClick={() => setSelected(span.span_id)}
96 className={cn(
97 "grid w-full grid-cols-[minmax(10rem,40%)_1fr] items-center gap-2 px-3 py-1 text-left text-xs hover:bg-raised/60",
98 on && "bg-raised",
99 )}
100 >
101 <span className="flex min-w-0 items-center gap-1.5" style={{ paddingLeft: depth * 14 }}>
102 <span className="size-2 shrink-0 rounded-sm" style={{ background: color(span.service) }} />
103 <span className={cn("truncate font-mono", span.status === "error" && "text-danger")}>{span.name}</span>
104 </span>
105 <span className="relative block h-4">
106 <span
107 className={cn("absolute top-1/2 h-2.5 -translate-y-1/2 rounded-[3px]", span.status === "error" && "ring-2 ring-danger ring-offset-1 ring-offset-bg")}
108 style={{ left: `${left}%`, width: `${width}%`, background: color(span.service) }}
109 />
110 <span
111 className="absolute top-1/2 -translate-y-1/2 pl-1 font-mono text-[0.6875rem] whitespace-nowrap text-muted tabular-nums"
112 style={left + width > 80 ? { right: `${100 - left}%`, paddingRight: 4 } : { left: `${left + width}%` }}
113 >
114 {duration(span.duration_us / 1000)}
115 </span>
116 </span>
117 </button>
118 </li>
119 );
120 })}
121 </ol>
122 </Card>
123
124 <Card className="min-w-0 self-start p-4 lg:sticky lg:top-4">{chosen ? <SpanDetail span={chosen} /> : <p className="text-sm text-muted">Choose a span.</p>}</Card>
125 </div>
126
127 <section className="mt-6" aria-labelledby="logs-title">
128 <h2 id="logs-title" className="text-sm font-semibold">
129 Logs <span className="font-normal text-faint">{logs.length}</span>
130 </h2>
131 {logs.length === 0 ? (
132 <p className="mt-2 text-sm text-muted">Nothing was written under this trace.</p>
133 ) : (
134 <Card className="mt-2 overflow-hidden">
135 <LogLines logs={logs} base={base} />
136 </Card>
137 )}
138 </section>
139 </div>
140 );
141}
142
143function SpanDetail({ span }: { span: TelemetrySpan }) {
144 const sections: [string, Record<string, unknown>][] = [
145 ["Attributes", span.attributes],
146 ["Resource", span.resource],
147 ];
148 return (
149 <div className="min-w-0 text-xs">
150 <p className="font-mono text-sm break-all">{span.name}</p>
151 <div className="mt-2 flex flex-wrap gap-1.5">
152 <Badge tone="neutral">{span.kind}</Badge>
153 <Badge tone={span.status === "error" ? "danger" : span.status === "ok" ? "success" : "neutral"}>{span.status}</Badge>
154 <SourceBadge source={span.source} />
155 </div>
156 {span.status_message && <p className="mt-2 text-danger">{span.status_message}</p>}
157 <dl className="mt-3 grid grid-cols-[6rem_1fr] gap-x-3 gap-y-1">
158 <dt className="text-faint">Service</dt>
159 <dd className="font-mono break-all">{span.service}</dd>
160 <dt className="text-faint">Duration</dt>
161 <dd className="tabular-nums">{duration(span.duration_us / 1000)}</dd>
162 <dt className="text-faint">Span</dt>
163 <dd className="font-mono">{span.span_id}</dd>
164 {span.parent_span_id && (
165 <>
166 <dt className="text-faint">Parent</dt>
167 <dd className="font-mono">{span.parent_span_id}</dd>
168 </>
169 )}
170 </dl>
171 {sections.map(([title, values]) =>
172 Object.keys(values).length ? (
173 <div key={title} className="mt-4">
174 <p className="font-medium">{title}</p>
175 <dl className="mt-1 space-y-1">
176 {Object.entries(values).map(([k, v]) => (
177 <div key={k} className="min-w-0">
178 <dt className="truncate font-mono text-faint">{k}</dt>
179 <dd className="font-mono break-all">{typeof v === "string" ? v : JSON.stringify(v)}</dd>
180 </div>
181 ))}
182 </dl>
183 </div>
184 ) : null,
185 )}
186 {span.events.length > 0 && (
187 <div className="mt-4">
188 <p className="font-medium">Events</p>
189 <ul className="mt-1 space-y-2">
190 {span.events.map((e, i) => (
191 <li key={i}>
192 <p className={cn("font-mono", e.name === "exception" && "text-danger")}>
193 {e.name} <span className="text-faint">+{duration((e.time_us - span.start_us) / 1000)}</span>
194 </p>
195 {Object.entries(e.attributes).map(([k, v]) => (
196 <p key={k} className="font-mono break-all whitespace-pre-wrap text-muted">
197 <span className="text-faint">{k}</span> {typeof v === "string" ? v : JSON.stringify(v)}
198 </p>
199 ))}
200 </li>
201 ))}
202 </ul>
203 </div>
204 )}
205 </div>
206 );
207}