Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
6146dab
adding profiling dependency on APM
cauemarcondes Jun 14, 2023
6e2fcef
Navigate from a service’s view into the Profiling
cauemarcondes Jun 14, 2023
c795d0b
renaming
cauemarcondes Jun 14, 2023
6895153
renaming
cauemarcondes Jun 14, 2023
54f892c
[CI] Auto-commit changed files from 'node scripts/lint_ts_projects --…
kibanamachine Jun 14, 2023
761d447
fixing section menu style
cauemarcondes Jun 14, 2023
ecdeb75
removing import from apm
cauemarcondes Jun 14, 2023
51b8bec
Merge branch 'profiling-apm-int-service-instances-table' of github.co…
cauemarcondes Jun 14, 2023
018a317
[CI] Auto-commit changed files from 'node scripts/lint_ts_projects --…
kibanamachine Jun 14, 2023
e9dc339
Merge branch 'main' of github.com:elastic/kibana into profiling-apm-i…
cauemarcondes Jun 21, 2023
84880c8
fixing CI
cauemarcondes Jun 21, 2023
5508b55
Merge branch 'main' of github.com:elastic/kibana into profiling-apm-i…
cauemarcondes Jun 21, 2023
0d5b05d
adding share dep
cauemarcondes Jun 21, 2023
5368b4a
Merge branch 'main' into profiling-apm-int-service-instances-table
cauemarcondes Jun 21, 2023
ed5de2e
Merge branch 'main' of github.com:elastic/kibana into profiling-apm-i…
cauemarcondes Jun 22, 2023
4f1d2c2
adding type to import
cauemarcondes Jun 22, 2023
5c5b105
Merge branch 'main' into profiling-apm-int-service-instances-table
cauemarcondes Jun 26, 2023
1bb2575
Merge branch 'main' of github.com:elastic/kibana into profiling-apm-i…
cauemarcondes Jun 26, 2023
28e9a6f
updatating profling linmits
cauemarcondes Jun 26, 2023
bb88a81
Merge branch 'main' of github.com:elastic/kibana into profiling-apm-i…
cauemarcondes Jun 28, 2023
b13a9f8
Merge branch 'main' into profiling-apm-int-service-instances-table
cauemarcondes Jul 3, 2023
1f1b1e5
Merge branch 'main' into profiling-apm-int-service-instances-table
cauemarcondes Jul 4, 2023
bd48a5d
Merge branch 'main' into profiling-apm-int-service-instances-table
cauemarcondes Jul 5, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/kbn-optimizer/limits.yml
Expand Up @@ -101,7 +101,7 @@ pageLoadAssetSize:
osquery: 107090
painlessLab: 179748
presentationUtil: 58834
profiling: 18628
profiling: 36694
remoteClusters: 51327
reporting: 57003
rollup: 97204
Expand Down
3 changes: 2 additions & 1 deletion x-pack/plugins/apm/kibana.jsonc
Expand Up @@ -45,7 +45,8 @@
"taskManager",
"usageCollection",
"customIntegrations", // Move this to requiredPlugins after completely migrating from the Tutorials Home App
"licenseManagement"
"licenseManagement",
"profiling"
cauemarcondes marked this conversation as resolved.
Show resolved Hide resolved
],
"requiredBundles": [
"fleet",
Expand Down
Expand Up @@ -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) {
Expand All @@ -38,13 +39,15 @@ export const getSections = ({
apmRouter,
infraLocators,
infraLinksAvailable,
profilingLocators,
}: {
transaction?: Transaction;
basePath: IBasePath;
location: Location;
apmRouter: ApmRouter;
infraLocators: InfraLocators;
infraLinksAvailable: boolean;
profilingLocators?: ProfilingLocators;
}) => {
if (!transaction) return [];
const hostName = transaction.host?.hostname;
Expand Down Expand Up @@ -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[] = [
Expand Down
Expand Up @@ -14,6 +14,7 @@ export interface Action {
href?: string;
onClick?: (event: MouseEvent) => void;
condition: boolean;
showNewBadge?: boolean;
}

interface Section {
Expand Down
Expand Up @@ -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';
Expand All @@ -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';

Expand Down Expand Up @@ -63,6 +65,9 @@ export function TransactionActionMenu({ transaction, isLoading }: Props) {

const [isActionPopoverOpen, setIsActionPopoverOpen] = useState(false);

const { isProfilingPluginInitialized, profilingLocators } =
useProfilingPlugin();

return (
<>
<ActionMenu
Expand All @@ -72,7 +77,7 @@ export function TransactionActionMenu({ transaction, isLoading }: Props) {
anchorPosition="downRight"
button={
<ActionMenuButton
isLoading={isLoading}
isLoading={isLoading || isProfilingPluginInitialized === undefined}
onClick={() =>
setIsActionPopoverOpen(
(prevIsActionPopoverOpen) => !prevIsActionPopoverOpen
Expand All @@ -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,
Expand All @@ -108,6 +122,7 @@ function ActionMenuSections({ transaction }: { transaction?: Transaction }) {
apmRouter,
infraLocators: locators,
infraLinksAvailable,
profilingLocators,
});

const externalMenuItems = useAsync(() => {
Expand Down Expand Up @@ -156,6 +171,7 @@ function ActionMenuSections({ transaction }: { transaction?: Transaction }) {
label={action.label}
href={action.href}
onClick={action.onClick}
showNewBadge={action.showNewBadge}
/>
))}
</SectionLinks>
Expand Down
35 changes: 35 additions & 0 deletions x-pack/plugins/apm/public/hooks/use_profiling_plugin.tsx
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're likely going to want this exact thing in the Hosts UI view (cc: @neptunian) -- I don't want to abstract too early so we can copy/paste at first, but eventually we should consider whether it makes sense to have an ObservabilityContext we all share/use/load. I imagine that would need to live in the observability-shared plugin (similar to my comment about locators).

@@ -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,
};
}
6 changes: 6 additions & 0 deletions x-pack/plugins/apm/public/plugin.ts
Expand Up @@ -61,6 +61,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 { registerApmRuleTypes } from './components/alerting/rule_types/register_apm_rule_types';
import {
getApmEnrollmentFlyoutData,
Expand Down Expand Up @@ -92,6 +96,7 @@ export interface ApmPluginSetupDeps {
triggersActionsUi: TriggersAndActionsUIPublicPluginSetup;
share: SharePluginSetup;
uiActions: UiActionsSetup;
profiling?: ProfilingPluginSetup;
}

export interface ApmPluginStartDeps {
Expand All @@ -117,6 +122,7 @@ export interface ApmPluginStartDeps {
storage: IStorageWrapper;
lens: LensPublicStart;
uiActions: UiActionsStart;
profiling?: ProfilingPluginStart;
}

const servicesTitle = i18n.translate('xpack.apm.navigation.servicesTitle', {
Expand Down
1 change: 1 addition & 0 deletions x-pack/plugins/apm/tsconfig.json
Expand Up @@ -92,6 +92,7 @@
"@kbn/dashboard-plugin",
"@kbn/controls-plugin",
"@kbn/core-http-server",
"@kbn/profiling-plugin",
"@kbn/unified-field-list",
"@kbn/slo-schema",
],
Expand Down
Expand Up @@ -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 (
Expand All @@ -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>
);
Expand All @@ -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>
);
}
2 changes: 1 addition & 1 deletion x-pack/plugins/profiling/common/setup.ts
Expand Up @@ -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: {
Expand Down
1 change: 1 addition & 0 deletions x-pack/plugins/profiling/kibana.jsonc
Expand Up @@ -19,6 +19,7 @@
"observability",
"observabilityShared",
"unifiedSearch",
"share"
],
"requiredBundles": [
"kibanaReact",
Expand Down
5 changes: 5 additions & 0 deletions x-pack/plugins/profiling/public/index.tsx
Expand Up @@ -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'];
30 changes: 30 additions & 0 deletions x-pack/plugins/profiling/public/locators/flamegraph_locator.ts
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like locators as an abstraction. I don't like how they introduce dependencies between observability apps (what happens if profiling wants to link to APM?).

Should we introduce a pattern for keeping locators in "observability-shared", separated by folders that could still be managed via our standard codeowners?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for raising this up. Let me see what I can move to the observability-shared.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't want to unnecessarily hold this feature up to solve this, so if we need to address this outside of this PR, that's fine. But we should talk about what this pattern should look like. APM already depends on the "infra" plugin and uses its locators (#158365) but the APM -> Infra plugin dependency causes lots of problems for us overall, imo.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My biggest worry is that the introduction of this dependency would lead us to start pulling other things in, and making it harder to disconnect that dependency in the future, which is the future I want us to aim for (no deps between obs UI plugins). I have a plan for how we might get around this, stay tuned.

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