feat: enhance the trace table graph for multiple refs (#464)

This commit is contained in:
Fine0830 2025-04-14 21:12:48 +08:00 committed by GitHub
parent 0775bf0034
commit 687ae07bb0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 207 additions and 248 deletions

View File

@ -34,6 +34,7 @@ interface TraceState {
conditions: Recordable; conditions: Recordable;
traceSpanLogs: Recordable[]; traceSpanLogs: Recordable[];
selectorStore: Recordable; selectorStore: Recordable;
selectedSpan: Recordable<Span>;
} }
export const traceStore = defineStore({ export const traceStore = defineStore({
@ -45,6 +46,7 @@ export const traceStore = defineStore({
traceList: [], traceList: [],
traceSpans: [], traceSpans: [],
currentTrace: {}, currentTrace: {},
selectedSpan: {},
conditions: { conditions: {
queryDuration: useAppStoreWithOut().durationTime, queryDuration: useAppStoreWithOut().durationTime,
traceState: "ALL", traceState: "ALL",
@ -64,6 +66,9 @@ export const traceStore = defineStore({
setTraceSpans(spans: Span[]) { setTraceSpans(spans: Span[]) {
this.traceSpans = spans; this.traceSpans = spans;
}, },
setSelectedSpan(span: Span) {
this.selectedSpan = span;
},
resetState() { resetState() {
this.traceSpans = []; this.traceSpans = [];
this.traceList = []; this.traceList = [];

View File

@ -48,6 +48,7 @@ export interface Span {
logs?: log[]; logs?: log[];
parentSegmentId?: string; parentSegmentId?: string;
refs?: Ref[]; refs?: Ref[];
key?: string;
} }
export type Ref = { export type Ref = {
type?: string; type?: string;

View File

@ -43,8 +43,7 @@ limitations under the License. -->
<Table <Table
:data="profileStore.segmentSpans" :data="profileStore.segmentSpans"
:traceId="profileStore.currentSegment.traceId" :traceId="profileStore.currentSegment.traceId"
:showBtnDetail="true" :headerType="WidgetType.Profile"
headerType="profile"
@select="selectSpan" @select="selectSpan"
/> />
</div> </div>
@ -53,13 +52,14 @@ limitations under the License. -->
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from "vue"; import { ref } from "vue";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import Table from "../../trace/components/Table/Index.vue"; import Table from "../../trace/components/Table.vue";
import { useProfileStore } from "@/store/modules/profile"; import { useProfileStore } from "@/store/modules/profile";
import Selector from "@/components/Selector.vue"; import Selector from "@/components/Selector.vue";
import type { Span } from "@/types/trace"; import type { Span } from "@/types/trace";
import type { Option } from "@/types/app"; import type { Option } from "@/types/app";
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
import { ProfileDataMode, ProfileDisplayMode } from "./data"; import { ProfileDataMode, ProfileDisplayMode } from "./data";
import { WidgetType } from "@/views/dashboard/data";
/* global defineEmits*/ /* global defineEmits*/
const emits = defineEmits(["loading", "displayMode"]); const emits = defineEmits(["loading", "displayMode"]);

View File

@ -84,7 +84,7 @@ limitations under the License. -->
:data="traceStore.traceSpans" :data="traceStore.traceSpans"
:traceId="traceStore.currentTrace.traceIds[0].value" :traceId="traceStore.currentTrace.traceIds[0].value"
:showBtnDetail="false" :showBtnDetail="false"
HeaderType="trace" :headerType="WidgetType.Trace"
/> />
</div> </div>
</div> </div>
@ -160,6 +160,7 @@ limitations under the License. -->
appStore, appStore,
loading, loading,
traceId, traceId,
WidgetType,
}; };
}, },
}); });

View File

@ -14,9 +14,19 @@ limitations under the License. -->
<div class="trace-t-loading" v-show="loading"> <div class="trace-t-loading" v-show="loading">
<Icon iconName="spinner" size="sm" /> <Icon iconName="spinner" size="sm" />
</div> </div>
<div ref="traceGraph" class="d3-graph"></div> <TableContainer
v-if="type === TraceGraphType.TABLE"
:tableData="segmentId"
:type="type"
:headerType="headerType"
:traceId="traceId"
@select="handleSelectSpan"
>
<div class="trace-tips" v-if="!segmentId.length">{{ $t("noData") }}</div>
</TableContainer>
<div v-else ref="traceGraph" class="d3-graph"></div>
<div id="trace-action-box"> <div id="trace-action-box">
<div @click="showDetail = true">Span details</div> <div @click="viewSpanDetails">Span details</div>
<div v-for="span in parentSpans" :key="span.segmentId" @click="viewParentSpan(span)"> <div v-for="span in parentSpans" :key="span.segmentId" @click="viewParentSpan(span)">
{{ `Parent span: ${span.endpointName} -> Start time: ${visDate(span.startTime)}` }} {{ `Parent span: ${span.endpointName} -> Start time: ${visDate(span.startTime)}` }}
</div> </div>
@ -45,15 +55,19 @@ limitations under the License. -->
import TreeGraph from "../../utils/d3-trace-tree"; import TreeGraph from "../../utils/d3-trace-tree";
import type { Span, Ref } from "@/types/trace"; import type { Span, Ref } from "@/types/trace";
import SpanDetail from "./SpanDetail.vue"; import SpanDetail from "./SpanDetail.vue";
import TableContainer from "../Table/TableContainer.vue";
import { useAppStoreWithOut } from "@/store/modules/app"; import { useAppStoreWithOut } from "@/store/modules/app";
import { debounce } from "@/utils/debounce"; import { debounce } from "@/utils/debounce";
import { mutationObserver } from "@/utils/mutation"; import { mutationObserver } from "@/utils/mutation";
import { TraceGraphType } from "../constant";
import { Themes } from "@/constants/data";
/* global defineProps, Nullable, defineExpose, Recordable */ /* global Recordable, Nullable */
const props = defineProps({ const props = defineProps({
data: { type: Array as PropType<Span[]>, default: () => [] }, data: { type: Array as PropType<Span[]>, default: () => [] },
traceId: { type: String, default: "" }, traceId: { type: String, default: "" },
type: { type: String, default: "List" }, type: { type: String, default: TraceGraphType.LIST },
headerType: { type: String, default: "" },
}); });
const appStore = useAppStoreWithOut(); const appStore = useAppStoreWithOut();
const loading = ref<boolean>(false); const loading = ref<boolean>(false);
@ -69,34 +83,29 @@ limitations under the License. -->
const debounceFunc = debounce(draw, 500); const debounceFunc = debounce(draw, 500);
const visDate = (date: number, pattern = "YYYY-MM-DD HH:mm:ss:SSS") => dayjs(date).format(pattern); const visDate = (date: number, pattern = "YYYY-MM-DD HH:mm:ss:SSS") => dayjs(date).format(pattern);
defineExpose({
tree,
});
onMounted(() => { onMounted(() => {
loading.value = true; loading.value = true;
changeTree(); changeTree();
if (!traceGraph.value) {
loading.value = false;
return;
}
draw(); draw();
loading.value = false; loading.value = false;
// monitor segment list width changes. // monitor segment list width changes.
mutationObserver.create("trigger-resize", () => { mutationObserver.create("trigger-resize", () => {
d3.selectAll(".d3-tip").remove(); d3.selectAll(".d3-tip").remove();
debounceFunc(); debounceFunc();
}); });
window.addEventListener("resize", debounceFunc); window.addEventListener("resize", debounceFunc);
}); });
function draw() { function draw() {
if (props.type === TraceGraphType.TABLE) {
segmentId.value = setLevel(segmentId.value);
return;
}
if (!traceGraph.value) { if (!traceGraph.value) {
return; return;
} }
d3.selectAll(".d3-tip").remove(); d3.selectAll(".d3-tip").remove();
if (props.type === "List") { if (props.type === TraceGraphType.LIST) {
tree.value = new ListGraph(traceGraph.value, handleSelectSpan); tree.value = new ListGraph(traceGraph.value, handleSelectSpan);
tree.value.init( tree.value.init(
{ label: "TRACE_ROOT", children: segmentId.value }, { label: "TRACE_ROOT", children: segmentId.value },
@ -104,7 +113,9 @@ limitations under the License. -->
fixSpansSize.value, fixSpansSize.value,
); );
tree.value.draw(); tree.value.draw();
} else { return;
}
if (props.type === TraceGraphType.TREE) {
tree.value = new TreeGraph(traceGraph.value, handleSelectSpan); tree.value = new TreeGraph(traceGraph.value, handleSelectSpan);
tree.value.init( tree.value.init(
{ label: `${props.traceId}`, children: segmentId.value }, { label: `${props.traceId}`, children: segmentId.value },
@ -112,12 +123,16 @@ limitations under the License. -->
); );
} }
} }
function handleSelectSpan(i: Recordable) { function handleSelectSpan(i: any) {
const spans = []; const spans = [];
const refSpans = []; const refSpans = [];
parentSpans.value = []; parentSpans.value = [];
refParentSpans.value = []; refParentSpans.value = [];
currentSpan.value = i.data; if (props.type === TraceGraphType.TABLE) {
currentSpan.value = i;
} else {
currentSpan.value = i.data;
}
if (!currentSpan.value) { if (!currentSpan.value) {
return; return;
} }
@ -145,8 +160,29 @@ limitations under the License. -->
} }
} }
function viewParentSpan(span: Recordable) { function viewParentSpan(span: Recordable) {
if (props.type === TraceGraphType.TABLE) {
setTableSpanStyle(span);
return;
}
tree.value.highlightParents(span); tree.value.highlightParents(span);
} }
function viewSpanDetails() {
showDetail.value = true;
hideActionBox();
}
function setTableSpanStyle(span: Recordable) {
const itemDom: any = document.querySelector(`.trace-item-${span.key}`);
const items: any = document.querySelectorAll(".trace-item");
for (const item of items) {
item.style.background = appStore.theme === Themes.Dark ? "#212224" : "#fff";
}
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) { function traverseTree(node: Recordable, spanId: string, segmentId: string, data: Recordable) {
if (!node || node.isBroken) { if (!node || node.isBroken) {
return; return;
@ -288,6 +324,7 @@ limitations under the License. -->
} }
for (const i of [...fixSpans, ...props.data]) { for (const i of [...fixSpans, ...props.data]) {
i.label = i.endpointName || "no operation name"; i.label = i.endpointName || "no operation name";
i.key = Math.random().toString(36).substring(2, 36);
i.children = []; i.children = [];
if (segmentGroup[i.segmentId]) { if (segmentGroup[i.segmentId]) {
segmentGroup[i.segmentId].push(i); segmentGroup[i.segmentId].push(i);
@ -360,6 +397,17 @@ limitations under the License. -->
} }
} }
} }
function setLevel(arr: Recordable[], level = 1, totalExec?: number) {
for (const item of arr) {
item.level = level;
totalExec = totalExec || item.endTime - item.startTime;
item.totalExec = totalExec;
if (item.children && item.children.length > 0) {
setLevel(item.children, level + 1, totalExec);
}
}
return arr;
}
function getRefsAllNodes(tree: Recordable) { function getRefsAllNodes(tree: Recordable) {
let nodes = []; let nodes = [];
let stack = [tree]; let stack = [tree];

View File

@ -22,7 +22,7 @@ limitations under the License. -->
</el-button> </el-button>
</div> </div>
<div class="list"> <div class="list">
<Graph :data="data" :traceId="traceId" type="List" /> <Graph :data="data" :traceId="traceId" :type="TraceGraphType.LIST" />
</div> </div>
</div> </div>
</template> </template>
@ -35,6 +35,7 @@ limitations under the License. -->
import type { Span } from "@/types/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 { Themes } from "@/constants/data";
import { TraceGraphType } from "./constant";
/* global defineProps, Recordable*/ /* global defineProps, Recordable*/
const props = defineProps({ const props = defineProps({

View File

@ -17,7 +17,7 @@ limitations under the License. -->
<div class="trace-t-loading" v-show="loading"> <div class="trace-t-loading" v-show="loading">
<Icon iconName="spinner" size="sm" /> <Icon iconName="spinner" size="sm" />
</div> </div>
<TableContainer :tableData="tableData" type="statistics" :HeaderType="HeaderType"> <TableContainer :tableData="tableData" :type="TraceGraphType.STATISTICS" :headerType="headerType">
<div class="trace-tips" v-if="!tableData.length">{{ $t("noData") }}</div> <div class="trace-tips" v-if="!tableData.length">{{ $t("noData") }}</div>
</TableContainer> </TableContainer>
</div> </div>
@ -28,13 +28,14 @@ limitations under the License. -->
import TableContainer from "./Table/TableContainer.vue"; import TableContainer from "./Table/TableContainer.vue";
import traceTable from "../utils/trace-table"; import traceTable from "../utils/trace-table";
import type { StatisticsSpan, Span, StatisticsGroupRef } from "@/types/trace"; import type { StatisticsSpan, Span, StatisticsGroupRef } from "@/types/trace";
import { TraceGraphType } from "./constant";
/* global defineProps, defineEmits, Recordable*/ /* global defineProps, defineEmits, Recordable*/
const props = defineProps({ const props = defineProps({
data: { type: Array as PropType<Span[]>, default: () => [] }, data: { type: Array as PropType<Span[]>, default: () => [] },
traceId: { type: String, default: "" }, traceId: { type: String, default: "" },
showBtnDetail: { type: Boolean, default: false }, showBtnDetail: { type: Boolean, default: false },
HeaderType: { type: String, default: "" }, headerType: { type: String, default: "" },
}); });
const emit = defineEmits(["load"]); const emit = defineEmits(["load"]);
const loading = ref<boolean>(true); const loading = ref<boolean>(true);

View File

@ -0,0 +1,38 @@
<!-- 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-table-charts">
<Graph :data="data" :traceId="traceId" :type="TraceGraphType.TABLE" :headerType="headerType" />
</div>
</template>
<script lang="ts" setup>
import type { PropType } from "vue";
import type { Span } from "@/types/trace";
import Graph from "./D3Graph/Index.vue";
import { TraceGraphType } from "./constant";
defineProps({
data: { type: Array as PropType<Span[]>, default: () => [] },
traceId: { type: String, default: "" },
headerType: { type: String, default: "" },
});
</script>
<style lang="scss" scoped>
.trace-table-charts {
overflow: auto;
padding: 10px;
height: 100%;
width: 100%;
position: relative;
}
</style>

View File

@ -1,102 +0,0 @@
<!-- 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-table">
<div class="trace-t-loading" v-show="loading">
<Icon iconName="spinner" size="sm" />
</div>
<TableContainer
:tableData="tableData"
type="table"
:headerType="headerType"
:traceId="traceId"
@select="handleSelectSpan"
>
<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 "./TableContainer.vue";
import traceTable from "../../utils/trace-table";
import type { Span } from "@/types/trace";
/* 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(["select", "view", "load"]);
const loading = ref<boolean>(true);
const tableData = ref<Recordable[]>([]);
const showDetail = ref<boolean>(false);
const currentSpan = ref<Span | Recordable>({});
onMounted(() => {
tableData.value = formatData(traceTable.changeTree(props.data, props.traceId));
loading.value = false;
emit("load", () => {
loading.value = true;
});
});
function formatData(arr: Recordable[], level = 1, totalExec?: number) {
for (const item of arr) {
item.level = level;
totalExec = totalExec || item.endTime - item.startTime;
item.totalExec = totalExec;
if (item.children && item.children.length > 0) {
formatData(item.children, level + 1, totalExec);
}
}
return arr;
}
function handleSelectSpan(data: Span) {
currentSpan.value = data;
if (!props.showBtnDetail) {
showDetail.value = true;
}
emit("select", data);
}
watch(
() => props.data,
() => {
if (!props.data.length) {
tableData.value = [];
return;
}
tableData.value = formatData(traceTable.changeTree(props.data, props.traceId));
loading.value = false;
},
);
</script>
<style lang="scss" scoped>
.trace-tips {
width: 100%;
text-align: center;
margin-top: 10px;
}
.trace-table {
height: 100%;
width: 100%;
}
</style>

View File

@ -14,8 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License. --> limitations under the License. -->
<template> <template>
<div class="trace"> <div class="trace-table">
<div class="trace-header" v-if="type === 'statistics'"> <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" :key="index">
{{ item.value }} {{ item.value }}
<span <span
@ -28,7 +28,7 @@ limitations under the License. -->
</span> </span>
</div> </div>
</div> </div>
<div class="trace-header" v-else> <div class="trace-table-header" v-else>
<div class="method" :style="`width: ${method}px`"> <div class="method" :style="`width: ${method}px`">
<span class="cp dragger" ref="dragger"> <span class="cp dragger" ref="dragger">
<Icon iconName="settings_ethernet" size="sm" /> <Icon iconName="settings_ethernet" size="sm" />
@ -44,10 +44,10 @@ limitations under the License. -->
:traceId="traceId" :traceId="traceId"
v-for="(item, index) in tableData" v-for="(item, index) in tableData"
:data="item" :data="item"
:key="'key' + index" :key="`key${index}`"
:type="type" :type="type"
:headerType="headerType" :headerType="headerType"
@select="selectItem" @click="selectItem"
/> />
<slot></slot> <slot></slot>
</div> </div>
@ -55,9 +55,11 @@ limitations under the License. -->
<script lang="ts" setup> <script lang="ts" setup>
import { ref, onMounted } from "vue"; import { ref, onMounted } from "vue";
import type { PropType } from "vue"; import type { PropType } from "vue";
import type { Span } from "@/types/trace"; import { useTraceStore } from "@/store/modules/trace";
import TableItem from "./TableItem.vue"; import TableItem from "./TableItem.vue";
import { ProfileConstant, TraceConstant, StatisticsConstant } from "./data"; import { ProfileConstant, TraceConstant, StatisticsConstant } from "./data";
import { TraceGraphType } from "../constant";
import { WidgetType } from "@/views/dashboard/data";
/* global defineProps, Nullable, defineEmits, Recordable*/ /* global defineProps, Nullable, defineEmits, Recordable*/
const props = defineProps({ const props = defineProps({
@ -67,20 +69,21 @@ limitations under the License. -->
traceId: { type: String, default: "" }, traceId: { type: String, default: "" },
}); });
const emits = defineEmits(["select"]); const emits = defineEmits(["select"]);
const traceStore = useTraceStore();
const method = ref<number>(300); const method = ref<number>(300);
const componentKey = ref<number>(300); const componentKey = ref<number>(300);
const flag = ref<boolean>(true); const flag = ref<boolean>(true);
const dragger = ref<Nullable<HTMLSpanElement>>(null); const dragger = ref<Nullable<HTMLSpanElement>>(null);
let headerData: Recordable[] = TraceConstant; let headerData: Recordable[] = TraceConstant;
if (props.headerType === "profile") { if (props.headerType === WidgetType.Profile) {
headerData = ProfileConstant; headerData = ProfileConstant;
} }
if (props.type === "statistics") { if (props.type === TraceGraphType.STATISTICS) {
headerData = StatisticsConstant; headerData = StatisticsConstant;
} }
onMounted(() => { onMounted(() => {
if (props.type === "statistics") { if (props.type === TraceGraphType.STATISTICS) {
return; return;
} }
const drag: Nullable<HTMLSpanElement> = dragger.value; const drag: Nullable<HTMLSpanElement> = dragger.value;
@ -101,8 +104,24 @@ limitations under the License. -->
}; };
}; };
}); });
function selectItem(span: Span) { function selectItem(event: MouseEvent) {
emits("select", span); 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 sortStatistics(key: string) { function sortStatistics(key: string) {
const element = props.tableData; const element = props.tableData;
@ -152,7 +171,7 @@ limitations under the License. -->
<style lang="scss" scoped> <style lang="scss" scoped>
@import url("./table.scss"); @import url("./table.scss");
.trace { .trace-table {
font-size: $font-size-smaller; font-size: $font-size-smaller;
height: 100%; height: 100%;
overflow: auto; overflow: auto;
@ -163,7 +182,7 @@ limitations under the License. -->
float: right; float: right;
} }
.trace-header { .trace-table-header {
white-space: nowrap; white-space: nowrap;
user-select: none; user-select: none;
border-left: 0; border-left: 0;
@ -171,7 +190,7 @@ limitations under the License. -->
border-bottom: 1px solid var(--sw-trace-list-border); border-bottom: 1px solid var(--sw-trace-list-border);
} }
.trace-header div { .trace-table-header div {
display: inline-block; display: inline-block;
background-color: var(--sw-table-header); background-color: var(--sw-table-header);
padding: 0 4px; padding: 0 4px;

View File

@ -14,17 +14,17 @@ See the License for the specific language governing permissions and
limitations under the License. --> limitations under the License. -->
<template> <template>
<div v-if="type === 'statistics'"> <div v-if="type === TraceGraphType.STATISTICS">
<div class="trace-item"> <div class="trace-item">
<div :class="['method']"> <div :class="['method']">
<el-tooltip :content="data.groupRef.endpointName" placement="bottom" :show-after="300"> <el-tooltip :content="data.groupRef.endpointName" placement="top" :show-after="300">
<span> <span>
{{ data.groupRef.endpointName }} {{ data.groupRef.endpointName }}
</span> </span>
</el-tooltip> </el-tooltip>
</div> </div>
<div :class="['type']"> <div :class="['type']">
<el-tooltip :content="data.groupRef.type" placement="bottom" :show-after="300"> <el-tooltip :content="data.groupRef.type" placement="top" :show-after="300">
<span> <span>
{{ data.groupRef.type }} {{ data.groupRef.type }}
</span> </span>
@ -55,6 +55,7 @@ limitations under the License. -->
'level' + (data.level - 1), 'level' + (data.level - 1),
{ 'trace-item-error': data.isError }, { 'trace-item-error': data.isError },
{ profiled: data.profiled === false }, { profiled: data.profiled === false },
`trace-item-${data.key}`,
]" ]"
:data-text="data.profiled === false ? 'No Thread Dump' : ''" :data-text="data.profiled === false ? 'No Thread Dump' : ''"
> >
@ -75,7 +76,7 @@ limitations under the License. -->
/> />
<el-tooltip <el-tooltip
:content="data.type === 'Entry' ? 'Entry' : 'Exit'" :content="data.type === 'Entry' ? 'Entry' : 'Exit'"
placement="bottom" placement="top"
:show-after="300" :show-after="300"
v-if="['Entry', 'Exit'].includes(data.type)" v-if="['Entry', 'Exit'].includes(data.type)"
> >
@ -83,12 +84,12 @@ limitations under the License. -->
<Icon :iconName="data.type === 'Entry' ? 'entry' : 'exit'" size="sm" class="mr-5" /> <Icon :iconName="data.type === 'Entry' ? 'entry' : 'exit'" size="sm" class="mr-5" />
</span> </span>
</el-tooltip> </el-tooltip>
<el-tooltip v-if="isCrossThread" content="CROSS_THREAD" placement="bottom" :show-after="300"> <el-tooltip v-if="isCrossThread" content="CROSS_THREAD" placement="top" :show-after="300">
<span> <span>
<Icon iconName="cross" size="sm" class="mr-5" /> <Icon iconName="cross" size="sm" class="mr-5" />
</span> </span>
</el-tooltip> </el-tooltip>
<el-tooltip :content="data.endpointName" placement="bottom" :show-after="300"> <el-tooltip :content="data.endpointName" placement="top" :show-after="300">
<span> <span>
{{ data.endpointName }} {{ data.endpointName }}
</span> </span>
@ -109,19 +110,19 @@ limitations under the License. -->
{{ data.dur ? data.dur + "" : "0" }} {{ data.dur ? data.dur + "" : "0" }}
</div> </div>
<div class="api"> <div class="api">
<el-tooltip :show-after="300" :content="data.component || '-'" placement="bottom"> <el-tooltip :show-after="300" :content="data.component || '-'" placement="top">
<span>{{ data.component || "-" }}</span> <span>{{ data.component || "-" }}</span>
</el-tooltip> </el-tooltip>
</div> </div>
<div class="application"> <div class="application">
<el-tooltip :show-after="300" :content="data.serviceCode || '-'" placement="bottom"> <el-tooltip :show-after="300" :content="data.serviceCode || '-'" placement="top">
<span>{{ data.serviceCode }}</span> <span>{{ data.serviceCode }}</span>
</el-tooltip> </el-tooltip>
</div> </div>
<div class="application" v-show="headerType === 'profile'" @click="viewSpan($event)"> <div class="application" v-show="headerType === WidgetType.Profile" @click="viewSpan($event)">
<span>{{ t("view") }}</span> <span>{{ t("view") }}</span>
</div> </div>
<div class="application" v-show="headerType !== 'profile'"> <div class="application" v-show="headerType !== WidgetType.Profile">
<span>{{ data.attachedEvents && data.attachedEvents.length }}</span> <span>{{ data.attachedEvents && data.attachedEvents.length }}</span>
</div> </div>
</div> </div>
@ -133,7 +134,6 @@ limitations under the License. -->
:data="child" :data="child"
:type="type" :type="type"
:headerType="headerType" :headerType="headerType"
@select="selectedItem(child)"
/> />
</div> </div>
<el-dialog v-model="showDetail" :destroy-on-close="true" fullscreen @closed="showDetail = false"> <el-dialog v-model="showDetail" :destroy-on-close="true" fullscreen @closed="showDetail = false">
@ -148,7 +148,10 @@ limitations under the License. -->
import SpanDetail from "../D3Graph/SpanDetail.vue"; import SpanDetail from "../D3Graph/SpanDetail.vue";
import { dateFormat } from "@/utils/dateFormat"; import { dateFormat } from "@/utils/dateFormat";
import { useAppStoreWithOut } from "@/store/modules/app"; import { useAppStoreWithOut } from "@/store/modules/app";
import { useTraceStore } from "@/store/modules/trace";
import { Themes } from "@/constants/data"; import { Themes } from "@/constants/data";
import { TraceGraphType } from "../constant";
import { WidgetType } from "@/views/dashboard/data";
/*global Recordable*/ /*global Recordable*/
const props = { const props = {
@ -161,10 +164,10 @@ limitations under the License. -->
export default defineComponent({ export default defineComponent({
name: "TableItem", name: "TableItem",
props, props,
emits: ["select"],
components: { SpanDetail }, components: { SpanDetail },
setup(props, { emit }) { setup(props) {
const appStore = useAppStoreWithOut(); const appStore = useAppStoreWithOut();
const traceStore = useTraceStore();
const displayChildren = ref<boolean>(true); const displayChildren = ref<boolean>(true);
const showDetail = ref<boolean>(false); const showDetail = ref<boolean>(false);
const { t } = useI18n(); const { t } = useI18n();
@ -193,7 +196,6 @@ limitations under the License. -->
const key = props.data.refs.findIndex((d: { type: string }) => d.type === "CROSS_THREAD"); const key = props.data.refs.findIndex((d: { type: string }) => d.type === "CROSS_THREAD");
return key > -1 ? true : false; return key > -1 ? true : false;
}); });
function toggle() { function toggle() {
displayChildren.value = !displayChildren.value; displayChildren.value = !displayChildren.value;
} }
@ -213,26 +215,27 @@ limitations under the License. -->
} }
function selectSpan(event: Recordable) { function selectSpan(event: Recordable) {
const dom = event.composedPath().find((d: Recordable) => d.className.includes("trace-item")); const dom = event.composedPath().find((d: Recordable) => d.className.includes("trace-item"));
selectedItem(props.data);
emit("select", props.data); if (props.headerType === WidgetType.Profile) {
if (props.headerType === "profile") {
showSelectSpan(dom); showSelectSpan(dom);
return; return;
} }
viewSpanDetail(dom); viewSpanDetail(dom);
} }
function viewSpan(event: Recordable) { function viewSpan(event: Recordable) {
showDetail.value = true;
const dom = event.composedPath().find((d: Recordable) => d.className.includes("trace-item")); const dom = event.composedPath().find((d: Recordable) => d.className.includes("trace-item"));
emit("select", props.data); selectedItem(props.data);
viewSpanDetail(dom); viewSpanDetail(dom);
} }
function selectedItem(span: Recordable) {
function selectedItem(data: HTMLSpanElement) { traceStore.setSelectedSpan(span);
emit("select", data);
} }
function viewSpanDetail(dom: HTMLSpanElement) { function viewSpanDetail(dom: HTMLSpanElement) {
showSelectSpan(dom); showSelectSpan(dom);
showDetail.value = true; if (props.type === TraceGraphType.STATISTICS) {
showDetail.value = true;
}
} }
watch( watch(
() => appStore.theme, () => appStore.theme,
@ -262,6 +265,8 @@ limitations under the License. -->
viewSpan, viewSpan,
t, t,
appStore, appStore,
TraceGraphType,
WidgetType,
}; };
}, },
}); });
@ -279,8 +284,6 @@ limitations under the License. -->
} }
.trace-item.level0 { .trace-item.level0 {
color: #448dfe;
&:hover { &:hover {
background: rgb(0 0 0 / 4%); background: rgb(0 0 0 / 4%);
} }

View File

@ -35,7 +35,7 @@ limitations under the License. -->
</a> </a>
</div> </div>
<div class="trace-tree"> <div class="trace-tree">
<Graph ref="charts" :data="data" :traceId="traceId" type="Tree" /> <Graph ref="charts" :data="data" :traceId="traceId" :type="TraceGraphType.TREE" />
</div> </div>
</div> </div>
</template> </template>
@ -46,6 +46,7 @@ limitations under the License. -->
import type { Span } from "@/types/trace"; import type { Span } from "@/types/trace";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { ref, computed } from "vue"; import { ref, computed } from "vue";
import { TraceGraphType } from "./constant";
/* global defineProps */ /* global defineProps */
const props = defineProps({ const props = defineProps({

View File

@ -0,0 +1,23 @@
/**
* 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.
*/
export enum TraceGraphType {
TREE = "Tree",
LIST = "List",
TABLE = "Table",
STATISTICS = "Statistics",
}

View File

@ -17,7 +17,7 @@
import List from "./List.vue"; import List from "./List.vue";
import Tree from "./Tree.vue"; import Tree from "./Tree.vue";
import Table from "./Table/Index.vue"; import Table from "./Table.vue";
import Statistics from "./Statistics.vue"; import Statistics from "./Statistics.vue";
export default { export default {

View File

@ -265,7 +265,7 @@ export default class ListGraph {
.attr("cx", (d: Recordable) => { .attr("cx", (d: Recordable) => {
const events = d.data.attachedEvents; const events = d.data.attachedEvents;
if (events && events.length > 9) { if (events && events.length > 9) {
return 272; return 273;
} else { } else {
return 270; return 270;
} }
@ -350,16 +350,6 @@ export default class ListGraph {
if (d.data.children.length === 0) return; if (d.data.children.length === 0) return;
this.click(d, this); this.click(d, this);
}); });
nodeUpdate
.transition()
.duration(400)
.attr("transform", (d: Recordable) => `translate(${d.y + 3},${d.x})`)
.style("opacity", 1)
.select("circle")
.style("fill", (d: Recordable) =>
d._children ? `${this.sequentialScale(this.list.indexOf(d.data.serviceCode))}` : "#eee",
);
// Transition exiting nodes to the parent's new position. // Transition exiting nodes to the parent's new position.
node node
.exit() .exit()

View File

@ -15,42 +15,13 @@
* limitations under the License. * limitations under the License.
*/ */
import type { Ref, Span, StatisticsSpan, StatisticsGroupRef, TraceTreeRef } from "@/types/trace"; import type { Span, TraceTreeRef } from "@/types/trace";
export default class TraceUtil { export default class TraceUtil {
public static buildTraceDataList(data: Span[]): string[] { public static buildTraceDataList(data: Span[]): string[] {
return Array.from(new Set(data.map((span: Span) => span.serviceCode))); return Array.from(new Set(data.map((span: Span) => span.serviceCode)));
} }
public static changeTree(data: Span[], currentTraceId: string) {
const segmentIdList: Span[] = [];
const traceTreeRef: Recordable = this.changeTreeCore(data);
traceTreeRef.segmentIdGroup.forEach((segmentId: string) => {
if (traceTreeRef.segmentMap.get(segmentId).refs) {
traceTreeRef.segmentMap.get(segmentId).refs.forEach((ref: Ref) => {
if (ref.traceId === currentTraceId) {
this.traverseTree(
traceTreeRef.segmentMap.get(ref.parentSegmentId) as Span,
ref.parentSpanId,
ref.parentSegmentId,
traceTreeRef.segmentMap.get(segmentId) as Span,
);
}
});
}
});
// set a breakpoint at this line
traceTreeRef.segmentMap.forEach((value: Span) => {
if ((value.refs && value.refs.length === 0) || !value.refs) {
segmentIdList.push(value as Span);
}
});
segmentIdList.forEach((segmentId: Span) => {
this.collapse(segmentId);
});
return segmentIdList;
}
public static changeStatisticsTree(data: Span[]): Map<string, Span[]> { public static changeStatisticsTree(data: Span[]): Map<string, Span[]> {
const result = new Map<string, Span[]>(); const result = new Map<string, Span[]>();
const traceTreeRef = this.changeTreeCore(data); const traceTreeRef = this.changeTreeCore(data);
@ -255,47 +226,6 @@ export default class TraceUtil {
} }
} }
private static traverseTree(node: Span, spanId: number, segmentId: string, childNode: Span) {
if (!node || node.isBroken) {
return;
}
if (node.spanId === spanId && node.segmentId === segmentId) {
node.children!.push(childNode);
return;
}
if (node.children && node.children.length > 0) {
for (const grandchild of node.children) {
this.traverseTree(grandchild, spanId, segmentId, childNode);
}
}
}
private static getSpanGroupData(groupspans: Span[], groupRef: StatisticsGroupRef): StatisticsSpan {
let maxTime = 0;
let minTime = 0;
let sumTime = 0;
const count = groupspans.length;
groupspans.forEach((groupspan: Span) => {
const duration = groupspan.dur || 0;
if (duration > maxTime) {
maxTime = duration;
}
if (duration < minTime) {
minTime = duration;
}
sumTime = sumTime + duration;
});
const avgTime = count === 0 ? 0 : sumTime / count;
return {
groupRef,
maxTime,
minTime,
sumTime,
avgTime,
count,
};
}
private static calculationChildren(nodes: Span[], result: Map<string, Span[]>): void { private static calculationChildren(nodes: Span[], result: Map<string, Span[]>): void {
nodes.forEach((node: Span) => { nodes.forEach((node: Span) => {
const groupRef = node.endpointName + ":" + node.type; const groupRef = node.endpointName + ":" + node.type;