mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Metrics UI] Refactor Process List API to fetch only top processes (#84716)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
a7c5b49343
commit
de289de6c1
14 changed files with 859 additions and 372 deletions
|
@ -5,16 +5,139 @@
|
|||
*/
|
||||
|
||||
import * as rt from 'io-ts';
|
||||
import { MetricsAPITimerangeRT, MetricsAPISeriesRT } from '../metrics_api';
|
||||
import { MetricsAPISeriesRT, MetricsAPIRow } from '../metrics_api';
|
||||
|
||||
const AggValueRT = rt.type({
|
||||
value: rt.number,
|
||||
});
|
||||
|
||||
export const ProcessListAPIRequestRT = rt.type({
|
||||
hostTerm: rt.record(rt.string, rt.string),
|
||||
timerange: MetricsAPITimerangeRT,
|
||||
timefield: rt.string,
|
||||
indexPattern: rt.string,
|
||||
to: rt.number,
|
||||
sortBy: rt.type({
|
||||
name: rt.string,
|
||||
isAscending: rt.boolean,
|
||||
}),
|
||||
searchFilter: rt.array(rt.record(rt.string, rt.record(rt.string, rt.unknown))),
|
||||
});
|
||||
|
||||
export const ProcessListAPIResponseRT = rt.array(MetricsAPISeriesRT);
|
||||
export const ProcessListAPIQueryAggregationRT = rt.type({
|
||||
summaryEvent: rt.type({
|
||||
summary: rt.type({
|
||||
hits: rt.type({
|
||||
hits: rt.array(
|
||||
rt.type({
|
||||
_source: rt.type({
|
||||
system: rt.type({
|
||||
process: rt.type({
|
||||
summary: rt.record(rt.string, rt.number),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
processes: rt.type({
|
||||
filteredProcs: rt.type({
|
||||
buckets: rt.array(
|
||||
rt.type({
|
||||
key: rt.string,
|
||||
cpu: AggValueRT,
|
||||
memory: AggValueRT,
|
||||
startTime: rt.type({
|
||||
value_as_string: rt.string,
|
||||
}),
|
||||
meta: rt.type({
|
||||
hits: rt.type({
|
||||
hits: rt.array(
|
||||
rt.type({
|
||||
_source: rt.type({
|
||||
process: rt.type({
|
||||
pid: rt.number,
|
||||
}),
|
||||
system: rt.type({
|
||||
process: rt.type({
|
||||
state: rt.string,
|
||||
}),
|
||||
}),
|
||||
user: rt.type({
|
||||
name: rt.string,
|
||||
}),
|
||||
}),
|
||||
})
|
||||
),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
export const ProcessListAPIResponseRT = rt.type({
|
||||
processList: rt.array(
|
||||
rt.type({
|
||||
cpu: rt.number,
|
||||
memory: rt.number,
|
||||
startTime: rt.number,
|
||||
pid: rt.number,
|
||||
state: rt.string,
|
||||
user: rt.string,
|
||||
command: rt.string,
|
||||
})
|
||||
),
|
||||
summary: rt.record(rt.string, rt.number),
|
||||
});
|
||||
|
||||
export type ProcessListAPIQueryAggregation = rt.TypeOf<typeof ProcessListAPIQueryAggregationRT>;
|
||||
|
||||
export type ProcessListAPIRequest = rt.TypeOf<typeof ProcessListAPIRequestRT>;
|
||||
|
||||
export type ProcessListAPIResponse = rt.TypeOf<typeof ProcessListAPIResponseRT>;
|
||||
|
||||
export const ProcessListAPIChartRequestRT = rt.type({
|
||||
hostTerm: rt.record(rt.string, rt.string),
|
||||
timefield: rt.string,
|
||||
indexPattern: rt.string,
|
||||
to: rt.number,
|
||||
command: rt.string,
|
||||
});
|
||||
|
||||
export const ProcessListAPIChartQueryAggregationRT = rt.type({
|
||||
process: rt.type({
|
||||
filteredProc: rt.type({
|
||||
buckets: rt.array(
|
||||
rt.type({
|
||||
timeseries: rt.type({
|
||||
buckets: rt.array(
|
||||
rt.type({
|
||||
key: rt.number,
|
||||
memory: AggValueRT,
|
||||
cpu: AggValueRT,
|
||||
})
|
||||
),
|
||||
}),
|
||||
})
|
||||
),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
export const ProcessListAPIChartResponseRT = rt.type({
|
||||
cpu: MetricsAPISeriesRT,
|
||||
memory: MetricsAPISeriesRT,
|
||||
});
|
||||
|
||||
export type ProcessListAPIChartQueryAggregation = rt.TypeOf<
|
||||
typeof ProcessListAPIChartQueryAggregationRT
|
||||
>;
|
||||
|
||||
export type ProcessListAPIChartRequest = rt.TypeOf<typeof ProcessListAPIChartRequestRT>;
|
||||
|
||||
export type ProcessListAPIChartResponse = rt.TypeOf<typeof ProcessListAPIChartResponseRT>;
|
||||
|
||||
export type ProcessListAPIRow = MetricsAPIRow;
|
||||
|
|
|
@ -5,16 +5,28 @@
|
|||
*/
|
||||
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { debounce } from 'lodash';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiSearchBar, EuiSpacer, EuiEmptyPrompt, EuiButton, Query } from '@elastic/eui';
|
||||
import { useProcessList } from '../../../../hooks/use_process_list';
|
||||
import { EuiSearchBar, EuiSpacer, EuiEmptyPrompt, EuiButton } from '@elastic/eui';
|
||||
import {
|
||||
useProcessList,
|
||||
SortBy,
|
||||
ProcessListContextProvider,
|
||||
} from '../../../../hooks/use_process_list';
|
||||
import { TabContent, TabProps } from '../shared';
|
||||
import { STATE_NAMES } from './states';
|
||||
import { SummaryTable } from './summary_table';
|
||||
import { ProcessesTable } from './processes_table';
|
||||
import { parseSearchString } from './parse_search_string';
|
||||
|
||||
const TabComponent = ({ currentTime, node, nodeType, options }: TabProps) => {
|
||||
const [searchFilter, setSearchFilter] = useState<Query>(EuiSearchBar.Query.MATCH_ALL);
|
||||
const [searchFilter, setSearchFilter] = useState<string>('');
|
||||
const [sortBy, setSortBy] = useState<SortBy>({
|
||||
name: 'cpu',
|
||||
isAscending: false,
|
||||
});
|
||||
|
||||
const timefield = options.fields!.timestamp;
|
||||
|
||||
const hostTerm = useMemo(() => {
|
||||
const field =
|
||||
|
@ -26,69 +38,80 @@ const TabComponent = ({ currentTime, node, nodeType, options }: TabProps) => {
|
|||
|
||||
const { loading, error, response, makeRequest: reload } = useProcessList(
|
||||
hostTerm,
|
||||
'metricbeat-*',
|
||||
options.fields!.timestamp,
|
||||
currentTime
|
||||
timefield,
|
||||
currentTime,
|
||||
sortBy,
|
||||
parseSearchString(searchFilter)
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<TabContent>
|
||||
<EuiEmptyPrompt
|
||||
iconType="tableDensityNormal"
|
||||
title={
|
||||
<h4>
|
||||
{i18n.translate('xpack.infra.metrics.nodeDetails.processListError', {
|
||||
defaultMessage: 'Unable to show process data',
|
||||
})}
|
||||
</h4>
|
||||
}
|
||||
actions={
|
||||
<EuiButton color="primary" fill onClick={reload}>
|
||||
{i18n.translate('xpack.infra.metrics.nodeDetails.processListRetry', {
|
||||
defaultMessage: 'Try again',
|
||||
})}
|
||||
</EuiButton>
|
||||
}
|
||||
/>
|
||||
</TabContent>
|
||||
);
|
||||
}
|
||||
const debouncedSearchOnChange = useMemo(
|
||||
() =>
|
||||
debounce<(props: { queryText: string }) => void>(
|
||||
({ queryText }) => setSearchFilter(queryText),
|
||||
500
|
||||
),
|
||||
[setSearchFilter]
|
||||
);
|
||||
|
||||
return (
|
||||
<TabContent>
|
||||
<SummaryTable isLoading={loading} processList={response ?? []} />
|
||||
<EuiSpacer size="m" />
|
||||
<EuiSearchBar
|
||||
query={searchFilter}
|
||||
onChange={({ query }) => setSearchFilter(query ?? EuiSearchBar.Query.MATCH_ALL)}
|
||||
box={{
|
||||
incremental: true,
|
||||
placeholder: i18n.translate('xpack.infra.metrics.nodeDetails.searchForProcesses', {
|
||||
defaultMessage: 'Search for processes…',
|
||||
}),
|
||||
}}
|
||||
filters={[
|
||||
{
|
||||
type: 'field_value_selection',
|
||||
field: 'state',
|
||||
name: 'State',
|
||||
operator: 'exact',
|
||||
multiSelect: false,
|
||||
options: Object.entries(STATE_NAMES).map(([value, view]: [string, string]) => ({
|
||||
value,
|
||||
view,
|
||||
})),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<EuiSpacer size="m" />
|
||||
<ProcessesTable
|
||||
currentTime={currentTime}
|
||||
isLoading={loading || !response}
|
||||
processList={response ?? []}
|
||||
searchFilter={searchFilter}
|
||||
/>
|
||||
<ProcessListContextProvider hostTerm={hostTerm} to={currentTime} timefield={timefield}>
|
||||
<SummaryTable
|
||||
isLoading={loading}
|
||||
processSummary={(!error ? response?.summary : null) ?? { total: 0 }}
|
||||
/>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiSearchBar
|
||||
onChange={debouncedSearchOnChange}
|
||||
box={{
|
||||
incremental: true,
|
||||
placeholder: i18n.translate('xpack.infra.metrics.nodeDetails.searchForProcesses', {
|
||||
defaultMessage: 'Search for processes…',
|
||||
}),
|
||||
}}
|
||||
filters={[
|
||||
{
|
||||
type: 'field_value_selection',
|
||||
field: 'state',
|
||||
name: 'State',
|
||||
operator: 'exact',
|
||||
multiSelect: false,
|
||||
options: Object.entries(STATE_NAMES).map(([value, view]: [string, string]) => ({
|
||||
value,
|
||||
view,
|
||||
})),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<EuiSpacer size="m" />
|
||||
{!error ? (
|
||||
<ProcessesTable
|
||||
currentTime={currentTime}
|
||||
isLoading={loading || !response}
|
||||
processList={response?.processList ?? []}
|
||||
sortBy={sortBy}
|
||||
setSortBy={setSortBy}
|
||||
/>
|
||||
) : (
|
||||
<EuiEmptyPrompt
|
||||
iconType="tableDensityNormal"
|
||||
title={
|
||||
<h4>
|
||||
{i18n.translate('xpack.infra.metrics.nodeDetails.processListError', {
|
||||
defaultMessage: 'Unable to show process data',
|
||||
})}
|
||||
</h4>
|
||||
}
|
||||
actions={
|
||||
<EuiButton color="primary" fill onClick={reload}>
|
||||
{i18n.translate('xpack.infra.metrics.nodeDetails.processListRetry', {
|
||||
defaultMessage: 'Try again',
|
||||
})}
|
||||
</EuiButton>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</ProcessListContextProvider>
|
||||
</TabContent>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,55 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { ProcessListAPIResponse } from '../../../../../../../../common/http_api';
|
||||
import { Process } from './types';
|
||||
|
||||
export const parseProcessList = (processList: ProcessListAPIResponse) =>
|
||||
processList.map((process) => {
|
||||
const command = process.id;
|
||||
let mostRecentPoint;
|
||||
for (let i = process.rows.length - 1; i >= 0; i--) {
|
||||
const point = process.rows[i];
|
||||
if (point && Array.isArray(point.meta) && point.meta?.length) {
|
||||
mostRecentPoint = point;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!mostRecentPoint) return { command, cpu: null, memory: null, startTime: null, state: null };
|
||||
|
||||
const { cpu, memory } = mostRecentPoint;
|
||||
const { system, process: processMeta, user } = (mostRecentPoint.meta as any[])[0];
|
||||
const startTime = system.process.cpu.start_time;
|
||||
const state = system.process.state;
|
||||
|
||||
const timeseries = {
|
||||
cpu: pickTimeseries(process.rows, 'cpu'),
|
||||
memory: pickTimeseries(process.rows, 'memory'),
|
||||
};
|
||||
|
||||
return {
|
||||
command,
|
||||
cpu,
|
||||
memory,
|
||||
startTime,
|
||||
state,
|
||||
pid: processMeta.pid,
|
||||
user: user.name,
|
||||
timeseries,
|
||||
} as Process;
|
||||
});
|
||||
|
||||
const pickTimeseries = (rows: any[], metricID: string) => ({
|
||||
rows: rows.map((row) => ({
|
||||
timestamp: row.timestamp,
|
||||
metric_0: row[metricID],
|
||||
})),
|
||||
columns: [
|
||||
{ name: 'timestamp', type: 'date' },
|
||||
{ name: 'metric_0', type: 'number' },
|
||||
],
|
||||
id: metricID,
|
||||
});
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
export const parseSearchString = (query: string) => {
|
||||
if (query.trim() === '') {
|
||||
return [
|
||||
{
|
||||
match_all: {},
|
||||
},
|
||||
];
|
||||
}
|
||||
const elements = query
|
||||
.split(' ')
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
const stateFilter = elements.filter((s) => s.startsWith('state='));
|
||||
const cmdlineFilters = elements.filter((s) => !s.startsWith('state='));
|
||||
return [
|
||||
...cmdlineFilters.map((clause) => ({
|
||||
query_string: {
|
||||
fields: ['system.process.cmdline'],
|
||||
query: `*${escapeReservedCharacters(clause)}*`,
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
})),
|
||||
...stateFilter.map((state) => ({
|
||||
match: {
|
||||
'system.process.state': state.replace('state=', ''),
|
||||
},
|
||||
})),
|
||||
];
|
||||
};
|
||||
|
||||
const escapeReservedCharacters = (clause: string) =>
|
||||
clause.replace(/([+-=!\(\)\{\}\[\]^"~*?:\\/!]|&&|\|\|)/g, '\\$1');
|
|
@ -4,9 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import moment from 'moment';
|
||||
import { first, last } from 'lodash';
|
||||
import React, { useState } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
EuiTableRow,
|
||||
|
@ -22,18 +20,10 @@ import {
|
|||
EuiButton,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import { Axis, Chart, Settings, Position, TooltipValue, niceTimeFormatter } from '@elastic/charts';
|
||||
import { AutoSizer } from '../../../../../../../components/auto_sizer';
|
||||
import { createFormatter } from '../../../../../../../../common/formatters';
|
||||
import { useUiSetting } from '../../../../../../../../../../../src/plugins/kibana_react/public';
|
||||
import { getChartTheme } from '../../../../../metrics_explorer/components/helpers/get_chart_theme';
|
||||
import { calculateDomain } from '../../../../../metrics_explorer/components/helpers/calculate_domain';
|
||||
import { MetricsExplorerChartType } from '../../../../../metrics_explorer/hooks/use_metrics_explorer_options';
|
||||
import { MetricExplorerSeriesChart } from '../../../../../metrics_explorer/components/series_chart';
|
||||
import { MetricsExplorerAggregation } from '../../../../../../../../common/http_api';
|
||||
import { Color } from '../../../../../../../../common/color_palette';
|
||||
import { euiStyled } from '../../../../../../../../../observability/public';
|
||||
import { Process } from './types';
|
||||
import { ProcessRowCharts } from './process_row_charts';
|
||||
|
||||
interface Props {
|
||||
cells: React.ReactNode[];
|
||||
|
@ -118,26 +108,7 @@ export const ProcessRow = ({ cells, item }: Props) => {
|
|||
<CodeLine>{item.user}</CodeLine>
|
||||
</EuiDescriptionListDescription>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiDescriptionListTitle>{cpuMetricLabel}</EuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription>
|
||||
<ProcessChart
|
||||
timeseries={item.timeseries.cpu}
|
||||
color={Color.color2}
|
||||
label={cpuMetricLabel}
|
||||
/>
|
||||
</EuiDescriptionListDescription>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiDescriptionListTitle>{memoryMetricLabel}</EuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription>
|
||||
<ProcessChart
|
||||
timeseries={item.timeseries.memory}
|
||||
color={Color.color0}
|
||||
label={memoryMetricLabel}
|
||||
/>
|
||||
</EuiDescriptionListDescription>
|
||||
</EuiFlexItem>
|
||||
<ProcessRowCharts command={item.command} />
|
||||
</EuiFlexGrid>
|
||||
</EuiDescriptionList>
|
||||
</ExpandedRowCell>
|
||||
|
@ -149,76 +120,6 @@ export const ProcessRow = ({ cells, item }: Props) => {
|
|||
);
|
||||
};
|
||||
|
||||
interface ProcessChartProps {
|
||||
timeseries: Process['timeseries']['x'];
|
||||
color: Color;
|
||||
label: string;
|
||||
}
|
||||
const ProcessChart = ({ timeseries, color, label }: ProcessChartProps) => {
|
||||
const chartMetric = {
|
||||
color,
|
||||
aggregation: 'avg' as MetricsExplorerAggregation,
|
||||
label,
|
||||
};
|
||||
const isDarkMode = useUiSetting<boolean>('theme:darkMode');
|
||||
|
||||
const dateFormatter = useMemo(() => {
|
||||
if (!timeseries) return () => '';
|
||||
const firstTimestamp = first(timeseries.rows)?.timestamp;
|
||||
const lastTimestamp = last(timeseries.rows)?.timestamp;
|
||||
|
||||
if (firstTimestamp == null || lastTimestamp == null) {
|
||||
return (value: number) => `${value}`;
|
||||
}
|
||||
|
||||
return niceTimeFormatter([firstTimestamp, lastTimestamp]);
|
||||
}, [timeseries]);
|
||||
|
||||
const yAxisFormatter = createFormatter('percent');
|
||||
|
||||
const tooltipProps = {
|
||||
headerFormatter: (tooltipValue: TooltipValue) =>
|
||||
moment(tooltipValue.value).format('Y-MM-DD HH:mm:ss.SSS'),
|
||||
};
|
||||
|
||||
const dataDomain = calculateDomain(timeseries, [chartMetric], false);
|
||||
const domain = dataDomain
|
||||
? {
|
||||
max: dataDomain.max * 1.1, // add 10% headroom.
|
||||
min: dataDomain.min,
|
||||
}
|
||||
: { max: 0, min: 0 };
|
||||
|
||||
return (
|
||||
<ChartContainer>
|
||||
<Chart>
|
||||
<MetricExplorerSeriesChart
|
||||
type={MetricsExplorerChartType.area}
|
||||
metric={chartMetric}
|
||||
id="0"
|
||||
series={timeseries}
|
||||
stack={false}
|
||||
/>
|
||||
<Axis
|
||||
id={'timestamp'}
|
||||
position={Position.Bottom}
|
||||
showOverlappingTicks={true}
|
||||
tickFormat={dateFormatter}
|
||||
/>
|
||||
<Axis
|
||||
id={'values'}
|
||||
position={Position.Left}
|
||||
tickFormat={yAxisFormatter}
|
||||
domain={domain}
|
||||
ticks={6}
|
||||
showGridLines
|
||||
/>
|
||||
<Settings tooltip={tooltipProps} theme={getChartTheme(isDarkMode)} />
|
||||
</Chart>
|
||||
</ChartContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export const CodeLine = euiStyled(EuiCode).attrs({
|
||||
transparentBackground: true,
|
||||
})`
|
||||
|
@ -246,22 +147,3 @@ const ExpandedRowCell = euiStyled(EuiTableRowCell).attrs({
|
|||
padding: 0 ${(props) => props.theme.eui.paddingSizes.m};
|
||||
background-color: ${(props) => props.theme.eui.euiColorLightestShade};
|
||||
`;
|
||||
|
||||
const ChartContainer = euiStyled.div`
|
||||
width: 300px;
|
||||
height: 140px;
|
||||
`;
|
||||
|
||||
const cpuMetricLabel = i18n.translate(
|
||||
'xpack.infra.metrics.nodeDetails.processes.expandedRowLabelCPU',
|
||||
{
|
||||
defaultMessage: 'CPU',
|
||||
}
|
||||
);
|
||||
|
||||
const memoryMetricLabel = i18n.translate(
|
||||
'xpack.infra.metrics.nodeDetails.processes.expandedRowLabelMemory',
|
||||
{
|
||||
defaultMessage: 'Memory',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -0,0 +1,164 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import moment from 'moment';
|
||||
import { first, last } from 'lodash';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
EuiDescriptionListTitle,
|
||||
EuiDescriptionListDescription,
|
||||
EuiFlexItem,
|
||||
EuiLoadingChart,
|
||||
EuiEmptyPrompt,
|
||||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
import { Axis, Chart, Settings, Position, TooltipValue, niceTimeFormatter } from '@elastic/charts';
|
||||
import { createFormatter } from '../../../../../../../../common/formatters';
|
||||
import { useUiSetting } from '../../../../../../../../../../../src/plugins/kibana_react/public';
|
||||
import { getChartTheme } from '../../../../../metrics_explorer/components/helpers/get_chart_theme';
|
||||
import { calculateDomain } from '../../../../../metrics_explorer/components/helpers/calculate_domain';
|
||||
import { MetricsExplorerChartType } from '../../../../../metrics_explorer/hooks/use_metrics_explorer_options';
|
||||
import { MetricExplorerSeriesChart } from '../../../../../metrics_explorer/components/series_chart';
|
||||
import { MetricsExplorerAggregation } from '../../../../../../../../common/http_api';
|
||||
import { Color } from '../../../../../../../../common/color_palette';
|
||||
import { euiStyled } from '../../../../../../../../../observability/public';
|
||||
import { useProcessListRowChart } from '../../../../hooks/use_process_list_row_chart';
|
||||
import { Process } from './types';
|
||||
|
||||
interface Props {
|
||||
command: string;
|
||||
}
|
||||
|
||||
export const ProcessRowCharts = ({ command }: Props) => {
|
||||
const { loading, error, response } = useProcessListRowChart(command);
|
||||
|
||||
const isLoading = loading || !response;
|
||||
|
||||
const cpuChart = error ? (
|
||||
<EuiEmptyPrompt iconType="alert" title={<EuiText>{failedToLoadChart}</EuiText>} />
|
||||
) : isLoading ? (
|
||||
<EuiLoadingChart />
|
||||
) : (
|
||||
<ProcessChart timeseries={response!.cpu} color={Color.color2} label={cpuMetricLabel} />
|
||||
);
|
||||
const memoryChart = error ? (
|
||||
<EuiEmptyPrompt iconType="alert" title={<EuiText>{failedToLoadChart}</EuiText>} />
|
||||
) : isLoading ? (
|
||||
<EuiLoadingChart />
|
||||
) : (
|
||||
<ProcessChart timeseries={response!.memory} color={Color.color0} label={memoryMetricLabel} />
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFlexItem>
|
||||
<EuiDescriptionListTitle>{cpuMetricLabel}</EuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription>{cpuChart}</EuiDescriptionListDescription>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiDescriptionListTitle>{memoryMetricLabel}</EuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription>{memoryChart}</EuiDescriptionListDescription>
|
||||
</EuiFlexItem>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface ProcessChartProps {
|
||||
timeseries: Process['timeseries']['x'];
|
||||
color: Color;
|
||||
label: string;
|
||||
}
|
||||
const ProcessChart = ({ timeseries, color, label }: ProcessChartProps) => {
|
||||
const chartMetric = {
|
||||
color,
|
||||
aggregation: 'avg' as MetricsExplorerAggregation,
|
||||
label,
|
||||
};
|
||||
const isDarkMode = useUiSetting<boolean>('theme:darkMode');
|
||||
|
||||
const dateFormatter = useMemo(() => {
|
||||
if (!timeseries) return () => '';
|
||||
const firstTimestamp = first(timeseries.rows)?.timestamp;
|
||||
const lastTimestamp = last(timeseries.rows)?.timestamp;
|
||||
|
||||
if (firstTimestamp == null || lastTimestamp == null) {
|
||||
return (value: number) => `${value}`;
|
||||
}
|
||||
|
||||
return niceTimeFormatter([firstTimestamp, lastTimestamp]);
|
||||
}, [timeseries]);
|
||||
|
||||
const yAxisFormatter = createFormatter('percent');
|
||||
|
||||
const tooltipProps = {
|
||||
headerFormatter: (tooltipValue: TooltipValue) =>
|
||||
moment(tooltipValue.value).format('Y-MM-DD HH:mm:ss.SSS'),
|
||||
};
|
||||
|
||||
const dataDomain = calculateDomain(timeseries, [chartMetric], false);
|
||||
const domain = dataDomain
|
||||
? {
|
||||
max: dataDomain.max * 1.1, // add 10% headroom.
|
||||
min: dataDomain.min,
|
||||
}
|
||||
: { max: 0, min: 0 };
|
||||
|
||||
return (
|
||||
<ChartContainer>
|
||||
<Chart>
|
||||
<MetricExplorerSeriesChart
|
||||
type={MetricsExplorerChartType.area}
|
||||
metric={chartMetric}
|
||||
id="0"
|
||||
series={timeseries}
|
||||
stack={false}
|
||||
/>
|
||||
<Axis
|
||||
id={'timestamp'}
|
||||
position={Position.Bottom}
|
||||
showOverlappingTicks={true}
|
||||
tickFormat={dateFormatter}
|
||||
/>
|
||||
<Axis
|
||||
id={'values'}
|
||||
position={Position.Left}
|
||||
tickFormat={yAxisFormatter}
|
||||
domain={domain}
|
||||
ticks={6}
|
||||
showGridLines
|
||||
/>
|
||||
<Settings tooltip={tooltipProps} theme={getChartTheme(isDarkMode)} />
|
||||
</Chart>
|
||||
</ChartContainer>
|
||||
);
|
||||
};
|
||||
|
||||
const ChartContainer = euiStyled.div`
|
||||
width: 300px;
|
||||
height: 140px;
|
||||
`;
|
||||
|
||||
const cpuMetricLabel = i18n.translate(
|
||||
'xpack.infra.metrics.nodeDetails.processes.expandedRowLabelCPU',
|
||||
{
|
||||
defaultMessage: 'CPU',
|
||||
}
|
||||
);
|
||||
|
||||
const memoryMetricLabel = i18n.translate(
|
||||
'xpack.infra.metrics.nodeDetails.processes.expandedRowLabelMemory',
|
||||
{
|
||||
defaultMessage: 'Memory',
|
||||
}
|
||||
);
|
||||
|
||||
const failedToLoadChart = i18n.translate(
|
||||
'xpack.infra.metrics.nodeDetails.processes.failedToLoadChart',
|
||||
{
|
||||
defaultMessage: 'Unable to load chart',
|
||||
}
|
||||
);
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { useMemo, useState, useEffect, useCallback } from 'react';
|
||||
import React, { useMemo, useState, useCallback } from 'react';
|
||||
import { omit } from 'lodash';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
|
@ -13,10 +13,8 @@ import {
|
|||
EuiTableBody,
|
||||
EuiTableHeaderCell,
|
||||
EuiTableRowCell,
|
||||
EuiSpacer,
|
||||
EuiTablePagination,
|
||||
EuiLoadingChart,
|
||||
Query,
|
||||
EuiEmptyPrompt,
|
||||
SortableProperties,
|
||||
LEFT_ALIGNMENT,
|
||||
RIGHT_ALIGNMENT,
|
||||
|
@ -24,17 +22,18 @@ import {
|
|||
import { ProcessListAPIResponse } from '../../../../../../../../common/http_api';
|
||||
import { FORMATTERS } from '../../../../../../../../common/formatters';
|
||||
import { euiStyled } from '../../../../../../../../../observability/public';
|
||||
import { SortBy } from '../../../../hooks/use_process_list';
|
||||
import { Process } from './types';
|
||||
import { ProcessRow, CodeLine } from './process_row';
|
||||
import { parseProcessList } from './parse_process_list';
|
||||
import { StateBadge } from './state_badge';
|
||||
import { STATE_ORDER } from './states';
|
||||
|
||||
interface TableProps {
|
||||
processList: ProcessListAPIResponse;
|
||||
processList: ProcessListAPIResponse['processList'];
|
||||
currentTime: number;
|
||||
isLoading: boolean;
|
||||
searchFilter: Query;
|
||||
sortBy: SortBy;
|
||||
setSortBy: (s: SortBy) => void;
|
||||
}
|
||||
|
||||
function useSortableProperties<T>(
|
||||
|
@ -43,25 +42,21 @@ function useSortableProperties<T>(
|
|||
getValue: (obj: T) => any;
|
||||
isAscending: boolean;
|
||||
}>,
|
||||
defaultSortProperty: string
|
||||
defaultSortProperty: string,
|
||||
callback: (s: SortBy) => void
|
||||
) {
|
||||
const [sortableProperties] = useState<SortableProperties<T>>(
|
||||
new SortableProperties(sortablePropertyItems, defaultSortProperty)
|
||||
);
|
||||
const [sortedColumn, setSortedColumn] = useState(
|
||||
omit(sortableProperties.getSortedProperty(), 'getValue')
|
||||
);
|
||||
|
||||
return {
|
||||
setSortedColumn: useCallback(
|
||||
updateSortableProperties: useCallback(
|
||||
(property) => {
|
||||
sortableProperties.sortOn(property);
|
||||
setSortedColumn(omit(sortableProperties.getSortedProperty(), 'getValue'));
|
||||
callback(omit(sortableProperties.getSortedProperty(), 'getValue'));
|
||||
},
|
||||
[sortableProperties]
|
||||
[sortableProperties, callback]
|
||||
),
|
||||
sortedColumn,
|
||||
sortItems: (items: T[]) => sortableProperties.sortItems(items),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -69,28 +64,15 @@ export const ProcessesTable = ({
|
|||
processList,
|
||||
currentTime,
|
||||
isLoading,
|
||||
searchFilter,
|
||||
sortBy,
|
||||
setSortBy,
|
||||
}: TableProps) => {
|
||||
const [currentPage, setCurrentPage] = useState(0);
|
||||
const [itemsPerPage, setItemsPerPage] = useState(10);
|
||||
useEffect(() => setCurrentPage(0), [processList, searchFilter, itemsPerPage]);
|
||||
|
||||
const { sortedColumn, sortItems, setSortedColumn } = useSortableProperties<Process>(
|
||||
const { updateSortableProperties } = useSortableProperties<Process>(
|
||||
[
|
||||
{
|
||||
name: 'state',
|
||||
getValue: (item: any) => STATE_ORDER.indexOf(item.state),
|
||||
isAscending: true,
|
||||
},
|
||||
{
|
||||
name: 'command',
|
||||
getValue: (item: any) => item.command.toLowerCase(),
|
||||
isAscending: true,
|
||||
},
|
||||
{
|
||||
name: 'startTime',
|
||||
getValue: (item: any) => Date.parse(item.startTime),
|
||||
isAscending: false,
|
||||
isAscending: true,
|
||||
},
|
||||
{
|
||||
name: 'cpu',
|
||||
|
@ -103,32 +85,34 @@ export const ProcessesTable = ({
|
|||
isAscending: false,
|
||||
},
|
||||
],
|
||||
'state'
|
||||
'cpu',
|
||||
setSortBy
|
||||
);
|
||||
|
||||
const currentItems = useMemo(() => {
|
||||
const filteredItems = Query.execute(searchFilter, parseProcessList(processList)) as Process[];
|
||||
if (!filteredItems.length) return [];
|
||||
const sortedItems = sortItems(filteredItems);
|
||||
return sortedItems;
|
||||
}, [processList, searchFilter, sortItems]);
|
||||
|
||||
const pageCount = useMemo(() => Math.ceil(currentItems.length / itemsPerPage), [
|
||||
itemsPerPage,
|
||||
currentItems,
|
||||
]);
|
||||
|
||||
const pageStartIdx = useMemo(() => currentPage * itemsPerPage + (currentPage > 0 ? 1 : 0), [
|
||||
currentPage,
|
||||
itemsPerPage,
|
||||
]);
|
||||
const currentItemsPage = useMemo(
|
||||
() => currentItems.slice(pageStartIdx, pageStartIdx + itemsPerPage),
|
||||
[pageStartIdx, currentItems, itemsPerPage]
|
||||
const currentItems = useMemo(
|
||||
() =>
|
||||
processList.sort(
|
||||
(a, b) => STATE_ORDER.indexOf(a.state) - STATE_ORDER.indexOf(b.state)
|
||||
) as Process[],
|
||||
[processList]
|
||||
);
|
||||
|
||||
if (isLoading) return <LoadingPlaceholder />;
|
||||
|
||||
if (currentItems.length === 0)
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
iconType="tableDensityNormal"
|
||||
title={
|
||||
<h4>
|
||||
{i18n.translate('xpack.infra.metrics.nodeDetails.noProcesses', {
|
||||
defaultMessage: 'No processes matched these search terms',
|
||||
})}
|
||||
</h4>
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiTable>
|
||||
|
@ -139,27 +123,18 @@ export const ProcessesTable = ({
|
|||
key={`${String(column.field)}-header`}
|
||||
align={column.align ?? LEFT_ALIGNMENT}
|
||||
width={column.width}
|
||||
onSort={column.sortable ? () => setSortedColumn(column.field) : undefined}
|
||||
isSorted={sortedColumn.name === column.field}
|
||||
isSortAscending={sortedColumn.name === column.field && sortedColumn.isAscending}
|
||||
onSort={column.sortable ? () => updateSortableProperties(column.field) : undefined}
|
||||
isSorted={sortBy.name === column.field}
|
||||
isSortAscending={sortBy.name === column.field && sortBy.isAscending}
|
||||
>
|
||||
{column.name}
|
||||
</EuiTableHeaderCell>
|
||||
))}
|
||||
</EuiTableHeader>
|
||||
<StyledTableBody>
|
||||
<ProcessesTableBody items={currentItemsPage} currentTime={currentTime} />
|
||||
<ProcessesTableBody items={currentItems} currentTime={currentTime} />
|
||||
</StyledTableBody>
|
||||
</EuiTable>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiTablePagination
|
||||
itemsPerPage={itemsPerPage}
|
||||
activePage={currentPage}
|
||||
pageCount={pageCount}
|
||||
itemsPerPageOptions={[10, 20, 50]}
|
||||
onChangePage={setCurrentPage}
|
||||
onChangeItemsPerPage={setItemsPerPage}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -213,8 +188,8 @@ const StyledTableBody = euiStyled(EuiTableBody)`
|
|||
|
||||
const ONE_MINUTE = 60 * 1000;
|
||||
const ONE_HOUR = ONE_MINUTE * 60;
|
||||
const RuntimeCell = ({ startTime, currentTime }: { startTime: string; currentTime: number }) => {
|
||||
const runtimeLength = currentTime - Date.parse(startTime);
|
||||
const RuntimeCell = ({ startTime, currentTime }: { startTime: number; currentTime: number }) => {
|
||||
const runtimeLength = currentTime - startTime;
|
||||
let remainingRuntimeMS = runtimeLength;
|
||||
const runtimeHours = Math.floor(remainingRuntimeMS / ONE_HOUR);
|
||||
remainingRuntimeMS -= runtimeHours * ONE_HOUR;
|
||||
|
@ -244,7 +219,7 @@ const columns: Array<{
|
|||
name: i18n.translate('xpack.infra.metrics.nodeDetails.processes.columnLabelState', {
|
||||
defaultMessage: 'State',
|
||||
}),
|
||||
sortable: true,
|
||||
sortable: false,
|
||||
render: (state: string) => <StateBadge state={state} />,
|
||||
width: 84,
|
||||
textOnly: false,
|
||||
|
@ -254,7 +229,7 @@ const columns: Array<{
|
|||
name: i18n.translate('xpack.infra.metrics.nodeDetails.processes.columnLabelCommand', {
|
||||
defaultMessage: 'Command',
|
||||
}),
|
||||
sortable: true,
|
||||
sortable: false,
|
||||
width: '40%',
|
||||
render: (command: string) => <CodeLine>{command}</CodeLine>,
|
||||
},
|
||||
|
@ -265,7 +240,7 @@ const columns: Array<{
|
|||
}),
|
||||
align: RIGHT_ALIGNMENT,
|
||||
sortable: true,
|
||||
render: (startTime: string, currentTime: number) => (
|
||||
render: (startTime: number, currentTime: number) => (
|
||||
<RuntimeCell startTime={startTime} currentTime={currentTime} />
|
||||
),
|
||||
},
|
||||
|
|
|
@ -5,16 +5,15 @@
|
|||
*/
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import { mapValues, countBy } from 'lodash';
|
||||
import { mapValues } from 'lodash';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiBasicTable, EuiLoadingSpinner, EuiBasicTableColumn } from '@elastic/eui';
|
||||
import { euiStyled } from '../../../../../../../../../observability/public';
|
||||
import { ProcessListAPIResponse } from '../../../../../../../../common/http_api';
|
||||
import { parseProcessList } from './parse_process_list';
|
||||
import { STATE_NAMES } from './states';
|
||||
|
||||
interface Props {
|
||||
processList: ProcessListAPIResponse;
|
||||
processSummary: ProcessListAPIResponse['summary'];
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
|
@ -22,18 +21,17 @@ type SummaryColumn = {
|
|||
total: number;
|
||||
} & Record<keyof typeof STATE_NAMES, number>;
|
||||
|
||||
export const SummaryTable = ({ processList, isLoading }: Props) => {
|
||||
const parsedList = parseProcessList(processList);
|
||||
export const SummaryTable = ({ processSummary, isLoading }: Props) => {
|
||||
const processCount = useMemo(
|
||||
() =>
|
||||
[
|
||||
{
|
||||
total: isLoading ? -1 : parsedList.length,
|
||||
total: isLoading ? -1 : processSummary.total,
|
||||
...mapValues(STATE_NAMES, () => (isLoading ? -1 : 0)),
|
||||
...(isLoading ? [] : countBy(parsedList, 'state')),
|
||||
...(isLoading ? {} : processSummary),
|
||||
},
|
||||
] as SummaryColumn[],
|
||||
[parsedList, isLoading]
|
||||
[processSummary, isLoading]
|
||||
);
|
||||
return (
|
||||
<StyleWrapper>
|
||||
|
|
|
@ -3,20 +3,32 @@
|
|||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import createContainter from 'constate';
|
||||
import { fold } from 'fp-ts/lib/Either';
|
||||
import { identity } from 'fp-ts/lib/function';
|
||||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
import { useEffect } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { ProcessListAPIResponse, ProcessListAPIResponseRT } from '../../../../../common/http_api';
|
||||
import { throwErrors, createPlainError } from '../../../../../common/runtime_types';
|
||||
import { useHTTPRequest } from '../../../../hooks/use_http_request';
|
||||
import { useSourceContext } from '../../../../containers/source';
|
||||
|
||||
export interface SortBy {
|
||||
name: string;
|
||||
isAscending: boolean;
|
||||
}
|
||||
|
||||
export function useProcessList(
|
||||
hostTerm: Record<string, string>,
|
||||
indexPattern: string,
|
||||
timefield: string,
|
||||
to: number
|
||||
to: number,
|
||||
sortBy: SortBy,
|
||||
searchFilter: object
|
||||
) {
|
||||
const { createDerivedIndexPattern } = useSourceContext();
|
||||
const indexPattern = createDerivedIndexPattern('metrics').title;
|
||||
|
||||
const [inErrorState, setInErrorState] = useState(false);
|
||||
const decodeResponse = (response: any) => {
|
||||
return pipe(
|
||||
ProcessListAPIResponseRT.decode(response),
|
||||
|
@ -24,32 +36,52 @@ export function useProcessList(
|
|||
);
|
||||
};
|
||||
|
||||
const timerange = {
|
||||
field: timefield,
|
||||
interval: 'modules',
|
||||
to,
|
||||
from: to - 15 * 60 * 1000, // 15 minutes
|
||||
};
|
||||
const parsedSortBy =
|
||||
sortBy.name === 'runtimeLength'
|
||||
? {
|
||||
...sortBy,
|
||||
name: 'startTime',
|
||||
}
|
||||
: sortBy;
|
||||
|
||||
const { error, loading, response, makeRequest } = useHTTPRequest<ProcessListAPIResponse>(
|
||||
'/api/metrics/process_list',
|
||||
'POST',
|
||||
JSON.stringify({
|
||||
hostTerm,
|
||||
timerange,
|
||||
timefield,
|
||||
indexPattern,
|
||||
to,
|
||||
sortBy: parsedSortBy,
|
||||
searchFilter,
|
||||
}),
|
||||
decodeResponse
|
||||
);
|
||||
|
||||
useEffect(() => setInErrorState(true), [error]);
|
||||
useEffect(() => setInErrorState(false), [loading]);
|
||||
|
||||
useEffect(() => {
|
||||
makeRequest();
|
||||
}, [makeRequest]);
|
||||
|
||||
return {
|
||||
error,
|
||||
error: inErrorState,
|
||||
loading,
|
||||
response,
|
||||
makeRequest,
|
||||
};
|
||||
}
|
||||
|
||||
function useProcessListParams(props: {
|
||||
hostTerm: Record<string, string>;
|
||||
timefield: string;
|
||||
to: number;
|
||||
}) {
|
||||
const { hostTerm, timefield, to } = props;
|
||||
const { createDerivedIndexPattern } = useSourceContext();
|
||||
const indexPattern = createDerivedIndexPattern('metrics').title;
|
||||
return { hostTerm, indexPattern, timefield, to };
|
||||
}
|
||||
const ProcessListContext = createContainter(useProcessListParams);
|
||||
export const [ProcessListContextProvider, useProcessListContext] = ProcessListContext;
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { fold } from 'fp-ts/lib/Either';
|
||||
import { identity } from 'fp-ts/lib/function';
|
||||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
import { useEffect, useState } from 'react';
|
||||
import {
|
||||
ProcessListAPIChartResponse,
|
||||
ProcessListAPIChartResponseRT,
|
||||
} from '../../../../../common/http_api';
|
||||
import { throwErrors, createPlainError } from '../../../../../common/runtime_types';
|
||||
import { useHTTPRequest } from '../../../../hooks/use_http_request';
|
||||
import { useProcessListContext } from './use_process_list';
|
||||
|
||||
export function useProcessListRowChart(command: string) {
|
||||
const [inErrorState, setInErrorState] = useState(false);
|
||||
const decodeResponse = (response: any) => {
|
||||
return pipe(
|
||||
ProcessListAPIChartResponseRT.decode(response),
|
||||
fold(throwErrors(createPlainError), identity)
|
||||
);
|
||||
};
|
||||
const { hostTerm, timefield, indexPattern, to } = useProcessListContext();
|
||||
|
||||
const { error, loading, response, makeRequest } = useHTTPRequest<ProcessListAPIChartResponse>(
|
||||
'/api/metrics/process_list/chart',
|
||||
'POST',
|
||||
JSON.stringify({
|
||||
hostTerm,
|
||||
timefield,
|
||||
indexPattern,
|
||||
to,
|
||||
command,
|
||||
}),
|
||||
decodeResponse
|
||||
);
|
||||
|
||||
useEffect(() => setInErrorState(true), [error]);
|
||||
useEffect(() => setInErrorState(false), [loading]);
|
||||
|
||||
useEffect(() => {
|
||||
makeRequest();
|
||||
}, [makeRequest]);
|
||||
|
||||
return {
|
||||
error: inErrorState,
|
||||
loading,
|
||||
response,
|
||||
makeRequest,
|
||||
};
|
||||
}
|
6
x-pack/plugins/infra/server/lib/host_details/common.ts
Normal file
6
x-pack/plugins/infra/server/lib/host_details/common.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
export const CMDLINE_FIELD = 'system.process.cmdline';
|
|
@ -4,61 +4,136 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { ProcessListAPIRequest, MetricsAPIRequest } from '../../../common/http_api';
|
||||
import { getAllMetricsData } from '../../utils/get_all_metrics_data';
|
||||
import { query } from '../metrics';
|
||||
import { ProcessListAPIRequest, ProcessListAPIQueryAggregation } from '../../../common/http_api';
|
||||
import { ESSearchClient } from '../metrics/types';
|
||||
import { CMDLINE_FIELD } from './common';
|
||||
|
||||
const TOP_N = 10;
|
||||
|
||||
export const getProcessList = async (
|
||||
client: ESSearchClient,
|
||||
{ hostTerm, timerange, indexPattern }: ProcessListAPIRequest
|
||||
search: ESSearchClient,
|
||||
{ hostTerm, timefield, indexPattern, to, sortBy, searchFilter }: ProcessListAPIRequest
|
||||
) => {
|
||||
const queryBody = {
|
||||
timerange,
|
||||
modules: ['system.cpu', 'system.memory'],
|
||||
groupBy: ['system.process.cmdline'],
|
||||
filters: [{ term: hostTerm }],
|
||||
indexPattern,
|
||||
limit: 9,
|
||||
metrics: [
|
||||
{
|
||||
id: 'cpu',
|
||||
aggregations: {
|
||||
cpu: {
|
||||
avg: {
|
||||
field: 'system.process.cpu.total.norm.pct',
|
||||
const body = {
|
||||
size: 0,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
range: {
|
||||
[timefield]: {
|
||||
gte: to - 60 * 1000, // 1 minute
|
||||
lte: to,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
term: hostTerm,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'memory',
|
||||
aggregations: {
|
||||
memory: {
|
||||
avg: {
|
||||
field: 'system.process.memory.rss.pct',
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
summaryEvent: {
|
||||
filter: {
|
||||
term: {
|
||||
'event.dataset': 'system.process.summary',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'meta',
|
||||
aggregations: {
|
||||
meta: {
|
||||
aggs: {
|
||||
summary: {
|
||||
top_hits: {
|
||||
size: 1,
|
||||
sort: [{ [timerange.field]: { order: 'desc' } }],
|
||||
_source: [
|
||||
'system.process.cpu.start_time',
|
||||
'system.process.state',
|
||||
'process.pid',
|
||||
'user.name',
|
||||
sort: [
|
||||
{
|
||||
[timefield]: {
|
||||
order: 'desc',
|
||||
},
|
||||
},
|
||||
],
|
||||
_source: ['system.process.summary'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
} as MetricsAPIRequest;
|
||||
return await getAllMetricsData((body: MetricsAPIRequest) => query(client, body), queryBody);
|
||||
processes: {
|
||||
filter: {
|
||||
bool: {
|
||||
must: searchFilter ?? [{ match_all: {} }],
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
filteredProcs: {
|
||||
terms: {
|
||||
field: CMDLINE_FIELD,
|
||||
size: TOP_N,
|
||||
order: {
|
||||
[sortBy.name]: sortBy.isAscending ? 'asc' : 'desc',
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
cpu: {
|
||||
avg: {
|
||||
field: 'system.process.cpu.total.pct',
|
||||
},
|
||||
},
|
||||
memory: {
|
||||
avg: {
|
||||
field: 'system.process.memory.rss.pct',
|
||||
},
|
||||
},
|
||||
startTime: {
|
||||
max: {
|
||||
field: 'system.process.cpu.start_time',
|
||||
},
|
||||
},
|
||||
meta: {
|
||||
top_hits: {
|
||||
size: 1,
|
||||
sort: [
|
||||
{
|
||||
[timefield]: {
|
||||
order: 'desc',
|
||||
},
|
||||
},
|
||||
],
|
||||
_source: ['system.process.state', 'user.name', 'process.pid'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
try {
|
||||
const result = await search<{}, ProcessListAPIQueryAggregation>({
|
||||
body,
|
||||
index: indexPattern,
|
||||
});
|
||||
const { buckets: processListBuckets } = result.aggregations!.processes.filteredProcs;
|
||||
const processList = processListBuckets.map((bucket) => {
|
||||
const meta = bucket.meta.hits.hits[0]._source;
|
||||
|
||||
return {
|
||||
cpu: bucket.cpu.value,
|
||||
memory: bucket.memory.value,
|
||||
startTime: Date.parse(bucket.startTime.value_as_string),
|
||||
pid: meta.process.pid,
|
||||
state: meta.system.process.state,
|
||||
user: meta.user.name,
|
||||
command: bucket.key,
|
||||
};
|
||||
});
|
||||
const {
|
||||
summary,
|
||||
} = result.aggregations!.summaryEvent.summary.hits.hits[0]._source.system.process;
|
||||
|
||||
return {
|
||||
processList,
|
||||
summary,
|
||||
};
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -0,0 +1,138 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { first } from 'lodash';
|
||||
import {
|
||||
ProcessListAPIChartRequest,
|
||||
ProcessListAPIChartQueryAggregation,
|
||||
ProcessListAPIRow,
|
||||
ProcessListAPIChartResponse,
|
||||
} from '../../../common/http_api';
|
||||
import { ESSearchClient } from '../metrics/types';
|
||||
import { CMDLINE_FIELD } from './common';
|
||||
|
||||
export const getProcessListChart = async (
|
||||
search: ESSearchClient,
|
||||
{ hostTerm, timefield, indexPattern, to, command }: ProcessListAPIChartRequest
|
||||
) => {
|
||||
const body = {
|
||||
size: 0,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
range: {
|
||||
[timefield]: {
|
||||
gte: to - 60 * 1000, // 1 minute
|
||||
lte: to,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
term: hostTerm,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
process: {
|
||||
filter: {
|
||||
bool: {
|
||||
must: [
|
||||
{
|
||||
match: {
|
||||
[CMDLINE_FIELD]: command,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
filteredProc: {
|
||||
terms: {
|
||||
field: CMDLINE_FIELD,
|
||||
size: 1,
|
||||
},
|
||||
aggs: {
|
||||
timeseries: {
|
||||
date_histogram: {
|
||||
field: timefield,
|
||||
fixed_interval: '1m',
|
||||
extended_bounds: {
|
||||
min: to - 60 * 15 * 1000, // 15 minutes,
|
||||
max: to,
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
cpu: {
|
||||
avg: {
|
||||
field: 'system.process.cpu.total.pct',
|
||||
},
|
||||
},
|
||||
memory: {
|
||||
avg: {
|
||||
field: 'system.process.memory.rss.pct',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
try {
|
||||
const result = await search<{}, ProcessListAPIChartQueryAggregation>({
|
||||
body,
|
||||
index: indexPattern,
|
||||
});
|
||||
const { buckets } = result.aggregations!.process.filteredProc;
|
||||
const timeseries = first(
|
||||
buckets.map((bucket) =>
|
||||
bucket.timeseries.buckets.reduce(
|
||||
(tsResult, tsBucket) => {
|
||||
tsResult.cpu.rows.push({
|
||||
metric_0: tsBucket.cpu.value,
|
||||
timestamp: tsBucket.key,
|
||||
});
|
||||
tsResult.memory.rows.push({
|
||||
metric_0: tsBucket.memory.value,
|
||||
timestamp: tsBucket.key,
|
||||
});
|
||||
return tsResult;
|
||||
},
|
||||
{
|
||||
cpu: {
|
||||
id: 'cpu',
|
||||
columns: TS_COLUMNS,
|
||||
rows: [] as ProcessListAPIRow[],
|
||||
},
|
||||
memory: {
|
||||
id: 'memory',
|
||||
columns: TS_COLUMNS,
|
||||
rows: [] as ProcessListAPIRow[],
|
||||
},
|
||||
}
|
||||
)
|
||||
)
|
||||
);
|
||||
return timeseries as ProcessListAPIChartResponse;
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
const TS_COLUMNS = [
|
||||
{
|
||||
name: 'timestamp',
|
||||
type: 'date',
|
||||
},
|
||||
{
|
||||
name: 'metric_0',
|
||||
type: 'number',
|
||||
},
|
||||
];
|
|
@ -13,7 +13,13 @@ import { InfraBackendLibs } from '../../lib/infra_types';
|
|||
import { throwErrors } from '../../../common/runtime_types';
|
||||
import { createSearchClient } from '../../lib/create_search_client';
|
||||
import { getProcessList } from '../../lib/host_details/process_list';
|
||||
import { ProcessListAPIRequestRT, ProcessListAPIResponseRT } from '../../../common/http_api';
|
||||
import { getProcessListChart } from '../../lib/host_details/process_list_chart';
|
||||
import {
|
||||
ProcessListAPIRequestRT,
|
||||
ProcessListAPIResponseRT,
|
||||
ProcessListAPIChartRequestRT,
|
||||
ProcessListAPIChartResponseRT,
|
||||
} from '../../../common/http_api';
|
||||
|
||||
const escapeHatch = schema.object({}, { unknowns: 'allow' });
|
||||
|
||||
|
@ -47,4 +53,33 @@ export const initProcessListRoute = (libs: InfraBackendLibs) => {
|
|||
}
|
||||
}
|
||||
);
|
||||
|
||||
framework.registerRoute(
|
||||
{
|
||||
method: 'post',
|
||||
path: '/api/metrics/process_list/chart',
|
||||
validate: {
|
||||
body: escapeHatch,
|
||||
},
|
||||
},
|
||||
async (requestContext, request, response) => {
|
||||
try {
|
||||
const options = pipe(
|
||||
ProcessListAPIChartRequestRT.decode(request.body),
|
||||
fold(throwErrors(Boom.badRequest), identity)
|
||||
);
|
||||
|
||||
const client = createSearchClient(requestContext, framework);
|
||||
const processListResponse = await getProcessListChart(client, options);
|
||||
|
||||
return response.ok({
|
||||
body: ProcessListAPIChartResponseRT.encode(processListResponse),
|
||||
});
|
||||
} catch (error) {
|
||||
return response.internalError({
|
||||
body: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue