mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[APM] Show badge for failed spans in waterfall (#109812)
Co-authored-by: Casper Hübertz <casper@formgeist.com>
This commit is contained in:
parent
5464af6923
commit
d3f6303014
42 changed files with 2503 additions and 2549 deletions
|
@ -73,11 +73,7 @@ export function TransactionDistribution({
|
|||
|
||||
const { urlParams } = useUrlParams();
|
||||
|
||||
const {
|
||||
waterfall,
|
||||
exceedsMax,
|
||||
status: waterfallStatus,
|
||||
} = useWaterfallFetcher();
|
||||
const { waterfall, status: waterfallStatus } = useWaterfallFetcher();
|
||||
|
||||
const markerCurrentTransaction =
|
||||
waterfall.entryWaterfallTransaction?.doc.transaction.duration.us;
|
||||
|
@ -215,7 +211,6 @@ export function TransactionDistribution({
|
|||
urlParams={urlParams}
|
||||
waterfall={waterfall}
|
||||
isLoading={waterfallStatus === FETCH_STATUS.LOADING}
|
||||
exceedsMax={exceedsMax}
|
||||
traceSamples={traceSamples}
|
||||
/>
|
||||
</>
|
||||
|
|
|
@ -13,9 +13,9 @@ import { useTimeRange } from '../../../hooks/use_time_range';
|
|||
import { getWaterfall } from './waterfall_with_summary/waterfall_container/Waterfall/waterfall_helpers/waterfall_helpers';
|
||||
|
||||
const INITIAL_DATA = {
|
||||
root: undefined,
|
||||
trace: { items: [], exceedsMax: false, errorDocs: [] },
|
||||
errorsPerTransaction: {},
|
||||
errorDocs: [],
|
||||
traceDocs: [],
|
||||
exceedsMax: false,
|
||||
};
|
||||
|
||||
export function useWaterfallFetcher() {
|
||||
|
@ -51,5 +51,5 @@ export function useWaterfallFetcher() {
|
|||
transactionId,
|
||||
]);
|
||||
|
||||
return { waterfall, status, error, exceedsMax: data.trace.exceedsMax };
|
||||
return { waterfall, status, error };
|
||||
}
|
||||
|
|
|
@ -1,38 +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 { fireEvent, render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { expectTextsInDocument } from '../../../../utils/testHelpers';
|
||||
import { ErrorCount } from './ErrorCount';
|
||||
|
||||
describe('ErrorCount', () => {
|
||||
it('shows singular error message', () => {
|
||||
const component = render(<ErrorCount count={1} />);
|
||||
expectTextsInDocument(component, ['1 Error']);
|
||||
});
|
||||
it('shows plural error message', () => {
|
||||
const component = render(<ErrorCount count={2} />);
|
||||
expectTextsInDocument(component, ['2 Errors']);
|
||||
});
|
||||
it('prevents click propagation', () => {
|
||||
const mock = jest.fn();
|
||||
const { getByText } = render(
|
||||
<button onClick={mock}>
|
||||
<ErrorCount count={1} />
|
||||
</button>
|
||||
);
|
||||
fireEvent(
|
||||
getByText('1 Error'),
|
||||
new MouseEvent('click', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
})
|
||||
);
|
||||
expect(mock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
|
@ -1,35 +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 { EuiText, EuiTextColor } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
|
||||
interface Props {
|
||||
count: number;
|
||||
}
|
||||
|
||||
export function ErrorCount({ count }: Props) {
|
||||
return (
|
||||
<EuiText size="xs">
|
||||
<h4>
|
||||
<EuiTextColor
|
||||
color="danger"
|
||||
onClick={(e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
{i18n.translate('xpack.apm.transactionDetails.errorCount', {
|
||||
defaultMessage:
|
||||
'{errorCount, number} {errorCount, plural, one {Error} other {Errors}}',
|
||||
values: { errorCount: count },
|
||||
})}
|
||||
</EuiTextColor>
|
||||
</h4>
|
||||
</EuiText>
|
||||
);
|
||||
}
|
|
@ -21,15 +21,9 @@ interface Props {
|
|||
transaction: Transaction;
|
||||
urlParams: ApmUrlParams;
|
||||
waterfall: IWaterfall;
|
||||
exceedsMax: boolean;
|
||||
}
|
||||
|
||||
export function TransactionTabs({
|
||||
transaction,
|
||||
urlParams,
|
||||
waterfall,
|
||||
exceedsMax,
|
||||
}: Props) {
|
||||
export function TransactionTabs({ transaction, urlParams, waterfall }: Props) {
|
||||
const history = useHistory();
|
||||
const tabs = [timelineTab, metadataTab, logsTab];
|
||||
const currentTab =
|
||||
|
@ -65,7 +59,6 @@ export function TransactionTabs({
|
|||
<TabContent
|
||||
urlParams={urlParams}
|
||||
waterfall={waterfall}
|
||||
exceedsMax={exceedsMax}
|
||||
transaction={transaction}
|
||||
/>
|
||||
</React.Fragment>
|
||||
|
@ -99,19 +92,11 @@ const logsTab = {
|
|||
function TimelineTabContent({
|
||||
urlParams,
|
||||
waterfall,
|
||||
exceedsMax,
|
||||
}: {
|
||||
urlParams: ApmUrlParams;
|
||||
waterfall: IWaterfall;
|
||||
exceedsMax: boolean;
|
||||
}) {
|
||||
return (
|
||||
<WaterfallContainer
|
||||
urlParams={urlParams}
|
||||
waterfall={waterfall}
|
||||
exceedsMax={exceedsMax}
|
||||
/>
|
||||
);
|
||||
return <WaterfallContainer urlParams={urlParams} waterfall={waterfall} />;
|
||||
}
|
||||
|
||||
function MetadataTabContent({ transaction }: { transaction: Transaction }) {
|
||||
|
|
|
@ -30,7 +30,6 @@ import { useApmParams } from '../../../../hooks/use_apm_params';
|
|||
interface Props {
|
||||
urlParams: ApmUrlParams;
|
||||
waterfall: IWaterfall;
|
||||
exceedsMax: boolean;
|
||||
isLoading: boolean;
|
||||
traceSamples: TraceSample[];
|
||||
}
|
||||
|
@ -38,7 +37,6 @@ interface Props {
|
|||
export function WaterfallWithSummary({
|
||||
urlParams,
|
||||
waterfall,
|
||||
exceedsMax,
|
||||
isLoading,
|
||||
traceSamples,
|
||||
}: Props) {
|
||||
|
@ -125,7 +123,7 @@ export function WaterfallWithSummary({
|
|||
<EuiSpacer size="s" />
|
||||
|
||||
<TransactionSummary
|
||||
errorCount={waterfall.errorsCount}
|
||||
errorCount={waterfall.apiResponse.errorDocs.length}
|
||||
totalDuration={waterfall.rootTransaction?.transaction.duration.us}
|
||||
transaction={entryTransaction}
|
||||
/>
|
||||
|
@ -135,7 +133,6 @@ export function WaterfallWithSummary({
|
|||
transaction={entryTransaction}
|
||||
urlParams={urlParams}
|
||||
waterfall={waterfall}
|
||||
exceedsMax={exceedsMax}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -22,8 +22,7 @@ interface AccordionWaterfallProps {
|
|||
level: number;
|
||||
duration: IWaterfall['duration'];
|
||||
waterfallItemId?: string;
|
||||
errorsPerTransaction: IWaterfall['errorsPerTransaction'];
|
||||
childrenByParentId: Record<string, IWaterfallSpanOrTransaction[]>;
|
||||
waterfall: IWaterfall;
|
||||
onToggleEntryTransaction?: () => void;
|
||||
timelineMargins: Margins;
|
||||
onClickWaterfallItem: (item: IWaterfallSpanOrTransaction) => void;
|
||||
|
@ -96,9 +95,8 @@ export function AccordionWaterfall(props: AccordionWaterfallProps) {
|
|||
item,
|
||||
level,
|
||||
duration,
|
||||
childrenByParentId,
|
||||
waterfall,
|
||||
waterfallItemId,
|
||||
errorsPerTransaction,
|
||||
timelineMargins,
|
||||
onClickWaterfallItem,
|
||||
onToggleEntryTransaction,
|
||||
|
@ -106,12 +104,8 @@ export function AccordionWaterfall(props: AccordionWaterfallProps) {
|
|||
|
||||
const nextLevel = level + 1;
|
||||
|
||||
const errorCount =
|
||||
item.docType === 'transaction'
|
||||
? errorsPerTransaction[item.doc.transaction.id]
|
||||
: 0;
|
||||
|
||||
const children = childrenByParentId[item.id] || [];
|
||||
const children = waterfall.childrenByParentId[item.id] || [];
|
||||
const errorCount = waterfall.getErrorCount(item.id);
|
||||
|
||||
// To indent the items creating the parent/child tree
|
||||
const marginLeftLevel = 8 * level;
|
||||
|
@ -121,7 +115,7 @@ export function AccordionWaterfall(props: AccordionWaterfallProps) {
|
|||
buttonClassName={`button_${item.id}`}
|
||||
key={item.id}
|
||||
id={item.id}
|
||||
hasError={errorCount > 0}
|
||||
hasError={item.doc.event?.outcome === 'failure'}
|
||||
marginLeftLevel={marginLeftLevel}
|
||||
childrenCount={children.length}
|
||||
buttonContent={
|
||||
|
@ -152,16 +146,11 @@ export function AccordionWaterfall(props: AccordionWaterfallProps) {
|
|||
>
|
||||
{children.map((child) => (
|
||||
<AccordionWaterfall
|
||||
{...props}
|
||||
key={child.id}
|
||||
isOpen={isOpen}
|
||||
item={child}
|
||||
level={nextLevel}
|
||||
waterfallItemId={waterfallItemId}
|
||||
errorsPerTransaction={errorsPerTransaction}
|
||||
duration={duration}
|
||||
childrenByParentId={childrenByParentId}
|
||||
timelineMargins={timelineMargins}
|
||||
onClickWaterfallItem={onClickWaterfallItem}
|
||||
item={child}
|
||||
/>
|
||||
))}
|
||||
</StyledAccordion>
|
||||
|
|
|
@ -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 React from 'react';
|
||||
import { EuiBadge, EuiToolTip } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useTheme } from '../../../../../../hooks/use_theme';
|
||||
|
||||
import { euiStyled } from '../../../../../../../../../../src/plugins/kibana_react/common';
|
||||
|
||||
const ResetLineHeight = euiStyled.span`
|
||||
line-height: initial;
|
||||
`;
|
||||
|
||||
export function FailureBadge({ outcome }: { outcome?: 'success' | 'failure' }) {
|
||||
const theme = useTheme();
|
||||
|
||||
if (outcome !== 'failure') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ResetLineHeight>
|
||||
<EuiToolTip
|
||||
content={i18n.translate('xpack.apm.failure_badge.tooltip', {
|
||||
defaultMessage: 'event.outcome = failure',
|
||||
})}
|
||||
>
|
||||
<EuiBadge color={theme.eui.euiColorDanger}>failure</EuiBadge>
|
||||
</EuiToolTip>
|
||||
</ResetLineHeight>
|
||||
);
|
||||
}
|
|
@ -21,7 +21,6 @@ import { WaterfallFlyout } from './waterfall_flyout';
|
|||
import {
|
||||
IWaterfall,
|
||||
IWaterfallItem,
|
||||
IWaterfallSpanOrTransaction,
|
||||
} from './waterfall_helpers/waterfall_helpers';
|
||||
|
||||
const Container = euiStyled.div`
|
||||
|
@ -61,9 +60,8 @@ const WaterfallItemsContainer = euiStyled.div`
|
|||
interface Props {
|
||||
waterfallItemId?: string;
|
||||
waterfall: IWaterfall;
|
||||
exceedsMax: boolean;
|
||||
}
|
||||
export function Waterfall({ waterfall, exceedsMax, waterfallItemId }: Props) {
|
||||
export function Waterfall({ waterfall, waterfallItemId }: Props) {
|
||||
const history = useHistory();
|
||||
const [isAccordionOpen, setIsAccordionOpen] = useState(true);
|
||||
const itemContainerHeight = 58; // TODO: This is a nasty way to calculate the height of the svg element. A better approach should be found
|
||||
|
@ -74,37 +72,10 @@ export function Waterfall({ waterfall, exceedsMax, waterfallItemId }: Props) {
|
|||
const agentMarks = getAgentMarks(waterfall.entryWaterfallTransaction?.doc);
|
||||
const errorMarks = getErrorMarks(waterfall.errorItems);
|
||||
|
||||
function renderItems(
|
||||
childrenByParentId: Record<string | number, IWaterfallSpanOrTransaction[]>
|
||||
) {
|
||||
const { entryWaterfallTransaction } = waterfall;
|
||||
if (!entryWaterfallTransaction) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<AccordionWaterfall
|
||||
// used to recreate the entire tree when `isAccordionOpen` changes, collapsing or expanding all elements.
|
||||
key={`accordion_state_${isAccordionOpen}`}
|
||||
isOpen={isAccordionOpen}
|
||||
item={entryWaterfallTransaction}
|
||||
level={0}
|
||||
waterfallItemId={waterfallItemId}
|
||||
errorsPerTransaction={waterfall.errorsPerTransaction}
|
||||
duration={duration}
|
||||
childrenByParentId={childrenByParentId}
|
||||
timelineMargins={TIMELINE_MARGINS}
|
||||
onClickWaterfallItem={(item: IWaterfallItem) =>
|
||||
toggleFlyout({ history, item })
|
||||
}
|
||||
onToggleEntryTransaction={() => setIsAccordionOpen((isOpen) => !isOpen)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<HeightRetainer>
|
||||
<Container>
|
||||
{exceedsMax && (
|
||||
{waterfall.apiResponse.exceedsMax && (
|
||||
<EuiCallOut
|
||||
color="warning"
|
||||
size="s"
|
||||
|
@ -132,7 +103,25 @@ export function Waterfall({ waterfall, exceedsMax, waterfallItemId }: Props) {
|
|||
/>
|
||||
</div>
|
||||
<WaterfallItemsContainer>
|
||||
{renderItems(waterfall.childrenByParentId)}
|
||||
{!waterfall.entryWaterfallTransaction ? null : (
|
||||
<AccordionWaterfall
|
||||
// used to recreate the entire tree when `isAccordionOpen` changes, collapsing or expanding all elements.
|
||||
key={`accordion_state_${isAccordionOpen}`}
|
||||
isOpen={isAccordionOpen}
|
||||
item={waterfall.entryWaterfallTransaction}
|
||||
level={0}
|
||||
waterfallItemId={waterfallItemId}
|
||||
duration={duration}
|
||||
waterfall={waterfall}
|
||||
timelineMargins={TIMELINE_MARGINS}
|
||||
onClickWaterfallItem={(item: IWaterfallItem) =>
|
||||
toggleFlyout({ history, item })
|
||||
}
|
||||
onToggleEntryTransaction={() =>
|
||||
setIsAccordionOpen((isOpen) => !isOpen)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</WaterfallItemsContainer>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -35,8 +35,9 @@ import { HttpInfoSummaryItem } from '../../../../../../shared/Summary/http_info_
|
|||
import { TimestampTooltip } from '../../../../../../shared/TimestampTooltip';
|
||||
import { ResponsiveFlyout } from '../ResponsiveFlyout';
|
||||
import { SyncBadge } from '../sync_badge';
|
||||
import { DatabaseContext } from './database_context';
|
||||
import { SpanDatabase } from './span_db';
|
||||
import { StickySpanProperties } from './sticky_span_properties';
|
||||
import { FailureBadge } from '../failure_badge';
|
||||
|
||||
function formatType(type: string) {
|
||||
switch (type) {
|
||||
|
@ -73,13 +74,11 @@ function getSpanTypes(span: Span) {
|
|||
};
|
||||
}
|
||||
|
||||
const SpanBadge = euiStyled(EuiBadge)`
|
||||
display: inline-block;
|
||||
margin-right: ${({ theme }) => theme.eui.euiSizeXS};
|
||||
`;
|
||||
|
||||
const HttpInfoContainer = euiStyled('div')`
|
||||
margin-right: ${({ theme }) => theme.eui.euiSizeXS};
|
||||
const ContainerWithMarginRight = euiStyled.div`
|
||||
/* add margin to all direct descendants */
|
||||
& > * {
|
||||
margin-right: ${({ theme }) => theme.eui.euiSizeXS};
|
||||
}
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
|
@ -101,7 +100,7 @@ export function SpanFlyout({
|
|||
|
||||
const stackframes = span.span.stacktrace;
|
||||
const codeLanguage = parentTransaction?.service.language?.name;
|
||||
const dbContext = span.span.db;
|
||||
const spanDb = span.span.db;
|
||||
const httpContext = span.span.http;
|
||||
const spanTypes = getSpanTypes(span);
|
||||
const spanHttpStatusCode = httpContext?.response?.status_code;
|
||||
|
@ -173,15 +172,13 @@ export function SpanFlyout({
|
|||
/>
|
||||
)}
|
||||
</>,
|
||||
<>
|
||||
<ContainerWithMarginRight>
|
||||
{spanHttpUrl && (
|
||||
<HttpInfoContainer>
|
||||
<HttpInfoSummaryItem
|
||||
method={spanHttpMethod}
|
||||
url={spanHttpUrl}
|
||||
status={spanHttpStatusCode}
|
||||
/>
|
||||
</HttpInfoContainer>
|
||||
<HttpInfoSummaryItem
|
||||
method={spanHttpMethod}
|
||||
url={spanHttpUrl}
|
||||
status={spanHttpStatusCode}
|
||||
/>
|
||||
)}
|
||||
<EuiToolTip
|
||||
content={i18n.translate(
|
||||
|
@ -189,7 +186,7 @@ export function SpanFlyout({
|
|||
{ defaultMessage: 'Type' }
|
||||
)}
|
||||
>
|
||||
<SpanBadge color="hollow">{spanTypes.spanType}</SpanBadge>
|
||||
<EuiBadge color="hollow">{spanTypes.spanType}</EuiBadge>
|
||||
</EuiToolTip>
|
||||
{spanTypes.spanSubtype && (
|
||||
<EuiToolTip
|
||||
|
@ -198,9 +195,7 @@ export function SpanFlyout({
|
|||
{ defaultMessage: 'Subtype' }
|
||||
)}
|
||||
>
|
||||
<SpanBadge color="hollow">
|
||||
{spanTypes.spanSubtype}
|
||||
</SpanBadge>
|
||||
<EuiBadge color="hollow">{spanTypes.spanSubtype}</EuiBadge>
|
||||
</EuiToolTip>
|
||||
)}
|
||||
{spanTypes.spanAction && (
|
||||
|
@ -210,15 +205,18 @@ export function SpanFlyout({
|
|||
{ defaultMessage: 'Action' }
|
||||
)}
|
||||
>
|
||||
<SpanBadge color="hollow">{spanTypes.spanAction}</SpanBadge>
|
||||
<EuiBadge color="hollow">{spanTypes.spanAction}</EuiBadge>
|
||||
</EuiToolTip>
|
||||
)}
|
||||
|
||||
<FailureBadge outcome={span.event?.outcome} />
|
||||
|
||||
<SyncBadge sync={span.span.sync} />
|
||||
</>,
|
||||
</ContainerWithMarginRight>,
|
||||
]}
|
||||
/>
|
||||
<EuiHorizontalRule />
|
||||
<DatabaseContext dbContext={dbContext} />
|
||||
<SpanDatabase spanDb={spanDb} />
|
||||
<EuiTabbedContent
|
||||
tabs={[
|
||||
{
|
||||
|
|
|
@ -30,20 +30,20 @@ const DatabaseStatement = euiStyled.div`
|
|||
`;
|
||||
|
||||
interface Props {
|
||||
dbContext?: NonNullable<Span['span']>['db'];
|
||||
spanDb?: NonNullable<Span['span']>['db'];
|
||||
}
|
||||
|
||||
export function DatabaseContext({ dbContext }: Props) {
|
||||
export function SpanDatabase({ spanDb }: Props) {
|
||||
const theme = useTheme();
|
||||
const dbSyntaxLineHeight = theme.eui.euiSizeL;
|
||||
const previewHeight = 240; // 10 * dbSyntaxLineHeight
|
||||
|
||||
if (!dbContext || !dbContext.statement) {
|
||||
if (!spanDb || !spanDb.statement) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (dbContext.type !== 'sql') {
|
||||
return <DatabaseStatement>{dbContext.statement}</DatabaseStatement>;
|
||||
if (spanDb.type !== 'sql') {
|
||||
return <DatabaseStatement>{spanDb.statement}</DatabaseStatement>;
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -73,7 +73,7 @@ export function DatabaseContext({ dbContext }: Props) {
|
|||
overflowX: 'scroll',
|
||||
}}
|
||||
>
|
||||
{dbContext.statement}
|
||||
{spanDb.statement}
|
||||
</SyntaxHighlighter>
|
||||
</TruncateHeightSection>
|
||||
</DatabaseStatement>
|
|
@ -8,12 +8,6 @@
|
|||
import { EuiBadge } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { euiStyled } from '../../../../../../../../../../src/plugins/kibana_react/common';
|
||||
|
||||
const SpanBadge = euiStyled(EuiBadge)`
|
||||
display: inline-block;
|
||||
margin-right: ${({ theme }) => theme.eui.euiSizeXS};
|
||||
`;
|
||||
|
||||
export interface SyncBadgeProps {
|
||||
/**
|
||||
|
@ -26,19 +20,19 @@ export function SyncBadge({ sync }: SyncBadgeProps) {
|
|||
switch (sync) {
|
||||
case true:
|
||||
return (
|
||||
<SpanBadge>
|
||||
<EuiBadge>
|
||||
{i18n.translate('xpack.apm.transactionDetails.syncBadgeBlocking', {
|
||||
defaultMessage: 'blocking',
|
||||
})}
|
||||
</SpanBadge>
|
||||
</EuiBadge>
|
||||
);
|
||||
case false:
|
||||
return (
|
||||
<SpanBadge>
|
||||
<EuiBadge>
|
||||
{i18n.translate('xpack.apm.transactionDetails.syncBadgeAsync', {
|
||||
defaultMessage: 'async',
|
||||
})}
|
||||
</SpanBadge>
|
||||
</EuiBadge>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
|
|
|
@ -55,7 +55,7 @@ export function WaterfallFlyout({
|
|||
rootTransactionDuration={
|
||||
waterfall.rootTransaction?.transaction.duration.us
|
||||
}
|
||||
errorCount={waterfall.errorsPerTransaction[currentItem.id]}
|
||||
errorCount={waterfall.getErrorCount(currentItem.id)}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
|
|
|
@ -912,11 +912,7 @@ Object {
|
|||
"skew": 0,
|
||||
},
|
||||
],
|
||||
"errorsCount": 1,
|
||||
"errorsPerTransaction": Object {
|
||||
"myTransactionId1": 2,
|
||||
"myTransactionId2": 3,
|
||||
},
|
||||
"getErrorCount": [Function],
|
||||
"items": Array [
|
||||
Object {
|
||||
"color": "",
|
||||
|
@ -2188,11 +2184,7 @@ Object {
|
|||
"skew": 0,
|
||||
},
|
||||
"errorItems": Array [],
|
||||
"errorsCount": 0,
|
||||
"errorsPerTransaction": Object {
|
||||
"myTransactionId1": 2,
|
||||
"myTransactionId2": 3,
|
||||
},
|
||||
"getErrorCount": [Function],
|
||||
"items": Array [
|
||||
Object {
|
||||
"color": "",
|
||||
|
|
|
@ -129,44 +129,39 @@ describe('waterfall_helpers', () => {
|
|||
|
||||
it('should return full waterfall', () => {
|
||||
const entryTransactionId = 'myTransactionId1';
|
||||
const errorsPerTransaction = {
|
||||
myTransactionId1: 2,
|
||||
myTransactionId2: 3,
|
||||
const apiResp = {
|
||||
traceDocs: hits,
|
||||
errorDocs,
|
||||
exceedsMax: false,
|
||||
};
|
||||
const waterfall = getWaterfall(
|
||||
{
|
||||
trace: { items: hits, errorDocs, exceedsMax: false },
|
||||
errorsPerTransaction,
|
||||
},
|
||||
entryTransactionId
|
||||
);
|
||||
const waterfall = getWaterfall(apiResp, entryTransactionId);
|
||||
const { apiResponse, ...waterfallRest } = waterfall;
|
||||
|
||||
expect(waterfall.items.length).toBe(6);
|
||||
expect(waterfall.items[0].id).toBe('myTransactionId1');
|
||||
expect(waterfall.errorItems.length).toBe(1);
|
||||
expect(waterfall.errorsCount).toEqual(1);
|
||||
expect(waterfall).toMatchSnapshot();
|
||||
expect(waterfall.getErrorCount('myTransactionId1')).toEqual(1);
|
||||
expect(waterfallRest).toMatchSnapshot();
|
||||
expect(apiResponse).toEqual(apiResp);
|
||||
});
|
||||
|
||||
it('should return partial waterfall', () => {
|
||||
const entryTransactionId = 'myTransactionId2';
|
||||
const errorsPerTransaction = {
|
||||
myTransactionId1: 2,
|
||||
myTransactionId2: 3,
|
||||
const apiResp = {
|
||||
traceDocs: hits,
|
||||
errorDocs,
|
||||
exceedsMax: false,
|
||||
};
|
||||
const waterfall = getWaterfall(
|
||||
{
|
||||
trace: { items: hits, errorDocs, exceedsMax: false },
|
||||
errorsPerTransaction,
|
||||
},
|
||||
entryTransactionId
|
||||
);
|
||||
const waterfall = getWaterfall(apiResp, entryTransactionId);
|
||||
|
||||
const { apiResponse, ...waterfallRest } = waterfall;
|
||||
|
||||
expect(waterfall.items.length).toBe(4);
|
||||
expect(waterfall.items[0].id).toBe('myTransactionId2');
|
||||
expect(waterfall.errorItems.length).toBe(0);
|
||||
expect(waterfall.errorsCount).toEqual(0);
|
||||
expect(waterfall).toMatchSnapshot();
|
||||
expect(waterfall.getErrorCount('myTransactionId2')).toEqual(0);
|
||||
expect(waterfallRest).toMatchSnapshot();
|
||||
expect(apiResponse).toEqual(apiResp);
|
||||
});
|
||||
it('should reparent spans', () => {
|
||||
const traceItems = [
|
||||
|
@ -238,8 +233,9 @@ describe('waterfall_helpers', () => {
|
|||
const entryTransactionId = 'myTransactionId1';
|
||||
const waterfall = getWaterfall(
|
||||
{
|
||||
trace: { items: traceItems, errorDocs: [], exceedsMax: false },
|
||||
errorsPerTransaction: {},
|
||||
traceDocs: traceItems,
|
||||
errorDocs: [],
|
||||
exceedsMax: false,
|
||||
},
|
||||
entryTransactionId
|
||||
);
|
||||
|
@ -247,6 +243,7 @@ describe('waterfall_helpers', () => {
|
|||
id: item.id,
|
||||
parentId: item.parent?.id,
|
||||
});
|
||||
|
||||
expect(waterfall.items.length).toBe(5);
|
||||
expect(getIdAndParentId(waterfall.items[0])).toEqual({
|
||||
id: 'myTransactionId1',
|
||||
|
@ -269,8 +266,9 @@ describe('waterfall_helpers', () => {
|
|||
parentId: 'mySpanIdB',
|
||||
});
|
||||
expect(waterfall.errorItems.length).toBe(0);
|
||||
expect(waterfall.errorsCount).toEqual(0);
|
||||
expect(waterfall.getErrorCount('myTransactionId1')).toEqual(0);
|
||||
});
|
||||
|
||||
it("shouldn't reparent spans when child id isn't found", () => {
|
||||
const traceItems = [
|
||||
{
|
||||
|
@ -341,8 +339,9 @@ describe('waterfall_helpers', () => {
|
|||
const entryTransactionId = 'myTransactionId1';
|
||||
const waterfall = getWaterfall(
|
||||
{
|
||||
trace: { items: traceItems, errorDocs: [], exceedsMax: false },
|
||||
errorsPerTransaction: {},
|
||||
traceDocs: traceItems,
|
||||
errorDocs: [],
|
||||
exceedsMax: false,
|
||||
},
|
||||
entryTransactionId
|
||||
);
|
||||
|
@ -372,7 +371,7 @@ describe('waterfall_helpers', () => {
|
|||
parentId: 'mySpanIdB',
|
||||
});
|
||||
expect(waterfall.errorItems.length).toBe(0);
|
||||
expect(waterfall.errorsCount).toEqual(0);
|
||||
expect(waterfall.getErrorCount('myTransactionId1')).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import { euiPaletteColorBlind } from '@elastic/eui';
|
||||
import { first, flatten, groupBy, isEmpty, sortBy, sum, uniq } from 'lodash';
|
||||
import { first, flatten, groupBy, isEmpty, sortBy, uniq } from 'lodash';
|
||||
import { APIReturnType } from '../../../../../../../services/rest/createCallApmApi';
|
||||
import { APMError } from '../../../../../../../../typings/es_schemas/ui/apm_error';
|
||||
import { Span } from '../../../../../../../../typings/es_schemas/ui/span';
|
||||
|
@ -35,10 +35,10 @@ export interface IWaterfall {
|
|||
duration: number;
|
||||
items: IWaterfallItem[];
|
||||
childrenByParentId: Record<string | number, IWaterfallSpanOrTransaction[]>;
|
||||
errorsPerTransaction: TraceAPIResponse['errorsPerTransaction'];
|
||||
errorsCount: number;
|
||||
getErrorCount: (parentId: string) => number;
|
||||
legends: IWaterfallLegend[];
|
||||
errorItems: IWaterfallError[];
|
||||
apiResponse: TraceAPIResponse;
|
||||
}
|
||||
|
||||
interface IWaterfallSpanItemBase<TDocument, TDoctype>
|
||||
|
@ -80,7 +80,8 @@ export type IWaterfallSpanOrTransaction =
|
|||
| IWaterfallTransaction
|
||||
| IWaterfallSpan;
|
||||
|
||||
export type IWaterfallItem = IWaterfallSpanOrTransaction | IWaterfallError;
|
||||
// export type IWaterfallItem = IWaterfallSpanOrTransaction | IWaterfallError;
|
||||
export type IWaterfallItem = IWaterfallSpanOrTransaction;
|
||||
|
||||
export interface IWaterfallLegend {
|
||||
type: WaterfallLegendType;
|
||||
|
@ -264,7 +265,7 @@ const getWaterfallDuration = (waterfallItems: IWaterfallItem[]) =>
|
|||
0
|
||||
);
|
||||
|
||||
const getWaterfallItems = (items: TraceAPIResponse['trace']['items']) =>
|
||||
const getWaterfallItems = (items: TraceAPIResponse['traceDocs']) =>
|
||||
items.map((item) => {
|
||||
const docType = item.processor.event;
|
||||
switch (docType) {
|
||||
|
@ -332,7 +333,7 @@ function isInEntryTransaction(
|
|||
}
|
||||
|
||||
function getWaterfallErrors(
|
||||
errorDocs: TraceAPIResponse['trace']['errorDocs'],
|
||||
errorDocs: TraceAPIResponse['errorDocs'],
|
||||
items: IWaterfallItem[],
|
||||
entryWaterfallTransaction?: IWaterfallTransaction
|
||||
) {
|
||||
|
@ -358,24 +359,44 @@ function getWaterfallErrors(
|
|||
);
|
||||
}
|
||||
|
||||
// map parent.id to the number of errors
|
||||
/*
|
||||
{ 'parentId': 2 }
|
||||
*/
|
||||
function getErrorCountByParentId(errorDocs: TraceAPIResponse['errorDocs']) {
|
||||
return errorDocs.reduce<Record<string, number>>((acc, doc) => {
|
||||
const parentId = doc.parent?.id;
|
||||
|
||||
if (!parentId) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
acc[parentId] = (acc[parentId] ?? 0) + 1;
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
export function getWaterfall(
|
||||
{ trace, errorsPerTransaction }: TraceAPIResponse,
|
||||
apiResponse: TraceAPIResponse,
|
||||
entryTransactionId?: Transaction['transaction']['id']
|
||||
): IWaterfall {
|
||||
if (isEmpty(trace.items) || !entryTransactionId) {
|
||||
if (isEmpty(apiResponse.traceDocs) || !entryTransactionId) {
|
||||
return {
|
||||
apiResponse,
|
||||
duration: 0,
|
||||
items: [],
|
||||
errorsPerTransaction,
|
||||
errorsCount: sum(Object.values(errorsPerTransaction)),
|
||||
legends: [],
|
||||
errorItems: [],
|
||||
childrenByParentId: {},
|
||||
getErrorCount: () => 0,
|
||||
};
|
||||
}
|
||||
|
||||
const errorCountByParentId = getErrorCountByParentId(apiResponse.errorDocs);
|
||||
|
||||
const waterfallItems: IWaterfallSpanOrTransaction[] = getWaterfallItems(
|
||||
trace.items
|
||||
apiResponse.traceDocs
|
||||
);
|
||||
|
||||
const childrenByParentId = getChildrenGroupedByParentId(
|
||||
|
@ -392,7 +413,7 @@ export function getWaterfall(
|
|||
entryWaterfallTransaction
|
||||
);
|
||||
const errorItems = getWaterfallErrors(
|
||||
trace.errorDocs,
|
||||
apiResponse.errorDocs,
|
||||
items,
|
||||
entryWaterfallTransaction
|
||||
);
|
||||
|
@ -402,14 +423,14 @@ export function getWaterfall(
|
|||
const legends = getLegends(items);
|
||||
|
||||
return {
|
||||
apiResponse,
|
||||
entryWaterfallTransaction,
|
||||
rootTransaction,
|
||||
duration,
|
||||
items,
|
||||
errorsPerTransaction,
|
||||
errorsCount: errorItems.length,
|
||||
legends,
|
||||
errorItems,
|
||||
childrenByParentId: getChildrenGroupedByParentId(items),
|
||||
getErrorCount: (parentId: string) => errorCountByParentId[parentId] ?? 0,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -5,18 +5,23 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiIcon, EuiText, EuiTitle, EuiToolTip } from '@elastic/eui';
|
||||
import { EuiBadge, EuiIcon, EuiText, EuiTitle, EuiToolTip } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { ReactNode } from 'react';
|
||||
import { useTheme } from '../../../../../../hooks/use_theme';
|
||||
import { euiStyled } from '../../../../../../../../../../src/plugins/kibana_react/common';
|
||||
import { isRumAgentName } from '../../../../../../../common/agent_name';
|
||||
import { TRACE_ID } from '../../../../../../../common/elasticsearch_fieldnames';
|
||||
import {
|
||||
TRACE_ID,
|
||||
TRANSACTION_ID,
|
||||
} from '../../../../../../../common/elasticsearch_fieldnames';
|
||||
import { asDuration } from '../../../../../../../common/utils/formatters';
|
||||
import { Margins } from '../../../../../shared/charts/Timeline';
|
||||
import { ErrorOverviewLink } from '../../../../../shared/Links/apm/ErrorOverviewLink';
|
||||
import { ErrorCount } from '../../ErrorCount';
|
||||
import { SyncBadge } from './sync_badge';
|
||||
import { IWaterfallSpanOrTransaction } from './waterfall_helpers/waterfall_helpers';
|
||||
import { FailureBadge } from './failure_badge';
|
||||
import { useApmRouter } from '../../../../../../hooks/use_apm_router';
|
||||
import { useApmParams } from '../../../../../../hooks/use_apm_params';
|
||||
|
||||
type ItemType = 'transaction' | 'span' | 'error';
|
||||
|
||||
|
@ -181,15 +186,6 @@ export function WaterfallItem({
|
|||
const width = (item.duration / totalDuration) * 100;
|
||||
const left = ((item.offset + item.skew) / totalDuration) * 100;
|
||||
|
||||
const tooltipContent = i18n.translate(
|
||||
'xpack.apm.transactionDetails.errorsOverviewLinkTooltip',
|
||||
{
|
||||
values: { errorCount },
|
||||
defaultMessage:
|
||||
'{errorCount, plural, one {View 1 related error} other {View # related errors}}',
|
||||
}
|
||||
);
|
||||
|
||||
const isCompositeSpan = item.docType === 'span' && item.doc.span.composite;
|
||||
const itemBarStyle = getItemBarStyle(item, color, width, left);
|
||||
|
||||
|
@ -216,27 +212,56 @@ export function WaterfallItem({
|
|||
</SpanActionToolTip>
|
||||
<HttpStatusCode item={item} />
|
||||
<NameLabel item={item} />
|
||||
{errorCount > 0 && item.docType === 'transaction' ? (
|
||||
<ErrorOverviewLink
|
||||
serviceName={item.doc.service.name}
|
||||
query={{
|
||||
kuery: `${TRACE_ID} : "${item.doc.trace.id}" and transaction.id : "${item.doc.transaction.id}"`,
|
||||
}}
|
||||
color="danger"
|
||||
style={{ textDecoration: 'none' }}
|
||||
>
|
||||
<EuiToolTip content={tooltipContent}>
|
||||
<ErrorCount count={errorCount} />
|
||||
</EuiToolTip>
|
||||
</ErrorOverviewLink>
|
||||
) : null}
|
||||
|
||||
<Duration item={item} />
|
||||
<RelatedErrors item={item} errorCount={errorCount} />
|
||||
{item.docType === 'span' && <SyncBadge sync={item.doc.span.sync} />}
|
||||
</ItemText>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
function RelatedErrors({
|
||||
item,
|
||||
errorCount,
|
||||
}: {
|
||||
item: IWaterfallSpanOrTransaction;
|
||||
errorCount: number;
|
||||
}) {
|
||||
const apmRouter = useApmRouter();
|
||||
const theme = useTheme();
|
||||
const { query } = useApmParams('/services/:serviceName/transactions/view');
|
||||
|
||||
const href = apmRouter.link(`/services/:serviceName/errors`, {
|
||||
path: { serviceName: item.doc.service.name },
|
||||
query: {
|
||||
...query,
|
||||
kuery: `${TRACE_ID} : "${item.doc.trace.id}" and ${TRANSACTION_ID} : "${item.doc.transaction?.id}"`,
|
||||
},
|
||||
});
|
||||
|
||||
if (errorCount > 0) {
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/click-events-have-key-events
|
||||
<div onClick={(e: React.MouseEvent) => e.stopPropagation()}>
|
||||
<EuiBadge
|
||||
href={href}
|
||||
color={theme.eui.euiColorDanger}
|
||||
iconType="arrowRight"
|
||||
>
|
||||
{i18n.translate('xpack.apm.waterfall.errorCount', {
|
||||
defaultMessage:
|
||||
'{errorCount, plural, one {View related error} other {View # related errors}}',
|
||||
values: { errorCount },
|
||||
})}
|
||||
</EuiBadge>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <FailureBadge outcome={item.doc.event?.outcome} />;
|
||||
}
|
||||
|
||||
function getItemBarStyle(
|
||||
item: IWaterfallSpanOrTransaction,
|
||||
color: string,
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
import React, { ComponentType } from 'react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { MockApmPluginContextWrapper } from '../../../../../context/apm_plugin/mock_apm_plugin_context';
|
||||
import { APIReturnType } from '../../../../../services/rest/createCallApmApi';
|
||||
import { WaterfallContainer } from './index';
|
||||
import { getWaterfall } from './Waterfall/waterfall_helpers/waterfall_helpers';
|
||||
import {
|
||||
|
@ -19,8 +18,6 @@ import {
|
|||
urlParams,
|
||||
} from './waterfallContainer.stories.data';
|
||||
|
||||
type TraceAPIResponse = APIReturnType<'GET /api/apm/traces/{traceId}'>;
|
||||
|
||||
export default {
|
||||
title: 'app/TransactionDetails/Waterfall',
|
||||
component: WaterfallContainer,
|
||||
|
@ -36,57 +33,24 @@ export default {
|
|||
};
|
||||
|
||||
export function Example() {
|
||||
const waterfall = getWaterfall(
|
||||
simpleTrace as TraceAPIResponse,
|
||||
'975c8d5bfd1dd20b'
|
||||
);
|
||||
return (
|
||||
<WaterfallContainer
|
||||
urlParams={urlParams}
|
||||
waterfall={waterfall}
|
||||
exceedsMax={false}
|
||||
/>
|
||||
);
|
||||
const waterfall = getWaterfall(simpleTrace, '975c8d5bfd1dd20b');
|
||||
return <WaterfallContainer urlParams={urlParams} waterfall={waterfall} />;
|
||||
}
|
||||
|
||||
export function WithErrors() {
|
||||
const waterfall = getWaterfall(
|
||||
(traceWithErrors as unknown) as TraceAPIResponse,
|
||||
'975c8d5bfd1dd20b'
|
||||
);
|
||||
return (
|
||||
<WaterfallContainer
|
||||
urlParams={urlParams}
|
||||
waterfall={waterfall}
|
||||
exceedsMax={false}
|
||||
/>
|
||||
);
|
||||
const waterfall = getWaterfall(traceWithErrors, '975c8d5bfd1dd20b');
|
||||
return <WaterfallContainer urlParams={urlParams} waterfall={waterfall} />;
|
||||
}
|
||||
|
||||
export function ChildStartsBeforeParent() {
|
||||
const waterfall = getWaterfall(
|
||||
traceChildStartBeforeParent as TraceAPIResponse,
|
||||
traceChildStartBeforeParent,
|
||||
'975c8d5bfd1dd20b'
|
||||
);
|
||||
return (
|
||||
<WaterfallContainer
|
||||
urlParams={urlParams}
|
||||
waterfall={waterfall}
|
||||
exceedsMax={false}
|
||||
/>
|
||||
);
|
||||
return <WaterfallContainer urlParams={urlParams} waterfall={waterfall} />;
|
||||
}
|
||||
|
||||
export function InferredSpans() {
|
||||
const waterfall = getWaterfall(
|
||||
inferredSpans as TraceAPIResponse,
|
||||
'f2387d37260d00bd'
|
||||
);
|
||||
return (
|
||||
<WaterfallContainer
|
||||
urlParams={urlParams}
|
||||
waterfall={waterfall}
|
||||
exceedsMax={false}
|
||||
/>
|
||||
);
|
||||
const waterfall = getWaterfall(inferredSpans, 'f2387d37260d00bd');
|
||||
return <WaterfallContainer urlParams={urlParams} waterfall={waterfall} />;
|
||||
}
|
||||
|
|
|
@ -19,14 +19,9 @@ import { useApmServiceContext } from '../../../../../context/apm_service/use_apm
|
|||
interface Props {
|
||||
urlParams: ApmUrlParams;
|
||||
waterfall: IWaterfall;
|
||||
exceedsMax: boolean;
|
||||
}
|
||||
|
||||
export function WaterfallContainer({
|
||||
urlParams,
|
||||
waterfall,
|
||||
exceedsMax,
|
||||
}: Props) {
|
||||
export function WaterfallContainer({ urlParams, waterfall }: Props) {
|
||||
const { serviceName } = useApmServiceContext();
|
||||
|
||||
if (!waterfall) {
|
||||
|
@ -83,7 +78,6 @@ export function WaterfallContainer({
|
|||
<Waterfall
|
||||
waterfallItemId={urlParams.waterfallItemId}
|
||||
waterfall={waterfall}
|
||||
exceedsMax={exceedsMax}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -9,7 +9,7 @@ import React from 'react';
|
|||
import { pickKeys } from '../../../../../common/utils/pick_keys';
|
||||
import { useUrlParams } from '../../../../context/url_params_context/use_url_params';
|
||||
import { APMQueryParams } from '../url_helpers';
|
||||
import { APMLink, APMLinkExtendProps, useAPMHref } from './APMLink';
|
||||
import { APMLink, APMLinkExtendProps } from './APMLink';
|
||||
|
||||
const persistedFilters: Array<keyof APMQueryParams> = [
|
||||
'host',
|
||||
|
@ -18,13 +18,6 @@ const persistedFilters: Array<keyof APMQueryParams> = [
|
|||
'serviceVersion',
|
||||
];
|
||||
|
||||
export function useErrorOverviewHref(serviceName: string) {
|
||||
return useAPMHref({
|
||||
path: `/services/${serviceName}/errors`,
|
||||
persistedFilters,
|
||||
});
|
||||
}
|
||||
|
||||
interface Props extends APMLinkExtendProps {
|
||||
serviceName: string;
|
||||
query?: APMQueryParams;
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
SERVICE,
|
||||
SPAN,
|
||||
LABELS,
|
||||
EVENT,
|
||||
TRANSACTION,
|
||||
TRACE,
|
||||
MESSAGE_SPAN,
|
||||
|
@ -20,6 +21,7 @@ export const SPAN_METADATA_SECTIONS: Section[] = [
|
|||
LABELS,
|
||||
TRACE,
|
||||
TRANSACTION,
|
||||
EVENT,
|
||||
SPAN,
|
||||
SERVICE,
|
||||
MESSAGE_SPAN,
|
||||
|
|
|
@ -9,6 +9,7 @@ import {
|
|||
Section,
|
||||
TRANSACTION,
|
||||
LABELS,
|
||||
EVENT,
|
||||
HTTP,
|
||||
HOST,
|
||||
CLIENT,
|
||||
|
@ -29,6 +30,7 @@ export const TRANSACTION_METADATA_SECTIONS: Section[] = [
|
|||
{ ...LABELS, required: true },
|
||||
TRACE,
|
||||
TRANSACTION,
|
||||
EVENT,
|
||||
HTTP,
|
||||
HOST,
|
||||
CLIENT,
|
||||
|
|
|
@ -21,6 +21,14 @@ export const LABELS: Section = {
|
|||
}),
|
||||
};
|
||||
|
||||
export const EVENT: Section = {
|
||||
key: 'event',
|
||||
label: i18n.translate('xpack.apm.metadataTable.section.eventLabel', {
|
||||
defaultMessage: 'event',
|
||||
}),
|
||||
properties: ['outcome'],
|
||||
};
|
||||
|
||||
export const HTTP: Section = {
|
||||
key: 'http',
|
||||
label: i18n.translate('xpack.apm.metadataTable.section.httpLabel', {
|
||||
|
|
|
@ -9,7 +9,6 @@ import { CoreSetup, CoreStart } from 'kibana/public';
|
|||
import * as t from 'io-ts';
|
||||
import type {
|
||||
ClientRequestParamsOf,
|
||||
EndpointOf,
|
||||
formatRequest as formatRequestType,
|
||||
ReturnOf,
|
||||
RouteRepositoryClient,
|
||||
|
@ -26,6 +25,7 @@ import { callApi } from './callApi';
|
|||
import type {
|
||||
APMServerRouteRepository,
|
||||
APMRouteHandlerResources,
|
||||
APIEndpoint,
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
} from '../../../server';
|
||||
import { InspectResponse } from '../../../typings/common';
|
||||
|
@ -47,16 +47,15 @@ export type AutoAbortedAPMClient = RouteRepositoryClient<
|
|||
Omit<APMClientOptions, 'signal'>
|
||||
>;
|
||||
|
||||
export type APIReturnType<
|
||||
TEndpoint extends EndpointOf<APMServerRouteRepository>
|
||||
> = ReturnOf<APMServerRouteRepository, TEndpoint> & {
|
||||
export type APIReturnType<TEndpoint extends APIEndpoint> = ReturnOf<
|
||||
APMServerRouteRepository,
|
||||
TEndpoint
|
||||
> & {
|
||||
_inspect?: InspectResponse;
|
||||
};
|
||||
|
||||
export type APIEndpoint = EndpointOf<APMServerRouteRepository>;
|
||||
|
||||
export type APIClientRequestParamsOf<
|
||||
TEndpoint extends EndpointOf<APMServerRouteRepository>
|
||||
TEndpoint extends APIEndpoint
|
||||
> = ClientRequestParamsOf<APMServerRouteRepository, TEndpoint>;
|
||||
|
||||
export type AbstractAPMRepository = ServerRouteRepository<
|
||||
|
|
|
@ -28,6 +28,14 @@ export async function createApmUsersAndRoles({
|
|||
kibana: Kibana;
|
||||
elasticsearch: Elasticsearch;
|
||||
}) {
|
||||
const isCredentialsValid = await getIsCredentialsValid({
|
||||
elasticsearch,
|
||||
kibana,
|
||||
});
|
||||
if (!isCredentialsValid) {
|
||||
throw new AbortError('Invalid username/password');
|
||||
}
|
||||
|
||||
const isSecurityEnabled = await getIsSecurityEnabled({
|
||||
elasticsearch,
|
||||
kibana,
|
||||
|
@ -86,3 +94,25 @@ async function getIsSecurityEnabled({
|
|||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function getIsCredentialsValid({
|
||||
elasticsearch,
|
||||
kibana,
|
||||
}: {
|
||||
elasticsearch: Elasticsearch;
|
||||
kibana: Kibana;
|
||||
}) {
|
||||
try {
|
||||
await callKibana({
|
||||
elasticsearch,
|
||||
kibana,
|
||||
options: {
|
||||
validateStatus: (status) => status >= 200 && status < 400,
|
||||
url: `/`,
|
||||
},
|
||||
});
|
||||
return true;
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -125,7 +125,10 @@ export const plugin = (initContext: PluginInitializerContext) =>
|
|||
export { APM_SERVER_FEATURE_ID } from '../common/alert_types';
|
||||
export { APMPlugin } from './plugin';
|
||||
export { APMPluginSetup } from './types';
|
||||
export { APMServerRouteRepository } from './routes/get_global_apm_server_route_repository';
|
||||
export {
|
||||
APMServerRouteRepository,
|
||||
APIEndpoint,
|
||||
} from './routes/get_global_apm_server_route_repository';
|
||||
export { APMRouteHandlerResources } from './routes/typings';
|
||||
|
||||
export type { ProcessorEvent } from '../common/processor_event';
|
||||
|
|
|
@ -8,15 +8,6 @@ Object {
|
|||
],
|
||||
},
|
||||
"body": Object {
|
||||
"aggs": Object {
|
||||
"by_transaction_id": Object {
|
||||
"terms": Object {
|
||||
"execution_hint": "map",
|
||||
"field": "transaction.id",
|
||||
"size": "myIndex",
|
||||
},
|
||||
},
|
||||
},
|
||||
"query": Object {
|
||||
"bool": Object {
|
||||
"filter": Array [
|
||||
|
|
|
@ -1,21 +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 { Setup, SetupTimeRange } from '../helpers/setup_request';
|
||||
import { getTraceItems } from './get_trace_items';
|
||||
|
||||
export async function getTrace(traceId: string, setup: Setup & SetupTimeRange) {
|
||||
const { errorsPerTransaction, ...trace } = await getTraceItems(
|
||||
traceId,
|
||||
setup
|
||||
);
|
||||
|
||||
return {
|
||||
trace,
|
||||
errorsPerTransaction,
|
||||
};
|
||||
}
|
|
@ -9,15 +9,13 @@ import { QueryDslQueryContainer } from '@elastic/elasticsearch/api/types';
|
|||
import { ProcessorEvent } from '../../../common/processor_event';
|
||||
import {
|
||||
TRACE_ID,
|
||||
PARENT_ID,
|
||||
TRANSACTION_DURATION,
|
||||
SPAN_DURATION,
|
||||
TRANSACTION_ID,
|
||||
PARENT_ID,
|
||||
ERROR_LOG_LEVEL,
|
||||
} from '../../../common/elasticsearch_fieldnames';
|
||||
import { rangeQuery } from '../../../../observability/server';
|
||||
import { Setup, SetupTimeRange } from '../helpers/setup_request';
|
||||
import { PromiseValueType } from '../../../typings/common';
|
||||
|
||||
export async function getTraceItems(
|
||||
traceId: string,
|
||||
|
@ -27,7 +25,7 @@ export async function getTraceItems(
|
|||
const maxTraceItems = config['xpack.apm.ui.maxTraceItems'];
|
||||
const excludedLogLevels = ['debug', 'info', 'warning'];
|
||||
|
||||
const errorResponsePromise = apmEventClient.search('get_trace_items', {
|
||||
const errorResponsePromise = apmEventClient.search('get_errors_docs', {
|
||||
apm: {
|
||||
events: [ProcessorEvent.error],
|
||||
},
|
||||
|
@ -42,20 +40,10 @@ export async function getTraceItems(
|
|||
must_not: { terms: { [ERROR_LOG_LEVEL]: excludedLogLevels } },
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
by_transaction_id: {
|
||||
terms: {
|
||||
field: TRANSACTION_ID,
|
||||
size: maxTraceItems,
|
||||
// high cardinality
|
||||
execution_hint: 'map' as const,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const traceResponsePromise = apmEventClient.search('get_trace_span_items', {
|
||||
const traceResponsePromise = apmEventClient.search('get_trace_docs', {
|
||||
apm: {
|
||||
events: [ProcessorEvent.span, ProcessorEvent.transaction],
|
||||
},
|
||||
|
@ -81,33 +69,18 @@ export async function getTraceItems(
|
|||
},
|
||||
});
|
||||
|
||||
const [errorResponse, traceResponse]: [
|
||||
// explicit intermediary types to avoid TS "excessively deep" error
|
||||
PromiseValueType<typeof errorResponsePromise>,
|
||||
PromiseValueType<typeof traceResponsePromise>
|
||||
] = (await Promise.all([errorResponsePromise, traceResponsePromise])) as any;
|
||||
const [errorResponse, traceResponse] = await Promise.all([
|
||||
errorResponsePromise,
|
||||
traceResponsePromise,
|
||||
]);
|
||||
|
||||
const exceedsMax = traceResponse.hits.total.value > maxTraceItems;
|
||||
|
||||
const items = traceResponse.hits.hits.map((hit) => hit._source);
|
||||
|
||||
const errorFrequencies = {
|
||||
errorDocs: errorResponse.hits.hits.map(({ _source }) => _source),
|
||||
errorsPerTransaction:
|
||||
errorResponse.aggregations?.by_transaction_id.buckets.reduce(
|
||||
(acc, current) => {
|
||||
return {
|
||||
...acc,
|
||||
[current.key]: current.doc_count,
|
||||
};
|
||||
},
|
||||
{} as Record<string, number>
|
||||
) ?? {},
|
||||
};
|
||||
const traceDocs = traceResponse.hits.hits.map((hit) => hit._source);
|
||||
const errorDocs = errorResponse.hits.hits.map((hit) => hit._source);
|
||||
|
||||
return {
|
||||
items,
|
||||
exceedsMax,
|
||||
...errorFrequencies,
|
||||
traceDocs,
|
||||
errorDocs,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -72,10 +72,10 @@ export type APMServerRouteRepository = ReturnType<
|
|||
// Ensure no APIs return arrays (or, by proxy, the any type),
|
||||
// to guarantee compatibility with _inspect.
|
||||
|
||||
type CompositeEndpoint = EndpointOf<APMServerRouteRepository>;
|
||||
export type APIEndpoint = EndpointOf<APMServerRouteRepository>;
|
||||
|
||||
type EndpointReturnTypes = {
|
||||
[Endpoint in CompositeEndpoint]: ReturnOf<APMServerRouteRepository, Endpoint>;
|
||||
[Endpoint in APIEndpoint]: ReturnOf<APMServerRouteRepository, Endpoint>;
|
||||
};
|
||||
|
||||
type ArrayLikeReturnTypes = PickByValue<EndpointReturnTypes, any[]>;
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import * as t from 'io-ts';
|
||||
import { setupRequest } from '../lib/helpers/setup_request';
|
||||
import { getTrace } from '../lib/traces/get_trace';
|
||||
import { getTraceItems } from '../lib/traces/get_trace_items';
|
||||
import { getTopTransactionGroupList } from '../lib/transaction_groups';
|
||||
import { createApmServerRoute } from './create_apm_server_route';
|
||||
import { environmentRt, kueryRt, rangeRt } from './default_api_types';
|
||||
|
@ -52,7 +52,7 @@ const tracesByIdRoute = createApmServerRoute({
|
|||
const { params } = resources;
|
||||
|
||||
const { traceId } = params.path;
|
||||
return getTrace(traceId, setup);
|
||||
return getTraceItems(traceId, setup);
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@ interface Processor {
|
|||
export interface SpanRaw extends APMBaseDoc {
|
||||
processor: Processor;
|
||||
trace: { id: string }; // trace is required
|
||||
event?: { outcome?: 'success' | 'failure' };
|
||||
service: {
|
||||
name: string;
|
||||
environment?: string;
|
||||
|
|
|
@ -28,6 +28,7 @@ export interface TransactionRaw extends APMBaseDoc {
|
|||
processor: Processor;
|
||||
timestamp: TimestampUs;
|
||||
trace: { id: string }; // trace is required
|
||||
event?: { outcome?: 'success' | 'failure' };
|
||||
transaction: {
|
||||
duration: { us: number };
|
||||
id: string;
|
||||
|
|
|
@ -7140,7 +7140,6 @@
|
|||
"xpack.apm.transactionDetails.distribution.panelTitle": "延迟分布",
|
||||
"xpack.apm.transactionDetails.emptySelectionText": "单击并拖动以选择范围",
|
||||
"xpack.apm.transactionDetails.errorCount": "{errorCount, number} 个 {errorCount, plural, other {错误}}",
|
||||
"xpack.apm.transactionDetails.errorsOverviewLinkTooltip": "{errorCount, plural, one {查看 1 个相关错误} other {查看 # 个相关错误}}",
|
||||
"xpack.apm.transactionDetails.noTraceParentButtonTooltip": "找不到上级追溯",
|
||||
"xpack.apm.transactionDetails.percentOfTraceLabelExplanation": "{parentType, select, transaction {事务} trace {追溯} }的百分比超过 100%,因为此{childType, select, span {跨度} transaction {事务} }比根事务花费更长的时间。",
|
||||
"xpack.apm.transactionDetails.requestMethodLabel": "请求方法",
|
||||
|
|
|
@ -11,11 +11,11 @@ import request from 'superagent';
|
|||
import { parseEndpoint } from '../../../plugins/apm/common/apm_api/parse_endpoint';
|
||||
import type {
|
||||
APIReturnType,
|
||||
APIEndpoint,
|
||||
APIClientRequestParamsOf,
|
||||
} from '../../../plugins/apm/public/services/rest/createCallApmApi';
|
||||
import type { APIEndpoint } from '../../../plugins/apm/server';
|
||||
|
||||
export function createSupertestClient(st: supertest.SuperTest<supertest.Test>) {
|
||||
export function createApmApiClient(st: supertest.SuperTest<supertest.Test>) {
|
||||
return async <TEndpoint extends APIEndpoint>(
|
||||
options: {
|
||||
endpoint: TEndpoint;
|
||||
|
@ -41,7 +41,7 @@ export function createSupertestClient(st: supertest.SuperTest<supertest.Test>) {
|
|||
};
|
||||
}
|
||||
|
||||
export type ApmApiSupertest = ReturnType<typeof createSupertestClient>;
|
||||
export type ApmApiSupertest = ReturnType<typeof createApmApiClient>;
|
||||
|
||||
export class ApmApiError extends Error {
|
||||
res: request.Response;
|
||||
|
|
|
@ -13,7 +13,7 @@ import { InheritedFtrProviderContext, InheritedServices } from './ftr_provider_c
|
|||
import { PromiseReturnType } from '../../../plugins/observability/typings/common';
|
||||
import { createApmUser, APM_TEST_PASSWORD, ApmUser } from './authentication';
|
||||
import { APMFtrConfigName } from '../configs';
|
||||
import { createSupertestClient } from './apm_api_supertest';
|
||||
import { createApmApiClient } from './apm_api_supertest';
|
||||
import { registry } from './registry';
|
||||
|
||||
interface Config {
|
||||
|
@ -52,7 +52,7 @@ async function getApmApiClient(
|
|||
auth: `${apmUser}:${APM_TEST_PASSWORD}`,
|
||||
});
|
||||
|
||||
return createSupertestClient(supertest(url));
|
||||
return createApmApiClient(supertest(url));
|
||||
}
|
||||
|
||||
export type CreateTestConfig = ReturnType<typeof createTestConfig>;
|
||||
|
|
|
@ -157,6 +157,9 @@ export default function apmApiIntegrationTests(providerContext: FtrProviderConte
|
|||
describe('traces/top_traces', function () {
|
||||
loadTestFile(require.resolve('./traces/top_traces'));
|
||||
});
|
||||
describe('/api/apm/traces/{traceId}', function () {
|
||||
loadTestFile(require.resolve('./traces/trace_by_id'));
|
||||
});
|
||||
|
||||
// transactions
|
||||
describe('transactions/breakdown', function () {
|
||||
|
|
|
@ -11,13 +11,13 @@ import archives from '../../common/fixtures/es_archiver/archives_metadata';
|
|||
import { registry } from '../../common/registry';
|
||||
import { APIReturnType } from '../../../../plugins/apm/public/services/rest/createCallApmApi';
|
||||
import { getServiceNodeIds } from './get_service_node_ids';
|
||||
import { createSupertestClient } from '../../common/apm_api_supertest';
|
||||
import { createApmApiClient } from '../../common/apm_api_supertest';
|
||||
|
||||
type ServiceOverviewInstanceDetails = APIReturnType<'GET /api/apm/services/{serviceName}/service_overview_instances/details/{serviceNodeName}'>;
|
||||
|
||||
export default function ApiTest({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('legacySupertestAsApmReadUser');
|
||||
const apmApiSupertest = createSupertestClient(supertest);
|
||||
const apmApiSupertest = createApmApiClient(supertest);
|
||||
|
||||
const archiveName = 'apm_8.0.0';
|
||||
const { start, end } = archives[archiveName];
|
||||
|
|
|
@ -14,12 +14,12 @@ import { APIReturnType } from '../../../../plugins/apm/public/services/rest/crea
|
|||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
||||
import archives from '../../common/fixtures/es_archiver/archives_metadata';
|
||||
import { registry } from '../../common/registry';
|
||||
import { createSupertestClient } from '../../common/apm_api_supertest';
|
||||
import { createApmApiClient } from '../../common/apm_api_supertest';
|
||||
import { getServiceNodeIds } from './get_service_node_ids';
|
||||
|
||||
export default function ApiTest({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('legacySupertestAsApmReadUser');
|
||||
const apmApiSupertest = createSupertestClient(supertest);
|
||||
const apmApiSupertest = createApmApiClient(supertest);
|
||||
|
||||
const archiveName = 'apm_8.0.0';
|
||||
const { start, end } = archives[archiveName];
|
||||
|
|
|
@ -12,14 +12,14 @@ import archives_metadata from '../../common/fixtures/es_archiver/archives_metada
|
|||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
||||
import { registry } from '../../common/registry';
|
||||
import { APIReturnType } from '../../../../plugins/apm/public/services/rest/createCallApmApi';
|
||||
import { createSupertestClient } from '../../common/apm_api_supertest';
|
||||
import { createApmApiClient } from '../../common/apm_api_supertest';
|
||||
import { getErrorGroupIds } from './get_error_group_ids';
|
||||
|
||||
type ErrorGroupsDetailedStatistics = APIReturnType<'GET /api/apm/services/{serviceName}/error_groups/detailed_statistics'>;
|
||||
|
||||
export default function ApiTest({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('legacySupertestAsApmReadUser');
|
||||
const apmApiSupertest = createSupertestClient(supertest);
|
||||
const apmApiSupertest = createApmApiClient(supertest);
|
||||
|
||||
const archiveName = 'apm_8.0.0';
|
||||
const metadata = archives_metadata[archiveName];
|
||||
|
|
92
x-pack/test/apm_api_integration/tests/traces/trace_by_id.tsx
Normal file
92
x-pack/test/apm_api_integration/tests/traces/trace_by_id.tsx
Normal file
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
import archives_metadata from '../../common/fixtures/es_archiver/archives_metadata';
|
||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
||||
import { registry } from '../../common/registry';
|
||||
import { createApmApiClient, SupertestReturnType } from '../../common/apm_api_supertest';
|
||||
|
||||
export default function ApiTest({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const apmApiSupertest = createApmApiClient(supertest);
|
||||
|
||||
const archiveName = 'apm_8.0.0';
|
||||
const metadata = archives_metadata[archiveName];
|
||||
const { start, end } = metadata;
|
||||
|
||||
registry.when('Trace does not exist', { config: 'basic', archives: [] }, () => {
|
||||
it('handles empty state', async () => {
|
||||
const response = await apmApiSupertest({
|
||||
endpoint: `GET /api/apm/traces/{traceId}`,
|
||||
params: {
|
||||
path: { traceId: 'foo' },
|
||||
query: { start, end },
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.status).to.be(200);
|
||||
expect(response.body).to.eql({ exceedsMax: false, traceDocs: [], errorDocs: [] });
|
||||
});
|
||||
});
|
||||
|
||||
registry.when('Trace exists', { config: 'basic', archives: [archiveName] }, () => {
|
||||
let response: SupertestReturnType<`GET /api/apm/traces/{traceId}`>;
|
||||
before(async () => {
|
||||
response = await apmApiSupertest({
|
||||
endpoint: `GET /api/apm/traces/{traceId}`,
|
||||
params: {
|
||||
path: { traceId: '64d0014f7530df24e549dd17cc0a8895' },
|
||||
query: { start, end },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('returns the correct status code', async () => {
|
||||
expect(response.status).to.be(200);
|
||||
});
|
||||
|
||||
it('returns the correct number of buckets', async () => {
|
||||
expectSnapshot(response.body.errorDocs.map((doc) => doc.error?.exception?.[0]?.message))
|
||||
.toMatchInline(`
|
||||
Array [
|
||||
"Test CaptureError",
|
||||
"Uncaught Error: Test Error in dashboard",
|
||||
]
|
||||
`);
|
||||
expectSnapshot(
|
||||
response.body.traceDocs.map((doc) =>
|
||||
doc.processor.event === 'transaction'
|
||||
? // @ts-expect-error
|
||||
`${doc.transaction.name} (transaction)`
|
||||
: // @ts-expect-error
|
||||
`${doc.span.name} (span)`
|
||||
)
|
||||
).toMatchInline(`
|
||||
Array [
|
||||
"/dashboard (transaction)",
|
||||
"GET /api/stats (transaction)",
|
||||
"APIRestController#topProducts (transaction)",
|
||||
"Parsing the document, executing sync. scripts (span)",
|
||||
"GET /api/products/top (span)",
|
||||
"GET /api/stats (span)",
|
||||
"Requesting and receiving the document (span)",
|
||||
"SELECT FROM customers (span)",
|
||||
"SELECT FROM order_lines (span)",
|
||||
"http://opbeans-frontend:3000/static/css/main.7bd7c5e8.css (span)",
|
||||
"SELECT FROM products (span)",
|
||||
"SELECT FROM orders (span)",
|
||||
"SELECT FROM order_lines (span)",
|
||||
"Making a connection to the server (span)",
|
||||
"Fire \\"load\\" event (span)",
|
||||
"empty query (span)",
|
||||
]
|
||||
`);
|
||||
expectSnapshot(response.body.exceedsMax).toMatchInline(`false`);
|
||||
});
|
||||
});
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue