mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[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:
parent
77498a9b69
commit
a0f8d910b7
13 changed files with 360 additions and 13 deletions
|
@ -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',
|
||||
};
|
||||
|
|
11
x-pack/plugins/profiling/.storybook/jest_setup.js
Normal file
11
x-pack/plugins/profiling/.storybook/jest_setup.js
Normal 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);
|
8
x-pack/plugins/profiling/.storybook/main.js
Normal file
8
x-pack/plugins/profiling/.storybook/main.js
Normal 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;
|
10
x-pack/plugins/profiling/.storybook/preview.js
Normal file
10
x-pack/plugins/profiling/.storybook/preview.js
Normal 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];
|
|
@ -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');
|
||||
})
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue