| 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 | */ |
| 6 | import { ChevronRight } from "lucide-react"; |
| 7 | import { useState } from "react"; |
| 8 | import { Link } from "react-router"; |
| 9 | |
| 10 | import type { TelemetryLog } from "@g1t/contracts"; |
| 11 | |
| 12 | import { cn } from "../lib/cn"; |
| 13 | import { clock } from "../lib/observability"; |
| 14 | import { LevelBadge } from "./observability"; |
| 15 | |
| 16 | export 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 | |
| 26 | function 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 | } |