mirror of
https://github.com/apache/skywalking-booster-ui.git
synced 2025-07-18 14:45:25 +00:00
feat: add trace tree
This commit is contained in:
parent
437ed482be
commit
bbb57160fe
@ -14,12 +14,22 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License. -->
|
limitations under the License. -->
|
||||||
<template>
|
<template>
|
||||||
<div class="trace-wrapper flex-v">
|
<div class="trace-wrapper flex-v">
|
||||||
|
<el-popover placement="bottom" trigger="click" :width="100">
|
||||||
|
<template #reference>
|
||||||
|
<span class="delete cp">
|
||||||
|
<Icon iconName="ellipsis_v" size="middle" class="operation" />
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<div class="tools">
|
||||||
|
<span @click="removeWidget">{{ t("delete") }}</span>
|
||||||
|
</div>
|
||||||
|
</el-popover>
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<Filter />
|
<Filter />
|
||||||
</div>
|
</div>
|
||||||
<div class="trace flex-h">
|
<div class="trace flex-h">
|
||||||
<TraceList />
|
<TraceList />
|
||||||
<Detail />
|
<TraceDetail />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -27,22 +37,36 @@ limitations under the License. -->
|
|||||||
import type { PropType } from "vue";
|
import type { PropType } from "vue";
|
||||||
import Filter from "../related/trace/Filter.vue";
|
import Filter from "../related/trace/Filter.vue";
|
||||||
import TraceList from "../related/trace/TraceList.vue";
|
import TraceList from "../related/trace/TraceList.vue";
|
||||||
import Detail from "../related/trace/Detail.vue";
|
import TraceDetail from "../related/trace/Detail.vue";
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
|
import { useDashboardStore } from "@/store/modules/dashboard";
|
||||||
|
|
||||||
/*global defineProps */
|
/*global defineProps */
|
||||||
defineProps({
|
const props = defineProps({
|
||||||
data: {
|
data: {
|
||||||
type: Object as PropType<any>,
|
type: Object as PropType<any>,
|
||||||
default: () => ({ graph: {} }),
|
default: () => ({ graph: {} }),
|
||||||
},
|
},
|
||||||
activeIndex: { type: String, default: "" },
|
activeIndex: { type: String, default: "" },
|
||||||
});
|
});
|
||||||
|
const { t } = useI18n();
|
||||||
|
const dashboardStore = useDashboardStore();
|
||||||
|
function removeWidget() {
|
||||||
|
dashboardStore.removeControls(props.data);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.trace-wrapper {
|
.trace-wrapper {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete {
|
||||||
|
position: absolute;
|
||||||
|
top: 5px;
|
||||||
|
right: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
@ -51,6 +75,19 @@ defineProps({
|
|||||||
border-bottom: 1px solid #dcdfe6;
|
border-bottom: 1px solid #dcdfe6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tools {
|
||||||
|
padding: 5px 0;
|
||||||
|
color: #999;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #409eff;
|
||||||
|
background-color: #eee;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.trace {
|
.trace {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
@ -62,32 +62,32 @@ limitations under the License. -->
|
|||||||
<div>
|
<div>
|
||||||
<el-button
|
<el-button
|
||||||
class="grey"
|
class="grey"
|
||||||
:class="{ ghost: displayMode !== 'list' }"
|
:class="{ ghost: displayMode !== 'List' }"
|
||||||
@click="displayMode = 'list'"
|
@click="displayMode = 'List'"
|
||||||
>
|
>
|
||||||
<Icon class="mr-5" size="sm" iconName="list-bulleted" />
|
<Icon class="mr-5" size="sm" iconName="list-bulleted" />
|
||||||
{{ t("list") }}
|
{{ t("list") }}
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
class="grey"
|
class="grey"
|
||||||
:class="{ ghost: displayMode !== 'tree' }"
|
:class="{ ghost: displayMode !== 'Tree' }"
|
||||||
@click="displayMode = 'tree'"
|
@click="displayMode = 'Tree'"
|
||||||
>
|
>
|
||||||
<Icon class="mr-5" size="sm" iconName="issue-child" />
|
<Icon class="mr-5" size="sm" iconName="issue-child" />
|
||||||
{{ t("tree") }}
|
{{ t("tree") }}
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
class="grey"
|
class="grey"
|
||||||
:class="{ ghost: displayMode !== 'table' }"
|
:class="{ ghost: displayMode !== 'Table' }"
|
||||||
@click="displayMode = 'table'"
|
@click="displayMode = 'Table'"
|
||||||
>
|
>
|
||||||
<Icon class="mr-5" size="sm" iconName="table" />
|
<Icon class="mr-5" size="sm" iconName="table" />
|
||||||
{{ t("table") }}
|
{{ t("table") }}
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
class="grey"
|
class="grey"
|
||||||
:class="{ ghost: displayMode !== 'statistics' }"
|
:class="{ ghost: displayMode !== 'Statistics' }"
|
||||||
@click="displayMode = 'statistics'"
|
@click="displayMode = 'Statistics'"
|
||||||
>
|
>
|
||||||
<Icon class="mr-5" size="sm" iconName="statistics-bulleted" />
|
<Icon class="mr-5" size="sm" iconName="statistics-bulleted" />
|
||||||
{{ t("statistics") }}
|
{{ t("statistics") }}
|
||||||
@ -95,54 +95,73 @@ limitations under the License. -->
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<List
|
<component
|
||||||
v-if="displayMode == 'list' && traceStore.currentTrace.endpointNames"
|
v-if="traceStore.currentTrace.endpointNames"
|
||||||
|
:is="displayMode"
|
||||||
:data="traceStore.traceSpans"
|
:data="traceStore.traceSpans"
|
||||||
:traceId="traceStore.currentTrace.traceIds[0].value"
|
:traceId="traceStore.currentTrace.traceIds[0].value"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts" setup>
|
<script lang="ts">
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { ref } from "vue";
|
import { ref, defineComponent } from "vue";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import { useTraceStore } from "@/store/modules/trace";
|
import { useTraceStore } from "@/store/modules/trace";
|
||||||
import { Option } from "@/types/app";
|
import { Option } from "@/types/app";
|
||||||
import copy from "@/utils/copy";
|
import copy from "@/utils/copy";
|
||||||
import List from "./components/List.vue";
|
import List from "./components/List.vue";
|
||||||
|
import graphs from "./components/index";
|
||||||
|
|
||||||
const { t } = useI18n();
|
export default defineComponent({
|
||||||
const traceStore = useTraceStore();
|
name: "TraceDetail",
|
||||||
const traceId = ref<string>("");
|
components: {
|
||||||
const displayMode = ref<string>("list");
|
...graphs,
|
||||||
const dateFormat = (date: number, pattern = "YYYY-MM-DD HH:mm:ss") =>
|
List,
|
||||||
dayjs(date).format(pattern);
|
},
|
||||||
function handleClick(ids: string[]) {
|
setup() {
|
||||||
let copyValue = null;
|
const { t } = useI18n();
|
||||||
if (ids.length === 1) {
|
const traceStore = useTraceStore();
|
||||||
copyValue = ids[0];
|
const traceId = ref<string>("");
|
||||||
} else {
|
const displayMode = ref<string>("List");
|
||||||
copyValue = ids.join(",");
|
const dateFormat = (date: number, pattern = "YYYY-MM-DD HH:mm:ss") =>
|
||||||
}
|
dayjs(date).format(pattern);
|
||||||
copy(copyValue);
|
function handleClick(ids: string[]) {
|
||||||
}
|
let copyValue = null;
|
||||||
|
if (ids.length === 1) {
|
||||||
|
copyValue = ids[0];
|
||||||
|
} else {
|
||||||
|
copyValue = ids.join(",");
|
||||||
|
}
|
||||||
|
copy(copyValue);
|
||||||
|
}
|
||||||
|
|
||||||
function changeTraceId(opt: Option[]) {
|
function changeTraceId(opt: Option[]) {
|
||||||
traceId.value = opt[0].value;
|
traceId.value = opt[0].value;
|
||||||
traceStore.getTraceSpans({ traceId: opt[0].value });
|
traceStore.getTraceSpans({ traceId: opt[0].value });
|
||||||
}
|
}
|
||||||
|
|
||||||
// function searchTraceLogs() {
|
// function searchTraceLogs() {
|
||||||
// this.showTraceLogs = true;
|
// this.showTraceLogs = true;
|
||||||
// this.GET_TRACE_SPAN_LOGS({
|
// this.GET_TRACE_SPAN_LOGS({
|
||||||
// condition: {
|
// condition: {
|
||||||
// relatedTrace: {
|
// relatedTrace: {
|
||||||
// traceId: this.traceId || traceStore.current.traceIds[0],
|
// traceId: this.traceId || traceStore.current.traceIds[0],
|
||||||
// },
|
// },
|
||||||
// paging: { pageNum: this.pageNum, pageSize: this.pageSize, needTotal: true },
|
// paging: { pageNum: this.pageNum, pageSize: this.pageSize, needTotal: true },
|
||||||
// },
|
// },
|
||||||
// });
|
// });
|
||||||
// }
|
// }
|
||||||
|
return {
|
||||||
|
traceStore,
|
||||||
|
displayMode,
|
||||||
|
dateFormat,
|
||||||
|
changeTraceId,
|
||||||
|
handleClick,
|
||||||
|
t,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.trace-detail {
|
.trace-detail {
|
||||||
|
332
src/views/dashboard/related/trace/components/Graph.vue
Normal file
332
src/views/dashboard/related/trace/components/Graph.vue
Normal file
@ -0,0 +1,332 @@
|
|||||||
|
<!-- 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-t-loading" v-show="loading">
|
||||||
|
<Icon iconName="spinner" size="sm" />
|
||||||
|
</div>
|
||||||
|
<div ref="traceGraph"></div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, watch, onBeforeUnmount, onMounted } from "vue";
|
||||||
|
import type { PropType } from "vue";
|
||||||
|
import _ from "lodash";
|
||||||
|
import * as d3 from "d3";
|
||||||
|
import ListGraph from "../utils/d3-trace-list";
|
||||||
|
import TreeGraph from "../utils/d3-trace-tree";
|
||||||
|
import { Span } from "@/types/trace";
|
||||||
|
|
||||||
|
/* global defineProps, Nullable*/
|
||||||
|
const props = defineProps({
|
||||||
|
data: { type: Array as PropType<Span[]>, default: () => [] },
|
||||||
|
traceId: { type: String, default: "" },
|
||||||
|
type: { type: String, default: "List" },
|
||||||
|
});
|
||||||
|
const loading = ref<boolean>(false);
|
||||||
|
const showDetail = ref<boolean>(false);
|
||||||
|
const fixSpansSize = ref<number>(0);
|
||||||
|
const segmentId = ref<any>([]);
|
||||||
|
const currentSpan = ref<Array<Span>>([]);
|
||||||
|
const tree = ref<any>(null);
|
||||||
|
const traceGraph = ref<Nullable<HTMLDivElement>>(null);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loading.value = true;
|
||||||
|
changeTree();
|
||||||
|
if (!traceGraph.value) {
|
||||||
|
loading.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (props.type === "List") {
|
||||||
|
tree.value = new ListGraph(traceGraph.value, handleSelectSpan);
|
||||||
|
tree.value.init(
|
||||||
|
{ label: "TRACE_ROOT", children: segmentId.value },
|
||||||
|
props.data,
|
||||||
|
fixSpansSize.value
|
||||||
|
);
|
||||||
|
tree.value.draw();
|
||||||
|
} else {
|
||||||
|
tree.value = new TreeGraph(traceGraph.value, handleSelectSpan);
|
||||||
|
tree.value.init(
|
||||||
|
{ label: `${props.traceId}`, children: segmentId.value },
|
||||||
|
props.data
|
||||||
|
);
|
||||||
|
}
|
||||||
|
loading.value = false;
|
||||||
|
});
|
||||||
|
function handleSelectSpan(i: any) {
|
||||||
|
currentSpan.value = i.data;
|
||||||
|
showDetail.value = true;
|
||||||
|
}
|
||||||
|
function traverseTree(node: any, spanId: string, segmentId: string, data: any) {
|
||||||
|
if (!node || node.isBroken) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (node.spanId === spanId && node.segmentId === segmentId) {
|
||||||
|
node.children.push(data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (node.children && node.children.length > 0) {
|
||||||
|
node.children.forEach((nodeItem: any) => {
|
||||||
|
traverseTree(nodeItem, spanId, segmentId, data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function changeTree() {
|
||||||
|
if (props.data.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
segmentId.value = [];
|
||||||
|
const segmentGroup: any = {};
|
||||||
|
const segmentIdGroup: any = [];
|
||||||
|
const fixSpans: any[] = [];
|
||||||
|
const segmentHeaders: any = [];
|
||||||
|
for (const span of props.data) {
|
||||||
|
if (span.parentSpanId === -1) {
|
||||||
|
segmentHeaders.push(span);
|
||||||
|
} else {
|
||||||
|
const index = props.data.findIndex(
|
||||||
|
(i: any) =>
|
||||||
|
i.segmentId === span.segmentId && i.spanId === span.spanId - 1
|
||||||
|
);
|
||||||
|
const fixSpanKeyContent = {
|
||||||
|
traceId: span.traceId,
|
||||||
|
segmentId: span.segmentId,
|
||||||
|
spanId: span.spanId - 1,
|
||||||
|
parentSpanId: span.spanId - 2,
|
||||||
|
};
|
||||||
|
if (index === -1 && !_.find(fixSpans, fixSpanKeyContent)) {
|
||||||
|
fixSpans.push({
|
||||||
|
...fixSpanKeyContent,
|
||||||
|
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: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
segmentHeaders.forEach((span: Span) => {
|
||||||
|
if (span.refs.length) {
|
||||||
|
span.refs.forEach((ref) => {
|
||||||
|
const index = props.data.findIndex(
|
||||||
|
(i: any) =>
|
||||||
|
ref.parentSegmentId === i.segmentId && ref.parentSpanId === i.spanId
|
||||||
|
);
|
||||||
|
if (index === -1) {
|
||||||
|
// create a known broken node.
|
||||||
|
const i = ref.parentSpanId;
|
||||||
|
const fixSpanKeyContent = {
|
||||||
|
traceId: ref.traceId,
|
||||||
|
segmentId: ref.parentSegmentId,
|
||||||
|
spanId: i,
|
||||||
|
parentSpanId: i > -1 ? 0 : -1,
|
||||||
|
};
|
||||||
|
if (!_.find(fixSpans, fixSpanKeyContent)) {
|
||||||
|
fixSpans.push({
|
||||||
|
...fixSpanKeyContent,
|
||||||
|
refs: [],
|
||||||
|
endpointName: `VNode: ${ref.parentSegmentId}`,
|
||||||
|
serviceCode: "VirtualNode",
|
||||||
|
type: `[Broken] ${ref.type}`,
|
||||||
|
peer: "",
|
||||||
|
component: `VirtualNode: #${i}`,
|
||||||
|
isError: true,
|
||||||
|
isBroken: true,
|
||||||
|
layer: "Broken",
|
||||||
|
tags: [],
|
||||||
|
logs: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// if root broken node is not exist, create a root broken node.
|
||||||
|
if (fixSpanKeyContent.parentSpanId > -1) {
|
||||||
|
const fixRootSpanKeyContent = {
|
||||||
|
traceId: ref.traceId,
|
||||||
|
segmentId: ref.parentSegmentId,
|
||||||
|
spanId: 0,
|
||||||
|
parentSpanId: -1,
|
||||||
|
};
|
||||||
|
if (!_.find(fixSpans, fixRootSpanKeyContent)) {
|
||||||
|
fixSpans.push({
|
||||||
|
...fixRootSpanKeyContent,
|
||||||
|
refs: [],
|
||||||
|
endpointName: `VNode: ${ref.parentSegmentId}`,
|
||||||
|
serviceCode: "VirtualNode",
|
||||||
|
type: `[Broken] ${ref.type}`,
|
||||||
|
peer: "",
|
||||||
|
component: `VirtualNode: #0`,
|
||||||
|
isError: true,
|
||||||
|
isBroken: true,
|
||||||
|
layer: "Broken",
|
||||||
|
tags: [],
|
||||||
|
logs: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
[...fixSpans, ...props.data].forEach((i) => {
|
||||||
|
i.label = i.endpointName || "no operation name";
|
||||||
|
i.children = [];
|
||||||
|
if (segmentGroup[i.segmentId] === undefined) {
|
||||||
|
segmentIdGroup.push(i.segmentId);
|
||||||
|
segmentGroup[i.segmentId] = [];
|
||||||
|
segmentGroup[i.segmentId].push(i);
|
||||||
|
} else {
|
||||||
|
segmentGroup[i.segmentId].push(i);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
fixSpansSize.value = fixSpans.length;
|
||||||
|
segmentIdGroup.forEach((id: string) => {
|
||||||
|
const currentSegment = segmentGroup[id].sort(
|
||||||
|
(a: Span, b: Span) => b.parentSpanId - a.parentSpanId
|
||||||
|
);
|
||||||
|
currentSegment.forEach((s: any) => {
|
||||||
|
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 = _.filter(props.data, (span: Span) => {
|
||||||
|
return _.find(span.refs, {
|
||||||
|
traceId: s.traceId,
|
||||||
|
parentSegmentId: s.segmentId,
|
||||||
|
parentSpanId: s.spanId,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
if (children.length > 0) {
|
||||||
|
s.children.push(...children);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
segmentGroup[id] = currentSegment[currentSegment.length - 1];
|
||||||
|
});
|
||||||
|
segmentIdGroup.forEach((id: string) => {
|
||||||
|
segmentGroup[id].refs.forEach((ref: any) => {
|
||||||
|
if (ref.traceId === props.traceId) {
|
||||||
|
traverseTree(
|
||||||
|
segmentGroup[ref.parentSegmentId],
|
||||||
|
ref.parentSpanId,
|
||||||
|
ref.parentSegmentId,
|
||||||
|
segmentGroup[id]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
for (const i in segmentGroup) {
|
||||||
|
if (segmentGroup[i].refs.length === 0) {
|
||||||
|
segmentId.value.push(segmentGroup[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
segmentId.value.forEach((i: any) => {
|
||||||
|
collapse(i);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function collapse(d: Span) {
|
||||||
|
if (d.children) {
|
||||||
|
let dur = d.endTime - d.startTime;
|
||||||
|
d.children.forEach((i: Span) => {
|
||||||
|
dur -= i.endTime - i.startTime;
|
||||||
|
});
|
||||||
|
d.dur = dur < 0 ? 0 : dur;
|
||||||
|
d.children.forEach((i: Span) => collapse(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
d3.selectAll(".d3-tip").remove();
|
||||||
|
});
|
||||||
|
watch(
|
||||||
|
() => props.data,
|
||||||
|
() => {
|
||||||
|
if (!props.data.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
loading.value = true;
|
||||||
|
changeTree();
|
||||||
|
tree.value.init(
|
||||||
|
{ label: "TRACE_ROOT", children: segmentId.value },
|
||||||
|
props.data,
|
||||||
|
fixSpansSize.value
|
||||||
|
);
|
||||||
|
tree.value.draw(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
loading.value = false;
|
||||||
|
}, 200);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.trace-node .group {
|
||||||
|
cursor: pointer;
|
||||||
|
fill-opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trace-node-container {
|
||||||
|
fill: rgba(0, 0, 0, 0);
|
||||||
|
stroke-width: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
fill: rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.trace-node .node-text {
|
||||||
|
font: 12.5px sans-serif;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.domain {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-charts-item {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border: 1px solid;
|
||||||
|
font-size: 11px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trace-list .trace-node rect {
|
||||||
|
&:hover {
|
||||||
|
fill: rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-c-text {
|
||||||
|
white-space: pre;
|
||||||
|
overflow: auto;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
</style>
|
@ -12,10 +12,7 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License. -->
|
limitations under the License. -->
|
||||||
<template>
|
<template>
|
||||||
<div class="time-charts">
|
<div class="time-charts">
|
||||||
<div class="trace-t-loading" v-show="loading">
|
<div>
|
||||||
<Icon iconName="spinner" size="sm" />
|
|
||||||
</div>
|
|
||||||
<div class="service-code">
|
|
||||||
<span
|
<span
|
||||||
class="time-charts-item mr-5"
|
class="time-charts-item mr-5"
|
||||||
v-for="(i, index) in list"
|
v-for="(i, index) in list"
|
||||||
@ -29,87 +26,36 @@ limitations under the License. -->
|
|||||||
{{ t("exportImage") }}
|
{{ t("exportImage") }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="trace-list">
|
<div class="trace-chart">
|
||||||
<div ref="traceList"></div>
|
<Graph :data="data" :traceId="traceId" type="List" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, watch, onBeforeUnmount, onMounted } from "vue";
|
import { computed } from "vue";
|
||||||
import type { PropType } from "vue";
|
import type { PropType } from "vue";
|
||||||
import _ from "lodash";
|
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import * as d3 from "d3";
|
import * as d3 from "d3";
|
||||||
import ListGraph from "./utils/d3-trace";
|
|
||||||
import { Span } from "@/types/trace";
|
import { Span } from "@/types/trace";
|
||||||
|
import Graph from "./Graph.vue";
|
||||||
|
|
||||||
/* global defineProps, Nullable*/
|
/* global defineProps*/
|
||||||
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: "" },
|
||||||
});
|
});
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const loading = ref<boolean>(false);
|
const list = computed(() =>
|
||||||
const showDetail = ref<boolean>(false);
|
Array.from(new Set(props.data.map((i: Span) => i.serviceCode)))
|
||||||
const fixSpansSize = ref<number>(0);
|
);
|
||||||
const segmentId = ref<any>([]);
|
|
||||||
const currentSpan = ref<Array<Span>>([]);
|
|
||||||
const list = ref<string[]>([]);
|
|
||||||
const tree = ref<any>(null);
|
|
||||||
const traceList = ref<Nullable<HTMLDivElement>>(null);
|
|
||||||
|
|
||||||
onMounted(() => {
|
function computedScale(i: number) {
|
||||||
loading.value = true;
|
|
||||||
changeTree();
|
|
||||||
if (!traceList.value) {
|
|
||||||
loading.value = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
tree.value = new ListGraph(traceList.value, handleSelectSpan);
|
|
||||||
tree.value.init(
|
|
||||||
{ label: "TRACE_ROOT", children: segmentId.value },
|
|
||||||
props.data,
|
|
||||||
fixSpansSize.value
|
|
||||||
);
|
|
||||||
tree.value.draw();
|
|
||||||
loading.value = false;
|
|
||||||
});
|
|
||||||
function handleSelectSpan(i: any) {
|
|
||||||
currentSpan.value = i.data;
|
|
||||||
showDetail.value = true;
|
|
||||||
}
|
|
||||||
function traverseTree(node: any, spanId: string, segmentId: string, data: any) {
|
|
||||||
if (!node || node.isBroken) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (node.spanId === spanId && node.segmentId === segmentId) {
|
|
||||||
node.children.push(data);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (node.children && node.children.length > 0) {
|
|
||||||
node.children.forEach((nodeItem: any) => {
|
|
||||||
traverseTree(nodeItem, spanId, segmentId, data);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function computedScale(i: any) {
|
|
||||||
// Rainbow map
|
|
||||||
const sequentialScale = d3
|
const sequentialScale = d3
|
||||||
.scaleSequential()
|
.scaleSequential()
|
||||||
.domain([0, list.value.length + 1])
|
.domain([0, list.value.length + 1])
|
||||||
.interpolator(d3.interpolateCool);
|
.interpolator(d3.interpolateCool);
|
||||||
return sequentialScale(i);
|
return sequentialScale(i);
|
||||||
}
|
}
|
||||||
function collapse(d: Span) {
|
|
||||||
if (d.children) {
|
|
||||||
let dur = d.endTime - d.startTime;
|
|
||||||
d.children.forEach((i: Span) => {
|
|
||||||
dur -= i.endTime - i.startTime;
|
|
||||||
});
|
|
||||||
d.dur = dur < 0 ? 0 : dur;
|
|
||||||
d.children.forEach((i: Span) => collapse(i));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function downloadTrace() {
|
function downloadTrace() {
|
||||||
const serializer = new XMLSerializer();
|
const serializer = new XMLSerializer();
|
||||||
const svgNode: any = d3.select(".trace-list-dowanload").node();
|
const svgNode: any = d3.select(".trace-list-dowanload").node();
|
||||||
@ -136,234 +82,15 @@ function downloadTrace() {
|
|||||||
tagA.click();
|
tagA.click();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
function changeTree() {
|
|
||||||
if (props.data.length === 0) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
list.value = Array.from(new Set(props.data.map((i: Span) => i.serviceCode)));
|
|
||||||
segmentId.value = [];
|
|
||||||
const segmentGroup: any = {};
|
|
||||||
const segmentIdGroup: any = [];
|
|
||||||
const fixSpans: any = [];
|
|
||||||
const segmentHeaders: any = [];
|
|
||||||
for (const span of props.data) {
|
|
||||||
if (span.parentSpanId === -1) {
|
|
||||||
segmentHeaders.push(span);
|
|
||||||
} else {
|
|
||||||
const index = props.data.findIndex(
|
|
||||||
(i: any) =>
|
|
||||||
i.segmentId === span.segmentId && i.spanId === span.spanId - 1
|
|
||||||
);
|
|
||||||
const fixSpanKeyContent = {
|
|
||||||
traceId: span.traceId,
|
|
||||||
segmentId: span.segmentId,
|
|
||||||
spanId: span.spanId - 1,
|
|
||||||
parentSpanId: span.spanId - 2,
|
|
||||||
};
|
|
||||||
if (index === -1 && !_.find(fixSpans, fixSpanKeyContent)) {
|
|
||||||
fixSpans.push({
|
|
||||||
...fixSpanKeyContent,
|
|
||||||
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: [],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
segmentHeaders.forEach((span: Span) => {
|
|
||||||
if (span.refs.length) {
|
|
||||||
span.refs.forEach((ref) => {
|
|
||||||
const index = props.data.findIndex(
|
|
||||||
(i: any) =>
|
|
||||||
ref.parentSegmentId === i.segmentId && ref.parentSpanId === i.spanId
|
|
||||||
);
|
|
||||||
if (index === -1) {
|
|
||||||
// create a known broken node.
|
|
||||||
const i = ref.parentSpanId;
|
|
||||||
const fixSpanKeyContent = {
|
|
||||||
traceId: ref.traceId,
|
|
||||||
segmentId: ref.parentSegmentId,
|
|
||||||
spanId: i,
|
|
||||||
parentSpanId: i > -1 ? 0 : -1,
|
|
||||||
};
|
|
||||||
if (!_.find(fixSpans, fixSpanKeyContent)) {
|
|
||||||
fixSpans.push({
|
|
||||||
...fixSpanKeyContent,
|
|
||||||
refs: [],
|
|
||||||
endpointName: `VNode: ${ref.parentSegmentId}`,
|
|
||||||
serviceCode: "VirtualNode",
|
|
||||||
type: `[Broken] ${ref.type}`,
|
|
||||||
peer: "",
|
|
||||||
component: `VirtualNode: #${i}`,
|
|
||||||
isError: true,
|
|
||||||
isBroken: true,
|
|
||||||
layer: "Broken",
|
|
||||||
tags: [],
|
|
||||||
logs: [],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// if root broken node is not exist, create a root broken node.
|
|
||||||
if (fixSpanKeyContent.parentSpanId > -1) {
|
|
||||||
const fixRootSpanKeyContent = {
|
|
||||||
traceId: ref.traceId,
|
|
||||||
segmentId: ref.parentSegmentId,
|
|
||||||
spanId: 0,
|
|
||||||
parentSpanId: -1,
|
|
||||||
};
|
|
||||||
if (!_.find(fixSpans, fixRootSpanKeyContent)) {
|
|
||||||
fixSpans.push({
|
|
||||||
...fixRootSpanKeyContent,
|
|
||||||
refs: [],
|
|
||||||
endpointName: `VNode: ${ref.parentSegmentId}`,
|
|
||||||
serviceCode: "VirtualNode",
|
|
||||||
type: `[Broken] ${ref.type}`,
|
|
||||||
peer: "",
|
|
||||||
component: `VirtualNode: #0`,
|
|
||||||
isError: true,
|
|
||||||
isBroken: true,
|
|
||||||
layer: "Broken",
|
|
||||||
tags: [],
|
|
||||||
logs: [],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
[...fixSpans, ...props.data].forEach((i) => {
|
|
||||||
i.label = i.endpointName || "no operation name";
|
|
||||||
i.children = [];
|
|
||||||
if (segmentGroup[i.segmentId] === undefined) {
|
|
||||||
segmentIdGroup.push(i.segmentId);
|
|
||||||
segmentGroup[i.segmentId] = [];
|
|
||||||
segmentGroup[i.segmentId].push(i);
|
|
||||||
} else {
|
|
||||||
segmentGroup[i.segmentId].push(i);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
fixSpansSize.value = fixSpans.length;
|
|
||||||
segmentIdGroup.forEach((id: string) => {
|
|
||||||
const currentSegment = segmentGroup[id].sort(
|
|
||||||
(a: Span, b: Span) => b.parentSpanId - a.parentSpanId
|
|
||||||
);
|
|
||||||
currentSegment.forEach((s: any) => {
|
|
||||||
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 = _.filter(props.data, (span: Span) => {
|
|
||||||
return _.find(span.refs, {
|
|
||||||
traceId: s.traceId,
|
|
||||||
parentSegmentId: s.segmentId,
|
|
||||||
parentSpanId: s.spanId,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
if (children.length > 0) {
|
|
||||||
s.children.push(...children);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
segmentGroup[id] = currentSegment[currentSegment.length - 1];
|
|
||||||
});
|
|
||||||
segmentIdGroup.forEach((id: string) => {
|
|
||||||
segmentGroup[id].refs.forEach((ref: any) => {
|
|
||||||
if (ref.traceId === props.traceId) {
|
|
||||||
traverseTree(
|
|
||||||
segmentGroup[ref.parentSegmentId],
|
|
||||||
ref.parentSpanId,
|
|
||||||
ref.parentSegmentId,
|
|
||||||
segmentGroup[id]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
for (const i in segmentGroup) {
|
|
||||||
if (segmentGroup[i].refs.length === 0) {
|
|
||||||
segmentId.value.push(segmentGroup[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
segmentId.value.forEach((i: any) => {
|
|
||||||
collapse(i);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
d3.selectAll(".d3-tip").remove();
|
|
||||||
});
|
|
||||||
watch(
|
|
||||||
() => props.data,
|
|
||||||
() => {
|
|
||||||
if (!props.data.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
loading.value = true;
|
|
||||||
changeTree();
|
|
||||||
tree.value.init(
|
|
||||||
{ label: "TRACE_ROOT", children: segmentId.value },
|
|
||||||
props.data,
|
|
||||||
fixSpansSize.value
|
|
||||||
);
|
|
||||||
tree.value.draw(() => {
|
|
||||||
setTimeout(() => {
|
|
||||||
loading.value = false;
|
|
||||||
}, 200);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.time-charts {
|
.time-charts {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
position: relative;
|
|
||||||
height: calc(100% - 95px);
|
height: calc(100% - 95px);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.trace-node .group {
|
|
||||||
cursor: pointer;
|
|
||||||
fill-opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.trace-node-container {
|
|
||||||
fill: rgba(0, 0, 0, 0);
|
|
||||||
stroke-width: 5px;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
fill: rgba(0, 0, 0, 0.05);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.trace-node .node-text {
|
|
||||||
font: 12.5px sans-serif;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.domain {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.time-charts-item {
|
.time-charts-item {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 2px 8px;
|
padding: 2px 8px;
|
||||||
@ -372,22 +99,10 @@ watch(
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.trace-list {
|
.trace-chart {
|
||||||
fill: rgba(0, 0, 0, 0);
|
fill: rgba(0, 0, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
.trace-list .trace-node rect {
|
|
||||||
&:hover {
|
|
||||||
fill: rgba(0, 0, 0, 0.05);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog-c-text {
|
|
||||||
white-space: pre;
|
|
||||||
overflow: auto;
|
|
||||||
font-family: monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
float: right;
|
float: right;
|
||||||
}
|
}
|
||||||
|
93
src/views/dashboard/related/trace/components/Tree.vue
Normal file
93
src/views/dashboard/related/trace/components/Tree.vue
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
<!-- 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-tree-charts flex-v">
|
||||||
|
<div>
|
||||||
|
<span
|
||||||
|
class="time-charts-item mr-5"
|
||||||
|
v-for="(i, index) in list"
|
||||||
|
:key="index"
|
||||||
|
:style="`color:${computedScale(index)}`"
|
||||||
|
>
|
||||||
|
<Icon iconName="issue-open-m" class="mr-5" size="sm" />
|
||||||
|
<span>{{ i }}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div style="padding: 0 30px">
|
||||||
|
<a class="trace-tree-btn mr-10" @click="tree.setDefault()">Default</a>
|
||||||
|
<a class="trace-tree-btn mr-10" @click="tree.getTopSlow()">
|
||||||
|
Top 5 of slow
|
||||||
|
</a>
|
||||||
|
<a class="trace-tree-btn mr-10" @click="tree.getTopChild()">
|
||||||
|
Top 5 of children
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="trace-tree" style="height: 100%">
|
||||||
|
<Graph :data="data" :traceId="traceId" type="Tree" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import * as d3 from "d3";
|
||||||
|
import Graph from "./Graph.vue";
|
||||||
|
import type { PropType } from "vue";
|
||||||
|
import { Span } from "@/types/trace";
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
|
import { ref, onMounted } from "vue";
|
||||||
|
|
||||||
|
/* global defineProps */
|
||||||
|
const props = defineProps({
|
||||||
|
data: { type: Array as PropType<Span[]>, default: () => [] },
|
||||||
|
traceId: { type: String, default: "" },
|
||||||
|
});
|
||||||
|
const { t } = useI18n();
|
||||||
|
const list = ref<string[]>([]);
|
||||||
|
const tree = ref<any>(null);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
list.value = Array.from(new Set(props.data.map((i: Span) => i.serviceCode)));
|
||||||
|
});
|
||||||
|
function computedScale(i: number) {
|
||||||
|
const sequentialScale = d3
|
||||||
|
.scaleSequential()
|
||||||
|
.domain([0, list.value.length + 1])
|
||||||
|
.interpolator(d3.interpolateCool);
|
||||||
|
return sequentialScale(i);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.trace-tree-btn {
|
||||||
|
display: inline-block;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 0px 7px;
|
||||||
|
background-color: #40454e;
|
||||||
|
color: #eee;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-charts {
|
||||||
|
overflow: auto;
|
||||||
|
padding: 10px;
|
||||||
|
position: relative;
|
||||||
|
height: calc(100% - 95px);
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-charts-item {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border: 1px solid;
|
||||||
|
font-size: 11px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
</style>
|
24
src/views/dashboard/related/trace/components/index.ts
Normal file
24
src/views/dashboard/related/trace/components/index.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
/**
|
||||||
|
* 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 List from "./List.vue";
|
||||||
|
import Tree from "./Tree.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
List,
|
||||||
|
Tree,
|
||||||
|
};
|
@ -17,17 +17,8 @@
|
|||||||
|
|
||||||
import * as d3 from "d3";
|
import * as d3 from "d3";
|
||||||
import d3tip from "d3-tip";
|
import d3tip from "d3-tip";
|
||||||
import { Span, Trace } from "@/types/trace";
|
import { Trace } from "@/types/trace";
|
||||||
import { func } from "vue-types";
|
|
||||||
|
|
||||||
const type = {
|
|
||||||
MQ: "#bf99f8",
|
|
||||||
Http: "#72a5fd",
|
|
||||||
Database: "#ff6732",
|
|
||||||
Unknown: "#ffc107",
|
|
||||||
Cache: "#00bcd4",
|
|
||||||
RPCFramework: "#ee4395",
|
|
||||||
};
|
|
||||||
export default class ListGraph {
|
export default class ListGraph {
|
||||||
private barHeight = 48;
|
private barHeight = 48;
|
||||||
private handleSelectSpan: Nullable<(i: Trace) => void> = null;
|
private handleSelectSpan: Nullable<(i: Trace) => void> = null;
|
||||||
@ -36,7 +27,6 @@ export default class ListGraph {
|
|||||||
private width = 0;
|
private width = 0;
|
||||||
private height = 0;
|
private height = 0;
|
||||||
private svg: any = null;
|
private svg: any = null;
|
||||||
private treemap: any = null;
|
|
||||||
private tip: any = null;
|
private tip: any = null;
|
||||||
private row: any[] = [];
|
private row: any[] = [];
|
||||||
private data: any = [];
|
private data: any = [];
|
||||||
@ -58,7 +48,6 @@ export default class ListGraph {
|
|||||||
.attr("class", "trace-list-dowanload")
|
.attr("class", "trace-list-dowanload")
|
||||||
.attr("width", this.width)
|
.attr("width", this.width)
|
||||||
.attr("height", this.height);
|
.attr("height", this.height);
|
||||||
this.treemap = d3.tree().size([this.height * 0.7, this.width]);
|
|
||||||
this.tip = (d3tip as any)()
|
this.tip = (d3tip as any)()
|
||||||
.attr("class", "d3-tip")
|
.attr("class", "d3-tip")
|
||||||
.offset([-8, 0])
|
.offset([-8, 0])
|
||||||
@ -250,7 +239,7 @@ export default class ListGraph {
|
|||||||
)
|
)
|
||||||
.on("click", (d: any) => {
|
.on("click", (d: any) => {
|
||||||
this.click(d, this);
|
this.click(d, this);
|
||||||
(d3 as any).event.stopPropagation();
|
// (d3 as any).event.stopPropagation();
|
||||||
});
|
});
|
||||||
node
|
node
|
||||||
.transition()
|
.transition()
|
410
src/views/dashboard/related/trace/utils/d3-trace-tree.ts
Normal file
410
src/views/dashboard/related/trace/utils/d3-trace-tree.ts
Normal file
@ -0,0 +1,410 @@
|
|||||||
|
/**
|
||||||
|
* 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 * as d3 from "d3";
|
||||||
|
import d3tip from "d3-tip";
|
||||||
|
import { Trace, Span } from "@/types/trace";
|
||||||
|
|
||||||
|
export default class TraceMap {
|
||||||
|
private i = 0;
|
||||||
|
private el: Nullable<HTMLDivElement> = null;
|
||||||
|
private handleSelectSpan: Nullable<(i: Trace) => void> = null;
|
||||||
|
private topSlow: any = [];
|
||||||
|
private height = 0;
|
||||||
|
private width = 0;
|
||||||
|
private topChild: any[] = [];
|
||||||
|
private body: any = null;
|
||||||
|
private tip: any = null;
|
||||||
|
private svg: any = null;
|
||||||
|
private treemap: any = null;
|
||||||
|
private data: any = null;
|
||||||
|
private row: any = null;
|
||||||
|
private min = 0;
|
||||||
|
private max = 0;
|
||||||
|
private list: string[] = [];
|
||||||
|
private xScale: any = null;
|
||||||
|
private sequentialScale: any = null;
|
||||||
|
private root: any = null;
|
||||||
|
private topSlowMax: number[] = [];
|
||||||
|
private topSlowMin: number[] = [];
|
||||||
|
private topChildMax: number[] = [];
|
||||||
|
private topChildMin: number[] = [];
|
||||||
|
private nodeUpdate: any = null;
|
||||||
|
|
||||||
|
constructor(el: HTMLDivElement, handleSelectSpan: (i: Trace) => void) {
|
||||||
|
this.el = el;
|
||||||
|
this.handleSelectSpan = handleSelectSpan;
|
||||||
|
this.i = 0;
|
||||||
|
this.topSlow = [];
|
||||||
|
this.topChild = [];
|
||||||
|
this.width = el.clientWidth;
|
||||||
|
this.height = el.clientHeight - 28;
|
||||||
|
this.body = d3
|
||||||
|
.select(this.el)
|
||||||
|
.append("svg")
|
||||||
|
.attr("width", this.width)
|
||||||
|
.attr("height", this.height);
|
||||||
|
this.tip = (d3tip as any)()
|
||||||
|
.attr("class", "d3-tip")
|
||||||
|
.offset([-8, 0])
|
||||||
|
.html(
|
||||||
|
(d: any) => `
|
||||||
|
<div class="mb-5">${d.data.label}</div>
|
||||||
|
${
|
||||||
|
d.data.dur
|
||||||
|
? '<div class="sm">SelfDuration: ' + d.data.dur + "ms</div>"
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
${
|
||||||
|
d.data.endTime - d.data.startTime
|
||||||
|
? '<div class="sm">TotalDuration: ' +
|
||||||
|
(d.data.endTime - d.data.startTime) +
|
||||||
|
"ms</div>"
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
`
|
||||||
|
);
|
||||||
|
this.svg = this.body
|
||||||
|
.append("g")
|
||||||
|
.attr("transform", () => `translate(120, 0)`);
|
||||||
|
this.svg.call(this.tip);
|
||||||
|
}
|
||||||
|
resize() {
|
||||||
|
if (!this.el) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// reset svg size
|
||||||
|
this.width = this.el.clientWidth;
|
||||||
|
this.height = this.el.clientHeight - 28;
|
||||||
|
this.body.attr("width", this.width).attr("height", this.height);
|
||||||
|
this.body.select("g").attr("transform", () => `translate(160, 0)`);
|
||||||
|
// reset zoom function for translate
|
||||||
|
const transform = d3.zoomTransform(this.body).translate(0, 0);
|
||||||
|
d3.zoom().transform(this.body, transform);
|
||||||
|
}
|
||||||
|
init(data: any, row: any) {
|
||||||
|
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 = Array.from(new Set(this.row.map((i: Span) => i.serviceCode)));
|
||||||
|
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.body.call(this.getZoomBehavior(this.svg));
|
||||||
|
this.root = d3.hierarchy(this.data, (d) => d.children);
|
||||||
|
this.root.x0 = this.height / 2;
|
||||||
|
this.root.y0 = 0;
|
||||||
|
this.topSlow = [];
|
||||||
|
this.topChild = [];
|
||||||
|
const that = this;
|
||||||
|
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];
|
||||||
|
this.topChildMin = this.topChild.sort((a: number, b: number) => b - a)[4];
|
||||||
|
this.update(this.root);
|
||||||
|
// Collapse the node and all it's children
|
||||||
|
function collapse(d: any) {
|
||||||
|
if (d.children) {
|
||||||
|
let dur = d.data.endTime - d.data.startTime;
|
||||||
|
d.children.forEach((i: any) => {
|
||||||
|
dur -= i.data.endTime - i.data.startTime;
|
||||||
|
});
|
||||||
|
d.dur = dur < 0 ? 0 : dur;
|
||||||
|
that.topSlow.push(dur);
|
||||||
|
that.topChild.push(d.children.length);
|
||||||
|
d.childrenLength = d.children.length;
|
||||||
|
d.children.forEach(collapse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
draw() {
|
||||||
|
this.update(this.root);
|
||||||
|
}
|
||||||
|
update(source: any) {
|
||||||
|
const that: any = this;
|
||||||
|
const treeData = this.treemap(this.root);
|
||||||
|
const nodes = treeData.descendants(),
|
||||||
|
links = treeData.descendants().slice(1);
|
||||||
|
|
||||||
|
nodes.forEach(function (d: any) {
|
||||||
|
d.y = d.depth * 140;
|
||||||
|
});
|
||||||
|
|
||||||
|
const node = this.svg.selectAll("g.node").data(nodes, (d: any) => {
|
||||||
|
return d.id || (d.id = ++this.i);
|
||||||
|
});
|
||||||
|
|
||||||
|
const nodeEnter = node
|
||||||
|
.enter()
|
||||||
|
.append("g")
|
||||||
|
.attr("class", "node")
|
||||||
|
.attr("cursor", "pointer")
|
||||||
|
.attr("transform", function () {
|
||||||
|
return "translate(" + source.y0 + "," + source.x0 + ")";
|
||||||
|
})
|
||||||
|
.on("mouseover", function (event: any, d: any) {
|
||||||
|
that.tip.show(d, this);
|
||||||
|
if (!that.timeUpdate) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const _node = that.timeUpdate._groups[0].filter(
|
||||||
|
(group: any) => group.__data__.id === that.i + 1
|
||||||
|
);
|
||||||
|
if (_node.length) {
|
||||||
|
that.timeTip.show(d, _node[0].children[1]);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.on("mouseout", function (event: any, d: any) {
|
||||||
|
that.tip.hide(d, this);
|
||||||
|
if (!that.timeUpdate) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const _node = that.timeUpdate._groups[0].filter(
|
||||||
|
(group: any) => group.__data__.id === that.i + 1
|
||||||
|
);
|
||||||
|
if (_node.length) {
|
||||||
|
that.timeTip.hide(d, _node[0].children[1]);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.on("click", function (d: any) {
|
||||||
|
(d3 as any).event.stopPropagation();
|
||||||
|
that.handleSelectSpan(d);
|
||||||
|
});
|
||||||
|
|
||||||
|
nodeEnter
|
||||||
|
.append("circle")
|
||||||
|
.attr("class", "node")
|
||||||
|
.attr("r", 1e-6)
|
||||||
|
.style("fill", (d: any) =>
|
||||||
|
d._children
|
||||||
|
? this.sequentialScale(this.list.indexOf(d.data.serviceCode))
|
||||||
|
: "#fff"
|
||||||
|
)
|
||||||
|
.attr("stroke", (d: any) =>
|
||||||
|
this.sequentialScale(this.list.indexOf(d.data.serviceCode))
|
||||||
|
)
|
||||||
|
.attr("stroke-width", 2.5);
|
||||||
|
|
||||||
|
nodeEnter
|
||||||
|
.append("text")
|
||||||
|
.attr("font-size", 11)
|
||||||
|
.attr("dy", "-0.5em")
|
||||||
|
.attr("x", function (d: any) {
|
||||||
|
return d.children || d._children ? -15 : 15;
|
||||||
|
})
|
||||||
|
.attr("text-anchor", function (d: any) {
|
||||||
|
return d.children || d._children ? "end" : "start";
|
||||||
|
})
|
||||||
|
.text((d: any) =>
|
||||||
|
d.data.label.length > 19
|
||||||
|
? (d.data.isError ? "◉ " : "") + d.data.label.slice(0, 19) + "..."
|
||||||
|
: (d.data.isError ? "◉ " : "") + d.data.label
|
||||||
|
)
|
||||||
|
.style("fill", (d: any) => (!d.data.isError ? "#3d444f" : "#E54C17"));
|
||||||
|
nodeEnter
|
||||||
|
.append("text")
|
||||||
|
.attr("class", "node-text")
|
||||||
|
.attr("x", function (d: any) {
|
||||||
|
return d.children || d._children ? -15 : 15;
|
||||||
|
})
|
||||||
|
.attr("dy", "1em")
|
||||||
|
.attr("fill", "#bbb")
|
||||||
|
.attr("text-anchor", function (d: any) {
|
||||||
|
return d.children || d._children ? "end" : "start";
|
||||||
|
})
|
||||||
|
.style("font-size", "10px")
|
||||||
|
.text(
|
||||||
|
(d: any) =>
|
||||||
|
`${d.data.layer || ""}${
|
||||||
|
d.data.component ? "-" + d.data.component : d.data.component || ""
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
nodeEnter
|
||||||
|
.append("rect")
|
||||||
|
.attr("rx", 1)
|
||||||
|
.attr("ry", 1)
|
||||||
|
.attr("height", 2)
|
||||||
|
.attr("width", 100)
|
||||||
|
.attr("x", function (d: any) {
|
||||||
|
return d.children || d._children ? "-110" : "10";
|
||||||
|
})
|
||||||
|
.attr("y", -1)
|
||||||
|
.style("fill", "#00000020");
|
||||||
|
nodeEnter
|
||||||
|
.append("rect")
|
||||||
|
.attr("rx", 1)
|
||||||
|
.attr("ry", 1)
|
||||||
|
.attr("height", 2)
|
||||||
|
.attr("width", (d: any) => {
|
||||||
|
if (!d.data.endTime || !d.data.startTime) return 0;
|
||||||
|
return this.xScale(d.data.endTime - d.data.startTime) + 1 || 0;
|
||||||
|
})
|
||||||
|
.attr("x", (d: any) => {
|
||||||
|
if (!d.data.endTime || !d.data.startTime) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (d.children || d._children) {
|
||||||
|
return -110 + this.xScale(d.data.startTime - this.min);
|
||||||
|
}
|
||||||
|
return 10 + this.xScale(d.data.startTime - this.min);
|
||||||
|
})
|
||||||
|
.attr("y", -1)
|
||||||
|
.style("fill", (d: any) =>
|
||||||
|
this.sequentialScale(this.list.indexOf(d.data.serviceCode))
|
||||||
|
);
|
||||||
|
const nodeUpdate = nodeEnter.merge(node);
|
||||||
|
this.nodeUpdate = nodeUpdate;
|
||||||
|
nodeUpdate
|
||||||
|
.transition()
|
||||||
|
.duration(600)
|
||||||
|
.attr("transform", function (d: any) {
|
||||||
|
return "translate(" + d.y + "," + d.x + ")";
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update the node attributes and style
|
||||||
|
nodeUpdate
|
||||||
|
.select("circle.node")
|
||||||
|
.attr("r", 5)
|
||||||
|
.style("fill", (d: any) =>
|
||||||
|
d._children
|
||||||
|
? this.sequentialScale(this.list.indexOf(d.data.serviceCode))
|
||||||
|
: "#fff"
|
||||||
|
)
|
||||||
|
.attr("cursor", "pointer")
|
||||||
|
.on("click", (d: any) => {
|
||||||
|
(d3 as any).event.stopPropagation();
|
||||||
|
click(d);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remove any exiting nodes
|
||||||
|
const nodeExit = node
|
||||||
|
.exit()
|
||||||
|
.transition()
|
||||||
|
.duration(600)
|
||||||
|
.attr("transform", function () {
|
||||||
|
return "translate(" + source.y + "," + source.x + ")";
|
||||||
|
})
|
||||||
|
.remove();
|
||||||
|
|
||||||
|
nodeExit.select("circle").attr("r", 1e-6);
|
||||||
|
|
||||||
|
nodeExit.select("text").style("fill-opacity", 1e-6);
|
||||||
|
|
||||||
|
const link = this.svg
|
||||||
|
.selectAll("path.tree-link")
|
||||||
|
.data(links, function (d: any) {
|
||||||
|
return d.id;
|
||||||
|
})
|
||||||
|
.style("stroke-width", 1.5);
|
||||||
|
|
||||||
|
const linkEnter = link
|
||||||
|
.enter()
|
||||||
|
.insert("path", "g")
|
||||||
|
.attr("class", "tree-link")
|
||||||
|
.attr("d", function () {
|
||||||
|
const o = { x: source.x0, y: source.y0 };
|
||||||
|
return diagonal(o, o);
|
||||||
|
})
|
||||||
|
.style("stroke-width", 1.5);
|
||||||
|
|
||||||
|
const linkUpdate = linkEnter.merge(link);
|
||||||
|
linkUpdate
|
||||||
|
.transition()
|
||||||
|
.duration(600)
|
||||||
|
.attr("d", function (d: any) {
|
||||||
|
return diagonal(d, d.parent);
|
||||||
|
});
|
||||||
|
|
||||||
|
nodes.forEach(function (d: any) {
|
||||||
|
d.x0 = d.x;
|
||||||
|
d.y0 = d.y;
|
||||||
|
});
|
||||||
|
function diagonal(s: any, d: any) {
|
||||||
|
return `M ${s.y} ${s.x}
|
||||||
|
C ${(s.y + d.y) / 2} ${s.x}, ${(s.y + d.y) / 2} ${d.x},
|
||||||
|
${d.y} ${d.x}`;
|
||||||
|
}
|
||||||
|
function click(d: any) {
|
||||||
|
if (d.children) {
|
||||||
|
d._children = d.children;
|
||||||
|
d.children = null;
|
||||||
|
} else {
|
||||||
|
d.children = d._children;
|
||||||
|
d._children = null;
|
||||||
|
}
|
||||||
|
that.update(d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setDefault() {
|
||||||
|
d3.selectAll(".time-inner").style("opacity", 1);
|
||||||
|
d3.selectAll(".time-inner-duration").style("opacity", 0);
|
||||||
|
d3.selectAll(".trace-tree-node-selfdur").style("opacity", 0);
|
||||||
|
d3.selectAll(".trace-tree-node-selfchild").style("opacity", 0);
|
||||||
|
this.nodeUpdate._groups[0].forEach((i: any) => {
|
||||||
|
d3.select(i).style("opacity", 1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
getTopChild() {
|
||||||
|
d3.selectAll(".time-inner").style("opacity", 1);
|
||||||
|
d3.selectAll(".time-inner-duration").style("opacity", 0);
|
||||||
|
d3.selectAll(".trace-tree-node-selfdur").style("opacity", 0);
|
||||||
|
d3.selectAll(".trace-tree-node-selfchild").style("opacity", 1);
|
||||||
|
this.nodeUpdate._groups[0].forEach((i: any) => {
|
||||||
|
d3.select(i).style("opacity", 0.2);
|
||||||
|
if (
|
||||||
|
i.__data__.data.children.length >= this.topChildMin &&
|
||||||
|
i.__data__.data.children.length <= this.topChildMax
|
||||||
|
) {
|
||||||
|
d3.select(i).style("opacity", 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
getTopSlow() {
|
||||||
|
d3.selectAll(".time-inner").style("opacity", 0);
|
||||||
|
d3.selectAll(".time-inner-duration").style("opacity", 1);
|
||||||
|
d3.selectAll(".trace-tree-node-selfchild").style("opacity", 0);
|
||||||
|
d3.selectAll(".trace-tree-node-selfdur").style("opacity", 1);
|
||||||
|
this.nodeUpdate._groups[0].forEach((i: any) => {
|
||||||
|
d3.select(i).style("opacity", 0.2);
|
||||||
|
if (
|
||||||
|
i.__data__.data.dur >= this.topSlowMin &&
|
||||||
|
i.__data__.data.dur <= this.topSlowMax
|
||||||
|
) {
|
||||||
|
d3.select(i).style("opacity", 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
getZoomBehavior(g: any) {
|
||||||
|
return d3
|
||||||
|
.zoom()
|
||||||
|
.scaleExtent([0.3, 10])
|
||||||
|
.on("zoom", () => {
|
||||||
|
g.attr(
|
||||||
|
"transform",
|
||||||
|
`translate(${(d3 as any).event.transform.x + 120},${
|
||||||
|
(d3 as any).event.transform.y
|
||||||
|
})scale(${(d3 as any).event.transform.k})`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user