| <!-- 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 scroll_hide flex-v"> |
| <transition-group name="fade" tag="div" style="padding: 10px 30px;"> |
| <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> |
| <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> |
| <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-tree" style="height:100%"> |
| <div class="trace-tree-inner" ref="traceTree"></div> |
| </div> |
| </div> |
| </template> |
| <script lang="js"> |
| import copy from '@/utils/copy'; |
| import * as d3 from 'd3'; |
| import Tree from './d3-trace-tree'; |
| import _ from 'lodash'; |
| /* eslint-disable */ |
| /* tslint:disable */ |
| export default { |
| props: ['data', 'traceId'], |
| data(){ |
| return { |
| segmentId:[], |
| showDetail: false, |
| list: [], |
| currentSpan: [], |
| }; |
| }, |
| watch: { |
| data() { |
| if(!this.data.length) {return;} |
| d3.select('.trace-tree-inner').selectAll('svg').selectAll('svg').remove(); |
| this.changeTree(); |
| this.tree.init({label:`${this.traceId}`, children: this.segmentId}, this.data); |
| } |
| }, |
| mounted() { |
| window.addEventListener('resize', this.resize); |
| this.changeTree(); |
| this.tree = new Tree(this.$refs.traceTree, this); |
| this.tree.init({label:`${this.traceId}`, children: this.segmentId}, this.data); |
| }, |
| beforeDestroy() { |
| window.removeEventListener('resize', this.resize); |
| }, |
| 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) { |
| for (let i = 0; i < node.children.length; i++) { |
| this.traverseTree(node.children[i],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, |
| }; |
| !_.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, |
| }; |
| !_.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 => { |
| let currentSegment = segmentGroup[id].sort((a,b) => b.parentSpanId-a.parentSpanId); |
| currentSegment.forEach(s =>{ |
| let 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}); |
| }); |
| 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]) |
| }; |
| }) |
| // if(segmentGroup[id].refs.length !==0 ) delete segmentGroup[id]; |
| }) |
| for (let i in segmentGroup) { |
| if(segmentGroup[i].refs.length ===0 ) |
| this.segmentId.push(segmentGroup[i]); |
| } |
| this.segmentId.forEach((_, i) => { |
| this.collapse(this.segmentId[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)); |
| } |
| }, |
| resize() { |
| this.tree.resize(); |
| }, |
| 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', |
| }, |
| ], |
| }) |
| }, |
| } |
| }; |
| </script> |
| <style lang="scss"> |
| .trace-tree-btn { |
| display: inline-block; |
| border-radius: 4px; |
| padding: 0px 7px; |
| background-color: #40454e; |
| color: #eee; |
| font-size: 11px; |
| } |
| .trace-tree-charts { |
| overflow: auto; |
| flex-grow: 1; |
| height: 100%; |
| } |
| .trace-node .group { |
| cursor: pointer; |
| fill-opacity: 0; |
| } |
| .trace-tree-inner { |
| height: 100%; |
| } |
| .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; |
| } |
| .tree-link { |
| fill: none; |
| stroke: rgba(0, 0, 0, 0.1); |
| stroke-width: 2px; |
| } |
| .time-charts-item { |
| display: inline-block; |
| padding: 2px 8px; |
| border: 1px solid; |
| font-size: 11px; |
| border-radius: 4px; |
| } |
| .trace-tree { |
| fill: rgba(0, 0, 0, 0); |
| flex-grow: 1; |
| } |
| .trace-tree .trace-node rect { |
| &:hover { |
| fill: rgba(0, 0, 0, 0.05); |
| } |
| } |
| .dialog-c-text { |
| white-space: pre; |
| overflow: auto; |
| font-family: monospace; |
| } |
| </style> |