@kopai/ui 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +2427 -1139
- package/dist/index.d.cts +36 -7
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +36 -7
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2376 -1082
- package/dist/index.mjs.map +1 -1
- package/package.json +13 -13
- package/src/components/KeyboardShortcuts/KeyboardShortcutsProvider.tsx +7 -7
- package/src/components/observability/DynamicDashboard/DynamicDashboard.test.tsx +5 -0
- package/src/components/observability/LogTimeline/LogFilter.tsx +5 -1
- package/src/components/observability/LogTimeline/index.tsx +6 -2
- package/src/components/observability/MetricHistogram/index.tsx +20 -19
- package/src/components/observability/MetricTimeSeries/index.tsx +8 -9
- package/src/components/observability/ServiceList/shortcuts.ts +1 -1
- package/src/components/observability/TraceComparison/index.tsx +332 -0
- package/src/components/observability/TraceDetail/TraceDetail.stories.tsx +0 -4
- package/src/components/observability/TraceDetail/index.tsx +4 -3
- package/src/components/observability/TraceSearch/DurationBar.tsx +38 -0
- package/src/components/observability/TraceSearch/ScatterPlot.tsx +135 -0
- package/src/components/observability/TraceSearch/SearchForm.tsx +195 -0
- package/src/components/observability/TraceSearch/SortDropdown.tsx +32 -0
- package/src/components/observability/TraceSearch/index.tsx +211 -218
- package/src/components/observability/TraceTimeline/DetailPane/EventsTab.tsx +1 -7
- package/src/components/observability/TraceTimeline/FlamegraphView.tsx +232 -0
- package/src/components/observability/TraceTimeline/GraphView.tsx +322 -0
- package/src/components/observability/TraceTimeline/Minimap.tsx +260 -0
- package/src/components/observability/TraceTimeline/SpanDetailInline.tsx +184 -0
- package/src/components/observability/TraceTimeline/SpanRow.tsx +14 -24
- package/src/components/observability/TraceTimeline/SpanSearch.tsx +76 -0
- package/src/components/observability/TraceTimeline/StatisticsView.tsx +223 -0
- package/src/components/observability/TraceTimeline/TimeRuler.tsx +54 -0
- package/src/components/observability/TraceTimeline/TimelineBar.tsx +18 -2
- package/src/components/observability/TraceTimeline/TraceHeader.tsx +116 -51
- package/src/components/observability/TraceTimeline/ViewTabs.tsx +34 -0
- package/src/components/observability/TraceTimeline/index.tsx +254 -110
- package/src/components/observability/index.ts +15 -0
- package/src/components/observability/renderers/OtelTraceDetail.tsx +1 -4
- package/src/components/observability/shared/TooltipEntryList.tsx +25 -0
- package/src/components/observability/utils/flatten-tree.ts +15 -0
- package/src/components/observability/utils/time.ts +9 -0
- package/src/hooks/use-kopai-data.test.ts +3 -0
- package/src/hooks/use-kopai-data.ts +11 -0
- package/src/hooks/use-live-logs.test.ts +3 -0
- package/src/lib/__snapshots__/generate-prompt-instructions.test.ts.snap +1 -1
- package/src/lib/component-catalog.ts +15 -0
- package/src/pages/observability.test.tsx +5 -0
- package/src/pages/observability.tsx +314 -235
- package/src/providers/kopai-provider.tsx +3 -0
|
@@ -19,9 +19,9 @@ import {
|
|
|
19
19
|
LogTimeline,
|
|
20
20
|
LogFilter,
|
|
21
21
|
TabBar,
|
|
22
|
-
ServiceList,
|
|
23
22
|
TraceSearch,
|
|
24
23
|
TraceDetail,
|
|
24
|
+
TraceComparison,
|
|
25
25
|
KeyboardShortcutsProvider,
|
|
26
26
|
useRegisterShortcuts,
|
|
27
27
|
DynamicDashboard,
|
|
@@ -43,7 +43,7 @@ type OtelTracesRow = denormalizedSignals.OtelTracesRow;
|
|
|
43
43
|
type Tab = "logs" | "services" | "metrics";
|
|
44
44
|
|
|
45
45
|
const TABS: { key: Tab; label: string; shortcutKey: string }[] = [
|
|
46
|
-
{ key: "services", label: "
|
|
46
|
+
{ key: "services", label: "Traces", shortcutKey: "T" },
|
|
47
47
|
{ key: "logs", label: "Logs", shortcutKey: "L" },
|
|
48
48
|
{ key: "metrics", label: "Metrics", shortcutKey: "M" },
|
|
49
49
|
];
|
|
@@ -54,9 +54,28 @@ const TABS: { key: Tab; label: string; shortcutKey: string }[] = [
|
|
|
54
54
|
|
|
55
55
|
interface URLState {
|
|
56
56
|
tab: Tab;
|
|
57
|
+
// Services tab search params
|
|
57
58
|
service: string | null;
|
|
59
|
+
operation: string | null;
|
|
60
|
+
tags: string | null;
|
|
61
|
+
lookback: string | null;
|
|
62
|
+
tsMin: string | null;
|
|
63
|
+
tsMax: string | null;
|
|
64
|
+
minDuration: string | null;
|
|
65
|
+
maxDuration: string | null;
|
|
66
|
+
limit: number | null;
|
|
67
|
+
sort: string | null;
|
|
68
|
+
// Trace detail
|
|
58
69
|
trace: string | null;
|
|
59
70
|
span: string | null;
|
|
71
|
+
view: string | null;
|
|
72
|
+
uiFind: string | null;
|
|
73
|
+
// Comparison
|
|
74
|
+
compare: string | null;
|
|
75
|
+
// Minimap (phase 8)
|
|
76
|
+
viewStart: string | null;
|
|
77
|
+
viewEnd: string | null;
|
|
78
|
+
// Existing
|
|
60
79
|
dashboardId: string | null;
|
|
61
80
|
}
|
|
62
81
|
|
|
@@ -72,24 +91,79 @@ function readURLState(): URLState {
|
|
|
72
91
|
: rawTab === "logs" || rawTab === "metrics"
|
|
73
92
|
? rawTab
|
|
74
93
|
: "services";
|
|
75
|
-
|
|
94
|
+
const rawLimit = params.get("limit");
|
|
95
|
+
const limit = rawLimit ? parseInt(rawLimit, 10) : null;
|
|
96
|
+
return {
|
|
97
|
+
tab,
|
|
98
|
+
service,
|
|
99
|
+
operation: params.get("operation"),
|
|
100
|
+
tags: params.get("tags"),
|
|
101
|
+
lookback: params.get("lookback"),
|
|
102
|
+
tsMin: params.get("tsMin"),
|
|
103
|
+
tsMax: params.get("tsMax"),
|
|
104
|
+
minDuration: params.get("minDuration"),
|
|
105
|
+
maxDuration: params.get("maxDuration"),
|
|
106
|
+
limit: limit !== null && !isNaN(limit) ? limit : null,
|
|
107
|
+
sort: params.get("sort"),
|
|
108
|
+
trace,
|
|
109
|
+
span,
|
|
110
|
+
view: params.get("view"),
|
|
111
|
+
uiFind: params.get("uiFind"),
|
|
112
|
+
compare: params.get("compare"),
|
|
113
|
+
viewStart: params.get("viewStart"),
|
|
114
|
+
viewEnd: params.get("viewEnd"),
|
|
115
|
+
dashboardId,
|
|
116
|
+
};
|
|
76
117
|
}
|
|
77
118
|
|
|
78
119
|
function pushURLState(
|
|
79
120
|
state: {
|
|
80
121
|
tab: Tab;
|
|
81
122
|
service?: string | null;
|
|
123
|
+
operation?: string | null;
|
|
124
|
+
tags?: string | null;
|
|
125
|
+
lookback?: string | null;
|
|
126
|
+
tsMin?: string | null;
|
|
127
|
+
tsMax?: string | null;
|
|
128
|
+
minDuration?: string | null;
|
|
129
|
+
maxDuration?: string | null;
|
|
130
|
+
limit?: number | null;
|
|
131
|
+
sort?: string | null;
|
|
82
132
|
trace?: string | null;
|
|
83
133
|
span?: string | null;
|
|
134
|
+
view?: string | null;
|
|
135
|
+
uiFind?: string | null;
|
|
136
|
+
compare?: string | null;
|
|
137
|
+
viewStart?: string | null;
|
|
138
|
+
viewEnd?: string | null;
|
|
84
139
|
dashboardId?: string | null;
|
|
85
140
|
},
|
|
86
141
|
{ replace = false }: { replace?: boolean } = {}
|
|
87
142
|
) {
|
|
88
143
|
const params = new URLSearchParams();
|
|
89
144
|
if (state.tab !== "services") params.set("tab", state.tab);
|
|
90
|
-
|
|
91
|
-
if (state.
|
|
92
|
-
|
|
145
|
+
|
|
146
|
+
if (state.tab === "services") {
|
|
147
|
+
if (state.service) params.set("service", state.service);
|
|
148
|
+
if (state.operation) params.set("operation", state.operation);
|
|
149
|
+
if (state.tags) params.set("tags", state.tags);
|
|
150
|
+
if (state.lookback) params.set("lookback", state.lookback);
|
|
151
|
+
if (state.tsMin) params.set("tsMin", state.tsMin);
|
|
152
|
+
if (state.tsMax) params.set("tsMax", state.tsMax);
|
|
153
|
+
if (state.minDuration) params.set("minDuration", state.minDuration);
|
|
154
|
+
if (state.maxDuration) params.set("maxDuration", state.maxDuration);
|
|
155
|
+
if (state.limit != null && state.limit !== 20)
|
|
156
|
+
params.set("limit", String(state.limit));
|
|
157
|
+
if (state.sort) params.set("sort", state.sort);
|
|
158
|
+
if (state.trace) params.set("trace", state.trace);
|
|
159
|
+
if (state.span) params.set("span", state.span);
|
|
160
|
+
if (state.view) params.set("view", state.view);
|
|
161
|
+
if (state.uiFind) params.set("uiFind", state.uiFind);
|
|
162
|
+
if (state.compare) params.set("compare", state.compare);
|
|
163
|
+
if (state.viewStart) params.set("viewStart", state.viewStart);
|
|
164
|
+
if (state.viewEnd) params.set("viewEnd", state.viewEnd);
|
|
165
|
+
}
|
|
166
|
+
|
|
93
167
|
// Preserve dashboardId from current URL if not explicitly provided
|
|
94
168
|
const dashboardId =
|
|
95
169
|
state.dashboardId !== undefined
|
|
@@ -115,8 +189,22 @@ let _cachedSearch = "";
|
|
|
115
189
|
let _cachedState: URLState = {
|
|
116
190
|
tab: "services",
|
|
117
191
|
service: null,
|
|
192
|
+
operation: null,
|
|
193
|
+
tags: null,
|
|
194
|
+
lookback: null,
|
|
195
|
+
tsMin: null,
|
|
196
|
+
tsMax: null,
|
|
197
|
+
minDuration: null,
|
|
198
|
+
maxDuration: null,
|
|
199
|
+
limit: null,
|
|
200
|
+
sort: null,
|
|
118
201
|
trace: null,
|
|
119
202
|
span: null,
|
|
203
|
+
view: null,
|
|
204
|
+
uiFind: null,
|
|
205
|
+
compare: null,
|
|
206
|
+
viewStart: null,
|
|
207
|
+
viewEnd: null,
|
|
120
208
|
dashboardId: null,
|
|
121
209
|
};
|
|
122
210
|
|
|
@@ -276,6 +364,42 @@ function parseDuration(input: string): string | undefined {
|
|
|
276
364
|
return String(Math.round(value * multipliers[unit]!));
|
|
277
365
|
}
|
|
278
366
|
|
|
367
|
+
// ---------------------------------------------------------------------------
|
|
368
|
+
// Logfmt helpers
|
|
369
|
+
// ---------------------------------------------------------------------------
|
|
370
|
+
|
|
371
|
+
function parseLogfmt(str: string): Record<string, string> {
|
|
372
|
+
const result: Record<string, string> = {};
|
|
373
|
+
const re = /(\w+)=(?:"([^"]*)"|([\S]*))/g;
|
|
374
|
+
let m: RegExpExecArray | null;
|
|
375
|
+
while ((m = re.exec(str)) !== null) {
|
|
376
|
+
const key = m[1];
|
|
377
|
+
if (key) result[key] = m[2] ?? m[3] ?? "";
|
|
378
|
+
}
|
|
379
|
+
return result;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
export function serializeLogfmt(rec: Record<string, string>): string {
|
|
383
|
+
return Object.entries(rec)
|
|
384
|
+
.map(([k, v]) => (v.includes(" ") ? `${k}="${v}"` : `${k}=${v}`))
|
|
385
|
+
.join(" ");
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// ---------------------------------------------------------------------------
|
|
389
|
+
// Lookback presets (ms values)
|
|
390
|
+
// ---------------------------------------------------------------------------
|
|
391
|
+
|
|
392
|
+
const LOOKBACK_MS: Record<string, number> = {
|
|
393
|
+
"5m": 5 * 60_000,
|
|
394
|
+
"15m": 15 * 60_000,
|
|
395
|
+
"30m": 30 * 60_000,
|
|
396
|
+
"1h": 60 * 60_000,
|
|
397
|
+
"2h": 2 * 60 * 60_000,
|
|
398
|
+
"6h": 6 * 60 * 60_000,
|
|
399
|
+
"12h": 12 * 60 * 60_000,
|
|
400
|
+
"24h": 24 * 60 * 60_000,
|
|
401
|
+
};
|
|
402
|
+
|
|
279
403
|
// ---------------------------------------------------------------------------
|
|
280
404
|
// Logs tab (live-tailing)
|
|
281
405
|
// ---------------------------------------------------------------------------
|
|
@@ -357,212 +481,159 @@ function LogsTab() {
|
|
|
357
481
|
// Services tab — data-fetching wrappers around extracted UI components
|
|
358
482
|
// ---------------------------------------------------------------------------
|
|
359
483
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
}) {
|
|
370
|
-
const { data, loading, error } = useKopaiData<{
|
|
371
|
-
data: OtelTracesRow[];
|
|
372
|
-
nextCursor: string | null;
|
|
373
|
-
}>(SERVICES_DS);
|
|
374
|
-
|
|
375
|
-
const services = useMemo(() => {
|
|
376
|
-
if (!data?.data) return [];
|
|
377
|
-
const names = new Set<string>();
|
|
378
|
-
for (const row of data.data) {
|
|
379
|
-
names.add(row.ServiceName ?? "unknown");
|
|
380
|
-
}
|
|
381
|
-
return Array.from(names)
|
|
382
|
-
.sort()
|
|
383
|
-
.map((name) => ({ name }));
|
|
384
|
-
}, [data]);
|
|
385
|
-
|
|
386
|
-
return (
|
|
387
|
-
<ServiceList
|
|
388
|
-
services={services}
|
|
389
|
-
isLoading={loading}
|
|
390
|
-
error={error ?? undefined}
|
|
391
|
-
onSelect={onSelect}
|
|
392
|
-
/>
|
|
393
|
-
);
|
|
484
|
+
interface TraceSummaryRow {
|
|
485
|
+
traceId: string;
|
|
486
|
+
rootServiceName: string;
|
|
487
|
+
rootSpanName: string;
|
|
488
|
+
startTimeNs: string;
|
|
489
|
+
durationNs: string;
|
|
490
|
+
spanCount: number;
|
|
491
|
+
errorCount: number;
|
|
492
|
+
services: Array<{ name: string; count: number; hasError: boolean }>;
|
|
394
493
|
}
|
|
395
494
|
|
|
396
495
|
function TraceSearchView({
|
|
397
|
-
service,
|
|
398
|
-
onBack,
|
|
399
496
|
onSelectTrace,
|
|
497
|
+
onCompare,
|
|
400
498
|
}: {
|
|
401
|
-
service: string;
|
|
402
|
-
onBack: () => void;
|
|
403
499
|
onSelectTrace: (traceId: string) => void;
|
|
500
|
+
onCompare: (traceIds: [string, string]) => void;
|
|
404
501
|
}) {
|
|
405
|
-
const
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
502
|
+
const urlState = useURLState();
|
|
503
|
+
const service = urlState.service;
|
|
504
|
+
|
|
505
|
+
// Build DataSource from URL state
|
|
506
|
+
const ds = useMemo<DataSource>(() => {
|
|
507
|
+
const params: Record<string, unknown> = {
|
|
508
|
+
limit: urlState.limit ?? 20,
|
|
509
|
+
sortOrder: "DESC" as const,
|
|
510
|
+
};
|
|
511
|
+
if (service) params.serviceName = service;
|
|
512
|
+
if (urlState.operation) params.spanName = urlState.operation;
|
|
513
|
+
if (urlState.lookback) {
|
|
514
|
+
const ms = LOOKBACK_MS[urlState.lookback];
|
|
515
|
+
if (ms) {
|
|
516
|
+
params.timestampMin = String((Date.now() - ms) * 1e6);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
if (urlState.tsMin) params.timestampMin = urlState.tsMin;
|
|
520
|
+
if (urlState.tsMax) params.timestampMax = urlState.tsMax;
|
|
521
|
+
if (urlState.minDuration) {
|
|
522
|
+
const parsed = parseDuration(urlState.minDuration);
|
|
523
|
+
if (parsed) params.durationMin = parsed;
|
|
524
|
+
}
|
|
525
|
+
if (urlState.maxDuration) {
|
|
526
|
+
const parsed = parseDuration(urlState.maxDuration);
|
|
527
|
+
if (parsed) params.durationMax = parsed;
|
|
528
|
+
}
|
|
529
|
+
if (urlState.tags) {
|
|
530
|
+
const tagMap = parseLogfmt(urlState.tags);
|
|
531
|
+
if (Object.keys(tagMap).length > 0) params.tags = tagMap;
|
|
532
|
+
}
|
|
533
|
+
return {
|
|
534
|
+
method: "searchTraceSummariesPage",
|
|
535
|
+
params,
|
|
536
|
+
} as DataSource;
|
|
537
|
+
}, [
|
|
538
|
+
service,
|
|
539
|
+
urlState.operation,
|
|
540
|
+
urlState.lookback,
|
|
541
|
+
urlState.tsMin,
|
|
542
|
+
urlState.tsMax,
|
|
543
|
+
urlState.minDuration,
|
|
544
|
+
urlState.maxDuration,
|
|
545
|
+
urlState.limit,
|
|
546
|
+
urlState.tags,
|
|
547
|
+
]);
|
|
409
548
|
|
|
410
549
|
const handleSearch = useCallback(
|
|
411
550
|
(filters: TraceSearchFilters) => {
|
|
412
|
-
|
|
413
|
-
|
|
551
|
+
pushURLState({
|
|
552
|
+
tab: "services",
|
|
553
|
+
service: filters.service ?? service,
|
|
554
|
+
operation: filters.operation ?? null,
|
|
555
|
+
tags: filters.tags ?? null,
|
|
556
|
+
lookback: filters.lookback ?? null,
|
|
557
|
+
minDuration: filters.minDuration ?? null,
|
|
558
|
+
maxDuration: filters.maxDuration ?? null,
|
|
414
559
|
limit: filters.limit,
|
|
415
|
-
|
|
416
|
-
};
|
|
417
|
-
if (filters.operation) params.spanName = filters.operation;
|
|
418
|
-
if (filters.lookbackMs) {
|
|
419
|
-
params.timestampMin = String((Date.now() - filters.lookbackMs) * 1e6);
|
|
420
|
-
}
|
|
421
|
-
if (filters.minDuration) {
|
|
422
|
-
const parsed = parseDuration(filters.minDuration);
|
|
423
|
-
if (parsed) params.durationMin = parsed;
|
|
424
|
-
}
|
|
425
|
-
if (filters.maxDuration) {
|
|
426
|
-
const parsed = parseDuration(filters.maxDuration);
|
|
427
|
-
if (parsed) params.durationMax = parsed;
|
|
428
|
-
}
|
|
429
|
-
setDs({
|
|
430
|
-
method: "searchTracesPage",
|
|
431
|
-
params,
|
|
432
|
-
} as DataSource);
|
|
560
|
+
});
|
|
433
561
|
},
|
|
434
562
|
[service]
|
|
435
563
|
);
|
|
436
564
|
|
|
565
|
+
// Fetch trace summaries
|
|
437
566
|
const { data, loading, error } = useKopaiData<{
|
|
438
|
-
data:
|
|
567
|
+
data: TraceSummaryRow[];
|
|
439
568
|
nextCursor: string | null;
|
|
440
569
|
}>(ds);
|
|
441
570
|
|
|
442
|
-
// Fetch
|
|
443
|
-
const
|
|
444
|
-
|
|
445
|
-
|
|
571
|
+
// Fetch services list
|
|
572
|
+
const serviceDs = useMemo<DataSource>(
|
|
573
|
+
() => ({ method: "getServices" as const }),
|
|
574
|
+
[]
|
|
446
575
|
);
|
|
576
|
+
const { data: servicesData } = useKopaiData<{ services: string[] }>(
|
|
577
|
+
serviceDs
|
|
578
|
+
);
|
|
579
|
+
const _services = servicesData?.services ?? [];
|
|
580
|
+
|
|
581
|
+
// Fetch operations for selected service
|
|
582
|
+
const operationDs = useMemo<DataSource | undefined>(
|
|
583
|
+
() =>
|
|
584
|
+
service
|
|
585
|
+
? { method: "getOperations" as const, params: { serviceName: service } }
|
|
586
|
+
: undefined,
|
|
587
|
+
[service]
|
|
588
|
+
);
|
|
589
|
+
const { data: opsData } = useKopaiData<{ operations: string[] }>(operationDs);
|
|
590
|
+
const operations = opsData?.operations ?? [];
|
|
447
591
|
|
|
448
|
-
|
|
449
|
-
if (!data?.data?.length) {
|
|
450
|
-
setFullTraces(new Map());
|
|
451
|
-
return;
|
|
452
|
-
}
|
|
453
|
-
const traceIds = [...new Set(data.data.map((r) => r.TraceId))];
|
|
454
|
-
const ac = new AbortController();
|
|
455
|
-
|
|
456
|
-
Promise.allSettled(
|
|
457
|
-
traceIds.map((tid) =>
|
|
458
|
-
client
|
|
459
|
-
.getTrace(tid, { signal: ac.signal })
|
|
460
|
-
.then((spans) => [tid, spans] as const)
|
|
461
|
-
)
|
|
462
|
-
)
|
|
463
|
-
.then((results) => {
|
|
464
|
-
if (!ac.signal.aborted) {
|
|
465
|
-
const entries = results
|
|
466
|
-
.filter(
|
|
467
|
-
(
|
|
468
|
-
r
|
|
469
|
-
): r is PromiseFulfilledResult<
|
|
470
|
-
readonly [string, OtelTracesRow[]]
|
|
471
|
-
> => r.status === "fulfilled"
|
|
472
|
-
)
|
|
473
|
-
.map((r) => r.value);
|
|
474
|
-
setFullTraces(new Map(entries));
|
|
475
|
-
}
|
|
476
|
-
})
|
|
477
|
-
.catch((err) => {
|
|
478
|
-
if (!ac.signal.aborted)
|
|
479
|
-
console.error("Failed to fetch full traces", err);
|
|
480
|
-
});
|
|
481
|
-
|
|
482
|
-
return () => ac.abort();
|
|
483
|
-
}, [data, client]);
|
|
484
|
-
|
|
485
|
-
// Derive unique operations for filter dropdown
|
|
486
|
-
const operations = useMemo(() => {
|
|
487
|
-
if (!data?.data) return [];
|
|
488
|
-
const set = new Set<string>();
|
|
489
|
-
for (const row of data.data) {
|
|
490
|
-
if (row.SpanName) set.add(row.SpanName);
|
|
491
|
-
}
|
|
492
|
-
return Array.from(set).sort();
|
|
493
|
-
}, [data]);
|
|
494
|
-
|
|
592
|
+
// Map TraceSummaryRow → TraceSummary
|
|
495
593
|
const traces = useMemo<TraceSummary[]>(() => {
|
|
496
594
|
if (!data?.data) return [];
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
595
|
+
return data.data.map((row) => ({
|
|
596
|
+
traceId: row.traceId,
|
|
597
|
+
rootSpanName: row.rootSpanName,
|
|
598
|
+
serviceName: row.rootServiceName,
|
|
599
|
+
durationMs: parseInt(row.durationNs, 10) / 1e6,
|
|
600
|
+
statusCode: row.errorCount > 0 ? "ERROR" : "OK",
|
|
601
|
+
timestampMs: parseInt(row.startTimeNs, 10) / 1e6,
|
|
602
|
+
spanCount: row.spanCount,
|
|
603
|
+
services: row.services,
|
|
604
|
+
errorCount: row.errorCount,
|
|
605
|
+
}));
|
|
606
|
+
}, [data]);
|
|
503
607
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
const spans = fullSpans ?? searchSpans;
|
|
507
|
-
|
|
508
|
-
const root = spans.find((s) => !s.ParentSpanId) ?? spans[0]!;
|
|
509
|
-
const durationNs = root.Duration ? parseInt(root.Duration, 10) : 0;
|
|
510
|
-
|
|
511
|
-
const svcMap = new Map<string, { count: number; hasError: boolean }>();
|
|
512
|
-
let errorCount = 0;
|
|
513
|
-
for (const s of spans) {
|
|
514
|
-
const svcName = s.ServiceName ?? "unknown";
|
|
515
|
-
const entry = svcMap.get(svcName) ?? { count: 0, hasError: false };
|
|
516
|
-
entry.count++;
|
|
517
|
-
if (s.StatusCode === "ERROR") {
|
|
518
|
-
entry.hasError = true;
|
|
519
|
-
errorCount++;
|
|
520
|
-
}
|
|
521
|
-
svcMap.set(svcName, entry);
|
|
522
|
-
}
|
|
523
|
-
const services = Array.from(svcMap.entries())
|
|
524
|
-
.map(([name, v]) => ({ name, count: v.count, hasError: v.hasError }))
|
|
525
|
-
.sort((a, b) => b.count - a.count);
|
|
526
|
-
|
|
527
|
-
return {
|
|
528
|
-
traceId,
|
|
529
|
-
rootSpanName: root.SpanName ?? "unknown",
|
|
530
|
-
serviceName: root.ServiceName ?? "unknown",
|
|
531
|
-
durationMs: durationNs / 1e6,
|
|
532
|
-
statusCode: root.StatusCode ?? "UNSET",
|
|
533
|
-
timestampMs: parseInt(root.Timestamp, 10) / 1e6,
|
|
534
|
-
spanCount: spans.length,
|
|
535
|
-
services,
|
|
536
|
-
errorCount,
|
|
537
|
-
};
|
|
538
|
-
});
|
|
539
|
-
}, [data, fullTraces]);
|
|
608
|
+
// Auto-execute on mount — the ds is already built from URL state,
|
|
609
|
+
// so useKopaiData fires automatically. No extra effect needed.
|
|
540
610
|
|
|
541
611
|
return (
|
|
542
612
|
<TraceSearch
|
|
543
|
-
|
|
613
|
+
services={_services}
|
|
614
|
+
service={service ?? ""}
|
|
544
615
|
traces={traces}
|
|
545
616
|
operations={operations}
|
|
546
617
|
isLoading={loading}
|
|
547
618
|
error={error ?? undefined}
|
|
548
619
|
onSelectTrace={onSelectTrace}
|
|
549
|
-
|
|
620
|
+
onCompare={onCompare}
|
|
550
621
|
onSearch={handleSearch}
|
|
551
622
|
/>
|
|
552
623
|
);
|
|
553
624
|
}
|
|
554
625
|
|
|
555
626
|
function TraceDetailView({
|
|
556
|
-
service,
|
|
557
627
|
traceId,
|
|
558
628
|
selectedSpanId,
|
|
559
629
|
onSelectSpan,
|
|
630
|
+
onDeselectSpan,
|
|
560
631
|
onBack,
|
|
561
632
|
}: {
|
|
562
|
-
service: string;
|
|
563
633
|
traceId: string;
|
|
564
634
|
selectedSpanId: string | null;
|
|
565
635
|
onSelectSpan: (spanId: string) => void;
|
|
636
|
+
onDeselectSpan: () => void;
|
|
566
637
|
onBack: () => void;
|
|
567
638
|
}) {
|
|
568
639
|
const ds = useMemo<DataSource>(
|
|
@@ -577,44 +648,42 @@ function TraceDetailView({
|
|
|
577
648
|
|
|
578
649
|
return (
|
|
579
650
|
<TraceDetail
|
|
580
|
-
service={service}
|
|
581
651
|
traceId={traceId}
|
|
582
652
|
rows={data ?? []}
|
|
583
653
|
isLoading={loading}
|
|
584
654
|
error={error ?? undefined}
|
|
585
655
|
selectedSpanId={selectedSpanId ?? undefined}
|
|
586
656
|
onSpanClick={(span) => onSelectSpan(span.spanId)}
|
|
657
|
+
onSpanDeselect={onDeselectSpan}
|
|
587
658
|
onBack={onBack}
|
|
588
659
|
/>
|
|
589
660
|
);
|
|
590
661
|
}
|
|
591
662
|
|
|
592
663
|
function ServicesTab({
|
|
593
|
-
selectedService,
|
|
594
664
|
selectedTraceId,
|
|
595
665
|
selectedSpanId,
|
|
596
|
-
|
|
666
|
+
compareParam,
|
|
597
667
|
onSelectTrace,
|
|
598
668
|
onSelectSpan,
|
|
599
|
-
|
|
600
|
-
|
|
669
|
+
onDeselectSpan,
|
|
670
|
+
onBack,
|
|
671
|
+
onCompare,
|
|
601
672
|
}: {
|
|
602
|
-
selectedService: string | null;
|
|
603
673
|
selectedTraceId: string | null;
|
|
604
674
|
selectedSpanId: string | null;
|
|
605
|
-
|
|
675
|
+
compareParam: string | null;
|
|
606
676
|
onSelectTrace: (traceId: string) => void;
|
|
607
677
|
onSelectSpan: (spanId: string) => void;
|
|
608
|
-
|
|
609
|
-
|
|
678
|
+
onDeselectSpan: () => void;
|
|
679
|
+
onBack: () => void;
|
|
680
|
+
onCompare: (traceIds: [string, string]) => void;
|
|
610
681
|
}) {
|
|
611
682
|
useRegisterShortcuts("services-tab", SERVICES_SHORTCUTS);
|
|
612
683
|
|
|
613
|
-
// Backspace → navigate back
|
|
614
|
-
const
|
|
615
|
-
|
|
616
|
-
const backToTraceListRef = useRef(onBackToTraceList);
|
|
617
|
-
backToTraceListRef.current = onBackToTraceList;
|
|
684
|
+
// Backspace → navigate back
|
|
685
|
+
const backRef = useRef(onBack);
|
|
686
|
+
backRef.current = onBack;
|
|
618
687
|
useEffect(() => {
|
|
619
688
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
620
689
|
if (
|
|
@@ -625,38 +694,43 @@ function ServicesTab({
|
|
|
625
694
|
return;
|
|
626
695
|
if (e.key === "Backspace") {
|
|
627
696
|
e.preventDefault();
|
|
628
|
-
|
|
629
|
-
backToTraceListRef.current();
|
|
630
|
-
} else if (selectedService) {
|
|
631
|
-
backToServicesRef.current();
|
|
632
|
-
}
|
|
697
|
+
backRef.current();
|
|
633
698
|
}
|
|
634
699
|
};
|
|
635
700
|
window.addEventListener("keydown", handleKeyDown);
|
|
636
701
|
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
637
|
-
}, [
|
|
702
|
+
}, []);
|
|
638
703
|
|
|
639
|
-
|
|
704
|
+
// Comparison view
|
|
705
|
+
if (compareParam) {
|
|
706
|
+
const [traceIdA, traceIdB] = compareParam.split(",");
|
|
707
|
+
if (traceIdA && traceIdB) {
|
|
708
|
+
return (
|
|
709
|
+
<TraceComparison
|
|
710
|
+
traceIdA={traceIdA}
|
|
711
|
+
traceIdB={traceIdB}
|
|
712
|
+
onBack={onBack}
|
|
713
|
+
/>
|
|
714
|
+
);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
if (selectedTraceId) {
|
|
640
719
|
return (
|
|
641
720
|
<TraceDetailView
|
|
642
|
-
service={selectedService}
|
|
643
721
|
traceId={selectedTraceId}
|
|
644
722
|
selectedSpanId={selectedSpanId}
|
|
645
723
|
onSelectSpan={onSelectSpan}
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
);
|
|
649
|
-
}
|
|
650
|
-
if (selectedService) {
|
|
651
|
-
return (
|
|
652
|
-
<TraceSearchView
|
|
653
|
-
service={selectedService}
|
|
654
|
-
onBack={onBackToServices}
|
|
655
|
-
onSelectTrace={onSelectTrace}
|
|
724
|
+
onDeselectSpan={onDeselectSpan}
|
|
725
|
+
onBack={onBack}
|
|
656
726
|
/>
|
|
657
727
|
);
|
|
658
728
|
}
|
|
659
|
-
|
|
729
|
+
|
|
730
|
+
// Default: TraceSearchView directly
|
|
731
|
+
return (
|
|
732
|
+
<TraceSearchView onSelectTrace={onSelectTrace} onCompare={onCompare} />
|
|
733
|
+
);
|
|
660
734
|
}
|
|
661
735
|
|
|
662
736
|
// ---------------------------------------------------------------------------
|
|
@@ -778,52 +852,57 @@ export default function ObservabilityPage({ client }: ObservabilityPageProps) {
|
|
|
778
852
|
const activeClient = client ?? getDefaultClient();
|
|
779
853
|
const {
|
|
780
854
|
tab: activeTab,
|
|
781
|
-
service: selectedService,
|
|
782
855
|
trace: selectedTraceId,
|
|
783
856
|
span: selectedSpanId,
|
|
857
|
+
compare: compareParam,
|
|
784
858
|
} = useURLState();
|
|
785
859
|
|
|
786
860
|
const handleTabChange = useCallback((tab: Tab) => {
|
|
787
861
|
pushURLState({ tab });
|
|
788
862
|
}, []);
|
|
789
863
|
|
|
790
|
-
const
|
|
791
|
-
pushURLState({ tab: "services",
|
|
864
|
+
const handleSelectTrace = useCallback((traceId: string) => {
|
|
865
|
+
pushURLState({ ...readURLState(), tab: "services", trace: traceId });
|
|
792
866
|
}, []);
|
|
793
867
|
|
|
794
|
-
const
|
|
795
|
-
(
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
});
|
|
801
|
-
},
|
|
802
|
-
[selectedService]
|
|
803
|
-
);
|
|
868
|
+
const handleSelectSpan = useCallback((spanId: string) => {
|
|
869
|
+
pushURLState(
|
|
870
|
+
{ ...readURLState(), tab: "services", span: spanId },
|
|
871
|
+
{ replace: true }
|
|
872
|
+
);
|
|
873
|
+
}, []);
|
|
804
874
|
|
|
805
|
-
const
|
|
806
|
-
(
|
|
807
|
-
|
|
808
|
-
{
|
|
809
|
-
tab: "services",
|
|
810
|
-
service: selectedService,
|
|
811
|
-
trace: selectedTraceId,
|
|
812
|
-
span: spanId,
|
|
813
|
-
},
|
|
814
|
-
{ replace: true }
|
|
815
|
-
);
|
|
816
|
-
},
|
|
817
|
-
[selectedService, selectedTraceId]
|
|
818
|
-
);
|
|
875
|
+
const handleDeselectSpan = useCallback(() => {
|
|
876
|
+
pushURLState({ ...readURLState(), span: null }, { replace: true });
|
|
877
|
+
}, []);
|
|
819
878
|
|
|
820
|
-
const
|
|
821
|
-
pushURLState({
|
|
879
|
+
const handleCompare = useCallback((traceIds: [string, string]) => {
|
|
880
|
+
pushURLState({
|
|
881
|
+
...readURLState(),
|
|
882
|
+
tab: "services",
|
|
883
|
+
trace: null,
|
|
884
|
+
span: null,
|
|
885
|
+
view: null,
|
|
886
|
+
uiFind: null,
|
|
887
|
+
viewStart: null,
|
|
888
|
+
viewEnd: null,
|
|
889
|
+
compare: traceIds.join(","),
|
|
890
|
+
});
|
|
822
891
|
}, []);
|
|
823
892
|
|
|
824
|
-
const
|
|
825
|
-
pushURLState({
|
|
826
|
-
|
|
893
|
+
const handleBack = useCallback(() => {
|
|
894
|
+
pushURLState({
|
|
895
|
+
...readURLState(),
|
|
896
|
+
tab: "services",
|
|
897
|
+
trace: null,
|
|
898
|
+
span: null,
|
|
899
|
+
view: null,
|
|
900
|
+
uiFind: null,
|
|
901
|
+
viewStart: null,
|
|
902
|
+
viewEnd: null,
|
|
903
|
+
compare: null,
|
|
904
|
+
});
|
|
905
|
+
}, []);
|
|
827
906
|
|
|
828
907
|
return (
|
|
829
908
|
<KopaiSDKProvider client={activeClient}>
|
|
@@ -841,14 +920,14 @@ export default function ObservabilityPage({ client }: ObservabilityPageProps) {
|
|
|
841
920
|
{activeTab === "logs" && <LogsTab />}
|
|
842
921
|
{activeTab === "services" && (
|
|
843
922
|
<ServicesTab
|
|
844
|
-
selectedService={selectedService}
|
|
845
923
|
selectedTraceId={selectedTraceId}
|
|
846
924
|
selectedSpanId={selectedSpanId}
|
|
847
|
-
|
|
925
|
+
compareParam={compareParam}
|
|
848
926
|
onSelectTrace={handleSelectTrace}
|
|
849
927
|
onSelectSpan={handleSelectSpan}
|
|
850
|
-
|
|
851
|
-
|
|
928
|
+
onDeselectSpan={handleDeselectSpan}
|
|
929
|
+
onBack={handleBack}
|
|
930
|
+
onCompare={handleCompare}
|
|
852
931
|
/>
|
|
853
932
|
)}
|
|
854
933
|
{activeTab === "metrics" && <MetricsTab />}
|