blob: 05f09e6381117cce7ada35850e66302e08098ef5 [file] [log] [blame]
/**
* 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="rk-trace-t-loading" v-show="loading">
<svg class="icon loading">
<use xlink:href="#spinner"></use>
</svg>
</div>
<transition-group name="fade" tag="a" class="mb-5">
<span class="time-charts-item mr-10" v-for="(i,index) in list" :key="i" :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>
<rk-sidebox :width="'50%'" :show.sync="showDetail" :title="$t('spanInfo')">
<div class="rk-trace-detail">
<h5 class="mb-15">{{$t('tags')}}.</h5>
<div class="mb-10 clear"><span class="g-sm-4 grey">{{$t('endpoint')}}:</span><span class="g-sm-8 wba">{{this.currentSpan.label}}</span></div>
<div class="mb-10 clear"><span class="g-sm-4 grey">{{$t('spanType')}}:</span><span class="g-sm-8 wba">{{this.currentSpan.type}}</span></div>
<div class="mb-10 clear"><span class="g-sm-4 grey">{{$t('component')}}:</span><span class="g-sm-8 wba">{{this.currentSpan.component}}</span></div>
<div class="mb-10 clear"><span class="g-sm-4 grey">Peer:</span><span class="g-sm-8 wba">{{this.currentSpan.peer||'No Peer'}}</span></div>
<div class="mb-10 clear"><span class="g-sm-4 grey">{{$t('error')}}:</span><span class="g-sm-8 wba">{{this.currentSpan.isError}}</span></div>
<div class="mb-10 clear" v-for="i in this.currentSpan.tags" :key="i.key">
<span class="g-sm-4 grey">{{i.key}}:</span>
<span class="g-sm-8 wba">
{{i.value}}
<svg v-if="i.key==='db.statement'" class="icon vm grey link-hover cp ml-5" @click="copy(i.value)">
<use xlink:href="#review-list"></use>
</svg>
</span>
</div>
<h5 class="mb-10" v-if="this.currentSpan.logs" v-show="this.currentSpan.logs.length">{{$t('logs')}}.</h5>
<div v-for="(i, index) in this.currentSpan.logs" :key="index">
<div class="mb-10 sm"><span class="mr-10">{{$t('time')}}:</span><span class="grey">{{i.time | dateformat}}</span></div>
<div class="mb-15 clear" v-for="(_i, _index) in i.data" :key="_index">
<div class="mb-10">{{_i.key}}:<span v-if="_i.key==='stack'" class="r rk-sidebox-magnify"
@click="showCurrentSpanDetail(_i.key, _i.value)">
<svg class="icon">
<use xlink:href="#magnify"></use>
</svg>
</span></div><pre class="pl-15 mt-0 mb-0 sm oa" >{{_i.value}}</pre>
</div>
</div>
</div>
</rk-sidebox>
<v-dialog width="90%"/>
<div class="trace-list">
<div ref="traceList"></div>
</div>
</div>
</template>
<script lang="js">
import copy from '@/utils/copy';
import * as d3 from 'd3';
import Trace from './d3-trace';
import _ from 'lodash';
export default {
props: ['data', 'traceId'],
data() {
return {
segmentId: [],
showDetail: false,
list: [],
currentSpan: [],
loading: true,
};
},
watch: {
data() {
if (!this.data.length) { return; }
this.loading = true;
this.changeTree();
this.tree.init({label: 'TRACE_ROOT', children: this.segmentId}, this.data);
this.tree.draw(() => {
setTimeout(() => {
this.loading = false;
}, 200);
});
},
},
beforeDestroy() {
d3.selectAll('.d3-tip').remove();
},
mounted() {
this.$eventBus.$on('TRACE-LIST-LOADING', this, () => { this.loading = true; });
// this.loading = true;
this.changeTree();
this.tree = new Trace(this.$refs.traceList, this);
this.tree.init({label: 'TRACE_ROOT', children: this.segmentId}, this.data);
this.tree.draw();
this.loading = false;
// this.computedScale();
},
methods: {
copy,
handleSelectSpan(i) {
this.currentSpan = i.data;
this.showDetail = true;
},
traverseTree(node, spanId, segmentId, data) {
if (!node) { return; }
if (node.spanId === spanId && node.segmentId === segmentId) {
node.children.push(data);
return;
}
if (node.children && node.children.length > 0) {
node.children.forEach((nodeItem) => {
this.traverseTree(nodeItem, spanId, segmentId, data);
});
}
},
computedScale(i) {
// Rainbow map
const sequentialScale = d3.scaleSequential()
.domain([0, this.list.length + 1])
.interpolator(d3.interpolateCool);
return sequentialScale(i);
},
changeTree() {
if (this.data.length === 0) {
return [];
}
this.list = Array.from(new Set(this.data.map((i) => i.serviceCode)));
this.segmentId = [];
const segmentGroup = {};
const segmentIdGroup = [];
const fixSpans = [];
const segmentHeaders = [];
this.data.forEach((span) => {
if (span.parentSpanId === -1) {
segmentHeaders.push(span);
} else {
const index = this.data.findIndex((i) => (
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) => {
if (span.refs.length) {
span.refs.forEach((ref) => {
const index = this.data.findIndex((i) => (
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, ...this.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);
}
});
segmentIdGroup.forEach((id) => {
const currentSegment = segmentGroup[id].sort((a, b) => b.parentSpanId - a.parentSpanId);
currentSegment.forEach((s) => {
const index = currentSegment.findIndex((i) => 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, b) => a.spanId - b.spanId);
}
}
if (s.isBroken) {
const children = _.filter(this.data, (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) => {
segmentGroup[id].refs.forEach((ref) => {
if (ref.traceId === this.traceId) {
this.traverseTree(
segmentGroup[ref.parentSegmentId],
ref.parentSpanId,
ref.parentSegmentId,
segmentGroup[id]);
}
});
});
for (const i in segmentGroup) {
if (segmentGroup[i].refs.length === 0 ) {
this.segmentId.push(segmentGroup[i]);
}
}
this.segmentId.forEach((i) => {
this.collapse(i);
});
},
collapse(d) {
if (d.children) {
let dur = d.endTime - d.startTime;
d.children.forEach((i) => {
dur -= (i.endTime - i.startTime);
});
d.dur = dur < 0 ? 0 : dur;
d.children.forEach((i) => this.collapse(i));
}
},
showCurrentSpanDetail(title, text) {
const textLineNumber = text.split('\n').length;
let textHeight = textLineNumber * 20.2 + 10;
const tmpHeight = window.innerHeight * 0.9;
textHeight = textHeight >= tmpHeight ? tmpHeight : textHeight;
this.$modal.show('dialog', {
title,
text: `<div style="height:${textHeight}px">${text}</div>`,
buttons: [
{
title: 'Copy',
handler: () => {
this.copy(text);
},
},
{
title: 'Close',
},
],
});
},
downloadTrace() {
const serializer = new XMLSerializer();
const svgNode = 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 = canvas.getContext('2d');
canvas.width = d3.select('.trace-list-dowanload')._groups[0][0].clientWidth;
canvas.height = d3.select('.trace-list-dowanload')._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();
};
},
},
};
</script>
<style lang="scss">
.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>