[Profiling-APM] Service Profiling flamegraph (#165360)

- Move files from profiling-data-access-plugin to a new Kibana pkg
@kbn/profiling-utils
- Create a Profling flamegraph embeddable component in the Profiling
plugin
- Create a Profiling flamegraph embeddable client in the
Observability-shared plugin
- Create a Profiling tab in APM (it's only visible when kibana setting
is enabled and Profiling has been initialized)
- This PR has not yet removed the Profiling dependency from the APM
plugin. For that, I need to refactor some parts on Profiling side and
move some logic to the data access plugin. This will be done on another
PR.

**How plugins can use the Profiling Flamegraph**

1. Call
[profilingDataAccessStart.services.fetchFlamechartData](https://github.com/elastic/kibana/blob/main/x-pack/plugins/profiling_data_access/server/services/fetch_flamechart/index.ts#L22),
it returns an
[ElasticFlameGraph](https://github.com/elastic/kibana/blob/main/x-pack/plugins/profiling_data_access/common/flamegraph.ts#L74).
2. Render the
[EmbeddableFlamegraph](https://github.com/elastic/kibana/pull/165360/files#diff-fb9763ef775d15950acb682abf7447259c3feae74fab413d4e1a14fdcc401351R21)
component passing the data received.



2aa3d1b6-3649-4e58-a088-11890a09feec

---


<img width="885" alt="Screenshot 2023-09-05 at 09 41 11"
src="dc65f870-c4f6-4654-8cdd-8e2cd8e97b00">

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Joseph Crail <joseph.crail@elastic.co>
This commit is contained in:
Cauê Marcondes 2023-09-12 09:19:31 +01:00 committed by GitHub
parent 6cb937a37a
commit a9e882d18b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
100 changed files with 1283 additions and 361 deletions

1
.github/CODEOWNERS vendored
View file

@ -553,6 +553,7 @@ examples/preboot_example @elastic/kibana-security @elastic/kibana-core
src/plugins/presentation_util @elastic/kibana-presentation
x-pack/plugins/profiling_data_access @elastic/profiling-ui
x-pack/plugins/profiling @elastic/profiling-ui
packages/kbn-profiling-utils @elastic/profiling-ui
x-pack/packages/kbn-random-sampling @elastic/kibana-visualizations
packages/kbn-react-field @elastic/kibana-data-discovery
packages/react/kibana_context/common @elastic/appex-sharedux

View file

@ -564,6 +564,7 @@
"@kbn/presentation-util-plugin": "link:src/plugins/presentation_util",
"@kbn/profiling-data-access-plugin": "link:x-pack/plugins/profiling_data_access",
"@kbn/profiling-plugin": "link:x-pack/plugins/profiling",
"@kbn/profiling-utils": "link:packages/kbn-profiling-utils",
"@kbn/random-sampling": "link:x-pack/packages/kbn-random-sampling",
"@kbn/react-field": "link:packages/kbn-react-field",
"@kbn/react-kibana-context-common": "link:packages/react/kibana_context/common",

View file

@ -0,0 +1,25 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { StackTraceResponse } from '../stack_traces';
import stackTraces1x from './stacktraces_60s_1x.json';
import stackTraces5x from './stacktraces_3600s_5x.json';
import stackTraces125x from './stacktraces_86400s_125x.json';
import stackTraces625x from './stacktraces_604800s_625x.json';
export const stackTraceFixtures: Array<{
response: StackTraceResponse;
seconds: number;
upsampledBy: number;
}> = [
{ response: stackTraces1x, seconds: 60, upsampledBy: 1 },
{ response: stackTraces5x, seconds: 3600, upsampledBy: 5 },
{ response: stackTraces125x, seconds: 86400, upsampledBy: 125 },
{ response: stackTraces625x, seconds: 604800, upsampledBy: 625 },
];

View file

@ -1,15 +1,15 @@
/*
* 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.
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { sum } from 'lodash';
import { createCalleeTree } from './callee';
import { decodeStackTraceResponse } from './stack_traces';
import { stackTraceFixtures } from './__fixtures__/stacktraces';
describe('Callee operations', () => {

View file

@ -1,8 +1,9 @@
/*
* 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.
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { createFrameGroupID, FrameGroupID } from './frame_group';
@ -20,24 +21,48 @@ import {
type NodeID = number;
/**
* Callee tree
*/
export interface CalleeTree {
/** size */
Size: number;
/** edges */
Edges: Array<Map<FrameGroupID, NodeID>>;
/** file ids */
FileID: string[];
/** frame types */
FrameType: number[];
/** inlines */
Inline: boolean[];
/** executable file names */
ExeFilename: string[];
/** address or lines */
AddressOrLine: number[];
/** function names */
FunctionName: string[];
/** function offsets */
FunctionOffset: number[];
/** source file names */
SourceFilename: string[];
/** source lines */
SourceLine: number[];
/** total cpu */
CountInclusive: number[];
/** self cpu */
CountExclusive: number[];
}
/**
* Create a callee tree
* @param events Map<StackTraceID, number>
* @param stackTraces Map<StackTraceID, StackTrace>
* @param stackFrames Map<StackFrameID, StackFrame>
* @param executables Map<FileID, Executable>
* @param totalFrames number
* @param samplingRate number
* @returns
*/
export function createCalleeTree(
events: Map<StackTraceID, number>,
stackTraces: Map<StackTraceID, StackTrace>,

View file

@ -1,12 +1,16 @@
/*
* 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.
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { UnionToIntersection, ValuesType } from 'utility-types';
/**
* Profiling Elasticsearch fields
*/
export enum ProfilingESField {
Timestamp = '@timestamp',
ContainerName = 'container.name',

View file

@ -1,14 +1,14 @@
/*
* 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.
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { createCalleeTree } from './callee';
import { createBaseFlameGraph, createFlameGraph } from './flamegraph';
import { decodeStackTraceResponse } from './stack_traces';
import { stackTraceFixtures } from './__fixtures__/stacktraces';
describe('Flamegraph operations', () => {

View file

@ -1,8 +1,9 @@
/*
* 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.
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { CalleeTree } from './callee';
@ -10,28 +11,49 @@ import { createFrameGroupID } from './frame_group';
import { fnv1a64 } from './hash';
import { createStackFrameMetadata, getCalleeLabel } from './profiling';
/**
* Base Flamegraph
*/
export interface BaseFlameGraph {
/** size */
Size: number;
/** edges */
Edges: number[][];
/** file ids */
FileID: string[];
/** frame types */
FrameType: number[];
/** inlines */
Inline: boolean[];
/** executable file names */
ExeFilename: string[];
/** address or line */
AddressOrLine: number[];
/** function names */
FunctionName: string[];
/** function offsets */
FunctionOffset: number[];
/** source file names */
SourceFilename: string[];
/** source lines */
SourceLine: number[];
/** total cpu */
CountInclusive: number[];
/** self cpu */
CountExclusive: number[];
/** total seconds */
TotalSeconds: number;
/** sampling rate */
SamplingRate: number;
}
// createBaseFlameGraph encapsulates the tree representation into a serialized form.
/**
* createBaseFlameGraph encapsulates the tree representation into a serialized form.
* @param tree CalleeTree
* @param samplingRate number
* @param totalSeconds number
* @returns BaseFlameGraph
*/
export function createBaseFlameGraph(
tree: CalleeTree,
samplingRate: number,
@ -71,14 +93,22 @@ export function createBaseFlameGraph(
return graph;
}
/** Elasticsearch flamegraph */
export interface ElasticFlameGraph extends BaseFlameGraph {
/** ID */
ID: string[];
/** Label */
Label: string[];
}
// createFlameGraph combines the base flamegraph with CPU-intensive values.
// This allows us to create a flamegraph in two steps (e.g. first on the server
// and finally in the browser).
/**
*
* createFlameGraph combines the base flamegraph with CPU-intensive values.
* This allows us to create a flamegraph in two steps (e.g. first on the server
* and finally in the browser).
* @param base BaseFlameGraph
* @returns ElasticFlameGraph
*/
export function createFlameGraph(base: BaseFlameGraph): ElasticFlameGraph {
const graph: ElasticFlameGraph = {
Size: base.Size,

View file

@ -1,8 +1,9 @@
/*
* 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.
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { createFrameGroupID } from './frame_group';

View file

@ -1,24 +1,34 @@
/*
* 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.
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { takeRight } from 'lodash';
import { StackFrameMetadata } from './profiling';
/** Frame group ID */
export type FrameGroupID = string;
function stripLeadingSubdirs(sourceFileName: string) {
return takeRight(sourceFileName.split('/'), 2).join('/');
}
// createFrameGroupID is the "standard" way of grouping frames, by commonly
// shared group identifiers.
//
// For ELF-symbolized frames, group by FunctionName, ExeFileName and FileID.
// For non-symbolized frames, group by FileID and AddressOrLine.
// otherwise group by ExeFileName, SourceFilename and FunctionName.
/**
*
* createFrameGroupID is the "standard" way of grouping frames, by commonly shared group identifiers.
* For ELF-symbolized frames, group by FunctionName, ExeFileName and FileID.
* For non-symbolized frames, group by FileID and AddressOrLine.
* otherwise group by ExeFileName, SourceFilename and FunctionName.
* @param fileID string
* @param addressOrLine string
* @param exeFilename string
* @param sourceFilename string
* @param functionName string
* @returns FrameGroupID
*/
export function createFrameGroupID(
fileID: StackFrameMetadata['FileID'],
addressOrLine: StackFrameMetadata['AddressOrLine'],

View file

@ -1,8 +1,9 @@
/*
* 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.
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { fnv1a64 } from './hash';

View file

@ -1,8 +1,9 @@
/*
* 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.
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
// prettier-ignore
@ -25,23 +26,24 @@ const lowerHex = [
'f0', 'f1', 'f2', 'f3', 'f4', 'f5', 'f6', 'f7', 'f8', 'f9', 'fa', 'fb', 'fc', 'fd', 'fe', 'ff',
];
// fnv1a64 computes a 64-bit hash of a byte array using the FNV-1a hash function [1].
//
// Due to the lack of a native uint64 in JavaScript, we operate on 64-bit values using an array
// of 4 uint16s instead. This method follows Knuth's Algorithm M in section 4.3.1 [2] using a
// modified multiword multiplication implementation described in [3]. The modifications include:
//
// * rewrite default algorithm for the special case m = n = 4
// * unroll loops
// * simplify expressions
// * create pre-computed lookup table for serialization to hexadecimal
//
// 1. https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function
// 2. Knuth, Donald E. The Art of Computer Programming, Volume 2, Third Edition: Seminumerical
// Algorithms. Addison-Wesley, 1998.
// 3. Warren, Henry S. Hacker's Delight. Upper Saddle River, NJ: Addison-Wesley, 2013.
/* eslint no-bitwise: ["error", { "allow": ["^=", ">>", "&"] }] */
/**
* - fnv1a64 computes a 64-bit hash of a byte array using the FNV-1a hash function [1].
* Due to the lack of a native uint64 in JavaScript, we operate on 64-bit values using an array
* of 4 uint16s instead. This method follows Knuth's Algorithm M in section 4.3.1 [2] using a
* modified multiword multiplication implementation described in [3]. The modifications include:
* - rewrite default algorithm for the special case m = n = 4
* - unroll loops
* - simplify expressions
* - create pre-computed lookup table for serialization to hexadecimal
* 1. https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function
* 2. Knuth, Donald E. The Art of Computer Programming, Volume 2, Third Edition: Seminumerical
* Algorithms. Addison-Wesley, 1998.
* 3. Warren, Henry S. Hacker's Delight. Upper Saddle River, NJ: Addison-Wesley, 2013.
* @param bytes Uint8Array
* @returns string
*/
export function fnv1a64(bytes: Uint8Array): string {
const n = bytes.length;
let [h0, h1, h2, h3] = [0x2325, 0x8422, 0x9ce4, 0xcbf2];

View file

@ -1,8 +1,9 @@
/*
* 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.
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import {

View file

@ -1,14 +1,27 @@
/*
* 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.
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
/**
* Stacktrace ID
*/
export type StackTraceID = string;
/**
* StackFrame ID
*/
export type StackFrameID = string;
/**
* File ID
*/
export type FileID = string;
/**
* Frame type
*/
export enum FrameType {
Unsymbolized = 0,
Python,
@ -35,94 +48,134 @@ const frameTypeDescriptions = {
[FrameType.PHPJIT]: 'PHP JIT',
};
/**
* get frame type name
* @param ft FrameType
* @returns string
*/
export function describeFrameType(ft: FrameType): string {
return frameTypeDescriptions[ft];
}
export interface StackTraceEvent {
/** stacktrace ID */
StackTraceID: StackTraceID;
/** count */
Count: number;
}
/** Stack trace */
export interface StackTrace {
/** frame ids */
FrameIDs: string[];
/** file ids */
FileIDs: string[];
/** address or lines */
AddressOrLines: number[];
/** types */
Types: number[];
}
/**
* Empty stack trace
*/
export const emptyStackTrace: StackTrace = {
/** Frame IDs */
FrameIDs: [],
/** File IDs */
FileIDs: [],
/** Address or lines */
AddressOrLines: [],
/** Types */
Types: [],
};
/** Stack frame */
export interface StackFrame {
/** file name */
FileName: string;
/** function name */
FunctionName: string;
/** function offset */
FunctionOffset: number;
/** line number */
LineNumber: number;
/** inline */
Inline: boolean;
}
/**
* Empty stack frame
*/
export const emptyStackFrame: StackFrame = {
/** File name */
FileName: '',
/** Function name */
FunctionName: '',
/** Function offset */
FunctionOffset: 0,
/** Line number */
LineNumber: 0,
/** Inline */
Inline: false,
};
/** Executable */
export interface Executable {
/** file name */
FileName: string;
}
/**
* Empty exectutable
*/
export const emptyExecutable: Executable = {
/** file name */
FileName: '',
};
/** Stack frame metadata */
export interface StackFrameMetadata {
// StackTrace.FrameID
/** StackTrace.FrameID */
FrameID: string;
// StackTrace.FileID
/** StackTrace.FileID */
FileID: FileID;
// StackTrace.Type
/** StackTrace.Type */
FrameType: FrameType;
// StackFrame.Inline
/** StackFrame.Inline */
Inline: boolean;
// StackTrace.AddressOrLine
/** StackTrace.AddressOrLine */
AddressOrLine: number;
// StackFrame.FunctionName
/** StackFrame.FunctionName */
FunctionName: string;
// StackFrame.FunctionOffset
/** StackFrame.FunctionOffset */
FunctionOffset: number;
// should this be StackFrame.SourceID?
/** should this be StackFrame.SourceID? */
SourceID: FileID;
// StackFrame.Filename
/** StackFrame.Filename */
SourceFilename: string;
// StackFrame.LineNumber
/** StackFrame.LineNumber */
SourceLine: number;
// auto-generated - see createStackFrameMetadata
/** auto-generated - see createStackFrameMetadata */
FunctionSourceLine: number;
// Executable.FileName
/** Executable.FileName */
ExeFileName: string;
// unused atm due to lack of symbolization metadata
/** unused atm due to lack of symbolization metadata */
CommitHash: string;
// unused atm due to lack of symbolization metadata
/** unused atm due to lack of symbolization metadata */
SourceCodeURL: string;
// unused atm due to lack of symbolization metadata
/** unused atm due to lack of symbolization metadata */
SourcePackageHash: string;
// unused atm due to lack of symbolization metadata
/** unused atm due to lack of symbolization metadata */
SourcePackageURL: string;
// unused atm due to lack of symbolization metadata
/** unused atm due to lack of symbolization metadata */
SamplingRate: number;
}
/**
* create stackframe metadata
* @param options Partial<StackFrameMetadata>
* @returns StackFrameMetadata
*/
export function createStackFrameMetadata(
options: Partial<StackFrameMetadata> = {}
): StackFrameMetadata {
@ -182,6 +235,11 @@ function getExeFileName(metadata: StackFrameMetadata) {
return describeFrameType(metadata.FrameType);
}
/**
* Get callee label
* @param metadata StackFrameMetadata
* @returns string
*/
export function getCalleeLabel(metadata: StackFrameMetadata) {
if (metadata.FunctionName !== '') {
const sourceFilename = metadata.SourceFilename;
@ -192,7 +250,11 @@ export function getCalleeLabel(metadata: StackFrameMetadata) {
}
return getExeFileName(metadata);
}
/**
* Get callee function name
* @param frame StackFrameMetadata
* @returns string
*/
export function getCalleeFunction(frame: StackFrameMetadata): string {
// In the best case scenario, we have the file names, source lines,
// and function names. However we need to deal with missing function or
@ -202,20 +264,32 @@ export function getCalleeFunction(frame: StackFrameMetadata): string {
// When there is no function name, only use the executable name
return frame.FunctionName ? exeDisplayName + ': ' + frame.FunctionName : exeDisplayName;
}
/**
* Frame symbol status
*/
export enum FrameSymbolStatus {
PARTIALLY_SYMBOLYZED = 'PARTIALLY_SYMBOLYZED',
NOT_SYMBOLIZED = 'NOT_SYMBOLIZED',
SYMBOLIZED = 'SYMBOLIZED',
}
export function getFrameSymbolStatus({
sourceFilename,
sourceLine,
exeFileName,
}: {
/** Frame symbols status params */
interface FrameSymbolStatusParams {
/** source file name */
sourceFilename: string;
/** source file line */
sourceLine: number;
/** executable file name */
exeFileName?: string;
}) {
}
/**
* Get frame symbol status
* @param param FrameSymbolStatusParams
* @returns FrameSymbolStatus
*/
export function getFrameSymbolStatus(param: FrameSymbolStatusParams) {
const { sourceFilename, sourceLine, exeFileName } = param;
if (sourceFilename === '' && sourceLine === 0) {
if (exeFileName) {
return FrameSymbolStatus.PARTIALLY_SYMBOLYZED;
@ -228,10 +302,28 @@ export function getFrameSymbolStatus({
}
const nativeLanguages = [FrameType.Native, FrameType.Kernel];
export function getLanguageType({ frameType }: { frameType: FrameType }) {
return nativeLanguages.includes(frameType) ? 'NATIVE' : 'INTERPRETED';
interface LanguageTypeParams {
/** frame type */
frameType: FrameType;
}
/**
* Get language type
* @param param LanguageTypeParams
* @returns string
*/
export function getLanguageType(param: LanguageTypeParams) {
return nativeLanguages.includes(param.frameType) ? 'NATIVE' : 'INTERPRETED';
}
/**
* Get callee source information.
* If we don't have the executable filename, display <unsymbolized>
* If no source line or filename available, display the executable offset
* @param frame StackFrameMetadata
* @returns string
*/
export function getCalleeSource(frame: StackFrameMetadata): string {
const frameSymbolStatus = getFrameSymbolStatus({
sourceFilename: frame.SourceFilename,
@ -254,6 +346,13 @@ export function getCalleeSource(frame: StackFrameMetadata): string {
}
}
/**
* Group stackframe by stack trace
* @param stackTraces Map<StackTraceID, StackTrace>
* @param stackFrames Map<StackFrameID, StackFrame>
* @param executables Map<FileID, Executable>
* @returns Record<string, StackFrameMetadata[]>
*/
export function groupStackFrameMetadataByStackTrace(
stackTraces: Map<StackTraceID, StackTrace>,
stackFrames: Map<StackFrameID, StackFrame>,

View file

@ -1,8 +1,9 @@
/*
* 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.
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import {

View file

@ -1,8 +1,9 @@
/*
* 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.
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { ProfilingESField } from './elasticsearch';
@ -15,13 +16,17 @@ import {
StackTraceID,
} from './profiling';
/** Profiling status response */
export interface ProfilingStatusResponse {
/** profiling enabled */
profiling: {
enabled: boolean;
};
/** resource management status*/
resource_management: {
enabled: boolean;
};
/** Indices creates / pre 8.9.1 data still available */
resources: {
created: boolean;
pre_8_9_1_data: boolean;
@ -58,24 +63,43 @@ interface ProfilingExecutables {
[key: string]: string;
}
/** Profiling stacktrace */
export interface StackTraceResponse {
/** stack trace events */
['stack_trace_events']?: ProfilingEvents;
/** stack traces */
['stack_traces']?: ProfilingStackTraces;
/** stack frames */
['stack_frames']?: ProfilingStackFrames;
/** executables */
['executables']?: ProfilingExecutables;
/** total frames */
['total_frames']: number;
/** sampling rate */
['sampling_rate']: number;
}
/** Decoded stack trace response */
export interface DecodedStackTraceResponse {
/** Map of Stacktrace ID and event */
events: Map<StackTraceID, number>;
/** Map of stacktrace ID and stacktrace */
stackTraces: Map<StackTraceID, StackTrace>;
/** Map of stackframe ID and stackframe */
stackFrames: Map<StackFrameID, StackFrame>;
/** Map of file ID and Executables */
executables: Map<FileID, Executable>;
/** Total number of frames */
totalFrames: number;
/** sampling rate */
samplingRate: number;
}
/**
* Generate Frame ID
* @param frameID string
* @param n number
* @returns string
*/
export const makeFrameID = (frameID: string, n: number): string => {
return n === 0 ? frameID : frameID + ';' + n.toString();
};
@ -119,6 +143,11 @@ const createInlineTrace = (
} as StackTrace;
};
/**
* Decodes stack trace response
* @param response StackTraceResponse
* @returns DecodedStackTraceResponse
*/
export function decodeStackTraceResponse(response: StackTraceResponse): DecodedStackTraceResponse {
const stackTraceEvents: Map<StackTraceID, number> = new Map();
for (const [key, value] of Object.entries(response.stack_trace_events ?? {})) {
@ -165,11 +194,17 @@ export function decodeStackTraceResponse(response: StackTraceResponse): DecodedS
};
}
/**
* Stacktraces options
*/
export enum StackTracesDisplayOption {
StackTraces = 'stackTraces',
Percentage = 'percentage',
}
/**
* Functions TopN types definition
*/
export enum TopNType {
Containers = 'containers',
Deployments = 'deployments',
@ -178,6 +213,11 @@ export enum TopNType {
Traces = 'traces',
}
/**
* Get Profiling ES field based on TopN Type
* @param type TopNType
* @returns string
*/
export function getFieldNameForTopNType(type: TopNType): string {
return {
[TopNType.Containers]: ProfilingESField.ContainerName,

View file

@ -0,0 +1,46 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export { decodeStackTraceResponse } from './common/stack_traces';
export { createBaseFlameGraph, createFlameGraph } from './common/flamegraph';
export { createCalleeTree } from './common/callee';
export { ProfilingESField } from './common/elasticsearch';
export {
groupStackFrameMetadataByStackTrace,
describeFrameType,
FrameType,
getCalleeFunction,
getCalleeSource,
getLanguageType,
FrameSymbolStatus,
getFrameSymbolStatus,
createStackFrameMetadata,
emptyExecutable,
emptyStackFrame,
emptyStackTrace,
} from './common/profiling';
export { getFieldNameForTopNType, TopNType, StackTracesDisplayOption } from './common/stack_traces';
export { createFrameGroupID } from './common/frame_group';
export type { CalleeTree } from './common/callee';
export type {
ProfilingStatusResponse,
StackTraceResponse,
DecodedStackTraceResponse,
} from './common/stack_traces';
export type { ElasticFlameGraph, BaseFlameGraph } from './common/flamegraph';
export type { FrameGroupID } from './common/frame_group';
export type {
Executable,
FileID,
StackFrame,
StackFrameID,
StackFrameMetadata,
StackTrace,
StackTraceID,
} from './common/profiling';

View file

@ -0,0 +1,13 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
module.exports = {
preset: '@kbn/test',
rootDir: '../..',
roots: ['<rootDir>/packages/kbn-profiling-utils'],
};

View file

@ -0,0 +1,5 @@
{
"type": "shared-common",
"id": "@kbn/profiling-utils",
"owner": "@elastic/profiling-ui"
}

View file

@ -0,0 +1,6 @@
{
"name": "@kbn/profiling-utils",
"private": true,
"version": "1.0.0",
"license": "SSPL-1.0 OR Elastic License 2.0"
}

View file

@ -0,0 +1,18 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "target/types",
"types": [
"jest",
"node"
]
},
"include": [
"**/*.ts",
"**/*.json",
],
"exclude": [
"target/**/*"
],
"kbn_references": []
}

View file

@ -1100,6 +1100,8 @@
"@kbn/profiling-data-access-plugin/*": ["x-pack/plugins/profiling_data_access/*"],
"@kbn/profiling-plugin": ["x-pack/plugins/profiling"],
"@kbn/profiling-plugin/*": ["x-pack/plugins/profiling/*"],
"@kbn/profiling-utils": ["packages/kbn-profiling-utils"],
"@kbn/profiling-utils/*": ["packages/kbn-profiling-utils/*"],
"@kbn/random-sampling": ["x-pack/packages/kbn-random-sampling"],
"@kbn/random-sampling/*": ["x-pack/packages/kbn-random-sampling/*"],
"@kbn/react-field": ["packages/kbn-react-field"],

View file

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

View file

@ -0,0 +1,75 @@
/*
* 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 { EmbeddableFlamegraph } from '@kbn/observability-shared-plugin/public';
import React from 'react';
import { useApmParams } from '../../../hooks/use_apm_params';
import { isPending, useFetcher } from '../../../hooks/use_fetcher';
import { useProfilingPlugin } from '../../../hooks/use_profiling_plugin';
import { useTimeRange } from '../../../hooks/use_time_range';
import { ApmDocumentType } from '../../../../common/document_type';
import { usePreferredDataSourceAndBucketSize } from '../../../hooks/use_preferred_data_source_and_bucket_size';
export function ProfilingOverview() {
const {
path: { serviceName },
query: { kuery, rangeFrom, rangeTo, environment },
} = useApmParams('/services/{serviceName}/profiling');
const { isProfilingAvailable } = useProfilingPlugin();
const { start, end } = useTimeRange({ rangeFrom, rangeTo });
const preferred = usePreferredDataSourceAndBucketSize({
start,
end,
kuery,
type: ApmDocumentType.TransactionMetric,
numBuckets: 20,
});
const { data, status } = useFetcher(
(callApmApi) => {
if (isProfilingAvailable && preferred) {
return callApmApi(
'GET /internal/apm/services/{serviceName}/profiling/flamegraph',
{
params: {
path: { serviceName },
query: {
start,
end,
kuery,
environment,
documentType: preferred.source.documentType,
rollupInterval: preferred.source.rollupInterval,
},
},
}
);
}
},
[
isProfilingAvailable,
preferred,
serviceName,
start,
end,
kuery,
environment,
]
);
if (!isProfilingAvailable) {
return null;
}
return (
<EmbeddableFlamegraph
data={data}
height="60vh"
isLoading={isPending(status)}
/>
);
}

View file

@ -37,6 +37,8 @@ import { TransactionOverview } from '../../app/transaction_overview';
import { ApmServiceTemplate } from '../templates/apm_service_template';
import { ApmServiceWrapper } from './apm_service_wrapper';
import { RedirectToDefaultServiceRouteView } from './redirect_to_default_service_route_view';
import { ProfilingOverview } from '../../app/profiling_overview';
import { SearchBar } from '../../shared/search_bar/search_bar';
function page({
title,
@ -47,12 +49,7 @@ function page({
title: string;
tab: React.ComponentProps<typeof ApmServiceTemplate>['selectedTab'];
element: React.ReactElement<any, any>;
searchBarOptions?: {
showUnifiedSearchBar?: boolean;
showTransactionTypeSelector?: boolean;
showTimeComparison?: boolean;
hidden?: boolean;
};
searchBarOptions?: React.ComponentProps<typeof SearchBar>;
}): {
element: React.ReactElement<any, any>;
} {
@ -365,6 +362,20 @@ export const serviceDetailRoute = {
}),
}),
},
'/services/{serviceName}/profiling': {
...page({
tab: 'profiling',
title: i18n.translate('xpack.apm.views.profiling.title', {
defaultMessage: 'Universal Profiling',
}),
element: <ProfilingOverview />,
searchBarOptions: {
showTimeComparison: false,
showTransactionTypeSelector: false,
showQueryInput: false,
},
}),
},
'/services/{serviceName}/': {
element: <RedirectToDefaultServiceRouteView />,
},

View file

@ -48,6 +48,7 @@ import { AnalyzeDataButton } from './analyze_data_button';
import { ServerlessType } from '../../../../../common/serverless';
import { useApmFeatureFlag } from '../../../../hooks/use_apm_feature_flag';
import { ApmFeatureFlagName } from '../../../../../common/apm_feature_flags';
import { useProfilingPlugin } from '../../../../hooks/use_profiling_plugin';
type Tab = NonNullable<EuiPageHeaderProps['tabs']>[0] & {
key:
@ -60,7 +61,8 @@ type Tab = NonNullable<EuiPageHeaderProps['tabs']>[0] & {
| 'infrastructure'
| 'service-map'
| 'logs'
| 'alerts';
| 'alerts'
| 'profiling';
hidden?: boolean;
};
@ -215,6 +217,7 @@ function useTabs({ selectedTab }: { selectedTab: Tab['key'] }) {
plugins,
capabilities
);
const { isProfilingAvailable } = useProfilingPlugin();
const router = useApmRouter();
const isInfraTabAvailable = useApmFeatureFlag(
@ -391,6 +394,24 @@ function useTabs({ selectedTab }: { selectedTab: Tab['key'] }) {
}),
hidden: !(isAlertingAvailable && canReadAlerts),
},
{
key: 'profiling',
href: router.link('/services/{serviceName}/profiling', {
path: { serviceName },
query,
}),
label: i18n.translate('xpack.apm.home.profilingTabLabel', {
defaultMessage: 'Universal Profiling',
}),
hidden: !isProfilingAvailable,
append: (
<EuiBadge color="accent">
{i18n.translate('xpack.apm.universalProfiling.newLabel', {
defaultMessage: 'New',
})}
</EuiBadge>
),
},
];
return tabs

View file

@ -23,6 +23,7 @@ interface Props {
hidden?: boolean;
showUnifiedSearchBar?: boolean;
showTimeComparison?: boolean;
showQueryInput?: boolean;
showTransactionTypeSelector?: boolean;
searchBarPlaceholder?: string;
searchBarBoolFilter?: QueryDslQueryContainer[];
@ -33,6 +34,7 @@ export function SearchBar({
showUnifiedSearchBar = true,
showTimeComparison = false,
showTransactionTypeSelector = false,
showQueryInput = true,
searchBarPlaceholder,
searchBarBoolFilter,
}: Props) {
@ -72,6 +74,7 @@ export function SearchBar({
<UnifiedSearchBar
placeholder={searchBarPlaceholder}
boolFilter={searchBarBoolFilter}
showQueryInput={showQueryInput}
/>
</EuiFlexItem>
)}

View file

@ -123,6 +123,7 @@ export function UnifiedSearchBar({
placeholder,
value,
showDatePicker = true,
showQueryInput = true,
showSubmitButton = true,
isClearable = true,
boolFilter,
@ -130,6 +131,7 @@ export function UnifiedSearchBar({
placeholder?: string;
value?: string;
showDatePicker?: boolean;
showQueryInput?: boolean;
showSubmitButton?: boolean;
isClearable?: boolean;
boolFilter?: QueryDslQueryContainer[];
@ -303,7 +305,7 @@ export function UnifiedSearchBar({
placeholder={searchbarPlaceholder}
useDefaultBehaviors={true}
indexPatterns={dataView ? [dataView] : undefined}
showQueryInput={true}
showQueryInput={showQueryInput}
showQueryMenu={false}
showFilterBar={false}
showDatePicker={showDatePicker}

View file

@ -31,12 +31,15 @@ export function useProfilingPlugin() {
fetchIsProfilingSetup();
}, [plugins.profiling]);
const isProfilingAvailable =
isProfilingIntegrationEnabled && isProfilingPluginInitialized;
return {
isProfilingPluginInitialized,
profilingLocators:
isProfilingIntegrationEnabled && isProfilingPluginInitialized
? plugins.profiling?.locators
: undefined,
profilingLocators: isProfilingAvailable
? plugins.profiling?.locators
: undefined,
isProfilingIntegrationEnabled,
isProfilingAvailable,
};
}

View file

@ -45,6 +45,7 @@ import { timeRangeMetadataRoute } from '../time_range_metadata/route';
import { traceRouteRepository } from '../traces/route';
import { transactionRouteRepository } from '../transactions/route';
import { assistantRouteRepository } from '../assistant_functions/route';
import { profilingRouteRepository } from '../profiling/route';
function getTypedGlobalApmServerRouteRepository() {
const repository = {
@ -83,6 +84,7 @@ function getTypedGlobalApmServerRouteRepository() {
...mobileRouteRepository,
...diagnosticsRepository,
...assistantRouteRepository,
...profilingRouteRepository,
};
return repository;

View file

@ -0,0 +1,66 @@
/*
* 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 { kqlQuery, rangeQuery } from '@kbn/observability-plugin/server';
import { ApmServiceTransactionDocumentType } from '../../../common/document_type';
import { HOST_HOSTNAME, SERVICE_NAME } from '../../../common/es_fields/apm';
import { RollupInterval } from '../../../common/rollup';
import { environmentQuery } from '../../../common/utils/environment_query';
import { APMEventClient } from '../../lib/helpers/create_es_client/create_apm_event_client';
export async function getServiceHostNames({
apmEventClient,
serviceName,
start,
end,
environment,
kuery,
documentType,
rollupInterval,
}: {
environment: string;
kuery: string;
serviceName: string;
start: number;
end: number;
apmEventClient: APMEventClient;
documentType: ApmServiceTransactionDocumentType;
rollupInterval: RollupInterval;
}) {
const response = await apmEventClient.search('get_service_host_names', {
apm: {
sources: [{ documentType, rollupInterval }],
},
body: {
track_total_hits: false,
size: 0,
query: {
bool: {
filter: [
{ term: { [SERVICE_NAME]: serviceName } },
...rangeQuery(start, end),
...environmentQuery(environment),
...kqlQuery(kuery),
],
},
},
aggs: {
hostNames: {
terms: {
field: HOST_HOSTNAME,
size: 500,
},
},
},
},
});
return (
response.aggregations?.hostNames.buckets.map(
(bucket) => bucket.key as string
) || []
);
}

View file

@ -0,0 +1,71 @@
/*
* 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 * as t from 'io-ts';
import type { BaseFlameGraph } from '@kbn/profiling-utils';
import { createApmServerRoute } from '../apm_routes/create_apm_server_route';
import {
environmentRt,
kueryRt,
rangeRt,
serviceTransactionDataSourceRt,
} from '../default_api_types';
import { getApmEventClient } from '../../lib/helpers/get_apm_event_client';
import { getServiceHostNames } from './get_service_host_names';
import { hostNamesToKuery } from './utils';
const profilingFlamegraphRoute = createApmServerRoute({
endpoint: 'GET /internal/apm/services/{serviceName}/profiling/flamegraph',
params: t.type({
path: t.type({ serviceName: t.string }),
query: t.intersection([
rangeRt,
kueryRt,
environmentRt,
serviceTransactionDataSourceRt,
]),
}),
options: { tags: ['access:apm'] },
handler: async (resources): Promise<BaseFlameGraph | undefined> => {
const { context, plugins, params } = resources;
const [esClient, apmEventClient, profilingDataAccessStart] =
await Promise.all([
(await context.core).elasticsearch.client,
await getApmEventClient(resources),
await plugins.profilingDataAccess?.start(),
]);
if (profilingDataAccessStart) {
const { start, end, kuery, environment, documentType, rollupInterval } =
params.query;
const { serviceName } = params.path;
const serviceHostNames = await getServiceHostNames({
apmEventClient,
start,
end,
kuery,
environment,
serviceName,
documentType,
rollupInterval,
});
return profilingDataAccessStart?.services.fetchFlamechartData({
esClient: esClient.asCurrentUser,
rangeFromMs: start,
rangeToMs: end,
kuery: hostNamesToKuery(serviceHostNames),
});
}
return undefined;
},
});
export const profilingRouteRepository = {
...profilingFlamegraphRoute,
};

View file

@ -0,0 +1,26 @@
/*
* 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 { HOST_NAME } from '../../../common/es_fields/apm';
import { hostNamesToKuery } from './utils';
describe('profiling utils', () => {
describe('hostNamesToKuery', () => {
it('returns a single hostname', () => {
expect(hostNamesToKuery(['foo'])).toEqual(`${HOST_NAME} : "foo"`);
});
it('returns multiple hostnames', () => {
expect(hostNamesToKuery(['foo', 'bar', 'baz'])).toEqual(
`${HOST_NAME} : "foo" OR ${HOST_NAME} : "bar" OR ${HOST_NAME} : "baz"`
);
});
it('return empty string when no hostname', () => {
expect(hostNamesToKuery([])).toEqual('');
});
});
});

View file

@ -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.
*/
import { isEmpty } from 'lodash';
import { HOST_NAME } from '../../../common/es_fields/apm';
export function hostNamesToKuery(hostNames: string[]) {
return hostNames.reduce<string>((acc, hostName) => {
if (isEmpty(acc)) {
return `${HOST_NAME} : "${hostName}"`;
}
return `${acc} OR ${HOST_NAME} : "${hostName}"`;
}, '');
}

View file

@ -61,6 +61,10 @@ import {
CustomIntegrationsPluginSetup,
CustomIntegrationsPluginStart,
} from '@kbn/custom-integrations-plugin/server';
import {
ProfilingDataAccessPluginSetup,
ProfilingDataAccessPluginStart,
} from '@kbn/profiling-data-access-plugin/server';
import { APMConfig } from '.';
export interface APMPluginSetup {
@ -91,6 +95,7 @@ export interface APMPluginSetupDependencies {
taskManager?: TaskManagerSetupContract;
usageCollection?: UsageCollectionSetup;
customIntegrations?: CustomIntegrationsPluginSetup;
profilingDataAccess?: ProfilingDataAccessPluginSetup;
}
export interface APMPluginStartDependencies {
// required dependencies
@ -116,4 +121,5 @@ export interface APMPluginStartDependencies {
taskManager?: TaskManagerStartContract;
usageCollection?: undefined;
customIntegrations?: CustomIntegrationsPluginStart;
profilingDataAccess?: ProfilingDataAccessPluginStart;
}

View file

@ -96,6 +96,8 @@
"@kbn/discover-plugin",
"@kbn/observability-ai-assistant-plugin",
"@kbn/apm-data-access-plugin",
"@kbn/profiling-data-access-plugin",
"@kbn/profiling-utils",
"@kbn/core-analytics-server",
"@kbn/analytics-client",
"@kbn/monaco"

View file

@ -7,7 +7,7 @@
"server": false,
"browser": true,
"configPath": ["xpack", "observability_shared"],
"requiredPlugins": ["cases", "guidedOnboarding", "uiActions"],
"requiredPlugins": ["cases", "guidedOnboarding", "uiActions", "embeddable"],
"optionalPlugins": [],
"requiredBundles": ["data", "inspector", "kibanaReact", "kibanaUtils"],
"extraPublicDirs": ["common"]

View file

@ -0,0 +1,63 @@
/*
* 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, { useEffect, useRef, useState } from 'react';
import type { BaseFlameGraph } from '@kbn/profiling-utils';
import { css } from '@emotion/react';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { ObservabilitySharedStart } from '../../../plugin';
import { EMBEDDABLE_FLAMEGRAPH } from '.';
interface Props {
data?: BaseFlameGraph;
height?: string;
isLoading: boolean;
}
export function EmbeddableFlamegraph({ data, height, isLoading }: Props) {
const { embeddable: embeddablePlugin } = useKibana<ObservabilitySharedStart>().services;
const [embeddable, setEmbeddable] = useState<any>();
const embeddableRoot: React.RefObject<HTMLDivElement> = useRef<HTMLDivElement>(null);
useEffect(() => {
async function createEmbeddable() {
const factory = embeddablePlugin?.getEmbeddableFactory(EMBEDDABLE_FLAMEGRAPH);
const input = { id: 'embeddable_profiling', data, isLoading };
const embeddableObject = await factory?.create(input);
setEmbeddable(embeddableObject);
}
createEmbeddable();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
if (embeddableRoot.current && embeddable) {
embeddable.render(embeddableRoot.current);
}
}, [embeddable, embeddableRoot]);
useEffect(() => {
if (embeddable) {
embeddable.updateInput({ data, isLoading });
embeddable.reload();
}
}, [data, embeddable, isLoading]);
return (
<div
css={css`
width: 100%;
height: ${height};
display: flex;
flex: 1 1 100%;
z-index: 1;
min-height: 0;
`}
ref={embeddableRoot}
/>
);
}

View file

@ -0,0 +1,9 @@
/*
* 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.
*/
/** Profiling flamegraph embeddable key */
export const EMBEDDABLE_FLAMEGRAPH = 'EMBEDDABLE_FLAMEGRAPH';

View file

@ -77,3 +77,6 @@ export {
casesFeatureId,
sloFeatureId,
} from '../common';
export { EMBEDDABLE_FLAMEGRAPH } from './components/profiling/embeddables';
export { EmbeddableFlamegraph } from './components/profiling/embeddables/embeddable_flamegraph';

View file

@ -11,6 +11,7 @@ import type { CoreStart, Plugin } from '@kbn/core/public';
import type { GuidedOnboardingPluginStart } from '@kbn/guided-onboarding-plugin/public';
import { CasesUiStart } from '@kbn/cases-plugin/public';
import { SpacesPluginStart } from '@kbn/spaces-plugin/public';
import type { EmbeddableStart } from '@kbn/embeddable-plugin/public';
import { createNavigationRegistry } from './components/page_template/helpers/navigation_registry';
import { createLazyObservabilityPageTemplate } from './components/page_template';
import { updateGlobalNavigation } from './services/update_global_navigation';
@ -20,6 +21,7 @@ export interface ObservabilitySharedStart {
cases: CasesUiStart;
guidedOnboarding: GuidedOnboardingPluginStart;
setIsSidebarEnabled: (isEnabled: boolean) => void;
embeddable: EmbeddableStart;
}
export type ObservabilitySharedPluginSetup = ReturnType<ObservabilitySharedPlugin['setup']>;

View file

@ -32,6 +32,8 @@
"@kbn/rison",
"@kbn/kibana-utils-plugin",
"@kbn/shared-ux-router",
"@kbn/embeddable-plugin",
"@kbn/profiling-utils"
],
"exclude": ["target/**/*"]
}

View file

@ -0,0 +1,17 @@
The stacktrace fixtures in this directory are originally from Elasticsearch's
`POST /_profiling/stacktraces` endpoint. They were subsequently filtered
through the `shrink_stacktrace_response.js` command in `x-pack/plugins/profiling/scripts/`
to reduce the size without losing sampling fidelity (see the script for further
details).
The naming convention for each stacktrace fixture follows this pattern:
```
stacktraces_{seconds}s_{upsampling rate}x.json
```
where `seconds` is the time span of the original query and `upsampling rate` is
the reciprocal of the sampling rate returned from the original query.
To add a new stacktrace fixture to the test suite, update `stacktraces.ts`
appropriately.

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { StackTraceResponse } from '../stack_traces';
import type { StackTraceResponse } from '@kbn/profiling-utils';
import stackTraces1x from './stacktraces_60s_1x.json';
import stackTraces5x from './stacktraces_3600s_5x.json';

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -5,16 +5,15 @@
* 2.0.
*/
import { sum } from 'lodash';
import { createCalleeTree } from '@kbn/profiling-data-access-plugin/common/callee';
import { createColumnarViewModel } from './columnar_view_model';
import {
createBaseFlameGraph,
createCalleeTree,
createFlameGraph,
} from '@kbn/profiling-data-access-plugin/common/flamegraph';
import { decodeStackTraceResponse } from '@kbn/profiling-data-access-plugin/common/stack_traces';
import { stackTraceFixtures } from '@kbn/profiling-data-access-plugin/common/__fixtures__/stacktraces';
decodeStackTraceResponse,
} from '@kbn/profiling-utils';
import { sum } from 'lodash';
import { createColumnarViewModel } from './columnar_view_model';
import { stackTraceFixtures } from './__fixtures__/stacktraces';
describe('Columnar view model operations', () => {
stackTraceFixtures.forEach(({ response, seconds, upsampledBy }) => {

View file

@ -6,7 +6,7 @@
*/
import { ColumnarViewModel } from '@elastic/charts';
import { ElasticFlameGraph } from '@kbn/profiling-data-access-plugin/common/flamegraph';
import type { ElasticFlameGraph } from '@kbn/profiling-utils';
import { frameTypeToRGB, rgbToRGBA } from './frame_type_colors';
function normalize(n: number, lower: number, upper: number): number {

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { FrameType } from '@kbn/profiling-data-access-plugin/common/profiling';
import { FrameType } from '@kbn/profiling-utils';
/*
* Helper to calculate the color of a given block to be drawn. The desirable outcomes of this are:

View file

@ -7,8 +7,8 @@
import { sum } from 'lodash';
import { createTopNFunctions } from './functions';
import { decodeStackTraceResponse } from '@kbn/profiling-data-access-plugin/common/stack_traces';
import { stackTraceFixtures } from '@kbn/profiling-data-access-plugin/common/__fixtures__/stacktraces';
import { decodeStackTraceResponse } from '@kbn/profiling-utils';
import { stackTraceFixtures } from './__fixtures__/stacktraces';
describe('TopN function operations', () => {
stackTraceFixtures.forEach(({ response, seconds, upsampledBy }) => {

View file

@ -4,25 +4,25 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import * as t from 'io-ts';
import { sumBy } from 'lodash';
import {
createFrameGroupID,
FrameGroupID,
} from '@kbn/profiling-data-access-plugin/common/frame_group';
import {
createStackFrameMetadata,
emptyExecutable,
emptyStackFrame,
emptyStackTrace,
import type {
Executable,
FileID,
FrameGroupID,
StackFrame,
StackFrameID,
StackFrameMetadata,
StackTrace,
StackTraceID,
} from '@kbn/profiling-data-access-plugin/common/profiling';
} from '@kbn/profiling-utils';
import {
createFrameGroupID,
createStackFrameMetadata,
emptyExecutable,
emptyStackFrame,
emptyStackTrace,
} from '@kbn/profiling-utils';
import * as t from 'io-ts';
import { sumBy } from 'lodash';
interface TopNFunctionAndFrameGroup {
Frame: StackFrameMetadata;

View file

@ -9,8 +9,8 @@ import { euiPaletteColorBlind } from '@elastic/eui';
import { InferSearchResponseOf } from '@kbn/es-types';
import { i18n } from '@kbn/i18n';
import { orderBy } from 'lodash';
import { ProfilingESField } from '@kbn/profiling-data-access-plugin/common/elasticsearch';
import { StackFrameMetadata } from '@kbn/profiling-data-access-plugin/common/profiling';
import { ProfilingESField } from '@kbn/profiling-utils';
import type { StackFrameMetadata } from '@kbn/profiling-utils';
import { createUniformBucketsForTimeRange } from './histogram';
export const OTHER_BUCKET_LABEL = i18n.translate('xpack.profiling.topn.otherBucketLabel', {

View file

@ -24,6 +24,7 @@
"observabilityAIAssistant",
"unifiedSearch",
"share",
"embeddable",
"profilingDataAccess"
],
"requiredBundles": [

View file

@ -19,7 +19,7 @@ import { EuiFlexGroup, EuiFlexItem, useEuiTheme } from '@elastic/eui';
import { Maybe } from '@kbn/observability-plugin/common/typings';
import React, { useEffect, useMemo, useState } from 'react';
import { useUiTracker } from '@kbn/observability-shared-plugin/public';
import { ElasticFlameGraph } from '@kbn/profiling-data-access-plugin/common/flamegraph';
import type { ElasticFlameGraph } from '@kbn/profiling-utils';
import { getFlamegraphModel } from '../../utils/get_flamegraph_model';
import { FlameGraphLegend } from './flame_graph_legend';
import { FrameInformationWindow } from '../frame_information_window';
@ -34,10 +34,9 @@ interface Props {
comparisonFlamegraph?: ElasticFlameGraph;
baseline?: number;
comparison?: number;
showInformationWindow: boolean;
toggleShowInformationWindow: () => void;
searchText?: string;
onChangeSearchText?: FlameSpec['onSearchTextChange'];
isEmbedded?: boolean;
}
export function FlameGraph({
@ -47,11 +46,14 @@ export function FlameGraph({
comparisonFlamegraph,
baseline,
comparison,
showInformationWindow,
toggleShowInformationWindow,
searchText,
onChangeSearchText,
isEmbedded = false,
}: Props) {
const [showInformationWindow, setShowInformationWindow] = useState(false);
function toggleShowInformationWindow() {
setShowInformationWindow((prev) => !prev);
}
const theme = useEuiTheme();
const trackProfilingEvent = useUiTracker({ app: 'profiling' });
@ -157,9 +159,7 @@ export function FlameGraph({
comparisonScaleFactor={comparison}
onShowMoreClick={() => {
trackProfilingEvent({ metric: 'flamegraph_node_details_click' });
if (!showInformationWindow) {
toggleShowInformationWindow();
}
toggleShowInformationWindow();
setHighlightedVmIndex(valueIndex);
}}
/>
@ -194,6 +194,8 @@ export function FlameGraph({
frame={selected}
totalSeconds={primaryFlamegraph?.TotalSeconds ?? 0}
totalSamples={totalSamples}
showAIAssistant={!isEmbedded}
showSymbolsStatus={!isEmbedded}
/>
)}
</>

View file

@ -0,0 +1,112 @@
/*
* 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 { i18n } from '@kbn/i18n';
import {
ContextualInsight,
Message,
MessageRole,
useObservabilityAIAssistant,
} from '@kbn/observability-ai-assistant-plugin/public';
import React, { useMemo } from 'react';
import { Frame } from '.';
interface Props {
frame?: Frame;
}
export function FrameInformationAIAssistant({ frame }: Props) {
const aiAssistant = useObservabilityAIAssistant();
const promptMessages = useMemo<Message[] | undefined>(() => {
if (frame?.functionName && frame.exeFileName) {
const functionName = frame.functionName;
const library = frame.exeFileName;
const now = new Date().toISOString();
return [
{
'@timestamp': now,
message: {
role: MessageRole.System,
content: `You are perf-gpt, a helpful assistant for performance analysis and optimisation
of software. Answer as concisely as possible.`,
},
},
{
'@timestamp': now,
message: {
role: MessageRole.User,
content: `I am a software engineer. I am trying to understand what a function in a particular
software library does.
The library is: ${library}
The function is: ${functionName}
Your have two tasks. Your first task is to desribe what the library is and what its use cases are, and to
describe what the function does. The output format should look as follows:
Library description: Provide a concise description of the library
Library use-cases: Provide a concise description of what the library is typically used for.
Function description: Provide a concise, technical, description of what the function does.
Assume the function ${functionName} from the library ${library} is consuming significant CPU resources.
Your second task is to suggest ways to optimize or improve the system that involve the ${functionName} function from the
${library} library. Types of improvements that would be useful to me are improvements that result in:
- Higher performance so that the system runs faster or uses less CPU
- Better memory efficient so that the system uses less RAM
- Better storage efficient so that the system stores less data on disk.
- Better network I/O efficiency so that less data is sent over the network
- Better disk I/O efficiency so that less data is read and written from disk
Make up to five suggestions. Your suggestions must meet all of the following criteria:
1. Your suggestions should detailed, technical and include concrete examples.
2. Your suggestions should be specific to improving performance of a system in which the ${functionName} function from
the ${library} library is consuming significant CPU.
3. If you suggest replacing the function or library with a more efficient replacement you must suggest at least
one concrete replacement.
If you know of fewer than five ways to improve the performance of a system in which the ${functionName} function from the
${library} library is consuming significant CPU, then provide fewer than five suggestions. If you do not know of any
way in which to improve the performance then say "I do not know how to improve the performance of systems where
this function is consuming a significant amount of CPU".
Do not suggest using a CPU profiler. I have already profiled my code. The profiler I used is Elastic Universal Profiler.
If there is specific information I should look for in the profiler output then tell me what information to look for
in the output of Elastic Universal Profiler.
You must not include URLs, web addresses or websites of any kind in your output.
If you have suggestions, the output format should look as follows:
Here are some suggestions as to how you might optimize your system if ${functionName} in ${library} is consuming
significant CPU resources:
1. Insert first suggestion
2. Insert second suggestion`,
},
},
];
}
return undefined;
}, [frame?.functionName, frame?.exeFileName]);
return (
<>
{aiAssistant.isEnabled() && promptMessages ? (
<ContextualInsight
messages={promptMessages}
title={i18n.translate('xpack.profiling.frameInformationWindow.optimizeFunction', {
defaultMessage: 'Optimize function',
})}
/>
) : null}
</>
);
}

View file

@ -6,7 +6,7 @@
*/
import { i18n } from '@kbn/i18n';
import { describeFrameType } from '@kbn/profiling-data-access-plugin/common/profiling';
import { describeFrameType } from '@kbn/profiling-utils';
import { NOT_AVAILABLE_LABEL } from '../../../common';
export function getInformationRows({

View file

@ -6,117 +6,42 @@
*/
import { EuiFlexGroup, EuiFlexItem, EuiText, EuiTitle } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import {
ContextualInsight,
Message,
MessageRole,
useObservabilityAIAssistant,
} from '@kbn/observability-ai-assistant-plugin/public';
import React, { useMemo } from 'react';
import {
FrameSymbolStatus,
getFrameSymbolStatus,
} from '@kbn/profiling-data-access-plugin/common/profiling';
import { FrameSymbolStatus, getFrameSymbolStatus } from '@kbn/profiling-utils';
import React from 'react';
import { FrameInformationAIAssistant } from './frame_information_ai_assistant';
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?: {
fileID: string;
frameType: number;
exeFileName: string;
addressOrLine: number;
functionName: string;
sourceFileName: string;
sourceLine: number;
countInclusive: number;
countExclusive: number;
};
totalSamples: number;
totalSeconds: number;
export interface Frame {
fileID: string;
frameType: number;
exeFileName: string;
addressOrLine: number;
functionName: string;
sourceFileName: string;
sourceLine: number;
countInclusive: number;
countExclusive: number;
}
export function FrameInformationWindow({ frame, totalSamples, totalSeconds }: Props) {
const aiAssistant = useObservabilityAIAssistant();
const promptMessages = useMemo<Message[] | undefined>(() => {
if (frame?.functionName && frame.exeFileName) {
const functionName = frame.functionName;
const library = frame.exeFileName;
const now = new Date().toISOString();
return [
{
'@timestamp': now,
message: {
role: MessageRole.System,
content: `You are perf-gpt, a helpful assistant for performance analysis and optimisation
of software. Answer as concisely as possible.`,
},
},
{
'@timestamp': now,
message: {
role: MessageRole.User,
content: `I am a software engineer. I am trying to understand what a function in a particular
software library does.
The library is: ${library}
The function is: ${functionName}
Your have two tasks. Your first task is to desribe what the library is and what its use cases are, and to
describe what the function does. The output format should look as follows:
Library description: Provide a concise description of the library
Library use-cases: Provide a concise description of what the library is typically used for.
Function description: Provide a concise, technical, description of what the function does.
Assume the function ${functionName} from the library ${library} is consuming significant CPU resources.
Your second task is to suggest ways to optimize or improve the system that involve the ${functionName} function from the
${library} library. Types of improvements that would be useful to me are improvements that result in:
- Higher performance so that the system runs faster or uses less CPU
- Better memory efficient so that the system uses less RAM
- Better storage efficient so that the system stores less data on disk.
- Better network I/O efficiency so that less data is sent over the network
- Better disk I/O efficiency so that less data is read and written from disk
Make up to five suggestions. Your suggestions must meet all of the following criteria:
1. Your suggestions should detailed, technical and include concrete examples.
2. Your suggestions should be specific to improving performance of a system in which the ${functionName} function from
the ${library} library is consuming significant CPU.
3. If you suggest replacing the function or library with a more efficient replacement you must suggest at least
one concrete replacement.
If you know of fewer than five ways to improve the performance of a system in which the ${functionName} function from the
${library} library is consuming significant CPU, then provide fewer than five suggestions. If you do not know of any
way in which to improve the performance then say "I do not know how to improve the performance of systems where
this function is consuming a significant amount of CPU".
Do not suggest using a CPU profiler. I have already profiled my code. The profiler I used is Elastic Universal Profiler.
If there is specific information I should look for in the profiler output then tell me what information to look for
in the output of Elastic Universal Profiler.
You must not include URLs, web addresses or websites of any kind in your output.
If you have suggestions, the output format should look as follows:
Here are some suggestions as to how you might optimize your system if ${functionName} in ${library} is consuming
significant CPU resources:
1. Insert first suggestion
2. Insert second suggestion`,
},
},
];
}
return undefined;
}, [frame?.functionName, frame?.exeFileName]);
export interface Props {
frame?: Frame;
totalSamples: number;
totalSeconds: number;
showAIAssistant?: boolean;
showSymbolsStatus?: boolean;
}
export function FrameInformationWindow({
frame,
totalSamples,
totalSeconds,
showAIAssistant = true,
showSymbolsStatus = true,
}: Props) {
if (!frame) {
return (
<FrameInformationPanel>
@ -170,23 +95,16 @@ export function FrameInformationWindow({ frame, totalSamples, totalSeconds }: Pr
<EuiFlexItem>
<KeyValueList data-test-subj="informationRows" rows={informationRows} />
</EuiFlexItem>
{aiAssistant.isEnabled() && promptMessages ? (
<>
<EuiFlexItem>
<ContextualInsight
messages={promptMessages}
title={i18n.translate('xpack.profiling.frameInformationWindow.optimizeFunction', {
defaultMessage: 'Optimize function',
})}
/>
</EuiFlexItem>
</>
) : undefined}
{symbolStatus !== FrameSymbolStatus.SYMBOLIZED && (
{showAIAssistant ? (
<EuiFlexItem>
<FrameInformationAIAssistant frame={frame} />
</EuiFlexItem>
) : null}
{showSymbolsStatus && symbolStatus !== FrameSymbolStatus.SYMBOLIZED ? (
<EuiFlexItem>
<MissingSymbolsCallout frameType={frame.frameType} />
</EuiFlexItem>
)}
) : null}
<EuiFlexItem>
<EuiFlexGroup direction="column">
<EuiFlexItem>

View file

@ -8,7 +8,7 @@
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { Meta } from '@storybook/react';
import React from 'react';
import { FrameType } from '@kbn/profiling-data-access-plugin/common/profiling';
import { FrameType } from '@kbn/profiling-utils';
import { MockProfilingDependenciesStorybook } from '../contexts/profiling_dependencies/mock_profiling_dependencies_storybook';
import { MissingSymbolsCallout } from './missing_symbols_callout';

View file

@ -9,7 +9,7 @@ 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 '@kbn/profiling-data-access-plugin/common/profiling';
import { FrameType, getLanguageType } from '@kbn/profiling-utils';
import { PROFILING_FEEDBACK_LINK } from '../profiling_app_page_template';
import { useProfilingDependencies } from '../contexts/profiling_dependencies/use_profiling_dependencies';
import { useProfilingRouter } from '../../hooks/use_profiling_router';

View file

@ -6,11 +6,8 @@
*/
import { EuiFlexGroup, EuiFlexItem, EuiLink, EuiText } from '@elastic/eui';
import React from 'react';
import {
getCalleeFunction,
getCalleeSource,
StackFrameMetadata,
} from '@kbn/profiling-data-access-plugin/common/profiling';
import { getCalleeFunction, getCalleeSource } from '@kbn/profiling-utils';
import type { StackFrameMetadata } from '@kbn/profiling-utils';
interface Props {
frame: StackFrameMetadata;

View file

@ -32,7 +32,7 @@ import {
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { StackFrameMetadata } from '@kbn/profiling-data-access-plugin/common/profiling';
import type { StackFrameMetadata } from '@kbn/profiling-utils';
import { CountPerTime, OTHER_BUCKET_LABEL, TopNSample } from '../../common/topn';
import { useKibanaTimeZoneSetting } from '../hooks/use_kibana_timezone_setting';
import { useProfilingChartsTheme } from '../hooks/use_profiling_charts_theme';

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { keyBy } from 'lodash';
import { StackFrameMetadata } from '@kbn/profiling-data-access-plugin/common/profiling';
import type { StackFrameMetadata } from '@kbn/profiling-utils';
import { TopNFunctions } from '../../../common/functions';
import { calculateImpactEstimates } from '../../../common/calculate_impact_estimates';

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 { EuiLoadingChart } from '@elastic/eui';
import React from 'react';
interface Props {
isLoading: boolean;
children: React.ReactElement;
}
export function AsyncEmbeddableComponent({ children, isLoading }: Props) {
return (
<>
{isLoading ? (
<div
style={{
width: '100%',
height: '100%',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
}}
>
<EuiLoadingChart size="xl" />
</div>
) : (
<>{children}</>
)}
</>
);
}

View file

@ -0,0 +1,50 @@
/*
* 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 { Embeddable, EmbeddableOutput } from '@kbn/embeddable-plugin/public';
import { EMBEDDABLE_FLAMEGRAPH } from '@kbn/observability-shared-plugin/public';
import React from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
import { createFlameGraph } from '@kbn/profiling-utils';
import { FlameGraph } from '../../components/flamegraph';
import { EmbeddableFlamegraphEmbeddableInput } from './embeddable_flamegraph_factory';
import { AsyncEmbeddableComponent } from '../async_embeddable_component';
export class EmbeddableFlamegraph extends Embeddable<
EmbeddableFlamegraphEmbeddableInput,
EmbeddableOutput
> {
readonly type = EMBEDDABLE_FLAMEGRAPH;
private _domNode?: HTMLElement;
render(domNode: HTMLElement) {
this._domNode = domNode;
const { data, isLoading } = this.input;
const flamegraph = !isLoading && data ? createFlameGraph(data) : undefined;
render(
<AsyncEmbeddableComponent isLoading={isLoading}>
<>
{flamegraph && (
<FlameGraph primaryFlamegraph={flamegraph} id="embddable_profiling" isEmbedded />
)}
</>
</AsyncEmbeddableComponent>,
domNode
);
}
public destroy() {
if (this._domNode) {
unmountComponentAtNode(this._domNode);
}
}
reload() {
if (this._domNode) {
this.render(this._domNode);
}
}
}

View file

@ -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 {
IContainer,
EmbeddableInput,
EmbeddableFactoryDefinition,
} from '@kbn/embeddable-plugin/public';
import type { BaseFlameGraph } from '@kbn/profiling-utils';
import { EMBEDDABLE_FLAMEGRAPH } from '@kbn/observability-shared-plugin/public';
interface EmbeddableFlamegraphInput {
data?: BaseFlameGraph;
isLoading: boolean;
}
export type EmbeddableFlamegraphEmbeddableInput = EmbeddableFlamegraphInput & EmbeddableInput;
export class EmbeddableFlamegraphFactory
implements EmbeddableFactoryDefinition<EmbeddableFlamegraphEmbeddableInput>
{
readonly type = EMBEDDABLE_FLAMEGRAPH;
async isEditable() {
return false;
}
async create(input: EmbeddableFlamegraphEmbeddableInput, parent?: IContainer) {
const { EmbeddableFlamegraph } = await import('./embeddable_flamegraph');
return new EmbeddableFlamegraph(input, {}, parent);
}
getDisplayName() {
return 'Universal Profiling Flamegraph';
}
}

View file

@ -16,11 +16,13 @@ import { i18n } from '@kbn/i18n';
import type { NavigationSection } from '@kbn/observability-shared-plugin/public';
import type { Location } from 'history';
import { BehaviorSubject, combineLatest, from, map } from 'rxjs';
import { EMBEDDABLE_FLAMEGRAPH } from '@kbn/observability-shared-plugin/public';
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';
import { EmbeddableFlamegraphFactory } from './embeddables/flamegraph/embeddable_flamegraph_factory';
export type ProfilingPluginSetup = ReturnType<ProfilingPlugin['setup']>;
export type ProfilingPluginStart = void;
@ -130,6 +132,11 @@ export class ProfilingPlugin implements Plugin {
},
});
pluginsSetup.embeddable.registerEmbeddableFactory(
EMBEDDABLE_FLAMEGRAPH,
new EmbeddableFlamegraphFactory()
);
return {
locators: {
flamegraphLocator: pluginsSetup.share.url.locators.create(

View file

@ -9,10 +9,7 @@ import { toNumberRt } from '@kbn/io-ts-utils';
import { createRouter, Outlet } from '@kbn/typed-react-router-config';
import * as t from 'io-ts';
import React from 'react';
import {
StackTracesDisplayOption,
TopNType,
} from '@kbn/profiling-data-access-plugin/common/stack_traces';
import { StackTracesDisplayOption, TopNType } from '@kbn/profiling-utils';
import { TopNFunctionSortField, topNFunctionSortFieldRt } from '../../common/functions';
import {
indexLifecyclePhaseRt,

View file

@ -6,10 +6,10 @@
*/
import { HttpFetchQuery } from '@kbn/core/public';
import {
BaseFlameGraph,
createFlameGraph,
ElasticFlameGraph,
} from '@kbn/profiling-data-access-plugin/common/flamegraph';
type BaseFlameGraph,
type ElasticFlameGraph,
} from '@kbn/profiling-utils';
import { getRoutePaths } from '../common';
import { TopNFunctions } from '../common/functions';
import type {
@ -106,6 +106,7 @@ export function getServices(): Services {
timeTo,
kuery,
};
const baseFlamegraph = (await http.get(paths.Flamechart, { query })) as BaseFlameGraph;
return createFlameGraph(baseFlamegraph);
},

View file

@ -24,6 +24,7 @@ import {
ObservabilityAIAssistantPluginSetup,
ObservabilityAIAssistantPluginStart,
} from '@kbn/observability-ai-assistant-plugin/public';
import { EmbeddableSetup } from '@kbn/embeddable-plugin/public';
export interface ProfilingPluginPublicSetupDeps {
observability: ObservabilityPublicSetup;
@ -34,6 +35,7 @@ export interface ProfilingPluginPublicSetupDeps {
charts: ChartsPluginSetup;
licensing: LicensingPluginSetup;
share: SharePluginSetup;
embeddable: EmbeddableSetup;
}
export interface ProfilingPluginPublicStartDeps {

View file

@ -8,8 +8,8 @@ import { ColumnarViewModel } from '@elastic/charts';
import { i18n } from '@kbn/i18n';
import d3 from 'd3';
import { compact, range, sum, uniqueId } from 'lodash';
import { describeFrameType, FrameType } from '@kbn/profiling-data-access-plugin/common/profiling';
import { ElasticFlameGraph } from '@kbn/profiling-data-access-plugin/common/flamegraph';
import { describeFrameType, FrameType } from '@kbn/profiling-utils';
import type { ElasticFlameGraph } from '@kbn/profiling-utils';
import { createColumnarViewModel } from '../../../common/columnar_view_model';
import { FRAME_TYPE_COLOR_MAP, rgbToRGBA } from '../../../common/frame_type_colors';
import { ComparisonMode } from '../../components/normalization_menu';

View file

@ -5,14 +5,14 @@
* 2.0.
*/
import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui';
import React, { useState } from 'react';
import React from 'react';
import { AsyncComponent } from '../../../components/async_component';
import { useProfilingDependencies } from '../../../components/contexts/profiling_dependencies/use_profiling_dependencies';
import { FlameGraph } from '../../../components/flamegraph';
import { NormalizationMode, NormalizationOptions } from '../../../components/normalization_menu';
import { useProfilingParams } from '../../../hooks/use_profiling_params';
import { useProfilingRouter } from '../../../hooks/use_profiling_router';
import { useProfilingRoutePath } from '../../../hooks/use_profiling_route_path';
import { useProfilingRouter } from '../../../hooks/use_profiling_router';
import { useTimeRange } from '../../../hooks/use_time_range';
import { useTimeRangeAsync } from '../../../hooks/use_time_range_async';
import { DifferentialFlameGraphSearchPanel } from './differential_flame_graph_search_panel';
@ -36,7 +36,6 @@ export function DifferentialFlameGraphsView() {
} = useProfilingParams('/flamegraphs/differential');
const routePath = useProfilingRoutePath();
const profilingRouter = useProfilingRouter();
const [showInformationWindow, setShowInformationWindow] = useState(false);
const timeRange = useTimeRange({ rangeFrom, rangeTo });
@ -55,15 +54,15 @@ export function DifferentialFlameGraphsView() {
return Promise.all([
fetchElasticFlamechart({
http,
timeFrom: timeRange.inSeconds.start,
timeTo: timeRange.inSeconds.end,
timeFrom: new Date(timeRange.start).getTime(),
timeTo: new Date(timeRange.end).getTime(),
kuery,
}),
comparisonTimeRange.inSeconds.start && comparisonTimeRange.inSeconds.end
comparisonTimeRange.start && comparisonTimeRange.end
? fetchElasticFlamechart({
http,
timeFrom: comparisonTimeRange.inSeconds.start,
timeTo: comparisonTimeRange.inSeconds.end,
timeFrom: new Date(comparisonTimeRange.start).getTime(),
timeTo: new Date(comparisonTimeRange.end).getTime(),
kuery: comparisonKuery,
})
: Promise.resolve(undefined),
@ -75,13 +74,13 @@ export function DifferentialFlameGraphsView() {
});
},
[
timeRange.inSeconds.start,
timeRange.inSeconds.end,
kuery,
comparisonTimeRange.inSeconds.start,
comparisonTimeRange.inSeconds.end,
comparisonKuery,
fetchElasticFlamechart,
timeRange.start,
timeRange.end,
kuery,
comparisonTimeRange.start,
comparisonTimeRange.end,
comparisonKuery,
]
);
@ -105,10 +104,6 @@ export function DifferentialFlameGraphsView() {
const isNormalizedByTime = normalizationMode === NormalizationMode.Time;
function toggleShowInformationWindow() {
setShowInformationWindow((prev) => !prev);
}
function handleSearchTextChange(newSearchText: string) {
// @ts-expect-error Code gets too complicated to satisfy TS constraints
profilingRouter.push(routePath, { query: { ...query, searchText: newSearchText } });
@ -134,8 +129,6 @@ export function DifferentialFlameGraphsView() {
comparisonMode={comparisonMode}
baseline={isNormalizedByTime ? baselineTime : baseline}
comparison={isNormalizedByTime ? comparisonTime : comparison}
showInformationWindow={showInformationWindow}
toggleShowInformationWindow={toggleShowInformationWindow}
searchText={searchText}
onChangeSearchText={handleSearchTextChange}
/>

View file

@ -5,13 +5,13 @@
* 2.0.
*/
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import React, { useState } from 'react';
import React from 'react';
import { AsyncComponent } from '../../../components/async_component';
import { useProfilingDependencies } from '../../../components/contexts/profiling_dependencies/use_profiling_dependencies';
import { FlameGraph } from '../../../components/flamegraph';
import { useProfilingParams } from '../../../hooks/use_profiling_params';
import { useProfilingRouter } from '../../../hooks/use_profiling_router';
import { useProfilingRoutePath } from '../../../hooks/use_profiling_route_path';
import { useProfilingRouter } from '../../../hooks/use_profiling_router';
import { useTimeRange } from '../../../hooks/use_time_range';
import { useTimeRangeAsync } from '../../../hooks/use_time_range_async';
@ -31,12 +31,12 @@ export function FlameGraphView() {
({ http }) => {
return fetchElasticFlamechart({
http,
timeFrom: timeRange.inSeconds.start,
timeTo: timeRange.inSeconds.end,
timeFrom: new Date(timeRange.start).getTime(),
timeTo: new Date(timeRange.end).getTime(),
kuery,
});
},
[timeRange.inSeconds.start, timeRange.inSeconds.end, kuery, fetchElasticFlamechart]
[fetchElasticFlamechart, timeRange.start, timeRange.end, kuery]
);
const { data } = state;
@ -45,11 +45,6 @@ export function FlameGraphView() {
const profilingRouter = useProfilingRouter();
const [showInformationWindow, setShowInformationWindow] = useState(false);
function toggleShowInformationWindow() {
setShowInformationWindow((prev) => !prev);
}
function handleSearchTextChange(newSearchText: string) {
// @ts-expect-error Code gets too complicated to satisfy TS constraints
profilingRouter.push(routePath, { query: { ...query, searchText: newSearchText } });
@ -62,8 +57,6 @@ export function FlameGraphView() {
<FlameGraph
id="flamechart"
primaryFlamegraph={data}
showInformationWindow={showInformationWindow}
toggleShowInformationWindow={toggleShowInformationWindow}
searchText={searchText}
onChangeSearchText={handleSearchTextChange}
/>

View file

@ -8,7 +8,7 @@
import { EuiPageHeaderContentProps } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { TypeOf } from '@kbn/typed-react-router-config';
import { TopNType } from '@kbn/profiling-data-access-plugin/common/stack_traces';
import { TopNType } from '@kbn/profiling-utils';
import { StatefulProfilingRouter } from '../../hooks/use_profiling_router';
import { ProfilingRoutes } from '../../routing';

View file

@ -7,10 +7,7 @@
import { EuiButton, EuiButtonGroup, EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import {
StackTracesDisplayOption,
TopNType,
} from '@kbn/profiling-data-access-plugin/common/stack_traces';
import { StackTracesDisplayOption, TopNType } from '@kbn/profiling-utils';
import { groupSamplesByCategory, TopNResponse } from '../../../common/topn';
import { useProfilingParams } from '../../hooks/use_profiling_params';
import { useProfilingRouter } from '../../hooks/use_profiling_router';

View file

@ -4,10 +4,7 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import {
StackTracesDisplayOption,
TopNType,
} from '@kbn/profiling-data-access-plugin/common/stack_traces';
import { StackTracesDisplayOption, TopNType } from '@kbn/profiling-utils';
import { getTracesViewRouteParams } from './utils';
describe('stack traces view utils', () => {

View file

@ -6,10 +6,7 @@
*/
import { TypeOf } from '@kbn/typed-react-router-config';
import {
getFieldNameForTopNType,
TopNType,
} from '@kbn/profiling-data-access-plugin/common/stack_traces';
import { getFieldNameForTopNType, TopNType } from '@kbn/profiling-utils';
import { ProfilingRoutes } from '../../routing';
export function getTracesViewRouteParams({

View file

@ -9,10 +9,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiLink, EuiPanel, EuiStat, EuiText } from '
import { i18n } from '@kbn/i18n';
import { asDynamicBytes } from '@kbn/observability-plugin/common';
import React from 'react';
import {
StackTracesDisplayOption,
TopNType,
} from '@kbn/profiling-data-access-plugin/common/stack_traces';
import { StackTracesDisplayOption, TopNType } from '@kbn/profiling-utils';
import { StorageExplorerSummaryAPIResponse } from '../../../common/storage_explorer';
import { useProfilingDependencies } from '../../components/contexts/profiling_dependencies/use_profiling_dependencies';
import { LabelWithHint } from '../../components/label_with_hint';

View file

@ -38,8 +38,8 @@ export function registerFlameChartSearchRoute({
const esClient = await getClient(context);
const flamegraph = await profilingDataAccess.services.fetchFlamechartData({
esClient,
rangeFrom: timeFrom,
rangeTo: timeTo,
rangeFromMs: timeFrom,
rangeToMs: timeTo,
kuery,
});

View file

@ -7,7 +7,7 @@
import { QueryDslBoolQuery } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { kqlQuery } from '@kbn/observability-plugin/server';
import { ProfilingESField } from '@kbn/profiling-data-access-plugin/common/elasticsearch';
import { ProfilingESField } from '@kbn/profiling-utils';
export interface ProjectTimeQuery {
bool: QueryDslBoolQuery;

View file

@ -4,7 +4,7 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { decodeStackTraceResponse } from '@kbn/profiling-data-access-plugin/common/stack_traces';
import { decodeStackTraceResponse } from '@kbn/profiling-utils';
import { ProfilingESClient } from '../utils/create_profiling_es_client';
import { ProjectTimeQuery } from './query';

View file

@ -6,7 +6,7 @@
*/
import { kqlQuery, termQuery } from '@kbn/observability-plugin/server';
import { ProfilingESField } from '@kbn/profiling-data-access-plugin/common/elasticsearch';
import { ProfilingESField } from '@kbn/profiling-utils';
import { computeBucketWidthFromTimeRangeAndBucketCount } from '../../../common/histogram';
import {
IndexLifecyclePhaseSelectOption,

View file

@ -6,7 +6,7 @@
*/
import { kqlQuery, termQuery } from '@kbn/observability-plugin/server';
import { ProfilingESField } from '@kbn/profiling-data-access-plugin/common/elasticsearch';
import { ProfilingESField } from '@kbn/profiling-utils';
import {
IndexLifecyclePhaseSelectOption,
indexLifeCyclePhaseToDataTier,

View file

@ -6,7 +6,7 @@
*/
import { kqlQuery } from '@kbn/observability-plugin/server';
import { keyBy } from 'lodash';
import { ProfilingESField } from '@kbn/profiling-data-access-plugin/common/elasticsearch';
import { ProfilingESField } from '@kbn/profiling-utils';
import { ProfilingESClient } from '../../utils/create_profiling_es_client';
interface HostDetails {

View file

@ -8,7 +8,7 @@
import { AggregationsAggregationContainer } from '@elastic/elasticsearch/lib/api/types';
import { coreMock } from '@kbn/core/server/mocks';
import { loggerMock } from '@kbn/logging-mocks';
import { ProfilingESField } from '@kbn/profiling-data-access-plugin/common/elasticsearch';
import { ProfilingESField } from '@kbn/profiling-utils';
import { ProfilingESClient } from '../utils/create_profiling_es_client';
import { topNElasticSearchQuery } from './topn';

View file

@ -7,18 +7,18 @@
import { schema } from '@kbn/config-schema';
import type { Logger } from '@kbn/core/server';
import { ProfilingESField } from '@kbn/profiling-data-access-plugin/common/elasticsearch';
import { groupStackFrameMetadataByStackTrace } from '@kbn/profiling-data-access-plugin/common/profiling';
import {
getFieldNameForTopNType,
groupStackFrameMetadataByStackTrace,
ProfilingESField,
TopNType,
} from '@kbn/profiling-data-access-plugin/common/stack_traces';
import { getRoutePaths, INDEX_EVENTS } from '../../common';
} from '@kbn/profiling-utils';
import { RouteRegisterParameters } from '.';
import { getRoutePaths, INDEX_EVENTS } from '../../common';
import { computeBucketWidthFromTimeRangeAndBucketCount } from '../../common/histogram';
import { createTopNSamples, getTopNAggregationRequest, TopNResponse } from '../../common/topn';
import { handleRouteHandlerError } from '../utils/handle_route_error_handler';
import { ProfilingESClient } from '../utils/create_profiling_es_client';
import { handleRouteHandlerError } from '../utils/handle_route_error_handler';
import { withProfilingSpan } from '../utils/with_profiling_span';
import { getClient } from './compat';
import { findDownsampledIndex } from './downsampling';

View file

@ -10,10 +10,7 @@ import type { ESSearchRequest, InferSearchResponseOf } from '@kbn/es-types';
import type { KibanaRequest } from '@kbn/core/server';
import { unwrapEsResponse } from '@kbn/observability-plugin/server';
import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import {
ProfilingStatusResponse,
StackTraceResponse,
} from '@kbn/profiling-data-access-plugin/common/stack_traces';
import type { ProfilingStatusResponse, StackTraceResponse } from '@kbn/profiling-utils';
import { withProfilingSpan } from './with_profiling_span';
export function cancelEsRequestOnAbort<T extends Promise<any>>(

View file

@ -48,7 +48,9 @@
"@kbn/utility-types",
"@kbn/usage-collection-plugin",
"@kbn/observability-ai-assistant-plugin",
"@kbn/profiling-data-access-plugin"
"@kbn/profiling-data-access-plugin",
"@kbn/embeddable-plugin",
"@kbn/profiling-utils"
// add references to other TypeScript projects the plugin depends on
// requiredPlugins from ./kibana.json

View file

@ -6,31 +6,33 @@
*/
import { ElasticsearchClient } from '@kbn/core/server';
import { RegisterServicesParams } from '../register_services';
import { createBaseFlameGraph, createCalleeTree } from '@kbn/profiling-utils';
import { withProfilingSpan } from '../../utils/with_profiling_span';
import { RegisterServicesParams } from '../register_services';
import { searchStackTraces } from '../search_stack_traces';
import { createCalleeTree } from '../../../common/callee';
import { createBaseFlameGraph } from '../../../common/flamegraph';
interface FetchFlamechartParams {
export interface FetchFlamechartParams {
esClient: ElasticsearchClient;
rangeFrom: number;
rangeTo: number;
rangeFromMs: number;
rangeToMs: number;
kuery: string;
}
export function createFetchFlamechart({ createProfilingEsClient }: RegisterServicesParams) {
return async ({ esClient, rangeFrom, rangeTo, kuery }: FetchFlamechartParams) => {
return async ({ esClient, rangeFromMs, rangeToMs, kuery }: FetchFlamechartParams) => {
const rangeFromSecs = rangeFromMs / 1000;
const rangeToSecs = rangeToMs / 1000;
const profilingEsClient = createProfilingEsClient({ esClient });
const targetSampleSize = 20000; // minimum number of samples to get statistically sound results
const totalSeconds = rangeTo - rangeFrom;
const totalSeconds = rangeToSecs - rangeFromSecs;
const { events, stackTraces, executables, stackFrames, totalFrames, samplingRate } =
await searchStackTraces({
client: profilingEsClient,
rangeFrom,
rangeTo,
rangeFrom: rangeFromSecs,
rangeTo: rangeToSecs,
kuery,
sampleSize: targetSampleSize,
});
@ -45,9 +47,7 @@ export function createFetchFlamechart({ createProfilingEsClient }: RegisterServi
samplingRate
);
const fg = createBaseFlameGraph(tree, samplingRate, totalSeconds);
return fg;
return createBaseFlameGraph(tree, samplingRate, totalSeconds);
});
return flamegraph;

View file

@ -7,7 +7,7 @@
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query';
import { decodeStackTraceResponse } from '../../../common/stack_traces';
import { decodeStackTraceResponse } from '@kbn/profiling-utils';
import { ProfilingESClient } from '../../utils/create_profiling_es_client';
export async function searchStackTraces({

View file

@ -8,8 +8,8 @@
import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { ElasticsearchClient } from '@kbn/core/server';
import type { ESSearchRequest, InferSearchResponseOf } from '@kbn/es-types';
import { unwrapEsResponse } from '@kbn/observability-plugin/server';
import { ProfilingStatusResponse, StackTraceResponse } from '../../common/stack_traces';
import type { ProfilingStatusResponse, StackTraceResponse } from '@kbn/profiling-utils';
import { unwrapEsResponse } from './unwrap_es_response';
import { withProfilingSpan } from './with_profiling_span';
export interface ProfilingESClient {

View file

@ -0,0 +1,47 @@
/*
* 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 { errors } from '@elastic/elasticsearch';
import { inspect } from 'util';
export class WrappedElasticsearchClientError extends Error {
originalError: errors.ElasticsearchClientError;
constructor(originalError: errors.ElasticsearchClientError) {
super(originalError.message);
const stack = this.stack;
this.originalError = originalError;
if (originalError instanceof errors.ResponseError) {
// make sure ES response body is visible when logged to the console
// @ts-expect-error
this.stack = {
valueOf() {
const value = stack?.valueOf() ?? '';
return value;
},
toString() {
const value =
stack?.toString() +
`\nResponse: ${inspect(originalError.meta.body, { depth: null })}\n`;
return value;
},
};
}
}
}
export function unwrapEsResponse<T extends Promise<{ body: any }>>(
responsePromise: T
): Promise<Awaited<T>['body']> {
return responsePromise
.then((res) => res.body)
.catch((err) => {
// make sure stacktrace is relative to where client was called
throw new WrappedElasticsearchClientError(err);
});
}

View file

@ -5,8 +5,6 @@
},
"include": [
"server/**/*",
"common/**/*.ts",
"common/**/*.json",
"jest.config.js"
],
"exclude": [
@ -17,7 +15,7 @@
"@kbn/core",
"@kbn/es-query",
"@kbn/es-types",
"@kbn/observability-plugin",
"@kbn/apm-utils"
"@kbn/apm-utils",
"@kbn/profiling-utils"
]
}

View file

@ -5142,6 +5142,10 @@
version "0.0.0"
uid ""
"@kbn/profiling-utils@link:packages/kbn-profiling-utils":
version "0.0.0"
uid ""
"@kbn/random-sampling@link:x-pack/packages/kbn-random-sampling":
version "0.0.0"
uid ""