[8.10] [APM] Paginate big traces (#165584) (#166137)

This commit is contained in:
Søren Louv-Jansen 2023-09-11 13:11:36 +02:00 committed by GitHub
parent 5bb2a71447
commit 672c588656
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 800 additions and 145 deletions

View file

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

View file

@ -23,7 +23,7 @@ describe('getCriticalPath', () => {
errorDocs: [],
exceedsMax: false,
spanLinksCountById: {},
traceItemCount: events.length,
traceDocsTotal: events.length,
maxTraceItems: 5000,
},
entryTransaction,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -48,7 +48,7 @@ Object {
},
},
},
"size": 5000,
"size": 1000,
"track_total_hits": false,
},
}

View file

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

View file

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

View file

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

View file

@ -17,6 +17,7 @@ export interface APMIndices {
span?: string;
transaction?: string;
metric?: string;
sourcemap?: string;
};
isSpaceAware?: boolean;
}

View file

@ -60,6 +60,7 @@ export async function inspectSearchParams(
span: 'myIndex',
transaction: 'myIndex',
metric: 'myIndex',
sourcemap: 'myIndex',
};
const mockConfig = new Proxy(
{},

View file

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

View file

@ -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}送信",

View file

@ -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} 传出",

View file

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

View file

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

View file

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