mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Profiling-APM] Service Profiling Top 10 Functions (#166226)
- Move logic to fetch the TopN functions to 'profiling-data-access-plugin' - Create new TopN functions embeddable - Refactor Universal profiling page on APM adding Tabs (Flamegraph | Top 10 functions) - Create a new API on APM to fetch Top functions <img width="1605" alt="Screenshot 2023-09-08 at 11 14 13" src="76fc7bf3
-094b-438b-99b8-3cab01539eb4"> <img width="1615" alt="Screenshot 2023-09-08 at 11 14 20" src="c4d3e97a
-eee0-4829-8d6f-545f9c844b18"> --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Joseph Crail <joseph.crail@elastic.co>
This commit is contained in:
parent
d0b759a47a
commit
7e25900bc5
39 changed files with 1074 additions and 248 deletions
3
.github/CODEOWNERS
vendored
3
.github/CODEOWNERS
vendored
|
@ -1398,6 +1398,9 @@ x-pack/plugins/translations/translations
|
|||
# Profiling api integration testing
|
||||
x-pack/test/profiling_api_integration @elastic/profiling-ui
|
||||
|
||||
# Observability shared profiling
|
||||
x-pack/plugins/observability_shared/public/components/profiling @elastic/profiling-ui
|
||||
|
||||
# Shared UX
|
||||
packages/react @elastic/appex-sharedux
|
||||
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
/*
|
||||
* 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.
|
||||
* 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 { sum } from 'lodash';
|
||||
import { createTopNFunctions } from './functions';
|
||||
import { decodeStackTraceResponse } from '@kbn/profiling-utils';
|
||||
import { decodeStackTraceResponse } from '..';
|
||||
import { stackTraceFixtures } from './__fixtures__/stacktraces';
|
||||
|
||||
describe('TopN function operations', () => {
|
|
@ -1,9 +1,12 @@
|
|||
/*
|
||||
* 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.
|
||||
* 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 * as t from 'io-ts';
|
||||
import { sumBy } from 'lodash';
|
||||
import type {
|
||||
Executable,
|
||||
FileID,
|
||||
|
@ -13,16 +16,14 @@ import type {
|
|||
StackFrameMetadata,
|
||||
StackTrace,
|
||||
StackTraceID,
|
||||
} from '@kbn/profiling-utils';
|
||||
} from '..';
|
||||
import {
|
||||
createFrameGroupID,
|
||||
createStackFrameMetadata,
|
||||
emptyExecutable,
|
||||
emptyStackFrame,
|
||||
emptyStackTrace,
|
||||
} from '@kbn/profiling-utils';
|
||||
import * as t from 'io-ts';
|
||||
import { sumBy } from 'lodash';
|
||||
} from '..';
|
||||
|
||||
interface TopNFunctionAndFrameGroup {
|
||||
Frame: StackFrameMetadata;
|
|
@ -26,6 +26,11 @@ export {
|
|||
} from './common/profiling';
|
||||
export { getFieldNameForTopNType, TopNType, StackTracesDisplayOption } from './common/stack_traces';
|
||||
export { createFrameGroupID } from './common/frame_group';
|
||||
export {
|
||||
createTopNFunctions,
|
||||
TopNFunctionSortField,
|
||||
topNFunctionSortFieldRt,
|
||||
} from './common/functions';
|
||||
|
||||
export type { CalleeTree } from './common/callee';
|
||||
export type {
|
||||
|
@ -44,3 +49,4 @@ export type {
|
|||
StackTrace,
|
||||
StackTraceID,
|
||||
} from './common/profiling';
|
||||
export type { TopNFunctions } from './common/functions';
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* 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 { toKueryFilterFormat } from './to_kuery_filter_format';
|
||||
|
||||
describe('toKueryFilterFormat', () => {
|
||||
it('returns a single value', () => {
|
||||
expect(toKueryFilterFormat('key', ['foo'])).toEqual(`key : "foo"`);
|
||||
});
|
||||
|
||||
it('returns multiple values default separator', () => {
|
||||
expect(toKueryFilterFormat('key', ['foo', 'bar', 'baz'])).toEqual(
|
||||
`key : "foo" OR key : "bar" OR key : "baz"`
|
||||
);
|
||||
});
|
||||
|
||||
it('returns multiple values custom separator', () => {
|
||||
expect(toKueryFilterFormat('key', ['foo', 'bar', 'baz'], 'AND')).toEqual(
|
||||
`key : "foo" AND key : "bar" AND key : "baz"`
|
||||
);
|
||||
});
|
||||
|
||||
it('return empty string when no hostname', () => {
|
||||
expect(toKueryFilterFormat('key', [])).toEqual('');
|
||||
});
|
||||
});
|
14
x-pack/plugins/apm/common/utils/to_kuery_filter_format.ts
Normal file
14
x-pack/plugins/apm/common/utils/to_kuery_filter_format.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export function toKueryFilterFormat(
|
||||
key: string,
|
||||
values: string[],
|
||||
separator: 'OR' | 'AND' = 'OR'
|
||||
) {
|
||||
return values.map((value) => `${key} : "${value}"`).join(` ${separator} `);
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* 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,
|
||||
EuiIcon,
|
||||
EuiText,
|
||||
EuiToolTip,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
|
||||
interface Props {
|
||||
hostNames?: string[];
|
||||
}
|
||||
export function HostnamesFilterWarning({ hostNames = [] }: Props) {
|
||||
function renderTooltipOptions() {
|
||||
return (
|
||||
<ul>
|
||||
{hostNames.map((hostName) => (
|
||||
<li key={hostName}>{`- ${hostName}`}</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiFlexGroup gutterSize="none">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText size="xs" color="subdued">
|
||||
{i18n.translate('xpack.apm.profiling.flamegraph.filteredLabel', {
|
||||
defaultMessage: 'Displaying items from specific host names',
|
||||
})}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiToolTip content={renderTooltipOptions()}>
|
||||
<EuiIcon type="questionInCircle" />
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
|
@ -5,22 +5,27 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EmbeddableFlamegraph } from '@kbn/observability-shared-plugin/public';
|
||||
import React from 'react';
|
||||
import {
|
||||
EuiSpacer,
|
||||
EuiTabbedContent,
|
||||
EuiTabbedContentProps,
|
||||
} from '@elastic/eui';
|
||||
import React, { useMemo } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useApmParams } from '../../../hooks/use_apm_params';
|
||||
import { isPending, useFetcher } from '../../../hooks/use_fetcher';
|
||||
import { useProfilingPlugin } from '../../../hooks/use_profiling_plugin';
|
||||
import { useTimeRange } from '../../../hooks/use_time_range';
|
||||
import { ApmDocumentType } from '../../../../common/document_type';
|
||||
import { ProfilingFlamegraph } from './profiling_flamegraph';
|
||||
import { ProfilingTopNFunctions } from './profiling_top_functions';
|
||||
import { usePreferredDataSourceAndBucketSize } from '../../../hooks/use_preferred_data_source_and_bucket_size';
|
||||
import { ApmDocumentType } from '../../../../common/document_type';
|
||||
|
||||
export function ProfilingOverview() {
|
||||
const {
|
||||
path: { serviceName },
|
||||
query: { kuery, rangeFrom, rangeTo, environment },
|
||||
query: { rangeFrom, rangeTo, environment, kuery },
|
||||
} = useApmParams('/services/{serviceName}/profiling');
|
||||
const { isProfilingAvailable } = useProfilingPlugin();
|
||||
|
||||
const { start, end } = useTimeRange({ rangeFrom, rangeTo });
|
||||
const preferred = usePreferredDataSourceAndBucketSize({
|
||||
start,
|
||||
|
@ -29,47 +34,59 @@ export function ProfilingOverview() {
|
|||
type: ApmDocumentType.TransactionMetric,
|
||||
numBuckets: 20,
|
||||
});
|
||||
const { data, status } = useFetcher(
|
||||
(callApmApi) => {
|
||||
if (isProfilingAvailable && preferred) {
|
||||
return callApmApi(
|
||||
'GET /internal/apm/services/{serviceName}/profiling/flamegraph',
|
||||
{
|
||||
params: {
|
||||
path: { serviceName },
|
||||
query: {
|
||||
start,
|
||||
end,
|
||||
kuery,
|
||||
environment,
|
||||
documentType: preferred.source.documentType,
|
||||
rollupInterval: preferred.source.rollupInterval,
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
},
|
||||
[
|
||||
isProfilingAvailable,
|
||||
preferred,
|
||||
serviceName,
|
||||
start,
|
||||
end,
|
||||
kuery,
|
||||
environment,
|
||||
]
|
||||
);
|
||||
|
||||
const tabs = useMemo((): EuiTabbedContentProps['tabs'] => {
|
||||
return [
|
||||
{
|
||||
id: 'flamegraph',
|
||||
name: i18n.translate('xpack.apm.profiling.tabs.flamegraph', {
|
||||
defaultMessage: 'Flamegraph',
|
||||
}),
|
||||
content: (
|
||||
<>
|
||||
<EuiSpacer />
|
||||
<ProfilingFlamegraph
|
||||
serviceName={serviceName}
|
||||
start={start}
|
||||
end={end}
|
||||
environment={environment}
|
||||
dataSource={preferred?.source}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'topNFunctions',
|
||||
name: i18n.translate('xpack.apm.profiling.tabs.topNFunctions', {
|
||||
defaultMessage: 'Top 10 Functions',
|
||||
}),
|
||||
content: (
|
||||
<>
|
||||
<EuiSpacer />
|
||||
<ProfilingTopNFunctions
|
||||
serviceName={serviceName}
|
||||
start={start}
|
||||
end={end}
|
||||
environment={environment}
|
||||
startIndex={0}
|
||||
endIndex={10}
|
||||
dataSource={preferred?.source}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
},
|
||||
];
|
||||
}, [end, environment, preferred?.source, serviceName, start]);
|
||||
|
||||
if (!isProfilingAvailable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<EmbeddableFlamegraph
|
||||
data={data}
|
||||
height="60vh"
|
||||
isLoading={isPending(status)}
|
||||
<EuiTabbedContent
|
||||
tabs={tabs}
|
||||
initialSelectedTab={tabs[0]}
|
||||
autoFocus="selected"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,96 @@
|
|||
/*
|
||||
* 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, EuiLink, EuiSpacer } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EmbeddableFlamegraph } from '@kbn/observability-shared-plugin/public';
|
||||
import React from 'react';
|
||||
import { HOST_NAME } from '../../../../common/es_fields/apm';
|
||||
import { toKueryFilterFormat } from '../../../../common/utils/to_kuery_filter_format';
|
||||
import { isPending, useFetcher } from '../../../hooks/use_fetcher';
|
||||
import { useProfilingPlugin } from '../../../hooks/use_profiling_plugin';
|
||||
import { HostnamesFilterWarning } from './host_names_filter_warning';
|
||||
import { ApmDataSourceWithSummary } from '../../../../common/data_source';
|
||||
import { ApmDocumentType } from '../../../../common/document_type';
|
||||
|
||||
interface Props {
|
||||
serviceName: string;
|
||||
start: string;
|
||||
end: string;
|
||||
environment: string;
|
||||
dataSource?: ApmDataSourceWithSummary<
|
||||
ApmDocumentType.TransactionMetric | ApmDocumentType.TransactionEvent
|
||||
>;
|
||||
}
|
||||
|
||||
export function ProfilingFlamegraph({
|
||||
start,
|
||||
end,
|
||||
serviceName,
|
||||
environment,
|
||||
dataSource,
|
||||
}: Props) {
|
||||
const { profilingLocators } = useProfilingPlugin();
|
||||
|
||||
const { data, status } = useFetcher(
|
||||
(callApmApi) => {
|
||||
if (dataSource) {
|
||||
return callApmApi(
|
||||
'GET /internal/apm/services/{serviceName}/profiling/flamegraph',
|
||||
{
|
||||
params: {
|
||||
path: { serviceName },
|
||||
query: {
|
||||
start,
|
||||
end,
|
||||
environment,
|
||||
documentType: dataSource.documentType,
|
||||
rollupInterval: dataSource.rollupInterval,
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
},
|
||||
[dataSource, serviceName, start, end, environment]
|
||||
);
|
||||
|
||||
const hostNamesKueryFormat = toKueryFilterFormat(
|
||||
HOST_NAME,
|
||||
data?.hostNames || []
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={false}>
|
||||
<HostnamesFilterWarning hostNames={data?.hostNames} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||
<EuiLink
|
||||
data-test-subj="apmProfilingFlamegraphGoToFlamegraphLink"
|
||||
href={profilingLocators?.flamegraphLocator.getRedirectUrl({
|
||||
kuery: hostNamesKueryFormat,
|
||||
})}
|
||||
>
|
||||
{i18n.translate('xpack.apm.profiling.flamegraph.link', {
|
||||
defaultMessage: 'Go to Universal Profiling Flamegraph',
|
||||
})}
|
||||
</EuiLink>
|
||||
</div>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer />
|
||||
<EmbeddableFlamegraph
|
||||
data={data?.flamegraph}
|
||||
isLoading={isPending(status)}
|
||||
height="60vh"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,103 @@
|
|||
/*
|
||||
* 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, EuiLink, EuiSpacer } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EmbeddableFunctions } from '@kbn/observability-shared-plugin/public';
|
||||
import React from 'react';
|
||||
import { HOST_NAME } from '../../../../common/es_fields/apm';
|
||||
import { toKueryFilterFormat } from '../../../../common/utils/to_kuery_filter_format';
|
||||
import { isPending, useFetcher } from '../../../hooks/use_fetcher';
|
||||
import { useProfilingPlugin } from '../../../hooks/use_profiling_plugin';
|
||||
import { HostnamesFilterWarning } from './host_names_filter_warning';
|
||||
import { ApmDataSourceWithSummary } from '../../../../common/data_source';
|
||||
import { ApmDocumentType } from '../../../../common/document_type';
|
||||
|
||||
interface Props {
|
||||
serviceName: string;
|
||||
start: string;
|
||||
end: string;
|
||||
environment: string;
|
||||
startIndex: number;
|
||||
endIndex: number;
|
||||
dataSource?: ApmDataSourceWithSummary<
|
||||
ApmDocumentType.TransactionMetric | ApmDocumentType.TransactionEvent
|
||||
>;
|
||||
}
|
||||
|
||||
export function ProfilingTopNFunctions({
|
||||
serviceName,
|
||||
start,
|
||||
end,
|
||||
environment,
|
||||
startIndex,
|
||||
endIndex,
|
||||
dataSource,
|
||||
}: Props) {
|
||||
const { profilingLocators } = useProfilingPlugin();
|
||||
|
||||
const { data, status } = useFetcher(
|
||||
(callApmApi) => {
|
||||
if (dataSource) {
|
||||
return callApmApi(
|
||||
'GET /internal/apm/services/{serviceName}/profiling/functions',
|
||||
{
|
||||
params: {
|
||||
path: { serviceName },
|
||||
query: {
|
||||
start,
|
||||
end,
|
||||
environment,
|
||||
startIndex,
|
||||
endIndex,
|
||||
documentType: dataSource.documentType,
|
||||
rollupInterval: dataSource.rollupInterval,
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
},
|
||||
[dataSource, serviceName, start, end, environment, startIndex, endIndex]
|
||||
);
|
||||
|
||||
const hostNamesKueryFormat = toKueryFilterFormat(
|
||||
HOST_NAME,
|
||||
data?.hostNames || []
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={false}>
|
||||
<HostnamesFilterWarning hostNames={data?.hostNames} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||
<EuiLink
|
||||
data-test-subj="apmProfilingTopNFunctionsGoToUniversalProfilingFlamegraphLink"
|
||||
href={profilingLocators?.topNFunctionsLocator.getRedirectUrl({
|
||||
kuery: hostNamesKueryFormat,
|
||||
})}
|
||||
>
|
||||
{i18n.translate('xpack.apm.profiling.topnFunctions.link', {
|
||||
defaultMessage: 'Go to Universal Profiling Functions',
|
||||
})}
|
||||
</EuiLink>
|
||||
</div>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer />
|
||||
<EmbeddableFunctions
|
||||
data={data?.functions}
|
||||
isLoading={isPending(status)}
|
||||
rangeFrom={new Date(start).valueOf()}
|
||||
rangeTo={new Date(end).valueOf()}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -4,7 +4,7 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { kqlQuery, rangeQuery } from '@kbn/observability-plugin/server';
|
||||
import { rangeQuery } from '@kbn/observability-plugin/server';
|
||||
import { ApmServiceTransactionDocumentType } from '../../../common/document_type';
|
||||
import { HOST_HOSTNAME, SERVICE_NAME } from '../../../common/es_fields/apm';
|
||||
import { RollupInterval } from '../../../common/rollup';
|
||||
|
@ -17,12 +17,10 @@ export async function getServiceHostNames({
|
|||
start,
|
||||
end,
|
||||
environment,
|
||||
kuery,
|
||||
documentType,
|
||||
rollupInterval,
|
||||
}: {
|
||||
environment: string;
|
||||
kuery: string;
|
||||
serviceName: string;
|
||||
start: number;
|
||||
end: number;
|
||||
|
@ -43,7 +41,6 @@ export async function getServiceHostNames({
|
|||
{ term: { [SERVICE_NAME]: serviceName } },
|
||||
...rangeQuery(start, end),
|
||||
...environmentQuery(environment),
|
||||
...kqlQuery(kuery),
|
||||
],
|
||||
},
|
||||
},
|
||||
|
|
|
@ -5,18 +5,19 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { toNumberRt } from '@kbn/io-ts-utils';
|
||||
import type { BaseFlameGraph, TopNFunctions } from '@kbn/profiling-utils';
|
||||
import * as t from 'io-ts';
|
||||
import type { BaseFlameGraph } from '@kbn/profiling-utils';
|
||||
import { HOST_NAME } from '../../../common/es_fields/apm';
|
||||
import { toKueryFilterFormat } from '../../../common/utils/to_kuery_filter_format';
|
||||
import { getApmEventClient } from '../../lib/helpers/get_apm_event_client';
|
||||
import { createApmServerRoute } from '../apm_routes/create_apm_server_route';
|
||||
import {
|
||||
environmentRt,
|
||||
kueryRt,
|
||||
rangeRt,
|
||||
serviceTransactionDataSourceRt,
|
||||
} from '../default_api_types';
|
||||
import { getApmEventClient } from '../../lib/helpers/get_apm_event_client';
|
||||
import { getServiceHostNames } from './get_service_host_names';
|
||||
import { hostNamesToKuery } from './utils';
|
||||
|
||||
const profilingFlamegraphRoute = createApmServerRoute({
|
||||
endpoint: 'GET /internal/apm/services/{serviceName}/profiling/flamegraph',
|
||||
|
@ -24,13 +25,16 @@ const profilingFlamegraphRoute = createApmServerRoute({
|
|||
path: t.type({ serviceName: t.string }),
|
||||
query: t.intersection([
|
||||
rangeRt,
|
||||
kueryRt,
|
||||
environmentRt,
|
||||
serviceTransactionDataSourceRt,
|
||||
]),
|
||||
}),
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async (resources): Promise<BaseFlameGraph | undefined> => {
|
||||
handler: async (
|
||||
resources
|
||||
): Promise<
|
||||
{ flamegraph: BaseFlameGraph; hostNames: string[] } | undefined
|
||||
> => {
|
||||
const { context, plugins, params } = resources;
|
||||
const [esClient, apmEventClient, profilingDataAccessStart] =
|
||||
await Promise.all([
|
||||
|
@ -39,7 +43,7 @@ const profilingFlamegraphRoute = createApmServerRoute({
|
|||
await plugins.profilingDataAccess?.start(),
|
||||
]);
|
||||
if (profilingDataAccessStart) {
|
||||
const { start, end, kuery, environment, documentType, rollupInterval } =
|
||||
const { start, end, environment, documentType, rollupInterval } =
|
||||
params.query;
|
||||
const { serviceName } = params.path;
|
||||
|
||||
|
@ -47,19 +51,80 @@ const profilingFlamegraphRoute = createApmServerRoute({
|
|||
apmEventClient,
|
||||
start,
|
||||
end,
|
||||
kuery,
|
||||
environment,
|
||||
serviceName,
|
||||
documentType,
|
||||
rollupInterval,
|
||||
});
|
||||
|
||||
return profilingDataAccessStart?.services.fetchFlamechartData({
|
||||
const flamegraph =
|
||||
await profilingDataAccessStart?.services.fetchFlamechartData({
|
||||
esClient: esClient.asCurrentUser,
|
||||
rangeFromMs: start,
|
||||
rangeToMs: end,
|
||||
kuery: toKueryFilterFormat(HOST_NAME, serviceHostNames),
|
||||
});
|
||||
|
||||
return { flamegraph, hostNames: serviceHostNames };
|
||||
}
|
||||
|
||||
return undefined;
|
||||
},
|
||||
});
|
||||
|
||||
const profilingFunctionsRoute = createApmServerRoute({
|
||||
endpoint: 'GET /internal/apm/services/{serviceName}/profiling/functions',
|
||||
params: t.type({
|
||||
path: t.type({ serviceName: t.string }),
|
||||
query: t.intersection([
|
||||
rangeRt,
|
||||
environmentRt,
|
||||
serviceTransactionDataSourceRt,
|
||||
t.type({ startIndex: toNumberRt, endIndex: toNumberRt }),
|
||||
]),
|
||||
}),
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async (
|
||||
resources
|
||||
): Promise<{ functions: TopNFunctions; hostNames: string[] } | undefined> => {
|
||||
const { context, plugins, params } = resources;
|
||||
const [esClient, apmEventClient, profilingDataAccessStart] =
|
||||
await Promise.all([
|
||||
(await context.core).elasticsearch.client,
|
||||
await getApmEventClient(resources),
|
||||
await plugins.profilingDataAccess?.start(),
|
||||
]);
|
||||
if (profilingDataAccessStart) {
|
||||
const {
|
||||
start,
|
||||
end,
|
||||
environment,
|
||||
startIndex,
|
||||
endIndex,
|
||||
documentType,
|
||||
rollupInterval,
|
||||
} = params.query;
|
||||
const { serviceName } = params.path;
|
||||
|
||||
const serviceHostNames = await getServiceHostNames({
|
||||
apmEventClient,
|
||||
start,
|
||||
end,
|
||||
environment,
|
||||
serviceName,
|
||||
documentType,
|
||||
rollupInterval,
|
||||
});
|
||||
|
||||
const functions = await profilingDataAccessStart?.services.fetchFunction({
|
||||
esClient: esClient.asCurrentUser,
|
||||
rangeFromMs: start,
|
||||
rangeToMs: end,
|
||||
kuery: hostNamesToKuery(serviceHostNames),
|
||||
kuery: toKueryFilterFormat(HOST_NAME, serviceHostNames),
|
||||
startIndex,
|
||||
endIndex,
|
||||
});
|
||||
return { functions, hostNames: serviceHostNames };
|
||||
}
|
||||
|
||||
return undefined;
|
||||
|
@ -68,4 +133,5 @@ const profilingFlamegraphRoute = createApmServerRoute({
|
|||
|
||||
export const profilingRouteRepository = {
|
||||
...profilingFlamegraphRoute,
|
||||
...profilingFunctionsRoute,
|
||||
};
|
||||
|
|
|
@ -5,59 +5,13 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import type { BaseFlameGraph } from '@kbn/profiling-utils';
|
||||
import { css } from '@emotion/react';
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import { ObservabilitySharedStart } from '../../../plugin';
|
||||
import React from 'react';
|
||||
import { ProfilingEmbeddable, ProfilingEmbeddableProps } from './profiling_embeddable';
|
||||
import { EMBEDDABLE_FLAMEGRAPH } from '.';
|
||||
|
||||
interface Props {
|
||||
data?: BaseFlameGraph;
|
||||
height?: string;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
export function EmbeddableFlamegraph({ data, height, isLoading }: Props) {
|
||||
const { embeddable: embeddablePlugin } = useKibana<ObservabilitySharedStart>().services;
|
||||
const [embeddable, setEmbeddable] = useState<any>();
|
||||
const embeddableRoot: React.RefObject<HTMLDivElement> = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
async function createEmbeddable() {
|
||||
const factory = embeddablePlugin?.getEmbeddableFactory(EMBEDDABLE_FLAMEGRAPH);
|
||||
const input = { id: 'embeddable_profiling', data, isLoading };
|
||||
const embeddableObject = await factory?.create(input);
|
||||
setEmbeddable(embeddableObject);
|
||||
}
|
||||
createEmbeddable();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (embeddableRoot.current && embeddable) {
|
||||
embeddable.render(embeddableRoot.current);
|
||||
}
|
||||
}, [embeddable, embeddableRoot]);
|
||||
|
||||
useEffect(() => {
|
||||
if (embeddable) {
|
||||
embeddable.updateInput({ data, isLoading });
|
||||
embeddable.reload();
|
||||
}
|
||||
}, [data, embeddable, isLoading]);
|
||||
|
||||
return (
|
||||
<div
|
||||
css={css`
|
||||
width: 100%;
|
||||
height: ${height};
|
||||
display: flex;
|
||||
flex: 1 1 100%;
|
||||
z-index: 1;
|
||||
min-height: 0;
|
||||
`}
|
||||
ref={embeddableRoot}
|
||||
/>
|
||||
);
|
||||
type Props = Omit<ProfilingEmbeddableProps<BaseFlameGraph>, 'embeddableFactoryId'>;
|
||||
|
||||
export function EmbeddableFlamegraph(props: Props) {
|
||||
return <ProfilingEmbeddable {...props} embeddableFactoryId={EMBEDDABLE_FLAMEGRAPH} />;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* 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 type { TopNFunctions } from '@kbn/profiling-utils';
|
||||
import React from 'react';
|
||||
import { ProfilingEmbeddable, ProfilingEmbeddableProps } from './profiling_embeddable';
|
||||
import { EMBEDDABLE_FUNCTIONS } from '.';
|
||||
|
||||
type Props = Omit<ProfilingEmbeddableProps<TopNFunctions>, 'embeddableFactoryId'> & {
|
||||
rangeFrom: number;
|
||||
rangeTo: number;
|
||||
};
|
||||
|
||||
export function EmbeddableFunctions(props: Props) {
|
||||
return <ProfilingEmbeddable {...props} embeddableFactoryId={EMBEDDABLE_FUNCTIONS} />;
|
||||
}
|
|
@ -7,3 +7,10 @@
|
|||
|
||||
/** Profiling flamegraph embeddable key */
|
||||
export const EMBEDDABLE_FLAMEGRAPH = 'EMBEDDABLE_FLAMEGRAPH';
|
||||
/** Profiling flamegraph embeddable */
|
||||
export { EmbeddableFlamegraph } from './embeddable_flamegraph';
|
||||
|
||||
/** Profiling functions embeddable key */
|
||||
export const EMBEDDABLE_FUNCTIONS = 'EMBEDDABLE_FUNCTIONS';
|
||||
/** Profiling functions embeddable */
|
||||
export { EmbeddableFunctions } from './embeddable_functions';
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* 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 { css } from '@emotion/react';
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { ObservabilitySharedStart } from '../../../plugin';
|
||||
|
||||
export interface ProfilingEmbeddableProps<T> {
|
||||
data?: T;
|
||||
embeddableFactoryId: string;
|
||||
isLoading: boolean;
|
||||
height?: string;
|
||||
}
|
||||
|
||||
export function ProfilingEmbeddable<T>({
|
||||
embeddableFactoryId,
|
||||
data,
|
||||
isLoading,
|
||||
height,
|
||||
...props
|
||||
}: ProfilingEmbeddableProps<T>) {
|
||||
const { embeddable: embeddablePlugin } = useKibana<ObservabilitySharedStart>().services;
|
||||
const [embeddable, setEmbeddable] = useState<any>();
|
||||
const embeddableRoot: React.RefObject<HTMLDivElement> = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
async function createEmbeddable() {
|
||||
const factory = embeddablePlugin?.getEmbeddableFactory(embeddableFactoryId);
|
||||
const input = { id: 'embeddable_profiling', data, isLoading };
|
||||
const embeddableObject = await factory?.create(input);
|
||||
setEmbeddable(embeddableObject);
|
||||
}
|
||||
createEmbeddable();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (embeddableRoot.current && embeddable) {
|
||||
embeddable.render(embeddableRoot.current);
|
||||
}
|
||||
}, [embeddable, embeddableRoot]);
|
||||
|
||||
useEffect(() => {
|
||||
if (embeddable) {
|
||||
embeddable.updateInput({ data, isLoading, ...props });
|
||||
embeddable.reload();
|
||||
}
|
||||
}, [data, embeddable, isLoading, props]);
|
||||
|
||||
return (
|
||||
<div
|
||||
css={css`
|
||||
width: 100%;
|
||||
height: ${height};
|
||||
display: flex;
|
||||
flex: 1 1 100%;
|
||||
z-index: 1;
|
||||
min-height: 0;
|
||||
`}
|
||||
ref={embeddableRoot}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -78,5 +78,9 @@ export {
|
|||
sloFeatureId,
|
||||
} from '../common';
|
||||
|
||||
export { EMBEDDABLE_FLAMEGRAPH } from './components/profiling/embeddables';
|
||||
export { EmbeddableFlamegraph } from './components/profiling/embeddables/embeddable_flamegraph';
|
||||
export {
|
||||
EMBEDDABLE_FLAMEGRAPH,
|
||||
EMBEDDABLE_FUNCTIONS,
|
||||
EmbeddableFlamegraph,
|
||||
EmbeddableFunctions,
|
||||
} from './components/profiling/embeddables';
|
||||
|
|
|
@ -16,7 +16,7 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { TopNFunctionSortField } from '../../../common/functions';
|
||||
import { TopNFunctionSortField } from '@kbn/profiling-utils';
|
||||
import { asCost } from '../../utils/formatters/as_cost';
|
||||
import { asWeight } from '../../utils/formatters/as_weight';
|
||||
import { StackFrameSummary } from '../stack_frame_summary';
|
||||
|
|
|
@ -20,7 +20,7 @@ import { last } from 'lodash';
|
|||
import React, { forwardRef, Ref, useMemo, useState } from 'react';
|
||||
import { GridOnScrollProps } from 'react-window';
|
||||
import { useUiTracker } from '@kbn/observability-shared-plugin/public';
|
||||
import { TopNFunctions, TopNFunctionSortField } from '../../../common/functions';
|
||||
import { TopNFunctions, TopNFunctionSortField } from '@kbn/profiling-utils';
|
||||
import { CPULabelWithHint } from '../cpu_label_with_hint';
|
||||
import { FrameInformationTooltip } from '../frame_information_window/frame_information_tooltip';
|
||||
import { LabelWithHint } from '../label_with_hint';
|
||||
|
@ -43,6 +43,7 @@ interface Props {
|
|||
sortDirection: 'asc' | 'desc';
|
||||
onChangeSort: (sorting: EuiDataGridSorting['columns'][0]) => void;
|
||||
dataTestSubj?: string;
|
||||
isEmbedded?: boolean;
|
||||
}
|
||||
|
||||
export const TopNFunctionsGrid = forwardRef(
|
||||
|
@ -63,6 +64,7 @@ export const TopNFunctionsGrid = forwardRef(
|
|||
sortDirection,
|
||||
onChangeSort,
|
||||
dataTestSubj = 'topNFunctionsGrid',
|
||||
isEmbedded = false,
|
||||
}: Props,
|
||||
ref: Ref<EuiDataGridRefProps> | undefined
|
||||
) => {
|
||||
|
@ -270,7 +272,7 @@ export const TopNFunctionsGrid = forwardRef(
|
|||
aria-label="TopN functions"
|
||||
columns={columns}
|
||||
columnVisibility={{ visibleColumns, setVisibleColumns }}
|
||||
rowCount={totalCount > 100 ? 100 : totalCount}
|
||||
rowCount={rows.length}
|
||||
renderCellValue={RenderCellValue}
|
||||
inMemory={{ level: 'sorting' }}
|
||||
sorting={{ columns: [{ id: sortField, direction: sortDirection }], onSort }}
|
||||
|
@ -281,6 +283,7 @@ export const TopNFunctionsGrid = forwardRef(
|
|||
// Left it empty on purpose as it is a required property on the pagination
|
||||
onChangeItemsPerPage: () => {},
|
||||
onChangePage,
|
||||
pageSizeOptions: [],
|
||||
}}
|
||||
rowHeightsOptions={{ defaultHeight: 'auto' }}
|
||||
toolbarVisibility={{
|
||||
|
@ -335,6 +338,8 @@ export const TopNFunctionsGrid = forwardRef(
|
|||
}}
|
||||
totalSeconds={totalSeconds}
|
||||
totalSamples={totalCount}
|
||||
showAIAssistant={!isEmbedded}
|
||||
showSymbolsStatus={!isEmbedded}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { TopNFunctions } from '../../../../common/functions';
|
||||
import type { TopNFunctions } from '@kbn/profiling-utils';
|
||||
|
||||
export const data = {
|
||||
TotalCount: 4,
|
||||
|
|
|
@ -5,8 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
import { keyBy } from 'lodash';
|
||||
import type { StackFrameMetadata } from '@kbn/profiling-utils';
|
||||
import { TopNFunctions } from '../../../common/functions';
|
||||
import type { StackFrameMetadata, TopNFunctions } from '@kbn/profiling-utils';
|
||||
import { calculateImpactEstimates } from '../../../common/calculate_impact_estimates';
|
||||
|
||||
export function getColorLabel(percent: number) {
|
||||
|
@ -71,62 +70,65 @@ export function getFunctionsRows({
|
|||
? keyBy(comparisonTopNFunctions.TopN, 'Id')
|
||||
: {};
|
||||
|
||||
return topNFunctions.TopN.filter((topN) => topN.CountExclusive > 0).map((topN, i) => {
|
||||
const comparisonRow = comparisonDataById?.[topN.Id];
|
||||
return topNFunctions.TopN.filter((topN) => topN.CountExclusive > 0)
|
||||
.slice(0, 100)
|
||||
.map((topN, i) => {
|
||||
const comparisonRow = comparisonDataById?.[topN.Id];
|
||||
|
||||
const scaledSelfCPU = scaleValue({
|
||||
value: topN.CountExclusive,
|
||||
scaleFactor: baselineScaleFactor,
|
||||
});
|
||||
const scaledSelfCPU = scaleValue({
|
||||
value: topN.CountExclusive,
|
||||
scaleFactor: baselineScaleFactor,
|
||||
});
|
||||
|
||||
const totalCPUPerc = (topN.CountInclusive / topNFunctions.TotalCount) * 100;
|
||||
const selfCPUPerc = (topN.CountExclusive / topNFunctions.TotalCount) * 100;
|
||||
const totalCPUPerc = (topN.CountInclusive / topNFunctions.TotalCount) * 100;
|
||||
const selfCPUPerc = (topN.CountExclusive / topNFunctions.TotalCount) * 100;
|
||||
|
||||
const impactEstimates =
|
||||
totalSeconds > 0
|
||||
? calculateImpactEstimates({
|
||||
countExclusive: topN.CountExclusive,
|
||||
countInclusive: topN.CountInclusive,
|
||||
totalSamples: topNFunctions.TotalCount,
|
||||
totalSeconds,
|
||||
})
|
||||
: undefined;
|
||||
const impactEstimates =
|
||||
totalSeconds > 0
|
||||
? calculateImpactEstimates({
|
||||
countExclusive: topN.CountExclusive,
|
||||
countInclusive: topN.CountInclusive,
|
||||
totalSamples: topNFunctions.TotalCount,
|
||||
totalSeconds,
|
||||
})
|
||||
: undefined;
|
||||
|
||||
function calculateDiff() {
|
||||
if (comparisonTopNFunctions && comparisonRow) {
|
||||
const comparisonScaledSelfCPU = scaleValue({
|
||||
value: comparisonRow.CountExclusive,
|
||||
scaleFactor: comparisonScaleFactor,
|
||||
});
|
||||
function calculateDiff() {
|
||||
if (comparisonTopNFunctions && comparisonRow) {
|
||||
const comparisonScaledSelfCPU = scaleValue({
|
||||
value: comparisonRow.CountExclusive,
|
||||
scaleFactor: comparisonScaleFactor,
|
||||
});
|
||||
|
||||
const scaledDiffSamples = scaledSelfCPU - comparisonScaledSelfCPU;
|
||||
const scaledDiffSamples = scaledSelfCPU - comparisonScaledSelfCPU;
|
||||
|
||||
return {
|
||||
rank: topN.Rank - comparisonRow.Rank,
|
||||
samples: scaledDiffSamples,
|
||||
selfCPU: comparisonRow.CountExclusive,
|
||||
totalCPU: comparisonRow.CountInclusive,
|
||||
selfCPUPerc:
|
||||
selfCPUPerc - (comparisonRow.CountExclusive / comparisonTopNFunctions.TotalCount) * 100,
|
||||
totalCPUPerc:
|
||||
totalCPUPerc -
|
||||
(comparisonRow.CountInclusive / comparisonTopNFunctions.TotalCount) * 100,
|
||||
};
|
||||
return {
|
||||
rank: topN.Rank - comparisonRow.Rank,
|
||||
samples: scaledDiffSamples,
|
||||
selfCPU: comparisonRow.CountExclusive,
|
||||
totalCPU: comparisonRow.CountInclusive,
|
||||
selfCPUPerc:
|
||||
selfCPUPerc -
|
||||
(comparisonRow.CountExclusive / comparisonTopNFunctions.TotalCount) * 100,
|
||||
totalCPUPerc:
|
||||
totalCPUPerc -
|
||||
(comparisonRow.CountInclusive / comparisonTopNFunctions.TotalCount) * 100,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
rank: topN.Rank,
|
||||
frame: topN.Frame,
|
||||
samples: scaledSelfCPU,
|
||||
selfCPUPerc,
|
||||
totalCPUPerc,
|
||||
selfCPU: topN.CountExclusive,
|
||||
totalCPU: topN.CountInclusive,
|
||||
impactEstimates,
|
||||
diff: calculateDiff(),
|
||||
};
|
||||
});
|
||||
return {
|
||||
rank: topN.Rank,
|
||||
frame: topN.Frame,
|
||||
samples: scaledSelfCPU,
|
||||
selfCPUPerc,
|
||||
totalCPUPerc,
|
||||
selfCPU: topN.CountExclusive,
|
||||
totalCPU: topN.CountInclusive,
|
||||
impactEstimates,
|
||||
diff: calculateDiff(),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function calculateBaseComparisonDiff({
|
||||
|
|
|
@ -8,8 +8,8 @@
|
|||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { useMemo } from 'react';
|
||||
import type { TopNFunctions } from '@kbn/profiling-utils';
|
||||
import { calculateImpactEstimates } from '../../../common/calculate_impact_estimates';
|
||||
import { TopNFunctions } from '../../../common/functions';
|
||||
import { asCost } from '../../utils/formatters/as_cost';
|
||||
import { asWeight } from '../../utils/formatters/as_weight';
|
||||
import { calculateBaseComparisonDiff } from '../topn_functions/utils';
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* 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 { Embeddable, EmbeddableOutput } from '@kbn/embeddable-plugin/public';
|
||||
import { EMBEDDABLE_FUNCTIONS } from '@kbn/observability-shared-plugin/public';
|
||||
import React from 'react';
|
||||
import { render, unmountComponentAtNode } from 'react-dom';
|
||||
import { AsyncEmbeddableComponent } from '../async_embeddable_component';
|
||||
import { EmbeddableFunctionsEmbeddableInput } from './embeddable_functions_factory';
|
||||
import { EmbeddableFunctionsGrid } from './embeddable_functions_grid';
|
||||
|
||||
export class EmbeddableFunctions extends Embeddable<
|
||||
EmbeddableFunctionsEmbeddableInput,
|
||||
EmbeddableOutput
|
||||
> {
|
||||
readonly type = EMBEDDABLE_FUNCTIONS;
|
||||
private _domNode?: HTMLElement;
|
||||
|
||||
render(domNode: HTMLElement) {
|
||||
this._domNode = domNode;
|
||||
const { data, isLoading, rangeFrom, rangeTo } = this.input;
|
||||
const totalSeconds = (rangeTo - rangeFrom) / 1000;
|
||||
render(
|
||||
<AsyncEmbeddableComponent isLoading={isLoading}>
|
||||
<div style={{ width: '100%' }}>
|
||||
<EmbeddableFunctionsGrid data={data} totalSeconds={totalSeconds} />
|
||||
</div>
|
||||
</AsyncEmbeddableComponent>,
|
||||
domNode
|
||||
);
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
if (this._domNode) {
|
||||
unmountComponentAtNode(this._domNode);
|
||||
}
|
||||
}
|
||||
|
||||
reload() {
|
||||
if (this._domNode) {
|
||||
this.render(this._domNode);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* 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 {
|
||||
EmbeddableFactoryDefinition,
|
||||
EmbeddableInput,
|
||||
IContainer,
|
||||
} from '@kbn/embeddable-plugin/public';
|
||||
import { EMBEDDABLE_FUNCTIONS } from '@kbn/observability-shared-plugin/public';
|
||||
import type { TopNFunctions } from '@kbn/profiling-utils';
|
||||
|
||||
interface EmbeddableFunctionsInput {
|
||||
data?: TopNFunctions;
|
||||
isLoading: boolean;
|
||||
rangeFrom: number;
|
||||
rangeTo: number;
|
||||
}
|
||||
|
||||
export type EmbeddableFunctionsEmbeddableInput = EmbeddableFunctionsInput & EmbeddableInput;
|
||||
|
||||
export class EmbeddableFunctionsFactory
|
||||
implements EmbeddableFactoryDefinition<EmbeddableFunctionsEmbeddableInput>
|
||||
{
|
||||
readonly type = EMBEDDABLE_FUNCTIONS;
|
||||
|
||||
async isEditable() {
|
||||
return false;
|
||||
}
|
||||
|
||||
async create(input: EmbeddableFunctionsEmbeddableInput, parent?: IContainer) {
|
||||
const { EmbeddableFunctions } = await import('./embeddable_functions');
|
||||
return new EmbeddableFunctions(input, {}, parent);
|
||||
}
|
||||
|
||||
getDisplayName() {
|
||||
return 'Universal Profiling Functions';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* 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 { TopNFunctionSortField, TopNFunctions } from '@kbn/profiling-utils';
|
||||
import React, { useState } from 'react';
|
||||
import { EuiDataGridSorting } from '@elastic/eui';
|
||||
import { TopNFunctionsGrid } from '../../components/topn_functions';
|
||||
|
||||
interface Props {
|
||||
data?: TopNFunctions;
|
||||
totalSeconds: number;
|
||||
}
|
||||
|
||||
export function EmbeddableFunctionsGrid({ data, totalSeconds }: Props) {
|
||||
const [sortField, setSortField] = useState(TopNFunctionSortField.Rank);
|
||||
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc');
|
||||
const [pageIndex, setPageIndex] = useState(0);
|
||||
|
||||
return (
|
||||
<TopNFunctionsGrid
|
||||
topNFunctions={data}
|
||||
totalSeconds={totalSeconds}
|
||||
isDifferentialView={false}
|
||||
pageIndex={pageIndex}
|
||||
onChangePage={setPageIndex}
|
||||
sortField={sortField}
|
||||
sortDirection={sortDirection}
|
||||
onChangeSort={(sorting: EuiDataGridSorting['columns'][0]) => {
|
||||
setSortField(sorting.id as TopNFunctionSortField);
|
||||
setSortDirection(sorting.direction);
|
||||
}}
|
||||
isEmbedded
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* 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 { EmbeddableSetup } from '@kbn/embeddable-plugin/public';
|
||||
import {
|
||||
EMBEDDABLE_FLAMEGRAPH,
|
||||
EMBEDDABLE_FUNCTIONS,
|
||||
} from '@kbn/observability-shared-plugin/public';
|
||||
import { EmbeddableFlamegraphFactory } from './flamegraph/embeddable_flamegraph_factory';
|
||||
import { EmbeddableFunctionsFactory } from './functions/embeddable_functions_factory';
|
||||
|
||||
export function registerEmbeddables(embeddable: EmbeddableSetup) {
|
||||
embeddable.registerEmbeddableFactory(EMBEDDABLE_FLAMEGRAPH, new EmbeddableFlamegraphFactory());
|
||||
embeddable.registerEmbeddableFactory(EMBEDDABLE_FUNCTIONS, new EmbeddableFunctionsFactory());
|
||||
}
|
|
@ -16,13 +16,12 @@ import { i18n } from '@kbn/i18n';
|
|||
import type { NavigationSection } from '@kbn/observability-shared-plugin/public';
|
||||
import type { Location } from 'history';
|
||||
import { BehaviorSubject, combineLatest, from, map } from 'rxjs';
|
||||
import { EMBEDDABLE_FLAMEGRAPH } from '@kbn/observability-shared-plugin/public';
|
||||
import { registerEmbeddables } from './embeddables/register_embeddables';
|
||||
import { FlamegraphLocatorDefinition } from './locators/flamegraph_locator';
|
||||
import { StacktracesLocatorDefinition } from './locators/stacktraces_locator';
|
||||
import { TopNFunctionsLocatorDefinition } from './locators/topn_functions_locator';
|
||||
import { getServices } from './services';
|
||||
import type { ProfilingPluginPublicSetupDeps, ProfilingPluginPublicStartDeps } from './types';
|
||||
import { EmbeddableFlamegraphFactory } from './embeddables/flamegraph/embeddable_flamegraph_factory';
|
||||
|
||||
export type ProfilingPluginSetup = ReturnType<ProfilingPlugin['setup']>;
|
||||
export type ProfilingPluginStart = void;
|
||||
|
@ -132,10 +131,7 @@ export class ProfilingPlugin implements Plugin {
|
|||
},
|
||||
});
|
||||
|
||||
pluginsSetup.embeddable.registerEmbeddableFactory(
|
||||
EMBEDDABLE_FLAMEGRAPH,
|
||||
new EmbeddableFlamegraphFactory()
|
||||
);
|
||||
registerEmbeddables(pluginsSetup.embeddable);
|
||||
|
||||
return {
|
||||
locators: {
|
||||
|
@ -150,11 +146,18 @@ export class ProfilingPlugin implements Plugin {
|
|||
),
|
||||
},
|
||||
hasSetup: async () => {
|
||||
const response = (await coreSetup.http.get('/internal/profiling/setup/es_resources')) as {
|
||||
has_setup: boolean;
|
||||
has_data: boolean;
|
||||
};
|
||||
return response.has_setup;
|
||||
try {
|
||||
const response = (await coreSetup.http.get('/internal/profiling/setup/es_resources')) as {
|
||||
has_setup: boolean;
|
||||
has_data: boolean;
|
||||
unauthorized: boolean;
|
||||
};
|
||||
|
||||
return response.has_setup;
|
||||
} catch (e) {
|
||||
// If any error happens while checking return as it has not been set up
|
||||
return false;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -9,8 +9,12 @@ import { toNumberRt } from '@kbn/io-ts-utils';
|
|||
import { createRouter, Outlet } from '@kbn/typed-react-router-config';
|
||||
import * as t from 'io-ts';
|
||||
import React from 'react';
|
||||
import { StackTracesDisplayOption, TopNType } from '@kbn/profiling-utils';
|
||||
import { TopNFunctionSortField, topNFunctionSortFieldRt } from '../../common/functions';
|
||||
import {
|
||||
StackTracesDisplayOption,
|
||||
TopNType,
|
||||
TopNFunctionSortField,
|
||||
topNFunctionSortFieldRt,
|
||||
} from '@kbn/profiling-utils';
|
||||
import {
|
||||
indexLifecyclePhaseRt,
|
||||
IndexLifecyclePhaseSelectOption,
|
||||
|
|
|
@ -7,11 +7,11 @@
|
|||
import { HttpFetchQuery } from '@kbn/core/public';
|
||||
import {
|
||||
createFlameGraph,
|
||||
TopNFunctions,
|
||||
type BaseFlameGraph,
|
||||
type ElasticFlameGraph,
|
||||
} from '@kbn/profiling-utils';
|
||||
import { getRoutePaths } from '../common';
|
||||
import { TopNFunctions } from '../common/functions';
|
||||
import type {
|
||||
IndexLifecyclePhaseSelectOption,
|
||||
IndicesStorageDetailsAPIResponse,
|
||||
|
|
|
@ -15,7 +15,7 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import React, { useRef } from 'react';
|
||||
import { GridOnScrollProps } from 'react-window';
|
||||
import { TopNFunctionSortField } from '../../../../common/functions';
|
||||
import { TopNFunctionSortField } from '@kbn/profiling-utils';
|
||||
import { AsyncComponent } from '../../../components/async_component';
|
||||
import { useProfilingDependencies } from '../../../components/contexts/profiling_dependencies/use_profiling_dependencies';
|
||||
import {
|
||||
|
@ -83,36 +83,31 @@ export function DifferentialTopNFunctionsView() {
|
|||
({ http }) => {
|
||||
return fetchTopNFunctions({
|
||||
http,
|
||||
timeFrom: timeRange.inSeconds.start,
|
||||
timeTo: timeRange.inSeconds.end,
|
||||
timeFrom: new Date(timeRange.start).getTime(),
|
||||
timeTo: new Date(timeRange.end).getTime(),
|
||||
startIndex: 0,
|
||||
endIndex: 100000,
|
||||
kuery,
|
||||
});
|
||||
},
|
||||
[timeRange.inSeconds.start, timeRange.inSeconds.end, kuery, fetchTopNFunctions]
|
||||
[fetchTopNFunctions, timeRange.start, timeRange.end, kuery]
|
||||
);
|
||||
|
||||
const comparisonState = useTimeRangeAsync(
|
||||
({ http }) => {
|
||||
if (!comparisonTimeRange.inSeconds.start || !comparisonTimeRange.inSeconds.end) {
|
||||
if (!comparisonTimeRange.start || !comparisonTimeRange.end) {
|
||||
return undefined;
|
||||
}
|
||||
return fetchTopNFunctions({
|
||||
http,
|
||||
timeFrom: comparisonTimeRange.inSeconds.start,
|
||||
timeTo: comparisonTimeRange.inSeconds.end,
|
||||
timeFrom: new Date(comparisonTimeRange.start).getTime(),
|
||||
timeTo: new Date(comparisonTimeRange.end).getTime(),
|
||||
startIndex: 0,
|
||||
endIndex: 100000,
|
||||
kuery: comparisonKuery,
|
||||
});
|
||||
},
|
||||
[
|
||||
comparisonTimeRange.inSeconds.start,
|
||||
comparisonTimeRange.inSeconds.end,
|
||||
comparisonKuery,
|
||||
fetchTopNFunctions,
|
||||
]
|
||||
[comparisonTimeRange.start, comparisonTimeRange.end, fetchTopNFunctions, comparisonKuery]
|
||||
);
|
||||
|
||||
const routePath = useProfilingRoutePath() as
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
import { EuiDataGridSorting, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { TopNFunctionSortField } from '../../../../common/functions';
|
||||
import { TopNFunctionSortField } from '@kbn/profiling-utils';
|
||||
import { AsyncComponent } from '../../../components/async_component';
|
||||
import { useProfilingDependencies } from '../../../components/contexts/profiling_dependencies/use_profiling_dependencies';
|
||||
import { TopNFunctionsGrid } from '../../../components/topn_functions';
|
||||
|
@ -29,14 +29,14 @@ export function TopNFunctionsView() {
|
|||
({ http }) => {
|
||||
return fetchTopNFunctions({
|
||||
http,
|
||||
timeFrom: timeRange.inSeconds.start,
|
||||
timeTo: timeRange.inSeconds.end,
|
||||
timeFrom: new Date(timeRange.start).getTime(),
|
||||
timeTo: new Date(timeRange.end).getTime(),
|
||||
startIndex: 0,
|
||||
endIndex: 100000,
|
||||
kuery,
|
||||
});
|
||||
},
|
||||
[timeRange.inSeconds.start, timeRange.inSeconds.end, kuery, fetchTopNFunctions]
|
||||
[fetchTopNFunctions, timeRange.start, timeRange.end, kuery]
|
||||
);
|
||||
|
||||
const profilingRouter = useProfilingRouter();
|
||||
|
|
|
@ -8,12 +8,8 @@
|
|||
import { schema, TypeOf } from '@kbn/config-schema';
|
||||
import { RouteRegisterParameters } from '.';
|
||||
import { getRoutePaths } from '../../common';
|
||||
import { createTopNFunctions } from '../../common/functions';
|
||||
import { handleRouteHandlerError } from '../utils/handle_route_error_handler';
|
||||
import { withProfilingSpan } from '../utils/with_profiling_span';
|
||||
import { getClient } from './compat';
|
||||
import { createCommonFilter } from './query';
|
||||
import { searchStackTraces } from './search_stacktraces';
|
||||
|
||||
const querySchema = schema.object({
|
||||
timeFrom: schema.number(),
|
||||
|
@ -28,46 +24,28 @@ type QuerySchemaType = TypeOf<typeof querySchema>;
|
|||
export function registerTopNFunctionsSearchRoute({
|
||||
router,
|
||||
logger,
|
||||
services: { createProfilingEsClient },
|
||||
dependencies: {
|
||||
start: { profilingDataAccess },
|
||||
},
|
||||
}: RouteRegisterParameters) {
|
||||
const paths = getRoutePaths();
|
||||
router.get(
|
||||
{
|
||||
path: paths.TopNFunctions,
|
||||
options: { tags: ['access:profiling'] },
|
||||
validate: {
|
||||
query: querySchema,
|
||||
},
|
||||
validate: { query: querySchema },
|
||||
},
|
||||
async (context, request, response) => {
|
||||
try {
|
||||
const { timeFrom, timeTo, startIndex, endIndex, kuery }: QuerySchemaType = request.query;
|
||||
const targetSampleSize = 20000; // minimum number of samples to get statistically sound results
|
||||
const esClient = await getClient(context);
|
||||
const profilingElasticsearchClient = createProfilingEsClient({ request, esClient });
|
||||
const filter = createCommonFilter({
|
||||
timeFrom,
|
||||
timeTo,
|
||||
const topNFunctions = await profilingDataAccess.services.fetchFunction({
|
||||
esClient,
|
||||
rangeFromMs: timeFrom,
|
||||
rangeToMs: timeTo,
|
||||
kuery,
|
||||
});
|
||||
|
||||
const { events, stackTraces, executables, stackFrames, samplingRate } =
|
||||
await searchStackTraces({
|
||||
client: profilingElasticsearchClient,
|
||||
filter,
|
||||
sampleSize: targetSampleSize,
|
||||
});
|
||||
|
||||
const topNFunctions = await withProfilingSpan('create_topn_functions', async () => {
|
||||
return createTopNFunctions({
|
||||
endIndex,
|
||||
events,
|
||||
executables,
|
||||
samplingRate,
|
||||
stackFrames,
|
||||
stackTraces,
|
||||
startIndex,
|
||||
});
|
||||
startIndex,
|
||||
endIndex,
|
||||
});
|
||||
|
||||
return response.ok({
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* 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 { ElasticsearchClient } from '@kbn/core/server';
|
||||
import { createTopNFunctions } from '@kbn/profiling-utils';
|
||||
import { withProfilingSpan } from '../../utils/with_profiling_span';
|
||||
import { RegisterServicesParams } from '../register_services';
|
||||
import { searchStackTraces } from '../search_stack_traces';
|
||||
|
||||
export interface FetchFunctionsParams {
|
||||
esClient: ElasticsearchClient;
|
||||
rangeFromMs: number;
|
||||
rangeToMs: number;
|
||||
kuery: string;
|
||||
startIndex: number;
|
||||
endIndex: number;
|
||||
}
|
||||
|
||||
const targetSampleSize = 20000; // minimum number of samples to get statistically sound results
|
||||
|
||||
export function createFetchFunctions({ createProfilingEsClient }: RegisterServicesParams) {
|
||||
return async ({
|
||||
esClient,
|
||||
rangeFromMs,
|
||||
rangeToMs,
|
||||
kuery,
|
||||
startIndex,
|
||||
endIndex,
|
||||
}: FetchFunctionsParams) => {
|
||||
const rangeFromSecs = rangeFromMs / 1000;
|
||||
const rangeToSecs = rangeToMs / 1000;
|
||||
|
||||
const profilingEsClient = createProfilingEsClient({ esClient });
|
||||
|
||||
const { events, stackTraces, executables, stackFrames, samplingRate } = await searchStackTraces(
|
||||
{
|
||||
client: profilingEsClient,
|
||||
rangeFrom: rangeFromSecs,
|
||||
rangeTo: rangeToSecs,
|
||||
kuery,
|
||||
sampleSize: targetSampleSize,
|
||||
}
|
||||
);
|
||||
|
||||
const topNFunctions = await withProfilingSpan('create_topn_functions', async () => {
|
||||
return createTopNFunctions({
|
||||
endIndex,
|
||||
events,
|
||||
executables,
|
||||
samplingRate,
|
||||
stackFrames,
|
||||
stackTraces,
|
||||
startIndex,
|
||||
});
|
||||
});
|
||||
|
||||
return topNFunctions;
|
||||
};
|
||||
}
|
|
@ -8,6 +8,7 @@
|
|||
import { ElasticsearchClient } from '@kbn/core/server';
|
||||
import { createFetchFlamechart } from './fetch_flamechart';
|
||||
import { ProfilingESClient } from '../utils/create_profiling_es_client';
|
||||
import { createFetchFunctions } from './functions';
|
||||
|
||||
export interface RegisterServicesParams {
|
||||
createProfilingEsClient: (params: {
|
||||
|
@ -17,5 +18,8 @@ export interface RegisterServicesParams {
|
|||
}
|
||||
|
||||
export function registerServices(params: RegisterServicesParams) {
|
||||
return { fetchFlamechartData: createFetchFlamechart(params) };
|
||||
return {
|
||||
fetchFlamechartData: createFetchFlamechart(params),
|
||||
fetchFunction: createFetchFunctions(params),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -5,10 +5,9 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query';
|
||||
import { decodeStackTraceResponse } from '@kbn/profiling-utils';
|
||||
import { ProfilingESClient } from '../../utils/create_profiling_es_client';
|
||||
import { kqlQuery } from '../../utils/query';
|
||||
|
||||
export async function searchStackTraces({
|
||||
client,
|
||||
|
@ -46,12 +45,3 @@ export async function searchStackTraces({
|
|||
|
||||
return decodeStackTraceResponse(response);
|
||||
}
|
||||
|
||||
function kqlQuery(kql?: string): estypes.QueryDslQueryContainer[] {
|
||||
if (!kql) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const ast = fromKueryExpression(kql);
|
||||
return [toElasticsearchQuery(ast)];
|
||||
}
|
||||
|
|
17
x-pack/plugins/profiling_data_access/server/utils/query.ts
Normal file
17
x-pack/plugins/profiling_data_access/server/utils/query.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* 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 type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query';
|
||||
|
||||
export function kqlQuery(kql?: string): estypes.QueryDslQueryContainer[] {
|
||||
if (!kql) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const ast = fromKueryExpression(kql);
|
||||
return [toElasticsearchQuery(ast)];
|
||||
}
|
137
x-pack/test/profiling_api_integration/tests/__snapshots__/functions.spec.snap
generated
Normal file
137
x-pack/test/profiling_api_integration/tests/__snapshots__/functions.spec.snap
generated
Normal file
|
@ -0,0 +1,137 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Profiling API tests functions.spec.ts cloud Loading profiling data Functions api returns correct result 1`] = `
|
||||
Object {
|
||||
"SamplingRate": 1,
|
||||
"TopN": Array [
|
||||
Object {
|
||||
"CountExclusive": 152,
|
||||
"CountInclusive": 152,
|
||||
"Frame": Object {
|
||||
"AddressOrLine": 10967732,
|
||||
"CommitHash": "",
|
||||
"ExeFileName": "6tVKI4mSYDEJ-ABAIpYXcg",
|
||||
"FileID": "6tVKI4mSYDEJ-ABAIpYXcg",
|
||||
"FrameID": "6tVKI4mSYDEJ-ABAIpYXcgAAAAAAp1q0",
|
||||
"FrameType": 4,
|
||||
"FunctionName": "",
|
||||
"FunctionOffset": 0,
|
||||
"FunctionSourceLine": 0,
|
||||
"Inline": false,
|
||||
"SamplingRate": 1,
|
||||
"SourceCodeURL": "",
|
||||
"SourceFilename": "",
|
||||
"SourceID": "",
|
||||
"SourceLine": 0,
|
||||
"SourcePackageHash": "",
|
||||
"SourcePackageURL": "",
|
||||
},
|
||||
"Id": "empty;6tVKI4mSYDEJ-ABAIpYXcg;10967732",
|
||||
"Rank": 1,
|
||||
},
|
||||
Object {
|
||||
"CountExclusive": 106,
|
||||
"CountInclusive": 106,
|
||||
"Frame": Object {
|
||||
"AddressOrLine": -1484822518,
|
||||
"CommitHash": "",
|
||||
"ExeFileName": "",
|
||||
"FileID": "AAAAAAAAV4sAAAAAAAAAHQ",
|
||||
"FrameID": "AAAAAAAAV4sAAAAAAAAAHQuTP52nf2gK",
|
||||
"FrameType": 5,
|
||||
"FunctionName": "",
|
||||
"FunctionOffset": 0,
|
||||
"FunctionSourceLine": 0,
|
||||
"Inline": false,
|
||||
"SamplingRate": 1,
|
||||
"SourceCodeURL": "",
|
||||
"SourceFilename": "",
|
||||
"SourceID": "",
|
||||
"SourceLine": 0,
|
||||
"SourcePackageHash": "",
|
||||
"SourcePackageURL": "",
|
||||
},
|
||||
"Id": "empty;AAAAAAAAV4sAAAAAAAAAHQ;-1484822518",
|
||||
"Rank": 2,
|
||||
},
|
||||
Object {
|
||||
"CountExclusive": 50,
|
||||
"CountInclusive": 50,
|
||||
"Frame": Object {
|
||||
"AddressOrLine": 42521194,
|
||||
"CommitHash": "",
|
||||
"ExeFileName": "XT4fd_WKeR1cE-hlLelCQA",
|
||||
"FileID": "XT4fd_WKeR1cE-hlLelCQA",
|
||||
"FrameID": "XT4fd_WKeR1cE-hlLelCQAAAAAACiNJq",
|
||||
"FrameType": 3,
|
||||
"FunctionName": "",
|
||||
"FunctionOffset": 0,
|
||||
"FunctionSourceLine": 0,
|
||||
"Inline": false,
|
||||
"SamplingRate": 1,
|
||||
"SourceCodeURL": "",
|
||||
"SourceFilename": "",
|
||||
"SourceID": "",
|
||||
"SourceLine": 0,
|
||||
"SourcePackageHash": "",
|
||||
"SourcePackageURL": "",
|
||||
},
|
||||
"Id": "empty;XT4fd_WKeR1cE-hlLelCQA;42521194",
|
||||
"Rank": 3,
|
||||
},
|
||||
Object {
|
||||
"CountExclusive": 49,
|
||||
"CountInclusive": 49,
|
||||
"Frame": Object {
|
||||
"AddressOrLine": 838088,
|
||||
"CommitHash": "",
|
||||
"ExeFileName": "6tVKI4mSYDEJ-ABAIpYXcg",
|
||||
"FileID": "6tVKI4mSYDEJ-ABAIpYXcg",
|
||||
"FrameID": "6tVKI4mSYDEJ-ABAIpYXcgAAAAAADMnI",
|
||||
"FrameType": 4,
|
||||
"FunctionName": "",
|
||||
"FunctionOffset": 0,
|
||||
"FunctionSourceLine": 0,
|
||||
"Inline": false,
|
||||
"SamplingRate": 1,
|
||||
"SourceCodeURL": "",
|
||||
"SourceFilename": "",
|
||||
"SourceID": "",
|
||||
"SourceLine": 0,
|
||||
"SourcePackageHash": "",
|
||||
"SourcePackageURL": "",
|
||||
},
|
||||
"Id": "empty;6tVKI4mSYDEJ-ABAIpYXcg;838088",
|
||||
"Rank": 4,
|
||||
},
|
||||
Object {
|
||||
"CountExclusive": 40,
|
||||
"CountInclusive": 42,
|
||||
"Frame": Object {
|
||||
"AddressOrLine": 1671872298,
|
||||
"CommitHash": "",
|
||||
"ExeFileName": "",
|
||||
"FileID": "AAAAAAAAV4sAAAAAAAAAHg",
|
||||
"FrameID": "AAAAAAAAV4sAAAAAAAAAHezOBKFjpr8q",
|
||||
"FrameType": 5,
|
||||
"FunctionName": "",
|
||||
"FunctionOffset": 0,
|
||||
"FunctionSourceLine": 0,
|
||||
"Inline": false,
|
||||
"SamplingRate": 1,
|
||||
"SourceCodeURL": "",
|
||||
"SourceFilename": "",
|
||||
"SourceID": "",
|
||||
"SourceLine": 0,
|
||||
"SourcePackageHash": "",
|
||||
"SourcePackageURL": "",
|
||||
},
|
||||
"Id": "empty;AAAAAAAAV4sAAAAAAAAAHg;1671872298",
|
||||
"Rank": 5,
|
||||
},
|
||||
],
|
||||
"TotalCount": 3599,
|
||||
"selfCPU": 397,
|
||||
"totalCPU": 399,
|
||||
}
|
||||
`;
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* 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 { getRoutePaths } from '@kbn/profiling-plugin/common';
|
||||
import { TopNFunctions } from '@kbn/profiling-utils';
|
||||
import expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../common/ftr_provider_context';
|
||||
|
||||
const profilingRoutePaths = getRoutePaths();
|
||||
|
||||
export default function featureControlsTests({ getService }: FtrProviderContext) {
|
||||
const registry = getService('registry');
|
||||
const profilingApiClient = getService('profilingApiClient');
|
||||
|
||||
const start = new Date('2023-03-17T01:00:00.000Z').getTime();
|
||||
const end = new Date('2023-03-17T01:05:00.000Z').getTime();
|
||||
|
||||
registry.when('Functions api', { config: 'cloud' }, () => {
|
||||
let functions: TopNFunctions;
|
||||
before(async () => {
|
||||
const response = await profilingApiClient.adminUser({
|
||||
endpoint: `GET ${profilingRoutePaths.TopNFunctions}`,
|
||||
params: {
|
||||
query: {
|
||||
timeFrom: start,
|
||||
timeTo: end,
|
||||
kuery: '',
|
||||
startIndex: 0,
|
||||
endIndex: 5,
|
||||
},
|
||||
},
|
||||
});
|
||||
functions = response.body as TopNFunctions;
|
||||
});
|
||||
it(`returns correct result`, async () => {
|
||||
expect(functions.TopN.length).to.equal(5);
|
||||
expect(functions.TotalCount).to.equal(3599);
|
||||
expect(functions.selfCPU).to.equal(397);
|
||||
expect(functions.totalCPU).to.equal(399);
|
||||
expectSnapshot(functions).toMatch();
|
||||
});
|
||||
});
|
||||
}
|
|
@ -141,5 +141,6 @@
|
|||
"@kbn/aiops-utils",
|
||||
"@kbn/stack-alerts-plugin",
|
||||
"@kbn/apm-data-access-plugin",
|
||||
"@kbn/profiling-utils",
|
||||
]
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue