[8.9] [Synthetics] Fix active error state (#160818) (#161879)

# Backport

This will backport the following commits from `main` to `8.9`:
- [[Synthetics] Fix active error state
(#160818)](https://github.com/elastic/kibana/pull/160818)

<!--- Backport version: 8.9.7 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Justin
Kambic","email":"jk@elastic.co"},"sourceCommit":{"committedDate":"2023-07-13T14:49:53Z","message":"[Synthetics]
Fix active error state (#160818)\n\nCo-authored-by: Shahzad
<shahzad31comp@gmail.com>","sha":"f142cd25f4b19ce1524ec466c481cbe128d1c426","branchLabelMapping":{"^v8.10.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["bug","release_note:fix","Team:uptime","v8.9.0","v8.10.0"],"number":160818,"url":"https://github.com/elastic/kibana/pull/160818","mergeCommit":{"message":"[Synthetics]
Fix active error state (#160818)\n\nCo-authored-by: Shahzad
<shahzad31comp@gmail.com>","sha":"f142cd25f4b19ce1524ec466c481cbe128d1c426"}},"sourceBranch":"main","suggestedTargetBranches":["8.9"],"targetPullRequestStates":[{"branch":"8.9","label":"v8.9.0","labelRegex":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"main","label":"v8.10.0","labelRegex":"^v8.10.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/160818","number":160818,"mergeCommit":{"message":"[Synthetics]
Fix active error state (#160818)\n\nCo-authored-by: Shahzad
<shahzad31comp@gmail.com>","sha":"f142cd25f4b19ce1524ec466c481cbe128d1c426"}}]}]
BACKPORT-->

Co-authored-by: Justin Kambic <jk@elastic.co>
This commit is contained in:
Kibana Machine 2023-07-13 12:03:08 -04:00 committed by GitHub
parent e72edd29a1
commit 69248aff08
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 66 additions and 99 deletions

View file

@ -4,7 +4,6 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import moment from 'moment';
import { useTimeZone } from '@kbn/observability-shared-plugin/public';
import { useParams } from 'react-router-dom';
import { useMemo } from 'react';
@ -49,11 +48,6 @@ export function useMonitorErrors(monitorIdArg?: string) {
},
},
},
{
term: {
'state.up': 0,
},
},
{
term: {
config_id: monitorIdArg ?? monitorId,
@ -69,7 +63,7 @@ export function useMonitorErrors(monitorIdArg?: string) {
},
sort: [{ 'state.started_at': 'desc' }],
aggs: {
errorStates: {
states: {
terms: {
field: 'state.id',
size: 10000,
@ -84,6 +78,13 @@ export function useMonitorErrors(monitorIdArg?: string) {
},
},
},
latest: {
top_hits: {
size: 1,
_source: ['monitor.status'],
sort: [{ '@timestamp': 'desc' }],
},
},
},
},
},
@ -95,27 +96,35 @@ export function useMonitorErrors(monitorIdArg?: string) {
);
return useMemo(() => {
const errorStates = data?.aggregations?.errorStates.buckets?.map((loc) => {
return loc.summary.hits.hits?.[0]._source as PingState;
});
const defaultValues = { upStates: [], errorStates: [] };
// re-bucket states into error/up
// including the `up` states is useful for determining error duration
const { errorStates, upStates } =
data?.aggregations?.states.buckets.reduce<{
upStates: PingState[];
errorStates: PingState[];
}>((prev, cur) => {
const source = cur.summary.hits.hits?.[0]._source as PingState | undefined;
if (source?.state.up === 0) {
prev.errorStates.push(source as PingState);
} else if (!!source?.state.up && source.state.up >= 1) {
prev.upStates.push(source as PingState);
}
return prev;
}, defaultValues) ?? defaultValues;
const hasActiveError: boolean =
errorStates?.some((errorState) => isActiveState(errorState)) || false;
data?.aggregations?.latest.hits.hits.length === 1 &&
(data?.aggregations?.latest.hits.hits[0]._source as { monitor: { status: string } }).monitor
.status === 'down' &&
!!errorStates?.length;
return {
errorStates,
upStates,
loading,
data,
hasActiveError,
};
}, [data, loading]);
}
export const isActiveState = (item: PingState) => {
const timestamp = item['@timestamp'];
const interval = moment(item.monitor.timespan?.lt).diff(
moment(item.monitor.timespan?.gte),
'milliseconds'
);
return moment().diff(moment(timestamp), 'milliseconds') < interval;
};

View file

@ -1,65 +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 * as useMonitorErrors from '../hooks/use_monitor_errors';
import { isErrorActive } from './errors_list';
describe('isErrorActive', () => {
let isActiveSpy: jest.SpyInstance;
beforeEach(() => {
isActiveSpy = jest.spyOn(useMonitorErrors, 'isActiveState').mockReturnValue(true);
});
const item = {
'@timestamp': '2023-05-04T00:00:00.000Z',
timestamp: '2023-05-04T00:00:00.000Z',
docId: 'SGIQ6IcBTfgfaiALCdZ8',
error: { message: 'Encountered an error and made this unhelpful message.' },
state: {
duration_ms: '415801',
checks: 8,
ends: null,
started_at: '2023-05-04T18:32:41.111671462Z',
id: 'foo',
up: 8,
down: 0,
status: 'up',
},
monitor: {
id: 'foo',
status: 'up',
type: 'browser',
check_group: 'f01850cc-eaaa-11ed-887d-caddd792d648',
timespan: { gte: '2023-05-04T00:00:00.000Z', lt: '2023-05-04T01:00:00.000Z' },
},
};
const lastErrorId = 'foo';
const latestPingStatus = 'down';
it('returns true if error is active', () => {
const result = isErrorActive(item, lastErrorId, latestPingStatus);
expect(result).toBe(true);
});
it('returns false if error is not active', () => {
isActiveSpy.mockReturnValue(false);
expect(
isErrorActive(
{ ...item, '@timestamp': '2023-05-04T02:00:00.000Z' },
lastErrorId,
latestPingStatus
)
).toBe(false);
});
it('returns false if `lastErrorId` does not match `item.state.id`', () => {
expect(isErrorActive(item, 'bar', latestPingStatus)).toBe(false);
});
it('returns false if latestPingStatus is `up`', () => {
expect(isErrorActive(item, lastErrorId, 'up')).toBe(false);
});
});

View file

@ -24,21 +24,31 @@ import { Ping, PingState } from '../../../../../../common/runtime_types';
import { useErrorFailedStep } from '../hooks/use_error_failed_step';
import { formatTestDuration } from '../../../utils/monitor_test_result/test_time_formats';
import { useDateFormat } from '../../../../../hooks/use_date_format';
import { isActiveState } from '../hooks/use_monitor_errors';
import { useMonitorLatestPing } from '../hooks/use_monitor_latest_ping';
export function isErrorActive(item: PingState, lastErrorId?: string, latestPingStatus?: string) {
// if the error is the most recent, `isActiveState`, and the monitor
// is not yet back up, label the error as active
return isActiveState(item) && lastErrorId === item.state.id && latestPingStatus !== 'up';
function isErrorActive(lastError: PingState, currentError: PingState, latestPing?: Ping) {
return (
latestPing?.monitor.status === 'down' &&
lastError['@timestamp'] === currentError['@timestamp'] &&
typeof currentError['@timestamp'] !== undefined
);
}
function getNextUpStateForResolvedError(errorState: PingState, upStates: PingState[]) {
for (const upState of upStates) {
if (moment(upState.state.started_at).valueOf() > moment(errorState['@timestamp']).valueOf())
return upState;
}
}
export const ErrorsList = ({
errorStates,
upStates,
loading,
location,
}: {
errorStates: PingState[];
upStates: PingState[];
loading: boolean;
location: ReturnType<typeof useSelectedLocation>;
}) => {
@ -64,7 +74,6 @@ export const ErrorsList = ({
const lastErrorTestRun = errorStates?.sort((a, b) => {
return moment(b.state.started_at).valueOf() - moment(a.state.started_at).valueOf();
})?.[0];
const isTabletOrGreater = useIsWithinMinBreakpoint('s');
const columns = [
@ -83,7 +92,8 @@ export const ErrorsList = ({
locationId={location?.id}
/>
);
if (isErrorActive(item, lastErrorTestRun?.state.id, latestPing?.monitor.status)) {
if (isErrorActive(lastErrorTestRun, item, latestPing)) {
return (
<EuiFlexGroup gutterSize="m" alignItems="center" wrap={true}>
<EuiFlexItem grow={false} className="eui-textNoWrap">
@ -118,7 +128,7 @@ export const ErrorsList = ({
}
return failedStep.synthetics?.step?.name;
},
render: (value: string, item: PingState) => {
render: (value: string) => {
const failedStep = failedSteps.find((step) => step.monitor.check_group === value);
if (!failedStep) {
return <>--</>;
@ -142,19 +152,20 @@ export const ErrorsList = ({
align: 'right' as const,
sortable: true,
render: (value: string, item: PingState) => {
const isActive = isActiveState(item);
let activeDuration = 0;
if (item.monitor.timespan) {
const diff = moment(item.monitor.timespan.lt).diff(
moment(item.monitor.timespan.gte),
'millisecond'
);
if (isActive) {
if (isErrorActive(lastErrorTestRun, item, latestPing)) {
const currentDiff = moment().diff(item['@timestamp']);
activeDuration = currentDiff < diff ? currentDiff : diff;
} else {
activeDuration = diff;
const resolvedState = getNextUpStateForResolvedError(item, upStates);
activeDuration = moment(resolvedState?.state.started_at).diff(item['@timestamp']) ?? 0;
}
}
return (

View file

@ -23,8 +23,10 @@ export const ErrorsTabContent = ({
errorStates,
loading,
location,
upStates,
}: {
errorStates: PingState[];
upStates: PingState[];
loading: boolean;
location: ReturnType<typeof useSelectedLocation>;
}) => {
@ -69,7 +71,12 @@ export const ErrorsTabContent = ({
<EuiFlexGroup gutterSize="m" wrap={true}>
<EuiFlexItem grow={2} css={{ minWidth: 260 }}>
<PanelWithTitle title={ERRORS_LABEL}>
<ErrorsList location={location} errorStates={errorStates} loading={loading} />
<ErrorsList
location={location}
errorStates={errorStates}
upStates={upStates}
loading={loading}
/>
</PanelWithTitle>
</EuiFlexItem>
<FailedTestsByStep time={time} />

View file

@ -23,7 +23,7 @@ import { MonitorPendingWrapper } from '../monitor_pending_wrapper';
import { useSelectedLocation } from '../hooks/use_selected_location';
export const MonitorErrors = () => {
const { errorStates, loading, data } = useMonitorErrors();
const { errorStates, upStates, loading, data } = useMonitorErrors();
const location = useSelectedLocation();
const initialLoading = !data;
@ -42,7 +42,12 @@ export const MonitorErrors = () => {
{initialLoading && <LoadingErrors />}
{emptyState && <EmptyErrors />}
<div style={{ visibility: initialLoading || emptyState ? 'collapse' : 'initial' }}>
<ErrorsTabContent location={location} errorStates={errorStates ?? []} loading={loading} />
<ErrorsTabContent
location={location}
errorStates={errorStates}
upStates={upStates}
loading={loading}
/>
</div>
</MonitorPendingWrapper>
);