94 lines
3.7 KiB
TypeScript
94 lines
3.7 KiB
TypeScript
import { AlertTriangle, ArrowRight } from "lucide-react"
|
||
import React from "react"
|
||
import { useObservabilityData } from "../hooks/useObservabilityData"
|
||
import { useIssues } from "../hooks/useIssues"
|
||
import { getServiceName, isSpanError } from "../lib/otel-utils"
|
||
|
||
/** Full-width alert strip docked under the top bar; renders nothing when there is nothing anomalous. */
|
||
export const AnomalyRadarBar: React.FC = () => {
|
||
const { logs, traces, showErrorsOnly, setShowErrorsOnly, setActiveTab } = useObservabilityData()
|
||
const { issues } = useIssues()
|
||
|
||
const errorStats = React.useMemo(() => {
|
||
let errorCount = 0
|
||
const services = new Set<string>()
|
||
const note = (name: string) => { if (name !== "unknown-service") services.add(name) }
|
||
|
||
logs.forEach((l) =>
|
||
(l.scopeLogs ?? []).forEach((sl) =>
|
||
(sl.logRecords ?? []).forEach((r) => {
|
||
if ((r.severityNumber ?? 0) >= 17) {
|
||
errorCount++
|
||
note(getServiceName(l.resource?.attributes ?? [], r.attributes ?? []))
|
||
}
|
||
}),
|
||
),
|
||
)
|
||
traces.forEach((t) =>
|
||
(t.resourceSpans ?? []).forEach((rs) =>
|
||
(rs.scopeSpans ?? []).forEach((ss) =>
|
||
(ss.spans ?? []).forEach((s) => {
|
||
if (isSpanError(s)) {
|
||
errorCount++
|
||
note(getServiceName(rs.resource?.attributes ?? [], s.attributes ?? []))
|
||
}
|
||
}),
|
||
),
|
||
),
|
||
)
|
||
return { errorCount, services: Array.from(services) }
|
||
}, [logs, traces])
|
||
|
||
const topOpenIssue = React.useMemo(
|
||
() => issues.filter((i) => !i.status || i.status === "open").sort((a, b) => b.count - a.count)[0] ?? null,
|
||
[issues],
|
||
)
|
||
|
||
if (errorStats.errorCount === 0 && !topOpenIssue) return null
|
||
|
||
const services = errorStats.services.slice(0, 3).join(", ") + (errorStats.services.length > 3 ? ` +${errorStats.services.length - 3}` : "")
|
||
|
||
return (
|
||
<div
|
||
role="alert"
|
||
className="sticky top-[58px] z-30 flex flex-wrap items-center gap-x-4 gap-y-1.5 border-b-2 border-ds-critical bg-ds-critical-bg px-6 py-2 font-[family-name:var(--font-ui)]"
|
||
>
|
||
<span className="inline-flex shrink-0 items-center gap-1.5 text-[11px] font-bold uppercase tracking-[0.1em] text-ds-critical">
|
||
<AlertTriangle className="h-3.5 w-3.5" aria-hidden="true" />
|
||
Anomaly radar
|
||
</span>
|
||
<span className="min-w-0 flex-1 truncate text-[12.5px] text-ds-text">
|
||
{errorStats.errorCount > 0 && (
|
||
<><strong className="font-semibold">{errorStats.errorCount} errors</strong> in the active window{services && <span className="text-ds-text-2"> · {services}</span>}</>
|
||
)}
|
||
{topOpenIssue && (
|
||
<span className="text-ds-text-2">
|
||
{errorStats.errorCount > 0 ? " · top issue " : ""}
|
||
<span className="font-medium text-ds-text">{topOpenIssue.title}</span> ×{topOpenIssue.count} ({topOpenIssue.service})
|
||
</span>
|
||
)}
|
||
</span>
|
||
<div className="flex shrink-0 items-center gap-2">
|
||
{!showErrorsOnly && (
|
||
<button
|
||
type="button"
|
||
onClick={() => setShowErrorsOnly(true)}
|
||
className="inline-flex h-7 items-center rounded-[8px] border border-ds-critical/40 px-2.5 text-[12px] font-medium text-ds-critical hover:bg-ds-critical/10"
|
||
>
|
||
Isolate errors
|
||
</button>
|
||
)}
|
||
<button
|
||
type="button"
|
||
onClick={() => setActiveTab("tab0")}
|
||
className="inline-flex h-7 items-center gap-1 rounded-[8px] bg-ds-critical px-2.5 text-[12px] font-semibold text-ds-on-accent hover:opacity-90"
|
||
>
|
||
Triage issues <ArrowRight className="h-3 w-3" aria-hidden="true" />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
export default AnomalyRadarBar
|