[Synthetics] Improve overview page performance !! (#201275)

## Summary

Improve overview page performance !!

Right now UI works for few hundred to 1000 monitors, but it starts
degrading after that, this PR makes sure, we refactor queries in such a
way that it scale up to 10k-20k monitors easily.


### Queries before
Before this PR, we were doing 2 steps queries, first fetch all saved
objects and the fetch all summary documents by passings all ids from
first phase. This meant that let's say if we have 20k saved objects,
first we will need to page through all of them to even start fetching
summaries. To fetch summary documents, we were using `top_hits` query
which can be memory expensive.


### Queries now
In this PR we fetch summaries and saved objects in parallel, since we
have space id on documents as well, there was no need to do 2 step
queries. Now we fetch both things in parallel and then we hydrate saved
object data from summary data. In this PR now we are using top_metrics
query to fetch each monitor status instead of `top_hits`


I tested on about 20k monitors, app performs reasoably well after the PR
<img width="1920" alt="image"
src="https://github.com/user-attachments/assets/c143e196-59a4-45b4-86b7-bd22ac4c5d4b">


On a very slow cluster on which kibana is local against a remote cluster

### After
<img width="1920" alt="image"
src="https://github.com/user-attachments/assets/540d0cdf-2f8c-44d1-af76-81953d9ca0ff">


### Before
<img width="1918" alt="image"
src="https://github.com/user-attachments/assets/5fdc314d-bb59-4137-9397-d8aee6bd4806">

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Shahzad 2024-12-11 20:33:33 +01:00 committed by GitHub
parent 52ee949f6f
commit b4ccb0c205
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 1336 additions and 1820 deletions

View file

@ -235,4 +235,7 @@ const commons = {
},
},
},
meta: {
space_id: 'default',
},
};

View file

@ -1,300 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import times from 'lodash/times';
import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { cloneDeep, intersection } from 'lodash';
import { SavedObjectsFindResult } from '@kbn/core-saved-objects-api-server';
import { MsearchMultisearchBody } from '@elastic/elasticsearch/lib/api/types';
import { isStatusEnabled } from '../../common/runtime_types/monitor_management/alert_config';
import { FINAL_SUMMARY_FILTER } from '../../common/constants/client_defaults';
import {
ConfigKey,
EncryptedSyntheticsMonitorAttributes,
OverviewPing,
OverviewStatus,
OverviewStatusMetaData,
} from '../../common/runtime_types';
import { createEsParams, SyntheticsEsClient } from '../lib';
const DEFAULT_MAX_ES_BUCKET_SIZE = 10000;
const fields = [
'@timestamp',
'summary',
'monitor',
'observer',
'config_id',
'error',
'agent',
'url',
'state',
'tags',
];
const getStatusQuery = ({
idSize,
idsToQuery,
range,
monitorLocationIds,
}: {
idSize: number;
monitorLocationIds: string[];
range: { from: string; to: string };
idsToQuery: string[];
}) => {
const params = createEsParams({
body: {
size: 0,
query: {
bool: {
filter: [
FINAL_SUMMARY_FILTER,
{
range: {
'@timestamp': {
gte: range.from,
lte: range.to,
},
},
},
{
terms: {
'monitor.id': idsToQuery,
},
},
] as QueryDslQueryContainer[],
},
},
aggs: {
id: {
terms: {
field: 'monitor.id',
size: idSize,
},
aggs: {
location: {
terms: {
field: 'observer.name',
size: monitorLocationIds.length || 100,
},
aggs: {
status: {
top_hits: {
size: 1,
sort: [
{
'@timestamp': {
order: 'desc',
},
},
],
_source: {
includes: fields,
},
},
},
},
},
},
},
},
},
});
if (monitorLocationIds.length > 0) {
params.body.query?.bool?.filter.push({
terms: {
'observer.name': monitorLocationIds,
},
});
}
return params;
};
type StatusQueryParams = ReturnType<typeof getStatusQuery>;
type OverviewStatusResponse = Omit<
OverviewStatus,
| 'disabledCount'
| 'allMonitorsCount'
| 'disabledMonitorsCount'
| 'projectMonitorsCount'
| 'disabledMonitorQueryIds'
| 'allIds'
>;
export async function queryMonitorStatus({
esClient,
monitorLocationIds,
range,
monitorQueryIds,
monitorLocationsMap,
monitorQueryIdToConfigIdMap,
monitors,
}: {
esClient: SyntheticsEsClient;
monitorLocationIds: string[];
range: { from: string; to: string };
monitorQueryIds: string[];
monitorLocationsMap: Record<string, string[]>;
monitorQueryIdToConfigIdMap: Record<string, string>;
monitors: Array<SavedObjectsFindResult<EncryptedSyntheticsMonitorAttributes>>;
}): Promise<OverviewStatusResponse> {
const idSize = Math.trunc(DEFAULT_MAX_ES_BUCKET_SIZE / monitorLocationIds.length || 1);
const pageCount = Math.ceil(monitorQueryIds.length / idSize);
let up = 0;
let down = 0;
const upConfigs: Record<string, OverviewStatusMetaData> = {};
const downConfigs: Record<string, OverviewStatusMetaData> = {};
const monitorsWithoutData = new Map(Object.entries(cloneDeep(monitorLocationsMap)));
const pendingConfigs: Record<string, OverviewStatusMetaData> = {};
const disabledConfigs: Record<string, OverviewStatusMetaData> = {};
monitors
.filter((monitor) => !monitor.attributes[ConfigKey.ENABLED])
.forEach((monitor) => {
const monitorQueryId = monitor.attributes[ConfigKey.MONITOR_QUERY_ID];
monitor.attributes[ConfigKey.LOCATIONS]?.forEach((location) => {
disabledConfigs[`${monitorQueryIdToConfigIdMap[monitorQueryId]}-${location.id}`] = {
configId: `${monitorQueryIdToConfigIdMap[monitorQueryId]}`,
monitorQueryId,
status: 'disabled',
locationId: location.id,
locationLabel: location.label,
...getMonitorMeta(monitor),
};
});
});
const queries: MsearchMultisearchBody[] = times(pageCount).map((i) => {
const idsToQuery = (monitorQueryIds as string[]).slice(i * idSize, i * idSize + idSize);
return getStatusQuery({
idSize,
monitorLocationIds,
range,
idsToQuery,
}).body;
});
if (queries.length) {
const { responses } = await esClient.msearch<StatusQueryParams, OverviewPing>(
queries,
'getCurrentStatusOverview'
);
responses.forEach((result) => {
result.aggregations?.id.buckets.forEach(({ location, key: queryId }) => {
const locationSummaries = location.buckets.map(({ status, key: locationName }) => {
const ping = status.hits.hits[0]._source;
return { location: locationName, ping };
});
const monitor = monitors.find((m) => m.attributes[ConfigKey.MONITOR_QUERY_ID] === queryId)!;
// discard any locations that are not in the monitorLocationsMap for the given monitor as well as those which are
// in monitorLocationsMap but not in listOfLocations
const monLocations = monitorLocationsMap?.[queryId];
const monQueriedLocations = intersection(monLocations, monitorLocationIds);
monQueriedLocations?.forEach((monLocation) => {
const locationSummary = locationSummaries.find(
(summary) => summary.location === monLocation
);
if (locationSummary) {
const { ping } = locationSummary;
const downCount = ping.summary?.down ?? 0;
const upCount = ping.summary?.up ?? 0;
const configId = ping.config_id;
const monitorQueryId = ping.monitor.id;
const meta = {
ping,
configId,
monitorQueryId,
locationId: monLocation,
timestamp: ping['@timestamp'],
locationLabel: ping.observer.geo!.name!,
...getMonitorMeta(monitor),
};
if (downCount > 0) {
down += 1;
downConfigs[`${configId}-${monLocation}`] = {
...meta,
status: 'down',
};
} else if (upCount > 0) {
up += 1;
upConfigs[`${configId}-${monLocation}`] = {
...meta,
status: 'up',
};
}
const monitorsMissingData = monitorsWithoutData.get(monitorQueryId) || [];
monitorsWithoutData.set(
monitorQueryId,
monitorsMissingData?.filter((loc) => loc !== monLocation)
);
if (!monitorsWithoutData.get(monitorQueryId)?.length) {
monitorsWithoutData.delete(monitorQueryId);
}
}
});
});
});
}
// identify the remaining monitors without data, to determine pending monitors
for (const [queryId, locs] of monitorsWithoutData) {
const monitor = monitors.find((m) => m.attributes[ConfigKey.MONITOR_QUERY_ID] === queryId)!;
locs.forEach((loc) => {
pendingConfigs[`${monitorQueryIdToConfigIdMap[queryId]}-${loc}`] = {
configId: `${monitorQueryIdToConfigIdMap[queryId]}`,
monitorQueryId: queryId,
status: 'unknown',
locationId: loc,
locationLabel: monitor.attributes[ConfigKey.LOCATIONS]?.find(
(location) => location.id === loc
)?.label!,
name: monitor.attributes[ConfigKey.NAME],
schedule: monitor.attributes[ConfigKey.SCHEDULE].number,
tags: monitor.attributes[ConfigKey.TAGS],
isEnabled: monitor.attributes[ConfigKey.ENABLED],
type: monitor.attributes[ConfigKey.MONITOR_TYPE],
projectId: monitor.attributes[ConfigKey.PROJECT_ID],
isStatusAlertEnabled: isStatusEnabled(monitor.attributes[ConfigKey.ALERT_CONFIG]),
updated_at: monitor.updated_at,
};
});
}
return {
up,
down,
pending: Object.values(pendingConfigs).length,
upConfigs,
downConfigs,
pendingConfigs,
enabledMonitorQueryIds: monitorQueryIds,
disabledConfigs,
};
}
const getMonitorMeta = (monitor: SavedObjectsFindResult<EncryptedSyntheticsMonitorAttributes>) => {
return {
name: monitor.attributes[ConfigKey.NAME],
schedule: monitor.attributes[ConfigKey.SCHEDULE].number,
tags: monitor.attributes[ConfigKey.TAGS],
isEnabled: monitor.attributes[ConfigKey.ENABLED],
type: monitor.attributes[ConfigKey.MONITOR_TYPE],
projectId: monitor.attributes[ConfigKey.PROJECT_ID],
isStatusAlertEnabled: isStatusEnabled(monitor.attributes[ConfigKey.ALERT_CONFIG]),
updated_at: monitor.updated_at,
spaceId: monitor.namespaces?.[0],
};
};

