mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Synthetics] Improve synthetics alerting (#186585)
## Summary Fixes https://github.com/elastic/kibana/issues/175298 Improve synthetics alerting !! User will be able to create custom synthetics status alert by defining three kind of criteria ### Monitor is down over last consective checks with threshold <img width="639" alt="image" src="390da238
-f7f2-4eb0-9606-3279b3199fdf"> ### From Locations threshold Will be considered down only when from defined number of locations <img width="618" alt="image" src="24741a10
-0880-4247-9048-8ce03df25bf5"> ### Over time with checks threshold just like uptime custom status alert <img width="631" alt="image" src="64e1c808
-8d4b-4dd0-b794-eb7f4e5d1e6b"> --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Dominique Clarke <dominique.clarke@elastic.co> Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com> Co-authored-by: Maryam Saeidi <maryam.saeidi@elastic.co> Co-authored-by: Justin Kambic <jk@elastic.co>
This commit is contained in:
parent
9e117c3aa2
commit
82d0b008cd
100 changed files with 6148 additions and 1301 deletions
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
|
@ -656,6 +656,7 @@ x-pack/plugins/observability_solution/observability_onboarding/e2e @elastic/obs-
|
|||
x-pack/plugins/observability_solution/observability_onboarding @elastic/obs-ux-logs-team @elastic/obs-ux-onboarding-team
|
||||
x-pack/plugins/observability_solution/observability @elastic/obs-ux-management-team
|
||||
x-pack/plugins/observability_solution/observability_shared @elastic/observability-ui
|
||||
x-pack/packages/observability/synthetics_test_data @elastic/obs-ux-management-team
|
||||
x-pack/packages/observability/observability_utils @elastic/observability-ui
|
||||
x-pack/test/security_api_integration/plugins/oidc_provider @elastic/kibana-security
|
||||
test/common/plugins/otel_metrics @elastic/obs-ux-infra_services-team
|
||||
|
|
|
@ -689,6 +689,7 @@
|
|||
"@kbn/observability-onboarding-plugin": "link:x-pack/plugins/observability_solution/observability_onboarding",
|
||||
"@kbn/observability-plugin": "link:x-pack/plugins/observability_solution/observability",
|
||||
"@kbn/observability-shared-plugin": "link:x-pack/plugins/observability_solution/observability_shared",
|
||||
"@kbn/observability-synthetics-test-data": "link:x-pack/packages/observability/synthetics_test_data",
|
||||
"@kbn/observability-utils": "link:x-pack/packages/observability/observability_utils",
|
||||
"@kbn/oidc-provider-plugin": "link:x-pack/test/security_api_integration/plugins/oidc_provider",
|
||||
"@kbn/open-telemetry-instrumented-plugin": "link:test/common/plugins/otel_metrics",
|
||||
|
|
|
@ -88,13 +88,15 @@ const ObservabilityUptimeAlertOptional = rt.partial({
|
|||
value: schemaStringArray,
|
||||
})
|
||||
),
|
||||
'location.id': schemaString,
|
||||
'location.name': schemaString,
|
||||
'location.id': schemaStringArray,
|
||||
'location.name': schemaStringArray,
|
||||
'monitor.id': schemaString,
|
||||
'monitor.name': schemaString,
|
||||
'monitor.state.id': schemaString,
|
||||
'monitor.tags': schemaStringArray,
|
||||
'monitor.type': schemaString,
|
||||
'observer.geo.name': schemaString,
|
||||
'observer.geo.name': schemaStringArray,
|
||||
'observer.name': schemaStringArray,
|
||||
'tls.server.hash.sha256': schemaString,
|
||||
'tls.server.x509.issuer.common_name': schemaString,
|
||||
'tls.server.x509.not_after': schemaDate,
|
||||
|
|
|
@ -1306,6 +1306,8 @@
|
|||
"@kbn/observability-plugin/*": ["x-pack/plugins/observability_solution/observability/*"],
|
||||
"@kbn/observability-shared-plugin": ["x-pack/plugins/observability_solution/observability_shared"],
|
||||
"@kbn/observability-shared-plugin/*": ["x-pack/plugins/observability_solution/observability_shared/*"],
|
||||
"@kbn/observability-synthetics-test-data": ["x-pack/packages/observability/synthetics_test_data"],
|
||||
"@kbn/observability-synthetics-test-data/*": ["x-pack/packages/observability/synthetics_test_data/*"],
|
||||
"@kbn/observability-utils": ["x-pack/packages/observability/observability_utils"],
|
||||
"@kbn/observability-utils/*": ["x-pack/packages/observability/observability_utils/*"],
|
||||
"@kbn/oidc-provider-plugin": ["x-pack/test/security_api_integration/plugins/oidc_provider"],
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
# @kbn/observability-synthetics-test-data
|
||||
|
||||
Provides utilities to generate synthetics test data
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { makeUpSummary, makeDownSummary } from './src/make_summaries';
|
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
preset: '@kbn/test',
|
||||
rootDir: '../../../..',
|
||||
roots: ['<rootDir>/x-pack/packages/observability/synthetics_test_data'],
|
||||
};
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"type": "shared-common",
|
||||
"id": "@kbn/observability-synthetics-test-data",
|
||||
"owner": "@elastic/obs-ux-management-team",
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"name": "@kbn/observability-synthetics-test-data",
|
||||
"descriptio": "Utils to generate observability synthetics test data",
|
||||
"author": "UX Management",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"license": "Elastic License 2.0"
|
||||
}
|
|
@ -6,124 +6,39 @@
|
|||
*/
|
||||
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { getGeoData } from './browser_docs';
|
||||
import moment from 'moment';
|
||||
import { getGeoData } from './utils';
|
||||
|
||||
export interface DocOverrides {
|
||||
timestamp?: string;
|
||||
monitorId?: string;
|
||||
name?: string;
|
||||
testRunId?: string;
|
||||
locationName?: string;
|
||||
location?: {
|
||||
id: string;
|
||||
label: string;
|
||||
};
|
||||
configId?: string;
|
||||
}
|
||||
|
||||
export const getUpHit = ({
|
||||
export const makeUpSummary = ({
|
||||
name,
|
||||
timestamp,
|
||||
monitorId,
|
||||
configId,
|
||||
testRunId,
|
||||
locationName,
|
||||
location,
|
||||
}: DocOverrides = {}) => ({
|
||||
...getGeoData(locationName),
|
||||
...getGeoData(location),
|
||||
...commons,
|
||||
summary: {
|
||||
up: 1,
|
||||
down: 0,
|
||||
final_attempt: true,
|
||||
},
|
||||
tcp: {
|
||||
rtt: {
|
||||
connect: {
|
||||
us: 22245,
|
||||
},
|
||||
},
|
||||
},
|
||||
agent: {
|
||||
name: 'docker-fleet-server',
|
||||
id: 'dd39a87d-a1e5-45a1-8dd9-e78d6a1391c6',
|
||||
type: 'heartbeat',
|
||||
ephemeral_id: '264bb432-93f6-4aa6-a14d-266c53b9e7c7',
|
||||
version: '8.7.0',
|
||||
},
|
||||
resolve: {
|
||||
rtt: {
|
||||
us: 3101,
|
||||
},
|
||||
ip: '142.250.181.196',
|
||||
},
|
||||
elastic_agent: {
|
||||
id: 'dd39a87d-a1e5-45a1-8dd9-e78d6a1391c6',
|
||||
version: '8.7.0',
|
||||
snapshot: true,
|
||||
},
|
||||
monitor: {
|
||||
duration: {
|
||||
us: 155239,
|
||||
},
|
||||
ip: '142.250.181.196',
|
||||
origin: 'ui',
|
||||
name: name ?? 'Test Monitor',
|
||||
timespan: {
|
||||
lt: '2022-12-18T09:55:04.211Z',
|
||||
gte: '2022-12-18T09:52:04.211Z',
|
||||
},
|
||||
fleet_managed: true,
|
||||
id: monitorId ?? 'b9d9e146-746f-427f-bbf5-6e786b5b4e73',
|
||||
check_group: 'a039fd21-7eb9-11ed-8949-0242ac120006',
|
||||
type: 'http',
|
||||
status: 'up',
|
||||
},
|
||||
url: {
|
||||
scheme: 'https',
|
||||
port: 443,
|
||||
domain: 'www.google.com',
|
||||
full: 'https://www.google.com',
|
||||
},
|
||||
monitor: getMonitorData({ id: monitorId, name, status: 'up', timestamp }),
|
||||
'@timestamp': timestamp ?? '2022-12-18T09:52:04.056Z',
|
||||
ecs: {
|
||||
version: '8.0.0',
|
||||
},
|
||||
config_id: configId ?? 'b9d9e146-746f-427f-bbf5-6e786b5b4e73',
|
||||
data_stream: {
|
||||
namespace: 'default',
|
||||
type: 'synthetics',
|
||||
dataset: 'http',
|
||||
},
|
||||
tls: {
|
||||
cipher: 'TLS-AES-128-GCM-SHA256',
|
||||
certificate_not_valid_before: '2022-11-28T08:19:01.000Z',
|
||||
established: true,
|
||||
server: {
|
||||
x509: {
|
||||
not_after: '2023-02-20T08:19:00.000Z',
|
||||
subject: {
|
||||
distinguished_name: 'CN=www.google.com',
|
||||
common_name: 'www.google.com',
|
||||
},
|
||||
not_before: '2022-11-28T08:19:01.000Z',
|
||||
public_key_curve: 'P-256',
|
||||
public_key_algorithm: 'ECDSA',
|
||||
signature_algorithm: 'SHA256-RSA',
|
||||
serial_number: '173037077033925240295268439311466214245',
|
||||
issuer: {
|
||||
distinguished_name: 'CN=GTS CA 1C3,O=Google Trust Services LLC,C=US',
|
||||
common_name: 'GTS CA 1C3',
|
||||
},
|
||||
},
|
||||
hash: {
|
||||
sha1: 'ea1b44061b864526c45619230b3299117d11bf4e',
|
||||
sha256: 'a5686448de09cc82b9cdad1e96357f919552ab14244da7948dd412ec0fc37d2b',
|
||||
},
|
||||
},
|
||||
rtt: {
|
||||
handshake: {
|
||||
us: 35023,
|
||||
},
|
||||
},
|
||||
version: '1.3',
|
||||
certificate_not_valid_after: '2023-02-20T08:19:00.000Z',
|
||||
version_protocol: 'tls',
|
||||
},
|
||||
state: {
|
||||
duration_ms: 0,
|
||||
checks: 1,
|
||||
|
@ -135,67 +50,81 @@ export const getUpHit = ({
|
|||
flap_history: [],
|
||||
status: 'up',
|
||||
},
|
||||
event: {
|
||||
agent_id_status: 'verified',
|
||||
ingested: '2022-12-18T09:52:11Z',
|
||||
dataset: 'http',
|
||||
},
|
||||
...(testRunId && { test_run_id: testRunId }),
|
||||
http: {
|
||||
rtt: {
|
||||
response_header: {
|
||||
us: 144758,
|
||||
},
|
||||
total: {
|
||||
us: 149191,
|
||||
},
|
||||
write_request: {
|
||||
us: 48,
|
||||
},
|
||||
content: {
|
||||
us: 401,
|
||||
},
|
||||
validate: {
|
||||
us: 145160,
|
||||
},
|
||||
},
|
||||
response: {
|
||||
headers: {
|
||||
Server: 'gws',
|
||||
P3p: 'CP="This is not a P3P policy! See g.co/p3phelp for more info."',
|
||||
Date: 'Thu, 29 Dec 2022 08:17:09 GMT',
|
||||
'X-Frame-Options': 'SAMEORIGIN',
|
||||
'Accept-Ranges': 'none',
|
||||
'Cache-Control': 'private, max-age=0',
|
||||
'X-Xss-Protection': '0',
|
||||
'Cross-Origin-Opener-Policy-Report-Only': 'same-origin-allow-popups; report-to="gws"',
|
||||
Vary: 'Accept-Encoding',
|
||||
Expires: '-1',
|
||||
'Content-Type': 'text/html; charset=ISO-8859-1',
|
||||
},
|
||||
status_code: 200,
|
||||
mime_type: 'text/html; charset=utf-8',
|
||||
body: {
|
||||
bytes: 13963,
|
||||
hash: 'a4c2cf7dead9fb9329fc3727fc152b6a12072410926430491d02a0c6dc3a70ff',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const firstDownHit = ({
|
||||
export const makeDownSummary = ({
|
||||
name,
|
||||
timestamp,
|
||||
monitorId,
|
||||
locationName,
|
||||
location,
|
||||
configId,
|
||||
}: DocOverrides = {}) => ({
|
||||
...getGeoData(locationName),
|
||||
...getGeoData(location),
|
||||
...commons,
|
||||
summary: {
|
||||
up: 0,
|
||||
down: 1,
|
||||
final_attempt: true,
|
||||
},
|
||||
monitor: getMonitorData({ id: monitorId, name, status: 'down', timestamp }),
|
||||
error: {
|
||||
message: 'received status code 200 expecting [500]',
|
||||
type: 'validate',
|
||||
},
|
||||
'@timestamp': timestamp ?? '2022-12-18T09:49:49.976Z',
|
||||
config_id: configId ?? 'b9d9e146-746f-427f-bbf5-6e786b5b4e73',
|
||||
state: {
|
||||
duration_ms: 0,
|
||||
checks: 1,
|
||||
ends: null,
|
||||
started_at: '2022-12-18T09:49:56.007551998Z',
|
||||
id: 'Test private location-18524a3d9a7-0',
|
||||
up: 0,
|
||||
down: 1,
|
||||
flap_history: [],
|
||||
status: 'down',
|
||||
},
|
||||
});
|
||||
|
||||
const getMonitorData = ({
|
||||
id,
|
||||
name,
|
||||
status,
|
||||
timestamp,
|
||||
}: {
|
||||
id?: string;
|
||||
name?: string;
|
||||
status: 'up' | 'down';
|
||||
timestamp?: string;
|
||||
}) => ({
|
||||
duration: {
|
||||
us: 152459,
|
||||
},
|
||||
origin: 'ui',
|
||||
ip: '142.250.181.196',
|
||||
name: name ?? 'Test Monitor',
|
||||
fleet_managed: true,
|
||||
check_group: uuidv4(),
|
||||
timespan: {
|
||||
lt: timestamp ?? '2022-12-18T09:52:50.128Z',
|
||||
gte: timestamp ? moment(timestamp).subtract(3, 'minutes') : '2022-12-18T09:49:50.128Z',
|
||||
},
|
||||
id: id ?? 'b9d9e146-746f-427f-bbf5-6e786b5b4e73',
|
||||
type: 'http',
|
||||
status: status ?? 'down',
|
||||
});
|
||||
|
||||
const commons = {
|
||||
url: {
|
||||
scheme: 'https',
|
||||
port: 443,
|
||||
domain: 'www.google.com',
|
||||
full: 'https://www.google.com',
|
||||
},
|
||||
ecs: {
|
||||
version: '8.0.0',
|
||||
},
|
||||
tcp: {
|
||||
rtt: {
|
||||
connect: {
|
||||
|
@ -203,6 +132,11 @@ export const firstDownHit = ({
|
|||
},
|
||||
},
|
||||
},
|
||||
event: {
|
||||
agent_id_status: 'verified',
|
||||
ingested: '2022-12-18T09:49:57Z',
|
||||
dataset: 'http',
|
||||
},
|
||||
agent: {
|
||||
name: 'docker-fleet-server',
|
||||
id: 'dd39a87d-a1e5-45a1-8dd9-e78d6a1391c6',
|
||||
|
@ -210,54 +144,22 @@ export const firstDownHit = ({
|
|||
ephemeral_id: '264bb432-93f6-4aa6-a14d-266c53b9e7c7',
|
||||
version: '8.7.0',
|
||||
},
|
||||
resolve: {
|
||||
rtt: {
|
||||
us: 3234,
|
||||
},
|
||||
ip: '142.250.181.196',
|
||||
},
|
||||
elastic_agent: {
|
||||
id: 'dd39a87d-a1e5-45a1-8dd9-e78d6a1391c6',
|
||||
version: '8.7.0',
|
||||
snapshot: true,
|
||||
},
|
||||
monitor: {
|
||||
duration: {
|
||||
us: 152459,
|
||||
},
|
||||
origin: 'ui',
|
||||
ip: '142.250.181.196',
|
||||
name: name ?? 'Test Monitor',
|
||||
fleet_managed: true,
|
||||
check_group: uuidv4(),
|
||||
timespan: {
|
||||
lt: '2022-12-18T09:52:50.128Z',
|
||||
gte: '2022-12-18T09:49:50.128Z',
|
||||
},
|
||||
id: monitorId ?? 'b9d9e146-746f-427f-bbf5-6e786b5b4e73',
|
||||
type: 'http',
|
||||
status: 'down',
|
||||
},
|
||||
error: {
|
||||
message: 'received status code 200 expecting [500]',
|
||||
type: 'validate',
|
||||
},
|
||||
url: {
|
||||
scheme: 'https',
|
||||
port: 443,
|
||||
domain: 'www.google.com',
|
||||
full: 'https://www.google.com',
|
||||
},
|
||||
'@timestamp': timestamp ?? '2022-12-18T09:49:49.976Z',
|
||||
ecs: {
|
||||
version: '8.0.0',
|
||||
},
|
||||
config_id: configId ?? 'b9d9e146-746f-427f-bbf5-6e786b5b4e73',
|
||||
data_stream: {
|
||||
namespace: 'default',
|
||||
type: 'synthetics',
|
||||
dataset: 'http',
|
||||
},
|
||||
resolve: {
|
||||
rtt: {
|
||||
us: 3101,
|
||||
},
|
||||
ip: '142.250.181.196',
|
||||
},
|
||||
tls: {
|
||||
established: true,
|
||||
cipher: 'TLS-AES-128-GCM-SHA256',
|
||||
|
@ -293,22 +195,6 @@ export const firstDownHit = ({
|
|||
certificate_not_valid_after: '2023-02-20T08:19:00.000Z',
|
||||
version_protocol: 'tls',
|
||||
},
|
||||
state: {
|
||||
duration_ms: 0,
|
||||
checks: 1,
|
||||
ends: null,
|
||||
started_at: '2022-12-18T09:49:56.007551998Z',
|
||||
id: 'Test private location-18524a3d9a7-0',
|
||||
up: 0,
|
||||
down: 1,
|
||||
flap_history: [],
|
||||
status: 'down',
|
||||
},
|
||||
event: {
|
||||
agent_id_status: 'verified',
|
||||
ingested: '2022-12-18T09:49:57Z',
|
||||
dataset: 'http',
|
||||
},
|
||||
http: {
|
||||
rtt: {
|
||||
response_header: {
|
||||
|
@ -349,4 +235,4 @@ export const firstDownHit = ({
|
|||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export const getGeoData = ({ id, label }: { label?: string; id?: string } = {}) => ({
|
||||
observer: {
|
||||
geo: {
|
||||
name: label ?? 'Dev Service',
|
||||
location: '41.8780, 93.0977',
|
||||
},
|
||||
name: id ?? 'dev',
|
||||
},
|
||||
});
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"extends": "../../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "target/types",
|
||||
"types": [
|
||||
"jest",
|
||||
"node",
|
||||
"react"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*"
|
||||
],
|
||||
"kbn_references": [
|
||||
]
|
||||
}
|
|
@ -9852,10 +9852,12 @@ Object {
|
|||
"type": "keyword",
|
||||
},
|
||||
"location.id": Object {
|
||||
"array": true,
|
||||
"required": false,
|
||||
"type": "keyword",
|
||||
},
|
||||
"location.name": Object {
|
||||
"array": true,
|
||||
"required": false,
|
||||
"type": "keyword",
|
||||
},
|
||||
|
@ -9867,6 +9869,10 @@ Object {
|
|||
"required": false,
|
||||
"type": "keyword",
|
||||
},
|
||||
"monitor.state.id": Object {
|
||||
"required": false,
|
||||
"type": "keyword",
|
||||
},
|
||||
"monitor.tags": Object {
|
||||
"array": true,
|
||||
"required": false,
|
||||
|
@ -9877,6 +9883,12 @@ Object {
|
|||
"type": "keyword",
|
||||
},
|
||||
"observer.geo.name": Object {
|
||||
"array": true,
|
||||
"required": false,
|
||||
"type": "keyword",
|
||||
},
|
||||
"observer.name": Object {
|
||||
"array": true,
|
||||
"required": false,
|
||||
"type": "keyword",
|
||||
},
|
||||
|
@ -9972,10 +9984,12 @@ Object {
|
|||
"type": "keyword",
|
||||
},
|
||||
"location.id": Object {
|
||||
"array": true,
|
||||
"required": false,
|
||||
"type": "keyword",
|
||||
},
|
||||
"location.name": Object {
|
||||
"array": true,
|
||||
"required": false,
|
||||
"type": "keyword",
|
||||
},
|
||||
|
@ -9987,6 +10001,10 @@ Object {
|
|||
"required": false,
|
||||
"type": "keyword",
|
||||
},
|
||||
"monitor.state.id": Object {
|
||||
"required": false,
|
||||
"type": "keyword",
|
||||
},
|
||||
"monitor.tags": Object {
|
||||
"array": true,
|
||||
"required": false,
|
||||
|
@ -9997,6 +10015,12 @@ Object {
|
|||
"type": "keyword",
|
||||
},
|
||||
"observer.geo.name": Object {
|
||||
"array": true,
|
||||
"required": false,
|
||||
"type": "keyword",
|
||||
},
|
||||
"observer.name": Object {
|
||||
"array": true,
|
||||
"required": false,
|
||||
"type": "keyword",
|
||||
},
|
||||
|
@ -10092,10 +10116,12 @@ Object {
|
|||
"type": "keyword",
|
||||
},
|
||||
"location.id": Object {
|
||||
"array": true,
|
||||
"required": false,
|
||||
"type": "keyword",
|
||||
},
|
||||
"location.name": Object {
|
||||
"array": true,
|
||||
"required": false,
|
||||
"type": "keyword",
|
||||
},
|
||||
|
@ -10107,6 +10133,10 @@ Object {
|
|||
"required": false,
|
||||
"type": "keyword",
|
||||
},
|
||||
"monitor.state.id": Object {
|
||||
"required": false,
|
||||
"type": "keyword",
|
||||
},
|
||||
"monitor.tags": Object {
|
||||
"array": true,
|
||||
"required": false,
|
||||
|
@ -10117,6 +10147,12 @@ Object {
|
|||
"type": "keyword",
|
||||
},
|
||||
"observer.geo.name": Object {
|
||||
"array": true,
|
||||
"required": false,
|
||||
"type": "keyword",
|
||||
},
|
||||
"observer.name": Object {
|
||||
"array": true,
|
||||
"required": false,
|
||||
"type": "keyword",
|
||||
},
|
||||
|
@ -10212,10 +10248,12 @@ Object {
|
|||
"type": "keyword",
|
||||
},
|
||||
"location.id": Object {
|
||||
"array": true,
|
||||
"required": false,
|
||||
"type": "keyword",
|
||||
},
|
||||
"location.name": Object {
|
||||
"array": true,
|
||||
"required": false,
|
||||
"type": "keyword",
|
||||
},
|
||||
|
@ -10227,6 +10265,10 @@ Object {
|
|||
"required": false,
|
||||
"type": "keyword",
|
||||
},
|
||||
"monitor.state.id": Object {
|
||||
"required": false,
|
||||
"type": "keyword",
|
||||
},
|
||||
"monitor.tags": Object {
|
||||
"array": true,
|
||||
"required": false,
|
||||
|
@ -10237,6 +10279,12 @@ Object {
|
|||
"type": "keyword",
|
||||
},
|
||||
"observer.geo.name": Object {
|
||||
"array": true,
|
||||
"required": false,
|
||||
"type": "keyword",
|
||||
},
|
||||
"observer.name": Object {
|
||||
"array": true,
|
||||
"required": false,
|
||||
"type": "keyword",
|
||||
},
|
||||
|
@ -10338,10 +10386,12 @@ Object {
|
|||
"type": "keyword",
|
||||
},
|
||||
"location.id": Object {
|
||||
"array": true,
|
||||
"required": false,
|
||||
"type": "keyword",
|
||||
},
|
||||
"location.name": Object {
|
||||
"array": true,
|
||||
"required": false,
|
||||
"type": "keyword",
|
||||
},
|
||||
|
@ -10353,6 +10403,10 @@ Object {
|
|||
"required": false,
|
||||
"type": "keyword",
|
||||
},
|
||||
"monitor.state.id": Object {
|
||||
"required": false,
|
||||
"type": "keyword",
|
||||
},
|
||||
"monitor.tags": Object {
|
||||
"array": true,
|
||||
"required": false,
|
||||
|
@ -10363,6 +10417,12 @@ Object {
|
|||
"type": "keyword",
|
||||
},
|
||||
"observer.geo.name": Object {
|
||||
"array": true,
|
||||
"required": false,
|
||||
"type": "keyword",
|
||||
},
|
||||
"observer.name": Object {
|
||||
"array": true,
|
||||
"required": false,
|
||||
"type": "keyword",
|
||||
},
|
||||
|
|
|
@ -54,44 +54,32 @@ export const FINAL_SUMMARY_FILTER = {
|
|||
},
|
||||
},
|
||||
{
|
||||
bool: {
|
||||
should: [
|
||||
{
|
||||
bool: {
|
||||
should: [
|
||||
{
|
||||
match: {
|
||||
'summary.final_attempt': true,
|
||||
},
|
||||
},
|
||||
],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
bool: {
|
||||
must_not: {
|
||||
bool: {
|
||||
should: [
|
||||
{
|
||||
exists: {
|
||||
field: 'summary.final_attempt',
|
||||
},
|
||||
},
|
||||
],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
minimum_should_match: 1,
|
||||
term: {
|
||||
'summary.final_attempt': true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const getRangeFilter = ({ from, to }: { from: string; to: string }) => ({
|
||||
range: {
|
||||
'@timestamp': {
|
||||
gte: from,
|
||||
lte: to,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const getTimespanFilter = ({ from, to }: { from: string; to: string }) => ({
|
||||
range: {
|
||||
'monitor.timespan': {
|
||||
gte: from,
|
||||
lte: to,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const SUMMARY_FILTER = { exists: { field: 'summary' } };
|
||||
|
||||
export const getLocationFilter = ({
|
||||
|
|
|
@ -11,6 +11,7 @@ export const MONITOR_NAME = 'monitor.name';
|
|||
export const MONITOR_TYPE = 'monitor.type';
|
||||
export const URL_FULL = 'url.full';
|
||||
export const URL_PORT = 'url.port';
|
||||
export const OBSERVER_NAME = 'observer.name';
|
||||
export const OBSERVER_GEO_NAME = 'observer.geo.name';
|
||||
export const ERROR_MESSAGE = 'error.message';
|
||||
export const STATE_ID = 'monitor.state.id';
|
||||
|
|
|
@ -53,7 +53,7 @@ describe('Alert Actions factory', () => {
|
|||
dedupKey: expect.any(String),
|
||||
eventAction: 'resolve',
|
||||
summary:
|
||||
'The alert for "{{context.monitorName}}" from {{context.locationName}} is no longer active: {{context.recoveryReason}}. - Elastic Synthetics\n\nDetails:\n\n- Monitor name: {{context.monitorName}} \n- {{context.monitorUrlLabel}}: {{{context.monitorUrl}}} \n- Monitor type: {{context.monitorType}} \n- From: {{context.locationName}} \n- Last error received: {{{context.lastErrorMessage}}} \n{{{context.linkMessage}}}',
|
||||
'The alert for monitor "{{context.monitorName}}" from {{context.locationNames}} is no longer active: {{context.recoveryReason}}. - Elastic Synthetics\n\nDetails:\n\n- Monitor name: {{context.monitorName}} \n- {{context.monitorUrlLabel}}: {{{context.monitorUrl}}} \n- Monitor type: {{context.monitorType}} \n- From: {{context.locationNames}} \n- Last error received: {{{context.lastErrorMessage}}} \n{{{context.linkMessage}}}',
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -193,7 +193,7 @@ describe('Alert Actions factory', () => {
|
|||
dedupKey: expect.any(String),
|
||||
eventAction: 'resolve',
|
||||
summary:
|
||||
'The alert for "{{context.monitorName}}" from {{context.locationName}} is no longer active: {{context.recoveryReason}}. - Elastic Synthetics\n\nDetails:\n\n- Monitor name: {{context.monitorName}} \n- {{context.monitorUrlLabel}}: {{{context.monitorUrl}}} \n- Monitor type: {{context.monitorType}} \n- From: {{context.locationName}} \n- Last error received: {{{context.lastErrorMessage}}} \n{{{context.linkMessage}}}',
|
||||
'The alert for monitor "{{context.monitorName}}" from {{context.locationNames}} is no longer active: {{context.recoveryReason}}. - Elastic Synthetics\n\nDetails:\n\n- Monitor name: {{context.monitorName}} \n- {{context.monitorUrlLabel}}: {{{context.monitorUrl}}} \n- Monitor type: {{context.monitorType}} \n- From: {{context.locationNames}} \n- Last error received: {{{context.lastErrorMessage}}} \n{{{context.linkMessage}}}',
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -230,8 +230,7 @@ describe('Alert Actions factory', () => {
|
|||
dedupKey: 'always-downxpack.uptime.alerts.actionGroups.monitorStatus',
|
||||
eventAction: 'trigger',
|
||||
severity: 'error',
|
||||
summary:
|
||||
'The alert for "{{context.monitorName}}" from {{context.locationName}} is no longer active: {{context.recoveryReason}}.\n\nDetails:\n\nMonitor name: {{context.monitorName}}\n{{context.monitorUrlLabel}}: {{{context.monitorUrl}}}\nMonitor type: {{context.monitorType}}\nFrom: {{context.locationName}}\nLatest error received: {{{context.lastErrorMessage}}}\n{{{context.linkMessage}}}',
|
||||
summary: SyntheticsMonitorStatusTranslations.defaultRecoveryMessage,
|
||||
},
|
||||
id: 'f2a3b195-ed76-499a-805d-82d24d4eeba9',
|
||||
},
|
||||
|
@ -263,11 +262,9 @@ describe('Alert Actions factory', () => {
|
|||
path: '',
|
||||
text: '',
|
||||
},
|
||||
message:
|
||||
'The alert for "{{context.monitorName}}" from {{context.locationName}} is no longer active: {{context.recoveryReason}}. - Elastic Synthetics\n\nDetails:\n\n- Monitor name: {{context.monitorName}} \n- {{context.monitorUrlLabel}}: {{{context.monitorUrl}}} \n- Monitor type: {{context.monitorType}} \n- From: {{context.locationName}} \n- Last error received: {{{context.lastErrorMessage}}} \n{{{context.linkMessage}}}',
|
||||
message: SyntheticsMonitorStatusTranslations.defaultRecoveryMessage,
|
||||
messageHTML: null,
|
||||
subject:
|
||||
'"{{context.monitorName}}" ({{context.locationName}}) {{context.recoveryStatus}} - Elastic Synthetics',
|
||||
subject: SyntheticsMonitorStatusTranslations.defaultRecoverySubjectMessage,
|
||||
to: ['test@email.com'],
|
||||
},
|
||||
},
|
||||
|
@ -286,11 +283,9 @@ describe('Alert Actions factory', () => {
|
|||
path: '',
|
||||
text: '',
|
||||
},
|
||||
message:
|
||||
'"{{context.monitorName}}" is {{{context.status}}} from {{context.locationName}}. - Elastic Synthetics\n\nDetails:\n\n- Monitor name: {{context.monitorName}} \n- {{context.monitorUrlLabel}}: {{{context.monitorUrl}}} \n- Monitor type: {{context.monitorType}} \n- Checked at: {{context.checkedAt}} \n- From: {{context.locationName}} \n- Error received: {{{context.lastErrorMessage}}} \n{{{context.linkMessage}}}',
|
||||
message: SyntheticsMonitorStatusTranslations.defaultActionMessage,
|
||||
messageHTML: null,
|
||||
subject:
|
||||
'"{{context.monitorName}}" ({{context.locationName}}) is down - Elastic Synthetics',
|
||||
subject: SyntheticsMonitorStatusTranslations.defaultSubjectMessage,
|
||||
to: ['test@email.com'],
|
||||
},
|
||||
},
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* 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 { getConditionType } from './status_rule';
|
||||
|
||||
describe('Status Rule', () => {
|
||||
it('should return the correct condition type for empty', () => {
|
||||
const { useLatestChecks } = getConditionType({} as any);
|
||||
expect(useLatestChecks).toBe(true);
|
||||
});
|
||||
|
||||
it('should return the correct condition type check based', () => {
|
||||
const { useLatestChecks, useTimeWindow } = getConditionType({
|
||||
window: {
|
||||
numberOfChecks: 5,
|
||||
},
|
||||
});
|
||||
expect(useLatestChecks).toBe(true);
|
||||
expect(useTimeWindow).toBe(false);
|
||||
});
|
||||
|
||||
it('should return the correct condition type time based', () => {
|
||||
const { useTimeWindow, useLatestChecks } = getConditionType({
|
||||
window: {
|
||||
time: {
|
||||
unit: 'm',
|
||||
size: 5,
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(useTimeWindow).toBe(true);
|
||||
expect(useLatestChecks).toBe(false);
|
||||
});
|
||||
});
|
|
@ -6,7 +6,102 @@
|
|||
*/
|
||||
|
||||
import { schema, TypeOf } from '@kbn/config-schema';
|
||||
import { isEmpty } from 'lodash';
|
||||
|
||||
export const StatusRulePramsSchema = schema.object({});
|
||||
export const TimeWindowSchema = schema.object({
|
||||
unit: schema.oneOf(
|
||||
[schema.literal('s'), schema.literal('m'), schema.literal('h'), schema.literal('d')],
|
||||
{
|
||||
defaultValue: 'm',
|
||||
}
|
||||
),
|
||||
size: schema.number({
|
||||
defaultValue: 5,
|
||||
}),
|
||||
});
|
||||
|
||||
export const NumberOfChecksSchema = schema.object({
|
||||
numberOfChecks: schema.number({
|
||||
defaultValue: 5,
|
||||
min: 1,
|
||||
max: 100,
|
||||
}),
|
||||
});
|
||||
|
||||
export const StatusRuleConditionSchema = schema.object({
|
||||
groupBy: schema.maybe(
|
||||
schema.string({
|
||||
defaultValue: 'locationId',
|
||||
})
|
||||
),
|
||||
downThreshold: schema.maybe(
|
||||
schema.number({
|
||||
defaultValue: 3,
|
||||
})
|
||||
),
|
||||
locationsThreshold: schema.maybe(
|
||||
schema.number({
|
||||
defaultValue: 1,
|
||||
})
|
||||
),
|
||||
window: schema.oneOf([
|
||||
schema.object({
|
||||
time: TimeWindowSchema,
|
||||
}),
|
||||
NumberOfChecksSchema,
|
||||
]),
|
||||
includeRetests: schema.maybe(schema.boolean()),
|
||||
});
|
||||
|
||||
export const StatusRulePramsSchema = schema.object({
|
||||
condition: schema.maybe(StatusRuleConditionSchema),
|
||||
monitorIds: schema.maybe(schema.arrayOf(schema.string())),
|
||||
locations: schema.maybe(schema.arrayOf(schema.string())),
|
||||
tags: schema.maybe(schema.arrayOf(schema.string())),
|
||||
monitorTypes: schema.maybe(schema.arrayOf(schema.string())),
|
||||
projects: schema.maybe(schema.arrayOf(schema.string())),
|
||||
kqlQuery: schema.maybe(schema.string()),
|
||||
});
|
||||
|
||||
export type TimeWindow = TypeOf<typeof TimeWindowSchema>;
|
||||
export type StatusRuleParams = TypeOf<typeof StatusRulePramsSchema>;
|
||||
export type StatusRuleCondition = TypeOf<typeof StatusRuleConditionSchema>;
|
||||
|
||||
export const getConditionType = (condition?: StatusRuleCondition) => {
|
||||
let numberOfChecks = 1;
|
||||
let timeWindow: TimeWindow = { unit: 'm', size: 1 };
|
||||
if (isEmpty(condition) || !condition?.window) {
|
||||
return {
|
||||
isLocationBased: false,
|
||||
useTimeWindow: false,
|
||||
timeWindow,
|
||||
useLatestChecks: true,
|
||||
numberOfChecks,
|
||||
downThreshold: 1,
|
||||
locationsThreshold: 1,
|
||||
isDefaultRule: true,
|
||||
};
|
||||
}
|
||||
const useTimeWindow = condition.window && 'time' in condition.window;
|
||||
const useLatestChecks = condition.window && 'numberOfChecks' in condition.window;
|
||||
|
||||
if (useLatestChecks) {
|
||||
numberOfChecks =
|
||||
condition && 'numberOfChecks' in condition.window ? condition.window.numberOfChecks : 1;
|
||||
}
|
||||
|
||||
if (useTimeWindow) {
|
||||
timeWindow = condition.window.time;
|
||||
numberOfChecks = condition?.downThreshold ?? 1;
|
||||
}
|
||||
|
||||
return {
|
||||
useTimeWindow,
|
||||
timeWindow,
|
||||
useLatestChecks,
|
||||
numberOfChecks,
|
||||
locationsThreshold: condition?.locationsThreshold ?? 1,
|
||||
downThreshold: condition?.downThreshold ?? 1,
|
||||
isDefaultRule: isEmpty(condition),
|
||||
};
|
||||
};
|
||||
|
|
|
@ -12,7 +12,7 @@ export const SyntheticsMonitorStatusTranslations = {
|
|||
'xpack.synthetics.alerts.syntheticsMonitorStatus.defaultActionMessage',
|
||||
{
|
||||
// the extra spaces before `\n` are needed to properly convert this from markdown to an HTML email
|
||||
defaultMessage: `"{monitorName}" is {status} from {locationName}. - Elastic Synthetics\n\nDetails:\n\n- Monitor name: {monitorName} \n- {monitorUrlLabel}: {monitorUrl} \n- Monitor type: {monitorType} \n- Checked at: {checkedAt} \n- From: {locationName} \n- Error received: {lastErrorMessage} \n{linkMessage}`,
|
||||
defaultMessage: `Monitor "{monitorName}" is {status} from {locationNames}.{pendingLastRunAt} - Elastic Synthetics\n\nDetails:\n\n- Monitor name: {monitorName} \n- {monitorUrlLabel}: {monitorUrl} \n- Monitor type: {monitorType} \n- Checked at: {checkedAt} \n- From: {locationNames} \n- Reason: {reason} \n- Error received: {lastErrorMessage} \n{linkMessage}`,
|
||||
values: {
|
||||
monitorName: '{{context.monitorName}}',
|
||||
monitorType: '{{context.monitorType}}',
|
||||
|
@ -20,29 +20,32 @@ export const SyntheticsMonitorStatusTranslations = {
|
|||
monitorUrlLabel: '{{context.monitorUrlLabel}}',
|
||||
status: '{{{context.status}}}',
|
||||
lastErrorMessage: '{{{context.lastErrorMessage}}}',
|
||||
locationName: '{{context.locationName}}',
|
||||
locationNames: '{{context.locationNames}}',
|
||||
checkedAt: '{{context.checkedAt}}',
|
||||
linkMessage: '{{{context.linkMessage}}}',
|
||||
pendingLastRunAt: '{{{context.pendingLastRunAt}}}',
|
||||
reason: '{{{context.reason}}}',
|
||||
},
|
||||
}
|
||||
),
|
||||
defaultSubjectMessage: i18n.translate(
|
||||
'xpack.synthetics.alerts.syntheticsMonitorStatus.defaultSubjectMessage',
|
||||
{
|
||||
defaultMessage: '"{monitorName}" ({locationName}) is down - Elastic Synthetics',
|
||||
defaultMessage: 'Monitor "{monitorName}" ({locationNames}) is down - Elastic Synthetics',
|
||||
values: {
|
||||
monitorName: '{{context.monitorName}}',
|
||||
locationName: '{{context.locationName}}',
|
||||
locationNames: '{{context.locationNames}}',
|
||||
},
|
||||
}
|
||||
),
|
||||
defaultRecoverySubjectMessage: i18n.translate(
|
||||
'xpack.synthetics.alerts.syntheticsMonitorStatus.defaultRecoverySubjectMessage',
|
||||
{
|
||||
defaultMessage: '"{monitorName}" ({locationName}) {recoveryStatus} - Elastic Synthetics',
|
||||
defaultMessage:
|
||||
'Monitor "{monitorName}" ({locationNames}) {recoveryStatus} - Elastic Synthetics',
|
||||
values: {
|
||||
recoveryStatus: '{{context.recoveryStatus}}',
|
||||
locationName: '{{context.locationName}}',
|
||||
locationNames: '{{context.locationNames}}',
|
||||
monitorName: '{{context.monitorName}}',
|
||||
},
|
||||
}
|
||||
|
@ -52,13 +55,13 @@ export const SyntheticsMonitorStatusTranslations = {
|
|||
{
|
||||
// the extra spaces before `\n` are needed to properly convert this from markdown to an HTML email
|
||||
defaultMessage:
|
||||
'The alert for "{monitorName}" from {locationName} is no longer active: {recoveryReason}. - Elastic Synthetics\n\nDetails:\n\n- Monitor name: {monitorName} \n- {monitorUrlLabel}: {monitorUrl} \n- Monitor type: {monitorType} \n- From: {locationName} \n- Last error received: {lastErrorMessage} \n{linkMessage}',
|
||||
'The alert for monitor "{monitorName}" from {locationNames} is no longer active: {recoveryReason}. - Elastic Synthetics\n\nDetails:\n\n- Monitor name: {monitorName} \n- {monitorUrlLabel}: {monitorUrl} \n- Monitor type: {monitorType} \n- From: {locationNames} \n- Last error received: {lastErrorMessage} \n{linkMessage}',
|
||||
values: {
|
||||
monitorName: '{{context.monitorName}}',
|
||||
monitorUrlLabel: '{{context.monitorUrlLabel}}',
|
||||
monitorUrl: '{{{context.monitorUrl}}}',
|
||||
monitorType: '{{context.monitorType}}',
|
||||
locationName: '{{context.locationName}}',
|
||||
locationNames: '{{context.locationNames}}',
|
||||
recoveryReason: '{{context.recoveryReason}}',
|
||||
lastErrorMessage: '{{{context.lastErrorMessage}}}',
|
||||
linkMessage: '{{{context.linkMessage}}}',
|
||||
|
@ -75,7 +78,7 @@ export const SyntheticsMonitorStatusTranslations = {
|
|||
|
||||
export const TlsTranslations = {
|
||||
defaultActionMessage: i18n.translate('xpack.synthetics.rules.tls.defaultActionMessage', {
|
||||
defaultMessage: `TLS certificate {commonName} {status} - Elastic Synthetics\n\nDetails:\n\n- Summary: {summary}\n- Common name: {commonName}\n- Issuer: {issuer}\n- Monitor: {monitorName} \n- Monitor URL: {monitorUrl} \n- Monitor type: {monitorType} \n- From: {locationName}`,
|
||||
defaultMessage: `TLS certificate {commonName} {status} - Elastic Synthetics\n\nDetails:\n\n- Summary: {summary}\n- Common name: {commonName}\n- Issuer: {issuer}\n- Monitor: {monitorName} \n- Monitor URL: {monitorUrl} \n- Monitor type: {monitorType} \n- From: {locationNames}`,
|
||||
values: {
|
||||
commonName: '{{context.commonName}}',
|
||||
issuer: '{{context.issuer}}',
|
||||
|
@ -84,11 +87,11 @@ export const TlsTranslations = {
|
|||
monitorName: '{{context.monitorName}}',
|
||||
monitorUrl: '{{{context.monitorUrl}}}',
|
||||
monitorType: '{{context.monitorType}}',
|
||||
locationName: '{{context.locationName}}',
|
||||
locationNames: '{{context.locationNames}}',
|
||||
},
|
||||
}),
|
||||
defaultRecoveryMessage: i18n.translate('xpack.synthetics.rules.tls.defaultRecoveryMessage', {
|
||||
defaultMessage: `TLS alert for monitor "{monitorName}" has recovered - Elastic Synthetics\n\nDetails:\n\n- Summary: {summary}\n- New status : {newStatus}\n- Previous status: {previousStatus}\n- Monitor: {monitorName} \n- URL: {monitorUrl} \n- Monitor type: {monitorType} \n- From: {locationName}`,
|
||||
defaultMessage: `TLS alert for monitor "{monitorName}" has recovered - Elastic Synthetics\n\nDetails:\n\n- Summary: {summary}\n- New status : {newStatus}\n- Previous status: {previousStatus}\n- Monitor: {monitorName} \n- URL: {monitorUrl} \n- Monitor type: {monitorType} \n- From: {locationNames}`,
|
||||
values: {
|
||||
summary: '{{context.summary}}',
|
||||
previousStatus: '{{context.previousStatus}}',
|
||||
|
@ -96,7 +99,7 @@ export const TlsTranslations = {
|
|||
monitorName: '{{context.monitorName}}',
|
||||
monitorUrl: '{{{context.monitorUrl}}}',
|
||||
monitorType: '{{context.monitorType}}',
|
||||
locationName: '{{context.locationName}}',
|
||||
locationNames: '{{context.locationNames}}',
|
||||
},
|
||||
}),
|
||||
name: i18n.translate('xpack.synthetics.rules.tls.clientName', {
|
||||
|
|
|
@ -17,8 +17,14 @@ export const syntheticsRuleFieldMap: FieldMap = {
|
|||
type: 'keyword',
|
||||
required: false,
|
||||
},
|
||||
'observer.name': {
|
||||
type: 'keyword',
|
||||
array: true,
|
||||
required: false,
|
||||
},
|
||||
'observer.geo.name': {
|
||||
type: 'keyword',
|
||||
array: true,
|
||||
required: false,
|
||||
},
|
||||
// monitor status alert fields
|
||||
|
@ -43,6 +49,10 @@ export const syntheticsRuleFieldMap: FieldMap = {
|
|||
array: true,
|
||||
required: false,
|
||||
},
|
||||
'monitor.state.id': {
|
||||
type: 'keyword',
|
||||
required: false,
|
||||
},
|
||||
configId: {
|
||||
type: 'keyword',
|
||||
required: false,
|
||||
|
@ -53,10 +63,12 @@ export const syntheticsRuleFieldMap: FieldMap = {
|
|||
},
|
||||
'location.id': {
|
||||
type: 'keyword',
|
||||
array: true,
|
||||
required: false,
|
||||
},
|
||||
'location.name': {
|
||||
type: 'keyword',
|
||||
array: true,
|
||||
required: false,
|
||||
},
|
||||
// tls alert fields
|
||||
|
|
|
@ -25,14 +25,7 @@ export const SyntheticsCommonStateCodec = t.intersection([
|
|||
|
||||
export type SyntheticsCommonState = t.TypeOf<typeof SyntheticsCommonStateCodec>;
|
||||
|
||||
export const SyntheticsMonitorStatusAlertStateCodec = t.type({
|
||||
configId: t.string,
|
||||
locationId: t.string,
|
||||
locationName: t.string,
|
||||
errorStartedAt: t.string,
|
||||
lastErrorMessage: t.string,
|
||||
stateId: t.string,
|
||||
});
|
||||
export const SyntheticsMonitorStatusAlertStateCodec = t.type({});
|
||||
|
||||
export type SyntheticsMonitorStatusAlertState = t.TypeOf<
|
||||
typeof SyntheticsMonitorStatusAlertStateCodec
|
||||
|
@ -45,6 +38,10 @@ export const AlertStatusMetaDataCodec = t.interface({
|
|||
locationId: t.string,
|
||||
timestamp: t.string,
|
||||
ping: OverviewPingCodec,
|
||||
checks: t.type({
|
||||
downWithinXChecks: t.number,
|
||||
down: t.number,
|
||||
}),
|
||||
});
|
||||
|
||||
export const StaleAlertStatusMetaDataCodec = t.intersection([
|
||||
|
@ -69,9 +66,6 @@ export const AlertPendingStatusMetaDataCodec = t.intersection([
|
|||
]);
|
||||
|
||||
export const AlertStatusCodec = t.interface({
|
||||
up: t.number,
|
||||
down: t.number,
|
||||
pending: t.number,
|
||||
upConfigs: t.record(t.string, AlertStatusMetaDataCodec),
|
||||
downConfigs: t.record(t.string, AlertStatusMetaDataCodec),
|
||||
pendingConfigs: t.record(t.string, AlertPendingStatusMetaDataCodec),
|
||||
|
@ -79,7 +73,7 @@ export const AlertStatusCodec = t.interface({
|
|||
staleDownConfigs: t.record(t.string, StaleAlertStatusMetaDataCodec),
|
||||
});
|
||||
|
||||
export type AlertPendingStatusMetaData = t.TypeOf<typeof AlertPendingStatusMetaDataCodec>;
|
||||
export type StaleDownConfig = t.TypeOf<typeof StaleAlertStatusMetaDataCodec>;
|
||||
export type AlertStatusMetaData = t.TypeOf<typeof AlertStatusMetaDataCodec>;
|
||||
export type AlertOverviewStatus = t.TypeOf<typeof AlertStatusCodec>;
|
||||
export type AlertStatusConfigs = Record<string, AlertStatusMetaData>;
|
||||
|
|
|
@ -96,6 +96,10 @@ export const MonitorType = t.intersection([
|
|||
status: t.string,
|
||||
type: t.string,
|
||||
check_group: t.string,
|
||||
timespan: t.type({
|
||||
gte: t.string,
|
||||
lt: t.string,
|
||||
}),
|
||||
}),
|
||||
t.partial({
|
||||
duration: t.type({
|
||||
|
@ -103,10 +107,7 @@ export const MonitorType = t.intersection([
|
|||
}),
|
||||
ip: t.string,
|
||||
name: t.string,
|
||||
timespan: t.type({
|
||||
gte: t.string,
|
||||
lt: t.string,
|
||||
}),
|
||||
|
||||
fleet_managed: t.boolean,
|
||||
project: t.type({
|
||||
id: t.string,
|
||||
|
|
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
* 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 { journey, step, before, after, expect } from '@elastic/synthetics';
|
||||
import { RetryService } from '@kbn/ftr-common-functional-services';
|
||||
import { syntheticsAppPageProvider } from '../../page_objects/synthetics_app';
|
||||
import { SyntheticsServices } from '../services/synthetics_services';
|
||||
|
||||
journey(`CustomStatusAlert`, async ({ page, params }) => {
|
||||
const syntheticsApp = syntheticsAppPageProvider({ page, kibanaUrl: params.kibanaUrl, params });
|
||||
|
||||
const services = new SyntheticsServices(params);
|
||||
const getService = params.getService;
|
||||
const retry: RetryService = getService('retry');
|
||||
|
||||
const firstCheckTime = new Date(Date.now()).toISOString();
|
||||
|
||||
let configId: string;
|
||||
|
||||
before(async () => {
|
||||
await services.cleaUp();
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await services.cleaUp();
|
||||
});
|
||||
|
||||
step('Go to monitors page', async () => {
|
||||
await syntheticsApp.navigateToOverview(true, 15);
|
||||
});
|
||||
|
||||
step('add test monitor', async () => {
|
||||
configId = await services.addTestMonitor(
|
||||
'Test Monitor',
|
||||
{
|
||||
type: 'http',
|
||||
urls: 'https://www.google.com',
|
||||
locations: ['us_central'],
|
||||
},
|
||||
configId
|
||||
);
|
||||
await services.addTestSummaryDocument({ timestamp: firstCheckTime, configId });
|
||||
});
|
||||
|
||||
step('should create status rule', async () => {
|
||||
await page.getByTestId('syntheticsRefreshButtonButton').click();
|
||||
await page.getByTestId('syntheticsAlertsRulesButton').click();
|
||||
await page.getByTestId('manageStatusRuleName').click();
|
||||
await page.getByTestId('createNewStatusRule').click();
|
||||
|
||||
await page.getByTestId('ruleNameInput').fill('Synthetics status rule');
|
||||
await page.getByTestId('saveRuleButton').click();
|
||||
await page.getByTestId('confirmModalConfirmButton').click();
|
||||
|
||||
await page.waitForSelector(`text='Created rule "Synthetics status rule"'`);
|
||||
});
|
||||
|
||||
step('verify rule creation', async () => {
|
||||
await retry.try(async () => {
|
||||
const rules = await services.getRules();
|
||||
expect(rules.length).toBe(3);
|
||||
expect(rules[2].params).toStrictEqual({
|
||||
condition: {
|
||||
downThreshold: 3,
|
||||
locationsThreshold: 1,
|
||||
groupBy: 'locationId',
|
||||
window: {
|
||||
numberOfChecks: 5,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -29,42 +29,50 @@ journey(`DefaultStatusAlert`, async ({ page, params }) => {
|
|||
|
||||
before(async () => {
|
||||
await services.cleaUp();
|
||||
await services.enableMonitorManagedViaApi();
|
||||
configId = await services.addTestMonitor('Test Monitor', {
|
||||
type: 'http',
|
||||
urls: 'https://www.google.com',
|
||||
custom_heartbeat_id: 'b9d9e146-746f-427f-bbf5-6e786b5b4e73',
|
||||
locations: [
|
||||
{ id: 'us_central', label: 'North America - US Central', isServiceManaged: true },
|
||||
],
|
||||
});
|
||||
await services.addTestSummaryDocument({ timestamp: firstCheckTime, configId });
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await services.cleaUp();
|
||||
});
|
||||
|
||||
step('setup monitor', async () => {
|
||||
const connectorId = await services.setupTestConnector();
|
||||
await services.setupSettings(connectorId.id);
|
||||
|
||||
configId = await services.addTestMonitor('Test Monitor', {
|
||||
type: 'http',
|
||||
urls: 'https://www.google.com',
|
||||
locations: [
|
||||
{ id: 'us_central', label: 'North America - US Central', isServiceManaged: true },
|
||||
],
|
||||
});
|
||||
await services.addTestSummaryDocument({
|
||||
timestamp: firstCheckTime,
|
||||
configId,
|
||||
});
|
||||
});
|
||||
|
||||
step('Go to monitors page', async () => {
|
||||
await syntheticsApp.navigateToOverview(true, 15);
|
||||
});
|
||||
|
||||
step('should create default status alert', async () => {
|
||||
await page.click(byTestId('xpack.synthetics.alertsPopover.toggleButton'));
|
||||
await page.isDisabled(byTestId('xpack.synthetics.toggleAlertFlyout'));
|
||||
await page.click(byTestId('xpack.synthetics.toggleAlertFlyout'));
|
||||
await page.getByTestId('syntheticsAlertsRulesButton').click();
|
||||
await page.getByTestId('manageStatusRuleName').click();
|
||||
await page.isDisabled(byTestId('editDefaultStatusRule'));
|
||||
await page.getByTestId('editDefaultStatusRule').click();
|
||||
|
||||
await page.waitForSelector('text=Monitor status rule');
|
||||
expect(await page.locator(`[data-test-subj="intervalFormRow"]`).count()).toEqual(0);
|
||||
await page.getByTestId('intervalInputUnit').selectOption('second');
|
||||
await page.getByTestId('intervalInput').fill('20');
|
||||
await page.click(byTestId('saveEditedRuleButton'));
|
||||
await page.waitForSelector("text=Updated 'Synthetics status internal rule'");
|
||||
});
|
||||
|
||||
step('Monitor is as up in overview page', async () => {
|
||||
await retry.tryForTime(90 * 1000, async () => {
|
||||
const totalDown = await page.textContent(
|
||||
byTestId('xpack.uptime.synthetics.overview.status.up')
|
||||
);
|
||||
expect(totalDown).toBe('1Up');
|
||||
const totalUp = await page.textContent(byTestId('syntheticsOverviewUp'));
|
||||
expect(totalUp).toBe('1Up');
|
||||
});
|
||||
|
||||
await page.hover('text=Test Monitor');
|
||||
|
@ -74,6 +82,8 @@ journey(`DefaultStatusAlert`, async ({ page, params }) => {
|
|||
step('Disable default alert for monitor', async () => {
|
||||
await page.click('text=Disable status alert');
|
||||
await page.waitForSelector(`text=Alerts are now disabled for the monitor "Test Monitor".`);
|
||||
await page.getByTestId('Test Monitor-us_central-metric-item').hover();
|
||||
await page.click('[aria-label="Open actions menu"]');
|
||||
await page.click('text=Enable status alert');
|
||||
});
|
||||
|
||||
|
@ -91,9 +101,7 @@ journey(`DefaultStatusAlert`, async ({ page, params }) => {
|
|||
|
||||
await page.waitForTimeout(5 * 1000);
|
||||
|
||||
const totalDown = await page.textContent(
|
||||
byTestId('xpack.uptime.synthetics.overview.status.down')
|
||||
);
|
||||
const totalDown = await page.textContent(byTestId('syntheticsOverviewDown'));
|
||||
expect(totalDown).toBe('1Down');
|
||||
});
|
||||
|
||||
|
@ -103,14 +111,17 @@ journey(`DefaultStatusAlert`, async ({ page, params }) => {
|
|||
const reasonMessage = getReasonMessage({
|
||||
name: 'Test Monitor',
|
||||
location: 'North America - US Central',
|
||||
timestamp: downCheckTime,
|
||||
status: 'down',
|
||||
checks: {
|
||||
downWithinXChecks: 1,
|
||||
down: 1,
|
||||
},
|
||||
});
|
||||
|
||||
await retry.tryForTime(3 * 60 * 1000, async () => {
|
||||
await page.click(byTestId('querySubmitButton'));
|
||||
|
||||
const alerts = await page.waitForSelector(`text=1 Alert`, { timeout: 20 * 1000 });
|
||||
const alerts = await page.waitForSelector(`text=1 Alert`, { timeout: 5 * 1000 });
|
||||
expect(await alerts.isVisible()).toBe(true);
|
||||
|
||||
const text = await page.textContent(`${byTestId('dataGridRowCell')} .euiLink`);
|
||||
|
@ -164,8 +175,11 @@ journey(`DefaultStatusAlert`, async ({ page, params }) => {
|
|||
const reasonMessage = getReasonMessage({
|
||||
name,
|
||||
location: 'North America - US Central',
|
||||
timestamp: downCheckTime,
|
||||
status: 'down',
|
||||
checks: {
|
||||
downWithinXChecks: 1,
|
||||
down: 1,
|
||||
},
|
||||
});
|
||||
|
||||
await retry.tryForTime(3 * 60 * 1000, async () => {
|
||||
|
@ -194,6 +208,5 @@ journey(`DefaultStatusAlert`, async ({ page, params }) => {
|
|||
await page.waitForTimeout(10 * 1000);
|
||||
|
||||
await page.click('[aria-label="View in app"]');
|
||||
await page.click(byTestId('breadcrumb /app/synthetics/monitors'));
|
||||
});
|
||||
});
|
||||
|
|
|
@ -18,7 +18,8 @@ export * from './private_locations.journey';
|
|||
export * from './alerting_default.journey';
|
||||
export * from './global_parameters.journey';
|
||||
export * from './detail_flyout';
|
||||
// export * from './alert_rules/default_status_alert.journey';
|
||||
export * from './alert_rules/default_status_alert.journey';
|
||||
export * from './alert_rules/custom_status_alert.journey';
|
||||
export * from './test_now_mode.journey';
|
||||
export * from './monitor_details_page/monitor_summary.journey';
|
||||
export * from './test_run_details.journey';
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { DocOverrides } from './sample_docs';
|
||||
import { DocOverrides } from '@kbn/observability-synthetics-test-data/src/make_summaries';
|
||||
|
||||
export const getGeoData = (locationName?: string, locationId?: string) => ({
|
||||
observer: {
|
||||
|
@ -22,10 +22,10 @@ export const journeySummary = ({
|
|||
timestamp,
|
||||
monitorId,
|
||||
testRunId,
|
||||
locationName,
|
||||
location,
|
||||
}: DocOverrides = {}) => {
|
||||
return {
|
||||
...getGeoData(locationName),
|
||||
...getGeoData(location?.label),
|
||||
summary: {
|
||||
up: 1,
|
||||
down: 0,
|
||||
|
@ -105,9 +105,9 @@ export const journeyStart = ({
|
|||
timestamp,
|
||||
monitorId,
|
||||
testRunId,
|
||||
locationName,
|
||||
location,
|
||||
}: DocOverrides = {}) => ({
|
||||
...getGeoData(locationName),
|
||||
...getGeoData(location?.label),
|
||||
test_run_id: testRunId ?? '07e339f4-4d56-4cdb-b314-96faacaee645',
|
||||
agent: {
|
||||
name: 'job-88fe737c53c39aea-lp69x',
|
||||
|
@ -167,14 +167,8 @@ export const journeyStart = ({
|
|||
},
|
||||
});
|
||||
|
||||
export const step1 = ({
|
||||
name,
|
||||
timestamp,
|
||||
monitorId,
|
||||
testRunId,
|
||||
locationName,
|
||||
}: DocOverrides = {}) => ({
|
||||
...getGeoData(locationName),
|
||||
export const step1 = ({ name, timestamp, monitorId, testRunId, location }: DocOverrides = {}) => ({
|
||||
...getGeoData(location?.label),
|
||||
test_run_id: testRunId ?? 'c16b1614-7f48-4791-8f46-9ccf3a896e20',
|
||||
agent: {
|
||||
name: 'job-76905d93798e6fff-z6nsb',
|
||||
|
@ -249,14 +243,8 @@ export const step1 = ({
|
|||
},
|
||||
});
|
||||
|
||||
export const step2 = ({
|
||||
name,
|
||||
timestamp,
|
||||
monitorId,
|
||||
testRunId,
|
||||
locationName,
|
||||
}: DocOverrides = {}) => ({
|
||||
...getGeoData(locationName),
|
||||
export const step2 = ({ name, timestamp, monitorId, testRunId, location }: DocOverrides = {}) => ({
|
||||
...getGeoData(location?.label),
|
||||
test_run_id: testRunId ?? 'c16b1614-7f48-4791-8f46-9ccf3a896e20',
|
||||
agent: {
|
||||
name: 'job-76905d93798e6fff-z6nsb',
|
||||
|
|
|
@ -9,10 +9,10 @@ import axios from 'axios';
|
|||
import type { Client } from '@elastic/elasticsearch';
|
||||
import { KbnClient } from '@kbn/test';
|
||||
import pMap from 'p-map';
|
||||
import { makeDownSummary, makeUpSummary } from '@kbn/observability-synthetics-test-data';
|
||||
import { SyntheticsMonitor } from '@kbn/synthetics-plugin/common/runtime_types';
|
||||
import { SYNTHETICS_API_URLS } from '@kbn/synthetics-plugin/common/constants';
|
||||
import { journeyStart, journeySummary, step1, step2 } from './data/browser_docs';
|
||||
import { firstDownHit, getUpHit } from './data/sample_docs';
|
||||
|
||||
export class SyntheticsServices {
|
||||
kibanaUrl: string;
|
||||
|
@ -113,22 +113,6 @@ export class SyntheticsServices {
|
|||
);
|
||||
}
|
||||
|
||||
async enableDefaultAlertingViaApi() {
|
||||
try {
|
||||
await axios.post(
|
||||
this.kibanaUrl + SYNTHETICS_API_URLS.ENABLE_DEFAULT_ALERTING,
|
||||
{ isDisabled: false },
|
||||
{
|
||||
auth: { username: 'elastic', password: 'changeme' },
|
||||
headers: { 'kbn-xsrf': 'true', 'x-elastic-internal-origin': 'synthetics-e2e' },
|
||||
}
|
||||
);
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
async addTestSummaryDocument({
|
||||
docType = 'summaryUp',
|
||||
timestamp = new Date(Date.now()).toISOString(),
|
||||
|
@ -157,14 +141,22 @@ export class SyntheticsServices {
|
|||
|
||||
let index = 'synthetics-http-default';
|
||||
|
||||
const commonData = { timestamp, monitorId, name, testRunId, locationName, configId };
|
||||
const commonData = {
|
||||
timestamp,
|
||||
name,
|
||||
testRunId,
|
||||
location: {
|
||||
id: 'us_central',
|
||||
label: locationName ?? 'North America - US Central',
|
||||
},
|
||||
configId,
|
||||
monitorId: monitorId ?? configId,
|
||||
};
|
||||
|
||||
switch (docType) {
|
||||
case 'stepEnd':
|
||||
index = 'synthetics-browser-default';
|
||||
|
||||
const stepDoc = stepIndex === 1 ? step1(commonData) : step2(commonData);
|
||||
|
||||
document = { ...stepDoc, ...document };
|
||||
break;
|
||||
case 'journeyEnd':
|
||||
|
@ -177,19 +169,19 @@ export class SyntheticsServices {
|
|||
break;
|
||||
case 'summaryDown':
|
||||
document = {
|
||||
...firstDownHit(commonData),
|
||||
...makeDownSummary(commonData),
|
||||
...document,
|
||||
};
|
||||
break;
|
||||
case 'summaryUp':
|
||||
document = {
|
||||
...getUpHit(commonData),
|
||||
...makeUpSummary(commonData),
|
||||
...document,
|
||||
};
|
||||
break;
|
||||
default:
|
||||
document = {
|
||||
...getUpHit(commonData),
|
||||
...makeUpSummary(commonData),
|
||||
...document,
|
||||
};
|
||||
}
|
||||
|
@ -228,4 +220,43 @@ export class SyntheticsServices {
|
|||
console.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
async getRules() {
|
||||
const response = await axios.get(this.kibanaUrl + '/internal/alerting/rules/_find', {
|
||||
auth: { username: 'elastic', password: 'changeme' },
|
||||
headers: { 'kbn-xsrf': 'true' },
|
||||
});
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
async setupTestConnector() {
|
||||
const indexConnector = {
|
||||
name: 'test index',
|
||||
config: { index: 'test-index' },
|
||||
secrets: {},
|
||||
connector_type_id: '.index',
|
||||
};
|
||||
const connector = await this.requester.request({
|
||||
path: `/api/actions/connector`,
|
||||
method: 'POST',
|
||||
body: indexConnector,
|
||||
});
|
||||
return connector.data as any;
|
||||
}
|
||||
|
||||
async setupSettings(connectorId?: string) {
|
||||
const settings = {
|
||||
certExpirationThreshold: 30,
|
||||
certAgeThreshold: 730,
|
||||
defaultConnectors: [connectorId],
|
||||
defaultEmail: { to: [], cc: [], bcc: [] },
|
||||
defaultStatusRuleEnabled: true,
|
||||
};
|
||||
const connector = await this.requester.request({
|
||||
path: `/api/synthetics/settings`,
|
||||
method: 'PUT',
|
||||
body: settings,
|
||||
});
|
||||
return connector.data;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,8 +14,9 @@
|
|||
"@kbn/ftr-common-functional-services",
|
||||
"@kbn/apm-plugin",
|
||||
"@kbn/es-archiver",
|
||||
"@kbn/repo-info",
|
||||
"@kbn/synthetics-plugin",
|
||||
"@kbn/repo-info",
|
||||
"@kbn/observability-synthetics-test-data",
|
||||
"@kbn/ftr-common-functional-ui-services"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* 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 React, { useCallback } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiFieldNumber, EuiPopoverTitle } from '@elastic/eui';
|
||||
import { StatusRuleCondition } from '../../../../../../common/rules/status_rule';
|
||||
import { PopoverExpression } from './popover_expression';
|
||||
import { StatusRuleParamsProps } from '../status_rule_ui';
|
||||
|
||||
interface Props {
|
||||
ruleParams: StatusRuleParamsProps['ruleParams'];
|
||||
setRuleParams: StatusRuleParamsProps['setRuleParams'];
|
||||
}
|
||||
|
||||
export const LocationsValueExpression = ({ ruleParams, setRuleParams }: Props) => {
|
||||
const { condition } = ruleParams;
|
||||
|
||||
const onLocationCountChange = useCallback(
|
||||
(value: number) => {
|
||||
setRuleParams('condition', {
|
||||
...ruleParams.condition,
|
||||
locationsThreshold: value,
|
||||
groupBy: value === 1 ? ruleParams.condition?.groupBy : 'none',
|
||||
} as StatusRuleCondition);
|
||||
},
|
||||
[ruleParams.condition, setRuleParams]
|
||||
);
|
||||
|
||||
const locationsThreshold =
|
||||
condition && 'locationsThreshold' in condition ? condition.locationsThreshold ?? 1 : 1;
|
||||
return (
|
||||
<PopoverExpression
|
||||
value={i18n.translate('xpack.synthetics.windowValueExpression.percentLabel', {
|
||||
defaultMessage:
|
||||
'{numberOfLocations} {numberOfLocations, plural, one {location} other {locations}}',
|
||||
values: { numberOfLocations: locationsThreshold },
|
||||
})}
|
||||
>
|
||||
<EuiPopoverTitle>
|
||||
{i18n.translate('xpack.synthetics.windowValueExpression.numberOfLocPopoverTitleLabel', {
|
||||
defaultMessage: 'Number of locations',
|
||||
})}
|
||||
</EuiPopoverTitle>
|
||||
<EuiFieldNumber
|
||||
data-test-subj="syntheticsWindowValueExpressionFieldNumber"
|
||||
min={1}
|
||||
max={100}
|
||||
compressed
|
||||
value={locationsThreshold}
|
||||
onChange={(evt) => onLocationCountChange(Number(evt.target.value))}
|
||||
/>
|
||||
</PopoverExpression>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,107 @@
|
|||
/*
|
||||
* 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 { ForLastExpression, TIME_UNITS } from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import React, { useCallback } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiFieldNumber, EuiPopoverTitle } from '@elastic/eui';
|
||||
import { PopoverExpression } from './popover_expression';
|
||||
import { getConditionType, TimeWindow } from '../../../../../../common/rules/status_rule';
|
||||
import { StatusRuleParamsProps } from '../status_rule_ui';
|
||||
|
||||
interface Props {
|
||||
ruleParams: StatusRuleParamsProps['ruleParams'];
|
||||
setRuleParams: StatusRuleParamsProps['setRuleParams'];
|
||||
}
|
||||
|
||||
export const WindowValueExpression = ({ ruleParams, setRuleParams }: Props) => {
|
||||
const { condition } = ruleParams;
|
||||
const timeWindow =
|
||||
condition && 'time' in condition.window
|
||||
? condition.window.time ?? {
|
||||
size: 5,
|
||||
unit: 'm',
|
||||
}
|
||||
: null;
|
||||
|
||||
const timeWindowSize = timeWindow?.size ?? 5;
|
||||
const timeWindowUnit = timeWindow?.unit ?? 'm';
|
||||
|
||||
const numberOfChecks =
|
||||
condition && 'numberOfChecks' in condition.window ? condition.window.numberOfChecks : null;
|
||||
|
||||
const { useTimeWindow } = getConditionType(ruleParams.condition);
|
||||
|
||||
const onTimeWindowChange = useCallback(
|
||||
(value: TimeWindow) => {
|
||||
setRuleParams('condition', {
|
||||
...ruleParams.condition,
|
||||
window: {
|
||||
...ruleParams.condition?.window,
|
||||
time: value,
|
||||
},
|
||||
});
|
||||
},
|
||||
[ruleParams.condition, setRuleParams]
|
||||
);
|
||||
|
||||
const onNumberOfChecksChange = useCallback(
|
||||
(value: number) => {
|
||||
setRuleParams('condition', {
|
||||
...ruleParams.condition,
|
||||
window: {
|
||||
...ruleParams.condition?.window,
|
||||
numberOfChecks: value,
|
||||
},
|
||||
});
|
||||
},
|
||||
[ruleParams.condition, setRuleParams]
|
||||
);
|
||||
|
||||
if (!useTimeWindow) {
|
||||
return (
|
||||
<PopoverExpression
|
||||
value={
|
||||
(numberOfChecks ?? 5) +
|
||||
i18n.translate('xpack.synthetics.windowValueExpression.checksLabel', {
|
||||
defaultMessage: ' checks',
|
||||
})
|
||||
}
|
||||
>
|
||||
<EuiPopoverTitle>
|
||||
{i18n.translate(
|
||||
'xpack.synthetics.windowValueExpression.numberOfChecksPopoverTitleLabel',
|
||||
{ defaultMessage: 'Number of checks' }
|
||||
)}
|
||||
</EuiPopoverTitle>
|
||||
<EuiFieldNumber
|
||||
data-test-subj="syntheticsWindowValueExpressionFieldNumber"
|
||||
min={1}
|
||||
max={10}
|
||||
compressed
|
||||
value={numberOfChecks ?? 5}
|
||||
onChange={(evt) => onNumberOfChecksChange(Number(evt.target.value))}
|
||||
/>
|
||||
</PopoverExpression>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ForLastExpression
|
||||
timeWindowSize={timeWindowSize}
|
||||
timeWindowUnit={timeWindowUnit}
|
||||
onChangeWindowSize={(val) => {
|
||||
onTimeWindowChange({ size: val ?? 5, unit: timeWindowUnit });
|
||||
}}
|
||||
onChangeWindowUnit={(val) => {
|
||||
onTimeWindowChange({ size: timeWindowSize, unit: (val ?? 'm') as TIME_UNITS });
|
||||
}}
|
||||
errors={{}}
|
||||
description=""
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,124 @@
|
|||
/*
|
||||
* 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 { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { useFetchSyntheticsSuggestions } from '../hooks/use_fetch_synthetics_suggestions';
|
||||
import { StatusRuleParamsProps } from '../status_rule_ui';
|
||||
import { LocationsField, MonitorField, MonitorTypeField, ProjectsField, TagsField } from './fields';
|
||||
|
||||
type FieldKeys = 'monitorIds' | 'projects' | 'tags' | 'locations' | 'monitorTypes';
|
||||
|
||||
interface Props {
|
||||
ruleParams: StatusRuleParamsProps['ruleParams'];
|
||||
setRuleParams: StatusRuleParamsProps['setRuleParams'];
|
||||
}
|
||||
|
||||
export const FieldFilters = ({ ruleParams, setRuleParams }: Props) => {
|
||||
const [search, setSearch] = useState<string>('');
|
||||
const [selectedField, setSelectedField] = useState<string>();
|
||||
|
||||
const {
|
||||
suggestions = [],
|
||||
isLoading,
|
||||
allSuggestions,
|
||||
} = useFetchSyntheticsSuggestions({
|
||||
search,
|
||||
fieldName: selectedField,
|
||||
});
|
||||
|
||||
const onFieldChange = useCallback(
|
||||
(key: FieldKeys, value?: string[]) => {
|
||||
setRuleParams(key, value);
|
||||
},
|
||||
[setRuleParams]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<MonitorField
|
||||
onChange={(val) => {
|
||||
onFieldChange('monitorIds', val);
|
||||
}}
|
||||
value={ruleParams.monitorIds}
|
||||
setSearch={setSearch}
|
||||
suggestions={suggestions}
|
||||
allSuggestions={allSuggestions}
|
||||
isLoading={isLoading}
|
||||
setSelectedField={setSelectedField}
|
||||
selectedField={selectedField}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<MonitorTypeField
|
||||
onChange={(val) => {
|
||||
onFieldChange('monitorTypes', val);
|
||||
}}
|
||||
value={ruleParams.monitorTypes}
|
||||
setSearch={setSearch}
|
||||
suggestions={suggestions}
|
||||
allSuggestions={allSuggestions}
|
||||
isLoading={isLoading}
|
||||
setSelectedField={setSelectedField}
|
||||
selectedField={selectedField}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<TagsField
|
||||
onChange={(val) => {
|
||||
onFieldChange('tags', val);
|
||||
}}
|
||||
value={ruleParams.tags}
|
||||
setSearch={setSearch}
|
||||
suggestions={suggestions}
|
||||
allSuggestions={allSuggestions}
|
||||
isLoading={isLoading}
|
||||
setSelectedField={setSelectedField}
|
||||
selectedField={selectedField}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<ProjectsField
|
||||
onChange={(val) => {
|
||||
onFieldChange('projects', val);
|
||||
}}
|
||||
value={ruleParams.projects}
|
||||
setSearch={setSearch}
|
||||
suggestions={suggestions}
|
||||
allSuggestions={allSuggestions}
|
||||
isLoading={isLoading}
|
||||
setSelectedField={setSelectedField}
|
||||
selectedField={selectedField}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<LocationsField
|
||||
onChange={(val) => {
|
||||
onFieldChange('locations', val);
|
||||
}}
|
||||
value={ruleParams.locations}
|
||||
setSearch={setSearch}
|
||||
suggestions={suggestions}
|
||||
allSuggestions={allSuggestions}
|
||||
isLoading={isLoading}
|
||||
setSelectedField={setSelectedField}
|
||||
selectedField={selectedField}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="m" />
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* 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 React, { ReactNode } from 'react';
|
||||
import { EuiExpression, EuiPopover, EuiExpressionProps } from '@elastic/eui';
|
||||
import { ALL_VALUE } from '@kbn/slo-schema';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { allOptionText } from './fields';
|
||||
import { Suggestion } from '../hooks/use_fetch_synthetics_suggestions';
|
||||
|
||||
interface Props {
|
||||
title?: ReactNode;
|
||||
value?: string[];
|
||||
children?: ReactNode;
|
||||
color?: EuiExpressionProps['color'];
|
||||
selectedField?: string;
|
||||
fieldName: string;
|
||||
setSelectedField: (value?: string) => void;
|
||||
allSuggestions?: Record<string, Suggestion[]>;
|
||||
}
|
||||
|
||||
export function FieldPopoverExpression({
|
||||
title,
|
||||
value,
|
||||
children,
|
||||
color,
|
||||
selectedField,
|
||||
fieldName,
|
||||
setSelectedField,
|
||||
allSuggestions,
|
||||
}: Props) {
|
||||
const isPopoverOpen = selectedField === fieldName;
|
||||
|
||||
const suggestions = allSuggestions?.[fieldName];
|
||||
|
||||
let label =
|
||||
!isEmpty(value) && value
|
||||
? suggestions
|
||||
?.filter((suggestion) => value.includes(suggestion.value))
|
||||
?.map((suggestion) => suggestion.label)
|
||||
.join(', ')
|
||||
: allOptionText;
|
||||
|
||||
if (value?.includes(ALL_VALUE)) {
|
||||
label = allOptionText;
|
||||
}
|
||||
|
||||
const closePopover = () => setSelectedField(selectedField === fieldName ? undefined : fieldName);
|
||||
return (
|
||||
<span>
|
||||
<EuiPopover
|
||||
isOpen={isPopoverOpen}
|
||||
anchorPosition="downLeft"
|
||||
closePopover={closePopover}
|
||||
button={
|
||||
<EuiExpression
|
||||
description={title}
|
||||
value={label}
|
||||
isActive={Boolean(selectedField)}
|
||||
color={color}
|
||||
onClick={closePopover}
|
||||
/>
|
||||
}
|
||||
repositionOnScroll
|
||||
>
|
||||
<div style={{ width: 300 }}>{children}</div>
|
||||
</EuiPopover>
|
||||
</span>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* 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 { onFieldChange } from './field_selector';
|
||||
import { EuiComboBoxOptionOption } from '@elastic/eui';
|
||||
import { ALL_VALUE } from '@kbn/slo-schema';
|
||||
|
||||
describe('onFieldChange', () => {
|
||||
let onChangeMock: jest.Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
onChangeMock = jest.fn();
|
||||
});
|
||||
|
||||
it('should filter out ALL_VALUE when a specific value is selected', () => {
|
||||
const selected: Array<EuiComboBoxOptionOption<string>> = [
|
||||
{ label: 'Option 2', value: ALL_VALUE },
|
||||
{ label: 'Option 1', value: 'value1' },
|
||||
];
|
||||
|
||||
onFieldChange(selected, onChangeMock);
|
||||
|
||||
expect(onChangeMock).toHaveBeenCalledWith(['value1']);
|
||||
});
|
||||
|
||||
it('should return an empty array when ALL_VALUE is selected', () => {
|
||||
const selected: Array<EuiComboBoxOptionOption<string>> = [
|
||||
{ label: 'Option 1', value: 'value1' },
|
||||
{ label: 'Option 2', value: ALL_VALUE },
|
||||
];
|
||||
|
||||
onFieldChange(selected, onChangeMock);
|
||||
|
||||
expect(onChangeMock).toHaveBeenCalledWith([]);
|
||||
});
|
||||
|
||||
it('should return an empty array when selected is empty', () => {
|
||||
const selected: Array<EuiComboBoxOptionOption<string>> = [];
|
||||
|
||||
onFieldChange(selected, onChangeMock);
|
||||
|
||||
expect(onChangeMock).toHaveBeenCalledWith([]);
|
||||
});
|
||||
|
||||
it('should call onChange with the filtered array when no ALL_VALUE is present', () => {
|
||||
const selected: Array<EuiComboBoxOptionOption<string>> = [
|
||||
{ label: 'Option 1', value: 'value1' },
|
||||
{ label: 'Option 2', value: 'value2' },
|
||||
];
|
||||
|
||||
onFieldChange(selected, onChangeMock);
|
||||
|
||||
expect(onChangeMock).toHaveBeenCalledWith(['value1', 'value2']);
|
||||
});
|
||||
|
||||
it('should return an empty array if the last selected option is ALL_VALUE', () => {
|
||||
const selected: Array<EuiComboBoxOptionOption<string>> = [
|
||||
{ label: 'Option 1', value: 'value1' },
|
||||
{ label: 'Option 2', value: ALL_VALUE },
|
||||
];
|
||||
|
||||
onFieldChange(selected, onChangeMock);
|
||||
|
||||
expect(onChangeMock).toHaveBeenCalledWith([]);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,110 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { EuiComboBox, EuiComboBoxOptionOption, EuiFormRow } from '@elastic/eui';
|
||||
import { ALL_VALUE } from '@kbn/slo-schema';
|
||||
import { debounce } from 'lodash';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { Suggestion } from '../hooks/use_fetch_synthetics_suggestions';
|
||||
|
||||
interface Option {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface Props {
|
||||
allowAllOption?: boolean;
|
||||
dataTestSubj: string;
|
||||
fieldName: 'monitorIds' | 'projects' | 'tags' | 'locations' | 'monitorTypes';
|
||||
suggestions?: Suggestion[];
|
||||
isLoading?: boolean;
|
||||
required?: boolean;
|
||||
value?: string[];
|
||||
onChange: (selected: string[]) => void;
|
||||
placeholder: string;
|
||||
setSearch: (val: string) => void;
|
||||
setSelectedField: (value: string) => void;
|
||||
}
|
||||
|
||||
const ALL_OPTION = {
|
||||
label: i18n.translate('xpack.synthetics.filter.alert.allLabel', {
|
||||
defaultMessage: 'All',
|
||||
}),
|
||||
value: ALL_VALUE,
|
||||
};
|
||||
|
||||
export function FieldSelector({
|
||||
allowAllOption = true,
|
||||
dataTestSubj,
|
||||
value,
|
||||
onChange,
|
||||
isLoading,
|
||||
placeholder,
|
||||
suggestions,
|
||||
setSearch,
|
||||
}: Props) {
|
||||
const options = (allowAllOption ? [ALL_OPTION] : []).concat(createOptions(suggestions));
|
||||
|
||||
const debouncedSearch = debounce((val) => setSearch(val), 200);
|
||||
|
||||
return (
|
||||
<EuiFormRow fullWidth>
|
||||
<EuiComboBox
|
||||
async
|
||||
placeholder={placeholder}
|
||||
data-test-subj={dataTestSubj}
|
||||
isClearable
|
||||
fullWidth
|
||||
isLoading={isLoading}
|
||||
onChange={(selected: Array<EuiComboBoxOptionOption<string>>) => {
|
||||
onFieldChange(selected, onChange);
|
||||
}}
|
||||
onSearchChange={(val: string) => debouncedSearch(val)}
|
||||
options={options}
|
||||
selectedOptions={value?.map((val) => {
|
||||
const option = options.find((opt) => opt.value === val);
|
||||
if (option) {
|
||||
return {
|
||||
value: val,
|
||||
label: option.label,
|
||||
'data-test-subj': `${dataTestSubj}SelectedValue`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
value: val,
|
||||
label: val,
|
||||
'data-test-subj': `${dataTestSubj}SelectedValue`,
|
||||
};
|
||||
})}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
}
|
||||
|
||||
export const onFieldChange = (
|
||||
selected: Array<EuiComboBoxOptionOption<string>>,
|
||||
onChange: (selected: string[]) => void
|
||||
) => {
|
||||
// removes ALL value option if a specific value is selected
|
||||
if (selected.length && selected.at(-1)?.value !== ALL_VALUE) {
|
||||
onChange(selected.filter((val) => val.value !== ALL_VALUE).map((val) => val.value!));
|
||||
return;
|
||||
}
|
||||
// removes specific value if ALL value is selected
|
||||
if (selected.length && selected.at(-1)?.value === ALL_VALUE) {
|
||||
onChange([]);
|
||||
return;
|
||||
}
|
||||
|
||||
onChange([]);
|
||||
};
|
||||
|
||||
function createOptions(suggestions: Suggestion[] = []): Option[] {
|
||||
return suggestions
|
||||
.map((suggestion) => ({ label: suggestion.label, value: suggestion.value }))
|
||||
.sort((a, b) => String(a.label).localeCompare(b.label));
|
||||
}
|
|
@ -0,0 +1,159 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { FieldPopoverExpression } from './field_popover_expression';
|
||||
import { Suggestion } from '../hooks/use_fetch_synthetics_suggestions';
|
||||
import { FieldSelector } from './field_selector';
|
||||
|
||||
interface FieldProps {
|
||||
value?: string[];
|
||||
onChange: (value?: string[]) => void;
|
||||
setSearch: (val: string) => void;
|
||||
suggestions?: Suggestion[];
|
||||
allSuggestions?: Record<string, Suggestion[]>;
|
||||
isLoading?: boolean;
|
||||
setSelectedField: (value?: string) => void;
|
||||
selectedField?: string;
|
||||
}
|
||||
|
||||
export const allOptionText = i18n.translate('xpack.synthetics.filter.alert.allLabel', {
|
||||
defaultMessage: 'All',
|
||||
});
|
||||
|
||||
export function MonitorField({ value, onChange, ...rest }: FieldProps) {
|
||||
return (
|
||||
<FieldPopoverExpression
|
||||
value={value}
|
||||
title={i18n.translate('xpack.synthetics.alerting.fields.monitor', {
|
||||
defaultMessage: 'Monitor',
|
||||
})}
|
||||
fieldName="monitorIds"
|
||||
selectedField={rest.selectedField}
|
||||
setSelectedField={rest.setSelectedField}
|
||||
allSuggestions={rest.allSuggestions}
|
||||
>
|
||||
<FieldSelector
|
||||
value={value}
|
||||
fieldName="monitorIds"
|
||||
onChange={onChange}
|
||||
placeholder={i18n.translate('xpack.synthetics.monitorNamesSelectPlaceholder', {
|
||||
defaultMessage: 'Select monitor name',
|
||||
})}
|
||||
dataTestSubj="monitorNameField"
|
||||
{...rest}
|
||||
/>
|
||||
</FieldPopoverExpression>
|
||||
);
|
||||
}
|
||||
|
||||
export function TagsField({ value, onChange, ...rest }: FieldProps) {
|
||||
return (
|
||||
<FieldPopoverExpression
|
||||
value={value}
|
||||
title={i18n.translate('xpack.synthetics.alerting.fields.tags', {
|
||||
defaultMessage: 'Tags',
|
||||
})}
|
||||
fieldName="tags"
|
||||
selectedField={rest.selectedField}
|
||||
setSelectedField={rest.setSelectedField}
|
||||
allSuggestions={rest.allSuggestions}
|
||||
>
|
||||
<FieldSelector
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder={i18n.translate('xpack.synthetics.tagsSelectPlaceholder', {
|
||||
defaultMessage: 'Select tags',
|
||||
})}
|
||||
dataTestSubj="tagsField"
|
||||
fieldName="tags"
|
||||
{...rest}
|
||||
/>
|
||||
</FieldPopoverExpression>
|
||||
);
|
||||
}
|
||||
|
||||
export function MonitorTypeField({ value, onChange, ...rest }: FieldProps) {
|
||||
const label = i18n.translate('xpack.synthetics.alerting.fields.type', {
|
||||
defaultMessage: 'Type',
|
||||
});
|
||||
return (
|
||||
<FieldPopoverExpression
|
||||
value={value}
|
||||
title={label}
|
||||
fieldName="monitorTypes"
|
||||
selectedField={rest.selectedField}
|
||||
setSelectedField={rest.setSelectedField}
|
||||
allSuggestions={rest.allSuggestions}
|
||||
>
|
||||
<FieldSelector
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder={i18n.translate('xpack.synthetics.monitorTypesSelectPlaceholder', {
|
||||
defaultMessage: 'Select monitor type',
|
||||
})}
|
||||
dataTestSubj="monitorTypeField"
|
||||
fieldName="monitorTypes"
|
||||
{...rest}
|
||||
/>
|
||||
</FieldPopoverExpression>
|
||||
);
|
||||
}
|
||||
|
||||
export function LocationsField({ value, onChange, ...rest }: FieldProps) {
|
||||
const label = i18n.translate('xpack.synthetics.alerting.fields.location', {
|
||||
defaultMessage: 'Locations',
|
||||
});
|
||||
return (
|
||||
<FieldPopoverExpression
|
||||
value={value}
|
||||
title={label}
|
||||
fieldName="locations"
|
||||
selectedField={rest.selectedField}
|
||||
setSelectedField={rest.setSelectedField}
|
||||
allSuggestions={rest.allSuggestions}
|
||||
>
|
||||
<FieldSelector
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder={i18n.translate('xpack.synthetics.locationSelectPlaceholder', {
|
||||
defaultMessage: 'Select location',
|
||||
})}
|
||||
dataTestSubj="monitorLocationField"
|
||||
fieldName="locations"
|
||||
{...rest}
|
||||
/>
|
||||
</FieldPopoverExpression>
|
||||
);
|
||||
}
|
||||
|
||||
export function ProjectsField({ value, onChange, ...rest }: FieldProps) {
|
||||
const label = i18n.translate('xpack.synthetics.alerting.fields.project', {
|
||||
defaultMessage: 'Projects',
|
||||
});
|
||||
return (
|
||||
<FieldPopoverExpression
|
||||
value={value}
|
||||
title={label}
|
||||
fieldName="projects"
|
||||
selectedField={rest.selectedField}
|
||||
setSelectedField={rest.setSelectedField}
|
||||
allSuggestions={rest.allSuggestions}
|
||||
>
|
||||
<FieldSelector
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder={i18n.translate('xpack.synthetics.projectSelectPlaceholder', {
|
||||
defaultMessage: 'Select project',
|
||||
})}
|
||||
dataTestSubj="monitorProjectField"
|
||||
fieldName="projects"
|
||||
{...rest}
|
||||
/>
|
||||
</FieldPopoverExpression>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,172 @@
|
|||
/*
|
||||
* 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 { EuiExpression, EuiPopover, EuiPopoverTitle, EuiSelectable } from '@elastic/eui';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { getConditionType, StatusRuleCondition } from '../../../../../../common/rules/status_rule';
|
||||
import { StatusRuleParamsProps } from '../status_rule_ui';
|
||||
|
||||
interface Props {
|
||||
ruleParams: StatusRuleParamsProps['ruleParams'];
|
||||
setRuleParams: StatusRuleParamsProps['setRuleParams'];
|
||||
}
|
||||
|
||||
export const WITHIN_TOTAL_CHECKS_LABEL = i18n.translate(
|
||||
'xpack.synthetics.monitorStatusRule.withinTotalChecks.label',
|
||||
{
|
||||
defaultMessage: 'Within total checks',
|
||||
}
|
||||
);
|
||||
|
||||
export const WITHIN_TOTAL_CHECKS_EXPRESSION = i18n.translate(
|
||||
'xpack.synthetics.monitorStatusRule.withinTotalChecks.expression',
|
||||
{
|
||||
defaultMessage: 'Within the last',
|
||||
}
|
||||
);
|
||||
|
||||
export const WITHIN_TIMERANGE_EXPRESSION = i18n.translate(
|
||||
'xpack.synthetics.monitorStatusRule.withinTimerange.expression',
|
||||
{
|
||||
defaultMessage: 'Within the last',
|
||||
}
|
||||
);
|
||||
|
||||
export const WITHIN_TIMERANGE_LABEL = i18n.translate(
|
||||
'xpack.synthetics.monitorStatusRule.withinTimerange.label',
|
||||
{
|
||||
defaultMessage: 'Within timerange',
|
||||
}
|
||||
);
|
||||
|
||||
interface Option {
|
||||
label: string;
|
||||
key: 'checksWindow' | 'timeWindow' | 'locations';
|
||||
}
|
||||
|
||||
const OPTIONS: Option[] = [
|
||||
{
|
||||
label: WITHIN_TOTAL_CHECKS_LABEL,
|
||||
key: 'checksWindow',
|
||||
},
|
||||
{
|
||||
label: WITHIN_TIMERANGE_LABEL,
|
||||
key: 'timeWindow',
|
||||
},
|
||||
];
|
||||
|
||||
export const DEFAULT_CONDITION = {
|
||||
window: { numberOfChecks: 5 },
|
||||
groupBy: 'locationId',
|
||||
downThreshold: 3,
|
||||
locationsThreshold: 1,
|
||||
};
|
||||
const getCheckedOption = (option: Option, condition?: StatusRuleCondition) => {
|
||||
const { useTimeWindow, isLocationBased } = getConditionType(condition);
|
||||
|
||||
if (isLocationBased && option.key === 'locations') {
|
||||
return 'on';
|
||||
}
|
||||
|
||||
if (option.key === 'timeWindow' && useTimeWindow && !isLocationBased) {
|
||||
return 'on';
|
||||
}
|
||||
if (option.key === 'checksWindow' && !useTimeWindow && !isLocationBased) {
|
||||
return 'on';
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const ForTheLastExpression = ({ ruleParams, setRuleParams }: Props) => {
|
||||
const { condition } = ruleParams;
|
||||
|
||||
const { useTimeWindow } = getConditionType(condition);
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const [options, setOptions] = useState<Option[]>(OPTIONS);
|
||||
|
||||
useEffect(() => {
|
||||
if (!condition) {
|
||||
setRuleParams('condition', DEFAULT_CONDITION);
|
||||
}
|
||||
}, [condition, setRuleParams]);
|
||||
|
||||
useEffect(() => {
|
||||
setOptions(
|
||||
OPTIONS.map((option) => ({
|
||||
key: option.key as 'checksWindow' | 'timeWindow',
|
||||
label: option.label,
|
||||
checked: getCheckedOption(option, condition),
|
||||
}))
|
||||
);
|
||||
}, [condition, useTimeWindow]);
|
||||
|
||||
const getDescriptiveText = () => {
|
||||
if (useTimeWindow) {
|
||||
return WITHIN_TIMERANGE_EXPRESSION;
|
||||
}
|
||||
return WITHIN_TOTAL_CHECKS_EXPRESSION;
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiPopover
|
||||
id="checkPopover"
|
||||
panelPaddingSize="s"
|
||||
button={
|
||||
<EuiExpression
|
||||
description={getDescriptiveText()}
|
||||
isActive={isOpen}
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
/>
|
||||
}
|
||||
isOpen={isOpen}
|
||||
closePopover={() => setIsOpen(false)}
|
||||
anchorPosition="downLeft"
|
||||
>
|
||||
<EuiSelectable<Option>
|
||||
singleSelection="always"
|
||||
options={options}
|
||||
onChange={(selectedValues) => {
|
||||
const selectedValue = selectedValues.filter((v) => v.checked === 'on')?.[0];
|
||||
switch (selectedValue?.key) {
|
||||
case 'checksWindow':
|
||||
setRuleParams('condition', {
|
||||
...ruleParams.condition,
|
||||
downThreshold: 5,
|
||||
locationsThreshold: 1,
|
||||
window: { numberOfChecks: 5 },
|
||||
});
|
||||
break;
|
||||
case 'timeWindow':
|
||||
setRuleParams('condition', {
|
||||
...ruleParams.condition,
|
||||
downThreshold: 5,
|
||||
locationsThreshold: 1,
|
||||
window: { time: { unit: 'm', size: 5 } },
|
||||
});
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}}
|
||||
>
|
||||
{(list) => (
|
||||
<div style={{ width: 240 }}>
|
||||
<EuiPopoverTitle>
|
||||
{i18n.translate('xpack.synthetics.forTheLastExpression.whenPopoverTitleLabel', {
|
||||
defaultMessage: 'When',
|
||||
})}
|
||||
</EuiPopoverTitle>
|
||||
{list}
|
||||
</div>
|
||||
)}
|
||||
</EuiSelectable>
|
||||
</EuiPopover>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiIconTip, EuiSwitch } from '@elastic/eui';
|
||||
|
||||
export const GroupByExpression = ({
|
||||
onChange,
|
||||
groupByLocation,
|
||||
locationsThreshold,
|
||||
}: {
|
||||
locationsThreshold: number;
|
||||
groupByLocation: boolean;
|
||||
onChange: (val: boolean) => void;
|
||||
}) => {
|
||||
const disabledGroupBy = locationsThreshold > 1;
|
||||
|
||||
return (
|
||||
<EuiFlexGroup gutterSize="s" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiSwitch
|
||||
compressed
|
||||
disabled={disabledGroupBy}
|
||||
label={i18n.translate('xpack.synthetics.groupByExpression.euiSwitch.groupByLabel', {
|
||||
defaultMessage: 'Receive distinct alerts for each location',
|
||||
})}
|
||||
checked={groupByLocation}
|
||||
onChange={(e) => onChange(e.target.checked)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
{disabledGroupBy ? (
|
||||
<EuiIconTip
|
||||
content={i18n.translate('xpack.synthetics.groupByExpression.euiSwitch.tooltip', {
|
||||
defaultMessage:
|
||||
'When locations threshold is greater than 1, group by location is disabled.',
|
||||
})}
|
||||
/>
|
||||
) : (
|
||||
<EuiIconTip
|
||||
content={i18n.translate('xpack.synthetics.groupByExpression.euiSwitch.tooltip', {
|
||||
defaultMessage:
|
||||
'When the monitor detects a failure on one or more locations, you receive an alert for each of these locations, instead of a single alert.',
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* 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 React, { useState, ReactNode } from 'react';
|
||||
import { EuiExpression, EuiPopover, EuiExpressionProps } from '@elastic/eui';
|
||||
|
||||
interface Props {
|
||||
title?: ReactNode;
|
||||
value: ReactNode;
|
||||
children?: ReactNode;
|
||||
color?: EuiExpressionProps['color'];
|
||||
}
|
||||
|
||||
export function PopoverExpression(props: Props) {
|
||||
const { title, value, children, color } = props;
|
||||
const [popoverOpen, setPopoverOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<EuiPopover
|
||||
isOpen={popoverOpen}
|
||||
anchorPosition="downLeft"
|
||||
closePopover={() => setPopoverOpen(false)}
|
||||
button={
|
||||
<EuiExpression
|
||||
description={title}
|
||||
value={value}
|
||||
isActive={popoverOpen}
|
||||
color={color}
|
||||
onClick={() => setPopoverOpen((state) => !state)}
|
||||
/>
|
||||
}
|
||||
repositionOnScroll
|
||||
>
|
||||
{children}
|
||||
</EuiPopover>
|
||||
);
|
||||
}
|
|
@ -21,12 +21,6 @@ export const ToggleFlyoutTranslations = {
|
|||
toggleTlsAriaLabel: i18n.translate('xpack.synthetics.toggleAlertFlyout.tls.ariaLabel', {
|
||||
defaultMessage: 'Open add tls rule flyout',
|
||||
}),
|
||||
toggleMonitorStatusContent: i18n.translate('xpack.synthetics.toggleAlertButton.content', {
|
||||
defaultMessage: 'Monitor status rule',
|
||||
}),
|
||||
toggleTlsContent: i18n.translate('xpack.synthetics.toggleTlsAlertButton.label.content', {
|
||||
defaultMessage: 'TLS certificate rule',
|
||||
}),
|
||||
navigateToAlertingUIAriaLabel: i18n.translate('xpack.synthetics.app.navigateToAlertingUi', {
|
||||
defaultMessage: 'Leave Synthetics and go to Alerting Management page',
|
||||
}),
|
||||
|
@ -40,3 +34,11 @@ export const ToggleFlyoutTranslations = {
|
|||
defaultMessage: 'Alerts and rules',
|
||||
}),
|
||||
};
|
||||
|
||||
export const TLS_RULE_NAME = i18n.translate('xpack.synthetics.toggleTlsAlertButton.label.content', {
|
||||
defaultMessage: 'TLS certificate rule',
|
||||
});
|
||||
|
||||
export const STATUS_RULE_NAME = i18n.translate('xpack.synthetics.toggleAlertButton.content', {
|
||||
defaultMessage: 'Monitor status rule',
|
||||
});
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* 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 { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import { useFetcher } from '@kbn/observability-shared-plugin/public';
|
||||
import { ClientPluginsStart } from '../../../../../plugin';
|
||||
|
||||
export interface Suggestion {
|
||||
label: string;
|
||||
value: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface UseFetchSyntheticsSuggestions {
|
||||
suggestions: Suggestion[];
|
||||
isLoading: boolean;
|
||||
allSuggestions?: Record<string, Suggestion[]>;
|
||||
}
|
||||
|
||||
export interface Params {
|
||||
fieldName?: string;
|
||||
filters?: {
|
||||
locations?: string[];
|
||||
monitorIds?: string[];
|
||||
tags?: string[];
|
||||
projects?: string[];
|
||||
};
|
||||
search: string;
|
||||
}
|
||||
|
||||
type ApiResponse = Record<string, Suggestion[]>;
|
||||
|
||||
export function useFetchSyntheticsSuggestions({
|
||||
filters,
|
||||
fieldName,
|
||||
search,
|
||||
}: Params): UseFetchSyntheticsSuggestions {
|
||||
const { http } = useKibana<ClientPluginsStart>().services;
|
||||
const { locations, monitorIds, tags, projects } = filters || {};
|
||||
|
||||
const { loading, data } = useFetcher(
|
||||
async ({ signal }) => {
|
||||
return await http.get<ApiResponse>('/internal/synthetics/suggestions', {
|
||||
query: {
|
||||
locations: locations || [],
|
||||
monitorQueryIds: monitorIds || [],
|
||||
tags: tags || [],
|
||||
projects: projects || [],
|
||||
query: search,
|
||||
},
|
||||
signal,
|
||||
});
|
||||
},
|
||||
[http, locations, monitorIds, tags, projects, search]
|
||||
);
|
||||
|
||||
return {
|
||||
suggestions: fieldName ? data?.[fieldName] ?? [] : [],
|
||||
allSuggestions: data,
|
||||
isLoading: Boolean(loading),
|
||||
};
|
||||
}
|
|
@ -8,6 +8,8 @@
|
|||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { selectDynamicSettings } from '../../../state/settings';
|
||||
import { useSyntheticsSettingsContext } from '../../../contexts';
|
||||
import {
|
||||
selectSyntheticsAlerts,
|
||||
|
@ -20,6 +22,7 @@ import {
|
|||
import { SYNTHETICS_TLS_RULE } from '../../../../../../common/constants/synthetics_alerts';
|
||||
import {
|
||||
selectAlertFlyoutVisibility,
|
||||
selectIsNewRule,
|
||||
selectMonitorListState,
|
||||
setAlertFlyoutVisible,
|
||||
} from '../../../state';
|
||||
|
@ -31,12 +34,16 @@ export const useSyntheticsRules = (isOpen: boolean) => {
|
|||
const defaultRules = useSelector(selectSyntheticsAlerts);
|
||||
const loading = useSelector(selectSyntheticsAlertsLoading);
|
||||
const alertFlyoutVisible = useSelector(selectAlertFlyoutVisibility);
|
||||
const isNewRule = useSelector(selectIsNewRule);
|
||||
const { settings } = useSelector(selectDynamicSettings);
|
||||
|
||||
const { canSave } = useSyntheticsSettingsContext();
|
||||
|
||||
const { loaded, data: monitors } = useSelector(selectMonitorListState);
|
||||
|
||||
const hasMonitors = loaded && monitors.absoluteTotal && monitors.absoluteTotal > 0;
|
||||
const defaultRulesEnabled =
|
||||
settings && (settings?.defaultStatusRuleEnabled || settings?.defaultTLSRuleEnabled);
|
||||
|
||||
const getOrCreateAlerts = useCallback(() => {
|
||||
if (canSave) {
|
||||
|
@ -47,7 +54,7 @@ export const useSyntheticsRules = (isOpen: boolean) => {
|
|||
}, [canSave, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
if (hasMonitors) {
|
||||
if (hasMonitors && defaultRulesEnabled) {
|
||||
if (!defaultRules) {
|
||||
// on initial load we prioritize loading the app
|
||||
setTimeout(() => {
|
||||
|
@ -59,22 +66,52 @@ export const useSyntheticsRules = (isOpen: boolean) => {
|
|||
}
|
||||
// we don't want to run this on defaultRules change
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [dispatch, isOpen, hasMonitors]);
|
||||
}, [dispatch, isOpen, hasMonitors, defaultRulesEnabled]);
|
||||
|
||||
const { triggersActionsUi } = useKibana<ClientPluginsStart>().services;
|
||||
|
||||
const EditAlertFlyout = useMemo(() => {
|
||||
const initialRule =
|
||||
alertFlyoutVisible === SYNTHETICS_TLS_RULE ? defaultRules?.tlsRule : defaultRules?.statusRule;
|
||||
if (!initialRule) {
|
||||
if (!initialRule || isNewRule) {
|
||||
return null;
|
||||
}
|
||||
return triggersActionsUi.getEditRuleFlyout({
|
||||
onClose: () => dispatch(setAlertFlyoutVisible(null)),
|
||||
hideInterval: true,
|
||||
initialRule,
|
||||
});
|
||||
}, [defaultRules, dispatch, triggersActionsUi, alertFlyoutVisible]);
|
||||
}, [
|
||||
alertFlyoutVisible,
|
||||
defaultRules?.tlsRule,
|
||||
defaultRules?.statusRule,
|
||||
isNewRule,
|
||||
triggersActionsUi,
|
||||
dispatch,
|
||||
]);
|
||||
|
||||
return useMemo(() => ({ loading, EditAlertFlyout }), [EditAlertFlyout, loading]);
|
||||
const NewRuleFlyout = useMemo(() => {
|
||||
if (!isNewRule || !alertFlyoutVisible) {
|
||||
return null;
|
||||
}
|
||||
return triggersActionsUi.getAddRuleFlyout({
|
||||
consumer: 'uptime',
|
||||
ruleTypeId: alertFlyoutVisible,
|
||||
onClose: () => dispatch(setAlertFlyoutVisible(null)),
|
||||
initialValues: {
|
||||
name:
|
||||
alertFlyoutVisible === SYNTHETICS_TLS_RULE
|
||||
? i18n.translate('xpack.synthetics.alerting.defaultRuleName.tls', {
|
||||
defaultMessage: 'Synthetics monitor TLS rule',
|
||||
})
|
||||
: i18n.translate('xpack.synthetics.alerting.defaultRuleName', {
|
||||
defaultMessage: 'Synthetics monitor status rule',
|
||||
}),
|
||||
},
|
||||
});
|
||||
}, [isNewRule, triggersActionsUi, dispatch, alertFlyoutVisible]);
|
||||
|
||||
return useMemo(
|
||||
() => ({ loading, EditAlertFlyout, NewRuleFlyout }),
|
||||
[EditAlertFlyout, loading, NewRuleFlyout]
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* 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 React, { useEffect } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import { Filter } from '@kbn/es-query';
|
||||
import { EuiFormRow } from '@elastic/eui';
|
||||
import { useSyntheticsDataView } from '../../contexts/synthetics_data_view_context';
|
||||
import { ClientPluginsStart } from '../../../../plugin';
|
||||
|
||||
export function AlertSearchBar({
|
||||
kqlQuery,
|
||||
onChange,
|
||||
}: {
|
||||
kqlQuery: string;
|
||||
onChange: (val: { kqlQuery?: string; filters?: Filter[] }) => void;
|
||||
}) {
|
||||
const {
|
||||
data: { query },
|
||||
unifiedSearch: {
|
||||
ui: { QueryStringInput },
|
||||
},
|
||||
} = useKibana<ClientPluginsStart>().services;
|
||||
|
||||
const dataView = useSyntheticsDataView();
|
||||
|
||||
useEffect(() => {
|
||||
const sub = query.state$.subscribe(() => {
|
||||
const queryState = query.getState();
|
||||
onChange({
|
||||
kqlQuery: String(queryState.query),
|
||||
});
|
||||
});
|
||||
|
||||
return sub.unsubscribe;
|
||||
}, [onChange, query]);
|
||||
|
||||
return (
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.synthetics.list.search.title', {
|
||||
defaultMessage: 'Filter by',
|
||||
})}
|
||||
fullWidth
|
||||
>
|
||||
<QueryStringInput
|
||||
appName="synthetics"
|
||||
iconType="search"
|
||||
placeholder={PLACEHOLDER}
|
||||
indexPatterns={dataView ? [dataView] : []}
|
||||
onChange={(queryN) => {
|
||||
onChange({
|
||||
kqlQuery: String(queryN.query),
|
||||
});
|
||||
}}
|
||||
onSubmit={(queryN) => {
|
||||
if (queryN) {
|
||||
onChange({
|
||||
kqlQuery: String(queryN.query),
|
||||
});
|
||||
}
|
||||
}}
|
||||
query={{ query: String(kqlQuery), language: 'kuery' }}
|
||||
autoSubmit={true}
|
||||
disableLanguageSwitcher={true}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
}
|
||||
|
||||
const PLACEHOLDER = i18n.translate('xpack.synthetics.list.search', {
|
||||
defaultMessage: 'Filter by KQL query',
|
||||
});
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* 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 { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
|
||||
export const RuleNameWithLoading = ({
|
||||
ruleName,
|
||||
isLoading,
|
||||
}: {
|
||||
ruleName: string;
|
||||
isLoading: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<EuiFlexGroup alignItems="center">
|
||||
<EuiFlexItem>{ruleName}</EuiFlexItem>
|
||||
{isLoading && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiLoadingSpinner />
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,157 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiExpression,
|
||||
EuiFlexItem,
|
||||
EuiFlexGroup,
|
||||
EuiSpacer,
|
||||
EuiTitle,
|
||||
EuiHorizontalRule,
|
||||
EuiIconTip,
|
||||
} from '@elastic/eui';
|
||||
import React, { useCallback } from 'react';
|
||||
import { ValueExpression } from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { GroupByExpression } from './common/group_by_field';
|
||||
import { WindowValueExpression } from './common/condition_window_value';
|
||||
import { DEFAULT_CONDITION, ForTheLastExpression } from './common/for_the_last_expression';
|
||||
import { StatusRuleParamsProps } from './status_rule_ui';
|
||||
import { LocationsValueExpression } from './common/condition_locations_value';
|
||||
|
||||
interface Props {
|
||||
ruleParams: StatusRuleParamsProps['ruleParams'];
|
||||
setRuleParams: StatusRuleParamsProps['setRuleParams'];
|
||||
}
|
||||
|
||||
export const StatusRuleExpression: React.FC<Props> = ({ ruleParams, setRuleParams }) => {
|
||||
const condition = ruleParams.condition ?? DEFAULT_CONDITION;
|
||||
const downThreshold = condition?.downThreshold ?? DEFAULT_CONDITION.downThreshold;
|
||||
|
||||
const locationsThreshold = condition?.locationsThreshold ?? DEFAULT_CONDITION.locationsThreshold;
|
||||
|
||||
const onThresholdChange = useCallback(
|
||||
(value: number) => {
|
||||
const prevCondition = ruleParams.condition ?? DEFAULT_CONDITION;
|
||||
setRuleParams('condition', {
|
||||
...prevCondition,
|
||||
downThreshold: value,
|
||||
});
|
||||
},
|
||||
[ruleParams.condition, setRuleParams]
|
||||
);
|
||||
|
||||
const onGroupByChange = useCallback(
|
||||
(groupByLocation: boolean) => {
|
||||
setRuleParams('condition', {
|
||||
...(ruleParams?.condition ?? DEFAULT_CONDITION),
|
||||
groupBy: groupByLocation ? 'locationId' : 'none',
|
||||
});
|
||||
},
|
||||
[ruleParams?.condition, setRuleParams]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiHorizontalRule size="half" margin="xs" />
|
||||
<EuiFlexGroup gutterSize="s" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle size="xs">
|
||||
<h3>
|
||||
{i18n.translate('xpack.synthetics.rules.status.condition.title', {
|
||||
defaultMessage: 'Condition',
|
||||
})}
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIconTip
|
||||
content={i18n.translate('xpack.synthetics.rule.condition.retests', {
|
||||
defaultMessage: 'Retests are included in the number of checks.',
|
||||
})}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiFlexGroup gutterSize="m">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiExpression
|
||||
aria-label={StatusTranslations.criteriaAriaLabel}
|
||||
color="success"
|
||||
description={StatusTranslations.criteriaDescription}
|
||||
value={StatusTranslations.criteriaValue}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<ValueExpression
|
||||
value={downThreshold}
|
||||
valueLabel={i18n.translate('xpack.synthetics.rules.status.valueLabel', {
|
||||
defaultMessage: '{threshold} times',
|
||||
values: { threshold: downThreshold },
|
||||
})}
|
||||
onChangeSelectedValue={(val) => {
|
||||
onThresholdChange(val);
|
||||
}}
|
||||
description={StatusTranslations.isDownDescription}
|
||||
errors={[]}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<ForTheLastExpression ruleParams={ruleParams} setRuleParams={setRuleParams} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<WindowValueExpression ruleParams={ruleParams} setRuleParams={setRuleParams} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="xs" />
|
||||
<EuiFlexGroup gutterSize="m">
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup gutterSize="s">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiExpression description={StatusTranslations.fromLocationsDescription} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<LocationsValueExpression ruleParams={ruleParams} setRuleParams={setRuleParams} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="m" />
|
||||
<GroupByExpression
|
||||
groupByLocation={ruleParams.condition?.groupBy === 'locationId'}
|
||||
onChange={onGroupByChange}
|
||||
locationsThreshold={locationsThreshold}
|
||||
/>
|
||||
<EuiSpacer size="l" />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const StatusTranslations = {
|
||||
criteriaAriaLabel: i18n.translate('xpack.synthetics.rules.status.criteriaExpression.ariaLabel', {
|
||||
defaultMessage:
|
||||
'An expression displaying the criteria for the monitors that are being watched by this alert',
|
||||
}),
|
||||
criteriaDescription: i18n.translate(
|
||||
'xpack.synthetics.alerts.tls.criteriaExpression.description',
|
||||
{
|
||||
defaultMessage: 'when',
|
||||
}
|
||||
),
|
||||
criteriaValue: i18n.translate('xpack.synthetics.status.criteriaExpression.value', {
|
||||
defaultMessage: 'monitor',
|
||||
}),
|
||||
isDownDescription: i18n.translate('xpack.synthetics.status.expirationExpression.description', {
|
||||
defaultMessage: 'is down ',
|
||||
}),
|
||||
fromLocationsDescription: i18n.translate(
|
||||
'xpack.synthetics.status.locationsThreshold.description',
|
||||
{
|
||||
defaultMessage: 'from at least',
|
||||
}
|
||||
),
|
||||
};
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* 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 React, { useCallback } from 'react';
|
||||
import { RuleTypeParamsExpressionProps } from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import { Filter } from '@kbn/es-query';
|
||||
import { EuiSpacer } from '@elastic/eui';
|
||||
import { FieldFilters } from './common/field_filters';
|
||||
import { AlertSearchBar } from './query_bar';
|
||||
import { StatusRuleExpression } from './status_rule_expression';
|
||||
import { StatusRuleParams } from '../../../../../common/rules/status_rule';
|
||||
|
||||
export type StatusRuleParamsProps = RuleTypeParamsExpressionProps<StatusRuleParams>;
|
||||
|
||||
export const StatusRuleComponent: React.FC<{
|
||||
ruleParams: StatusRuleParamsProps['ruleParams'];
|
||||
setRuleParams: StatusRuleParamsProps['setRuleParams'];
|
||||
}> = ({ ruleParams, setRuleParams }) => {
|
||||
const onFiltersChange = useCallback(
|
||||
(val: { kqlQuery?: string; filters?: Filter[] }) => {
|
||||
setRuleParams('kqlQuery', val.kqlQuery);
|
||||
},
|
||||
[setRuleParams]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<AlertSearchBar kqlQuery={ruleParams.kqlQuery ?? ''} onChange={onFiltersChange} />
|
||||
<EuiSpacer size="m" />
|
||||
<FieldFilters ruleParams={ruleParams} setRuleParams={setRuleParams} />
|
||||
<StatusRuleExpression ruleParams={ruleParams} setRuleParams={setRuleParams} />
|
||||
<EuiSpacer size="m" />
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -11,21 +11,18 @@ import { useKibana } from '@kbn/kibana-react-plugin/public';
|
|||
import {
|
||||
EuiContextMenu,
|
||||
EuiContextMenuPanelDescriptor,
|
||||
EuiContextMenuPanelItemDescriptor,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiHeaderLink,
|
||||
EuiLoadingSpinner,
|
||||
EuiPopover,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { RuleNameWithLoading } from './rule_name_with_loading';
|
||||
import {
|
||||
SYNTHETICS_STATUS_RULE,
|
||||
SYNTHETICS_TLS_RULE,
|
||||
} from '../../../../../common/constants/synthetics_alerts';
|
||||
import { ManageRulesLink } from '../common/links/manage_rules_link';
|
||||
import { ClientPluginsStart } from '../../../../plugin';
|
||||
import { ToggleFlyoutTranslations } from './hooks/translations';
|
||||
import { STATUS_RULE_NAME, TLS_RULE_NAME, ToggleFlyoutTranslations } from './hooks/translations';
|
||||
import { useSyntheticsRules } from './hooks/use_synthetics_rules';
|
||||
import {
|
||||
selectAlertFlyoutVisibility,
|
||||
|
@ -40,67 +37,84 @@ export const ToggleAlertFlyoutButton = () => {
|
|||
const { application } = useKibana<ClientPluginsStart>().services;
|
||||
const hasUptimeWrite = application?.capabilities.uptime?.save ?? false;
|
||||
|
||||
const { EditAlertFlyout, loading } = useSyntheticsRules(isOpen);
|
||||
|
||||
const { EditAlertFlyout, loading, NewRuleFlyout } = useSyntheticsRules(isOpen);
|
||||
const { loaded, data: monitors } = useSelector(selectMonitorListState);
|
||||
|
||||
const hasMonitors = loaded && monitors.absoluteTotal && monitors.absoluteTotal > 0;
|
||||
|
||||
const monitorStatusAlertContextMenuItem: EuiContextMenuPanelItemDescriptor = {
|
||||
'aria-label': ToggleFlyoutTranslations.toggleMonitorStatusAriaLabel,
|
||||
'data-test-subj': 'xpack.synthetics.toggleAlertFlyout',
|
||||
name: (
|
||||
<EuiFlexGroup alignItems="center">
|
||||
<EuiFlexItem>{ToggleFlyoutTranslations.toggleMonitorStatusContent}</EuiFlexItem>
|
||||
{loading && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiLoadingSpinner />
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
onClick: () => {
|
||||
dispatch(setAlertFlyoutVisible(SYNTHETICS_STATUS_RULE));
|
||||
setIsOpen(false);
|
||||
},
|
||||
toolTipContent: !hasUptimeWrite ? noWritePermissionsTooltipContent : null,
|
||||
disabled: !hasUptimeWrite || loading,
|
||||
icon: 'bell',
|
||||
};
|
||||
|
||||
const tlsAlertContextMenuItem: EuiContextMenuPanelItemDescriptor = {
|
||||
'aria-label': ToggleFlyoutTranslations.toggleMonitorStatusAriaLabel,
|
||||
'data-test-subj': 'xpack.synthetics.toggleAlertFlyout.tls',
|
||||
name: (
|
||||
<EuiFlexGroup alignItems="center">
|
||||
<EuiFlexItem>{ToggleFlyoutTranslations.toggleTlsContent}</EuiFlexItem>
|
||||
{loading && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiLoadingSpinner />
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
onClick: () => {
|
||||
dispatch(setAlertFlyoutVisible(SYNTHETICS_TLS_RULE));
|
||||
setIsOpen(false);
|
||||
},
|
||||
toolTipContent: !hasUptimeWrite ? noWritePermissionsTooltipContent : null,
|
||||
disabled: !hasUptimeWrite || loading,
|
||||
icon: 'bell',
|
||||
};
|
||||
|
||||
const managementContextItem: EuiContextMenuPanelItemDescriptor = {
|
||||
'aria-label': ToggleFlyoutTranslations.navigateToAlertingUIAriaLabel,
|
||||
'data-test-subj': 'xpack.synthetics.navigateToAlertingUi',
|
||||
name: <ManageRulesLink />,
|
||||
icon: 'tableOfContents',
|
||||
};
|
||||
|
||||
const panels: EuiContextMenuPanelDescriptor[] = [
|
||||
{
|
||||
id: 0,
|
||||
items: [monitorStatusAlertContextMenuItem, tlsAlertContextMenuItem, managementContextItem],
|
||||
items: [
|
||||
{
|
||||
name: STATUS_RULE_NAME,
|
||||
'data-test-subj': 'manageStatusRuleName',
|
||||
panel: 1,
|
||||
},
|
||||
{
|
||||
name: TLS_RULE_NAME,
|
||||
'data-test-subj': 'manageTlsRuleName',
|
||||
panel: 2,
|
||||
},
|
||||
{
|
||||
'aria-label': ToggleFlyoutTranslations.navigateToAlertingUIAriaLabel,
|
||||
'data-test-subj': 'xpack.synthetics.navigateToAlertingUi',
|
||||
name: <ManageRulesLink />,
|
||||
icon: 'tableOfContents',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
items: [
|
||||
{
|
||||
name: CREATE_STATUS_RULE,
|
||||
'data-test-subj': 'createNewStatusRule',
|
||||
icon: 'plusInCircle',
|
||||
onClick: () => {
|
||||
dispatch(setAlertFlyoutVisible({ id: SYNTHETICS_STATUS_RULE, isNewRuleFlyout: true }));
|
||||
setIsOpen(false);
|
||||
},
|
||||
},
|
||||
{
|
||||
'aria-label': ToggleFlyoutTranslations.toggleMonitorStatusAriaLabel,
|
||||
'data-test-subj': 'editDefaultStatusRule',
|
||||
name: <RuleNameWithLoading ruleName={EDIT_STATUS_RULE} isLoading={loading} />,
|
||||
onClick: () => {
|
||||
dispatch(setAlertFlyoutVisible({ id: SYNTHETICS_STATUS_RULE, isNewRuleFlyout: false }));
|
||||
setIsOpen(false);
|
||||
},
|
||||
toolTipContent: !hasUptimeWrite ? noWritePermissionsTooltipContent : null,
|
||||
disabled: !hasUptimeWrite || loading,
|
||||
icon: 'bell',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
items: [
|
||||
{
|
||||
name: CREATE_TLS_RULE_NAME,
|
||||
'data-test-subj': 'createNewTLSRule',
|
||||
icon: 'plusInCircle',
|
||||
onClick: () => {
|
||||
dispatch(setAlertFlyoutVisible({ id: SYNTHETICS_TLS_RULE, isNewRuleFlyout: true }));
|
||||
setIsOpen(false);
|
||||
},
|
||||
},
|
||||
{
|
||||
'aria-label': ToggleFlyoutTranslations.toggleMonitorStatusAriaLabel,
|
||||
'data-test-subj': 'editDefaultTlsRule',
|
||||
name: <RuleNameWithLoading ruleName={EDIT_TLS_RULE_NAME} isLoading={loading} />,
|
||||
onClick: () => {
|
||||
dispatch(setAlertFlyoutVisible({ id: SYNTHETICS_TLS_RULE, isNewRuleFlyout: false }));
|
||||
setIsOpen(false);
|
||||
},
|
||||
toolTipContent: !hasUptimeWrite ? noWritePermissionsTooltipContent : null,
|
||||
disabled: !hasUptimeWrite || loading,
|
||||
icon: 'bell',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -113,7 +127,7 @@ export const ToggleAlertFlyoutButton = () => {
|
|||
<EuiHeaderLink
|
||||
color="text"
|
||||
aria-label={ToggleFlyoutTranslations.toggleButtonAriaLabel}
|
||||
data-test-subj="xpack.synthetics.alertsPopover.toggleButton"
|
||||
data-test-subj="syntheticsAlertsRulesButton"
|
||||
iconType="arrowDown"
|
||||
iconSide="right"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
|
@ -130,6 +144,7 @@ export const ToggleAlertFlyoutButton = () => {
|
|||
<EuiContextMenu initialPanelId={0} panels={panels} />
|
||||
</EuiPopover>
|
||||
{alertFlyoutVisible && EditAlertFlyout}
|
||||
{alertFlyoutVisible && NewRuleFlyout}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -140,3 +155,31 @@ const noWritePermissionsTooltipContent = i18n.translate(
|
|||
defaultMessage: 'You do not have sufficient permissions to perform this action.',
|
||||
}
|
||||
);
|
||||
|
||||
export const EDIT_TLS_RULE_NAME = i18n.translate(
|
||||
'xpack.synthetics.toggleTlsAlertButton.label.default',
|
||||
{
|
||||
defaultMessage: 'Edit default TLS rule',
|
||||
}
|
||||
);
|
||||
|
||||
export const EDIT_STATUS_RULE = i18n.translate(
|
||||
'xpack.synthetics.toggleStatusAlertButton.label.default',
|
||||
{
|
||||
defaultMessage: 'Edit default status rule',
|
||||
}
|
||||
);
|
||||
|
||||
export const CREATE_TLS_RULE_NAME = i18n.translate(
|
||||
'xpack.synthetics.toggleTlsAlertButton.createRule',
|
||||
{
|
||||
defaultMessage: 'Create TLS rule',
|
||||
}
|
||||
);
|
||||
|
||||
export const CREATE_STATUS_RULE = i18n.translate(
|
||||
'xpack.synthetics.toggleStatusAlertButton.createRule',
|
||||
{
|
||||
defaultMessage: 'Create status rule',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -35,6 +35,10 @@ describe('Monitor Detail Flyout', () => {
|
|||
status: 'up',
|
||||
type: 'http',
|
||||
check_group: 'check-group',
|
||||
timespan: {
|
||||
gte: 'now-15m',
|
||||
lt: 'now',
|
||||
},
|
||||
},
|
||||
url: {
|
||||
full: 'https://www.elastic.co',
|
||||
|
|
|
@ -97,7 +97,7 @@ export function OverviewStatus({ titleAppend }: { titleAppend?: React.ReactNode
|
|||
<EuiFlexGroup gutterSize="xl" justifyContent="spaceAround">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiStat
|
||||
data-test-subj="xpack.uptime.synthetics.overview.status.up"
|
||||
data-test-subj="syntheticsOverviewUp"
|
||||
description={upDescription}
|
||||
reverse
|
||||
title={title(statusConfig?.up)}
|
||||
|
@ -107,7 +107,7 @@ export function OverviewStatus({ titleAppend }: { titleAppend?: React.ReactNode
|
|||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiStat
|
||||
data-test-subj="xpack.uptime.synthetics.overview.status.down"
|
||||
data-test-subj="syntheticsOverviewDown"
|
||||
description={downDescription}
|
||||
reverse
|
||||
title={title(statusConfig?.down)}
|
||||
|
|
|
@ -6,13 +6,15 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { CoreStart } from '@kbn/core/public';
|
||||
import { Provider as ReduxProvider } from 'react-redux';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { CoreStart } from '@kbn/core/public';
|
||||
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
||||
import { RuleTypeParamsExpressionProps } from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import { EuiSpacer, EuiText } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { StatusRuleComponent } from '../../../components/alerts/status_rule_ui';
|
||||
import { kibanaService } from '../../../../../utils/kibana_service';
|
||||
import { ClientPluginsStart } from '../../../../../plugin';
|
||||
import { store } from '../../../state';
|
||||
|
@ -28,16 +30,23 @@ interface Props {
|
|||
export default function MonitorStatusAlert({ coreStart, plugins, params }: Props) {
|
||||
kibanaService.coreStart = coreStart;
|
||||
const queryClient = new QueryClient();
|
||||
const { ruleParams } = params;
|
||||
return (
|
||||
<ReduxProvider store={store}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<KibanaContextProvider services={{ ...coreStart, ...plugins }}>
|
||||
<EuiText>
|
||||
<FormattedMessage
|
||||
id="xpack.synthetics.alertRule.monitorStatus.description"
|
||||
defaultMessage="Manage synthetics monitor status rule actions."
|
||||
/>
|
||||
</EuiText>
|
||||
{params.id && isEmpty(ruleParams) && (
|
||||
<EuiText>
|
||||
<FormattedMessage
|
||||
id="xpack.synthetics.alertRule.monitorStatus.description"
|
||||
defaultMessage="Manage synthetics monitor status rule actions."
|
||||
/>
|
||||
</EuiText>
|
||||
)}
|
||||
|
||||
{(!params.id || !isEmpty(ruleParams)) && (
|
||||
<StatusRuleComponent ruleParams={ruleParams} setRuleParams={params.setRuleParams} />
|
||||
)}
|
||||
|
||||
<EuiSpacer size="m" />
|
||||
</KibanaContextProvider>
|
||||
|
|
|
@ -41,7 +41,7 @@ export const initMonitorStatusAlertType: AlertTypeInitializer = ({
|
|||
},
|
||||
defaultActionMessage,
|
||||
defaultRecoveryMessage,
|
||||
requiresAppContext: true,
|
||||
requiresAppContext: false,
|
||||
format: ({ fields }) => {
|
||||
return {
|
||||
reason: fields[ALERT_REASON] || '',
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import { PayloadAction } from '@reduxjs/toolkit';
|
||||
import { call, put, takeEvery, select, takeLatest, debounce } from 'redux-saga/effects';
|
||||
import { quietFetchOverviewStatusAction } from '../overview_status';
|
||||
import { fetchOverviewStatusAction, quietFetchOverviewStatusAction } from '../overview_status';
|
||||
import { enableDefaultAlertingAction } from '../alert_rules';
|
||||
import {
|
||||
ConfigKey,
|
||||
|
@ -15,7 +15,7 @@ import {
|
|||
SyntheticsMonitorWithId,
|
||||
} from '../../../../../common/runtime_types';
|
||||
import { kibanaService } from '../../../../utils/kibana_service';
|
||||
import { MonitorOverviewPageState } from '../overview';
|
||||
import { MonitorOverviewPageState, selectOverviewPageState } from '../overview';
|
||||
import { selectOverviewState } from '../overview/selectors';
|
||||
import { fetchEffectFactory, sendErrorToast, sendSuccessToast } from '../utils/fetch_effect';
|
||||
import { serializeHttpFetchError } from '../utils/http_error';
|
||||
|
@ -53,7 +53,13 @@ export function* enableMonitorAlertEffect() {
|
|||
try {
|
||||
const response = yield call(fetchUpsertMonitor, action.payload);
|
||||
yield put(enableMonitorAlertAction.success(response as SyntheticsMonitorWithId));
|
||||
const pageState = (yield select(selectOverviewPageState)) as MonitorOverviewPageState;
|
||||
sendSuccessToast(action.payload.success);
|
||||
yield put(
|
||||
fetchOverviewStatusAction.get({
|
||||
pageState,
|
||||
})
|
||||
);
|
||||
if (
|
||||
(response as EncryptedSyntheticsSavedMonitor)[ConfigKey.ALERT_CONFIG]?.status?.enabled
|
||||
) {
|
||||
|
|
|
@ -7,13 +7,7 @@
|
|||
|
||||
import { createReducer } from '@reduxjs/toolkit';
|
||||
|
||||
import { enableMonitorAlertAction } from '../monitor_list/actions';
|
||||
import { isStatusEnabled } from '../../../../../common/runtime_types/monitor_management/alert_config';
|
||||
import {
|
||||
ConfigKey,
|
||||
OverviewStatusMetaData,
|
||||
OverviewStatusState,
|
||||
} from '../../../../../common/runtime_types';
|
||||
import { OverviewStatusMetaData, OverviewStatusState } from '../../../../../common/runtime_types';
|
||||
import { IHttpSerializedFetchError } from '..';
|
||||
import {
|
||||
clearOverviewStatusErrorAction,
|
||||
|
@ -27,7 +21,6 @@ export interface OverviewStatusStateReducer {
|
|||
status: OverviewStatusState | null;
|
||||
allConfigs?: OverviewStatusMetaData[];
|
||||
disabledConfigs?: OverviewStatusMetaData[];
|
||||
sortedByStatus?: OverviewStatusMetaData[];
|
||||
error: IHttpSerializedFetchError | null;
|
||||
}
|
||||
|
||||
|
@ -63,24 +56,6 @@ export const overviewStatusReducer = createReducer(initialState, (builder) => {
|
|||
state.error = action.payload;
|
||||
state.loading = false;
|
||||
})
|
||||
.addCase(enableMonitorAlertAction.success, (state, action) => {
|
||||
const monitorObject = action.payload;
|
||||
if (!('errors' in monitorObject)) {
|
||||
const isStatusAlertEnabled = isStatusEnabled(monitorObject[ConfigKey.ALERT_CONFIG]);
|
||||
state.allConfigs = state.allConfigs?.map((monitor) => {
|
||||
if (
|
||||
monitor.configId === monitorObject[ConfigKey.CONFIG_ID] ||
|
||||
monitor.monitorQueryId === monitorObject[ConfigKey.MONITOR_QUERY_ID]
|
||||
) {
|
||||
return {
|
||||
...monitor,
|
||||
isStatusAlertEnabled,
|
||||
};
|
||||
}
|
||||
return monitor;
|
||||
});
|
||||
}
|
||||
})
|
||||
.addCase(clearOverviewStatusErrorAction, (state) => {
|
||||
state.error = null;
|
||||
});
|
||||
|
|
|
@ -16,9 +16,10 @@ export interface PopoverState {
|
|||
open: boolean;
|
||||
}
|
||||
|
||||
export const setAlertFlyoutVisible = createAction<
|
||||
typeof SYNTHETICS_STATUS_RULE | typeof SYNTHETICS_TLS_RULE | null
|
||||
>('[UI] TOGGLE ALERT FLYOUT');
|
||||
export const setAlertFlyoutVisible = createAction<{
|
||||
id: typeof SYNTHETICS_STATUS_RULE | typeof SYNTHETICS_TLS_RULE | null;
|
||||
isNewRuleFlyout: boolean;
|
||||
} | null>('[UI] TOGGLE ALERT FLYOUT');
|
||||
|
||||
export const setBasePath = createAction<string>('[UI] SET BASE PATH');
|
||||
|
||||
|
|
|
@ -22,7 +22,8 @@ import {
|
|||
} from './actions';
|
||||
|
||||
export interface UiState {
|
||||
alertFlyoutVisible: typeof SYNTHETICS_TLS_RULE | typeof SYNTHETICS_STATUS_RULE | null;
|
||||
ruleFlyoutVisible: typeof SYNTHETICS_TLS_RULE | typeof SYNTHETICS_STATUS_RULE | null;
|
||||
isNewRuleFlyout?: boolean | null;
|
||||
basePath: string;
|
||||
esKuery: string;
|
||||
searchText: string;
|
||||
|
@ -31,7 +32,8 @@ export interface UiState {
|
|||
}
|
||||
|
||||
const initialState: UiState = {
|
||||
alertFlyoutVisible: null,
|
||||
isNewRuleFlyout: false,
|
||||
ruleFlyoutVisible: null,
|
||||
basePath: '',
|
||||
esKuery: '',
|
||||
searchText: '',
|
||||
|
@ -45,7 +47,8 @@ export const uiReducer = createReducer(initialState, (builder) => {
|
|||
state.integrationsPopoverOpen = action.payload;
|
||||
})
|
||||
.addCase(setAlertFlyoutVisible, (state, action) => {
|
||||
state.alertFlyoutVisible = action.payload;
|
||||
state.ruleFlyoutVisible = action.payload?.id ?? null;
|
||||
state.isNewRuleFlyout = action.payload?.isNewRuleFlyout ?? null;
|
||||
})
|
||||
.addCase(setBasePath, (state, action) => {
|
||||
state.basePath = action.payload;
|
||||
|
|
|
@ -12,5 +12,10 @@ const uiStateSelector = (appState: SyntheticsAppState) => appState.ui;
|
|||
|
||||
export const selectAlertFlyoutVisibility = createSelector(
|
||||
uiStateSelector,
|
||||
({ alertFlyoutVisible }) => alertFlyoutVisible
|
||||
({ ruleFlyoutVisible }) => ruleFlyoutVisible
|
||||
);
|
||||
|
||||
export const selectIsNewRule = createSelector(
|
||||
uiStateSelector,
|
||||
({ isNewRuleFlyout }) => isNewRuleFlyout
|
||||
);
|
||||
|
|
|
@ -24,7 +24,7 @@ import { MonitorDetailsState } from '../../../state';
|
|||
*/
|
||||
export const mockState: SyntheticsAppState = {
|
||||
ui: {
|
||||
alertFlyoutVisible: null,
|
||||
ruleFlyoutVisible: null,
|
||||
basePath: 'yyz',
|
||||
esKuery: '',
|
||||
integrationsPopoverOpen: null,
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
require('@kbn/babel-register').install();
|
||||
require('./tasks/generate_monitors').generateMonitors();
|
|
@ -0,0 +1,97 @@
|
|||
/*
|
||||
* 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 axios from 'axios';
|
||||
import moment from 'moment';
|
||||
|
||||
const UP_MONITORS = 0;
|
||||
const DOWN_MONITORS = 10;
|
||||
|
||||
export const generateMonitors = async () => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`Generating ${UP_MONITORS} up monitors`);
|
||||
for (let i = 0; i < UP_MONITORS; i++) {
|
||||
await createMonitor(getHttpMonitor());
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`Generating ${DOWN_MONITORS} down monitors`);
|
||||
for (let i = 0; i < DOWN_MONITORS; i++) {
|
||||
await createMonitor(getHttpMonitor(true));
|
||||
}
|
||||
};
|
||||
|
||||
const createMonitor = async (monitor: any) => {
|
||||
await axios
|
||||
.request({
|
||||
data: monitor,
|
||||
method: 'post',
|
||||
url: 'http://127.0.0.1:5601/test/api/synthetics/monitors',
|
||||
auth: { username: 'elastic', password: 'jdpAyka8HBiq81dFAIB86Nkp' },
|
||||
headers: { 'kbn-xsrf': 'true', 'elastic-api-version': '2023-10-31' },
|
||||
})
|
||||
.catch((error) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(error);
|
||||
});
|
||||
};
|
||||
|
||||
const getHttpMonitor = (isDown?: boolean) => {
|
||||
return {
|
||||
type: 'http',
|
||||
form_monitor_type: 'http',
|
||||
enabled: true,
|
||||
alert: { status: { enabled: true }, tls: { enabled: true } },
|
||||
schedule: { number: '3', unit: 'm' },
|
||||
'service.name': '',
|
||||
config_id: '',
|
||||
tags: [],
|
||||
timeout: '16',
|
||||
name: 'Monitor at ' + moment().format('LTS'),
|
||||
locations: [
|
||||
{ id: 'us_central_staging', label: 'US Central Staging', isServiceManaged: true },
|
||||
{ id: 'us_central', label: 'North America - US Central', isServiceManaged: true },
|
||||
{ id: 'us_central_qa', label: 'US Central QA', isServiceManaged: true },
|
||||
],
|
||||
namespace: 'default',
|
||||
origin: 'ui',
|
||||
journey_id: '',
|
||||
hash: '',
|
||||
id: '',
|
||||
params: '',
|
||||
max_attempts: 2,
|
||||
revision: 1,
|
||||
__ui: { is_tls_enabled: false },
|
||||
urls: 'https://www.google.com',
|
||||
max_redirects: '0',
|
||||
'url.port': null,
|
||||
password: '',
|
||||
proxy_url: '',
|
||||
proxy_headers: {},
|
||||
'check.response.body.negative': [],
|
||||
'check.response.body.positive': isDown ? ["i don't exist"] : [],
|
||||
'check.response.json': [],
|
||||
'response.include_body': 'on_error',
|
||||
'check.response.headers': {},
|
||||
'response.include_headers': true,
|
||||
'check.response.status': [],
|
||||
'check.request.body': { type: 'text', value: '' },
|
||||
'check.request.headers': {},
|
||||
'check.request.method': 'GET',
|
||||
username: '',
|
||||
mode: 'any',
|
||||
'response.include_body_max_bytes': '1024',
|
||||
ipv4: true,
|
||||
ipv6: true,
|
||||
'ssl.certificate_authorities': '',
|
||||
'ssl.certificate': '',
|
||||
'ssl.key': '',
|
||||
'ssl.key_passphrase': '',
|
||||
'ssl.verification_mode': 'full',
|
||||
'ssl.supported_protocols': ['TLSv1.1', 'TLSv1.2', 'TLSv1.3'],
|
||||
};
|
||||
};
|
|
@ -13,6 +13,9 @@ import {
|
|||
} from '../../common/runtime_types/alert_rules/common';
|
||||
|
||||
const dateFormat = 'MMM D, YYYY @ HH:mm:ss.SSS';
|
||||
const monitorName = 'test-monitor';
|
||||
const monitorId = '12345';
|
||||
const configId = '56789';
|
||||
|
||||
describe('updateState', () => {
|
||||
let spy: jest.SpyInstance<string, []>;
|
||||
|
@ -190,7 +193,6 @@ describe('updateState', () => {
|
|||
describe('setRecoveredAlertsContext', () => {
|
||||
const alertUuid = 'alert-id';
|
||||
const location = 'us_west';
|
||||
const configId = '12345';
|
||||
const idWithLocation = `${configId}-${location}`;
|
||||
const basePath = {
|
||||
publicBaseUrl: 'https://localhost:5601',
|
||||
|
@ -210,10 +212,19 @@ describe('setRecoveredAlertsContext', () => {
|
|||
},
|
||||
},
|
||||
monitor: {
|
||||
name: 'test-monitor',
|
||||
name: monitorName,
|
||||
},
|
||||
observer: {
|
||||
geo: {
|
||||
name: location,
|
||||
},
|
||||
},
|
||||
} as StaleDownConfig['ping'],
|
||||
timestamp: new Date().toISOString(),
|
||||
checks: {
|
||||
downWithinXChecks: 1,
|
||||
down: 0,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -227,20 +238,23 @@ describe('setRecoveredAlertsContext', () => {
|
|||
alert: {
|
||||
getUuid: () => alertUuid,
|
||||
getId: () => idWithLocation,
|
||||
getState: () => ({}),
|
||||
getState: () => ({
|
||||
downThreshold: 1,
|
||||
}),
|
||||
setContext: jest.fn(),
|
||||
},
|
||||
hit: {
|
||||
'kibana.alert.instance.id': idWithLocation,
|
||||
'location.id': location,
|
||||
configId,
|
||||
downThreshold: 1,
|
||||
},
|
||||
},
|
||||
]),
|
||||
setAlertData: jest.fn(),
|
||||
isTrackedAlert: jest.fn(),
|
||||
};
|
||||
const staleDownConfigs: Record<string, StaleDownConfig> = {
|
||||
const staleDownConfigs: AlertOverviewStatus['staleDownConfigs'] = {
|
||||
[idWithLocation]: {
|
||||
configId,
|
||||
monitorQueryId: 'stale-config',
|
||||
|
@ -252,11 +266,20 @@ describe('setRecoveredAlertsContext', () => {
|
|||
id: '123456',
|
||||
},
|
||||
monitor: {
|
||||
name: 'test-monitor',
|
||||
name: monitorName,
|
||||
},
|
||||
observer: {
|
||||
geo: {
|
||||
name: location,
|
||||
},
|
||||
},
|
||||
} as StaleDownConfig['ping'],
|
||||
timestamp: new Date().toISOString(),
|
||||
isDeleted: true,
|
||||
checks: {
|
||||
downWithinXChecks: 1,
|
||||
down: 1,
|
||||
},
|
||||
},
|
||||
};
|
||||
setRecoveredAlertsContext({
|
||||
|
@ -267,26 +290,30 @@ describe('setRecoveredAlertsContext', () => {
|
|||
upConfigs: {},
|
||||
dateFormat,
|
||||
tz: 'UTC',
|
||||
groupByLocation: true,
|
||||
});
|
||||
expect(alertsClientMock.setAlertData).toBeCalledWith({
|
||||
id: idWithLocation,
|
||||
context: {
|
||||
checkedAt: 'Feb 26, 2023 @ 00:00:00.000',
|
||||
configId: '12345',
|
||||
configId,
|
||||
linkMessage: '',
|
||||
alertDetailsUrl: 'https://localhost:5601/app/observability/alerts/alert-id',
|
||||
monitorName: 'test-monitor',
|
||||
recoveryReason: 'the monitor has been deleted',
|
||||
'kibana.alert.reason': 'the monitor has been deleted',
|
||||
monitorName,
|
||||
recoveryReason: 'has been deleted',
|
||||
recoveryStatus: 'has been deleted',
|
||||
monitorUrl: '(unavailable)',
|
||||
monitorUrlLabel: 'URL',
|
||||
reason:
|
||||
'Monitor "test-monitor" from Unnamed-location is recovered. Checked at February 25, 2023 7:00 PM.',
|
||||
'Monitor "test-monitor" from us_west is recovered. Alert when 1 out of the last 1 checks are down from at least 1 location.',
|
||||
stateId: '123456',
|
||||
status: 'recovered',
|
||||
locationId: location,
|
||||
locationNames: location,
|
||||
locationName: location,
|
||||
idWithLocation,
|
||||
timestamp: '2023-02-26T00:00:00.000Z',
|
||||
downThreshold: 1,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
@ -301,7 +328,9 @@ describe('setRecoveredAlertsContext', () => {
|
|||
alert: {
|
||||
getUuid: () => alertUuid,
|
||||
getId: () => idWithLocation,
|
||||
getState: () => ({}),
|
||||
getState: () => ({
|
||||
downThreshold: 1,
|
||||
}),
|
||||
setContext: jest.fn(),
|
||||
},
|
||||
hit: {
|
||||
|
@ -314,7 +343,7 @@ describe('setRecoveredAlertsContext', () => {
|
|||
setAlertData: jest.fn(),
|
||||
isTrackedAlert: jest.fn(),
|
||||
};
|
||||
const staleDownConfigs: Record<string, StaleDownConfig> = {
|
||||
const staleDownConfigs: AlertOverviewStatus['staleDownConfigs'] = {
|
||||
[idWithLocation]: {
|
||||
configId,
|
||||
monitorQueryId: 'stale-config',
|
||||
|
@ -328,9 +357,18 @@ describe('setRecoveredAlertsContext', () => {
|
|||
monitor: {
|
||||
name: 'test-monitor',
|
||||
},
|
||||
observer: {
|
||||
geo: {
|
||||
name: 'us_west',
|
||||
},
|
||||
},
|
||||
} as StaleDownConfig['ping'],
|
||||
timestamp: new Date().toISOString(),
|
||||
isLocationRemoved: true,
|
||||
checks: {
|
||||
downWithinXChecks: 1,
|
||||
down: 1,
|
||||
},
|
||||
},
|
||||
};
|
||||
setRecoveredAlertsContext({
|
||||
|
@ -341,26 +379,30 @@ describe('setRecoveredAlertsContext', () => {
|
|||
upConfigs: {},
|
||||
dateFormat,
|
||||
tz: 'UTC',
|
||||
groupByLocation: true,
|
||||
});
|
||||
expect(alertsClientMock.setAlertData).toBeCalledWith({
|
||||
id: idWithLocation,
|
||||
context: {
|
||||
configId: '12345',
|
||||
configId,
|
||||
checkedAt: 'Feb 26, 2023 @ 00:00:00.000',
|
||||
monitorUrl: '(unavailable)',
|
||||
reason:
|
||||
'Monitor "test-monitor" from Unnamed-location is recovered. Checked at February 25, 2023 7:00 PM.',
|
||||
idWithLocation,
|
||||
linkMessage: '',
|
||||
alertDetailsUrl: 'https://localhost:5601/app/observability/alerts/alert-id',
|
||||
monitorName: 'test-monitor',
|
||||
monitorName,
|
||||
recoveryReason: 'this location has been removed from the monitor',
|
||||
'kibana.alert.reason': 'this location has been removed from the monitor',
|
||||
recoveryStatus: 'has recovered',
|
||||
stateId: '123456',
|
||||
status: 'recovered',
|
||||
monitorUrlLabel: 'URL',
|
||||
idWithLocation,
|
||||
timestamp: '2023-02-26T00:00:00.000Z',
|
||||
locationName: location,
|
||||
locationNames: location,
|
||||
reason:
|
||||
'Monitor "test-monitor" from us_west is recovered. Alert when 1 out of the last 1 checks are down from at least 1 location.',
|
||||
locationId: location,
|
||||
downThreshold: 1,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
@ -375,7 +417,9 @@ describe('setRecoveredAlertsContext', () => {
|
|||
alert: {
|
||||
getId: () => idWithLocation,
|
||||
getUuid: () => alertUuid,
|
||||
getState: () => ({}),
|
||||
getState: () => ({
|
||||
downThreshold: 1,
|
||||
}),
|
||||
setContext: jest.fn(),
|
||||
},
|
||||
hit: {
|
||||
|
@ -388,7 +432,7 @@ describe('setRecoveredAlertsContext', () => {
|
|||
setAlertData: jest.fn(),
|
||||
isTrackedAlert: jest.fn(),
|
||||
};
|
||||
const staleDownConfigs: Record<string, StaleDownConfig> = {
|
||||
const staleDownConfigs: AlertOverviewStatus['staleDownConfigs'] = {
|
||||
[idWithLocation]: {
|
||||
configId,
|
||||
monitorQueryId: 'stale-config',
|
||||
|
@ -405,6 +449,10 @@ describe('setRecoveredAlertsContext', () => {
|
|||
} as StaleDownConfig['ping'],
|
||||
timestamp: new Date().toISOString(),
|
||||
isLocationRemoved: true,
|
||||
checks: {
|
||||
downWithinXChecks: 1,
|
||||
down: 1,
|
||||
},
|
||||
},
|
||||
};
|
||||
setRecoveredAlertsContext({
|
||||
|
@ -415,6 +463,7 @@ describe('setRecoveredAlertsContext', () => {
|
|||
upConfigs,
|
||||
dateFormat,
|
||||
tz: 'UTC',
|
||||
groupByLocation: true,
|
||||
});
|
||||
expect(alertsClientMock.setAlertData).toBeCalledWith({
|
||||
id: idWithLocation,
|
||||
|
@ -422,22 +471,250 @@ describe('setRecoveredAlertsContext', () => {
|
|||
configId,
|
||||
idWithLocation,
|
||||
alertDetailsUrl: 'https://localhost:5601/app/observability/alerts/alert-id',
|
||||
monitorName: 'test-monitor',
|
||||
monitorName,
|
||||
status: 'up',
|
||||
recoveryReason:
|
||||
'the monitor is now up again. It ran successfully at Feb 26, 2023 @ 00:00:00.000',
|
||||
'kibana.alert.reason':
|
||||
'the monitor is now up again. It ran successfully at Feb 26, 2023 @ 00:00:00.000',
|
||||
recoveryStatus: 'is now up',
|
||||
locationId: location,
|
||||
locationNames: location,
|
||||
locationName: location,
|
||||
checkedAt: 'Feb 26, 2023 @ 00:00:00.000',
|
||||
linkMessage:
|
||||
'- Link: https://localhost:5601/app/synthetics/monitor/12345/errors/123456?locationId=us_west',
|
||||
linkMessage: `- Link: https://localhost:5601/app/synthetics/monitor/${configId}/errors/123456?locationId=us_west`,
|
||||
monitorUrl: '(unavailable)',
|
||||
monitorUrlLabel: 'URL',
|
||||
reason:
|
||||
'Monitor "test-monitor" from Unnamed-location is recovered. Checked at February 25, 2023 7:00 PM.',
|
||||
stateId: null,
|
||||
'Monitor "test-monitor" from us_west is recovered. Alert when 1 out of the last 1 checks are down from at least 1 location.',
|
||||
timestamp: '2023-02-26T00:00:00.000Z',
|
||||
downThreshold: 1,
|
||||
stateId: '123456',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('sets the correct default recovery summary', () => {
|
||||
const alertsClientMock = {
|
||||
report: jest.fn(),
|
||||
getAlertLimitValue: jest.fn().mockReturnValue(10),
|
||||
setAlertLimitReached: jest.fn(),
|
||||
getRecoveredAlerts: jest.fn().mockReturnValue([
|
||||
{
|
||||
alert: {
|
||||
getId: () => idWithLocation,
|
||||
getUuid: () => alertUuid,
|
||||
getState: () => ({
|
||||
downThreshold: 1,
|
||||
}),
|
||||
setContext: jest.fn(),
|
||||
},
|
||||
hit: {
|
||||
'kibana.alert.instance.id': idWithLocation,
|
||||
'location.id': location,
|
||||
'monitor.name': monitorName,
|
||||
'monitor.id': monitorId,
|
||||
'@timestamp': new Date().toISOString(),
|
||||
'agent.name': 'test-host',
|
||||
'observer.geo.name': 'Unnamed-location',
|
||||
'observer.name.keyword': 'Unnamed-location-id',
|
||||
'monitor.type': 'HTTP',
|
||||
'error.message': 'test-error-message',
|
||||
configId,
|
||||
},
|
||||
},
|
||||
]),
|
||||
setAlertData: jest.fn(),
|
||||
isTrackedAlert: jest.fn(),
|
||||
};
|
||||
const staleDownConfigs: AlertOverviewStatus['staleDownConfigs'] = {};
|
||||
setRecoveredAlertsContext({
|
||||
alertsClient: alertsClientMock,
|
||||
basePath,
|
||||
spaceId: 'default',
|
||||
staleDownConfigs,
|
||||
upConfigs: {},
|
||||
dateFormat,
|
||||
tz: 'UTC',
|
||||
groupByLocation: true,
|
||||
});
|
||||
expect(alertsClientMock.setAlertData).toBeCalledWith({
|
||||
id: idWithLocation,
|
||||
context: {
|
||||
configId,
|
||||
idWithLocation,
|
||||
alertDetailsUrl: 'https://localhost:5601/app/observability/alerts/alert-id',
|
||||
monitorName,
|
||||
monitorId,
|
||||
status: 'recovered',
|
||||
recoveryReason: 'the alert condition is no longer met',
|
||||
recoveryStatus: 'has recovered',
|
||||
locationId: location,
|
||||
checkedAt: 'Feb 26, 2023 @ 00:00:00.000',
|
||||
linkMessage: '',
|
||||
monitorUrl: '(unavailable)',
|
||||
monitorUrlLabel: 'URL',
|
||||
reason:
|
||||
'Monitor "test-monitor" from Unnamed-location is recovered. Alert when 1 out of the last 1 checks are down from at least 1 location.',
|
||||
timestamp: '2023-02-26T00:00:00.000Z',
|
||||
downThreshold: 1,
|
||||
locationNames: 'Unnamed-location',
|
||||
locationName: 'Unnamed-location',
|
||||
lastErrorMessage: 'test-error-message',
|
||||
monitorType: 'HTTP',
|
||||
hostName: 'test-host',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('sets the recovery summary for recovered custom alerts', () => {
|
||||
const alertsClientMock = {
|
||||
report: jest.fn(),
|
||||
getAlertLimitValue: jest.fn().mockReturnValue(10),
|
||||
setAlertLimitReached: jest.fn(),
|
||||
getRecoveredAlerts: jest.fn().mockReturnValue([
|
||||
{
|
||||
alert: {
|
||||
getId: () => idWithLocation,
|
||||
getUuid: () => alertUuid,
|
||||
getState: () => ({
|
||||
downThreshold: 1,
|
||||
configId,
|
||||
}),
|
||||
setContext: jest.fn(),
|
||||
},
|
||||
hit: {
|
||||
'kibana.alert.instance.id': idWithLocation,
|
||||
'location.id': ['us_central', 'us_west'],
|
||||
'monitor.name': monitorName,
|
||||
'monitor.id': monitorId,
|
||||
'monitor.type': 'HTTP',
|
||||
'monitor.state.id': '123456',
|
||||
'@timestamp': new Date().toISOString(),
|
||||
'observer.geo.name': ['us-central', 'us-east'],
|
||||
'error.message': 'test-error-message',
|
||||
'url.full': 'http://test_url.com',
|
||||
configId,
|
||||
'agent.name': 'test-agent',
|
||||
},
|
||||
},
|
||||
]),
|
||||
setAlertData: jest.fn(),
|
||||
isTrackedAlert: jest.fn(),
|
||||
};
|
||||
const staleDownConfigs: AlertOverviewStatus['staleDownConfigs'] = {};
|
||||
setRecoveredAlertsContext({
|
||||
alertsClient: alertsClientMock,
|
||||
basePath,
|
||||
spaceId: 'default',
|
||||
staleDownConfigs,
|
||||
upConfigs: {},
|
||||
dateFormat,
|
||||
tz: 'UTC',
|
||||
groupByLocation: true,
|
||||
});
|
||||
expect(alertsClientMock.setAlertData).toBeCalledWith({
|
||||
id: idWithLocation,
|
||||
context: {
|
||||
configId,
|
||||
idWithLocation,
|
||||
alertDetailsUrl: 'https://localhost:5601/app/observability/alerts/alert-id',
|
||||
monitorName,
|
||||
monitorId,
|
||||
status: 'recovered',
|
||||
recoveryReason: 'the alert condition is no longer met',
|
||||
recoveryStatus: 'has recovered',
|
||||
locationId: 'us_central and us_west',
|
||||
checkedAt: 'Feb 26, 2023 @ 00:00:00.000',
|
||||
linkMessage:
|
||||
'- Link: https://localhost:5601/app/synthetics/monitor/56789/errors/123456?locationId=us_central',
|
||||
monitorUrl: 'http://test_url.com',
|
||||
hostName: 'test-agent',
|
||||
monitorUrlLabel: 'URL',
|
||||
reason:
|
||||
'Monitor "test-monitor" from us-central and us-east is recovered. Alert when 1 out of the last 1 checks are down from at least 1 location.',
|
||||
stateId: '123456',
|
||||
timestamp: '2023-02-26T00:00:00.000Z',
|
||||
downThreshold: 1,
|
||||
locationNames: 'us-central and us-east',
|
||||
locationName: 'us-central and us-east',
|
||||
monitorType: 'HTTP',
|
||||
lastErrorMessage: 'test-error-message',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('handles ungrouped recoveries', () => {
|
||||
const alertsClientMock = {
|
||||
report: jest.fn(),
|
||||
getAlertLimitValue: jest.fn().mockReturnValue(10),
|
||||
setAlertLimitReached: jest.fn(),
|
||||
getRecoveredAlerts: jest.fn().mockReturnValue([
|
||||
{
|
||||
alert: {
|
||||
getId: () => idWithLocation,
|
||||
getUuid: () => alertUuid,
|
||||
getState: () => ({
|
||||
downThreshold: 1,
|
||||
configId,
|
||||
}),
|
||||
setContext: jest.fn(),
|
||||
},
|
||||
hit: {
|
||||
'kibana.alert.instance.id': idWithLocation,
|
||||
'location.id': location,
|
||||
'monitor.name': monitorName,
|
||||
'monitor.type': 'HTTP',
|
||||
'monitor.id': monitorId,
|
||||
'agent.name': 'test-agent',
|
||||
'@timestamp': new Date().toISOString(),
|
||||
'observer.geo.name': ['us-central', 'us-east'],
|
||||
'error.message': 'test-error-message',
|
||||
'url.full': 'http://test_url.com',
|
||||
'monitor.state.id': '123456',
|
||||
configId,
|
||||
},
|
||||
},
|
||||
]),
|
||||
setAlertData: jest.fn(),
|
||||
isTrackedAlert: jest.fn(),
|
||||
};
|
||||
const staleDownConfigs: AlertOverviewStatus['staleDownConfigs'] = {};
|
||||
setRecoveredAlertsContext({
|
||||
alertsClient: alertsClientMock,
|
||||
basePath,
|
||||
spaceId: 'default',
|
||||
staleDownConfigs,
|
||||
upConfigs: {},
|
||||
dateFormat,
|
||||
tz: 'UTC',
|
||||
groupByLocation: false,
|
||||
});
|
||||
expect(alertsClientMock.setAlertData).toBeCalledWith({
|
||||
id: idWithLocation,
|
||||
context: {
|
||||
configId,
|
||||
idWithLocation,
|
||||
alertDetailsUrl: 'https://localhost:5601/app/observability/alerts/alert-id',
|
||||
monitorName,
|
||||
monitorId,
|
||||
status: 'recovered',
|
||||
recoveryReason: 'the alert condition is no longer met',
|
||||
recoveryStatus: 'has recovered',
|
||||
locationId: location,
|
||||
checkedAt: 'Feb 26, 2023 @ 00:00:00.000',
|
||||
linkMessage:
|
||||
'- Link: https://localhost:5601/app/synthetics/monitor/56789/errors/123456?locationId=us_west',
|
||||
monitorUrl: 'http://test_url.com',
|
||||
hostName: 'test-agent',
|
||||
monitorUrlLabel: 'URL',
|
||||
reason:
|
||||
'Monitor "test-monitor" from us-central and us-east is recovered. Alert when 1 out of the last 1 checks are down from at least 1 location.',
|
||||
stateId: '123456',
|
||||
timestamp: '2023-02-26T00:00:00.000Z',
|
||||
downThreshold: 1,
|
||||
locationNames: 'us-central and us-east',
|
||||
locationName: 'us-central and us-east',
|
||||
monitorType: 'HTTP',
|
||||
lastErrorMessage: 'test-error-message',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
|
@ -4,24 +4,26 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import moment, { Moment } from 'moment';
|
||||
import moment from 'moment';
|
||||
import { isRight } from 'fp-ts/lib/Either';
|
||||
import Mustache from 'mustache';
|
||||
import { IBasePath } from '@kbn/core/server';
|
||||
import {
|
||||
IRuleTypeAlerts,
|
||||
ActionGroupIdsOf,
|
||||
AlertInstanceContext as AlertContext,
|
||||
AlertInstanceState as AlertState,
|
||||
IRuleTypeAlerts,
|
||||
} from '@kbn/alerting-plugin/server';
|
||||
import { getAlertDetailsUrl } from '@kbn/observability-plugin/common';
|
||||
import { addSpaceIdToPath } from '@kbn/spaces-plugin/common';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query';
|
||||
import { legacyExperimentalFieldMap, ObservabilityUptimeAlert } from '@kbn/alerts-as-data-utils';
|
||||
import { PublicAlertsClient } from '@kbn/alerting-plugin/server/alerts_client/types';
|
||||
import { ALERT_REASON } from '@kbn/rule-data-utils';
|
||||
import { legacyExperimentalFieldMap } from '@kbn/alerts-as-data-utils';
|
||||
import {
|
||||
PublicAlertsClient,
|
||||
RecoveredAlertData,
|
||||
} from '@kbn/alerting-plugin/server/alerts_client/types';
|
||||
import { StatusRuleParams, TimeWindow } from '../../common/rules/status_rule';
|
||||
import { syntheticsRuleFieldMap } from '../../common/rules/synthetics_rule_field_map';
|
||||
import { combineFiltersAndUserSearch, stringifyKueries } from '../../common/lib';
|
||||
import {
|
||||
|
@ -29,17 +31,18 @@ import {
|
|||
SYNTHETICS_RULE_TYPES_ALERT_CONTEXT,
|
||||
} from '../../common/constants/synthetics_alerts';
|
||||
import { getUptimeIndexPattern, IndexPatternTitleAndFields } from '../queries/get_index_pattern';
|
||||
import { StatusCheckFilters } from '../../common/runtime_types';
|
||||
import { OverviewPing, StatusCheckFilters } from '../../common/runtime_types';
|
||||
import { SyntheticsEsClient } from '../lib';
|
||||
import { getMonitorSummary } from './status_rule/message_utils';
|
||||
import {
|
||||
AlertOverviewStatus,
|
||||
SyntheticsCommonState,
|
||||
SyntheticsCommonStateCodec,
|
||||
SyntheticsMonitorStatusAlertState,
|
||||
} from '../../common/runtime_types/alert_rules/common';
|
||||
import { getSyntheticsErrorRouteFromMonitorId } from '../../common/utils/get_synthetics_monitor_url';
|
||||
import { ALERT_DETAILS_URL, RECOVERY_REASON } from './action_variables';
|
||||
import type { MonitorSummaryStatusRule } from './status_rule/types';
|
||||
import type { MonitorStatusAlertDocument, MonitorSummaryStatusRule } from './status_rule/types';
|
||||
|
||||
export const updateState = (
|
||||
state: SyntheticsCommonState,
|
||||
|
@ -124,70 +127,58 @@ export const getRelativeViewInAppUrl = ({
|
|||
stateId: string;
|
||||
locationId: string;
|
||||
}) => {
|
||||
const relativeViewInAppUrl = getSyntheticsErrorRouteFromMonitorId({
|
||||
return getSyntheticsErrorRouteFromMonitorId({
|
||||
configId,
|
||||
stateId,
|
||||
locationId,
|
||||
});
|
||||
|
||||
return relativeViewInAppUrl;
|
||||
};
|
||||
|
||||
export const getErrorDuration = (startedAt: Moment, endsAt: Moment) => {
|
||||
const diffInDays = endsAt.diff(startedAt, 'days');
|
||||
if (diffInDays > 1) {
|
||||
return i18n.translate('xpack.synthetics.errorDetails.errorDuration.days', {
|
||||
defaultMessage: '{value} days',
|
||||
values: { value: diffInDays },
|
||||
});
|
||||
}
|
||||
const diffInHours = endsAt.diff(startedAt, 'hours');
|
||||
if (diffInHours > 1) {
|
||||
return i18n.translate('xpack.synthetics.errorDetails.errorDuration.hours', {
|
||||
defaultMessage: '{value} hours',
|
||||
values: { value: diffInHours },
|
||||
});
|
||||
}
|
||||
const diffInMinutes = endsAt.diff(startedAt, 'minutes');
|
||||
return i18n.translate('xpack.synthetics.errorDetails.errorDuration.mins', {
|
||||
defaultMessage: '{value} mins',
|
||||
values: { value: diffInMinutes },
|
||||
});
|
||||
};
|
||||
|
||||
export const setRecoveredAlertsContext = ({
|
||||
alertsClient,
|
||||
basePath,
|
||||
spaceId,
|
||||
staleDownConfigs,
|
||||
staleDownConfigs = {},
|
||||
upConfigs,
|
||||
dateFormat,
|
||||
tz,
|
||||
params,
|
||||
groupByLocation,
|
||||
}: {
|
||||
alertsClient: PublicAlertsClient<
|
||||
ObservabilityUptimeAlert,
|
||||
AlertState,
|
||||
MonitorStatusAlertDocument,
|
||||
SyntheticsMonitorStatusAlertState,
|
||||
AlertContext,
|
||||
ActionGroupIdsOf<MonitorStatusActionGroup>
|
||||
>;
|
||||
basePath?: IBasePath;
|
||||
spaceId?: string;
|
||||
params?: StatusRuleParams;
|
||||
staleDownConfigs: AlertOverviewStatus['staleDownConfigs'];
|
||||
upConfigs: AlertOverviewStatus['upConfigs'];
|
||||
dateFormat: string;
|
||||
tz: string;
|
||||
groupByLocation: boolean;
|
||||
}) => {
|
||||
const recoveredAlerts = alertsClient.getRecoveredAlerts() ?? [];
|
||||
for (const recoveredAlert of recoveredAlerts) {
|
||||
const recoveredAlertId = recoveredAlert.alert.getId();
|
||||
const alertUuid = recoveredAlert.alert.getUuid();
|
||||
|
||||
const state = recoveredAlert.alert.getState();
|
||||
const alertHit = recoveredAlert.hit;
|
||||
const locationId = alertHit?.['location.id'];
|
||||
const alertState = recoveredAlert.alert.getState();
|
||||
const configId = alertHit?.configId;
|
||||
const locationIds = alertHit?.['location.id'] ? [alertHit?.['location.id']].flat() : [];
|
||||
const locationName = alertHit?.['observer.geo.name']
|
||||
? [alertHit?.['observer.geo.name']].flat()
|
||||
: [];
|
||||
let syntheticsStateId = alertHit?.['monitor.state.id'];
|
||||
|
||||
let recoveryReason = '';
|
||||
let recoveryReason = i18n.translate(
|
||||
'xpack.synthetics.alerts.monitorStatus.defaultRecovery.reason',
|
||||
{
|
||||
defaultMessage: `the alert condition is no longer met`,
|
||||
}
|
||||
);
|
||||
let recoveryStatus = i18n.translate(
|
||||
'xpack.synthetics.alerts.monitorStatus.defaultRecovery.status',
|
||||
{
|
||||
|
@ -195,107 +186,94 @@ export const setRecoveredAlertsContext = ({
|
|||
}
|
||||
);
|
||||
let isUp = false;
|
||||
let linkMessage = '';
|
||||
let monitorSummary: MonitorSummaryStatusRule | null = null;
|
||||
let lastErrorMessage;
|
||||
let linkMessage = getDefaultLinkMessage({
|
||||
basePath,
|
||||
spaceId,
|
||||
syntheticsStateId,
|
||||
configId,
|
||||
locationId: locationIds[0],
|
||||
});
|
||||
let monitorSummary: MonitorSummaryStatusRule | undefined = getDefaultRecoveredSummary({
|
||||
recoveredAlert,
|
||||
tz,
|
||||
dateFormat,
|
||||
params,
|
||||
});
|
||||
let lastErrorMessage = alertHit?.['error.message'];
|
||||
|
||||
if (recoveredAlertId && locationId && staleDownConfigs[recoveredAlertId]) {
|
||||
const downConfig = staleDownConfigs[recoveredAlertId];
|
||||
const { ping } = downConfig;
|
||||
monitorSummary = getMonitorSummary(
|
||||
ping,
|
||||
RECOVERED_LABEL,
|
||||
locationId,
|
||||
downConfig.configId,
|
||||
dateFormat,
|
||||
tz
|
||||
);
|
||||
lastErrorMessage = monitorSummary.lastErrorMessage;
|
||||
|
||||
if (downConfig.isDeleted) {
|
||||
recoveryStatus = i18n.translate(
|
||||
'xpack.synthetics.alerts.monitorStatus.deleteMonitor.status',
|
||||
{
|
||||
defaultMessage: `has been deleted`,
|
||||
}
|
||||
);
|
||||
recoveryReason = i18n.translate(
|
||||
'xpack.synthetics.alerts.monitorStatus.deleteMonitor.reason',
|
||||
{
|
||||
defaultMessage: `the monitor has been deleted`,
|
||||
}
|
||||
);
|
||||
} else if (downConfig.isLocationRemoved) {
|
||||
recoveryStatus = i18n.translate(
|
||||
'xpack.synthetics.alerts.monitorStatus.removedLocation.status',
|
||||
{
|
||||
defaultMessage: `has recovered`,
|
||||
}
|
||||
);
|
||||
recoveryReason = i18n.translate(
|
||||
'xpack.synthetics.alerts.monitorStatus.removedLocation.reason',
|
||||
{
|
||||
defaultMessage: `this location has been removed from the monitor`,
|
||||
}
|
||||
);
|
||||
}
|
||||
if (!groupByLocation && monitorSummary) {
|
||||
const formattedLocationNames = locationName.join(` ${AND_LABEL} `);
|
||||
const formattedLocationIds = locationIds.join(` ${AND_LABEL} `);
|
||||
monitorSummary.locationNames = formattedLocationNames;
|
||||
monitorSummary.locationName = formattedLocationNames;
|
||||
monitorSummary.locationId = formattedLocationIds;
|
||||
}
|
||||
|
||||
if (configId && recoveredAlertId && locationId && upConfigs[recoveredAlertId]) {
|
||||
// pull the last error from state, since it is not available on the up ping
|
||||
lastErrorMessage = alertHit?.['error.message'];
|
||||
|
||||
const upConfig = upConfigs[recoveredAlertId];
|
||||
isUp = Boolean(upConfig) || false;
|
||||
const ping = upConfig.ping;
|
||||
|
||||
monitorSummary = getMonitorSummary(
|
||||
ping,
|
||||
RECOVERED_LABEL,
|
||||
locationId,
|
||||
configId,
|
||||
if (recoveredAlertId && locationIds && staleDownConfigs[recoveredAlertId]) {
|
||||
const summary = getDeletedMonitorOrLocationSummary({
|
||||
staleDownConfigs,
|
||||
recoveredAlertId,
|
||||
locationIds,
|
||||
dateFormat,
|
||||
tz
|
||||
);
|
||||
|
||||
// When alert is flapping, the stateId is not available on ping.state.ends.id, use state instead
|
||||
const stateId = ping.state?.ends?.id || state.stateId;
|
||||
const upTimestamp = ping['@timestamp'];
|
||||
const checkedAt = moment(upTimestamp).tz(tz).format(dateFormat);
|
||||
recoveryStatus = i18n.translate('xpack.synthetics.alerts.monitorStatus.upCheck.status', {
|
||||
defaultMessage: `is now up`,
|
||||
tz,
|
||||
params,
|
||||
});
|
||||
recoveryReason = i18n.translate(
|
||||
'xpack.synthetics.alerts.monitorStatus.upCheck.reasonWithoutDuration',
|
||||
{
|
||||
defaultMessage: `the monitor is now up again. It ran successfully at {checkedAt}`,
|
||||
values: {
|
||||
checkedAt,
|
||||
},
|
||||
}
|
||||
);
|
||||
if (summary) {
|
||||
monitorSummary = {
|
||||
...monitorSummary,
|
||||
...summary.monitorSummary,
|
||||
};
|
||||
recoveryStatus = summary.recoveryStatus;
|
||||
recoveryReason = summary.recoveryReason;
|
||||
lastErrorMessage = summary.lastErrorMessage;
|
||||
syntheticsStateId = summary.stateId ? summary.stateId : syntheticsStateId;
|
||||
}
|
||||
// Cannot display link message for deleted monitors or deleted locations
|
||||
linkMessage = '';
|
||||
}
|
||||
|
||||
if (basePath && spaceId && stateId) {
|
||||
const relativeViewInAppUrl = getRelativeViewInAppUrl({
|
||||
configId,
|
||||
locationId,
|
||||
stateId,
|
||||
});
|
||||
linkMessage = getFullViewInAppMessage(basePath, spaceId, relativeViewInAppUrl);
|
||||
if (configId && recoveredAlertId && locationIds && upConfigs[recoveredAlertId]) {
|
||||
const summary = getUpMonitorRecoverySummary({
|
||||
upConfigs,
|
||||
recoveredAlertId,
|
||||
alertHit,
|
||||
locationIds,
|
||||
configId,
|
||||
basePath,
|
||||
spaceId,
|
||||
dateFormat,
|
||||
tz,
|
||||
params,
|
||||
});
|
||||
if (summary) {
|
||||
monitorSummary = {
|
||||
...monitorSummary,
|
||||
...summary.monitorSummary,
|
||||
};
|
||||
recoveryStatus = summary.recoveryStatus;
|
||||
recoveryReason = summary.recoveryReason;
|
||||
isUp = summary.isUp;
|
||||
lastErrorMessage = summary.lastErrorMessage;
|
||||
linkMessage = summary.linkMessage ? summary.linkMessage : linkMessage;
|
||||
syntheticsStateId = summary.stateId ? summary.stateId : syntheticsStateId;
|
||||
}
|
||||
}
|
||||
|
||||
const context = {
|
||||
...state,
|
||||
...alertState,
|
||||
...(monitorSummary ? monitorSummary : {}),
|
||||
locationId,
|
||||
locationId: locationIds.join(` ${AND_LABEL} `),
|
||||
idWithLocation: recoveredAlertId,
|
||||
lastErrorMessage,
|
||||
recoveryStatus,
|
||||
linkMessage,
|
||||
stateId: syntheticsStateId,
|
||||
...(isUp ? { status: 'up' } : {}),
|
||||
...(recoveryReason ? { [RECOVERY_REASON]: recoveryReason } : {}),
|
||||
...(recoveryReason ? { [ALERT_REASON]: recoveryReason } : {}),
|
||||
...(recoveryReason
|
||||
? {
|
||||
[RECOVERY_REASON]: recoveryReason,
|
||||
}
|
||||
: {}),
|
||||
...(basePath && spaceId && alertUuid
|
||||
? { [ALERT_DETAILS_URL]: getAlertDetailsUrl(basePath, spaceId, alertUuid) }
|
||||
: {}),
|
||||
|
@ -304,6 +282,220 @@ export const setRecoveredAlertsContext = ({
|
|||
}
|
||||
};
|
||||
|
||||
export const getDefaultLinkMessage = ({
|
||||
basePath,
|
||||
spaceId,
|
||||
syntheticsStateId,
|
||||
configId,
|
||||
locationId,
|
||||
}: {
|
||||
basePath?: IBasePath;
|
||||
spaceId?: string;
|
||||
syntheticsStateId?: string;
|
||||
configId?: string;
|
||||
locationId?: string;
|
||||
}) => {
|
||||
if (basePath && spaceId && syntheticsStateId && configId && locationId) {
|
||||
const relativeViewInAppUrl = getRelativeViewInAppUrl({
|
||||
configId,
|
||||
locationId,
|
||||
stateId: syntheticsStateId,
|
||||
});
|
||||
return getFullViewInAppMessage(basePath, spaceId, relativeViewInAppUrl);
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
export const getDefaultRecoveredSummary = ({
|
||||
recoveredAlert,
|
||||
tz,
|
||||
dateFormat,
|
||||
params,
|
||||
}: {
|
||||
recoveredAlert: RecoveredAlertData<
|
||||
MonitorStatusAlertDocument,
|
||||
AlertState,
|
||||
AlertContext,
|
||||
ActionGroupIdsOf<MonitorStatusActionGroup>
|
||||
>;
|
||||
tz: string;
|
||||
dateFormat: string;
|
||||
params?: StatusRuleParams;
|
||||
}) => {
|
||||
if (!recoveredAlert.hit) return; // TODO: handle this case
|
||||
const hit = recoveredAlert.hit;
|
||||
const locationId = hit['location.id'];
|
||||
const configId = hit.configId;
|
||||
return getMonitorSummary({
|
||||
monitorInfo: {
|
||||
monitor: {
|
||||
id: hit['monitor.id'],
|
||||
name: hit['monitor.name'],
|
||||
type: hit['monitor.type'],
|
||||
},
|
||||
config_id: configId,
|
||||
observer: {
|
||||
geo: {
|
||||
name: hit['observer.geo.name'] || hit['location.name'],
|
||||
},
|
||||
name: locationId,
|
||||
},
|
||||
agent: {
|
||||
name: hit['agent.name'] || '',
|
||||
},
|
||||
'@timestamp': String(hit['@timestamp']),
|
||||
...(hit['error.message'] ? { error: { message: hit['error.message'] } } : {}),
|
||||
...(hit['url.full'] ? { url: { full: hit['url.full'] } } : {}),
|
||||
} as unknown as OverviewPing,
|
||||
statusMessage: RECOVERED_LABEL,
|
||||
locationId,
|
||||
configId,
|
||||
dateFormat,
|
||||
tz,
|
||||
params,
|
||||
});
|
||||
};
|
||||
|
||||
export const getDeletedMonitorOrLocationSummary = ({
|
||||
staleDownConfigs,
|
||||
recoveredAlertId,
|
||||
locationIds,
|
||||
dateFormat,
|
||||
tz,
|
||||
params,
|
||||
}: {
|
||||
staleDownConfigs: AlertOverviewStatus['staleDownConfigs'];
|
||||
recoveredAlertId: string;
|
||||
locationIds: string[];
|
||||
dateFormat: string;
|
||||
tz: string;
|
||||
params?: StatusRuleParams;
|
||||
}) => {
|
||||
const downConfig = staleDownConfigs[recoveredAlertId];
|
||||
const { ping } = downConfig;
|
||||
const monitorSummary = getMonitorSummary({
|
||||
monitorInfo: ping,
|
||||
statusMessage: RECOVERED_LABEL,
|
||||
locationId: locationIds,
|
||||
configId: downConfig.configId,
|
||||
dateFormat,
|
||||
tz,
|
||||
params,
|
||||
});
|
||||
const lastErrorMessage = monitorSummary.lastErrorMessage;
|
||||
|
||||
if (downConfig.isDeleted) {
|
||||
return {
|
||||
lastErrorMessage,
|
||||
monitorSummary,
|
||||
stateId: ping.state?.id,
|
||||
recoveryStatus: i18n.translate('xpack.synthetics.alerts.monitorStatus.deleteMonitor.status', {
|
||||
defaultMessage: `has been deleted`,
|
||||
}),
|
||||
recoveryReason: i18n.translate('xpack.synthetics.alerts.monitorStatus.deleteMonitor.status', {
|
||||
defaultMessage: `has been deleted`,
|
||||
}),
|
||||
};
|
||||
} else if (downConfig.isLocationRemoved) {
|
||||
return {
|
||||
monitorSummary,
|
||||
lastErrorMessage,
|
||||
stateId: ping.state?.id,
|
||||
recoveryStatus: i18n.translate(
|
||||
'xpack.synthetics.alerts.monitorStatus.removedLocation.status',
|
||||
{
|
||||
defaultMessage: `has recovered`,
|
||||
}
|
||||
),
|
||||
recoveryReason: i18n.translate(
|
||||
'xpack.synthetics.alerts.monitorStatus.removedLocation.reason',
|
||||
{
|
||||
defaultMessage: `this location has been removed from the monitor`,
|
||||
}
|
||||
),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const getUpMonitorRecoverySummary = ({
|
||||
upConfigs,
|
||||
recoveredAlertId,
|
||||
alertHit,
|
||||
locationIds,
|
||||
configId,
|
||||
basePath,
|
||||
spaceId,
|
||||
dateFormat,
|
||||
tz,
|
||||
params,
|
||||
}: {
|
||||
upConfigs: AlertOverviewStatus['upConfigs'];
|
||||
recoveredAlertId: string;
|
||||
alertHit: any;
|
||||
locationIds: string[];
|
||||
configId: string;
|
||||
basePath?: IBasePath;
|
||||
spaceId?: string;
|
||||
dateFormat: string;
|
||||
tz: string;
|
||||
params?: StatusRuleParams;
|
||||
}) => {
|
||||
// pull the last error from state, since it is not available on the up ping
|
||||
const lastErrorMessage = alertHit?.['error.message'];
|
||||
let linkMessage = '';
|
||||
|
||||
const upConfig = upConfigs[recoveredAlertId];
|
||||
const isUp = Boolean(upConfig) || false;
|
||||
const ping = upConfig.ping;
|
||||
|
||||
const monitorSummary = getMonitorSummary({
|
||||
monitorInfo: ping,
|
||||
statusMessage: RECOVERED_LABEL,
|
||||
locationId: locationIds,
|
||||
configId,
|
||||
dateFormat,
|
||||
tz,
|
||||
params,
|
||||
});
|
||||
|
||||
// When alert is flapping, the stateId is not available on ping.state.ends.id, use state instead
|
||||
const stateId = ping.state?.ends?.id;
|
||||
const upTimestamp = ping['@timestamp'];
|
||||
const checkedAt = moment(upTimestamp).tz(tz).format(dateFormat);
|
||||
const recoveryStatus = i18n.translate('xpack.synthetics.alerts.monitorStatus.upCheck.status', {
|
||||
defaultMessage: `is now up`,
|
||||
});
|
||||
const recoveryReason = i18n.translate(
|
||||
'xpack.synthetics.alerts.monitorStatus.upCheck.reasonWithoutDuration',
|
||||
{
|
||||
defaultMessage: `the monitor is now up again. It ran successfully at {checkedAt}`,
|
||||
values: {
|
||||
checkedAt,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (basePath && spaceId && stateId) {
|
||||
const relativeViewInAppUrl = getRelativeViewInAppUrl({
|
||||
configId,
|
||||
locationId: locationIds[0],
|
||||
stateId,
|
||||
});
|
||||
linkMessage = getFullViewInAppMessage(basePath, spaceId, relativeViewInAppUrl);
|
||||
}
|
||||
|
||||
return {
|
||||
monitorSummary,
|
||||
lastErrorMessage,
|
||||
recoveryStatus,
|
||||
recoveryReason,
|
||||
isUp,
|
||||
linkMessage,
|
||||
stateId,
|
||||
};
|
||||
};
|
||||
|
||||
export const RECOVERED_LABEL = i18n.translate('xpack.synthetics.monitorStatus.recoveredLabel', {
|
||||
defaultMessage: 'recovered',
|
||||
});
|
||||
|
@ -355,9 +547,39 @@ export const syntheticsRuleTypeFieldMap = {
|
|||
...legacyExperimentalFieldMap,
|
||||
};
|
||||
|
||||
export const SyntheticsRuleTypeAlertDefinition: IRuleTypeAlerts<ObservabilityUptimeAlert> = {
|
||||
export const SyntheticsRuleTypeAlertDefinition: IRuleTypeAlerts<MonitorStatusAlertDocument> = {
|
||||
context: SYNTHETICS_RULE_TYPES_ALERT_CONTEXT,
|
||||
mappings: { fieldMap: syntheticsRuleTypeFieldMap },
|
||||
useLegacyAlerts: true,
|
||||
shouldWrite: true,
|
||||
};
|
||||
|
||||
export function getTimeUnitLabel(timeWindow: TimeWindow) {
|
||||
const { size: timeValue = 1, unit: timeUnit } = timeWindow;
|
||||
switch (timeUnit) {
|
||||
case 's':
|
||||
return i18n.translate('xpack.synthetics.timeUnits.secondLabel', {
|
||||
defaultMessage: '{timeValue, plural, one {second} other {seconds}}',
|
||||
values: { timeValue },
|
||||
});
|
||||
case 'm':
|
||||
return i18n.translate('xpack.synthetics.timeUnits.minuteLabel', {
|
||||
defaultMessage: '{timeValue, plural, one {minute} other {minutes}}',
|
||||
values: { timeValue },
|
||||
});
|
||||
case 'h':
|
||||
return i18n.translate('xpack.synthetics.timeUnits.hourLabel', {
|
||||
defaultMessage: '{timeValue, plural, one {hour} other {hours}}',
|
||||
values: { timeValue },
|
||||
});
|
||||
case 'd':
|
||||
return i18n.translate('xpack.synthetics.timeUnits.dayLabel', {
|
||||
defaultMessage: '{timeValue, plural, one {day} other {days}}',
|
||||
values: { timeValue },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const AND_LABEL = i18n.translate('xpack.synthetics.alerts.monitorStatus.andLabel', {
|
||||
defaultMessage: 'and',
|
||||
});
|
||||
|
|
|
@ -8,6 +8,9 @@
|
|||
import moment from 'moment';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ALERT_REASON } from '@kbn/rule-data-utils';
|
||||
import { AlertStatusMetaData } from '../../../common/runtime_types/alert_rules/common';
|
||||
import { getConditionType, StatusRuleParams } from '../../../common/rules/status_rule';
|
||||
import { AND_LABEL, getTimeUnitLabel } from '../common';
|
||||
import { ALERT_REASON_MSG } from '../action_variables';
|
||||
import { MonitorSummaryStatusRule } from './types';
|
||||
import {
|
||||
|
@ -15,6 +18,7 @@ import {
|
|||
MONITOR_TYPE,
|
||||
MONITOR_NAME,
|
||||
OBSERVER_GEO_NAME,
|
||||
OBSERVER_NAME,
|
||||
URL_FULL,
|
||||
ERROR_MESSAGE,
|
||||
AGENT_NAME,
|
||||
|
@ -23,33 +27,64 @@ import {
|
|||
import { OverviewPing } from '../../../common/runtime_types';
|
||||
import { UNNAMED_LOCATION } from '../../../common/constants';
|
||||
|
||||
export const getMonitorAlertDocument = (monitorSummary: MonitorSummaryStatusRule) => ({
|
||||
export const getMonitorAlertDocument = (
|
||||
monitorSummary: MonitorSummaryStatusRule,
|
||||
locationNames: string[],
|
||||
locationIds: string[],
|
||||
useLatestChecks: boolean
|
||||
) => ({
|
||||
[MONITOR_ID]: monitorSummary.monitorId,
|
||||
[MONITOR_TYPE]: monitorSummary.monitorType,
|
||||
[MONITOR_NAME]: monitorSummary.monitorName,
|
||||
[URL_FULL]: monitorSummary.monitorUrl,
|
||||
[OBSERVER_GEO_NAME]: monitorSummary.locationName,
|
||||
[OBSERVER_GEO_NAME]: locationNames,
|
||||
[OBSERVER_NAME]: locationIds,
|
||||
[ERROR_MESSAGE]: monitorSummary.lastErrorMessage,
|
||||
[AGENT_NAME]: monitorSummary.hostName,
|
||||
[ALERT_REASON]: monitorSummary.reason,
|
||||
[STATE_ID]: monitorSummary.stateId,
|
||||
'location.id': monitorSummary.locationId,
|
||||
'location.name': monitorSummary.locationName,
|
||||
'location.id': locationIds,
|
||||
'location.name': locationNames,
|
||||
configId: monitorSummary.configId,
|
||||
'kibana.alert.evaluation.threshold': monitorSummary.downThreshold,
|
||||
'kibana.alert.evaluation.value':
|
||||
(useLatestChecks ? monitorSummary.checks?.downWithinXChecks : monitorSummary.checks?.down) ?? 1,
|
||||
'monitor.tags': monitorSummary.monitorTags ?? [],
|
||||
});
|
||||
|
||||
export const getMonitorSummary = (
|
||||
monitorInfo: OverviewPing,
|
||||
statusMessage: string,
|
||||
locationId: string,
|
||||
configId: string,
|
||||
dateFormat: string,
|
||||
tz: string
|
||||
): MonitorSummaryStatusRule => {
|
||||
const monitorName = monitorInfo.monitor?.name ?? monitorInfo.monitor?.id;
|
||||
const observerLocation = monitorInfo.observer?.geo?.name ?? UNNAMED_LOCATION;
|
||||
const checkedAt = moment(monitorInfo['@timestamp']).tz(tz).format(dateFormat);
|
||||
export interface MonitorSummaryData {
|
||||
monitorInfo: OverviewPing;
|
||||
statusMessage: string;
|
||||
locationId: string[];
|
||||
configId: string;
|
||||
dateFormat: string;
|
||||
tz: string;
|
||||
checks?: {
|
||||
downWithinXChecks: number;
|
||||
down: number;
|
||||
};
|
||||
params?: StatusRuleParams;
|
||||
}
|
||||
|
||||
export const getMonitorSummary = ({
|
||||
monitorInfo,
|
||||
locationId,
|
||||
configId,
|
||||
tz,
|
||||
dateFormat,
|
||||
statusMessage,
|
||||
checks,
|
||||
params,
|
||||
}: MonitorSummaryData): MonitorSummaryStatusRule => {
|
||||
const { downThreshold } = getConditionType(params?.condition);
|
||||
const monitorName = monitorInfo?.monitor?.name ?? monitorInfo?.monitor?.id;
|
||||
const locationName = monitorInfo?.observer?.geo?.name ?? UNNAMED_LOCATION;
|
||||
const formattedLocationName = Array.isArray(locationName)
|
||||
? locationName.join(` ${AND_LABEL} `)
|
||||
: locationName;
|
||||
const checkedAt = moment(monitorInfo?.['@timestamp'])
|
||||
.tz(tz || 'UTC')
|
||||
.format(dateFormat);
|
||||
const typeToLabelMap: Record<string, string> = {
|
||||
http: 'HTTP',
|
||||
tcp: 'TCP',
|
||||
|
@ -65,11 +100,11 @@ export const getMonitorSummary = (
|
|||
browser: 'URL',
|
||||
};
|
||||
const monitorType = monitorInfo.monitor?.type;
|
||||
const stateId = monitorInfo.state?.id || null;
|
||||
const stateId = monitorInfo.state?.id;
|
||||
|
||||
return {
|
||||
checkedAt,
|
||||
locationId,
|
||||
locationId: locationId?.join?.(` ${AND_LABEL} `) ?? '',
|
||||
configId,
|
||||
monitorUrl: monitorInfo.url?.full || UNAVAILABLE_LABEL,
|
||||
monitorUrlLabel: typeToUrlLabelMap[monitorType] || 'URL',
|
||||
|
@ -77,40 +112,162 @@ export const getMonitorSummary = (
|
|||
monitorName,
|
||||
monitorType: typeToLabelMap[monitorInfo.monitor?.type] || monitorInfo.monitor?.type,
|
||||
lastErrorMessage: monitorInfo.error?.message!,
|
||||
locationName: monitorInfo.observer?.geo?.name!,
|
||||
locationName: formattedLocationName,
|
||||
locationNames: formattedLocationName,
|
||||
hostName: monitorInfo.agent?.name!,
|
||||
status: statusMessage,
|
||||
stateId,
|
||||
[ALERT_REASON_MSG]: getReasonMessage({
|
||||
name: monitorName,
|
||||
location: observerLocation,
|
||||
location: formattedLocationName,
|
||||
status: statusMessage,
|
||||
timestamp: monitorInfo['@timestamp'],
|
||||
checks,
|
||||
params,
|
||||
}),
|
||||
checks,
|
||||
downThreshold,
|
||||
timestamp: monitorInfo['@timestamp'],
|
||||
monitorTags: monitorInfo.tags,
|
||||
};
|
||||
};
|
||||
|
||||
export const getUngroupedReasonMessage = ({
|
||||
statusConfigs,
|
||||
monitorName,
|
||||
params,
|
||||
status = DOWN_LABEL,
|
||||
}: {
|
||||
statusConfigs: AlertStatusMetaData[];
|
||||
monitorName: string;
|
||||
params: StatusRuleParams;
|
||||
status?: string;
|
||||
checks?: {
|
||||
downWithinXChecks: number;
|
||||
down: number;
|
||||
};
|
||||
}) => {
|
||||
const { useLatestChecks, numberOfChecks, timeWindow, downThreshold, locationsThreshold } =
|
||||
getConditionType(params.condition);
|
||||
|
||||
return i18n.translate(
|
||||
'xpack.synthetics.alertRules.monitorStatus.reasonMessage.location.ungrouped.multiple',
|
||||
{
|
||||
defaultMessage: `Monitor "{name}" is {status} {locationDetails}. Alert when down {threshold} {threshold, plural, one {time} other {times}} {condition} from at least {locationsThreshold} {locationsThreshold, plural, one {location} other {locations}}.`,
|
||||
values: {
|
||||
name: monitorName,
|
||||
status,
|
||||
threshold: downThreshold,
|
||||
locationsThreshold,
|
||||
condition: useLatestChecks
|
||||
? i18n.translate(
|
||||
'xpack.synthetics.alertRules.monitorStatus.reasonMessage.condition.latestChecks',
|
||||
{
|
||||
defaultMessage: 'out of the last {numberOfChecks} checks',
|
||||
values: { numberOfChecks },
|
||||
}
|
||||
)
|
||||
: i18n.translate(
|
||||
'xpack.synthetics.alertRules.monitorStatus.reasonMessage.condition.timeWindow',
|
||||
{
|
||||
defaultMessage: 'within the last {time} {unit}',
|
||||
values: {
|
||||
time: timeWindow.size,
|
||||
unit: getTimeUnitLabel(timeWindow),
|
||||
},
|
||||
}
|
||||
),
|
||||
locationDetails: statusConfigs
|
||||
.map((c) => {
|
||||
return i18n.translate(
|
||||
'xpack.synthetics.alertRules.monitorStatus.reasonMessage.locationDetails',
|
||||
{
|
||||
defaultMessage:
|
||||
'{downCount} {downCount, plural, one {time} other {times}} from {locName}',
|
||||
values: {
|
||||
locName: c.ping.observer.geo?.name,
|
||||
downCount: useLatestChecks ? c.checks?.downWithinXChecks : c.checks?.down,
|
||||
},
|
||||
}
|
||||
);
|
||||
})
|
||||
.join(` ${AND_LABEL} `),
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const getReasonMessage = ({
|
||||
name,
|
||||
status,
|
||||
location,
|
||||
timestamp,
|
||||
checks,
|
||||
params,
|
||||
}: {
|
||||
name: string;
|
||||
location: string;
|
||||
status: string;
|
||||
timestamp: string;
|
||||
checks?: {
|
||||
downWithinXChecks: number;
|
||||
down: number;
|
||||
};
|
||||
params?: StatusRuleParams;
|
||||
}) => {
|
||||
const checkedAt = moment(timestamp).format('LLL');
|
||||
|
||||
return i18n.translate('xpack.synthetics.alertRules.monitorStatus.reasonMessage', {
|
||||
defaultMessage: `Monitor "{name}" from {location} is {status}. Checked at {checkedAt}.`,
|
||||
const { useTimeWindow, numberOfChecks, locationsThreshold, downThreshold } = getConditionType(
|
||||
params?.condition
|
||||
);
|
||||
if (useTimeWindow) {
|
||||
return getReasonMessageForTimeWindow({
|
||||
name,
|
||||
location,
|
||||
status,
|
||||
params,
|
||||
});
|
||||
}
|
||||
return i18n.translate('xpack.synthetics.alertRules.monitorStatus.reasonMessage.new', {
|
||||
defaultMessage: `Monitor "{name}" from {location} is {status}. {checksSummary}Alert when {downThreshold} out of the last {numberOfChecks} checks are down from at least {locationsThreshold} {locationsThreshold, plural, one {location} other {locations}}.`,
|
||||
values: {
|
||||
name,
|
||||
status,
|
||||
location,
|
||||
checkedAt,
|
||||
downThreshold,
|
||||
locationsThreshold,
|
||||
numberOfChecks,
|
||||
checksSummary: checks
|
||||
? i18n.translate('xpack.synthetics.alertRules.monitorStatus.reasonMessage.checksSummary', {
|
||||
defaultMessage:
|
||||
'Monitor is down {downChecks} {downChecks, plural, one {time} other {times}} within the last {numberOfChecks} checks. ',
|
||||
values: {
|
||||
downChecks: checks.downWithinXChecks,
|
||||
numberOfChecks,
|
||||
},
|
||||
})
|
||||
: '',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const getReasonMessageForTimeWindow = ({
|
||||
name,
|
||||
location,
|
||||
status = DOWN_LABEL,
|
||||
params,
|
||||
}: {
|
||||
name: string;
|
||||
location: string;
|
||||
status?: string;
|
||||
params?: StatusRuleParams;
|
||||
}) => {
|
||||
const { timeWindow, locationsThreshold, downThreshold } = getConditionType(params?.condition);
|
||||
return i18n.translate('xpack.synthetics.alertRules.monitorStatus.reasonMessage.timeBased', {
|
||||
defaultMessage: `Monitor "{name}" from {location} is {status}. Alert when {downThreshold} checks are down within the last {size} {unitLabel} from at least {locationsThreshold} {locationsThreshold, plural, one {location} other {locations}}.`,
|
||||
values: {
|
||||
name,
|
||||
status,
|
||||
location,
|
||||
downThreshold,
|
||||
unitLabel: getTimeUnitLabel(timeWindow),
|
||||
locationsThreshold,
|
||||
size: timeWindow.size,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
|
@ -7,25 +7,15 @@
|
|||
|
||||
import { DEFAULT_APP_CATEGORIES } from '@kbn/core/server';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { ActionGroupIdsOf } from '@kbn/alerting-plugin/common';
|
||||
import {
|
||||
GetViewInAppRelativeUrlFnOpts,
|
||||
AlertInstanceContext as AlertContext,
|
||||
RuleExecutorOptions,
|
||||
AlertsClientError,
|
||||
} from '@kbn/alerting-plugin/server';
|
||||
import { getAlertDetailsUrl, observabilityPaths } from '@kbn/observability-plugin/common';
|
||||
import { ObservabilityUptimeAlert } from '@kbn/alerts-as-data-utils';
|
||||
import { GetViewInAppRelativeUrlFnOpts, AlertsClientError } from '@kbn/alerting-plugin/server';
|
||||
import { observabilityPaths } from '@kbn/observability-plugin/common';
|
||||
import apm from 'elastic-apm-node';
|
||||
import { AlertOverviewStatus } from '../../../common/runtime_types/alert_rules/common';
|
||||
import { StatusRuleExecutorOptions } from './types';
|
||||
import { syntheticsRuleFieldMap } from '../../../common/rules/synthetics_rule_field_map';
|
||||
import { SyntheticsPluginsSetupDependencies, SyntheticsServerSetup } from '../../types';
|
||||
import { DOWN_LABEL, getMonitorAlertDocument, getMonitorSummary } from './message_utils';
|
||||
import {
|
||||
AlertOverviewStatus,
|
||||
SyntheticsCommonState,
|
||||
SyntheticsMonitorStatusAlertState,
|
||||
} from '../../../common/runtime_types/alert_rules/common';
|
||||
import { StatusRuleExecutor } from './status_rule_executor';
|
||||
import { StatusRulePramsSchema, StatusRuleParams } from '../../../common/rules/status_rule';
|
||||
import { StatusRulePramsSchema } from '../../../common/rules/status_rule';
|
||||
import {
|
||||
MONITOR_STATUS,
|
||||
SYNTHETICS_ALERT_RULE_TYPES,
|
||||
|
@ -33,22 +23,12 @@ import {
|
|||
import {
|
||||
setRecoveredAlertsContext,
|
||||
updateState,
|
||||
getViewInAppUrl,
|
||||
getRelativeViewInAppUrl,
|
||||
getFullViewInAppMessage,
|
||||
SyntheticsRuleTypeAlertDefinition,
|
||||
} from '../common';
|
||||
import { ALERT_DETAILS_URL, getActionVariables, VIEW_IN_APP_URL } from '../action_variables';
|
||||
import { getActionVariables } from '../action_variables';
|
||||
import { STATUS_RULE_NAME } from '../translations';
|
||||
import { SyntheticsMonitorClient } from '../../synthetics_service/synthetics_monitor/synthetics_monitor_client';
|
||||
|
||||
type MonitorStatusRuleTypeParams = StatusRuleParams;
|
||||
type MonitorStatusActionGroups = ActionGroupIdsOf<typeof MONITOR_STATUS>;
|
||||
type MonitorStatusRuleTypeState = SyntheticsCommonState;
|
||||
type MonitorStatusAlertState = SyntheticsMonitorStatusAlertState;
|
||||
type MonitorStatusAlertContext = AlertContext;
|
||||
type MonitorStatusAlert = ObservabilityUptimeAlert;
|
||||
|
||||
export const registerSyntheticsStatusCheckRule = (
|
||||
server: SyntheticsServerSetup,
|
||||
plugins: SyntheticsPluginsSetupDependencies,
|
||||
|
@ -74,94 +54,44 @@ export const registerSyntheticsStatusCheckRule = (
|
|||
isExportable: true,
|
||||
minimumLicenseRequired: 'basic',
|
||||
doesSetRecoveryContext: true,
|
||||
executor: async (
|
||||
options: RuleExecutorOptions<
|
||||
MonitorStatusRuleTypeParams,
|
||||
MonitorStatusRuleTypeState,
|
||||
MonitorStatusAlertState,
|
||||
MonitorStatusAlertContext,
|
||||
MonitorStatusActionGroups,
|
||||
MonitorStatusAlert
|
||||
>
|
||||
) => {
|
||||
const { state: ruleState, params, services, spaceId, previousStartedAt, startedAt } = options;
|
||||
const { alertsClient, savedObjectsClient, scopedClusterClient, uiSettingsClient } = services;
|
||||
executor: async (options: StatusRuleExecutorOptions) => {
|
||||
apm.setTransactionName('Synthetics Status Rule Executor');
|
||||
const { state: ruleState, params, services, spaceId } = options;
|
||||
const { alertsClient, uiSettingsClient } = services;
|
||||
if (!alertsClient) {
|
||||
throw new AlertsClientError();
|
||||
}
|
||||
const { basePath } = server;
|
||||
const dateFormat = await uiSettingsClient.get('dateFormat');
|
||||
const timezone = await uiSettingsClient.get('dateFormat:tz');
|
||||
|
||||
const [dateFormat, timezone] = await Promise.all([
|
||||
uiSettingsClient.get('dateFormat'),
|
||||
uiSettingsClient.get('dateFormat:tz'),
|
||||
]);
|
||||
const tz = timezone === 'Browser' ? 'UTC' : timezone;
|
||||
|
||||
const statusRule = new StatusRuleExecutor(
|
||||
previousStartedAt,
|
||||
params,
|
||||
savedObjectsClient,
|
||||
scopedClusterClient.asCurrentUser,
|
||||
server,
|
||||
syntheticsMonitorClient
|
||||
);
|
||||
const groupBy = params?.condition?.groupBy ?? 'locationId';
|
||||
const groupByLocation = groupBy === 'locationId';
|
||||
|
||||
const statusRule = new StatusRuleExecutor(server, syntheticsMonitorClient, options);
|
||||
|
||||
const { downConfigs, staleDownConfigs, upConfigs } = await statusRule.getDownChecks(
|
||||
ruleState.meta?.downConfigs as AlertOverviewStatus['downConfigs']
|
||||
);
|
||||
|
||||
Object.entries(downConfigs).forEach(([idWithLocation, { ping, configId }]) => {
|
||||
const locationId = ping.observer.name ?? '';
|
||||
const alertId = idWithLocation;
|
||||
const monitorSummary = getMonitorSummary(
|
||||
ping,
|
||||
DOWN_LABEL,
|
||||
locationId,
|
||||
configId,
|
||||
dateFormat,
|
||||
tz
|
||||
);
|
||||
|
||||
const { uuid, start } = alertsClient.report({
|
||||
id: alertId,
|
||||
actionGroup: MONITOR_STATUS.id,
|
||||
});
|
||||
const errorStartedAt = start ?? startedAt.toISOString();
|
||||
|
||||
let relativeViewInAppUrl = '';
|
||||
if (monitorSummary.stateId) {
|
||||
relativeViewInAppUrl = getRelativeViewInAppUrl({
|
||||
configId,
|
||||
stateId: monitorSummary.stateId,
|
||||
locationId,
|
||||
});
|
||||
}
|
||||
|
||||
const payload = getMonitorAlertDocument(monitorSummary);
|
||||
|
||||
const context = {
|
||||
...monitorSummary,
|
||||
idWithLocation,
|
||||
errorStartedAt,
|
||||
linkMessage: monitorSummary.stateId
|
||||
? getFullViewInAppMessage(basePath, spaceId, relativeViewInAppUrl)
|
||||
: '',
|
||||
[VIEW_IN_APP_URL]: getViewInAppUrl(basePath, spaceId, relativeViewInAppUrl),
|
||||
[ALERT_DETAILS_URL]: getAlertDetailsUrl(basePath, spaceId, uuid),
|
||||
};
|
||||
|
||||
alertsClient.setAlertData({
|
||||
id: alertId,
|
||||
payload,
|
||||
context,
|
||||
});
|
||||
statusRule.handleDownMonitorThresholdAlert({
|
||||
downConfigs,
|
||||
});
|
||||
|
||||
setRecoveredAlertsContext({
|
||||
alertsClient,
|
||||
basePath,
|
||||
spaceId,
|
||||
staleDownConfigs,
|
||||
upConfigs,
|
||||
dateFormat,
|
||||
tz,
|
||||
params,
|
||||
groupByLocation,
|
||||
staleDownConfigs,
|
||||
upConfigs,
|
||||
});
|
||||
|
||||
return {
|
||||
|
|
|
@ -0,0 +1,104 @@
|
|||
/*
|
||||
* 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 { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query';
|
||||
import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
|
||||
import { StatusRuleParams } from '../../../../common/rules/status_rule';
|
||||
import { SyntheticsEsClient } from '../../../lib';
|
||||
import {
|
||||
FINAL_SUMMARY_FILTER,
|
||||
getRangeFilter,
|
||||
getTimeSpanFilter,
|
||||
} from '../../../../common/constants/client_defaults';
|
||||
|
||||
export async function queryFilterMonitors({
|
||||
spaceId,
|
||||
esClient,
|
||||
ruleParams,
|
||||
}: {
|
||||
spaceId: string;
|
||||
esClient: SyntheticsEsClient;
|
||||
ruleParams: StatusRuleParams;
|
||||
}) {
|
||||
if (!ruleParams.kqlQuery) {
|
||||
return;
|
||||
}
|
||||
const filters = toElasticsearchQuery(fromKueryExpression(ruleParams.kqlQuery));
|
||||
const { body: result } = await esClient.search({
|
||||
body: {
|
||||
size: 0,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
FINAL_SUMMARY_FILTER,
|
||||
getRangeFilter({ from: 'now-24h/m', to: 'now/m' }),
|
||||
getTimeSpanFilter(),
|
||||
{
|
||||
term: {
|
||||
'meta.space_id': spaceId,
|
||||
},
|
||||
},
|
||||
{
|
||||
bool: {
|
||||
should: filters,
|
||||
},
|
||||
},
|
||||
...getFilters(ruleParams),
|
||||
],
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
ids: {
|
||||
terms: {
|
||||
size: 10000,
|
||||
field: 'config_id',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return result.aggregations?.ids.buckets.map((bucket) => bucket.key as string);
|
||||
}
|
||||
|
||||
const getFilters = (ruleParams: StatusRuleParams) => {
|
||||
const { monitorTypes, locations, tags, projects } = ruleParams;
|
||||
const filters: QueryDslQueryContainer[] = [];
|
||||
if (monitorTypes?.length) {
|
||||
filters.push({
|
||||
terms: {
|
||||
'monitor.type': monitorTypes,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (locations?.length) {
|
||||
filters.push({
|
||||
terms: {
|
||||
'observer.name': locations,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (tags?.length) {
|
||||
filters.push({
|
||||
terms: {
|
||||
tags,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (projects?.length) {
|
||||
filters.push({
|
||||
terms: {
|
||||
'monitor.project.id': projects,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return filters;
|
||||
};
|
|
@ -8,15 +8,15 @@
|
|||
import pMap from 'p-map';
|
||||
import times from 'lodash/times';
|
||||
import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import { cloneDeep, intersection } from 'lodash';
|
||||
import { intersection } from 'lodash';
|
||||
import { AlertStatusMetaData } from '../../../../common/runtime_types/alert_rules/common';
|
||||
import {
|
||||
AlertOverviewStatus,
|
||||
AlertPendingStatusMetaData,
|
||||
AlertStatusMetaData,
|
||||
} from '../../../common/runtime_types/alert_rules/common';
|
||||
import { createEsParams, SyntheticsEsClient } from '../../lib';
|
||||
import { OverviewPing } from '../../../common/runtime_types';
|
||||
import { FINAL_SUMMARY_FILTER } from '../../../common/constants/client_defaults';
|
||||
FINAL_SUMMARY_FILTER,
|
||||
getTimespanFilter,
|
||||
SUMMARY_FILTER,
|
||||
} from '../../../../common/constants/client_defaults';
|
||||
import { OverviewPing } from '../../../../common/runtime_types';
|
||||
import { createEsParams, SyntheticsEsClient } from '../../../lib';
|
||||
|
||||
const DEFAULT_MAX_ES_BUCKET_SIZE = 10000;
|
||||
|
||||
|
@ -32,23 +32,35 @@ const fields = [
|
|||
'state',
|
||||
'tags',
|
||||
];
|
||||
type StatusConfigs = Record<string, AlertStatusMetaData>;
|
||||
|
||||
export async function queryMonitorStatusForAlert(
|
||||
esClient: SyntheticsEsClient,
|
||||
monitorLocationIds: string[],
|
||||
range: { from: string; to: string },
|
||||
monitorQueryIds: string[],
|
||||
monitorLocationsMap: Record<string, string[]>,
|
||||
monitorQueryIdToConfigIdMap: Record<string, string>
|
||||
): Promise<AlertOverviewStatus> {
|
||||
export interface AlertStatusResponse {
|
||||
upConfigs: StatusConfigs;
|
||||
downConfigs: StatusConfigs;
|
||||
enabledMonitorQueryIds: string[];
|
||||
}
|
||||
|
||||
export async function queryMonitorStatusAlert({
|
||||
esClient,
|
||||
monitorLocationIds,
|
||||
range,
|
||||
monitorQueryIds,
|
||||
monitorLocationsMap,
|
||||
numberOfChecks,
|
||||
includeRetests = true,
|
||||
}: {
|
||||
esClient: SyntheticsEsClient;
|
||||
monitorLocationIds: string[];
|
||||
range: { from: string; to: string };
|
||||
monitorQueryIds: string[];
|
||||
monitorLocationsMap: Record<string, string[]>;
|
||||
numberOfChecks: number;
|
||||
includeRetests?: boolean;
|
||||
}): Promise<AlertStatusResponse> {
|
||||
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, AlertStatusMetaData> = {};
|
||||
const downConfigs: Record<string, AlertStatusMetaData> = {};
|
||||
const monitorsWithoutData = new Map(Object.entries(cloneDeep(monitorLocationsMap)));
|
||||
const pendingConfigs: Record<string, AlertPendingStatusMetaData> = {};
|
||||
const upConfigs: StatusConfigs = {};
|
||||
const downConfigs: StatusConfigs = {};
|
||||
|
||||
await pMap(
|
||||
times(pageCount),
|
||||
|
@ -60,15 +72,8 @@ export async function queryMonitorStatusForAlert(
|
|||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
FINAL_SUMMARY_FILTER,
|
||||
{
|
||||
range: {
|
||||
'@timestamp': {
|
||||
gte: range.from,
|
||||
lte: range.to,
|
||||
},
|
||||
},
|
||||
},
|
||||
...(includeRetests ? [SUMMARY_FILTER] : [FINAL_SUMMARY_FILTER]),
|
||||
getTimespanFilter({ from: range.from, to: range.to }),
|
||||
{
|
||||
terms: {
|
||||
'monitor.id': idsToQuery,
|
||||
|
@ -90,9 +95,18 @@ export async function queryMonitorStatusForAlert(
|
|||
size: monitorLocationIds.length || 100,
|
||||
},
|
||||
aggs: {
|
||||
status: {
|
||||
downChecks: {
|
||||
filter: {
|
||||
range: {
|
||||
'summary.down': {
|
||||
gte: '1',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
totalChecks: {
|
||||
top_hits: {
|
||||
size: 1,
|
||||
size: numberOfChecks,
|
||||
sort: [
|
||||
{
|
||||
'@timestamp': {
|
||||
|
@ -121,62 +135,60 @@ export async function queryMonitorStatusForAlert(
|
|||
});
|
||||
}
|
||||
|
||||
const { body: result } = await esClient.search<OverviewPing, typeof params>(
|
||||
params,
|
||||
'getCurrentStatusOverview' + i
|
||||
);
|
||||
const { body: result } = await esClient.search<OverviewPing, typeof params>(params);
|
||||
|
||||
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 locationSummaries = location.buckets.map(
|
||||
({ key: locationId, totalChecks, downChecks }) => {
|
||||
return { locationId, totalChecks, downChecks };
|
||||
}
|
||||
);
|
||||
|
||||
// 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) => {
|
||||
monQueriedLocations?.forEach((monLocationId) => {
|
||||
const locationSummary = locationSummaries.find(
|
||||
(summary) => summary.location === monLocation
|
||||
(summary) => summary.locationId === monLocationId
|
||||
);
|
||||
|
||||
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 { totalChecks, downChecks } = locationSummary;
|
||||
const latestPing = totalChecks.hits.hits[0]._source;
|
||||
const downCount = downChecks.doc_count;
|
||||
const isLatestPingUp = (latestPing.summary?.up ?? 0) > 0;
|
||||
const configId = latestPing.config_id;
|
||||
const monitorQueryId = latestPing.monitor.id;
|
||||
|
||||
const meta = {
|
||||
ping,
|
||||
const meta: AlertStatusMetaData = {
|
||||
ping: latestPing,
|
||||
configId,
|
||||
monitorQueryId,
|
||||
locationId: monLocation,
|
||||
timestamp: ping['@timestamp'],
|
||||
locationId: monLocationId,
|
||||
timestamp: latestPing['@timestamp'],
|
||||
checks: {
|
||||
downWithinXChecks: totalChecks.hits.hits.reduce(
|
||||
(acc, curr) => acc + ((curr._source.summary.down ?? 0) > 0 ? 1 : 0),
|
||||
0
|
||||
),
|
||||
down: downCount,
|
||||
},
|
||||
status: 'up',
|
||||
};
|
||||
|
||||
if (downCount > 0) {
|
||||
down += 1;
|
||||
downConfigs[`${configId}-${monLocation}`] = {
|
||||
downConfigs[`${configId}-${monLocationId}`] = {
|
||||
...meta,
|
||||
status: 'down',
|
||||
};
|
||||
} else if (upCount > 0) {
|
||||
up += 1;
|
||||
upConfigs[`${configId}-${monLocation}`] = {
|
||||
}
|
||||
if (isLatestPingUp) {
|
||||
upConfigs[`${configId}-${monLocationId}`] = {
|
||||
...meta,
|
||||
status: 'up',
|
||||
};
|
||||
}
|
||||
const monitorsMissingData = monitorsWithoutData.get(monitorQueryId) || [];
|
||||
monitorsWithoutData.set(
|
||||
monitorQueryId,
|
||||
monitorsMissingData?.filter((loc) => loc !== monLocation)
|
||||
);
|
||||
if (!monitorsWithoutData.get(monitorQueryId)?.length) {
|
||||
monitorsWithoutData.delete(monitorQueryId);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -184,26 +196,9 @@ export async function queryMonitorStatusForAlert(
|
|||
{ concurrency: 5 }
|
||||
);
|
||||
|
||||
// identify the remaining monitors without data, to determine pending monitors
|
||||
for (const [queryId, locs] of monitorsWithoutData) {
|
||||
locs.forEach((loc) => {
|
||||
pendingConfigs[`${monitorQueryIdToConfigIdMap[queryId]}-${loc}`] = {
|
||||
configId: `${monitorQueryIdToConfigIdMap[queryId]}`,
|
||||
monitorQueryId: queryId,
|
||||
status: 'unknown',
|
||||
locationId: loc,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
up,
|
||||
down,
|
||||
pending: Object.values(pendingConfigs).length,
|
||||
upConfigs,
|
||||
downConfigs,
|
||||
pendingConfigs,
|
||||
enabledMonitorQueryIds: monitorQueryIds,
|
||||
staleDownConfigs: {},
|
||||
};
|
||||
}
|
|
@ -4,10 +4,10 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import moment from 'moment';
|
||||
import { loggerMock } from '@kbn/logging-mocks';
|
||||
import { savedObjectsClientMock } from '@kbn/core-saved-objects-api-server-mocks';
|
||||
import { StatusRuleExecutor } from './status_rule_executor';
|
||||
import { coreMock } from '@kbn/core/server/mocks';
|
||||
import { getDoesMonitorMeetLocationThreshold, StatusRuleExecutor } from './status_rule_executor';
|
||||
import { mockEncryptedSO } from '../../synthetics_service/utils/mocks';
|
||||
import { elasticsearchClientMock } from '@kbn/core-elasticsearch-client-server-mocks';
|
||||
import { SyntheticsMonitorClient } from '../../synthetics_service/synthetics_monitor/synthetics_monitor_client';
|
||||
|
@ -16,8 +16,12 @@ import * as monitorUtils from '../../saved_objects/synthetics_monitor/get_all_mo
|
|||
import * as locationsUtils from '../../synthetics_service/get_all_locations';
|
||||
import type { PublicLocation } from '../../../common/runtime_types';
|
||||
import { SyntheticsServerSetup } from '../../types';
|
||||
import { AlertStatusMetaData } from '../../../common/runtime_types/alert_rules/common';
|
||||
|
||||
describe('StatusRuleExecutor', () => {
|
||||
// @ts-ignore
|
||||
Date.now = jest.fn(() => new Date('2024-05-13T12:33:37.000Z'));
|
||||
|
||||
const mockEsClient = elasticsearchClientMock.createElasticsearchClient();
|
||||
const logger = loggerMock.create();
|
||||
const soClient = savedObjectsClientMock.create();
|
||||
|
@ -59,166 +63,611 @@ describe('StatusRuleExecutor', () => {
|
|||
|
||||
const monitorClient = new SyntheticsMonitorClient(syntheticsService, serverMock);
|
||||
|
||||
it('should only query enabled monitors', async () => {
|
||||
const spy = jest.spyOn(monitorUtils, 'getAllMonitors').mockResolvedValue([]);
|
||||
const statusRule = new StatusRuleExecutor(
|
||||
moment().toDate(),
|
||||
{},
|
||||
soClient,
|
||||
mockEsClient,
|
||||
serverMock,
|
||||
monitorClient
|
||||
);
|
||||
const { downConfigs, staleDownConfigs } = await statusRule.getDownChecks({});
|
||||
const mockStart = coreMock.createStart();
|
||||
const uiSettingsClient = mockStart.uiSettings.asScopedToClient(soClient);
|
||||
|
||||
expect(downConfigs).toEqual({});
|
||||
expect(staleDownConfigs).toEqual({});
|
||||
const statusRule = new StatusRuleExecutor(serverMock, monitorClient, {
|
||||
params: {},
|
||||
services: {
|
||||
uiSettingsClient,
|
||||
savedObjectsClient: soClient,
|
||||
scopedClusterClient: { asCurrentUser: mockEsClient },
|
||||
},
|
||||
rule: {
|
||||
name: 'test',
|
||||
},
|
||||
} as any);
|
||||
|
||||
expect(spy).toHaveBeenCalledWith({
|
||||
filter: 'synthetics-monitor.attributes.alert.status.enabled: true',
|
||||
soClient,
|
||||
});
|
||||
});
|
||||
describe('DefaultRule', () => {
|
||||
it('should only query enabled monitors', async () => {
|
||||
const spy = jest.spyOn(monitorUtils, 'getAllMonitors').mockResolvedValue([]);
|
||||
|
||||
it('marks deleted configs as expected', async () => {
|
||||
jest.spyOn(monitorUtils, 'getAllMonitors').mockResolvedValue(testMonitors);
|
||||
const statusRule = new StatusRuleExecutor(
|
||||
moment().toDate(),
|
||||
{},
|
||||
soClient,
|
||||
mockEsClient,
|
||||
serverMock,
|
||||
monitorClient
|
||||
);
|
||||
const { downConfigs, staleDownConfigs } = await statusRule.getDownChecks({});
|
||||
|
||||
const { downConfigs } = await statusRule.getDownChecks({});
|
||||
expect(downConfigs).toEqual({});
|
||||
expect(staleDownConfigs).toEqual({});
|
||||
|
||||
expect(downConfigs).toEqual({});
|
||||
|
||||
const staleDownConfigs = await statusRule.markDeletedConfigs({
|
||||
id1: {
|
||||
locationId: 'us-east-1',
|
||||
configId: 'id1',
|
||||
status: 'down',
|
||||
timestamp: '2021-06-01T00:00:00.000Z',
|
||||
monitorQueryId: 'test',
|
||||
ping: {} as any,
|
||||
},
|
||||
'2548dab3-4752-4b4d-89a2-ae3402b6fb04-us_central_dev': {
|
||||
locationId: 'us_central_dev',
|
||||
configId: '2548dab3-4752-4b4d-89a2-ae3402b6fb04',
|
||||
status: 'down',
|
||||
timestamp: '2021-06-01T00:00:00.000Z',
|
||||
monitorQueryId: 'test',
|
||||
ping: {} as any,
|
||||
},
|
||||
'2548dab3-4752-4b4d-89a2-ae3402b6fb04-us_central_qa': {
|
||||
locationId: 'us_central_qa',
|
||||
configId: '2548dab3-4752-4b4d-89a2-ae3402b6fb04',
|
||||
status: 'down',
|
||||
timestamp: '2021-06-01T00:00:00.000Z',
|
||||
monitorQueryId: 'test',
|
||||
ping: {} as any,
|
||||
},
|
||||
expect(spy).toHaveBeenCalledWith({
|
||||
filter: 'synthetics-monitor.attributes.alert.status.enabled: true',
|
||||
soClient,
|
||||
});
|
||||
});
|
||||
|
||||
expect(staleDownConfigs).toEqual({
|
||||
id1: {
|
||||
configId: 'id1',
|
||||
isDeleted: true,
|
||||
locationId: 'us-east-1',
|
||||
monitorQueryId: 'test',
|
||||
ping: {},
|
||||
status: 'down',
|
||||
timestamp: '2021-06-01T00:00:00.000Z',
|
||||
},
|
||||
'2548dab3-4752-4b4d-89a2-ae3402b6fb04-us_central_dev': {
|
||||
configId: '2548dab3-4752-4b4d-89a2-ae3402b6fb04',
|
||||
isLocationRemoved: true,
|
||||
locationId: 'us_central_dev',
|
||||
monitorQueryId: 'test',
|
||||
ping: {},
|
||||
status: 'down',
|
||||
timestamp: '2021-06-01T00:00:00.000Z',
|
||||
},
|
||||
});
|
||||
});
|
||||
it('marks deleted configs as expected', async () => {
|
||||
jest.spyOn(monitorUtils, 'getAllMonitors').mockResolvedValue(testMonitors);
|
||||
|
||||
it('does not mark deleted config when monitor does not contain location label', async () => {
|
||||
jest.spyOn(monitorUtils, 'getAllMonitors').mockResolvedValue([
|
||||
{
|
||||
...testMonitors[0],
|
||||
attributes: {
|
||||
...testMonitors[0].attributes,
|
||||
locations: [
|
||||
{
|
||||
geo: { lon: -95.86, lat: 41.25 },
|
||||
isServiceManaged: true,
|
||||
id: 'us_central_qa',
|
||||
},
|
||||
],
|
||||
const { downConfigs } = await statusRule.getDownChecks({});
|
||||
|
||||
expect(downConfigs).toEqual({});
|
||||
|
||||
const staleDownConfigs = await statusRule.markDeletedConfigs({
|
||||
id2: {
|
||||
locationId: 'us-east-1',
|
||||
configId: 'id2',
|
||||
status: 'down',
|
||||
timestamp: '2021-06-01T00:00:00.000Z',
|
||||
monitorQueryId: 'test',
|
||||
ping: {} as any,
|
||||
checks: {
|
||||
downWithinXChecks: 1,
|
||||
down: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
const statusRule = new StatusRuleExecutor(
|
||||
moment().toDate(),
|
||||
{},
|
||||
soClient,
|
||||
mockEsClient,
|
||||
serverMock,
|
||||
monitorClient
|
||||
);
|
||||
'id1-us_central_dev': {
|
||||
locationId: 'us_central_dev',
|
||||
configId: 'id1',
|
||||
status: 'down',
|
||||
timestamp: '2021-06-01T00:00:00.000Z',
|
||||
monitorQueryId: 'test',
|
||||
ping: {} as any,
|
||||
checks: {
|
||||
downWithinXChecks: 1,
|
||||
down: 1,
|
||||
},
|
||||
},
|
||||
'id1-us_central_qa': {
|
||||
locationId: 'us_central_qa',
|
||||
configId: 'id1',
|
||||
status: 'down',
|
||||
timestamp: '2021-06-01T00:00:00.000Z',
|
||||
monitorQueryId: 'test',
|
||||
ping: {} as any,
|
||||
checks: {
|
||||
downWithinXChecks: 1,
|
||||
down: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { downConfigs } = await statusRule.getDownChecks({});
|
||||
|
||||
expect(downConfigs).toEqual({});
|
||||
|
||||
const staleDownConfigs = await statusRule.markDeletedConfigs({
|
||||
id1: {
|
||||
locationId: 'us-east-1',
|
||||
configId: 'id1',
|
||||
status: 'down',
|
||||
timestamp: '2021-06-01T00:00:00.000Z',
|
||||
monitorQueryId: 'test',
|
||||
ping: {} as any,
|
||||
},
|
||||
'2548dab3-4752-4b4d-89a2-ae3402b6fb04-us_central_dev': {
|
||||
locationId: 'us_central_dev',
|
||||
configId: '2548dab3-4752-4b4d-89a2-ae3402b6fb04',
|
||||
status: 'down',
|
||||
timestamp: '2021-06-01T00:00:00.000Z',
|
||||
monitorQueryId: 'test',
|
||||
ping: {} as any,
|
||||
},
|
||||
'2548dab3-4752-4b4d-89a2-ae3402b6fb04-us_central_qa': {
|
||||
locationId: 'us_central_qa',
|
||||
configId: '2548dab3-4752-4b4d-89a2-ae3402b6fb04',
|
||||
status: 'down',
|
||||
timestamp: '2021-06-01T00:00:00.000Z',
|
||||
monitorQueryId: 'test',
|
||||
ping: {} as any,
|
||||
},
|
||||
expect(staleDownConfigs).toEqual({
|
||||
id2: {
|
||||
configId: 'id2',
|
||||
isDeleted: true,
|
||||
locationId: 'us-east-1',
|
||||
monitorQueryId: 'test',
|
||||
ping: {},
|
||||
status: 'down',
|
||||
timestamp: '2021-06-01T00:00:00.000Z',
|
||||
checks: {
|
||||
downWithinXChecks: 1,
|
||||
down: 1,
|
||||
},
|
||||
},
|
||||
'id1-us_central_dev': {
|
||||
configId: 'id1',
|
||||
isLocationRemoved: true,
|
||||
locationId: 'us_central_dev',
|
||||
monitorQueryId: 'test',
|
||||
ping: {},
|
||||
status: 'down',
|
||||
timestamp: '2021-06-01T00:00:00.000Z',
|
||||
checks: {
|
||||
downWithinXChecks: 1,
|
||||
down: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
expect(staleDownConfigs).toEqual({
|
||||
id1: {
|
||||
configId: 'id1',
|
||||
isDeleted: true,
|
||||
locationId: 'us-east-1',
|
||||
monitorQueryId: 'test',
|
||||
ping: {},
|
||||
status: 'down',
|
||||
timestamp: '2021-06-01T00:00:00.000Z',
|
||||
},
|
||||
'2548dab3-4752-4b4d-89a2-ae3402b6fb04-us_central_dev': {
|
||||
configId: '2548dab3-4752-4b4d-89a2-ae3402b6fb04',
|
||||
isLocationRemoved: true,
|
||||
locationId: 'us_central_dev',
|
||||
monitorQueryId: 'test',
|
||||
ping: {},
|
||||
status: 'down',
|
||||
timestamp: '2021-06-01T00:00:00.000Z',
|
||||
},
|
||||
it('does not mark deleted config when monitor does not contain location label', async () => {
|
||||
jest.spyOn(monitorUtils, 'getAllMonitors').mockResolvedValue([
|
||||
{
|
||||
...testMonitors[0],
|
||||
attributes: {
|
||||
...testMonitors[0].attributes,
|
||||
locations: [
|
||||
{
|
||||
geo: { lon: -95.86, lat: 41.25 },
|
||||
isServiceManaged: true,
|
||||
id: 'us_central_qa',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const { downConfigs } = await statusRule.getDownChecks({});
|
||||
|
||||
expect(downConfigs).toEqual({});
|
||||
|
||||
const staleDownConfigs = await statusRule.markDeletedConfigs({
|
||||
id2: {
|
||||
locationId: 'us-east-1',
|
||||
configId: 'id2',
|
||||
status: 'down',
|
||||
timestamp: '2021-06-01T00:00:00.000Z',
|
||||
monitorQueryId: 'test',
|
||||
ping: {} as any,
|
||||
checks: {
|
||||
downWithinXChecks: 1,
|
||||
down: 1,
|
||||
},
|
||||
},
|
||||
'id1-us_central_dev': {
|
||||
locationId: 'us_central_dev',
|
||||
configId: 'id1',
|
||||
status: 'down',
|
||||
timestamp: '2021-06-01T00:00:00.000Z',
|
||||
monitorQueryId: 'test',
|
||||
ping: {} as any,
|
||||
checks: {
|
||||
downWithinXChecks: 1,
|
||||
down: 1,
|
||||
},
|
||||
},
|
||||
'id1-us_central_qa': {
|
||||
locationId: 'us_central_qa',
|
||||
configId: 'id1',
|
||||
status: 'down',
|
||||
timestamp: '2021-06-01T00:00:00.000Z',
|
||||
monitorQueryId: 'test',
|
||||
ping: {} as any,
|
||||
checks: {
|
||||
downWithinXChecks: 1,
|
||||
down: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(staleDownConfigs).toEqual({
|
||||
id2: {
|
||||
configId: 'id2',
|
||||
isDeleted: true,
|
||||
locationId: 'us-east-1',
|
||||
monitorQueryId: 'test',
|
||||
ping: {},
|
||||
status: 'down',
|
||||
timestamp: '2021-06-01T00:00:00.000Z',
|
||||
checks: {
|
||||
downWithinXChecks: 1,
|
||||
down: 1,
|
||||
},
|
||||
},
|
||||
'id1-us_central_dev': {
|
||||
configId: 'id1',
|
||||
isLocationRemoved: true,
|
||||
locationId: 'us_central_dev',
|
||||
monitorQueryId: 'test',
|
||||
ping: {},
|
||||
status: 'down',
|
||||
timestamp: '2021-06-01T00:00:00.000Z',
|
||||
checks: {
|
||||
downWithinXChecks: 1,
|
||||
down: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleDownMonitorThresholdAlert', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should alert if monitor meet location threshold', async () => {
|
||||
const spy = jest.spyOn(statusRule, 'scheduleAlert');
|
||||
statusRule.handleDownMonitorThresholdAlert({
|
||||
downConfigs: {
|
||||
'id1-us_central_qa': {
|
||||
locationId: 'us_central_qa',
|
||||
configId: 'id1',
|
||||
status: 'down',
|
||||
timestamp: '2021-06-01T00:00:00.000Z',
|
||||
monitorQueryId: 'test',
|
||||
ping: testPing,
|
||||
checks: {
|
||||
downWithinXChecks: 1,
|
||||
down: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(spy).toHaveBeenCalledWith({
|
||||
alertId: 'id1-us_central_qa',
|
||||
downThreshold: 1,
|
||||
idWithLocation: 'id1-us_central_qa',
|
||||
locationNames: ['Test location'],
|
||||
locationIds: ['test'],
|
||||
monitorSummary: {
|
||||
checkedAt: '2024-05-13T12:33:37Z',
|
||||
checks: { down: 1, downWithinXChecks: 1 },
|
||||
configId: 'id1',
|
||||
downThreshold: 1,
|
||||
hostName: undefined,
|
||||
lastErrorMessage: undefined,
|
||||
locationId: 'us_central_qa',
|
||||
locationName: 'Test location',
|
||||
locationNames: 'Test location',
|
||||
monitorId: 'test',
|
||||
monitorName: 'test monitor',
|
||||
monitorTags: ['dev'],
|
||||
monitorType: 'browser',
|
||||
monitorUrl: 'https://www.google.com',
|
||||
monitorUrlLabel: 'URL',
|
||||
reason:
|
||||
'Monitor "test monitor" from Test location is down. Monitor is down 1 time within the last 1 checks. Alert when 1 out of the last 1 checks are down from at least 1 location.',
|
||||
stateId: undefined,
|
||||
status: 'down',
|
||||
timestamp: '2024-05-13T12:33:37.000Z',
|
||||
},
|
||||
statusConfig: {
|
||||
checks: { down: 1, downWithinXChecks: 1 },
|
||||
configId: 'id1',
|
||||
locationId: 'us_central_qa',
|
||||
monitorQueryId: 'test',
|
||||
ping: testPing,
|
||||
status: 'down',
|
||||
timestamp: '2021-06-01T00:00:00.000Z',
|
||||
},
|
||||
useLatestChecks: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should not alert if monitor do not meet location threshold', async () => {
|
||||
statusRule.params = {
|
||||
condition: {
|
||||
window: {
|
||||
numberOfChecks: 1,
|
||||
},
|
||||
downThreshold: 1,
|
||||
locationsThreshold: 2,
|
||||
},
|
||||
};
|
||||
|
||||
const spy = jest.spyOn(statusRule, 'scheduleAlert');
|
||||
statusRule.handleDownMonitorThresholdAlert({
|
||||
downConfigs: {
|
||||
'id1-us_central_qa': {
|
||||
locationId: 'us_central_qa',
|
||||
configId: 'id1',
|
||||
status: 'down',
|
||||
timestamp: '2021-06-01T00:00:00.000Z',
|
||||
monitorQueryId: 'test',
|
||||
ping: testPing,
|
||||
checks: {
|
||||
downWithinXChecks: 1,
|
||||
down: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(spy).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('should send 2 alerts', async () => {
|
||||
statusRule.params = {
|
||||
condition: {
|
||||
window: {
|
||||
numberOfChecks: 1,
|
||||
},
|
||||
downThreshold: 1,
|
||||
locationsThreshold: 1,
|
||||
},
|
||||
};
|
||||
const spy = jest.spyOn(statusRule, 'scheduleAlert');
|
||||
statusRule.handleDownMonitorThresholdAlert({
|
||||
downConfigs: {
|
||||
'id1-us_central_qa': {
|
||||
locationId: 'us_central_qa',
|
||||
configId: 'id1',
|
||||
status: 'down',
|
||||
timestamp: '2021-06-01T00:00:00.000Z',
|
||||
monitorQueryId: 'test',
|
||||
ping: testPing,
|
||||
checks: {
|
||||
downWithinXChecks: 1,
|
||||
down: 1,
|
||||
},
|
||||
},
|
||||
'id1-us_central_dev': {
|
||||
locationId: 'us_central_dev',
|
||||
configId: 'id1',
|
||||
status: 'down',
|
||||
timestamp: '2021-06-01T00:00:00.000Z',
|
||||
monitorQueryId: 'test',
|
||||
ping: testPing,
|
||||
checks: {
|
||||
downWithinXChecks: 1,
|
||||
down: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(spy).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should send 1 alert for un-grouped', async () => {
|
||||
statusRule.params = {
|
||||
condition: {
|
||||
groupBy: 'none',
|
||||
window: {
|
||||
numberOfChecks: 1,
|
||||
},
|
||||
downThreshold: 1,
|
||||
locationsThreshold: 1,
|
||||
},
|
||||
};
|
||||
const spy = jest.spyOn(statusRule, 'scheduleAlert');
|
||||
statusRule.handleDownMonitorThresholdAlert({
|
||||
downConfigs: {
|
||||
'id1-us_central_qa': {
|
||||
locationId: 'us_central_qa',
|
||||
configId: 'id1',
|
||||
status: 'down',
|
||||
timestamp: '2021-06-01T00:00:00.000Z',
|
||||
monitorQueryId: 'test',
|
||||
ping: testPing,
|
||||
checks: {
|
||||
downWithinXChecks: 1,
|
||||
down: 1,
|
||||
},
|
||||
},
|
||||
'id1-us_central_dev': {
|
||||
locationId: 'us_central_dev',
|
||||
configId: 'id1',
|
||||
status: 'down',
|
||||
timestamp: '2021-06-01T00:00:00.000Z',
|
||||
monitorQueryId: 'test',
|
||||
ping: testPing,
|
||||
checks: {
|
||||
downWithinXChecks: 1,
|
||||
down: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
expect(spy).toHaveBeenCalledWith({
|
||||
alertId: 'id1',
|
||||
downThreshold: 1,
|
||||
idWithLocation: 'id1',
|
||||
locationIds: ['test', 'test'],
|
||||
locationNames: ['Test location', 'Test location'],
|
||||
monitorSummary: {
|
||||
checkedAt: '2024-05-13T12:33:37Z',
|
||||
checks: { down: 1, downWithinXChecks: 1 },
|
||||
configId: 'id1',
|
||||
downThreshold: 1,
|
||||
hostName: undefined,
|
||||
lastErrorMessage: undefined,
|
||||
locationId: 'test and test',
|
||||
locationName: 'Test location',
|
||||
locationNames: 'Test location and Test location',
|
||||
monitorId: 'test',
|
||||
monitorName: 'test monitor',
|
||||
monitorTags: ['dev'],
|
||||
monitorType: 'browser',
|
||||
monitorUrl: 'https://www.google.com',
|
||||
monitorUrlLabel: 'URL',
|
||||
reason:
|
||||
'Monitor "test monitor" is down 1 time from Test location and 1 time from Test location. Alert when down 1 time out of the last 1 checks from at least 1 location.',
|
||||
status: 'down',
|
||||
timestamp: '2024-05-13T12:33:37.000Z',
|
||||
},
|
||||
statusConfig: {
|
||||
checks: { down: 1, downWithinXChecks: 1 },
|
||||
configId: 'id1',
|
||||
locationId: 'us_central_qa',
|
||||
monitorQueryId: 'test',
|
||||
ping: testPing,
|
||||
status: 'down',
|
||||
timestamp: '2021-06-01T00:00:00.000Z',
|
||||
},
|
||||
useLatestChecks: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDoesMonitorMeetLocationThreshold', () => {
|
||||
describe('when useTimeWindow is false', () => {
|
||||
it('should return false if monitor does not meets location threshold', () => {
|
||||
const matchesByLocation: AlertStatusMetaData[] = [
|
||||
{
|
||||
checks: { down: 0, downWithinXChecks: 0 },
|
||||
locationId: 'us_central_qa',
|
||||
ping: testPing,
|
||||
configId: 'id1',
|
||||
monitorQueryId: 'test',
|
||||
status: 'down',
|
||||
timestamp: '2021-06-01T00:00:00.000Z',
|
||||
},
|
||||
];
|
||||
const res = getDoesMonitorMeetLocationThreshold({
|
||||
matchesByLocation,
|
||||
locationsThreshold: 1,
|
||||
downThreshold: 1,
|
||||
useTimeWindow: false,
|
||||
});
|
||||
expect(res).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true if monitor meets location threshold', () => {
|
||||
const matchesByLocation: AlertStatusMetaData[] = [
|
||||
{
|
||||
checks: { down: 1, downWithinXChecks: 1 },
|
||||
locationId: 'us_central_qa',
|
||||
ping: testPing,
|
||||
configId: 'id1',
|
||||
monitorQueryId: 'test',
|
||||
status: 'down',
|
||||
timestamp: '2021-06-01T00:00:00.000Z',
|
||||
},
|
||||
];
|
||||
const res = getDoesMonitorMeetLocationThreshold({
|
||||
matchesByLocation,
|
||||
locationsThreshold: 1,
|
||||
downThreshold: 1,
|
||||
useTimeWindow: false,
|
||||
});
|
||||
expect(res).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if monitor does not meets 2 location threshold', () => {
|
||||
const matchesByLocation: AlertStatusMetaData[] = [
|
||||
{
|
||||
checks: { down: 1, downWithinXChecks: 1 },
|
||||
locationId: 'us_central_qa',
|
||||
ping: testPing,
|
||||
configId: 'id1',
|
||||
monitorQueryId: 'test',
|
||||
status: 'down',
|
||||
timestamp: '2021-06-01T00:00:00.000Z',
|
||||
},
|
||||
];
|
||||
const res = getDoesMonitorMeetLocationThreshold({
|
||||
matchesByLocation,
|
||||
locationsThreshold: 2,
|
||||
downThreshold: 1,
|
||||
useTimeWindow: false,
|
||||
});
|
||||
expect(res).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true if monitor meets 2 location threshold', () => {
|
||||
const matchesByLocation: AlertStatusMetaData[] = [
|
||||
{
|
||||
checks: { down: 1, downWithinXChecks: 1 },
|
||||
locationId: 'us_central_qa',
|
||||
ping: testPing,
|
||||
configId: 'id1',
|
||||
monitorQueryId: 'test',
|
||||
status: 'down',
|
||||
timestamp: '2021-06-01T00:00:00.000Z',
|
||||
},
|
||||
{
|
||||
checks: { down: 1, downWithinXChecks: 1 },
|
||||
locationId: 'us_central',
|
||||
ping: testPing,
|
||||
configId: 'id1',
|
||||
monitorQueryId: 'test',
|
||||
status: 'down',
|
||||
timestamp: '2021-06-01T00:00:00.000Z',
|
||||
},
|
||||
];
|
||||
const res = getDoesMonitorMeetLocationThreshold({
|
||||
matchesByLocation,
|
||||
locationsThreshold: 2,
|
||||
downThreshold: 1,
|
||||
useTimeWindow: false,
|
||||
});
|
||||
expect(res).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when useTimeWindow is true', () => {
|
||||
it('should return false if monitor does not meets location threshold', () => {
|
||||
const matchesByLocation: AlertStatusMetaData[] = [
|
||||
{
|
||||
checks: { down: 0, downWithinXChecks: 0 },
|
||||
locationId: 'us_central_qa',
|
||||
ping: testPing,
|
||||
configId: 'id1',
|
||||
monitorQueryId: 'test',
|
||||
status: 'down',
|
||||
timestamp: '2021-06-01T00:00:00.000Z',
|
||||
},
|
||||
];
|
||||
const res = getDoesMonitorMeetLocationThreshold({
|
||||
matchesByLocation,
|
||||
locationsThreshold: 1,
|
||||
downThreshold: 1,
|
||||
useTimeWindow: true,
|
||||
});
|
||||
expect(res).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true if monitor meets location threshold', () => {
|
||||
const matchesByLocation: AlertStatusMetaData[] = [
|
||||
{
|
||||
checks: { down: 1, downWithinXChecks: 0 },
|
||||
locationId: 'us_central_qa',
|
||||
ping: testPing,
|
||||
configId: 'id1',
|
||||
monitorQueryId: 'test',
|
||||
status: 'down',
|
||||
timestamp: '2021-06-01T00:00:00.000Z',
|
||||
},
|
||||
];
|
||||
const res = getDoesMonitorMeetLocationThreshold({
|
||||
matchesByLocation,
|
||||
locationsThreshold: 1,
|
||||
downThreshold: 1,
|
||||
useTimeWindow: true,
|
||||
});
|
||||
expect(res).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if monitor does not meets 2 location threshold', () => {
|
||||
const matchesByLocation: AlertStatusMetaData[] = [
|
||||
{
|
||||
checks: { down: 1, downWithinXChecks: 0 },
|
||||
locationId: 'us_central_qa',
|
||||
ping: testPing,
|
||||
configId: 'id1',
|
||||
monitorQueryId: 'test',
|
||||
status: 'down',
|
||||
timestamp: '2021-06-01T00:00:00.000Z',
|
||||
},
|
||||
];
|
||||
const res = getDoesMonitorMeetLocationThreshold({
|
||||
matchesByLocation,
|
||||
locationsThreshold: 2,
|
||||
downThreshold: 1,
|
||||
useTimeWindow: true,
|
||||
});
|
||||
expect(res).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true if monitor meets 2 location threshold', () => {
|
||||
const matchesByLocation: AlertStatusMetaData[] = [
|
||||
{
|
||||
checks: { down: 1, downWithinXChecks: 0 },
|
||||
locationId: 'us_central_qa',
|
||||
ping: testPing,
|
||||
configId: 'id1',
|
||||
monitorQueryId: 'test',
|
||||
status: 'down',
|
||||
timestamp: '2021-06-01T00:00:00.000Z',
|
||||
},
|
||||
{
|
||||
checks: { down: 1, downWithinXChecks: 1 },
|
||||
locationId: 'us_central',
|
||||
ping: testPing,
|
||||
configId: 'id1',
|
||||
monitorQueryId: 'test',
|
||||
status: 'down',
|
||||
timestamp: '2021-06-01T00:00:00.000Z',
|
||||
},
|
||||
];
|
||||
const res = getDoesMonitorMeetLocationThreshold({
|
||||
matchesByLocation,
|
||||
locationsThreshold: 2,
|
||||
downThreshold: 1,
|
||||
useTimeWindow: true,
|
||||
});
|
||||
expect(res).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -226,7 +675,7 @@ describe('StatusRuleExecutor', () => {
|
|||
const testMonitors = [
|
||||
{
|
||||
type: 'synthetics-monitor',
|
||||
id: '2548dab3-4752-4b4d-89a2-ae3402b6fb04',
|
||||
id: 'id1',
|
||||
attributes: {
|
||||
type: 'browser',
|
||||
form_monitor_type: 'multistep',
|
||||
|
@ -234,7 +683,7 @@ const testMonitors = [
|
|||
alert: { status: { enabled: false } },
|
||||
schedule: { unit: 'm', number: '10' },
|
||||
'service.name': '',
|
||||
config_id: '2548dab3-4752-4b4d-89a2-ae3402b6fb04',
|
||||
config_id: 'id1',
|
||||
tags: [],
|
||||
timeout: null,
|
||||
name: 'https://www.google.com',
|
||||
|
@ -250,7 +699,7 @@ const testMonitors = [
|
|||
origin: 'ui',
|
||||
journey_id: '',
|
||||
hash: '',
|
||||
id: '2548dab3-4752-4b4d-89a2-ae3402b6fb04',
|
||||
id: 'id1',
|
||||
project_id: '',
|
||||
playwright_options: '',
|
||||
__ui: {
|
||||
|
@ -289,3 +738,22 @@ const testMonitors = [
|
|||
sort: ['https://www.google.com', 1889],
|
||||
},
|
||||
] as any;
|
||||
|
||||
const testPing = {
|
||||
'@timestamp': '2024-05-13T12:33:37.000Z',
|
||||
monitor: {
|
||||
id: 'test',
|
||||
name: 'test monitor',
|
||||
type: 'browser',
|
||||
},
|
||||
tags: ['dev'],
|
||||
url: {
|
||||
full: 'https://www.google.com',
|
||||
},
|
||||
observer: {
|
||||
name: 'test',
|
||||
geo: {
|
||||
name: 'Test location',
|
||||
},
|
||||
},
|
||||
} as any;
|
||||
|
|
|
@ -9,9 +9,31 @@ import {
|
|||
SavedObjectsClientContract,
|
||||
SavedObjectsFindResult,
|
||||
} from '@kbn/core-saved-objects-api-server';
|
||||
import { ElasticsearchClient } from '@kbn/core-elasticsearch-server';
|
||||
import { AlertOverviewStatus } from '../../../common/runtime_types/alert_rules/common';
|
||||
import { queryMonitorStatusForAlert } from './query_monitor_status_alert';
|
||||
import { Logger } from '@kbn/core/server';
|
||||
import { intersection, isEmpty, uniq } from 'lodash';
|
||||
import { getAlertDetailsUrl } from '@kbn/observability-plugin/common';
|
||||
import {
|
||||
AlertOverviewStatus,
|
||||
AlertStatusConfigs,
|
||||
AlertStatusMetaData,
|
||||
StaleDownConfig,
|
||||
} from '../../../common/runtime_types/alert_rules/common';
|
||||
import { queryFilterMonitors } from './queries/filter_monitors';
|
||||
import { MonitorSummaryStatusRule, StatusRuleExecutorOptions } from './types';
|
||||
import {
|
||||
AND_LABEL,
|
||||
getFullViewInAppMessage,
|
||||
getRelativeViewInAppUrl,
|
||||
getViewInAppUrl,
|
||||
} from '../common';
|
||||
import {
|
||||
DOWN_LABEL,
|
||||
getMonitorAlertDocument,
|
||||
getMonitorSummary,
|
||||
getUngroupedReasonMessage,
|
||||
} from './message_utils';
|
||||
import { queryMonitorStatusAlert } from './queries/query_monitor_status_alert';
|
||||
import { parseArrayFilters } from '../../routes/common';
|
||||
import { SyntheticsServerSetup } from '../../types';
|
||||
import { SyntheticsEsClient } from '../../lib';
|
||||
import { SYNTHETICS_INDEX_PATTERN } from '../../../common/constants';
|
||||
|
@ -19,11 +41,13 @@ import {
|
|||
getAllMonitors,
|
||||
processMonitors,
|
||||
} from '../../saved_objects/synthetics_monitor/get_all_monitors';
|
||||
import { StatusRuleParams } from '../../../common/rules/status_rule';
|
||||
import { getConditionType, StatusRuleParams } from '../../../common/rules/status_rule';
|
||||
import { ConfigKey, EncryptedSyntheticsMonitorAttributes } from '../../../common/runtime_types';
|
||||
import { SyntheticsMonitorClient } from '../../synthetics_service/synthetics_monitor/synthetics_monitor_client';
|
||||
import { monitorAttributes } from '../../../common/types/saved_objects';
|
||||
import { AlertConfigKey } from '../../../common/constants/monitor_management';
|
||||
import { ALERT_DETAILS_URL, VIEW_IN_APP_URL } from '../action_variables';
|
||||
import { MONITOR_STATUS } from '../../../common/constants/synthetics_alerts';
|
||||
|
||||
export class StatusRuleExecutor {
|
||||
previousStartedAt: Date | null;
|
||||
|
@ -33,106 +57,176 @@ export class StatusRuleExecutor {
|
|||
server: SyntheticsServerSetup;
|
||||
syntheticsMonitorClient: SyntheticsMonitorClient;
|
||||
monitors: Array<SavedObjectsFindResult<EncryptedSyntheticsMonitorAttributes>> = [];
|
||||
hasCustomCondition: boolean;
|
||||
monitorLocationsMap: Record<string, string[]>; // monitorId: locationIds
|
||||
dateFormat?: string;
|
||||
tz?: string;
|
||||
options: StatusRuleExecutorOptions;
|
||||
logger: Logger;
|
||||
ruleName: string;
|
||||
|
||||
constructor(
|
||||
previousStartedAt: Date | null,
|
||||
p: StatusRuleParams,
|
||||
soClient: SavedObjectsClientContract,
|
||||
scopedClient: ElasticsearchClient,
|
||||
server: SyntheticsServerSetup,
|
||||
syntheticsMonitorClient: SyntheticsMonitorClient
|
||||
syntheticsMonitorClient: SyntheticsMonitorClient,
|
||||
options: StatusRuleExecutorOptions
|
||||
) {
|
||||
const { services, params, previousStartedAt, rule } = options;
|
||||
const { scopedClusterClient, savedObjectsClient } = services;
|
||||
this.ruleName = rule.name;
|
||||
this.logger = server.logger;
|
||||
this.previousStartedAt = previousStartedAt;
|
||||
this.params = p;
|
||||
this.soClient = soClient;
|
||||
this.esClient = new SyntheticsEsClient(this.soClient, scopedClient, {
|
||||
this.params = params;
|
||||
this.soClient = savedObjectsClient;
|
||||
this.esClient = new SyntheticsEsClient(this.soClient, scopedClusterClient.asCurrentUser, {
|
||||
heartbeatIndices: SYNTHETICS_INDEX_PATTERN,
|
||||
});
|
||||
this.server = server;
|
||||
this.syntheticsMonitorClient = syntheticsMonitorClient;
|
||||
this.hasCustomCondition = !isEmpty(this.params);
|
||||
this.monitorLocationsMap = {};
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
debug(message: string) {
|
||||
this.logger.debug(`[Status Rule Executor][${this.ruleName}] ${message}`);
|
||||
}
|
||||
|
||||
async init() {
|
||||
const { uiSettingsClient } = this.options.services;
|
||||
this.dateFormat = await uiSettingsClient.get('dateFormat');
|
||||
const timezone = await uiSettingsClient.get('dateFormat:tz');
|
||||
this.tz = timezone === 'Browser' ? 'UTC' : timezone;
|
||||
}
|
||||
|
||||
async getMonitors() {
|
||||
this.monitors = await getAllMonitors({
|
||||
soClient: this.soClient,
|
||||
filter: `${monitorAttributes}.${AlertConfigKey.STATUS_ENABLED}: true`,
|
||||
const baseFilter = !this.hasCustomCondition
|
||||
? `${monitorAttributes}.${AlertConfigKey.STATUS_ENABLED}: true`
|
||||
: '';
|
||||
|
||||
const configIds = await queryFilterMonitors({
|
||||
spaceId: this.options.spaceId,
|
||||
esClient: this.esClient,
|
||||
ruleParams: this.params,
|
||||
});
|
||||
|
||||
const {
|
||||
allIds,
|
||||
enabledMonitorQueryIds,
|
||||
monitorLocationIds,
|
||||
const { filtersStr } = parseArrayFilters({
|
||||
configIds,
|
||||
filter: baseFilter,
|
||||
tags: this.params?.tags,
|
||||
locations: this.params?.locations,
|
||||
monitorTypes: this.params?.monitorTypes,
|
||||
monitorQueryIds: this.params?.monitorIds,
|
||||
projects: this.params?.projects,
|
||||
});
|
||||
|
||||
this.monitors = await getAllMonitors({
|
||||
soClient: this.soClient,
|
||||
filter: filtersStr,
|
||||
});
|
||||
|
||||
this.debug(`Found ${this.monitors.length} monitors for params ${JSON.stringify(this.params)}`);
|
||||
return processMonitors(this.monitors);
|
||||
}
|
||||
|
||||
async getDownChecks(prevDownConfigs: AlertStatusConfigs = {}): Promise<AlertOverviewStatus> {
|
||||
await this.init();
|
||||
const { enabledMonitorQueryIds, maxPeriod, monitorLocationIds, monitorLocationsMap } =
|
||||
await this.getMonitors();
|
||||
|
||||
const range = this.getRange(maxPeriod);
|
||||
|
||||
const { numberOfChecks } = getConditionType(this.params.condition);
|
||||
|
||||
if (enabledMonitorQueryIds.length === 0) {
|
||||
const staleDownConfigs = this.markDeletedConfigs(prevDownConfigs);
|
||||
return {
|
||||
downConfigs: { ...prevDownConfigs },
|
||||
upConfigs: {},
|
||||
staleDownConfigs,
|
||||
enabledMonitorQueryIds,
|
||||
pendingConfigs: {},
|
||||
};
|
||||
}
|
||||
|
||||
const queryLocations = this.params?.locations;
|
||||
|
||||
// Account for locations filter
|
||||
const listOfLocationAfterFilter = queryLocations
|
||||
? intersection(monitorLocationIds, queryLocations)
|
||||
: monitorLocationIds;
|
||||
|
||||
const currentStatus = await queryMonitorStatusAlert({
|
||||
esClient: this.esClient,
|
||||
monitorLocationIds: listOfLocationAfterFilter,
|
||||
range,
|
||||
monitorQueryIds: enabledMonitorQueryIds,
|
||||
numberOfChecks,
|
||||
monitorLocationsMap,
|
||||
projectMonitorsCount,
|
||||
monitorQueryIdToConfigIdMap,
|
||||
} = processMonitors(this.monitors);
|
||||
includeRetests: this.params.condition?.includeRetests,
|
||||
});
|
||||
|
||||
const { downConfigs, upConfigs } = currentStatus;
|
||||
|
||||
this.debug(
|
||||
`Found ${Object.keys(downConfigs).length} down configs and ${
|
||||
Object.keys(upConfigs).length
|
||||
} up configs`
|
||||
);
|
||||
|
||||
const downConfigsById = getConfigsByIds(downConfigs);
|
||||
const upConfigsById = getConfigsByIds(upConfigs);
|
||||
|
||||
uniq([...downConfigsById.keys(), ...upConfigsById.keys()]).forEach((configId) => {
|
||||
const downCount = downConfigsById.get(configId)?.length ?? 0;
|
||||
const upCount = upConfigsById.get(configId)?.length ?? 0;
|
||||
const name = this.monitors.find((m) => m.id === configId)?.attributes.name ?? configId;
|
||||
this.debug(
|
||||
`Monitor: ${name} with id ${configId} has ${downCount} down check and ${upCount} up check`
|
||||
);
|
||||
});
|
||||
|
||||
Object.keys(prevDownConfigs).forEach((locId) => {
|
||||
if (!downConfigs[locId] && !upConfigs[locId]) {
|
||||
downConfigs[locId] = prevDownConfigs[locId];
|
||||
}
|
||||
});
|
||||
|
||||
const staleDownConfigs = this.markDeletedConfigs(downConfigs);
|
||||
|
||||
return {
|
||||
enabledMonitorQueryIds,
|
||||
monitorLocationIds,
|
||||
allIds,
|
||||
monitorLocationsMap,
|
||||
projectMonitorsCount,
|
||||
monitorQueryIdToConfigIdMap,
|
||||
...currentStatus,
|
||||
staleDownConfigs,
|
||||
pendingConfigs: {},
|
||||
};
|
||||
}
|
||||
|
||||
async getDownChecks(
|
||||
prevDownConfigs: AlertOverviewStatus['downConfigs'] = {}
|
||||
): Promise<AlertOverviewStatus> {
|
||||
const {
|
||||
monitorLocationIds,
|
||||
enabledMonitorQueryIds,
|
||||
monitorLocationsMap,
|
||||
monitorQueryIdToConfigIdMap,
|
||||
} = await this.getMonitors();
|
||||
const from = this.previousStartedAt
|
||||
getRange = (maxPeriod: number) => {
|
||||
let from = this.previousStartedAt
|
||||
? moment(this.previousStartedAt).subtract(1, 'minute').toISOString()
|
||||
: 'now-2m';
|
||||
|
||||
if (enabledMonitorQueryIds.length > 0) {
|
||||
const currentStatus = await queryMonitorStatusForAlert(
|
||||
this.esClient,
|
||||
monitorLocationIds,
|
||||
{
|
||||
to: 'now',
|
||||
from,
|
||||
},
|
||||
enabledMonitorQueryIds,
|
||||
monitorLocationsMap,
|
||||
monitorQueryIdToConfigIdMap
|
||||
);
|
||||
const condition = this.params.condition;
|
||||
if (condition && 'numberOfChecks' in condition?.window) {
|
||||
const numberOfChecks = condition.window.numberOfChecks;
|
||||
from = moment()
|
||||
.subtract(maxPeriod * numberOfChecks, 'milliseconds')
|
||||
.subtract(5, 'minutes')
|
||||
.toISOString();
|
||||
} else if (condition && 'time' in condition.window) {
|
||||
const time = condition.window.time;
|
||||
const { unit, size } = time;
|
||||
|
||||
const downConfigs = currentStatus.downConfigs;
|
||||
const upConfigs = currentStatus.upConfigs;
|
||||
|
||||
Object.keys(prevDownConfigs).forEach((locId) => {
|
||||
if (!downConfigs[locId] && !upConfigs[locId]) {
|
||||
downConfigs[locId] = prevDownConfigs[locId];
|
||||
}
|
||||
});
|
||||
|
||||
const staleDownConfigs = this.markDeletedConfigs(downConfigs);
|
||||
|
||||
return {
|
||||
...currentStatus,
|
||||
staleDownConfigs,
|
||||
};
|
||||
from = moment().subtract(size, unit).toISOString();
|
||||
}
|
||||
const staleDownConfigs = this.markDeletedConfigs(prevDownConfigs);
|
||||
return {
|
||||
downConfigs: { ...prevDownConfigs },
|
||||
upConfigs: {},
|
||||
pendingConfigs: {},
|
||||
staleDownConfigs,
|
||||
down: 0,
|
||||
up: 0,
|
||||
pending: 0,
|
||||
enabledMonitorQueryIds,
|
||||
};
|
||||
}
|
||||
|
||||
markDeletedConfigs(downConfigs: AlertOverviewStatus['downConfigs']) {
|
||||
this.debug(
|
||||
`Using range from ${from} to now, diff of ${moment().diff(from, 'minutes')} minutes`
|
||||
);
|
||||
|
||||
return { from, to: 'now' };
|
||||
};
|
||||
|
||||
markDeletedConfigs(downConfigs: AlertStatusConfigs): Record<string, StaleDownConfig> {
|
||||
const monitors = this.monitors;
|
||||
const staleDownConfigs: AlertOverviewStatus['staleDownConfigs'] = {};
|
||||
Object.keys(downConfigs).forEach((locPlusId) => {
|
||||
|
@ -158,4 +252,221 @@ export class StatusRuleExecutor {
|
|||
|
||||
return staleDownConfigs;
|
||||
}
|
||||
|
||||
handleDownMonitorThresholdAlert = ({ downConfigs }: { downConfigs: AlertStatusConfigs }) => {
|
||||
const { useTimeWindow, useLatestChecks, downThreshold, locationsThreshold } = getConditionType(
|
||||
this.params?.condition
|
||||
);
|
||||
const groupBy = this.params?.condition?.groupBy ?? 'locationId';
|
||||
|
||||
if (groupBy === 'locationId' && locationsThreshold === 1) {
|
||||
Object.entries(downConfigs).forEach(([idWithLocation, statusConfig]) => {
|
||||
const doesMonitorMeetLocationThreshold = getDoesMonitorMeetLocationThreshold({
|
||||
matchesByLocation: [statusConfig],
|
||||
locationsThreshold,
|
||||
downThreshold,
|
||||
useTimeWindow: useTimeWindow || false,
|
||||
});
|
||||
if (doesMonitorMeetLocationThreshold) {
|
||||
const alertId = idWithLocation;
|
||||
const monitorSummary = this.getMonitorDownSummary({
|
||||
statusConfig,
|
||||
});
|
||||
|
||||
return this.scheduleAlert({
|
||||
idWithLocation,
|
||||
alertId,
|
||||
monitorSummary,
|
||||
statusConfig,
|
||||
downThreshold,
|
||||
useLatestChecks,
|
||||
locationNames: [statusConfig.ping.observer.geo?.name!],
|
||||
locationIds: [statusConfig.ping.observer.name!],
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
const downConfigsById = getConfigsByIds(downConfigs);
|
||||
|
||||
for (const [configId, configs] of downConfigsById) {
|
||||
const doesMonitorMeetLocationThreshold = getDoesMonitorMeetLocationThreshold({
|
||||
matchesByLocation: configs,
|
||||
locationsThreshold,
|
||||
downThreshold,
|
||||
useTimeWindow: useTimeWindow || false,
|
||||
});
|
||||
|
||||
if (doesMonitorMeetLocationThreshold) {
|
||||
const alertId = configId;
|
||||
const monitorSummary = this.getUngroupedDownSummary({
|
||||
statusConfigs: configs,
|
||||
});
|
||||
return this.scheduleAlert({
|
||||
idWithLocation: configId,
|
||||
alertId,
|
||||
monitorSummary,
|
||||
statusConfig: configs[0],
|
||||
downThreshold,
|
||||
useLatestChecks,
|
||||
locationNames: configs.map((c) => c.ping.observer.geo?.name!),
|
||||
locationIds: configs.map((c) => c.ping.observer.name!),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
getMonitorDownSummary({ statusConfig }: { statusConfig: AlertStatusMetaData }) {
|
||||
const { ping, configId, locationId, checks } = statusConfig;
|
||||
|
||||
return getMonitorSummary({
|
||||
monitorInfo: ping,
|
||||
statusMessage: DOWN_LABEL,
|
||||
locationId: [locationId],
|
||||
configId,
|
||||
dateFormat: this.dateFormat ?? 'Y-MM-DD HH:mm:ss',
|
||||
tz: this.tz ?? 'UTC',
|
||||
checks,
|
||||
params: this.params,
|
||||
});
|
||||
}
|
||||
|
||||
getUngroupedDownSummary({ statusConfigs }: { statusConfigs: AlertStatusMetaData[] }) {
|
||||
const sampleConfig = statusConfigs[0];
|
||||
const { ping, configId, checks } = sampleConfig;
|
||||
const baseSummary = getMonitorSummary({
|
||||
monitorInfo: ping,
|
||||
statusMessage: DOWN_LABEL,
|
||||
locationId: statusConfigs.map((c) => c.ping.observer.name!),
|
||||
configId,
|
||||
dateFormat: this.dateFormat!,
|
||||
tz: this.tz!,
|
||||
checks,
|
||||
params: this.params,
|
||||
});
|
||||
baseSummary.reason = getUngroupedReasonMessage({
|
||||
statusConfigs,
|
||||
monitorName: baseSummary.monitorName,
|
||||
params: this.params,
|
||||
});
|
||||
if (statusConfigs.length > 1) {
|
||||
baseSummary.locationNames = statusConfigs
|
||||
.map((c) => c.ping.observer.geo?.name!)
|
||||
.join(` ${AND_LABEL} `);
|
||||
}
|
||||
|
||||
return baseSummary;
|
||||
}
|
||||
|
||||
scheduleAlert({
|
||||
idWithLocation,
|
||||
alertId,
|
||||
monitorSummary,
|
||||
statusConfig,
|
||||
downThreshold,
|
||||
useLatestChecks = false,
|
||||
locationNames,
|
||||
locationIds,
|
||||
}: {
|
||||
idWithLocation: string;
|
||||
alertId: string;
|
||||
monitorSummary: MonitorSummaryStatusRule;
|
||||
statusConfig: AlertStatusMetaData;
|
||||
downThreshold: number;
|
||||
useLatestChecks?: boolean;
|
||||
locationNames: string[];
|
||||
locationIds: string[];
|
||||
}) {
|
||||
const { configId, locationId, checks } = statusConfig;
|
||||
const { spaceId, startedAt } = this.options;
|
||||
const { alertsClient } = this.options.services;
|
||||
const { basePath } = this.server;
|
||||
if (!alertsClient) return;
|
||||
|
||||
const { uuid: alertUuid, start } = alertsClient.report({
|
||||
id: alertId,
|
||||
actionGroup: MONITOR_STATUS.id,
|
||||
});
|
||||
const errorStartedAt = start ?? startedAt.toISOString() ?? monitorSummary.timestamp;
|
||||
|
||||
let relativeViewInAppUrl = '';
|
||||
if (monitorSummary.stateId) {
|
||||
relativeViewInAppUrl = getRelativeViewInAppUrl({
|
||||
configId,
|
||||
locationId,
|
||||
stateId: monitorSummary.stateId,
|
||||
});
|
||||
}
|
||||
|
||||
const context = {
|
||||
...monitorSummary,
|
||||
idWithLocation,
|
||||
checks,
|
||||
downThreshold,
|
||||
errorStartedAt,
|
||||
linkMessage: monitorSummary.stateId
|
||||
? getFullViewInAppMessage(basePath, spaceId, relativeViewInAppUrl)
|
||||
: '',
|
||||
[VIEW_IN_APP_URL]: getViewInAppUrl(basePath, spaceId, relativeViewInAppUrl),
|
||||
[ALERT_DETAILS_URL]: getAlertDetailsUrl(basePath, spaceId, alertUuid),
|
||||
};
|
||||
|
||||
const alertDocument = getMonitorAlertDocument(
|
||||
monitorSummary,
|
||||
locationNames,
|
||||
locationIds,
|
||||
useLatestChecks
|
||||
);
|
||||
|
||||
alertsClient.setAlertData({
|
||||
id: alertId,
|
||||
payload: alertDocument,
|
||||
context,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const getDoesMonitorMeetLocationThreshold = ({
|
||||
matchesByLocation,
|
||||
locationsThreshold,
|
||||
downThreshold,
|
||||
useTimeWindow,
|
||||
}: {
|
||||
matchesByLocation: AlertStatusMetaData[];
|
||||
locationsThreshold: number;
|
||||
downThreshold: number;
|
||||
useTimeWindow: boolean;
|
||||
}) => {
|
||||
// for location based we need to make sure, monitor is down for the threshold for all locations
|
||||
const getMatchingLocationsWithDownThresholdWithXChecks = (matches: AlertStatusMetaData[]) => {
|
||||
return matches.filter((config) => (config.checks?.downWithinXChecks ?? 1) >= downThreshold);
|
||||
};
|
||||
const getMatchingLocationsWithDownThresholdWithinTimeWindow = (
|
||||
matches: AlertStatusMetaData[]
|
||||
) => {
|
||||
return matches.filter((config) => (config.checks?.down ?? 1) >= downThreshold);
|
||||
};
|
||||
if (useTimeWindow) {
|
||||
const matchingLocationsWithDownThreshold =
|
||||
getMatchingLocationsWithDownThresholdWithinTimeWindow(matchesByLocation);
|
||||
return matchingLocationsWithDownThreshold.length >= locationsThreshold;
|
||||
} else {
|
||||
const matchingLocationsWithDownThreshold =
|
||||
getMatchingLocationsWithDownThresholdWithXChecks(matchesByLocation);
|
||||
return matchingLocationsWithDownThreshold.length >= locationsThreshold;
|
||||
}
|
||||
};
|
||||
|
||||
export const getConfigsByIds = (
|
||||
downConfigs: AlertStatusConfigs
|
||||
): Map<string, AlertStatusMetaData[]> => {
|
||||
const downConfigsById = new Map<string, AlertStatusMetaData[]>();
|
||||
Object.entries(downConfigs).forEach(([_, config]) => {
|
||||
const { configId } = config;
|
||||
if (!downConfigsById.has(configId)) {
|
||||
downConfigsById.set(configId, []);
|
||||
}
|
||||
downConfigsById.get(configId)?.push(config);
|
||||
});
|
||||
return downConfigsById;
|
||||
};
|
||||
|
|
|
@ -5,6 +5,47 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ObservabilityUptimeAlert } from '@kbn/alerts-as-data-utils';
|
||||
import { ActionGroupIdsOf } from '@kbn/alerting-types';
|
||||
import {
|
||||
AlertInstanceContext as AlertContext,
|
||||
RuleExecutorOptions,
|
||||
} from '@kbn/alerting-plugin/server';
|
||||
import { StatusRuleParams } from '../../../common/rules/status_rule';
|
||||
import { MONITOR_STATUS } from '../../../common/constants/synthetics_alerts';
|
||||
import {
|
||||
SyntheticsCommonState,
|
||||
SyntheticsMonitorStatusAlertState,
|
||||
} from '../../../common/runtime_types/alert_rules/common';
|
||||
|
||||
type MonitorStatusRuleTypeParams = StatusRuleParams;
|
||||
type MonitorStatusActionGroups = ActionGroupIdsOf<typeof MONITOR_STATUS>;
|
||||
type MonitorStatusRuleTypeState = SyntheticsCommonState;
|
||||
type MonitorStatusAlertState = SyntheticsMonitorStatusAlertState;
|
||||
type MonitorStatusAlertContext = AlertContext;
|
||||
|
||||
export type StatusRuleExecutorOptions = RuleExecutorOptions<
|
||||
MonitorStatusRuleTypeParams,
|
||||
MonitorStatusRuleTypeState,
|
||||
MonitorStatusAlertState,
|
||||
MonitorStatusAlertContext,
|
||||
MonitorStatusActionGroups,
|
||||
MonitorStatusAlertDocument
|
||||
>;
|
||||
|
||||
export type MonitorStatusAlertDocument = ObservabilityUptimeAlert &
|
||||
Required<
|
||||
Pick<
|
||||
ObservabilityUptimeAlert,
|
||||
| 'monitor.id'
|
||||
| 'monitor.type'
|
||||
| 'monitor.name'
|
||||
| 'configId'
|
||||
| 'observer.geo.name'
|
||||
| 'location.name'
|
||||
| 'location.id'
|
||||
>
|
||||
>;
|
||||
export interface MonitorSummaryStatusRule {
|
||||
reason: string;
|
||||
status: string;
|
||||
|
@ -17,8 +58,15 @@ export interface MonitorSummaryStatusRule {
|
|||
monitorType: string;
|
||||
monitorName: string;
|
||||
locationName: string;
|
||||
lastErrorMessage: string;
|
||||
stateId: string | null;
|
||||
locationNames: string;
|
||||
monitorUrlLabel: string;
|
||||
monitorTags?: string[];
|
||||
downThreshold: number;
|
||||
checks?: {
|
||||
downWithinXChecks: number;
|
||||
down: number;
|
||||
};
|
||||
stateId?: string;
|
||||
lastErrorMessage?: string;
|
||||
timestamp: string;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* 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';
|
||||
import { EncryptedSyntheticsMonitorAttributes, OverviewPing } from '../../../common/runtime_types';
|
||||
|
||||
export const getMonitorToPing = (
|
||||
monitor: EncryptedSyntheticsMonitorAttributes,
|
||||
locationId: string
|
||||
) => {
|
||||
const location = monitor.locations.find((loc) => loc.id === locationId);
|
||||
return {
|
||||
monitor: {
|
||||
id: monitor.id,
|
||||
name: monitor.name,
|
||||
type: monitor.type,
|
||||
},
|
||||
observer: {
|
||||
name: location?.id,
|
||||
geo: {
|
||||
name: location?.label,
|
||||
},
|
||||
},
|
||||
config_id: monitor.config_id,
|
||||
} as OverviewPing;
|
||||
};
|
||||
|
||||
export const getIntervalFromTimespan = (timespan: { gte: string; lt: string }) => {
|
||||
const start = moment(timespan.gte);
|
||||
const end = moment(timespan.lt);
|
||||
return end.diff(start, 'seconds');
|
||||
};
|
|
@ -88,6 +88,15 @@ export const commonMonitorStateI18: Array<{
|
|||
}
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'locationNames',
|
||||
description: i18n.translate(
|
||||
'xpack.synthetics.alertRules.monitorStatus.actionVariables.state.locationNames',
|
||||
{
|
||||
defaultMessage: 'Location names from which the checks are performed.',
|
||||
}
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'locationId',
|
||||
description: i18n.translate(
|
||||
|
|
|
@ -90,7 +90,7 @@ export class SyntheticsEsClient {
|
|||
esRequestStatus = RequestStatus.ERROR;
|
||||
}
|
||||
const isInspectorEnabled = await this.getInspectEnabled();
|
||||
if (isInspectorEnabled && this.request) {
|
||||
if ((isInspectorEnabled || this.isDev) && this.request) {
|
||||
this.inspectableEsQueries.push(
|
||||
getInspectResponse({
|
||||
esError,
|
||||
|
@ -102,7 +102,9 @@ export class SyntheticsEsClient {
|
|||
startTime: startTimeNow,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (isInspectorEnabled && this.request) {
|
||||
debugESCall({
|
||||
startTime,
|
||||
request: this.request,
|
||||
|
@ -218,9 +220,6 @@ export class SyntheticsEsClient {
|
|||
return {};
|
||||
}
|
||||
async getInspectEnabled() {
|
||||
if (this.isDev) {
|
||||
return true;
|
||||
}
|
||||
if (!this.uiSettings) {
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -5,8 +5,43 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { parseArrayFilters } from './common';
|
||||
import { getSavedObjectKqlFilter } from './common';
|
||||
|
||||
describe('common utils', () => {
|
||||
it('tests parseArrayFilters', () => {
|
||||
const filters = parseArrayFilters({
|
||||
configIds: ['1 4', '2 6', '5'],
|
||||
});
|
||||
expect(filters.filtersStr).toMatchInlineSnapshot(
|
||||
`"synthetics-monitor.attributes.config_id:(\\"1 4\\" OR \\"2 6\\" OR \\"5\\")"`
|
||||
);
|
||||
});
|
||||
it('tests parseArrayFilters with tags and configIds', () => {
|
||||
const filters = parseArrayFilters({
|
||||
configIds: ['1', '2'],
|
||||
tags: ['tag1', 'tag2'],
|
||||
});
|
||||
expect(filters.filtersStr).toMatchInlineSnapshot(
|
||||
`"synthetics-monitor.attributes.tags:(\\"tag1\\" OR \\"tag2\\") AND synthetics-monitor.attributes.config_id:(\\"1\\" OR \\"2\\")"`
|
||||
);
|
||||
});
|
||||
it('tests parseArrayFilters with all options', () => {
|
||||
const filters = parseArrayFilters({
|
||||
configIds: ['1', '2'],
|
||||
tags: ['tag1', 'tag2'],
|
||||
locations: ['loc1', 'loc2'],
|
||||
monitorTypes: ['type1', 'type2'],
|
||||
projects: ['project1', 'project2'],
|
||||
monitorQueryIds: ['query1', 'query2'],
|
||||
schedules: ['schedule1', 'schedule2'],
|
||||
});
|
||||
expect(filters.filtersStr).toMatchInlineSnapshot(
|
||||
`"synthetics-monitor.attributes.tags:(\\"tag1\\" OR \\"tag2\\") AND synthetics-monitor.attributes.project_id:(\\"project1\\" OR \\"project2\\") AND synthetics-monitor.attributes.type:(\\"type1\\" OR \\"type2\\") AND synthetics-monitor.attributes.schedule.number:(\\"schedule1\\" OR \\"schedule2\\") AND synthetics-monitor.attributes.id:(\\"query1\\" OR \\"query2\\") AND synthetics-monitor.attributes.config_id:(\\"1\\" OR \\"2\\")"`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSavedObjectKqlFilter', () => {
|
||||
it('returns empty string if no values are provided', () => {
|
||||
expect(getSavedObjectKqlFilter({ field: 'tags' })).toBe('');
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import { schema, TypeOf } from '@kbn/config-schema';
|
||||
import { SavedObjectsFindResponse } from '@kbn/core/server';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { escapeQuotes } from '@kbn/es-query';
|
||||
import { RouteContext } from './types';
|
||||
import { MonitorSortFieldSchema } from '../../common/runtime_types/monitor_management/sort_field';
|
||||
|
@ -110,16 +111,7 @@ export const getMonitors = async (
|
|||
return context.savedObjectsClient.find(findParams);
|
||||
};
|
||||
|
||||
export const getMonitorFilters = async ({
|
||||
tags,
|
||||
filter,
|
||||
locations,
|
||||
projects,
|
||||
monitorTypes,
|
||||
schedules,
|
||||
monitorQueryIds,
|
||||
context,
|
||||
}: {
|
||||
interface Filters {
|
||||
filter?: string;
|
||||
tags?: string | string[];
|
||||
monitorTypes?: string | string[];
|
||||
|
@ -127,10 +119,35 @@ export const getMonitorFilters = async ({
|
|||
projects?: string | string[];
|
||||
schedules?: string | string[];
|
||||
monitorQueryIds?: string | string[];
|
||||
context: RouteContext;
|
||||
}) => {
|
||||
}
|
||||
|
||||
export const getMonitorFilters = async (
|
||||
data: {
|
||||
context: RouteContext;
|
||||
} & Filters
|
||||
) => {
|
||||
const { context, locations } = data;
|
||||
const locationFilter = await parseLocationFilter(context, locations);
|
||||
|
||||
return parseArrayFilters({
|
||||
...data,
|
||||
locationFilter,
|
||||
});
|
||||
};
|
||||
|
||||
export const parseArrayFilters = ({
|
||||
tags,
|
||||
filter,
|
||||
configIds,
|
||||
projects,
|
||||
monitorTypes,
|
||||
schedules,
|
||||
monitorQueryIds,
|
||||
locationFilter,
|
||||
}: Filters & {
|
||||
locationFilter?: string | string[];
|
||||
configIds?: string[];
|
||||
}) => {
|
||||
const filtersStr = [
|
||||
filter,
|
||||
getSavedObjectKqlFilter({ field: 'tags', values: tags }),
|
||||
|
@ -139,9 +156,11 @@ export const getMonitorFilters = async ({
|
|||
getSavedObjectKqlFilter({ field: 'locations.id', values: locationFilter }),
|
||||
getSavedObjectKqlFilter({ field: 'schedule.number', values: schedules }),
|
||||
getSavedObjectKqlFilter({ field: 'id', values: monitorQueryIds }),
|
||||
getSavedObjectKqlFilter({ field: 'config_id', values: configIds }),
|
||||
]
|
||||
.filter((f) => !!f)
|
||||
.join(' AND ');
|
||||
|
||||
return { filtersStr, locationFilter };
|
||||
};
|
||||
|
||||
|
@ -156,7 +175,11 @@ export const getSavedObjectKqlFilter = ({
|
|||
operator?: string;
|
||||
searchAtRoot?: boolean;
|
||||
}) => {
|
||||
if (!values) {
|
||||
if (values === 'All' || (Array.isArray(values) && values?.includes('All'))) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (isEmpty(values) || !values) {
|
||||
return '';
|
||||
}
|
||||
let fieldKey = '';
|
||||
|
|
|
@ -17,21 +17,32 @@ import {
|
|||
LocationStatus,
|
||||
} from '../../common/runtime_types';
|
||||
|
||||
export const getDevLocation = (devUrl: string): PublicLocation => ({
|
||||
id: 'dev',
|
||||
label: 'Dev Service',
|
||||
geo: { lat: 0, lon: 0 },
|
||||
url: devUrl,
|
||||
isServiceManaged: true,
|
||||
status: LocationStatus.EXPERIMENTAL,
|
||||
isInvalid: false,
|
||||
});
|
||||
export const getDevLocation = (devUrl: string): PublicLocation[] => [
|
||||
{
|
||||
id: 'dev',
|
||||
label: 'Dev Service',
|
||||
geo: { lat: 0, lon: 0 },
|
||||
url: devUrl,
|
||||
isServiceManaged: true,
|
||||
status: LocationStatus.EXPERIMENTAL,
|
||||
isInvalid: false,
|
||||
},
|
||||
{
|
||||
id: 'dev2',
|
||||
label: 'Dev Service 2',
|
||||
geo: { lat: 0, lon: 0 },
|
||||
url: devUrl,
|
||||
isServiceManaged: true,
|
||||
status: LocationStatus.EXPERIMENTAL,
|
||||
isInvalid: false,
|
||||
},
|
||||
];
|
||||
|
||||
export async function getServiceLocations(server: SyntheticsServerSetup) {
|
||||
let locations: PublicLocations = [];
|
||||
|
||||
if (server.config.service?.devUrl) {
|
||||
locations = [getDevLocation(server.config.service.devUrl)];
|
||||
locations = getDevLocation(server.config.service.devUrl);
|
||||
}
|
||||
const manifestUrl = server.config.service?.manifestUrl;
|
||||
|
||||
|
|
|
@ -200,6 +200,18 @@ describe('SyntheticsService', () => {
|
|||
isServiceManaged: true,
|
||||
status: LocationStatus.EXPERIMENTAL,
|
||||
},
|
||||
{
|
||||
geo: {
|
||||
lat: 0,
|
||||
lon: 0,
|
||||
},
|
||||
id: 'dev2',
|
||||
isInvalid: false,
|
||||
isServiceManaged: true,
|
||||
label: 'Dev Service 2',
|
||||
status: 'experimental',
|
||||
url: 'http://localhost',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
|
|
|
@ -98,6 +98,10 @@
|
|||
"@kbn/presentation-util-plugin",
|
||||
"@kbn/core-application-browser",
|
||||
"@kbn/dashboard-plugin",
|
||||
"@kbn/search-types",
|
||||
"@kbn/slo-schema",
|
||||
"@kbn/alerting-types",
|
||||
"@kbn/babel-register",
|
||||
"@kbn/slo-plugin",
|
||||
"@kbn/ebt-tools",
|
||||
"@kbn/alerting-types"
|
||||
|
|
|
@ -17,8 +17,14 @@ export const uptimeRuleFieldMap: FieldMap = {
|
|||
type: 'keyword',
|
||||
required: false,
|
||||
},
|
||||
'observer.name': {
|
||||
type: 'keyword',
|
||||
array: true,
|
||||
required: false,
|
||||
},
|
||||
'observer.geo.name': {
|
||||
type: 'keyword',
|
||||
array: true,
|
||||
required: false,
|
||||
},
|
||||
// monitor status alert fields
|
||||
|
@ -43,6 +49,10 @@ export const uptimeRuleFieldMap: FieldMap = {
|
|||
array: true,
|
||||
required: false,
|
||||
},
|
||||
'monitor.state.id': {
|
||||
type: 'keyword',
|
||||
required: false,
|
||||
},
|
||||
configId: {
|
||||
type: 'keyword',
|
||||
required: false,
|
||||
|
@ -53,10 +63,12 @@ export const uptimeRuleFieldMap: FieldMap = {
|
|||
},
|
||||
'location.id': {
|
||||
type: 'keyword',
|
||||
array: true,
|
||||
required: false,
|
||||
},
|
||||
'location.name': {
|
||||
type: 'keyword',
|
||||
array: true,
|
||||
required: false,
|
||||
},
|
||||
// tls alert fields
|
||||
|
|
|
@ -113,7 +113,10 @@ const mockCommonAlertDocumentFields = (monitorInfo: GetMonitorStatusResult['moni
|
|||
'monitor.name': monitorInfo.monitor.name || monitorInfo.monitor.id,
|
||||
'monitor.type': monitorInfo.monitor.type,
|
||||
'url.full': monitorInfo.url?.full,
|
||||
'observer.geo.name': monitorInfo.observer?.geo?.name,
|
||||
'observer.geo.name': monitorInfo.observer?.geo?.name
|
||||
? [monitorInfo.observer.geo.name]
|
||||
: undefined,
|
||||
'observer.name': [],
|
||||
});
|
||||
|
||||
const mockStatusAlertDocument = (
|
||||
|
|
|
@ -213,8 +213,8 @@ export const getMonitorAlertDocument = (monitorSummary: MonitorSummary) => ({
|
|||
'monitor.name': monitorSummary.monitorName,
|
||||
'monitor.tags': monitorSummary.tags,
|
||||
'url.full': monitorSummary.monitorUrl,
|
||||
'observer.geo.name': monitorSummary.observerLocation,
|
||||
'observer.name': monitorSummary.observerName,
|
||||
'observer.geo.name': [monitorSummary.observerLocation],
|
||||
'observer.name': [monitorSummary.observerName!],
|
||||
'error.message': monitorSummary.latestErrorMessage,
|
||||
'agent.name': monitorSummary.observerHostname,
|
||||
[ALERT_REASON]: monitorSummary.reason,
|
||||
|
|
|
@ -43888,11 +43888,9 @@
|
|||
"xpack.synthetics.alertRules.monitorStatus.actionVariables.state.status": "Statut du moniteur (par ex. \"arrêté\").",
|
||||
"xpack.synthetics.alertRules.monitorStatus.browser.label": "navigateur",
|
||||
"xpack.synthetics.alertRules.monitorStatus.host.label": "Hôte",
|
||||
"xpack.synthetics.alertRules.monitorStatus.reasonMessage": "Le moniteur \"{name}\" de {location} est {status}. Vérifié à {checkedAt}.",
|
||||
"xpack.synthetics.alertRules.monitorStatus.unavailableUrlLabel": "(indisponible)",
|
||||
"xpack.synthetics.alerts.monitorStatus.absoluteLink.label": "- Lien",
|
||||
"xpack.synthetics.alerts.monitorStatus.defaultRecovery.status": "a récupéré",
|
||||
"xpack.synthetics.alerts.monitorStatus.deleteMonitor.reason": "le moniteur a été supprimé",
|
||||
"xpack.synthetics.alerts.monitorStatus.deleteMonitor.status": "a été supprimé",
|
||||
"xpack.synthetics.alerts.monitorStatus.downLabel": "bas",
|
||||
"xpack.synthetics.alerts.monitorStatus.relativeLink.label": "- Lien relatif",
|
||||
|
@ -43902,7 +43900,6 @@
|
|||
"xpack.synthetics.alerts.monitorStatus.upCheck.status": "est désormais disponible",
|
||||
"xpack.synthetics.alerts.settings.addConnector": "Ajouter un connecteur",
|
||||
"xpack.synthetics.alerts.syntheticsMonitorStatus.clientName": "Statut du moniteur",
|
||||
"xpack.synthetics.alerts.syntheticsMonitorStatus.defaultRecoverySubjectMessage": "\"{monitorName}\" ({locationName}) {recoveryStatus} - Elastic Synthetics",
|
||||
"xpack.synthetics.alerts.syntheticsMonitorStatus.description": "Alerte lorsqu'un moniteur est arrêté.",
|
||||
"xpack.synthetics.alerts.syntheticsMonitorTLS.defaultRecoverySubjectMessage": "L'alerte a été résolue pour le certificat {commonName} - Elastic Synthetics",
|
||||
"xpack.synthetics.alerts.syntheticsMonitorTLS.defaultSubjectMessage": "L'alerte a été déclenchée pour le certificat {commonName} - Elastic Synthetics",
|
||||
|
|
|
@ -43627,11 +43627,9 @@
|
|||
"xpack.synthetics.alertRules.monitorStatus.actionVariables.state.status": "監視ステータス(例:「ダウン」)。",
|
||||
"xpack.synthetics.alertRules.monitorStatus.browser.label": "ブラウザー",
|
||||
"xpack.synthetics.alertRules.monitorStatus.host.label": "ホスト",
|
||||
"xpack.synthetics.alertRules.monitorStatus.reasonMessage": "{location}のモニター\"{name}\"は{status}です。{checkedAt}に確認されました。",
|
||||
"xpack.synthetics.alertRules.monitorStatus.unavailableUrlLabel": "(使用不可)",
|
||||
"xpack.synthetics.alerts.monitorStatus.absoluteLink.label": "- リンク",
|
||||
"xpack.synthetics.alerts.monitorStatus.defaultRecovery.status": "回復しました",
|
||||
"xpack.synthetics.alerts.monitorStatus.deleteMonitor.reason": "モニターが削除されました",
|
||||
"xpack.synthetics.alerts.monitorStatus.deleteMonitor.status": "が削除されました",
|
||||
"xpack.synthetics.alerts.monitorStatus.downLabel": "ダウン",
|
||||
"xpack.synthetics.alerts.monitorStatus.relativeLink.label": "- 相対リンク",
|
||||
|
@ -43641,10 +43639,6 @@
|
|||
"xpack.synthetics.alerts.monitorStatus.upCheck.status": "現在起動しています",
|
||||
"xpack.synthetics.alerts.settings.addConnector": "コネクターの追加",
|
||||
"xpack.synthetics.alerts.syntheticsMonitorStatus.clientName": "監視ステータス",
|
||||
"xpack.synthetics.alerts.syntheticsMonitorStatus.defaultActionMessage": "{locationName}の\"{monitorName}\"は{status}です - Elastic Synthetics\n\n詳細:\n\n- モニター名:{monitorName} \n- {monitorUrlLabel}: {monitorUrl} \n- モニタータイプ:{monitorType} \n- 確認日時:{checkedAt} \n- 開始場所:{locationName} \n- 受信したエラー:{lastErrorMessage} \n{linkMessage}",
|
||||
"xpack.synthetics.alerts.syntheticsMonitorStatus.defaultRecoveryMessage": "{locationName}の\"{monitorName}\"のアラートはアクティブではありません:{recoveryReason} - Elastic Synthetics\n\n詳細:\n\n- モニター名:{monitorName} \n- {monitorUrlLabel}: {monitorUrl} \n- モニタータイプ:{monitorType} \n- 開始場所:{locationName} \n- 前回受信したエラー:{lastErrorMessage} \n{linkMessage}",
|
||||
"xpack.synthetics.alerts.syntheticsMonitorStatus.defaultRecoverySubjectMessage": "\"{monitorName}\" ({locationName}) {recoveryStatus} - Elastic Synthetics",
|
||||
"xpack.synthetics.alerts.syntheticsMonitorStatus.defaultSubjectMessage": "\"{monitorName}\" ({locationName})は停止しています - Elastic Synthetics",
|
||||
"xpack.synthetics.alerts.syntheticsMonitorStatus.description": "モニターがダウンしているときにアラートを通知します。",
|
||||
"xpack.synthetics.alerts.syntheticsMonitorTLS.defaultRecoverySubjectMessage": "証明書{commonName}のアラートが解決しました - Elastic Synthetics",
|
||||
"xpack.synthetics.alerts.syntheticsMonitorTLS.defaultSubjectMessage": "証明書{commonName}のアラートがトリガーされました - Elastic Synthetics",
|
||||
|
@ -44486,8 +44480,6 @@
|
|||
"xpack.synthetics.rules.tls.agingLabel": "古すぎます",
|
||||
"xpack.synthetics.rules.tls.clientName": "シンセティックTLS",
|
||||
"xpack.synthetics.rules.tls.criteriaExpression.ariaLabel": "このアラートで監視されているモニターの条件を示す式",
|
||||
"xpack.synthetics.rules.tls.defaultActionMessage": "TLS証明書{commonName} {status} - Elastic Synthetics\n\n詳細:\n\n- 概要:{summary}\n- 共通名:{commonName}\n- 発行元:{issuer}\n- モニター:{monitorName} \n- モニターURL:{monitorUrl} \n- モニタータイプ:{monitorType} \n- 開始場所:{locationName}",
|
||||
"xpack.synthetics.rules.tls.defaultRecoveryMessage": "モニター\"{monitorName}\"のTLSアラートが回復しました - Elastic Synthetics\n\n詳細:\n\n- 概要:{summary}\n- 新しいステータス:{newStatus}\n- 前のステータス:{previousStatus}\n- モニター:{monitorName} \n- URL:{monitorUrl} \n- モニタータイプ:{monitorType} \n- 開始場所:{locationName}",
|
||||
"xpack.synthetics.rules.tls.description": "シンセティック監視のTLS証明書の有効期限が近いときにアラートを発行します。",
|
||||
"xpack.synthetics.rules.tls.expiredLabel": "有効期限切れです",
|
||||
"xpack.synthetics.rules.tls.expiringLabel": "まもなく有効期限切れです",
|
||||
|
|
|
@ -43678,11 +43678,9 @@
|
|||
"xpack.synthetics.alertRules.monitorStatus.actionVariables.state.status": "监测状态(例如“关闭”)。",
|
||||
"xpack.synthetics.alertRules.monitorStatus.browser.label": "浏览器",
|
||||
"xpack.synthetics.alertRules.monitorStatus.host.label": "主机",
|
||||
"xpack.synthetics.alertRules.monitorStatus.reasonMessage": "来自 {location} 的监测“{name}”为 {status} 状态。已于 {checkedAt} 检查。",
|
||||
"xpack.synthetics.alertRules.monitorStatus.unavailableUrlLabel": "(不可用)",
|
||||
"xpack.synthetics.alerts.monitorStatus.absoluteLink.label": "- 链接",
|
||||
"xpack.synthetics.alerts.monitorStatus.defaultRecovery.status": "已恢复",
|
||||
"xpack.synthetics.alerts.monitorStatus.deleteMonitor.reason": "此监测已删除",
|
||||
"xpack.synthetics.alerts.monitorStatus.deleteMonitor.status": "已删除",
|
||||
"xpack.synthetics.alerts.monitorStatus.downLabel": "关闭",
|
||||
"xpack.synthetics.alerts.monitorStatus.relativeLink.label": "- 相对链接",
|
||||
|
@ -43692,10 +43690,6 @@
|
|||
"xpack.synthetics.alerts.monitorStatus.upCheck.status": "现已打开",
|
||||
"xpack.synthetics.alerts.settings.addConnector": "添加连接器",
|
||||
"xpack.synthetics.alerts.syntheticsMonitorStatus.clientName": "监测状态",
|
||||
"xpack.synthetics.alerts.syntheticsMonitorStatus.defaultActionMessage": "来自 {locationName} 的“{monitorName}”为 {status}。- Elastic Synthetics\n\n详情:\n\n- 监测名称:{monitorName} \n- {monitorUrlLabel}:{monitorUrl} \n- 监测类型:{monitorType} \n- 检查时间:{checkedAt} \n- 来自:{locationName} \n- 收到错误:{lastErrorMessage} \n{linkMessage}",
|
||||
"xpack.synthetics.alerts.syntheticsMonitorStatus.defaultRecoveryMessage": "来自 {locationName} 的“{monitorName}”的告警不再处于活动状态:{recoveryReason}。- Elastic Synthetics\n\n详情:\n\n- 监测名称:{monitorName} \n- {monitorUrlLabel}:{monitorUrl} \n- 监测类型:{monitorType} \n- 来自:{locationName} \n- 收到的上一个错误:{lastErrorMessage} \n{linkMessage}",
|
||||
"xpack.synthetics.alerts.syntheticsMonitorStatus.defaultRecoverySubjectMessage": "“{monitorName}”({locationName}) {recoveryStatus} - Elastic Synthetics",
|
||||
"xpack.synthetics.alerts.syntheticsMonitorStatus.defaultSubjectMessage": "“{monitorName}”({locationName}) 已关闭 - Elastic Synthetics",
|
||||
"xpack.synthetics.alerts.syntheticsMonitorStatus.description": "监测关闭时告警。",
|
||||
"xpack.synthetics.alerts.syntheticsMonitorTLS.defaultRecoverySubjectMessage": "告警已解析证书 {commonName} - Elastic Synthetics",
|
||||
"xpack.synthetics.alerts.syntheticsMonitorTLS.defaultSubjectMessage": "已针对证书 {commonName} 触发告警 - Elastic Synthetics",
|
||||
|
@ -44537,8 +44531,6 @@
|
|||
"xpack.synthetics.rules.tls.agingLabel": "过旧",
|
||||
"xpack.synthetics.rules.tls.clientName": "Synthetics TLS",
|
||||
"xpack.synthetics.rules.tls.criteriaExpression.ariaLabel": "显示正由此告警监视的监测条件的表达式",
|
||||
"xpack.synthetics.rules.tls.defaultActionMessage": "TLS 证书 {commonName} {status} - Elastic Synthetics\n\n详情:\n\n- 摘要:{summary}\n- 常见名称:{commonName}\n- 颁发者:{issuer}\n- 监测:{monitorName} \n- 监测 URL:{monitorUrl} \n- 监测类型:{monitorType} \n- 来自:{locationName}",
|
||||
"xpack.synthetics.rules.tls.defaultRecoveryMessage": "监测“{monitorName}”的 TLS 告警已恢复 - Elastic Synthetics\n\n详情:\n\n- 摘要:{summary}\n- 新状态:{newStatus}\n- 之前的状态:{previousStatus}\n- 监测:{monitorName} \n- URL:{monitorUrl} \n- 监测类型:{monitorType} \n- 来自:{locationName}",
|
||||
"xpack.synthetics.rules.tls.description": "Synthetics 监测的 TLS 证书即将到期时告警。",
|
||||
"xpack.synthetics.rules.tls.expiredLabel": "已过期",
|
||||
"xpack.synthetics.rules.tls.expiringLabel": "即将到期",
|
||||
|
|
|
@ -24,6 +24,7 @@ import { ClosablePopoverTitle } from './components';
|
|||
import { IErrorObject } from '../../types';
|
||||
|
||||
export interface ForLastExpressionProps {
|
||||
description?: string;
|
||||
timeWindowSize?: number;
|
||||
timeWindowUnit?: string;
|
||||
errors: IErrorObject;
|
||||
|
@ -45,6 +46,12 @@ export interface ForLastExpressionProps {
|
|||
display?: 'fullWidth' | 'inline';
|
||||
}
|
||||
|
||||
const FOR_LAST_LABEL = i18n.translate(
|
||||
'xpack.triggersActionsUI.common.expressionItems.forTheLast.descriptionLabel',
|
||||
{
|
||||
defaultMessage: 'for the last',
|
||||
}
|
||||
);
|
||||
export const ForLastExpression = ({
|
||||
timeWindowSize,
|
||||
timeWindowUnit = 's',
|
||||
|
@ -53,6 +60,7 @@ export const ForLastExpression = ({
|
|||
onChangeWindowSize,
|
||||
onChangeWindowUnit,
|
||||
popupPosition,
|
||||
description = FOR_LAST_LABEL,
|
||||
}: ForLastExpressionProps) => {
|
||||
const [alertDurationPopoverOpen, setAlertDurationPopoverOpen] = useState(false);
|
||||
|
||||
|
@ -60,12 +68,7 @@ export const ForLastExpression = ({
|
|||
<EuiPopover
|
||||
button={
|
||||
<EuiExpression
|
||||
description={i18n.translate(
|
||||
'xpack.triggersActionsUI.common.expressionItems.forTheLast.descriptionLabel',
|
||||
{
|
||||
defaultMessage: 'for the last',
|
||||
}
|
||||
)}
|
||||
description={description}
|
||||
data-test-subj="forLastExpression"
|
||||
value={`${timeWindowSize ?? '?'} ${getTimeUnitLabel(
|
||||
timeWindowUnit as TIME_UNITS,
|
||||
|
@ -98,12 +101,12 @@ export const ForLastExpression = ({
|
|||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFormRow
|
||||
isInvalid={Number(errors.timeWindowSize.length) > 0}
|
||||
isInvalid={Number(errors.timeWindowSize?.length) > 0}
|
||||
error={errors.timeWindowSize as string[]}
|
||||
>
|
||||
<EuiFieldNumber
|
||||
data-test-subj="timeWindowSizeNumber"
|
||||
isInvalid={Number(errors.timeWindowSize.length) > 0}
|
||||
isInvalid={Number(errors.timeWindowSize?.length) > 0}
|
||||
min={0}
|
||||
value={timeWindowSize || ''}
|
||||
onChange={(e) => {
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import React, { ReactNode, useState } from 'react';
|
||||
import {
|
||||
EuiExpression,
|
||||
EuiPopover,
|
||||
|
@ -20,6 +20,7 @@ import { IErrorObject } from '../../types';
|
|||
export interface ValueExpressionProps {
|
||||
description: string;
|
||||
value: number;
|
||||
valueLabel?: string | ReactNode;
|
||||
onChangeSelectedValue: (updatedValue: number) => void;
|
||||
popupPosition?:
|
||||
| 'upCenter'
|
||||
|
@ -41,6 +42,7 @@ export interface ValueExpressionProps {
|
|||
export const ValueExpression = ({
|
||||
description,
|
||||
value,
|
||||
valueLabel,
|
||||
onChangeSelectedValue,
|
||||
display = 'inline',
|
||||
popupPosition,
|
||||
|
@ -53,7 +55,7 @@ export const ValueExpression = ({
|
|||
<EuiExpression
|
||||
data-test-subj="valueExpression"
|
||||
description={description}
|
||||
value={value}
|
||||
value={valueLabel ?? value}
|
||||
isActive={valuePopoverOpen}
|
||||
display={display === 'inline' ? 'inline' : 'columns'}
|
||||
onClick={() => {
|
||||
|
|
|
@ -354,6 +354,10 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions)
|
|||
'--xpack.task_manager.allow_reading_invalid_state=false',
|
||||
'--xpack.actions.queued.max=500',
|
||||
`--xpack.stack_connectors.enableExperimental=${JSON.stringify(experimentalFeatures)}`,
|
||||
'--xpack.uptime.service.password=test',
|
||||
'--xpack.uptime.service.username=localKibanaIntegrationTestsUser',
|
||||
'--xpack.uptime.service.devUrl=mockDevUrl',
|
||||
'--xpack.uptime.service.manifestUrl=mockDevUrl',
|
||||
],
|
||||
},
|
||||
};
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import type { Client } from '@elastic/elasticsearch';
|
||||
import type { Agent as SuperTestAgent } from 'supertest';
|
||||
import expect from '@kbn/expect';
|
||||
import { ToolingLog } from '@kbn/tooling-log';
|
||||
import { ThresholdParams } from '@kbn/observability-plugin/common/custom_threshold_rule/types';
|
||||
import { refreshSavedObjectIndices } from './refresh_index';
|
||||
|
@ -62,7 +63,7 @@ export async function createRule<Params = ThresholdParams>({
|
|||
logger: ToolingLog;
|
||||
esClient: Client;
|
||||
}) {
|
||||
const { body } = await supertest
|
||||
const { body, status } = await supertest
|
||||
.post(`/api/alerting/rule`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
|
@ -75,8 +76,9 @@ export async function createRule<Params = ThresholdParams>({
|
|||
name,
|
||||
rule_type_id: ruleTypeId,
|
||||
actions,
|
||||
})
|
||||
.expect(200);
|
||||
});
|
||||
|
||||
expect(status).to.eql(200, JSON.stringify(body));
|
||||
|
||||
await refreshSavedObjectIndices(esClient);
|
||||
logger.debug(`Created rule id: ${body.id}`);
|
||||
|
|
|
@ -14,6 +14,7 @@ import type {
|
|||
SearchResponse,
|
||||
} from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import type { RetryService } from '@kbn/ftr-common-functional-services';
|
||||
import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
|
||||
import { retry } from '../../common/retry';
|
||||
|
||||
const TIMEOUT = 70_000;
|
||||
|
@ -63,6 +64,7 @@ export async function waitForDocumentInIndex<T>({
|
|||
timeout = TIMEOUT,
|
||||
retries = RETRIES,
|
||||
retryDelay = RETRY_DELAY,
|
||||
filters,
|
||||
}: {
|
||||
esClient: Client;
|
||||
indexName: string;
|
||||
|
@ -72,6 +74,7 @@ export async function waitForDocumentInIndex<T>({
|
|||
timeout?: number;
|
||||
retries?: number;
|
||||
retryDelay?: number;
|
||||
filters?: QueryDslQueryContainer[];
|
||||
}): Promise<SearchResponse<T, Record<string, AggregationsAggregate>>> {
|
||||
return await retry<SearchResponse<T, Record<string, AggregationsAggregate>>>({
|
||||
testFn: async () => {
|
||||
|
@ -79,6 +82,15 @@ export async function waitForDocumentInIndex<T>({
|
|||
index: indexName,
|
||||
rest_total_hits_as_int: true,
|
||||
ignore_unavailable: true,
|
||||
body: filters
|
||||
? {
|
||||
query: {
|
||||
bool: {
|
||||
filter: filters,
|
||||
},
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
if (!response.hits.total || (response.hits.total as number) < docCountTarget) {
|
||||
logger.debug(`Document count is ${response.hits.total}, should be ${docCountTarget}`);
|
||||
|
@ -104,12 +116,16 @@ export async function waitForAlertInIndex<T>({
|
|||
ruleId,
|
||||
retryService,
|
||||
logger,
|
||||
filters = [],
|
||||
retryDelay,
|
||||
}: {
|
||||
esClient: Client;
|
||||
indexName: string;
|
||||
ruleId: string;
|
||||
retryService: RetryService;
|
||||
logger: ToolingLog;
|
||||
filters?: QueryDslQueryContainer[];
|
||||
retryDelay?: number;
|
||||
}): Promise<SearchResponse<T, Record<string, AggregationsAggregate>>> {
|
||||
return await retry<SearchResponse<T, Record<string, AggregationsAggregate>>>({
|
||||
testFn: async () => {
|
||||
|
@ -117,14 +133,21 @@ export async function waitForAlertInIndex<T>({
|
|||
index: indexName,
|
||||
body: {
|
||||
query: {
|
||||
term: {
|
||||
'kibana.alert.rule.uuid': ruleId,
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
term: {
|
||||
'kibana.alert.rule.uuid': ruleId,
|
||||
},
|
||||
},
|
||||
...filters,
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
if (response.hits.hits.length === 0) {
|
||||
throw new Error('No hits found');
|
||||
throw new Error(`No hits found for the ruleId: ${ruleId}`);
|
||||
}
|
||||
return response;
|
||||
},
|
||||
|
@ -133,6 +156,6 @@ export async function waitForAlertInIndex<T>({
|
|||
retryService,
|
||||
timeout: TIMEOUT,
|
||||
retries: RETRIES,
|
||||
retryDelay: RETRY_DELAY,
|
||||
retryDelay: retryDelay ?? RETRY_DELAY,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -21,7 +21,8 @@ export default function ({ loadTestFile }: any) {
|
|||
loadTestFile(require.resolve('./custom_threshold_rule_data_view'));
|
||||
});
|
||||
describe('Synthetics', () => {
|
||||
loadTestFile(require.resolve('./synthetics_rule'));
|
||||
loadTestFile(require.resolve('./synthetics/synthetics_default_rule'));
|
||||
loadTestFile(require.resolve('./synthetics/custom_status_rule'));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -5,132 +5,13 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { SYNTHETICS_API_URLS } from '@kbn/synthetics-plugin/common/constants';
|
||||
import { SanitizedRule } from '@kbn/alerting-plugin/common';
|
||||
import { omit } from 'lodash';
|
||||
import { TlsTranslations } from '@kbn/synthetics-plugin/common/rules/synthetics/translations';
|
||||
import { FtrProviderContext } from '../common/ftr_provider_context';
|
||||
import {
|
||||
SyntheticsMonitorStatusTranslations,
|
||||
TlsTranslations,
|
||||
} from '@kbn/synthetics-plugin/common/rules/synthetics/translations';
|
||||
import { SanitizedRule } from '@kbn/alerting-types';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function ({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const server = getService('kibanaServer');
|
||||
|
||||
const testActions = [
|
||||
'custom.ssl.noCustom',
|
||||
'notification-email',
|
||||
'preconfigured-es-index-action',
|
||||
'my-deprecated-servicenow',
|
||||
'my-slack1',
|
||||
];
|
||||
|
||||
describe('SyntheticsRules', () => {
|
||||
before(async () => {
|
||||
await server.savedObjects.cleanStandardList();
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await server.savedObjects.cleanStandardList();
|
||||
});
|
||||
|
||||
it('creates rule when settings are configured', async () => {
|
||||
await supertest
|
||||
.put(SYNTHETICS_API_URLS.DYNAMIC_SETTINGS)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({
|
||||
certExpirationThreshold: 30,
|
||||
certAgeThreshold: 730,
|
||||
defaultConnectors: testActions.slice(0, 2),
|
||||
defaultEmail: { to: ['test@gmail.com'], cc: [], bcc: [] },
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const response = await supertest
|
||||
.post(SYNTHETICS_API_URLS.ENABLE_DEFAULT_ALERTING)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send();
|
||||
const statusResult = response.body.statusRule;
|
||||
const tlsResult = response.body.tlsRule;
|
||||
expect(statusResult.actions.length).eql(4);
|
||||
expect(tlsResult.actions.length).eql(4);
|
||||
|
||||
compareRules(statusResult, statusRule);
|
||||
compareRules(tlsResult, tlsRule);
|
||||
|
||||
testActions.slice(0, 2).forEach((action) => {
|
||||
const { recoveredAction, firingAction } = getActionById(statusRule, action);
|
||||
const resultAction = getActionById(statusResult, action);
|
||||
expect(firingAction).eql(resultAction.firingAction);
|
||||
expect(recoveredAction).eql(resultAction.recoveredAction);
|
||||
});
|
||||
|
||||
testActions.slice(0, 2).forEach((action) => {
|
||||
const { recoveredAction, firingAction } = getActionById(tlsRule, action);
|
||||
const resultAction = getActionById(tlsResult, action);
|
||||
expect(firingAction).eql(resultAction.firingAction);
|
||||
expect(recoveredAction).eql(resultAction.recoveredAction);
|
||||
});
|
||||
});
|
||||
|
||||
it('updates rules when settings are updated', async () => {
|
||||
await supertest
|
||||
.put(SYNTHETICS_API_URLS.DYNAMIC_SETTINGS)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({
|
||||
certExpirationThreshold: 30,
|
||||
certAgeThreshold: 730,
|
||||
defaultConnectors: testActions,
|
||||
defaultEmail: { to: ['test@gmail.com'], cc: [], bcc: [] },
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const response = await supertest
|
||||
.put(SYNTHETICS_API_URLS.ENABLE_DEFAULT_ALERTING)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send();
|
||||
|
||||
const statusResult = response.body.statusRule;
|
||||
const tlsResult = response.body.tlsRule;
|
||||
expect(statusResult.actions.length).eql(9);
|
||||
expect(tlsResult.actions.length).eql(9);
|
||||
|
||||
compareRules(statusResult, statusRule);
|
||||
compareRules(tlsResult, tlsRule);
|
||||
|
||||
testActions.forEach((action) => {
|
||||
const { recoveredAction, firingAction } = getActionById(statusRule, action);
|
||||
const resultAction = getActionById(statusResult, action);
|
||||
expect(firingAction).eql(resultAction.firingAction);
|
||||
expect(recoveredAction).eql(resultAction.recoveredAction);
|
||||
});
|
||||
testActions.forEach((action) => {
|
||||
const { recoveredAction, firingAction } = getActionById(tlsRule, action);
|
||||
const resultAction = getActionById(tlsResult, action);
|
||||
expect(firingAction).eql(resultAction.firingAction);
|
||||
expect(recoveredAction).eql(resultAction.recoveredAction);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
const compareRules = (rule1: SanitizedRule, rule2: SanitizedRule) => {
|
||||
expect(rule1.alertTypeId).eql(rule2.alertTypeId);
|
||||
expect(rule1.schedule).eql(rule2.schedule);
|
||||
};
|
||||
|
||||
const getActionById = (rule: SanitizedRule, id: string) => {
|
||||
const actions = rule.actions.filter((action) => action.id === id);
|
||||
const recoveredAction = actions.find(
|
||||
(action) => 'group' in action && action.group === 'recovered'
|
||||
);
|
||||
const firingAction = actions.find((action) => 'group' in action && action.group !== 'recovered');
|
||||
return {
|
||||
recoveredAction: omit(recoveredAction, ['uuid']),
|
||||
firingAction: omit(firingAction, ['uuid']),
|
||||
};
|
||||
};
|
||||
|
||||
const statusRule = {
|
||||
export const statusRule = {
|
||||
id: 'dbbc39f0-1781-11ee-80b9-6522650f1d50',
|
||||
notifyWhen: null,
|
||||
consumer: 'uptime',
|
||||
|
@ -152,7 +33,7 @@ const statusRule = {
|
|||
{
|
||||
group: 'recovered',
|
||||
params: {
|
||||
body: 'The alert for "{{context.monitorName}}" from {{context.locationName}} is no longer active: {{context.recoveryReason}}. - Elastic Synthetics\n\nDetails:\n\n- Monitor name: {{context.monitorName}} \n- {{context.monitorUrlLabel}}: {{{context.monitorUrl}}} \n- Monitor type: {{context.monitorType}} \n- From: {{context.locationName}} \n- Last error received: {{{context.lastErrorMessage}}} \n{{{context.linkMessage}}}',
|
||||
body: SyntheticsMonitorStatusTranslations.defaultRecoveryMessage,
|
||||
},
|
||||
frequency: { notifyWhen: 'onActionGroupChange', throttle: null, summary: false },
|
||||
uuid: '789f2b81-e098-4f33-9802-1d355f4fabbe',
|
||||
|
@ -162,7 +43,7 @@ const statusRule = {
|
|||
{
|
||||
group: 'xpack.synthetics.alerts.actionGroups.monitorStatus',
|
||||
params: {
|
||||
body: '"{{context.monitorName}}" is {{{context.status}}} from {{context.locationName}}. - Elastic Synthetics\n\nDetails:\n\n- Monitor name: {{context.monitorName}} \n- {{context.monitorUrlLabel}}: {{{context.monitorUrl}}} \n- Monitor type: {{context.monitorType}} \n- Checked at: {{context.checkedAt}} \n- From: {{context.locationName}} \n- Error received: {{{context.lastErrorMessage}}} \n{{{context.linkMessage}}}',
|
||||
body: SyntheticsMonitorStatusTranslations.defaultActionMessage,
|
||||
},
|
||||
frequency: { notifyWhen: 'onActionGroupChange', throttle: null, summary: false },
|
||||
uuid: '1b3f3958-f019-4ca0-b6b1-ccc4cf51d501',
|
||||
|
@ -173,10 +54,8 @@ const statusRule = {
|
|||
group: 'recovered',
|
||||
params: {
|
||||
to: ['test@gmail.com'],
|
||||
subject:
|
||||
'"{{context.monitorName}}" ({{context.locationName}}) {{context.recoveryStatus}} - Elastic Synthetics',
|
||||
message:
|
||||
'The alert for "{{context.monitorName}}" from {{context.locationName}} is no longer active: {{context.recoveryReason}}. - Elastic Synthetics\n\nDetails:\n\n- Monitor name: {{context.monitorName}} \n- {{context.monitorUrlLabel}}: {{{context.monitorUrl}}} \n- Monitor type: {{context.monitorType}} \n- From: {{context.locationName}} \n- Last error received: {{{context.lastErrorMessage}}} \n{{{context.linkMessage}}}',
|
||||
subject: SyntheticsMonitorStatusTranslations.defaultRecoverySubjectMessage,
|
||||
message: SyntheticsMonitorStatusTranslations.defaultRecoveryMessage,
|
||||
messageHTML: null,
|
||||
cc: [],
|
||||
bcc: [],
|
||||
|
@ -191,10 +70,8 @@ const statusRule = {
|
|||
group: 'xpack.synthetics.alerts.actionGroups.monitorStatus',
|
||||
params: {
|
||||
to: ['test@gmail.com'],
|
||||
subject:
|
||||
'"{{context.monitorName}}" ({{context.locationName}}) is down - Elastic Synthetics',
|
||||
message:
|
||||
'"{{context.monitorName}}" is {{{context.status}}} from {{context.locationName}}. - Elastic Synthetics\n\nDetails:\n\n- Monitor name: {{context.monitorName}} \n- {{context.monitorUrlLabel}}: {{{context.monitorUrl}}} \n- Monitor type: {{context.monitorType}} \n- Checked at: {{context.checkedAt}} \n- From: {{context.locationName}} \n- Error received: {{{context.lastErrorMessage}}} \n{{{context.linkMessage}}}',
|
||||
subject: SyntheticsMonitorStatusTranslations.defaultSubjectMessage,
|
||||
message: SyntheticsMonitorStatusTranslations.defaultActionMessage,
|
||||
messageHTML: null,
|
||||
cc: [],
|
||||
bcc: [],
|
||||
|
@ -250,10 +127,8 @@ const statusRule = {
|
|||
subAction: 'pushToService',
|
||||
subActionParams: {
|
||||
incident: {
|
||||
short_description:
|
||||
'"{{context.monitorName}}" is {{{context.status}}} from {{context.locationName}}. - Elastic Synthetics\n\nDetails:\n\n- Monitor name: {{context.monitorName}} \n- {{context.monitorUrlLabel}}: {{{context.monitorUrl}}} \n- Monitor type: {{context.monitorType}} \n- Checked at: {{context.checkedAt}} \n- From: {{context.locationName}} \n- Error received: {{{context.lastErrorMessage}}} \n{{{context.linkMessage}}}',
|
||||
description:
|
||||
'"{{context.monitorName}}" is {{{context.status}}} from {{context.locationName}}. - Elastic Synthetics\n\nDetails:\n\n- Monitor name: {{context.monitorName}} \n- {{context.monitorUrlLabel}}: {{{context.monitorUrl}}} \n- Monitor type: {{context.monitorType}} \n- Checked at: {{context.checkedAt}} \n- From: {{context.locationName}} \n- Error received: {{{context.lastErrorMessage}}} \n{{{context.linkMessage}}}',
|
||||
short_description: SyntheticsMonitorStatusTranslations.defaultActionMessage,
|
||||
description: SyntheticsMonitorStatusTranslations.defaultActionMessage,
|
||||
impact: '2',
|
||||
severity: '2',
|
||||
urgency: '2',
|
||||
|
@ -275,8 +150,7 @@ const statusRule = {
|
|||
{
|
||||
group: 'recovered',
|
||||
params: {
|
||||
message:
|
||||
'The alert for "{{context.monitorName}}" from {{context.locationName}} is no longer active: {{context.recoveryReason}}. - Elastic Synthetics\n\nDetails:\n\n- Monitor name: {{context.monitorName}} \n- {{context.monitorUrlLabel}}: {{{context.monitorUrl}}} \n- Monitor type: {{context.monitorType}} \n- From: {{context.locationName}} \n- Last error received: {{{context.lastErrorMessage}}} \n{{{context.linkMessage}}}',
|
||||
message: SyntheticsMonitorStatusTranslations.defaultRecoveryMessage,
|
||||
},
|
||||
frequency: { notifyWhen: 'onActionGroupChange', throttle: null, summary: false },
|
||||
uuid: '2d73f370-a90c-4347-8480-753cbeae719f',
|
||||
|
@ -286,8 +160,7 @@ const statusRule = {
|
|||
{
|
||||
group: 'xpack.synthetics.alerts.actionGroups.monitorStatus',
|
||||
params: {
|
||||
message:
|
||||
'"{{context.monitorName}}" is {{{context.status}}} from {{context.locationName}}. - Elastic Synthetics\n\nDetails:\n\n- Monitor name: {{context.monitorName}} \n- {{context.monitorUrlLabel}}: {{{context.monitorUrl}}} \n- Monitor type: {{context.monitorType}} \n- Checked at: {{context.checkedAt}} \n- From: {{context.locationName}} \n- Error received: {{{context.lastErrorMessage}}} \n{{{context.linkMessage}}}',
|
||||
message: SyntheticsMonitorStatusTranslations.defaultActionMessage,
|
||||
},
|
||||
frequency: { notifyWhen: 'onActionGroupChange', throttle: null, summary: false },
|
||||
uuid: '1c5d0dd1-c360-4e14-8e4f-f24aa5c640c6',
|
||||
|
@ -339,7 +212,8 @@ const statusRule = {
|
|||
},
|
||||
ruleTypeId: 'xpack.synthetics.alerts.monitorStatus',
|
||||
} as unknown as SanitizedRule;
|
||||
const tlsRule = {
|
||||
|
||||
export const tlsRule = {
|
||||
id: 'dbbc12e0-1781-11ee-80b9-6522650f1d50',
|
||||
notifyWhen: null,
|
||||
consumer: 'uptime',
|
|
@ -0,0 +1,83 @@
|
|||
/*
|
||||
* 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 { v4 as uuidv4 } from 'uuid';
|
||||
import { privateLocationsSavedObjectName } from '@kbn/synthetics-plugin/common/saved_objects/private_locations';
|
||||
import { privateLocationsSavedObjectId } from '@kbn/synthetics-plugin/server/saved_objects/private_locations';
|
||||
import { SyntheticsPrivateLocations } from '@kbn/synthetics-plugin/common/runtime_types';
|
||||
import { Agent as SuperTestAgent } from 'supertest';
|
||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
||||
|
||||
export const INSTALLED_VERSION = '1.1.1';
|
||||
|
||||
export class PrivateLocationTestService {
|
||||
private supertest: SuperTestAgent;
|
||||
private readonly getService: FtrProviderContext['getService'];
|
||||
|
||||
constructor(getService: FtrProviderContext['getService']) {
|
||||
this.supertest = getService('supertest');
|
||||
this.getService = getService;
|
||||
}
|
||||
|
||||
async installSyntheticsPackage() {
|
||||
await this.supertest.post('/api/fleet/setup').set('kbn-xsrf', 'true').send().expect(200);
|
||||
const response = await this.supertest
|
||||
.get(`/api/fleet/epm/packages/synthetics/${INSTALLED_VERSION}`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.expect(200);
|
||||
if (response.body.item.status !== 'installed') {
|
||||
await this.supertest
|
||||
.post(`/api/fleet/epm/packages/synthetics/${INSTALLED_VERSION}`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({ force: true })
|
||||
.expect(200);
|
||||
}
|
||||
}
|
||||
|
||||
async addTestPrivateLocation() {
|
||||
const apiResponse = await this.addFleetPolicy(uuidv4());
|
||||
const testPolicyId = apiResponse.body.item.id;
|
||||
return (await this.setTestLocations([testPolicyId]))[0];
|
||||
}
|
||||
|
||||
async addFleetPolicy(name: string) {
|
||||
return this.supertest
|
||||
.post('/api/fleet/agent_policies?sys_monitoring=true')
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({
|
||||
name,
|
||||
description: '',
|
||||
namespace: 'default',
|
||||
monitoring_enabled: [],
|
||||
})
|
||||
.expect(200);
|
||||
}
|
||||
|
||||
async setTestLocations(testFleetPolicyIds: string[]) {
|
||||
const server = this.getService('kibanaServer');
|
||||
|
||||
const locations: SyntheticsPrivateLocations = testFleetPolicyIds.map((id, index) => ({
|
||||
label: 'Test private location ' + index,
|
||||
agentPolicyId: id,
|
||||
id,
|
||||
geo: {
|
||||
lat: 0,
|
||||
lon: 0,
|
||||
},
|
||||
isServiceManaged: false,
|
||||
}));
|
||||
|
||||
await server.savedObjects.create({
|
||||
type: privateLocationsSavedObjectName,
|
||||
id: privateLocationsSavedObjectId,
|
||||
attributes: {
|
||||
locations,
|
||||
},
|
||||
overwrite: true,
|
||||
});
|
||||
return locations;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { SYNTHETICS_API_URLS } from '@kbn/synthetics-plugin/common/constants';
|
||||
import { SanitizedRule } from '@kbn/alerting-plugin/common';
|
||||
import { omit } from 'lodash';
|
||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
||||
import { statusRule, tlsRule } from './data';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function ({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const server = getService('kibanaServer');
|
||||
|
||||
const testActions = [
|
||||
'custom.ssl.noCustom',
|
||||
'notification-email',
|
||||
'preconfigured-es-index-action',
|
||||
'my-deprecated-servicenow',
|
||||
'my-slack1',
|
||||
];
|
||||
|
||||
describe('SyntheticsDefaultRules', () => {
|
||||
before(async () => {
|
||||
await server.savedObjects.cleanStandardList();
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await server.savedObjects.cleanStandardList();
|
||||
});
|
||||
|
||||
it('creates rule when settings are configured', async () => {
|
||||
await supertest
|
||||
.put(SYNTHETICS_API_URLS.DYNAMIC_SETTINGS)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({
|
||||
certExpirationThreshold: 30,
|
||||
certAgeThreshold: 730,
|
||||
defaultConnectors: testActions.slice(0, 2),
|
||||
defaultEmail: { to: ['test@gmail.com'], cc: [], bcc: [] },
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const response = await supertest
|
||||
.post(SYNTHETICS_API_URLS.ENABLE_DEFAULT_ALERTING)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send();
|
||||
const statusResult = response.body.statusRule;
|
||||
const tlsResult = response.body.tlsRule;
|
||||
expect(statusResult.actions.length).eql(4);
|
||||
expect(tlsResult.actions.length).eql(4);
|
||||
|
||||
compareRules(statusResult, statusRule);
|
||||
compareRules(tlsResult, tlsRule);
|
||||
|
||||
testActions.slice(0, 2).forEach((action) => {
|
||||
const { recoveredAction, firingAction } = getActionById(statusRule, action);
|
||||
const resultAction = getActionById(statusResult, action);
|
||||
expect(firingAction).eql(resultAction.firingAction);
|
||||
expect(recoveredAction).eql(resultAction.recoveredAction);
|
||||
});
|
||||
|
||||
testActions.slice(0, 2).forEach((action) => {
|
||||
const { recoveredAction, firingAction } = getActionById(tlsRule, action);
|
||||
const resultAction = getActionById(tlsResult, action);
|
||||
expect(firingAction).eql(resultAction.firingAction);
|
||||
expect(recoveredAction).eql(resultAction.recoveredAction);
|
||||
});
|
||||
});
|
||||
|
||||
it('updates rules when settings are updated', async () => {
|
||||
await supertest
|
||||
.put(SYNTHETICS_API_URLS.DYNAMIC_SETTINGS)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({
|
||||
certExpirationThreshold: 30,
|
||||
certAgeThreshold: 730,
|
||||
defaultConnectors: testActions,
|
||||
defaultEmail: { to: ['test@gmail.com'], cc: [], bcc: [] },
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const response = await supertest
|
||||
.put(SYNTHETICS_API_URLS.ENABLE_DEFAULT_ALERTING)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send();
|
||||
|
||||
const statusResult = response.body.statusRule;
|
||||
const tlsResult = response.body.tlsRule;
|
||||
expect(statusResult.actions.length).eql(9);
|
||||
expect(tlsResult.actions.length).eql(9);
|
||||
|
||||
compareRules(statusResult, statusRule);
|
||||
compareRules(tlsResult, tlsRule);
|
||||
|
||||
testActions.forEach((action) => {
|
||||
const { recoveredAction, firingAction } = getActionById(statusRule, action);
|
||||
const resultAction = getActionById(statusResult, action);
|
||||
expect(firingAction).eql(resultAction.firingAction);
|
||||
expect(recoveredAction).eql(resultAction.recoveredAction);
|
||||
});
|
||||
testActions.forEach((action) => {
|
||||
const { recoveredAction, firingAction } = getActionById(tlsRule, action);
|
||||
const resultAction = getActionById(tlsResult, action);
|
||||
expect(firingAction).eql(resultAction.firingAction);
|
||||
expect(recoveredAction).eql(resultAction.recoveredAction);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
const compareRules = (rule1: SanitizedRule, rule2: SanitizedRule) => {
|
||||
expect(rule1.alertTypeId).eql(rule2.alertTypeId);
|
||||
expect(rule1.schedule).eql(rule2.schedule);
|
||||
};
|
||||
|
||||
const getActionById = (rule: SanitizedRule, id: string) => {
|
||||
const actions = rule.actions.filter((action) => action.id === id);
|
||||
const recoveredAction = actions.find(
|
||||
(action) => 'group' in action && action.group === 'recovered'
|
||||
);
|
||||
const firingAction = actions.find((action) => 'group' in action && action.group !== 'recovered');
|
||||
return {
|
||||
recoveredAction: omit(recoveredAction, ['uuid']),
|
||||
firingAction: omit(firingAction, ['uuid']),
|
||||
};
|
||||
};
|
|
@ -0,0 +1,292 @@
|
|||
/*
|
||||
* 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 { StatusRuleParams } from '@kbn/synthetics-plugin/common/rules/status_rule';
|
||||
import type { Client } from '@elastic/elasticsearch';
|
||||
import { ToolingLog } from '@kbn/tooling-log';
|
||||
import { makeDownSummary, makeUpSummary } from '@kbn/observability-synthetics-test-data';
|
||||
import { RetryService } from '@kbn/ftr-common-functional-services';
|
||||
import { EncryptedSyntheticsSavedMonitor } from '@kbn/synthetics-plugin/common/runtime_types';
|
||||
import moment from 'moment';
|
||||
import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
|
||||
import { Agent as SuperTestAgent } from 'supertest';
|
||||
import { SYNTHETICS_API_URLS } from '@kbn/synthetics-plugin/common/constants';
|
||||
import expect from '@kbn/expect';
|
||||
import { waitForAlertInIndex } from '../helpers/alerting_wait_for_helpers';
|
||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
||||
import { PrivateLocationTestService } from './private_location_test_service';
|
||||
import { createIndexConnector, createRule } from '../helpers/alerting_api_helper';
|
||||
|
||||
export const SYNTHETICS_ALERT_ACTION_INDEX = 'alert-action-synthetics';
|
||||
export class SyntheticsRuleHelper {
|
||||
supertest: SuperTestAgent;
|
||||
logger: ToolingLog;
|
||||
esClient: Client;
|
||||
retryService: RetryService;
|
||||
locService: PrivateLocationTestService;
|
||||
alertActionIndex: string;
|
||||
actionId: string | null = null;
|
||||
|
||||
constructor(getService: FtrProviderContext['getService']) {
|
||||
this.esClient = getService('es');
|
||||
this.supertest = getService('supertest');
|
||||
this.logger = getService('log');
|
||||
this.retryService = getService('retry');
|
||||
this.locService = new PrivateLocationTestService(getService);
|
||||
this.alertActionIndex = SYNTHETICS_ALERT_ACTION_INDEX;
|
||||
}
|
||||
|
||||
async createIndexAction() {
|
||||
await this.esClient.indices.create({
|
||||
index: this.alertActionIndex,
|
||||
body: {
|
||||
mappings: {
|
||||
properties: {
|
||||
'monitor.id': {
|
||||
type: 'keyword',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const actionId = await createIndexConnector({
|
||||
supertest: this.supertest,
|
||||
name: 'Index Connector: Synthetics API test',
|
||||
indexName: this.alertActionIndex,
|
||||
logger: this.logger,
|
||||
});
|
||||
this.actionId = actionId;
|
||||
}
|
||||
|
||||
async createCustomStatusRule({
|
||||
params,
|
||||
name,
|
||||
}: {
|
||||
params: StatusRuleParams;
|
||||
name?: string;
|
||||
actions?: any[];
|
||||
}) {
|
||||
if (this.actionId === null) {
|
||||
throw new Error('Index action not created. Call createIndexAction() first');
|
||||
}
|
||||
return await createRule<StatusRuleParams>({
|
||||
params,
|
||||
name: name ?? 'Custom status rule',
|
||||
ruleTypeId: 'xpack.synthetics.alerts.monitorStatus',
|
||||
consumer: 'alerts',
|
||||
supertest: this.supertest,
|
||||
esClient: this.esClient,
|
||||
logger: this.logger,
|
||||
schedule: { interval: '15s' },
|
||||
actions: [
|
||||
{
|
||||
group: 'recovered',
|
||||
id: this.actionId,
|
||||
params: {
|
||||
documents: [
|
||||
{
|
||||
status: 'recovered',
|
||||
reason: '{{context.reason}}',
|
||||
locationNames: '{{context.locationNames}}',
|
||||
locationId: '{{context.locationId}}',
|
||||
linkMessage: '{{context.linkMessage}}',
|
||||
recoveryReason: '{{context.recoveryReason}}',
|
||||
recoveryStatus: '{{context.recoveryStatus}}',
|
||||
'monitor.id': '{{context.monitorId}}',
|
||||
},
|
||||
],
|
||||
},
|
||||
frequency: {
|
||||
notify_when: 'onActionGroupChange',
|
||||
throttle: null,
|
||||
summary: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
group: 'xpack.synthetics.alerts.actionGroups.monitorStatus',
|
||||
id: this.actionId,
|
||||
params: {
|
||||
documents: [
|
||||
{
|
||||
status: 'active',
|
||||
reason: '{{context.reason}}',
|
||||
locationNames: '{{context.locationNames}}',
|
||||
locationId: '{{context.locationId}}',
|
||||
linkMessage: '{{context.linkMessage}}',
|
||||
'monitor.id': '{{context.monitorId}}',
|
||||
},
|
||||
],
|
||||
},
|
||||
frequency: {
|
||||
notify_when: 'onActionGroupChange',
|
||||
throttle: null,
|
||||
summary: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
async addMonitor(name: string) {
|
||||
const testData = {
|
||||
locations: [
|
||||
{ id: 'dev', isServiceManaged: true, label: 'Dev Service' },
|
||||
{ id: 'dev2', isServiceManaged: true, label: 'Dev Service 2' },
|
||||
],
|
||||
name,
|
||||
type: 'http',
|
||||
url: 'http://www.google.com',
|
||||
schedule: 1,
|
||||
};
|
||||
const res = await this.supertest
|
||||
.post(SYNTHETICS_API_URLS.SYNTHETICS_MONITORS)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(testData);
|
||||
|
||||
expect(res.status).to.eql(200, JSON.stringify(res.body));
|
||||
|
||||
return res.body as EncryptedSyntheticsSavedMonitor;
|
||||
}
|
||||
|
||||
async deleteMonitor(monitorId: string) {
|
||||
const res = await this.supertest
|
||||
.delete(SYNTHETICS_API_URLS.SYNTHETICS_MONITORS + '/' + monitorId)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send();
|
||||
|
||||
expect(res.status).to.eql(200);
|
||||
}
|
||||
|
||||
async updateTestMonitor(monitorId: string, updates: Record<string, any>) {
|
||||
const result = await this.supertest
|
||||
.put(SYNTHETICS_API_URLS.SYNTHETICS_MONITORS + `/${monitorId}`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(updates);
|
||||
|
||||
expect(result.status).to.eql(200, JSON.stringify(result.body));
|
||||
|
||||
return result.body as EncryptedSyntheticsSavedMonitor;
|
||||
}
|
||||
|
||||
async addPrivateLocation() {
|
||||
await this.locService.installSyntheticsPackage();
|
||||
return this.locService.addTestPrivateLocation();
|
||||
}
|
||||
|
||||
async waitForStatusAlert({
|
||||
ruleId,
|
||||
filters,
|
||||
}: {
|
||||
ruleId: string;
|
||||
filters?: QueryDslQueryContainer[];
|
||||
}) {
|
||||
return await waitForAlertInIndex({
|
||||
ruleId,
|
||||
filters,
|
||||
esClient: this.esClient,
|
||||
retryService: this.retryService,
|
||||
logger: this.logger,
|
||||
indexName: '.internal.alerts-observability.uptime.alerts-default*',
|
||||
retryDelay: 1000,
|
||||
});
|
||||
}
|
||||
|
||||
async makeSummaries({
|
||||
downChecks = 0,
|
||||
upChecks = 0,
|
||||
monitor,
|
||||
location,
|
||||
}: {
|
||||
downChecks?: number;
|
||||
upChecks?: number;
|
||||
monitor: EncryptedSyntheticsSavedMonitor;
|
||||
location?: {
|
||||
id: string;
|
||||
label: string;
|
||||
};
|
||||
}) {
|
||||
const docs = [];
|
||||
// lets make some down checks
|
||||
for (let i = downChecks; i > 0; i--) {
|
||||
const doc = await this.addSummaryDocument({
|
||||
monitor,
|
||||
location,
|
||||
status: 'down',
|
||||
timestamp: moment()
|
||||
.subtract(i - 1, 'minutes')
|
||||
.toISOString(),
|
||||
});
|
||||
docs.push(doc);
|
||||
}
|
||||
|
||||
// lets make some up checks
|
||||
for (let i = upChecks; i > 0; i--) {
|
||||
const doc = await this.addSummaryDocument({
|
||||
monitor,
|
||||
location,
|
||||
status: 'up',
|
||||
timestamp: moment()
|
||||
.subtract(i - 1, 'minutes')
|
||||
.toISOString(),
|
||||
});
|
||||
docs.push(doc);
|
||||
}
|
||||
return docs;
|
||||
}
|
||||
|
||||
async addSummaryDocument({
|
||||
monitor,
|
||||
location,
|
||||
status = 'up',
|
||||
timestamp = new Date(Date.now()).toISOString(),
|
||||
}: {
|
||||
monitor: EncryptedSyntheticsSavedMonitor;
|
||||
status?: 'up' | 'down';
|
||||
timestamp?: string;
|
||||
location?: {
|
||||
id: string;
|
||||
label: string;
|
||||
};
|
||||
}) {
|
||||
let document = {
|
||||
'@timestamp': timestamp,
|
||||
};
|
||||
|
||||
const index = 'synthetics-http-default';
|
||||
|
||||
const commonData = {
|
||||
timestamp,
|
||||
location,
|
||||
monitorId: monitor.id,
|
||||
name: monitor.name,
|
||||
configId: monitor.config_id,
|
||||
};
|
||||
|
||||
if (status === 'down') {
|
||||
document = {
|
||||
...makeDownSummary(commonData),
|
||||
...document,
|
||||
};
|
||||
} else {
|
||||
document = {
|
||||
...makeUpSummary(commonData),
|
||||
...document,
|
||||
};
|
||||
}
|
||||
|
||||
this.logger.debug(
|
||||
`created synthetics summary, status: ${status}, monitor: "${monitor.name}", location: "${location?.label}"`
|
||||
);
|
||||
await this.esClient.index({
|
||||
index,
|
||||
document,
|
||||
refresh: true,
|
||||
});
|
||||
|
||||
return document;
|
||||
}
|
||||
}
|
|
@ -10,7 +10,6 @@ import { v4 as uuidv4 } from 'uuid';
|
|||
import {
|
||||
ConfigKey,
|
||||
HTTPFields,
|
||||
LocationStatus,
|
||||
PrivateLocation,
|
||||
ServiceLocation,
|
||||
} from '@kbn/synthetics-plugin/common/runtime_types';
|
||||
|
@ -19,6 +18,7 @@ import { formatKibanaNamespace } from '@kbn/synthetics-plugin/common/formatters'
|
|||
import { omit } from 'lodash';
|
||||
import { PackagePolicy } from '@kbn/fleet-plugin/common';
|
||||
import expect from '@kbn/expect';
|
||||
import { getDevLocation } from '@kbn/synthetics-plugin/server/synthetics_service/get_service_locations';
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
import { getFixtureJson } from './helper/get_fixture_json';
|
||||
import { comparePolicies, getTestSyntheticsPolicy } from './sample_data/test_policy';
|
||||
|
@ -79,15 +79,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
const apiResponse = await supertestAPI.get(SYNTHETICS_API_URLS.SERVICE_LOCATIONS);
|
||||
|
||||
const testResponse: Array<PrivateLocation | ServiceLocation> = [
|
||||
{
|
||||
id: 'dev',
|
||||
label: 'Dev Service',
|
||||
geo: { lat: 0, lon: 0 },
|
||||
url: 'mockDevUrl',
|
||||
isServiceManaged: true,
|
||||
status: LocationStatus.EXPERIMENTAL,
|
||||
isInvalid: false,
|
||||
},
|
||||
...getDevLocation('mockDevUrl'),
|
||||
{
|
||||
id: testFleetPolicyID,
|
||||
isServiceManaged: false,
|
||||
|
|
|
@ -2036,7 +2036,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
failedMonitors: [
|
||||
{
|
||||
details:
|
||||
"Invalid locations specified. Elastic managed Location(s) 'does not exist' not found. Available locations are 'dev'",
|
||||
"Invalid locations specified. Elastic managed Location(s) 'does not exist' not found. Available locations are 'dev|dev2'",
|
||||
id: httpProjectMonitors.monitors[1].id,
|
||||
payload: {
|
||||
'check.request': {
|
||||
|
|
|
@ -44,7 +44,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
it('return error if invalid location specified', async () => {
|
||||
const { message } = await addMonitorAPI({ type: 'http', locations: ['mars'] }, 400);
|
||||
expect(message).eql(
|
||||
"Invalid locations specified. Elastic managed Location(s) 'mars' not found. Available locations are 'dev'"
|
||||
"Invalid locations specified. Elastic managed Location(s) 'mars' not found. Available locations are 'dev|dev2'"
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -68,7 +68,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
400
|
||||
);
|
||||
expect(result.message).eql(
|
||||
"Invalid locations specified. Elastic managed Location(s) 'mars' not found. Available locations are 'dev' Private Location(s) 'moon' not found. No private location available to use."
|
||||
"Invalid locations specified. Elastic managed Location(s) 'mars' not found. Available locations are 'dev|dev2' Private Location(s) 'moon' not found. No private location available to use."
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -115,7 +115,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
400
|
||||
);
|
||||
expect(message).eql(
|
||||
"Invalid locations specified. Elastic managed Location(s) 'mars' not found. Available locations are 'dev'"
|
||||
"Invalid locations specified. Elastic managed Location(s) 'mars' not found. Available locations are 'dev|dev2'"
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -141,7 +141,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
400
|
||||
);
|
||||
expect(result.message).eql(
|
||||
"Invalid locations specified. Elastic managed Location(s) 'mars' not found. Available locations are 'dev' Private Location(s) 'moon' not found. No private location available to use."
|
||||
"Invalid locations specified. Elastic managed Location(s) 'mars' not found. Available locations are 'dev|dev2' Private Location(s) 'moon' not found. No private location available to use."
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -154,13 +154,6 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
value: expect.any(String),
|
||||
})),
|
||||
]),
|
||||
projects: [
|
||||
{
|
||||
count: 2,
|
||||
label: project,
|
||||
value: project,
|
||||
},
|
||||
],
|
||||
monitorTypes: [
|
||||
{
|
||||
count: 20,
|
||||
|
@ -173,6 +166,13 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
value: 'icmp',
|
||||
},
|
||||
],
|
||||
projects: [
|
||||
{
|
||||
count: 2,
|
||||
label: project,
|
||||
value: project,
|
||||
},
|
||||
],
|
||||
tags: expect.arrayContaining([
|
||||
{
|
||||
count: 21,
|
||||
|
@ -242,6 +242,13 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
value: expect.any(String),
|
||||
}))
|
||||
),
|
||||
monitorTypes: [
|
||||
{
|
||||
count: 2,
|
||||
label: 'icmp',
|
||||
value: 'icmp',
|
||||
},
|
||||
],
|
||||
projects: [
|
||||
{
|
||||
count: 2,
|
||||
|
@ -249,18 +256,6 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
value: project,
|
||||
},
|
||||
],
|
||||
monitorTypes: [
|
||||
// {
|
||||
// count: 20,
|
||||
// label: 'http',
|
||||
// value: 'http',
|
||||
// },
|
||||
{
|
||||
count: 2,
|
||||
label: 'icmp',
|
||||
value: 'icmp',
|
||||
},
|
||||
],
|
||||
tags: expect.arrayContaining([
|
||||
{
|
||||
count: 1,
|
||||
|
|
|
@ -82,6 +82,15 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
status: LocationStatus.EXPERIMENTAL,
|
||||
isInvalid: false,
|
||||
},
|
||||
{
|
||||
id: 'dev2',
|
||||
label: 'Dev Service 2',
|
||||
geo: { lat: 0, lon: 0 },
|
||||
url: 'mockDevUrl',
|
||||
isServiceManaged: true,
|
||||
status: LocationStatus.EXPERIMENTAL,
|
||||
isInvalid: false,
|
||||
},
|
||||
{
|
||||
id: testFleetPolicyID,
|
||||
isInvalid: false,
|
||||
|
|
|
@ -168,6 +168,8 @@
|
|||
"@kbn/reporting-server",
|
||||
"@kbn/data-quality-plugin",
|
||||
"@kbn/ml-trained-models-utils",
|
||||
"@kbn/observability-synthetics-test-data",
|
||||
"@kbn/ml-trained-models-utils",
|
||||
"@kbn/openapi-common",
|
||||
"@kbn/securitysolution-lists-common",
|
||||
"@kbn/securitysolution-exceptions-common",
|
||||
|
@ -181,6 +183,7 @@
|
|||
"@kbn/management-settings-ids",
|
||||
"@kbn/mock-idp-utils",
|
||||
"@kbn/cloud-security-posture-common",
|
||||
"@kbn/saved-objects-management-plugin"
|
||||
"@kbn/saved-objects-management-plugin",
|
||||
"@kbn/alerting-types"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -5883,6 +5883,10 @@
|
|||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/observability-synthetics-test-data@link:x-pack/packages/observability/synthetics_test_data":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/observability-utils@link:x-pack/packages/observability/observability_utils":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue