[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:
Cauê Marcondes 2023-09-20 16:35:19 +01:00 committed by GitHub
parent d0b759a47a
commit 7e25900bc5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
39 changed files with 1074 additions and 248 deletions

3
.github/CODEOWNERS vendored
View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import type { TopNFunctions } from '../../../../common/functions';
import type { TopNFunctions } from '@kbn/profiling-utils';
export const data = {
TotalCount: 4,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

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

View file

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

View file

@ -141,5 +141,6 @@
"@kbn/aiops-utils",
"@kbn/stack-alerts-plugin",
"@kbn/apm-data-access-plugin",
"@kbn/profiling-utils",
]
}