View file

@ -4,126 +4,11 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { intersection } from 'lodash';
import datemath, { Unit } from '@kbn/datemath';
import moment from 'moment';
import { RouteContext, SyntheticsRestApiRouteFactory } from '../types';
import { ConfigKey, OverviewStatusState } from '../../../common/runtime_types';
import {
getAllMonitors,
processMonitors,
} from '../../saved_objects/synthetics_monitor/get_all_monitors';
import { queryMonitorStatus } from '../../queries/query_monitor_status';
import { OverviewStatusService } from './overview_status_service';
import { SyntheticsRestApiRouteFactory } from '../types';
import { OverviewStatusState } from '../../../common/runtime_types';
import { SYNTHETICS_API_URLS } from '../../../common/constants';
import { getMonitorFilters, OverviewStatusSchema, OverviewStatusQuery } from '../common';
/**
* Helper function that converts a monitor's schedule to a value to use to generate
* an appropriate look-back window for snapshot count.
* @param schedule a number/unit pair that represents how often a configured monitor runs
* @returns schedule interval in ms
*/
export function periodToMs(schedule: { number: string; unit: Unit }) {
if (Object.keys(datemath.unitsMap).indexOf(schedule.unit) === -1) return 0;
return parseInt(schedule.number, 10) * datemath.unitsMap[schedule.unit].base;
}
/**
* Multi-stage function that first queries all the user's saved object monitor configs.
*
* Subsequently, fetch the status for each monitor per location in the data streams.
* @returns The counts of up/down/disabled monitor by location, and a map of each monitor:location status.
*/
export async function getStatus(context: RouteContext, params: OverviewStatusQuery) {
const { syntheticsEsClient, savedObjectsClient } = context;
const { query, scopeStatusByLocation = true, showFromAllSpaces } = params;
/**
* Walk through all monitor saved objects, bucket IDs by disabled/enabled status.
*
* Track max period to make sure the snapshot query should reach back far enough to catch
* latest ping for all enabled monitors.
*/
const { filtersStr, locationFilter: queryLocations } = await getMonitorFilters({
...params,
context,
});
const allMonitors = await getAllMonitors({
soClient: savedObjectsClient,
showFromAllSpaces,
search: query ? `${query}*` : undefined,
filter: filtersStr,
fields: [
ConfigKey.ENABLED,
ConfigKey.LOCATIONS,
ConfigKey.MONITOR_QUERY_ID,
ConfigKey.CONFIG_ID,
ConfigKey.SCHEDULE,
ConfigKey.MONITOR_SOURCE_TYPE,
ConfigKey.MONITOR_TYPE,
ConfigKey.NAME,
ConfigKey.TAGS,
ConfigKey.PROJECT_ID,
ConfigKey.ALERT_CONFIG,
],
});
const {
enabledMonitorQueryIds,
disabledMonitorQueryIds,
allIds,
disabledCount,
maxPeriod,
monitorLocationIds,
monitorLocationsMap,
disabledMonitorsCount,
projectMonitorsCount,
monitorQueryIdToConfigIdMap,
} = processMonitors(allMonitors, queryLocations);
// Account for locations filter
const listOfLocationAfterFilter =
queryLocations && scopeStatusByLocation
? intersection(monitorLocationIds, queryLocations)
: monitorLocationIds;
const range = {
from: moment().subtract(maxPeriod, 'milliseconds').subtract(20, 'minutes').toISOString(),
to: 'now',
};
const { up, down, pending, upConfigs, downConfigs, pendingConfigs, disabledConfigs } =
await queryMonitorStatus({
range,
monitors: allMonitors,
monitorLocationsMap,
monitorQueryIdToConfigIdMap,
esClient: syntheticsEsClient,
monitorLocationIds: listOfLocationAfterFilter,
monitorQueryIds: enabledMonitorQueryIds,
});
return {
allIds,
allMonitorsCount: allMonitors.length,
disabledMonitorsCount,
projectMonitorsCount,
enabledMonitorQueryIds,
disabledMonitorQueryIds,
disabledCount,
up,
down,
pending,
upConfigs,
downConfigs,
pendingConfigs,
disabledConfigs,
};
}
import { OverviewStatusSchema } from '../common';
export const createGetCurrentStatusRoute: SyntheticsRestApiRouteFactory = () => ({
method: 'GET',
@ -132,9 +17,7 @@ export const createGetCurrentStatusRoute: SyntheticsRestApiRouteFactory = () =>
query: OverviewStatusSchema,
},
handler: async (routeContext): Promise<OverviewStatusState> => {
const { request } = routeContext;
const params = request.query as OverviewStatusQuery;
return await getStatus(routeContext, params);
const statusOverview = new OverviewStatusService(routeContext);
return await statusOverview.getOverviewStatus();
},
});

View file

@ -0,0 +1,793 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { SavedObjectsFindResult } from '@kbn/core-saved-objects-api-server';
import * as monitorsFns from '../../saved_objects/synthetics_monitor/get_all_monitors';
import { EncryptedSyntheticsMonitorAttributes } from '../../../common/runtime_types';
import { getUptimeESMockClient } from '../../queries/test_helpers';
import * as commonLibs from '../common';
import * as allLocationsFn from '../../synthetics_service/get_all_locations';
import { OverviewStatusService, SUMMARIES_PAGE_SIZE } from './overview_status_service';
import times from 'lodash/times';
import { flatten } from 'lodash';
const japanLoc = {
id: 'asia_japan',
label: 'Asia/Pacific - Japan',
};
const germanyLoc = {
id: 'europe_germany',
label: 'Europe - Germany',
};
const allLocations: any = [japanLoc, germanyLoc];
jest.spyOn(allLocationsFn, 'getAllLocations').mockResolvedValue({
publicLocations: allLocations,
privateLocations: [],
allLocations,
});
jest.mock('../../saved_objects/synthetics_monitor/get_all_monitors', () => ({
...jest.requireActual('../../saved_objects/synthetics_monitor/get_all_monitors'),
getAllMonitors: jest.fn(),
}));
jest.spyOn(commonLibs, 'getMonitors').mockResolvedValue({
per_page: 10,
saved_objects: [
{
id: 'mon-1',
attributes: {
enabled: false,
locations: [{ id: 'us-east1' }, { id: 'us-west1' }, { id: 'japan' }],
},
},
{
id: 'mon-2',
attributes: {
enabled: true,
locations: [{ id: 'us-east1' }, { id: 'us-west1' }, { id: 'japan' }],
schedule: {
number: '10',
unit: 'm',
},
},
},
],
} as any);
describe('current status route', () => {
const testMonitors = [
{
attributes: {
config_id: 'id1',
id: 'id1',
type: 'browser',
enabled: true,
name: 'test monitor 1',
project_id: 'project-id',
tags: ['tag-1', 'tag-2'],
schedule: {
number: '1',
unit: 'm',
},
locations: [japanLoc],
},
},
{
attributes: {
id: 'id2',
config_id: 'id2',
enabled: true,
type: 'browser',
name: 'test monitor 2',
project_id: 'project-id',
tags: ['tag-1', 'tag-2'],
schedule: {
number: '1',
unit: 'm',
},
locations: allLocations,
},
},
];
describe('OverviewStatusService', () => {
it('parses expected agg fields', async () => {
const { esClient, syntheticsEsClient } = getUptimeESMockClient();
esClient.search.mockResponseOnce(
getEsResponse({
buckets: [
{
key: {
monitorId: 'id1',
locationId: japanLoc.id,
},
status: {
key: japanLoc.id,
top: [
{
metrics: {
'monitor.status': 'up',
},
sort: ['2022-09-15T16:19:16.724Z'],
},
],
},
},
{
key: {
monitorId: 'id2',
locationId: japanLoc.id,
},
status: {
key: japanLoc.id,
top: [
{
metrics: {
'monitor.status': 'up',
},
sort: ['2022-09-15T16:19:16.724Z'],
},
],
},
},
{
key: {
monitorId: 'id2',
locationId: germanyLoc.id,
},
status: {
key: germanyLoc.id,
top: [
{
metrics: {
'monitor.status': 'down',
},
sort: ['2022-09-15T16:19:16.724Z'],
},
],
},
},
],
})
);
const routeContext: any = {
request: {},
syntheticsEsClient,
};
const overviewStatusService = new OverviewStatusService(routeContext);
overviewStatusService.getMonitorConfigs = jest.fn().mockResolvedValue(testMonitors as any);
expect(await overviewStatusService.getOverviewStatus()).toMatchInlineSnapshot(`
Object {
"allIds": Array [
"id1",
"id2",
],
"allMonitorsCount": 2,
"disabledConfigs": Object {},
"disabledCount": 0,
"disabledMonitorQueryIds": Array [],
"disabledMonitorsCount": 0,
"down": 1,
"downConfigs": Object {
"id2-europe_germany": Object {
"configId": "id2",
"isEnabled": true,
"isStatusAlertEnabled": false,
"locationId": "europe_germany",
"locationLabel": "Europe - Germany",
"monitorQueryId": "id2",
"name": "test monitor 2",
"projectId": "project-id",
"schedule": "1",
"spaceId": undefined,
"status": "down",
"tags": Array [
"tag-1",
"tag-2",
],
"timestamp": "2022-09-15T16:19:16.724Z",
"type": "browser",
"updated_at": undefined,
},
},
"enabledMonitorQueryIds": Array [
"id1",
"id2",
],
"pending": 0,
"pendingConfigs": Object {},
"projectMonitorsCount": 0,
"up": 2,
"upConfigs": Object {
"id1-asia_japan": Object {
"configId": "id1",
"isEnabled": true,
"isStatusAlertEnabled": false,
"locationId": "asia_japan",
"locationLabel": "Asia/Pacific - Japan",
"monitorQueryId": "id1",
"name": "test monitor 1",
"projectId": "project-id",
"schedule": "1",
"spaceId": undefined,
"status": "up",
"tags": Array [
"tag-1",
"tag-2",
],
"timestamp": "2022-09-15T16:19:16.724Z",
"type": "browser",
"updated_at": undefined,
},
"id2-asia_japan": Object {
"configId": "id2",
"isEnabled": true,
"isStatusAlertEnabled": false,
"locationId": "asia_japan",
"locationLabel": "Asia/Pacific - Japan",
"monitorQueryId": "id2",
"name": "test monitor 2",
"projectId": "project-id",
"schedule": "1",
"spaceId": undefined,
"status": "up",
"tags": Array [
"tag-1",
"tag-2",
],
"timestamp": "2022-09-15T16:19:16.724Z",
"type": "browser",
"updated_at": undefined,
},
},
}
`);
});
it('handles limits with multiple requests', async () => {
const { esClient, syntheticsEsClient } = getUptimeESMockClient();
esClient.search.mockResponseOnce(
getEsResponse({
after: {},
buckets: flatten(
times(SUMMARIES_PAGE_SIZE).map(() => [
{
key: {
monitorId: 'id1',
locationId: japanLoc.id,
},
status: {
key: japanLoc.id,
top: [
{
metrics: {
'monitor.status': 'up',
},
sort: ['2022-09-15T16:19:16.724Z'],
},
],
},
},
{
key: {
monitorId: 'id2',
locationId: japanLoc.id,
},
status: {
key: japanLoc.id,
top: [
{
metrics: {
'monitor.status': 'up',
},
sort: ['2022-09-15T16:19:16.724Z'],
},
],
},
},
{
key: {
monitorId: 'id2',
locationId: germanyLoc.id,
},
status: {
key: germanyLoc.id,
top: [
{
metrics: {
'monitor.status': 'down',
},
sort: ['2022-09-15T16:19:16.724Z'],
},
],
},
},
])
),
})
);
const routeContext: any = {
request: {},
syntheticsEsClient,
};
const overviewStatusService = new OverviewStatusService(routeContext);
overviewStatusService.getMonitorConfigs = jest.fn().mockResolvedValue(testMonitors as any);
expect(await overviewStatusService.getOverviewStatus()).toMatchInlineSnapshot(`
Object {
"allIds": Array [
"id1",
"id2",
],
"allMonitorsCount": 2,
"disabledConfigs": Object {},
"disabledCount": 0,
"disabledMonitorQueryIds": Array [],
"disabledMonitorsCount": 0,
"down": 1,
"downConfigs": Object {
"id2-europe_germany": Object {
"configId": "id2",
"isEnabled": true,
"isStatusAlertEnabled": false,
"locationId": "europe_germany",
"locationLabel": "Europe - Germany",
"monitorQueryId": "id2",
"name": "test monitor 2",
"projectId": "project-id",
"schedule": "1",
"spaceId": undefined,
"status": "down",
"tags": Array [
"tag-1",
"tag-2",
],
"timestamp": "2022-09-15T16:19:16.724Z",
"type": "browser",
"updated_at": undefined,
},
},
"enabledMonitorQueryIds": Array [
"id1",
"id2",
],
"pending": 0,
"pendingConfigs": Object {},
"projectMonitorsCount": 0,
"up": 2,
"upConfigs": Object {
"id1-asia_japan": Object {
"configId": "id1",
"isEnabled": true,
"isStatusAlertEnabled": false,
"locationId": "asia_japan",
"locationLabel": "Asia/Pacific - Japan",
"monitorQueryId": "id1",
"name": "test monitor 1",
"projectId": "project-id",
"schedule": "1",
"spaceId": undefined,
"status": "up",
"tags": Array [
"tag-1",
"tag-2",
],
"timestamp": "2022-09-15T16:19:16.724Z",
"type": "browser",
"updated_at": undefined,
},
"id2-asia_japan": Object {
"configId": "id2",
"isEnabled": true,
"isStatusAlertEnabled": false,
"locationId": "asia_japan",
"locationLabel": "Asia/Pacific - Japan",
"monitorQueryId": "id2",
"name": "test monitor 2",
"projectId": "project-id",
"schedule": "1",
"spaceId": undefined,
"status": "up",
"tags": Array [
"tag-1",
"tag-2",
],
"timestamp": "2022-09-15T16:19:16.724Z",
"type": "browser",
"updated_at": undefined,
},
},
}
`);
expect(esClient.search).toHaveBeenCalledTimes(2);
});
it('handles pending configs', async () => {
const { esClient, syntheticsEsClient } = getUptimeESMockClient();
esClient.search.mockResponseOnce(
getEsResponse({
buckets: [],
})
);
const routeContext: any = {
request: {},
syntheticsEsClient,
};
const overviewStatusService = new OverviewStatusService(routeContext);
overviewStatusService.getMonitorConfigs = jest.fn().mockResolvedValue(testMonitors as any);
expect(await overviewStatusService.getOverviewStatus()).toMatchInlineSnapshot(`
Object {
"allIds": Array [
"id1",
"id2",
],
"allMonitorsCount": 2,
"disabledConfigs": Object {},
"disabledCount": 0,
"disabledMonitorQueryIds": Array [],
"disabledMonitorsCount": 0,
"down": 0,
"downConfigs": Object {},
"enabledMonitorQueryIds": Array [
"id1",
"id2",
],
"pending": 3,
"pendingConfigs": Object {
"id1-asia_japan": Object {
"configId": "id1",
"isEnabled": true,
"isStatusAlertEnabled": false,
"locationId": "asia_japan",
"locationLabel": "Asia/Pacific - Japan",
"monitorQueryId": "id1",
"name": "test monitor 1",
"projectId": "project-id",
"schedule": "1",
"spaceId": undefined,
"status": "unknown",
"tags": Array [
"tag-1",
"tag-2",
],
"timestamp": undefined,
"type": "browser",
"updated_at": undefined,
},
"id2-asia_japan": Object {
"configId": "id2",
"isEnabled": true,
"isStatusAlertEnabled": false,
"locationId": "asia_japan",
"locationLabel": "Asia/Pacific - Japan",
"monitorQueryId": "id2",
"name": "test monitor 2",
"projectId": "project-id",
"schedule": "1",
"spaceId": undefined,
"status": "unknown",
"tags": Array [
"tag-1",
"tag-2",
],
"timestamp": undefined,
"type": "browser",
"updated_at": undefined,
},
"id2-europe_germany": Object {
"configId": "id2",
"isEnabled": true,
"isStatusAlertEnabled": false,
"locationId": "europe_germany",
"locationLabel": "Europe - Germany",
"monitorQueryId": "id2",
"name": "test monitor 2",
"projectId": "project-id",
"schedule": "1",
"spaceId": undefined,
"status": "unknown",
"tags": Array [
"tag-1",
"tag-2",
],
"timestamp": undefined,
"type": "browser",
"updated_at": undefined,
},
},
"projectMonitorsCount": 0,
"up": 0,
"upConfigs": Object {},
}
`);
});
});
describe('getStatus', () => {
jest.spyOn(allLocationsFn, 'getAllLocations').mockResolvedValue({
publicLocations: allLocations,
privateLocations: [],
allLocations: [
{
id: 'us_central_qa',
label: 'US Central QA',
},
{
id: 'us_central',
label: 'North America - US Central',
},
] as any,
});
it.each([
[['US Central QA'], 1],
[['North America - US Central'], 1],
[['North America - US Central', 'US Central QA'], 2],
[undefined, 2],
])('handles disabled count when using location filters', async (locations, disabledCount) => {
jest.spyOn(monitorsFns, 'getAllMonitors').mockResolvedValue([
{
type: 'synthetics-monitor',
id: 'a9a94f2f-47ba-4fe2-afaa-e5cd29b281f1',
attributes: {
enabled: false,
schedule: {
number: '3',
unit: 'm',
},
config_id: 'a9a94f2f-47ba-4fe2-afaa-e5cd29b281f1',
locations: [
{
isServiceManaged: true,
label: 'US Central QA',
id: 'us_central_qa',
},
{
isServiceManaged: true,
label: 'North America - US Central',
id: 'us_central',
},
],
origin: 'project',
id: 'a-test2-default',
},
references: [],
migrationVersion: {
'synthetics-monitor': '8.6.0',
},
coreMigrationVersion: '8.0.0',
updated_at: '2023-02-28T14:31:37.641Z',
created_at: '2023-02-28T14:31:37.641Z',
version: 'Wzg0MzkzLDVd',
namespaces: ['default'],
score: null,
sort: ['a', 3013],
} as unknown as SavedObjectsFindResult<EncryptedSyntheticsMonitorAttributes>,
]);
const { esClient, syntheticsEsClient } = getUptimeESMockClient();
esClient.msearch.mockResponseOnce({
responses: [
getEsResponse({
buckets: [
{
key: 'id1',
location: {
buckets: [
{
key: 'Asia/Pacific - Japan',
status: {
hits: {
hits: [
{
_source: {
'@timestamp': '2022-09-15T16:08:16.724Z',
monitor: {
status: 'up',
id: 'id1',
},
summary: {
up: 1,
down: 0,
},
config_id: 'id1',
observer: {
geo: {
name: 'Asia/Pacific - Japan',
},
},
},
},
],
},
},
},
],
},
},
{
key: 'id2',
location: {
buckets: [
{
key: 'Asia/Pacific - Japan',
status: {
hits: {
hits: [
{
_source: {
'@timestamp': '2022-09-15T16:09:16.724Z',
monitor: {
status: 'up',
id: 'id2',
},
summary: {
up: 1,
down: 0,
},
config_id: 'id2',
observer: {
geo: {
name: 'Asia/Pacific - Japan',
},
},
},
},
],
},
},
},
{
key: 'Europe - Germany',
status: {
hits: {
hits: [
{
_source: {
'@timestamp': '2022-09-15T16:19:16.724Z',
monitor: {
status: 'down',
id: 'id2',
},
summary: {
down: 1,
up: 0,
},
config_id: 'id2',
observer: {
geo: {
name: 'Europe - Germany',
},
},
},
},
],
},
},
},
],
},
},
],
}),
],
took: 605,
});
const overviewStatusService = new OverviewStatusService({
request: {
query: {
locations,
},
},
syntheticsEsClient,
} as any);
const result = await overviewStatusService.getOverviewStatus();
expect(result).toEqual(
expect.objectContaining({
disabledCount,
})
);
});
it.each([
[['US Central QA'], 1],
[['North America - US Central'], 1],
[['North America - US Central', 'US Central QA'], 2],
[undefined, 2],
])('handles pending count when using location filters', async (locations, pending) => {
jest.spyOn(monitorsFns, 'getAllMonitors').mockResolvedValue([
{
type: 'synthetics-monitor',
id: 'a9a94f2f-47ba-4fe2-afaa-e5cd29b281f1',
attributes: {
enabled: true,
schedule: {
number: '3',
unit: 'm',
},
config_id: 'a9a94f2f-47ba-4fe2-afaa-e5cd29b281f1',
locations: [
{
isServiceManaged: true,
label: 'US Central QA',
id: 'us_central_qa',
},
{
isServiceManaged: true,
label: 'North America - US Central',
id: 'us_central',
},
],
origin: 'project',
id: 'a-test2-default',
},
references: [],
migrationVersion: {
'synthetics-monitor': '8.6.0',
},
coreMigrationVersion: '8.0.0',
updated_at: '2023-02-28T14:31:37.641Z',
created_at: '2023-02-28T14:31:37.641Z',
version: 'Wzg0MzkzLDVd',
namespaces: ['default'],
score: null,
sort: ['a', 3013],
} as unknown as SavedObjectsFindResult<EncryptedSyntheticsMonitorAttributes>,
]);
const { esClient, syntheticsEsClient } = getUptimeESMockClient();
esClient.search.mockResponseOnce(
getEsResponse({
buckets: [],
})
);
const overviewStatusService = new OverviewStatusService({
request: {
query: {
locations,
},
},
syntheticsEsClient,
} as any);
const result = await overviewStatusService.getOverviewStatus();
expect(result.pending).toEqual(pending);
});
});
});
function getEsResponse({ buckets, after }: { buckets: any[]; after?: any }) {
return {
took: 605,
timed_out: false,
_shards: {
total: 3,
successful: 3,
skipped: 0,
failed: 0,
},
hits: {
hits: [],
},
aggregations: {
monitors: {
buckets,
after_key: after,
},
},
};
}

