From 9def784c3d2f13243b5eda4b88d8567ee3f9b909 Mon Sep 17 00:00:00 2001 From: Kevin Delemme Date: Wed, 19 Jun 2024 09:04:48 -0400 Subject: [PATCH] feat(slo): group by instance id (#186131) --- .../src/rest_specs/routes/find_group.ts | 1 + .../kbn-slo-schema/src/schema/common.ts | 1 + .../grouped_slos/group_list_view.tsx | 23 +----- .../grouped_slos/group_view.test.tsx | 56 +++++++++++++- .../grouped_slos/hooks/use_group_name.test.ts | 76 +++++++++++++++++++ .../grouped_slos/hooks/use_group_name.ts | 67 ++++++++++++++++ .../slos/components/slo_list_group_by.tsx | 18 ++++- .../slo/server/services/find_slo_groups.ts | 10 ++- 8 files changed, 225 insertions(+), 27 deletions(-) create mode 100644 x-pack/plugins/observability_solution/slo/public/pages/slos/components/grouped_slos/hooks/use_group_name.test.ts create mode 100644 x-pack/plugins/observability_solution/slo/public/pages/slos/components/grouped_slos/hooks/use_group_name.ts diff --git a/x-pack/packages/kbn-slo-schema/src/rest_specs/routes/find_group.ts b/x-pack/packages/kbn-slo-schema/src/rest_specs/routes/find_group.ts index 3c04134c8331286..7c3f5e576cea502 100644 --- a/x-pack/packages/kbn-slo-schema/src/rest_specs/routes/find_group.ts +++ b/x-pack/packages/kbn-slo-schema/src/rest_specs/routes/find_group.ts @@ -12,6 +12,7 @@ const groupBySchema = t.union([ t.literal('slo.tags'), t.literal('status'), t.literal('slo.indicator.type'), + t.literal('slo.instanceId'), t.literal('_index'), ]); diff --git a/x-pack/packages/kbn-slo-schema/src/schema/common.ts b/x-pack/packages/kbn-slo-schema/src/schema/common.ts index 188a5b8f0726662..ff30ae5f254838d 100644 --- a/x-pack/packages/kbn-slo-schema/src/schema/common.ts +++ b/x-pack/packages/kbn-slo-schema/src/schema/common.ts @@ -81,6 +81,7 @@ const groupSummarySchema = t.type({ id: t.string, instanceId: t.string, name: t.string, + groupings: t.record(t.string, t.unknown), }), }), violated: t.number, diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/grouped_slos/group_list_view.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/grouped_slos/group_list_view.tsx index eefe5c1ba1e8782..cee082338d78c33 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/grouped_slos/group_list_view.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/grouped_slos/group_list_view.tsx @@ -27,12 +27,12 @@ import React, { memo, useState } from 'react'; import { paths } from '../../../../../common/locators/paths'; import { useFetchSloList } from '../../../../hooks/use_fetch_slo_list'; import { useKibana } from '../../../../utils/kibana_react'; -import { SLI_OPTIONS } from '../../../slo_edit/constants'; import { useSloFormattedSLIValue } from '../../hooks/use_slo_summary'; import type { SortDirection, SortField } from '../../hooks/use_url_search_state'; import { SlosView } from '../slos_view'; import { GroupByField } from '../slo_list_group_by'; import { SLOView } from '../toggle_slo_view'; +import { useGroupName } from './hooks/use_group_name'; interface Props { group: string; @@ -57,26 +57,7 @@ export function GroupListView({ }: Props) { const groupQuery = `"${groupBy}": "${group}"`; const query = kqlQuery ? `${groupQuery} and ${kqlQuery}` : groupQuery; - let groupName = group.toLowerCase(); - if (groupBy === 'slo.indicator.type') { - groupName = SLI_OPTIONS.find((option) => option.value === group)?.text ?? group; - } - if (groupBy === '_index') { - // get remote cluster name from index name - if (groupName.includes(':.')) { - const [remoteClusterName] = groupName.split(':.'); - groupName = i18n.translate('xpack.slo.group.remoteCluster', { - defaultMessage: 'Remote Cluster: {remoteClusterName}', - values: { - remoteClusterName, - }, - }); - } else { - groupName = i18n.translate('xpack.slo.group.remoteCluster.localKibana', { - defaultMessage: 'Local Kibana', - }); - } - } + const groupName = useGroupName(groupBy, group, summary); const [page, setPage] = useState(0); const [accordionState, setAccordionState] = useState<'open' | 'closed'>('closed'); diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/grouped_slos/group_view.test.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/grouped_slos/group_view.test.tsx index 3c61534d479b7be..775659f6be58053 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/grouped_slos/group_view.test.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/grouped_slos/group_view.test.tsx @@ -105,17 +105,65 @@ describe('Group View', () => { { group: 'production', groupBy: 'slo.tags', - summary: { total: 3, worst: 0.95, healthy: 2, violated: 1, degrading: 0, noData: 0 }, + summary: { + total: 3, + worst: { + sliValue: 1, + status: 'healthy', + slo: { + id: 'irrelevant', + instanceId: 'irrelevant', + name: 'irrelevant', + groupings: {}, + }, + }, + healthy: 2, + violated: 1, + degrading: 0, + noData: 0, + }, }, { group: 'something', groupBy: 'slo.tags', - summary: { total: 1, worst: 0.9, healthy: 0, violated: 1, degrading: 0, noData: 0 }, + summary: { + total: 1, + worst: { + sliValue: 1, + status: 'healthy', + slo: { + id: 'irrelevant', + instanceId: 'irrelevant', + name: 'irrelevant', + groupings: {}, + }, + }, + healthy: 0, + violated: 1, + degrading: 0, + noData: 0, + }, }, { group: 'anything', groupBy: 'slo.tags', - summary: { total: 2, worst: 0.85, healthy: 1, violated: 0, degrading: 0, noData: 1 }, + summary: { + total: 2, + worst: { + sliValue: 1, + status: 'healthy', + slo: { + id: 'irrelevant', + instanceId: 'irrelevant', + name: 'irrelevant', + groupings: {}, + }, + }, + healthy: 1, + violated: 0, + degrading: 0, + noData: 1, + }, }, ], }, @@ -163,7 +211,7 @@ describe('Group View', () => { results: [ { group: 'production', - groupBy: 'tags', + groupBy: 'slo.tags', summary: { total: 3, worst: 0.95, healthy: 2, violated: 1, degrading: 0, noData: 0 }, }, ], diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/grouped_slos/hooks/use_group_name.test.ts b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/grouped_slos/hooks/use_group_name.test.ts new file mode 100644 index 000000000000000..fa2b5192665ea76 --- /dev/null +++ b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/grouped_slos/hooks/use_group_name.test.ts @@ -0,0 +1,76 @@ +/* + * 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 { SLO_SUMMARY_DESTINATION_INDEX_PATTERN } from '../../../../../../common/constants'; +import { GroupSummary } from '@kbn/slo-schema'; +import { useGroupName } from './use_group_name'; + +describe('useGroupName', () => { + it('returns the group name for ungrouped', () => { + const groupName = useGroupName('ungrouped', 'irrelevant', {} as GroupSummary); + expect(groupName).toBe('irrelevant'); + }); + + it('returns the group name for slo tags', () => { + const groupName = useGroupName('slo.tags', 'some-tag', {} as GroupSummary); + expect(groupName).toBe('some-tag'); + }); + + it('returns the group name for status', () => { + const groupName = useGroupName('status', 'HEALTHY', {} as GroupSummary); + expect(groupName).toBe('healthy'); + }); + + it('returns the group name for slo instanceId', () => { + const summary = { + total: 2, + violated: 0, + healthy: 2, + degrading: 0, + noData: 0, + worst: { + sliValue: 1, + status: 'healthy', + slo: { + id: 'slo-id', + name: 'slo-name', + instanceId: 'domain.com', + groupings: { + host: { + name: 'domain.com', + }, + }, + }, + }, + } as GroupSummary; + const groupName = useGroupName('slo.instanceId', 'domain.com', summary); + expect(groupName).toBe('host.name: domain.com'); + }); + + it('returns the group name for slo indicator type', () => { + const groupName = useGroupName('slo.indicator.type', 'sli.kql.custom', {} as GroupSummary); + expect(groupName).toBe('Custom Query'); + }); + + it('returns the group name for local index', () => { + const groupName = useGroupName( + '_index', + SLO_SUMMARY_DESTINATION_INDEX_PATTERN, + {} as GroupSummary + ); + expect(groupName).toBe('Local Kibana'); + }); + + it('returns the group name for remote index', () => { + const groupName = useGroupName( + '_index', + `my-remote-cluster:${SLO_SUMMARY_DESTINATION_INDEX_PATTERN}`, + {} as GroupSummary + ); + expect(groupName).toBe('Remote Cluster: my-remote-cluster'); + }); +}); diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/grouped_slos/hooks/use_group_name.ts b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/grouped_slos/hooks/use_group_name.ts new file mode 100644 index 000000000000000..ca6ddff88fb19fa --- /dev/null +++ b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/grouped_slos/hooks/use_group_name.ts @@ -0,0 +1,67 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { ALL_VALUE, GroupSummary } from '@kbn/slo-schema'; +import { assertNever } from '@kbn/std'; +import { SLI_OPTIONS } from '../../../../slo_edit/constants'; +import { GroupByField } from '../../slo_list_group_by'; + +export function useGroupName(groupBy: GroupByField, group: string, summary?: GroupSummary) { + const groupName = group.toLowerCase(); + + switch (groupBy) { + case 'ungrouped': + case 'slo.tags': + case 'status': + return groupName; + case 'slo.instanceId': + if (groupName === ALL_VALUE || !summary) { + return i18n.translate('xpack.slo.group.ungroupedInstanceId', { + defaultMessage: 'Ungrouped', + }); + } + + const groupNames = flattenObject(summary.worst.slo.groupings); + return Object.entries(groupNames) + .map(([key, value]) => `${key}: ${value}`) + .join(', '); + case 'slo.indicator.type': + return SLI_OPTIONS.find((option) => option.value === group)?.text ?? groupName; + case '_index': + if (groupName.includes(':.')) { + const [remoteClusterName] = groupName.split(':.'); + return i18n.translate('xpack.slo.group.remoteCluster', { + defaultMessage: 'Remote Cluster: {remoteClusterName}', + values: { + remoteClusterName, + }, + }); + } + + return i18n.translate('xpack.slo.group.remoteCluster.localKibana', { + defaultMessage: 'Local Kibana', + }); + + default: + assertNever(groupBy); + } +} + +function flattenObject(obj: Record, parentKey = '', result: Record = {}) { + for (const key in obj) { + if (obj.hasOwnProperty(key)) { + const newKey = parentKey ? `${parentKey}.${key}` : key; + if (typeof obj[key] === 'object' && obj[key] !== null) { + flattenObject(obj[key], newKey, result); + } else { + result[newKey] = obj[key]; + } + } + } + return result; +} diff --git a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/slo_list_group_by.tsx b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/slo_list_group_by.tsx index b2764a4b9ab197d..d7175553fd17831 100644 --- a/x-pack/plugins/observability_solution/slo/public/pages/slos/components/slo_list_group_by.tsx +++ b/x-pack/plugins/observability_solution/slo/public/pages/slos/components/slo_list_group_by.tsx @@ -13,7 +13,13 @@ import type { SearchState } from '../hooks/use_url_search_state'; import type { Option } from './slo_context_menu'; import { ContextMenuItem, SLOContextMenu } from './slo_context_menu'; -export type GroupByField = 'ungrouped' | 'slo.tags' | 'status' | 'slo.indicator.type' | '_index'; +export type GroupByField = + | 'ungrouped' + | 'slo.tags' + | 'status' + | 'slo.indicator.type' + | 'slo.instanceId' + | '_index'; export interface Props { onStateChange: (newState: Partial) => void; state: SearchState; @@ -80,6 +86,16 @@ export function SloGroupBy({ onStateChange, state, loading }: Props) { handleChangeGroupBy('slo.indicator.type'); }, }, + { + label: i18n.translate('xpack.slo.list.groupBy.sloInstanceId', { + defaultMessage: 'SLO instance id', + }), + checked: groupBy === 'slo.instanceId', + value: 'slo.instanceId', + onClick: () => { + handleChangeGroupBy('slo.instanceId'); + }, + }, ]; if (hasRemoteEnabled) { diff --git a/x-pack/plugins/observability_solution/slo/server/services/find_slo_groups.ts b/x-pack/plugins/observability_solution/slo/server/services/find_slo_groups.ts index c3fadc6f60ba0ce..7282f276ea572aa 100644 --- a/x-pack/plugins/observability_solution/slo/server/services/find_slo_groups.ts +++ b/x-pack/plugins/observability_solution/slo/server/services/find_slo_groups.ts @@ -87,7 +87,14 @@ export class FindSLOGroups { }, }, _source: { - includes: ['sliValue', 'status', 'slo.id', 'slo.instanceId', 'slo.name'], + includes: [ + 'sliValue', + 'status', + 'slo.id', + 'slo.instanceId', + 'slo.name', + 'slo.groupings', + ], }, size: 1, }, @@ -165,6 +172,7 @@ export class FindSLOGroups { id: sourceSummaryDoc.slo.id, instanceId: sourceSummaryDoc.slo.instanceId, name: sourceSummaryDoc.slo.name, + groupings: sourceSummaryDoc.slo.groupings, }, }, violated: bucket.violated?.doc_count,