mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[ProfilingxAPM] Link APM from Profiling UI (#180677)
closes https://github.com/elastic/kibana/issues/178719 A new ES API has been created to support linking APM from the Profiling UI. It's called `topN/functions`. The new API allows grouping fields. So we first fetch functions grouping by `service.name` and when the user opens the APM Transactions we make another request grouping by `transaction.name`. A new Advanced setting was created to toggle the old API on (fetch functions from Stacktraces API): It's turned off by default. <img width="1235" alt="Screenshot 2024-04-12 at 10 39 36" src="ee6e7731
-2f44-43ca-9793-23ba87e22e6e"> When there are services on the selected function: *If we cannot find the transaction, we show `N/A`. <img width="933" alt="Screenshot 2024-04-12 at 10 16 34" src="2c5dbf60
-3a47-4f4c-a46d-8a0984e0e482"> When there are **no** services on the selected function: *hide the APM transactions section <img width="921" alt="Screenshot 2024-04-12 at 10 59 14" src="3fc4c5b1
-da62-47c8-97a8-8bcbd1ae1b75"> -- Performance boost: The new API is faster than the Stacktraces API, especially because there's no logic on the Kibana side. Stacktraces API: <img width="1210" alt="Screenshot 2024-04-12 at 10 50 26" src="158d73d1
-ed91-4652-97c1-c7c3328d5e3d"> TopN/Functions API: <img width="1195" alt="Screenshot 2024-04-12 at 10 51 20" src="2de4ef46
-eb8a-4557-b7b8-a1c2fed6fd8a"> --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
d769242c37
commit
b900b86c1b
39 changed files with 1267 additions and 83 deletions
|
@ -486,6 +486,9 @@ If you're enrolled in the AWS Enterprise Discount Program (EDP), enter your disc
|
|||
[[observability-profiling-azure-cost-discount-rate]]`observability:profilingAzureCostDiscountRate`::
|
||||
If you have an Azure Enterprise Agreement with Microsoft, enter your discount rate to update the profiling cost calculation.
|
||||
|
||||
[[observability-profiling-use-topNFunctions-from-stacktraces]]`observability:profilingFetchTopNFunctionsFromStacktraces`::
|
||||
Switch to fetch the TopN Functions from the Stacktraces API.
|
||||
|
||||
[[observability-profiling-cost-per-vcpu-per-hour]]`observability:profilingCostPervCPUPerHour`::
|
||||
Default Hourly Cost per CPU Core for machines not on AWS or Azure.
|
||||
|
||||
|
|
40
packages/kbn-profiling-utils/common/es_functions.ts
Normal file
40
packages/kbn-profiling-utils/common/es_functions.ts
Normal file
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export interface Frame {
|
||||
frame_type: number;
|
||||
inline: boolean;
|
||||
address_or_line: number;
|
||||
function_name: string;
|
||||
file_name: string;
|
||||
line_number: number;
|
||||
executable_file_name: string;
|
||||
}
|
||||
|
||||
interface TopNFunction {
|
||||
id: string;
|
||||
rank: number;
|
||||
frame: Frame;
|
||||
sub_groups: Record<string, number>;
|
||||
self_count: number;
|
||||
total_count: number;
|
||||
self_annual_co2_tons: number;
|
||||
total_annual_co2_tons: number;
|
||||
self_annual_costs_usd: number;
|
||||
total_annual_costs_usd: number;
|
||||
}
|
||||
|
||||
export interface ESTopNFunctions {
|
||||
self_count: number;
|
||||
total_count: number;
|
||||
self_annual_co2_tons: number;
|
||||
self_annual_cost_usd: number;
|
||||
topn: TopNFunction[];
|
||||
}
|
||||
|
||||
export type AggregationField = 'service.name' | 'transaction.name';
|
|
@ -49,6 +49,7 @@ type TopNFunction = Pick<
|
|||
> & {
|
||||
Id: string;
|
||||
Rank: number;
|
||||
subGroups: Record<string, number>;
|
||||
};
|
||||
|
||||
export interface TopNFunctions {
|
||||
|
@ -207,6 +208,7 @@ export function createTopNFunctions({
|
|||
selfAnnualCostUSD: frameAndCount.selfAnnualCostUSD,
|
||||
totalAnnualCO2kgs: frameAndCount.totalAnnualCO2kgs,
|
||||
totalAnnualCostUSD: frameAndCount.totalAnnualCostUSD,
|
||||
subGroups: {},
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
@ -53,3 +53,4 @@ export type {
|
|||
} from './common/profiling';
|
||||
export type { ProfilingStatus } from './common/profiling_status';
|
||||
export type { TopNFunctions } from './common/functions';
|
||||
export type { AggregationField, ESTopNFunctions } from './common/es_functions';
|
||||
|
|
|
@ -671,4 +671,8 @@ export const stackManagementSchema: MakeSchemaFrom<UsageStats> = {
|
|||
type: 'keyword',
|
||||
_meta: { description: 'Non-default value of setting.' },
|
||||
},
|
||||
'observability:profilingFetchTopNFunctionsFromStacktraces': {
|
||||
type: 'boolean',
|
||||
_meta: { description: 'Non-default value of setting.' },
|
||||
},
|
||||
};
|
||||
|
|
|
@ -176,4 +176,5 @@ export interface UsageStats {
|
|||
'observability:apmEnableTransactionProfiling': boolean;
|
||||
'devTools:enablePersistentConsole': boolean;
|
||||
'aiAssistant:preferredAIAssistantType': string;
|
||||
'observability:profilingFetchTopNFunctionsFromStacktraces': boolean;
|
||||
}
|
||||
|
|
|
@ -10451,6 +10451,12 @@
|
|||
"_meta": {
|
||||
"description": "Non-default value of setting."
|
||||
}
|
||||
},
|
||||
"observability:profilingFetchTopNFunctionsFromStacktraces": {
|
||||
"type": "boolean",
|
||||
"_meta": {
|
||||
"description": "Non-default value of setting."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -11887,4 +11893,4 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,101 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { Redirect } from 'react-router-dom';
|
||||
import { css } from '@emotion/react';
|
||||
import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher';
|
||||
import { getRedirectToTransactionDetailPageUrl } from '../trace_link/get_redirect_to_transaction_detail_page_url';
|
||||
import { useApmParams } from '../../../hooks/use_apm_params';
|
||||
import { useTimeRange } from '../../../hooks/use_time_range';
|
||||
|
||||
export function TransactionDetailsByNameLink() {
|
||||
const {
|
||||
query: {
|
||||
rangeFrom = 'now-15m',
|
||||
rangeTo = 'now',
|
||||
transactionName,
|
||||
serviceName,
|
||||
},
|
||||
} = useApmParams('/link-to/transaction');
|
||||
|
||||
const { start, end } = useTimeRange({ rangeFrom, rangeTo });
|
||||
|
||||
const { data = { transaction: null }, status } = useFetcher(
|
||||
(callApmApi) => {
|
||||
return callApmApi('GET /internal/apm/transactions', {
|
||||
params: {
|
||||
query: {
|
||||
start,
|
||||
end,
|
||||
transactionName,
|
||||
serviceName,
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
[start, end, transactionName, serviceName]
|
||||
);
|
||||
|
||||
if (status === FETCH_STATUS.SUCCESS) {
|
||||
if (data.transaction) {
|
||||
return (
|
||||
<Redirect
|
||||
to={getRedirectToTransactionDetailPageUrl({
|
||||
transaction: data.transaction,
|
||||
rangeFrom,
|
||||
rangeTo,
|
||||
})}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
css={css`
|
||||
height: 100%;
|
||||
display: flex;
|
||||
`}
|
||||
>
|
||||
<EuiEmptyPrompt
|
||||
iconType="apmTrace"
|
||||
title={
|
||||
<h2>
|
||||
{i18n.translate(
|
||||
'xpack.apm.transactionDetailsLink.h2.transactionNotFound',
|
||||
{ defaultMessage: 'No transaction found' }
|
||||
)}
|
||||
</h2>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
css={css`
|
||||
height: 100%;
|
||||
display: flex;
|
||||
`}
|
||||
>
|
||||
<EuiEmptyPrompt
|
||||
iconType="apmTrace"
|
||||
title={
|
||||
<h2>
|
||||
{i18n.translate(
|
||||
'xpack.apm.transactionDetailsLink.h2.fetchingTransactionLabel',
|
||||
{ defaultMessage: 'Fetching transaction...' }
|
||||
)}
|
||||
</h2>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -23,6 +23,7 @@ import { ApmMainTemplate } from './templates/apm_main_template';
|
|||
import { ServiceGroupsList } from '../app/service_groups';
|
||||
import { offsetRt } from '../../../common/comparison_rt';
|
||||
import { diagnosticsRoute } from '../app/diagnostics';
|
||||
import { TransactionDetailsByNameLink } from '../app/transaction_details_link';
|
||||
|
||||
const ServiceGroupsTitle = i18n.translate(
|
||||
'xpack.apm.views.serviceGroups.title',
|
||||
|
@ -34,6 +35,18 @@ const ServiceGroupsTitle = i18n.translate(
|
|||
* creates the routes.
|
||||
*/
|
||||
const apmRoutes = {
|
||||
'/link-to/transaction': {
|
||||
element: <TransactionDetailsByNameLink />,
|
||||
params: t.type({
|
||||
query: t.intersection([
|
||||
t.type({ transactionName: t.string, serviceName: t.string }),
|
||||
t.partial({
|
||||
rangeFrom: t.string,
|
||||
rangeTo: t.string,
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
},
|
||||
'/link-to/transaction/{transactionId}': {
|
||||
element: <TransactionLink />,
|
||||
params: t.intersection([
|
||||
|
@ -69,7 +82,12 @@ const apmRoutes = {
|
|||
},
|
||||
'/': {
|
||||
element: (
|
||||
<Breadcrumb title="APM" href="/">
|
||||
<Breadcrumb
|
||||
title={i18n.translate('xpack.apm..breadcrumb.apmLabel', {
|
||||
defaultMessage: 'APM',
|
||||
})}
|
||||
href="/"
|
||||
>
|
||||
<Outlet />
|
||||
</Breadcrumb>
|
||||
),
|
||||
|
|
|
@ -36,6 +36,7 @@ import {
|
|||
import { getSpan } from '../transactions/get_span';
|
||||
import { Transaction } from '../../../typings/es_schemas/ui/transaction';
|
||||
import { Span } from '../../../typings/es_schemas/ui/span';
|
||||
import { getTransactionByName } from '../transactions/get_transaction_by_name';
|
||||
|
||||
const tracesRoute = createApmServerRoute({
|
||||
endpoint: 'GET /internal/apm/traces',
|
||||
|
@ -186,6 +187,42 @@ const transactionByIdRoute = createApmServerRoute({
|
|||
},
|
||||
});
|
||||
|
||||
const transactionByNameRoute = createApmServerRoute({
|
||||
endpoint: 'GET /internal/apm/transactions',
|
||||
params: t.type({
|
||||
query: t.intersection([
|
||||
rangeRt,
|
||||
t.type({
|
||||
transactionName: t.string,
|
||||
serviceName: t.string,
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async (
|
||||
resources
|
||||
): Promise<{
|
||||
transaction: Transaction;
|
||||
}> => {
|
||||
const {
|
||||
params: {
|
||||
query: { start, end, transactionName, serviceName },
|
||||
},
|
||||
} = resources;
|
||||
|
||||
const apmEventClient = await getApmEventClient(resources);
|
||||
return {
|
||||
transaction: await getTransactionByName({
|
||||
transactionName,
|
||||
apmEventClient,
|
||||
start,
|
||||
end,
|
||||
serviceName,
|
||||
}),
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
const findTracesRoute = createApmServerRoute({
|
||||
endpoint: 'GET /internal/apm/traces/find',
|
||||
params: t.type({
|
||||
|
@ -338,4 +375,5 @@ export const traceRouteRepository = {
|
|||
...aggregatedCriticalPathRoute,
|
||||
...transactionFromTraceByIdRoute,
|
||||
...spanFromTraceByIdRoute,
|
||||
...transactionByNameRoute,
|
||||
};
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* 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 { rangeQuery } from '@kbn/observability-plugin/server';
|
||||
import { ApmDocumentType } from '../../../../common/document_type';
|
||||
import {
|
||||
SERVICE_NAME,
|
||||
TRANSACTION_NAME,
|
||||
} from '../../../../common/es_fields/apm';
|
||||
import { RollupInterval } from '../../../../common/rollup';
|
||||
import { asMutableArray } from '../../../../common/utils/as_mutable_array';
|
||||
import { APMEventClient } from '../../../lib/helpers/create_es_client/create_apm_event_client';
|
||||
|
||||
export async function getTransactionByName({
|
||||
transactionName,
|
||||
serviceName,
|
||||
apmEventClient,
|
||||
start,
|
||||
end,
|
||||
}: {
|
||||
transactionName: string;
|
||||
serviceName: string;
|
||||
apmEventClient: APMEventClient;
|
||||
start: number;
|
||||
end: number;
|
||||
}) {
|
||||
const resp = await apmEventClient.search('get_transaction', {
|
||||
apm: {
|
||||
sources: [
|
||||
{
|
||||
documentType: ApmDocumentType.TransactionEvent,
|
||||
rollupInterval: RollupInterval.None,
|
||||
},
|
||||
],
|
||||
},
|
||||
body: {
|
||||
track_total_hits: false,
|
||||
size: 1,
|
||||
terminate_after: 1,
|
||||
query: {
|
||||
bool: {
|
||||
filter: asMutableArray([
|
||||
{ term: { [TRANSACTION_NAME]: transactionName } },
|
||||
{ term: { [SERVICE_NAME]: serviceName } },
|
||||
...rangeQuery(start, end),
|
||||
]),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return resp.hits.hits[0]?._source;
|
||||
}
|
|
@ -54,6 +54,7 @@ export {
|
|||
profilingAzureCostDiscountRate,
|
||||
apmEnableTransactionProfiling,
|
||||
apmEnableServiceInventoryTableSearchBar,
|
||||
profilingFetchTopNFunctionsFromStacktraces,
|
||||
} from './ui_settings_keys';
|
||||
|
||||
export {
|
||||
|
|
|
@ -43,3 +43,5 @@ export const profilingAWSCostDiscountRate = 'observability:profilingAWSCostDisco
|
|||
export const profilingCostPervCPUPerHour = 'observability:profilingCostPervCPUPerHour';
|
||||
export const profilingAzureCostDiscountRate = 'observability:profilingAzureCostDiscountRate';
|
||||
export const apmEnableTransactionProfiling = 'observability:apmEnableTransactionProfiling';
|
||||
export const profilingFetchTopNFunctionsFromStacktraces =
|
||||
'observability:profilingFetchTopNFunctionsFromStacktraces';
|
||||
|
|
|
@ -43,6 +43,7 @@ import {
|
|||
apmEnableTransactionProfiling,
|
||||
enableInfrastructureAssetCustomDashboards,
|
||||
apmEnableServiceInventoryTableSearchBar,
|
||||
profilingFetchTopNFunctionsFromStacktraces,
|
||||
} from '../common/ui_settings_keys';
|
||||
|
||||
const betaLabel = i18n.translate('xpack.observability.uiSettings.betaLabel', {
|
||||
|
@ -600,6 +601,21 @@ export const uiSettings: Record<string, UiSettings> = {
|
|||
schema: schema.boolean(),
|
||||
requiresPageReload: true,
|
||||
},
|
||||
[profilingFetchTopNFunctionsFromStacktraces]: {
|
||||
category: [observabilityFeatureId],
|
||||
name: i18n.translate('xpack.observability.profilingFetchTopNFunctionsFromStacktraces', {
|
||||
defaultMessage: 'Switch to fetch the TopN Functions from the Stacktraces API.',
|
||||
}),
|
||||
description: i18n.translate(
|
||||
'xpack.observability.profilingFetchTopNFunctionsFromStacktracesDescription',
|
||||
{
|
||||
defaultMessage: `The topN functions pages use the topN/functions API, turn it on to switch to the stacktraces api`,
|
||||
}
|
||||
),
|
||||
value: false,
|
||||
schema: schema.boolean(),
|
||||
requiresPageReload: false,
|
||||
},
|
||||
};
|
||||
|
||||
function throttlingDocsLink({ href }: { href: string }) {
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* 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 qs from 'query-string';
|
||||
import type { SerializableRecord } from '@kbn/utility-types';
|
||||
import type { LocatorDefinition, LocatorPublic } from '@kbn/share-plugin/public';
|
||||
|
||||
export interface ServiceOverviewParams extends SerializableRecord {
|
||||
serviceName: string;
|
||||
rangeFrom?: string;
|
||||
rangeTo?: string;
|
||||
}
|
||||
|
||||
export type ServiceOverviewLocator = LocatorPublic<ServiceOverviewParams>;
|
||||
|
||||
export class ServiceOverviewLocatorDefinition implements LocatorDefinition<ServiceOverviewParams> {
|
||||
public readonly id = 'serviceOverviewLocator';
|
||||
|
||||
public readonly getLocation = async ({
|
||||
rangeFrom,
|
||||
rangeTo,
|
||||
serviceName,
|
||||
}: ServiceOverviewParams) => {
|
||||
const params = { rangeFrom, rangeTo };
|
||||
return {
|
||||
app: 'apm',
|
||||
path: `/services/${serviceName}/overview?${qs.stringify(params)}`,
|
||||
state: {},
|
||||
};
|
||||
};
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* 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 qs from 'query-string';
|
||||
import type { SerializableRecord } from '@kbn/utility-types';
|
||||
import type { LocatorDefinition, LocatorPublic } from '@kbn/share-plugin/public';
|
||||
|
||||
export interface TransactionDetailsByNameParams extends SerializableRecord {
|
||||
serviceName: string;
|
||||
transactionName: string;
|
||||
rangeFrom?: string;
|
||||
rangeTo?: string;
|
||||
}
|
||||
|
||||
export type TransactionDetailsByNameLocator = LocatorPublic<TransactionDetailsByNameParams>;
|
||||
|
||||
export class TransactionDetailsByNameLocatorDefinition
|
||||
implements LocatorDefinition<TransactionDetailsByNameParams>
|
||||
{
|
||||
public readonly id = 'TransactionDetailsByNameLocator';
|
||||
|
||||
public readonly getLocation = async ({
|
||||
rangeFrom,
|
||||
rangeTo,
|
||||
serviceName,
|
||||
transactionName,
|
||||
}: TransactionDetailsByNameParams) => {
|
||||
const params = { rangeFrom, rangeTo, serviceName, transactionName };
|
||||
return {
|
||||
app: 'apm',
|
||||
path: `/link-to/transaction?${qs.stringify(params)}`,
|
||||
state: {},
|
||||
};
|
||||
};
|
||||
}
|
|
@ -39,7 +39,15 @@ import {
|
|||
type TopNFunctionsLocator,
|
||||
TopNFunctionsLocatorDefinition,
|
||||
} from './locators/profiling/topn_functions_locator';
|
||||
import {
|
||||
type ServiceOverviewLocator,
|
||||
ServiceOverviewLocatorDefinition,
|
||||
} from './locators/apm/service_overview_locator';
|
||||
import { updateGlobalNavigation } from './services/update_global_navigation';
|
||||
import {
|
||||
type TransactionDetailsByNameLocator,
|
||||
TransactionDetailsByNameLocatorDefinition,
|
||||
} from './locators/apm/transaction_details_by_name_locator';
|
||||
export interface ObservabilitySharedSetup {
|
||||
share: SharePluginSetup;
|
||||
}
|
||||
|
@ -67,6 +75,10 @@ interface ObservabilitySharedLocators {
|
|||
topNFunctionsLocator: TopNFunctionsLocator;
|
||||
stacktracesLocator: StacktracesLocator;
|
||||
};
|
||||
apm: {
|
||||
serviceOverview: ServiceOverviewLocator;
|
||||
transactionDetailsByName: TransactionDetailsByNameLocator;
|
||||
};
|
||||
}
|
||||
|
||||
export class ObservabilitySharedPlugin implements Plugin {
|
||||
|
@ -131,6 +143,12 @@ export class ObservabilitySharedPlugin implements Plugin {
|
|||
topNFunctionsLocator: urlService.locators.create(new TopNFunctionsLocatorDefinition()),
|
||||
stacktracesLocator: urlService.locators.create(new StacktracesLocatorDefinition()),
|
||||
},
|
||||
apm: {
|
||||
serviceOverview: urlService.locators.create(new ServiceOverviewLocatorDefinition()),
|
||||
transactionDetailsByName: urlService.locators.create(
|
||||
new TransactionDetailsByNameLocatorDefinition()
|
||||
),
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@ export function getRoutePaths() {
|
|||
TopNHosts: `${BASE_ROUTE_PATH}/topn/hosts`,
|
||||
TopNThreads: `${BASE_ROUTE_PATH}/topn/threads`,
|
||||
TopNTraces: `${BASE_ROUTE_PATH}/topn/traces`,
|
||||
APMTransactions: `${BASE_ROUTE_PATH}/topn/functions/apm/transactions`,
|
||||
Flamechart: `${BASE_ROUTE_PATH}/flamechart`,
|
||||
HasSetupESResources: `${BASE_ROUTE_PATH}/setup/es_resources`,
|
||||
SetupDataCollectionInstructions: `${BASE_ROUTE_PATH}/setup/instructions`,
|
||||
|
|
|
@ -31,9 +31,9 @@ describe('Differential Functions page', () => {
|
|||
cy.wait('@getTopNFunctions');
|
||||
[
|
||||
{ id: 'overallPerformance', value: '0%' },
|
||||
{ id: 'annualizedCo2', value: '74.49 lbs / 33.79 kg' },
|
||||
{ id: 'annualizedCost', value: '$318.32' },
|
||||
{ id: 'totalNumberOfSamples', value: '513' },
|
||||
{ id: 'annualizedCo2', value: '79.81 lbs / 36.2 kg' },
|
||||
{ id: 'annualizedCost', value: '$341.05' },
|
||||
{ id: 'totalNumberOfSamples', value: '17,186' },
|
||||
].forEach((item) => {
|
||||
cy.get(`[data-test-subj="${item.id}_value"]`).contains(item.value);
|
||||
cy.get(`[data-test-subj="${item.id}_comparison_value"]`).should('not.exist');
|
||||
|
@ -50,9 +50,9 @@ describe('Differential Functions page', () => {
|
|||
cy.wait('@getTopNFunctions');
|
||||
[
|
||||
{ id: 'overallPerformance', value: '0%' },
|
||||
{ id: 'annualizedCo2', value: '0 lbs / 0 kg', comparisonValue: '74.49 lbs / 33.79 kg' },
|
||||
{ id: 'annualizedCost', value: '$0', comparisonValue: '$318.32' },
|
||||
{ id: 'totalNumberOfSamples', value: '0', comparisonValue: '15,390' },
|
||||
{ id: 'annualizedCo2', value: '0 lbs / 0 kg', comparisonValue: '79.81 lbs / 36.2 kg' },
|
||||
{ id: 'annualizedCost', value: '$0', comparisonValue: '$341.05' },
|
||||
{ id: 'totalNumberOfSamples', value: '0', comparisonValue: '515,580' },
|
||||
].forEach((item) => {
|
||||
cy.get(`[data-test-subj="${item.id}_value"]`).contains(item.value);
|
||||
if (item.comparisonValue) {
|
||||
|
@ -73,23 +73,23 @@ describe('Differential Functions page', () => {
|
|||
cy.wait('@getTopNFunctions');
|
||||
cy.wait('@getTopNFunctions');
|
||||
[
|
||||
{ id: 'overallPerformance', value: '65.89%', icon: 'sortUp_success' },
|
||||
{ id: 'overallPerformance', value: '78.01%', icon: 'sortUp_success' },
|
||||
{
|
||||
id: 'annualizedCo2',
|
||||
value: '74.49 lbs / 33.79 kg',
|
||||
comparisonValue: '25.41 lbs / 11.53 kg (65.89%)',
|
||||
value: '9.81 lbs / 36.2 kg',
|
||||
comparisonValue: '28.23 lbs / 12.81 kg (64.62%)',
|
||||
icon: 'comparison_sortUp_success',
|
||||
},
|
||||
{
|
||||
id: 'annualizedCost',
|
||||
value: '$318.32',
|
||||
comparisonValue: '$108.59 (65.89%)',
|
||||
value: '$341.05',
|
||||
comparisonValue: '$120.65 (64.62%)',
|
||||
icon: 'comparison_sortUp_success',
|
||||
},
|
||||
{
|
||||
id: 'totalNumberOfSamples',
|
||||
value: '513',
|
||||
comparisonValue: '175 (65.89%)',
|
||||
value: '17,186',
|
||||
comparisonValue: '3,780 (78.01%)',
|
||||
icon: 'comparison_sortUp_success',
|
||||
},
|
||||
].forEach((item) => {
|
||||
|
@ -113,23 +113,23 @@ describe('Differential Functions page', () => {
|
|||
cy.wait('@getTopNFunctions');
|
||||
cy.wait('@getTopNFunctions');
|
||||
[
|
||||
{ id: 'overallPerformance', value: '193.14%', icon: 'sortDown_danger' },
|
||||
{ id: 'overallPerformance', value: '354.66%', icon: 'sortDown_danger' },
|
||||
{
|
||||
id: 'annualizedCo2',
|
||||
value: '25.41 lbs / 11.53 kg',
|
||||
comparisonValue: '74.49 lbs / 33.79 kg (193.14%)',
|
||||
value: '28.23 lbs / 12.81 kg',
|
||||
comparisonValue: '79.81 lbs / 36.2 kg (182.67%)',
|
||||
icon: 'comparison_sortDown_danger',
|
||||
},
|
||||
{
|
||||
id: 'annualizedCost',
|
||||
value: '$108.59',
|
||||
comparisonValue: '$318.32 (193.14%)',
|
||||
value: '$120.65',
|
||||
comparisonValue: '$341.05 (182.67%)',
|
||||
icon: 'comparison_sortDown_danger',
|
||||
},
|
||||
{
|
||||
id: 'totalNumberOfSamples',
|
||||
value: '175',
|
||||
comparisonValue: '513 (193.14%)',
|
||||
value: '3,780',
|
||||
comparisonValue: '17,186 (354.66%)',
|
||||
icon: 'comparison_sortDown_danger',
|
||||
},
|
||||
].forEach((item) => {
|
||||
|
|
|
@ -36,10 +36,10 @@ describe('Functions page', () => {
|
|||
const firstRowSelector = '[data-grid-row-index="0"] [data-test-subj="dataGridRowCell"]';
|
||||
cy.get(firstRowSelector).eq(1).contains('1');
|
||||
cy.get(firstRowSelector).eq(2).contains('vmlinux');
|
||||
cy.get(firstRowSelector).eq(3).contains('5.46%');
|
||||
cy.get(firstRowSelector).eq(4).contains('5.46%');
|
||||
cy.get(firstRowSelector).eq(5).contains('4.07 lbs / 1.84 kg');
|
||||
cy.get(firstRowSelector).eq(6).contains('$17.37');
|
||||
cy.get(firstRowSelector).eq(3).contains('0.16%');
|
||||
cy.get(firstRowSelector).eq(4).contains('0.16%');
|
||||
cy.get(firstRowSelector).eq(5).contains('4.41 lbs / 2 kg');
|
||||
cy.get(firstRowSelector).eq(6).contains('$18.61');
|
||||
cy.get(firstRowSelector).eq(7).contains('28');
|
||||
});
|
||||
|
||||
|
@ -56,8 +56,8 @@ describe('Functions page', () => {
|
|||
{ parentKey: 'informationRows', key: 'executable', value: 'vmlinux' },
|
||||
{ parentKey: 'informationRows', key: 'function', value: 'N/A' },
|
||||
{ parentKey: 'informationRows', key: 'sourceFile', value: 'N/A' },
|
||||
{ parentKey: 'impactEstimates', key: 'totalCPU', value: '5.46%' },
|
||||
{ parentKey: 'impactEstimates', key: 'selfCPU', value: '5.46%' },
|
||||
{ parentKey: 'impactEstimates', key: 'totalCPU', value: '0.16%' },
|
||||
{ parentKey: 'impactEstimates', key: 'selfCPU', value: '0.16%' },
|
||||
{ parentKey: 'impactEstimates', key: 'samples', value: '28' },
|
||||
{ parentKey: 'impactEstimates', key: 'selfSamples', value: '28' },
|
||||
{ parentKey: 'impactEstimates', key: 'coreSeconds', value: '1.4 seconds' },
|
||||
|
@ -66,16 +66,16 @@ describe('Functions page', () => {
|
|||
{ parentKey: 'impactEstimates', key: 'annualizedSelfCoreSeconds', value: '17.03 days' },
|
||||
{ parentKey: 'impactEstimates', key: 'co2Emission', value: '~0.00 lbs / ~0.00 kg' },
|
||||
{ parentKey: 'impactEstimates', key: 'selfCo2Emission', value: '~0.00 lbs / ~0.00 kg' },
|
||||
{ parentKey: 'impactEstimates', key: 'annualizedCo2Emission', value: '4.07 lbs / 1.84 kg' },
|
||||
{ parentKey: 'impactEstimates', key: 'annualizedCo2Emission', value: '4.41 lbs / 2 kg' },
|
||||
{
|
||||
parentKey: 'impactEstimates',
|
||||
key: 'annualizedSelfCo2Emission',
|
||||
value: '4.07 lbs / 1.84 kg',
|
||||
value: '4.41 lbs / 2 kg',
|
||||
},
|
||||
{ parentKey: 'impactEstimates', key: 'dollarCost', value: '$~0.00' },
|
||||
{ parentKey: 'impactEstimates', key: 'selfDollarCost', value: '$~0.00' },
|
||||
{ parentKey: 'impactEstimates', key: 'annualizedDollarCost', value: '$17.37' },
|
||||
{ parentKey: 'impactEstimates', key: 'annualizedSelfDollarCost', value: '$17.37' },
|
||||
{ parentKey: 'impactEstimates', key: 'annualizedDollarCost', value: '$18.61' },
|
||||
{ parentKey: 'impactEstimates', key: 'annualizedSelfDollarCost', value: '$18.61' },
|
||||
].forEach(({ parentKey, key, value }) => {
|
||||
cy.get(`[data-test-subj="${parentKey}_${key}"]`).contains(value);
|
||||
});
|
||||
|
@ -118,32 +118,32 @@ describe('Functions page', () => {
|
|||
columnIndex: 3,
|
||||
highRank: 1,
|
||||
lowRank: 389,
|
||||
highValue: '5.46%',
|
||||
highValue: '0.16%',
|
||||
lowValue: '0.00%',
|
||||
},
|
||||
{
|
||||
columnKey: 'totalCPU',
|
||||
columnIndex: 4,
|
||||
highRank: 3623,
|
||||
highRank: 693,
|
||||
lowRank: 44,
|
||||
highValue: '60.43%',
|
||||
lowValue: '0.19%',
|
||||
highValue: '1.80%',
|
||||
lowValue: '0.01%',
|
||||
},
|
||||
{
|
||||
columnKey: 'annualizedCo2',
|
||||
columnIndex: 5,
|
||||
highRank: 1,
|
||||
highRank: 693,
|
||||
lowRank: 44,
|
||||
highValue: '45.01 lbs / 20.42 kg',
|
||||
lowValue: '0.15 lbs / 0.07 kg',
|
||||
highValue: '48.28 lbs / 21.9 kg',
|
||||
lowValue: '0 lbs / 0 kg',
|
||||
},
|
||||
{
|
||||
columnKey: 'annualizedDollarCost',
|
||||
columnIndex: 6,
|
||||
highRank: 1,
|
||||
highRank: 693,
|
||||
lowRank: 44,
|
||||
highValue: '$192.36',
|
||||
lowValue: '$0.62',
|
||||
highValue: '$206.09',
|
||||
lowValue: '$0.66',
|
||||
},
|
||||
].forEach(({ columnKey, columnIndex, highRank, highValue, lowRank, lowValue }) => {
|
||||
cy.get(`[data-test-subj="dataGridHeaderCell-${columnKey}"]`).click();
|
||||
|
@ -170,7 +170,7 @@ describe('Functions page', () => {
|
|||
|
||||
cy.get('[data-test-subj="dataGridHeaderCell-frame"]').click();
|
||||
cy.contains('Sort A-Z').click();
|
||||
cy.get(firstRowSelector).eq(1).contains('371');
|
||||
cy.get(firstRowSelector).eq(1).contains('88');
|
||||
cy.get(firstRowSelector).eq(2).contains('/');
|
||||
});
|
||||
|
||||
|
@ -189,7 +189,7 @@ describe('Functions page', () => {
|
|||
const firstRowSelector = '[data-grid-row-index="0"] [data-test-subj="dataGridRowCell"]';
|
||||
cy.get(firstRowSelector).eq(1).contains('1');
|
||||
cy.get(firstRowSelector).eq(2).contains('vmlinux');
|
||||
cy.get(firstRowSelector).eq(5).contains('4.07 lbs / 1.84 kg');
|
||||
cy.get(firstRowSelector).eq(5).contains('4.41 lbs / 2 kg');
|
||||
cy.contains('Settings').click();
|
||||
cy.contains('Advanced Settings');
|
||||
cy.get(`[data-test-subj="management-settings-editField-${profilingCo2PerKWH}"]`)
|
||||
|
@ -208,7 +208,7 @@ describe('Functions page', () => {
|
|||
});
|
||||
cy.go('back');
|
||||
cy.wait('@getTopNFunctions');
|
||||
cy.get(firstRowSelector).eq(5).contains('1.87k lbs / 847.83 kg');
|
||||
cy.get(firstRowSelector).eq(5).contains('2k lbs / 908.4 kg');
|
||||
const firstRowSelectorActionButton =
|
||||
'[data-grid-row-index="0"] [data-test-subj="dataGridRowCell"] .euiButtonIcon';
|
||||
cy.get(firstRowSelectorActionButton).click();
|
||||
|
@ -218,12 +218,12 @@ describe('Functions page', () => {
|
|||
{
|
||||
parentKey: 'impactEstimates',
|
||||
key: 'annualizedCo2Emission',
|
||||
value: '1.87k lbs / 847.83 kg',
|
||||
value: '2k lbs / 908.4 kg',
|
||||
},
|
||||
{
|
||||
parentKey: 'impactEstimates',
|
||||
key: 'annualizedSelfCo2Emission',
|
||||
value: '1.87k lbs / 847.83 kg',
|
||||
value: '2k lbs / 908.4 kg',
|
||||
},
|
||||
].forEach(({ parentKey, key, value }) => {
|
||||
cy.get(`[data-test-subj="${parentKey}_${key}"]`).contains(value);
|
||||
|
|
|
@ -89,8 +89,8 @@ describe('Storage explorer page', () => {
|
|||
cy.wait('@indicesDetails');
|
||||
cy.get('table > tbody tr.euiTableRow').should('have.length', 10);
|
||||
});
|
||||
|
||||
it('displays a chart with percentage of each index', () => {
|
||||
// Skipping it we should not rely on dom elements from third-level libraries to write our tests
|
||||
it.skip('displays a chart with percentage of each index', () => {
|
||||
cy.intercept('GET', '/internal/profiling/storage_explorer/indices_storage_details?*').as(
|
||||
'indicesDetails'
|
||||
);
|
||||
|
@ -108,6 +108,7 @@ describe('Storage explorer page', () => {
|
|||
];
|
||||
|
||||
cy.get('.echChartPointerContainer table tbody tr').each(($row, idx) => {
|
||||
// These are no longer valid elements on charts
|
||||
cy.wrap($row).find('th').contains(indices[idx].index);
|
||||
cy.wrap($row).find('td').contains(indices[idx].perc);
|
||||
});
|
||||
|
|
|
@ -13,7 +13,8 @@
|
|||
"security",
|
||||
"cloud",
|
||||
"fleet",
|
||||
"observabilityAIAssistant"
|
||||
"observabilityAIAssistant",
|
||||
"apmDataAccess",
|
||||
],
|
||||
"requiredPlugins": [
|
||||
"charts",
|
||||
|
|
|
@ -0,0 +1,306 @@
|
|||
/*
|
||||
* 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 {
|
||||
Comparators,
|
||||
Criteria,
|
||||
EuiBasicTable,
|
||||
EuiBasicTableColumn,
|
||||
EuiFieldSearch,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiLink,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { isEmpty } from 'lodash';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import useDebounce from 'react-use/lib/useDebounce';
|
||||
import { NOT_AVAILABLE_LABEL } from '../../../common';
|
||||
import { AsyncStatus } from '../../hooks/use_async';
|
||||
import { useAnyOfProfilingParams } from '../../hooks/use_profiling_params';
|
||||
import { useTimeRange } from '../../hooks/use_time_range';
|
||||
import { useTimeRangeAsync } from '../../hooks/use_time_range_async';
|
||||
import { asNumber } from '../../utils/formatters/as_number';
|
||||
import { useProfilingDependencies } from '../contexts/profiling_dependencies/use_profiling_dependencies';
|
||||
|
||||
interface Props {
|
||||
serviceNames: Record<string, number>;
|
||||
functionName: string;
|
||||
}
|
||||
|
||||
interface ServicesAndTransactions {
|
||||
serviceName: string;
|
||||
serviceSamples: number;
|
||||
transactionName: string | null;
|
||||
transactionSamples: number | null;
|
||||
}
|
||||
|
||||
const findServicesAndTransactions = (
|
||||
servicesAndTransactions: ServicesAndTransactions[],
|
||||
pageIndex: number,
|
||||
pageSize: number,
|
||||
sortField: keyof ServicesAndTransactions,
|
||||
sortDirection: 'asc' | 'desc',
|
||||
filter: string
|
||||
) => {
|
||||
let filteredItems: ServicesAndTransactions[] = servicesAndTransactions;
|
||||
if (!isEmpty(filter)) {
|
||||
filteredItems = servicesAndTransactions.filter((item) => item.serviceName.includes(filter));
|
||||
}
|
||||
|
||||
let sortedItems: ServicesAndTransactions[];
|
||||
if (sortField) {
|
||||
sortedItems = filteredItems
|
||||
.slice(0)
|
||||
.sort(Comparators.property(sortField, Comparators.default(sortDirection)));
|
||||
} else {
|
||||
sortedItems = filteredItems;
|
||||
}
|
||||
|
||||
let pageOfItems;
|
||||
|
||||
if (!pageIndex && !pageSize) {
|
||||
pageOfItems = sortedItems;
|
||||
} else {
|
||||
const startIndex = pageIndex * pageSize;
|
||||
pageOfItems = sortedItems.slice(
|
||||
startIndex,
|
||||
Math.min(startIndex + pageSize, filteredItems.length)
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
pageOfItems,
|
||||
totalItemCount: filteredItems.length,
|
||||
};
|
||||
};
|
||||
|
||||
export function APMTransactions({ functionName, serviceNames }: Props) {
|
||||
const {
|
||||
query: { rangeFrom, rangeTo },
|
||||
} = useAnyOfProfilingParams('/functions/*', '/flamegraphs/*');
|
||||
const timeRange = useTimeRange({ rangeFrom, rangeTo });
|
||||
|
||||
const {
|
||||
services: { fetchTopNFunctionAPMTransactions },
|
||||
setup: { observabilityShared },
|
||||
} = useProfilingDependencies();
|
||||
|
||||
const [pageIndex, setPageIndex] = useState(0);
|
||||
const [pageSize, setPageSize] = useState(5);
|
||||
const [sortField, setSortField] = useState<keyof ServicesAndTransactions>('serviceSamples');
|
||||
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc');
|
||||
const [filter, setFilter] = useState('');
|
||||
const [filterDebounced, setFilterDebounced] = useState('');
|
||||
|
||||
useDebounce(
|
||||
() => {
|
||||
setFilterDebounced(filter);
|
||||
},
|
||||
500,
|
||||
[filter]
|
||||
);
|
||||
|
||||
const onTableChange = ({ page, sort }: Criteria<ServicesAndTransactions>) => {
|
||||
if (page) {
|
||||
const { index, size } = page;
|
||||
setPageIndex(index);
|
||||
setPageSize(size);
|
||||
}
|
||||
if (sort) {
|
||||
const { field, direction } = sort;
|
||||
setSortField(field);
|
||||
setSortDirection(direction);
|
||||
}
|
||||
};
|
||||
|
||||
const initialServices: ServicesAndTransactions[] = useMemo(() => {
|
||||
return Object.keys(serviceNames).map((key) => {
|
||||
const samples = serviceNames[key];
|
||||
return {
|
||||
serviceName: key,
|
||||
serviceSamples: samples,
|
||||
transactionName: null,
|
||||
transactionSamples: null,
|
||||
};
|
||||
});
|
||||
}, [serviceNames]);
|
||||
|
||||
const { pageOfItems, totalItemCount } = useMemo(
|
||||
() =>
|
||||
findServicesAndTransactions(
|
||||
initialServices,
|
||||
pageIndex,
|
||||
pageSize,
|
||||
sortField,
|
||||
sortDirection,
|
||||
filterDebounced
|
||||
),
|
||||
[initialServices, pageIndex, pageSize, sortField, sortDirection, filterDebounced]
|
||||
);
|
||||
|
||||
const { status, data: transactionsPerServiceMap = pageOfItems } = useTimeRangeAsync(
|
||||
({ http }) => {
|
||||
const serviceNamesToSearch = pageOfItems.map((item) => item.serviceName).sort();
|
||||
if (serviceNamesToSearch.length) {
|
||||
return fetchTopNFunctionAPMTransactions({
|
||||
http,
|
||||
timeFrom: new Date(timeRange.start).getTime(),
|
||||
timeTo: new Date(timeRange.end).getTime(),
|
||||
functionName,
|
||||
serviceNames: serviceNamesToSearch,
|
||||
}).then((resp) => {
|
||||
return pageOfItems.flatMap((item) => {
|
||||
const transactionDetails = resp[item.serviceName];
|
||||
if (transactionDetails?.transactions?.length) {
|
||||
return transactionDetails.transactions.map((transaction) => ({
|
||||
...item,
|
||||
transactionName: transaction.name,
|
||||
transactionSamples: transaction.samples,
|
||||
}));
|
||||
}
|
||||
return [item];
|
||||
});
|
||||
});
|
||||
}
|
||||
return Promise.resolve(pageOfItems);
|
||||
},
|
||||
[fetchTopNFunctionAPMTransactions, functionName, pageOfItems, timeRange.end, timeRange.start]
|
||||
);
|
||||
|
||||
const isLoadingTransactions = status !== AsyncStatus.Settled;
|
||||
|
||||
const columns: Array<EuiBasicTableColumn<ServicesAndTransactions>> = useMemo(
|
||||
() => [
|
||||
{
|
||||
field: 'serviceName',
|
||||
name: i18n.translate('xpack.profiling.apmTransactions.columns.serviceName', {
|
||||
defaultMessage: 'Service Name',
|
||||
}),
|
||||
truncateText: true,
|
||||
sortable: true,
|
||||
render: (_, { serviceName }) => {
|
||||
return (
|
||||
<EuiLink
|
||||
data-test-subj="profilingColumnsLink"
|
||||
href={observabilityShared.locators.apm.serviceOverview.getRedirectUrl({
|
||||
serviceName,
|
||||
rangeFrom,
|
||||
rangeTo,
|
||||
})}
|
||||
>
|
||||
{serviceName}
|
||||
</EuiLink>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'serviceSamples',
|
||||
name: i18n.translate('xpack.profiling.apmTransactions.columns.serviceSamplesName', {
|
||||
defaultMessage: 'Service Samples',
|
||||
}),
|
||||
width: '150px',
|
||||
sortable: true,
|
||||
render(_, { serviceSamples }) {
|
||||
return asNumber(serviceSamples);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'transactionName',
|
||||
name: i18n.translate('xpack.profiling.apmTransactions.columns.transactionName', {
|
||||
defaultMessage: 'Transaction Name',
|
||||
}),
|
||||
truncateText: true,
|
||||
render(_, { serviceName, transactionName }) {
|
||||
if (isLoadingTransactions) {
|
||||
return '--';
|
||||
}
|
||||
|
||||
if (transactionName) {
|
||||
return (
|
||||
<EuiLink
|
||||
data-test-subj="profilingColumnsLink"
|
||||
href={observabilityShared.locators.apm.transactionDetailsByName.getRedirectUrl({
|
||||
serviceName,
|
||||
transactionName,
|
||||
rangeFrom,
|
||||
rangeTo,
|
||||
})}
|
||||
>
|
||||
{transactionName}
|
||||
</EuiLink>
|
||||
);
|
||||
}
|
||||
return NOT_AVAILABLE_LABEL;
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'transactionSamples',
|
||||
name: i18n.translate('xpack.profiling.apmTransactions.columns.transactionSamples', {
|
||||
defaultMessage: 'Transaction Samples',
|
||||
}),
|
||||
width: '150px',
|
||||
render(_, { transactionSamples }) {
|
||||
if (isLoadingTransactions) {
|
||||
return '--';
|
||||
}
|
||||
|
||||
if (transactionSamples === null) {
|
||||
return NOT_AVAILABLE_LABEL;
|
||||
}
|
||||
return asNumber(transactionSamples);
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
isLoadingTransactions,
|
||||
observabilityShared.locators.apm.serviceOverview,
|
||||
observabilityShared.locators.apm.transactionDetailsByName,
|
||||
rangeFrom,
|
||||
rangeTo,
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup direction="column" gutterSize="s">
|
||||
<EuiFlexItem>
|
||||
<EuiFieldSearch
|
||||
data-test-subj="profilingAPMTransactionsFieldText"
|
||||
value={filter}
|
||||
onChange={(e) => setFilter(e.target.value)}
|
||||
isClearable
|
||||
fullWidth
|
||||
placeholder={i18n.translate('xpack.profiling.apmTransactions.searchPlaceholder', {
|
||||
defaultMessage: 'Search services by name',
|
||||
})}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiBasicTable
|
||||
loading={isLoadingTransactions}
|
||||
tableCaption={i18n.translate('xpack.profiling.apmTransactions.tableCaption', {
|
||||
defaultMessage: 'APM Services and Transactions links',
|
||||
})}
|
||||
items={transactionsPerServiceMap}
|
||||
columns={columns}
|
||||
pagination={{
|
||||
pageIndex,
|
||||
pageSize,
|
||||
totalItemCount,
|
||||
showPerPageOptions: false,
|
||||
}}
|
||||
sorting={{
|
||||
sort: {
|
||||
field: sortField,
|
||||
direction: sortDirection,
|
||||
},
|
||||
}}
|
||||
onChange={onTableChange}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
|
@ -8,6 +8,7 @@
|
|||
import React, { useMemo } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { Message } from '@kbn/observability-ai-assistant-plugin/public';
|
||||
import { EuiFlexItem } from '@elastic/eui';
|
||||
import { Frame } from '.';
|
||||
import { useProfilingDependencies } from '../contexts/profiling_dependencies/use_profiling_dependencies';
|
||||
|
||||
|
@ -78,12 +79,14 @@ export function FrameInformationAIAssistant({ frame }: Props) {
|
|||
return (
|
||||
<>
|
||||
{observabilityAIAssistant?.ObservabilityAIAssistantContextualInsight && promptMessages ? (
|
||||
<observabilityAIAssistant.ObservabilityAIAssistantContextualInsight
|
||||
messages={promptMessages}
|
||||
title={i18n.translate('xpack.profiling.frameInformationWindow.optimizeFunction', {
|
||||
defaultMessage: 'Optimize function',
|
||||
})}
|
||||
/>
|
||||
<EuiFlexItem>
|
||||
<observabilityAIAssistant.ObservabilityAIAssistantContextualInsight
|
||||
messages={promptMessages}
|
||||
title={i18n.translate('xpack.profiling.frameInformationWindow.optimizeFunction', {
|
||||
defaultMessage: 'Optimize function',
|
||||
})}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -4,12 +4,24 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiStat, EuiTitle } from '@elastic/eui';
|
||||
import {
|
||||
EuiAccordion,
|
||||
EuiAccordionProps,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiIcon,
|
||||
EuiStat,
|
||||
EuiText,
|
||||
EuiTextColor,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FrameSymbolStatus, getFrameSymbolStatus } from '@kbn/profiling-utils';
|
||||
import React from 'react';
|
||||
import { isEmpty } from 'lodash';
|
||||
import React, { useState } from 'react';
|
||||
import { useCalculateImpactEstimate } from '../../hooks/use_calculate_impact_estimates';
|
||||
import { FramesSummary } from '../frames_summary';
|
||||
import { APMTransactions } from './apm_transactions';
|
||||
import { EmptyFrame } from './empty_frame';
|
||||
import { FrameInformationAIAssistant } from './frame_information_ai_assistant';
|
||||
import { FrameInformationPanel } from './frame_information_panel';
|
||||
|
@ -32,6 +44,7 @@ export interface Frame {
|
|||
totalAnnualCO2Kgs: number;
|
||||
selfAnnualCostUSD: number;
|
||||
totalAnnualCostUSD: number;
|
||||
subGroups?: Record<string, number>;
|
||||
}
|
||||
|
||||
export interface Props {
|
||||
|
@ -59,6 +72,7 @@ export function FrameInformationWindow({
|
|||
rank,
|
||||
compressed = false,
|
||||
}: Props) {
|
||||
const [accordionState, setAccordionState] = useState<EuiAccordionProps['forceState']>('closed');
|
||||
const calculateImpactEstimates = useCalculateImpactEstimate();
|
||||
|
||||
if (!frame) {
|
||||
|
@ -79,6 +93,7 @@ export function FrameInformationWindow({
|
|||
functionName,
|
||||
sourceFileName,
|
||||
sourceLine,
|
||||
subGroups = {},
|
||||
} = frame;
|
||||
|
||||
const informationRows = getInformationRows({
|
||||
|
@ -143,14 +158,59 @@ export function FrameInformationWindow({
|
|||
))}
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<FrameInformationAIAssistant frame={frame} />
|
||||
</EuiFlexItem>
|
||||
<FrameInformationAIAssistant frame={frame} />
|
||||
{showSymbolsStatus && symbolStatus !== FrameSymbolStatus.SYMBOLIZED ? (
|
||||
<EuiFlexItem>
|
||||
<MissingSymbolsCallout frameType={frame.frameType} />
|
||||
</EuiFlexItem>
|
||||
) : null}
|
||||
{isEmpty(subGroups) ? null : (
|
||||
<EuiFlexItem>
|
||||
<EuiAccordion
|
||||
id="apmTransactions"
|
||||
borders="horizontal"
|
||||
buttonProps={{ paddingSize: 'm' }}
|
||||
buttonContent={
|
||||
<div>
|
||||
<EuiFlexGroup gutterSize="s" alignItems="center" responsive={false}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIcon type="apmApp" />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiTitle size="xs">
|
||||
<h3>
|
||||
{i18n.translate(
|
||||
'xpack.profiling.frameInformationWindow.apmTransactions',
|
||||
{ defaultMessage: 'Distributed Tracing Correlation' }
|
||||
)}
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiText size="s">
|
||||
<p>
|
||||
<EuiTextColor color="subdued">
|
||||
{i18n.translate(
|
||||
'xpack.profiling.frameInformationWindow.apmTransactions.description',
|
||||
{
|
||||
defaultMessage:
|
||||
'A curated view of APM services and transactions that call this function.',
|
||||
}
|
||||
)}
|
||||
</EuiTextColor>
|
||||
</p>
|
||||
</EuiText>
|
||||
</div>
|
||||
}
|
||||
forceState={accordionState}
|
||||
onToggle={(isOpen) => setAccordionState(isOpen ? 'open' : 'closed')}
|
||||
>
|
||||
{accordionState === 'open' ? (
|
||||
<APMTransactions functionName={functionName} serviceNames={subGroups} />
|
||||
) : null}
|
||||
</EuiAccordion>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
<EuiFlexItem>
|
||||
<FramesSummary
|
||||
compressed={compressed}
|
||||
|
|
|
@ -48,6 +48,7 @@ export interface IFunctionRow {
|
|||
selfAnnualCostUSD: number;
|
||||
totalAnnualCO2kgs: number;
|
||||
totalAnnualCostUSD: number;
|
||||
subGroups?: Record<string, number>;
|
||||
diff?: {
|
||||
rank: number;
|
||||
samples: number;
|
||||
|
@ -149,6 +150,7 @@ export function getFunctionsRows({
|
|||
selfAnnualCostUSD: topN.selfAnnualCostUSD,
|
||||
totalAnnualCO2kgs: topN.totalAnnualCO2kgs,
|
||||
totalAnnualCostUSD: topN.totalAnnualCostUSD,
|
||||
subGroups: topN.subGroups,
|
||||
diff: calculateDiff(),
|
||||
};
|
||||
});
|
||||
|
@ -214,5 +216,6 @@ export function convertRowToFrame(row: IFunctionRow) {
|
|||
totalAnnualCO2Kgs: row.totalAnnualCO2kgs,
|
||||
selfAnnualCostUSD: row.selfAnnualCostUSD,
|
||||
totalAnnualCostUSD: row.totalAnnualCostUSD,
|
||||
subGroups: row.subGroups,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -23,6 +23,13 @@ import { TopNResponse } from '../common/topn';
|
|||
import type { SetupDataCollectionInstructions } from '../server/routes/setup/get_cloud_setup_instructions';
|
||||
import { AutoAbortedHttpService } from './hooks/use_auto_aborted_http_client';
|
||||
|
||||
export interface APMTransactionsPerService {
|
||||
[serviceName: string]: {
|
||||
serviceName: string;
|
||||
transactions: Array<{ name: string | null; samples: number | null }>;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ProfilingSetupStatus {
|
||||
has_setup: boolean;
|
||||
has_data: boolean;
|
||||
|
@ -77,6 +84,13 @@ export interface Services {
|
|||
http: AutoAbortedHttpService;
|
||||
indexLifecyclePhase: IndexLifecyclePhaseSelectOption;
|
||||
}) => Promise<IndicesStorageDetailsAPIResponse>;
|
||||
fetchTopNFunctionAPMTransactions: (params: {
|
||||
http: AutoAbortedHttpService;
|
||||
timeFrom: number;
|
||||
timeTo: number;
|
||||
functionName: string;
|
||||
serviceNames: string[];
|
||||
}) => Promise<APMTransactionsPerService>;
|
||||
}
|
||||
|
||||
export function getServices(): Services {
|
||||
|
@ -167,5 +181,16 @@ export function getServices(): Services {
|
|||
)) as IndicesStorageDetailsAPIResponse;
|
||||
return eventsMetricsSizeTimeseries;
|
||||
},
|
||||
fetchTopNFunctionAPMTransactions: ({ functionName, http, serviceNames, timeFrom, timeTo }) => {
|
||||
const query: HttpFetchQuery = {
|
||||
timeFrom,
|
||||
timeTo,
|
||||
functionName,
|
||||
serviceNames: JSON.stringify(serviceNames),
|
||||
};
|
||||
return http.get(paths.APMTransactions, {
|
||||
query,
|
||||
}) as Promise<APMTransactionsPerService>;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@ import {
|
|||
profilingAzureCostDiscountRate,
|
||||
profilingCostPervCPUPerHour,
|
||||
profilingShowErrorFrames,
|
||||
profilingFetchTopNFunctionsFromStacktraces,
|
||||
} from '@kbn/observability-plugin/common';
|
||||
import { useEditableSettings, useUiTracker } from '@kbn/observability-shared-plugin/public';
|
||||
import { isEmpty } from 'lodash';
|
||||
|
@ -53,7 +54,7 @@ const costSettings = [
|
|||
profilingAzureCostDiscountRate,
|
||||
profilingCostPervCPUPerHour,
|
||||
];
|
||||
const miscSettings = [profilingShowErrorFrames];
|
||||
const miscSettings = [profilingShowErrorFrames, profilingFetchTopNFunctionsFromStacktraces];
|
||||
|
||||
export function Settings() {
|
||||
const trackProfilingEvent = useUiTracker({ app: 'profiling' });
|
||||
|
|
|
@ -0,0 +1,119 @@
|
|||
/*
|
||||
* 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 { schema, TypeOf } from '@kbn/config-schema';
|
||||
import { termQuery } from '@kbn/observability-plugin/server';
|
||||
import { keyBy } from 'lodash';
|
||||
import { IDLE_SOCKET_TIMEOUT, RouteRegisterParameters } from '.';
|
||||
import { getRoutePaths } from '../../common';
|
||||
import { handleRouteHandlerError } from '../utils/handle_route_error_handler';
|
||||
import { getClient } from './compat';
|
||||
|
||||
const querySchema = schema.object({
|
||||
timeFrom: schema.number(),
|
||||
timeTo: schema.number(),
|
||||
functionName: schema.string(),
|
||||
serviceNames: schema.arrayOf(schema.string()),
|
||||
});
|
||||
|
||||
type QuerySchemaType = TypeOf<typeof querySchema>;
|
||||
|
||||
export function registerTopNFunctionsAPMTransactionsRoute({
|
||||
router,
|
||||
logger,
|
||||
dependencies: {
|
||||
start: { profilingDataAccess },
|
||||
setup: { apmDataAccess },
|
||||
},
|
||||
}: RouteRegisterParameters) {
|
||||
const paths = getRoutePaths();
|
||||
router.get(
|
||||
{
|
||||
path: paths.APMTransactions,
|
||||
options: {
|
||||
tags: ['access:profiling', 'access:apm'],
|
||||
timeout: { idleSocket: IDLE_SOCKET_TIMEOUT },
|
||||
},
|
||||
validate: { query: querySchema },
|
||||
},
|
||||
async (context, request, response) => {
|
||||
try {
|
||||
if (!apmDataAccess) {
|
||||
return response.ok({
|
||||
body: [],
|
||||
});
|
||||
}
|
||||
const core = await context.core;
|
||||
const { transaction: transactionIndices } = await apmDataAccess.getApmIndices(
|
||||
core.savedObjects.client
|
||||
);
|
||||
|
||||
const esClient = await getClient(context);
|
||||
|
||||
const { timeFrom, timeTo, functionName, serviceNames }: QuerySchemaType = request.query;
|
||||
const startSecs = timeFrom / 1000;
|
||||
const endSecs = timeTo / 1000;
|
||||
|
||||
const transactionsPerService = await Promise.all(
|
||||
serviceNames.slice(0, 5).map(async (serviceName) => {
|
||||
const apmFunctions = await profilingDataAccess.services.fetchESFunctions({
|
||||
core,
|
||||
esClient,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
...termQuery('service.name', serviceName),
|
||||
{
|
||||
range: {
|
||||
['@timestamp']: {
|
||||
gte: String(startSecs),
|
||||
lt: String(endSecs),
|
||||
format: 'epoch_second',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
aggregationField: 'transaction.name',
|
||||
indices: transactionIndices.split(','),
|
||||
stacktraceIdsField: 'transaction.profiler_stack_trace_ids',
|
||||
limit: 1000,
|
||||
});
|
||||
const apmFunction = apmFunctions.TopN.find(
|
||||
(topNFunction) => topNFunction.Frame.FunctionName === functionName
|
||||
);
|
||||
|
||||
if (apmFunction?.subGroups) {
|
||||
const subGroups = apmFunction.subGroups;
|
||||
return {
|
||||
serviceName,
|
||||
transactions: Object.keys(subGroups).map((key) => ({
|
||||
name: key,
|
||||
samples: subGroups[key],
|
||||
})),
|
||||
};
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const transactionsGroupedByService = keyBy(transactionsPerService, 'serviceName');
|
||||
|
||||
return response.ok({
|
||||
body: transactionsGroupedByService,
|
||||
});
|
||||
} catch (error) {
|
||||
return handleRouteHandlerError({
|
||||
error,
|
||||
logger,
|
||||
response,
|
||||
message: 'Error while fetching TopN functions',
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import { schema, TypeOf } from '@kbn/config-schema';
|
||||
import { kqlQuery } from '@kbn/observability-plugin/server';
|
||||
import { profilingFetchTopNFunctionsFromStacktraces } from '@kbn/observability-plugin/common';
|
||||
import { IDLE_SOCKET_TIMEOUT, RouteRegisterParameters } from '.';
|
||||
import { getRoutePaths } from '../../common';
|
||||
import { handleRouteHandlerError } from '../utils/handle_route_error_handler';
|
||||
|
@ -45,29 +46,43 @@ export function registerTopNFunctionsSearchRoute({
|
|||
const endSecs = timeTo / 1000;
|
||||
|
||||
const esClient = await getClient(context);
|
||||
const topNFunctions = await profilingDataAccess.services.fetchFunctions({
|
||||
core,
|
||||
esClient,
|
||||
startIndex,
|
||||
endIndex,
|
||||
totalSeconds: endSecs - startSecs,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
...kqlQuery(kuery),
|
||||
{
|
||||
range: {
|
||||
['@timestamp']: {
|
||||
gte: String(startSecs),
|
||||
lt: String(endSecs),
|
||||
format: 'epoch_second',
|
||||
},
|
||||
|
||||
const query = {
|
||||
bool: {
|
||||
filter: [
|
||||
...kqlQuery(kuery),
|
||||
{
|
||||
range: {
|
||||
['@timestamp']: {
|
||||
gte: String(startSecs),
|
||||
lt: String(endSecs),
|
||||
format: 'epoch_second',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const useStacktracesAPI = await core.uiSettings.client.get<boolean>(
|
||||
profilingFetchTopNFunctionsFromStacktraces
|
||||
);
|
||||
|
||||
const topNFunctions = useStacktracesAPI
|
||||
? await profilingDataAccess.services.fetchFunctions({
|
||||
core,
|
||||
esClient,
|
||||
startIndex,
|
||||
endIndex,
|
||||
totalSeconds: endSecs - startSecs,
|
||||
query,
|
||||
})
|
||||
: await profilingDataAccess.services.fetchESFunctions({
|
||||
core,
|
||||
esClient,
|
||||
query,
|
||||
aggregationField: 'service.name',
|
||||
});
|
||||
|
||||
return response.ok({
|
||||
body: topNFunctions,
|
||||
|
|
|
@ -16,6 +16,7 @@ import {
|
|||
TelemetryUsageCounter,
|
||||
} from '../types';
|
||||
import { ProfilingESClient } from '../utils/create_profiling_es_client';
|
||||
import { registerTopNFunctionsAPMTransactionsRoute } from './apm';
|
||||
import { registerFlameChartSearchRoute } from './flamechart';
|
||||
import { registerTopNFunctionsSearchRoute } from './functions';
|
||||
import { registerSetupRoute } from './setup/route';
|
||||
|
@ -61,4 +62,5 @@ export function registerRoutes(params: RouteRegisterParameters) {
|
|||
// and will show instructions on how to add data
|
||||
registerSetupRoute(params);
|
||||
registerStorageExplorerRoute(params);
|
||||
registerTopNFunctionsAPMTransactionsRoute(params);
|
||||
}
|
||||
|
|
|
@ -69,6 +69,7 @@ describe('TopN data from Elasticsearch', () => {
|
|||
},
|
||||
}) as Promise<any>
|
||||
),
|
||||
topNFunctions: jest.fn(),
|
||||
};
|
||||
const logger = loggerMock.create();
|
||||
|
||||
|
|
|
@ -17,6 +17,10 @@ import {
|
|||
ProfilingDataAccessPluginStart,
|
||||
} from '@kbn/profiling-data-access-plugin/server';
|
||||
import { SecurityPluginSetup, SecurityPluginStart } from '@kbn/security-plugin/server';
|
||||
import {
|
||||
ApmDataAccessPluginSetup,
|
||||
ApmDataAccessPluginStart,
|
||||
} from '@kbn/apm-data-access-plugin/server';
|
||||
|
||||
export interface ProfilingPluginSetupDeps {
|
||||
observability: ObservabilityPluginSetup;
|
||||
|
@ -27,6 +31,7 @@ export interface ProfilingPluginSetupDeps {
|
|||
usageCollection?: UsageCollectionSetup;
|
||||
profilingDataAccess: ProfilingDataAccessPluginSetup;
|
||||
security?: SecurityPluginSetup;
|
||||
apmDataAccess?: ApmDataAccessPluginSetup;
|
||||
}
|
||||
|
||||
export interface ProfilingPluginStartDeps {
|
||||
|
@ -37,6 +42,7 @@ export interface ProfilingPluginStartDeps {
|
|||
spaces?: SpacesPluginStart;
|
||||
profilingDataAccess: ProfilingDataAccessPluginStart;
|
||||
security?: SecurityPluginStart;
|
||||
apmDataAccess?: ApmDataAccessPluginStart;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
|
|
|
@ -11,7 +11,9 @@ import type { KibanaRequest } from '@kbn/core/server';
|
|||
import { unwrapEsResponse } from '@kbn/observability-plugin/server';
|
||||
import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import type {
|
||||
AggregationField,
|
||||
BaseFlameGraph,
|
||||
ESTopNFunctions,
|
||||
ProfilingStatusResponse,
|
||||
StackTraceResponse,
|
||||
} from '@kbn/profiling-utils';
|
||||
|
@ -45,6 +47,21 @@ export interface ProfilingESClient {
|
|||
query: QueryDslQueryContainer;
|
||||
sampleSize: number;
|
||||
}): Promise<BaseFlameGraph>;
|
||||
topNFunctions(params: {
|
||||
query: QueryDslQueryContainer;
|
||||
limit?: number;
|
||||
sampleSize?: number;
|
||||
indices?: string[];
|
||||
stacktraceIdsField?: string;
|
||||
aggregationField?: AggregationField;
|
||||
co2PerKWH?: number;
|
||||
datacenterPUE?: number;
|
||||
pervCPUWattX86?: number;
|
||||
pervCPUWattArm64?: number;
|
||||
awsCostDiscountRate?: number;
|
||||
azureCostDiscountRate?: number;
|
||||
costPervCPUPerHour?: number;
|
||||
}): Promise<ESTopNFunctions>;
|
||||
}
|
||||
|
||||
export function createProfilingEsClient({
|
||||
|
@ -151,5 +168,51 @@ export function createProfilingEsClient({
|
|||
});
|
||||
return unwrapEsResponse(promise) as Promise<BaseFlameGraph>;
|
||||
},
|
||||
topNFunctions({
|
||||
query,
|
||||
aggregationField,
|
||||
indices,
|
||||
stacktraceIdsField,
|
||||
co2PerKWH,
|
||||
datacenterPUE,
|
||||
awsCostDiscountRate,
|
||||
costPervCPUPerHour,
|
||||
pervCPUWattArm64,
|
||||
pervCPUWattX86,
|
||||
azureCostDiscountRate,
|
||||
sampleSize,
|
||||
limit,
|
||||
}) {
|
||||
const controller = new AbortController();
|
||||
|
||||
const promise = withProfilingSpan('_profiling/topn/functions', () => {
|
||||
return esClient.transport.request(
|
||||
{
|
||||
method: 'POST',
|
||||
path: encodeURI('/_profiling/topn/functions'),
|
||||
body: {
|
||||
query,
|
||||
sample_size: sampleSize,
|
||||
limit,
|
||||
indices,
|
||||
stacktrace_ids_field: stacktraceIdsField,
|
||||
aggregation_field: aggregationField,
|
||||
co2_per_kwh: co2PerKWH,
|
||||
per_core_watt_x86: pervCPUWattX86,
|
||||
per_core_watt_arm64: pervCPUWattArm64,
|
||||
datacenter_pue: datacenterPUE,
|
||||
aws_cost_factor: awsCostDiscountRate,
|
||||
cost_per_core_hour: costPervCPUPerHour,
|
||||
azure_cost_factor: azureCostDiscountRate,
|
||||
},
|
||||
},
|
||||
{
|
||||
signal: controller.signal,
|
||||
meta: true,
|
||||
}
|
||||
);
|
||||
});
|
||||
return unwrapEsResponse(promise) as Promise<ESTopNFunctions>;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -53,7 +53,8 @@
|
|||
"@kbn/security-plugin",
|
||||
"@kbn/shared-ux-utility",
|
||||
"@kbn/management-settings-components-field-row",
|
||||
"@kbn/deeplinks-observability"
|
||||
"@kbn/deeplinks-observability",
|
||||
"@kbn/apm-data-access-plugin"
|
||||
// add references to other TypeScript projects the plugin depends on
|
||||
|
||||
// requiredPlugins from ./kibana.json
|
||||
|
|
|
@ -9,7 +9,9 @@ import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWith
|
|||
import { ElasticsearchClient } from '@kbn/core/server';
|
||||
import type { ESSearchRequest, InferSearchResponseOf } from '@kbn/es-types';
|
||||
import type {
|
||||
AggregationField,
|
||||
BaseFlameGraph,
|
||||
ESTopNFunctions,
|
||||
ProfilingStatusResponse,
|
||||
StackTraceResponse,
|
||||
} from '@kbn/profiling-utils';
|
||||
|
@ -49,4 +51,19 @@ export interface ProfilingESClient {
|
|||
indices?: string[];
|
||||
stacktraceIdsField?: string;
|
||||
}): Promise<BaseFlameGraph>;
|
||||
topNFunctions(params: {
|
||||
query: QueryDslQueryContainer;
|
||||
limit?: number;
|
||||
sampleSize?: number;
|
||||
indices?: string[];
|
||||
stacktraceIdsField?: string;
|
||||
aggregationField?: AggregationField;
|
||||
co2PerKWH?: number;
|
||||
datacenterPUE?: number;
|
||||
pervCPUWattX86?: number;
|
||||
pervCPUWattArm64?: number;
|
||||
awsCostDiscountRate?: number;
|
||||
azureCostDiscountRate?: number;
|
||||
costPervCPUPerHour?: number;
|
||||
}): Promise<ESTopNFunctions>;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,129 @@
|
|||
/*
|
||||
* 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 {
|
||||
profilingAWSCostDiscountRate,
|
||||
profilingCo2PerKWH,
|
||||
profilingCostPervCPUPerHour,
|
||||
profilingDatacenterPUE,
|
||||
profilingPervCPUWattArm64,
|
||||
profilingPervCPUWattX86,
|
||||
profilingAzureCostDiscountRate,
|
||||
profilingShowErrorFrames,
|
||||
} from '@kbn/observability-plugin/common';
|
||||
import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import { CoreRequestHandlerContext, ElasticsearchClient } from '@kbn/core/server';
|
||||
import {
|
||||
AggregationField,
|
||||
convertTonsToKgs,
|
||||
ESTopNFunctions,
|
||||
TopNFunctions,
|
||||
} from '@kbn/profiling-utils';
|
||||
import { RegisterServicesParams } from '../register_services';
|
||||
import { percentToFactor } from '../../utils/percent_to_factor';
|
||||
|
||||
export interface FetchFunctionsParams {
|
||||
core: CoreRequestHandlerContext;
|
||||
esClient: ElasticsearchClient;
|
||||
indices?: string[];
|
||||
stacktraceIdsField?: string;
|
||||
query: QueryDslQueryContainer;
|
||||
aggregationField?: AggregationField;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
const targetSampleSize = 20000; // minimum number of samples to get statistically sound results
|
||||
|
||||
export function createFetchESFunctions({ createProfilingEsClient }: RegisterServicesParams) {
|
||||
return async ({
|
||||
core,
|
||||
esClient,
|
||||
indices,
|
||||
stacktraceIdsField,
|
||||
query,
|
||||
aggregationField,
|
||||
limit,
|
||||
}: FetchFunctionsParams) => {
|
||||
const [
|
||||
co2PerKWH,
|
||||
datacenterPUE,
|
||||
pervCPUWattX86,
|
||||
pervCPUWattArm64,
|
||||
awsCostDiscountRate,
|
||||
costPervCPUPerHour,
|
||||
azureCostDiscountRate,
|
||||
] = await Promise.all([
|
||||
core.uiSettings.client.get<number>(profilingCo2PerKWH),
|
||||
core.uiSettings.client.get<number>(profilingDatacenterPUE),
|
||||
core.uiSettings.client.get<number>(profilingPervCPUWattX86),
|
||||
core.uiSettings.client.get<number>(profilingPervCPUWattArm64),
|
||||
core.uiSettings.client.get<number>(profilingAWSCostDiscountRate),
|
||||
core.uiSettings.client.get<number>(profilingCostPervCPUPerHour),
|
||||
core.uiSettings.client.get<number>(profilingAzureCostDiscountRate),
|
||||
core.uiSettings.client.get<boolean>(profilingShowErrorFrames),
|
||||
]);
|
||||
|
||||
const profilingEsClient = createProfilingEsClient({ esClient });
|
||||
|
||||
const esTopNFunctions = await profilingEsClient.topNFunctions({
|
||||
sampleSize: targetSampleSize,
|
||||
limit,
|
||||
query,
|
||||
indices,
|
||||
stacktraceIdsField,
|
||||
aggregationField,
|
||||
co2PerKWH,
|
||||
datacenterPUE,
|
||||
pervCPUWattX86,
|
||||
pervCPUWattArm64,
|
||||
awsCostDiscountRate: percentToFactor(awsCostDiscountRate),
|
||||
costPervCPUPerHour,
|
||||
azureCostDiscountRate: percentToFactor(azureCostDiscountRate),
|
||||
});
|
||||
|
||||
return transformToKibanaTopNFunction(esTopNFunctions);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms object returned by ES because we share a lot of components in the UI with the current data model
|
||||
* We must first align the ES api response type then remove this
|
||||
*/
|
||||
function transformToKibanaTopNFunction(esTopNFunctions: ESTopNFunctions): TopNFunctions {
|
||||
return {
|
||||
TotalCount: esTopNFunctions.total_count,
|
||||
totalCPU: esTopNFunctions.total_count,
|
||||
selfCPU: esTopNFunctions.self_count,
|
||||
totalAnnualCO2Kgs: convertTonsToKgs(esTopNFunctions.self_annual_co2_tons),
|
||||
totalAnnualCostUSD: esTopNFunctions.self_annual_cost_usd,
|
||||
SamplingRate: 1,
|
||||
TopN: esTopNFunctions.topn.map((item) => {
|
||||
return {
|
||||
Id: item.id,
|
||||
Rank: item.rank,
|
||||
CountExclusive: item.self_count,
|
||||
CountInclusive: item.total_count,
|
||||
selfAnnualCO2kgs: convertTonsToKgs(item.self_annual_co2_tons),
|
||||
selfAnnualCostUSD: item.self_annual_costs_usd,
|
||||
totalAnnualCO2kgs: convertTonsToKgs(item.total_annual_co2_tons),
|
||||
totalAnnualCostUSD: item.total_annual_costs_usd,
|
||||
subGroups: item.sub_groups,
|
||||
Frame: {
|
||||
AddressOrLine: item.frame.address_or_line,
|
||||
ExeFileName: item.frame.executable_file_name,
|
||||
FrameType: item.frame.frame_type,
|
||||
FunctionName: item.frame.function_name,
|
||||
Inline: item.frame.inline,
|
||||
SourceFilename: item.frame.file_name,
|
||||
SourceLine: item.frame.line_number,
|
||||
FileID: '',
|
||||
FrameID: '',
|
||||
FunctionOffset: 0,
|
||||
},
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
|
@ -13,6 +13,7 @@ import { createGetStatusService } from './status';
|
|||
import { ProfilingESClient } from '../../common/profiling_es_client';
|
||||
import { createFetchFunctions } from './functions';
|
||||
import { createSetupState } from './setup_state';
|
||||
import { createFetchESFunctions } from './functions/es_functions';
|
||||
|
||||
export interface RegisterServicesParams {
|
||||
createProfilingEsClient: (params: {
|
||||
|
@ -31,6 +32,8 @@ export function registerServices(params: RegisterServicesParams) {
|
|||
fetchFlamechartData: createFetchFlamechart(params),
|
||||
getStatus: createGetStatusService(params),
|
||||
getSetupState: createSetupState(params),
|
||||
// Legacy fetch functions api based on stacktraces
|
||||
fetchFunctions: createFetchFunctions(params),
|
||||
fetchESFunctions: createFetchESFunctions(params),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import { ElasticsearchClient } from '@kbn/core/server';
|
|||
import type { ESSearchRequest, InferSearchResponseOf } from '@kbn/es-types';
|
||||
import type {
|
||||
BaseFlameGraph,
|
||||
ESTopNFunctions,
|
||||
ProfilingStatusResponse,
|
||||
StackTraceResponse,
|
||||
} from '@kbn/profiling-utils';
|
||||
|
@ -150,5 +151,51 @@ export function createProfilingEsClient({
|
|||
});
|
||||
return unwrapEsResponse(promise) as Promise<BaseFlameGraph>;
|
||||
},
|
||||
topNFunctions({
|
||||
query,
|
||||
aggregationField,
|
||||
indices,
|
||||
stacktraceIdsField,
|
||||
co2PerKWH,
|
||||
datacenterPUE,
|
||||
awsCostDiscountRate,
|
||||
costPervCPUPerHour,
|
||||
pervCPUWattArm64,
|
||||
pervCPUWattX86,
|
||||
azureCostDiscountRate,
|
||||
sampleSize,
|
||||
limit,
|
||||
}) {
|
||||
const controller = new AbortController();
|
||||
|
||||
const promise = withProfilingSpan('_profiling/topn/functions', () => {
|
||||
return esClient.transport.request(
|
||||
{
|
||||
method: 'POST',
|
||||
path: encodeURI('/_profiling/topn/functions'),
|
||||
body: {
|
||||
query,
|
||||
sample_size: sampleSize,
|
||||
limit,
|
||||
indices,
|
||||
stacktrace_ids_field: stacktraceIdsField,
|
||||
aggregation_field: aggregationField,
|
||||
co2_per_kwh: co2PerKWH,
|
||||
per_core_watt_x86: pervCPUWattX86,
|
||||
per_core_watt_arm64: pervCPUWattArm64,
|
||||
datacenter_pue: datacenterPUE,
|
||||
aws_cost_factor: awsCostDiscountRate,
|
||||
cost_per_core_hour: costPervCPUPerHour,
|
||||
azure_cost_factor: azureCostDiscountRate,
|
||||
},
|
||||
},
|
||||
{
|
||||
signal: controller.signal,
|
||||
meta: true,
|
||||
}
|
||||
);
|
||||
});
|
||||
return unwrapEsResponse(promise) as Promise<ESTopNFunctions>;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue