Menu
popagent
publicLatest change f38159a422d1050b3854c3573ac956fa3a5017b8 - Add observability UI memory contract by AkurAI Build
import { useCallback, useEffect, useState } from "react";
import { AlertTriangle, CheckCircle2, ChevronDown, ChevronRight, RefreshCw } from "lucide-react";
import type {
ObservabilityLogListResponse,
ObservabilityOverviewResponse,
ObservabilityTraceListResponse,
ObservabilityTraceResponse,
} from "../api-types";
import { apiFetch } from "./api";
export function ObservabilitySummary({ overview }: { overview: ObservabilityOverviewResponse }) {
return <section aria-label="Observability summary" className="mt-6 grid gap-3 sm:grid-cols-2 xl:grid-cols-4">
{[
["Runs", overview.runs.toLocaleString()],
["Errors", `${overview.errors.toLocaleString()} · ${(overview.errorRate * 100).toFixed(1)}%`],
["P95 latency", overview.p95LatencyMs === null ? "—" : `${Math.round(overview.p95LatencyMs).toLocaleString()} ms`],
["Input tokens", overview.inputTokens.toLocaleString()],
["Output tokens", overview.outputTokens.toLocaleString()],
["Process RSS", `${(overview.processRssBytes / 1_048_576).toFixed(1)} MiB`],
["Heap used", `${(overview.processHeapUsedBytes / 1_048_576).toFixed(1)} MiB`],
].map(([label, value]) => <article key={label} className="rounded border border-line bg-panel p-4"><div className="text-xs uppercase tracking-wide text-ink-dim">{label}</div><div className="mt-2 text-xl font-semibold">{value}</div></article>)}
</section>;
}
export function ObservabilityPage() {
const [overview, setOverview] = useState<ObservabilityOverviewResponse>();
const [traces, setTraces] = useState<ObservabilityTraceListResponse>();
const [logs, setLogs] = useState<ObservabilityLogListResponse>();
const [selected, setSelected] = useState<ObservabilityTraceResponse>();
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
const load = useCallback(async () => {
setLoading(true);
setError("");
const [overviewResponse, tracesResponse, logsResponse] = await Promise.all([
apiFetch("/api/observability/overview"),
apiFetch("/api/observability/traces?perPage=20"),
apiFetch("/api/observability/logs?perPage=20"),
]);
if (!overviewResponse.ok || !tracesResponse.ok || !logsResponse.ok) {
setError("Observability data is unavailable.");
} else {
setOverview(await overviewResponse.json() as ObservabilityOverviewResponse);
setTraces(await tracesResponse.json() as ObservabilityTraceListResponse);
setLogs(await logsResponse.json() as ObservabilityLogListResponse);
}
setLoading(false);
}, []);
useEffect(() => { void load(); }, [load]);
const toggleTrace = async (traceId: string) => {
if (selected?.traceId === traceId) return setSelected(undefined);
const response = await apiFetch(`/api/observability/traces/${traceId}`);
if (response.ok) setSelected(await response.json() as ObservabilityTraceResponse);
};
return <div className="w-full max-w-6xl">
<div className="flex items-start justify-between gap-4">
<div><h1 className="text-xl font-semibold">Observability</h1><p className="mt-1 text-sm text-ink-dim">Agent runs, latency, tokens, process memory, traces, and correlated warnings from the last 24 hours.</p></div>
<button type="button" onClick={() => void load()} disabled={loading} className="liquid-control flex min-h-11 items-center gap-2 rounded px-3 text-sm"><RefreshCw className={`size-4 ${loading ? "animate-spin" : ""}`} />Refresh</button>
</div>
{error ? <div role="alert" className="mt-6 rounded border border-red-500/30 bg-red-500/10 p-4 text-sm text-red-300">{error}</div> : null}
{loading && !overview ? <p className="mt-6 text-sm text-ink-dim">Loading observability…</p> : null}
{overview ? <ObservabilitySummary overview={overview} /> : null}
<div className="mt-5 grid gap-5 xl:grid-cols-[1.4fr_1fr]">
<section className="rounded border border-line bg-panel p-4">
<h2 className="font-medium">Recent traces</h2>
<div className="mt-3 space-y-2">{traces?.traces.length ? traces.traces.map((trace) => <article key={trace.traceId} className="rounded border border-line bg-bg">
<button type="button" aria-expanded={selected?.traceId === trace.traceId} onClick={() => void toggleTrace(trace.traceId)} className="flex min-h-11 w-full items-center gap-3 px-3 py-2 text-left">
{selected?.traceId === trace.traceId ? <ChevronDown className="size-4 shrink-0" /> : <ChevronRight className="size-4 shrink-0" />}
{trace.status === "error" ? <AlertTriangle className="size-4 shrink-0 text-red-400" /> : <CheckCircle2 className="size-4 shrink-0 text-emerald-400" />}
<span className="min-w-0 flex-1"><span className="block truncate text-sm font-medium">{trace.entityName ?? trace.name}</span><span className="block truncate font-mono text-[10px] text-ink-dim">{trace.traceId}</span></span>
<span className="text-xs text-ink-dim">{trace.durationMs === null ? "running" : `${Math.round(trace.durationMs)} ms`}</span>
</button>
{selected?.traceId === trace.traceId ? <div className="border-t border-line px-3 py-2"><ol className="space-y-1">{selected.spans.map((span) => <li key={span.spanId} className="rounded border border-line/70 bg-panel px-2 py-1.5 text-xs"><div className="flex items-center gap-2"><span className={span.status === "error" ? "text-red-400" : "text-ink-dim"}>{span.spanType}</span><span className="min-w-0 flex-1 truncate">{span.entityName ?? span.name}</span><span className="text-ink-dim">{span.durationMs === null ? "—" : `${Math.round(span.durationMs)} ms`}</span></div>{span.error ? <pre className="mt-1 overflow-x-auto whitespace-pre-wrap break-words text-red-300">{typeof span.error === "string" ? span.error : JSON.stringify(span.error, null, 2)}</pre> : null}</li>)}</ol></div> : null}
</article>) : <p className="text-sm text-ink-dim">No traces in this period.</p>}</div>
</section>
<section className="rounded border border-line bg-panel p-4">
<h2 className="font-medium">Warnings and errors</h2>
<div className="mt-3 space-y-2">{logs?.logs.length ? logs.logs.map((log) => <article key={log.id ?? `${log.timestamp}-${log.message}`} className="rounded border border-line bg-bg p-3"><div className="flex items-center gap-2 text-[10px] uppercase tracking-wide"><span className={log.level === "warn" ? "text-amber-400" : "text-red-400"}>{log.level}</span><time className="text-ink-dim">{new Date(log.timestamp).toLocaleString()}</time></div><p className="mt-1 break-words text-sm">{log.message}</p>{log.traceId ? <div className="mt-1 truncate font-mono text-[10px] text-ink-dim">{log.traceId}</div> : null}</article>) : <p className="text-sm text-ink-dim">No warnings or errors in this period.</p>}</div>
</section>
</div>
</div>;
}