mirror of
https://github.com/apache/skywalking-booster-ui.git
synced 2025-10-14 20:01:28 +00:00
feat: adapt new trace protocol and implement new trace view (#499)
This commit is contained in:
17
src/assets/icons/download.svg
Normal file
17
src/assets/icons/download.svg
Normal file
@@ -0,0 +1,17 @@
|
||||
<!-- Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
contributor license agreements. See the NOTICE file distributed with
|
||||
this work for additional information regarding copyright ownership.
|
||||
The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
(the "License"); you may not use this file except in compliance with
|
||||
the License. You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License. -->
|
||||
<svg t="1758874892311" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M731.428571 341.333333h73.142858a73.142857 73.142857 0 0 1 73.142857 73.142857v414.476191a73.142857 73.142857 0 0 1-73.142857 73.142857H219.428571a73.142857 73.142857 0 0 1-73.142857-73.142857V414.47619a73.142857 73.142857 0 0 1 73.142857-73.142857h73.142858v73.142857H219.428571v414.476191h585.142858V414.47619h-73.142858v-73.142857z m-176.90819-242.590476l0.048762 397.092572 84.577524-84.601905 51.687619 51.712-172.373334 172.397714-172.397714-172.373333 51.712-51.736381 83.626667 83.626666V98.742857h73.142857z" p-id="4697"></path>
|
||||
</svg>
|
After Width: | Height: | Size: 1.4 KiB |
@@ -310,6 +310,9 @@ limitations under the License. -->
|
||||
break;
|
||||
}
|
||||
dates.value = [start, end];
|
||||
if (!props.showButtons) {
|
||||
ok(true);
|
||||
}
|
||||
};
|
||||
const submit = () => {
|
||||
inputDates.value = dates.value;
|
||||
|
@@ -163,3 +163,73 @@ export const TraceSpansFromColdStage = {
|
||||
}
|
||||
`,
|
||||
};
|
||||
export const HasQueryTracesV2Support = {
|
||||
query: `
|
||||
hasQueryTracesV2Support
|
||||
`,
|
||||
};
|
||||
|
||||
export const QueryV2Traces = {
|
||||
variable: "$condition: TraceQueryCondition",
|
||||
query: `
|
||||
queryTraces(condition: $condition) {
|
||||
traces {
|
||||
spans {
|
||||
traceId
|
||||
segmentId
|
||||
spanId
|
||||
parentSpanId
|
||||
refs {
|
||||
traceId
|
||||
parentSegmentId
|
||||
parentSpanId
|
||||
type
|
||||
}
|
||||
serviceCode
|
||||
serviceInstanceName
|
||||
startTime
|
||||
endTime
|
||||
endpointName
|
||||
type
|
||||
peer
|
||||
component
|
||||
isError
|
||||
layer
|
||||
tags {
|
||||
key
|
||||
value
|
||||
}
|
||||
logs {
|
||||
time
|
||||
data {
|
||||
key
|
||||
value
|
||||
}
|
||||
}
|
||||
attachedEvents {
|
||||
startTime {
|
||||
seconds
|
||||
nanos
|
||||
}
|
||||
event
|
||||
endTime {
|
||||
seconds
|
||||
nanos
|
||||
}
|
||||
tags {
|
||||
key
|
||||
value
|
||||
}
|
||||
summary {
|
||||
key
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
retrievedTimeRange {
|
||||
startTime
|
||||
endTime
|
||||
}
|
||||
}`,
|
||||
};
|
||||
|
@@ -17,11 +17,45 @@
|
||||
import { httpQuery } from "../base";
|
||||
import { HttpURL } from "./url";
|
||||
|
||||
export default async function fetchQuery({ method, json, path }: { method: string; json?: unknown; path: string }) {
|
||||
export default async function fetchQuery({
|
||||
method,
|
||||
json,
|
||||
path,
|
||||
}: {
|
||||
method: string;
|
||||
json?: Record<string, unknown>;
|
||||
path: string;
|
||||
}) {
|
||||
const upperMethod = method.toUpperCase();
|
||||
let url = (HttpURL as Record<string, string>)[path];
|
||||
let body: unknown | undefined = json;
|
||||
|
||||
if (upperMethod === "GET" && json && typeof json === "object") {
|
||||
const params = new URLSearchParams();
|
||||
const stringifyValue = (val: unknown): string => {
|
||||
if (val instanceof Date) return val.toISOString();
|
||||
if (typeof val === "object") return JSON.stringify(val);
|
||||
return String(val);
|
||||
};
|
||||
for (const [key, value] of Object.entries(json)) {
|
||||
if (value === undefined || value === null) continue;
|
||||
if (Array.isArray(value)) {
|
||||
for (const v of value as unknown[]) params.append(key, stringifyValue(v));
|
||||
continue;
|
||||
}
|
||||
params.append(key, stringifyValue(value));
|
||||
}
|
||||
const queryString = params.toString();
|
||||
if (queryString) {
|
||||
url += (url.includes("?") ? "&" : "?") + queryString;
|
||||
}
|
||||
body = undefined;
|
||||
}
|
||||
|
||||
const response = await httpQuery({
|
||||
method,
|
||||
json,
|
||||
url: (HttpURL as { [key: string]: string })[path],
|
||||
method: upperMethod,
|
||||
json: body,
|
||||
url,
|
||||
});
|
||||
if (response.errors) {
|
||||
response.errors = response.errors.map((e: { message: string }) => e.message).join(" ");
|
||||
|
@@ -15,7 +15,15 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Traces, TraceSpans, TraceTagKeys, TraceTagValues, TraceSpansFromColdStage } from "../fragments/trace";
|
||||
import {
|
||||
Traces,
|
||||
TraceSpans,
|
||||
TraceTagKeys,
|
||||
TraceTagValues,
|
||||
TraceSpansFromColdStage,
|
||||
HasQueryTracesV2Support,
|
||||
QueryV2Traces,
|
||||
} from "../fragments/trace";
|
||||
|
||||
export const queryTraces = `query queryTraces(${Traces.variable}) {${Traces.query}}`;
|
||||
|
||||
@@ -26,3 +34,7 @@ export const queryTraceTagKeys = `query queryTraceTagKeys(${TraceTagKeys.variabl
|
||||
export const queryTraceTagValues = `query queryTraceTagValues(${TraceTagValues.variable}) {${TraceTagValues.query}}`;
|
||||
|
||||
export const queryTraceSpansFromColdStage = `query queryTraceSpansFromColdStage(${TraceSpansFromColdStage.variable}) {${TraceSpansFromColdStage.query}}`;
|
||||
|
||||
export const queryHasQueryTracesV2Support = `query queryHasQueryTracesV2Support {${HasQueryTracesV2Support.query}}`;
|
||||
|
||||
export const queryV2Traces = `query queryV2Traces(${QueryV2Traces.variable}) {${QueryV2Traces.query}}`;
|
||||
|
@@ -89,6 +89,7 @@ limitations under the License. -->
|
||||
import router from "@/router";
|
||||
import { useAppStoreWithOut, InitializationDurationRow } from "@/store/modules/app";
|
||||
import { useDashboardStore } from "@/store/modules/dashboard";
|
||||
import { useTraceStore } from "@/store/modules/trace";
|
||||
import type { DashboardItem } from "@/types/dashboard";
|
||||
import timeFormat from "@/utils/timeFormat";
|
||||
import { MetricCatalog } from "@/views/dashboard/data";
|
||||
@@ -98,10 +99,10 @@ limitations under the License. -->
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useRoute } from "vue-router";
|
||||
|
||||
/*global Indexable */
|
||||
const { t, te } = useI18n();
|
||||
const appStore = useAppStoreWithOut();
|
||||
const dashboardStore = useDashboardStore();
|
||||
const traceStore = useTraceStore();
|
||||
const route = useRoute();
|
||||
const pathNames = ref<{ path?: string; name: string; selected: boolean }[][]>([]);
|
||||
const showTimeRangeTips = ref<boolean>(false);
|
||||
@@ -124,6 +125,7 @@ limitations under the License. -->
|
||||
getVersion();
|
||||
getNavPaths();
|
||||
setTTL();
|
||||
traceStore.getHasQueryTracesV2Support();
|
||||
|
||||
function changeTheme() {
|
||||
const root = document.documentElement;
|
||||
@@ -391,7 +393,7 @@ limitations under the License. -->
|
||||
}
|
||||
|
||||
function resetDuration() {
|
||||
const { duration }: Indexable = route.params;
|
||||
const { duration } = route.params as { duration: string };
|
||||
if (duration) {
|
||||
const d = JSON.parse(duration);
|
||||
|
||||
|
@@ -17,7 +17,7 @@ limitations under the License. -->
|
||||
<div :class="isCollapse ? 'logo-icon-collapse' : 'logo-icon'">
|
||||
<Icon :size="isCollapse ? 'xl' : 'logo'" :iconName="isCollapse ? 'logo' : 'logo-sw'" />
|
||||
</div>
|
||||
<div class="menu scroll_bar_dark" :style="isCollapse ? {} : { width: '220px' }">
|
||||
<div class="menu scroll_bar_style" :style="isCollapse ? {} : { width: '220px' }">
|
||||
<el-menu
|
||||
active-text-color="#448dfe"
|
||||
background-color="#252a2f"
|
||||
|
@@ -31,7 +31,7 @@ const msg = {
|
||||
profiles: "Profiles",
|
||||
database: "Database",
|
||||
mySQL: "MySQL/MariaDB",
|
||||
serviceName: "Service Name",
|
||||
serviceName: "Service name",
|
||||
technologies: "Technologies",
|
||||
health: "Health",
|
||||
groupName: "Group Name",
|
||||
@@ -406,5 +406,11 @@ const msg = {
|
||||
minutes: "Minutes",
|
||||
invalidProfilingDurationRange: "Please enter a valid duration between 1 and 900 seconds",
|
||||
taskCreatedSuccessfully: "Task created successfully",
|
||||
runQuery: "Run Query",
|
||||
spansTable: "Spans Table",
|
||||
download: "Download",
|
||||
totalSpans: "Total Spans",
|
||||
spanName: "Span name",
|
||||
parentId: "Parent ID",
|
||||
};
|
||||
export default msg;
|
||||
|
@@ -406,5 +406,11 @@ const msg = {
|
||||
minutes: "Minutos",
|
||||
invalidProfilingDurationRange: "Por favor ingrese una duración válida entre 1 y 900 segundos",
|
||||
taskCreatedSuccessfully: "Tarea creada exitosamente",
|
||||
runQuery: "Ejecutar Consulta",
|
||||
spansTable: "Tabla de Lapso",
|
||||
download: "Descargar",
|
||||
totalSpans: "Total Lapso",
|
||||
spanName: "Nombre de Lapso",
|
||||
parentId: "ID Padre",
|
||||
};
|
||||
export default msg;
|
||||
|
@@ -404,5 +404,11 @@ const msg = {
|
||||
minutes: "分钟",
|
||||
invalidProfilingDurationRange: "请输入1到900秒之间的有效时长",
|
||||
taskCreatedSuccessfully: "任务创建成功",
|
||||
runQuery: "运行查询",
|
||||
spansTable: "Spans表格",
|
||||
download: "下载",
|
||||
totalSpans: "总跨度",
|
||||
spanName: "跨度名称",
|
||||
parentId: "父ID",
|
||||
};
|
||||
export default msg;
|
||||
|
@@ -37,9 +37,15 @@ interface TraceState {
|
||||
selectorStore: ReturnType<typeof useSelectorStore>;
|
||||
selectedSpan: Nullable<Span>;
|
||||
serviceList: string[];
|
||||
currentSpan: Nullable<Span>;
|
||||
hasQueryTracesV2Support: boolean;
|
||||
v2Traces: Trace[];
|
||||
loading: boolean;
|
||||
}
|
||||
const { getDurationTime } = useDuration();
|
||||
|
||||
export const PageSize = 20;
|
||||
|
||||
export const traceStore = defineStore({
|
||||
id: "trace",
|
||||
state: (): TraceState => ({
|
||||
@@ -54,24 +60,39 @@ export const traceStore = defineStore({
|
||||
queryDuration: getDurationTime(),
|
||||
traceState: "ALL",
|
||||
queryOrder: QueryOrders[0].value,
|
||||
paging: { pageNum: 1, pageSize: 20 },
|
||||
paging: { pageNum: 1, pageSize: PageSize },
|
||||
},
|
||||
traceSpanLogs: [],
|
||||
selectorStore: useSelectorStore(),
|
||||
serviceList: [],
|
||||
currentSpan: null,
|
||||
hasQueryTracesV2Support: false,
|
||||
v2Traces: [],
|
||||
loading: false,
|
||||
}),
|
||||
actions: {
|
||||
setTraceCondition(data: Recordable) {
|
||||
this.conditions = { ...this.conditions, ...data };
|
||||
},
|
||||
setCurrentTrace(trace: Trace) {
|
||||
this.currentTrace = trace;
|
||||
setCurrentTrace(trace: Nullable<Trace>) {
|
||||
this.currentTrace = trace || {};
|
||||
},
|
||||
setTraceSpans(spans: Span[]) {
|
||||
this.traceSpans = spans;
|
||||
},
|
||||
setSelectedSpan(span: Span) {
|
||||
this.selectedSpan = span;
|
||||
setSelectedSpan(span: Nullable<Span>) {
|
||||
this.selectedSpan = span || {};
|
||||
},
|
||||
setCurrentSpan(span: Nullable<Span>) {
|
||||
this.currentSpan = span || {};
|
||||
},
|
||||
setV2Spans(traceId: string) {
|
||||
const trace = this.traceList.find((d: Trace) => d.traceId === traceId);
|
||||
this.setTraceSpans(trace?.spans || []);
|
||||
this.serviceList = Array.from(new Set(trace?.spans.map((i: Span) => i.serviceCode)));
|
||||
},
|
||||
setTraceList(traces: Trace[]) {
|
||||
this.traceList = traces;
|
||||
},
|
||||
resetState() {
|
||||
this.traceSpans = [];
|
||||
@@ -156,14 +177,20 @@ export const traceStore = defineStore({
|
||||
return response;
|
||||
},
|
||||
async getTraces() {
|
||||
if (this.hasQueryTracesV2Support) {
|
||||
return this.fetchV2Traces();
|
||||
}
|
||||
this.loading = true;
|
||||
const response = await graphql.query("queryTraces").params({ condition: this.conditions });
|
||||
if (response.errors) {
|
||||
this.loading = false;
|
||||
return response;
|
||||
}
|
||||
if (!response.data.data.traces.length) {
|
||||
this.traceList = [];
|
||||
this.setCurrentTrace({});
|
||||
this.setTraceSpans([]);
|
||||
this.loading = false;
|
||||
return response;
|
||||
}
|
||||
this.getTraceSpans({ traceId: response.data.data.traces[0].traceIds[0] });
|
||||
@@ -177,8 +204,13 @@ export const traceStore = defineStore({
|
||||
return response;
|
||||
},
|
||||
async getTraceSpans(params: { traceId: string }) {
|
||||
if (this.hasQueryTracesV2Support) {
|
||||
this.setV2Spans(params.traceId);
|
||||
return new Promise((resolve) => resolve({}));
|
||||
}
|
||||
const appStore = useAppStoreWithOut();
|
||||
let response;
|
||||
this.loading = true;
|
||||
if (appStore.coldStageMode) {
|
||||
response = await graphql
|
||||
.query("queryTraceSpansFromColdStage")
|
||||
@@ -186,6 +218,7 @@ export const traceStore = defineStore({
|
||||
} else {
|
||||
response = await graphql.query("querySpans").params(params);
|
||||
}
|
||||
this.loading = false;
|
||||
if (response.errors) {
|
||||
return response;
|
||||
}
|
||||
@@ -209,6 +242,61 @@ export const traceStore = defineStore({
|
||||
async getTagValues(tagKey: string) {
|
||||
return await graphql.query("queryTraceTagValues").params({ tagKey, duration: useAppStoreWithOut().durationTime });
|
||||
},
|
||||
async getHasQueryTracesV2Support() {
|
||||
const response = await graphql.query("queryHasQueryTracesV2Support").params({});
|
||||
this.hasQueryTracesV2Support = response.data.hasQueryTracesV2Support;
|
||||
return response;
|
||||
},
|
||||
async fetchV2Traces() {
|
||||
this.loading = true;
|
||||
const response = await graphql.query("queryV2Traces").params({ condition: this.conditions });
|
||||
this.loading = false;
|
||||
if (response.errors) {
|
||||
this.traceList = [];
|
||||
this.setCurrentTrace({});
|
||||
this.setTraceSpans([]);
|
||||
return response;
|
||||
}
|
||||
this.v2Traces = response.data.queryTraces.traces || [];
|
||||
this.traceList = this.v2Traces
|
||||
.map((d: Trace) => {
|
||||
const newSpans = d.spans.map((span: Span) => {
|
||||
return {
|
||||
...span,
|
||||
traceId: span.traceId,
|
||||
duration: span.endTime - span.startTime,
|
||||
label: `${span.serviceCode}: ${span.endpointName}`,
|
||||
};
|
||||
});
|
||||
const trace =
|
||||
newSpans.find((span: Span) => span.parentSpanId === -1 && span.refs.length === 0) || newSpans[0];
|
||||
return {
|
||||
endpointNames: trace.endpointName ? [trace.endpointName] : [],
|
||||
traceIds: trace.traceId ? [{ value: trace.traceId, label: trace.traceId }] : [],
|
||||
start: trace.startTime,
|
||||
duration: trace.endTime - trace.startTime,
|
||||
isError: trace.isError,
|
||||
spans: newSpans,
|
||||
traceId: trace.traceId,
|
||||
key: trace.traceId,
|
||||
serviceCode: trace.serviceCode,
|
||||
label: `${trace.serviceCode}: ${trace.endpointName}`,
|
||||
};
|
||||
})
|
||||
.sort((a: Trace, b: Trace) => b.duration - a.duration);
|
||||
const trace = this.traceList[0];
|
||||
if (!trace) {
|
||||
this.traceList = [];
|
||||
this.setCurrentTrace({});
|
||||
this.setTraceSpans([]);
|
||||
return response;
|
||||
}
|
||||
|
||||
this.serviceList = Array.from(new Set(trace.spans.map((i: Span) => i.serviceCode)));
|
||||
this.setTraceSpans(trace.spans);
|
||||
this.setCurrentTrace(trace || {});
|
||||
return response;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@@ -185,13 +185,13 @@
|
||||
}
|
||||
|
||||
.scroll_bar_style::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
background-color: #eee;
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
background-color: var(--sw-scrollbar-track);
|
||||
}
|
||||
|
||||
.scroll_bar_style::-webkit-scrollbar-track {
|
||||
background-color: #eee;
|
||||
background-color: var(--sw-scrollbar-track);
|
||||
border-radius: 3px;
|
||||
box-shadow: inset 0 0 6px $disabled-color;
|
||||
}
|
||||
@@ -199,26 +199,9 @@
|
||||
.scroll_bar_style::-webkit-scrollbar-thumb {
|
||||
border-radius: 3px;
|
||||
box-shadow: inset 0 0 6px $disabled-color;
|
||||
background-color: #aaa;
|
||||
background-color: var(--sw-scrollbar-thumb);
|
||||
}
|
||||
|
||||
.scroll_bar_dark::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background-color: #666;
|
||||
}
|
||||
|
||||
.scroll_bar_dark::-webkit-scrollbar-track {
|
||||
background-color: #252a2f;
|
||||
border-radius: 3px;
|
||||
box-shadow: inset 0 0 6px #999;
|
||||
}
|
||||
|
||||
.scroll_bar_dark::-webkit-scrollbar-thumb {
|
||||
border-radius: 3px;
|
||||
box-shadow: inset 0 0 6px #888;
|
||||
background-color: #999;
|
||||
}
|
||||
.d3-tip {
|
||||
line-height: 1;
|
||||
padding: 8px;
|
||||
|
@@ -71,6 +71,11 @@ html {
|
||||
--sw-marketplace-border: #dedfe0;
|
||||
--sw-grid-item-active: #d4d7de;
|
||||
--sw-trace-line: #999;
|
||||
--sw-scrollbar-track: #eee;
|
||||
--sw-scrollbar-thumb: #aaa;
|
||||
--sw-font-grey-color: #a7aebb;
|
||||
--sw-trace-list-path: rgba(0, 0, 0, 0.1);
|
||||
--sw-trace-table-selected: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
html.dark {
|
||||
@@ -81,7 +86,7 @@ html.dark {
|
||||
--disabled-color: #999;
|
||||
--dashboard-tool-bg: #000;
|
||||
--text-color-placeholder: #ccc;
|
||||
--border-color: #262629;
|
||||
--border-color: #333;
|
||||
--border-color-primary: #4b4b52;
|
||||
--layout-background: #000;
|
||||
--box-shadow-color: #606266;
|
||||
@@ -114,6 +119,11 @@ html.dark {
|
||||
--sw-marketplace-border: #606266;
|
||||
--sw-grid-item-active: #73767a;
|
||||
--sw-trace-line: #e8e8e8;
|
||||
--sw-scrollbar-track: #252a2f;
|
||||
--sw-scrollbar-thumb: #888;
|
||||
--sw-font-grey-color: #a7aebb;
|
||||
--sw-trace-list-path: rgba(244, 244, 244, 0.4);
|
||||
--sw-trace-table-selected: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.el-drawer__header {
|
||||
|
@@ -27,6 +27,10 @@ export interface Trace {
|
||||
spans: Span[];
|
||||
endpointNames: string[];
|
||||
traceId: string;
|
||||
serviceCode: string;
|
||||
label: string;
|
||||
id: string;
|
||||
parentId: string;
|
||||
}
|
||||
|
||||
export interface Span {
|
||||
@@ -46,6 +50,7 @@ export interface Span {
|
||||
refs: Array<Ref>;
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
duration?: number;
|
||||
dur?: number;
|
||||
children?: Span[];
|
||||
tags?: { value: string; key: string }[];
|
||||
@@ -63,6 +68,8 @@ export interface Span {
|
||||
avgTime?: number;
|
||||
count?: number;
|
||||
profiled?: boolean;
|
||||
startTimeNanos?: number;
|
||||
event?: string;
|
||||
}
|
||||
export type Ref = {
|
||||
type?: string;
|
||||
|
70
src/utils/color.ts
Normal file
70
src/utils/color.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// Service color palette for consistent coloring across trace visualizations
|
||||
export const ServicePalette = [
|
||||
"#10b981", // emerald-500
|
||||
"#f59e0b", // amber-500
|
||||
"#8b5cf6", // violet-500
|
||||
"#06b6d4", // cyan-500
|
||||
"#84cc16", // lime-500
|
||||
"#f97316", // orange-500
|
||||
"#ec4899", // pink-500
|
||||
"#6366f1", // indigo-500
|
||||
"#14b8a6", // teal-500
|
||||
"#a855f7", // purple-500
|
||||
"#22c55e", // green-500
|
||||
"#eab308", // yellow-500
|
||||
"#f43f5e", // rose-500
|
||||
"#0ea5e9", // sky-500
|
||||
"#8b5a2b", // brown-500
|
||||
"#64748b", // slate-500
|
||||
"#dc2626", // red-600
|
||||
"#059669", // emerald-600
|
||||
"#d97706", // amber-600
|
||||
"#7c3aed", // violet-600
|
||||
"#0891b2", // cyan-600
|
||||
"#65a30d", // lime-600
|
||||
"#ea580ce6", // orange-600
|
||||
"#db2777", // pink-600
|
||||
"#4f46e5", // indigo-600
|
||||
"#0d9488", // teal-600
|
||||
"#9333ea", // purple-600
|
||||
"#16a34a", // green-600
|
||||
"#ca8a04", // yellow-600
|
||||
"#e11d48", // rose-600
|
||||
"#0284c7", // sky-600
|
||||
"#92400e", // brown-600
|
||||
"#475569", // slate-600
|
||||
];
|
||||
// Reuse the same service color function from TracesTable
|
||||
function generateHash(str: string) {
|
||||
let hash = 0;
|
||||
if (str.length === 0) return hash;
|
||||
for (let i = 0; i < str.length; i += 1) {
|
||||
const c = str.charCodeAt(i);
|
||||
hash = (hash << 5) - hash + c;
|
||||
hash |= 0; // Convert to 32bit integer
|
||||
}
|
||||
return Math.abs(hash); // Only positive number.
|
||||
}
|
||||
|
||||
export function getServiceColor(serviceName: string): string {
|
||||
if (!serviceName) return "#eee";
|
||||
const hash = generateHash(serviceName);
|
||||
return ServicePalette[hash % ServicePalette.length];
|
||||
}
|
@@ -31,16 +31,15 @@ export const readFile = (event: any) => {
|
||||
};
|
||||
});
|
||||
};
|
||||
export const saveFile = (data: any, name: string) => {
|
||||
const newData = JSON.stringify(data);
|
||||
const tagA = document.createElement("a");
|
||||
tagA.download = name;
|
||||
tagA.style.display = "none";
|
||||
const blob = new Blob([newData]);
|
||||
const url = URL.createObjectURL(blob);
|
||||
tagA.href = url;
|
||||
document.body.appendChild(tagA);
|
||||
tagA.click();
|
||||
document.body.removeChild(tagA);
|
||||
URL.revokeObjectURL(url);
|
||||
export const saveFileAsJSON = (data: unknown, filename: string) => {
|
||||
const jsonContent = JSON.stringify(data, null, 2);
|
||||
const blob = new Blob([jsonContent], { type: "application/json;charset=utf-8;" });
|
||||
const link = document.createElement("a");
|
||||
link.href = URL.createObjectURL(blob);
|
||||
link.download = filename;
|
||||
link.style.display = "none";
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(link.href);
|
||||
};
|
||||
|
@@ -179,7 +179,7 @@ limitations under the License. -->
|
||||
import { useDashboardStore } from "@/store/modules/dashboard";
|
||||
import router from "@/router";
|
||||
import type { DashboardItem, LayoutConfig } from "@/types/dashboard";
|
||||
import { saveFile, readFile } from "@/utils/file";
|
||||
import { saveFileAsJSON, readFile } from "@/utils/file";
|
||||
import { EntityType } from "./data";
|
||||
import { isEmptyObject } from "@/utils/is";
|
||||
import { WidgetType } from "@/views/dashboard/data";
|
||||
@@ -356,7 +356,7 @@ limitations under the License. -->
|
||||
optimizeTemplate(item.configuration.children);
|
||||
}
|
||||
const name = `dashboards.json`;
|
||||
saveFile(templates, name);
|
||||
saveFileAsJSON(templates, name);
|
||||
setTimeout(() => {
|
||||
multipleTableRef.value!.clearSelection();
|
||||
}, 2000);
|
||||
|
@@ -24,36 +24,16 @@ limitations under the License. -->
|
||||
<span>{{ t("delete") }}</span>
|
||||
</div>
|
||||
</el-popover>
|
||||
<div class="header">
|
||||
<Filter :needQuery="needQuery" :data="data" @get="getService" @search="popSegmentList" />
|
||||
</div>
|
||||
<div class="trace flex-h">
|
||||
<TraceList class="trace-list" :style="`width: ${currentWidth}px;`" />
|
||||
<div
|
||||
@mouseover="showIcon = true"
|
||||
@mouseout="showIcon = false"
|
||||
@mousedown="mousedown($event)"
|
||||
@mouseup="mouseup($event)"
|
||||
>
|
||||
<div class="trace-line" />
|
||||
<span class="trace-icon" v-show="showIcon" @mousedown="triggerArrow" @mouseup="stopObserve($event)">
|
||||
<Icon class="trace-arrow" :icon-name="isLeft ? 'chevron-left' : 'chevron-right'" size="lg" />
|
||||
</span>
|
||||
</div>
|
||||
<TraceDetail :serviceId="serviceId" />
|
||||
</div>
|
||||
<Content :data="data" />
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { provide, ref, onMounted, onUnmounted } from "vue";
|
||||
import { provide } from "vue";
|
||||
import type { PropType } from "vue";
|
||||
import Filter from "../related/trace/Filter.vue";
|
||||
import TraceList from "../related/trace/TraceList.vue";
|
||||
import TraceDetail from "../related/trace/Detail.vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useDashboardStore } from "@/store/modules/dashboard";
|
||||
import { mutationObserver } from "@/utils/mutation";
|
||||
import type { LayoutConfig } from "@/types/dashboard";
|
||||
import Content from "@/views/dashboard/related/trace/Content.vue";
|
||||
|
||||
/* global defineProps */
|
||||
const props = defineProps({
|
||||
@@ -65,76 +45,12 @@ limitations under the License. -->
|
||||
needQuery: { type: Boolean, default: true },
|
||||
});
|
||||
provide("options", props.data);
|
||||
const defaultWidth = 280,
|
||||
minArrowLeftWidth = 120;
|
||||
const serviceId = ref<string>("");
|
||||
const { t } = useI18n();
|
||||
const dashboardStore = useDashboardStore();
|
||||
const isLeft = ref<boolean>(true);
|
||||
const showIcon = ref<boolean>(false);
|
||||
const currentWidth = ref<number>(defaultWidth);
|
||||
const isDrag = ref<boolean>(false);
|
||||
|
||||
function removeWidget() {
|
||||
dashboardStore.removeControls(props.data);
|
||||
}
|
||||
function getService(id: string) {
|
||||
serviceId.value = id;
|
||||
}
|
||||
// When click the arrow, the width of the segment list is determined by the direction it points to.
|
||||
function triggerArrow() {
|
||||
currentWidth.value = isLeft.value ? 0 : defaultWidth;
|
||||
isLeft.value = !isLeft.value;
|
||||
startObserve();
|
||||
}
|
||||
function popSegmentList() {
|
||||
if (currentWidth.value >= defaultWidth) {
|
||||
return;
|
||||
}
|
||||
currentWidth.value = defaultWidth;
|
||||
isLeft.value = true;
|
||||
}
|
||||
function startObserve() {
|
||||
mutationObserver.observe("trigger-resize", document.querySelector(".trace-list")!, {
|
||||
attributes: true,
|
||||
attributeFilter: ["style"],
|
||||
});
|
||||
}
|
||||
function stopObserve(event: MouseEvent) {
|
||||
mutationObserver.disconnect("trigger-resize");
|
||||
event.stopPropagation();
|
||||
}
|
||||
const mousemove = (event: MouseEvent) => {
|
||||
if (!isDrag.value) {
|
||||
return;
|
||||
}
|
||||
const diffX = event.clientX;
|
||||
let leftWidth = document.querySelector(".trace-list")!.getBoundingClientRect();
|
||||
currentWidth.value = diffX - leftWidth.left;
|
||||
isLeft.value = currentWidth.value >= minArrowLeftWidth;
|
||||
};
|
||||
const mouseup = (event: MouseEvent) => {
|
||||
showIcon.value = false;
|
||||
isDrag.value = false;
|
||||
stopObserve(event);
|
||||
};
|
||||
const mousedown = (event: MouseEvent) => {
|
||||
if ((event.target as HTMLDivElement)?.className === "trace-line") {
|
||||
isDrag.value = true;
|
||||
startObserve();
|
||||
event.stopPropagation();
|
||||
}
|
||||
};
|
||||
onMounted(() => {
|
||||
document.addEventListener("mousedown", mousedown);
|
||||
document.addEventListener("mousemove", mousemove);
|
||||
document.addEventListener("mouseup", mouseup);
|
||||
});
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener("mousedown", mousedown);
|
||||
document.removeEventListener("mousemove", mousemove);
|
||||
document.removeEventListener("mouseup", mouseup);
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.trace-wrapper {
|
||||
@@ -150,13 +66,6 @@ limitations under the License. -->
|
||||
right: 3px;
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: 10px;
|
||||
font-size: $font-size-smaller;
|
||||
border-bottom: 1px solid $border-color;
|
||||
min-width: 1000px;
|
||||
}
|
||||
|
||||
.tools {
|
||||
padding: 5px 0;
|
||||
color: #999;
|
||||
@@ -169,47 +78,4 @@ limitations under the License. -->
|
||||
background-color: $popper-hover-bg-color;
|
||||
}
|
||||
}
|
||||
|
||||
.trace {
|
||||
min-height: calc(100% - 150px);
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
min-width: 1000px;
|
||||
}
|
||||
|
||||
.trace-list {
|
||||
max-width: 480px;
|
||||
}
|
||||
|
||||
.trace-line {
|
||||
position: relative;
|
||||
width: 2px;
|
||||
height: 100%;
|
||||
background-color: var(--sw-trace-line);
|
||||
cursor: ew-resize;
|
||||
|
||||
&:hover {
|
||||
color: $active-color;
|
||||
background-color: $active-background;
|
||||
}
|
||||
}
|
||||
|
||||
.trace-icon {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: calc(50% - 15px);
|
||||
text-align: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
transform: translateX(-11px);
|
||||
line-height: 24px;
|
||||
border-radius: 50%;
|
||||
background-color: $layout-background;
|
||||
box-shadow: 0 3px 5px rgb(45 60 80 / 20%);
|
||||
}
|
||||
|
||||
.trace-arrow {
|
||||
padding-bottom: 1px;
|
||||
color: $active-color;
|
||||
}
|
||||
</style>
|
||||
|
@@ -15,7 +15,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
export const dragIgnoreFrom =
|
||||
"svg.d3-trace-tree, .dragger, .micro-topo-chart, .schedules, .vis-item, .vis-timeline, .process-svg";
|
||||
"svg.d3-trace-tree, .dragger, .micro-topo-chart, .schedules, .vis-item, .vis-timeline, .process-svg, .trace-query-content";
|
||||
|
||||
export const PodsChartTypes = ["EndpointList", "InstanceList"];
|
||||
|
||||
|
@@ -16,7 +16,7 @@ limitations under the License. -->
|
||||
<div class="flex-h content">
|
||||
<TaskList />
|
||||
<div class="vis-graph ml-5">
|
||||
<div class="mb-20">
|
||||
<div class="mb-20 mt-10">
|
||||
<Filter />
|
||||
</div>
|
||||
<div class="stack" v-loading="asyncProfilingStore.loadingTree">
|
||||
|
@@ -41,7 +41,7 @@ limitations under the License. -->
|
||||
</div>
|
||||
<div class="profile-table">
|
||||
<Table
|
||||
:data="(profileStore.segmentSpans as SegmentSpan[])"
|
||||
:tableData="(profileStore.segmentSpans as SegmentSpan[])"
|
||||
:traceId="profileStore.currentSegment?.traceId"
|
||||
:headerType="WidgetType.Profile"
|
||||
@select="selectSpan"
|
||||
@@ -52,7 +52,7 @@ limitations under the License. -->
|
||||
<script lang="ts" setup>
|
||||
import { ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import Table from "../../trace/components/Table.vue";
|
||||
import Table from "@/views/dashboard/related/trace/components/Table/TableContainer.vue";
|
||||
import { useProfileStore } from "@/store/modules/profile";
|
||||
import Selector from "@/components/Selector.vue";
|
||||
import type { SegmentSpan, ProfileTimeRange, ProfileAnalyzeParams } from "@/types/profile";
|
||||
|
193
src/views/dashboard/related/trace/Content.vue
Normal file
193
src/views/dashboard/related/trace/Content.vue
Normal file
@@ -0,0 +1,193 @@
|
||||
<!-- Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
contributor license agreements. See the NOTICE file distributed with
|
||||
this work for additional information regarding copyright ownership.
|
||||
The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
(the "License"); you may not use this file except in compliance with
|
||||
the License. You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License. -->
|
||||
<template>
|
||||
<div class="search-bar">
|
||||
<Filter :needQuery="needQuery" :data="data" @get="getService" @search="popSegmentList" />
|
||||
<div class="filter-row flex-h mt-10" v-if="traceStore.hasQueryTracesV2Support">
|
||||
<div class="grey mr-10 label">{{ t("limit") }}</div>
|
||||
<el-input-number size="small" v-model="limit" :min="10" @change="changeLimit" />
|
||||
</div>
|
||||
</div>
|
||||
<TraceQuery v-if="traceStore.hasQueryTracesV2Support" style="height: 100%" />
|
||||
<div v-else class="trace flex-h">
|
||||
<SegmentList class="trace-list" :style="`width: ${currentWidth}px;`" />
|
||||
<div
|
||||
@mouseover="showIcon = true"
|
||||
@mouseout="showIcon = false"
|
||||
@mousedown="mousedown($event)"
|
||||
@mouseup="mouseup($event)"
|
||||
>
|
||||
<div class="trace-line" />
|
||||
<span class="trace-icon" v-show="showIcon" @mousedown="triggerArrow" @mouseup="stopObserve($event)">
|
||||
<Icon class="trace-arrow" :icon-name="isLeft ? 'chevron-left' : 'chevron-right'" size="lg" />
|
||||
</span>
|
||||
</div>
|
||||
<SpanList :serviceId="serviceId" />
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { provide, ref, onMounted, onUnmounted } from "vue";
|
||||
import type { PropType } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useTraceStore, PageSize } from "@/store/modules/trace";
|
||||
import Filter from "./components/TraceList/Filter.vue";
|
||||
import SegmentList from "./components/TraceList/SegmentList.vue";
|
||||
import SpanList from "./components/TraceList/SpanList.vue";
|
||||
import type { LayoutConfig } from "@/types/dashboard";
|
||||
import { mutationObserver } from "@/utils/mutation";
|
||||
import TraceQuery from "./components/TraceQuery/Index.vue";
|
||||
/*global defineProps */
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object as PropType<LayoutConfig>,
|
||||
default: () => ({}),
|
||||
},
|
||||
});
|
||||
provide("options", props.data);
|
||||
const { t } = useI18n();
|
||||
const traceStore = useTraceStore();
|
||||
const serviceId = ref<string>("");
|
||||
const showIcon = ref<boolean>(false);
|
||||
const isLeft = ref<boolean>(true);
|
||||
const currentWidth = ref<number>(280);
|
||||
const needQuery = ref<boolean>(true);
|
||||
const isDrag = ref<boolean>(false);
|
||||
const limit = ref(PageSize);
|
||||
const defaultWidth = 280;
|
||||
const minArrowLeftWidth = 120;
|
||||
|
||||
function getService(id: string) {
|
||||
serviceId.value = id;
|
||||
}
|
||||
function changeLimit(val: number | undefined) {
|
||||
if (!val) return;
|
||||
traceStore.setTraceCondition({
|
||||
paging: { pageNum: 1, pageSize: val },
|
||||
});
|
||||
}
|
||||
|
||||
// When click the arrow, the width of the segment list is determined by the direction it points to.
|
||||
function triggerArrow() {
|
||||
currentWidth.value = isLeft.value ? 0 : defaultWidth;
|
||||
isLeft.value = !isLeft.value;
|
||||
startObserve();
|
||||
}
|
||||
function popSegmentList() {
|
||||
if (currentWidth.value >= defaultWidth) {
|
||||
return;
|
||||
}
|
||||
currentWidth.value = defaultWidth;
|
||||
isLeft.value = true;
|
||||
}
|
||||
|
||||
function startObserve() {
|
||||
mutationObserver.observe("trigger-resize", document.querySelector(".trace-list")!, {
|
||||
attributes: true,
|
||||
attributeFilter: ["style"],
|
||||
});
|
||||
}
|
||||
function stopObserve(event: MouseEvent) {
|
||||
mutationObserver.disconnect("trigger-resize");
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
const mousemove = (event: MouseEvent) => {
|
||||
if (!isDrag.value) {
|
||||
return;
|
||||
}
|
||||
const diffX = event.clientX;
|
||||
let leftWidth = document.querySelector(".trace-list")!.getBoundingClientRect();
|
||||
currentWidth.value = diffX - leftWidth.left;
|
||||
isLeft.value = currentWidth.value >= minArrowLeftWidth;
|
||||
};
|
||||
const mouseup = (event: MouseEvent) => {
|
||||
showIcon.value = false;
|
||||
isDrag.value = false;
|
||||
stopObserve(event);
|
||||
};
|
||||
const mousedown = (event: MouseEvent) => {
|
||||
if ((event.target as HTMLDivElement)?.className === "trace-line") {
|
||||
isDrag.value = true;
|
||||
startObserve();
|
||||
event.stopPropagation();
|
||||
}
|
||||
};
|
||||
onMounted(() => {
|
||||
document.addEventListener("mousedown", mousedown);
|
||||
document.addEventListener("mousemove", mousemove);
|
||||
document.addEventListener("mouseup", mouseup);
|
||||
});
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener("mousedown", mousedown);
|
||||
document.removeEventListener("mousemove", mousemove);
|
||||
document.removeEventListener("mouseup", mouseup);
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.search-bar {
|
||||
padding: 10px;
|
||||
font-size: $font-size-smaller;
|
||||
border-bottom: 1px solid $border-color;
|
||||
min-width: 1000px;
|
||||
}
|
||||
|
||||
.trace {
|
||||
min-height: calc(100% - 160px);
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
min-width: 1000px;
|
||||
}
|
||||
|
||||
.trace-list {
|
||||
max-width: 480px;
|
||||
}
|
||||
|
||||
.trace-line {
|
||||
position: relative;
|
||||
width: 2px;
|
||||
height: 100%;
|
||||
background-color: var(--sw-trace-line);
|
||||
cursor: ew-resize;
|
||||
|
||||
&:hover {
|
||||
color: $active-color;
|
||||
background-color: $active-background;
|
||||
}
|
||||
}
|
||||
|
||||
.trace-icon {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: calc(50% - 15px);
|
||||
text-align: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
transform: translateX(-11px);
|
||||
line-height: 24px;
|
||||
border-radius: 50%;
|
||||
background-color: $layout-background;
|
||||
box-shadow: 0 3px 5px rgb(45 60 80 / 20%);
|
||||
}
|
||||
|
||||
.trace-arrow {
|
||||
padding-bottom: 1px;
|
||||
color: $active-color;
|
||||
}
|
||||
|
||||
.label {
|
||||
height: 28px;
|
||||
line-height: 28px;
|
||||
}
|
||||
</style>
|
@@ -14,27 +14,27 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License. -->
|
||||
<template>
|
||||
<div class="trace-wrapper flex-v">
|
||||
<div class="header">
|
||||
<Header :data="data" />
|
||||
<div class="search-bar">
|
||||
<SearchBar :data="data" />
|
||||
</div>
|
||||
<div class="trace flex-h">
|
||||
<TraceList />
|
||||
<TraceDetail />
|
||||
<SegmentList />
|
||||
<SpanList />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { provide } from "vue";
|
||||
import type { PropType } from "vue";
|
||||
import Header from "./Header.vue";
|
||||
import TraceList from "./TraceList.vue";
|
||||
import TraceDetail from "./Detail.vue";
|
||||
import type { LayoutConfig } from "@/types/dashboard";
|
||||
import SearchBar from "./components/TraceList/SearchBar.vue";
|
||||
import SegmentList from "./components/TraceList/SegmentList.vue";
|
||||
import SpanList from "./components/TraceList/SpanList.vue";
|
||||
/*global defineProps */
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object as PropType<LayoutConfig>,
|
||||
default: () => ({ graph: {} }),
|
||||
default: () => ({}),
|
||||
},
|
||||
});
|
||||
provide("options", props.data);
|
||||
@@ -47,18 +47,4 @@ limitations under the License. -->
|
||||
position: relative;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: 10px;
|
||||
font-size: $font-size-smaller;
|
||||
border-bottom: 1px solid $border-color;
|
||||
min-width: 1200px;
|
||||
}
|
||||
|
||||
.trace {
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
min-width: 1200px;
|
||||
height: calc(100% - 100px);
|
||||
}
|
||||
</style>
|
||||
|
@@ -20,6 +20,8 @@ limitations under the License. -->
|
||||
:type="type"
|
||||
:headerType="headerType"
|
||||
:traceId="traceId"
|
||||
:selectedMaxTimestamp="selectedMaxTimestamp"
|
||||
:selectedMinTimestamp="selectedMinTimestamp"
|
||||
@select="handleSelectSpan"
|
||||
>
|
||||
<div class="trace-tips" v-if="!segmentId.length">{{ $t("noData") }}</div>
|
||||
@@ -47,47 +49,74 @@ limitations under the License. -->
|
||||
</el-dialog>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch, onBeforeUnmount, onMounted } from "vue";
|
||||
import type { PropType } from "vue";
|
||||
import { ref, watch, onBeforeUnmount, onMounted, nextTick } from "vue";
|
||||
import * as d3 from "d3";
|
||||
import dayjs from "dayjs";
|
||||
import ListGraph from "../../utils/d3-trace-list";
|
||||
import TreeGraph from "../../utils/d3-trace-tree";
|
||||
import ListGraph from "./utils/d3-trace-list";
|
||||
import TreeGraph from "./utils/d3-trace-tree";
|
||||
import type { Span, Ref } from "@/types/trace";
|
||||
import SpanDetail from "./SpanDetail.vue";
|
||||
import TableContainer from "../Table/TableContainer.vue";
|
||||
import { useAppStoreWithOut } from "@/store/modules/app";
|
||||
import { useTraceStore } from "@/store/modules/trace";
|
||||
import { debounce } from "@/utils/debounce";
|
||||
import { mutationObserver } from "@/utils/mutation";
|
||||
import { TraceGraphType } from "../constant";
|
||||
import { Themes } from "@/constants/data";
|
||||
import { TraceGraphType } from "../VisGraph/constant";
|
||||
import type { SegmentSpan } from "@/types/profile";
|
||||
import { buildSegmentForest, collapseTree, getRefsAllNodes } from "./utils/helper";
|
||||
|
||||
/* global Recordable, Nullable */
|
||||
const props = defineProps({
|
||||
data: { type: Array as PropType<(Span | SegmentSpan)[]>, default: () => [] },
|
||||
traceId: { type: String, default: "" },
|
||||
type: { type: String, default: TraceGraphType.LIST },
|
||||
headerType: { type: String, default: "" },
|
||||
});
|
||||
const emits = defineEmits(["select"]);
|
||||
/* global Nullable */
|
||||
type Props = {
|
||||
data: (Span | SegmentSpan)[];
|
||||
traceId: string;
|
||||
type: string;
|
||||
headerType?: string;
|
||||
selectedMaxTimestamp?: number;
|
||||
selectedMinTimestamp?: number;
|
||||
minTimestamp: number;
|
||||
maxTimestamp: number;
|
||||
};
|
||||
type Emits = {
|
||||
(e: "select", value: Span): void;
|
||||
};
|
||||
const props = defineProps<Props>();
|
||||
const emits = defineEmits<Emits>();
|
||||
const appStore = useAppStoreWithOut();
|
||||
const loading = ref<boolean>(false);
|
||||
const showDetail = ref<boolean>(false);
|
||||
const fixSpansSize = ref<number>(0);
|
||||
const segmentId = ref<Recordable[]>([]);
|
||||
const segmentId = ref<Span[]>([]);
|
||||
const currentSpan = ref<Nullable<Span>>(null);
|
||||
const refSpans = ref<Array<Ref>>([]);
|
||||
const tree = ref<Nullable<any>>(null);
|
||||
const traceGraph = ref<Nullable<HTMLDivElement>>(null);
|
||||
const parentSpans = ref<Array<Span | SegmentSpan>>([]);
|
||||
const refParentSpans = ref<Array<Span | SegmentSpan>>([]);
|
||||
const traceStore = useTraceStore();
|
||||
const debounceFunc = debounce(draw, 500);
|
||||
const visDate = (date: number, pattern = "YYYY-MM-DD HH:mm:ss:SSS") => dayjs(date).format(pattern);
|
||||
// Store previous timestamp values to check for significant changes
|
||||
const prevSelectedMaxTimestamp = ref<number>(props.selectedMaxTimestamp || 0);
|
||||
const prevSelectedMinTimestamp = ref<number>(props.selectedMinTimestamp || 0);
|
||||
|
||||
onMounted(() => {
|
||||
const visDate = (date: number, pattern = "YYYY-MM-DD HH:mm:ss:SSS") => dayjs(date).format(pattern);
|
||||
// Debounced version of onSpanPanelToggled to prevent excessive re-renders
|
||||
const debouncedOnSpanPanelToggled = debounce(draw, 150);
|
||||
|
||||
// Check if timestamp change is significant enough to warrant a redraw
|
||||
function isTimestampChangeSignificant(newMax: number, newMin: number): boolean {
|
||||
const maxDiff = Math.abs(newMax - prevSelectedMaxTimestamp.value);
|
||||
const minDiff = Math.abs(newMin - prevSelectedMinTimestamp.value);
|
||||
const totalRange = props.maxTimestamp - props.minTimestamp;
|
||||
|
||||
// Consider change significant if it's more than 0.1% of the total range
|
||||
const threshold = totalRange * 0.001;
|
||||
|
||||
return maxDiff > threshold || minDiff > threshold;
|
||||
}
|
||||
onMounted(async () => {
|
||||
loading.value = true;
|
||||
changeTree();
|
||||
await nextTick();
|
||||
draw();
|
||||
loading.value = false;
|
||||
// monitor segment list width changes.
|
||||
@@ -96,11 +125,12 @@ limitations under the License. -->
|
||||
debounceFunc();
|
||||
});
|
||||
window.addEventListener("resize", debounceFunc);
|
||||
window.addEventListener("spanPanelToggled", draw);
|
||||
});
|
||||
|
||||
function draw() {
|
||||
if (props.type === TraceGraphType.TABLE) {
|
||||
segmentId.value = setLevel(segmentId.value);
|
||||
segmentId.value = setLevel(segmentId.value) as Span[];
|
||||
return;
|
||||
}
|
||||
if (!traceGraph.value) {
|
||||
@@ -108,21 +138,35 @@ limitations under the License. -->
|
||||
}
|
||||
d3.selectAll(".d3-tip").remove();
|
||||
if (props.type === TraceGraphType.LIST) {
|
||||
tree.value = new ListGraph(traceGraph.value, handleSelectSpan);
|
||||
tree.value.init(
|
||||
{ label: "TRACE_ROOT", children: segmentId.value },
|
||||
getRefsAllNodes({ label: "TRACE_ROOT", children: segmentId.value }),
|
||||
fixSpansSize.value,
|
||||
);
|
||||
tree.value = new ListGraph({ el: traceGraph.value, handleSelectSpan: handleSelectSpan });
|
||||
tree.value.init({
|
||||
data: { label: "TRACE_ROOT", children: segmentId.value },
|
||||
row: getRefsAllNodes({ label: "TRACE_ROOT", children: segmentId.value }),
|
||||
fixSpansSize: fixSpansSize.value,
|
||||
selectedMaxTimestamp: props.selectedMaxTimestamp,
|
||||
selectedMinTimestamp: props.selectedMinTimestamp,
|
||||
});
|
||||
tree.value.draw();
|
||||
selectInitialSpan();
|
||||
return;
|
||||
}
|
||||
if (props.type === TraceGraphType.TREE) {
|
||||
tree.value = new TreeGraph(traceGraph.value, handleSelectSpan);
|
||||
tree.value.init(
|
||||
{ label: `${props.traceId}`, children: segmentId.value },
|
||||
getRefsAllNodes({ label: "TRACE_ROOT", children: segmentId.value }),
|
||||
);
|
||||
tree.value = new TreeGraph({ el: traceGraph.value, handleSelectSpan });
|
||||
tree.value.init({
|
||||
data: { label: `${props.traceId}`, children: segmentId.value },
|
||||
row: getRefsAllNodes({ label: "TRACE_ROOT", children: segmentId.value }),
|
||||
selectedMaxTimestamp: props.selectedMaxTimestamp,
|
||||
selectedMinTimestamp: props.selectedMinTimestamp,
|
||||
});
|
||||
}
|
||||
}
|
||||
function selectInitialSpan() {
|
||||
if (segmentId.value && segmentId.value.length > 0) {
|
||||
const root = segmentId.value[0];
|
||||
traceStore.setCurrentSpan(root);
|
||||
if (tree.value && typeof tree.value.highlightSpan === "function") {
|
||||
tree.value.highlightSpan(root as any);
|
||||
}
|
||||
}
|
||||
}
|
||||
function handleSelectSpan(i: any) {
|
||||
@@ -162,7 +206,7 @@ limitations under the License. -->
|
||||
item && parentSpans.value.push(item);
|
||||
}
|
||||
}
|
||||
function viewParentSpan(span: Recordable) {
|
||||
function viewParentSpan(span: Span) {
|
||||
if (props.type === TraceGraphType.TABLE) {
|
||||
setTableSpanStyle(span);
|
||||
return;
|
||||
@@ -173,234 +217,34 @@ limitations under the License. -->
|
||||
showDetail.value = true;
|
||||
hideActionBox();
|
||||
}
|
||||
function setTableSpanStyle(span: Recordable) {
|
||||
const itemDom: any = document.querySelector(`.trace-item-${span.key}`);
|
||||
const items: any = document.querySelectorAll(".trace-item");
|
||||
function setTableSpanStyle(span: Span) {
|
||||
const itemDom: HTMLSpanElement | null = document.querySelector(`.trace-item-${span.key}`);
|
||||
const items: HTMLSpanElement[] = Array.from(document.querySelectorAll(".trace-item")) as HTMLSpanElement[];
|
||||
for (const item of items) {
|
||||
item.style.background = appStore.theme === Themes.Dark ? "#212224" : "#fff";
|
||||
item.style.background = "transparent";
|
||||
}
|
||||
if (itemDom) {
|
||||
itemDom.style.background = "var(--sw-trace-table-selected)";
|
||||
}
|
||||
itemDom.style.background = appStore.theme === Themes.Dark ? "rgba(255, 255, 255, 0.1)" : "rgba(0, 0, 0, 0.1)";
|
||||
hideActionBox();
|
||||
}
|
||||
function hideActionBox() {
|
||||
const box: any = document.querySelector("#trace-action-box");
|
||||
box.style.display = "none";
|
||||
}
|
||||
function traverseTree(node: Recordable, spanId: string, segmentId: string, data: Recordable) {
|
||||
if (!node || node.isBroken) {
|
||||
return;
|
||||
}
|
||||
if (node.spanId === spanId && node.segmentId === segmentId) {
|
||||
node.children.push(data);
|
||||
return;
|
||||
}
|
||||
for (const nodeItem of node.children || []) {
|
||||
traverseTree(nodeItem, spanId, segmentId, data);
|
||||
}
|
||||
}
|
||||
function changeTree() {
|
||||
if (props.data.length === 0) {
|
||||
if (!props.data.length) {
|
||||
return [];
|
||||
}
|
||||
segmentId.value = [];
|
||||
const segmentGroup: Recordable = {};
|
||||
const segmentIdGroup: string[] = [];
|
||||
const fixSpans: Span[] = [];
|
||||
const segmentHeaders: Span[] = [];
|
||||
for (const span of props.data) {
|
||||
if (span.refs.length) {
|
||||
refSpans.value.push(...span.refs);
|
||||
}
|
||||
if (span.parentSpanId === -1) {
|
||||
segmentHeaders.push(span);
|
||||
} else {
|
||||
const item = props.data.find(
|
||||
(i: Span) => i.traceId === span.traceId && i.segmentId === span.segmentId && i.spanId === span.spanId - 1,
|
||||
);
|
||||
const content = fixSpans.find(
|
||||
(i: Span) =>
|
||||
i.traceId === span.traceId &&
|
||||
i.segmentId === span.segmentId &&
|
||||
i.spanId === span.spanId - 1 &&
|
||||
i.parentSpanId === span.spanId - 2,
|
||||
);
|
||||
if (!item && !content) {
|
||||
fixSpans.push({
|
||||
traceId: span.traceId,
|
||||
segmentId: span.segmentId,
|
||||
spanId: span.spanId - 1,
|
||||
parentSpanId: span.spanId - 2,
|
||||
refs: [],
|
||||
endpointName: `VNode: ${span.segmentId}`,
|
||||
serviceCode: "VirtualNode",
|
||||
type: `[Broken] ${span.type}`,
|
||||
peer: "",
|
||||
component: `VirtualNode: #${span.spanId - 1}`,
|
||||
isError: true,
|
||||
isBroken: true,
|
||||
layer: "Broken",
|
||||
tags: [],
|
||||
logs: [],
|
||||
startTime: 0,
|
||||
endTime: 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const span of segmentHeaders) {
|
||||
if (span.refs.length) {
|
||||
let exit = null;
|
||||
for (const ref of span.refs) {
|
||||
const e = props.data.find(
|
||||
(i: Recordable) =>
|
||||
ref.traceId === i.traceId && ref.parentSegmentId === i.segmentId && ref.parentSpanId === i.spanId,
|
||||
);
|
||||
if (e) {
|
||||
exit = e;
|
||||
}
|
||||
}
|
||||
if (!exit) {
|
||||
const ref = span.refs[0];
|
||||
// create a known broken node.
|
||||
const parentSpanId = ref.parentSpanId > -1 ? 0 : -1;
|
||||
const content = fixSpans.find(
|
||||
(i: Span) =>
|
||||
i.traceId === ref.traceId &&
|
||||
i.segmentId === ref.parentSegmentId &&
|
||||
i.spanId === ref.parentSpanId &&
|
||||
i.parentSpanId === parentSpanId,
|
||||
);
|
||||
if (!content) {
|
||||
fixSpans.push({
|
||||
traceId: ref.traceId,
|
||||
segmentId: ref.parentSegmentId,
|
||||
spanId: ref.parentSpanId,
|
||||
parentSpanId,
|
||||
refs: [],
|
||||
endpointName: `VNode: ${ref.parentSegmentId}`,
|
||||
serviceCode: "VirtualNode",
|
||||
type: `[Broken] ${ref.type}`,
|
||||
peer: "",
|
||||
component: `VirtualNode: #${ref.parentSpanId}`,
|
||||
isError: true,
|
||||
isBroken: true,
|
||||
layer: "Broken",
|
||||
tags: [],
|
||||
logs: [],
|
||||
startTime: 0,
|
||||
endTime: 0,
|
||||
});
|
||||
}
|
||||
// if root broken node is not exist, create a root broken node.
|
||||
if (parentSpanId > -1) {
|
||||
const content = fixSpans.find(
|
||||
(i: Span) =>
|
||||
i.traceId === ref.traceId &&
|
||||
i.segmentId === ref.parentSegmentId &&
|
||||
i.spanId === 0 &&
|
||||
i.parentSpanId === -1,
|
||||
);
|
||||
if (!content) {
|
||||
fixSpans.push({
|
||||
traceId: ref.traceId,
|
||||
segmentId: ref.parentSegmentId,
|
||||
spanId: 0,
|
||||
parentSpanId: -1,
|
||||
refs: [],
|
||||
endpointName: `VNode: ${ref.parentSegmentId}`,
|
||||
serviceCode: "VirtualNode",
|
||||
type: `[Broken] ${ref.type}`,
|
||||
peer: "",
|
||||
component: `VirtualNode: #0`,
|
||||
isError: true,
|
||||
isBroken: true,
|
||||
layer: "Broken",
|
||||
tags: [],
|
||||
logs: [],
|
||||
startTime: 0,
|
||||
endTime: 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const i of [...fixSpans, ...props.data]) {
|
||||
i.label = i.endpointName || "no operation name";
|
||||
i.key = Math.random().toString(36).substring(2, 36);
|
||||
i.children = [];
|
||||
if (segmentGroup[i.segmentId]) {
|
||||
segmentGroup[i.segmentId].push(i);
|
||||
} else {
|
||||
segmentIdGroup.push(i.segmentId);
|
||||
segmentGroup[i.segmentId] = [i];
|
||||
}
|
||||
}
|
||||
fixSpansSize.value = fixSpans.length;
|
||||
for (const id of segmentIdGroup) {
|
||||
const currentSegment = segmentGroup[id].sort((a: Span, b: Span) => b.parentSpanId - a.parentSpanId);
|
||||
for (const s of currentSegment) {
|
||||
const index = currentSegment.findIndex((i: Span) => i.spanId === s.parentSpanId);
|
||||
if (index > -1) {
|
||||
if (
|
||||
(currentSegment[index].isBroken && currentSegment[index].parentSpanId === -1) ||
|
||||
!currentSegment[index].isBroken
|
||||
) {
|
||||
currentSegment[index].children.push(s);
|
||||
currentSegment[index].children.sort((a: Span, b: Span) => a.spanId - b.spanId);
|
||||
}
|
||||
}
|
||||
if (s.isBroken) {
|
||||
const children = props.data.filter((span: Span) =>
|
||||
span.refs.find(
|
||||
(d) => d.traceId === s.traceId && d.parentSegmentId === s.segmentId && d.parentSpanId === s.spanId,
|
||||
),
|
||||
);
|
||||
if (children.length) {
|
||||
s.children.push(...children);
|
||||
}
|
||||
}
|
||||
}
|
||||
segmentGroup[id] = currentSegment[currentSegment.length - 1];
|
||||
}
|
||||
for (const id of segmentIdGroup) {
|
||||
for (const ref of segmentGroup[id].refs) {
|
||||
if (ref.traceId === props.traceId) {
|
||||
traverseTree(segmentGroup[ref.parentSegmentId], ref.parentSpanId, ref.parentSegmentId, segmentGroup[id]);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const i in segmentGroup) {
|
||||
for (const ref of segmentGroup[i].refs) {
|
||||
if (!segmentGroup[ref.parentSegmentId]) {
|
||||
segmentId.value.push(segmentGroup[i]);
|
||||
}
|
||||
}
|
||||
if (!segmentGroup[i].refs.length && segmentGroup[i].parentSpanId === -1) {
|
||||
segmentId.value.push(segmentGroup[i]);
|
||||
}
|
||||
}
|
||||
for (const i of segmentId.value) {
|
||||
collapse(i);
|
||||
const { roots, fixSpansSize: fixSize, refSpans: refs } = buildSegmentForest(props.data as Span[], props.traceId);
|
||||
segmentId.value = roots;
|
||||
fixSpansSize.value = fixSize;
|
||||
refSpans.value = refs;
|
||||
for (const root of segmentId.value) {
|
||||
collapseTree(root, refSpans.value);
|
||||
}
|
||||
}
|
||||
function collapse(d: Span | Recordable) {
|
||||
if (d.children) {
|
||||
const item = refSpans.value.find((s: Ref) => s.parentSpanId === d.spanId && s.parentSegmentId === d.segmentId);
|
||||
let dur = d.endTime - d.startTime;
|
||||
for (const i of d.children) {
|
||||
dur -= i.endTime - i.startTime;
|
||||
}
|
||||
d.dur = dur < 0 ? 0 : dur;
|
||||
if (item) {
|
||||
d.children = d.children.sort(compare("startTime"));
|
||||
}
|
||||
for (const i of d.children) {
|
||||
collapse(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
function setLevel(arr: Recordable[], level = 1, totalExec?: number) {
|
||||
function setLevel(arr: Span[], level = 1, totalExec?: number) {
|
||||
for (const item of arr) {
|
||||
item.level = level;
|
||||
totalExec = totalExec || item.endTime - item.startTime;
|
||||
@@ -411,34 +255,12 @@ limitations under the License. -->
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
function getRefsAllNodes(tree: Recordable) {
|
||||
let nodes = [];
|
||||
let stack = [tree];
|
||||
|
||||
while (stack.length > 0) {
|
||||
const node = stack.pop();
|
||||
nodes.push(node);
|
||||
|
||||
if (node?.children && node.children.length > 0) {
|
||||
for (let i = node.children.length - 1; i >= 0; i--) {
|
||||
stack.push(node.children[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nodes;
|
||||
}
|
||||
function compare(p: string) {
|
||||
return (m: Recordable, n: Recordable) => {
|
||||
const a = m[p];
|
||||
const b = n[p];
|
||||
return a - b;
|
||||
};
|
||||
}
|
||||
onBeforeUnmount(() => {
|
||||
d3.selectAll(".d3-tip").remove();
|
||||
window.removeEventListener("resize", debounceFunc);
|
||||
mutationObserver.deleteObserve("trigger-resize");
|
||||
window.removeEventListener("spanPanelToggled", draw);
|
||||
});
|
||||
watch(
|
||||
() => props.data,
|
||||
@@ -455,7 +277,22 @@ limitations under the License. -->
|
||||
watch(
|
||||
() => appStore.theme,
|
||||
() => {
|
||||
tree.value.init({ label: "TRACE_ROOT", children: segmentId.value }, props.data, fixSpansSize.value);
|
||||
if (props.type === TraceGraphType.LIST) {
|
||||
tree.value.init({
|
||||
data: { label: "TRACE_ROOT", children: segmentId.value },
|
||||
row: getRefsAllNodes({ label: "TRACE_ROOT", children: segmentId.value }),
|
||||
fixSpansSize: fixSpansSize.value,
|
||||
selectedMaxTimestamp: props.selectedMaxTimestamp,
|
||||
selectedMinTimestamp: props.selectedMinTimestamp,
|
||||
});
|
||||
} else if (props.type === TraceGraphType.TREE) {
|
||||
tree.value.init({
|
||||
data: { label: `${props.traceId}`, children: segmentId.value },
|
||||
row: getRefsAllNodes({ label: "TRACE_ROOT", children: segmentId.value }),
|
||||
selectedMaxTimestamp: props.selectedMaxTimestamp,
|
||||
selectedMinTimestamp: props.selectedMinTimestamp,
|
||||
});
|
||||
}
|
||||
tree.value.draw(() => {
|
||||
setTimeout(() => {
|
||||
loading.value = false;
|
||||
@@ -463,6 +300,20 @@ limitations under the License. -->
|
||||
});
|
||||
},
|
||||
);
|
||||
watch(
|
||||
() => [props.selectedMaxTimestamp, props.selectedMinTimestamp],
|
||||
([newMax, newMin]) => {
|
||||
// Only trigger redraw if the change is significant
|
||||
if (isTimestampChangeSignificant(newMax as number, newMin as number)) {
|
||||
// Update previous values
|
||||
prevSelectedMaxTimestamp.value = newMax as number;
|
||||
prevSelectedMinTimestamp.value = newMin as number;
|
||||
|
||||
// Use debounced version to prevent excessive re-renders
|
||||
debouncedOnSpanPanelToggled();
|
||||
}
|
||||
},
|
||||
);
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.d3-graph {
|
||||
|
@@ -83,7 +83,7 @@ limitations under the License. -->
|
||||
ref="eventGraph"
|
||||
v-if="currentSpan.attachedEvents && currentSpan.attachedEvents.length"
|
||||
></div>
|
||||
<el-button class="popup-btn" type="primary" @click="getTaceLogs">
|
||||
<el-button class="popup-btn" @click="getTaceLogs">
|
||||
{{ t("relatedTraceLogs") }}
|
||||
</el-button>
|
||||
</div>
|
||||
@@ -149,9 +149,8 @@ limitations under the License. -->
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted, inject } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import type { PropType } from "vue";
|
||||
import dayjs from "dayjs";
|
||||
import ListGraph from "../../utils/d3-trace-list";
|
||||
import ListGraph from "./utils/d3-trace-list";
|
||||
import copy from "@/utils/copy";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { dateFormat } from "@/utils/dateFormat";
|
||||
@@ -163,10 +162,11 @@ limitations under the License. -->
|
||||
import { WidgetType } from "@/views/dashboard/data";
|
||||
import type { LayoutConfig, DashboardItem } from "@/types/dashboard";
|
||||
/*global defineProps, Nullable, Recordable */
|
||||
const props = defineProps({
|
||||
currentSpan: { type: Object as PropType<Span>, default: () => ({}) },
|
||||
traceId: { type: String, default: "" },
|
||||
});
|
||||
type Props = {
|
||||
currentSpan: Span;
|
||||
traceId?: string;
|
||||
};
|
||||
const props = defineProps<Props>();
|
||||
const options: LayoutConfig | null = inject("options") || null;
|
||||
const { t } = useI18n();
|
||||
const traceStore = useTraceStore();
|
||||
@@ -233,7 +233,7 @@ limitations under the License. -->
|
||||
return a.startTime - b.startTime;
|
||||
});
|
||||
|
||||
tree.value = new ListGraph(eventGraph.value, selectEvent);
|
||||
tree.value = new ListGraph({ el: eventGraph.value, handleSelectSpan: selectEvent });
|
||||
tree.value.init(
|
||||
{
|
||||
children: events,
|
||||
@@ -287,9 +287,9 @@ limitations under the License. -->
|
||||
}
|
||||
|
||||
.popup-btn {
|
||||
margin-top: 40px;
|
||||
width: 100%;
|
||||
margin-top: 20px;
|
||||
text-align: center;
|
||||
padding: 0 30px;
|
||||
}
|
||||
|
||||
.item span {
|
||||
|
@@ -17,13 +17,12 @@
|
||||
|
||||
import * as d3 from "d3";
|
||||
import d3tip from "d3-tip";
|
||||
import type { Trace } from "@/types/trace";
|
||||
import type { Trace, Span } from "@/types/trace";
|
||||
import dayjs from "dayjs";
|
||||
import icons from "@/assets/img/icons";
|
||||
import { useAppStoreWithOut } from "@/store/modules/app";
|
||||
import { Themes } from "@/constants/data";
|
||||
import { useTraceStore } from "@/store/modules/trace";
|
||||
import { getServiceColor } from "@/utils/color";
|
||||
|
||||
const xScaleWidth = 0.6;
|
||||
export default class ListGraph {
|
||||
private barHeight = 48;
|
||||
private handleSelectSpan: Nullable<(i: Trace) => void> = null;
|
||||
@@ -36,19 +35,16 @@ export default class ListGraph {
|
||||
private prompt: any = null;
|
||||
private row: any[] = [];
|
||||
private data: any = [];
|
||||
private min = 0;
|
||||
private max = 0;
|
||||
private list: any[] = [];
|
||||
private minTimestamp = 0;
|
||||
private maxTimestamp = 0;
|
||||
private xScale: any = null;
|
||||
private xAxis: any = null;
|
||||
private sequentialScale: any = null;
|
||||
private root: any = null;
|
||||
private selectedNode: any = null;
|
||||
constructor(el: HTMLDivElement, handleSelectSpan: (i: Trace) => void) {
|
||||
constructor({ el, handleSelectSpan }: { el: HTMLDivElement; handleSelectSpan: (i: Trace) => void }) {
|
||||
this.handleSelectSpan = handleSelectSpan;
|
||||
this.el = el;
|
||||
this.width = el.getBoundingClientRect().width - 10;
|
||||
this.height = el.getBoundingClientRect().height - 10;
|
||||
d3.select(`.${this.el?.className} .trace-list`).remove();
|
||||
this.svg = d3
|
||||
.select(this.el)
|
||||
@@ -80,39 +76,48 @@ export default class ListGraph {
|
||||
this.svg.call(this.tip);
|
||||
this.svg.call(this.prompt);
|
||||
}
|
||||
diagonal(d: Recordable) {
|
||||
diagonal(d: Indexable) {
|
||||
return `M ${d.source.y} ${d.source.x + 5}
|
||||
L ${d.source.y} ${d.target.x - 30}
|
||||
L${d.target.y} ${d.target.x - 20}
|
||||
L${d.target.y} ${d.target.x - 5}`;
|
||||
}
|
||||
init(data: Recordable, row: Recordable[], fixSpansSize: number) {
|
||||
init({
|
||||
data,
|
||||
row,
|
||||
fixSpansSize,
|
||||
selectedMaxTimestamp,
|
||||
selectedMinTimestamp,
|
||||
}: {
|
||||
data: Indexable;
|
||||
row: Indexable[];
|
||||
fixSpansSize: number;
|
||||
selectedMaxTimestamp?: number;
|
||||
selectedMinTimestamp?: number;
|
||||
}) {
|
||||
d3.select(`.${this.el?.className} .trace-xaxis`).remove();
|
||||
d3.select("#trace-action-box").style("display", "none");
|
||||
this.row = row;
|
||||
this.data = data;
|
||||
this.min = d3.min(this.row.map((i) => i.startTime));
|
||||
this.max = d3.max(this.row.map((i) => i.endTime - this.min)) || 0;
|
||||
this.list = useTraceStore().serviceList || [];
|
||||
const min = d3.min(this.row.map((i) => i.startTime));
|
||||
const max = d3.max(this.row.map((i) => i.endTime));
|
||||
this.minTimestamp = selectedMinTimestamp ?? min;
|
||||
this.maxTimestamp = selectedMaxTimestamp ?? max;
|
||||
this.xScale = d3
|
||||
.scaleLinear()
|
||||
.range([0, this.width * 0.387])
|
||||
.domain([0, this.max]);
|
||||
.range([0, this.width * xScaleWidth])
|
||||
.domain([this.minTimestamp - min, this.maxTimestamp - min]);
|
||||
this.xAxis = d3.axisTop(this.xScale).tickFormat((d: any) => {
|
||||
if (d === 0) return 0;
|
||||
if (d >= 1000) return d / 1000 + "s";
|
||||
return d;
|
||||
if (d === 0) return d.toFixed(2);
|
||||
if (d >= 1000) return (d / 1000).toFixed(2) + "s";
|
||||
return d.toFixed(2);
|
||||
});
|
||||
this.svg.attr("height", (this.row.length + fixSpansSize + 1) * this.barHeight);
|
||||
this.svg
|
||||
.append("g")
|
||||
.attr("class", "trace-xaxis")
|
||||
.attr("transform", `translate(${this.width * 0.618 - 20},${30})`)
|
||||
.attr("transform", `translate(${this.width * (1 - xScaleWidth) - 20},${30})`)
|
||||
.call(this.xAxis);
|
||||
this.sequentialScale = d3
|
||||
.scaleSequential()
|
||||
.domain([0, this.list.length + 1])
|
||||
.interpolator(d3.interpolateCool);
|
||||
this.root = d3.hierarchy(this.data, (d) => d.children);
|
||||
this.root.x0 = 0;
|
||||
this.root.y0 = 0;
|
||||
@@ -141,7 +146,6 @@ export default class ListGraph {
|
||||
}
|
||||
update(source: Recordable, callback: Function) {
|
||||
const t = this;
|
||||
const appStore = useAppStoreWithOut();
|
||||
const nodes = this.root.descendants();
|
||||
let index = -1;
|
||||
this.root.eachBefore((n: Recordable) => {
|
||||
@@ -186,12 +190,12 @@ export default class ListGraph {
|
||||
});
|
||||
nodeEnter
|
||||
.append("rect")
|
||||
.attr("height", 42)
|
||||
.attr("height", this.barHeight)
|
||||
.attr("ry", 2)
|
||||
.attr("rx", 2)
|
||||
.attr("y", -22)
|
||||
.attr("y", -this.barHeight / 2)
|
||||
.attr("x", 20)
|
||||
.attr("width", "100%")
|
||||
.attr("width", "200px")
|
||||
.attr("fill", "rgba(0,0,0,0)");
|
||||
nodeEnter
|
||||
.append("image")
|
||||
@@ -242,24 +246,18 @@ export default class ListGraph {
|
||||
event.stopPropagation();
|
||||
t.prompt.hide(d, this);
|
||||
});
|
||||
nodeEnter
|
||||
.append("text")
|
||||
.attr("x", 13)
|
||||
.attr("y", 5)
|
||||
.attr("fill", `#e54c17`)
|
||||
.html((d: Recordable) => (d.data.isError ? "◉" : ""));
|
||||
nodeEnter
|
||||
.append("text")
|
||||
.attr("class", "node-text")
|
||||
.attr("x", 35)
|
||||
.attr("y", -6)
|
||||
.attr("fill", (d: Recordable) => (d.data.isError ? `#e54c17` : appStore.theme === Themes.Dark ? "#eee" : "#333"))
|
||||
.html((d: Recordable) => {
|
||||
.attr("fill", (d: { data: Span }) => (d.data.isError ? `var(--el-color-danger)` : `var(--font-color)`))
|
||||
.html((d: { data: Span }) => {
|
||||
const { label } = d.data;
|
||||
if (d.data.label === "TRACE_ROOT") {
|
||||
return "";
|
||||
}
|
||||
const label = d.data.label.length > 30 ? `${d.data.label.slice(0, 30)}...` : `${d.data.label}`;
|
||||
return label;
|
||||
return (label || "").length > 30 ? `${label?.slice(0, 30)}...` : `${label}`;
|
||||
});
|
||||
nodeEnter
|
||||
.append("circle")
|
||||
@@ -289,7 +287,7 @@ export default class ListGraph {
|
||||
.attr("y", -1)
|
||||
.attr("fill", "#e66")
|
||||
.style("font-size", "10px")
|
||||
.text((d: Recordable) => {
|
||||
.text((d: { data: Span }) => {
|
||||
const events = d.data.attachedEvents;
|
||||
if (events && events.length) {
|
||||
return `${events.length}`;
|
||||
@@ -302,18 +300,15 @@ export default class ListGraph {
|
||||
.attr("class", "node-text")
|
||||
.attr("x", 35)
|
||||
.attr("y", 12)
|
||||
.attr("fill", appStore.theme === Themes.Dark ? "#777" : "#ccc")
|
||||
.attr("fill", (d: { data: Span }) => (d.data.isError ? `var(--el-color-danger)` : `var(--disabled-color)`))
|
||||
.style("font-size", "11px")
|
||||
.text(
|
||||
(d: Recordable) =>
|
||||
`${d.data.layer || ""} ${
|
||||
d.data.component
|
||||
? "- " + d.data.component
|
||||
: d.data.event
|
||||
? this.visDate(d.data.startTime) + ":" + d.data.startTimeNanos
|
||||
: ""
|
||||
}`,
|
||||
);
|
||||
.text((d: { data: Span }) => {
|
||||
const { layer, component, event, startTime, startTimeNanos } = d.data;
|
||||
const text = `${layer || ""} ${
|
||||
component ? "- " + component : event ? this.visDate(startTime) + ":" + startTimeNanos : ""
|
||||
}`;
|
||||
return text.length > 20 ? `${text?.slice(0, 20)}...` : `${text}`;
|
||||
});
|
||||
nodeEnter
|
||||
.append("rect")
|
||||
.attr("rx", 2)
|
||||
@@ -321,15 +316,33 @@ export default class ListGraph {
|
||||
.attr("height", 4)
|
||||
.attr("width", (d: Recordable) => {
|
||||
if (!d.data.endTime || !d.data.startTime) return 0;
|
||||
return this.xScale(d.data.endTime - d.data.startTime) + 1 || 0;
|
||||
// Calculate the actual start and end times within the visible range
|
||||
let spanStart = d.data.startTime;
|
||||
let spanEnd = d.data.endTime;
|
||||
|
||||
const isIn = d.data.startTime > this.maxTimestamp || d.data.endTime < this.minTimestamp;
|
||||
if (isIn) return 0;
|
||||
|
||||
// If the span is completely outside the visible range, don't show it
|
||||
if (spanStart >= spanEnd) return 0;
|
||||
if (spanStart < this.minTimestamp) spanStart = this.minTimestamp;
|
||||
if (spanEnd > this.maxTimestamp) spanEnd = this.maxTimestamp;
|
||||
if (spanStart >= spanEnd) return 0;
|
||||
const min = d3.min(this.row.map((i) => i.startTime));
|
||||
return this.xScale(spanEnd - min) - this.xScale(spanStart - min) + 1 || 0;
|
||||
})
|
||||
.attr("x", (d: Recordable) => {
|
||||
if (!d.data.endTime || !d.data.startTime) return 0;
|
||||
const isIn = d.data.startTime > this.maxTimestamp || d.data.endTime < this.minTimestamp;
|
||||
if (isIn) return 0;
|
||||
// Calculate the actual start time within the visible range
|
||||
let spanStart = d.data.startTime;
|
||||
if (spanStart < this.minTimestamp) spanStart = this.minTimestamp;
|
||||
const min = d3.min(this.row.map((i) => i.startTime));
|
||||
return this.width * (1 - xScaleWidth) - d.y - 25 + this.xScale(spanStart - min) || 0;
|
||||
})
|
||||
.attr("x", (d: Recordable) =>
|
||||
!d.data.endTime || !d.data.startTime
|
||||
? 0
|
||||
: this.width * 0.618 - 20 - d.y + this.xScale(d.data.startTime - this.min) || 0,
|
||||
)
|
||||
.attr("y", -2)
|
||||
.style("fill", (d: Recordable) => `${this.sequentialScale(this.list.indexOf(d.data.serviceCode))}`);
|
||||
.style("fill", (d: Recordable) => `${getServiceColor(d.data.serviceCode || "")}`);
|
||||
const nodeUpdate = nodeEnter.merge(node);
|
||||
nodeUpdate
|
||||
.transition()
|
||||
@@ -338,14 +351,12 @@ export default class ListGraph {
|
||||
.style("opacity", 1);
|
||||
nodeUpdate
|
||||
.append("circle")
|
||||
.attr("r", appStore.theme === Themes.Dark ? 4 : 3)
|
||||
.attr("r", 3)
|
||||
.style("cursor", "pointer")
|
||||
.attr("stroke-width", appStore.theme === Themes.Dark ? 3 : 2.5)
|
||||
.style("fill", (d: Recordable) =>
|
||||
d._children ? `${this.sequentialScale(this.list.indexOf(d.data.serviceCode))}` : "#eee",
|
||||
)
|
||||
.attr("stroke-width", 3)
|
||||
.style("fill", (d: Recordable) => (d._children ? `${getServiceColor(d.data.serviceCode)}` : "#eee"))
|
||||
.style("stroke", (d: Recordable) =>
|
||||
d.data.label === "TRACE_ROOT" ? "" : `${this.sequentialScale(this.list.indexOf(d.data.serviceCode))}`,
|
||||
d.data.label === "TRACE_ROOT" ? "" : `${getServiceColor(d.data.serviceCode)}`,
|
||||
)
|
||||
.on("click", (event: any, d: Recordable) => {
|
||||
event.stopPropagation();
|
||||
@@ -368,8 +379,8 @@ export default class ListGraph {
|
||||
.enter()
|
||||
.insert("path", "g")
|
||||
.attr("class", "trace-link")
|
||||
.attr("fill", appStore.theme === Themes.Dark ? "rgba(244,244,244,0)" : "rgba(0,0,0,0)")
|
||||
.attr("stroke", appStore.theme === Themes.Dark ? "rgba(244,244,244,0.4)" : "rgba(0, 0, 0, 0.1)")
|
||||
.attr("fill", "none")
|
||||
.attr("stroke", "var(--sw-trace-list-path)")
|
||||
.attr("stroke-width", 2)
|
||||
.attr("transform", `translate(5, 0)`)
|
||||
.attr("d", () => {
|
||||
@@ -432,6 +443,23 @@ export default class ListGraph {
|
||||
behavior: "smooth",
|
||||
});
|
||||
}
|
||||
highlightSpan(span: Recordable) {
|
||||
if (!span) {
|
||||
return;
|
||||
}
|
||||
const nodes = this.root?.descendants ? this.root.descendants() : [];
|
||||
const targetNode = nodes.find(
|
||||
(node: { data: Span }) =>
|
||||
span.spanId === node.data.spanId &&
|
||||
span.segmentId === node.data.segmentId &&
|
||||
span.traceId === node.data.traceId,
|
||||
);
|
||||
if (!targetNode) return;
|
||||
this.selectedNode?.classed("highlighted", false);
|
||||
const sel = d3.select(`#list-node-${targetNode.id}`);
|
||||
sel.classed("highlighted", true);
|
||||
this.selectedNode = sel;
|
||||
}
|
||||
visDate(date: number, pattern = "YYYY-MM-DD HH:mm:ss:SSS") {
|
||||
return dayjs(date).format(pattern);
|
||||
}
|
@@ -19,8 +19,8 @@ import * as d3 from "d3";
|
||||
import d3tip from "d3-tip";
|
||||
import type { Trace, Span } from "@/types/trace";
|
||||
import { useAppStoreWithOut } from "@/store/modules/app";
|
||||
import { useTraceStore } from "@/store/modules/trace";
|
||||
import { Themes } from "@/constants/data";
|
||||
import { getServiceColor } from "@/utils/color";
|
||||
|
||||
export default class TraceMap {
|
||||
private i = 0;
|
||||
@@ -36,11 +36,9 @@ export default class TraceMap {
|
||||
private treemap: Nullable<any> = null;
|
||||
private data: Nullable<any> = null;
|
||||
private row: Nullable<any> = null;
|
||||
private min = 0;
|
||||
private max = 0;
|
||||
private list: string[] = [];
|
||||
private minTimestamp = 0;
|
||||
private maxTimestamp = 0;
|
||||
private xScale: Nullable<any> = null;
|
||||
private sequentialScale: Nullable<any> = null;
|
||||
private root: Nullable<any> = null;
|
||||
private topSlowMax: number[] = [];
|
||||
private topSlowMin: number[] = [];
|
||||
@@ -49,14 +47,14 @@ export default class TraceMap {
|
||||
private nodeUpdate: Nullable<any> = null;
|
||||
private selectedNode: any = null;
|
||||
|
||||
constructor(el: HTMLDivElement, handleSelectSpan: (i: Trace) => void) {
|
||||
constructor({ el, handleSelectSpan }: { el: HTMLDivElement; handleSelectSpan: (i: Trace) => void }) {
|
||||
this.el = el;
|
||||
this.handleSelectSpan = handleSelectSpan;
|
||||
this.i = 0;
|
||||
this.topSlow = [];
|
||||
this.topChild = [];
|
||||
this.width = el.clientWidth - 20;
|
||||
this.height = el.clientHeight - 30;
|
||||
this.width = el.getBoundingClientRect().width - 10;
|
||||
this.height = el.getBoundingClientRect().height - 10;
|
||||
d3.select(`.${this.el?.className} .d3-trace-tree`).remove();
|
||||
this.body = d3
|
||||
.select(this.el)
|
||||
@@ -81,19 +79,28 @@ export default class TraceMap {
|
||||
this.svg = this.body.append("g").attr("transform", () => `translate(120, 0)`);
|
||||
this.svg.call(this.tip);
|
||||
}
|
||||
init(data: Recordable, row: Recordable) {
|
||||
init({
|
||||
data,
|
||||
row,
|
||||
selectedMaxTimestamp,
|
||||
selectedMinTimestamp,
|
||||
}: {
|
||||
data: Span;
|
||||
row: Span[];
|
||||
selectedMaxTimestamp?: number;
|
||||
selectedMinTimestamp?: number;
|
||||
}) {
|
||||
d3.select("#trace-action-box").style("display", "none");
|
||||
this.treemap = d3.tree().size([row.length * 35, this.width]);
|
||||
this.row = row;
|
||||
this.data = data;
|
||||
this.min = Number(d3.min(this.row.map((i: Span) => i.startTime)));
|
||||
this.max = Number(d3.max(this.row.map((i: Span) => i.endTime - this.min)));
|
||||
this.list = useTraceStore().serviceList || [];
|
||||
this.xScale = d3.scaleLinear().range([0, 100]).domain([0, this.max]);
|
||||
this.sequentialScale = d3
|
||||
.scaleSequential()
|
||||
.domain([0, this.list.length + 1])
|
||||
.interpolator(d3.interpolateCool);
|
||||
this.minTimestamp = selectedMinTimestamp ?? Number(d3.min(this.row.map((i: Span) => i.startTime)));
|
||||
this.maxTimestamp =
|
||||
selectedMaxTimestamp ?? Number(d3.max(this.row.map((i: Span) => i.endTime - this.minTimestamp)));
|
||||
this.xScale = d3
|
||||
.scaleLinear()
|
||||
.range([0, 100])
|
||||
.domain([0, this.maxTimestamp - this.minTimestamp]);
|
||||
|
||||
this.body.call(this.getZoomBehavior(this.svg));
|
||||
this.root = d3.hierarchy(this.data, (d) => d.children);
|
||||
@@ -102,7 +109,7 @@ export default class TraceMap {
|
||||
this.topSlow = [];
|
||||
this.topChild = [];
|
||||
const that = this;
|
||||
this.root.children.forEach(collapse);
|
||||
this.root.children?.forEach(collapse);
|
||||
this.topSlowMax = this.topSlow.sort((a: number, b: number) => b - a)[0];
|
||||
this.topSlowMin = this.topSlow.sort((a: number, b: number) => b - a)[4];
|
||||
this.topChildMax = this.topChild.sort((a: number, b: number) => b - a)[0];
|
||||
@@ -218,11 +225,9 @@ export default class TraceMap {
|
||||
.append("circle")
|
||||
.attr("class", "node")
|
||||
.attr("r", 2)
|
||||
.attr("stroke", (d: Recordable) => this.sequentialScale(this.list.indexOf(d.data.serviceCode)))
|
||||
.attr("stroke", (d: Recordable) => getServiceColor(d.data.serviceCode))
|
||||
.attr("stroke-width", 2.5)
|
||||
.attr("fill", (d: Recordable) =>
|
||||
d.data.children.length ? this.sequentialScale(this.list.indexOf(d.data.serviceCode)) : "#fff",
|
||||
);
|
||||
.attr("fill", (d: Recordable) => (d.data.children.length ? getServiceColor(d.data.serviceCode) : "#fff"));
|
||||
nodeEnter
|
||||
.append("text")
|
||||
.attr("class", "trace-node-text")
|
||||
@@ -276,21 +281,31 @@ export default class TraceMap {
|
||||
.attr("rx", 1)
|
||||
.attr("ry", 1)
|
||||
.attr("height", 2)
|
||||
.attr("width", (d: Recordable) => {
|
||||
if (!d.data.endTime || !d.data.startTime) return 0;
|
||||
return this.xScale(d.data.endTime - d.data.startTime) + 1 || 0;
|
||||
.attr("width", (d: { data: Span }) => {
|
||||
let spanStart = d.data.startTime;
|
||||
let spanEnd = d.data.endTime;
|
||||
if (!spanEnd || !spanStart) return 0;
|
||||
if (spanStart > this.maxTimestamp || spanEnd < this.minTimestamp) return 0;
|
||||
if (spanStart < this.minTimestamp) spanStart = this.minTimestamp;
|
||||
if (spanEnd > this.maxTimestamp) spanEnd = this.maxTimestamp;
|
||||
return this.xScale(spanEnd - spanStart) + 1 || 0;
|
||||
})
|
||||
.attr("x", (d: Recordable) => {
|
||||
if (!d.data.endTime || !d.data.startTime) {
|
||||
.attr("x", (d: Indexable) => {
|
||||
let spanStart = d.data.startTime;
|
||||
let spanEnd = d.data.endTime;
|
||||
if (!spanEnd || !spanStart) {
|
||||
return 0;
|
||||
}
|
||||
if (spanStart > this.maxTimestamp || spanEnd < this.minTimestamp) return 0;
|
||||
if (spanStart < this.minTimestamp) spanStart = this.minTimestamp;
|
||||
if (spanEnd > this.maxTimestamp) spanEnd = this.maxTimestamp;
|
||||
if (d.children || d._children) {
|
||||
return -110 + this.xScale(d.data.startTime - this.min);
|
||||
return -110 + this.xScale(spanStart - this.minTimestamp);
|
||||
}
|
||||
return 10 + this.xScale(d.data.startTime - this.min);
|
||||
return 10 + this.xScale(spanStart - this.minTimestamp);
|
||||
})
|
||||
.attr("y", -1)
|
||||
.style("fill", (d: Recordable) => this.sequentialScale(this.list.indexOf(d.data.serviceCode)));
|
||||
.style("fill", (d: { data: Span }) => getServiceColor(d.data.serviceCode));
|
||||
const nodeUpdate = nodeEnter.merge(node);
|
||||
this.nodeUpdate = nodeUpdate;
|
||||
nodeUpdate
|
@@ -0,0 +1,281 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { Span, Ref } from "@/types/trace";
|
||||
|
||||
/* global Indexable */
|
||||
|
||||
export interface BuildTreeResult {
|
||||
roots: Span[];
|
||||
fixSpansSize: number;
|
||||
refSpans: Ref[];
|
||||
}
|
||||
|
||||
export function buildSegmentForest(data: Span[], traceId: string): BuildTreeResult {
|
||||
const refSpans: Ref[] = [];
|
||||
const segmentGroup: { [key: string]: any } = {};
|
||||
const segmentIdGroup: string[] = [];
|
||||
const fixSpans: Span[] = [];
|
||||
const segmentHeaders: Span[] = [];
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
return { roots: [], fixSpansSize: 0, refSpans };
|
||||
}
|
||||
|
||||
for (const span of data) {
|
||||
if (span.refs && span.refs.length) {
|
||||
refSpans.push(...span.refs);
|
||||
}
|
||||
if (span.parentSpanId === -1) {
|
||||
segmentHeaders.push(span);
|
||||
} else {
|
||||
const item = data.find(
|
||||
(i: Span) => i.traceId === span.traceId && i.segmentId === span.segmentId && i.spanId === span.spanId - 1,
|
||||
);
|
||||
const content = fixSpans.find(
|
||||
(i: Span) =>
|
||||
i.traceId === span.traceId &&
|
||||
i.segmentId === span.segmentId &&
|
||||
i.spanId === span.spanId - 1 &&
|
||||
i.parentSpanId === span.spanId - 2,
|
||||
);
|
||||
if (!item && !content) {
|
||||
fixSpans.push({
|
||||
traceId: span.traceId,
|
||||
segmentId: span.segmentId,
|
||||
spanId: span.spanId - 1,
|
||||
parentSpanId: span.spanId - 2,
|
||||
refs: [],
|
||||
endpointName: `VNode: ${span.segmentId}`,
|
||||
serviceCode: "VirtualNode",
|
||||
type: `[Broken] ${span.type}`,
|
||||
peer: "",
|
||||
component: `VirtualNode: #${span.spanId - 1}`,
|
||||
isError: true,
|
||||
isBroken: true,
|
||||
layer: "Broken",
|
||||
tags: [],
|
||||
logs: [],
|
||||
startTime: 0,
|
||||
endTime: 0,
|
||||
} as Span);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const span of segmentHeaders) {
|
||||
if (span.refs && span.refs.length) {
|
||||
let exit: Span | null = null;
|
||||
for (const ref of span.refs) {
|
||||
const e = data.find(
|
||||
(i: Span) =>
|
||||
ref.traceId === i.traceId && ref.parentSegmentId === i.segmentId && ref.parentSpanId === i.spanId,
|
||||
);
|
||||
if (e) {
|
||||
exit = e;
|
||||
}
|
||||
}
|
||||
if (!exit) {
|
||||
const ref = span.refs[0];
|
||||
const parentSpanId = ref.parentSpanId > -1 ? 0 : -1;
|
||||
const content = fixSpans.find(
|
||||
(i: Span) =>
|
||||
i.traceId === ref.traceId &&
|
||||
i.segmentId === ref.parentSegmentId &&
|
||||
i.spanId === ref.parentSpanId &&
|
||||
i.parentSpanId === parentSpanId,
|
||||
);
|
||||
if (!content) {
|
||||
fixSpans.push({
|
||||
traceId: ref.traceId,
|
||||
segmentId: ref.parentSegmentId,
|
||||
spanId: ref.parentSpanId,
|
||||
parentSpanId,
|
||||
refs: [],
|
||||
endpointName: `VNode: ${ref.parentSegmentId}`,
|
||||
serviceCode: "VirtualNode",
|
||||
type: `[Broken] ${ref.type}`,
|
||||
peer: "",
|
||||
component: `VirtualNode: #${ref.parentSpanId}`,
|
||||
isError: true,
|
||||
isBroken: true,
|
||||
layer: "Broken",
|
||||
tags: [],
|
||||
logs: [],
|
||||
startTime: 0,
|
||||
endTime: 0,
|
||||
} as Span);
|
||||
}
|
||||
if (parentSpanId > -1) {
|
||||
const exists = fixSpans.find(
|
||||
(i: Span) =>
|
||||
i.traceId === ref.traceId &&
|
||||
i.segmentId === ref.parentSegmentId &&
|
||||
i.spanId === 0 &&
|
||||
i.parentSpanId === -1,
|
||||
);
|
||||
if (!exists) {
|
||||
fixSpans.push({
|
||||
traceId: ref.traceId,
|
||||
segmentId: ref.parentSegmentId,
|
||||
spanId: 0,
|
||||
parentSpanId: -1,
|
||||
refs: [],
|
||||
endpointName: `VNode: ${ref.parentSegmentId}`,
|
||||
serviceCode: "VirtualNode",
|
||||
type: `[Broken] ${ref.type}`,
|
||||
peer: "",
|
||||
component: `VirtualNode: #0`,
|
||||
isError: true,
|
||||
isBroken: true,
|
||||
layer: "Broken",
|
||||
tags: [],
|
||||
logs: [],
|
||||
startTime: 0,
|
||||
endTime: 0,
|
||||
} as Span);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const i of [...fixSpans, ...data]) {
|
||||
i.label = i.endpointName || "no operation name";
|
||||
i.key = Math.random().toString(36).substring(2, 36);
|
||||
i.children = [];
|
||||
if (segmentGroup[i.segmentId]) {
|
||||
segmentGroup[i.segmentId].push(i);
|
||||
} else {
|
||||
segmentIdGroup.push(i.segmentId);
|
||||
segmentGroup[i.segmentId] = [i];
|
||||
}
|
||||
}
|
||||
|
||||
for (const id of segmentIdGroup) {
|
||||
const currentSegment = segmentGroup[id].sort((a: Span, b: Span) => b.parentSpanId - a.parentSpanId);
|
||||
for (const s of currentSegment) {
|
||||
const index = currentSegment.findIndex((i: Span) => i.spanId === s.parentSpanId);
|
||||
if (index > -1) {
|
||||
if (
|
||||
(currentSegment[index].isBroken && currentSegment[index].parentSpanId === -1) ||
|
||||
!currentSegment[index].isBroken
|
||||
) {
|
||||
currentSegment[index].children?.push(s);
|
||||
currentSegment[index].children?.sort((a: Span, b: Span) => a.spanId - b.spanId);
|
||||
}
|
||||
}
|
||||
if (s.isBroken) {
|
||||
const children = data.filter(
|
||||
(span: Span) =>
|
||||
!!span.refs?.find(
|
||||
(d) => d.traceId === s.traceId && d.parentSegmentId === s.segmentId && d.parentSpanId === s.spanId,
|
||||
),
|
||||
);
|
||||
if (children.length) {
|
||||
s.children?.push(...children);
|
||||
}
|
||||
}
|
||||
}
|
||||
segmentGroup[id] = currentSegment[currentSegment.length - 1];
|
||||
}
|
||||
|
||||
for (const id of segmentIdGroup) {
|
||||
for (const ref of segmentGroup[id].refs || []) {
|
||||
if (ref.traceId === traceId) {
|
||||
traverseTree(segmentGroup[ref.parentSegmentId], ref.parentSpanId, ref.parentSegmentId, segmentGroup[id]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const roots: Span[] = [];
|
||||
for (const i in segmentGroup) {
|
||||
let pushed = false;
|
||||
for (const ref of segmentGroup[i].refs || []) {
|
||||
if (!segmentGroup[ref.parentSegmentId]) {
|
||||
roots.push(segmentGroup[i]);
|
||||
pushed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (
|
||||
!pushed &&
|
||||
(!segmentGroup[i].refs || segmentGroup[i].refs.length === 0) &&
|
||||
segmentGroup[i].parentSpanId === -1
|
||||
) {
|
||||
roots.push(segmentGroup[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return { roots, fixSpansSize: fixSpans.length, refSpans };
|
||||
}
|
||||
|
||||
export function collapseTree(d: Span, refSpans: Ref[]): void {
|
||||
if (d.children) {
|
||||
const item = refSpans.find((s: Ref) => s.parentSpanId === d.spanId && s.parentSegmentId === d.segmentId);
|
||||
let dur = d.endTime - d.startTime;
|
||||
for (const i of d.children) {
|
||||
dur -= i.endTime - i.startTime;
|
||||
}
|
||||
d.dur = dur < 0 ? 0 : dur;
|
||||
if (item) {
|
||||
d.children = d.children?.sort(compare("startTime"));
|
||||
}
|
||||
for (const i of d.children) {
|
||||
collapseTree(i, refSpans);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function traverseTree(node: Span, spanId: number, segmentId: string, data: Span) {
|
||||
if (!node || node.isBroken) {
|
||||
return;
|
||||
}
|
||||
if (node.spanId === spanId && node.segmentId === segmentId) {
|
||||
node.children?.push(data);
|
||||
return;
|
||||
}
|
||||
for (const nodeItem of (node as Span).children || []) {
|
||||
traverseTree(nodeItem, spanId, segmentId, data);
|
||||
}
|
||||
}
|
||||
|
||||
function compare(p: string) {
|
||||
return (m: Span, n: Span) => {
|
||||
const a = (m as Indexable)[p];
|
||||
const b = (n as Indexable)[p];
|
||||
return a - b;
|
||||
};
|
||||
}
|
||||
|
||||
export function getRefsAllNodes(tree: Indexable) {
|
||||
const nodes = [];
|
||||
const stack = [tree];
|
||||
|
||||
while (stack.length > 0) {
|
||||
const node = stack.pop();
|
||||
nodes.push(node);
|
||||
|
||||
if (node?.children && node.children.length > 0) {
|
||||
for (let i = node.children.length - 1; i >= 0; i--) {
|
||||
stack.push(node.children[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nodes;
|
||||
}
|
@@ -16,11 +16,11 @@ limitations under the License. -->
|
||||
<template>
|
||||
<div class="trace-table">
|
||||
<div class="trace-table-header" v-if="type === TraceGraphType.STATISTICS">
|
||||
<div :class="item.label" v-for="(item, index) in headerData" :key="index">
|
||||
<div :class="item.label" v-for="(item, index) in headerData as typeof StatisticsConstant" :key="index">
|
||||
{{ item.value }}
|
||||
<span
|
||||
class="r cp"
|
||||
@click="sortStatistics(item.key)"
|
||||
@click="sortStatistics(item.key || '')"
|
||||
:key="componentKey"
|
||||
v-if="item.key !== 'endpointName' && item.key !== 'type'"
|
||||
>
|
||||
@@ -47,34 +47,41 @@ limitations under the License. -->
|
||||
:key="`key${index}`"
|
||||
:type="type"
|
||||
:headerType="headerType"
|
||||
@click="selectItem"
|
||||
:selectedMaxTimestamp="selectedMaxTimestamp"
|
||||
:selectedMinTimestamp="selectedMinTimestamp"
|
||||
@selectedSpan="selectItem"
|
||||
/>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted } from "vue";
|
||||
import type { PropType } from "vue";
|
||||
import { useTraceStore } from "@/store/modules/trace";
|
||||
import type { Span } from "@/types/trace";
|
||||
import TableItem from "./TableItem.vue";
|
||||
import { ProfileConstant, TraceConstant, StatisticsConstant } from "./data";
|
||||
import { TraceGraphType } from "../constant";
|
||||
import { TraceGraphType } from "../VisGraph/constant";
|
||||
import { WidgetType } from "@/views/dashboard/data";
|
||||
|
||||
/* global defineProps, Nullable, defineEmits, Recordable*/
|
||||
const props = defineProps({
|
||||
tableData: { type: Array as PropType<Recordable>, default: () => [] },
|
||||
type: { type: String, default: "" },
|
||||
headerType: { type: String, default: "" },
|
||||
traceId: { type: String, default: "" },
|
||||
});
|
||||
const emits = defineEmits(["select"]);
|
||||
const traceStore = useTraceStore();
|
||||
/* global defineProps, Nullable, defineEmits*/
|
||||
type Props = {
|
||||
tableData: Span[];
|
||||
type?: string;
|
||||
headerType?: string;
|
||||
traceId: string;
|
||||
selectedMaxTimestamp?: number;
|
||||
selectedMinTimestamp?: number;
|
||||
};
|
||||
type Emits = {
|
||||
(e: "select", value: Span): void;
|
||||
};
|
||||
const props = defineProps<Props>();
|
||||
const emits = defineEmits<Emits>();
|
||||
|
||||
const method = ref<number>(300);
|
||||
const componentKey = ref<number>(300);
|
||||
const flag = ref<boolean>(true);
|
||||
const dragger = ref<Nullable<HTMLSpanElement>>(null);
|
||||
let headerData: Recordable[] = TraceConstant;
|
||||
let headerData: typeof TraceConstant | typeof ProfileConstant | typeof StatisticsConstant = TraceConstant;
|
||||
|
||||
if (props.headerType === WidgetType.Profile) {
|
||||
headerData = ProfileConstant;
|
||||
@@ -104,31 +111,15 @@ limitations under the License. -->
|
||||
};
|
||||
};
|
||||
});
|
||||
function selectItem(event: MouseEvent) {
|
||||
emits("select", traceStore.selectedSpan);
|
||||
if (props.headerType === WidgetType.Profile) {
|
||||
return;
|
||||
}
|
||||
if (props.type === TraceGraphType.STATISTICS) {
|
||||
return;
|
||||
}
|
||||
const item: any = document.querySelector("#trace-action-box");
|
||||
const tableBox = document.querySelector(".trace-table-charts")?.getBoundingClientRect();
|
||||
if (!tableBox) {
|
||||
return;
|
||||
}
|
||||
const offsetX = event.x - tableBox.x;
|
||||
const offsetY = event.y - tableBox.y;
|
||||
item.style.display = "block";
|
||||
item.style.top = `${offsetY + 20}px`;
|
||||
item.style.left = `${offsetX + 10}px`;
|
||||
function selectItem(span: Span) {
|
||||
emits("select", span);
|
||||
}
|
||||
function sortStatistics(key: string) {
|
||||
const element = props.tableData;
|
||||
for (let i = 0; i < element.length; i++) {
|
||||
for (let j = 0; j < element.length - i - 1; j++) {
|
||||
let val1;
|
||||
let val2;
|
||||
let val1: number | undefined;
|
||||
let val2: number | undefined;
|
||||
if (key === "maxTime") {
|
||||
val1 = element[j].maxTime;
|
||||
val2 = element[j + 1].maxTime;
|
||||
@@ -150,13 +141,13 @@ limitations under the License. -->
|
||||
val2 = element[j + 1].count;
|
||||
}
|
||||
if (flag.value) {
|
||||
if (val1 < val2) {
|
||||
if (val1 && val2 && val1 < val2) {
|
||||
const tmp = element[j];
|
||||
element[j] = element[j + 1];
|
||||
element[j + 1] = tmp;
|
||||
}
|
||||
} else {
|
||||
if (val1 > val2) {
|
||||
if (val1 && val2 && val1 > val2) {
|
||||
const tmp = element[j];
|
||||
element[j] = element[j + 1];
|
||||
element[j + 1] = tmp;
|
||||
|
@@ -49,15 +49,16 @@ limitations under the License. -->
|
||||
</div>
|
||||
<div v-else>
|
||||
<div
|
||||
@click="selectSpan"
|
||||
:class="[
|
||||
'trace-item',
|
||||
'level' + ((data.level || 0) - 1),
|
||||
{ 'trace-item-error': data.isError },
|
||||
{ profiled: data.profiled === false },
|
||||
`trace-item-${data.key}`,
|
||||
{ highlighted: inTimeRange },
|
||||
]"
|
||||
:data-text="data.profiled === false ? 'No Thread Dump' : ''"
|
||||
@click="hideActionBox"
|
||||
>
|
||||
<div
|
||||
:class="['method', 'level' + ((data.level || 0) - 1)]"
|
||||
@@ -65,6 +66,8 @@ limitations under the License. -->
|
||||
'text-indent': ((data.level || 0) - 1) * 10 + 'px',
|
||||
width: `${method}px`,
|
||||
}"
|
||||
@click="selectSpan"
|
||||
@click.stop
|
||||
>
|
||||
<Icon
|
||||
:style="!displayChildren ? 'transform: rotate(-90deg);' : ''"
|
||||
@@ -73,6 +76,7 @@ limitations under the License. -->
|
||||
iconName="arrow-down"
|
||||
size="sm"
|
||||
class="mr-5"
|
||||
@click="hideActionBox"
|
||||
/>
|
||||
<el-tooltip
|
||||
:content="data.type === 'Entry' ? 'Entry' : 'Exit'"
|
||||
@@ -90,7 +94,7 @@ limitations under the License. -->
|
||||
</span>
|
||||
</el-tooltip>
|
||||
<el-tooltip :content="data.endpointName" placement="top" :show-after="300">
|
||||
<span>
|
||||
<span class="link-span">
|
||||
{{ data.endpointName }}
|
||||
</span>
|
||||
</el-tooltip>
|
||||
@@ -116,7 +120,7 @@ limitations under the License. -->
|
||||
</div>
|
||||
<div class="application">
|
||||
<el-tooltip :show-after="300" :content="data.serviceCode || '-'" placement="top">
|
||||
<span>{{ data.serviceCode }}</span>
|
||||
<span :style="{ color: getServiceColor(data.serviceCode || '') }">{{ data.serviceCode }}</span>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<div class="application" v-show="headerType === WidgetType.Profile" @click="viewSpan($event)">
|
||||
@@ -128,12 +132,15 @@ limitations under the License. -->
|
||||
</div>
|
||||
<div v-show="data.children && data.children.length > 0 && displayChildren" class="children-trace">
|
||||
<table-item
|
||||
:method="method"
|
||||
v-for="(child, index) in data.children"
|
||||
:method="method"
|
||||
:key="index"
|
||||
:data="child"
|
||||
:type="type"
|
||||
:headerType="headerType"
|
||||
:selectedMaxTimestamp="selectedMaxTimestamp"
|
||||
:selectedMinTimestamp="selectedMinTimestamp"
|
||||
@selectedSpan="selectItem"
|
||||
/>
|
||||
</div>
|
||||
<el-dialog v-model="showDetail" :destroy-on-close="true" fullscreen @closed="showDetail = false">
|
||||
@@ -141,136 +148,150 @@ limitations under the License. -->
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
<script lang="ts" setup>
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { ref, computed, defineComponent, watch } from "vue";
|
||||
import type { PropType } from "vue";
|
||||
import { ref, computed, watch } from "vue";
|
||||
import SpanDetail from "../D3Graph/SpanDetail.vue";
|
||||
import { dateFormat } from "@/utils/dateFormat";
|
||||
import { useAppStoreWithOut } from "@/store/modules/app";
|
||||
import { useTraceStore } from "@/store/modules/trace";
|
||||
import { Themes } from "@/constants/data";
|
||||
import { TraceGraphType } from "../constant";
|
||||
import { TraceGraphType } from "../VisGraph/constant";
|
||||
import { WidgetType } from "@/views/dashboard/data";
|
||||
import type { Span, Ref } from "@/types/trace";
|
||||
import { getServiceColor } from "@/utils/color";
|
||||
|
||||
/*global Recordable*/
|
||||
const props = {
|
||||
data: { type: Object as PropType<Span>, default: () => ({}) },
|
||||
method: { type: Number, default: 0 },
|
||||
type: { type: String, default: "" },
|
||||
headerType: { type: String, default: "" },
|
||||
traceId: { type: String, traceId: "" },
|
||||
};
|
||||
export default defineComponent({
|
||||
name: "TableItem",
|
||||
props,
|
||||
components: { SpanDetail },
|
||||
setup(props) {
|
||||
const appStore = useAppStoreWithOut();
|
||||
const traceStore = useTraceStore();
|
||||
const displayChildren = ref<boolean>(true);
|
||||
const showDetail = ref<boolean>(false);
|
||||
const { t } = useI18n();
|
||||
const selfTime = computed(() => (props.data.dur ? props.data.dur : 0));
|
||||
const execTime = computed(() =>
|
||||
props.data.endTime - props.data.startTime ? props.data.endTime - props.data.startTime : 0,
|
||||
);
|
||||
const outterPercent = computed(() => {
|
||||
if (props.data.level === 1) {
|
||||
return "100%";
|
||||
} else {
|
||||
const data = props.data;
|
||||
const exec = data.endTime - data.startTime ? data.endTime - data.startTime : 0;
|
||||
let result = (exec / (data.totalExec || 0)) * 100;
|
||||
result = result > 100 ? 100 : result;
|
||||
const resultStr = result.toFixed(4) + "%";
|
||||
return resultStr === "0.0000%" ? "0.9%" : resultStr;
|
||||
}
|
||||
});
|
||||
const innerPercent = computed(() => {
|
||||
const result = (selfTime.value / execTime.value) * 100;
|
||||
const resultStr = result.toFixed(4) + "%";
|
||||
return resultStr === "0.0000%" ? "0.9%" : resultStr;
|
||||
});
|
||||
const isCrossThread = computed(() => {
|
||||
const key = props.data.refs?.findIndex((d: Ref) => d.type === "CROSS_THREAD") ?? -1;
|
||||
return key > -1 ? true : false;
|
||||
});
|
||||
function toggle() {
|
||||
displayChildren.value = !displayChildren.value;
|
||||
}
|
||||
function showSelectSpan(dom: HTMLSpanElement) {
|
||||
if (!dom) {
|
||||
return;
|
||||
}
|
||||
const items: any = document.querySelectorAll(".trace-item");
|
||||
for (const item of items) {
|
||||
item.style.background = appStore.theme === Themes.Dark ? "#212224" : "#fff";
|
||||
}
|
||||
dom.style.background = appStore.theme === Themes.Dark ? "rgba(255, 255, 255, 0.1)" : "rgba(0, 0, 0, 0.1)";
|
||||
const p: any = document.getElementsByClassName("profiled")[0];
|
||||
if (p) {
|
||||
p.style.background = appStore.theme === Themes.Dark ? "#333" : "#eee";
|
||||
}
|
||||
}
|
||||
function selectSpan(event: Recordable) {
|
||||
const dom = event.composedPath().find((d: Recordable) => d.className.includes("trace-item"));
|
||||
selectedItem(props.data);
|
||||
if (props.headerType === WidgetType.Profile) {
|
||||
showSelectSpan(dom);
|
||||
return;
|
||||
}
|
||||
viewSpanDetail(dom);
|
||||
}
|
||||
function viewSpan(event: Recordable) {
|
||||
showDetail.value = true;
|
||||
const dom = event.composedPath().find((d: Recordable) => d.className.includes("trace-item"));
|
||||
selectedItem(props.data);
|
||||
viewSpanDetail(dom);
|
||||
}
|
||||
function selectedItem(span: Span) {
|
||||
traceStore.setSelectedSpan(span);
|
||||
}
|
||||
function viewSpanDetail(dom: HTMLSpanElement) {
|
||||
showSelectSpan(dom);
|
||||
if (props.type === TraceGraphType.STATISTICS) {
|
||||
showDetail.value = true;
|
||||
}
|
||||
}
|
||||
watch(
|
||||
() => appStore.theme,
|
||||
() => {
|
||||
const items: any = document.querySelectorAll(".trace-item");
|
||||
for (const item of items) {
|
||||
item.style.background = appStore.theme === Themes.Dark ? "#212224" : "#fff";
|
||||
}
|
||||
const p: any = document.getElementsByClassName("profiled")[0];
|
||||
if (p) {
|
||||
p.style.background = appStore.theme === Themes.Dark ? "#333" : "#eee";
|
||||
}
|
||||
},
|
||||
);
|
||||
return {
|
||||
displayChildren,
|
||||
outterPercent,
|
||||
innerPercent,
|
||||
isCrossThread,
|
||||
viewSpanDetail,
|
||||
toggle,
|
||||
dateFormat,
|
||||
showSelectSpan,
|
||||
showDetail,
|
||||
selectSpan,
|
||||
selectedItem,
|
||||
viewSpan,
|
||||
t,
|
||||
appStore,
|
||||
TraceGraphType,
|
||||
WidgetType,
|
||||
};
|
||||
},
|
||||
interface Props {
|
||||
data: Span;
|
||||
method: number;
|
||||
type?: string;
|
||||
headerType?: string;
|
||||
traceId?: string;
|
||||
selectedMaxTimestamp?: number;
|
||||
selectedMinTimestamp?: number;
|
||||
}
|
||||
interface Emits {
|
||||
(e: "selectedSpan", value: Span): void;
|
||||
}
|
||||
const emits = defineEmits<Emits>();
|
||||
const props = defineProps<Props>();
|
||||
const appStore = useAppStoreWithOut();
|
||||
const traceStore = useTraceStore();
|
||||
const displayChildren = ref<boolean>(true);
|
||||
const showDetail = ref<boolean>(false);
|
||||
const { t } = useI18n();
|
||||
const selfTime = computed(() => (props.data.dur ? props.data.dur : 0));
|
||||
const execTime = computed(() =>
|
||||
props.data.endTime - props.data.startTime > 0 ? props.data.endTime - props.data.startTime : 0,
|
||||
);
|
||||
const outterPercent = computed(() => {
|
||||
if (props.data.level === 1) {
|
||||
return "100%";
|
||||
} else {
|
||||
const { data } = props;
|
||||
let result = (execTime.value / (data.totalExec || 0)) * 100;
|
||||
result = result > 100 ? 100 : result;
|
||||
const resultStr = result.toFixed(4) + "%";
|
||||
return resultStr === "0.0000%" ? "0.9%" : resultStr;
|
||||
}
|
||||
});
|
||||
const innerPercent = computed(() => {
|
||||
const result = (selfTime.value / execTime.value) * 100;
|
||||
const resultStr = result.toFixed(4) + "%";
|
||||
return resultStr === "0.0000%" ? "0.9%" : resultStr;
|
||||
});
|
||||
const isCrossThread = computed(() => {
|
||||
const key = props.data.refs?.findIndex((d: Ref) => d.type === "CROSS_THREAD") ?? -1;
|
||||
return key > -1 ? true : false;
|
||||
});
|
||||
const inTimeRange = computed(() => {
|
||||
if (props.selectedMinTimestamp === undefined || props.selectedMaxTimestamp === undefined) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return props.data.startTime <= props.selectedMaxTimestamp && props.data.endTime >= props.selectedMinTimestamp;
|
||||
});
|
||||
function toggle() {
|
||||
displayChildren.value = !displayChildren.value;
|
||||
}
|
||||
function selectItem(span: Span) {
|
||||
emits("selectedSpan", span);
|
||||
}
|
||||
function showSelectSpan(dom: HTMLSpanElement) {
|
||||
if (!dom) {
|
||||
return;
|
||||
}
|
||||
const items: HTMLSpanElement[] = Array.from(document.querySelectorAll(".trace-item")) as HTMLSpanElement[];
|
||||
for (const item of items) {
|
||||
item.style.background = "transparent";
|
||||
}
|
||||
dom.style.background = "var(--sw-trace-table-selected)";
|
||||
const p = document.getElementsByClassName("profiled")[0] as HTMLSpanElement | null;
|
||||
if (p) {
|
||||
p.style.background = "var(--border-color-primary)";
|
||||
}
|
||||
}
|
||||
function selectSpan(event: MouseEvent) {
|
||||
emits("selectedSpan", props.data);
|
||||
const dom = event
|
||||
.composedPath()
|
||||
.find((d: EventTarget) => (d as HTMLElement).className.includes("trace-item")) as HTMLSpanElement;
|
||||
selectedItem(props.data);
|
||||
if (props.headerType === WidgetType.Profile) {
|
||||
showSelectSpan(dom);
|
||||
return;
|
||||
}
|
||||
viewSpanDetail(dom);
|
||||
if (props.type === TraceGraphType.STATISTICS) {
|
||||
return;
|
||||
}
|
||||
const item: HTMLSpanElement | null = document.querySelector("#trace-action-box");
|
||||
const tableBox = document.querySelector(".trace-table-charts")?.getBoundingClientRect();
|
||||
if (!tableBox || !item) {
|
||||
return;
|
||||
}
|
||||
const offsetX = event.x - tableBox.x;
|
||||
const offsetY = event.y - tableBox.y;
|
||||
item.style.display = "block";
|
||||
item.style.top = `${offsetY + 20}px`;
|
||||
item.style.left = `${offsetX + 10}px`;
|
||||
}
|
||||
function viewSpan(event: MouseEvent) {
|
||||
showDetail.value = true;
|
||||
const dom = event
|
||||
.composedPath()
|
||||
.find((d: EventTarget) => (d as HTMLElement).className.includes("trace-item")) as HTMLSpanElement;
|
||||
selectedItem(props.data);
|
||||
viewSpanDetail(dom);
|
||||
}
|
||||
function selectedItem(span: Span) {
|
||||
traceStore.setSelectedSpan(span);
|
||||
emits("selectedSpan", span);
|
||||
}
|
||||
function viewSpanDetail(dom: HTMLSpanElement) {
|
||||
showSelectSpan(dom);
|
||||
if (props.type === TraceGraphType.STATISTICS) {
|
||||
showDetail.value = true;
|
||||
}
|
||||
}
|
||||
function hideActionBox() {
|
||||
const item: HTMLSpanElement | null = document.querySelector("#trace-action-box");
|
||||
if (item) {
|
||||
item.style.display = "none";
|
||||
}
|
||||
}
|
||||
watch(
|
||||
() => appStore.theme,
|
||||
() => {
|
||||
const items: HTMLSpanElement[] = Array.from(document.querySelectorAll(".trace-item")) as HTMLSpanElement[];
|
||||
for (const item of items) {
|
||||
item.style.background = "transparent";
|
||||
}
|
||||
const p = document.getElementsByClassName("profiled")[0] as HTMLSpanElement | null;
|
||||
if (p) {
|
||||
p.style.background = "var(--border-color-primary)";
|
||||
}
|
||||
},
|
||||
);
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import url("./table.scss");
|
||||
@@ -290,6 +311,10 @@ limitations under the License. -->
|
||||
}
|
||||
}
|
||||
|
||||
.highlighted {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.profiled {
|
||||
background-color: var(--sw-table-header);
|
||||
position: relative;
|
||||
@@ -334,7 +359,6 @@ limitations under the License. -->
|
||||
.trace-item {
|
||||
white-space: nowrap;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.trace-item.selected {
|
||||
@@ -358,6 +382,7 @@ limitations under the License. -->
|
||||
|
||||
.trace-item > div.method {
|
||||
padding-left: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.trace-item div.exec-percent {
|
||||
@@ -385,4 +410,8 @@ limitations under the License. -->
|
||||
top: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.link-span {
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
|
@@ -48,7 +48,7 @@ export const ProfileConstant = [
|
||||
label: "application",
|
||||
value: "Operation",
|
||||
},
|
||||
];
|
||||
] as const;
|
||||
|
||||
export const TraceConstant = [
|
||||
{
|
||||
@@ -83,7 +83,7 @@ export const TraceConstant = [
|
||||
label: "application",
|
||||
value: "Attached Events",
|
||||
},
|
||||
];
|
||||
] as const;
|
||||
|
||||
export const StatisticsConstant = [
|
||||
{
|
||||
@@ -121,4 +121,4 @@ export const StatisticsConstant = [
|
||||
value: "Hits",
|
||||
key: "count",
|
||||
},
|
||||
];
|
||||
] as const;
|
||||
|
@@ -13,52 +13,56 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License. -->
|
||||
<template>
|
||||
<div class="flex-h row">
|
||||
<div class="mr-10 flex-h" v-if="dashboardStore.entity === EntityType[1].value">
|
||||
<span class="grey mr-5 label">{{ t("service") }}:</span>
|
||||
<Selector
|
||||
size="small"
|
||||
:value="state.service.value"
|
||||
:options="traceStore.services"
|
||||
placeholder="Select a service"
|
||||
@change="changeField('service', $event)"
|
||||
/>
|
||||
<div class="flex-h row" style="justify-content: space-between">
|
||||
<div class="flex-h">
|
||||
<div class="mr-10 flex-h" v-if="dashboardStore.entity === EntityType[1].value">
|
||||
<span class="grey mr-5 label">{{ t("service") }}:</span>
|
||||
<Selector
|
||||
size="small"
|
||||
:value="state.service.value"
|
||||
:options="traceStore.services"
|
||||
placeholder="Select a service"
|
||||
@change="changeField('service', $event)"
|
||||
/>
|
||||
</div>
|
||||
<div class="mr-10 flex-h" v-if="dashboardStore.entity !== EntityType[3].value">
|
||||
<span class="grey mr-5 label">{{ t("instance") }}:</span>
|
||||
<Selector
|
||||
size="small"
|
||||
:value="state.instance.value"
|
||||
:options="traceStore.instances"
|
||||
placeholder="Select a instance"
|
||||
@change="changeField('instance', $event)"
|
||||
/>
|
||||
</div>
|
||||
<div class="mr-10 flex-h" v-if="dashboardStore.entity !== EntityType[2].value">
|
||||
<span class="grey mr-5 label">{{ t("endpoint") }}:</span>
|
||||
<Selector
|
||||
size="small"
|
||||
:value="state.endpoint.value"
|
||||
:options="traceStore.endpoints"
|
||||
placeholder="Select a endpoint"
|
||||
:isRemote="true"
|
||||
@change="changeField('endpoint', $event)"
|
||||
@query="searchEndpoints"
|
||||
/>
|
||||
</div>
|
||||
<div class="mr-10 flex-h">
|
||||
<span class="grey mr-5 label">{{ t("status") }}:</span>
|
||||
<Selector
|
||||
size="small"
|
||||
:value="state.status.value"
|
||||
:options="Status"
|
||||
placeholder="Select a status"
|
||||
@change="changeField('status', $event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mr-10 flex-h" v-if="dashboardStore.entity !== EntityType[3].value">
|
||||
<span class="grey mr-5 label">{{ t("instance") }}:</span>
|
||||
<Selector
|
||||
size="small"
|
||||
:value="state.instance.value"
|
||||
:options="traceStore.instances"
|
||||
placeholder="Select a instance"
|
||||
@change="changeField('instance', $event)"
|
||||
/>
|
||||
<div class="mr-10">
|
||||
<el-button type="primary" @click="searchTraces" :loading="traceStore.loading">
|
||||
{{ t("runQuery") }}
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="mr-10 flex-h" v-if="dashboardStore.entity !== EntityType[2].value">
|
||||
<span class="grey mr-5 label">{{ t("endpoint") }}:</span>
|
||||
<Selector
|
||||
size="small"
|
||||
:value="state.endpoint.value"
|
||||
:options="traceStore.endpoints"
|
||||
placeholder="Select a endpoint"
|
||||
:isRemote="true"
|
||||
@change="changeField('endpoint', $event)"
|
||||
@query="searchEndpoints"
|
||||
/>
|
||||
</div>
|
||||
<div class="mr-10 flex-h">
|
||||
<span class="grey mr-5 label">{{ t("status") }}:</span>
|
||||
<Selector
|
||||
size="small"
|
||||
:value="state.status.value"
|
||||
:options="Status"
|
||||
placeholder="Select a status"
|
||||
@change="changeField('status', $event)"
|
||||
/>
|
||||
</div>
|
||||
<el-button size="small" type="primary" @click="searchTraces" class="search-btn">
|
||||
{{ t("search") }}
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="flex-h row">
|
||||
<div class="mr-10">
|
||||
@@ -72,7 +76,7 @@ limitations under the License. -->
|
||||
<el-input size="small" class="inputs" v-model="maxTraceDuration" type="number" />
|
||||
</div>
|
||||
<div>
|
||||
<span class="sm b grey mr-5">{{ t("timeRange") }}:</span>
|
||||
<span class="sm b grey mr-5">{{ t("timeRange") }}</span>
|
||||
<TimePicker
|
||||
:value="[durationRow.start, durationRow.end]"
|
||||
:maxRange="maxRange"
|
||||
@@ -91,15 +95,15 @@ limitations under the License. -->
|
||||
import type { PropType } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import type { Option, DurationTime, Duration } from "@/types/app";
|
||||
import { useTraceStore } from "@/store/modules/trace";
|
||||
import { useTraceStore, PageSize } from "@/store/modules/trace";
|
||||
import { useDashboardStore } from "@/store/modules/dashboard";
|
||||
import { useAppStoreWithOut, InitializationDurationRow } from "@/store/modules/app";
|
||||
import { useSelectorStore } from "@/store/modules/selectors";
|
||||
import timeFormat from "@/utils/timeFormat";
|
||||
import ConditionTags from "@/views/components/ConditionTags.vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { EntityType, QueryOrders, Status } from "../../data";
|
||||
import type { LayoutConfig } from "@/types/dashboard";
|
||||
import { EntityType, QueryOrders, Status } from "@/views/dashboard/data";
|
||||
import type { LayoutConfig, FilterDuration } from "@/types/dashboard";
|
||||
import { useDuration } from "@/hooks/useDuration";
|
||||
|
||||
/*global defineProps, defineEmits, Recordable */
|
||||
@@ -108,7 +112,7 @@ limitations under the License. -->
|
||||
needQuery: { type: Boolean, default: true },
|
||||
data: {
|
||||
type: Object as PropType<LayoutConfig>,
|
||||
default: () => ({ graph: {} }),
|
||||
default: () => ({}),
|
||||
},
|
||||
});
|
||||
const { t } = useI18n();
|
||||
@@ -117,20 +121,19 @@ limitations under the License. -->
|
||||
const dashboardStore = useDashboardStore();
|
||||
const traceStore: ReturnType<typeof useTraceStore> = useTraceStore();
|
||||
const { setDurationRow, getDurationTime, getMaxRange } = useDuration();
|
||||
const filters = reactive<Recordable>(props.data.filters || {});
|
||||
const traceId = ref<string>(filters.traceId || "");
|
||||
const { duration: filtersDuration } = filters;
|
||||
const duration = ref<DurationTime>(
|
||||
const filters = computed(() => props.data.filters || {});
|
||||
const traceId = ref<string>(filters.value.traceId || "");
|
||||
const { duration: filtersDuration } = filters.value;
|
||||
const duration = ref<DurationTime | FilterDuration>(
|
||||
filtersDuration
|
||||
? { start: filtersDuration.startTime || "", end: filtersDuration.endTime || "", step: filtersDuration.step || "" }
|
||||
: getDurationTime(),
|
||||
);
|
||||
const minTraceDuration = ref<number>();
|
||||
const maxTraceDuration = ref<number>();
|
||||
const tagsList = ref<string[]>([]);
|
||||
const tagsMap = ref<Option[]>([]);
|
||||
const state = reactive<Recordable>({
|
||||
status: filters.status === "ERROR" ? Status[2] : Status[0],
|
||||
status: filters.value.status === "ERROR" ? Status[2] : Status[0],
|
||||
instance: { value: "0", label: "All" },
|
||||
endpoint: { value: "0", label: "All" },
|
||||
service: { value: "", label: "" },
|
||||
@@ -139,9 +142,9 @@ limitations under the License. -->
|
||||
const maxRange = computed(() =>
|
||||
getMaxRange(appStore.coldStageMode ? appStore.recordsTTL?.coldTrace || 0 : appStore.recordsTTL?.trace || 0),
|
||||
);
|
||||
if (filters.queryOrder) {
|
||||
if (filters.value.queryOrder) {
|
||||
traceStore.setTraceCondition({
|
||||
queryOrder: filters.queryOrder,
|
||||
queryOrder: filters.value.queryOrder,
|
||||
});
|
||||
}
|
||||
if (props.needQuery) {
|
||||
@@ -149,7 +152,7 @@ limitations under the License. -->
|
||||
}
|
||||
|
||||
async function init() {
|
||||
duration.value = filters.duration || appStore.durationTime;
|
||||
duration.value = filters.value.duration || appStore.durationTime;
|
||||
if (dashboardStore.entity === EntityType[1].value) {
|
||||
await getServices();
|
||||
}
|
||||
@@ -221,8 +224,10 @@ limitations under the License. -->
|
||||
minTraceDuration: Number(minTraceDuration.value),
|
||||
maxTraceDuration: Number(maxTraceDuration.value),
|
||||
traceId: traceId.value || undefined,
|
||||
paging: { pageNum: 1, pageSize: 20 },
|
||||
};
|
||||
if (!traceStore.hasQueryTracesV2Support) {
|
||||
param.paging = { pageNum: 1, pageSize: PageSize };
|
||||
}
|
||||
if (props.data.filters && props.data.filters.id) {
|
||||
param = {
|
||||
...param,
|
||||
@@ -267,8 +272,7 @@ limitations under the License. -->
|
||||
emits("get", state.service.id);
|
||||
}
|
||||
}
|
||||
function updateTags(data: { tagsMap: Array<Option>; tagsList: string[] }) {
|
||||
tagsList.value = data.tagsList;
|
||||
function updateTags(data: { tagsMap: Array<Option> }) {
|
||||
tagsMap.value = data.tagsMap;
|
||||
}
|
||||
async function searchEndpoints(keyword: string) {
|
||||
@@ -345,21 +349,12 @@ limitations under the License. -->
|
||||
|
||||
.row {
|
||||
margin-bottom: 5px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.traceId {
|
||||
width: 270px;
|
||||
}
|
||||
|
||||
.search-btn {
|
||||
cursor: pointer;
|
||||
width: 80px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 10px;
|
||||
}
|
||||
|
||||
.label {
|
||||
line-height: 24px;
|
||||
}
|
@@ -91,7 +91,7 @@ limitations under the License. -->
|
||||
import timeFormat from "@/utils/timeFormat";
|
||||
import ConditionTags from "@/views/components/ConditionTags.vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { EntityType, QueryOrders, Status } from "../../data";
|
||||
import { EntityType, QueryOrders, Status } from "@/views/dashboard/data";
|
||||
import type { LayoutConfig } from "@/types/dashboard";
|
||||
|
||||
const FiltersKeys: { [key: string]: string } = {
|
@@ -83,9 +83,9 @@ limitations under the License. -->
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useTraceStore } from "@/store/modules/trace";
|
||||
import { useTraceStore, PageSize } from "@/store/modules/trace";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { QueryOrders } from "../../data";
|
||||
import { QueryOrders } from "@/views/dashboard/data";
|
||||
import type { Option } from "@/types/app";
|
||||
import type { Trace } from "@/types/trace";
|
||||
import { dateFormat } from "@/utils/dateFormat";
|
||||
@@ -94,7 +94,7 @@ limitations under the License. -->
|
||||
const traceStore = useTraceStore();
|
||||
const loading = ref<boolean>(false);
|
||||
const selectedKey = ref<string>("");
|
||||
const pageSize = 20;
|
||||
const pageSize = PageSize;
|
||||
const total = computed(() =>
|
||||
traceStore.traceList.length === pageSize
|
||||
? pageSize * traceStore.conditions.paging.pageNum + 1
|
@@ -13,14 +13,16 @@ limitations under the License. -->
|
||||
<template>
|
||||
<div class="trace-detail" v-loading="loading">
|
||||
<div class="trace-detail-wrapper clear" v-if="traceStore.currentTrace?.endpointNames">
|
||||
<h5 class="mb-5 mt-0">
|
||||
<span class="vm">{{ traceStore.currentTrace?.endpointNames?.[0] }}</span>
|
||||
<div class="trace-log-btn">
|
||||
<el-button size="small" class="mr-10" type="primary" @click="searchTraceLogs">
|
||||
<div class="flex-h" style="justify-content: space-between">
|
||||
<h5 class="mb-5 mt-0">
|
||||
<span class="vm">{{ traceStore.currentTrace?.endpointNames?.[0] }}</span>
|
||||
</h5>
|
||||
<div>
|
||||
<el-button size="small" class="mr-10" @click="searchTraceLogs">
|
||||
{{ t("viewLogs") }}
|
||||
</el-button>
|
||||
</div>
|
||||
</h5>
|
||||
</div>
|
||||
<div class="mb-5 blue">
|
||||
<Selector
|
||||
size="small"
|
||||
@@ -46,33 +48,13 @@ limitations under the License. -->
|
||||
<div class="tag mr-5">{{ t("spans") }}</div>
|
||||
<span class="sm">{{ traceStore.traceSpans.length }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<el-button class="grey" size="small" :class="{ ghost: displayMode !== 'List' }" @click="displayMode = 'List'">
|
||||
<Icon class="mr-5" size="sm" iconName="list-bulleted" />
|
||||
{{ t("list") }}
|
||||
</el-button>
|
||||
<el-button class="grey" size="small" :class="{ ghost: displayMode !== 'Tree' }" @click="displayMode = 'Tree'">
|
||||
<Icon class="mr-5" size="sm" iconName="issue-child" />
|
||||
{{ t("tree") }}
|
||||
</el-button>
|
||||
<el-button
|
||||
class="grey"
|
||||
size="small"
|
||||
:class="{ ghost: displayMode !== 'Table' }"
|
||||
@click="displayMode = 'Table'"
|
||||
>
|
||||
<Icon class="mr-5" size="sm" iconName="table" />
|
||||
{{ t("table") }}
|
||||
</el-button>
|
||||
<el-button
|
||||
class="grey"
|
||||
size="small"
|
||||
:class="{ ghost: displayMode !== 'Statistics' }"
|
||||
@click="displayMode = 'Statistics'"
|
||||
>
|
||||
<Icon class="mr-5" size="sm" iconName="statistics-bulleted" />
|
||||
{{ t("statistics") }}
|
||||
</el-button>
|
||||
<div class="flex-h trace-type item">
|
||||
<el-radio-group v-model="spansGraphType" size="small">
|
||||
<el-radio-button v-for="option in GraphTypeOptions" :key="option.value" :value="option.value">
|
||||
<Icon :iconName="option.icon" />
|
||||
{{ t(option.label) }}
|
||||
</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -80,7 +62,7 @@ limitations under the License. -->
|
||||
<div class="trace-chart">
|
||||
<component
|
||||
v-if="traceStore.currentTrace?.endpointNames"
|
||||
:is="displayMode"
|
||||
:is="spansGraphType"
|
||||
:data="traceStore.traceSpans"
|
||||
:traceId="traceStore.currentTrace?.traceIds?.[0]?.value"
|
||||
:showBtnDetail="false"
|
||||
@@ -95,13 +77,14 @@ limitations under the License. -->
|
||||
import { useTraceStore } from "@/store/modules/trace";
|
||||
import type { Option } from "@/types/app";
|
||||
import copy from "@/utils/copy";
|
||||
import graphs from "./components/index";
|
||||
import graphs from "../VisGraph/index";
|
||||
import { ElMessage } from "element-plus";
|
||||
import getDashboard from "@/hooks/useDashboardsSession";
|
||||
import type { LayoutConfig } from "@/types/dashboard";
|
||||
import { dateFormat } from "@/utils/dateFormat";
|
||||
import { useAppStoreWithOut } from "@/store/modules/app";
|
||||
import { WidgetType } from "@/views/dashboard/data";
|
||||
import { GraphTypeOptions } from "../VisGraph/constant";
|
||||
|
||||
const props = {
|
||||
serviceId: { type: String, default: "" },
|
||||
@@ -120,7 +103,7 @@ limitations under the License. -->
|
||||
const traceStore = useTraceStore();
|
||||
const loading = ref<boolean>(false);
|
||||
const traceId = ref<string>("");
|
||||
const displayMode = ref<string>("List");
|
||||
const spansGraphType = ref<string>("List");
|
||||
|
||||
function handleClick() {
|
||||
copy(traceId.value || traceStore.currentTrace?.traceIds?.[0]?.value);
|
||||
@@ -150,7 +133,6 @@ limitations under the License. -->
|
||||
}
|
||||
return {
|
||||
traceStore,
|
||||
displayMode,
|
||||
dateFormat,
|
||||
changeTraceId,
|
||||
handleClick,
|
||||
@@ -160,6 +142,8 @@ limitations under the License. -->
|
||||
loading,
|
||||
traceId,
|
||||
WidgetType,
|
||||
spansGraphType,
|
||||
GraphTypeOptions,
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -196,6 +180,7 @@ limitations under the License. -->
|
||||
|
||||
.item {
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.trace-detail-ids {
|
||||
@@ -208,10 +193,6 @@ limitations under the License. -->
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.trace-log-btn {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.tag {
|
||||
display: inline-block;
|
||||
border-radius: 4px;
|
||||
@@ -225,4 +206,16 @@ limitations under the License. -->
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.trace-type {
|
||||
// padding: 10px 0 10px 10px;
|
||||
border-bottom: 1px solid $border-color-primary;
|
||||
|
||||
/* Make radio buttons content align nicely with icons */
|
||||
:deep(.el-radio-button__inner) {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,47 @@
|
||||
<!-- Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
contributor license agreements. See the NOTICE file distributed with
|
||||
this work for additional information regarding copyright ownership.
|
||||
The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
(the "License"); you may not use this file except in compliance with
|
||||
the License. You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License. -->
|
||||
<template>
|
||||
<div class="trace-query flex-v">
|
||||
<TracesTable />
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { onUnmounted } from "vue";
|
||||
import type { PropType } from "vue";
|
||||
import type { LayoutConfig } from "@/types/dashboard";
|
||||
import { useTraceStore } from "@/store/modules/trace";
|
||||
import TracesTable from "./TracesTable.vue";
|
||||
|
||||
/*global defineProps */
|
||||
defineProps({
|
||||
data: {
|
||||
type: Object as PropType<LayoutConfig>,
|
||||
default: () => ({}),
|
||||
},
|
||||
});
|
||||
const traceStore = useTraceStore();
|
||||
onUnmounted(() => {
|
||||
traceStore.setTraceList([]);
|
||||
traceStore.setSelectedSpan(null);
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.trace-query {
|
||||
width: 100%;
|
||||
font-size: $font-size-smaller;
|
||||
overflow: auto;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,107 @@
|
||||
<!-- Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
contributor license agreements. See the NOTICE file distributed with
|
||||
this work for additional information regarding copyright ownership.
|
||||
The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
(the "License"); you may not use this file except in compliance with
|
||||
the License. You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License. -->
|
||||
<template>
|
||||
<div class="trace-min-timeline">
|
||||
<div class="timeline-marker-fixed">
|
||||
<svg width="100%" height="20px">
|
||||
<MinTimelineMarker :minTimestamp="minTimestamp" :maxTimestamp="maxTimestamp" :lineHeight="20" />
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
class="timeline-content scroll_bar_style"
|
||||
:style="{ paddingRight: (trace.spans.length + 1) * rowHeight < 200 ? '20px' : '14px' }"
|
||||
>
|
||||
<svg ref="svgEle" width="100%" :height="`${(trace.spans.length + 1) * rowHeight}px`">
|
||||
<MinTimelineOverlay
|
||||
:minTimestamp="minTimestamp"
|
||||
:maxTimestamp="maxTimestamp"
|
||||
@setSelectedMinTimestamp="setSelectedMinTimestamp"
|
||||
@setSelectedMaxTimestamp="setSelectedMaxTimestamp"
|
||||
/>
|
||||
<MinTimelineSelector
|
||||
:minTimestamp="minTimestamp"
|
||||
:maxTimestamp="maxTimestamp"
|
||||
:selectedMinTimestamp="selectedMinTimestamp"
|
||||
:selectedMaxTimestamp="selectedMaxTimestamp"
|
||||
@setSelectedMinTimestamp="setSelectedMinTimestamp"
|
||||
@setSelectedMaxTimestamp="setSelectedMaxTimestamp"
|
||||
/>
|
||||
<g
|
||||
v-for="(item, index) in trace.spans"
|
||||
:key="index"
|
||||
:transform="`translate(0, ${(index + 1) * rowHeight + 3})`"
|
||||
>
|
||||
<SpanNode :span="item" :minTimestamp="minTimestamp" :maxTimestamp="maxTimestamp" :depth="index + 1" />
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref } from "vue";
|
||||
import type { Trace } from "@/types/trace";
|
||||
import SpanNode from "./SpanNode.vue";
|
||||
import MinTimelineMarker from "./MinTimelineMarker.vue";
|
||||
import MinTimelineOverlay from "./MinTimelineOverlay.vue";
|
||||
import MinTimelineSelector from "./MinTimelineSelector.vue";
|
||||
|
||||
interface Props {
|
||||
trace: Trace;
|
||||
minTimestamp: number;
|
||||
maxTimestamp: number;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const svgEle = ref<SVGSVGElement | null>(null);
|
||||
const rowHeight = 12;
|
||||
|
||||
const selectedMinTimestamp = ref<number>(props.minTimestamp);
|
||||
const selectedMaxTimestamp = ref<number>(props.maxTimestamp);
|
||||
|
||||
const emit = defineEmits(["updateSelectedMaxTimestamp", "updateSelectedMinTimestamp"]);
|
||||
|
||||
const setSelectedMinTimestamp = (value: number) => {
|
||||
selectedMinTimestamp.value = value;
|
||||
emit("updateSelectedMinTimestamp", value);
|
||||
};
|
||||
const setSelectedMaxTimestamp = (value: number) => {
|
||||
selectedMaxTimestamp.value = value;
|
||||
emit("updateSelectedMaxTimestamp", value);
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.trace-min-timeline {
|
||||
width: 100%;
|
||||
max-height: 200px;
|
||||
border-bottom: 1px solid var(--el-border-color-light);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.timeline-marker-fixed {
|
||||
width: 100%;
|
||||
padding-right: 20px;
|
||||
padding-top: 5px;
|
||||
background: var(--el-bg-color);
|
||||
border-bottom: 1px solid var(--el-border-color-light);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.timeline-content {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,57 @@
|
||||
<!-- Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
contributor license agreements. See the NOTICE file distributed with
|
||||
this work for additional information regarding copyright ownership.
|
||||
The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
(the "License"); you may not use this file except in compliance with
|
||||
the License. You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License. -->
|
||||
<template>
|
||||
<g v-for="(marker, index) in markers" :key="marker.duration">
|
||||
<line
|
||||
:x1="`${marker.position}%`"
|
||||
:y1="0"
|
||||
:x2="`${marker.position}%`"
|
||||
:y2="lineHeight ? `${lineHeight}` : '100%'"
|
||||
stroke="var(--el-border-color-light)"
|
||||
/>
|
||||
<text
|
||||
:key="`label-${marker.duration}`"
|
||||
:x="`${marker.position}%`"
|
||||
:y="12"
|
||||
font-size="10"
|
||||
fill="var(--sw-font-grey-color)"
|
||||
text-anchor="right"
|
||||
:transform="`translate(${index === markers.length - 1 ? -50 : 5}, 0)`"
|
||||
>
|
||||
{{ marker.duration }}ms
|
||||
</text>
|
||||
</g>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { computed } from "vue";
|
||||
|
||||
interface Props {
|
||||
minTimestamp: number;
|
||||
maxTimestamp: number;
|
||||
lineHeight?: number | string;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const markers = computed(() => {
|
||||
const maxDuration = props.maxTimestamp - props.minTimestamp;
|
||||
const markerDurations = [0, (maxDuration * 1) / 3, (maxDuration * 2) / 3, maxDuration];
|
||||
|
||||
return markerDurations.map((duration) => ({
|
||||
duration: duration.toFixed(2),
|
||||
position: maxDuration > 0 ? (duration / maxDuration) * 100 : 0,
|
||||
}));
|
||||
});
|
||||
</script>
|
||||
<style scoped></style>
|
@@ -0,0 +1,147 @@
|
||||
<!-- Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
contributor license agreements. See the NOTICE file distributed with
|
||||
this work for additional information regarding copyright ownership.
|
||||
The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
(the "License"); you may not use this file except in compliance with
|
||||
the License. You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License. -->
|
||||
<template>
|
||||
<g>
|
||||
<rect
|
||||
v-if="mouseDownX !== undefined && currentX !== undefined"
|
||||
:x="`${Math.min(mouseDownX, currentX)}%`"
|
||||
y="0"
|
||||
:width="`${Math.abs(mouseDownX - currentX)}%`"
|
||||
height="100%"
|
||||
fill="var(--el-color-primary-light-5)"
|
||||
fill-opacity="0.2"
|
||||
pointer-events="none"
|
||||
/>
|
||||
<rect
|
||||
ref="rootEl"
|
||||
x="0"
|
||||
y="0"
|
||||
width="100%"
|
||||
height="100%"
|
||||
@mousedown="handleMouseDown"
|
||||
@mousemove="handleMouseHoverMove"
|
||||
@mouseleave="handleMouseHoverLeave"
|
||||
fill-opacity="0"
|
||||
cursor="col-resize"
|
||||
/>
|
||||
<line
|
||||
v-if="hoverX"
|
||||
:x1="`${hoverX}%`"
|
||||
:y1="0"
|
||||
:x2="`${hoverX}%`"
|
||||
y2="100%"
|
||||
stroke="var(--el-color-primary-light-5)"
|
||||
stroke-width="1"
|
||||
pointer-events="none"
|
||||
/>
|
||||
</g>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { onBeforeUnmount, ref } from "vue";
|
||||
|
||||
interface Props {
|
||||
minTimestamp: number;
|
||||
maxTimestamp: number;
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: "setSelectedMaxTimestamp", value: number): void;
|
||||
(e: "setSelectedMinTimestamp", value: number): void;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const emit = defineEmits<Emits>();
|
||||
const rootEl = ref<SVGRectElement | null>(null);
|
||||
const mouseDownX = ref<number | undefined>(undefined);
|
||||
const currentX = ref<number | undefined>(undefined);
|
||||
const hoverX = ref<number | undefined>(undefined);
|
||||
const mouseDownXRef = ref<number | undefined>(undefined);
|
||||
const isDragging = ref(false);
|
||||
|
||||
function handleMouseMove(e: MouseEvent) {
|
||||
if (!rootEl.value) {
|
||||
return;
|
||||
}
|
||||
const x = calculateX(rootEl.value.getBoundingClientRect(), e.pageX);
|
||||
currentX.value = x;
|
||||
}
|
||||
function handleMouseUp(e: MouseEvent) {
|
||||
if (!isDragging.value || !rootEl.value || mouseDownXRef.value === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const x = calculateX(rootEl.value.getBoundingClientRect(), e.pageX);
|
||||
const adjustedX = Math.abs(x - mouseDownXRef.value) < 1 ? x + 1 : x;
|
||||
|
||||
const t1 = (mouseDownXRef.value / 100) * (props.maxTimestamp - props.minTimestamp) + props.minTimestamp;
|
||||
const t2 = (adjustedX / 100) * (props.maxTimestamp - props.minTimestamp) + props.minTimestamp;
|
||||
const newMinTimestmap = Math.min(t1, t2);
|
||||
const newMaxTimestamp = Math.max(t1, t2);
|
||||
|
||||
emit("setSelectedMinTimestamp", newMinTimestmap);
|
||||
emit("setSelectedMaxTimestamp", newMaxTimestamp);
|
||||
|
||||
currentX.value = undefined;
|
||||
mouseDownX.value = undefined;
|
||||
mouseDownXRef.value = undefined;
|
||||
isDragging.value = false;
|
||||
|
||||
document.removeEventListener("mousemove", handleMouseMove);
|
||||
document.removeEventListener("mouseup", handleMouseUp);
|
||||
}
|
||||
|
||||
const calculateX = (parentRect: DOMRect, x: number) => {
|
||||
const value = ((x - parentRect.left) / (parentRect.right - parentRect.left)) * 100;
|
||||
if (value <= 0) {
|
||||
return 0;
|
||||
}
|
||||
if (value >= 100) {
|
||||
return 100;
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
function handleMouseDown(e: MouseEvent) {
|
||||
if (!rootEl.value) {
|
||||
return;
|
||||
}
|
||||
const x = calculateX(rootEl.value.getBoundingClientRect(), e.pageX);
|
||||
currentX.value = x;
|
||||
mouseDownX.value = x;
|
||||
mouseDownXRef.value = x;
|
||||
isDragging.value = true;
|
||||
|
||||
document.addEventListener("mousemove", handleMouseMove);
|
||||
document.addEventListener("mouseup", handleMouseUp);
|
||||
}
|
||||
|
||||
function handleMouseHoverMove(e: MouseEvent) {
|
||||
if (e.buttons !== 0 || !rootEl.value) {
|
||||
return;
|
||||
}
|
||||
const x = calculateX(rootEl.value.getBoundingClientRect(), e.pageX);
|
||||
hoverX.value = x;
|
||||
}
|
||||
|
||||
function handleMouseHoverLeave() {
|
||||
hoverX.value = undefined;
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener("mousemove", handleMouseMove);
|
||||
document.removeEventListener("mouseup", handleMouseUp);
|
||||
isDragging.value = false;
|
||||
});
|
||||
</script>
|
@@ -0,0 +1,157 @@
|
||||
<!-- Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
contributor license agreements. See the NOTICE file distributed with
|
||||
this work for additional information regarding copyright ownership.
|
||||
The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
(the "License"); you may not use this file except in compliance with
|
||||
the License. You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License. -->
|
||||
<template>
|
||||
<g>
|
||||
<rect
|
||||
x="0"
|
||||
y="0"
|
||||
:width="`${boundaryLeft}%`"
|
||||
height="100%"
|
||||
fill="var(--el-color-primary-light-8)"
|
||||
fill-opacity="0.6"
|
||||
pointer-events="none"
|
||||
/>
|
||||
<rect
|
||||
:x="`${boundaryRight}%`"
|
||||
y="0"
|
||||
:width="`${100 - boundaryRight}%`"
|
||||
height="100%"
|
||||
fill="var(--el-color-primary-light-8)"
|
||||
fill-opacity="0.6"
|
||||
pointer-events="none"
|
||||
/>
|
||||
<rect
|
||||
:x="`${boundaryLeft}%`"
|
||||
y="0"
|
||||
width="3"
|
||||
height="100%"
|
||||
fill="var(--el-color-primary-light-5)"
|
||||
transform="translate(-1)"
|
||||
pointer-events="none"
|
||||
/>
|
||||
<rect
|
||||
:x="`${boundaryRight}%`"
|
||||
y="0"
|
||||
width="3"
|
||||
height="100%"
|
||||
fill="var(--el-color-primary-light-5)"
|
||||
transform="translate(-1)"
|
||||
pointer-events="none"
|
||||
/>
|
||||
|
||||
<rect
|
||||
v-if="minMouseDownX !== undefined && minCurrentX !== undefined"
|
||||
:x="`${Math.min(minMouseDownX, minCurrentX)}%`"
|
||||
y="0"
|
||||
:width="`${Math.abs(minMouseDownX - minCurrentX)}%`"
|
||||
height="100%"
|
||||
fill="var(--el-color-primary-light-6)"
|
||||
fill-opacity="0.4"
|
||||
pointer-events="none"
|
||||
/>
|
||||
<rect
|
||||
v-if="maxMouseDownX !== undefined && maxCurrentX !== undefined"
|
||||
:x="`${Math.min(maxMouseDownX, maxCurrentX)}%`"
|
||||
y="0"
|
||||
:width="`${Math.abs(maxMouseDownX - maxCurrentX)}%`"
|
||||
height="100%"
|
||||
fill="var(--el-color-primary-light-6)"
|
||||
fill-opacity="0.4"
|
||||
pointer-events="none"
|
||||
/>
|
||||
<rect
|
||||
:x="`${boundaryLeft}%`"
|
||||
y="0"
|
||||
width="6"
|
||||
height="40%"
|
||||
fill="var(--el-color-primary)"
|
||||
@mousedown="minRangeHandler.onMouseDown"
|
||||
cursor="pointer"
|
||||
transform="translate(-3)"
|
||||
/>
|
||||
<rect
|
||||
:x="`${boundaryRight}%`"
|
||||
y="0"
|
||||
width="6"
|
||||
height="40%"
|
||||
fill="var(--el-color-primary)"
|
||||
@mousedown="maxRangeHandler.onMouseDown"
|
||||
cursor="pointer"
|
||||
transform="translate(-3)"
|
||||
/>
|
||||
</g>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, onMounted } from "vue";
|
||||
import { useRangeTimestampHandler, adjustPercentValue } from "./useHooks";
|
||||
|
||||
interface Props {
|
||||
minTimestamp: number;
|
||||
maxTimestamp: number;
|
||||
selectedMinTimestamp: number;
|
||||
selectedMaxTimestamp: number;
|
||||
}
|
||||
const emit = defineEmits<{
|
||||
(e: "setSelectedMinTimestamp", value: number): void;
|
||||
(e: "setSelectedMaxTimestamp", value: number): void;
|
||||
}>();
|
||||
const props = defineProps<Props>();
|
||||
const svgEle = ref<SVGSVGElement | null>(null);
|
||||
|
||||
onMounted(() => {
|
||||
const element = document.querySelector(".trace-min-timeline svg") as SVGSVGElement;
|
||||
if (element) {
|
||||
svgEle.value = element;
|
||||
}
|
||||
});
|
||||
const maxOpositeX = computed(
|
||||
() => ((props.selectedMaxTimestamp - props.minTimestamp) / (props.maxTimestamp - props.minTimestamp)) * 100,
|
||||
);
|
||||
const minOpositeX = computed(
|
||||
() => ((props.selectedMinTimestamp - props.minTimestamp) / (props.maxTimestamp - props.minTimestamp)) * 100,
|
||||
);
|
||||
|
||||
const minRangeHandler = computed(() => {
|
||||
return useRangeTimestampHandler({
|
||||
rootEl: svgEle.value,
|
||||
minTimestamp: props.minTimestamp,
|
||||
maxTimestamp: props.maxTimestamp,
|
||||
selectedTimestamp: props.selectedMaxTimestamp,
|
||||
isSmallerThanOpositeX: true,
|
||||
setTimestamp: (value) => emit("setSelectedMinTimestamp", value),
|
||||
});
|
||||
});
|
||||
const maxRangeHandler = computed(() =>
|
||||
useRangeTimestampHandler({
|
||||
rootEl: svgEle.value,
|
||||
minTimestamp: props.minTimestamp,
|
||||
maxTimestamp: props.maxTimestamp,
|
||||
selectedTimestamp: props.selectedMinTimestamp,
|
||||
isSmallerThanOpositeX: false,
|
||||
setTimestamp: (value) => emit("setSelectedMaxTimestamp", value),
|
||||
}),
|
||||
);
|
||||
|
||||
const boundaryLeft = computed(() => {
|
||||
return adjustPercentValue(minOpositeX.value);
|
||||
});
|
||||
|
||||
const boundaryRight = computed(() => adjustPercentValue(maxOpositeX.value));
|
||||
|
||||
const minMouseDownX = computed(() => minRangeHandler.value.mouseDownX.value);
|
||||
const minCurrentX = computed(() => minRangeHandler.value.currentX.value);
|
||||
const maxMouseDownX = computed(() => maxRangeHandler.value.mouseDownX.value);
|
||||
const maxCurrentX = computed(() => maxRangeHandler.value.currentX.value);
|
||||
</script>
|
@@ -0,0 +1,120 @@
|
||||
<!-- Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
contributor license agreements. See the NOTICE file distributed with
|
||||
this work for additional information regarding copyright ownership.
|
||||
The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
(the "License"); you may not use this file except in compliance with
|
||||
the License. You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License. -->
|
||||
<template>
|
||||
<rect :x="`${startPct}%`" :y="0" :width="`${widthPct}%`" :height="barHeight" :fill="barColor" rx="2" ry="2" />
|
||||
<!-- Label and Duration Text -->
|
||||
<text
|
||||
v-if="showLabel"
|
||||
:x="0"
|
||||
:y="barHeight - 7"
|
||||
font-size="10"
|
||||
fill="var(--sw-font-grey-color)"
|
||||
text-anchor="start"
|
||||
class="span-label"
|
||||
>
|
||||
{{ span.label || "Unknown" }}
|
||||
</text>
|
||||
<text
|
||||
v-if="showDuration"
|
||||
:x="`${100}%`"
|
||||
:y="barHeight - 7"
|
||||
font-size="10"
|
||||
fill="var(--sw-font-grey-color)"
|
||||
text-anchor="end"
|
||||
class="span-duration"
|
||||
>
|
||||
{{ span.duration }}ms
|
||||
</text>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from "vue";
|
||||
import type { Span } from "@/types/trace";
|
||||
import { getServiceColor } from "@/utils/color";
|
||||
|
||||
interface Props {
|
||||
span: Span;
|
||||
minTimestamp: number;
|
||||
maxTimestamp: number;
|
||||
depth: number;
|
||||
showDuration?: boolean;
|
||||
showLabel?: boolean;
|
||||
selectedMaxTimestamp?: number;
|
||||
selectedMinTimestamp?: number;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const barHeight = 3;
|
||||
|
||||
const widthScale = computed(() => {
|
||||
const { selectedMinTimestamp, selectedMaxTimestamp, minTimestamp, maxTimestamp } = props;
|
||||
let max = maxTimestamp - minTimestamp;
|
||||
if (selectedMaxTimestamp !== undefined && selectedMinTimestamp !== undefined) {
|
||||
max = selectedMaxTimestamp - selectedMinTimestamp;
|
||||
}
|
||||
return (duration: number | undefined | null) => {
|
||||
const d = Math.max(0, duration || 0);
|
||||
return (d / max) * 100;
|
||||
};
|
||||
});
|
||||
const startPct = computed(() => {
|
||||
const { span, selectedMinTimestamp, minTimestamp } = props;
|
||||
const end = span.endTime;
|
||||
let start = span.startTime;
|
||||
if (selectedMinTimestamp !== undefined) {
|
||||
start = selectedMinTimestamp > start ? (end < selectedMinTimestamp ? 0 : selectedMinTimestamp) : start;
|
||||
}
|
||||
const dur = start - (selectedMinTimestamp || minTimestamp);
|
||||
|
||||
return Math.max(0, widthScale.value(dur));
|
||||
});
|
||||
|
||||
const widthPct = computed(() => {
|
||||
const { span, selectedMinTimestamp, selectedMaxTimestamp } = props;
|
||||
let start = span.startTime;
|
||||
let end = span.endTime;
|
||||
if (selectedMinTimestamp !== undefined) {
|
||||
start = selectedMinTimestamp > start ? selectedMinTimestamp : start;
|
||||
if (end < selectedMinTimestamp) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
if (selectedMaxTimestamp !== undefined) {
|
||||
end = selectedMaxTimestamp < end ? selectedMaxTimestamp : end;
|
||||
if (span.startTime > selectedMaxTimestamp) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
const dur = end - start;
|
||||
return Math.max(0, widthScale.value(dur));
|
||||
});
|
||||
|
||||
const barColor = computed(() => {
|
||||
const serviceName = props.span.serviceCode || "";
|
||||
return getServiceColor(serviceName);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.span-label {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.span-duration {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
font-weight: 400;
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,59 @@
|
||||
<!-- Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
contributor license agreements. See the NOTICE file distributed with
|
||||
this work for additional information regarding copyright ownership.
|
||||
The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
(the "License"); you may not use this file except in compliance with
|
||||
the License. You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License. -->
|
||||
<template>
|
||||
<div class="timeline-tool flex-h">
|
||||
<div class="flex-h trace-type item">
|
||||
<el-radio-group v-model="spansGraphType" size="small">
|
||||
<el-radio-button v-for="option in GraphTypeOptions" :key="option.value" :value="option.value">
|
||||
<Icon :iconName="option.icon" />
|
||||
{{ t(option.label) }}
|
||||
</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
<div>
|
||||
<el-button size="small" @click="onToggleMinTimeline">
|
||||
<Icon iconName="sort" size="middle" />
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { GraphTypeOptions } from "../VisGraph/constant";
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "toggleMinTimeline"): void;
|
||||
(e: "updateSpansGraphType", value: string): void;
|
||||
}>();
|
||||
const { t } = useI18n();
|
||||
const spansGraphType = ref<string>(GraphTypeOptions[2].value);
|
||||
|
||||
function onToggleMinTimeline() {
|
||||
emit("toggleMinTimeline");
|
||||
}
|
||||
|
||||
// Watch for changes in spansGraphType and emit to parent
|
||||
watch(spansGraphType, (newValue) => {
|
||||
emit("updateSpansGraphType", newValue);
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.timeline-tool {
|
||||
justify-content: space-between;
|
||||
padding: 10px 5px;
|
||||
border-bottom: 1px solid var(--el-border-color-light);
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,175 @@
|
||||
<!-- Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
contributor license agreements. See the NOTICE file distributed with
|
||||
this work for additional information regarding copyright ownership.
|
||||
The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
(the "License"); you may not use this file except in compliance with
|
||||
the License. You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License. -->
|
||||
<template>
|
||||
<div class="trace-query-content">
|
||||
<div class="trace-info">
|
||||
<div class="flex-h" style="justify-content: space-between">
|
||||
<h3>{{ trace.label }}</h3>
|
||||
<div>
|
||||
<el-dropdown @command="handleDownload" trigger="click">
|
||||
<el-button size="small">
|
||||
{{ t("download") }}
|
||||
<el-icon class="el-icon--right"><arrow-down /></el-icon>
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="json">Download JSON</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
<div class="trace-meta flex-h">
|
||||
<div>
|
||||
<span class="grey mr-5">{{ t("duration") }}</span>
|
||||
<span class="value">{{ trace.duration }}ms</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="grey mr-5">{{ t("services") }}</span>
|
||||
<span class="value">{{ trace.serviceCode || "Unknown" }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="grey mr-5">{{ t("totalSpans") }}</span>
|
||||
<span class="value">{{ trace.spans?.length || 0 }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="grey mr-5">{{ t("traceID") }}</span>
|
||||
<span class="value">{{ trace.traceId }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-h">
|
||||
<div class="detail-section-timeline flex-v">
|
||||
<MinTimeline
|
||||
v-show="minTimelineVisible"
|
||||
:trace="trace"
|
||||
:minTimestamp="minTimestamp"
|
||||
:maxTimestamp="maxTimestamp"
|
||||
@updateSelectedMaxTimestamp="handleSelectedMaxTimestamp"
|
||||
@updateSelectedMinTimestamp="handleSelectedMinTimestamp"
|
||||
/>
|
||||
<TimelineTool @toggleMinTimeline="toggleMinTimeline" @updateSpansGraphType="handleSpansGraphTypeUpdate" />
|
||||
<component
|
||||
v-if="traceStore.currentTrace?.endpointNames"
|
||||
:is="graphs[spansGraphType as keyof typeof graphs]"
|
||||
:data="trace.spans"
|
||||
:traceId="traceStore.currentTrace?.traceId"
|
||||
:showBtnDetail="false"
|
||||
:headerType="WidgetType.Trace"
|
||||
:selectedMaxTimestamp="selectedMaxTimestamp"
|
||||
:selectedMinTimestamp="selectedMinTimestamp"
|
||||
:minTimestamp="minTimestamp"
|
||||
:maxTimestamp="maxTimestamp"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { ArrowDown } from "@element-plus/icons-vue";
|
||||
import type { Trace } from "@/types/trace";
|
||||
import MinTimeline from "./MinTimeline.vue";
|
||||
import { saveFileAsJSON } from "@/utils/file";
|
||||
import { useTraceStore } from "@/store/modules/trace";
|
||||
import TimelineTool from "./TimelineTool.vue";
|
||||
import graphs from "../VisGraph/index";
|
||||
import { WidgetType } from "@/views/dashboard/data";
|
||||
import { GraphTypeOptions } from "../VisGraph/constant";
|
||||
|
||||
interface Props {
|
||||
trace: Trace;
|
||||
}
|
||||
|
||||
const { t } = useI18n();
|
||||
const props = defineProps<Props>();
|
||||
const traceStore = useTraceStore();
|
||||
// Time range like xScale domain [0, max]
|
||||
const minTimestamp = computed(() => {
|
||||
if (!props.trace.spans.length) return 0;
|
||||
return Math.min(...props.trace.spans.map((s) => s.startTime || 0));
|
||||
});
|
||||
|
||||
const maxTimestamp = computed(() => {
|
||||
const timestamps = props.trace.spans.map((span) => span.endTime || 0);
|
||||
if (timestamps.length === 0) return 0;
|
||||
|
||||
return Math.max(...timestamps);
|
||||
});
|
||||
const selectedMaxTimestamp = ref<number>(maxTimestamp.value);
|
||||
const selectedMinTimestamp = ref<number>(minTimestamp.value);
|
||||
const minTimelineVisible = ref<boolean>(true);
|
||||
const spansGraphType = ref<string>(GraphTypeOptions[2].value);
|
||||
|
||||
function handleSelectedMaxTimestamp(value: number) {
|
||||
selectedMaxTimestamp.value = value;
|
||||
}
|
||||
|
||||
function handleSelectedMinTimestamp(value: number) {
|
||||
selectedMinTimestamp.value = value;
|
||||
}
|
||||
|
||||
function toggleMinTimeline() {
|
||||
minTimelineVisible.value = !minTimelineVisible.value;
|
||||
}
|
||||
|
||||
function handleSpansGraphTypeUpdate(value: string) {
|
||||
spansGraphType.value = value;
|
||||
}
|
||||
|
||||
function handleDownload() {
|
||||
const trace = props.trace;
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
||||
const baseFilename = `trace-${trace.traceId}-${timestamp}`;
|
||||
const spans = trace.spans.map((span) => {
|
||||
const { duration, label, ...newSpan } = span;
|
||||
return newSpan;
|
||||
});
|
||||
try {
|
||||
saveFileAsJSON(spans, `${baseFilename}.json`);
|
||||
ElMessage.success("Trace data downloaded as JSON");
|
||||
} catch (error) {
|
||||
console.error("Download error:", error);
|
||||
ElMessage.error("Failed to download file");
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.trace-info {
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 1px solid var(--el-border-color-light);
|
||||
}
|
||||
|
||||
.trace-info h3 {
|
||||
margin: 0 0 10px;
|
||||
color: var(--el-text-color-primary);
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.trace-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.detail-section-timeline {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,379 @@
|
||||
<!-- Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
contributor license agreements. See the NOTICE file distributed with
|
||||
this work for additional information regarding copyright ownership.
|
||||
The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
(the "License"); you may not use this file except in compliance with
|
||||
the License. You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License. -->
|
||||
<template>
|
||||
<div class="flex-h result-header">
|
||||
<div style="align-items: center"> {{ filteredTraces.length }} of {{ totalTraces }} Results </div>
|
||||
<div class="flex-h" style="align-items: center">
|
||||
<el-switch
|
||||
v-model="expandAll"
|
||||
size="large"
|
||||
active-text="Expand All"
|
||||
inactive-text="Collapse All"
|
||||
class="mr-20"
|
||||
@change="toggleAllExpansion"
|
||||
/>
|
||||
<Selector
|
||||
placeholder="Service filters"
|
||||
@change="changeServiceFilters"
|
||||
:value="selectedServiceNames"
|
||||
:options="serviceNames"
|
||||
:multiple="true"
|
||||
style="width: 500px"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="trace-query-table scroll_bar_style">
|
||||
<el-table
|
||||
ref="tableRef"
|
||||
:data="filteredTraces"
|
||||
:border="false"
|
||||
:preserve-expanded-content="true"
|
||||
:default-sort="{ prop: 'duration', order: 'descending' }"
|
||||
style="width: 100%"
|
||||
v-loading="traceStore.loading"
|
||||
>
|
||||
<el-table-column type="expand">
|
||||
<template #default="props">
|
||||
<div class="flex-h service-tags">
|
||||
<el-tag
|
||||
v-for="(value, key) in getEndpoints(props.row.spans)"
|
||||
:key="key"
|
||||
class="mr-5 cp"
|
||||
disable-transitions
|
||||
:style="{ backgroundColor: getServiceColor(value[0]), color: 'white', border: 'none' }"
|
||||
@click="toggleServiceTags(value[0], props.row)"
|
||||
>
|
||||
{{ value[0] }}: {{ value[1] }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="Root" prop="label" />
|
||||
<el-table-column label="Start Time" prop="timestamp" width="220">
|
||||
<template #default="props">
|
||||
{{ dateFormat(props.row.start) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="Spans" prop="spans.length" width="100" />
|
||||
<el-table-column label="Duration (ms)" prop="duration" width="200" sortable>
|
||||
<template #default="props">
|
||||
<div class="duration-cell">
|
||||
<el-progress
|
||||
:percentage="getDurationProgress(props.row.duration)"
|
||||
:stroke-width="22"
|
||||
:text-inside="true"
|
||||
color="rgba(64, 158, 255, 0.4)"
|
||||
>
|
||||
<div class="duration-value">{{ props.row.duration || 0 }}ms</div>
|
||||
</el-progress>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column fixed="right" label="Operations" width="120">
|
||||
<template #default="props">
|
||||
<el-button size="small" @click="(e: MouseEvent) => handleShowTrace(e, props.row)"> Show </el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- Loading indicator -->
|
||||
<div v-if="loading" class="loading-container">
|
||||
<el-icon class="is-loading">
|
||||
<Loading />
|
||||
</el-icon>
|
||||
<span>Loading more traces...</span>
|
||||
</div>
|
||||
|
||||
<!-- Load more button (fallback) -->
|
||||
<div v-else-if="hasMoreItems" class="load-more-container">
|
||||
<el-button @click="loadMoreItems" :loading="loading">
|
||||
Load More ({{ totalTraces - filteredTraces.length }} remaining)
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Trace Details Dialog -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
fullscreen
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="true"
|
||||
destroy-on-close
|
||||
>
|
||||
<TraceContent v-if="selectedTrace" :trace="selectedTrace" />
|
||||
</el-dialog>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, nextTick, watch, onMounted, onUnmounted } from "vue";
|
||||
import { ElTable, ElDialog, ElIcon } from "element-plus";
|
||||
import { Loading } from "@element-plus/icons-vue";
|
||||
import { dateFormat } from "@/utils/dateFormat";
|
||||
import { useTraceStore } from "@/store/modules/trace";
|
||||
import type { Trace } from "@/types/trace";
|
||||
import type { Option } from "@/types/app";
|
||||
import { getServiceColor } from "@/utils/color";
|
||||
import TraceContent from "./TraceContent.vue";
|
||||
|
||||
const PageSize = 10;
|
||||
const traceStore = useTraceStore();
|
||||
const expandAll = ref<boolean>(true);
|
||||
const tableRef = ref<InstanceType<typeof ElTable>>();
|
||||
const selectedServiceNames = ref<string[]>([]);
|
||||
const dialogVisible = ref<boolean>(false);
|
||||
const selectedTrace = ref<Trace | null>(null);
|
||||
// Infinite scroll state
|
||||
const loadedItemsCount = ref<number>(PageSize);
|
||||
const loading = ref<boolean>(false);
|
||||
const loadMoreThreshold = 100; // pixels from bottom to trigger load
|
||||
|
||||
// Calculate max duration for progress bar scaling
|
||||
const maxDuration = computed(() => {
|
||||
if (!traceStore.traceList.length) return 1;
|
||||
const durations = traceStore.traceList.map((trace: Trace) => trace.duration || 0);
|
||||
return Math.max(...durations);
|
||||
});
|
||||
|
||||
// All filtered traces based on selected service names
|
||||
const allFilteredTraces = computed<Trace[]>(() => {
|
||||
const rows = traceStore.traceList as Trace[];
|
||||
if (!selectedServiceNames.value.length) return rows;
|
||||
const selected = new Set(selectedServiceNames.value);
|
||||
return rows.filter((row) => {
|
||||
const rowService = row?.serviceCode;
|
||||
if (rowService && selected.has(rowService)) return true;
|
||||
for (const s of row.spans || []) {
|
||||
const name = s?.serviceCode;
|
||||
if (name && selected.has(name)) return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
});
|
||||
|
||||
const filteredTraces = computed<Trace[]>(() => {
|
||||
return allFilteredTraces.value.slice(0, loadedItemsCount.value);
|
||||
});
|
||||
|
||||
const totalTraces = computed<number>(() => allFilteredTraces.value.length);
|
||||
|
||||
const hasMoreItems = computed<boolean>(() => {
|
||||
return loadedItemsCount.value < allFilteredTraces.value.length;
|
||||
});
|
||||
|
||||
// Service names options for the header Selector
|
||||
const serviceNames = computed<Option[]>(() => {
|
||||
const names = new Set<string>();
|
||||
const rows = traceStore.traceList as Trace[];
|
||||
for (const row of rows) {
|
||||
if (row?.serviceCode) {
|
||||
names.add(row.serviceCode);
|
||||
}
|
||||
for (const s of row.spans || []) {
|
||||
if (s?.serviceCode) {
|
||||
names.add(s.serviceCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
return Array.from(names)
|
||||
.sort((a, b) => a.localeCompare(b))
|
||||
.map((n) => ({ label: n, value: n }));
|
||||
});
|
||||
|
||||
function getEndpoints(spans: Trace[]) {
|
||||
const endpoints = new Map<string, number>();
|
||||
for (const d of spans) {
|
||||
endpoints.set(d.serviceCode, (endpoints.get(d.serviceCode) || 0) + 1);
|
||||
}
|
||||
return endpoints;
|
||||
}
|
||||
|
||||
function handleShowTrace(e: MouseEvent, row: Trace) {
|
||||
traceStore.setCurrentTrace(row);
|
||||
selectedTrace.value = row;
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
function getDurationProgress(duration: number): number {
|
||||
if (maxDuration.value === 0) return 0;
|
||||
return Math.round((duration / maxDuration.value) * 100);
|
||||
}
|
||||
|
||||
function toggleServiceTags(serviceName: string, row: Trace) {
|
||||
selectedServiceNames.value = selectedServiceNames.value.includes(serviceName)
|
||||
? selectedServiceNames.value.filter((name) => name !== serviceName)
|
||||
: [...selectedServiceNames.value, serviceName];
|
||||
|
||||
// Expand the row to show the service tags
|
||||
nextTick(() => {
|
||||
if (tableRef.value) {
|
||||
tableRef.value.toggleRowExpansion(row, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function changeServiceFilters(selected: Option[]) {
|
||||
selectedServiceNames.value = selected.map((o) => String(o.value));
|
||||
}
|
||||
|
||||
// Infinite scroll handlers
|
||||
function loadMoreItems() {
|
||||
if (loading.value || !hasMoreItems.value) return;
|
||||
|
||||
loading.value = true;
|
||||
|
||||
// Simulate loading delay for better UX
|
||||
setTimeout(() => {
|
||||
const nextBatch = Math.min(PageSize, allFilteredTraces.value.length - loadedItemsCount.value);
|
||||
loadedItemsCount.value += nextBatch;
|
||||
|
||||
// Apply current expand state to all currently visible items
|
||||
nextTick(() => {
|
||||
if (tableRef.value) {
|
||||
const allVisibleItems = filteredTraces.value;
|
||||
for (const row of allVisibleItems) {
|
||||
tableRef.value.toggleRowExpansion(row, expandAll.value);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
loading.value = false;
|
||||
}, 300);
|
||||
}
|
||||
|
||||
function handleScroll(event: Event) {
|
||||
const target = event.target as HTMLElement;
|
||||
const { scrollTop, scrollHeight, clientHeight } = target;
|
||||
|
||||
// Check if user is near the bottom
|
||||
if (scrollHeight - scrollTop - clientHeight < loadMoreThreshold && hasMoreItems.value && !loading.value) {
|
||||
loadMoreItems();
|
||||
}
|
||||
}
|
||||
|
||||
// Reset loaded items when filters change
|
||||
watch(selectedServiceNames, () => {
|
||||
loadedItemsCount.value = PageSize;
|
||||
// Apply expand state to newly visible items after filter change
|
||||
nextTick(() => {
|
||||
if (tableRef.value) {
|
||||
const visibleItems = filteredTraces.value;
|
||||
for (const row of visibleItems) {
|
||||
tableRef.value.toggleRowExpansion(row, expandAll.value);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Watch for expandAll state changes to apply to all visible items
|
||||
watch(expandAll, () => {
|
||||
nextTick(() => {
|
||||
if (tableRef.value) {
|
||||
const visibleItems = filteredTraces.value;
|
||||
for (const row of visibleItems) {
|
||||
tableRef.value.toggleRowExpansion(row, expandAll.value);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Watch for trace list changes (when new query is executed) to reapply expand state
|
||||
watch(
|
||||
() => traceStore.traceList,
|
||||
() => {
|
||||
// Reset loaded items count when new data is loaded
|
||||
loadedItemsCount.value = PageSize;
|
||||
|
||||
// Reapply expand state to newly loaded traces
|
||||
nextTick(() => {
|
||||
if (tableRef.value) {
|
||||
const visibleItems = filteredTraces.value;
|
||||
for (const row of visibleItems) {
|
||||
tableRef.value.toggleRowExpansion(row, expandAll.value);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
// Setup scroll listener
|
||||
onMounted(() => {
|
||||
const tableContainer = document.querySelector(".trace-query-table");
|
||||
if (tableContainer) {
|
||||
tableContainer.addEventListener("scroll", handleScroll);
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
const tableContainer = document.querySelector(".trace-query-table");
|
||||
if (tableContainer) {
|
||||
tableContainer.removeEventListener("scroll", handleScroll);
|
||||
}
|
||||
});
|
||||
|
||||
// Toggle all table row expansions (only for loaded items)
|
||||
function toggleAllExpansion() {
|
||||
// The expandAll watcher will handle applying the state to all visible items
|
||||
// This function just needs to trigger the change
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.trace-query-table {
|
||||
width: 100%;
|
||||
font-size: $font-size-smaller;
|
||||
overflow: auto;
|
||||
height: calc(100% - 100px);
|
||||
}
|
||||
|
||||
.service-tags {
|
||||
width: 100%;
|
||||
padding-left: 60px;
|
||||
}
|
||||
|
||||
.duration-cell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.duration-value {
|
||||
font-size: $font-size-smaller;
|
||||
font-weight: 500;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.result-header {
|
||||
justify-content: space-between;
|
||||
padding: 20px 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding: 20px 0;
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: $font-size-smaller;
|
||||
}
|
||||
|
||||
.load-more-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 20px 0;
|
||||
border-top: 1px solid var(--el-border-color-light);
|
||||
margin-top: 10px;
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,128 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { ref, computed } from "vue";
|
||||
|
||||
export const adjustPercentValue = (value: number) => {
|
||||
if (value <= 0) {
|
||||
return 0;
|
||||
}
|
||||
if (value >= 100) {
|
||||
return 100;
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
const calculateX = ({
|
||||
parentRect,
|
||||
x,
|
||||
opositeX,
|
||||
isSmallerThanOpositeX,
|
||||
}: {
|
||||
parentRect: DOMRect;
|
||||
x: number;
|
||||
opositeX: number;
|
||||
isSmallerThanOpositeX: boolean;
|
||||
}) => {
|
||||
let value = ((x - parentRect.left) / (parentRect.right - parentRect.left)) * 100;
|
||||
if (isSmallerThanOpositeX) {
|
||||
if (value >= opositeX) {
|
||||
value = opositeX - 1;
|
||||
}
|
||||
} else if (value <= opositeX) {
|
||||
value = opositeX + 1;
|
||||
}
|
||||
return adjustPercentValue(value);
|
||||
};
|
||||
|
||||
export const useRangeTimestampHandler = ({
|
||||
rootEl,
|
||||
minTimestamp,
|
||||
maxTimestamp,
|
||||
selectedTimestamp,
|
||||
isSmallerThanOpositeX,
|
||||
setTimestamp,
|
||||
}: {
|
||||
rootEl: SVGSVGElement | null;
|
||||
minTimestamp: number;
|
||||
maxTimestamp: number;
|
||||
selectedTimestamp: number;
|
||||
isSmallerThanOpositeX: boolean;
|
||||
setTimestamp: (value: number) => void;
|
||||
}) => {
|
||||
const currentX = ref<number>();
|
||||
const mouseDownX = ref<number>();
|
||||
const isDragging = ref(false);
|
||||
const selectedTimestampComputed = ref(selectedTimestamp);
|
||||
const opositeX = computed(() => {
|
||||
return ((selectedTimestampComputed.value - minTimestamp) / (maxTimestamp - minTimestamp)) * 100;
|
||||
});
|
||||
|
||||
const onMouseMove = (e: MouseEvent) => {
|
||||
if (!rootEl) {
|
||||
return;
|
||||
}
|
||||
const x = calculateX({
|
||||
parentRect: rootEl.getBoundingClientRect(),
|
||||
x: e.pageX,
|
||||
opositeX: opositeX.value,
|
||||
isSmallerThanOpositeX,
|
||||
});
|
||||
currentX.value = x;
|
||||
};
|
||||
|
||||
const onMouseUp = (e: MouseEvent) => {
|
||||
if (!rootEl) {
|
||||
return;
|
||||
}
|
||||
|
||||
const x = calculateX({
|
||||
parentRect: rootEl.getBoundingClientRect(),
|
||||
x: e.pageX,
|
||||
opositeX: opositeX.value,
|
||||
isSmallerThanOpositeX,
|
||||
});
|
||||
const timestamp = (x / 100) * (maxTimestamp - minTimestamp) + minTimestamp;
|
||||
selectedTimestampComputed.value = timestamp;
|
||||
setTimestamp(timestamp);
|
||||
currentX.value = undefined;
|
||||
mouseDownX.value = undefined;
|
||||
isDragging.value = false;
|
||||
|
||||
document.removeEventListener("mousemove", onMouseMove);
|
||||
document.removeEventListener("mouseup", onMouseUp);
|
||||
};
|
||||
|
||||
const onMouseDown = (e: MouseEvent) => {
|
||||
if (!rootEl) {
|
||||
return;
|
||||
}
|
||||
const x = calculateX({
|
||||
parentRect: rootEl.getBoundingClientRect(),
|
||||
x: (e.currentTarget as SVGRectElement).getBoundingClientRect().x + 3,
|
||||
opositeX: opositeX.value,
|
||||
isSmallerThanOpositeX,
|
||||
});
|
||||
currentX.value = x;
|
||||
mouseDownX.value = x;
|
||||
isDragging.value = true;
|
||||
|
||||
document.addEventListener("mousemove", onMouseMove);
|
||||
document.addEventListener("mouseup", onMouseUp);
|
||||
};
|
||||
|
||||
return { currentX, mouseDownX, onMouseDown, isDragging };
|
||||
};
|
@@ -17,56 +17,59 @@ limitations under the License. -->
|
||||
class="charts-item mr-5"
|
||||
v-for="(i, index) in traceStore.serviceList"
|
||||
:key="index"
|
||||
:style="`color:${computedScale(index)}`"
|
||||
:style="`color:${getServiceColor(i)}`"
|
||||
>
|
||||
<Icon iconName="issue-open-m" class="mr-5" size="sm" />
|
||||
<span>{{ i }}</span>
|
||||
</span>
|
||||
<el-button class="btn" size="small" type="primary" @click="downloadTrace">
|
||||
{{ t("exportImage") }}
|
||||
<el-button class="btn" size="small" @click="downloadTrace">
|
||||
<Icon iconName="download" size="sm" />
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="list">
|
||||
<Graph :data="data" :traceId="traceId" :type="TraceGraphType.LIST" />
|
||||
<Graph
|
||||
:data="data"
|
||||
:traceId="traceId"
|
||||
:type="TraceGraphType.LIST"
|
||||
:selectedMaxTimestamp="selectedMaxTimestamp"
|
||||
:selectedMinTimestamp="selectedMinTimestamp"
|
||||
:minTimestamp="minTimestamp"
|
||||
:maxTimestamp="maxTimestamp"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import type { PropType } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import * as d3 from "d3";
|
||||
import { useAppStoreWithOut } from "@/store/modules/app";
|
||||
import { useTraceStore } from "@/store/modules/trace";
|
||||
import type { Span } from "@/types/trace";
|
||||
import Graph from "./D3Graph/Index.vue";
|
||||
import Graph from "../D3Graph/Index.vue";
|
||||
import { Themes } from "@/constants/data";
|
||||
import { TraceGraphType } from "./constant";
|
||||
import { getServiceColor } from "@/utils/color";
|
||||
|
||||
/* global defineProps, Recordable*/
|
||||
defineProps({
|
||||
data: { type: Array as PropType<Span[]>, default: () => [] },
|
||||
traceId: { type: String, default: "" },
|
||||
});
|
||||
const { t } = useI18n();
|
||||
/* global defineProps, Indexable*/
|
||||
type Props = {
|
||||
data: Span[];
|
||||
traceId: string;
|
||||
selectedMaxTimestamp?: number;
|
||||
selectedMinTimestamp?: number;
|
||||
minTimestamp: number;
|
||||
maxTimestamp: number;
|
||||
};
|
||||
defineProps<Props>();
|
||||
const appStore = useAppStoreWithOut();
|
||||
const traceStore = useTraceStore();
|
||||
|
||||
function computedScale(i: number) {
|
||||
const sequentialScale = d3
|
||||
.scaleSequential()
|
||||
.domain([0, traceStore.serviceList.length + 1])
|
||||
.interpolator(d3.interpolateCool);
|
||||
return sequentialScale(i);
|
||||
}
|
||||
|
||||
function downloadTrace() {
|
||||
const serializer = new XMLSerializer();
|
||||
const svgNode: any = d3.select(".trace-list").node();
|
||||
const source = `<?xml version="1.0" standalone="no"?>\r\n${serializer.serializeToString(svgNode)}`;
|
||||
const canvas = document.createElement("canvas");
|
||||
const context: any = canvas.getContext("2d");
|
||||
canvas.width = (d3.select(".trace-list") as Recordable)._groups[0][0].clientWidth;
|
||||
canvas.height = (d3.select(".trace-list") as Recordable)._groups[0][0].clientHeight;
|
||||
canvas.width = (d3.select(".trace-list") as Indexable)._groups[0][0].clientWidth;
|
||||
canvas.height = (d3.select(".trace-list") as Indexable)._groups[0][0].clientHeight;
|
||||
context.fillStyle = appStore.theme === Themes.Dark ? "#212224" : `#fff`;
|
||||
context.fillRect(0, 0, canvas.width, canvas.height);
|
||||
const image = new Image();
|
||||
@@ -94,6 +97,7 @@ limitations under the License. -->
|
||||
border: 1px solid;
|
||||
font-size: 11px;
|
||||
border-radius: 4px;
|
||||
margin: 3px;
|
||||
}
|
||||
|
||||
.btn {
|
@@ -17,29 +17,39 @@ limitations under the License. -->
|
||||
<div class="trace-t-loading" v-show="loading">
|
||||
<Icon iconName="spinner" size="sm" />
|
||||
</div>
|
||||
<TableContainer :tableData="tableData" :type="TraceGraphType.STATISTICS" :headerType="headerType">
|
||||
<TableContainer
|
||||
:tableData="tableData"
|
||||
:type="TraceGraphType.STATISTICS"
|
||||
:headerType="headerType"
|
||||
:traceId="traceId"
|
||||
>
|
||||
<div class="trace-tips" v-if="!tableData.length">{{ $t("noData") }}</div>
|
||||
</TableContainer>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch, onMounted } from "vue";
|
||||
import type { PropType } from "vue";
|
||||
import TableContainer from "./Table/TableContainer.vue";
|
||||
import traceTable from "../utils/trace-table";
|
||||
import TableContainer from "../Table/TableContainer.vue";
|
||||
import traceTable from "../D3Graph/utils/trace-table";
|
||||
import type { StatisticsSpan, Span, StatisticsGroupRef } from "@/types/trace";
|
||||
import { TraceGraphType } from "./constant";
|
||||
|
||||
/* global defineProps, defineEmits, Recordable*/
|
||||
const props = defineProps({
|
||||
data: { type: Array as PropType<Span[]>, default: () => [] },
|
||||
traceId: { type: String, default: "" },
|
||||
showBtnDetail: { type: Boolean, default: false },
|
||||
headerType: { type: String, default: "" },
|
||||
});
|
||||
const emit = defineEmits(["load"]);
|
||||
/* global defineProps, defineEmits*/
|
||||
type Props = {
|
||||
data: Span[];
|
||||
traceId: string;
|
||||
showBtnDetail: boolean;
|
||||
headerType: string;
|
||||
selectedMaxTimestamp: number;
|
||||
selectedMinTimestamp: number;
|
||||
};
|
||||
type Emits = {
|
||||
(e: "load", callback: () => void): void;
|
||||
};
|
||||
const props = defineProps<Props>();
|
||||
const emit = defineEmits<Emits>();
|
||||
const loading = ref<boolean>(true);
|
||||
const tableData = ref<Recordable>([]);
|
||||
const tableData = ref<any[]>([]);
|
||||
const list = ref<any[]>([]);
|
||||
|
||||
onMounted(() => {
|
@@ -18,24 +18,34 @@ limitations under the License. -->
|
||||
:type="TraceGraphType.TABLE"
|
||||
:headerType="headerType"
|
||||
@select="getSelectedSpan"
|
||||
:selectedMaxTimestamp="selectedMaxTimestamp"
|
||||
:selectedMinTimestamp="selectedMinTimestamp"
|
||||
:minTimestamp="minTimestamp"
|
||||
:maxTimestamp="maxTimestamp"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import type { PropType } from "vue";
|
||||
import type { Span } from "@/types/trace";
|
||||
import type { SegmentSpan } from "@/types/profile";
|
||||
import Graph from "./D3Graph/Index.vue";
|
||||
import { TraceGraphType } from "./constant";
|
||||
import Graph from "../D3Graph/Index.vue";
|
||||
import { TraceGraphType } from "../VisGraph/constant";
|
||||
|
||||
defineProps({
|
||||
data: { type: Array as PropType<(Span | SegmentSpan)[]>, default: () => [] },
|
||||
traceId: { type: String, default: "" },
|
||||
headerType: { type: String, default: "" },
|
||||
});
|
||||
const emits = defineEmits(["select"]);
|
||||
type Props = {
|
||||
data: SegmentSpan[];
|
||||
traceId: string;
|
||||
selectedMaxTimestamp?: number;
|
||||
selectedMinTimestamp?: number;
|
||||
minTimestamp: number;
|
||||
maxTimestamp: number;
|
||||
headerType?: string;
|
||||
};
|
||||
type Emits = {
|
||||
(e: "select", value: SegmentSpan): void;
|
||||
};
|
||||
defineProps<Props>();
|
||||
const emits = defineEmits<Emits>();
|
||||
|
||||
function getSelectedSpan(span: Span) {
|
||||
function getSelectedSpan(span: SegmentSpan) {
|
||||
emits("select", span);
|
||||
}
|
||||
</script>
|
@@ -17,7 +17,7 @@ limitations under the License. -->
|
||||
class="time-charts-item mr-5"
|
||||
v-for="(i, index) in traceStore.serviceList"
|
||||
:key="index"
|
||||
:style="`color:${computedScale(index)}`"
|
||||
:style="`color:${getServiceColor(i)}`"
|
||||
>
|
||||
<Icon iconName="issue-open-m" class="mr-5" size="sm" />
|
||||
<span>{{ i }}</span>
|
||||
@@ -35,40 +35,44 @@ limitations under the License. -->
|
||||
</a>
|
||||
</div>
|
||||
<div class="trace-tree">
|
||||
<Graph ref="charts" :data="data" :traceId="traceId" :type="TraceGraphType.TREE" />
|
||||
<Graph
|
||||
ref="charts"
|
||||
:data="data"
|
||||
:traceId="traceId"
|
||||
:type="TraceGraphType.TREE"
|
||||
:selectedMaxTimestamp="selectedMaxTimestamp"
|
||||
:selectedMinTimestamp="selectedMinTimestamp"
|
||||
:minTimestamp="minTimestamp"
|
||||
:maxTimestamp="maxTimestamp"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import * as d3 from "d3";
|
||||
import Graph from "./D3Graph/Index.vue";
|
||||
import type { PropType } from "vue";
|
||||
import Graph from "../D3Graph/Index.vue";
|
||||
import type { Span } from "@/types/trace";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { ref } from "vue";
|
||||
import { TraceGraphType } from "./constant";
|
||||
import { useTraceStore } from "@/store/modules/trace";
|
||||
import { getServiceColor } from "@/utils/color";
|
||||
|
||||
/* global defineProps */
|
||||
defineProps({
|
||||
data: { type: Array as PropType<Span[]>, default: () => [] },
|
||||
traceId: { type: String, default: "" },
|
||||
});
|
||||
type Props = {
|
||||
data: Span[];
|
||||
traceId: string;
|
||||
selectedMaxTimestamp?: number;
|
||||
selectedMinTimestamp?: number;
|
||||
minTimestamp: number;
|
||||
maxTimestamp: number;
|
||||
};
|
||||
defineProps<Props>();
|
||||
const { t } = useI18n();
|
||||
const traceStore = useTraceStore();
|
||||
const charts = ref<any>(null);
|
||||
|
||||
function computedScale(i: number) {
|
||||
const sequentialScale = d3
|
||||
.scaleSequential()
|
||||
.domain([0, traceStore.serviceList.length + 1])
|
||||
.interpolator(d3.interpolateCool);
|
||||
return sequentialScale(i);
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.trace-tree {
|
||||
height: 100%;
|
||||
min-height: 400px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
@@ -84,7 +88,7 @@ limitations under the License. -->
|
||||
.trace-tree-charts {
|
||||
overflow: auto;
|
||||
padding: 10px;
|
||||
position: relative;
|
||||
// position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
@@ -95,5 +99,6 @@ limitations under the License. -->
|
||||
border: 1px solid;
|
||||
font-size: 11px;
|
||||
border-radius: 4px;
|
||||
margin: 3px;
|
||||
}
|
||||
</style>
|
@@ -21,3 +21,9 @@ export enum TraceGraphType {
|
||||
TABLE = "Table",
|
||||
STATISTICS = "Statistics",
|
||||
}
|
||||
export const GraphTypeOptions = [
|
||||
{ value: "List", icon: "list-bulleted", label: "list" },
|
||||
{ value: "Tree", icon: "issue-child", label: "tree" },
|
||||
{ value: "Table", icon: "table", label: "table" },
|
||||
{ value: "Statistics", icon: "statistics-bulleted", label: "statistics" },
|
||||
] as const;
|
Reference in New Issue
Block a user