mirror of
https://github.com/apache/skywalking-booster-ui.git
synced 2025-07-18 13:25:25 +00:00
feat: add trace list
This commit is contained in:
parent
c23e24293b
commit
ceceabec66
1
package-lock.json
generated
1
package-lock.json
generated
@ -13,6 +13,7 @@
|
|||||||
"d3-tip": "^0.9.1",
|
"d3-tip": "^0.9.1",
|
||||||
"echarts": "^5.2.2",
|
"echarts": "^5.2.2",
|
||||||
"element-plus": "^2.0.2",
|
"element-plus": "^2.0.2",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
"pinia": "^2.0.5",
|
"pinia": "^2.0.5",
|
||||||
"three": "^0.131.3",
|
"three": "^0.131.3",
|
||||||
"three-orbit-controls": "^82.1.0",
|
"three-orbit-controls": "^82.1.0",
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
"d3-tip": "^0.9.1",
|
"d3-tip": "^0.9.1",
|
||||||
"echarts": "^5.2.2",
|
"echarts": "^5.2.2",
|
||||||
"element-plus": "^2.0.2",
|
"element-plus": "^2.0.2",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
"pinia": "^2.0.5",
|
"pinia": "^2.0.5",
|
||||||
"three": "^0.131.3",
|
"three": "^0.131.3",
|
||||||
"three-orbit-controls": "^82.1.0",
|
"three-orbit-controls": "^82.1.0",
|
||||||
|
10
src/views/dashboard/related/trace/Content.vue → src/assets/icons/spinner.svg
Normal file → Executable file
10
src/views/dashboard/related/trace/Content.vue → src/assets/icons/spinner.svg
Normal file → Executable file
@ -4,15 +4,13 @@ this work for additional information regarding copyright ownership.
|
|||||||
The ASF licenses this file to You under the Apache License, Version 2.0
|
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 not use this file except in compliance with
|
||||||
the License. You may obtain a copy of the License at
|
the License. You may obtain a copy of the License at
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
Unless required by applicable law or agreed to in writing, software
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
See the License for the specific language governing permissions and
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License. -->
|
limitations under the License. -->
|
||||||
<template>
|
|
||||||
<Detail />
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14"><g fill="none" fill-rule="evenodd"><circle cx="7" cy="7" r="6" stroke="#000" stroke-opacity=".1" stroke-width="2"/><path fill="#000" fill-opacity=".1" fill-rule="nonzero" d="M7 0a7 7 0 0 1 7 7h-2a5 5 0 0 0-5-5V0z"/></g></svg>
|
||||||
</template>
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import Detail from "./components/Detail.vue";
|
|
||||||
</script>
|
|
Before Width: | Height: | Size: 877 B After Width: | Height: | Size: 1.0 KiB |
@ -19,7 +19,7 @@ limitations under the License. -->
|
|||||||
</div>
|
</div>
|
||||||
<div class="trace flex-h">
|
<div class="trace flex-h">
|
||||||
<TraceList />
|
<TraceList />
|
||||||
<Content />
|
<Detail />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -27,7 +27,7 @@ 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 Content from "../related/trace/Content.vue";
|
import Detail from "../related/trace/Detail.vue";
|
||||||
|
|
||||||
/*global defineProps */
|
/*global defineProps */
|
||||||
defineProps({
|
defineProps({
|
||||||
|
@ -92,14 +92,20 @@ limitations under the License. -->
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<List
|
||||||
|
v-if="displayMode == 'list' && traceStore.currentTrace.endpointNames"
|
||||||
|
:data="traceStore.traceSpans"
|
||||||
|
:traceId="traceStore.currentTrace.traceIds[0].value"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { ref, onMounted } from "vue";
|
import { ref } 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";
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const traceStore = useTraceStore();
|
const traceStore = useTraceStore();
|
||||||
@ -107,11 +113,6 @@ const traceId = ref<string>("");
|
|||||||
const displayMode = ref<string>("list");
|
const displayMode = ref<string>("list");
|
||||||
const dateFormat = (date: number, pattern = "YYYY-MM-DD HH:mm:ss") =>
|
const dateFormat = (date: number, pattern = "YYYY-MM-DD HH:mm:ss") =>
|
||||||
dayjs(date).format(pattern);
|
dayjs(date).format(pattern);
|
||||||
onMounted(() => {
|
|
||||||
if (traceStore.currentTrace.traceIds && traceStore.currentTrace.traceIds[0]) {
|
|
||||||
traceId.value = traceStore.currentTrace.traceIds[0].value;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
function handleClick(ids: string[]) {
|
function handleClick(ids: string[]) {
|
||||||
let copyValue = null;
|
let copyValue = null;
|
||||||
if (ids.length === 1) {
|
if (ids.length === 1) {
|
@ -32,7 +32,7 @@ limitations under the License. -->
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="trace-t-loading" v-loading="loading">
|
<div class="trace-t-loading" v-show="loading">
|
||||||
<Icon iconName="spinner" size="sm" />
|
<Icon iconName="spinner" size="sm" />
|
||||||
</div>
|
</div>
|
||||||
<div class="trace-t-wrapper">
|
<div class="trace-t-wrapper">
|
||||||
|
390
src/views/dashboard/related/trace/components/List.vue
Normal file
390
src/views/dashboard/related/trace/components/List.vue
Normal file
@ -0,0 +1,390 @@
|
|||||||
|
<!-- 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="time-charts scroll_hide">
|
||||||
|
<div class="trace-t-loading" v-show="loading">
|
||||||
|
<Icon iconName="spinner" size="sm" />
|
||||||
|
</div>
|
||||||
|
<transition-group name="fade" tag="a" class="mb-5">
|
||||||
|
<span
|
||||||
|
class="time-charts-item mr-10"
|
||||||
|
v-for="(i, index) in data"
|
||||||
|
:key="index"
|
||||||
|
:style="`color:${computedScale(index)}`"
|
||||||
|
>
|
||||||
|
<svg class="icon vm mr-5 sm">
|
||||||
|
<use xlink:href="#issue-open-m"></use>
|
||||||
|
</svg>
|
||||||
|
<span>{{ i }}</span>
|
||||||
|
</span>
|
||||||
|
</transition-group>
|
||||||
|
<a class="rk-btn r vm tc" @click="downloadTrace">{{ t("exportImage") }}</a>
|
||||||
|
<div class="trace-list">
|
||||||
|
<div ref="traceList"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, watch, onBeforeUnmount, onMounted } from "vue";
|
||||||
|
import type { PropType } from "vue";
|
||||||
|
import _ from "lodash";
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
|
import * as d3 from "d3";
|
||||||
|
import ListGraph from "./utils/d3-trace";
|
||||||
|
import copy from "@/utils/copy";
|
||||||
|
import { Span, Trace } from "@/types/trace";
|
||||||
|
import { Option } from "@/types/app";
|
||||||
|
/* global defineProps, Nullable*/
|
||||||
|
const props = defineProps({
|
||||||
|
data: { type: Array as PropType<Span[]>, default: () => [] },
|
||||||
|
traceId: { type: String, default: "" },
|
||||||
|
});
|
||||||
|
const { t } = useI18n();
|
||||||
|
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 list = ref<any>([]);
|
||||||
|
const tree = ref<any>(null);
|
||||||
|
const traceList = ref<Nullable<HTMLDivElement>>(null);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
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
|
||||||
|
.scaleSequential()
|
||||||
|
.domain([0, list.value.length + 1])
|
||||||
|
.interpolator(d3.interpolateCool);
|
||||||
|
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() {
|
||||||
|
const serializer = new XMLSerializer();
|
||||||
|
const svgNode: any = d3.select(".trace-list-dowanload").node();
|
||||||
|
const source = `<?xml version="1.0" standalone="no"?>\r\n${serializer.serializeToString(
|
||||||
|
svgNode
|
||||||
|
)}`;
|
||||||
|
const canvas = document.createElement("canvas");
|
||||||
|
const context: any = canvas.getContext("2d");
|
||||||
|
canvas.width = (
|
||||||
|
d3.select(".trace-list-dowanload") as any
|
||||||
|
)._groups[0][0].clientWidth;
|
||||||
|
canvas.height = (
|
||||||
|
d3.select(".trace-list-dowanload") as any
|
||||||
|
)._groups[0][0].clientHeight;
|
||||||
|
context.fillStyle = "#fff";
|
||||||
|
context.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
const image = new Image();
|
||||||
|
image.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(source)}`;
|
||||||
|
image.onload = () => {
|
||||||
|
context.drawImage(image, 0, 0);
|
||||||
|
const tagA = document.createElement("a");
|
||||||
|
tagA.download = "trace-list.png";
|
||||||
|
tagA.href = canvas.toDataURL("image/png");
|
||||||
|
tagA.click();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function changeTree() {
|
||||||
|
if (props.data.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
list.value = Array.from(new Set(props.data.map((i: any) => 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>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.time-charts {
|
||||||
|
overflow: auto;
|
||||||
|
padding: 10px 30px;
|
||||||
|
position: relative;
|
||||||
|
min-height: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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 {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
</style>
|
313
src/views/dashboard/related/trace/components/utils/d3-trace.ts
Normal file
313
src/views/dashboard/related/trace/components/utils/d3-trace.ts
Normal file
@ -0,0 +1,313 @@
|
|||||||
|
/**</template>
|
||||||
|
* 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 { Span, Trace } from "@/types/trace";
|
||||||
|
|
||||||
|
const type = {
|
||||||
|
MQ: "#bf99f8",
|
||||||
|
Http: "#72a5fd",
|
||||||
|
Database: "#ff6732",
|
||||||
|
Unknown: "#ffc107",
|
||||||
|
Cache: "#00bcd4",
|
||||||
|
RPCFramework: "#ee4395",
|
||||||
|
};
|
||||||
|
export default class ListGraph {
|
||||||
|
private barHeight = 48;
|
||||||
|
private handleSelectSpan: Nullable<(i: Trace) => void> = null;
|
||||||
|
private el: Nullable<HTMLDivElement> = null;
|
||||||
|
private i = 0;
|
||||||
|
private width = 0;
|
||||||
|
private height = 0;
|
||||||
|
private svg: any = null;
|
||||||
|
private treemap: any = null;
|
||||||
|
private tip: any = null;
|
||||||
|
private row: any[] = [];
|
||||||
|
private data: any = [];
|
||||||
|
private min = 0;
|
||||||
|
private max = 0;
|
||||||
|
private list: any[] = [];
|
||||||
|
private xScale: any = null;
|
||||||
|
private xAxis: any = null;
|
||||||
|
private sequentialScale: any = null;
|
||||||
|
private root: any = null;
|
||||||
|
constructor(el: HTMLDivElement, handleSelectSpan: (i: Trace) => void) {
|
||||||
|
this.handleSelectSpan = handleSelectSpan;
|
||||||
|
this.el = el;
|
||||||
|
this.width = el.clientWidth;
|
||||||
|
this.height = el.clientHeight;
|
||||||
|
this.svg = d3
|
||||||
|
.select(this.el)
|
||||||
|
.append("svg")
|
||||||
|
.attr("class", "trace-list-dowanload")
|
||||||
|
.attr("width", this.width)
|
||||||
|
.attr("height", this.height);
|
||||||
|
this.treemap = d3.tree().size([this.height * 0.7, this.width]);
|
||||||
|
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.call(this.tip);
|
||||||
|
}
|
||||||
|
diagonal(d: any) {
|
||||||
|
return `M ${d.source.y} ${d.source.x + 5}
|
||||||
|
L ${d.source.y} ${d.target.x - 30}
|
||||||
|
L${d.target.y} ${d.target.x - 20}
|
||||||
|
L${d.target.y} ${d.target.x - 5}`;
|
||||||
|
}
|
||||||
|
init(data: any, row: any[], fixSpansSize: number) {
|
||||||
|
d3.select(".trace-xaxis").remove();
|
||||||
|
this.row = row;
|
||||||
|
this.data = data;
|
||||||
|
this.min = d3.min(this.row.map((i) => i.startTime));
|
||||||
|
this.max = d3.max(this.row.map((i) => i.endTime - this.min)) || 0;
|
||||||
|
this.list = Array.from(new Set(this.row.map((i) => i.serviceCode)));
|
||||||
|
this.xScale = d3
|
||||||
|
.scaleLinear()
|
||||||
|
.range([0, this.width * 0.387])
|
||||||
|
.domain([0, this.max]);
|
||||||
|
this.xAxis = d3.axisTop(this.xScale).tickFormat((d: any) => {
|
||||||
|
if (d === 0) return 0;
|
||||||
|
if (d >= 1000) return d / 1000 + "s";
|
||||||
|
return d;
|
||||||
|
});
|
||||||
|
this.svg.attr(
|
||||||
|
"height",
|
||||||
|
(this.row.length + fixSpansSize + 1) * this.barHeight
|
||||||
|
);
|
||||||
|
this.svg
|
||||||
|
.append("g")
|
||||||
|
.attr("class", "trace-xaxis")
|
||||||
|
|
||||||
|
.attr("transform", `translate(${this.width * 0.618 - 20},${30})`)
|
||||||
|
.call(this.xAxis);
|
||||||
|
this.sequentialScale = d3
|
||||||
|
.scaleSequential()
|
||||||
|
.domain([0, this.list.length + 1])
|
||||||
|
.interpolator(d3.interpolateCool);
|
||||||
|
this.root = d3.hierarchy(this.data, (d) => d.children);
|
||||||
|
this.root.x0 = 0;
|
||||||
|
this.root.y0 = 0;
|
||||||
|
}
|
||||||
|
draw(callback: any) {
|
||||||
|
this.update(this.root, callback);
|
||||||
|
}
|
||||||
|
click(d: any, scope: any) {
|
||||||
|
if (!d.data.type) return;
|
||||||
|
if (d.children) {
|
||||||
|
d._children = d.children;
|
||||||
|
d.children = null;
|
||||||
|
} else {
|
||||||
|
d.children = d._children;
|
||||||
|
d._children = null;
|
||||||
|
}
|
||||||
|
scope.update(d);
|
||||||
|
}
|
||||||
|
update(source: any, callback: any) {
|
||||||
|
const nodes = this.root.descendants();
|
||||||
|
let index = -1;
|
||||||
|
this.root.eachBefore((n: any) => {
|
||||||
|
n.x = ++index * this.barHeight + 24;
|
||||||
|
n.y = n.depth * 12;
|
||||||
|
});
|
||||||
|
const node = this.svg
|
||||||
|
.selectAll(".trace-node")
|
||||||
|
.data(nodes, (d: any) => d.id || (d.id = ++this.i));
|
||||||
|
const nodeEnter = node
|
||||||
|
.enter()
|
||||||
|
.append("g")
|
||||||
|
.attr("transform", `translate(${source.y0},${source.x0})`)
|
||||||
|
.attr("class", "trace-node")
|
||||||
|
.style("opacity", 0)
|
||||||
|
.on("mouseover", (d: Trace) => {
|
||||||
|
this.tip.show(d, this);
|
||||||
|
})
|
||||||
|
.on("mouseout", (d: Trace) => {
|
||||||
|
this.tip.hide(d, this);
|
||||||
|
})
|
||||||
|
.on("click", (d: Trace) => {
|
||||||
|
if (this.handleSelectSpan) {
|
||||||
|
this.handleSelectSpan(d);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
nodeEnter
|
||||||
|
.append("rect")
|
||||||
|
.attr("height", 42)
|
||||||
|
.attr("ry", 2)
|
||||||
|
.attr("rx", 2)
|
||||||
|
.attr("y", -22)
|
||||||
|
.attr("x", 20)
|
||||||
|
.attr("width", "100%")
|
||||||
|
.attr("fill", "rgba(0,0,0,0)");
|
||||||
|
nodeEnter
|
||||||
|
.append("text")
|
||||||
|
.attr("x", 13)
|
||||||
|
.attr("y", 5)
|
||||||
|
.attr("fill", "#E54C17")
|
||||||
|
.html((d: any) => (d.data.isError ? "◉" : ""));
|
||||||
|
nodeEnter
|
||||||
|
.append("text")
|
||||||
|
.attr("class", "node-text")
|
||||||
|
.attr("x", 35)
|
||||||
|
.attr("y", -6)
|
||||||
|
.attr("fill", "#333")
|
||||||
|
.text((d: any) => {
|
||||||
|
if (d.data.label === "TRACE_ROOT") {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return d.data.label.length > 40
|
||||||
|
? `${d.data.label.slice(0, 40)}...`
|
||||||
|
: `${d.data.label}`;
|
||||||
|
});
|
||||||
|
nodeEnter
|
||||||
|
.append("text")
|
||||||
|
.attr("class", "node-text")
|
||||||
|
.attr("x", 35)
|
||||||
|
.attr("y", 12)
|
||||||
|
.attr("fill", "#ccc")
|
||||||
|
.style("font-size", "11px")
|
||||||
|
.text(
|
||||||
|
(d: any) =>
|
||||||
|
`${d.data.layer || ""} ${
|
||||||
|
d.data.component ? "- " + d.data.component : d.data.component || ""
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
nodeEnter
|
||||||
|
.append("rect")
|
||||||
|
.attr("rx", 2)
|
||||||
|
.attr("ry", 2)
|
||||||
|
.attr("height", 4)
|
||||||
|
.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) =>
|
||||||
|
!d.data.endTime || !d.data.startTime
|
||||||
|
? 0
|
||||||
|
: this.width * 0.618 -
|
||||||
|
20 -
|
||||||
|
d.y +
|
||||||
|
this.xScale(d.data.startTime - this.min) || 0
|
||||||
|
)
|
||||||
|
.attr("y", -2)
|
||||||
|
.style(
|
||||||
|
"fill",
|
||||||
|
(d: any) =>
|
||||||
|
`${this.sequentialScale(this.list.indexOf(d.data.serviceCode))}`
|
||||||
|
);
|
||||||
|
nodeEnter
|
||||||
|
.transition()
|
||||||
|
.duration(400)
|
||||||
|
.attr("transform", (d: any) => `translate(${d.y},${d.x})`)
|
||||||
|
.style("opacity", 1);
|
||||||
|
nodeEnter
|
||||||
|
.append("circle")
|
||||||
|
.attr("r", 3)
|
||||||
|
.style("cursor", "pointer")
|
||||||
|
.attr("stroke-width", 2.5)
|
||||||
|
.attr("fill", (d: any) =>
|
||||||
|
d._children
|
||||||
|
? `${this.sequentialScale(this.list.indexOf(d.data.serviceCode))}`
|
||||||
|
: "rbga(0,0,0,0)"
|
||||||
|
)
|
||||||
|
.style("stroke", (d: any) =>
|
||||||
|
d.data.label === "TRACE_ROOT"
|
||||||
|
? ""
|
||||||
|
: `${this.sequentialScale(this.list.indexOf(d.data.serviceCode))}`
|
||||||
|
)
|
||||||
|
.on("click", (d: any) => {
|
||||||
|
this.click(d, this);
|
||||||
|
(d3 as any).event.stopPropagation();
|
||||||
|
});
|
||||||
|
node
|
||||||
|
.transition()
|
||||||
|
.duration(400)
|
||||||
|
.attr("transform", (d: any) => `translate(${d.y},${d.x})`)
|
||||||
|
.style("opacity", 1)
|
||||||
|
.select("circle")
|
||||||
|
.attr("fill", (d: any) =>
|
||||||
|
d._children
|
||||||
|
? `${this.sequentialScale(this.list.indexOf(d.data.serviceCode))}`
|
||||||
|
: ""
|
||||||
|
);
|
||||||
|
|
||||||
|
// Transition exiting nodes to the parent's new position.
|
||||||
|
node
|
||||||
|
.exit()
|
||||||
|
.transition()
|
||||||
|
.duration(400)
|
||||||
|
.attr("transform", `translate(${source.y},${source.x})`)
|
||||||
|
.style("opacity", 0)
|
||||||
|
.remove();
|
||||||
|
const link = this.svg
|
||||||
|
.selectAll(".trace-link")
|
||||||
|
.data(this.root.links(), function (d: any) {
|
||||||
|
return d.target.id;
|
||||||
|
});
|
||||||
|
|
||||||
|
link
|
||||||
|
.enter()
|
||||||
|
.insert("path", "g")
|
||||||
|
.attr("class", "trace-link")
|
||||||
|
.attr("fill", "rgba(0,0,0,0)")
|
||||||
|
.attr("stroke", "rgba(0, 0, 0, 0.1)")
|
||||||
|
.attr("stroke-width", 2)
|
||||||
|
.attr("d", () => {
|
||||||
|
const o = { x: source.x0 + 35, y: source.y0 };
|
||||||
|
return this.diagonal({ source: o, target: o });
|
||||||
|
})
|
||||||
|
.transition()
|
||||||
|
.duration(400)
|
||||||
|
.attr("d", this.diagonal);
|
||||||
|
|
||||||
|
link.transition().duration(400).attr("d", this.diagonal);
|
||||||
|
|
||||||
|
link
|
||||||
|
.exit()
|
||||||
|
.transition()
|
||||||
|
.duration(400)
|
||||||
|
.attr("d", () => {
|
||||||
|
const o = { x: source.x + 35, y: source.y };
|
||||||
|
return this.diagonal({ source: o, target: o });
|
||||||
|
})
|
||||||
|
.remove();
|
||||||
|
this.root.each(function (d: any) {
|
||||||
|
d.x0 = d.x;
|
||||||
|
d.y0 = d.y;
|
||||||
|
});
|
||||||
|
if (callback) {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user