Skip to content

Commit

Permalink
[ObsUX] Add Top Functions to Infra Profilling tab (#171974)
Browse files Browse the repository at this point in the history
Closes #171962

## Summary

This integrated Profiling Top Functions embeddable into the Infra's
Profiling tab in asset details.

![CleanShot 2023-11-28 at 14 20
38@2x](https://github.com/elastic/kibana/assets/793851/408ca866-1bc9-4b66-9ba1-d090cce0f7da)

## How to Test

* Connect local kibana to oblt cluster that has Profiling configured
(e.g. edge)
* Add this to your dev `kibana.yml`
```
xpack.profiling.enabled: true
xpack.infra.profilingEnabled: true

# Direct ES URL on the oblt cluster that you're using, in case of edge it's https://edge-oblt.es.us-west2.gcp.elastic-cloud.com:443
xpack.profiling.elasticsearch.hosts: REMOTE_CLUSTER_ES_URL

# If needed create a new user on the remote oblt cluster
xpack.profiling.elasticsearch.username: REMOTE_CLUSTER_USER
xpack.profiling.elasticsearch.password: REMOTE_CLUSTER_PASWORD
```

* Open kibana, go to Hosts
* Open a flyout for one of the hosts and make sure you see the Profiling
tab with both Flamegraph and Top Functions
* Open Host details as a full page and also make sure you see the same
* Make sure Profiling data updates when you change dates in the date
picker
  • Loading branch information
mykolaharmash authored Nov 29, 2023
1 parent c662bb3 commit ad2ca24
Show file tree
Hide file tree
Showing 12 changed files with 365 additions and 91 deletions.
24 changes: 18 additions & 6 deletions x-pack/plugins/infra/common/http_api/profiling_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,24 @@

import * as rt from 'io-ts';

export const InfraProfilingRequestParamsRT = rt.type({
export const InfraProfilingFlamegraphRequestParamsRT = rt.type({
hostname: rt.string,
timeRange: rt.type({
from: rt.number,
to: rt.number,
}),
from: rt.number,
to: rt.number,
});

export type InfraProfilingRequestParams = rt.TypeOf<typeof InfraProfilingRequestParamsRT>;
export const InfraProfilingFunctionsRequestParamsRT = rt.type({
hostname: rt.string,
from: rt.number,
to: rt.number,
startIndex: rt.number,
endIndex: rt.number,
});

export type InfraProfilingFlamegraphRequestParams = rt.TypeOf<
typeof InfraProfilingFlamegraphRequestParamsRT
>;

export type InfraProfilingFunctionsRequestParams = rt.TypeOf<
typeof InfraProfilingFunctionsRequestParamsRT
>;
Original file line number Diff line number Diff line change
@@ -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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { useEffect, useMemo } from 'react';
import type { BaseFlameGraph } from '@kbn/profiling-utils';
import { type InfraProfilingFlamegraphRequestParams } from '../../../../common/http_api/profiling_api';
import { useHTTPRequest } from '../../../hooks/use_http_request';
import { useRequestObservable } from './use_request_observable';

interface Props {
params: InfraProfilingFlamegraphRequestParams;
isActive: boolean;
}

export function useProfilingFlamegraphData({ params, isActive }: Props) {
const { request$ } = useRequestObservable<BaseFlameGraph>();
const fetchOptions = useMemo(() => ({ query: params }), [params]);
const { loading, error, response, makeRequest } = useHTTPRequest<BaseFlameGraph>(
`/api/infra/profiling/flamegraph`,
'GET',
undefined,
undefined,
undefined,
undefined,
true,
fetchOptions
);

useEffect(() => {
if (!isActive) {
return;
}

request$.next(makeRequest);
}, [isActive, makeRequest, request$]);

return {
loading,
error,
response,
};
}
Original file line number Diff line number Diff line change
@@ -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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { useEffect, useMemo } from 'react';
import type { TopNFunctions } from '@kbn/profiling-utils';
import { type InfraProfilingFunctionsRequestParams } from '../../../../common/http_api/profiling_api';
import { useHTTPRequest } from '../../../hooks/use_http_request';
import { useRequestObservable } from './use_request_observable';

interface Props {
params: InfraProfilingFunctionsRequestParams;
isActive: boolean;
}

export function useProfilingFunctionsData({ params, isActive }: Props) {
const { request$ } = useRequestObservable<TopNFunctions>();
const fetchOptions = useMemo(() => ({ query: params }), [params]);
const { loading, error, response, makeRequest } = useHTTPRequest<TopNFunctions>(
'/api/infra/profiling/functions',
'GET',
undefined,
undefined,
undefined,
undefined,
true,
fetchOptions
);

useEffect(() => {
if (!isActive) {
return;
}

request$.next(makeRequest);
}, [isActive, makeRequest, request$]);

return {
loading,
error,
response,
};
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { EuiEmptyPrompt } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';

export function ErrorPrompt() {
return (
<EuiEmptyPrompt
color="warning"
iconType="warning"
titleSize="xs"
title={
<h2>
{i18n.translate('xpack.infra.profiling.loadErrorTitle', {
defaultMessage: 'Unable to load the Profiling data',
})}
</h2>
}
body={
<p>
{i18n.translate('xpack.infra.profiling.loadErrorBody', {
defaultMessage:
'There was an error while trying to load profiling data. Try refreshing the page',
})}
</p>
}
/>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* 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, { useMemo } from 'react';
import { EmbeddableFlamegraph } from '@kbn/observability-shared-plugin/public';
import { useAssetDetailsRenderPropsContext } from '../../hooks/use_asset_details_render_props';
import { useDatePickerContext } from '../../hooks/use_date_picker';
import { useProfilingFlamegraphData } from '../../hooks/use_profiling_flamegraph_data';
import { useTabSwitcherContext } from '../../hooks/use_tab_switcher';
import { ContentTabIds } from '../../types';
import { ErrorPrompt } from './error_prompt';

export function Flamegraph() {
const { asset } = useAssetDetailsRenderPropsContext();
const { activeTabId } = useTabSwitcherContext();
const { getDateRangeInTimestamp } = useDatePickerContext();
const { from, to } = getDateRangeInTimestamp();

const params = useMemo(
() => ({
hostname: asset.name,
from,
to,
}),
[asset.name, from, to]
);
const { error, loading, response } = useProfilingFlamegraphData({
isActive: activeTabId === ContentTabIds.PROFILING,
params,
});

if (error !== null) {
return <ErrorPrompt />;
}

return <EmbeddableFlamegraph data={response ?? undefined} isLoading={loading} height="60vh" />;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* 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, { useMemo } from 'react';
import { EmbeddableFunctions } from '@kbn/observability-shared-plugin/public';
import { useAssetDetailsRenderPropsContext } from '../../hooks/use_asset_details_render_props';
import { useDatePickerContext } from '../../hooks/use_date_picker';
import { useProfilingFunctionsData } from '../../hooks/use_profiling_functions_data';
import { useTabSwitcherContext } from '../../hooks/use_tab_switcher';
import { ContentTabIds } from '../../types';
import { ErrorPrompt } from './error_prompt';

export function Functions() {
const { asset } = useAssetDetailsRenderPropsContext();
const { activeTabId } = useTabSwitcherContext();
const { getDateRangeInTimestamp } = useDatePickerContext();
const { from, to } = getDateRangeInTimestamp();

const params = useMemo(
() => ({
hostname: asset.name,
from,
to,
startIndex: 0,
endIndex: 10,
}),
[asset.name, from, to]
);

const { error, loading, response } = useProfilingFunctionsData({
isActive: activeTabId === ContentTabIds.PROFILING,
params,
});

if (error !== null) {
return <ErrorPrompt />;
}

return (
<EmbeddableFunctions
data={response ?? undefined}
isLoading={loading}
rangeFrom={from}
rangeTo={to}
/>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,44 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { i18n } from '@kbn/i18n';

import { EuiSpacer, EuiTabbedContent, type EuiTabbedContentProps } from '@elastic/eui';
import React from 'react';
import { EmbeddableFlamegraph } from '@kbn/observability-shared-plugin/public';
import { BaseFlameGraph } from '@kbn/profiling-utils';
import { useAssetDetailsRenderPropsContext } from '../../hooks/use_asset_details_render_props';
import { useDatePickerContext } from '../../hooks/use_date_picker';
import { useProfilingFlamegraphData } from '../../hooks/use_profilling_flamegraph_data';
import { useRequestObservable } from '../../hooks/use_request_observable';
import { useTabSwitcherContext } from '../../hooks/use_tab_switcher';
import { ContentTabIds } from '../../types';
import { Flamegraph } from './flamegraph';
import { Functions } from './functions';

export function Profiling() {
const { request$ } = useRequestObservable<BaseFlameGraph>();
const { asset } = useAssetDetailsRenderPropsContext();
const { activeTabId } = useTabSwitcherContext();
const { getDateRangeInTimestamp } = useDatePickerContext();
const { loading, response } = useProfilingFlamegraphData({
active: activeTabId === ContentTabIds.PROFILING,
request$,
hostname: asset.name,
timeRange: getDateRangeInTimestamp(),
});
const tabs: EuiTabbedContentProps['tabs'] = [
{
id: 'flamegraph',
name: i18n.translate('xpack.infra.profiling.flamegraphTabName', {
defaultMessage: 'Flamegraph',
}),
content: (
<>
<EuiSpacer />
<Flamegraph />
</>
),
},
{
id: 'functions',
name: i18n.translate('xpack.infra.tabs.profiling.functionsTabName', {
defaultMessage: 'Top 10 Functions',
}),
content: (
<>
<EuiSpacer />
<Functions />
</>
),
},
];

return <EmbeddableFlamegraph data={response ?? undefined} isLoading={loading} height="60vh" />;
return (
<>
<EuiTabbedContent tabs={tabs} initialSelectedTab={tabs[0]} />
</>
);
}
Loading

0 comments on commit ad2ca24

Please sign in to comment.