[APM] Show badge for failed spans in waterfall (#109812)

Co-authored-by: Casper Hübertz <casper@formgeist.com>
This commit is contained in:
Søren Louv-Jansen 2021-09-08 23:37:28 +02:00 committed by GitHub
parent 5464af6923
commit d3f6303014
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
42 changed files with 2503 additions and 2549 deletions

View file

@ -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}
/>
</>

View file

@ -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 };
}

View file

@ -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();
});
});

View file

@ -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>
);
}

View file

@ -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 }) {

View file

@ -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}
/>
</>
);

View file

@ -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>

View file

@ -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>
);
}

View file

@ -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>

View file

@ -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={[
{

View file

@ -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>

View file

@ -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;

View file

@ -55,7 +55,7 @@ export function WaterfallFlyout({
rootTransactionDuration={
waterfall.rootTransaction?.transaction.duration.us
}
errorCount={waterfall.errorsPerTransaction[currentItem.id]}
errorCount={waterfall.getErrorCount(currentItem.id)}
/>
);
default:

View file

@ -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": "",

View file

@ -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);
});
});

View file

@ -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,
};
}

View file

@ -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,

View file

@ -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} />;
}

View file

@ -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>
);

View file

@ -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;

View file

@ -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,

View file

@ -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,

View file

@ -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', {

View file

@ -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<

View file

@ -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;
}
}

View file

@ -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';

View file

@ -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 [

View file

@ -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,
};
}

View file

@ -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,
};
}

View file

@ -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[]>;

View file

@ -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);
},
});

View file

@ -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;

View file

@ -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;

View file

@ -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": "请求方法",

View file

@ -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;

View file

@ -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>;

View file

@ -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 () {

View file

@ -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];

View file

@ -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];

View file

@ -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];

View 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`);
});
});
}