[APM] Color by span type when there's only one service (#90424)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
acb7b7726e
commit
e2abb03ad0
|
@ -11,7 +11,7 @@ import { getErrorMarks } from './get_error_marks';
|
|||
describe('getErrorMarks', () => {
|
||||
describe('returns empty array', () => {
|
||||
it('when items are missing', () => {
|
||||
expect(getErrorMarks([], {})).toEqual([]);
|
||||
expect(getErrorMarks([])).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -22,17 +22,17 @@ describe('getErrorMarks', () => {
|
|||
offset: 10,
|
||||
skew: 5,
|
||||
doc: { error: { id: 1 }, service: { name: 'opbeans-java' } },
|
||||
color: 'red',
|
||||
} as unknown,
|
||||
{
|
||||
docType: 'error',
|
||||
offset: 50,
|
||||
skew: 0,
|
||||
doc: { error: { id: 2 }, service: { name: 'opbeans-node' } },
|
||||
color: 'blue',
|
||||
} as unknown,
|
||||
] as IWaterfallError[];
|
||||
expect(
|
||||
getErrorMarks(items, { 'opbeans-java': 'red', 'opbeans-node': 'blue' })
|
||||
).toEqual([
|
||||
expect(getErrorMarks(items)).toEqual([
|
||||
{
|
||||
type: 'errorMark',
|
||||
offset: 15,
|
||||
|
@ -59,22 +59,24 @@ describe('getErrorMarks', () => {
|
|||
offset: 10,
|
||||
skew: 5,
|
||||
doc: { error: { id: 1 }, service: { name: 'opbeans-java' } },
|
||||
color: '',
|
||||
} as unknown,
|
||||
{
|
||||
docType: 'error',
|
||||
offset: 50,
|
||||
skew: 0,
|
||||
doc: { error: { id: 2 }, service: { name: 'opbeans-node' } },
|
||||
color: '',
|
||||
} as unknown,
|
||||
] as IWaterfallError[];
|
||||
expect(getErrorMarks(items, {})).toEqual([
|
||||
expect(getErrorMarks(items)).toEqual([
|
||||
{
|
||||
type: 'errorMark',
|
||||
offset: 15,
|
||||
verticalLine: false,
|
||||
id: 1,
|
||||
error: { error: { id: 1 }, service: { name: 'opbeans-java' } },
|
||||
serviceColor: undefined,
|
||||
serviceColor: '',
|
||||
},
|
||||
{
|
||||
type: 'errorMark',
|
||||
|
@ -82,7 +84,7 @@ describe('getErrorMarks', () => {
|
|||
verticalLine: false,
|
||||
id: 2,
|
||||
error: { error: { id: 2 }, service: { name: 'opbeans-node' } },
|
||||
serviceColor: undefined,
|
||||
serviceColor: '',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
|
|
@ -7,22 +7,16 @@
|
|||
|
||||
import { isEmpty } from 'lodash';
|
||||
import { ErrorRaw } from '../../../../../../../typings/es_schemas/raw/error_raw';
|
||||
import {
|
||||
IWaterfallError,
|
||||
IServiceColors,
|
||||
} from '../Waterfall/waterfall_helpers/waterfall_helpers';
|
||||
import { IWaterfallError } from '../Waterfall/waterfall_helpers/waterfall_helpers';
|
||||
import { Mark } from '.';
|
||||
|
||||
export interface ErrorMark extends Mark {
|
||||
type: 'errorMark';
|
||||
error: ErrorRaw;
|
||||
serviceColor?: string;
|
||||
serviceColor: string;
|
||||
}
|
||||
|
||||
export const getErrorMarks = (
|
||||
errorItems: IWaterfallError[],
|
||||
serviceColors: IServiceColors
|
||||
): ErrorMark[] => {
|
||||
export const getErrorMarks = (errorItems: IWaterfallError[]): ErrorMark[] => {
|
||||
if (isEmpty(errorItems)) {
|
||||
return [];
|
||||
}
|
||||
|
@ -33,6 +27,6 @@ export const getErrorMarks = (
|
|||
verticalLine: false,
|
||||
id: error.doc.error.id,
|
||||
error: error.doc,
|
||||
serviceColor: serviceColors[error.doc.service.name],
|
||||
serviceColor: error.color,
|
||||
}));
|
||||
};
|
||||
|
|
|
@ -1,46 +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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiTitle } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common';
|
||||
import { px, unit } from '../../../../../style/variables';
|
||||
import { Legend } from '../../../../shared/charts/Legend';
|
||||
import { IServiceColors } from './Waterfall/waterfall_helpers/waterfall_helpers';
|
||||
|
||||
const Legends = euiStyled.div`
|
||||
display: flex;
|
||||
|
||||
> * {
|
||||
margin-right: ${px(unit)};
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
serviceColors: IServiceColors;
|
||||
}
|
||||
|
||||
export function ServiceLegends({ serviceColors }: Props) {
|
||||
return (
|
||||
<Legends>
|
||||
<EuiTitle size="xxxs">
|
||||
<span>
|
||||
{i18n.translate('xpack.apm.transactionDetails.servicesTitle', {
|
||||
defaultMessage: 'Services',
|
||||
})}
|
||||
</span>
|
||||
</EuiTitle>
|
||||
{Object.entries(serviceColors).map(([label, color]) => (
|
||||
<Legend key={color} color={color} text={label} />
|
||||
))}
|
||||
</Legends>
|
||||
);
|
||||
}
|
|
@ -14,7 +14,7 @@ import { asDuration } from '../../../../../../../common/utils/formatters';
|
|||
import { isRumAgentName } from '../../../../../../../common/agent_name';
|
||||
import { px, unit, units } from '../../../../../../style/variables';
|
||||
import { ErrorCount } from '../../ErrorCount';
|
||||
import { IWaterfallItem } from './waterfall_helpers/waterfall_helpers';
|
||||
import { IWaterfallSpanOrTransaction } from './waterfall_helpers/waterfall_helpers';
|
||||
import { ErrorOverviewLink } from '../../../../../shared/Links/apm/ErrorOverviewLink';
|
||||
import { TRACE_ID } from '../../../../../../../common/elasticsearch_fieldnames';
|
||||
import { SyncBadge } from './SyncBadge';
|
||||
|
@ -75,14 +75,14 @@ const ItemText = euiStyled.span`
|
|||
interface IWaterfallItemProps {
|
||||
timelineMargins: Margins;
|
||||
totalDuration?: number;
|
||||
item: IWaterfallItem;
|
||||
item: IWaterfallSpanOrTransaction;
|
||||
color: string;
|
||||
isSelected: boolean;
|
||||
errorCount: number;
|
||||
onClick: () => unknown;
|
||||
}
|
||||
|
||||
function PrefixIcon({ item }: { item: IWaterfallItem }) {
|
||||
function PrefixIcon({ item }: { item: IWaterfallSpanOrTransaction }) {
|
||||
switch (item.docType) {
|
||||
case 'span': {
|
||||
// icon for database spans
|
||||
|
@ -110,7 +110,7 @@ function PrefixIcon({ item }: { item: IWaterfallItem }) {
|
|||
|
||||
interface SpanActionToolTipProps {
|
||||
children: ReactNode;
|
||||
item?: IWaterfallItem;
|
||||
item?: IWaterfallSpanOrTransaction;
|
||||
}
|
||||
|
||||
function SpanActionToolTip({ item, children }: SpanActionToolTipProps) {
|
||||
|
@ -124,7 +124,7 @@ function SpanActionToolTip({ item, children }: SpanActionToolTipProps) {
|
|||
return <>{children}</>;
|
||||
}
|
||||
|
||||
function Duration({ item }: { item: IWaterfallItem }) {
|
||||
function Duration({ item }: { item: IWaterfallSpanOrTransaction }) {
|
||||
return (
|
||||
<EuiText color="subdued" size="xs">
|
||||
{asDuration(item.duration)}
|
||||
|
@ -132,7 +132,7 @@ function Duration({ item }: { item: IWaterfallItem }) {
|
|||
);
|
||||
}
|
||||
|
||||
function HttpStatusCode({ item }: { item: IWaterfallItem }) {
|
||||
function HttpStatusCode({ item }: { item: IWaterfallSpanOrTransaction }) {
|
||||
// http status code for transactions of type 'request'
|
||||
const httpStatusCode =
|
||||
item.docType === 'transaction' && item.doc.transaction.type === 'request'
|
||||
|
@ -146,7 +146,7 @@ function HttpStatusCode({ item }: { item: IWaterfallItem }) {
|
|||
return <EuiText size="xs">{httpStatusCode}</EuiText>;
|
||||
}
|
||||
|
||||
function NameLabel({ item }: { item: IWaterfallItem }) {
|
||||
function NameLabel({ item }: { item: IWaterfallSpanOrTransaction }) {
|
||||
switch (item.docType) {
|
||||
case 'span':
|
||||
return <EuiText size="s">{item.doc.span.name}</EuiText>;
|
||||
|
@ -156,8 +156,6 @@ function NameLabel({ item }: { item: IWaterfallItem }) {
|
|||
<h5>{item.doc.transaction.name}</h5>
|
||||
</EuiTitle>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -14,22 +14,21 @@ import { Margins } from '../../../../../shared/charts/Timeline';
|
|||
import { WaterfallItem } from './WaterfallItem';
|
||||
import {
|
||||
IWaterfall,
|
||||
IWaterfallItem,
|
||||
IWaterfallSpanOrTransaction,
|
||||
} from './waterfall_helpers/waterfall_helpers';
|
||||
|
||||
interface AccordionWaterfallProps {
|
||||
isOpen: boolean;
|
||||
item: IWaterfallItem;
|
||||
item: IWaterfallSpanOrTransaction;
|
||||
level: number;
|
||||
serviceColors: IWaterfall['serviceColors'];
|
||||
duration: IWaterfall['duration'];
|
||||
waterfallItemId?: string;
|
||||
location: Location;
|
||||
errorsPerTransaction: IWaterfall['errorsPerTransaction'];
|
||||
childrenByParentId: Record<string, IWaterfallItem[]>;
|
||||
childrenByParentId: Record<string, IWaterfallSpanOrTransaction[]>;
|
||||
onToggleEntryTransaction?: () => void;
|
||||
timelineMargins: Margins;
|
||||
onClickWaterfallItem: (item: IWaterfallItem) => void;
|
||||
onClickWaterfallItem: (item: IWaterfallSpanOrTransaction) => void;
|
||||
}
|
||||
|
||||
const StyledAccordion = euiStyled(EuiAccordion).withConfig({
|
||||
|
@ -98,7 +97,6 @@ export function AccordionWaterfall(props: AccordionWaterfallProps) {
|
|||
const {
|
||||
item,
|
||||
level,
|
||||
serviceColors,
|
||||
duration,
|
||||
childrenByParentId,
|
||||
waterfallItemId,
|
||||
|
@ -134,7 +132,7 @@ export function AccordionWaterfall(props: AccordionWaterfallProps) {
|
|||
<WaterfallItem
|
||||
key={item.id}
|
||||
timelineMargins={timelineMargins}
|
||||
color={serviceColors[item.doc.service.name]}
|
||||
color={item.color}
|
||||
item={item}
|
||||
totalDuration={duration}
|
||||
isSelected={item.id === waterfallItemId}
|
||||
|
@ -161,7 +159,6 @@ export function AccordionWaterfall(props: AccordionWaterfallProps) {
|
|||
isOpen={isOpen}
|
||||
item={child}
|
||||
level={nextLevel}
|
||||
serviceColors={serviceColors}
|
||||
waterfallItemId={waterfallItemId}
|
||||
location={location}
|
||||
errorsPerTransaction={errorsPerTransaction}
|
||||
|
|
|
@ -21,6 +21,7 @@ import { WaterfallFlyout } from './WaterfallFlyout';
|
|||
import {
|
||||
IWaterfall,
|
||||
IWaterfallItem,
|
||||
IWaterfallSpanOrTransaction,
|
||||
} from './waterfall_helpers/waterfall_helpers';
|
||||
|
||||
const Container = euiStyled.div`
|
||||
|
@ -76,13 +77,13 @@ export function Waterfall({
|
|||
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.items.length;
|
||||
|
||||
const { serviceColors, duration } = waterfall;
|
||||
const { duration } = waterfall;
|
||||
|
||||
const agentMarks = getAgentMarks(waterfall.entryWaterfallTransaction?.doc);
|
||||
const errorMarks = getErrorMarks(waterfall.errorItems, serviceColors);
|
||||
const errorMarks = getErrorMarks(waterfall.errorItems);
|
||||
|
||||
function renderItems(
|
||||
childrenByParentId: Record<string | number, IWaterfallItem[]>
|
||||
childrenByParentId: Record<string | number, IWaterfallSpanOrTransaction[]>
|
||||
) {
|
||||
const { entryWaterfallTransaction } = waterfall;
|
||||
if (!entryWaterfallTransaction) {
|
||||
|
@ -95,7 +96,6 @@ export function Waterfall({
|
|||
isOpen={isAccordionOpen}
|
||||
item={entryWaterfallTransaction}
|
||||
level={0}
|
||||
serviceColors={serviceColors}
|
||||
waterfallItemId={waterfallItemId}
|
||||
location={location}
|
||||
errorsPerTransaction={waterfall.errorsPerTransaction}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -15,6 +15,7 @@ import {
|
|||
IWaterfallItem,
|
||||
IWaterfallTransaction,
|
||||
IWaterfallError,
|
||||
IWaterfallSpanOrTransaction,
|
||||
} from './waterfall_helpers';
|
||||
import { APMError } from '../../../../../../../../typings/es_schemas/ui/apm_error';
|
||||
|
||||
|
@ -377,7 +378,12 @@ describe('waterfall_helpers', () => {
|
|||
|
||||
describe('getWaterfallItems', () => {
|
||||
it('should order items correctly', () => {
|
||||
const items: IWaterfallItem[] = [
|
||||
const legendValues = {
|
||||
serviceName: 'opbeans-java',
|
||||
spanType: '',
|
||||
};
|
||||
|
||||
const items: IWaterfallSpanOrTransaction[] = [
|
||||
{
|
||||
docType: 'span',
|
||||
doc: {
|
||||
|
@ -397,6 +403,8 @@ describe('waterfall_helpers', () => {
|
|||
duration: 210,
|
||||
offset: 0,
|
||||
skew: 0,
|
||||
legendValues,
|
||||
color: '',
|
||||
},
|
||||
{
|
||||
docType: 'span',
|
||||
|
@ -417,6 +425,8 @@ describe('waterfall_helpers', () => {
|
|||
duration: 4694,
|
||||
offset: 0,
|
||||
skew: 0,
|
||||
legendValues,
|
||||
color: '',
|
||||
},
|
||||
{
|
||||
docType: 'span',
|
||||
|
@ -437,6 +447,8 @@ describe('waterfall_helpers', () => {
|
|||
duration: 4694,
|
||||
offset: 0,
|
||||
skew: 0,
|
||||
legendValues,
|
||||
color: '',
|
||||
},
|
||||
{
|
||||
docType: 'transaction',
|
||||
|
@ -451,6 +463,8 @@ describe('waterfall_helpers', () => {
|
|||
duration: 3581,
|
||||
offset: 0,
|
||||
skew: 0,
|
||||
legendValues,
|
||||
color: '',
|
||||
},
|
||||
{
|
||||
docType: 'transaction',
|
||||
|
@ -466,6 +480,8 @@ describe('waterfall_helpers', () => {
|
|||
duration: 9480,
|
||||
offset: 0,
|
||||
skew: 0,
|
||||
legendValues,
|
||||
color: '',
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -489,7 +505,7 @@ describe('waterfall_helpers', () => {
|
|||
transaction: { id: 'a' },
|
||||
timestamp: { us: 10 },
|
||||
} as unknown) as Transaction,
|
||||
} as IWaterfallItem,
|
||||
} as IWaterfallSpanOrTransaction,
|
||||
{
|
||||
docType: 'span',
|
||||
id: 'b',
|
||||
|
@ -501,7 +517,7 @@ describe('waterfall_helpers', () => {
|
|||
parent: { id: 'a' },
|
||||
timestamp: { us: 20 },
|
||||
} as unknown) as Span,
|
||||
} as IWaterfallItem,
|
||||
} as IWaterfallSpanOrTransaction,
|
||||
];
|
||||
const childrenByParentId = groupBy(items, (hit) =>
|
||||
hit.parentId ? hit.parentId : 'root'
|
||||
|
@ -522,7 +538,7 @@ describe('waterfall_helpers', () => {
|
|||
timestamp: { us: 0 },
|
||||
},
|
||||
duration: 50,
|
||||
} as IWaterfallItem;
|
||||
} as IWaterfallSpanOrTransaction;
|
||||
|
||||
const parent = {
|
||||
docType: 'transaction',
|
||||
|
@ -531,7 +547,7 @@ describe('waterfall_helpers', () => {
|
|||
},
|
||||
duration: 100,
|
||||
skew: 5,
|
||||
} as IWaterfallItem;
|
||||
} as IWaterfallSpanOrTransaction;
|
||||
|
||||
expect(getClockSkew(child, parent)).toBe(130);
|
||||
});
|
||||
|
@ -543,7 +559,7 @@ describe('waterfall_helpers', () => {
|
|||
timestamp: { us: 250 },
|
||||
},
|
||||
duration: 50,
|
||||
} as IWaterfallItem;
|
||||
} as IWaterfallSpanOrTransaction;
|
||||
|
||||
const parent = {
|
||||
docType: 'transaction',
|
||||
|
@ -552,7 +568,7 @@ describe('waterfall_helpers', () => {
|
|||
},
|
||||
duration: 100,
|
||||
skew: 5,
|
||||
} as IWaterfallItem;
|
||||
} as IWaterfallSpanOrTransaction;
|
||||
|
||||
expect(getClockSkew(child, parent)).toBe(0);
|
||||
});
|
||||
|
@ -564,7 +580,7 @@ describe('waterfall_helpers', () => {
|
|||
timestamp: { us: 150 },
|
||||
},
|
||||
duration: 50,
|
||||
} as IWaterfallItem;
|
||||
} as IWaterfallSpanOrTransaction;
|
||||
|
||||
const parent = {
|
||||
docType: 'transaction',
|
||||
|
@ -573,7 +589,7 @@ describe('waterfall_helpers', () => {
|
|||
},
|
||||
duration: 100,
|
||||
skew: 5,
|
||||
} as IWaterfallItem;
|
||||
} as IWaterfallSpanOrTransaction;
|
||||
|
||||
expect(getClockSkew(child, parent)).toBe(0);
|
||||
});
|
||||
|
@ -590,7 +606,7 @@ describe('waterfall_helpers', () => {
|
|||
},
|
||||
duration: 100,
|
||||
skew: 5,
|
||||
} as IWaterfallItem;
|
||||
} as IWaterfallSpanOrTransaction;
|
||||
|
||||
expect(getClockSkew(child, parent)).toBe(5);
|
||||
});
|
||||
|
@ -607,7 +623,7 @@ describe('waterfall_helpers', () => {
|
|||
},
|
||||
duration: 100,
|
||||
skew: 5,
|
||||
} as IWaterfallItem;
|
||||
} as IWaterfallSpanOrTransaction;
|
||||
|
||||
expect(getClockSkew(child, parent)).toBe(5);
|
||||
});
|
||||
|
|
|
@ -5,17 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import theme from '@elastic/eui/dist/eui_theme_light.json';
|
||||
import {
|
||||
first,
|
||||
flatten,
|
||||
groupBy,
|
||||
isEmpty,
|
||||
sortBy,
|
||||
sum,
|
||||
uniq,
|
||||
zipObject,
|
||||
} from 'lodash';
|
||||
import { euiPaletteColorBlind } from '@elastic/eui';
|
||||
import { first, flatten, groupBy, isEmpty, sortBy, sum, uniq } from 'lodash';
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
import { TraceAPIResponse } from '../../../../../../../../server/lib/traces/get_trace';
|
||||
import { APMError } from '../../../../../../../../typings/es_schemas/ui/apm_error';
|
||||
|
@ -23,11 +14,16 @@ import { Span } from '../../../../../../../../typings/es_schemas/ui/span';
|
|||
import { Transaction } from '../../../../../../../../typings/es_schemas/ui/transaction';
|
||||
|
||||
interface IWaterfallGroup {
|
||||
[key: string]: IWaterfallItem[];
|
||||
[key: string]: IWaterfallSpanOrTransaction[];
|
||||
}
|
||||
|
||||
const ROOT_ID = 'root';
|
||||
|
||||
export enum WaterfallLegendType {
|
||||
ServiceName = 'serviceName',
|
||||
SpanType = 'spanType',
|
||||
}
|
||||
|
||||
export interface IWaterfall {
|
||||
entryWaterfallTransaction?: IWaterfallTransaction;
|
||||
rootTransaction?: Transaction;
|
||||
|
@ -37,46 +33,69 @@ export interface IWaterfall {
|
|||
*/
|
||||
duration: number;
|
||||
items: IWaterfallItem[];
|
||||
childrenByParentId: Record<string | number, IWaterfallItem[]>;
|
||||
childrenByParentId: Record<string | number, IWaterfallSpanOrTransaction[]>;
|
||||
errorsPerTransaction: TraceAPIResponse['errorsPerTransaction'];
|
||||
errorsCount: number;
|
||||
serviceColors: IServiceColors;
|
||||
legends: IWaterfallLegend[];
|
||||
errorItems: IWaterfallError[];
|
||||
}
|
||||
|
||||
interface IWaterfallItemBase<T, U> {
|
||||
docType: U;
|
||||
doc: T;
|
||||
|
||||
id: string;
|
||||
|
||||
parent?: IWaterfallItem;
|
||||
parentId?: string;
|
||||
|
||||
interface IWaterfallSpanItemBase<TDocument, TDoctype>
|
||||
extends IWaterfallItemBase<TDocument, TDoctype> {
|
||||
/**
|
||||
* Latency in us
|
||||
*/
|
||||
duration: number;
|
||||
legendValues: Record<WaterfallLegendType, string>;
|
||||
}
|
||||
|
||||
interface IWaterfallItemBase<TDocument, TDoctype> {
|
||||
doc: TDocument;
|
||||
docType: TDoctype;
|
||||
id: string;
|
||||
parent?: IWaterfallSpanOrTransaction;
|
||||
parentId?: string;
|
||||
color: string;
|
||||
/**
|
||||
* offset from first item in us
|
||||
*/
|
||||
offset: number;
|
||||
|
||||
/**
|
||||
* skew from timestamp in us
|
||||
*/
|
||||
skew: number;
|
||||
}
|
||||
|
||||
export type IWaterfallTransaction = IWaterfallItemBase<
|
||||
export type IWaterfallError = IWaterfallItemBase<APMError, 'error'>;
|
||||
|
||||
export type IWaterfallTransaction = IWaterfallSpanItemBase<
|
||||
Transaction,
|
||||
'transaction'
|
||||
>;
|
||||
export type IWaterfallSpan = IWaterfallItemBase<Span, 'span'>;
|
||||
export type IWaterfallError = IWaterfallItemBase<APMError, 'error'>;
|
||||
|
||||
export type IWaterfallItem = IWaterfallTransaction | IWaterfallSpan;
|
||||
export type IWaterfallSpan = IWaterfallSpanItemBase<Span, 'span'>;
|
||||
|
||||
export type IWaterfallSpanOrTransaction =
|
||||
| IWaterfallTransaction
|
||||
| IWaterfallSpan;
|
||||
|
||||
export type IWaterfallItem = IWaterfallSpanOrTransaction | IWaterfallError;
|
||||
|
||||
export interface IWaterfallLegend {
|
||||
type: WaterfallLegendType;
|
||||
value: string | undefined;
|
||||
color: string;
|
||||
}
|
||||
|
||||
function getLegendValues(transactionOrSpan: Transaction | Span) {
|
||||
return {
|
||||
[WaterfallLegendType.ServiceName]: transactionOrSpan.service.name,
|
||||
[WaterfallLegendType.SpanType]:
|
||||
'span' in transactionOrSpan
|
||||
? transactionOrSpan.span.subtype || transactionOrSpan.span.type
|
||||
: '',
|
||||
};
|
||||
}
|
||||
|
||||
function getTransactionItem(transaction: Transaction): IWaterfallTransaction {
|
||||
return {
|
||||
|
@ -87,6 +106,8 @@ function getTransactionItem(transaction: Transaction): IWaterfallTransaction {
|
|||
duration: transaction.transaction.duration.us,
|
||||
offset: 0,
|
||||
skew: 0,
|
||||
legendValues: getLegendValues(transaction),
|
||||
color: '',
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -99,6 +120,8 @@ function getSpanItem(span: Span): IWaterfallSpan {
|
|||
duration: span.span.duration.us,
|
||||
offset: 0,
|
||||
skew: 0,
|
||||
legendValues: getLegendValues(span),
|
||||
color: '',
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -110,7 +133,8 @@ function getErrorItem(
|
|||
const entryTimestamp = entryWaterfallTransaction?.doc.timestamp.us ?? 0;
|
||||
const parent = items.find(
|
||||
(waterfallItem) => waterfallItem.id === error.parent?.id
|
||||
);
|
||||
) as IWaterfallSpanOrTransaction | undefined;
|
||||
|
||||
const errorItem: IWaterfallError = {
|
||||
docType: 'error',
|
||||
doc: error,
|
||||
|
@ -119,7 +143,7 @@ function getErrorItem(
|
|||
parentId: parent?.id,
|
||||
offset: error.timestamp.us - entryTimestamp,
|
||||
skew: 0,
|
||||
duration: 0,
|
||||
color: '',
|
||||
};
|
||||
|
||||
return {
|
||||
|
@ -130,7 +154,7 @@ function getErrorItem(
|
|||
|
||||
export function getClockSkew(
|
||||
item: IWaterfallItem | IWaterfallError,
|
||||
parentItem?: IWaterfallItem
|
||||
parentItem?: IWaterfallSpanOrTransaction
|
||||
) {
|
||||
if (!parentItem) {
|
||||
return 0;
|
||||
|
@ -168,9 +192,9 @@ export function getOrderedWaterfallItems(
|
|||
const visitedWaterfallItemSet = new Set();
|
||||
|
||||
function getSortedChildren(
|
||||
item: IWaterfallItem,
|
||||
parentItem?: IWaterfallItem
|
||||
): IWaterfallItem[] {
|
||||
item: IWaterfallSpanOrTransaction,
|
||||
parentItem?: IWaterfallSpanOrTransaction
|
||||
): IWaterfallSpanOrTransaction[] {
|
||||
if (visitedWaterfallItemSet.has(item)) {
|
||||
return [];
|
||||
}
|
||||
|
@ -203,27 +227,39 @@ function getRootTransaction(childrenByParentId: IWaterfallGroup) {
|
|||
}
|
||||
}
|
||||
|
||||
export type IServiceColors = Record<string, string>;
|
||||
function getLegends(waterfallItems: IWaterfallItem[]) {
|
||||
const onlyBaseSpanItems = waterfallItems.filter(
|
||||
(item) => item.docType === 'span' || item.docType === 'transaction'
|
||||
) as IWaterfallSpanOrTransaction[];
|
||||
|
||||
function getServiceColors(waterfallItems: IWaterfallItem[]) {
|
||||
const services = uniq(waterfallItems.map((item) => item.doc.service.name));
|
||||
const legends = [
|
||||
WaterfallLegendType.ServiceName,
|
||||
WaterfallLegendType.SpanType,
|
||||
].flatMap((legendType) => {
|
||||
const allLegendValues = uniq(
|
||||
onlyBaseSpanItems.map((item) => item.legendValues[legendType])
|
||||
);
|
||||
|
||||
const assignedColors = [
|
||||
theme.euiColorVis1,
|
||||
theme.euiColorVis0,
|
||||
theme.euiColorVis3,
|
||||
theme.euiColorVis2,
|
||||
theme.euiColorVis6,
|
||||
theme.euiColorVis7,
|
||||
theme.euiColorVis5,
|
||||
];
|
||||
const palette = euiPaletteColorBlind({
|
||||
rotations: Math.ceil(allLegendValues.length / 10),
|
||||
});
|
||||
|
||||
return zipObject(services, assignedColors) as IServiceColors;
|
||||
return allLegendValues.map((value, index) => ({
|
||||
type: legendType,
|
||||
value,
|
||||
color: palette[index],
|
||||
}));
|
||||
});
|
||||
|
||||
return legends;
|
||||
}
|
||||
|
||||
const getWaterfallDuration = (waterfallItems: IWaterfallItem[]) =>
|
||||
Math.max(
|
||||
...waterfallItems.map((item) => item.offset + item.skew + item.duration),
|
||||
...waterfallItems.map(
|
||||
(item) =>
|
||||
item.offset + item.skew + ('duration' in item ? item.duration : 0)
|
||||
),
|
||||
0
|
||||
);
|
||||
|
||||
|
@ -238,7 +274,7 @@ const getWaterfallItems = (items: TraceAPIResponse['trace']['items']) =>
|
|||
}
|
||||
});
|
||||
|
||||
function reparentSpans(waterfallItems: IWaterfallItem[]) {
|
||||
function reparentSpans(waterfallItems: IWaterfallSpanOrTransaction[]) {
|
||||
// find children that needs to be re-parented and map them to their correct parent id
|
||||
const childIdToParentIdMapping = Object.fromEntries(
|
||||
flatten(
|
||||
|
@ -266,7 +302,9 @@ function reparentSpans(waterfallItems: IWaterfallItem[]) {
|
|||
});
|
||||
}
|
||||
|
||||
const getChildrenGroupedByParentId = (waterfallItems: IWaterfallItem[]) =>
|
||||
const getChildrenGroupedByParentId = (
|
||||
waterfallItems: IWaterfallSpanOrTransaction[]
|
||||
) =>
|
||||
groupBy(waterfallItems, (item) => (item.parentId ? item.parentId : ROOT_ID));
|
||||
|
||||
const getEntryWaterfallTransaction = (
|
||||
|
@ -329,13 +367,15 @@ export function getWaterfall(
|
|||
items: [],
|
||||
errorsPerTransaction,
|
||||
errorsCount: sum(Object.values(errorsPerTransaction)),
|
||||
serviceColors: {},
|
||||
legends: [],
|
||||
errorItems: [],
|
||||
childrenByParentId: {},
|
||||
};
|
||||
}
|
||||
|
||||
const waterfallItems: IWaterfallItem[] = getWaterfallItems(trace.items);
|
||||
const waterfallItems: IWaterfallSpanOrTransaction[] = getWaterfallItems(
|
||||
trace.items
|
||||
);
|
||||
|
||||
const childrenByParentId = getChildrenGroupedByParentId(
|
||||
reparentSpans(waterfallItems)
|
||||
|
@ -358,7 +398,7 @@ export function getWaterfall(
|
|||
|
||||
const rootTransaction = getRootTransaction(childrenByParentId);
|
||||
const duration = getWaterfallDuration(items);
|
||||
const serviceColors = getServiceColors(items);
|
||||
const legends = getLegends(items);
|
||||
|
||||
return {
|
||||
entryWaterfallTransaction,
|
||||
|
@ -367,7 +407,7 @@ export function getWaterfall(
|
|||
items,
|
||||
errorsPerTransaction,
|
||||
errorsCount: errorItems.length,
|
||||
serviceColors,
|
||||
legends,
|
||||
errorItems,
|
||||
childrenByParentId: getChildrenGroupedByParentId(items),
|
||||
};
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiFlexGroup } from '@elastic/eui';
|
||||
import { EuiFlexItem } from '@elastic/eui';
|
||||
import { EuiTitle } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { Legend } from '../../../../shared/charts/Legend';
|
||||
import {
|
||||
IWaterfallLegend,
|
||||
WaterfallLegendType,
|
||||
} from './Waterfall/waterfall_helpers/waterfall_helpers';
|
||||
|
||||
interface Props {
|
||||
legends: IWaterfallLegend[];
|
||||
type: WaterfallLegendType;
|
||||
}
|
||||
|
||||
const LEGEND_LABELS = {
|
||||
[WaterfallLegendType.ServiceName]: i18n.translate(
|
||||
'xpack.apm.transactionDetails.servicesTitle',
|
||||
{
|
||||
defaultMessage: 'Services',
|
||||
}
|
||||
),
|
||||
[WaterfallLegendType.SpanType]: i18n.translate(
|
||||
'xpack.apm.transactionDetails.spanTypeLegendTitle',
|
||||
{
|
||||
defaultMessage: 'Type',
|
||||
}
|
||||
),
|
||||
};
|
||||
export function WaterfallLegends({ legends, type }: Props) {
|
||||
return (
|
||||
<EuiFlexGroup alignItems="center" gutterSize="m">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle size="xxxs">
|
||||
<span>{LEGEND_LABELS[type]}</span>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup direction="row" gutterSize="s">
|
||||
{legends.map((legend) => (
|
||||
<EuiFlexItem grow={false} key={legend.value}>
|
||||
<Legend color={legend.color} text={legend.value} />
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
|
@ -7,10 +7,15 @@
|
|||
|
||||
import { Location } from 'history';
|
||||
import React from 'react';
|
||||
import { keyBy } from 'lodash';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { IUrlParams } from '../../../../../context/url_params_context/types';
|
||||
import { ServiceLegends } from './ServiceLegends';
|
||||
import { IWaterfall } from './Waterfall/waterfall_helpers/waterfall_helpers';
|
||||
import {
|
||||
IWaterfall,
|
||||
WaterfallLegendType,
|
||||
} from './Waterfall/waterfall_helpers/waterfall_helpers';
|
||||
import { Waterfall } from './Waterfall';
|
||||
import { WaterfallLegends } from './WaterfallLegends';
|
||||
|
||||
interface Props {
|
||||
urlParams: IUrlParams;
|
||||
|
@ -25,13 +30,59 @@ export function WaterfallContainer({
|
|||
waterfall,
|
||||
exceedsMax,
|
||||
}: Props) {
|
||||
const { serviceName } = useParams<{ serviceName: string }>();
|
||||
|
||||
if (!waterfall) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { legends, items } = waterfall;
|
||||
|
||||
// Service colors are needed to color the dot in the error popover
|
||||
const serviceLegends = legends.filter(
|
||||
({ type }) => type === WaterfallLegendType.ServiceName
|
||||
);
|
||||
const serviceColors = serviceLegends.reduce((colorMap, legend) => {
|
||||
return {
|
||||
...colorMap,
|
||||
[legend.value!]: legend.color,
|
||||
};
|
||||
}, {} as Record<string, string>);
|
||||
|
||||
// only color by span type if there are only events for one service
|
||||
const colorBy =
|
||||
serviceLegends.length > 1
|
||||
? WaterfallLegendType.ServiceName
|
||||
: WaterfallLegendType.SpanType;
|
||||
|
||||
const displayedLegends = legends.filter((legend) => legend.type === colorBy);
|
||||
|
||||
const legendsByValue = keyBy(displayedLegends, 'value');
|
||||
|
||||
// mutate items rather than rebuilding both items and childrenByParentId
|
||||
items.forEach((item) => {
|
||||
let color = '';
|
||||
if ('legendValues' in item) {
|
||||
color = legendsByValue[item.legendValues[colorBy]].color;
|
||||
}
|
||||
|
||||
if (!color) {
|
||||
// fall back to service color if there's no span.type, e.g. for transactions
|
||||
color = serviceColors[item.doc.service.name];
|
||||
}
|
||||
|
||||
item.color = color;
|
||||
});
|
||||
|
||||
// default to serviceName if value is empty, e.g. for transactions (which don't
|
||||
// have span.type or span.subtype)
|
||||
const legendsWithFallbackLabel = displayedLegends.map((legend) => {
|
||||
return { ...legend, value: !legend.value ? serviceName : legend.value };
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ServiceLegends serviceColors={waterfall.serviceColors} />
|
||||
<WaterfallLegends legends={legendsWithFallbackLabel} type={colorBy} />
|
||||
<Waterfall
|
||||
location={location}
|
||||
waterfallItemId={urlParams.waterfallItemId}
|
||||
|
|
Loading…
Reference in a new issue