[APM] Hoist loading of waterfall and flatten it (#24651)
* [APM] Hoist loading of waterfall and flatten it Remove unused test Convert to typescript * Address feedback * Make `totalDuration` optional * Renamed rootTransaction to traceRoot * [APM] Only show relevant service legends * Adds services label to the service legend * [APM] Clock skew fix Only skew child spans from the same service Take parent skew into account when finding diff # Conflicts: # x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/waterfall_helpers/__snapshots__/waterfall_helpers.test.ts.snap # x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.test.ts # x-pack/plugins/apm/public/components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.ts * Use switch statement
This commit is contained in:
parent
5a019cc31c
commit
8f4b0e9bf8
31 changed files with 522 additions and 676 deletions
|
@ -16,7 +16,6 @@ import {
|
|||
} from '../../../../style/variables';
|
||||
import { get, capitalize, isEmpty } from 'lodash';
|
||||
import { STATUS } from '../../../../constants';
|
||||
|
||||
import { StickyProperties } from '../../../shared/StickyProperties';
|
||||
import { Tab, HeaderMedium } from '../../../shared/UIComponents';
|
||||
import DiscoverButton from '../../../shared/DiscoverButton';
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
|
||||
import { connect } from 'react-redux';
|
||||
import { IReduxState } from '../../../store/rootReducer';
|
||||
// @ts-ignore
|
||||
import { getUrlParams } from '../../../store/urlParams';
|
||||
import { TraceOverview as View } from './view';
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import { EuiCallOut, EuiLink, EuiSpacer, EuiText } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { RRRRenderArgs } from 'react-redux-request';
|
||||
import { RRRRenderResponse } from 'react-redux-request';
|
||||
import { ITransactionGroup } from '../../../../typings/TransactionGroup';
|
||||
// @ts-ignore
|
||||
import { TraceListRequest } from '../../../store/reactReduxRequest/traceList';
|
||||
|
@ -37,7 +37,7 @@ export function TraceOverview(props: Props) {
|
|||
<EuiSpacer />
|
||||
<TraceListRequest
|
||||
urlParams={urlParams}
|
||||
render={({ data, status }: RRRRenderArgs<ITransactionGroup[]>) => (
|
||||
render={({ data, status }: RRRRenderResponse<ITransactionGroup[]>) => (
|
||||
<TraceList
|
||||
items={data}
|
||||
isLoading={status === 'LOADING'}
|
||||
|
|
|
@ -6,8 +6,6 @@
|
|||
|
||||
import { get } from 'lodash';
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { selectWaterfallRoot } from 'x-pack/plugins/apm/public/store/selectors/waterfall';
|
||||
import {
|
||||
REQUEST_URL_FULL,
|
||||
TRANSACTION_DURATION,
|
||||
|
@ -17,7 +15,6 @@ import {
|
|||
import { Transaction } from '../../../../../typings/Transaction';
|
||||
// @ts-ignore
|
||||
import { asTime } from '../../../../utils/formatters';
|
||||
// @ts-ignore
|
||||
import {
|
||||
IStickyProperty,
|
||||
StickyProperties
|
||||
|
@ -25,27 +22,26 @@ import {
|
|||
|
||||
function getDurationPercent(
|
||||
transactionDuration: number,
|
||||
rootDuration?: number
|
||||
totalDuration?: number
|
||||
) {
|
||||
if (rootDuration === undefined || rootDuration === 0) {
|
||||
if (!totalDuration) {
|
||||
return '';
|
||||
}
|
||||
return ((transactionDuration / rootDuration) * 100).toFixed(2) + '%';
|
||||
return ((transactionDuration / totalDuration) * 100).toFixed(2) + '%';
|
||||
}
|
||||
|
||||
interface Props {
|
||||
transaction: Transaction;
|
||||
root?: Transaction;
|
||||
totalDuration?: number;
|
||||
}
|
||||
|
||||
export function StickyTransactionPropertiesComponent({
|
||||
export function StickyTransactionProperties({
|
||||
transaction,
|
||||
root
|
||||
totalDuration
|
||||
}: Props) {
|
||||
const timestamp = get(transaction, '@timestamp');
|
||||
const timestamp = transaction['@timestamp'];
|
||||
const url = get(transaction, REQUEST_URL_FULL, 'N/A');
|
||||
const duration = transaction.transaction.duration.us;
|
||||
const rootDuration = root && root.transaction.duration.us;
|
||||
const stickyProperties: IStickyProperty[] = [
|
||||
{
|
||||
label: 'Timestamp',
|
||||
|
@ -69,7 +65,7 @@ export function StickyTransactionPropertiesComponent({
|
|||
},
|
||||
{
|
||||
label: '% of trace',
|
||||
val: getDurationPercent(duration, rootDuration),
|
||||
val: getDurationPercent(duration, totalDuration),
|
||||
width: '25%'
|
||||
},
|
||||
{
|
||||
|
@ -89,11 +85,3 @@ export function StickyTransactionPropertiesComponent({
|
|||
|
||||
return <StickyProperties stickyProperties={stickyProperties} />;
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: any, props: Partial<Props>) => ({
|
||||
root: selectWaterfallRoot(state, props)
|
||||
});
|
||||
|
||||
export const StickyTransactionProperties = connect<{}, {}, Props>(
|
||||
mapStateToProps
|
||||
)(StickyTransactionPropertiesComponent);
|
||||
|
|
|
@ -23,6 +23,7 @@ import {
|
|||
PropertiesTable
|
||||
} from '../../../shared/PropertiesTable';
|
||||
import { WaterfallContainer } from './WaterfallContainer';
|
||||
import { IWaterfall } from './WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers';
|
||||
|
||||
const TableContainer = styled.div`
|
||||
padding: ${px(units.plus)} ${px(units.plus)} 0;
|
||||
|
@ -44,11 +45,15 @@ interface TransactionPropertiesTableProps {
|
|||
location: any;
|
||||
transaction: Transaction;
|
||||
urlParams: IUrlParams;
|
||||
waterfall: IWaterfall;
|
||||
}
|
||||
|
||||
export const TransactionPropertiesTable: React.SFC<
|
||||
TransactionPropertiesTableProps
|
||||
> = ({ location, transaction, urlParams }) => {
|
||||
export function TransactionPropertiesTable({
|
||||
location,
|
||||
transaction,
|
||||
urlParams,
|
||||
waterfall
|
||||
}: TransactionPropertiesTableProps) {
|
||||
const tabs = getTabs(transaction);
|
||||
const currentTab = getCurrentTab(tabs, urlParams.detailTab);
|
||||
const agentName = transaction.context.service.agent.name;
|
||||
|
@ -84,6 +89,7 @@ export const TransactionPropertiesTable: React.SFC<
|
|||
transaction={transaction}
|
||||
location={location}
|
||||
urlParams={urlParams}
|
||||
waterfall={waterfall}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
@ -98,4 +104,4 @@ export const TransactionPropertiesTable: React.SFC<
|
|||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EuiTitle } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { px, unit } from '../../../../../style/variables';
|
||||
|
@ -13,7 +14,7 @@ import Legend from '../../../../shared/charts/Legend';
|
|||
const Legends = styled.div`
|
||||
display: flex;
|
||||
|
||||
div {
|
||||
> * {
|
||||
margin-right: ${px(unit)};
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
|
@ -30,6 +31,9 @@ interface Props {
|
|||
export function ServiceLegends({ serviceColors }: Props) {
|
||||
return (
|
||||
<Legends>
|
||||
<EuiTitle size="xxxs">
|
||||
<span>Services</span>
|
||||
</EuiTitle>
|
||||
{Object.entries(serviceColors).map(([label, color]) => (
|
||||
<Legend key={color} color={color} text={label} />
|
||||
))}
|
||||
|
|
|
@ -9,17 +9,20 @@ import {
|
|||
SERVICE_NAME,
|
||||
TRANSACTION_NAME
|
||||
} from 'x-pack/plugins/apm/common/constants';
|
||||
// @ts-ignore
|
||||
import { StickyProperties } from 'x-pack/plugins/apm/public/components/shared/StickyProperties';
|
||||
import { TransactionLink } from 'x-pack/plugins/apm/public/components/shared/TransactionLink';
|
||||
import { KibanaLink } from 'x-pack/plugins/apm/public/utils/url';
|
||||
import { Transaction } from 'x-pack/plugins/apm/typings/Transaction';
|
||||
|
||||
interface Props {
|
||||
transaction: Transaction;
|
||||
transaction?: Transaction;
|
||||
}
|
||||
|
||||
export function FlyoutTopLevelProperties({ transaction }: Props) {
|
||||
if (!transaction) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const stickyProperties = [
|
||||
{
|
||||
label: 'Service',
|
||||
|
|
|
@ -12,7 +12,6 @@ import { first } from 'lodash';
|
|||
import { Span } from '../../../../../../../../typings/Span';
|
||||
// @ts-ignore
|
||||
import { asMillis } from '../../../../../../../utils/formatters';
|
||||
// @ts-ignore
|
||||
import { StickyProperties } from '../../../../../../shared/StickyProperties';
|
||||
|
||||
function getSpanLabel(type: string) {
|
||||
|
@ -32,10 +31,14 @@ function getPrimaryType(type: string) {
|
|||
|
||||
interface Props {
|
||||
span: Span;
|
||||
totalDuration: number;
|
||||
totalDuration?: number;
|
||||
}
|
||||
|
||||
export function StickySpanProperties({ span, totalDuration }: Props) {
|
||||
if (!totalDuration) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const spanName = span.span.name;
|
||||
const spanDuration = span.span.duration.us;
|
||||
const relativeDuration = spanDuration / totalDuration;
|
||||
|
|
|
@ -55,8 +55,8 @@ function getDiscoverQuery(span: Span) {
|
|||
|
||||
interface Props {
|
||||
span?: Span;
|
||||
parentTransaction: Transaction;
|
||||
totalDuration: number;
|
||||
parentTransaction?: Transaction;
|
||||
totalDuration?: number;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
|
@ -70,7 +70,7 @@ export function SpanFlyout({
|
|||
return null;
|
||||
}
|
||||
const stackframes = span.span.stacktrace;
|
||||
const codeLanguage = get(span, SERVICE_LANGUAGE_NAME);
|
||||
const codeLanguage: string = get(span, SERVICE_LANGUAGE_NAME);
|
||||
const dbContext = span.context.db;
|
||||
|
||||
return (
|
||||
|
|
|
@ -66,7 +66,10 @@ export function TransactionFlyout({
|
|||
<EuiFlyoutBody>
|
||||
<FlyoutTopLevelProperties transaction={transactionDoc} />
|
||||
<EuiHorizontalRule />
|
||||
<StickyTransactionProperties transaction={transactionDoc} />
|
||||
<StickyTransactionProperties
|
||||
transaction={transactionDoc}
|
||||
totalDuration={waterfall.traceRootDuration}
|
||||
/>
|
||||
<EuiHorizontalRule />
|
||||
<TransactionPropertiesTableForFlyout
|
||||
transaction={transactionDoc}
|
||||
|
|
|
@ -88,7 +88,7 @@ interface ITimelineMargins {
|
|||
|
||||
interface IWaterfallItemProps {
|
||||
timelineMargins: ITimelineMargins;
|
||||
totalDuration: number;
|
||||
totalDuration?: number;
|
||||
item: IWaterfallItem;
|
||||
color: string;
|
||||
isSelected: boolean;
|
||||
|
@ -115,8 +115,12 @@ export function WaterfallItem({
|
|||
isSelected,
|
||||
onClick
|
||||
}: IWaterfallItemProps) {
|
||||
if (!totalDuration) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const width = (item.duration / totalDuration) * 100;
|
||||
const left = (item.offset / totalDuration) * 100;
|
||||
const left = ((item.offset + item.skew) / totalDuration) * 100;
|
||||
const Label = item.docType === 'transaction' ? TransactionLabel : SpanLabel;
|
||||
|
||||
// Note: the <Prefix> appears *after* the item name in the DOM order
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import React, { Component } from 'react';
|
||||
// @ts-ignore
|
||||
import { StickyContainer } from 'react-sticky';
|
||||
import styled from 'styled-components';
|
||||
|
@ -62,26 +62,19 @@ export class Waterfall extends Component<Props> {
|
|||
});
|
||||
};
|
||||
|
||||
public renderWaterfall = (item?: IWaterfallItem) => {
|
||||
if (!item) {
|
||||
return null;
|
||||
}
|
||||
|
||||
public getWaterfallItem = (item: IWaterfallItem) => {
|
||||
const { serviceColors, waterfall, urlParams }: Props = this.props;
|
||||
|
||||
return (
|
||||
<Fragment key={item.id}>
|
||||
<WaterfallItem
|
||||
timelineMargins={TIMELINE_MARGINS}
|
||||
color={serviceColors[item.serviceName]}
|
||||
item={item}
|
||||
totalDuration={waterfall.duration}
|
||||
isSelected={item.id === urlParams.waterfallItemId}
|
||||
onClick={() => this.onOpenFlyout(item)}
|
||||
/>
|
||||
|
||||
{item.children && item.children.map(this.renderWaterfall)}
|
||||
</Fragment>
|
||||
<WaterfallItem
|
||||
key={item.id}
|
||||
timelineMargins={TIMELINE_MARGINS}
|
||||
color={serviceColors[item.serviceName]}
|
||||
item={item}
|
||||
totalDuration={waterfall.duration}
|
||||
isSelected={item.id === urlParams.waterfallItemId}
|
||||
onClick={() => this.onOpenFlyout(item)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -98,11 +91,15 @@ export class Waterfall extends Component<Props> {
|
|||
|
||||
switch (currentItem.docType) {
|
||||
case 'span':
|
||||
const parentTransaction = waterfall.getTransactionById(
|
||||
currentItem.parentId
|
||||
);
|
||||
|
||||
return (
|
||||
<SpanFlyout
|
||||
totalDuration={waterfall.duration}
|
||||
span={currentItem.span}
|
||||
parentTransaction={currentItem.parentTransaction}
|
||||
parentTransaction={parentTransaction}
|
||||
onClose={this.onCloseFlyout}
|
||||
/>
|
||||
);
|
||||
|
@ -124,7 +121,7 @@ export class Waterfall extends Component<Props> {
|
|||
public render() {
|
||||
const { waterfall } = this.props;
|
||||
const itemContainerHeight = 58; // TODO: This is a nasty way to calculate the height of the svg element. A better approach should be found
|
||||
const waterfallHeight = itemContainerHeight * waterfall.childrenCount;
|
||||
const waterfallHeight = itemContainerHeight * waterfall.items.length;
|
||||
|
||||
return (
|
||||
<Container>
|
||||
|
@ -140,7 +137,7 @@ export class Waterfall extends Component<Props> {
|
|||
paddingTop: TIMELINE_MARGINS.top
|
||||
}}
|
||||
>
|
||||
{this.renderWaterfall(waterfall.root)}
|
||||
{waterfall.items.map(this.getWaterfallItem)}
|
||||
</div>
|
||||
</StickyContainer>
|
||||
|
||||
|
|
|
@ -1,265 +1,11 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`getWaterfallRoot 1`] = `
|
||||
Object {
|
||||
"itemsById": Object {
|
||||
"a": Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"children": Array [],
|
||||
"docType": "span",
|
||||
"duration": 4694,
|
||||
"id": "b2",
|
||||
"name": "GET [0:0:0:0:0:0:0:1]",
|
||||
"offset": 1000,
|
||||
"parentId": "a",
|
||||
"parentTransaction": Object {},
|
||||
"serviceName": "opbeans-java",
|
||||
"span": Object {
|
||||
"transaction": Object {
|
||||
"id": "a",
|
||||
},
|
||||
},
|
||||
"timestamp": 1536763736367000,
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"children": Array [],
|
||||
"docType": "span",
|
||||
"duration": 210,
|
||||
"id": "d",
|
||||
"name": "SELECT",
|
||||
"offset": 5000,
|
||||
"parentId": "c",
|
||||
"parentTransaction": Object {},
|
||||
"serviceName": "opbeans-java",
|
||||
"span": Object {
|
||||
"transaction": Object {
|
||||
"id": "c",
|
||||
},
|
||||
},
|
||||
"timestamp": 1536763736371000,
|
||||
},
|
||||
],
|
||||
"docType": "transaction",
|
||||
"duration": 3581,
|
||||
"id": "c",
|
||||
"name": "APIRestController#productsRemote",
|
||||
"offset": 3000,
|
||||
"parentId": "b",
|
||||
"serviceName": "opbeans-java",
|
||||
"timestamp": 1536763736369000,
|
||||
"transaction": Object {},
|
||||
},
|
||||
],
|
||||
"docType": "span",
|
||||
"duration": 4694,
|
||||
"id": "b",
|
||||
"name": "GET [0:0:0:0:0:0:0:1]",
|
||||
"offset": 2000,
|
||||
"parentId": "a",
|
||||
"parentTransaction": Object {},
|
||||
"serviceName": "opbeans-java",
|
||||
"span": Object {
|
||||
"transaction": Object {
|
||||
"id": "a",
|
||||
},
|
||||
},
|
||||
"timestamp": 1536763736368000,
|
||||
},
|
||||
],
|
||||
"docType": "transaction",
|
||||
"duration": 9480,
|
||||
"id": "a",
|
||||
"name": "APIRestController#products",
|
||||
"offset": 0,
|
||||
"serviceName": "opbeans-java",
|
||||
"timestamp": 1536763736366000,
|
||||
"transaction": Object {},
|
||||
},
|
||||
"b": Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"children": Array [],
|
||||
"docType": "span",
|
||||
"duration": 210,
|
||||
"id": "d",
|
||||
"name": "SELECT",
|
||||
"offset": 5000,
|
||||
"parentId": "c",
|
||||
"parentTransaction": Object {},
|
||||
"serviceName": "opbeans-java",
|
||||
"span": Object {
|
||||
"transaction": Object {
|
||||
"id": "c",
|
||||
},
|
||||
},
|
||||
"timestamp": 1536763736371000,
|
||||
},
|
||||
],
|
||||
"docType": "transaction",
|
||||
"duration": 3581,
|
||||
"id": "c",
|
||||
"name": "APIRestController#productsRemote",
|
||||
"offset": 3000,
|
||||
"parentId": "b",
|
||||
"serviceName": "opbeans-java",
|
||||
"timestamp": 1536763736369000,
|
||||
"transaction": Object {},
|
||||
},
|
||||
],
|
||||
"docType": "span",
|
||||
"duration": 4694,
|
||||
"id": "b",
|
||||
"name": "GET [0:0:0:0:0:0:0:1]",
|
||||
"offset": 2000,
|
||||
"parentId": "a",
|
||||
"parentTransaction": Object {},
|
||||
"serviceName": "opbeans-java",
|
||||
"span": Object {
|
||||
"transaction": Object {
|
||||
"id": "a",
|
||||
},
|
||||
},
|
||||
"timestamp": 1536763736368000,
|
||||
},
|
||||
"b2": Object {
|
||||
"children": Array [],
|
||||
"docType": "span",
|
||||
"duration": 4694,
|
||||
"id": "b2",
|
||||
"name": "GET [0:0:0:0:0:0:0:1]",
|
||||
"offset": 1000,
|
||||
"parentId": "a",
|
||||
"parentTransaction": Object {},
|
||||
"serviceName": "opbeans-java",
|
||||
"span": Object {
|
||||
"transaction": Object {
|
||||
"id": "a",
|
||||
},
|
||||
},
|
||||
"timestamp": 1536763736367000,
|
||||
},
|
||||
"c": Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"children": Array [],
|
||||
"docType": "span",
|
||||
"duration": 210,
|
||||
"id": "d",
|
||||
"name": "SELECT",
|
||||
"offset": 5000,
|
||||
"parentId": "c",
|
||||
"parentTransaction": Object {},
|
||||
"serviceName": "opbeans-java",
|
||||
"span": Object {
|
||||
"transaction": Object {
|
||||
"id": "c",
|
||||
},
|
||||
},
|
||||
"timestamp": 1536763736371000,
|
||||
},
|
||||
],
|
||||
"docType": "transaction",
|
||||
"duration": 3581,
|
||||
"id": "c",
|
||||
"name": "APIRestController#productsRemote",
|
||||
"offset": 3000,
|
||||
"parentId": "b",
|
||||
"serviceName": "opbeans-java",
|
||||
"timestamp": 1536763736369000,
|
||||
"transaction": Object {},
|
||||
},
|
||||
"d": Object {
|
||||
"children": Array [],
|
||||
"docType": "span",
|
||||
"duration": 210,
|
||||
"id": "d",
|
||||
"name": "SELECT",
|
||||
"offset": 5000,
|
||||
"parentId": "c",
|
||||
"parentTransaction": Object {},
|
||||
"serviceName": "opbeans-java",
|
||||
"span": Object {
|
||||
"transaction": Object {
|
||||
"id": "c",
|
||||
},
|
||||
},
|
||||
"timestamp": 1536763736371000,
|
||||
},
|
||||
},
|
||||
"root": Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"children": Array [],
|
||||
"docType": "span",
|
||||
"duration": 4694,
|
||||
"id": "b2",
|
||||
"name": "GET [0:0:0:0:0:0:0:1]",
|
||||
"offset": 1000,
|
||||
"parentId": "a",
|
||||
"parentTransaction": Object {},
|
||||
"serviceName": "opbeans-java",
|
||||
"span": Object {
|
||||
"transaction": Object {
|
||||
"id": "a",
|
||||
},
|
||||
},
|
||||
"timestamp": 1536763736367000,
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"children": Array [],
|
||||
"docType": "span",
|
||||
"duration": 210,
|
||||
"id": "d",
|
||||
"name": "SELECT",
|
||||
"offset": 5000,
|
||||
"parentId": "c",
|
||||
"parentTransaction": Object {},
|
||||
"serviceName": "opbeans-java",
|
||||
"span": Object {
|
||||
"transaction": Object {
|
||||
"id": "c",
|
||||
},
|
||||
},
|
||||
"timestamp": 1536763736371000,
|
||||
},
|
||||
],
|
||||
"docType": "transaction",
|
||||
"duration": 3581,
|
||||
"id": "c",
|
||||
"name": "APIRestController#productsRemote",
|
||||
"offset": 3000,
|
||||
"parentId": "b",
|
||||
"serviceName": "opbeans-java",
|
||||
"timestamp": 1536763736369000,
|
||||
"transaction": Object {},
|
||||
},
|
||||
],
|
||||
"docType": "span",
|
||||
"duration": 4694,
|
||||
"id": "b",
|
||||
"name": "GET [0:0:0:0:0:0:0:1]",
|
||||
"offset": 2000,
|
||||
"parentId": "a",
|
||||
"parentTransaction": Object {},
|
||||
"serviceName": "opbeans-java",
|
||||
"span": Object {
|
||||
"transaction": Object {
|
||||
"id": "a",
|
||||
},
|
||||
},
|
||||
"timestamp": 1536763736368000,
|
||||
},
|
||||
exports[`waterfall_helpers getWaterfallItems should order items correctly 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"childIds": Array [
|
||||
"b2",
|
||||
"b",
|
||||
],
|
||||
"docType": "transaction",
|
||||
"duration": 9480,
|
||||
|
@ -267,8 +13,77 @@ Object {
|
|||
"name": "APIRestController#products",
|
||||
"offset": 0,
|
||||
"serviceName": "opbeans-java",
|
||||
"skew": 0,
|
||||
"timestamp": 1536763736366000,
|
||||
"transaction": Object {},
|
||||
},
|
||||
}
|
||||
Object {
|
||||
"childIds": Array [],
|
||||
"docType": "span",
|
||||
"duration": 4694,
|
||||
"id": "b2",
|
||||
"name": "GET [0:0:0:0:0:0:0:1]",
|
||||
"offset": 1000,
|
||||
"parentId": "a",
|
||||
"serviceName": "opbeans-java",
|
||||
"skew": 0,
|
||||
"span": Object {
|
||||
"transaction": Object {
|
||||
"id": "a",
|
||||
},
|
||||
},
|
||||
"timestamp": 1536763736367000,
|
||||
},
|
||||
Object {
|
||||
"childIds": Array [
|
||||
"c",
|
||||
],
|
||||
"docType": "span",
|
||||
"duration": 4694,
|
||||
"id": "b",
|
||||
"name": "GET [0:0:0:0:0:0:0:1]",
|
||||
"offset": 2000,
|
||||
"parentId": "a",
|
||||
"serviceName": "opbeans-java",
|
||||
"skew": 0,
|
||||
"span": Object {
|
||||
"transaction": Object {
|
||||
"id": "a",
|
||||
},
|
||||
},
|
||||
"timestamp": 1536763736368000,
|
||||
},
|
||||
Object {
|
||||
"childIds": Array [
|
||||
"d",
|
||||
],
|
||||
"docType": "transaction",
|
||||
"duration": 3581,
|
||||
"id": "c",
|
||||
"name": "APIRestController#productsRemote",
|
||||
"offset": 3000,
|
||||
"parentId": "b",
|
||||
"serviceName": "opbeans-java",
|
||||
"skew": 0,
|
||||
"timestamp": 1536763736369000,
|
||||
"transaction": Object {},
|
||||
},
|
||||
Object {
|
||||
"childIds": Array [],
|
||||
"docType": "span",
|
||||
"duration": 210,
|
||||
"id": "d",
|
||||
"name": "SELECT",
|
||||
"offset": 5000,
|
||||
"parentId": "c",
|
||||
"serviceName": "opbeans-java",
|
||||
"skew": 0,
|
||||
"span": Object {
|
||||
"transaction": Object {
|
||||
"id": "c",
|
||||
},
|
||||
},
|
||||
"timestamp": 1536763736371000,
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
|
|
@ -4,82 +4,101 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { groupBy, indexBy } from 'lodash';
|
||||
import { Span } from 'x-pack/plugins/apm/typings/Span';
|
||||
import { Transaction } from 'x-pack/plugins/apm/typings/Transaction';
|
||||
import { getWaterfallRoot, IWaterfallItem } from './waterfall_helpers';
|
||||
import {
|
||||
getWaterfallItems,
|
||||
IWaterfallIndex,
|
||||
IWaterfallItem
|
||||
} from './waterfall_helpers';
|
||||
|
||||
it('getWaterfallRoot', () => {
|
||||
const items: IWaterfallItem[] = [
|
||||
{
|
||||
id: 'd',
|
||||
parentId: 'c',
|
||||
serviceName: 'opbeans-java',
|
||||
name: 'SELECT',
|
||||
duration: 210,
|
||||
timestamp: 1536763736371000,
|
||||
offset: 0,
|
||||
docType: 'span',
|
||||
parentTransaction: {} as Transaction,
|
||||
span: {
|
||||
transaction: {
|
||||
id: 'c'
|
||||
describe('waterfall_helpers', () => {
|
||||
describe('getWaterfallItems', () => {
|
||||
it('should order items correctly', () => {
|
||||
const items: IWaterfallItem[] = [
|
||||
{
|
||||
id: 'd',
|
||||
parentId: 'c',
|
||||
serviceName: 'opbeans-java',
|
||||
name: 'SELECT',
|
||||
duration: 210,
|
||||
timestamp: 1536763736371000,
|
||||
offset: 0,
|
||||
skew: 0,
|
||||
docType: 'span',
|
||||
span: {
|
||||
transaction: {
|
||||
id: 'c'
|
||||
}
|
||||
} as Span
|
||||
},
|
||||
{
|
||||
id: 'b',
|
||||
parentId: 'a',
|
||||
serviceName: 'opbeans-java',
|
||||
name: 'GET [0:0:0:0:0:0:0:1]',
|
||||
duration: 4694,
|
||||
timestamp: 1536763736368000,
|
||||
offset: 0,
|
||||
skew: 0,
|
||||
docType: 'span',
|
||||
span: {
|
||||
transaction: {
|
||||
id: 'a'
|
||||
}
|
||||
} as Span
|
||||
},
|
||||
{
|
||||
id: 'b2',
|
||||
parentId: 'a',
|
||||
serviceName: 'opbeans-java',
|
||||
name: 'GET [0:0:0:0:0:0:0:1]',
|
||||
duration: 4694,
|
||||
timestamp: 1536763736367000,
|
||||
offset: 0,
|
||||
skew: 0,
|
||||
docType: 'span',
|
||||
span: {
|
||||
transaction: {
|
||||
id: 'a'
|
||||
}
|
||||
} as Span
|
||||
},
|
||||
{
|
||||
id: 'c',
|
||||
parentId: 'b',
|
||||
serviceName: 'opbeans-java',
|
||||
name: 'APIRestController#productsRemote',
|
||||
duration: 3581,
|
||||
timestamp: 1536763736369000,
|
||||
offset: 0,
|
||||
skew: 0,
|
||||
docType: 'transaction',
|
||||
transaction: {} as Transaction
|
||||
},
|
||||
{
|
||||
id: 'a',
|
||||
serviceName: 'opbeans-java',
|
||||
name: 'APIRestController#products',
|
||||
duration: 9480,
|
||||
timestamp: 1536763736366000,
|
||||
offset: 0,
|
||||
skew: 0,
|
||||
docType: 'transaction',
|
||||
transaction: {} as Transaction
|
||||
}
|
||||
} as Span
|
||||
},
|
||||
{
|
||||
id: 'b',
|
||||
parentId: 'a',
|
||||
serviceName: 'opbeans-java',
|
||||
name: 'GET [0:0:0:0:0:0:0:1]',
|
||||
duration: 4694,
|
||||
timestamp: 1536763736368000,
|
||||
offset: 0,
|
||||
docType: 'span',
|
||||
parentTransaction: {} as Transaction,
|
||||
span: {
|
||||
transaction: {
|
||||
id: 'a'
|
||||
}
|
||||
} as Span
|
||||
},
|
||||
{
|
||||
id: 'b2',
|
||||
parentId: 'a',
|
||||
serviceName: 'opbeans-java',
|
||||
name: 'GET [0:0:0:0:0:0:0:1]',
|
||||
duration: 4694,
|
||||
timestamp: 1536763736367000,
|
||||
offset: 0,
|
||||
docType: 'span',
|
||||
parentTransaction: {} as Transaction,
|
||||
span: {
|
||||
transaction: {
|
||||
id: 'a'
|
||||
}
|
||||
} as Span
|
||||
},
|
||||
{
|
||||
id: 'c',
|
||||
parentId: 'b',
|
||||
serviceName: 'opbeans-java',
|
||||
name: 'APIRestController#productsRemote',
|
||||
duration: 3581,
|
||||
timestamp: 1536763736369000,
|
||||
offset: 0,
|
||||
docType: 'transaction',
|
||||
transaction: {} as Transaction
|
||||
},
|
||||
{
|
||||
id: 'a',
|
||||
serviceName: 'opbeans-java',
|
||||
name: 'APIRestController#products',
|
||||
duration: 9480,
|
||||
timestamp: 1536763736366000,
|
||||
offset: 0,
|
||||
docType: 'transaction',
|
||||
transaction: {} as Transaction
|
||||
}
|
||||
];
|
||||
];
|
||||
|
||||
expect(getWaterfallRoot(items, items[4])).toMatchSnapshot();
|
||||
const childrenByParentId = groupBy(
|
||||
items,
|
||||
hit => (hit.parentId ? hit.parentId : 'root')
|
||||
);
|
||||
const itemsById: IWaterfallIndex = indexBy(items, 'id');
|
||||
const entryTransactionItem = childrenByParentId.root[0];
|
||||
expect(
|
||||
getWaterfallItems(childrenByParentId, itemsById, entryTransactionItem)
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -4,7 +4,15 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { groupBy, indexBy, sortBy } from 'lodash';
|
||||
import {
|
||||
first,
|
||||
flatten,
|
||||
groupBy,
|
||||
indexBy,
|
||||
isEmpty,
|
||||
sortBy,
|
||||
uniq
|
||||
} from 'lodash';
|
||||
import { Span } from '../../../../../../../../typings/Span';
|
||||
import { Transaction } from '../../../../../../../../typings/Transaction';
|
||||
|
||||
|
@ -12,12 +20,18 @@ export interface IWaterfallIndex {
|
|||
[key: string]: IWaterfallItem;
|
||||
}
|
||||
|
||||
export interface IWaterfallGroup {
|
||||
[key: string]: IWaterfallItem[];
|
||||
}
|
||||
|
||||
export interface IWaterfall {
|
||||
duration: number;
|
||||
traceRoot?: Transaction;
|
||||
traceRootDuration?: number;
|
||||
duration?: number;
|
||||
services: string[];
|
||||
childrenCount: number;
|
||||
root: IWaterfallItem;
|
||||
items: IWaterfallItem[];
|
||||
itemsById: IWaterfallIndex;
|
||||
getTransactionById: (id?: IWaterfallItem['id']) => Transaction | undefined;
|
||||
}
|
||||
|
||||
interface IWaterfallItemBase {
|
||||
|
@ -28,25 +42,22 @@ interface IWaterfallItemBase {
|
|||
duration: number;
|
||||
timestamp: number;
|
||||
offset: number;
|
||||
skew: number;
|
||||
childIds?: Array<IWaterfallItemBase['id']>;
|
||||
}
|
||||
|
||||
interface IWaterfallItemTransaction extends IWaterfallItemBase {
|
||||
transaction: Transaction;
|
||||
docType: 'transaction';
|
||||
children?: Array<IWaterfallItemSpan | IWaterfallItemTransaction>;
|
||||
}
|
||||
|
||||
interface IWaterfallItemSpan extends IWaterfallItemBase {
|
||||
parentTransaction: Transaction;
|
||||
span: Span;
|
||||
docType: 'span';
|
||||
children?: Array<IWaterfallItemSpan | IWaterfallItemTransaction>;
|
||||
}
|
||||
|
||||
export type IWaterfallItem = IWaterfallItemSpan | IWaterfallItemTransaction;
|
||||
|
||||
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
|
||||
|
||||
function getTransactionItem(
|
||||
transaction: Transaction
|
||||
): IWaterfallItemTransaction {
|
||||
|
@ -58,6 +69,7 @@ function getTransactionItem(
|
|||
duration: transaction.transaction.duration.us,
|
||||
timestamp: new Date(transaction['@timestamp']).getTime() * 1000,
|
||||
offset: 0,
|
||||
skew: 0,
|
||||
docType: 'transaction',
|
||||
transaction
|
||||
};
|
||||
|
@ -71,14 +83,13 @@ function getTransactionItem(
|
|||
duration: transaction.transaction.duration.us,
|
||||
timestamp: transaction.timestamp.us,
|
||||
offset: 0,
|
||||
skew: 0,
|
||||
docType: 'transaction',
|
||||
transaction
|
||||
};
|
||||
}
|
||||
|
||||
type PartialSpanItem = Omit<IWaterfallItemSpan, 'parentTransaction'>;
|
||||
|
||||
function getSpanItem(span: Span): PartialSpanItem {
|
||||
function getSpanItem(span: Span): IWaterfallItemSpan {
|
||||
if (span.version === 'v1') {
|
||||
return {
|
||||
id: span.span.id,
|
||||
|
@ -89,6 +100,7 @@ function getSpanItem(span: Span): PartialSpanItem {
|
|||
timestamp:
|
||||
new Date(span['@timestamp']).getTime() * 1000 + span.span.start.us,
|
||||
offset: 0,
|
||||
skew: 0,
|
||||
docType: 'span',
|
||||
span
|
||||
};
|
||||
|
@ -102,66 +114,94 @@ function getSpanItem(span: Span): PartialSpanItem {
|
|||
duration: span.span.duration.us,
|
||||
timestamp: span.timestamp.us,
|
||||
offset: 0,
|
||||
skew: 0,
|
||||
docType: 'span',
|
||||
span
|
||||
};
|
||||
}
|
||||
|
||||
export function getWaterfallRoot(
|
||||
items: Array<PartialSpanItem | IWaterfallItemTransaction>,
|
||||
function getClockSkew(
|
||||
item: IWaterfallItem,
|
||||
itemsById: IWaterfallIndex,
|
||||
parentTransactionSkew: number
|
||||
) {
|
||||
switch (item.docType) {
|
||||
case 'span':
|
||||
return parentTransactionSkew;
|
||||
case 'transaction': {
|
||||
if (!item.parentId) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const parentItem = itemsById[item.parentId];
|
||||
|
||||
// determine if child starts before the parent, and in that case how much
|
||||
const diff = parentItem.timestamp + parentItem.skew - item.timestamp;
|
||||
|
||||
// If child transaction starts after parent span there is no clock skew
|
||||
if (diff < 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// latency can only be calculated if parent duration is larger than child duration
|
||||
const latency = Math.max(parentItem.duration - item.duration, 0);
|
||||
const skew = diff + latency / 2;
|
||||
return skew;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getWaterfallItems(
|
||||
childrenByParentId: IWaterfallGroup,
|
||||
itemsById: IWaterfallIndex,
|
||||
entryTransactionItem: IWaterfallItem
|
||||
) {
|
||||
const itemsByParentId = groupBy(
|
||||
items,
|
||||
item => (item.parentId ? item.parentId : 'root')
|
||||
);
|
||||
const itemsById: IWaterfallIndex = {};
|
||||
function getSortedChildren(
|
||||
item: IWaterfallItem,
|
||||
parentTransactionSkew: number
|
||||
): IWaterfallItem[] {
|
||||
const skew = getClockSkew(item, itemsById, parentTransactionSkew);
|
||||
const children = sortBy(childrenByParentId[item.id] || [], 'timestamp');
|
||||
|
||||
const itemsByTransactionId = indexBy(
|
||||
items.filter(item => item.docType === 'transaction'),
|
||||
item => item.id
|
||||
) as { [key: string]: IWaterfallItemTransaction };
|
||||
item.childIds = children.map(child => child.id);
|
||||
item.offset = item.timestamp - entryTransactionItem.timestamp;
|
||||
item.skew = skew;
|
||||
|
||||
function getWithChildren(
|
||||
item: PartialSpanItem | IWaterfallItemTransaction
|
||||
): IWaterfallItem {
|
||||
const children = itemsByParentId[item.id] || [];
|
||||
const nextChildren = sortBy(children, 'timestamp').map(getWithChildren);
|
||||
let fullItem;
|
||||
|
||||
// add parent transaction to spans
|
||||
if (item.docType === 'span') {
|
||||
fullItem = {
|
||||
parentTransaction:
|
||||
itemsByTransactionId[item.span.transaction.id].transaction,
|
||||
...item,
|
||||
offset: item.timestamp - entryTransactionItem.timestamp,
|
||||
children: nextChildren
|
||||
};
|
||||
} else {
|
||||
fullItem = {
|
||||
...item,
|
||||
offset: item.timestamp - entryTransactionItem.timestamp,
|
||||
children: nextChildren
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: Think about storing this tree as a single, flat, indexed structure
|
||||
// with "children" being an array of ids, instead of it being a real tree
|
||||
itemsById[item.id] = fullItem;
|
||||
|
||||
return fullItem;
|
||||
const deepChildren = flatten(
|
||||
children.map(child => getSortedChildren(child, skew))
|
||||
);
|
||||
return [item, ...deepChildren];
|
||||
}
|
||||
|
||||
return { root: getWithChildren(entryTransactionItem), itemsById };
|
||||
return getSortedChildren(entryTransactionItem, 0);
|
||||
}
|
||||
|
||||
function getTraceRoot(childrenByParentId: IWaterfallGroup) {
|
||||
const item = first(childrenByParentId.root);
|
||||
if (item && item.docType === 'transaction') {
|
||||
return item.transaction;
|
||||
}
|
||||
}
|
||||
|
||||
function getServices(items: IWaterfallItem[]) {
|
||||
const serviceNames = items.map(item => item.serviceName);
|
||||
return uniq(serviceNames);
|
||||
}
|
||||
|
||||
export function getWaterfall(
|
||||
hits: Array<Span | Transaction>,
|
||||
services: string[],
|
||||
entryTransaction: Transaction
|
||||
): IWaterfall {
|
||||
const items = hits
|
||||
if (isEmpty(hits)) {
|
||||
return {
|
||||
services: [],
|
||||
items: [],
|
||||
itemsById: {},
|
||||
getTransactionById: () => undefined
|
||||
};
|
||||
}
|
||||
|
||||
const filteredHits = hits
|
||||
.filter(hit => {
|
||||
const docType = hit.processor.event;
|
||||
return ['span', 'transaction'].includes(docType);
|
||||
|
@ -178,13 +218,37 @@ export function getWaterfall(
|
|||
}
|
||||
});
|
||||
|
||||
const childrenByParentId = groupBy(
|
||||
filteredHits,
|
||||
hit => (hit.parentId ? hit.parentId : 'root')
|
||||
);
|
||||
const entryTransactionItem = getTransactionItem(entryTransaction);
|
||||
const { root, itemsById } = getWaterfallRoot(items, entryTransactionItem);
|
||||
const itemsById: IWaterfallIndex = indexBy(filteredHits, 'id');
|
||||
const items = getWaterfallItems(
|
||||
childrenByParentId,
|
||||
itemsById,
|
||||
entryTransactionItem
|
||||
);
|
||||
const traceRoot = getTraceRoot(childrenByParentId);
|
||||
|
||||
const getTransactionById = (id?: IWaterfallItem['id']) => {
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const item = itemsById[id];
|
||||
if (item.docType === 'transaction') {
|
||||
return item.transaction;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
duration: root.duration,
|
||||
services,
|
||||
childrenCount: hits.length,
|
||||
root,
|
||||
itemsById
|
||||
traceRoot,
|
||||
traceRootDuration: traceRoot && traceRoot.transaction.duration.us,
|
||||
duration: entryTransaction.transaction.duration.us,
|
||||
services: getServices(items),
|
||||
items,
|
||||
itemsById,
|
||||
getTransactionById
|
||||
};
|
||||
}
|
||||
|
|
|
@ -14,96 +14,55 @@ import {
|
|||
import { Transaction } from '../../../../../../typings/Transaction';
|
||||
|
||||
import { EuiBadge, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui';
|
||||
import { RRRRender } from 'react-redux-request';
|
||||
import { WaterfallV1Request } from 'x-pack/plugins/apm/public/store/reactReduxRequest/waterfallV1';
|
||||
import { WaterfallV2Request } from 'x-pack/plugins/apm/public/store/reactReduxRequest/waterfallV2';
|
||||
|
||||
import { IUrlParams } from 'x-pack/plugins/apm/public/store/urlParams';
|
||||
import { WaterfallResponse } from 'x-pack/plugins/apm/typings/waterfall';
|
||||
import { getAgentMarks } from './get_agent_marks';
|
||||
import { getServiceColors } from './getServiceColors';
|
||||
import { ServiceLegends } from './ServiceLegends';
|
||||
import { Waterfall } from './Waterfall';
|
||||
import { getWaterfall } from './Waterfall/waterfall_helpers/waterfall_helpers';
|
||||
import { IWaterfall } from './Waterfall/waterfall_helpers/waterfall_helpers';
|
||||
|
||||
interface Props {
|
||||
urlParams: IUrlParams;
|
||||
transaction: Transaction;
|
||||
location: any;
|
||||
}
|
||||
|
||||
interface WaterfallRequestProps {
|
||||
urlParams: IUrlParams;
|
||||
transaction: Transaction;
|
||||
render: RRRRender<WaterfallResponse>;
|
||||
}
|
||||
|
||||
function WaterfallRequest({
|
||||
urlParams,
|
||||
transaction,
|
||||
render
|
||||
}: WaterfallRequestProps) {
|
||||
const hasTrace = transaction.hasOwnProperty('trace');
|
||||
if (hasTrace) {
|
||||
return (
|
||||
<WaterfallV2Request
|
||||
urlParams={urlParams}
|
||||
transaction={transaction}
|
||||
render={render}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<WaterfallV1Request
|
||||
urlParams={urlParams}
|
||||
transaction={transaction}
|
||||
render={render}
|
||||
/>
|
||||
);
|
||||
}
|
||||
waterfall: IWaterfall;
|
||||
}
|
||||
|
||||
export function WaterfallContainer({
|
||||
location,
|
||||
urlParams,
|
||||
transaction
|
||||
transaction,
|
||||
waterfall
|
||||
}: Props) {
|
||||
return (
|
||||
<WaterfallRequest
|
||||
urlParams={urlParams}
|
||||
transaction={transaction}
|
||||
render={({ data }) => {
|
||||
const agentMarks = getAgentMarks(transaction);
|
||||
const waterfall = getWaterfall(data.hits, data.services, transaction);
|
||||
if (!waterfall) {
|
||||
return null;
|
||||
}
|
||||
const serviceColors = getServiceColors(waterfall.services);
|
||||
const agentMarks = getAgentMarks(transaction);
|
||||
if (!waterfall) {
|
||||
return null;
|
||||
}
|
||||
const serviceColors = getServiceColors(waterfall.services);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem>
|
||||
<ServiceLegends serviceColors={serviceColors} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiToolTip
|
||||
content="Distributed tracing is now supported as a beta feature of the APM UI timeline visualisation."
|
||||
position="left"
|
||||
>
|
||||
<EuiBadge color="hollow">Beta</EuiBadge>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<Waterfall
|
||||
agentMarks={agentMarks}
|
||||
location={location}
|
||||
serviceColors={serviceColors}
|
||||
urlParams={urlParams}
|
||||
waterfall={waterfall}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
return (
|
||||
<div>
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem>
|
||||
<ServiceLegends serviceColors={serviceColors} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiToolTip
|
||||
content="Distributed tracing is now supported as a beta feature of the APM UI timeline visualisation."
|
||||
position="left"
|
||||
>
|
||||
<EuiBadge color="hollow">Beta</EuiBadge>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<Waterfall
|
||||
agentMarks={agentMarks}
|
||||
location={location}
|
||||
serviceColors={serviceColors}
|
||||
urlParams={urlParams}
|
||||
waterfall={waterfall}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -22,20 +22,22 @@ import EmptyMessage from '../../../shared/EmptyMessage';
|
|||
import { TransactionLink } from '../../../shared/TransactionLink';
|
||||
import { DiscoverTransactionLink } from './ActionMenu';
|
||||
import { StickyTransactionProperties } from './StickyTransactionProperties';
|
||||
// @ts-ignore
|
||||
import { TransactionPropertiesTable } from './TransactionPropertiesTable';
|
||||
import { IWaterfall } from './WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers';
|
||||
|
||||
function MaybeViewTraceLink({
|
||||
root,
|
||||
transaction
|
||||
transaction,
|
||||
waterfall
|
||||
}: {
|
||||
root: ITransaction;
|
||||
transaction: ITransaction;
|
||||
waterfall: IWaterfall;
|
||||
}) {
|
||||
const isRoot = transaction.transaction.id === root.transaction.id;
|
||||
let button;
|
||||
const isRoot =
|
||||
transaction.transaction.id ===
|
||||
(waterfall.traceRoot && waterfall.traceRoot.transaction.id);
|
||||
|
||||
if (isRoot || !root) {
|
||||
let button;
|
||||
if (isRoot) {
|
||||
button = (
|
||||
<EuiToolTip content="Currently viewing the full trace">
|
||||
<EuiButton iconType="apmApp" disabled={true}>
|
||||
|
@ -49,7 +51,9 @@ function MaybeViewTraceLink({
|
|||
|
||||
return (
|
||||
<EuiFlexItem grow={false}>
|
||||
<TransactionLink transaction={root}>{button}</TransactionLink>
|
||||
<TransactionLink transaction={waterfall.traceRoot}>
|
||||
{button}
|
||||
</TransactionLink>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
}
|
||||
|
@ -58,14 +62,14 @@ interface Props {
|
|||
transaction: ITransaction;
|
||||
urlParams: IUrlParams;
|
||||
location: Location;
|
||||
waterfallRoot?: ITransaction;
|
||||
waterfall: IWaterfall;
|
||||
}
|
||||
|
||||
export const Transaction: React.SFC<Props> = ({
|
||||
transaction,
|
||||
urlParams,
|
||||
location,
|
||||
waterfallRoot
|
||||
waterfall
|
||||
}) => {
|
||||
if (isEmpty(transaction)) {
|
||||
return (
|
||||
|
@ -76,8 +80,6 @@ export const Transaction: React.SFC<Props> = ({
|
|||
);
|
||||
}
|
||||
|
||||
const root = waterfallRoot || transaction;
|
||||
|
||||
return (
|
||||
<EuiPanel paddingSize="m">
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
|
@ -96,14 +98,20 @@ export const Transaction: React.SFC<Props> = ({
|
|||
</EuiButtonEmpty>
|
||||
</DiscoverTransactionLink>
|
||||
</EuiFlexItem>
|
||||
<MaybeViewTraceLink transaction={transaction} root={root} />
|
||||
<MaybeViewTraceLink
|
||||
transaction={transaction}
|
||||
waterfall={waterfall}
|
||||
/>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiSpacer />
|
||||
|
||||
<StickyTransactionProperties transaction={transaction} root={root} />
|
||||
<StickyTransactionProperties
|
||||
transaction={transaction}
|
||||
totalDuration={waterfall.traceRootDuration}
|
||||
/>
|
||||
|
||||
<EuiSpacer />
|
||||
|
||||
|
@ -111,6 +119,7 @@ export const Transaction: React.SFC<Props> = ({
|
|||
transaction={transaction}
|
||||
location={location}
|
||||
urlParams={urlParams}
|
||||
waterfall={waterfall}
|
||||
/>
|
||||
</EuiPanel>
|
||||
);
|
||||
|
|
|
@ -6,29 +6,16 @@
|
|||
|
||||
import { connect } from 'react-redux';
|
||||
import { TransactionDetailsView } from 'x-pack/plugins/apm/public/components/app/TransactionDetails/view';
|
||||
import { selectWaterfallRoot } from 'x-pack/plugins/apm/public/store/selectors/waterfall';
|
||||
import {
|
||||
getUrlParams,
|
||||
IUrlParams
|
||||
} from 'x-pack/plugins/apm/public/store/urlParams';
|
||||
import { Transaction } from '../../../../typings/Transaction';
|
||||
import { getUrlParams } from 'x-pack/plugins/apm/public/store/urlParams';
|
||||
import { IReduxState } from '../../../store/rootReducer';
|
||||
|
||||
interface Props {
|
||||
location: any;
|
||||
urlParams: IUrlParams;
|
||||
waterfallRoot: Transaction;
|
||||
}
|
||||
|
||||
function mapStateToProps(state: any = {}, props: Partial<Props>) {
|
||||
function mapStateToProps(state = {} as IReduxState) {
|
||||
return {
|
||||
location: state.location,
|
||||
urlParams: getUrlParams(state),
|
||||
waterfallRoot: selectWaterfallRoot(state, props)
|
||||
urlParams: getUrlParams(state)
|
||||
};
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {};
|
||||
export const TransactionDetails = connect<{}, {}, Props>(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(TransactionDetailsView);
|
||||
export const TransactionDetails = connect(mapStateToProps)(
|
||||
TransactionDetailsView
|
||||
);
|
||||
|
|
|
@ -6,13 +6,13 @@
|
|||
|
||||
import { EuiSpacer } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { RRRRenderArgs } from 'react-redux-request';
|
||||
import { Transaction as ITransaction } from '../../../../typings/Transaction';
|
||||
import { RRRRenderResponse } from 'react-redux-request';
|
||||
// @ts-ignore
|
||||
import { TransactionDetailsRequest } from '../../../store/reactReduxRequest/transactionDetails';
|
||||
// @ts-ignore
|
||||
import { TransactionDetailsChartsRequest } from '../../../store/reactReduxRequest/transactionDetailsCharts';
|
||||
import { TransactionDistributionRequest } from '../../../store/reactReduxRequest/transactionDistribution';
|
||||
import { WaterfallRequest } from '../../../store/reactReduxRequest/waterfall';
|
||||
import { IUrlParams } from '../../../store/urlParams';
|
||||
// @ts-ignore
|
||||
import TransactionCharts from '../../shared/charts/TransactionCharts';
|
||||
|
@ -26,14 +26,9 @@ import { Transaction } from './Transaction';
|
|||
interface Props {
|
||||
urlParams: IUrlParams;
|
||||
location: any;
|
||||
waterfallRoot: ITransaction;
|
||||
}
|
||||
|
||||
export function TransactionDetailsView({
|
||||
urlParams,
|
||||
location,
|
||||
waterfallRoot
|
||||
}: Props) {
|
||||
export function TransactionDetailsView({ urlParams, location }: Props) {
|
||||
return (
|
||||
<div>
|
||||
<HeaderLarge>{urlParams.transactionName}</HeaderLarge>
|
||||
|
@ -44,7 +39,7 @@ export function TransactionDetailsView({
|
|||
|
||||
<TransactionDetailsChartsRequest
|
||||
urlParams={urlParams}
|
||||
render={({ data }: RRRRenderArgs<any>) => (
|
||||
render={({ data }: RRRRenderResponse<any>) => (
|
||||
<TransactionCharts
|
||||
charts={data}
|
||||
urlParams={urlParams}
|
||||
|
@ -55,7 +50,7 @@ export function TransactionDetailsView({
|
|||
|
||||
<TransactionDistributionRequest
|
||||
urlParams={urlParams}
|
||||
render={({ data }: RRRRenderArgs<any>) => (
|
||||
render={({ data }) => (
|
||||
<Distribution
|
||||
distribution={data}
|
||||
urlParams={urlParams}
|
||||
|
@ -68,13 +63,21 @@ export function TransactionDetailsView({
|
|||
|
||||
<TransactionDetailsRequest
|
||||
urlParams={urlParams}
|
||||
render={(res: RRRRenderArgs<any>) => {
|
||||
render={({ data: transaction }) => {
|
||||
return (
|
||||
<Transaction
|
||||
location={location}
|
||||
transaction={res.data}
|
||||
<WaterfallRequest
|
||||
urlParams={urlParams}
|
||||
waterfallRoot={waterfallRoot}
|
||||
transaction={transaction}
|
||||
render={({ data: waterfall }) => {
|
||||
return (
|
||||
<Transaction
|
||||
location={location}
|
||||
transaction={transaction}
|
||||
urlParams={urlParams}
|
||||
waterfall={waterfall}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
|
|
|
@ -9,7 +9,7 @@ import { Transaction } from '../../../typings/Transaction';
|
|||
import { KibanaLink, legacyEncodeURIComponent } from '../../utils/url';
|
||||
|
||||
interface TransactionLinkProps {
|
||||
transaction: Transaction;
|
||||
transaction?: Transaction;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -32,7 +32,7 @@ class Timeline extends PureComponent {
|
|||
|
||||
Timeline.propTypes = {
|
||||
agentMarks: PropTypes.array,
|
||||
duration: PropTypes.number.isRequired,
|
||||
duration: PropTypes.number,
|
||||
height: PropTypes.number.isRequired,
|
||||
header: PropTypes.node,
|
||||
margins: PropTypes.object.isRequired,
|
||||
|
|
|
@ -180,7 +180,7 @@ export async function loadSpans({
|
|||
}
|
||||
|
||||
export async function loadTrace({ traceId, start, end }: IUrlParams) {
|
||||
const result: WaterfallResponse = await callApi(
|
||||
const hits: WaterfallResponse = await callApi(
|
||||
{
|
||||
pathname: `/api/apm/traces/${traceId}`,
|
||||
query: {
|
||||
|
@ -193,8 +193,7 @@ export async function loadTrace({ traceId, start, end }: IUrlParams) {
|
|||
}
|
||||
);
|
||||
|
||||
result.hits = result.hits.map(addVersion);
|
||||
return result;
|
||||
return hits.map(addVersion);
|
||||
}
|
||||
|
||||
export async function loadTransaction({
|
||||
|
|
|
@ -5,19 +5,29 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { createInitialDataSelector } from './helpers';
|
||||
import { Request } from 'react-redux-request';
|
||||
import { Request, RRRRender } from 'react-redux-request';
|
||||
import { Transaction } from 'x-pack/plugins/apm/typings/Transaction';
|
||||
import { loadTransaction } from '../../services/rest/apm';
|
||||
import { IReduxState } from '../rootReducer';
|
||||
import { IUrlParams } from '../urlParams';
|
||||
// @ts-ignore
|
||||
import { createInitialDataSelector } from './helpers';
|
||||
|
||||
const ID = 'transactionDetails';
|
||||
const INITIAL_DATA = {};
|
||||
const withInitialData = createInitialDataSelector(INITIAL_DATA);
|
||||
|
||||
export function getTransactionDetails(state) {
|
||||
export function getTransactionDetails(state: IReduxState) {
|
||||
return withInitialData(state.reactReduxRequest[ID]);
|
||||
}
|
||||
|
||||
export function TransactionDetailsRequest({ urlParams, render }) {
|
||||
export function TransactionDetailsRequest({
|
||||
urlParams,
|
||||
render
|
||||
}: {
|
||||
urlParams: IUrlParams;
|
||||
render: RRRRender<Transaction>;
|
||||
}) {
|
||||
const { serviceName, start, end, transactionId, traceId, kuery } = urlParams;
|
||||
|
||||
if (!(serviceName && start && end && transactionId)) {
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Request, RRRRenderArgs } from 'react-redux-request';
|
||||
import { Request, RRRRender, RRRRenderResponse } from 'react-redux-request';
|
||||
import { IDistributionResponse } from '../../../server/lib/transactions/distribution/get_distribution';
|
||||
import { loadTransactionDistribution } from '../../services/rest/apm';
|
||||
import { IReduxState } from '../rootReducer';
|
||||
|
@ -17,13 +17,9 @@ const ID = 'transactionDistribution';
|
|||
const INITIAL_DATA = { buckets: [], totalHits: 0 };
|
||||
const withInitialData = createInitialDataSelector(INITIAL_DATA);
|
||||
|
||||
interface RrrResponse<T> {
|
||||
data: T;
|
||||
}
|
||||
|
||||
export function getTransactionDistribution(
|
||||
state: IReduxState
|
||||
): RrrResponse<IDistributionResponse> {
|
||||
): RRRRenderResponse<IDistributionResponse> {
|
||||
return withInitialData(state.reactReduxRequest[ID]);
|
||||
}
|
||||
|
||||
|
@ -41,7 +37,7 @@ export function TransactionDistributionRequest({
|
|||
render
|
||||
}: {
|
||||
urlParams: IUrlParams;
|
||||
render: (args: RRRRenderArgs<any>) => any;
|
||||
render: RRRRender<IDistributionResponse>;
|
||||
}) {
|
||||
const { serviceName, start, end, transactionName, kuery } = urlParams;
|
||||
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* 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 from 'react';
|
||||
import { RRRRender } from 'react-redux-request';
|
||||
import { Transaction } from 'x-pack/plugins/apm/typings/Transaction';
|
||||
import { IWaterfall } from '../../components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers';
|
||||
import { IUrlParams } from '../urlParams';
|
||||
import { WaterfallV1Request } from './waterfallV1';
|
||||
import { WaterfallV2Request } from './waterfallV2';
|
||||
|
||||
interface Props {
|
||||
urlParams: IUrlParams;
|
||||
transaction: Transaction;
|
||||
render: RRRRender<IWaterfall>;
|
||||
}
|
||||
|
||||
export function WaterfallRequest({ urlParams, transaction, render }: Props) {
|
||||
const hasTrace = transaction.hasOwnProperty('trace');
|
||||
if (hasTrace) {
|
||||
return (
|
||||
<WaterfallV2Request
|
||||
urlParams={urlParams}
|
||||
transaction={transaction}
|
||||
render={render}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<WaterfallV1Request
|
||||
urlParams={urlParams}
|
||||
transaction={transaction}
|
||||
render={render}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -13,7 +13,10 @@ import {
|
|||
} from 'x-pack/plugins/apm/common/constants';
|
||||
import { Span } from 'x-pack/plugins/apm/typings/Span';
|
||||
import { Transaction } from 'x-pack/plugins/apm/typings/Transaction';
|
||||
import { WaterfallResponse } from 'x-pack/plugins/apm/typings/waterfall';
|
||||
import {
|
||||
getWaterfall,
|
||||
IWaterfall
|
||||
} from '../../components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers';
|
||||
import { loadSpans } from '../../services/rest/apm';
|
||||
import { IUrlParams } from '../urlParams';
|
||||
// @ts-ignore
|
||||
|
@ -24,7 +27,7 @@ export const ID = 'waterfallV1';
|
|||
interface Props {
|
||||
urlParams: IUrlParams;
|
||||
transaction: Transaction;
|
||||
render: RRRRender<WaterfallResponse>;
|
||||
render: RRRRender<IWaterfall>;
|
||||
}
|
||||
|
||||
export function WaterfallV1Request({ urlParams, transaction, render }: Props) {
|
||||
|
@ -42,12 +45,8 @@ export function WaterfallV1Request({ urlParams, transaction, render }: Props) {
|
|||
fn={loadSpans}
|
||||
args={[{ serviceName, start, end, transactionId }]}
|
||||
render={({ status, data = [], args }) => {
|
||||
const res = {
|
||||
hits: [transaction, ...data],
|
||||
services: [serviceName]
|
||||
};
|
||||
|
||||
return render({ status, data: res, args });
|
||||
const waterfall = getWaterfall([transaction, ...data], transaction);
|
||||
return render({ status, data: waterfall, args });
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -10,6 +10,10 @@ import { Request, RRRRender } from 'react-redux-request';
|
|||
import { TRACE_ID } from 'x-pack/plugins/apm/common/constants';
|
||||
import { Transaction } from 'x-pack/plugins/apm/typings/Transaction';
|
||||
import { WaterfallResponse } from 'x-pack/plugins/apm/typings/waterfall';
|
||||
import {
|
||||
getWaterfall,
|
||||
IWaterfall
|
||||
} from '../../components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers';
|
||||
import { loadTrace } from '../../services/rest/apm';
|
||||
import { IUrlParams } from '../urlParams';
|
||||
// @ts-ignore
|
||||
|
@ -20,10 +24,9 @@ export const ID = 'waterfallV2';
|
|||
interface Props {
|
||||
urlParams: IUrlParams;
|
||||
transaction: Transaction;
|
||||
render: RRRRender<WaterfallResponse>;
|
||||
render: RRRRender<IWaterfall>;
|
||||
}
|
||||
|
||||
const defaultData = { hits: [], services: [] };
|
||||
export function WaterfallV2Request({ urlParams, transaction, render }: Props) {
|
||||
const { start, end } = urlParams;
|
||||
const traceId: string = get(transaction, TRACE_ID);
|
||||
|
@ -37,9 +40,10 @@ export function WaterfallV2Request({ urlParams, transaction, render }: Props) {
|
|||
id={ID}
|
||||
fn={loadTrace}
|
||||
args={[{ traceId, start, end }]}
|
||||
render={({ args, data = defaultData, status }) =>
|
||||
render({ args, data, status })
|
||||
}
|
||||
render={({ args, data = [], status }) => {
|
||||
const waterfall = getWaterfall(data, transaction);
|
||||
return render({ args, data: waterfall, status });
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,47 +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.
|
||||
*/
|
||||
|
||||
import { RRRRenderArgs } from 'react-redux-request';
|
||||
import { createSelector, ParametricSelector } from 'reselect';
|
||||
import { TransactionV2 } from '../../../typings/Transaction';
|
||||
import { WaterfallResponse } from '../../../typings/waterfall';
|
||||
import { ID as v1ID } from '../reactReduxRequest/waterfallV1';
|
||||
import { ID as v2ID } from '../reactReduxRequest/waterfallV2';
|
||||
|
||||
interface ReduxState {
|
||||
reactReduxRequest: {
|
||||
[v1ID]?: RRRRenderArgs<WaterfallResponse>;
|
||||
[v2ID]?: RRRRenderArgs<WaterfallResponse>;
|
||||
};
|
||||
}
|
||||
|
||||
export const selectWaterfall: ParametricSelector<
|
||||
ReduxState,
|
||||
any,
|
||||
WaterfallResponse | null
|
||||
> = state => {
|
||||
const waterfall =
|
||||
state.reactReduxRequest[v1ID] || state.reactReduxRequest[v2ID];
|
||||
|
||||
if (!waterfall || !waterfall.data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return waterfall.data;
|
||||
};
|
||||
|
||||
export const selectWaterfallRoot = createSelector(
|
||||
[selectWaterfall],
|
||||
waterfall => {
|
||||
if (!waterfall || !waterfall.hits) {
|
||||
return;
|
||||
}
|
||||
|
||||
return waterfall.hits.find(
|
||||
hit => hit.version === 'v2' && !hit.parent
|
||||
) as TransactionV2;
|
||||
}
|
||||
);
|
|
@ -6,8 +6,7 @@
|
|||
|
||||
import { SearchParams, SearchResponse } from 'elasticsearch';
|
||||
import { WaterfallResponse } from 'x-pack/plugins/apm/typings/waterfall';
|
||||
import { SERVICE_NAME, TRACE_ID } from '../../../common/constants';
|
||||
import { TermsAggsBucket } from '../../../typings/elasticsearch';
|
||||
import { TRACE_ID } from '../../../common/constants';
|
||||
import { Span } from '../../../typings/Span';
|
||||
import { Transaction } from '../../../typings/Transaction';
|
||||
import { Setup } from '../helpers/setup_request';
|
||||
|
@ -37,14 +36,6 @@ export async function getTrace(
|
|||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
aggs: {
|
||||
services: {
|
||||
terms: {
|
||||
field: SERVICE_NAME,
|
||||
size: 500
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -54,10 +45,5 @@ export async function getTrace(
|
|||
params
|
||||
);
|
||||
|
||||
return {
|
||||
services: (resp.aggregations.services.buckets as TermsAggsBucket[]).map(
|
||||
bucket => bucket.key
|
||||
),
|
||||
hits: resp.hits.hits.map(hit => hit._source)
|
||||
};
|
||||
return resp.hits.hits.map(hit => hit._source);
|
||||
}
|
||||
|
|
|
@ -9,14 +9,14 @@
|
|||
declare module 'react-redux-request' {
|
||||
import React from 'react';
|
||||
|
||||
export interface RRRRenderArgs<T, P = any[]> {
|
||||
export interface RRRRenderResponse<T, P = any[]> {
|
||||
status: 'SUCCESS' | 'LOADING' | 'FAILURE';
|
||||
data: T;
|
||||
args: P;
|
||||
}
|
||||
|
||||
export type RRRRender<T, P = any[]> = (
|
||||
args: RRRRenderArgs<T, P>
|
||||
res: RRRRenderResponse<T, P>
|
||||
) => JSX.Element | null;
|
||||
|
||||
export interface RequestProps<T, P> {
|
||||
|
|
|
@ -7,7 +7,4 @@
|
|||
import { Span } from './Span';
|
||||
import { Transaction } from './Transaction';
|
||||
|
||||
export interface WaterfallResponse {
|
||||
services: string[];
|
||||
hits: Array<Transaction | Span>;
|
||||
}
|
||||
export type WaterfallResponse = Array<Transaction | Span>;
|
||||
|
|
Loading…
Reference in a new issue