From d77c2e9c3b3c9eae764ef89cff020d67a80dc1e7 Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Mon, 15 Feb 2021 11:40:29 +0100 Subject: [PATCH 01/10] Profiling API call --- .../elasticsearch_fieldnames.test.ts.snap | 42 ++++ .../apm/common/elasticsearch_fieldnames.ts | 8 + x-pack/plugins/apm/common/processor_event.ts | 1 + x-pack/plugins/apm/common/ui_settings_keys.ts | 1 + .../app/Main/route_config/index.tsx | 14 ++ .../service_details/service_detail_tabs.tsx | 21 +- .../app/service_profiling/index.tsx | 86 +++++++ .../Links/apm/service_profiling_link.tsx | 39 ++++ .../create_apm_event_client/index.ts | 2 + .../services/profiling/get_service_profile.ts | 221 ++++++++++++++++++ .../apm/server/routes/create_apm_api.ts | 2 + x-pack/plugins/apm/server/routes/services.ts | 33 +++ x-pack/plugins/apm/server/ui_settings.ts | 15 ++ .../apm/typings/es_schemas/ui/profile.ts | 41 ++++ x-pack/typings/elasticsearch/index.d.ts | 2 +- 15 files changed, 526 insertions(+), 2 deletions(-) create mode 100644 x-pack/plugins/apm/public/components/app/service_profiling/index.tsx create mode 100644 x-pack/plugins/apm/public/components/shared/Links/apm/service_profiling_link.tsx create mode 100644 x-pack/plugins/apm/server/lib/services/profiling/get_service_profile.ts create mode 100644 x-pack/plugins/apm/typings/es_schemas/ui/profile.ts diff --git a/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap b/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap index acbd0398b59ba..96291c43c562a 100644 --- a/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap +++ b/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap @@ -121,6 +121,20 @@ exports[`Error POD_NAME 1`] = `undefined`; exports[`Error PROCESSOR_EVENT 1`] = `"error"`; +exports[`Error PROFILE_CPU_NS 1`] = `undefined`; + +exports[`Error PROFILE_DURATION 1`] = `undefined`; + +exports[`Error PROFILE_ID 1`] = `undefined`; + +exports[`Error PROFILE_SAMPLES_COUNT 1`] = `undefined`; + +exports[`Error PROFILE_STACK 1`] = `undefined`; + +exports[`Error PROFILE_TOP_ID 1`] = `undefined`; + +exports[`Error PROFILE_WALL_US 1`] = `undefined`; + exports[`Error SERVICE 1`] = ` Object { "language": Object { @@ -330,6 +344,20 @@ exports[`Span POD_NAME 1`] = `undefined`; exports[`Span PROCESSOR_EVENT 1`] = `"span"`; +exports[`Span PROFILE_CPU_NS 1`] = `undefined`; + +exports[`Span PROFILE_DURATION 1`] = `undefined`; + +exports[`Span PROFILE_ID 1`] = `undefined`; + +exports[`Span PROFILE_SAMPLES_COUNT 1`] = `undefined`; + +exports[`Span PROFILE_STACK 1`] = `undefined`; + +exports[`Span PROFILE_TOP_ID 1`] = `undefined`; + +exports[`Span PROFILE_WALL_US 1`] = `undefined`; + exports[`Span SERVICE 1`] = ` Object { "name": "service name", @@ -545,6 +573,20 @@ exports[`Transaction POD_NAME 1`] = `undefined`; exports[`Transaction PROCESSOR_EVENT 1`] = `"transaction"`; +exports[`Transaction PROFILE_CPU_NS 1`] = `undefined`; + +exports[`Transaction PROFILE_DURATION 1`] = `undefined`; + +exports[`Transaction PROFILE_ID 1`] = `undefined`; + +exports[`Transaction PROFILE_SAMPLES_COUNT 1`] = `undefined`; + +exports[`Transaction PROFILE_STACK 1`] = `undefined`; + +exports[`Transaction PROFILE_TOP_ID 1`] = `undefined`; + +exports[`Transaction PROFILE_WALL_US 1`] = `undefined`; + exports[`Transaction SERVICE 1`] = ` Object { "language": Object { diff --git a/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts b/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts index 6ecb1b0a7b097..6840c18f6b55a 100644 --- a/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts +++ b/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts @@ -132,3 +132,11 @@ export const LCP_FIELD = 'transaction.marks.agent.largestContentfulPaint'; export const TBT_FIELD = 'transaction.experience.tbt'; export const FID_FIELD = 'transaction.experience.fid'; export const CLS_FIELD = 'transaction.experience.cls'; + +export const PROFILE_ID = 'profile.id'; +export const PROFILE_DURATION = 'profile.duration'; +export const PROFILE_TOP_ID = 'profile.top.id'; +export const PROFILE_CPU_NS = 'profile.cpu.ns'; +export const PROFILE_WALL_US = 'profile.wall.us'; +export const PROFILE_SAMPLES_COUNT = 'profile.samples.count'; +export const PROFILE_STACK = 'profile.stack'; diff --git a/x-pack/plugins/apm/common/processor_event.ts b/x-pack/plugins/apm/common/processor_event.ts index 9eb9ee60c1998..57705e7ed4ce0 100644 --- a/x-pack/plugins/apm/common/processor_event.ts +++ b/x-pack/plugins/apm/common/processor_event.ts @@ -10,6 +10,7 @@ export enum ProcessorEvent { error = 'error', metric = 'metric', span = 'span', + profile = 'profile', } /** * Processor events that are searchable in the UI via the query bar. diff --git a/x-pack/plugins/apm/common/ui_settings_keys.ts b/x-pack/plugins/apm/common/ui_settings_keys.ts index 83d358068905e..cb185b835a280 100644 --- a/x-pack/plugins/apm/common/ui_settings_keys.ts +++ b/x-pack/plugins/apm/common/ui_settings_keys.ts @@ -7,3 +7,4 @@ export const enableCorrelations = 'apm:enableCorrelations'; export const enableServiceOverview = 'apm:enableServiceOverview'; +export const enableProfiling = 'apm:enableProfiling'; diff --git a/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx b/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx index 08d95aca24714..a7cbd7a79b4a7 100644 --- a/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx @@ -114,6 +114,12 @@ function ServiceDetailsTransactions( return ; } +function ServiceDetailsProfiling( + props: RouteComponentProps<{ serviceName: string }> +) { + return ; +} + function SettingsAgentConfiguration(props: RouteComponentProps<{}>) { return ( @@ -307,6 +313,14 @@ export const routes: APMRouteDefinition[] = [ return query.transactionName as string; }, }, + { + exact: true, + path: '/services/:serviceName/profiling', + component: withApmServiceContext(ServiceDetailsProfiling), + breadcrumb: i18n.translate('xpack.apm.breadcrumb.serviceProfilingTitle', { + defaultMessage: 'Profiling', + }), + }, { exact: true, path: '/services/:serviceName/service-map', diff --git a/x-pack/plugins/apm/public/components/app/service_details/service_detail_tabs.tsx b/x-pack/plugins/apm/public/components/app/service_details/service_detail_tabs.tsx index 23f699b63d207..81d2fec34a466 100644 --- a/x-pack/plugins/apm/public/components/app/service_details/service_detail_tabs.tsx +++ b/x-pack/plugins/apm/public/components/app/service_details/service_detail_tabs.tsx @@ -9,7 +9,10 @@ import { EuiTab } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { ReactNode } from 'react'; import { isJavaAgentName, isRumAgentName } from '../../../../common/agent_name'; -import { enableServiceOverview } from '../../../../common/ui_settings_keys'; +import { + enableServiceOverview, + enableProfiling, +} from '../../../../common/ui_settings_keys'; import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; import { useUrlParams } from '../../../context/url_params_context/use_url_params'; @@ -19,6 +22,7 @@ import { useServiceMapHref } from '../../shared/Links/apm/ServiceMapLink'; import { useServiceNodeOverviewHref } from '../../shared/Links/apm/ServiceNodeOverviewLink'; import { useServiceOverviewHref } from '../../shared/Links/apm/service_overview_link'; import { useTransactionsOverviewHref } from '../../shared/Links/apm/transaction_overview_link'; +import { useServiceProfilingHref } from '../../shared/Links/apm/service_profiling_link'; import { MainTabs } from '../../shared/main_tabs'; import { ErrorGroupOverview } from '../error_group_overview'; import { ServiceMap } from '../ServiceMap'; @@ -26,6 +30,7 @@ import { ServiceNodeOverview } from '../service_node_overview'; import { ServiceMetrics } from '../service_metrics'; import { ServiceOverview } from '../service_overview'; import { TransactionOverview } from '../transaction_overview'; +import { ServiceProfiling } from '../service_profiling'; interface Tab { key: string; @@ -42,6 +47,7 @@ interface Props { | 'nodes' | 'overview' | 'service-map' + | 'profiling' | 'transactions'; } @@ -113,6 +119,15 @@ export function ServiceDetailTabs({ serviceName, tab }: Props) { ) : null, }; + const profilingTab = { + key: 'profiling', + href: useServiceProfilingHref({ serviceName }), + text: i18n.translate('xpack.apm.serviceDetails.profilingTabLabel', { + defaultMessage: 'Profiling', + }), + render: () => , + }; + const tabs: Tab[] = [transactionsTab, errorsTab]; if (uiSettings.get(enableServiceOverview)) { @@ -127,6 +142,10 @@ export function ServiceDetailTabs({ serviceName, tab }: Props) { tabs.push(serviceMapTab); + if (uiSettings.get(enableProfiling)) { + tabs.push(profilingTab); + } + const selectedTab = tabs.find((serviceTab) => serviceTab.key === tab); return ( diff --git a/x-pack/plugins/apm/public/components/app/service_profiling/index.tsx b/x-pack/plugins/apm/public/components/app/service_profiling/index.tsx new file mode 100644 index 0000000000000..ee4b48a3572f0 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/service_profiling/index.tsx @@ -0,0 +1,86 @@ +/* + * 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 { Chart, Partition, PartitionLayout, Settings } from '@elastic/charts'; +import { EuiPanel } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiPage, EuiTitle } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { useUrlParams } from '../../../context/url_params_context/use_url_params'; +import { useFetcher } from '../../../hooks/use_fetcher'; +import { SearchBar } from '../../shared/search_bar'; + +interface ServiceProfilingProps { + serviceName: string; + environment?: string; +} + +export function ServiceProfiling({ + serviceName, + environment, +}: ServiceProfilingProps) { + const { + urlParams: { start, end }, + } = useUrlParams(); + + // @ts-expect-error + const { data } = useFetcher( + (callApmApi) => { + if (!start || !end) { + return undefined; + } + + return callApmApi({ + endpoint: 'GET /api/apm/services/{serviceName}/profiling', + params: { + path: { + serviceName, + }, + query: { + start, + end, + environment, + }, + }, + }); + }, + [start, end, environment, serviceName] + ); + + return ( + <> + + + + + +

+ {i18n.translate('xpack.apm.profilingOverviewTitle', { + defaultMessage: 'Profiling', + })} +

+
+
+ + + + + + + + +
+
+ + ); +} diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/service_profiling_link.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/service_profiling_link.tsx new file mode 100644 index 0000000000000..ab3b085e4e255 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/service_profiling_link.tsx @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiLink } from '@elastic/eui'; +import React from 'react'; +import { APMLinkExtendProps, useAPMHref } from './APMLink'; + +interface ServiceProfilingLinkProps extends APMLinkExtendProps { + serviceName: string; + environment?: string; +} + +export function useServiceProfilingHref({ + serviceName, + environment, +}: ServiceProfilingLinkProps) { + const query = environment + ? { + environment, + } + : {}; + return useAPMHref({ + path: `/services/${serviceName}/profiling`, + query, + }); +} + +export function ServiceProfilingLink({ + serviceName, + environment, + ...rest +}: ServiceProfilingLinkProps) { + const href = useServiceProfilingHref({ serviceName, environment }); + return ; +} diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts index c47d511ca565c..368c0eb305f21 100644 --- a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts @@ -6,6 +6,7 @@ */ import { ValuesType } from 'utility-types'; +import { Profile } from '../../../../../typings/es_schemas/ui/profile'; import { ElasticsearchClient, KibanaRequest, @@ -43,6 +44,7 @@ type TypeOfProcessorEvent = { transaction: Transaction; span: Span; metric: Metric; + profile: Profile; }[T]; type ESSearchRequestOf = Omit< diff --git a/x-pack/plugins/apm/server/lib/services/profiling/get_service_profile.ts b/x-pack/plugins/apm/server/lib/services/profiling/get_service_profile.ts new file mode 100644 index 0000000000000..c9ecd081d5f3f --- /dev/null +++ b/x-pack/plugins/apm/server/lib/services/profiling/get_service_profile.ts @@ -0,0 +1,221 @@ +/* + * 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 { ProcessorEvent } from '../../../../common/processor_event'; +import { ESFilter } from '../../../../../../typings/elasticsearch'; +import { + PROFILE_CPU_NS, + PROFILE_DURATION, + PROFILE_ID, + PROFILE_SAMPLES_COUNT, + PROFILE_STACK, + PROFILE_TOP_ID, + PROFILE_WALL_US, + SERVICE_NAME, +} from '../../../../common/elasticsearch_fieldnames'; +import { rangeFilter } from '../../../../common/utils/range_filter'; +import { getEnvironmentUiFilterES } from '../../helpers/convert_ui_filters/get_environment_ui_filter_es'; +import { APMEventClient } from '../../helpers/create_es_client/create_apm_event_client'; +import { Setup, SetupTimeRange } from '../../helpers/setup_request'; + +const MAX_STACK_IDS = 10000; +const MAX_PROFILE_IDS = 1000; + +export interface ProfileNode { + id: string; + stats: { + self: Record; + total: Record; + }; + parentId?: string; +} + +async function getProfileStats({ + apmEventClient, + filter, +}: { + apmEventClient: APMEventClient; + filter: ESFilter[]; +}) { + const response = await apmEventClient.search({ + apm: { + events: [ProcessorEvent.profile], + }, + body: { + size: 0, + query: { + bool: { + filter, + }, + }, + aggs: { + profiles: { + terms: { + field: PROFILE_ID, + size: MAX_PROFILE_IDS, + }, + aggs: { + latest: { + top_metrics: { + metrics: [ + { field: '@timestamp' }, + { field: PROFILE_DURATION }, + ] as const, + sort: { + '@timestamp': 'desc', + }, + }, + }, + }, + }, + stacks: { + terms: { + field: PROFILE_TOP_ID, + size: MAX_STACK_IDS, + }, + aggs: { + [PROFILE_CPU_NS]: { + sum: { + field: PROFILE_CPU_NS, + }, + }, + [PROFILE_WALL_US]: { + sum: { + field: PROFILE_WALL_US, + }, + }, + [PROFILE_SAMPLES_COUNT]: { + sum: { + field: PROFILE_SAMPLES_COUNT, + }, + }, + }, + }, + }, + }, + }); + + const profiles = + response.aggregations?.profiles.buckets.map((profile) => { + const latest = profile.latest.top[0].metrics ?? {}; + + return { + id: profile.key as string, + duration: latest[PROFILE_DURATION] as number, + timestamp: latest['@timestamp'] as number, + }; + }) ?? []; + + const stacks = + response.aggregations?.stacks.buckets.map((stack) => { + return { + id: stack.key as string, + [PROFILE_CPU_NS]: stack[PROFILE_CPU_NS].value, + [PROFILE_WALL_US]: stack[PROFILE_WALL_US].value, + [PROFILE_SAMPLES_COUNT]: stack[PROFILE_SAMPLES_COUNT].value, + }; + }) ?? []; + + return { + profiles, + stacks, + }; +} + +async function getStacks({ + apmEventClient, + filter, +}: { + apmEventClient: APMEventClient; + filter: ESFilter[]; +}) { + const response = await apmEventClient.search({ + apm: { + events: [ProcessorEvent.profile], + }, + body: { + size: MAX_STACK_IDS, + query: { + bool: { + filter, + }, + }, + collapse: { + field: PROFILE_TOP_ID, + }, + _source: [PROFILE_TOP_ID, PROFILE_STACK], + }, + }); + + return response.hits.hits.map((hit) => hit._source); +} + +export async function getServiceProfile({ + serviceName, + setup, + environment, +}: { + serviceName: string; + setup: Setup & SetupTimeRange; + environment?: string; +}) { + const { apmEventClient, start, end } = setup; + + const filter: ESFilter[] = [ + { range: rangeFilter(start, end) }, + { term: { [SERVICE_NAME]: serviceName } }, + ...getEnvironmentUiFilterES(environment), + ]; + + const [profileStats, stacks] = await Promise.all([ + getProfileStats({ apmEventClient, filter }), + getStacks({ apmEventClient, filter }), + ]); + + const nodes: Record = {}; + + stacks.forEach((stack) => { + const reversedStack = stack.profile.stack.concat().reverse(); + + reversedStack.forEach((frame, index) => { + let node = nodes[frame.id]; + if (!node) { + node = nodes[frame.id] = { + id: frame.id, + stats: { self: {}, total: {} }, + }; + } + + if (index > 0) { + node.parentId = nodes[reversedStack[index - 1].id].id; + } + }); + }); + + profileStats.stacks.forEach((stackStats) => { + const { id, ...stats } = stackStats; + const node = nodes[id]; + if (nodes[id]) { + Object.assign(nodes[id].stats.self, stats); + let current: ProfileNode | undefined = node; + while (current) { + Object.keys(stats).forEach((statName) => { + const totalVal = current!.stats.total[statName] || 0; + current!.stats.total[statName] = + totalVal + (stats[statName as keyof typeof stats] ?? 0); + }); + current = current.parentId ? nodes[current.parentId] : undefined; + } + } else { + // console.log('Could not find frame for', id); + } + }); + + return { + profiles: profileStats.profiles, + nodes, + }; +} diff --git a/x-pack/plugins/apm/server/routes/create_apm_api.ts b/x-pack/plugins/apm/server/routes/create_apm_api.ts index d22bcb1c501e0..6dd101712cb9f 100644 --- a/x-pack/plugins/apm/server/routes/create_apm_api.ts +++ b/x-pack/plugins/apm/server/routes/create_apm_api.ts @@ -29,6 +29,7 @@ import { serviceMetadataDetailsRoute, serviceMetadataIconsRoute, serviceInstancesRoute, + serviceProfilingRoute, } from './services'; import { agentConfigurationRoute, @@ -131,6 +132,7 @@ const createApmApi = () => { .add(serviceMetadataDetailsRoute) .add(serviceMetadataIconsRoute) .add(serviceInstancesRoute) + .add(serviceProfilingRoute) // Agent configuration .add(getSingleAgentConfigurationRoute) diff --git a/x-pack/plugins/apm/server/routes/services.ts b/x-pack/plugins/apm/server/routes/services.ts index ff064e0571d13..b5cd681a83713 100644 --- a/x-pack/plugins/apm/server/routes/services.ts +++ b/x-pack/plugins/apm/server/routes/services.ts @@ -27,6 +27,7 @@ import { offsetPreviousPeriodCoordinates } from '../utils/offset_previous_period import { createRoute } from './create_route'; import { comparisonRangeRt, rangeRt, uiFiltersRt } from './default_api_types'; import { withApmSpan } from '../utils/with_apm_span'; +import { getServiceProfile } from '../lib/services/profiling/get_service_profile'; export const servicesRoute = createRoute({ endpoint: 'GET /api/apm/services', @@ -443,3 +444,35 @@ export const serviceDependenciesRoute = createRoute({ }); }, }); + +export const serviceProfilingRoute = createRoute({ + endpoint: 'GET /api/apm/services/{serviceName}/profiling', + params: t.type({ + path: t.type({ + serviceName: t.string, + }), + query: t.intersection([ + rangeRt, + t.partial({ + environment: t.string, + }), + ]), + }), + options: { + tags: ['access:apm'], + }, + handler: async ({ context, request }) => { + const setup = await setupRequest(context, request); + + const { + path: { serviceName }, + query: { environment }, + } = context.params; + + return getServiceProfile({ + serviceName, + environment, + setup, + }); + }, +}); diff --git a/x-pack/plugins/apm/server/ui_settings.ts b/x-pack/plugins/apm/server/ui_settings.ts index a52cdbcc4f079..ada355ee89ac3 100644 --- a/x-pack/plugins/apm/server/ui_settings.ts +++ b/x-pack/plugins/apm/server/ui_settings.ts @@ -11,6 +11,7 @@ import { UiSettingsParams } from '../../../../src/core/types'; import { enableCorrelations, enableServiceOverview, + enableProfiling, } from '../common/ui_settings_keys'; /** @@ -45,4 +46,18 @@ export const uiSettings: Record> = { ), schema: schema.boolean(), }, + [enableProfiling]: { + category: ['observability'], + name: i18n.translate('xpack.apm.enableProfilingExperimentName', { + defaultMessage: 'APM Profiling', + }), + value: false, + description: i18n.translate( + 'xpack.apm.enableProfilingExperimentDescription', + { + defaultMessage: 'Enable the Profiling tab for services in APM.', + } + ), + schema: schema.boolean(), + }, }; diff --git a/x-pack/plugins/apm/typings/es_schemas/ui/profile.ts b/x-pack/plugins/apm/typings/es_schemas/ui/profile.ts new file mode 100644 index 0000000000000..8d61a4b78c6f0 --- /dev/null +++ b/x-pack/plugins/apm/typings/es_schemas/ui/profile.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Observer } from '@elastic/eui/src/components/observer/observer'; +import { APMBaseDoc } from '../raw/apm_base_doc'; +import { Agent } from './fields/agent'; + +interface ProfileStackFrame { + filename?: string; + line?: string; + function: string; + id: string; +} + +export interface Profile { + agent: Agent; + '@timestamp': string; + labels?: { + [key: string]: string | number | boolean; + }; + observer?: Observer; + profile: { + top: ProfileStackFrame; + duration: number; + stack: ProfileStackFrame[]; + id: string; + wall?: { + us: number; + }; + cpu?: { + ns: number; + }; + samples: { + count: number; + }; + }; +} diff --git a/x-pack/typings/elasticsearch/index.d.ts b/x-pack/typings/elasticsearch/index.d.ts index b174db739b030..41630e81f13e4 100644 --- a/x-pack/typings/elasticsearch/index.d.ts +++ b/x-pack/typings/elasticsearch/index.d.ts @@ -25,7 +25,7 @@ export type MaybeReadonlyArray = T[] | readonly T[]; interface CollapseQuery { field: string; - inner_hits: { + inner_hits?: { name: string; size?: number; sort?: SortOptions; From faaa6834201d7a08a572d5f3aebfaec6d0c69817 Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Tue, 16 Feb 2021 10:56:28 +0100 Subject: [PATCH 02/10] Flamegraph --- x-pack/plugins/apm/common/profiling.ts | 19 ++ .../app/service_profiling/index.tsx | 46 +---- .../service_profiling_flamegraph.tsx | 195 ++++++++++++++++++ .../service_profiling_timeline.tsx | 6 + .../unpack_processor_events.ts | 2 + ...ts => get_service_profiling_statistics.ts} | 111 +++++----- .../get_service_profiling_timeline.ts | 95 +++++++++ .../apm/server/routes/create_apm_api.ts | 6 +- x-pack/plugins/apm/server/routes/services.ts | 50 ++++- .../apm/typings/es_schemas/ui/profile.ts | 3 +- 10 files changed, 435 insertions(+), 98 deletions(-) create mode 100644 x-pack/plugins/apm/common/profiling.ts create mode 100644 x-pack/plugins/apm/public/components/app/service_profiling/service_profiling_flamegraph.tsx create mode 100644 x-pack/plugins/apm/public/components/app/service_profiling/service_profiling_timeline.tsx rename x-pack/plugins/apm/server/lib/services/profiling/{get_service_profile.ts => get_service_profiling_statistics.ts} (66%) create mode 100644 x-pack/plugins/apm/server/lib/services/profiling/get_service_profiling_timeline.ts diff --git a/x-pack/plugins/apm/common/profiling.ts b/x-pack/plugins/apm/common/profiling.ts new file mode 100644 index 0000000000000..6b8468274d155 --- /dev/null +++ b/x-pack/plugins/apm/common/profiling.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export enum ProfilingValueType { + wallTime = 'wall_time', + cpuTime = 'cpu_time', +} + +export interface ProfileNode { + id: string; + name: string; + value: number; + count: number; + children: string[]; +} diff --git a/x-pack/plugins/apm/public/components/app/service_profiling/index.tsx b/x-pack/plugins/apm/public/components/app/service_profiling/index.tsx index ee4b48a3572f0..6021cafb5402a 100644 --- a/x-pack/plugins/apm/public/components/app/service_profiling/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_profiling/index.tsx @@ -4,14 +4,14 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { Chart, Partition, PartitionLayout, Settings } from '@elastic/charts'; import { EuiPanel } from '@elastic/eui'; import { EuiFlexGroup, EuiFlexItem, EuiPage, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; +import { ProfilingValueType } from '../../../../common/profiling'; import { useUrlParams } from '../../../context/url_params_context/use_url_params'; -import { useFetcher } from '../../../hooks/use_fetcher'; import { SearchBar } from '../../shared/search_bar'; +import { ServiceProfilingFlamegraph } from './service_profiling_flamegraph'; interface ServiceProfilingProps { serviceName: string; @@ -26,29 +26,7 @@ export function ServiceProfiling({ urlParams: { start, end }, } = useUrlParams(); - // @ts-expect-error - const { data } = useFetcher( - (callApmApi) => { - if (!start || !end) { - return undefined; - } - - return callApmApi({ - endpoint: 'GET /api/apm/services/{serviceName}/profiling', - params: { - path: { - serviceName, - }, - query: { - start, - end, - environment, - }, - }, - }); - }, - [start, end, environment, serviceName] - ); + const valueType = ProfilingValueType.cpuTime; return ( <> @@ -66,17 +44,13 @@ export function ServiceProfiling({ - - - - + diff --git a/x-pack/plugins/apm/public/components/app/service_profiling/service_profiling_flamegraph.tsx b/x-pack/plugins/apm/public/components/app/service_profiling/service_profiling_flamegraph.tsx new file mode 100644 index 0000000000000..4121fe209d5ba --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/service_profiling/service_profiling_flamegraph.tsx @@ -0,0 +1,195 @@ +/* + * 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 React from 'react'; +import { sumBy } from 'lodash'; +import { + Chart, + Datum, + Partition, + PartitionLayout, + PrimitiveValue, + Settings, +} from '@elastic/charts'; +import { euiPaletteForTemperature } from '@elastic/eui'; +import { useMemo } from 'react'; +import { useChartTheme } from '../../../../../observability/public'; +import { ProfileNode, ProfilingValueType } from '../../../../common/profiling'; +import { useFetcher } from '../../../hooks/use_fetcher'; +import { useTheme } from '../../../hooks/use_theme'; + +interface ProfileDataPoint { + value: number; + name: string | undefined; + depth: number; + layers: Record; +} + +export function ServiceProfilingFlamegraph({ + serviceName, + environment, + valueType, + start, + end, +}: { + serviceName: string; + environment?: string; + valueType?: ProfilingValueType; + start?: string; + end?: string; +}) { + const theme = useTheme(); + + const { data } = useFetcher( + (callApmApi) => { + if (!start || !end || !valueType) { + return undefined; + } + + return callApmApi({ + endpoint: 'GET /api/apm/services/{serviceName}/profiling/statistics', + params: { + path: { + serviceName, + }, + query: { + start, + end, + environment, + valueType, + }, + }, + }); + }, + [start, end, environment, serviceName, valueType] + ); + + const spec = useMemo(() => { + if (!data) { + return { + layers: [], + points: [], + }; + } + + const { rootNodes, nodes } = data; + + const getDataPoints = ( + node: ProfileNode, + depth: number + ): ProfileDataPoint[] => { + const { children } = node; + + if (!children.length) { + return [ + { + name: node.name, + value: node.value, + depth, + layers: { + [depth]: node.name, + }, + }, + ]; + } + + const childDataPoints = children + .flatMap((childId) => getDataPoints(nodes[childId], depth + 1)) + .map((point) => ({ + ...point, + layers: { + ...point.layers, + [depth]: node.name, + }, + })); + + const totalTime = sumBy(childDataPoints, 'value'); + const selfTime = node.value - totalTime; + + if (selfTime === 0) { + return childDataPoints; + } + + return [ + ...childDataPoints, + { + name: undefined, + value: selfTime, + layers: { [depth + 1]: undefined }, + depth, + }, + ]; + }; + + const root = { + name: 'root', + id: 'root', + children: rootNodes, + ...rootNodes.reduce( + (prev, id) => { + const node = nodes[id]; + + return { + value: prev.value + node.value, + count: prev.count + node.count, + }; + }, + { value: 0, count: 0 } + ), + }; + + const points = getDataPoints(root, 0); + + const maxDepth = Math.max(...points.map((point) => point.depth)); + + const colors = euiPaletteForTemperature(80).slice(50); + + const layers = [...new Array(maxDepth)].map((_, depth) => { + return { + groupByRollup: (d: Datum) => d.layers[depth], + nodeLabel: (d: PrimitiveValue) => String(d), + showAccessor: (d: PrimitiveValue) => !!d, + shape: { + fillColor: () => colors[Math.floor(Math.random() * colors.length)], + }, + }; + }); + + return { + points, + layers, + }; + }, [data]); + + const chartTheme = useChartTheme(); + + const chartSize = { + height: 600, + width: '100%', + }; + + return ( + + + d.value as number} + valueFormatter={() => ''} + config={{ + fillLabel: { + fontFamily: theme.eui.euiCodeFontFamily, + }, + fontFamily: theme.eui.euiCodeFontFamily, + minFontSize: 9, + maxFontSize: 9, + partitionLayout: PartitionLayout.icicle, + }} + /> + + ); +} diff --git a/x-pack/plugins/apm/public/components/app/service_profiling/service_profiling_timeline.tsx b/x-pack/plugins/apm/public/components/app/service_profiling/service_profiling_timeline.tsx new file mode 100644 index 0000000000000..1fec1c76430eb --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/service_profiling/service_profiling_timeline.tsx @@ -0,0 +1,6 @@ +/* + * 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. + */ diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/unpack_processor_events.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/unpack_processor_events.ts index eef9aff946ea7..38989d172a73f 100644 --- a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/unpack_processor_events.ts +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/unpack_processor_events.ts @@ -23,6 +23,8 @@ const processorEventIndexMap: Record = { [ProcessorEvent.span]: 'apm_oss.spanIndices', [ProcessorEvent.metric]: 'apm_oss.metricsIndices', [ProcessorEvent.error]: 'apm_oss.errorIndices', + // TODO: should have its own config setting + [ProcessorEvent.profile]: 'apm_oss.transactionIndices', }; export function unpackProcessorEvents( diff --git a/x-pack/plugins/apm/server/lib/services/profiling/get_service_profile.ts b/x-pack/plugins/apm/server/lib/services/profiling/get_service_profiling_statistics.ts similarity index 66% rename from x-pack/plugins/apm/server/lib/services/profiling/get_service_profile.ts rename to x-pack/plugins/apm/server/lib/services/profiling/get_service_profiling_statistics.ts index c9ecd081d5f3f..99803f5c38716 100644 --- a/x-pack/plugins/apm/server/lib/services/profiling/get_service_profile.ts +++ b/x-pack/plugins/apm/server/lib/services/profiling/get_service_profiling_statistics.ts @@ -4,6 +4,8 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { keyBy } from 'lodash'; +import { ProfilingValueType, ProfileNode } from '../../../../common/profiling'; import { ProcessorEvent } from '../../../../common/processor_event'; import { ESFilter } from '../../../../../../typings/elasticsearch'; import { @@ -24,21 +26,22 @@ import { Setup, SetupTimeRange } from '../../helpers/setup_request'; const MAX_STACK_IDS = 10000; const MAX_PROFILE_IDS = 1000; -export interface ProfileNode { - id: string; - stats: { - self: Record; - total: Record; - }; - parentId?: string; -} +const maybeAdd = (to: any[], value: any) => { + if (to.includes(value)) { + return; + } -async function getProfileStats({ + to.push(value); +}; + +async function getProfilingStats({ apmEventClient, filter, + valueTypeField, }: { apmEventClient: APMEventClient; filter: ESFilter[]; + valueTypeField: string; }) { const response = await apmEventClient.search({ apm: { @@ -77,14 +80,9 @@ async function getProfileStats({ size: MAX_STACK_IDS, }, aggs: { - [PROFILE_CPU_NS]: { - sum: { - field: PROFILE_CPU_NS, - }, - }, - [PROFILE_WALL_US]: { + value: { sum: { - field: PROFILE_WALL_US, + field: valueTypeField, }, }, [PROFILE_SAMPLES_COUNT]: { @@ -113,9 +111,8 @@ async function getProfileStats({ response.aggregations?.stacks.buckets.map((stack) => { return { id: stack.key as string, - [PROFILE_CPU_NS]: stack[PROFILE_CPU_NS].value, - [PROFILE_WALL_US]: stack[PROFILE_WALL_US].value, - [PROFILE_SAMPLES_COUNT]: stack[PROFILE_SAMPLES_COUNT].value, + value: stack.value.value!, + count: stack[PROFILE_SAMPLES_COUNT].value!, }; }) ?? []; @@ -125,7 +122,7 @@ async function getProfileStats({ }; } -async function getStacks({ +async function getProfilesWithStacks({ apmEventClient, filter, }: { @@ -153,69 +150,75 @@ async function getStacks({ return response.hits.hits.map((hit) => hit._source); } -export async function getServiceProfile({ +export async function getServiceProfilingStatistics({ serviceName, setup, environment, + valueType, }: { serviceName: string; setup: Setup & SetupTimeRange; environment?: string; + valueType: ProfilingValueType; }) { const { apmEventClient, start, end } = setup; + const valueTypeField = { + [ProfilingValueType.wallTime]: PROFILE_WALL_US, + [ProfilingValueType.cpuTime]: PROFILE_CPU_NS, + }[valueType]; + const filter: ESFilter[] = [ { range: rangeFilter(start, end) }, { term: { [SERVICE_NAME]: serviceName } }, ...getEnvironmentUiFilterES(environment), + { exists: { field: valueTypeField } }, ]; - const [profileStats, stacks] = await Promise.all([ - getProfileStats({ apmEventClient, filter }), - getStacks({ apmEventClient, filter }), + const [profileStats, profileStacks] = await Promise.all([ + getProfilingStats({ apmEventClient, filter, valueTypeField }), + getProfilesWithStacks({ apmEventClient, filter }), ]); const nodes: Record = {}; + const rootNodes: string[] = []; - stacks.forEach((stack) => { - const reversedStack = stack.profile.stack.concat().reverse(); + function getNode({ id, name }: { id: string; name: string }) { + let node = nodes[id]; + if (!node) { + node = { id, name, value: 0, count: 0, children: [] }; + nodes[id] = node; + } + return node; + } - reversedStack.forEach((frame, index) => { - let node = nodes[frame.id]; - if (!node) { - node = nodes[frame.id] = { - id: frame.id, - stats: { self: {}, total: {} }, - }; - } + const stackStatsById = keyBy(profileStats.stacks, 'id'); + + profileStacks.forEach((profile) => { + const stats = stackStatsById[profile.profile.top.id]; + const frames = profile.profile.stack.concat().reverse(); - if (index > 0) { - node.parentId = nodes[reversedStack[index - 1].id].id; + frames.forEach((frame, index) => { + const node = getNode({ id: frame.id, name: frame.function }); + + if (index === frames.length - 1) { + node.value += stats.value; + node.count += stats.count; } - }); - }); - profileStats.stacks.forEach((stackStats) => { - const { id, ...stats } = stackStats; - const node = nodes[id]; - if (nodes[id]) { - Object.assign(nodes[id].stats.self, stats); - let current: ProfileNode | undefined = node; - while (current) { - Object.keys(stats).forEach((statName) => { - const totalVal = current!.stats.total[statName] || 0; - current!.stats.total[statName] = - totalVal + (stats[statName as keyof typeof stats] ?? 0); - }); - current = current.parentId ? nodes[current.parentId] : undefined; + if (index === 0) { + // root node + maybeAdd(rootNodes, node.id); + } else { + const parent = nodes[frames[index - 1].id]; + maybeAdd(parent.children, node.id); } - } else { - // console.log('Could not find frame for', id); - } + }); }); return { profiles: profileStats.profiles, nodes, + rootNodes, }; } diff --git a/x-pack/plugins/apm/server/lib/services/profiling/get_service_profiling_timeline.ts b/x-pack/plugins/apm/server/lib/services/profiling/get_service_profiling_timeline.ts new file mode 100644 index 0000000000000..b3296e204d3b5 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/services/profiling/get_service_profiling_timeline.ts @@ -0,0 +1,95 @@ +/* + * 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 { rangeFilter } from '../../../../common/utils/range_filter'; +import { ProcessorEvent } from '../../../../common/processor_event'; +import { + PROFILE_CPU_NS, + PROFILE_WALL_US, + SERVICE_NAME, +} from '../../../../common/elasticsearch_fieldnames'; +import { ProfilingValueType } from '../../../../common/profiling'; +import { Setup, SetupTimeRange } from '../../helpers/setup_request'; +import { getBucketSize } from '../../helpers/get_bucket_size'; +import { getEnvironmentUiFilterES } from '../../helpers/convert_ui_filters/get_environment_ui_filter_es'; + +export async function getServiceProfilingTimeline({ + serviceName, + environment, + setup, +}: { + serviceName: string; + setup: Setup & SetupTimeRange; + environment?: string; +}) { + const { apmEventClient, start, end, esFilter } = setup; + + const response = await apmEventClient.search({ + apm: { + events: [ProcessorEvent.profile], + }, + body: { + size: 0, + query: { + bool: { + filter: [ + { term: { [SERVICE_NAME]: serviceName } }, + { range: rangeFilter(start, end) }, + ...getEnvironmentUiFilterES(environment), + ...esFilter, + ], + }, + }, + aggs: { + timeseries: { + date_histogram: { + field: '@timestamp', + fixed_interval: getBucketSize({ start, end }).intervalString, + min_doc_count: 0, + extended_bounds: { + min: start, + max: end, + }, + }, + aggs: { + value_type: { + filters: { + filters: { + [ProfilingValueType.cpuTime]: { + exists: { field: PROFILE_CPU_NS }, + }, + [ProfilingValueType.wallTime]: { + exists: { field: PROFILE_WALL_US }, + }, + }, + }, + }, + }, + }, + }, + }, + }); + + const { aggregations } = response; + + if (!aggregations) { + return []; + } + + return aggregations.timeseries.buckets.map((bucket) => { + return { + x: bucket.key, + count: bucket.doc_count, + valueTypes: { + [ProfilingValueType.cpuTime]: + bucket.value_type.buckets.cpu_time.doc_count, + [ProfilingValueType.wallTime]: + bucket.value_type.buckets.wall_time.doc_count, + }, + }; + }); +} diff --git a/x-pack/plugins/apm/server/routes/create_apm_api.ts b/x-pack/plugins/apm/server/routes/create_apm_api.ts index 6dd101712cb9f..0378e561468ad 100644 --- a/x-pack/plugins/apm/server/routes/create_apm_api.ts +++ b/x-pack/plugins/apm/server/routes/create_apm_api.ts @@ -29,7 +29,8 @@ import { serviceMetadataDetailsRoute, serviceMetadataIconsRoute, serviceInstancesRoute, - serviceProfilingRoute, + serviceProfilingStatisticsRoute, + serviceProfilingTimelineRoute, } from './services'; import { agentConfigurationRoute, @@ -132,7 +133,8 @@ const createApmApi = () => { .add(serviceMetadataDetailsRoute) .add(serviceMetadataIconsRoute) .add(serviceInstancesRoute) - .add(serviceProfilingRoute) + .add(serviceProfilingTimelineRoute) + .add(serviceProfilingStatisticsRoute) // Agent configuration .add(getSingleAgentConfigurationRoute) diff --git a/x-pack/plugins/apm/server/routes/services.ts b/x-pack/plugins/apm/server/routes/services.ts index b5cd681a83713..9a4d10dc974cf 100644 --- a/x-pack/plugins/apm/server/routes/services.ts +++ b/x-pack/plugins/apm/server/routes/services.ts @@ -27,7 +27,9 @@ import { offsetPreviousPeriodCoordinates } from '../utils/offset_previous_period import { createRoute } from './create_route'; import { comparisonRangeRt, rangeRt, uiFiltersRt } from './default_api_types'; import { withApmSpan } from '../utils/with_apm_span'; -import { getServiceProfile } from '../lib/services/profiling/get_service_profile'; +import { getServiceProfilingStatistics } from '../lib/services/profiling/get_service_profiling_statistics'; +import { getServiceProfilingTimeline } from '../lib/services/profiling/get_service_profiling_timeline'; +import { ProfilingValueType } from '../../common/profiling'; export const servicesRoute = createRoute({ endpoint: 'GET /api/apm/services', @@ -445,14 +447,15 @@ export const serviceDependenciesRoute = createRoute({ }, }); -export const serviceProfilingRoute = createRoute({ - endpoint: 'GET /api/apm/services/{serviceName}/profiling', +export const serviceProfilingTimelineRoute = createRoute({ + endpoint: 'GET /api/apm/services/{serviceName}/profiling/timeline', params: t.type({ path: t.type({ serviceName: t.string, }), query: t.intersection([ rangeRt, + uiFiltersRt, t.partial({ environment: t.string, }), @@ -469,9 +472,48 @@ export const serviceProfilingRoute = createRoute({ query: { environment }, } = context.params; - return getServiceProfile({ + return getServiceProfilingTimeline({ + setup, + serviceName, + environment, + }); + }, +}); + +export const serviceProfilingStatisticsRoute = createRoute({ + endpoint: 'GET /api/apm/services/{serviceName}/profiling/statistics', + params: t.type({ + path: t.type({ + serviceName: t.string, + }), + query: t.intersection([ + rangeRt, + t.partial({ + environment: t.string, + }), + t.type({ + valueType: t.union([ + t.literal(ProfilingValueType.wallTime), + t.literal(ProfilingValueType.cpuTime), + ]), + }), + ]), + }), + options: { + tags: ['access:apm'], + }, + handler: async ({ context, request }) => { + const setup = await setupRequest(context, request); + + const { + path: { serviceName }, + query: { environment, valueType }, + } = context.params; + + return getServiceProfilingStatistics({ serviceName, environment, + valueType, setup, }); }, diff --git a/x-pack/plugins/apm/typings/es_schemas/ui/profile.ts b/x-pack/plugins/apm/typings/es_schemas/ui/profile.ts index 8d61a4b78c6f0..e8fbe8805fd0f 100644 --- a/x-pack/plugins/apm/typings/es_schemas/ui/profile.ts +++ b/x-pack/plugins/apm/typings/es_schemas/ui/profile.ts @@ -6,10 +6,9 @@ */ import { Observer } from '@elastic/eui/src/components/observer/observer'; -import { APMBaseDoc } from '../raw/apm_base_doc'; import { Agent } from './fields/agent'; -interface ProfileStackFrame { +export interface ProfileStackFrame { filename?: string; line?: string; function: string; From 5a1b62d81508085009cf98710f8f0d93eb5a3101 Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Wed, 17 Feb 2021 15:39:05 +0100 Subject: [PATCH 03/10] Timeline --- .../app/service_profiling/index.tsx | 44 ++++++++++++++- .../service_profiling_flamegraph.tsx | 15 +++-- .../service_profiling_timeline.tsx | 56 +++++++++++++++++++ .../get_service_profiling_timeline.ts | 23 ++++++-- 4 files changed, 127 insertions(+), 11 deletions(-) diff --git a/x-pack/plugins/apm/public/components/app/service_profiling/index.tsx b/x-pack/plugins/apm/public/components/app/service_profiling/index.tsx index 6021cafb5402a..551c9163b7629 100644 --- a/x-pack/plugins/apm/public/components/app/service_profiling/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_profiling/index.tsx @@ -7,11 +7,13 @@ import { EuiPanel } from '@elastic/eui'; import { EuiFlexGroup, EuiFlexItem, EuiPage, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { ProfilingValueType } from '../../../../common/profiling'; import { useUrlParams } from '../../../context/url_params_context/use_url_params'; +import { useFetcher } from '../../../hooks/use_fetcher'; import { SearchBar } from '../../shared/search_bar'; import { ServiceProfilingFlamegraph } from './service_profiling_flamegraph'; +import { ServiceProfilingTimeline } from './service_profiling_timeline'; interface ServiceProfilingProps { serviceName: string; @@ -26,7 +28,44 @@ export function ServiceProfiling({ urlParams: { start, end }, } = useUrlParams(); - const valueType = ProfilingValueType.cpuTime; + const { data = [] } = useFetcher( + (callApmApi) => { + if (!start || !end) { + return; + } + + return callApmApi({ + endpoint: 'GET /api/apm/services/{serviceName}/profiling/timeline', + params: { + path: { serviceName }, + query: { start, end, environment, uiFilters: JSON.stringify({}) }, + }, + }); + }, + [start, end, serviceName, environment] + ); + + const [valueType, setValueType] = useState(); + + useEffect(() => { + if (!data.length) { + return; + } + + const availableValueTypes = data.reduce((set, point) => { + (Object.keys(point.valueTypes) as ProfilingValueType[]) + .filter((type) => point.valueTypes[type] > 0) + .forEach((type) => { + set.add(type); + }); + + return set; + }, new Set()); + + if (!valueType || !availableValueTypes.has(valueType)) { + setValueType(Array.from(availableValueTypes)[0]); + } + }, [data, valueType]); return ( <> @@ -44,6 +83,7 @@ export function ServiceProfiling({ + point.depth)); - const colors = euiPaletteForTemperature(80).slice(50); - const layers = [...new Array(maxDepth)].map((_, depth) => { return { groupByRollup: (d: Datum) => d.layers[depth], nodeLabel: (d: PrimitiveValue) => String(d), showAccessor: (d: PrimitiveValue) => !!d, shape: { - fillColor: () => colors[Math.floor(Math.random() * colors.length)], + fillColor: (d: { dataName: string }) => { + const integer = + Math.abs(seedrandom(d.dataName).int32()) % colors.length; + return colors[integer]; + }, }, }; }); @@ -167,7 +172,7 @@ export function ServiceProfilingFlamegraph({ const chartTheme = useChartTheme(); const chartSize = { - height: 600, + height: 800, width: '100%', }; @@ -183,10 +188,12 @@ export function ServiceProfilingFlamegraph({ config={{ fillLabel: { fontFamily: theme.eui.euiCodeFontFamily, + clip: true, }, fontFamily: theme.eui.euiCodeFontFamily, minFontSize: 9, maxFontSize: 9, + maxRowCount: 1, partitionLayout: PartitionLayout.icicle, }} /> diff --git a/x-pack/plugins/apm/public/components/app/service_profiling/service_profiling_timeline.tsx b/x-pack/plugins/apm/public/components/app/service_profiling/service_profiling_timeline.tsx index 1fec1c76430eb..2da7efe2330f9 100644 --- a/x-pack/plugins/apm/public/components/app/service_profiling/service_profiling_timeline.tsx +++ b/x-pack/plugins/apm/public/components/app/service_profiling/service_profiling_timeline.tsx @@ -4,3 +4,59 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import React from 'react'; +import { + Axis, + BarSeries, + Chart, + niceTimeFormatter, + Position, + ScaleType, + Settings, +} from '@elastic/charts'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { useChartTheme } from '../../../../../observability/public'; +import { ProfilingValueType } from '../../../../common/profiling'; + +type ProfilingTimelineItem = { + x: number; + count: number; +} & { valueTypes: Record }; + +export function ServiceProfilingTimeline({ + start, + end, + profiles, +}: { + profiles: ProfilingTimelineItem[]; + start?: number; + end?: number; +}) { + const chartTheme = useChartTheme(); + + const xFormat = niceTimeFormatter([start!, end!]); + + return ( + + + + + + + d.valueTypes[ProfilingValueType.cpuTime], + (d) => d.valueTypes[ProfilingValueType.wallTime], + ]} + data={profiles} + /> + + + + ); +} diff --git a/x-pack/plugins/apm/server/lib/services/profiling/get_service_profiling_timeline.ts b/x-pack/plugins/apm/server/lib/services/profiling/get_service_profiling_timeline.ts index b3296e204d3b5..c05e4733462ff 100644 --- a/x-pack/plugins/apm/server/lib/services/profiling/get_service_profiling_timeline.ts +++ b/x-pack/plugins/apm/server/lib/services/profiling/get_service_profiling_timeline.ts @@ -9,6 +9,7 @@ import { rangeFilter } from '../../../../common/utils/range_filter'; import { ProcessorEvent } from '../../../../common/processor_event'; import { PROFILE_CPU_NS, + PROFILE_ID, PROFILE_WALL_US, SERVICE_NAME, } from '../../../../common/elasticsearch_fieldnames'; @@ -56,6 +57,11 @@ export async function getServiceProfilingTimeline({ }, }, aggs: { + num_profiles: { + cardinality: { + field: PROFILE_ID, + }, + }, value_type: { filters: { filters: { @@ -67,6 +73,13 @@ export async function getServiceProfilingTimeline({ }, }, }, + aggs: { + num_profiles: { + cardinality: { + field: PROFILE_ID, + }, + }, + }, }, }, }, @@ -83,12 +96,12 @@ export async function getServiceProfilingTimeline({ return aggregations.timeseries.buckets.map((bucket) => { return { x: bucket.key, - count: bucket.doc_count, + count: bucket.num_profiles.value, valueTypes: { - [ProfilingValueType.cpuTime]: - bucket.value_type.buckets.cpu_time.doc_count, - [ProfilingValueType.wallTime]: - bucket.value_type.buckets.wall_time.doc_count, + // TODO: use enum as object key. not possible right now + // because of https://github.com/microsoft/TypeScript/issues/37888 + cpu_time: bucket.value_type.buckets.cpu_time.num_profiles.value, + wall_time: bucket.value_type.buckets.wall_time.num_profiles.value, }, }; }); From 20ff20b91ea0973dd98fd8e476004c1e3d964eb8 Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Thu, 18 Feb 2021 11:38:28 +0100 Subject: [PATCH 04/10] Select profile type --- .../app/service_profiling/index.tsx | 14 +- .../service_profiling_flamegraph.tsx | 2 +- .../service_profiling_timeline.tsx | 130 +++++++++++++++--- .../get_service_profiling_statistics.ts | 18 ++- .../get_service_profiling_timeline.ts | 22 +-- 5 files changed, 145 insertions(+), 41 deletions(-) diff --git a/x-pack/plugins/apm/public/components/app/service_profiling/index.tsx b/x-pack/plugins/apm/public/components/app/service_profiling/index.tsx index 551c9163b7629..f82d78d8fab66 100644 --- a/x-pack/plugins/apm/public/components/app/service_profiling/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_profiling/index.tsx @@ -53,7 +53,9 @@ export function ServiceProfiling({ } const availableValueTypes = data.reduce((set, point) => { - (Object.keys(point.valueTypes) as ProfilingValueType[]) + (Object.keys(point.valueTypes).filter( + (type) => type !== 'unknown' + ) as ProfilingValueType[]) .filter((type) => point.valueTypes[type] > 0) .forEach((type) => { set.add(type); @@ -83,7 +85,15 @@ export function ServiceProfiling({ - + { + setValueType(type); + }} + selectedValueType={valueType} + /> }; +} & { valueTypes: Record }; + +const labels = { + unknown: i18n.translate('xpack.apm.serviceProfiling.valueTypeLabel.unknown', { + defaultMessage: 'Other', + }), + [ProfilingValueType.cpuTime]: i18n.translate( + 'xpack.apm.serviceProfiling.valueTypeLabel.cpuTime', + { + defaultMessage: 'On-CPU', + } + ), + [ProfilingValueType.wallTime]: i18n.translate( + 'xpack.apm.serviceProfiling.valueTypeLabel.wallTime', + { + defaultMessage: 'Wall', + } + ), +}; + +const palette = euiPaletteColorBlind(); export function ServiceProfilingTimeline({ start, end, - profiles, + series, + onValueTypeSelect, + selectedValueType, }: { - profiles: ProfilingTimelineItem[]; - start?: number; - end?: number; + series: ProfilingTimelineItem[]; + start: string; + end: string; + onValueTypeSelect: (valueType: ProfilingValueType) => void; + selectedValueType: ProfilingValueType | undefined; }) { const chartTheme = useChartTheme(); - const xFormat = niceTimeFormatter([start!, end!]); + const xFormat = niceTimeFormatter([Date.parse(start), Date.parse(end)]); + + function getSeriesForValueType(type: ProfilingValueType | 'unknown') { + const label = labels[type]; + + return { + name: label, + id: type, + data: series.map((coord) => ({ + x: coord.x, + y: coord.valueTypes[type], + })), + }; + } + + const specs = [ + getSeriesForValueType('unknown'), + getSeriesForValueType(ProfilingValueType.cpuTime), + getSeriesForValueType(ProfilingValueType.wallTime), + ] + .filter((spec) => spec.data.some((coord) => coord.y > 0)) + .map((spec, index) => { + return { + ...spec, + color: palette[index], + }; + }); return ( - + - d.valueTypes[ProfilingValueType.cpuTime], - (d) => d.valueTypes[ProfilingValueType.wallTime], - ]} - data={profiles} - /> + {specs.map((spec) => ( + + ))} + + + {specs.map((spec) => ( + + + + + + + { + if (spec.id !== 'unknown') { + onValueTypeSelect(spec.id); + } + }} + > + {spec.name} + + + + + ))} + + ); } diff --git a/x-pack/plugins/apm/server/lib/services/profiling/get_service_profiling_statistics.ts b/x-pack/plugins/apm/server/lib/services/profiling/get_service_profiling_statistics.ts index 99803f5c38716..204029aa5edd0 100644 --- a/x-pack/plugins/apm/server/lib/services/profiling/get_service_profiling_statistics.ts +++ b/x-pack/plugins/apm/server/lib/services/profiling/get_service_profiling_statistics.ts @@ -4,7 +4,8 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { keyBy } from 'lodash'; +import { keyBy, last } from 'lodash'; +import { ProfileStackFrame } from '../../../../typings/es_schemas/ui/profile'; import { ProfilingValueType, ProfileNode } from '../../../../common/profiling'; import { ProcessorEvent } from '../../../../common/processor_event'; import { ESFilter } from '../../../../../../typings/elasticsearch'; @@ -18,8 +19,7 @@ import { PROFILE_WALL_US, SERVICE_NAME, } from '../../../../common/elasticsearch_fieldnames'; -import { rangeFilter } from '../../../../common/utils/range_filter'; -import { getEnvironmentUiFilterES } from '../../helpers/convert_ui_filters/get_environment_ui_filter_es'; +import { rangeQuery, environmentQuery } from '../../../../common/utils/queries'; import { APMEventClient } from '../../helpers/create_es_client/create_apm_event_client'; import { Setup, SetupTimeRange } from '../../helpers/setup_request'; @@ -150,6 +150,12 @@ async function getProfilesWithStacks({ return response.hits.hits.map((hit) => hit._source); } +function getNodeNameFromFrame(frame: ProfileStackFrame) { + return [last(frame.function.split('/')), frame.line] + .filter(Boolean) + .join(':'); +} + export async function getServiceProfilingStatistics({ serviceName, setup, @@ -169,9 +175,9 @@ export async function getServiceProfilingStatistics({ }[valueType]; const filter: ESFilter[] = [ - { range: rangeFilter(start, end) }, + ...rangeQuery(start, end), { term: { [SERVICE_NAME]: serviceName } }, - ...getEnvironmentUiFilterES(environment), + ...environmentQuery(environment), { exists: { field: valueTypeField } }, ]; @@ -199,7 +205,7 @@ export async function getServiceProfilingStatistics({ const frames = profile.profile.stack.concat().reverse(); frames.forEach((frame, index) => { - const node = getNode({ id: frame.id, name: frame.function }); + const node = getNode({ id: frame.id, name: getNodeNameFromFrame(frame) }); if (index === frames.length - 1) { node.value += stats.value; diff --git a/x-pack/plugins/apm/server/lib/services/profiling/get_service_profiling_timeline.ts b/x-pack/plugins/apm/server/lib/services/profiling/get_service_profiling_timeline.ts index c05e4733462ff..f09bf3a4de68c 100644 --- a/x-pack/plugins/apm/server/lib/services/profiling/get_service_profiling_timeline.ts +++ b/x-pack/plugins/apm/server/lib/services/profiling/get_service_profiling_timeline.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { rangeFilter } from '../../../../common/utils/range_filter'; +import { rangeQuery, environmentQuery } from '../../../../common/utils/queries'; import { ProcessorEvent } from '../../../../common/processor_event'; import { PROFILE_CPU_NS, @@ -16,7 +16,6 @@ import { import { ProfilingValueType } from '../../../../common/profiling'; import { Setup, SetupTimeRange } from '../../helpers/setup_request'; import { getBucketSize } from '../../helpers/get_bucket_size'; -import { getEnvironmentUiFilterES } from '../../helpers/convert_ui_filters/get_environment_ui_filter_es'; export async function getServiceProfilingTimeline({ serviceName, @@ -39,8 +38,8 @@ export async function getServiceProfilingTimeline({ bool: { filter: [ { term: { [SERVICE_NAME]: serviceName } }, - { range: rangeFilter(start, end) }, - ...getEnvironmentUiFilterES(environment), + ...rangeQuery(start, end), + ...environmentQuery(environment), ...esFilter, ], }, @@ -57,14 +56,17 @@ export async function getServiceProfilingTimeline({ }, }, aggs: { - num_profiles: { - cardinality: { - field: PROFILE_ID, - }, - }, value_type: { filters: { filters: { + unknown: { + bool: { + must_not: [ + { exists: { field: PROFILE_CPU_NS } }, + { exists: { field: PROFILE_WALL_US } }, + ], + }, + }, [ProfilingValueType.cpuTime]: { exists: { field: PROFILE_CPU_NS }, }, @@ -96,8 +98,8 @@ export async function getServiceProfilingTimeline({ return aggregations.timeseries.buckets.map((bucket) => { return { x: bucket.key, - count: bucket.num_profiles.value, valueTypes: { + unknown: bucket.value_type.buckets.unknown.num_profiles.value, // TODO: use enum as object key. not possible right now // because of https://github.com/microsoft/TypeScript/issues/37888 cpu_time: bucket.value_type.buckets.cpu_time.num_profiles.value, From c1790ac0fffa5e069162a883be50fe50c09a76ba Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Thu, 18 Feb 2021 11:46:32 +0100 Subject: [PATCH 05/10] Instrument profiling es calls --- .../get_service_profiling_statistics.ts | 286 +++++++++--------- .../get_service_profiling_timeline.ts | 129 ++++---- 2 files changed, 214 insertions(+), 201 deletions(-) diff --git a/x-pack/plugins/apm/server/lib/services/profiling/get_service_profiling_statistics.ts b/x-pack/plugins/apm/server/lib/services/profiling/get_service_profiling_statistics.ts index 204029aa5edd0..91b0c2bfb03c1 100644 --- a/x-pack/plugins/apm/server/lib/services/profiling/get_service_profiling_statistics.ts +++ b/x-pack/plugins/apm/server/lib/services/profiling/get_service_profiling_statistics.ts @@ -22,6 +22,7 @@ import { import { rangeQuery, environmentQuery } from '../../../../common/utils/queries'; import { APMEventClient } from '../../helpers/create_es_client/create_apm_event_client'; import { Setup, SetupTimeRange } from '../../helpers/setup_request'; +import { withApmSpan } from '../../../utils/with_apm_span'; const MAX_STACK_IDS = 10000; const MAX_PROFILE_IDS = 1000; @@ -34,7 +35,7 @@ const maybeAdd = (to: any[], value: any) => { to.push(value); }; -async function getProfilingStats({ +function getProfilingStats({ apmEventClient, filter, valueTypeField, @@ -43,111 +44,115 @@ async function getProfilingStats({ filter: ESFilter[]; valueTypeField: string; }) { - const response = await apmEventClient.search({ - apm: { - events: [ProcessorEvent.profile], - }, - body: { - size: 0, - query: { - bool: { - filter, - }, + return withApmSpan('get_profile_stats', async () => { + const response = await apmEventClient.search({ + apm: { + events: [ProcessorEvent.profile], }, - aggs: { - profiles: { - terms: { - field: PROFILE_ID, - size: MAX_PROFILE_IDS, + body: { + size: 0, + query: { + bool: { + filter, }, - aggs: { - latest: { - top_metrics: { - metrics: [ - { field: '@timestamp' }, - { field: PROFILE_DURATION }, - ] as const, - sort: { - '@timestamp': 'desc', + }, + aggs: { + profiles: { + terms: { + field: PROFILE_ID, + size: MAX_PROFILE_IDS, + }, + aggs: { + latest: { + top_metrics: { + metrics: [ + { field: '@timestamp' }, + { field: PROFILE_DURATION }, + ] as const, + sort: { + '@timestamp': 'desc', + }, }, }, }, }, - }, - stacks: { - terms: { - field: PROFILE_TOP_ID, - size: MAX_STACK_IDS, - }, - aggs: { - value: { - sum: { - field: valueTypeField, - }, + stacks: { + terms: { + field: PROFILE_TOP_ID, + size: MAX_STACK_IDS, }, - [PROFILE_SAMPLES_COUNT]: { - sum: { - field: PROFILE_SAMPLES_COUNT, + aggs: { + value: { + sum: { + field: valueTypeField, + }, + }, + [PROFILE_SAMPLES_COUNT]: { + sum: { + field: PROFILE_SAMPLES_COUNT, + }, }, }, }, }, }, - }, - }); + }); - const profiles = - response.aggregations?.profiles.buckets.map((profile) => { - const latest = profile.latest.top[0].metrics ?? {}; - - return { - id: profile.key as string, - duration: latest[PROFILE_DURATION] as number, - timestamp: latest['@timestamp'] as number, - }; - }) ?? []; - - const stacks = - response.aggregations?.stacks.buckets.map((stack) => { - return { - id: stack.key as string, - value: stack.value.value!, - count: stack[PROFILE_SAMPLES_COUNT].value!, - }; - }) ?? []; - - return { - profiles, - stacks, - }; + const profiles = + response.aggregations?.profiles.buckets.map((profile) => { + const latest = profile.latest.top[0].metrics ?? {}; + + return { + id: profile.key as string, + duration: latest[PROFILE_DURATION] as number, + timestamp: latest['@timestamp'] as number, + }; + }) ?? []; + + const stacks = + response.aggregations?.stacks.buckets.map((stack) => { + return { + id: stack.key as string, + value: stack.value.value!, + count: stack[PROFILE_SAMPLES_COUNT].value!, + }; + }) ?? []; + + return { + profiles, + stacks, + }; + }); } -async function getProfilesWithStacks({ +function getProfilesWithStacks({ apmEventClient, filter, }: { apmEventClient: APMEventClient; filter: ESFilter[]; }) { - const response = await apmEventClient.search({ - apm: { - events: [ProcessorEvent.profile], - }, - body: { - size: MAX_STACK_IDS, - query: { - bool: { - filter, - }, + return withApmSpan('get_profiles_with_stacks', async () => { + const response = await apmEventClient.search({ + apm: { + events: [ProcessorEvent.profile], }, - collapse: { - field: PROFILE_TOP_ID, + body: { + size: MAX_STACK_IDS, + query: { + bool: { + filter, + }, + }, + collapse: { + field: PROFILE_TOP_ID, + }, + _source: [PROFILE_TOP_ID, PROFILE_STACK], }, - _source: [PROFILE_TOP_ID, PROFILE_STACK], - }, - }); + }); - return response.hits.hits.map((hit) => hit._source); + return response.hits.hits.map((hit) => hit._source); + }); } function getNodeNameFromFrame(frame: ProfileStackFrame) { @@ -167,64 +172,69 @@ export async function getServiceProfilingStatistics({ environment?: string; valueType: ProfilingValueType; }) { - const { apmEventClient, start, end } = setup; - - const valueTypeField = { - [ProfilingValueType.wallTime]: PROFILE_WALL_US, - [ProfilingValueType.cpuTime]: PROFILE_CPU_NS, - }[valueType]; - - const filter: ESFilter[] = [ - ...rangeQuery(start, end), - { term: { [SERVICE_NAME]: serviceName } }, - ...environmentQuery(environment), - { exists: { field: valueTypeField } }, - ]; - - const [profileStats, profileStacks] = await Promise.all([ - getProfilingStats({ apmEventClient, filter, valueTypeField }), - getProfilesWithStacks({ apmEventClient, filter }), - ]); - - const nodes: Record = {}; - const rootNodes: string[] = []; - - function getNode({ id, name }: { id: string; name: string }) { - let node = nodes[id]; - if (!node) { - node = { id, name, value: 0, count: 0, children: [] }; - nodes[id] = node; - } - return node; - } - - const stackStatsById = keyBy(profileStats.stacks, 'id'); - - profileStacks.forEach((profile) => { - const stats = stackStatsById[profile.profile.top.id]; - const frames = profile.profile.stack.concat().reverse(); - - frames.forEach((frame, index) => { - const node = getNode({ id: frame.id, name: getNodeNameFromFrame(frame) }); - - if (index === frames.length - 1) { - node.value += stats.value; - node.count += stats.count; + return withApmSpan('get_service_profiling_statistics', async () => { + const { apmEventClient, start, end } = setup; + + const valueTypeField = { + [ProfilingValueType.wallTime]: PROFILE_WALL_US, + [ProfilingValueType.cpuTime]: PROFILE_CPU_NS, + }[valueType]; + + const filter: ESFilter[] = [ + ...rangeQuery(start, end), + { term: { [SERVICE_NAME]: serviceName } }, + ...environmentQuery(environment), + { exists: { field: valueTypeField } }, + ]; + + const [profileStats, profileStacks] = await Promise.all([ + getProfilingStats({ apmEventClient, filter, valueTypeField }), + getProfilesWithStacks({ apmEventClient, filter }), + ]); + + const nodes: Record = {}; + const rootNodes: string[] = []; + + function getNode({ id, name }: { id: string; name: string }) { + let node = nodes[id]; + if (!node) { + node = { id, name, value: 0, count: 0, children: [] }; + nodes[id] = node; } + return node; + } - if (index === 0) { - // root node - maybeAdd(rootNodes, node.id); - } else { - const parent = nodes[frames[index - 1].id]; - maybeAdd(parent.children, node.id); - } + const stackStatsById = keyBy(profileStats.stacks, 'id'); + + profileStacks.forEach((profile) => { + const stats = stackStatsById[profile.profile.top.id]; + const frames = profile.profile.stack.concat().reverse(); + + frames.forEach((frame, index) => { + const node = getNode({ + id: frame.id, + name: getNodeNameFromFrame(frame), + }); + + if (index === frames.length - 1) { + node.value += stats.value; + node.count += stats.count; + } + + if (index === 0) { + // root node + maybeAdd(rootNodes, node.id); + } else { + const parent = nodes[frames[index - 1].id]; + maybeAdd(parent.children, node.id); + } + }); }); - }); - return { - profiles: profileStats.profiles, - nodes, - rootNodes, - }; + return { + profiles: profileStats.profiles, + nodes, + rootNodes, + }; + }); } diff --git a/x-pack/plugins/apm/server/lib/services/profiling/get_service_profiling_timeline.ts b/x-pack/plugins/apm/server/lib/services/profiling/get_service_profiling_timeline.ts index f09bf3a4de68c..51c08bceef5f1 100644 --- a/x-pack/plugins/apm/server/lib/services/profiling/get_service_profiling_timeline.ts +++ b/x-pack/plugins/apm/server/lib/services/profiling/get_service_profiling_timeline.ts @@ -16,6 +16,7 @@ import { import { ProfilingValueType } from '../../../../common/profiling'; import { Setup, SetupTimeRange } from '../../helpers/setup_request'; import { getBucketSize } from '../../helpers/get_bucket_size'; +import { withApmSpan } from '../../../utils/with_apm_span'; export async function getServiceProfilingTimeline({ serviceName, @@ -26,59 +27,61 @@ export async function getServiceProfilingTimeline({ setup: Setup & SetupTimeRange; environment?: string; }) { - const { apmEventClient, start, end, esFilter } = setup; + return withApmSpan('get_service_profiling_timeline', async () => { + const { apmEventClient, start, end, esFilter } = setup; - const response = await apmEventClient.search({ - apm: { - events: [ProcessorEvent.profile], - }, - body: { - size: 0, - query: { - bool: { - filter: [ - { term: { [SERVICE_NAME]: serviceName } }, - ...rangeQuery(start, end), - ...environmentQuery(environment), - ...esFilter, - ], - }, + const response = await apmEventClient.search({ + apm: { + events: [ProcessorEvent.profile], }, - aggs: { - timeseries: { - date_histogram: { - field: '@timestamp', - fixed_interval: getBucketSize({ start, end }).intervalString, - min_doc_count: 0, - extended_bounds: { - min: start, - max: end, - }, + body: { + size: 0, + query: { + bool: { + filter: [ + { term: { [SERVICE_NAME]: serviceName } }, + ...rangeQuery(start, end), + ...environmentQuery(environment), + ...esFilter, + ], }, - aggs: { - value_type: { - filters: { + }, + aggs: { + timeseries: { + date_histogram: { + field: '@timestamp', + fixed_interval: getBucketSize({ start, end }).intervalString, + min_doc_count: 0, + extended_bounds: { + min: start, + max: end, + }, + }, + aggs: { + value_type: { filters: { - unknown: { - bool: { - must_not: [ - { exists: { field: PROFILE_CPU_NS } }, - { exists: { field: PROFILE_WALL_US } }, - ], + filters: { + unknown: { + bool: { + must_not: [ + { exists: { field: PROFILE_CPU_NS } }, + { exists: { field: PROFILE_WALL_US } }, + ], + }, + }, + [ProfilingValueType.cpuTime]: { + exists: { field: PROFILE_CPU_NS }, + }, + [ProfilingValueType.wallTime]: { + exists: { field: PROFILE_WALL_US }, }, - }, - [ProfilingValueType.cpuTime]: { - exists: { field: PROFILE_CPU_NS }, - }, - [ProfilingValueType.wallTime]: { - exists: { field: PROFILE_WALL_US }, }, }, - }, - aggs: { - num_profiles: { - cardinality: { - field: PROFILE_ID, + aggs: { + num_profiles: { + cardinality: { + field: PROFILE_ID, + }, }, }, }, @@ -86,25 +89,25 @@ export async function getServiceProfilingTimeline({ }, }, }, - }, - }); + }); - const { aggregations } = response; + const { aggregations } = response; - if (!aggregations) { - return []; - } + if (!aggregations) { + return []; + } - return aggregations.timeseries.buckets.map((bucket) => { - return { - x: bucket.key, - valueTypes: { - unknown: bucket.value_type.buckets.unknown.num_profiles.value, - // TODO: use enum as object key. not possible right now - // because of https://github.com/microsoft/TypeScript/issues/37888 - cpu_time: bucket.value_type.buckets.cpu_time.num_profiles.value, - wall_time: bucket.value_type.buckets.wall_time.num_profiles.value, - }, - }; + return aggregations.timeseries.buckets.map((bucket) => { + return { + x: bucket.key, + valueTypes: { + unknown: bucket.value_type.buckets.unknown.num_profiles.value, + // TODO: use enum as object key. not possible right now + // because of https://github.com/microsoft/TypeScript/issues/37888 + cpu_time: bucket.value_type.buckets.cpu_time.num_profiles.value, + wall_time: bucket.value_type.buckets.wall_time.num_profiles.value, + }, + }; + }); }); } From 02342c22ae3a31a685ec2344ec596d1bc19c5d88 Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Thu, 18 Feb 2021 12:55:18 +0100 Subject: [PATCH 06/10] Enable drilldown --- .../app/service_profiling/service_profiling_flamegraph.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugins/apm/public/components/app/service_profiling/service_profiling_flamegraph.tsx b/x-pack/plugins/apm/public/components/app/service_profiling/service_profiling_flamegraph.tsx index d528314261974..70a09879642eb 100644 --- a/x-pack/plugins/apm/public/components/app/service_profiling/service_profiling_flamegraph.tsx +++ b/x-pack/plugins/apm/public/components/app/service_profiling/service_profiling_flamegraph.tsx @@ -190,6 +190,7 @@ export function ServiceProfilingFlamegraph({ fontFamily: theme.eui.euiCodeFontFamily, // clip: true, }, + drilldown: true, fontFamily: theme.eui.euiCodeFontFamily, minFontSize: 9, maxFontSize: 9, From b21065b4bb5acb7eaf51eadb22e542629155f0c7 Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Thu, 18 Feb 2021 22:45:48 +0100 Subject: [PATCH 07/10] Highlighting + fqn + collapsing --- x-pack/plugins/apm/common/profiling.ts | 4 +- .../app/service_profiling/index.tsx | 11 +- .../service_profiling_flamegraph.tsx | 309 ++++++++++++++---- .../get_service_profiling_statistics.ts | 161 +++++---- x-pack/plugins/apm/server/routes/services.ts | 1 + 5 files changed, 343 insertions(+), 143 deletions(-) diff --git a/x-pack/plugins/apm/common/profiling.ts b/x-pack/plugins/apm/common/profiling.ts index 6b8468274d155..746ea03a76ca1 100644 --- a/x-pack/plugins/apm/common/profiling.ts +++ b/x-pack/plugins/apm/common/profiling.ts @@ -12,8 +12,8 @@ export enum ProfilingValueType { export interface ProfileNode { id: string; - name: string; + label: string; + fqn: string; value: number; - count: number; children: string[]; } diff --git a/x-pack/plugins/apm/public/components/app/service_profiling/index.tsx b/x-pack/plugins/apm/public/components/app/service_profiling/index.tsx index f82d78d8fab66..5d1f2866dcb93 100644 --- a/x-pack/plugins/apm/public/components/app/service_profiling/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_profiling/index.tsx @@ -26,6 +26,7 @@ export function ServiceProfiling({ }: ServiceProfilingProps) { const { urlParams: { start, end }, + uiFilters, } = useUrlParams(); const { data = [] } = useFetcher( @@ -38,11 +39,16 @@ export function ServiceProfiling({ endpoint: 'GET /api/apm/services/{serviceName}/profiling/timeline', params: { path: { serviceName }, - query: { start, end, environment, uiFilters: JSON.stringify({}) }, + query: { + start, + end, + environment, + uiFilters: JSON.stringify(uiFilters), + }, }, }); }, - [start, end, serviceName, environment] + [start, end, serviceName, environment, uiFilters] ); const [valueType, setValueType] = useState(); @@ -100,6 +106,7 @@ export function ServiceProfiling({ valueType={valueType} start={start} end={end} + uiFilters={uiFilters} /> diff --git a/x-pack/plugins/apm/public/components/app/service_profiling/service_profiling_flamegraph.tsx b/x-pack/plugins/apm/public/components/app/service_profiling/service_profiling_flamegraph.tsx index 70a09879642eb..ca09bb4734ec7 100644 --- a/x-pack/plugins/apm/public/components/app/service_profiling/service_profiling_flamegraph.tsx +++ b/x-pack/plugins/apm/public/components/app/service_profiling/service_profiling_flamegraph.tsx @@ -4,8 +4,6 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React from 'react'; -import { sumBy } from 'lodash'; import { Chart, Datum, @@ -13,39 +11,105 @@ import { PartitionLayout, PrimitiveValue, Settings, + TooltipInfo, } from '@elastic/charts'; -import { euiPaletteForTemperature } from '@elastic/eui'; -import { useMemo } from 'react'; -import seedrandom from 'seedrandom/'; +import { EuiInMemoryTable } from '@elastic/eui'; +import { EuiFieldText } from '@elastic/eui'; +import { + EuiCheckbox, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + euiPaletteForTemperature, + EuiText, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { find, sumBy, sortBy } from 'lodash'; +import { rgba } from 'polished'; +import React, { useMemo, useState } from 'react'; +import seedrandom from 'seedrandom'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { useChartTheme } from '../../../../../observability/public'; import { ProfileNode, ProfilingValueType } from '../../../../common/profiling'; +import { asDuration } from '../../../../common/utils/formatters'; +import { UIFilters } from '../../../../typings/ui_filters'; import { useFetcher } from '../../../hooks/use_fetcher'; import { useTheme } from '../../../hooks/use_theme'; +import { px, unit } from '../../../style/variables'; const colors = euiPaletteForTemperature(100).slice(50, 85); interface ProfileDataPoint { + id: string; value: number; - name: string | undefined; depth: number; layers: Record; } +const TooltipContainer = euiStyled.div` + background-color: ${(props) => props.theme.eui.euiColorDarkestShade}; + border-radius: ${(props) => props.theme.eui.euiBorderRadius}; + color: ${(props) => props.theme.eui.euiColorLightestShade}; + padding: ${(props) => props.theme.eui.paddingSizes.s}; +`; + +function CustomTooltip({ + values, + nodes, +}: TooltipInfo & { + nodes: Record; +}) { + const first = values[0]; + + const foundNode = find(nodes, (node) => node.label === first.label); + + const label = foundNode?.fqn ?? first.label; + const value = asDuration(first.value) + first.formattedValue; + + return ( + + + + + + + {label} + + + + {value} + + + + + ); +} + export function ServiceProfilingFlamegraph({ serviceName, environment, valueType, start, end, + uiFilters, }: { serviceName: string; environment?: string; valueType?: ProfilingValueType; start?: string; end?: string; + uiFilters: UIFilters; }) { const theme = useTheme(); + const [collapseSimilarFrames, setCollapseSimilarFrames] = useState(true); + const [highlightFilter, setHighlightFilter] = useState(''); + const { data } = useFetcher( (callApmApi) => { if (!start || !end || !valueType) { @@ -63,19 +127,17 @@ export function ServiceProfilingFlamegraph({ end, environment, valueType, + uiFilters: JSON.stringify(uiFilters), }, }, }); }, - [start, end, environment, serviceName, valueType] + [start, end, environment, serviceName, valueType, uiFilters] ); - const spec = useMemo(() => { + const points = useMemo(() => { if (!data) { - return { - layers: [], - points: [], - }; + return []; } const { rootNodes, nodes } = data; @@ -87,27 +149,38 @@ export function ServiceProfilingFlamegraph({ const { children } = node; if (!children.length) { + // edge return [ { - name: node.name, + id: node.id, value: node.value, depth, layers: { - [depth]: node.name, + [depth]: node.id, }, }, ]; } - const childDataPoints = children - .flatMap((childId) => getDataPoints(nodes[childId], depth + 1)) - .map((point) => ({ - ...point, - layers: { - ...point.layers, - [depth]: node.name, - }, - })); + const directChildNodes = children.map((childId) => nodes[childId]); + + const shouldCollapse = + collapseSimilarFrames && + node.value === 0 && + directChildNodes.length === 1 && + directChildNodes[0].value === 0; + + const nextDepth = shouldCollapse ? depth : depth + 1; + + const childDataPoints = children.flatMap((childId) => + getDataPoints(nodes[childId], nextDepth) + ); + + if (!shouldCollapse) { + childDataPoints.forEach((point) => { + point.layers[depth] = node.id; + }); + } const totalTime = sumBy(childDataPoints, 'value'); const selfTime = node.value - totalTime; @@ -119,42 +192,57 @@ export function ServiceProfilingFlamegraph({ return [ ...childDataPoints, { - name: undefined, + id: '', value: selfTime, - layers: { [depth + 1]: undefined }, + layers: { [nextDepth]: '' }, depth, }, ]; }; const root = { - name: 'root', id: 'root', + label: 'root', + fqn: 'root', children: rootNodes, - ...rootNodes.reduce( - (prev, id) => { - const node = nodes[id]; - - return { - value: prev.value + node.value, - count: prev.count + node.count, - }; - }, - { value: 0, count: 0 } - ), + value: 0, }; - const points = getDataPoints(root, 0); + nodes.root = root; + + return getDataPoints(root, 0); + }, [data, collapseSimilarFrames]); + + const layers = useMemo(() => { + if (!data || !points.length) { + return []; + } + + const { nodes } = data; const maxDepth = Math.max(...points.map((point) => point.depth)); - const layers = [...new Array(maxDepth)].map((_, depth) => { + return [...new Array(maxDepth)].map((_, depth) => { return { groupByRollup: (d: Datum) => d.layers[depth], - nodeLabel: (d: PrimitiveValue) => String(d), - showAccessor: (d: PrimitiveValue) => !!d, + nodeLabel: (id: PrimitiveValue) => { + if (nodes[id!]) { + return nodes[id!].label; + } + return ''; + }, + showAccessor: (id: PrimitiveValue) => !!id, shape: { fillColor: (d: { dataName: string }) => { + const node = nodes[d.dataName]; + + if ( + !node || + (highlightFilter && !node.fqn.includes(highlightFilter)) + ) { + return rgba(0, 0, 0, 0.25); + } + const integer = Math.abs(seedrandom(d.dataName).int32()) % colors.length; return colors[integer]; @@ -162,42 +250,123 @@ export function ServiceProfilingFlamegraph({ }, }; }); - - return { - points, - layers, - }; - }, [data]); + }, [points, highlightFilter, data]); const chartTheme = useChartTheme(); const chartSize = { - height: 800, + height: layers.length * 20, width: '100%', }; + const items = Object.values(data?.nodes ?? {}).filter((node) => + highlightFilter ? node.fqn.includes(highlightFilter) : true + ); + return ( - - - d.value as number} - valueFormatter={() => ''} - config={{ - fillLabel: { - fontFamily: theme.eui.euiCodeFontFamily, - // clip: true, - }, - drilldown: true, - fontFamily: theme.eui.euiCodeFontFamily, - minFontSize: 9, - maxFontSize: 9, - maxRowCount: 1, - partitionLayout: PartitionLayout.icicle, - }} - /> - + + + + ( + + ), + }} + /> + d.value as number} + valueFormatter={() => ''} + config={{ + fillLabel: { + fontFamily: theme.eui.euiCodeFontFamily, + clip: true, + }, + drilldown: true, + fontFamily: theme.eui.euiCodeFontFamily, + minFontSize: 9, + maxFontSize: 9, + maxRowCount: 1, + partitionLayout: PartitionLayout.icicle, + }} + /> + + + + + + { + setCollapseSimilarFrames((state) => !state); + }} + label={i18n.translate( + 'xpack.apm.profiling.collapseSimilarFrames', + { + defaultMessage: 'Collapse similar', + } + )} + /> + + + { + if (!e.target.value) { + setHighlightFilter(''); + } + }} + onKeyPress={(e) => { + if (e.charCode === 13) { + setHighlightFilter(() => e.target.value); + } + }} + /> + + + asDuration(us), + width: px(unit * 6), + }, + ]} + /> + + + + ); } diff --git a/x-pack/plugins/apm/server/lib/services/profiling/get_service_profiling_statistics.ts b/x-pack/plugins/apm/server/lib/services/profiling/get_service_profiling_statistics.ts index 91b0c2bfb03c1..9c50e77888b93 100644 --- a/x-pack/plugins/apm/server/lib/services/profiling/get_service_profiling_statistics.ts +++ b/x-pack/plugins/apm/server/lib/services/profiling/get_service_profiling_statistics.ts @@ -11,9 +11,6 @@ import { ProcessorEvent } from '../../../../common/processor_event'; import { ESFilter } from '../../../../../../typings/elasticsearch'; import { PROFILE_CPU_NS, - PROFILE_DURATION, - PROFILE_ID, - PROFILE_SAMPLES_COUNT, PROFILE_STACK, PROFILE_TOP_ID, PROFILE_WALL_US, @@ -25,7 +22,7 @@ import { Setup, SetupTimeRange } from '../../helpers/setup_request'; import { withApmSpan } from '../../../utils/with_apm_span'; const MAX_STACK_IDS = 10000; -const MAX_PROFILE_IDS = 1000; +const MAX_STACKS_PER_REQUEST = 1000; const maybeAdd = (to: any[], value: any) => { if (to.includes(value)) { @@ -57,29 +54,13 @@ function getProfilingStats({ }, }, aggs: { - profiles: { - terms: { - field: PROFILE_ID, - size: MAX_PROFILE_IDS, - }, - aggs: { - latest: { - top_metrics: { - metrics: [ - { field: '@timestamp' }, - { field: PROFILE_DURATION }, - ] as const, - sort: { - '@timestamp': 'desc', - }, - }, - }, - }, - }, stacks: { terms: { field: PROFILE_TOP_ID, size: MAX_STACK_IDS, + order: { + value: 'desc', + }, }, aggs: { value: { @@ -87,41 +68,21 @@ function getProfilingStats({ field: valueTypeField, }, }, - [PROFILE_SAMPLES_COUNT]: { - sum: { - field: PROFILE_SAMPLES_COUNT, - }, - }, }, }, }, }, }); - const profiles = - response.aggregations?.profiles.buckets.map((profile) => { - const latest = profile.latest.top[0].metrics ?? {}; - - return { - id: profile.key as string, - duration: latest[PROFILE_DURATION] as number, - timestamp: latest['@timestamp'] as number, - }; - }) ?? []; - const stacks = response.aggregations?.stacks.buckets.map((stack) => { return { id: stack.key as string, value: stack.value.value!, - count: stack[PROFILE_SAMPLES_COUNT].value!, }; }) ?? []; - return { - profiles, - stacks, - }; + return stacks; }); } @@ -133,29 +94,91 @@ function getProfilesWithStacks({ filter: ESFilter[]; }) { return withApmSpan('get_profiles_with_stacks', async () => { - const response = await apmEventClient.search({ - apm: { - events: [ProcessorEvent.profile], - }, - body: { - size: MAX_STACK_IDS, - query: { - bool: { - filter, - }, + const cardinalityResponse = await withApmSpan('get_top_cardinality', () => + apmEventClient.search({ + apm: { + events: [ProcessorEvent.profile], }, - collapse: { - field: PROFILE_TOP_ID, + body: { + size: 0, + query: { + bool: { filter }, + }, + aggs: { + top: { + cardinality: { + field: PROFILE_TOP_ID, + }, + }, + }, }, - _source: [PROFILE_TOP_ID, PROFILE_STACK], - }, + }) + ); + + const cardinality = cardinalityResponse.aggregations?.top.value ?? 0; + + const numStacksToFetch = Math.min( + Math.ceil(cardinality * 1.1), + MAX_STACK_IDS + ); + + const partitions = Math.ceil(numStacksToFetch / MAX_STACKS_PER_REQUEST); + + if (partitions === 0) { + return []; + } + + const allResponses = await withApmSpan('get_all_stacks', async () => { + return Promise.all( + [...new Array(partitions)].map(async (_, num) => { + const response = await withApmSpan('get_partition', () => + apmEventClient.search({ + apm: { + events: [ProcessorEvent.profile], + }, + body: { + query: { + bool: { + filter, + }, + }, + aggs: { + top: { + terms: { + field: PROFILE_TOP_ID, + size: Math.max(MAX_STACKS_PER_REQUEST), + include: { + num_partitions: partitions, + partition: num, + }, + }, + aggs: { + latest: { + top_hits: { + _source: [PROFILE_TOP_ID, PROFILE_STACK], + }, + }, + }, + }, + }, + }, + }) + ); + + return ( + response.aggregations?.top.buckets.flatMap((bucket) => { + return bucket.latest.hits.hits[0]._source; + }) ?? [] + ); + }) + ); }); - return response.hits.hits.map((hit) => hit._source); + return allResponses.flat(); }); } -function getNodeNameFromFrame(frame: ProfileStackFrame) { +function getNodeLabelFromFrame(frame: ProfileStackFrame) { return [last(frame.function.split('/')), frame.line] .filter(Boolean) .join(':'); @@ -185,6 +208,7 @@ export async function getServiceProfilingStatistics({ { term: { [SERVICE_NAME]: serviceName } }, ...environmentQuery(environment), { exists: { field: valueTypeField } }, + ...setup.esFilter, ]; const [profileStats, profileStacks] = await Promise.all([ @@ -195,30 +219,30 @@ export async function getServiceProfilingStatistics({ const nodes: Record = {}; const rootNodes: string[] = []; - function getNode({ id, name }: { id: string; name: string }) { + function getNode(frame: ProfileStackFrame) { + const { id, filename, function: functionName, line } = frame; + const location = [functionName, line].filter(Boolean).join(':'); + const fqn = [filename, location].filter(Boolean).join('/'); + const label = last(location.split('/'))!; let node = nodes[id]; if (!node) { - node = { id, name, value: 0, count: 0, children: [] }; + node = { id, label, fqn, value: 0, children: [] }; nodes[id] = node; } return node; } - const stackStatsById = keyBy(profileStats.stacks, 'id'); + const stackStatsById = keyBy(profileStats, 'id'); profileStacks.forEach((profile) => { const stats = stackStatsById[profile.profile.top.id]; const frames = profile.profile.stack.concat().reverse(); frames.forEach((frame, index) => { - const node = getNode({ - id: frame.id, - name: getNodeNameFromFrame(frame), - }); + const node = getNode(frame); if (index === frames.length - 1) { node.value += stats.value; - node.count += stats.count; } if (index === 0) { @@ -232,7 +256,6 @@ export async function getServiceProfilingStatistics({ }); return { - profiles: profileStats.profiles, nodes, rootNodes, }; diff --git a/x-pack/plugins/apm/server/routes/services.ts b/x-pack/plugins/apm/server/routes/services.ts index f1a71f28f5474..fec1f6993b8bd 100644 --- a/x-pack/plugins/apm/server/routes/services.ts +++ b/x-pack/plugins/apm/server/routes/services.ts @@ -515,6 +515,7 @@ export const serviceProfilingStatisticsRoute = createRoute({ }), query: t.intersection([ rangeRt, + uiFiltersRt, t.partial({ environment: t.string, }), From bc98e827cd3d7f8d8717e04806b289a2e66d6c8b Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Fri, 19 Feb 2021 10:12:03 +0100 Subject: [PATCH 08/10] Add heap profile support --- .../apm/common/elasticsearch_fieldnames.ts | 10 +- x-pack/plugins/apm/common/profiling.ts | 93 +++++++++++++++++++ .../app/service_profiling/index.tsx | 52 +++++++---- .../service_profiling_flamegraph.tsx | 65 +++++++++++-- .../service_profiling_timeline.tsx | 49 +++++----- .../get_service_profiling_statistics.ts | 46 ++++++--- .../get_service_profiling_timeline.ts | 40 ++++---- x-pack/plugins/apm/server/routes/services.ts | 6 ++ 8 files changed, 276 insertions(+), 85 deletions(-) diff --git a/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts b/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts index 6840c18f6b55a..ffd05b281208d 100644 --- a/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts +++ b/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts @@ -136,7 +136,13 @@ export const CLS_FIELD = 'transaction.experience.cls'; export const PROFILE_ID = 'profile.id'; export const PROFILE_DURATION = 'profile.duration'; export const PROFILE_TOP_ID = 'profile.top.id'; +export const PROFILE_STACK = 'profile.stack'; + +export const PROFILE_SAMPLES_COUNT = 'profile.samples.count'; export const PROFILE_CPU_NS = 'profile.cpu.ns'; export const PROFILE_WALL_US = 'profile.wall.us'; -export const PROFILE_SAMPLES_COUNT = 'profile.samples.count'; -export const PROFILE_STACK = 'profile.stack'; + +export const PROFILE_ALLOC_OBJECTS = 'profile.alloc_objects.count'; +export const PROFILE_ALLOC_SPACE = 'profile.alloc_space.bytes'; +export const PROFILE_INUSE_OBJECTS = 'profile.inuse_objects.count'; +export const PROFILE_INUSE_SPACE = 'profile.inuse_space.bytes'; diff --git a/x-pack/plugins/apm/common/profiling.ts b/x-pack/plugins/apm/common/profiling.ts index 746ea03a76ca1..e185ef704aed0 100644 --- a/x-pack/plugins/apm/common/profiling.ts +++ b/x-pack/plugins/apm/common/profiling.ts @@ -4,10 +4,32 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { i18n } from '@kbn/i18n'; +import { + PROFILE_ALLOC_OBJECTS, + PROFILE_ALLOC_SPACE, + PROFILE_CPU_NS, + PROFILE_INUSE_OBJECTS, + PROFILE_INUSE_SPACE, + PROFILE_SAMPLES_COUNT, + PROFILE_WALL_US, +} from './elasticsearch_fieldnames'; export enum ProfilingValueType { wallTime = 'wall_time', cpuTime = 'cpu_time', + samples = 'samples', + allocObjects = 'alloc_objects', + allocSpace = 'alloc_space', + inuseObjects = 'inuse_objects', + inuseSpace = 'inuse_space', +} + +export enum ProfilingValueTypeUnit { + ns = 'ns', + us = 'us', + count = 'count', + bytes = 'bytes', } export interface ProfileNode { @@ -17,3 +39,74 @@ export interface ProfileNode { value: number; children: string[]; } + +const config = { + [ProfilingValueType.wallTime]: { + unit: ProfilingValueTypeUnit.us, + label: i18n.translate( + 'xpack.apm.serviceProfiling.valueTypeLabel.wallTime', + { + defaultMessage: 'Wall', + } + ), + field: PROFILE_WALL_US, + }, + [ProfilingValueType.cpuTime]: { + unit: ProfilingValueTypeUnit.ns, + label: i18n.translate('xpack.apm.serviceProfiling.valueTypeLabel.cpuTime', { + defaultMessage: 'On-CPU', + }), + field: PROFILE_CPU_NS, + }, + [ProfilingValueType.samples]: { + unit: ProfilingValueTypeUnit.count, + label: i18n.translate('xpack.apm.serviceProfiling.valueTypeLabel.samples', { + defaultMessage: 'Samples', + }), + field: PROFILE_SAMPLES_COUNT, + }, + [ProfilingValueType.allocObjects]: { + unit: ProfilingValueTypeUnit.count, + label: i18n.translate( + 'xpack.apm.serviceProfiling.valueTypeLabel.allocObjects', + { + defaultMessage: 'Alloc. objects', + } + ), + field: PROFILE_ALLOC_OBJECTS, + }, + [ProfilingValueType.allocSpace]: { + unit: ProfilingValueTypeUnit.bytes, + label: i18n.translate( + 'xpack.apm.serviceProfiling.valueTypeLabel.allocSpace', + { + defaultMessage: 'Alloc. space', + } + ), + field: PROFILE_ALLOC_SPACE, + }, + [ProfilingValueType.inuseObjects]: { + unit: ProfilingValueTypeUnit.count, + label: i18n.translate( + 'xpack.apm.serviceProfiling.valueTypeLabel.inuseObjects', + { + defaultMessage: 'In-use objects', + } + ), + field: PROFILE_INUSE_OBJECTS, + }, + [ProfilingValueType.inuseSpace]: { + unit: ProfilingValueTypeUnit.bytes, + label: i18n.translate( + 'xpack.apm.serviceProfiling.valueTypeLabel.inuseSpace', + { + defaultMessage: 'In-use space', + } + ), + field: PROFILE_INUSE_SPACE, + }, +}; + +export const getValueTypeConfig = (type: ProfilingValueType) => { + return config[type]; +}; diff --git a/x-pack/plugins/apm/public/components/app/service_profiling/index.tsx b/x-pack/plugins/apm/public/components/app/service_profiling/index.tsx index 5d1f2866dcb93..2496cbd3f2650 100644 --- a/x-pack/plugins/apm/public/components/app/service_profiling/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_profiling/index.tsx @@ -8,7 +8,10 @@ import { EuiPanel } from '@elastic/eui'; import { EuiFlexGroup, EuiFlexItem, EuiPage, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useEffect, useState } from 'react'; -import { ProfilingValueType } from '../../../../common/profiling'; +import { + getValueTypeConfig, + ProfilingValueType, +} from '../../../../common/profiling'; import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { useFetcher } from '../../../hooks/use_fetcher'; import { SearchBar } from '../../shared/search_bar'; @@ -91,23 +94,36 @@ export function ServiceProfiling({ - { - setValueType(type); - }} - selectedValueType={valueType} - /> - + + + { + setValueType(type); + }} + selectedValueType={valueType} + /> + + {valueType ? ( + + +

{getValueTypeConfig(valueType).label}

+
+
+ ) : null} + + + +
diff --git a/x-pack/plugins/apm/public/components/app/service_profiling/service_profiling_flamegraph.tsx b/x-pack/plugins/apm/public/components/app/service_profiling/service_profiling_flamegraph.tsx index ca09bb4734ec7..feb2302200e7e 100644 --- a/x-pack/plugins/apm/public/components/app/service_profiling/service_profiling_flamegraph.tsx +++ b/x-pack/plugins/apm/public/components/app/service_profiling/service_profiling_flamegraph.tsx @@ -15,6 +15,7 @@ import { } from '@elastic/charts'; import { EuiInMemoryTable } from '@elastic/eui'; import { EuiFieldText } from '@elastic/eui'; +import { EuiToolTip } from '@elastic/eui'; import { EuiCheckbox, EuiFlexGroup, @@ -24,14 +25,23 @@ import { EuiText, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { find, sumBy, sortBy } from 'lodash'; +import { find, sumBy } from 'lodash'; import { rgba } from 'polished'; import React, { useMemo, useState } from 'react'; import seedrandom from 'seedrandom'; import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { useChartTheme } from '../../../../../observability/public'; -import { ProfileNode, ProfilingValueType } from '../../../../common/profiling'; -import { asDuration } from '../../../../common/utils/formatters'; +import { + getValueTypeConfig, + ProfileNode, + ProfilingValueType, + ProfilingValueTypeUnit, +} from '../../../../common/profiling'; +import { + asDuration, + asDynamicBytes, + asInteger, +} from '../../../../common/utils/formatters'; import { UIFilters } from '../../../../typings/ui_filters'; import { useFetcher } from '../../../hooks/use_fetcher'; import { useTheme } from '../../../hooks/use_theme'; @@ -53,18 +63,39 @@ const TooltipContainer = euiStyled.div` padding: ${(props) => props.theme.eui.paddingSizes.s}; `; +const formatValue = ( + value: number, + valueUnit: ProfilingValueTypeUnit +): string => { + switch (valueUnit) { + case ProfilingValueTypeUnit.ns: + return asDuration(value / 1000); + + case ProfilingValueTypeUnit.us: + return asDuration(value); + + case ProfilingValueTypeUnit.count: + return asInteger(value); + + case ProfilingValueTypeUnit.bytes: + return asDynamicBytes(value); + } +}; + function CustomTooltip({ values, nodes, + valueUnit, }: TooltipInfo & { nodes: Record; + valueUnit: ProfilingValueTypeUnit; }) { const first = values[0]; const foundNode = find(nodes, (node) => node.label === first.label); const label = foundNode?.fqn ?? first.label; - const value = asDuration(first.value) + first.formattedValue; + const value = formatValue(first.value, valueUnit) + first.formattedValue; return ( @@ -263,6 +294,10 @@ export function ServiceProfilingFlamegraph({ highlightFilter ? node.fqn.includes(highlightFilter) : true ); + const valueUnit = valueType + ? getValueTypeConfig(valueType).unit + : ProfilingValueTypeUnit.count; + return ( @@ -271,7 +306,11 @@ export function ServiceProfilingFlamegraph({ theme={chartTheme} tooltip={{ customTooltip: (info) => ( - + ), }} /> @@ -284,8 +323,10 @@ export function ServiceProfilingFlamegraph({ config={{ fillLabel: { fontFamily: theme.eui.euiCodeFontFamily, + // @ts-ignore (coming soon in Elastic charts) clip: true, }, + // @ts-expect-error (coming soon in Elastic charts) drilldown: true, fontFamily: theme.eui.euiCodeFontFamily, minFontSize: 9, @@ -326,7 +367,7 @@ export function ServiceProfilingFlamegraph({ }} onKeyPress={(e) => { if (e.charCode === 13) { - setHighlightFilter(() => e.target.value); + setHighlightFilter(() => (e.target as any).value); } }} /> @@ -339,7 +380,6 @@ export function ServiceProfilingFlamegraph({ field: 'value', direction: 'desc', }, - enableAllColumns: true, }} pagination={{ pageSize: 20, @@ -353,13 +393,20 @@ export function ServiceProfilingFlamegraph({ defaultMessage: 'Name', }), truncateText: true, + render: (_, item) => { + return ( + + {item.label} + + ); + }, }, { field: 'value', name: i18n.translate('xpack.apm.profiling.table.value', { - defaultMessage: 'Value', + defaultMessage: 'Self', }), - render: (us) => asDuration(us), + render: (_, item) => formatValue(item.value, valueUnit), width: px(unit * 6), }, ]} diff --git a/x-pack/plugins/apm/public/components/app/service_profiling/service_profiling_timeline.tsx b/x-pack/plugins/apm/public/components/app/service_profiling/service_profiling_timeline.tsx index 43885506d5c64..d5dc2f5d56afc 100644 --- a/x-pack/plugins/apm/public/components/app/service_profiling/service_profiling_timeline.tsx +++ b/x-pack/plugins/apm/public/components/app/service_profiling/service_profiling_timeline.tsx @@ -24,30 +24,15 @@ import { import { i18n } from '@kbn/i18n'; import React from 'react'; import { useChartTheme } from '../../../../../observability/public'; -import { ProfilingValueType } from '../../../../common/profiling'; +import { + getValueTypeConfig, + ProfilingValueType, +} from '../../../../common/profiling'; type ProfilingTimelineItem = { x: number; } & { valueTypes: Record }; -const labels = { - unknown: i18n.translate('xpack.apm.serviceProfiling.valueTypeLabel.unknown', { - defaultMessage: 'Other', - }), - [ProfilingValueType.cpuTime]: i18n.translate( - 'xpack.apm.serviceProfiling.valueTypeLabel.cpuTime', - { - defaultMessage: 'On-CPU', - } - ), - [ProfilingValueType.wallTime]: i18n.translate( - 'xpack.apm.serviceProfiling.valueTypeLabel.wallTime', - { - defaultMessage: 'Wall', - } - ), -}; - const palette = euiPaletteColorBlind(); export function ServiceProfilingTimeline({ @@ -68,7 +53,12 @@ export function ServiceProfilingTimeline({ const xFormat = niceTimeFormatter([Date.parse(start), Date.parse(end)]); function getSeriesForValueType(type: ProfilingValueType | 'unknown') { - const label = labels[type]; + const label = + type === 'unknown' + ? i18n.translate('xpack.apm.serviceProfiling.valueTypeLabel.unknown', { + defaultMessage: 'Other', + }) + : getValueTypeConfig(type).label; return { name: label, @@ -82,8 +72,9 @@ export function ServiceProfilingTimeline({ const specs = [ getSeriesForValueType('unknown'), - getSeriesForValueType(ProfilingValueType.cpuTime), - getSeriesForValueType(ProfilingValueType.wallTime), + ...Object.values(ProfilingValueType).map((type) => + getSeriesForValueType(type) + ), ] .filter((spec) => spec.data.some((coord) => coord.y > 0)) .map((spec, index) => { @@ -94,9 +85,9 @@ export function ServiceProfilingTimeline({ }); return ( - + - + @@ -107,6 +98,7 @@ export function ServiceProfilingTimeline({ xScaleType={ScaleType.Time} yScaleType={ScaleType.Linear} xAccessor="x" + stackAccessors={['x']} /> ))} @@ -135,7 +127,14 @@ export function ServiceProfilingTimeline({ } }} > - {spec.name} + + {spec.name} + diff --git a/x-pack/plugins/apm/server/lib/services/profiling/get_service_profiling_statistics.ts b/x-pack/plugins/apm/server/lib/services/profiling/get_service_profiling_statistics.ts index 9c50e77888b93..ffb798ac0da43 100644 --- a/x-pack/plugins/apm/server/lib/services/profiling/get_service_profiling_statistics.ts +++ b/x-pack/plugins/apm/server/lib/services/profiling/get_service_profiling_statistics.ts @@ -5,15 +5,20 @@ * 2.0. */ import { keyBy, last } from 'lodash'; +import { Logger } from 'kibana/server'; +import util from 'util'; +import { maybe } from '../../../../common/utils/maybe'; import { ProfileStackFrame } from '../../../../typings/es_schemas/ui/profile'; -import { ProfilingValueType, ProfileNode } from '../../../../common/profiling'; +import { + ProfilingValueType, + ProfileNode, + getValueTypeConfig, +} from '../../../../common/profiling'; import { ProcessorEvent } from '../../../../common/processor_event'; import { ESFilter } from '../../../../../../typings/elasticsearch'; import { - PROFILE_CPU_NS, PROFILE_STACK, PROFILE_TOP_ID, - PROFILE_WALL_US, SERVICE_NAME, } from '../../../../common/elasticsearch_fieldnames'; import { rangeQuery, environmentQuery } from '../../../../common/utils/queries'; @@ -178,30 +183,23 @@ function getProfilesWithStacks({ }); } -function getNodeLabelFromFrame(frame: ProfileStackFrame) { - return [last(frame.function.split('/')), frame.line] - .filter(Boolean) - .join(':'); -} - export async function getServiceProfilingStatistics({ serviceName, setup, environment, valueType, + logger, }: { serviceName: string; setup: Setup & SetupTimeRange; environment?: string; valueType: ProfilingValueType; + logger: Logger; }) { return withApmSpan('get_service_profiling_statistics', async () => { const { apmEventClient, start, end } = setup; - const valueTypeField = { - [ProfilingValueType.wallTime]: PROFILE_WALL_US, - [ProfilingValueType.cpuTime]: PROFILE_CPU_NS, - }[valueType]; + const valueTypeField = getValueTypeConfig(valueType).field; const filter: ESFilter[] = [ ...rangeQuery(start, end), @@ -234,14 +232,22 @@ export async function getServiceProfilingStatistics({ const stackStatsById = keyBy(profileStats, 'id'); + const missingStacks: string[] = []; + profileStacks.forEach((profile) => { - const stats = stackStatsById[profile.profile.top.id]; + const stats = maybe(stackStatsById[profile.profile.top.id]); + + if (!stats) { + missingStacks.push(profile.profile.top.id); + return; + } + const frames = profile.profile.stack.concat().reverse(); frames.forEach((frame, index) => { const node = getNode(frame); - if (index === frames.length - 1) { + if (index === frames.length - 1 && stats) { node.value += stats.value; } @@ -255,6 +261,16 @@ export async function getServiceProfilingStatistics({ }); }); + if (missingStacks.length > 0) { + logger.warn( + `Could not find stats for all stacks: ${util.inspect({ + numProfileStats: profileStats.length, + numStacks: profileStacks.length, + missing: missingStacks, + })}` + ); + } + return { nodes, rootNodes, diff --git a/x-pack/plugins/apm/server/lib/services/profiling/get_service_profiling_timeline.ts b/x-pack/plugins/apm/server/lib/services/profiling/get_service_profiling_timeline.ts index 51c08bceef5f1..dc29d6a43d82d 100644 --- a/x-pack/plugins/apm/server/lib/services/profiling/get_service_profiling_timeline.ts +++ b/x-pack/plugins/apm/server/lib/services/profiling/get_service_profiling_timeline.ts @@ -4,20 +4,28 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import { mapKeys, mapValues } from 'lodash'; import { rangeQuery, environmentQuery } from '../../../../common/utils/queries'; import { ProcessorEvent } from '../../../../common/processor_event'; import { - PROFILE_CPU_NS, PROFILE_ID, - PROFILE_WALL_US, SERVICE_NAME, } from '../../../../common/elasticsearch_fieldnames'; -import { ProfilingValueType } from '../../../../common/profiling'; +import { + getValueTypeConfig, + ProfilingValueType, +} from '../../../../common/profiling'; import { Setup, SetupTimeRange } from '../../helpers/setup_request'; import { getBucketSize } from '../../helpers/get_bucket_size'; import { withApmSpan } from '../../../utils/with_apm_span'; +const configMap = mapValues( + mapKeys(ProfilingValueType, (val, key) => val), + (value) => getValueTypeConfig(value) +) as Record>; + +const allFields = Object.values(configMap).map((config) => config.field); + export async function getServiceProfilingTimeline({ serviceName, environment, @@ -63,18 +71,14 @@ export async function getServiceProfilingTimeline({ filters: { unknown: { bool: { - must_not: [ - { exists: { field: PROFILE_CPU_NS } }, - { exists: { field: PROFILE_WALL_US } }, - ], + must_not: allFields.map((field) => ({ + exists: { field }, + })), }, }, - [ProfilingValueType.cpuTime]: { - exists: { field: PROFILE_CPU_NS }, - }, - [ProfilingValueType.wallTime]: { - exists: { field: PROFILE_WALL_US }, - }, + ...mapValues(configMap, ({ field }) => ({ + exists: { field }, + })), }, }, aggs: { @@ -104,8 +108,12 @@ export async function getServiceProfilingTimeline({ unknown: bucket.value_type.buckets.unknown.num_profiles.value, // TODO: use enum as object key. not possible right now // because of https://github.com/microsoft/TypeScript/issues/37888 - cpu_time: bucket.value_type.buckets.cpu_time.num_profiles.value, - wall_time: bucket.value_type.buckets.wall_time.num_profiles.value, + ...mapValues(configMap, (_, key) => { + return ( + bucket.value_type.buckets[key as ProfilingValueType]?.num_profiles + .value ?? 0 + ); + }), }, }; }); diff --git a/x-pack/plugins/apm/server/routes/services.ts b/x-pack/plugins/apm/server/routes/services.ts index fec1f6993b8bd..d396aafc54247 100644 --- a/x-pack/plugins/apm/server/routes/services.ts +++ b/x-pack/plugins/apm/server/routes/services.ts @@ -523,6 +523,11 @@ export const serviceProfilingStatisticsRoute = createRoute({ valueType: t.union([ t.literal(ProfilingValueType.wallTime), t.literal(ProfilingValueType.cpuTime), + t.literal(ProfilingValueType.samples), + t.literal(ProfilingValueType.allocObjects), + t.literal(ProfilingValueType.allocSpace), + t.literal(ProfilingValueType.inuseObjects), + t.literal(ProfilingValueType.inuseSpace), ]), }), ]), @@ -543,6 +548,7 @@ export const serviceProfilingStatisticsRoute = createRoute({ environment, valueType, setup, + logger: context.logger, }); }, }); From 252aaa96708c713053692c468c2b1db9a4df54f4 Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Fri, 19 Feb 2021 10:16:15 +0100 Subject: [PATCH 09/10] Updated snapshot --- .../elasticsearch_fieldnames.test.ts.snap | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap b/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap index 96291c43c562a..cc1b6688daa46 100644 --- a/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap +++ b/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap @@ -121,12 +121,20 @@ exports[`Error POD_NAME 1`] = `undefined`; exports[`Error PROCESSOR_EVENT 1`] = `"error"`; +exports[`Error PROFILE_ALLOC_OBJECTS 1`] = `undefined`; + +exports[`Error PROFILE_ALLOC_SPACE 1`] = `undefined`; + exports[`Error PROFILE_CPU_NS 1`] = `undefined`; exports[`Error PROFILE_DURATION 1`] = `undefined`; exports[`Error PROFILE_ID 1`] = `undefined`; +exports[`Error PROFILE_INUSE_OBJECTS 1`] = `undefined`; + +exports[`Error PROFILE_INUSE_SPACE 1`] = `undefined`; + exports[`Error PROFILE_SAMPLES_COUNT 1`] = `undefined`; exports[`Error PROFILE_STACK 1`] = `undefined`; @@ -344,12 +352,20 @@ exports[`Span POD_NAME 1`] = `undefined`; exports[`Span PROCESSOR_EVENT 1`] = `"span"`; +exports[`Span PROFILE_ALLOC_OBJECTS 1`] = `undefined`; + +exports[`Span PROFILE_ALLOC_SPACE 1`] = `undefined`; + exports[`Span PROFILE_CPU_NS 1`] = `undefined`; exports[`Span PROFILE_DURATION 1`] = `undefined`; exports[`Span PROFILE_ID 1`] = `undefined`; +exports[`Span PROFILE_INUSE_OBJECTS 1`] = `undefined`; + +exports[`Span PROFILE_INUSE_SPACE 1`] = `undefined`; + exports[`Span PROFILE_SAMPLES_COUNT 1`] = `undefined`; exports[`Span PROFILE_STACK 1`] = `undefined`; @@ -573,12 +589,20 @@ exports[`Transaction POD_NAME 1`] = `undefined`; exports[`Transaction PROCESSOR_EVENT 1`] = `"transaction"`; +exports[`Transaction PROFILE_ALLOC_OBJECTS 1`] = `undefined`; + +exports[`Transaction PROFILE_ALLOC_SPACE 1`] = `undefined`; + exports[`Transaction PROFILE_CPU_NS 1`] = `undefined`; exports[`Transaction PROFILE_DURATION 1`] = `undefined`; exports[`Transaction PROFILE_ID 1`] = `undefined`; +exports[`Transaction PROFILE_INUSE_OBJECTS 1`] = `undefined`; + +exports[`Transaction PROFILE_INUSE_SPACE 1`] = `undefined`; + exports[`Transaction PROFILE_SAMPLES_COUNT 1`] = `undefined`; exports[`Transaction PROFILE_STACK 1`] = `undefined`; From 06377b33e363c7551e92f782e45971c12383084b Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Fri, 19 Feb 2021 11:04:54 +0100 Subject: [PATCH 10/10] Hidden feature flag --- x-pack/plugins/apm/common/ui_settings_keys.ts | 1 - .../app/Home/__snapshots__/Home.test.tsx.snap | 2 + .../service_details/service_detail_tabs.tsx | 46 +++++++++++++++---- .../app/service_profiling/index.tsx | 9 +++- .../service_profiling_flamegraph.tsx | 1 - .../apm_plugin/mock_apm_plugin_context.tsx | 1 + x-pack/plugins/apm/public/index.ts | 1 + x-pack/plugins/apm/server/index.ts | 2 + x-pack/plugins/apm/server/ui_settings.ts | 19 +------- 9 files changed, 50 insertions(+), 32 deletions(-) diff --git a/x-pack/plugins/apm/common/ui_settings_keys.ts b/x-pack/plugins/apm/common/ui_settings_keys.ts index 902fce2ed4df0..427c30605e71b 100644 --- a/x-pack/plugins/apm/common/ui_settings_keys.ts +++ b/x-pack/plugins/apm/common/ui_settings_keys.ts @@ -6,4 +6,3 @@ */ export const enableServiceOverview = 'apm:enableServiceOverview'; -export const enableProfiling = 'apm:enableProfiling'; diff --git a/x-pack/plugins/apm/public/components/app/Home/__snapshots__/Home.test.tsx.snap b/x-pack/plugins/apm/public/components/app/Home/__snapshots__/Home.test.tsx.snap index 82fabff610191..5094287a402ea 100644 --- a/x-pack/plugins/apm/public/components/app/Home/__snapshots__/Home.test.tsx.snap +++ b/x-pack/plugins/apm/public/components/app/Home/__snapshots__/Home.test.tsx.snap @@ -8,6 +8,7 @@ exports[`Home component should render services 1`] = ` "setHeaderActionMenu": [Function], }, "config": Object { + "profilingEnabled": false, "serviceMapEnabled": true, "ui": Object { "enabled": false, @@ -95,6 +96,7 @@ exports[`Home component should render traces 1`] = ` "setHeaderActionMenu": [Function], }, "config": Object { + "profilingEnabled": false, "serviceMapEnabled": true, "ui": Object { "enabled": false, diff --git a/x-pack/plugins/apm/public/components/app/service_details/service_detail_tabs.tsx b/x-pack/plugins/apm/public/components/app/service_details/service_detail_tabs.tsx index 046b5440fc218..5c239ef6f7665 100644 --- a/x-pack/plugins/apm/public/components/app/service_details/service_detail_tabs.tsx +++ b/x-pack/plugins/apm/public/components/app/service_details/service_detail_tabs.tsx @@ -8,11 +8,11 @@ import { EuiTab } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { ReactNode } from 'react'; +import { EuiBetaBadge } from '@elastic/eui'; +import { EuiFlexItem } from '@elastic/eui'; +import { EuiFlexGroup } from '@elastic/eui'; import { isJavaAgentName, isRumAgentName } from '../../../../common/agent_name'; -import { - enableServiceOverview, - enableProfiling, -} from '../../../../common/ui_settings_keys'; +import { enableServiceOverview } from '../../../../common/ui_settings_keys'; import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; import { useUrlParams } from '../../../context/url_params_context/use_url_params'; @@ -36,7 +36,7 @@ import { Correlations } from '../correlations'; interface Tab { key: string; href: string; - text: string; + text: ReactNode; render: () => ReactNode; } @@ -54,7 +54,10 @@ interface Props { export function ServiceDetailTabs({ serviceName, tab }: Props) { const { agentName } = useApmServiceContext(); - const { uiSettings } = useApmPluginContext().core; + const { + core: { uiSettings }, + config, + } = useApmPluginContext(); const { urlParams: { latencyAggregationType }, } = useUrlParams(); @@ -123,9 +126,32 @@ export function ServiceDetailTabs({ serviceName, tab }: Props) { const profilingTab = { key: 'profiling', href: useServiceProfilingHref({ serviceName }), - text: i18n.translate('xpack.apm.serviceDetails.profilingTabLabel', { - defaultMessage: 'Profiling', - }), + text: ( + + + {i18n.translate('xpack.apm.serviceDetails.profilingTabLabel', { + defaultMessage: 'Profiling', + })} + + + + + + ), render: () => , }; @@ -143,7 +169,7 @@ export function ServiceDetailTabs({ serviceName, tab }: Props) { tabs.push(serviceMapTab); - if (uiSettings.get(enableProfiling)) { + if (config.profilingEnabled) { tabs.push(profilingTab); } diff --git a/x-pack/plugins/apm/public/components/app/service_profiling/index.tsx b/x-pack/plugins/apm/public/components/app/service_profiling/index.tsx index 2496cbd3f2650..09a42f9b2df90 100644 --- a/x-pack/plugins/apm/public/components/app/service_profiling/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_profiling/index.tsx @@ -4,8 +4,13 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { EuiPanel } from '@elastic/eui'; -import { EuiFlexGroup, EuiFlexItem, EuiPage, EuiTitle } from '@elastic/eui'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiPage, + EuiPanel, + EuiTitle, +} from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useEffect, useState } from 'react'; import { diff --git a/x-pack/plugins/apm/public/components/app/service_profiling/service_profiling_flamegraph.tsx b/x-pack/plugins/apm/public/components/app/service_profiling/service_profiling_flamegraph.tsx index feb2302200e7e..4a35e0fbaaf66 100644 --- a/x-pack/plugins/apm/public/components/app/service_profiling/service_profiling_flamegraph.tsx +++ b/x-pack/plugins/apm/public/components/app/service_profiling/service_profiling_flamegraph.tsx @@ -326,7 +326,6 @@ export function ServiceProfilingFlamegraph({ // @ts-ignore (coming soon in Elastic charts) clip: true, }, - // @ts-expect-error (coming soon in Elastic charts) drilldown: true, fontFamily: theme.eui.euiCodeFontFamily, minFontSize: 9, diff --git a/x-pack/plugins/apm/public/context/apm_plugin/mock_apm_plugin_context.tsx b/x-pack/plugins/apm/public/context/apm_plugin/mock_apm_plugin_context.tsx index d17a4a27b646c..024deca558497 100644 --- a/x-pack/plugins/apm/public/context/apm_plugin/mock_apm_plugin_context.tsx +++ b/x-pack/plugins/apm/public/context/apm_plugin/mock_apm_plugin_context.tsx @@ -81,6 +81,7 @@ const mockConfig: ConfigSchema = { ui: { enabled: false, }, + profilingEnabled: false, }; const mockPlugin = { diff --git a/x-pack/plugins/apm/public/index.ts b/x-pack/plugins/apm/public/index.ts index 5460c6dc625a6..2734269b9cff9 100644 --- a/x-pack/plugins/apm/public/index.ts +++ b/x-pack/plugins/apm/public/index.ts @@ -13,6 +13,7 @@ import { ApmPlugin, ApmPluginSetup, ApmPluginStart } from './plugin'; export interface ConfigSchema { serviceMapEnabled: boolean; + profilingEnabled: boolean; ui: { enabled: boolean; }; diff --git a/x-pack/plugins/apm/server/index.ts b/x-pack/plugins/apm/server/index.ts index 52b5765a984d5..00910353ac278 100644 --- a/x-pack/plugins/apm/server/index.ts +++ b/x-pack/plugins/apm/server/index.ts @@ -16,6 +16,7 @@ export const config = { exposeToBrowser: { serviceMapEnabled: true, ui: true, + profilingEnabled: true, }, schema: schema.object({ enabled: schema.boolean({ defaultValue: true }), @@ -47,6 +48,7 @@ export const config = { metricsInterval: schema.number({ defaultValue: 30 }), maxServiceEnvironments: schema.number({ defaultValue: 100 }), maxServiceSelection: schema.number({ defaultValue: 50 }), + profilingEnabled: schema.boolean({ defaultValue: false }), }), }; diff --git a/x-pack/plugins/apm/server/ui_settings.ts b/x-pack/plugins/apm/server/ui_settings.ts index fd9f46f21dc00..5952cdb702295 100644 --- a/x-pack/plugins/apm/server/ui_settings.ts +++ b/x-pack/plugins/apm/server/ui_settings.ts @@ -8,10 +8,7 @@ import { schema } from '@kbn/config-schema'; import { i18n } from '@kbn/i18n'; import { UiSettingsParams } from '../../../../src/core/types'; -import { - enableServiceOverview, - enableProfiling, -} from '../common/ui_settings_keys'; +import { enableServiceOverview } from '../common/ui_settings_keys'; /** * uiSettings definitions for APM. @@ -31,18 +28,4 @@ export const uiSettings: Record> = { ), schema: schema.boolean(), }, - [enableProfiling]: { - category: ['observability'], - name: i18n.translate('xpack.apm.enableProfilingExperimentName', { - defaultMessage: 'APM Profiling', - }), - value: false, - description: i18n.translate( - 'xpack.apm.enableProfilingExperimentDescription', - { - defaultMessage: 'Enable the Profiling tab for services in APM.', - } - ), - schema: schema.boolean(), - }, };