@kopai/ui 0.7.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 +2451 -1157
- 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 +2399 -1099
- 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 +25 -14
- 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/OtelLogTimeline.tsx +9 -5
- 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 +4 -0
- package/src/hooks/use-kopai-data.ts +11 -0
- package/src/hooks/use-live-logs.test.ts +4 -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 +16 -12
- package/src/pages/observability.tsx +323 -245
- package/src/providers/kopai-provider.tsx +4 -0
package/dist/index.mjs
CHANGED
|
@@ -2,12 +2,11 @@ import { createContext, memo, useCallback, useContext, useEffect, useMemo, useRe
|
|
|
2
2
|
import { QueryClient, QueryClientProvider, useQuery } from "@tanstack/react-query";
|
|
3
3
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
4
4
|
import { KopaiClient } from "@kopai/sdk";
|
|
5
|
-
import
|
|
5
|
+
import { z } from "zod";
|
|
6
6
|
import { dataFilterSchemas } from "@kopai/core";
|
|
7
|
-
import {
|
|
7
|
+
import { Area, AreaChart, Bar, BarChart, Brush, CartesianGrid, Cell, Legend, Line, LineChart, ReferenceLine, ResponsiveContainer, Scatter, ScatterChart, Tooltip, XAxis, YAxis } from "recharts";
|
|
8
8
|
import { createPortal } from "react-dom";
|
|
9
|
-
import {
|
|
10
|
-
|
|
9
|
+
import { useVirtualizer } from "@tanstack/react-virtual";
|
|
11
10
|
//#region src/providers/kopai-provider.tsx
|
|
12
11
|
const KopaiSDKContext = createContext(null);
|
|
13
12
|
const queryClient = new QueryClient({ defaultOptions: { queries: {
|
|
@@ -29,7 +28,6 @@ function useKopaiSDK() {
|
|
|
29
28
|
if (!ctx) throw new Error("useKopaiSDK must be used within KopaiSDKProvider");
|
|
30
29
|
return ctx.client;
|
|
31
30
|
}
|
|
32
|
-
|
|
33
31
|
//#endregion
|
|
34
32
|
//#region src/hooks/use-kopai-data.ts
|
|
35
33
|
function fetchForDataSource(client, dataSource, signal) {
|
|
@@ -39,6 +37,9 @@ function fetchForDataSource(client, dataSource, signal) {
|
|
|
39
37
|
case "searchMetricsPage": return client.searchMetricsPage(dataSource.params, { signal });
|
|
40
38
|
case "getTrace": return client.getTrace(dataSource.params.traceId, { signal });
|
|
41
39
|
case "discoverMetrics": return client.discoverMetrics({ signal });
|
|
40
|
+
case "getServices": return client.getServices({ signal });
|
|
41
|
+
case "getOperations": return client.getOperations(dataSource.params.serviceName, { signal });
|
|
42
|
+
case "searchTraceSummariesPage": return client.searchTraceSummariesPage(dataSource.params, { signal });
|
|
42
43
|
default: {
|
|
43
44
|
const exhaustiveCheck = dataSource;
|
|
44
45
|
throw new Error(`Unknown method: ${exhaustiveCheck.method}`);
|
|
@@ -64,7 +65,6 @@ function useKopaiData(dataSource) {
|
|
|
64
65
|
refetch
|
|
65
66
|
};
|
|
66
67
|
}
|
|
67
|
-
|
|
68
68
|
//#endregion
|
|
69
69
|
//#region src/lib/log-buffer.ts
|
|
70
70
|
function logKey(row) {
|
|
@@ -116,7 +116,6 @@ var LogBuffer = class {
|
|
|
116
116
|
this.keys.clear();
|
|
117
117
|
}
|
|
118
118
|
};
|
|
119
|
-
|
|
120
119
|
//#endregion
|
|
121
120
|
//#region src/hooks/use-live-logs.ts
|
|
122
121
|
function useLiveLogs({ params, pollIntervalMs = 3e3, maxLogs = 1e3, enabled = true }) {
|
|
@@ -171,7 +170,6 @@ function useLiveLogs({ params, pollIntervalMs = 3e3, maxLogs = 1e3, enabled = tr
|
|
|
171
170
|
setLive
|
|
172
171
|
};
|
|
173
172
|
}
|
|
174
|
-
|
|
175
173
|
//#endregion
|
|
176
174
|
//#region src/lib/component-catalog.ts
|
|
177
175
|
const dataSourceSchema = z.discriminatedUnion("method", [
|
|
@@ -199,6 +197,21 @@ const dataSourceSchema = z.discriminatedUnion("method", [
|
|
|
199
197
|
method: z.literal("discoverMetrics"),
|
|
200
198
|
params: z.object({}).optional(),
|
|
201
199
|
refetchIntervalMs: z.number().optional()
|
|
200
|
+
}),
|
|
201
|
+
z.object({
|
|
202
|
+
method: z.literal("getServices"),
|
|
203
|
+
params: z.object({}).optional(),
|
|
204
|
+
refetchIntervalMs: z.number().optional()
|
|
205
|
+
}),
|
|
206
|
+
z.object({
|
|
207
|
+
method: z.literal("getOperations"),
|
|
208
|
+
params: z.object({ serviceName: z.string() }),
|
|
209
|
+
refetchIntervalMs: z.number().optional()
|
|
210
|
+
}),
|
|
211
|
+
z.object({
|
|
212
|
+
method: z.literal("searchTraceSummariesPage"),
|
|
213
|
+
params: dataFilterSchemas.traceSummariesFilterSchema,
|
|
214
|
+
refetchIntervalMs: z.number().optional()
|
|
202
215
|
})
|
|
203
216
|
]);
|
|
204
217
|
const componentDefinitionSchema = z.object({
|
|
@@ -206,7 +219,7 @@ const componentDefinitionSchema = z.object({
|
|
|
206
219
|
description: z.string().describe("Component description to be displayed by the prompt generator"),
|
|
207
220
|
props: z.unknown()
|
|
208
221
|
}).describe("All options and properties necessary to render the React component with renderer");
|
|
209
|
-
|
|
222
|
+
z.object({
|
|
210
223
|
name: z.string().describe("catalog name"),
|
|
211
224
|
components: z.record(z.string().describe("React component name"), componentDefinitionSchema)
|
|
212
225
|
});
|
|
@@ -253,7 +266,6 @@ function createCatalog(catalogConfig) {
|
|
|
253
266
|
uiTreeSchema
|
|
254
267
|
};
|
|
255
268
|
}
|
|
256
|
-
|
|
257
269
|
//#endregion
|
|
258
270
|
//#region src/lib/observability-catalog.ts
|
|
259
271
|
const observabilityCatalog = createCatalog({
|
|
@@ -412,7 +424,6 @@ const observabilityCatalog = createCatalog({
|
|
|
412
424
|
}
|
|
413
425
|
}
|
|
414
426
|
});
|
|
415
|
-
|
|
416
427
|
//#endregion
|
|
417
428
|
//#region src/components/observability/TabBar/index.tsx
|
|
418
429
|
function renderLabel(label, shortcutKey) {
|
|
@@ -441,7 +452,6 @@ function TabBar({ tabs, active, onChange }) {
|
|
|
441
452
|
}, t.key))
|
|
442
453
|
});
|
|
443
454
|
}
|
|
444
|
-
|
|
445
455
|
//#endregion
|
|
446
456
|
//#region src/components/observability/utils/colors.ts
|
|
447
457
|
/**
|
|
@@ -461,38 +471,6 @@ function getSpanBarColor(serviceName, isError) {
|
|
|
461
471
|
if (isError) return ERROR_COLOR;
|
|
462
472
|
return getServiceColor(serviceName);
|
|
463
473
|
}
|
|
464
|
-
|
|
465
|
-
//#endregion
|
|
466
|
-
//#region src/components/observability/ServiceList/index.tsx
|
|
467
|
-
function ServiceList({ services, isLoading, error, onSelect }) {
|
|
468
|
-
if (isLoading) return /* @__PURE__ */ jsxs("div", {
|
|
469
|
-
className: "flex items-center gap-2 text-muted-foreground py-8",
|
|
470
|
-
children: [/* @__PURE__ */ jsx("div", { className: "w-4 h-4 border-2 border-muted-foreground border-t-transparent rounded-full animate-spin" }), "Loading services..."]
|
|
471
|
-
});
|
|
472
|
-
if (error) return /* @__PURE__ */ jsxs("div", {
|
|
473
|
-
className: "text-red-400 py-4",
|
|
474
|
-
children: ["Error loading services: ", error.message]
|
|
475
|
-
});
|
|
476
|
-
if (services.length === 0) return /* @__PURE__ */ jsx("div", {
|
|
477
|
-
className: "text-muted-foreground py-8",
|
|
478
|
-
children: "No services found"
|
|
479
|
-
});
|
|
480
|
-
return /* @__PURE__ */ jsx("div", {
|
|
481
|
-
className: "space-y-1",
|
|
482
|
-
children: services.map((svc) => /* @__PURE__ */ jsx("button", {
|
|
483
|
-
onClick: () => onSelect(svc.name),
|
|
484
|
-
className: "w-full text-left px-4 py-3 rounded-lg border border-border hover:border-foreground/30 hover:bg-muted/50 transition-colors group",
|
|
485
|
-
children: /* @__PURE__ */ jsxs("span", {
|
|
486
|
-
className: "flex items-center gap-2 font-medium text-foreground",
|
|
487
|
-
children: [/* @__PURE__ */ jsx("span", {
|
|
488
|
-
className: "inline-block w-2.5 h-2.5 rounded-full shrink-0",
|
|
489
|
-
style: { backgroundColor: getServiceColor(svc.name) }
|
|
490
|
-
}), svc.name]
|
|
491
|
-
})
|
|
492
|
-
}, svc.name))
|
|
493
|
-
});
|
|
494
|
-
}
|
|
495
|
-
|
|
496
474
|
//#endregion
|
|
497
475
|
//#region src/components/observability/utils/time.ts
|
|
498
476
|
/**
|
|
@@ -514,6 +492,10 @@ function formatTimestamp$1(timestampMs) {
|
|
|
514
492
|
timeZoneName: "short"
|
|
515
493
|
});
|
|
516
494
|
}
|
|
495
|
+
function formatRelativeTime$1(eventTimeMs, spanStartMs) {
|
|
496
|
+
const relativeMs = eventTimeMs - spanStartMs;
|
|
497
|
+
return `${relativeMs < 0 ? "-" : "+"}${formatDuration(Math.abs(relativeMs))}`;
|
|
498
|
+
}
|
|
517
499
|
function calculateRelativeTime(timeMs, minTimeMs, maxTimeMs) {
|
|
518
500
|
const totalDuration = maxTimeMs - minTimeMs;
|
|
519
501
|
if (totalDuration === 0) return 0;
|
|
@@ -523,259 +505,556 @@ function calculateRelativeDuration(durationMs, totalDurationMs) {
|
|
|
523
505
|
if (totalDurationMs === 0) return 0;
|
|
524
506
|
return durationMs / totalDurationMs;
|
|
525
507
|
}
|
|
526
|
-
|
|
527
508
|
//#endregion
|
|
528
|
-
//#region src/components/observability/TraceSearch/
|
|
509
|
+
//#region src/components/observability/TraceSearch/SearchForm.tsx
|
|
510
|
+
/**
|
|
511
|
+
* SearchForm - Jaeger-style sidebar search form for trace filtering.
|
|
512
|
+
* Owns its own form state; parent only receives values on submit.
|
|
513
|
+
*/
|
|
529
514
|
const LOOKBACK_OPTIONS$1 = [
|
|
530
515
|
{
|
|
531
516
|
label: "Last 5 Minutes",
|
|
532
|
-
|
|
517
|
+
value: "5m"
|
|
533
518
|
},
|
|
534
519
|
{
|
|
535
520
|
label: "Last 15 Minutes",
|
|
536
|
-
|
|
521
|
+
value: "15m"
|
|
537
522
|
},
|
|
538
523
|
{
|
|
539
524
|
label: "Last 30 Minutes",
|
|
540
|
-
|
|
525
|
+
value: "30m"
|
|
541
526
|
},
|
|
542
527
|
{
|
|
543
528
|
label: "Last 1 Hour",
|
|
544
|
-
|
|
529
|
+
value: "1h"
|
|
545
530
|
},
|
|
546
531
|
{
|
|
547
532
|
label: "Last 2 Hours",
|
|
548
|
-
|
|
533
|
+
value: "2h"
|
|
549
534
|
},
|
|
550
535
|
{
|
|
551
536
|
label: "Last 6 Hours",
|
|
552
|
-
|
|
537
|
+
value: "6h"
|
|
553
538
|
},
|
|
554
539
|
{
|
|
555
540
|
label: "Last 12 Hours",
|
|
556
|
-
|
|
541
|
+
value: "12h"
|
|
557
542
|
},
|
|
558
543
|
{
|
|
559
544
|
label: "Last 24 Hours",
|
|
560
|
-
|
|
545
|
+
value: "24h"
|
|
561
546
|
}
|
|
562
547
|
];
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
const [
|
|
566
|
-
const [
|
|
567
|
-
const [
|
|
568
|
-
const [
|
|
569
|
-
const [
|
|
570
|
-
const
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
548
|
+
const inputClass = "w-full bg-muted/50 border border-border rounded px-2 py-1.5 text-sm text-foreground";
|
|
549
|
+
function SearchForm({ services, operations, initialValues, onSubmit, isLoading }) {
|
|
550
|
+
const [service, setService] = useState(initialValues?.service ?? "");
|
|
551
|
+
const [operation, setOperation] = useState(initialValues?.operation ?? "");
|
|
552
|
+
const [tags, setTags] = useState(initialValues?.tags ?? "");
|
|
553
|
+
const [lookback, setLookback] = useState(initialValues?.lookback ?? "");
|
|
554
|
+
const [minDuration, setMinDuration] = useState(initialValues?.minDuration ?? "");
|
|
555
|
+
const [maxDuration, setMaxDuration] = useState(initialValues?.maxDuration ?? "");
|
|
556
|
+
const [limit, setLimit] = useState(initialValues?.limit ?? 20);
|
|
557
|
+
useEffect(() => {
|
|
558
|
+
if (initialValues?.service != null) setService(initialValues.service);
|
|
559
|
+
}, [initialValues?.service]);
|
|
560
|
+
const handleSubmit = () => {
|
|
561
|
+
onSubmit({
|
|
562
|
+
service,
|
|
563
|
+
operation,
|
|
564
|
+
tags,
|
|
565
|
+
lookback,
|
|
566
|
+
minDuration,
|
|
567
|
+
maxDuration,
|
|
576
568
|
limit
|
|
577
569
|
});
|
|
578
570
|
};
|
|
579
|
-
return /* @__PURE__ */ jsxs("div", {
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
/* @__PURE__ */ jsx("span", {
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
children: filtersOpen ? "▲" : "▼"
|
|
571
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
572
|
+
className: "space-y-4",
|
|
573
|
+
children: [
|
|
574
|
+
/* @__PURE__ */ jsx("h3", {
|
|
575
|
+
className: "text-sm font-semibold text-foreground uppercase tracking-wider",
|
|
576
|
+
children: "Search"
|
|
577
|
+
}),
|
|
578
|
+
/* @__PURE__ */ jsxs("label", {
|
|
579
|
+
className: "block space-y-1",
|
|
580
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
581
|
+
className: "text-xs text-muted-foreground",
|
|
582
|
+
children: "Service"
|
|
583
|
+
}), /* @__PURE__ */ jsxs("select", {
|
|
584
|
+
value: service,
|
|
585
|
+
onChange: (e) => setService(e.target.value),
|
|
586
|
+
className: inputClass,
|
|
587
|
+
children: [/* @__PURE__ */ jsx("option", {
|
|
588
|
+
value: "",
|
|
589
|
+
children: "All Services"
|
|
590
|
+
}), services.map((s) => /* @__PURE__ */ jsx("option", {
|
|
591
|
+
value: s,
|
|
592
|
+
children: s
|
|
593
|
+
}, s))]
|
|
603
594
|
})]
|
|
604
|
-
}),
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
}), operations.map((op) => /* @__PURE__ */ jsx("option", {
|
|
622
|
-
value: op,
|
|
623
|
-
children: op
|
|
624
|
-
}, op))]
|
|
625
|
-
})]
|
|
626
|
-
}),
|
|
627
|
-
/* @__PURE__ */ jsxs("label", {
|
|
628
|
-
className: "space-y-1",
|
|
629
|
-
children: [/* @__PURE__ */ jsx("span", {
|
|
630
|
-
className: "text-xs text-muted-foreground",
|
|
631
|
-
children: "Lookback"
|
|
632
|
-
}), /* @__PURE__ */ jsxs("select", {
|
|
633
|
-
value: lookbackIdx,
|
|
634
|
-
onChange: (e) => setLookbackIdx(Number(e.target.value)),
|
|
635
|
-
className: "w-full bg-muted/50 border border-border rounded px-2 py-1.5 text-sm text-foreground",
|
|
636
|
-
children: [/* @__PURE__ */ jsx("option", {
|
|
637
|
-
value: -1,
|
|
638
|
-
children: "All time"
|
|
639
|
-
}), LOOKBACK_OPTIONS$1.map((opt, i) => /* @__PURE__ */ jsx("option", {
|
|
640
|
-
value: i,
|
|
641
|
-
children: opt.label
|
|
642
|
-
}, i))]
|
|
643
|
-
})]
|
|
644
|
-
}),
|
|
645
|
-
/* @__PURE__ */ jsxs("label", {
|
|
646
|
-
className: "space-y-1",
|
|
647
|
-
children: [/* @__PURE__ */ jsx("span", {
|
|
648
|
-
className: "text-xs text-muted-foreground",
|
|
649
|
-
children: "Limit"
|
|
650
|
-
}), /* @__PURE__ */ jsx("input", {
|
|
651
|
-
type: "number",
|
|
652
|
-
min: 1,
|
|
653
|
-
max: 1e3,
|
|
654
|
-
value: limit,
|
|
655
|
-
onChange: (e) => {
|
|
656
|
-
const n = Number(e.target.value);
|
|
657
|
-
setLimit(Number.isNaN(n) ? 20 : Math.max(1, Math.min(1e3, n)));
|
|
658
|
-
},
|
|
659
|
-
className: "w-full bg-muted/50 border border-border rounded px-2 py-1.5 text-sm text-foreground"
|
|
660
|
-
})]
|
|
661
|
-
}),
|
|
662
|
-
/* @__PURE__ */ jsxs("label", {
|
|
663
|
-
className: "space-y-1",
|
|
664
|
-
children: [/* @__PURE__ */ jsx("span", {
|
|
665
|
-
className: "text-xs text-muted-foreground",
|
|
666
|
-
children: "Min Duration"
|
|
667
|
-
}), /* @__PURE__ */ jsx("input", {
|
|
668
|
-
type: "text",
|
|
669
|
-
placeholder: "e.g. 100ms",
|
|
670
|
-
value: minDuration,
|
|
671
|
-
onChange: (e) => setMinDuration(e.target.value),
|
|
672
|
-
className: "w-full bg-muted/50 border border-border rounded px-2 py-1.5 text-sm text-foreground placeholder:text-muted-foreground/50"
|
|
673
|
-
})]
|
|
674
|
-
}),
|
|
675
|
-
/* @__PURE__ */ jsxs("label", {
|
|
676
|
-
className: "space-y-1",
|
|
677
|
-
children: [/* @__PURE__ */ jsx("span", {
|
|
678
|
-
className: "text-xs text-muted-foreground",
|
|
679
|
-
children: "Max Duration"
|
|
680
|
-
}), /* @__PURE__ */ jsx("input", {
|
|
681
|
-
type: "text",
|
|
682
|
-
placeholder: "e.g. 5s",
|
|
683
|
-
value: maxDuration,
|
|
684
|
-
onChange: (e) => setMaxDuration(e.target.value),
|
|
685
|
-
className: "w-full bg-muted/50 border border-border rounded px-2 py-1.5 text-sm text-foreground placeholder:text-muted-foreground/50"
|
|
686
|
-
})]
|
|
687
|
-
})
|
|
688
|
-
]
|
|
689
|
-
}), /* @__PURE__ */ jsx("button", {
|
|
690
|
-
onClick: handleFindTraces,
|
|
691
|
-
className: "px-4 py-1.5 text-sm font-medium bg-foreground text-background rounded hover:bg-foreground/90 transition-colors",
|
|
692
|
-
children: "Find Traces"
|
|
595
|
+
}),
|
|
596
|
+
/* @__PURE__ */ jsxs("label", {
|
|
597
|
+
className: "block space-y-1",
|
|
598
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
599
|
+
className: "text-xs text-muted-foreground",
|
|
600
|
+
children: "Operation"
|
|
601
|
+
}), /* @__PURE__ */ jsxs("select", {
|
|
602
|
+
value: operation,
|
|
603
|
+
onChange: (e) => setOperation(e.target.value),
|
|
604
|
+
className: inputClass,
|
|
605
|
+
children: [/* @__PURE__ */ jsx("option", {
|
|
606
|
+
value: "",
|
|
607
|
+
children: "All Operations"
|
|
608
|
+
}), operations.map((op) => /* @__PURE__ */ jsx("option", {
|
|
609
|
+
value: op,
|
|
610
|
+
children: op
|
|
611
|
+
}, op))]
|
|
693
612
|
})]
|
|
694
|
-
})
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
613
|
+
}),
|
|
614
|
+
/* @__PURE__ */ jsxs("label", {
|
|
615
|
+
className: "block space-y-1",
|
|
616
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
617
|
+
className: "text-xs text-muted-foreground",
|
|
618
|
+
children: "Tags"
|
|
619
|
+
}), /* @__PURE__ */ jsx("textarea", {
|
|
620
|
+
value: tags,
|
|
621
|
+
onChange: (e) => setTags(e.target.value),
|
|
622
|
+
placeholder: "key=value key2=\"quoted value\"",
|
|
623
|
+
rows: 3,
|
|
624
|
+
className: `${inputClass} placeholder:text-muted-foreground/50 resize-y`
|
|
625
|
+
})]
|
|
626
|
+
}),
|
|
627
|
+
/* @__PURE__ */ jsxs("label", {
|
|
628
|
+
className: "block space-y-1",
|
|
629
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
630
|
+
className: "text-xs text-muted-foreground",
|
|
631
|
+
children: "Lookback"
|
|
632
|
+
}), /* @__PURE__ */ jsxs("select", {
|
|
633
|
+
value: lookback,
|
|
634
|
+
onChange: (e) => setLookback(e.target.value),
|
|
635
|
+
className: inputClass,
|
|
636
|
+
children: [/* @__PURE__ */ jsx("option", {
|
|
637
|
+
value: "",
|
|
638
|
+
children: "All time"
|
|
639
|
+
}), LOOKBACK_OPTIONS$1.map((opt) => /* @__PURE__ */ jsx("option", {
|
|
640
|
+
value: opt.value,
|
|
641
|
+
children: opt.label
|
|
642
|
+
}, opt.value))]
|
|
643
|
+
})]
|
|
644
|
+
}),
|
|
645
|
+
/* @__PURE__ */ jsxs("div", {
|
|
646
|
+
className: "grid grid-cols-2 gap-2",
|
|
647
|
+
children: [/* @__PURE__ */ jsxs("label", {
|
|
648
|
+
className: "block space-y-1",
|
|
649
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
650
|
+
className: "text-xs text-muted-foreground",
|
|
651
|
+
children: "Min Duration"
|
|
652
|
+
}), /* @__PURE__ */ jsx("input", {
|
|
653
|
+
type: "text",
|
|
654
|
+
placeholder: "e.g. 100ms",
|
|
655
|
+
value: minDuration,
|
|
656
|
+
onChange: (e) => setMinDuration(e.target.value),
|
|
657
|
+
className: `${inputClass} placeholder:text-muted-foreground/50`
|
|
658
|
+
})]
|
|
659
|
+
}), /* @__PURE__ */ jsxs("label", {
|
|
660
|
+
className: "block space-y-1",
|
|
661
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
662
|
+
className: "text-xs text-muted-foreground",
|
|
663
|
+
children: "Max Duration"
|
|
664
|
+
}), /* @__PURE__ */ jsx("input", {
|
|
665
|
+
type: "text",
|
|
666
|
+
placeholder: "e.g. 5s",
|
|
667
|
+
value: maxDuration,
|
|
668
|
+
onChange: (e) => setMaxDuration(e.target.value),
|
|
669
|
+
className: `${inputClass} placeholder:text-muted-foreground/50`
|
|
670
|
+
})]
|
|
671
|
+
})]
|
|
672
|
+
}),
|
|
673
|
+
/* @__PURE__ */ jsxs("label", {
|
|
674
|
+
className: "block space-y-1",
|
|
675
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
676
|
+
className: "text-xs text-muted-foreground",
|
|
677
|
+
children: "Limit"
|
|
678
|
+
}), /* @__PURE__ */ jsx("input", {
|
|
679
|
+
type: "number",
|
|
680
|
+
min: 1,
|
|
681
|
+
max: 1e3,
|
|
682
|
+
value: limit,
|
|
683
|
+
onChange: (e) => {
|
|
684
|
+
const n = Number(e.target.value);
|
|
685
|
+
setLimit(Number.isNaN(n) ? 20 : Math.max(1, Math.min(1e3, n)));
|
|
686
|
+
},
|
|
687
|
+
className: inputClass
|
|
688
|
+
})]
|
|
689
|
+
}),
|
|
690
|
+
/* @__PURE__ */ jsx("button", {
|
|
691
|
+
onClick: handleSubmit,
|
|
692
|
+
disabled: isLoading,
|
|
693
|
+
className: "w-full px-4 py-2 text-sm font-medium bg-foreground text-background rounded hover:bg-foreground/90 transition-colors disabled:opacity-50",
|
|
694
|
+
children: isLoading ? "Searching..." : "Find Traces"
|
|
695
|
+
})
|
|
696
|
+
]
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
//#endregion
|
|
700
|
+
//#region src/components/observability/TraceSearch/ScatterPlot.tsx
|
|
701
|
+
/**
|
|
702
|
+
* ScatterPlot - Scatter chart showing trace duration vs timestamp.
|
|
703
|
+
*/
|
|
704
|
+
function CustomTooltip$1({ active, payload }) {
|
|
705
|
+
if (!active || !payload?.[0]) return null;
|
|
706
|
+
const d = payload[0].payload;
|
|
707
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
708
|
+
className: "bg-background border border-border rounded px-3 py-2 text-xs shadow-lg",
|
|
709
|
+
children: [
|
|
710
|
+
/* @__PURE__ */ jsxs("div", {
|
|
711
|
+
className: "font-medium text-foreground",
|
|
713
712
|
children: [
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
713
|
+
d.serviceName,
|
|
714
|
+
": ",
|
|
715
|
+
d.rootSpanName
|
|
716
|
+
]
|
|
717
|
+
}),
|
|
718
|
+
/* @__PURE__ */ jsxs("div", {
|
|
719
|
+
className: "text-muted-foreground mt-1",
|
|
720
|
+
children: [
|
|
721
|
+
d.spanCount,
|
|
722
|
+
" span",
|
|
723
|
+
d.spanCount !== 1 ? "s" : "",
|
|
724
|
+
" ·",
|
|
725
|
+
" ",
|
|
726
|
+
formatDuration(d.y)
|
|
727
|
+
]
|
|
728
|
+
}),
|
|
729
|
+
/* @__PURE__ */ jsx("div", {
|
|
730
|
+
className: "text-muted-foreground",
|
|
731
|
+
children: formatTimestamp$1(d.x)
|
|
732
|
+
})
|
|
733
|
+
]
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
function ScatterPlot({ traces, onSelectTrace }) {
|
|
737
|
+
const data = useMemo(() => traces.map((t) => ({
|
|
738
|
+
x: t.timestampMs,
|
|
739
|
+
y: t.durationMs,
|
|
740
|
+
traceId: t.traceId,
|
|
741
|
+
serviceName: t.serviceName,
|
|
742
|
+
rootSpanName: t.rootSpanName,
|
|
743
|
+
spanCount: t.spanCount,
|
|
744
|
+
hasError: t.errorCount > 0
|
|
745
|
+
})), [traces]);
|
|
746
|
+
const handleClick = useCallback((entry) => {
|
|
747
|
+
const payload = entry?.payload;
|
|
748
|
+
if (payload?.traceId) onSelectTrace(payload.traceId);
|
|
749
|
+
}, [onSelectTrace]);
|
|
750
|
+
if (traces.length === 0) return null;
|
|
751
|
+
return /* @__PURE__ */ jsx("div", {
|
|
752
|
+
className: "border border-border rounded-lg p-4 bg-background",
|
|
753
|
+
children: /* @__PURE__ */ jsx(ResponsiveContainer, {
|
|
754
|
+
width: "100%",
|
|
755
|
+
height: 200,
|
|
756
|
+
children: /* @__PURE__ */ jsxs(ScatterChart, {
|
|
757
|
+
margin: {
|
|
758
|
+
top: 8,
|
|
759
|
+
right: 8,
|
|
760
|
+
bottom: 4,
|
|
761
|
+
left: 0
|
|
762
|
+
},
|
|
763
|
+
children: [
|
|
764
|
+
/* @__PURE__ */ jsx(CartesianGrid, {
|
|
765
|
+
strokeDasharray: "3 3",
|
|
766
|
+
stroke: "hsl(var(--border))",
|
|
767
|
+
opacity: .4
|
|
733
768
|
}),
|
|
734
|
-
/* @__PURE__ */
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
t.errorCount,
|
|
749
|
-
" Error",
|
|
750
|
-
t.errorCount !== 1 ? "s" : ""
|
|
751
|
-
]
|
|
752
|
-
}),
|
|
753
|
-
t.services.map((svc) => /* @__PURE__ */ jsxs("span", {
|
|
754
|
-
className: "inline-flex items-center gap-1 text-xs px-1.5 py-0.5 rounded",
|
|
755
|
-
style: {
|
|
756
|
-
backgroundColor: `${getServiceColor(svc.name)}20`,
|
|
757
|
-
color: getServiceColor(svc.name)
|
|
758
|
-
},
|
|
759
|
-
children: [
|
|
760
|
-
svc.hasError && /* @__PURE__ */ jsx("span", { className: "w-1.5 h-1.5 rounded-full bg-red-500 shrink-0" }),
|
|
761
|
-
svc.name,
|
|
762
|
-
" (",
|
|
763
|
-
svc.count,
|
|
764
|
-
")"
|
|
765
|
-
]
|
|
766
|
-
}, svc.name))
|
|
767
|
-
]
|
|
769
|
+
/* @__PURE__ */ jsx(XAxis, {
|
|
770
|
+
dataKey: "x",
|
|
771
|
+
type: "number",
|
|
772
|
+
domain: ["dataMin", "dataMax"],
|
|
773
|
+
tickFormatter: (v) => {
|
|
774
|
+
const d = new Date(v);
|
|
775
|
+
return `${d.getHours().toString().padStart(2, "0")}:${d.getMinutes().toString().padStart(2, "0")}`;
|
|
776
|
+
},
|
|
777
|
+
tick: {
|
|
778
|
+
fontSize: 11,
|
|
779
|
+
fill: "hsl(var(--muted-foreground))"
|
|
780
|
+
},
|
|
781
|
+
stroke: "hsl(var(--border))",
|
|
782
|
+
name: "Time"
|
|
768
783
|
}),
|
|
769
|
-
/* @__PURE__ */ jsx(
|
|
770
|
-
|
|
771
|
-
|
|
784
|
+
/* @__PURE__ */ jsx(YAxis, {
|
|
785
|
+
dataKey: "y",
|
|
786
|
+
type: "number",
|
|
787
|
+
tickFormatter: (v) => formatDuration(v),
|
|
788
|
+
tick: {
|
|
789
|
+
fontSize: 11,
|
|
790
|
+
fill: "hsl(var(--muted-foreground))"
|
|
791
|
+
},
|
|
792
|
+
stroke: "hsl(var(--border))",
|
|
793
|
+
name: "Duration",
|
|
794
|
+
width: 70
|
|
795
|
+
}),
|
|
796
|
+
/* @__PURE__ */ jsx(Tooltip, { content: /* @__PURE__ */ jsx(CustomTooltip$1, {}) }),
|
|
797
|
+
/* @__PURE__ */ jsx(Scatter, {
|
|
798
|
+
data,
|
|
799
|
+
onClick: handleClick,
|
|
800
|
+
cursor: "pointer",
|
|
801
|
+
children: data.map((point, i) => /* @__PURE__ */ jsx(Cell, {
|
|
802
|
+
fill: point.hasError ? "#ef4444" : getServiceColor(point.serviceName),
|
|
803
|
+
stroke: point.hasError ? "#ef4444" : "none",
|
|
804
|
+
strokeWidth: point.hasError ? 2 : 0
|
|
805
|
+
}, i))
|
|
772
806
|
})
|
|
773
807
|
]
|
|
774
|
-
}
|
|
808
|
+
})
|
|
775
809
|
})
|
|
776
|
-
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
//#endregion
|
|
813
|
+
//#region src/components/observability/TraceSearch/SortDropdown.tsx
|
|
814
|
+
const SORT_OPTIONS = [
|
|
815
|
+
{
|
|
816
|
+
value: "recent",
|
|
817
|
+
label: "Most Recent"
|
|
818
|
+
},
|
|
819
|
+
{
|
|
820
|
+
value: "longest",
|
|
821
|
+
label: "Longest First"
|
|
822
|
+
},
|
|
823
|
+
{
|
|
824
|
+
value: "shortest",
|
|
825
|
+
label: "Shortest First"
|
|
826
|
+
},
|
|
827
|
+
{
|
|
828
|
+
value: "mostSpans",
|
|
829
|
+
label: "Most Spans"
|
|
830
|
+
},
|
|
831
|
+
{
|
|
832
|
+
value: "leastSpans",
|
|
833
|
+
label: "Least Spans"
|
|
834
|
+
}
|
|
835
|
+
];
|
|
836
|
+
function SortDropdown({ value, onChange }) {
|
|
837
|
+
return /* @__PURE__ */ jsx("select", {
|
|
838
|
+
value,
|
|
839
|
+
onChange: (e) => onChange(e.target.value),
|
|
840
|
+
className: "bg-muted/50 border border-border rounded px-2 py-1.5 text-sm text-foreground",
|
|
841
|
+
children: SORT_OPTIONS.map((opt) => /* @__PURE__ */ jsx("option", {
|
|
842
|
+
value: opt.value,
|
|
843
|
+
children: opt.label
|
|
844
|
+
}, opt.value))
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
//#endregion
|
|
848
|
+
//#region src/components/observability/TraceSearch/DurationBar.tsx
|
|
849
|
+
/**
|
|
850
|
+
* DurationBar - Horizontal bar showing relative trace duration.
|
|
851
|
+
*/
|
|
852
|
+
function DurationBar({ durationMs, maxDurationMs, color }) {
|
|
853
|
+
const rawPct = maxDurationMs > 0 ? durationMs / maxDurationMs * 100 : 0;
|
|
854
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
855
|
+
className: "flex items-center gap-2",
|
|
856
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
857
|
+
className: "flex-1 h-2 bg-muted/30 rounded overflow-hidden",
|
|
858
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
859
|
+
className: "h-full rounded",
|
|
860
|
+
style: {
|
|
861
|
+
width: `${durationMs <= 0 ? 0 : Math.min(Math.max(rawPct, 1), 100)}%`,
|
|
862
|
+
backgroundColor: color,
|
|
863
|
+
opacity: .7
|
|
864
|
+
}
|
|
865
|
+
})
|
|
866
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
867
|
+
className: "text-xs text-foreground/80 shrink-0 w-16 text-right font-mono",
|
|
868
|
+
children: formatDuration(durationMs)
|
|
869
|
+
})]
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
//#endregion
|
|
873
|
+
//#region src/components/observability/TraceSearch/index.tsx
|
|
874
|
+
function sortTraces(traces, sort) {
|
|
875
|
+
const sorted = [...traces];
|
|
876
|
+
switch (sort) {
|
|
877
|
+
case "longest": return sorted.sort((a, b) => b.durationMs - a.durationMs);
|
|
878
|
+
case "shortest": return sorted.sort((a, b) => a.durationMs - b.durationMs);
|
|
879
|
+
case "mostSpans": return sorted.sort((a, b) => b.spanCount - a.spanCount);
|
|
880
|
+
case "leastSpans": return sorted.sort((a, b) => a.spanCount - b.spanCount);
|
|
881
|
+
default: return sorted.sort((a, b) => b.timestampMs - a.timestampMs);
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
function TraceSearch({ services = [], service, operations = [], traces, isLoading, error, onSelectTrace, onSearch, onCompare, sort: controlledSort, onSortChange }) {
|
|
885
|
+
const [internalSort, setInternalSort] = useState("recent");
|
|
886
|
+
const currentSort = controlledSort ?? internalSort;
|
|
887
|
+
const handleSortChange = (s) => {
|
|
888
|
+
if (onSortChange) onSortChange(s);
|
|
889
|
+
else setInternalSort(s);
|
|
890
|
+
};
|
|
891
|
+
const [selected, setSelected] = useState(/* @__PURE__ */ new Set());
|
|
892
|
+
const toggleSelected = (traceId) => {
|
|
893
|
+
setSelected((prev) => {
|
|
894
|
+
const next = new Set(prev);
|
|
895
|
+
if (next.has(traceId)) next.delete(traceId);
|
|
896
|
+
else {
|
|
897
|
+
if (next.size >= 2) return prev;
|
|
898
|
+
next.add(traceId);
|
|
899
|
+
}
|
|
900
|
+
return next;
|
|
901
|
+
});
|
|
902
|
+
};
|
|
903
|
+
const handleFormSubmit = (values) => {
|
|
904
|
+
onSearch?.({
|
|
905
|
+
service: values.service || void 0,
|
|
906
|
+
operation: values.operation || void 0,
|
|
907
|
+
tags: values.tags || void 0,
|
|
908
|
+
lookback: values.lookback || void 0,
|
|
909
|
+
minDuration: values.minDuration || void 0,
|
|
910
|
+
maxDuration: values.maxDuration || void 0,
|
|
911
|
+
limit: values.limit
|
|
912
|
+
});
|
|
913
|
+
};
|
|
914
|
+
const sortedTraces = useMemo(() => sortTraces(traces, currentSort), [traces, currentSort]);
|
|
915
|
+
const maxDurationMs = useMemo(() => Math.max(...traces.map((t) => t.durationMs), 0), [traces]);
|
|
916
|
+
const selectedArr = Array.from(selected);
|
|
917
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
918
|
+
className: "flex gap-6 min-h-0",
|
|
919
|
+
children: [onSearch && /* @__PURE__ */ jsx("div", {
|
|
920
|
+
className: "w-72 shrink-0 border border-border rounded-lg p-4 self-start",
|
|
921
|
+
children: /* @__PURE__ */ jsx(SearchForm, {
|
|
922
|
+
services,
|
|
923
|
+
operations,
|
|
924
|
+
initialValues: { service },
|
|
925
|
+
onSubmit: handleFormSubmit,
|
|
926
|
+
isLoading
|
|
927
|
+
})
|
|
928
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
929
|
+
className: "flex-1 min-w-0 space-y-4",
|
|
930
|
+
children: [
|
|
931
|
+
traces.length > 0 && /* @__PURE__ */ jsx(ScatterPlot, {
|
|
932
|
+
traces,
|
|
933
|
+
onSelectTrace
|
|
934
|
+
}),
|
|
935
|
+
/* @__PURE__ */ jsxs("div", {
|
|
936
|
+
className: "flex items-center justify-between gap-2",
|
|
937
|
+
children: [/* @__PURE__ */ jsxs("span", {
|
|
938
|
+
className: "text-sm text-muted-foreground",
|
|
939
|
+
children: [
|
|
940
|
+
traces.length,
|
|
941
|
+
" Trace",
|
|
942
|
+
traces.length !== 1 ? "s" : ""
|
|
943
|
+
]
|
|
944
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
945
|
+
className: "flex items-center gap-2",
|
|
946
|
+
children: [onCompare && selected.size === 2 && /* @__PURE__ */ jsx("button", {
|
|
947
|
+
onClick: () => onCompare(selectedArr),
|
|
948
|
+
className: "px-3 py-1.5 text-xs font-medium bg-foreground text-background rounded hover:bg-foreground/90 transition-colors",
|
|
949
|
+
children: "Compare"
|
|
950
|
+
}), /* @__PURE__ */ jsx(SortDropdown, {
|
|
951
|
+
value: currentSort,
|
|
952
|
+
onChange: handleSortChange
|
|
953
|
+
})]
|
|
954
|
+
})]
|
|
955
|
+
}),
|
|
956
|
+
isLoading && /* @__PURE__ */ jsxs("div", {
|
|
957
|
+
className: "flex items-center gap-2 text-muted-foreground py-8",
|
|
958
|
+
children: [/* @__PURE__ */ jsx("div", { className: "w-4 h-4 border-2 border-muted-foreground border-t-transparent rounded-full animate-spin" }), "Loading traces..."]
|
|
959
|
+
}),
|
|
960
|
+
error && /* @__PURE__ */ jsxs("div", {
|
|
961
|
+
className: "text-red-400 py-4",
|
|
962
|
+
children: ["Error loading traces: ", error.message]
|
|
963
|
+
}),
|
|
964
|
+
!isLoading && !error && traces.length === 0 && /* @__PURE__ */ jsx("div", {
|
|
965
|
+
className: "text-muted-foreground py-8",
|
|
966
|
+
children: "No traces found"
|
|
967
|
+
}),
|
|
968
|
+
sortedTraces.length > 0 && /* @__PURE__ */ jsx("div", {
|
|
969
|
+
className: "space-y-2",
|
|
970
|
+
children: sortedTraces.map((t) => /* @__PURE__ */ jsx("div", {
|
|
971
|
+
className: "border border-border rounded-lg px-4 py-3 hover:border-foreground/30 hover:bg-muted/30 cursor-pointer transition-colors",
|
|
972
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
973
|
+
className: "flex items-center gap-2",
|
|
974
|
+
children: [onCompare && /* @__PURE__ */ jsx("input", {
|
|
975
|
+
type: "checkbox",
|
|
976
|
+
checked: selected.has(t.traceId),
|
|
977
|
+
onChange: () => toggleSelected(t.traceId),
|
|
978
|
+
onClick: (e) => e.stopPropagation(),
|
|
979
|
+
className: "shrink-0",
|
|
980
|
+
disabled: !selected.has(t.traceId) && selected.size >= 2
|
|
981
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
982
|
+
className: "flex-1 min-w-0",
|
|
983
|
+
onClick: () => onSelectTrace(t.traceId),
|
|
984
|
+
children: [
|
|
985
|
+
/* @__PURE__ */ jsx("div", {
|
|
986
|
+
className: "flex items-baseline justify-between gap-2",
|
|
987
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
988
|
+
className: "flex items-baseline gap-1.5 min-w-0",
|
|
989
|
+
children: [/* @__PURE__ */ jsxs("span", {
|
|
990
|
+
className: "font-medium text-foreground truncate",
|
|
991
|
+
children: [
|
|
992
|
+
t.serviceName,
|
|
993
|
+
": ",
|
|
994
|
+
t.rootSpanName
|
|
995
|
+
]
|
|
996
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
997
|
+
className: "text-xs font-mono text-muted-foreground shrink-0",
|
|
998
|
+
children: t.traceId.slice(0, 7)
|
|
999
|
+
})]
|
|
1000
|
+
})
|
|
1001
|
+
}),
|
|
1002
|
+
/* @__PURE__ */ jsx("div", {
|
|
1003
|
+
className: "mt-1.5",
|
|
1004
|
+
children: /* @__PURE__ */ jsx(DurationBar, {
|
|
1005
|
+
durationMs: t.durationMs,
|
|
1006
|
+
maxDurationMs,
|
|
1007
|
+
color: getServiceColor(t.serviceName)
|
|
1008
|
+
})
|
|
1009
|
+
}),
|
|
1010
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1011
|
+
className: "flex items-center flex-wrap gap-1.5 mt-1.5",
|
|
1012
|
+
children: [
|
|
1013
|
+
/* @__PURE__ */ jsxs("span", {
|
|
1014
|
+
className: "text-xs px-1.5 py-0.5 rounded bg-muted text-muted-foreground",
|
|
1015
|
+
children: [
|
|
1016
|
+
t.spanCount,
|
|
1017
|
+
" Span",
|
|
1018
|
+
t.spanCount !== 1 ? "s" : ""
|
|
1019
|
+
]
|
|
1020
|
+
}),
|
|
1021
|
+
t.errorCount > 0 && /* @__PURE__ */ jsxs("span", {
|
|
1022
|
+
className: "text-xs px-1.5 py-0.5 rounded bg-red-500/20 text-red-400",
|
|
1023
|
+
children: [
|
|
1024
|
+
t.errorCount,
|
|
1025
|
+
" Error",
|
|
1026
|
+
t.errorCount !== 1 ? "s" : ""
|
|
1027
|
+
]
|
|
1028
|
+
}),
|
|
1029
|
+
t.services.map((svc) => /* @__PURE__ */ jsxs("span", {
|
|
1030
|
+
className: "inline-flex items-center gap-1 text-xs px-1.5 py-0.5 rounded",
|
|
1031
|
+
style: {
|
|
1032
|
+
backgroundColor: `${getServiceColor(svc.name)}20`,
|
|
1033
|
+
color: getServiceColor(svc.name)
|
|
1034
|
+
},
|
|
1035
|
+
children: [
|
|
1036
|
+
svc.hasError && /* @__PURE__ */ jsx("span", { className: "w-1.5 h-1.5 rounded-full bg-red-500 shrink-0" }),
|
|
1037
|
+
svc.name,
|
|
1038
|
+
" (",
|
|
1039
|
+
svc.count,
|
|
1040
|
+
")"
|
|
1041
|
+
]
|
|
1042
|
+
}, svc.name))
|
|
1043
|
+
]
|
|
1044
|
+
}),
|
|
1045
|
+
/* @__PURE__ */ jsx("div", {
|
|
1046
|
+
className: "text-xs text-muted-foreground mt-1 text-right",
|
|
1047
|
+
children: formatTimestamp$1(t.timestampMs)
|
|
1048
|
+
})
|
|
1049
|
+
]
|
|
1050
|
+
})]
|
|
1051
|
+
})
|
|
1052
|
+
}, t.traceId))
|
|
1053
|
+
})
|
|
1054
|
+
]
|
|
1055
|
+
})]
|
|
1056
|
+
});
|
|
777
1057
|
}
|
|
778
|
-
|
|
779
1058
|
//#endregion
|
|
780
1059
|
//#region src/components/observability/utils/flatten-tree.ts
|
|
781
1060
|
function flattenTree(rootSpans, collapsedIds) {
|
|
@@ -790,6 +1069,17 @@ function flattenTree(rootSpans, collapsedIds) {
|
|
|
790
1069
|
rootSpans.forEach((root) => traverse(root, 0));
|
|
791
1070
|
return result;
|
|
792
1071
|
}
|
|
1072
|
+
/** Flatten all spans (ignoring collapse state) with depth. */
|
|
1073
|
+
function flattenAllSpans(rootSpans) {
|
|
1074
|
+
return flattenTree(rootSpans, /* @__PURE__ */ new Set());
|
|
1075
|
+
}
|
|
1076
|
+
function spanMatchesSearch(span, query) {
|
|
1077
|
+
const q = query.toLowerCase();
|
|
1078
|
+
if (span.name.toLowerCase().includes(q)) return true;
|
|
1079
|
+
if (span.serviceName.toLowerCase().includes(q)) return true;
|
|
1080
|
+
for (const val of Object.values(span.attributes)) if (String(val).toLowerCase().includes(q)) return true;
|
|
1081
|
+
return false;
|
|
1082
|
+
}
|
|
793
1083
|
function getAllSpanIds(rootSpans) {
|
|
794
1084
|
const ids = [];
|
|
795
1085
|
function traverse(span) {
|
|
@@ -799,15 +1089,26 @@ function getAllSpanIds(rootSpans) {
|
|
|
799
1089
|
rootSpans.forEach((root) => traverse(root));
|
|
800
1090
|
return ids;
|
|
801
1091
|
}
|
|
802
|
-
|
|
803
1092
|
//#endregion
|
|
804
1093
|
//#region src/components/observability/TraceTimeline/TraceHeader.tsx
|
|
805
|
-
function
|
|
1094
|
+
function computeMaxDepth(spans) {
|
|
1095
|
+
let max = 0;
|
|
1096
|
+
function walk(nodes, depth) {
|
|
1097
|
+
for (const node of nodes) {
|
|
1098
|
+
if (depth > max) max = depth;
|
|
1099
|
+
walk(node.children, depth + 1);
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
walk(spans, 1);
|
|
1103
|
+
return max;
|
|
1104
|
+
}
|
|
1105
|
+
function TraceHeader({ trace, services = [], onHeaderToggle, isCollapsed = false }) {
|
|
806
1106
|
const [copied, setCopied] = useState(false);
|
|
807
1107
|
const rootSpan = trace.rootSpans[0];
|
|
808
1108
|
const rootServiceName = rootSpan?.serviceName ?? "unknown";
|
|
809
1109
|
const rootSpanName = rootSpan?.name ?? "unknown";
|
|
810
1110
|
const totalDuration = trace.maxTimeMs - trace.minTimeMs;
|
|
1111
|
+
const maxDepth = computeMaxDepth(trace.rootSpans);
|
|
811
1112
|
const handleCopyTraceId = async () => {
|
|
812
1113
|
try {
|
|
813
1114
|
await navigator.clipboard.writeText(trace.traceId);
|
|
@@ -817,9 +1118,35 @@ function TraceHeader({ trace }) {
|
|
|
817
1118
|
console.error("Failed to copy trace ID:", err);
|
|
818
1119
|
}
|
|
819
1120
|
};
|
|
820
|
-
return /* @__PURE__ */
|
|
1121
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
821
1122
|
className: "bg-background border-b border-border px-4 py-3",
|
|
822
|
-
children: /* @__PURE__ */ jsxs("div", {
|
|
1123
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
1124
|
+
className: "flex items-center gap-2 mb-1",
|
|
1125
|
+
children: [onHeaderToggle && /* @__PURE__ */ jsx("button", {
|
|
1126
|
+
onClick: onHeaderToggle,
|
|
1127
|
+
className: "p-0.5 text-muted-foreground hover:text-foreground",
|
|
1128
|
+
"aria-label": isCollapsed ? "Expand header" : "Collapse header",
|
|
1129
|
+
children: /* @__PURE__ */ jsx("svg", {
|
|
1130
|
+
className: `w-4 h-4 transition-transform ${isCollapsed ? "-rotate-90" : ""}`,
|
|
1131
|
+
fill: "none",
|
|
1132
|
+
stroke: "currentColor",
|
|
1133
|
+
viewBox: "0 0 24 24",
|
|
1134
|
+
children: /* @__PURE__ */ jsx("path", {
|
|
1135
|
+
strokeLinecap: "round",
|
|
1136
|
+
strokeLinejoin: "round",
|
|
1137
|
+
strokeWidth: 2,
|
|
1138
|
+
d: "M19 9l-7 7-7-7"
|
|
1139
|
+
})
|
|
1140
|
+
})
|
|
1141
|
+
}), /* @__PURE__ */ jsxs("span", {
|
|
1142
|
+
className: "text-sm font-semibold text-foreground",
|
|
1143
|
+
children: [
|
|
1144
|
+
rootServiceName,
|
|
1145
|
+
": ",
|
|
1146
|
+
rootSpanName
|
|
1147
|
+
]
|
|
1148
|
+
})]
|
|
1149
|
+
}), !isCollapsed && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("div", {
|
|
823
1150
|
className: "flex items-center gap-6 flex-wrap",
|
|
824
1151
|
children: [
|
|
825
1152
|
/* @__PURE__ */ jsxs("div", {
|
|
@@ -845,43 +1172,30 @@ function TraceHeader({ trace }) {
|
|
|
845
1172
|
className: "flex items-center gap-2",
|
|
846
1173
|
children: [/* @__PURE__ */ jsx("span", {
|
|
847
1174
|
className: "text-xs font-semibold text-muted-foreground",
|
|
848
|
-
children: "
|
|
849
|
-
}), /* @__PURE__ */
|
|
850
|
-
className: "text-sm",
|
|
851
|
-
children:
|
|
852
|
-
/* @__PURE__ */ jsx("span", {
|
|
853
|
-
className: "text-muted-foreground",
|
|
854
|
-
children: rootServiceName
|
|
855
|
-
}),
|
|
856
|
-
/* @__PURE__ */ jsx("span", {
|
|
857
|
-
className: "mx-1 text-muted-foreground/70",
|
|
858
|
-
children: "/"
|
|
859
|
-
}),
|
|
860
|
-
/* @__PURE__ */ jsx("span", {
|
|
861
|
-
className: "font-medium text-foreground",
|
|
862
|
-
children: rootSpanName
|
|
863
|
-
})
|
|
864
|
-
]
|
|
1175
|
+
children: "Duration:"
|
|
1176
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
1177
|
+
className: "text-sm font-medium text-foreground",
|
|
1178
|
+
children: formatDuration(totalDuration)
|
|
865
1179
|
})]
|
|
866
1180
|
}),
|
|
867
1181
|
/* @__PURE__ */ jsxs("div", {
|
|
868
1182
|
className: "flex items-center gap-2",
|
|
869
1183
|
children: [/* @__PURE__ */ jsx("span", {
|
|
870
1184
|
className: "text-xs font-semibold text-muted-foreground",
|
|
871
|
-
children: "
|
|
1185
|
+
children: "Spans:"
|
|
872
1186
|
}), /* @__PURE__ */ jsx("span", {
|
|
873
1187
|
className: "text-sm font-medium text-foreground",
|
|
874
|
-
children:
|
|
1188
|
+
children: trace.totalSpanCount
|
|
875
1189
|
})]
|
|
876
1190
|
}),
|
|
877
1191
|
/* @__PURE__ */ jsxs("div", {
|
|
878
1192
|
className: "flex items-center gap-2",
|
|
879
1193
|
children: [/* @__PURE__ */ jsx("span", {
|
|
880
1194
|
className: "text-xs font-semibold text-muted-foreground",
|
|
881
|
-
children: "
|
|
1195
|
+
children: "Depth:"
|
|
882
1196
|
}), /* @__PURE__ */ jsx("span", {
|
|
883
1197
|
className: "text-sm font-medium text-foreground",
|
|
884
|
-
children:
|
|
1198
|
+
children: maxDepth
|
|
885
1199
|
})]
|
|
886
1200
|
}),
|
|
887
1201
|
/* @__PURE__ */ jsxs("div", {
|
|
@@ -895,10 +1209,21 @@ function TraceHeader({ trace }) {
|
|
|
895
1209
|
})]
|
|
896
1210
|
})
|
|
897
1211
|
]
|
|
898
|
-
})
|
|
1212
|
+
}), services.length > 0 && /* @__PURE__ */ jsx("div", {
|
|
1213
|
+
className: "flex items-center gap-3 mt-2 flex-wrap",
|
|
1214
|
+
children: services.map((svc) => /* @__PURE__ */ jsxs("div", {
|
|
1215
|
+
className: "flex items-center gap-1.5",
|
|
1216
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
1217
|
+
className: "w-2.5 h-2.5 rounded-full flex-shrink-0",
|
|
1218
|
+
style: { backgroundColor: getServiceColor(svc) }
|
|
1219
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
1220
|
+
className: "text-xs text-muted-foreground",
|
|
1221
|
+
children: svc
|
|
1222
|
+
})]
|
|
1223
|
+
}, svc))
|
|
1224
|
+
})] })]
|
|
899
1225
|
});
|
|
900
1226
|
}
|
|
901
|
-
|
|
902
1227
|
//#endregion
|
|
903
1228
|
//#region src/components/observability/TraceTimeline/Tooltip.tsx
|
|
904
1229
|
function Tooltip$1({ content, children }) {
|
|
@@ -928,7 +1253,6 @@ function Tooltip$1({ content, children }) {
|
|
|
928
1253
|
children: content
|
|
929
1254
|
}), document.body)] });
|
|
930
1255
|
}
|
|
931
|
-
|
|
932
1256
|
//#endregion
|
|
933
1257
|
//#region src/components/observability/TraceTimeline/TimelineBar.tsx
|
|
934
1258
|
function TimelineBar({ span, relativeStart, relativeDuration }) {
|
|
@@ -936,45 +1260,48 @@ function TimelineBar({ span, relativeStart, relativeDuration }) {
|
|
|
936
1260
|
const barColor = getSpanBarColor(span.serviceName, isError);
|
|
937
1261
|
const leftPercent = relativeStart * 100;
|
|
938
1262
|
const widthPercent = Math.max(.2, relativeDuration * 100);
|
|
1263
|
+
const isWide = widthPercent > 8;
|
|
1264
|
+
const tooltipText = `${span.name}\n${formatDuration(span.durationMs)}\nStatus: ${isError ? "ERROR" : "OK"}`;
|
|
1265
|
+
const durationLabel = formatDuration(span.durationMs);
|
|
939
1266
|
return /* @__PURE__ */ jsx("div", {
|
|
940
1267
|
className: "relative h-full",
|
|
941
1268
|
children: /* @__PURE__ */ jsx(Tooltip$1, {
|
|
942
|
-
content:
|
|
943
|
-
children: /* @__PURE__ */
|
|
1269
|
+
content: tooltipText,
|
|
1270
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
944
1271
|
className: "absolute inset-0",
|
|
945
|
-
children: /* @__PURE__ */ jsx("div", {
|
|
946
|
-
className: "absolute top-1/2 -translate-y-1/2 h-2 rounded-sm cursor-pointer hover:opacity-80 transition-opacity",
|
|
1272
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
1273
|
+
className: "absolute top-1/2 -translate-y-1/2 h-2 rounded-sm cursor-pointer hover:opacity-80 transition-opacity flex items-center",
|
|
947
1274
|
style: {
|
|
948
1275
|
left: `${leftPercent}%`,
|
|
949
1276
|
width: `max(2px, ${widthPercent}%)`,
|
|
950
1277
|
backgroundColor: barColor
|
|
951
|
-
}
|
|
952
|
-
|
|
1278
|
+
},
|
|
1279
|
+
children: isWide && /* @__PURE__ */ jsx("span", {
|
|
1280
|
+
className: "text-[10px] font-mono text-white px-1 truncate",
|
|
1281
|
+
children: durationLabel
|
|
1282
|
+
})
|
|
1283
|
+
}), !isWide && /* @__PURE__ */ jsx("span", {
|
|
1284
|
+
className: "absolute top-1/2 -translate-y-1/2 text-[10px] font-mono text-muted-foreground whitespace-nowrap",
|
|
1285
|
+
style: { left: `calc(${leftPercent + widthPercent}% + 4px)` },
|
|
1286
|
+
children: durationLabel
|
|
1287
|
+
})]
|
|
953
1288
|
})
|
|
954
1289
|
})
|
|
955
1290
|
});
|
|
956
1291
|
}
|
|
957
|
-
|
|
958
1292
|
//#endregion
|
|
959
1293
|
//#region src/components/observability/TraceTimeline/SpanRow.tsx
|
|
960
|
-
function
|
|
961
|
-
const attrs = span.attributes;
|
|
962
|
-
const method = attrs["http.method"];
|
|
963
|
-
const url = attrs["http.url"] || attrs["http.target"];
|
|
964
|
-
const statusCode = attrs["http.status_code"];
|
|
965
|
-
if (!method && !url) return null;
|
|
966
|
-
const parts = [];
|
|
967
|
-
if (method) parts.push(String(method));
|
|
968
|
-
if (url) parts.push(String(url));
|
|
969
|
-
if (statusCode) parts.push(`[${statusCode}]`);
|
|
970
|
-
return parts.join(" ");
|
|
971
|
-
}
|
|
972
|
-
const SpanRow = memo(function SpanRow({ span, level, isCollapsed, isSelected, isParentOfHovered = false, relativeStart, relativeDuration, onClick, onToggleCollapse, onMouseEnter, onMouseLeave }) {
|
|
1294
|
+
const SpanRow = memo(function SpanRow({ span, level, isCollapsed, isSelected, isParentOfHovered = false, relativeStart, relativeDuration, onClick, onToggleCollapse, onMouseEnter, onMouseLeave, uiFind }) {
|
|
973
1295
|
const hasChildren = span.children.length > 0;
|
|
974
1296
|
const isError = span.status === "ERROR";
|
|
975
|
-
const
|
|
1297
|
+
const serviceColor = getServiceColor(span.serviceName);
|
|
1298
|
+
const isDimmed = uiFind ? !spanMatchesSearch(span, uiFind) : false;
|
|
976
1299
|
return /* @__PURE__ */ jsxs("div", {
|
|
977
1300
|
className: `flex h-8 border-b border-border hover:bg-muted cursor-pointer ${isSelected ? "bg-blue-100 dark:bg-blue-900/30 hover:bg-blue-100 dark:hover:bg-blue-900/30" : ""}`,
|
|
1301
|
+
style: {
|
|
1302
|
+
borderLeft: `3px solid ${serviceColor}`,
|
|
1303
|
+
opacity: isDimmed ? .4 : 1
|
|
1304
|
+
},
|
|
978
1305
|
onClick,
|
|
979
1306
|
onMouseEnter,
|
|
980
1307
|
onMouseLeave,
|
|
@@ -1029,7 +1356,8 @@ const SpanRow = memo(function SpanRow({ span, level, isCollapsed, isSelected, is
|
|
|
1029
1356
|
})
|
|
1030
1357
|
}),
|
|
1031
1358
|
/* @__PURE__ */ jsx("span", {
|
|
1032
|
-
className: "text-xs
|
|
1359
|
+
className: "text-xs flex-shrink-0 mr-2 font-medium",
|
|
1360
|
+
style: { color: serviceColor },
|
|
1033
1361
|
children: span.serviceName
|
|
1034
1362
|
}),
|
|
1035
1363
|
/* @__PURE__ */ jsx("span", {
|
|
@@ -1044,10 +1372,6 @@ const SpanRow = memo(function SpanRow({ span, level, isCollapsed, isSelected, is
|
|
|
1044
1372
|
")"
|
|
1045
1373
|
]
|
|
1046
1374
|
}),
|
|
1047
|
-
httpContext && /* @__PURE__ */ jsx("span", {
|
|
1048
|
-
className: "text-xs text-muted-foreground truncate ml-2 flex-shrink-0 max-w-xs",
|
|
1049
|
-
children: httpContext
|
|
1050
|
-
}),
|
|
1051
1375
|
/* @__PURE__ */ jsx("span", {
|
|
1052
1376
|
className: "text-xs text-muted-foreground flex-shrink-0 ml-2",
|
|
1053
1377
|
children: formatDuration(span.durationMs)
|
|
@@ -1063,7 +1387,6 @@ const SpanRow = memo(function SpanRow({ span, level, isCollapsed, isSelected, is
|
|
|
1063
1387
|
})]
|
|
1064
1388
|
});
|
|
1065
1389
|
});
|
|
1066
|
-
|
|
1067
1390
|
//#endregion
|
|
1068
1391
|
//#region src/components/observability/utils/attributes.ts
|
|
1069
1392
|
function formatAttributeValue(value) {
|
|
@@ -1082,500 +1405,203 @@ function formatSeriesLabel(labels) {
|
|
|
1082
1405
|
function isComplexValue(value) {
|
|
1083
1406
|
return typeof value === "object" && value !== null && (Array.isArray(value) || Object.keys(value).length > 0);
|
|
1084
1407
|
}
|
|
1085
|
-
|
|
1086
1408
|
//#endregion
|
|
1087
|
-
//#region src/components/observability/TraceTimeline/
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
"
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
"http.request_content_length",
|
|
1098
|
-
"http.response_content_length"
|
|
1099
|
-
]);
|
|
1100
|
-
function AttributesTab$1({ span }) {
|
|
1101
|
-
const { httpAttributes, otherAttributes, resourceAttributes } = useMemo(() => {
|
|
1102
|
-
const http = [];
|
|
1103
|
-
const other = [];
|
|
1104
|
-
const resource = [];
|
|
1105
|
-
if (span.attributes) Object.entries(span.attributes).forEach(([key, value]) => {
|
|
1106
|
-
if (HTTP_SEMANTIC_CONVENTIONS.has(key)) http.push([key, value]);
|
|
1107
|
-
else other.push([key, value]);
|
|
1108
|
-
});
|
|
1109
|
-
if (span.resourceAttributes) Object.entries(span.resourceAttributes).forEach(([key, value]) => {
|
|
1110
|
-
resource.push([key, value]);
|
|
1111
|
-
});
|
|
1112
|
-
http.sort(([a], [b]) => a.localeCompare(b));
|
|
1113
|
-
other.sort(([a], [b]) => a.localeCompare(b));
|
|
1114
|
-
resource.sort(([a], [b]) => a.localeCompare(b));
|
|
1115
|
-
return {
|
|
1116
|
-
httpAttributes: http,
|
|
1117
|
-
otherAttributes: other,
|
|
1118
|
-
resourceAttributes: resource
|
|
1119
|
-
};
|
|
1120
|
-
}, [span]);
|
|
1121
|
-
if (!(httpAttributes.length > 0 || otherAttributes.length > 0 || resourceAttributes.length > 0)) return /* @__PURE__ */ jsx("div", {
|
|
1122
|
-
className: "text-sm text-muted-foreground text-center py-8",
|
|
1123
|
-
children: "No attributes available"
|
|
1124
|
-
});
|
|
1125
|
-
return /* @__PURE__ */ jsxs("div", {
|
|
1126
|
-
className: "space-y-6",
|
|
1409
|
+
//#region src/components/observability/TraceTimeline/SpanDetailInline.tsx
|
|
1410
|
+
function CollapsibleSection({ title, count, children }) {
|
|
1411
|
+
const [open, setOpen] = useState(false);
|
|
1412
|
+
if (count === 0) return null;
|
|
1413
|
+
return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsxs("button", {
|
|
1414
|
+
className: "flex items-center gap-1 text-xs font-medium text-foreground hover:text-blue-600 dark:hover:text-blue-400 py-1",
|
|
1415
|
+
onClick: (e) => {
|
|
1416
|
+
e.stopPropagation();
|
|
1417
|
+
setOpen((p) => !p);
|
|
1418
|
+
},
|
|
1127
1419
|
children: [
|
|
1128
|
-
|
|
1129
|
-
className: "
|
|
1130
|
-
children:
|
|
1131
|
-
}),
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
children: "Span Attributes"
|
|
1142
|
-
}), /* @__PURE__ */ jsx("div", {
|
|
1143
|
-
className: "space-y-2",
|
|
1144
|
-
children: otherAttributes.map(([key, value]) => /* @__PURE__ */ jsx(AttributeRow, {
|
|
1145
|
-
attrKey: key,
|
|
1146
|
-
value
|
|
1147
|
-
}, key))
|
|
1148
|
-
})] }),
|
|
1149
|
-
resourceAttributes.length > 0 && /* @__PURE__ */ jsxs("section", { children: [/* @__PURE__ */ jsx("h3", {
|
|
1150
|
-
className: "text-sm font-semibold text-foreground mb-3",
|
|
1151
|
-
children: "Resource Attributes"
|
|
1152
|
-
}), /* @__PURE__ */ jsx("div", {
|
|
1153
|
-
className: "space-y-2",
|
|
1154
|
-
children: resourceAttributes.map(([key, value]) => /* @__PURE__ */ jsx(AttributeRow, {
|
|
1155
|
-
attrKey: key,
|
|
1156
|
-
value
|
|
1157
|
-
}, key))
|
|
1158
|
-
})] })
|
|
1420
|
+
/* @__PURE__ */ jsx("span", {
|
|
1421
|
+
className: "w-3 text-center",
|
|
1422
|
+
children: open ? "▾" : "▸"
|
|
1423
|
+
}),
|
|
1424
|
+
title,
|
|
1425
|
+
/* @__PURE__ */ jsxs("span", {
|
|
1426
|
+
className: "text-muted-foreground",
|
|
1427
|
+
children: [
|
|
1428
|
+
"(",
|
|
1429
|
+
count,
|
|
1430
|
+
")"
|
|
1431
|
+
]
|
|
1432
|
+
})
|
|
1159
1433
|
]
|
|
1160
|
-
})
|
|
1434
|
+
}), open && /* @__PURE__ */ jsx("div", {
|
|
1435
|
+
className: "ml-4 mt-1 space-y-1",
|
|
1436
|
+
children
|
|
1437
|
+
})] });
|
|
1161
1438
|
}
|
|
1162
|
-
function
|
|
1163
|
-
const
|
|
1164
|
-
const formattedValue = formatAttributeValue(value);
|
|
1439
|
+
function KeyValueRow({ k, v }) {
|
|
1440
|
+
const formatted = formatAttributeValue(v);
|
|
1165
1441
|
return /* @__PURE__ */ jsxs("div", {
|
|
1166
|
-
className:
|
|
1167
|
-
children: [
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
children: isComplex ? /* @__PURE__ */ jsx("pre", {
|
|
1174
|
-
className: "text-xs text-foreground bg-background p-2 rounded border border-border overflow-x-auto",
|
|
1175
|
-
children: formattedValue
|
|
1176
|
-
}) : /* @__PURE__ */ jsx("span", {
|
|
1442
|
+
className: "flex gap-2 text-xs font-mono py-0.5",
|
|
1443
|
+
children: [
|
|
1444
|
+
/* @__PURE__ */ jsx("span", {
|
|
1445
|
+
className: "text-muted-foreground flex-shrink-0",
|
|
1446
|
+
children: k
|
|
1447
|
+
}),
|
|
1448
|
+
/* @__PURE__ */ jsx("span", {
|
|
1177
1449
|
className: "text-foreground",
|
|
1178
|
-
children:
|
|
1450
|
+
children: "="
|
|
1451
|
+
}),
|
|
1452
|
+
/* @__PURE__ */ jsx("span", {
|
|
1453
|
+
className: "text-foreground break-all",
|
|
1454
|
+
children: formatted
|
|
1179
1455
|
})
|
|
1180
|
-
|
|
1181
|
-
});
|
|
1182
|
-
}
|
|
1183
|
-
|
|
1184
|
-
//#endregion
|
|
1185
|
-
//#region src/components/observability/TraceTimeline/DetailPane/EventsTab.tsx
|
|
1186
|
-
function formatRelativeTime$1(eventTimeMs, spanStartMs) {
|
|
1187
|
-
const relativeMs = eventTimeMs - spanStartMs;
|
|
1188
|
-
return `${relativeMs < 0 ? "-" : "+"}${formatDuration(Math.abs(relativeMs))}`;
|
|
1189
|
-
}
|
|
1190
|
-
function EventsTab({ span }) {
|
|
1191
|
-
const [expandedEvents, setExpandedEvents] = useState(/* @__PURE__ */ new Set());
|
|
1192
|
-
const toggleEventExpanded = (index) => {
|
|
1193
|
-
setExpandedEvents((prev) => {
|
|
1194
|
-
const next = new Set(prev);
|
|
1195
|
-
if (next.has(index)) next.delete(index);
|
|
1196
|
-
else next.add(index);
|
|
1197
|
-
return next;
|
|
1198
|
-
});
|
|
1199
|
-
};
|
|
1200
|
-
if (!span.events || span.events.length === 0) return /* @__PURE__ */ jsx("div", {
|
|
1201
|
-
className: "text-sm text-muted-foreground text-center py-8",
|
|
1202
|
-
children: "No events available"
|
|
1203
|
-
});
|
|
1204
|
-
return /* @__PURE__ */ jsx("div", {
|
|
1205
|
-
className: "space-y-3",
|
|
1206
|
-
children: span.events.map((event, index) => {
|
|
1207
|
-
const isExpanded = expandedEvents.has(index);
|
|
1208
|
-
const hasAttributes = event.attributes && Object.keys(event.attributes).length > 0;
|
|
1209
|
-
const relativeTime = formatRelativeTime$1(event.timeUnixMs, span.startTimeUnixMs);
|
|
1210
|
-
return /* @__PURE__ */ jsxs("div", {
|
|
1211
|
-
className: "border border-border rounded-lg overflow-hidden",
|
|
1212
|
-
children: [
|
|
1213
|
-
/* @__PURE__ */ jsx("div", {
|
|
1214
|
-
className: "bg-muted p-3",
|
|
1215
|
-
children: /* @__PURE__ */ jsxs("div", {
|
|
1216
|
-
className: "flex items-start justify-between gap-2",
|
|
1217
|
-
children: [/* @__PURE__ */ jsxs("div", {
|
|
1218
|
-
className: "flex-1 min-w-0",
|
|
1219
|
-
children: [/* @__PURE__ */ jsx("div", {
|
|
1220
|
-
className: "font-medium text-sm text-foreground truncate",
|
|
1221
|
-
children: event.name
|
|
1222
|
-
}), /* @__PURE__ */ jsxs("div", {
|
|
1223
|
-
className: "text-xs text-muted-foreground mt-1 font-mono",
|
|
1224
|
-
children: [relativeTime, " from span start"]
|
|
1225
|
-
})]
|
|
1226
|
-
}), hasAttributes && /* @__PURE__ */ jsx("button", {
|
|
1227
|
-
onClick: () => toggleEventExpanded(index),
|
|
1228
|
-
className: "p-1 hover:bg-muted/80 rounded transition-colors",
|
|
1229
|
-
"aria-label": isExpanded ? "Collapse attributes" : "Expand attributes",
|
|
1230
|
-
"aria-expanded": isExpanded,
|
|
1231
|
-
children: /* @__PURE__ */ jsx("svg", {
|
|
1232
|
-
className: `w-4 h-4 text-muted-foreground transition-transform ${isExpanded ? "rotate-180" : ""}`,
|
|
1233
|
-
fill: "none",
|
|
1234
|
-
stroke: "currentColor",
|
|
1235
|
-
viewBox: "0 0 24 24",
|
|
1236
|
-
children: /* @__PURE__ */ jsx("path", {
|
|
1237
|
-
strokeLinecap: "round",
|
|
1238
|
-
strokeLinejoin: "round",
|
|
1239
|
-
strokeWidth: 2,
|
|
1240
|
-
d: "M19 9l-7 7-7-7"
|
|
1241
|
-
})
|
|
1242
|
-
})
|
|
1243
|
-
})]
|
|
1244
|
-
})
|
|
1245
|
-
}),
|
|
1246
|
-
hasAttributes && isExpanded && /* @__PURE__ */ jsxs("div", {
|
|
1247
|
-
className: "p-3 bg-background border-t border-border",
|
|
1248
|
-
children: [/* @__PURE__ */ jsx("div", {
|
|
1249
|
-
className: "text-xs font-semibold text-foreground mb-2",
|
|
1250
|
-
children: "Attributes"
|
|
1251
|
-
}), /* @__PURE__ */ jsx("div", {
|
|
1252
|
-
className: "space-y-2",
|
|
1253
|
-
children: Object.entries(event.attributes).map(([key, value]) => /* @__PURE__ */ jsxs("div", {
|
|
1254
|
-
className: "grid grid-cols-[minmax(100px,1fr)_2fr] gap-3 text-xs",
|
|
1255
|
-
children: [/* @__PURE__ */ jsx("div", {
|
|
1256
|
-
className: "font-mono font-medium text-foreground break-words",
|
|
1257
|
-
children: key
|
|
1258
|
-
}), /* @__PURE__ */ jsx("div", {
|
|
1259
|
-
className: "text-foreground break-words",
|
|
1260
|
-
children: typeof value === "object" ? /* @__PURE__ */ jsx("pre", {
|
|
1261
|
-
className: "text-xs bg-muted p-2 rounded border border-border overflow-x-auto",
|
|
1262
|
-
children: formatAttributeValue(value)
|
|
1263
|
-
}) : /* @__PURE__ */ jsx("span", { children: formatAttributeValue(value) })
|
|
1264
|
-
})]
|
|
1265
|
-
}, key))
|
|
1266
|
-
})]
|
|
1267
|
-
}),
|
|
1268
|
-
!hasAttributes && /* @__PURE__ */ jsx("div", {
|
|
1269
|
-
className: "px-3 pb-3 text-xs text-muted-foreground italic",
|
|
1270
|
-
children: "No attributes"
|
|
1271
|
-
})
|
|
1272
|
-
]
|
|
1273
|
-
}, index);
|
|
1274
|
-
})
|
|
1275
|
-
});
|
|
1276
|
-
}
|
|
1277
|
-
|
|
1278
|
-
//#endregion
|
|
1279
|
-
//#region src/components/observability/TraceTimeline/DetailPane/LinksTab.tsx
|
|
1280
|
-
function truncateId(id) {
|
|
1281
|
-
return id.length > 8 ? `${id.substring(0, 8)}...` : id;
|
|
1282
|
-
}
|
|
1283
|
-
function LinksTab({ span, onLinkClick }) {
|
|
1284
|
-
const [expandedLinks, setExpandedLinks] = useState(/* @__PURE__ */ new Set());
|
|
1285
|
-
const [copiedId, setCopiedId] = useState(null);
|
|
1286
|
-
const toggleLinkExpanded = (index) => {
|
|
1287
|
-
setExpandedLinks((prev) => {
|
|
1288
|
-
const next = new Set(prev);
|
|
1289
|
-
if (next.has(index)) next.delete(index);
|
|
1290
|
-
else next.add(index);
|
|
1291
|
-
return next;
|
|
1292
|
-
});
|
|
1293
|
-
};
|
|
1294
|
-
const copyToClipboard = async (text, type, index) => {
|
|
1295
|
-
try {
|
|
1296
|
-
await navigator.clipboard.writeText(text);
|
|
1297
|
-
setCopiedId(`${type}-${index}-${text}`);
|
|
1298
|
-
setTimeout(() => setCopiedId(null), 2e3);
|
|
1299
|
-
} catch (err) {
|
|
1300
|
-
console.error("Failed to copy:", err);
|
|
1301
|
-
}
|
|
1302
|
-
};
|
|
1303
|
-
if (!span.links || span.links.length === 0) return /* @__PURE__ */ jsx("div", {
|
|
1304
|
-
className: "text-sm text-muted-foreground text-center py-8",
|
|
1305
|
-
children: "No links available"
|
|
1306
|
-
});
|
|
1307
|
-
return /* @__PURE__ */ jsx("div", {
|
|
1308
|
-
className: "space-y-3",
|
|
1309
|
-
children: span.links.map((link, index) => {
|
|
1310
|
-
const isExpanded = expandedLinks.has(index);
|
|
1311
|
-
const hasAttributes = link.attributes && Object.keys(link.attributes).length > 0;
|
|
1312
|
-
return /* @__PURE__ */ jsxs("div", {
|
|
1313
|
-
className: "border border-border rounded-lg overflow-hidden",
|
|
1314
|
-
children: [/* @__PURE__ */ jsxs("div", {
|
|
1315
|
-
className: "bg-muted p-3",
|
|
1316
|
-
children: [
|
|
1317
|
-
/* @__PURE__ */ jsxs("div", {
|
|
1318
|
-
className: "mb-2",
|
|
1319
|
-
children: [/* @__PURE__ */ jsx("div", {
|
|
1320
|
-
className: "text-xs font-semibold text-muted-foreground mb-1",
|
|
1321
|
-
children: "Trace ID"
|
|
1322
|
-
}), /* @__PURE__ */ jsxs("div", {
|
|
1323
|
-
className: "flex items-center gap-2",
|
|
1324
|
-
children: [/* @__PURE__ */ jsx("code", {
|
|
1325
|
-
className: "text-xs font-mono text-foreground bg-background px-2 py-1 rounded border border-border flex-1 truncate",
|
|
1326
|
-
title: link.traceId,
|
|
1327
|
-
children: truncateId(link.traceId)
|
|
1328
|
-
}), /* @__PURE__ */ jsx("button", {
|
|
1329
|
-
onClick: () => copyToClipboard(link.traceId, "trace", index),
|
|
1330
|
-
className: "p-1 hover:bg-muted/80 rounded transition-colors",
|
|
1331
|
-
"aria-label": "Copy trace ID",
|
|
1332
|
-
children: /* @__PURE__ */ jsx("svg", {
|
|
1333
|
-
className: `w-4 h-4 ${copiedId === `trace-${index}-${link.traceId}` ? "text-green-600" : "text-muted-foreground"}`,
|
|
1334
|
-
fill: "none",
|
|
1335
|
-
stroke: "currentColor",
|
|
1336
|
-
viewBox: "0 0 24 24",
|
|
1337
|
-
children: copiedId === `trace-${index}-${link.traceId}` ? /* @__PURE__ */ jsx("path", {
|
|
1338
|
-
strokeLinecap: "round",
|
|
1339
|
-
strokeLinejoin: "round",
|
|
1340
|
-
strokeWidth: 2,
|
|
1341
|
-
d: "M5 13l4 4L19 7"
|
|
1342
|
-
}) : /* @__PURE__ */ jsx("path", {
|
|
1343
|
-
strokeLinecap: "round",
|
|
1344
|
-
strokeLinejoin: "round",
|
|
1345
|
-
strokeWidth: 2,
|
|
1346
|
-
d: "M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"
|
|
1347
|
-
})
|
|
1348
|
-
})
|
|
1349
|
-
})]
|
|
1350
|
-
})]
|
|
1351
|
-
}),
|
|
1352
|
-
/* @__PURE__ */ jsxs("div", {
|
|
1353
|
-
className: "mb-2",
|
|
1354
|
-
children: [/* @__PURE__ */ jsx("div", {
|
|
1355
|
-
className: "text-xs font-semibold text-muted-foreground mb-1",
|
|
1356
|
-
children: "Span ID"
|
|
1357
|
-
}), /* @__PURE__ */ jsxs("div", {
|
|
1358
|
-
className: "flex items-center gap-2",
|
|
1359
|
-
children: [/* @__PURE__ */ jsx("code", {
|
|
1360
|
-
className: "text-xs font-mono text-foreground bg-background px-2 py-1 rounded border border-border flex-1 truncate",
|
|
1361
|
-
title: link.spanId,
|
|
1362
|
-
children: truncateId(link.spanId)
|
|
1363
|
-
}), /* @__PURE__ */ jsx("button", {
|
|
1364
|
-
onClick: () => copyToClipboard(link.spanId, "span", index),
|
|
1365
|
-
className: "p-1 hover:bg-muted/80 rounded transition-colors",
|
|
1366
|
-
"aria-label": "Copy span ID",
|
|
1367
|
-
children: /* @__PURE__ */ jsx("svg", {
|
|
1368
|
-
className: `w-4 h-4 ${copiedId === `span-${index}-${link.spanId}` ? "text-green-600" : "text-muted-foreground"}`,
|
|
1369
|
-
fill: "none",
|
|
1370
|
-
stroke: "currentColor",
|
|
1371
|
-
viewBox: "0 0 24 24",
|
|
1372
|
-
children: copiedId === `span-${index}-${link.spanId}` ? /* @__PURE__ */ jsx("path", {
|
|
1373
|
-
strokeLinecap: "round",
|
|
1374
|
-
strokeLinejoin: "round",
|
|
1375
|
-
strokeWidth: 2,
|
|
1376
|
-
d: "M5 13l4 4L19 7"
|
|
1377
|
-
}) : /* @__PURE__ */ jsx("path", {
|
|
1378
|
-
strokeLinecap: "round",
|
|
1379
|
-
strokeLinejoin: "round",
|
|
1380
|
-
strokeWidth: 2,
|
|
1381
|
-
d: "M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"
|
|
1382
|
-
})
|
|
1383
|
-
})
|
|
1384
|
-
})]
|
|
1385
|
-
})]
|
|
1386
|
-
}),
|
|
1387
|
-
onLinkClick && /* @__PURE__ */ jsx("button", {
|
|
1388
|
-
onClick: () => onLinkClick(link.traceId, link.spanId),
|
|
1389
|
-
className: "w-full mt-2 px-3 py-2 bg-primary text-primary-foreground text-sm font-medium rounded hover:bg-primary/90 transition-colors",
|
|
1390
|
-
children: "Navigate to Span"
|
|
1391
|
-
}),
|
|
1392
|
-
hasAttributes && /* @__PURE__ */ jsxs("button", {
|
|
1393
|
-
onClick: () => toggleLinkExpanded(index),
|
|
1394
|
-
className: "w-full mt-2 px-3 py-1.5 text-xs text-foreground bg-background border border-border rounded hover:bg-muted transition-colors flex items-center justify-center gap-1",
|
|
1395
|
-
"aria-expanded": isExpanded,
|
|
1396
|
-
children: [/* @__PURE__ */ jsxs("span", { children: [
|
|
1397
|
-
isExpanded ? "Hide" : "Show",
|
|
1398
|
-
" Attributes (",
|
|
1399
|
-
Object.keys(link.attributes).length,
|
|
1400
|
-
")"
|
|
1401
|
-
] }), /* @__PURE__ */ jsx("svg", {
|
|
1402
|
-
className: `w-3 h-3 transition-transform ${isExpanded ? "rotate-180" : ""}`,
|
|
1403
|
-
fill: "none",
|
|
1404
|
-
stroke: "currentColor",
|
|
1405
|
-
viewBox: "0 0 24 24",
|
|
1406
|
-
children: /* @__PURE__ */ jsx("path", {
|
|
1407
|
-
strokeLinecap: "round",
|
|
1408
|
-
strokeLinejoin: "round",
|
|
1409
|
-
strokeWidth: 2,
|
|
1410
|
-
d: "M19 9l-7 7-7-7"
|
|
1411
|
-
})
|
|
1412
|
-
})]
|
|
1413
|
-
})
|
|
1414
|
-
]
|
|
1415
|
-
}), hasAttributes && isExpanded && /* @__PURE__ */ jsx("div", {
|
|
1416
|
-
className: "p-3 bg-background border-t border-border",
|
|
1417
|
-
children: /* @__PURE__ */ jsx("div", {
|
|
1418
|
-
className: "space-y-2",
|
|
1419
|
-
children: Object.entries(link.attributes).map(([key, value]) => /* @__PURE__ */ jsxs("div", {
|
|
1420
|
-
className: "grid grid-cols-[minmax(100px,1fr)_2fr] gap-3 text-xs",
|
|
1421
|
-
children: [/* @__PURE__ */ jsx("div", {
|
|
1422
|
-
className: "font-mono font-medium text-foreground break-words",
|
|
1423
|
-
children: key
|
|
1424
|
-
}), /* @__PURE__ */ jsx("div", {
|
|
1425
|
-
className: "text-foreground break-words",
|
|
1426
|
-
children: typeof value === "object" ? /* @__PURE__ */ jsx("pre", {
|
|
1427
|
-
className: "text-xs bg-muted p-2 rounded border border-border overflow-x-auto",
|
|
1428
|
-
children: formatAttributeValue(value)
|
|
1429
|
-
}) : /* @__PURE__ */ jsx("span", { children: formatAttributeValue(value) })
|
|
1430
|
-
})]
|
|
1431
|
-
}, key))
|
|
1432
|
-
})
|
|
1433
|
-
})]
|
|
1434
|
-
}, index);
|
|
1435
|
-
})
|
|
1456
|
+
]
|
|
1436
1457
|
});
|
|
1437
1458
|
}
|
|
1438
|
-
|
|
1439
|
-
//#endregion
|
|
1440
|
-
//#region src/components/observability/TraceTimeline/DetailPane/index.tsx
|
|
1441
|
-
function DetailPane({ span, onClose, onLinkClick, initialTab = "attributes" }) {
|
|
1442
|
-
const [activeTab, setActiveTab] = useState(initialTab);
|
|
1459
|
+
function SpanDetailInline({ span, traceStartMs }) {
|
|
1443
1460
|
const [copiedId, setCopiedId] = useState(false);
|
|
1444
|
-
const
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
const handleCopySpanId = useCallback(async () => {
|
|
1461
|
+
const serviceColor = getServiceColor(span.serviceName);
|
|
1462
|
+
const relativeStartMs = span.startTimeUnixMs - traceStartMs;
|
|
1463
|
+
const handleCopy = useCallback(async () => {
|
|
1448
1464
|
try {
|
|
1449
1465
|
await navigator.clipboard.writeText(span.spanId);
|
|
1450
1466
|
setCopiedId(true);
|
|
1451
1467
|
setTimeout(() => setCopiedId(false), 2e3);
|
|
1452
|
-
} catch
|
|
1453
|
-
console.error("Failed to copy span ID:", err);
|
|
1454
|
-
}
|
|
1468
|
+
} catch {}
|
|
1455
1469
|
}, [span.spanId]);
|
|
1470
|
+
const spanAttrs = Object.entries(span.attributes).sort(([a], [b]) => a.localeCompare(b));
|
|
1471
|
+
const resourceAttrs = Object.entries(span.resourceAttributes).sort(([a], [b]) => a.localeCompare(b));
|
|
1456
1472
|
return /* @__PURE__ */ jsxs("div", {
|
|
1457
|
-
className: "
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
}, [onClose]),
|
|
1461
|
-
tabIndex: -1,
|
|
1462
|
-
role: "complementary",
|
|
1463
|
-
"aria-label": "Span details",
|
|
1473
|
+
className: "border-b border-border bg-muted/50 px-4 py-3",
|
|
1474
|
+
style: { borderLeft: `3px solid ${serviceColor}` },
|
|
1475
|
+
onClick: (e) => e.stopPropagation(),
|
|
1464
1476
|
children: [
|
|
1465
1477
|
/* @__PURE__ */ jsxs("div", {
|
|
1466
|
-
className: "
|
|
1467
|
-
children: [
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
children: /* @__PURE__ */ jsx("path", {
|
|
1484
|
-
strokeLinecap: "round",
|
|
1485
|
-
strokeLinejoin: "round",
|
|
1486
|
-
strokeWidth: 2,
|
|
1487
|
-
d: "M6 18L18 6M6 6l12 12"
|
|
1488
|
-
})
|
|
1478
|
+
className: "mb-2",
|
|
1479
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
1480
|
+
className: "text-sm font-medium text-foreground",
|
|
1481
|
+
children: span.name
|
|
1482
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
1483
|
+
className: "flex flex-wrap gap-x-4 gap-y-1 text-xs text-muted-foreground mt-1",
|
|
1484
|
+
children: [
|
|
1485
|
+
/* @__PURE__ */ jsxs("span", { children: ["Service: ", /* @__PURE__ */ jsx("span", {
|
|
1486
|
+
className: "text-foreground",
|
|
1487
|
+
children: span.serviceName
|
|
1488
|
+
})] }),
|
|
1489
|
+
/* @__PURE__ */ jsxs("span", { children: [
|
|
1490
|
+
"Duration:",
|
|
1491
|
+
" ",
|
|
1492
|
+
/* @__PURE__ */ jsx("span", {
|
|
1493
|
+
className: "text-foreground",
|
|
1494
|
+
children: formatDuration(span.durationMs)
|
|
1489
1495
|
})
|
|
1490
|
-
})
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
children: /* @__PURE__ */ jsx("div", {
|
|
1495
|
-
className: "text-sm font-medium text-foreground truncate",
|
|
1496
|
-
title: span.name,
|
|
1497
|
-
children: span.name
|
|
1498
|
-
})
|
|
1499
|
-
}),
|
|
1500
|
-
/* @__PURE__ */ jsxs("div", {
|
|
1501
|
-
className: "flex items-center gap-2",
|
|
1502
|
-
children: [
|
|
1496
|
+
] }),
|
|
1497
|
+
/* @__PURE__ */ jsxs("span", { children: [
|
|
1498
|
+
"Start:",
|
|
1499
|
+
" ",
|
|
1503
1500
|
/* @__PURE__ */ jsx("span", {
|
|
1504
|
-
className: "text-
|
|
1505
|
-
children:
|
|
1506
|
-
}),
|
|
1507
|
-
/* @__PURE__ */ jsx("code", {
|
|
1508
|
-
className: "text-xs font-mono text-foreground bg-muted px-2 py-1 rounded flex-1 truncate",
|
|
1509
|
-
title: span.spanId,
|
|
1510
|
-
children: span.spanId
|
|
1511
|
-
}),
|
|
1512
|
-
/* @__PURE__ */ jsx("button", {
|
|
1513
|
-
onClick: handleCopySpanId,
|
|
1514
|
-
className: "p-1 hover:bg-muted rounded transition-colors",
|
|
1515
|
-
"aria-label": "Copy span ID",
|
|
1516
|
-
children: /* @__PURE__ */ jsx("svg", {
|
|
1517
|
-
className: `w-4 h-4 ${copiedId ? "text-green-600 dark:text-green-400" : "text-muted-foreground"}`,
|
|
1518
|
-
fill: "none",
|
|
1519
|
-
stroke: "currentColor",
|
|
1520
|
-
viewBox: "0 0 24 24",
|
|
1521
|
-
children: copiedId ? /* @__PURE__ */ jsx("path", {
|
|
1522
|
-
strokeLinecap: "round",
|
|
1523
|
-
strokeLinejoin: "round",
|
|
1524
|
-
strokeWidth: 2,
|
|
1525
|
-
d: "M5 13l4 4L19 7"
|
|
1526
|
-
}) : /* @__PURE__ */ jsx("path", {
|
|
1527
|
-
strokeLinecap: "round",
|
|
1528
|
-
strokeLinejoin: "round",
|
|
1529
|
-
strokeWidth: 2,
|
|
1530
|
-
d: "M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"
|
|
1531
|
-
})
|
|
1532
|
-
})
|
|
1501
|
+
className: "text-foreground",
|
|
1502
|
+
children: formatDuration(relativeStartMs)
|
|
1533
1503
|
})
|
|
1534
|
-
]
|
|
1535
|
-
|
|
1536
|
-
|
|
1504
|
+
] }),
|
|
1505
|
+
/* @__PURE__ */ jsxs("span", { children: ["Kind: ", /* @__PURE__ */ jsx("span", {
|
|
1506
|
+
className: "text-foreground",
|
|
1507
|
+
children: span.kind
|
|
1508
|
+
})] }),
|
|
1509
|
+
span.status !== "UNSET" && /* @__PURE__ */ jsxs("span", { children: [
|
|
1510
|
+
"Status:",
|
|
1511
|
+
" ",
|
|
1512
|
+
/* @__PURE__ */ jsx("span", {
|
|
1513
|
+
className: span.status === "ERROR" ? "text-red-500" : "text-foreground",
|
|
1514
|
+
children: span.status
|
|
1515
|
+
})
|
|
1516
|
+
] })
|
|
1517
|
+
]
|
|
1518
|
+
})]
|
|
1537
1519
|
}),
|
|
1538
|
-
/* @__PURE__ */
|
|
1539
|
-
className: "
|
|
1540
|
-
role: "tablist",
|
|
1541
|
-
"aria-label": "Span detail tabs",
|
|
1520
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1521
|
+
className: "space-y-1",
|
|
1542
1522
|
children: [
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1523
|
+
/* @__PURE__ */ jsx(CollapsibleSection, {
|
|
1524
|
+
title: "Tags",
|
|
1525
|
+
count: spanAttrs.length,
|
|
1526
|
+
children: spanAttrs.map(([k, v]) => /* @__PURE__ */ jsx(KeyValueRow, {
|
|
1527
|
+
k,
|
|
1528
|
+
v
|
|
1529
|
+
}, k))
|
|
1530
|
+
}),
|
|
1531
|
+
/* @__PURE__ */ jsx(CollapsibleSection, {
|
|
1532
|
+
title: "Process",
|
|
1533
|
+
count: resourceAttrs.length,
|
|
1534
|
+
children: resourceAttrs.map(([k, v]) => /* @__PURE__ */ jsx(KeyValueRow, {
|
|
1535
|
+
k,
|
|
1536
|
+
v
|
|
1537
|
+
}, k))
|
|
1538
|
+
}),
|
|
1539
|
+
/* @__PURE__ */ jsx(CollapsibleSection, {
|
|
1540
|
+
title: "Events",
|
|
1541
|
+
count: span.events.length,
|
|
1542
|
+
children: span.events.map((event, i) => /* @__PURE__ */ jsxs("div", {
|
|
1543
|
+
className: "text-xs border-l-2 border-border pl-2 py-1.5 space-y-0.5",
|
|
1544
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
1545
|
+
className: "flex items-center gap-2",
|
|
1546
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
1547
|
+
className: "font-mono text-muted-foreground flex-shrink-0",
|
|
1548
|
+
children: formatRelativeTime$1(event.timeUnixMs, span.startTimeUnixMs)
|
|
1549
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
1550
|
+
className: "font-medium text-foreground",
|
|
1551
|
+
children: event.name
|
|
1552
|
+
})]
|
|
1553
|
+
}), Object.entries(event.attributes).map(([k, v]) => /* @__PURE__ */ jsx(KeyValueRow, {
|
|
1554
|
+
k,
|
|
1555
|
+
v
|
|
1556
|
+
}, k))]
|
|
1557
|
+
}, i))
|
|
1558
|
+
}),
|
|
1559
|
+
/* @__PURE__ */ jsx(CollapsibleSection, {
|
|
1560
|
+
title: "Links",
|
|
1561
|
+
count: span.links.length,
|
|
1562
|
+
children: span.links.map((link, i) => /* @__PURE__ */ jsxs("div", {
|
|
1563
|
+
className: "text-xs font-mono py-0.5",
|
|
1555
1564
|
children: [
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1565
|
+
/* @__PURE__ */ jsx("span", {
|
|
1566
|
+
className: "text-muted-foreground",
|
|
1567
|
+
children: "trace:"
|
|
1568
|
+
}),
|
|
1569
|
+
" ",
|
|
1570
|
+
link.traceId,
|
|
1571
|
+
" ",
|
|
1572
|
+
/* @__PURE__ */ jsx("span", {
|
|
1573
|
+
className: "text-muted-foreground",
|
|
1574
|
+
children: "span:"
|
|
1575
|
+
}),
|
|
1576
|
+
" ",
|
|
1577
|
+
link.spanId
|
|
1559
1578
|
]
|
|
1560
|
-
})
|
|
1561
|
-
}
|
|
1562
|
-
|
|
1579
|
+
}, i))
|
|
1580
|
+
})
|
|
1581
|
+
]
|
|
1563
1582
|
}),
|
|
1564
1583
|
/* @__PURE__ */ jsxs("div", {
|
|
1565
|
-
className: "flex-
|
|
1584
|
+
className: "flex items-center justify-end gap-2 mt-2 pt-2 border-t border-border",
|
|
1566
1585
|
children: [
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1586
|
+
/* @__PURE__ */ jsx("span", {
|
|
1587
|
+
className: "text-xs text-muted-foreground",
|
|
1588
|
+
children: "SpanID:"
|
|
1589
|
+
}),
|
|
1590
|
+
/* @__PURE__ */ jsx("code", {
|
|
1591
|
+
className: "text-xs font-mono text-foreground",
|
|
1592
|
+
children: span.spanId
|
|
1593
|
+
}),
|
|
1594
|
+
/* @__PURE__ */ jsx("button", {
|
|
1595
|
+
onClick: handleCopy,
|
|
1596
|
+
className: "text-xs text-muted-foreground hover:text-foreground",
|
|
1597
|
+
"aria-label": "Copy span ID",
|
|
1598
|
+
children: copiedId ? "✓" : "Copy"
|
|
1572
1599
|
})
|
|
1573
1600
|
]
|
|
1574
1601
|
})
|
|
1575
1602
|
]
|
|
1576
1603
|
});
|
|
1577
1604
|
}
|
|
1578
|
-
|
|
1579
1605
|
//#endregion
|
|
1580
1606
|
//#region src/components/observability/shared/LoadingSkeleton.tsx
|
|
1581
1607
|
function LoadingSkeleton() {
|
|
@@ -1617,7 +1643,6 @@ function LoadingSkeleton() {
|
|
|
1617
1643
|
})]
|
|
1618
1644
|
});
|
|
1619
1645
|
}
|
|
1620
|
-
|
|
1621
1646
|
//#endregion
|
|
1622
1647
|
//#region src/components/KeyboardShortcuts/context.ts
|
|
1623
1648
|
const noop = () => {};
|
|
@@ -1637,7 +1662,6 @@ function useRegisterShortcuts(id, group) {
|
|
|
1637
1662
|
unregister
|
|
1638
1663
|
]);
|
|
1639
1664
|
}
|
|
1640
|
-
|
|
1641
1665
|
//#endregion
|
|
1642
1666
|
//#region src/components/KeyboardShortcuts/ShortcutsHelpDialog.tsx
|
|
1643
1667
|
function ShortcutsHelpDialog({ open, onClose, groups }) {
|
|
@@ -1694,7 +1718,6 @@ function ShortcutsHelpDialog({ open, onClose, groups }) {
|
|
|
1694
1718
|
})
|
|
1695
1719
|
});
|
|
1696
1720
|
}
|
|
1697
|
-
|
|
1698
1721
|
//#endregion
|
|
1699
1722
|
//#region src/components/KeyboardShortcuts/KeyboardShortcutsProvider.tsx
|
|
1700
1723
|
const GENERAL_GROUP = {
|
|
@@ -1705,8 +1728,8 @@ const GENERAL_GROUP = {
|
|
|
1705
1728
|
description: "Toggle shortcuts help"
|
|
1706
1729
|
},
|
|
1707
1730
|
{
|
|
1708
|
-
keys: ["Shift", "
|
|
1709
|
-
description: "
|
|
1731
|
+
keys: ["Shift", "T"],
|
|
1732
|
+
description: "Traces tab"
|
|
1710
1733
|
},
|
|
1711
1734
|
{
|
|
1712
1735
|
keys: ["Shift", "L"],
|
|
@@ -1737,8 +1760,8 @@ function KeyboardShortcutsProvider({ children, onNavigateServices, onNavigateLog
|
|
|
1737
1760
|
}, []);
|
|
1738
1761
|
useEffect(() => {
|
|
1739
1762
|
function handleKeyDown(e) {
|
|
1740
|
-
|
|
1741
|
-
if (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.tagName === "SELECT" || target.isContentEditable) return;
|
|
1763
|
+
if (!(e.target instanceof HTMLElement)) return;
|
|
1764
|
+
if (e.target.tagName === "INPUT" || e.target.tagName === "TEXTAREA" || e.target.tagName === "SELECT" || e.target.isContentEditable) return;
|
|
1742
1765
|
if (e.shiftKey && e.key === "?") {
|
|
1743
1766
|
e.preventDefault();
|
|
1744
1767
|
setIsOpen((v) => !v);
|
|
@@ -1749,7 +1772,7 @@ function KeyboardShortcutsProvider({ children, onNavigateServices, onNavigateLog
|
|
|
1749
1772
|
setIsOpen(false);
|
|
1750
1773
|
return;
|
|
1751
1774
|
}
|
|
1752
|
-
if (e.shiftKey && e.key === "
|
|
1775
|
+
if (e.shiftKey && e.key === "T") {
|
|
1753
1776
|
e.preventDefault();
|
|
1754
1777
|
onNavigateServices();
|
|
1755
1778
|
return;
|
|
@@ -1789,7 +1812,6 @@ function KeyboardShortcutsProvider({ children, onNavigateServices, onNavigateLog
|
|
|
1789
1812
|
})]
|
|
1790
1813
|
});
|
|
1791
1814
|
}
|
|
1792
|
-
|
|
1793
1815
|
//#endregion
|
|
1794
1816
|
//#region src/components/observability/TraceTimeline/shortcuts.ts
|
|
1795
1817
|
const TRACE_VIEWER_SHORTCUTS = {
|
|
@@ -1841,7 +1863,914 @@ const TRACE_VIEWER_SHORTCUTS = {
|
|
|
1841
1863
|
}
|
|
1842
1864
|
]
|
|
1843
1865
|
};
|
|
1844
|
-
|
|
1866
|
+
//#endregion
|
|
1867
|
+
//#region src/components/observability/TraceTimeline/TimeRuler.tsx
|
|
1868
|
+
const TICK_COUNT = 5;
|
|
1869
|
+
function TimeRuler({ totalDurationMs, leftColumnWidth, offsetMs = 0 }) {
|
|
1870
|
+
const ticks = Array.from({ length: TICK_COUNT + 1 }, (_, i) => {
|
|
1871
|
+
const fraction = i / TICK_COUNT;
|
|
1872
|
+
return {
|
|
1873
|
+
label: formatDuration(offsetMs + totalDurationMs * fraction),
|
|
1874
|
+
percent: fraction * 100
|
|
1875
|
+
};
|
|
1876
|
+
});
|
|
1877
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1878
|
+
className: "flex border-b border-border bg-background",
|
|
1879
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
1880
|
+
className: "flex-shrink-0",
|
|
1881
|
+
style: { width: leftColumnWidth }
|
|
1882
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
1883
|
+
className: "flex-1 relative h-6 px-2",
|
|
1884
|
+
children: ticks.map((tick) => /* @__PURE__ */ jsxs("div", {
|
|
1885
|
+
className: "absolute top-0 h-full flex flex-col justify-end",
|
|
1886
|
+
style: { left: `${tick.percent}%` },
|
|
1887
|
+
children: [/* @__PURE__ */ jsx("div", { className: "h-2 border-l border-muted-foreground/40" }), /* @__PURE__ */ jsx("span", {
|
|
1888
|
+
className: "text-[10px] text-muted-foreground font-mono -translate-x-1/2 absolute bottom-0 whitespace-nowrap",
|
|
1889
|
+
style: {
|
|
1890
|
+
left: 0,
|
|
1891
|
+
transform: tick.percent === 100 ? "translateX(-100%)" : tick.percent === 0 ? "none" : "translateX(-50%)"
|
|
1892
|
+
},
|
|
1893
|
+
children: tick.label
|
|
1894
|
+
})]
|
|
1895
|
+
}, tick.percent))
|
|
1896
|
+
})]
|
|
1897
|
+
});
|
|
1898
|
+
}
|
|
1899
|
+
//#endregion
|
|
1900
|
+
//#region src/components/observability/TraceTimeline/SpanSearch.tsx
|
|
1901
|
+
function SpanSearch({ value, onChange, matchCount, currentMatch, onPrev, onNext }) {
|
|
1902
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1903
|
+
className: "flex items-center gap-1 px-2 py-1 border-b border-border bg-background",
|
|
1904
|
+
children: [/* @__PURE__ */ jsx("input", {
|
|
1905
|
+
type: "text",
|
|
1906
|
+
placeholder: "Find...",
|
|
1907
|
+
value,
|
|
1908
|
+
onChange: (e) => onChange(e.target.value),
|
|
1909
|
+
className: "bg-muted text-foreground text-sm px-2 py-0.5 rounded border border-border outline-none focus:border-blue-500 w-48"
|
|
1910
|
+
}), value && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1911
|
+
/* @__PURE__ */ jsx("span", {
|
|
1912
|
+
className: "text-xs text-muted-foreground whitespace-nowrap",
|
|
1913
|
+
children: matchCount > 0 ? `${currentMatch + 1}/${matchCount}` : "0 matches"
|
|
1914
|
+
}),
|
|
1915
|
+
/* @__PURE__ */ jsx("button", {
|
|
1916
|
+
onClick: onPrev,
|
|
1917
|
+
disabled: matchCount === 0,
|
|
1918
|
+
className: "p-0.5 text-muted-foreground hover:text-foreground disabled:opacity-30",
|
|
1919
|
+
"aria-label": "Previous match",
|
|
1920
|
+
children: /* @__PURE__ */ jsx("svg", {
|
|
1921
|
+
className: "w-3.5 h-3.5",
|
|
1922
|
+
fill: "none",
|
|
1923
|
+
stroke: "currentColor",
|
|
1924
|
+
viewBox: "0 0 24 24",
|
|
1925
|
+
children: /* @__PURE__ */ jsx("path", {
|
|
1926
|
+
strokeLinecap: "round",
|
|
1927
|
+
strokeLinejoin: "round",
|
|
1928
|
+
strokeWidth: 2,
|
|
1929
|
+
d: "M5 15l7-7 7 7"
|
|
1930
|
+
})
|
|
1931
|
+
})
|
|
1932
|
+
}),
|
|
1933
|
+
/* @__PURE__ */ jsx("button", {
|
|
1934
|
+
onClick: onNext,
|
|
1935
|
+
disabled: matchCount === 0,
|
|
1936
|
+
className: "p-0.5 text-muted-foreground hover:text-foreground disabled:opacity-30",
|
|
1937
|
+
"aria-label": "Next match",
|
|
1938
|
+
children: /* @__PURE__ */ jsx("svg", {
|
|
1939
|
+
className: "w-3.5 h-3.5",
|
|
1940
|
+
fill: "none",
|
|
1941
|
+
stroke: "currentColor",
|
|
1942
|
+
viewBox: "0 0 24 24",
|
|
1943
|
+
children: /* @__PURE__ */ jsx("path", {
|
|
1944
|
+
strokeLinecap: "round",
|
|
1945
|
+
strokeLinejoin: "round",
|
|
1946
|
+
strokeWidth: 2,
|
|
1947
|
+
d: "M19 9l-7 7-7-7"
|
|
1948
|
+
})
|
|
1949
|
+
})
|
|
1950
|
+
})
|
|
1951
|
+
] })]
|
|
1952
|
+
});
|
|
1953
|
+
}
|
|
1954
|
+
//#endregion
|
|
1955
|
+
//#region src/components/observability/TraceTimeline/ViewTabs.tsx
|
|
1956
|
+
const VIEWS = [
|
|
1957
|
+
"timeline",
|
|
1958
|
+
"graph",
|
|
1959
|
+
"statistics",
|
|
1960
|
+
"flamegraph"
|
|
1961
|
+
];
|
|
1962
|
+
const VIEW_LABELS = {
|
|
1963
|
+
timeline: "Timeline",
|
|
1964
|
+
graph: "Graph",
|
|
1965
|
+
statistics: "Statistics",
|
|
1966
|
+
flamegraph: "Flamegraph"
|
|
1967
|
+
};
|
|
1968
|
+
function ViewTabs({ activeView, onChange }) {
|
|
1969
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1970
|
+
className: "flex border-b border-border bg-background",
|
|
1971
|
+
children: VIEWS.map((view) => /* @__PURE__ */ jsx("button", {
|
|
1972
|
+
onClick: () => onChange(view),
|
|
1973
|
+
className: `px-4 py-1.5 text-sm font-medium transition-colors ${activeView === view ? "text-foreground border-b-2 border-blue-500" : "text-muted-foreground hover:text-foreground"}`,
|
|
1974
|
+
children: VIEW_LABELS[view]
|
|
1975
|
+
}, view))
|
|
1976
|
+
});
|
|
1977
|
+
}
|
|
1978
|
+
//#endregion
|
|
1979
|
+
//#region src/components/observability/TraceTimeline/GraphView.tsx
|
|
1980
|
+
/**
|
|
1981
|
+
* GraphView - SVG-based DAG showing service dependencies within a trace.
|
|
1982
|
+
*/
|
|
1983
|
+
function buildDAG(trace) {
|
|
1984
|
+
const nodeMap = /* @__PURE__ */ new Map();
|
|
1985
|
+
const edgeMap = /* @__PURE__ */ new Map();
|
|
1986
|
+
const childServices = /* @__PURE__ */ new Map();
|
|
1987
|
+
function walk(span, parentService) {
|
|
1988
|
+
const svc = span.serviceName;
|
|
1989
|
+
const existing = nodeMap.get(svc);
|
|
1990
|
+
if (existing) {
|
|
1991
|
+
existing.spanCount++;
|
|
1992
|
+
if (span.status === "ERROR") existing.errorCount++;
|
|
1993
|
+
} else nodeMap.set(svc, {
|
|
1994
|
+
spanCount: 1,
|
|
1995
|
+
errorCount: span.status === "ERROR" ? 1 : 0
|
|
1996
|
+
});
|
|
1997
|
+
if (parentService && parentService !== svc) {
|
|
1998
|
+
const key = `${parentService}→${svc}`;
|
|
1999
|
+
const edge = edgeMap.get(key);
|
|
2000
|
+
if (edge) {
|
|
2001
|
+
edge.callCount++;
|
|
2002
|
+
edge.totalDurationMs += span.durationMs;
|
|
2003
|
+
} else edgeMap.set(key, {
|
|
2004
|
+
callCount: 1,
|
|
2005
|
+
totalDurationMs: span.durationMs
|
|
2006
|
+
});
|
|
2007
|
+
if (!childServices.has(parentService)) childServices.set(parentService, /* @__PURE__ */ new Set());
|
|
2008
|
+
const parentChildren = childServices.get(parentService);
|
|
2009
|
+
if (parentChildren) parentChildren.add(svc);
|
|
2010
|
+
}
|
|
2011
|
+
for (const child of span.children) walk(child, svc);
|
|
2012
|
+
}
|
|
2013
|
+
for (const root of trace.rootSpans) walk(root);
|
|
2014
|
+
const edges = [];
|
|
2015
|
+
for (const [key, meta] of edgeMap) {
|
|
2016
|
+
const [from, to] = key.split("→");
|
|
2017
|
+
if (from && to) edges.push({
|
|
2018
|
+
from,
|
|
2019
|
+
to,
|
|
2020
|
+
...meta
|
|
2021
|
+
});
|
|
2022
|
+
}
|
|
2023
|
+
return {
|
|
2024
|
+
nodeMap,
|
|
2025
|
+
edges,
|
|
2026
|
+
childServices
|
|
2027
|
+
};
|
|
2028
|
+
}
|
|
2029
|
+
const NODE_W = 160;
|
|
2030
|
+
const NODE_H = 60;
|
|
2031
|
+
const LAYER_GAP_Y = 100;
|
|
2032
|
+
const NODE_GAP_X = 40;
|
|
2033
|
+
function layoutNodes(nodeMap, edges) {
|
|
2034
|
+
const children = /* @__PURE__ */ new Map();
|
|
2035
|
+
const hasParent = /* @__PURE__ */ new Set();
|
|
2036
|
+
for (const e of edges) {
|
|
2037
|
+
if (!children.has(e.from)) children.set(e.from, /* @__PURE__ */ new Set());
|
|
2038
|
+
const fromChildren = children.get(e.from);
|
|
2039
|
+
if (fromChildren) fromChildren.add(e.to);
|
|
2040
|
+
hasParent.add(e.to);
|
|
2041
|
+
}
|
|
2042
|
+
const roots = [...nodeMap.keys()].filter((s) => !hasParent.has(s));
|
|
2043
|
+
if (roots.length === 0 && nodeMap.size > 0) {
|
|
2044
|
+
const firstKey = nodeMap.keys().next().value;
|
|
2045
|
+
if (firstKey !== void 0) roots.push(firstKey);
|
|
2046
|
+
}
|
|
2047
|
+
const layerOf = /* @__PURE__ */ new Map();
|
|
2048
|
+
const enqueueCount = /* @__PURE__ */ new Map();
|
|
2049
|
+
const maxEnqueue = nodeMap.size * 2;
|
|
2050
|
+
const queue = [];
|
|
2051
|
+
for (const r of roots) {
|
|
2052
|
+
layerOf.set(r, 0);
|
|
2053
|
+
queue.push(r);
|
|
2054
|
+
}
|
|
2055
|
+
while (queue.length > 0) {
|
|
2056
|
+
const cur = queue.shift();
|
|
2057
|
+
if (!cur) continue;
|
|
2058
|
+
const curLayer = layerOf.get(cur);
|
|
2059
|
+
if (curLayer === void 0) continue;
|
|
2060
|
+
const kids = children.get(cur);
|
|
2061
|
+
if (!kids) continue;
|
|
2062
|
+
for (const kid of kids) {
|
|
2063
|
+
const prev = layerOf.get(kid);
|
|
2064
|
+
const count = enqueueCount.get(kid) ?? 0;
|
|
2065
|
+
if (prev === void 0 && count < maxEnqueue) {
|
|
2066
|
+
layerOf.set(kid, curLayer + 1);
|
|
2067
|
+
enqueueCount.set(kid, count + 1);
|
|
2068
|
+
queue.push(kid);
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
for (const name of nodeMap.keys()) if (!layerOf.has(name)) layerOf.set(name, 0);
|
|
2073
|
+
const layers = /* @__PURE__ */ new Map();
|
|
2074
|
+
for (const [name, layer] of layerOf) {
|
|
2075
|
+
if (!layers.has(layer)) layers.set(layer, []);
|
|
2076
|
+
const layerNames = layers.get(layer);
|
|
2077
|
+
if (layerNames) layerNames.push(name);
|
|
2078
|
+
}
|
|
2079
|
+
const nodes = [];
|
|
2080
|
+
const totalWidth = Math.max(...Array.from(layers.values()).map((l) => l.length), 1) * (NODE_W + NODE_GAP_X) - NODE_GAP_X;
|
|
2081
|
+
for (const [layer, names] of layers) {
|
|
2082
|
+
const offsetX = (totalWidth - (names.length * (NODE_W + NODE_GAP_X) - NODE_GAP_X)) / 2;
|
|
2083
|
+
names.forEach((name, i) => {
|
|
2084
|
+
const meta = nodeMap.get(name);
|
|
2085
|
+
if (!meta) return;
|
|
2086
|
+
nodes.push({
|
|
2087
|
+
name,
|
|
2088
|
+
spanCount: meta.spanCount,
|
|
2089
|
+
errorCount: meta.errorCount,
|
|
2090
|
+
layer,
|
|
2091
|
+
x: offsetX + i * (NODE_W + NODE_GAP_X),
|
|
2092
|
+
y: layer * (NODE_H + LAYER_GAP_Y)
|
|
2093
|
+
});
|
|
2094
|
+
});
|
|
2095
|
+
}
|
|
2096
|
+
return nodes;
|
|
2097
|
+
}
|
|
2098
|
+
function GraphView({ trace }) {
|
|
2099
|
+
const { nodes, edges, svgWidth, svgHeight } = useMemo(() => {
|
|
2100
|
+
const { nodeMap, edges } = buildDAG(trace);
|
|
2101
|
+
const nodes = layoutNodes(nodeMap, edges);
|
|
2102
|
+
const maxX = Math.max(...nodes.map((n) => n.x + NODE_W), NODE_W);
|
|
2103
|
+
const maxY = Math.max(...nodes.map((n) => n.y + NODE_H), NODE_H);
|
|
2104
|
+
const padding = 40;
|
|
2105
|
+
return {
|
|
2106
|
+
nodes,
|
|
2107
|
+
edges,
|
|
2108
|
+
svgWidth: maxX + padding * 2,
|
|
2109
|
+
svgHeight: maxY + padding * 2
|
|
2110
|
+
};
|
|
2111
|
+
}, [trace]);
|
|
2112
|
+
const nodeByName = useMemo(() => {
|
|
2113
|
+
const m = /* @__PURE__ */ new Map();
|
|
2114
|
+
for (const n of nodes) m.set(n.name, n);
|
|
2115
|
+
return m;
|
|
2116
|
+
}, [nodes]);
|
|
2117
|
+
const padding = 40;
|
|
2118
|
+
return /* @__PURE__ */ jsx("div", {
|
|
2119
|
+
className: "flex-1 overflow-auto bg-background p-4 flex justify-center",
|
|
2120
|
+
children: /* @__PURE__ */ jsxs("svg", {
|
|
2121
|
+
viewBox: `0 0 ${svgWidth} ${svgHeight}`,
|
|
2122
|
+
width: svgWidth,
|
|
2123
|
+
height: svgHeight,
|
|
2124
|
+
role: "img",
|
|
2125
|
+
"aria-label": "Service dependency graph",
|
|
2126
|
+
children: [
|
|
2127
|
+
/* @__PURE__ */ jsx("defs", { children: /* @__PURE__ */ jsx("marker", {
|
|
2128
|
+
id: "arrowhead",
|
|
2129
|
+
markerWidth: "10",
|
|
2130
|
+
markerHeight: "7",
|
|
2131
|
+
refX: "9",
|
|
2132
|
+
refY: "3.5",
|
|
2133
|
+
orient: "auto",
|
|
2134
|
+
children: /* @__PURE__ */ jsx("polygon", {
|
|
2135
|
+
points: "0 0, 10 3.5, 0 7",
|
|
2136
|
+
fill: "#94a3b8"
|
|
2137
|
+
})
|
|
2138
|
+
}) }),
|
|
2139
|
+
edges.map((edge) => {
|
|
2140
|
+
const from = nodeByName.get(edge.from);
|
|
2141
|
+
const to = nodeByName.get(edge.to);
|
|
2142
|
+
if (!from || !to) return null;
|
|
2143
|
+
const x1 = padding + from.x + NODE_W / 2;
|
|
2144
|
+
const y1 = padding + from.y + NODE_H;
|
|
2145
|
+
const x2 = padding + to.x + NODE_W / 2;
|
|
2146
|
+
const y2 = padding + to.y;
|
|
2147
|
+
const midY = (y1 + y2) / 2;
|
|
2148
|
+
return /* @__PURE__ */ jsxs("g", { children: [/* @__PURE__ */ jsx("path", {
|
|
2149
|
+
d: `M ${x1} ${y1} C ${x1} ${midY}, ${x2} ${midY}, ${x2} ${y2}`,
|
|
2150
|
+
fill: "none",
|
|
2151
|
+
stroke: "#475569",
|
|
2152
|
+
strokeWidth: 1.5,
|
|
2153
|
+
markerEnd: "url(#arrowhead)"
|
|
2154
|
+
}), edge.callCount > 1 && /* @__PURE__ */ jsxs("text", {
|
|
2155
|
+
x: (x1 + x2) / 2,
|
|
2156
|
+
y: midY - 6,
|
|
2157
|
+
textAnchor: "middle",
|
|
2158
|
+
fontSize: 11,
|
|
2159
|
+
fill: "#94a3b8",
|
|
2160
|
+
children: [edge.callCount, "x"]
|
|
2161
|
+
})] }, `${edge.from}→${edge.to}`);
|
|
2162
|
+
}),
|
|
2163
|
+
nodes.map((node) => {
|
|
2164
|
+
const color = getServiceColor(node.name);
|
|
2165
|
+
const hasError = node.errorCount > 0;
|
|
2166
|
+
const textColor = "#f8fafc";
|
|
2167
|
+
const nx = padding + node.x;
|
|
2168
|
+
const ny = padding + node.y;
|
|
2169
|
+
return /* @__PURE__ */ jsxs("g", { children: [
|
|
2170
|
+
/* @__PURE__ */ jsx("rect", {
|
|
2171
|
+
x: nx,
|
|
2172
|
+
y: ny,
|
|
2173
|
+
width: NODE_W,
|
|
2174
|
+
height: NODE_H,
|
|
2175
|
+
rx: 8,
|
|
2176
|
+
ry: 8,
|
|
2177
|
+
fill: color,
|
|
2178
|
+
stroke: hasError ? "#ef4444" : "none",
|
|
2179
|
+
strokeWidth: hasError ? 2 : 0
|
|
2180
|
+
}),
|
|
2181
|
+
/* @__PURE__ */ jsx("text", {
|
|
2182
|
+
x: nx + NODE_W / 2,
|
|
2183
|
+
y: ny + 24,
|
|
2184
|
+
textAnchor: "middle",
|
|
2185
|
+
fontSize: 13,
|
|
2186
|
+
fontWeight: 600,
|
|
2187
|
+
fill: textColor,
|
|
2188
|
+
children: node.name.length > 18 ? node.name.slice(0, 16) + "..." : node.name
|
|
2189
|
+
}),
|
|
2190
|
+
/* @__PURE__ */ jsxs("text", {
|
|
2191
|
+
x: nx + NODE_W / 2,
|
|
2192
|
+
y: ny + 44,
|
|
2193
|
+
textAnchor: "middle",
|
|
2194
|
+
fontSize: 11,
|
|
2195
|
+
fill: textColor,
|
|
2196
|
+
opacity: .85,
|
|
2197
|
+
children: [
|
|
2198
|
+
node.spanCount,
|
|
2199
|
+
" span",
|
|
2200
|
+
node.spanCount !== 1 ? "s" : "",
|
|
2201
|
+
node.errorCount > 0 && ` · ${node.errorCount} err`
|
|
2202
|
+
]
|
|
2203
|
+
})
|
|
2204
|
+
] }, node.name);
|
|
2205
|
+
})
|
|
2206
|
+
]
|
|
2207
|
+
})
|
|
2208
|
+
});
|
|
2209
|
+
}
|
|
2210
|
+
//#endregion
|
|
2211
|
+
//#region src/components/observability/TraceTimeline/StatisticsView.tsx
|
|
2212
|
+
function computeSelfTime(span) {
|
|
2213
|
+
const childrenTotal = span.children.reduce((sum, child) => sum + child.durationMs, 0);
|
|
2214
|
+
return Math.max(0, span.durationMs - childrenTotal);
|
|
2215
|
+
}
|
|
2216
|
+
function computeStats(trace) {
|
|
2217
|
+
const allFlattened = flattenAllSpans(trace.rootSpans);
|
|
2218
|
+
const groups = /* @__PURE__ */ new Map();
|
|
2219
|
+
for (const { span } of allFlattened) {
|
|
2220
|
+
const key = `${span.serviceName}:${span.name}`;
|
|
2221
|
+
let group = groups.get(key);
|
|
2222
|
+
if (!group) {
|
|
2223
|
+
group = {
|
|
2224
|
+
spans: [],
|
|
2225
|
+
selfTimes: []
|
|
2226
|
+
};
|
|
2227
|
+
groups.set(key, group);
|
|
2228
|
+
}
|
|
2229
|
+
group.spans.push(span);
|
|
2230
|
+
group.selfTimes.push(computeSelfTime(span));
|
|
2231
|
+
}
|
|
2232
|
+
const stats = [];
|
|
2233
|
+
for (const [key, { spans, selfTimes }] of groups) {
|
|
2234
|
+
const durations = spans.map((s) => s.durationMs);
|
|
2235
|
+
const count = spans.length;
|
|
2236
|
+
const totalDuration = durations.reduce((a, b) => a + b, 0);
|
|
2237
|
+
const selfTimeTotal = selfTimes.reduce((a, b) => a + b, 0);
|
|
2238
|
+
const firstSpan = spans[0];
|
|
2239
|
+
if (!firstSpan) continue;
|
|
2240
|
+
stats.push({
|
|
2241
|
+
key,
|
|
2242
|
+
serviceName: firstSpan.serviceName,
|
|
2243
|
+
spanName: firstSpan.name,
|
|
2244
|
+
count,
|
|
2245
|
+
totalDuration,
|
|
2246
|
+
avgDuration: totalDuration / count,
|
|
2247
|
+
minDuration: Math.min(...durations),
|
|
2248
|
+
maxDuration: Math.max(...durations),
|
|
2249
|
+
selfTimeTotal,
|
|
2250
|
+
selfTimeAvg: selfTimeTotal / count,
|
|
2251
|
+
selfTimeMin: Math.min(...selfTimes),
|
|
2252
|
+
selfTimeMax: Math.max(...selfTimes)
|
|
2253
|
+
});
|
|
2254
|
+
}
|
|
2255
|
+
return stats;
|
|
2256
|
+
}
|
|
2257
|
+
function getSortValue(stat, field) {
|
|
2258
|
+
switch (field) {
|
|
2259
|
+
case "name": return stat.key.toLowerCase();
|
|
2260
|
+
case "count": return stat.count;
|
|
2261
|
+
case "total": return stat.totalDuration;
|
|
2262
|
+
case "avg": return stat.avgDuration;
|
|
2263
|
+
case "min": return stat.minDuration;
|
|
2264
|
+
case "max": return stat.maxDuration;
|
|
2265
|
+
case "selfTotal": return stat.selfTimeTotal;
|
|
2266
|
+
case "selfAvg": return stat.selfTimeAvg;
|
|
2267
|
+
case "selfMin": return stat.selfTimeMin;
|
|
2268
|
+
case "selfMax": return stat.selfTimeMax;
|
|
2269
|
+
}
|
|
2270
|
+
}
|
|
2271
|
+
const COLUMNS = [
|
|
2272
|
+
{
|
|
2273
|
+
label: "Name",
|
|
2274
|
+
field: "name"
|
|
2275
|
+
},
|
|
2276
|
+
{
|
|
2277
|
+
label: "Count",
|
|
2278
|
+
field: "count"
|
|
2279
|
+
},
|
|
2280
|
+
{
|
|
2281
|
+
label: "Total",
|
|
2282
|
+
field: "total"
|
|
2283
|
+
},
|
|
2284
|
+
{
|
|
2285
|
+
label: "Avg",
|
|
2286
|
+
field: "avg"
|
|
2287
|
+
},
|
|
2288
|
+
{
|
|
2289
|
+
label: "Min",
|
|
2290
|
+
field: "min"
|
|
2291
|
+
},
|
|
2292
|
+
{
|
|
2293
|
+
label: "Max",
|
|
2294
|
+
field: "max"
|
|
2295
|
+
},
|
|
2296
|
+
{
|
|
2297
|
+
label: "ST Total",
|
|
2298
|
+
field: "selfTotal"
|
|
2299
|
+
},
|
|
2300
|
+
{
|
|
2301
|
+
label: "ST Avg",
|
|
2302
|
+
field: "selfAvg"
|
|
2303
|
+
},
|
|
2304
|
+
{
|
|
2305
|
+
label: "ST Min",
|
|
2306
|
+
field: "selfMin"
|
|
2307
|
+
},
|
|
2308
|
+
{
|
|
2309
|
+
label: "ST Max",
|
|
2310
|
+
field: "selfMax"
|
|
2311
|
+
}
|
|
2312
|
+
];
|
|
2313
|
+
function StatisticsView({ trace }) {
|
|
2314
|
+
const [sortField, setSortField] = useState("total");
|
|
2315
|
+
const [sortAsc, setSortAsc] = useState(false);
|
|
2316
|
+
const stats = useMemo(() => computeStats(trace), [trace]);
|
|
2317
|
+
const sorted = useMemo(() => {
|
|
2318
|
+
const copy = [...stats];
|
|
2319
|
+
copy.sort((a, b) => {
|
|
2320
|
+
const aVal = getSortValue(a, sortField);
|
|
2321
|
+
const bVal = getSortValue(b, sortField);
|
|
2322
|
+
let cmp;
|
|
2323
|
+
if (typeof aVal === "string" && typeof bVal === "string") cmp = aVal.localeCompare(bVal);
|
|
2324
|
+
else if (typeof aVal === "number" && typeof bVal === "number") cmp = aVal - bVal;
|
|
2325
|
+
else cmp = 0;
|
|
2326
|
+
return sortAsc ? cmp : -cmp;
|
|
2327
|
+
});
|
|
2328
|
+
return copy;
|
|
2329
|
+
}, [
|
|
2330
|
+
stats,
|
|
2331
|
+
sortField,
|
|
2332
|
+
sortAsc
|
|
2333
|
+
]);
|
|
2334
|
+
const handleSort = (field) => {
|
|
2335
|
+
if (sortField === field) setSortAsc((p) => !p);
|
|
2336
|
+
else {
|
|
2337
|
+
setSortField(field);
|
|
2338
|
+
setSortAsc(false);
|
|
2339
|
+
}
|
|
2340
|
+
};
|
|
2341
|
+
return /* @__PURE__ */ jsx("div", {
|
|
2342
|
+
className: "flex-1 overflow-auto p-2",
|
|
2343
|
+
children: /* @__PURE__ */ jsxs("table", {
|
|
2344
|
+
className: "w-full text-sm border-collapse",
|
|
2345
|
+
children: [/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsx("tr", {
|
|
2346
|
+
className: "border-b border-border",
|
|
2347
|
+
children: COLUMNS.map((col) => /* @__PURE__ */ jsxs("th", {
|
|
2348
|
+
className: "px-3 py-2 text-left text-xs font-medium text-muted-foreground cursor-pointer select-none hover:text-foreground whitespace-nowrap",
|
|
2349
|
+
onClick: () => handleSort(col.field),
|
|
2350
|
+
children: [
|
|
2351
|
+
col.label,
|
|
2352
|
+
" ",
|
|
2353
|
+
sortField === col.field ? sortAsc ? "▲" : "▼" : ""
|
|
2354
|
+
]
|
|
2355
|
+
}, col.field))
|
|
2356
|
+
}) }), /* @__PURE__ */ jsx("tbody", { children: sorted.map((stat, i) => /* @__PURE__ */ jsxs("tr", {
|
|
2357
|
+
className: `border-b border-border/50 ${i % 2 === 0 ? "bg-background" : "bg-muted/30"}`,
|
|
2358
|
+
children: [
|
|
2359
|
+
/* @__PURE__ */ jsxs("td", {
|
|
2360
|
+
className: "px-3 py-1.5 text-foreground font-mono text-xs whitespace-nowrap",
|
|
2361
|
+
children: [
|
|
2362
|
+
/* @__PURE__ */ jsx("span", {
|
|
2363
|
+
className: "text-muted-foreground",
|
|
2364
|
+
children: stat.serviceName
|
|
2365
|
+
}),
|
|
2366
|
+
/* @__PURE__ */ jsx("span", {
|
|
2367
|
+
className: "text-muted-foreground/50",
|
|
2368
|
+
children: ":"
|
|
2369
|
+
}),
|
|
2370
|
+
" ",
|
|
2371
|
+
stat.spanName
|
|
2372
|
+
]
|
|
2373
|
+
}),
|
|
2374
|
+
/* @__PURE__ */ jsx("td", {
|
|
2375
|
+
className: "px-3 py-1.5 text-foreground tabular-nums",
|
|
2376
|
+
children: stat.count
|
|
2377
|
+
}),
|
|
2378
|
+
/* @__PURE__ */ jsx("td", {
|
|
2379
|
+
className: "px-3 py-1.5 text-foreground tabular-nums",
|
|
2380
|
+
children: formatDuration(stat.totalDuration)
|
|
2381
|
+
}),
|
|
2382
|
+
/* @__PURE__ */ jsx("td", {
|
|
2383
|
+
className: "px-3 py-1.5 text-foreground tabular-nums",
|
|
2384
|
+
children: formatDuration(stat.avgDuration)
|
|
2385
|
+
}),
|
|
2386
|
+
/* @__PURE__ */ jsx("td", {
|
|
2387
|
+
className: "px-3 py-1.5 text-foreground tabular-nums",
|
|
2388
|
+
children: formatDuration(stat.minDuration)
|
|
2389
|
+
}),
|
|
2390
|
+
/* @__PURE__ */ jsx("td", {
|
|
2391
|
+
className: "px-3 py-1.5 text-foreground tabular-nums",
|
|
2392
|
+
children: formatDuration(stat.maxDuration)
|
|
2393
|
+
}),
|
|
2394
|
+
/* @__PURE__ */ jsx("td", {
|
|
2395
|
+
className: "px-3 py-1.5 text-foreground tabular-nums",
|
|
2396
|
+
children: formatDuration(stat.selfTimeTotal)
|
|
2397
|
+
}),
|
|
2398
|
+
/* @__PURE__ */ jsx("td", {
|
|
2399
|
+
className: "px-3 py-1.5 text-foreground tabular-nums",
|
|
2400
|
+
children: formatDuration(stat.selfTimeAvg)
|
|
2401
|
+
}),
|
|
2402
|
+
/* @__PURE__ */ jsx("td", {
|
|
2403
|
+
className: "px-3 py-1.5 text-foreground tabular-nums",
|
|
2404
|
+
children: formatDuration(stat.selfTimeMin)
|
|
2405
|
+
}),
|
|
2406
|
+
/* @__PURE__ */ jsx("td", {
|
|
2407
|
+
className: "px-3 py-1.5 text-foreground tabular-nums",
|
|
2408
|
+
children: formatDuration(stat.selfTimeMax)
|
|
2409
|
+
})
|
|
2410
|
+
]
|
|
2411
|
+
}, stat.key)) })]
|
|
2412
|
+
})
|
|
2413
|
+
});
|
|
2414
|
+
}
|
|
2415
|
+
//#endregion
|
|
2416
|
+
//#region src/components/observability/TraceTimeline/FlamegraphView.tsx
|
|
2417
|
+
const ROW_HEIGHT = 24;
|
|
2418
|
+
const MIN_WIDTH = 1;
|
|
2419
|
+
const LABEL_MIN_WIDTH = 40;
|
|
2420
|
+
function findSpanById(rootSpans, spanId) {
|
|
2421
|
+
for (const root of rootSpans) {
|
|
2422
|
+
if (root.spanId === spanId) return root;
|
|
2423
|
+
const found = findSpanById(root.children, spanId);
|
|
2424
|
+
if (found) return found;
|
|
2425
|
+
}
|
|
2426
|
+
return null;
|
|
2427
|
+
}
|
|
2428
|
+
function getAncestorPath(rootSpans, targetId) {
|
|
2429
|
+
const path = [];
|
|
2430
|
+
function walk(span, ancestors) {
|
|
2431
|
+
if (span.spanId === targetId) {
|
|
2432
|
+
path.push(...ancestors, span);
|
|
2433
|
+
return true;
|
|
2434
|
+
}
|
|
2435
|
+
for (const child of span.children) if (walk(child, [...ancestors, span])) return true;
|
|
2436
|
+
return false;
|
|
2437
|
+
}
|
|
2438
|
+
for (const root of rootSpans) if (walk(root, [])) break;
|
|
2439
|
+
return path;
|
|
2440
|
+
}
|
|
2441
|
+
function FlamegraphView({ trace, onSpanClick, selectedSpanId }) {
|
|
2442
|
+
const [zoomSpanId, setZoomSpanId] = useState(null);
|
|
2443
|
+
const [tooltip, setTooltip] = useState(null);
|
|
2444
|
+
const zoomRoot = useMemo(() => {
|
|
2445
|
+
if (!zoomSpanId) return null;
|
|
2446
|
+
return findSpanById(trace.rootSpans, zoomSpanId);
|
|
2447
|
+
}, [trace.rootSpans, zoomSpanId]);
|
|
2448
|
+
const breadcrumbs = useMemo(() => {
|
|
2449
|
+
if (!zoomSpanId) return [];
|
|
2450
|
+
return getAncestorPath(trace.rootSpans, zoomSpanId);
|
|
2451
|
+
}, [trace.rootSpans, zoomSpanId]);
|
|
2452
|
+
const viewRoots = zoomRoot ? [zoomRoot] : trace.rootSpans;
|
|
2453
|
+
const viewMinTime = zoomRoot ? zoomRoot.startTimeUnixMs : trace.minTimeMs;
|
|
2454
|
+
const viewDuration = (zoomRoot ? zoomRoot.endTimeUnixMs : trace.maxTimeMs) - viewMinTime;
|
|
2455
|
+
const flatSpans = useMemo(() => flattenAllSpans(viewRoots).map((fs) => ({
|
|
2456
|
+
span: fs.span,
|
|
2457
|
+
depth: fs.level
|
|
2458
|
+
})), [viewRoots]);
|
|
2459
|
+
const maxDepth = useMemo(() => flatSpans.reduce((max, fs) => Math.max(max, fs.depth), 0) + 1, [flatSpans]);
|
|
2460
|
+
const svgWidth = 1200;
|
|
2461
|
+
const svgHeight = maxDepth * ROW_HEIGHT;
|
|
2462
|
+
const handleClick = useCallback((span) => {
|
|
2463
|
+
onSpanClick?.(span);
|
|
2464
|
+
setZoomSpanId(span.spanId);
|
|
2465
|
+
}, [onSpanClick]);
|
|
2466
|
+
const handleZoomOut = useCallback((spanId) => {
|
|
2467
|
+
setZoomSpanId(spanId);
|
|
2468
|
+
}, []);
|
|
2469
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
2470
|
+
className: "flex-1 overflow-auto p-2",
|
|
2471
|
+
children: [
|
|
2472
|
+
breadcrumbs.length > 0 && /* @__PURE__ */ jsxs("div", {
|
|
2473
|
+
className: "flex items-center gap-1 text-xs text-muted-foreground mb-2 flex-wrap",
|
|
2474
|
+
children: [/* @__PURE__ */ jsx("button", {
|
|
2475
|
+
className: "hover:text-foreground underline",
|
|
2476
|
+
onClick: () => handleZoomOut(null),
|
|
2477
|
+
children: "root"
|
|
2478
|
+
}), breadcrumbs.map((bc, i) => /* @__PURE__ */ jsxs("span", {
|
|
2479
|
+
className: "flex items-center gap-1",
|
|
2480
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
2481
|
+
className: "text-muted-foreground/50",
|
|
2482
|
+
children: ">"
|
|
2483
|
+
}), i < breadcrumbs.length - 1 ? /* @__PURE__ */ jsxs("button", {
|
|
2484
|
+
className: "hover:text-foreground underline",
|
|
2485
|
+
onClick: () => handleZoomOut(bc.spanId),
|
|
2486
|
+
children: [
|
|
2487
|
+
bc.serviceName,
|
|
2488
|
+
": ",
|
|
2489
|
+
bc.name
|
|
2490
|
+
]
|
|
2491
|
+
}) : /* @__PURE__ */ jsxs("span", {
|
|
2492
|
+
className: "text-foreground",
|
|
2493
|
+
children: [
|
|
2494
|
+
bc.serviceName,
|
|
2495
|
+
": ",
|
|
2496
|
+
bc.name
|
|
2497
|
+
]
|
|
2498
|
+
})]
|
|
2499
|
+
}, bc.spanId))]
|
|
2500
|
+
}),
|
|
2501
|
+
/* @__PURE__ */ jsx("div", {
|
|
2502
|
+
className: "overflow-x-auto",
|
|
2503
|
+
children: /* @__PURE__ */ jsx("svg", {
|
|
2504
|
+
width: svgWidth,
|
|
2505
|
+
height: svgHeight,
|
|
2506
|
+
className: "block",
|
|
2507
|
+
onMouseLeave: () => setTooltip(null),
|
|
2508
|
+
children: flatSpans.map(({ span, depth }) => {
|
|
2509
|
+
const x = viewDuration > 0 ? (span.startTimeUnixMs - viewMinTime) / viewDuration * svgWidth : 0;
|
|
2510
|
+
const w = viewDuration > 0 ? Math.max(MIN_WIDTH, span.durationMs / viewDuration * svgWidth) : svgWidth;
|
|
2511
|
+
const y = depth * ROW_HEIGHT;
|
|
2512
|
+
const color = getServiceColor(span.serviceName);
|
|
2513
|
+
const isSelected = span.spanId === selectedSpanId;
|
|
2514
|
+
const showLabel = w >= LABEL_MIN_WIDTH;
|
|
2515
|
+
const label = `${span.serviceName}: ${span.name}`;
|
|
2516
|
+
return /* @__PURE__ */ jsxs("g", {
|
|
2517
|
+
className: "cursor-pointer",
|
|
2518
|
+
onClick: () => handleClick(span),
|
|
2519
|
+
onMouseEnter: (e) => setTooltip({
|
|
2520
|
+
span,
|
|
2521
|
+
x: e.clientX,
|
|
2522
|
+
y: e.clientY
|
|
2523
|
+
}),
|
|
2524
|
+
onMouseMove: (e) => setTooltip((prev) => prev ? {
|
|
2525
|
+
...prev,
|
|
2526
|
+
x: e.clientX,
|
|
2527
|
+
y: e.clientY
|
|
2528
|
+
} : null),
|
|
2529
|
+
onMouseLeave: () => setTooltip(null),
|
|
2530
|
+
children: [/* @__PURE__ */ jsx("rect", {
|
|
2531
|
+
x,
|
|
2532
|
+
y,
|
|
2533
|
+
width: w,
|
|
2534
|
+
height: ROW_HEIGHT - 1,
|
|
2535
|
+
fill: color,
|
|
2536
|
+
opacity: .85,
|
|
2537
|
+
rx: 2,
|
|
2538
|
+
stroke: isSelected ? "#ffffff" : "transparent",
|
|
2539
|
+
strokeWidth: isSelected ? 2 : 0,
|
|
2540
|
+
className: "hover:opacity-100"
|
|
2541
|
+
}), showLabel && /* @__PURE__ */ jsx("text", {
|
|
2542
|
+
x: x + 4,
|
|
2543
|
+
y: y + ROW_HEIGHT / 2 + 1,
|
|
2544
|
+
dominantBaseline: "middle",
|
|
2545
|
+
fill: "#ffffff",
|
|
2546
|
+
fontSize: 11,
|
|
2547
|
+
fontFamily: "monospace",
|
|
2548
|
+
clipPath: `inset(0 0 0 0)`,
|
|
2549
|
+
children: /* @__PURE__ */ jsx("tspan", { children: label.length > w / 7 ? label.slice(0, Math.floor(w / 7) - 1) + "…" : label })
|
|
2550
|
+
})]
|
|
2551
|
+
}, span.spanId);
|
|
2552
|
+
})
|
|
2553
|
+
})
|
|
2554
|
+
}),
|
|
2555
|
+
tooltip && /* @__PURE__ */ jsxs("div", {
|
|
2556
|
+
className: "fixed z-50 pointer-events-none bg-popover border border-border rounded px-3 py-2 text-xs shadow-lg",
|
|
2557
|
+
style: {
|
|
2558
|
+
left: tooltip.x + 12,
|
|
2559
|
+
top: tooltip.y + 12
|
|
2560
|
+
},
|
|
2561
|
+
children: [
|
|
2562
|
+
/* @__PURE__ */ jsx("div", {
|
|
2563
|
+
className: "font-medium text-foreground",
|
|
2564
|
+
children: tooltip.span.name
|
|
2565
|
+
}),
|
|
2566
|
+
/* @__PURE__ */ jsx("div", {
|
|
2567
|
+
className: "text-muted-foreground",
|
|
2568
|
+
children: tooltip.span.serviceName
|
|
2569
|
+
}),
|
|
2570
|
+
/* @__PURE__ */ jsx("div", {
|
|
2571
|
+
className: "text-foreground mt-1",
|
|
2572
|
+
children: formatDuration(tooltip.span.durationMs)
|
|
2573
|
+
})
|
|
2574
|
+
]
|
|
2575
|
+
})
|
|
2576
|
+
]
|
|
2577
|
+
});
|
|
2578
|
+
}
|
|
2579
|
+
//#endregion
|
|
2580
|
+
//#region src/components/observability/TraceTimeline/Minimap.tsx
|
|
2581
|
+
/**
|
|
2582
|
+
* Minimap - Compressed overview of all spans with a draggable viewport.
|
|
2583
|
+
*/
|
|
2584
|
+
const MINIMAP_HEIGHT = 40;
|
|
2585
|
+
const SPAN_HEIGHT = 2;
|
|
2586
|
+
const SPAN_GAP = 1;
|
|
2587
|
+
const MIN_VIEWPORT_WIDTH = .02;
|
|
2588
|
+
const HANDLE_WIDTH = 6;
|
|
2589
|
+
function Minimap({ trace, viewStart, viewEnd, onViewChange }) {
|
|
2590
|
+
const containerRef = useRef(null);
|
|
2591
|
+
const dragRef = useRef(null);
|
|
2592
|
+
const cleanupRef = useRef(null);
|
|
2593
|
+
useEffect(() => {
|
|
2594
|
+
return () => {
|
|
2595
|
+
cleanupRef.current?.();
|
|
2596
|
+
};
|
|
2597
|
+
}, []);
|
|
2598
|
+
const allSpans = useMemo(() => flattenAllSpans(trace.rootSpans), [trace.rootSpans]);
|
|
2599
|
+
const traceDuration = trace.maxTimeMs - trace.minTimeMs;
|
|
2600
|
+
const getFraction = useCallback((clientX) => {
|
|
2601
|
+
const el = containerRef.current;
|
|
2602
|
+
if (!el) return 0;
|
|
2603
|
+
const rect = el.getBoundingClientRect();
|
|
2604
|
+
if (!rect.width) return 0;
|
|
2605
|
+
return Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
|
|
2606
|
+
}, []);
|
|
2607
|
+
const clampView = useCallback((start, end) => {
|
|
2608
|
+
let s = Math.max(0, Math.min(1 - MIN_VIEWPORT_WIDTH, start));
|
|
2609
|
+
let e = Math.max(s + MIN_VIEWPORT_WIDTH, Math.min(1, end));
|
|
2610
|
+
if (e > 1) {
|
|
2611
|
+
e = 1;
|
|
2612
|
+
s = Math.max(0, e - Math.max(MIN_VIEWPORT_WIDTH, end - start));
|
|
2613
|
+
}
|
|
2614
|
+
return [s, e];
|
|
2615
|
+
}, []);
|
|
2616
|
+
const handleMouseDown = useCallback((e, mode) => {
|
|
2617
|
+
e.preventDefault();
|
|
2618
|
+
e.stopPropagation();
|
|
2619
|
+
cleanupRef.current?.();
|
|
2620
|
+
dragRef.current = {
|
|
2621
|
+
mode,
|
|
2622
|
+
startX: e.clientX,
|
|
2623
|
+
origViewStart: viewStart,
|
|
2624
|
+
origViewEnd: viewEnd
|
|
2625
|
+
};
|
|
2626
|
+
const handleMouseMove = (ev) => {
|
|
2627
|
+
const drag = dragRef.current;
|
|
2628
|
+
if (!drag || !containerRef.current) return;
|
|
2629
|
+
const rect = containerRef.current.getBoundingClientRect();
|
|
2630
|
+
if (!rect.width) return;
|
|
2631
|
+
const deltaFrac = (ev.clientX - drag.startX) / rect.width;
|
|
2632
|
+
let newStart;
|
|
2633
|
+
let newEnd;
|
|
2634
|
+
if (drag.mode === "pan") {
|
|
2635
|
+
const width = drag.origViewEnd - drag.origViewStart;
|
|
2636
|
+
newStart = drag.origViewStart + deltaFrac;
|
|
2637
|
+
newEnd = newStart + width;
|
|
2638
|
+
if (newStart < 0) {
|
|
2639
|
+
newStart = 0;
|
|
2640
|
+
newEnd = width;
|
|
2641
|
+
}
|
|
2642
|
+
if (newEnd > 1) {
|
|
2643
|
+
newEnd = 1;
|
|
2644
|
+
newStart = 1 - width;
|
|
2645
|
+
}
|
|
2646
|
+
} else if (drag.mode === "resize-left") {
|
|
2647
|
+
newStart = drag.origViewStart + deltaFrac;
|
|
2648
|
+
newEnd = drag.origViewEnd;
|
|
2649
|
+
} else {
|
|
2650
|
+
newStart = drag.origViewStart;
|
|
2651
|
+
newEnd = drag.origViewEnd + deltaFrac;
|
|
2652
|
+
}
|
|
2653
|
+
const [s, e] = clampView(newStart, newEnd);
|
|
2654
|
+
onViewChange(s, e);
|
|
2655
|
+
};
|
|
2656
|
+
const handleMouseUp = () => {
|
|
2657
|
+
dragRef.current = null;
|
|
2658
|
+
cleanupRef.current = null;
|
|
2659
|
+
window.removeEventListener("mousemove", handleMouseMove);
|
|
2660
|
+
window.removeEventListener("mouseup", handleMouseUp);
|
|
2661
|
+
};
|
|
2662
|
+
window.addEventListener("mousemove", handleMouseMove);
|
|
2663
|
+
window.addEventListener("mouseup", handleMouseUp);
|
|
2664
|
+
cleanupRef.current = handleMouseUp;
|
|
2665
|
+
}, [
|
|
2666
|
+
viewStart,
|
|
2667
|
+
viewEnd,
|
|
2668
|
+
onViewChange,
|
|
2669
|
+
clampView
|
|
2670
|
+
]);
|
|
2671
|
+
const handleBackgroundClick = useCallback((e) => {
|
|
2672
|
+
if (dragRef.current) return;
|
|
2673
|
+
if (e.target !== e.currentTarget) return;
|
|
2674
|
+
const frac = getFraction(e.clientX);
|
|
2675
|
+
const half = (viewEnd - viewStart) / 2;
|
|
2676
|
+
const [s, eVal] = clampView(frac - half, frac + half);
|
|
2677
|
+
onViewChange(s, eVal);
|
|
2678
|
+
}, [
|
|
2679
|
+
viewStart,
|
|
2680
|
+
viewEnd,
|
|
2681
|
+
onViewChange,
|
|
2682
|
+
getFraction,
|
|
2683
|
+
clampView
|
|
2684
|
+
]);
|
|
2685
|
+
const handleKeyDown = useCallback((e) => {
|
|
2686
|
+
const step = .05;
|
|
2687
|
+
const width = viewEnd - viewStart;
|
|
2688
|
+
let newStart;
|
|
2689
|
+
if (e.key === "ArrowLeft" || e.key === "ArrowUp") newStart = viewStart - step;
|
|
2690
|
+
else if (e.key === "ArrowRight" || e.key === "ArrowDown") newStart = viewStart + step;
|
|
2691
|
+
else return;
|
|
2692
|
+
e.preventDefault();
|
|
2693
|
+
const [s, eVal] = clampView(newStart, newStart + width);
|
|
2694
|
+
onViewChange(s, eVal);
|
|
2695
|
+
}, [
|
|
2696
|
+
viewStart,
|
|
2697
|
+
viewEnd,
|
|
2698
|
+
onViewChange,
|
|
2699
|
+
clampView
|
|
2700
|
+
]);
|
|
2701
|
+
const viewStartPct = viewStart * 100;
|
|
2702
|
+
const viewEndPct = viewEnd * 100;
|
|
2703
|
+
const viewWidthPct = viewEndPct - viewStartPct;
|
|
2704
|
+
const totalRows = allSpans.length;
|
|
2705
|
+
const availableHeight = MINIMAP_HEIGHT - 4;
|
|
2706
|
+
const rowHeight = totalRows > 0 ? Math.min(SPAN_HEIGHT + SPAN_GAP, availableHeight / totalRows) : SPAN_HEIGHT;
|
|
2707
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
2708
|
+
ref: containerRef,
|
|
2709
|
+
className: "relative w-full border-b border-border bg-muted/30 select-none",
|
|
2710
|
+
style: { height: MINIMAP_HEIGHT },
|
|
2711
|
+
onClick: handleBackgroundClick,
|
|
2712
|
+
onKeyDown: handleKeyDown,
|
|
2713
|
+
role: "slider",
|
|
2714
|
+
tabIndex: 0,
|
|
2715
|
+
"aria-label": "Trace minimap viewport",
|
|
2716
|
+
"aria-valuemin": 0,
|
|
2717
|
+
"aria-valuemax": 100,
|
|
2718
|
+
"aria-valuenow": Math.round(viewStartPct),
|
|
2719
|
+
children: [
|
|
2720
|
+
traceDuration > 0 && allSpans.map(({ span }, i) => {
|
|
2721
|
+
const left = (span.startTimeUnixMs - trace.minTimeMs) / traceDuration * 100;
|
|
2722
|
+
const width = Math.max(.2, span.durationMs / traceDuration * 100);
|
|
2723
|
+
const color = getSpanBarColor(span.serviceName, span.status === "ERROR");
|
|
2724
|
+
return /* @__PURE__ */ jsx("div", {
|
|
2725
|
+
className: "absolute pointer-events-none",
|
|
2726
|
+
style: {
|
|
2727
|
+
left: `${left}%`,
|
|
2728
|
+
width: `${width}%`,
|
|
2729
|
+
top: 2 + i * rowHeight,
|
|
2730
|
+
height: Math.max(1, rowHeight - SPAN_GAP),
|
|
2731
|
+
backgroundColor: color,
|
|
2732
|
+
opacity: .8,
|
|
2733
|
+
borderRadius: 1
|
|
2734
|
+
}
|
|
2735
|
+
}, span.spanId);
|
|
2736
|
+
}),
|
|
2737
|
+
viewStartPct > 0 && /* @__PURE__ */ jsx("div", {
|
|
2738
|
+
className: "absolute top-0 left-0 h-full bg-black/30 pointer-events-none",
|
|
2739
|
+
style: { width: `${viewStartPct}%` }
|
|
2740
|
+
}),
|
|
2741
|
+
viewEndPct < 100 && /* @__PURE__ */ jsx("div", {
|
|
2742
|
+
className: "absolute top-0 h-full bg-black/30 pointer-events-none",
|
|
2743
|
+
style: {
|
|
2744
|
+
left: `${viewEndPct}%`,
|
|
2745
|
+
right: 0
|
|
2746
|
+
}
|
|
2747
|
+
}),
|
|
2748
|
+
/* @__PURE__ */ jsxs("div", {
|
|
2749
|
+
className: "absolute top-0 h-full border border-blue-500/50 bg-blue-500/10 cursor-grab active:cursor-grabbing",
|
|
2750
|
+
style: {
|
|
2751
|
+
left: `${viewStartPct}%`,
|
|
2752
|
+
width: `${viewWidthPct}%`
|
|
2753
|
+
},
|
|
2754
|
+
onMouseDown: (e) => handleMouseDown(e, "pan"),
|
|
2755
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
2756
|
+
className: "absolute top-0 left-0 h-full cursor-ew-resize z-10",
|
|
2757
|
+
style: {
|
|
2758
|
+
width: HANDLE_WIDTH,
|
|
2759
|
+
marginLeft: -HANDLE_WIDTH / 2
|
|
2760
|
+
},
|
|
2761
|
+
onMouseDown: (e) => handleMouseDown(e, "resize-left")
|
|
2762
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
2763
|
+
className: "absolute top-0 right-0 h-full cursor-ew-resize z-10",
|
|
2764
|
+
style: {
|
|
2765
|
+
width: HANDLE_WIDTH,
|
|
2766
|
+
marginRight: -HANDLE_WIDTH / 2
|
|
2767
|
+
},
|
|
2768
|
+
onMouseDown: (e) => handleMouseDown(e, "resize-right")
|
|
2769
|
+
})]
|
|
2770
|
+
})
|
|
2771
|
+
]
|
|
2772
|
+
});
|
|
2773
|
+
}
|
|
1845
2774
|
//#endregion
|
|
1846
2775
|
//#region src/components/observability/TraceTimeline/index.tsx
|
|
1847
2776
|
/**
|
|
@@ -1930,25 +2859,43 @@ function isSpanAncestorOf(potentialAncestor, descendantId, flattenedSpans) {
|
|
|
1930
2859
|
}
|
|
1931
2860
|
return false;
|
|
1932
2861
|
}
|
|
1933
|
-
function
|
|
2862
|
+
function collectServices(rootSpans) {
|
|
2863
|
+
const set = /* @__PURE__ */ new Set();
|
|
2864
|
+
function walk(span) {
|
|
2865
|
+
set.add(span.serviceName);
|
|
2866
|
+
span.children.forEach(walk);
|
|
2867
|
+
}
|
|
2868
|
+
rootSpans.forEach(walk);
|
|
2869
|
+
return Array.from(set).sort();
|
|
2870
|
+
}
|
|
2871
|
+
function TraceTimeline({ rows, onSpanClick, onSpanDeselect, selectedSpanId: externalSelectedSpanId, isLoading, error, view: externalView, onViewChange, uiFind: externalUiFind, onUiFindChange, viewStart: externalViewStart, viewEnd: externalViewEnd, onViewRangeChange }) {
|
|
1934
2872
|
useRegisterShortcuts("trace-viewer", TRACE_VIEWER_SHORTCUTS);
|
|
1935
2873
|
const [collapsedIds, setCollapsedIds] = useState(/* @__PURE__ */ new Set());
|
|
1936
2874
|
const [internalSelectedSpanId, setInternalSelectedSpanId] = useState(null);
|
|
1937
2875
|
const [hoveredSpanId, setHoveredSpanId] = useState(null);
|
|
2876
|
+
const [internalView, setInternalView] = useState("timeline");
|
|
2877
|
+
const [internalUiFind, setInternalUiFind] = useState("");
|
|
2878
|
+
const [currentMatchIndex, setCurrentMatchIndex] = useState(0);
|
|
2879
|
+
const [headerCollapsed, setHeaderCollapsed] = useState(false);
|
|
2880
|
+
const [internalViewStart, setInternalViewStart] = useState(0);
|
|
2881
|
+
const [internalViewEnd, setInternalViewEnd] = useState(1);
|
|
1938
2882
|
const selectedSpanId = externalSelectedSpanId ?? internalSelectedSpanId;
|
|
2883
|
+
const viewStart = externalViewStart ?? internalViewStart;
|
|
2884
|
+
const viewEnd = externalViewEnd ?? internalViewEnd;
|
|
2885
|
+
const activeView = externalView ?? internalView;
|
|
2886
|
+
const uiFind = externalUiFind ?? internalUiFind;
|
|
1939
2887
|
const scrollRef = useRef(null);
|
|
1940
2888
|
const announcementRef = useRef(null);
|
|
1941
2889
|
const parsedTrace = useMemo(() => buildTrace(rows), [rows]);
|
|
2890
|
+
const services = useMemo(() => parsedTrace ? collectServices(parsedTrace.rootSpans) : [], [parsedTrace]);
|
|
1942
2891
|
const flattenedSpans = useMemo(() => {
|
|
1943
2892
|
if (!parsedTrace) return [];
|
|
1944
2893
|
return flattenTree(parsedTrace.rootSpans, collapsedIds);
|
|
1945
2894
|
}, [parsedTrace, collapsedIds]);
|
|
1946
|
-
const
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
overscan: 5
|
|
1951
|
-
});
|
|
2895
|
+
const matchingIndices = useMemo(() => {
|
|
2896
|
+
if (!uiFind) return [];
|
|
2897
|
+
return flattenedSpans.map((item, idx) => spanMatchesSearch(item.span, uiFind) ? idx : -1).filter((idx) => idx !== -1);
|
|
2898
|
+
}, [flattenedSpans, uiFind]);
|
|
1952
2899
|
const handleToggleCollapse = (spanId) => {
|
|
1953
2900
|
setCollapsedIds((prev) => {
|
|
1954
2901
|
const next = new Set(prev);
|
|
@@ -1957,11 +2904,22 @@ function TraceTimeline({ rows, onSpanClick, selectedSpanId: externalSelectedSpan
|
|
|
1957
2904
|
return next;
|
|
1958
2905
|
});
|
|
1959
2906
|
};
|
|
2907
|
+
const handleDeselect = useCallback(() => {
|
|
2908
|
+
setInternalSelectedSpanId(null);
|
|
2909
|
+
onSpanDeselect?.();
|
|
2910
|
+
}, [onSpanDeselect]);
|
|
1960
2911
|
const handleSpanClick = useCallback((span) => {
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
2912
|
+
if (selectedSpanId === span.spanId) handleDeselect();
|
|
2913
|
+
else {
|
|
2914
|
+
setInternalSelectedSpanId(span.spanId);
|
|
2915
|
+
onSpanClick?.(span);
|
|
2916
|
+
if (announcementRef.current) announcementRef.current.textContent = `Selected span: ${span.name}, duration: ${formatDuration(span.durationMs)}`;
|
|
2917
|
+
}
|
|
2918
|
+
}, [
|
|
2919
|
+
onSpanClick,
|
|
2920
|
+
selectedSpanId,
|
|
2921
|
+
handleDeselect
|
|
2922
|
+
]);
|
|
1965
2923
|
const handleExpandAll = useCallback(() => {
|
|
1966
2924
|
setCollapsedIds(/* @__PURE__ */ new Set());
|
|
1967
2925
|
}, []);
|
|
@@ -2007,26 +2965,80 @@ function TraceTimeline({ rows, onSpanClick, selectedSpanId: externalSelectedSpan
|
|
|
2007
2965
|
else setCollapsedIds((prev) => {
|
|
2008
2966
|
const next = new Set(prev);
|
|
2009
2967
|
next.delete(selectedItem.span.spanId);
|
|
2010
|
-
return next;
|
|
2011
|
-
});
|
|
2012
|
-
}, [selectedSpanId, flattenedSpans]);
|
|
2013
|
-
const
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2968
|
+
return next;
|
|
2969
|
+
});
|
|
2970
|
+
}, [selectedSpanId, flattenedSpans]);
|
|
2971
|
+
const handleViewChange = useCallback((view) => {
|
|
2972
|
+
if (onViewChange) onViewChange(view);
|
|
2973
|
+
else setInternalView(view);
|
|
2974
|
+
}, [onViewChange]);
|
|
2975
|
+
const handleUiFindChange = useCallback((value) => {
|
|
2976
|
+
if (onUiFindChange) onUiFindChange(value);
|
|
2977
|
+
else setInternalUiFind(value);
|
|
2978
|
+
setCurrentMatchIndex(0);
|
|
2979
|
+
}, [onUiFindChange]);
|
|
2980
|
+
const handleViewRangeChange = useCallback((start, end) => {
|
|
2981
|
+
if (onViewRangeChange) onViewRangeChange(start, end);
|
|
2982
|
+
else {
|
|
2983
|
+
setInternalViewStart(start);
|
|
2984
|
+
setInternalViewEnd(end);
|
|
2985
|
+
}
|
|
2986
|
+
}, [onViewRangeChange]);
|
|
2987
|
+
const scrollToSpan = useCallback((spanId) => {
|
|
2988
|
+
(scrollRef.current?.querySelector(`[data-span-id="${spanId}"]`))?.scrollIntoView({
|
|
2989
|
+
block: "center",
|
|
2021
2990
|
behavior: "smooth"
|
|
2022
2991
|
});
|
|
2992
|
+
}, []);
|
|
2993
|
+
const handleSearchNext = useCallback(() => {
|
|
2994
|
+
if (matchingIndices.length === 0) return;
|
|
2995
|
+
const next = (currentMatchIndex + 1) % matchingIndices.length;
|
|
2996
|
+
setCurrentMatchIndex(next);
|
|
2997
|
+
const idx = matchingIndices[next];
|
|
2998
|
+
if (idx !== void 0) {
|
|
2999
|
+
const item = flattenedSpans[idx];
|
|
3000
|
+
if (item) {
|
|
3001
|
+
handleSpanClick(item.span);
|
|
3002
|
+
scrollToSpan(item.span.spanId);
|
|
3003
|
+
}
|
|
3004
|
+
}
|
|
2023
3005
|
}, [
|
|
2024
|
-
|
|
3006
|
+
matchingIndices,
|
|
3007
|
+
currentMatchIndex,
|
|
2025
3008
|
flattenedSpans,
|
|
2026
|
-
|
|
3009
|
+
handleSpanClick,
|
|
3010
|
+
scrollToSpan
|
|
3011
|
+
]);
|
|
3012
|
+
const handleSearchPrev = useCallback(() => {
|
|
3013
|
+
if (matchingIndices.length === 0) return;
|
|
3014
|
+
const prev = (currentMatchIndex - 1 + matchingIndices.length) % matchingIndices.length;
|
|
3015
|
+
setCurrentMatchIndex(prev);
|
|
3016
|
+
const idx = matchingIndices[prev];
|
|
3017
|
+
if (idx !== void 0) {
|
|
3018
|
+
const item = flattenedSpans[idx];
|
|
3019
|
+
if (item) {
|
|
3020
|
+
handleSpanClick(item.span);
|
|
3021
|
+
scrollToSpan(item.span.spanId);
|
|
3022
|
+
}
|
|
3023
|
+
}
|
|
3024
|
+
}, [
|
|
3025
|
+
matchingIndices,
|
|
3026
|
+
currentMatchIndex,
|
|
3027
|
+
flattenedSpans,
|
|
3028
|
+
handleSpanClick,
|
|
3029
|
+
scrollToSpan
|
|
2027
3030
|
]);
|
|
3031
|
+
useEffect(() => {
|
|
3032
|
+
if (!selectedSpanId) return;
|
|
3033
|
+
scrollToSpan(selectedSpanId);
|
|
3034
|
+
}, [selectedSpanId, scrollToSpan]);
|
|
2028
3035
|
useEffect(() => {
|
|
2029
3036
|
const handleKeyDown = (e) => {
|
|
3037
|
+
if (e.key === "Escape" && selectedSpanId) {
|
|
3038
|
+
e.preventDefault();
|
|
3039
|
+
handleDeselect();
|
|
3040
|
+
return;
|
|
3041
|
+
}
|
|
2030
3042
|
if (!(scrollRef.current?.parentElement)?.contains(document.activeElement)) return;
|
|
2031
3043
|
switch (e.key) {
|
|
2032
3044
|
case "ArrowUp":
|
|
@@ -2049,10 +3061,7 @@ function TraceTimeline({ rows, onSpanClick, selectedSpanId: externalSelectedSpan
|
|
|
2049
3061
|
e.preventDefault();
|
|
2050
3062
|
handleCollapseExpand(false);
|
|
2051
3063
|
break;
|
|
2052
|
-
case "Escape":
|
|
2053
|
-
e.preventDefault();
|
|
2054
|
-
handleDeselect();
|
|
2055
|
-
break;
|
|
3064
|
+
case "Escape": break;
|
|
2056
3065
|
case "Enter":
|
|
2057
3066
|
if (selectedSpanId) {
|
|
2058
3067
|
e.preventDefault();
|
|
@@ -2120,10 +3129,9 @@ function TraceTimeline({ rows, onSpanClick, selectedSpanId: externalSelectedSpan
|
|
|
2120
3129
|
})
|
|
2121
3130
|
});
|
|
2122
3131
|
const totalDurationMs = parsedTrace.maxTimeMs - parsedTrace.minTimeMs;
|
|
2123
|
-
|
|
2124
|
-
return /* @__PURE__ */ jsxs("div", {
|
|
3132
|
+
return /* @__PURE__ */ jsx("div", {
|
|
2125
3133
|
className: "flex h-full bg-background",
|
|
2126
|
-
children:
|
|
3134
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
2127
3135
|
className: "flex flex-col flex-1 min-w-0",
|
|
2128
3136
|
children: [
|
|
2129
3137
|
/* @__PURE__ */ jsx("div", {
|
|
@@ -2133,39 +3141,58 @@ function TraceTimeline({ rows, onSpanClick, selectedSpanId: externalSelectedSpan
|
|
|
2133
3141
|
"aria-live": "polite",
|
|
2134
3142
|
"aria-atomic": "true"
|
|
2135
3143
|
}),
|
|
2136
|
-
/* @__PURE__ */ jsx(TraceHeader, {
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
3144
|
+
/* @__PURE__ */ jsx(TraceHeader, {
|
|
3145
|
+
trace: parsedTrace,
|
|
3146
|
+
services,
|
|
3147
|
+
onHeaderToggle: () => setHeaderCollapsed((p) => !p),
|
|
3148
|
+
isCollapsed: headerCollapsed
|
|
3149
|
+
}),
|
|
3150
|
+
/* @__PURE__ */ jsx(ViewTabs, {
|
|
3151
|
+
activeView,
|
|
3152
|
+
onChange: handleViewChange
|
|
3153
|
+
}),
|
|
3154
|
+
/* @__PURE__ */ jsx(SpanSearch, {
|
|
3155
|
+
value: uiFind,
|
|
3156
|
+
onChange: handleUiFindChange,
|
|
3157
|
+
matchCount: matchingIndices.length,
|
|
3158
|
+
currentMatch: currentMatchIndex,
|
|
3159
|
+
onPrev: handleSearchPrev,
|
|
3160
|
+
onNext: handleSearchNext
|
|
3161
|
+
}),
|
|
3162
|
+
activeView === "graph" ? /* @__PURE__ */ jsx(GraphView, { trace: parsedTrace }) : activeView === "statistics" ? /* @__PURE__ */ jsx(StatisticsView, { trace: parsedTrace }) : activeView === "flamegraph" ? /* @__PURE__ */ jsx(FlamegraphView, {
|
|
3163
|
+
trace: parsedTrace,
|
|
3164
|
+
onSpanClick: handleSpanClick,
|
|
3165
|
+
selectedSpanId: selectedSpanId ?? void 0
|
|
3166
|
+
}) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
3167
|
+
/* @__PURE__ */ jsx(Minimap, {
|
|
3168
|
+
trace: parsedTrace,
|
|
3169
|
+
viewStart,
|
|
3170
|
+
viewEnd,
|
|
3171
|
+
onViewChange: handleViewRangeChange
|
|
3172
|
+
}),
|
|
3173
|
+
/* @__PURE__ */ jsx(TimeRuler, {
|
|
3174
|
+
totalDurationMs: totalDurationMs * (viewEnd - viewStart),
|
|
3175
|
+
leftColumnWidth: "24rem",
|
|
3176
|
+
offsetMs: totalDurationMs * viewStart
|
|
3177
|
+
}),
|
|
3178
|
+
/* @__PURE__ */ jsx("div", {
|
|
3179
|
+
ref: scrollRef,
|
|
3180
|
+
className: "flex-1 overflow-auto outline-none",
|
|
3181
|
+
role: "tree",
|
|
3182
|
+
"aria-label": "Trace timeline",
|
|
3183
|
+
tabIndex: 0,
|
|
3184
|
+
children: flattenedSpans.map((item) => {
|
|
2152
3185
|
const { span, level } = item;
|
|
2153
3186
|
const isCollapsed = collapsedIds.has(span.spanId);
|
|
2154
3187
|
const isSelected = span.spanId === selectedSpanId;
|
|
2155
3188
|
const isHovered = span.spanId === hoveredSpanId;
|
|
2156
3189
|
const isParentOfHovered = hoveredSpanId ? isSpanAncestorOf(span, hoveredSpanId, flattenedSpans) : false;
|
|
2157
|
-
const
|
|
2158
|
-
const
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
left: 0,
|
|
2164
|
-
width: "100%",
|
|
2165
|
-
height: `${virtualItem.size}px`,
|
|
2166
|
-
transform: `translateY(${virtualItem.start}px)`
|
|
2167
|
-
},
|
|
2168
|
-
children: /* @__PURE__ */ jsx(SpanRow, {
|
|
3190
|
+
const viewRange = viewEnd - viewStart;
|
|
3191
|
+
const relativeStart = (calculateRelativeTime(span.startTimeUnixMs, parsedTrace.minTimeMs, parsedTrace.maxTimeMs) - viewStart) / viewRange;
|
|
3192
|
+
const relativeDuration = calculateRelativeDuration(span.durationMs, totalDurationMs) / viewRange;
|
|
3193
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
3194
|
+
"data-span-id": span.spanId,
|
|
3195
|
+
children: [/* @__PURE__ */ jsx(SpanRow, {
|
|
2169
3196
|
span,
|
|
2170
3197
|
level,
|
|
2171
3198
|
isCollapsed,
|
|
@@ -2177,34 +3204,30 @@ function TraceTimeline({ rows, onSpanClick, selectedSpanId: externalSelectedSpan
|
|
|
2177
3204
|
onClick: () => handleSpanClick(span),
|
|
2178
3205
|
onToggleCollapse: () => handleToggleCollapse(span.spanId),
|
|
2179
3206
|
onMouseEnter: () => setHoveredSpanId(span.spanId),
|
|
2180
|
-
onMouseLeave: () => setHoveredSpanId(null)
|
|
2181
|
-
|
|
3207
|
+
onMouseLeave: () => setHoveredSpanId(null),
|
|
3208
|
+
uiFind: uiFind || void 0
|
|
3209
|
+
}), isSelected && /* @__PURE__ */ jsx(SpanDetailInline, {
|
|
3210
|
+
span,
|
|
3211
|
+
traceStartMs: parsedTrace.minTimeMs
|
|
3212
|
+
})]
|
|
2182
3213
|
}, span.spanId);
|
|
2183
3214
|
})
|
|
2184
3215
|
})
|
|
2185
|
-
})
|
|
3216
|
+
] })
|
|
2186
3217
|
]
|
|
2187
|
-
})
|
|
2188
|
-
className: "w-96 h-full flex-shrink-0",
|
|
2189
|
-
children: /* @__PURE__ */ jsx(DetailPane, {
|
|
2190
|
-
span: selectedSpan,
|
|
2191
|
-
onClose: handleDeselect,
|
|
2192
|
-
onLinkClick: void 0
|
|
2193
|
-
})
|
|
2194
|
-
})]
|
|
3218
|
+
})
|
|
2195
3219
|
});
|
|
2196
3220
|
}
|
|
2197
|
-
|
|
2198
3221
|
//#endregion
|
|
2199
3222
|
//#region src/components/observability/TraceDetail/index.tsx
|
|
2200
|
-
function TraceDetail({
|
|
3223
|
+
function TraceDetail({ traceId, rows, isLoading, error, selectedSpanId, onSpanClick, onSpanDeselect, onBack }) {
|
|
2201
3224
|
return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsxs("div", {
|
|
2202
3225
|
className: "flex items-center gap-1.5 text-sm text-muted-foreground mb-4",
|
|
2203
3226
|
children: [
|
|
2204
|
-
/* @__PURE__ */
|
|
3227
|
+
/* @__PURE__ */ jsx("button", {
|
|
2205
3228
|
onClick: onBack,
|
|
2206
3229
|
className: "hover:text-foreground transition-colors",
|
|
2207
|
-
children:
|
|
3230
|
+
children: "Traces"
|
|
2208
3231
|
}),
|
|
2209
3232
|
/* @__PURE__ */ jsx("span", { children: "/" }),
|
|
2210
3233
|
/* @__PURE__ */ jsxs("span", {
|
|
@@ -2217,10 +3240,10 @@ function TraceDetail({ service, traceId, rows, isLoading, error, selectedSpanId,
|
|
|
2217
3240
|
isLoading,
|
|
2218
3241
|
error,
|
|
2219
3242
|
selectedSpanId,
|
|
2220
|
-
onSpanClick
|
|
3243
|
+
onSpanClick,
|
|
3244
|
+
onSpanDeselect
|
|
2221
3245
|
})] });
|
|
2222
3246
|
}
|
|
2223
|
-
|
|
2224
3247
|
//#endregion
|
|
2225
3248
|
//#region src/components/observability/LogTimeline/LogRow.tsx
|
|
2226
3249
|
function formatTimestamp(timeMs) {
|
|
@@ -2346,7 +3369,6 @@ const LogRow = memo(function LogRow({ log, isSelected, onClick, searchText, rela
|
|
|
2346
3369
|
]
|
|
2347
3370
|
});
|
|
2348
3371
|
});
|
|
2349
|
-
|
|
2350
3372
|
//#endregion
|
|
2351
3373
|
//#region src/components/observability/LogTimeline/LogDetailPane/AttributesTab.tsx
|
|
2352
3374
|
function AttributesTab({ log }) {
|
|
@@ -2379,7 +3401,6 @@ function AttributesTab({ log }) {
|
|
|
2379
3401
|
})
|
|
2380
3402
|
});
|
|
2381
3403
|
}
|
|
2382
|
-
|
|
2383
3404
|
//#endregion
|
|
2384
3405
|
//#region src/components/observability/LogTimeline/LogDetailPane/JsonTreeView.tsx
|
|
2385
3406
|
function JsonTreeView({ data, level = 0 }) {
|
|
@@ -2467,7 +3488,6 @@ function formatPrimitiveValue(value) {
|
|
|
2467
3488
|
if (typeof value === "number") return String(value);
|
|
2468
3489
|
return String(value);
|
|
2469
3490
|
}
|
|
2470
|
-
|
|
2471
3491
|
//#endregion
|
|
2472
3492
|
//#region src/components/observability/LogTimeline/LogDetailPane/index.tsx
|
|
2473
3493
|
function LogDetailPane({ log, onClose, onTraceLinkClick, initialTab = "message", wordWrap = true }) {
|
|
@@ -2679,7 +3699,6 @@ function getSeverityColor(severity) {
|
|
|
2679
3699
|
bg: "bg-gray-50 dark:bg-gray-800/20"
|
|
2680
3700
|
};
|
|
2681
3701
|
}
|
|
2682
|
-
|
|
2683
3702
|
//#endregion
|
|
2684
3703
|
//#region src/components/observability/LogTimeline/shortcuts.ts
|
|
2685
3704
|
const LOG_VIEWER_SHORTCUTS = {
|
|
@@ -2731,7 +3750,6 @@ const LOG_VIEWER_SHORTCUTS = {
|
|
|
2731
3750
|
}
|
|
2732
3751
|
]
|
|
2733
3752
|
};
|
|
2734
|
-
|
|
2735
3753
|
//#endregion
|
|
2736
3754
|
//#region src/components/observability/LogTimeline/index.tsx
|
|
2737
3755
|
/**
|
|
@@ -2937,7 +3955,7 @@ function LogTimeline({ rows, onLogClick, onTraceLinkClick, selectedLogId: extern
|
|
|
2937
3955
|
useEffect(() => {
|
|
2938
3956
|
const handleKeyDown = (e) => {
|
|
2939
3957
|
const isFormField = e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement || e.target instanceof HTMLSelectElement;
|
|
2940
|
-
if (isFormField && e.key === "Escape") {
|
|
3958
|
+
if (isFormField && e.key === "Escape" && e.target instanceof HTMLElement) {
|
|
2941
3959
|
e.target.blur();
|
|
2942
3960
|
return;
|
|
2943
3961
|
}
|
|
@@ -3178,7 +4196,6 @@ function LogTimeline({ rows, onLogClick, onTraceLinkClick, selectedLogId: extern
|
|
|
3178
4196
|
})]
|
|
3179
4197
|
});
|
|
3180
4198
|
}
|
|
3181
|
-
|
|
3182
4199
|
//#endregion
|
|
3183
4200
|
//#region src/components/observability/LogTimeline/LogFilter.tsx
|
|
3184
4201
|
/**
|
|
@@ -3279,7 +4296,7 @@ function MultiSelect({ options, selected, onChange, testId }) {
|
|
|
3279
4296
|
useEffect(() => {
|
|
3280
4297
|
if (!dropOpen) return;
|
|
3281
4298
|
const handler = (e) => {
|
|
3282
|
-
if (ref.current && !ref.current.contains(e.target)) setDropOpen(false);
|
|
4299
|
+
if (ref.current && e.target instanceof Node && !ref.current.contains(e.target)) setDropOpen(false);
|
|
3283
4300
|
};
|
|
3284
4301
|
document.addEventListener("mousedown", handler);
|
|
3285
4302
|
return () => document.removeEventListener("mousedown", handler);
|
|
@@ -3730,7 +4747,6 @@ function LogFilter({ value, onChange, rows = [], selectedServices = [], onSelect
|
|
|
3730
4747
|
})]
|
|
3731
4748
|
});
|
|
3732
4749
|
}
|
|
3733
|
-
|
|
3734
4750
|
//#endregion
|
|
3735
4751
|
//#region src/components/observability/utils/lttb.ts
|
|
3736
4752
|
function triangleArea(p1, p2, p3) {
|
|
@@ -3796,7 +4812,27 @@ function downsampleLTTB(data, targetPoints) {
|
|
|
3796
4812
|
if (lastPoint) sampled.push(lastPoint);
|
|
3797
4813
|
return sampled;
|
|
3798
4814
|
}
|
|
3799
|
-
|
|
4815
|
+
//#endregion
|
|
4816
|
+
//#region src/components/observability/shared/TooltipEntryList.tsx
|
|
4817
|
+
function TooltipEntryList({ payload, displayLabelMap, formatValue }) {
|
|
4818
|
+
return payload.map((entry, i) => {
|
|
4819
|
+
const dataKey = entry.dataKey;
|
|
4820
|
+
const value = entry.value;
|
|
4821
|
+
if (typeof dataKey !== "string" || typeof value !== "number") return null;
|
|
4822
|
+
return /* @__PURE__ */ jsxs("p", {
|
|
4823
|
+
className: "text-sm",
|
|
4824
|
+
style: { color: entry.color },
|
|
4825
|
+
children: [
|
|
4826
|
+
/* @__PURE__ */ jsxs("span", {
|
|
4827
|
+
className: "font-medium",
|
|
4828
|
+
children: [displayLabelMap.get(dataKey) ?? dataKey, ":"]
|
|
4829
|
+
}),
|
|
4830
|
+
" ",
|
|
4831
|
+
formatValue(value)
|
|
4832
|
+
]
|
|
4833
|
+
}, i);
|
|
4834
|
+
});
|
|
4835
|
+
}
|
|
3800
4836
|
//#endregion
|
|
3801
4837
|
//#region src/components/observability/utils/units.ts
|
|
3802
4838
|
const BYTE_SCALES = [
|
|
@@ -3971,7 +5007,6 @@ function formatDisplayValue(value, scale) {
|
|
|
3971
5007
|
function formatOtelValue(value, unit) {
|
|
3972
5008
|
return formatDisplayValue(value, resolveUnitScale(unit, Math.abs(value)));
|
|
3973
5009
|
}
|
|
3974
|
-
|
|
3975
5010
|
//#endregion
|
|
3976
5011
|
//#region src/components/observability/MetricTimeSeries/index.tsx
|
|
3977
5012
|
/**
|
|
@@ -4009,7 +5044,14 @@ function buildMetrics(rows) {
|
|
|
4009
5044
|
for (const row of rows) {
|
|
4010
5045
|
const name = row.MetricName ?? "unknown";
|
|
4011
5046
|
const type = row.MetricType;
|
|
4012
|
-
|
|
5047
|
+
let value;
|
|
5048
|
+
if (type === "Gauge" || type === "Sum") value = "Value" in row ? row.Value : void 0;
|
|
5049
|
+
else if (type === "Histogram" || type === "ExponentialHistogram" || type === "Summary") {
|
|
5050
|
+
const sum = "Sum" in row ? row.Sum : void 0;
|
|
5051
|
+
const count = "Count" in row ? row.Count : void 0;
|
|
5052
|
+
if (sum != null && count != null && count > 0) value = sum / count;
|
|
5053
|
+
}
|
|
5054
|
+
if (value === void 0) continue;
|
|
4013
5055
|
if (!metricMap.has(name)) metricMap.set(name, /* @__PURE__ */ new Map());
|
|
4014
5056
|
if (!metricMeta.has(name)) metricMeta.set(name, {
|
|
4015
5057
|
description: row.MetricDescription ?? "",
|
|
@@ -4028,8 +5070,6 @@ function buildMetrics(rows) {
|
|
|
4028
5070
|
dataPoints: []
|
|
4029
5071
|
});
|
|
4030
5072
|
}
|
|
4031
|
-
if (!("Value" in row)) continue;
|
|
4032
|
-
const value = row.Value;
|
|
4033
5073
|
const timestamp = parseInt(row.TimeUnix, 10) / 1e6;
|
|
4034
5074
|
seriesMap.get(seriesKey).dataPoints.push({
|
|
4035
5075
|
timestamp,
|
|
@@ -4275,18 +5315,11 @@ function CustomTooltip({ active, payload, label, formatTime, formatValue, displa
|
|
|
4275
5315
|
children: [/* @__PURE__ */ jsx("p", {
|
|
4276
5316
|
className: "text-gray-400 text-xs mb-2",
|
|
4277
5317
|
children: formatTime(typeof label === "number" ? label : Number(label))
|
|
4278
|
-
}),
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
className: "font-medium",
|
|
4284
|
-
children: [displayLabelMap.get(entry.dataKey) ?? entry.dataKey, ":"]
|
|
4285
|
-
}),
|
|
4286
|
-
" ",
|
|
4287
|
-
formatValue(entry.value)
|
|
4288
|
-
]
|
|
4289
|
-
}, i))]
|
|
5318
|
+
}), /* @__PURE__ */ jsx(TooltipEntryList, {
|
|
5319
|
+
payload,
|
|
5320
|
+
displayLabelMap,
|
|
5321
|
+
formatValue
|
|
5322
|
+
})]
|
|
4290
5323
|
});
|
|
4291
5324
|
}
|
|
4292
5325
|
function MetricLoadingSkeleton({ height = 400 }) {
|
|
@@ -4333,7 +5366,6 @@ function MetricLoadingSkeleton({ height = 400 }) {
|
|
|
4333
5366
|
})
|
|
4334
5367
|
});
|
|
4335
5368
|
}
|
|
4336
|
-
|
|
4337
5369
|
//#endregion
|
|
4338
5370
|
//#region src/components/observability/MetricHistogram/index.tsx
|
|
4339
5371
|
/**
|
|
@@ -4347,6 +5379,9 @@ const COLORS = [
|
|
|
4347
5379
|
"#00C49F",
|
|
4348
5380
|
"#0088FE"
|
|
4349
5381
|
];
|
|
5382
|
+
function isBucketData(v) {
|
|
5383
|
+
return typeof v === "object" && v !== null && "bucket" in v && "lowerBound" in v;
|
|
5384
|
+
}
|
|
4350
5385
|
const defaultFormatBucketLabel = (bound, index, bounds) => {
|
|
4351
5386
|
if (index === 0) return `≤${bound}`;
|
|
4352
5387
|
if (index === bounds.length) return `>${bounds[bounds.length - 1]}`;
|
|
@@ -4389,7 +5424,8 @@ function buildHistogramData(rows, formatLabel = defaultFormatBucketLabel) {
|
|
|
4389
5424
|
};
|
|
4390
5425
|
buckets.push(bucket);
|
|
4391
5426
|
}
|
|
4392
|
-
|
|
5427
|
+
const prev = bucket[seriesName];
|
|
5428
|
+
bucket[seriesName] = (typeof prev === "number" ? prev : 0) + count;
|
|
4393
5429
|
}
|
|
4394
5430
|
}
|
|
4395
5431
|
buckets.sort((a, b) => a.lowerBound - b.lowerBound);
|
|
@@ -4526,25 +5562,18 @@ function MetricHistogram({ rows, isLoading = false, error, height = 400, unit: u
|
|
|
4526
5562
|
}
|
|
4527
5563
|
function HistogramTooltip({ active, payload, formatValue, boundsScale, displayLabelMap }) {
|
|
4528
5564
|
if (!active || !payload?.length) return null;
|
|
4529
|
-
const
|
|
4530
|
-
if (!
|
|
5565
|
+
const raw = payload[0]?.payload;
|
|
5566
|
+
if (!isBucketData(raw)) return null;
|
|
4531
5567
|
return /* @__PURE__ */ jsxs("div", {
|
|
4532
5568
|
className: "bg-background border border-gray-700 rounded-lg p-3 shadow-lg",
|
|
4533
5569
|
children: [/* @__PURE__ */ jsxs("p", {
|
|
4534
5570
|
className: "text-gray-300 text-sm font-medium mb-2",
|
|
4535
|
-
children: ["Bucket: ", boundsScale ? `${formatDisplayValue(
|
|
4536
|
-
}),
|
|
4537
|
-
|
|
4538
|
-
|
|
4539
|
-
|
|
4540
|
-
|
|
4541
|
-
className: "font-medium",
|
|
4542
|
-
children: [displayLabelMap.get(entry.dataKey) ?? entry.dataKey, ":"]
|
|
4543
|
-
}),
|
|
4544
|
-
" ",
|
|
4545
|
-
formatValue(entry.value)
|
|
4546
|
-
]
|
|
4547
|
-
}, i))]
|
|
5571
|
+
children: ["Bucket: ", boundsScale ? `${formatDisplayValue(raw.lowerBound, boundsScale)} – ${raw.upperBound === Infinity ? "∞" : formatDisplayValue(raw.upperBound, boundsScale)}` : raw.bucket]
|
|
5572
|
+
}), /* @__PURE__ */ jsx(TooltipEntryList, {
|
|
5573
|
+
payload,
|
|
5574
|
+
displayLabelMap,
|
|
5575
|
+
formatValue
|
|
5576
|
+
})]
|
|
4548
5577
|
});
|
|
4549
5578
|
}
|
|
4550
5579
|
function HistogramLoadingSkeleton({ height = 400 }) {
|
|
@@ -4584,7 +5613,6 @@ function HistogramLoadingSkeleton({ height = 400 }) {
|
|
|
4584
5613
|
})
|
|
4585
5614
|
});
|
|
4586
5615
|
}
|
|
4587
|
-
|
|
4588
5616
|
//#endregion
|
|
4589
5617
|
//#region src/components/observability/MetricStat/index.tsx
|
|
4590
5618
|
/**
|
|
@@ -4777,7 +5805,6 @@ function TrendIndicator({ direction, value }) {
|
|
|
4777
5805
|
children: [arrow, value !== void 0 && ` ${Math.abs(value).toFixed(1)}%`]
|
|
4778
5806
|
});
|
|
4779
5807
|
}
|
|
4780
|
-
|
|
4781
5808
|
//#endregion
|
|
4782
5809
|
//#region src/components/observability/MetricTable/index.tsx
|
|
4783
5810
|
/**
|
|
@@ -4917,7 +5944,6 @@ function MetricTable({ rows, isLoading = false, error, maxRows = 100, formatValu
|
|
|
4917
5944
|
})]
|
|
4918
5945
|
});
|
|
4919
5946
|
}
|
|
4920
|
-
|
|
4921
5947
|
//#endregion
|
|
4922
5948
|
//#region src/lib/renderer.tsx
|
|
4923
5949
|
/**
|
|
@@ -5022,7 +6048,6 @@ function Renderer({ tree, registry, fallback }) {
|
|
|
5022
6048
|
fallback
|
|
5023
6049
|
});
|
|
5024
6050
|
}
|
|
5025
|
-
|
|
5026
6051
|
//#endregion
|
|
5027
6052
|
//#region src/lib/catalog.ts
|
|
5028
6053
|
const dashboardCatalog = createCatalog({
|
|
@@ -5222,8 +6247,7 @@ const dashboardCatalog = createCatalog({
|
|
|
5222
6247
|
}
|
|
5223
6248
|
}
|
|
5224
6249
|
});
|
|
5225
|
-
|
|
5226
|
-
|
|
6250
|
+
Object.keys(dashboardCatalog.components);
|
|
5227
6251
|
//#endregion
|
|
5228
6252
|
//#region src/components/dashboard/Badge/index.tsx
|
|
5229
6253
|
function Badge({ element }) {
|
|
@@ -5247,7 +6271,6 @@ function Badge({ element }) {
|
|
|
5247
6271
|
children: text
|
|
5248
6272
|
});
|
|
5249
6273
|
}
|
|
5250
|
-
|
|
5251
6274
|
//#endregion
|
|
5252
6275
|
//#region src/components/dashboard/Card/index.tsx
|
|
5253
6276
|
function Card({ element, children }) {
|
|
@@ -5288,7 +6311,6 @@ function Card({ element, children }) {
|
|
|
5288
6311
|
})]
|
|
5289
6312
|
});
|
|
5290
6313
|
}
|
|
5291
|
-
|
|
5292
6314
|
//#endregion
|
|
5293
6315
|
//#region src/components/dashboard/Divider/index.tsx
|
|
5294
6316
|
function Divider({ element }) {
|
|
@@ -5326,7 +6348,6 @@ function Divider({ element }) {
|
|
|
5326
6348
|
margin: "16px 0"
|
|
5327
6349
|
} });
|
|
5328
6350
|
}
|
|
5329
|
-
|
|
5330
6351
|
//#endregion
|
|
5331
6352
|
//#region src/components/dashboard/Empty/index.tsx
|
|
5332
6353
|
function Empty({ element, onAction }) {
|
|
@@ -5369,7 +6390,6 @@ function Empty({ element, onAction }) {
|
|
|
5369
6390
|
]
|
|
5370
6391
|
});
|
|
5371
6392
|
}
|
|
5372
|
-
|
|
5373
6393
|
//#endregion
|
|
5374
6394
|
//#region src/components/dashboard/Grid/index.tsx
|
|
5375
6395
|
function Grid({ element, children }) {
|
|
@@ -5387,7 +6407,6 @@ function Grid({ element, children }) {
|
|
|
5387
6407
|
children
|
|
5388
6408
|
});
|
|
5389
6409
|
}
|
|
5390
|
-
|
|
5391
6410
|
//#endregion
|
|
5392
6411
|
//#region src/components/dashboard/Heading/index.tsx
|
|
5393
6412
|
function Heading({ element }) {
|
|
@@ -5406,7 +6425,6 @@ function Heading({ element }) {
|
|
|
5406
6425
|
children: text
|
|
5407
6426
|
});
|
|
5408
6427
|
}
|
|
5409
|
-
|
|
5410
6428
|
//#endregion
|
|
5411
6429
|
//#region src/components/dashboard/Stack/index.tsx
|
|
5412
6430
|
function Stack({ element, children }) {
|
|
@@ -5430,7 +6448,6 @@ function Stack({ element, children }) {
|
|
|
5430
6448
|
children
|
|
5431
6449
|
});
|
|
5432
6450
|
}
|
|
5433
|
-
|
|
5434
6451
|
//#endregion
|
|
5435
6452
|
//#region src/components/dashboard/Text/index.tsx
|
|
5436
6453
|
function Text({ element }) {
|
|
@@ -5449,7 +6466,6 @@ function Text({ element }) {
|
|
|
5449
6466
|
children: content
|
|
5450
6467
|
});
|
|
5451
6468
|
}
|
|
5452
|
-
|
|
5453
6469
|
//#endregion
|
|
5454
6470
|
//#region src/components/observability/renderers/OtelLogTimeline.tsx
|
|
5455
6471
|
function OtelLogTimeline(props) {
|
|
@@ -5461,13 +6477,16 @@ function OtelLogTimeline(props) {
|
|
|
5461
6477
|
children: "No data source"
|
|
5462
6478
|
});
|
|
5463
6479
|
const response = props.data;
|
|
5464
|
-
return /* @__PURE__ */ jsx(
|
|
5465
|
-
|
|
5466
|
-
|
|
5467
|
-
|
|
6480
|
+
return /* @__PURE__ */ jsx("div", {
|
|
6481
|
+
style: { height: props.element.props.height ?? 600 },
|
|
6482
|
+
className: "flex flex-col min-h-0",
|
|
6483
|
+
children: /* @__PURE__ */ jsx(LogTimeline, {
|
|
6484
|
+
rows: response?.data ?? [],
|
|
6485
|
+
isLoading: props.loading,
|
|
6486
|
+
error: props.error ?? void 0
|
|
6487
|
+
})
|
|
5468
6488
|
});
|
|
5469
6489
|
}
|
|
5470
|
-
|
|
5471
6490
|
//#endregion
|
|
5472
6491
|
//#region src/components/observability/renderers/OtelMetricDiscovery.tsx
|
|
5473
6492
|
const TYPE_ORDER = {
|
|
@@ -5545,7 +6564,6 @@ function OtelMetricDiscovery(props) {
|
|
|
5545
6564
|
})
|
|
5546
6565
|
});
|
|
5547
6566
|
}
|
|
5548
|
-
|
|
5549
6567
|
//#endregion
|
|
5550
6568
|
//#region src/components/observability/renderers/OtelMetricHistogram.tsx
|
|
5551
6569
|
function OtelMetricHistogram(props) {
|
|
@@ -5566,7 +6584,6 @@ function OtelMetricHistogram(props) {
|
|
|
5566
6584
|
unit: props.element.props.unit ?? void 0
|
|
5567
6585
|
});
|
|
5568
6586
|
}
|
|
5569
|
-
|
|
5570
6587
|
//#endregion
|
|
5571
6588
|
//#region src/components/observability/renderers/OtelMetricStat.tsx
|
|
5572
6589
|
function OtelMetricStat(props) {
|
|
@@ -5587,7 +6604,6 @@ function OtelMetricStat(props) {
|
|
|
5587
6604
|
formatValue: formatOtelValue
|
|
5588
6605
|
});
|
|
5589
6606
|
}
|
|
5590
|
-
|
|
5591
6607
|
//#endregion
|
|
5592
6608
|
//#region src/components/observability/renderers/OtelMetricTable.tsx
|
|
5593
6609
|
function OtelMetricTable(props) {
|
|
@@ -5606,7 +6622,6 @@ function OtelMetricTable(props) {
|
|
|
5606
6622
|
maxRows: props.element.props.maxRows ?? 100
|
|
5607
6623
|
});
|
|
5608
6624
|
}
|
|
5609
|
-
|
|
5610
6625
|
//#endregion
|
|
5611
6626
|
//#region src/components/observability/renderers/OtelMetricTimeSeries.tsx
|
|
5612
6627
|
function OtelMetricTimeSeries(props) {
|
|
@@ -5628,7 +6643,6 @@ function OtelMetricTimeSeries(props) {
|
|
|
5628
6643
|
unit: props.element.props.unit ?? void 0
|
|
5629
6644
|
});
|
|
5630
6645
|
}
|
|
5631
|
-
|
|
5632
6646
|
//#endregion
|
|
5633
6647
|
//#region src/components/observability/renderers/OtelTraceDetail.tsx
|
|
5634
6648
|
function OtelTraceDetail(props) {
|
|
@@ -5640,19 +6654,15 @@ function OtelTraceDetail(props) {
|
|
|
5640
6654
|
children: "No data source"
|
|
5641
6655
|
});
|
|
5642
6656
|
const rows = props.data?.data ?? [];
|
|
5643
|
-
const
|
|
5644
|
-
const service = firstRow?.ServiceName ?? "unknown";
|
|
5645
|
-
const traceId = firstRow?.TraceId ?? "";
|
|
6657
|
+
const traceId = rows[0]?.TraceId ?? "";
|
|
5646
6658
|
return /* @__PURE__ */ jsx(TraceDetail, {
|
|
5647
6659
|
rows,
|
|
5648
6660
|
isLoading: props.loading,
|
|
5649
6661
|
error: props.error ?? void 0,
|
|
5650
|
-
service,
|
|
5651
6662
|
traceId,
|
|
5652
6663
|
onBack: () => {}
|
|
5653
6664
|
});
|
|
5654
6665
|
}
|
|
5655
|
-
|
|
5656
6666
|
//#endregion
|
|
5657
6667
|
//#region src/components/observability/DynamicDashboard/index.tsx
|
|
5658
6668
|
const MetricsRenderer = createRendererFromCatalog(observabilityCatalog, {
|
|
@@ -5678,24 +6688,275 @@ function DynamicDashboard({ kopaiClient, uiTree }) {
|
|
|
5678
6688
|
children: /* @__PURE__ */ jsx(MetricsRenderer, { tree: uiTree })
|
|
5679
6689
|
});
|
|
5680
6690
|
}
|
|
5681
|
-
|
|
6691
|
+
//#endregion
|
|
6692
|
+
//#region src/components/observability/TraceComparison/index.tsx
|
|
6693
|
+
function computeTraceStats(rows) {
|
|
6694
|
+
if (rows.length === 0) return {
|
|
6695
|
+
durationMs: 0,
|
|
6696
|
+
spanCount: 0
|
|
6697
|
+
};
|
|
6698
|
+
let minTs = Infinity;
|
|
6699
|
+
let maxEnd = -Infinity;
|
|
6700
|
+
for (const row of rows) {
|
|
6701
|
+
const startMs = parseInt(row.Timestamp, 10) / 1e6;
|
|
6702
|
+
const endMs = startMs + (row.Duration ? parseInt(row.Duration, 10) : 0) / 1e6;
|
|
6703
|
+
minTs = Math.min(minTs, startMs);
|
|
6704
|
+
maxEnd = Math.max(maxEnd, endMs);
|
|
6705
|
+
}
|
|
6706
|
+
return {
|
|
6707
|
+
durationMs: maxEnd - minTs,
|
|
6708
|
+
spanCount: rows.length
|
|
6709
|
+
};
|
|
6710
|
+
}
|
|
6711
|
+
function collectSignatures(rows) {
|
|
6712
|
+
const map = /* @__PURE__ */ new Map();
|
|
6713
|
+
for (const row of rows) {
|
|
6714
|
+
const key = `${row.ServiceName ?? "unknown"}::${row.SpanName ?? ""}`;
|
|
6715
|
+
const durMs = (row.Duration ? parseInt(row.Duration, 10) : 0) / 1e6;
|
|
6716
|
+
const existing = map.get(key);
|
|
6717
|
+
if (existing) {
|
|
6718
|
+
existing.count++;
|
|
6719
|
+
existing.totalDurationMs += durMs;
|
|
6720
|
+
} else map.set(key, {
|
|
6721
|
+
count: 1,
|
|
6722
|
+
totalDurationMs: durMs
|
|
6723
|
+
});
|
|
6724
|
+
}
|
|
6725
|
+
return map;
|
|
6726
|
+
}
|
|
6727
|
+
function computeDiff(rowsA, rowsB) {
|
|
6728
|
+
const sigA = collectSignatures(rowsA);
|
|
6729
|
+
const sigB = collectSignatures(rowsB);
|
|
6730
|
+
const allKeys = new Set([...sigA.keys(), ...sigB.keys()]);
|
|
6731
|
+
const result = [];
|
|
6732
|
+
for (const key of allKeys) {
|
|
6733
|
+
const [serviceName = "unknown", spanName = ""] = key.split("::");
|
|
6734
|
+
const a = sigA.get(key);
|
|
6735
|
+
const b = sigB.get(key);
|
|
6736
|
+
const countA = a?.count ?? 0;
|
|
6737
|
+
const countB = b?.count ?? 0;
|
|
6738
|
+
const avgA = a ? a.totalDurationMs / a.count : 0;
|
|
6739
|
+
const avgB = b ? b.totalDurationMs / b.count : 0;
|
|
6740
|
+
result.push({
|
|
6741
|
+
serviceName,
|
|
6742
|
+
spanName,
|
|
6743
|
+
countA,
|
|
6744
|
+
countB,
|
|
6745
|
+
avgDurationA: avgA,
|
|
6746
|
+
avgDurationB: avgB,
|
|
6747
|
+
deltaMs: avgB - avgA
|
|
6748
|
+
});
|
|
6749
|
+
}
|
|
6750
|
+
return result.sort((a, b) => {
|
|
6751
|
+
const aShared = a.countA > 0 && a.countB > 0;
|
|
6752
|
+
if (aShared !== (b.countA > 0 && b.countB > 0)) return aShared ? 1 : -1;
|
|
6753
|
+
return Math.abs(b.deltaMs) - Math.abs(a.deltaMs);
|
|
6754
|
+
});
|
|
6755
|
+
}
|
|
6756
|
+
function formatDelta(ms) {
|
|
6757
|
+
return `${ms > 0 ? "+" : ""}${formatDuration(ms)}`;
|
|
6758
|
+
}
|
|
6759
|
+
function TraceComparison({ traceIdA, traceIdB, onBack }) {
|
|
6760
|
+
const dsA = useMemo(() => ({
|
|
6761
|
+
method: "getTrace",
|
|
6762
|
+
params: { traceId: traceIdA }
|
|
6763
|
+
}), [traceIdA]);
|
|
6764
|
+
const dsB = useMemo(() => ({
|
|
6765
|
+
method: "getTrace",
|
|
6766
|
+
params: { traceId: traceIdB }
|
|
6767
|
+
}), [traceIdB]);
|
|
6768
|
+
const { data: rowsA, loading: loadingA, error: errorA } = useKopaiData(dsA);
|
|
6769
|
+
const { data: rowsB, loading: loadingB, error: errorB } = useKopaiData(dsB);
|
|
6770
|
+
const statsA = useMemo(() => computeTraceStats(rowsA ?? []), [rowsA]);
|
|
6771
|
+
const statsB = useMemo(() => computeTraceStats(rowsB ?? []), [rowsB]);
|
|
6772
|
+
const diff = useMemo(() => computeDiff(rowsA ?? [], rowsB ?? []), [rowsA, rowsB]);
|
|
6773
|
+
const durationDelta = statsB.durationMs - statsA.durationMs;
|
|
6774
|
+
const spanDelta = statsB.spanCount - statsA.spanCount;
|
|
6775
|
+
const isLoading = loadingA || loadingB;
|
|
6776
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
6777
|
+
className: "flex flex-col gap-4",
|
|
6778
|
+
children: [
|
|
6779
|
+
/* @__PURE__ */ jsxs("div", {
|
|
6780
|
+
className: "flex items-center justify-between bg-background border border-border rounded-lg p-4",
|
|
6781
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
6782
|
+
className: "flex items-center gap-4",
|
|
6783
|
+
children: [/* @__PURE__ */ jsx("button", {
|
|
6784
|
+
onClick: onBack,
|
|
6785
|
+
className: "text-sm text-muted-foreground hover:text-foreground transition-colors",
|
|
6786
|
+
children: "← Back"
|
|
6787
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
6788
|
+
className: "flex items-center gap-6 text-sm",
|
|
6789
|
+
children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("span", {
|
|
6790
|
+
className: "text-muted-foreground mr-1",
|
|
6791
|
+
children: "A:"
|
|
6792
|
+
}), /* @__PURE__ */ jsxs("span", {
|
|
6793
|
+
className: "font-mono text-xs text-foreground",
|
|
6794
|
+
children: [traceIdA.slice(0, 16), "..."]
|
|
6795
|
+
})] }), /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("span", {
|
|
6796
|
+
className: "text-muted-foreground mr-1",
|
|
6797
|
+
children: "B:"
|
|
6798
|
+
}), /* @__PURE__ */ jsxs("span", {
|
|
6799
|
+
className: "font-mono text-xs text-foreground",
|
|
6800
|
+
children: [traceIdB.slice(0, 16), "..."]
|
|
6801
|
+
})] })]
|
|
6802
|
+
})]
|
|
6803
|
+
}), !isLoading && /* @__PURE__ */ jsxs("div", {
|
|
6804
|
+
className: "flex items-center gap-6 text-sm",
|
|
6805
|
+
children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("span", {
|
|
6806
|
+
className: "text-muted-foreground mr-1",
|
|
6807
|
+
children: "Duration delta:"
|
|
6808
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
6809
|
+
className: durationDelta > 0 ? "text-red-400" : durationDelta < 0 ? "text-green-400" : "text-foreground",
|
|
6810
|
+
children: formatDelta(durationDelta)
|
|
6811
|
+
})] }), /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("span", {
|
|
6812
|
+
className: "text-muted-foreground mr-1",
|
|
6813
|
+
children: "Span count delta:"
|
|
6814
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
6815
|
+
className: spanDelta > 0 ? "text-red-400" : spanDelta < 0 ? "text-green-400" : "text-foreground",
|
|
6816
|
+
children: spanDelta > 0 ? `+${spanDelta}` : String(spanDelta)
|
|
6817
|
+
})] })]
|
|
6818
|
+
})]
|
|
6819
|
+
}),
|
|
6820
|
+
/* @__PURE__ */ jsxs("div", {
|
|
6821
|
+
className: "grid grid-cols-2 gap-4",
|
|
6822
|
+
style: { height: "50vh" },
|
|
6823
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
6824
|
+
className: "border border-border rounded-lg overflow-hidden",
|
|
6825
|
+
children: /* @__PURE__ */ jsx(TraceTimeline, {
|
|
6826
|
+
rows: rowsA ?? [],
|
|
6827
|
+
isLoading: loadingA,
|
|
6828
|
+
error: errorA ?? void 0
|
|
6829
|
+
})
|
|
6830
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
6831
|
+
className: "border border-border rounded-lg overflow-hidden",
|
|
6832
|
+
children: /* @__PURE__ */ jsx(TraceTimeline, {
|
|
6833
|
+
rows: rowsB ?? [],
|
|
6834
|
+
isLoading: loadingB,
|
|
6835
|
+
error: errorB ?? void 0
|
|
6836
|
+
})
|
|
6837
|
+
})]
|
|
6838
|
+
}),
|
|
6839
|
+
!isLoading && diff.length > 0 && /* @__PURE__ */ jsxs("div", {
|
|
6840
|
+
className: "border border-border rounded-lg overflow-hidden",
|
|
6841
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
6842
|
+
className: "px-4 py-3 border-b border-border bg-background",
|
|
6843
|
+
children: /* @__PURE__ */ jsx("h3", {
|
|
6844
|
+
className: "text-sm font-medium text-foreground",
|
|
6845
|
+
children: "Structural Diff"
|
|
6846
|
+
})
|
|
6847
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
6848
|
+
className: "overflow-x-auto",
|
|
6849
|
+
children: /* @__PURE__ */ jsxs("table", {
|
|
6850
|
+
className: "w-full text-sm",
|
|
6851
|
+
children: [/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", {
|
|
6852
|
+
className: "border-b border-border bg-muted/30",
|
|
6853
|
+
children: [
|
|
6854
|
+
/* @__PURE__ */ jsx("th", {
|
|
6855
|
+
className: "text-left px-4 py-2 text-muted-foreground font-medium",
|
|
6856
|
+
children: "Service"
|
|
6857
|
+
}),
|
|
6858
|
+
/* @__PURE__ */ jsx("th", {
|
|
6859
|
+
className: "text-left px-4 py-2 text-muted-foreground font-medium",
|
|
6860
|
+
children: "Span"
|
|
6861
|
+
}),
|
|
6862
|
+
/* @__PURE__ */ jsx("th", {
|
|
6863
|
+
className: "text-right px-4 py-2 text-muted-foreground font-medium",
|
|
6864
|
+
children: "Count A"
|
|
6865
|
+
}),
|
|
6866
|
+
/* @__PURE__ */ jsx("th", {
|
|
6867
|
+
className: "text-right px-4 py-2 text-muted-foreground font-medium",
|
|
6868
|
+
children: "Count B"
|
|
6869
|
+
}),
|
|
6870
|
+
/* @__PURE__ */ jsx("th", {
|
|
6871
|
+
className: "text-right px-4 py-2 text-muted-foreground font-medium",
|
|
6872
|
+
children: "Avg Dur A"
|
|
6873
|
+
}),
|
|
6874
|
+
/* @__PURE__ */ jsx("th", {
|
|
6875
|
+
className: "text-right px-4 py-2 text-muted-foreground font-medium",
|
|
6876
|
+
children: "Avg Dur B"
|
|
6877
|
+
}),
|
|
6878
|
+
/* @__PURE__ */ jsx("th", {
|
|
6879
|
+
className: "text-right px-4 py-2 text-muted-foreground font-medium",
|
|
6880
|
+
children: "Delta"
|
|
6881
|
+
})
|
|
6882
|
+
]
|
|
6883
|
+
}) }), /* @__PURE__ */ jsx("tbody", { children: diff.map((row) => {
|
|
6884
|
+
const onlyA = row.countA > 0 && row.countB === 0;
|
|
6885
|
+
const onlyB = row.countA === 0 && row.countB > 0;
|
|
6886
|
+
return /* @__PURE__ */ jsxs("tr", {
|
|
6887
|
+
className: `border-b border-border/50 ${onlyA ? "bg-red-500/5" : onlyB ? "bg-green-500/5" : ""}`,
|
|
6888
|
+
children: [
|
|
6889
|
+
/* @__PURE__ */ jsx("td", {
|
|
6890
|
+
className: "px-4 py-1.5 text-foreground",
|
|
6891
|
+
children: row.serviceName
|
|
6892
|
+
}),
|
|
6893
|
+
/* @__PURE__ */ jsx("td", {
|
|
6894
|
+
className: "px-4 py-1.5 font-mono text-xs text-foreground",
|
|
6895
|
+
children: row.spanName
|
|
6896
|
+
}),
|
|
6897
|
+
/* @__PURE__ */ jsx("td", {
|
|
6898
|
+
className: "px-4 py-1.5 text-right text-foreground",
|
|
6899
|
+
children: row.countA || /* @__PURE__ */ jsx("span", {
|
|
6900
|
+
className: "text-muted-foreground",
|
|
6901
|
+
children: "-"
|
|
6902
|
+
})
|
|
6903
|
+
}),
|
|
6904
|
+
/* @__PURE__ */ jsx("td", {
|
|
6905
|
+
className: "px-4 py-1.5 text-right text-foreground",
|
|
6906
|
+
children: row.countB || /* @__PURE__ */ jsx("span", {
|
|
6907
|
+
className: "text-muted-foreground",
|
|
6908
|
+
children: "-"
|
|
6909
|
+
})
|
|
6910
|
+
}),
|
|
6911
|
+
/* @__PURE__ */ jsx("td", {
|
|
6912
|
+
className: "px-4 py-1.5 text-right text-foreground",
|
|
6913
|
+
children: row.countA > 0 ? formatDuration(row.avgDurationA) : /* @__PURE__ */ jsx("span", {
|
|
6914
|
+
className: "text-muted-foreground",
|
|
6915
|
+
children: "-"
|
|
6916
|
+
})
|
|
6917
|
+
}),
|
|
6918
|
+
/* @__PURE__ */ jsx("td", {
|
|
6919
|
+
className: "px-4 py-1.5 text-right text-foreground",
|
|
6920
|
+
children: row.countB > 0 ? formatDuration(row.avgDurationB) : /* @__PURE__ */ jsx("span", {
|
|
6921
|
+
className: "text-muted-foreground",
|
|
6922
|
+
children: "-"
|
|
6923
|
+
})
|
|
6924
|
+
}),
|
|
6925
|
+
/* @__PURE__ */ jsx("td", {
|
|
6926
|
+
className: "px-4 py-1.5 text-right",
|
|
6927
|
+
children: row.countA > 0 && row.countB > 0 ? /* @__PURE__ */ jsx("span", {
|
|
6928
|
+
className: row.deltaMs > 0 ? "text-red-400" : row.deltaMs < 0 ? "text-green-400" : "text-foreground",
|
|
6929
|
+
children: formatDelta(row.deltaMs)
|
|
6930
|
+
}) : /* @__PURE__ */ jsx("span", {
|
|
6931
|
+
className: onlyA ? "text-red-400" : "text-green-400",
|
|
6932
|
+
children: onlyA ? "removed" : "added"
|
|
6933
|
+
})
|
|
6934
|
+
})
|
|
6935
|
+
]
|
|
6936
|
+
}, `${row.serviceName}::${row.spanName}`);
|
|
6937
|
+
}) })]
|
|
6938
|
+
})
|
|
6939
|
+
})]
|
|
6940
|
+
})
|
|
6941
|
+
]
|
|
6942
|
+
});
|
|
6943
|
+
}
|
|
5682
6944
|
//#endregion
|
|
5683
6945
|
//#region src/components/observability/ServiceList/shortcuts.ts
|
|
5684
6946
|
const SERVICES_SHORTCUTS = {
|
|
5685
|
-
name: "
|
|
6947
|
+
name: "Traces",
|
|
5686
6948
|
shortcuts: [{
|
|
5687
6949
|
keys: ["Backspace"],
|
|
5688
6950
|
description: "Go back"
|
|
5689
6951
|
}]
|
|
5690
6952
|
};
|
|
5691
|
-
|
|
5692
6953
|
//#endregion
|
|
5693
6954
|
//#region src/pages/observability.tsx
|
|
5694
6955
|
const TABS = [
|
|
5695
6956
|
{
|
|
5696
6957
|
key: "services",
|
|
5697
|
-
label: "
|
|
5698
|
-
shortcutKey: "
|
|
6958
|
+
label: "Traces",
|
|
6959
|
+
shortcutKey: "T"
|
|
5699
6960
|
},
|
|
5700
6961
|
{
|
|
5701
6962
|
key: "logs",
|
|
@@ -5715,20 +6976,53 @@ function readURLState() {
|
|
|
5715
6976
|
const span = params.get("span");
|
|
5716
6977
|
const dashboardId = params.get("dashboardId");
|
|
5717
6978
|
const rawTab = params.get("tab");
|
|
6979
|
+
const tab = service ? "services" : rawTab === "logs" || rawTab === "metrics" ? rawTab : "services";
|
|
6980
|
+
const rawLimit = params.get("limit");
|
|
6981
|
+
const limit = rawLimit ? parseInt(rawLimit, 10) : null;
|
|
5718
6982
|
return {
|
|
5719
|
-
tab
|
|
6983
|
+
tab,
|
|
5720
6984
|
service,
|
|
6985
|
+
operation: params.get("operation"),
|
|
6986
|
+
tags: params.get("tags"),
|
|
6987
|
+
lookback: params.get("lookback"),
|
|
6988
|
+
tsMin: params.get("tsMin"),
|
|
6989
|
+
tsMax: params.get("tsMax"),
|
|
6990
|
+
minDuration: params.get("minDuration"),
|
|
6991
|
+
maxDuration: params.get("maxDuration"),
|
|
6992
|
+
limit: limit !== null && !isNaN(limit) ? limit : null,
|
|
6993
|
+
sort: params.get("sort"),
|
|
5721
6994
|
trace,
|
|
5722
6995
|
span,
|
|
6996
|
+
view: params.get("view"),
|
|
6997
|
+
uiFind: params.get("uiFind"),
|
|
6998
|
+
compare: params.get("compare"),
|
|
6999
|
+
viewStart: params.get("viewStart"),
|
|
7000
|
+
viewEnd: params.get("viewEnd"),
|
|
5723
7001
|
dashboardId
|
|
5724
7002
|
};
|
|
5725
7003
|
}
|
|
5726
7004
|
function pushURLState(state, { replace = false } = {}) {
|
|
5727
7005
|
const params = new URLSearchParams();
|
|
5728
7006
|
if (state.tab !== "services") params.set("tab", state.tab);
|
|
5729
|
-
if (state.
|
|
5730
|
-
|
|
5731
|
-
|
|
7007
|
+
if (state.tab === "services") {
|
|
7008
|
+
if (state.service) params.set("service", state.service);
|
|
7009
|
+
if (state.operation) params.set("operation", state.operation);
|
|
7010
|
+
if (state.tags) params.set("tags", state.tags);
|
|
7011
|
+
if (state.lookback) params.set("lookback", state.lookback);
|
|
7012
|
+
if (state.tsMin) params.set("tsMin", state.tsMin);
|
|
7013
|
+
if (state.tsMax) params.set("tsMax", state.tsMax);
|
|
7014
|
+
if (state.minDuration) params.set("minDuration", state.minDuration);
|
|
7015
|
+
if (state.maxDuration) params.set("maxDuration", state.maxDuration);
|
|
7016
|
+
if (state.limit != null && state.limit !== 20) params.set("limit", String(state.limit));
|
|
7017
|
+
if (state.sort) params.set("sort", state.sort);
|
|
7018
|
+
if (state.trace) params.set("trace", state.trace);
|
|
7019
|
+
if (state.span) params.set("span", state.span);
|
|
7020
|
+
if (state.view) params.set("view", state.view);
|
|
7021
|
+
if (state.uiFind) params.set("uiFind", state.uiFind);
|
|
7022
|
+
if (state.compare) params.set("compare", state.compare);
|
|
7023
|
+
if (state.viewStart) params.set("viewStart", state.viewStart);
|
|
7024
|
+
if (state.viewEnd) params.set("viewEnd", state.viewEnd);
|
|
7025
|
+
}
|
|
5732
7026
|
const dashboardId = state.dashboardId !== void 0 ? state.dashboardId : new URLSearchParams(window.location.search).get("dashboardId");
|
|
5733
7027
|
if (dashboardId) params.set("dashboardId", dashboardId);
|
|
5734
7028
|
const qs = params.toString();
|
|
@@ -5745,8 +7039,22 @@ let _cachedSearch = "";
|
|
|
5745
7039
|
let _cachedState = {
|
|
5746
7040
|
tab: "services",
|
|
5747
7041
|
service: null,
|
|
7042
|
+
operation: null,
|
|
7043
|
+
tags: null,
|
|
7044
|
+
lookback: null,
|
|
7045
|
+
tsMin: null,
|
|
7046
|
+
tsMax: null,
|
|
7047
|
+
minDuration: null,
|
|
7048
|
+
maxDuration: null,
|
|
7049
|
+
limit: null,
|
|
7050
|
+
sort: null,
|
|
5748
7051
|
trace: null,
|
|
5749
7052
|
span: null,
|
|
7053
|
+
view: null,
|
|
7054
|
+
uiFind: null,
|
|
7055
|
+
compare: null,
|
|
7056
|
+
viewStart: null,
|
|
7057
|
+
viewEnd: null,
|
|
5750
7058
|
dashboardId: null
|
|
5751
7059
|
};
|
|
5752
7060
|
function getURLSnapshot() {
|
|
@@ -5856,6 +7164,26 @@ function parseDuration(input) {
|
|
|
5856
7164
|
s: 1e9
|
|
5857
7165
|
}[unit]));
|
|
5858
7166
|
}
|
|
7167
|
+
function parseLogfmt(str) {
|
|
7168
|
+
const result = {};
|
|
7169
|
+
const re = /(\w+)=(?:"([^"]*)"|([\S]*))/g;
|
|
7170
|
+
let m;
|
|
7171
|
+
while ((m = re.exec(str)) !== null) {
|
|
7172
|
+
const key = m[1];
|
|
7173
|
+
if (key) result[key] = m[2] ?? m[3] ?? "";
|
|
7174
|
+
}
|
|
7175
|
+
return result;
|
|
7176
|
+
}
|
|
7177
|
+
const LOOKBACK_MS = {
|
|
7178
|
+
"5m": 5 * 6e4,
|
|
7179
|
+
"15m": 15 * 6e4,
|
|
7180
|
+
"30m": 30 * 6e4,
|
|
7181
|
+
"1h": 60 * 6e4,
|
|
7182
|
+
"2h": 120 * 6e4,
|
|
7183
|
+
"6h": 360 * 6e4,
|
|
7184
|
+
"12h": 720 * 6e4,
|
|
7185
|
+
"24h": 1440 * 6e4
|
|
7186
|
+
};
|
|
5859
7187
|
function LogsTab() {
|
|
5860
7188
|
const [initState] = useState(() => readLogFilters());
|
|
5861
7189
|
const [filters, setFilters] = useState(initState.filters);
|
|
@@ -5915,187 +7243,146 @@ function LogsTab() {
|
|
|
5915
7243
|
})]
|
|
5916
7244
|
});
|
|
5917
7245
|
}
|
|
5918
|
-
|
|
5919
|
-
|
|
5920
|
-
|
|
5921
|
-
|
|
5922
|
-
sortOrder: "DESC"
|
|
5923
|
-
}
|
|
5924
|
-
};
|
|
5925
|
-
function ServiceListView({ onSelect }) {
|
|
5926
|
-
const { data, loading, error } = useKopaiData(SERVICES_DS);
|
|
5927
|
-
return /* @__PURE__ */ jsx(ServiceList, {
|
|
5928
|
-
services: useMemo(() => {
|
|
5929
|
-
if (!data?.data) return [];
|
|
5930
|
-
const names = /* @__PURE__ */ new Set();
|
|
5931
|
-
for (const row of data.data) names.add(row.ServiceName ?? "unknown");
|
|
5932
|
-
return Array.from(names).sort().map((name) => ({ name }));
|
|
5933
|
-
}, [data]),
|
|
5934
|
-
isLoading: loading,
|
|
5935
|
-
error: error ?? void 0,
|
|
5936
|
-
onSelect
|
|
5937
|
-
});
|
|
5938
|
-
}
|
|
5939
|
-
function TraceSearchView({ service, onBack, onSelectTrace }) {
|
|
5940
|
-
const [ds, setDs] = useState(() => ({
|
|
5941
|
-
method: "searchTracesPage",
|
|
5942
|
-
params: {
|
|
5943
|
-
serviceName: service,
|
|
5944
|
-
limit: 20,
|
|
5945
|
-
sortOrder: "DESC"
|
|
5946
|
-
}
|
|
5947
|
-
}));
|
|
5948
|
-
const handleSearch = useCallback((filters) => {
|
|
7246
|
+
function TraceSearchView({ onSelectTrace, onCompare }) {
|
|
7247
|
+
const urlState = useURLState();
|
|
7248
|
+
const service = urlState.service;
|
|
7249
|
+
const ds = useMemo(() => {
|
|
5949
7250
|
const params = {
|
|
5950
|
-
|
|
5951
|
-
limit: filters.limit,
|
|
7251
|
+
limit: urlState.limit ?? 20,
|
|
5952
7252
|
sortOrder: "DESC"
|
|
5953
7253
|
};
|
|
5954
|
-
if (
|
|
5955
|
-
if (
|
|
5956
|
-
if (
|
|
5957
|
-
const
|
|
7254
|
+
if (service) params.serviceName = service;
|
|
7255
|
+
if (urlState.operation) params.spanName = urlState.operation;
|
|
7256
|
+
if (urlState.lookback) {
|
|
7257
|
+
const ms = LOOKBACK_MS[urlState.lookback];
|
|
7258
|
+
if (ms) params.timestampMin = String((Date.now() - ms) * 1e6);
|
|
7259
|
+
}
|
|
7260
|
+
if (urlState.tsMin) params.timestampMin = urlState.tsMin;
|
|
7261
|
+
if (urlState.tsMax) params.timestampMax = urlState.tsMax;
|
|
7262
|
+
if (urlState.minDuration) {
|
|
7263
|
+
const parsed = parseDuration(urlState.minDuration);
|
|
5958
7264
|
if (parsed) params.durationMin = parsed;
|
|
5959
7265
|
}
|
|
5960
|
-
if (
|
|
5961
|
-
const parsed = parseDuration(
|
|
7266
|
+
if (urlState.maxDuration) {
|
|
7267
|
+
const parsed = parseDuration(urlState.maxDuration);
|
|
5962
7268
|
if (parsed) params.durationMax = parsed;
|
|
5963
7269
|
}
|
|
5964
|
-
|
|
5965
|
-
|
|
7270
|
+
if (urlState.tags) {
|
|
7271
|
+
const tagMap = parseLogfmt(urlState.tags);
|
|
7272
|
+
if (Object.keys(tagMap).length > 0) params.tags = tagMap;
|
|
7273
|
+
}
|
|
7274
|
+
return {
|
|
7275
|
+
method: "searchTraceSummariesPage",
|
|
5966
7276
|
params
|
|
7277
|
+
};
|
|
7278
|
+
}, [
|
|
7279
|
+
service,
|
|
7280
|
+
urlState.operation,
|
|
7281
|
+
urlState.lookback,
|
|
7282
|
+
urlState.tsMin,
|
|
7283
|
+
urlState.tsMax,
|
|
7284
|
+
urlState.minDuration,
|
|
7285
|
+
urlState.maxDuration,
|
|
7286
|
+
urlState.limit,
|
|
7287
|
+
urlState.tags
|
|
7288
|
+
]);
|
|
7289
|
+
const handleSearch = useCallback((filters) => {
|
|
7290
|
+
pushURLState({
|
|
7291
|
+
tab: "services",
|
|
7292
|
+
service: filters.service ?? service,
|
|
7293
|
+
operation: filters.operation ?? null,
|
|
7294
|
+
tags: filters.tags ?? null,
|
|
7295
|
+
lookback: filters.lookback ?? null,
|
|
7296
|
+
minDuration: filters.minDuration ?? null,
|
|
7297
|
+
maxDuration: filters.maxDuration ?? null,
|
|
7298
|
+
limit: filters.limit
|
|
5967
7299
|
});
|
|
5968
7300
|
}, [service]);
|
|
5969
7301
|
const { data, loading, error } = useKopaiData(ds);
|
|
5970
|
-
const
|
|
5971
|
-
const
|
|
5972
|
-
|
|
5973
|
-
|
|
5974
|
-
|
|
5975
|
-
|
|
5976
|
-
|
|
5977
|
-
|
|
5978
|
-
const ac = new AbortController();
|
|
5979
|
-
Promise.allSettled(traceIds.map((tid) => client.getTrace(tid, { signal: ac.signal }).then((spans) => [tid, spans]))).then((results) => {
|
|
5980
|
-
if (!ac.signal.aborted) {
|
|
5981
|
-
const entries = results.filter((r) => r.status === "fulfilled").map((r) => r.value);
|
|
5982
|
-
setFullTraces(new Map(entries));
|
|
5983
|
-
}
|
|
5984
|
-
}).catch((err) => {
|
|
5985
|
-
if (!ac.signal.aborted) console.error("Failed to fetch full traces", err);
|
|
5986
|
-
});
|
|
5987
|
-
return () => ac.abort();
|
|
5988
|
-
}, [data, client]);
|
|
5989
|
-
const operations = useMemo(() => {
|
|
7302
|
+
const { data: servicesData } = useKopaiData(useMemo(() => ({ method: "getServices" }), []));
|
|
7303
|
+
const _services = servicesData?.services ?? [];
|
|
7304
|
+
const { data: opsData } = useKopaiData(useMemo(() => service ? {
|
|
7305
|
+
method: "getOperations",
|
|
7306
|
+
params: { serviceName: service }
|
|
7307
|
+
} : void 0, [service]));
|
|
7308
|
+
const operations = opsData?.operations ?? [];
|
|
7309
|
+
const traces = useMemo(() => {
|
|
5990
7310
|
if (!data?.data) return [];
|
|
5991
|
-
|
|
5992
|
-
|
|
5993
|
-
|
|
7311
|
+
return data.data.map((row) => ({
|
|
7312
|
+
traceId: row.traceId,
|
|
7313
|
+
rootSpanName: row.rootSpanName,
|
|
7314
|
+
serviceName: row.rootServiceName,
|
|
7315
|
+
durationMs: parseInt(row.durationNs, 10) / 1e6,
|
|
7316
|
+
statusCode: row.errorCount > 0 ? "ERROR" : "OK",
|
|
7317
|
+
timestampMs: parseInt(row.startTimeNs, 10) / 1e6,
|
|
7318
|
+
spanCount: row.spanCount,
|
|
7319
|
+
services: row.services,
|
|
7320
|
+
errorCount: row.errorCount
|
|
7321
|
+
}));
|
|
5994
7322
|
}, [data]);
|
|
5995
7323
|
return /* @__PURE__ */ jsx(TraceSearch, {
|
|
5996
|
-
|
|
5997
|
-
|
|
5998
|
-
|
|
5999
|
-
const grouped = /* @__PURE__ */ new Map();
|
|
6000
|
-
for (const row of data.data) {
|
|
6001
|
-
const tid = row.TraceId;
|
|
6002
|
-
if (!grouped.has(tid)) grouped.set(tid, []);
|
|
6003
|
-
grouped.get(tid).push(row);
|
|
6004
|
-
}
|
|
6005
|
-
return Array.from(grouped.entries()).map(([traceId, searchSpans]) => {
|
|
6006
|
-
const spans = fullTraces.get(traceId) ?? searchSpans;
|
|
6007
|
-
const root = spans.find((s) => !s.ParentSpanId) ?? spans[0];
|
|
6008
|
-
const durationNs = root.Duration ? parseInt(root.Duration, 10) : 0;
|
|
6009
|
-
const svcMap = /* @__PURE__ */ new Map();
|
|
6010
|
-
let errorCount = 0;
|
|
6011
|
-
for (const s of spans) {
|
|
6012
|
-
const svcName = s.ServiceName ?? "unknown";
|
|
6013
|
-
const entry = svcMap.get(svcName) ?? {
|
|
6014
|
-
count: 0,
|
|
6015
|
-
hasError: false
|
|
6016
|
-
};
|
|
6017
|
-
entry.count++;
|
|
6018
|
-
if (s.StatusCode === "ERROR") {
|
|
6019
|
-
entry.hasError = true;
|
|
6020
|
-
errorCount++;
|
|
6021
|
-
}
|
|
6022
|
-
svcMap.set(svcName, entry);
|
|
6023
|
-
}
|
|
6024
|
-
const services = Array.from(svcMap.entries()).map(([name, v]) => ({
|
|
6025
|
-
name,
|
|
6026
|
-
count: v.count,
|
|
6027
|
-
hasError: v.hasError
|
|
6028
|
-
})).sort((a, b) => b.count - a.count);
|
|
6029
|
-
return {
|
|
6030
|
-
traceId,
|
|
6031
|
-
rootSpanName: root.SpanName ?? "unknown",
|
|
6032
|
-
serviceName: root.ServiceName ?? "unknown",
|
|
6033
|
-
durationMs: durationNs / 1e6,
|
|
6034
|
-
statusCode: root.StatusCode ?? "UNSET",
|
|
6035
|
-
timestampMs: parseInt(root.Timestamp, 10) / 1e6,
|
|
6036
|
-
spanCount: spans.length,
|
|
6037
|
-
services,
|
|
6038
|
-
errorCount
|
|
6039
|
-
};
|
|
6040
|
-
});
|
|
6041
|
-
}, [data, fullTraces]),
|
|
7324
|
+
services: _services,
|
|
7325
|
+
service: service ?? "",
|
|
7326
|
+
traces,
|
|
6042
7327
|
operations,
|
|
6043
7328
|
isLoading: loading,
|
|
6044
7329
|
error: error ?? void 0,
|
|
6045
7330
|
onSelectTrace,
|
|
6046
|
-
|
|
7331
|
+
onCompare,
|
|
6047
7332
|
onSearch: handleSearch
|
|
6048
7333
|
});
|
|
6049
7334
|
}
|
|
6050
|
-
function TraceDetailView({
|
|
7335
|
+
function TraceDetailView({ traceId, selectedSpanId, onSelectSpan, onDeselectSpan, onBack }) {
|
|
6051
7336
|
const { data, loading, error } = useKopaiData(useMemo(() => ({
|
|
6052
7337
|
method: "getTrace",
|
|
6053
7338
|
params: { traceId }
|
|
6054
7339
|
}), [traceId]));
|
|
6055
7340
|
return /* @__PURE__ */ jsx(TraceDetail, {
|
|
6056
|
-
service,
|
|
6057
7341
|
traceId,
|
|
6058
7342
|
rows: data ?? [],
|
|
6059
7343
|
isLoading: loading,
|
|
6060
7344
|
error: error ?? void 0,
|
|
6061
7345
|
selectedSpanId: selectedSpanId ?? void 0,
|
|
6062
7346
|
onSpanClick: (span) => onSelectSpan(span.spanId),
|
|
7347
|
+
onSpanDeselect: onDeselectSpan,
|
|
6063
7348
|
onBack
|
|
6064
7349
|
});
|
|
6065
7350
|
}
|
|
6066
|
-
function ServicesTab({
|
|
7351
|
+
function ServicesTab({ selectedTraceId, selectedSpanId, compareParam, onSelectTrace, onSelectSpan, onDeselectSpan, onBack, onCompare }) {
|
|
6067
7352
|
useRegisterShortcuts("services-tab", SERVICES_SHORTCUTS);
|
|
6068
|
-
const
|
|
6069
|
-
|
|
6070
|
-
const backToTraceListRef = useRef(onBackToTraceList);
|
|
6071
|
-
backToTraceListRef.current = onBackToTraceList;
|
|
7353
|
+
const backRef = useRef(onBack);
|
|
7354
|
+
backRef.current = onBack;
|
|
6072
7355
|
useEffect(() => {
|
|
6073
7356
|
const handleKeyDown = (e) => {
|
|
6074
7357
|
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement || e.target instanceof HTMLSelectElement) return;
|
|
6075
7358
|
if (e.key === "Backspace") {
|
|
6076
7359
|
e.preventDefault();
|
|
6077
|
-
|
|
6078
|
-
else if (selectedService) backToServicesRef.current();
|
|
7360
|
+
backRef.current();
|
|
6079
7361
|
}
|
|
6080
7362
|
};
|
|
6081
7363
|
window.addEventListener("keydown", handleKeyDown);
|
|
6082
7364
|
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
6083
|
-
}, [
|
|
6084
|
-
if (
|
|
6085
|
-
|
|
7365
|
+
}, []);
|
|
7366
|
+
if (compareParam) {
|
|
7367
|
+
const [traceIdA, traceIdB] = compareParam.split(",");
|
|
7368
|
+
if (traceIdA && traceIdB) return /* @__PURE__ */ jsx(TraceComparison, {
|
|
7369
|
+
traceIdA,
|
|
7370
|
+
traceIdB,
|
|
7371
|
+
onBack
|
|
7372
|
+
});
|
|
7373
|
+
}
|
|
7374
|
+
if (selectedTraceId) return /* @__PURE__ */ jsx(TraceDetailView, {
|
|
6086
7375
|
traceId: selectedTraceId,
|
|
6087
7376
|
selectedSpanId,
|
|
6088
7377
|
onSelectSpan,
|
|
6089
|
-
|
|
7378
|
+
onDeselectSpan,
|
|
7379
|
+
onBack
|
|
6090
7380
|
});
|
|
6091
|
-
|
|
6092
|
-
|
|
6093
|
-
|
|
6094
|
-
onSelectTrace
|
|
7381
|
+
return /* @__PURE__ */ jsx(TraceSearchView, {
|
|
7382
|
+
onSelectTrace,
|
|
7383
|
+
onCompare
|
|
6095
7384
|
});
|
|
6096
|
-
return /* @__PURE__ */ jsx(ServiceListView, { onSelect: onSelectService });
|
|
6097
7385
|
}
|
|
6098
|
-
const DASHBOARDS_API_BASE = "/dashboards";
|
|
6099
7386
|
const METRICS_TREE = {
|
|
6100
7387
|
root: "root",
|
|
6101
7388
|
elements: {
|
|
@@ -6156,14 +7443,12 @@ const METRICS_TREE = {
|
|
|
6156
7443
|
}
|
|
6157
7444
|
}
|
|
6158
7445
|
};
|
|
6159
|
-
function useDashboardTree(dashboardId) {
|
|
7446
|
+
function useDashboardTree(client, dashboardId) {
|
|
6160
7447
|
const { data, isFetching, error } = useQuery({
|
|
6161
7448
|
queryKey: ["dashboard-tree", dashboardId],
|
|
6162
7449
|
queryFn: async ({ signal }) => {
|
|
6163
|
-
const
|
|
6164
|
-
|
|
6165
|
-
const json = await res.json();
|
|
6166
|
-
const parsed = observabilityCatalog.uiTreeSchema.safeParse(json.uiTree);
|
|
7450
|
+
const dashboard = await client.getDashboard(dashboardId, { signal });
|
|
7451
|
+
const parsed = observabilityCatalog.uiTreeSchema.safeParse(dashboard.uiTree);
|
|
6167
7452
|
if (!parsed.success) {
|
|
6168
7453
|
const issue = parsed.error.issues[0];
|
|
6169
7454
|
const path = issue?.path.length ? issue.path.join(".") + ": " : "";
|
|
@@ -6182,7 +7467,7 @@ function useDashboardTree(dashboardId) {
|
|
|
6182
7467
|
function MetricsTab() {
|
|
6183
7468
|
const kopaiClient = useKopaiSDK();
|
|
6184
7469
|
const { dashboardId } = useURLState();
|
|
6185
|
-
const { loading, error, tree } = useDashboardTree(dashboardId);
|
|
7470
|
+
const { loading, error, tree } = useDashboardTree(kopaiClient, dashboardId);
|
|
6186
7471
|
if (loading) return /* @__PURE__ */ jsx("p", {
|
|
6187
7472
|
className: "text-muted-foreground text-sm",
|
|
6188
7473
|
children: "Loading dashboard..."
|
|
@@ -6203,40 +7488,56 @@ function getDefaultClient() {
|
|
|
6203
7488
|
}
|
|
6204
7489
|
function ObservabilityPage({ client }) {
|
|
6205
7490
|
const activeClient = client ?? getDefaultClient();
|
|
6206
|
-
const { tab: activeTab,
|
|
7491
|
+
const { tab: activeTab, trace: selectedTraceId, span: selectedSpanId, compare: compareParam } = useURLState();
|
|
6207
7492
|
const handleTabChange = useCallback((tab) => {
|
|
6208
7493
|
pushURLState({ tab });
|
|
6209
7494
|
}, []);
|
|
6210
|
-
const handleSelectService = useCallback((service) => {
|
|
6211
|
-
pushURLState({
|
|
6212
|
-
tab: "services",
|
|
6213
|
-
service
|
|
6214
|
-
});
|
|
6215
|
-
}, []);
|
|
6216
7495
|
const handleSelectTrace = useCallback((traceId) => {
|
|
6217
7496
|
pushURLState({
|
|
7497
|
+
...readURLState(),
|
|
6218
7498
|
tab: "services",
|
|
6219
|
-
service: selectedService,
|
|
6220
7499
|
trace: traceId
|
|
6221
7500
|
});
|
|
6222
|
-
}, [
|
|
7501
|
+
}, []);
|
|
6223
7502
|
const handleSelectSpan = useCallback((spanId) => {
|
|
6224
7503
|
pushURLState({
|
|
7504
|
+
...readURLState(),
|
|
6225
7505
|
tab: "services",
|
|
6226
|
-
service: selectedService,
|
|
6227
|
-
trace: selectedTraceId,
|
|
6228
7506
|
span: spanId
|
|
6229
7507
|
}, { replace: true });
|
|
6230
|
-
}, [selectedService, selectedTraceId]);
|
|
6231
|
-
const handleBackToServices = useCallback(() => {
|
|
6232
|
-
pushURLState({ tab: "services" });
|
|
6233
7508
|
}, []);
|
|
6234
|
-
const
|
|
7509
|
+
const handleDeselectSpan = useCallback(() => {
|
|
7510
|
+
pushURLState({
|
|
7511
|
+
...readURLState(),
|
|
7512
|
+
span: null
|
|
7513
|
+
}, { replace: true });
|
|
7514
|
+
}, []);
|
|
7515
|
+
const handleCompare = useCallback((traceIds) => {
|
|
6235
7516
|
pushURLState({
|
|
7517
|
+
...readURLState(),
|
|
6236
7518
|
tab: "services",
|
|
6237
|
-
|
|
7519
|
+
trace: null,
|
|
7520
|
+
span: null,
|
|
7521
|
+
view: null,
|
|
7522
|
+
uiFind: null,
|
|
7523
|
+
viewStart: null,
|
|
7524
|
+
viewEnd: null,
|
|
7525
|
+
compare: traceIds.join(",")
|
|
6238
7526
|
});
|
|
6239
|
-
}, [
|
|
7527
|
+
}, []);
|
|
7528
|
+
const handleBack = useCallback(() => {
|
|
7529
|
+
pushURLState({
|
|
7530
|
+
...readURLState(),
|
|
7531
|
+
tab: "services",
|
|
7532
|
+
trace: null,
|
|
7533
|
+
span: null,
|
|
7534
|
+
view: null,
|
|
7535
|
+
uiFind: null,
|
|
7536
|
+
viewStart: null,
|
|
7537
|
+
viewEnd: null,
|
|
7538
|
+
compare: null
|
|
7539
|
+
});
|
|
7540
|
+
}, []);
|
|
6240
7541
|
return /* @__PURE__ */ jsx(KopaiSDKProvider, {
|
|
6241
7542
|
client: activeClient,
|
|
6242
7543
|
children: /* @__PURE__ */ jsx(KeyboardShortcutsProvider, {
|
|
@@ -6251,21 +7552,20 @@ function ObservabilityPage({ client }) {
|
|
|
6251
7552
|
}),
|
|
6252
7553
|
activeTab === "logs" && /* @__PURE__ */ jsx(LogsTab, {}),
|
|
6253
7554
|
activeTab === "services" && /* @__PURE__ */ jsx(ServicesTab, {
|
|
6254
|
-
selectedService,
|
|
6255
7555
|
selectedTraceId,
|
|
6256
7556
|
selectedSpanId,
|
|
6257
|
-
|
|
7557
|
+
compareParam,
|
|
6258
7558
|
onSelectTrace: handleSelectTrace,
|
|
6259
7559
|
onSelectSpan: handleSelectSpan,
|
|
6260
|
-
|
|
6261
|
-
|
|
7560
|
+
onDeselectSpan: handleDeselectSpan,
|
|
7561
|
+
onBack: handleBack,
|
|
7562
|
+
onCompare: handleCompare
|
|
6262
7563
|
}),
|
|
6263
7564
|
activeTab === "metrics" && /* @__PURE__ */ jsx(MetricsTab, {})
|
|
6264
7565
|
] })
|
|
6265
7566
|
})
|
|
6266
7567
|
});
|
|
6267
7568
|
}
|
|
6268
|
-
|
|
6269
7569
|
//#endregion
|
|
6270
7570
|
//#region src/lib/generate-prompt-instructions.ts
|
|
6271
7571
|
function formatPropType(prop) {
|
|
@@ -6395,7 +7695,7 @@ ${JSON.stringify(unifiedSchema)}
|
|
|
6395
7695
|
|
|
6396
7696
|
${JSON.stringify(exampleElements)}`;
|
|
6397
7697
|
}
|
|
6398
|
-
|
|
6399
7698
|
//#endregion
|
|
6400
7699
|
export { KopaiSDKProvider, ObservabilityPage, Renderer, createCatalog, createRendererFromCatalog, generatePromptInstructions, observabilityCatalog, useKopaiSDK };
|
|
7700
|
+
|
|
6401
7701
|
//# sourceMappingURL=index.mjs.map
|