From 3e40a2fce8499b949a608dc8592597a956cc10d2 Mon Sep 17 00:00:00 2001 From: Zacqary Xeper Date: Tue, 1 Dec 2020 11:21:00 -0600 Subject: [PATCH 1/5] Rework Process List API to fetch top 20 processes --- .../http_api/host_details/process_list.ts | 83 ++++++++- .../node_details/tabs/processes/index.tsx | 22 ++- .../tabs/processes/process_row.tsx | 4 +- .../tabs/processes/processes_table.tsx | 92 ++++------ .../tabs/processes/summary_table.tsx | 14 +- .../inventory_view/hooks/use_process_list.ts | 27 ++- .../server/lib/host_details/process_list.ts | 160 +++++++++++++----- 7 files changed, 275 insertions(+), 127 deletions(-) diff --git a/x-pack/plugins/infra/common/http_api/host_details/process_list.ts b/x-pack/plugins/infra/common/http_api/host_details/process_list.ts index 4b4a0a54b9d13..8638aaa61a809 100644 --- a/x-pack/plugins/infra/common/http_api/host_details/process_list.ts +++ b/x-pack/plugins/infra/common/http_api/host_details/process_list.ts @@ -5,15 +5,92 @@ */ 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, + timefield: rt.string, indexPattern: rt.string, + to: rt.number, + sortBy: rt.type({ + name: rt.string, + isAscending: rt.boolean, + }), + searchFilter: rt.record(rt.string, rt.record(rt.string, rt.unknown)), }); -export const ProcessListAPIResponseRT = rt.array(MetricsAPISeriesRT); +export const ProcessListAPIQueryAggregationRT = rt.type({ + processCount: rt.type({ + value: rt.number, + }), + states: rt.type({ + buckets: rt.array( + rt.type({ + key: rt.string, + count: rt.type({ + value: rt.number, + }), + }) + ), + }), + processes: rt.type({ + filteredProcs: rt.type({ + buckets: rt.array( + rt.type({ + key: rt.string, + cpu: rt.type({ + value: rt.number, + }), + memory: rt.type({ + value: rt.number, + }), + 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, + }) + ), + summary: rt.type({ + total: rt.number, + statesCount: rt.record(rt.string, rt.number), + }), +}); + +export type ProcessListAPIQueryAggregation = rt.TypeOf; export type ProcessListAPIRequest = rt.TypeOf; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/index.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/index.tsx index 836d491e6210e..7aa604670b8e8 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/index.tsx @@ -7,7 +7,7 @@ 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 { useProcessList, SortBy } from '../../../../hooks/use_process_list'; import { TabContent, TabProps } from '../shared'; import { STATE_NAMES } from './states'; import { SummaryTable } from './summary_table'; @@ -15,6 +15,10 @@ import { ProcessesTable } from './processes_table'; const TabComponent = ({ currentTime, node, nodeType, options }: TabProps) => { const [searchFilter, setSearchFilter] = useState(EuiSearchBar.Query.MATCH_ALL); + const [sortBy, setSortBy] = useState({ + name: 'cpu', + isAscending: false, + }); const hostTerm = useMemo(() => { const field = @@ -28,9 +32,13 @@ const TabComponent = ({ currentTime, node, nodeType, options }: TabProps) => { hostTerm, 'metricbeat-*', options.fields!.timestamp, - currentTime + currentTime, + sortBy, + Query.toESQuery(searchFilter) ); + console.log(Query.toESQuery(searchFilter), Query.toESQueryString(searchFilter)); + if (error) { return ( @@ -57,7 +65,10 @@ const TabComponent = ({ currentTime, node, nodeType, options }: TabProps) => { return ( - + { ); diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/process_row.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/process_row.tsx index bbf4a25fc49a7..bcb863b0eb6e2 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/process_row.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/process_row.tsx @@ -118,7 +118,7 @@ export const ProcessRow = ({ cells, item }: Props) => { {item.user} - + {/* {cpuMetricLabel} { label={memoryMetricLabel} /> - + */} diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/processes_table.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/processes_table.tsx index 43f3a333fda83..a0b4b20a8c3b2 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/processes_table.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/processes_table.tsx @@ -24,17 +24,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( @@ -43,25 +44,21 @@ function useSortableProperties( getValue: (obj: T) => any; isAscending: boolean; }>, - defaultSortProperty: string + defaultSortProperty: string, + callback: (s: SortBy) => void ) { const [sortableProperties] = useState>( 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 +66,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( + const { updateSortableProperties } = useSortableProperties( [ - { - 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,28 +87,16 @@ 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 ; @@ -139,19 +111,19 @@ 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} ))} - + - + {/* + /> */} ); }; @@ -213,8 +185,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 +216,7 @@ const columns: Array<{ name: i18n.translate('xpack.infra.metrics.nodeDetails.processes.columnLabelState', { defaultMessage: 'State', }), - sortable: true, + sortable: false, render: (state: string) => , width: 84, textOnly: false, @@ -254,7 +226,7 @@ const columns: Array<{ name: i18n.translate('xpack.infra.metrics.nodeDetails.processes.columnLabelCommand', { defaultMessage: 'Command', }), - sortable: true, + sortable: false, width: '40%', render: (command: string) => {command}, }, @@ -265,7 +237,7 @@ const columns: Array<{ }), align: RIGHT_ALIGNMENT, sortable: true, - render: (startTime: string, currentTime: number) => ( + render: (startTime: number, currentTime: number) => ( ), }, diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/summary_table.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/summary_table.tsx index 59becb0bf534d..81d8829c9027d 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/summary_table.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/summary_table.tsx @@ -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; -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.statesCount), }, ] as SummaryColumn[], - [parsedList, isLoading] + [processSummary, isLoading] ); return ( diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_process_list.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_process_list.ts index 8e0843fe8b278..593ff4b0e0124 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_process_list.ts +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_process_list.ts @@ -11,11 +11,18 @@ import { ProcessListAPIResponse, ProcessListAPIResponseRT } from '../../../../.. import { throwErrors, createPlainError } from '../../../../../common/runtime_types'; import { useHTTPRequest } from '../../../../hooks/use_http_request'; +export interface SortBy { + name: string; + isAscending: boolean; +} + export function useProcessList( hostTerm: Record, indexPattern: string, timefield: string, - to: number + to: number, + sortBy: SortBy, + searchFilter: object ) { const decodeResponse = (response: any) => { return pipe( @@ -24,20 +31,24 @@ 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( '/api/metrics/process_list', 'POST', JSON.stringify({ hostTerm, - timerange, + timefield, indexPattern, + to, + sortBy: parsedSortBy, + searchFilter: {}, }), decodeResponse ); diff --git a/x-pack/plugins/infra/server/lib/host_details/process_list.ts b/x-pack/plugins/infra/server/lib/host_details/process_list.ts index 99e8b2e8f6ab1..87153b2fffddf 100644 --- a/x-pack/plugins/infra/server/lib/host_details/process_list.ts +++ b/x-pack/plugins/infra/server/lib/host_details/process_list.ts @@ -4,61 +4,139 @@ * 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'; 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 boolFilter = searchFilter.bool ?? {}; + const shouldAgg = [ + { + match_all: {}, + }, + ]; + + const body = { + size: 0, + query: { + bool: { + filter: [ + { + range: { + [timefield]: { + gte: to - 60 * 1000, // 1 minute + lte: to, + }, }, }, + { + term: hostTerm, + }, + ], + }, + }, + aggs: { + processCount: { + cardinality: { + field: 'system.process.cmdline', }, }, - { - id: 'memory', - aggregations: { - memory: { - avg: { - field: 'system.process.memory.rss.pct', + states: { + terms: { + field: 'system.process.state', + size: 10, + }, + aggs: { + count: { + cardinality: { + field: 'system.process.cmdline', }, }, }, }, - { - 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', - ], + processes: { + filter: { + bool: { + should: shouldAgg, + }, + }, + aggs: { + filteredProcs: { + terms: { + field: 'system.process.cmdline', + size: 20, + 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'], + }, + }, }, }, }, }, - ], - } as MetricsAPIRequest; - return await getAllMetricsData((body: MetricsAPIRequest) => query(client, body), queryBody); + }, + }; + 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 { processCount, states } = result.aggregations!; + const statesCount = states.buckets.reduce( + (resultRecord, { key, count }) => ({ ...resultRecord, [key]: count.value }), + {} + ); + return { + processList, + summary: { + total: processCount.value, + statesCount, + }, + }; + } catch (e) { + throw e; + } }; From 774e53166e037f30af2e4d692bb6ea351e0f8004 Mon Sep 17 00:00:00 2001 From: Zacqary Xeper Date: Tue, 1 Dec 2020 13:29:14 -0600 Subject: [PATCH 2/5] Get search bar working --- .../http_api/host_details/process_list.ts | 2 +- .../node_details/tabs/processes/index.tsx | 80 ++++++++++--------- .../tabs/processes/parse_process_list.ts | 55 ------------- .../tabs/processes/parse_search_string.ts | 37 +++++++++ .../tabs/processes/processes_table.tsx | 16 +++- .../inventory_view/hooks/use_process_list.ts | 10 ++- .../server/lib/host_details/process_list.ts | 9 +-- 7 files changed, 103 insertions(+), 106 deletions(-) delete mode 100644 x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/parse_process_list.ts create mode 100644 x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/parse_search_string.ts diff --git a/x-pack/plugins/infra/common/http_api/host_details/process_list.ts b/x-pack/plugins/infra/common/http_api/host_details/process_list.ts index 8638aaa61a809..4bce244d89ad4 100644 --- a/x-pack/plugins/infra/common/http_api/host_details/process_list.ts +++ b/x-pack/plugins/infra/common/http_api/host_details/process_list.ts @@ -15,7 +15,7 @@ export const ProcessListAPIRequestRT = rt.type({ name: rt.string, isAscending: rt.boolean, }), - searchFilter: rt.record(rt.string, rt.record(rt.string, rt.unknown)), + searchFilter: rt.array(rt.record(rt.string, rt.record(rt.string, rt.unknown))), }); export const ProcessListAPIQueryAggregationRT = rt.type({ diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/index.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/index.tsx index 7aa604670b8e8..2fe1fb8598d3e 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/index.tsx @@ -5,16 +5,18 @@ */ 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 { EuiSearchBar, EuiSpacer, EuiEmptyPrompt, EuiButton } from '@elastic/eui'; import { useProcessList, SortBy } 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(EuiSearchBar.Query.MATCH_ALL); + const [searchFilter, setSearchFilter] = useState(''); const [sortBy, setSortBy] = useState({ name: 'cpu', isAscending: false, @@ -34,45 +36,27 @@ const TabComponent = ({ currentTime, node, nodeType, options }: TabProps) => { options.fields!.timestamp, currentTime, sortBy, - Query.toESQuery(searchFilter) + parseSearchString(searchFilter) ); - console.log(Query.toESQuery(searchFilter), Query.toESQueryString(searchFilter)); - - if (error) { - return ( - - - {i18n.translate('xpack.infra.metrics.nodeDetails.processListError', { - defaultMessage: 'Unable to show process data', - })} - - } - actions={ - - {i18n.translate('xpack.infra.metrics.nodeDetails.processListRetry', { - defaultMessage: 'Try again', - })} - - } - /> - - ); - } + const debouncedSearchOnChange = useMemo( + () => + debounce<(props: { queryText: string }) => void>( + ({ queryText }) => setSearchFilter(queryText), + 500 + ), + [setSearchFilter] + ); return ( setSearchFilter(query ?? EuiSearchBar.Query.MATCH_ALL)} + onChange={debouncedSearchOnChange} box={{ incremental: true, placeholder: i18n.translate('xpack.infra.metrics.nodeDetails.searchForProcesses', { @@ -94,13 +78,33 @@ const TabComponent = ({ currentTime, node, nodeType, options }: TabProps) => { ]} /> - + {!error ? ( + + ) : ( + + {i18n.translate('xpack.infra.metrics.nodeDetails.processListError', { + defaultMessage: 'Unable to show process data', + })} + + } + actions={ + + {i18n.translate('xpack.infra.metrics.nodeDetails.processListRetry', { + defaultMessage: 'Try again', + })} + + } + /> + )} ); }; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/parse_process_list.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/parse_process_list.ts deleted file mode 100644 index 88584ef2987e1..0000000000000 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/parse_process_list.ts +++ /dev/null @@ -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, -}); diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/parse_search_string.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/parse_search_string.ts new file mode 100644 index 0000000000000..455656306c8d0 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/parse_search_string.ts @@ -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'); diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/processes_table.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/processes_table.tsx index a0b4b20a8c3b2..4b4cc3d3e611d 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/processes_table.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/processes_table.tsx @@ -16,7 +16,7 @@ import { EuiSpacer, EuiTablePagination, EuiLoadingChart, - Query, + EuiEmptyPrompt, SortableProperties, LEFT_ALIGNMENT, RIGHT_ALIGNMENT, @@ -101,6 +101,20 @@ export const ProcessesTable = ({ if (isLoading) return ; + if (currentItems.length === 0) + return ( + + {i18n.translate('xpack.infra.metrics.nodeDetails.noProcesses', { + defaultMessage: 'No processes matched these search terms', + })} + + } + /> + ); + return ( <> diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_process_list.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_process_list.ts index 593ff4b0e0124..3b85b3a17085e 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_process_list.ts +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_process_list.ts @@ -6,7 +6,7 @@ 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'; @@ -24,6 +24,7 @@ export function useProcessList( sortBy: SortBy, searchFilter: object ) { + const [inErrorState, setInErrorState] = useState(false); const decodeResponse = (response: any) => { return pipe( ProcessListAPIResponseRT.decode(response), @@ -48,17 +49,20 @@ export function useProcessList( indexPattern, to, sortBy: parsedSortBy, - searchFilter: {}, + searchFilter, }), decodeResponse ); + useEffect(() => setInErrorState(true), [error]); + useEffect(() => setInErrorState(false), [loading]); + useEffect(() => { makeRequest(); }, [makeRequest]); return { - error, + error: inErrorState, loading, response, makeRequest, diff --git a/x-pack/plugins/infra/server/lib/host_details/process_list.ts b/x-pack/plugins/infra/server/lib/host_details/process_list.ts index 87153b2fffddf..a9e2ea74ff42c 100644 --- a/x-pack/plugins/infra/server/lib/host_details/process_list.ts +++ b/x-pack/plugins/infra/server/lib/host_details/process_list.ts @@ -11,13 +11,6 @@ export const getProcessList = async ( search: ESSearchClient, { hostTerm, timefield, indexPattern, to, sortBy, searchFilter }: ProcessListAPIRequest ) => { - const boolFilter = searchFilter.bool ?? {}; - const shouldAgg = [ - { - match_all: {}, - }, - ]; - const body = { size: 0, query: { @@ -59,7 +52,7 @@ export const getProcessList = async ( processes: { filter: { bool: { - should: shouldAgg, + must: searchFilter ?? [{ match_all: {} }], }, }, aggs: { From 80036072d8d81b8f1968ac90235e087eda241663 Mon Sep 17 00:00:00 2001 From: Zacqary Xeper Date: Tue, 1 Dec 2020 17:07:25 -0600 Subject: [PATCH 3/5] Add lazy loading of CPU and memory charts --- .../http_api/host_details/process_list.ts | 65 +++++-- .../node_details/tabs/processes/index.tsx | 121 +++++++------ .../tabs/processes/process_row.tsx | 22 +-- .../tabs/processes/process_row_charts.tsx | 164 ++++++++++++++++++ .../tabs/processes/processes_table.tsx | 13 +- .../inventory_view/hooks/use_process_list.ts | 19 +- .../hooks/use_process_list_row_chart.ts | 54 ++++++ .../infra/server/lib/host_details/common.ts | 6 + .../server/lib/host_details/process_list.ts | 10 +- .../lib/host_details/process_list_chart.ts | 137 +++++++++++++++ .../infra/server/routes/process_list/index.ts | 37 +++- 11 files changed, 541 insertions(+), 107 deletions(-) create mode 100644 x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/process_row_charts.tsx create mode 100644 x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_process_list_row_chart.ts create mode 100644 x-pack/plugins/infra/server/lib/host_details/common.ts create mode 100644 x-pack/plugins/infra/server/lib/host_details/process_list_chart.ts diff --git a/x-pack/plugins/infra/common/http_api/host_details/process_list.ts b/x-pack/plugins/infra/common/http_api/host_details/process_list.ts index 4bce244d89ad4..87855128c5280 100644 --- a/x-pack/plugins/infra/common/http_api/host_details/process_list.ts +++ b/x-pack/plugins/infra/common/http_api/host_details/process_list.ts @@ -5,6 +5,11 @@ */ import * as rt from 'io-ts'; +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), @@ -19,16 +24,12 @@ export const ProcessListAPIRequestRT = rt.type({ }); export const ProcessListAPIQueryAggregationRT = rt.type({ - processCount: rt.type({ - value: rt.number, - }), + processCount: AggValueRT, states: rt.type({ buckets: rt.array( rt.type({ key: rt.string, - count: rt.type({ - value: rt.number, - }), + count: AggValueRT, }) ), }), @@ -37,12 +38,8 @@ export const ProcessListAPIQueryAggregationRT = rt.type({ buckets: rt.array( rt.type({ key: rt.string, - cpu: rt.type({ - value: rt.number, - }), - memory: rt.type({ - value: rt.number, - }), + cpu: AggValueRT, + memory: AggValueRT, startTime: rt.type({ value_as_string: rt.string, }), @@ -82,6 +79,7 @@ export const ProcessListAPIResponseRT = rt.type({ pid: rt.number, state: rt.string, user: rt.string, + command: rt.string, }) ), summary: rt.type({ @@ -95,3 +93,46 @@ export type ProcessListAPIQueryAggregation = rt.TypeOf; export type ProcessListAPIResponse = rt.TypeOf; + +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; + +export type ProcessListAPIChartResponse = rt.TypeOf; + +export type ProcessListAPIRow = MetricsAPIRow; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/index.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/index.tsx index 2fe1fb8598d3e..dc82e9a58db52 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/index.tsx @@ -4,11 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useMemo, useState } from 'react'; +import React, { useMemo, useState, useCallback } from 'react'; import { debounce } from 'lodash'; import { i18n } from '@kbn/i18n'; import { EuiSearchBar, EuiSpacer, EuiEmptyPrompt, EuiButton } from '@elastic/eui'; -import { useProcessList, SortBy } from '../../../../hooks/use_process_list'; +import { + useProcessList, + SortBy, + ProcessListContextProvider, +} from '../../../../hooks/use_process_list'; import { TabContent, TabProps } from '../shared'; import { STATE_NAMES } from './states'; import { SummaryTable } from './summary_table'; @@ -22,6 +26,8 @@ const TabComponent = ({ currentTime, node, nodeType, options }: TabProps) => { isAscending: false, }); + const timefield = options.fields!.timestamp; + const hostTerm = useMemo(() => { const field = options.fields && Reflect.has(options.fields, nodeType) @@ -32,8 +38,7 @@ const TabComponent = ({ currentTime, node, nodeType, options }: TabProps) => { const { loading, error, response, makeRequest: reload } = useProcessList( hostTerm, - 'metricbeat-*', - options.fields!.timestamp, + timefield, currentTime, sortBy, parseSearchString(searchFilter) @@ -50,61 +55,63 @@ const TabComponent = ({ currentTime, node, nodeType, options }: TabProps) => { return ( - - - ({ - value, - view, - })), - }, - ]} - /> - - {!error ? ( - + - ) : ( - - {i18n.translate('xpack.infra.metrics.nodeDetails.processListError', { - defaultMessage: 'Unable to show process data', - })} - - } - actions={ - - {i18n.translate('xpack.infra.metrics.nodeDetails.processListRetry', { - defaultMessage: 'Try again', - })} - - } + + ({ + value, + view, + })), + }, + ]} /> - )} + + {!error ? ( + + ) : ( + + {i18n.translate('xpack.infra.metrics.nodeDetails.processListError', { + defaultMessage: 'Unable to show process data', + })} + + } + actions={ + + {i18n.translate('xpack.infra.metrics.nodeDetails.processListRetry', { + defaultMessage: 'Try again', + })} + + } + /> + )} + ); }; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/process_row.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/process_row.tsx index bcb863b0eb6e2..f95d360a06c9c 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/process_row.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/process_row.tsx @@ -34,6 +34,7 @@ import { MetricsExplorerAggregation } from '../../../../../../../../common/http_ 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 +119,7 @@ export const ProcessRow = ({ cells, item }: Props) => { {item.user} - {/* - {cpuMetricLabel} - - - - - - {memoryMetricLabel} - - - - */} + diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/process_row_charts.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/process_row_charts.tsx new file mode 100644 index 0000000000000..7b7a285b5d6b8 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/process_row_charts.tsx @@ -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 ? ( + {failedToLoadChart}} /> + ) : isLoading ? ( + + ) : ( + + ); + const memoryChart = error ? ( + {failedToLoadChart}} /> + ) : isLoading ? ( + + ) : ( + + ); + + return ( + <> + + {cpuMetricLabel} + {cpuChart} + + + {memoryMetricLabel} + {memoryChart} + + + ); +}; + +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('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 ( + + + + + + + + + ); +}; + +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', + } +); diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/processes_table.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/processes_table.tsx index 4b4cc3d3e611d..1952ba947761c 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/processes_table.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/processes_table.tsx @@ -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,8 +13,6 @@ import { EuiTableBody, EuiTableHeaderCell, EuiTableRowCell, - EuiSpacer, - EuiTablePagination, EuiLoadingChart, EuiEmptyPrompt, SortableProperties, @@ -137,15 +135,6 @@ export const ProcessesTable = ({ - {/* - */} ); }; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_process_list.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_process_list.ts index 3b85b3a17085e..888c4321a1905 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_process_list.ts +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_process_list.ts @@ -3,6 +3,7 @@ * 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'; @@ -10,6 +11,7 @@ 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; @@ -18,12 +20,14 @@ export interface SortBy { export function useProcessList( hostTerm: Record, - indexPattern: string, timefield: string, 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( @@ -68,3 +72,16 @@ export function useProcessList( makeRequest, }; } + +function useProcessListParams(props: { + hostTerm: Record; + 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; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_process_list_row_chart.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_process_list_row_chart.ts new file mode 100644 index 0000000000000..ef638319fd9f4 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_process_list_row_chart.ts @@ -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( + '/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, + }; +} diff --git a/x-pack/plugins/infra/server/lib/host_details/common.ts b/x-pack/plugins/infra/server/lib/host_details/common.ts new file mode 100644 index 0000000000000..ddf606e417126 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/host_details/common.ts @@ -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'; diff --git a/x-pack/plugins/infra/server/lib/host_details/process_list.ts b/x-pack/plugins/infra/server/lib/host_details/process_list.ts index a9e2ea74ff42c..ed29ce37c4a6a 100644 --- a/x-pack/plugins/infra/server/lib/host_details/process_list.ts +++ b/x-pack/plugins/infra/server/lib/host_details/process_list.ts @@ -6,6 +6,7 @@ import { ProcessListAPIRequest, ProcessListAPIQueryAggregation } from '../../../common/http_api'; import { ESSearchClient } from '../metrics/types'; +import { CMDLINE_FIELD } from './common'; export const getProcessList = async ( search: ESSearchClient, @@ -33,7 +34,7 @@ export const getProcessList = async ( aggs: { processCount: { cardinality: { - field: 'system.process.cmdline', + field: CMDLINE_FIELD, }, }, states: { @@ -44,7 +45,7 @@ export const getProcessList = async ( aggs: { count: { cardinality: { - field: 'system.process.cmdline', + field: CMDLINE_FIELD, }, }, }, @@ -58,7 +59,7 @@ export const getProcessList = async ( aggs: { filteredProcs: { terms: { - field: 'system.process.cmdline', + field: CMDLINE_FIELD, size: 20, order: { [sortBy.name]: sortBy.isAscending ? 'asc' : 'desc', @@ -107,6 +108,7 @@ export const getProcessList = async ( 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, @@ -119,7 +121,7 @@ export const getProcessList = async ( }); const { processCount, states } = result.aggregations!; const statesCount = states.buckets.reduce( - (resultRecord, { key, count }) => ({ ...resultRecord, [key]: count.value }), + (stateResult, { key, count }) => ({ ...stateResult, [key]: count.value }), {} ); return { diff --git a/x-pack/plugins/infra/server/lib/host_details/process_list_chart.ts b/x-pack/plugins/infra/server/lib/host_details/process_list_chart.ts new file mode 100644 index 0000000000000..94363dece60a9 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/host_details/process_list_chart.ts @@ -0,0 +1,137 @@ +/* + * 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, +} 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; + } catch (e) { + throw e; + } +}; + +const TS_COLUMNS = [ + { + name: 'timestamp', + type: 'date', + }, + { + name: 'metric_0', + type: 'number', + }, +]; diff --git a/x-pack/plugins/infra/server/routes/process_list/index.ts b/x-pack/plugins/infra/server/routes/process_list/index.ts index 9851613255d8d..cf7765737e78b 100644 --- a/x-pack/plugins/infra/server/routes/process_list/index.ts +++ b/x-pack/plugins/infra/server/routes/process_list/index.ts @@ -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, + }); + } + } + ); }; From dbbebbe2414f4370e77ccff5b0ed5202bd74cfa5 Mon Sep 17 00:00:00 2001 From: Zacqary Xeper Date: Wed, 2 Dec 2020 11:43:38 -0600 Subject: [PATCH 4/5] Fix typecheck --- .../node_details/tabs/processes/index.tsx | 2 +- .../tabs/processes/process_row.tsx | 102 +----------------- .../lib/host_details/process_list_chart.ts | 3 +- 3 files changed, 4 insertions(+), 103 deletions(-) diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/index.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/index.tsx index dc82e9a58db52..f20ad2bdb4552 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useMemo, useState, useCallback } from 'react'; +import React, { useMemo, useState } from 'react'; import { debounce } from 'lodash'; import { i18n } from '@kbn/i18n'; import { EuiSearchBar, EuiSpacer, EuiEmptyPrompt, EuiButton } from '@elastic/eui'; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/process_row.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/process_row.tsx index f95d360a06c9c..4718ed09dc9b2 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/process_row.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/process_row.tsx @@ -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,16 +20,7 @@ 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'; @@ -131,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('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 ( - - - - - - - - - ); -}; - export const CodeLine = euiStyled(EuiCode).attrs({ transparentBackground: true, })` @@ -228,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', - } -); diff --git a/x-pack/plugins/infra/server/lib/host_details/process_list_chart.ts b/x-pack/plugins/infra/server/lib/host_details/process_list_chart.ts index 94363dece60a9..11df1937764c8 100644 --- a/x-pack/plugins/infra/server/lib/host_details/process_list_chart.ts +++ b/x-pack/plugins/infra/server/lib/host_details/process_list_chart.ts @@ -9,6 +9,7 @@ import { ProcessListAPIChartRequest, ProcessListAPIChartQueryAggregation, ProcessListAPIRow, + ProcessListAPIChartResponse, } from '../../../common/http_api'; import { ESSearchClient } from '../metrics/types'; import { CMDLINE_FIELD } from './common'; @@ -119,7 +120,7 @@ export const getProcessListChart = async ( ) ) ); - return timeseries; + return timeseries as ProcessListAPIChartResponse; } catch (e) { throw e; } From 101205f91722a3f9371e74a8afbd115822f9fec1 Mon Sep 17 00:00:00 2001 From: Zacqary Xeper Date: Thu, 3 Dec 2020 15:12:07 -0600 Subject: [PATCH 5/5] Fix fetching process summary when Metricbeat has include_top_n enabled --- .../http_api/host_details/process_list.ts | 29 +++++++----- .../node_details/tabs/processes/index.tsx | 2 +- .../tabs/processes/summary_table.tsx | 2 +- .../server/lib/host_details/process_list.ts | 46 ++++++++++--------- 4 files changed, 43 insertions(+), 36 deletions(-) diff --git a/x-pack/plugins/infra/common/http_api/host_details/process_list.ts b/x-pack/plugins/infra/common/http_api/host_details/process_list.ts index 87855128c5280..3141d100307c4 100644 --- a/x-pack/plugins/infra/common/http_api/host_details/process_list.ts +++ b/x-pack/plugins/infra/common/http_api/host_details/process_list.ts @@ -24,14 +24,22 @@ export const ProcessListAPIRequestRT = rt.type({ }); export const ProcessListAPIQueryAggregationRT = rt.type({ - processCount: AggValueRT, - states: rt.type({ - buckets: rt.array( - rt.type({ - key: rt.string, - count: AggValueRT, - }) - ), + 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({ @@ -82,10 +90,7 @@ export const ProcessListAPIResponseRT = rt.type({ command: rt.string, }) ), - summary: rt.type({ - total: rt.number, - statesCount: rt.record(rt.string, rt.number), - }), + summary: rt.record(rt.string, rt.number), }); export type ProcessListAPIQueryAggregation = rt.TypeOf; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/index.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/index.tsx index f20ad2bdb4552..6ee3c9f1fae80 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/index.tsx @@ -58,7 +58,7 @@ const TabComponent = ({ currentTime, node, nodeType, options }: TabProps) => { { { total: isLoading ? -1 : processSummary.total, ...mapValues(STATE_NAMES, () => (isLoading ? -1 : 0)), - ...(isLoading ? {} : processSummary.statesCount), + ...(isLoading ? {} : processSummary), }, ] as SummaryColumn[], [processSummary, isLoading] diff --git a/x-pack/plugins/infra/server/lib/host_details/process_list.ts b/x-pack/plugins/infra/server/lib/host_details/process_list.ts index ed29ce37c4a6a..e9d35f3601634 100644 --- a/x-pack/plugins/infra/server/lib/host_details/process_list.ts +++ b/x-pack/plugins/infra/server/lib/host_details/process_list.ts @@ -8,6 +8,8 @@ import { ProcessListAPIRequest, ProcessListAPIQueryAggregation } from '../../../ import { ESSearchClient } from '../metrics/types'; import { CMDLINE_FIELD } from './common'; +const TOP_N = 10; + export const getProcessList = async ( search: ESSearchClient, { hostTerm, timefield, indexPattern, to, sortBy, searchFilter }: ProcessListAPIRequest @@ -32,20 +34,24 @@ export const getProcessList = async ( }, }, aggs: { - processCount: { - cardinality: { - field: CMDLINE_FIELD, - }, - }, - states: { - terms: { - field: 'system.process.state', - size: 10, + summaryEvent: { + filter: { + term: { + 'event.dataset': 'system.process.summary', + }, }, aggs: { - count: { - cardinality: { - field: CMDLINE_FIELD, + summary: { + top_hits: { + size: 1, + sort: [ + { + [timefield]: { + order: 'desc', + }, + }, + ], + _source: ['system.process.summary'], }, }, }, @@ -60,7 +66,7 @@ export const getProcessList = async ( filteredProcs: { terms: { field: CMDLINE_FIELD, - size: 20, + size: TOP_N, order: { [sortBy.name]: sortBy.isAscending ? 'asc' : 'desc', }, @@ -119,17 +125,13 @@ export const getProcessList = async ( command: bucket.key, }; }); - const { processCount, states } = result.aggregations!; - const statesCount = states.buckets.reduce( - (stateResult, { key, count }) => ({ ...stateResult, [key]: count.value }), - {} - ); + const { + summary, + } = result.aggregations!.summaryEvent.summary.hits.hits[0]._source.system.process; + return { processList, - summary: { - total: processCount.value, - statesCount, - }, + summary, }; } catch (e) { throw e;