@kopai/ui 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +2427 -1139
- package/dist/index.d.cts +36 -7
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +36 -7
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2376 -1082
- package/dist/index.mjs.map +1 -1
- package/package.json +13 -13
- package/src/components/KeyboardShortcuts/KeyboardShortcutsProvider.tsx +7 -7
- package/src/components/observability/DynamicDashboard/DynamicDashboard.test.tsx +5 -0
- package/src/components/observability/LogTimeline/LogFilter.tsx +5 -1
- package/src/components/observability/LogTimeline/index.tsx +6 -2
- package/src/components/observability/MetricHistogram/index.tsx +20 -19
- package/src/components/observability/MetricTimeSeries/index.tsx +8 -9
- package/src/components/observability/ServiceList/shortcuts.ts +1 -1
- package/src/components/observability/TraceComparison/index.tsx +332 -0
- package/src/components/observability/TraceDetail/TraceDetail.stories.tsx +0 -4
- package/src/components/observability/TraceDetail/index.tsx +4 -3
- package/src/components/observability/TraceSearch/DurationBar.tsx +38 -0
- package/src/components/observability/TraceSearch/ScatterPlot.tsx +135 -0
- package/src/components/observability/TraceSearch/SearchForm.tsx +195 -0
- package/src/components/observability/TraceSearch/SortDropdown.tsx +32 -0
- package/src/components/observability/TraceSearch/index.tsx +211 -218
- package/src/components/observability/TraceTimeline/DetailPane/EventsTab.tsx +1 -7
- package/src/components/observability/TraceTimeline/FlamegraphView.tsx +232 -0
- package/src/components/observability/TraceTimeline/GraphView.tsx +322 -0
- package/src/components/observability/TraceTimeline/Minimap.tsx +260 -0
- package/src/components/observability/TraceTimeline/SpanDetailInline.tsx +184 -0
- package/src/components/observability/TraceTimeline/SpanRow.tsx +14 -24
- package/src/components/observability/TraceTimeline/SpanSearch.tsx +76 -0
- package/src/components/observability/TraceTimeline/StatisticsView.tsx +223 -0
- package/src/components/observability/TraceTimeline/TimeRuler.tsx +54 -0
- package/src/components/observability/TraceTimeline/TimelineBar.tsx +18 -2
- package/src/components/observability/TraceTimeline/TraceHeader.tsx +116 -51
- package/src/components/observability/TraceTimeline/ViewTabs.tsx +34 -0
- package/src/components/observability/TraceTimeline/index.tsx +254 -110
- package/src/components/observability/index.ts +15 -0
- package/src/components/observability/renderers/OtelTraceDetail.tsx +1 -4
- package/src/components/observability/shared/TooltipEntryList.tsx +25 -0
- package/src/components/observability/utils/flatten-tree.ts +15 -0
- package/src/components/observability/utils/time.ts +9 -0
- package/src/hooks/use-kopai-data.test.ts +3 -0
- package/src/hooks/use-kopai-data.ts +11 -0
- package/src/hooks/use-live-logs.test.ts +3 -0
- package/src/lib/__snapshots__/generate-prompt-instructions.test.ts.snap +1 -1
- package/src/lib/component-catalog.ts +15 -0
- package/src/pages/observability.test.tsx +5 -0
- package/src/pages/observability.tsx +314 -235
- package/src/providers/kopai-provider.tsx +3 -0
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
|
}, []);
|
|
@@ -2010,23 +2968,77 @@ function TraceTimeline({ rows, onSpanClick, selectedSpanId: externalSelectedSpan
|
|
|
2010
2968
|
return next;
|
|
2011
2969
|
});
|
|
2012
2970
|
}, [selectedSpanId, flattenedSpans]);
|
|
2013
|
-
const
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
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
|
/**
|
|
@@ -4280,18 +5315,11 @@ function CustomTooltip({ active, payload, label, formatTime, formatValue, displa
|
|
|
4280
5315
|
children: [/* @__PURE__ */ jsx("p", {
|
|
4281
5316
|
className: "text-gray-400 text-xs mb-2",
|
|
4282
5317
|
children: formatTime(typeof label === "number" ? label : Number(label))
|
|
4283
|
-
}),
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
|
|
4287
|
-
|
|
4288
|
-
className: "font-medium",
|
|
4289
|
-
children: [displayLabelMap.get(entry.dataKey) ?? entry.dataKey, ":"]
|
|
4290
|
-
}),
|
|
4291
|
-
" ",
|
|
4292
|
-
formatValue(entry.value)
|
|
4293
|
-
]
|
|
4294
|
-
}, i))]
|
|
5318
|
+
}), /* @__PURE__ */ jsx(TooltipEntryList, {
|
|
5319
|
+
payload,
|
|
5320
|
+
displayLabelMap,
|
|
5321
|
+
formatValue
|
|
5322
|
+
})]
|
|
4295
5323
|
});
|
|
4296
5324
|
}
|
|
4297
5325
|
function MetricLoadingSkeleton({ height = 400 }) {
|
|
@@ -4338,7 +5366,6 @@ function MetricLoadingSkeleton({ height = 400 }) {
|
|
|
4338
5366
|
})
|
|
4339
5367
|
});
|
|
4340
5368
|
}
|
|
4341
|
-
|
|
4342
5369
|
//#endregion
|
|
4343
5370
|
//#region src/components/observability/MetricHistogram/index.tsx
|
|
4344
5371
|
/**
|
|
@@ -4352,6 +5379,9 @@ const COLORS = [
|
|
|
4352
5379
|
"#00C49F",
|
|
4353
5380
|
"#0088FE"
|
|
4354
5381
|
];
|
|
5382
|
+
function isBucketData(v) {
|
|
5383
|
+
return typeof v === "object" && v !== null && "bucket" in v && "lowerBound" in v;
|
|
5384
|
+
}
|
|
4355
5385
|
const defaultFormatBucketLabel = (bound, index, bounds) => {
|
|
4356
5386
|
if (index === 0) return `≤${bound}`;
|
|
4357
5387
|
if (index === bounds.length) return `>${bounds[bounds.length - 1]}`;
|
|
@@ -4394,7 +5424,8 @@ function buildHistogramData(rows, formatLabel = defaultFormatBucketLabel) {
|
|
|
4394
5424
|
};
|
|
4395
5425
|
buckets.push(bucket);
|
|
4396
5426
|
}
|
|
4397
|
-
|
|
5427
|
+
const prev = bucket[seriesName];
|
|
5428
|
+
bucket[seriesName] = (typeof prev === "number" ? prev : 0) + count;
|
|
4398
5429
|
}
|
|
4399
5430
|
}
|
|
4400
5431
|
buckets.sort((a, b) => a.lowerBound - b.lowerBound);
|
|
@@ -4531,25 +5562,18 @@ function MetricHistogram({ rows, isLoading = false, error, height = 400, unit: u
|
|
|
4531
5562
|
}
|
|
4532
5563
|
function HistogramTooltip({ active, payload, formatValue, boundsScale, displayLabelMap }) {
|
|
4533
5564
|
if (!active || !payload?.length) return null;
|
|
4534
|
-
const
|
|
4535
|
-
if (!
|
|
5565
|
+
const raw = payload[0]?.payload;
|
|
5566
|
+
if (!isBucketData(raw)) return null;
|
|
4536
5567
|
return /* @__PURE__ */ jsxs("div", {
|
|
4537
5568
|
className: "bg-background border border-gray-700 rounded-lg p-3 shadow-lg",
|
|
4538
5569
|
children: [/* @__PURE__ */ jsxs("p", {
|
|
4539
5570
|
className: "text-gray-300 text-sm font-medium mb-2",
|
|
4540
|
-
children: ["Bucket: ", boundsScale ? `${formatDisplayValue(
|
|
4541
|
-
}),
|
|
4542
|
-
|
|
4543
|
-
|
|
4544
|
-
|
|
4545
|
-
|
|
4546
|
-
className: "font-medium",
|
|
4547
|
-
children: [displayLabelMap.get(entry.dataKey) ?? entry.dataKey, ":"]
|
|
4548
|
-
}),
|
|
4549
|
-
" ",
|
|
4550
|
-
formatValue(entry.value)
|
|
4551
|
-
]
|
|
4552
|
-
}, 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
|
+
})]
|
|
4553
5577
|
});
|
|
4554
5578
|
}
|
|
4555
5579
|
function HistogramLoadingSkeleton({ height = 400 }) {
|
|
@@ -4589,7 +5613,6 @@ function HistogramLoadingSkeleton({ height = 400 }) {
|
|
|
4589
5613
|
})
|
|
4590
5614
|
});
|
|
4591
5615
|
}
|
|
4592
|
-
|
|
4593
5616
|
//#endregion
|
|
4594
5617
|
//#region src/components/observability/MetricStat/index.tsx
|
|
4595
5618
|
/**
|
|
@@ -4782,7 +5805,6 @@ function TrendIndicator({ direction, value }) {
|
|
|
4782
5805
|
children: [arrow, value !== void 0 && ` ${Math.abs(value).toFixed(1)}%`]
|
|
4783
5806
|
});
|
|
4784
5807
|
}
|
|
4785
|
-
|
|
4786
5808
|
//#endregion
|
|
4787
5809
|
//#region src/components/observability/MetricTable/index.tsx
|
|
4788
5810
|
/**
|
|
@@ -4922,7 +5944,6 @@ function MetricTable({ rows, isLoading = false, error, maxRows = 100, formatValu
|
|
|
4922
5944
|
})]
|
|
4923
5945
|
});
|
|
4924
5946
|
}
|
|
4925
|
-
|
|
4926
5947
|
//#endregion
|
|
4927
5948
|
//#region src/lib/renderer.tsx
|
|
4928
5949
|
/**
|
|
@@ -5027,7 +6048,6 @@ function Renderer({ tree, registry, fallback }) {
|
|
|
5027
6048
|
fallback
|
|
5028
6049
|
});
|
|
5029
6050
|
}
|
|
5030
|
-
|
|
5031
6051
|
//#endregion
|
|
5032
6052
|
//#region src/lib/catalog.ts
|
|
5033
6053
|
const dashboardCatalog = createCatalog({
|
|
@@ -5227,8 +6247,7 @@ const dashboardCatalog = createCatalog({
|
|
|
5227
6247
|
}
|
|
5228
6248
|
}
|
|
5229
6249
|
});
|
|
5230
|
-
|
|
5231
|
-
|
|
6250
|
+
Object.keys(dashboardCatalog.components);
|
|
5232
6251
|
//#endregion
|
|
5233
6252
|
//#region src/components/dashboard/Badge/index.tsx
|
|
5234
6253
|
function Badge({ element }) {
|
|
@@ -5252,7 +6271,6 @@ function Badge({ element }) {
|
|
|
5252
6271
|
children: text
|
|
5253
6272
|
});
|
|
5254
6273
|
}
|
|
5255
|
-
|
|
5256
6274
|
//#endregion
|
|
5257
6275
|
//#region src/components/dashboard/Card/index.tsx
|
|
5258
6276
|
function Card({ element, children }) {
|
|
@@ -5293,7 +6311,6 @@ function Card({ element, children }) {
|
|
|
5293
6311
|
})]
|
|
5294
6312
|
});
|
|
5295
6313
|
}
|
|
5296
|
-
|
|
5297
6314
|
//#endregion
|
|
5298
6315
|
//#region src/components/dashboard/Divider/index.tsx
|
|
5299
6316
|
function Divider({ element }) {
|
|
@@ -5331,7 +6348,6 @@ function Divider({ element }) {
|
|
|
5331
6348
|
margin: "16px 0"
|
|
5332
6349
|
} });
|
|
5333
6350
|
}
|
|
5334
|
-
|
|
5335
6351
|
//#endregion
|
|
5336
6352
|
//#region src/components/dashboard/Empty/index.tsx
|
|
5337
6353
|
function Empty({ element, onAction }) {
|
|
@@ -5374,7 +6390,6 @@ function Empty({ element, onAction }) {
|
|
|
5374
6390
|
]
|
|
5375
6391
|
});
|
|
5376
6392
|
}
|
|
5377
|
-
|
|
5378
6393
|
//#endregion
|
|
5379
6394
|
//#region src/components/dashboard/Grid/index.tsx
|
|
5380
6395
|
function Grid({ element, children }) {
|
|
@@ -5392,7 +6407,6 @@ function Grid({ element, children }) {
|
|
|
5392
6407
|
children
|
|
5393
6408
|
});
|
|
5394
6409
|
}
|
|
5395
|
-
|
|
5396
6410
|
//#endregion
|
|
5397
6411
|
//#region src/components/dashboard/Heading/index.tsx
|
|
5398
6412
|
function Heading({ element }) {
|
|
@@ -5411,7 +6425,6 @@ function Heading({ element }) {
|
|
|
5411
6425
|
children: text
|
|
5412
6426
|
});
|
|
5413
6427
|
}
|
|
5414
|
-
|
|
5415
6428
|
//#endregion
|
|
5416
6429
|
//#region src/components/dashboard/Stack/index.tsx
|
|
5417
6430
|
function Stack({ element, children }) {
|
|
@@ -5435,7 +6448,6 @@ function Stack({ element, children }) {
|
|
|
5435
6448
|
children
|
|
5436
6449
|
});
|
|
5437
6450
|
}
|
|
5438
|
-
|
|
5439
6451
|
//#endregion
|
|
5440
6452
|
//#region src/components/dashboard/Text/index.tsx
|
|
5441
6453
|
function Text({ element }) {
|
|
@@ -5454,7 +6466,6 @@ function Text({ element }) {
|
|
|
5454
6466
|
children: content
|
|
5455
6467
|
});
|
|
5456
6468
|
}
|
|
5457
|
-
|
|
5458
6469
|
//#endregion
|
|
5459
6470
|
//#region src/components/observability/renderers/OtelLogTimeline.tsx
|
|
5460
6471
|
function OtelLogTimeline(props) {
|
|
@@ -5476,7 +6487,6 @@ function OtelLogTimeline(props) {
|
|
|
5476
6487
|
})
|
|
5477
6488
|
});
|
|
5478
6489
|
}
|
|
5479
|
-
|
|
5480
6490
|
//#endregion
|
|
5481
6491
|
//#region src/components/observability/renderers/OtelMetricDiscovery.tsx
|
|
5482
6492
|
const TYPE_ORDER = {
|
|
@@ -5554,7 +6564,6 @@ function OtelMetricDiscovery(props) {
|
|
|
5554
6564
|
})
|
|
5555
6565
|
});
|
|
5556
6566
|
}
|
|
5557
|
-
|
|
5558
6567
|
//#endregion
|
|
5559
6568
|
//#region src/components/observability/renderers/OtelMetricHistogram.tsx
|
|
5560
6569
|
function OtelMetricHistogram(props) {
|
|
@@ -5575,7 +6584,6 @@ function OtelMetricHistogram(props) {
|
|
|
5575
6584
|
unit: props.element.props.unit ?? void 0
|
|
5576
6585
|
});
|
|
5577
6586
|
}
|
|
5578
|
-
|
|
5579
6587
|
//#endregion
|
|
5580
6588
|
//#region src/components/observability/renderers/OtelMetricStat.tsx
|
|
5581
6589
|
function OtelMetricStat(props) {
|
|
@@ -5596,7 +6604,6 @@ function OtelMetricStat(props) {
|
|
|
5596
6604
|
formatValue: formatOtelValue
|
|
5597
6605
|
});
|
|
5598
6606
|
}
|
|
5599
|
-
|
|
5600
6607
|
//#endregion
|
|
5601
6608
|
//#region src/components/observability/renderers/OtelMetricTable.tsx
|
|
5602
6609
|
function OtelMetricTable(props) {
|
|
@@ -5615,7 +6622,6 @@ function OtelMetricTable(props) {
|
|
|
5615
6622
|
maxRows: props.element.props.maxRows ?? 100
|
|
5616
6623
|
});
|
|
5617
6624
|
}
|
|
5618
|
-
|
|
5619
6625
|
//#endregion
|
|
5620
6626
|
//#region src/components/observability/renderers/OtelMetricTimeSeries.tsx
|
|
5621
6627
|
function OtelMetricTimeSeries(props) {
|
|
@@ -5637,7 +6643,6 @@ function OtelMetricTimeSeries(props) {
|
|
|
5637
6643
|
unit: props.element.props.unit ?? void 0
|
|
5638
6644
|
});
|
|
5639
6645
|
}
|
|
5640
|
-
|
|
5641
6646
|
//#endregion
|
|
5642
6647
|
//#region src/components/observability/renderers/OtelTraceDetail.tsx
|
|
5643
6648
|
function OtelTraceDetail(props) {
|
|
@@ -5649,19 +6654,15 @@ function OtelTraceDetail(props) {
|
|
|
5649
6654
|
children: "No data source"
|
|
5650
6655
|
});
|
|
5651
6656
|
const rows = props.data?.data ?? [];
|
|
5652
|
-
const
|
|
5653
|
-
const service = firstRow?.ServiceName ?? "unknown";
|
|
5654
|
-
const traceId = firstRow?.TraceId ?? "";
|
|
6657
|
+
const traceId = rows[0]?.TraceId ?? "";
|
|
5655
6658
|
return /* @__PURE__ */ jsx(TraceDetail, {
|
|
5656
6659
|
rows,
|
|
5657
6660
|
isLoading: props.loading,
|
|
5658
6661
|
error: props.error ?? void 0,
|
|
5659
|
-
service,
|
|
5660
6662
|
traceId,
|
|
5661
6663
|
onBack: () => {}
|
|
5662
6664
|
});
|
|
5663
6665
|
}
|
|
5664
|
-
|
|
5665
6666
|
//#endregion
|
|
5666
6667
|
//#region src/components/observability/DynamicDashboard/index.tsx
|
|
5667
6668
|
const MetricsRenderer = createRendererFromCatalog(observabilityCatalog, {
|
|
@@ -5687,24 +6688,275 @@ function DynamicDashboard({ kopaiClient, uiTree }) {
|
|
|
5687
6688
|
children: /* @__PURE__ */ jsx(MetricsRenderer, { tree: uiTree })
|
|
5688
6689
|
});
|
|
5689
6690
|
}
|
|
5690
|
-
|
|
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
|
+
}
|
|
5691
6944
|
//#endregion
|
|
5692
6945
|
//#region src/components/observability/ServiceList/shortcuts.ts
|
|
5693
6946
|
const SERVICES_SHORTCUTS = {
|
|
5694
|
-
name: "
|
|
6947
|
+
name: "Traces",
|
|
5695
6948
|
shortcuts: [{
|
|
5696
6949
|
keys: ["Backspace"],
|
|
5697
6950
|
description: "Go back"
|
|
5698
6951
|
}]
|
|
5699
6952
|
};
|
|
5700
|
-
|
|
5701
6953
|
//#endregion
|
|
5702
6954
|
//#region src/pages/observability.tsx
|
|
5703
6955
|
const TABS = [
|
|
5704
6956
|
{
|
|
5705
6957
|
key: "services",
|
|
5706
|
-
label: "
|
|
5707
|
-
shortcutKey: "
|
|
6958
|
+
label: "Traces",
|
|
6959
|
+
shortcutKey: "T"
|
|
5708
6960
|
},
|
|
5709
6961
|
{
|
|
5710
6962
|
key: "logs",
|
|
@@ -5724,20 +6976,53 @@ function readURLState() {
|
|
|
5724
6976
|
const span = params.get("span");
|
|
5725
6977
|
const dashboardId = params.get("dashboardId");
|
|
5726
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;
|
|
5727
6982
|
return {
|
|
5728
|
-
tab
|
|
6983
|
+
tab,
|
|
5729
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"),
|
|
5730
6994
|
trace,
|
|
5731
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"),
|
|
5732
7001
|
dashboardId
|
|
5733
7002
|
};
|
|
5734
7003
|
}
|
|
5735
7004
|
function pushURLState(state, { replace = false } = {}) {
|
|
5736
7005
|
const params = new URLSearchParams();
|
|
5737
7006
|
if (state.tab !== "services") params.set("tab", state.tab);
|
|
5738
|
-
if (state.
|
|
5739
|
-
|
|
5740
|
-
|
|
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
|
+
}
|
|
5741
7026
|
const dashboardId = state.dashboardId !== void 0 ? state.dashboardId : new URLSearchParams(window.location.search).get("dashboardId");
|
|
5742
7027
|
if (dashboardId) params.set("dashboardId", dashboardId);
|
|
5743
7028
|
const qs = params.toString();
|
|
@@ -5754,8 +7039,22 @@ let _cachedSearch = "";
|
|
|
5754
7039
|
let _cachedState = {
|
|
5755
7040
|
tab: "services",
|
|
5756
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,
|
|
5757
7051
|
trace: null,
|
|
5758
7052
|
span: null,
|
|
7053
|
+
view: null,
|
|
7054
|
+
uiFind: null,
|
|
7055
|
+
compare: null,
|
|
7056
|
+
viewStart: null,
|
|
7057
|
+
viewEnd: null,
|
|
5759
7058
|
dashboardId: null
|
|
5760
7059
|
};
|
|
5761
7060
|
function getURLSnapshot() {
|
|
@@ -5865,6 +7164,26 @@ function parseDuration(input) {
|
|
|
5865
7164
|
s: 1e9
|
|
5866
7165
|
}[unit]));
|
|
5867
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
|
+
};
|
|
5868
7187
|
function LogsTab() {
|
|
5869
7188
|
const [initState] = useState(() => readLogFilters());
|
|
5870
7189
|
const [filters, setFilters] = useState(initState.filters);
|
|
@@ -5924,185 +7243,145 @@ function LogsTab() {
|
|
|
5924
7243
|
})]
|
|
5925
7244
|
});
|
|
5926
7245
|
}
|
|
5927
|
-
|
|
5928
|
-
|
|
5929
|
-
|
|
5930
|
-
|
|
5931
|
-
sortOrder: "DESC"
|
|
5932
|
-
}
|
|
5933
|
-
};
|
|
5934
|
-
function ServiceListView({ onSelect }) {
|
|
5935
|
-
const { data, loading, error } = useKopaiData(SERVICES_DS);
|
|
5936
|
-
return /* @__PURE__ */ jsx(ServiceList, {
|
|
5937
|
-
services: useMemo(() => {
|
|
5938
|
-
if (!data?.data) return [];
|
|
5939
|
-
const names = /* @__PURE__ */ new Set();
|
|
5940
|
-
for (const row of data.data) names.add(row.ServiceName ?? "unknown");
|
|
5941
|
-
return Array.from(names).sort().map((name) => ({ name }));
|
|
5942
|
-
}, [data]),
|
|
5943
|
-
isLoading: loading,
|
|
5944
|
-
error: error ?? void 0,
|
|
5945
|
-
onSelect
|
|
5946
|
-
});
|
|
5947
|
-
}
|
|
5948
|
-
function TraceSearchView({ service, onBack, onSelectTrace }) {
|
|
5949
|
-
const [ds, setDs] = useState(() => ({
|
|
5950
|
-
method: "searchTracesPage",
|
|
5951
|
-
params: {
|
|
5952
|
-
serviceName: service,
|
|
5953
|
-
limit: 20,
|
|
5954
|
-
sortOrder: "DESC"
|
|
5955
|
-
}
|
|
5956
|
-
}));
|
|
5957
|
-
const handleSearch = useCallback((filters) => {
|
|
7246
|
+
function TraceSearchView({ onSelectTrace, onCompare }) {
|
|
7247
|
+
const urlState = useURLState();
|
|
7248
|
+
const service = urlState.service;
|
|
7249
|
+
const ds = useMemo(() => {
|
|
5958
7250
|
const params = {
|
|
5959
|
-
|
|
5960
|
-
limit: filters.limit,
|
|
7251
|
+
limit: urlState.limit ?? 20,
|
|
5961
7252
|
sortOrder: "DESC"
|
|
5962
7253
|
};
|
|
5963
|
-
if (
|
|
5964
|
-
if (
|
|
5965
|
-
if (
|
|
5966
|
-
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);
|
|
5967
7264
|
if (parsed) params.durationMin = parsed;
|
|
5968
7265
|
}
|
|
5969
|
-
if (
|
|
5970
|
-
const parsed = parseDuration(
|
|
7266
|
+
if (urlState.maxDuration) {
|
|
7267
|
+
const parsed = parseDuration(urlState.maxDuration);
|
|
5971
7268
|
if (parsed) params.durationMax = parsed;
|
|
5972
7269
|
}
|
|
5973
|
-
|
|
5974
|
-
|
|
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",
|
|
5975
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
|
|
5976
7299
|
});
|
|
5977
7300
|
}, [service]);
|
|
5978
7301
|
const { data, loading, error } = useKopaiData(ds);
|
|
5979
|
-
const
|
|
5980
|
-
const
|
|
5981
|
-
|
|
5982
|
-
|
|
5983
|
-
|
|
5984
|
-
|
|
5985
|
-
|
|
5986
|
-
|
|
5987
|
-
const ac = new AbortController();
|
|
5988
|
-
Promise.allSettled(traceIds.map((tid) => client.getTrace(tid, { signal: ac.signal }).then((spans) => [tid, spans]))).then((results) => {
|
|
5989
|
-
if (!ac.signal.aborted) {
|
|
5990
|
-
const entries = results.filter((r) => r.status === "fulfilled").map((r) => r.value);
|
|
5991
|
-
setFullTraces(new Map(entries));
|
|
5992
|
-
}
|
|
5993
|
-
}).catch((err) => {
|
|
5994
|
-
if (!ac.signal.aborted) console.error("Failed to fetch full traces", err);
|
|
5995
|
-
});
|
|
5996
|
-
return () => ac.abort();
|
|
5997
|
-
}, [data, client]);
|
|
5998
|
-
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(() => {
|
|
5999
7310
|
if (!data?.data) return [];
|
|
6000
|
-
|
|
6001
|
-
|
|
6002
|
-
|
|
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
|
+
}));
|
|
6003
7322
|
}, [data]);
|
|
6004
7323
|
return /* @__PURE__ */ jsx(TraceSearch, {
|
|
6005
|
-
|
|
6006
|
-
|
|
6007
|
-
|
|
6008
|
-
const grouped = /* @__PURE__ */ new Map();
|
|
6009
|
-
for (const row of data.data) {
|
|
6010
|
-
const tid = row.TraceId;
|
|
6011
|
-
if (!grouped.has(tid)) grouped.set(tid, []);
|
|
6012
|
-
grouped.get(tid).push(row);
|
|
6013
|
-
}
|
|
6014
|
-
return Array.from(grouped.entries()).map(([traceId, searchSpans]) => {
|
|
6015
|
-
const spans = fullTraces.get(traceId) ?? searchSpans;
|
|
6016
|
-
const root = spans.find((s) => !s.ParentSpanId) ?? spans[0];
|
|
6017
|
-
const durationNs = root.Duration ? parseInt(root.Duration, 10) : 0;
|
|
6018
|
-
const svcMap = /* @__PURE__ */ new Map();
|
|
6019
|
-
let errorCount = 0;
|
|
6020
|
-
for (const s of spans) {
|
|
6021
|
-
const svcName = s.ServiceName ?? "unknown";
|
|
6022
|
-
const entry = svcMap.get(svcName) ?? {
|
|
6023
|
-
count: 0,
|
|
6024
|
-
hasError: false
|
|
6025
|
-
};
|
|
6026
|
-
entry.count++;
|
|
6027
|
-
if (s.StatusCode === "ERROR") {
|
|
6028
|
-
entry.hasError = true;
|
|
6029
|
-
errorCount++;
|
|
6030
|
-
}
|
|
6031
|
-
svcMap.set(svcName, entry);
|
|
6032
|
-
}
|
|
6033
|
-
const services = Array.from(svcMap.entries()).map(([name, v]) => ({
|
|
6034
|
-
name,
|
|
6035
|
-
count: v.count,
|
|
6036
|
-
hasError: v.hasError
|
|
6037
|
-
})).sort((a, b) => b.count - a.count);
|
|
6038
|
-
return {
|
|
6039
|
-
traceId,
|
|
6040
|
-
rootSpanName: root.SpanName ?? "unknown",
|
|
6041
|
-
serviceName: root.ServiceName ?? "unknown",
|
|
6042
|
-
durationMs: durationNs / 1e6,
|
|
6043
|
-
statusCode: root.StatusCode ?? "UNSET",
|
|
6044
|
-
timestampMs: parseInt(root.Timestamp, 10) / 1e6,
|
|
6045
|
-
spanCount: spans.length,
|
|
6046
|
-
services,
|
|
6047
|
-
errorCount
|
|
6048
|
-
};
|
|
6049
|
-
});
|
|
6050
|
-
}, [data, fullTraces]),
|
|
7324
|
+
services: _services,
|
|
7325
|
+
service: service ?? "",
|
|
7326
|
+
traces,
|
|
6051
7327
|
operations,
|
|
6052
7328
|
isLoading: loading,
|
|
6053
7329
|
error: error ?? void 0,
|
|
6054
7330
|
onSelectTrace,
|
|
6055
|
-
|
|
7331
|
+
onCompare,
|
|
6056
7332
|
onSearch: handleSearch
|
|
6057
7333
|
});
|
|
6058
7334
|
}
|
|
6059
|
-
function TraceDetailView({
|
|
7335
|
+
function TraceDetailView({ traceId, selectedSpanId, onSelectSpan, onDeselectSpan, onBack }) {
|
|
6060
7336
|
const { data, loading, error } = useKopaiData(useMemo(() => ({
|
|
6061
7337
|
method: "getTrace",
|
|
6062
7338
|
params: { traceId }
|
|
6063
7339
|
}), [traceId]));
|
|
6064
7340
|
return /* @__PURE__ */ jsx(TraceDetail, {
|
|
6065
|
-
service,
|
|
6066
7341
|
traceId,
|
|
6067
7342
|
rows: data ?? [],
|
|
6068
7343
|
isLoading: loading,
|
|
6069
7344
|
error: error ?? void 0,
|
|
6070
7345
|
selectedSpanId: selectedSpanId ?? void 0,
|
|
6071
7346
|
onSpanClick: (span) => onSelectSpan(span.spanId),
|
|
7347
|
+
onSpanDeselect: onDeselectSpan,
|
|
6072
7348
|
onBack
|
|
6073
7349
|
});
|
|
6074
7350
|
}
|
|
6075
|
-
function ServicesTab({
|
|
7351
|
+
function ServicesTab({ selectedTraceId, selectedSpanId, compareParam, onSelectTrace, onSelectSpan, onDeselectSpan, onBack, onCompare }) {
|
|
6076
7352
|
useRegisterShortcuts("services-tab", SERVICES_SHORTCUTS);
|
|
6077
|
-
const
|
|
6078
|
-
|
|
6079
|
-
const backToTraceListRef = useRef(onBackToTraceList);
|
|
6080
|
-
backToTraceListRef.current = onBackToTraceList;
|
|
7353
|
+
const backRef = useRef(onBack);
|
|
7354
|
+
backRef.current = onBack;
|
|
6081
7355
|
useEffect(() => {
|
|
6082
7356
|
const handleKeyDown = (e) => {
|
|
6083
7357
|
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement || e.target instanceof HTMLSelectElement) return;
|
|
6084
7358
|
if (e.key === "Backspace") {
|
|
6085
7359
|
e.preventDefault();
|
|
6086
|
-
|
|
6087
|
-
else if (selectedService) backToServicesRef.current();
|
|
7360
|
+
backRef.current();
|
|
6088
7361
|
}
|
|
6089
7362
|
};
|
|
6090
7363
|
window.addEventListener("keydown", handleKeyDown);
|
|
6091
7364
|
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
6092
|
-
}, [
|
|
6093
|
-
if (
|
|
6094
|
-
|
|
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, {
|
|
6095
7375
|
traceId: selectedTraceId,
|
|
6096
7376
|
selectedSpanId,
|
|
6097
7377
|
onSelectSpan,
|
|
6098
|
-
|
|
7378
|
+
onDeselectSpan,
|
|
7379
|
+
onBack
|
|
6099
7380
|
});
|
|
6100
|
-
|
|
6101
|
-
|
|
6102
|
-
|
|
6103
|
-
onSelectTrace
|
|
7381
|
+
return /* @__PURE__ */ jsx(TraceSearchView, {
|
|
7382
|
+
onSelectTrace,
|
|
7383
|
+
onCompare
|
|
6104
7384
|
});
|
|
6105
|
-
return /* @__PURE__ */ jsx(ServiceListView, { onSelect: onSelectService });
|
|
6106
7385
|
}
|
|
6107
7386
|
const METRICS_TREE = {
|
|
6108
7387
|
root: "root",
|
|
@@ -6209,40 +7488,56 @@ function getDefaultClient() {
|
|
|
6209
7488
|
}
|
|
6210
7489
|
function ObservabilityPage({ client }) {
|
|
6211
7490
|
const activeClient = client ?? getDefaultClient();
|
|
6212
|
-
const { tab: activeTab,
|
|
7491
|
+
const { tab: activeTab, trace: selectedTraceId, span: selectedSpanId, compare: compareParam } = useURLState();
|
|
6213
7492
|
const handleTabChange = useCallback((tab) => {
|
|
6214
7493
|
pushURLState({ tab });
|
|
6215
7494
|
}, []);
|
|
6216
|
-
const handleSelectService = useCallback((service) => {
|
|
6217
|
-
pushURLState({
|
|
6218
|
-
tab: "services",
|
|
6219
|
-
service
|
|
6220
|
-
});
|
|
6221
|
-
}, []);
|
|
6222
7495
|
const handleSelectTrace = useCallback((traceId) => {
|
|
6223
7496
|
pushURLState({
|
|
7497
|
+
...readURLState(),
|
|
6224
7498
|
tab: "services",
|
|
6225
|
-
service: selectedService,
|
|
6226
7499
|
trace: traceId
|
|
6227
7500
|
});
|
|
6228
|
-
}, [
|
|
7501
|
+
}, []);
|
|
6229
7502
|
const handleSelectSpan = useCallback((spanId) => {
|
|
6230
7503
|
pushURLState({
|
|
7504
|
+
...readURLState(),
|
|
6231
7505
|
tab: "services",
|
|
6232
|
-
service: selectedService,
|
|
6233
|
-
trace: selectedTraceId,
|
|
6234
7506
|
span: spanId
|
|
6235
7507
|
}, { replace: true });
|
|
6236
|
-
}, [selectedService, selectedTraceId]);
|
|
6237
|
-
const handleBackToServices = useCallback(() => {
|
|
6238
|
-
pushURLState({ tab: "services" });
|
|
6239
7508
|
}, []);
|
|
6240
|
-
const
|
|
7509
|
+
const handleDeselectSpan = useCallback(() => {
|
|
7510
|
+
pushURLState({
|
|
7511
|
+
...readURLState(),
|
|
7512
|
+
span: null
|
|
7513
|
+
}, { replace: true });
|
|
7514
|
+
}, []);
|
|
7515
|
+
const handleCompare = useCallback((traceIds) => {
|
|
6241
7516
|
pushURLState({
|
|
7517
|
+
...readURLState(),
|
|
6242
7518
|
tab: "services",
|
|
6243
|
-
|
|
7519
|
+
trace: null,
|
|
7520
|
+
span: null,
|
|
7521
|
+
view: null,
|
|
7522
|
+
uiFind: null,
|
|
7523
|
+
viewStart: null,
|
|
7524
|
+
viewEnd: null,
|
|
7525
|
+
compare: traceIds.join(",")
|
|
6244
7526
|
});
|
|
6245
|
-
}, [
|
|
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
|
+
}, []);
|
|
6246
7541
|
return /* @__PURE__ */ jsx(KopaiSDKProvider, {
|
|
6247
7542
|
client: activeClient,
|
|
6248
7543
|
children: /* @__PURE__ */ jsx(KeyboardShortcutsProvider, {
|
|
@@ -6257,21 +7552,20 @@ function ObservabilityPage({ client }) {
|
|
|
6257
7552
|
}),
|
|
6258
7553
|
activeTab === "logs" && /* @__PURE__ */ jsx(LogsTab, {}),
|
|
6259
7554
|
activeTab === "services" && /* @__PURE__ */ jsx(ServicesTab, {
|
|
6260
|
-
selectedService,
|
|
6261
7555
|
selectedTraceId,
|
|
6262
7556
|
selectedSpanId,
|
|
6263
|
-
|
|
7557
|
+
compareParam,
|
|
6264
7558
|
onSelectTrace: handleSelectTrace,
|
|
6265
7559
|
onSelectSpan: handleSelectSpan,
|
|
6266
|
-
|
|
6267
|
-
|
|
7560
|
+
onDeselectSpan: handleDeselectSpan,
|
|
7561
|
+
onBack: handleBack,
|
|
7562
|
+
onCompare: handleCompare
|
|
6268
7563
|
}),
|
|
6269
7564
|
activeTab === "metrics" && /* @__PURE__ */ jsx(MetricsTab, {})
|
|
6270
7565
|
] })
|
|
6271
7566
|
})
|
|
6272
7567
|
});
|
|
6273
7568
|
}
|
|
6274
|
-
|
|
6275
7569
|
//#endregion
|
|
6276
7570
|
//#region src/lib/generate-prompt-instructions.ts
|
|
6277
7571
|
function formatPropType(prop) {
|
|
@@ -6401,7 +7695,7 @@ ${JSON.stringify(unifiedSchema)}
|
|
|
6401
7695
|
|
|
6402
7696
|
${JSON.stringify(exampleElements)}`;
|
|
6403
7697
|
}
|
|
6404
|
-
|
|
6405
7698
|
//#endregion
|
|
6406
7699
|
export { KopaiSDKProvider, ObservabilityPage, Renderer, createCatalog, createRendererFromCatalog, generatePromptInstructions, observabilityCatalog, useKopaiSDK };
|
|
7700
|
+
|
|
6407
7701
|
//# sourceMappingURL=index.mjs.map
|