[APM] Show Universal Profiling on Transaction view (#176302)

New Setting: Default value is `False`
<img width="1070" alt="Screenshot 2024-02-08 at 15 06 43"
src="46e8273c-4389-4de5-8b93-77d3e7a191d8">
---
<img width="1335" alt="Screenshot 2024-02-08 at 14 59 17"
src="45531abc-1c1c-4525-a2dc-b7573f857fa6">
<img width="1339" alt="Screenshot 2024-02-08 at 14 59 33"
src="d0ca0c7e-d33e-4f4a-83d4-70ef96b2bf1b">
This commit is contained in:
Cauê Marcondes 2024-02-13 10:58:35 +00:00 committed by GitHub
parent 8477b6b8bc
commit db08fb5c5b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
34 changed files with 957 additions and 203 deletions

View file

@ -448,6 +448,9 @@ Determines whether the <<service-time-comparison, comparison feature>> is enable
[[observability-apm-enable-infra-view]]`observability:enableInfrastructureView`::
Enables the Infrastructure view in the APM app.
[[observability-apm-enable-transaction-profiling]]`observability:apmEnableTransactionProfiling`::
Enable Universal Profiling on Transaction view.
[[observability-enable-inspect-es-queries]]`observability:enableInspectEsQueries`::
When enabled, allows you to inspect {es} queries in API responses.

View file

@ -15,6 +15,7 @@ export { jsonRt } from './src/json_rt';
export { mergeRt } from './src/merge_rt';
export { strictKeysRt } from './src/strict_keys_rt';
export { isoToEpochRt } from './src/iso_to_epoch_rt';
export { isoToEpochSecsRt } from './src/iso_to_epoch_secs_rt';
export { toNumberRt } from './src/to_number_rt';
export { toBooleanRt } from './src/to_boolean_rt';
export { toJsonSchema } from './src/to_json_schema';

View file

@ -0,0 +1,33 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { isoToEpochSecsRt } from '.';
import { isRight } from 'fp-ts/lib/Either';
describe('isoToEpochSecsRt', () => {
it('validates whether its input is a valid ISO timestamp', () => {
expect(isRight(isoToEpochSecsRt.decode(1566299881499))).toBe(false);
expect(isRight(isoToEpochSecsRt.decode('2019-08-20T11:18:31.407Z'))).toBe(true);
});
it('decodes valid ISO timestamps to epoch secs time', () => {
const iso = '2019-08-20T11:18:31.407Z';
const result = isoToEpochSecsRt.decode(iso);
if (isRight(result)) {
expect(result.right).toBe(new Date(iso).getTime() / 1000);
} else {
fail();
}
});
it('encodes epoch secs time to ISO string', () => {
expect(isoToEpochSecsRt.encode(1566299911407)).toBe('2019-08-20T11:18:31.407Z');
});
});

View file

@ -0,0 +1,25 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { chain } from 'fp-ts/lib/Either';
import { pipe } from 'fp-ts/lib/function';
import * as t from 'io-ts';
import { isoToEpochRt } from '../iso_to_epoch_rt';
export const isoToEpochSecsRt = new t.Type<number, string, unknown>(
'isoToEpochSecsRt',
t.number.is,
(value) =>
pipe(
isoToEpochRt.decode(value),
chain((epochMsValue) => {
return t.success(epochMsValue / 1000);
})
),
(output) => new Date(output).toISOString()
);

View file

@ -568,6 +568,10 @@ export const stackManagementSchema: MakeSchemaFrom<UsageStats> = {
type: 'boolean',
_meta: { description: 'Non-default value of setting.' },
},
'observability:apmEnableTransactionProfiling': {
type: 'boolean',
_meta: { description: 'Non-default value of setting.' },
},
'observability:profilingShowErrorFrames': {
type: 'boolean',
_meta: { description: 'Non-default value of setting.' },

View file

@ -167,5 +167,6 @@ export interface UsageStats {
'observability:profilingCostPervCPUPerHour': number;
'observability:profilingAWSCostDiscountRate': number;
'data_views:fields_excluded_data_tiers': string;
'observability:apmEnableTransactionProfiling': boolean;
'devTools:enableDockedConsole': boolean;
}

View file

@ -10168,6 +10168,12 @@
"description": "Non-default value of setting."
}
},
"observability:apmEnableTransactionProfiling": {
"type": "boolean",
"_meta": {
"description": "Non-default value of setting."
}
},
"observability:profilingShowErrorFrames": {
"type": "boolean",
"_meta": {
@ -11705,4 +11711,4 @@
}
}
}
}
}

View file

@ -320,6 +320,8 @@ exports[`Error TRANSACTION_OVERFLOW_COUNT 1`] = `undefined`;
exports[`Error TRANSACTION_PAGE_URL 1`] = `undefined`;
exports[`Error TRANSACTION_PROFILER_STACK_TRACE_IDS 1`] = `undefined`;
exports[`Error TRANSACTION_RESULT 1`] = `undefined`;
exports[`Error TRANSACTION_ROOT 1`] = `undefined`;
@ -645,6 +647,8 @@ exports[`Span TRANSACTION_OVERFLOW_COUNT 1`] = `undefined`;
exports[`Span TRANSACTION_PAGE_URL 1`] = `undefined`;
exports[`Span TRANSACTION_PROFILER_STACK_TRACE_IDS 1`] = `undefined`;
exports[`Span TRANSACTION_RESULT 1`] = `undefined`;
exports[`Span TRANSACTION_ROOT 1`] = `undefined`;
@ -988,6 +992,8 @@ exports[`Transaction TRANSACTION_OVERFLOW_COUNT 1`] = `undefined`;
exports[`Transaction TRANSACTION_PAGE_URL 1`] = `undefined`;
exports[`Transaction TRANSACTION_PROFILER_STACK_TRACE_IDS 1`] = `undefined`;
exports[`Transaction TRANSACTION_RESULT 1`] = `"transaction result"`;
exports[`Transaction TRANSACTION_ROOT 1`] = `undefined`;

View file

