[Integrate Profiling with APM] Navigate from the transaction details view into the Profiling (#159686)

- The new profiling items will be only visible when the profiling plugin
has been already installed. Otherwise, these are going to be hidden.
- The profiling plugin exposes three new Locators to facilitate the
navigation to the Flamegraph, TopN functions and Stacktraces pages.
- Add `new` badge property on the section component

<img width="486" alt="Screenshot 2023-06-14 at 1 55 09 PM"
src="6e353bfc-050c-4294-a4e4-fc46205d5d0e">



234863a4-0d99-4140-a5b5-702896b2c4a3


ee1635bd-5127-41d6-b434-4cee9b5ebe92


46ec9bb7-2cd0-43fc-9a1e-0d6eef70612f

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Cauê Marcondes 2023-07-06 14:00:50 -03:00 committed by GitHub
parent b22dd68d39
commit d118fb4ba4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 267 additions and 15 deletions

View file

@ -102,7 +102,7 @@ pageLoadAssetSize:
osquery: 107090
painlessLab: 179748
presentationUtil: 58834
profiling: 18628
profiling: 36694
remoteClusters: 51327
reporting: 57003
rollup: 97204

View file

@ -46,7 +46,8 @@
"taskManager",
"usageCollection",
"customIntegrations", // Move this to requiredPlugins after completely migrating from the Tutorials Home App
"licenseManagement"
"licenseManagement",
"profiling"
],
"requiredBundles": [
"advancedSettings",

View file

@ -12,13 +12,14 @@ import { isEmpty, pickBy } from 'lodash';
import moment from 'moment';
import url from 'url';
import type { InfraLocators } from '@kbn/infra-plugin/common/locators';
import type { ProfilingLocators } from '@kbn/profiling-plugin/public';
import type { Transaction } from '../../../../typings/es_schemas/ui/transaction';
import { getDiscoverHref } from '../links/discover_links/discover_link';
import { getDiscoverQuery } from '../links/discover_links/discover_transaction_link';
import { getInfraHref } from '../links/infra_link';
import { fromQuery } from '../links/url_helpers';
import { SectionRecord, getNonEmptySections, Action } from './sections_helper';
import { TRACE_ID } from '../../../../common/es_fields/apm';
import { HOST_NAME, TRACE_ID } from '../../../../common/es_fields/apm';
import { ApmRouter } from '../../routing/apm_route_config';
function getInfraMetricsQuery(transaction: Transaction) {
@ -38,6 +39,7 @@ export const getSections = ({
apmRouter,
infraLocators,
infraLinksAvailable,
profilingLocators,
}: {
transaction?: Transaction;
basePath: IBasePath;
@ -45,6 +47,7 @@ export const getSections = ({
apmRouter: ApmRouter;
infraLocators: InfraLocators;
infraLinksAvailable: boolean;
profilingLocators?: ProfilingLocators;
}) => {
if (!transaction) return [];
const hostName = transaction.host?.hostname;
@ -163,6 +166,42 @@ export const getSections = ({
}),
condition: !!hostName,
},
{
key: 'hostProfilingFlamegraph',
label: i18n.translate(
'xpack.apm.transactionActionMenu.showHostProfilingFlamegraphLinkLabel',
{ defaultMessage: 'Host flamegraph' }
),
href: profilingLocators?.flamegraphLocator.getRedirectUrl({
kuery: `${HOST_NAME}: "${hostName}"`,
}),
condition: !!hostName && !!profilingLocators,
showNewBadge: true,
},
{
key: 'hostProfilingTopNFunctions',
label: i18n.translate(
'xpack.apm.transactionActionMenu.showHostProfilingTopNFunctionsLinkLabel',
{ defaultMessage: 'Host topN functions' }
),
href: profilingLocators?.topNFunctionsLocator.getRedirectUrl({
kuery: `${HOST_NAME}: "${hostName}"`,
}),
condition: !!hostName && !!profilingLocators,
showNewBadge: true,
},
{
key: 'hostProfilingStacktraces',
label: i18n.translate(
'xpack.apm.transactionActionMenu.showHostProfilingStacktracesLinkLabel',
{ defaultMessage: 'Host stacktraces' }
),
href: profilingLocators?.stacktracesLocator.getRedirectUrl({
kuery: `${HOST_NAME}: "${hostName}"`,
}),
condition: !!hostName && !!profilingLocators,
showNewBadge: true,
},
];
const logActions: Action[] = [

View file

@ -14,6 +14,7 @@ export interface Action {
href?: string;
onClick?: (event: MouseEvent) => void;
condition: boolean;
showNewBadge?: boolean;
}
interface Section {

View file

@ -7,17 +7,18 @@
import { EuiButton } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { ObservabilityTriggerId } from '@kbn/observability-shared-plugin/common';
import {
ActionMenu,
ActionMenuDivider,
getContextMenuItemsFromActions,
Section,
SectionLink,
SectionLinks,
SectionSubtitle,
SectionTitle,
} from '@kbn/observability-shared-plugin/public';
import { ObservabilityTriggerId } from '@kbn/observability-shared-plugin/common';
import { getContextMenuItemsFromActions } from '@kbn/observability-shared-plugin/public';
import { ProfilingLocators } from '@kbn/profiling-plugin/public';
import React, { useState } from 'react';
import { useLocation } from 'react-router-dom';
import useAsync from 'react-use/lib/useAsync';
@ -27,6 +28,7 @@ import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_
import { useLicenseContext } from '../../../context/license/use_license_context';
import { useApmFeatureFlag } from '../../../hooks/use_apm_feature_flag';
import { useApmRouter } from '../../../hooks/use_apm_router';
import { useProfilingPlugin } from '../../../hooks/use_profiling_plugin';
import { CustomLinkMenuSection } from './custom_link_menu_section';
import { getSections } from './sections';
@ -63,6 +65,9 @@ export function TransactionActionMenu({ transaction, isLoading }: Props) {
const [isActionPopoverOpen, setIsActionPopoverOpen] = useState(false);
const { isProfilingPluginInitialized, profilingLocators } =
useProfilingPlugin();
return (
<>
<ActionMenu
@ -72,7 +77,7 @@ export function TransactionActionMenu({ transaction, isLoading }: Props) {
anchorPosition="downRight"
button={
<ActionMenuButton
isLoading={isLoading}
isLoading={isLoading || isProfilingPluginInitialized === undefined}
onClick={() =>
setIsActionPopoverOpen(
(prevIsActionPopoverOpen) => !prevIsActionPopoverOpen
@ -81,14 +86,23 @@ export function TransactionActionMenu({ transaction, isLoading }: Props) {
/>
}
>
<ActionMenuSections transaction={transaction} />
<ActionMenuSections
transaction={transaction}
profilingLocators={profilingLocators}
/>
{hasGoldLicense && <CustomLinkMenuSection transaction={transaction} />}
</ActionMenu>
</>
);
}
function ActionMenuSections({ transaction }: { transaction?: Transaction }) {
function ActionMenuSections({
transaction,
profilingLocators,
}: {
transaction?: Transaction;
profilingLocators?: ProfilingLocators;
}) {
const {
core,
uiActions,
@ -108,6 +122,7 @@ function ActionMenuSections({ transaction }: { transaction?: Transaction }) {
apmRouter,
infraLocators: locators,
infraLinksAvailable,
profilingLocators,
});
const externalMenuItems = useAsync(() => {
@ -156,6 +171,7 @@ function ActionMenuSections({ transaction }: { transaction?: Transaction }) {
label={action.label}
href={action.href}
onClick={action.onClick}
showNewBadge={action.showNewBadge}
/>
))}
</SectionLinks>

View file

@ -0,0 +1,35 @@
/*
* 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 { useEffect, useState } from 'react';
import { useApmPluginContext } from '../context/apm_plugin/use_apm_plugin_context';
export function useProfilingPlugin() {
const { plugins } = useApmPluginContext();
const [isProfilingPluginInitialized, setIsProfilingPluginInitialized] =
useState<boolean | undefined>();
useEffect(() => {
async function fetchIsProfilingSetup() {
if (!plugins.profiling) {
setIsProfilingPluginInitialized(false);
return;
}
const resp = await plugins.profiling.hasSetup();
setIsProfilingPluginInitialized(resp);
}
fetchIsProfilingSetup();
}, [plugins.profiling]);
return {
isProfilingPluginInitialized,
profilingLocators: isProfilingPluginInitialized
? plugins.profiling?.locators
: undefined,
};
}

View file

@ -62,6 +62,10 @@ import { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
import { UiActionsStart, UiActionsSetup } from '@kbn/ui-actions-plugin/public';
import { ObservabilityTriggerId } from '@kbn/observability-shared-plugin/common';
import { LicenseManagementUIPluginSetup } from '@kbn/license-management-plugin/public';
import {
ProfilingPluginSetup,
ProfilingPluginStart,
} from '@kbn/profiling-plugin/public';
import {
DiscoverStart,
DiscoverSetup,
@ -98,6 +102,7 @@ export interface ApmPluginSetupDeps {
triggersActionsUi: TriggersAndActionsUIPublicPluginSetup;
share: SharePluginSetup;
uiActions: UiActionsSetup;
profiling?: ProfilingPluginSetup;
}
export interface ApmPluginStartDeps {
@ -124,6 +129,7 @@ export interface ApmPluginStartDeps {
storage: IStorageWrapper;
lens: LensPublicStart;
uiActions: UiActionsStart;
profiling?: ProfilingPluginStart;
}
const servicesTitle = i18n.translate('xpack.apm.navigation.servicesTitle', {

View file

@ -92,6 +92,7 @@
"@kbn/dashboard-plugin",
"@kbn/controls-plugin",
"@kbn/core-http-server",
"@kbn/profiling-plugin",
"@kbn/logs-shared-plugin",
"@kbn/unified-field-list",
"@kbn/slo-schema",

View file

@ -11,10 +11,14 @@ import {
EuiSpacer,
EuiListGroupItem,
EuiListGroupItemProps,
EuiFlexGroup,
EuiFlexItem,
EuiBadge,
} from '@elastic/eui';
import React, { ReactNode } from 'react';
import styled from 'styled-components';
import { EuiListGroupProps } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
export function SectionTitle({ children }: { children?: ReactNode }) {
return (
@ -40,7 +44,7 @@ export function SectionSubtitle({ children }: { children?: ReactNode }) {
export function SectionLinks({ children, ...props }: { children?: ReactNode } & EuiListGroupProps) {
return (
<EuiListGroup {...props} flush={true} bordered={false}>
<EuiListGroup {...props} size={'s'} color={'primary'} flush={true} bordered={false}>
{children}
</EuiListGroup>
);
@ -58,6 +62,24 @@ export const Section = styled.div`
`;
export type SectionLinkProps = EuiListGroupItemProps;
export function SectionLink(props: SectionLinkProps) {
return <EuiListGroupItem style={{ padding: 0 }} size={'xs'} {...props} />;
export function SectionLink({
showNewBadge,
...props
}: SectionLinkProps & { showNewBadge?: boolean }) {
return (
<EuiFlexGroup gutterSize="none">
<EuiFlexItem>
<EuiListGroupItem style={{ padding: 0 }} size={'xs'} {...props} />
</EuiFlexItem>
{showNewBadge && (
<EuiFlexItem grow={false} style={{ justifyContent: 'center' }}>
<EuiBadge color="accent">
{i18n.translate('xpack.observabilityShared.sectionLink.newLabel', {
defaultMessage: 'New',
})}
</EuiBadge>
</EuiFlexItem>
)}
</EuiFlexGroup>
);
}

View file

@ -6,7 +6,7 @@
*/
import { merge } from 'lodash';
import { RecursivePartial } from '@kbn/apm-plugin/typings/common';
import type { RecursivePartial } from '@elastic/eui';
export interface SetupState {
cloud: {

View file

@ -19,6 +19,7 @@
"observability",
"observabilityShared",
"unifiedSearch",
"share"
],
"requiredBundles": [
"kibanaReact",

View file

@ -6,7 +6,12 @@
*/
import { ProfilingPlugin } from './plugin';
import type { ProfilingPluginSetup, ProfilingPluginStart } from './plugin';
export function plugin() {
return new ProfilingPlugin();
}
export type { ProfilingPluginSetup, ProfilingPluginStart };
export type ProfilingLocators = ProfilingPluginSetup['locators'];

View file

@ -0,0 +1,30 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import qs from 'query-string';
import type { SerializableRecord } from '@kbn/utility-types';
import type { LocatorDefinition, LocatorPublic } from '@kbn/share-plugin/public';
export interface FlamegraphLocatorParams extends SerializableRecord {
kuery?: string;
rangeFrom?: string;
rangeTo?: string;
}
export type FlamegraphLocator = LocatorPublic<FlamegraphLocatorParams>;
export class FlamegraphLocatorDefinition implements LocatorDefinition<FlamegraphLocatorParams> {
public readonly id = 'flamegraphLocator';
public readonly getLocation = async ({ rangeFrom, rangeTo, kuery }: FlamegraphLocatorParams) => {
const params = { rangeFrom, rangeTo, kuery };
return {
app: 'profiling',
path: `/flamegraphs/flamegraph?${qs.stringify(params)}`,
state: {},
};
};
}

View file

@ -0,0 +1,30 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import qs from 'query-string';
import type { SerializableRecord } from '@kbn/utility-types';
import type { LocatorDefinition, LocatorPublic } from '@kbn/share-plugin/public';
export interface StacktracesLocatorParams extends SerializableRecord {
kuery?: string;
rangeFrom?: string;
rangeTo?: string;
}
export type StacktracesLocator = LocatorPublic<StacktracesLocatorParams>;
export class StacktracesLocatorDefinition implements LocatorDefinition<StacktracesLocatorParams> {
public readonly id = 'stacktracesLocator';
public readonly getLocation = async ({ rangeFrom, rangeTo, kuery }: StacktracesLocatorParams) => {
const params = { rangeFrom, rangeTo, kuery };
return {
app: 'profiling',
path: `/stacktraces/threads?${qs.stringify(params)}`,
state: {},
};
};
}

View file

@ -0,0 +1,36 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import qs from 'query-string';
import type { SerializableRecord } from '@kbn/utility-types';
import type { LocatorDefinition, LocatorPublic } from '@kbn/share-plugin/public';
export interface TopNFunctionsLocatorParams extends SerializableRecord {
kuery?: string;
rangeFrom?: string;
rangeTo?: string;
}
export type TopNFunctionsLocator = LocatorPublic<TopNFunctionsLocatorParams>;
export class TopNFunctionsLocatorDefinition
implements LocatorDefinition<TopNFunctionsLocatorParams>
{
public readonly id = 'topNFunctionsLocator';
public readonly getLocation = async ({
rangeFrom,
rangeTo,
kuery,
}: TopNFunctionsLocatorParams) => {
const params = { rangeFrom, rangeTo, kuery };
return {
app: 'profiling',
path: `/functions/topn?${qs.stringify(params)}`,
state: {},
};
};
}

View file

@ -14,15 +14,20 @@ import {
} from '@kbn/core/public';
import { i18n } from '@kbn/i18n';
import type { NavigationSection } from '@kbn/observability-shared-plugin/public';
import { Location } from 'history';
import type { Location } from 'history';
import { BehaviorSubject, combineLatest, from, map } from 'rxjs';
import { FlamegraphLocatorDefinition } from './locators/flamegraph_locator';
import { StacktracesLocatorDefinition } from './locators/stacktraces_locator';
import { TopNFunctionsLocatorDefinition } from './locators/topn_functions_locator';
import { getServices } from './services';
import type { ProfilingPluginPublicSetupDeps, ProfilingPluginPublicStartDeps } from './types';
export type ProfilingPluginSetup = ReturnType<ProfilingPlugin['setup']>;
export type ProfilingPluginStart = void;
export class ProfilingPlugin implements Plugin {
public setup(coreSetup: CoreSetup, pluginsSetup: ProfilingPluginPublicSetupDeps) {
// Register an application into the side navigation menu
const links = [
{
id: 'stacktraces',
@ -125,6 +130,27 @@ export class ProfilingPlugin implements Plugin {
};
},
});
return {
locators: {
flamegraphLocator: pluginsSetup.share.url.locators.create(
new FlamegraphLocatorDefinition()
),
topNFunctionsLocator: pluginsSetup.share.url.locators.create(
new TopNFunctionsLocatorDefinition()
),
stacktracesLocator: pluginsSetup.share.url.locators.create(
new StacktracesLocatorDefinition()
),
},
hasSetup: async () => {
const response = (await coreSetup.http.get('/api/profiling/v1/setup/es_resources')) as {
has_setup: boolean;
has_data: boolean;
};
return response.has_setup;
},
};
}
public start(core: CoreStart) {

View file

@ -19,6 +19,7 @@ import {
} from '@kbn/observability-shared-plugin/public/plugin';
import { ChartsPluginSetup, ChartsPluginStart } from '@kbn/charts-plugin/public';
import { LicensingPluginSetup } from '@kbn/licensing-plugin/public';
import type { SharePluginSetup, SharePluginStart } from '@kbn/share-plugin/public';
export interface ProfilingPluginPublicSetupDeps {
observability: ObservabilityPublicSetup;
@ -27,6 +28,7 @@ export interface ProfilingPluginPublicSetupDeps {
data: DataPublicPluginSetup;
charts: ChartsPluginSetup;
licensing: LicensingPluginSetup;
share: SharePluginSetup;
}
export interface ProfilingPluginPublicStartDeps {
@ -35,4 +37,5 @@ export interface ProfilingPluginPublicStartDeps {
dataViews: DataViewsPublicPluginStart;
data: DataPublicPluginStart;
charts: ChartsPluginStart;
share: SharePluginStart;
}

View file

@ -45,7 +45,7 @@
"@kbn/share-plugin",
"@kbn/observability-shared-plugin",
"@kbn/licensing-plugin",
"@kbn/apm-plugin",
"@kbn/utility-types",
// add references to other TypeScript projects the plugin depends on
// requiredPlugins from ./kibana.json