diff --git a/x-pack/plugins/observability_solution/synthetics/common/constants/synthetics/rest_api.ts b/x-pack/plugins/observability_solution/synthetics/common/constants/synthetics/rest_api.ts index 0003360c707b63..0d68c1d9de31cf 100644 --- a/x-pack/plugins/observability_solution/synthetics/common/constants/synthetics/rest_api.ts +++ b/x-pack/plugins/observability_solution/synthetics/common/constants/synthetics/rest_api.ts @@ -26,6 +26,8 @@ export enum SYNTHETICS_API_URLS { SYNTHETICS_OVERVIEW = '/internal/synthetics/overview', PINGS = '/internal/synthetics/pings', PING_STATUSES = '/internal/synthetics/ping_statuses', + MONITOR_STATUS_HEATMAP = '/internal/synthetics/ping_heatmap', + OVERVIEW_TRENDS = '/internal/synthetics/overview_trends', OVERVIEW_STATUS = `/internal/synthetics/overview_status`, INDEX_SIZE = `/internal/synthetics/index_size`, AGENT_POLICIES = `/internal/synthetics/agent_policies`, diff --git a/x-pack/plugins/observability_solution/synthetics/common/runtime_types/ping/ping.ts b/x-pack/plugins/observability_solution/synthetics/common/runtime_types/ping/ping.ts index 5d481038b0869b..03397afaf60fc0 100644 --- a/x-pack/plugins/observability_solution/synthetics/common/runtime_types/ping/ping.ts +++ b/x-pack/plugins/observability_solution/synthetics/common/runtime_types/ping/ping.ts @@ -255,24 +255,6 @@ export const PingStateType = t.type({ export type Ping = t.TypeOf; export type PingState = t.TypeOf; -export const PingStatusType = t.intersection([ - t.type({ - timestamp: t.string, - docId: t.string, - config_id: t.string, - locationId: t.string, - summary: t.partial({ - down: t.number, - up: t.number, - }), - }), - t.partial({ - error: PingErrorType, - }), -]); - -export type PingStatus = t.TypeOf; - export const PingsResponseType = t.type({ total: t.number, pings: t.array(PingType), @@ -280,15 +262,6 @@ export const PingsResponseType = t.type({ export type PingsResponse = t.TypeOf; -export const PingStatusesResponseType = t.type({ - total: t.number, - pings: t.array(PingStatusType), - from: t.string, - to: t.string, -}); - -export type PingStatusesResponse = t.TypeOf; - export const GetPingsParamsType = t.intersection([ t.type({ dateRange: DateRangeType, @@ -306,3 +279,17 @@ export const GetPingsParamsType = t.intersection([ ]); export type GetPingsParams = t.TypeOf; + +export const MonitorStatusHeatmapBucketType = t.type({ + doc_count: t.number, + down: t.type({ + value: t.number, + }), + up: t.type({ + value: t.number, + }), + key: t.number, + key_as_string: t.string, +}); + +export type MonitorStatusHeatmapBucket = t.TypeOf; diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitor_details/hooks/use_ping_statuses.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitor_details/hooks/use_ping_statuses.tsx deleted file mode 100644 index cb408678c022cd..00000000000000 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitor_details/hooks/use_ping_statuses.tsx +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { useEffect, useCallback, useRef } from 'react'; -import { useSelector, useDispatch } from 'react-redux'; - -import { ConfigKey, PingStatus } from '../../../../../../common/runtime_types'; -import { - getMonitorPingStatusesAction, - selectIsMonitorStatusesLoading, - selectPingStatusesForMonitorAndLocationAsc, -} from '../../../state'; - -import { useSelectedMonitor } from './use_selected_monitor'; -import { useSelectedLocation } from './use_selected_location'; - -export const usePingStatuses = ({ - from, - to, - size, - monitorInterval, - lastRefresh, -}: { - from: number; - to: number; - size: number; - monitorInterval: number; - lastRefresh: number; -}) => { - const { monitor } = useSelectedMonitor(); - const location = useSelectedLocation(); - - const pingStatusesSelector = useCallback(() => { - return selectPingStatusesForMonitorAndLocationAsc( - monitor?.[ConfigKey.CONFIG_ID] ?? '', - location?.label ?? '' - ); - }, [monitor, location?.label]); - const isLoading = useSelector(selectIsMonitorStatusesLoading); - const pingStatuses = useSelector(pingStatusesSelector()) as PingStatus[]; - const dispatch = useDispatch(); - - const lastCall = useRef({ monitorId: '', locationLabel: '', to: 0, from: 0, lastRefresh: 0 }); - const toDiff = Math.abs(lastCall.current.to - to) / (1000 * 60); - const fromDiff = Math.abs(lastCall.current.from - from) / (1000 * 60); - const lastRefreshDiff = Math.abs(lastCall.current.lastRefresh - lastRefresh) / (1000 * 60); - const isDataChangedEnough = - toDiff >= monitorInterval || - fromDiff >= monitorInterval || - lastRefreshDiff >= 3 || // Minimum monitor interval - monitor?.id !== lastCall.current.monitorId || - location?.label !== lastCall.current.locationLabel; - - useEffect(() => { - if (!isLoading && isDataChangedEnough && monitor?.id && location?.label && from && to && size) { - dispatch( - getMonitorPingStatusesAction.get({ - monitorId: monitor.id, - locationId: location.label, - from, - to, - size, - }) - ); - - lastCall.current = { - monitorId: monitor.id, - locationLabel: location?.label, - to, - from, - lastRefresh, - }; - } - // `isLoading` shouldn't be included in deps - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [dispatch, monitor?.id, location?.label, from, to, size, isDataChangedEnough, lastRefresh]); - - return pingStatuses.filter(({ timestamp }) => { - const timestampN = Number(new Date(timestamp)); - return timestampN >= from && timestampN <= to; - }); -}; - -export const usePingStatusesIsLoading = () => { - return useSelector(selectIsMonitorStatusesLoading) as boolean; -}; diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitor_details/monitor_status/monitor_status_data.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitor_details/monitor_status/monitor_status_data.ts index f588ab242adf97..e5ee43aa04f8d7 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitor_details/monitor_status/monitor_status_data.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitor_details/monitor_status/monitor_status_data.ts @@ -16,7 +16,7 @@ import { COLOR_MODES_STANDARD, } from '@elastic/eui'; import type { BrushEvent } from '@elastic/charts'; -import { PingStatus } from '../../../../../../common/runtime_types'; +import { MonitorStatusHeatmapBucket } from '../../../../../../common/runtime_types'; export const SUCCESS_VIZ_COLOR = VISUALIZATION_COLORS[0]; export const DANGER_VIZ_COLOR = VISUALIZATION_COLORS[VISUALIZATION_COLORS.length - 1]; @@ -114,28 +114,26 @@ export function createTimeBuckets(intervalMinutes: number, from: number, to: num export function createStatusTimeBins( timeBuckets: MonitorStatusTimeBucket[], - pingStatuses: PingStatus[] + heatmapData: MonitorStatusHeatmapBucket[] ): MonitorStatusTimeBin[] { - let iPingStatus = 0; - return (timeBuckets ?? []).map((bucket) => { - const currentBin: MonitorStatusTimeBin = { - start: bucket.start, - end: bucket.end, - ups: 0, - downs: 0, - value: 0, + return timeBuckets.map(({ start, end }) => { + const { ups, downs } = heatmapData + .filter(({ key }) => key >= start && key <= end) + .reduce( + (acc, cur) => ({ + ups: acc.ups + cur.up.value, + downs: acc.downs + cur.down.value, + }), + { ups: 0, downs: 0 } + ); + + return { + start, + end, + ups, + downs, + value: ups + downs === 0 ? 0 : getStatusEffectiveValue(ups, downs), }; - while ( - iPingStatus < pingStatuses.length && - moment(pingStatuses[iPingStatus].timestamp).valueOf() < bucket.end - ) { - currentBin.ups += pingStatuses[iPingStatus]?.summary.up ?? 0; - currentBin.downs += pingStatuses[iPingStatus]?.summary.down ?? 0; - currentBin.value = getStatusEffectiveValue(currentBin.ups, currentBin.downs); - iPingStatus++; - } - - return currentBin; }); } diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitor_details/monitor_status/monitor_status_panel.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitor_details/monitor_status/monitor_status_panel.tsx index fc75701098761d..cb8da0ea599d69 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitor_details/monitor_status/monitor_status_panel.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitor_details/monitor_status/monitor_status_panel.tsx @@ -5,12 +5,11 @@ * 2.0. */ -import React, { useMemo } from 'react'; +import React, { useMemo, useRef } from 'react'; -import { EuiPanel, useEuiTheme, EuiResizeObserver, EuiSpacer } from '@elastic/eui'; +import { EuiPanel, useEuiTheme, EuiResizeObserver, EuiSpacer, EuiProgress } from '@elastic/eui'; import { Chart, Settings, Heatmap, ScaleType, Tooltip, LEGACY_LIGHT_THEME } from '@elastic/charts'; import { i18n } from '@kbn/i18n'; -import { usePingStatusesIsLoading } from '../hooks/use_ping_statuses'; import { MonitorStatusHeader } from './monitor_status_header'; import { MonitorStatusCellTooltip } from './monitor_status_cell_tooltip'; import { MonitorStatusLegend } from './monitor_status_legend'; @@ -32,9 +31,9 @@ export const MonitorStatusPanel = ({ onBrushed, }: MonitorStatusPanelProps) => { const { euiTheme, colorMode } = useEuiTheme(); - const { timeBins, handleResize, getTimeBinByXValue, xDomain, intervalByWidth } = - useMonitorStatusData({ from, to }); - const isPingStatusesLoading = usePingStatusesIsLoading(); + const initialSizeRef = useRef(null); + const { loading, timeBins, handleResize, getTimeBinByXValue, xDomain, minsPerBin } = + useMonitorStatusData({ from, to, initialSizeRef }); const heatmap = useMemo(() => { return getMonitorStatusChartTheme(euiTheme, brushable); @@ -53,61 +52,66 @@ export const MonitorStatusPanel = ({ - - {(resizeRef) => ( -
- - ( - + handleResize(e)}> + {(resizeRef) => ( +
+ {minsPerBin && ( + + ( + + )} /> - )} - /> - { - onBrushed?.(getBrushData(brushArea)); - }} - locale={i18n.getLocale()} - /> - timeBin.end} - yAccessor={() => 'T'} - valueAccessor={(timeBin) => timeBin.value} - valueFormatter={(d) => d.toFixed(2)} - xAxisLabelFormatter={getXAxisLabelFormatter(intervalByWidth)} - timeZone="UTC" - xScale={{ - type: ScaleType.Time, - interval: { - type: 'calendar', - unit: 'm', - value: intervalByWidth, - }, - }} - /> - -
- )} -
+ { + onBrushed?.(getBrushData(brushArea)); + }} + locale={i18n.getLocale()} + /> + end} + yAccessor={() => 'T'} + valueAccessor={(timeBin) => timeBin.value} + valueFormatter={(d) => d.toFixed(2)} + xAxisLabelFormatter={getXAxisLabelFormatter(minsPerBin)} + timeZone="UTC" + xScale={{ + type: ScaleType.Time, + interval: { + type: 'calendar', + unit: 'm', + value: minsPerBin, + }, + }} + /> +
+ )} +
+ )} +
+ + {loading && } ); }; diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitor_details/monitor_status/use_monitor_status_data.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitor_details/monitor_status/use_monitor_status_data.ts index 3465c229c65ce1..59d807cb3bef81 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitor_details/monitor_status/use_monitor_status_data.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitor_details/monitor_status/use_monitor_status_data.ts @@ -5,75 +5,134 @@ * 2.0. */ -import { useCallback, useMemo, useState } from 'react'; -import { throttle } from 'lodash'; +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useSelector, useDispatch } from 'react-redux'; +import { useDebounce } from 'react-use'; +import { useLocation } from 'react-router-dom'; -import { scheduleToMinutes } from '../../../../../../common/lib/schedule_to_time'; import { useSyntheticsRefreshContext } from '../../../contexts/synthetics_refresh_context'; import { useSelectedMonitor } from '../hooks/use_selected_monitor'; -import { usePingStatuses } from '../hooks/use_ping_statuses'; import { dateToMilli, createTimeBuckets, - createStatusTimeBins, CHART_CELL_WIDTH, indexBinsByEndTime, MonitorStatusPanelProps, + createStatusTimeBins, + MonitorStatusTimeBin, } from './monitor_status_data'; +import { useSelectedLocation } from '../hooks/use_selected_location'; +import { + clearMonitorStatusHeatmapAction, + quietGetMonitorStatusHeatmapAction, + selectHeatmap, +} from '../../../state/status_heatmap'; + +type Props = Pick & { + initialSizeRef?: React.MutableRefObject; +}; -export const useMonitorStatusData = ({ - from, - to, -}: Pick) => { +export const useMonitorStatusData = ({ from, to, initialSizeRef }: Props) => { const { lastRefresh } = useSyntheticsRefreshContext(); const { monitor } = useSelectedMonitor(); - const monitorInterval = Math.max(3, monitor?.schedule ? scheduleToMinutes(monitor?.schedule) : 3); + const location = useSelectedLocation(); + const pageLocation = useLocation(); const fromMillis = dateToMilli(from); const toMillis = dateToMilli(to); const totalMinutes = Math.ceil(toMillis - fromMillis) / (1000 * 60); - const pingStatuses = usePingStatuses({ - from: fromMillis, - to: toMillis, - size: Math.min(10000, Math.ceil((totalMinutes / monitorInterval) * 2)), // Acts as max size between from - to - monitorInterval, + + const [binsAvailableByWidth, setBinsAvailableByWidth] = useState(null); + const [debouncedBinsCount, setDebouncedCount] = useState(null); + + const minsPerBin = + debouncedBinsCount !== null ? Math.floor(totalMinutes / debouncedBinsCount) : null; + + const dispatch = useDispatch(); + const { heatmap: dateHistogram, loading } = useSelector(selectHeatmap); + + useEffect(() => { + if (binsAvailableByWidth === null && initialSizeRef?.current) { + setBinsAvailableByWidth(Math.floor(initialSizeRef?.current?.clientWidth / CHART_CELL_WIDTH)); + } + }, [binsAvailableByWidth, initialSizeRef]); + + useEffect(() => { + if (monitor?.id && location?.label && debouncedBinsCount !== null && minsPerBin !== null) { + dispatch( + quietGetMonitorStatusHeatmapAction.get({ + monitorId: monitor.id, + location: location.label, + from, + to, + interval: minsPerBin, + }) + ); + } + }, [ + dispatch, + from, + to, + minsPerBin, + location?.label, + monitor?.id, lastRefresh, - }); + debouncedBinsCount, + ]); - const [binsAvailableByWidth, setBinsAvailableByWidth] = useState(50); - const intervalByWidth = Math.floor( - Math.max(monitorInterval, totalMinutes / binsAvailableByWidth) - ); + useEffect(() => { + dispatch(clearMonitorStatusHeatmapAction()); + }, [dispatch, pageLocation.pathname]); - // Disabling deps warning as we wanna throttle the callback - // eslint-disable-next-line react-hooks/exhaustive-deps const handleResize = useCallback( - throttle((e: { width: number; height: number }) => { - setBinsAvailableByWidth(Math.floor(e.width / CHART_CELL_WIDTH)); - }, 500), + (e: { width: number; height: number }) => + setBinsAvailableByWidth(Math.floor(e.width / CHART_CELL_WIDTH)), [] ); - const { timeBins, timeBinsByEndTime, xDomain } = useMemo(() => { - const timeBuckets = createTimeBuckets(intervalByWidth, fromMillis, toMillis); - const bins = createStatusTimeBins(timeBuckets, pingStatuses); - const indexedBins = indexBinsByEndTime(bins); + useDebounce( + async () => { + setDebouncedCount(binsAvailableByWidth); + }, + 500, + [binsAvailableByWidth] + ); - const timeDomain = { - min: bins?.[0]?.end ?? fromMillis, - max: bins?.[bins.length - 1]?.end ?? toMillis, + const { timeBins, timeBinMap, xDomain } = useMemo((): { + timeBins: MonitorStatusTimeBin[]; + timeBinMap: Map; + xDomain: { min: number; max: number }; + } => { + if (minsPerBin === null) { + return { + timeBins: [], + timeBinMap: new Map(), + xDomain: { + min: fromMillis, + max: toMillis, + }, + }; + } + const timeBuckets = createTimeBuckets(minsPerBin ?? 50, fromMillis, toMillis); + const bins = createStatusTimeBins(timeBuckets, dateHistogram); + return { + timeBins: bins, + timeBinMap: indexBinsByEndTime(bins), + xDomain: { + min: bins?.[0]?.end ?? fromMillis, + max: bins?.at(-1)?.end ?? toMillis, + }, }; - - return { timeBins: bins, timeBinsByEndTime: indexedBins, xDomain: timeDomain }; - }, [intervalByWidth, pingStatuses, fromMillis, toMillis]); + }, [minsPerBin, fromMillis, toMillis, dateHistogram]); return { - intervalByWidth, + loading, + minsPerBin, timeBins, + getTimeBinByXValue: (xValue: number | undefined) => + xValue === undefined ? undefined : timeBinMap.get(xValue), xDomain, handleResize, - getTimeBinByXValue: (xValue: number | undefined) => - xValue === undefined ? undefined : timeBinsByEndTime.get(xValue), }; }; diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/index.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/index.ts index ef319d13740b2f..079a68d4444a81 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/index.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/index.ts @@ -17,7 +17,6 @@ export * from './monitor_list'; export * from './monitor_details'; export * from './overview'; export * from './browser_journey'; -export * from './ping_status'; export * from './private_locations'; export type { UpsertMonitorResponse } from './monitor_management/api'; diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/ping_status/actions.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/ping_status/actions.ts deleted file mode 100644 index f268a5a2c5600e..00000000000000 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/ping_status/actions.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { PingStatusesResponse } from '../../../../../common/runtime_types'; -import { createAsyncAction } from '../utils/actions'; - -import { PingStatusActionArgs } from './models'; - -export const getMonitorPingStatusesAction = createAsyncAction< - PingStatusActionArgs, - PingStatusesResponse ->('[PING STATUSES] GET PING STATUSES'); diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/ping_status/api.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/ping_status/api.ts deleted file mode 100644 index 38930dfb02cb8a..00000000000000 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/ping_status/api.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { SYNTHETICS_API_URLS } from '../../../../../common/constants'; -import { - PingStatusesResponse, - PingStatusesResponseType, -} from '../../../../../common/runtime_types'; -import { apiService } from '../../../../utils/api_service'; - -export const fetchMonitorPingStatuses = async ({ - monitorId, - locationId, - from, - to, - size, -}: { - monitorId: string; - locationId: string; - from: string; - to: string; - size: number; -}): Promise => { - const locations = JSON.stringify([locationId]); - const sort = 'desc'; - - return await apiService.get( - SYNTHETICS_API_URLS.PING_STATUSES, - { monitorId, from, to, locations, sort, size }, - PingStatusesResponseType - ); -}; diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/ping_status/effects.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/ping_status/effects.ts deleted file mode 100644 index cffae2fb859f5e..00000000000000 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/ping_status/effects.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { takeEvery } from 'redux-saga/effects'; -import { fetchEffectFactory } from '../utils/fetch_effect'; -import { fetchMonitorPingStatuses } from './api'; - -import { getMonitorPingStatusesAction } from './actions'; - -export function* fetchPingStatusesEffect() { - yield takeEvery( - getMonitorPingStatusesAction.get, - fetchEffectFactory( - fetchMonitorPingStatuses, - getMonitorPingStatusesAction.success, - getMonitorPingStatusesAction.fail - ) as ReturnType - ); -} diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/ping_status/index.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/ping_status/index.ts deleted file mode 100644 index 350db7cb41177f..00000000000000 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/ping_status/index.ts +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { createReducer } from '@reduxjs/toolkit'; - -import { PingStatus } from '../../../../../common/runtime_types'; - -import { IHttpSerializedFetchError } from '../utils/http_error'; - -import { getMonitorPingStatusesAction } from './actions'; - -export interface PingStatusState { - pingStatuses: { - [monitorId: string]: { - [locationId: string]: { - [timestamp: string]: PingStatus; - }; - }; - }; - loading: boolean; - error: IHttpSerializedFetchError | null; -} - -const initialState: PingStatusState = { - pingStatuses: {}, - loading: false, - error: null, -}; - -export const pingStatusReducer = createReducer(initialState, (builder) => { - builder - .addCase(getMonitorPingStatusesAction.get, (state) => { - state.loading = true; - }) - .addCase(getMonitorPingStatusesAction.success, (state, action) => { - (action.payload.pings ?? []).forEach((ping) => { - // eslint-disable-next-line @typescript-eslint/naming-convention - const { config_id, locationId, timestamp } = ping; - if (!state.pingStatuses[config_id]) { - state.pingStatuses[config_id] = {}; - } - - if (!state.pingStatuses[config_id][locationId]) { - state.pingStatuses[config_id][locationId] = {}; - } - - state.pingStatuses[config_id][locationId][timestamp] = ping; - }); - - state.loading = false; - }) - .addCase(getMonitorPingStatusesAction.fail, (state, action) => { - state.error = action.payload; - state.loading = false; - }); -}); - -export * from './actions'; -export * from './effects'; -export * from './selectors'; diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/ping_status/selectors.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/ping_status/selectors.ts deleted file mode 100644 index cf3061e0fab33f..00000000000000 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/ping_status/selectors.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { createSelector } from 'reselect'; - -import { PingStatus } from '../../../../../common/runtime_types'; -import { SyntheticsAppState } from '../root_reducer'; - -import { PingStatusState } from '.'; - -type PingSelectorReturnType = (state: SyntheticsAppState) => PingStatus[]; - -const getState = (appState: SyntheticsAppState) => appState.pingStatus; - -export const selectIsMonitorStatusesLoading = createSelector(getState, (state) => state.loading); - -export const selectPingStatusesForMonitorAndLocationAsc = ( - monitorId: string, - locationId: string -): PingSelectorReturnType => - createSelector([(state: SyntheticsAppState) => state.pingStatus], (state: PingStatusState) => { - return Object.values(state?.pingStatuses?.[monitorId]?.[locationId] ?? {}).sort( - (a, b) => Number(new Date(a.timestamp)) - Number(new Date(b.timestamp)) - ); - }); diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/root_effect.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/root_effect.ts index 62d671d8e98fd7..1a565fe772aa67 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/root_effect.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/root_effect.ts @@ -33,11 +33,12 @@ import { upsertMonitorEffect, fetchMonitorFiltersEffect, } from './monitor_list'; -import { fetchMonitorOverviewEffect } from './overview'; + import { fetchServiceLocationsEffect } from './service_locations'; import { browserJourneyEffects, fetchJourneyStepsEffect } from './browser_journey'; -import { fetchPingStatusesEffect } from './ping_status'; import { fetchOverviewStatusEffect } from './overview_status'; +import { fetchMonitorStatusHeatmap, quietFetchMonitorStatusHeatmap } from './status_heatmap'; +import { fetchOverviewTrendStats, refreshOverviewTrendStats } from './overview/effects'; export const rootEffect = function* root(): Generator { yield all([ @@ -47,11 +48,9 @@ export const rootEffect = function* root(): Generator { fork(fetchServiceLocationsEffect), fork(fetchMonitorListEffect), fork(fetchSyntheticsMonitorEffect), - fork(fetchMonitorOverviewEffect), fork(browserJourneyEffects), fork(fetchOverviewStatusEffect), fork(fetchNetworkEventsEffect), - fork(fetchPingStatusesEffect), fork(fetchAgentPoliciesEffect), fork(fetchPrivateLocationsEffect), fork(fetchDynamicSettingsEffect), @@ -71,5 +70,9 @@ export const rootEffect = function* root(): Generator { fork(getCertsListEffect), fork(getDefaultAlertingEffect), fork(enableDefaultAlertingSilentlyEffect), + fork(fetchMonitorStatusHeatmap), + fork(quietFetchMonitorStatusHeatmap), + fork(fetchOverviewTrendStats), + fork(refreshOverviewTrendStats), ]); }; diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/root_reducer.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/root_reducer.ts index a17749de498ff4..f8ace41e93191f 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/root_reducer.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/root_reducer.ts @@ -30,12 +30,11 @@ import { monitorListReducer, MonitorListState } from './monitor_list'; import { serviceLocationsReducer, ServiceLocationsState } from './service_locations'; import { monitorOverviewReducer, MonitorOverviewState } from './overview'; import { BrowserJourneyState } from './browser_journey/models'; -import { pingStatusReducer, PingStatusState } from './ping_status'; +import { monitorStatusHeatmapReducer, MonitorStatusHeatmap } from './status_heatmap'; export interface SyntheticsAppState { ui: UiState; settings: SettingsState; - pingStatus: PingStatusState; elasticsearch: QueriesState; monitorList: MonitorListState; overview: MonitorOverviewState; @@ -52,12 +51,12 @@ export interface SyntheticsAppState { serviceLocations: ServiceLocationsState; overviewStatus: OverviewStatusStateReducer; syntheticsEnablement: SyntheticsEnablementState; + monitorStatusHeatmap: MonitorStatusHeatmap; } export const rootReducer = combineReducers({ ui: uiReducer, settings: settingsReducer, - pingStatus: pingStatusReducer, monitorList: monitorListReducer, overview: monitorOverviewReducer, globalParams: globalParamsReducer, @@ -74,4 +73,5 @@ export const rootReducer = combineReducers({ syntheticsEnablement: syntheticsEnablementReducer, certificates: certificatesReducer, certsList: certsListReducer, + monitorStatusHeatmap: monitorStatusHeatmapReducer, }); diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/status_heatmap/actions.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/status_heatmap/actions.ts new file mode 100644 index 00000000000000..f56fe727e7cbb9 --- /dev/null +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/status_heatmap/actions.ts @@ -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 { createAction } from '@reduxjs/toolkit'; +import { MonitorStatusHeatmapBucket } from '../../../../../common/runtime_types'; +import { createAsyncAction } from '../utils/actions'; + +import { MonitorStatusHeatmapActionArgs } from './models'; + +export const getMonitorStatusHeatmapAction = createAsyncAction< + MonitorStatusHeatmapActionArgs, + MonitorStatusHeatmapBucket[] +>('MONITOR STATUS HEATMAP'); + +export const clearMonitorStatusHeatmapAction = createAction('CLEAR MONITOR STATUS HEATMAP'); + +export const quietGetMonitorStatusHeatmapAction = createAsyncAction< + MonitorStatusHeatmapActionArgs, + MonitorStatusHeatmapBucket[] +>('QUIET GET MONITOR STATUS HEATMAP'); diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/status_heatmap/api.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/status_heatmap/api.ts new file mode 100644 index 00000000000000..482e7d3c159394 --- /dev/null +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/status_heatmap/api.ts @@ -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 { SYNTHETICS_API_URLS } from '../../../../../common/constants'; +import { MonitorStatusHeatmapBucket } from '../../../../../common/runtime_types'; +import { apiService } from '../../../../utils/api_service'; + +export const fetchMonitorStatusHeatmap = async ({ + monitorId, + location, + from, + to, + interval, +}: { + monitorId: string; + location: string; + from: string | number; + to: string | number; + interval: number; +}): Promise => { + const response = await apiService.get<{ + result: MonitorStatusHeatmapBucket[]; + }>(SYNTHETICS_API_URLS.MONITOR_STATUS_HEATMAP, { + monitorId, + location, + from, + to, + interval, + }); + return response.result; +}; diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/status_heatmap/effects.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/status_heatmap/effects.ts new file mode 100644 index 00000000000000..beffc42367e9ad --- /dev/null +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/status_heatmap/effects.ts @@ -0,0 +1,34 @@ +/* + * 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 { takeLatest } from 'redux-saga/effects'; +import { fetchEffectFactory } from '../utils/fetch_effect'; +import { fetchMonitorStatusHeatmap as api } from './api'; + +import { getMonitorStatusHeatmapAction, quietGetMonitorStatusHeatmapAction } from './actions'; + +export function* fetchMonitorStatusHeatmap() { + yield takeLatest( + getMonitorStatusHeatmapAction.get, + fetchEffectFactory( + api, + getMonitorStatusHeatmapAction.success, + getMonitorStatusHeatmapAction.fail + ) as ReturnType + ); +} + +export function* quietFetchMonitorStatusHeatmap() { + yield takeLatest( + quietGetMonitorStatusHeatmapAction.get, + fetchEffectFactory( + api, + getMonitorStatusHeatmapAction.success, + getMonitorStatusHeatmapAction.fail + ) as ReturnType + ); +} diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/status_heatmap/index.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/status_heatmap/index.ts new file mode 100644 index 00000000000000..29f8a1ba873459 --- /dev/null +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/status_heatmap/index.ts @@ -0,0 +1,60 @@ +/* + * 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 { createReducer } from '@reduxjs/toolkit'; + +import { MonitorStatusHeatmapBucket } from '../../../../../common/runtime_types'; + +import { IHttpSerializedFetchError } from '../utils/http_error'; + +import { + clearMonitorStatusHeatmapAction, + getMonitorStatusHeatmapAction, + quietGetMonitorStatusHeatmapAction, +} from './actions'; + +export interface MonitorStatusHeatmap { + heatmap: MonitorStatusHeatmapBucket[]; + loading: boolean; + error: IHttpSerializedFetchError | null; +} + +const initialState: MonitorStatusHeatmap = { + heatmap: [], + loading: false, + error: null, +}; + +export const monitorStatusHeatmapReducer = createReducer(initialState, (builder) => { + builder + .addCase(quietGetMonitorStatusHeatmapAction.success, (state, action) => { + state.heatmap = action.payload; + state.loading = false; + }) + .addCase(quietGetMonitorStatusHeatmapAction.get, (state) => { + state.loading = true; + }) + .addCase(getMonitorStatusHeatmapAction.get, (state) => { + state.loading = true; + state.heatmap = []; + }) + .addCase(getMonitorStatusHeatmapAction.success, (state, action) => { + state.heatmap = action.payload; + state.loading = false; + }) + .addCase(getMonitorStatusHeatmapAction.fail, (state, action) => { + state.error = action.payload; + state.loading = false; + }) + .addCase(clearMonitorStatusHeatmapAction, (state) => { + state.heatmap = []; + }); +}); + +export * from './actions'; +export * from './effects'; +export * from './selectors'; diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/ping_status/models.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/status_heatmap/models.ts similarity index 78% rename from x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/ping_status/models.ts rename to x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/status_heatmap/models.ts index bae8ae9acb3fa8..fbf5812306511e 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/ping_status/models.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/status_heatmap/models.ts @@ -5,10 +5,10 @@ * 2.0. */ -export interface PingStatusActionArgs { - monitorId: string; - locationId: string; +export interface MonitorStatusHeatmapActionArgs { from: string | number; to: string | number; - size: number; + interval: number; + monitorId: string; + location: string; } diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/status_heatmap/selectors.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/status_heatmap/selectors.ts new file mode 100644 index 00000000000000..dc28e8473d12d6 --- /dev/null +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/status_heatmap/selectors.ts @@ -0,0 +1,14 @@ +/* + * 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 { createSelector } from 'reselect'; + +import { SyntheticsAppState } from '../root_reducer'; + +const getState = (appState: SyntheticsAppState) => appState.monitorStatusHeatmap; + +export const selectHeatmap = createSelector(getState, (state) => state); diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/utils/testing/__mocks__/synthetics_store.mock.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/utils/testing/__mocks__/synthetics_store.mock.ts index abfc08919b33ad..6a086abccc5e9a 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/utils/testing/__mocks__/synthetics_store.mock.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/utils/testing/__mocks__/synthetics_store.mock.ts @@ -118,7 +118,6 @@ export const mockState: SyntheticsAppState = { monitorDetails: getMonitorDetailsMockSlice(), browserJourney: getBrowserJourneyMockSlice(), networkEvents: {}, - pingStatus: getPingStatusesMockSlice(), agentPolicies: { loading: false, error: null, @@ -164,6 +163,11 @@ export const mockState: SyntheticsAppState = { certs: [], }, }, + monitorStatusHeatmap: { + heatmap: [], + loading: false, + error: null, + }, }; function getBrowserJourneyMockSlice() { @@ -490,33 +494,3 @@ function getMonitorDetailsMockSlice() { selectedLocationId: 'us_central', } as MonitorDetailsState; } - -function getPingStatusesMockSlice() { - const monitorDetails = getMonitorDetailsMockSlice(); - - return { - pingStatuses: monitorDetails.pings.data.reduce((acc, cur) => { - const geoName = cur.observer.geo?.name!; - if (!acc[cur.monitor.id]) { - acc[cur.monitor.id] = {}; - } - - if (!acc[cur.monitor.id][geoName]) { - acc[cur.monitor.id][geoName] = {}; - } - - acc[cur.monitor.id][geoName][cur.timestamp] = { - timestamp: cur.timestamp, - error: undefined, - locationId: geoName, - config_id: cur.config_id!, - docId: cur.docId, - summary: cur.summary!, - }; - - return acc; - }, {} as SyntheticsAppState['pingStatus']['pingStatuses']), - loading: false, - error: null, - } as SyntheticsAppState['pingStatus']; -} diff --git a/x-pack/plugins/observability_solution/synthetics/server/common/pings/monitor_status_heatmap.ts b/x-pack/plugins/observability_solution/synthetics/server/common/pings/monitor_status_heatmap.ts new file mode 100644 index 00000000000000..cc6015463e9a50 --- /dev/null +++ b/x-pack/plugins/observability_solution/synthetics/server/common/pings/monitor_status_heatmap.ts @@ -0,0 +1,88 @@ +/* + * 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 { SyntheticsEsClient } from '../../lib'; + +export async function queryMonitorHeatmap({ + syntheticsEsClient, + from, + to, + monitorId, + location, + intervalInMinutes, +}: { + syntheticsEsClient: SyntheticsEsClient; + from: number | string; + to: number | string; + monitorId: string; + location: string; + intervalInMinutes: number; +}) { + return syntheticsEsClient.search({ + body: { + size: 0, + query: { + bool: { + filter: [ + { + exists: { + field: 'summary', + }, + }, + { + range: { + '@timestamp': { + gte: from, + lte: to, + }, + }, + }, + { + term: { + 'monitor.id': monitorId, + }, + }, + { + term: { + 'observer.geo.name': location, + }, + }, + ], + }, + }, + sort: [ + { + '@timestamp': { + order: 'desc', + }, + }, + ], + _source: false, + fields: ['@timestamp', 'config_id', 'summary.*', 'error.*', 'observer.geo.name'], + aggs: { + heatmap: { + date_histogram: { + field: '@timestamp', + fixed_interval: `${intervalInMinutes}m`, + }, + aggs: { + up: { + sum: { + field: 'summary.up', + }, + }, + down: { + sum: { + field: 'summary.down', + }, + }, + }, + }, + }, + }, + }); +} diff --git a/x-pack/plugins/observability_solution/synthetics/server/routes/index.ts b/x-pack/plugins/observability_solution/synthetics/server/routes/index.ts index c97abe44a6c5a6..7316c1e515f35a 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/routes/index.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/routes/index.ts @@ -46,7 +46,7 @@ import { installIndexTemplatesRoute } from './synthetics_service/install_index_t import { editSyntheticsMonitorRoute } from './monitor_cruds/edit_monitor'; import { addSyntheticsMonitorRoute } from './monitor_cruds/add_monitor'; import { addSyntheticsProjectMonitorRoute } from './monitor_cruds/add_monitor_project'; -import { syntheticsGetPingsRoute, syntheticsGetPingStatusesRoute } from './pings'; +import { syntheticsGetPingsRoute, syntheticsGetPingHeatmapRoute } from './pings'; import { createGetCurrentStatusRoute } from './overview_status/overview_status'; import { getHasIntegrationMonitorsRoute } from './fleet/get_has_integration_monitors'; import { enableDefaultAlertingRoute } from './default_alerts/enable_default_alert'; @@ -76,7 +76,6 @@ export const syntheticsAppRestApiRoutes: SyntheticsRestApiRouteFactory[] = [ getServiceAllowedRoute, getAPIKeySyntheticsRoute, syntheticsGetPingsRoute, - syntheticsGetPingStatusesRoute, getHasIntegrationMonitorsRoute, createGetCurrentStatusRoute, getIndexSizesRoute, @@ -101,6 +100,8 @@ export const syntheticsAppRestApiRoutes: SyntheticsRestApiRouteFactory[] = [ getConnectorTypesRoute, createGetDynamicSettingsRoute, createPostDynamicSettingsRoute, + syntheticsGetPingHeatmapRoute, + createOverviewTrendsRoute, ]; export const syntheticsAppPublicRestApiRoutes: SyntheticsRestApiRouteFactory[] = [ diff --git a/x-pack/plugins/observability_solution/synthetics/server/routes/pings/get_ping_statuses.ts b/x-pack/plugins/observability_solution/synthetics/server/routes/pings/get_ping_statuses.ts deleted file mode 100644 index 2d1fa867430f70..00000000000000 --- a/x-pack/plugins/observability_solution/synthetics/server/routes/pings/get_ping_statuses.ts +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { SyntheticsRestApiRouteFactory } from '../types'; -import { SYNTHETICS_API_URLS } from '../../../common/constants'; -import { PingError, PingStatus } from '../../../common/runtime_types'; -import { queryPings } from '../../common/pings/query_pings'; - -import { getPingsRouteQuerySchema } from './get_pings'; - -export const syntheticsGetPingStatusesRoute: SyntheticsRestApiRouteFactory = () => ({ - method: 'GET', - path: SYNTHETICS_API_URLS.PING_STATUSES, - validate: { - query: getPingsRouteQuerySchema, - }, - handler: async ({ uptimeEsClient, request, response }): Promise => { - const { - from, - to, - index, - monitorId, - status, - sort, - size, - pageIndex, - locations, - excludedLocations, - } = request.query; - - const result = await queryPings({ - uptimeEsClient, - dateRange: { from, to }, - index, - monitorId, - status, - sort, - size, - pageIndex, - locations: locations ? JSON.parse(locations) : [], - excludedLocations, - fields: ['@timestamp', 'config_id', 'summary.*', 'error.*', 'observer.geo.name'], - fieldsExtractorFn: extractPingStatus, - }); - - return { - ...result, - from, - to, - }; - }, -}); - -function grabPingError(doc: any): PingError | undefined { - const docContainsError = Object.keys(doc?.fields ?? {}).some((key) => key.startsWith('error.')); - if (!docContainsError) { - return undefined; - } - - return { - code: doc.fields['error.code']?.[0], - id: doc.fields['error.id']?.[0], - stack_trace: doc.fields['error.stack_trace']?.[0], - type: doc.fields['error.type']?.[0], - message: doc.fields['error.message']?.[0], - }; -} - -function extractPingStatus(doc: any) { - return { - timestamp: doc.fields['@timestamp']?.[0], - docId: doc._id, - config_id: doc.fields.config_id?.[0], - locationId: doc.fields['observer.geo.name']?.[0], - summary: { up: doc.fields['summary.up']?.[0], down: doc.fields['summary.down']?.[0] }, - error: grabPingError(doc), - } as PingStatus; -} diff --git a/x-pack/plugins/observability_solution/synthetics/server/routes/pings/index.ts b/x-pack/plugins/observability_solution/synthetics/server/routes/pings/index.ts index 89fa3194d4dc2d..b18a1dcc3a7b8f 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/routes/pings/index.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/routes/pings/index.ts @@ -6,4 +6,4 @@ */ export { syntheticsGetPingsRoute } from './get_pings'; -export { syntheticsGetPingStatusesRoute } from './get_ping_statuses'; +export { syntheticsGetPingHeatmapRoute } from './ping_heatmap'; diff --git a/x-pack/plugins/observability_solution/synthetics/server/routes/pings/ping_heatmap.ts b/x-pack/plugins/observability_solution/synthetics/server/routes/pings/ping_heatmap.ts new file mode 100644 index 00000000000000..a7eb44967c81c7 --- /dev/null +++ b/x-pack/plugins/observability_solution/synthetics/server/routes/pings/ping_heatmap.ts @@ -0,0 +1,43 @@ +/* + * 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 { schema } from '@kbn/config-schema'; +import { MonitorStatusHeatmapBucket } from '../../../common/runtime_types'; +import { SYNTHETICS_API_URLS } from '../../../common/constants'; +import { queryMonitorHeatmap } from '../../common/pings/monitor_status_heatmap'; +import { SyntheticsRestApiRouteFactory } from '../types'; + +export const syntheticsGetPingHeatmapRoute: SyntheticsRestApiRouteFactory = () => ({ + method: 'GET', + path: SYNTHETICS_API_URLS.MONITOR_STATUS_HEATMAP, + validate: { + query: schema.object({ + from: schema.maybe(schema.oneOf([schema.number(), schema.string()])), + to: schema.maybe(schema.oneOf([schema.number(), schema.string()])), + interval: schema.number(), + monitorId: schema.string(), + location: schema.string(), + }), + }, + handler: async ({ + syntheticsEsClient, + request, + }): Promise => { + const { from, to, interval: intervalInMinutes, monitorId, location } = request.query; + + const result = await queryMonitorHeatmap({ + syntheticsEsClient, + from, + to, + monitorId, + location, + intervalInMinutes, + }); + + return result.body.aggregations?.heatmap?.buckets as MonitorStatusHeatmapBucket[]; + }, +});