View file

@ -0,0 +1,363 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import moment from 'moment/moment';
import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { SavedObjectsFindResult } from '@kbn/core-saved-objects-api-server';
import { isEmpty } from 'lodash';
import { withApmSpan } from '@kbn/apm-data-access-plugin/server/utils/with_apm_span';
import { asMutableArray } from '../../../common/utils/as_mutable_array';
import { getMonitorFilters, OverviewStatusQuery } from '../common';
import {
getAllMonitors,
processMonitors,
} from '../../saved_objects/synthetics_monitor/get_all_monitors';
import { ConfigKey } from '../../../common/constants/monitor_management';
import { RouteContext } from '../types';
import {
EncryptedSyntheticsMonitorAttributes,
OverviewStatusMetaData,
} from '../../../common/runtime_types';
import { isStatusEnabled } from '../../../common/runtime_types/monitor_management/alert_config';
import {
FINAL_SUMMARY_FILTER,
getRangeFilter,
getTimespanFilter,
} from '../../../common/constants/client_defaults';
type LocationStatus = Array<{
status: string;
locationId: string;
timestamp: string;
}>;
export const SUMMARIES_PAGE_SIZE = 5000;
export class OverviewStatusService {
filterData: {
locationFilter?: string[] | string;
filtersStr?: string;
} = {};
constructor(
private readonly routeContext: RouteContext<Record<string, any>, OverviewStatusQuery>
) {}
async getOverviewStatus() {
const { request } = this.routeContext;
const queryParams = request.query as OverviewStatusQuery;
this.filterData = await getMonitorFilters({
...queryParams,
context: this.routeContext,
});
const [allConfigs, statusResult] = await Promise.all([
this.getMonitorConfigs(),
this.getQueryResult(),
]);
const { up, down, pending, upConfigs, downConfigs, pendingConfigs, disabledConfigs } =
this.processOverviewStatus(allConfigs, statusResult);
const {
enabledMonitorQueryIds,
disabledMonitorQueryIds,
allIds,
disabledCount,
disabledMonitorsCount,
projectMonitorsCount,
} = processMonitors(allConfigs, this.filterData?.locationFilter);
return {
allIds,
allMonitorsCount: allConfigs.length,
disabledMonitorsCount,
projectMonitorsCount,
enabledMonitorQueryIds,
disabledMonitorQueryIds,
disabledCount,
up,
down,
pending,
upConfigs,
downConfigs,
pendingConfigs,
disabledConfigs,
};
}
getEsDataFilters() {
const { spaceId, request } = this.routeContext;
const params = request.query || {};
const {
scopeStatusByLocation = true,
tags,
monitorTypes,
projects,
showFromAllSpaces,
} = params;
const { locationFilter } = this.filterData;
const getTermFilter = (field: string, value: string | string[] | undefined) => {
if (!value || isEmpty(value)) {
return [];
}
if (Array.isArray(value)) {
return [
{
terms: {
[field]: value,
},
},
];
}
return [
{
term: {
[field]: value,
},
},
];
};
const filters: QueryDslQueryContainer[] = [
...(showFromAllSpaces ? [] : [{ term: { 'meta.space_id': spaceId } }]),
...getTermFilter('monitor.type', monitorTypes),
...getTermFilter('tags', tags),
...getTermFilter('monitor.project.id', projects),
];
if (scopeStatusByLocation && !isEmpty(locationFilter) && locationFilter) {
filters.push({
terms: {
'observer.name': locationFilter,
},
});
}
return filters;
}
async getQueryResult() {
return withApmSpan('monitor_status_data', async () => {
const range = {
// max monitor schedule period is 4 hours, 20 minute subtraction is to be on safe side
from: moment().subtract(4, 'hours').subtract(20, 'minutes').toISOString(),
to: 'now',
};
let hasMoreData = true;
const monitorByIds = new Map<string, LocationStatus>();
let afterKey: any;
let count = 0;
do {
const result = await this.routeContext.syntheticsEsClient.search(
{
body: {
size: 0,
query: {
bool: {
filter: [
FINAL_SUMMARY_FILTER,
getRangeFilter({ from: range.from, to: range.to }),
getTimespanFilter({ from: 'now-15m', to: 'now' }),
...this.getEsDataFilters(),
] as QueryDslQueryContainer[],
},
},
aggs: {
monitors: {
composite: {
size: SUMMARIES_PAGE_SIZE,
sources: asMutableArray([
{
monitorId: {
terms: {
field: 'monitor.id',
},
},
},
{
locationId: {
terms: {
field: 'observer.name',
},
},
},
] as const),
after: afterKey,
},
aggs: {
status: {
top_metrics: {
metrics: {
field: 'monitor.status',
},
sort: {
'@timestamp': 'desc',
},
},
},
},
},
},
},
},
'getCurrentStatusOverview' + count
);
count += 1;
const data = result.body.aggregations?.monitors;
hasMoreData = (data?.buckets ?? []).length >= SUMMARIES_PAGE_SIZE;
afterKey = data?.after_key;
data?.buckets.forEach(({ status: statusAgg, key: bKey }) => {
const monitorId = String(bKey.monitorId);
const locationId = String(bKey.locationId);
const status = String(statusAgg.top?.[0].metrics?.['monitor.status']);
const timestamp = String(statusAgg.top[0].sort[0]);
if (!monitorByIds.has(String(monitorId))) {
monitorByIds.set(monitorId, []);
}
monitorByIds.get(monitorId)?.push({ status, locationId, timestamp });
});
} while (hasMoreData && afterKey);
return monitorByIds;
});
}
processOverviewStatus(
monitors: Array<SavedObjectsFindResult<EncryptedSyntheticsMonitorAttributes>>,
statusData: Map<string, LocationStatus>
) {
let up = 0;
let down = 0;
const upConfigs: Record<string, OverviewStatusMetaData> = {};
const downConfigs: Record<string, OverviewStatusMetaData> = {};
const pendingConfigs: Record<string, OverviewStatusMetaData> = {};
const disabledConfigs: Record<string, OverviewStatusMetaData> = {};
const enabledMonitors = monitors.filter((monitor) => monitor.attributes[ConfigKey.ENABLED]);
const disabledMonitors = monitors.filter((monitor) => !monitor.attributes[ConfigKey.ENABLED]);
const queryLocIds = this.filterData?.locationFilter;
disabledMonitors.forEach((monitor) => {
const monitorQueryId = monitor.attributes[ConfigKey.MONITOR_QUERY_ID];
const meta = this.getMonitorMeta(monitor);
monitor.attributes[ConfigKey.LOCATIONS]?.forEach((location) => {
disabledConfigs[`${meta.configId}-${location.id}`] = {
monitorQueryId,
status: 'disabled',
locationId: location.id,
locationLabel: location.label,
...meta,
};
});
});
enabledMonitors.forEach((monitor) => {
const monitorId = monitor.attributes[ConfigKey.MONITOR_QUERY_ID];
const monitorStatus = statusData.get(monitorId);
// discard any locations that are not in the monitorLocationsMap for the given monitor as well as those which are
// in monitorLocationsMap but not in listOfLocations
const monLocations = monitor.attributes[ConfigKey.LOCATIONS];
monLocations?.forEach((monLocation) => {
if (!isEmpty(queryLocIds) && !queryLocIds?.includes(monLocation.id)) {
// filter out location provided via query
return;
}
const locData = monitorStatus?.find((loc) => loc.locationId === monLocation.id);
const meta = {
monitorQueryId: monitorId,
locationId: monLocation.id,
timestamp: locData?.timestamp,
locationLabel: monLocation.label,
...this.getMonitorMeta(monitor),
};
const monLocId = `${meta.configId}-${monLocation.id}`;
if (locData) {
if (locData.status === 'down') {
down += 1;
downConfigs[monLocId] = {
...meta,
status: 'down',
};
} else if (locData.status === 'up') {
up += 1;
upConfigs[monLocId] = {
...meta,
status: 'up',
};
}
} else {
pendingConfigs[monLocId] = {
status: 'unknown',
...meta,
};
}
});
});
return {
up,
down,
pending: Object.values(pendingConfigs).length,
upConfigs,
downConfigs,
pendingConfigs,
disabledConfigs,
};
}
async getMonitorConfigs() {
const { savedObjectsClient, request } = this.routeContext;
const { query, showFromAllSpaces } = request.query || {};
/**
* Walk through all monitor saved objects, bucket IDs by disabled/enabled status.
*
* Track max period to make sure the snapshot query should reach back far enough to catch
* latest ping for all enabled monitors.
*/
const { filtersStr } = this.filterData;
return await getAllMonitors({
soClient: savedObjectsClient,
showFromAllSpaces,
search: query ? `${query}*` : '',
filter: filtersStr,
fields: [
ConfigKey.ENABLED,
ConfigKey.LOCATIONS,
ConfigKey.MONITOR_QUERY_ID,
ConfigKey.CONFIG_ID,
ConfigKey.SCHEDULE,
ConfigKey.MONITOR_SOURCE_TYPE,
ConfigKey.MONITOR_TYPE,
ConfigKey.NAME,
ConfigKey.TAGS,
ConfigKey.PROJECT_ID,
ConfigKey.ALERT_CONFIG,
],
});
}
getMonitorMeta(monitor: SavedObjectsFindResult<EncryptedSyntheticsMonitorAttributes>) {
return {
name: monitor.attributes[ConfigKey.NAME],
configId: monitor.attributes[ConfigKey.CONFIG_ID],
schedule: monitor.attributes[ConfigKey.SCHEDULE].number,
tags: monitor.attributes[ConfigKey.TAGS],
isEnabled: monitor.attributes[ConfigKey.ENABLED],
type: monitor.attributes[ConfigKey.MONITOR_TYPE],
projectId: monitor.attributes[ConfigKey.PROJECT_ID],
isStatusAlertEnabled: isStatusEnabled(monitor.attributes[ConfigKey.ALERT_CONFIG]),
updated_at: monitor.updated_at,
spaceId: monitor.namespaces?.[0],
};
}
}

