mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[APM]Embeddable Trace Waterfall Enhancements (#217679)
For the embeddable waterfall to be successful, we want to remove unnecessary information and be able to select which records should be displayed. We need to remove: - Accordions - Services Legend We want to display (or hide anything that isn't): - root, - direct parent, - current span or transaction (highlighted) - up to 2 children. - Errors will be represented with an icon in the embeddable form of the waterfall and the badge in the regular form https://github.com/user-attachments/assets/bf8d34d7-173c-4a1a-8ccf-2f98f43fc625 ## Using the embeddable: 1: Loads standard trace waterfall (like the one on APM UI) ``` <ReactEmbeddableRenderer type="APM_TRACE_WATERFALL_EMBEDDABLE" getParentApi={() => ({ getSerializedStateForChild: () => ({ rawState: { serviceName: 'foo', traceId: 'e7b9d541fae0e25106291f7ac0947acd', entryTransactionId: '2d94d9d4fda31c18', rangeFrom: '2025-03-26T00:00:00.513Z', rangeTo: '2025-03-26T20:52:42.513Z', displayLimit: 5, //optional param when omitted it renders the entire waterfall }, }), })} hidePanelChrome={true} /> ``` 2: Loads focused trace waterfall (some trace events are hidden and a summary is available) ``` <ReactEmbeddableRenderer type="APM_TRACE_WATERFALL_EMBEDDABLE" getParentApi={() => ({ getSerializedStateForChild: () => ({ rawState: { traceId: 'e7b9d541fae0e25106291f7ac0947acd', rangeFrom: '2025-03-26T00:00:00.513Z', rangeTo: '2025-03-26T20:52:42.513Z', docId: SPAN_OR_TRANSACTION_ID }, }), })} hidePanelChrome={true} /> ```
This commit is contained in:
parent
e5851e44e6
commit
822aef361c
21 changed files with 1342 additions and 82 deletions
|
@ -151,8 +151,13 @@ const VirtualRow = React.memo(
|
|||
const WaterfallNode = React.memo((props: WaterfallNodeProps) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const { duration, waterfallItemId, onClickWaterfallItem, timelineMargins, node } = props;
|
||||
const { criticalPathSegmentsById, getErrorCount, updateTreeNode, showCriticalPath } =
|
||||
useWaterfallContext();
|
||||
const {
|
||||
criticalPathSegmentsById,
|
||||
getErrorCount,
|
||||
updateTreeNode,
|
||||
showCriticalPath,
|
||||
isEmbeddable,
|
||||
} = useWaterfallContext();
|
||||
|
||||
const displayedColor = showCriticalPath ? transparentize(0.5, node.item.color) : node.item.color;
|
||||
const marginLeftLevel = 8 * node.level;
|
||||
|
@ -210,6 +215,7 @@ const WaterfallNode = React.memo((props: WaterfallNodeProps) => {
|
|||
marginLeftLevel={marginLeftLevel}
|
||||
onClick={onWaterfallItemClick}
|
||||
segments={segments}
|
||||
isEmbeddable={isEmbeddable}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
@ -274,7 +280,7 @@ function ToggleAccordionButton({
|
|||
<EuiIcon type={isOpen ? 'arrowDown' : 'arrowRight'} />
|
||||
</div>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false} style={{ position: 'relative' }}>
|
||||
<EuiFlexItem grow={false} css={{ position: 'relative' }}>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
|
|
|
@ -26,7 +26,7 @@ import {
|
|||
convertTreeToList,
|
||||
updateTraceTreeNode,
|
||||
reparentOrphanItems,
|
||||
getLegendsAndColorBy,
|
||||
generateLegendsAndAssignColorsToWaterfall,
|
||||
WaterfallLegendType,
|
||||
} from './waterfall_helpers';
|
||||
import type { APMError } from '../../../../../../../../typings/es_schemas/ui/apm_error';
|
||||
|
@ -1147,7 +1147,7 @@ describe('waterfall_helpers', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('getLegendsAndColorBy', () => {
|
||||
describe('generateLegendsAndAssignColorsToWaterfall', () => {
|
||||
const createWaterfallItem = (overrides: Partial<IWaterfallItem>): IWaterfallItem =>
|
||||
({
|
||||
docType: 'span',
|
||||
|
@ -1160,7 +1160,7 @@ describe('waterfall_helpers', () => {
|
|||
...overrides,
|
||||
} as IWaterfallItem);
|
||||
|
||||
describe('getLegendsAndColorBy', () => {
|
||||
describe('generateLegendsAndAssignColorsToWaterfall', () => {
|
||||
it('should generate legends for multiple services', () => {
|
||||
const waterfallItems: IWaterfallItem[] = [
|
||||
createWaterfallItem({
|
||||
|
@ -1177,7 +1177,7 @@ describe('waterfall_helpers', () => {
|
|||
} as Partial<IWaterfallSpan>),
|
||||
];
|
||||
|
||||
const { legends, colorBy } = getLegendsAndColorBy(waterfallItems);
|
||||
const { legends, colorBy } = generateLegendsAndAssignColorsToWaterfall(waterfallItems);
|
||||
expect(legends.length).toBeGreaterThanOrEqual(2);
|
||||
expect(colorBy).toBe(WaterfallLegendType.ServiceName);
|
||||
});
|
||||
|
@ -1192,7 +1192,7 @@ describe('waterfall_helpers', () => {
|
|||
} as Partial<IWaterfallSpan>),
|
||||
];
|
||||
|
||||
const { legends, colorBy } = getLegendsAndColorBy(waterfallItems);
|
||||
const { legends, colorBy } = generateLegendsAndAssignColorsToWaterfall(waterfallItems);
|
||||
expect(legends.length).toBeGreaterThanOrEqual(2);
|
||||
expect(colorBy).toBe(WaterfallLegendType.SpanType);
|
||||
});
|
||||
|
@ -1207,7 +1207,7 @@ describe('waterfall_helpers', () => {
|
|||
} as Partial<IWaterfallSpan>),
|
||||
];
|
||||
|
||||
getLegendsAndColorBy(waterfallItems);
|
||||
generateLegendsAndAssignColorsToWaterfall(waterfallItems);
|
||||
|
||||
expect(waterfallItems[0].color).toBeDefined();
|
||||
expect(waterfallItems[1].color).toBeDefined();
|
||||
|
@ -1222,13 +1222,13 @@ describe('waterfall_helpers', () => {
|
|||
} as Partial<IWaterfallSpan>),
|
||||
];
|
||||
|
||||
getLegendsAndColorBy(waterfallItems);
|
||||
generateLegendsAndAssignColorsToWaterfall(waterfallItems);
|
||||
|
||||
expect(waterfallItems[0].color).toBeDefined();
|
||||
});
|
||||
|
||||
it('should handle empty input without errors', () => {
|
||||
expect(() => getLegendsAndColorBy([])).not.toThrow();
|
||||
expect(() => generateLegendsAndAssignColorsToWaterfall([])).not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -127,7 +127,7 @@ function getLegendValues(transactionOrSpan: WaterfallTransaction | WaterfallSpan
|
|||
};
|
||||
}
|
||||
|
||||
function getTransactionItem(
|
||||
export function getTransactionItem(
|
||||
transaction: WaterfallTransaction,
|
||||
linkedChildrenCount: number = 0
|
||||
): IWaterfallTransaction {
|
||||
|
@ -148,7 +148,7 @@ function getTransactionItem(
|
|||
};
|
||||
}
|
||||
|
||||
function getSpanItem(span: WaterfallSpan, linkedChildrenCount: number = 0): IWaterfallSpan {
|
||||
export function getSpanItem(span: WaterfallSpan, linkedChildrenCount: number = 0): IWaterfallSpan {
|
||||
return {
|
||||
docType: 'span',
|
||||
doc: span,
|
||||
|
@ -265,7 +265,7 @@ function getRootWaterfallTransaction(
|
|||
}
|
||||
}
|
||||
|
||||
export function getLegendsAndColorBy(waterfallItems: IWaterfallItem[]) {
|
||||
export function generateLegendsAndAssignColorsToWaterfall(waterfallItems: IWaterfallItem[]) {
|
||||
const onlyBaseSpanItems = waterfallItems.filter(
|
||||
(item) => item.docType === 'span' || item.docType === 'transaction'
|
||||
) as IWaterfallSpanOrTransaction[];
|
||||
|
@ -544,7 +544,7 @@ export function getWaterfall(apiResponse: TraceAPIResponse): IWaterfall {
|
|||
const rootWaterfallTransaction = getRootWaterfallTransaction(childrenByParentId);
|
||||
|
||||
const duration = getWaterfallDuration(items);
|
||||
const { legends, colorBy } = getLegendsAndColorBy(items);
|
||||
const { legends, colorBy } = generateLegendsAndAssignColorsToWaterfall(items);
|
||||
|
||||
return {
|
||||
entryWaterfallTransaction,
|
||||
|
|
|
@ -6,25 +6,24 @@
|
|||
*/
|
||||
|
||||
import { EuiBadge, EuiIcon, EuiText, EuiTitle, EuiToolTip, useEuiTheme } from '@elastic/eui';
|
||||
import styled from '@emotion/styled';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { ReactNode } from 'react';
|
||||
import React, { useRef, useEffect, useState } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { isMobileAgentName, isRumAgentName } from '../../../../../../../common/agent_name';
|
||||
import { SPAN_ID, TRACE_ID, TRANSACTION_ID } from '../../../../../../../common/es_fields/apm';
|
||||
import { asDuration } from '../../../../../../../common/utils/formatters';
|
||||
import { useAnyOfApmParams } from '../../../../../../hooks/use_apm_params';
|
||||
import { useApmRouter } from '../../../../../../hooks/use_apm_router';
|
||||
import type { Margins } from '../../../../../shared/charts/timeline';
|
||||
import { TruncateWithTooltip } from '../../../../../shared/truncate_with_tooltip';
|
||||
import { SyncBadge } from './badge/sync_badge';
|
||||
import { SpanLinksBadge } from './badge/span_links_badge';
|
||||
import { ColdStartBadge } from './badge/cold_start_badge';
|
||||
import type { IWaterfallSpanOrTransaction } from './waterfall_helpers/waterfall_helpers';
|
||||
import { SpanLinksBadge } from './badge/span_links_badge';
|
||||
import { SyncBadge } from './badge/sync_badge';
|
||||
import { FailureBadge } from './failure_badge';
|
||||
import { useApmRouter } from '../../../../../../hooks/use_apm_router';
|
||||
import { useAnyOfApmParams } from '../../../../../../hooks/use_apm_params';
|
||||
import { OrphanItemTooltipIcon } from './orphan_item_tooltip_icon';
|
||||
import { SpanMissingDestinationTooltip } from './span_missing_destination_tooltip';
|
||||
import { useWaterfallContext } from './context/use_waterfall';
|
||||
import type { IWaterfallSpanOrTransaction } from './waterfall_helpers/waterfall_helpers';
|
||||
|
||||
type ItemType = 'transaction' | 'span' | 'error';
|
||||
|
||||
|
@ -33,6 +32,7 @@ interface IContainerStyleProps {
|
|||
timelineMargins: Margins;
|
||||
isSelected: boolean;
|
||||
hasToggle: boolean;
|
||||
hasOnClick: boolean;
|
||||
}
|
||||
|
||||
interface IBarStyleProps {
|
||||
|
@ -44,6 +44,7 @@ const Container = styled.div<IContainerStyleProps>`
|
|||
position: relative;
|
||||
display: block;
|
||||
user-select: none;
|
||||
min-height: 44px;
|
||||
padding-top: ${({ theme }) => theme.euiTheme.size.s};
|
||||
padding-bottom: ${({ theme }) => theme.euiTheme.size.m};
|
||||
margin-right: ${(props) => props.timelineMargins.right}px;
|
||||
|
@ -53,7 +54,7 @@ const Container = styled.div<IContainerStyleProps>`
|
|||
: props.timelineMargins.left}px;
|
||||
background-color: ${({ isSelected, theme }) =>
|
||||
isSelected ? theme.euiTheme.colors.lightestShade : 'initial'};
|
||||
cursor: pointer;
|
||||
cursor: ${(props) => (props.hasOnClick ? 'pointer' : 'default')}};
|
||||
|
||||
&:hover {
|
||||
background-color: ${({ theme }) => theme.euiTheme.colors.lightestShade};
|
||||
|
@ -124,6 +125,7 @@ interface IWaterfallItemProps {
|
|||
color: string;
|
||||
}>;
|
||||
onClick?: (flyoutDetailTab: string) => unknown;
|
||||
isEmbeddable?: boolean;
|
||||
}
|
||||
|
||||
function PrefixIcon({ item }: { item: IWaterfallSpanOrTransaction }) {
|
||||
|
@ -202,7 +204,7 @@ function NameLabel({ item }: { item: IWaterfallSpanOrTransaction }) {
|
|||
name = `${item.doc.span.composite.count}${compositePrefix} ${name}`;
|
||||
}
|
||||
return (
|
||||
<EuiText style={{ overflow: 'hidden' }} size="s">
|
||||
<EuiText css={{ overflow: 'hidden' }} size="s">
|
||||
<TruncateWithTooltip content={name} text={name} />
|
||||
</EuiText>
|
||||
);
|
||||
|
@ -226,9 +228,9 @@ export function WaterfallItem({
|
|||
marginLeftLevel,
|
||||
onClick,
|
||||
segments,
|
||||
isEmbeddable = false,
|
||||
}: IWaterfallItemProps) {
|
||||
const [widthFactor, setWidthFactor] = useState(1);
|
||||
const { isEmbeddable } = useWaterfallContext();
|
||||
const waterfallItemRef: React.RefObject<any> = useRef(null);
|
||||
useEffect(() => {
|
||||
if (waterfallItemRef?.current && marginLeftLevel) {
|
||||
|
@ -264,6 +266,7 @@ export function WaterfallItem({
|
|||
onClick(waterfallItemFlyoutTab);
|
||||
}
|
||||
}}
|
||||
hasOnClick={onClick !== undefined}
|
||||
>
|
||||
<ItemBar // using inline styles instead of props to avoid generating a css class for each item
|
||||
style={itemBarStyle}
|
||||
|
@ -296,7 +299,7 @@ export function WaterfallItem({
|
|||
|
||||
<Duration item={item} />
|
||||
{isEmbeddable ? (
|
||||
<FailureBadge outcome={item.doc.event?.outcome} />
|
||||
<EmbeddableErrorIcon errorCount={errorCount} />
|
||||
) : (
|
||||
<RelatedErrors item={item} errorCount={errorCount} />
|
||||
)}
|
||||
|
@ -316,6 +319,14 @@ export function WaterfallItem({
|
|||
);
|
||||
}
|
||||
|
||||
function EmbeddableErrorIcon({ errorCount }: { errorCount: number }) {
|
||||
const theme = useEuiTheme();
|
||||
if (errorCount <= 0) {
|
||||
return null;
|
||||
}
|
||||
return <EuiIcon type="errorFilled" color={theme.euiTheme.colors.danger} size="s" />;
|
||||
}
|
||||
|
||||
function RelatedErrors({
|
||||
item,
|
||||
errorCount,
|
||||
|
|
|
@ -27,9 +27,16 @@ export interface TimelineProps {
|
|||
xMin?: number;
|
||||
xMax?: number;
|
||||
margins: Margins;
|
||||
numberOfTicks?: number;
|
||||
}
|
||||
|
||||
export function TimelineAxisContainer({ xMax, xMin, margins, marks }: TimelineProps) {
|
||||
export function TimelineAxisContainer({
|
||||
xMax,
|
||||
xMin,
|
||||
margins,
|
||||
marks,
|
||||
numberOfTicks,
|
||||
}: TimelineProps) {
|
||||
const [width, setWidth] = useState(0);
|
||||
if (xMax === undefined) {
|
||||
return null;
|
||||
|
@ -38,7 +45,7 @@ export function TimelineAxisContainer({ xMax, xMin, margins, marks }: TimelinePr
|
|||
return (
|
||||
<EuiResizeObserver onResize={(size) => setWidth(size.width)}>
|
||||
{(resizeRef) => {
|
||||
const plotValues = getPlotValues({ width, xMin, xMax, margins });
|
||||
const plotValues = getPlotValues({ width, xMin, xMax, margins, numberOfTicks });
|
||||
const topTraceDuration = xMax - (xMin ?? 0);
|
||||
return (
|
||||
<div style={{ width: '100%', height: '100%' }} ref={resizeRef}>
|
||||
|
|
|
@ -15,11 +15,13 @@ export function getPlotValues({
|
|||
xMin = 0,
|
||||
xMax,
|
||||
margins,
|
||||
numberOfTicks = 7,
|
||||
}: {
|
||||
width: number;
|
||||
xMin?: number;
|
||||
xMax: number;
|
||||
margins: Margins;
|
||||
numberOfTicks?: number;
|
||||
}) {
|
||||
const xScale = scaleLinear()
|
||||
.domain([xMin, xMax])
|
||||
|
@ -27,7 +29,7 @@ export function getPlotValues({
|
|||
|
||||
return {
|
||||
margins,
|
||||
tickValues: xScale.ticks(7),
|
||||
tickValues: xScale.ticks(numberOfTicks),
|
||||
width,
|
||||
xDomain: xScale.domain(),
|
||||
xMax,
|
||||
|
|
|
@ -0,0 +1,244 @@
|
|||
/*
|
||||
* 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 type { Meta, StoryFn } from '@storybook/react';
|
||||
import React from 'react';
|
||||
import { FocusedTraceWaterfall } from '.';
|
||||
import type { APIReturnType } from '../../../services/rest/create_call_apm_api';
|
||||
import { MockApmPluginStorybook } from '../../../context/apm_plugin/mock_apm_plugin_storybook';
|
||||
|
||||
type TraceItems = APIReturnType<'GET /internal/apm/traces/{traceId}/{docId}'>;
|
||||
|
||||
const stories: Meta<any> = {
|
||||
title: 'app/TransactionDetails/focusedTraceWaterfall',
|
||||
component: FocusedTraceWaterfall,
|
||||
decorators: [
|
||||
(StoryComponent) => (
|
||||
<MockApmPluginStorybook routePath="/services/{serviceName}/transactions/view?rangeFrom=now-15m&rangeTo=now&transactionName=testTransactionName">
|
||||
<StoryComponent />
|
||||
</MockApmPluginStorybook>
|
||||
),
|
||||
],
|
||||
};
|
||||
export default stories;
|
||||
|
||||
export const Example: StoryFn<any> = () => {
|
||||
return <FocusedTraceWaterfall items={data} />;
|
||||
};
|
||||
|
||||
const data: TraceItems = {
|
||||
traceItems: {
|
||||
rootTransaction: {
|
||||
service: {
|
||||
environment: 'dev',
|
||||
name: 'products-server-classic-apm',
|
||||
},
|
||||
transaction: {
|
||||
duration: {
|
||||
us: 7087,
|
||||
},
|
||||
result: 'HTTP 2xx',
|
||||
type: 'request',
|
||||
id: '3383bc5055a6ae91',
|
||||
name: 'POST /products/:id/buy',
|
||||
},
|
||||
span: {
|
||||
id: '3383bc5055a6ae91',
|
||||
links: [],
|
||||
},
|
||||
timestamp: {
|
||||
us: 1743790903871005,
|
||||
},
|
||||
trace: {
|
||||
id: '9733815865d7f9aedcacd0893fbaa1fb',
|
||||
},
|
||||
processor: {
|
||||
event: 'transaction',
|
||||
},
|
||||
agent: {
|
||||
name: 'nodejs',
|
||||
},
|
||||
event: {
|
||||
outcome: 'success',
|
||||
},
|
||||
},
|
||||
parentDoc: {
|
||||
service: {
|
||||
environment: 'dev',
|
||||
name: 'products-server-classic-apm',
|
||||
},
|
||||
span: {
|
||||
action: 'POST',
|
||||
name: 'POST localhost:8080',
|
||||
id: 'b1d5ad707d1a2d36',
|
||||
subtype: 'http',
|
||||
sync: false,
|
||||
type: 'external',
|
||||
duration: {
|
||||
us: 5891,
|
||||
},
|
||||
destination: {
|
||||
service: {
|
||||
resource: 'localhost:8080',
|
||||
},
|
||||
},
|
||||
links: [],
|
||||
},
|
||||
transaction: {
|
||||
id: '3383bc5055a6ae91',
|
||||
},
|
||||
timestamp: {
|
||||
us: 1743790903871622,
|
||||
},
|
||||
trace: {
|
||||
id: '9733815865d7f9aedcacd0893fbaa1fb',
|
||||
},
|
||||
processor: {
|
||||
event: 'span',
|
||||
},
|
||||
agent: {
|
||||
name: 'nodejs',
|
||||
},
|
||||
parent: {
|
||||
id: '3383bc5055a6ae91',
|
||||
},
|
||||
event: {
|
||||
outcome: 'success',
|
||||
},
|
||||
},
|
||||
focusedTraceDoc: {
|
||||
service: {
|
||||
environment: 'dev',
|
||||
name: 'payments-server-classic-apm',
|
||||
},
|
||||
transaction: {
|
||||
duration: {
|
||||
us: 4298,
|
||||
},
|
||||
result: 'HTTP 2xx',
|
||||
type: 'request',
|
||||
id: '86682e13644e14b9',
|
||||
name: 'PaymentsController#processPayment',
|
||||
},
|
||||
span: {
|
||||
id: '86682e13644e14b9',
|
||||
links: [],
|
||||
},
|
||||
timestamp: {
|
||||
us: 1743790903873047,
|
||||
},
|
||||
trace: {
|
||||
id: '9733815865d7f9aedcacd0893fbaa1fb',
|
||||
},
|
||||
processor: {
|
||||
event: 'transaction',
|
||||
},
|
||||
agent: {
|
||||
name: 'java',
|
||||
},
|
||||
parent: {
|
||||
id: 'b1d5ad707d1a2d36',
|
||||
},
|
||||
event: {
|
||||
outcome: 'success',
|
||||
},
|
||||
},
|
||||
focusedTraceTree: [
|
||||
{
|
||||
traceDoc: {
|
||||
service: {
|
||||
environment: 'dev',
|
||||
name: 'payments-server-classic-apm',
|
||||
},
|
||||
span: {
|
||||
name: 'POST localhost',
|
||||
id: 'ffe79ff737e435cf',
|
||||
subtype: 'http',
|
||||
type: 'external',
|
||||
duration: {
|
||||
us: 1870,
|
||||
},
|
||||
destination: {
|
||||
service: {
|
||||
resource: 'localhost:4001',
|
||||
},
|
||||
},
|
||||
links: [],
|
||||
},
|
||||
transaction: {
|
||||
id: '86682e13644e14b9',
|
||||
},
|
||||
timestamp: {
|
||||
us: 1743790903874690,
|
||||
},
|
||||
trace: {
|
||||
id: '9733815865d7f9aedcacd0893fbaa1fb',
|
||||
},
|
||||
processor: {
|
||||
event: 'span',
|
||||
},
|
||||
agent: {
|
||||
name: 'java',
|
||||
},
|
||||
parent: {
|
||||
id: '86682e13644e14b9',
|
||||
},
|
||||
event: {
|
||||
outcome: 'success',
|
||||
},
|
||||
},
|
||||
children: [
|
||||
{
|
||||
traceDoc: {
|
||||
service: {
|
||||
environment: 'dev',
|
||||
name: 'dispatch-server-classic-apm',
|
||||
},
|
||||
transaction: {
|
||||
duration: {
|
||||
us: 22,
|
||||
},
|
||||
result: 'HTTP 2xx',
|
||||
type: 'request',
|
||||
id: 'bb0397cdea74db7c',
|
||||
name: 'POST /dispatch/{id}',
|
||||
},
|
||||
span: {
|
||||
id: 'bb0397cdea74db7c',
|
||||
links: [],
|
||||
},
|
||||
timestamp: {
|
||||
us: 1743790903876709,
|
||||
},
|
||||
trace: {
|
||||
id: '9733815865d7f9aedcacd0893fbaa1fb',
|
||||
},
|
||||
processor: {
|
||||
event: 'transaction',
|
||||
},
|
||||
agent: {
|
||||
name: 'go',
|
||||
},
|
||||
parent: {
|
||||
id: 'ffe79ff737e435cf',
|
||||
},
|
||||
event: {
|
||||
outcome: 'success',
|
||||
},
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
summary: {
|
||||
services: 1,
|
||||
traceEvents: 1,
|
||||
errors: 0,
|
||||
},
|
||||
};
|
|
@ -0,0 +1,179 @@
|
|||
/*
|
||||
* 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 { EuiSpacer, useEuiTheme } from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import React, { useMemo } from 'react';
|
||||
import type { WaterfallSpan, WaterfallTransaction } from '../../../../common/waterfall/typings';
|
||||
import type { APIReturnType } from '../../../services/rest/create_call_apm_api';
|
||||
import type {
|
||||
IWaterfallSpan,
|
||||
IWaterfallTransaction,
|
||||
} from '../../app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/waterfall_helpers/waterfall_helpers';
|
||||
import {
|
||||
generateLegendsAndAssignColorsToWaterfall,
|
||||
getSpanItem,
|
||||
getTransactionItem,
|
||||
} from '../../app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/waterfall_helpers/waterfall_helpers';
|
||||
import { WaterfallItem } from '../../app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/waterfall_item';
|
||||
import { TimelineAxisContainer, VerticalLinesContainer } from '../charts/timeline';
|
||||
import { TraceSummary } from './trace_summary';
|
||||
|
||||
type FocusedTrace = APIReturnType<'GET /internal/apm/traces/{traceId}/{docId}'>;
|
||||
|
||||
interface Props {
|
||||
items: FocusedTrace;
|
||||
isEmbeddable?: boolean;
|
||||
}
|
||||
|
||||
const margin = {
|
||||
top: 40,
|
||||
left: 20,
|
||||
right: 50,
|
||||
bottom: 0,
|
||||
};
|
||||
|
||||
function convertChildrenToWatefallItem(
|
||||
children: NonNullable<FocusedTrace['traceItems']>['focusedTraceTree'],
|
||||
rootWaterfallTransaction: IWaterfallTransaction
|
||||
) {
|
||||
function convert(
|
||||
child: NonNullable<FocusedTrace['traceItems']>['focusedTraceTree'][0]
|
||||
): Array<IWaterfallTransaction | IWaterfallSpan> {
|
||||
const waterfallItem =
|
||||
child.traceDoc.processor.event === 'transaction'
|
||||
? getTransactionItem(child.traceDoc as WaterfallTransaction, 0)
|
||||
: getSpanItem(child.traceDoc as WaterfallSpan, 0);
|
||||
|
||||
waterfallItem.offset = calculateOffset({ item: waterfallItem, rootWaterfallTransaction });
|
||||
|
||||
const convertedChildren = child.children?.length ? child.children.flatMap(convert) : [];
|
||||
|
||||
return [waterfallItem, ...convertedChildren];
|
||||
}
|
||||
|
||||
return children.flatMap(convert);
|
||||
}
|
||||
|
||||
const calculateOffset = ({
|
||||
item,
|
||||
rootWaterfallTransaction,
|
||||
}: {
|
||||
item: IWaterfallTransaction | IWaterfallSpan;
|
||||
rootWaterfallTransaction: IWaterfallTransaction;
|
||||
}) => item.doc.timestamp.us - rootWaterfallTransaction.doc.timestamp.us;
|
||||
|
||||
export function FocusedTraceWaterfall({ items, isEmbeddable = false }: Props) {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
const traceItems = items.traceItems;
|
||||
|
||||
const waterfall: {
|
||||
items: Array<IWaterfallTransaction | IWaterfallSpan>;
|
||||
totalDuration: number;
|
||||
focusedItemId?: string;
|
||||
} = useMemo(() => {
|
||||
const waterfallItems: Array<IWaterfallTransaction | IWaterfallSpan> = [];
|
||||
|
||||
if (!traceItems) {
|
||||
return {
|
||||
items: [],
|
||||
totalDuration: 0,
|
||||
focusedItemId: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
const rootWaterfallTransaction = getTransactionItem(
|
||||
traceItems.rootTransaction as WaterfallTransaction,
|
||||
0
|
||||
);
|
||||
|
||||
waterfallItems.push(rootWaterfallTransaction);
|
||||
|
||||
const parentItem = traceItems.parentDoc
|
||||
? traceItems.parentDoc.processor.event === 'transaction'
|
||||
? getTransactionItem(traceItems.parentDoc as WaterfallTransaction, 0)
|
||||
: getSpanItem(traceItems.parentDoc as WaterfallSpan, 0)
|
||||
: undefined;
|
||||
|
||||
if (parentItem && parentItem.id !== rootWaterfallTransaction.id) {
|
||||
parentItem.offset = calculateOffset({ item: parentItem, rootWaterfallTransaction });
|
||||
waterfallItems.push(parentItem);
|
||||
}
|
||||
|
||||
const focusedItem =
|
||||
traceItems.focusedTraceDoc.processor.event === 'transaction'
|
||||
? getTransactionItem(traceItems.focusedTraceDoc as WaterfallTransaction, 0)
|
||||
: getSpanItem(traceItems.focusedTraceDoc as WaterfallSpan, 0);
|
||||
|
||||
focusedItem.offset = calculateOffset({ item: focusedItem, rootWaterfallTransaction });
|
||||
|
||||
if (focusedItem.id !== rootWaterfallTransaction.id) {
|
||||
waterfallItems.push(focusedItem);
|
||||
}
|
||||
|
||||
const focusedItemChildren = convertChildrenToWatefallItem(
|
||||
traceItems.focusedTraceTree,
|
||||
rootWaterfallTransaction
|
||||
);
|
||||
|
||||
waterfallItems.push(...focusedItemChildren);
|
||||
generateLegendsAndAssignColorsToWaterfall(waterfallItems);
|
||||
|
||||
return {
|
||||
items: waterfallItems,
|
||||
totalDuration: rootWaterfallTransaction.duration,
|
||||
focusedItemId: focusedItem.id,
|
||||
};
|
||||
}, [traceItems]);
|
||||
|
||||
if (!waterfall.items.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
css={css`
|
||||
position: relative;
|
||||
`}
|
||||
>
|
||||
<div
|
||||
css={css`
|
||||
display: flex;
|
||||
position: sticky;
|
||||
top: var(--euiFixedHeadersOffset, 0);
|
||||
z-index: ${euiTheme.levels.menu};
|
||||
background-color: ${euiTheme.colors.emptyShade};
|
||||
border-bottom: ${euiTheme.border.thin};
|
||||
`}
|
||||
>
|
||||
<TimelineAxisContainer
|
||||
xMax={waterfall.totalDuration}
|
||||
margins={margin}
|
||||
numberOfTicks={3}
|
||||
/>
|
||||
</div>
|
||||
<VerticalLinesContainer xMax={waterfall.totalDuration} margins={margin} />
|
||||
{waterfall.items.map((item) => (
|
||||
<WaterfallItem
|
||||
timelineMargins={margin}
|
||||
color={item.color}
|
||||
hasToggle={false}
|
||||
errorCount={0}
|
||||
isSelected={item.id === waterfall.focusedItemId}
|
||||
item={item}
|
||||
marginLeftLevel={0}
|
||||
totalDuration={waterfall.totalDuration}
|
||||
isEmbeddable={isEmbeddable}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<EuiSpacer />
|
||||
<TraceSummary summary={items.summary} />
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* 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 type { PropsWithChildren } from 'react';
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { TraceSummary } from './trace_summary';
|
||||
import { MockApmPluginContextWrapper } from '../../../context/apm_plugin/mock_apm_plugin_context';
|
||||
|
||||
const wrapper = ({ children }: PropsWithChildren<unknown>) => (
|
||||
<MockApmPluginContextWrapper>{children}</MockApmPluginContextWrapper>
|
||||
);
|
||||
|
||||
describe('TraceSummary', () => {
|
||||
it.each([
|
||||
{
|
||||
summary: { services: 0, traceEvents: 0, errors: 0 },
|
||||
expectedText: ['0 services', '0 trace events', '0 errors'],
|
||||
},
|
||||
{
|
||||
summary: { services: 1, traceEvents: 1, errors: 1 },
|
||||
expectedText: ['1 service', '1 trace event', '1 error'],
|
||||
},
|
||||
{
|
||||
summary: { services: 5, traceEvents: 10, errors: 3 },
|
||||
expectedText: ['5 services', '10 trace events', '3 errors'],
|
||||
},
|
||||
])('renders correct pluralization for $summary', ({ summary, expectedText }) => {
|
||||
render(<TraceSummary summary={summary} />, { wrapper });
|
||||
for (const text of expectedText) {
|
||||
expect(screen.getByText(text)).toBeInTheDocument();
|
||||
}
|
||||
});
|
||||
});
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiText, useEuiTheme } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { css } from '@emotion/react';
|
||||
import type { APIReturnType } from '../../../services/rest/create_call_apm_api';
|
||||
|
||||
type TraceSummary = APIReturnType<'GET /internal/apm/traces/{traceId}/{docId}'>['summary'];
|
||||
|
||||
interface Props {
|
||||
summary: TraceSummary;
|
||||
}
|
||||
|
||||
function Divider() {
|
||||
const theme = useEuiTheme();
|
||||
return (
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
css={css`
|
||||
width: ${theme.euiTheme.border.width.thin};
|
||||
background-color: ${theme.euiTheme.border.color};
|
||||
`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function TraceSummary({ summary }: Props) {
|
||||
const theme = useEuiTheme();
|
||||
return (
|
||||
<EuiFlexGroup gutterSize="s">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText color="subdued" size="s">
|
||||
{i18n.translate('xpack.apm.traceSummary.servicesFlexItemLabel', {
|
||||
defaultMessage: '{services} {services, plural, one {service} other {services}}',
|
||||
values: { services: summary.services },
|
||||
})}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<Divider />
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText color="subdued" size="s">
|
||||
{i18n.translate('xpack.apm.traceSummary.traceEventsFlexItemLabel', {
|
||||
defaultMessage:
|
||||
'{traceEvents} {traceEvents, plural, one {trace event} other {trace events}}',
|
||||
values: { traceEvents: summary.traceEvents },
|
||||
})}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<Divider />
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup gutterSize="s" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIcon type="errorFilled" color={theme.euiTheme.colors.danger} size="s" />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText color="subdued" size="s">
|
||||
{i18n.translate('xpack.apm.traceSummary.errorsFlexItemLabel', {
|
||||
defaultMessage: '{errors} {errors, plural, one {error} other {errors}}',
|
||||
values: { errors: summary.errors },
|
||||
})}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* 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 { EuiText } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { FocusedTraceWaterfall } from '../../components/shared/focused_trace_waterfall';
|
||||
import { isPending, useFetcher } from '../../hooks/use_fetcher';
|
||||
import { Loading } from './loading';
|
||||
import type { ApmTraceWaterfallEmbeddableFocusedProps } from './react_embeddable_factory';
|
||||
export function FocusedTraceWaterfallEmbeddable({
|
||||
rangeFrom,
|
||||
rangeTo,
|
||||
traceId,
|
||||
docId,
|
||||
}: ApmTraceWaterfallEmbeddableFocusedProps) {
|
||||
const { data, status } = useFetcher(
|
||||
(callApmApi) => {
|
||||
return callApmApi('GET /internal/apm/traces/{traceId}/{docId}', {
|
||||
params: {
|
||||
path: { traceId, docId },
|
||||
query: { start: rangeFrom, end: rangeTo },
|
||||
},
|
||||
});
|
||||
},
|
||||
[docId, rangeFrom, rangeTo, traceId]
|
||||
);
|
||||
|
||||
if (isPending(status)) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
if (data === undefined) {
|
||||
return (
|
||||
<EuiText>
|
||||
{i18n.translate(
|
||||
'xpack.apm.focusedTraceWaterfallEmbeddable.traceWaterfallCouldNotTextLabel',
|
||||
{ defaultMessage: 'Trace waterfall could not be loaded' }
|
||||
)}
|
||||
</EuiText>
|
||||
);
|
||||
}
|
||||
|
||||
return <FocusedTraceWaterfall items={data} isEmbeddable />;
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* 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, EuiFlexItem, EuiLoadingSpinner, EuiText } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
|
||||
export function Loading() {
|
||||
return (
|
||||
<EuiFlexGroup direction="column" alignItems="center" gutterSize="s">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiLoadingSpinner size="l" />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText>
|
||||
{i18n.translate(
|
||||
'xpack.apm.traceWaterfallEmbeddable.loadingTraceWaterfallSkeletonTextLabel',
|
||||
{ defaultMessage: 'Loading trace waterfall' }
|
||||
)}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
|
@ -9,20 +9,33 @@ import type { SerializedTitles } from '@kbn/presentation-publishing';
|
|||
import { initializeTitleManager, useBatchedPublishingSubjects } from '@kbn/presentation-publishing';
|
||||
import React from 'react';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { ApmEmbeddableContext } from '../embeddable_context';
|
||||
import type { EmbeddableDeps } from '../types';
|
||||
import { APM_TRACE_WATERFALL_EMBEDDABLE } from './constant';
|
||||
import { TraceWaterfallEmbeddable } from './trace_waterfall_embeddable';
|
||||
import { FocusedTraceWaterfallEmbeddable } from './focused_trace_waterfall_embeddable';
|
||||
|
||||
export interface ApmTraceWaterfallEmbeddableProps extends SerializedTitles {
|
||||
serviceName: string;
|
||||
interface BaseProps {
|
||||
traceId: string;
|
||||
entryTransactionId: string;
|
||||
rangeFrom: string;
|
||||
rangeTo: string;
|
||||
}
|
||||
|
||||
export interface ApmTraceWaterfallEmbeddableFocusedProps extends BaseProps, SerializedTitles {
|
||||
docId: string;
|
||||
}
|
||||
|
||||
export interface ApmTraceWaterfallEmbeddableEntryProps extends BaseProps, SerializedTitles {
|
||||
serviceName: string;
|
||||
entryTransactionId: string;
|
||||
displayLimit?: number;
|
||||
}
|
||||
|
||||
export type ApmTraceWaterfallEmbeddableProps =
|
||||
| ApmTraceWaterfallEmbeddableFocusedProps
|
||||
| ApmTraceWaterfallEmbeddableEntryProps;
|
||||
|
||||
export const getApmTraceWaterfallEmbeddableFactory = (deps: EmbeddableDeps) => {
|
||||
const factory: ReactEmbeddableFactory<
|
||||
ApmTraceWaterfallEmbeddableProps,
|
||||
|
@ -35,12 +48,15 @@ export const getApmTraceWaterfallEmbeddableFactory = (deps: EmbeddableDeps) => {
|
|||
},
|
||||
buildEmbeddable: async (state, buildApi, uuid, parentApi) => {
|
||||
const titleManager = initializeTitleManager(state);
|
||||
const serviceName$ = new BehaviorSubject(state.serviceName);
|
||||
const serviceName$ = new BehaviorSubject('serviceName' in state ? state.serviceName : '');
|
||||
const traceId$ = new BehaviorSubject(state.traceId);
|
||||
const entryTransactionId$ = new BehaviorSubject(state.entryTransactionId);
|
||||
const entryTransactionId$ = new BehaviorSubject(
|
||||
'entryTransactionId' in state ? state.entryTransactionId : ''
|
||||
);
|
||||
const rangeFrom$ = new BehaviorSubject(state.rangeFrom);
|
||||
const rangeTo$ = new BehaviorSubject(state.rangeTo);
|
||||
const displayLimit$ = new BehaviorSubject(state.displayLimit);
|
||||
const displayLimit$ = new BehaviorSubject('displayLimit' in state ? state.displayLimit : 0);
|
||||
const docId$ = new BehaviorSubject('docId' in state ? state.docId : '');
|
||||
|
||||
const api = buildApi(
|
||||
{
|
||||
|
@ -55,6 +71,7 @@ export const getApmTraceWaterfallEmbeddableFactory = (deps: EmbeddableDeps) => {
|
|||
rangeFrom: rangeFrom$.getValue(),
|
||||
rangeTo: rangeTo$.getValue(),
|
||||
displayLimit: displayLimit$.getValue(),
|
||||
docId: docId$.getValue(),
|
||||
},
|
||||
};
|
||||
},
|
||||
|
@ -67,32 +84,51 @@ export const getApmTraceWaterfallEmbeddableFactory = (deps: EmbeddableDeps) => {
|
|||
rangeFrom: [rangeFrom$, (value) => rangeFrom$.next(value)],
|
||||
rangeTo: [rangeTo$, (value) => rangeFrom$.next(value)],
|
||||
displayLimit: [displayLimit$, (value) => displayLimit$.next(value)],
|
||||
docId: [docId$, (value) => docId$.next(value)],
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
api,
|
||||
Component: () => {
|
||||
const [serviceName, traceId, entryTransactionId, rangeFrom, rangeTo, displayLimit] =
|
||||
useBatchedPublishingSubjects(
|
||||
serviceName$,
|
||||
traceId$,
|
||||
entryTransactionId$,
|
||||
rangeFrom$,
|
||||
rangeTo$,
|
||||
displayLimit$
|
||||
);
|
||||
const [
|
||||
serviceName,
|
||||
traceId,
|
||||
entryTransactionId,
|
||||
rangeFrom,
|
||||
rangeTo,
|
||||
displayLimit,
|
||||
docId,
|
||||
] = useBatchedPublishingSubjects(
|
||||
serviceName$,
|
||||
traceId$,
|
||||
entryTransactionId$,
|
||||
rangeFrom$,
|
||||
rangeTo$,
|
||||
displayLimit$,
|
||||
docId$
|
||||
);
|
||||
const content = isEmpty(docId) ? (
|
||||
<TraceWaterfallEmbeddable
|
||||
serviceName={serviceName}
|
||||
traceId={traceId}
|
||||
entryTransactionId={entryTransactionId}
|
||||
rangeFrom={rangeFrom}
|
||||
rangeTo={rangeTo}
|
||||
displayLimit={displayLimit}
|
||||
/>
|
||||
) : (
|
||||
<FocusedTraceWaterfallEmbeddable
|
||||
traceId={traceId}
|
||||
rangeFrom={rangeFrom}
|
||||
rangeTo={rangeTo}
|
||||
docId={docId}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<ApmEmbeddableContext deps={deps} rangeFrom={rangeFrom} rangeTo={rangeTo}>
|
||||
<TraceWaterfallEmbeddable
|
||||
serviceName={serviceName}
|
||||
traceId={traceId}
|
||||
entryTransactionId={entryTransactionId}
|
||||
rangeFrom={rangeFrom}
|
||||
rangeTo={rangeTo}
|
||||
displayLimit={displayLimit}
|
||||
/>
|
||||
{content}
|
||||
</ApmEmbeddableContext>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -4,14 +4,14 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, EuiText } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { useWaterfallFetcher } from '../../components/app/transaction_details/use_waterfall_fetcher';
|
||||
import { Waterfall } from '../../components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall';
|
||||
import { isPending } from '../../hooks/use_fetcher';
|
||||
import type { ApmTraceWaterfallEmbeddableProps } from './react_embeddable_factory';
|
||||
import { WaterfallLegends } from '../../components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall_legends';
|
||||
import { isPending } from '../../hooks/use_fetcher';
|
||||
import { Loading } from './loading';
|
||||
import type { ApmTraceWaterfallEmbeddableEntryProps } from './react_embeddable_factory';
|
||||
|
||||
export function TraceWaterfallEmbeddable({
|
||||
serviceName,
|
||||
|
@ -20,7 +20,7 @@ export function TraceWaterfallEmbeddable({
|
|||
rangeTo,
|
||||
traceId,
|
||||
displayLimit,
|
||||
}: ApmTraceWaterfallEmbeddableProps) {
|
||||
}: ApmTraceWaterfallEmbeddableEntryProps) {
|
||||
const waterfallFetchResult = useWaterfallFetcher({
|
||||
traceId,
|
||||
transactionId: entryTransactionId,
|
||||
|
@ -29,21 +29,7 @@ export function TraceWaterfallEmbeddable({
|
|||
});
|
||||
|
||||
if (isPending(waterfallFetchResult.status)) {
|
||||
return (
|
||||
<EuiFlexGroup direction="column" alignItems="center" gutterSize="s">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiLoadingSpinner size="l" />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText>
|
||||
{i18n.translate(
|
||||
'xpack.apm.traceWaterfallEmbeddable.loadingTraceWaterfallSkeletonTextLabel',
|
||||
{ defaultMessage: 'Loading trace waterfall' }
|
||||
)}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
const { legends, colorBy } = waterfallFetchResult.waterfall;
|
||||
|
|
|
@ -0,0 +1,160 @@
|
|||
/*
|
||||
* 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 type { WaterfallSpan, WaterfallTransaction } from '../../../common/waterfall/typings';
|
||||
import { buildChildrenTree, buildFocusedTraceItems } from './build_focused_trace_items';
|
||||
import type { TraceItems } from './get_trace_items';
|
||||
|
||||
const mockTraceDoc = (id: string, parentId?: string) =>
|
||||
({
|
||||
span: { id },
|
||||
parent: parentId ? { id: parentId } : undefined,
|
||||
} as WaterfallTransaction | WaterfallSpan);
|
||||
|
||||
describe('buildChildrenTree', () => {
|
||||
it('returns an empty array when no children are found', () => {
|
||||
const initialTraceDoc = mockTraceDoc('1');
|
||||
const itemsGroupedByParentId = {};
|
||||
const result = buildChildrenTree({
|
||||
initialTraceDoc,
|
||||
itemsGroupedByParentId,
|
||||
maxNumberOfChildren: 2,
|
||||
});
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('builds a tree with children and nested children', () => {
|
||||
const initialTraceDoc = mockTraceDoc('1');
|
||||
const itemsGroupedByParentId = {
|
||||
'1': [mockTraceDoc('2'), mockTraceDoc('3')],
|
||||
'2': [mockTraceDoc('4')],
|
||||
};
|
||||
const result = buildChildrenTree({
|
||||
initialTraceDoc,
|
||||
itemsGroupedByParentId,
|
||||
maxNumberOfChildren: 5,
|
||||
});
|
||||
expect(result).toEqual([
|
||||
{
|
||||
traceDoc: mockTraceDoc('2'),
|
||||
children: [
|
||||
{
|
||||
traceDoc: mockTraceDoc('4'),
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
traceDoc: mockTraceDoc('3'),
|
||||
children: [],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('respects the maxNumberOfChildren limit with direct children', () => {
|
||||
const initialTraceDoc = mockTraceDoc('1');
|
||||
const itemsGroupedByParentId = {
|
||||
'1': [mockTraceDoc('2'), mockTraceDoc('3'), mockTraceDoc('4')],
|
||||
};
|
||||
const result = buildChildrenTree({
|
||||
initialTraceDoc,
|
||||
itemsGroupedByParentId,
|
||||
maxNumberOfChildren: 2,
|
||||
});
|
||||
expect(result).toEqual([
|
||||
{
|
||||
traceDoc: mockTraceDoc('2'),
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
traceDoc: mockTraceDoc('3'),
|
||||
children: [],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('respects the maxNumberOfChildren limit', () => {
|
||||
const initialTraceDoc = mockTraceDoc('1');
|
||||
const itemsGroupedByParentId = {
|
||||
'1': [mockTraceDoc('2'), mockTraceDoc('3')],
|
||||
'2': [mockTraceDoc('4')],
|
||||
};
|
||||
const result = buildChildrenTree({
|
||||
initialTraceDoc,
|
||||
itemsGroupedByParentId,
|
||||
maxNumberOfChildren: 2,
|
||||
});
|
||||
expect(result).toEqual([
|
||||
{
|
||||
traceDoc: mockTraceDoc('2'),
|
||||
children: [
|
||||
{
|
||||
traceDoc: mockTraceDoc('4'),
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildFocusedTraceItems', () => {
|
||||
it('returns undefined if the focused trace document is not found', () => {
|
||||
const traceItems = { traceDocs: [mockTraceDoc('1')] } as unknown as TraceItems;
|
||||
const result = buildFocusedTraceItems({ traceItems, docId: 'non-existent-id' });
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns the correct focused trace document and its children', () => {
|
||||
const traceItems = {
|
||||
traceDocs: [
|
||||
mockTraceDoc('1'),
|
||||
mockTraceDoc('2', '1'),
|
||||
mockTraceDoc('3', '1'),
|
||||
mockTraceDoc('4', '2'),
|
||||
],
|
||||
} as unknown as TraceItems;
|
||||
const result = buildFocusedTraceItems({ traceItems, docId: '1' });
|
||||
expect(result).toEqual({
|
||||
rootTransaction: mockTraceDoc('1'),
|
||||
parentDoc: undefined,
|
||||
focusedTraceDoc: mockTraceDoc('1'),
|
||||
focusedTraceTree: [
|
||||
{
|
||||
traceDoc: mockTraceDoc('2', '1'),
|
||||
children: [
|
||||
{
|
||||
traceDoc: mockTraceDoc('4', '2'),
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('returns the correct parent document if it exists', () => {
|
||||
const traceItems = {
|
||||
traceDocs: [mockTraceDoc('1'), mockTraceDoc('2', '1'), mockTraceDoc('3', '2')],
|
||||
} as unknown as TraceItems;
|
||||
const result = buildFocusedTraceItems({ traceItems, docId: '3' });
|
||||
expect(result).toEqual({
|
||||
rootTransaction: mockTraceDoc('1'),
|
||||
parentDoc: mockTraceDoc('2', '1'),
|
||||
focusedTraceDoc: mockTraceDoc('3', '2'),
|
||||
focusedTraceTree: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('handles root transactions correctly', () => {
|
||||
const traceItems = {
|
||||
traceDocs: [mockTraceDoc('1'), mockTraceDoc('2', '1'), mockTraceDoc('3', '2')],
|
||||
} as unknown as TraceItems;
|
||||
const result = buildFocusedTraceItems({ traceItems, docId: '1' });
|
||||
expect(result?.rootTransaction).toEqual(mockTraceDoc('1'));
|
||||
});
|
||||
});
|
|
@ -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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { isEmpty } from 'lodash';
|
||||
import type { TraceDoc, TraceItems } from './get_trace_items';
|
||||
|
||||
interface Child {
|
||||
traceDoc: TraceDoc;
|
||||
children?: Child[];
|
||||
}
|
||||
|
||||
const MAX_NUMBER_OF_CHILDREN = 2;
|
||||
|
||||
export function buildChildrenTree({
|
||||
initialTraceDoc,
|
||||
itemsGroupedByParentId,
|
||||
maxNumberOfChildren,
|
||||
}: {
|
||||
initialTraceDoc: TraceDoc;
|
||||
itemsGroupedByParentId: Record<string, TraceDoc[]>;
|
||||
maxNumberOfChildren: number;
|
||||
}) {
|
||||
let _processedItemsCount = 0;
|
||||
function findChildren(traceDoc: TraceDoc) {
|
||||
const id = getId(traceDoc);
|
||||
if (!id) {
|
||||
return [];
|
||||
}
|
||||
const children: Child[] = [];
|
||||
const _children = itemsGroupedByParentId[id];
|
||||
if (isEmpty(_children)) {
|
||||
return [];
|
||||
}
|
||||
for (let i = 0; i < _children.length; i++) {
|
||||
const child = _children[i];
|
||||
_processedItemsCount++;
|
||||
if (_processedItemsCount > maxNumberOfChildren) {
|
||||
break;
|
||||
}
|
||||
children.push({ traceDoc: child, children: findChildren(child) });
|
||||
}
|
||||
return children;
|
||||
}
|
||||
return findChildren(initialTraceDoc);
|
||||
}
|
||||
|
||||
export interface FocusedTraceItems {
|
||||
rootTransaction: TraceDoc;
|
||||
parentDoc?: TraceDoc;
|
||||
focusedTraceDoc: TraceDoc;
|
||||
focusedTraceTree: Child[];
|
||||
}
|
||||
|
||||
export function buildFocusedTraceItems({
|
||||
traceItems,
|
||||
docId,
|
||||
}: {
|
||||
traceItems: TraceItems;
|
||||
docId: string;
|
||||
}): FocusedTraceItems | undefined {
|
||||
const { traceDocs } = traceItems;
|
||||
|
||||
const itemsById = traceDocs.reduce<Record<string, TraceDoc>>((acc, curr) => {
|
||||
const id = getId(curr);
|
||||
if (!id) {
|
||||
return acc;
|
||||
}
|
||||
acc[id] = curr;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const focusedTraceDoc = itemsById[docId];
|
||||
if (!focusedTraceDoc) {
|
||||
return undefined;
|
||||
}
|
||||
const parentDoc = focusedTraceDoc.parent?.id ? itemsById[focusedTraceDoc.parent?.id] : undefined;
|
||||
|
||||
const itemsGroupedByParentId = traceDocs.reduce<Record<string, TraceDoc[]>>((acc, curr) => {
|
||||
const parentId = curr.parent?.id;
|
||||
const isRootTransaction = !parentId;
|
||||
if (isRootTransaction) {
|
||||
acc.root = [curr];
|
||||
return acc;
|
||||
}
|
||||
|
||||
const group = acc[parentId] || [];
|
||||
group.push(curr);
|
||||
acc[parentId] = group;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const rootTransaction = itemsGroupedByParentId.root?.[0];
|
||||
|
||||
const focusedTraceTree = buildChildrenTree({
|
||||
initialTraceDoc: focusedTraceDoc,
|
||||
itemsGroupedByParentId,
|
||||
maxNumberOfChildren: MAX_NUMBER_OF_CHILDREN,
|
||||
});
|
||||
|
||||
return {
|
||||
rootTransaction,
|
||||
parentDoc,
|
||||
focusedTraceDoc,
|
||||
focusedTraceTree,
|
||||
};
|
||||
}
|
||||
|
||||
const getId = (traceDoc: TraceDoc) => traceDoc.span?.id ?? traceDoc.transaction?.id;
|
|
@ -64,9 +64,11 @@ import { ApmDocumentType } from '../../../common/document_type';
|
|||
import { RollupInterval } from '../../../common/rollup';
|
||||
import { mapOtelToSpanLink } from '../span_links/utils';
|
||||
|
||||
export type TraceDoc = WaterfallTransaction | WaterfallSpan;
|
||||
|
||||
export interface TraceItems {
|
||||
exceedsMax: boolean;
|
||||
traceDocs: Array<WaterfallTransaction | WaterfallSpan>;
|
||||
traceDocs: TraceDoc[];
|
||||
errorDocs: WaterfallError[];
|
||||
spanLinksCountById: Record<string, number>;
|
||||
traceDocsTotal: number;
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* 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 type { APMEventClient } from '@kbn/apm-data-access-plugin/server';
|
||||
import { ProcessorEvent } from '@kbn/observability-plugin/common';
|
||||
import { rangeQuery, termQuery } from '@kbn/observability-plugin/server';
|
||||
import { SERVICE_NAME, TRACE_ID } from '../../../common/es_fields/apm';
|
||||
|
||||
export async function getTraceSummaryCount({
|
||||
apmEventClient,
|
||||
start,
|
||||
end,
|
||||
traceId,
|
||||
}: {
|
||||
apmEventClient: APMEventClient;
|
||||
start: number;
|
||||
end: number;
|
||||
traceId: string;
|
||||
}) {
|
||||
const params = {
|
||||
apm: {
|
||||
events: [ProcessorEvent.span, ProcessorEvent.transaction],
|
||||
},
|
||||
track_total_hits: true,
|
||||
size: 0,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [...rangeQuery(start, end), ...termQuery(TRACE_ID, traceId)],
|
||||
},
|
||||
},
|
||||
aggs: { serviceCount: { cardinality: { field: SERVICE_NAME } } },
|
||||
};
|
||||
|
||||
const { aggregations, hits } = await apmEventClient.search(
|
||||
'observability_overview_get_service_count',
|
||||
params
|
||||
);
|
||||
|
||||
return { services: aggregations?.serviceCount.value || 0, traceEvents: hits.total.value };
|
||||
}
|
|
@ -5,29 +5,32 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import * as t from 'io-ts';
|
||||
import { toNumberRt } from '@kbn/io-ts-utils';
|
||||
import * as t from 'io-ts';
|
||||
import { TraceSearchType } from '../../../common/trace_explorer';
|
||||
import type { Span } from '../../../typings/es_schemas/ui/span';
|
||||
import type { Transaction } from '../../../typings/es_schemas/ui/transaction';
|
||||
import { getApmEventClient } from '../../lib/helpers/get_apm_event_client';
|
||||
import { getRandomSampler } from '../../lib/helpers/get_random_sampler';
|
||||
import { getSearchTransactionsEvents } from '../../lib/helpers/transactions';
|
||||
import { createApmServerRoute } from '../apm_routes/create_apm_server_route';
|
||||
import { environmentRt, kueryRt, probabilityRt, rangeRt } from '../default_api_types';
|
||||
import { getSpan } from '../transactions/get_span';
|
||||
import { getTransaction } from '../transactions/get_transaction';
|
||||
import { getTransactionByName } from '../transactions/get_transaction_by_name';
|
||||
import {
|
||||
type TransactionDetailRedirectInfo,
|
||||
getRootTransactionByTraceId,
|
||||
type TransactionDetailRedirectInfo,
|
||||
} from '../transactions/get_transaction_by_trace';
|
||||
import type { FocusedTraceItems } from './build_focused_trace_items';
|
||||
import { buildFocusedTraceItems } from './build_focused_trace_items';
|
||||
import type { TopTracesPrimaryStatsResponse } from './get_top_traces_primary_stats';
|
||||
import { getTopTracesPrimaryStats } from './get_top_traces_primary_stats';
|
||||
import type { TraceItems } from './get_trace_items';
|
||||
import { getTraceItems } from './get_trace_items';
|
||||
import type { TraceSamplesResponse } from './get_trace_samples_by_query';
|
||||
import { getTraceSamplesByQuery } from './get_trace_samples_by_query';
|
||||
import { getRandomSampler } from '../../lib/helpers/get_random_sampler';
|
||||
import { getApmEventClient } from '../../lib/helpers/get_apm_event_client';
|
||||
import { getSpan } from '../transactions/get_span';
|
||||
import type { Transaction } from '../../../typings/es_schemas/ui/transaction';
|
||||
import type { Span } from '../../../typings/es_schemas/ui/span';
|
||||
import { getTransactionByName } from '../transactions/get_transaction_by_name';
|
||||
import { getTraceSummaryCount } from './get_trace_summary_count';
|
||||
|
||||
const tracesRoute = createApmServerRoute({
|
||||
endpoint: 'GET /internal/apm/traces',
|
||||
|
@ -114,6 +117,49 @@ const tracesByIdRoute = createApmServerRoute({
|
|||
},
|
||||
});
|
||||
|
||||
const focusedTraceRoute = createApmServerRoute({
|
||||
endpoint: 'GET /internal/apm/traces/{traceId}/{docId}',
|
||||
params: t.type({
|
||||
path: t.type({
|
||||
traceId: t.string,
|
||||
docId: t.string,
|
||||
}),
|
||||
query: t.intersection([rangeRt, t.partial({ maxTraceItems: toNumberRt })]),
|
||||
}),
|
||||
security: { authz: { requiredPrivileges: ['apm'] } },
|
||||
handler: async (
|
||||
resources
|
||||
): Promise<{
|
||||
traceItems?: FocusedTraceItems;
|
||||
summary: { services: number; traceEvents: number; errors: number };
|
||||
}> => {
|
||||
const apmEventClient = await getApmEventClient(resources);
|
||||
const { params, config, logger } = resources;
|
||||
const { traceId, docId } = params.path;
|
||||
const { start, end } = params.query;
|
||||
|
||||
const [traceItems, traceSummaryCount] = await Promise.all([
|
||||
getTraceItems({
|
||||
traceId,
|
||||
config,
|
||||
apmEventClient,
|
||||
start,
|
||||
end,
|
||||
maxTraceItemsFromUrlParam: params.query.maxTraceItems,
|
||||
logger,
|
||||
}),
|
||||
getTraceSummaryCount({ apmEventClient, start, end, traceId }),
|
||||
]);
|
||||
|
||||
const focusedTraceItems = buildFocusedTraceItems({ traceItems, docId });
|
||||
|
||||
return {
|
||||
traceItems: focusedTraceItems,
|
||||
summary: { ...traceSummaryCount, errors: traceItems.errorDocs.length },
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
const rootTransactionByTraceIdRoute = createApmServerRoute({
|
||||
endpoint: 'GET /internal/apm/traces/{traceId}/root_transaction',
|
||||
params: t.type({
|
||||
|
@ -319,4 +365,5 @@ export const traceRouteRepository = {
|
|||
...transactionFromTraceByIdRoute,
|
||||
...spanFromTraceByIdRoute,
|
||||
...transactionByNameRoute,
|
||||
...focusedTraceRoute,
|
||||
};
|
||||
|
|
|
@ -0,0 +1,238 @@
|
|||
/*
|
||||
* 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 { apm, timerange } from '@kbn/apm-synthtrace-client';
|
||||
import expect from '@kbn/expect';
|
||||
import type { Environment } from '@kbn/apm-plugin/common/environment_rt';
|
||||
import { ENVIRONMENT_ALL } from '@kbn/apm-plugin/common/environment_filter_values';
|
||||
import { TraceSearchType } from '@kbn/apm-plugin/common/trace_explorer';
|
||||
import type { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api';
|
||||
import { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace';
|
||||
import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context';
|
||||
import { generateTrace } from './generate_trace';
|
||||
|
||||
type FocusedTraceResponseType = APIReturnType<'GET /internal/apm/traces/{traceId}/{docId}'>;
|
||||
|
||||
export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) {
|
||||
const apmApiClient = getService('apmApi');
|
||||
const synthtrace = getService('synthtrace');
|
||||
|
||||
const start = new Date('2022-01-01T00:00:00.000Z').getTime();
|
||||
const end = new Date('2022-01-01T00:15:00.000Z').getTime() - 1;
|
||||
|
||||
const endWithOffset = end + 100000;
|
||||
|
||||
describe('traces', () => {
|
||||
async function fetchTraceSamples({
|
||||
query,
|
||||
type,
|
||||
environment,
|
||||
}: {
|
||||
query: string;
|
||||
type: TraceSearchType;
|
||||
environment: Environment;
|
||||
}) {
|
||||
return apmApiClient.readUser({
|
||||
endpoint: `GET /internal/apm/traces/find`,
|
||||
params: {
|
||||
query: {
|
||||
query,
|
||||
type,
|
||||
start: new Date(start).toISOString(),
|
||||
end: new Date(endWithOffset).toISOString(),
|
||||
environment,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function fetchFocusedTrace({
|
||||
traceId,
|
||||
docId,
|
||||
}: {
|
||||
traceId: string;
|
||||
docId: string | undefined;
|
||||
}) {
|
||||
if (!docId) {
|
||||
return undefined;
|
||||
}
|
||||
return apmApiClient.readUser({
|
||||
endpoint: `GET /internal/apm/traces/{traceId}/{docId}`,
|
||||
params: {
|
||||
path: { traceId, docId },
|
||||
query: {
|
||||
start: new Date(start).toISOString(),
|
||||
end: new Date(endWithOffset).toISOString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
let apmSynthtraceEsClient: ApmSynthtraceEsClient;
|
||||
before(async () => {
|
||||
apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient();
|
||||
});
|
||||
|
||||
after(() => apmSynthtraceEsClient.clean());
|
||||
|
||||
describe('when traces exist', () => {
|
||||
before(() => {
|
||||
const java = apm
|
||||
.service({ name: 'java', environment: 'production', agentName: 'java' })
|
||||
.instance('java');
|
||||
|
||||
const node = apm
|
||||
.service({ name: 'node', environment: 'development', agentName: 'nodejs' })
|
||||
.instance('node');
|
||||
|
||||
const python = apm
|
||||
.service({ name: 'python', environment: 'production', agentName: 'python' })
|
||||
.instance('python');
|
||||
|
||||
return apmSynthtraceEsClient.index(
|
||||
timerange(start, end)
|
||||
.interval('15m')
|
||||
.rate(1)
|
||||
.generator((timestamp) =>
|
||||
generateTrace(timestamp, [python, node, java], 'elasticsearch')
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
describe('focused trace', () => {
|
||||
let traceId: string;
|
||||
let focusedTrace: FocusedTraceResponseType | undefined;
|
||||
let rootTransactionId: string | undefined;
|
||||
before(async () => {
|
||||
const response = await fetchTraceSamples({
|
||||
query: '',
|
||||
type: TraceSearchType.kql,
|
||||
environment: ENVIRONMENT_ALL.value,
|
||||
});
|
||||
|
||||
expect(response.status).to.be(200);
|
||||
traceId = response.body.traceSamples[0].traceId;
|
||||
const focusedTraceResponse = await fetchFocusedTrace({
|
||||
traceId,
|
||||
docId: response.body.traceSamples[0].transactionId,
|
||||
});
|
||||
expect(focusedTraceResponse?.status).to.be(200);
|
||||
focusedTrace = focusedTraceResponse?.body;
|
||||
rootTransactionId = focusedTrace?.traceItems?.rootTransaction?.transaction?.id;
|
||||
});
|
||||
|
||||
describe('focus on root transaction', () => {
|
||||
it('returns same root transaction and focused item', async () => {
|
||||
expect(focusedTrace?.traceItems?.rootTransaction?.transaction?.id).to.eql(
|
||||
focusedTrace?.traceItems?.focusedTraceDoc?.transaction?.id
|
||||
);
|
||||
});
|
||||
|
||||
it('does not have parent item', () => {
|
||||
expect(focusedTrace?.traceItems?.parentDoc).to.be(undefined);
|
||||
});
|
||||
|
||||
it('has 2 children', () => {
|
||||
expect(focusedTrace?.traceItems?.focusedTraceTree.length).to.eql(1);
|
||||
expect(focusedTrace?.traceItems?.focusedTraceTree?.[0]?.children?.length).to.eql(1);
|
||||
});
|
||||
|
||||
it('returns trace summary', () => {
|
||||
expect(focusedTrace?.summary).to.eql({
|
||||
services: 3,
|
||||
traceEvents: 6,
|
||||
errors: 0,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('focus on node transaction', () => {
|
||||
let nodeParentSpanId: string | undefined;
|
||||
let nodeTransactionId: string | undefined;
|
||||
before(async () => {
|
||||
nodeParentSpanId = focusedTrace?.traceItems?.focusedTraceTree?.[0]?.traceDoc?.span?.id;
|
||||
nodeTransactionId =
|
||||
focusedTrace?.traceItems?.focusedTraceTree?.[0]?.children?.[0]?.traceDoc?.transaction
|
||||
?.id;
|
||||
const focusedTraceResponse = await fetchFocusedTrace({
|
||||
traceId,
|
||||
docId: nodeTransactionId,
|
||||
});
|
||||
expect(focusedTraceResponse?.status).to.be(200);
|
||||
focusedTrace = focusedTraceResponse?.body;
|
||||
});
|
||||
|
||||
it('focus on node transaction', () => {
|
||||
expect(focusedTrace?.traceItems?.focusedTraceDoc?.transaction?.id).to.eql(
|
||||
nodeTransactionId
|
||||
);
|
||||
});
|
||||
|
||||
it('returns root transaction', async () => {
|
||||
expect(focusedTrace?.traceItems?.rootTransaction?.transaction?.id).to.eql(
|
||||
rootTransactionId
|
||||
);
|
||||
});
|
||||
|
||||
it('returns parent span', () => {
|
||||
expect(focusedTrace?.traceItems?.parentDoc?.span?.id).to.eql(nodeParentSpanId);
|
||||
});
|
||||
|
||||
it('has 2 children', () => {
|
||||
expect(focusedTrace?.traceItems?.focusedTraceTree.length).to.eql(1);
|
||||
expect(focusedTrace?.traceItems?.focusedTraceTree?.[0]?.children?.length).to.eql(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('focused item not found', () => {
|
||||
before(async () => {
|
||||
const focusedTraceResponse = await fetchFocusedTrace({
|
||||
traceId,
|
||||
docId: 'bar',
|
||||
});
|
||||
expect(focusedTraceResponse?.status).to.be(200);
|
||||
focusedTrace = focusedTraceResponse?.body;
|
||||
});
|
||||
|
||||
it('returns trace summary', () => {
|
||||
expect(focusedTrace?.summary).to.eql({
|
||||
services: 3,
|
||||
traceEvents: 6,
|
||||
errors: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns empty focused trace', () => {
|
||||
expect(focusedTrace?.traceItems).to.be(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('trace not found', () => {
|
||||
before(async () => {
|
||||
const focusedTraceResponse = await fetchFocusedTrace({
|
||||
traceId: 'foo',
|
||||
docId: 'bar',
|
||||
});
|
||||
expect(focusedTraceResponse?.status).to.be(200);
|
||||
focusedTrace = focusedTraceResponse?.body;
|
||||
});
|
||||
|
||||
it('returns trace summary', () => {
|
||||
expect(focusedTrace?.summary).to.eql({
|
||||
services: 0,
|
||||
traceEvents: 0,
|
||||
errors: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns empty focused trace', () => {
|
||||
expect(focusedTrace?.traceItems).to.be(undefined);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -15,5 +15,6 @@ export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext)
|
|||
loadTestFile(require.resolve('./top_traces.spec.ts'));
|
||||
loadTestFile(require.resolve('./trace_by_id.spec.ts'));
|
||||
loadTestFile(require.resolve('./transaction_details.spec.ts'));
|
||||
loadTestFile(require.resolve('./focused_trace.spec.ts'));
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue