mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[APM] Show Universal Profiling on Transaction view (#176302)
New Setting: Default value is `False` <img width="1070" alt="Screenshot 2024-02-08 at 15 06 43" src="46e8273c
-4389-4de5-8b93-77d3e7a191d8"> --- <img width="1335" alt="Screenshot 2024-02-08 at 14 59 17" src="45531abc
-1c1c-4525-a2dc-b7573f857fa6"> <img width="1339" alt="Screenshot 2024-02-08 at 14 59 33" src="d0ca0c7e
-d33e-4f4a-83d4-70ef96b2bf1b">
This commit is contained in:
parent
8477b6b8bc
commit
db08fb5c5b
34 changed files with 957 additions and 203 deletions
|
@ -448,6 +448,9 @@ Determines whether the <<service-time-comparison, comparison feature>> is enable
|
|||
[[observability-apm-enable-infra-view]]`observability:enableInfrastructureView`::
|
||||
Enables the Infrastructure view in the APM app.
|
||||
|
||||
[[observability-apm-enable-transaction-profiling]]`observability:apmEnableTransactionProfiling`::
|
||||
Enable Universal Profiling on Transaction view.
|
||||
|
||||
[[observability-enable-inspect-es-queries]]`observability:enableInspectEsQueries`::
|
||||
When enabled, allows you to inspect {es} queries in API responses.
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@ export { jsonRt } from './src/json_rt';
|
|||
export { mergeRt } from './src/merge_rt';
|
||||
export { strictKeysRt } from './src/strict_keys_rt';
|
||||
export { isoToEpochRt } from './src/iso_to_epoch_rt';
|
||||
export { isoToEpochSecsRt } from './src/iso_to_epoch_secs_rt';
|
||||
export { toNumberRt } from './src/to_number_rt';
|
||||
export { toBooleanRt } from './src/to_boolean_rt';
|
||||
export { toJsonSchema } from './src/to_json_schema';
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* 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 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { isoToEpochSecsRt } from '.';
|
||||
import { isRight } from 'fp-ts/lib/Either';
|
||||
|
||||
describe('isoToEpochSecsRt', () => {
|
||||
it('validates whether its input is a valid ISO timestamp', () => {
|
||||
expect(isRight(isoToEpochSecsRt.decode(1566299881499))).toBe(false);
|
||||
|
||||
expect(isRight(isoToEpochSecsRt.decode('2019-08-20T11:18:31.407Z'))).toBe(true);
|
||||
});
|
||||
|
||||
it('decodes valid ISO timestamps to epoch secs time', () => {
|
||||
const iso = '2019-08-20T11:18:31.407Z';
|
||||
const result = isoToEpochSecsRt.decode(iso);
|
||||
|
||||
if (isRight(result)) {
|
||||
expect(result.right).toBe(new Date(iso).getTime() / 1000);
|
||||
} else {
|
||||
fail();
|
||||
}
|
||||
});
|
||||
|
||||
it('encodes epoch secs time to ISO string', () => {
|
||||
expect(isoToEpochSecsRt.encode(1566299911407)).toBe('2019-08-20T11:18:31.407Z');
|
||||
});
|
||||
});
|
25
packages/kbn-io-ts-utils/src/iso_to_epoch_secs_rt/index.ts
Normal file
25
packages/kbn-io-ts-utils/src/iso_to_epoch_secs_rt/index.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* 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 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { chain } from 'fp-ts/lib/Either';
|
||||
import { pipe } from 'fp-ts/lib/function';
|
||||
import * as t from 'io-ts';
|
||||
import { isoToEpochRt } from '../iso_to_epoch_rt';
|
||||
|
||||
export const isoToEpochSecsRt = new t.Type<number, string, unknown>(
|
||||
'isoToEpochSecsRt',
|
||||
t.number.is,
|
||||
(value) =>
|
||||
pipe(
|
||||
isoToEpochRt.decode(value),
|
||||
chain((epochMsValue) => {
|
||||
return t.success(epochMsValue / 1000);
|
||||
})
|
||||
),
|
||||
(output) => new Date(output).toISOString()
|
||||
);
|
|
@ -568,6 +568,10 @@ export const stackManagementSchema: MakeSchemaFrom<UsageStats> = {
|
|||
type: 'boolean',
|
||||
_meta: { description: 'Non-default value of setting.' },
|
||||
},
|
||||
'observability:apmEnableTransactionProfiling': {
|
||||
type: 'boolean',
|
||||
_meta: { description: 'Non-default value of setting.' },
|
||||
},
|
||||
'observability:profilingShowErrorFrames': {
|
||||
type: 'boolean',
|
||||
_meta: { description: 'Non-default value of setting.' },
|
||||
|
|
|
@ -167,5 +167,6 @@ export interface UsageStats {
|
|||
'observability:profilingCostPervCPUPerHour': number;
|
||||
'observability:profilingAWSCostDiscountRate': number;
|
||||
'data_views:fields_excluded_data_tiers': string;
|
||||
'observability:apmEnableTransactionProfiling': boolean;
|
||||
'devTools:enableDockedConsole': boolean;
|
||||
}
|
||||
|
|
|
@ -10168,6 +10168,12 @@
|
|||
"description": "Non-default value of setting."
|
||||
}
|
||||
},
|
||||
"observability:apmEnableTransactionProfiling": {
|
||||
"type": "boolean",
|
||||
"_meta": {
|
||||
"description": "Non-default value of setting."
|
||||
}
|
||||
},
|
||||
"observability:profilingShowErrorFrames": {
|
||||
"type": "boolean",
|
||||
"_meta": {
|
||||
|
@ -11705,4 +11711,4 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -320,6 +320,8 @@ exports[`Error TRANSACTION_OVERFLOW_COUNT 1`] = `undefined`;
|
|||
|
||||
exports[`Error TRANSACTION_PAGE_URL 1`] = `undefined`;
|
||||
|
||||
exports[`Error TRANSACTION_PROFILER_STACK_TRACE_IDS 1`] = `undefined`;
|
||||
|
||||
exports[`Error TRANSACTION_RESULT 1`] = `undefined`;
|
||||
|
||||
exports[`Error TRANSACTION_ROOT 1`] = `undefined`;
|
||||
|
@ -645,6 +647,8 @@ exports[`Span TRANSACTION_OVERFLOW_COUNT 1`] = `undefined`;
|
|||
|
||||
exports[`Span TRANSACTION_PAGE_URL 1`] = `undefined`;
|
||||
|
||||
exports[`Span TRANSACTION_PROFILER_STACK_TRACE_IDS 1`] = `undefined`;
|
||||
|
||||
exports[`Span TRANSACTION_RESULT 1`] = `undefined`;
|
||||
|
||||
exports[`Span TRANSACTION_ROOT 1`] = `undefined`;
|
||||
|
@ -988,6 +992,8 @@ exports[`Transaction TRANSACTION_OVERFLOW_COUNT 1`] = `undefined`;
|
|||
|
||||
exports[`Transaction TRANSACTION_PAGE_URL 1`] = `undefined`;
|
||||
|
||||
exports[`Transaction TRANSACTION_PROFILER_STACK_TRACE_IDS 1`] = `undefined`;
|
||||
|
||||
exports[`Transaction TRANSACTION_RESULT 1`] = `"transaction result"`;
|
||||
|
||||
exports[`Transaction TRANSACTION_ROOT 1`] = `undefined`;
|
||||
|
|
|
@ -65,6 +65,8 @@ export const TRANSACTION_OVERFLOW_COUNT =
|
|||
'transaction.aggregation.overflow_count';
|
||||
// for transaction metrics
|
||||
export const TRANSACTION_ROOT = 'transaction.root';
|
||||
export const TRANSACTION_PROFILER_STACK_TRACE_IDS =
|
||||
'transaction.profiler_stack_trace_ids';
|
||||
|
||||
export const EVENT_OUTCOME = 'event.outcome';
|
||||
|
||||
|
|
|
@ -5,16 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import {
|
||||
EuiEmptyPrompt,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiLink,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EmbeddableFlamegraph } from '@kbn/observability-shared-plugin/public';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { ApmDataSourceWithSummary } from '../../../../common/data_source';
|
||||
import { ApmDocumentType } from '../../../../common/document_type';
|
||||
|
@ -23,12 +14,9 @@ import {
|
|||
mergeKueries,
|
||||
toKueryFilterFormat,
|
||||
} from '../../../../common/utils/kuery_utils';
|
||||
import {
|
||||
FETCH_STATUS,
|
||||
isPending,
|
||||
useFetcher,
|
||||
} from '../../../hooks/use_fetcher';
|
||||
import { useProfilingPlugin } from '../../../hooks/use_profiling_plugin';
|
||||
import { useFetcher } from '../../../hooks/use_fetcher';
|
||||
import { ProfilingFlamegraphChart } from '../../shared/profiling/flamegraph';
|
||||
import { ProfilingFlamegraphLink } from '../../shared/profiling/flamegraph/flamegraph_link';
|
||||
import { HostnamesFilterWarning } from './host_names_filter_warning';
|
||||
|
||||
interface Props {
|
||||
|
@ -54,8 +42,6 @@ export function ProfilingFlamegraph({
|
|||
rangeFrom,
|
||||
rangeTo,
|
||||
}: Props) {
|
||||
const { profilingLocators } = useProfilingPlugin();
|
||||
|
||||
const { data, status } = useFetcher(
|
||||
(callApmApi) => {
|
||||
if (dataSource) {
|
||||
|
@ -92,41 +78,16 @@ export function ProfilingFlamegraph({
|
|||
<HostnamesFilterWarning hostNames={data?.hostNames} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||
<EuiLink
|
||||
data-test-subj="apmProfilingFlamegraphGoToFlamegraphLink"
|
||||
href={profilingLocators?.flamegraphLocator.getRedirectUrl({
|
||||
kuery: mergeKueries([`(${hostNamesKueryFormat})`, kuery]),
|
||||
rangeFrom,
|
||||
rangeTo,
|
||||
})}
|
||||
>
|
||||
{i18n.translate('xpack.apm.profiling.flamegraph.link', {
|
||||
defaultMessage: 'Go to Universal Profiling Flamegraph',
|
||||
})}
|
||||
</EuiLink>
|
||||
</div>
|
||||
<ProfilingFlamegraphLink
|
||||
kuery={mergeKueries([`(${hostNamesKueryFormat})`, kuery])}
|
||||
rangeFrom={rangeFrom}
|
||||
rangeTo={rangeTo}
|
||||
justifyContent="flexEnd"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer />
|
||||
{status === FETCH_STATUS.SUCCESS && isEmpty(data) ? (
|
||||
<EuiEmptyPrompt
|
||||
titleSize="s"
|
||||
title={
|
||||
<div>
|
||||
{i18n.translate('xpack.apm.profiling.flamegraph.noDataFound', {
|
||||
defaultMessage: 'No data found',
|
||||
})}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<EmbeddableFlamegraph
|
||||
data={data?.flamegraph}
|
||||
isLoading={isPending(status)}
|
||||
height="60vh"
|
||||
/>
|
||||
)}
|
||||
<ProfilingFlamegraphChart data={data?.flamegraph} status={status} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -5,8 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiLink, EuiSpacer } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
|
||||
import { EmbeddableFunctions } from '@kbn/observability-shared-plugin/public';
|
||||
import React from 'react';
|
||||
import { ApmDataSourceWithSummary } from '../../../../common/data_source';
|
||||
|
@ -17,7 +16,7 @@ import {
|
|||
toKueryFilterFormat,
|
||||
} from '../../../../common/utils/kuery_utils';
|
||||
import { isPending, useFetcher } from '../../../hooks/use_fetcher';
|
||||
import { useProfilingPlugin } from '../../../hooks/use_profiling_plugin';
|
||||
import { ProfilingTopNFunctionsLink } from '../../shared/profiling/top_functions/top_functions_link';
|
||||
import { HostnamesFilterWarning } from './host_names_filter_warning';
|
||||
|
||||
interface Props {
|
||||
|
@ -47,8 +46,6 @@ export function ProfilingTopNFunctions({
|
|||
rangeFrom,
|
||||
rangeTo,
|
||||
}: Props) {
|
||||
const { profilingLocators } = useProfilingPlugin();
|
||||
|
||||
const { data, status } = useFetcher(
|
||||
(callApmApi) => {
|
||||
if (dataSource) {
|
||||
|
@ -96,20 +93,12 @@ export function ProfilingTopNFunctions({
|
|||
<HostnamesFilterWarning hostNames={data?.hostNames} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||
<EuiLink
|
||||
data-test-subj="apmProfilingTopNFunctionsGoToUniversalProfilingFlamegraphLink"
|
||||
href={profilingLocators?.topNFunctionsLocator.getRedirectUrl({
|
||||
kuery: mergeKueries([`(${hostNamesKueryFormat})`, kuery]),
|
||||
rangeFrom,
|
||||
rangeTo,
|
||||
})}
|
||||
>
|
||||
{i18n.translate('xpack.apm.profiling.topnFunctions.link', {
|
||||
defaultMessage: 'Go to Universal Profiling Functions',
|
||||
})}
|
||||
</EuiLink>
|
||||
</div>
|
||||
<ProfilingTopNFunctionsLink
|
||||
kuery={mergeKueries([`(${hostNamesKueryFormat})`, kuery])}
|
||||
rangeFrom={rangeFrom}
|
||||
rangeTo={rangeTo}
|
||||
justifyContent="flexEnd"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer />
|
||||
|
|
|
@ -21,6 +21,7 @@ import {
|
|||
enableAgentExplorerView,
|
||||
apmEnableProfilingIntegration,
|
||||
apmEnableTableSearchBar,
|
||||
apmEnableTransactionProfiling,
|
||||
} from '@kbn/observability-plugin/common';
|
||||
import { isEmpty } from 'lodash';
|
||||
import React from 'react';
|
||||
|
@ -57,7 +58,9 @@ function getApmSettingsKeys(isProfilingIntegrationEnabled: boolean) {
|
|||
];
|
||||
|
||||
if (isProfilingIntegrationEnabled) {
|
||||
keys.push(apmEnableProfilingIntegration);
|
||||
keys.push(
|
||||
...[apmEnableProfilingIntegration, apmEnableTransactionProfiling]
|
||||
);
|
||||
}
|
||||
|
||||
return keys;
|
||||
|
|
|
@ -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 { EuiSpacer } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { useFetcher } from '../../../hooks/use_fetcher';
|
||||
import { useTimeRange } from '../../../hooks/use_time_range';
|
||||
import { ProfilingFlamegraphChart } from '../../shared/profiling/flamegraph';
|
||||
import { ProfilingFlamegraphLink } from '../../shared/profiling/flamegraph/flamegraph_link';
|
||||
|
||||
interface Props {
|
||||
serviceName: string;
|
||||
rangeFrom: string;
|
||||
rangeTo: string;
|
||||
kuery: string;
|
||||
transactionName: string;
|
||||
transactionType?: string;
|
||||
environment: string;
|
||||
}
|
||||
|
||||
export function ProfilingFlamegraph({
|
||||
serviceName,
|
||||
rangeFrom,
|
||||
rangeTo,
|
||||
kuery,
|
||||
transactionName,
|
||||
transactionType,
|
||||
environment,
|
||||
}: Props) {
|
||||
const { start, end } = useTimeRange({ rangeFrom, rangeTo });
|
||||
|
||||
const { data, status } = useFetcher(
|
||||
(callApmApi) => {
|
||||
if (!transactionType) {
|
||||
return;
|
||||
}
|
||||
return callApmApi(
|
||||
'GET /internal/apm/services/{serviceName}/transactions/flamegraph',
|
||||
{
|
||||
params: {
|
||||
path: { serviceName },
|
||||
query: {
|
||||
start,
|
||||
end,
|
||||
kuery,
|
||||
transactionName,
|
||||
transactionType,
|
||||
environment,
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
},
|
||||
[
|
||||
serviceName,
|
||||
start,
|
||||
end,
|
||||
kuery,
|
||||
transactionName,
|
||||
transactionType,
|
||||
environment,
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ProfilingFlamegraphLink
|
||||
kuery={kuery}
|
||||
rangeFrom={rangeFrom}
|
||||
rangeTo={rangeTo}
|
||||
justifyContent="flexEnd"
|
||||
/>
|
||||
<EuiSpacer />
|
||||
<ProfilingFlamegraphChart data={data} status={status} />
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,122 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiLoadingSpinner,
|
||||
EuiSpacer,
|
||||
EuiTabbedContent,
|
||||
EuiTabbedContentProps,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { useMemo } from 'react';
|
||||
import { ProfilingEmptyState } from '@kbn/observability-shared-plugin/public';
|
||||
import { useApmParams } from '../../../hooks/use_apm_params';
|
||||
import { useProfilingPlugin } from '../../../hooks/use_profiling_plugin';
|
||||
import { ProfilingFlamegraph } from './profiling_flamegraph';
|
||||
import { ProfilingTopNFunctions } from './profiling_top_functions';
|
||||
|
||||
function ProfilingTab() {
|
||||
const {
|
||||
query: {
|
||||
rangeFrom,
|
||||
rangeTo,
|
||||
environment,
|
||||
kuery,
|
||||
transactionName,
|
||||
transactionType,
|
||||
},
|
||||
path: { serviceName },
|
||||
} = useApmParams('/services/{serviceName}/transactions/view');
|
||||
const { isProfilingAvailable, isLoading } = useProfilingPlugin();
|
||||
|
||||
const tabs = useMemo((): EuiTabbedContentProps['tabs'] => {
|
||||
return [
|
||||
{
|
||||
id: 'flamegraph',
|
||||
name: i18n.translate(
|
||||
'xpack.apm.transactions.profiling.tabs.flamegraph',
|
||||
{ defaultMessage: 'Flamegraph' }
|
||||
),
|
||||
content: (
|
||||
<>
|
||||
<EuiSpacer />
|
||||
<ProfilingFlamegraph
|
||||
serviceName={serviceName}
|
||||
rangeFrom={rangeFrom}
|
||||
rangeTo={rangeTo}
|
||||
kuery={kuery}
|
||||
transactionName={transactionName}
|
||||
transactionType={transactionType}
|
||||
environment={environment}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'topNFunctions',
|
||||
name: i18n.translate(
|
||||
'xpack.apm.transactions.profiling.tabs.topNFunctions',
|
||||
{ defaultMessage: 'Top 10 Functions' }
|
||||
),
|
||||
content: (
|
||||
<>
|
||||
<EuiSpacer />
|
||||
<ProfilingTopNFunctions
|
||||
serviceName={serviceName}
|
||||
rangeFrom={rangeFrom}
|
||||
rangeTo={rangeTo}
|
||||
kuery={kuery}
|
||||
transactionName={transactionName}
|
||||
transactionType={transactionType}
|
||||
environment={environment}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
},
|
||||
];
|
||||
}, [
|
||||
environment,
|
||||
kuery,
|
||||
rangeFrom,
|
||||
rangeTo,
|
||||
serviceName,
|
||||
transactionName,
|
||||
transactionType,
|
||||
]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<EuiFlexGroup alignItems="center" justifyContent="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiLoadingSpinner size="m" />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
|
||||
if (isProfilingAvailable === false) {
|
||||
return <ProfilingEmptyState />;
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiTabbedContent
|
||||
tabs={tabs}
|
||||
initialSelectedTab={tabs[0]}
|
||||
autoFocus="selected"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export const profilingTab = {
|
||||
dataTestSubj: 'apmProfilingTabButton',
|
||||
key: 'Profiling',
|
||||
label: i18n.translate('xpack.apm.transactionDetails.tabs.ProfilingLabel', {
|
||||
defaultMessage: 'Universal Profiling',
|
||||
}),
|
||||
component: ProfilingTab,
|
||||
};
|
|
@ -0,0 +1,87 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { EuiSpacer } from '@elastic/eui';
|
||||
import { EmbeddableFunctions } from '@kbn/observability-shared-plugin/public';
|
||||
import React from 'react';
|
||||
import { isPending, useFetcher } from '../../../hooks/use_fetcher';
|
||||
import { useTimeRange } from '../../../hooks/use_time_range';
|
||||
import { ProfilingTopNFunctionsLink } from '../../shared/profiling/top_functions/top_functions_link';
|
||||
|
||||
interface Props {
|
||||
serviceName: string;
|
||||
rangeFrom: string;
|
||||
rangeTo: string;
|
||||
kuery: string;
|
||||
transactionName: string;
|
||||
transactionType?: string;
|
||||
environment: string;
|
||||
}
|
||||
|
||||
export function ProfilingTopNFunctions({
|
||||
serviceName,
|
||||
rangeFrom,
|
||||
rangeTo,
|
||||
kuery,
|
||||
transactionName,
|
||||
transactionType,
|
||||
environment,
|
||||
}: Props) {
|
||||
const { start, end } = useTimeRange({ rangeFrom, rangeTo });
|
||||
|
||||
const { data, status } = useFetcher(
|
||||
(callApmApi) => {
|
||||
if (!transactionType) {
|
||||
return;
|
||||
}
|
||||
return callApmApi(
|
||||
'GET /internal/apm/services/{serviceName}/transactions/functions',
|
||||
{
|
||||
params: {
|
||||
path: { serviceName },
|
||||
query: {
|
||||
start,
|
||||
end,
|
||||
kuery,
|
||||
transactionName,
|
||||
startIndex: 0,
|
||||
endIndex: 10,
|
||||
transactionType,
|
||||
environment,
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
},
|
||||
[
|
||||
serviceName,
|
||||
start,
|
||||
end,
|
||||
kuery,
|
||||
transactionName,
|
||||
transactionType,
|
||||
environment,
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ProfilingTopNFunctionsLink
|
||||
kuery={kuery}
|
||||
rangeFrom={rangeFrom}
|
||||
rangeTo={rangeTo}
|
||||
justifyContent="flexEnd"
|
||||
/>
|
||||
<EuiSpacer />
|
||||
<EmbeddableFunctions
|
||||
data={data}
|
||||
isLoading={isPending(status)}
|
||||
rangeFrom={new Date(start).valueOf()}
|
||||
rangeTo={new Date(end).valueOf()}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -5,37 +5,29 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { omit } from 'lodash';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import {
|
||||
EuiPanel,
|
||||
EuiSpacer,
|
||||
EuiTabs,
|
||||
EuiTab,
|
||||
EuiFlexItem,
|
||||
EuiFlexGroup,
|
||||
} from '@elastic/eui';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { XYBrushEvent } from '@elastic/charts';
|
||||
import { EuiPanel, EuiSpacer, EuiTab, EuiTabs } from '@elastic/eui';
|
||||
import { omit } from 'lodash';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { maybe } from '../../../../common/utils/maybe';
|
||||
import { useLegacyUrlParams } from '../../../context/url_params_context/use_url_params';
|
||||
import { useAnyOfApmParams } from '../../../hooks/use_apm_params';
|
||||
import { useCriticalPathFeatureEnabledSetting } from '../../../hooks/use_critical_path_feature_enabled_setting';
|
||||
import { FETCH_STATUS } from '../../../hooks/use_fetcher';
|
||||
import { useSampleChartSelection } from '../../../hooks/use_sample_chart_selection';
|
||||
import {
|
||||
TraceSamplesFetchResult,
|
||||
useTransactionTraceSamplesFetcher,
|
||||
} from '../../../hooks/use_transaction_trace_samples_fetcher';
|
||||
|
||||
import { maybe } from '../../../../common/utils/maybe';
|
||||
import { fromQuery, toQuery } from '../../shared/links/url_helpers';
|
||||
|
||||
import { aggregatedCriticalPathTab } from './aggregated_critical_path_tab';
|
||||
import { failedTransactionsCorrelationsTab } from './failed_transactions_correlations_tab';
|
||||
import { latencyCorrelationsTab } from './latency_correlations_tab';
|
||||
import { profilingTab } from './profiling_tab';
|
||||
import { traceSamplesTab } from './trace_samples_tab';
|
||||
import { useSampleChartSelection } from '../../../hooks/use_sample_chart_selection';
|
||||
import { FETCH_STATUS } from '../../../hooks/use_fetcher';
|
||||
import { useCriticalPathFeatureEnabledSetting } from '../../../hooks/use_critical_path_feature_enabled_setting';
|
||||
import { aggregatedCriticalPathTab } from './aggregated_critical_path_tab';
|
||||
import { useTransactionProfilingSetting } from '../../../hooks/use_profiling_integration_setting';
|
||||
|
||||
export interface TabContentProps {
|
||||
clearChartSelection: () => void;
|
||||
|
@ -46,12 +38,6 @@ export interface TabContentProps {
|
|||
traceSamplesFetchResult: TraceSamplesFetchResult;
|
||||
}
|
||||
|
||||
const tabs = [
|
||||
traceSamplesTab,
|
||||
latencyCorrelationsTab,
|
||||
failedTransactionsCorrelationsTab,
|
||||
];
|
||||
|
||||
export function TransactionDetailsTabs() {
|
||||
const { query } = useAnyOfApmParams(
|
||||
'/services/{serviceName}/transactions/view',
|
||||
|
@ -59,10 +45,24 @@ export function TransactionDetailsTabs() {
|
|||
);
|
||||
|
||||
const isCriticalPathFeatureEnabled = useCriticalPathFeatureEnabledSetting();
|
||||
const isTransactionProfilingEnabled = useTransactionProfilingSetting();
|
||||
|
||||
const availableTabs = isCriticalPathFeatureEnabled
|
||||
? tabs.concat(aggregatedCriticalPathTab)
|
||||
: tabs;
|
||||
const availableTabs = useMemo(() => {
|
||||
const tabs = [
|
||||
traceSamplesTab,
|
||||
latencyCorrelationsTab,
|
||||
failedTransactionsCorrelationsTab,
|
||||
];
|
||||
if (isCriticalPathFeatureEnabled) {
|
||||
tabs.push(aggregatedCriticalPathTab);
|
||||
}
|
||||
|
||||
if (isTransactionProfilingEnabled) {
|
||||
tabs.push(profilingTab);
|
||||
}
|
||||
|
||||
return tabs;
|
||||
}, [isCriticalPathFeatureEnabled, isTransactionProfilingEnabled]);
|
||||
|
||||
const { urlParams } = useLegacyUrlParams();
|
||||
const history = useHistory();
|
||||
|
@ -140,20 +140,16 @@ export function TransactionDetailsTabs() {
|
|||
</EuiTabs>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiPanel hasBorder={true}>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<TabContent
|
||||
{...{
|
||||
clearChartSelection,
|
||||
onFilter,
|
||||
sampleRangeFrom,
|
||||
sampleRangeTo,
|
||||
selectSampleFromChartSelection,
|
||||
traceSamplesFetchResult,
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<TabContent
|
||||
{...{
|
||||
clearChartSelection,
|
||||
onFilter,
|
||||
sampleRangeFrom,
|
||||
sampleRangeTo,
|
||||
selectSampleFromChartSelection,
|
||||
traceSamplesFetchResult,
|
||||
}}
|
||||
/>
|
||||
</EuiPanel>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexGroupProps,
|
||||
EuiFlexItem,
|
||||
EuiLink,
|
||||
} from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useProfilingPlugin } from '../../../../hooks/use_profiling_plugin';
|
||||
|
||||
interface Props {
|
||||
kuery: string;
|
||||
rangeFrom: string;
|
||||
rangeTo: string;
|
||||
justifyContent?: EuiFlexGroupProps['justifyContent'];
|
||||
}
|
||||
|
||||
export function ProfilingFlamegraphLink({
|
||||
kuery,
|
||||
rangeFrom,
|
||||
rangeTo,
|
||||
justifyContent = 'flexStart',
|
||||
}: Props) {
|
||||
const { profilingLocators } = useProfilingPlugin();
|
||||
return (
|
||||
<EuiFlexGroup justifyContent={justifyContent}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiLink
|
||||
data-test-subj="apmProfilingFlamegraphGoToFlamegraphLink"
|
||||
href={profilingLocators?.flamegraphLocator.getRedirectUrl({
|
||||
kuery,
|
||||
rangeFrom,
|
||||
rangeTo,
|
||||
})}
|
||||
>
|
||||
{i18n.translate('xpack.apm.profiling.flamegraph.link', {
|
||||
defaultMessage: 'Go to Universal Profiling Flamegraph',
|
||||
})}
|
||||
</EuiLink>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { EuiEmptyPrompt } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EmbeddableFlamegraph } from '@kbn/observability-shared-plugin/public';
|
||||
import { BaseFlameGraph } from '@kbn/profiling-utils';
|
||||
import { isEmpty } from 'lodash';
|
||||
import React from 'react';
|
||||
import { FETCH_STATUS, isPending } from '../../../../hooks/use_fetcher';
|
||||
|
||||
interface Props {
|
||||
data?: BaseFlameGraph;
|
||||
status: FETCH_STATUS;
|
||||
}
|
||||
|
||||
export function ProfilingFlamegraphChart({ data, status }: Props) {
|
||||
return (
|
||||
<>
|
||||
{status === FETCH_STATUS.SUCCESS &&
|
||||
(isEmpty(data) || data?.TotalSamples === 0) ? (
|
||||
<EuiEmptyPrompt
|
||||
titleSize="s"
|
||||
title={
|
||||
<div>
|
||||
{i18n.translate('xpack.apm.profiling.flamegraph.noDataFound', {
|
||||
defaultMessage: 'No data found',
|
||||
})}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<EmbeddableFlamegraph
|
||||
data={data}
|
||||
isLoading={isPending(status)}
|
||||
height="35vh"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexGroupProps,
|
||||
EuiFlexItem,
|
||||
EuiLink,
|
||||
} from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useProfilingPlugin } from '../../../../hooks/use_profiling_plugin';
|
||||
|
||||
interface Props {
|
||||
kuery: string;
|
||||
rangeFrom: string;
|
||||
rangeTo: string;
|
||||
justifyContent?: EuiFlexGroupProps['justifyContent'];
|
||||
}
|
||||
|
||||
export function ProfilingTopNFunctionsLink({
|
||||
kuery,
|
||||
rangeFrom,
|
||||
rangeTo,
|
||||
justifyContent = 'flexStart',
|
||||
}: Props) {
|
||||
const { profilingLocators } = useProfilingPlugin();
|
||||
|
||||
return (
|
||||
<EuiFlexGroup justifyContent={justifyContent}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiLink
|
||||
data-test-subj="apmProfilingTopNFunctionsGoToUniversalProfilingFlamegraphLink"
|
||||
href={profilingLocators?.topNFunctionsLocator.getRedirectUrl({
|
||||
kuery,
|
||||
rangeFrom,
|
||||
rangeTo,
|
||||
})}
|
||||
>
|
||||
{i18n.translate('xpack.apm.profiling.topnFunctions.link', {
|
||||
defaultMessage: 'Go to Universal Profiling Functions',
|
||||
})}
|
||||
</EuiLink>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
|
@ -6,8 +6,12 @@
|
|||
*/
|
||||
|
||||
import { useUiSetting } from '@kbn/kibana-react-plugin/public';
|
||||
import { apmEnableProfilingIntegration } from '@kbn/observability-plugin/common';
|
||||
import {
|
||||
apmEnableProfilingIntegration,
|
||||
apmEnableTransactionProfiling,
|
||||
} from '@kbn/observability-plugin/common';
|
||||
import { ApmFeatureFlagName } from '../../common/apm_feature_flags';
|
||||
import { useApmPluginContext } from '../context/apm_plugin/use_apm_plugin_context';
|
||||
import { useApmFeatureFlag } from './use_apm_feature_flag';
|
||||
|
||||
export function useProfilingIntegrationSetting() {
|
||||
|
@ -23,3 +27,14 @@ export function useProfilingIntegrationSetting() {
|
|||
isProfilingIntegrationUiSettingEnabled
|
||||
);
|
||||
}
|
||||
|
||||
export function useTransactionProfilingSetting() {
|
||||
const { core } = useApmPluginContext();
|
||||
const isProfilingIntegrationEnabled = useProfilingIntegrationSetting();
|
||||
|
||||
const isTransactionProfilingEnabled = core.uiSettings.get<boolean>(
|
||||
apmEnableTransactionProfiling
|
||||
);
|
||||
|
||||
return isProfilingIntegrationEnabled && isTransactionProfilingEnabled;
|
||||
}
|
||||
|
|
|
@ -342,4 +342,8 @@ export class APMEventClient {
|
|||
cb: (opts) => this.esClient.termsEnum(requestParams, opts),
|
||||
});
|
||||
}
|
||||
|
||||
getIndicesFromProcessorEvent(processorEvent: ProcessorEvent) {
|
||||
return processorEventsToIndex([processorEvent], this.indices);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,10 +5,18 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { toNumberRt } from '@kbn/io-ts-utils';
|
||||
import { isoToEpochSecsRt, toNumberRt } from '@kbn/io-ts-utils';
|
||||
import type { BaseFlameGraph, TopNFunctions } from '@kbn/profiling-utils';
|
||||
import * as t from 'io-ts';
|
||||
import { HOST_NAME } from '../../../common/es_fields/apm';
|
||||
import { kqlQuery, termQuery } from '@kbn/observability-plugin/server';
|
||||
import { ProcessorEvent } from '@kbn/observability-plugin/common';
|
||||
import {
|
||||
HOST_NAME,
|
||||
SERVICE_NAME,
|
||||
TRANSACTION_NAME,
|
||||
TRANSACTION_PROFILER_STACK_TRACE_IDS,
|
||||
TRANSACTION_TYPE,
|
||||
} from '../../../common/es_fields/apm';
|
||||
import {
|
||||
mergeKueries,
|
||||
toKueryFilterFormat,
|
||||
|
@ -22,6 +30,7 @@ import {
|
|||
serviceTransactionDataSourceRt,
|
||||
} from '../default_api_types';
|
||||
import { getServiceHostNames } from './get_service_host_names';
|
||||
import { environmentQuery } from '../../../common/utils/environment_query';
|
||||
|
||||
const profilingFlamegraphRoute = createApmServerRoute({
|
||||
endpoint: 'GET /internal/apm/services/{serviceName}/profiling/flamegraph',
|
||||
|
@ -66,17 +75,35 @@ const profilingFlamegraphRoute = createApmServerRoute({
|
|||
if (!serviceHostNames.length) {
|
||||
return undefined;
|
||||
}
|
||||
const startSecs = start / 1000;
|
||||
const endSecs = end / 1000;
|
||||
|
||||
const flamegraph =
|
||||
await profilingDataAccessStart?.services.fetchFlamechartData({
|
||||
core,
|
||||
esClient: esClient.asCurrentUser,
|
||||
rangeFromMs: start,
|
||||
rangeToMs: end,
|
||||
kuery: mergeKueries([
|
||||
`(${toKueryFilterFormat(HOST_NAME, serviceHostNames)})`,
|
||||
kuery,
|
||||
]),
|
||||
totalSeconds: endSecs - startSecs,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
...kqlQuery(
|
||||
mergeKueries([
|
||||
`(${toKueryFilterFormat(HOST_NAME, serviceHostNames)})`,
|
||||
kuery,
|
||||
])
|
||||
),
|
||||
{
|
||||
range: {
|
||||
['@timestamp']: {
|
||||
gte: String(startSecs),
|
||||
lt: String(endSecs),
|
||||
format: 'epoch_second',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return { flamegraph, hostNames: serviceHostNames };
|
||||
|
@ -137,17 +164,36 @@ const profilingFunctionsRoute = createApmServerRoute({
|
|||
return undefined;
|
||||
}
|
||||
|
||||
const startSecs = start / 1000;
|
||||
const endSecs = end / 1000;
|
||||
|
||||
const functions = await profilingDataAccessStart?.services.fetchFunction({
|
||||
core,
|
||||
esClient: esClient.asCurrentUser,
|
||||
rangeFromMs: start,
|
||||
rangeToMs: end,
|
||||
kuery: mergeKueries([
|
||||
`(${toKueryFilterFormat(HOST_NAME, serviceHostNames)})`,
|
||||
kuery,
|
||||
]),
|
||||
startIndex,
|
||||
endIndex,
|
||||
totalSeconds: endSecs - startSecs,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
...kqlQuery(
|
||||
mergeKueries([
|
||||
`(${toKueryFilterFormat(HOST_NAME, serviceHostNames)})`,
|
||||
kuery,
|
||||
])
|
||||
),
|
||||
{
|
||||
range: {
|
||||
['@timestamp']: {
|
||||
gte: String(startSecs),
|
||||
lt: String(endSecs),
|
||||
format: 'epoch_second',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
return { functions, hostNames: serviceHostNames };
|
||||
}
|
||||
|
@ -156,6 +202,159 @@ const profilingFunctionsRoute = createApmServerRoute({
|
|||
},
|
||||
});
|
||||
|
||||
const transactionsFlamegraphRoute = createApmServerRoute({
|
||||
endpoint: 'GET /internal/apm/services/{serviceName}/transactions/flamegraph',
|
||||
params: t.type({
|
||||
path: t.type({ serviceName: t.string }),
|
||||
query: t.intersection([
|
||||
kueryRt,
|
||||
environmentRt,
|
||||
t.type({
|
||||
transactionName: t.string,
|
||||
start: isoToEpochSecsRt,
|
||||
end: isoToEpochSecsRt,
|
||||
transactionType: t.string,
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async (resources): Promise<BaseFlameGraph | undefined> => {
|
||||
const { context, plugins, params } = resources;
|
||||
const core = await context.core;
|
||||
const [esClient, profilingDataAccessStart, apmEventClient] =
|
||||
await Promise.all([
|
||||
core.elasticsearch.client,
|
||||
await plugins.profilingDataAccess?.start(),
|
||||
getApmEventClient(resources),
|
||||
]);
|
||||
if (profilingDataAccessStart) {
|
||||
const { serviceName } = params.path;
|
||||
const {
|
||||
start,
|
||||
end,
|
||||
kuery,
|
||||
transactionName,
|
||||
transactionType,
|
||||
environment,
|
||||
} = params.query;
|
||||
|
||||
const indices = apmEventClient.getIndicesFromProcessorEvent(
|
||||
ProcessorEvent.transaction
|
||||
);
|
||||
|
||||
return await profilingDataAccessStart?.services.fetchFlamechartData({
|
||||
core,
|
||||
esClient: esClient.asCurrentUser,
|
||||
indices,
|
||||
stacktraceIdsField: TRANSACTION_PROFILER_STACK_TRACE_IDS,
|
||||
totalSeconds: end - start,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
...kqlQuery(kuery),
|
||||
...termQuery(SERVICE_NAME, serviceName),
|
||||
...termQuery(TRANSACTION_NAME, transactionName),
|
||||
...environmentQuery(environment),
|
||||
...termQuery(TRANSACTION_TYPE, transactionType),
|
||||
{
|
||||
range: {
|
||||
['@timestamp']: {
|
||||
gte: String(start),
|
||||
lt: String(end),
|
||||
format: 'epoch_second',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return undefined;
|
||||
},
|
||||
});
|
||||
|
||||
const transactionsFunctionsRoute = createApmServerRoute({
|
||||
endpoint: 'GET /internal/apm/services/{serviceName}/transactions/functions',
|
||||
params: t.type({
|
||||
path: t.type({ serviceName: t.string }),
|
||||
query: t.intersection([
|
||||
environmentRt,
|
||||
t.type({
|
||||
start: isoToEpochSecsRt,
|
||||
end: isoToEpochSecsRt,
|
||||
startIndex: toNumberRt,
|
||||
endIndex: toNumberRt,
|
||||
transactionName: t.string,
|
||||
transactionType: t.string,
|
||||
}),
|
||||
kueryRt,
|
||||
]),
|
||||
}),
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async (resources): Promise<TopNFunctions | undefined> => {
|
||||
const { context, plugins, params } = resources;
|
||||
const core = await context.core;
|
||||
|
||||
const [esClient, profilingDataAccessStart, apmEventClient] =
|
||||
await Promise.all([
|
||||
core.elasticsearch.client,
|
||||
await plugins.profilingDataAccess?.start(),
|
||||
getApmEventClient(resources),
|
||||
]);
|
||||
if (profilingDataAccessStart) {
|
||||
const {
|
||||
start,
|
||||
end,
|
||||
startIndex,
|
||||
endIndex,
|
||||
kuery,
|
||||
transactionName,
|
||||
transactionType,
|
||||
environment,
|
||||
} = params.query;
|
||||
const { serviceName } = params.path;
|
||||
|
||||
const indices = apmEventClient.getIndicesFromProcessorEvent(
|
||||
ProcessorEvent.transaction
|
||||
);
|
||||
|
||||
return profilingDataAccessStart?.services.fetchFunction({
|
||||
core,
|
||||
esClient: esClient.asCurrentUser,
|
||||
startIndex,
|
||||
endIndex,
|
||||
indices,
|
||||
stacktraceIdsField: TRANSACTION_PROFILER_STACK_TRACE_IDS,
|
||||
totalSeconds: end - start,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
...kqlQuery(kuery),
|
||||
...termQuery(SERVICE_NAME, serviceName),
|
||||
...termQuery(TRANSACTION_NAME, transactionName),
|
||||
...environmentQuery(environment),
|
||||
...termQuery(TRANSACTION_TYPE, transactionType),
|
||||
{
|
||||
range: {
|
||||
['@timestamp']: {
|
||||
gte: String(start),
|
||||
lt: String(end),
|
||||
format: 'epoch_second',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return undefined;
|
||||
},
|
||||
});
|
||||
|
||||
const profilingStatusRoute = createApmServerRoute({
|
||||
endpoint: 'GET /internal/apm/profiling/status',
|
||||
options: { tags: ['access:apm'] },
|
||||
|
@ -190,4 +389,6 @@ export const profilingRouteRepository = {
|
|||
...profilingFlamegraphRoute,
|
||||
...profilingStatusRoute,
|
||||
...profilingFunctionsRoute,
|
||||
...transactionsFlamegraphRoute,
|
||||
...transactionsFunctionsRoute,
|
||||
};
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import type { CoreRequestHandlerContext } from '@kbn/core-http-request-handler-context-server';
|
||||
import type { ProfilingDataAccessPluginStart } from '@kbn/profiling-data-access-plugin/server';
|
||||
import type { BaseFlameGraph } from '@kbn/profiling-utils';
|
||||
import { kqlQuery } from '@kbn/observability-plugin/server';
|
||||
import type { InfraProfilingFlamegraphRequestParams } from '../../../../common/http_api/profiling_api';
|
||||
|
||||
export async function fetchProfilingFlamegraph(
|
||||
|
@ -15,11 +16,28 @@ export async function fetchProfilingFlamegraph(
|
|||
profilingDataAccess: ProfilingDataAccessPluginStart,
|
||||
coreRequestContext: CoreRequestHandlerContext
|
||||
): Promise<BaseFlameGraph> {
|
||||
const startSecs = from / 1000;
|
||||
const endSecs = to / 1000;
|
||||
|
||||
return await profilingDataAccess.services.fetchFlamechartData({
|
||||
core: coreRequestContext,
|
||||
esClient: coreRequestContext.elasticsearch.client.asCurrentUser,
|
||||
rangeFromMs: from,
|
||||
rangeToMs: to,
|
||||
kuery,
|
||||
totalSeconds: endSecs - startSecs,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
...kqlQuery(kuery),
|
||||
{
|
||||
range: {
|
||||
['@timestamp']: {
|
||||
gte: String(startSecs),
|
||||
lt: String(endSecs),
|
||||
format: 'epoch_second',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import type { CoreRequestHandlerContext } from '@kbn/core-http-request-handler-context-server';
|
||||
import type { ProfilingDataAccessPluginStart } from '@kbn/profiling-data-access-plugin/server';
|
||||
import type { TopNFunctions } from '@kbn/profiling-utils';
|
||||
import { kqlQuery } from '@kbn/observability-plugin/server';
|
||||
import type { InfraProfilingFunctionsRequestParams } from '../../../../common/http_api/profiling_api';
|
||||
|
||||
export async function fetchProfilingFunctions(
|
||||
|
@ -16,14 +17,30 @@ export async function fetchProfilingFunctions(
|
|||
coreRequestContext: CoreRequestHandlerContext
|
||||
): Promise<TopNFunctions> {
|
||||
const { kuery, from, to, startIndex, endIndex } = params;
|
||||
const startSecs = from / 1000;
|
||||
const endSecs = to / 1000;
|
||||
|
||||
return await profilingDataAccess.services.fetchFunction({
|
||||
core: coreRequestContext,
|
||||
esClient: coreRequestContext.elasticsearch.client.asCurrentUser,
|
||||
rangeFromMs: from,
|
||||
rangeToMs: to,
|
||||
kuery,
|
||||
startIndex,
|
||||
endIndex,
|
||||
totalSeconds: endSecs - startSecs,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
...kqlQuery(kuery),
|
||||
{
|
||||
range: {
|
||||
['@timestamp']: {
|
||||
gte: String(startSecs),
|
||||
lt: String(endSecs),
|
||||
format: 'epoch_second',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
@ -51,6 +51,7 @@ export {
|
|||
profilingPervCPUWattArm64,
|
||||
profilingAWSCostDiscountRate,
|
||||
profilingCostPervCPUPerHour,
|
||||
apmEnableTransactionProfiling,
|
||||
} from './ui_settings_keys';
|
||||
|
||||
export {
|
||||
|
|
|
@ -39,3 +39,4 @@ export const profilingCo2PerKWH = 'observability:profilingCo2PerKWH';
|
|||
export const profilingDatacenterPUE = 'observability:profilingDatacenterPUE';
|
||||
export const profilingAWSCostDiscountRate = 'observability:profilingAWSCostDiscountRate';
|
||||
export const profilingCostPervCPUPerHour = 'observability:profilingCostPervCPUPerHour';
|
||||
export const apmEnableTransactionProfiling = 'observability:apmEnableTransactionProfiling';
|
||||
|
|
|
@ -39,6 +39,7 @@ import {
|
|||
profilingAWSCostDiscountRate,
|
||||
profilingCostPervCPUPerHour,
|
||||
enableInfrastructureProfilingIntegration,
|
||||
apmEnableTransactionProfiling,
|
||||
enableInfrastructureHostsCustomDashboards,
|
||||
} from '../common/ui_settings_keys';
|
||||
|
||||
|
@ -552,6 +553,15 @@ export const uiSettings: Record<string, UiSettings> = {
|
|||
schema: schema.number({ min: 0, max: 100 }),
|
||||
requiresPageReload: true,
|
||||
},
|
||||
[apmEnableTransactionProfiling]: {
|
||||
category: [observabilityFeatureId],
|
||||
name: i18n.translate('xpack.observability.apmEnableTransactionProfiling', {
|
||||
defaultMessage: 'Enable Universal Profiling on Transaction view',
|
||||
}),
|
||||
value: false,
|
||||
schema: schema.boolean(),
|
||||
requiresPageReload: true,
|
||||
},
|
||||
};
|
||||
|
||||
function throttlingDocsLink({ href }: { href: string }) {
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { kqlQuery } from '@kbn/observability-plugin/server';
|
||||
import { IDLE_SOCKET_TIMEOUT, RouteRegisterParameters } from '.';
|
||||
import { getRoutePaths } from '../../common';
|
||||
import { handleRouteHandlerError } from '../utils/handle_route_error_handler';
|
||||
|
@ -35,15 +36,31 @@ export function registerFlameChartSearchRoute({
|
|||
const { timeFrom, timeTo, kuery } = request.query;
|
||||
|
||||
const core = await context.core;
|
||||
const startSecs = timeFrom / 1000;
|
||||
const endSecs = timeTo / 1000;
|
||||
|
||||
try {
|
||||
const esClient = await getClient(context);
|
||||
const flamegraph = await profilingDataAccess.services.fetchFlamechartData({
|
||||
core,
|
||||
esClient,
|
||||
rangeFromMs: timeFrom,
|
||||
rangeToMs: timeTo,
|
||||
kuery,
|
||||
totalSeconds: endSecs - startSecs,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
...kqlQuery(kuery),
|
||||
{
|
||||
range: {
|
||||
['@timestamp']: {
|
||||
gte: String(startSecs),
|
||||
lt: String(endSecs),
|
||||
format: 'epoch_second',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return response.ok({ body: flamegraph });
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import { schema, TypeOf } from '@kbn/config-schema';
|
||||
import { kqlQuery } from '@kbn/observability-plugin/server';
|
||||
import { IDLE_SOCKET_TIMEOUT, RouteRegisterParameters } from '.';
|
||||
import { getRoutePaths } from '../../common';
|
||||
import { handleRouteHandlerError } from '../utils/handle_route_error_handler';
|
||||
|
@ -40,15 +41,32 @@ export function registerTopNFunctionsSearchRoute({
|
|||
const core = await context.core;
|
||||
|
||||
const { timeFrom, timeTo, startIndex, endIndex, kuery }: QuerySchemaType = request.query;
|
||||
const startSecs = timeFrom / 1000;
|
||||
const endSecs = timeTo / 1000;
|
||||
|
||||
const esClient = await getClient(context);
|
||||
const topNFunctions = await profilingDataAccess.services.fetchFunction({
|
||||
core,
|
||||
esClient,
|
||||
rangeFromMs: timeFrom,
|
||||
rangeToMs: timeTo,
|
||||
kuery,
|
||||
startIndex,
|
||||
endIndex,
|
||||
totalSeconds: endSecs - startSecs,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
...kqlQuery(kuery),
|
||||
{
|
||||
range: {
|
||||
['@timestamp']: {
|
||||
gte: String(startSecs),
|
||||
lt: String(endSecs),
|
||||
format: 'epoch_second',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return response.ok({
|
||||
|
|
|
@ -29,6 +29,8 @@ export interface ProfilingESClient {
|
|||
pervCPUWattArm64?: number;
|
||||
awsCostDiscountRate?: number;
|
||||
costPervCPUPerHour?: number;
|
||||
indices?: string[];
|
||||
stacktraceIdsField?: string;
|
||||
}): Promise<StackTraceResponse>;
|
||||
profilingStatus(params?: { waitForResourcesCreated?: boolean }): Promise<ProfilingStatusResponse>;
|
||||
getEsClient(): ElasticsearchClient;
|
||||
|
@ -42,5 +44,7 @@ export interface ProfilingESClient {
|
|||
pervCPUWattArm64?: number;
|
||||
awsCostDiscountRate?: number;
|
||||
costPervCPUPerHour?: number;
|
||||
indices?: string[];
|
||||
stacktraceIdsField?: string;
|
||||
}): Promise<BaseFlameGraph>;
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import { CoreRequestHandlerContext, ElasticsearchClient } from '@kbn/core/server';
|
||||
import {
|
||||
profilingAWSCostDiscountRate,
|
||||
|
@ -15,24 +15,28 @@ import {
|
|||
profilingPervCPUWattX86,
|
||||
} from '@kbn/observability-plugin/common';
|
||||
import { percentToFactor } from '../../utils/percent_to_factor';
|
||||
import { kqlQuery } from '../../utils/query';
|
||||
import { RegisterServicesParams } from '../register_services';
|
||||
|
||||
export interface FetchFlamechartParams {
|
||||
esClient: ElasticsearchClient;
|
||||
core: CoreRequestHandlerContext;
|
||||
rangeFromMs: number;
|
||||
rangeToMs: number;
|
||||
kuery: string;
|
||||
indices?: string[];
|
||||
stacktraceIdsField?: string;
|
||||
query: QueryDslQueryContainer;
|
||||
totalSeconds: number;
|
||||
}
|
||||
|
||||
const targetSampleSize = 20000; // minimum number of samples to get statistically sound results
|
||||
|
||||
export function createFetchFlamechart({ createProfilingEsClient }: RegisterServicesParams) {
|
||||
return async ({ core, esClient, rangeFromMs, rangeToMs, kuery }: FetchFlamechartParams) => {
|
||||
const rangeFromSecs = rangeFromMs / 1000;
|
||||
const rangeToSecs = rangeToMs / 1000;
|
||||
|
||||
return async ({
|
||||
core,
|
||||
esClient,
|
||||
indices,
|
||||
stacktraceIdsField,
|
||||
query,
|
||||
totalSeconds,
|
||||
}: FetchFlamechartParams) => {
|
||||
const [
|
||||
co2PerKWH,
|
||||
datacenterPUE,
|
||||
|
@ -50,24 +54,9 @@ export function createFetchFlamechart({ createProfilingEsClient }: RegisterServi
|
|||
]);
|
||||
|
||||
const profilingEsClient = createProfilingEsClient({ esClient });
|
||||
const totalSeconds = rangeToSecs - rangeFromSecs;
|
||||
|
||||
const flamegraph = await profilingEsClient.profilingFlamegraph({
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
...kqlQuery(kuery),
|
||||
{
|
||||
range: {
|
||||
['@timestamp']: {
|
||||
gte: String(rangeFromSecs),
|
||||
lt: String(rangeToSecs),
|
||||
format: 'epoch_second',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
query,
|
||||
sampleSize: targetSampleSize,
|
||||
durationSeconds: totalSeconds,
|
||||
co2PerKWH,
|
||||
|
@ -76,6 +65,8 @@ export function createFetchFlamechart({ createProfilingEsClient }: RegisterServi
|
|||
pervCPUWattArm64,
|
||||
awsCostDiscountRate: percentToFactor(awsCostDiscountRate),
|
||||
costPervCPUPerHour,
|
||||
indices,
|
||||
stacktraceIdsField,
|
||||
});
|
||||
return { ...flamegraph, TotalSeconds: totalSeconds };
|
||||
};
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
} from '@kbn/observability-plugin/common';
|
||||
import { CoreRequestHandlerContext, ElasticsearchClient } from '@kbn/core/server';
|
||||
import { createTopNFunctions } from '@kbn/profiling-utils';
|
||||
import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import { percentToFactor } from '../../utils/percent_to_factor';
|
||||
import { withProfilingSpan } from '../../utils/with_profiling_span';
|
||||
import { RegisterServicesParams } from '../register_services';
|
||||
|
@ -23,11 +24,12 @@ import { searchStackTraces } from '../search_stack_traces';
|
|||
export interface FetchFunctionsParams {
|
||||
core: CoreRequestHandlerContext;
|
||||
esClient: ElasticsearchClient;
|
||||
rangeFromMs: number;
|
||||
rangeToMs: number;
|
||||
kuery: string;
|
||||
startIndex: number;
|
||||
endIndex: number;
|
||||
indices?: string[];
|
||||
stacktraceIdsField?: string;
|
||||
query: QueryDslQueryContainer;
|
||||
totalSeconds: number;
|
||||
}
|
||||
|
||||
const targetSampleSize = 20000; // minimum number of samples to get statistically sound results
|
||||
|
@ -36,16 +38,13 @@ export function createFetchFunctions({ createProfilingEsClient }: RegisterServic
|
|||
return async ({
|
||||
core,
|
||||
esClient,
|
||||
rangeFromMs,
|
||||
rangeToMs,
|
||||
kuery,
|
||||
startIndex,
|
||||
endIndex,
|
||||
indices,
|
||||
stacktraceIdsField,
|
||||
query,
|
||||
totalSeconds,
|
||||
}: FetchFunctionsParams) => {
|
||||
const rangeFromSecs = rangeFromMs / 1000;
|
||||
const rangeToSecs = rangeToMs / 1000;
|
||||
const totalSeconds = rangeToSecs - rangeFromSecs;
|
||||
|
||||
const [
|
||||
co2PerKWH,
|
||||
datacenterPUE,
|
||||
|
@ -69,9 +68,6 @@ export function createFetchFunctions({ createProfilingEsClient }: RegisterServic
|
|||
const { events, stackTraces, executables, stackFrames, samplingRate } = await searchStackTraces(
|
||||
{
|
||||
client: profilingEsClient,
|
||||
rangeFrom: rangeFromSecs,
|
||||
rangeTo: rangeToSecs,
|
||||
kuery,
|
||||
sampleSize: targetSampleSize,
|
||||
durationSeconds: totalSeconds,
|
||||
co2PerKWH,
|
||||
|
@ -80,6 +76,9 @@ export function createFetchFunctions({ createProfilingEsClient }: RegisterServic
|
|||
pervCPUWattArm64,
|
||||
awsCostDiscountRate: percentToFactor(awsCostDiscountRate),
|
||||
costPervCPUPerHour,
|
||||
indices,
|
||||
stacktraceIdsField,
|
||||
query,
|
||||
showErrorFrames,
|
||||
}
|
||||
);
|
||||
|
|
|
@ -6,15 +6,12 @@
|
|||
*/
|
||||
|
||||
import { decodeStackTraceResponse } from '@kbn/profiling-utils';
|
||||
import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import { ProfilingESClient } from '../../../common/profiling_es_client';
|
||||
import { kqlQuery } from '../../utils/query';
|
||||
|
||||
export async function searchStackTraces({
|
||||
client,
|
||||
sampleSize,
|
||||
rangeFrom,
|
||||
rangeTo,
|
||||
kuery,
|
||||
durationSeconds,
|
||||
co2PerKWH,
|
||||
datacenterPUE,
|
||||
|
@ -22,13 +19,13 @@ export async function searchStackTraces({
|
|||
pervCPUWattArm64,
|
||||
awsCostDiscountRate,
|
||||
costPervCPUPerHour,
|
||||
indices,
|
||||
stacktraceIdsField,
|
||||
query,
|
||||
showErrorFrames,
|
||||
}: {
|
||||
client: ProfilingESClient;
|
||||
sampleSize: number;
|
||||
rangeFrom: number;
|
||||
rangeTo: number;
|
||||
kuery: string;
|
||||
durationSeconds: number;
|
||||
co2PerKWH: number;
|
||||
datacenterPUE: number;
|
||||
|
@ -36,26 +33,13 @@ export async function searchStackTraces({
|
|||
pervCPUWattArm64: number;
|
||||
awsCostDiscountRate: number;
|
||||
costPervCPUPerHour: number;
|
||||
indices?: string[];
|
||||
stacktraceIdsField?: string;
|
||||
query: QueryDslQueryContainer;
|
||||
showErrorFrames: boolean;
|
||||
}) {
|
||||
const response = await client.profilingStacktraces({
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
...kqlQuery(kuery),
|
||||
{
|
||||
range: {
|
||||
['@timestamp']: {
|
||||
gte: String(rangeFrom),
|
||||
lt: String(rangeTo),
|
||||
format: 'epoch_second',
|
||||
boost: 1.0,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
query,
|
||||
sampleSize,
|
||||
durationSeconds,
|
||||
co2PerKWH,
|
||||
|
@ -64,6 +48,8 @@ export async function searchStackTraces({
|
|||
pervCPUWattArm64,
|
||||
awsCostDiscountRate,
|
||||
costPervCPUPerHour,
|
||||
indices,
|
||||
stacktraceIdsField,
|
||||
});
|
||||
|
||||
return decodeStackTraceResponse(response, showErrorFrames);
|
||||
|
|
|
@ -49,6 +49,8 @@ export function createProfilingEsClient({
|
|||
costPervCPUPerHour,
|
||||
pervCPUWattArm64,
|
||||
pervCPUWattX86,
|
||||
indices,
|
||||
stacktraceIdsField,
|
||||
}) {
|
||||
const controller = new AbortController();
|
||||
const promise = withProfilingSpan('_profiling/stacktraces', () => {
|
||||
|
@ -66,6 +68,8 @@ export function createProfilingEsClient({
|
|||
datacenter_pue: datacenterPUE,
|
||||
aws_cost_factor: awsCostDiscountRate,
|
||||
cost_per_core_hour: costPervCPUPerHour,
|
||||
indices,
|
||||
stacktrace_ids_field: stacktraceIdsField,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -110,6 +114,8 @@ export function createProfilingEsClient({
|
|||
costPervCPUPerHour,
|
||||
pervCPUWattArm64,
|
||||
pervCPUWattX86,
|
||||
indices,
|
||||
stacktraceIdsField,
|
||||
}) {
|
||||
const controller = new AbortController();
|
||||
|
||||
|
@ -128,6 +134,8 @@ export function createProfilingEsClient({
|
|||
datacenter_pue: datacenterPUE,
|
||||
aws_cost_factor: awsCostDiscountRate,
|
||||
cost_per_core_hour: costPervCPUPerHour,
|
||||
indices,
|
||||
stacktrace_ids_field: stacktraceIdsField,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue