mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[Metrics UI] Add Process tab to Enhanced Node Details (#83477)
This commit is contained in:
parent
e07d6d0b38
commit
21c0258e6b
20 changed files with 1163 additions and 48 deletions
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* 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 * from './process_list';
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* 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 * as rt from 'io-ts';
|
||||
import { MetricsAPITimerangeRT, MetricsAPISeriesRT } from '../metrics_api';
|
||||
|
||||
export const ProcessListAPIRequestRT = rt.type({
|
||||
hostTerm: rt.record(rt.string, rt.string),
|
||||
timerange: MetricsAPITimerangeRT,
|
||||
indexPattern: rt.string,
|
||||
});
|
||||
|
||||
export const ProcessListAPIResponseRT = rt.array(MetricsAPISeriesRT);
|
||||
|
||||
export type ProcessListAPIRequest = rt.TypeOf<typeof ProcessListAPIRequestRT>;
|
||||
|
||||
export type ProcessListAPIResponse = rt.TypeOf<typeof ProcessListAPIResponseRT>;
|
|
@ -11,3 +11,4 @@ export * from './metrics_explorer';
|
|||
export * from './metrics_api';
|
||||
export * from './log_alerts';
|
||||
export * from './snapshot_api';
|
||||
export * from './host_details';
|
||||
|
|
|
@ -38,7 +38,11 @@ export const BottomDrawer: React.FC<{
|
|||
<BottomActionContainer ref={isOpen ? measureRef : null} isOpen={isOpen}>
|
||||
<BottomActionTopBar ref={isOpen ? null : measureRef}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<ShowHideButton iconType={isOpen ? 'arrowDown' : 'arrowRight'} onClick={onClick}>
|
||||
<ShowHideButton
|
||||
aria-expanded={isOpen}
|
||||
iconType={isOpen ? 'arrowDown' : 'arrowRight'}
|
||||
onClick={onClick}
|
||||
>
|
||||
{isOpen ? hideHistory : showHistory}
|
||||
</ShowHideButton>
|
||||
</EuiFlexItem>
|
||||
|
|
|
@ -4,11 +4,9 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EuiTabbedContent } from '@elastic/eui';
|
||||
import { EuiPortal, EuiTabs, EuiTab, EuiPanel, EuiTitle } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { EuiPanel } from '@elastic/eui';
|
||||
import React, { CSSProperties, useMemo } from 'react';
|
||||
import { EuiText } from '@elastic/eui';
|
||||
import React, { CSSProperties, useMemo, useState } from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty } from '@elastic/eui';
|
||||
import { euiStyled } from '../../../../../../../observability/public';
|
||||
import { InfraWaffleMapNode, InfraWaffleMapOptions } from '../../../../../lib/lib';
|
||||
|
@ -17,6 +15,7 @@ import { MetricsTab } from './tabs/metrics';
|
|||
import { LogsTab } from './tabs/logs';
|
||||
import { ProcessesTab } from './tabs/processes';
|
||||
import { PropertiesTab } from './tabs/properties';
|
||||
import { OVERLAY_Y_START, OVERLAY_BOTTOM_MARGIN, OVERLAY_HEADER_SIZE } from './tabs/shared';
|
||||
|
||||
interface Props {
|
||||
isOpen: boolean;
|
||||
|
@ -48,46 +47,63 @@ export const NodeContextPopover = ({
|
|||
});
|
||||
}, [tabConfigs, node, nodeType, currentTime, options]);
|
||||
|
||||
const [selectedTab, setSelectedTab] = useState(0);
|
||||
|
||||
if (!isOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiPanel hasShadow={true} paddingSize={'none'} style={panelStyle}>
|
||||
<OverlayHeader>
|
||||
<EuiFlexGroup alignItems={'center'}>
|
||||
<EuiFlexItem grow={true}>
|
||||
<EuiText>
|
||||
<h4>{node.name}</h4>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty onClick={onClose} iconType={'cross'}>
|
||||
<FormattedMessage id="xpack.infra.infra.nodeDetails.close" defaultMessage="Close" />
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</OverlayHeader>
|
||||
<EuiTabbedContent tabs={tabs} />
|
||||
</EuiPanel>
|
||||
<EuiPortal>
|
||||
<EuiPanel hasShadow={true} paddingSize={'none'} style={panelStyle}>
|
||||
<OverlayHeader>
|
||||
<OverlayHeaderTitleWrapper>
|
||||
<EuiFlexItem grow={true}>
|
||||
<EuiTitle size="s">
|
||||
<h4>{node.name}</h4>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty onClick={onClose} iconType={'cross'}>
|
||||
<FormattedMessage id="xpack.infra.infra.nodeDetails.close" defaultMessage="Close" />
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
</OverlayHeaderTitleWrapper>
|
||||
<EuiTabs>
|
||||
{tabs.map((tab, i) => (
|
||||
<EuiTab key={tab.id} isSelected={i === selectedTab} onClick={() => setSelectedTab(i)}>
|
||||
{tab.name}
|
||||
</EuiTab>
|
||||
))}
|
||||
</EuiTabs>
|
||||
</OverlayHeader>
|
||||
{tabs[selectedTab].content}
|
||||
</EuiPanel>
|
||||
</EuiPortal>
|
||||
);
|
||||
};
|
||||
|
||||
const OverlayHeader = euiStyled.div`
|
||||
border-color: ${(props) => props.theme.eui.euiBorderColor};
|
||||
border-bottom-width: ${(props) => props.theme.eui.euiBorderWidthThick};
|
||||
padding: ${(props) => props.theme.eui.euiSizeS};
|
||||
padding-bottom: 0;
|
||||
overflow: hidden;
|
||||
background-color: ${(props) => props.theme.eui.euiColorLightestShade};
|
||||
height: ${OVERLAY_HEADER_SIZE}px;
|
||||
`;
|
||||
|
||||
const OverlayHeaderTitleWrapper = euiStyled(EuiFlexGroup).attrs({ alignItems: 'center' })`
|
||||
padding: ${(props) => props.theme.eui.paddingSizes.s} ${(props) =>
|
||||
props.theme.eui.paddingSizes.m} 0;
|
||||
`;
|
||||
|
||||
const panelStyle: CSSProperties = {
|
||||
position: 'absolute',
|
||||
right: 10,
|
||||
top: -100,
|
||||
top: OVERLAY_Y_START,
|
||||
width: '50%',
|
||||
maxWidth: 600,
|
||||
maxWidth: 730,
|
||||
zIndex: 2,
|
||||
height: '50vh',
|
||||
height: `calc(100vh - ${OVERLAY_Y_START + OVERLAY_BOTTOM_MARGIN}px)`,
|
||||
overflow: 'hidden',
|
||||
};
|
||||
|
|
|
@ -1,21 +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 React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { TabContent, TabProps } from './shared';
|
||||
|
||||
const TabComponent = (props: TabProps) => {
|
||||
return <TabContent>Processes Placeholder</TabContent>;
|
||||
};
|
||||
|
||||
export const ProcessesTab = {
|
||||
id: 'processes',
|
||||
name: i18n.translate('xpack.infra.nodeDetails.tabs.processes', {
|
||||
defaultMessage: 'Processes',
|
||||
}),
|
||||
content: TabComponent,
|
||||
};
|
|
@ -0,0 +1,102 @@
|
|||
/*
|
||||
* 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, useState } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiSearchBar, EuiSpacer, EuiEmptyPrompt, EuiButton, Query } from '@elastic/eui';
|
||||
import { useProcessList } 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';
|
||||
|
||||
const TabComponent = ({ currentTime, node, nodeType, options }: TabProps) => {
|
||||
const [searchFilter, setSearchFilter] = useState<Query>(EuiSearchBar.Query.MATCH_ALL);
|
||||
|
||||
const hostTerm = useMemo(() => {
|
||||
const field =
|
||||
options.fields && Reflect.has(options.fields, nodeType)
|
||||
? Reflect.get(options.fields, nodeType)
|
||||
: nodeType;
|
||||
return { [field]: node.name };
|
||||
}, [options, node, nodeType]);
|
||||
|
||||
const { loading, error, response, makeRequest: reload } = useProcessList(
|
||||
hostTerm,
|
||||
'metricbeat-*',
|
||||
options.fields!.timestamp,
|
||||
currentTime
|
||||
);
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
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}
|
||||
/>
|
||||
</TabContent>
|
||||
);
|
||||
};
|
||||
|
||||
export const ProcessesTab = {
|
||||
id: 'processes',
|
||||
name: i18n.translate('xpack.infra.metrics.nodeDetails.tabs.processes', {
|
||||
defaultMessage: 'Processes',
|
||||
}),
|
||||
content: TabComponent,
|
||||
};
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* 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,267 @@
|
|||
/*
|
||||
* 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, { useState, useMemo } from 'react';
|
||||
import moment from 'moment';
|
||||
import { first, last } from 'lodash';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
EuiTableRow,
|
||||
EuiTableRowCell,
|
||||
EuiButtonEmpty,
|
||||
EuiCode,
|
||||
EuiDescriptionList,
|
||||
EuiDescriptionListTitle,
|
||||
EuiDescriptionListDescription,
|
||||
EuiFlexGrid,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
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';
|
||||
|
||||
interface Props {
|
||||
cells: React.ReactNode[];
|
||||
item: Process;
|
||||
}
|
||||
|
||||
export const ProcessRow = ({ cells, item }: Props) => {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiTableRow>
|
||||
<EuiTableRowCell isExpander textOnly={false}>
|
||||
<EuiButtonEmpty
|
||||
iconType={isExpanded ? 'arrowDown' : 'arrowRight'}
|
||||
aria-expanded={isExpanded}
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
/>
|
||||
</EuiTableRowCell>
|
||||
{cells}
|
||||
</EuiTableRow>
|
||||
<EuiTableRow isExpandable isExpandedRow={isExpanded}>
|
||||
{isExpanded && (
|
||||
<AutoSizer bounds>
|
||||
{({ measureRef, bounds: { height = 0 } }) => (
|
||||
<ExpandedRowCell commandHeight={height}>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiDescriptionList compressed>
|
||||
<EuiFlexGroup gutterSize="s">
|
||||
<EuiFlexItem>
|
||||
<div ref={measureRef}>
|
||||
<EuiDescriptionListTitle>
|
||||
{i18n.translate(
|
||||
'xpack.infra.metrics.nodeDetails.processes.expandedRowLabelCommand',
|
||||
{
|
||||
defaultMessage: 'Command',
|
||||
}
|
||||
)}
|
||||
</EuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription>
|
||||
<ExpandedCommandLine>{item.command}</ExpandedCommandLine>
|
||||
</EuiDescriptionListDescription>
|
||||
</div>
|
||||
</EuiFlexItem>
|
||||
{item.apmTrace && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton>
|
||||
{i18n.translate(
|
||||
'xpack.infra.metrics.nodeDetails.processes.viewTraceInAPM',
|
||||
{
|
||||
defaultMessage: 'View trace in APM',
|
||||
}
|
||||
)}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
<EuiFlexGrid columns={2} gutterSize="s">
|
||||
<EuiFlexItem>
|
||||
<EuiDescriptionListTitle>
|
||||
{i18n.translate(
|
||||
'xpack.infra.metrics.nodeDetails.processes.expandedRowLabelPID',
|
||||
{
|
||||
defaultMessage: 'PID',
|
||||
}
|
||||
)}
|
||||
</EuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription>
|
||||
<CodeLine>{item.pid}</CodeLine>
|
||||
</EuiDescriptionListDescription>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiDescriptionListTitle>
|
||||
{i18n.translate(
|
||||
'xpack.infra.metrics.nodeDetails.processes.expandedRowLabelUser',
|
||||
{
|
||||
defaultMessage: 'User',
|
||||
}
|
||||
)}
|
||||
</EuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription>
|
||||
<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>
|
||||
</EuiFlexGrid>
|
||||
</EuiDescriptionList>
|
||||
</ExpandedRowCell>
|
||||
)}
|
||||
</AutoSizer>
|
||||
)}
|
||||
</EuiTableRow>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
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,
|
||||
})`
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
padding: 0 !important;
|
||||
& code.euiCodeBlock__code {
|
||||
white-space: nowrap !important;
|
||||
vertical-align: middle;
|
||||
}
|
||||
`;
|
||||
|
||||
const ExpandedCommandLine = euiStyled(EuiCode).attrs({
|
||||
transparentBackground: true,
|
||||
})`
|
||||
padding: 0 !important;
|
||||
margin-bottom: ${(props) => props.theme.eui.euiSizeS};
|
||||
`;
|
||||
|
||||
const ExpandedRowCell = euiStyled(EuiTableRowCell).attrs({
|
||||
textOnly: false,
|
||||
colSpan: 6,
|
||||
})<{ commandHeight: number }>`
|
||||
height: ${(props) => props.commandHeight + 240}px;
|
||||
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,288 @@
|
|||
/*
|
||||
* 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, useState, useEffect, useCallback } from 'react';
|
||||
import { omit } from 'lodash';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
EuiTable,
|
||||
EuiTableHeader,
|
||||
EuiTableBody,
|
||||
EuiTableHeaderCell,
|
||||
EuiTableRowCell,
|
||||
EuiSpacer,
|
||||
EuiTablePagination,
|
||||
EuiLoadingChart,
|
||||
Query,
|
||||
SortableProperties,
|
||||
LEFT_ALIGNMENT,
|
||||
RIGHT_ALIGNMENT,
|
||||
} from '@elastic/eui';
|
||||
import { ProcessListAPIResponse } from '../../../../../../../../common/http_api';
|
||||
import { FORMATTERS } from '../../../../../../../../common/formatters';
|
||||
import { euiStyled } from '../../../../../../../../../observability/public';
|
||||
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;
|
||||
currentTime: number;
|
||||
isLoading: boolean;
|
||||
searchFilter: Query;
|
||||
}
|
||||
|
||||
function useSortableProperties<T>(
|
||||
sortablePropertyItems: Array<{
|
||||
name: string;
|
||||
getValue: (obj: T) => any;
|
||||
isAscending: boolean;
|
||||
}>,
|
||||
defaultSortProperty: string
|
||||
) {
|
||||
const [sortableProperties] = useState<SortableProperties<T>>(
|
||||
new SortableProperties(sortablePropertyItems, defaultSortProperty)
|
||||
);
|
||||
const [sortedColumn, setSortedColumn] = useState(
|
||||
omit(sortableProperties.getSortedProperty(), 'getValue')
|
||||
);
|
||||
|
||||
return {
|
||||
setSortedColumn: useCallback(
|
||||
(property) => {
|
||||
sortableProperties.sortOn(property);
|
||||
setSortedColumn(omit(sortableProperties.getSortedProperty(), 'getValue'));
|
||||
},
|
||||
[sortableProperties]
|
||||
),
|
||||
sortedColumn,
|
||||
sortItems: (items: T[]) => sortableProperties.sortItems(items),
|
||||
};
|
||||
}
|
||||
|
||||
export const ProcessesTable = ({
|
||||
processList,
|
||||
currentTime,
|
||||
isLoading,
|
||||
searchFilter,
|
||||
}: TableProps) => {
|
||||
const [currentPage, setCurrentPage] = useState(0);
|
||||
const [itemsPerPage, setItemsPerPage] = useState(10);
|
||||
useEffect(() => setCurrentPage(0), [processList, searchFilter, itemsPerPage]);
|
||||
|
||||
const { sortedColumn, sortItems, setSortedColumn } = 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,
|
||||
},
|
||||
{
|
||||
name: 'cpu',
|
||||
getValue: (item: any) => item.cpu,
|
||||
isAscending: false,
|
||||
},
|
||||
{
|
||||
name: 'memory',
|
||||
getValue: (item: any) => item.memory,
|
||||
isAscending: false,
|
||||
},
|
||||
],
|
||||
'state'
|
||||
);
|
||||
|
||||
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]
|
||||
);
|
||||
|
||||
if (isLoading) return <LoadingPlaceholder />;
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiTable>
|
||||
<EuiTableHeader>
|
||||
<EuiTableHeaderCell width={24} />
|
||||
{columns.map((column) => (
|
||||
<EuiTableHeaderCell
|
||||
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}
|
||||
>
|
||||
{column.name}
|
||||
</EuiTableHeaderCell>
|
||||
))}
|
||||
</EuiTableHeader>
|
||||
<StyledTableBody>
|
||||
<ProcessesTableBody items={currentItemsPage} currentTime={currentTime} />
|
||||
</StyledTableBody>
|
||||
</EuiTable>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiTablePagination
|
||||
itemsPerPage={itemsPerPage}
|
||||
activePage={currentPage}
|
||||
pageCount={pageCount}
|
||||
itemsPerPageOptions={[10, 20, 50]}
|
||||
onChangePage={setCurrentPage}
|
||||
onChangeItemsPerPage={setItemsPerPage}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const LoadingPlaceholder = () => {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '200px',
|
||||
padding: '16px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<EuiLoadingChart size="xl" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface TableBodyProps {
|
||||
items: Process[];
|
||||
currentTime: number;
|
||||
}
|
||||
const ProcessesTableBody = ({ items, currentTime }: TableBodyProps) => (
|
||||
<>
|
||||
{items.map((item, i) => {
|
||||
const cells = columns.map((column) => (
|
||||
<EuiTableRowCell
|
||||
key={`${String(column.field)}-${i}`}
|
||||
header={column.name}
|
||||
align={column.align ?? LEFT_ALIGNMENT}
|
||||
textOnly={column.textOnly ?? true}
|
||||
>
|
||||
{column.render ? column.render(item[column.field], currentTime) : item[column.field]}
|
||||
</EuiTableRowCell>
|
||||
));
|
||||
return <ProcessRow cells={cells} item={item} key={`row-${i}`} />;
|
||||
})}
|
||||
</>
|
||||
);
|
||||
|
||||
const StyledTableBody = euiStyled(EuiTableBody)`
|
||||
& .euiTableCellContent {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
|
||||
}
|
||||
`;
|
||||
|
||||
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);
|
||||
let remainingRuntimeMS = runtimeLength;
|
||||
const runtimeHours = Math.floor(remainingRuntimeMS / ONE_HOUR);
|
||||
remainingRuntimeMS -= runtimeHours * ONE_HOUR;
|
||||
const runtimeMinutes = Math.floor(remainingRuntimeMS / ONE_MINUTE);
|
||||
remainingRuntimeMS -= runtimeMinutes * ONE_MINUTE;
|
||||
const runtimeSeconds = Math.floor(remainingRuntimeMS / 1000);
|
||||
remainingRuntimeMS -= runtimeSeconds * 1000;
|
||||
|
||||
const runtimeDisplayHours = runtimeHours ? `${runtimeHours}:` : '';
|
||||
const runtimeDisplayMinutes = runtimeMinutes < 10 ? `0${runtimeMinutes}:` : `${runtimeMinutes}:`;
|
||||
const runtimeDisplaySeconds = runtimeSeconds < 10 ? `0${runtimeSeconds}` : runtimeSeconds;
|
||||
|
||||
return <>{`${runtimeDisplayHours}${runtimeDisplayMinutes}${runtimeDisplaySeconds}`}</>;
|
||||
};
|
||||
|
||||
const columns: Array<{
|
||||
field: keyof Process;
|
||||
name: string;
|
||||
sortable: boolean;
|
||||
render?: Function;
|
||||
width?: string | number;
|
||||
textOnly?: boolean;
|
||||
align?: typeof RIGHT_ALIGNMENT | typeof LEFT_ALIGNMENT;
|
||||
}> = [
|
||||
{
|
||||
field: 'state',
|
||||
name: i18n.translate('xpack.infra.metrics.nodeDetails.processes.columnLabelState', {
|
||||
defaultMessage: 'State',
|
||||
}),
|
||||
sortable: true,
|
||||
render: (state: string) => <StateBadge state={state} />,
|
||||
width: 84,
|
||||
textOnly: false,
|
||||
},
|
||||
{
|
||||
field: 'command',
|
||||
name: i18n.translate('xpack.infra.metrics.nodeDetails.processes.columnLabelCommand', {
|
||||
defaultMessage: 'Command',
|
||||
}),
|
||||
sortable: true,
|
||||
width: '40%',
|
||||
render: (command: string) => <CodeLine>{command}</CodeLine>,
|
||||
},
|
||||
{
|
||||
field: 'startTime',
|
||||
name: i18n.translate('xpack.infra.metrics.nodeDetails.processes.columnLabelTime', {
|
||||
defaultMessage: 'Time',
|
||||
}),
|
||||
align: RIGHT_ALIGNMENT,
|
||||
sortable: true,
|
||||
render: (startTime: string, currentTime: number) => (
|
||||
<RuntimeCell startTime={startTime} currentTime={currentTime} />
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'cpu',
|
||||
name: i18n.translate('xpack.infra.metrics.nodeDetails.processes.columnLabelCPU', {
|
||||
defaultMessage: 'CPU',
|
||||
}),
|
||||
sortable: true,
|
||||
render: (value: number) => FORMATTERS.percent(value),
|
||||
},
|
||||
{
|
||||
field: 'memory',
|
||||
name: i18n.translate('xpack.infra.metrics.nodeDetails.processes.columnLabelMemory', {
|
||||
defaultMessage: 'Mem.',
|
||||
}),
|
||||
sortable: true,
|
||||
render: (value: number) => FORMATTERS.percent(value),
|
||||
},
|
||||
];
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* 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 from 'react';
|
||||
import { EuiBadge } from '@elastic/eui';
|
||||
import { STATE_NAMES } from './states';
|
||||
|
||||
export const StateBadge = ({ state }: { state: string }) => {
|
||||
switch (state) {
|
||||
case 'running':
|
||||
return <EuiBadge color="secondary">{STATE_NAMES.running}</EuiBadge>;
|
||||
case 'sleeping':
|
||||
return <EuiBadge color="default">{STATE_NAMES.sleeping}</EuiBadge>;
|
||||
case 'dead':
|
||||
return <EuiBadge color="danger">{STATE_NAMES.dead}</EuiBadge>;
|
||||
case 'stopped':
|
||||
return <EuiBadge color="warning">{STATE_NAMES.stopped}</EuiBadge>;
|
||||
case 'idle':
|
||||
return <EuiBadge color="primary">{STATE_NAMES.idle}</EuiBadge>;
|
||||
case 'zombie':
|
||||
return <EuiBadge color="danger">{STATE_NAMES.zombie}</EuiBadge>;
|
||||
default:
|
||||
return <EuiBadge color="hollow">{STATE_NAMES.unknown}</EuiBadge>;
|
||||
}
|
||||
};
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export const STATE_NAMES = {
|
||||
running: i18n.translate('xpack.infra.metrics.nodeDetails.processes.stateRunning', {
|
||||
defaultMessage: 'Running',
|
||||
}),
|
||||
sleeping: i18n.translate('xpack.infra.metrics.nodeDetails.processes.stateSleeping', {
|
||||
defaultMessage: 'Sleeping',
|
||||
}),
|
||||
dead: i18n.translate('xpack.infra.metrics.nodeDetails.processes.stateDead', {
|
||||
defaultMessage: 'Dead',
|
||||
}),
|
||||
stopped: i18n.translate('xpack.infra.metrics.nodeDetails.processes.stateStopped', {
|
||||
defaultMessage: 'Stopped',
|
||||
}),
|
||||
idle: i18n.translate('xpack.infra.metrics.nodeDetails.processes.stateIdle', {
|
||||
defaultMessage: 'Idle',
|
||||
}),
|
||||
zombie: i18n.translate('xpack.infra.metrics.nodeDetails.processes.stateZombie', {
|
||||
defaultMessage: 'Zombie',
|
||||
}),
|
||||
unknown: i18n.translate('xpack.infra.metrics.nodeDetails.processes.stateUnknown', {
|
||||
defaultMessage: 'Unknown',
|
||||
}),
|
||||
};
|
||||
|
||||
export const STATE_ORDER = ['running', 'sleeping', 'stopped', 'idle', 'dead', 'zombie', 'unknown'];
|
|
@ -0,0 +1,81 @@
|
|||
/*
|
||||
* 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 { mapValues, countBy } 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;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
type SummaryColumn = {
|
||||
total: number;
|
||||
} & Record<keyof typeof STATE_NAMES, number>;
|
||||
|
||||
export const SummaryTable = ({ processList, isLoading }: Props) => {
|
||||
const parsedList = parseProcessList(processList);
|
||||
const processCount = useMemo(
|
||||
() =>
|
||||
[
|
||||
{
|
||||
total: isLoading ? -1 : parsedList.length,
|
||||
...mapValues(STATE_NAMES, () => (isLoading ? -1 : 0)),
|
||||
...(isLoading ? [] : countBy(parsedList, 'state')),
|
||||
},
|
||||
] as SummaryColumn[],
|
||||
[parsedList, isLoading]
|
||||
);
|
||||
return (
|
||||
<StyleWrapper>
|
||||
<EuiBasicTable items={processCount} columns={columns} />
|
||||
</StyleWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
const loadingRenderer = (value: number) => (value === -1 ? <LoadingSpinner /> : value);
|
||||
|
||||
const columns = [
|
||||
{
|
||||
field: 'total',
|
||||
name: i18n.translate('xpack.infra.metrics.nodeDetails.processes.headingTotalProcesses', {
|
||||
defaultMessage: 'Total processes',
|
||||
}),
|
||||
width: 125,
|
||||
render: loadingRenderer,
|
||||
},
|
||||
...Object.entries(STATE_NAMES).map(([field, name]) => ({ field, name, render: loadingRenderer })),
|
||||
] as Array<EuiBasicTableColumn<SummaryColumn>>;
|
||||
|
||||
const LoadingSpinner = euiStyled(EuiLoadingSpinner).attrs({ size: 'm' })`
|
||||
margin-top: 2px;
|
||||
margin-bottom: 3px;
|
||||
`;
|
||||
|
||||
const StyleWrapper = euiStyled.div`
|
||||
& .euiTableHeaderCell {
|
||||
border-bottom: none;
|
||||
& .euiTableCellContent {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
& .euiTableCellContent__text {
|
||||
font-size: ${(props) => props.theme.eui.euiFontSizeS};
|
||||
}
|
||||
}
|
||||
|
||||
& .euiTableRowCell {
|
||||
border-top: none;
|
||||
& .euiTableCellContent {
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
`;
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* 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 { MetricsExplorerSeries } from '../../../../../../../../common/http_api';
|
||||
import { STATE_NAMES } from './states';
|
||||
|
||||
export interface Process {
|
||||
command: string;
|
||||
cpu: number;
|
||||
memory: number;
|
||||
startTime: number;
|
||||
state: keyof typeof STATE_NAMES;
|
||||
pid: number;
|
||||
user: string;
|
||||
timeseries: {
|
||||
[x: string]: MetricsExplorerSeries;
|
||||
};
|
||||
apmTrace?: string; // Placeholder
|
||||
}
|
|
@ -15,6 +15,13 @@ export interface TabProps {
|
|||
nodeType: InventoryItemType;
|
||||
}
|
||||
|
||||
export const OVERLAY_Y_START = 266;
|
||||
export const OVERLAY_BOTTOM_MARGIN = 16;
|
||||
export const OVERLAY_HEADER_SIZE = 96;
|
||||
const contentHeightOffset = OVERLAY_Y_START + OVERLAY_BOTTOM_MARGIN + OVERLAY_HEADER_SIZE;
|
||||
export const TabContent = euiStyled.div`
|
||||
padding: ${(props) => props.theme.eui.paddingSizes.l};
|
||||
padding: ${(props) => props.theme.eui.paddingSizes.s};
|
||||
height: calc(100vh - ${contentHeightOffset}px);
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
`;
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* 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 } from 'react';
|
||||
import { ProcessListAPIResponse, ProcessListAPIResponseRT } from '../../../../../common/http_api';
|
||||
import { throwErrors, createPlainError } from '../../../../../common/runtime_types';
|
||||
import { useHTTPRequest } from '../../../../hooks/use_http_request';
|
||||
|
||||
export function useProcessList(
|
||||
hostTerm: Record<string, string>,
|
||||
indexPattern: string,
|
||||
timefield: string,
|
||||
to: number
|
||||
) {
|
||||
const decodeResponse = (response: any) => {
|
||||
return pipe(
|
||||
ProcessListAPIResponseRT.decode(response),
|
||||
fold(throwErrors(createPlainError), identity)
|
||||
);
|
||||
};
|
||||
|
||||
const timerange = {
|
||||
field: timefield,
|
||||
interval: 'modules',
|
||||
to,
|
||||
from: to - 15 * 60 * 1000, // 15 minutes
|
||||
};
|
||||
|
||||
const { error, loading, response, makeRequest } = useHTTPRequest<ProcessListAPIResponse>(
|
||||
'/api/metrics/process_list',
|
||||
'POST',
|
||||
JSON.stringify({
|
||||
hostTerm,
|
||||
timerange,
|
||||
indexPattern,
|
||||
}),
|
||||
decodeResponse
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
makeRequest();
|
||||
}, [makeRequest]);
|
||||
|
||||
return {
|
||||
error,
|
||||
loading,
|
||||
response,
|
||||
makeRequest,
|
||||
};
|
||||
}
|
|
@ -41,6 +41,7 @@ import { initLogSourceConfigurationRoutes, initLogSourceStatusRoutes } from './r
|
|||
import { initSourceRoute } from './routes/source';
|
||||
import { initAlertPreviewRoute } from './routes/alerting';
|
||||
import { initGetLogAlertsChartPreviewDataRoute } from './routes/log_alerts';
|
||||
import { initProcessListRoute } from './routes/process_list';
|
||||
|
||||
export const initInfraServer = (libs: InfraBackendLibs) => {
|
||||
const schema = makeExecutableSchema({
|
||||
|
@ -82,4 +83,5 @@ export const initInfraServer = (libs: InfraBackendLibs) => {
|
|||
initLogSourceStatusRoutes(libs);
|
||||
initAlertPreviewRoute(libs);
|
||||
initGetLogAlertsChartPreviewDataRoute(libs);
|
||||
initProcessListRoute(libs);
|
||||
};
|
||||
|
|
64
x-pack/plugins/infra/server/lib/host_details/process_list.ts
Normal file
64
x-pack/plugins/infra/server/lib/host_details/process_list.ts
Normal file
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* 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 { ProcessListAPIRequest, MetricsAPIRequest } from '../../../common/http_api';
|
||||
import { getAllMetricsData } from '../../utils/get_all_metrics_data';
|
||||
import { query } from '../metrics';
|
||||
import { ESSearchClient } from '../metrics/types';
|
||||
|
||||
export const getProcessList = async (
|
||||
client: ESSearchClient,
|
||||
{ hostTerm, timerange, indexPattern }: 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',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'memory',
|
||||
aggregations: {
|
||||
memory: {
|
||||
avg: {
|
||||
field: 'system.process.memory.rss.pct',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'meta',
|
||||
aggregations: {
|
||||
meta: {
|
||||
top_hits: {
|
||||
size: 1,
|
||||
sort: [{ [timerange.field]: { order: 'desc' } }],
|
||||
_source: [
|
||||
'system.process.cpu.start_time',
|
||||
'system.process.state',
|
||||
'process.pid',
|
||||
'user.name',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
} as MetricsAPIRequest;
|
||||
return await getAllMetricsData((body: MetricsAPIRequest) => query(client, body), queryBody);
|
||||
};
|
50
x-pack/plugins/infra/server/routes/process_list/index.ts
Normal file
50
x-pack/plugins/infra/server/routes/process_list/index.ts
Normal 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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import Boom from '@hapi/boom';
|
||||
import { pipe } from 'fp-ts/lib/pipeable';
|
||||
import { fold } from 'fp-ts/lib/Either';
|
||||
import { identity } from 'fp-ts/lib/function';
|
||||
import { schema } from '@kbn/config-schema';
|
||||
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';
|
||||
|
||||
const escapeHatch = schema.object({}, { unknowns: 'allow' });
|
||||
|
||||
export const initProcessListRoute = (libs: InfraBackendLibs) => {
|
||||
const { framework } = libs;
|
||||
framework.registerRoute(
|
||||
{
|
||||
method: 'post',
|
||||
path: '/api/metrics/process_list',
|
||||
validate: {
|
||||
body: escapeHatch,
|
||||
},
|
||||
},
|
||||
async (requestContext, request, response) => {
|
||||
try {
|
||||
const options = pipe(
|
||||
ProcessListAPIRequestRT.decode(request.body),
|
||||
fold(throwErrors(Boom.badRequest), identity)
|
||||
);
|
||||
|
||||
const client = createSearchClient(requestContext, framework);
|
||||
const processListResponse = await getProcessList(client, options);
|
||||
|
||||
return response.ok({
|
||||
body: ProcessListAPIResponseRT.encode(processListResponse),
|
||||
});
|
||||
} catch (error) {
|
||||
return response.internalError({
|
||||
body: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
34
x-pack/plugins/infra/server/utils/get_all_metrics_data.ts
Normal file
34
x-pack/plugins/infra/server/utils/get_all_metrics_data.ts
Normal file
|
@ -0,0 +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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { MetricsAPIResponse, MetricsAPISeries } from '../../common/http_api/metrics_api';
|
||||
|
||||
export const getAllMetricsData = async <Options extends object = {}>(
|
||||
query: (options: Options) => Promise<MetricsAPIResponse>,
|
||||
options: Options,
|
||||
previousBuckets: MetricsAPISeries[] = []
|
||||
): Promise<MetricsAPISeries[]> => {
|
||||
const response = await query(options);
|
||||
|
||||
// Nothing available, return the previous buckets.
|
||||
if (response.series.length === 0) {
|
||||
return previousBuckets;
|
||||
}
|
||||
|
||||
const currentBuckets = response.series;
|
||||
|
||||
// if there are no currentBuckets then we are finished paginating through the results
|
||||
if (!response.info.afterKey) {
|
||||
return previousBuckets.concat(currentBuckets);
|
||||
}
|
||||
|
||||
// There is possibly more data, concat previous and current buckets and call ourselves recursively.
|
||||
const newOptions = {
|
||||
...options,
|
||||
afterKey: response.info.afterKey,
|
||||
};
|
||||
return getAllMetricsData(query, newOptions, previousBuckets.concat(currentBuckets));
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue