mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
parent
5bb2a71447
commit
672c588656
24 changed files with 800 additions and 145 deletions
|
@ -49,7 +49,7 @@ const scenario: Scenario<ApmFields> = async (runOptions: RunOptions) => {
|
|||
timestamp,
|
||||
children: (_) => {
|
||||
_.service({
|
||||
repeat: 10,
|
||||
repeat: 80,
|
||||
serviceInstance: synthNode,
|
||||
transactionName: 'GET /nodejs/products',
|
||||
latency: 100,
|
||||
|
@ -60,7 +60,7 @@ const scenario: Scenario<ApmFields> = async (runOptions: RunOptions) => {
|
|||
transactionName: 'GET /go',
|
||||
children: (_) => {
|
||||
_.service({
|
||||
repeat: 20,
|
||||
repeat: 50,
|
||||
serviceInstance: synthJava,
|
||||
transactionName: 'GET /java',
|
||||
children: (_) => {
|
||||
|
@ -83,7 +83,7 @@ const scenario: Scenario<ApmFields> = async (runOptions: RunOptions) => {
|
|||
serviceInstance: synthNode,
|
||||
transactionName: 'GET /nodejs/users',
|
||||
latency: 100,
|
||||
repeat: 10,
|
||||
repeat: 40,
|
||||
children: (_) => {
|
||||
_.service({
|
||||
serviceInstance: synthGo,
|
||||
|
@ -91,7 +91,7 @@ const scenario: Scenario<ApmFields> = async (runOptions: RunOptions) => {
|
|||
latency: 50,
|
||||
children: (_) => {
|
||||
_.service({
|
||||
repeat: 10,
|
||||
repeat: 40,
|
||||
serviceInstance: synthDotnet,
|
||||
transactionName: 'GET /dotnet/cases/4',
|
||||
latency: 50,
|
||||
|
|
|
@ -23,7 +23,7 @@ describe('getCriticalPath', () => {
|
|||
errorDocs: [],
|
||||
exceedsMax: false,
|
||||
spanLinksCountById: {},
|
||||
traceItemCount: events.length,
|
||||
traceDocsTotal: events.length,
|
||||
maxTraceItems: 5000,
|
||||
},
|
||||
entryTransaction,
|
||||
|
|
|
@ -0,0 +1,138 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-shadow */
|
||||
import { apm, timerange, DistributedTrace } from '@kbn/apm-synthtrace-client';
|
||||
import { synthtrace } from '../../../../synthtrace';
|
||||
|
||||
const RATE_PER_MINUTE = 1;
|
||||
|
||||
export function generateLargeTrace({
|
||||
start,
|
||||
end,
|
||||
rootTransactionName,
|
||||
repeaterFactor,
|
||||
environment,
|
||||
}: {
|
||||
start: number;
|
||||
end: number;
|
||||
rootTransactionName: string;
|
||||
repeaterFactor: number;
|
||||
environment: string;
|
||||
}) {
|
||||
const range = timerange(start, end);
|
||||
|
||||
const synthRum = apm
|
||||
.service({ name: 'synth-rum', environment, agentName: 'rum-js' })
|
||||
.instance('my-instance');
|
||||
|
||||
const synthNode = apm
|
||||
.service({ name: 'synth-node', environment, agentName: 'nodejs' })
|
||||
.instance('my-instance');
|
||||
|
||||
const synthGo = apm
|
||||
.service({ name: 'synth-go', environment, agentName: 'go' })
|
||||
.instance('my-instance');
|
||||
|
||||
const synthDotnet = apm
|
||||
.service({ name: 'synth-dotnet', environment, agentName: 'dotnet' })
|
||||
.instance('my-instance');
|
||||
|
||||
const synthJava = apm
|
||||
.service({ name: 'synth-java', environment, agentName: 'java' })
|
||||
.instance('my-instance');
|
||||
|
||||
const traces = range.ratePerMinute(RATE_PER_MINUTE).generator((timestamp) => {
|
||||
return new DistributedTrace({
|
||||
serviceInstance: synthRum,
|
||||
transactionName: rootTransactionName,
|
||||
timestamp,
|
||||
children: (_) => {
|
||||
_.service({
|
||||
repeat: 5 * repeaterFactor,
|
||||
serviceInstance: synthNode,
|
||||
transactionName: 'GET /nodejs/products',
|
||||
latency: 100,
|
||||
|
||||
children: (_) => {
|
||||
_.service({
|
||||
serviceInstance: synthGo,
|
||||
transactionName: 'GET /go',
|
||||
children: (_) => {
|
||||
_.service({
|
||||
repeat: 5 * repeaterFactor,
|
||||
serviceInstance: synthJava,
|
||||
transactionName: 'GET /java',
|
||||
children: (_) => {
|
||||
_.external({
|
||||
name: 'GET telemetry.elastic.co',
|
||||
url: 'https://telemetry.elastic.co/ping',
|
||||
duration: 50,
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
_.db({
|
||||
name: 'GET apm-*/_search',
|
||||
type: 'elasticsearch',
|
||||
duration: 400,
|
||||
});
|
||||
_.db({ name: 'GET', type: 'redis', duration: 500 });
|
||||
_.db({
|
||||
name: 'SELECT * FROM users',
|
||||
type: 'sqlite',
|
||||
duration: 600,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
_.service({
|
||||
serviceInstance: synthNode,
|
||||
transactionName: 'GET /nodejs/users',
|
||||
latency: 100,
|
||||
repeat: 5 * repeaterFactor,
|
||||
children: (_) => {
|
||||
_.service({
|
||||
serviceInstance: synthGo,
|
||||
transactionName: 'GET /go/security',
|
||||
latency: 50,
|
||||
children: (_) => {
|
||||
_.service({
|
||||
repeat: 5 * repeaterFactor,
|
||||
serviceInstance: synthDotnet,
|
||||
transactionName: 'GET /dotnet/cases/4',
|
||||
latency: 50,
|
||||
children: (_) =>
|
||||
_.db({
|
||||
name: 'GET apm-*/_search',
|
||||
type: 'elasticsearch',
|
||||
duration: 600,
|
||||
statement: JSON.stringify(
|
||||
{
|
||||
query: {
|
||||
query_string: {
|
||||
query: '(new york city) OR (big apple)',
|
||||
default_field: 'content',
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2
|
||||
),
|
||||
}),
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
}).getTransaction();
|
||||
});
|
||||
|
||||
return synthtrace.index(traces);
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* 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 { synthtrace } from '../../../../synthtrace';
|
||||
import { generateLargeTrace } from './generate_large_trace';
|
||||
|
||||
const start = '2021-10-10T00:00:00.000Z';
|
||||
const end = '2021-10-10T00:01:00.000Z';
|
||||
const rootTransactionName = `Large trace`;
|
||||
|
||||
const timeRange = { rangeFrom: start, rangeTo: end };
|
||||
|
||||
describe('Large Trace in waterfall', () => {
|
||||
before(() => {
|
||||
synthtrace.clean();
|
||||
|
||||
generateLargeTrace({
|
||||
start: new Date(start).getTime(),
|
||||
end: new Date(end).getTime(),
|
||||
rootTransactionName,
|
||||
repeaterFactor: 10,
|
||||
environment: 'large_trace',
|
||||
});
|
||||
});
|
||||
|
||||
after(() => {
|
||||
synthtrace.clean();
|
||||
});
|
||||
|
||||
describe('when navigating to a trace sample with default maxTraceItems', () => {
|
||||
beforeEach(() => {
|
||||
cy.loginAsViewerUser();
|
||||
cy.visitKibana(
|
||||
`/app/apm/services/synth-rum/transactions/view?${new URLSearchParams({
|
||||
...timeRange,
|
||||
transactionName: rootTransactionName,
|
||||
})}`
|
||||
);
|
||||
});
|
||||
|
||||
it('renders waterfall items', () => {
|
||||
cy.getByTestSubj('waterfallItem').should('have.length.greaterThan', 200);
|
||||
});
|
||||
|
||||
it('shows warning about trace size', () => {
|
||||
cy.getByTestSubj('apmWaterfallSizeWarning').should(
|
||||
'have.text',
|
||||
'The number of items in this trace is 15551 which is higher than the current limit of 5000. Please increase the limit via `xpack.apm.ui.maxTraceItems` to see the full trace'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when navigating to a trace sample with maxTraceItems=20000', () => {
|
||||
beforeEach(() => {
|
||||
cy.loginAsViewerUser();
|
||||
|
||||
cy.intercept('GET', '/internal/apm/traces/**', (req) => {
|
||||
req.query.maxTraceItems = 20000;
|
||||
}).as('getTraces');
|
||||
|
||||
cy.visitKibana(
|
||||
`/app/apm/services/synth-rum/transactions/view?${new URLSearchParams({
|
||||
...timeRange,
|
||||
transactionName: rootTransactionName,
|
||||
})}`
|
||||
);
|
||||
});
|
||||
|
||||
it('renders waterfall items', () => {
|
||||
cy.getByTestSubj('waterfallItem').should('have.length.greaterThan', 400);
|
||||
});
|
||||
|
||||
it('does not show the warning about trace size', () => {
|
||||
cy.getByTestSubj('apmWaterfallSizeWarning').should('not.exist');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -16,7 +16,7 @@ const INITIAL_DATA: APIReturnType<'GET /internal/apm/traces/{traceId}'> = {
|
|||
traceDocs: [],
|
||||
exceedsMax: false,
|
||||
spanLinksCountById: {},
|
||||
traceItemCount: 0,
|
||||
traceDocsTotal: 0,
|
||||
maxTraceItems: 0,
|
||||
},
|
||||
entryTransaction: undefined,
|
||||
|
|
|
@ -132,6 +132,7 @@ export function AccordionWaterfall(props: AccordionWaterfallProps) {
|
|||
|
||||
return (
|
||||
<StyledAccordion
|
||||
data-test-subj="waterfallItem"
|
||||
className="waterfall_accordion"
|
||||
style={{ position: 'relative' }}
|
||||
buttonClassName={`button_${item.id}`}
|
||||
|
|
|
@ -113,14 +113,15 @@ export function Waterfall({
|
|||
<Container>
|
||||
{waterfall.exceedsMax && (
|
||||
<EuiCallOut
|
||||
data-test-subj="apmWaterfallSizeWarning"
|
||||
color="warning"
|
||||
size="s"
|
||||
iconType="warning"
|
||||
title={i18n.translate('xpack.apm.waterfall.exceedsMax', {
|
||||
defaultMessage:
|
||||
'The number of items in this trace is {traceItemCount} which is higher than the current limit of {maxTraceItems}. Please increase the limit to see the full trace',
|
||||
'The number of items in this trace is {traceDocsTotal} which is higher than the current limit of {maxTraceItems}. Please increase the limit via `xpack.apm.ui.maxTraceItems` to see the full trace',
|
||||
values: {
|
||||
traceItemCount: waterfall.traceItemCount,
|
||||
traceDocsTotal: waterfall.traceDocsTotal,
|
||||
maxTraceItems: waterfall.maxTraceItems,
|
||||
},
|
||||
})}
|
||||
|
@ -161,9 +162,9 @@ export function Waterfall({
|
|||
) => toggleFlyout({ history, item, flyoutDetailTab })}
|
||||
showCriticalPath={showCriticalPath}
|
||||
maxLevelOpen={
|
||||
waterfall.traceItemCount > 500
|
||||
waterfall.traceDocsTotal > 500
|
||||
? maxLevelOpen
|
||||
: waterfall.traceItemCount
|
||||
: waterfall.traceDocsTotal
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -138,7 +138,7 @@ describe('waterfall_helpers', () => {
|
|||
errorDocs,
|
||||
exceedsMax: false,
|
||||
spanLinksCountById: {},
|
||||
traceItemCount: hits.length,
|
||||
traceDocsTotal: hits.length,
|
||||
maxTraceItems: 5000,
|
||||
},
|
||||
entryTransaction: {
|
||||
|
@ -168,7 +168,7 @@ describe('waterfall_helpers', () => {
|
|||
errorDocs,
|
||||
exceedsMax: false,
|
||||
spanLinksCountById: {},
|
||||
traceItemCount: hits.length,
|
||||
traceDocsTotal: hits.length,
|
||||
maxTraceItems: 5000,
|
||||
},
|
||||
entryTransaction: {
|
||||
|
@ -271,7 +271,7 @@ describe('waterfall_helpers', () => {
|
|||
errorDocs: [],
|
||||
exceedsMax: false,
|
||||
spanLinksCountById: {},
|
||||
traceItemCount: traceItems.length,
|
||||
traceDocsTotal: traceItems.length,
|
||||
maxTraceItems: 5000,
|
||||
},
|
||||
entryTransaction: {
|
||||
|
@ -390,7 +390,7 @@ describe('waterfall_helpers', () => {
|
|||
errorDocs: [],
|
||||
exceedsMax: false,
|
||||
spanLinksCountById: {},
|
||||
traceItemCount: traceItems.length,
|
||||
traceDocsTotal: traceItems.length,
|
||||
maxTraceItems: 5000,
|
||||
},
|
||||
entryTransaction: {
|
||||
|
|
|
@ -46,7 +46,7 @@ export interface IWaterfall {
|
|||
errorItems: IWaterfallError[];
|
||||
exceedsMax: boolean;
|
||||
totalErrorsCount: number;
|
||||
traceItemCount: number;
|
||||
traceDocsTotal: number;
|
||||
maxTraceItems: number;
|
||||
}
|
||||
|
||||
|
@ -427,7 +427,7 @@ export function getWaterfall(apiResponse: TraceAPIResponse): IWaterfall {
|
|||
getErrorCount: () => 0,
|
||||
exceedsMax: false,
|
||||
totalErrorsCount: 0,
|
||||
traceItemCount: 0,
|
||||
traceDocsTotal: 0,
|
||||
maxTraceItems: 0,
|
||||
};
|
||||
}
|
||||
|
@ -476,7 +476,7 @@ export function getWaterfall(apiResponse: TraceAPIResponse): IWaterfall {
|
|||
getErrorCount: (parentId: string) => errorCountByParentId[parentId] ?? 0,
|
||||
exceedsMax: traceItems.exceedsMax,
|
||||
totalErrorsCount: traceItems.errorDocs.length,
|
||||
traceItemCount: traceItems.traceItemCount,
|
||||
traceDocsTotal: traceItems.traceDocsTotal,
|
||||
maxTraceItems: traceItems.maxTraceItems,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -83,7 +83,7 @@ export const Example: Story<any> = () => {
|
|||
traceDocs,
|
||||
errorDocs: errorDocs.map((error) => dedot(error, {}) as WaterfallError),
|
||||
spanLinksCountById: {},
|
||||
traceItemCount: traceDocs.length,
|
||||
traceDocsTotal: traceDocs.length,
|
||||
maxTraceItems: 5000,
|
||||
};
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@ import { PluginSetupContract as AlertingPluginSetupContract } from '@kbn/alertin
|
|||
import { ObservabilityPluginSetup } from '@kbn/observability-plugin/server';
|
||||
import { DEFAULT_FLAPPING_SETTINGS } from '@kbn/alerting-plugin/common';
|
||||
import { APMConfig, APM_SERVER_FEATURE_ID } from '../../..';
|
||||
import { RegisterRuleDependencies } from '../register_apm_rule_types';
|
||||
|
||||
export const createRuleTypeMocks = () => {
|
||||
let alertExecutor: (...args: any[]) => Promise<any>;
|
||||
|
@ -56,28 +57,30 @@ export const createRuleTypeMocks = () => {
|
|||
shouldWriteAlerts: () => true,
|
||||
};
|
||||
|
||||
const dependencies = {
|
||||
alerting,
|
||||
basePath: {
|
||||
prepend: (path: string) => `http://localhost:5601/eyr${path}`,
|
||||
publicBaseUrl: 'http://localhost:5601/eyr',
|
||||
serverBasePath: '/eyr',
|
||||
} as IBasePath,
|
||||
config$: mockedConfig$,
|
||||
observability: {
|
||||
getAlertDetailsConfig: jest.fn().mockReturnValue({ apm: true }),
|
||||
} as unknown as ObservabilityPluginSetup,
|
||||
logger: loggerMock,
|
||||
ruleDataClient: ruleRegistryMocks.createRuleDataClient(
|
||||
'.alerts-observability.apm.alerts'
|
||||
) as IRuleDataClient,
|
||||
alertsLocator: {
|
||||
getLocation: jest.fn().mockImplementation(() => ({
|
||||
path: 'mockedAlertsLocator > getLocation',
|
||||
})),
|
||||
} as any as LocatorPublic<AlertsLocatorParams>,
|
||||
} as unknown as RegisterRuleDependencies;
|
||||
|
||||
return {
|
||||
dependencies: {
|
||||
alerting,
|
||||
basePath: {
|
||||
prepend: (path: string) => `http://localhost:5601/eyr${path}`,
|
||||
publicBaseUrl: 'http://localhost:5601/eyr',
|
||||
serverBasePath: '/eyr',
|
||||
} as IBasePath,
|
||||
config$: mockedConfig$,
|
||||
observability: {
|
||||
getAlertDetailsConfig: jest.fn().mockReturnValue({ apm: true }),
|
||||
} as unknown as ObservabilityPluginSetup,
|
||||
logger: loggerMock,
|
||||
ruleDataClient: ruleRegistryMocks.createRuleDataClient(
|
||||
'.alerts-observability.apm.alerts'
|
||||
) as IRuleDataClient,
|
||||
alertsLocator: {
|
||||
getLocation: jest.fn().mockImplementation(() => ({
|
||||
path: 'mockedAlertsLocator > getLocation',
|
||||
})),
|
||||
} as any as LocatorPublic<AlertsLocatorParams>,
|
||||
},
|
||||
dependencies,
|
||||
services,
|
||||
scheduleActions,
|
||||
executor: async ({ params }: { params: Record<string, any> }) => {
|
||||
|
|
|
@ -72,7 +72,13 @@ export async function getApmIndices({
|
|||
}
|
||||
|
||||
export type ApmIndexSettingsResponse = Array<{
|
||||
configurationName: 'transaction' | 'span' | 'error' | 'metric' | 'onboarding';
|
||||
configurationName:
|
||||
| 'transaction'
|
||||
| 'span'
|
||||
| 'error'
|
||||
| 'metric'
|
||||
| 'onboarding'
|
||||
| 'sourcemap';
|
||||
defaultValue: string; // value defined in kibana[.dev].yml
|
||||
savedValue: string | undefined;
|
||||
}>;
|
||||
|
|
|
@ -48,7 +48,7 @@ Object {
|
|||
},
|
||||
},
|
||||
},
|
||||
"size": 5000,
|
||||
"size": 1000,
|
||||
"track_total_hits": false,
|
||||
},
|
||||
}
|
||||
|
|
|
@ -5,12 +5,15 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { Logger } from '@kbn/logging';
|
||||
import { SortResults } from '@elastic/elasticsearch/lib/api/types';
|
||||
import {
|
||||
QueryDslQueryContainer,
|
||||
Sort,
|
||||
} from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import { ProcessorEvent } from '@kbn/observability-plugin/common';
|
||||
import { rangeQuery } from '@kbn/observability-plugin/server';
|
||||
import { last } from 'lodash';
|
||||
import { APMConfig } from '../..';
|
||||
import {
|
||||
AGENT_NAME,
|
||||
|
@ -58,18 +61,28 @@ export interface TraceItems {
|
|||
traceDocs: Array<WaterfallTransaction | WaterfallSpan>;
|
||||
errorDocs: WaterfallError[];
|
||||
spanLinksCountById: Record<string, number>;
|
||||
traceItemCount: number;
|
||||
traceDocsTotal: number;
|
||||
maxTraceItems: number;
|
||||
}
|
||||
|
||||
export async function getTraceItems(
|
||||
traceId: string,
|
||||
config: APMConfig,
|
||||
apmEventClient: APMEventClient,
|
||||
start: number,
|
||||
end: number
|
||||
): Promise<TraceItems> {
|
||||
const maxTraceItems = config.ui.maxTraceItems;
|
||||
export async function getTraceItems({
|
||||
traceId,
|
||||
config,
|
||||
apmEventClient,
|
||||
start,
|
||||
end,
|
||||
maxTraceItemsFromUrlParam,
|
||||
logger,
|
||||
}: {
|
||||
traceId: string;
|
||||
config: APMConfig;
|
||||
apmEventClient: APMEventClient;
|
||||
start: number;
|
||||
end: number;
|
||||
maxTraceItemsFromUrlParam?: number;
|
||||
logger: Logger;
|
||||
}): Promise<TraceItems> {
|
||||
const maxTraceItems = maxTraceItemsFromUrlParam ?? config.ui.maxTraceItems;
|
||||
const excludedLogLevels = ['debug', 'info', 'warning'];
|
||||
|
||||
const errorResponsePromise = apmEventClient.search('get_errors_docs', {
|
||||
|
@ -78,7 +91,7 @@ export async function getTraceItems(
|
|||
},
|
||||
body: {
|
||||
track_total_hits: false,
|
||||
size: maxTraceItems,
|
||||
size: 1000,
|
||||
_source: [
|
||||
TIMESTAMP,
|
||||
TRACE_ID,
|
||||
|
@ -102,58 +115,13 @@ export async function getTraceItems(
|
|||
},
|
||||
});
|
||||
|
||||
const traceResponsePromise = apmEventClient.search('get_trace_docs', {
|
||||
apm: {
|
||||
events: [ProcessorEvent.span, ProcessorEvent.transaction],
|
||||
},
|
||||
body: {
|
||||
track_total_hits: Math.max(10000, maxTraceItems + 1),
|
||||
size: maxTraceItems,
|
||||
_source: [
|
||||
TIMESTAMP,
|
||||
TRACE_ID,
|
||||
PARENT_ID,
|
||||
SERVICE_NAME,
|
||||
SERVICE_ENVIRONMENT,
|
||||
AGENT_NAME,
|
||||
EVENT_OUTCOME,
|
||||
PROCESSOR_EVENT,
|
||||
TRANSACTION_DURATION,
|
||||
TRANSACTION_ID,
|
||||
TRANSACTION_NAME,
|
||||
TRANSACTION_TYPE,
|
||||
TRANSACTION_RESULT,
|
||||
FAAS_COLDSTART,
|
||||
SPAN_ID,
|
||||
SPAN_TYPE,
|
||||
SPAN_SUBTYPE,
|
||||
SPAN_ACTION,
|
||||
SPAN_NAME,
|
||||
SPAN_DURATION,
|
||||
SPAN_LINKS,
|
||||
SPAN_COMPOSITE_COUNT,
|
||||
SPAN_COMPOSITE_COMPRESSION_STRATEGY,
|
||||
SPAN_COMPOSITE_SUM,
|
||||
SPAN_SYNC,
|
||||
CHILD_ID,
|
||||
],
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{ term: { [TRACE_ID]: traceId } },
|
||||
...rangeQuery(start, end),
|
||||
] as QueryDslQueryContainer[],
|
||||
should: {
|
||||
exists: { field: PARENT_ID },
|
||||
},
|
||||
},
|
||||
},
|
||||
sort: [
|
||||
{ _score: { order: 'asc' as const } },
|
||||
{ [TRANSACTION_DURATION]: { order: 'desc' as const } },
|
||||
{ [SPAN_DURATION]: { order: 'desc' as const } },
|
||||
] as Sort,
|
||||
},
|
||||
const traceResponsePromise = getTraceDocsPaginated({
|
||||
apmEventClient,
|
||||
maxTraceItems,
|
||||
traceId,
|
||||
start,
|
||||
end,
|
||||
logger,
|
||||
});
|
||||
|
||||
const [errorResponse, traceResponse, spanLinksCountById] = await Promise.all([
|
||||
|
@ -162,9 +130,9 @@ export async function getTraceItems(
|
|||
getSpanLinksCountById({ traceId, apmEventClient, start, end }),
|
||||
]);
|
||||
|
||||
const traceItemCount = traceResponse.hits.total.value;
|
||||
const exceedsMax = traceItemCount > maxTraceItems;
|
||||
const traceDocs = traceResponse.hits.hits.map((hit) => hit._source);
|
||||
const traceDocsTotal = traceResponse.total;
|
||||
const exceedsMax = traceDocsTotal > maxTraceItems;
|
||||
const traceDocs = traceResponse.hits.map((hit) => hit._source);
|
||||
const errorDocs = errorResponse.hits.hits.map((hit) => hit._source);
|
||||
|
||||
return {
|
||||
|
@ -172,7 +140,157 @@ export async function getTraceItems(
|
|||
traceDocs,
|
||||
errorDocs,
|
||||
spanLinksCountById,
|
||||
traceItemCount,
|
||||
traceDocsTotal,
|
||||
maxTraceItems,
|
||||
};
|
||||
}
|
||||
|
||||
const MAX_ITEMS_PER_PAGE = 10000; // 10000 is the max allowed by ES
|
||||
|
||||
async function getTraceDocsPaginated({
|
||||
apmEventClient,
|
||||
maxTraceItems,
|
||||
traceId,
|
||||
start,
|
||||
end,
|
||||
hits = [],
|
||||
searchAfter,
|
||||
logger,
|
||||
}: {
|
||||
apmEventClient: APMEventClient;
|
||||
maxTraceItems: number;
|
||||
traceId: string;
|
||||
start: number;
|
||||
end: number;
|
||||
logger: Logger;
|
||||
hits?: Awaited<ReturnType<typeof getTraceDocsPerPage>>['hits'];
|
||||
searchAfter?: SortResults;
|
||||
}): ReturnType<typeof getTraceDocsPerPage> {
|
||||
const response = await getTraceDocsPerPage({
|
||||
apmEventClient,
|
||||
maxTraceItems,
|
||||
traceId,
|
||||
start,
|
||||
end,
|
||||
searchAfter,
|
||||
});
|
||||
|
||||
const mergedHits = [...hits, ...response.hits];
|
||||
|
||||
logger.debug(
|
||||
`Paginating traces: retrieved: ${response.hits.length}, (total: ${mergedHits.length} of ${response.total}), maxTraceItems: ${maxTraceItems}`
|
||||
);
|
||||
|
||||
if (
|
||||
mergedHits.length >= maxTraceItems ||
|
||||
mergedHits.length >= response.total ||
|
||||
mergedHits.length === 0 ||
|
||||
response.hits.length < MAX_ITEMS_PER_PAGE
|
||||
) {
|
||||
return {
|
||||
hits: mergedHits,
|
||||
total: response.total,
|
||||
};
|
||||
}
|
||||
|
||||
return getTraceDocsPaginated({
|
||||
apmEventClient,
|
||||
maxTraceItems,
|
||||
traceId,
|
||||
start,
|
||||
end,
|
||||
hits: mergedHits,
|
||||
searchAfter: last(response.hits)?.sort,
|
||||
logger,
|
||||
});
|
||||
}
|
||||
|
||||
async function getTraceDocsPerPage({
|
||||
apmEventClient,
|
||||
maxTraceItems,
|
||||
traceId,
|
||||
start,
|
||||
end,
|
||||
searchAfter,
|
||||
}: {
|
||||
apmEventClient: APMEventClient;
|
||||
maxTraceItems: number;
|
||||
traceId: string;
|
||||
start: number;
|
||||
end: number;
|
||||
searchAfter?: SortResults;
|
||||
}) {
|
||||
const size = Math.min(maxTraceItems, MAX_ITEMS_PER_PAGE);
|
||||
|
||||
const body = {
|
||||
track_total_hits: true,
|
||||
size,
|
||||
search_after: searchAfter,
|
||||
_source: [
|
||||
TIMESTAMP,
|
||||
TRACE_ID,
|
||||
PARENT_ID,
|
||||
SERVICE_NAME,
|
||||
SERVICE_ENVIRONMENT,
|
||||
AGENT_NAME,
|
||||
EVENT_OUTCOME,
|
||||
PROCESSOR_EVENT,
|
||||
TRANSACTION_DURATION,
|
||||
TRANSACTION_ID,
|
||||
TRANSACTION_NAME,
|
||||
TRANSACTION_TYPE,
|
||||
TRANSACTION_RESULT,
|
||||
FAAS_COLDSTART,
|
||||
SPAN_ID,
|
||||
SPAN_TYPE,
|
||||
SPAN_SUBTYPE,
|
||||
SPAN_ACTION,
|
||||
SPAN_NAME,
|
||||
SPAN_DURATION,
|
||||
SPAN_LINKS,
|
||||
SPAN_COMPOSITE_COUNT,
|
||||
SPAN_COMPOSITE_COMPRESSION_STRATEGY,
|
||||
SPAN_COMPOSITE_SUM,
|
||||
SPAN_SYNC,
|
||||
CHILD_ID,
|
||||
],
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{ term: { [TRACE_ID]: traceId } },
|
||||
...rangeQuery(start, end),
|
||||
] as QueryDslQueryContainer[],
|
||||
should: {
|
||||
exists: { field: PARENT_ID },
|
||||
},
|
||||
},
|
||||
},
|
||||
sort: [
|
||||
{ _score: 'asc' },
|
||||
{
|
||||
_script: {
|
||||
type: 'number',
|
||||
script: {
|
||||
lang: 'painless',
|
||||
source: `if (doc['${TRANSACTION_DURATION}'].size() > 0) { return doc['${TRANSACTION_DURATION}'].value } else { return doc['${SPAN_DURATION}'].value }`,
|
||||
},
|
||||
order: 'desc',
|
||||
},
|
||||
},
|
||||
{ '@timestamp': 'asc' },
|
||||
{ _doc: 'asc' },
|
||||
] as Sort,
|
||||
};
|
||||
|
||||
const res = await apmEventClient.search('get_trace_docs', {
|
||||
apm: {
|
||||
events: [ProcessorEvent.span, ProcessorEvent.transaction],
|
||||
},
|
||||
body,
|
||||
});
|
||||
|
||||
return {
|
||||
hits: res.hits.hits,
|
||||
total: res.hits.total.value,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -20,7 +20,13 @@ describe('trace queries', () => {
|
|||
|
||||
it('fetches a trace', async () => {
|
||||
mock = await inspectSearchParams(({ mockConfig, mockApmEventClient }) =>
|
||||
getTraceItems('foo', mockConfig, mockApmEventClient, 0, 50000)
|
||||
getTraceItems({
|
||||
traceId: 'foo',
|
||||
config: mockConfig,
|
||||
apmEventClient: mockApmEventClient,
|
||||
start: 0,
|
||||
end: 50000,
|
||||
})
|
||||
);
|
||||
|
||||
expect(mock.params).toMatchSnapshot();
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import * as t from 'io-ts';
|
||||
import { nonEmptyStringRt } from '@kbn/io-ts-utils';
|
||||
import { nonEmptyStringRt, toNumberRt } from '@kbn/io-ts-utils';
|
||||
import { TraceSearchType } from '../../../common/trace_explorer';
|
||||
import { getSearchTransactionsEvents } from '../../lib/helpers/transactions';
|
||||
import { createApmServerRoute } from '../apm_routes/create_apm_server_route';
|
||||
|
@ -84,7 +84,11 @@ const tracesByIdRoute = createApmServerRoute({
|
|||
path: t.type({
|
||||
traceId: t.string,
|
||||
}),
|
||||
query: t.intersection([rangeRt, t.type({ entryTransactionId: t.string })]),
|
||||
query: t.intersection([
|
||||
rangeRt,
|
||||
t.type({ entryTransactionId: t.string }),
|
||||
t.partial({ maxTraceItems: toNumberRt }),
|
||||
]),
|
||||
}),
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async (
|
||||
|
@ -94,11 +98,19 @@ const tracesByIdRoute = createApmServerRoute({
|
|||
entryTransaction?: Transaction;
|
||||
}> => {
|
||||
const apmEventClient = await getApmEventClient(resources);
|
||||
const { params, config } = resources;
|
||||
const { params, config, logger } = resources;
|
||||
const { traceId } = params.path;
|
||||
const { start, end, entryTransactionId } = params.query;
|
||||
const [traceItems, entryTransaction] = await Promise.all([
|
||||
getTraceItems(traceId, config, apmEventClient, start, end),
|
||||
getTraceItems({
|
||||
traceId,
|
||||
config,
|
||||
apmEventClient,
|
||||
start,
|
||||
end,
|
||||
maxTraceItemsFromUrlParam: params.query.maxTraceItems,
|
||||
logger,
|
||||
}),
|
||||
getTransaction({
|
||||
transactionId: entryTransactionId,
|
||||
traceId,
|
||||
|
|
|
@ -17,6 +17,7 @@ export interface APMIndices {
|
|||
span?: string;
|
||||
transaction?: string;
|
||||
metric?: string;
|
||||
sourcemap?: string;
|
||||
};
|
||||
isSpaceAware?: boolean;
|
||||
}
|
||||
|
|
|
@ -60,6 +60,7 @@ export async function inspectSearchParams(
|
|||
span: 'myIndex',
|
||||
transaction: 'myIndex',
|
||||
metric: 'myIndex',
|
||||
sourcemap: 'myIndex',
|
||||
};
|
||||
const mockConfig = new Proxy(
|
||||
{},
|
||||
|
|
|
@ -7620,7 +7620,7 @@
|
|||
"xpack.apm.tutorial.windowsServerInstructions.textPre": "1. Téléchargez le fichier .zip APM Server pour Windows via la [page de téléchargement]({downloadPageLink}).\n2. Extrayez le contenu du fichier compressé (ZIP) dans {zipFileExtractFolder}.\n3. Renommez le répertoire {apmServerDirectory} en \"APM-Server\".\n4. Ouvrez une invite PowerShell en tant qu'administrateur (faites un clic droit sur l'icône PowerShell et sélectionnez **Exécuter en tant qu'administrateur**). Si vous exécutez Windows XP, vous devrez peut-être télécharger et installer PowerShell.\n5. Dans l'invite PowerShell, exécutez les commandes suivantes pour installer le serveur APM en tant que service Windows :",
|
||||
"xpack.apm.unifiedSearchBar.placeholder": "Rechercher {event, select, transaction {transactions} metric {indicateurs} error {erreurs} other {transactions, erreurs et indicateurs}} (par exemple {queryExample})",
|
||||
"xpack.apm.waterfall.errorCount": "{errorCount, plural, one {Voir l'erreur associée} many {Voir les # erreurs associées} other {Voir les # erreurs associées}}",
|
||||
"xpack.apm.waterfall.exceedsMax": "Le nombre d'éléments dans cette trace est de {traceItemCount}, ce qui est supérieur à la limite actuelle de {maxTraceItems}. Veuillez augmenter la limite pour afficher la trace complète.",
|
||||
"xpack.apm.waterfall.exceedsMax": "Le nombre d'éléments dans cette trace est de {traceDocsTotal}, ce qui est supérieur à la limite actuelle de {maxTraceItems}. Veuillez augmenter la limite pour afficher la trace complète.",
|
||||
"xpack.apm.waterfall.spanLinks.badge": "{total} {total, plural, one {Lien d'intervalle} many {Liens d'intervalle} other {Liens d'intervalle}}",
|
||||
"xpack.apm.waterfall.spanLinks.tooltip.linkedChildren": "{linkedChildren} entrants",
|
||||
"xpack.apm.waterfall.spanLinks.tooltip.linkedParents": "{linkedParents} sortants",
|
||||
|
|
|
@ -7636,7 +7636,7 @@
|
|||
"xpack.apm.tutorial.windowsServerInstructions.textPre": "1.[ダウンロードページ]({downloadPageLink})からAPM Server Windows zipファイルをダウンロードします。\n2.zipファイルのコンテンツを{zipFileExtractFolder}に解凍します。\n3.「{apmServerDirectory}」ディレクトリの名前を「APM-Server」に変更します。\n4.管理者としてPowerShellプロンプトを開きます(PowerShellアイコンを右クリックして「管理者として実行」を選択します)。Windows XPをご使用の場合、PowerShellのダウンロードとインストールが必要な場合があります。\n5.PowerShellプロンプトで次のコマンドを実行し、APM ServerをWindowsサービスとしてインストールします。",
|
||||
"xpack.apm.unifiedSearchBar.placeholder": "{event, select, transaction {トランザクション} metric {メトリック} error {エラー} other {トランザクション、エラー、およびメトリック}}を検索(例:{queryExample})",
|
||||
"xpack.apm.waterfall.errorCount": "{errorCount, plural, other {#件の関連エラーを表示}}",
|
||||
"xpack.apm.waterfall.exceedsMax": "このトレースの項目数は{traceItemCount}であり、現在の制限値{maxTraceItems}より大きくなっています。トレース全体を表示するには、制限を大きくしてください",
|
||||
"xpack.apm.waterfall.exceedsMax": "このトレースの項目数は{traceDocsTotal}であり、現在の制限値{maxTraceItems}より大きくなっています。トレース全体を表示するには、制限を大きくしてください",
|
||||
"xpack.apm.waterfall.spanLinks.badge": "{total} {total, plural, other {スパンリンク}}",
|
||||
"xpack.apm.waterfall.spanLinks.tooltip.linkedChildren": "{linkedChildren}受信",
|
||||
"xpack.apm.waterfall.spanLinks.tooltip.linkedParents": "{linkedParents}送信",
|
||||
|
|
|
@ -7635,7 +7635,7 @@
|
|||
"xpack.apm.tutorial.windowsServerInstructions.textPre": "1.从[下载页面]({downloadPageLink}) 下载 APM Server Windows zip 文件。\n2.将 zip 文件的内容解压缩到 {zipFileExtractFolder}。\n3.将 {apmServerDirectory} 目录重命名为 `APM-Server`。\n4.以管理员身份打开 PowerShell 提示符(右键单击 PowerShell 图标,然后选择**以管理员身份运行**)。如果运行的是 Windows XP,则可能需要下载并安装 PowerShell。\n5.从 PowerShell 提示符处,运行以下命令以将 APM Server 安装为 Windows 服务:",
|
||||
"xpack.apm.unifiedSearchBar.placeholder": "搜索{event, select, transaction {事务} metric {指标} error {错误} other {事务、错误和指标}}(例如 {queryExample})",
|
||||
"xpack.apm.waterfall.errorCount": "{errorCount, plural, other {查看 # 个相关错误}}",
|
||||
"xpack.apm.waterfall.exceedsMax": "此跟踪中的项目数为 {traceItemCount},这高于 {maxTraceItems} 的当前限值。请增加该限值以查看完整追溯信息",
|
||||
"xpack.apm.waterfall.exceedsMax": "此跟踪中的项目数为 {traceDocsTotal},这高于 {maxTraceItems} 的当前限值。请增加该限值以查看完整追溯信息",
|
||||
"xpack.apm.waterfall.spanLinks.badge": "{total} {total, plural, other {跨度链接}}",
|
||||
"xpack.apm.waterfall.spanLinks.tooltip.linkedChildren": "{linkedChildren} 传入",
|
||||
"xpack.apm.waterfall.spanLinks.tooltip.linkedParents": "{linkedParents} 传出",
|
||||
|
|
|
@ -0,0 +1,141 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-shadow */
|
||||
|
||||
import { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace';
|
||||
import { apm, timerange, DistributedTrace } from '@kbn/apm-synthtrace-client';
|
||||
|
||||
const RATE_PER_MINUTE = 1;
|
||||
|
||||
export function generateLargeTrace({
|
||||
start,
|
||||
end,
|
||||
rootTransactionName,
|
||||
synthtraceEsClient,
|
||||
repeaterFactor,
|
||||
environment,
|
||||
}: {
|
||||
start: number;
|
||||
end: number;
|
||||
rootTransactionName: string;
|
||||
synthtraceEsClient: ApmSynthtraceEsClient;
|
||||
repeaterFactor: number;
|
||||
environment: string;
|
||||
}) {
|
||||
const range = timerange(start, end);
|
||||
|
||||
const synthRum = apm
|
||||
.service({ name: 'synth-rum', environment, agentName: 'rum-js' })
|
||||
.instance('my-instance');
|
||||
|
||||
const synthNode = apm
|
||||
.service({ name: 'synth-node', environment, agentName: 'nodejs' })
|
||||
.instance('my-instance');
|
||||
|
||||
const synthGo = apm
|
||||
.service({ name: 'synth-go', environment, agentName: 'go' })
|
||||
.instance('my-instance');
|
||||
|
||||
const synthDotnet = apm
|
||||
.service({ name: 'synth-dotnet', environment, agentName: 'dotnet' })
|
||||
.instance('my-instance');
|
||||
|
||||
const synthJava = apm
|
||||
.service({ name: 'synth-java', environment, agentName: 'java' })
|
||||
.instance('my-instance');
|
||||
|
||||
const traces = range.ratePerMinute(RATE_PER_MINUTE).generator((timestamp) => {
|
||||
return new DistributedTrace({
|
||||
serviceInstance: synthRum,
|
||||
transactionName: rootTransactionName,
|
||||
timestamp,
|
||||
children: (_) => {
|
||||
_.service({
|
||||
repeat: 5 * repeaterFactor,
|
||||
serviceInstance: synthNode,
|
||||
transactionName: 'GET /nodejs/products',
|
||||
latency: 100,
|
||||
|
||||
children: (_) => {
|
||||
_.service({
|
||||
serviceInstance: synthGo,
|
||||
transactionName: 'GET /go',
|
||||
children: (_) => {
|
||||
_.service({
|
||||
repeat: 5 * repeaterFactor,
|
||||
serviceInstance: synthJava,
|
||||
transactionName: 'GET /java',
|
||||
children: (_) => {
|
||||
_.external({
|
||||
name: 'GET telemetry.elastic.co',
|
||||
url: 'https://telemetry.elastic.co/ping',
|
||||
duration: 50,
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
_.db({
|
||||
name: 'GET apm-*/_search',
|
||||
type: 'elasticsearch',
|
||||
duration: 400,
|
||||
});
|
||||
_.db({ name: 'GET', type: 'redis', duration: 500 });
|
||||
_.db({
|
||||
name: 'SELECT * FROM users',
|
||||
type: 'sqlite',
|
||||
duration: 600,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
_.service({
|
||||
serviceInstance: synthNode,
|
||||
transactionName: 'GET /nodejs/users',
|
||||
latency: 100,
|
||||
repeat: 5 * repeaterFactor,
|
||||
children: (_) => {
|
||||
_.service({
|
||||
serviceInstance: synthGo,
|
||||
transactionName: 'GET /go/security',
|
||||
latency: 50,
|
||||
children: (_) => {
|
||||
_.service({
|
||||
repeat: 5 * repeaterFactor,
|
||||
serviceInstance: synthDotnet,
|
||||
transactionName: 'GET /dotnet/cases/4',
|
||||
latency: 50,
|
||||
children: (_) =>
|
||||
_.db({
|
||||
name: 'GET apm-*/_search',
|
||||
type: 'elasticsearch',
|
||||
duration: 600,
|
||||
statement: JSON.stringify(
|
||||
{
|
||||
query: {
|
||||
query_string: {
|
||||
query: '(new york city) OR (big apple)',
|
||||
default_field: 'content',
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2
|
||||
),
|
||||
}),
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
}).getTransaction();
|
||||
});
|
||||
|
||||
return synthtraceEsClient.index(traces);
|
||||
}
|
|
@ -0,0 +1,153 @@
|
|||
/*
|
||||
* 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 {
|
||||
PROCESSOR_EVENT,
|
||||
TRACE_ID,
|
||||
SERVICE_ENVIRONMENT,
|
||||
TRANSACTION_ID,
|
||||
PARENT_ID,
|
||||
} from '@kbn/apm-plugin/common/es_fields/apm';
|
||||
import type { Client } from '@elastic/elasticsearch';
|
||||
import type { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api';
|
||||
import expect from '@kbn/expect';
|
||||
import { ApmApiClient } from '../../../common/config';
|
||||
import { FtrProviderContext } from '../../../common/ftr_provider_context';
|
||||
import { generateLargeTrace } from './generate_large_trace';
|
||||
|
||||
const start = new Date('2023-01-01T00:00:00.000Z').getTime();
|
||||
const end = new Date('2023-01-01T00:01:00.000Z').getTime() - 1;
|
||||
const rootTransactionName = 'Long trace';
|
||||
const environment = 'long_trace_scenario';
|
||||
|
||||
export default function ApiTest({ getService }: FtrProviderContext) {
|
||||
const registry = getService('registry');
|
||||
const apmApiClient = getService('apmApiClient');
|
||||
const synthtraceEsClient = getService('synthtraceEsClient');
|
||||
const es = getService('es');
|
||||
|
||||
registry.when('Large trace', { config: 'basic', archives: [] }, () => {
|
||||
describe('when the trace is large (>15.000 items)', () => {
|
||||
before(async () => {
|
||||
await synthtraceEsClient.clean();
|
||||
await generateLargeTrace({
|
||||
start,
|
||||
end,
|
||||
rootTransactionName,
|
||||
synthtraceEsClient,
|
||||
repeaterFactor: 10,
|
||||
environment,
|
||||
});
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await synthtraceEsClient.clean();
|
||||
});
|
||||
|
||||
describe('when maxTraceItems is 5000 (default)', () => {
|
||||
let trace: APIReturnType<'GET /internal/apm/traces/{traceId}'>;
|
||||
before(async () => {
|
||||
trace = await getTrace({ es, apmApiClient, maxTraceItems: 5000 });
|
||||
});
|
||||
|
||||
it('and traceDocsTotal is correct', () => {
|
||||
expect(trace.traceItems.traceDocsTotal).to.be(15551);
|
||||
});
|
||||
|
||||
it('and traceDocs is correct', () => {
|
||||
expect(trace.traceItems.traceDocs.length).to.be(5000);
|
||||
});
|
||||
|
||||
it('and maxTraceItems is correct', () => {
|
||||
expect(trace.traceItems.maxTraceItems).to.be(5000);
|
||||
});
|
||||
|
||||
it('and exceedsMax is correct', () => {
|
||||
expect(trace.traceItems.exceedsMax).to.be(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when maxTraceItems is 20000', () => {
|
||||
let trace: APIReturnType<'GET /internal/apm/traces/{traceId}'>;
|
||||
before(async () => {
|
||||
trace = await getTrace({ es, apmApiClient, maxTraceItems: 20000 });
|
||||
});
|
||||
|
||||
it('and traceDocsTotal is correct', () => {
|
||||
expect(trace.traceItems.traceDocsTotal).to.be(15551);
|
||||
});
|
||||
|
||||
it('and traceDocs is correct', () => {
|
||||
expect(trace.traceItems.traceDocs.length).to.be(15551);
|
||||
});
|
||||
|
||||
it('and maxTraceItems is correct', () => {
|
||||
expect(trace.traceItems.maxTraceItems).to.be(20000);
|
||||
});
|
||||
|
||||
it('and exceedsMax is correct', () => {
|
||||
expect(trace.traceItems.exceedsMax).to.be(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function getRootTransaction(es: Client) {
|
||||
const params = {
|
||||
index: 'traces-apm*',
|
||||
_source: [TRACE_ID, TRANSACTION_ID],
|
||||
body: {
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{ term: { [PROCESSOR_EVENT]: 'transaction' } },
|
||||
{ term: { [SERVICE_ENVIRONMENT]: environment } },
|
||||
],
|
||||
must_not: [{ exists: { field: PARENT_ID } }],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
interface Hit {
|
||||
trace: { id: string };
|
||||
transaction: { id: string };
|
||||
}
|
||||
|
||||
const res = await es.search<Hit>(params);
|
||||
|
||||
return {
|
||||
traceId: res.hits.hits[0]?._source?.trace.id as string,
|
||||
transactionId: res.hits.hits[0]?._source?.transaction.id as string,
|
||||
};
|
||||
}
|
||||
|
||||
async function getTrace({
|
||||
es,
|
||||
apmApiClient,
|
||||
maxTraceItems,
|
||||
}: {
|
||||
es: Client;
|
||||
apmApiClient: ApmApiClient;
|
||||
maxTraceItems?: number;
|
||||
}) {
|
||||
const rootTransaction = await getRootTransaction(es);
|
||||
const res = await apmApiClient.readUser({
|
||||
endpoint: `GET /internal/apm/traces/{traceId}`,
|
||||
params: {
|
||||
path: { traceId: rootTransaction.traceId },
|
||||
query: {
|
||||
start: new Date(start).toISOString(),
|
||||
end: new Date(end).toISOString(),
|
||||
entryTransactionId: rootTransaction.transactionId,
|
||||
maxTraceItems,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return res.body;
|
||||
}
|
|
@ -4,6 +4,7 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import type { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api';
|
||||
import { apm, timerange } from '@kbn/apm-synthtrace-client';
|
||||
import expect from '@kbn/expect';
|
||||
import { Readable } from 'stream';
|
||||
|
@ -17,30 +18,17 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
const start = new Date('2022-01-01T00:00:00.000Z').getTime();
|
||||
const end = new Date('2022-01-01T00:15:00.000Z').getTime() - 1;
|
||||
|
||||
async function fetchTraces({
|
||||
traceId,
|
||||
query,
|
||||
}: {
|
||||
traceId: string;
|
||||
query: { start: string; end: string; entryTransactionId: string };
|
||||
}) {
|
||||
return await apmApiClient.readUser({
|
||||
endpoint: `GET /internal/apm/traces/{traceId}`,
|
||||
params: {
|
||||
path: { traceId },
|
||||
query,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
registry.when('Trace does not exist', { config: 'basic', archives: [] }, () => {
|
||||
it('handles empty state', async () => {
|
||||
const response = await fetchTraces({
|
||||
traceId: 'foo',
|
||||
query: {
|
||||
start: new Date(start).toISOString(),
|
||||
end: new Date(end).toISOString(),
|
||||
entryTransactionId: 'foo',
|
||||
const response = await apmApiClient.readUser({
|
||||
endpoint: `GET /internal/apm/traces/{traceId}`,
|
||||
params: {
|
||||
path: { traceId: 'foo' },
|
||||
query: {
|
||||
start: new Date(start).toISOString(),
|
||||
end: new Date(end).toISOString(),
|
||||
entryTransactionId: 'foo',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -51,7 +39,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
traceDocs: [],
|
||||
errorDocs: [],
|
||||
spanLinksCountById: {},
|
||||
traceItemCount: 0,
|
||||
traceDocsTotal: 0,
|
||||
maxTraceItems: 5000,
|
||||
},
|
||||
});
|
||||
|
@ -61,6 +49,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
registry.when('Trace exists', { config: 'basic', archives: [] }, () => {
|
||||
let entryTransactionId: string;
|
||||
let serviceATraceId: string;
|
||||
|
||||
before(async () => {
|
||||
const instanceJava = apm
|
||||
.service({ name: 'synth-apple', environment: 'production', agentName: 'java' })
|
||||
|
@ -106,19 +95,24 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
after(() => synthtraceEsClient.clean());
|
||||
|
||||
describe('return trace', () => {
|
||||
let traces: Awaited<ReturnType<typeof fetchTraces>>['body'];
|
||||
let traces: APIReturnType<'GET /internal/apm/traces/{traceId}'>;
|
||||
before(async () => {
|
||||
const response = await fetchTraces({
|
||||
traceId: serviceATraceId,
|
||||
query: {
|
||||
start: new Date(start).toISOString(),
|
||||
end: new Date(end).toISOString(),
|
||||
entryTransactionId,
|
||||
const response = await apmApiClient.readUser({
|
||||
endpoint: `GET /internal/apm/traces/{traceId}`,
|
||||
params: {
|
||||
path: { traceId: serviceATraceId },
|
||||
query: {
|
||||
start: new Date(start).toISOString(),
|
||||
end: new Date(end).toISOString(),
|
||||
entryTransactionId,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.status).to.eql(200);
|
||||
traces = response.body;
|
||||
});
|
||||
|
||||
it('returns some errors', () => {
|
||||
expect(traces.traceItems.errorDocs.length).to.be.greaterThan(0);
|
||||
expect(traces.traceItems.errorDocs[0].error.exception?.[0].message).to.eql(
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue