| <!-- 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')"> |
| <TraceSpanLogs :currentSpan="currentSpan" /> |
| </rk-sidebox> |
| <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'; |
| import TraceSpanLogs from './trace-span-logs.vue'; |
| export default { |
| components: {TraceSpanLogs}, |
| props: ['data', 'traceId'], |
| data() { |
| return { |
| segmentId: [], |
| showDetail: false, |
| list: [], |
| currentSpan: [], |
| loading: true, |
| fixSpansSize: 0, |
| showRelatedLogs: false, |
| }; |
| }, |
| watch: { |
| data() { |
| if (!this.data.length) { return; } |
| this.loading = true; |
| this.changeTree(); |
| this.tree.init({label: 'TRACE_ROOT', children: this.segmentId}, this.data, this.fixSpansSize); |
| 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.fixSpansSize); |
| this.tree.draw(); |
| this.loading = false; |
| // this.computedScale(); |
| }, |
| methods: { |
| handleSelectSpan(i) { |
| this.currentSpan = i.data; |
| this.showDetail = true; |
| }, |
| traverseTree(node, spanId, segmentId, data) { |
| 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) => { |
| 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); |
| } |
| }); |
| this.fixSpansSize = fixSpans.length; |
| 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)); |
| } |
| }, |
| 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> |