more otel
This commit is contained in:
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 >{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: "{searchQuery}"</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;
|
||||
Reference in new issue
Block a user