mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
This PR optimizes both the snapshot component and the monitor list on the overview page by using the new monitor.timespan field from elastic/beats#14778. Note that the functionality here will work with heartbeats lacking that patch, but the performance improvements will be absent. This PR adapts the snapshot tests to use synthetically generated data which should be easier to maintain. As a result some of that code is refactored as well. See #52433 parent issue as well.
This commit is contained in:
parent
ace18932b0
commit
40346ca0d9
47 changed files with 492 additions and 342 deletions
|
@ -2173,18 +2173,6 @@
|
|||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "mixed",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": { "kind": "SCALAR", "name": "Int", "ofType": null }
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "total",
|
||||
"description": "",
|
||||
|
|
|
@ -419,8 +419,6 @@ export interface SnapshotCount {
|
|||
|
||||
down: number;
|
||||
|
||||
mixed: number;
|
||||
|
||||
total: number;
|
||||
}
|
||||
|
||||
|
|
|
@ -8,7 +8,6 @@ import * as t from 'io-ts';
|
|||
|
||||
export const SnapshotType = t.type({
|
||||
down: t.number,
|
||||
mixed: t.number,
|
||||
total: t.number,
|
||||
up: t.number,
|
||||
});
|
||||
|
|
|
@ -13,7 +13,6 @@ describe('Snapshot component', () => {
|
|||
const snapshot: Snapshot = {
|
||||
up: 8,
|
||||
down: 2,
|
||||
mixed: 0,
|
||||
total: 10,
|
||||
};
|
||||
|
||||
|
|
|
@ -103,6 +103,7 @@ exports[`DonutChart component renders a donut chart 1`] = `
|
|||
</span>
|
||||
<span
|
||||
class="euiFlexItem c2"
|
||||
data-test-subj="xpack.uptime.snapshot.donutChart.down"
|
||||
>
|
||||
32
|
||||
</span>
|
||||
|
@ -150,6 +151,7 @@ exports[`DonutChart component renders a donut chart 1`] = `
|
|||
</span>
|
||||
<span
|
||||
class="euiFlexItem c2"
|
||||
data-test-subj="xpack.uptime.snapshot.donutChart.up"
|
||||
>
|
||||
95
|
||||
</span>
|
||||
|
|
|
@ -5,6 +5,7 @@ exports[`DonutChartLegend applies valid props as expected 1`] = `
|
|||
<DonutChartLegendRow
|
||||
color="#bd271e"
|
||||
content={23}
|
||||
data-test-subj="xpack.uptime.snapshot.donutChart.down"
|
||||
message="Down"
|
||||
/>
|
||||
<EuiSpacer
|
||||
|
@ -13,6 +14,7 @@ exports[`DonutChartLegend applies valid props as expected 1`] = `
|
|||
<DonutChartLegendRow
|
||||
color="#d3dae6"
|
||||
content={45}
|
||||
data-test-subj="xpack.uptime.snapshot.donutChart.up"
|
||||
message="Up"
|
||||
/>
|
||||
</styled.div>
|
||||
|
|
|
@ -21,6 +21,7 @@ exports[`DonutChartLegendRow passes appropriate props 1`] = `
|
|||
</Styled(EuiFlexItem)>
|
||||
<Styled(EuiFlexItem)
|
||||
component="span"
|
||||
data-test-subj="foo"
|
||||
>
|
||||
23
|
||||
</Styled(EuiFlexItem)>
|
||||
|
|
|
@ -11,7 +11,7 @@ import React from 'react';
|
|||
describe('DonutChartLegendRow', () => {
|
||||
it('passes appropriate props', () => {
|
||||
const wrapper = shallowWithIntl(
|
||||
<DonutChartLegendRow color="green" message="Foo" content={23} />
|
||||
<DonutChartLegendRow color="green" message="Foo" content={23} data-test-subj="foo" />
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
|
|
@ -73,7 +73,7 @@ export const DonutChart = ({ height, down, up, width }: DonutChartProps) => {
|
|||
<EuiFlexGroup alignItems="center" responsive={false}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<svg
|
||||
aria-label={i18n.translate('xpack.uptime.donutChart.ariaLabel', {
|
||||
aria-label={i18n.translate('xpack.uptime.snapshot.donutChart.ariaLabel', {
|
||||
defaultMessage:
|
||||
'Pie chart showing the current status. {down} of {total} monitors are down.',
|
||||
values: { down, total: up + down },
|
||||
|
|
|
@ -34,17 +34,19 @@ export const DonutChartLegend = ({ down, up }: Props) => {
|
|||
<DonutChartLegendRow
|
||||
color={danger}
|
||||
content={down}
|
||||
message={i18n.translate('xpack.uptime.donutChart.legend.downRowLabel', {
|
||||
message={i18n.translate('xpack.uptime.snapshot.donutChart.legend.downRowLabel', {
|
||||
defaultMessage: 'Down',
|
||||
})}
|
||||
data-test-subj={'xpack.uptime.snapshot.donutChart.down'}
|
||||
/>
|
||||
<EuiSpacer size="m" />
|
||||
<DonutChartLegendRow
|
||||
color={gray}
|
||||
content={up}
|
||||
message={i18n.translate('xpack.uptime.donutChart.legend.upRowLabel', {
|
||||
message={i18n.translate('xpack.uptime.snapshot.donutChart.legend.upRowLabel', {
|
||||
defaultMessage: 'Up',
|
||||
})}
|
||||
data-test-subj={'xpack.uptime.snapshot.donutChart.up'}
|
||||
/>
|
||||
</LegendContainer>
|
||||
);
|
||||
|
|
|
@ -23,9 +23,10 @@ interface Props {
|
|||
color: string;
|
||||
message: string;
|
||||
content: string | number;
|
||||
'data-test-subj': string;
|
||||
}
|
||||
|
||||
export const DonutChartLegendRow = ({ color, content, message }: Props) => (
|
||||
export const DonutChartLegendRow = ({ color, content, message, 'data-test-subj': dts }: Props) => (
|
||||
<EuiFlexGroup gutterSize="l" responsive={false}>
|
||||
<EuiFlexItemReducedMargin component="span" grow={false}>
|
||||
<EuiHealth color={color} />
|
||||
|
@ -33,6 +34,8 @@ export const DonutChartLegendRow = ({ color, content, message }: Props) => (
|
|||
<EuiFlexItemReducedMargin component="span" grow={false}>
|
||||
{message}
|
||||
</EuiFlexItemReducedMargin>
|
||||
<EuiFlexItemAlignRight component="span">{content}</EuiFlexItemAlignRight>
|
||||
<EuiFlexItemAlignRight component="span" data-test-subj={dts}>
|
||||
{content}
|
||||
</EuiFlexItemAlignRight>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
|
|
|
@ -271,7 +271,6 @@
|
|||
"monitor": {
|
||||
"id": null,
|
||||
"name": "elastic",
|
||||
"status": "mixed",
|
||||
"type": null,
|
||||
"__typename": "MonitorState"
|
||||
},
|
||||
|
@ -377,7 +376,6 @@
|
|||
"monitor": {
|
||||
"id": null,
|
||||
"name": null,
|
||||
"status": "mixed",
|
||||
"type": null,
|
||||
"__typename": "MonitorState"
|
||||
},
|
||||
|
|
|
@ -20,8 +20,6 @@ const getHealthColor = (status: string): string => {
|
|||
return 'success';
|
||||
case 'down':
|
||||
return 'danger';
|
||||
case 'mixed':
|
||||
return 'warning';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
|
@ -37,10 +35,6 @@ const getHealthMessage = (status: string): string | null => {
|
|||
return i18n.translate('xpack.uptime.monitorList.statusColumn.downLabel', {
|
||||
defaultMessage: 'Down',
|
||||
});
|
||||
case 'mixed':
|
||||
return i18n.translate('xpack.uptime.monitorList.statusColumn.mixedLabel', {
|
||||
defaultMessage: 'Mixed',
|
||||
});
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`snapshot API throws when server response doesn't correspond to expected type 1`] = `
|
||||
[Error: Invalid value undefined supplied to : { down: number, mixed: number, total: number, up: number }/down: number
|
||||
Invalid value undefined supplied to : { down: number, mixed: number, total: number, up: number }/mixed: number
|
||||
Invalid value undefined supplied to : { down: number, mixed: number, total: number, up: number }/total: number
|
||||
Invalid value undefined supplied to : { down: number, mixed: number, total: number, up: number }/up: number]
|
||||
[Error: Invalid value undefined supplied to : { down: number, total: number, up: number }/down: number
|
||||
Invalid value undefined supplied to : { down: number, total: number, up: number }/total: number
|
||||
Invalid value undefined supplied to : { down: number, total: number, up: number }/up: number]
|
||||
`;
|
||||
|
|
|
@ -14,7 +14,7 @@ describe('snapshot API', () => {
|
|||
fetchMock = jest.spyOn(window, 'fetch');
|
||||
mockResponse = {
|
||||
ok: true,
|
||||
json: () => new Promise(r => r({ up: 3, down: 12, mixed: 0, total: 15 })),
|
||||
json: () => new Promise(r => r({ up: 3, down: 12, total: 15 })),
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -34,7 +34,7 @@ describe('snapshot API', () => {
|
|||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
'/api/uptime/snapshot/count?dateRangeStart=now-15m&dateRangeEnd=now&filters=monitor.id%3A%22auto-http-0X21EE76EAC459873F%22&statusFilter=up'
|
||||
);
|
||||
expect(resp).toEqual({ up: 3, down: 12, mixed: 0, total: 15 });
|
||||
expect(resp).toEqual({ up: 3, down: 12, total: 15 });
|
||||
});
|
||||
|
||||
it(`throws when server response doesn't correspond to expected type`, async () => {
|
||||
|
|
|
@ -4,7 +4,6 @@ exports[`snapshot reducer appends a current error to existing errors list 1`] =
|
|||
Object {
|
||||
"count": Object {
|
||||
"down": 0,
|
||||
"mixed": 0,
|
||||
"total": 0,
|
||||
"up": 0,
|
||||
},
|
||||
|
@ -19,7 +18,6 @@ exports[`snapshot reducer changes the count when a snapshot fetch succeeds 1`] =
|
|||
Object {
|
||||
"count": Object {
|
||||
"down": 15,
|
||||
"mixed": 0,
|
||||
"total": 25,
|
||||
"up": 10,
|
||||
},
|
||||
|
@ -32,7 +30,6 @@ exports[`snapshot reducer sets the state's status to loading during a fetch 1`]
|
|||
Object {
|
||||
"count": Object {
|
||||
"down": 0,
|
||||
"mixed": 0,
|
||||
"total": 0,
|
||||
"up": 0,
|
||||
},
|
||||
|
@ -45,7 +42,6 @@ exports[`snapshot reducer updates existing state 1`] = `
|
|||
Object {
|
||||
"count": Object {
|
||||
"down": 1,
|
||||
"mixed": 0,
|
||||
"total": 4,
|
||||
"up": 3,
|
||||
},
|
||||
|
|
|
@ -21,7 +21,7 @@ describe('snapshot reducer', () => {
|
|||
expect(
|
||||
snapshotReducer(
|
||||
{
|
||||
count: { down: 1, mixed: 0, total: 4, up: 3 },
|
||||
count: { down: 1, total: 4, up: 3 },
|
||||
errors: [],
|
||||
loading: false,
|
||||
},
|
||||
|
@ -47,7 +47,6 @@ describe('snapshot reducer', () => {
|
|||
payload: {
|
||||
up: 10,
|
||||
down: 15,
|
||||
mixed: 0,
|
||||
total: 25,
|
||||
},
|
||||
};
|
||||
|
|
|
@ -21,7 +21,6 @@ export interface SnapshotState {
|
|||
const initialState: SnapshotState = {
|
||||
count: {
|
||||
down: 0,
|
||||
mixed: 0,
|
||||
total: 0,
|
||||
up: 0,
|
||||
},
|
||||
|
|
|
@ -19,7 +19,6 @@ describe('state selectors', () => {
|
|||
count: {
|
||||
up: 2,
|
||||
down: 0,
|
||||
mixed: 0,
|
||||
total: 2,
|
||||
},
|
||||
errors: [],
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`get snapshot helper reduces check groups as expected 1`] = `
|
||||
Object {
|
||||
"down": 1,
|
||||
"mixed": 0,
|
||||
"total": 3,
|
||||
"up": 2,
|
||||
}
|
||||
`;
|
|
@ -1,106 +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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { getSnapshotCountHelper } from '../get_snapshot_helper';
|
||||
import { MonitorGroups } from '../search';
|
||||
|
||||
describe('get snapshot helper', () => {
|
||||
let mockIterator: any;
|
||||
beforeAll(() => {
|
||||
mockIterator = jest.fn();
|
||||
const summaryTimestamp = new Date('2019-01-01');
|
||||
const firstResult: MonitorGroups = {
|
||||
id: 'firstGroup',
|
||||
groups: [
|
||||
{
|
||||
monitorId: 'first-monitor',
|
||||
location: 'us-east-1',
|
||||
checkGroup: 'abc',
|
||||
status: 'down',
|
||||
summaryTimestamp,
|
||||
},
|
||||
{
|
||||
monitorId: 'first-monitor',
|
||||
location: 'us-west-1',
|
||||
checkGroup: 'abc',
|
||||
status: 'up',
|
||||
summaryTimestamp,
|
||||
},
|
||||
{
|
||||
monitorId: 'first-monitor',
|
||||
location: 'amsterdam',
|
||||
checkGroup: 'abc',
|
||||
status: 'down',
|
||||
summaryTimestamp,
|
||||
},
|
||||
],
|
||||
};
|
||||
const secondResult: MonitorGroups = {
|
||||
id: 'secondGroup',
|
||||
groups: [
|
||||
{
|
||||
monitorId: 'second-monitor',
|
||||
location: 'us-east-1',
|
||||
checkGroup: 'yyz',
|
||||
status: 'up',
|
||||
summaryTimestamp,
|
||||
},
|
||||
{
|
||||
monitorId: 'second-monitor',
|
||||
location: 'us-west-1',
|
||||
checkGroup: 'yyz',
|
||||
status: 'up',
|
||||
summaryTimestamp,
|
||||
},
|
||||
{
|
||||
monitorId: 'second-monitor',
|
||||
location: 'amsterdam',
|
||||
checkGroup: 'yyz',
|
||||
status: 'up',
|
||||
summaryTimestamp,
|
||||
},
|
||||
],
|
||||
};
|
||||
const thirdResult: MonitorGroups = {
|
||||
id: 'thirdGroup',
|
||||
groups: [
|
||||
{
|
||||
monitorId: 'third-monitor',
|
||||
location: 'us-east-1',
|
||||
checkGroup: 'dt',
|
||||
status: 'up',
|
||||
summaryTimestamp,
|
||||
},
|
||||
{
|
||||
monitorId: 'third-monitor',
|
||||
location: 'us-west-1',
|
||||
checkGroup: 'dt',
|
||||
status: 'up',
|
||||
summaryTimestamp,
|
||||
},
|
||||
{
|
||||
monitorId: 'third-monitor',
|
||||
location: 'amsterdam',
|
||||
checkGroup: 'dt',
|
||||
status: 'up',
|
||||
summaryTimestamp,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const mockNext = jest
|
||||
.fn()
|
||||
.mockReturnValueOnce(firstResult)
|
||||
.mockReturnValueOnce(secondResult)
|
||||
.mockReturnValueOnce(thirdResult)
|
||||
.mockReturnValueOnce(null);
|
||||
mockIterator.next = mockNext;
|
||||
});
|
||||
|
||||
it('reduces check groups as expected', async () => {
|
||||
expect(await getSnapshotCountHelper(mockIterator)).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -4,22 +4,12 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { UMMonitorStatesAdapter, CursorPagination } from './adapter_types';
|
||||
import { UMMonitorStatesAdapter } from './adapter_types';
|
||||
import { INDEX_NAMES, CONTEXT_DEFAULTS } from '../../../../common/constants';
|
||||
import { fetchPage } from './search';
|
||||
import { MonitorGroupIterator } from './search/monitor_group_iterator';
|
||||
import { getSnapshotCountHelper } from './get_snapshot_helper';
|
||||
|
||||
export interface QueryContext {
|
||||
count: (query: Record<string, any>) => Promise<any>;
|
||||
search: (query: Record<string, any>) => Promise<any>;
|
||||
dateRangeStart: string;
|
||||
dateRangeEnd: string;
|
||||
pagination: CursorPagination;
|
||||
filterClause: any | null;
|
||||
size: number;
|
||||
statusFilter?: string;
|
||||
}
|
||||
import { Snapshot } from '../../../../common/runtime_types';
|
||||
import { QueryContext } from './search/query_context';
|
||||
|
||||
export const elasticsearchMonitorStatesAdapter: UMMonitorStatesAdapter = {
|
||||
// Gets a page of monitor states.
|
||||
|
@ -35,16 +25,15 @@ export const elasticsearchMonitorStatesAdapter: UMMonitorStatesAdapter = {
|
|||
statusFilter = statusFilter === null ? undefined : statusFilter;
|
||||
const size = 10;
|
||||
|
||||
const queryContext: QueryContext = {
|
||||
count: (query: Record<string, any>): Promise<any> => callES('count', query),
|
||||
search: (query: Record<string, any>): Promise<any> => callES('search', query),
|
||||
const queryContext = new QueryContext(
|
||||
callES,
|
||||
dateRangeStart,
|
||||
dateRangeEnd,
|
||||
pagination,
|
||||
filterClause: filters && filters !== '' ? JSON.parse(filters) : null,
|
||||
filters && filters !== '' ? JSON.parse(filters) : null,
|
||||
size,
|
||||
statusFilter,
|
||||
};
|
||||
statusFilter
|
||||
);
|
||||
|
||||
const page = await fetchPage(queryContext);
|
||||
|
||||
|
@ -55,18 +44,46 @@ export const elasticsearchMonitorStatesAdapter: UMMonitorStatesAdapter = {
|
|||
};
|
||||
},
|
||||
|
||||
getSnapshotCount: async ({ callES, dateRangeStart, dateRangeEnd, filters, statusFilter }) => {
|
||||
const context: QueryContext = {
|
||||
count: query => callES('count', query),
|
||||
search: query => callES('search', query),
|
||||
getSnapshotCount: async ({
|
||||
callES,
|
||||
dateRangeStart,
|
||||
dateRangeEnd,
|
||||
filters,
|
||||
statusFilter,
|
||||
}): Promise<Snapshot> => {
|
||||
if (!(statusFilter === 'up' || statusFilter === 'down' || statusFilter === undefined)) {
|
||||
throw new Error(`Invalid status filter value '${statusFilter}'`);
|
||||
}
|
||||
|
||||
const context = new QueryContext(
|
||||
callES,
|
||||
dateRangeStart,
|
||||
dateRangeEnd,
|
||||
pagination: CONTEXT_DEFAULTS.CURSOR_PAGINATION,
|
||||
filterClause: filters && filters !== '' ? JSON.parse(filters) : null,
|
||||
size: CONTEXT_DEFAULTS.MAX_MONITORS_FOR_SNAPSHOT_COUNT,
|
||||
statusFilter,
|
||||
CONTEXT_DEFAULTS.CURSOR_PAGINATION,
|
||||
filters && filters !== '' ? JSON.parse(filters) : null,
|
||||
CONTEXT_DEFAULTS.MAX_MONITORS_FOR_SNAPSHOT_COUNT,
|
||||
statusFilter
|
||||
);
|
||||
|
||||
// Calculate the total, up, and down counts.
|
||||
const counts = await fastStatusCount(context);
|
||||
|
||||
// Check if the last count was accurate, if not, we need to perform a slower count with the
|
||||
// MonitorGroupsIterator.
|
||||
if (!(await context.hasTimespan())) {
|
||||
// Figure out whether 'up' or 'down' is more common. It's faster to count the lower cardinality
|
||||
// one then use subtraction to figure out its opposite.
|
||||
const [leastCommonStatus, mostCommonStatus]: Array<'up' | 'down'> =
|
||||
counts.up > counts.down ? ['down', 'up'] : ['up', 'down'];
|
||||
counts[leastCommonStatus] = await slowStatusCount(context, leastCommonStatus);
|
||||
counts[mostCommonStatus] = counts.total - counts[leastCommonStatus];
|
||||
}
|
||||
|
||||
return {
|
||||
total: statusFilter ? counts[statusFilter] : counts.total,
|
||||
up: statusFilter === 'down' ? 0 : counts.up,
|
||||
down: statusFilter === 'up' ? 0 : counts.down,
|
||||
};
|
||||
return getSnapshotCountHelper(new MonitorGroupIterator(context));
|
||||
},
|
||||
|
||||
statesIndexExists: async ({ callES }) => {
|
||||
|
@ -92,3 +109,46 @@ const jsonifyPagination = (p: any): string | null => {
|
|||
|
||||
return JSON.stringify(p);
|
||||
};
|
||||
|
||||
const fastStatusCount = async (context: QueryContext): Promise<Snapshot> => {
|
||||
const params = {
|
||||
index: INDEX_NAMES.HEARTBEAT,
|
||||
body: {
|
||||
size: 0,
|
||||
query: { bool: { filter: await context.dateAndCustomFilters() } },
|
||||
aggs: {
|
||||
unique: {
|
||||
// We set the precision threshold to 40k which is the max precision supported by cardinality
|
||||
cardinality: { field: 'monitor.id', precision_threshold: 40000 },
|
||||
},
|
||||
down: {
|
||||
filter: { range: { 'summary.down': { gt: 0 } } },
|
||||
aggs: {
|
||||
unique: { cardinality: { field: 'monitor.id', precision_threshold: 40000 } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const statistics = await context.search(params);
|
||||
const total = statistics.aggregations.unique.value;
|
||||
const down = statistics.aggregations.down.unique.value;
|
||||
|
||||
return {
|
||||
total,
|
||||
down,
|
||||
up: total - down,
|
||||
};
|
||||
};
|
||||
|
||||
const slowStatusCount = async (context: QueryContext, status: string): Promise<number> => {
|
||||
const downContext = context.clone();
|
||||
downContext.statusFilter = status;
|
||||
const iterator = new MonitorGroupIterator(downContext);
|
||||
let count = 0;
|
||||
while (await iterator.next()) {
|
||||
count++;
|
||||
}
|
||||
return count;
|
||||
};
|
||||
|
|
|
@ -1,40 +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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { MonitorGroups, MonitorGroupIterator } from './search';
|
||||
import { Snapshot } from '../../../../common/runtime_types';
|
||||
|
||||
const reduceItemsToCounts = (items: MonitorGroups[]) => {
|
||||
let down = 0;
|
||||
let up = 0;
|
||||
items.forEach(item => {
|
||||
if (item.groups.some(group => group.status === 'down')) {
|
||||
down++;
|
||||
} else {
|
||||
up++;
|
||||
}
|
||||
});
|
||||
return {
|
||||
down,
|
||||
mixed: 0,
|
||||
total: down + up,
|
||||
up,
|
||||
};
|
||||
};
|
||||
|
||||
export const getSnapshotCountHelper = async (iterator: MonitorGroupIterator): Promise<Snapshot> => {
|
||||
const items: MonitorGroups[] = [];
|
||||
let res: MonitorGroups | null;
|
||||
// query the index to find the most recent check group for each monitor/location
|
||||
do {
|
||||
res = await iterator.next();
|
||||
if (res) {
|
||||
items.push(res);
|
||||
}
|
||||
} while (res !== null);
|
||||
|
||||
return reduceItemsToCounts(items);
|
||||
};
|
|
@ -11,7 +11,7 @@ import {
|
|||
MonitorGroupsFetcher,
|
||||
MonitorGroupsPage,
|
||||
} from '../fetch_page';
|
||||
import { QueryContext } from '../../elasticsearch_monitor_states_adapter';
|
||||
import { QueryContext } from '../query_context';
|
||||
import { MonitorSummary } from '../../../../../../common/graphql/types';
|
||||
import { nextPagination, prevPagination, simpleQueryContext } from './test_helpers';
|
||||
|
||||
|
|
|
@ -11,8 +11,8 @@ import {
|
|||
MonitorGroupIterator,
|
||||
} from '../monitor_group_iterator';
|
||||
import { simpleQueryContext } from './test_helpers';
|
||||
import { QueryContext } from '../../elasticsearch_monitor_states_adapter';
|
||||
import { MonitorGroups } from '../fetch_page';
|
||||
import { QueryContext } from '../query_context';
|
||||
|
||||
describe('iteration', () => {
|
||||
let iterator: MonitorGroupIterator | null = null;
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import { CursorPagination } from '../../adapter_types';
|
||||
import { CursorDirection, SortOrder } from '../../../../../../common/graphql/types';
|
||||
import { QueryContext } from '../../elasticsearch_monitor_states_adapter';
|
||||
import { QueryContext } from '../query_context';
|
||||
|
||||
export const prevPagination = (key: any): CursorPagination => {
|
||||
return {
|
||||
|
@ -23,14 +23,5 @@ export const nextPagination = (key: any): CursorPagination => {
|
|||
};
|
||||
};
|
||||
export const simpleQueryContext = (): QueryContext => {
|
||||
return {
|
||||
count: _query => new Promise(r => ({})),
|
||||
search: _query => new Promise(r => ({})),
|
||||
dateRangeEnd: '',
|
||||
dateRangeStart: '',
|
||||
filterClause: undefined,
|
||||
pagination: nextPagination('something'),
|
||||
size: 0,
|
||||
statusFilter: '',
|
||||
};
|
||||
return new QueryContext(undefined, '', '', nextPagination('something'), undefined, 0, '');
|
||||
};
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
import { get, sortBy } from 'lodash';
|
||||
import { QueryContext } from '../elasticsearch_monitor_states_adapter';
|
||||
import { QueryContext } from './query_context';
|
||||
import { getHistogramIntervalFormatted } from '../../../helper';
|
||||
import { INDEX_NAMES, STATES } from '../../../../../common/constants';
|
||||
import {
|
||||
|
@ -77,19 +77,19 @@ export const enrichMonitorGroups: MonitorEnricher = async (
|
|||
}
|
||||
String agentIdIP = agentId + "-" + (ip == null ? "" : ip.toString());
|
||||
def ts = doc["@timestamp"][0].toInstant().toEpochMilli();
|
||||
|
||||
|
||||
def lastCheck = state.checksByAgentIdIP[agentId];
|
||||
Instant lastTs = lastCheck != null ? lastCheck["@timestamp"] : null;
|
||||
if (lastTs != null && lastTs > ts) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
curCheck.put("@timestamp", ts);
|
||||
|
||||
|
||||
Map agent = new HashMap();
|
||||
agent.id = agentId;
|
||||
curCheck.put("agent", agent);
|
||||
|
||||
|
||||
if (state.globals.url == null) {
|
||||
Map url = new HashMap();
|
||||
Collection fields = ["full", "original", "scheme", "username", "password", "domain", "port", "path", "query", "fragment"];
|
||||
|
@ -102,7 +102,7 @@ export const enrichMonitorGroups: MonitorEnricher = async (
|
|||
}
|
||||
state.globals.url = url;
|
||||
}
|
||||
|
||||
|
||||
Map monitor = new HashMap();
|
||||
monitor.status = doc["monitor.status"][0];
|
||||
monitor.ip = ip;
|
||||
|
@ -113,7 +113,7 @@ export const enrichMonitorGroups: MonitorEnricher = async (
|
|||
}
|
||||
}
|
||||
curCheck.monitor = monitor;
|
||||
|
||||
|
||||
if (curCheck.observer == null) {
|
||||
curCheck.observer = new HashMap();
|
||||
}
|
||||
|
@ -144,14 +144,14 @@ export const enrichMonitorGroups: MonitorEnricher = async (
|
|||
if (!doc["tls.certificate_not_valid_before"].isEmpty()) {
|
||||
curCheck.tls.certificate_not_valid_before = doc["tls.certificate_not_valid_before"][0];
|
||||
}
|
||||
|
||||
|
||||
state.checksByAgentIdIP[agentIdIP] = curCheck;
|
||||
`,
|
||||
combine_script: 'return state;',
|
||||
reduce_script: `
|
||||
// The final document
|
||||
Map result = new HashMap();
|
||||
|
||||
|
||||
Map checks = new HashMap();
|
||||
Instant maxTs = Instant.ofEpochMilli(0);
|
||||
Collection ips = new HashSet();
|
||||
|
@ -159,7 +159,7 @@ export const enrichMonitorGroups: MonitorEnricher = async (
|
|||
Collection podUids = new HashSet();
|
||||
Collection containerIds = new HashSet();
|
||||
Collection tls = new HashSet();
|
||||
String name = null;
|
||||
String name = null;
|
||||
for (state in states) {
|
||||
result.putAll(state.globals);
|
||||
for (entry in state.checksByAgentIdIP.entrySet()) {
|
||||
|
@ -167,18 +167,18 @@ export const enrichMonitorGroups: MonitorEnricher = async (
|
|||
def check = entry.getValue();
|
||||
def lastBestCheck = checks.get(agentIdIP);
|
||||
def checkTs = Instant.ofEpochMilli(check.get("@timestamp"));
|
||||
|
||||
|
||||
if (maxTs.isBefore(checkTs)) { maxTs = checkTs}
|
||||
|
||||
|
||||
if (lastBestCheck == null || lastBestCheck.get("@timestamp") < checkTs) {
|
||||
check["@timestamp"] = check["@timestamp"];
|
||||
checks[agentIdIP] = check
|
||||
}
|
||||
|
||||
|
||||
if (check.monitor.name != null && check.monitor.name != "") {
|
||||
name = check.monitor.name;
|
||||
}
|
||||
|
||||
|
||||
ips.add(check.monitor.ip);
|
||||
if (check.observer != null && check.observer.geo != null && check.observer.geo.name != null) {
|
||||
geoNames.add(check.observer.geo.name);
|
||||
|
@ -194,45 +194,45 @@ export const enrichMonitorGroups: MonitorEnricher = async (
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// We just use the values so we can store these as nested docs
|
||||
result.checks = checks.values();
|
||||
result.put("@timestamp", maxTs);
|
||||
|
||||
|
||||
|
||||
|
||||
Map summary = new HashMap();
|
||||
summary.up = checks.entrySet().stream().filter(c -> c.getValue().monitor.status == "up").count();
|
||||
summary.down = checks.size() - summary.up;
|
||||
result.summary = summary;
|
||||
|
||||
|
||||
Map monitor = new HashMap();
|
||||
monitor.ip = ips;
|
||||
monitor.name = name;
|
||||
monitor.status = summary.down > 0 ? (summary.up > 0 ? "mixed": "down") : "up";
|
||||
monitor.status = summary.down > 0 ? "down" : "up";
|
||||
result.monitor = monitor;
|
||||
|
||||
|
||||
Map observer = new HashMap();
|
||||
Map geo = new HashMap();
|
||||
observer.geo = geo;
|
||||
geo.name = geoNames;
|
||||
result.observer = observer;
|
||||
|
||||
|
||||
if (!podUids.isEmpty()) {
|
||||
result.kubernetes = new HashMap();
|
||||
result.kubernetes.pod = new HashMap();
|
||||
result.kubernetes.pod.uid = podUids;
|
||||
}
|
||||
|
||||
|
||||
if (!containerIds.isEmpty()) {
|
||||
result.container = new HashMap();
|
||||
result.container.id = containerIds;
|
||||
}
|
||||
|
||||
|
||||
if (!tls.isEmpty()) {
|
||||
result.tls = new HashMap();
|
||||
result.tls = tls;
|
||||
}
|
||||
|
||||
|
||||
return result;
|
||||
`,
|
||||
},
|
||||
|
|
|
@ -6,8 +6,8 @@
|
|||
|
||||
import { refinePotentialMatches } from './refine_potential_matches';
|
||||
import { findPotentialMatches } from './find_potential_matches';
|
||||
import { QueryContext } from '../elasticsearch_monitor_states_adapter';
|
||||
import { ChunkFetcher, ChunkResult } from './monitor_group_iterator';
|
||||
import { QueryContext } from './query_context';
|
||||
|
||||
/**
|
||||
* Fetches a single 'chunk' of data with a single query, then uses a secondary query to filter out erroneous matches.
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import { flatten } from 'lodash';
|
||||
import { CursorPagination } from '../adapter_types';
|
||||
import { QueryContext } from '../elasticsearch_monitor_states_adapter';
|
||||
import { QueryContext } from './query_context';
|
||||
import { QUERY } from '../../../../../common/constants';
|
||||
import { CursorDirection, MonitorSummary, SortOrder } from '../../../../../common/graphql/types';
|
||||
import { enrichMonitorGroups } from './enrich_monitor_groups';
|
||||
|
@ -51,6 +51,7 @@ const fetchPageMonitorGroups: MonitorGroupsFetcher = async (
|
|||
size: number
|
||||
): Promise<MonitorGroupsPage> => {
|
||||
const monitorGroups: MonitorGroups[] = [];
|
||||
|
||||
const iterator = new MonitorGroupIterator(queryContext);
|
||||
|
||||
let paginationBefore: CursorPagination | null = null;
|
||||
|
|
|
@ -5,10 +5,9 @@
|
|||
*/
|
||||
|
||||
import { get, set } from 'lodash';
|
||||
import { QueryContext } from '../elasticsearch_monitor_states_adapter';
|
||||
import { CursorDirection } from '../../../../../common/graphql/types';
|
||||
import { INDEX_NAMES } from '../../../../../common/constants';
|
||||
import { makeDateRangeFilter } from '../../../helper/make_date_rate_filter';
|
||||
import { QueryContext } from './query_context';
|
||||
|
||||
// This is the first phase of the query. In it, we find the most recent check groups that matched the given query.
|
||||
// Note that these check groups may not be the most recent groups for the matching monitor ID! We'll filter those
|
||||
|
@ -55,7 +54,7 @@ export const findPotentialMatches = async (
|
|||
};
|
||||
|
||||
const query = async (queryContext: QueryContext, searchAfter: any, size: number) => {
|
||||
const body = queryBody(queryContext, searchAfter, size);
|
||||
const body = await queryBody(queryContext, searchAfter, size);
|
||||
|
||||
const params = {
|
||||
index: INDEX_NAMES.HEARTBEAT,
|
||||
|
@ -65,15 +64,11 @@ const query = async (queryContext: QueryContext, searchAfter: any, size: number)
|
|||
return await queryContext.search(params);
|
||||
};
|
||||
|
||||
const queryBody = (queryContext: QueryContext, searchAfter: any, size: number) => {
|
||||
const queryBody = async (queryContext: QueryContext, searchAfter: any, size: number) => {
|
||||
const compositeOrder = cursorDirectionToOrder(queryContext.pagination.cursorDirection);
|
||||
|
||||
const filters: any[] = [
|
||||
makeDateRangeFilter(queryContext.dateRangeStart, queryContext.dateRangeEnd),
|
||||
];
|
||||
if (queryContext.filterClause) {
|
||||
filters.push(queryContext.filterClause);
|
||||
}
|
||||
const filters = await queryContext.dateAndCustomFilters();
|
||||
|
||||
if (queryContext.statusFilter) {
|
||||
filters.push({ match: { 'monitor.status': queryContext.statusFilter } });
|
||||
}
|
||||
|
@ -82,6 +77,11 @@ const queryBody = (queryContext: QueryContext, searchAfter: any, size: number) =
|
|||
size: 0,
|
||||
query: { bool: { filter: filters } },
|
||||
aggs: {
|
||||
has_timespan: {
|
||||
filter: {
|
||||
exists: { field: 'monitor.timespan' },
|
||||
},
|
||||
},
|
||||
monitors: {
|
||||
composite: {
|
||||
size,
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { QueryContext } from '../elasticsearch_monitor_states_adapter';
|
||||
import { QueryContext } from './query_context';
|
||||
import { CursorPagination } from '../adapter_types';
|
||||
import { fetchChunk } from './fetch_chunk';
|
||||
import { CursorDirection } from '../../../../../common/graphql/types';
|
||||
|
@ -155,7 +155,7 @@ export class MonitorGroupIterator {
|
|||
|
||||
// Returns a copy of this fetcher that goes backwards from the current position
|
||||
reverse(): MonitorGroupIterator | null {
|
||||
const reverseContext = Object.assign({}, this.queryContext);
|
||||
const reverseContext = this.queryContext.clone();
|
||||
const current = this.getCurrent();
|
||||
|
||||
reverseContext.pagination = {
|
||||
|
|
|
@ -0,0 +1,135 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import DateMath from '@elastic/datemath';
|
||||
import { APICaller } from 'kibana/server';
|
||||
import { CursorPagination } from '../adapter_types';
|
||||
import { INDEX_NAMES } from '../../../../../common/constants';
|
||||
|
||||
export class QueryContext {
|
||||
callES: APICaller;
|
||||
dateRangeStart: string;
|
||||
dateRangeEnd: string;
|
||||
pagination: CursorPagination;
|
||||
filterClause: any | null;
|
||||
size: number;
|
||||
statusFilter?: string;
|
||||
hasTimespanCache?: boolean;
|
||||
|
||||
constructor(
|
||||
database: any,
|
||||
dateRangeStart: string,
|
||||
dateRangeEnd: string,
|
||||
pagination: CursorPagination,
|
||||
filterClause: any | null,
|
||||
size: number,
|
||||
statusFilter?: string
|
||||
) {
|
||||
this.callES = database;
|
||||
this.dateRangeStart = dateRangeStart;
|
||||
this.dateRangeEnd = dateRangeEnd;
|
||||
this.pagination = pagination;
|
||||
this.filterClause = filterClause;
|
||||
this.size = size;
|
||||
this.statusFilter = statusFilter;
|
||||
}
|
||||
|
||||
async search(params: any): Promise<any> {
|
||||
params.index = INDEX_NAMES.HEARTBEAT;
|
||||
return this.callES('search', params);
|
||||
}
|
||||
|
||||
async count(params: any): Promise<any> {
|
||||
params.index = INDEX_NAMES.HEARTBEAT;
|
||||
return this.callES('count', params);
|
||||
}
|
||||
|
||||
async dateAndCustomFilters(): Promise<any[]> {
|
||||
const clauses = [await this.dateRangeFilter()];
|
||||
if (this.filterClause) {
|
||||
clauses.push(this.filterClause);
|
||||
}
|
||||
return clauses;
|
||||
}
|
||||
|
||||
async dateRangeFilter(forceNoTimespan?: boolean): Promise<any> {
|
||||
const timestampClause = {
|
||||
range: { '@timestamp': { gte: this.dateRangeStart, lte: this.dateRangeEnd } },
|
||||
};
|
||||
|
||||
if (forceNoTimespan === true || !(await this.hasTimespan())) {
|
||||
return timestampClause;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
const tsStart = DateMath.parse(this.dateRangeEnd).subtract(10, 'seconds');
|
||||
const tsEnd = DateMath.parse(this.dateRangeEnd)!;
|
||||
|
||||
return {
|
||||
bool: {
|
||||
filter: [
|
||||
timestampClause,
|
||||
{
|
||||
bool: {
|
||||
should: [
|
||||
{
|
||||
range: {
|
||||
'monitor.timespan': {
|
||||
gte: tsStart.toISOString(),
|
||||
lte: tsEnd.toISOString(),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
bool: {
|
||||
must_not: { exists: { field: 'monitor.timespan' } },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async hasTimespan(): Promise<boolean> {
|
||||
if (this.hasTimespanCache) {
|
||||
return this.hasTimespanCache;
|
||||
}
|
||||
|
||||
this.hasTimespanCache =
|
||||
(
|
||||
await this.count({
|
||||
body: {
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
await this.dateRangeFilter(true),
|
||||
{ exists: { field: 'monitor.timespan' } },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
terminate_after: 1,
|
||||
})
|
||||
).count > 0;
|
||||
|
||||
return this.hasTimespanCache;
|
||||
}
|
||||
|
||||
clone(): QueryContext {
|
||||
return new QueryContext(
|
||||
this.callES,
|
||||
this.dateRangeStart,
|
||||
this.dateRangeEnd,
|
||||
this.pagination,
|
||||
this.filterClause,
|
||||
this.size,
|
||||
this.statusFilter
|
||||
);
|
||||
}
|
||||
}
|
|
@ -5,10 +5,9 @@
|
|||
*/
|
||||
|
||||
import { INDEX_NAMES } from '../../../../../common/constants';
|
||||
import { QueryContext } from '../elasticsearch_monitor_states_adapter';
|
||||
import { QueryContext } from './query_context';
|
||||
import { CursorDirection } from '../../../../../common/graphql/types';
|
||||
import { MonitorGroups, MonitorLocCheckGroup } from './fetch_page';
|
||||
import { makeDateRangeFilter } from '../../../helper/make_date_rate_filter';
|
||||
|
||||
/**
|
||||
* Determines whether the provided check groups are the latest complete check groups for their associated monitor ID's.
|
||||
|
@ -103,7 +102,7 @@ export const mostRecentCheckGroups = async (
|
|||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
makeDateRangeFilter(queryContext.dateRangeStart, queryContext.dateRangeEnd),
|
||||
await queryContext.dateRangeFilter(),
|
||||
{ terms: { 'monitor.id': potentialMatchMonitorIDs } },
|
||||
// only match summary docs because we only want the latest *complete* check group.
|
||||
{ exists: { field: 'summary' } },
|
||||
|
|
|
@ -11824,9 +11824,9 @@
|
|||
"xpack.uptime.pingList.expandRow": "拡張",
|
||||
"xpack.uptime.snapshot.pingsOverTimeTitle": "一定時間のピング",
|
||||
"xpack.uptime.snapshotHistogram.yAxis.title": "ピング",
|
||||
"xpack.uptime.donutChart.ariaLabel": "現在のステータスを表す円グラフ、{total} 個中 {down} 個のモニターがダウンしています。",
|
||||
"xpack.uptime.donutChart.legend.downRowLabel": "ダウン",
|
||||
"xpack.uptime.donutChart.legend.upRowLabel": "アップ",
|
||||
"xpack.uptime.snapshot.donutChart.ariaLabel": "現在のステータスを表す円グラフ、{total} 個中 {down} 個のモニターがダウンしています。",
|
||||
"xpack.uptime.snapshot.donutChart.legend.downRowLabel": "ダウン",
|
||||
"xpack.uptime.snapshot.donutChart.legend.upRowLabel": "アップ",
|
||||
"xpack.uptime.durationChart.emptyPrompt.description": "このモニターは選択された時間範囲で一度も {emphasizedText} していません。",
|
||||
"xpack.uptime.durationChart.emptyPrompt.title": "利用可能な期間データがありません",
|
||||
"xpack.uptime.emptyStateError.notAuthorized": "アップタイムデータの表示が承認されていません。システム管理者にお問い合わせください。",
|
||||
|
|
|
@ -11852,9 +11852,9 @@
|
|||
"xpack.uptime.pingList.expandRow": "展开",
|
||||
"xpack.uptime.snapshot.pingsOverTimeTitle": "时移 Ping 数",
|
||||
"xpack.uptime.snapshotHistogram.yAxis.title": "Ping",
|
||||
"xpack.uptime.donutChart.ariaLabel": "显示当前状态的饼图。{down} 个监测已关闭,共 {total} 个。",
|
||||
"xpack.uptime.donutChart.legend.downRowLabel": "关闭",
|
||||
"xpack.uptime.donutChart.legend.upRowLabel": "运行",
|
||||
"xpack.uptime.snapshot.donutChart.ariaLabel": "显示当前状态的饼图。{down} 个监测已关闭,共 {total} 个。",
|
||||
"xpack.uptime.snapshot.donutChart.legend.downRowLabel": "关闭",
|
||||
"xpack.uptime.snapshot.donutChart.legend.upRowLabel": "运行",
|
||||
"xpack.uptime.durationChart.emptyPrompt.description": "在选定时间范围内此监测从未{emphasizedText}。",
|
||||
"xpack.uptime.durationChart.emptyPrompt.title": "没有持续时间数据",
|
||||
"xpack.uptime.emptyStateError.notAuthorized": "您无权查看 Uptime 数据,请联系系统管理员。",
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
{
|
||||
"up": 93,
|
||||
"up": 10,
|
||||
"down": 7,
|
||||
"mixed": 0,
|
||||
"total": 100
|
||||
"total": 17
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
{
|
||||
"up": 0,
|
||||
"down": 7,
|
||||
"mixed": 0,
|
||||
"total": 7
|
||||
"down": 0,
|
||||
"total": 0
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
{
|
||||
"up": 0,
|
||||
"down": 7,
|
||||
"mixed": 0,
|
||||
"total": 7
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
{
|
||||
"up": 93,
|
||||
"up": 10,
|
||||
"down": 0,
|
||||
"mixed": 0,
|
||||
"total": 93
|
||||
"total": 10
|
||||
}
|
|
@ -5,11 +5,12 @@
|
|||
*/
|
||||
|
||||
import uuid from 'uuid';
|
||||
import { merge } from 'lodash';
|
||||
import { merge, flattenDeep } from 'lodash';
|
||||
|
||||
const INDEX_NAME = 'heartbeat-8.0.0';
|
||||
|
||||
export const makePing = async (
|
||||
es: any,
|
||||
index: string,
|
||||
monitorId: string,
|
||||
fields: { [key: string]: any },
|
||||
mogrify: (doc: any) => any
|
||||
|
@ -101,7 +102,7 @@ export const makePing = async (
|
|||
const doc = mogrify(merge(baseDoc, fields));
|
||||
|
||||
await es.index({
|
||||
index,
|
||||
index: INDEX_NAME,
|
||||
refresh: true,
|
||||
body: doc,
|
||||
});
|
||||
|
@ -111,7 +112,6 @@ export const makePing = async (
|
|||
|
||||
export const makeCheck = async (
|
||||
es: any,
|
||||
index: string,
|
||||
monitorId: string,
|
||||
numIps: number,
|
||||
fields: { [key: string]: any },
|
||||
|
@ -137,7 +137,7 @@ export const makeCheck = async (
|
|||
if (i === numIps - 1) {
|
||||
pingFields.summary = summary;
|
||||
}
|
||||
const doc = await makePing(es, index, monitorId, pingFields, mogrify);
|
||||
const doc = await makePing(es, monitorId, pingFields, mogrify);
|
||||
docs.push(doc);
|
||||
// @ts-ignore
|
||||
summary[doc.monitor.status]++;
|
||||
|
@ -147,16 +147,78 @@ export const makeCheck = async (
|
|||
|
||||
export const makeChecks = async (
|
||||
es: any,
|
||||
index: string,
|
||||
monitorId: string,
|
||||
numChecks: number,
|
||||
numIps: number,
|
||||
every: number, // number of millis between checks
|
||||
fields: { [key: string]: any } = {},
|
||||
mogrify: (doc: any) => any = d => d
|
||||
) => {
|
||||
const checks = [];
|
||||
const oldestTime = new Date().getTime() - numChecks * every;
|
||||
let newestTime = oldestTime;
|
||||
for (let li = 0; li < numChecks; li++) {
|
||||
checks.push(await makeCheck(es, index, monitorId, numIps, fields, mogrify));
|
||||
const checkDate = new Date(newestTime + every);
|
||||
newestTime = checkDate.getTime() + every;
|
||||
fields = merge(fields, {
|
||||
'@timestamp': checkDate.toISOString(),
|
||||
monitor: {
|
||||
timespan: {
|
||||
gte: checkDate.toISOString(),
|
||||
lt: new Date(newestTime).toISOString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
checks.push(await makeCheck(es, monitorId, numIps, fields, mogrify));
|
||||
}
|
||||
|
||||
return checks;
|
||||
};
|
||||
|
||||
export const makeChecksWithStatus = async (
|
||||
es: any,
|
||||
monitorId: string,
|
||||
numChecks: number,
|
||||
numIps: number,
|
||||
every: number,
|
||||
fields: { [key: string]: any } = {},
|
||||
status: 'up' | 'down',
|
||||
mogrify: (doc: any) => any = d => d
|
||||
) => {
|
||||
const oppositeStatus = status === 'up' ? 'down' : 'up';
|
||||
|
||||
return await makeChecks(es, monitorId, numChecks, numIps, every, fields, d => {
|
||||
d.monitor.status = status;
|
||||
if (d.summary) {
|
||||
d.summary[status] += d.summary[oppositeStatus];
|
||||
d.summary[oppositeStatus] = 0;
|
||||
}
|
||||
|
||||
return mogrify(d);
|
||||
});
|
||||
};
|
||||
|
||||
// Helper for processing a list of checks to find the time picker bounds.
|
||||
export const getChecksDateRange = (checks: any[]) => {
|
||||
// Flatten 2d arrays
|
||||
const flattened = flattenDeep(checks);
|
||||
|
||||
let startTime = 1 / 0;
|
||||
let endTime = -1 / 0;
|
||||
flattened.forEach(c => {
|
||||
const ts = Date.parse(c['@timestamp']);
|
||||
|
||||
if (ts < startTime) {
|
||||
startTime = ts;
|
||||
}
|
||||
|
||||
if (ts > endTime) {
|
||||
endTime = ts;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
start: new Date(startTime).toISOString(),
|
||||
end: new Date(endTime).toISOString(),
|
||||
};
|
||||
};
|
||||
|
|
|
@ -8,7 +8,7 @@ import expect from '@kbn/expect';
|
|||
import { monitorStatesQueryString } from '../../../../../legacy/plugins/uptime/public/queries/monitor_states_query';
|
||||
import { expectFixtureEql } from './helpers/expect_fixture_eql';
|
||||
import { FtrProviderContext } from '../../../ftr_provider_context';
|
||||
import { makeChecks } from './helpers/make_checks';
|
||||
import { makeChecksWithStatus } from './helpers/make_checks';
|
||||
|
||||
export default function({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
|
@ -104,11 +104,10 @@ export default function({ getService }: FtrProviderContext) {
|
|||
};
|
||||
|
||||
before(async () => {
|
||||
const index = 'heartbeat-8.0.0';
|
||||
|
||||
const es = getService('legacyEs');
|
||||
dateRangeStart = new Date().toISOString();
|
||||
checks = await makeChecks(es, index, testMonitorId, 1, numIps, {}, d => {
|
||||
checks = await makeChecksWithStatus(es, testMonitorId, 1, numIps, 1, {}, 'up', d => {
|
||||
// turn an all up status into having at least one down
|
||||
if (d.summary) {
|
||||
d.monitor.status = 'down';
|
||||
d.summary.up--;
|
||||
|
|
|
@ -9,8 +9,8 @@ import { FtrProviderContext } from '../../../ftr_provider_context';
|
|||
export default function({ getService, loadTestFile }: FtrProviderContext) {
|
||||
const esArchiver = getService('esArchiver');
|
||||
describe('uptime REST endpoints', () => {
|
||||
before('load heartbeat data', () => esArchiver.load('uptime/full_heartbeat'));
|
||||
after('unload', () => esArchiver.unload('uptime/full_heartbeat'));
|
||||
before('load heartbeat data', () => esArchiver.load('uptime/blank'));
|
||||
after('unload', () => esArchiver.unload('uptime/blank'));
|
||||
loadTestFile(require.resolve('./snapshot'));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -6,47 +6,97 @@
|
|||
|
||||
import { expectFixtureEql } from '../graphql/helpers/expect_fixture_eql';
|
||||
import { FtrProviderContext } from '../../../ftr_provider_context';
|
||||
import { makeChecksWithStatus, getChecksDateRange } from '../graphql/helpers/make_checks';
|
||||
|
||||
export default function({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
|
||||
describe('snapshot count', () => {
|
||||
let dateRangeStart = '2019-01-28T17:40:08.078Z';
|
||||
let dateRangeEnd = '2025-01-28T19:00:16.078Z';
|
||||
const dateRangeStart = new Date().toISOString();
|
||||
const dateRangeEnd = new Date().toISOString();
|
||||
|
||||
it('will fetch the full set of snapshot counts', async () => {
|
||||
const apiResponse = await supertest.get(
|
||||
`/api/uptime/snapshot/count?dateRangeStart=${dateRangeStart}&dateRangeEnd=${dateRangeEnd}`
|
||||
);
|
||||
expectFixtureEql(apiResponse.body, 'snapshot');
|
||||
describe('when no data is present', async () => {
|
||||
it('returns a null snapshot', async () => {
|
||||
const apiResponse = await supertest.get(
|
||||
`/api/uptime/snapshot/count?dateRangeStart=${dateRangeStart}&dateRangeEnd=${dateRangeEnd}`
|
||||
);
|
||||
|
||||
expectFixtureEql(apiResponse.body, 'snapshot_empty');
|
||||
});
|
||||
});
|
||||
|
||||
it('will fetch a monitor snapshot filtered by down status', async () => {
|
||||
const filters = `{"bool":{"must":[{"match":{"monitor.status":{"query":"down","operator":"and"}}}]}}`;
|
||||
const statusFilter = 'down';
|
||||
const apiResponse = await supertest.get(
|
||||
`/api/uptime/snapshot/count?dateRangeStart=${dateRangeStart}&dateRangeEnd=${dateRangeEnd}&filters=${filters}&statusFilter=${statusFilter}`
|
||||
);
|
||||
expectFixtureEql(apiResponse.body, 'snapshot_filtered_by_down');
|
||||
});
|
||||
describe('when data is present', async () => {
|
||||
const numUpMonitors = 10;
|
||||
const numDownMonitors = 7;
|
||||
const numIps = 2;
|
||||
const checksPerMonitor = 5;
|
||||
const scheduleEvery = 10000; // fake monitor checks every 10s
|
||||
let dateRange: { start: string; end: string };
|
||||
|
||||
it('will fetch a monitor snapshot filtered by up status', async () => {
|
||||
const filters = `{"bool":{"must":[{"match":{"monitor.status":{"query":"up","operator":"and"}}}]}}`;
|
||||
const statusFilter = 'up';
|
||||
const apiResponse = await supertest.get(
|
||||
`/api/uptime/snapshot/count?dateRangeStart=${dateRangeStart}&dateRangeEnd=${dateRangeEnd}&filters=${filters}&statusFilter=${statusFilter}`
|
||||
);
|
||||
expectFixtureEql(apiResponse.body, 'snapshot_filtered_by_up');
|
||||
});
|
||||
[true, false].forEach(async (includeTimespan: boolean) => {
|
||||
describe(`with timespans ${includeTimespan ? 'included' : 'missing'}`, async () => {
|
||||
before(async () => {
|
||||
const promises: Array<Promise<any>> = [];
|
||||
|
||||
it('returns a null snapshot when no data is present', async () => {
|
||||
dateRangeStart = '2019-01-25T04:30:54.740Z';
|
||||
dateRangeEnd = '2025-01-28T04:50:54.740Z';
|
||||
const filters = `{"bool":{"must":[{"match":{"monitor.status":{"query":"down","operator":"and"}}}]}}`;
|
||||
const apiResponse = await supertest.get(
|
||||
`/api/uptime/snapshot/count?dateRangeStart=${dateRangeStart}&dateRangeEnd=${dateRangeEnd}&filters=${filters}`
|
||||
);
|
||||
expectFixtureEql(apiResponse.body, 'snapshot_empty');
|
||||
// When includeTimespan is false we have to remove the values there.
|
||||
let mogrify = (d: any) => d;
|
||||
if ((includeTimespan = false)) {
|
||||
mogrify = (d: any): any => {
|
||||
d.monitor.delete('timespan');
|
||||
return d;
|
||||
};
|
||||
}
|
||||
|
||||
const makeMonitorChecks = async (monitorId: string, status: 'up' | 'down') => {
|
||||
return makeChecksWithStatus(
|
||||
getService('legacyEs'),
|
||||
monitorId,
|
||||
checksPerMonitor,
|
||||
numIps,
|
||||
scheduleEvery,
|
||||
{},
|
||||
status,
|
||||
mogrify
|
||||
);
|
||||
};
|
||||
|
||||
for (let i = 0; i < numUpMonitors; i++) {
|
||||
promises.push(makeMonitorChecks(`up-${i}`, 'up'));
|
||||
}
|
||||
for (let i = 0; i < numDownMonitors; i++) {
|
||||
promises.push(makeMonitorChecks(`down-${i}`, 'down'));
|
||||
}
|
||||
|
||||
const allResults = await Promise.all(promises);
|
||||
dateRange = getChecksDateRange(allResults);
|
||||
});
|
||||
|
||||
it('will count all statuses correctly', async () => {
|
||||
const apiResponse = await supertest.get(
|
||||
`/api/uptime/snapshot/count?dateRangeStart=${dateRange.start}&dateRangeEnd=${dateRange.end}`
|
||||
);
|
||||
|
||||
expectFixtureEql(apiResponse.body, 'snapshot');
|
||||
});
|
||||
|
||||
it('will fetch a monitor snapshot filtered by down status', async () => {
|
||||
const statusFilter = 'down';
|
||||
const apiResponse = await supertest.get(
|
||||
`/api/uptime/snapshot/count?dateRangeStart=${dateRange.start}&dateRangeEnd=${dateRange.end}&statusFilter=${statusFilter}`
|
||||
);
|
||||
|
||||
expectFixtureEql(apiResponse.body, 'snapshot_filtered_by_down');
|
||||
});
|
||||
|
||||
it('will fetch a monitor snapshot filtered by up status', async () => {
|
||||
const statusFilter = 'up';
|
||||
const apiResponse = await supertest.get(
|
||||
`/api/uptime/snapshot/count?dateRangeStart=${dateRange.start}&dateRangeEnd=${dateRange.end}&statusFilter=${statusFilter}`
|
||||
);
|
||||
expectFixtureEql(apiResponse.body, 'snapshot_filtered_by_up');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -64,5 +64,27 @@ export default ({ getPageObjects }: FtrProviderContext) => {
|
|||
'0009-up',
|
||||
]);
|
||||
});
|
||||
|
||||
describe('snapshot counts', () => {
|
||||
it('updates the snapshot count when status filter is set to down', async () => {
|
||||
await pageObjects.uptime.goToUptimePageAndSetDateRange(
|
||||
DEFAULT_DATE_START,
|
||||
DEFAULT_DATE_END
|
||||
);
|
||||
await pageObjects.uptime.setStatusFilter('down');
|
||||
const counts = await pageObjects.uptime.getSnapshotCount();
|
||||
expect(counts).to.eql({ up: '0', down: '7' });
|
||||
});
|
||||
|
||||
it('updates the snapshot count when status filter is set to up', async () => {
|
||||
await pageObjects.uptime.goToUptimePageAndSetDateRange(
|
||||
DEFAULT_DATE_START,
|
||||
DEFAULT_DATE_END
|
||||
);
|
||||
await pageObjects.uptime.setStatusFilter('up');
|
||||
const counts = await pageObjects.uptime.getSnapshotCount();
|
||||
expect(counts).to.eql({ up: '93', down: '0' });
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
|
|
@ -1267,6 +1267,9 @@
|
|||
"type": {
|
||||
"ignore_above": 1024,
|
||||
"type": "keyword"
|
||||
},
|
||||
"timespan": {
|
||||
"type": "date_range"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -70,5 +70,9 @@ export function UptimePageProvider({ getPageObjects, getService }: FtrProviderCo
|
|||
await uptimeService.setStatusFilterDown();
|
||||
}
|
||||
}
|
||||
|
||||
public async getSnapshotCount() {
|
||||
return await uptimeService.getSnapshotCount();
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
|
|
@ -49,5 +49,11 @@ export function UptimeProvider({ getService }: FtrProviderContext) {
|
|||
async setStatusFilterDown() {
|
||||
await testSubjects.click('xpack.uptime.filterBar.filterStatusDown');
|
||||
},
|
||||
async getSnapshotCount() {
|
||||
return {
|
||||
up: await testSubjects.getVisibleText('xpack.uptime.snapshot.donutChart.up'),
|
||||
down: await testSubjects.getVisibleText('xpack.uptime.snapshot.donutChart.down'),
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue