mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[APM] Transaction breakdown charts (#39531)
* [APM] Transaction breakdown graphs Closes #36441. * Don't use .keyword now that mapping is correct * Use better heuristic for determining noHits; wrap App in memo to prevent unnecessary re-renders * Remove nested panel around TransactionBreakdown * Fix display issues in charts * Address review feedback * Address additional review feedback
This commit is contained in:
parent
c8e4f9c0b8
commit
73bb21b771
29 changed files with 19679 additions and 369 deletions
|
@ -56,12 +56,16 @@ exports[`Error SPAN_ID 1`] = `undefined`;
|
|||
|
||||
exports[`Error SPAN_NAME 1`] = `undefined`;
|
||||
|
||||
exports[`Error SPAN_SELF_TIME_SUM 1`] = `undefined`;
|
||||
|
||||
exports[`Error SPAN_SUBTYPE 1`] = `undefined`;
|
||||
|
||||
exports[`Error SPAN_TYPE 1`] = `undefined`;
|
||||
|
||||
exports[`Error TRACE_ID 1`] = `"trace id"`;
|
||||
|
||||
exports[`Error TRANSACTION_BREAKDOWN_COUNT 1`] = `undefined`;
|
||||
|
||||
exports[`Error TRANSACTION_DURATION 1`] = `undefined`;
|
||||
|
||||
exports[`Error TRANSACTION_ID 1`] = `"transaction id"`;
|
||||
|
@ -134,12 +138,16 @@ exports[`Span SPAN_ID 1`] = `"span id"`;
|
|||
|
||||
exports[`Span SPAN_NAME 1`] = `"span name"`;
|
||||
|
||||
exports[`Span SPAN_SELF_TIME_SUM 1`] = `undefined`;
|
||||
|
||||
exports[`Span SPAN_SUBTYPE 1`] = `"my subtype"`;
|
||||
|
||||
exports[`Span SPAN_TYPE 1`] = `"span type"`;
|
||||
|
||||
exports[`Span TRACE_ID 1`] = `"trace id"`;
|
||||
|
||||
exports[`Span TRANSACTION_BREAKDOWN_COUNT 1`] = `undefined`;
|
||||
|
||||
exports[`Span TRANSACTION_DURATION 1`] = `undefined`;
|
||||
|
||||
exports[`Span TRANSACTION_ID 1`] = `"transaction id"`;
|
||||
|
@ -212,12 +220,16 @@ exports[`Transaction SPAN_ID 1`] = `undefined`;
|
|||
|
||||
exports[`Transaction SPAN_NAME 1`] = `undefined`;
|
||||
|
||||
exports[`Transaction SPAN_SELF_TIME_SUM 1`] = `undefined`;
|
||||
|
||||
exports[`Transaction SPAN_SUBTYPE 1`] = `undefined`;
|
||||
|
||||
exports[`Transaction SPAN_TYPE 1`] = `undefined`;
|
||||
|
||||
exports[`Transaction TRACE_ID 1`] = `"trace id"`;
|
||||
|
||||
exports[`Transaction TRANSACTION_BREAKDOWN_COUNT 1`] = `undefined`;
|
||||
|
||||
exports[`Transaction TRANSACTION_DURATION 1`] = `1337`;
|
||||
|
||||
exports[`Transaction TRANSACTION_ID 1`] = `"transaction id"`;
|
||||
|
|
|
@ -20,12 +20,14 @@ export const TRANSACTION_RESULT = 'transaction.result';
|
|||
export const TRANSACTION_NAME = 'transaction.name';
|
||||
export const TRANSACTION_ID = 'transaction.id';
|
||||
export const TRANSACTION_SAMPLED = 'transaction.sampled';
|
||||
export const TRANSACTION_BREAKDOWN_COUNT = 'transaction.breakdown.count';
|
||||
|
||||
export const TRACE_ID = 'trace.id';
|
||||
|
||||
export const SPAN_DURATION = 'span.duration.us';
|
||||
export const SPAN_TYPE = 'span.type';
|
||||
export const SPAN_SUBTYPE = 'span.subtype';
|
||||
export const SPAN_SELF_TIME_SUM = 'span.self_time.sum.us';
|
||||
export const SPAN_ACTION = 'span.action';
|
||||
export const SPAN_NAME = 'span.name';
|
||||
export const SPAN_ID = 'span.id';
|
||||
|
|
|
@ -18,6 +18,7 @@ import { useLocation } from '../../../hooks/useLocation';
|
|||
import { useUrlParams } from '../../../hooks/useUrlParams';
|
||||
import { FETCH_STATUS } from '../../../hooks/useFetcher';
|
||||
import { TransactionBreakdown } from '../../shared/TransactionBreakdown';
|
||||
import { ChartsTimeContextProvider } from '../../../context/ChartsTimeContext';
|
||||
|
||||
export function TransactionDetails() {
|
||||
const location = useLocation();
|
||||
|
@ -42,16 +43,18 @@ export function TransactionDetails() {
|
|||
</EuiTitle>
|
||||
</ApmHeader>
|
||||
|
||||
<TransactionBreakdown />
|
||||
<ChartsTimeContextProvider>
|
||||
<TransactionBreakdown />
|
||||
|
||||
<EuiSpacer size="s" />
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<TransactionCharts
|
||||
hasMLJob={false}
|
||||
charts={transactionDetailsChartsData}
|
||||
urlParams={urlParams}
|
||||
location={location}
|
||||
/>
|
||||
<TransactionCharts
|
||||
hasMLJob={false}
|
||||
charts={transactionDetailsChartsData}
|
||||
urlParams={urlParams}
|
||||
location={location}
|
||||
/>
|
||||
</ChartsTimeContextProvider>
|
||||
|
||||
<EuiHorizontalRule size="full" margin="l" />
|
||||
|
||||
|
|
|
@ -27,6 +27,7 @@ import { useFetcher } from '../../../hooks/useFetcher';
|
|||
import { getHasMLJob } from '../../../services/rest/ml';
|
||||
import { history } from '../../../utils/history';
|
||||
import { useLocation } from '../../../hooks/useLocation';
|
||||
import { ChartsTimeContextProvider } from '../../../context/ChartsTimeContext';
|
||||
|
||||
interface Props {
|
||||
urlParams: IUrlParams;
|
||||
|
@ -114,16 +115,18 @@ export function TransactionOverview({
|
|||
</EuiFormRow>
|
||||
) : null}
|
||||
|
||||
<TransactionBreakdown />
|
||||
<ChartsTimeContextProvider>
|
||||
<TransactionBreakdown initialIsOpen={true} />
|
||||
|
||||
<EuiSpacer size="s" />
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<TransactionCharts
|
||||
hasMLJob={hasMLJob}
|
||||
charts={transactionOverviewCharts}
|
||||
location={location}
|
||||
urlParams={urlParams}
|
||||
/>
|
||||
<TransactionCharts
|
||||
hasMLJob={hasMLJob}
|
||||
charts={transactionOverviewCharts}
|
||||
location={location}
|
||||
urlParams={urlParams}
|
||||
/>
|
||||
</ChartsTimeContextProvider>
|
||||
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
|
|
|
@ -4,10 +4,62 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { useMemo, useCallback } from 'react';
|
||||
import numeral from '@elastic/numeral';
|
||||
import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n';
|
||||
import { Coordinate } from '../../../../../typings/timeseries';
|
||||
import { TransactionLineChart } from '../../charts/TransactionCharts/TransactionLineChart';
|
||||
import { asPercent } from '../../../../utils/formatters';
|
||||
import { unit } from '../../../../style/variables';
|
||||
|
||||
const TransactionBreakdownGraph: React.FC<{}> = () => {
|
||||
return <div />;
|
||||
interface Props {
|
||||
timeseries: Array<{
|
||||
name: string;
|
||||
color: string;
|
||||
values: Array<{ x: number; y: number | null }>;
|
||||
}>;
|
||||
}
|
||||
|
||||
const TransactionBreakdownGraph: React.FC<Props> = props => {
|
||||
const { timeseries } = props;
|
||||
|
||||
const series: React.ComponentProps<
|
||||
typeof TransactionLineChart
|
||||
>['series'] = useMemo(
|
||||
() => {
|
||||
return timeseries.map(timeseriesConfig => {
|
||||
return {
|
||||
title: timeseriesConfig.name,
|
||||
color: timeseriesConfig.color,
|
||||
data: timeseriesConfig.values,
|
||||
type: 'area',
|
||||
hideLegend: true
|
||||
};
|
||||
}, {});
|
||||
},
|
||||
[timeseries]
|
||||
);
|
||||
|
||||
const tickFormatY = useCallback((y: number | null) => {
|
||||
return numeral(y || 0).format('0 %');
|
||||
}, []);
|
||||
|
||||
const formatTooltipValue = useCallback((coordinate: Coordinate) => {
|
||||
return coordinate.y !== null && coordinate.y !== undefined
|
||||
? asPercent(coordinate.y, 1)
|
||||
: NOT_AVAILABLE_LABEL;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<TransactionLineChart
|
||||
series={series}
|
||||
stacked={true}
|
||||
tickFormatY={tickFormatY}
|
||||
formatTooltipValue={formatTooltipValue}
|
||||
yMax={1}
|
||||
height={unit * 12}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export { TransactionBreakdownGraph };
|
||||
|
|
|
@ -19,7 +19,6 @@ import { FORMATTERS } from '../../../../../infra/public/utils/formatters';
|
|||
interface TransactionBreakdownKpi {
|
||||
name: string;
|
||||
percentage: number;
|
||||
count: number;
|
||||
color: string;
|
||||
}
|
||||
|
||||
|
|
|
@ -12,7 +12,6 @@ import {
|
|||
EuiSpacer,
|
||||
EuiPanel
|
||||
} from '@elastic/eui';
|
||||
import { sortBy } from 'lodash';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import styled from 'styled-components';
|
||||
import { useTransactionBreakdown } from '../../../hooks/useTransactionBreakdown';
|
||||
|
@ -38,8 +37,10 @@ const NoTransactionsTitle = styled.span`
|
|||
font-weight: bold;
|
||||
`;
|
||||
|
||||
const TransactionBreakdown: React.FC = () => {
|
||||
const [showChart, setShowChart] = useState(false);
|
||||
const TransactionBreakdown: React.FC<{
|
||||
initialIsOpen?: boolean;
|
||||
}> = ({ initialIsOpen }) => {
|
||||
const [showChart, setShowChart] = useState(!!initialIsOpen);
|
||||
|
||||
const {
|
||||
data,
|
||||
|
@ -47,23 +48,65 @@ const TransactionBreakdown: React.FC = () => {
|
|||
receivedDataDuringLifetime
|
||||
} = useTransactionBreakdown();
|
||||
|
||||
const kpis = useMemo(
|
||||
const kpis = data ? data.kpis : undefined;
|
||||
const timeseriesPerSubtype = data ? data.timeseries_per_subtype : undefined;
|
||||
|
||||
const legends = useMemo(
|
||||
() => {
|
||||
return data
|
||||
? sortBy(data, 'name').map((breakdown, index) => {
|
||||
return {
|
||||
...breakdown,
|
||||
color: COLORS[index % COLORS.length]
|
||||
};
|
||||
})
|
||||
: null;
|
||||
const names = kpis ? kpis.map(kpi => kpi.name).sort() : [];
|
||||
|
||||
return names.map((name, index) => {
|
||||
return {
|
||||
name,
|
||||
color: COLORS[index % COLORS.length]
|
||||
};
|
||||
});
|
||||
},
|
||||
[data]
|
||||
[kpis]
|
||||
);
|
||||
|
||||
const sortedAndColoredKpis = useMemo(
|
||||
() => {
|
||||
if (!kpis) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return legends.map(legend => {
|
||||
const { color } = legend;
|
||||
|
||||
const breakdown = kpis.find(
|
||||
b => b.name === legend.name
|
||||
) as typeof kpis[0];
|
||||
|
||||
return {
|
||||
...breakdown,
|
||||
color
|
||||
};
|
||||
});
|
||||
},
|
||||
[kpis, legends]
|
||||
);
|
||||
|
||||
const loading = status === FETCH_STATUS.LOADING || status === undefined;
|
||||
|
||||
const hasHits = data && data.length > 0;
|
||||
const hasHits = data && data.kpis.length > 0;
|
||||
const timeseries = useMemo(
|
||||
() => {
|
||||
if (!timeseriesPerSubtype) {
|
||||
return [];
|
||||
}
|
||||
return legends.map(legend => {
|
||||
const series = timeseriesPerSubtype[legend.name];
|
||||
|
||||
return {
|
||||
name: legend.name,
|
||||
values: series,
|
||||
color: legend.color
|
||||
};
|
||||
});
|
||||
},
|
||||
[timeseriesPerSubtype, legends]
|
||||
);
|
||||
|
||||
return receivedDataDuringLifetime ? (
|
||||
<EuiPanel>
|
||||
|
@ -77,9 +120,11 @@ const TransactionBreakdown: React.FC = () => {
|
|||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
{hasHits && kpis ? (
|
||||
{hasHits && sortedAndColoredKpis ? (
|
||||
<EuiFlexItem>
|
||||
{kpis && <TransactionBreakdownKpiList kpis={kpis} />}
|
||||
{sortedAndColoredKpis && (
|
||||
<TransactionBreakdownKpiList kpis={sortedAndColoredKpis} />
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
) : (
|
||||
!loading && (
|
||||
|
@ -114,7 +159,7 @@ const TransactionBreakdown: React.FC = () => {
|
|||
)}
|
||||
{showChart && hasHits ? (
|
||||
<EuiFlexItem>
|
||||
<TransactionBreakdownGraph />
|
||||
<TransactionBreakdownGraph timeseries={timeseries} />
|
||||
</EuiFlexItem>
|
||||
) : null}
|
||||
</EuiFlexGroup>
|
||||
|
|
|
@ -19,18 +19,22 @@ function getPointByX(serie, x) {
|
|||
|
||||
class InteractivePlot extends PureComponent {
|
||||
getMarkPoints = hoverX => {
|
||||
return this.props.series
|
||||
.filter(serie =>
|
||||
serie.data.some(point => point.x === hoverX && point.y != null)
|
||||
)
|
||||
.map(serie => {
|
||||
const { x, y } = getPointByX(serie, hoverX) || {};
|
||||
return {
|
||||
x,
|
||||
y,
|
||||
color: serie.color
|
||||
};
|
||||
});
|
||||
return (
|
||||
this.props.series
|
||||
.filter(serie =>
|
||||
serie.data.some(point => point.x === hoverX && point.y != null)
|
||||
)
|
||||
.map(serie => {
|
||||
const { x, y } = getPointByX(serie, hoverX) || {};
|
||||
return {
|
||||
x,
|
||||
y,
|
||||
color: serie.color
|
||||
};
|
||||
})
|
||||
// needs to be reversed, as StaticPlot.js does the same
|
||||
.reverse()
|
||||
);
|
||||
};
|
||||
|
||||
getTooltipPoints = hoverX => {
|
||||
|
|
|
@ -42,6 +42,7 @@ class StaticPlot extends PureComponent {
|
|||
curve={'curveMonotoneX'}
|
||||
data={serie.data}
|
||||
color={serie.color}
|
||||
stack={serie.stack}
|
||||
/>
|
||||
);
|
||||
case 'area':
|
||||
|
@ -53,8 +54,9 @@ class StaticPlot extends PureComponent {
|
|||
curve={'curveMonotoneX'}
|
||||
data={serie.data}
|
||||
color={serie.color}
|
||||
stroke={serie.color}
|
||||
stroke={serie.stack ? 'rgba(0,0,0,0)' : serie.color}
|
||||
fill={serie.areaColor || rgba(serie.color, 0.3)}
|
||||
stack={serie.stack}
|
||||
/>
|
||||
);
|
||||
case 'areaMaxHeight':
|
||||
|
|
|
@ -33,15 +33,32 @@ export class InnerCustomPlot extends PureComponent {
|
|||
getEnabledSeries = createSelector(
|
||||
state => state.visibleSeries,
|
||||
state => state.seriesEnabledState,
|
||||
(visibleSeries, seriesEnabledState) =>
|
||||
visibleSeries.filter((serie, i) => !seriesEnabledState[i])
|
||||
state => state.stacked,
|
||||
(visibleSeries, seriesEnabledState, stacked) =>
|
||||
visibleSeries
|
||||
.filter((serie, i) => !seriesEnabledState[i])
|
||||
.map(serie => {
|
||||
return stacked ? { ...serie, stack: true } : serie;
|
||||
})
|
||||
);
|
||||
|
||||
getOptions = createSelector(
|
||||
state => state.width,
|
||||
state => state.yMin,
|
||||
state => state.yMax,
|
||||
(width, yMin, yMax) => ({ width, yMin, yMax })
|
||||
state => state.start,
|
||||
state => state.end,
|
||||
state => state.height,
|
||||
state => state.stacked,
|
||||
(width, yMin, yMax, start, end, height, stacked) => ({
|
||||
width,
|
||||
yMin,
|
||||
yMax,
|
||||
start,
|
||||
end,
|
||||
height,
|
||||
stacked
|
||||
})
|
||||
);
|
||||
|
||||
getPlotValues = createSelector(
|
||||
|
@ -61,6 +78,18 @@ export class InnerCustomPlot extends PureComponent {
|
|||
}
|
||||
);
|
||||
|
||||
getStackedPlotSeries = createSelector(
|
||||
state => state.enabledSeries,
|
||||
series => {
|
||||
return series.map(serie => {
|
||||
return {
|
||||
...serie,
|
||||
type: 'line'
|
||||
};
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
clickLegend = i => {
|
||||
this.setState(({ seriesEnabledState }) => {
|
||||
const nextSeriesEnabledState = this.props.series.map((value, _i) => {
|
||||
|
@ -113,7 +142,7 @@ export class InnerCustomPlot extends PureComponent {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { series, truncateLegends, noHits, width } = this.props;
|
||||
const { series, truncateLegends, noHits, width, stacked } = this.props;
|
||||
|
||||
if (isEmpty(series) || !width) {
|
||||
return null;
|
||||
|
@ -126,7 +155,8 @@ export class InnerCustomPlot extends PureComponent {
|
|||
const visibleSeries = this.getVisibleSeries({ series });
|
||||
const enabledSeries = this.getEnabledSeries({
|
||||
visibleSeries,
|
||||
seriesEnabledState: this.state.seriesEnabledState
|
||||
seriesEnabledState: this.state.seriesEnabledState,
|
||||
stacked
|
||||
});
|
||||
const options = this.getOptions(this.props);
|
||||
|
||||
|
@ -140,6 +170,16 @@ export class InnerCustomPlot extends PureComponent {
|
|||
return null;
|
||||
}
|
||||
|
||||
const staticPlot = (
|
||||
<StaticPlot
|
||||
noHits={noHits}
|
||||
plotValues={plotValues}
|
||||
series={enabledSeries}
|
||||
tickFormatY={this.props.tickFormatY}
|
||||
tickFormatX={this.props.tickFormatX}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div style={{ position: 'relative', height: plotValues.XY_HEIGHT }}>
|
||||
|
@ -151,6 +191,12 @@ export class InnerCustomPlot extends PureComponent {
|
|||
tickFormatX={this.props.tickFormatX}
|
||||
/>
|
||||
|
||||
{stacked
|
||||
? React.cloneElement(staticPlot, {
|
||||
series: this.getStackedPlotSeries({ enabledSeries })
|
||||
})
|
||||
: null}
|
||||
|
||||
<InteractivePlot
|
||||
plotValues={plotValues}
|
||||
hoverX={this.props.hoverX}
|
||||
|
@ -193,14 +239,17 @@ InnerCustomPlot.propTypes = {
|
|||
series: PropTypes.array.isRequired,
|
||||
tickFormatY: PropTypes.func,
|
||||
truncateLegends: PropTypes.bool,
|
||||
width: PropTypes.number.isRequired
|
||||
width: PropTypes.number.isRequired,
|
||||
stacked: PropTypes.bool,
|
||||
height: PropTypes.number
|
||||
};
|
||||
|
||||
InnerCustomPlot.defaultProps = {
|
||||
formatTooltipValue: p => p.y,
|
||||
tickFormatX: undefined,
|
||||
tickFormatY: y => y,
|
||||
truncateLegends: false
|
||||
truncateLegends: false,
|
||||
stacked: false
|
||||
};
|
||||
|
||||
export default makeWidthFlexible(InnerCustomPlot);
|
||||
|
|
|
@ -46,7 +46,7 @@ function getFlattenedCoordinates(visibleSeries, enabledSeries) {
|
|||
export function getPlotValues(
|
||||
visibleSeries,
|
||||
enabledSeries,
|
||||
{ width, yMin = 0, yMax = 'max' }
|
||||
{ width, yMin = 0, yMax = 'max', start, end, height, stacked }
|
||||
) {
|
||||
const flattenedCoordinates = getFlattenedCoordinates(
|
||||
visibleSeries,
|
||||
|
@ -56,14 +56,17 @@ export function getPlotValues(
|
|||
return null;
|
||||
}
|
||||
|
||||
const xMin = d3.min(flattenedCoordinates, d => d.x);
|
||||
const xMax = d3.max(flattenedCoordinates, d => d.x);
|
||||
const xMin = start ? start : d3.min(flattenedCoordinates, d => d.x);
|
||||
|
||||
const xMax = end ? end : d3.max(flattenedCoordinates, d => d.x);
|
||||
|
||||
if (yMax === 'max') {
|
||||
yMax = d3.max(flattenedCoordinates, d => d.y);
|
||||
}
|
||||
if (yMin === 'min') {
|
||||
yMin = d3.min(flattenedCoordinates, d => d.y);
|
||||
}
|
||||
|
||||
const xScale = getXScale(xMin, xMax, width);
|
||||
const yScale = getYScale(yMin, yMax);
|
||||
|
||||
|
@ -75,22 +78,26 @@ export function getPlotValues(
|
|||
y: yScale,
|
||||
yTickValues,
|
||||
XY_MARGIN,
|
||||
XY_HEIGHT,
|
||||
XY_WIDTH: width
|
||||
XY_HEIGHT: height || XY_HEIGHT,
|
||||
XY_WIDTH: width,
|
||||
...(stacked ? { stackBy: 'y' } : {})
|
||||
};
|
||||
}
|
||||
|
||||
export function SharedPlot({ plotValues, ...props }) {
|
||||
const { XY_HEIGHT: height, XY_MARGIN: margin, XY_WIDTH: width } = plotValues;
|
||||
|
||||
return (
|
||||
<div style={{ position: 'absolute', top: 0, left: 0 }}>
|
||||
<XYPlot
|
||||
dontCheckIfEmpty
|
||||
height={XY_HEIGHT}
|
||||
margin={XY_MARGIN}
|
||||
height={height}
|
||||
margin={margin}
|
||||
xType="time"
|
||||
width={plotValues.XY_WIDTH}
|
||||
width={width}
|
||||
xDomain={plotValues.x.domain()}
|
||||
yDomain={plotValues.y.domain()}
|
||||
stackBy={plotValues.stackBy}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
|
@ -101,6 +108,7 @@ SharedPlot.propTypes = {
|
|||
plotValues: PropTypes.shape({
|
||||
x: PropTypes.func.isRequired,
|
||||
y: PropTypes.func.isRequired,
|
||||
XY_WIDTH: PropTypes.number.isRequired
|
||||
XY_WIDTH: PropTypes.number.isRequired,
|
||||
height: PropTypes.number
|
||||
}).isRequired
|
||||
};
|
||||
|
|
|
@ -2903,6 +2903,7 @@ Array [
|
|||
.c4 {
|
||||
color: #98a2b3;
|
||||
padding-bottom: 0;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.c7 {
|
||||
|
@ -5002,7 +5003,7 @@ Array [
|
|||
>
|
||||
<circle
|
||||
cx={360}
|
||||
cy={189.74989696}
|
||||
cy={132.27231180799998}
|
||||
onClick={[Function]}
|
||||
onContextMenu={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
|
@ -5010,9 +5011,9 @@ Array [
|
|||
r={5}
|
||||
style={
|
||||
Object {
|
||||
"fill": "#3185fc",
|
||||
"fill": "#f98510",
|
||||
"opacity": 1,
|
||||
"stroke": "#3185fc",
|
||||
"stroke": "#f98510",
|
||||
"strokeWidth": 1,
|
||||
}
|
||||
}
|
||||
|
@ -5036,7 +5037,7 @@ Array [
|
|||
/>
|
||||
<circle
|
||||
cx={360}
|
||||
cy={132.27231180799998}
|
||||
cy={189.74989696}
|
||||
onClick={[Function]}
|
||||
onContextMenu={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
|
@ -5044,9 +5045,9 @@ Array [
|
|||
r={5}
|
||||
style={
|
||||
Object {
|
||||
"fill": "#f98510",
|
||||
"fill": "#3185fc",
|
||||
"opacity": 1,
|
||||
"stroke": "#f98510",
|
||||
"stroke": "#3185fc",
|
||||
"strokeWidth": 1,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -61,6 +61,7 @@ const LegendContainer = styled.div`
|
|||
const LegendGray = styled(Legend)`
|
||||
color: ${theme.euiColorMediumShade};
|
||||
padding-bottom: 0;
|
||||
padding-right: ${px(units.half)};
|
||||
`;
|
||||
|
||||
const Value = styled.div`
|
||||
|
|
|
@ -0,0 +1,112 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import { flatten } from 'lodash';
|
||||
import d3 from 'd3';
|
||||
import {
|
||||
Coordinate,
|
||||
RectCoordinate
|
||||
} from '../../../../../../typings/timeseries';
|
||||
import { useChartsTime } from '../../../../../hooks/useChartsTime';
|
||||
import { getEmptySeries } from '../../CustomPlot/getEmptySeries';
|
||||
// @ts-ignore
|
||||
import CustomPlot from '../../CustomPlot';
|
||||
import { toQuery, fromQuery } from '../../../Links/url_helpers';
|
||||
import { history } from '../../../../../utils/history';
|
||||
|
||||
interface Props {
|
||||
series: Array<{
|
||||
color: string;
|
||||
title: React.ReactNode;
|
||||
titleShort?: React.ReactNode;
|
||||
data: Array<Coordinate | RectCoordinate>;
|
||||
type: string;
|
||||
}>;
|
||||
stacked?: boolean;
|
||||
truncateLegends?: boolean;
|
||||
tickFormatY: (y: number | null) => React.ReactNode;
|
||||
formatTooltipValue: (c: Coordinate) => React.ReactNode;
|
||||
yMax?: string | number;
|
||||
height?: number;
|
||||
}
|
||||
|
||||
const TransactionLineChart: React.FC<Props> = (props: Props) => {
|
||||
const {
|
||||
series,
|
||||
tickFormatY,
|
||||
formatTooltipValue,
|
||||
stacked = false,
|
||||
yMax = 'max',
|
||||
height,
|
||||
truncateLegends
|
||||
} = props;
|
||||
|
||||
const flattenedCoordinates = flatten(series.map(serie => serie.data));
|
||||
|
||||
const start = d3.min(flattenedCoordinates, d => d.x);
|
||||
const end = d3.max(flattenedCoordinates, d => d.x);
|
||||
|
||||
const noHits = series.every(
|
||||
serie =>
|
||||
serie.data.filter(value => 'y' in value && value.y !== null).length === 0
|
||||
);
|
||||
const { time, setTime } = useChartsTime();
|
||||
|
||||
const hoverXHandlers = useMemo(
|
||||
() => {
|
||||
return {
|
||||
onHover: (hoverX: number) => {
|
||||
setTime(hoverX);
|
||||
},
|
||||
onMouseLeave: () => {
|
||||
setTime(null);
|
||||
},
|
||||
onSelectionEnd: (range: { start: number; end: number }) => {
|
||||
setTime(null);
|
||||
|
||||
const currentSearch = toQuery(history.location.search);
|
||||
const nextSearch = {
|
||||
rangeFrom: new Date(range.start).toISOString(),
|
||||
rangeTo: new Date(range.end).toISOString()
|
||||
};
|
||||
|
||||
history.push({
|
||||
...history.location,
|
||||
search: fromQuery({
|
||||
...currentSearch,
|
||||
...nextSearch
|
||||
})
|
||||
});
|
||||
},
|
||||
hoverX: time
|
||||
};
|
||||
},
|
||||
[time, setTime]
|
||||
);
|
||||
|
||||
if (!start || !end) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<CustomPlot
|
||||
noHits={noHits}
|
||||
series={noHits ? getEmptySeries(start, end) : series}
|
||||
start={new Date(start).getTime()}
|
||||
end={new Date(end).getTime()}
|
||||
{...hoverXHandlers}
|
||||
tickFormatY={tickFormatY}
|
||||
formatTooltipValue={formatTooltipValue}
|
||||
stacked={stacked}
|
||||
yMax={yMax}
|
||||
height={height}
|
||||
truncateLegends={truncateLegends}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export { TransactionLineChart };
|
|
@ -23,11 +23,8 @@ import { ITransactionChartData } from '../../../../selectors/chartSelectors';
|
|||
import { IUrlParams } from '../../../../context/UrlParamsContext/types';
|
||||
import { asInteger, asMillis, tpmUnit } from '../../../../utils/formatters';
|
||||
import { MLJobLink } from '../../Links/MachineLearningLinks/MLJobLink';
|
||||
// @ts-ignore
|
||||
import CustomPlot from '../CustomPlot';
|
||||
import { SyncChartGroup } from '../SyncChartGroup';
|
||||
import { LicenseContext } from '../../../../context/LicenseContext';
|
||||
import { getEmptySeries } from '../CustomPlot/getEmptySeries';
|
||||
import { TransactionLineChart } from './TransactionLineChart';
|
||||
|
||||
interface TransactionChartProps {
|
||||
hasMLJob: boolean;
|
||||
|
@ -56,7 +53,7 @@ const msTimeUnitLabel = i18n.translate(
|
|||
);
|
||||
|
||||
export class TransactionCharts extends Component<TransactionChartProps> {
|
||||
public getResponseTimeTickFormatter = (t: number) => {
|
||||
public getResponseTimeTickFormatter = (t: number | null) => {
|
||||
return this.props.charts.noHits ? `- ${msTimeUnitLabel}` : asMillis(t);
|
||||
};
|
||||
|
||||
|
@ -134,61 +131,51 @@ export class TransactionCharts extends Component<TransactionChartProps> {
|
|||
|
||||
public render() {
|
||||
const { charts, urlParams } = this.props;
|
||||
const { noHits, responseTimeSeries, tpmSeries } = charts;
|
||||
const { transactionType, start, end } = urlParams;
|
||||
const { responseTimeSeries, tpmSeries } = charts;
|
||||
const { transactionType } = urlParams;
|
||||
|
||||
return (
|
||||
<SyncChartGroup
|
||||
render={hoverXHandlers => (
|
||||
<EuiFlexGrid columns={2} gutterSize="s">
|
||||
<EuiFlexItem>
|
||||
<EuiPanel>
|
||||
<React.Fragment>
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem>
|
||||
<EuiTitle size="xs">
|
||||
<span>{responseTimeLabel(transactionType)}</span>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<LicenseContext.Consumer>
|
||||
{license =>
|
||||
this.renderMLHeader(license.features.ml.is_available)
|
||||
}
|
||||
</LicenseContext.Consumer>
|
||||
</EuiFlexGroup>
|
||||
<CustomPlot
|
||||
noHits={noHits}
|
||||
series={
|
||||
noHits ? getEmptySeries(start, end) : responseTimeSeries
|
||||
}
|
||||
{...hoverXHandlers}
|
||||
tickFormatY={this.getResponseTimeTickFormatter}
|
||||
formatTooltipValue={this.getResponseTimeTooltipFormatter}
|
||||
/>
|
||||
</React.Fragment>
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem style={{ flexShrink: 1 }}>
|
||||
<EuiPanel>
|
||||
<React.Fragment>
|
||||
<EuiFlexGrid columns={2} gutterSize="s">
|
||||
<EuiFlexItem>
|
||||
<EuiPanel>
|
||||
<React.Fragment>
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem>
|
||||
<EuiTitle size="xs">
|
||||
<span>{tpmLabel(transactionType)}</span>
|
||||
<span>{responseTimeLabel(transactionType)}</span>
|
||||
</EuiTitle>
|
||||
<CustomPlot
|
||||
noHits={noHits}
|
||||
series={noHits ? getEmptySeries(start, end) : tpmSeries}
|
||||
{...hoverXHandlers}
|
||||
tickFormatY={this.getTPMFormatter}
|
||||
formatTooltipValue={this.getTPMTooltipFormatter}
|
||||
truncateLegends
|
||||
/>
|
||||
</React.Fragment>
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGrid>
|
||||
)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<LicenseContext.Consumer>
|
||||
{license =>
|
||||
this.renderMLHeader(license.features.ml.is_available)
|
||||
}
|
||||
</LicenseContext.Consumer>
|
||||
</EuiFlexGroup>
|
||||
<TransactionLineChart
|
||||
series={responseTimeSeries}
|
||||
tickFormatY={this.getResponseTimeTickFormatter}
|
||||
formatTooltipValue={this.getResponseTimeTooltipFormatter}
|
||||
/>
|
||||
</React.Fragment>
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem style={{ flexShrink: 1 }}>
|
||||
<EuiPanel>
|
||||
<React.Fragment>
|
||||
<EuiTitle size="xs">
|
||||
<span>{tpmLabel(transactionType)}</span>
|
||||
</EuiTitle>
|
||||
<TransactionLineChart
|
||||
series={tpmSeries}
|
||||
tickFormatY={this.getTPMFormatter}
|
||||
formatTooltipValue={this.getTPMTooltipFormatter}
|
||||
truncateLegends
|
||||
/>
|
||||
</React.Fragment>
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGrid>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { useMemo, useState } from 'react';
|
||||
|
||||
const ChartsTimeContext = React.createContext<{
|
||||
time: number | null;
|
||||
setTime: (time: number | null) => unknown;
|
||||
} | null>(null);
|
||||
|
||||
const ChartsTimeContextProvider: React.FC = ({ children }) => {
|
||||
const [time, setTime] = useState<number | null>(null);
|
||||
|
||||
const value = useMemo(
|
||||
() => {
|
||||
return {
|
||||
time,
|
||||
setTime
|
||||
};
|
||||
},
|
||||
[time, setTime]
|
||||
);
|
||||
|
||||
return <ChartsTimeContext.Provider value={value} children={children} />;
|
||||
};
|
||||
|
||||
export { ChartsTimeContext, ChartsTimeContextProvider };
|
18
x-pack/legacy/plugins/apm/public/hooks/useChartsTime.tsx
Normal file
18
x-pack/legacy/plugins/apm/public/hooks/useChartsTime.tsx
Normal file
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { useContext } from 'react';
|
||||
import { ChartsTimeContext } from '../context/ChartsTimeContext';
|
||||
|
||||
export function useChartsTime() {
|
||||
const context = useContext(ChartsTimeContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error('Missing ChartsTime context provider');
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
|
@ -36,7 +36,7 @@ export function useTransactionBreakdown() {
|
|||
|
||||
const receivedDataDuringLifetime = useRef(false);
|
||||
|
||||
if (data && data.length) {
|
||||
if (data && data.kpis.length) {
|
||||
receivedDataDuringLifetime.current = true;
|
||||
}
|
||||
|
||||
|
|
|
@ -29,7 +29,7 @@ const MainContainer = styled.div`
|
|||
min-height: calc(100vh - ${topNavHeight});
|
||||
`;
|
||||
|
||||
function App() {
|
||||
const App = () => {
|
||||
useUpdateBadgeEffect();
|
||||
|
||||
return (
|
||||
|
@ -51,7 +51,7 @@ function App() {
|
|||
</UrlParamsProvider>
|
||||
</MatchedRouteProvider>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export class Plugin {
|
||||
public start(core: CoreStart) {
|
||||
|
|
|
@ -12,7 +12,11 @@ import { rgba } from 'polished';
|
|||
import { TimeSeriesAPIResponse } from '../../server/lib/transactions/charts';
|
||||
import { ApmTimeSeriesResponse } from '../../server/lib/transactions/charts/get_timeseries_data/transform';
|
||||
import { StringMap } from '../../typings/common';
|
||||
import { Coordinate, RectCoordinate } from '../../typings/timeseries';
|
||||
import {
|
||||
Coordinate,
|
||||
RectCoordinate,
|
||||
TimeSeries
|
||||
} from '../../typings/timeseries';
|
||||
import { asDecimal, asMillis, tpmUnit } from '../utils/formatters';
|
||||
import { IUrlParams } from '../context/UrlParamsContext/types';
|
||||
|
||||
|
@ -27,7 +31,7 @@ export interface ITpmBucket {
|
|||
export interface ITransactionChartData {
|
||||
noHits: boolean;
|
||||
tpmSeries: ITpmBucket[];
|
||||
responseTimeSeries: TimeSerie[];
|
||||
responseTimeSeries: TimeSeries[];
|
||||
}
|
||||
|
||||
const INITIAL_DATA = {
|
||||
|
@ -63,18 +67,6 @@ export function getTransactionCharts(
|
|||
};
|
||||
}
|
||||
|
||||
interface TimeSerie {
|
||||
title: string;
|
||||
titleShort?: string;
|
||||
hideLegend?: boolean;
|
||||
hideTooltipValue?: boolean;
|
||||
data: Array<Coordinate | RectCoordinate>;
|
||||
legendValue?: string;
|
||||
type: string;
|
||||
color: string;
|
||||
areaColor?: string;
|
||||
}
|
||||
|
||||
export function getResponseTimeSeries({
|
||||
apmTimeseries,
|
||||
anomalyTimeseries
|
||||
|
@ -82,7 +74,7 @@ export function getResponseTimeSeries({
|
|||
const { overallAvgDuration } = apmTimeseries;
|
||||
const { avg, p95, p99 } = apmTimeseries.responseTimes;
|
||||
|
||||
const series: TimeSerie[] = [
|
||||
const series: TimeSeries[] = [
|
||||
{
|
||||
title: i18n.translate('xpack.apm.transactions.chart.averageLabel', {
|
||||
defaultMessage: 'Avg.'
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`getTransactionBreakdown returns transaction breakdowns grouped by type and subtype 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"count": 15,
|
||||
"name": "app",
|
||||
"percentage": 0.6666666666666666,
|
||||
},
|
||||
Object {
|
||||
"count": 175,
|
||||
"name": "mysql",
|
||||
"percentage": 0.3333333333333333,
|
||||
},
|
||||
Object {
|
||||
"count": 225,
|
||||
"name": "elasticsearch",
|
||||
"percentage": 0.16666666666666666,
|
||||
},
|
||||
]
|
||||
`;
|
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export const MAX_KPIS = 20;
|
|
@ -5,8 +5,9 @@
|
|||
*/
|
||||
|
||||
import { getTransactionBreakdown } from '.';
|
||||
import { noDataResponse } from './mock-responses/noData';
|
||||
import { dataResponse } from './mock-responses/data';
|
||||
import * as constants from './constants';
|
||||
import noDataResponse from './mock-responses/noData.json';
|
||||
import dataResponse from './mock-responses/data.json';
|
||||
|
||||
describe('getTransactionBreakdown', () => {
|
||||
it('returns an empty array if no data is available', async () => {
|
||||
|
@ -26,7 +27,9 @@ describe('getTransactionBreakdown', () => {
|
|||
}
|
||||
});
|
||||
|
||||
expect(response.length).toBe(0);
|
||||
expect(response.kpis.length).toBe(0);
|
||||
|
||||
expect(Object.keys(response.timeseries_per_subtype).length).toBe(0);
|
||||
});
|
||||
|
||||
it('returns transaction breakdowns grouped by type and subtype', async () => {
|
||||
|
@ -46,26 +49,78 @@ describe('getTransactionBreakdown', () => {
|
|||
}
|
||||
});
|
||||
|
||||
expect(response.length).toBe(3);
|
||||
expect(response.kpis.length).toBe(4);
|
||||
|
||||
expect(response.map(breakdown => breakdown.name)).toEqual([
|
||||
expect(response.kpis.map(kpi => kpi.name)).toEqual([
|
||||
'app',
|
||||
'mysql',
|
||||
'elasticsearch'
|
||||
'http',
|
||||
'postgresql',
|
||||
'dispatcher-servlet'
|
||||
]);
|
||||
|
||||
expect(response[0]).toEqual({
|
||||
count: 15,
|
||||
expect(response.kpis[0]).toEqual({
|
||||
name: 'app',
|
||||
percentage: 2 / 3
|
||||
percentage: 0.5408550899466306
|
||||
});
|
||||
|
||||
expect(response[1]).toEqual({
|
||||
count: 175,
|
||||
name: 'mysql',
|
||||
percentage: 1 / 3
|
||||
expect(response.kpis[2]).toEqual({
|
||||
name: 'postgresql',
|
||||
percentage: 0.047366859295002
|
||||
});
|
||||
});
|
||||
|
||||
it('returns a timeseries grouped by type and subtype', async () => {
|
||||
const clientSpy = jest.fn().mockReturnValueOnce(dataResponse);
|
||||
|
||||
const response = await getTransactionBreakdown({
|
||||
serviceName: 'myServiceName',
|
||||
setup: {
|
||||
start: 0,
|
||||
end: 500000,
|
||||
client: { search: clientSpy } as any,
|
||||
config: {
|
||||
get: () => 'myIndex' as any,
|
||||
has: () => true
|
||||
},
|
||||
uiFiltersES: []
|
||||
}
|
||||
});
|
||||
|
||||
expect(response).toMatchSnapshot();
|
||||
const { timeseries_per_subtype: timeseriesPerSubtype } = response;
|
||||
|
||||
expect(Object.keys(timeseriesPerSubtype).length).toBe(4);
|
||||
|
||||
// empty buckets should result in null values for visible types
|
||||
expect(timeseriesPerSubtype.app.length).toBe(276);
|
||||
expect(timeseriesPerSubtype.app.length).not.toBe(257);
|
||||
|
||||
expect(timeseriesPerSubtype.app[0].x).toBe(1561102380000);
|
||||
|
||||
expect(timeseriesPerSubtype.app[0].y).toBeCloseTo(0.8689440187037277);
|
||||
});
|
||||
|
||||
it('should not include more KPIs than MAX_KPIs', async () => {
|
||||
// @ts-ignore
|
||||
constants.MAX_KPIS = 2;
|
||||
|
||||
const clientSpy = jest.fn().mockReturnValueOnce(dataResponse);
|
||||
|
||||
const response = await getTransactionBreakdown({
|
||||
serviceName: 'myServiceName',
|
||||
setup: {
|
||||
start: 0,
|
||||
end: 500000,
|
||||
client: { search: clientSpy } as any,
|
||||
config: {
|
||||
get: () => 'myIndex' as any,
|
||||
has: () => true
|
||||
},
|
||||
uiFiltersES: []
|
||||
}
|
||||
});
|
||||
|
||||
const { timeseries_per_subtype: timeseriesPerSubtype } = response;
|
||||
|
||||
expect(Object.keys(timeseriesPerSubtype)).toEqual(['app', 'http']);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -4,15 +4,30 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { flatten } from 'lodash';
|
||||
import { flatten, sortByOrder } from 'lodash';
|
||||
import {
|
||||
SERVICE_NAME,
|
||||
SPAN_SUBTYPE,
|
||||
SPAN_TYPE,
|
||||
SPAN_SELF_TIME_SUM,
|
||||
TRANSACTION_TYPE,
|
||||
TRANSACTION_NAME,
|
||||
TRANSACTION_BREAKDOWN_COUNT
|
||||
} from '../../../../common/elasticsearch_fieldnames';
|
||||
import { PromiseReturnType } from '../../../../typings/common';
|
||||
import { Setup } from '../../helpers/setup_request';
|
||||
import { rangeFilter } from '../../helpers/range_filter';
|
||||
import { getMetricsDateHistogramParams } from '../../helpers/metrics';
|
||||
import { MAX_KPIS } from './constants';
|
||||
|
||||
export type TransactionBreakdownAPIResponse = PromiseReturnType<
|
||||
typeof getTransactionBreakdown
|
||||
>;
|
||||
|
||||
interface TimeseriesMap {
|
||||
[key: string]: Array<{ x: number; y: number | null }>;
|
||||
}
|
||||
|
||||
export async function getTransactionBreakdown({
|
||||
setup,
|
||||
serviceName,
|
||||
|
@ -24,6 +39,47 @@ export async function getTransactionBreakdown({
|
|||
}) {
|
||||
const { uiFiltersES, client, config, start, end } = setup;
|
||||
|
||||
const subAggs = {
|
||||
sum_all_self_times: {
|
||||
sum: {
|
||||
field: SPAN_SELF_TIME_SUM
|
||||
}
|
||||
},
|
||||
total_transaction_breakdown_count: {
|
||||
sum: {
|
||||
field: TRANSACTION_BREAKDOWN_COUNT
|
||||
}
|
||||
},
|
||||
types: {
|
||||
terms: {
|
||||
field: SPAN_TYPE,
|
||||
size: 20,
|
||||
order: {
|
||||
_count: 'desc'
|
||||
}
|
||||
},
|
||||
aggs: {
|
||||
subtypes: {
|
||||
terms: {
|
||||
field: SPAN_SUBTYPE,
|
||||
missing: '',
|
||||
size: 20,
|
||||
order: {
|
||||
_count: 'desc'
|
||||
}
|
||||
},
|
||||
aggs: {
|
||||
total_self_time_per_subtype: {
|
||||
sum: {
|
||||
field: SPAN_SELF_TIME_SUM
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const params = {
|
||||
index: config.get<string>('apm_oss.metricsIndices'),
|
||||
body: {
|
||||
|
@ -33,14 +89,14 @@ export async function getTransactionBreakdown({
|
|||
must: [
|
||||
{
|
||||
term: {
|
||||
'service.name.keyword': {
|
||||
[SERVICE_NAME]: {
|
||||
value: serviceName
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
term: {
|
||||
'transaction.type.keyword': {
|
||||
[TRANSACTION_TYPE]: {
|
||||
value: 'request'
|
||||
}
|
||||
}
|
||||
|
@ -51,7 +107,7 @@ export async function getTransactionBreakdown({
|
|||
? [
|
||||
{
|
||||
term: {
|
||||
'transaction.name.keyword': {
|
||||
[TRANSACTION_NAME]: {
|
||||
value: transactionName
|
||||
}
|
||||
}
|
||||
|
@ -62,48 +118,10 @@ export async function getTransactionBreakdown({
|
|||
}
|
||||
},
|
||||
aggs: {
|
||||
sum_all_self_times: {
|
||||
sum: {
|
||||
field: 'span.self_time.sum.us'
|
||||
}
|
||||
},
|
||||
total_transaction_breakdown_count: {
|
||||
sum: {
|
||||
field: 'transaction.breakdown.count'
|
||||
}
|
||||
},
|
||||
types: {
|
||||
terms: {
|
||||
field: 'span.type.keyword',
|
||||
size: 20,
|
||||
order: {
|
||||
_count: 'desc'
|
||||
}
|
||||
},
|
||||
aggs: {
|
||||
subtypes: {
|
||||
terms: {
|
||||
field: 'span.subtype.keyword',
|
||||
missing: '',
|
||||
size: 20,
|
||||
order: {
|
||||
_count: 'desc'
|
||||
}
|
||||
},
|
||||
aggs: {
|
||||
total_self_time_per_subtype: {
|
||||
sum: {
|
||||
field: 'span.self_time.sum.us'
|
||||
}
|
||||
},
|
||||
total_span_count_per_subtype: {
|
||||
sum: {
|
||||
field: 'span.self_time.count'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
...subAggs,
|
||||
by_date: {
|
||||
date_histogram: getMetricsDateHistogramParams(start, end),
|
||||
aggs: subAggs
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -111,24 +129,65 @@ export async function getTransactionBreakdown({
|
|||
|
||||
const resp = await client.search(params);
|
||||
|
||||
const sumAllSelfTimes = resp.aggregations.sum_all_self_times.value || 0;
|
||||
const formatBucket = (
|
||||
aggs:
|
||||
| typeof resp['aggregations']
|
||||
| typeof resp['aggregations']['by_date']['buckets'][0]
|
||||
) => {
|
||||
const sumAllSelfTimes = aggs.sum_all_self_times.value || 0;
|
||||
|
||||
const breakdowns = flatten(
|
||||
resp.aggregations.types.buckets.map(bucket => {
|
||||
const type = bucket.key;
|
||||
const breakdowns = flatten(
|
||||
aggs.types.buckets.map(bucket => {
|
||||
const type = bucket.key;
|
||||
|
||||
return bucket.subtypes.buckets.map(subBucket => {
|
||||
return {
|
||||
name: subBucket.key || type,
|
||||
percentage:
|
||||
(subBucket.total_self_time_per_subtype.value || 0) /
|
||||
sumAllSelfTimes,
|
||||
count: subBucket.total_span_count_per_subtype.value || 0
|
||||
return bucket.subtypes.buckets.map(subBucket => {
|
||||
return {
|
||||
name: subBucket.key || type,
|
||||
percentage:
|
||||
(subBucket.total_self_time_per_subtype.value || 0) /
|
||||
sumAllSelfTimes
|
||||
};
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
return breakdowns;
|
||||
};
|
||||
|
||||
const kpis = sortByOrder(
|
||||
formatBucket(resp.aggregations),
|
||||
'percentage',
|
||||
'desc'
|
||||
).slice(0, MAX_KPIS);
|
||||
|
||||
const kpiNames = kpis.map(kpi => kpi.name);
|
||||
|
||||
const timeseriesPerSubtype = resp.aggregations.by_date.buckets.reduce(
|
||||
(prev, bucket) => {
|
||||
const formattedValues = formatBucket(bucket);
|
||||
const time = bucket.key;
|
||||
|
||||
return kpiNames.reduce((p, kpiName) => {
|
||||
const value = formattedValues.find(val => val.name === kpiName) || {
|
||||
name: kpiName,
|
||||
percentage: null
|
||||
};
|
||||
});
|
||||
})
|
||||
// limit to 20 items because of UI constraints
|
||||
).slice(0, 20);
|
||||
|
||||
return breakdowns;
|
||||
const { name, percentage } = value;
|
||||
if (!p[name]) {
|
||||
p[name] = [];
|
||||
}
|
||||
return {
|
||||
...p,
|
||||
[value.name]: p[name].concat({ x: time, y: percentage })
|
||||
};
|
||||
}, prev);
|
||||
},
|
||||
{} as TimeseriesMap
|
||||
);
|
||||
|
||||
return {
|
||||
kpis,
|
||||
timeseries_per_subtype: timeseriesPerSubtype
|
||||
};
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,88 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export const dataResponse = {
|
||||
took: 8,
|
||||
timed_out: false,
|
||||
_shards: {
|
||||
total: 85,
|
||||
successful: 85,
|
||||
skipped: 0,
|
||||
failed: 0
|
||||
},
|
||||
hits: {
|
||||
total: {
|
||||
value: 6,
|
||||
relation: 'eq'
|
||||
},
|
||||
max_score: null,
|
||||
hits: []
|
||||
},
|
||||
aggregations: {
|
||||
types: {
|
||||
doc_count_error_upper_bound: 0,
|
||||
sum_other_doc_count: 0,
|
||||
buckets: [
|
||||
{
|
||||
key: 'app',
|
||||
doc_count: 2,
|
||||
subtypes: {
|
||||
doc_count_error_upper_bound: 0,
|
||||
sum_other_doc_count: 0,
|
||||
buckets: [
|
||||
{
|
||||
key: '',
|
||||
doc_count: 2,
|
||||
total_self_time_per_subtype: {
|
||||
value: 400.0
|
||||
},
|
||||
total_span_count_per_subtype: {
|
||||
value: 15.0
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'db',
|
||||
doc_count: 2,
|
||||
subtypes: {
|
||||
doc_count_error_upper_bound: 0,
|
||||
sum_other_doc_count: 0,
|
||||
buckets: [
|
||||
{
|
||||
key: 'mysql',
|
||||
doc_count: 2,
|
||||
total_self_time_per_subtype: {
|
||||
value: 200.0
|
||||
},
|
||||
total_span_count_per_subtype: {
|
||||
value: 175.0
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'elasticsearch',
|
||||
doc_count: 3,
|
||||
total_self_time_per_subtype: {
|
||||
value: 100.0
|
||||
},
|
||||
total_span_count_per_subtype: {
|
||||
value: 225.0
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
total_transaction_breakdown_count: {
|
||||
value: 15.0
|
||||
},
|
||||
sum_all_self_times: {
|
||||
value: 600.0
|
||||
}
|
||||
}
|
||||
};
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"took": 1,
|
||||
"timed_out": false,
|
||||
"_shards": { "total": 1, "successful": 1, "skipped": 0, "failed": 0 },
|
||||
"hits": {
|
||||
"total": { "value": 0, "relation": "eq" },
|
||||
"max_score": null,
|
||||
"hits": []
|
||||
},
|
||||
"aggregations": {
|
||||
"by_date": { "buckets": [] },
|
||||
"types": {
|
||||
"doc_count_error_upper_bound": 0,
|
||||
"sum_other_doc_count": 0,
|
||||
"buckets": []
|
||||
},
|
||||
"total_transaction_breakdown_count": { "value": 0 },
|
||||
"sum_all_self_times": { "value": 0 }
|
||||
}
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export const noDataResponse = {
|
||||
took: 11,
|
||||
timed_out: false,
|
||||
_shards: {
|
||||
total: 85,
|
||||
successful: 85,
|
||||
skipped: 0,
|
||||
failed: 0
|
||||
},
|
||||
hits: {
|
||||
total: {
|
||||
value: 0,
|
||||
relation: 'eq'
|
||||
},
|
||||
max_score: null,
|
||||
hits: []
|
||||
},
|
||||
aggregations: {
|
||||
types: {
|
||||
doc_count_error_upper_bound: 0,
|
||||
sum_other_doc_count: 0,
|
||||
buckets: []
|
||||
},
|
||||
total_transaction_breakdown_count: {
|
||||
value: 0.0
|
||||
},
|
||||
sum_all_self_times: {
|
||||
value: 0.0
|
||||
}
|
||||
}
|
||||
};
|
|
@ -14,5 +14,17 @@ export interface RectCoordinate {
|
|||
x0: number;
|
||||
}
|
||||
|
||||
export interface TimeSeries {
|
||||
title: string;
|
||||
titleShort?: string;
|
||||
hideLegend?: boolean;
|
||||
hideTooltipValue?: boolean;
|
||||
data: Array<Coordinate | RectCoordinate>;
|
||||
legendValue?: string;
|
||||
type: string;
|
||||
color: string;
|
||||
areaColor?: string;
|
||||
}
|
||||
|
||||
export type ChartType = 'area' | 'linemark';
|
||||
export type YUnit = 'percent' | 'bytes' | 'number';
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue