mirror of
https://github.com/apache/skywalking-booster-ui.git
synced 2025-05-02 01:54:20 +00:00
feat: enhance trace list graph (#459)
This commit is contained in:
parent
0ea8335fee
commit
39b4626317
9
src/types/trace.d.ts
vendored
9
src/types/trace.d.ts
vendored
@ -50,7 +50,7 @@ export interface Span {
|
||||
refs?: Ref[];
|
||||
}
|
||||
export type Ref = {
|
||||
type: string;
|
||||
type?: string;
|
||||
parentSegmentId: string;
|
||||
parentSpanId: number;
|
||||
traceId: string;
|
||||
@ -60,13 +60,6 @@ export interface log {
|
||||
data: Map<string, string>;
|
||||
}
|
||||
|
||||
export interface Ref {
|
||||
traceId: string;
|
||||
parentSegmentId: string;
|
||||
parentSpanId: number;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface StatisticsSpan {
|
||||
groupRef: StatisticsGroupRef;
|
||||
maxTime: number;
|
||||
|
@ -77,6 +77,7 @@ limitations under the License. -->
|
||||
border-bottom: 1px solid $border-color-primary;
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
min-height: 350px;
|
||||
}
|
||||
|
||||
.log-header {
|
||||
|
@ -15,7 +15,24 @@ limitations under the License. -->
|
||||
<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">
|
||||
<div id="trace-action-box">
|
||||
<div @click="showDetail = true">Span details</div>
|
||||
<div v-for="span in parentSpans" :key="span.segmentId" @click="viewParentSpan(span)">
|
||||
{{ `Parent span: ${span.endpointName} -> Start time: ${visDate(span.startTime)}` }}
|
||||
</div>
|
||||
<div v-for="span in refParentSpans" :key="span.segmentId" @click="viewParentSpan(span)">
|
||||
{{ `Ref to span: ${span.endpointName} -> Start time: ${visDate(span.startTime)}` }}
|
||||
</div>
|
||||
</div>
|
||||
<el-dialog
|
||||
v-model="showDetail"
|
||||
width="60%"
|
||||
center
|
||||
align-center
|
||||
:destroy-on-close="true"
|
||||
@closed="showDetail = false"
|
||||
v-if="currentSpan?.segmentId"
|
||||
>
|
||||
<SpanDetail :currentSpan="currentSpan" />
|
||||
</el-dialog>
|
||||
</template>
|
||||
@ -23,6 +40,7 @@ limitations under the License. -->
|
||||
import { ref, watch, onBeforeUnmount, onMounted } from "vue";
|
||||
import type { PropType } from "vue";
|
||||
import * as d3 from "d3";
|
||||
import dayjs from "dayjs";
|
||||
import ListGraph from "../../utils/d3-trace-list";
|
||||
import TreeGraph from "../../utils/d3-trace-tree";
|
||||
import type { Span, Ref } from "@/types/trace";
|
||||
@ -42,11 +60,14 @@ limitations under the License. -->
|
||||
const showDetail = ref<boolean>(false);
|
||||
const fixSpansSize = ref<number>(0);
|
||||
const segmentId = ref<Recordable[]>([]);
|
||||
const currentSpan = ref<Array<Span>>([]);
|
||||
const currentSpan = ref<Nullable<Span>>(null);
|
||||
const refSpans = ref<Array<Ref>>([]);
|
||||
const tree = ref<Nullable<any>>(null);
|
||||
const traceGraph = ref<Nullable<HTMLDivElement>>(null);
|
||||
const parentSpans = ref<Array<Span>>([]);
|
||||
const refParentSpans = ref<Array<Span>>([]);
|
||||
const debounceFunc = debounce(draw, 500);
|
||||
const visDate = (date: number, pattern = "YYYY-MM-DD HH:mm:ss:SSS") => dayjs(date).format(pattern);
|
||||
|
||||
defineExpose({
|
||||
tree,
|
||||
@ -77,16 +98,54 @@ limitations under the License. -->
|
||||
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 }, props.data, fixSpansSize.value);
|
||||
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 }, props.data);
|
||||
tree.value.init(
|
||||
{ label: `${props.traceId}`, children: segmentId.value },
|
||||
getRefsAllNodes({ label: "TRACE_ROOT", children: segmentId.value }),
|
||||
);
|
||||
}
|
||||
}
|
||||
function handleSelectSpan(i: Recordable) {
|
||||
const spans = [];
|
||||
const refSpans = [];
|
||||
parentSpans.value = [];
|
||||
refParentSpans.value = [];
|
||||
currentSpan.value = i.data;
|
||||
showDetail.value = true;
|
||||
if (!currentSpan.value) {
|
||||
return;
|
||||
}
|
||||
for (const ref of currentSpan.value.refs || []) {
|
||||
refSpans.push(ref);
|
||||
}
|
||||
if (currentSpan.value.parentSpanId > -1) {
|
||||
spans.push({
|
||||
parentSegmentId: currentSpan.value.segmentId,
|
||||
parentSpanId: currentSpan.value.parentSpanId,
|
||||
traceId: currentSpan.value.traceId,
|
||||
});
|
||||
}
|
||||
for (const span of refSpans) {
|
||||
const item = props.data.find(
|
||||
(d) => d.segmentId === span.parentSegmentId && d.spanId === span.parentSpanId && d.traceId === span.traceId,
|
||||
);
|
||||
item && refParentSpans.value.push(item);
|
||||
}
|
||||
for (const span of spans) {
|
||||
const item = props.data.find(
|
||||
(d) => d.segmentId === span.parentSegmentId && d.spanId === span.parentSpanId && d.traceId === span.traceId,
|
||||
);
|
||||
item && parentSpans.value.push(item);
|
||||
}
|
||||
}
|
||||
function viewParentSpan(span: Recordable) {
|
||||
tree.value.highlightParents(span);
|
||||
}
|
||||
function traverseTree(node: Recordable, spanId: string, segmentId: string, data: Recordable) {
|
||||
if (!node || node.isBroken) {
|
||||
@ -272,21 +331,12 @@ limitations under the License. -->
|
||||
}
|
||||
}
|
||||
for (const i in segmentGroup) {
|
||||
if (segmentGroup[i].refs.length) {
|
||||
let exit = null;
|
||||
for (const ref of segmentGroup[i].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) {
|
||||
for (const ref of segmentGroup[i].refs) {
|
||||
if (!segmentGroup[ref.parentSegmentId]) {
|
||||
segmentId.value.push(segmentGroup[i]);
|
||||
}
|
||||
} else {
|
||||
}
|
||||
if (!segmentGroup[i].refs.length && segmentGroup[i].parentSpanId === -1) {
|
||||
segmentId.value.push(segmentGroup[i]);
|
||||
}
|
||||
}
|
||||
@ -310,6 +360,23 @@ limitations under the License. -->
|
||||
}
|
||||
}
|
||||
}
|
||||
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];
|
||||
@ -346,7 +413,7 @@ limitations under the License. -->
|
||||
},
|
||||
);
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
<style lang="scss">
|
||||
.d3-graph {
|
||||
height: 100%;
|
||||
}
|
||||
@ -356,36 +423,41 @@ limitations under the License. -->
|
||||
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;
|
||||
font: 12px sans-serif;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.domain {
|
||||
.trace-node.highlighted .node-text {
|
||||
font-weight: bold;
|
||||
fill: #409eff;
|
||||
}
|
||||
|
||||
.trace-node.highlightedParent .node-text {
|
||||
font-weight: bold;
|
||||
fill: #409eff;
|
||||
}
|
||||
|
||||
#trace-action-box {
|
||||
position: absolute;
|
||||
color: $font-color;
|
||||
cursor: pointer;
|
||||
border: var(--sw-topology-border);
|
||||
border-radius: 3px;
|
||||
background-color: $theme-background;
|
||||
padding: 10px 0;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.time-charts-item {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border: 1px solid;
|
||||
font-size: 11px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
div {
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
text-align: left;
|
||||
padding: 0 15px;
|
||||
}
|
||||
|
||||
.dialog-c-text {
|
||||
white-space: pre;
|
||||
overflow: auto;
|
||||
font-family: monospace;
|
||||
div:hover {
|
||||
color: $active-color;
|
||||
background-color: $popper-hover-bg-color;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -87,7 +87,14 @@ limitations under the License. -->
|
||||
{{ t("relatedTraceLogs") }}
|
||||
</el-button>
|
||||
</div>
|
||||
<el-dialog v-model="showEventDetail" :destroy-on-close="true" fullscreen @closed="showEventDetail = false">
|
||||
<el-dialog
|
||||
v-model="showEventDetail"
|
||||
width="60%"
|
||||
center
|
||||
align-center
|
||||
:destroy-on-close="true"
|
||||
@closed="showEventDetail = false"
|
||||
>
|
||||
<div>
|
||||
<div class="mb-10">
|
||||
<span class="grey title">Name:</span>
|
||||
@ -115,7 +122,14 @@ limitations under the License. -->
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
<el-dialog v-model="showRelatedLogs" :destroy-on-close="true" fullscreen @closed="showRelatedLogs = false">
|
||||
<el-dialog
|
||||
v-model="showRelatedLogs"
|
||||
width="60%"
|
||||
center
|
||||
align-center
|
||||
:destroy-on-close="true"
|
||||
@closed="showRelatedLogs = false"
|
||||
>
|
||||
<el-pagination
|
||||
v-model="pageNum"
|
||||
:page-size="pageSize"
|
||||
@ -295,4 +309,10 @@ limitations under the License. -->
|
||||
.link {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.log-tips {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
margin: 50px 0;
|
||||
}
|
||||
</style>
|
||||
|
@ -31,8 +31,10 @@ limitations under the License. -->
|
||||
import type { PropType } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import * as d3 from "d3";
|
||||
import { useAppStoreWithOut } from "@/store/modules/app";
|
||||
import type { Span } from "@/types/trace";
|
||||
import Graph from "./D3Graph/Index.vue";
|
||||
import { Themes } from "@/constants/data";
|
||||
|
||||
/* global defineProps, Recordable*/
|
||||
const props = defineProps({
|
||||
@ -40,6 +42,7 @@ limitations under the License. -->
|
||||
traceId: { type: String, default: "" },
|
||||
});
|
||||
const { t } = useI18n();
|
||||
const appStore = useAppStoreWithOut();
|
||||
const list = computed(() => Array.from(new Set(props.data.map((i: Span) => i.serviceCode))));
|
||||
|
||||
function computedScale(i: number) {
|
||||
@ -52,13 +55,13 @@ limitations under the License. -->
|
||||
|
||||
function downloadTrace() {
|
||||
const serializer = new XMLSerializer();
|
||||
const svgNode: any = d3.select(".trace-list-dowanload").node();
|
||||
const svgNode: any = d3.select(".trace-list").node();
|
||||
const source = `<?xml version="1.0" standalone="no"?>\r\n${serializer.serializeToString(svgNode)}`;
|
||||
const canvas = document.createElement("canvas");
|
||||
const context: any = canvas.getContext("2d");
|
||||
canvas.width = (d3.select(".trace-list-dowanload") as Recordable)._groups[0][0].clientWidth;
|
||||
canvas.height = (d3.select(".trace-list-dowanload") as Recordable)._groups[0][0].clientHeight;
|
||||
context.fillStyle = "#fff";
|
||||
canvas.width = (d3.select(".trace-list") as Recordable)._groups[0][0].clientWidth;
|
||||
canvas.height = (d3.select(".trace-list") as Recordable)._groups[0][0].clientHeight;
|
||||
context.fillStyle = appStore.theme === Themes.Dark ? "#212224" : `#fff`;
|
||||
context.fillRect(0, 0, canvas.width, canvas.height);
|
||||
const image = new Image();
|
||||
image.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(source)}`;
|
||||
@ -93,6 +96,7 @@ limitations under the License. -->
|
||||
|
||||
.list {
|
||||
height: calc(100% - 150px);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.event-tag {
|
||||
|
@ -89,12 +89,6 @@ limitations under the License. -->
|
||||
);
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.dialog-c-text {
|
||||
white-space: pre;
|
||||
overflow: auto;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.trace-tips {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
|
@ -42,16 +42,17 @@ export default class ListGraph {
|
||||
private xAxis: any = null;
|
||||
private sequentialScale: any = null;
|
||||
private root: any = null;
|
||||
private selectedNode: any = null;
|
||||
constructor(el: HTMLDivElement, handleSelectSpan: (i: Trace) => void) {
|
||||
this.handleSelectSpan = handleSelectSpan;
|
||||
this.el = el;
|
||||
this.width = el.getBoundingClientRect().width - 10;
|
||||
this.height = el.getBoundingClientRect().height - 10;
|
||||
d3.select(".trace-list-dowanload").remove();
|
||||
d3.select(`.${this.el?.className} .trace-list`).remove();
|
||||
this.svg = d3
|
||||
.select(this.el)
|
||||
.append("svg")
|
||||
.attr("class", "trace-list-dowanload")
|
||||
.attr("class", "trace-list")
|
||||
.attr("width", this.width > 0 ? this.width : 10)
|
||||
.attr("height", this.height > 0 ? this.height : 10)
|
||||
.attr("transform", `translate(-5, 0)`);
|
||||
@ -85,7 +86,8 @@ export default class ListGraph {
|
||||
L${d.target.y} ${d.target.x - 5}`;
|
||||
}
|
||||
init(data: Recordable, row: Recordable[], fixSpansSize: number) {
|
||||
d3.select(".trace-xaxis").remove();
|
||||
d3.select(`.${this.el?.className} .trace-xaxis`).remove();
|
||||
d3.select("#trace-action-box").style("display", "none");
|
||||
this.row = row;
|
||||
this.data = data;
|
||||
this.min = d3.min(this.row.map((i) => i.startTime));
|
||||
@ -142,19 +144,43 @@ export default class ListGraph {
|
||||
.enter()
|
||||
.append("g")
|
||||
.attr("transform", `translate(${source.y0},${source.x0})`)
|
||||
.attr("id", (d: Recordable) => `list-node-${d.id}`)
|
||||
.attr("class", "trace-node")
|
||||
.attr("style", "cursor: pointer")
|
||||
.style("opacity", 0)
|
||||
.on("mouseover", function (event: MouseEvent, d: Trace) {
|
||||
t.tip.show(d, this);
|
||||
})
|
||||
.on("mouseout", function (event: MouseEvent, d: Trace) {
|
||||
t.tip.hide(d, this);
|
||||
})
|
||||
.on("click", (event: MouseEvent, d: Trace) => {
|
||||
if (this.handleSelectSpan) {
|
||||
this.handleSelectSpan(d);
|
||||
.on("click", function (event: MouseEvent, d: Trace & { id: string }) {
|
||||
event.stopPropagation();
|
||||
const hasClass = d3.select(this).classed("highlighted");
|
||||
if (t.selectedNode) {
|
||||
t.selectedNode.classed("highlighted", false);
|
||||
d3.select("#trace-action-box").style("display", "none");
|
||||
}
|
||||
if (hasClass) {
|
||||
t.selectedNode = null;
|
||||
return;
|
||||
}
|
||||
d3.select(this).classed("highlighted", true);
|
||||
const nodeBox = this.getBoundingClientRect();
|
||||
const svgBox = (d3.select(`.${t.el?.className} .trace-list`) as any).node().getBoundingClientRect();
|
||||
const offsetX = nodeBox.x - svgBox.x;
|
||||
const offsetY = nodeBox.y - svgBox.y;
|
||||
d3.select("#trace-action-box")
|
||||
.style("display", "block")
|
||||
.style("left", `${offsetX + 30}px`)
|
||||
.style("top", `${offsetY + 40}px`);
|
||||
t.selectedNode = d3.select(this);
|
||||
if (t.handleSelectSpan) {
|
||||
t.handleSelectSpan(d);
|
||||
}
|
||||
t.root.descendants().map((node: { id: number }) => {
|
||||
d3.select(`#list-node-${node.id}`).classed("highlightedParent", false);
|
||||
return node;
|
||||
});
|
||||
});
|
||||
nodeEnter
|
||||
.append("rect")
|
||||
@ -246,7 +272,7 @@ export default class ListGraph {
|
||||
})
|
||||
.attr("cy", -5)
|
||||
.attr("fill", "none")
|
||||
.attr("stroke", appStore.theme === Themes.Dark ? "#666" : "#e66")
|
||||
.attr("stroke", "#e66")
|
||||
.style("opacity", (d: Recordable) => {
|
||||
const events = d.data.attachedEvents;
|
||||
if (events && events.length) {
|
||||
@ -259,7 +285,7 @@ export default class ListGraph {
|
||||
.append("text")
|
||||
.attr("x", 267)
|
||||
.attr("y", -1)
|
||||
.attr("fill", appStore.theme === Themes.Dark ? "#666" : "#e66")
|
||||
.attr("fill", "#e66")
|
||||
.style("font-size", "10px")
|
||||
.text((d: Recordable) => {
|
||||
const events = d.data.attachedEvents;
|
||||
@ -381,6 +407,36 @@ export default class ListGraph {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
highlightParents(span: Recordable) {
|
||||
if (!span) {
|
||||
return;
|
||||
}
|
||||
const nodes = this.root.descendants().map((node: { id: number }) => {
|
||||
d3.select(`#list-node-${node.id}`).classed("highlightedParent", false);
|
||||
return node;
|
||||
});
|
||||
const parentSpan = nodes.find(
|
||||
(node: Recordable) =>
|
||||
span.spanId === node.data.spanId &&
|
||||
span.segmentId === node.data.segmentId &&
|
||||
span.traceId === node.data.traceId,
|
||||
);
|
||||
if (!parentSpan) return;
|
||||
d3.select(`#list-node-${parentSpan.id}`).classed("highlightedParent", true);
|
||||
d3.select("#trace-action-box").style("display", "none");
|
||||
this.selectedNode.classed("highlighted", false);
|
||||
const container = document.querySelector(".trace-chart .charts");
|
||||
const containerRect = container?.getBoundingClientRect();
|
||||
if (!containerRect) return;
|
||||
const targetElement = document.querySelector(`#list-node-${parentSpan.id}`);
|
||||
if (!targetElement) return;
|
||||
const targetRect = targetElement.getBoundingClientRect();
|
||||
container?.scrollTo({
|
||||
left: targetRect.left - containerRect.left + container?.scrollLeft,
|
||||
top: targetRect.top - containerRect.top + container?.scrollTop - 100,
|
||||
behavior: "smooth",
|
||||
});
|
||||
}
|
||||
visDate(date: number, pattern = "YYYY-MM-DD HH:mm:ss:SSS") {
|
||||
return dayjs(date).format(pattern);
|
||||
}
|
||||
|
@ -55,7 +55,7 @@ export default class TraceMap {
|
||||
this.topChild = [];
|
||||
this.width = el.clientWidth - 20;
|
||||
this.height = el.clientHeight - 30;
|
||||
d3.select(".d3-trace-tree").remove();
|
||||
d3.select(`.${this.el?.className} .d3-trace-tree`).remove();
|
||||
this.body = d3
|
||||
.select(this.el)
|
||||
.append("svg")
|
||||
|
Loading…
Reference in New Issue
Block a user