Skip to content

Commit 32df996

Browse files
jerisccvsen
authored andcommitted
Development: Related Traces Feature (#354)
* duplicated trends tab in trace details * changed names of duplicated tab * server configuration file * created proof of concept tab for traces related by trace id * implemented filter by time, visitor id, & new logic to prevent relation by fields that are not part of the trace * small fix for traceId not appearing to be searchable * adapted to serviceName to universal search in relatedTraces * minor change * fixed bug where the value of x-ha- tags where not included in related traces query, plus additional features * changed error throwing mechanism, fixed unselected option * removed searchableKeysStore dependency from traceDetailsStore, removed uiState dependency from relatedTraces components * migrated trace details config to base file, adapted minor fixes * hides relatedTraces tab when it isn't configured in base.js * implemented suggested features from virtual meeting * moved trace details tests into own describe, fixed proptypes issue in tests * fixed react modal issue and universal search warning * fixed react-ga warning in traces test * mocked backend service to resolve all warning messages in universal test * added timeline route in mockadapter to resolve test warnings * wrote most coverage for related traces feature * added full test coverage to related traces components * added relatedTraces stub and traceDetailsStore tests * cleaning stuff * small commit for cleaning purposes * changed small documentation comment * changed more comments
1 parent a13ce5e commit 32df996

File tree

13 files changed

+956
-95
lines changed

13 files changed

+956
-95
lines changed

server/config/base.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,14 @@ module.exports = {
130130
longName: '30 days',
131131
value: 30 * 24 * 60 * 60 * 1000
132132
}
133+
],
134+
135+
relatedTracesOptions: [
136+
{
137+
fieldTag: 'url2',
138+
propertyToMatch: 'url2',
139+
fieldDescription: 'test trait'
140+
}
133141
]
134142

135143
// use if you need SAML back SSO auth

server/routes/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@ router.get('*', (req, res) => {
4646
tracesTimePresetOptions: config.connectors.traces.timePresetOptions,
4747
timeWindowPresetOptions: config.timeWindowPresetOptions,
4848
tracesTTL: config.connectors.traces.ttl,
49-
trendsTTL: config.connectors.trends.ttl
49+
trendsTTL: config.connectors.trends.ttl,
50+
relatedTracesOptions: config.relatedTracesOptions
5051
});
5152

5253
onFinished(res, () => {

server/views/index.pug

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ html
2727
tracesTimePresetOptions: !{JSON.stringify(tracesTimePresetOptions || ['5m', '15m', '1h', '4h', '12h', '24h', '3d'])},
2828
timeWindowPresetOptions: !{JSON.stringify(timeWindowPresetOptions || [{shortName: '5m', longName: '5 minutes', value: 5 * 60 * 1000}, {shortName: '15m', longName: '15 minutes', value: 15 * 60 * 1000}, {shortName: '1h', longName: '1 hour', value: 60 * 60 * 1000}, {shortName: '6h', longName: '6 hours', value: 6 * 60 * 60 * 1000}, {shortName: '12h', longName: '12 hours', value: 12 * 60 * 60 * 1000}, {shortName: '24h', longName: '24 hours', value: 24 * 60 * 60 * 1000}, {shortName: '3d', longName: '3 days', value: 3 * 24 * 60 * 60 * 1000}, {shortName: '7d', longName: '7 days', value: 7 * 24 * 60 * 60 * 1000}, {shortName: '30d', longName: '30 days', value: 30 * 24 * 60 * 60 * 1000}])},
2929
tracesTTL: !{JSON.stringify(tracesTTL || -1)},
30-
trendsTTL: !{JSON.stringify(trendsTTL || -1)}
30+
trendsTTL: !{JSON.stringify(trendsTTL || -1)},
31+
relatedTracesOptions: !{JSON.stringify(relatedTracesOptions || [])}
3132
}
3233
script(async, defer, src = bundleCommonsJsPath)
3334
script(async, defer, src = bundleAppJsPath)

src/components/common/modal.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ const modalStyles = {
3838
};
3939

4040
const ModalView = ({serviceName, title, isOpen, closeModal, children}) => (
41-
<Modal isOpen={isOpen} onRequestClose={closeModal} style={modalStyles} closeTimeoutMS={200} contentLabel={'Modal'}>
41+
<Modal isOpen={isOpen} onRequestClose={closeModal} style={modalStyles} closeTimeoutMS={200} contentLabel={'Modal'} ariaHideApp={false}>
4242
<header className="clearfix">
4343
<div className="pull-left">
4444
<div>{serviceName && (<span className={`service-spans label ${colorMapper.toBackgroundClass(serviceName)}`}>{serviceName}</span>)}</div>
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/*
2+
* Copyright 2018 Expedia, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import React from 'react';
18+
import PropTypes from 'prop-types';
19+
import {PropTypes as MobxPropTypes} from 'mobx-react';
20+
21+
import colorMapper from '../../../../utils/serviceColorMapper';
22+
import linkBuilder from '../../../../utils/linkBuilder';
23+
import formatters from '../../../../utils/formatters';
24+
25+
export default class RelatedTracesRow extends React.Component {
26+
static propTypes = {
27+
traceId: PropTypes.string.isRequired,
28+
serviceName: PropTypes.string.isRequired,
29+
operationName: PropTypes.string.isRequired,
30+
spanCount: PropTypes.number.isRequired,
31+
startTime: PropTypes.number.isRequired,
32+
rootError: PropTypes.bool.isRequired,
33+
services: MobxPropTypes.observableArray, // eslint-disable-line react/require-default-props
34+
duration: PropTypes.number.isRequired,
35+
isUniversalSearch: PropTypes.bool.isRequired
36+
};
37+
38+
static openTrendDetailInNewTab(traceId, serviceName, operationName, isUniversalSearch) {
39+
let traceUrl = '';
40+
if (isUniversalSearch) {
41+
const search = {traceId}; // TODO add specific time for trace
42+
traceUrl = linkBuilder.withAbsoluteUrl(linkBuilder.universalSearchTracesLink(search));
43+
} else {
44+
traceUrl = linkBuilder.withAbsoluteUrl(linkBuilder.createTracesLink({
45+
serviceName,
46+
operationName,
47+
traceId
48+
}));
49+
}
50+
51+
const tab = window.open(traceUrl, '_blank');
52+
tab.focus();
53+
}
54+
55+
// Formatters copied from trace ResultsTable.jsx and then refactored into jsx.
56+
// START TIME
57+
static timeColumnFormatter(startTime) {
58+
return (<div>
59+
<div className="table__primary">{formatters.toTimeago(startTime)}</div>
60+
<div className="table__secondary">{formatters.toTimestring(startTime)}</div>
61+
</div>);
62+
}
63+
// ROOT SUCCESS
64+
static errorFormatter(cell) {
65+
const status = cell ? 'error' : 'success';
66+
return (<div className="table__status">
67+
<img src={`/images/${status}.svg`} alt={status} height="24" width="24" />
68+
</div>);
69+
}
70+
// TOTAL DURATION
71+
static totalDurationColumnFormatter(duration) {
72+
return <div className="table__primary-duration text-right">{formatters.toDurationString(duration)}</div>;
73+
}
74+
// SPAN COUNT
75+
static handleServiceList(services) {
76+
const serviceList = services.slice(0, 2).map(svc =>
77+
<span key={svc.name} className={'service-spans label ' + colorMapper.toBackgroundClass(svc.name)}>{svc.name +' x' + svc.spanCount}</span> // eslint-disable-line
78+
);
79+
80+
if (services.length > 2) {
81+
serviceList.push(<span key="extra">...</span>);
82+
}
83+
return serviceList;
84+
}
85+
static spanColumnFormatter(spanCount, services) {
86+
return (<div>
87+
<div className="table__primary">{spanCount}</div>
88+
<div>{RelatedTracesRow.handleServiceList(services)}</div>
89+
</div>);
90+
}
91+
92+
93+
render() {
94+
const {traceId, serviceName, operationName, spanCount, services, startTime, rootError, duration, isUniversalSearch} = this.props;
95+
96+
return (
97+
<tr onClick={() => RelatedTracesRow.openTrendDetailInNewTab(traceId, serviceName, operationName, isUniversalSearch)}>
98+
<td className="trace-trend-table_cell">
99+
{RelatedTracesRow.timeColumnFormatter(startTime)}
100+
</td>
101+
<td className="trace-trend-table_cell">
102+
<div className={`service-spans label label-default ${colorMapper.toBackgroundClass(serviceName)}`}>{serviceName}</div>
103+
<div className="trace-trend-table_op-name">{operationName}</div>
104+
</td>
105+
<td className="trace-trend-table_cell">
106+
{RelatedTracesRow.errorFormatter(rootError)}
107+
</td>
108+
<td className="trace-trend-table_cell">
109+
{RelatedTracesRow.spanColumnFormatter(spanCount, services)}
110+
</td>
111+
<td className="trace-trend-table_cell">
112+
{RelatedTracesRow.totalDurationColumnFormatter(duration)}
113+
</td>
114+
</tr>
115+
);
116+
}
117+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/* eslint-disable react/prefer-stateless-function */
2+
/*
3+
* Copyright 2018 Expedia, Inc.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
import React from 'react';
19+
import PropTypes from 'prop-types';
20+
import {PropTypes as MobxPropTypes} from 'mobx-react';
21+
22+
import RelatedTracesRow from './relatedTracesRow';
23+
import linkBuilder from '../../../../utils/linkBuilder';
24+
25+
export default class relatedTracesTab extends React.Component {
26+
static propTypes = {
27+
searchQuery: PropTypes.object.isRequired,
28+
relatedTraces: MobxPropTypes.observableArray, // eslint-disable-line react/require-default-props
29+
isUniversalSearch: PropTypes.bool.isRequired
30+
};
31+
32+
constructor(props) {
33+
super(props);
34+
const numDisplayedTraces = 5;
35+
this.state = {
36+
numDisplayedTraces // compute the fields of the original trace
37+
};
38+
39+
this.showMoreTraces = this.showMoreTraces.bind(this);
40+
}
41+
42+
showMoreTraces() {
43+
const traceUrl = linkBuilder.withAbsoluteUrl(linkBuilder.universalSearchTracesLink(this.props.searchQuery));
44+
45+
const tab = window.open(traceUrl, '_blank');
46+
tab.focus();
47+
}
48+
49+
render() {
50+
const {relatedTraces, isUniversalSearch} = this.props;
51+
const {numDisplayedTraces} = this.state;
52+
53+
const relatedTracesList = relatedTraces.slice(0, numDisplayedTraces);
54+
55+
return (
56+
<article>
57+
<table className="trace-trend-table">
58+
<thead className="trace-trend-table_header">
59+
<tr>
60+
<th width="20" className="trace-trend-table_cell">Start Time</th>
61+
<th width="30" className="trace-trend-table_cell">Root</th>
62+
<th width="20" className="trace-trend-table_cell">Root Success</th>
63+
<th width="60" className="trace-trend-table_cell">Span Count</th>
64+
<th width="20" className="trace-trend-table_cell text-right">Total Duration</th>
65+
</tr>
66+
</thead>
67+
<tbody>
68+
{
69+
relatedTracesList.map(relatedTrace => (
70+
<RelatedTracesRow
71+
key={relatedTrace.traceId}
72+
{...relatedTrace}
73+
isUniversalSearch={isUniversalSearch}
74+
/>
75+
))
76+
}
77+
</tbody>
78+
</table>
79+
<span style={{position: 'absolute', marginLeft: '10px', marginTop: '5px'}}> {relatedTraces.length > numDisplayedTraces ? `5 of ${relatedTraces.length} Results` : 'End of Results'}</span>
80+
<div style={{textAlign: 'center', marginTop: '15px'}}>
81+
<a role="button" className="btn btn-default" onClick={this.showMoreTraces} tabIndex="-1">{relatedTraces.length > numDisplayedTraces ? `Show All(${relatedTraces.length}) in Universal` : 'View in Universal Search' }</a>
82+
</div>
83+
</article>
84+
);
85+
}
86+
}
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
/*
2+
* Copyright 2018 Expedia, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
import React from 'react';
17+
import { observer } from 'mobx-react';
18+
import PropTypes from 'prop-types';
19+
20+
import Loading from '../../../common/loading';
21+
import Error from '../../../common/error';
22+
import RelatedTracesTab from './relatedTracesTab';
23+
import { toPresetDisplayText } from '../../utils/presets';
24+
25+
@observer
26+
export default class RelatedTracesTabContainer extends React.Component {
27+
static propTypes = {
28+
traceId: PropTypes.string.isRequired, // eslint-disable-line react/no-unused-prop-types
29+
store: PropTypes.object.isRequired,
30+
isUniversalSearch: PropTypes.bool.isRequired
31+
};
32+
33+
static timePresetOptions = (window.haystackUiConfig && window.haystackUiConfig.tracesTimePresetOptions);
34+
35+
static fieldOptions = (window.haystackUiConfig && window.haystackUiConfig.relatedTracesOptions);
36+
37+
constructor(props) {
38+
super(props);
39+
const selectedFieldIndex = null;
40+
const selectedTimeIndex = 2; // The default time preset is the third
41+
this.state = {
42+
selectedFieldIndex,
43+
selectedTimeIndex,
44+
/**
45+
* The following computes a dictionary tags of all spans of this trace
46+
* This computation relies that the spans have already been calculated in the traceDetailsStore, which happens
47+
* when the Timeline View (which is default) is viewed, and fetchTraceDetails has complete.
48+
*/
49+
tags: this.props.store.tags
50+
};
51+
52+
this.handleTimeChange = this.handleTimeChange.bind(this);
53+
this.handleFieldChange = this.handleFieldChange.bind(this);
54+
this.fetchRelatedTraces = this.fetchRelatedTraces.bind(this);
55+
}
56+
57+
componentWillMount() {
58+
this.fetchRelatedTraces(this.state.selectedTimeIndex);
59+
}
60+
61+
componentDidUpdate(prevProps, prevState) {
62+
if (this.state.selectedTimeIndex !== prevState.selectedTimeIndex
63+
|| this.state.selectedFieldIndex !== prevState.selectedFieldIndex) {
64+
this.fetchRelatedTraces();
65+
}
66+
}
67+
68+
fetchRelatedTraces() {
69+
// If the field is unselected
70+
if (this.state.selectedFieldIndex === null) {
71+
return this.props.store.rejectRelatedTracesPromise('Field is not selected');
72+
}
73+
74+
const chosenField = RelatedTracesTabContainer.fieldOptions[this.state.selectedFieldIndex];
75+
76+
// Rejects API promise if the trace does not have the chosen field
77+
if (!this.state.tags[chosenField.propertyToMatch] && chosenField.propertyToMatch !== 'traceId') {
78+
return this.props.store.rejectRelatedTracesPromise('This trace does not have the chosen field');
79+
}
80+
81+
// Builds Query
82+
const query = {
83+
serviceName: '',
84+
[chosenField.fieldTag]: this.props[chosenField.propertyToMatch] || this.state.tags[chosenField.propertyToMatch],
85+
timePreset: RelatedTracesTabContainer.timePresetOptions[this.state.selectedTimeIndex]
86+
};
87+
88+
return this.props.store.fetchRelatedTraces(query);
89+
}r
90+
91+
handleFieldChange(event) {
92+
const selectedFieldIndex = event.target.value;
93+
this.setState({
94+
selectedFieldIndex
95+
});
96+
}
97+
98+
handleTimeChange(event) {
99+
const selectedTimeIndex = event.target.value;
100+
this.setState({
101+
selectedTimeIndex
102+
});
103+
}
104+
105+
render() {
106+
const { store, isUniversalSearch } = this.props;
107+
const { selectedTimeIndex, selectedFieldIndex} = this.state;
108+
109+
return (
110+
<section>
111+
<div className="text-left">
112+
<span>Relate Traces by: </span>
113+
<select id="field" className="time-range-selector" value={selectedFieldIndex || ''} onChange={this.handleFieldChange}>
114+
{!selectedFieldIndex ? <option key="empty" value="" /> : null}
115+
{RelatedTracesTabContainer.fieldOptions.map((fieldOp, index) => (
116+
<option
117+
key={fieldOp.fieldTag}
118+
value={index}
119+
>{fieldOp.fieldDescription}</option>))}
120+
</select>
121+
<span style={{paddingLeft: '5px'}}>of the </span>
122+
<select id="time" className="time-range-selector" value={selectedTimeIndex} onChange={this.handleTimeChange}>
123+
{RelatedTracesTabContainer.timePresetOptions.map((preset, index) => (
124+
<option
125+
key={preset}
126+
value={index}
127+
>{toPresetDisplayText(preset)}</option>))}
128+
</select>
129+
</div>
130+
{ store.relatedTracesPromiseState && store.relatedTracesPromiseState.case({
131+
pending: () => <Loading />,
132+
rejected: reason => <Error errorMessage={reason}/>,
133+
fulfilled: () => ((store.relatedTraces && store.relatedTraces.length)
134+
? <RelatedTracesTab searchQuery={store.searchQuery} relatedTraces={store.relatedTraces} isUniversalSearch={isUniversalSearch}/>
135+
: <Error errorMessage="No related traces found"/>)
136+
})
137+
}
138+
</section>
139+
);
140+
}
141+
}

0 commit comments

Comments
 (0)