[Synthetics] Monitor status rule, show to which monitors rule applies !! (#209281)

## Summary

Fixes https://github.com/elastic/kibana/issues/198688

Monitor status rule, show to which monitors rule applies !!

With filters shows to which monitors the rule applies 
<img width="1728" alt="image"
src="https://github.com/user-attachments/assets/5536ad12-d0ed-4394-a511-8dd826bf3b56"
/>


Also showing an inspect flyout with query details

<img width="1728" alt="image"
src="https://github.com/user-attachments/assets/d441e4f9-8f0c-4fac-a2ca-5f214f8d2709"
/>
This commit is contained in:
Shahzad 2025-02-06 17:07:22 +01:00 committed by GitHub
parent 5da812ceda
commit ac89e472a6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 466 additions and 61 deletions

View file

@ -54,4 +54,6 @@ export enum SYNTHETICS_API_URLS {
SYNTHETICS_MONITORS_PROJECT_DELETE = '/api/synthetics/project/{projectName}/monitors/_bulk_delete',
DYNAMIC_SETTINGS = `/api/synthetics/settings`,
INSPECT_STATUS_RULE = '/internal/synthetics/inspect_status_rule',
}

View file

@ -71,9 +71,17 @@ export const AlertStatusCodec = t.interface({
pendingConfigs: t.record(t.string, AlertPendingStatusMetaDataCodec),
enabledMonitorQueryIds: t.array(t.string),
staleDownConfigs: t.record(t.string, StaleAlertStatusMetaDataCodec),
maxPeriod: t.number,
});
export type StaleDownConfig = t.TypeOf<typeof StaleAlertStatusMetaDataCodec>;
export type AlertStatusMetaData = t.TypeOf<typeof AlertStatusMetaDataCodec>;
export type AlertOverviewStatus = t.TypeOf<typeof AlertStatusCodec>;
export type StatusRuleInspect = AlertOverviewStatus & {
monitors: Array<{
id: string;
name: string;
type: string;
}>;
};
export type AlertStatusConfigs = Record<string, AlertStatusMetaData>;

View file

@ -9,7 +9,7 @@ import React, { useEffect } from 'react';
import { i18n } from '@kbn/i18n';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { Filter } from '@kbn/es-query';
import { EuiFormRow } from '@elastic/eui';
import { EuiFormRow, EuiSkeletonText } from '@elastic/eui';
import { useSyntheticsDataView } from '../../contexts/synthetics_data_view_context';
import { ClientPluginsStart } from '../../../../plugin';
@ -40,6 +40,10 @@ export function AlertSearchBar({
return () => sub.unsubscribe();
}, [onChange, query]);
if (!dataView) {
return <EuiSkeletonText lines={1} />;
}
return (
<EuiFormRow
label={i18n.translate('xpack.synthetics.list.search.title', {
@ -51,7 +55,7 @@ export function AlertSearchBar({
appName="synthetics"
iconType="search"
placeholder={PLACEHOLDER}
indexPatterns={dataView ? [dataView] : []}
indexPatterns={[dataView]}
onChange={(queryN) => {
onChange({
kqlQuery: String(queryN.query),

View file

@ -10,6 +10,7 @@ import { RuleTypeParamsExpressionProps } from '@kbn/triggers-actions-ui-plugin/p
import { Filter } from '@kbn/es-query';
import { EuiSpacer } from '@elastic/eui';
import { SyntheticsMonitorStatusRuleParams as StatusRuleParams } from '@kbn/response-ops-rule-params/synthetics_monitor_status';
import { StatusRuleViz } from './status_rule_viz';
import { FieldFilters } from './common/field_filters';
import { AlertSearchBar } from './query_bar';
import { StatusRuleExpression } from './status_rule_expression';
@ -32,8 +33,9 @@ export const StatusRuleComponent: React.FC<{
<AlertSearchBar kqlQuery={ruleParams.kqlQuery ?? ''} onChange={onFiltersChange} />
<EuiSpacer size="m" />
<FieldFilters ruleParams={ruleParams} setRuleParams={setRuleParams} />
<StatusRuleExpression ruleParams={ruleParams} setRuleParams={setRuleParams} />
<StatusRuleViz ruleParams={ruleParams} />
<EuiSpacer size="m" />
<StatusRuleExpression ruleParams={ruleParams} setRuleParams={setRuleParams} />
</>
);
};

View file

@ -0,0 +1,113 @@
/*
* 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 React, { useEffect } from 'react';
import {
EuiButtonEmpty,
EuiCallOut,
EuiFlexGroup,
EuiFlexItem,
EuiPopover,
EuiPopoverTitle,
EuiSpacer,
} from '@elastic/eui';
import { useSelector, useDispatch } from 'react-redux';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { i18n } from '@kbn/i18n';
import { useInspectorContext } from '@kbn/observability-shared-plugin/public';
import { RuleMonitorsTable } from '../rule_monitors_table';
import { apiService } from '../../../../utils/api_service';
import { inspectStatusRuleAction } from '../../state/alert_rules';
import { selectInspectStatusRule } from '../../state/alert_rules/selectors';
import { StatusRuleParamsProps } from './status_rule_ui';
import { ClientPluginsStart } from '../../../../plugin';
export const StatusRuleViz = ({
ruleParams,
}: {
ruleParams: StatusRuleParamsProps['ruleParams'];
}) => {
const { data } = useSelector(selectInspectStatusRule);
const dispatch = useDispatch();
const {
services: { inspector },
} = useKibana<ClientPluginsStart>();
const [isPopoverOpen, setIsPopoverOpen] = React.useState(false);
const { inspectorAdapters, addInspectorRequest } = useInspectorContext();
const inspect = () => {
inspector.open(inspectorAdapters);
};
useEffect(() => {
apiService.addInspectorRequest = addInspectorRequest;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
inspectorAdapters?.requests?.reset();
dispatch(inspectStatusRuleAction.get(ruleParams));
}, [ruleParams, dispatch, inspectorAdapters?.requests]);
return (
<EuiCallOut iconType="search" size="s">
<EuiFlexGroup alignItems="center" gutterSize="none">
<EuiFlexItem grow={false}>
{i18n.translate('xpack.synthetics.statusRuleViz.ruleAppliesToFlexItemLabel', {
defaultMessage: 'Rule applies to ',
})}
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiPopover
isOpen={isPopoverOpen}
closePopover={() => setIsPopoverOpen(false)}
button={
<EuiButtonEmpty
data-test-subj="syntheticsStatusRuleVizMonitorQueryIDsButton"
size="xs"
onClick={() => setIsPopoverOpen(!isPopoverOpen)}
>
{i18n.translate('xpack.synthetics.statusRuleViz.monitorQueryIdsPopoverButton', {
defaultMessage:
'{total} existing {total, plural, one {monitor} other {monitors}}',
values: { total: data?.monitors.length },
})}
</EuiButtonEmpty>
}
>
<EuiPopoverTitle>
{i18n.translate('xpack.synthetics.statusRuleViz.monitorsPopoverTitleLabel', {
defaultMessage: 'Monitors',
})}
</EuiPopoverTitle>
{i18n.translate('xpack.synthetics.statusRuleViz.ruleAppliesToFollowingPopoverLabel', {
defaultMessage: 'Rule applies to following existing monitors.',
})}
<EuiSpacer size="s" />
<RuleMonitorsTable />
</EuiPopover>
</EuiFlexItem>
{/* to push detail button to end*/}
<EuiFlexItem />
<EuiFlexItem grow={false}>
<EuiButtonEmpty
data-test-subj="syntheticsStatusRuleVizInspectButton"
onClick={inspect}
iconType="inspect"
size="xs"
>
{i18n.translate('xpack.synthetics.rules.details', {
defaultMessage: 'Details',
})}
</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGroup>
</EuiCallOut>
);
};

View file

@ -0,0 +1,102 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { Criteria, EuiLink, EuiInMemoryTable, EuiSearchBarProps } from '@elastic/eui';
import React from 'react';
import { i18n } from '@kbn/i18n';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { useSelector } from 'react-redux';
import { uniqBy } from 'lodash';
import { selectInspectStatusRule } from '../state/alert_rules/selectors';
import { ClientPluginsStart } from '../../../plugin';
export const RuleMonitorsTable = () => {
const {
services: { http },
} = useKibana<ClientPluginsStart>();
const { data } = useSelector(selectInspectStatusRule);
const [pageIndex, setPageIndex] = React.useState(0);
const [pageSize, setPageSize] = React.useState(10);
const onTableChange = ({ page }: Criteria<any>) => {
if (page) {
setPageIndex(page.index);
setPageSize(page.size);
}
};
const columns = [
{
field: 'name',
name: i18n.translate('xpack.synthetics.ruleDetails.monitorsTable.nameColumn', {
defaultMessage: 'Name',
}),
render: (name: string, monitor: { id: string }) => (
<EuiLink
data-test-subj="ColumnsLink"
href={http.basePath.prepend(`/app/synthetics/monitors/${monitor.id}`)}
>
{name}
</EuiLink>
),
},
{
field: 'id',
name: i18n.translate('xpack.synthetics.ruleDetails.monitorsTable.idColumn', {
defaultMessage: 'ID',
}),
},
{
field: 'type',
name: i18n.translate('xpack.synthetics.ruleDetails.monitorsTable.type', {
defaultMessage: 'Type',
}),
},
];
const search: EuiSearchBarProps = {
box: {
incremental: true,
schema: true,
},
filters: [
{
type: 'field_value_selection',
field: 'type',
name: i18n.translate('xpack.synthetics.ruleDetails.monitorsTable.typeFilter', {
defaultMessage: 'Type',
}),
multiSelect: false,
options:
uniqBy(
data?.monitors.map((monitor) => ({
value: monitor.type,
name: monitor.type,
})),
'value'
) ?? [],
},
],
};
return (
<EuiInMemoryTable
tableLayout="auto"
css={{ width: 600 }}
items={data?.monitors ?? []}
columns={columns}
onChange={onTableChange}
search={search}
pagination={{
pageIndex,
pageSize,
showPerPageOptions: true,
}}
/>
);
};

View file

@ -9,6 +9,7 @@ import React from 'react';
import { CoreStart } from '@kbn/core/public';
import { Provider as ReduxProvider } from 'react-redux';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { InspectorContextProvider } from '@kbn/observability-shared-plugin/public';
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
import { RuleTypeParamsExpressionProps } from '@kbn/triggers-actions-ui-plugin/public';
import { EuiSpacer, EuiText } from '@elastic/eui';
@ -32,25 +33,27 @@ export default function MonitorStatusAlert({ coreStart, plugins, params }: Props
const queryClient = new QueryClient();
const { ruleParams } = params;
return (
<ReduxProvider store={store}>
<QueryClientProvider client={queryClient}>
<KibanaContextProvider services={{ ...coreStart, ...plugins }}>
{params.id && isEmpty(ruleParams) && (
<EuiText>
<FormattedMessage
id="xpack.synthetics.alertRule.monitorStatus.description"
defaultMessage="Manage synthetics monitor status rule actions."
/>
</EuiText>
)}
<InspectorContextProvider>
<ReduxProvider store={store}>
<QueryClientProvider client={queryClient}>
<KibanaContextProvider services={{ ...coreStart, ...plugins }}>
{params.id && isEmpty(ruleParams) && (
<EuiText>
<FormattedMessage
id="xpack.synthetics.alertRule.monitorStatus.description"
defaultMessage="Manage synthetics monitor status rule actions."
/>
</EuiText>
)}
{(!params.id || !isEmpty(ruleParams)) && (
<StatusRuleComponent ruleParams={ruleParams} setRuleParams={params.setRuleParams} />
)}
{(!params.id || !isEmpty(ruleParams)) && (
<StatusRuleComponent ruleParams={ruleParams} setRuleParams={params.setRuleParams} />
)}
<EuiSpacer size="m" />
</KibanaContextProvider>
</QueryClientProvider>
</ReduxProvider>
<EuiSpacer size="m" />
</KibanaContextProvider>
</QueryClientProvider>
</ReduxProvider>
</InspectorContextProvider>
);
}

View file

@ -5,6 +5,8 @@
* 2.0.
*/
import { StatusRuleInspect } from '../../../../../common/runtime_types/alert_rules/common';
import { StatusRuleParamsProps } from '../../components/alerts/status_rule_ui';
import { DEFAULT_ALERT_RESPONSE } from '../../../../../common/types/default_alerts';
import { createAsyncAction } from '../utils/actions';
@ -23,3 +25,8 @@ export const enableDefaultAlertingSilentlyAction = createAsyncAction<void, DEFAU
export const updateDefaultAlertingAction = createAsyncAction<void, DEFAULT_ALERT_RESPONSE>(
'updateDefaultAlertingAction'
);
export const inspectStatusRuleAction = createAsyncAction<
StatusRuleParamsProps['ruleParams'],
StatusRuleInspect
>('inspectStatusRuleAction');

View file

@ -5,10 +5,18 @@
* 2.0.
*/
import { StatusRuleInspect } from '../../../../../common/runtime_types/alert_rules/common';
import { StatusRuleParamsProps } from '../../components/alerts/status_rule_ui';
import { SYNTHETICS_API_URLS } from '../../../../../common/constants';
import { DEFAULT_ALERT_RESPONSE } from '../../../../../common/types/default_alerts';
import { apiService } from '../../../../utils/api_service';
export async function inspectStatusAlertAPI(
ruleParams: StatusRuleParamsProps['ruleParams']
): Promise<StatusRuleInspect> {
return apiService.post(SYNTHETICS_API_URLS.INSPECT_STATUS_RULE, ruleParams);
}
export async function getDefaultAlertingAPI(): Promise<DEFAULT_ALERT_RESPONSE> {
return apiService.get(SYNTHETICS_API_URLS.ENABLE_DEFAULT_ALERTING);
}

View file

@ -11,10 +11,16 @@ import {
enableDefaultAlertingAction,
enableDefaultAlertingSilentlyAction,
getDefaultAlertingAction,
inspectStatusRuleAction,
updateDefaultAlertingAction,
} from './actions';
import { fetchEffectFactory } from '../utils/fetch_effect';
import { enableDefaultAlertingAPI, getDefaultAlertingAPI, updateDefaultAlertingAPI } from './api';
import {
enableDefaultAlertingAPI,
getDefaultAlertingAPI,
inspectStatusAlertAPI,
updateDefaultAlertingAPI,
} from './api';
export function* getDefaultAlertingEffect() {
yield takeLeading(
@ -68,6 +74,21 @@ export function* updateDefaultAlertingEffect() {
);
}
export function* inspectStatusRuleEffect() {
yield takeLeading(
inspectStatusRuleAction.get,
fetchEffectFactory(
inspectStatusAlertAPI,
inspectStatusRuleAction.success,
inspectStatusRuleAction.fail,
'',
i18n.translate('xpack.synthetics.settings.statusRule.inspect', {
defaultMessage: 'Failed to inspect monitor status rule type.',
})
)
);
}
const successMessage = i18n.translate('xpack.synthetics.settings.enableAlerting', {
defaultMessage:
'Monitor status rule successfully updated. Changes will take effect on the next rule execution.',

View file

@ -6,26 +6,34 @@
*/
import { createReducer } from '@reduxjs/toolkit';
import { StatusRuleInspect } from '../../../../../common/runtime_types/alert_rules/common';
import { DEFAULT_ALERT_RESPONSE } from '../../../../../common/types/default_alerts';
import { IHttpSerializedFetchError } from '..';
import {
enableDefaultAlertingAction,
enableDefaultAlertingSilentlyAction,
getDefaultAlertingAction,
inspectStatusRuleAction,
updateDefaultAlertingAction,
} from './actions';
export interface DefaultAlertingState {
inspectData?: StatusRuleInspect;
data?: DEFAULT_ALERT_RESPONSE;
success: boolean | null;
loading: boolean;
error: IHttpSerializedFetchError | null;
inspectLoading: boolean;
inspectError?: IHttpSerializedFetchError | null;
}
const initialSettingState: DefaultAlertingState = {
success: null,
loading: false,
error: null,
inspectData: undefined,
inspectLoading: false,
inspectError: null,
};
export const defaultAlertingReducer = createReducer(initialSettingState, (builder) => {
@ -60,6 +68,18 @@ export const defaultAlertingReducer = createReducer(initialSettingState, (builde
state.error = action.payload;
state.loading = false;
state.success = false;
})
.addCase(inspectStatusRuleAction.get, (state) => {
state.inspectLoading = true;
})
.addCase(inspectStatusRuleAction.success, (state, action) => {
state.inspectData = action.payload;
state.inspectLoading = false;
state.inspectError = null;
})
.addCase(inspectStatusRuleAction.fail, (state, action) => {
state.inspectError = action.payload;
state.inspectLoading = false;
});
});

View file

@ -12,3 +12,10 @@ const getState = (appState: SyntheticsAppState) => appState.defaultAlerting;
export const selectSyntheticsAlerts = createSelector(getState, (state) => state.data);
export const selectSyntheticsAlertsLoading = createSelector(getState, (state) => state.loading);
export const selectSyntheticsAlertsLoaded = createSelector(getState, (state) => state.success);
export const selectInspectStatusRule = createSelector(getState, (state) => {
return {
loading: state.inspectLoading,
data: state.inspectData,
error: state.inspectError,
};
});

View file

@ -18,6 +18,7 @@ import {
enableDefaultAlertingEffect,
enableDefaultAlertingSilentlyEffect,
getDefaultAlertingEffect,
inspectStatusRuleEffect,
updateDefaultAlertingEffect,
} from './alert_rules/effects';
import { executeEsQueryEffect } from './elasticsearch';
@ -80,6 +81,7 @@ export const rootEffect = function* root(): Generator {
fork(quietFetchMonitorStatusHeatmap),
fork(fetchOverviewTrendStats),
fork(refreshOverviewTrendStats),
fork(inspectStatusRuleEffect),
...privateLocationsEffects.map((effect) => fork(effect)),
]);
};

View file

@ -130,6 +130,7 @@ export const mockState: SyntheticsAppState = {
locationMonitors: [],
},
defaultAlerting: {
inspectLoading: false,
loading: false,
error: null,
success: null,

View file

@ -12,6 +12,7 @@ import { observabilityPaths } from '@kbn/observability-plugin/common';
import apm from 'elastic-apm-node';
import { SYNTHETICS_ALERT_RULE_TYPES } from '@kbn/rule-data-utils';
import { syntheticsMonitorStatusRuleParamsSchema } from '@kbn/response-ops-rule-params/synthetics_monitor_status';
import { SyntheticsEsClient } from '../../lib';
import { AlertOverviewStatus } from '../../../common/runtime_types/alert_rules/common';
import { StatusRuleExecutorOptions } from './types';
import { syntheticsRuleFieldMap } from '../../../common/rules/synthetics_rule_field_map';
@ -26,6 +27,7 @@ import {
import { getActionVariables } from '../action_variables';
import { STATUS_RULE_NAME } from '../translations';
import { SyntheticsMonitorClient } from '../../synthetics_service/synthetics_monitor/synthetics_monitor_client';
import { SYNTHETICS_INDEX_PATTERN } from '../../../common/constants';
export const registerSyntheticsStatusCheckRule = (
server: SyntheticsServerSetup,
@ -55,7 +57,7 @@ export const registerSyntheticsStatusCheckRule = (
executor: async (options: StatusRuleExecutorOptions) => {
apm.setTransactionName('Synthetics Status Rule Executor');
const { state: ruleState, params, services, spaceId } = options;
const { alertsClient, uiSettingsClient } = services;
const { alertsClient, uiSettingsClient, scopedClusterClient, savedObjectsClient } = services;
if (!alertsClient) {
throw new AlertsClientError();
}
@ -70,7 +72,15 @@ export const registerSyntheticsStatusCheckRule = (
const groupBy = params?.condition?.groupBy ?? 'locationId';
const groupByLocation = groupBy === 'locationId';
const statusRule = new StatusRuleExecutor(server, syntheticsMonitorClient, options);
const esClient = new SyntheticsEsClient(
savedObjectsClient,
scopedClusterClient.asCurrentUser,
{
heartbeatIndices: SYNTHETICS_INDEX_PATTERN,
}
);
const statusRule = new StatusRuleExecutor(esClient, server, syntheticsMonitorClient, options);
const { downConfigs, staleDownConfigs, upConfigs } = await statusRule.getDownChecks(
ruleState.meta?.downConfigs as AlertOverviewStatus['downConfigs']

View file

@ -28,39 +28,42 @@ export async function queryFilterMonitors({
return;
}
const filters = toElasticsearchQuery(fromKueryExpression(ruleParams.kqlQuery));
const { body: result } = await esClient.search({
body: {
size: 0,
query: {
bool: {
filter: [
FINAL_SUMMARY_FILTER,
getRangeFilter({ from: 'now-24h/m', to: 'now/m' }),
getTimeSpanFilter(),
{
term: {
'meta.space_id': spaceId,
const { body: result } = await esClient.search(
{
body: {
size: 0,
query: {
bool: {
filter: [
FINAL_SUMMARY_FILTER,
getRangeFilter({ from: 'now-24h/m', to: 'now/m' }),
getTimeSpanFilter(),
{
term: {
'meta.space_id': spaceId,
},
},
},
{
bool: {
should: filters,
{
bool: {
should: filters,
},
},
},
...getFilters(ruleParams),
],
...getFilters(ruleParams),
],
},
},
},
aggs: {
ids: {
terms: {
size: 10000,
field: 'config_id',
aggs: {
ids: {
terms: {
size: 10000,
field: 'config_id',
},
},
},
},
},
});
'queryFilterMonitors'
);
return result.aggregations?.ids.buckets.map((bucket) => bucket.key as string);
}

View file

@ -12,7 +12,7 @@ import { intersection } from 'lodash';
import { AlertStatusMetaData } from '../../../../common/runtime_types/alert_rules/common';
import {
FINAL_SUMMARY_FILTER,
getTimespanFilter,
getRangeFilter,
SUMMARY_FILTER,
} from '../../../../common/constants/client_defaults';
import { OverviewPing } from '../../../../common/runtime_types';
@ -75,7 +75,7 @@ export async function queryMonitorStatusAlert({
bool: {
filter: [
...(includeRetests ? [SUMMARY_FILTER] : [FINAL_SUMMARY_FILTER]),
getTimespanFilter({ from: range.from, to: range.to }),
getRangeFilter({ from: range.from, to: range.to }),
{
terms: {
'monitor.id': idsToQuery,
@ -137,7 +137,10 @@ export async function queryMonitorStatusAlert({
});
}
const { body: result } = await esClient.search<OverviewPing, typeof params>(params);
const { body: result } = await esClient.search<OverviewPing, typeof params>(
params,
'Monitors status rule query'
);
result.aggregations?.id.buckets.forEach(({ location, key: queryId }) => {
const locationSummaries = location.buckets.map(

View file

@ -17,6 +17,8 @@ import * as locationsUtils from '../../synthetics_service/get_all_locations';
import type { PublicLocation } from '../../../common/runtime_types';
import { SyntheticsServerSetup } from '../../types';
import { AlertStatusMetaData } from '../../../common/runtime_types/alert_rules/common';
import { SyntheticsEsClient } from '../../lib';
import { SYNTHETICS_INDEX_PATTERN } from '../../../common/constants';
describe('StatusRuleExecutor', () => {
// @ts-ignore
@ -65,8 +67,11 @@ describe('StatusRuleExecutor', () => {
const mockStart = coreMock.createStart();
const uiSettingsClient = mockStart.uiSettings.asScopedToClient(soClient);
const esClient = new SyntheticsEsClient(soClient, mockEsClient, {
heartbeatIndices: SYNTHETICS_INDEX_PATTERN,
});
const statusRule = new StatusRuleExecutor(serverMock, monitorClient, {
const statusRule = new StatusRuleExecutor(esClient, serverMock, monitorClient, {
params: {},
services: {
uiSettingsClient,

View file

@ -18,6 +18,7 @@ import {
AlertStatusConfigs,
AlertStatusMetaData,
StaleDownConfig,
StatusRuleInspect,
} from '../../../common/runtime_types/alert_rules/common';
import { queryFilterMonitors } from './queries/filter_monitors';
import { MonitorSummaryStatusRule, StatusRuleExecutorOptions } from './types';
@ -37,7 +38,6 @@ import { queryMonitorStatusAlert } from './queries/query_monitor_status_alert';
import { parseArrayFilters } from '../../routes/common';
import { SyntheticsServerSetup } from '../../types';
import { SyntheticsEsClient } from '../../lib';
import { SYNTHETICS_INDEX_PATTERN } from '../../../common/constants';
import {
getAllMonitors,
processMonitors,
@ -67,20 +67,19 @@ export class StatusRuleExecutor {
ruleName: string;
constructor(
esClient: SyntheticsEsClient,
server: SyntheticsServerSetup,
syntheticsMonitorClient: SyntheticsMonitorClient,
options: StatusRuleExecutorOptions
) {
const { services, params, previousStartedAt, rule } = options;
const { scopedClusterClient, savedObjectsClient } = services;
const { savedObjectsClient } = services;
this.ruleName = rule.name;
this.logger = server.logger;
this.previousStartedAt = previousStartedAt;
this.params = params;
this.soClient = savedObjectsClient;
this.esClient = new SyntheticsEsClient(this.soClient, scopedClusterClient.asCurrentUser, {
heartbeatIndices: SYNTHETICS_INDEX_PATTERN,
});
this.esClient = esClient;
this.server = server;
this.syntheticsMonitorClient = syntheticsMonitorClient;
this.hasCustomCondition = !isEmpty(this.params);
@ -97,6 +96,7 @@ export class StatusRuleExecutor {
this.dateFormat = await uiSettingsClient.get('dateFormat');
const timezone = await uiSettingsClient.get('dateFormat:tz');
this.tz = timezone === 'Browser' ? 'UTC' : timezone;
return await this.getMonitors();
}
async getMonitors() {
@ -135,9 +135,8 @@ export class StatusRuleExecutor {
}
async getDownChecks(prevDownConfigs: AlertStatusConfigs = {}): Promise<AlertOverviewStatus> {
await this.init();
const { enabledMonitorQueryIds, maxPeriod, monitorLocationIds, monitorLocationsMap } =
await this.getMonitors();
await this.init();
const range = this.getRange(maxPeriod);
@ -151,6 +150,7 @@ export class StatusRuleExecutor {
staleDownConfigs,
enabledMonitorQueryIds,
pendingConfigs: {},
maxPeriod,
};
}
@ -203,6 +203,7 @@ export class StatusRuleExecutor {
...currentStatus,
staleDownConfigs,
pendingConfigs: {},
maxPeriod,
};
}
@ -430,6 +431,26 @@ export class StatusRuleExecutor {
context,
});
}
getRuleThresholdOverview = async (): Promise<StatusRuleInspect> => {
const data = await this.getDownChecks({});
return {
...data,
monitors: [
...this.monitors.map((monitor) => ({
id: monitor.id,
name: monitor.attributes.name,
type: monitor.attributes.type,
})),
// add some 1000 dummy monitors to test the pagination
...new Array(1000).fill(null).map((_, index) => ({
id: `dummy-${index}`,
name: `dummy-${index}`,
type: 'http',
})),
],
};
};
}
export const getDoesMonitorMeetLocationThreshold = ({

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import { syntheticsInspectStatusRuleRoute } from './rules/inspect_status_rule';
import { syntheticsGetLatestTestRunRoute } from './pings/get_latest_test_run';
import { deleteSyntheticsParamsBulkRoute } from './settings/params/delete_params_bulk';
import { deleteSyntheticsMonitorBulkRoute } from './monitor_cruds/bulk_cruds/delete_monitor_bulk';
@ -101,6 +102,7 @@ export const syntheticsAppRestApiRoutes: SyntheticsRestApiRouteFactory[] = [
createPostDynamicSettingsRoute,
syntheticsGetPingHeatmapRoute,
createOverviewTrendsRoute,
syntheticsInspectStatusRuleRoute,
];
export const syntheticsAppPublicRestApiRoutes: SyntheticsRestApiRouteFactory[] = [

View file

@ -0,0 +1,61 @@
/*
* 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 {
SyntheticsMonitorStatusRuleParams,
syntheticsMonitorStatusRuleParamsSchema,
} from '@kbn/response-ops-rule-params/synthetics_monitor_status';
import { StatusRuleExecutorOptions } from '../../alert_rules/status_rule/types';
import { StatusRuleExecutor } from '../../alert_rules/status_rule/status_rule_executor';
import { SyntheticsRestApiRouteFactory } from '../types';
import { SYNTHETICS_API_URLS } from '../../../common/constants';
export const syntheticsInspectStatusRuleRoute: SyntheticsRestApiRouteFactory = () => ({
method: 'POST',
path: SYNTHETICS_API_URLS.INSPECT_STATUS_RULE,
validate: {
body: syntheticsMonitorStatusRuleParamsSchema,
},
handler: async ({
request,
server,
syntheticsMonitorClient,
savedObjectsClient,
spaceId,
context,
syntheticsEsClient,
}): Promise<any> => {
const { uiSettings, elasticsearch } = await context.core;
const { client: esClient } = elasticsearch;
const ruleParams = request.body as SyntheticsMonitorStatusRuleParams;
const services = {
scopedClusterClient: esClient,
savedObjectsClient,
uiSettingsClient: uiSettings.client,
} as unknown as StatusRuleExecutorOptions['services'];
const statusRule = new StatusRuleExecutor(syntheticsEsClient, server, syntheticsMonitorClient, {
spaceId,
services,
params: ruleParams,
state: {} as any,
previousStartedAt: new Date(),
rule: { name: 'Inspect Status Rule', id: 'inspect-status-rule' } as any,
logger: server.logger,
executionId: 'inspect-status-rule',
startedAt: new Date(),
isServerless: false,
startedAtOverridden: false,
flappingSettings: {} as any,
getTimeRange: {} as any,
} as StatusRuleExecutorOptions);
return await statusRule.getRuleThresholdOverview();
},
});