mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Synthetics] ensure metric color and current status are synced (#145031)
## Summary Relates to https://github.com/elastic/kibana/issues/146001 This PR ensures that the status of monitors is in sync with the current status metric by deriving the status of each individual monitor card based on the current status api. Updates the current status api to [return objects representing all the down monitors and all the up monitors](https://github.com/elastic/kibana/pull/145031/files#diff-db953498cb683d4f81a105a8c53f80ef08aea4bb687e6a29f651dd4e148bcc55R122), with the keys being the combination of the monitor config id and location name. These two objects are [combined into one object in redux](https://github.com/elastic/kibana/pull/145031/files#diff-2fb9b43772c61e2706a828651abd651a2a03b33b9f7b59c91d91c3a245edf655R77). Creates a new `useStatusByLocationOverview` [hook](https://github.com/elastic/kibana/pull/145031/files#diff-b80b952c8c3e15be0472d8b63a23f6ca0798dfbfdbce6a1ed056d7f954ab200eR11) that consumes this object. [Uses that hook](https://github.com/elastic/kibana/pull/145031/files#diff-d65b7f3fa061b4f65d63f72296d29f603e10757e595dd688a34fa99deeac3250R56) in Overview Metric item to derive the status. ### Testing 1. Create a handful of monitors, ensuring you have at least 1 ui monitor and 1 project monitor 2. Ensure your cluster is connected to the synthetics service, either by running the service locally or connecting to dev environment 3. Navigate to overview. Ensure the number of down monitor cards and number of up monitor cards matches the current status metric 4. Use the sort by status feature to test for regression. That logic was lightly touched in this PR
This commit is contained in:
parent
d0039eadf6
commit
578d643032
12 changed files with 153 additions and 81 deletions
|
@ -8,19 +8,28 @@
|
|||
import * as t from 'io-ts';
|
||||
|
||||
export const OverviewStatusMetaDataCodec = t.interface({
|
||||
heartbeatId: t.string,
|
||||
monitorQueryId: t.string,
|
||||
configId: t.string,
|
||||
location: t.string,
|
||||
status: t.string,
|
||||
});
|
||||
|
||||
export const OverviewStatusType = t.type({
|
||||
export const OverviewStatusCodec = t.interface({
|
||||
up: t.number,
|
||||
down: t.number,
|
||||
disabledCount: t.number,
|
||||
upConfigs: t.array(OverviewStatusMetaDataCodec),
|
||||
downConfigs: t.array(OverviewStatusMetaDataCodec),
|
||||
upConfigs: t.record(t.string, OverviewStatusMetaDataCodec),
|
||||
downConfigs: t.record(t.string, OverviewStatusMetaDataCodec),
|
||||
enabledIds: t.array(t.string),
|
||||
});
|
||||
|
||||
export type OverviewStatus = t.TypeOf<typeof OverviewStatusType>;
|
||||
export const OverviewStatusStateCodec = t.intersection([
|
||||
OverviewStatusCodec,
|
||||
t.interface({
|
||||
allConfigs: t.record(t.string, OverviewStatusMetaDataCodec),
|
||||
}),
|
||||
]);
|
||||
|
||||
export type OverviewStatus = t.TypeOf<typeof OverviewStatusCodec>;
|
||||
export type OverviewStatusState = t.TypeOf<typeof OverviewStatusStateCodec>;
|
||||
export type OverviewStatusMetaData = t.TypeOf<typeof OverviewStatusMetaDataCodec>;
|
||||
|
|
|
@ -10,19 +10,30 @@ import { Chart, Settings, Metric, MetricTrendShape } from '@elastic/charts';
|
|||
import { EuiPanel } from '@elastic/eui';
|
||||
import { DARK_THEME } from '@elastic/charts';
|
||||
import { useTheme } from '@kbn/observability-plugin/public';
|
||||
import { useLocationName, useStatusByLocation } from '../../../../hooks';
|
||||
import { useLocationName, useStatusByLocationOverview } from '../../../../hooks';
|
||||
import { formatDuration } from '../../../../utils/formatting';
|
||||
import { MonitorOverviewItem, Ping } from '../../../../../../../common/runtime_types';
|
||||
import { MonitorOverviewItem } from '../../../../../../../common/runtime_types';
|
||||
import { ActionsPopover } from './actions_popover';
|
||||
import { OverviewGridItemLoader } from './overview_grid_item_loader';
|
||||
|
||||
export const getColor = (theme: ReturnType<typeof useTheme>, isEnabled: boolean, ping?: Ping) => {
|
||||
export const getColor = (
|
||||
theme: ReturnType<typeof useTheme>,
|
||||
isEnabled: boolean,
|
||||
status?: string
|
||||
) => {
|
||||
if (!isEnabled) {
|
||||
return theme.eui.euiColorLightestShade;
|
||||
}
|
||||
return (ping?.summary?.down || 0) > 0
|
||||
? theme.eui.euiColorVis9_behindText
|
||||
: theme.eui.euiColorVis0_behindText;
|
||||
switch (status) {
|
||||
case 'down':
|
||||
return theme.eui.euiColorVis9_behindText;
|
||||
case 'up':
|
||||
return theme.eui.euiColorVis0_behindText;
|
||||
case 'unknown':
|
||||
return theme.eui.euiColorGhost;
|
||||
default:
|
||||
return theme.eui.euiColorVis0_behindText;
|
||||
}
|
||||
};
|
||||
|
||||
export const MetricItem = ({
|
||||
|
@ -41,8 +52,7 @@ export const MetricItem = ({
|
|||
const [isMouseOver, setIsMouseOver] = useState(false);
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
||||
const locationName = useLocationName({ locationId: monitor.location?.id });
|
||||
const { locations } = useStatusByLocation(monitor.configId);
|
||||
const ping = locations.find((loc) => loc.observer?.geo?.name === locationName);
|
||||
const status = useStatusByLocationOverview(monitor.configId, locationName);
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
|
@ -92,7 +102,7 @@ export const MetricItem = ({
|
|||
</span>
|
||||
),
|
||||
valueFormatter: (d: number) => formatDuration(d),
|
||||
color: getColor(theme, monitor.isEnabled, ping),
|
||||
color: getColor(theme, monitor.isEnabled, status),
|
||||
},
|
||||
],
|
||||
]}
|
||||
|
|
|
@ -13,6 +13,10 @@ import { OverviewGrid } from './overview_grid';
|
|||
import * as hooks from '../../../../hooks/use_last_50_duration_chart';
|
||||
|
||||
describe('Overview Grid', () => {
|
||||
const locationIdToName: Record<string, string> = {
|
||||
us_central: 'Us Central',
|
||||
us_east: 'US East',
|
||||
};
|
||||
const getMockData = (): MonitorOverviewItem[] => {
|
||||
const data: MonitorOverviewItem[] = [];
|
||||
for (let i = 0; i < 20; i++) {
|
||||
|
@ -72,8 +76,17 @@ describe('Overview Grid', () => {
|
|||
loaded: true,
|
||||
loading: false,
|
||||
status: {
|
||||
downConfigs: [],
|
||||
upConfigs: [],
|
||||
downConfigs: {},
|
||||
upConfigs: {},
|
||||
allConfigs: getMockData().reduce((acc, cur) => {
|
||||
acc[`${cur.id}-${locationIdToName[cur.location.id]}`] = {
|
||||
configId: cur.configId,
|
||||
monitorQueryId: cur.id,
|
||||
location: locationIdToName[cur.location.id],
|
||||
status: 'down',
|
||||
};
|
||||
return acc;
|
||||
}, {} as Record<string, any>),
|
||||
},
|
||||
},
|
||||
serviceLocations: {
|
||||
|
@ -121,8 +134,17 @@ describe('Overview Grid', () => {
|
|||
loaded: true,
|
||||
loading: false,
|
||||
status: {
|
||||
downConfigs: [],
|
||||
upConfigs: [],
|
||||
downConfigs: {},
|
||||
upConfigs: {},
|
||||
allConfigs: getMockData().reduce((acc, cur) => {
|
||||
acc[`${cur.id}-${locationIdToName[cur.location.id]}`] = {
|
||||
configId: cur.configId,
|
||||
monitorQueryId: cur.id,
|
||||
location: locationIdToName[cur.location.id],
|
||||
status: 'down',
|
||||
};
|
||||
return acc;
|
||||
}, {} as Record<string, any>),
|
||||
},
|
||||
},
|
||||
serviceLocations: {
|
||||
|
|
|
@ -15,5 +15,6 @@ export * from './use_last_x_checks';
|
|||
export * from './use_last_50_duration_chart';
|
||||
export * from './use_location_name';
|
||||
export * from './use_status_by_location';
|
||||
export * from './use_status_by_location_overview';
|
||||
export * from './use_composite_image';
|
||||
export * from './use_dimensions';
|
||||
|
|
|
@ -56,40 +56,40 @@ describe('useMonitorsSortedByStatus', () => {
|
|||
sortField: 'name.keyword',
|
||||
},
|
||||
status: {
|
||||
upConfigs: [
|
||||
{
|
||||
upConfigs: {
|
||||
[`test-monitor-1-${location2.label}`]: {
|
||||
configId: 'test-monitor-1',
|
||||
heartbeatId: 'test-monitor-1',
|
||||
monitorQueryId: 'test-monitor-1',
|
||||
location: location2.label,
|
||||
},
|
||||
{
|
||||
[`test-monitor-2-${location2.label}`]: {
|
||||
configId: 'test-monitor-2',
|
||||
heartbeatId: 'test-monitor-2',
|
||||
monitorQueryId: 'test-monitor-2',
|
||||
location: location2.label,
|
||||
},
|
||||
{
|
||||
[`test-monitor-3-${location2.label}`]: {
|
||||
configId: 'test-monitor-3',
|
||||
heartbeatId: 'test-monitor-3',
|
||||
monitorQueryId: 'test-monitor-3',
|
||||
location: location2.label,
|
||||
},
|
||||
],
|
||||
downConfigs: [
|
||||
{
|
||||
},
|
||||
downConfigs: {
|
||||
[`test-monitor-1-${location1.label}`]: {
|
||||
configId: 'test-monitor-1',
|
||||
heartbeatId: 'test-monitor-1',
|
||||
monitorQueryId: 'test-monitor-1',
|
||||
location: location1.label,
|
||||
},
|
||||
{
|
||||
[`test-monitor-2-${location1.label}`]: {
|
||||
configId: 'test-monitor-2',
|
||||
heartbeatId: 'test-monitor-2',
|
||||
monitorQueryId: 'test-monitor-2',
|
||||
location: location1.label,
|
||||
},
|
||||
{
|
||||
[`test-monitor-3${location1.label}`]: {
|
||||
configId: 'test-monitor-3',
|
||||
heartbeatId: 'test-monitor-3',
|
||||
monitorQueryId: 'test-monitor-3',
|
||||
location: location1.label,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
data: {
|
||||
total: 0,
|
||||
|
|
|
@ -34,7 +34,7 @@ export function useMonitorsSortedByStatus() {
|
|||
|
||||
const { downConfigs } = status;
|
||||
const downMonitorMap: Record<string, string[]> = {};
|
||||
downConfigs.forEach(({ location, configId }) => {
|
||||
Object.values(downConfigs).forEach(({ location, configId }) => {
|
||||
if (downMonitorMap[configId]) {
|
||||
downMonitorMap[configId].push(location);
|
||||
} else {
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* 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 { useSelector } from 'react-redux';
|
||||
import { selectOverviewStatus } from '../state/overview';
|
||||
|
||||
export function useStatusByLocationOverview(configId: string, locationName?: string) {
|
||||
const { status } = useSelector(selectOverviewStatus);
|
||||
if (!locationName || !status) {
|
||||
return 'unknown';
|
||||
}
|
||||
const allConfigs = status.allConfigs;
|
||||
|
||||
return allConfigs[`${configId}-${locationName}`]?.status || 'unknown';
|
||||
}
|
|
@ -12,7 +12,7 @@ import {
|
|||
MonitorOverviewResultCodec,
|
||||
FetchMonitorOverviewQueryArgs,
|
||||
OverviewStatus,
|
||||
OverviewStatusType,
|
||||
OverviewStatusCodec,
|
||||
} from '../../../../../common/runtime_types';
|
||||
import { apiService } from '../../../../utils/api_service';
|
||||
import { MonitorOverviewPageState } from './models';
|
||||
|
@ -54,5 +54,5 @@ export const fetchOverviewStatus = async (
|
|||
pageState: MonitorOverviewPageState
|
||||
): Promise<OverviewStatus> => {
|
||||
const params = toMonitorOverviewQueryArgs(pageState);
|
||||
return await apiService.get(SYNTHETICS_API_URLS.OVERVIEW_STATUS, params, OverviewStatusType);
|
||||
return await apiService.get(SYNTHETICS_API_URLS.OVERVIEW_STATUS, params, OverviewStatusCodec);
|
||||
};
|
||||
|
|
|
@ -72,7 +72,10 @@ export const monitorOverviewReducer = createReducer(initialState, (builder) => {
|
|||
state.flyoutConfig = action.payload;
|
||||
})
|
||||
.addCase(fetchOverviewStatusAction.success, (state, action) => {
|
||||
state.status = action.payload;
|
||||
state.status = {
|
||||
...action.payload,
|
||||
allConfigs: { ...action.payload.upConfigs, ...action.payload.downConfigs },
|
||||
};
|
||||
})
|
||||
.addCase(fetchOverviewStatusAction.fail, (state, action) => {
|
||||
state.statusError = action.payload;
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { MonitorOverviewResult, OverviewStatus } from '../../../../../common/runtime_types';
|
||||
import { MonitorOverviewResult, OverviewStatusState } from '../../../../../common/runtime_types';
|
||||
|
||||
import { IHttpSerializedFetchError } from '../utils/http_error';
|
||||
|
||||
|
@ -31,6 +31,6 @@ export interface MonitorOverviewState {
|
|||
loading: boolean;
|
||||
loaded: boolean;
|
||||
error: IHttpSerializedFetchError | null;
|
||||
status: OverviewStatus | null;
|
||||
status: OverviewStatusState | null;
|
||||
statusError: IHttpSerializedFetchError | null;
|
||||
}
|
||||
|
|
|
@ -87,7 +87,7 @@ describe('current status route', () => {
|
|||
config_id: 'id1',
|
||||
observer: {
|
||||
geo: {
|
||||
name: 'test-location',
|
||||
name: 'Asia/Pacific - Japan',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -122,7 +122,7 @@ describe('current status route', () => {
|
|||
config_id: 'id2',
|
||||
observer: {
|
||||
geo: {
|
||||
name: 'test-location',
|
||||
name: 'Asia/Pacific - Japan',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -150,7 +150,7 @@ describe('current status route', () => {
|
|||
config_id: 'id2',
|
||||
observer: {
|
||||
geo: {
|
||||
name: 'test-location',
|
||||
name: 'Europe - Germany',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -168,25 +168,28 @@ describe('current status route', () => {
|
|||
down: 1,
|
||||
enabledIds: ['id1', 'id2'],
|
||||
up: 2,
|
||||
upConfigs: [
|
||||
{
|
||||
upConfigs: {
|
||||
'id1-Asia/Pacific - Japan': {
|
||||
configId: 'id1',
|
||||
heartbeatId: 'id1',
|
||||
location: 'test-location',
|
||||
monitorQueryId: 'id1',
|
||||
location: 'Asia/Pacific - Japan',
|
||||
status: 'up',
|
||||
},
|
||||
{
|
||||
'id2-Asia/Pacific - Japan': {
|
||||
configId: 'id2',
|
||||
heartbeatId: 'id2',
|
||||
location: 'test-location',
|
||||
monitorQueryId: 'id2',
|
||||
location: 'Asia/Pacific - Japan',
|
||||
status: 'up',
|
||||
},
|
||||
],
|
||||
downConfigs: [
|
||||
{
|
||||
},
|
||||
downConfigs: {
|
||||
'id2-Europe - Germany': {
|
||||
configId: 'id2',
|
||||
heartbeatId: 'id2',
|
||||
location: 'test-location',
|
||||
monitorQueryId: 'id2',
|
||||
location: 'Europe - Germany',
|
||||
status: 'down',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -217,7 +220,7 @@ describe('current status route', () => {
|
|||
config_id: 'id1',
|
||||
observer: {
|
||||
geo: {
|
||||
name: 'test-location',
|
||||
name: 'Asia/Pacific - Japan',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -252,7 +255,7 @@ describe('current status route', () => {
|
|||
config_id: 'id2',
|
||||
observer: {
|
||||
geo: {
|
||||
name: 'test-location',
|
||||
name: 'Asia/Pacific - Japan',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -280,7 +283,7 @@ describe('current status route', () => {
|
|||
config_id: 'id2',
|
||||
observer: {
|
||||
geo: {
|
||||
name: 'test-location',
|
||||
name: 'Europe - Germany',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -305,25 +308,28 @@ describe('current status route', () => {
|
|||
down: 1,
|
||||
enabledIds: ['id1', 'id2'],
|
||||
up: 2,
|
||||
upConfigs: [
|
||||
{
|
||||
upConfigs: {
|
||||
'id1-Asia/Pacific - Japan': {
|
||||
configId: 'id1',
|
||||
heartbeatId: 'id1',
|
||||
location: 'test-location',
|
||||
monitorQueryId: 'id1',
|
||||
location: 'Asia/Pacific - Japan',
|
||||
status: 'up',
|
||||
},
|
||||
{
|
||||
'id2-Asia/Pacific - Japan': {
|
||||
configId: 'id2',
|
||||
heartbeatId: 'id2',
|
||||
location: 'test-location',
|
||||
monitorQueryId: 'id2',
|
||||
location: 'Asia/Pacific - Japan',
|
||||
status: 'up',
|
||||
},
|
||||
],
|
||||
downConfigs: [
|
||||
{
|
||||
},
|
||||
downConfigs: {
|
||||
'id2-Europe - Germany': {
|
||||
configId: 'id2',
|
||||
heartbeatId: 'id2',
|
||||
location: 'test-location',
|
||||
monitorQueryId: 'id2',
|
||||
location: 'Europe - Germany',
|
||||
status: 'down',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(esClient.search).toHaveBeenCalledTimes(2);
|
||||
// These assertions are to ensure that we are paginating through the IDs we use for filtering
|
||||
|
|
|
@ -107,30 +107,32 @@ export async function queryMonitorStatus(
|
|||
}
|
||||
let up = 0;
|
||||
let down = 0;
|
||||
const upConfigs: OverviewStatusMetaData[] = [];
|
||||
const downConfigs: OverviewStatusMetaData[] = [];
|
||||
const upConfigs: Record<string, OverviewStatusMetaData> = {};
|
||||
const downConfigs: Record<string, OverviewStatusMetaData> = {};
|
||||
for await (const response of promises) {
|
||||
response.aggregations?.id.buckets.forEach(({ location }: { key: string; location: any }) => {
|
||||
location.buckets.forEach(({ status }: { key: string; status: any }) => {
|
||||
const downCount = status.hits.hits[0]._source.summary.down;
|
||||
const upCount = status.hits.hits[0]._source.summary.up;
|
||||
const configId = status.hits.hits[0]._source.config_id;
|
||||
const heartbeatId = status.hits.hits[0]._source.monitor.id;
|
||||
const monitorQueryId = status.hits.hits[0]._source.monitor.id;
|
||||
const locationName = status.hits.hits[0]._source.observer?.geo?.name;
|
||||
if (upCount > 0) {
|
||||
up += 1;
|
||||
upConfigs.push({
|
||||
upConfigs[`${configId}-${locationName}`] = {
|
||||
configId,
|
||||
heartbeatId,
|
||||
monitorQueryId,
|
||||
location: locationName,
|
||||
});
|
||||
status: 'up',
|
||||
};
|
||||
} else if (downCount > 0) {
|
||||
down += 1;
|
||||
downConfigs.push({
|
||||
downConfigs[`${configId}-${locationName}`] = {
|
||||
configId,
|
||||
heartbeatId,
|
||||
monitorQueryId,
|
||||
location: locationName,
|
||||
});
|
||||
status: 'down',
|
||||
};
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue