Skip to content

Commit

Permalink
[INFRA] Add alerts count to hosts data (elastic#176034)
Browse files Browse the repository at this point in the history
Closes elastic#175567

#### What has been done
- create alerts infra client to retrieve alerts data
- add endpoint to get the alerts count for hosts
- merged alerts count with hosts data
- add alertsCount badge to hosts table
- sort by alert count by clicking on the column title
- If no hosts have active alerts, the column should be hidden.
- Default hosts sorting is alerts count and cpu desc


https://github.com/elastic/kibana/assets/31922082/96794041-d129-49e2-920c-80b45c624144
  • Loading branch information
MiriamAparicio authored Feb 15, 2024
1 parent 930b012 commit f76e7ec
Show file tree
Hide file tree
Showing 17 changed files with 351 additions and 63 deletions.
1 change: 1 addition & 0 deletions x-pack/plugins/infra/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export const METRICS_APP = 'metrics';
export const LOGS_APP = 'logs';

export const METRICS_FEATURE_ID = 'infrastructure';
export const INFRA_ALERT_FEATURE_ID = 'infrastructure';
export const LOGS_FEATURE_ID = 'logs';

export type InfraFeatureId = typeof METRICS_FEATURE_ID | typeof LOGS_FEATURE_ID;
Expand Down
15 changes: 10 additions & 5 deletions x-pack/plugins/infra/common/http_api/infra/get_infra_metrics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,16 @@ export const GetInfraMetricsRequestBodyPayloadRT = rt.intersection([
}),
]);

export const InfraAssetMetricsItemRT = rt.type({
name: rt.string,
metrics: rt.array(InfraAssetMetricsRT),
metadata: rt.array(InfraAssetMetadataRT),
});
export const InfraAssetMetricsItemRT = rt.intersection([
rt.type({
name: rt.string,
metrics: rt.array(InfraAssetMetricsRT),
metadata: rt.array(InfraAssetMetadataRT),
}),
rt.partial({
alertsCount: rt.number,
}),
]);

export const GetInfraMetricsResponsePayloadRT = rt.type({
type: rt.literal('host'),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,4 @@ import type { ValidFeatureId } from '@kbn/rule-data-utils';
export const ALERTS_PER_PAGE = 10;
export const ALERTS_TABLE_ID = 'xpack.infra.hosts.alerts.table';

export const INFRA_ALERT_FEATURE_ID = 'infrastructure';
export const infraAlertFeatureIds: ValidFeatureId[] = [AlertConsumers.INFRASTRUCTURE];
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ const mockHostNode: InfraAssetMetricsItem[] = [
{ name: 'cloud.provider', value: 'aws' },
],
name: 'host-0',
alertsCount: 0,
},
{
metrics: [
Expand Down Expand Up @@ -109,6 +110,7 @@ const mockHostNode: InfraAssetMetricsItem[] = [
{ name: 'host.ip', value: '243.86.94.22' },
],
name: 'host-1',
alertsCount: 0,
},
];

Expand Down Expand Up @@ -161,6 +163,7 @@ describe('useHostTable hook', () => {
diskSpaceUsage: 0.2040001,
memoryFree: 34359.738368,
normalizedLoad1m: 239.2040001,
alertsCount: 0,
},
{
name: 'host-1',
Expand All @@ -178,6 +181,7 @@ describe('useHostTable hook', () => {
diskSpaceUsage: 0.5400000214576721,
memoryFree: 9.194304,
normalizedLoad1m: 100,
alertsCount: 0,
},
];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import { isEqual } from 'lodash';
import { isNumber } from 'lodash/fp';
import { CloudProvider } from '@kbn/custom-icons';
import { findInventoryModel } from '@kbn/metrics-data-access-plugin/common';
import { EuiToolTip } from '@elastic/eui';
import { EuiBadge } from '@elastic/eui';
import { useKibanaContextForPlugin } from '../../../../hooks/use_kibana';
import { createInventoryMetricFormatter } from '../../inventory_view/lib/create_inventory_metric_formatter';
import { EntryTitle } from '../components/table/entry_title';
Expand Down Expand Up @@ -44,6 +46,7 @@ interface HostMetadata {
export type HostNodeRow = HostMetadata &
HostMetrics & {
name: string;
alertsCount?: number;
};

/**
Expand All @@ -54,7 +57,7 @@ const formatMetric = (type: InfraAssetMetricType, value: number | undefined | nu
};

const buildItemsList = (nodes: InfraAssetMetricsItem[]): HostNodeRow[] => {
return nodes.map(({ metrics, metadata, name }) => {
return nodes.map(({ metrics, metadata, name, alertsCount }) => {
const metadataKeyValue = metadata.reduce(
(acc, curr) => ({
...acc,
Expand All @@ -79,12 +82,14 @@ const buildItemsList = (nodes: InfraAssetMetricsItem[]): HostNodeRow[] => {
}),
{} as HostMetrics
),

alertsCount: alertsCount ?? 0,
};
});
};

const isTitleColumn = (cell: any): cell is HostNodeRow['title'] => {
return typeof cell === 'object' && cell && 'name' in cell;
const isTitleColumn = (cell: HostNodeRow[keyof HostNodeRow]): cell is HostNodeRow['title'] => {
return cell !== null && typeof cell === 'object' && cell && 'name' in cell;
};

const sortValues = (aValue: any, bValue: any, { direction }: Sorting) => {
Expand Down Expand Up @@ -124,6 +129,8 @@ export const useHostsTable = () => {
const [selectedItems, setSelectedItems] = useState<HostNodeRow[]>([]);
const { hostNodes } = useHostsViewContext();

const displayAlerts = hostNodes.some((item) => 'alertsCount' in item);

const { value: formulas } = useAsync(() => inventoryModel.metrics.getFormulas());

const [{ detailsItemId, pagination, sorting }, setProperties] = useHostsTableUrlState();
Expand Down Expand Up @@ -221,6 +228,39 @@ export const useHostsTable = () => {
},
],
},
...(displayAlerts
? [
{
name: TABLE_COLUMN_LABEL.alertsCount,
field: 'alertsCount',
sortable: true,
'data-test-subj': 'hostsView-tableRow-alertsCount',
render: (alertsCount: HostNodeRow['alertsCount'], row: HostNodeRow) => {
if (!alertsCount) {
return null;
}
return (
<EuiToolTip position="top" content={TABLE_COLUMN_LABEL.alertsCount}>
<EuiBadge
iconType="warning"
color="danger"
onClick={() => {
setProperties({ detailsItemId: row.id === detailsItemId ? null : row.id });
}}
onClickAriaLabel={TABLE_COLUMN_LABEL.alertsCount}
iconOnClick={() => {
setProperties({ detailsItemId: row.id === detailsItemId ? null : row.id });
}}
iconOnClickAriaLabel={TABLE_COLUMN_LABEL.alertsCount}
>
{alertsCount}
</EuiBadge>
</EuiToolTip>
);
},
},
]
: []),
{
name: TABLE_COLUMN_LABEL.title,
field: 'title',
Expand Down Expand Up @@ -315,7 +355,6 @@ export const useHostsTable = () => {
'data-test-subj': 'hostsView-tableRow-rx',
render: (avg: number) => formatMetric('rx', avg),
align: 'right',
width: '120px',
},
{
name: (
Expand All @@ -330,7 +369,6 @@ export const useHostsTable = () => {
'data-test-subj': 'hostsView-tableRow-tx',
render: (avg: number) => formatMetric('tx', avg),
align: 'right',
width: '120px',
},
],
[
Expand All @@ -344,6 +382,7 @@ export const useHostsTable = () => {
formulas?.tx.value,
reportHostEntryClick,
setProperties,
displayAlerts,
]
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ import { DEFAULT_PAGE_SIZE, LOCAL_STORAGE_PAGE_SIZE_KEY } from '../constants';
export const GET_DEFAULT_TABLE_PROPERTIES: TableProperties = {
detailsItemId: null,
sorting: {
direction: 'asc',
field: 'name',
direction: 'desc',
field: 'alertsCount',
},
pagination: {
pageIndex: 0,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
import { i18n } from '@kbn/i18n';

export const TABLE_COLUMN_LABEL = {
alertsCount: i18n.translate('xpack.infra.hostsViewPage.table.alertsColumnHeader', {
defaultMessage: 'Active alerts',
}),

title: i18n.translate('xpack.infra.hostsViewPage.table.nameColumnHeader', {
defaultMessage: 'Name',
}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ import { PluginSetupContract as FeaturesPluginSetup } from '@kbn/features-plugin
import { SpacesPluginSetup } from '@kbn/spaces-plugin/server';
import { PluginSetupContract as AlertingPluginContract } from '@kbn/alerting-plugin/server';
import { MlPluginSetup } from '@kbn/ml-plugin/server';
import { RuleRegistryPluginSetupContract } from '@kbn/rule-registry-plugin/server';
import {
RuleRegistryPluginSetupContract,
RuleRegistryPluginStartContract,
} from '@kbn/rule-registry-plugin/server';
import { ObservabilityPluginSetup } from '@kbn/observability-plugin/server';
import { LogsSharedPluginSetup, LogsSharedPluginStart } from '@kbn/logs-shared-plugin/server';
import { VersionedRouteConfig } from '@kbn/core-http-server';
Expand Down Expand Up @@ -59,6 +62,7 @@ export interface InfraServerPluginStartDeps {
dataViews: DataViewsPluginStart;
logsShared: LogsSharedPluginStart;
profilingDataAccess?: ProfilingDataAccessPluginStart;
ruleRegistry: RuleRegistryPluginStartContract;
apmDataAccess: ApmDataAccessPluginStart;
}

Expand Down
13 changes: 12 additions & 1 deletion x-pack/plugins/infra/server/routes/infra/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
GetInfraMetricsResponsePayloadRT,
} from '../../../common/http_api/infra';
import { InfraBackendLibs } from '../../lib/infra_types';
import { getInfraAlertsClient } from './lib/helpers/get_infra_alerts_client';
import { getHosts } from './lib/host/get_hosts';

export const initInfraMetricsRoute = (libs: InfraBackendLibs) => {
Expand All @@ -35,10 +36,20 @@ export const initInfraMetricsRoute = (libs: InfraBackendLibs) => {

try {
const searchClient = data.search.asScoped(request);
const alertsClient = await getInfraAlertsClient({
getStartServices: libs.getStartServices,
request,
});
const soClient = savedObjects.getScopedClient(request);
const source = await libs.sources.getSourceConfiguration(soClient, params.sourceId);

const hosts = await getHosts({ searchClient, sourceConfig: source.configuration, params });
const hosts = await getHosts({
searchClient,
alertsClient,
sourceConfig: source.configuration,
params,
});

return response.ok({
body: GetInfraMetricsResponsePayloadRT.encode(hosts),
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { isEmpty } from 'lodash';
import { ESSearchRequest, InferSearchResponseOf } from '@kbn/es-types';
import { ParsedTechnicalFields } from '@kbn/rule-registry-plugin/common';
import { KibanaRequest } from '@kbn/core/server';
import type { InfraPluginStartServicesAccessor } from '../../../../types';

type RequiredParams = ESSearchRequest & {
size: number;
track_total_hits: boolean | number;
};

export type InfraAlertsClient = Awaited<ReturnType<typeof getInfraAlertsClient>>;

export async function getInfraAlertsClient({
getStartServices,
request,
}: {
getStartServices: InfraPluginStartServicesAccessor;
request: KibanaRequest;
}) {
const [, { ruleRegistry }] = await getStartServices();
const alertsClient = await ruleRegistry.getRacClientWithRequest(request);
const infraAlertsIndices = await alertsClient.getAuthorizedAlertsIndices(['infrastructure']);

if (!infraAlertsIndices || isEmpty(infraAlertsIndices)) {
throw Error('No alert indices exist for "infrastrucuture"');
}

return {
search<TParams extends RequiredParams>(
searchParams: TParams
): Promise<InferSearchResponseOf<ParsedTechnicalFields, TParams>> {
return alertsClient.find({
...searchParams,
index: infraAlertsIndices.join(','),
}) as Promise<any>;
},
};
}
20 changes: 18 additions & 2 deletions x-pack/plugins/infra/server/routes/infra/lib/host/get_hosts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { mapToApiResponse } from '../mapper';
import { hasFilters } from '../utils';
import { GetHostsArgs } from '../types';
import { getAllHosts } from './get_all_hosts';
import { getHostsAlertsCount } from './get_hosts_alerts_count';

export const getHosts = async (args: GetHostsArgs): Promise<GetInfraMetricsResponsePayload> => {
const runFilterQuery = hasFilters(args.params.query);
Expand All @@ -23,8 +24,23 @@ export const getHosts = async (args: GetHostsArgs): Promise<GetInfraMetricsRespo
};
}

const result = await getAllHosts(args, hostNamesShortList);
return mapToApiResponse(args.params, result?.nodes.buckets);
const {
range: { from, to },
limit,
} = args.params;

const [result, alertsCountResponse] = await Promise.all([
getAllHosts(args, hostNamesShortList),
getHostsAlertsCount({
alertsClient: args.alertsClient,
hostNamesShortList,
from,
to,
maxNumHosts: limit,
}),
]);

return mapToApiResponse(args.params, result?.nodes.buckets, alertsCountResponse);
};

const getFilteredHostNames = async (args: GetHostsArgs) => {
Expand Down
Loading

0 comments on commit f76e7ec

Please sign in to comment.