mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[SLOs] Filter out stale SLOs by default from overview (#182745)
## Summary Fixes https://github.com/elastic/kibana/issues/176712 Added a way to define threshold to declare SLOs stale state , for now i have chosen if it hasn't been updated in 48 hours, need product feedback on this <img width="1145" alt="image" src="2ce11a60
-2ed5-4929-a8ab-9d26d93f6956"> Added an overview panel to indicate how many SLOs are in stale state , user can click on each EuiState component to filter SLOs by <img width="1725" alt="image" src="352a893d
-083d-4448-91bd-6737cfbe63f1"> Also added Alerts overview , clicking will take user to alerts or rules page, pref filtered with burn rate <img width="505" alt="image" src="97387708
-54e5-4c0d-9a09-d740e2087e79">
This commit is contained in:
parent
47cdb1ae6e
commit
3e9a8086d7
30 changed files with 952 additions and 33 deletions
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
import * as t from 'io-ts';
|
||||
import { toBooleanRt } from '@kbn/io-ts-utils';
|
||||
import { sloWithDataResponseSchema } from '../slo';
|
||||
|
||||
const sortDirectionSchema = t.union([t.literal('asc'), t.literal('desc')]);
|
||||
|
@ -23,6 +24,7 @@ const findSLOParamsSchema = t.partial({
|
|||
perPage: t.string,
|
||||
sortBy: sortBySchema,
|
||||
sortDirection: sortDirectionSchema,
|
||||
hideStale: toBooleanRt,
|
||||
}),
|
||||
});
|
||||
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* 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 * as t from 'io-ts';
|
||||
|
||||
const getOverviewParamsSchema = t.partial({
|
||||
query: t.partial({
|
||||
kqlQuery: t.string,
|
||||
filters: t.string,
|
||||
}),
|
||||
});
|
||||
|
||||
const getOverviewResponseSchema = t.type({
|
||||
violated: t.number,
|
||||
degrading: t.number,
|
||||
stale: t.number,
|
||||
healthy: t.number,
|
||||
worst: t.type({
|
||||
value: t.number,
|
||||
id: t.string,
|
||||
}),
|
||||
noData: t.number,
|
||||
burnRateRules: t.number,
|
||||
burnRateActiveAlerts: t.number,
|
||||
burnRateRecoveredAlerts: t.number,
|
||||
});
|
||||
|
||||
type GetOverviewParams = t.TypeOf<typeof getOverviewParamsSchema.props.query>;
|
||||
type GetOverviewResponse = t.OutputOf<typeof getOverviewResponseSchema>;
|
||||
|
||||
export { getOverviewParamsSchema, getOverviewResponseSchema };
|
||||
export type { GetOverviewParams, GetOverviewResponse };
|
|
@ -43,11 +43,16 @@ const statusSchema = t.union([
|
|||
t.literal('VIOLATED'),
|
||||
]);
|
||||
|
||||
const summarySchema = t.type({
|
||||
status: statusSchema,
|
||||
sliValue: t.number,
|
||||
errorBudget: errorBudgetSchema,
|
||||
});
|
||||
const summarySchema = t.intersection([
|
||||
t.type({
|
||||
status: statusSchema,
|
||||
sliValue: t.number,
|
||||
errorBudget: errorBudgetSchema,
|
||||
}),
|
||||
t.partial({
|
||||
summaryUpdatedAt: t.union([t.string, t.null]),
|
||||
}),
|
||||
]);
|
||||
|
||||
const groupingsSchema = t.record(t.string, t.union([t.string, t.number]));
|
||||
|
||||
|
|
|
@ -10,6 +10,9 @@ import * as t from 'io-ts';
|
|||
export const sloSettingsSchema = t.type({
|
||||
useAllRemoteClusters: t.boolean,
|
||||
selectedRemoteClusters: t.array(t.string),
|
||||
staleThresholdInHours: t.number,
|
||||
});
|
||||
|
||||
export const sloServerlessSettingsSchema = t.type({});
|
||||
export const sloServerlessSettingsSchema = t.type({
|
||||
staleThresholdInHours: t.number,
|
||||
});
|
||||
|
|
|
@ -88,3 +88,6 @@ export const getSLOSummaryPipelineId = (sloId: string, sloRevision: number) =>
|
|||
|
||||
export const SYNTHETICS_INDEX_PATTERN = 'synthetics-*';
|
||||
export const SYNTHETICS_DEFAULT_GROUPINGS = ['monitor.name', 'observer.geo.name', 'monitor.id'];
|
||||
|
||||
// in hours
|
||||
export const DEFAULT_STALE_SLO_THRESHOLD_HOURS = 48;
|
||||
|
|
|
@ -6,13 +6,17 @@
|
|||
*/
|
||||
|
||||
import { getListOfSloSummaryIndices } from './summary_indices';
|
||||
import { SLO_SUMMARY_DESTINATION_INDEX_PATTERN } from './constants';
|
||||
import {
|
||||
DEFAULT_STALE_SLO_THRESHOLD_HOURS,
|
||||
SLO_SUMMARY_DESTINATION_INDEX_PATTERN,
|
||||
} from './constants';
|
||||
|
||||
describe('getListOfSloSummaryIndices', () => {
|
||||
it('should return default index if disabled', function () {
|
||||
const settings = {
|
||||
useAllRemoteClusters: false,
|
||||
selectedRemoteClusters: [],
|
||||
staleThresholdInHours: DEFAULT_STALE_SLO_THRESHOLD_HOURS,
|
||||
};
|
||||
const result = getListOfSloSummaryIndices(settings, []);
|
||||
expect(result).toBe(SLO_SUMMARY_DESTINATION_INDEX_PATTERN);
|
||||
|
@ -22,6 +26,7 @@ describe('getListOfSloSummaryIndices', () => {
|
|||
const settings = {
|
||||
useAllRemoteClusters: true,
|
||||
selectedRemoteClusters: [],
|
||||
staleThresholdInHours: DEFAULT_STALE_SLO_THRESHOLD_HOURS,
|
||||
};
|
||||
const clustersByName = [
|
||||
{ name: 'cluster1', isConnected: true },
|
||||
|
@ -37,6 +42,7 @@ describe('getListOfSloSummaryIndices', () => {
|
|||
const settings = {
|
||||
useAllRemoteClusters: false,
|
||||
selectedRemoteClusters: ['cluster1'],
|
||||
staleThresholdInHours: DEFAULT_STALE_SLO_THRESHOLD_HOURS,
|
||||
};
|
||||
const clustersByName = [
|
||||
{ name: 'cluster1', isConnected: true },
|
||||
|
|
|
@ -97,6 +97,11 @@ get:
|
|||
enum: [asc, desc]
|
||||
default: asc
|
||||
example: asc
|
||||
- name: hideStale
|
||||
in: query
|
||||
description: Hide stale SLOs from the list as defined by stale SLO threshold in SLO settings
|
||||
schema:
|
||||
type: boolean
|
||||
responses:
|
||||
'200':
|
||||
description: Successful request
|
||||
|
|
|
@ -27,12 +27,19 @@ interface SloGroupListFilter {
|
|||
groupsFilter?: string[];
|
||||
}
|
||||
|
||||
interface SLOOverviewFilter {
|
||||
kqlQuery: string;
|
||||
filters: string;
|
||||
lastRefresh?: number;
|
||||
}
|
||||
|
||||
export const sloKeys = {
|
||||
all: ['slo'] as const,
|
||||
lists: () => [...sloKeys.all, 'list'] as const,
|
||||
list: (filters: SloListFilter) => [...sloKeys.lists(), filters] as const,
|
||||
group: (filters: SloGroupListFilter) => [...sloKeys.groups(), filters] as const,
|
||||
groups: () => [...sloKeys.all, 'group'] as const,
|
||||
overview: (filters: SLOOverviewFilter) => ['overview', filters] as const,
|
||||
details: () => [...sloKeys.all, 'details'] as const,
|
||||
detail: (sloId?: string) => [...sloKeys.details(), sloId] as const,
|
||||
rules: () => [...sloKeys.all, 'rules'] as const,
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* 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 { observabilityPaths } from '@kbn/observability-plugin/common';
|
||||
import rison from '@kbn/rison';
|
||||
import { useKibana } from '../utils/kibana_react';
|
||||
|
||||
export const useAlertsUrl = () => {
|
||||
const { basePath } = useKibana().services.http;
|
||||
|
||||
const kuery = 'kibana.alert.rule.rule_type_id:("slo.rules.burnRate")';
|
||||
|
||||
return (status?: 'active' | 'recovered') =>
|
||||
`${basePath.prepend(observabilityPaths.alerts)}?_a=${rison.encode({
|
||||
kuery,
|
||||
rangeFrom: 'now-24h',
|
||||
rangeTo: 'now',
|
||||
status,
|
||||
})}`;
|
||||
};
|
|
@ -103,6 +103,7 @@ export function useFetchSloList({
|
|||
...(page !== undefined && { page }),
|
||||
...(perPage !== undefined && { perPage }),
|
||||
...(filters && { filters }),
|
||||
hideStale: true,
|
||||
},
|
||||
signal,
|
||||
});
|
||||
|
|
|
@ -17,22 +17,27 @@ import {
|
|||
EuiButtonEmpty,
|
||||
EuiButton,
|
||||
EuiSpacer,
|
||||
EuiFieldNumber,
|
||||
} from '@elastic/eui';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useFetcher } from '@kbn/observability-shared-plugin/public';
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import { isEqual } from 'lodash';
|
||||
import { DEFAULT_STALE_SLO_THRESHOLD_HOURS } from '../../../common/constants';
|
||||
import { useGetSettings } from './use_get_settings';
|
||||
import { usePutSloSettings } from './use_put_slo_settings';
|
||||
|
||||
export function SettingsForm() {
|
||||
const [useAllRemoteClusters, setUseAllRemoteClusters] = useState(false);
|
||||
const [selectedRemoteClusters, setSelectedRemoteClusters] = useState<string[]>([]);
|
||||
const [staleThresholdInHours, setStaleThresholdInHours] = useState(
|
||||
DEFAULT_STALE_SLO_THRESHOLD_HOURS
|
||||
);
|
||||
|
||||
const { http } = useKibana().services;
|
||||
|
||||
const { data: currentSettings } = useGetSettings();
|
||||
const { mutateAsync: updateSettings } = usePutSloSettings();
|
||||
const { mutateAsync: updateSettings, isLoading: isUpdating } = usePutSloSettings();
|
||||
|
||||
const { data, loading } = useFetcher(() => {
|
||||
return http?.get<Array<{ name: string }>>('/api/remote_clusters');
|
||||
|
@ -42,6 +47,7 @@ export function SettingsForm() {
|
|||
if (currentSettings) {
|
||||
setUseAllRemoteClusters(currentSettings.useAllRemoteClusters);
|
||||
setSelectedRemoteClusters(currentSettings.selectedRemoteClusters);
|
||||
setStaleThresholdInHours(currentSettings.staleThresholdInHours);
|
||||
}
|
||||
}, [currentSettings]);
|
||||
|
||||
|
@ -50,6 +56,7 @@ export function SettingsForm() {
|
|||
settings: {
|
||||
useAllRemoteClusters,
|
||||
selectedRemoteClusters,
|
||||
staleThresholdInHours,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -119,18 +126,57 @@ export function SettingsForm() {
|
|||
/>
|
||||
</EuiFormRow>
|
||||
<EuiSpacer size="m" />
|
||||
</EuiDescribedFormGroup>
|
||||
<EuiDescribedFormGroup
|
||||
title={
|
||||
<h3>
|
||||
{i18n.translate('xpack.slo.settingsForm.h3.staleThresholdLabel', {
|
||||
defaultMessage: 'Stale SLOs threshold',
|
||||
})}
|
||||
</h3>
|
||||
}
|
||||
description={
|
||||
<p>
|
||||
{i18n.translate('xpack.slo.settingsForm.select.staleThresholdLabel', {
|
||||
defaultMessage:
|
||||
'SLOs not updated within the defined stale threshold will be hidden by default from the overview list.',
|
||||
})}
|
||||
</p>
|
||||
}
|
||||
>
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.slo.settingsForm.euiFormRow.select.selectThresholdLabel', {
|
||||
defaultMessage: 'Select threshold',
|
||||
})}
|
||||
>
|
||||
<EuiFieldNumber
|
||||
data-test-subj="sloSettingsFormFieldNumber"
|
||||
value={staleThresholdInHours}
|
||||
onChange={(evt) => {
|
||||
setStaleThresholdInHours(Number(evt.target.value));
|
||||
}}
|
||||
append={i18n.translate('xpack.slo.settingsForm.euiFormRow.select.hours', {
|
||||
defaultMessage: 'Hours',
|
||||
})}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFlexGroup justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
isLoading={loading}
|
||||
isLoading={loading || isUpdating}
|
||||
data-test-subj="o11ySettingsFormCancelButton"
|
||||
onClick={() => {
|
||||
setUseAllRemoteClusters(currentSettings?.useAllRemoteClusters || false);
|
||||
setSelectedRemoteClusters(currentSettings?.selectedRemoteClusters || []);
|
||||
setStaleThresholdInHours(
|
||||
currentSettings?.staleThresholdInHours ?? DEFAULT_STALE_SLO_THRESHOLD_HOURS
|
||||
);
|
||||
}}
|
||||
isDisabled={isEqual(currentSettings, {
|
||||
useAllRemoteClusters,
|
||||
selectedRemoteClusters,
|
||||
staleThresholdInHours,
|
||||
})}
|
||||
>
|
||||
{i18n.translate('xpack.slo.settingsForm.euiButtonEmpty.cancelLabel', {
|
||||
|
@ -140,7 +186,7 @@ export function SettingsForm() {
|
|||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
isLoading={loading}
|
||||
isLoading={loading || isUpdating}
|
||||
data-test-subj="o11ySettingsFormSaveButton"
|
||||
color="primary"
|
||||
fill
|
||||
|
@ -148,6 +194,7 @@ export function SettingsForm() {
|
|||
isDisabled={isEqual(currentSettings, {
|
||||
useAllRemoteClusters,
|
||||
selectedRemoteClusters,
|
||||
staleThresholdInHours,
|
||||
})}
|
||||
>
|
||||
{i18n.translate('xpack.slo.settingsForm.applyButtonEmptyLabel', {
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import { GetSLOSettingsResponse } from '@kbn/slo-schema';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { DEFAULT_STALE_SLO_THRESHOLD_HOURS } from '../../../common/constants';
|
||||
import { useKibana } from '../../utils/kibana_react';
|
||||
|
||||
export const useGetSettings = () => {
|
||||
|
@ -30,4 +31,5 @@ export const useGetSettings = () => {
|
|||
const defaultSettings: GetSLOSettingsResponse = {
|
||||
useAllRemoteClusters: false,
|
||||
selectedRemoteClusters: [],
|
||||
staleThresholdInHours: DEFAULT_STALE_SLO_THRESHOLD_HOURS,
|
||||
};
|
||||
|
|
|
@ -23,6 +23,7 @@ import {
|
|||
} from '@kbn/presentation-util-plugin/public';
|
||||
import { ALL_VALUE, HistoricalSummaryResponse, SLOWithSummaryResponse } from '@kbn/slo-schema';
|
||||
import { Rule } from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import moment from 'moment';
|
||||
import React, { useState } from 'react';
|
||||
import { SloDeleteModal } from '../../../../components/slo/delete_confirmation_modal/slo_delete_confirmation_modal';
|
||||
import { SloResetConfirmationModal } from '../../../../components/slo/reset_confirmation_modal/slo_reset_confirmation_modal';
|
||||
|
@ -124,7 +125,17 @@ export function SloCardItem({ slo, rules, activeAlerts, historicalSummary, refet
|
|||
overflow: hidden;
|
||||
position: relative;
|
||||
`}
|
||||
title={slo.summary.status}
|
||||
title={
|
||||
slo.summary.summaryUpdatedAt
|
||||
? i18n.translate('xpack.slo.sloCardItem.euiPanel.lastUpdatedLabel', {
|
||||
defaultMessage: '{status}, Last updated: {value}',
|
||||
values: {
|
||||
status: slo.summary.status,
|
||||
value: moment(slo.summary.summaryUpdatedAt).fromNow(),
|
||||
},
|
||||
})
|
||||
: slo.summary.status
|
||||
}
|
||||
>
|
||||
<SloCardChart
|
||||
slo={slo}
|
||||
|
|
|
@ -6,10 +6,13 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiCallOut } from '@elastic/eui';
|
||||
import { EuiButtonEmpty, EuiCallOut } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useUrlSearchState } from '../hooks/use_url_search_state';
|
||||
|
||||
export function SloListEmpty() {
|
||||
const { onStateChange } = useUrlSearchState();
|
||||
|
||||
return (
|
||||
<EuiCallOut
|
||||
title={i18n.translate('xpack.slo.list.emptyTitle', {
|
||||
|
@ -21,6 +24,23 @@ export function SloListEmpty() {
|
|||
{i18n.translate('xpack.slo.list.emptyMessage', {
|
||||
defaultMessage: 'There are no results for your criteria.',
|
||||
})}
|
||||
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="sloSloListEmptyLinkButtonButton"
|
||||
onClick={() => {
|
||||
onStateChange({
|
||||
kqlQuery: '',
|
||||
filters: [],
|
||||
tagsFilter: undefined,
|
||||
statusFilter: undefined,
|
||||
});
|
||||
}}
|
||||
color="primary"
|
||||
>
|
||||
{i18n.translate('xpack.slo.sloListEmpty.clearFiltersButtonLabel', {
|
||||
defaultMessage: 'Clear filters',
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
</EuiCallOut>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* 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 { EuiFlexItem, EuiStat, EuiToolTip } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { useUrlSearchState } from '../../hooks/use_url_search_state';
|
||||
|
||||
export function OverViewItem({
|
||||
title,
|
||||
description,
|
||||
titleColor,
|
||||
isLoading,
|
||||
query,
|
||||
tooltip,
|
||||
onClick,
|
||||
}: {
|
||||
title?: string | number;
|
||||
description: string;
|
||||
titleColor: string;
|
||||
isLoading: boolean;
|
||||
query?: string;
|
||||
tooltip?: string;
|
||||
onClick?: () => void;
|
||||
}) {
|
||||
const { onStateChange } = useUrlSearchState();
|
||||
|
||||
return (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiToolTip content={tooltip}>
|
||||
<EuiStat
|
||||
title={title}
|
||||
description={description}
|
||||
titleColor={titleColor}
|
||||
reverse={true}
|
||||
isLoading={isLoading}
|
||||
onClick={() => {
|
||||
if (onClick) {
|
||||
onClick();
|
||||
return;
|
||||
}
|
||||
onStateChange({
|
||||
kqlQuery: query,
|
||||
});
|
||||
}}
|
||||
css={{
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,96 @@
|
|||
/*
|
||||
* 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 { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText, EuiTitle, EuiPanel } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { GetOverviewResponse } from '@kbn/slo-schema/src/rest_specs/routes/get_overview';
|
||||
import { rulesLocatorID, RulesParams } from '@kbn/observability-plugin/public';
|
||||
import { useAlertsUrl } from '../../../../hooks/use_alerts_url';
|
||||
import { useKibana } from '../../../../utils/kibana_react';
|
||||
import { OverViewItem } from './overview_item';
|
||||
|
||||
export function SLOOverviewAlerts({
|
||||
data,
|
||||
isLoading,
|
||||
}: {
|
||||
data?: GetOverviewResponse;
|
||||
isLoading: boolean;
|
||||
}) {
|
||||
const {
|
||||
application,
|
||||
share: {
|
||||
url: { locators },
|
||||
},
|
||||
} = useKibana().services;
|
||||
|
||||
const locator = locators.get<RulesParams>(rulesLocatorID);
|
||||
|
||||
const getAlertsUrl = useAlertsUrl();
|
||||
|
||||
return (
|
||||
<EuiPanel hasShadow={false} hasBorder={true}>
|
||||
<EuiFlexGroup alignItems="center" gutterSize="m">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle size="xs">
|
||||
<h3>
|
||||
{i18n.translate('xpack.slo.sLOsOverview.h3.burnRateLabel', {
|
||||
defaultMessage: 'Burn rate',
|
||||
})}
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiText color="subdued" size="s">
|
||||
{i18n.translate('xpack.slo.sLOsOverview.lastTextLabel', {
|
||||
defaultMessage: 'Last 24h',
|
||||
})}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiSpacer size="xs" />
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<OverViewItem
|
||||
title={data?.burnRateActiveAlerts}
|
||||
description={i18n.translate('xpack.slo.sLOsOverview.euiStat.burnRateActiveAlerts', {
|
||||
defaultMessage: 'Active alerts',
|
||||
})}
|
||||
titleColor="danger"
|
||||
isLoading={isLoading}
|
||||
onClick={() => {
|
||||
application.navigateToUrl(getAlertsUrl('active'));
|
||||
}}
|
||||
/>
|
||||
<OverViewItem
|
||||
title={data?.burnRateRecoveredAlerts}
|
||||
description={i18n.translate('xpack.slo.sLOsOverview.euiStat.burnRateRecoveredAlerts', {
|
||||
defaultMessage: 'Recovered alerts',
|
||||
})}
|
||||
titleColor="success"
|
||||
isLoading={isLoading}
|
||||
onClick={() => {
|
||||
application.navigateToUrl(getAlertsUrl('recovered'));
|
||||
}}
|
||||
/>
|
||||
<OverViewItem
|
||||
title={data?.burnRateRules}
|
||||
description={i18n.translate('xpack.slo.sLOsOverview.euiStat.burnRateRules', {
|
||||
defaultMessage: 'Rules',
|
||||
})}
|
||||
titleColor="default"
|
||||
isLoading={isLoading}
|
||||
onClick={() => {
|
||||
locator?.navigate({
|
||||
type: ['slo.rules.burnRate'],
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,127 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiPanel,
|
||||
EuiSpacer,
|
||||
EuiTitle,
|
||||
useEuiTheme,
|
||||
} from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { DEFAULT_STALE_SLO_THRESHOLD_HOURS } from '../../../../../common/constants';
|
||||
import { SLOOverviewAlerts } from './slo_overview_alerts';
|
||||
import { useGetSettings } from '../../../slo_settings/use_get_settings';
|
||||
import { useFetchSLOsOverview } from '../../hooks/use_fetch_slos_overview';
|
||||
import { useUrlSearchState } from '../../hooks/use_url_search_state';
|
||||
import { OverViewItem } from './overview_item';
|
||||
|
||||
export function SLOsOverview() {
|
||||
const { state } = useUrlSearchState();
|
||||
const { kqlQuery, filters, tagsFilter, statusFilter } = state;
|
||||
|
||||
const { data, isLoading } = useFetchSLOsOverview({
|
||||
kqlQuery,
|
||||
filters,
|
||||
tagsFilter,
|
||||
statusFilter,
|
||||
});
|
||||
|
||||
const theme = useEuiTheme().euiTheme;
|
||||
const { data: currentSettings } = useGetSettings();
|
||||
|
||||
return (
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={2}>
|
||||
<EuiPanel hasShadow={false} hasBorder={true}>
|
||||
<EuiTitle size="xs">
|
||||
<h3>
|
||||
{i18n.translate('xpack.slo.sLOsOverview.h3.overviewLabel', {
|
||||
defaultMessage: 'Overview',
|
||||
})}
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="xs" />
|
||||
<EuiFlexGroup gutterSize="xl" justifyContent="spaceBetween">
|
||||
<OverViewItem
|
||||
title={data?.healthy}
|
||||
description={i18n.translate('xpack.slo.sLOsOverview.euiStat.healthyLabel', {
|
||||
defaultMessage: 'Healthy',
|
||||
})}
|
||||
titleColor="success"
|
||||
isLoading={isLoading}
|
||||
query={`status : HEALTHY`}
|
||||
tooltip={i18n.translate('xpack.slo.sLOsOverview.euiStat.healthyLabel.tooltip', {
|
||||
defaultMessage: 'Click to filter SLOs by Healthy status.',
|
||||
})}
|
||||
/>
|
||||
<OverViewItem
|
||||
title={data?.violated}
|
||||
description={i18n.translate('xpack.slo.sLOsOverview.euiStat.violatedLabel', {
|
||||
defaultMessage: 'Violated',
|
||||
})}
|
||||
titleColor="danger"
|
||||
query={`status : VIOLATED`}
|
||||
isLoading={isLoading}
|
||||
tooltip={i18n.translate('xpack.slo.sLOsOverview.euiStat.violatedLabel.tooltip', {
|
||||
defaultMessage: 'Click to filter SLOs by Violated status.',
|
||||
})}
|
||||
/>
|
||||
<OverViewItem
|
||||
title={data?.noData}
|
||||
description={i18n.translate('xpack.slo.sLOsOverview.euiStat.noDataLabel', {
|
||||
defaultMessage: 'No data',
|
||||
})}
|
||||
titleColor="subdued"
|
||||
query={`status : NO_DATA`}
|
||||
isLoading={isLoading}
|
||||
tooltip={i18n.translate('xpack.slo.sLOsOverview.euiStat.noDataLabel.tooltip', {
|
||||
defaultMessage: 'Click to filter SLOs by no data status.',
|
||||
})}
|
||||
/>
|
||||
<OverViewItem
|
||||
title={data?.degrading}
|
||||
description={i18n.translate('xpack.slo.sLOsOverview.euiStat.degradingLabel', {
|
||||
defaultMessage: 'Degrading',
|
||||
})}
|
||||
query={`status : DEGRADING`}
|
||||
isLoading={isLoading}
|
||||
tooltip={i18n.translate('xpack.slo.sLOsOverview.euiStat.degradingLabel.tooltip', {
|
||||
defaultMessage: 'Click to filter SLOs by Degrading status.',
|
||||
})}
|
||||
titleColor={theme.colors.warningText}
|
||||
/>
|
||||
<OverViewItem
|
||||
title={data?.stale}
|
||||
description={i18n.translate('xpack.slo.sLOsOverview.euiStat.staleLabel', {
|
||||
defaultMessage: 'Stale',
|
||||
})}
|
||||
titleColor="subdued"
|
||||
isLoading={isLoading}
|
||||
query={`summaryUpdatedAt < "now-${
|
||||
currentSettings?.staleThresholdInHours ?? DEFAULT_STALE_SLO_THRESHOLD_HOURS
|
||||
}h"`}
|
||||
tooltip={i18n.translate('xpack.slo.sLOsOverview.euiStat.staleLabel.tooltip', {
|
||||
defaultMessage:
|
||||
'Click to filter SLOs which have not been updated in last {value} hour. They are filtered out by default from the list. Threshold can be changed in SLO settings.',
|
||||
values: {
|
||||
value:
|
||||
currentSettings?.staleThresholdInHours ?? DEFAULT_STALE_SLO_THRESHOLD_HOURS,
|
||||
},
|
||||
})}
|
||||
/>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={1}>
|
||||
<SLOOverviewAlerts data={data} isLoading={isLoading} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,108 @@
|
|||
/*
|
||||
* 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 { useQuery } from '@tanstack/react-query';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { buildQueryFromFilters, Filter } from '@kbn/es-query';
|
||||
import { useMemo } from 'react';
|
||||
import { GetOverviewResponse } from '@kbn/slo-schema/src/rest_specs/routes/get_overview';
|
||||
import { sloKeys } from '../../../hooks/query_key_factory';
|
||||
import { SLO_SUMMARY_DESTINATION_INDEX_PATTERN } from '../../../../common/constants';
|
||||
import { useCreateDataView } from '../../../hooks/use_create_data_view';
|
||||
import { useKibana } from '../../../utils/kibana_react';
|
||||
import { SearchState } from './use_url_search_state';
|
||||
|
||||
interface SLOsOverviewParams {
|
||||
kqlQuery?: string;
|
||||
tagsFilter?: SearchState['tagsFilter'];
|
||||
statusFilter?: SearchState['statusFilter'];
|
||||
filters?: Filter[];
|
||||
lastRefresh?: number;
|
||||
}
|
||||
|
||||
interface UseFetchSloGroupsResponse {
|
||||
isLoading: boolean;
|
||||
isRefetching: boolean;
|
||||
isSuccess: boolean;
|
||||
isError: boolean;
|
||||
data: GetOverviewResponse | undefined;
|
||||
}
|
||||
|
||||
export function useFetchSLOsOverview({
|
||||
kqlQuery = '',
|
||||
tagsFilter,
|
||||
statusFilter,
|
||||
filters: filterDSL = [],
|
||||
lastRefresh,
|
||||
}: SLOsOverviewParams = {}): UseFetchSloGroupsResponse {
|
||||
const {
|
||||
http,
|
||||
notifications: { toasts },
|
||||
} = useKibana().services;
|
||||
|
||||
const { dataView } = useCreateDataView({
|
||||
indexPatternString: SLO_SUMMARY_DESTINATION_INDEX_PATTERN,
|
||||
});
|
||||
|
||||
const filters = useMemo(() => {
|
||||
try {
|
||||
return JSON.stringify(
|
||||
buildQueryFromFilters(
|
||||
[
|
||||
...filterDSL,
|
||||
...(tagsFilter ? [tagsFilter] : []),
|
||||
...(statusFilter ? [statusFilter] : []),
|
||||
],
|
||||
dataView,
|
||||
{
|
||||
ignoreFilterIfFieldNotInIndex: true,
|
||||
}
|
||||
)
|
||||
);
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
}, [filterDSL, tagsFilter, statusFilter, dataView]);
|
||||
const { data, isLoading, isSuccess, isError, isRefetching } = useQuery({
|
||||
queryKey: sloKeys.overview({
|
||||
kqlQuery,
|
||||
filters,
|
||||
lastRefresh,
|
||||
}),
|
||||
queryFn: async ({ signal }) => {
|
||||
return await http.get<GetOverviewResponse>('/internal/observability/slos/overview', {
|
||||
query: {
|
||||
...(kqlQuery && { kqlQuery }),
|
||||
...(filters && { filters }),
|
||||
},
|
||||
signal,
|
||||
});
|
||||
},
|
||||
cacheTime: 0,
|
||||
refetchOnWindowFocus: false,
|
||||
retry: (failureCount, error) => {
|
||||
if (String(error) === 'Error: Forbidden') {
|
||||
return false;
|
||||
}
|
||||
return failureCount < 4;
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toasts.addError(error, {
|
||||
title: i18n.translate('xpack.slo.overview.list.errorNotification', {
|
||||
defaultMessage: 'Something went wrong while fetching SLOs overview',
|
||||
}),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
data,
|
||||
isLoading,
|
||||
isSuccess,
|
||||
isError,
|
||||
isRefetching,
|
||||
};
|
||||
}
|
|
@ -6,8 +6,10 @@
|
|||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useBreadcrumbs } from '@kbn/observability-shared-plugin/public';
|
||||
import React, { useEffect } from 'react';
|
||||
import { EuiSpacer } from '@elastic/eui';
|
||||
import { useBreadcrumbs } from '@kbn/observability-shared-plugin/public';
|
||||
import { SLOsOverview } from './components/slos_overview/slos_overview';
|
||||
import { paths } from '../../../common/locators/paths';
|
||||
import { HeaderMenu } from '../../components/header_menu/header_menu';
|
||||
import { SloOutdatedCallout } from '../../components/slo/slo_outdated_callout';
|
||||
|
@ -66,6 +68,8 @@ export function SlosPage() {
|
|||
>
|
||||
<HeaderMenu />
|
||||
<SloOutdatedCallout />
|
||||
<SLOsOverview />
|
||||
<EuiSpacer size="m" />
|
||||
<SloList />
|
||||
</ObservabilityPageTemplate>
|
||||
);
|
||||
|
|
|
@ -17,7 +17,10 @@ import {
|
|||
} from '@kbn/core/server';
|
||||
import { PluginSetupContract, PluginStartContract } from '@kbn/alerting-plugin/server';
|
||||
import { PluginSetupContract as FeaturesSetup } from '@kbn/features-plugin/server';
|
||||
import { RuleRegistryPluginSetupContract } from '@kbn/rule-registry-plugin/server';
|
||||
import {
|
||||
RuleRegistryPluginSetupContract,
|
||||
RuleRegistryPluginStartContract,
|
||||
} from '@kbn/rule-registry-plugin/server';
|
||||
import {
|
||||
TaskManagerSetupContract,
|
||||
TaskManagerStartContract,
|
||||
|
@ -56,6 +59,7 @@ export interface PluginStart {
|
|||
alerting: PluginStartContract;
|
||||
taskManager: TaskManagerStartContract;
|
||||
spaces?: SpacesPluginStart;
|
||||
ruleRegistry: RuleRegistryPluginStartContract;
|
||||
}
|
||||
|
||||
const sloRuleTypes = [SLO_BURN_RATE_RULE_TYPE_ID];
|
||||
|
@ -153,6 +157,10 @@ export class SloPlugin implements Plugin<SloPluginSetup> {
|
|||
const [, pluginStart] = await core.getStartServices();
|
||||
return pluginStart.alerting.getRulesClientWithRequest(request);
|
||||
},
|
||||
getRacClientWithRequest: async (request) => {
|
||||
const [, pluginStart] = await core.getStartServices();
|
||||
return pluginStart.ruleRegistry.getRacClientWithRequest(request);
|
||||
},
|
||||
},
|
||||
logger: this.logger,
|
||||
repository: getSloServerRouteRepository({
|
||||
|
|
|
@ -8,7 +8,11 @@ import { errors } from '@elastic/elasticsearch';
|
|||
import Boom from '@hapi/boom';
|
||||
import { RulesClientApi } from '@kbn/alerting-plugin/server/types';
|
||||
import { CoreSetup, KibanaRequest, Logger, RouteRegistrar } from '@kbn/core/server';
|
||||
import { RuleDataPluginService } from '@kbn/rule-registry-plugin/server';
|
||||
import {
|
||||
AlertsClient,
|
||||
RuleDataPluginService,
|
||||
RuleRegistryPluginSetupContract,
|
||||
} from '@kbn/rule-registry-plugin/server';
|
||||
import {
|
||||
decodeRequestParams,
|
||||
parseEndpoint,
|
||||
|
@ -33,10 +37,12 @@ interface RegisterRoutes {
|
|||
export interface RegisterRoutesDependencies {
|
||||
pluginsSetup: {
|
||||
core: CoreSetup;
|
||||
ruleRegistry: RuleRegistryPluginSetupContract;
|
||||
};
|
||||
getSpacesStart: () => Promise<SpacesPluginStart | undefined>;
|
||||
ruleDataService: RuleDataPluginService;
|
||||
getRulesClientWithRequest: (request: KibanaRequest) => Promise<RulesClientApi>;
|
||||
getRacClientWithRequest: (request: KibanaRequest) => Promise<AlertsClient>;
|
||||
}
|
||||
|
||||
export function registerRoutes({ config, repository, core, logger, dependencies }: RegisterRoutes) {
|
||||
|
|
|
@ -27,6 +27,8 @@ import {
|
|||
resetSLOParamsSchema,
|
||||
updateSLOParamsSchema,
|
||||
} from '@kbn/slo-schema';
|
||||
import { getOverviewParamsSchema } from '@kbn/slo-schema/src/rest_specs/routes/get_overview';
|
||||
import { GetSLOsOverview } from '../../services/get_slos_overview';
|
||||
import type { IndicatorTypes } from '../../domain/models';
|
||||
import {
|
||||
CreateSLO,
|
||||
|
@ -663,6 +665,37 @@ const putSloSettings = (isServerless?: boolean) =>
|
|||
},
|
||||
});
|
||||
|
||||
const getSLOsOverview = createSloServerRoute({
|
||||
endpoint: 'GET /internal/observability/slos/overview',
|
||||
options: {
|
||||
tags: ['access:slo_read'],
|
||||
access: 'internal',
|
||||
},
|
||||
params: getOverviewParamsSchema,
|
||||
handler: async ({ context, params, request, logger, dependencies }) => {
|
||||
await assertPlatinumLicense(context);
|
||||
|
||||
const soClient = (await context.core).savedObjects.client;
|
||||
const esClient = (await context.core).elasticsearch.client.asCurrentUser;
|
||||
|
||||
const racClient = await dependencies.getRacClientWithRequest(request);
|
||||
|
||||
const spaces = await dependencies.getSpacesStart();
|
||||
const spaceId = (await spaces?.spacesService?.getActiveSpace(request))?.id ?? 'default';
|
||||
const rulesClient = await dependencies.getRulesClientWithRequest(request);
|
||||
|
||||
const slosOverview = new GetSLOsOverview(
|
||||
soClient,
|
||||
esClient,
|
||||
spaceId,
|
||||
logger,
|
||||
rulesClient,
|
||||
racClient
|
||||
);
|
||||
return await slosOverview.execute(params?.query ?? {});
|
||||
},
|
||||
});
|
||||
|
||||
export const getSloRouteRepository = (isServerless?: boolean) => {
|
||||
return {
|
||||
...fetchSloHealthRoute,
|
||||
|
@ -686,5 +719,6 @@ export const getSloRouteRepository = (isServerless?: boolean) => {
|
|||
...resetSLORoute,
|
||||
...findSLOGroupsRoute,
|
||||
...getSLOSuggestionsRoute,
|
||||
...getSLOsOverview,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
|
||||
exports[`Summary Search Client returns the summary documents without duplicate temporary summary documents 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
|
@ -48,6 +49,7 @@ Object {
|
|||
},
|
||||
"sliValue": -1,
|
||||
"status": "NO_DATA",
|
||||
"summaryUpdatedAt": null,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
|
@ -63,6 +65,7 @@ Object {
|
|||
},
|
||||
"sliValue": -1,
|
||||
"status": "NO_DATA",
|
||||
"summaryUpdatedAt": null,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
|
@ -78,6 +81,7 @@ Object {
|
|||
},
|
||||
"sliValue": -1,
|
||||
"status": "NO_DATA",
|
||||
"summaryUpdatedAt": null,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
|
@ -93,6 +97,7 @@ Object {
|
|||
},
|
||||
"sliValue": -1,
|
||||
"status": "NO_DATA",
|
||||
"summaryUpdatedAt": null,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
|
@ -108,6 +113,7 @@ Object {
|
|||
},
|
||||
"sliValue": -1,
|
||||
"status": "NO_DATA",
|
||||
"summaryUpdatedAt": null,
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
|
@ -45,6 +45,7 @@ describe('FindSLO', () => {
|
|||
"page": 1,
|
||||
"perPage": 25,
|
||||
},
|
||||
undefined,
|
||||
]
|
||||
`);
|
||||
|
||||
|
@ -139,6 +140,7 @@ describe('FindSLO', () => {
|
|||
"page": 2,
|
||||
"perPage": 10,
|
||||
},
|
||||
undefined,
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
|
|
@ -27,7 +27,8 @@ export class FindSLO {
|
|||
params.kqlQuery ?? '',
|
||||
params.filters ?? '',
|
||||
toSort(params),
|
||||
toPagination(params)
|
||||
toPagination(params),
|
||||
params.hideStale
|
||||
);
|
||||
|
||||
const localSloDefinitions = await this.repository.findAllByIds(
|
||||
|
|
|
@ -12,10 +12,10 @@ import {
|
|||
Pagination,
|
||||
sloGroupWithSummaryResponseSchema,
|
||||
} from '@kbn/slo-schema';
|
||||
import { getListOfSummaryIndices, getSloSettings } from './slo_settings';
|
||||
import { DEFAULT_SLO_GROUPS_PAGE_SIZE } from '../../common/constants';
|
||||
import { IllegalArgumentError } from '../errors';
|
||||
import { typedSearch } from '../utils/queries';
|
||||
import { getListOfSummaryIndices } from './slo_settings';
|
||||
import { EsSummaryDocument } from './summary_transform_generator/helpers/create_temp_summary';
|
||||
import { getElasticsearchQueryOrThrow } from './transform_generators';
|
||||
|
||||
|
@ -56,8 +56,8 @@ export class FindSLOGroups {
|
|||
} catch (e) {
|
||||
this.logger.error(`Failed to parse filters: ${e.message}`);
|
||||
}
|
||||
|
||||
const indices = await getListOfSummaryIndices(this.soClient, this.esClient);
|
||||
const settings = await getSloSettings(this.soClient);
|
||||
const { indices } = await getListOfSummaryIndices(this.esClient, settings);
|
||||
|
||||
const hasSelectedTags = groupBy === 'slo.tags' && groupsFilter.length > 0;
|
||||
|
||||
|
|
|
@ -0,0 +1,153 @@
|
|||
/*
|
||||
* 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 { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server';
|
||||
import { ElasticsearchClient } from '@kbn/core-elasticsearch-server';
|
||||
import { Logger } from '@kbn/logging';
|
||||
import {
|
||||
GetOverviewParams,
|
||||
GetOverviewResponse,
|
||||
} from '@kbn/slo-schema/src/rest_specs/routes/get_overview';
|
||||
import { RulesClientApi } from '@kbn/alerting-plugin/server/types';
|
||||
import { AlertsClient } from '@kbn/rule-registry-plugin/server';
|
||||
import moment from 'moment';
|
||||
import { observabilityAlertFeatureIds } from '@kbn/observability-plugin/common';
|
||||
import { typedSearch } from '../utils/queries';
|
||||
import { getElasticsearchQueryOrThrow } from './transform_generators';
|
||||
import { getListOfSummaryIndices, getSloSettings } from './slo_settings';
|
||||
|
||||
export class GetSLOsOverview {
|
||||
constructor(
|
||||
private soClient: SavedObjectsClientContract,
|
||||
private esClient: ElasticsearchClient,
|
||||
private spaceId: string,
|
||||
private logger: Logger,
|
||||
private rulesClient: RulesClientApi,
|
||||
private racClient: AlertsClient
|
||||
) {}
|
||||
|
||||
public async execute(params: GetOverviewParams = {}): Promise<GetOverviewResponse> {
|
||||
const settings = await getSloSettings(this.soClient);
|
||||
const { indices } = await getListOfSummaryIndices(this.esClient, settings);
|
||||
|
||||
const kqlQuery = params.kqlQuery ?? '';
|
||||
const filters = params.filters ?? '';
|
||||
let parsedFilters: any = {};
|
||||
try {
|
||||
parsedFilters = JSON.parse(filters);
|
||||
} catch (e) {
|
||||
this.logger.error(`Failed to parse filters: ${e.message}`);
|
||||
}
|
||||
|
||||
const response = await typedSearch(this.esClient, {
|
||||
index: indices,
|
||||
size: 0,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{ term: { spaceId: this.spaceId } },
|
||||
getElasticsearchQueryOrThrow(kqlQuery),
|
||||
...(parsedFilters.filter ?? []),
|
||||
],
|
||||
must_not: [...(parsedFilters.must_not ?? [])],
|
||||
},
|
||||
},
|
||||
body: {
|
||||
aggs: {
|
||||
worst: {
|
||||
top_hits: {
|
||||
sort: {
|
||||
errorBudgetRemaining: {
|
||||
order: 'asc',
|
||||
},
|
||||
},
|
||||
_source: {
|
||||
includes: ['sliValue', 'status', 'slo.id', 'slo.instanceId', 'slo.name'],
|
||||
},
|
||||
size: 1,
|
||||
},
|
||||
},
|
||||
stale: {
|
||||
filter: {
|
||||
range: {
|
||||
summaryUpdatedAt: {
|
||||
lt: `now-${settings.staleThresholdInHours}h`,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
violated: {
|
||||
filter: {
|
||||
term: {
|
||||
status: 'VIOLATED',
|
||||
},
|
||||
},
|
||||
},
|
||||
healthy: {
|
||||
filter: {
|
||||
term: {
|
||||
status: 'HEALTHY',
|
||||
},
|
||||
},
|
||||
},
|
||||
degrading: {
|
||||
filter: {
|
||||
term: {
|
||||
status: 'DEGRADING',
|
||||
},
|
||||
},
|
||||
},
|
||||
noData: {
|
||||
filter: {
|
||||
term: {
|
||||
status: 'NO_DATA',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const [rules, alerts] = await Promise.all([
|
||||
this.rulesClient.find({
|
||||
options: {
|
||||
search: 'alert.attributes.alertTypeId:("slo.rules.burnRate")',
|
||||
},
|
||||
}),
|
||||
|
||||
this.racClient.getAlertSummary({
|
||||
featureIds: observabilityAlertFeatureIds,
|
||||
gte: moment().subtract(24, 'hours').toISOString(),
|
||||
lte: moment().toISOString(),
|
||||
filter: [
|
||||
{
|
||||
term: {
|
||||
'kibana.alert.rule.rule_type_id': 'slo.rules.burnRate',
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
]);
|
||||
|
||||
const aggs = response.aggregations;
|
||||
|
||||
return {
|
||||
violated: aggs?.violated.doc_count ?? 0,
|
||||
degrading: aggs?.degrading.doc_count ?? 0,
|
||||
healthy: aggs?.healthy.doc_count ?? 0,
|
||||
noData: aggs?.noData.doc_count ?? 0,
|
||||
stale: aggs?.stale.doc_count ?? 0,
|
||||
worst: {
|
||||
value: 0,
|
||||
id: 'id',
|
||||
},
|
||||
burnRateRules: rules.total,
|
||||
burnRateActiveAlerts: alerts.activeAlertCount,
|
||||
burnRateRecoveredAlerts: alerts.recoveredAlertCount,
|
||||
};
|
||||
}
|
||||
}
|
|
@ -9,7 +9,10 @@ import { ElasticsearchClient } from '@kbn/core-elasticsearch-server';
|
|||
import { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server';
|
||||
import { SavedObjectsErrorHelpers } from '@kbn/core-saved-objects-server';
|
||||
import { PutSLOSettingsParams, sloSettingsSchema } from '@kbn/slo-schema';
|
||||
import { SLO_SUMMARY_DESTINATION_INDEX_PATTERN } from '../../common/constants';
|
||||
import {
|
||||
DEFAULT_STALE_SLO_THRESHOLD_HOURS,
|
||||
SLO_SUMMARY_DESTINATION_INDEX_PATTERN,
|
||||
} from '../../common/constants';
|
||||
import { getListOfSloSummaryIndices } from '../../common/summary_indices';
|
||||
import { StoredSLOSettings } from '../domain/models';
|
||||
import { sloSettingsObjectId, SO_SLO_SETTINGS_TYPE } from '../saved_objects/slo_settings';
|
||||
|
@ -20,12 +23,15 @@ export const getSloSettings = async (soClient: SavedObjectsClientContract) => {
|
|||
SO_SLO_SETTINGS_TYPE,
|
||||
sloSettingsObjectId(soClient.getCurrentNamespace())
|
||||
);
|
||||
// set if it's not there
|
||||
soObject.attributes.staleThresholdInHours = soObject.attributes.staleThresholdInHours ?? 2;
|
||||
return sloSettingsSchema.encode(soObject.attributes);
|
||||
} catch (e) {
|
||||
if (SavedObjectsErrorHelpers.isNotFoundError(e)) {
|
||||
return {
|
||||
useAllRemoteClusters: false,
|
||||
selectedRemoteClusters: [],
|
||||
staleThresholdInHours: DEFAULT_STALE_SLO_THRESHOLD_HOURS,
|
||||
};
|
||||
}
|
||||
throw e;
|
||||
|
@ -49,15 +55,12 @@ export const storeSloSettings = async (
|
|||
};
|
||||
|
||||
export const getListOfSummaryIndices = async (
|
||||
soClient: SavedObjectsClientContract,
|
||||
esClient: ElasticsearchClient
|
||||
esClient: ElasticsearchClient,
|
||||
settings: StoredSLOSettings
|
||||
) => {
|
||||
const indices: string[] = [SLO_SUMMARY_DESTINATION_INDEX_PATTERN];
|
||||
|
||||
const settings = await getSloSettings(soClient);
|
||||
const { useAllRemoteClusters, selectedRemoteClusters } = settings;
|
||||
if (!useAllRemoteClusters && selectedRemoteClusters.length === 0) {
|
||||
return indices;
|
||||
return { indices: [SLO_SUMMARY_DESTINATION_INDEX_PATTERN], settings };
|
||||
}
|
||||
|
||||
const clustersByName = await esClient.cluster.remoteInfo();
|
||||
|
@ -67,5 +70,5 @@ export const getListOfSummaryIndices = async (
|
|||
isConnected: clustersByName[clusterName].connected,
|
||||
}));
|
||||
|
||||
return getListOfSloSummaryIndices(settings, clusterInfo);
|
||||
return { indices: getListOfSloSummaryIndices(settings, clusterInfo) };
|
||||
};
|
||||
|
|
|
@ -121,4 +121,117 @@ describe('Summary Search Client', () => {
|
|||
expect(results).toMatchSnapshot();
|
||||
expect(results.total).toBe(5);
|
||||
});
|
||||
|
||||
it('handles hideStale filter', async () => {
|
||||
await service.search('', '', defaultSort, defaultPagination, true);
|
||||
expect(esClientMock.search.mock.calls[0]).toEqual([
|
||||
{
|
||||
from: 0,
|
||||
index: ['.slo-observability.summary-v3*'],
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
term: {
|
||||
spaceId: 'default',
|
||||
},
|
||||
},
|
||||
{
|
||||
bool: {
|
||||
should: [
|
||||
{
|
||||
term: {
|
||||
isTempDoc: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
range: {
|
||||
summaryUpdatedAt: {
|
||||
gte: 'now-2h',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
match_all: {},
|
||||
},
|
||||
],
|
||||
must_not: [],
|
||||
},
|
||||
},
|
||||
size: 40,
|
||||
sort: {
|
||||
isTempDoc: {
|
||||
order: 'asc',
|
||||
},
|
||||
sliValue: {
|
||||
order: 'asc',
|
||||
},
|
||||
},
|
||||
track_total_hits: true,
|
||||
},
|
||||
]);
|
||||
|
||||
await service.search('', '', defaultSort, defaultPagination);
|
||||
expect(esClientMock.search.mock.calls[1]).toEqual([
|
||||
{
|
||||
from: 0,
|
||||
index: ['.slo-observability.summary-v3*'],
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
term: {
|
||||
spaceId: 'default',
|
||||
},
|
||||
},
|
||||
{
|
||||
match_all: {},
|
||||
},
|
||||
],
|
||||
must_not: [],
|
||||
},
|
||||
},
|
||||
size: 40,
|
||||
sort: {
|
||||
isTempDoc: {
|
||||
order: 'asc',
|
||||
},
|
||||
sliValue: {
|
||||
order: 'asc',
|
||||
},
|
||||
},
|
||||
track_total_hits: true,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('handles summaryUpdate kql filter override', async () => {
|
||||
await service.search('summaryUpdatedAt > now-2h', '', defaultSort, defaultPagination, true);
|
||||
expect(esClientMock.search.mock.calls[0]).toEqual([
|
||||
{
|
||||
from: 0,
|
||||
index: ['.slo-observability.summary-v3*'],
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{ term: { spaceId: 'default' } },
|
||||
{
|
||||
bool: {
|
||||
minimum_should_match: 1,
|
||||
should: [{ range: { summaryUpdatedAt: { gt: 'now-2h' } } }],
|
||||
},
|
||||
},
|
||||
],
|
||||
must_not: [],
|
||||
},
|
||||
},
|
||||
size: 40,
|
||||
sort: { isTempDoc: { order: 'asc' }, sliValue: { order: 'asc' } },
|
||||
track_total_hits: true,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -10,11 +10,12 @@ import { ElasticsearchClient, Logger, SavedObjectsClientContract } from '@kbn/co
|
|||
import { ALL_VALUE, Paginated, Pagination } from '@kbn/slo-schema';
|
||||
import { assertNever } from '@kbn/std';
|
||||
import { partition } from 'lodash';
|
||||
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import { SLO_SUMMARY_DESTINATION_INDEX_PATTERN } from '../../common/constants';
|
||||
import { Groupings, SLODefinition, SLOId, Summary } from '../domain/models';
|
||||
import { Groupings, SLODefinition, SLOId, StoredSLOSettings, Summary } from '../domain/models';
|
||||
import { toHighPrecision } from '../utils/number';
|
||||
import { createEsParams, typedSearch } from '../utils/queries';
|
||||
import { getListOfSummaryIndices } from './slo_settings';
|
||||
import { getListOfSummaryIndices, getSloSettings } from './slo_settings';
|
||||
import { EsSummaryDocument } from './summary_transform_generator/helpers/create_temp_summary';
|
||||
import { getElasticsearchQueryOrThrow } from './transform_generators';
|
||||
import { fromRemoteSummaryDocumentToSloDefinition } from './unsafe_federated/remote_summary_doc_to_slo';
|
||||
|
@ -44,7 +45,8 @@ export interface SummarySearchClient {
|
|||
kqlQuery: string,
|
||||
filters: string,
|
||||
sort: Sort,
|
||||
pagination: Pagination
|
||||
pagination: Pagination,
|
||||
hideStale?: boolean
|
||||
): Promise<Paginated<SummaryResult>>;
|
||||
}
|
||||
|
||||
|
@ -60,7 +62,8 @@ export class DefaultSummarySearchClient implements SummarySearchClient {
|
|||
kqlQuery: string,
|
||||
filters: string,
|
||||
sort: Sort,
|
||||
pagination: Pagination
|
||||
pagination: Pagination,
|
||||
hideStale?: boolean
|
||||
): Promise<Paginated<SummaryResult>> {
|
||||
let parsedFilters: any = {};
|
||||
|
||||
|
@ -69,8 +72,8 @@ export class DefaultSummarySearchClient implements SummarySearchClient {
|
|||
} catch (e) {
|
||||
this.logger.error(`Failed to parse filters: ${e.message}`);
|
||||
}
|
||||
|
||||
const indices = await getListOfSummaryIndices(this.soClient, this.esClient);
|
||||
const settings = await getSloSettings(this.soClient);
|
||||
const { indices } = await getListOfSummaryIndices(this.esClient, settings);
|
||||
const esParams = createEsParams({
|
||||
index: indices,
|
||||
track_total_hits: true,
|
||||
|
@ -78,6 +81,7 @@ export class DefaultSummarySearchClient implements SummarySearchClient {
|
|||
bool: {
|
||||
filter: [
|
||||
{ term: { spaceId: this.spaceId } },
|
||||
...excludeStaleSummaryFilter(settings, kqlQuery, hideStale),
|
||||
getElasticsearchQueryOrThrow(kqlQuery),
|
||||
...(parsedFilters.filter ?? []),
|
||||
],
|
||||
|
@ -159,6 +163,7 @@ export class DefaultSummarySearchClient implements SummarySearchClient {
|
|||
},
|
||||
sliValue: toHighPrecision(doc._source.sliValue),
|
||||
status: summaryDoc.status,
|
||||
summaryUpdatedAt: summaryDoc.summaryUpdatedAt,
|
||||
},
|
||||
groupings: getFlattenedGroupings({
|
||||
groupings: summaryDoc.slo.groupings,
|
||||
|
@ -189,6 +194,32 @@ export class DefaultSummarySearchClient implements SummarySearchClient {
|
|||
}
|
||||
}
|
||||
|
||||
function excludeStaleSummaryFilter(
|
||||
settings: StoredSLOSettings,
|
||||
kqlFilter: string,
|
||||
hideStale?: boolean
|
||||
): estypes.QueryDslQueryContainer[] {
|
||||
if (kqlFilter.includes('summaryUpdatedAt') || !settings.staleThresholdInHours || !hideStale) {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
{
|
||||
bool: {
|
||||
should: [
|
||||
{ term: { isTempDoc: true } },
|
||||
{
|
||||
range: {
|
||||
summaryUpdatedAt: {
|
||||
gte: `now-${settings.staleThresholdInHours}h`,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function getRemoteClusterName(index: string) {
|
||||
if (index.includes(':')) {
|
||||
return index.split(':')[0];
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue