AkurAI Build
Menu

popagent

public

Latest change ac87cc5fc89df21b21c6d2b8938cb47d98ed176c - Expose Popagent process memory metrics 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 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 ? <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> : 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>;
}