View file

@ -0,0 +1,27 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { periodToMs } from './utils';
describe('periodToMs', () => {
it('returns 0 for unsupported unit type', () => {
// @ts-expect-error Providing invalid value to test handler in function
expect(periodToMs({ number: '10', unit: 'rad' })).toEqual(0);
});
it('converts seconds', () => {
expect(periodToMs({ number: '10', unit: 's' })).toEqual(10_000);
});
it('converts minutes', () => {
expect(periodToMs({ number: '1', unit: 'm' })).toEqual(60_000);
});
it('converts hours', () => {
expect(periodToMs({ number: '1', unit: 'h' })).toEqual(3_600_000);
});
});

View file

@ -0,0 +1,20 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
/**
* Helper function that converts a monitor's schedule to a value to use to generate
* an appropriate look-back window for snapshot count.
* @param schedule a number/unit pair that represents how often a configured monitor runs
* @returns schedule interval in ms
*/
import datemath, { Unit } from '@kbn/datemath';
export function periodToMs(schedule: { number: string; unit: Unit }) {
if (Object.keys(datemath.unitsMap).indexOf(schedule.unit) === -1) return 0;
return parseInt(schedule.number, 10) * datemath.unitsMap[schedule.unit].base;
}

View file

@ -11,8 +11,9 @@ import {
SavedObjectsFindResult,
} from '@kbn/core-saved-objects-api-server';
import { intersection } from 'lodash';
import { withApmSpan } from '@kbn/apm-data-access-plugin/server/utils';
import { periodToMs } from '../../routes/overview_status/utils';
import { syntheticsMonitorType } from '../../../common/types/saved_objects';
import { periodToMs } from '../../routes/overview_status/overview_status';
import {
ConfigKey,
EncryptedSyntheticsMonitorAttributes,
@ -34,26 +35,28 @@ export const getAllMonitors = async ({
filter?: string;
showFromAllSpaces?: boolean;
} & Pick<SavedObjectsFindOptions, 'sortField' | 'sortOrder' | 'fields' | 'searchFields'>) => {
const finder = soClient.createPointInTimeFinder<EncryptedSyntheticsMonitorAttributes>({
type: syntheticsMonitorType,
perPage: 1000,
search,
sortField,
sortOrder,
fields,
filter,
searchFields,
...(showFromAllSpaces && { namespaces: ['*'] }),
return withApmSpan('get_all_monitors', async () => {
const finder = soClient.createPointInTimeFinder<EncryptedSyntheticsMonitorAttributes>({
type: syntheticsMonitorType,
perPage: 5000,
search,
sortField,
sortOrder,
fields,
filter,
searchFields,
...(showFromAllSpaces && { namespaces: ['*'] }),
});
const hits: Array<SavedObjectsFindResult<EncryptedSyntheticsMonitorAttributes>> = [];
for await (const result of finder.find()) {
hits.push(...result.saved_objects);
}
finder.close().catch(() => {});
return hits;
});
const hits: Array<SavedObjectsFindResult<EncryptedSyntheticsMonitorAttributes>> = [];
for await (const result of finder.find()) {
hits.push(...result.saved_objects);
}
finder.close().catch(() => {});
return hits;
};
export const processMonitors = (
@ -86,13 +89,13 @@ export const processMonitors = (
monitorQueryIdToConfigIdMap[attrs[ConfigKey.MONITOR_QUERY_ID]] = attrs[ConfigKey.CONFIG_ID];
const monitorLocations = attrs[ConfigKey.LOCATIONS].map((location) => location.id);
const monitorLocIds = attrs[ConfigKey.LOCATIONS].map((location) => location.id);
if (attrs[ConfigKey.ENABLED] === false) {
const queriedLocations = Array.isArray(queryLocations) ? queryLocations : [queryLocations];
const intersectingLocations = intersection(
monitorLocations,
queryLocations ? queriedLocations : monitorLocations
monitorLocIds,
queryLocations ? queriedLocations : monitorLocIds
);
disabledCount += intersectingLocations.length;
disabledMonitorsCount += 1;
@ -101,9 +104,9 @@ export const processMonitors = (
enabledMonitorQueryIds.push(attrs[ConfigKey.MONITOR_QUERY_ID]);
monitorLocationsMap[attrs[ConfigKey.MONITOR_QUERY_ID]] = queryLocations
? intersection(monitorLocations, queryLocations)
: monitorLocations;
listOfLocationsSet = new Set([...listOfLocationsSet, ...monitorLocations]);
? intersection(monitorLocIds, queryLocations)
: monitorLocIds;
listOfLocationsSet = new Set([...listOfLocationsSet, ...monitorLocIds]);
maxPeriod = Math.max(maxPeriod, periodToMs(attrs[ConfigKey.SCHEDULE]));
}

View file

@ -4,107 +4,114 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { withApmSpan } from '@kbn/apm-data-access-plugin/server/utils/with_apm_span';
import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common';
import { isEmpty } from 'lodash';
import { isKibanaResponse } from '@kbn/core-http-server';
import { isTestUser, SyntheticsEsClient } from './lib';
import { checkIndicesReadPrivileges } from './synthetics_service/authentication/check_has_privilege';
import { SYNTHETICS_INDEX_PATTERN } from '../common/constants';
import { syntheticsServiceApiKey } from './saved_objects/service_api_key';
import { isTestUser, SyntheticsEsClient } from './lib';
import { SYNTHETICS_INDEX_PATTERN } from '../common/constants';
import { checkIndicesReadPrivileges } from './synthetics_service/authentication/check_has_privilege';
import { SyntheticsRouteWrapper } from './routes/types';
export const syntheticsRouteWrapper: SyntheticsRouteWrapper = (
uptimeRoute,
syntheticsRoute,
server,
syntheticsMonitorClient
) => ({
...uptimeRoute,
...syntheticsRoute,
options: {
...(uptimeRoute.options ?? {}),
...(syntheticsRoute.options ?? {}),
},
security: {
authz: {
requiredPrivileges: [
'uptime-read',
...(uptimeRoute.requiredPrivileges ?? []),
...(uptimeRoute?.writeAccess ? ['uptime-write'] : []),
...(syntheticsRoute.requiredPrivileges ?? []),
...(syntheticsRoute?.writeAccess ? ['uptime-write'] : []),
],
},
},
handler: async (context, request, response) => {
const { elasticsearch, savedObjects, uiSettings } = await context.core;
return withApmSpan('synthetics_route_handler', async () => {
const { elasticsearch, savedObjects, uiSettings } = await context.core;
const { client: esClient } = elasticsearch;
const savedObjectsClient = savedObjects.getClient({
includedHiddenTypes: [syntheticsServiceApiKey.name],
});
const { client: esClient } = elasticsearch;
const savedObjectsClient = savedObjects.getClient({
includedHiddenTypes: [syntheticsServiceApiKey.name],
});
// specifically needed for the synthetics service api key generation
server.authSavedObjectsClient = savedObjectsClient;
// specifically needed for the synthetics service api key generation
server.authSavedObjectsClient = savedObjectsClient;
const syntheticsEsClient = new SyntheticsEsClient(savedObjectsClient, esClient.asCurrentUser, {
request,
uiSettings,
isDev: Boolean(server.isDev) && !isTestUser(server),
heartbeatIndices: SYNTHETICS_INDEX_PATTERN,
});
server.syntheticsEsClient = syntheticsEsClient;
const spaceId = server.spaces?.spacesService.getSpaceId(request) ?? DEFAULT_SPACE_ID;
try {
const res = await uptimeRoute.handler({
syntheticsEsClient,
const syntheticsEsClient = new SyntheticsEsClient(
savedObjectsClient,
context,
request,
response,
server,
spaceId,
syntheticsMonitorClient,
});
if (isKibanaResponse(res)) {
return res;
}
const inspectData = await syntheticsEsClient.getInspectData(uptimeRoute.path);
if (Array.isArray(res)) {
if (isEmpty(inspectData)) {
return response.ok({
body: res,
});
} else {
return response.ok({
body: {
result: res,
...inspectData,
},
});
esClient.asCurrentUser,
{
request,
uiSettings,
isDev: Boolean(server.isDev) && !isTestUser(server),
heartbeatIndices: SYNTHETICS_INDEX_PATTERN,
}
}
);
return response.ok({
body: {
...res,
...(await syntheticsEsClient.getInspectData(uptimeRoute.path)),
},
});
} catch (e) {
if (e.statusCode === 403) {
const privileges = await checkIndicesReadPrivileges(syntheticsEsClient);
if (!privileges.has_all_requested) {
return response.forbidden({
body: {
message:
'MissingIndicesPrivileges: You do not have permission to read from the synthetics-* indices. Please contact your administrator.',
},
});
server.syntheticsEsClient = syntheticsEsClient;
const spaceId = server.spaces?.spacesService.getSpaceId(request) ?? DEFAULT_SPACE_ID;
try {
const res = await syntheticsRoute.handler({
syntheticsEsClient,
savedObjectsClient,
context,
request,
response,
server,
spaceId,
syntheticsMonitorClient,
});
if (isKibanaResponse(res)) {
return res;
}
const inspectData = await syntheticsEsClient.getInspectData(syntheticsRoute.path);
if (Array.isArray(res)) {
if (isEmpty(inspectData)) {
return response.ok({
body: res,
});
} else {
return response.ok({
body: {
result: res,
...inspectData,
},
});
}
}
return response.ok({
body: {
...res,
...inspectData,
},
});
} catch (e) {
if (e.statusCode === 403) {
const privileges = await checkIndicesReadPrivileges(syntheticsEsClient);
if (!privileges.has_all_requested) {
return response.forbidden({
body: {
message:
'MissingIndicesPrivileges: You do not have permission to read from the synthetics-* indices. Please contact your administrator.',
},
});
}
}
server.logger.error(e);
throw e;
}
server.logger.error(e);
throw e;
}
});
},
});

View file

@ -356,11 +356,18 @@ export class SyntheticsService {
if (output) {
this.logger.debug(`1 monitor will be pushed to synthetics service.`);
this.syncErrors = await this.apiClient.post({
monitors,
output,
license,
});
this.apiClient
.post({
monitors,
output,
license,
})
.then((res) => {
this.syncErrors = res;
})
.catch((e) => {
this.logger.error(e);
});
}
return this.syncErrors;
} catch (e) {

View file

@ -106,7 +106,8 @@
"@kbn/core-chrome-browser",
"@kbn/core-rendering-browser",
"@kbn/index-lifecycle-management-common-shared",
"@kbn/core-http-server-utils"
"@kbn/core-http-server-utils",
"@kbn/apm-data-access-plugin"
],
"exclude": ["target/**/*"]
}