From 5085b62c9c2139526d4f4d6f7d30a75d0a26cea5 Mon Sep 17 00:00:00 2001 From: Andrew Cholakian Date: Thu, 9 Jul 2020 08:51:55 -0500 Subject: [PATCH] [Uptime] Remove Scripted Metric Query (#69229) Resolves #68096 , removing unnecessary scripted metric query from overview page and unifying the Check and Ping types. This simplifies the types in a number of ways, and reduces the total quantity of code to execute the queries for the overview page. It also fixes the Tls and related types which were inconsistent and presented a problem here since they are used by this JS. There are now three stages where before there were four: Find potential matches: where we determine which monitor IDs are eligible for the overview page Refine potential matches: where we determine which ones actually match and return the summary documents for each location to build the MonitorSummary object Get monitor histograms: where we calculate the histograms for each monitor. In the future we might make this a separate API call. This improves the overall code structure, and leaves the test coverage about the same depending on how you look at it. I think we can do more to improve the quality of code / tests here, but this seemed like a good initial place to draw the line for now. In perfunctory testing on our internal observability clusters I saw perf improve from 2.5s to 1.1s on the Uptime homepage with no filters. So, it looks like there are potentially perf improvements (no real benchmarking was done). Previously, this returned all pings from the latest check group. This was not actually used anywhere, only the summary pings are required for the current UI, so we now only return those from the list API as this saves a query. --- .../common/runtime_types/monitor/state.ts | 91 +- .../uptime/common/runtime_types/ping/ping.ts | 71 +- .../ssl_certificate.test.tsx.snap | 3 +- .../__test__/ssl_certificate.test.tsx | 19 +- .../status_bar/ssl_certificate.tsx | 22 +- .../__snapshots__/monitor_list.test.tsx.snap | 978 ++++++++++++------ .../__tests__/monitor_list.test.tsx | 216 ++-- .../monitor_list_status_column.test.tsx | 84 +- .../monitor_list/cert_status_column.tsx | 13 +- .../overview/monitor_list/monitor_list.tsx | 16 +- .../monitor_list_drawer.test.tsx.snap | 103 +- .../monitor_list_drawer/__tests__/data.json | 2 +- .../__tests__/integration_group.test.tsx | 33 +- .../__tests__/monitor_list_drawer.test.tsx | 61 +- .../__tests__/monitor_status_list.test.tsx | 58 +- .../actions_popover/integration_group.tsx | 6 +- .../monitor_list_drawer.tsx | 4 +- .../monitor_status_list.tsx | 18 +- .../monitor_list_status_column.tsx | 32 +- .../__tests__/get_apm_href.test.ts | 29 +- .../__tests__/get_infra_href.test.ts | 202 ++-- .../__tests__/get_logging_href.test.ts | 50 +- .../observability_integration/build_href.ts | 8 +- .../get_infra_href.ts | 6 +- .../get_logging_href.ts | 6 +- .../public/state/actions/monitor_list.ts | 6 +- .../uptime/public/state/api/monitor_list.ts | 8 +- .../plugins/uptime/public/state/api/utils.ts | 30 +- .../public/state/reducers/monitor_list.ts | 8 +- .../__tests__/get_latest_monitor.test.ts | 14 +- .../server/lib/requests/get_latest_monitor.ts | 14 +- .../server/lib/requests/get_monitor_states.ts | 123 ++- .../__tests__/enrich_monitor_groups.test.ts | 98 -- .../search/__tests__/fetch_page.test.ts | 111 -- .../__tests__/monitor_group_iterator.test.ts | 100 -- .../monitor_summary_iterator.test.ts | 131 +++ .../refine_potential_matches.test.ts | 112 -- .../requests/search/enrich_monitor_groups.ts | 409 -------- .../server/lib/requests/search/fetch_chunk.ts | 7 +- .../server/lib/requests/search/fetch_page.ts | 138 --- .../requests/search/find_potential_matches.ts | 9 +- .../server/lib/requests/search/index.ts | 3 +- ...terator.ts => monitor_summary_iterator.ts} | 105 +- .../lib/requests/search/query_context.ts | 18 + .../search/refine_potential_matches.ts | 118 +-- .../server/lib/requests/uptime_requests.ts | 4 +- .../rest/fixtures/monitor_latest_status.json | 5 +- .../uptime/rest/monitor_states_generated.ts | 12 - .../uptime/rest/monitor_states_real_data.ts | 4 +- 49 files changed, 1727 insertions(+), 1991 deletions(-) delete mode 100644 x-pack/plugins/uptime/server/lib/requests/search/__tests__/enrich_monitor_groups.test.ts delete mode 100644 x-pack/plugins/uptime/server/lib/requests/search/__tests__/fetch_page.test.ts delete mode 100644 x-pack/plugins/uptime/server/lib/requests/search/__tests__/monitor_group_iterator.test.ts create mode 100644 x-pack/plugins/uptime/server/lib/requests/search/__tests__/monitor_summary_iterator.test.ts delete mode 100644 x-pack/plugins/uptime/server/lib/requests/search/__tests__/refine_potential_matches.test.ts delete mode 100644 x-pack/plugins/uptime/server/lib/requests/search/enrich_monitor_groups.ts delete mode 100644 x-pack/plugins/uptime/server/lib/requests/search/fetch_page.ts rename x-pack/plugins/uptime/server/lib/requests/search/{monitor_group_iterator.ts => monitor_summary_iterator.ts} (61%) diff --git a/x-pack/plugins/uptime/common/runtime_types/monitor/state.ts b/x-pack/plugins/uptime/common/runtime_types/monitor/state.ts index 7fad5051919c..edbaacd72504 100644 --- a/x-pack/plugins/uptime/common/runtime_types/monitor/state.ts +++ b/x-pack/plugins/uptime/common/runtime_types/monitor/state.ts @@ -5,68 +5,9 @@ */ import * as t from 'io-ts'; - -export const CheckMonitorType = t.intersection([ - t.partial({ - name: t.string, - ip: t.union([t.array(t.union([t.string, t.null])), t.string, t.null]), - }), - t.type({ - status: t.string, - }), -]); - -export const CheckType = t.intersection([ - t.partial({ - agent: t.partial({ - id: t.string, - }), - container: t.type({ - id: t.string, - }), - kubernetes: t.type({ - pod: t.type({ - uid: t.string, - }), - }), - observer: t.type({ - geo: t.partial({ - name: t.string, - location: t.partial({ - lat: t.number, - lon: t.number, - }), - }), - }), - }), - t.type({ - monitor: CheckMonitorType, - timestamp: t.number, - }), -]); - -export type Check = t.TypeOf; +import { PingType } from '../ping/ping'; export const StateType = t.intersection([ - t.partial({ - checks: t.array(CheckType), - observer: t.partial({ - geo: t.partial({ - name: t.array(t.string), - }), - }), - summary: t.partial({ - up: t.number, - down: t.number, - geo: t.partial({ - name: t.string, - location: t.partial({ - lat: t.number, - lon: t.number, - }), - }), - }), - }), t.type({ timestamp: t.string, url: t.partial({ @@ -76,9 +17,31 @@ export const StateType = t.intersection([ port: t.number, scheme: t.string, }), + summaryPings: t.array(PingType), + summary: t.partial({ + status: t.string, + up: t.number, + down: t.number, + }), + monitor: t.partial({ + name: t.string, + }), + }), + t.partial({ + tls: t.partial({ + not_after: t.union([t.string, t.null]), + not_before: t.union([t.string, t.null]), + }), + observer: t.type({ + geo: t.type({ + name: t.array(t.string), + }), + }), }), ]); +export type MonitorSummaryState = t.TypeOf; + export const HistogramPointType = t.type({ timestamp: t.number, up: t.union([t.number, t.undefined]), @@ -105,18 +68,18 @@ export const MonitorSummaryType = t.intersection([ export type MonitorSummary = t.TypeOf; -export const MonitorSummaryResultType = t.intersection([ +export const MonitorSummariesResultType = t.intersection([ t.partial({ - summaries: t.array(MonitorSummaryType), + totalSummaryCount: t.number, }), t.type({ + summaries: t.array(MonitorSummaryType), prevPagePagination: t.union([t.string, t.null]), nextPagePagination: t.union([t.string, t.null]), - totalSummaryCount: t.number, }), ]); -export type MonitorSummaryResult = t.TypeOf; +export type MonitorSummariesResult = t.TypeOf; export const FetchMonitorStatesQueryArgsType = t.intersection([ t.partial({ diff --git a/x-pack/plugins/uptime/common/runtime_types/ping/ping.ts b/x-pack/plugins/uptime/common/runtime_types/ping/ping.ts index ab539b38c3e4..d037b4da882a 100644 --- a/x-pack/plugins/uptime/common/runtime_types/ping/ping.ts +++ b/x-pack/plugins/uptime/common/runtime_types/ping/ping.ts @@ -16,11 +16,51 @@ export const HttpResponseBodyType = t.partial({ export type HttpResponseBody = t.TypeOf; -export const TlsType = t.partial({ +const ECSDistinguishedName = t.type({ + common_name: t.string, + distinguished_name: t.string, +}); + +export const X509ExpiryType = t.type({ not_after: t.string, not_before: t.string, }); +export type X509Expiry = t.TypeOf; + +export const X509Type = t.intersection([ + t.type({ + issuer: ECSDistinguishedName, + subject: ECSDistinguishedName, + serial_number: t.string, + public_key_algorithm: t.string, + signature_algorithm: t.string, + }), + X509ExpiryType, + t.partial({ + public_key_curve: t.string, + public_key_exponent: t.number, + public_key_size: t.number, + }), +]); + +export type X509 = t.TypeOf; + +export const TlsType = t.partial({ + // deprecated in favor of server.x509.not_after/not_before + certificate_not_valid_after: t.string, + certificate_not_valid_before: t.string, + cipher: t.string, + established: t.boolean, + server: t.partial({ + hash: t.type({ + sha256: t.string, + sha1: t.string, + }), + x509: X509Type, + }), +}); + export type Tls = t.TypeOf; export const MonitorType = t.intersection([ @@ -123,6 +163,11 @@ export const PingType = t.intersection([ observer: t.partial({ geo: t.partial({ name: t.string, + location: t.union([ + t.string, + t.partial({ lat: t.number, lon: t.number }), + t.partial({ lat: t.string, lon: t.string }), + ]), }), }), resolve: t.partial({ @@ -156,6 +201,30 @@ export const PingType = t.intersection([ export type Ping = t.TypeOf; +// Convenience function for tests etc that makes an empty ping +// object with the minimum of fields. +export const makePing = (f: { + docId?: string; + type?: string; + id?: string; + timestamp?: string; + ip?: string; + status?: string; + duration?: number; +}): Ping => { + return { + docId: f.docId || 'myDocId', + timestamp: f.timestamp || '2020-07-07T01:14:08Z', + monitor: { + id: f.id || 'myId', + type: f.type || 'myType', + ip: f.ip || '127.0.0.1', + status: f.status || 'up', + duration: { us: f.duration || 100000 }, + }, + }; +}; + export const PingsResponseType = t.type({ total: t.number, locations: t.array(t.string), diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/__test__/__snapshots__/ssl_certificate.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/status_details/__test__/__snapshots__/ssl_certificate.test.tsx.snap index 591b3f222d03..64802ad383b7 100644 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/__test__/__snapshots__/ssl_certificate.test.tsx.snap +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/__test__/__snapshots__/ssl_certificate.test.tsx.snap @@ -176,7 +176,8 @@ exports[`SSL Certificate component shallow renders 1`] = ` diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/__test__/ssl_certificate.test.tsx b/x-pack/plugins/uptime/public/components/monitor/status_details/__test__/ssl_certificate.test.tsx index a4b360ea690d..01652044e7a5 100644 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/__test__/ssl_certificate.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/__test__/ssl_certificate.test.tsx @@ -15,12 +15,13 @@ import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../../common/constants'; describe('SSL Certificate component', () => { let monitorTls: Tls; + const dateInTwoMonths = moment().add(2, 'month').toString(); + const yesterday = moment().subtract(1, 'day').toString(); beforeEach(() => { - const dateInTwoMonths = moment().add(2, 'month').toString(); - monitorTls = { - not_after: dateInTwoMonths, + certificate_not_valid_after: dateInTwoMonths, + certificate_not_valid_before: yesterday, }; const useDispatchSpy = jest.spyOn(redux, 'useDispatch'); @@ -32,7 +33,8 @@ describe('SSL Certificate component', () => { it('shallow renders', () => { const monitorTls1 = { - not_after: '2020-04-24T11:41:38.200Z', + certificate_not_valid_after: '2020-04-24T11:41:38.200Z', + certificate_not_valid_before: '2019-04-24T11:41:38.200Z', }; const component = shallowWithRouter(); expect(component).toMatchSnapshot(); @@ -45,7 +47,8 @@ describe('SSL Certificate component', () => { it('renders null if invalid date', () => { monitorTls = { - not_after: 'i am so invalid date', + certificate_not_valid_after: 'i am so invalid date', + certificate_not_valid_before: 'i am so invalid date', }; const component = renderWithRouter(); expect(component).toMatchSnapshot(); @@ -54,7 +57,8 @@ describe('SSL Certificate component', () => { it('renders expiration date with a warning state if ssl expiry date is less than 5 days', () => { const dateIn5Days = moment().add(5, 'day').toString(); monitorTls = { - not_after: dateIn5Days, + certificate_not_valid_after: dateIn5Days, + certificate_not_valid_before: yesterday, }; const component = mountWithRouter(); @@ -69,7 +73,8 @@ describe('SSL Certificate component', () => { it('does not render the expiration date with a warning state if expiry date is greater than a month', () => { const dateIn40Days = moment().add(40, 'day').toString(); monitorTls = { - not_after: dateIn40Days, + certificate_not_valid_after: dateIn40Days, + certificate_not_valid_before: yesterday, }; const component = mountWithRouter(); diff --git a/x-pack/plugins/uptime/public/components/monitor/status_details/status_bar/ssl_certificate.tsx b/x-pack/plugins/uptime/public/components/monitor/status_details/status_bar/ssl_certificate.tsx index 93720ab313ee..ffe4f5d759e0 100644 --- a/x-pack/plugins/uptime/public/components/monitor/status_details/status_bar/ssl_certificate.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/status_details/status_bar/ssl_certificate.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { Link } from 'react-router-dom'; import { EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { Tls } from '../../../../../common/runtime_types'; +import { Tls, X509Expiry } from '../../../../../common/runtime_types'; import { CERTIFICATES_ROUTE } from '../../../../../common/constants'; import { MonListDescription, MonListTitle } from './status_bar'; import { CertStatusColumn } from '../../../overview/monitor_list/cert_status_column'; @@ -21,7 +21,21 @@ interface Props { } export const MonitorSSLCertificate = ({ tls }: Props) => { - return tls?.not_after ? ( + let expiry: X509Expiry | null = null; + if (tls?.server?.x509) { + expiry = tls.server.x509; + } else if (tls?.certificate_not_valid_after && tls?.certificate_not_valid_before) { + expiry = { + not_after: tls.certificate_not_valid_after, + not_before: tls.certificate_not_valid_before, + }; + } + + if (!expiry) { + return null; + } + + return ( <> { - + - ) : null; + ); }; diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap b/x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap index d593dcc21b59..126b2eb9dfdf 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap @@ -51,21 +51,56 @@ exports[`MonitorList component MonitorListPagination component renders a no item } } > - + > + + `; @@ -120,83 +155,150 @@ exports[`MonitorList component MonitorListPagination component renders the pagin } } > - + > + + `; @@ -251,21 +353,56 @@ exports[`MonitorList component renders a no items message when no data is provid } } > - + > + + `; @@ -320,84 +457,151 @@ exports[`MonitorList component renders error list 1`] = ` } } > - + > + + `; @@ -452,83 +656,150 @@ exports[`MonitorList component renders loading state 1`] = ` } } > - + > + + `; @@ -712,7 +983,7 @@ exports[`MonitorList component renders the monitor list 1`] = ` @@ -760,7 +1031,7 @@ exports[`MonitorList component renders the monitor list 1`] = ` @@ -861,7 +1132,7 @@ exports[`MonitorList component renders the monitor list 1`] = `
- 1897 Yr ago + 1898 Yr ago
@@ -1038,7 +1309,7 @@ exports[`MonitorList component renders the monitor list 1`] = `
- 1895 Yr ago + 1896 Yr ago
@@ -1050,7 +1321,7 @@ exports[`MonitorList component renders the monitor list 1`] = `
- in 1/1 Location + in 0/1 Location
@@ -1301,82 +1572,149 @@ exports[`MonitorList component shallow renders the monitor list 1`] = ` } } > - + > + + `; diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/monitor_list.test.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/monitor_list.test.tsx index 7d09f4161fca..3bba1e9e894c 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/monitor_list.test.tsx +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/monitor_list.test.tsx @@ -6,17 +6,99 @@ import React from 'react'; import { - MonitorSummaryResult, + MonitorSummariesResult, CursorDirection, SortOrder, + makePing, + Ping, + MonitorSummary, } from '../../../../../common/runtime_types'; import { MonitorListComponent, noItemsMessage } from '../monitor_list'; import { renderWithRouter, shallowWithRouter } from '../../../../lib'; import * as redux from 'react-redux'; +const testFooPings: Ping[] = [ + makePing({ + docId: 'foo1', + id: 'foo', + type: 'icmp', + status: 'up', + duration: 123, + timestamp: '124', + ip: '127.0.0.1', + }), + makePing({ + docId: 'foo2', + id: 'foo', + type: 'icmp', + status: 'up', + duration: 123, + timestamp: '125', + ip: '127.0.0.2', + }), + makePing({ + docId: 'foo3', + id: 'foo', + type: 'icmp', + status: 'down', + duration: 123, + timestamp: '126', + ip: '127.0.0.3', + }), +]; + +const testFooSummary: MonitorSummary = { + monitor_id: 'foo', + state: { + monitor: {}, + summaryPings: testFooPings, + summary: { + up: 1, + down: 2, + }, + timestamp: '123', + url: {}, + }, +}; + +const testBarPings: Ping[] = [ + makePing({ + docId: 'bar1', + id: 'bar', + type: 'icmp', + status: 'down', + duration: 123, + timestamp: '125', + ip: '127.0.0.1', + }), + makePing({ + docId: 'bar2', + id: 'bar', + type: 'icmp', + status: 'down', + duration: 123, + timestamp: '126', + ip: '127.0.0.1', + }), +]; + +const testBarSummary: MonitorSummary = { + monitor_id: 'bar', + state: { + monitor: {}, + summaryPings: testBarPings, + summary: { + up: 2, + down: 0, + }, + timestamp: '125', + url: {}, + }, +}; + // Failing: See https://github.com/elastic/kibana/issues/70386 describe.skip('MonitorList component', () => { - let result: MonitorSummaryResult; + let result: MonitorSummariesResult; let localStorageMock: any; beforeEach(() => { @@ -36,69 +118,7 @@ describe.skip('MonitorList component', () => { result = { nextPagePagination: null, prevPagePagination: null, - summaries: [ - { - monitor_id: 'foo', - state: { - checks: [ - { - monitor: { - ip: '127.0.0.1', - status: 'up', - }, - timestamp: 124, - }, - { - monitor: { - ip: '127.0.0.2', - status: 'down', - }, - timestamp: 125, - }, - { - monitor: { - ip: '127.0.0.3', - status: 'down', - }, - timestamp: 126, - }, - ], - summary: { - up: 1, - down: 2, - }, - timestamp: '123', - url: {}, - }, - }, - { - monitor_id: 'bar', - state: { - checks: [ - { - monitor: { - ip: '127.0.0.1', - status: 'up', - }, - timestamp: 125, - }, - { - monitor: { - ip: '127.0.0.2', - status: 'up', - }, - timestamp: 126, - }, - ], - summary: { - up: 2, - down: 0, - }, - timestamp: '125', - url: {}, - }, - }, - ], + summaries: [testFooSummary, testBarSummary], totalSummaryCount: 2, }; }); @@ -171,7 +191,7 @@ describe.skip('MonitorList component', () => { }); describe('MonitorListPagination component', () => { - let paginationResult: MonitorSummaryResult; + let paginationResult: MonitorSummariesResult; beforeEach(() => { paginationResult = { @@ -185,69 +205,7 @@ describe.skip('MonitorList component', () => { cursorDirection: CursorDirection.AFTER, sortOrder: SortOrder.ASC, }), - summaries: [ - { - monitor_id: 'foo', - state: { - checks: [ - { - monitor: { - ip: '127.0.0.1', - status: 'up', - }, - timestamp: 124, - }, - { - monitor: { - ip: '127.0.0.2', - status: 'down', - }, - timestamp: 125, - }, - { - monitor: { - ip: '127.0.0.3', - status: 'down', - }, - timestamp: 126, - }, - ], - summary: { - up: 1, - down: 2, - }, - timestamp: '123', - url: {}, - }, - }, - { - monitor_id: 'bar', - state: { - checks: [ - { - monitor: { - ip: '127.0.0.1', - status: 'up', - }, - timestamp: 125, - }, - { - monitor: { - ip: '127.0.0.2', - status: 'up', - }, - timestamp: 126, - }, - ], - summary: { - up: 2, - down: 0, - }, - timestamp: '125', - url: {}, - }, - }, - ], + summaries: [testFooSummary, testBarSummary], totalSummaryCount: 2, }; }); diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/monitor_list_status_column.test.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/monitor_list_status_column.test.tsx index d765c0b33ea4..9696ecc2c237 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/monitor_list_status_column.test.tsx +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/__tests__/monitor_list_status_column.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import moment from 'moment'; import { renderWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; import { getLocationStatus, MonitorListStatusColumn } from '../monitor_list_status_column'; -import { Check } from '../../../../../common/runtime_types'; +import { Ping } from '../../../../../common/runtime_types'; import { STATUS } from '../../../../../common/constants'; describe('MonitorListStatusColumn', () => { @@ -20,19 +20,23 @@ describe('MonitorListStatusColumn', () => { Date.prototype.toString = jest.fn(() => 'Tue, 01 Jan 2019 00:00:00 GMT'); }); - let upChecks: Check[]; + let upChecks: Ping[]; - let downChecks: Check[]; + let downChecks: Ping[]; - let checks: Check[]; + let summaryPings: Ping[]; beforeEach(() => { upChecks = [ { + docId: '1', monitor: { ip: '104.86.46.103', name: '', status: 'up', + id: 'myMonitor', + type: 'icmp', + duration: { us: 123 }, }, observer: { geo: { @@ -43,13 +47,17 @@ describe('MonitorListStatusColumn', () => { }, }, }, - timestamp: 1579794631464, + timestamp: '1579794631464', }, { + docId: '2', monitor: { ip: '104.86.46.103', name: '', status: 'up', + id: 'myMonitor', + type: 'icmp', + duration: { us: 123 }, }, observer: { geo: { @@ -60,13 +68,17 @@ describe('MonitorListStatusColumn', () => { }, }, }, - timestamp: 1579794634220, + timestamp: '1579794634220', }, { + docId: '3', monitor: { ip: '104.86.46.103', name: '', status: 'up', + id: 'myMonitor', + type: 'icmp', + duration: { us: 123 }, }, observer: { geo: { @@ -77,16 +89,20 @@ describe('MonitorListStatusColumn', () => { }, }, }, - timestamp: 1579794628368, + timestamp: '1579794628368', }, ]; downChecks = [ { + docId: '4', monitor: { ip: '104.86.46.103', name: '', status: 'down', + id: 'myMonitor', + type: 'icmp', + duration: { us: 123 }, }, observer: { geo: { @@ -97,13 +113,17 @@ describe('MonitorListStatusColumn', () => { }, }, }, - timestamp: 1579794631464, + timestamp: '1579794631464', }, { + docId: '5', monitor: { ip: '104.86.46.103', name: '', status: 'down', + id: 'myMonitor', + type: 'icmp', + duration: { us: 123 }, }, observer: { geo: { @@ -114,13 +134,17 @@ describe('MonitorListStatusColumn', () => { }, }, }, - timestamp: 1579794634220, + timestamp: '1579794634220', }, { + docId: '6', monitor: { ip: '104.86.46.103', name: '', status: 'down', + id: 'myMonitor', + type: 'icmp', + duration: { us: 123 }, }, observer: { geo: { @@ -131,16 +155,20 @@ describe('MonitorListStatusColumn', () => { }, }, }, - timestamp: 1579794628368, + timestamp: '1579794628368', }, ]; - checks = [ + summaryPings = [ { + docId: '7', monitor: { ip: '104.86.46.103', name: '', status: 'up', + id: 'myMonitor', + type: 'icmp', + duration: { us: 123 }, }, observer: { geo: { @@ -151,13 +179,17 @@ describe('MonitorListStatusColumn', () => { }, }, }, - timestamp: 1579794631464, + timestamp: '1579794631464', }, { + docId: '8', monitor: { ip: '104.86.46.103', name: '', status: 'down', + id: 'myMonitor', + type: 'icmp', + duration: { us: 123 }, }, observer: { geo: { @@ -168,13 +200,17 @@ describe('MonitorListStatusColumn', () => { }, }, }, - timestamp: 1579794634220, + timestamp: '1579794634220', }, { + docId: '9', monitor: { ip: '104.86.46.103', name: '', status: 'down', + id: 'myMonitor', + type: 'icmp', + duration: { us: 123 }, }, observer: { geo: { @@ -185,45 +221,53 @@ describe('MonitorListStatusColumn', () => { }, }, }, - timestamp: 1579794628368, + timestamp: '1579794628368', }, ]; }); it('provides expected tooltip and display times', () => { const component = shallowWithIntl( - + ); expect(component).toMatchSnapshot(); }); it('can handle a non-numeric timestamp value', () => { const component = shallowWithIntl( - + ); expect(component).toMatchSnapshot(); }); it('will display location status', () => { const component = shallowWithIntl( - + ); expect(component).toMatchSnapshot(); }); it('will render display location status', () => { const component = renderWithIntl( - + ); expect(component).toMatchSnapshot(); }); it(' will test getLocationStatus location', () => { - let statusMessage = getLocationStatus(checks, STATUS.UP); + let statusMessage = getLocationStatus(summaryPings, STATUS.UP); expect(statusMessage).toBe('in 1/3 Locations'); - statusMessage = getLocationStatus(checks, STATUS.DOWN); + statusMessage = getLocationStatus(summaryPings, STATUS.DOWN); expect(statusMessage).toBe('in 2/3 Locations'); diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/cert_status_column.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/cert_status_column.tsx index b7c70198912f..318e18385ba1 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/cert_status_column.tsx +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/cert_status_column.tsx @@ -8,13 +8,13 @@ import React from 'react'; import moment from 'moment'; import styled from 'styled-components'; import { EuiIcon, EuiText, EuiToolTip } from '@elastic/eui'; -import { Cert, Tls } from '../../../../common/runtime_types'; +import { X509Expiry } from '../../../../common/runtime_types'; import { useCertStatus } from '../../../hooks'; import { EXPIRED, EXPIRES, EXPIRES_SOON } from '../../certificates/translations'; import { CERT_STATUS } from '../../../../common/constants'; interface Props { - cert: Cert | Tls; + expiry: X509Expiry; boldStyle?: boolean; } @@ -31,14 +31,15 @@ const H4Text = styled.h4` } `; -export const CertStatusColumn: React.FC = ({ cert, boldStyle = false }) => { - const certStatus = useCertStatus(cert?.not_after); +export const CertStatusColumn: React.FC = ({ expiry, boldStyle = false }) => { + const notAfter = expiry?.not_after; + const certStatus = useCertStatus(notAfter); - const relativeDate = moment(cert?.not_after).fromNow(); + const relativeDate = moment(notAfter).fromNow(); const CertStatus = ({ color, text }: { color: string; text: string }) => { return ( - + {boldStyle ? ( diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list.tsx index 75d587579f66..ce4c518d8225 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list.tsx +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list.tsx @@ -16,7 +16,7 @@ import { } from '@elastic/eui'; import React, { useState } from 'react'; import styled from 'styled-components'; -import { HistogramPoint } from '../../../../common/runtime_types'; +import { HistogramPoint, X509Expiry } from '../../../../common/runtime_types'; import { MonitorSummary } from '../../../../common/runtime_types'; import { MonitorListStatusColumn } from './monitor_list_status_column'; import { ExpandedRowMap } from './types'; @@ -79,14 +79,18 @@ export const MonitorListComponent: React.FC = ({ const columns = [ { align: 'left' as const, - field: 'state.monitor.status', + field: 'state.summary.status', name: labels.STATUS_COLUMN_LABEL, mobileOptions: { fullWidth: true, }, - render: (status: string, { state: { timestamp, checks } }: MonitorSummary) => { + render: (status: string, { state: { timestamp, summaryPings } }: MonitorSummary) => { return ( - + ); }, }, @@ -116,9 +120,9 @@ export const MonitorListComponent: React.FC = ({ }, { align: 'left' as const, - field: 'state.tls', + field: 'state.tls.server.x509', name: labels.TLS_COLUMN_LABEL, - render: (tls: any) => , + render: (x509: X509Expiry) => , }, { align: 'center' as const, diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/monitor_list_drawer.test.tsx.snap b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/monitor_list_drawer.test.tsx.snap index 877ba889ec1f..329fb8bade10 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/monitor_list_drawer.test.tsx.snap +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/__snapshots__/monitor_list_drawer.test.tsx.snap @@ -99,33 +99,65 @@ exports[`MonitorListDrawer component renders a MonitorListDrawer when there are Object { "monitor_id": "foo", "state": Object { - "checks": Array [ - Object { - "monitor": Object { - "ip": "127.0.0.1", - "status": "up", - }, - "timestamp": 121, - }, - Object { - "monitor": Object { - "ip": "127.0.0.2", - "status": "down", - }, - "timestamp": 123, - }, - Object { - "monitor": Object { - "ip": "127.0.0.3", - "status": "up", - }, - "timestamp": 125, - }, - ], + "monitor": Object {}, "summary": Object { "down": 0, "up": 1, }, + "summaryPings": Array [ + Object { + "docId": "foo", + "monitor": Object { + "duration": Object { + "us": 121, + }, + "id": "foo", + "ip": "127.0.0.1", + "status": "up", + "type": "icmp", + }, + "timestamp": "121", + }, + Object { + "docId": "foo-0", + "monitor": Object { + "duration": Object { + "us": 100000, + }, + "id": "foo", + "ip": "127.0.0.1", + "status": "down", + "type": "icmp", + }, + "timestamp": "0", + }, + Object { + "docId": "foo-1", + "monitor": Object { + "duration": Object { + "us": 1, + }, + "id": "foo", + "ip": "127.0.0.2", + "status": "up", + "type": "icmp", + }, + "timestamp": "1", + }, + Object { + "docId": "foo-2", + "monitor": Object { + "duration": Object { + "us": 2, + }, + "id": "foo", + "ip": "127.0.0.3", + "status": "down", + "type": "icmp", + }, + "timestamp": "2", + }, + ], "timestamp": "123", "url": Object { "domain": "expired.badssl.com", @@ -238,19 +270,26 @@ exports[`MonitorListDrawer component renders a MonitorListDrawer when there is o Object { "monitor_id": "foo", "state": Object { - "checks": Array [ - Object { - "monitor": Object { - "ip": "127.0.0.1", - "status": "up", - }, - "timestamp": 121, - }, - ], + "monitor": Object {}, "summary": Object { "down": 0, "up": 1, }, + "summaryPings": Array [ + Object { + "docId": "foo", + "monitor": Object { + "duration": Object { + "us": 121, + }, + "id": "foo", + "ip": "127.0.0.1", + "status": "up", + "type": "icmp", + }, + "timestamp": "121", + }, + ], "timestamp": "123", "url": Object { "domain": "expired.badssl.com", diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/data.json b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/data.json index e8142f0480c4..012280c8147d 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/data.json +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/data.json @@ -615,7 +615,7 @@ "__typename": "MonitorSummary" } ], - "__typename": "MonitorSummaryResult" + "__typename": "MonitorSummariesResult" } } } diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/integration_group.test.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/integration_group.test.tsx index 1c587568fe61..df9f1febd40c 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/integration_group.test.tsx +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/integration_group.test.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; -import { MonitorSummary } from '../../../../../../common/runtime_types'; +import { MonitorSummary, makePing } from '../../../../../../common/runtime_types'; import { shallowWithIntl } from 'test_utils/enzyme_helpers'; import { IntegrationGroup, extractSummaryValues } from '../actions_popover/integration_group'; @@ -17,7 +17,8 @@ describe('IntegrationGroup', () => { monitor_id: '12345', state: { summary: {}, - checks: [], + monitor: {}, + summaryPings: [], timestamp: '123', url: {}, }, @@ -46,6 +47,12 @@ describe('IntegrationGroup', () => { mockSummary = { state: { timestamp: 'foo', + summaryPings: [], + monitor: {}, + summary: { + up: 0, + down: 0, + }, url: {}, }, }; @@ -76,22 +83,25 @@ describe('IntegrationGroup', () => { }); it('finds pod uid', () => { - mockSummary.state.checks = [ - { kubernetes: { pod: { uid: 'myuid' } }, monitor: { status: 'up' }, timestamp: 123 }, + mockSummary.state.summaryPings = [ + { + ...makePing({}), + kubernetes: { pod: { uid: 'myuid' } }, + }, ]; expect(extractSummaryValues(mockSummary)).toMatchInlineSnapshot(` Object { "containerId": undefined, "domain": "", - "ip": undefined, + "ip": "127.0.0.1", "podUid": "myuid", } `); }); it('does not throw for missing kubernetes fields', () => { - mockSummary.state.checks = []; + mockSummary.state.summaryPings = []; expect(extractSummaryValues(mockSummary)).toMatchInlineSnapshot(` Object { @@ -104,22 +114,25 @@ describe('IntegrationGroup', () => { }); it('finds container id', () => { - mockSummary.state.checks = [ - { container: { id: 'mycontainer' }, monitor: { status: 'up' }, timestamp: 123 }, + mockSummary.state.summaryPings = [ + { + ...makePing({}), + container: { id: 'mycontainer' }, + }, ]; expect(extractSummaryValues(mockSummary)).toMatchInlineSnapshot(` Object { "containerId": "mycontainer", "domain": "", - "ip": undefined, + "ip": "127.0.0.1", "podUid": undefined, } `); }); it('finds ip field', () => { - mockSummary.state.checks = [{ monitor: { ip: '127.0.0.1', status: 'up' }, timestamp: 123 }]; + mockSummary.state.summaryPings = [makePing({ ip: '127.0.0.1', status: 'up' })]; expect(extractSummaryValues(mockSummary)).toMatchInlineSnapshot(` Object { diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/monitor_list_drawer.test.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/monitor_list_drawer.test.tsx index 4bc0c3f0a40b..502ccd53ef80 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/monitor_list_drawer.test.tsx +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/monitor_list_drawer.test.tsx @@ -6,7 +6,7 @@ import 'jest'; import React from 'react'; import { MonitorListDrawerComponent } from '../monitor_list_drawer'; -import { Check, MonitorDetails, MonitorSummary } from '../../../../../../common/runtime_types'; +import { MonitorDetails, MonitorSummary, makePing } from '../../../../../../common/runtime_types'; import { shallowWithRouter } from '../../../../../lib'; describe('MonitorListDrawer component', () => { @@ -17,14 +17,17 @@ describe('MonitorListDrawer component', () => { summary = { monitor_id: 'foo', state: { - checks: [ - { - monitor: { - ip: '127.0.0.1', - status: 'up', - }, - timestamp: 121, - }, + monitor: {}, + summaryPings: [ + makePing({ + docId: 'foo', + id: 'foo', + ip: '127.0.0.1', + type: 'icmp', + status: 'up', + timestamp: '121', + duration: 121, + }), ], summary: { up: 1, @@ -55,7 +58,7 @@ describe('MonitorListDrawer component', () => { }); it('renders nothing when no check data is present', () => { - delete summary.state.checks; + delete summary.state.summaryPings; const component = shallowWithRouter( ); @@ -70,30 +73,20 @@ describe('MonitorListDrawer component', () => { }); it('renders a MonitorListDrawer when there are many checks', () => { - const checks: Check[] = [ - { - monitor: { - ip: '127.0.0.1', - status: 'up', - }, - timestamp: 121, - }, - { - monitor: { - ip: '127.0.0.2', - status: 'down', - }, - timestamp: 123, - }, - { - monitor: { - ip: '127.0.0.3', - status: 'up', - }, - timestamp: 125, - }, - ]; - summary.state.checks = checks; + for (let i = 0; i < 3; i++) { + summary.state.summaryPings.push( + makePing({ + docId: `foo-${i}`, + id: 'foo', + ip: `127.0.0.${1 + i}`, + type: 'icmp', + timestamp: `${i}`, + duration: i, + status: i % 2 !== 0 ? 'up' : 'down', + }) + ); + } + const component = shallowWithRouter( ); diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/monitor_status_list.test.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/monitor_status_list.test.tsx index c7f3aef4075e..c0143828dc69 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/monitor_status_list.test.tsx +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/__tests__/monitor_status_list.test.tsx @@ -8,10 +8,10 @@ import { shallowWithIntl } from 'test_utils/enzyme_helpers'; import React from 'react'; import moment from 'moment'; import { MonitorStatusList } from '../monitor_status_list'; -import { Check } from '../../../../../../common/runtime_types'; +import { Ping } from '../../../../../../common/runtime_types'; describe('MonitorStatusList component', () => { - let checks: Check[]; + let pings: Ping[]; beforeAll(() => { moment.prototype.toLocaleString = jest.fn(() => '2019-06-21 15:29:26'); @@ -19,105 +19,137 @@ describe('MonitorStatusList component', () => { }); beforeEach(() => { - checks = [ + pings = [ { + docId: '1', monitor: { ip: '151.101.130.217', name: 'elastic', status: 'up', + id: 'myMonitor', + type: 'icmp', + duration: { us: 123 }, }, observer: { geo: {}, }, - timestamp: 1570538236414, + timestamp: '1570538236414', }, { + docId: '2', monitor: { ip: '151.101.194.217', name: 'elastic', status: 'up', + id: 'myMonitor', + type: 'icmp', + duration: { us: 123 }, }, observer: { geo: {}, }, - timestamp: 1570538236414, + timestamp: '1570538236414', }, { + docId: '3', monitor: { ip: '151.101.2.217', name: 'elastic', status: 'up', + id: 'myMonitor', + type: 'icmp', + duration: { us: 123 }, }, observer: { geo: {}, }, - timestamp: 1570538236414, + timestamp: '1570538236414', }, { + docId: '4', monitor: { ip: '151.101.66.217', name: 'elastic', status: 'up', + id: 'myMonitor', + type: 'icmp', + duration: { us: 123 }, }, observer: { geo: {}, }, - timestamp: 1570538236414, + timestamp: '1570538236414', }, { + docId: '4', monitor: { ip: '2a04:4e42:200::729', name: 'elastic', status: 'down', + id: 'myMonitor', + type: 'icmp', + duration: { us: 123 }, }, observer: { geo: {}, }, - timestamp: 1570538236414, + timestamp: '1570538236414', }, { + docId: '5', monitor: { ip: '2a04:4e42:400::729', name: 'elastic', status: 'down', + id: 'myMonitor', + type: 'icmp', + duration: { us: 123 }, }, observer: { geo: {}, }, - timestamp: 1570538236414, + timestamp: '1570538236414', }, { + docId: '6', monitor: { ip: '2a04:4e42:600::729', name: 'elastic', status: 'down', + id: 'myMonitor', + type: 'icmp', + duration: { us: 123 }, }, observer: { geo: {}, }, - timestamp: 1570538236414, + timestamp: '1570538236414', }, { + docId: '5', monitor: { ip: '2a04:4e42::729', name: 'elastic', status: 'down', + id: 'myMonitor', + type: 'icmp', + duration: { us: 123 }, }, observer: { geo: {}, }, - timestamp: 1570538236414, + timestamp: '1570538236414', }, ]; }); it('renders checks', () => { - const component = shallowWithIntl(); + const component = shallowWithIntl(); expect(component).toMatchSnapshot(); }); it('renders null in place of child status with missing ip', () => { - const component = shallowWithIntl(); + const component = shallowWithIntl(); expect(component).toMatchSnapshot(); }); }); diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/actions_popover/integration_group.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/actions_popover/integration_group.tsx index 55a99ab8541f..38aa9287b0c4 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/actions_popover/integration_group.tsx +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/actions_popover/integration_group.tsx @@ -27,9 +27,9 @@ interface IntegrationGroupProps { export const extractSummaryValues = (summary: Pick) => { const domain = summary.state.url?.domain ?? ''; - const podUid = summary.state.checks?.[0]?.kubernetes?.pod.uid ?? undefined; - const containerId = summary.state.checks?.[0]?.container?.id ?? undefined; - const ip = summary.state.checks?.[0]?.monitor.ip ?? undefined; + const podUid = summary.state.summaryPings?.[0]?.kubernetes?.pod?.uid ?? undefined; + const containerId = summary.state.summaryPings?.[0]?.container?.id ?? undefined; + const ip = summary.state.summaryPings?.[0]?.monitor.ip ?? undefined; return { domain, diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/monitor_list_drawer.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/monitor_list_drawer.tsx index 8e97ce4d692d..305455c8ba57 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/monitor_list_drawer.tsx +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/monitor_list_drawer.tsx @@ -36,7 +36,7 @@ interface MonitorListDrawerProps { export function MonitorListDrawerComponent({ summary, monitorDetails }: MonitorListDrawerProps) { const monitorUrl = summary?.state?.url?.full || ''; - return summary && summary.state.checks ? ( + return summary && summary.state.summaryPings ? ( @@ -52,7 +52,7 @@ export function MonitorListDrawerComponent({ summary, monitorDetails }: MonitorL - + {monitorDetails && monitorDetails.error && ( { +export const MonitorStatusList = ({ summaryPings }: MonitorStatusListProps) => { const upChecks: Set = new Set(); const downChecks: Set = new Set(); - checks.forEach((check: Check) => { + summaryPings.forEach((ping: Ping) => { // Doing this way because name is either string or null, get() default value only works on undefined value - const location = get(check, 'observer.geo.name', null) || UNNAMED_LOCATION; + const location = ping.observer?.geo?.name ?? UNNAMED_LOCATION; - if (check.monitor.status === STATUS.UP) { + if (ping.monitor.status === STATUS.UP) { upChecks.add(upperFirst(location)); - } else if (check.monitor.status === STATUS.DOWN) { + } else if (ping.monitor.status === STATUS.DOWN) { downChecks.add(upperFirst(location)); } }); diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_status_column.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_status_column.tsx index f80c73dcf5bb..68ddf512e4d3 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_status_column.tsx +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_status_column.tsx @@ -11,7 +11,7 @@ import { upperFirst } from 'lodash'; import styled from 'styled-components'; import { EuiHealth, EuiFlexGroup, EuiFlexItem, EuiText, EuiToolTip } from '@elastic/eui'; import { parseTimestamp } from './parse_timestamp'; -import { Check } from '../../../../common/runtime_types'; +import { Ping } from '../../../../common/runtime_types'; import { STATUS, SHORT_TIMESPAN_LOCALE, @@ -24,7 +24,7 @@ import * as labels from './translations'; interface MonitorListStatusColumnProps { status: string; timestamp: string; - checks: Check[]; + summaryPings: Ping[]; } const PaddedSpan = styled.span` @@ -75,27 +75,27 @@ const getRelativeShortTimeStamp = (timeStamp: any) => { return shortTimestamp; }; -export const getLocationStatus = (checks: Check[], status: string) => { - const upChecks: Set = new Set(); - const downChecks: Set = new Set(); +export const getLocationStatus = (summaryPings: Ping[], status: string) => { + const upPings: Set = new Set(); + const downPings: Set = new Set(); - checks.forEach((check: Check) => { - const location = check?.observer?.geo?.name ?? UNNAMED_LOCATION; + summaryPings.forEach((summaryPing: Ping) => { + const location = summaryPing?.observer?.geo?.name ?? UNNAMED_LOCATION; - if (check.monitor.status === STATUS.UP) { - upChecks.add(upperFirst(location)); - } else if (check.monitor.status === STATUS.DOWN) { - downChecks.add(upperFirst(location)); + if (summaryPing.monitor.status === STATUS.UP) { + upPings.add(upperFirst(location)); + } else if (summaryPing.monitor.status === STATUS.DOWN) { + downPings.add(upperFirst(location)); } }); // if monitor is down in one dns, it will be considered down so removing it from up list - const absUpChecks: Set = new Set([...upChecks].filter((item) => !downChecks.has(item))); + const absUpChecks: Set = new Set([...upPings].filter((item) => !downPings.has(item))); - const totalLocations = absUpChecks.size + downChecks.size; + const totalLocations = absUpChecks.size + downPings.size; let statusMessage = ''; if (status === STATUS.DOWN) { - statusMessage = `${downChecks.size}/${totalLocations}`; + statusMessage = `${downPings.size}/${totalLocations}`; } else { statusMessage = `${absUpChecks.size}/${totalLocations}`; } @@ -115,7 +115,7 @@ export const getLocationStatus = (checks: Check[], status: string) => { export const MonitorListStatusColumn = ({ status, - checks = [], + summaryPings = [], timestamp: tsString, }: MonitorListStatusColumnProps) => { const timestamp = parseTimestamp(tsString); @@ -140,7 +140,7 @@ export const MonitorListStatusColumn = ({ - {getLocationStatus(checks, status)} + {getLocationStatus(summaryPings, status)} ); diff --git a/x-pack/plugins/uptime/public/lib/helper/observability_integration/__tests__/get_apm_href.test.ts b/x-pack/plugins/uptime/public/lib/helper/observability_integration/__tests__/get_apm_href.test.ts index f27ed78d593a..8e320a8d9533 100644 --- a/x-pack/plugins/uptime/public/lib/helper/observability_integration/__tests__/get_apm_href.test.ts +++ b/x-pack/plugins/uptime/public/lib/helper/observability_integration/__tests__/get_apm_href.test.ts @@ -5,7 +5,7 @@ */ import { getApmHref } from '../get_apm_href'; -import { MonitorSummary } from '../../../../../common/runtime_types'; +import { MonitorSummary, makePing } from '../../../../../common/runtime_types'; describe('getApmHref', () => { let summary: MonitorSummary; @@ -15,22 +15,17 @@ describe('getApmHref', () => { monitor_id: 'foo', state: { summary: {}, - checks: [ - { - monitor: { - ip: '151.101.202.217', - status: 'up', - }, - container: { - id: 'test-container-id', - }, - kubernetes: { - pod: { - uid: 'test-pod-id', - }, - }, - timestamp: 123, - }, + monitor: {}, + summaryPings: [ + makePing({ + docId: 'foo', + id: 'foo', + type: 'test', + ip: '151.101.202.217', + status: 'up', + timestamp: '123', + duration: 123, + }), ], timestamp: '123', url: { diff --git a/x-pack/plugins/uptime/public/lib/helper/observability_integration/__tests__/get_infra_href.test.ts b/x-pack/plugins/uptime/public/lib/helper/observability_integration/__tests__/get_infra_href.test.ts index ee5db74af22c..befa11d74df4 100644 --- a/x-pack/plugins/uptime/public/lib/helper/observability_integration/__tests__/get_infra_href.test.ts +++ b/x-pack/plugins/uptime/public/lib/helper/observability_integration/__tests__/get_infra_href.test.ts @@ -5,33 +5,32 @@ */ import { getInfraContainerHref, getInfraKubernetesHref, getInfraIpHref } from '../get_infra_href'; -import { MonitorSummary } from '../../../../../common/runtime_types'; +import { MonitorSummary, makePing, Ping } from '../../../../../common/runtime_types'; describe('getInfraHref', () => { let summary: MonitorSummary; beforeEach(() => { + const ping: Ping = { + ...makePing({ + docId: 'myDocId', + type: 'test', + id: 'myId', + ip: '151.101.202.217', + status: 'up', + duration: 123, + timestamp: '123', + }), + container: { id: 'test-container-id' }, + kubernetes: { pod: { uid: 'test-pod-uid' } }, + }; + summary = { monitor_id: 'foo', state: { - checks: [ - { - monitor: { - ip: '151.101.202.217', - status: 'up', - }, - container: { - id: 'test-container-id', - }, - kubernetes: { - pod: { - uid: 'test-pod-uid', - }, - }, - timestamp: 123, - }, - ], + summaryPings: [ping], summary: {}, url: {}, + monitor: {}, timestamp: '123', }, }; @@ -51,50 +50,36 @@ describe('getInfraHref', () => { }); it('getInfraContainerHref returns undefined when no container id is present', () => { - summary.state.checks = []; + summary.state.summaryPings = []; expect(getInfraContainerHref(summary, 'foo')).toBeUndefined(); }); it('getInfraContainerHref returns the first item when multiple container ids are supplied', () => { - summary.state.checks = [ - { - monitor: { - ip: '151.101.202.217', - status: 'up', - }, - container: { - id: 'test-container-id', - }, - kubernetes: { - pod: { - uid: 'test-pod-uid', - }, - }, - timestamp: 123, - }, - { - monitor: { - ip: '151.101.202.27', - status: 'up', - }, - container: { - id: 'test-container-id-foo', - }, - kubernetes: { - pod: { - uid: 'test-pod-uid-bar', - }, - }, - timestamp: 123, - }, - ]; + const pingBase = makePing({ + docId: 'myDocId', + type: 'test', + id: 'myId', + ip: '151.101.202.217', + status: 'up', + duration: 123, + timestamp: '123', + }); + const pingTestContainerId: Ping = { + ...pingBase, + container: { id: 'test-container-id' }, + }; + const pingTestFooContainerId: Ping = { + ...pingBase, + container: { id: 'test-container-id-foo' }, + }; + summary.state.summaryPings = [pingTestContainerId, pingTestFooContainerId]; expect(getInfraContainerHref(summary, 'bar')).toMatchInlineSnapshot( `"bar/app/metrics/link-to/container-detail/test-container-id"` ); }); - it('getInfraContainerHref returns undefined when checks are undefined', () => { - delete summary.state.checks; + it('getInfraContainerHref returns undefined when summaryPings are undefined', () => { + delete summary.state.summaryPings; expect(getInfraContainerHref(summary, '')).toBeUndefined(); }); @@ -111,55 +96,41 @@ describe('getInfraHref', () => { }); it('getInfraKubernetesHref returns undefined when no pod data is present', () => { - summary.state.checks = []; + summary.state.summaryPings = []; expect(getInfraKubernetesHref(summary, 'foo')).toBeUndefined(); }); it('getInfraKubernetesHref selects the first pod uid when there are multiple', () => { - summary.state.checks = [ - { - monitor: { - ip: '151.101.202.217', - status: 'up', - }, - container: { - id: 'test-container-id', - }, - kubernetes: { - pod: { - uid: 'test-pod-uid', - }, - }, - timestamp: 123, - }, - { - monitor: { - ip: '151.101.202.27', - status: 'up', - }, - container: { - id: 'test-container-id-foo', - }, - kubernetes: { - pod: { - uid: 'test-pod-uid-bar', - }, - }, - timestamp: 123, - }, - ]; + const pingBase = makePing({ + docId: 'myDocId', + type: 'test', + id: 'myId', + ip: '151.101.202.217', + status: 'up', + duration: 123, + timestamp: '123', + }); + const pingTestPodId: Ping = { + ...pingBase, + kubernetes: { pod: { uid: 'test-pod-uid' } }, + }; + const pingTestBarPodId: Ping = { + ...pingBase, + kubernetes: { pod: { uid: 'test-pod-uid-bar' } }, + }; + summary.state.summaryPings = [pingTestPodId, pingTestBarPodId]; expect(getInfraKubernetesHref(summary, '')).toMatchInlineSnapshot( `"/app/metrics/link-to/pod-detail/test-pod-uid"` ); }); - it('getInfraKubernetesHref returns undefined when checks are undefined', () => { - delete summary.state.checks; + it('getInfraKubernetesHref returns undefined when summaryPings are undefined', () => { + delete summary.state.summaryPings; expect(getInfraKubernetesHref(summary, '')).toBeUndefined(); }); - it('getInfraKubernetesHref returns undefined when checks are null', () => { - delete summary.state.checks![0]!.kubernetes!.pod!.uid; + it('getInfraKubernetesHref returns undefined when summaryPings are null', () => { + delete summary.state.summaryPings![0]!.kubernetes!.pod!.uid; expect(getInfraKubernetesHref(summary, '')).toBeUndefined(); }); @@ -177,47 +148,42 @@ describe('getInfraHref', () => { }); it('getInfraIpHref returns undefined when ip is undefined', () => { - summary.state.checks = []; + summary.state.summaryPings = []; expect(getInfraIpHref(summary, 'foo')).toBeUndefined(); }); it('getInfraIpHref returns undefined when ip is null', () => { - delete summary.state.checks![0].monitor.ip; + delete summary.state.summaryPings![0].monitor.ip; expect(getInfraIpHref(summary, 'foo')).toBeUndefined(); }); it('getInfraIpHref returns a url for ors between multiple ips', () => { - summary.state.checks = [ - { - timestamp: 123, - monitor: { - ip: '152.151.23.192', - status: 'up', - }, - }, - { - monitor: { - ip: '151.101.202.217', - status: 'up', - }, - container: { - id: 'test-container-id', - }, - kubernetes: { - pod: { - uid: 'test-pod-uid', - }, - }, - timestamp: 123, - }, - ]; + const pingOne = makePing({ + docId: 'myDocId', + type: 'test', + id: 'myId', + ip: '152.151.23.192', + status: 'up', + duration: 123, + timestamp: '123', + }); + const pingTwo = makePing({ + docId: 'myDocId2', + type: 'test', + id: 'myId', + ip: '151.101.202.217', + status: 'up', + duration: 123, + timestamp: '123', + }); + summary.state.summaryPings = [pingOne, pingTwo]; expect(getInfraIpHref(summary, 'foo')).toMatchInlineSnapshot( `"foo/app/metrics/inventory?waffleFilter=(expression:'host.ip%20%3A%20152.151.23.192%20or%20host.ip%20%3A%20151.101.202.217',kind:kuery)"` ); }); - it('getInfraIpHref returns undefined if checks are undefined', () => { - delete summary.state.checks; + it('getInfraIpHref returns undefined if summaryPings are undefined', () => { + delete summary.state.summaryPings; expect(getInfraIpHref(summary, 'foo')).toBeUndefined(); }); }); diff --git a/x-pack/plugins/uptime/public/lib/helper/observability_integration/__tests__/get_logging_href.test.ts b/x-pack/plugins/uptime/public/lib/helper/observability_integration/__tests__/get_logging_href.test.ts index b188a8d1b8ef..203d5b84c433 100644 --- a/x-pack/plugins/uptime/public/lib/helper/observability_integration/__tests__/get_logging_href.test.ts +++ b/x-pack/plugins/uptime/public/lib/helper/observability_integration/__tests__/get_logging_href.test.ts @@ -9,34 +9,36 @@ import { getLoggingKubernetesHref, getLoggingIpHref, } from '../get_logging_href'; -import { MonitorSummary } from '../../../../../common/runtime_types'; +import { MonitorSummary, makePing } from '../../../../../common/runtime_types'; describe('getLoggingHref', () => { let summary: MonitorSummary; beforeEach(() => { + const ping = makePing({ + docId: 'myDocId', + type: 'test', + id: 'myId', + ip: '151.101.202.217', + status: 'up', + duration: 123, + timestamp: '123', + }); + ping.container = { + id: 'test-container-id', + }; + ping.kubernetes = { + pod: { + uid: 'test-pod-id', + }, + }; summary = { monitor_id: 'foo', state: { summary: {}, - checks: [ - { - monitor: { - ip: '151.101.202.217', - status: 'up', - }, - container: { - id: 'test-container-id', - }, - kubernetes: { - pod: { - uid: 'test-pod-id', - }, - }, - timestamp: 123, - }, - ], + summaryPings: [ping], timestamp: '123', + monitor: {}, url: {}, }, }; @@ -91,32 +93,32 @@ describe('getLoggingHref', () => { }); it('returns undefined if necessary container is not present', () => { - delete summary.state.checks; + delete summary.state.summaryPings; expect(getLoggingContainerHref(summary, '')).toBeUndefined(); }); it('returns undefined if necessary container is null', () => { - delete summary.state.checks![0].container!.id; + delete summary.state.summaryPings![0].container!.id; expect(getLoggingContainerHref(summary, '')).toBeUndefined(); }); it('returns undefined if necessary pod is not present', () => { - delete summary.state.checks; + delete summary.state.summaryPings; expect(getLoggingKubernetesHref(summary, '')).toBeUndefined(); }); it('returns undefined if necessary pod is null', () => { - delete summary.state.checks![0].kubernetes!.pod!.uid; + delete summary.state.summaryPings![0].kubernetes!.pod!.uid; expect(getLoggingKubernetesHref(summary, '')).toBeUndefined(); }); it('returns undefined ip href if ip is not present', () => { - delete summary.state.checks; + delete summary.state.summaryPings; expect(getLoggingIpHref(summary, '')).toBeUndefined(); }); it('returns undefined ip href if ip is null', () => { - delete summary.state.checks![0].monitor.ip; + delete summary.state.summaryPings![0].monitor.ip; expect(getLoggingIpHref(summary, '')).toBeUndefined(); }); }); diff --git a/x-pack/plugins/uptime/public/lib/helper/observability_integration/build_href.ts b/x-pack/plugins/uptime/public/lib/helper/observability_integration/build_href.ts index 397d23a18332..94383262b0ac 100644 --- a/x-pack/plugins/uptime/public/lib/helper/observability_integration/build_href.ts +++ b/x-pack/plugins/uptime/public/lib/helper/observability_integration/build_href.ts @@ -5,7 +5,7 @@ */ import { get } from 'lodash'; -import { Check } from '../../../../common/runtime_types'; +import { Ping } from '../../../../common/runtime_types'; /** * Builds URLs to the designated features by extracting values from the provided @@ -16,12 +16,12 @@ import { Check } from '../../../../common/runtime_types'; * @param getHref a function that returns the full URL */ export const buildHref = ( - checks: Check[], + summaryPings: Ping[], path: string, getHref: (value: string | string[] | undefined) => string | undefined ): string | undefined => { - const queryValue = checks - .map((check) => get(check, path, undefined)) + const queryValue = summaryPings + .map((ping) => get(ping, path, undefined)) .filter((value: string | undefined) => value !== undefined); if (queryValue.length === 0) { return getHref(undefined); diff --git a/x-pack/plugins/uptime/public/lib/helper/observability_integration/get_infra_href.ts b/x-pack/plugins/uptime/public/lib/helper/observability_integration/get_infra_href.ts index 384067e4b033..33d24a0f081b 100644 --- a/x-pack/plugins/uptime/public/lib/helper/observability_integration/get_infra_href.ts +++ b/x-pack/plugins/uptime/public/lib/helper/observability_integration/get_infra_href.ts @@ -22,7 +22,7 @@ export const getInfraContainerHref = ( `/app/metrics/link-to/container-detail/${encodeURIComponent(ret)}` ); }; - return buildHref(summary.state.checks || [], 'container.id', getHref); + return buildHref(summary.state.summaryPings || [], 'container.id', getHref); }; export const getInfraKubernetesHref = ( @@ -37,7 +37,7 @@ export const getInfraKubernetesHref = ( return addBasePath(basePath, `/app/metrics/link-to/pod-detail/${encodeURIComponent(ret)}`); }; - return buildHref(summary.state.checks || [], 'kubernetes.pod.uid', getHref); + return buildHref(summary.state.summaryPings || [], 'kubernetes.pod.uid', getHref); }; export const getInfraIpHref = (summary: MonitorSummary, basePath: string) => { @@ -63,5 +63,5 @@ export const getInfraIpHref = (summary: MonitorSummary, basePath: string) => { `/app/metrics/inventory?waffleFilter=(expression:'${encodeURIComponent(ips)}',kind:kuery)` ); }; - return buildHref(summary.state.checks || [], 'monitor.ip', getHref); + return buildHref(summary.state.summaryPings || [], 'monitor.ip', getHref); }; diff --git a/x-pack/plugins/uptime/public/lib/helper/observability_integration/get_logging_href.ts b/x-pack/plugins/uptime/public/lib/helper/observability_integration/get_logging_href.ts index 222c7b57c927..c4fee330e976 100644 --- a/x-pack/plugins/uptime/public/lib/helper/observability_integration/get_logging_href.ts +++ b/x-pack/plugins/uptime/public/lib/helper/observability_integration/get_logging_href.ts @@ -22,7 +22,7 @@ export const getLoggingContainerHref = ( `/app/logs?logFilter=${encodeURI(`(expression:'container.id : ${ret}',kind:kuery)`)}` ); }; - return buildHref(summary.state.checks || [], 'container.id', getHref); + return buildHref(summary.state.summaryPings || [], 'container.id', getHref); }; export const getLoggingKubernetesHref = (summary: MonitorSummary, basePath: string) => { @@ -36,7 +36,7 @@ export const getLoggingKubernetesHref = (summary: MonitorSummary, basePath: stri `/app/logs?logFilter=${encodeURI(`(expression:'pod.uid : ${ret}',kind:kuery)`)}` ); }; - return buildHref(summary.state.checks || [], 'kubernetes.pod.uid', getHref); + return buildHref(summary.state.summaryPings || [], 'kubernetes.pod.uid', getHref); }; export const getLoggingIpHref = (summary: MonitorSummary, basePath: string) => { @@ -50,5 +50,5 @@ export const getLoggingIpHref = (summary: MonitorSummary, basePath: string) => { `/app/logs?logFilter=(expression:'${encodeURIComponent(`host.ip : ${ret}`)}',kind:kuery)` ); }; - return buildHref(summary.state.checks || [], 'monitor.ip', getHref); + return buildHref(summary.state.summaryPings || [], 'monitor.ip', getHref); }; diff --git a/x-pack/plugins/uptime/public/state/actions/monitor_list.ts b/x-pack/plugins/uptime/public/state/actions/monitor_list.ts index ee2267a9058a..da1c80f061c3 100644 --- a/x-pack/plugins/uptime/public/state/actions/monitor_list.ts +++ b/x-pack/plugins/uptime/public/state/actions/monitor_list.ts @@ -5,8 +5,10 @@ */ import { createAction } from 'redux-actions'; -import { FetchMonitorStatesQueryArgs, MonitorSummaryResult } from '../../../common/runtime_types'; +import { FetchMonitorStatesQueryArgs, MonitorSummariesResult } from '../../../common/runtime_types'; export const getMonitorList = createAction('GET_MONITOR_LIST'); -export const getMonitorListSuccess = createAction('GET_MONITOR_LIST_SUCCESS'); +export const getMonitorListSuccess = createAction( + 'GET_MONITOR_LIST_SUCCESS' +); export const getMonitorListFailure = createAction('GET_MONITOR_LIST_FAIL'); diff --git a/x-pack/plugins/uptime/public/state/api/monitor_list.ts b/x-pack/plugins/uptime/public/state/api/monitor_list.ts index 084bcb4bd2a9..7c595dc255c8 100644 --- a/x-pack/plugins/uptime/public/state/api/monitor_list.ts +++ b/x-pack/plugins/uptime/public/state/api/monitor_list.ts @@ -8,12 +8,12 @@ import { API_URLS } from '../../../common/constants'; import { apiService } from './utils'; import { FetchMonitorStatesQueryArgs, - MonitorSummaryResult, - MonitorSummaryResultType, + MonitorSummariesResult, + MonitorSummariesResultType, } from '../../../common/runtime_types'; export const fetchMonitorList = async ( params: FetchMonitorStatesQueryArgs -): Promise => { - return await apiService.get(API_URLS.MONITOR_LIST, params, MonitorSummaryResultType); +): Promise => { + return await apiService.get(API_URLS.MONITOR_LIST, params, MonitorSummariesResultType); }; diff --git a/x-pack/plugins/uptime/public/state/api/utils.ts b/x-pack/plugins/uptime/public/state/api/utils.ts index f2efd2ecb875..4f3765275c49 100644 --- a/x-pack/plugins/uptime/public/state/api/utils.ts +++ b/x-pack/plugins/uptime/public/state/api/utils.ts @@ -7,6 +7,31 @@ import { PathReporter } from 'io-ts/lib/PathReporter'; import { isRight } from 'fp-ts/lib/Either'; import { HttpFetchQuery, HttpSetup } from 'src/core/public'; +import * as t from 'io-ts'; +import { isObject } from 'lodash'; + +// TODO: Copied from https://github.com/elastic/kibana/blob/master/x-pack/plugins/security_solution/common/format_errors.ts +// We should figure out a better way to share this +export const formatErrors = (errors: t.Errors): string[] => { + return errors.map((error) => { + if (error.message != null) { + return error.message; + } else { + const keyContext = error.context + .filter( + (entry) => entry.key != null && !Number.isInteger(+entry.key) && entry.key.trim() !== '' + ) + .map((entry) => entry.key) + .join('.'); + + const nameContext = error.context.find((entry) => entry.type?.name?.length > 0); + const suppliedValue = + keyContext !== '' ? keyContext : nameContext != null ? nameContext.type.name : ''; + const value = isObject(error.value) ? JSON.stringify(error.value) : error.value; + return `Invalid value "${value}" supplied to "${suppliedValue}"`; + } + }); +}; class ApiService { private static instance: ApiService; @@ -40,7 +65,10 @@ class ApiService { } else { // eslint-disable-next-line no-console console.error( - `API ${apiUrl} is not returning expected response, ${PathReporter.report(decoded)}` + `API ${apiUrl} is not returning expected response, ${formatErrors( + decoded.left + )} for response`, + response ); } } diff --git a/x-pack/plugins/uptime/public/state/reducers/monitor_list.ts b/x-pack/plugins/uptime/public/state/reducers/monitor_list.ts index 59a794a549d5..c4ba445eb123 100644 --- a/x-pack/plugins/uptime/public/state/reducers/monitor_list.ts +++ b/x-pack/plugins/uptime/public/state/reducers/monitor_list.ts @@ -6,12 +6,12 @@ import { handleActions, Action } from 'redux-actions'; import { getMonitorList, getMonitorListSuccess, getMonitorListFailure } from '../actions'; -import { MonitorSummaryResult } from '../../../common/runtime_types'; +import { MonitorSummariesResult } from '../../../common/runtime_types'; export interface MonitorList { error?: Error; loading: boolean; - list: MonitorSummaryResult; + list: MonitorSummariesResult; } export const initialState: MonitorList = { @@ -24,7 +24,7 @@ export const initialState: MonitorList = { loading: false, }; -type Payload = MonitorSummaryResult & Error; +type Payload = MonitorSummariesResult & Error; export const monitorListReducer = handleActions( { @@ -34,7 +34,7 @@ export const monitorListReducer = handleActions( }), [String(getMonitorListSuccess)]: ( state: MonitorList, - action: Action + action: Action ) => ({ ...state, loading: false, diff --git a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_latest_monitor.test.ts b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_latest_monitor.test.ts index aa3574874af8..01384ec14523 100644 --- a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_latest_monitor.test.ts +++ b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_latest_monitor.test.ts @@ -32,14 +32,7 @@ describe('getLatestMonitor', () => { }, }, size: 1, - _source: [ - 'url', - 'monitor', - 'observer', - '@timestamp', - 'tls.certificate_not_valid_after', - 'tls.certificate_not_valid_before', - ], + _source: ['url', 'monitor', 'observer', '@timestamp', 'tls.*'], sort: { '@timestamp': { order: 'desc' }, }, @@ -90,10 +83,7 @@ describe('getLatestMonitor', () => { "type": "http", }, "timestamp": "123456", - "tls": Object { - "not_after": undefined, - "not_before": undefined, - }, + "tls": undefined, } `); expect(result.timestamp).toBe('123456'); diff --git a/x-pack/plugins/uptime/server/lib/requests/get_latest_monitor.ts b/x-pack/plugins/uptime/server/lib/requests/get_latest_monitor.ts index a47e6173d9f0..a58208fc2bb9 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_latest_monitor.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_latest_monitor.ts @@ -45,14 +45,7 @@ export const getLatestMonitor: UMElasticsearchQueryFn { if (!p) { @@ -43,7 +38,7 @@ const jsonifyPagination = (p: any): string | null => { // Gets a page of monitor states. export const getMonitorStates: UMElasticsearchQueryFn< GetMonitorStatesParams, - GetMonitorStatesResult + MonitorSummariesResult > = async ({ callES, dynamicSettings, @@ -68,11 +63,113 @@ export const getMonitorStates: UMElasticsearchQueryFn< statusFilter ); - const page = await fetchPage(queryContext); + const size = Math.min(queryContext.size, QUERY.DEFAULT_AGGS_CAP); + + const iterator = new MonitorSummaryIterator(queryContext); + const page = await iterator.nextPage(size); + + const histograms = await getHistogramForMonitors( + queryContext, + page.monitorSummaries.map((s) => s.monitor_id) + ); + + page.monitorSummaries.forEach((s) => { + s.histogram = histograms[s.monitor_id]; + }); return { - summaries: page.items, + summaries: page.monitorSummaries, nextPagePagination: jsonifyPagination(page.nextPagePagination), prevPagePagination: jsonifyPagination(page.prevPagePagination), }; }; + +export const getHistogramForMonitors = async ( + queryContext: QueryContext, + monitorIds: string[] +): Promise<{ [key: string]: Histogram }> => { + const params = { + index: queryContext.heartbeatIndices, + body: { + size: 0, + query: { + bool: { + filter: [ + { + range: { + 'summary.down': { gt: 0 }, + }, + }, + { + terms: { + 'monitor.id': monitorIds, + }, + }, + { + range: { + '@timestamp': { + gte: queryContext.dateRangeStart, + lte: queryContext.dateRangeEnd, + }, + }, + }, + ], + }, + }, + aggs: { + histogram: { + date_histogram: { + field: '@timestamp', + // 12 seems to be a good size for performance given + // long monitor lists of up to 100 on the overview page + fixed_interval: + getHistogramInterval(queryContext.dateRangeStart, queryContext.dateRangeEnd, 12) + + 'ms', + missing: 0, + }, + aggs: { + by_id: { + terms: { + field: 'monitor.id', + size: Math.max(monitorIds.length, 1), + }, + aggs: { + totalDown: { + sum: { field: 'summary.down' }, + }, + }, + }, + }, + }, + }, + }, + }; + const result = await queryContext.search(params); + + const histoBuckets: any[] = result.aggregations.histogram.buckets; + const simplified = histoBuckets.map((histoBucket: any): { timestamp: number; byId: any } => { + const byId: { [key: string]: number } = {}; + histoBucket.by_id.buckets.forEach((idBucket: any) => { + byId[idBucket.key] = idBucket.totalDown.value; + }); + return { + timestamp: parseInt(histoBucket.key, 10), + byId, + }; + }); + + const histosById: { [key: string]: Histogram } = {}; + monitorIds.forEach((id: string) => { + const points: HistogramPoint[] = []; + simplified.forEach((simpleHisto) => { + points.push({ + timestamp: simpleHisto.timestamp, + up: undefined, + down: simpleHisto.byId[id], + }); + }); + histosById[id] = { points }; + }); + + return histosById; +}; diff --git a/x-pack/plugins/uptime/server/lib/requests/search/__tests__/enrich_monitor_groups.test.ts b/x-pack/plugins/uptime/server/lib/requests/search/__tests__/enrich_monitor_groups.test.ts deleted file mode 100644 index dd7996b68c41..000000000000 --- a/x-pack/plugins/uptime/server/lib/requests/search/__tests__/enrich_monitor_groups.test.ts +++ /dev/null @@ -1,98 +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 { sortChecksBy } from '../enrich_monitor_groups'; - -describe('enrich monitor groups', () => { - describe('sortChecksBy', () => { - it('identifies lesser geo name', () => { - expect( - sortChecksBy( - { observer: { geo: { name: 'less' } }, monitor: { status: 'up' } }, - { observer: { geo: { name: 'more' } }, monitor: { status: 'up' } } - ) - ).toBe(-1); - }); - - it('identifies greater geo name', () => { - expect( - sortChecksBy( - { observer: { geo: { name: 'more' } }, monitor: { status: 'up' } }, - { observer: { geo: { name: 'less' } }, monitor: { status: 'up' } } - ) - ).toBe(1); - }); - - it('identifies equivalent geo name and sorts by lesser ip', () => { - expect( - sortChecksBy( - { observer: { geo: { name: 'same' } }, monitor: { ip: '127.0.0.1', status: 'up' } }, - { observer: { geo: { name: 'same' } }, monitor: { ip: '127.0.0.2', status: 'up' } } - ) - ).toBe(-1); - }); - - it('identifies equivalent geo name and sorts by greater ip', () => { - expect( - sortChecksBy( - { observer: { geo: { name: 'same' } }, monitor: { ip: '127.0.0.2', status: 'up' } }, - { observer: { geo: { name: 'same' } }, monitor: { ip: '127.0.0.1', status: 'up' } } - ) - ).toBe(1); - }); - - it('identifies equivalent geo name and sorts by equivalent ip', () => { - expect( - sortChecksBy( - { observer: { geo: { name: 'same' } }, monitor: { ip: '127.0.0.1', status: 'up' } }, - { observer: { geo: { name: 'same' } }, monitor: { ip: '127.0.0.1', status: 'up' } } - ) - ).toBe(0); - }); - - it('handles equivalent ip arrays', () => { - expect( - sortChecksBy( - { observer: { geo: { name: 'same' } }, monitor: { ip: ['127.0.0.1'], status: 'up' } }, - { observer: { geo: { name: 'same' } }, monitor: { ip: ['127.0.0.1'], status: 'up' } } - ) - ).toBe(0); - }); - - it('handles non-equal ip arrays', () => { - expect( - sortChecksBy( - { - observer: { geo: { name: 'same' } }, - monitor: { ip: ['127.0.0.2', '127.0.0.9'], status: 'up' }, - }, - { - observer: { geo: { name: 'same' } }, - monitor: { ip: ['127.0.0.3', '127.0.0.1'], status: 'up' }, - } - ) - ).toBe(1); - }); - - it('handles undefined observer fields', () => { - expect( - sortChecksBy( - { observer: undefined, monitor: { ip: ['127.0.0.1'], status: 'up' } }, - { observer: { geo: { name: 'same' } }, monitor: { ip: ['127.0.0.1'], status: 'up' } } - ) - ).toBe(-1); - }); - - it('handles undefined ip fields', () => { - expect( - sortChecksBy( - { observer: { geo: { name: 'same' } }, monitor: { ip: undefined, status: 'up' } }, - { observer: { geo: { name: 'same' } }, monitor: { ip: ['127.0.0.1'], status: 'up' } } - ) - ).toBe(-1); - }); - }); -}); diff --git a/x-pack/plugins/uptime/server/lib/requests/search/__tests__/fetch_page.test.ts b/x-pack/plugins/uptime/server/lib/requests/search/__tests__/fetch_page.test.ts deleted file mode 100644 index b98a5afcca33..000000000000 --- a/x-pack/plugins/uptime/server/lib/requests/search/__tests__/fetch_page.test.ts +++ /dev/null @@ -1,111 +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 { - fetchPage, - MonitorEnricher, - MonitorGroups, - MonitorGroupsFetcher, - MonitorGroupsPage, -} from '../fetch_page'; -import { QueryContext } from '../query_context'; -import { MonitorSummary } from '../../../../../common/runtime_types'; -import { nextPagination, prevPagination, simpleQueryContext } from './test_helpers'; - -const simpleFixture: MonitorGroups[] = [ - { - id: 'foo', - groups: [ - { - monitorId: 'foo', - location: 'foo-loc', - checkGroup: 'foo-check', - status: 'up', - summaryTimestamp: new Date(), - }, - ], - }, - { - id: 'bar', - groups: [ - { - monitorId: 'bar', - location: 'bar-loc', - checkGroup: 'bar-check', - status: 'down', - summaryTimestamp: new Date(), - }, - ], - }, -]; - -const simpleFetcher = (monitorGroups: MonitorGroups[]): MonitorGroupsFetcher => { - return async (queryContext: QueryContext, size: number): Promise => { - return { - monitorGroups, - prevPagePagination: prevPagination(monitorGroups[0].id), - nextPagePagination: nextPagination(monitorGroups[monitorGroups.length - 1].id), - }; - }; -}; - -const simpleEnricher = (monitorGroups: MonitorGroups[]): MonitorEnricher => { - return async (_queryContext: QueryContext, checkGroups: string[]): Promise => { - return checkGroups.map((cg) => { - const monitorGroup = monitorGroups.find((mg) => mg.groups.some((g) => g.checkGroup === cg))!; - return { - monitor_id: monitorGroup.id, - state: { - summary: {}, - timestamp: new Date(Date.parse('1999-12-31')).valueOf().toString(), - url: {}, - }, - }; - }); - }; -}; - -describe('fetching a page', () => { - it('returns the enriched groups', async () => { - const res = await fetchPage( - simpleQueryContext(), - simpleFetcher(simpleFixture), - simpleEnricher(simpleFixture) - ); - expect(res).toMatchInlineSnapshot(` - Object { - "items": Array [ - Object { - "monitor_id": "foo", - "state": Object { - "summary": Object {}, - "timestamp": "946598400000", - "url": Object {}, - }, - }, - Object { - "monitor_id": "bar", - "state": Object { - "summary": Object {}, - "timestamp": "946598400000", - "url": Object {}, - }, - }, - ], - "nextPagePagination": Object { - "cursorDirection": "AFTER", - "cursorKey": "bar", - "sortOrder": "ASC", - }, - "prevPagePagination": Object { - "cursorDirection": "BEFORE", - "cursorKey": "foo", - "sortOrder": "ASC", - }, - } - `); - }); -}); diff --git a/x-pack/plugins/uptime/server/lib/requests/search/__tests__/monitor_group_iterator.test.ts b/x-pack/plugins/uptime/server/lib/requests/search/__tests__/monitor_group_iterator.test.ts deleted file mode 100644 index 0ce5e7519547..000000000000 --- a/x-pack/plugins/uptime/server/lib/requests/search/__tests__/monitor_group_iterator.test.ts +++ /dev/null @@ -1,100 +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 { - CHUNK_SIZE, - ChunkFetcher, - ChunkResult, - MonitorGroupIterator, -} from '../monitor_group_iterator'; -import { simpleQueryContext } from './test_helpers'; -import { MonitorGroups } from '../fetch_page'; -import { QueryContext } from '../query_context'; - -describe('iteration', () => { - let iterator: MonitorGroupIterator | null = null; - let fetched: MonitorGroups[]; - - const setup = async (numGroups: number) => { - fetched = []; - const expectedMonitorGroups = makeMonitorGroups(numGroups); - const chunkFetcher = mockChunkFetcher(expectedMonitorGroups); - iterator = new MonitorGroupIterator(simpleQueryContext(), [], -1, chunkFetcher); - - while (true) { - const got = await iterator.next(); - if (got) { - fetched.push(got); - } else { - break; - } - } - }; - - describe('matching', () => { - [ - { name: 'zero results', numGroups: 0 }, - { name: 'one result', numGroups: 1 }, - { name: 'less than chunk', numGroups: CHUNK_SIZE - 1 }, - { name: 'multiple full chunks', numGroups: CHUNK_SIZE * 3 }, - { name: 'multiple full chunks + partial', numGroups: CHUNK_SIZE * 3 + 3 }, - ].forEach(({ name, numGroups }) => { - describe(`scenario given ${name}`, () => { - beforeEach(async () => { - await setup(numGroups); - }); - - it('should receive the expected number of results', () => { - expect(fetched.length).toEqual(numGroups); - }); - - it('should have no remaining pages', async () => { - expect(await iterator!.paginationAfterCurrent()).toBeNull(); - }); - }); - }); - }); -}); - -const makeMonitorGroups = (count: number): MonitorGroups[] => { - const groups: MonitorGroups[] = []; - for (let i = 0; i < count; i++) { - const id = `monitor-${i}`; - - groups.push({ - id, - groups: [ - { - monitorId: id, - location: 'a-location', - status: 'up', - checkGroup: `check-group-${i}`, - summaryTimestamp: new Date(), - }, - ], - }); - } - return groups; -}; - -const mockChunkFetcher = (groups: MonitorGroups[]): ChunkFetcher => { - const buffer = groups.slice(0); // Clone it since we'll modify it - return async ( - queryContext: QueryContext, - searchAfter: any, - size: number - ): Promise => { - const resultMonitorGroups = buffer.splice(0, size); - const resultSearchAfter = - buffer.length === 0 - ? null - : { monitor_id: resultMonitorGroups[resultMonitorGroups.length - 1].id }; - return { - monitorGroups: resultMonitorGroups, - searchAfter: resultSearchAfter, - }; - }; -}; diff --git a/x-pack/plugins/uptime/server/lib/requests/search/__tests__/monitor_summary_iterator.test.ts b/x-pack/plugins/uptime/server/lib/requests/search/__tests__/monitor_summary_iterator.test.ts new file mode 100644 index 000000000000..8ba5be943304 --- /dev/null +++ b/x-pack/plugins/uptime/server/lib/requests/search/__tests__/monitor_summary_iterator.test.ts @@ -0,0 +1,131 @@ +/* + * 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 { + CHUNK_SIZE, + ChunkFetcher, + ChunkResult, + MonitorSummaryIterator, +} from '../monitor_summary_iterator'; +import { simpleQueryContext } from './test_helpers'; +import { QueryContext } from '../query_context'; +import { MonitorSummary } from '../../../../../common/runtime_types'; + +describe('iteration', () => { + let iterator: MonitorSummaryIterator | null = null; + let fetched: MonitorSummary[]; + let fullSummaryDataset: MonitorSummary[]; + + const setup = async (numSummaries: number) => { + fetched = []; + fullSummaryDataset = makeMonitorSummaries(numSummaries); + const chunkFetcher = mockChunkFetcher(fullSummaryDataset); + iterator = new MonitorSummaryIterator(simpleQueryContext(), [], -1, chunkFetcher); + }; + + const fetchAllViaNext = async () => { + while (true) { + const got = await iterator!.next(); + if (got) { + fetched.push(got); + } else { + break; + } + } + }; + + describe('matching', () => { + [ + { name: 'zero results', numSummaries: 0 }, + { name: 'one result', numSummaries: 1 }, + { name: 'less than chunk', numSummaries: CHUNK_SIZE - 1 }, + { name: 'multiple full chunks', numSummaries: CHUNK_SIZE * 3 }, + { name: 'multiple full chunks + partial', numSummaries: CHUNK_SIZE * 3 + 3 }, + ].forEach(({ name, numSummaries }) => { + describe(`scenario given ${name}`, () => { + beforeEach(async () => { + await setup(numSummaries); + }); + + describe('fetching via next', () => { + beforeEach(async () => { + await fetchAllViaNext(); + }); + + it('should receive the expected number of results', async () => { + expect(fetched.length).toEqual(numSummaries); + }); + + it('should have no remaining pages', async () => { + expect(await iterator!.paginationAfterCurrent()).toBeNull(); + }); + }); + + describe('nextPage()', () => { + const pageSize = 900; + + it('should fetch no more than the page size results', async () => { + const page = await iterator!.nextPage(pageSize); + const expectedLength = numSummaries < pageSize ? numSummaries : pageSize; + expect(page.monitorSummaries).toHaveLength(expectedLength); + }); + + it('should return all the results if called until none remain', async () => { + const receivedResults: MonitorSummary[] = []; + while (true) { + const page = await iterator!.nextPage(pageSize); + if (page.monitorSummaries.length === 0) { + break; + } + page.monitorSummaries.forEach((s) => receivedResults.push(s)); + } + expect(receivedResults.length).toEqual(fullSummaryDataset.length); + }); + }); + }); + }); + }); +}); + +const makeMonitorSummaries = (count: number): MonitorSummary[] => { + const summaries: MonitorSummary[] = []; + for (let i = 0; i < count; i++) { + const id = `monitor-${i}`; + + summaries.push({ + monitor_id: id, + state: { + monitor: {}, + timestamp: (123 + i).toString(), + url: {}, + summaryPings: [], + summary: { up: 1, down: 0 }, + }, + }); + } + return summaries; +}; + +const mockChunkFetcher = (summaries: MonitorSummary[]): ChunkFetcher => { + const buffer = summaries.slice(0); // Clone it since we'll modify it + return async ( + queryContext: QueryContext, + searchAfter: any, + size: number + ): Promise => { + const offset = searchAfter?.monitor_id ? parseInt(searchAfter.monitor_id.split('-')[1], 10) : 0; + const resultMonitorSummaries = buffer.slice(offset, offset + size); + const resultSearchAfter = + offset > buffer.length - 1 + ? null + : { monitor_id: `monitor-${offset + resultMonitorSummaries.length}` }; + + return { + monitorSummaries: resultMonitorSummaries, + searchAfter: resultSearchAfter, + }; + }; +}; diff --git a/x-pack/plugins/uptime/server/lib/requests/search/__tests__/refine_potential_matches.test.ts b/x-pack/plugins/uptime/server/lib/requests/search/__tests__/refine_potential_matches.test.ts deleted file mode 100644 index 283f5fb8909f..000000000000 --- a/x-pack/plugins/uptime/server/lib/requests/search/__tests__/refine_potential_matches.test.ts +++ /dev/null @@ -1,112 +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 { fullyMatchingIds } from '../refine_potential_matches'; -import { MonitorLocCheckGroup } from '..'; - -const mockQueryResult = (opts: { latestSummary: any; latestMatching: any }) => { - return { - aggregations: { - monitor: { - buckets: [ - { - key: 'my-monitor', - location: { - buckets: [ - { - key: 'my-location', - summaries: { - latest: { - hits: { - hits: [ - { - _source: opts.latestSummary, - }, - ], - }, - }, - }, - latest_matching: { - top: { - hits: { - hits: [ - { - _source: opts.latestMatching, - }, - ], - }, - }, - }, - }, - ], - }, - }, - ], - }, - }, - }; -}; - -describe('fully matching IDs', () => { - it('should exclude items whose latest result does not match', () => { - const queryRes = mockQueryResult({ - latestSummary: { - '@timestamp': '2020-06-04T12:39:54.698-0500', - monitor: { - check_group: 'latest-summary-check-group', - }, - summary: { - up: 1, - down: 0, - }, - }, - latestMatching: { - '@timestamp': '2019-06-04T12:39:54.698-0500', - summary: { - up: 1, - down: 0, - }, - }, - }); - const res = fullyMatchingIds(queryRes, undefined); - const expected = new Map(); - expect(res).toEqual(expected); - }); - - it('should include items whose latest result does match', () => { - const queryRes = mockQueryResult({ - latestSummary: { - '@timestamp': '2020-06-04T12:39:54.698-0500', - monitor: { - check_group: 'latest-summary-check-group', - }, - summary: { - up: 1, - down: 0, - }, - }, - latestMatching: { - '@timestamp': '2020-06-04T12:39:54.698-0500', - summary: { - up: 1, - down: 0, - }, - }, - }); - const res = fullyMatchingIds(queryRes, undefined); - const expected = new Map(); - expected.set('my-monitor', [ - { - checkGroup: 'latest-summary-check-group', - location: 'my-location', - monitorId: 'my-monitor', - status: 'up', - summaryTimestamp: new Date('2020-06-04T12:39:54.698-0500'), - }, - ]); - expect(res).toEqual(expected); - }); -}); diff --git a/x-pack/plugins/uptime/server/lib/requests/search/enrich_monitor_groups.ts b/x-pack/plugins/uptime/server/lib/requests/search/enrich_monitor_groups.ts deleted file mode 100644 index f5c4c55a4e30..000000000000 --- a/x-pack/plugins/uptime/server/lib/requests/search/enrich_monitor_groups.ts +++ /dev/null @@ -1,409 +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 { QueryContext } from './query_context'; -import { - Check, - Histogram, - HistogramPoint, - MonitorSummary, - CursorDirection, - SortOrder, -} from '../../../../common/runtime_types'; -import { MonitorEnricher } from './fetch_page'; -import { getHistogramInterval } from '../../helper/get_histogram_interval'; - -export const enrichMonitorGroups: MonitorEnricher = async ( - queryContext: QueryContext, - checkGroups: string[] -): Promise => { - // TODO the scripted metric query here is totally unnecessary and largely - // redundant with the way the code works now. This could be simplified - // to a much simpler query + some JS processing. - const params = { - index: queryContext.heartbeatIndices, - body: { - query: { - bool: { - filter: [{ terms: { 'monitor.check_group': checkGroups } }], - }, - }, - size: 0, - aggs: { - monitors: { - composite: { - /** - * TODO: extract this to a constant; we can't be looking for _only_ - * ten monitors, because it's possible our check groups selection will represent more than ten. - * - * We were previously passing the after key from the check groups query regardless of the number of monitors we had, - * it's important that the latest check group from the final monitor we use is what we return, or we will be way ahead in terms - * of check groups and end up skipping monitors on subsequent calls. - */ - size: 500, - sources: [ - { - monitor_id: { - terms: { - field: 'monitor.id', - order: cursorDirectionToOrder(queryContext.pagination.cursorDirection), - }, - }, - }, - ], - }, - aggregations: { - state: { - scripted_metric: { - init_script: ` - // Globals are values that should be identical across all docs - // We can cheat a bit by always overwriting these and make the - // assumption that there is no variation in these across checks - state.globals = new HashMap(); - // Here we store stuff broken out by agent.id and monitor.id - // This should correspond to a unique check. - state.checksByAgentIdIP = new HashMap(); - `, - map_script: ` - Map curCheck = new HashMap(); - String agentId = doc["agent.id"][0]; - String ip = null; - if (doc["monitor.ip"].length > 0) { - ip = doc["monitor.ip"][0]; - } - 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"]; - for (field in fields) { - String docPath = "url." + field; - def val = doc[docPath]; - if (!val.isEmpty()) { - url[field] = val[0]; - } - } - state.globals.url = url; - } - - Map monitor = new HashMap(); - monitor.status = doc["monitor.status"][0]; - monitor.ip = ip; - if (!doc["monitor.name"].isEmpty()) { - String monitorName = doc["monitor.name"][0]; - if (monitor.name != "") { - monitor.name = monitorName; - } - } - curCheck.monitor = monitor; - - if (curCheck.observer == null) { - curCheck.observer = new HashMap(); - } - if (curCheck.observer.geo == null) { - curCheck.observer.geo = new HashMap(); - } - if (!doc["observer.geo.name"].isEmpty()) { - curCheck.observer.geo.name = doc["observer.geo.name"][0]; - } - if (!doc["observer.geo.location"].isEmpty()) { - curCheck.observer.geo.location = doc["observer.geo.location"][0]; - } - if (!doc["kubernetes.pod.uid"].isEmpty() && curCheck.kubernetes == null) { - curCheck.kubernetes = new HashMap(); - curCheck.kubernetes.pod = new HashMap(); - curCheck.kubernetes.pod.uid = doc["kubernetes.pod.uid"][0]; - } - if (!doc["container.id"].isEmpty() && curCheck.container == null) { - curCheck.container = new HashMap(); - curCheck.container.id = doc["container.id"][0]; - } - if (curCheck.tls == null) { - curCheck.tls = new HashMap(); - } - if (!doc["tls.certificate_not_valid_after"].isEmpty()) { - curCheck.tls.not_after = doc["tls.certificate_not_valid_after"][0]; - } - if (!doc["tls.certificate_not_valid_before"].isEmpty()) { - curCheck.tls.not_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(); - Collection geoNames = new HashSet(); - Collection podUids = new HashSet(); - Collection containerIds = new HashSet(); - Collection tls = new HashSet(); - String name = null; - for (state in states) { - result.putAll(state.globals); - for (entry in state.checksByAgentIdIP.entrySet()) { - def agentIdIP = entry.getKey(); - 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); - } - if (check.kubernetes != null && check.kubernetes.pod != null) { - podUids.add(check.kubernetes.pod.uid); - } - if (check.container != null) { - containerIds.add(check.container.id); - } - if (check.tls != null) { - tls.add(check.tls); - } - } - } - - // 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 ? "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; - `, - }, - }, - }, - }, - }, - }, - }; - - const items = await queryContext.search(params); - - const monitorBuckets = items?.aggregations?.monitors?.buckets ?? []; - - const monitorIds: string[] = []; - const summaries: MonitorSummary[] = monitorBuckets.map((monitor: any) => { - const monitorId = monitor.key.monitor_id; - monitorIds.push(monitorId); - const state: any = monitor.state?.value; - state.timestamp = state['@timestamp']; - const { checks } = state; - if (Array.isArray(checks)) { - checks.sort(sortChecksBy); - state.checks = state.checks.map((check: any) => ({ - ...check, - timestamp: check['@timestamp'], - })); - } else { - state.checks = []; - } - return { - monitor_id: monitorId, - state, - }; - }); - - const histogramMap = await getHistogramForMonitors(queryContext, monitorIds); - - const resItems = summaries.map((summary) => ({ - ...summary, - histogram: histogramMap[summary.monitor_id], - })); - - const sortedResItems: any = resItems.sort((a, b) => { - if (a.monitor_id === b.monitor_id) return 0; - return a.monitor_id > b.monitor_id ? 1 : -1; - }); - - if (queryContext.pagination.sortOrder === SortOrder.DESC) { - sortedResItems.reverse(); - } - - return sortedResItems; -}; - -const getHistogramForMonitors = async ( - queryContext: QueryContext, - monitorIds: string[] -): Promise<{ [key: string]: Histogram }> => { - const params = { - index: queryContext.heartbeatIndices, - body: { - size: 0, - query: { - bool: { - filter: [ - { - range: { - 'summary.down': { gt: 0 }, - }, - }, - { - terms: { - 'monitor.id': monitorIds, - }, - }, - { - range: { - '@timestamp': { - gte: queryContext.dateRangeStart, - lte: queryContext.dateRangeEnd, - }, - }, - }, - ], - }, - }, - aggs: { - histogram: { - date_histogram: { - field: '@timestamp', - // 12 seems to be a good size for performance given - // long monitor lists of up to 100 on the overview page - fixed_interval: - getHistogramInterval(queryContext.dateRangeStart, queryContext.dateRangeEnd, 12) + - 'ms', - missing: 0, - }, - aggs: { - by_id: { - terms: { - field: 'monitor.id', - size: Math.max(monitorIds.length, 1), - }, - aggs: { - totalDown: { - sum: { field: 'summary.down' }, - }, - }, - }, - }, - }, - }, - }, - }; - const result = await queryContext.search(params); - - const histoBuckets: any[] = result.aggregations.histogram.buckets; - const simplified = histoBuckets.map((histoBucket: any): { timestamp: number; byId: any } => { - const byId: { [key: string]: number } = {}; - histoBucket.by_id.buckets.forEach((idBucket: any) => { - byId[idBucket.key] = idBucket.totalDown.value; - }); - return { - timestamp: parseInt(histoBucket.key, 10), - byId, - }; - }); - - const histosById: { [key: string]: Histogram } = {}; - monitorIds.forEach((id: string) => { - const points: HistogramPoint[] = []; - simplified.forEach((simpleHisto) => { - points.push({ - timestamp: simpleHisto.timestamp, - up: undefined, - down: simpleHisto.byId[id], - }); - }); - histosById[id] = { points }; - }); - - return histosById; -}; - -const cursorDirectionToOrder = (cd: CursorDirection): 'asc' | 'desc' => { - return CursorDirection[cd] === CursorDirection.AFTER ? 'asc' : 'desc'; -}; - -const getStringValue = (value: string | Array | null | undefined): string => { - if (Array.isArray(value)) { - value.sort(); - return value[0] ?? ''; - } - return value ?? ''; -}; - -export const sortChecksBy = ( - a: Pick, - b: Pick -) => { - const nameA: string = a.observer?.geo?.name ?? ''; - const nameB: string = b.observer?.geo?.name ?? ''; - - if (nameA === nameB) { - const ipA = getStringValue(a.monitor.ip); - const ipB = getStringValue(b.monitor.ip); - - if (ipA === ipB) { - return 0; - } - return ipA > ipB ? 1 : -1; - } - return nameA > nameB ? 1 : -1; -}; diff --git a/x-pack/plugins/uptime/server/lib/requests/search/fetch_chunk.ts b/x-pack/plugins/uptime/server/lib/requests/search/fetch_chunk.ts index 2a5f1f1261cb..82bc5e012386 100644 --- a/x-pack/plugins/uptime/server/lib/requests/search/fetch_chunk.ts +++ b/x-pack/plugins/uptime/server/lib/requests/search/fetch_chunk.ts @@ -6,7 +6,7 @@ import { refinePotentialMatches } from './refine_potential_matches'; import { findPotentialMatches } from './find_potential_matches'; -import { ChunkFetcher, ChunkResult } from './monitor_group_iterator'; +import { ChunkFetcher, ChunkResult } from './monitor_summary_iterator'; import { QueryContext } from './query_context'; /** @@ -18,9 +18,6 @@ import { QueryContext } from './query_context'; * @param searchAfter indicates where Elasticsearch should continue querying on subsequent requests, if at all * @param size the minimum size of the matches to chunk */ -// Note that all returned data may be erroneous. If `searchAfter` is returned the caller should invoke this function -// repeatedly with the new searchAfter value as there may be more matching data in a future chunk. If `searchAfter` -// is falsey there is no more data to fetch. export const fetchChunk: ChunkFetcher = async ( queryContext: QueryContext, searchAfter: any, @@ -34,7 +31,7 @@ export const fetchChunk: ChunkFetcher = async ( const matching = await refinePotentialMatches(queryContext, monitorIds); return { - monitorGroups: matching, + monitorSummaries: matching, searchAfter: foundSearchAfter, }; }; diff --git a/x-pack/plugins/uptime/server/lib/requests/search/fetch_page.ts b/x-pack/plugins/uptime/server/lib/requests/search/fetch_page.ts deleted file mode 100644 index 831236d8ccb5..000000000000 --- a/x-pack/plugins/uptime/server/lib/requests/search/fetch_page.ts +++ /dev/null @@ -1,138 +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 { flatten } from 'lodash'; -import { CursorPagination } from './types'; -import { QueryContext } from './query_context'; -import { QUERY } from '../../../../common/constants'; -import { CursorDirection, MonitorSummary, SortOrder } from '../../../../common/runtime_types'; -import { enrichMonitorGroups } from './enrich_monitor_groups'; -import { MonitorGroupIterator } from './monitor_group_iterator'; - -/** - * - * Gets a single page of results per the settings in the provided queryContext. These results are very minimal, - * just monitor IDs and check groups. This takes an optional `MonitorGroupEnricher` that post-processes the minimal - * data, decorating it appropriately. The function also takes a fetcher, which does all the actual fetching. - * @param queryContext defines the criteria for the data on the current page - * @param monitorGroupFetcher performs paginated monitor fetching - * @param monitorEnricher decorates check group results with additional data - */ -// just monitor IDs and check groups. This takes an optional `MonitorGroupEnricher` that post-processes the minimal -// data, decorating it appropriately. The function also takes a fetcher, which does all the actual fetching. -export const fetchPage = async ( - queryContext: QueryContext, - monitorGroupFetcher: MonitorGroupsFetcher = fetchPageMonitorGroups, - monitorEnricher: MonitorEnricher = enrichMonitorGroups -): Promise => { - const size = Math.min(queryContext.size, QUERY.DEFAULT_AGGS_CAP); - const monitorPage = await monitorGroupFetcher(queryContext, size); - - const checkGroups: string[] = flatten( - monitorPage.monitorGroups.map((monitorGroups) => monitorGroups.groups.map((g) => g.checkGroup)) - ); - - const enrichedMonitors = await monitorEnricher(queryContext, checkGroups); - - return { - items: enrichedMonitors, - nextPagePagination: monitorPage.nextPagePagination, - prevPagePagination: monitorPage.prevPagePagination, - }; -}; - -// Fetches the most recent monitor groups for the given page, -// in the manner demanded by the `queryContext` and return at most `size` results. -const fetchPageMonitorGroups: MonitorGroupsFetcher = async ( - queryContext: QueryContext, - size: number -): Promise => { - const monitorGroups: MonitorGroups[] = []; - - const iterator = new MonitorGroupIterator(queryContext); - - let paginationBefore: CursorPagination | null = null; - while (monitorGroups.length < size) { - const monitor = await iterator.next(); - if (!monitor) { - break; // No more items to fetch - } - monitorGroups.push(monitor); - - // We want the before pagination to be before the first item we encounter - if (monitorGroups.length === 1) { - paginationBefore = await iterator.paginationBeforeCurrent(); - } - } - - // We have to create these objects before checking if we can navigate backward - const paginationAfter = await iterator.paginationAfterCurrent(); - - const ssAligned = searchSortAligned(queryContext.pagination); - - if (!ssAligned) { - monitorGroups.reverse(); - } - - return { - monitorGroups, - nextPagePagination: ssAligned ? paginationAfter : paginationBefore, - prevPagePagination: ssAligned ? paginationBefore : paginationAfter, - }; -}; - -// Returns true if the order returned by the ES query matches the requested sort order. -// This useful to determine if the results need to be reversed from their ES results order. -// I.E. when navigating backwards using prevPagePagination (CursorDirection.Before) yet using a SortOrder.ASC. -const searchSortAligned = (pagination: CursorPagination): boolean => { - if (pagination.cursorDirection === CursorDirection.AFTER) { - return pagination.sortOrder === SortOrder.ASC; - } else { - return pagination.sortOrder === SortOrder.DESC; - } -}; - -// Minimal interface representing the most recent set of groups accompanying a MonitorId in a given context. -export interface MonitorGroups { - id: string; - groups: MonitorLocCheckGroup[]; -} - -// Representation of the data returned when aggregating summary check groups. -export interface MonitorLocCheckGroup { - monitorId: string; - location: string | null; - checkGroup: string; - status: 'up' | 'down'; - summaryTimestamp: Date; -} - -// Represents a page that has not yet been enriched. -export interface MonitorGroupsPage { - monitorGroups: MonitorGroups[]; - nextPagePagination: CursorPagination | null; - prevPagePagination: CursorPagination | null; -} - -// Representation of a full page of results with pagination data for constructing next/prev links. -export interface EnrichedPage { - items: MonitorSummary[]; - nextPagePagination: CursorPagination | null; - prevPagePagination: CursorPagination | null; -} - -// A function that does the work of matching the minimal set of data for this query, returning just matching fields -// that are efficient to access while performing the query. -export type MonitorGroupsFetcher = ( - queryContext: QueryContext, - size: number -) => Promise; - -// A function that takes a set of check groups and returns richer MonitorSummary objects. -export type MonitorEnricher = ( - queryContext: QueryContext, - checkGroups: string[] -) => Promise; diff --git a/x-pack/plugins/uptime/server/lib/requests/search/find_potential_matches.ts b/x-pack/plugins/uptime/server/lib/requests/search/find_potential_matches.ts index ac4ff91230b9..8bdf7faf380e 100644 --- a/x-pack/plugins/uptime/server/lib/requests/search/find_potential_matches.ts +++ b/x-pack/plugins/uptime/server/lib/requests/search/find_potential_matches.ts @@ -5,7 +5,6 @@ */ import { get, set } from 'lodash'; -import { CursorDirection } from '../../../../common/runtime_types'; import { QueryContext } from './query_context'; /** @@ -44,8 +43,6 @@ const query = async (queryContext: QueryContext, searchAfter: any, size: number) }; const queryBody = async (queryContext: QueryContext, searchAfter: any, size: number) => { - const compositeOrder = cursorDirectionToOrder(queryContext.pagination.cursorDirection); - const filters = await queryContext.dateAndCustomFilters(); if (queryContext.statusFilter) { @@ -66,7 +63,7 @@ const queryBody = async (queryContext: QueryContext, searchAfter: any, size: num size, sources: [ { - monitor_id: { terms: { field: 'monitor.id', order: compositeOrder } }, + monitor_id: { terms: { field: 'monitor.id', order: queryContext.cursorOrder() } }, }, ], }, @@ -80,7 +77,3 @@ const queryBody = async (queryContext: QueryContext, searchAfter: any, size: num return body; }; - -const cursorDirectionToOrder = (cd: CursorDirection): 'asc' | 'desc' => { - return CursorDirection[cd] === CursorDirection.AFTER ? 'asc' : 'desc'; -}; diff --git a/x-pack/plugins/uptime/server/lib/requests/search/index.ts b/x-pack/plugins/uptime/server/lib/requests/search/index.ts index 8de5687808d6..dd8ca56f1fd7 100644 --- a/x-pack/plugins/uptime/server/lib/requests/search/index.ts +++ b/x-pack/plugins/uptime/server/lib/requests/search/index.ts @@ -4,6 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -export { fetchPage, MonitorGroups, MonitorLocCheckGroup, MonitorGroupsPage } from './fetch_page'; -export { MonitorGroupIterator } from './monitor_group_iterator'; +export { MonitorSummaryIterator } from './monitor_summary_iterator'; export { QueryContext } from './query_context'; diff --git a/x-pack/plugins/uptime/server/lib/requests/search/monitor_group_iterator.ts b/x-pack/plugins/uptime/server/lib/requests/search/monitor_summary_iterator.ts similarity index 61% rename from x-pack/plugins/uptime/server/lib/requests/search/monitor_group_iterator.ts rename to x-pack/plugins/uptime/server/lib/requests/search/monitor_summary_iterator.ts index 1ead81fa7102..0a726eb38394 100644 --- a/x-pack/plugins/uptime/server/lib/requests/search/monitor_group_iterator.ts +++ b/x-pack/plugins/uptime/server/lib/requests/search/monitor_summary_iterator.ts @@ -6,8 +6,7 @@ import { QueryContext } from './query_context'; import { fetchChunk } from './fetch_chunk'; -import { CursorDirection } from '../../../../common/runtime_types'; -import { MonitorGroups } from './fetch_page'; +import { CursorDirection, MonitorSummary } from '../../../../common/runtime_types'; import { CursorPagination } from './types'; // Hardcoded chunk size for how many monitors to fetch at a time when querying @@ -22,7 +21,7 @@ export type ChunkFetcher = ( // Result of fetching more results from the search. export interface ChunkResult { - monitorGroups: MonitorGroups[]; + monitorSummaries: MonitorSummary[]; searchAfter: any; } @@ -32,21 +31,19 @@ export interface ChunkResult { * querying, this class provides a `next` function that is cleaner to call. `next` provides the next matching result, * which may require many subsequent fetches, while keeping the external API clean. */ -// matches, or may simple be empty results that tell us a to keep looking for more, this class exists to simplify things. -// The idea is that you can call next() on it and receive the next matching result, even if internally we need to fetch -// multiple chunks to find that result. -export class MonitorGroupIterator { +export class MonitorSummaryIterator { queryContext: QueryContext; // Cache representing pre-fetched query results. - // The first item is the CheckGroup this represents. - buffer: MonitorGroups[]; + // The first item is the MonitorSummary this represents. + buffer: MonitorSummary[]; bufferPos: number; searchAfter: any; chunkFetcher: ChunkFetcher; + endOfResults: boolean; // true if we've hit the end of results from ES constructor( queryContext: QueryContext, - initialBuffer: MonitorGroups[] = [], + initialBuffer: MonitorSummary[] = [], initialBufferPos: number = -1, chunkFetcher: ChunkFetcher = fetchChunk ) { @@ -55,11 +52,12 @@ export class MonitorGroupIterator { this.bufferPos = initialBufferPos; this.searchAfter = queryContext.pagination.cursorKey; this.chunkFetcher = chunkFetcher; + this.endOfResults = false; } // Fetch the next matching result. - async next(): Promise { - await this.bufferNext(CHUNK_SIZE); + async next(): Promise { + await this.bufferNext(); const found = this.buffer[this.bufferPos + 1]; if (found) { @@ -69,29 +67,62 @@ export class MonitorGroupIterator { return null; } + async nextPage(size: number): Promise { + const monitorSummaries: MonitorSummary[] = []; + let paginationBefore: CursorPagination | null = null; + while (monitorSummaries.length < size) { + const monitor = await this.next(); + if (!monitor) { + break; // No more items to fetch + } + monitorSummaries.push(monitor); + + // We want the before pagination to be before the first item we encounter + if (monitorSummaries.length === 1) { + paginationBefore = await this.paginationBeforeCurrent(); + } + } + + // We have to create these objects before checking if we can navigate backward + const paginationAfter = await this.paginationAfterCurrent(); + + const ssAligned = this.queryContext.searchSortAligned(); + + if (!ssAligned) { + monitorSummaries.reverse(); + } + + return { + monitorSummaries, + nextPagePagination: ssAligned ? paginationAfter : paginationBefore, + prevPagePagination: ssAligned ? paginationBefore : paginationAfter, + }; + } + // Look ahead to see if there are additional results. - async peek(): Promise { - await this.bufferNext(CHUNK_SIZE); + async peek(): Promise { + await this.bufferNext(); return this.buffer[this.bufferPos + 1] || null; } // Returns the last item fetched with next(). null if no items fetched with // next or if next has not yet been invoked. - getCurrent(): MonitorGroups | null { + getCurrent(): MonitorSummary | null { return this.buffer[this.bufferPos] || null; } // Attempts to buffer at most `size` number of additional results, stopping when at least one additional // result is buffered or there are no more matching items to be found. - async bufferNext(size: number = CHUNK_SIZE): Promise { - // The next element is already buffered. + async bufferNext(): Promise { + // Nothing to do if there are no more results or + // the next element is already buffered. if (this.buffer[this.bufferPos + 1]) { return; } - while (true) { - const result = await this.attemptBufferMore(size); - if (result.gotHit || !result.hasMore) { + while (!this.endOfResults) { + const result = await this.attemptBufferMore(); + if (result.gotHit) { return; } } @@ -103,9 +134,7 @@ export class MonitorGroupIterator { * to free up space. * @param size the number of items to chunk */ - async attemptBufferMore( - size: number = CHUNK_SIZE - ): Promise<{ hasMore: boolean; gotHit: boolean }> { + async attemptBufferMore(): Promise<{ gotHit: boolean }> { // Trim the buffer to just the current element since we'll be fetching more const current = this.getCurrent(); @@ -117,17 +146,21 @@ export class MonitorGroupIterator { this.bufferPos = 0; } - const results = await this.chunkFetcher(this.queryContext, this.searchAfter, size); + const results = await this.chunkFetcher(this.queryContext, this.searchAfter, CHUNK_SIZE); // If we've hit the end of the stream searchAfter will be empty - - results.monitorGroups.forEach((mig: MonitorGroups) => this.buffer.push(mig)); + results.monitorSummaries.forEach((ms: MonitorSummary) => this.buffer.push(ms)); if (results.searchAfter) { this.searchAfter = results.searchAfter; } + // Remember, the chunk fetcher might return no results in one chunk, but still have more matching + // results, so we use the searchAfter field to determine whether we keep going. + if (!results.searchAfter) { + this.endOfResults = true; + } + return { - gotHit: results.monitorGroups.length > 0, - hasMore: !!results.searchAfter, + gotHit: results.monitorSummaries.length > 0, }; } @@ -142,7 +175,7 @@ export class MonitorGroupIterator { if (!current) { return null; } - const cursorKey = { monitor_id: current.id }; + const cursorKey = { monitor_id: current.monitor_id }; return Object.assign({}, this.queryContext.pagination, { cursorKey }); } @@ -154,12 +187,12 @@ export class MonitorGroupIterator { } // Returns a copy of this fetcher that goes backwards from the current position - reverse(): MonitorGroupIterator | null { + reverse(): MonitorSummaryIterator | null { const reverseContext = this.queryContext.clone(); const current = this.getCurrent(); reverseContext.pagination = { - cursorKey: current ? { monitor_id: current.id } : null, + cursorKey: current ? { monitor_id: current.monitor_id } : null, sortOrder: this.queryContext.pagination.sortOrder, cursorDirection: this.queryContext.pagination.cursorDirection === CursorDirection.AFTER @@ -168,12 +201,18 @@ export class MonitorGroupIterator { }; return current - ? new MonitorGroupIterator(reverseContext, [current], 0, this.chunkFetcher) + ? new MonitorSummaryIterator(reverseContext, [current], 0, this.chunkFetcher) : null; } // Returns a copy of this with a shallow copied buffer. Note that the queryContext is still shared! clone() { - return new MonitorGroupIterator(this.queryContext, this.buffer.slice(0), this.bufferPos); + return new MonitorSummaryIterator(this.queryContext, this.buffer.slice(0), this.bufferPos); } } + +export interface MonitorSummariesPage { + monitorSummaries: MonitorSummary[]; + nextPagePagination: CursorPagination | null; + prevPagePagination: CursorPagination | null; +} diff --git a/x-pack/plugins/uptime/server/lib/requests/search/query_context.ts b/x-pack/plugins/uptime/server/lib/requests/search/query_context.ts index b2fb9ce68c13..5d97e635f3e7 100644 --- a/x-pack/plugins/uptime/server/lib/requests/search/query_context.ts +++ b/x-pack/plugins/uptime/server/lib/requests/search/query_context.ts @@ -8,6 +8,7 @@ import moment from 'moment'; import { LegacyAPICaller } from 'src/core/server'; import { CursorPagination } from './types'; import { parseRelativeDate } from '../../helper'; +import { CursorDirection, SortOrder } from '../../../../common/runtime_types'; export class QueryContext { callES: LegacyAPICaller; @@ -146,4 +147,21 @@ export class QueryContext { this.statusFilter ); } + + // Returns true if the order returned by the ES query matches the requested sort order. + // This useful to determine if the results need to be reversed from their ES results order. + // I.E. when navigating backwards using prevPagePagination (CursorDirection.Before) yet using a SortOrder.ASC. + searchSortAligned(): boolean { + if (this.pagination.cursorDirection === CursorDirection.AFTER) { + return this.pagination.sortOrder === SortOrder.ASC; + } else { + return this.pagination.sortOrder === SortOrder.DESC; + } + } + + cursorOrder(): 'asc' | 'desc' { + return CursorDirection[this.pagination.cursorDirection] === CursorDirection.AFTER + ? 'asc' + : 'desc'; + } } diff --git a/x-pack/plugins/uptime/server/lib/requests/search/refine_potential_matches.ts b/x-pack/plugins/uptime/server/lib/requests/search/refine_potential_matches.ts index 2f54f3f6dd68..f631d5c963ca 100644 --- a/x-pack/plugins/uptime/server/lib/requests/search/refine_potential_matches.ts +++ b/x-pack/plugins/uptime/server/lib/requests/search/refine_potential_matches.ts @@ -5,8 +5,7 @@ */ import { QueryContext } from './query_context'; -import { CursorDirection } from '../../../../common/runtime_types'; -import { MonitorGroups, MonitorLocCheckGroup } from './fetch_page'; +import { MonitorSummary, Ping } from '../../../../common/runtime_types'; /** * Determines whether the provided check groups are the latest complete check groups for their associated monitor ID's. @@ -19,80 +18,83 @@ import { MonitorGroups, MonitorLocCheckGroup } from './fetch_page'; export const refinePotentialMatches = async ( queryContext: QueryContext, potentialMatchMonitorIDs: string[] -): Promise => { +): Promise => { if (potentialMatchMonitorIDs.length === 0) { return []; } const queryResult = await query(queryContext, potentialMatchMonitorIDs); - const recentGroupsMatchingStatus = await fullyMatchingIds(queryResult, queryContext.statusFilter); - - // Return the monitor groups filtering out potential matches that weren't current - const matches: MonitorGroups[] = potentialMatchMonitorIDs - .map((id: string) => { - return { id, groups: recentGroupsMatchingStatus.get(id) || [] }; - }) - .filter((mrg) => mrg.groups.length > 0); - - // Sort matches by ID - matches.sort((a: MonitorGroups, b: MonitorGroups) => { - return a.id === b.id ? 0 : a.id > b.id ? 1 : -1; - }); - - if (queryContext.pagination.cursorDirection === CursorDirection.BEFORE) { - matches.reverse(); - } - return matches; + return await fullyMatchingIds(queryResult, queryContext.statusFilter); }; -export const fullyMatchingIds = (queryResult: any, statusFilter?: string) => { - const matching = new Map(); - MonitorLoop: for (const monBucket of queryResult.aggregations.monitor.buckets) { - const monitorId: string = monBucket.key; - const groups: MonitorLocCheckGroup[] = []; +export const fullyMatchingIds = (queryResult: any, statusFilter?: string): MonitorSummary[] => { + const summaries: MonitorSummary[] = []; + for (const monBucket of queryResult.aggregations.monitor.buckets) { // Did at least one location match? let matched = false; + + const summaryPings: Ping[] = []; + for (const locBucket of monBucket.location.buckets) { - const location = locBucket.key; - const latestSource = locBucket.summaries.latest.hits.hits[0]._source; - const latestStillMatchingSource = locBucket.latest_matching.top.hits.hits[0]?._source; + const latest = locBucket.summaries.latest.hits.hits[0]; + const latestStillMatching = locBucket.latest_matching.top.hits.hits[0]; // If the most recent document still matches the most recent document matching the current filters // we can include this in the result // // We just check if the timestamp is greater. Note this may match an incomplete check group // that has not yet sent a summary doc if ( - latestStillMatchingSource && - latestStillMatchingSource['@timestamp'] >= latestSource['@timestamp'] + latestStillMatching && + latestStillMatching._source['@timestamp'] >= latest._source['@timestamp'] ) { matched = true; } - const checkGroup = latestSource.monitor.check_group; - const status = latestSource.summary.down > 0 ? 'down' : 'up'; - // This monitor doesn't match, so just skip ahead and don't add it to the output - // Only skip in case of up statusFilter, for a monitor to be up, all checks should be up - if (statusFilter === 'up' && statusFilter !== status) { - continue MonitorLoop; - } - - groups.push({ - monitorId, - location, - checkGroup, - status, - summaryTimestamp: new Date(latestSource['@timestamp']), + summaryPings.push({ + docId: latest._id, + timestamp: latest._source['@timestamp'], + ...latest._source, }); } - // If one location matched, include data from all locations in the result set - if (matched) { - matching.set(monitorId, groups); + const someDown = summaryPings.some((p) => (p.summary?.down ?? 0) > 0); + const statusFilterOk = !statusFilter ? true : statusFilter === 'up' ? !someDown : someDown; + + if (matched && statusFilterOk) { + summaries.push(summaryPingsToSummary(summaryPings)); } } - return matching; + return summaries; +}; + +export const summaryPingsToSummary = (summaryPings: Ping[]): MonitorSummary => { + summaryPings.sort((a, b) => + a.timestamp > b.timestamp ? 1 : a.timestamp === b.timestamp ? 0 : -1 + ); + const latest = summaryPings[summaryPings.length - 1]; + return { + monitor_id: latest.monitor.id, + state: { + timestamp: latest.timestamp, + monitor: { + name: latest.monitor?.name, + }, + url: latest.url ?? {}, + summary: { + up: summaryPings.reduce((acc, p) => (p.summary?.up ?? 0) + acc, 0), + down: summaryPings.reduce((acc, p) => (p.summary?.down ?? 0) + acc, 0), + status: summaryPings.some((p) => (p.summary?.down ?? 0) > 0) ? 'down' : 'up', + }, + summaryPings, + tls: latest.tls, + // easier to ensure to use '' for an empty geo name in terms of types + observer: { + geo: { name: summaryPings.map((p) => p.observer?.geo?.name ?? '').filter((n) => n !== '') }, + }, + }, + }; }; export const query = async ( @@ -113,7 +115,11 @@ export const query = async ( }, aggs: { monitor: { - terms: { field: 'monitor.id', size: potentialMatchMonitorIDs.length }, + terms: { + field: 'monitor.id', + size: potentialMatchMonitorIDs.length, + order: { _key: queryContext.cursorOrder() }, + }, aggs: { location: { terms: { field: 'observer.geo.name', missing: 'N/A', size: 100 }, @@ -125,14 +131,6 @@ export const query = async ( latest: { top_hits: { sort: [{ '@timestamp': 'desc' }], - _source: { - includes: [ - 'monitor.check_group', - '@timestamp', - 'summary.up', - 'summary.down', - ], - }, size: 1, }, }, @@ -144,10 +142,8 @@ export const query = async ( aggs: { top: { top_hits: { + _source: ['@timestamp'], sort: [{ '@timestamp': 'desc' }], - _source: { - includes: ['monitor.check_group', '@timestamp'], - }, size: 1, }, }, diff --git a/x-pack/plugins/uptime/server/lib/requests/uptime_requests.ts b/x-pack/plugins/uptime/server/lib/requests/uptime_requests.ts index 85fc2c3ef977..ae3a729e41c7 100644 --- a/x-pack/plugins/uptime/server/lib/requests/uptime_requests.ts +++ b/x-pack/plugins/uptime/server/lib/requests/uptime_requests.ts @@ -17,6 +17,7 @@ import { GetCertsParams, GetPingsParams, CertResult, + MonitorSummariesResult, } from '../../../common/runtime_types'; import { MonitorDurationResult } from '../../../common/types'; @@ -31,7 +32,6 @@ import { GetMonitorStatusParams, GetMonitorStatusResult, } from '.'; -import { GetMonitorStatesResult } from './get_monitor_states'; import { GetSnapshotCountParams } from './get_snapshot_counts'; import { IIndexPattern } from '../../../../../../src/plugins/data/server'; @@ -45,7 +45,7 @@ export interface UptimeRequests { getMonitorDurationChart: ESQ; getMonitorDetails: ESQ; getMonitorLocations: ESQ; - getMonitorStates: ESQ; + getMonitorStates: ESQ; getMonitorStatus: ESQ; getPings: ESQ; getPingHistogram: ESQ; diff --git a/x-pack/test/api_integration/apis/uptime/rest/fixtures/monitor_latest_status.json b/x-pack/test/api_integration/apis/uptime/rest/fixtures/monitor_latest_status.json index 1baff443bd97..9a33be807670 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/fixtures/monitor_latest_status.json +++ b/x-pack/test/api_integration/apis/uptime/rest/fixtures/monitor_latest_status.json @@ -27,6 +27,5 @@ "full": "http://localhost:5678/pattern?r=200x1" }, "docId": "h5toHm0B0I9WX_CznN_V", - "timestamp": "2019-09-11T03:40:34.371Z", - "tls": {} -} + "timestamp": "2019-09-11T03:40:34.371Z" +} \ No newline at end of file diff --git a/x-pack/test/api_integration/apis/uptime/rest/monitor_states_generated.ts b/x-pack/test/api_integration/apis/uptime/rest/monitor_states_generated.ts index 34cf15e06156..3e06373042d5 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/monitor_states_generated.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/monitor_states_generated.ts @@ -53,18 +53,6 @@ export default function ({ getService }: FtrProviderContext) { nonSummaryIp = checks[0][0].monitor.ip; }); - it('should return all IPs', async () => { - const filters = makeApiParams(testMonitorId); - const url = getBaseUrl(dateRangeStart, dateRangeEnd) + `&filters=${filters}`; - const apiResponse = await supertest.get(url); - const res = apiResponse.body; - - const uniqueIps = new Set(); - res.summaries[0].state.checks.forEach((c: any) => uniqueIps.add(c.monitor.ip)); - - expect(uniqueIps.size).to.eql(4); - }); - it('should match non summary documents without a status filter', async () => { const filters = makeApiParams(testMonitorId, [{ match: { 'monitor.ip': nonSummaryIp } }]); diff --git a/x-pack/test/api_integration/apis/uptime/rest/monitor_states_real_data.ts b/x-pack/test/api_integration/apis/uptime/rest/monitor_states_real_data.ts index bcf4e63a9f6e..d3c49bb49ff5 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/monitor_states_real_data.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/monitor_states_real_data.ts @@ -7,7 +7,7 @@ import expect from '@kbn/expect'; import { isRight } from 'fp-ts/lib/Either'; import { FtrProviderContext } from '../../../ftr_provider_context'; -import { MonitorSummaryResultType } from '../../../../../plugins/uptime/common/runtime_types'; +import { MonitorSummariesResultType } from '../../../../../plugins/uptime/common/runtime_types'; import { API_URLS } from '../../../../../plugins/uptime/common/constants'; interface ExpectedMonitorStatesPage { @@ -38,7 +38,7 @@ const checkMonitorStatesResponse = ({ prevPagination, nextPagination, }: ExpectedMonitorStatesPage) => { - const decoded = MonitorSummaryResultType.decode(response); + const decoded = MonitorSummariesResultType.decode(response); expect(isRight(decoded)).to.be.ok(); if (isRight(decoded)) { const { summaries, prevPagePagination, nextPagePagination, totalSummaryCount } = decoded.right;