blob: d5974b6085e69323ce30ef699b91b6b85004b4e7 [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.
*/
import React, { PureComponent } from 'react';
import { Tag, List, Card, Row, Col, Badge, Button } from 'antd';
import * as d3 from 'd3';
import moment from 'moment';
import { formatDuration } from '../../utils/time';
import DescriptionList from "../DescriptionList";
import styles from './index.less';
import TraceTree from '../TraceTree';
const ButtonGroup = Button.Group;
const { Description } = DescriptionList;
const height = 36;
const margin = 10;
const offX = 15;
const offY = 6;
const timeFormat = 'YYYY-MM-DD HH:mm:ss.SSS';
class TraceStack extends PureComponent {
state = {
nodes: [],
idMap: {},
colorMap: {},
bap: [],
span: {},
key: 'tags',
treeMode: true,
}
componentWillMount() {
const { spans } = this.props;
const { colorMap } = this.state;
const serviceList = Array.from(new Set(spans.map(i => i.serviceCode)));
const sequentialScale = d3.scaleSequential()
.domain([0, serviceList.length])
.interpolator(d3.interpolateCool);
spans.forEach((span) => {
if (!colorMap[span.serviceCode]) {
colorMap[span.serviceCode] = sequentialScale(serviceList.indexOf(span.serviceCode))
}
this.buildNode(span);
});
const { nodes } = this.state;
const minStartTimeNode = nodes.reduce((acc, n) => (acc.startTime > n.startTime ? n : acc));
this.state.nodes = nodes.map(n =>
({ ...n, startOffset: n.startTime - minStartTimeNode.startTime }));
}
componentDidMount() {
this.state.width = this.axis.parentNode.clientWidth - 50;
this.drawAxis();
this.displayData();
window.addEventListener('resize', this.resize);
}
onTabChange = (key, type) => {
this.setState({ [type]: key });
}
buildNode = (span) => {
const { nodes, idMap } = this.state;
const node = {};
node.serviceCode = span.serviceCode;
node.startTime = span.startTime;
node.endTime = span.endTime;
node.duration = span.endTime - span.startTime;
node.content = span.endpointName;
node.spanSegId = this.id(span.segmentId, span.spanId);
node.parentSpanSegId = this.findParent(span);
node.refs = span.refs;
node.type = span.type;
node.peer = span.peer;
node.component = span.component;
node.isError = span.isError;
node.layer = span.layer;
node.tags = span.tags;
node.logs = span.logs;
nodes.push(node);
idMap[node.spanSegId] = nodes.length - 1;
}
id = (...seg) => seg.join();
findParent = (span) => {
const { spans } = this.props;
if (span.refs) {
const ref = span.refs.find(_ => spans.findIndex(s =>
this.id(_.parentSegmentId, _.parentSpanId) === this.id(s.segmentId, s.spanId)) > -1);
if (ref) {
return this.id(ref.parentSegmentId, ref.parentSpanId);
}
}
const result = this.id(span.segmentId, span.parentSpanId);
if (spans.findIndex(s => result === this.id(s.segmentId, s.spanId)) > -1) {
return result;
}
return null;
}
drawAxis = () => {
const { width } = this.state;
const { nodes, bap } = this.state;
const dataSet = nodes.map(node => node.startOffset + node.duration);
const bits = d3.max(dataSet).toString().length;
const percentScale = Math.ceil(d3.max(dataSet) / (10 ** (bits - 2)));
const axisHeight = 20;
const svg = d3.select(this.axis).append('svg')
.attr('width', width)
.attr('height', axisHeight)
.attr('style', 'overflow: visible');
const xScale = d3.scaleLinear()
.domain([0, d3.max(dataSet)])
.range([0, width]);
const axis = d3.axisTop(xScale).ticks(4).tickSize([(height * nodes.length) + 40])
.tickFormat(formatDuration);
svg.append('g')
.attr('class', styles.axis)
.attr('transform', `translate(0, ${axisHeight})`)
.call(axis);
bap.push(bits);
bap.push(percentScale);
return bap;
}
displayData = () => {
const { nodes, bap, width, colorMap } = this.state;
const svgContainer = d3.select(this.duration).append('svg').attr('height', height * nodes.length).attr('style', 'overflow: visible');
const positionMap = {};
nodes.forEach((node, index) => {
const { startOffset: startTime, duration, content,
serviceCode, spanSegId, parentSpanSegId } = node;
const rectWith = ((duration * width) / (bap[1] * (10 ** (bap[0] - 4)))) / 100;
const beginX = ((startTime * width) / (bap[1] * (10 ** (bap[0] - 4)))) / 100;
const bar = svgContainer.append('g').attr('transform', (d, i) => `translate(0,${i * height})`);
const beginY = index * height;
positionMap[spanSegId] = { x: beginX, y: beginY };
const rectHeight = height - margin;
const position = { width, top: beginY, left: beginX };
const container = bar.append('rect').attr('spanSegId', spanSegId).attr('x', -5).attr('y', beginY - 5)
.attr('width', width + 10)
.attr('height', rectHeight + 10)
.attr('class', styles.backgroudHide)
.on('mouseover', () => { this.selectTimeline(container, true); })
.on('mouseout', () => { this.selectTimeline(container, false); })
.on('click', () => { this.showSpanModal(node, position, container); });
bar.append('rect').attr('x', beginX).attr('y', beginY).attr('width', rectWith + 0.1)
.attr('height', rectHeight)
.on('mouseover', () => { this.selectTimeline(container, true); })
.on('mouseout', () => { this.selectTimeline(container, false); })
.on('click', () => { this.showSpanModal(node, position, container); })
.style('fill', colorMap[serviceCode]);
bar.append('text')
.attr('x', () => {
if ((width - beginX) < (content.length * 5.2)) {
return beginX - content.length * 5.2 - 45
}
return beginX + 8;
})
.attr('y', (index * height) + (height / 2) - 2)
.attr('class', styles.rectText)
.on('mouseover', () => { this.selectTimeline(container, true); })
.on('mouseout', () => { this.selectTimeline(container, false); })
.on('click', () => { this.showSpanModal(node, position, container); })
.text(`${content} ${formatDuration(duration)}`);
if (node.isError) {
bar.append('svg:image')
.attr('xlink:href', 'img/icon/error.png')
.attr('x', width + (rectHeight / 2))
.attr('y', beginY)
.attr('width', rectHeight)
.attr('height', rectHeight);
}
if (index > 0 && positionMap[parentSpanSegId]) {
const parentX = positionMap[parentSpanSegId].x;
const parentY = positionMap[parentSpanSegId].y;
const defs = svgContainer.append('defs');
const arrowMarker = defs.append('marker')
.attr('id', 'arrow')
.attr('markerUnits', 'strokeWidth')
.attr('markerWidth', 5)
.attr('markerHeight', 5)
.attr('viewBox', '-5 -5 10 10')
.attr('refX', 0)
.attr('refY', 0)
.attr('orient', 'auto');
arrowMarker.append('path')
.attr('d', 'M 0,0 m -5,-5 L 5,0 L -5,5 Z')
.attr('fill', '#8543e0').attr('opacity', 0.8);
const parentLeftBottomX = parentX;
const parentLeftBottomY = (Number(parentY) + Number(height)) - Number(margin);
const selfMiddleX = beginX;
const selfMiddleY = beginY + ((height - margin) / 2);
if ((beginX - parentX) < 10) {
svgContainer.append('line').attr('x1', parentLeftBottomX - offX).attr('y1', parentLeftBottomY - offY).attr('class', styles.connlines)
.on('mouseover', () => { this.selectTimeline(container, true); })
.on('mouseout', () => { this.selectTimeline(container, false); })
.on('click', () => { this.showSpanModal(node, position, container); })
.attr('x2', parentLeftBottomX)
.attr('y2', parentLeftBottomY - offY);
svgContainer.append('line').attr('x1', parentLeftBottomX - offX).attr('y1', parentLeftBottomY - offY).attr('class', styles.connlines)
.on('mouseover', () => { this.selectTimeline(container, true); })
.on('mouseout', () => { this.selectTimeline(container, false); })
.on('click', () => { this.showSpanModal(node, position, container); })
.attr('x2', parentLeftBottomX - offX)
.attr('y2', selfMiddleY);
svgContainer.append('line').attr('x1', parentLeftBottomX - offX).attr('y1', selfMiddleY).attr('class', styles.connlines)
.on('mouseover', () => { this.selectTimeline(container, true); })
.on('mouseout', () => { this.selectTimeline(container, false); })
.on('click', () => { this.showSpanModal(node, position, container); })
.attr('x2', selfMiddleX - 5)
.attr('y2', selfMiddleY)
.attr('marker-end', 'url(#arrow)');
} else {
svgContainer.append('line').attr('x1', parentLeftBottomX).attr('y1', parentLeftBottomY).attr('class', styles.connlines)
.on('mouseover', () => { this.selectTimeline(container, true); })
.on('mouseout', () => { this.selectTimeline(container, false); })
.on('click', () => { this.showSpanModal(node, position, container); })
.attr('x2', parentLeftBottomX)
.attr('y2', selfMiddleY);
svgContainer.append('line').attr('x1', parentLeftBottomX).attr('y1', selfMiddleY).attr('class', styles.connlines)
.on('mouseover', () => { this.selectTimeline(container, true); })
.on('mouseout', () => { this.selectTimeline(container, false); })
.on('click', () => { this.showSpanModal(node, position, container); })
.attr('x2', selfMiddleX - 5)
.attr('y2', selfMiddleY)
.attr('marker-end', 'url(#arrow)');
}
}
});
}
selectTimeline = (container, isOver) => {
const {...stateData} = this.state;
if (stateData.container === container) {
return;
}
container.attr('class', isOver ? styles.backgroud : styles.backgroudHide);
}
showSpanModal = (span, position, container) => {
const {...stateData} = this.state;
const { container: old } = this.state;
if (old) {
old.attr('class', styles.backgroudHide);
}
container.attr('class', styles.backgroudSelected);
this.setState({
...stateData,
span,
key: 'tags',
position,
container,
});
}
hideSpanModal = () => {
const { container: old } = this.state;
const {...stateData} = this.state;
if (old) {
old.attr('class', styles.backgroudHide);
}
this.setState({
...stateData,
span: {},
container: undefined,
});
}
resize = () => {
const {...stateData} = this.state;
if (!this.axis) {
return;
}
this.setState({width:this.axis.parentNode.clientWidth - 50});
if (!this.axis || stateData.width <= 0) {
return;
}
this.axis.innerHTML = '';
this.duration.innerHTML = '';
this.drawAxis();
this.displayData();
this.setState({ ...stateData, span: {} });
}
renderTitle = (items) => {
return (
<Row type="flex" justify="start" gutter={15}>
{
items.map((_) => {
return (
<Col key={_.name}>
<span>{_.name}</span>
<Badge count={_.count} style={{ backgroundColor: '#1890FF', marginLeft: 5 }} />
</Col>
);
})
}
</Row>
);
}
render() {
const { spans } = this.props;
const { colorMap, span = {}, position = { width: 100, top: 0 } } = this.state;
const legendButtons = Object.keys(colorMap).map(key =>
(<Tag color={colorMap[key]} key={key}>{key}</Tag>));
const tabList = [];
const contentList = {};
if (span.content) {
tabList.push({
key: 'tags',
tab: 'Tags',
});
const base = [
{
title: 'span type',
content: span.type,
},
{
title: 'component',
content: span.component,
},
{
title: 'peer',
content: span.peer,
},
{
title: 'is error',
content: `${span.isError}`,
},
];
const data = base.concat(span.tags.map(t => ({ title: t.key, content: t.value })));
contentList.tags = (
<DescriptionList layout="vertical">
{data.map(_ =>
<Description key={_.title} term={_.title}>{_.content}</Description>)}
</DescriptionList>);
}
if (span.logs) {
tabList.push({
key: 'logs',
tab: 'Logs',
});
contentList.logs = (
<List
itemLayout="horizontal"
dataSource={span.logs}
renderItem={log => (
<List.Item>
<List.Item.Meta
size="small"
title={moment(log.time).format('mm:ss.SSS')}
description={
<DescriptionList layout="vertical" col={1}>
{log.data.map((_) => {
return (
<Description key={_.key} term={_.key}>
<pre className={styles.pre}>{_.value}</pre>
</Description>);
})}
</DescriptionList>
}
/>
</List.Item>
)}
/>);
}
if (!span.parentSpanSegId && span.refs) {
tabList.push({
key: 'relatedTraces',
tab: 'Related Trace',
});
contentList.relatedTraces = (
<DescriptionList layout="vertical">
{span.refs.map(_ => <Description key={_.type} term={_.type}>{_.traceId}</Description>)}
</DescriptionList>);
}
const { top, left, width } = position;
const {...stateData} = this.state;
const toolTipStyle = { position: 'absolute', top: top + 86 };
if (contentList.logs) {
toolTipStyle.left = 0;
toolTipStyle.width = width;
} else {
const right = width - left;
if (left * 2 > width) {
toolTipStyle.right = right;
toolTipStyle.maxWidth = left;
} else {
toolTipStyle.left = left;
toolTipStyle.maxWidth = right;
}
}
return (
<div className={styles.stack}>
<div style={{ paddingBottom: 10 }}>
<ButtonGroup>
<Button type={stateData.treeMode ? "primary": ""} onClick={() => this.setState({treeMode:true})}>TreeMode</Button>
<Button type={stateData.treeMode ? "": "primary"} onClick={() => this.setState({treeMode: false})}>ListMode</Button>
</ButtonGroup>
</div>
<div style={{ paddingBottom: 10 }}>
{ legendButtons }
</div>
<div style={{display: stateData.treeMode?'none':'block'}} className={styles.duration} ref={(el) => { this.duration = el; }} />
<div style={{display: stateData.treeMode?'none':'block'}} ref={(el) => { this.axis = el; }} />
<div style={{display: stateData.treeMode?'block':'none'}}>
<TraceTree showSpanModal={this.showSpanModal} data={spans} id="" />
</div>
{tabList.length > 0 ? (
<Card
type="inner"
title={this.renderTitle([
{
name: 'Start Time',
count: `${moment(span.startTime).format(timeFormat)}`,
},
{
name: 'Duration',
count: `${formatDuration(span.duration)}`,
},
])}
tabList={tabList}
onTabChange={(key) => { this.onTabChange(key, 'key'); }}
style={toolTipStyle}
extra={<Button type="primary" shape="circle" icon="close" ghost onClick={this.hideSpanModal} />}
>
{contentList[stateData.key]}
</Card>
) : null}
</div>
);
}
}
export default TraceStack;