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

+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>
)
}