more otel
Build and Publish / Metadata (push) Successful in 2s
Build and Publish / BuildAndDeployAmd64 (push) Successful in 58s

This commit is contained in:
mats committed 2026-09-18 20:51:37 +00:00
1 parent 9620e3bfa6
commit 21f04d696b
88 files changed
+23419 -192

No files matched your search

@@ -0,0 +1,251 @@
import React from "react";
import { useObservabilityData } from "../hooks/useObservabilityData";
import { useIssues } from "../hooks/useIssues";
import { getServiceName, isSpanError } from "../lib/otel-utils";
export const CommandAndQueryBar: React.FC = () => {
const {
logs,
traces,
metrics,
searchQuery,
setSearchQuery,
showErrorsOnly,
setShowErrorsOnly,
selectedServices,
toggleService,
resetFilters,
minDurationMs,
setMinDurationMs,
timeWindow,
setTimeWindow,
setActiveTab,
} = useObservabilityData();
const { issues } = useIssues();
// Compute real error metrics from filtered signals
const errorStats = React.useMemo(() => {
let errorCount = 0;
const servicesWithErrors = new Set<string>();
logs.forEach((l) =>
(l.scopeLogs ?? []).forEach((sl) =>
(sl.logRecords ?? []).forEach((r) => {
if ((r.severityNumber ?? 0) >= 17) {
errorCount++;
const sName = getServiceName(l.resource?.attributes ?? [], r.attributes ?? []);
if (sName !== "unknown-service") {
servicesWithErrors.add(sName);
}
}
}),
),
);
traces.forEach((t) =>
(t.resourceSpans ?? []).forEach((rs) =>
(rs.scopeSpans ?? []).forEach((ss) =>
(ss.spans ?? []).forEach((s) => {
if (isSpanError(s)) {
errorCount++;
const sName = getServiceName(rs.resource?.attributes ?? [], s.attributes ?? []);
if (sName !== "unknown-service") {
servicesWithErrors.add(sName);
}
}
}),
),
),
);
return {
errorCount,
services: Array.from(servicesWithErrors),
};
}, [logs, traces]);
// Top open issue if available
const topOpenIssue = React.useMemo(() => {
const open = issues.filter((i) => !i.status || i.status === "open");
return open.sort((a, b) => b.count - a.count)[0] || null;
}, [issues]);
const totalFilteredEvents = logs.length + traces.length + metrics.length;
const exportNDJSON = () => {
const records = [
...traces.map((t) => JSON.stringify({ type: "traces", ...t })),
...logs.map((l) => JSON.stringify({ type: "logs", ...l })),
...metrics.map((m) => JSON.stringify({ type: "metrics", ...m })),
].join("\n");
const blob = new Blob([records], { type: "application/x-ndjson" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `hyperscope-filtered-telemetry-${new Date().toISOString()}.ndjson`;
a.click();
URL.revokeObjectURL(url);
};
const hasActiveFilters =
Boolean(searchQuery) ||
showErrorsOnly ||
selectedServices.size > 0 ||
minDurationMs > 0 ||
timeWindow !== "all";
return (
<div className="flex flex-col gap-2.5 mb-4 font-body">
{/* Real Anomaly Radar Banner */}
{(errorStats.errorCount > 0 || topOpenIssue) && (
<div className="rounded-xl border border-rose-500/30 bg-gradient-to-r from-rose-950/40 via-slate-950/60 to-rose-950/20 p-3 shadow-lg flex flex-col sm:flex-row sm:items-center justify-between gap-3 animate-in fade-in duration-300">
<div className="flex items-center gap-2.5 min-w-0">
<div className="flex h-7 w-7 shrink-0 items-center justify-center rounded-lg bg-rose-500/20 text-rose-400 border border-rose-500/40">
<span className="material-symbols-outlined text-[18px]">warning</span>
</div>
<div className="flex flex-col min-w-0">
<div className="flex items-center gap-2">
<span className="font-mono text-xs font-bold text-rose-300 uppercase tracking-wider">
Anomaly Radar:
</span>
<span className="font-mono text-xs text-white truncate">
{errorStats.errorCount > 0
? `${errorStats.errorCount} errors in active window (${errorStats.services.join(", ") || "various"})`
: `${topOpenIssue?.count} occurrences of "${topOpenIssue?.title}"`}
</span>
</div>
{topOpenIssue && (
<span className="text-[11px] text-slate-400 truncate">
Top Issue: <span className="text-slate-200 font-semibold">{topOpenIssue.title}</span> (Service: {topOpenIssue.service}, ×{topOpenIssue.count})
</span>
)}
</div>
</div>
<div className="flex items-center gap-2 shrink-0">
<button
type="button"
onClick={() => setActiveTab("tab0")}
className="px-3 py-1 rounded-lg bg-rose-500 hover:bg-rose-600 text-white font-mono text-xs font-bold transition-colors shadow-sm"
>
Triage Issues
</button>
{!showErrorsOnly && (
<button
type="button"
onClick={() => setShowErrorsOnly(true)}
className="px-2.5 py-1 rounded-lg bg-white/10 hover:bg-white/20 text-slate-200 font-mono text-xs transition-colors"
>
Isolate Errors
</button>
)}
</div>
</div>
)}
{/* Filter and Telemetry Status Bar */}
<div className="flex flex-wrap items-center justify-between gap-2.5 p-3 rounded-xl bg-surface-container-lowest border border-surface-container/50 shadow-sm">
<div className="flex items-center gap-2 flex-wrap">
<span className="font-mono text-[10px] text-outline uppercase tracking-wider font-semibold shrink-0">
Active Context:
</span>
<span className="font-mono text-xs text-secondary font-semibold">
{totalFilteredEvents} Signals ({timeWindow === "all" ? "All Time" : `Last ${timeWindow}`})
</span>
{/* Selected Service Pills */}
{Array.from(selectedServices).map((service) => (
<span
key={service}
className="inline-flex items-center gap-1 rounded-full bg-primary-container/20 text-primary-container border border-primary-container/40 px-2.5 py-0.5 font-mono text-xs font-semibold"
>
<span>service:{service}</span>
<button
type="button"
onClick={() => toggleService(service)}
className="hover:text-white"
>
×
</button>
</span>
))}
{/* Errors Only Pill */}
{showErrorsOnly && (
<span className="inline-flex items-center gap-1 rounded-full bg-error-container text-on-error-container px-2.5 py-0.5 font-mono text-xs font-semibold">
<span>Errors Only</span>
<button
type="button"
onClick={() => setShowErrorsOnly(false)}
className="hover:text-white"
>
×
</button>
</span>
)}
{/* Slow Spans Pill */}
{minDurationMs > 0 && (
<span className="inline-flex items-center gap-1 rounded-full bg-amber-500/20 text-amber-300 border border-amber-500/40 px-2.5 py-0.5 font-mono text-xs font-semibold">
<span>Latency &gt;{minDurationMs}ms</span>
<button
type="button"
onClick={() => setMinDurationMs(0)}
className="hover:text-white"
>
×
</button>
</span>
)}
{/* Search Query Pill */}
{searchQuery && (
<span className="inline-flex items-center gap-1 rounded-full bg-surface-container text-on-surface border border-surface-container-high px-2.5 py-0.5 font-mono text-xs">
<span>query: &quot;{searchQuery}&quot;</span>
<button
type="button"
onClick={() => setSearchQuery("")}
className="hover:text-white"
>
×
</button>
</span>
)}
{hasActiveFilters && (
<button
type="button"
onClick={() => {
resetFilters();
setMinDurationMs(0);
setTimeWindow("all");
}}
className="text-outline hover:text-white font-mono text-xs underline ml-1"
>
Clear all
</button>
)}
</div>
{/* Export and Action buttons */}
<div className="flex items-center gap-2">
<button
type="button"
onClick={exportNDJSON}
className="flex items-center gap-1 px-2.5 py-1 bg-surface-container-low hover:bg-surface-container text-on-surface font-mono text-xs rounded-lg transition-colors border border-surface-container/40 shadow-sm"
title="Export currently filtered telemetry signals as NDJSON"
>
<span className="material-symbols-outlined text-[15px]">download</span>
<span>Export NDJSON</span>
</button>
</div>
</div>
</div>
);
};
export default CommandAndQueryBar;
@@ -0,0 +1,9 @@
import { TabsComponent } from "./Tabs";
export const EntryVisualization = () => {
return (
<div className="animate-in fade-in slide-in-from-bottom-4 duration-700">
<TabsComponent />
</div>
);
};
File diff suppressed because it is too large. Load diff
+372
View File
@@ -0,0 +1,372 @@
import { useMemo } from "react";
import { ArrowDownRight, ArrowUpRight, Minus } from "lucide-react";
import { getServicePalette } from "../lib/otel-utils";
export interface GaugeMeterProps {
name: string;
service?: string;
value: number;
min?: number;
max?: number;
avg?: number;
unit?: string;
size?: "sm" | "md" | "lg";
thresholds?: {
warn: number;
danger: number;
};
subtitle?: string;
history?: number[];
showTrend?: boolean;
isSum?: boolean;
aggLabel?: string;
soloPod?: string;
podCount?: number;
podSummaries?: Array<{ pod: string; value: number }>;
}
const formatNumber = (val: number, maxDecimals = 2) => {
if (Math.abs(val) >= 1_000_000) {
return `${(val / 1_000_000).toFixed(1)}M`;
}
if (Math.abs(val) >= 1_000) {
return `${(val / 1_000).toFixed(1)}k`;
}
if (Number.isInteger(val)) {
return val.toString();
}
return val.toFixed(maxDecimals);
};
// SVG arc calculations
const polarToCartesian = (centerX: number, centerY: number, radius: number, angleInDegrees: number) => {
const angleInRadians = ((angleInDegrees - 90) * Math.PI) / 180.0;
return {
x: centerX + radius * Math.cos(angleInRadians),
y: centerY + radius * Math.sin(angleInRadians),
};
};
const describeArc = (x: number, y: number, radius: number, startAngle: number, endAngle: number) => {
const start = polarToCartesian(x, y, radius, endAngle);
const end = polarToCartesian(x, y, radius, startAngle);
const largeArcFlag = endAngle - startAngle <= 180 ? "0" : "1";
return ["M", start.x, start.y, "A", radius, radius, 0, largeArcFlag, 0, end.x, end.y].join(" ");
};
export const GaugeMeter = ({
name,
service,
value,
min: customMin,
max: customMax,
avg,
unit = "",
size = "md",
thresholds,
subtitle,
history = [],
showTrend = true,
isSum = false,
aggLabel,
soloPod,
podCount,
podSummaries,
}: GaugeMeterProps) => {
const servicePalette = service ? getServicePalette(service) : null;
// Determine min/max range
const { min, max } = useMemo(() => {
let computedMin = customMin !== undefined ? customMin : 0;
let computedMax = customMax !== undefined ? customMax : 100;
if (history.length > 0 && customMin === undefined && customMax === undefined) {
const histMin = Math.min(...history);
const histMax = Math.max(...history);
if (histMax <= 1 && histMin >= 0) {
// Percentage ratio 0..1
computedMin = 0;
computedMax = 1;
} else if (histMax <= 100 && histMin >= 0) {
// 0..100%
computedMin = 0;
computedMax = 100;
} else {
computedMin = Math.floor(Math.min(0, histMin));
computedMax = Math.ceil(histMax * 1.15) || 100;
}
}
if (computedMax <= computedMin) {
computedMax = computedMin + 1;
}
return { min: computedMin, max: computedMax };
}, [customMin, customMax, history]);
// Normalized percent 0..100
const percent = useMemo(() => {
const clamped = Math.max(min, Math.min(value, max));
return ((clamped - min) / (max - min)) * 100;
}, [value, min, max]);
// Color determination based on thresholds
const warnThreshold = thresholds?.warn ?? 70;
const dangerThreshold = thresholds?.danger ?? 90;
const statusColor = useMemo(() => {
if (percent >= dangerThreshold) {
return {
text: "text-rose-400",
stroke: "#f43f5e",
gradientStart: "#fb7185",
gradientEnd: "#e11d48",
bg: "bg-rose-500/10",
border: "border-rose-500/30",
glow: "rgba(244, 63, 94, 0.35)",
label: "Critical",
};
}
if (percent >= warnThreshold) {
return {
text: "text-amber-400",
stroke: "#f59e0b",
gradientStart: "#fbbf24",
gradientEnd: "#d97706",
bg: "bg-amber-500/10",
border: "border-amber-500/30",
glow: "rgba(245, 158, 11, 0.35)",
label: "Warning",
};
}
return {
text: "text-cyan-400",
stroke: "#06b6d4",
gradientStart: "#38bdf8",
gradientEnd: "#0284c7",
bg: "bg-cyan-500/10",
border: "border-cyan-500/30",
glow: "rgba(6, 182, 212, 0.35)",
label: "Nominal",
};
}, [percent, warnThreshold, dangerThreshold]);
// Trend calculation
const trend = useMemo(() => {
if (!showTrend || history.length < 2) return null;
const prev = history[history.length - 2];
const diff = value - prev;
if (Math.abs(diff) < 0.0001) return { direction: "flat", text: "steady", color: "text-slate-500" };
if (diff > 0) return { direction: "up", text: `+${formatNumber(diff)}`, color: "text-rose-400" };
return { direction: "down", text: `${formatNumber(diff)}`, color: "text-emerald-400" };
}, [value, history, showTrend]);
// Dimensions
const dim = size === "sm" ? 120 : size === "lg" ? 220 : 160;
const strokeWidth = size === "sm" ? 8 : size === "lg" ? 14 : 10;
const radius = (dim - strokeWidth * 2) / 2;
const centerX = dim / 2;
const centerY = dim / 2 + (size === "sm" ? 4 : 8);
// 240 degree arc from -120 to +120 degrees
const startAngle = -120;
const endAngle = 120;
const totalAngle = endAngle - startAngle;
const valueAngle = startAngle + (percent / 100) * totalAngle;
const bgArc = describeArc(centerX, centerY, radius, startAngle, endAngle);
const valueArc = describeArc(centerX, centerY, radius, startAngle, Math.min(valueAngle, endAngle));
// Ticks at 0, 50, 100
const tickAngles = [startAngle, startAngle + totalAngle * 0.5, endAngle];
const gaugeId = useMemo(
() => `${service ? service.replace(/[^a-zA-Z0-9]/g, "_") + "_" : ""}${name.replace(/[^a-zA-Z0-9]/g, "_")}`,
[service, name]
);
return (
<div className="flex flex-col items-center justify-between rounded-2xl border border-white/10 bg-slate-900/40 p-4 shadow-lg backdrop-blur-sm transition-all hover:border-white/20">
<div className="flex w-full items-start justify-between gap-2 border-b border-white/5 pb-2">
<div className="flex flex-col min-w-0 pr-1">
<div className="flex items-center gap-1 flex-wrap mb-1">
{service && (
<span
className={`inline-flex items-center gap-1 self-start rounded px-1.5 py-0.5 text-[9px] font-mono font-bold uppercase tracking-wider border ${servicePalette?.bg} ${servicePalette?.text} ${servicePalette?.border}`}
title={`Owning Service: ${service}`}
>
<span className="w-1.5 h-1.5 rounded-full bg-current opacity-80" />
<span className="truncate max-w-[120px]">{service}</span>
</span>
)}
{podCount !== undefined && podCount > 1 && (
<span
className="inline-flex items-center gap-1 rounded bg-sky-500/15 px-1.5 py-0.5 text-[9px] font-mono font-bold text-sky-300 border border-sky-500/30"
title={`Sum of values across ${podCount} pods`}
>
<span className="text-[11px] font-black leading-none text-sky-400">∑</span>
<span>{podCount} pods</span>
</span>
)}
</div>
<span className="truncate text-xs font-bold uppercase tracking-wider text-slate-300" title={name}>
{name}
</span>
</div>
<span className={`shrink-0 rounded-full px-2 py-0.5 text-[9px] font-black uppercase tracking-wider ${statusColor.bg} ${statusColor.text} ${statusColor.border} border`}>
{statusColor.label}
</span>
</div>
<div className="relative my-2 flex items-center justify-center">
<svg width={dim} height={dim * 0.85} viewBox={`0 0 ${dim} ${dim * 0.85}`} className="overflow-visible">
<defs>
<linearGradient id={`gauge-grad-${gaugeId}`} x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stopColor={statusColor.gradientStart} />
<stop offset="100%" stopColor={statusColor.gradientEnd} />
</linearGradient>
<filter id={`gauge-glow-${gaugeId}`} x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="0" stdDeviation="3" floodColor={statusColor.glow} />
</filter>
</defs>
{/* Background Track */}
<path
d={bgArc}
fill="none"
stroke="rgba(255, 255, 255, 0.08)"
strokeWidth={strokeWidth}
strokeLinecap="round"
/>
{/* Ticks */}
{tickAngles.map((angle, idx) => {
const inner = polarToCartesian(centerX, centerY, radius - strokeWidth * 0.8, angle);
const outer = polarToCartesian(centerX, centerY, radius + strokeWidth * 0.8, angle);
return (
<line
key={idx}
x1={inner.x}
y1={inner.y}
x2={outer.x}
y2={outer.y}
stroke="rgba(255, 255, 255, 0.2)"
strokeWidth={1.5}
/>
);
})}
{/* Active Value Arc */}
{percent > 0 && (
<path
d={valueArc}
fill="none"
stroke={`url(#gauge-grad-${gaugeId})`}
strokeWidth={strokeWidth}
strokeLinecap="round"
filter={`url(#gauge-glow-${gaugeId})`}
/>
)}
{/* Needle / Value dot indicator */}
{percent > 0 && (() => {
const tip = polarToCartesian(centerX, centerY, radius, valueAngle);
return (
<circle
cx={tip.x}
cy={tip.y}
r={strokeWidth * 0.6}
fill="#ffffff"
stroke={statusColor.stroke}
strokeWidth={2}
className="transition-all duration-300"
/>
);
})()}
</svg>
{/* Center Readout */}
<div className="absolute inset-0 flex flex-col items-center justify-center pt-3 text-center pointer-events-none">
{soloPod ? (
<span className="text-[8px] font-mono uppercase font-black text-amber-400 tracking-wider truncate max-w-[100px]" title={`Solo: ${soloPod}`}>
{soloPod}
</span>
) : aggLabel ? (
<span className="text-[8.5px] font-mono uppercase font-black text-sky-400 tracking-wider">
{aggLabel}
</span>
) : podCount !== undefined && podCount > 1 ? (
<span className="text-[8.5px] font-mono uppercase font-black text-sky-400 tracking-wider">
{isSum ? "Sum (∑)" : "Avg (μ)"}
</span>
) : null}
<div className="flex items-baseline gap-1">
<span className={`font-mono font-black tracking-tight ${size === "sm" ? "text-lg" : size === "lg" ? "text-3xl" : "text-2xl"} ${statusColor.text}`}>
{formatNumber(value)}
</span>
{unit && (
<span className="text-[10px] font-bold uppercase text-slate-400">
{unit}
</span>
)}
</div>
{podCount !== undefined && podCount > 1 ? (
<span
className="text-[9px] font-mono text-slate-400 truncate max-w-[100px]"
title={isSum ? `Average: ${formatNumber(value / podCount)} per pod` : `Fleet average across ${podCount} pods`}
>
{isSum ? `~${formatNumber(value / podCount)}/pod` : `${podCount} pods avg`}
</span>
) : subtitle ? (
<span className="text-[10px] text-slate-500 truncate max-w-[90px]">
{subtitle}
</span>
) : null}
</div>
</div>
{/* Footer range indicators & trend */}
<div className="flex w-full items-center justify-between border-t border-white/5 pt-2 text-[10px] text-slate-400">
<span className="font-mono">
<span className="text-slate-600">min</span> {formatNumber(min)}
</span>
{trend && (
<span className={`inline-flex items-center gap-0.5 font-mono text-[9px] font-bold ${trend.color}`}>
{trend.direction === "up" && <ArrowUpRight className="h-2.5 w-2.5" />}
{trend.direction === "down" && <ArrowDownRight className="h-2.5 w-2.5" />}
{trend.direction === "flat" && <Minus className="h-2.5 w-2.5" />}
{trend.text}
</span>
)}
{avg !== undefined && (
<span className="font-mono text-slate-400">
<span className="text-slate-600">avg</span> {formatNumber(avg)}
</span>
)}
<span className="font-mono">
<span className="text-slate-600">max</span> {formatNumber(max)}
</span>
</div>
{/* Pod Breakdown pills if available */}
{podSummaries && podSummaries.length > 1 && (
<div className="mt-2 flex w-full flex-wrap items-center gap-1 border-t border-white/5 pt-1.5">
{podSummaries.slice(0, 3).map((ps) => (
<div
key={ps.pod}
className="flex items-center gap-1 rounded bg-white/5 px-1.5 py-0.5 text-[8.5px] font-mono text-slate-400"
title={`${ps.pod}: ${formatNumber(ps.value)} ${unit || ""}`}
>
<span className="truncate max-w-[65px] text-slate-300">{ps.pod}</span>
<span className="font-bold text-sky-300">{formatNumber(ps.value)}</span>
</div>
))}
{podSummaries.length > 3 && (
<span className="text-[8px] font-mono text-slate-500 self-center">
+{podSummaries.length - 3}
</span>
)}
</div>
)}
</div>
);
};
+601
View File
@@ -0,0 +1,601 @@
import { useMemo, useState } from "react"
import {
Activity,
AlertOctagon,
Bug,
CheckCircle2,
ChevronDown,
ChevronUp,
Clock,
EyeOff,
ExternalLink,
Globe,
MonitorSmartphone,
RefreshCw,
RotateCcw,
ScrollText,
ShieldBan,
X,
} from "lucide-react"
import { useObservabilityData } from "../hooks/useObservabilityData"
import { authFetch } from "../lib/auth"
import { formatRelativeTime } from "../lib/timeUtils"
import { getServicePalette } from "../lib/otel-utils"
import {
issueStatusMeta,
issueStatusOf,
latestBreadcrumbs,
latestStacktrace,
sampleContext,
trendPoints,
} from "../lib/issue-utils"
import type { Issue, IssueSample } from "../lib/types"
const kindMeta: Record<string, { label: string; Icon: typeof Activity }> = {
span_error: { label: "Trace Error", Icon: Activity },
log_error: { label: "Log Error", Icon: ScrollText },
frontend_error: { label: "Frontend Error", Icon: Globe },
}
type StatusFilter = "all" | "open" | "resolved" | "ignored"
type KindFilter = "all" | "span_error" | "log_error" | "frontend_error"
interface HoneypotIP {
ip: string
count: number
first_seen: string
last_seen: string
promoted_until: string
}
const statusFilters: { value: StatusFilter; label: string }[] = [
{ value: "all", label: "All" },
{ value: "open", label: "Open" },
{ value: "resolved", label: "Resolved" },
{ value: "ignored", label: "Ignored" },
]
const kindFilters: { value: KindFilter; label: string; Icon: typeof Activity }[] = [
{ value: "all", label: "All kinds", Icon: Bug },
{ value: "span_error", label: "Traces", Icon: Activity },
{ value: "log_error", label: "Logs", Icon: ScrollText },
{ value: "frontend_error", label: "Frontend", Icon: Globe },
]
// Lightweight inline-SVG sparkline of the issue's hourly occurrence trend.
// Deliberately not recharts: issue lists can hold dozens of cards and each
// recharts instance carries meaningful render cost.
const TrendSparkline = ({ issue }: { issue: Issue }) => {
const points = trendPoints(issue, 24)
const max = Math.max(...points.map((p) => p.count), 1)
const width = 96
const height = 24
const step = width / (points.length - 1)
const path = points
.map((p, i) => `${i === 0 ? "M" : "L"}${(i * step).toFixed(1)},${(height - (p.count / max) * (height - 2) - 1).toFixed(1)}`)
.join(" ")
const gradientId = `sparkFill-${issue.id}`
return (
<span
className="inline-flex shrink-0 flex-col items-end gap-0.5"
title="Occurrences per hour (last 24h)"
>
<svg width={width} height={height} className="overflow-visible">
<path d={`${path} L${width},${height} L0,${height} Z`} fill={`url(#${gradientId})`} opacity="0.35" />
<path d={path} fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinejoin="round" className="text-rose-400/80" />
<defs>
<linearGradient id={gradientId} x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="currentColor" className="text-rose-400" />
<stop offset="100%" stopColor="currentColor" stopOpacity="0" className="text-rose-400" />
</linearGradient>
</defs>
</svg>
</span>
)
}
const StatusBadge = ({ issue }: { issue: Issue }) => {
const status = issueStatusOf(issue)
const meta = issueStatusMeta[status]
return (
<span className={`shrink-0 rounded border px-1.5 py-0.5 text-[9px] font-bold uppercase tracking-wider ${meta.className}`}>
{meta.label}
</span>
)
}
const StacktraceView = ({ stacktrace }: { stacktrace: string }) => (
<pre className="max-h-64 overflow-auto rounded-xl border border-white/5 bg-black/60 p-3 font-mono text-[10px] leading-relaxed text-slate-300 whitespace-pre-wrap break-all">
{stacktrace}
</pre>
)
const BreadcrumbsView = ({ issue }: { issue: Issue }) => {
const crumbs = latestBreadcrumbs(issue)
if (!crumbs.length) return null
return (
<div className="space-y-1.5">
<p className="flex items-center gap-1.5 text-[10px] font-bold uppercase tracking-widest text-slate-500">
<Clock className="h-3 w-3" />
Breadcrumbs before error
</p>
<div className="max-h-48 space-y-1 overflow-auto rounded-xl border border-white/5 bg-black/40 p-3">
{crumbs.map((crumb, idx) => (
<div key={`${crumb.t}-${idx}`} className="flex items-baseline gap-2 font-mono text-[10px]">
<span className="shrink-0 text-slate-600">
{new Date(crumb.t).toLocaleTimeString([], { hour12: false })}
</span>
<span className="shrink-0 rounded bg-white/5 px-1.5 py-0.5 text-[9px] font-bold uppercase text-sky-400/80">
{crumb.category}
</span>
<span className="min-w-0 break-all text-slate-300">{crumb.message.slice(0, 200)}</span>
</div>
))}
</div>
</div>
)
}
const SampleContextChips = ({ sample }: { sample: IssueSample }) => {
const ctx = sampleContext(sample)
if (!ctx.length) return null
return (
<span className="inline-flex shrink-0 items-center gap-1 rounded-full border border-amber-400/20 bg-amber-400/10 px-2 py-0.5 font-mono text-[10px] font-bold text-amber-300">
{ctx.join(" · ")}
</span>
)
}
const StatusActions = ({
issue,
onStatusChange,
busy,
}: {
issue: Issue
onStatusChange: (issue: Issue, status: string) => void
busy: boolean
}) => {
const status = issueStatusOf(issue)
const actions: { to: string; label: string; Icon: typeof CheckCircle2 }[] =
status === "open"
? [
{ to: "resolved", label: "Resolve", Icon: CheckCircle2 },
{ to: "ignored", label: "Ignore", Icon: EyeOff },
]
: [{ to: "open", label: "Reopen", Icon: RotateCcw }]
return (
<div className="flex items-center gap-2">
{actions.map(({ to, label, Icon }) => (
<button
key={to}
disabled={busy}
onClick={(e) => {
e.stopPropagation()
onStatusChange(issue, to)
}}
className="inline-flex items-center gap-1.5 rounded-lg border border-white/10 bg-white/5 px-2.5 py-1 text-[10px] font-bold uppercase tracking-wider text-slate-400 transition-all hover:border-white/20 hover:text-white disabled:opacity-50"
>
<Icon className="h-3 w-3" />
{label}
</button>
))}
</div>
)
}
const IssueCard = ({
issue,
onStatusChange,
statusBusy,
}: {
issue: Issue
onStatusChange: (issue: Issue, status: string) => void
statusBusy: boolean
}) => {
const { setActiveTraceId, setActiveSpanId, setActiveTab } = useObservabilityData()
const [expanded, setExpanded] = useState(false)
const meta = kindMeta[issue.kind] ?? { label: issue.kind, Icon: Bug }
const isFrontend = issue.kind === "frontend_error"
const badgeGradient = isFrontend
? "from-violet-500 via-sky-500 to-cyan-400"
: "from-rose-500 via-red-500 to-orange-400"
const palette = getServicePalette(issue.service)
const latestSample = issue.samples[issue.samples.length - 1]
const stacktrace = latestStacktrace(issue)
const hasMappedStacktrace = issue.samples.some((sample) => Boolean(sample.resolvedStacktrace))
const contextSample = issue.samples.find((s) => s.httpMethod || s.httpRoute || s.httpStatus)
const viewTrace = (traceId: string, spanId?: string) => {
setActiveTraceId(traceId)
if (spanId) setActiveSpanId(spanId)
setActiveTab("tab1")
}
return (
<div className="group relative flex flex-col overflow-hidden rounded-xl border border-rose-500/10 bg-slate-950/40 transition-all hover:border-rose-500/25 hover:bg-slate-900/50">
<div className={`absolute left-0 top-0 bottom-0 w-1 bg-gradient-to-b ${badgeGradient} opacity-50 transition-opacity group-hover:opacity-100`} />
<div
onClick={() => setExpanded((prev) => !prev)}
className="flex cursor-pointer flex-wrap items-center gap-x-4 gap-y-2 p-3 pl-4"
>
<span className={`inline-flex shrink-0 items-center gap-1.5 rounded px-1.5 py-0.5 text-[10px] font-bold uppercase tracking-wider bg-gradient-to-r ${badgeGradient} text-slate-950`}>
<meta.Icon className="h-3 w-3" />
{meta.label}
</span>
<span className={`shrink-0 rounded px-1.5 py-0.5 text-[10px] font-bold uppercase tracking-wider ${palette.bg} ${palette.text} ${palette.border} border`}>
{issue.service}
</span>
<span className="flex-1 min-w-[240px] break-all font-mono text-[12px] text-slate-200">
{issue.title}
</span>
<TrendSparkline issue={issue} />
<span className="shrink-0 rounded-full border border-rose-500/20 bg-rose-500/10 px-2 py-0.5 text-[10px] font-bold text-rose-300">
×{issue.count}
</span>
<StatusBadge issue={issue} />
<span className="shrink-0 text-[10px] text-slate-500 whitespace-nowrap">
last seen {formatRelativeTime(issue.lastSeen / 1e6)}
</span>
{expanded ? <ChevronUp className="h-3 w-3 text-slate-500" /> : <ChevronDown className="h-3 w-3 text-slate-500" />}
</div>
{expanded && (
<div className="space-y-4 border-t border-white/5 bg-black/40 p-4 text-[12px]">
<div className="flex flex-wrap items-center justify-between gap-3">
<div className="flex flex-wrap items-center gap-4 text-[10px] text-slate-500">
<span>First seen {formatRelativeTime(issue.firstSeen / 1e6)}</span>
{issue.affectedServices.length > 1 && (
<span>Affects: {issue.affectedServices.join(", ")}</span>
)}
{issue.status === "resolved" && issue.resolvedAt ? (
<span>Resolved {formatRelativeTime(issue.resolvedAt / 1e6)}</span>
) : null}
</div>
<StatusActions issue={issue} onStatusChange={onStatusChange} busy={statusBusy} />
</div>
{stacktrace && (
<div className="space-y-1.5">
<p className="text-[10px] font-bold uppercase tracking-widest text-slate-500">
{hasMappedStacktrace ? "Source-mapped stacktrace" : "Stacktrace"}
</p>
<StacktraceView stacktrace={stacktrace} />
</div>
)}
<BreadcrumbsView issue={issue} />
<div className="flex flex-wrap gap-4 text-[10px] text-slate-500">
{(contextSample?.userAgent || latestSample?.userAgent) && (
<span className="inline-flex items-center gap-1.5 max-w-[360px] truncate" title={contextSample?.userAgent || latestSample?.userAgent}>
<MonitorSmartphone className="h-3 w-3 shrink-0" />
{(contextSample?.userAgent || latestSample?.userAgent)?.slice(0, 80)}
</span>
)}
{(contextSample?.release || latestSample?.release) && (
<span>release: {contextSample?.release || latestSample?.release}</span>
)}
{(contextSample?.environment || latestSample?.environment) && (
<span>env: {contextSample?.environment || latestSample?.environment}</span>
)}
</div>
<div className="space-y-2">
<p className="text-[10px] font-bold uppercase tracking-widest text-slate-500">
Recent occurrences ({issue.samples.length})
</p>
<div className="space-y-1.5">
{[...issue.samples].reverse().map((sample, idx) => (
<div
key={`${sample.timestamp}-${idx}`}
className="flex flex-wrap items-center gap-3 rounded-lg border border-white/5 bg-white/5 px-2.5 py-1.5"
>
<span className="shrink-0 text-slate-500 font-mono text-[10px]">
{new Date(sample.timestamp / 1e6).toLocaleTimeString([], {
hour12: false,
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
})}
</span>
<SampleContextChips sample={sample} />
<span className="flex-1 min-w-[160px] break-all text-slate-300">{sample.message}</span>
{sample.url && (
<a
href={sample.url}
target="_blank"
rel="noreferrer"
onClick={(e) => e.stopPropagation()}
className="flex shrink-0 items-center gap-1 rounded-full bg-violet-500/10 border border-violet-400/20 px-2.5 py-0.5 text-[10px] font-bold text-violet-300 hover:bg-violet-500/20 hover:border-violet-400/30 transition-all max-w-[220px] truncate"
title={sample.url}
>
<ExternalLink className="h-2.5 w-2.5 shrink-0" />
<span className="truncate">{sample.url.replace(/^https?:\/\//, "")}</span>
</a>
)}
{sample.traceId && (
<button
onClick={(e) => {
e.stopPropagation()
viewTrace(sample.traceId!, sample.spanId)
}}
className="group/trace flex shrink-0 items-center gap-1.5 rounded-full bg-sky-500/10 border border-sky-400/20 px-2.5 py-0.5 text-[10px] font-bold text-sky-400 hover:bg-sky-500/20 hover:border-sky-400/30 transition-all"
>
<Activity className="h-2.5 w-2.5 group-hover/trace:animate-pulse" />
TRACE:{sample.traceId.slice(0, 6)}
</button>
)}
</div>
))}
</div>
</div>
{latestSample?.traceId && (
<div className="mt-1 pt-4 border-t border-white/5 flex justify-end">
<button
onClick={() => viewTrace(latestSample.traceId!, latestSample.spanId)}
className="inline-flex items-center gap-2 rounded-xl bg-sky-500 px-4 py-2 text-[10px] font-black uppercase tracking-widest text-slate-950 transition-all hover:bg-sky-400 hover:scale-[1.02] active:scale-[0.98] shadow-lg shadow-sky-500/20"
>
<Activity className="h-3 w-3" />
View Latest Trace
</button>
</div>
)}
</div>
)}
</div>
)
}
interface IssuesVisualizerProps {
issues: Issue[]
loading: boolean
error: string | null
refresh: () => void
onStatusChange: (issue: Issue, status: string) => void
statusBusy: boolean
}
export const IssuesVisualizer = ({
issues,
loading,
error,
refresh,
onStatusChange,
statusBusy,
}: IssuesVisualizerProps) => {
const { selectedServices, searchQuery } = useObservabilityData()
const [statusFilter, setStatusFilter] = useState<StatusFilter>("all")
const [kindFilter, setKindFilter] = useState<KindFilter>("all")
const [excludeSpamIPs, setExcludeSpamIPs] = useState(false)
const [showSpamIPs, setShowSpamIPs] = useState(false)
const [spamIPs, setSpamIPs] = useState<HoneypotIP[]>([])
const [spamIPsLoading, setSpamIPsLoading] = useState(false)
const [spamIPsError, setSpamIPsError] = useState<string | null>(null)
const loadSpamIPs = async () => {
setSpamIPsLoading(true)
setSpamIPsError(null)
try {
const response = await authFetch("/api/honeypot/ips")
if (!response.ok) throw new Error(`HTTP ${response.status}`)
setSpamIPs((await response.json()) as HoneypotIP[])
} catch (err) {
setSpamIPsError(err instanceof Error ? err.message : String(err))
} finally {
setSpamIPsLoading(false)
}
}
const spamIPSet = useMemo(() => new Set(spamIPs.map((entry) => entry.ip)), [spamIPs])
const statusCounts = useMemo(() => {
const counts = { all: issues.length, open: 0, resolved: 0, ignored: 0 }
issues.forEach((issue) => {
counts[issueStatusOf(issue)] += 1
})
return counts
}, [issues])
const filteredIssues = useMemo(() => {
return issues.filter((issue) => {
if (statusFilter !== "all" && issueStatusOf(issue) !== statusFilter) return false
if (kindFilter !== "all" && issue.kind !== kindFilter) return false
if (
excludeSpamIPs &&
issue.samples.some(
(sample) =>
(sample.httpStatus ?? 0) >= 400 &&
Boolean(sample.sourceIp) &&
spamIPSet.has(sample.sourceIp!),
)
) {
return false
}
if (selectedServices.size > 0) {
const matches =
selectedServices.has(issue.service) ||
issue.affectedServices.some((s) => selectedServices.has(s))
if (!matches) return false
}
if (searchQuery) {
const q = searchQuery.toLowerCase()
const matches =
issue.title.toLowerCase().includes(q) || issue.service.toLowerCase().includes(q)
if (!matches) return false
}
return true
})
}, [issues, selectedServices, searchQuery, statusFilter, kindFilter, excludeSpamIPs, spamIPSet])
if (loading && issues.length === 0) {
return (
<div className="py-12 text-center text-sm text-slate-400 flex flex-col items-center justify-center">
<AlertOctagon className="h-10 w-10 mx-auto mb-4 opacity-20 animate-pulse" />
Loading issues...
</div>
)
}
return (
<div className="space-y-4">
<div className="sticky top-0 z-20 flex flex-wrap items-center justify-between gap-3 py-2 border-b border-white/5 mb-2 bg-slate-950/80 backdrop-blur">
<div className="flex flex-wrap items-center gap-3">
<span className="inline-flex items-center gap-2 text-[10px] font-black uppercase tracking-[0.2em] text-slate-500">
<AlertOctagon className="h-3 w-3 text-rose-400" />
Deduplicated Issues
</span>
{/* Status filter chips */}
<div className="flex items-center gap-1 rounded-lg border border-white/5 bg-white/5 p-0.5">
{statusFilters.map(({ value, label }) => (
<button
key={value}
onClick={() => setStatusFilter(value)}
className={`rounded px-2 py-0.5 text-[10px] font-bold uppercase tracking-wider transition-all ${
statusFilter === value
? "bg-sky-500/20 text-sky-300 ring-1 ring-sky-500/30"
: "text-slate-500 hover:text-slate-300"
}`}
>
{label}
<span className="ml-1 opacity-60">{statusCounts[value]}</span>
</button>
))}
</div>
<button
onClick={() => setExcludeSpamIPs((value) => !value)}
className={`inline-flex items-center gap-1.5 rounded-lg border px-2.5 py-1 text-[10px] font-bold uppercase tracking-wider transition-all ${
excludeSpamIPs
? "border-amber-400/40 bg-amber-400/15 text-amber-200"
: "border-white/10 text-slate-500 hover:border-white/20 hover:text-slate-300"
}`}
title="Hide HTTP errors whose recorded source IP is on the honeypot list"
>
<ShieldBan className="h-3 w-3" />
{excludeSpamIPs ? "Spam IPs hidden" : "Hide spam IPs"}
</button>
{/* Kind filter chips */}
<div className="flex items-center gap-1 rounded-lg border border-white/5 bg-white/5 p-0.5">
{kindFilters.map(({ value, label, Icon }) => (
<button
key={value}
onClick={() => setKindFilter(value)}
className={`inline-flex items-center gap-1 rounded px-2 py-0.5 text-[10px] font-bold uppercase tracking-wider transition-all ${
kindFilter === value
? "bg-violet-500/20 text-violet-300 ring-1 ring-violet-500/30"
: "text-slate-500 hover:text-slate-300"
}`}
>
<Icon className="h-3 w-3" />
{label}
</button>
))}
</div>
<span className="text-[10px] text-slate-500">
{filteredIssues.length} shown
</span>
</div>
<div className="flex items-center gap-3">
{error && <span className="text-[10px] text-rose-400">Failed to load: {error}</span>}
<button
onClick={() => {
const next = !showSpamIPs
setShowSpamIPs(next)
if (next && spamIPs.length === 0) void loadSpamIPs()
}}
className="flex items-center gap-1.5 rounded-lg border border-white/10 px-2.5 py-1 text-[10px] font-bold uppercase tracking-wider text-slate-400 hover:text-white hover:border-white/20 transition-all"
>
<ShieldBan className="h-3 w-3" />
Spam IPs ({spamIPs.length})
</button>
<button
onClick={() => refresh()}
className="flex items-center gap-1.5 rounded-lg border border-white/10 px-2.5 py-1 text-[10px] font-bold uppercase tracking-wider text-slate-400 hover:text-white hover:border-white/20 transition-all"
>
<RefreshCw className={`h-3 w-3 ${loading ? "animate-spin" : ""}`} />
Refresh
</button>
</div>
</div>
{showSpamIPs && (
<div className="rounded-xl border border-amber-400/20 bg-amber-400/5 p-3 text-xs text-slate-300">
<div className="flex items-center justify-between gap-3 mb-2">
<div>
<div className="font-bold text-amber-200">Promoted honeypot IPs</div>
<div className="text-[10px] text-slate-500">Used by the ingress tarpit synchronizer.</div>
</div>
<button
onClick={() => setShowSpamIPs(false)}
className="rounded p-1 text-slate-500 hover:text-white"
aria-label="Close spam IP list"
>
<X className="h-4 w-4" />
</button>
</div>
{spamIPsLoading && <div className="text-slate-500">Loading…</div>}
{spamIPsError && <div className="text-rose-300">Failed to load: {spamIPsError}</div>}
{!spamIPsLoading && !spamIPsError && spamIPs.length === 0 && (
<div className="text-slate-500">No promoted IPs.</div>
)}
{spamIPs.length > 0 && (
<div className="grid gap-1 sm:grid-cols-2 lg:grid-cols-3">
{spamIPs.map((entry) => (
<div key={entry.ip} className="flex items-center justify-between rounded-lg bg-black/20 px-2.5 py-1.5 font-mono">
<span className="text-slate-200">{entry.ip}</span>
<span className="text-[10px] text-amber-300">×{entry.count}</span>
</div>
))}
</div>
)}
</div>
)}
{filteredIssues.length === 0 ? (
<div className="flex flex-col items-center justify-center py-20 text-slate-500">
<div className="h-12 w-12 rounded-full bg-emerald-500/10 flex items-center justify-center mb-4">
<AlertOctagon className="h-6 w-6 text-emerald-400/60" />
</div>
<p className="text-sm font-medium text-slate-300">
{issues.length === 0 ? "No issues detected" : "No issues match your filters"}
</p>
<p className="text-xs mt-1 text-slate-600">
{issues.length === 0
? "Error-status spans and error/fatal-severity logs will show up here as they arrive."
: "Try adjusting your filters or search query."}
</p>
</div>
) : (
<div className="space-y-2 animate-in fade-in slide-in-from-bottom-2 duration-500">
{filteredIssues.map((issue) => (
<IssueCard
key={issue.id}
issue={issue}
onStatusChange={onStatusChange}
statusBusy={statusBusy}
/>
))}
</div>
)}
</div>
)
}
export default IssuesVisualizer
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,76 @@
import { useMemo, useState } from "react"
import { GitBranch, RefreshCw, ThumbsDown, ThumbsUp } from "lucide-react"
import { usePatterns } from "../hooks/usePatterns"
import { formatRelativeTime } from "../lib/timeUtils"
import type { Issue, SimilarIssue } from "../lib/types"
const PatternIssues = ({ pattern, issues, similar, review }: {
pattern: { issueIds: string[] }
issues: Issue[]
similar: (id: string) => Promise<SimilarIssue[]>
review: (id: string, otherId: string, decision: "same" | "different") => Promise<void>
}) => {
const [expanded, setExpanded] = useState(false)
const [related, setRelated] = useState<Record<string, SimilarIssue[]>>({})
const [busy, setBusy] = useState<string | null>(null)
const members = pattern.issueIds.map((id) => issues.find((issue) => issue.id === id)).filter(Boolean) as Issue[]
const toggle = async (issue: Issue) => {
if (expanded) { setExpanded(false); return }
setExpanded(true)
if (!related[issue.id]) {
const suggestions = await similar(issue.id)
setRelated((prev) => ({ ...prev, [issue.id]: suggestions }))
}
}
const submitReview = async (issue: Issue, item: SimilarIssue, decision: "same" | "different") => {
const key = `${issue.id}:${item.issue.id}`
setBusy(key)
try {
await review(issue.id, item.issue.id, decision)
setRelated((prev) => ({ ...prev, [issue.id]: (prev[issue.id] ?? []).map((entry) => entry.issue.id === item.issue.id ? { ...entry, decision } : entry) }))
} finally { setBusy(null) }
}
return (
<div className="mt-3 space-y-2 border-t border-white/5 pt-3">
<div className="flex flex-wrap gap-2">
{members.map((issue) => <button key={issue.id} onClick={() => void toggle(issue)} className="rounded-lg border border-white/10 bg-white/5 px-2 py-1 text-left text-[10px] text-slate-300 hover:border-sky-400/40">{issue.title} <span className="text-slate-500">×{issue.count}</span></button>)}
</div>
{expanded && members[0] && <div className="space-y-2 rounded-lg bg-black/20 p-3">
<div className="text-[10px] uppercase tracking-widest text-slate-500">Deterministic suggestions for {members[0].title}</div>
{(related[members[0].id] ?? []).map((item) => {
const key = `${members[0].id}:${item.issue.id}`
return <div key={item.issue.id} className="flex flex-wrap items-center gap-2 text-[11px]">
<span className="text-slate-300">{item.issue.title}</span><span className="text-sky-300">{Math.round(item.score * 100)}%</span><span className="text-slate-500">{item.reasons.join(" · ")}</span>
<button disabled={busy === key} onClick={() => void submitReview(members[0], item, "same")} className="ml-auto inline-flex items-center gap-1 rounded border border-emerald-400/20 px-2 py-1 text-emerald-300 hover:bg-emerald-400/10"><ThumbsUp className="h-3 w-3" /> same</button>
<button disabled={busy === key} onClick={() => void submitReview(members[0], item, "different")} className="inline-flex items-center gap-1 rounded border border-rose-400/20 px-2 py-1 text-rose-300 hover:bg-rose-400/10"><ThumbsDown className="h-3 w-3" /> separate</button>
{item.decision && <span className="text-slate-500">reviewed: {item.decision}</span>}
</div>
})}
</div>}
</div>
)
}
export function PatternsVisualizer() {
const { patterns, issues, loading, error, refresh, similar, review } = usePatterns()
const [query, setQuery] = useState("")
const visible = useMemo(() => patterns.filter((pattern) => `${pattern.service} ${pattern.kind} ${pattern.signature}`.toLowerCase().includes(query.toLowerCase())), [patterns, query])
if (loading && patterns.length === 0) return <div className="py-16 text-center text-sm text-slate-500">Loading deterministic patterns…</div>
return <div className="space-y-4">
<div className="flex flex-wrap items-center gap-3 border-b border-white/5 pb-3">
<div><div className="flex items-center gap-2 text-[10px] font-black uppercase tracking-[0.2em] text-slate-500"><GitBranch className="h-3 w-3 text-violet-400" /> Similarity Patterns</div><div className="mt-1 text-xs text-slate-500">Exact issues stay separate; broader deterministic patterns are reviewable.</div></div>
<input value={query} onChange={(event) => setQuery(event.target.value)} placeholder="Filter patterns…" className="ml-auto rounded-lg border border-white/10 bg-white/5 px-3 py-2 text-xs text-slate-200 outline-none placeholder:text-slate-600 focus:border-sky-400/40" />
<button onClick={() => void refresh()} className="inline-flex items-center gap-2 rounded-lg border border-white/10 px-3 py-2 text-[10px] font-bold uppercase tracking-wider text-slate-400 hover:text-white"><RefreshCw className="h-3 w-3" /> Refresh</button>
</div>
{error && <div className="rounded-lg border border-rose-400/20 bg-rose-400/5 p-3 text-xs text-rose-300">Failed to load patterns: {String(error)}</div>}
{visible.length === 0 ? <div className="py-16 text-center text-sm text-slate-500">No deterministic patterns observed yet.</div> : <div className="space-y-3">{visible.map((pattern) => <div key={pattern.id} className="rounded-xl border border-violet-400/15 bg-slate-950/40 p-4">
<div className="flex flex-wrap items-center gap-3"><span className="rounded bg-violet-500/15 px-2 py-1 text-[10px] font-bold uppercase text-violet-300">{pattern.service}</span><span className="text-xs text-slate-400">{pattern.kind}</span><span className="text-xs text-slate-300">{pattern.issueCount} exact issues · {pattern.occurrenceCount} occurrences</span><span className="ml-auto text-[10px] text-slate-600">last seen {formatRelativeTime(pattern.lastSeen / 1e6)}</span></div>
<div className="mt-3 break-all font-mono text-[10px] text-slate-500">{pattern.signature.split("\u0000").join(" · ")}</div>
<PatternIssues pattern={pattern} issues={issues} similar={similar} review={review} />
</div>)}</div>}
</div>
}
@@ -0,0 +1,48 @@
import { useState } from "react"
import { Activity, CheckCircle2, CloudCog, RefreshCw, XCircle } from "lucide-react"
import { useRollouts } from "../hooks/useRollouts"
import { formatRelativeTime } from "../lib/timeUtils"
import type { RolloutImpact } from "../lib/types"
const phaseStyle: Record<string, { className: string; Icon: typeof Activity }> = {
completed: { className: "text-emerald-300 bg-emerald-400/10 border-emerald-400/20", Icon: CheckCircle2 },
failed: { className: "text-rose-300 bg-rose-400/10 border-rose-400/20", Icon: XCircle },
progressing: { className: "text-amber-300 bg-amber-400/10 border-amber-400/20", Icon: Activity },
}
export function RolloutsVisualizer() {
const { rollouts, loading, error, refresh, impact } = useRollouts()
const [selected, setSelected] = useState<string | null>(null)
const [impactData, setImpactData] = useState<Record<string, RolloutImpact>>({})
const [impactError, setImpactError] = useState<string | null>(null)
const select = async (id: string) => {
setSelected(selected === id ? null : id)
if (impactData[id]) return
try {
setImpactError(null)
const result = await impact(id)
setImpactData((previous) => ({ ...previous, [id]: result }))
}
catch (cause) { setImpactError(cause instanceof Error ? cause.message : String(cause)) }
}
if (loading && rollouts.length === 0) return <div className="py-16 text-center text-sm text-slate-500">Loading cluster rollouts…</div>
return <div className="space-y-4">
<div className="flex flex-wrap items-center gap-3 border-b border-white/5 pb-3">
<div><div className="flex items-center gap-2 text-[10px] font-black uppercase tracking-[0.2em] text-slate-500"><CloudCog className="h-3 w-3 text-sky-400" /> Cluster Rollouts</div><div className="mt-1 text-xs text-slate-500">Read-only Kubernetes deployment state with optional Prometheus impact data.</div></div>
<button onClick={() => void refresh()} className="ml-auto inline-flex items-center gap-2 rounded-lg border border-white/10 px-3 py-2 text-[10px] font-bold uppercase tracking-wider text-slate-400 hover:text-white"><RefreshCw className="h-3 w-3" /> Refresh</button>
</div>
{error && <div className="rounded-lg border border-amber-400/20 bg-amber-400/5 p-3 text-xs text-amber-200">Rollout watcher unavailable: {String(error)}</div>}
{impactError && <div className="rounded-lg border border-rose-400/20 bg-rose-400/5 p-3 text-xs text-rose-300">Impact query failed: {impactError}</div>}
{rollouts.length === 0 ? <div className="py-16 text-center text-sm text-slate-500">No rollout data. Enable the Kubernetes watcher and grant its read-only RBAC permissions.</div> : <div className="space-y-2">{rollouts.map((rollout) => {
const status = phaseStyle[rollout.phase] ?? phaseStyle.progressing
const Icon = status.Icon
const data = impactData[rollout.id]
return <div key={rollout.id} className="rounded-xl border border-white/10 bg-slate-950/40 p-4">
<button onClick={() => void select(rollout.id)} className="w-full text-left"><div className="flex flex-wrap items-center gap-3"><span className={`inline-flex items-center gap-1 rounded border px-2 py-1 text-[10px] font-bold uppercase ${status.className}`}><Icon className="h-3 w-3" /> {rollout.phase}</span><span className="font-mono text-xs text-slate-200">{rollout.namespace}/{rollout.workload}</span><span className="text-xs text-slate-400">revision {rollout.revision || "unknown"}</span><span className="ml-auto text-[10px] text-slate-500">{formatRelativeTime(rollout.lastSeen / 1e6)}</span></div><div className="mt-3 grid gap-2 text-[11px] text-slate-400 sm:grid-cols-2 lg:grid-cols-4"><span>release: <b className="text-slate-200">{rollout.release || "unlabeled"}</b></span><span>image: <b className="font-mono text-slate-200">{rollout.image || "unknown"}</b></span><span>replicas: <b className="text-slate-200">{rollout.availableReplicas}/{rollout.desiredReplicas} ready</b></span><span>reason: <b className="text-slate-200">{rollout.reason || "unknown"}</b></span></div></button>
{selected === rollout.id && <div className="mt-4 border-t border-white/5 pt-3 text-xs text-slate-400"><div>Started {formatRelativeTime(rollout.startedAt / 1e6)}{rollout.completedAt ? ` · completed ${formatRelativeTime(rollout.completedAt / 1e6)}` : ""}</div><div className="mt-2 text-slate-500">{rollout.message || "No rollout message reported."}</div>{data && <div className="mt-3 rounded-lg bg-black/20 p-3"><div className="flex items-center gap-2 text-[10px] font-bold uppercase tracking-widest text-slate-500"><Activity className="h-3 w-3" /> Prometheus error-rate points</div>{!data.configured ? <div className="mt-2 text-slate-600">Prometheus is not configured.</div> : data.points.length === 0 ? <div className="mt-2 text-slate-600">No matching Prometheus series.</div> : <div className="mt-2 flex flex-wrap gap-2">{data.points.slice(-12).map((point) => <span key={`${point.timestamp}-${point.value}`} className="rounded bg-sky-400/10 px-2 py-1 font-mono text-[10px] text-sky-300">{new Date(point.timestamp * 1000).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}: {point.value}</span>)}</div>}</div>}</div>}
</div>
})}</div>}
</div>
}
+504
View File
@@ -0,0 +1,504 @@
import React, { useState, useRef } from "react";
import {
Search,
Radar,
Play,
Square,
Copy,
Check,
ExternalLink,
} from "lucide-react";
import { authFetch } from "../lib/auth";
import { useObservabilityData } from "../hooks/useObservabilityData";
interface SearchResultEntry {
type: "traces" | "metrics" | "logs" | string;
timestamp: number;
data: any;
}
export const SearchAndWatcher: React.FC = () => {
const { setActiveTraceId, setActiveTab } = useObservabilityData();
// Search state
const [searchQuery, setSearchQuery] = useState("");
const [searchType, setSearchType] = useState<string>("");
const [searchLimit, setSearchLimit] = useState<number>(100);
const [searchTimeRange, setSearchTimeRange] = useState<string>("24h");
const [searchResults, setSearchResults] = useState<SearchResultEntry[] | null>(null);
const [searchLoading, setSearchLoading] = useState(false);
const [searchError, setSearchError] = useState<string | null>(null);
// Watcher state
const [watchQuery, setWatchQuery] = useState("");
const [watchType, setWatchType] = useState<string>("");
const [watchTimeout, setWatchTimeout] = useState<number>(30);
const [isWatching, setIsWatching] = useState(false);
const [caughtSignal, setCaughtSignal] = useState<SearchResultEntry | null>(null);
const [watchStatus, setWatchStatus] = useState<string | null>(null);
const abortControllerRef = useRef<AbortController | null>(null);
// UI helpers
const [copiedId, setCopiedId] = useState<string | null>(null);
const [expandedIndex, setExpandedIndex] = useState<number | null>(null);
const copyToClipboard = (text: string, id: string) => {
navigator.clipboard.writeText(text);
setCopiedId(id);
setTimeout(() => setCopiedId(null), 1500);
};
const handleRunSearch = async (e?: React.FormEvent) => {
if (e) e.preventDefault();
setSearchLoading(true);
setSearchError(null);
try {
const params = new URLSearchParams();
if (searchQuery.trim()) params.set("q", searchQuery.trim());
if (searchType) params.set("type", searchType);
params.set("limit", String(searchLimit));
if (searchTimeRange !== "all") {
const msMap: Record<string, number> = {
"1h": 60 * 60 * 1000,
"6h": 6 * 60 * 60 * 1000,
"24h": 24 * 60 * 60 * 1000,
"7d": 7 * 24 * 60 * 60 * 1000,
};
const ms = msMap[searchTimeRange] || 24 * 60 * 60 * 1000;
const sinceNano = BigInt(Date.now() - ms) * 1_000_000n;
params.set("since", sinceNano.toString());
}
const res = await authFetch(`/api/history/search?${params.toString()}`);
if (!res.ok) throw new Error(`Search failed: HTTP ${res.status}`);
const data = await res.json();
setSearchResults(Array.isArray(data) ? data : []);
} catch (err) {
setSearchError(err instanceof Error ? err.message : String(err));
} finally {
setSearchLoading(false);
}
};
const startWatcher = async () => {
if (isWatching) return;
setIsWatching(true);
setCaughtSignal(null);
setWatchStatus("Trap armed. Waiting for telemetry match...");
const controller = new AbortController();
abortControllerRef.current = controller;
try {
const params = new URLSearchParams();
if (watchQuery.trim()) params.set("q", watchQuery.trim());
if (watchType) params.set("type", watchType);
params.set("timeout", String(watchTimeout));
const res = await authFetch(`/api/history/wait?${params.toString()}`, {
signal: controller.signal,
});
if (!res.ok) {
if (res.status === 504) {
setWatchStatus(`Timeout reached (${watchTimeout}s) without match.`);
} else {
setWatchStatus(`Error: HTTP ${res.status}`);
}
return;
}
const signal = await res.json();
setCaughtSignal(signal);
setWatchStatus("Target signal trapped successfully!");
} catch (err: any) {
if (err.name === "AbortError") {
setWatchStatus("Trap disarmed by user.");
} else {
setWatchStatus(`Watcher error: ${err.message || String(err)}`);
}
} finally {
setIsWatching(false);
abortControllerRef.current = null;
}
};
const cancelWatcher = () => {
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
setIsWatching(false);
};
// Helper to extract trace ID from an entry
const extractTraceId = (entry: SearchResultEntry): string | null => {
const raw = JSON.stringify(entry.data);
const match = raw.match(/"traceId":"([a-f0-9]{16,32})"/i);
return match ? match[1] : null;
};
return (
<div className="space-y-8 max-w-6xl font-body">
{/* Top Banner */}
<div className="rounded-2xl border border-sky-500/20 bg-gradient-to-r from-sky-950/40 via-slate-900/60 to-slate-950/80 p-5 shadow-xl backdrop-blur-md">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div className="space-y-1">
<div className="flex items-center gap-2">
<div className="flex h-7 w-7 items-center justify-center rounded-lg bg-sky-500/20 text-sky-400">
<Radar className="h-4 w-4 animate-pulse" />
</div>
<h2 className="text-base font-bold text-white tracking-tight">
Archive Search &amp; Live Signal Trap
</h2>
</div>
<p className="text-xs text-slate-400 max-w-2xl leading-relaxed">
Expose deep telemetry data beyond the browser buffer. Query full ClickHouse &amp; persistent storage logs/traces, or arm an OTLP signal trap to catch anomalies as they hit the server.
</p>
</div>
</div>
</div>
{/* SECTION 1: Live Signal Trap */}
<section className="rounded-2xl border border-white/10 bg-slate-950/40 p-5 shadow-xl space-y-4">
<div className="flex items-center justify-between border-b border-white/5 pb-3">
<div className="flex items-center gap-2">
<Radar className="h-4 w-4 text-violet-400" />
<h3 className="text-xs font-black uppercase tracking-[0.2em] text-slate-400">
Live Signal Trap (/api/history/wait)
</h3>
</div>
<span className="text-[11px] font-mono text-slate-500">
Long-poll subscriber for active debugging
</span>
</div>
<div className="grid grid-cols-1 md:grid-cols-12 gap-3 items-center">
<div className="md:col-span-6">
<label className="block font-mono text-[10px] text-slate-400 uppercase tracking-wider mb-1">
Target Keyword / Pattern / Trace ID
</label>
<div className="relative flex items-center">
<input
type="text"
value={watchQuery}
onChange={(e) => setWatchQuery(e.target.value)}
placeholder="e.g. 'error', '500', 'db_timeout', or service name..."
disabled={isWatching}
className="w-full h-9 rounded-xl border border-white/10 bg-black/40 px-3 text-xs text-white placeholder-slate-500 focus:border-violet-500/50 focus:outline-none disabled:opacity-50 font-mono"
/>
</div>
</div>
<div className="md:col-span-3">
<label className="block font-mono text-[10px] text-slate-400 uppercase tracking-wider mb-1">
Signal Type
</label>
<select
value={watchType}
onChange={(e) => setWatchType(e.target.value)}
disabled={isWatching}
className="w-full h-9 rounded-xl border border-white/10 bg-black/40 px-3 text-xs text-white focus:border-violet-500/50 focus:outline-none disabled:opacity-50 font-mono"
>
<option value="">Any Telemetry</option>
<option value="traces">Traces Only</option>
<option value="logs">Logs Only</option>
<option value="metrics">Metrics Only</option>
</select>
</div>
<div className="md:col-span-3 flex items-end gap-2">
<div className="flex-1">
<label className="block font-mono text-[10px] text-slate-400 uppercase tracking-wider mb-1">
Timeout
</label>
<select
value={watchTimeout}
onChange={(e) => setWatchTimeout(Number(e.target.value))}
disabled={isWatching}
className="w-full h-9 rounded-xl border border-white/10 bg-black/40 px-3 text-xs text-white focus:border-violet-500/50 focus:outline-none disabled:opacity-50 font-mono"
>
<option value={15}>15 seconds</option>
<option value={30}>30 seconds</option>
<option value={60}>60 seconds</option>
</select>
</div>
{isWatching ? (
<button
type="button"
onClick={cancelWatcher}
className="h-9 px-4 rounded-xl bg-rose-500 hover:bg-rose-600 text-white font-mono text-xs font-bold transition-colors flex items-center gap-1.5 shadow-lg shadow-rose-500/20"
>
<Square className="h-3.5 w-3.5 fill-current" />
<span>Cancel</span>
</button>
) : (
<button
type="button"
onClick={startWatcher}
className="h-9 px-4 rounded-xl bg-violet-500 hover:bg-violet-600 text-white font-mono text-xs font-bold transition-colors flex items-center gap-1.5 shadow-lg shadow-violet-500/20"
>
<Play className="h-3.5 w-3.5 fill-current" />
<span>Arm Trap</span>
</button>
)}
</div>
</div>
{/* Watcher Status or Live Trigger Output */}
{watchStatus && (
<div
className={`rounded-xl p-3 text-xs font-mono flex items-center justify-between border ${
isWatching
? "bg-violet-500/10 border-violet-500/30 text-violet-300 animate-pulse"
: caughtSignal
? "bg-emerald-500/10 border-emerald-500/30 text-emerald-300"
: "bg-slate-900/60 border-white/5 text-slate-400"
}`}
>
<div className="flex items-center gap-2">
<span
className={`h-2 w-2 rounded-full ${
isWatching ? "bg-violet-400 animate-ping" : caughtSignal ? "bg-emerald-400" : "bg-slate-500"
}`}
/>
<span>{watchStatus}</span>
</div>
</div>
)}
{/* Captured Signal Preview */}
{caughtSignal && (
<div className="rounded-xl border border-emerald-500/20 bg-black/40 p-4 space-y-2 animate-in fade-in duration-300">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="rounded bg-emerald-500/20 text-emerald-300 border border-emerald-500/40 px-2 py-0.5 text-[10px] font-bold uppercase">
{caughtSignal.type}
</span>
<span className="font-mono text-xs text-slate-300">
Captured at {new Date(caughtSignal.timestamp / 1_000_000).toLocaleTimeString()}
</span>
</div>
{extractTraceId(caughtSignal) && (
<button
onClick={() => {
const traceId = extractTraceId(caughtSignal);
if (traceId) {
setActiveTraceId(traceId);
setActiveTab("tab1");
}
}}
className="inline-flex items-center gap-1.5 rounded-lg bg-sky-500 px-3 py-1 text-[11px] font-bold text-slate-950 hover:bg-sky-400 transition-colors"
>
<ExternalLink className="h-3 w-3" />
Jump to Captured Trace
</button>
)}
</div>
<pre className="max-h-60 overflow-auto rounded-lg border border-white/5 bg-slate-950 p-3 font-mono text-[11px] text-slate-300">
<code>{JSON.stringify(caughtSignal.data, null, 2)}</code>
</pre>
</div>
)}
</section>
{/* SECTION 2: Persistent Storage Search */}
<section className="rounded-2xl border border-white/10 bg-slate-950/40 p-5 shadow-xl space-y-4">
<div className="flex items-center justify-between border-b border-white/5 pb-3">
<div className="flex items-center gap-2">
<Search className="h-4 w-4 text-sky-400" />
<h3 className="text-xs font-black uppercase tracking-[0.2em] text-slate-400">
Query Persistent Telemetry Store (/api/history/search)
</h3>
</div>
<span className="text-[11px] font-mono text-slate-500">
Searches full database history beyond active memory
</span>
</div>
<form onSubmit={handleRunSearch} className="space-y-3">
<div className="grid grid-cols-1 md:grid-cols-12 gap-3">
<div className="md:col-span-4">
<label className="block font-mono text-[10px] text-slate-400 uppercase tracking-wider mb-1">
Search Query
</label>
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Free-text substring in telemetry JSON..."
className="w-full h-9 rounded-xl border border-white/10 bg-black/40 px-3 text-xs text-white placeholder-slate-500 focus:border-sky-500/50 focus:outline-none font-mono"
/>
</div>
<div className="md:col-span-2">
<label className="block font-mono text-[10px] text-slate-400 uppercase tracking-wider mb-1">
Type
</label>
<select
value={searchType}
onChange={(e) => setSearchType(e.target.value)}
className="w-full h-9 rounded-xl border border-white/10 bg-black/40 px-3 text-xs text-white focus:border-sky-500/50 focus:outline-none font-mono"
>
<option value="">All Types</option>
<option value="traces">Traces</option>
<option value="logs">Logs</option>
<option value="metrics">Metrics</option>
</select>
</div>
<div className="md:col-span-2">
<label className="block font-mono text-[10px] text-slate-400 uppercase tracking-wider mb-1">
Time Window
</label>
<select
value={searchTimeRange}
onChange={(e) => setSearchTimeRange(e.target.value)}
className="w-full h-9 rounded-xl border border-white/10 bg-black/40 px-3 text-xs text-white focus:border-sky-500/50 focus:outline-none font-mono"
>
<option value="1h">Last 1 Hour</option>
<option value="6h">Last 6 Hours</option>
<option value="24h">Last 24 Hours</option>
<option value="7d">Last 7 Days</option>
<option value="all">All Storage</option>
</select>
</div>
<div className="md:col-span-2">
<label className="block font-mono text-[10px] text-slate-400 uppercase tracking-wider mb-1">
Limit
</label>
<select
value={searchLimit}
onChange={(e) => setSearchLimit(Number(e.target.value))}
className="w-full h-9 rounded-xl border border-white/10 bg-black/40 px-3 text-xs text-white focus:border-sky-500/50 focus:outline-none font-mono"
>
<option value={50}>50 entries</option>
<option value={100}>100 entries</option>
<option value={250}>250 entries</option>
<option value={500}>500 entries</option>
</select>
</div>
<div className="md:col-span-2 flex items-end">
<button
type="submit"
disabled={searchLoading}
className="w-full h-9 rounded-xl bg-sky-500 hover:bg-sky-400 text-slate-950 font-mono text-xs font-bold transition-colors flex items-center justify-center gap-1.5 shadow-lg shadow-sky-500/20 disabled:opacity-50"
>
<Search className="h-3.5 w-3.5" />
<span>{searchLoading ? "Searching..." : "Search"}</span>
</button>
</div>
</div>
</form>
{searchError && (
<div className="rounded-xl border border-rose-500/20 bg-rose-500/10 p-3 text-xs font-mono text-rose-300">
{searchError}
</div>
)}
{/* Results List */}
{searchResults !== null && (
<div className="space-y-3 pt-2">
<div className="flex items-center justify-between text-[11px] font-mono text-slate-400 border-b border-white/5 pb-2">
<span>Found {searchResults.length} persistent storage matches</span>
</div>
{searchResults.length === 0 ? (
<div className="py-12 text-center text-sm text-slate-500 font-mono">
No telemetry entries matched your search criteria in the selected time range.
</div>
) : (
<div className="space-y-2 max-h-[500px] overflow-y-auto pr-1">
{searchResults.map((entry, idx) => {
const traceId = extractTraceId(entry);
const isExpanded = expandedIndex === idx;
const timeFormatted = entry.timestamp
? new Date(entry.timestamp / 1_000_000).toLocaleString()
: "—";
return (
<div
key={idx}
className="rounded-xl border border-white/5 bg-black/30 p-3 text-xs font-mono transition-colors hover:border-white/15 space-y-2"
>
<div
onClick={() => setExpandedIndex(isExpanded ? null : idx)}
className="flex cursor-pointer items-center justify-between gap-3"
>
<div className="flex items-center gap-2">
<span
className={`rounded px-1.5 py-0.5 text-[9px] font-bold uppercase tracking-wider ${
entry.type === "traces"
? "bg-sky-500/20 text-sky-300 border border-sky-500/30"
: entry.type === "logs"
? "bg-amber-500/20 text-amber-300 border border-amber-500/30"
: "bg-emerald-500/20 text-emerald-300 border border-emerald-500/30"
}`}
>
{entry.type}
</span>
<span className="text-slate-400 text-[11px]">{timeFormatted}</span>
{traceId && (
<span className="text-sky-400 font-bold">
trace:{traceId.slice(0, 8)}...
</span>
)}
</div>
<div className="flex items-center gap-2">
{traceId && (
<button
onClick={(e) => {
e.stopPropagation();
setActiveTraceId(traceId);
setActiveTab("tab1");
}}
className="rounded bg-sky-500/10 border border-sky-500/30 px-2 py-0.5 text-[10px] text-sky-300 hover:bg-sky-500/20 transition-colors"
>
Open Trace
</button>
)}
<button
onClick={(e) => {
e.stopPropagation();
copyToClipboard(JSON.stringify(entry.data, null, 2), `res-${idx}`);
}}
className="p-1 text-slate-500 hover:text-white transition-colors"
title="Copy JSON"
>
{copiedId === `res-${idx}` ? (
<Check className="h-3 w-3 text-emerald-400" />
) : (
<Copy className="h-3 w-3" />
)}
</button>
</div>
</div>
{isExpanded && (
<pre className="mt-2 max-h-56 overflow-auto rounded-lg border border-white/5 bg-slate-950 p-2.5 text-[10px] text-slate-300 leading-relaxed">
<code>{JSON.stringify(entry.data, null, 2)}</code>
</pre>
)}
</div>
);
})}
</div>
)}
</div>
)}
</section>
</div>
);
};
export default SearchAndWatcher;
+495
View File
@@ -0,0 +1,495 @@
import { useMemo, useState } from "react";
import { Layers } from "lucide-react";
import { formatDuration } from "../lib/timeUtils";
export interface GraphSpan {
spanId: string;
parentSpanId?: string;
service: string;
duration: number;
hasError?: boolean;
}
interface ServiceNode {
id: string;
name: string;
spanCount: number;
errorCount: number;
totalDuration: number;
layer: number;
x: number;
y: number;
}
interface ServiceEdge {
id: string;
source: string;
target: string;
callCount: number;
avgDuration: number;
hasError: boolean;
}
interface ServiceGraphProps {
spans: GraphSpan[];
onSelectService?: (serviceName: string | null) => void;
selectedService?: string | null;
}
export const ServiceGraph = ({
spans,
onSelectService,
selectedService,
}: ServiceGraphProps) => {
const [hoveredNode, setHoveredNode] = useState<string | null>(null);
const { nodes, edges, width, height } = useMemo(() => {
// 1. Map spans by ID for parent lookup
const spanMap = new Map<string, GraphSpan>();
spans.forEach((s) => spanMap.set(s.spanId, s));
// 2. Aggregate service metrics
const serviceStats = new Map<
string,
{ spanCount: number; errorCount: number; totalDuration: number }
>();
spans.forEach((span) => {
const current = serviceStats.get(span.service) || {
spanCount: 0,
errorCount: 0,
totalDuration: 0,
};
current.spanCount += 1;
if (span.hasError) current.errorCount += 1;
current.totalDuration += span.duration;
serviceStats.set(span.service, current);
});
// 3. Aggregate cross-service edges
const edgeMap = new Map<
string,
{ source: string; target: string; count: number; totalDur: number; errors: number }
>();
spans.forEach((span) => {
if (span.parentSpanId && spanMap.has(span.parentSpanId)) {
const parentSpan = spanMap.get(span.parentSpanId)!;
if (parentSpan.service !== span.service) {
const edgeKey = `${parentSpan.service}->${span.service}`;
const currentEdge = edgeMap.get(edgeKey) || {
source: parentSpan.service,
target: span.service,
count: 0,
totalDur: 0,
errors: 0,
};
currentEdge.count += 1;
currentEdge.totalDur += span.duration;
if (span.hasError) currentEdge.errors += 1;
edgeMap.set(edgeKey, currentEdge);
}
}
});
const uniqueServices = Array.from(serviceStats.keys());
// 4. Determine layer depth for each service (Topological ordering)
const inDegree = new Map<string, number>();
uniqueServices.forEach((s) => inDegree.set(s, 0));
edgeMap.forEach((edge) => {
inDegree.set(edge.target, (inDegree.get(edge.target) || 0) + 1);
});
const layers = new Map<string, number>();
// Roots have 0 in-degree
const queue: string[] = [];
uniqueServices.forEach((s) => {
if ((inDegree.get(s) || 0) === 0) {
layers.set(s, 0);
queue.push(s);
}
});
// If there's a cycle or no zero-in-degree root, fallback to first service
if (queue.length === 0 && uniqueServices.length > 0) {
layers.set(uniqueServices[0], 0);
queue.push(uniqueServices[0]);
}
while (queue.length > 0) {
const current = queue.shift()!;
const currentLayer = layers.get(current) || 0;
edgeMap.forEach((edge) => {
if (edge.source === current) {
const targetLayer = Math.max(layers.get(edge.target) || 0, currentLayer + 1);
layers.set(edge.target, targetLayer);
if (!queue.includes(edge.target)) {
queue.push(edge.target);
}
}
});
}
// Ensure all services have a layer
uniqueServices.forEach((s) => {
if (!layers.has(s)) layers.set(s, 0);
});
// Group services by layer
const layerGroups = new Map<number, string[]>();
uniqueServices.forEach((s) => {
const l = layers.get(s) || 0;
const group = layerGroups.get(l) || [];
group.push(s);
layerGroups.set(l, group);
});
const totalLayers = Math.max(1, layerGroups.size);
const nodeWidth = 200;
const nodeHeight = 84;
const xGap = 160;
const yGap = 36;
let maxNodesInLayer = 1;
layerGroups.forEach((group) => {
if (group.length > maxNodesInLayer) maxNodesInLayer = group.length;
});
const canvasWidth = Math.max(700, totalLayers * (nodeWidth + xGap));
const canvasHeight = Math.max(300, maxNodesInLayer * (nodeHeight + yGap) + 60);
// Compute positions
const computedNodes: ServiceNode[] = [];
const nodePosMap = new Map<string, { x: number; y: number }>();
Array.from(layerGroups.keys())
.sort((a, b) => a - b)
.forEach((layerIdx) => {
const group = layerGroups.get(layerIdx) || [];
const layerX = 40 + layerIdx * (nodeWidth + xGap);
const groupHeight = group.length * nodeHeight + (group.length - 1) * yGap;
const startY = Math.max(40, (canvasHeight - groupHeight) / 2);
group.forEach((serviceName, idxInGroup) => {
const layerY = startY + idxInGroup * (nodeHeight + yGap);
const stats = serviceStats.get(serviceName)!;
computedNodes.push({
id: serviceName,
name: serviceName,
spanCount: stats.spanCount,
errorCount: stats.errorCount,
totalDuration: stats.totalDuration,
layer: layerIdx,
x: layerX,
y: layerY,
});
nodePosMap.set(serviceName, { x: layerX, y: layerY });
});
});
// Computed edges with coordinates
const computedEdges: ServiceEdge[] = [];
edgeMap.forEach((edge, key) => {
computedEdges.push({
id: key,
source: edge.source,
target: edge.target,
callCount: edge.count,
avgDuration: edge.totalDur / edge.count,
hasError: edge.errors > 0,
});
});
return {
nodes: computedNodes,
edges: computedEdges,
width: canvasWidth,
height: canvasHeight,
};
}, [spans]);
if (nodes.length === 0) {
return (
<div className="py-12 text-center text-sm text-slate-500">
No service nodes available to visualize.
</div>
);
}
const nodeMap = new Map(nodes.map((n) => [n.id, n]));
return (
<div className="relative overflow-hidden rounded-2xl border border-white/10 bg-slate-950/60 p-4 shadow-2xl backdrop-blur-md">
<div className="mb-4 flex flex-wrap items-center justify-between gap-4 border-b border-white/5 pb-3">
<div className="flex items-center gap-2">
<Layers className="h-4 w-4 text-sky-400" />
<span className="text-xs font-bold uppercase tracking-widest text-slate-300">
Service Call Graph ({nodes.length} Services, {edges.length} Cross-Service Links)
</span>
</div>
<div className="flex items-center gap-2 text-[10px] text-slate-400">
<span className="inline-flex items-center gap-1">
<span className="h-2 w-2 rounded-full bg-emerald-400" /> Healthy
</span>
<span className="inline-flex items-center gap-1">
<span className="h-2 w-2 rounded-full bg-rose-400" /> Errors
</span>
{selectedService && (
<button
onClick={() => onSelectService?.(null)}
className="ml-2 rounded-lg bg-sky-500/20 px-2 py-0.5 text-sky-300 hover:bg-sky-500/30 transition-all font-semibold"
>
Clear filter ({selectedService})
</button>
)}
</div>
</div>
<div className="overflow-x-auto">
<svg
width={width}
height={height}
viewBox={`0 0 ${width} ${height}`}
className="mx-auto select-none"
>
<defs>
<marker
id="arrow-normal"
viewBox="0 0 10 10"
refX="8"
refY="5"
markerWidth="6"
markerHeight="6"
orient="auto-start-reverse"
>
<path d="M 0 1.5 L 8 5 L 0 8.5 z" fill="#38bdf8" />
</marker>
<marker
id="arrow-error"
viewBox="0 0 10 10"
refX="8"
refY="5"
markerWidth="6"
markerHeight="6"
orient="auto-start-reverse"
>
<path d="M 0 1.5 L 8 5 L 0 8.5 z" fill="#f43f5e" />
</marker>
<linearGradient id="edge-grad" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stopColor="#38bdf8" stopOpacity="0.8" />
<stop offset="100%" stopColor="#818cf8" stopOpacity="0.8" />
</linearGradient>
<linearGradient id="edge-error-grad" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stopColor="#f43f5e" stopOpacity="0.9" />
<stop offset="100%" stopColor="#fb7185" stopOpacity="0.9" />
</linearGradient>
</defs>
{/* Render Connecting Edges */}
{edges.map((edge) => {
const src = nodeMap.get(edge.source);
const tgt = nodeMap.get(edge.target);
if (!src || !tgt) return null;
const startX = src.x + 200;
const startY = src.y + 42;
const endX = tgt.x;
const endY = tgt.y + 42;
const dx = endX - startX;
const cpx1 = startX + dx * 0.45;
const cpy1 = startY;
const cpx2 = endX - dx * 0.45;
const cpy2 = endY;
const pathD = `M ${startX} ${startY} C ${cpx1} ${cpy1}, ${cpx2} ${cpy2}, ${endX} ${endY}`;
const midX = (startX + endX) / 2;
const midY = (startY + endY) / 2;
const isEdgeHighlighted =
hoveredNode === edge.source ||
hoveredNode === edge.target ||
selectedService === edge.source ||
selectedService === edge.target;
return (
<g key={edge.id} className="transition-opacity duration-300">
{/* Background path for hit testing */}
<path
d={pathD}
fill="none"
stroke="transparent"
strokeWidth={20}
className="cursor-pointer"
/>
{/* Visible animated curve */}
<path
d={pathD}
fill="none"
stroke={
edge.hasError
? "url(#edge-error-grad)"
: isEdgeHighlighted
? "#38bdf8"
: "rgba(56, 189, 248, 0.4)"
}
strokeWidth={isEdgeHighlighted ? 3 : 2}
strokeDasharray={edge.hasError ? "4 2" : undefined}
markerEnd={edge.hasError ? "url(#arrow-error)" : "url(#arrow-normal)"}
/>
{/* Edge Call Badge */}
<g transform={`translate(${midX - 35}, ${midY - 12})`}>
<rect
width={70}
height={22}
rx={6}
fill="#030712"
stroke={edge.hasError ? "rgba(244,63,94,0.4)" : "rgba(255,255,255,0.1)"}
strokeWidth={1}
/>
<text
x={35}
y={14}
textAnchor="middle"
fill={edge.hasError ? "#fb7185" : "#94a3b8"}
fontSize={9}
fontWeight="bold"
fontFamily="monospace"
>
{edge.callCount}x · {formatDuration(edge.avgDuration)}
</text>
</g>
</g>
);
})}
{/* Render Service Nodes */}
{nodes.map((node) => {
const isSelected = selectedService === node.name;
const isHovered = hoveredNode === node.name;
const hasError = node.errorCount > 0;
return (
<g
key={node.id}
transform={`translate(${node.x}, ${node.y})`}
onClick={() => onSelectService?.(isSelected ? null : node.name)}
onMouseEnter={() => setHoveredNode(node.name)}
onMouseLeave={() => setHoveredNode(null)}
className="cursor-pointer"
>
{/* Node Box */}
<rect
width={200}
height={84}
rx={16}
fill="#090d16"
stroke={
isSelected
? "#38bdf8"
: hasError
? "#f43f5e"
: isHovered
? "rgba(255, 255, 255, 0.3)"
: "rgba(255, 255, 255, 0.1)"
}
strokeWidth={isSelected ? 2.5 : hasError ? 2 : 1}
filter={isSelected ? "drop-shadow(0 0 12px rgba(56,189,248,0.4))" : undefined}
/>
{/* Left Accent Stripe */}
<rect
x={0}
y={0}
width={6}
height={84}
rx={3}
fill={hasError ? "#f43f5e" : "#38bdf8"}
/>
{/* Service Name Header */}
<g transform="translate(16, 24)">
<text
x={0}
y={0}
fill="#ffffff"
fontSize={13}
fontWeight="bold"
fontFamily="sans-serif"
>
{node.name.length > 20 ? `${node.name.slice(0, 18)}...` : node.name}
</text>
</g>
{/* Spans Count & Latency Pills */}
<g transform="translate(16, 44)">
<text x={0} y={10} fill="#64748b" fontSize={10} fontFamily="monospace">
Spans: <tspan fill="#e2e8f0">{node.spanCount}</tspan>
</text>
<text x={75} y={10} fill="#64748b" fontSize={10} fontFamily="monospace">
Time: <tspan fill="#38bdf8">{formatDuration(node.totalDuration)}</tspan>
</text>
</g>
{/* Status Badges */}
<g transform="translate(16, 68)">
{hasError ? (
<g>
<rect width={80} height={16} rx={4} fill="rgba(244,63,94,0.15)" />
<text
x={40}
y={11}
textAnchor="middle"
fill="#fb7185"
fontSize={9}
fontWeight="bold"
>
{node.errorCount} Error{node.errorCount > 1 ? "s" : ""}
</text>
</g>
) : (
<g>
<rect width={60} height={16} rx={4} fill="rgba(16,185,129,0.15)" />
<text
x={30}
y={11}
textAnchor="middle"
fill="#34d399"
fontSize={9}
fontWeight="bold"
>
Healthy
</text>
</g>
)}
{isSelected && (
<g transform="translate(90, 0)">
<rect width={55} height={16} rx={4} fill="rgba(56,189,248,0.2)" />
<text
x={27.5}
y={11}
textAnchor="middle"
fill="#38bdf8"
fontSize={9}
fontWeight="bold"
>
Active
</text>
</g>
)}
</g>
</g>
);
})}
</svg>
</div>
{nodes.length === 1 && (
<div className="mt-3 text-center text-xs text-slate-500">
This trace was executed inside a single service ({nodes[0].name}). No cross-service network calls were made.
</div>
)}
</div>
);
};
+112
View File
@@ -0,0 +1,112 @@
import { useState } from "react"
import { Check, Copy, ExternalLink, FileText, Globe } from "lucide-react"
import { useObservabilityData } from "../hooks/useObservabilityData"
const scriptSnippet = `<script src="https://otel.k6n.net/static/otel-rum.js"></script>
<script>
var rum = OtelRUM.init({
serviceName: "my-web-app",
baseUrl: "https://otel.k6n.net", // Target OTel collector (defaults to script origin)
});
// Optional: propagate trace context to your own API calls
fetch("/api/orders", { headers: rum.traceHeaders() });
// Optional: report a caught exception manually
try { risky(); } catch (err) { rum.captureException(err); }
// React render failures should be reported from an error boundary.
// componentDidCatch(error, info) {
// rum.captureException(error, { "react.component_stack": info.componentStack });
// }
</script>`
const CopyButton = ({ text }: { text: string }) => {
const [copied, setCopied] = useState(false)
return (
<button
onClick={async () => {
try {
await navigator.clipboard.writeText(text)
setCopied(true)
setTimeout(() => setCopied(false), 1500)
} catch {
// clipboard API unavailable; nothing to fall back to here
}
}}
className="flex items-center gap-1.5 rounded-lg border border-white/10 px-2.5 py-1 text-[10px] font-bold uppercase tracking-wider text-slate-400 hover:text-white hover:border-white/20 transition-all"
>
{copied ? <Check className="h-3 w-3 text-emerald-400" /> : <Copy className="h-3 w-3" />}
{copied ? "Copied" : "Copy"}
</button>
)
}
export const SetupPanel = () => {
const { setActiveTab } = useObservabilityData()
return (
<div className="space-y-6 max-w-3xl">
<div className="rounded-xl border border-white/10 bg-slate-950/40 p-5">
<div className="flex items-center justify-between gap-4">
<span className="inline-flex items-center gap-2 text-[10px] font-black uppercase tracking-[0.2em] text-slate-500">
<Globe className="h-3.5 w-3.5 text-violet-400" />
Browser instrumentation (otel-rum.js)
</span>
<CopyButton text={scriptSnippet} />
</div>
<p className="mt-2 text-[12px] text-slate-400">
Drop this into any page to capture uncaught errors, unhandled rejections and failed
resource loads as OTLP logs. Errors show up here under the{" "}
<span className="text-slate-200 font-semibold">Issues</span> tab tagged{" "}
<span className="text-violet-300 font-semibold">Frontend Error</span>, and correlate with
backend traces via <code className="text-sky-300">traceHeaders()</code>.
</p>
<pre className="mt-3 overflow-x-auto rounded-lg border border-white/5 bg-black/50 p-3 text-[11px] leading-relaxed text-slate-300">
<code>{scriptSnippet}</code>
</pre>
<div className="mt-4 flex items-center gap-3">
<a
href="/static/otel-rum.js"
target="_blank"
rel="noreferrer"
className="inline-flex items-center gap-1.5 text-[11px] font-semibold text-sky-400 hover:text-sky-300"
>
<ExternalLink className="h-3 w-3" />
View raw script at /static/otel-rum.js
</a>
<span className="text-slate-600">•</span>
<button
type="button"
onClick={() => setActiveTab("tab7")}
className="inline-flex items-center gap-1.5 text-[11px] font-semibold text-indigo-400 hover:text-indigo-300"
>
<Globe className="h-3 w-3" />
Open Frontend & RUM Explorer →
</button>
</div>
</div>
<div className="rounded-xl border border-white/10 bg-slate-950/40 p-5">
<span className="inline-flex items-center gap-2 text-[10px] font-black uppercase tracking-[0.2em] text-slate-500">
<FileText className="h-3.5 w-3.5 text-sky-400" />
Agent instructions
</span>
<p className="mt-2 text-[12px] text-slate-400">
A single doc an AI coding agent can fetch before touching this API - endpoints, query
params and response shapes for traces, logs, metrics and issues.
</p>
<a
href="/agents_example.md"
target="_blank"
rel="noreferrer"
className="mt-3 inline-flex items-center gap-1.5 text-[11px] font-semibold text-sky-400 hover:text-sky-300"
>
<ExternalLink className="h-3 w-3" />
Open /agents_example.md
</a>
</div>
</div>
)
}
export default SetupPanel
+344
View File
@@ -0,0 +1,344 @@
import React from "react";
import { useObservabilityData } from "../hooks/useObservabilityData";
import { useIssues } from "../hooks/useIssues";
import { useStatsAvailability } from "../hooks/useStats";
export const Sidebar: React.FC = () => {
const {
activeTab,
setActiveTab,
traces,
logs,
metrics,
isConnected,
uniqueServices,
serviceHealth,
selectedServices,
toggleService,
resetFilters,
} = useObservabilityData();
const { issues } = useIssues();
const statsAvailable = useStatsAvailability();
// Active open issues count
const openIssuesCount = React.useMemo(() => {
return issues.filter((i) => !i.status || i.status === "open").length;
}, [issues]);
const errorLogCount = React.useMemo(() => {
let errs = 0;
logs.forEach((log) => {
(log.scopeLogs ?? []).forEach((sl) => {
(sl.logRecords ?? []).forEach((r) => {
if ((r.severityNumber ?? 0) >= 17) errs += 1;
});
});
});
return errs;
}, [logs]);
const totalSpans = React.useMemo(() => {
let count = 0;
traces.forEach((t) => {
(t.resourceSpans ?? []).forEach((rs) => {
(rs.scopeSpans ?? []).forEach((ss) => {
count += (ss.spans ?? []).length;
});
});
});
return count;
}, [traces]);
const { frontendCount, frontendErrorCount } = React.useMemo(() => {
let count = 0;
let errors = 0;
logs.forEach((log) => {
(log.scopeLogs ?? []).forEach((sl) => {
(sl.logRecords ?? []).forEach((r) => {
const attrs = r.attributes ?? [];
const source = attrs.find((a) => a.key === "source")?.value.stringValue;
const isRum = sl.scope?.name === "otel-rum";
const vital = attrs.find((a) => a.key === "rum.vital")?.value.stringValue;
const isFrontend = source === "frontend" || isRum || Boolean(vital);
if (isFrontend) {
count++;
if ((r.severityNumber ?? 0) >= 17 || attrs.some((a) => a.key === "exception.type")) {
errors++;
}
}
});
});
});
return { frontendCount: count, frontendErrorCount: errors };
}, [logs]);
const navItems = [
{
tab: "tab0",
label: "Issues & Anomalies",
icon: "error",
badge: openIssuesCount > 0 ? `${openIssuesCount} active` : `${issues.length} total`,
badgeType: openIssuesCount > 0 ? ("error" as const) : ("outline" as const),
},
{
tab: "tab8",
label: "Similarity Patterns",
icon: "hub",
badge: undefined,
badgeType: "outline" as const,
},
{
tab: "tab9",
label: "Cluster Rollouts",
icon: "cloud_sync",
badge: undefined,
badgeType: "outline" as const,
},
{
tab: "tab1",
label: "Traces & Spans",
icon: "account_tree",
badge: totalSpans > 0 ? `${totalSpans} spans` : `${traces.length} traces`,
badgeType: totalSpans > 0 ? ("secondary" as const) : ("outline" as const),
},
{
tab: "tab3",
label: "Live Log Stream",
icon: "terminal",
badge: errorLogCount > 0 ? `${errorLogCount} err` : `${logs.length} logs`,
badgeType: errorLogCount > 0 ? ("error" as const) : ("secondary" as const),
},
{
tab: "tab2",
label: "Metrics & APM",
icon: "query_stats",
badge: `${metrics.length} metrics`,
badgeType: "outline" as const,
},
{
tab: "tab7",
label: "Frontend & RUM",
icon: "devices",
badge:
frontendCount > 0
? frontendErrorCount > 0
? `${frontendErrorCount} err`
: `${frontendCount} sig`
: "RUM",
badgeType: frontendErrorCount > 0 ? ("error" as const) : ("secondary" as const),
},
...(statsAvailable
? [
{
tab: "tab5",
label: "Historical Trends",
icon: "trending_up",
badge: "ClickHouse",
badgeType: "outline" as const,
},
]
: []),
{
tab: "tab6",
label: "Archive & Trap",
icon: "radar",
badge: "Deep Query",
badgeType: "secondary" as const,
},
{
tab: "tab4",
label: "Setup & Agent API",
icon: "build",
badge: "RUM / Doc",
badgeType: "outline" as const,
},
];
return (
<aside className="fixed left-0 top-0 h-full w-72 bg-surface-container-lowest z-50 flex flex-col justify-between select-none shadow-[0_1px_8px_rgba(0,0,0,0.04)] border-r border-surface-container/60">
<div className="flex flex-col flex-1 overflow-y-auto">
{/* Brand Header */}
<div className="h-14 px-4 flex items-center justify-between bg-surface-container-lowest border-b border-surface-container/40">
<div className="flex items-center gap-2.5">
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-primary-container to-secondary text-on-primary font-black text-sm shadow-md">
<span className="material-symbols-outlined text-[18px]">hub</span>
</div>
<div className="flex flex-col">
<div className="flex items-center gap-1.5">
<span className="font-headline font-semibold text-base tracking-tight text-on-surface leading-none">
HyperScope
</span>
<span className="font-mono text-[10px] px-1 py-0.5 rounded bg-surface-container-high text-primary-container leading-none uppercase font-bold">
OTel
</span>
</div>
<span className="font-mono text-[11px] text-outline mt-0.5 leading-none">
Realtime Observability
</span>
</div>
</div>
<div
className={`flex items-center gap-1 px-2 py-0.5 rounded-full font-mono text-[9px] font-bold uppercase tracking-wider ${
isConnected
? "bg-secondary-container/20 text-secondary border border-secondary-container/40"
: "bg-amber-500/15 text-amber-400 border border-amber-500/30 animate-pulse"
}`}
>
<span
className={`w-1.5 h-1.5 rounded-full ${
isConnected ? "bg-secondary animate-pulse" : "bg-amber-400"
}`}
/>
<span>{isConnected ? "LIVE" : "RECONNECT"}</span>
</div>
</div>
{/* Telemetry Core Navigation */}
<div className="px-3 mt-3">
<div className="px-2 pb-1.5 font-mono text-[10px] text-outline uppercase tracking-wider font-semibold">
Telemetry Views
</div>
<nav className="space-y-1">
{navItems.map((item) => {
const isActive = activeTab === item.tab;
return (
<button
key={item.tab}
type="button"
onClick={() => setActiveTab(item.tab)}
className={`w-full flex items-center justify-between px-2.5 py-2 rounded-lg transition-colors group text-left ${
isActive
? "bg-surface-container-high text-primary font-semibold shadow-sm"
: "text-on-surface-variant hover:bg-surface-container hover:text-on-surface"
}`}
>
<div className="flex items-center gap-2.5">
<span
className={`material-symbols-outlined text-[18px] transition-colors ${
isActive
? "text-primary-container"
: "text-outline group-hover:text-primary-container"
}`}
>
{item.icon}
</span>
<span className="text-xs font-body font-medium">{item.label}</span>
</div>
{item.badge && (
<span
className={`font-mono text-[10px] px-1.5 py-0.5 rounded font-medium ${
item.badgeType === "error"
? "bg-error-container text-on-error-container font-semibold"
: item.badgeType === "secondary"
? "bg-surface-container text-secondary"
: "bg-surface-container text-outline"
}`}
>
{item.badge}
</span>
)}
</button>
);
})}
</nav>
</div>
{/* Real Discovered Services Fleet List */}
<div className="px-3 mt-5 flex-1">
<div className="px-2 pb-1.5 flex items-center justify-between">
<span className="font-mono text-[10px] text-outline uppercase tracking-wider font-semibold">
Discovered Services ({uniqueServices.length})
</span>
{selectedServices.size > 0 && (
<button
type="button"
onClick={resetFilters}
className="font-mono text-[10px] text-primary-container hover:underline"
>
Clear
</button>
)}
</div>
<div className="space-y-1 font-mono text-xs">
{uniqueServices.length === 0 ? (
<div className="px-2.5 py-3 text-center rounded-lg bg-surface-container-low/50 text-[11px] text-outline italic">
Listening for service signals...
</div>
) : (
uniqueServices.map((service) => {
const isSelected = selectedServices.has(service);
const health = serviceHealth[service];
const hasErrors = health?.hasErrors || false;
const errorCount = health?.errorCount || 0;
return (
<button
key={service}
type="button"
onClick={() => toggleService(service)}
className={`w-full flex items-center justify-between px-2.5 py-1.5 rounded-lg text-left transition-colors ${
isSelected
? "bg-primary-container/20 text-primary-container border border-primary-container/40 font-semibold"
: "text-on-surface-variant hover:bg-surface-container hover:text-on-surface"
}`}
>
<div className="flex items-center gap-2 min-w-0 pr-1">
<span
className={`w-2 h-2 rounded-full shrink-0 ${
hasErrors
? "bg-error animate-pulse"
: isSelected
? "bg-primary-container"
: "bg-emerald-400/80"
}`}
title={hasErrors ? `${errorCount} errors in active window` : "Healthy"}
/>
<span className="truncate text-xs">{service}</span>
</div>
{hasErrors ? (
<span className="font-mono text-[10px] px-1.5 py-0.5 rounded bg-error-container text-on-error-container font-semibold shrink-0">
{errorCount} err
</span>
) : (
<span className="font-mono text-[10px] text-outline shrink-0">
{health?.totalSignals ?? 0}
</span>
)}
</button>
);
})
)}
</div>
</div>
</div>
{/* Receiver Endpoints & WebSocket Health Status */}
<div className="p-3 bg-surface-container-lowest border-t border-surface-container/40">
<div className="p-2.5 rounded-lg bg-surface-container-low flex flex-col gap-1.5 border border-surface-container/40 font-mono text-[11px]">
<div className="flex items-center justify-between">
<span className="text-outline">Receiver Ports</span>
<span className="text-on-surface font-semibold">4317 / 4318</span>
</div>
<div className="flex items-center justify-between">
<span className="text-outline">Web UI &amp; API</span>
<span className="text-on-surface font-semibold">:8080</span>
</div>
<div className="flex items-center justify-between pt-1 border-t border-surface-container/40">
<span className="text-outline">Live Pipeline</span>
<span
className={`font-semibold ${
isConnected ? "text-secondary" : "text-amber-400"
}`}
>
{isConnected ? "Connected" : "Reconnecting..."}
</span>
</div>
</div>
</div>
</aside>
);
};
@@ -0,0 +1,582 @@
import React, { useMemo, useState } from "react";
import type { LogEntry, LogRecord, OtelAttribute } from "../lib/types";
import { useObservabilityData } from "../hooks/useObservabilityData";
import { formatAttributeValue, getServiceName } from "../lib/otel-utils";
interface FlattenedRecord extends LogRecord {
scope?: string;
resourceAttributes: OtelAttribute[];
serviceName: string;
}
export const StructuredLogTerminal: React.FC<{ logs: LogEntry[] }> = ({ logs }) => {
const { setActiveTraceId, setActiveSpanId, setActiveTab, selectedServices, toggleService } =
useObservabilityData();
const [expandedRowId, setExpandedRowId] = useState<string | null>(null);
const [streamFilter, setStreamFilter] = useState("");
const [wrapText, setWrapText] = useState(false);
const [cleared, setCleared] = useState(false);
// Severity filters
const [selectedSeverities, setSelectedSeverities] = useState<Record<string, boolean>>({
FATAL: true,
ERROR: true,
WARN: true,
INFO: true,
DEBUG: false,
});
const toggleSeverity = (sev: string) => {
setSelectedSeverities((prev) => ({ ...prev, [sev]: !prev[sev] }));
};
// Flatten all log records
const allRecords: FlattenedRecord[] = useMemo(() => {
if (cleared) return [];
return logs.flatMap((log) =>
(log.scopeLogs ?? []).flatMap((scopeLog) =>
(scopeLog.logRecords ?? []).map((record) => {
const serviceName = getServiceName(
log.resource?.attributes ?? [],
record.attributes ?? [],
);
return {
...record,
scope: scopeLog.scope?.name,
resourceAttributes: log.resource?.attributes ?? [],
serviceName,
};
}),
),
);
}, [logs, cleared]);
// Counts by severity
const severityCounts = useMemo(() => {
const counts = { FATAL: 0, ERROR: 0, WARN: 0, INFO: 0, DEBUG: 0 };
allRecords.forEach((r) => {
const num = r.severityNumber ?? 0;
if (num >= 21) counts.FATAL++;
else if (num >= 17) counts.ERROR++;
else if (num >= 13) counts.WARN++;
else if (num >= 9) counts.INFO++;
else counts.DEBUG++;
});
return counts;
}, [allRecords]);
// Service facet counts
const serviceCounts = useMemo(() => {
const counts = new Map<string, number>();
allRecords.forEach((r) => {
counts.set(r.serviceName, (counts.get(r.serviceName) || 0) + 1);
});
return Array.from(counts.entries()).sort((a, b) => b[1] - a[1]);
}, [allRecords]);
// Real diagnostic heuristic calculated from actual logs
const errorHeuristic = useMemo(() => {
const errorRecords = allRecords.filter((r) => (r.severityNumber ?? 0) >= 17);
if (errorRecords.length === 0) {
return {
text: serviceCounts.length > 0
? `All ${serviceCounts.length} active service(s) nominal. 0 error logs recorded in current buffer.`
: "No log signals received yet. Waiting for OTLP logs...",
hasErrors: false,
};
}
const errByService: Record<string, number> = {};
errorRecords.forEach((r) => {
errByService[r.serviceName] = (errByService[r.serviceName] || 0) + 1;
});
const sorted = Object.entries(errByService).sort((a, b) => b[1] - a[1]);
const topService = sorted[0]?.[0] || "unknown";
const topCount = sorted[0]?.[1] || 0;
const pct = Math.round((topCount / errorRecords.length) * 100);
return {
text: `${pct}% of current ERROR logs (${topCount} of ${errorRecords.length}) originated in "${topService}".`,
hasErrors: true,
};
}, [allRecords, serviceCounts.length]);
// Filter records based on selected severities, services, and local stream search
const visibleRecords = useMemo(() => {
return allRecords.filter((r) => {
const num = r.severityNumber ?? 0;
let sevKey = "DEBUG";
if (num >= 21) sevKey = "FATAL";
else if (num >= 17) sevKey = "ERROR";
else if (num >= 13) sevKey = "WARN";
else if (num >= 9) sevKey = "INFO";
if (!selectedSeverities[sevKey]) return false;
if (streamFilter.trim()) {
const query = streamFilter.toLowerCase().trim();
const msg = (r.body?.stringValue ?? "").toLowerCase();
const srv = r.serviceName.toLowerCase();
const trace = (r.traceId ?? "").toLowerCase();
const attrs = (r.attributes ?? []).some(
(a) =>
a.key.toLowerCase().includes(query) ||
formatAttributeValue(a.value).toLowerCase().includes(query),
);
if (!msg.includes(query) && !srv.includes(query) && !trace.includes(query) && !attrs) {
return false;
}
}
return true;
});
}, [allRecords, selectedSeverities, streamFilter]);
const copyLogStream = () => {
if (visibleRecords.length === 0) return;
const excerpt = visibleRecords
.slice(0, 50)
.map((r) => {
const time = new Date(Number(BigInt(r.timeUnixNano || "0") / 1_000_000n)).toISOString();
return `[${time}] ${r.severityText || "LOG"} ${r.serviceName}: ${r.body?.stringValue ?? ""}`;
})
.join("\n");
navigator.clipboard.writeText(excerpt);
alert("Copied visible log excerpt to clipboard.");
};
const navigateToTraceSpan = (traceId: string, spanId?: string) => {
setActiveTraceId(traceId);
if (spanId) setActiveSpanId(spanId);
setActiveTab("tab1");
};
const formatIsoTimestamp = (nanoStr?: string) => {
if (!nanoStr) return "—";
const millis = Number(BigInt(nanoStr) / 1_000_000n);
return new Date(millis).toISOString();
};
return (
<div className="grid grid-cols-1 lg:grid-cols-12 gap-4 items-start font-body">
{/* LEFT PANEL: Faceted Filter Breakdown (3 Cols) */}
<aside className="lg:col-span-3 flex flex-col gap-4">
{/* Severity Breakdown Card */}
<div className="bg-surface-container-lowest p-4 rounded-xl shadow-md flex flex-col gap-2 border border-surface-container/50">
<div className="flex items-center justify-between pb-1 border-b border-surface-container/40">
<span className="font-mono text-[10px] text-outline uppercase tracking-wider font-semibold">
Severity Breakdown
</span>
<span className="font-mono text-xs text-outline">{allRecords.length} Total</span>
</div>
<div className="space-y-1">
{/* FATAL */}
<label className="flex items-center justify-between p-1.5 rounded-lg bg-surface-container-low hover:bg-surface-container transition-colors cursor-pointer group">
<div className="flex items-center gap-2">
<input
type="checkbox"
checked={selectedSeverities.FATAL}
onChange={() => toggleSeverity("FATAL")}
className="rounded bg-surface-container-high border-outline text-error focus:ring-0"
/>
<span className="font-mono text-xs text-error font-bold tracking-tight">FATAL</span>
</div>
<div className="flex items-center gap-1.5">
<span className="font-mono text-[10px] px-1.5 py-0.5 rounded bg-error-container text-on-error-container font-semibold">
{severityCounts.FATAL}
</span>
<span className="w-1.5 h-1.5 rounded-full bg-error" />
</div>
</label>
{/* ERROR */}
<label className="flex items-center justify-between p-1.5 rounded-lg bg-surface-container-low hover:bg-surface-container transition-colors cursor-pointer group">
<div className="flex items-center gap-2">
<input
type="checkbox"
checked={selectedSeverities.ERROR}
onChange={() => toggleSeverity("ERROR")}
className="rounded bg-surface-container-high border-outline text-error focus:ring-0"
/>
<span className="font-mono text-xs text-error font-medium">ERROR</span>
</div>
<div className="flex items-center gap-1.5">
<span className="font-mono text-[10px] px-1.5 py-0.5 rounded bg-error-container text-on-error-container font-semibold">
{severityCounts.ERROR}
</span>
<span className="w-1.5 h-1.5 rounded-full bg-error" />
</div>
</label>
{/* WARN */}
<label className="flex items-center justify-between p-1.5 rounded-lg bg-surface-container-low hover:bg-surface-container transition-colors cursor-pointer group">
<div className="flex items-center gap-2">
<input
type="checkbox"
checked={selectedSeverities.WARN}
onChange={() => toggleSeverity("WARN")}
className="rounded bg-surface-container-high border-outline text-primary-fixed-dim focus:ring-0"
/>
<span className="font-mono text-xs text-primary-fixed-dim">WARN</span>
</div>
<span className="font-mono text-[10px] px-1.5 py-0.5 rounded bg-surface-container-high text-on-surface font-medium">
{severityCounts.WARN}
</span>
</label>
{/* INFO */}
<label className="flex items-center justify-between p-1.5 rounded-lg bg-surface-container-low hover:bg-surface-container transition-colors cursor-pointer group">
<div className="flex items-center gap-2">
<input
type="checkbox"
checked={selectedSeverities.INFO}
onChange={() => toggleSeverity("INFO")}
className="rounded bg-surface-container-high border-outline text-secondary focus:ring-0"
/>
<span className="font-mono text-xs text-secondary">INFO</span>
</div>
<span className="font-mono text-[10px] px-1.5 py-0.5 rounded bg-surface-container-high text-on-surface-variant">
{severityCounts.INFO}
</span>
</label>
{/* DEBUG */}
<label className="flex items-center justify-between p-1.5 rounded-lg bg-surface-container-low hover:bg-surface-container transition-colors cursor-pointer group">
<div className="flex items-center gap-2">
<input
type="checkbox"
checked={selectedSeverities.DEBUG}
onChange={() => toggleSeverity("DEBUG")}
className="rounded bg-surface-container-high border-outline text-outline focus:ring-0"
/>
<span className="font-mono text-xs text-outline">DEBUG</span>
</div>
<span className="font-mono text-[10px] px-1.5 py-0.5 rounded bg-surface-container-high text-outline">
{severityCounts.DEBUG}
</span>
</label>
</div>
</div>
{/* Service Facet Card */}
<div className="bg-surface-container-lowest p-4 rounded-xl shadow-md flex flex-col gap-2 border border-surface-container/50">
<div className="flex items-center justify-between pb-1 border-b border-surface-container/40">
<span className="font-mono text-[10px] text-outline uppercase tracking-wider font-semibold">
Service Name
</span>
<span className="material-symbols-outlined text-outline text-[16px]">filter_alt</span>
</div>
<div className="space-y-1 font-mono text-xs">
{serviceCounts.slice(0, 6).map(([serviceName, count]) => {
const isSelected = selectedServices.has(serviceName);
return (
<div
key={serviceName}
onClick={() => toggleService(serviceName)}
className={`flex items-center justify-between px-2.5 py-1.5 rounded-lg cursor-pointer transition-colors ${
isSelected
? "bg-surface-container text-primary-container font-semibold"
: "hover:bg-surface-container text-on-surface-variant"
}`}
>
<div className="flex items-center gap-2 min-w-0">
<span
className={`w-1.5 h-1.5 rounded-full shrink-0 ${
isSelected ? "bg-primary-container" : "bg-outline"
}`}
/>
<span className="truncate">{serviceName}</span>
</div>
<span className="font-mono text-[10px] text-outline">{count}</span>
</div>
);
})}
{serviceCounts.length === 0 && (
<p className="text-center py-2 text-outline text-xs italic">No services detected</p>
)}
</div>
</div>
{/* AI Root Cause Diagnostic Hint Box */}
<div className="bg-surface-container-low p-4 rounded-xl shadow-md flex flex-col gap-1.5 border border-surface-container/60">
<div className="flex items-center gap-2 text-primary-container">
<span className="material-symbols-outlined text-[18px]">psychology</span>
<span className="font-headline font-semibold text-xs leading-none">
Diagnostic Heuristic
</span>
</div>
<p className="font-body text-xs text-on-surface-variant leading-relaxed">
{errorHeuristic.text}
</p>
<div className="pt-1 flex items-center justify-between">
<button
onClick={() => {
setSelectedSeverities({ FATAL: true, ERROR: true, WARN: false, INFO: false, DEBUG: false });
}}
className="font-mono text-[10px] text-primary-container hover:underline uppercase flex items-center gap-1 font-semibold"
>
<span>Filter Only Error Signatures</span>
<span className="material-symbols-outlined text-[14px]">arrow_forward</span>
</button>
</div>
</div>
</aside>
{/* RIGHT PANEL: High-Density Structured Live Log Stream (9 Cols) */}
<main className="lg:col-span-9 flex flex-col bg-surface-container-lowest rounded-xl shadow-md overflow-hidden border border-surface-container/50">
{/* Stream Controls Bar */}
<div className="flex flex-wrap items-center justify-between gap-2 p-2.5 bg-surface-container-low border-b border-surface-container/40">
<div className="flex items-center gap-2 flex-1 min-w-[240px]">
<div className="relative flex-1">
<span className="material-symbols-outlined absolute left-2.5 top-1/2 -translate-y-1/2 text-outline text-[16px] pointer-events-none">
filter_list
</span>
<input
type="text"
value={streamFilter}
onChange={(e) => setStreamFilter(e.target.value)}
placeholder="Filter current buffer (e.g. 'timeout', 'status:504')..."
className="w-full h-7 pl-8 pr-3 rounded bg-surface-container text-on-surface font-mono text-xs placeholder:text-outline focus:outline-none focus:bg-surface-container-high transition-colors border border-surface-container/50"
/>
</div>
</div>
<div className="flex items-center gap-2">
<label className="flex items-center gap-1.5 font-mono text-xs text-outline cursor-pointer select-none">
<input
type="checkbox"
checked={wrapText}
onChange={(e) => setWrapText(e.target.checked)}
className="rounded bg-surface-container border-outline text-primary-container focus:ring-0"
/>
<span>Wrap Text</span>
</label>
<div className="h-3 w-px bg-surface-container-high" />
<button
onClick={() => setCleared(true)}
className="text-outline hover:text-on-surface p-1 rounded hover:bg-surface-container transition-colors"
title="Clear Buffer"
>
<span className="material-symbols-outlined text-[18px]">clear_all</span>
</button>
<button
onClick={copyLogStream}
className="text-outline hover:text-on-surface p-1 rounded hover:bg-surface-container transition-colors"
title="Copy Visible Buffer"
>
<span className="material-symbols-outlined text-[18px]">content_copy</span>
</button>
</div>
</div>
{/* Log Terminal Headers */}
<div className="grid grid-cols-12 px-4 py-1.5 bg-surface-container text-outline font-mono uppercase tracking-wider select-none text-[11px] border-b border-surface-container/60">
<div className="col-span-2">Timestamp (UTC)</div>
<div className="col-span-1">Level</div>
<div className="col-span-2">Service &amp; Trace</div>
<div className="col-span-7">Log Message &amp; Structured Payload</div>
</div>
{/* Realtime Log Entries List */}
<div className="divide-y divide-surface-container-high/40 font-mono text-xs max-h-[640px] overflow-y-auto select-text">
{visibleRecords.length === 0 ? (
<div className="p-8 text-center text-outline text-xs">
{cleared
? "Stream buffer cleared. Waiting for new telemetry records..."
: "No log records match the current filters."}
</div>
) : (
visibleRecords.map((record) => {
const num = record.severityNumber ?? 0;
let sevLabel = record.severityText?.toUpperCase() || "INFO";
let sevClass = "bg-surface-container-high text-secondary";
if (num >= 21) {
sevLabel = "FATAL";
sevClass = "bg-error text-on-error font-extrabold";
} else if (num >= 17) {
sevLabel = "ERROR";
sevClass = "bg-error-container text-on-error-container font-bold";
} else if (num >= 13) {
sevLabel = "WARN";
sevClass = "bg-surface-container-high text-primary-fixed-dim font-medium";
}
const rowId = record.uid;
const isExpanded = expandedRowId === rowId;
const isoTime = formatIsoTimestamp(record.timeUnixNano);
const bodyMessage = record.body?.stringValue ?? "(empty body)";
return (
<div
key={rowId}
className="flex flex-col bg-surface-container-lowest hover:bg-surface-container/30 transition-colors"
>
<div
onClick={() => setExpandedRowId(isExpanded ? null : rowId)}
className="grid grid-cols-12 px-4 py-2 items-start cursor-pointer group"
>
<div className="col-span-2 text-outline whitespace-nowrap pt-0.5 text-[11px]">
{isoTime}
</div>
<div className="col-span-1">
<span
className={`font-mono text-[10px] px-1.5 py-0.5 rounded tracking-tight ${sevClass}`}
>
{sevLabel}
</span>
</div>
<div className="col-span-2 flex flex-col gap-0.5 min-w-0 pr-2">
<span className="text-primary truncate font-medium">{record.serviceName}</span>
{record.traceId && (
<div className="flex items-center gap-1">
<span className="material-symbols-outlined text-[12px] text-primary-container">
link
</span>
<button
onClick={(e) => {
e.stopPropagation();
navigateToTraceSpan(record.traceId!, record.spanId);
}}
className="text-primary-container hover:underline truncate font-mono text-[10px]"
>
{record.traceId.slice(0, 12)}
</button>
</div>
)}
</div>
<div className="col-span-7 flex items-start justify-between min-w-0">
<div
className={`text-on-surface font-normal ${
wrapText ? "break-words" : "truncate"
}`}
>
{num >= 17 && (
<span className="text-error font-medium mr-1">Exception:</span>
)}
{bodyMessage}
</div>
<span className="material-symbols-outlined text-outline text-[16px] group-hover:text-on-surface transition-transform shrink-0 ml-2">
{isExpanded ? "unfold_less" : "unfold_more"}
</span>
</div>
</div>
{/* Structured Payload Drawer (Expanded) */}
{isExpanded && (
<div className="px-4 py-3 bg-surface-container-low text-xs flex flex-col gap-2 border-t border-surface-container">
<div className="flex items-center justify-between border-b border-surface-container pb-1">
<span className="font-mono text-[10px] text-outline uppercase tracking-wider">
OpenTelemetry Attributes &amp; Resource Scope
</span>
{record.traceId && (
<button
onClick={() => {
navigator.clipboard.writeText(
`trace:${record.traceId} span:${record.spanId || ""}`,
);
alert("Copied trace context to clipboard.");
}}
className="flex items-center gap-1 font-mono text-[10px] text-primary-container hover:text-primary transition-colors"
>
<span className="material-symbols-outlined text-[14px]">copy_all</span>
<span>Copy Trace Context</span>
</button>
)}
</div>
{/* Attributes Grid */}
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-2 font-mono text-xs">
{record.spanId && (
<div className="p-1.5 rounded bg-surface-container flex flex-col">
<span className="text-outline text-[10px] uppercase">span_id</span>
<span className="text-primary-container truncate font-medium">
{record.spanId}
</span>
</div>
)}
{record.traceId && (
<div className="p-1.5 rounded bg-surface-container flex flex-col">
<span className="text-outline text-[10px] uppercase">trace_id</span>
<span className="text-on-surface truncate">{record.traceId}</span>
</div>
)}
{record.scope && (
<div className="p-1.5 rounded bg-surface-container flex flex-col">
<span className="text-outline text-[10px] uppercase">scope</span>
<span className="text-secondary truncate">{record.scope}</span>
</div>
)}
<div className="p-1.5 rounded bg-surface-container flex flex-col">
<span className="text-outline text-[10px] uppercase">service.name</span>
<span className="text-primary truncate">{record.serviceName}</span>
</div>
</div>
{/* Raw JSON block */}
<pre className="p-2.5 rounded bg-surface-container text-on-surface overflow-x-auto text-[11px] leading-relaxed border border-surface-container-high/40">
<code>
{JSON.stringify(
{
timestamp: isoTime,
severity: sevLabel,
service_name: record.serviceName,
trace_id: record.traceId,
span_id: record.spanId,
body: bodyMessage,
attributes: Object.fromEntries(
(record.attributes ?? []).map((a) => [
a.key,
formatAttributeValue(a.value),
]),
),
resource_attributes: Object.fromEntries(
(record.resourceAttributes ?? []).map((a) => [
a.key,
formatAttributeValue(a.value),
]),
),
},
null,
2,
)}
</code>
</pre>
</div>
)}
</div>
);
})
)}
</div>
{/* Log Terminal Footer Status */}
<div className="flex items-center justify-between px-4 py-2 bg-surface-container-low text-outline font-mono text-xs border-t border-surface-container">
<div className="flex items-center gap-3">
<span>
Showing {visibleRecords.length} of {allRecords.length} matched events
</span>
<span className="h-3 w-px bg-surface-container-high" />
<span>Buffer: Healthy</span>
</div>
<div className="flex items-center gap-2">
<span className="w-1.5 h-1.5 rounded-full bg-secondary" />
<span className="text-on-surface">Auto-scroll: LIVE STREAM</span>
</div>
</div>
</main>
</div>
);
};
+513
View File
@@ -0,0 +1,513 @@
import { useCallback, useState } from "react"
import * as Tabs from "@radix-ui/react-tabs"
import {
Activity,
AlertOctagon,
LineChart,
ScrollText,
AlertTriangle,
Info,
CheckCircle2,
ChevronDown,
ChevronUp,
TrendingUp,
Wrench,
Radar,
Globe,
GitBranch,
CloudCog,
} from "lucide-react"
import type { LucideIcon } from "lucide-react"
import { MetricsVisualizer } from "./MetricsVisualizer"
import TraceVisualization from "./TraceVisualization"
import { IssuesVisualizer } from "./IssuesVisualizer"
import { SetupPanel } from "./SetupPanel"
import { SearchAndWatcher } from "./SearchAndWatcher"
import { FrontendVisualizer } from "./FrontendVisualizer"
import { ViewErrorBoundary } from "./ViewErrorBoundary"
import { useObservabilityData } from "../hooks/useObservabilityData"
import { useIssues } from "../hooks/useIssues"
import { useStatsAvailability } from "../hooks/useStats"
import { TrendsVisualizer } from "./TrendsVisualizer"
import { authFetch } from "../lib/auth"
import type { Issue, LogEntry, OtelAttribute } from "../lib/types"
import { formatAttributeValue, getServicePalette, getServiceName } from "../lib/otel-utils"
import { TelemetryFilters } from "./TelemetryFilters"
import { PatternsVisualizer } from "./PatternsVisualizer"
import { RolloutsVisualizer } from "./RolloutsVisualizer"
type SeverityMeta = {
baseLabel: string
gradient: string
ring: string
icon: LucideIcon
}
const severityStyles: Record<string, SeverityMeta> = {
ERROR: {
baseLabel: "Error",
gradient: "from-rose-500 via-red-500 to-orange-400",
ring: "ring-rose-400/30",
icon: AlertTriangle,
},
WARN: {
baseLabel: "Warning",
gradient: "from-amber-400 via-orange-400 to-rose-400",
ring: "ring-amber-300/30",
icon: AlertTriangle,
},
INFO: {
baseLabel: "Info",
gradient: "from-sky-500 via-blue-500 to-violet-500",
ring: "ring-sky-400/30",
icon: Info,
},
DEBUG: {
baseLabel: "Debug",
gradient: "from-emerald-400 via-teal-400 to-cyan-400",
ring: "ring-emerald-300/30",
icon: CheckCircle2,
},
}
const fallbackSeverity: SeverityMeta = {
baseLabel: "Log",
gradient: "from-slate-600 via-slate-500 to-slate-400",
ring: "ring-white/20",
icon: Info,
}
const getSeverityPresentation = (
severityText?: string,
severityNumber?: number,
) => {
if (severityText) {
const normalized = severityText.toUpperCase()
if (severityStyles[normalized]) {
const meta = severityStyles[normalized]
return { ...meta, display: meta.baseLabel }
}
return { ...fallbackSeverity, display: severityText }
}
if (severityNumber !== undefined) {
if (severityNumber >= 17) {
const meta = severityStyles.ERROR
return { ...meta, display: meta.baseLabel }
}
if (severityNumber >= 13) {
const meta = severityStyles.WARN
return { ...meta, display: meta.baseLabel }
}
if (severityNumber >= 9) {
const meta = severityStyles.INFO
return { ...meta, display: meta.baseLabel }
}
const meta = severityStyles.DEBUG
return { ...meta, display: meta.baseLabel }
}
return { ...fallbackSeverity, display: fallbackSeverity.baseLabel }
}
// tabItems moved inside component to access live data
export const LogEntryView = ({ log }: { log: LogEntry }) => {
const { setActiveTraceId, setActiveSpanId, setActiveTab } = useObservabilityData()
const [expandedId, setExpandedId] = useState<string | null>(null)
const logRecords = (log.scopeLogs ?? []).flatMap((scopeLog) =>
(scopeLog.logRecords ?? []).map((record) => ({
...record,
scope: scopeLog.scope?.name,
resourceAttributes: log.resource?.attributes ?? [],
})),
)
const getRecordServiceName = (recordAttributes: OtelAttribute[]) => {
return getServiceName(log.resource?.attributes ?? [], recordAttributes)
}
const navigateToSpan = (traceId: string, spanId?: string) => {
setActiveTraceId(traceId)
if (spanId) setActiveSpanId(spanId)
setActiveTab("tab1")
}
const formatTimestamp = (timeUnixNano: string) => {
const timestamp = (parseInt(timeUnixNano ?? "0") || 0) / 1e9
const date = new Date(timestamp * 1000)
return date.toLocaleTimeString([], { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit', fractionalSecondDigits: 3 })
}
if (logRecords.length === 0) {
return null
}
return (
<div className="flex flex-col gap-1.5">
{logRecords.map((record) => {
const severity = getSeverityPresentation(
record.severityText,
record.severityNumber,
)
const bodyMessage = record.body?.stringValue ?? "(no body)"
const recordKey = record.uid
const isExpanded = expandedId === recordKey
return (
<div
key={recordKey}
className="group relative flex flex-col overflow-hidden rounded-xl border border-white/5 bg-slate-950/40 transition-all hover:border-white/10 hover:bg-slate-900/50"
>
{/* The Main Log Row */}
<div
onClick={() => setExpandedId(isExpanded ? null : recordKey)}
className="flex cursor-pointer flex-wrap items-center gap-x-4 gap-y-2 p-2.5 font-mono text-[12px]"
>
<div className={`absolute left-0 top-0 bottom-0 w-1 bg-gradient-to-b ${severity.gradient} opacity-40 transition-opacity group-hover:opacity-100`} />
{/* Timestamp */}
<span className="shrink-0 text-slate-500 whitespace-nowrap ml-1">
{formatTimestamp(record.timeUnixNano)}
</span>
{/* Service Badge */}
{(() => {
const sName = getRecordServiceName(record.attributes ?? [])
const palette = getServicePalette(sName)
return (
<span className={`shrink-0 rounded px-1.5 py-0.5 text-[10px] font-bold uppercase tracking-wider ${palette.bg} ${palette.text} ${palette.border} border`}>
{sName}
</span>
)
})()}
{/* Severity Badge */}
<span className={`shrink-0 rounded px-1.5 py-0.5 text-[10px] font-bold uppercase tracking-wider bg-gradient-to-r ${severity.gradient} text-slate-950`}>
{severity.display}
</span>
{/* Log Message */}
<span className="flex-1 min-w-[300px] break-all text-slate-200">
{bodyMessage}
</span>
{/* Action Icons */}
<div className="flex items-center gap-2">
{record.traceId && (
<button
onClick={(e) => {
e.stopPropagation()
navigateToSpan(record.traceId!, record.spanId)
}}
className="group/trace flex items-center gap-1.5 rounded-full bg-sky-500/10 border border-sky-400/20 px-2.5 py-0.5 text-[10px] font-bold text-sky-400 hover:bg-sky-500/20 hover:border-sky-400/30 transition-all"
>
<Activity className="h-2.5 w-2.5 group-hover/trace:animate-pulse" />
TRACE:{record.traceId.slice(0, 6)}
</button>
)}
{isExpanded ? <ChevronUp className="h-3 w-3 text-slate-500" /> : <ChevronDown className="h-3 w-3 text-slate-500" />}
</div>
</div>
{/* Expansion Content */}
{isExpanded && (
<div className="border-t border-white/5 bg-black/40 p-4 text-[12px]">
<div className="grid gap-6 md:grid-cols-2">
{(record.attributes ?? []).length > 0 && (
<div className="space-y-2">
<p className="text-[10px] font-bold uppercase tracking-widest text-slate-500">Attributes</p>
<div className="space-y-1">
{(record.attributes ?? []).map(attr => (
<div key={attr.key} className="flex gap-2">
<span className="text-sky-400 shrink-0">{attr.key}:</span>
<span className="text-slate-300 break-all">{formatAttributeValue(attr.value)}</span>
</div>
))}
</div>
</div>
)}
{(record.resourceAttributes ?? []).length > 0 && (
<div className="space-y-2">
<p className="text-[10px] font-bold uppercase tracking-widest text-slate-500">Resource</p>
<div className="space-y-1">
{(record.resourceAttributes ?? []).map(attr => (
<div key={attr.key} className="flex gap-2">
<span className="text-emerald-400 shrink-0">{attr.key}:</span>
<span className="text-slate-400 break-all">{formatAttributeValue(attr.value)}</span>
</div>
))}
</div>
</div>
)}
</div>
<div className="mt-4 pt-4 border-t border-white/5 flex flex-wrap items-center justify-between gap-4">
<div className="flex gap-4 text-[10px] text-slate-500">
<span>Scope: {record.scope || "unknown"}</span>
{record.spanId && <span>Span ID: {record.spanId}</span>}
{record.traceId && <span>Trace ID: {record.traceId}</span>}
</div>
{record.traceId && (
<button
onClick={() => navigateToSpan(record.traceId!, record.spanId)}
className="inline-flex items-center gap-2 rounded-xl bg-sky-500 px-4 py-2 text-[10px] font-black uppercase tracking-widest text-slate-950 transition-all hover:bg-sky-400 hover:scale-[1.02] active:scale-[0.98] shadow-lg shadow-sky-500/20"
>
<Activity className="h-3 w-3" />
Jump to Span in Trace
</button>
)}
</div>
</div>
)}
</div>
)
})}
</div>
)
}
import { StructuredLogTerminal } from "./StructuredLogTerminal"
const LogVisualizer = ({ logs }: { logs: LogEntry[] }) => {
return <StructuredLogTerminal logs={logs} />
}
export const TabsComponent = () => {
const { traces, logs, metrics, activeTab, setActiveTab } = useObservabilityData()
const issuesState = useIssues()
const [statusBusy, setStatusBusy] = useState(false)
// Trends tab only exists when the backend has ClickHouse-backed stats;
// null while the capability check is in flight.
const statsAvailable = useStatsAvailability()
// Triage action: PATCH the issue status, then refresh so counts/badges
// reflect the server state. Failures surface via the issues error banner.
const handleIssueStatusChange = useCallback(
async (issue: Issue, status: string) => {
setStatusBusy(true)
try {
const res = await authFetch(`/api/issues/${issue.id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ status }),
})
if (!res.ok) throw new Error(`HTTP ${res.status}`)
} catch (err) {
console.error("Failed to update issue status:", err)
} finally {
await issuesState.refresh()
setStatusBusy(false)
}
},
[issuesState],
)
const numberFormatter = new Intl.NumberFormat("en-US", {
notation: "compact",
maximumFractionDigits: 1,
})
const frontendCount = logs.reduce((acc, log) => {
return (
acc +
(log.scopeLogs ?? []).reduce((sAcc, sl) => {
return (
sAcc +
(sl.logRecords ?? []).filter((r) => {
const attrs = r.attributes ?? []
return (
attrs.some((a) => a.key === "source" && a.value?.stringValue === "frontend") ||
sl.scope?.name === "otel-rum" ||
attrs.some((a) => a.key === "rum.vital") ||
attrs.some((a) => a.key === "browser.url")
)
}).length
)
}, 0)
)
}, 0)
const tabItems = [
{
value: "tab0",
label: "Issues",
description: "Deduplicated errors",
Icon: AlertOctagon,
count: issuesState.issues.length
},
{
value: "tab8",
label: "Patterns",
description: "Deterministic similarity review",
Icon: GitBranch,
count: undefined as number | undefined,
},
{
value: "tab9",
label: "Rollouts",
description: "Cluster deploy correlation",
Icon: CloudCog,
count: undefined as number | undefined,
},
{
value: "tab1",
label: "Traces",
description: "Span timelines & focus",
Icon: Activity,
count: traces.length
},
{
value: "tab2",
label: "Metrics",
description: "Performance trends",
Icon: LineChart,
count: metrics.length
},
{
value: "tab3",
label: "Logs",
description: "System log streams",
Icon: ScrollText,
count: logs.length
},
{
value: "tab7",
label: "Frontend & RUM",
description: "Web Vitals & browser errors",
Icon: Globe,
count: frontendCount,
},
// Historical trends need the ClickHouse-backed stats endpoints.
...(statsAvailable
? [
{
value: "tab5",
label: "Trends",
description: "History graphs & release comparison",
Icon: TrendingUp,
count: undefined as number | undefined,
},
]
: []),
{
value: "tab6",
label: "Archive & Trap",
description: "Persistent storage search & signal trap",
Icon: Radar,
count: undefined,
},
{
value: "tab4",
label: "Setup",
description: "Instrument a new service",
Icon: Wrench,
count: undefined
},
]
return (
<div className="flex flex-col gap-6">
<TelemetryFilters />
<Tabs.Root
value={activeTab}
onValueChange={setActiveTab}
className="flex flex-col gap-4"
>
<Tabs.List
className="flex gap-8 border-b border-white/10 pb-2 mb-2 w-full overflow-x-auto"
aria-label="Telemetry explorer views"
>
{tabItems.map(({ value, label, Icon, count }) => (
<Tabs.Trigger
key={value}
value={value}
id={`tab-trigger-${value}`}
aria-label={`${label} tab`}
className="group relative flex items-center gap-2.5 pb-2 text-sm font-semibold text-slate-400 outline-none transition-colors hover:text-slate-200 data-[state=active]:text-white"
>
<div className="absolute -bottom-[2px] left-0 right-0 h-0.5 bg-sky-400 opacity-0 transition-opacity group-data-[state=active]:opacity-100" />
<Icon className="h-4 w-4 text-slate-500 group-data-[state=active]:text-sky-400" />
<span aria-hidden="true">{label}</span>
{count !== undefined && (
<span className="inline-flex rounded-full bg-slate-800 px-2 py-0.5 text-[10px] font-bold text-slate-300 group-data-[state=active]:bg-sky-500/20 group-data-[state=active]:text-sky-300">
{numberFormatter.format(count)}
</span>
)}
</Tabs.Trigger>
))}
</Tabs.List>
<Tabs.Content
className="outline-none py-2"
value="tab0"
>
{activeTab === "tab0" && (
<ViewErrorBoundary>
<IssuesVisualizer
issues={issuesState.issues}
loading={issuesState.loading}
error={issuesState.error}
refresh={issuesState.refresh}
onStatusChange={handleIssueStatusChange}
statusBusy={statusBusy}
/>
</ViewErrorBoundary>
)}
</Tabs.Content>
<Tabs.Content
className="outline-none py-2"
value="tab1"
>
{activeTab === "tab1" && <ViewErrorBoundary><TraceVisualization traces={traces} /></ViewErrorBoundary>}
</Tabs.Content>
<Tabs.Content className="outline-none py-2" value="tab8">
{activeTab === "tab8" && <ViewErrorBoundary><PatternsVisualizer /></ViewErrorBoundary>}
</Tabs.Content>
<Tabs.Content className="outline-none py-2" value="tab9">
{activeTab === "tab9" && <ViewErrorBoundary><RolloutsVisualizer /></ViewErrorBoundary>}
</Tabs.Content>
<Tabs.Content
className="outline-none py-2"
value="tab2"
>
{activeTab === "tab2" && <ViewErrorBoundary><MetricsVisualizer /></ViewErrorBoundary>}
</Tabs.Content>
<Tabs.Content
className="outline-none py-2"
value="tab3"
>
{activeTab === "tab3" && <ViewErrorBoundary><LogVisualizer logs={logs} /></ViewErrorBoundary>}
</Tabs.Content>
<Tabs.Content
className="outline-none py-2"
value="tab7"
>
{activeTab === "tab7" && <ViewErrorBoundary><FrontendVisualizer /></ViewErrorBoundary>}
</Tabs.Content>
<Tabs.Content
className="outline-none py-2"
value="tab4"
>
{activeTab === "tab4" && <ViewErrorBoundary><SetupPanel /></ViewErrorBoundary>}
</Tabs.Content>
{statsAvailable && (
<Tabs.Content
className="outline-none py-2"
value="tab5"
>
{activeTab === "tab5" && <ViewErrorBoundary><TrendsVisualizer /></ViewErrorBoundary>}
</Tabs.Content>
)}
<Tabs.Content
className="outline-none py-2"
value="tab6"
>
{activeTab === "tab6" && <ViewErrorBoundary><SearchAndWatcher /></ViewErrorBoundary>}
</Tabs.Content>
</Tabs.Root>
</div>
)
}
+124
View File
@@ -0,0 +1,124 @@
import { X, Activity, AlertTriangle } from "lucide-react"
import { useObservabilityData } from "../hooks/useObservabilityData"
import { getServicePalette } from "../lib/otel-utils"
export const TelemetryFilters = () => {
const {
selectedServices,
toggleService,
excludedAttributes,
removeExcludedValue,
clearAttributeKey,
uniqueServices,
filtersExpanded: isExpanded,
} = useObservabilityData()
const hasExcludedKeys = Object.keys(excludedAttributes).length > 0
if (!isExpanded) return null
return (
<div className="border-b border-white/10 pb-8 mb-6 animate-in fade-in slide-in-from-top-2 duration-300">
<div className="grid gap-8 lg:grid-cols-2">
{/* Service Multi-Select */}
<div className="space-y-4">
<div className="flex items-center gap-2">
<Activity className="h-3 w-3 text-slate-500" />
<span className="text-[10px] font-black uppercase tracking-[0.2em] text-slate-500">
Service Focus
</span>
</div>
<div className="flex flex-wrap gap-2.5">
{uniqueServices.map((service) => {
const isActive = selectedServices.has(service)
const palette = getServicePalette(service)
return (
<button
key={service}
type="button"
onClick={() => toggleService(service)}
className={`group relative inline-flex items-center gap-3 rounded-2xl border px-5 py-2.5 text-[10px] font-bold uppercase tracking-[0.2em] transition-all duration-300 ${isActive
? `border-transparent bg-gradient-to-br ${palette.gradient} text-slate-950 shadow-lg shadow-sky-500/20`
: "border-white/10 bg-slate-900/40 text-slate-400 hover:border-white/20 hover:text-white"
}`}
>
{!isActive && (
<span
className={`h-2 w-2 rounded-full bg-gradient-to-r ${palette.gradient} opacity-50 block`}
/>
)}
{service}
{isActive && (
<span className="ml-1 flex h-4 w-4 items-center justify-center rounded-full bg-black/10">
<X className="h-3 w-3" />
</span>
)}
</button>
)
})}
{uniqueServices.length === 0 && (
<span className="text-[11px] text-slate-600 italic font-medium px-1 underline decoration-white/5 underline-offset-4">
Listening for service signals...
</span>
)}
</div>
</div>
{/* Attribute Exclusions */}
<div className="space-y-4">
<div className="flex items-center gap-2">
<AlertTriangle className="h-3 w-3 text-slate-500" />
<span className="text-[10px] font-black uppercase tracking-[0.2em] text-slate-500">
Active Noise Suppression
</span>
</div>
<div className="flex flex-wrap gap-3">
{hasExcludedKeys ? (
Object.entries(excludedAttributes).map(([key, valueSet]) => (
<div
key={key}
className="flex flex-col gap-2 rounded-2xl border border-white/5 bg-slate-900/40 p-3 transition-all hover:border-white/10"
>
<div className="flex items-center justify-between mb-0.5">
<span className="text-[10px] font-bold text-sky-400 font-mono">
{key}
</span>
<button
type="button"
onClick={() => clearAttributeKey(key)}
className="text-[9px] font-black uppercase tracking-widest text-slate-600 hover:text-white transition-colors ml-4"
>
Remove All
</button>
</div>
<div className="flex flex-wrap gap-1.5">
{Array.from(valueSet).map((value) => (
<button
key={value}
type="button"
onClick={() => removeExcludedValue(key, value)}
className="group inline-flex items-center gap-2 rounded-full border border-white/5 bg-slate-950/80 px-2.5 py-0.5 text-[10px] text-slate-300 transition-all hover:border-rose-500/50 hover:text-rose-400"
>
<span className="max-w-[10rem] truncate">{value}</span>
<X className="h-2.5 w-2.5 text-slate-600 group-hover:text-rose-500" />
</button>
))}
</div>
</div>
))
) : (
<div className="flex flex-col items-center justify-center w-full py-4 border border-dashed border-white/5 rounded-3xl bg-slate-900/20">
<p className="text-[11px] text-slate-600 italic font-medium">
No active attribute exclusions.
</p>
<p className="text-[9px] text-slate-700 uppercase tracking-widest mt-1">
Click attributes in details to suppress chatty items
</p>
</div>
)}
</div>
</div>
</div>
</div>
)
}
+232
View File
@@ -0,0 +1,232 @@
import React, { useRef, useEffect } from "react";
import { useObservabilityData } from "../hooks/useObservabilityData";
import { useIssues } from "../hooks/useIssues";
import { currentUser, login, logout } from "../lib/auth";
export const TopHeader: React.FC = () => {
const user = currentUser();
const {
searchQuery,
setSearchQuery,
showErrorsOnly,
setShowErrorsOnly,
resetFilters,
setActiveTab,
timeWindow,
setTimeWindow,
isStreaming,
setIsStreaming,
bufferedCount,
flushBufferedStream,
minDurationMs,
setMinDurationMs,
} = useObservabilityData();
const { issues } = useIssues();
const searchInputRef = useRef<HTMLInputElement>(null);
const openIssuesCount = React.useMemo(() => {
return issues.filter((i) => !i.status || i.status === "open").length;
}, [issues]);
// ⌘K / Ctrl+K keyboard shortcut to focus search
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.key === "k") {
e.preventDefault();
searchInputRef.current?.focus();
}
};
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, []);
return (
<header className="fixed top-0 left-72 right-0 h-14 bg-surface-container-lowest/90 backdrop-blur-xl z-40 flex items-center justify-between px-6 shadow-[0_1px_8px_rgba(0,0,0,0.04)] border-b border-surface-container/60 font-body">
{/* Search and Time Range */}
<div className="flex items-center gap-3 flex-1 max-w-3xl">
{/* Global Search Bar */}
<div className="relative flex-1">
<span className="material-symbols-outlined absolute left-2.5 top-1/2 -translate-y-1/2 text-outline text-[18px] pointer-events-none">
search
</span>
<input
ref={searchInputRef}
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="⌘K search attributes, services, traceId, spanId, or log payload..."
className="w-full h-8 pl-8 pr-16 rounded-lg bg-surface-container-low text-on-surface font-mono text-xs placeholder:text-outline focus:outline-none focus:bg-surface-container transition-colors border border-surface-container/40"
/>
<div className="absolute right-2.5 top-1/2 -translate-y-1/2 flex items-center gap-0.5 pointer-events-none select-none">
<kbd className="font-mono text-[10px] text-outline px-1 rounded bg-surface-container-high border border-white/5">
⌘
</kbd>
<kbd className="font-mono text-[10px] text-outline px-1 rounded bg-surface-container-high border border-white/5">
K
</kbd>
</div>
</div>
{/* Real Time Window Selector */}
<div className="flex items-center bg-surface-container-low rounded-lg p-0.5 border border-surface-container/40 select-none">
<span className="material-symbols-outlined text-outline text-[14px] ml-1 mr-0.5">
schedule
</span>
{(["15m", "1h", "6h", "24h", "all"] as const).map((w) => (
<button
key={w}
type="button"
onClick={() => setTimeWindow(w)}
className={`px-2 py-0.5 font-mono text-[11px] rounded transition-colors ${
timeWindow === w
? "bg-surface-container text-primary-container font-bold shadow-sm"
: "text-outline hover:text-on-surface"
}`}
>
{w === "all" ? "All" : w}
</button>
))}
</div>
</div>
{/* Action Buttons & Streaming Controls */}
<div className="flex items-center gap-3">
{/* Real Streaming Pause/Resume Button */}
<button
type="button"
onClick={() => {
if (isStreaming) {
setIsStreaming(false);
} else {
setIsStreaming(true);
flushBufferedStream();
}
}}
className={`flex items-center gap-1.5 px-2.5 py-1 rounded-lg font-mono text-[11px] font-semibold transition-colors border ${
isStreaming
? "bg-surface-container-low text-secondary border-surface-container hover:bg-surface-container"
: "bg-amber-500/20 text-amber-300 border-amber-500/40 animate-pulse"
}`}
title={isStreaming ? "Pause stream to freeze buffer for inspection" : "Resume live telemetry tail"}
>
<span
className={`w-2 h-2 rounded-full ${
isStreaming ? "bg-secondary animate-pulse" : "bg-amber-400"
}`}
/>
<span>
{isStreaming
? "Live Tail"
: `Paused ${bufferedCount > 0 ? `(${bufferedCount})` : ""}`}
</span>
</button>
{/* All vs Errors Toggle */}
<div className="flex rounded-lg bg-surface-container-low p-0.5 border border-surface-container/40">
<button
type="button"
onClick={() => setShowErrorsOnly(false)}
className={`px-2.5 py-1 text-[10px] font-mono font-bold uppercase tracking-wider rounded transition-colors ${
!showErrorsOnly
? "bg-surface-container-high text-primary font-bold shadow-sm"
: "text-outline hover:text-on-surface"
}`}
>
All
</button>
<button
type="button"
onClick={() => setShowErrorsOnly(true)}
className={`flex items-center gap-1 px-2.5 py-1 text-[10px] font-mono font-bold uppercase tracking-wider rounded transition-colors ${
showErrorsOnly
? "bg-error-container text-on-error-container font-bold shadow-sm"
: "text-outline hover:text-error"
}`}
>
<span className="material-symbols-outlined text-[12px]">warning</span>
Errors
</button>
</div>
{/* Slow Spans Toggle (>500ms) */}
<button
type="button"
onClick={() => setMinDurationMs(minDurationMs > 0 ? 0 : 500)}
className={`flex items-center gap-1 px-2 py-1 text-[10px] font-mono font-bold uppercase tracking-wider rounded-lg border transition-colors ${
minDurationMs > 0
? "bg-amber-500/20 text-amber-300 border-amber-500/40 font-bold"
: "bg-surface-container-low text-outline hover:text-on-surface border-surface-container/40"
}`}
title="Filter to slow spans (>500ms latency)"
>
<span className="material-symbols-outlined text-[13px]">speed</span>
<span>&gt;500ms</span>
</button>
<button
type="button"
onClick={() => {
resetFilters();
setMinDurationMs(0);
}}
className="px-2 py-1 text-[10px] font-mono font-bold uppercase tracking-wider text-outline hover:text-on-surface rounded-lg bg-surface-container-low hover:bg-surface-container border border-surface-container/40 transition-colors"
title="Reset active filters"
>
Reset
</button>
<div className="h-4 w-px bg-surface-container-high" />
{/* Active Issues / Incident Counter Button */}
<button
type="button"
onClick={() => setActiveTab("tab0")}
aria-label="Active issues"
className={`relative flex items-center gap-1.5 px-2.5 py-1 rounded-lg font-mono text-xs font-bold transition-colors border ${
openIssuesCount > 0
? "bg-error-container text-on-error-container border-error/30 animate-pulse"
: "bg-surface-container-low text-outline hover:text-on-surface border-surface-container/40"
}`}
title="Open Issues & Anomalies"
>
<span className="material-symbols-outlined text-[16px]">notifications_active</span>
<span>{openIssuesCount}</span>
</button>
{/* User Account / Auth */}
{user ? (
<div className="flex items-center gap-2 pl-1 select-none">
<div
className="w-8 h-8 rounded-full bg-primary/15 text-primary flex items-center justify-center font-bold text-xs border border-primary/20 shadow-sm"
title={user.email || user.sub}
>
{(user.name || user.email || user.sub).charAt(0).toUpperCase()}
</div>
<div className="hidden xl:flex flex-col text-left">
<span className="font-body text-xs text-on-surface leading-tight font-medium truncate max-w-[120px]">
{user.name || user.email?.split("@")[0] || user.sub}
</span>
<button
type="button"
onClick={() => logout()}
className="font-mono text-[10px] text-outline hover:text-error text-left leading-none transition-colors"
>
Sign out
</button>
</div>
</div>
) : (
<button
type="button"
onClick={() => login()}
className="flex items-center gap-1 px-2.5 py-1 rounded-lg bg-primary/10 text-primary hover:bg-primary/20 font-mono text-xs transition-colors border border-primary/20 select-none"
>
<span className="material-symbols-outlined text-[15px]">login</span>
<span>Sign in</span>
</button>
)}
</div>
</header>
);
};
File diff suppressed because it is too large. Load diff
+323
View File
@@ -0,0 +1,323 @@
import { useMemo, useState } from "react"
import {
Area,
AreaChart,
CartesianGrid,
Legend,
Line,
LineChart,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from "recharts"
import { AlertOctagon, GitCompareArrows, RefreshCw, Server } from "lucide-react"
import { useStats } from "../hooks/useStats"
import type { StatsPoint } from "../lib/types"
const compactFormatter = new Intl.NumberFormat("en-US", {
notation: "compact",
maximumFractionDigits: 1,
})
// Stable palette for release lines; cycles when there are many releases.
const RELEASE_COLORS = ["#38bdf8", "#f472b6", "#34d399", "#fbbf24", "#a78bfa", "#fb7185"]
const label = (bucket: string, granularity: "hour" | "day") => {
const d = new Date(bucket.replace(" ", "T") + "Z")
if (Number.isNaN(d.getTime())) return bucket
return granularity === "day"
? d.toLocaleDateString([], { month: "short", day: "numeric" })
: d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", hour12: false })
}
const ChartTooltip = ({ active, payload, granularity }: any) => {
if (!active || !payload?.length) return null
const p = payload[0].payload
const d = new Date(String(p.bucket).replace(" ", "T") + "Z")
return (
<div className="rounded-xl border border-white/10 bg-slate-950/95 p-3 text-xs shadow-2xl backdrop-blur-md">
<div className="mb-1 font-mono text-[10px] text-slate-400">
{d.toLocaleString([], {
month: "short",
day: "numeric",
hour: granularity === "hour" ? "2-digit" : undefined,
minute: granularity === "hour" ? "2-digit" : undefined,
hour12: false,
})}
</div>
{payload.map((entry: any) => (
<div key={entry.name} className="flex items-center justify-between gap-4">
<span className="flex items-center gap-1.5 text-slate-300">
<span className="h-1.5 w-1.5 rounded-full" style={{ background: entry.color }} />
{entry.name}
</span>
<span className="font-mono font-bold text-white">{entry.value}</span>
</div>
))}
</div>
)
}
const rangeOptions = [24, 72, 168, 720] // 1d, 3d, 7d, 30d
export const TrendsVisualizer = () => {
const [hours, setHours] = useState(72)
const [service, setService] = useState("")
const { errorPoints, granularity, releaseSeries, services, loading, error, refresh } = useStats({
hours,
service,
})
const errorData = useMemo(
() =>
errorPoints.map((p: StatsPoint) => ({
bucket: p.bucket,
total: p.total,
errors: p.errors,
})),
[errorPoints],
)
const releases = useMemo(() => Object.keys(releaseSeries).sort(), [releaseSeries])
const releaseData = useMemo(() => {
// Recharts wants rows, not per-series arrays: bucket -> {release: errors}.
const byBucket: Record<string, Record<string, number | string>> = {}
releases.forEach((rel) => {
releaseSeries[rel].forEach((p) => {
byBucket[p.bucket] = byBucket[p.bucket] || { bucket: p.bucket }
byBucket[p.bucket][rel] = p.errors
})
})
return Object.values(byBucket).sort((a, b) =>
String(a.bucket).localeCompare(String(b.bucket)),
)
}, [releaseSeries, releases])
const totals = useMemo(() => {
let total = 0
let errors = 0
errorPoints.forEach((p) => {
total += p.total
errors += p.errors
})
return { total, errors }
}, [errorPoints])
if (loading && errorPoints.length === 0) {
return (
<div className="flex min-h-[240px] flex-col items-center justify-center gap-3 text-slate-400">
<RefreshCw className="h-6 w-6 animate-spin opacity-40" />
<p className="text-sm">Loading trends…</p>
</div>
)
}
return (
<div className="space-y-6">
{/* Toolbar */}
<div className="sticky top-0 z-20 flex flex-wrap items-center justify-between gap-3 border-b border-white/5 bg-slate-950/80 py-2 backdrop-blur">
<div className="flex flex-wrap items-center gap-3">
<span className="inline-flex items-center gap-2 text-[10px] font-black uppercase tracking-[0.2em] text-slate-500">
<GitCompareArrows className="h-3 w-3 text-sky-400" />
Historical Trends
</span>
<div className="flex items-center gap-1 rounded-lg border border-white/5 bg-white/5 p-0.5">
{rangeOptions.map((h) => (
<button
key={h}
onClick={() => setHours(h)}
className={`rounded px-2 py-0.5 text-[10px] font-bold uppercase tracking-wider transition-all ${
hours === h
? "bg-sky-500/20 text-sky-300 ring-1 ring-sky-500/30"
: "text-slate-500 hover:text-slate-300"
}`}
>
{h < 24 ? `${h}h` : `${h / 24}d`}
</button>
))}
</div>
{services.length > 0 && (
<select
value={service}
onChange={(e) => setService(e.target.value)}
className="rounded-lg border border-white/10 bg-slate-900/60 px-2 py-1 text-[10px] font-bold uppercase tracking-wider text-slate-300 outline-none"
aria-label="Filter by service"
>
<option value="">All services</option>
{services.map((s) => (
<option key={s} value={s}>
{s}
</option>
))}
</select>
)}
</div>
<div className="flex items-center gap-3">
<span className="font-mono text-[10px] text-slate-500">
{compactFormatter.format(totals.total)} entries ·{" "}
<span className={totals.errors > 0 ? "text-rose-400" : "text-emerald-400"}>
{compactFormatter.format(totals.errors)} errors
</span>
</span>
<button
onClick={() => refresh()}
className="flex items-center gap-1.5 rounded-lg border border-white/10 px-2.5 py-1 text-[10px] font-bold uppercase tracking-wider text-slate-400 transition-all hover:border-white/20 hover:text-white"
>
<RefreshCw className={`h-3 w-3 ${loading ? "animate-spin" : ""}`} />
Refresh
</button>
</div>
</div>
{error && (
<div className="flex items-center gap-2 rounded-xl border border-rose-500/20 bg-rose-500/5 p-4 text-sm text-rose-300">
<AlertOctagon className="h-4 w-4 shrink-0" />
{error}
</div>
)}
{/* Error trend */}
<section className="rounded-2xl border border-white/5 bg-slate-950/40 p-4">
<header className="mb-3 flex items-center justify-between">
<h3 className="flex items-center gap-2 text-xs font-bold uppercase tracking-[0.2em] text-slate-400">
<AlertOctagon className="h-3.5 w-3.5 text-rose-400" />
Entries &amp; errors over time
</h3>
<span className="text-[10px] uppercase tracking-wider text-slate-600">
per {granularity}
</span>
</header>
{errorData.length === 0 ? (
<p className="py-12 text-center text-sm text-slate-500">
No data in this range yet.
</p>
) : (
<div className="h-64 w-full">
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={errorData} margin={{ top: 5, right: 10, left: 0, bottom: 0 }}>
<defs>
<linearGradient id="trendFill" x1="0" y1="0" x2="0" y2="100%">
<stop offset="5%" stopColor="#38bdf8" stopOpacity={0.35} />
<stop offset="95%" stopColor="#38bdf8" stopOpacity={0} />
</linearGradient>
<linearGradient id="trendErrFill" x1="0" y1="0" x2="0" y2="100%">
<stop offset="5%" stopColor="#fb7185" stopOpacity={0.45} />
<stop offset="95%" stopColor="#fb7185" stopOpacity={0} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="rgba(255,255,255,0.05)" />
<XAxis
dataKey="bucket"
tickFormatter={(b) => label(b, granularity)}
stroke="#475569"
fontSize={10}
tickLine={false}
axisLine={false}
minTickGap={40}
/>
<YAxis
stroke="#475569"
fontSize={10}
tickLine={false}
axisLine={false}
width={36}
tickFormatter={(v) => compactFormatter.format(v)}
/>
<Tooltip content={<ChartTooltip granularity={granularity} />} />
<Legend
wrapperStyle={{ fontSize: 10, textTransform: "uppercase", letterSpacing: "0.1em" }}
/>
<Area
type="monotone"
dataKey="total"
name="Total"
stroke="#38bdf8"
strokeWidth={2}
fill="url(#trendFill)"
isAnimationActive={false}
/>
<Area
type="monotone"
dataKey="errors"
name="Errors"
stroke="#fb7185"
strokeWidth={2}
fill="url(#trendErrFill)"
isAnimationActive={false}
/>
</AreaChart>
</ResponsiveContainer>
</div>
)}
</section>
{/* Release comparison */}
<section className="rounded-2xl border border-white/5 bg-slate-950/40 p-4">
<header className="mb-3 flex items-center justify-between">
<h3 className="flex items-center gap-2 text-xs font-bold uppercase tracking-[0.2em] text-slate-400">
<GitCompareArrows className="h-3.5 w-3.5 text-sky-400" />
Errors by release
</h3>
<span className="text-[10px] uppercase tracking-wider text-slate-600">
{releases.length} release{releases.length === 1 ? "" : "s"} · overlay
</span>
</header>
{releaseData.length === 0 || releases.length === 0 ? (
<p className="py-12 text-center text-sm text-slate-500">
No release-tagged telemetry in this range. Have services send{" "}
<code className="rounded bg-white/5 px-1 font-mono text-[10px]">service.release</code>{" "}
to compare deploys here.
</p>
) : (
<div className="h-64 w-full">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={releaseData} margin={{ top: 5, right: 10, left: 0, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="rgba(255,255,255,0.05)" />
<XAxis
dataKey="bucket"
tickFormatter={(b) => label(b, "hour")}
stroke="#475569"
fontSize={10}
tickLine={false}
axisLine={false}
minTickGap={40}
/>
<YAxis
stroke="#475569"
fontSize={10}
tickLine={false}
axisLine={false}
width={36}
tickFormatter={(v) => compactFormatter.format(v)}
/>
<Tooltip content={<ChartTooltip granularity="hour" />} />
<Legend
wrapperStyle={{ fontSize: 10, textTransform: "uppercase", letterSpacing: "0.1em" }}
/>
{releases.map((rel, i) => (
<Line
key={rel}
type="monotone"
dataKey={rel}
stroke={RELEASE_COLORS[i % RELEASE_COLORS.length]}
strokeWidth={2}
dot={false}
isAnimationActive={false}
/>
))}
</LineChart>
</ResponsiveContainer>
</div>
)}
</section>
<p className="flex items-center justify-center gap-1.5 pb-2 text-center text-[10px] text-slate-600">
<Server className="h-3 w-3" />
Served from the ClickHouse hourly rollup ({service || "all services"}, last {hours}h)
</p>
</div>
)
}
export default TrendsVisualizer
@@ -0,0 +1,62 @@
import { Component, type ErrorInfo, type ReactNode } from "react"
import { AlertTriangle, RotateCcw } from "lucide-react"
interface Props {
children: ReactNode
}
interface State {
error: Error | null
}
// Keeps a rendering bug in one telemetry view from blanking the whole app.
export class ViewErrorBoundary extends Component<Props, State> {
state: State = { error: null }
static getDerivedStateFromError(error: Error): State {
return { error }
}
componentDidCatch(error: Error, info: ErrorInfo) {
console.error("View crashed:", error, info.componentStack)
const rum = (window as Window & {
OtelRUM?: {
last?: {
captureException: (error: Error, extra?: Record<string, unknown>) => void
}
}
}).OtelRUM?.last
rum?.captureException(error, {
"exception.type": "ReactErrorBoundary",
"react.component_stack": info.componentStack,
})
}
render() {
if (this.state.error) {
return (
<div className="flex min-h-[240px] flex-col items-center justify-center gap-4 rounded-3xl border border-rose-500/20 bg-rose-500/5 p-12 text-center">
<AlertTriangle className="h-10 w-10 text-rose-400" />
<div>
<p className="text-lg font-semibold text-white">
This view hit an unexpected error
</p>
<p className="mt-1 max-w-md truncate font-mono text-xs text-rose-300/80">
{this.state.error.message}
</p>
</div>
<button
type="button"
onClick={() => this.setState({ error: null })}
className="inline-flex items-center gap-2 rounded-xl border border-rose-500/30 bg-rose-500/10 px-4 py-2 text-[10px] font-black uppercase tracking-[0.2em] text-rose-300 transition-all hover:bg-rose-500/20"
>
<RotateCcw className="h-3 w-3" />
Retry
</button>
</div>
)
}
return this.props.children
}
}
@@ -0,0 +1,385 @@
import { useMemo, useState } from "react";
import { useObservabilityData } from "../hooks/useObservabilityData";
import { isSpanError } from "../lib/otel-utils";
export const WaveformHistogram = () => {
const {
logs,
traces,
unfocusedLogs,
unfocusedTraces,
selectedTimeRange,
setSelectedTimeRange,
setActiveTab,
} = useObservabilityData();
// Use unfocused data so that waveform buckets remain stable across the window
const sourceLogs = unfocusedLogs || logs;
const sourceTraces = unfocusedTraces || traces;
const { buckets, hasErrorSurge, surgePercent, timeLabels, totalCount } = useMemo(() => {
const numBuckets = 24;
const bucketStats = Array.from({ length: numBuckets }, () => ({
error: 0,
warn: 0,
info: 0,
total: 0,
traceCount: 0,
logCount: 0,
}));
// Collect timestamps and severity levels from real telemetry items
const items: Array<{ time: number; level: "error" | "warn" | "info"; isTrace?: boolean }> = [];
sourceLogs.forEach((log) => {
(log.scopeLogs ?? []).forEach((sl) => {
(sl.logRecords ?? []).forEach((r) => {
const t = Number(BigInt(r.timeUnixNano || "0") / 1_000_000n);
if (t > 0) {
const sev = r.severityNumber ?? 0;
const level = sev >= 17 ? "error" : sev >= 13 ? "warn" : "info";
items.push({ time: t, level, isTrace: false });
}
});
});
});
sourceTraces.forEach((trace) => {
(trace.resourceSpans ?? []).forEach((rs) => {
(rs.scopeSpans ?? []).forEach((ss) => {
(ss.spans ?? []).forEach((s) => {
const t = Number(BigInt(s.startTimeUnixNano || "0") / 1_000_000n);
if (t > 0) {
const hasErr = isSpanError(s);
items.push({ time: t, level: hasErr ? "error" : "info", isTrace: true });
}
});
});
});
});
if (items.length === 0) {
return {
buckets: [],
hasErrorSurge: false,
surgePercent: 0,
timeLabels: [],
totalCount: 0,
};
}
const minTime = Math.min(...items.map((it) => it.time));
const maxTime = Math.max(...items.map((it) => it.time));
const range = Math.max(maxTime - minTime, 1000);
items.forEach((it) => {
const idx = Math.min(numBuckets - 1, Math.floor(((it.time - minTime) / range) * numBuckets));
bucketStats[idx][it.level] += 1;
bucketStats[idx].total += 1;
if (it.isTrace) {
bucketStats[idx].traceCount += 1;
} else {
bucketStats[idx].logCount += 1;
}
});
// Check for surge in real errors
let maxErrors = 0;
let baselineErrors = 0;
bucketStats.forEach((b, idx) => {
if (b.error > maxErrors) maxErrors = b.error;
if (idx < numBuckets / 2) baselineErrors += b.error;
});
const avgBaseline = baselineErrors / (numBuckets / 2) || 1;
const computedSurge = Math.round(((maxErrors - avgBaseline) / avgBaseline) * 100);
const timeLabels = [
new Date(minTime).toLocaleTimeString([], { hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit" }),
new Date(minTime + range * 0.33).toLocaleTimeString([], { hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit" }),
new Date(minTime + range * 0.66).toLocaleTimeString([], { hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit" }),
new Date(maxTime).toLocaleTimeString([], { hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit" }),
];
// Normalize heights to percentage
const maxBucketTotal = Math.max(...bucketStats.map((b) => b.total), 1);
const normalized = bucketStats.map((b, idx) => {
const scale = (b.total / maxBucketTotal) * 100;
const errorPct = b.total > 0 ? (b.error / b.total) * scale : 0;
const warnPct = b.total > 0 ? (b.warn / b.total) * scale : 0;
const infoPct = b.total > 0 ? (b.info / b.total) * scale : 0;
const startTime = minTime + Math.floor((idx * range) / numBuckets);
const endTime = minTime + Math.floor(((idx + 1) * range) / numBuckets);
const timeLabel = `${new Date(startTime).toLocaleTimeString([], { hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit" })} – ${new Date(endTime).toLocaleTimeString([], { hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit" })}`;
return {
idx,
error: errorPct,
warn: warnPct,
info: infoPct,
rawError: b.error,
rawWarn: b.warn,
rawInfo: b.info,
total: b.total,
traceCount: b.traceCount,
logCount: b.logCount,
startTime,
endTime,
timeLabel,
};
});
return {
buckets: normalized,
hasErrorSurge: maxErrors >= 5 && computedSurge > 50,
surgePercent: computedSurge,
timeLabels,
totalCount: items.length,
};
}, [sourceLogs, sourceTraces]);
const [isCollapsed, setIsCollapsed] = useState(false);
// Check which bucket matches the active selectedTimeRange
const activeBucket = useMemo(() => {
if (!selectedTimeRange || buckets.length === 0) return null;
return (
buckets.find(
(b) =>
selectedTimeRange.startMs <= b.endTime && selectedTimeRange.endMs >= b.startTime,
) || null
);
}, [selectedTimeRange, buckets]);
const handleBucketClick = (b: (typeof buckets)[0]) => {
if (activeBucket && activeBucket.idx === b.idx) {
// Toggle off if clicking the already selected bucket
setSelectedTimeRange(null);
} else {
setSelectedTimeRange({
startMs: b.startTime,
endMs: b.endTime,
label: b.timeLabel,
});
}
};
if (buckets.length === 0) {
return (
<div className="relative bg-surface-container-lowest p-3.5 rounded-xl shadow-md mb-4 border border-surface-container/50">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="material-symbols-outlined text-primary-container text-[18px]">
monitoring
</span>
<h2 className="font-headline font-semibold text-xs text-on-surface">
Telemetry Volume &amp; Error Waveform
</h2>
</div>
<span className="font-mono text-xs text-outline">Listening for OTLP traffic...</span>
</div>
</div>
);
}
return (
<div className="relative bg-surface-container-lowest p-3.5 rounded-xl shadow-md mb-4 overflow-hidden border border-surface-container/50 font-body">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2 mb-2">
<div className="flex items-center gap-3 flex-wrap">
<button
type="button"
onClick={() => setIsCollapsed(!isCollapsed)}
className="flex items-center gap-1.5 hover:text-white transition-colors"
>
<span className="material-symbols-outlined text-primary-container text-[18px]">
monitoring
</span>
<h2 className="font-headline font-semibold text-xs uppercase tracking-wider text-on-surface leading-none">
Telemetry Volume Waveform
</h2>
<span className="material-symbols-outlined text-outline text-[16px]">
{isCollapsed ? "expand_more" : "expand_less"}
</span>
</button>
<span className="font-mono text-xs text-outline">
{totalCount} events in current buffer
</span>
{hasErrorSurge && (
<span className="px-2 py-0.5 rounded-full bg-error-container text-on-error-container font-mono text-[10px] font-bold animate-pulse flex items-center gap-1">
<span className="w-1.5 h-1.5 rounded-full bg-error" />
SURGE +{surgePercent}%
</span>
)}
{selectedTimeRange && (
<span className="px-2 py-0.5 rounded-full bg-sky-500/20 text-sky-300 font-mono text-[10px] font-bold border border-sky-400/30 flex items-center gap-1">
<span className="w-1.5 h-1.5 rounded-full bg-sky-400 animate-ping" />
Time Focus Active
<button
type="button"
onClick={(e) => {
e.stopPropagation();
setSelectedTimeRange(null);
}}
className="ml-1 hover:text-white"
title="Clear time focus"
>
✕
</button>
</span>
)}
</div>
{/* Legend */}
<div className="flex items-center gap-4 font-mono text-xs select-none">
<div className="flex items-center gap-1.5">
<span className="w-2.5 h-2.5 rounded-sm bg-error"></span>
<span className="text-on-surface">ERROR</span>
</div>
<div className="flex items-center gap-1.5">
<span className="w-2.5 h-2.5 rounded-sm bg-primary-fixed-dim"></span>
<span className="text-on-surface">WARN</span>
</div>
<div className="flex items-center gap-1.5">
<span className="w-2.5 h-2.5 rounded-sm bg-secondary"></span>
<span className="text-on-surface">INFO</span>
</div>
</div>
</div>
{!isCollapsed && (
<>
{/* Stacked SVG Histogram with Real Traffic */}
<div className="relative w-full h-24 flex items-end">
<div className="w-full h-full flex items-end justify-between gap-1 pt-3">
{buckets.map((b) => {
const isSurgeBar = hasErrorSurge && b.error > 40;
const isSelected = activeBucket?.idx === b.idx;
const hasAnySelected = activeBucket !== null;
return (
<div
key={b.idx}
onClick={() => handleBucketClick(b)}
className={`flex-1 flex flex-col justify-end h-full gap-0.5 group cursor-pointer relative transition-all rounded-sm ${
isSelected
? "ring-2 ring-sky-400 bg-sky-400/25 scale-y-105 z-20 shadow-md shadow-sky-500/20"
: hasAnySelected
? "opacity-40 hover:opacity-100 hover:brightness-125"
: "hover:brightness-125"
}`}
title={`${b.timeLabel}\nTraces: ${b.traceCount} • Logs: ${b.logCount}\nErrors: ${b.rawError}, Warns: ${b.rawWarn}, Info: ${b.rawInfo}\nClick to ${isSelected ? "clear focus" : "focus and show events for this period"}`}
>
{/* Active Selection Focus Pin Indicator */}
{isSelected && (
<div className="absolute -top-4 left-1/2 -translate-x-1/2 flex flex-col items-center pointer-events-none z-20 whitespace-nowrap">
<span className="px-1.5 py-0.5 bg-sky-500 text-slate-950 font-mono text-[9px] font-black rounded shadow">
FOCUS
</span>
<span className="w-0.5 h-1 bg-sky-400"></span>
</div>
)}
{/* Incident Flag Marker on Real Surge */}
{!isSelected && isSurgeBar && b.idx === 15 && (
<div className="absolute -top-3 left-1/2 -translate-x-1/2 flex flex-col items-center pointer-events-none z-10 whitespace-nowrap animate-bounce">
<span className="px-1.5 py-0.5 bg-error-container text-on-error-container font-mono text-[10px] rounded shadow font-semibold flex items-center gap-1">
<span className="w-1.5 h-1.5 rounded-full bg-error animate-ping"></span>
SURGE +{surgePercent}%
</span>
<span className="w-0.5 h-2 bg-error"></span>
</div>
)}
{/* Error segment */}
{b.error > 0 && (
<div
className="w-full bg-error rounded-t-sm transition-all group-hover:brightness-125"
style={{ height: `${b.error}%` }}
/>
)}
{/* Warn segment */}
{b.warn > 0 && (
<div
className="w-full bg-primary-fixed-dim transition-all group-hover:brightness-125"
style={{ height: `${b.warn}%` }}
/>
)}
{/* Info segment */}
{b.info > 0 && (
<div
className="w-full bg-secondary/80 rounded-b-sm transition-all group-hover:brightness-125"
style={{ height: `${b.info}%` }}
/>
)}
</div>
);
})}
</div>
</div>
{/* Real Time Scale Axes */}
<div className="flex justify-between items-center text-outline font-mono text-xs pt-2 select-none border-t border-surface-container/40 mt-1">
<span>{timeLabels[0]}</span>
<span>{timeLabels[1]}</span>
<span className={hasErrorSurge ? "text-error font-semibold" : ""}>
{timeLabels[2]} {hasErrorSurge ? "(SURGE)" : ""}
</span>
<span>{timeLabels[3]}</span>
</div>
{/* Time Focus Bar with quick tab navigation and clear button */}
{activeBucket && (
<div className="mt-2.5 flex flex-wrap items-center justify-between gap-3 rounded-xl bg-sky-500/10 border border-sky-500/30 px-3.5 py-2 animate-in fade-in slide-in-from-top-1 duration-200">
<div className="flex items-center gap-2.5 min-w-0">
<span className="flex h-2 w-2 rounded-full bg-sky-400 animate-ping" />
<div className="flex items-center gap-2 flex-wrap">
<span className="font-semibold text-xs text-white">
Focused Time Period:
</span>
<span className="font-mono text-xs text-sky-300 font-bold">
{activeBucket.timeLabel}
</span>
<span className="text-[11px] text-slate-400 font-mono">
({activeBucket.traceCount} trace events • {activeBucket.logCount} log records
{activeBucket.rawError > 0 ? ` • ${activeBucket.rawError} errors` : ""})
</span>
</div>
</div>
<div className="flex items-center gap-2 font-mono text-xs">
<button
type="button"
onClick={() => setActiveTab("tab1")}
className="flex items-center gap-1.5 px-2.5 py-1 rounded-lg bg-sky-500/20 hover:bg-sky-500/30 text-sky-300 font-bold transition-all border border-sky-400/30"
>
<span className="material-symbols-outlined text-[14px]">timeline</span>
Show Traces ({activeBucket.traceCount})
</button>
<button
type="button"
onClick={() => setActiveTab("tab3")}
className="flex items-center gap-1.5 px-2.5 py-1 rounded-lg bg-sky-500/20 hover:bg-sky-500/30 text-sky-300 font-bold transition-all border border-sky-400/30"
>
<span className="material-symbols-outlined text-[14px]">receipt_long</span>
Show Logs ({activeBucket.logCount})
</button>
<button
type="button"
onClick={() => setSelectedTimeRange(null)}
className="flex items-center gap-1 px-2 py-1 rounded-lg bg-white/10 hover:bg-white/20 text-slate-300 transition-colors"
title="Clear time focus"
>
<span className="material-symbols-outlined text-[14px]">close</span>
Clear Focus
</button>
</div>
</div>
)}
</>
)}
</div>
);
};