[Profling] Adding symbols callout on frame information window (#154478)

This PR adds a callout on the Frame information window for each symbol
status and Storybook support.

`Symbolized`:
<img width="897" alt="Screenshot 2023-04-05 at 2 46 36 PM"
src="https://user-images.githubusercontent.com/55978943/230176122-ad495e1d-76aa-431c-a6a7-4f2f319625c9.png">

`Native language`:
<img width="901" alt="Screenshot 2023-04-05 at 2 46 24 PM"
src="https://user-images.githubusercontent.com/55978943/230176224-e247d57c-538b-4c35-8a74-dd0176ac0f0c.png">

`Interpreted language`:
<img width="893" alt="Screenshot 2023-04-05 at 2 46 16 PM"
src="https://user-images.githubusercontent.com/55978943/230176264-d2d9b72a-6048-4ba3-93af-b60f9ea04001.png">

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Cauê Marcondes 2023-04-10 13:11:04 -04:00 committed by GitHub
parent 77498a9b69
commit a0f8d910b7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 360 additions and 13 deletions

View file

@ -50,4 +50,5 @@ export const storybookAliases = {
triggers_actions_ui: 'x-pack/plugins/triggers_actions_ui/.storybook',
ui_actions_enhanced: 'src/plugins/ui_actions_enhanced/.storybook',
unified_search: 'src/plugins/unified_search/.storybook',
profiling: 'x-pack/plugins/profiling/.storybook',
};

View file

@ -0,0 +1,11 @@
/*
* 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 { setGlobalConfig } from '@storybook/testing-react';
import * as globalStorybookConfig from './preview';
setGlobalConfig(globalStorybookConfig);

View file

@ -0,0 +1,8 @@
/*
* 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.
*/
module.exports = require('@kbn/storybook').defaultConfig;

View file

@ -0,0 +1,10 @@
/*
* 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 { EuiThemeProviderDecorator } from '@kbn/kibana-react-plugin/common';
export const decorators = [EuiThemeProviderDecorator];

View file

@ -8,11 +8,14 @@
import {
createStackFrameID,
createStackFrameMetadata,
FrameSymbolStatus,
FrameType,
getAddressFromStackFrameID,
getCalleeFunction,
getCalleeSource,
getFileIDFromStackFrameID,
getFrameSymbolStatus,
getLanguageType,
} from './profiling';
describe('Stack frame operations', () => {
@ -88,3 +91,43 @@ describe('Stack frame metadata operations', () => {
expect(getCalleeSource(metadata)).toEqual('runtime/malloc.go');
});
});
describe('getFrameSymbolStatus', () => {
it('returns partially symbolized when metadata has executable name but no source name and source line', () => {
expect(getFrameSymbolStatus({ sourceFilename: '', sourceLine: 0, exeFileName: 'foo' })).toEqual(
FrameSymbolStatus.PARTIALLY_SYMBOLYZED
);
});
it('returns not symbolized when metadata has no source name and source line and executable name', () => {
expect(getFrameSymbolStatus({ sourceFilename: '', sourceLine: 0 })).toEqual(
FrameSymbolStatus.NOT_SYMBOLIZED
);
});
it('returns symbolized when metadata has source name and source line', () => {
expect(getFrameSymbolStatus({ sourceFilename: 'foo', sourceLine: 10 })).toEqual(
FrameSymbolStatus.SYMBOLIZED
);
});
});
describe('getLanguageType', () => {
[FrameType.Native, FrameType.Kernel].map((type) =>
it(`returns native for ${type}`, () => {
expect(getLanguageType({ frameType: type })).toEqual('NATIVE');
})
);
[
FrameType.JVM,
FrameType.JavaScript,
FrameType.PHP,
FrameType.PHPJIT,
FrameType.Perl,
FrameType.Python,
FrameType.Ruby,
].map((type) =>
it(`returns interpreted for ${type}`, () => {
expect(getLanguageType({ frameType: type })).toEqual('INTERPRETED');
})
);
});

View file

@ -234,23 +234,56 @@ export function getCalleeFunction(frame: StackFrameMetadata): string {
// When there is no function name, only use the executable name
return frame.FunctionName ? exeDisplayName + ': ' + frame.FunctionName : exeDisplayName;
}
export enum FrameSymbolStatus {
PARTIALLY_SYMBOLYZED = 'PARTIALLY_SYMBOLYZED',
NOT_SYMBOLIZED = 'NOT_SYMBOLIZED',
SYMBOLIZED = 'SYMBOLIZED',
}
export function getFrameSymbolStatus({
sourceFilename,
sourceLine,
exeFileName,
}: {
sourceFilename: string;
sourceLine: number;
exeFileName?: string;
}) {
if (sourceFilename === '' && sourceLine === 0) {
if (exeFileName) {
return FrameSymbolStatus.PARTIALLY_SYMBOLYZED;
}
return FrameSymbolStatus.NOT_SYMBOLIZED;
}
return FrameSymbolStatus.SYMBOLIZED;
}
const nativeLanguages = [FrameType.Native, FrameType.Kernel];
export function getLanguageType({ frameType }: { frameType: FrameType }) {
return nativeLanguages.includes(frameType) ? 'NATIVE' : 'INTERPRETED';
}
export function getCalleeSource(frame: StackFrameMetadata): string {
if (frame.SourceFilename === '' && frame.SourceLine === 0) {
if (frame.ExeFileName) {
const frameSymbolStatus = getFrameSymbolStatus({
sourceFilename: frame.SourceFilename,
sourceLine: frame.SourceLine,
exeFileName: frame.ExeFileName,
});
switch (frameSymbolStatus) {
case FrameSymbolStatus.NOT_SYMBOLIZED: {
// If we don't have the executable filename, display <unsymbolized>
return '<unsymbolized>';
}
case FrameSymbolStatus.PARTIALLY_SYMBOLYZED: {
// If no source line or filename available, display the executable offset
return frame.ExeFileName + '+0x' + frame.AddressOrLine.toString(16);
}
// If we don't have the executable filename, display <unsymbolized>
return '<unsymbolized>';
case FrameSymbolStatus.SYMBOLIZED: {
return frame.SourceFilename + (frame.SourceLine !== 0 ? `#${frame.SourceLine}` : '');
}
}
if (frame.SourceFilename !== '' && frame.SourceLine === 0) {
return frame.SourceFilename;
}
return frame.SourceFilename + (frame.SourceLine !== 0 ? `#${frame.SourceLine}` : '');
}
export function groupStackFrameMetadataByStackTrace(

View file

@ -0,0 +1,93 @@
/*
* 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 { CoreStart } from '@kbn/core/public';
import { EuiThemeProvider } from '@kbn/kibana-react-plugin/common';
import { createKibanaReactContext } from '@kbn/kibana-react-plugin/public';
import { MlLocatorDefinition } from '@kbn/ml-plugin/public';
import { UrlService } from '@kbn/share-plugin/common/url_service';
import React, { ReactNode } from 'react';
import { Observable } from 'rxjs';
import { ProfilingDependenciesContextProvider } from './profiling_dependencies_context';
const urlService = new UrlService({
navigate: async () => {},
getUrl: async ({ app, path }, { absolute }) => {
return `${absolute ? 'http://localhost:8888' : ''}/app/${app}${path}`;
},
shortUrls: () => ({ get: () => {} } as any),
});
const locator = urlService.locators.create(new MlLocatorDefinition());
const mockPlugin = {
ml: {
locator,
},
data: {
query: {
timefilter: { timefilter: { setTime: () => {}, getTime: () => ({}) } },
},
},
};
const mockCore = {
application: {
currentAppId$: new Observable(),
getUrlForApp: (appId: string) => '',
navigateToUrl: (url: string) => {},
},
chrome: {
docTitle: { change: () => {} },
setBreadcrumbs: () => {},
setHelpExtension: () => {},
setBadge: () => {},
},
docLinks: {
DOC_LINK_VERSION: 'current',
ELASTIC_WEBSITE_URL: 'https://www.elastic.co/',
links: { observability: { guide: '' } },
},
http: {
basePath: {
prepend: (path: string) => `/basepath${path}`,
get: () => '/basepath',
},
},
i18n: {
Context: ({ children }: { children: ReactNode }) => children,
},
notifications: {
toasts: {
addWarning: () => {},
addDanger: () => {},
add: () => {},
},
},
};
const mockProfilingDependenciesContext = {
core: mockCore,
plugins: mockPlugin,
} as any;
export function MockProfilingDependenciesStorybook({ children }: { children?: ReactNode }) {
const KibanaReactContext = createKibanaReactContext(
mockProfilingDependenciesContext.core as unknown as Partial<CoreStart>
);
return (
<EuiThemeProvider darkMode={false}>
<KibanaReactContext.Provider>
<ProfilingDependenciesContextProvider
// We should keep adding more stuff to the mock object as we need
value={{ start: mockProfilingDependenciesContext, setup: {} as any, services: {} as any }}
>
{children}
</ProfilingDependenciesContextProvider>
</KibanaReactContext.Provider>
</EuiThemeProvider>
);
}

View file

@ -14,7 +14,7 @@ interface Props extends FrameInformationWindowProps {
export function FrameInformationTooltip({ onClose, ...props }: Props) {
return (
<EuiFlyout onClose={onClose} size="s">
<EuiFlyout onClose={onClose} size="m">
<EuiFlyoutBody>
<FrameInformationWindow {...props} />
</EuiFlyoutBody>

View file

@ -7,10 +7,12 @@
import { EuiFlexGroup, EuiFlexItem, EuiText, EuiTitle } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { FrameSymbolStatus, getFrameSymbolStatus } from '../../../common/profiling';
import { FrameInformationPanel } from './frame_information_panel';
import { getImpactRows } from './get_impact_rows';
import { getInformationRows } from './get_information_rows';
import { KeyValueList } from './key_value_list';
import { MissingSymbolsCallout } from './missing_symbols_callout';
export interface Props {
frame?: {
@ -41,6 +43,12 @@ export function FrameInformationWindow({ frame, totalSamples, totalSeconds }: Pr
);
}
const symbolStatus = getFrameSymbolStatus({
sourceFilename: frame.sourceFileName,
sourceLine: frame.sourceLine,
exeFileName: frame.exeFileName,
});
const {
fileID,
frameType,
@ -76,6 +84,11 @@ export function FrameInformationWindow({ frame, totalSamples, totalSeconds }: Pr
<EuiFlexItem>
<KeyValueList rows={informationRows} />
</EuiFlexItem>
{symbolStatus !== FrameSymbolStatus.SYMBOLIZED && (
<EuiFlexItem>
<MissingSymbolsCallout frameType={frame.frameType} />
</EuiFlexItem>
)}
<EuiFlexItem>
<EuiFlexGroup direction="column">
<EuiFlexItem>

View file

@ -0,0 +1,42 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { Meta } from '@storybook/react';
import React from 'react';
import { FrameType } from '../../../common/profiling';
import { MockProfilingDependenciesStorybook } from '../contexts/profiling_dependencies/mock_profiling_dependencies_storybook';
import { MissingSymbolsCallout } from './missing_symbols_callout';
const stories: Meta<{}> = {
title: 'shared/Frame information window/Missing symbols',
component: MissingSymbolsCallout,
decorators: [
(StoryComponent, { globals }) => {
return (
<MockProfilingDependenciesStorybook>
<StoryComponent />
</MockProfilingDependenciesStorybook>
);
},
],
};
export default stories;
export function Examples() {
return (
<EuiFlexGroup direction="column">
<EuiFlexItem>
<MissingSymbolsCallout frameType={FrameType.Native} />
</EuiFlexItem>
<EuiFlexItem>
<MissingSymbolsCallout frameType={FrameType.JVM} />
</EuiFlexItem>
</EuiFlexGroup>
);
}

View file

@ -0,0 +1,90 @@
/*
* 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 { EuiButton, EuiCallOut, EuiLink } from '@elastic/eui';
import React from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import { FrameType, getLanguageType } from '../../../common/profiling';
import { PROFILING_FEEDBACK_LINK } from '../profiling_app_page_template';
import { useProfilingDependencies } from '../contexts/profiling_dependencies/use_profiling_dependencies';
interface Props {
frameType: FrameType;
}
export function MissingSymbolsCallout({ frameType }: Props) {
const languageType = getLanguageType({ frameType });
const { docLinks } = useProfilingDependencies().start.core;
if (languageType === 'NATIVE') {
return (
<EuiCallOut
title={i18n.translate(
'xpack.profiling.frameInformationWindow.missingSymbols.native.title',
{ defaultMessage: 'Missing symbols' }
)}
color="warning"
iconType="help"
>
<p>
<FormattedMessage
id="xpack.profiling.frameInformationWindow.missingSymbols.native"
defaultMessage="To see function names and line numbers in traces of applications written in programming languages that compile to native code (C, C++, Rust, Go, etc.), you need to push symbols to the cluster using the elastic-profiling binary. {readMore}, or download the binary below."
values={{
readMore: (
<EuiLink
href={`${docLinks.ELASTIC_WEBSITE_URL}/guide/en/observability/${docLinks.DOC_LINK_VERSION}/profiling-add-symbols.html`}
target="_blank"
>
{i18n.translate(
'xpack.profiling.frameInformationWindow.missingSymbols.native.readMore',
{ defaultMessage: 'Read more' }
)}
</EuiLink>
),
}}
/>
</p>
<EuiButton
href="https://container-library.elastic.co/r/observability/profiling-agent"
target="_blank"
color="warning"
>
{i18n.translate(
'xpack.profiling.frameInformationWindow.missingSymbols.native.downloadBinary',
{ defaultMessage: 'Download elastic-profiling binary' }
)}
</EuiButton>
</EuiCallOut>
);
}
return (
<EuiCallOut
title={i18n.translate(
'xpack.profiling.frameInformationWindow.missingSymbols.interpreted.title',
{ defaultMessage: 'Missing symbols error' }
)}
color="warning"
iconType="help"
>
<p>
{i18n.translate('xpack.profiling.frameInformationWindow.missingSymbols.interpreted', {
defaultMessage:
'Symbols are not available because of an error in the unwinder for this language or an unknown error with the interpreter.',
})}
</p>
<EuiButton href={PROFILING_FEEDBACK_LINK} target="_blank" color="warning">
{i18n.translate(
'xpack.profiling.frameInformationWindow.missingSymbols.interpreted.reportProblem',
{ defaultMessage: 'Report a problem' }
)}
</EuiButton>
</EuiCallOut>
);
}

View file

@ -19,7 +19,7 @@ import { NoDataPageProps } from '@kbn/shared-ux-page-no-data-types';
import { useProfilingDependencies } from '../contexts/profiling_dependencies/use_profiling_dependencies';
import { PrimaryProfilingSearchBar } from './primary_profiling_search_bar';
const PROFILING_FEEDBACK_LINK = 'https://ela.st/profiling-feedback';
export const PROFILING_FEEDBACK_LINK = 'https://ela.st/profiling-feedback';
export function ProfilingAppPageTemplate({
children,

View file

@ -41,6 +41,9 @@
"@kbn/spaces-plugin",
"@kbn/cloud-plugin",
"@kbn/shared-ux-prompt-not-found",
"@kbn/i18n-react",
"@kbn/ml-plugin",
"@kbn/share-plugin",
// add references to other TypeScript projects the plugin depends on
// requiredPlugins from ./kibana.json