@ -65,6 +65,8 @@ export const TRANSACTION_OVERFLOW_COUNT =
'transaction.aggregation.overflow_count';
// for transaction metrics
export const TRANSACTION_ROOT = 'transaction.root';
export const TRANSACTION_PROFILER_STACK_TRACE_IDS =
'transaction.profiler_stack_trace_ids';
export const EVENT_OUTCOME = 'event.outcome';

View file

@ -5,16 +5,7 @@
* 2.0.
*/
import {
EuiEmptyPrompt,
EuiFlexGroup,
EuiFlexItem,
EuiLink,
EuiSpacer,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { EmbeddableFlamegraph } from '@kbn/observability-shared-plugin/public';
import { isEmpty } from 'lodash';
import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
import React from 'react';
import { ApmDataSourceWithSummary } from '../../../../common/data_source';
import { ApmDocumentType } from '../../../../common/document_type';
@ -23,12 +14,9 @@ import {
mergeKueries,
toKueryFilterFormat,
} from '../../../../common/utils/kuery_utils';
import {
FETCH_STATUS,
isPending,
useFetcher,
} from '../../../hooks/use_fetcher';
import { useProfilingPlugin } from '../../../hooks/use_profiling_plugin';
import { useFetcher } from '../../../hooks/use_fetcher';
import { ProfilingFlamegraphChart } from '../../shared/profiling/flamegraph';
import { ProfilingFlamegraphLink } from '../../shared/profiling/flamegraph/flamegraph_link';
import { HostnamesFilterWarning } from './host_names_filter_warning';
interface Props {
@ -54,8 +42,6 @@ export function ProfilingFlamegraph({
rangeFrom,
rangeTo,
}: Props) {
const { profilingLocators } = useProfilingPlugin();
const { data, status } = useFetcher(
(callApmApi) => {
if (dataSource) {
@ -92,41 +78,16 @@ export function ProfilingFlamegraph({
<HostnamesFilterWarning hostNames={data?.hostNames} />
</EuiFlexItem>
<EuiFlexItem>
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
<EuiLink
data-test-subj="apmProfilingFlamegraphGoToFlamegraphLink"
href={profilingLocators?.flamegraphLocator.getRedirectUrl({
kuery: mergeKueries([`(${hostNamesKueryFormat})`, kuery]),
rangeFrom,
rangeTo,
})}
>
{i18n.translate('xpack.apm.profiling.flamegraph.link', {
defaultMessage: 'Go to Universal Profiling Flamegraph',
})}
</EuiLink>
</div>
<ProfilingFlamegraphLink
kuery={mergeKueries([`(${hostNamesKueryFormat})`, kuery])}
rangeFrom={rangeFrom}
rangeTo={rangeTo}
justifyContent="flexEnd"
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer />
{status === FETCH_STATUS.SUCCESS && isEmpty(data) ? (
<EuiEmptyPrompt
titleSize="s"
title={
<div>
{i18n.translate('xpack.apm.profiling.flamegraph.noDataFound', {
defaultMessage: 'No data found',
})}
</div>
}
/>
) : (
<EmbeddableFlamegraph
data={data?.flamegraph}
isLoading={isPending(status)}
height="60vh"
/>
)}
<ProfilingFlamegraphChart data={data?.flamegraph} status={status} />
</>
);
}

View file

@ -5,8 +5,7 @@
* 2.0.
*/
import { EuiFlexGroup, EuiFlexItem, EuiLink, EuiSpacer } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
import { EmbeddableFunctions } from '@kbn/observability-shared-plugin/public';
import React from 'react';
import { ApmDataSourceWithSummary } from '../../../../common/data_source';
@ -17,7 +16,7 @@ import {
toKueryFilterFormat,
} from '../../../../common/utils/kuery_utils';
import { isPending, useFetcher } from '../../../hooks/use_fetcher';
import { useProfilingPlugin } from '../../../hooks/use_profiling_plugin';
import { ProfilingTopNFunctionsLink } from '../../shared/profiling/top_functions/top_functions_link';
import { HostnamesFilterWarning } from './host_names_filter_warning';
interface Props {
@ -47,8 +46,6 @@ export function ProfilingTopNFunctions({
rangeFrom,
rangeTo,
}: Props) {
const { profilingLocators } = useProfilingPlugin();
const { data, status } = useFetcher(
(callApmApi) => {
if (dataSource) {
@ -96,20 +93,12 @@ export function ProfilingTopNFunctions({
<HostnamesFilterWarning hostNames={data?.hostNames} />
</EuiFlexItem>
<EuiFlexItem>
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
<EuiLink
data-test-subj="apmProfilingTopNFunctionsGoToUniversalProfilingFlamegraphLink"
href={profilingLocators?.topNFunctionsLocator.getRedirectUrl({
kuery: mergeKueries([`(${hostNamesKueryFormat})`, kuery]),
rangeFrom,
rangeTo,
})}
>
{i18n.translate('xpack.apm.profiling.topnFunctions.link', {
defaultMessage: 'Go to Universal Profiling Functions',
})}
</EuiLink>
</div>
<ProfilingTopNFunctionsLink
kuery={mergeKueries([`(${hostNamesKueryFormat})`, kuery])}
rangeFrom={rangeFrom}
rangeTo={rangeTo}
justifyContent="flexEnd"
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer />

View file

@ -21,6 +21,7 @@ import {
enableAgentExplorerView,
apmEnableProfilingIntegration,
apmEnableTableSearchBar,
apmEnableTransactionProfiling,
} from '@kbn/observability-plugin/common';
import { isEmpty } from 'lodash';
import React from 'react';
@ -57,7 +58,9 @@ function getApmSettingsKeys(isProfilingIntegrationEnabled: boolean) {
];
if (isProfilingIntegrationEnabled) {
keys.push(apmEnableProfilingIntegration);
keys.push(
...[apmEnableProfilingIntegration, apmEnableTransactionProfiling]
);
}
return keys;

View file

@ -0,0 +1,80 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { EuiSpacer } from '@elastic/eui';
import React from 'react';
import { useFetcher } from '../../../hooks/use_fetcher';
import { useTimeRange } from '../../../hooks/use_time_range';
import { ProfilingFlamegraphChart } from '../../shared/profiling/flamegraph';
import { ProfilingFlamegraphLink } from '../../shared/profiling/flamegraph/flamegraph_link';
interface Props {
serviceName: string;
rangeFrom: string;
rangeTo: string;
kuery: string;
transactionName: string;
transactionType?: string;
environment: string;
}
export function ProfilingFlamegraph({
serviceName,
rangeFrom,
rangeTo,
kuery,
transactionName,
transactionType,
environment,
}: Props) {
const { start, end } = useTimeRange({ rangeFrom, rangeTo });
const { data, status } = useFetcher(
(callApmApi) => {
if (!transactionType) {
return;
}
return callApmApi(
'GET /internal/apm/services/{serviceName}/transactions/flamegraph',
{
params: {
path: { serviceName },
query: {
start,
end,
kuery,
transactionName,
transactionType,
environment,
},
},
}
);
},
[
serviceName,
start,
end,
kuery,
transactionName,
transactionType,
environment,
]
);
return (
<>
<ProfilingFlamegraphLink
kuery={kuery}
rangeFrom={rangeFrom}
rangeTo={rangeTo}
justifyContent="flexEnd"
/>
<EuiSpacer />
<ProfilingFlamegraphChart data={data} status={status} />
</>
);
}

View file

@ -0,0 +1,122 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import {
EuiFlexGroup,
EuiFlexItem,
EuiLoadingSpinner,
EuiSpacer,
EuiTabbedContent,
EuiTabbedContentProps,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React, { useMemo } from 'react';
import { ProfilingEmptyState } from '@kbn/observability-shared-plugin/public';
import { useApmParams } from '../../../hooks/use_apm_params';
import { useProfilingPlugin } from '../../../hooks/use_profiling_plugin';
import { ProfilingFlamegraph } from './profiling_flamegraph';
import { ProfilingTopNFunctions } from './profiling_top_functions';
function ProfilingTab() {
const {
query: {
rangeFrom,
rangeTo,
environment,
kuery,
transactionName,
transactionType,
},
path: { serviceName },
} = useApmParams('/services/{serviceName}/transactions/view');
const { isProfilingAvailable, isLoading } = useProfilingPlugin();
const tabs = useMemo((): EuiTabbedContentProps['tabs'] => {
return [
{
id: 'flamegraph',
name: i18n.translate(
'xpack.apm.transactions.profiling.tabs.flamegraph',
{ defaultMessage: 'Flamegraph' }
),
content: (
<>
<EuiSpacer />
<ProfilingFlamegraph
serviceName={serviceName}
rangeFrom={rangeFrom}
rangeTo={rangeTo}
kuery={kuery}
transactionName={transactionName}
transactionType={transactionType}
environment={environment}
/>
</>
),
},
{
id: 'topNFunctions',
name: i18n.translate(
'xpack.apm.transactions.profiling.tabs.topNFunctions',
{ defaultMessage: 'Top 10 Functions' }
),
content: (
<>
<EuiSpacer />
<ProfilingTopNFunctions
serviceName={serviceName}
rangeFrom={rangeFrom}
rangeTo={rangeTo}
kuery={kuery}
transactionName={transactionName}
transactionType={transactionType}
environment={environment}
/>
</>
),
},
];
}, [
environment,
kuery,
rangeFrom,
rangeTo,
serviceName,
transactionName,
transactionType,
]);
if (isLoading) {
return (
<EuiFlexGroup alignItems="center" justifyContent="center">
<EuiFlexItem grow={false}>
<EuiLoadingSpinner size="m" />
</EuiFlexItem>
</EuiFlexGroup>
);
}
if (isProfilingAvailable === false) {
return <ProfilingEmptyState />;
}
return (
<EuiTabbedContent
tabs={tabs}
initialSelectedTab={tabs[0]}
autoFocus="selected"
/>
);
}
export const profilingTab = {
dataTestSubj: 'apmProfilingTabButton',
key: 'Profiling',
label: i18n.translate('xpack.apm.transactionDetails.tabs.ProfilingLabel', {
defaultMessage: 'Universal Profiling',
}),
component: ProfilingTab,
};

View file

@ -0,0 +1,87 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { EuiSpacer } from '@elastic/eui';
import { EmbeddableFunctions } from '@kbn/observability-shared-plugin/public';
import React from 'react';
import { isPending, useFetcher } from '../../../hooks/use_fetcher';
import { useTimeRange } from '../../../hooks/use_time_range';
import { ProfilingTopNFunctionsLink } from '../../shared/profiling/top_functions/top_functions_link';
interface Props {
serviceName: string;
rangeFrom: string;
rangeTo: string;
kuery: string;
transactionName: string;
transactionType?: string;
environment: string;
}
export function ProfilingTopNFunctions({
serviceName,
rangeFrom,
rangeTo,
kuery,
transactionName,
transactionType,
environment,
}: Props) {
const { start, end } = useTimeRange({ rangeFrom, rangeTo });
const { data, status } = useFetcher(
(callApmApi) => {
if (!transactionType) {
return;
}
return callApmApi(
'GET /internal/apm/services/{serviceName}/transactions/functions',
{
params: {
path: { serviceName },
query: {
start,
end,
kuery,
transactionName,
startIndex: 0,
endIndex: 10,
transactionType,
environment,
},
},
}
);
},
[
serviceName,
start,
end,
kuery,
transactionName,
transactionType,
environment,
]
);
return (
<>
<ProfilingTopNFunctionsLink
kuery={kuery}
rangeFrom={rangeFrom}
rangeTo={rangeTo}
justifyContent="flexEnd"
/>
<EuiSpacer />
<EmbeddableFunctions
data={data}
isLoading={isPending(status)}
rangeFrom={new Date(start).valueOf()}
rangeTo={new Date(end).valueOf()}
/>
</>
);
}

View file

@ -5,37 +5,29 @@
* 2.0.
*/
import React, { useCallback, useEffect, useState } from 'react';
import { omit } from 'lodash';
import { useHistory } from 'react-router-dom';
import {
EuiPanel,
EuiSpacer,
EuiTabs,
EuiTab,
EuiFlexItem,
EuiFlexGroup,
} from '@elastic/eui';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { XYBrushEvent } from '@elastic/charts';
import { EuiPanel, EuiSpacer, EuiTab, EuiTabs } from '@elastic/eui';
import { omit } from 'lodash';
import { useHistory } from 'react-router-dom';
import { maybe } from '../../../../common/utils/maybe';
import { useLegacyUrlParams } from '../../../context/url_params_context/use_url_params';
import { useAnyOfApmParams } from '../../../hooks/use_apm_params';
import { useCriticalPathFeatureEnabledSetting } from '../../../hooks/use_critical_path_feature_enabled_setting';
import { FETCH_STATUS } from '../../../hooks/use_fetcher';
import { useSampleChartSelection } from '../../../hooks/use_sample_chart_selection';
import {
TraceSamplesFetchResult,
useTransactionTraceSamplesFetcher,
} from '../../../hooks/use_transaction_trace_samples_fetcher';
import { maybe } from '../../../../common/utils/maybe';
import { fromQuery, toQuery } from '../../shared/links/url_helpers';
import { aggregatedCriticalPathTab } from './aggregated_critical_path_tab';
import { failedTransactionsCorrelationsTab } from './failed_transactions_correlations_tab';
import { latencyCorrelationsTab } from './latency_correlations_tab';
import { profilingTab } from './profiling_tab';
import { traceSamplesTab } from './trace_samples_tab';
import { useSampleChartSelection } from '../../../hooks/use_sample_chart_selection';
import { FETCH_STATUS } from '../../../hooks/use_fetcher';
import { useCriticalPathFeatureEnabledSetting } from '../../../hooks/use_critical_path_feature_enabled_setting';
import { aggregatedCriticalPathTab } from './aggregated_critical_path_tab';
import { useTransactionProfilingSetting } from '../../../hooks/use_profiling_integration_setting';
export interface TabContentProps {
clearChartSelection: () => void;
@ -46,12 +38,6 @@ export interface TabContentProps {
traceSamplesFetchResult: TraceSamplesFetchResult;
}
const tabs = [
traceSamplesTab,
latencyCorrelationsTab,
failedTransactionsCorrelationsTab,
];
export function TransactionDetailsTabs() {
const { query } = useAnyOfApmParams(
'/services/{serviceName}/transactions/view',
@ -59,10 +45,24 @@ export function TransactionDetailsTabs() {
);
const isCriticalPathFeatureEnabled = useCriticalPathFeatureEnabledSetting();
const isTransactionProfilingEnabled = useTransactionProfilingSetting();
const availableTabs = isCriticalPathFeatureEnabled
? tabs.concat(aggregatedCriticalPathTab)
: tabs;
const availableTabs = useMemo(() => {
const tabs = [
traceSamplesTab,
latencyCorrelationsTab,
failedTransactionsCorrelationsTab,
];
if (isCriticalPathFeatureEnabled) {
tabs.push(aggregatedCriticalPathTab);
}
if (isTransactionProfilingEnabled) {
tabs.push(profilingTab);
}
return tabs;
}, [isCriticalPathFeatureEnabled, isTransactionProfilingEnabled]);
const { urlParams } = useLegacyUrlParams();
const history = useHistory();
@ -140,20 +140,16 @@ export function TransactionDetailsTabs() {
</EuiTabs>
<EuiSpacer size="m" />
<EuiPanel hasBorder={true}>
<EuiFlexGroup>
<EuiFlexItem>
<TabContent
{...{
clearChartSelection,
onFilter,
sampleRangeFrom,
sampleRangeTo,
selectSampleFromChartSelection,
traceSamplesFetchResult,
}}
/>
</EuiFlexItem>
</EuiFlexGroup>
<TabContent
{...{
clearChartSelection,
onFilter,
sampleRangeFrom,
sampleRangeTo,
selectSampleFromChartSelection,
traceSamplesFetchResult,
}}
/>
</EuiPanel>
</>
);

View file

@ -0,0 +1,50 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import {
EuiFlexGroup,
EuiFlexGroupProps,
EuiFlexItem,
EuiLink,
} from '@elastic/eui';
import React from 'react';
import { i18n } from '@kbn/i18n';
import { useProfilingPlugin } from '../../../../hooks/use_profiling_plugin';
interface Props {
kuery: string;
rangeFrom: string;
rangeTo: string;
justifyContent?: EuiFlexGroupProps['justifyContent'];
}
export function ProfilingFlamegraphLink({
kuery,
rangeFrom,
rangeTo,
justifyContent = 'flexStart',
}: Props) {
const { profilingLocators } = useProfilingPlugin();
return (
<EuiFlexGroup justifyContent={justifyContent}>
<EuiFlexItem grow={false}>
<EuiLink
data-test-subj="apmProfilingFlamegraphGoToFlamegraphLink"
href={profilingLocators?.flamegraphLocator.getRedirectUrl({
kuery,
rangeFrom,
rangeTo,
})}
>
{i18n.translate('xpack.apm.profiling.flamegraph.link', {
defaultMessage: 'Go to Universal Profiling Flamegraph',
})}
</EuiLink>
</EuiFlexItem>
</EuiFlexGroup>
);
}

View file

@ -0,0 +1,44 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { EuiEmptyPrompt } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { EmbeddableFlamegraph } from '@kbn/observability-shared-plugin/public';
import { BaseFlameGraph } from '@kbn/profiling-utils';
import { isEmpty } from 'lodash';
import React from 'react';
import { FETCH_STATUS, isPending } from '../../../../hooks/use_fetcher';
interface Props {
data?: BaseFlameGraph;
status: FETCH_STATUS;
}
export function ProfilingFlamegraphChart({ data, status }: Props) {
return (
<>
{status === FETCH_STATUS.SUCCESS &&
(isEmpty(data) || data?.TotalSamples === 0) ? (
<EuiEmptyPrompt
titleSize="s"
title={
<div>
{i18n.translate('xpack.apm.profiling.flamegraph.noDataFound', {
defaultMessage: 'No data found',
})}
</div>
}
/>
) : (
<EmbeddableFlamegraph
data={data}
isLoading={isPending(status)}
height="35vh"
/>
)}
</>
);
}

View file

@ -0,0 +1,51 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import {
EuiFlexGroup,
EuiFlexGroupProps,
EuiFlexItem,
EuiLink,
} from '@elastic/eui';
import React from 'react';
import { i18n } from '@kbn/i18n';
import { useProfilingPlugin } from '../../../../hooks/use_profiling_plugin';
interface Props {
kuery: string;
rangeFrom: string;
rangeTo: string;
justifyContent?: EuiFlexGroupProps['justifyContent'];
}
export function ProfilingTopNFunctionsLink({
kuery,
rangeFrom,
rangeTo,
justifyContent = 'flexStart',
}: Props) {
const { profilingLocators } = useProfilingPlugin();
return (
<EuiFlexGroup justifyContent={justifyContent}>
<EuiFlexItem grow={false}>
<EuiLink
data-test-subj="apmProfilingTopNFunctionsGoToUniversalProfilingFlamegraphLink"
href={profilingLocators?.topNFunctionsLocator.getRedirectUrl({
kuery,
rangeFrom,
rangeTo,
})}
>
{i18n.translate('xpack.apm.profiling.topnFunctions.link', {
defaultMessage: 'Go to Universal Profiling Functions',
})}
</EuiLink>
</EuiFlexItem>
</EuiFlexGroup>
);
}

View file

@ -6,8 +6,12 @@
*/
import { useUiSetting } from '@kbn/kibana-react-plugin/public';
import { apmEnableProfilingIntegration } from '@kbn/observability-plugin/common';
import {
apmEnableProfilingIntegration,
apmEnableTransactionProfiling,
} from '@kbn/observability-plugin/common';
import { ApmFeatureFlagName } from '../../common/apm_feature_flags';
import { useApmPluginContext } from '../context/apm_plugin/use_apm_plugin_context';
import { useApmFeatureFlag } from './use_apm_feature_flag';
export function useProfilingIntegrationSetting() {
@ -23,3 +27,14 @@ export function useProfilingIntegrationSetting() {
isProfilingIntegrationUiSettingEnabled
);
}
export function useTransactionProfilingSetting() {
const { core } = useApmPluginContext();
const isProfilingIntegrationEnabled = useProfilingIntegrationSetting();
const isTransactionProfilingEnabled = core.uiSettings.get<boolean>(
apmEnableTransactionProfiling
);
return isProfilingIntegrationEnabled && isTransactionProfilingEnabled;
}

View file

@ -342,4 +342,8 @@ export class APMEventClient {
cb: (opts) => this.esClient.termsEnum(requestParams, opts),
});
}
getIndicesFromProcessorEvent(processorEvent: ProcessorEvent) {
return processorEventsToIndex([processorEvent], this.indices);
}
}

View file

@ -5,10 +5,18 @@
* 2.0.
*/
import { toNumberRt } from '@kbn/io-ts-utils';
import { isoToEpochSecsRt, toNumberRt } from '@kbn/io-ts-utils';
import type { BaseFlameGraph, TopNFunctions } from '@kbn/profiling-utils';
import * as t from 'io-ts';
import { HOST_NAME } from '../../../common/es_fields/apm';
import { kqlQuery, termQuery } from '@kbn/observability-plugin/server';
import { ProcessorEvent } from '@kbn/observability-plugin/common';
import {
HOST_NAME,
SERVICE_NAME,
TRANSACTION_NAME,
TRANSACTION_PROFILER_STACK_TRACE_IDS,
TRANSACTION_TYPE,
} from '../../../common/es_fields/apm';
import {
mergeKueries,
toKueryFilterFormat,
@ -22,6 +30,7 @@ import {
serviceTransactionDataSourceRt,
} from '../default_api_types';
import { getServiceHostNames } from './get_service_host_names';
import { environmentQuery } from '../../../common/utils/environment_query';
const profilingFlamegraphRoute = createApmServerRoute({
endpoint: 'GET /internal/apm/services/{serviceName}/profiling/flamegraph',
@ -66,17 +75,35 @@ const profilingFlamegraphRoute = createApmServerRoute({
if (!serviceHostNames.length) {
return undefined;
}
const startSecs = start / 1000;
const endSecs = end / 1000;
const flamegraph =
await profilingDataAccessStart?.services.fetchFlamechartData({
core,
esClient: esClient.asCurrentUser,
rangeFromMs: start,
rangeToMs: end,
kuery: mergeKueries([
`(${toKueryFilterFormat(HOST_NAME, serviceHostNames)})`,
kuery,
]),
totalSeconds: endSecs - startSecs,
query: {
bool: {
filter: [
...kqlQuery(
mergeKueries([
`(${toKueryFilterFormat(HOST_NAME, serviceHostNames)})`,
kuery,
])
),
{
range: {
['@timestamp']: {
gte: String(startSecs),
lt: String(endSecs),
format: 'epoch_second',
},
},
},
],
},
},
});
return { flamegraph, hostNames: serviceHostNames };
@ -137,17 +164,36 @@ const profilingFunctionsRoute = createApmServerRoute({
return undefined;
}
const startSecs = start / 1000;
const endSecs = end / 1000;
const functions = await profilingDataAccessStart?.services.fetchFunction({
core,
esClient: esClient.asCurrentUser,
rangeFromMs: start,
rangeToMs: end,
kuery: mergeKueries([
`(${toKueryFilterFormat(HOST_NAME, serviceHostNames)})`,
kuery,
]),
startIndex,
endIndex,
totalSeconds: endSecs - startSecs,
query: {
bool: {
filter: [
...kqlQuery(
mergeKueries([
`(${toKueryFilterFormat(HOST_NAME, serviceHostNames)})`,
kuery,
])
),
{
range: {
['@timestamp']: {
gte: String(startSecs),
lt: String(endSecs),
format: 'epoch_second',
},
},
},
],
},
},
});
return { functions, hostNames: serviceHostNames };
}
@ -156,6 +202,159 @@ const profilingFunctionsRoute = createApmServerRoute({
},
});
const transactionsFlamegraphRoute = createApmServerRoute({
endpoint: 'GET /internal/apm/services/{serviceName}/transactions/flamegraph',
params: t.type({
path: t.type({ serviceName: t.string }),
query: t.intersection([
kueryRt,
environmentRt,
t.type({
transactionName: t.string,
start: isoToEpochSecsRt,
end: isoToEpochSecsRt,
transactionType: t.string,
}),
]),
}),
options: { tags: ['access:apm'] },
handler: async (resources): Promise<BaseFlameGraph | undefined> => {
const { context, plugins, params } = resources;
const core = await context.core;
const [esClient, profilingDataAccessStart, apmEventClient] =
await Promise.all([
core.elasticsearch.client,
await plugins.profilingDataAccess?.start(),
getApmEventClient(resources),
]);
if (profilingDataAccessStart) {
const { serviceName } = params.path;
const {
start,
end,
kuery,
transactionName,
transactionType,
environment,
} = params.query;
const indices = apmEventClient.getIndicesFromProcessorEvent(
ProcessorEvent.transaction
);
return await profilingDataAccessStart?.services.fetchFlamechartData({
core,
esClient: esClient.asCurrentUser,
indices,
stacktraceIdsField: TRANSACTION_PROFILER_STACK_TRACE_IDS,
totalSeconds: end - start,
query: {
bool: {
filter: [
...kqlQuery(kuery),
...termQuery(SERVICE_NAME, serviceName),
...termQuery(TRANSACTION_NAME, transactionName),
...environmentQuery(environment),
...termQuery(TRANSACTION_TYPE, transactionType),
{
range: {
['@timestamp']: {
gte: String(start),
lt: String(end),
format: 'epoch_second',
},
},
},
],
},
},
});
}
return undefined;
},
});
const transactionsFunctionsRoute = createApmServerRoute({
endpoint: 'GET /internal/apm/services/{serviceName}/transactions/functions',
params: t.type({
path: t.type({ serviceName: t.string }),
query: t.intersection([
environmentRt,
t.type({
start: isoToEpochSecsRt,
end: isoToEpochSecsRt,
startIndex: toNumberRt,
endIndex: toNumberRt,
transactionName: t.string,
transactionType: t.string,
}),
kueryRt,
]),
}),
options: { tags: ['access:apm'] },
handler: async (resources): Promise<TopNFunctions | undefined> => {
const { context, plugins, params } = resources;
const core = await context.core;
const [esClient, profilingDataAccessStart, apmEventClient] =
await Promise.all([
core.elasticsearch.client,
await plugins.profilingDataAccess?.start(),
getApmEventClient(resources),
]);
if (profilingDataAccessStart) {
const {
start,
end,
startIndex,
endIndex,
kuery,
transactionName,
transactionType,
environment,
} = params.query;
const { serviceName } = params.path;
const indices = apmEventClient.getIndicesFromProcessorEvent(
ProcessorEvent.transaction
);
return profilingDataAccessStart?.services.fetchFunction({
core,
esClient: esClient.asCurrentUser,
startIndex,
endIndex,
indices,
stacktraceIdsField: TRANSACTION_PROFILER_STACK_TRACE_IDS,
totalSeconds: end - start,
query: {
bool: {
filter: [
...kqlQuery(kuery),
...termQuery(SERVICE_NAME, serviceName),
...termQuery(TRANSACTION_NAME, transactionName),
...environmentQuery(environment),
...termQuery(TRANSACTION_TYPE, transactionType),
{
range: {
['@timestamp']: {
gte: String(start),
lt: String(end),
format: 'epoch_second',
},
},
},
],
},
},
});
}
return undefined;
},
});
const profilingStatusRoute = createApmServerRoute({
endpoint: 'GET /internal/apm/profiling/status',
options: { tags: ['access:apm'] },
@ -190,4 +389,6 @@ export const profilingRouteRepository = {
...profilingFlamegraphRoute,
...profilingStatusRoute,
...profilingFunctionsRoute,
...transactionsFlamegraphRoute,
...transactionsFunctionsRoute,
};

View file

@ -8,6 +8,7 @@
import type { CoreRequestHandlerContext } from '@kbn/core-http-request-handler-context-server';
import type { ProfilingDataAccessPluginStart } from '@kbn/profiling-data-access-plugin/server';
import type { BaseFlameGraph } from '@kbn/profiling-utils';
import { kqlQuery } from '@kbn/observability-plugin/server';
import type { InfraProfilingFlamegraphRequestParams } from '../../../../common/http_api/profiling_api';
export async function fetchProfilingFlamegraph(
@ -15,11 +16,28 @@ export async function fetchProfilingFlamegraph(
profilingDataAccess: ProfilingDataAccessPluginStart,
coreRequestContext: CoreRequestHandlerContext
): Promise<BaseFlameGraph> {
const startSecs = from / 1000;
const endSecs = to / 1000;
return await profilingDataAccess.services.fetchFlamechartData({
core: coreRequestContext,
esClient: coreRequestContext.elasticsearch.client.asCurrentUser,
rangeFromMs: from,
rangeToMs: to,
kuery,
totalSeconds: endSecs - startSecs,
query: {
bool: {
filter: [
...kqlQuery(kuery),
{
range: {
['@timestamp']: {
gte: String(startSecs),
lt: String(endSecs),
format: 'epoch_second',
},
},
},
],
},
},
});
}

View file

@ -8,6 +8,7 @@
import type { CoreRequestHandlerContext } from '@kbn/core-http-request-handler-context-server';
import type { ProfilingDataAccessPluginStart } from '@kbn/profiling-data-access-plugin/server';
import type { TopNFunctions } from '@kbn/profiling-utils';
import { kqlQuery } from '@kbn/observability-plugin/server';
import type { InfraProfilingFunctionsRequestParams } from '../../../../common/http_api/profiling_api';
export async function fetchProfilingFunctions(
@ -16,14 +17,30 @@ export async function fetchProfilingFunctions(
coreRequestContext: CoreRequestHandlerContext
): Promise<TopNFunctions> {
const { kuery, from, to, startIndex, endIndex } = params;
const startSecs = from / 1000;
const endSecs = to / 1000;
return await profilingDataAccess.services.fetchFunction({
core: coreRequestContext,
esClient: coreRequestContext.elasticsearch.client.asCurrentUser,
rangeFromMs: from,
rangeToMs: to,
kuery,
startIndex,
endIndex,
totalSeconds: endSecs - startSecs,
query: {
bool: {
filter: [
...kqlQuery(kuery),
{
range: {
['@timestamp']: {
gte: String(startSecs),
lt: String(endSecs),
format: 'epoch_second',
},
},
},
],
},
},
});
}

View file

@ -51,6 +51,7 @@ export {
profilingPervCPUWattArm64,
profilingAWSCostDiscountRate,
profilingCostPervCPUPerHour,
apmEnableTransactionProfiling,
} from './ui_settings_keys';
export {

View file

@ -39,3 +39,4 @@ export const profilingCo2PerKWH = 'observability:profilingCo2PerKWH';
export const profilingDatacenterPUE = 'observability:profilingDatacenterPUE';
export const profilingAWSCostDiscountRate = 'observability:profilingAWSCostDiscountRate';
export const profilingCostPervCPUPerHour = 'observability:profilingCostPervCPUPerHour';
export const apmEnableTransactionProfiling = 'observability:apmEnableTransactionProfiling';

View file

@ -39,6 +39,7 @@ import {
profilingAWSCostDiscountRate,
profilingCostPervCPUPerHour,
enableInfrastructureProfilingIntegration,
apmEnableTransactionProfiling,
enableInfrastructureHostsCustomDashboards,
} from '../common/ui_settings_keys';
@ -552,6 +553,15 @@ export const uiSettings: Record<string, UiSettings> = {
schema: schema.number({ min: 0, max: 100 }),
requiresPageReload: true,
},
[apmEnableTransactionProfiling]: {
category: [observabilityFeatureId],
name: i18n.translate('xpack.observability.apmEnableTransactionProfiling', {
defaultMessage: 'Enable Universal Profiling on Transaction view',
}),
value: false,
schema: schema.boolean(),
requiresPageReload: true,
},
};
function throttlingDocsLink({ href }: { href: string }) {

View file

@ -6,6 +6,7 @@
*/
import { schema } from '@kbn/config-schema';
import { kqlQuery } from '@kbn/observability-plugin/server';
import { IDLE_SOCKET_TIMEOUT, RouteRegisterParameters } from '.';
import { getRoutePaths } from '../../common';
import { handleRouteHandlerError } from '../utils/handle_route_error_handler';
@ -35,15 +36,31 @@ export function registerFlameChartSearchRoute({
const { timeFrom, timeTo, kuery } = request.query;
const core = await context.core;
const startSecs = timeFrom / 1000;
const endSecs = timeTo / 1000;
try {
const esClient = await getClient(context);
const flamegraph = await profilingDataAccess.services.fetchFlamechartData({
core,
esClient,
rangeFromMs: timeFrom,
rangeToMs: timeTo,
kuery,
totalSeconds: endSecs - startSecs,
query: {
bool: {
filter: [
...kqlQuery(kuery),
{
range: {
['@timestamp']: {
gte: String(startSecs),
lt: String(endSecs),
format: 'epoch_second',
},
},
},
],
},
},
});
return response.ok({ body: flamegraph });

View file

@ -6,6 +6,7 @@
*/
import { schema, TypeOf } from '@kbn/config-schema';
import { kqlQuery } from '@kbn/observability-plugin/server';
import { IDLE_SOCKET_TIMEOUT, RouteRegisterParameters } from '.';
import { getRoutePaths } from '../../common';
import { handleRouteHandlerError } from '../utils/handle_route_error_handler';
@ -40,15 +41,32 @@ export function registerTopNFunctionsSearchRoute({
const core = await context.core;
const { timeFrom, timeTo, startIndex, endIndex, kuery }: QuerySchemaType = request.query;
const startSecs = timeFrom / 1000;
const endSecs = timeTo / 1000;
const esClient = await getClient(context);
const topNFunctions = await profilingDataAccess.services.fetchFunction({
core,
esClient,
rangeFromMs: timeFrom,
rangeToMs: timeTo,
kuery,
startIndex,
endIndex,
totalSeconds: endSecs - startSecs,
query: {
bool: {
filter: [
...kqlQuery(kuery),
{
range: {
['@timestamp']: {
gte: String(startSecs),
lt: String(endSecs),
format: 'epoch_second',
},
},
},
],
},
},
});
return response.ok({

View file

@ -29,6 +29,8 @@ export interface ProfilingESClient {
pervCPUWattArm64?: number;
awsCostDiscountRate?: number;
costPervCPUPerHour?: number;
indices?: string[];
stacktraceIdsField?: string;
}): Promise<StackTraceResponse>;
profilingStatus(params?: { waitForResourcesCreated?: boolean }): Promise<ProfilingStatusResponse>;
getEsClient(): ElasticsearchClient;
@ -42,5 +44,7 @@ export interface ProfilingESClient {
pervCPUWattArm64?: number;
awsCostDiscountRate?: number;
costPervCPUPerHour?: number;
indices?: string[];
stacktraceIdsField?: string;
}): Promise<BaseFlameGraph>;
}

View file

@ -4,7 +4,7 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { CoreRequestHandlerContext, ElasticsearchClient } from '@kbn/core/server';
import {
profilingAWSCostDiscountRate,
@ -15,24 +15,28 @@ import {
profilingPervCPUWattX86,
} from '@kbn/observability-plugin/common';
import { percentToFactor } from '../../utils/percent_to_factor';
import { kqlQuery } from '../../utils/query';
import { RegisterServicesParams } from '../register_services';
export interface FetchFlamechartParams {
esClient: ElasticsearchClient;
core: CoreRequestHandlerContext;
rangeFromMs: number;
rangeToMs: number;
kuery: string;
indices?: string[];
stacktraceIdsField?: string;
query: QueryDslQueryContainer;
totalSeconds: number;
}
const targetSampleSize = 20000; // minimum number of samples to get statistically sound results
export function createFetchFlamechart({ createProfilingEsClient }: RegisterServicesParams) {
return async ({ core, esClient, rangeFromMs, rangeToMs, kuery }: FetchFlamechartParams) => {
const rangeFromSecs = rangeFromMs / 1000;
const rangeToSecs = rangeToMs / 1000;
return async ({
core,
esClient,
indices,
stacktraceIdsField,
query,
totalSeconds,
}: FetchFlamechartParams) => {
const [
co2PerKWH,
datacenterPUE,
@ -50,24 +54,9 @@ export function createFetchFlamechart({ createProfilingEsClient }: RegisterServi
]);
const profilingEsClient = createProfilingEsClient({ esClient });
const totalSeconds = rangeToSecs - rangeFromSecs;
const flamegraph = await profilingEsClient.profilingFlamegraph({
query: {
bool: {
filter: [
...kqlQuery(kuery),
{
range: {
['@timestamp']: {
gte: String(rangeFromSecs),
lt: String(rangeToSecs),
format: 'epoch_second',
},
},
},
],
},
},
query,
sampleSize: targetSampleSize,
durationSeconds: totalSeconds,
co2PerKWH,
@ -76,6 +65,8 @@ export function createFetchFlamechart({ createProfilingEsClient }: RegisterServi
pervCPUWattArm64,
awsCostDiscountRate: percentToFactor(awsCostDiscountRate),
costPervCPUPerHour,
indices,
stacktraceIdsField,
});
return { ...flamegraph, TotalSeconds: totalSeconds };
};

View file

@ -15,6 +15,7 @@ import {
} from '@kbn/observability-plugin/common';
import { CoreRequestHandlerContext, ElasticsearchClient } from '@kbn/core/server';
import { createTopNFunctions } from '@kbn/profiling-utils';
import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { percentToFactor } from '../../utils/percent_to_factor';
import { withProfilingSpan } from '../../utils/with_profiling_span';
import { RegisterServicesParams } from '../register_services';
@ -23,11 +24,12 @@ import { searchStackTraces } from '../search_stack_traces';
export interface FetchFunctionsParams {
core: CoreRequestHandlerContext;
esClient: ElasticsearchClient;
rangeFromMs: number;
rangeToMs: number;
kuery: string;
startIndex: number;
endIndex: number;
indices?: string[];
stacktraceIdsField?: string;
query: QueryDslQueryContainer;
totalSeconds: number;
}
const targetSampleSize = 20000; // minimum number of samples to get statistically sound results
@ -36,16 +38,13 @@ export function createFetchFunctions({ createProfilingEsClient }: RegisterServic
return async ({
core,
esClient,
rangeFromMs,
rangeToMs,
kuery,
startIndex,
endIndex,
indices,
stacktraceIdsField,
query,
totalSeconds,
}: FetchFunctionsParams) => {
const rangeFromSecs = rangeFromMs / 1000;
const rangeToSecs = rangeToMs / 1000;
const totalSeconds = rangeToSecs - rangeFromSecs;
const [
co2PerKWH,
datacenterPUE,
@ -69,9 +68,6 @@ export function createFetchFunctions({ createProfilingEsClient }: RegisterServic
const { events, stackTraces, executables, stackFrames, samplingRate } = await searchStackTraces(
{
client: profilingEsClient,
rangeFrom: rangeFromSecs,
rangeTo: rangeToSecs,
kuery,
sampleSize: targetSampleSize,
durationSeconds: totalSeconds,
co2PerKWH,
@ -80,6 +76,9 @@ export function createFetchFunctions({ createProfilingEsClient }: RegisterServic
pervCPUWattArm64,
awsCostDiscountRate: percentToFactor(awsCostDiscountRate),
costPervCPUPerHour,
indices,
stacktraceIdsField,
query,
showErrorFrames,
}
);

View file

@ -6,15 +6,12 @@
*/
import { decodeStackTraceResponse } from '@kbn/profiling-utils';
import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { ProfilingESClient } from '../../../common/profiling_es_client';
import { kqlQuery } from '../../utils/query';
export async function searchStackTraces({
client,
sampleSize,
rangeFrom,
rangeTo,
kuery,
durationSeconds,
co2PerKWH,
datacenterPUE,
@ -22,13 +19,13 @@ export async function searchStackTraces({
pervCPUWattArm64,
awsCostDiscountRate,
costPervCPUPerHour,
indices,
stacktraceIdsField,
query,
showErrorFrames,
}: {
client: ProfilingESClient;
sampleSize: number;
rangeFrom: number;
rangeTo: number;
kuery: string;
durationSeconds: number;
co2PerKWH: number;
datacenterPUE: number;
@ -36,26 +33,13 @@ export async function searchStackTraces({
pervCPUWattArm64: number;
awsCostDiscountRate: number;
costPervCPUPerHour: number;
indices?: string[];
stacktraceIdsField?: string;
query: QueryDslQueryContainer;
showErrorFrames: boolean;
}) {
const response = await client.profilingStacktraces({
query: {
bool: {
filter: [
...kqlQuery(kuery),
{
range: {
['@timestamp']: {
gte: String(rangeFrom),
lt: String(rangeTo),
format: 'epoch_second',
boost: 1.0,
},
},
},
],
},
},
query,
sampleSize,
durationSeconds,
co2PerKWH,
@ -64,6 +48,8 @@ export async function searchStackTraces({
pervCPUWattArm64,
awsCostDiscountRate,
costPervCPUPerHour,
indices,
stacktraceIdsField,
});
return decodeStackTraceResponse(response, showErrorFrames);

View file

@ -49,6 +49,8 @@ export function createProfilingEsClient({
costPervCPUPerHour,
pervCPUWattArm64,
pervCPUWattX86,
indices,
stacktraceIdsField,
}) {
const controller = new AbortController();
const promise = withProfilingSpan('_profiling/stacktraces', () => {
@ -66,6 +68,8 @@ export function createProfilingEsClient({
datacenter_pue: datacenterPUE,
aws_cost_factor: awsCostDiscountRate,
cost_per_core_hour: costPervCPUPerHour,
indices,
stacktrace_ids_field: stacktraceIdsField,
},
},
{
@ -110,6 +114,8 @@ export function createProfilingEsClient({
costPervCPUPerHour,
pervCPUWattArm64,
pervCPUWattX86,
indices,
stacktraceIdsField,
}) {
const controller = new AbortController();
@ -128,6 +134,8 @@ export function createProfilingEsClient({
datacenter_pue: datacenterPUE,
aws_cost_factor: awsCostDiscountRate,
cost_per_core_hour: costPervCPUPerHour,
indices,
stacktrace_ids_field: stacktraceIdsField,
},
},
{