mirror of
https://github.com/apache/skywalking-booster-ui.git
synced 2025-05-07 02:22:55 +00:00
407 lines
12 KiB
Vue
407 lines
12 KiB
Vue
<!-- 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" class="d3-graph"></div>
|
|
<el-dialog v-model="showDetail" :destroy-on-close="true" fullscreen @closed="showDetail = false">
|
|
<SpanDetail :currentSpan="currentSpan" />
|
|
</el-dialog>
|
|
</template>
|
|
<script lang="ts" setup>
|
|
import { ref, watch, onBeforeUnmount, onMounted } from "vue";
|
|
import type { PropType } from "vue";
|
|
import * as d3 from "d3";
|
|
import ListGraph from "../../utils/d3-trace-list";
|
|
import TreeGraph from "../../utils/d3-trace-tree";
|
|
import type { Span, Ref } from "@/types/trace";
|
|
import SpanDetail from "./SpanDetail.vue";
|
|
import { useAppStoreWithOut } from "@/store/modules/app";
|
|
import { debounce } from "@/utils/debounce";
|
|
import { mutationObserver } from "@/utils/mutation";
|
|
|
|
/* global defineProps, Nullable, defineExpose, Recordable */
|
|
const props = defineProps({
|
|
data: { type: Array as PropType<Span[]>, default: () => [] },
|
|
traceId: { type: String, default: "" },
|
|
type: { type: String, default: "List" },
|
|
});
|
|
const appStore = useAppStoreWithOut();
|
|
const loading = ref<boolean>(false);
|
|
const showDetail = ref<boolean>(false);
|
|
const fixSpansSize = ref<number>(0);
|
|
const segmentId = ref<Recordable[]>([]);
|
|
const currentSpan = ref<Array<Span>>([]);
|
|
const refSpans = ref<Array<Ref>>([]);
|
|
const tree = ref<Nullable<any>>(null);
|
|
const traceGraph = ref<Nullable<HTMLDivElement>>(null);
|
|
const debounceFunc = debounce(draw, 500);
|
|
|
|
defineExpose({
|
|
tree,
|
|
});
|
|
onMounted(() => {
|
|
loading.value = true;
|
|
changeTree();
|
|
if (!traceGraph.value) {
|
|
loading.value = false;
|
|
return;
|
|
}
|
|
draw();
|
|
loading.value = false;
|
|
|
|
// monitor segment list width changes.
|
|
mutationObserver.create("trigger-resize", () => {
|
|
d3.selectAll(".d3-tip").remove();
|
|
debounceFunc();
|
|
});
|
|
|
|
window.addEventListener("resize", debounceFunc);
|
|
});
|
|
|
|
function draw() {
|
|
if (!traceGraph.value) {
|
|
return;
|
|
}
|
|
d3.selectAll(".d3-tip").remove();
|
|
if (props.type === "List") {
|
|
tree.value = new ListGraph(traceGraph.value, handleSelectSpan);
|
|
tree.value.init(
|
|
{ label: "TRACE_ROOT", children: segmentId.value },
|
|
getRefsAllNodes({ label: "TRACE_ROOT", children: segmentId.value }),
|
|
fixSpansSize.value,
|
|
);
|
|
tree.value.draw();
|
|
} else {
|
|
tree.value = new TreeGraph(traceGraph.value, handleSelectSpan);
|
|
tree.value.init(
|
|
{ label: `${props.traceId}`, children: segmentId.value },
|
|
getRefsAllNodes({ label: "TRACE_ROOT", children: segmentId.value }),
|
|
);
|
|
}
|
|
}
|
|
function handleSelectSpan(i: Recordable) {
|
|
currentSpan.value = i.data;
|
|
showDetail.value = true;
|
|
}
|
|
function traverseTree(node: Recordable, spanId: string, segmentId: string, data: Recordable) {
|
|
if (!node || node.isBroken) {
|
|
return;
|
|
}
|
|
if (node.spanId === spanId && node.segmentId === segmentId) {
|
|
node.children.push(data);
|
|
return;
|
|
}
|
|
for (const nodeItem of node.children || []) {
|
|
traverseTree(nodeItem, spanId, segmentId, data);
|
|
}
|
|
}
|
|
function changeTree() {
|
|
if (props.data.length === 0) {
|
|
return [];
|
|
}
|
|
segmentId.value = [];
|
|
const segmentGroup: Recordable = {};
|
|
const segmentIdGroup: string[] = [];
|
|
const fixSpans: Span[] = [];
|
|
const segmentHeaders: Span[] = [];
|
|
for (const span of props.data) {
|
|
if (span.refs.length) {
|
|
refSpans.value.push(...span.refs);
|
|
}
|
|
if (span.parentSpanId === -1) {
|
|
segmentHeaders.push(span);
|
|
} else {
|
|
const item = props.data.find(
|
|
(i: Span) => i.traceId === span.traceId && i.segmentId === span.segmentId && i.spanId === span.spanId - 1,
|
|
);
|
|
const content = fixSpans.find(
|
|
(i: Span) =>
|
|
i.traceId === span.traceId &&
|
|
i.segmentId === span.segmentId &&
|
|
i.spanId === span.spanId - 1 &&
|
|
i.parentSpanId === span.spanId - 2,
|
|
);
|
|
if (!item && !content) {
|
|
fixSpans.push({
|
|
traceId: span.traceId,
|
|
segmentId: span.segmentId,
|
|
spanId: span.spanId - 1,
|
|
parentSpanId: span.spanId - 2,
|
|
refs: [],
|
|
endpointName: `VNode: ${span.segmentId}`,
|
|
serviceCode: "VirtualNode",
|
|
type: `[Broken] ${span.type}`,
|
|
peer: "",
|
|
component: `VirtualNode: #${span.spanId - 1}`,
|
|
isError: true,
|
|
isBroken: true,
|
|
layer: "Broken",
|
|
tags: [],
|
|
logs: [],
|
|
startTime: 0,
|
|
endTime: 0,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
for (const span of segmentHeaders) {
|
|
if (span.refs.length) {
|
|
let exit = null;
|
|
for (const ref of span.refs) {
|
|
const e = props.data.find(
|
|
(i: Recordable) =>
|
|
ref.traceId === i.traceId && ref.parentSegmentId === i.segmentId && ref.parentSpanId === i.spanId,
|
|
);
|
|
if (e) {
|
|
exit = e;
|
|
}
|
|
}
|
|
if (!exit) {
|
|
const ref = span.refs[0];
|
|
// create a known broken node.
|
|
const parentSpanId = ref.parentSpanId > -1 ? 0 : -1;
|
|
const content = fixSpans.find(
|
|
(i: Span) =>
|
|
i.traceId === ref.traceId &&
|
|
i.segmentId === ref.parentSegmentId &&
|
|
i.spanId === ref.parentSpanId &&
|
|
i.parentSpanId === parentSpanId,
|
|
);
|
|
if (!content) {
|
|
fixSpans.push({
|
|
traceId: ref.traceId,
|
|
segmentId: ref.parentSegmentId,
|
|
spanId: ref.parentSpanId,
|
|
parentSpanId,
|
|
refs: [],
|
|
endpointName: `VNode: ${ref.parentSegmentId}`,
|
|
serviceCode: "VirtualNode",
|
|
type: `[Broken] ${ref.type}`,
|
|
peer: "",
|
|
component: `VirtualNode: #${ref.parentSpanId}`,
|
|
isError: true,
|
|
isBroken: true,
|
|
layer: "Broken",
|
|
tags: [],
|
|
logs: [],
|
|
startTime: 0,
|
|
endTime: 0,
|
|
});
|
|
}
|
|
// if root broken node is not exist, create a root broken node.
|
|
if (parentSpanId > -1) {
|
|
const content = fixSpans.find(
|
|
(i: Span) =>
|
|
i.traceId === ref.traceId &&
|
|
i.segmentId === ref.parentSegmentId &&
|
|
i.spanId === 0 &&
|
|
i.parentSpanId === -1,
|
|
);
|
|
if (!content) {
|
|
fixSpans.push({
|
|
traceId: ref.traceId,
|
|
segmentId: ref.parentSegmentId,
|
|
spanId: 0,
|
|
parentSpanId: -1,
|
|
refs: [],
|
|
endpointName: `VNode: ${ref.parentSegmentId}`,
|
|
serviceCode: "VirtualNode",
|
|
type: `[Broken] ${ref.type}`,
|
|
peer: "",
|
|
component: `VirtualNode: #0`,
|
|
isError: true,
|
|
isBroken: true,
|
|
layer: "Broken",
|
|
tags: [],
|
|
logs: [],
|
|
startTime: 0,
|
|
endTime: 0,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
for (const i of [...fixSpans, ...props.data]) {
|
|
i.label = i.endpointName || "no operation name";
|
|
i.children = [];
|
|
if (segmentGroup[i.segmentId]) {
|
|
segmentGroup[i.segmentId].push(i);
|
|
} else {
|
|
segmentIdGroup.push(i.segmentId);
|
|
segmentGroup[i.segmentId] = [i];
|
|
}
|
|
}
|
|
fixSpansSize.value = fixSpans.length;
|
|
for (const id of segmentIdGroup) {
|
|
const currentSegment = segmentGroup[id].sort((a: Span, b: Span) => b.parentSpanId - a.parentSpanId);
|
|
for (const s of currentSegment) {
|
|
const index = currentSegment.findIndex((i: Span) => i.spanId === s.parentSpanId);
|
|
if (index > -1) {
|
|
if (
|
|
(currentSegment[index].isBroken && currentSegment[index].parentSpanId === -1) ||
|
|
!currentSegment[index].isBroken
|
|
) {
|
|
currentSegment[index].children.push(s);
|
|
currentSegment[index].children.sort((a: Span, b: Span) => a.spanId - b.spanId);
|
|
}
|
|
}
|
|
if (s.isBroken) {
|
|
const children = props.data.filter((span: Span) =>
|
|
span.refs.find(
|
|
(d) => d.traceId === s.traceId && d.parentSegmentId === s.segmentId && d.parentSpanId === s.spanId,
|
|
),
|
|
);
|
|
if (children.length) {
|
|
s.children.push(...children);
|
|
}
|
|
}
|
|
}
|
|
segmentGroup[id] = currentSegment[currentSegment.length - 1];
|
|
}
|
|
for (const id of segmentIdGroup) {
|
|
for (const ref of segmentGroup[id].refs) {
|
|
if (ref.traceId === props.traceId) {
|
|
traverseTree(segmentGroup[ref.parentSegmentId], ref.parentSpanId, ref.parentSegmentId, segmentGroup[id]);
|
|
}
|
|
}
|
|
}
|
|
for (const i in segmentGroup) {
|
|
for (const ref of segmentGroup[i].refs) {
|
|
if (!segmentGroup[ref.parentSegmentId]) {
|
|
segmentId.value.push(segmentGroup[i]);
|
|
}
|
|
}
|
|
if (!segmentGroup[i].refs.length && segmentGroup[i].parentSpanId === -1) {
|
|
segmentId.value.push(segmentGroup[i]);
|
|
}
|
|
}
|
|
for (const i of segmentId.value) {
|
|
collapse(i);
|
|
}
|
|
}
|
|
function collapse(d: Span | Recordable) {
|
|
if (d.children) {
|
|
const item = refSpans.value.find((s: Ref) => s.parentSpanId === d.spanId && s.parentSegmentId === d.segmentId);
|
|
let dur = d.endTime - d.startTime;
|
|
for (const i of d.children) {
|
|
dur -= i.endTime - i.startTime;
|
|
}
|
|
d.dur = dur < 0 ? 0 : dur;
|
|
if (item) {
|
|
d.children = d.children.sort(compare("startTime"));
|
|
}
|
|
for (const i of d.children) {
|
|
collapse(i);
|
|
}
|
|
}
|
|
}
|
|
function getRefsAllNodes(tree: Recordable) {
|
|
let nodes = [];
|
|
let stack = [tree];
|
|
|
|
while (stack.length > 0) {
|
|
const node = stack.pop();
|
|
nodes.push(node);
|
|
|
|
if (node?.children && node.children.length > 0) {
|
|
for (let i = node.children.length - 1; i >= 0; i--) {
|
|
stack.push(node.children[i]);
|
|
}
|
|
}
|
|
}
|
|
|
|
return nodes;
|
|
}
|
|
function compare(p: string) {
|
|
return (m: Recordable, n: Recordable) => {
|
|
const a = m[p];
|
|
const b = n[p];
|
|
return a - b;
|
|
};
|
|
}
|
|
onBeforeUnmount(() => {
|
|
d3.selectAll(".d3-tip").remove();
|
|
window.removeEventListener("resize", debounceFunc);
|
|
mutationObserver.deleteObserve("trigger-resize");
|
|
});
|
|
watch(
|
|
() => props.data,
|
|
() => {
|
|
if (!props.data.length) {
|
|
return;
|
|
}
|
|
loading.value = true;
|
|
changeTree();
|
|
draw();
|
|
loading.value = false;
|
|
},
|
|
);
|
|
watch(
|
|
() => appStore.theme,
|
|
() => {
|
|
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>
|
|
.d3-graph {
|
|
height: 100%;
|
|
}
|
|
|
|
.trace-node .group {
|
|
cursor: pointer;
|
|
fill-opacity: 0;
|
|
}
|
|
|
|
.trace-node-container {
|
|
fill: rgb(0 0 0 / 0%);
|
|
stroke-width: 5px;
|
|
cursor: pointer;
|
|
|
|
&:hover {
|
|
fill: rgb(0 0 0 / 5%);
|
|
}
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
.dialog-c-text {
|
|
white-space: pre;
|
|
overflow: auto;
|
|
font-family: monospace;
|
|
}
|
|
</style>
|