Skip to content
96 linesCodeBlameRaw
1/**
2 * Log lines, as the Logs page, a request's page and a trace show them:
3 * one row each (time, level, service, the line), opened for its
4 * attributes and links to the request and trace it was written under.
5 */
6import { ChevronRight } from "lucide-react";
7import { useState } from "react";
8import { Link } from "react-router";
9
10import type { TelemetryLog } from "@g1t/contracts";
11
12import { cn } from "../lib/cn";
13import { clock } from "../lib/observability";
14import { LevelBadge } from "./observability";
15
16export function LogLines({ logs, base, showRequest = true, fresh }: { logs: TelemetryLog[]; base: string; showRequest?: boolean; fresh?: Set<string> }) {
17 return (
18 <ol className="divide-y divide-line font-mono text-xs" aria-label="Log lines">
19 {logs.map((log) => (
20 <LogLine key={log.id} log={log} base={base} showRequest={showRequest} fresh={fresh?.has(log.id) ?? false} />
21 ))}
22 </ol>
23 );
24}
25
26function LogLine({ log, base, showRequest, fresh }: { log: TelemetryLog; base: string; showRequest: boolean; fresh: boolean }) {
27 const [open, setOpen] = useState(false);
28 const attributes = Object.entries(log.attributes);
29 const multiline = log.body.includes("\n");
30 return (
31 <li className={cn("group", log.severity === "error" || log.severity === "fatal" ? "bg-danger/5" : log.severity === "warn" ? "bg-warn/5" : "", fresh && "animate-[g1t-fresh_1.2s_ease-out]")}>
32 <button
33 type="button"
34 onClick={() => setOpen(!open)}
35 aria-expanded={open}
36 className="grid w-full grid-cols-[1rem_8.5rem_3.25rem_minmax(0,1fr)] items-start gap-2 px-3 py-1.5 text-left hover:bg-raised/60 sm:grid-cols-[1rem_10.5rem_3.25rem_7rem_minmax(0,1fr)]"
37 >
38 <ChevronRight size={12} className={cn("mt-0.5 text-faint transition-transform", open && "rotate-90")} />
39 <span className="text-faint tabular-nums">{clock(log.time_us, true).slice(6)}</span>
40 <LevelBadge level={log.severity} />
41 <span className="hidden truncate text-muted sm:block">{log.service || "—"}</span>
42 <span className={cn("min-w-0 text-fg", open || !multiline ? "break-words whitespace-pre-wrap" : "truncate")}>{open ? log.body : log.body.split("\n")[0]}</span>
43 </button>
44 {open && (
45 <div className="space-y-2 px-3 pt-1 pb-3 pl-9">
46 <dl className="grid grid-cols-[minmax(8rem,auto)_1fr] gap-x-4 gap-y-1">
47 <dt className="text-faint">time</dt>
48 <dd className="tabular-nums">{clock(log.time_us, true)} UTC</dd>
49 {log.environment && (
50 <>
51 <dt className="text-faint">environment</dt>
52 <dd>{log.environment}</dd>
53 </>
54 )}
55 {log.deployment_id && (
56 <>
57 <dt className="text-faint">deployment</dt>
58 <dd>
59 <Link to={`${base}/deployments/${log.deployment_id}`} className="text-accent hover:underline">
60 {log.deployment_id}
61 </Link>
62 </dd>
63 </>
64 )}
65 {showRequest && log.request_id && (
66 <>
67 <dt className="text-faint">request</dt>
68 <dd>
69 <Link to={`${base}/observability/requests/${log.request_id}`} className="text-accent hover:underline">
70 {log.request_id}
71 </Link>
72 </dd>
73 </>
74 )}
75 {log.trace_id && (
76 <>
77 <dt className="text-faint">trace</dt>
78 <dd>
79 <Link to={`${base}/observability/traces/${log.trace_id}`} className="text-accent hover:underline">
80 {log.trace_id}
81 </Link>
82 </dd>
83 </>
84 )}
85 {attributes.map(([key, value]) => (
86 <div key={key} className="contents">
87 <dt className="truncate text-faint">{key}</dt>
88 <dd className="break-all">{typeof value === "string" ? value : JSON.stringify(value)}</dd>
89 </div>
90 ))}
91 </dl>
92 </div>
93 )}
94 </li>
95 );
96}