diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index ec9cabb80a621..047087a14f0d7 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -813,6 +813,7 @@ packages/kbn-yarn-lock-validator @elastic/kibana-operations /x-pack/test/functional/es_archives/uptime @elastic/uptime /x-pack/test/functional/services/uptime @elastic/uptime /x-pack/test/api_integration/apis/uptime @elastic/uptime +/x-pack/test/api_integration/apis/synthetics @elastic/uptime /x-pack/plugins/observability/public/components/shared/exploratory_view @elastic/uptime /x-pack/plugins/observability/public/components/shared/field_value_suggestions @elastic/uptime /x-pack/plugins/observability/public/components/shared/core_web_vitals @elastic/uptime diff --git a/x-pack/plugins/alerting/server/alerts_service/lib/create_concrete_write_index.test.ts b/x-pack/plugins/alerting/server/alerts_service/lib/create_concrete_write_index.test.ts index b345e821be3fe..a4cb6a26d3767 100644 --- a/x-pack/plugins/alerting/server/alerts_service/lib/create_concrete_write_index.test.ts +++ b/x-pack/plugins/alerting/server/alerts_service/lib/create_concrete_write_index.test.ts @@ -167,6 +167,37 @@ describe('createConcreteWriteIndex', () => { expect(logger.error).toHaveBeenCalledWith(`Error creating concrete write index - fail`); }); + it(`should retry getting index on transient ES error`, async () => { + clusterClient.indices.getAlias.mockImplementation(async () => ({})); + const error = new Error(`fail`) as EsError; + error.meta = { + body: { + error: { + type: 'resource_already_exists_exception', + }, + }, + }; + clusterClient.indices.create.mockRejectedValueOnce(error); + clusterClient.indices.get + .mockRejectedValueOnce(new EsErrors.ConnectionError('foo')) + .mockRejectedValueOnce(new EsErrors.TimeoutError('timeout')) + .mockImplementationOnce(async () => ({ + '.internal.alerts-test.alerts-default-000001': { + aliases: { '.alerts-test.alerts-default': { is_write_index: true } }, + }, + })); + + await createConcreteWriteIndex({ + logger, + esClient: clusterClient, + indexPatterns: IndexPatterns, + totalFieldsLimit: 2500, + }); + + expect(clusterClient.indices.get).toHaveBeenCalledTimes(3); + expect(logger.error).toHaveBeenCalledWith(`Error creating concrete write index - fail`); + }); + it(`should log and throw error if ES throws resource_already_exists_exception error and existing index is not the write index`, async () => { clusterClient.indices.getAlias.mockImplementation(async () => ({})); const error = new Error(`fail`) as EsError; @@ -265,6 +296,42 @@ describe('createConcreteWriteIndex', () => { expect(clusterClient.indices.putMapping).toHaveBeenCalledTimes(2); }); + it(`should retry simulateIndexTemplate on transient ES errors`, async () => { + clusterClient.indices.getAlias.mockImplementation(async () => GetAliasResponse); + clusterClient.indices.simulateIndexTemplate + .mockRejectedValueOnce(new EsErrors.ConnectionError('foo')) + .mockRejectedValueOnce(new EsErrors.TimeoutError('timeout')) + .mockImplementation(async () => SimulateTemplateResponse); + + await createConcreteWriteIndex({ + logger, + esClient: clusterClient, + indexPatterns: IndexPatterns, + totalFieldsLimit: 2500, + }); + + expect(clusterClient.indices.simulateIndexTemplate).toHaveBeenCalledTimes(4); + }); + + it(`should retry getting alias on transient ES errors`, async () => { + clusterClient.indices.getAlias + .mockRejectedValueOnce(new EsErrors.ConnectionError('foo')) + .mockRejectedValueOnce(new EsErrors.TimeoutError('timeout')) + .mockImplementation(async () => GetAliasResponse); + clusterClient.indices.simulateIndexTemplate.mockImplementation( + async () => SimulateTemplateResponse + ); + + await createConcreteWriteIndex({ + logger, + esClient: clusterClient, + indexPatterns: IndexPatterns, + totalFieldsLimit: 2500, + }); + + expect(clusterClient.indices.getAlias).toHaveBeenCalledTimes(3); + }); + it(`should retry settings update on transient ES errors`, async () => { clusterClient.indices.getAlias.mockImplementation(async () => GetAliasResponse); clusterClient.indices.simulateIndexTemplate.mockImplementation( diff --git a/x-pack/plugins/alerting/server/alerts_service/lib/create_concrete_write_index.ts b/x-pack/plugins/alerting/server/alerts_service/lib/create_concrete_write_index.ts index df32e021602cf..31aface312913 100644 --- a/x-pack/plugins/alerting/server/alerts_service/lib/create_concrete_write_index.ts +++ b/x-pack/plugins/alerting/server/alerts_service/lib/create_concrete_write_index.ts @@ -68,9 +68,10 @@ const updateUnderlyingMapping = async ({ const { index, alias } = concreteIndexInfo; let simulatedIndexMapping: IndicesSimulateIndexTemplateResponse; try { - simulatedIndexMapping = await esClient.indices.simulateIndexTemplate({ - name: index, - }); + simulatedIndexMapping = await retryTransientEsErrors( + () => esClient.indices.simulateIndexTemplate({ name: index }), + { logger } + ); } catch (err) { logger.error( `Ignored PUT mappings for alias ${alias}; error generating simulated mappings: ${err.message}` @@ -149,10 +150,14 @@ export const createConcreteWriteIndex = async ({ try { // Specify both the index pattern for the backing indices and their aliases // The alias prevents the request from finding other namespaces that could match the -* pattern - const response = await esClient.indices.getAlias({ - index: indexPatterns.pattern, - name: indexPatterns.basePattern, - }); + const response = await retryTransientEsErrors( + () => + esClient.indices.getAlias({ + index: indexPatterns.pattern, + name: indexPatterns.basePattern, + }), + { logger } + ); concreteIndices = Object.entries(response).flatMap(([index, { aliases }]) => Object.entries(aliases).map(([aliasName, aliasProperties]) => ({ @@ -213,9 +218,7 @@ export const createConcreteWriteIndex = async ({ }, }, }), - { - logger, - } + { logger } ); } catch (error) { logger.error(`Error creating concrete write index - ${error.message}`); @@ -223,9 +226,10 @@ export const createConcreteWriteIndex = async ({ // something else created it so suppress the error. If it's not the write // index, that's bad, throw an error. if (error?.meta?.body?.error?.type === 'resource_already_exists_exception') { - const existingIndices = await esClient.indices.get({ - index: indexPatterns.name, - }); + const existingIndices = await retryTransientEsErrors( + () => esClient.indices.get({ index: indexPatterns.name }), + { logger } + ); if (!existingIndices[indexPatterns.name]?.aliases?.[indexPatterns.alias]?.is_write_index) { throw Error( `Attempted to create index: ${indexPatterns.name} as the write index for alias: ${indexPatterns.alias}, but the index already exists and is not the write index for the alias` diff --git a/x-pack/plugins/alerting/server/alerts_service/lib/create_or_update_component_template.test.ts b/x-pack/plugins/alerting/server/alerts_service/lib/create_or_update_component_template.test.ts index 6eda12a6898e8..1083807a29c9f 100644 --- a/x-pack/plugins/alerting/server/alerts_service/lib/create_or_update_component_template.test.ts +++ b/x-pack/plugins/alerting/server/alerts_service/lib/create_or_update_component_template.test.ts @@ -188,4 +188,82 @@ describe('createOrUpdateComponentTemplate', () => { }, }); }); + + it(`should retry getIndexTemplate and putIndexTemplate on transient ES errors`, async () => { + clusterClient.cluster.putComponentTemplate.mockRejectedValueOnce( + new EsErrors.ResponseError( + elasticsearchClientMock.createApiResponse({ + statusCode: 400, + body: { + error: { + root_cause: [ + { + type: 'illegal_argument_exception', + reason: + 'updating component template [.alerts-ecs-mappings] results in invalid composable template [.alerts-security.alerts-default-index-template] after templates are merged', + }, + ], + type: 'illegal_argument_exception', + reason: + 'updating component template [.alerts-ecs-mappings] results in invalid composable template [.alerts-security.alerts-default-index-template] after templates are merged', + caused_by: { + type: 'illegal_argument_exception', + reason: + 'composable template [.alerts-security.alerts-default-index-template] template after composition with component templates [.alerts-ecs-mappings, .alerts-security.alerts-mappings, .alerts-technical-mappings] is invalid', + caused_by: { + type: 'illegal_argument_exception', + reason: + 'invalid composite mappings for [.alerts-security.alerts-default-index-template]', + caused_by: { + type: 'illegal_argument_exception', + reason: 'Limit of total fields [1900] has been exceeded', + }, + }, + }, + }, + }, + }) + ) + ); + const existingIndexTemplate = { + name: 'test-template', + index_template: { + index_patterns: ['test*'], + composed_of: ['test-mappings'], + template: { + settings: { + auto_expand_replicas: '0-1', + hidden: true, + 'index.lifecycle': { + name: '.alerts-ilm-policy', + rollover_alias: `.alerts-empty-default`, + }, + 'index.mapping.total_fields.limit': 1800, + }, + mappings: { + dynamic: false, + }, + }, + }, + }; + clusterClient.indices.getIndexTemplate + .mockRejectedValueOnce(new EsErrors.ConnectionError('foo')) + .mockRejectedValueOnce(new EsErrors.TimeoutError('timeout')) + .mockResolvedValueOnce({ + index_templates: [existingIndexTemplate], + }); + clusterClient.indices.putIndexTemplate + .mockRejectedValueOnce(new EsErrors.ConnectionError('foo')) + .mockRejectedValueOnce(new EsErrors.TimeoutError('timeout')) + .mockResolvedValue({ acknowledged: true }); + await createOrUpdateComponentTemplate({ + logger, + esClient: clusterClient, + template: ComponentTemplate, + totalFieldsLimit: 2500, + }); + + expect(clusterClient.indices.getIndexTemplate).toHaveBeenCalledTimes(3); + expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalledTimes(3); + }); }); diff --git a/x-pack/plugins/alerting/server/alerts_service/lib/create_or_update_component_template.ts b/x-pack/plugins/alerting/server/alerts_service/lib/create_or_update_component_template.ts index e9d2ca73ff3fb..97cdef833adbc 100644 --- a/x-pack/plugins/alerting/server/alerts_service/lib/create_or_update_component_template.ts +++ b/x-pack/plugins/alerting/server/alerts_service/lib/create_or_update_component_template.ts @@ -23,10 +23,14 @@ interface CreateOrUpdateComponentTemplateOpts { const getIndexTemplatesUsingComponentTemplate = async ( esClient: ElasticsearchClient, componentTemplateName: string, - totalFieldsLimit: number + totalFieldsLimit: number, + logger: Logger ) => { // Get all index templates and filter down to just the ones referencing this component template - const { index_templates: indexTemplates } = await esClient.indices.getIndexTemplate(); + const { index_templates: indexTemplates } = await retryTransientEsErrors( + () => esClient.indices.getIndexTemplate(), + { logger } + ); const indexTemplatesUsingComponentTemplate = (indexTemplates ?? []).filter( (indexTemplate: IndicesGetIndexTemplateIndexTemplateItem) => indexTemplate.index_template.composed_of.includes(componentTemplateName) @@ -34,19 +38,23 @@ const getIndexTemplatesUsingComponentTemplate = async ( await asyncForEach( indexTemplatesUsingComponentTemplate, async (template: IndicesGetIndexTemplateIndexTemplateItem) => { - await esClient.indices.putIndexTemplate({ - name: template.name, - body: { - ...template.index_template, - template: { - ...template.index_template.template, - settings: { - ...template.index_template.template?.settings, - 'index.mapping.total_fields.limit': totalFieldsLimit, + await retryTransientEsErrors( + () => + esClient.indices.putIndexTemplate({ + name: template.name, + body: { + ...template.index_template, + template: { + ...template.index_template.template, + settings: { + ...template.index_template.template?.settings, + 'index.mapping.total_fields.limit': totalFieldsLimit, + }, + }, }, - }, - }, - }); + }), + { logger } + ); } ); }; @@ -54,10 +62,11 @@ const getIndexTemplatesUsingComponentTemplate = async ( const createOrUpdateComponentTemplateHelper = async ( esClient: ElasticsearchClient, template: ClusterPutComponentTemplateRequest, - totalFieldsLimit: number + totalFieldsLimit: number, + logger: Logger ) => { try { - await esClient.cluster.putComponentTemplate(template); + await retryTransientEsErrors(() => esClient.cluster.putComponentTemplate(template), { logger }); } catch (error) { const reason = error?.meta?.body?.error?.caused_by?.caused_by?.caused_by?.reason; if (reason && reason.match(/Limit of total fields \[\d+\] has been exceeded/) != null) { @@ -68,10 +77,17 @@ const createOrUpdateComponentTemplateHelper = async ( // number of new ECS fields pushes the composed mapping above the limit, this error will // occur. We have to update the field limit inside the index template now otherwise we // can never update the component template - await getIndexTemplatesUsingComponentTemplate(esClient, template.name, totalFieldsLimit); + await getIndexTemplatesUsingComponentTemplate( + esClient, + template.name, + totalFieldsLimit, + logger + ); // Try to update the component template again - await esClient.cluster.putComponentTemplate(template); + await retryTransientEsErrors(() => esClient.cluster.putComponentTemplate(template), { + logger, + }); } else { throw error; } @@ -87,10 +103,7 @@ export const createOrUpdateComponentTemplate = async ({ logger.info(`Installing component template ${template.name}`); try { - await retryTransientEsErrors( - () => createOrUpdateComponentTemplateHelper(esClient, template, totalFieldsLimit), - { logger } - ); + await createOrUpdateComponentTemplateHelper(esClient, template, totalFieldsLimit, logger); } catch (err) { logger.error(`Error installing component template ${template.name} - ${err.message}`); throw err; diff --git a/x-pack/plugins/alerting/server/alerts_service/lib/create_or_update_index_template.test.ts b/x-pack/plugins/alerting/server/alerts_service/lib/create_or_update_index_template.test.ts index c095274b539b9..d4ce203a0d0e3 100644 --- a/x-pack/plugins/alerting/server/alerts_service/lib/create_or_update_index_template.test.ts +++ b/x-pack/plugins/alerting/server/alerts_service/lib/create_or_update_index_template.test.ts @@ -137,6 +137,21 @@ describe('createOrUpdateIndexTemplate', () => { expect(clusterClient.indices.putIndexTemplate).toHaveBeenCalledTimes(3); }); + it(`should retry simulateTemplate on transient ES errors`, async () => { + clusterClient.indices.simulateTemplate + .mockRejectedValueOnce(new EsErrors.ConnectionError('foo')) + .mockRejectedValueOnce(new EsErrors.TimeoutError('timeout')) + .mockImplementation(async () => SimulateTemplateResponse); + clusterClient.indices.putIndexTemplate.mockResolvedValue({ acknowledged: true }); + await createOrUpdateIndexTemplate({ + logger, + esClient: clusterClient, + template: IndexTemplate, + }); + + expect(clusterClient.indices.simulateTemplate).toHaveBeenCalledTimes(3); + }); + it(`should log and throw error if max retries exceeded`, async () => { clusterClient.indices.simulateTemplate.mockImplementation(async () => SimulateTemplateResponse); clusterClient.indices.putIndexTemplate.mockRejectedValue(new EsErrors.ConnectionError('foo')); diff --git a/x-pack/plugins/alerting/server/alerts_service/lib/create_or_update_index_template.ts b/x-pack/plugins/alerting/server/alerts_service/lib/create_or_update_index_template.ts index 0c5c221403186..a17fad2d875ed 100644 --- a/x-pack/plugins/alerting/server/alerts_service/lib/create_or_update_index_template.ts +++ b/x-pack/plugins/alerting/server/alerts_service/lib/create_or_update_index_template.ts @@ -100,7 +100,10 @@ export const createOrUpdateIndexTemplate = async ({ let mappings: MappingTypeMapping = {}; try { // Simulate the index template to proactively identify any issues with the mapping - const simulateResponse = await esClient.indices.simulateTemplate(template); + const simulateResponse = await retryTransientEsErrors( + () => esClient.indices.simulateTemplate(template), + { logger } + ); mappings = simulateResponse.template.mappings; } catch (err) { logger.error( diff --git a/x-pack/plugins/infra/common/alerting/logs/log_threshold/types.ts b/x-pack/plugins/infra/common/alerting/logs/log_threshold/types.ts index c0ad0af7e69fe..2eec5b035c793 100644 --- a/x-pack/plugins/infra/common/alerting/logs/log_threshold/types.ts +++ b/x-pack/plugins/infra/common/alerting/logs/log_threshold/types.ts @@ -401,3 +401,8 @@ export const isOptimizableGroupedThreshold = ( return false; } }; + +export interface ExecutionTimeRange { + gte?: number; + lte: number; +} diff --git a/x-pack/plugins/infra/common/http_api/log_alerts/chart_preview_data.ts b/x-pack/plugins/infra/common/http_api/log_alerts/chart_preview_data.ts index 15f0df5222e7d..afdecfc64b906 100644 --- a/x-pack/plugins/infra/common/http_api/log_alerts/chart_preview_data.ts +++ b/x-pack/plugins/infra/common/http_api/log_alerts/chart_preview_data.ts @@ -85,6 +85,13 @@ export const getLogAlertsChartPreviewDataRequestPayloadRT = rt.type({ logView: persistedLogViewReferenceRT, alertParams: getLogAlertsChartPreviewDataAlertParamsSubsetRT, buckets: rt.number, + executionTimeRange: rt.union([ + rt.undefined, + rt.type({ + gte: rt.number, + lte: rt.number, + }), + ]), }), }); diff --git a/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_details_app_section/index.tsx b/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_details_app_section/index.tsx new file mode 100644 index 0000000000000..ad5ef8a99f23b --- /dev/null +++ b/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_details_app_section/index.tsx @@ -0,0 +1,57 @@ +/* + * 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 } from '@elastic/eui'; +import { ALERT_DURATION, ALERT_END } from '@kbn/rule-data-utils'; +import moment from 'moment'; +import React from 'react'; +import { type PartialCriterion } from '../../../../../common/alerting/logs/log_threshold'; +import { CriterionPreview } from '../expression_editor/criterion_preview_chart'; +import { AlertDetailsAppSectionProps } from './types'; + +const AlertDetailsAppSection = ({ rule, alert }: AlertDetailsAppSectionProps) => { + const ruleWindowSizeMS = moment + .duration(rule.params.timeSize, rule.params.timeUnit) + .asMilliseconds(); + const alertDurationMS = alert.fields[ALERT_DURATION]! / 1000; + const TWENTY_TIMES_RULE_WINDOW_MS = 20 * ruleWindowSizeMS; + /** + * This is part or the requirements (RFC). + * If the alert is less than 20 units of `FOR THE LAST ` then we should draw a time range of 20 units. + * IE. The user set "FOR THE LAST 5 minutes" at a minimum we should show 100 minutes. + */ + const rangeFrom = + alertDurationMS < TWENTY_TIMES_RULE_WINDOW_MS + ? Number(moment(alert.start).subtract(TWENTY_TIMES_RULE_WINDOW_MS, 'millisecond').format('x')) + : Number(moment(alert.start).subtract(ruleWindowSizeMS, 'millisecond').format('x')); + + const rangeTo = alert.active + ? Date.now() + : Number(moment(alert.fields[ALERT_END]).add(ruleWindowSizeMS, 'millisecond').format('x')); + + return ( + // Create a chart per-criteria + + {rule.params.criteria.map((criteria) => { + const chartCriterion = criteria as PartialCriterion; + return ( + + + + ); + })} + + ); +}; +// eslint-disable-next-line import/no-default-export +export default AlertDetailsAppSection; diff --git a/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_details_app_section/types.ts b/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_details_app_section/types.ts new file mode 100644 index 0000000000000..8a4b23b630b7b --- /dev/null +++ b/x-pack/plugins/infra/public/alerting/log_threshold/components/alert_details_app_section/types.ts @@ -0,0 +1,15 @@ +/* + * 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 { Rule } from '@kbn/alerting-plugin/common'; +import { TopAlert } from '@kbn/observability-plugin/public'; +import { PartialRuleParams } from '../../../../../common/alerting/logs/log_threshold'; + +export interface AlertDetailsAppSectionProps { + rule: Rule; + alert: TopAlert; +} diff --git a/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/criterion_preview_chart.tsx b/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/criterion_preview_chart.tsx index 5ffefdd57119e..e5ea692ce7792 100644 --- a/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/criterion_preview_chart.tsx +++ b/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/criterion_preview_chart.tsx @@ -21,6 +21,7 @@ import { import { EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { useKibana } from '@kbn/kibana-react-plugin/public'; +import { ExecutionTimeRange } from '../../../../types'; import { ChartContainer, LoadingState, @@ -56,6 +57,7 @@ interface Props { chartCriterion: Partial; sourceId: string; showThreshold: boolean; + executionTimeRange?: ExecutionTimeRange; } export const CriterionPreview: React.FC = ({ @@ -63,6 +65,7 @@ export const CriterionPreview: React.FC = ({ chartCriterion, sourceId, showThreshold, + executionTimeRange, }) => { const chartAlertParams: GetLogAlertsChartPreviewDataAlertParamsSubset | null = useMemo(() => { const { field, comparator, value } = chartCriterion; @@ -106,6 +109,7 @@ export const CriterionPreview: React.FC = ({ threshold={ruleParams.count} chartAlertParams={chartAlertParams} showThreshold={showThreshold} + executionTimeRange={executionTimeRange} /> ); }; @@ -116,6 +120,7 @@ interface ChartProps { threshold?: Threshold; chartAlertParams: GetLogAlertsChartPreviewDataAlertParamsSubset; showThreshold: boolean; + executionTimeRange?: ExecutionTimeRange; } const CriterionPreviewChart: React.FC = ({ @@ -124,6 +129,7 @@ const CriterionPreviewChart: React.FC = ({ threshold, chartAlertParams, showThreshold, + executionTimeRange, }) => { const { uiSettings } = useKibana().services; const isDarkMode = uiSettings?.get('theme:darkMode') || false; @@ -138,6 +144,7 @@ const CriterionPreviewChart: React.FC = ({ sourceId, ruleParams: chartAlertParams, buckets, + executionTimeRange, }); useDebounce( diff --git a/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/hooks/use_chart_preview_data.tsx b/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/hooks/use_chart_preview_data.tsx index 0b99cea2fd7c9..913962f8703d1 100644 --- a/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/hooks/use_chart_preview_data.tsx +++ b/x-pack/plugins/infra/public/alerting/log_threshold/components/expression_editor/hooks/use_chart_preview_data.tsx @@ -8,6 +8,7 @@ import { useState, useMemo } from 'react'; import { HttpHandler } from '@kbn/core/public'; import { useKibana } from '@kbn/kibana-react-plugin/public'; +import { ExecutionTimeRange } from '../../../../../types'; import { useTrackedPromise } from '../../../../../utils/use_tracked_promise'; import { GetLogAlertsChartPreviewDataSuccessResponsePayload, @@ -22,11 +23,16 @@ interface Options { sourceId: string; ruleParams: GetLogAlertsChartPreviewDataAlertParamsSubset; buckets: number; + executionTimeRange?: ExecutionTimeRange; } -export const useChartPreviewData = ({ sourceId, ruleParams, buckets }: Options) => { +export const useChartPreviewData = ({ + sourceId, + ruleParams, + buckets, + executionTimeRange, +}: Options) => { const { http } = useKibana().services; - const [chartPreviewData, setChartPreviewData] = useState< GetLogAlertsChartPreviewDataSuccessResponsePayload['data']['series'] >([]); @@ -36,7 +42,13 @@ export const useChartPreviewData = ({ sourceId, ruleParams, buckets }: Options) cancelPreviousOn: 'creation', createPromise: async () => { setHasError(false); - return await callGetChartPreviewDataAPI(sourceId, http!.fetch, ruleParams, buckets); + return await callGetChartPreviewDataAPI( + sourceId, + http!.fetch, + ruleParams, + buckets, + executionTimeRange + ); }, onResolve: ({ data: { series } }) => { setHasError(false); @@ -66,7 +78,8 @@ export const callGetChartPreviewDataAPI = async ( sourceId: string, fetch: HttpHandler, alertParams: GetLogAlertsChartPreviewDataAlertParamsSubset, - buckets: number + buckets: number, + executionTimeRange?: ExecutionTimeRange ) => { const response = await fetch(LOG_ALERTS_CHART_PREVIEW_DATA_PATH, { method: 'POST', @@ -76,6 +89,7 @@ export const callGetChartPreviewDataAPI = async ( logView: { type: 'log-view-reference', logViewId: sourceId }, alertParams, buckets, + executionTimeRange, }, }) ), diff --git a/x-pack/plugins/infra/public/alerting/log_threshold/log_threshold_rule_type.tsx b/x-pack/plugins/infra/public/alerting/log_threshold/log_threshold_rule_type.tsx index fa32b92806dcd..84ad31764b68a 100644 --- a/x-pack/plugins/infra/public/alerting/log_threshold/log_threshold_rule_type.tsx +++ b/x-pack/plugins/infra/public/alerting/log_threshold/log_threshold_rule_type.tsx @@ -7,6 +7,7 @@ import { i18n } from '@kbn/i18n'; import { ObservabilityRuleTypeModel } from '@kbn/observability-plugin/public'; +import { lazy } from 'react'; import { LOG_DOCUMENT_COUNT_RULE_TYPE_ID, PartialRuleParams, @@ -33,6 +34,7 @@ export function createLogThresholdRuleType( documentationUrl(docLinks) { return `${docLinks.links.observability.logsThreshold}`; }, + alertDetailsAppSection: lazy(() => import('./components/alert_details_app_section')), ruleParamsExpression, validate: validateExpression, defaultActionMessage: i18n.translate( diff --git a/x-pack/plugins/infra/public/types.ts b/x-pack/plugins/infra/public/types.ts index 269425e580bbc..e3e8d1e1c4ba6 100644 --- a/x-pack/plugins/infra/public/types.ts +++ b/x-pack/plugins/infra/public/types.ts @@ -112,6 +112,11 @@ export interface LensOptions { breakdownSize: number; } +export interface ExecutionTimeRange { + gte: number; + lte: number; +} + type PropsOf = T extends React.ComponentType ? ComponentProps : never; type FirstArgumentOf = Func extends (arg1: infer FirstArgument, ...rest: any[]) => any ? FirstArgument diff --git a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_chart_preview.ts b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_chart_preview.ts index 4483ad30c0246..4455eb5f53657 100644 --- a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_chart_preview.ts +++ b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_chart_preview.ts @@ -7,6 +7,7 @@ import { i18n } from '@kbn/i18n'; import { + ExecutionTimeRange, GroupedSearchQueryResponse, GroupedSearchQueryResponseRT, isOptimizedGroupedSearchQueryResponse, @@ -35,7 +36,8 @@ export async function getChartPreviewData( resolvedLogView: ResolvedLogView, callWithRequest: KibanaFramework['callWithRequest'], alertParams: GetLogAlertsChartPreviewDataAlertParamsSubset, - buckets: number + buckets: number, + executionTimeRange?: ExecutionTimeRange ) { const { indices, timestampField, runtimeMappings } = resolvedLogView; const { groupBy, timeSize, timeUnit } = alertParams; @@ -47,11 +49,10 @@ export async function getChartPreviewData( timeSize: timeSize * buckets, }; - const executionTimestamp = Date.now(); const { rangeFilter } = buildFiltersFromCriteria( expandedAlertParams, timestampField, - executionTimestamp + executionTimeRange ); const query = isGrouped @@ -60,14 +61,14 @@ export async function getChartPreviewData( timestampField, indices, runtimeMappings, - executionTimestamp + executionTimeRange ) : getUngroupedESQuery( expandedAlertParams, timestampField, indices, runtimeMappings, - executionTimestamp + executionTimeRange ); if (!query) { diff --git a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.test.ts b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.test.ts index 37c2cf002573b..79a258c620baf 100644 --- a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.test.ts +++ b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.test.ts @@ -140,7 +140,9 @@ const baseRuleParams: Pick { ...baseRuleParams, criteria: positiveCriteria, }; - const filters = buildFiltersFromCriteria(ruleParams, TIMESTAMP_FIELD, EXECUTION_TIMESTAMP); + const filters = buildFiltersFromCriteria(ruleParams, TIMESTAMP_FIELD, EXECUTION_TIMERANGE); expect(filters.mustFilters).toEqual(expectedPositiveFilterClauses); }); @@ -182,14 +184,14 @@ describe('Log threshold executor', () => { ...baseRuleParams, criteria: negativeCriteria, }; - const filters = buildFiltersFromCriteria(ruleParams, TIMESTAMP_FIELD, EXECUTION_TIMESTAMP); + const filters = buildFiltersFromCriteria(ruleParams, TIMESTAMP_FIELD, EXECUTION_TIMERANGE); expect(filters.mustNotFilters).toEqual(expectedNegativeFilterClauses); }); test('Handles time range', () => { const ruleParams: RuleParams = { ...baseRuleParams, criteria: [] }; - const filters = buildFiltersFromCriteria(ruleParams, TIMESTAMP_FIELD, EXECUTION_TIMESTAMP); + const filters = buildFiltersFromCriteria(ruleParams, TIMESTAMP_FIELD, EXECUTION_TIMERANGE); expect(typeof filters.rangeFilter.range[TIMESTAMP_FIELD].gte).toBe('number'); expect(typeof filters.rangeFilter.range[TIMESTAMP_FIELD].lte).toBe('number'); expect(filters.rangeFilter.range[TIMESTAMP_FIELD].format).toBe('epoch_millis'); @@ -212,7 +214,7 @@ describe('Log threshold executor', () => { TIMESTAMP_FIELD, FILEBEAT_INDEX, runtimeMappings, - EXECUTION_TIMESTAMP + EXECUTION_TIMERANGE ); expect(query).toEqual({ index: 'filebeat-*', @@ -264,7 +266,7 @@ describe('Log threshold executor', () => { TIMESTAMP_FIELD, FILEBEAT_INDEX, runtimeMappings, - EXECUTION_TIMESTAMP + EXECUTION_TIMERANGE ); expect(query).toEqual({ @@ -344,7 +346,7 @@ describe('Log threshold executor', () => { TIMESTAMP_FIELD, FILEBEAT_INDEX, runtimeMappings, - EXECUTION_TIMESTAMP + EXECUTION_TIMERANGE ); expect(query).toEqual({ diff --git a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts index 1d0470c244fd1..7812b55e78b11 100644 --- a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts @@ -46,6 +46,7 @@ import { RatioRuleParams, UngroupedSearchQueryResponse, UngroupedSearchQueryResponseRT, + ExecutionTimeRange, } from '../../../../common/alerting/logs/log_threshold'; import { decodeOrThrow } from '../../../../common/runtime_types'; import { getLogsAppAlertUrl } from '../../../../common/formatters/alert_link'; @@ -346,20 +347,23 @@ const getESQuery = ( runtimeMappings: estypes.MappingRuntimeFields, executionTimestamp: number ) => { + const executionTimeRange = { + lte: executionTimestamp, + }; return hasGroupBy(alertParams) ? getGroupedESQuery( alertParams, timestampField, indexPattern, runtimeMappings, - executionTimestamp + executionTimeRange ) : getUngroupedESQuery( alertParams, timestampField, indexPattern, runtimeMappings, - executionTimestamp + executionTimeRange ); }; @@ -641,14 +645,14 @@ export const processGroupByRatioResults = ( export const buildFiltersFromCriteria = ( params: Pick & { criteria: CountCriteria }, timestampField: string, - executionTimestamp: number + executionTimeRange?: ExecutionTimeRange ) => { const { timeSize, timeUnit, criteria } = params; const interval = `${timeSize}${timeUnit}`; const intervalAsSeconds = getIntervalInSeconds(interval); const intervalAsMs = intervalAsSeconds * 1000; - const to = executionTimestamp; - const from = to - intervalAsMs; + const to = executionTimeRange?.lte || Date.now(); + const from = executionTimeRange?.gte || to - intervalAsMs; const positiveCriteria = criteria.filter((criterion) => positiveComparators.includes(criterion.comparator) @@ -699,7 +703,7 @@ export const getGroupedESQuery = ( timestampField: string, index: string, runtimeMappings: estypes.MappingRuntimeFields, - executionTimestamp: number + executionTimeRange?: ExecutionTimeRange ): estypes.SearchRequest | undefined => { // IMPORTANT: // For the group by scenario we need to account for users utilizing "less than" configurations @@ -721,7 +725,7 @@ export const getGroupedESQuery = ( const { rangeFilter, groupedRangeFilter, mustFilters, mustNotFilters } = buildFiltersFromCriteria( params, timestampField, - executionTimestamp + executionTimeRange ); if (isOptimizableGroupedThreshold(comparator, value)) { @@ -812,12 +816,12 @@ export const getUngroupedESQuery = ( timestampField: string, index: string, runtimeMappings: estypes.MappingRuntimeFields, - executionTimestamp: number + executionTimeRange?: ExecutionTimeRange ): object => { const { rangeFilter, mustFilters, mustNotFilters } = buildFiltersFromCriteria( params, timestampField, - executionTimestamp + executionTimeRange ); const body: estypes.SearchRequest['body'] = { diff --git a/x-pack/plugins/infra/server/routes/log_alerts/chart_preview_data.ts b/x-pack/plugins/infra/server/routes/log_alerts/chart_preview_data.ts index fbc530397f4e3..d04a9a9bf491a 100644 --- a/x-pack/plugins/infra/server/routes/log_alerts/chart_preview_data.ts +++ b/x-pack/plugins/infra/server/routes/log_alerts/chart_preview_data.ts @@ -29,7 +29,7 @@ export const initGetLogAlertsChartPreviewDataRoute = ({ }, framework.router.handleLegacyErrors(async (requestContext, request, response) => { const { - data: { logView, buckets, alertParams }, + data: { logView, buckets, alertParams, executionTimeRange }, } = request.body; const [, , { logViews }] = await getStartServices(); @@ -41,7 +41,8 @@ export const initGetLogAlertsChartPreviewDataRoute = ({ resolvedLogView, framework.callWithRequest, alertParams, - buckets + buckets, + executionTimeRange ); return response.ok({ diff --git a/x-pack/plugins/observability/e2e/journeys/step_duration.journey.ts b/x-pack/plugins/observability/e2e/journeys/step_duration.journey.ts index 52da0a786fdb6..10cc98fa2da6a 100644 --- a/x-pack/plugins/observability/e2e/journeys/step_duration.journey.ts +++ b/x-pack/plugins/observability/e2e/journeys/step_duration.journey.ts @@ -27,7 +27,7 @@ journey('Exploratory view', async ({ page, params }) => { reportType: 'kpi-over-time', allSeries: [ { - dataType: 'synthetics', + dataType: 'uptime', time: { from: moment().subtract(10, 'y').toISOString(), to: moment().toISOString(), diff --git a/x-pack/plugins/observability/public/components/app/observability_status/content.ts b/x-pack/plugins/observability/public/components/app/observability_status/content.ts index 319c701fd96c2..3081ee2ebf29b 100644 --- a/x-pack/plugins/observability/public/components/app/observability_status/content.ts +++ b/x-pack/plugins/observability/public/components/app/observability_status/content.ts @@ -86,7 +86,7 @@ export const getContent = ( weight: 2, }, { - id: 'synthetics', + id: 'uptime', title: i18n.translate('xpack.observability.statusVisualization.uptime.title', { defaultMessage: 'Uptime', }), diff --git a/x-pack/plugins/observability/public/components/app/section/uptime/index.tsx b/x-pack/plugins/observability/public/components/app/section/uptime/index.tsx index 8e76c1316be21..a7d482d6da2f6 100644 --- a/x-pack/plugins/observability/public/components/app/section/uptime/index.tsx +++ b/x-pack/plugins/observability/public/components/app/section/uptime/index.tsx @@ -53,7 +53,7 @@ export function UptimeSection({ bucketSize }: Props) { const { data, status } = useFetcher( () => { if (bucketSize && absoluteStart && absoluteEnd) { - return getDataHandler('synthetics')?.fetchData({ + return getDataHandler('uptime')?.fetchData({ absoluteTime: { start: absoluteStart, end: absoluteEnd }, relativeTime: { start: relativeStart, end: relativeEnd }, timeZone, @@ -75,7 +75,7 @@ export function UptimeSection({ bucketSize }: Props) { ] ); - if (!hasDataMap.synthetics?.hasData) { + if (!hasDataMap.uptime?.hasData) { return null; } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.test.tsx index 1080c1d51d28d..d98a8a8260cbd 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.test.tsx @@ -46,6 +46,7 @@ describe('useExpViewTimeRange', function () { infra_logs: mockDataView, infra_metrics: mockDataView, synthetics: mockDataView, + uptime: mockDataView, alerts: mockDataView, }, }); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/labels.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/labels.ts index 7f975c5777d4d..add254e327d18 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/labels.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/labels.ts @@ -8,6 +8,7 @@ import { i18n } from '@kbn/i18n'; export enum DataTypes { SYNTHETICS = 'synthetics', + UPTIME = 'uptime', UX = 'ux', MOBILE = 'mobile', METRICS = 'infra_metrics', @@ -27,6 +28,10 @@ export const DataTypesLabels: Record = { } ), + [DataTypes.UPTIME]: i18n.translate('xpack.observability.overview.exploratoryView.uptimeLabel', { + defaultMessage: 'Uptime', + }), + [DataTypes.METRICS]: i18n.translate('xpack.observability.overview.exploratoryView.metricsLabel', { defaultMessage: 'Metrics', }), diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/obsv_exploratory_view.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/obsv_exploratory_view.tsx index fb30cb1734642..9b3f7b470dd32 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/obsv_exploratory_view.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/obsv_exploratory_view.tsx @@ -38,6 +38,10 @@ import { getLogsKPIConfig } from './configurations/infra_logs/kpi_over_time_conf import { getSingleMetricConfig } from './configurations/rum/single_metric_config'; export const dataTypes: Array<{ id: AppDataType; label: string }> = [ + { + id: DataTypes.UPTIME, + label: DataTypesLabels[DataTypes.UPTIME], + }, { id: DataTypes.SYNTHETICS, label: DataTypesLabels[DataTypes.SYNTHETICS], @@ -85,6 +89,12 @@ export const obsvReportConfigMap = { getSyntheticsSingleMetricConfig, getSyntheticsHeatmapConfig, ], + [DataTypes.UPTIME]: [ + getSyntheticsKPIConfig, + getSyntheticsDistributionConfig, + getSyntheticsSingleMetricConfig, + getSyntheticsHeatmapConfig, + ], [DataTypes.MOBILE]: [ getMobileKPIConfig, getMobileKPIDistributionConfig, diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/date_picker_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/date_picker_col.tsx index 7a45920cfd83f..cc6c76755689b 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/date_picker_col.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/date_picker_col.tsx @@ -27,7 +27,8 @@ interface Props { const AddDataComponents: Record = { mobile: MobileAddData, ux: UXAddData, - synthetics: SyntheticsAddData, + uptime: SyntheticsAddData, + synthetics: null, apm: null, infra_logs: null, infra_metrics: null, diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts index 243aefafbc22b..690f00e19fec1 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts @@ -149,6 +149,7 @@ interface FormatType extends SerializedFieldFormat { export type AppDataType = | 'synthetics' + | 'uptime' | 'ux' | 'infra_logs' | 'infra_metrics' diff --git a/x-pack/plugins/observability/public/components/slo/slo_status_badge/slo_status_badge.stories.tsx b/x-pack/plugins/observability/public/components/slo/slo_status_badge/slo_status_badge.stories.tsx index 1c1313beb15cf..e8a76e8dc1118 100644 --- a/x-pack/plugins/observability/public/components/slo/slo_status_badge/slo_status_badge.stories.tsx +++ b/x-pack/plugins/observability/public/components/slo/slo_status_badge/slo_status_badge.stories.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { ComponentStory } from '@storybook/react'; +import { EuiFlexGroup } from '@elastic/eui'; import { KibanaReactStorybookDecorator } from '../../../utils/kibana_react.storybook_decorator'; import { SloStatusBadge as Component, SloStatusProps } from './slo_status_badge'; import { buildSlo } from '../../../data/slo/slo'; @@ -19,7 +20,9 @@ export default { }; const Template: ComponentStory = (props: SloStatusProps) => ( - + + + ); const defaultProps = { diff --git a/x-pack/plugins/observability/public/context/constants.ts b/x-pack/plugins/observability/public/context/constants.ts index 695febb81aff6..962622b128ded 100644 --- a/x-pack/plugins/observability/public/context/constants.ts +++ b/x-pack/plugins/observability/public/context/constants.ts @@ -8,6 +8,7 @@ export const ALERT_APP = 'alert'; export const UX_APP = 'ux'; export const SYNTHETICS_APP = 'synthetics'; +export const UPTIME_APP = 'uptime'; export const APM_APP = 'apm'; export const INFRA_LOGS_APP = 'infra_logs'; export const INFRA_METRICS_APP = 'infra_metrics'; diff --git a/x-pack/plugins/observability/public/context/has_data_context.test.tsx b/x-pack/plugins/observability/public/context/has_data_context.test.tsx index 1cac519680272..6d6a99e9b8642 100644 --- a/x-pack/plugins/observability/public/context/has_data_context.test.tsx +++ b/x-pack/plugins/observability/public/context/has_data_context.test.tsx @@ -37,7 +37,7 @@ function unregisterAll() { unregisterDataHandler({ appName: 'apm' }); unregisterDataHandler({ appName: 'infra_logs' }); unregisterDataHandler({ appName: 'infra_metrics' }); - unregisterDataHandler({ appName: 'synthetics' }); + unregisterDataHandler({ appName: 'uptime' }); unregisterDataHandler({ appName: 'ux' }); } @@ -73,7 +73,7 @@ describe('HasDataContextProvider', () => { expect(result.current).toEqual({ hasDataMap: { apm: { hasData: undefined, status: 'success' }, - synthetics: { hasData: undefined, status: 'success' }, + uptime: { hasData: undefined, status: 'success' }, infra_logs: { hasData: undefined, status: 'success' }, infra_metrics: { hasData: undefined, status: 'success' }, ux: { hasData: undefined, status: 'success' }, @@ -98,7 +98,7 @@ describe('HasDataContextProvider', () => { }, { appName: 'infra_metrics', hasData: async () => ({ hasData: false }) }, { - appName: 'synthetics', + appName: 'uptime', hasData: async () => ({ hasData: false }), }, { @@ -128,7 +128,7 @@ describe('HasDataContextProvider', () => { expect(result.current).toEqual({ hasDataMap: { apm: { hasData: false, status: 'success' }, - synthetics: { + uptime: { hasData: false, status: 'success', }, @@ -161,7 +161,7 @@ describe('HasDataContextProvider', () => { hasData: async () => ({ hasData: false, indices: 'metric-*' }), }, { - appName: 'synthetics', + appName: 'uptime', hasData: async () => ({ hasData: false, indices: 'heartbeat-*, synthetics-*' }), }, { @@ -190,7 +190,7 @@ describe('HasDataContextProvider', () => { expect(result.current).toEqual({ hasDataMap: { apm: { hasData: true, status: 'success' }, - synthetics: { + uptime: { hasData: false, indices: 'heartbeat-*, synthetics-*', status: 'success', @@ -225,7 +225,7 @@ describe('HasDataContextProvider', () => { hasData: async () => ({ hasData: true, indices: 'metric-*' }), }, { - appName: 'synthetics', + appName: 'uptime', hasData: async () => ({ hasData: true, indices: 'heartbeat-*, synthetics-*' }), }, { @@ -257,7 +257,7 @@ describe('HasDataContextProvider', () => { hasData: true, status: 'success', }, - synthetics: { + uptime: { hasData: true, indices: 'heartbeat-*, synthetics-*', status: 'success', @@ -309,7 +309,7 @@ describe('HasDataContextProvider', () => { expect(result.current).toEqual({ hasDataMap: { apm: { hasData: true, indices: sampleAPMIndices, status: 'success' }, - synthetics: { hasData: undefined, status: 'success' }, + uptime: { hasData: undefined, status: 'success' }, infra_logs: { hasData: undefined, status: 'success' }, infra_metrics: { hasData: undefined, status: 'success' }, ux: { hasData: undefined, status: 'success' }, @@ -358,7 +358,7 @@ describe('HasDataContextProvider', () => { indices: sampleAPMIndices, status: 'success', }, - synthetics: { hasData: undefined, status: 'success' }, + uptime: { hasData: undefined, status: 'success' }, infra_logs: { hasData: undefined, status: 'success' }, infra_metrics: { hasData: undefined, status: 'success' }, ux: { hasData: undefined, status: 'success' }, @@ -391,7 +391,7 @@ describe('HasDataContextProvider', () => { hasData: async () => ({ hasData: true, indices: 'metric-*' }), }, { - appName: 'synthetics', + appName: 'uptime', hasData: async () => ({ hasData: true, indices: 'heartbeat-*, synthetics-*' }), }, { @@ -420,7 +420,7 @@ describe('HasDataContextProvider', () => { expect(result.current).toEqual({ hasDataMap: { apm: { hasData: undefined, status: 'failure' }, - synthetics: { + uptime: { hasData: true, indices: 'heartbeat-*, synthetics-*', status: 'success', @@ -465,7 +465,7 @@ describe('HasDataContextProvider', () => { }, }, { - appName: 'synthetics', + appName: 'uptime', hasData: async () => { throw new Error('BOOMMMMM'); }, @@ -498,7 +498,7 @@ describe('HasDataContextProvider', () => { expect(result.current).toEqual({ hasDataMap: { apm: { hasData: undefined, status: 'failure' }, - synthetics: { hasData: undefined, status: 'failure' }, + uptime: { hasData: undefined, status: 'failure' }, infra_logs: { hasData: undefined, status: 'failure' }, infra_metrics: { hasData: undefined, status: 'failure' }, ux: { hasData: undefined, status: 'failure' }, @@ -544,7 +544,7 @@ describe('HasDataContextProvider', () => { expect(result.current).toEqual({ hasDataMap: { apm: { hasData: undefined, status: 'success' }, - synthetics: { hasData: undefined, status: 'success' }, + uptime: { hasData: undefined, status: 'success' }, infra_logs: { hasData: undefined, status: 'success' }, infra_metrics: { hasData: undefined, status: 'success' }, ux: { hasData: undefined, status: 'success' }, diff --git a/x-pack/plugins/observability/public/context/has_data_context.tsx b/x-pack/plugins/observability/public/context/has_data_context.tsx index 775b401b50866..dda9ca4ef37fe 100644 --- a/x-pack/plugins/observability/public/context/has_data_context.tsx +++ b/x-pack/plugins/observability/public/context/has_data_context.tsx @@ -15,7 +15,7 @@ import { APM_APP, INFRA_LOGS_APP, INFRA_METRICS_APP, - SYNTHETICS_APP, + UPTIME_APP, UX_APP, } from './constants'; import { getDataHandler } from '../data_handler'; @@ -50,7 +50,7 @@ export const HasDataContext = createContext({} as HasDataContextValue); const apps: DataContextApps[] = [ APM_APP, - SYNTHETICS_APP, + UPTIME_APP, INFRA_LOGS_APP, INFRA_METRICS_APP, UX_APP, @@ -100,7 +100,7 @@ export function HasDataContextProvider({ children }: { children: React.ReactNode serviceName: resultUx?.serviceName as string, }); break; - case SYNTHETICS_APP: + case UPTIME_APP: const resultSy = await getDataHandler(app)?.hasData(); updateState({ hasData: resultSy?.hasData, indices: resultSy?.indices }); diff --git a/x-pack/plugins/observability/public/data_handler.test.ts b/x-pack/plugins/observability/public/data_handler.test.ts index d0cca3b5272e7..736fd3d968bda 100644 --- a/x-pack/plugins/observability/public/data_handler.test.ts +++ b/x-pack/plugins/observability/public/data_handler.test.ts @@ -188,7 +188,7 @@ describe('registerDataHandler', () => { }); describe('Uptime', () => { registerDataHandler({ - appName: 'synthetics', + appName: 'uptime', fetchData: async () => { return { title: 'uptime', @@ -226,13 +226,13 @@ describe('registerDataHandler', () => { }); it('registered data handler', () => { - const dataHandler = getDataHandler('synthetics'); + const dataHandler = getDataHandler('uptime'); expect(dataHandler?.fetchData).toBeDefined(); expect(dataHandler?.hasData).toBeDefined(); }); it('returns data when fetchData is called', async () => { - const dataHandler = getDataHandler('synthetics'); + const dataHandler = getDataHandler('uptime'); const response = await dataHandler?.fetchData(params); expect(response).toEqual({ title: 'uptime', diff --git a/x-pack/plugins/observability/public/pages/overview/components/empty_sections.tsx b/x-pack/plugins/observability/public/pages/overview/components/empty_sections.tsx index 704d0c6432ddd..bc91f20bf3b9c 100644 --- a/x-pack/plugins/observability/public/pages/overview/components/empty_sections.tsx +++ b/x-pack/plugins/observability/public/pages/overview/components/empty_sections.tsx @@ -106,7 +106,7 @@ const getEmptySections = ({ http }: { http: HttpSetup }): ISection[] => { href: http.basePath.prepend('/app/home#/tutorial_directory/metrics'), }, { - id: 'synthetics', + id: 'uptime', title: i18n.translate('xpack.observability.emptySection.apps.uptime.title', { defaultMessage: 'Uptime', }), diff --git a/x-pack/plugins/observability/public/pages/overview/overview.stories.tsx b/x-pack/plugins/observability/public/pages/overview/overview.stories.tsx index b139564019e74..e17a527933806 100644 --- a/x-pack/plugins/observability/public/pages/overview/overview.stories.tsx +++ b/x-pack/plugins/observability/public/pages/overview/overview.stories.tsx @@ -31,7 +31,7 @@ function unregisterAll() { unregisterDataHandler({ appName: 'apm' }); unregisterDataHandler({ appName: 'infra_logs' }); unregisterDataHandler({ appName: 'infra_metrics' }); - unregisterDataHandler({ appName: 'synthetics' }); + unregisterDataHandler({ appName: 'uptime' }); } const sampleAPMIndices = { transaction: 'apm-*' } as ApmIndicesConfig; @@ -235,7 +235,7 @@ storiesOf('app/Overview', module) hasData: async () => ({ hasData: false, indices: 'metric-*' }), }); registerDataHandler({ - appName: 'synthetics', + appName: 'uptime', fetchData: fetchUptimeData, hasData: async () => ({ hasData: false, indices: 'heartbeat-*,synthetics-*' }), }); @@ -323,7 +323,7 @@ storiesOf('app/Overview', module) hasData: async () => ({ hasData: true, indices: 'metric-*' }), }); registerDataHandler({ - appName: 'synthetics', + appName: 'uptime', fetchData: fetchUptimeData, hasData: async () => ({ hasData: true, indices: 'heartbeat-*,synthetics-*' }), }); @@ -349,7 +349,7 @@ storiesOf('app/Overview', module) hasData: async () => ({ hasData: true, indices: 'metric-*' }), }); registerDataHandler({ - appName: 'synthetics', + appName: 'uptime', fetchData: fetchUptimeData, hasData: async () => ({ hasData: true, indices: 'heartbeat-*,synthetics-*' }), }); @@ -377,7 +377,7 @@ storiesOf('app/Overview', module) hasData: async () => ({ hasData: true, indices: 'metric-*' }), }); registerDataHandler({ - appName: 'synthetics', + appName: 'uptime', fetchData: fetchUptimeData, hasData: async () => ({ hasData: true, indices: 'heartbeat-*,synthetics-*' }), }); @@ -402,7 +402,7 @@ storiesOf('app/Overview', module) hasData: async () => ({ hasData: true, indices: 'metric-*' }), }); registerDataHandler({ - appName: 'synthetics', + appName: 'uptime', fetchData: async () => emptyUptimeResponse, hasData: async () => ({ hasData: true, indices: 'heartbeat-*,synthetics-*' }), }); @@ -434,7 +434,7 @@ storiesOf('app/Overview', module) hasData: async () => ({ hasData: true, indices: 'metric-*' }), }); registerDataHandler({ - appName: 'synthetics', + appName: 'uptime', fetchData: async () => { throw new Error('Error fetching Uptime data'); }, @@ -472,7 +472,7 @@ storiesOf('app/Overview', module) }, }); registerDataHandler({ - appName: 'synthetics', + appName: 'uptime', fetchData: fetchUptimeData, // @ts-ignore throws an error instead hasData: async () => { @@ -509,7 +509,7 @@ storiesOf('app/Overview', module) }, }); registerDataHandler({ - appName: 'synthetics', + appName: 'uptime', fetchData: fetchUptimeData, // @ts-ignore throws an error instead hasData: async () => { diff --git a/x-pack/plugins/observability/public/pages/slo_details/components/header_title.tsx b/x-pack/plugins/observability/public/pages/slo_details/components/header_title.tsx index d2448d8959ad7..44e31dac68949 100644 --- a/x-pack/plugins/observability/public/pages/slo_details/components/header_title.tsx +++ b/x-pack/plugins/observability/public/pages/slo_details/components/header_title.tsx @@ -22,14 +22,14 @@ export function HeaderTitle(props: Props) { return ; } + if (!slo) { + return null; + } + return ( - - {slo && slo.name} - {!!slo && ( - - - - )} + + {slo.name} + ); } diff --git a/x-pack/plugins/observability/public/pages/slo_details/helpers/convert_sli_apm_params_to_apm_app_deeplink_url.ts b/x-pack/plugins/observability/public/pages/slo_details/helpers/convert_sli_apm_params_to_apm_app_deeplink_url.ts deleted file mode 100644 index c90b64cb73558..0000000000000 --- a/x-pack/plugins/observability/public/pages/slo_details/helpers/convert_sli_apm_params_to_apm_app_deeplink_url.ts +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -interface Props { - duration: string; - environment: string; - filter: string | undefined; - service: string; - transactionName: string; - transactionType: string; -} - -export function convertSliApmParamsToApmAppDeeplinkUrl({ - duration, - environment, - filter, - service, - transactionName, - transactionType, -}: Props) { - if (!service) { - return ''; - } - - const environmentPartial = environment - ? `&environment=${environment === '*' ? 'ENVIRONMENT_ALL' : environment}` - : ''; - - const transactionTypePartial = transactionType - ? `&transactionType=${transactionType === '*' ? '' : transactionType}` - : ''; - - const dateRangePartial = duration ? `&rangeFrom=now-${duration}&rangeTo=now` : ''; - - const filterPartial = - filter || transactionName - ? `&kuery=${encodeURIComponent( - `${ - transactionName && transactionName !== '*' - ? `transaction.name : "${transactionName}"` - : '' - } ${filter ? `and ${filter}` : ''}` - )}` - : ''; - - return `/app/apm/services/${service}/overview?comparisonEnabled=true${environmentPartial}${transactionTypePartial}${filterPartial}${dateRangePartial}`; -} diff --git a/x-pack/plugins/observability/public/pages/slo_details/slo_details.tsx b/x-pack/plugins/observability/public/pages/slo_details/slo_details.tsx index dc745ec353459..bbb606e76c079 100644 --- a/x-pack/plugins/observability/public/pages/slo_details/slo_details.tsx +++ b/x-pack/plugins/observability/public/pages/slo_details/slo_details.tsx @@ -22,10 +22,11 @@ import PageNotFound from '../404'; import { SloDetails } from './components/slo_details'; import { HeaderTitle } from './components/header_title'; import { HeaderControl } from './components/header_control'; -import { convertSliApmParamsToApmAppDeeplinkUrl } from './helpers/convert_sli_apm_params_to_apm_app_deeplink_url'; import { paths } from '../../config/paths'; import type { SloDetailsPathParams } from './types'; import type { ObservabilityAppServices } from '../../application/types'; +import { isApmIndicatorType } from '../../utils/slo/indicator'; +import { convertSliApmParamsToApmAppDeeplinkUrl } from '../../utils/slo/convert_sli_apm_params_to_apm_app_deeplink_url'; export function SloDetailsPage() { const { @@ -80,7 +81,7 @@ export function SloDetailsPage() { pageTitle: , rightSideItems: [ , - slo?.indicator.type.includes('apm') ? ( + !!slo && isApmIndicatorType(slo.indicator.type) ? ( = (props: Props) => ( + + + +); + +export const Default = Template.bind({}); +Default.args = { activeAlerts: { count: 2, ruleIds: ['rule-1', 'rule-2'] } }; diff --git a/x-pack/plugins/observability/public/pages/slos/components/badges/slo_active_alerts_badge.tsx b/x-pack/plugins/observability/public/pages/slos/components/badges/slo_active_alerts_badge.tsx new file mode 100644 index 0000000000000..e64d4b2b67fcc --- /dev/null +++ b/x-pack/plugins/observability/public/pages/slos/components/badges/slo_active_alerts_badge.tsx @@ -0,0 +1,60 @@ +/* + * 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 { EuiBadge, EuiFlexItem } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { paths } from '../../../../config/paths'; +import { useKibana } from '../../../../utils/kibana_react'; + +import { ActiveAlerts } from '../../../../hooks/slo/use_fetch_active_alerts'; +import { toAlertsPageQueryFilter } from '../../helpers/alerts_page_query_filter'; + +export interface Props { + activeAlerts?: ActiveAlerts; +} + +export function SloActiveAlertsBadge({ activeAlerts }: Props) { + const { + application: { navigateToUrl }, + http: { basePath }, + } = useKibana().services; + + const handleActiveAlertsClick = () => { + if (activeAlerts) { + navigateToUrl( + `${basePath.prepend(paths.observability.alerts)}?_a=${toAlertsPageQueryFilter( + activeAlerts + )}` + ); + } + }; + + if (!activeAlerts) { + return null; + } + + return ( + + + {i18n.translate('xpack.observability.slo.slo.activeAlertsBadge.label', { + defaultMessage: '{count, plural, one {# alert} other {# alerts}}', + values: { count: activeAlerts.count }, + })} + + + ); +} diff --git a/x-pack/plugins/observability/public/pages/slos/components/badges/slo_badges.stories.tsx b/x-pack/plugins/observability/public/pages/slos/components/badges/slo_badges.stories.tsx index f3abb871e6e2f..50430e0f6bd0b 100644 --- a/x-pack/plugins/observability/public/pages/slos/components/badges/slo_badges.stories.tsx +++ b/x-pack/plugins/observability/public/pages/slos/components/badges/slo_badges.stories.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { ComponentStory } from '@storybook/react'; +import { EuiFlexGroup } from '@elastic/eui'; import { buildForecastedSlo } from '../../../../data/slo/slo'; import { KibanaReactStorybookDecorator } from '../../../../utils/kibana_react.storybook_decorator'; import { SloBadges as Component, Props } from './slo_badges'; @@ -18,7 +19,11 @@ export default { decorators: [KibanaReactStorybookDecorator], }; -const Template: ComponentStory = (props: Props) => ; +const Template: ComponentStory = (props: Props) => ( + + + +); const defaultProps = { slo: buildForecastedSlo(), diff --git a/x-pack/plugins/observability/public/pages/slos/components/badges/slo_badges.tsx b/x-pack/plugins/observability/public/pages/slos/components/badges/slo_badges.tsx index 1ad2a8892ae28..dacee509af089 100644 --- a/x-pack/plugins/observability/public/pages/slos/components/badges/slo_badges.tsx +++ b/x-pack/plugins/observability/public/pages/slos/components/badges/slo_badges.tsx @@ -6,17 +6,14 @@ */ import React from 'react'; -import { EuiBadge, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { EuiFlexGroup } from '@elastic/eui'; import { SLOWithSummaryResponse } from '@kbn/slo-schema'; -import { i18n } from '@kbn/i18n'; -import { useKibana } from '../../../../utils/kibana_react'; import { SloIndicatorTypeBadge } from './slo_indicator_type_badge'; import { SloStatusBadge } from '../../../../components/slo/slo_status_badge'; import { SloTimeWindowBadge } from './slo_time_window_badge'; -import { toAlertsPageQueryFilter } from '../../helpers/alerts_page_query_filter'; -import { paths } from '../../../../config/paths'; import type { ActiveAlerts } from '../../../../hooks/slo/use_fetch_active_alerts'; +import { SloActiveAlertsBadge } from './slo_active_alerts_badge'; export interface Props { slo: SLOWithSummaryResponse; @@ -24,49 +21,12 @@ export interface Props { } export function SloBadges({ slo, activeAlerts }: Props) { - const { - application: { navigateToUrl }, - http: { basePath }, - } = useKibana().services; - - const handleClick = () => { - if (activeAlerts) { - navigateToUrl( - `${basePath.prepend(paths.observability.alerts)}?_a=${toAlertsPageQueryFilter( - activeAlerts - )}` - ); - } - }; - return ( - - - - - - - {!!activeAlerts && ( - - - {i18n.translate('xpack.observability.slo.slo.activeAlertsBadge.label', { - defaultMessage: '{count, plural, one {# alert} other {# alerts}}', - values: { count: activeAlerts.count }, - })} - - - )} + + + ); } diff --git a/x-pack/plugins/observability/public/pages/slos/components/badges/slo_indicator_type_badge.stories.tsx b/x-pack/plugins/observability/public/pages/slos/components/badges/slo_indicator_type_badge.stories.tsx index a8a3933be95d4..fa451706de164 100644 --- a/x-pack/plugins/observability/public/pages/slos/components/badges/slo_indicator_type_badge.stories.tsx +++ b/x-pack/plugins/observability/public/pages/slos/components/badges/slo_indicator_type_badge.stories.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { ComponentStory } from '@storybook/react'; +import { EuiFlexGroup } from '@elastic/eui'; import { buildCustomKqlIndicator, buildApmAvailabilityIndicator, @@ -23,7 +24,11 @@ export default { decorators: [KibanaReactStorybookDecorator], }; -const Template: ComponentStory = (props: Props) => ; +const Template: ComponentStory = (props: Props) => ( + + + +); export const WithCustomKql = Template.bind({}); WithCustomKql.args = { slo: buildSlo({ indicator: buildCustomKqlIndicator() }) }; diff --git a/x-pack/plugins/observability/public/pages/slos/components/badges/slo_indicator_type_badge.tsx b/x-pack/plugins/observability/public/pages/slos/components/badges/slo_indicator_type_badge.tsx index 8a16a0e73fd06..87f005e2fa5c0 100644 --- a/x-pack/plugins/observability/public/pages/slos/components/badges/slo_indicator_type_badge.tsx +++ b/x-pack/plugins/observability/public/pages/slos/components/badges/slo_indicator_type_badge.tsx @@ -6,21 +6,83 @@ */ import React from 'react'; -import { EuiBadge } from '@elastic/eui'; +import { EuiBadge, EuiFlexItem, EuiToolTip } from '@elastic/eui'; import { SLOWithSummaryResponse } from '@kbn/slo-schema'; import { euiLightVars } from '@kbn/ui-theme'; +import { i18n } from '@kbn/i18n'; +import { useKibana } from '../../../../utils/kibana_react'; +import { convertSliApmParamsToApmAppDeeplinkUrl } from '../../../../utils/slo/convert_sli_apm_params_to_apm_app_deeplink_url'; +import { isApmIndicatorType } from '../../../../utils/slo/indicator'; import { toIndicatorTypeLabel } from '../../../../utils/slo/labels'; + export interface Props { slo: SLOWithSummaryResponse; } export function SloIndicatorTypeBadge({ slo }: Props) { + const { + application: { navigateToUrl }, + http: { basePath }, + } = useKibana().services; + + const handleNavigateToApm = () => { + if ( + slo.indicator.type === 'sli.apm.transactionDuration' || + slo.indicator.type === 'sli.apm.transactionErrorRate' + ) { + const { + indicator: { + params: { environment, filter, service, transactionName, transactionType }, + }, + timeWindow: { duration }, + } = slo; + + const url = convertSliApmParamsToApmAppDeeplinkUrl({ + duration, + environment, + filter, + service, + transactionName, + transactionType, + }); + + navigateToUrl(basePath.prepend(url)); + } + }; + return ( -
- - {toIndicatorTypeLabel(slo.indicator.type)} - -
+ <> + + + {toIndicatorTypeLabel(slo.indicator.type)} + + + {isApmIndicatorType(slo.indicator.type) && 'service' in slo.indicator.params && ( + + + + {slo.indicator.params.service} + + + + )} + ); } diff --git a/x-pack/plugins/observability/public/pages/slos/components/badges/slo_time_window_badge.stories.tsx b/x-pack/plugins/observability/public/pages/slos/components/badges/slo_time_window_badge.stories.tsx index 361b839b7bf7e..7f534e831f9fb 100644 --- a/x-pack/plugins/observability/public/pages/slos/components/badges/slo_time_window_badge.stories.tsx +++ b/x-pack/plugins/observability/public/pages/slos/components/badges/slo_time_window_badge.stories.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { ComponentStory } from '@storybook/react'; +import { EuiFlexGroup } from '@elastic/eui'; import { KibanaReactStorybookDecorator } from '../../../../utils/kibana_react.storybook_decorator'; import { SloTimeWindowBadge as Component, Props } from './slo_time_window_badge'; import { buildSlo } from '../../../../data/slo/slo'; @@ -18,7 +19,11 @@ export default { decorators: [KibanaReactStorybookDecorator], }; -const Template: ComponentStory = (props: Props) => ; +const Template: ComponentStory = (props: Props) => ( + + + +); export const With7DaysRolling = Template.bind({}); With7DaysRolling.args = { slo: buildSlo({ timeWindow: { duration: '7d', isRolling: true } }) }; diff --git a/x-pack/plugins/observability/public/pages/slos/components/badges/slo_time_window_badge.tsx b/x-pack/plugins/observability/public/pages/slos/components/badges/slo_time_window_badge.tsx index e7291a2292390..aefec729896f1 100644 --- a/x-pack/plugins/observability/public/pages/slos/components/badges/slo_time_window_badge.tsx +++ b/x-pack/plugins/observability/public/pages/slos/components/badges/slo_time_window_badge.tsx @@ -7,7 +7,7 @@ import moment from 'moment'; import React from 'react'; -import { EuiBadge } from '@elastic/eui'; +import { EuiBadge, EuiFlexItem } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { euiLightVars } from '@kbn/ui-theme'; import { SLOWithSummaryResponse } from '@kbn/slo-schema'; @@ -24,7 +24,7 @@ export function SloTimeWindowBadge({ slo }: Props) { const unit = slo.timeWindow.duration.slice(-1); if ('isRolling' in slo.timeWindow) { return ( -
+ {toDurationLabel(slo.timeWindow.duration)} -
+ ); } @@ -50,7 +50,7 @@ export function SloTimeWindowBadge({ slo }: Props) { const elapsedDurationInDays = now.diff(periodStart, 'days') + 1; return ( -
+ {i18n.translate('xpack.observability.slo.slo.timeWindow.calendar', { defaultMessage: '{elapsed}/{total} days', @@ -60,6 +60,6 @@ export function SloTimeWindowBadge({ slo }: Props) { }, })} -
+ ); } diff --git a/x-pack/plugins/observability/public/typings/fetch_overview_data/index.ts b/x-pack/plugins/observability/public/typings/fetch_overview_data/index.ts index 64a66dff66e42..78837ed27f800 100644 --- a/x-pack/plugins/observability/public/typings/fetch_overview_data/index.ts +++ b/x-pack/plugins/observability/public/typings/fetch_overview_data/index.ts @@ -76,7 +76,7 @@ export type HasData = ( export type ObservabilityFetchDataPlugins = Exclude< ObservabilityApp, - 'observability-overview' | 'stack_monitoring' | 'uptime' | 'fleet' + 'observability-overview' | 'stack_monitoring' | 'fleet' | 'synthetics' >; export interface DataHandler< @@ -154,7 +154,7 @@ export interface ObservabilityFetchDataResponse { apm: ApmFetchDataResponse; infra_metrics: MetricsFetchDataResponse; infra_logs: LogsFetchDataResponse; - synthetics: UptimeFetchDataResponse; + uptime: UptimeFetchDataResponse; ux: UxFetchDataResponse; } @@ -162,6 +162,6 @@ export interface ObservabilityHasDataResponse { apm: APMHasDataResponse; infra_metrics: InfraMetricsHasDataResponse; infra_logs: InfraLogsHasDataResponse; - synthetics: SyntheticsHasDataResponse; + uptime: SyntheticsHasDataResponse; ux: UXHasDataResponse; } diff --git a/x-pack/plugins/observability/public/utils/observability_data_views/observability_data_views.ts b/x-pack/plugins/observability/public/utils/observability_data_views/observability_data_views.ts index 75851f87fa8d6..f5e8893ce1b28 100644 --- a/x-pack/plugins/observability/public/utils/observability_data_views/observability_data_views.ts +++ b/x-pack/plugins/observability/public/utils/observability_data_views/observability_data_views.ts @@ -33,6 +33,7 @@ const appFieldFormats: Record = { infra_metrics: infraMetricsFieldFormats, ux: rumFieldFormats, apm: apmFieldFormats, + uptime: syntheticsFieldFormats, synthetics: syntheticsFieldFormats, mobile: apmFieldFormats, alerts: null, @@ -43,6 +44,7 @@ const appRuntimeFields: Record = { synthetics: 'synthetics_static_index_pattern_id', + uptime: 'uptime_static_index_pattern_id', apm: 'apm_static_index_pattern_id', ux: 'rum_static_index_pattern_id', infra_logs: 'infra_logs_static_index_pattern_id', @@ -75,6 +78,11 @@ const getAppDataViewId = (app: AppDataType, indices: string) => { export async function getDataTypeIndices(dataType: AppDataType) { switch (dataType) { + case 'synthetics': + return { + hasData: true, + indices: 'synthetics-*', + }; case 'mobile': case 'ux': case 'apm': diff --git a/x-pack/plugins/observability/public/pages/slo_details/helpers/convert_sli_apm_params_to_apm_app_deeplink_url.test.ts b/x-pack/plugins/observability/public/utils/slo/convert_sli_apm_params_to_apm_app_deeplink_url.test.ts similarity index 54% rename from x-pack/plugins/observability/public/pages/slo_details/helpers/convert_sli_apm_params_to_apm_app_deeplink_url.test.ts rename to x-pack/plugins/observability/public/utils/slo/convert_sli_apm_params_to_apm_app_deeplink_url.test.ts index f69cba40335cb..11236c3d6bbd8 100644 --- a/x-pack/plugins/observability/public/pages/slo_details/helpers/convert_sli_apm_params_to_apm_app_deeplink_url.test.ts +++ b/x-pack/plugins/observability/public/utils/slo/convert_sli_apm_params_to_apm_app_deeplink_url.test.ts @@ -10,7 +10,7 @@ import { convertSliApmParamsToApmAppDeeplinkUrl } from './convert_sli_apm_params const SLI_APM_PARAMS = { duration: '30-d', environment: 'fooEnvironment', - filter: 'agent.name : "beats" ', + filter: 'agent.name : "beats" and agent.version : 3.4.12 ', service: 'barService', transactionName: 'bazName', transactionType: 'blarfType', @@ -20,8 +20,8 @@ describe('convertSliApmParamsToApmAppDeeplinkUrl', () => { it('should return a correct APM deeplink when all params have a value', () => { const url = convertSliApmParamsToApmAppDeeplinkUrl(SLI_APM_PARAMS); - expect(url).toBe( - '/app/apm/services/barService/overview?comparisonEnabled=true&environment=fooEnvironment&transactionType=blarfType&kuery=transaction.name%20%3A%20%22bazName%22%20and%20agent.name%20%3A%20%22beats%22%20&rangeFrom=now-30-d&rangeTo=now' + expect(url).toMatchInlineSnapshot( + `"/app/apm/services/barService/overview?comparisonEnabled=true&environment=fooEnvironment&transactionType=blarfType&rangeFrom=now-30-d&rangeTo=now&kuery=transaction.name+%3A+%22bazName%22+and+agent.name+%3A+%22beats%22+and+agent.version+%3A+3.4.12+"` ); }); @@ -31,8 +31,8 @@ describe('convertSliApmParamsToApmAppDeeplinkUrl', () => { duration: '', }); - expect(url).toBe( - '/app/apm/services/barService/overview?comparisonEnabled=true&environment=fooEnvironment&transactionType=blarfType&kuery=transaction.name%20%3A%20%22bazName%22%20and%20agent.name%20%3A%20%22beats%22%20' + expect(url).toMatchInlineSnapshot( + `"/app/apm/services/barService/overview?comparisonEnabled=true&environment=fooEnvironment&transactionType=blarfType&kuery=transaction.name+%3A+%22bazName%22+and+agent.name+%3A+%22beats%22+and+agent.version+%3A+3.4.12+"` ); }); @@ -42,8 +42,8 @@ describe('convertSliApmParamsToApmAppDeeplinkUrl', () => { environment: '', }); - expect(url).toBe( - '/app/apm/services/barService/overview?comparisonEnabled=true&transactionType=blarfType&kuery=transaction.name%20%3A%20%22bazName%22%20and%20agent.name%20%3A%20%22beats%22%20&rangeFrom=now-30-d&rangeTo=now' + expect(url).toMatchInlineSnapshot( + `"/app/apm/services/barService/overview?comparisonEnabled=true&transactionType=blarfType&rangeFrom=now-30-d&rangeTo=now&kuery=transaction.name+%3A+%22bazName%22+and+agent.name+%3A+%22beats%22+and+agent.version+%3A+3.4.12+"` ); }); @@ -53,8 +53,8 @@ describe('convertSliApmParamsToApmAppDeeplinkUrl', () => { filter: '', }); - expect(url).toBe( - '/app/apm/services/barService/overview?comparisonEnabled=true&environment=fooEnvironment&transactionType=blarfType&kuery=transaction.name%20%3A%20%22bazName%22%20&rangeFrom=now-30-d&rangeTo=now' + expect(url).toMatchInlineSnapshot( + `"/app/apm/services/barService/overview?comparisonEnabled=true&environment=fooEnvironment&transactionType=blarfType&rangeFrom=now-30-d&rangeTo=now&kuery=transaction.name+%3A+%22bazName%22"` ); }); @@ -64,7 +64,7 @@ describe('convertSliApmParamsToApmAppDeeplinkUrl', () => { service: '', }); - expect(url).toBe(''); + expect(url).toMatchInlineSnapshot(`""`); }); it('should return a correct APM deeplink when empty transactionName is passed', () => { @@ -73,8 +73,8 @@ describe('convertSliApmParamsToApmAppDeeplinkUrl', () => { transactionName: '', }); - expect(url).toBe( - '/app/apm/services/barService/overview?comparisonEnabled=true&environment=fooEnvironment&transactionType=blarfType&kuery=%20and%20agent.name%20%3A%20%22beats%22%20&rangeFrom=now-30-d&rangeTo=now' + expect(url).toMatchInlineSnapshot( + `"/app/apm/services/barService/overview?comparisonEnabled=true&environment=fooEnvironment&transactionType=blarfType&rangeFrom=now-30-d&rangeTo=now&kuery=agent.name+%3A+%22beats%22+and+agent.version+%3A+3.4.12+"` ); }); @@ -84,8 +84,8 @@ describe('convertSliApmParamsToApmAppDeeplinkUrl', () => { transactionType: '', }); - expect(url).toBe( - '/app/apm/services/barService/overview?comparisonEnabled=true&environment=fooEnvironment&kuery=transaction.name%20%3A%20%22bazName%22%20and%20agent.name%20%3A%20%22beats%22%20&rangeFrom=now-30-d&rangeTo=now' + expect(url).toMatchInlineSnapshot( + `"/app/apm/services/barService/overview?comparisonEnabled=true&environment=fooEnvironment&rangeFrom=now-30-d&rangeTo=now&kuery=transaction.name+%3A+%22bazName%22+and+agent.name+%3A+%22beats%22+and+agent.version+%3A+3.4.12+"` ); }); }); diff --git a/x-pack/plugins/observability/public/utils/slo/convert_sli_apm_params_to_apm_app_deeplink_url.ts b/x-pack/plugins/observability/public/utils/slo/convert_sli_apm_params_to_apm_app_deeplink_url.ts new file mode 100644 index 0000000000000..d15096cc59a01 --- /dev/null +++ b/x-pack/plugins/observability/public/utils/slo/convert_sli_apm_params_to_apm_app_deeplink_url.ts @@ -0,0 +1,57 @@ +/* + * 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. + */ + +interface Props { + duration: string; + environment: string; + filter: string | undefined; + service: string; + transactionName: string; + transactionType: string; +} + +export function convertSliApmParamsToApmAppDeeplinkUrl({ + duration, + environment, + filter, + service, + transactionName, + transactionType, +}: Props) { + if (!service) { + return ''; + } + + const qs = new URLSearchParams('comparisonEnabled=true'); + + if (environment) { + qs.append('environment', environment === '*' ? 'ENVIRONMENT_ALL' : environment); + } + + if (transactionType) { + qs.append('transactionType', transactionType === '*' ? '' : transactionType); + } + + if (duration) { + qs.append('rangeFrom', `now-${duration}`); + qs.append('rangeTo', 'now'); + } + + const kueryParams = []; + if (transactionName && transactionName !== '*') { + kueryParams.push(`transaction.name : "${transactionName}"`); + } + if (filter && filter.length > 0) { + kueryParams.push(filter); + } + + if (kueryParams.length > 0) { + qs.append('kuery', kueryParams.join(' and ')); + } + + return `/app/apm/services/${service}/overview?${qs.toString()}`; +} diff --git a/x-pack/plugins/observability/public/utils/slo/indicator.ts b/x-pack/plugins/observability/public/utils/slo/indicator.ts new file mode 100644 index 0000000000000..955eab9e33299 --- /dev/null +++ b/x-pack/plugins/observability/public/utils/slo/indicator.ts @@ -0,0 +1,11 @@ +/* + * 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 { SLOWithSummaryResponse } from '@kbn/slo-schema'; + +export const isApmIndicatorType = (indicatorType: SLOWithSummaryResponse['indicator']['type']) => + ['sli.apm.transactionDuration', 'sli.apm.transactionErrorRate'].includes(indicatorType); diff --git a/x-pack/plugins/synthetics/common/constants/synthetics/rest_api.ts b/x-pack/plugins/synthetics/common/constants/synthetics/rest_api.ts index a0875d9df4977..e310762c79146 100644 --- a/x-pack/plugins/synthetics/common/constants/synthetics/rest_api.ts +++ b/x-pack/plugins/synthetics/common/constants/synthetics/rest_api.ts @@ -16,4 +16,9 @@ export enum SYNTHETICS_API_URLS { SYNC_GLOBAL_PARAMS = `/synthetics/sync_global_params`, ENABLE_DEFAULT_ALERTING = `/synthetics/enable_default_alerting`, JOURNEY = `/internal/synthetics/journey/{checkGroup}`, + SYNTHETICS_SUCCESSFUL_CHECK = `/internal/synthetics/synthetics/check/success`, + JOURNEY_SCREENSHOT_BLOCKS = `/internal/synthetics/journey/screenshot/block`, + JOURNEY_FAILED_STEPS = `/internal/synthetics/journeys/failed_steps`, + NETWORK_EVENTS = `/internal/synthetics/network_events`, + JOURNEY_SCREENSHOT = `/internal/synthetics/journey/screenshot/{checkGroup}/{stepIndex}`, } diff --git a/x-pack/plugins/synthetics/common/formatters/browser/formatters.ts b/x-pack/plugins/synthetics/common/formatters/browser/formatters.ts index 409f2b44343e8..9e9f7283ec38f 100644 --- a/x-pack/plugins/synthetics/common/formatters/browser/formatters.ts +++ b/x-pack/plugins/synthetics/common/formatters/browser/formatters.ts @@ -6,18 +6,16 @@ */ import { BrowserFields, ConfigKey } from '../../runtime_types/monitor_management'; +import { Formatter, commonFormatters } from '../common/formatters'; import { - Formatter, - commonFormatters, - objectToJsonFormatter, arrayToJsonFormatter, + objectToJsonFormatter, stringToJsonFormatter, -} from '../common/formatters'; -import { - tlsValueToYamlFormatter, - tlsValueToStringFormatter, tlsArrayToYamlFormatter, -} from '../tls/formatters'; + tlsValueToStringFormatter, + tlsValueToYamlFormatter, +} from '../formatting_utils'; + import { tlsFormatters } from '../tls/formatters'; export type BrowserFormatMap = Record; @@ -37,44 +35,40 @@ const throttlingFormatter: Formatter = (fields) => { .join('/'); }; -export const browserFormatters: BrowserFormatMap = { - [ConfigKey.METADATA]: (fields) => objectToJsonFormatter(fields[ConfigKey.METADATA]), - [ConfigKey.URLS]: null, - [ConfigKey.PORT]: null, +export const deprecatedZipUrlFormatters = { [ConfigKey.SOURCE_ZIP_URL]: null, [ConfigKey.SOURCE_ZIP_USERNAME]: null, [ConfigKey.SOURCE_ZIP_PASSWORD]: null, [ConfigKey.SOURCE_ZIP_FOLDER]: null, [ConfigKey.SOURCE_ZIP_PROXY_URL]: null, + [ConfigKey.ZIP_URL_TLS_CERTIFICATE_AUTHORITIES]: tlsValueToYamlFormatter, + [ConfigKey.ZIP_URL_TLS_CERTIFICATE]: tlsValueToYamlFormatter, + [ConfigKey.ZIP_URL_TLS_KEY]: tlsValueToYamlFormatter, + [ConfigKey.ZIP_URL_TLS_KEY_PASSPHRASE]: tlsValueToStringFormatter, + [ConfigKey.ZIP_URL_TLS_VERIFICATION_MODE]: tlsValueToStringFormatter, + [ConfigKey.ZIP_URL_TLS_VERSION]: tlsArrayToYamlFormatter, +}; + +export const browserFormatters: BrowserFormatMap = { [ConfigKey.SOURCE_PROJECT_CONTENT]: null, - [ConfigKey.SOURCE_INLINE]: (fields) => stringToJsonFormatter(fields[ConfigKey.SOURCE_INLINE]), [ConfigKey.PARAMS]: null, [ConfigKey.SCREENSHOTS]: null, [ConfigKey.IS_THROTTLING_ENABLED]: null, [ConfigKey.DOWNLOAD_SPEED]: null, [ConfigKey.UPLOAD_SPEED]: null, [ConfigKey.LATENCY]: null, - [ConfigKey.SYNTHETICS_ARGS]: (fields) => arrayToJsonFormatter(fields[ConfigKey.SYNTHETICS_ARGS]), - [ConfigKey.ZIP_URL_TLS_CERTIFICATE_AUTHORITIES]: (fields) => - tlsValueToYamlFormatter(fields[ConfigKey.ZIP_URL_TLS_CERTIFICATE_AUTHORITIES]), - [ConfigKey.ZIP_URL_TLS_CERTIFICATE]: (fields) => - tlsValueToYamlFormatter(fields[ConfigKey.ZIP_URL_TLS_CERTIFICATE]), - [ConfigKey.ZIP_URL_TLS_KEY]: (fields) => - tlsValueToYamlFormatter(fields[ConfigKey.ZIP_URL_TLS_KEY]), - [ConfigKey.ZIP_URL_TLS_KEY_PASSPHRASE]: (fields) => - tlsValueToStringFormatter(fields[ConfigKey.ZIP_URL_TLS_KEY_PASSPHRASE]), - [ConfigKey.ZIP_URL_TLS_VERIFICATION_MODE]: (fields) => - tlsValueToStringFormatter(fields[ConfigKey.ZIP_URL_TLS_VERIFICATION_MODE]), - [ConfigKey.ZIP_URL_TLS_VERSION]: (fields) => - tlsArrayToYamlFormatter(fields[ConfigKey.ZIP_URL_TLS_VERSION]), - [ConfigKey.JOURNEY_FILTERS_MATCH]: (fields) => - stringToJsonFormatter(fields[ConfigKey.JOURNEY_FILTERS_MATCH]), - [ConfigKey.JOURNEY_FILTERS_TAGS]: (fields) => - arrayToJsonFormatter(fields[ConfigKey.JOURNEY_FILTERS_TAGS]), - [ConfigKey.THROTTLING_CONFIG]: throttlingFormatter, [ConfigKey.IGNORE_HTTPS_ERRORS]: null, [ConfigKey.PLAYWRIGHT_OPTIONS]: null, [ConfigKey.TEXT_ASSERTION]: null, + [ConfigKey.PORT]: null, + [ConfigKey.URLS]: null, + [ConfigKey.METADATA]: objectToJsonFormatter, + [ConfigKey.SOURCE_INLINE]: stringToJsonFormatter, + [ConfigKey.SYNTHETICS_ARGS]: arrayToJsonFormatter, + [ConfigKey.JOURNEY_FILTERS_MATCH]: stringToJsonFormatter, + [ConfigKey.JOURNEY_FILTERS_TAGS]: arrayToJsonFormatter, + [ConfigKey.THROTTLING_CONFIG]: throttlingFormatter, + ...deprecatedZipUrlFormatters, ...commonFormatters, ...tlsFormatters, }; diff --git a/x-pack/plugins/synthetics/common/formatters/common/formatters.test.ts b/x-pack/plugins/synthetics/common/formatters/common/formatters.test.ts index 6ab1838737297..47286270d28ed 100644 --- a/x-pack/plugins/synthetics/common/formatters/common/formatters.test.ts +++ b/x-pack/plugins/synthetics/common/formatters/common/formatters.test.ts @@ -5,49 +5,62 @@ * 2.0. */ +import { ConfigKey } from '../../runtime_types'; import { + secondsToCronFormatter, arrayToJsonFormatter, objectToJsonFormatter, stringToJsonFormatter, - secondsToCronFormatter, -} from './formatters'; +} from '../formatting_utils'; describe('formatters', () => { describe('cronToSecondsNormalizer', () => { it('takes a number of seconds and converts it to cron format', () => { - expect(secondsToCronFormatter('3')).toEqual('3s'); + expect(secondsToCronFormatter({ [ConfigKey.WAIT]: '3' }, ConfigKey.WAIT)).toEqual('3s'); }); }); describe('arrayToJsonFormatter', () => { it('takes an array and converts it to json', () => { - expect(arrayToJsonFormatter(['tag1', 'tag2'])).toEqual('["tag1","tag2"]'); + expect(arrayToJsonFormatter({ [ConfigKey.TAGS]: ['tag1', 'tag2'] }, ConfigKey.TAGS)).toEqual( + '["tag1","tag2"]' + ); }); it('returns null if the array has length of 0', () => { - expect(arrayToJsonFormatter([])).toEqual(null); + expect(arrayToJsonFormatter({ [ConfigKey.TAGS]: [] }, ConfigKey.TAGS)).toEqual(null); }); }); describe('objectToJsonFormatter', () => { it('takes a json object string and returns an object', () => { - expect(objectToJsonFormatter({ key: 'value' })).toEqual('{"key":"value"}'); + expect( + objectToJsonFormatter( + { [ConfigKey.RESPONSE_HEADERS_CHECK]: { key: 'value' } }, + ConfigKey.RESPONSE_HEADERS_CHECK + ) + ).toEqual('{"key":"value"}'); }); it('returns null if the object has no keys', () => { - expect(objectToJsonFormatter({})).toEqual(null); + expect(objectToJsonFormatter({ [ConfigKey.METADATA]: {} }, ConfigKey.METADATA)).toEqual(null); }); }); describe('stringToJsonFormatter', () => { it('takes a string and returns an json string', () => { - expect(stringToJsonFormatter('step("test step", () => {})')).toEqual( - '"step(\\"test step\\", () => {})"' - ); + expect( + stringToJsonFormatter( + { [ConfigKey.SOURCE_INLINE]: 'step("test step", () => {})' }, + ConfigKey.SOURCE_INLINE + ) + ).toEqual('"step(\\"test step\\", () => {})"'); }); it('returns null if the string is falsy', () => { - expect(stringToJsonFormatter('')).toEqual(null); + expect( + stringToJsonFormatter({ [ConfigKey.SOURCE_INLINE]: '' }, ConfigKey.SOURCE_INLINE) + ).toEqual(null); }); }); }); diff --git a/x-pack/plugins/synthetics/common/formatters/common/formatters.ts b/x-pack/plugins/synthetics/common/formatters/common/formatters.ts index c900d809a4e29..3122bcdb46442 100644 --- a/x-pack/plugins/synthetics/common/formatters/common/formatters.ts +++ b/x-pack/plugins/synthetics/common/formatters/common/formatters.ts @@ -5,26 +5,21 @@ * 2.0. */ -import { CommonFields, ConfigKey, MonitorFields } from '../../runtime_types/monitor_management'; +import { CommonFields, ConfigKey, SourceType } from '../../runtime_types/monitor_management'; +import { arrayToJsonFormatter, FormatterFn } from '../formatting_utils'; -export type Formatter = null | ((fields: Partial) => string | null); +export type Formatter = null | FormatterFn; export type CommonFormatMap = Record; export const commonFormatters: CommonFormatMap = { + [ConfigKey.APM_SERVICE_NAME]: null, [ConfigKey.NAME]: null, [ConfigKey.LOCATIONS]: null, [ConfigKey.MONITOR_TYPE]: null, [ConfigKey.ENABLED]: null, [ConfigKey.ALERT_CONFIG]: null, [ConfigKey.CONFIG_ID]: null, - [ConfigKey.SCHEDULE]: (fields) => - JSON.stringify( - `@every ${fields[ConfigKey.SCHEDULE]?.number}${fields[ConfigKey.SCHEDULE]?.unit}` - ), - [ConfigKey.APM_SERVICE_NAME]: null, - [ConfigKey.TAGS]: (fields) => arrayToJsonFormatter(fields[ConfigKey.TAGS]), - [ConfigKey.TIMEOUT]: (fields) => secondsToCronFormatter(fields[ConfigKey.TIMEOUT] || undefined), [ConfigKey.NAMESPACE]: null, [ConfigKey.REVISION]: null, [ConfigKey.MONITOR_SOURCE_TYPE]: null, @@ -35,14 +30,14 @@ export const commonFormatters: CommonFormatMap = { [ConfigKey.ORIGINAL_SPACE]: null, [ConfigKey.CONFIG_HASH]: null, [ConfigKey.MONITOR_QUERY_ID]: null, + [ConfigKey.SCHEDULE]: (fields) => + JSON.stringify( + `@every ${fields[ConfigKey.SCHEDULE]?.number}${fields[ConfigKey.SCHEDULE]?.unit}` + ), + [ConfigKey.TAGS]: arrayToJsonFormatter, + [ConfigKey.TIMEOUT]: (fields) => secondsToCronFormatter(fields[ConfigKey.TIMEOUT] || undefined), + [ConfigKey.MONITOR_SOURCE_TYPE]: (fields) => + fields[ConfigKey.MONITOR_SOURCE_TYPE] || SourceType.UI, }; -export const arrayToJsonFormatter = (value: string[] = []) => - value.length ? JSON.stringify(value) : null; - export const secondsToCronFormatter = (value: string = '') => (value ? `${value}s` : null); - -export const objectToJsonFormatter = (value: Record = {}) => - Object.keys(value).length ? JSON.stringify(value) : null; - -export const stringToJsonFormatter = (value: string = '') => (value ? JSON.stringify(value) : null); diff --git a/x-pack/plugins/synthetics/common/formatters/format_synthetics_policy.test.ts b/x-pack/plugins/synthetics/common/formatters/format_synthetics_policy.test.ts index 78660be7c18cf..9eca4aa70b216 100644 --- a/x-pack/plugins/synthetics/common/formatters/format_synthetics_policy.test.ts +++ b/x-pack/plugins/synthetics/common/formatters/format_synthetics_policy.test.ts @@ -7,12 +7,14 @@ import { ConfigKey, DataStream } from '../runtime_types'; import { formatSyntheticsPolicy } from './format_synthetics_policy'; +const gParams = { proxyUrl: 'https://proxy.com' }; describe('formatSyntheticsPolicy', () => { it('formats browser policy', () => { const { formattedPolicy } = formatSyntheticsPolicy( testNewPolicy, DataStream.BROWSER, - browserConfig + browserConfig, + gParams ); expect(formattedPolicy).toEqual({ @@ -377,7 +379,8 @@ describe('formatSyntheticsPolicy', () => { }, params: { type: 'yaml', - value: '', + value: + '{"proxyUrl":"https://proxy.com/local","proxyUsername":"username","proxyPassword":"password"}', }, playwright_options: { type: 'yaml', @@ -500,10 +503,15 @@ describe('formatSyntheticsPolicy', () => { }); it.each([true, false])('formats http policy', (isTLSEnabled) => { - const { formattedPolicy } = formatSyntheticsPolicy(testNewPolicy, DataStream.HTTP, { - ...httpPolicy, - [ConfigKey.METADATA]: { is_tls_enabled: isTLSEnabled }, - }); + const { formattedPolicy } = formatSyntheticsPolicy( + testNewPolicy, + DataStream.HTTP, + { + ...httpPolicy, + [ConfigKey.METADATA]: { is_tls_enabled: isTLSEnabled }, + }, + gParams + ); expect(formattedPolicy).toEqual({ enabled: true, @@ -591,7 +599,7 @@ describe('formatSyntheticsPolicy', () => { }, proxy_url: { type: 'text', - value: '', + value: 'https://proxy.com', }, 'response.include_body': { type: 'text', @@ -848,12 +856,9 @@ describe('formatSyntheticsPolicy', () => { vars: { __ui: { type: 'yaml', - value: - '{"script_source":{"is_generated_script":false,"file_name":""},"is_zip_url_tls_enabled":false,"is_tls_enabled":false}', }, config_id: { type: 'text', - value: '00bb3ceb-a242-4c7a-8405-8da963661374', }, enabled: { type: 'bool', @@ -861,23 +866,19 @@ describe('formatSyntheticsPolicy', () => { }, 'filter_journeys.match': { type: 'text', - value: null, }, 'filter_journeys.tags': { type: 'yaml', - value: null, }, id: { type: 'text', - value: '00bb3ceb-a242-4c7a-8405-8da963661374', }, ignore_https_errors: { type: 'bool', - value: false, }, location_name: { type: 'text', - value: 'Test private location 0', + value: 'Fleet managed', }, 'monitor.project.id': { type: 'text', @@ -887,19 +888,15 @@ describe('formatSyntheticsPolicy', () => { }, name: { type: 'text', - value: 'Test HTTP Monitor 03', }, origin: { type: 'text', - value: 'ui', }, params: { type: 'yaml', - value: '', }, playwright_options: { type: 'yaml', - value: '', }, run_once: { type: 'bool', @@ -911,32 +908,24 @@ describe('formatSyntheticsPolicy', () => { }, screenshots: { type: 'text', - value: 'on', }, 'service.name': { type: 'text', - value: '', }, 'source.inline.script': { type: 'yaml', - value: - '"step(\\"Visit /users api route\\", async () => {\\\\n const response = await page.goto(\'https://nextjs-test-synthetics.vercel.app/api/users\');\\\\n expect(response.status()).toEqual(200);\\\\n});"', }, 'source.project.content': { type: 'text', - value: '', }, 'source.zip_url.folder': { type: 'text', - value: '', }, 'source.zip_url.password': { type: 'password', - value: '', }, 'source.zip_url.proxy_url': { type: 'text', - value: '', }, 'source.zip_url.ssl.certificate': { type: 'yaml', @@ -958,27 +947,21 @@ describe('formatSyntheticsPolicy', () => { }, 'source.zip_url.url': { type: 'text', - value: '', }, 'source.zip_url.username': { type: 'text', - value: '', }, synthetics_args: { type: 'text', - value: null, }, tags: { type: 'yaml', - value: '["cookie-test","browser"]', }, 'throttling.config': { type: 'text', - value: '5d/3u/20l', }, timeout: { type: 'text', - value: '16s', }, type: { type: 'text', @@ -1232,7 +1215,8 @@ const browserConfig: any = { is_zip_url_tls_enabled: false, is_tls_enabled: false, }, - params: '', + params: + '{"proxyUrl":"https://proxy.com/local","proxyUsername":"username","proxyPassword":"password"}', 'url.port': null, 'source.inline.script': 'step("Visit /users api route", async () => {\\n const response = await page.goto(\'https://nextjs-test-synthetics.vercel.app/api/users\');\\n expect(response.status()).toEqual(200);\\n});', @@ -1295,7 +1279,7 @@ const httpPolicy: any = { max_redirects: '0', 'url.port': null, password: 'changeme', - proxy_url: '', + proxy_url: '${proxyUrl}', 'check.response.body.negative': [], 'check.response.body.positive': [], 'response.include_body': 'on_error', @@ -1314,6 +1298,6 @@ const httpPolicy: any = { 'ssl.supported_protocols': ['TLSv1.1', 'TLSv1.2', 'TLSv1.3'], fields: { config_id: '51ccd9d9-fc3f-4718-ba9d-b6ef80e73fc5' }, fields_under_root: true, - params: '', + params: '{"proxyUrl":"https://proxy.com"}', location_name: 'Test private location 0', }; diff --git a/x-pack/plugins/synthetics/common/formatters/format_synthetics_policy.ts b/x-pack/plugins/synthetics/common/formatters/format_synthetics_policy.ts index 866948f7215a2..392a50d59ed28 100644 --- a/x-pack/plugins/synthetics/common/formatters/format_synthetics_policy.ts +++ b/x-pack/plugins/synthetics/common/formatters/format_synthetics_policy.ts @@ -6,9 +6,20 @@ */ import { NewPackagePolicy } from '@kbn/fleet-plugin/common'; -import { formatters } from './formatters'; +import { cloneDeep } from 'lodash'; +import { replaceStringWithParams } from './formatting_utils'; +import { syntheticsPolicyFormatters } from './formatters'; import { ConfigKey, DataStream, MonitorFields } from '../runtime_types'; +export const PARAMS_KEYS_TO_SKIP = [ + 'secrets', + 'fields', + ConfigKey.LOCATIONS, + ConfigKey.TLS_VERSION, + ConfigKey.SOURCE_PROJECT_CONTENT, + ConfigKey.SOURCE_INLINE, +]; + export const formatSyntheticsPolicy = ( newPolicy: NewPackagePolicy, monitorType: DataStream, @@ -19,11 +30,12 @@ export const formatSyntheticsPolicy = ( 'monitor.project.id': string; } >, + params: Record, isLegacy?: boolean ) => { const configKeys = Object.keys(config) as ConfigKey[]; - const formattedPolicy = { ...newPolicy }; + const formattedPolicy = cloneDeep(newPolicy); const currentInput = formattedPolicy.inputs.find( (input) => input.type === `synthetics/${monitorType}` @@ -44,15 +56,18 @@ export const formatSyntheticsPolicy = ( configKeys.forEach((key) => { const configItem = dataStream?.vars?.[key]; if (configItem) { - if (formatters[key]) { - configItem.value = formatters[key]?.(config); + if (syntheticsPolicyFormatters[key]) { + configItem.value = syntheticsPolicyFormatters[key]?.(config, key); } else if (key === ConfigKey.MONITOR_SOURCE_TYPE && isLegacy) { configItem.value = undefined; } else { configItem.value = config[key] === undefined || config[key] === null ? null : config[key]; } + if (!PARAMS_KEYS_TO_SKIP.includes(key)) { + configItem.value = replaceStringWithParams(configItem.value, params); + } } }); - return { formattedPolicy, dataStream, currentInput }; + return { formattedPolicy, hasDataStream: Boolean(dataStream), hasInput: Boolean(currentInput) }; }; diff --git a/x-pack/plugins/synthetics/common/formatters/formatters.ts b/x-pack/plugins/synthetics/common/formatters/formatters.ts index 99931e4e24294..1dad248efc5f4 100644 --- a/x-pack/plugins/synthetics/common/formatters/formatters.ts +++ b/x-pack/plugins/synthetics/common/formatters/formatters.ts @@ -5,30 +5,15 @@ * 2.0. */ import { INVALID_NAMESPACE_CHARACTERS } from '@kbn/fleet-plugin/common'; -import { DataStream } from '../runtime_types'; -import { httpFormatters, HTTPFormatMap } from './http/formatters'; -import { tcpFormatters, TCPFormatMap } from './tcp/formatters'; -import { icmpFormatters, ICMPFormatMap } from './icmp/formatters'; -import { browserFormatters, BrowserFormatMap } from './browser/formatters'; -import { commonFormatters, CommonFormatMap } from './common/formatters'; +import { HTTPFormatMap, httpFormatters } from './http/formatters'; +import { TCPFormatMap, tcpFormatters } from './tcp/formatters'; +import { ICMPFormatMap, icmpFormatters } from './icmp/formatters'; +import { BrowserFormatMap, browserFormatters } from './browser/formatters'; +import { CommonFormatMap, commonFormatters } from './common/formatters'; type Formatters = HTTPFormatMap & TCPFormatMap & ICMPFormatMap & BrowserFormatMap & CommonFormatMap; -interface FormatterMap { - [DataStream.HTTP]: HTTPFormatMap; - [DataStream.ICMP]: ICMPFormatMap; - [DataStream.TCP]: TCPFormatMap; - [DataStream.BROWSER]: BrowserFormatMap; -} - -export const formattersMap: FormatterMap = { - [DataStream.HTTP]: httpFormatters, - [DataStream.ICMP]: icmpFormatters, - [DataStream.TCP]: tcpFormatters, - [DataStream.BROWSER]: browserFormatters, -}; - -export const formatters: Formatters = { +export const syntheticsPolicyFormatters: Formatters = { ...httpFormatters, ...icmpFormatters, ...tcpFormatters, @@ -39,6 +24,5 @@ export const formatters: Formatters = { /* Formats kibana space id into a valid Fleet-compliant datastream namespace */ export const formatKibanaNamespace = (spaceId: string) => { const namespaceRegExp = new RegExp(INVALID_NAMESPACE_CHARACTERS, 'g'); - const kibanaNamespace = spaceId.replace(namespaceRegExp, '_'); - return kibanaNamespace; + return spaceId.replace(namespaceRegExp, '_'); }; diff --git a/x-pack/plugins/synthetics/common/formatters/formatting_utils.test.ts b/x-pack/plugins/synthetics/common/formatters/formatting_utils.test.ts new file mode 100644 index 0000000000000..8c11753b66ab4 --- /dev/null +++ b/x-pack/plugins/synthetics/common/formatters/formatting_utils.test.ts @@ -0,0 +1,68 @@ +/* + * 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 { replaceStringWithParams } from './formatting_utils'; +import { loggerMock } from '@kbn/logging-mocks'; + +describe('replaceStringWithParams', () => { + const logger = loggerMock.create(); + + it('replaces params', () => { + const result = replaceStringWithParams( + '${homePageUrl}', + { homePageUrl: 'https://elastic.co' }, + logger + ); + + expect(result).toEqual('https://elastic.co'); + }); + + it('returns empty value in case no param', () => { + const result = replaceStringWithParams('${homePageUrl}', {}, logger); + + expect(result).toEqual(''); + }); + + it('works on objects', () => { + const result = replaceStringWithParams( + { key: 'Basic ${homePageUrl}' }, + { homePageUrl: 'https://elastic.co' }, + logger + ); + + expect(result).toEqual({ key: 'Basic https://elastic.co' }); + }); + + it('works on arrays', () => { + const result = replaceStringWithParams( + ['Basic ${homePageUrl}'], + { homePageUrl: 'https://elastic.co' }, + logger + ); + + expect(result).toEqual(['Basic https://elastic.co']); + }); + + it('works on multiple', () => { + const result = replaceStringWithParams( + 'Basic ${homePageUrl} ${homePageUrl1}', + { homePageUrl: 'https://elastic.co', homePageUrl1: 'https://elastic.co/product' }, + logger + ); + + expect(result).toEqual('Basic https://elastic.co https://elastic.co/product'); + }); + + it('works with default value', () => { + const result = replaceStringWithParams( + 'Basic ${homePageUrl:https://elastic.co} ${homePageUrl1}', + { homePageUrl1: 'https://elastic.co/product' }, + logger + ); + + expect(result).toEqual('Basic https://elastic.co https://elastic.co/product'); + }); +}); diff --git a/x-pack/plugins/synthetics/common/formatters/formatting_utils.ts b/x-pack/plugins/synthetics/common/formatters/formatting_utils.ts new file mode 100644 index 0000000000000..9013496b96eeb --- /dev/null +++ b/x-pack/plugins/synthetics/common/formatters/formatting_utils.ts @@ -0,0 +1,96 @@ +/* + * 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 { Logger } from '@kbn/logging'; +import { replaceVarsWithParams, ParsedVars } from './lightweight_param_formatter'; +import variableParser from './variable_parser'; +import { ConfigKey, MonitorFields } from '../runtime_types'; + +export type FormatterFn = (fields: Partial, key: ConfigKey) => string | null; + +export const arrayToJsonFormatter: FormatterFn = (fields, key) => { + const value = (fields[key] as string[]) ?? []; + return value.length ? JSON.stringify(value) : null; +}; + +export const objectToJsonFormatter: FormatterFn = (fields, fieldKey) => { + const value = (fields[fieldKey] as Record) ?? {}; + if (Object.keys(value).length === 0) return null; + + return JSON.stringify(value); +}; + +// only add tls settings if they are enabled by the user and isEnabled is true +export const tlsValueToYamlFormatter: FormatterFn = (fields, key) => { + if (fields[ConfigKey.METADATA]?.is_tls_enabled) { + const tlsValue = (fields[key] as string) ?? ''; + + return tlsValue ? JSON.stringify(tlsValue) : null; + } else { + return null; + } +}; + +export const tlsValueToStringFormatter: FormatterFn = (fields, key) => { + if (fields[ConfigKey.METADATA]?.is_tls_enabled) { + const tlsValue = (fields[key] as string) ?? ''; + + return tlsValue || null; + } else { + return null; + } +}; + +export const tlsArrayToYamlFormatter: FormatterFn = (fields, key) => { + if (fields[ConfigKey.METADATA]?.is_tls_enabled) { + const tlsValue = (fields[key] as string[]) ?? []; + + return tlsValue.length ? JSON.stringify(tlsValue) : null; + } else { + return null; + } +}; + +export const stringToJsonFormatter: FormatterFn = (fields, key) => { + const value = (fields[key] as string) ?? ''; + + return value ? JSON.stringify(value) : null; +}; + +export const replaceStringWithParams = ( + value: string | boolean | {} | [], + params: Record, + logger?: Logger +) => { + if (!value || typeof value === 'boolean') { + return value as string | null; + } + + try { + if (typeof value !== 'string') { + const strValue = JSON.stringify(value); + const parsedVars: ParsedVars = variableParser.parse(strValue); + + const parseValue = replaceVarsWithParams(parsedVars, params); + return JSON.parse(parseValue); + } + + const parsedVars: ParsedVars = variableParser.parse(value); + + return replaceVarsWithParams(parsedVars, params); + } catch (e) { + logger?.error(`error parsing vars for value ${JSON.stringify(value)}, ${e}`); + } + + return value as string | null; +}; + +export const secondsToCronFormatter: FormatterFn = (fields, key) => { + const value = (fields[key] as string) ?? ''; + + return value ? `${value}s` : null; +}; diff --git a/x-pack/plugins/synthetics/common/formatters/http/formatters.ts b/x-pack/plugins/synthetics/common/formatters/http/formatters.ts index 5eeb5888255dc..300e4f9fdb9ff 100644 --- a/x-pack/plugins/synthetics/common/formatters/http/formatters.ts +++ b/x-pack/plugins/synthetics/common/formatters/http/formatters.ts @@ -8,40 +8,31 @@ import { tlsFormatters } from '../tls/formatters'; import { HTTPFields, ConfigKey } from '../../runtime_types/monitor_management'; -import { - Formatter, - commonFormatters, - objectToJsonFormatter, - arrayToJsonFormatter, -} from '../common/formatters'; +import { Formatter, commonFormatters } from '../common/formatters'; +import { arrayToJsonFormatter, objectToJsonFormatter } from '../formatting_utils'; export type HTTPFormatMap = Record; export const httpFormatters: HTTPFormatMap = { - [ConfigKey.METADATA]: (fields) => objectToJsonFormatter(fields[ConfigKey.METADATA]), - [ConfigKey.URLS]: null, [ConfigKey.MAX_REDIRECTS]: null, + [ConfigKey.REQUEST_METHOD_CHECK]: null, + [ConfigKey.RESPONSE_BODY_INDEX]: null, + [ConfigKey.RESPONSE_HEADERS_INDEX]: null, + [ConfigKey.METADATA]: objectToJsonFormatter, + [ConfigKey.URLS]: null, [ConfigKey.USERNAME]: null, [ConfigKey.PASSWORD]: null, [ConfigKey.PROXY_URL]: null, - [ConfigKey.RESPONSE_BODY_CHECK_NEGATIVE]: (fields) => - arrayToJsonFormatter(fields[ConfigKey.RESPONSE_BODY_CHECK_NEGATIVE]), - [ConfigKey.RESPONSE_BODY_CHECK_POSITIVE]: (fields) => - arrayToJsonFormatter(fields[ConfigKey.RESPONSE_BODY_CHECK_POSITIVE]), - [ConfigKey.RESPONSE_BODY_INDEX]: null, - [ConfigKey.RESPONSE_HEADERS_CHECK]: (fields) => - objectToJsonFormatter(fields[ConfigKey.RESPONSE_HEADERS_CHECK]), - [ConfigKey.RESPONSE_HEADERS_INDEX]: null, - [ConfigKey.RESPONSE_STATUS_CHECK]: (fields) => - arrayToJsonFormatter(fields[ConfigKey.RESPONSE_STATUS_CHECK]), + [ConfigKey.PORT]: null, + [ConfigKey.RESPONSE_BODY_CHECK_NEGATIVE]: arrayToJsonFormatter, + [ConfigKey.RESPONSE_BODY_CHECK_POSITIVE]: arrayToJsonFormatter, + [ConfigKey.RESPONSE_HEADERS_CHECK]: objectToJsonFormatter, + [ConfigKey.RESPONSE_STATUS_CHECK]: arrayToJsonFormatter, + [ConfigKey.REQUEST_HEADERS_CHECK]: objectToJsonFormatter, [ConfigKey.REQUEST_BODY_CHECK]: (fields) => fields[ConfigKey.REQUEST_BODY_CHECK]?.value ? JSON.stringify(fields[ConfigKey.REQUEST_BODY_CHECK]?.value) : null, - [ConfigKey.REQUEST_HEADERS_CHECK]: (fields) => - objectToJsonFormatter(fields[ConfigKey.REQUEST_HEADERS_CHECK]), - [ConfigKey.REQUEST_METHOD_CHECK]: null, - [ConfigKey.PORT]: null, ...tlsFormatters, ...commonFormatters, }; diff --git a/x-pack/plugins/synthetics/common/formatters/icmp/formatters.ts b/x-pack/plugins/synthetics/common/formatters/icmp/formatters.ts index 720329d7a7d3a..0ebf69db5b408 100644 --- a/x-pack/plugins/synthetics/common/formatters/icmp/formatters.ts +++ b/x-pack/plugins/synthetics/common/formatters/icmp/formatters.ts @@ -5,14 +5,15 @@ * 2.0. */ +import { secondsToCronFormatter } from '../formatting_utils'; import { ICMPFields, ConfigKey } from '../../runtime_types/monitor_management'; -import { Formatter, commonFormatters, secondsToCronFormatter } from '../common/formatters'; +import { Formatter, commonFormatters } from '../common/formatters'; export type ICMPFormatMap = Record; export const icmpFormatters: ICMPFormatMap = { [ConfigKey.HOSTS]: null, - [ConfigKey.WAIT]: (fields) => secondsToCronFormatter(fields[ConfigKey.WAIT]), + [ConfigKey.WAIT]: secondsToCronFormatter, ...commonFormatters, }; diff --git a/x-pack/plugins/synthetics/common/formatters/lightweight_param_formatter.test.ts b/x-pack/plugins/synthetics/common/formatters/lightweight_param_formatter.test.ts new file mode 100644 index 0000000000000..facfd9d5e6536 --- /dev/null +++ b/x-pack/plugins/synthetics/common/formatters/lightweight_param_formatter.test.ts @@ -0,0 +1,165 @@ +/* + * 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 { ParsedVars, replaceVarsWithParams } from './lightweight_param_formatter'; +import variableParser from './variable_parser'; + +const params = { + splice: 'value', + reference: 'abc', + nested: 'value', + this: 'value', + HOME: '/user/shahzad', +}; + +describe('LightweightParamFormatter', () => { + it('should return null if no params are passed', () => { + const expected: ParsedVars = [ + { content: 'test ', type: 'nonvar' }, + { content: { default: null, name: 'splice' }, type: 'var' }, + { content: ' this', type: 'nonvar' }, + ]; + const formatter = variableParser.parse('test ${splice} this'); + expect(formatter).toEqual(expected); + + const result = replaceVarsWithParams(formatter, params); + expect(result).toEqual('test value this'); + }); + it('plain string', () => { + const expected: ParsedVars = [{ content: 'string', type: 'nonvar' }]; + const formatter = variableParser.parse('string'); + expect(formatter).toEqual(expected); + + const result = replaceVarsWithParams(formatter, params); + expect(result).toEqual('string'); + }); + it('string containing :', () => { + const expected: ParsedVars = [{ content: 'just:a:string', type: 'nonvar' }]; + const formatter = variableParser.parse('just:a:string'); + expect(formatter).toEqual(expected); + + const result = replaceVarsWithParams(formatter, params); + expect(result).toEqual('just:a:string'); + }); + it('string containing }', () => { + const expected: ParsedVars = [{ content: 'abc } def', type: 'nonvar' }]; + const formatter = variableParser.parse('abc } def'); + expect(formatter).toEqual(expected); + + const result = replaceVarsWithParams(formatter, params); + expect(result).toEqual('abc } def'); + }); + it('string containing regex with $', () => { + const expected: ParsedVars = [{ content: 'log$,|,l,e,g,$', type: 'nonvar' }]; + const formatter = variableParser.parse('log$|leg$'); + expect(formatter).toEqual(expected); + + const result = replaceVarsWithParams(formatter, params); + expect(result).toEqual('log$,|,l,e,g,$'); + }); + it('string with escaped var', () => { + const expected: ParsedVars = [{ content: 'escaped $,${var}', type: 'nonvar' }]; + const formatter = variableParser.parse('escaped $${var}'); + expect(formatter).toEqual(expected); + + const result = replaceVarsWithParams(formatter, params); + expect(result).toEqual('escaped $,${var}'); + }); + it('works with simple variable', () => { + const expected: ParsedVars = [{ content: { default: null, name: 'reference' }, type: 'var' }]; + const formatter = variableParser.parse('${reference}'); + expect(formatter).toEqual(expected); + + const result = replaceVarsWithParams(formatter, params); + expect(result).toEqual('abc'); + }); + it('exp at beginning', () => { + const formatter = variableParser.parse('${splice} test'); + expect(formatter).toEqual([ + { content: { default: null, name: 'splice' }, type: 'var' }, + { content: ' test', type: 'nonvar' }, + ]); + }); + it('exp at end', () => { + const expected: ParsedVars = [ + { content: 'test ', type: 'nonvar' }, + { content: { default: null, name: 'this' }, type: 'var' }, + ]; + + const formatter = variableParser.parse('test ${this}'); + expect(formatter).toEqual(expected); + + const result = replaceVarsWithParams(formatter, params); + expect(result).toEqual('test value'); + }); + + it('exp with default', () => { + const expected: ParsedVars = [{ content: { default: 'default', name: 'test' }, type: 'var' }]; + + const formatter = variableParser.parse('${test:default}'); + expect(formatter).toEqual(expected); + + const result = replaceVarsWithParams(formatter, params); + expect(result).toEqual('default'); + }); + + it('exp with default which has value', () => { + const expected: ParsedVars = [{ content: { default: 'default', name: 'splice' }, type: 'var' }]; + + const formatter = variableParser.parse('${splice:default}'); + expect(formatter).toEqual(expected); + + const result = replaceVarsWithParams(formatter, params); + expect(result).toEqual('value'); + }); + it('exp with default exp', () => { + const formatter = variableParser.parse('${test:the ${default} value}', params); + expect(formatter).toEqual([ + { + content: { + default: 'the ${default', + name: 'test', + }, + type: 'var', + }, + { + content: ' value}', + type: 'nonvar', + }, + ]); + }); + it('exp with default containing }', () => { + const formatter = variableParser.parse('${test:abc$}def}'); + expect(formatter).toEqual([ + { content: { default: 'abc$', name: 'test' }, type: 'var' }, + { content: 'def}', type: 'nonvar' }, + ]); + }); + it.skip('exp with default containing } escaped', () => { + const formatter = variableParser.parse('${test:abc$\\}def}', params); + expect(formatter).toEqual([ + { + content: { + default: 'abc}def', + name: 'test', + }, + type: 'var', + }, + ]); + }); + it('exp with default containing :', () => { + const expected: ParsedVars = [ + { content: { default: 'https://default:1234', name: 'test' }, type: 'var' }, + ]; + + const formatter = variableParser.parse('${test:https://default:1234}'); + expect(formatter).toEqual(expected); + + const result = replaceVarsWithParams(formatter, params); + expect(result).toEqual('https://default:1234'); + }); +}); diff --git a/x-pack/plugins/synthetics/common/formatters/lightweight_param_formatter.ts b/x-pack/plugins/synthetics/common/formatters/lightweight_param_formatter.ts new file mode 100644 index 0000000000000..e7ee00433cc87 --- /dev/null +++ b/x-pack/plugins/synthetics/common/formatters/lightweight_param_formatter.ts @@ -0,0 +1,22 @@ +/* + * 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 type ParsedVars = Array< + | { content: string; type: 'nonvar' } + | { content: { default: null | string; name: string }; type: 'var' } +>; + +export function replaceVarsWithParams(vars: ParsedVars, params: Record) { + return vars + .map((v) => { + if (v.type === 'nonvar') { + return v.content; + } + return params[v.content.name]?.trim() || v.content.default; + }) + .join(''); +} diff --git a/x-pack/plugins/synthetics/common/formatters/tcp/formatters.ts b/x-pack/plugins/synthetics/common/formatters/tcp/formatters.ts index 98c9a50bcd2fb..6acb9abe21877 100644 --- a/x-pack/plugins/synthetics/common/formatters/tcp/formatters.ts +++ b/x-pack/plugins/synthetics/common/formatters/tcp/formatters.ts @@ -7,18 +7,20 @@ import { TCPFields, ConfigKey } from '../../runtime_types/monitor_management'; -import { Formatter, commonFormatters, objectToJsonFormatter } from '../common/formatters'; +import { Formatter, commonFormatters } from '../common/formatters'; +import { objectToJsonFormatter } from '../formatting_utils'; import { tlsFormatters } from '../tls/formatters'; export type TCPFormatMap = Record; export const tcpFormatters: TCPFormatMap = { - [ConfigKey.METADATA]: (fields) => objectToJsonFormatter(fields[ConfigKey.METADATA]), + [ConfigKey.METADATA]: objectToJsonFormatter, [ConfigKey.HOSTS]: null, - [ConfigKey.PROXY_URL]: null, [ConfigKey.PROXY_USE_LOCAL_RESOLVER]: null, [ConfigKey.RESPONSE_RECEIVE_CHECK]: null, [ConfigKey.REQUEST_SEND_CHECK]: null, + [ConfigKey.PROXY_URL]: null, + [ConfigKey.PROXY_URL]: null, [ConfigKey.PORT]: null, [ConfigKey.URLS]: null, ...tlsFormatters, diff --git a/x-pack/plugins/synthetics/common/formatters/tls/formatters.ts b/x-pack/plugins/synthetics/common/formatters/tls/formatters.ts index 7d53a94c0af71..13b007795c875 100644 --- a/x-pack/plugins/synthetics/common/formatters/tls/formatters.ts +++ b/x-pack/plugins/synthetics/common/formatters/tls/formatters.ts @@ -4,43 +4,21 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { TLSFields, TLSVersion, ConfigKey } from '../../runtime_types/monitor_management'; +import { TLSFields, ConfigKey } from '../../runtime_types/monitor_management'; +import { + tlsArrayToYamlFormatter, + tlsValueToStringFormatter, + tlsValueToYamlFormatter, +} from '../formatting_utils'; import { Formatter } from '../common/formatters'; type TLSFormatMap = Record; export const tlsFormatters: TLSFormatMap = { - [ConfigKey.TLS_CERTIFICATE_AUTHORITIES]: (fields) => - fields[ConfigKey.METADATA]?.is_tls_enabled - ? tlsValueToYamlFormatter(fields[ConfigKey.TLS_CERTIFICATE_AUTHORITIES]) - : null, - [ConfigKey.TLS_CERTIFICATE]: (fields) => - fields[ConfigKey.METADATA]?.is_tls_enabled - ? tlsValueToYamlFormatter(fields[ConfigKey.TLS_CERTIFICATE]) - : null, - [ConfigKey.TLS_KEY]: (fields) => - fields[ConfigKey.METADATA]?.is_tls_enabled - ? tlsValueToYamlFormatter(fields[ConfigKey.TLS_KEY]) - : null, - [ConfigKey.TLS_KEY_PASSPHRASE]: (fields) => - fields[ConfigKey.METADATA]?.is_tls_enabled - ? tlsValueToStringFormatter(fields[ConfigKey.TLS_KEY_PASSPHRASE]) - : null, - [ConfigKey.TLS_VERIFICATION_MODE]: (fields) => - fields[ConfigKey.METADATA]?.is_tls_enabled - ? tlsValueToStringFormatter(fields[ConfigKey.TLS_VERIFICATION_MODE]) - : null, - [ConfigKey.TLS_VERSION]: (fields) => - fields[ConfigKey.METADATA]?.is_tls_enabled - ? tlsArrayToYamlFormatter(fields[ConfigKey.TLS_VERSION]) - : null, + [ConfigKey.TLS_CERTIFICATE_AUTHORITIES]: tlsValueToYamlFormatter, + [ConfigKey.TLS_CERTIFICATE]: tlsValueToYamlFormatter, + [ConfigKey.TLS_KEY]: tlsValueToYamlFormatter, + [ConfigKey.TLS_KEY_PASSPHRASE]: tlsValueToStringFormatter, + [ConfigKey.TLS_VERIFICATION_MODE]: tlsValueToStringFormatter, + [ConfigKey.TLS_VERSION]: tlsArrayToYamlFormatter, }; - -// only add tls settings if they are enabled by the user and isEnabled is true -export const tlsValueToYamlFormatter = (tlsValue: string | null = '') => - tlsValue ? JSON.stringify(tlsValue) : null; - -export const tlsValueToStringFormatter = (tlsValue: string | null = '') => tlsValue || null; - -export const tlsArrayToYamlFormatter = (tlsValue: TLSVersion[] | null = []) => - tlsValue?.length ? JSON.stringify(tlsValue) : null; diff --git a/x-pack/plugins/synthetics/common/formatters/variable_parser.js b/x-pack/plugins/synthetics/common/formatters/variable_parser.js new file mode 100644 index 0000000000000..04b4b26b1a713 --- /dev/null +++ b/x-pack/plugins/synthetics/common/formatters/variable_parser.js @@ -0,0 +1,903 @@ +/* + * 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. + */ + +/* + * Generated by PEG.js 0.10.0. + * + * http://pegjs.org/ + */ +function peg$subclass(child, parent) { + function ctor() { + this.constructor = child; + } + ctor.prototype = parent.prototype; + // eslint-disable-next-line new-cap + child.prototype = new ctor(); +} + +function pegSyntaxError(message, expected, found, location) { + this.message = message; + this.expected = expected; + this.found = found; + this.location = location; + this.name = 'SyntaxError'; + + if (typeof Error.captureStackTrace === 'function') { + Error.captureStackTrace(this, pegSyntaxError); + } +} + +peg$subclass(pegSyntaxError, Error); + +pegSyntaxError.buildMessage = function (expected, found) { + const DESCRIBE_EXPECTATION_FNS = { + literal: function (expectation) { + return '"' + literalEscape(expectation.text) + '"'; + }, + + class: function (expectation) { + let escapedParts = ''; + let i; + + for (i = 0; i < expectation.parts.length; i++) { + escapedParts += + expectation.parts[i] instanceof Array + ? classEscape(expectation.parts[i][0]) + '-' + classEscape(expectation.parts[i][1]) + : classEscape(expectation.parts[i]); + } + + return '[' + (expectation.inverted ? '^' : '') + escapedParts + ']'; + }, + + // eslint-disable-next-line no-unused-vars + any: function (expectation) { + return 'any character'; + }, + + // eslint-disable-next-line no-unused-vars + end: function (expectation) { + return 'end of input'; + }, + + other: function (expectation) { + return expectation.description; + }, + }; + + function hex(ch) { + return ch.charCodeAt(0).toString(16).toUpperCase(); + } + + function literalEscape(s) { + return s + .replace(/\\/g, '\\\\') + .replace(/"/g, '\\"') + .replace(/\0/g, '\\0') + .replace(/\t/g, '\\t') + .replace(/\n/g, '\\n') + .replace(/\r/g, '\\r') + .replace(/[\x00-\x0F]/g, function (ch) { + return '\\x0' + hex(ch); + }) + .replace(/[\x10-\x1F\x7F-\x9F]/g, function (ch) { + return '\\x' + hex(ch); + }); + } + + function classEscape(s) { + return s + .replace(/\\/g, '\\\\') + .replace(/\]/g, '\\]') + .replace(/\^/g, '\\^') + .replace(/-/g, '\\-') + .replace(/\0/g, '\\0') + .replace(/\t/g, '\\t') + .replace(/\n/g, '\\n') + .replace(/\r/g, '\\r') + .replace(/[\x00-\x0F]/g, function (ch) { + return '\\x0' + hex(ch); + }) + .replace(/[\x10-\x1F\x7F-\x9F]/g, function (ch) { + return '\\x' + hex(ch); + }); + } + + function describeExpectation(expectation) { + return DESCRIBE_EXPECTATION_FNS[expectation.type](expectation); + } + + function describeExpected(expected) { + const descriptions = new Array(expected.length); + let i; + let j; + + for (i = 0; i < expected.length; i++) { + descriptions[i] = describeExpectation(expected[i]); + } + + descriptions.sort(); + + if (descriptions.length > 0) { + for (i = 1, j = 1; i < descriptions.length; i++) { + if (descriptions[i - 1] !== descriptions[i]) { + descriptions[j] = descriptions[i]; + j++; + } + } + descriptions.length = j; + } + + switch (descriptions.length) { + case 1: + return descriptions[0]; + + case 2: + return descriptions[0] + ' or ' + descriptions[1]; + + default: + return ( + descriptions.slice(0, -1).join(', ') + ', or ' + descriptions[descriptions.length - 1] + ); + } + } + + function describeFound(found) { + return found ? '"' + literalEscape(found) + '"' : 'end of input'; + } + + return 'Expected ' + describeExpected(expected) + ' but ' + describeFound(found) + ' found.'; +}; + +function pegParse(input, options) { + options = options !== void 0 ? options : {}; + + const peg$FAILED = {}; + + const peg$startRuleFunctions = { Exps: peg$parseExps }; + let peg$startRuleFunction = peg$parseExps; + + const peg$c0 = function (e) { + return e.map((x) => x[0]); + }; + const peg$c1 = '${'; + const peg$c2 = peg$literalExpectation('${', false); + const peg$c3 = '}'; + const peg$c4 = peg$literalExpectation('}', false); + const peg$c5 = function (varCont) { + return { + type: 'var', + content: { + name: varCont[0][0].content, + default: varCont[0][1] ? varCont[0][1].content : null, + }, + }; + }; + const peg$c6 = function (varCont) { + return { type: 'varname', content: varCont.map((c) => c.char || c).join('') }; + }; + const peg$c7 = ':'; + const peg$c8 = peg$literalExpectation(':', false); + const peg$c9 = function (defCont) { + return { type: 'vardefault', content: defCont.join('') }; + }; + const peg$c10 = /^[^}:\\\r\n]/; + const peg$c11 = peg$classExpectation(['}', ':', '\\', '\r', '\n'], true, false); + const peg$c12 = '\\'; + const peg$c13 = peg$literalExpectation('\\', false); + const peg$c14 = function () { + return { type: 'char', char: '\\' }; + }; + const peg$c15 = function () { + return { type: 'char', char: '\x7d' }; + }; + const peg$c16 = function (sequence) { + return sequence.char; + }; + const peg$c17 = /^[^}\\\r\n]/; + const peg$c18 = peg$classExpectation(['}', '\\', '\r', '\n'], true, false); + const peg$c19 = function (nonVarCont) { + return { type: 'nonvar', content: nonVarCont.map((c) => c.char || c).join('') }; + }; + const peg$c20 = /^[^$]/; + const peg$c21 = peg$classExpectation(['$'], true, false); + const peg$c22 = '$'; + const peg$c23 = peg$literalExpectation('$', false); + const peg$c24 = /^[^{]/; + const peg$c25 = peg$classExpectation(['{'], true, false); + const peg$c26 = peg$otherExpectation('whitespace'); + const peg$c27 = /^[ \t\n\r]/; + const peg$c28 = peg$classExpectation([' ', '\t', '\n', '\r'], false, false); + + let peg$currPos = 0; + let peg$savedPos = 0; + const peg$posDetailsCache = [{ line: 1, column: 1 }]; + let peg$maxFailPos = 0; + let peg$maxFailExpected = []; + let peg$silentFails = 0; + + let peg$result; + + if ('startRule' in options) { + if (!(options.startRule in peg$startRuleFunctions)) { + throw new Error('Can\'t start parsing from rule "' + options.startRule + '".'); + } + + peg$startRuleFunction = peg$startRuleFunctions[options.startRule]; + } + + // eslint-disable-next-line no-unused-vars + function text() { + return input.substring(peg$savedPos, peg$currPos); + } + // eslint-disable-next-line no-unused-vars + function location() { + return peg$computeLocation(peg$savedPos, peg$currPos); + } + // eslint-disable-next-line no-unused-vars + function expected(description, location) { + location = location !== void 0 ? location : peg$computeLocation(peg$savedPos, peg$currPos); + + throw peg$buildStructuredError( + [peg$otherExpectation(description)], + input.substring(peg$savedPos, peg$currPos), + location + ); + } + // eslint-disable-next-line no-unused-vars + function error(message, location) { + location = location !== void 0 ? location : peg$computeLocation(peg$savedPos, peg$currPos); + + throw peg$buildSimpleError(message, location); + } + + function peg$literalExpectation(text, ignoreCase) { + return { type: 'literal', text: text, ignoreCase: ignoreCase }; + } + + function peg$classExpectation(parts, inverted, ignoreCase) { + return { type: 'class', parts: parts, inverted: inverted, ignoreCase: ignoreCase }; + } + + // eslint-disable-next-line no-unused-vars + function peg$anyExpectation() { + return { type: 'any' }; + } + + function peg$endExpectation() { + return { type: 'end' }; + } + + function peg$otherExpectation(description) { + return { type: 'other', description: description }; + } + + function peg$computePosDetails(pos) { + let details = peg$posDetailsCache[pos]; + let p; + + if (details) { + return details; + } else { + p = pos - 1; + while (!peg$posDetailsCache[p]) { + p--; + } + + details = peg$posDetailsCache[p]; + details = { + line: details.line, + column: details.column, + }; + + while (p < pos) { + if (input.charCodeAt(p) === 10) { + details.line++; + details.column = 1; + } else { + details.column++; + } + + p++; + } + + peg$posDetailsCache[pos] = details; + return details; + } + } + + function peg$computeLocation(startPos, endPos) { + const startPosDetails = peg$computePosDetails(startPos); + const endPosDetails = peg$computePosDetails(endPos); + + return { + start: { + offset: startPos, + line: startPosDetails.line, + column: startPosDetails.column, + }, + end: { + offset: endPos, + line: endPosDetails.line, + column: endPosDetails.column, + }, + }; + } + + function peg$fail(expected) { + if (peg$currPos < peg$maxFailPos) { + return; + } + + if (peg$currPos > peg$maxFailPos) { + peg$maxFailPos = peg$currPos; + peg$maxFailExpected = []; + } + + peg$maxFailExpected.push(expected); + } + + function peg$buildSimpleError(message, location) { + // eslint-disable-next-line new-cap + return new pegSyntaxError(message, null, null, location); + } + + function peg$buildStructuredError(expected, found, location) { + // eslint-disable-next-line new-cap + return new pegSyntaxError( + pegSyntaxError.buildMessage(expected, found), + expected, + found, + location + ); + } + + function peg$parseExps() { + let s0; + let s1; + let s2; + + s0 = peg$currPos; + s1 = []; + s2 = peg$parseExp(); + if (s2 !== peg$FAILED) { + while (s2 !== peg$FAILED) { + s1.push(s2); + s2 = peg$parseExp(); + } + } else { + s1 = peg$FAILED; + } + if (s1 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c0(s1); + } + s0 = s1; + + return s0; + } + + function peg$parseExp() { + let s0; + let s1; + + s0 = []; + s1 = peg$parseVar(); + if (s1 !== peg$FAILED) { + while (s1 !== peg$FAILED) { + s0.push(s1); + s1 = peg$parseVar(); + } + } else { + s0 = peg$FAILED; + } + if (s0 === peg$FAILED) { + s0 = []; + s1 = peg$parseNonVar(); + if (s1 !== peg$FAILED) { + while (s1 !== peg$FAILED) { + s0.push(s1); + s1 = peg$parseNonVar(); + } + } else { + s0 = peg$FAILED; + } + } + + return s0; + } + + function peg$parseVar() { + let s0; + let s1; + let s2; + let s3; + let s4; + let s5; + + s0 = peg$currPos; + if (input.substr(peg$currPos, 2) === peg$c1) { + s1 = peg$c1; + peg$currPos += 2; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { + peg$fail(peg$c2); + } + } + if (s1 !== peg$FAILED) { + s2 = peg$parse_(); + if (s2 !== peg$FAILED) { + s3 = []; + s4 = peg$parseVarInner(); + if (s4 !== peg$FAILED) { + while (s4 !== peg$FAILED) { + s3.push(s4); + s4 = peg$parseVarInner(); + } + } else { + s3 = peg$FAILED; + } + if (s3 !== peg$FAILED) { + s4 = peg$parse_(); + if (s4 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 125) { + s5 = peg$c3; + peg$currPos++; + } else { + s5 = peg$FAILED; + if (peg$silentFails === 0) { + peg$fail(peg$c4); + } + } + if (s5 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c5(s3); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parseVarInner() { + let s0; + let s1; + let s2; + + s0 = peg$currPos; + s1 = peg$parseVarName(); + if (s1 !== peg$FAILED) { + s2 = peg$parseVarDefault(); + if (s2 === peg$FAILED) { + s2 = null; + } + if (s2 !== peg$FAILED) { + s1 = [s1, s2]; + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parseVarName() { + let s0; + let s1; + let s2; + + s0 = peg$currPos; + s1 = []; + s2 = peg$parseVarNameChar(); + if (s2 !== peg$FAILED) { + while (s2 !== peg$FAILED) { + s1.push(s2); + s2 = peg$parseVarNameChar(); + } + } else { + s1 = peg$FAILED; + } + if (s1 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c6(s1); + } + s0 = s1; + + return s0; + } + + function peg$parseVarDefault() { + let s0; + let s1; + let s2; + let s3; + + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 58) { + s1 = peg$c7; + peg$currPos++; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { + peg$fail(peg$c8); + } + } + if (s1 !== peg$FAILED) { + s2 = []; + s3 = peg$parseVarDefaultChar(); + if (s3 !== peg$FAILED) { + while (s3 !== peg$FAILED) { + s2.push(s3); + s3 = peg$parseVarDefaultChar(); + } + } else { + s2 = peg$FAILED; + } + if (s2 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c9(s2); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parseVarNameChar() { + let s0; + let s1; + let s2; + let s3; + + if (peg$c10.test(input.charAt(peg$currPos))) { + s0 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s0 = peg$FAILED; + if (peg$silentFails === 0) { + peg$fail(peg$c11); + } + } + if (s0 === peg$FAILED) { + s0 = peg$currPos; + s1 = peg$parseEscape(); + if (s1 !== peg$FAILED) { + s2 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 92) { + s3 = peg$c12; + peg$currPos++; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { + peg$fail(peg$c13); + } + } + if (s3 !== peg$FAILED) { + peg$savedPos = s2; + s3 = peg$c14(); + } + s2 = s3; + if (s2 === peg$FAILED) { + s2 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 125) { + s3 = peg$c3; + peg$currPos++; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { + peg$fail(peg$c4); + } + } + if (s3 !== peg$FAILED) { + peg$savedPos = s2; + s3 = peg$c15(); + } + s2 = s3; + } + if (s2 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c16(s2); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } + + return s0; + } + + function peg$parseVarDefaultChar() { + let s0; + let s1; + let s2; + let s3; + + if (peg$c17.test(input.charAt(peg$currPos))) { + s0 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s0 = peg$FAILED; + if (peg$silentFails === 0) { + peg$fail(peg$c18); + } + } + if (s0 === peg$FAILED) { + s0 = peg$currPos; + s1 = peg$parseEscape(); + if (s1 !== peg$FAILED) { + s2 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 92) { + s3 = peg$c12; + peg$currPos++; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { + peg$fail(peg$c13); + } + } + if (s3 !== peg$FAILED) { + peg$savedPos = s2; + s3 = peg$c14(); + } + s2 = s3; + if (s2 === peg$FAILED) { + s2 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 125) { + s3 = peg$c3; + peg$currPos++; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { + peg$fail(peg$c4); + } + } + if (s3 !== peg$FAILED) { + peg$savedPos = s2; + s3 = peg$c15(); + } + s2 = s3; + } + if (s2 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c16(s2); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } + + return s0; + } + + function peg$parseNonVar() { + let s0; + let s1; + let s2; + + s0 = peg$currPos; + s1 = []; + s2 = peg$parseNonVarCont(); + if (s2 !== peg$FAILED) { + while (s2 !== peg$FAILED) { + s1.push(s2); + s2 = peg$parseNonVarCont(); + } + } else { + s1 = peg$FAILED; + } + if (s1 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c19(s1); + } + s0 = s1; + + return s0; + } + + function peg$parseNonVarCont() { + let s0; + + s0 = peg$parseDollarNotVarStart(); + if (s0 === peg$FAILED) { + s0 = peg$parseNotDollar(); + } + + return s0; + } + + function peg$parseNotDollar() { + let s0; + + if (peg$c20.test(input.charAt(peg$currPos))) { + s0 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s0 = peg$FAILED; + if (peg$silentFails === 0) { + peg$fail(peg$c21); + } + } + + return s0; + } + + function peg$parseDollarNotVarStart() { + let s0; + let s1; + let s2; + let s3; + + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 36) { + s1 = peg$c22; + peg$currPos++; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { + peg$fail(peg$c23); + } + } + if (s1 !== peg$FAILED) { + s2 = []; + if (peg$c24.test(input.charAt(peg$currPos))) { + s3 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { + peg$fail(peg$c25); + } + } + if (s3 !== peg$FAILED) { + while (s3 !== peg$FAILED) { + s2.push(s3); + if (peg$c24.test(input.charAt(peg$currPos))) { + s3 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { + peg$fail(peg$c25); + } + } + } + } else { + s2 = peg$FAILED; + } + if (s2 !== peg$FAILED) { + s1 = [s1, s2]; + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parse_() { + let s0; + let s1; + + peg$silentFails++; + // eslint-disable-next-line prefer-const + s0 = []; + if (peg$c27.test(input.charAt(peg$currPos))) { + s1 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { + peg$fail(peg$c28); + } + } + while (s1 !== peg$FAILED) { + s0.push(s1); + if (peg$c27.test(input.charAt(peg$currPos))) { + s1 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { + peg$fail(peg$c28); + } + } + } + peg$silentFails--; + if (s0 === peg$FAILED) { + s1 = peg$FAILED; + if (peg$silentFails === 0) { + peg$fail(peg$c26); + } + } + + return s0; + } + + function peg$parseEscape() { + let s0; + + if (input.charCodeAt(peg$currPos) === 92) { + s0 = peg$c12; + peg$currPos++; + } else { + s0 = peg$FAILED; + if (peg$silentFails === 0) { + peg$fail(peg$c13); + } + } + + return s0; + } + + // eslint-disable-next-line prefer-const + peg$result = peg$startRuleFunction(); + + if (peg$result !== peg$FAILED && peg$currPos === input.length) { + return peg$result; + } else { + if (peg$result !== peg$FAILED && peg$currPos < input.length) { + peg$fail(peg$endExpectation()); + } + + throw peg$buildStructuredError( + peg$maxFailExpected, + peg$maxFailPos < input.length ? input.charAt(peg$maxFailPos) : null, + peg$maxFailPos < input.length + ? peg$computeLocation(peg$maxFailPos, peg$maxFailPos + 1) + : peg$computeLocation(peg$maxFailPos, peg$maxFailPos) + ); + } +} + +export default { + SyntaxError: pegSyntaxError, + parse: pegParse, +}; diff --git a/x-pack/plugins/synthetics/e2e/journeys/synthetics/alert_rules/default_status_alert.journey.ts b/x-pack/plugins/synthetics/e2e/journeys/synthetics/alert_rules/default_status_alert.journey.ts index 666366b9555e4..ae8ffeed83431 100644 --- a/x-pack/plugins/synthetics/e2e/journeys/synthetics/alert_rules/default_status_alert.journey.ts +++ b/x-pack/plugins/synthetics/e2e/journeys/synthetics/alert_rules/default_status_alert.journey.ts @@ -58,8 +58,6 @@ journey(`DefaultStatusAlert`, async ({ page, params }) => { await page.isDisabled(byTestId('xpack.synthetics.toggleAlertFlyout')); await page.click(byTestId('xpack.synthetics.toggleAlertFlyout')); await page.waitForSelector('text=Edit rule'); - await page.selectOption(byTestId('intervalInputUnit'), { label: 'second' }); - await page.fill(byTestId('intervalInput'), '20'); await page.click(byTestId('saveEditedRuleButton')); await page.waitForSelector("text=Updated 'Synthetics internal alert'"); }); @@ -94,6 +92,8 @@ journey(`DefaultStatusAlert`, async ({ page, params }) => { await page.click(byTestId('syntheticsMonitorManagementTab')); await page.click(byTestId('syntheticsMonitorOverviewTab')); + await page.waitForTimeout(5 * 1000); + const totalDown = await page.textContent( byTestId('xpack.uptime.synthetics.overview.status.down') ); diff --git a/x-pack/plugins/synthetics/kibana.jsonc b/x-pack/plugins/synthetics/kibana.jsonc index 3236a730f1a59..036ee19d52ffc 100644 --- a/x-pack/plugins/synthetics/kibana.jsonc +++ b/x-pack/plugins/synthetics/kibana.jsonc @@ -30,7 +30,6 @@ "triggersActionsUi", "usageCollection", "unifiedSearch", - "spaces", "bfetch" ], "optionalPlugins": [ @@ -39,6 +38,7 @@ "fleet", "home", "ml", + "spaces", "telemetry" ], "requiredBundles": [ @@ -49,6 +49,7 @@ "kibanaUtils", "ml", "observability", + "spaces", "indexLifecycleManagement" ] } diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/alerts/hooks/use_synthetics_alert.ts b/x-pack/plugins/synthetics/public/apps/synthetics/components/alerts/hooks/use_synthetics_alert.ts index 3b1ec20345ae4..2f7dd459bc927 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/alerts/hooks/use_synthetics_alert.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/alerts/hooks/use_synthetics_alert.ts @@ -39,6 +39,7 @@ export const useSyntheticsAlert = (isOpen: boolean) => { } return triggersActionsUi.getEditRuleFlyout({ onClose: () => dispatch(setAlertFlyoutVisible(false)), + hideInterval: true, initialRule: alert, }); }, [alert, dispatch, triggersActionsUi]); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/screenshot/journey_screenshot_dialog.test.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/screenshot/journey_screenshot_dialog.test.tsx index eb180b8e362e1..44e6db576d3fb 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/screenshot/journey_screenshot_dialog.test.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/screenshot/journey_screenshot_dialog.test.tsx @@ -56,12 +56,11 @@ describe('JourneyScreenshotDialog', () => { expect(() => render()).not.toThrowError(); }); - it('shows loading indicator when image is loading', () => { + it('shows loading indicator when image is loading', async () => { const { queryByTestId } = render(); expect(queryByTestId('screenshotImageLoadingProgress')).not.toBeInTheDocument(); userEvent.click(queryByTestId('screenshotImageNextButton')); - expect(queryByTestId('screenshotImageLoadingProgress')).toBeInTheDocument(); }); it('respects maxSteps', () => { @@ -69,7 +68,6 @@ describe('JourneyScreenshotDialog', () => { expect(queryByTestId('screenshotImageLoadingProgress')).not.toBeInTheDocument(); userEvent.click(queryByTestId('screenshotImageNextButton')); - expect(queryByTestId('screenshotImageLoadingProgress')).toBeInTheDocument(); expect(queryByTestId('screenshotImageNextButton')).toHaveProperty('disabled'); }); @@ -79,6 +77,6 @@ describe('JourneyScreenshotDialog', () => { 'src', 'http://localhost/test-img-url-1' ); - expect(getByText('First step')).toBeInTheDocument(); + expect(getByText('Step: 1 of 1')).toBeInTheDocument(); }); }); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/screenshot/journey_screenshot_dialog.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/screenshot/journey_screenshot_dialog.tsx index dd3607e8221f5..818541b1f73ea 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/screenshot/journey_screenshot_dialog.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/screenshot/journey_screenshot_dialog.tsx @@ -28,6 +28,7 @@ import { useIsWithinMaxBreakpoint, } from '@elastic/eui'; +import { SYNTHETICS_API_URLS } from '../../../../../../common/constants'; import { SyntheticsSettingsContext } from '../../../contexts'; import { useRetrieveStepImage } from '../monitor_test_result/use_retrieve_step_image'; @@ -54,7 +55,7 @@ export const JourneyScreenshotDialog = ({ const [stepNumber, setStepNumber] = useState(initialStepNumber); const { basePath } = useContext(SyntheticsSettingsContext); - const imgPath = `${basePath}/internal/uptime/journey/screenshot/${checkGroup}/${stepNumber}`; + const imgPath = getScreenshotUrl({ basePath, checkGroup, stepNumber }); const imageResult = useRetrieveStepImage({ hasIntersected: true, @@ -205,6 +206,24 @@ export const JourneyScreenshotDialog = ({ ) : null; }; +export const getScreenshotUrl = ({ + basePath, + checkGroup, + stepNumber, +}: { + basePath: string; + checkGroup?: string; + stepNumber: number; +}) => { + if (!checkGroup) { + return ''; + } + return `${basePath}${SYNTHETICS_API_URLS.JOURNEY_SCREENSHOT.replace( + '{checkGroup}', + checkGroup + ).replace('{stepIndex}', stepNumber.toString())}`; +}; + export const formatScreenshotStepsCount = (stepNumber: number, totalSteps: number) => i18n.translate('xpack.synthetics.monitor.stepOfSteps', { defaultMessage: 'Step: {stepNumber} of {totalSteps}', diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/screenshot/journey_step_screenshot_container.test.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/screenshot/journey_step_screenshot_container.test.tsx index fc194af26038c..9f5e313b78dd3 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/screenshot/journey_step_screenshot_container.test.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/screenshot/journey_step_screenshot_container.test.tsx @@ -11,13 +11,14 @@ import { JourneyStepScreenshotContainer } from './journey_step_screenshot_contai import { render } from '../../../utils/testing'; import * as observabilityPublic from '@kbn/observability-plugin/public'; import * as retrieveHooks from '../monitor_test_result/use_retrieve_step_image'; +import { getScreenshotUrl } from './journey_screenshot_dialog'; jest.mock('@kbn/observability-plugin/public'); jest.setTimeout(10 * 1000); -const imgPath1 = '/internal/uptime/journey/screenshot/test-check-group/1'; -const imgPath2 = '/internal/uptime/journey/screenshot/test-check-group/2'; +const imgPath1 = getScreenshotUrl({ basePath: '', checkGroup: 'test-check-group', stepNumber: 1 }); +const imgPath2 = getScreenshotUrl({ basePath: '', checkGroup: 'test-check-group', stepNumber: 2 }); const testImageDataResult = { [imgPath1]: { attempts: 1, diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/screenshot/journey_step_screenshot_container.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/screenshot/journey_step_screenshot_container.tsx index e7f7d05e1fcf4..f4fd7470545e7 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/screenshot/journey_step_screenshot_container.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/screenshot/journey_step_screenshot_container.tsx @@ -8,6 +8,7 @@ import React, { useContext } from 'react'; import useIntersection from 'react-use/lib/useIntersection'; +import { getScreenshotUrl } from './journey_screenshot_dialog'; import { SyntheticsSettingsContext } from '../../../contexts'; import { useRetrieveStepImage } from '../monitor_test_result/use_retrieve_step_image'; @@ -42,7 +43,7 @@ export const JourneyStepScreenshotContainer = ({ const { basePath } = useContext(SyntheticsSettingsContext); const imgPath = checkGroup - ? `${basePath}/internal/uptime/journey/screenshot/${checkGroup}/${initialStepNumber}` + ? getScreenshotUrl({ basePath, checkGroup, stepNumber: initialStepNumber }) : ''; const intersection = useIntersection(intersectionRef, { diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_synthetics_priviliges.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_synthetics_priviliges.tsx new file mode 100644 index 0000000000000..b1f1d57a33fd7 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_synthetics_priviliges.tsx @@ -0,0 +1,79 @@ +/* + * 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 { useSelector } from 'react-redux'; +import React from 'react'; +import { + EuiEmptyPrompt, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiMarkdownFormat, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { css } from '@emotion/react'; +import { i18n } from '@kbn/i18n'; +import { selectOverviewStatus } from '../state/overview_status'; +import { SYNTHETICS_INDEX_PATTERN } from '../../../../common/constants'; + +export const useSyntheticsPrivileges = () => { + const { error } = useSelector(selectOverviewStatus); + + if (error?.body?.message?.startsWith('MissingIndicesPrivileges:')) { + return ( + + + + + + ); + } +}; + +const Unprivileged = ({ unprivilegedIndices }: { unprivilegedIndices: string[] }) => ( + } + title={ +

+ +

+ } + body={ +

+ +

+ } + footer={ + `\n- \`${idx}\``) + } + /> + } + /> +); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/routes.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/routes.tsx index 413680f14d602..d60c4c25d95a1 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/routes.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/routes.tsx @@ -18,6 +18,7 @@ import { APP_WRAPPER_CLASS } from '@kbn/core/public'; import { useKibana } from '@kbn/kibana-react-plugin/public'; import type { LazyObservabilityPageTemplateProps } from '@kbn/observability-plugin/public'; import { useInspectorContext } from '@kbn/observability-plugin/public'; +import { useSyntheticsPrivileges } from './hooks/use_synthetics_priviliges'; import { ClientPluginsStart } from '../../plugin'; import { getMonitorsRoute } from './components/monitors_page/route_config'; import { getMonitorDetailsRoute } from './components/monitor_details/route_config'; @@ -192,6 +193,8 @@ export const PageRouter: FC = () => { apiService.addInspectorRequest = addInspectorRequest; + const isUnPrivileged = useSyntheticsPrivileges(); + return ( {routes.map( @@ -207,12 +210,12 @@ export const PageRouter: FC = () => {
- + {isUnPrivileged || }
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/browser_journey/api.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/browser_journey/api.ts index 33ed12db4b5d3..b4b352d388768 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/state/browser_journey/api.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/browser_journey/api.ts @@ -17,7 +17,7 @@ import { Ping, PingType, } from '../../../../../common/runtime_types'; -import { API_URLS, SYNTHETICS_API_URLS } from '../../../../../common/constants'; +import { SYNTHETICS_API_URLS } from '../../../../../common/constants'; export interface FetchJourneyStepsParams { checkGroup: string; @@ -25,7 +25,7 @@ export interface FetchJourneyStepsParams { } export async function fetchScreenshotBlockSet(params: string[]): Promise { - return apiService.post(API_URLS.JOURNEY_SCREENSHOT_BLOCKS, { + return apiService.post(SYNTHETICS_API_URLS.JOURNEY_SCREENSHOT_BLOCKS, { hashes: params, }); } @@ -45,7 +45,11 @@ export async function fetchJourneysFailedSteps({ }: { checkGroups: string[]; }): Promise { - return apiService.get(API_URLS.JOURNEY_FAILED_STEPS, { checkGroups }, FailedStepsApiResponseType); + return apiService.get( + SYNTHETICS_API_URLS.JOURNEY_FAILED_STEPS, + { checkGroups }, + FailedStepsApiResponseType + ); } export async function fetchLastSuccessfulCheck({ @@ -60,7 +64,7 @@ export async function fetchLastSuccessfulCheck({ location?: string; }): Promise { return await apiService.get( - API_URLS.SYNTHETICS_SUCCESSFUL_CHECK, + SYNTHETICS_API_URLS.SYNTHETICS_SUCCESSFUL_CHECK, { monitorId, timestamp, diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/network_events/api.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/network_events/api.ts index 8c52ebba47dad..7bc509aaacb03 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/state/network_events/api.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/network_events/api.ts @@ -9,7 +9,7 @@ import { SyntheticsNetworkEventsApiResponse, SyntheticsNetworkEventsApiResponseType, } from '../../../../../common/runtime_types'; -import { API_URLS } from '../../../../../common/constants'; +import { SYNTHETICS_API_URLS } from '../../../../../common/constants'; import { apiService } from '../../../../utils/api_service'; import { FetchNetworkEventsParams } from './actions'; @@ -17,7 +17,7 @@ export async function fetchNetworkEvents( params: FetchNetworkEventsParams ): Promise { return (await apiService.get( - API_URLS.NETWORK_EVENTS, + SYNTHETICS_API_URLS.NETWORK_EVENTS, { checkGroup: params.checkGroup, stepIndex: params.stepIndex, diff --git a/x-pack/plugins/synthetics/public/hooks/use_kibana_space.tsx b/x-pack/plugins/synthetics/public/hooks/use_kibana_space.tsx index 1a4b8e31e1228..bdb87133441d5 100644 --- a/x-pack/plugins/synthetics/public/hooks/use_kibana_space.tsx +++ b/x-pack/plugins/synthetics/public/hooks/use_kibana_space.tsx @@ -7,6 +7,7 @@ import type { Space } from '@kbn/spaces-plugin/common'; import { useKibana } from '@kbn/kibana-react-plugin/public'; import { useFetcher } from '@kbn/observability-plugin/public'; +import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common'; import { ClientPluginsStart } from '../plugin'; export const useKibanaSpace = () => { @@ -17,7 +18,7 @@ export const useKibanaSpace = () => { loading, error, } = useFetcher>(() => { - return services.spaces?.getActiveSpace(); + return services.spaces?.getActiveSpace() ?? Promise.resolve({ id: DEFAULT_SPACE_ID } as Space); }, [services.spaces]); return { diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/app/uptime_page_template.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/app/uptime_page_template.tsx index 9e3d0d6ecd384..c1003f969586c 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/app/uptime_page_template.tsx +++ b/x-pack/plugins/synthetics/public/legacy_uptime/app/uptime_page_template.tsx @@ -9,7 +9,7 @@ import React, { useEffect } from 'react'; import { EuiPageHeaderProps, EuiPageTemplateProps } from '@elastic/eui'; import { useKibana } from '@kbn/kibana-react-plugin/public'; import { useInspectorContext } from '@kbn/observability-plugin/public'; -import { CERTIFICATES_ROUTE, OVERVIEW_ROUTE } from '../../../common/constants'; +import { CERTIFICATES_ROUTE, OVERVIEW_ROUTE, SETTINGS_ROUTE } from '../../../common/constants'; import { ClientPluginsStart } from '../../plugin'; import { useNoDataConfig } from './use_no_data_config'; import { EmptyStateLoading } from '../components/overview/empty_state/empty_state_loading'; @@ -42,7 +42,7 @@ export const UptimePageTemplateComponent: React.FC inspectorAdapters.requests.reset(); }, [inspectorAdapters.requests]); - if (error) { + if (error && path !== SETTINGS_ROUTE) { return ; } diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/components/common/header/action_menu_content.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/components/common/header/action_menu_content.tsx index a0c7f68d6b67a..ad5551dfc00bf 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/components/common/header/action_menu_content.tsx +++ b/x-pack/plugins/synthetics/public/legacy_uptime/components/common/header/action_menu_content.tsx @@ -52,7 +52,7 @@ export function ActionMenuContent(): React.ReactElement { reportType: 'kpi-over-time', allSeries: [ { - dataType: 'synthetics', + dataType: 'uptime', seriesType: 'area', selectedMetricField: 'monitor.duration.us', time: { from: dateRangeStart, to: dateRangeEnd }, diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor/monitor_duration/monitor_duration.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor/monitor_duration/monitor_duration.tsx index 822a476f5225a..562cfd8030095 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor/monitor_duration/monitor_duration.tsx +++ b/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor/monitor_duration/monitor_duration.tsx @@ -18,7 +18,6 @@ interface DurationChartProps { hasMLJob: boolean; anomalies: AnomalyRecords | null; locationDurationLines: LocationDurationLine[]; - exploratoryViewLink: string; } /** diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor/monitor_duration/monitor_duration_container.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor/monitor_duration/monitor_duration_container.tsx index 32bf47451f957..a6fce56d9269e 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor/monitor_duration/monitor_duration_container.tsx +++ b/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor/monitor_duration/monitor_duration_container.tsx @@ -8,7 +8,6 @@ import React, { useContext, useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { JobStat } from '@kbn/ml-plugin/public'; -import { createExploratoryViewUrl } from '@kbn/observability-plugin/public'; import { useGetUrlParams } from '../../../hooks'; import { getAnomalyRecordsAction, @@ -25,7 +24,6 @@ import { UptimeRefreshContext } from '../../../contexts'; import { MonitorDurationComponent } from './monitor_duration'; import { MonitorIdParam } from '../../../../../common/types'; import { getMLJobId } from '../../../../../common/lib'; -import { useUptimeSettingsContext } from '../../../contexts/uptime_settings_context'; export const MonitorDuration: React.FC = ({ monitorId }) => { const { dateRangeStart, dateRangeEnd, absoluteDateRangeStart, absoluteDateRangeEnd } = @@ -47,27 +45,6 @@ export const MonitorDuration: React.FC = ({ monitorId }) => { const { lastRefresh } = useContext(UptimeRefreshContext); - const { basePath } = useUptimeSettingsContext(); - - const exploratoryViewLink = createExploratoryViewUrl( - { - reportType: 'kpi-over-time', - allSeries: [ - { - name: `${monitorId}-response-duration`, - time: { from: dateRangeStart, to: dateRangeEnd }, - reportDefinitions: { - 'monitor.id': [monitorId] as string[], - }, - breakdown: 'observer.geo.name', - operationType: 'average', - dataType: 'synthetics', - }, - ], - }, - basePath - ); - useEffect(() => { if (isMLAvailable && hasMLJob) { const anomalyParams = { @@ -96,7 +73,6 @@ export const MonitorDuration: React.FC = ({ monitorId }) => { anomalies={anomalies} hasMLJob={hasMLJob} loading={loading || jobsLoading} - exploratoryViewLink={exploratoryViewLink} locationDurationLines={durationLines?.locationDurationLines ?? []} /> ); diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor/synthetics/waterfall/components/waterfall_marker_trend.test.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor/synthetics/waterfall/components/waterfall_marker_trend.test.tsx index aeca7940ef773..0813108d1d1fa 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor/synthetics/waterfall/components/waterfall_marker_trend.test.tsx +++ b/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor/synthetics/waterfall/components/waterfall_marker_trend.test.tsx @@ -98,7 +98,7 @@ describe('', () => { selectedMetricField: 'field', time: { to: '2021-12-03T14:35:41.072Z', from: '2021-12-03T13:47:41.072Z' }, seriesType: 'area', - dataType: 'synthetics', + dataType: 'uptime', reportDefinitions: { 'monitor.name': [null], 'synthetics.step.name.keyword': ['test-name'], diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/components/synthetics/check_steps/step_field_trend.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/components/synthetics/check_steps/step_field_trend.tsx index f337a9ba0bed5..4968897941e77 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/components/synthetics/check_steps/step_field_trend.tsx +++ b/x-pack/plugins/synthetics/public/legacy_uptime/components/synthetics/check_steps/step_field_trend.tsx @@ -51,7 +51,7 @@ export function StepFieldTrend({ selectedMetricField: field, time: getLast48Intervals(activeStep), seriesType: 'area', - dataType: 'synthetics', + dataType: 'uptime', reportDefinitions: { 'monitor.name': [activeStep.monitor.name!], 'synthetics.step.name.keyword': [activeStep.synthetics.step?.name!], diff --git a/x-pack/plugins/synthetics/public/plugin.ts b/x-pack/plugins/synthetics/public/plugin.ts index 46ecafb97559f..77de03480be75 100644 --- a/x-pack/plugins/synthetics/public/plugin.ts +++ b/x-pack/plugins/synthetics/public/plugin.ts @@ -80,7 +80,7 @@ export interface ClientPluginsStart { triggersActionsUi: TriggersAndActionsUIPublicPluginStart; cases: CasesUiStart; dataViews: DataViewsPublicPluginStart; - spaces: SpacesPluginStart; + spaces?: SpacesPluginStart; cloud?: CloudStart; appName: string; storage: IStorageWrapper; @@ -132,7 +132,7 @@ export class UptimePlugin plugins.share.url.locators.create(editMonitorNavigatorParams); plugins.observability.dashboard.register({ - appName: 'synthetics', + appName: 'uptime', hasData: async () => { const dataHelper = await getUptimeDataHelper(); const status = await dataHelper.indexStatus(); diff --git a/x-pack/plugins/synthetics/server/legacy_uptime/lib/adapters/framework/adapter_types.ts b/x-pack/plugins/synthetics/server/legacy_uptime/lib/adapters/framework/adapter_types.ts index ec77b83977a09..ba1779d1c69f1 100644 --- a/x-pack/plugins/synthetics/server/legacy_uptime/lib/adapters/framework/adapter_types.ts +++ b/x-pack/plugins/synthetics/server/legacy_uptime/lib/adapters/framework/adapter_types.ts @@ -54,7 +54,7 @@ export interface UptimeServerSetup { router: UptimeRouter; config: UptimeConfig; cloud?: CloudSetup; - spaces: SpacesPluginStart; + spaces?: SpacesPluginStart; fleet: FleetStartContract; security: SecurityPluginStart; savedObjectsClient?: SavedObjectsClientContract; @@ -89,5 +89,5 @@ export interface UptimeCorePluginsStart { encryptedSavedObjects: EncryptedSavedObjectsPluginStart; taskManager: TaskManagerStartContract; telemetry: TelemetryPluginStart; - spaces: SpacesPluginStart; + spaces?: SpacesPluginStart; } diff --git a/x-pack/plugins/synthetics/server/legacy_uptime/lib/lib.ts b/x-pack/plugins/synthetics/server/legacy_uptime/lib/lib.ts index c77f75dae0eaa..cf76c5abc978b 100644 --- a/x-pack/plugins/synthetics/server/legacy_uptime/lib/lib.ts +++ b/x-pack/plugins/synthetics/server/legacy_uptime/lib/lib.ts @@ -51,7 +51,7 @@ export class UptimeEsClient { request?: KibanaRequest; baseESClient: ElasticsearchClient; heartbeatIndices: string; - isInspectorEnabled: boolean | undefined; + isInspectorEnabled?: Promise; inspectableEsQueries: InspectResponse = []; uiSettings?: CoreRequestHandlerContext['uiSettings']; savedObjectsClient: SavedObjectsClientContract; @@ -59,31 +59,28 @@ export class UptimeEsClient { constructor( savedObjectsClient: SavedObjectsClientContract, esClient: ElasticsearchClient, - isDev: boolean = false, - uiSettings?: CoreRequestHandlerContext['uiSettings'], - request?: KibanaRequest + options?: { + isDev?: boolean; + uiSettings?: CoreRequestHandlerContext['uiSettings']; + request?: KibanaRequest; + heartbeatIndices?: string; + } ) { + const { isDev = false, uiSettings, request, heartbeatIndices = '' } = options ?? {}; this.uiSettings = uiSettings; this.baseESClient = esClient; - this.isInspectorEnabled = undefined; this.savedObjectsClient = savedObjectsClient; this.request = request; - this.heartbeatIndices = ''; + this.heartbeatIndices = heartbeatIndices; this.isDev = isDev; this.inspectableEsQueries = []; + this.getInspectEnabled(); } async initSettings() { const self = this; - if (!self.heartbeatIndices) { - const [isInspectorEnabled, dynamicSettings] = await Promise.all([ - getInspectEnabled(self.uiSettings), - savedObjectsAdapter.getUptimeDynamicSettings(self.savedObjectsClient), - ]); - - self.heartbeatIndices = dynamicSettings?.heartbeatIndices || ''; - self.isInspectorEnabled = isInspectorEnabled; - } + const heartbeatIndices = await this.getIndices(); + self.heartbeatIndices = heartbeatIndices || ''; } async search( @@ -123,8 +120,8 @@ export class UptimeEsClient { }) ); } - - if (this.isInspectorEnabled && this.request) { + const isInspectorEnabled = await this.getInspectEnabled(); + if (isInspectorEnabled && this.request) { debugESCall({ startTime, request: this.request, @@ -155,7 +152,9 @@ export class UptimeEsClient { esError = e; } - if (this.isInspectorEnabled && this.request) { + const isInspectorEnabled = await this.getInspectEnabled(); + + if (isInspectorEnabled && this.request) { debugESCall({ startTime, request: this.request, @@ -175,27 +174,39 @@ export class UptimeEsClient { return this.savedObjectsClient; } - getInspectData(path: string) { - const isInspectorEnabled = - (this.isInspectorEnabled || this.isDev) && path !== API_URLS.DYNAMIC_SETTINGS; + async getInspectData(path: string) { + const isInspectorEnabled = await this.getInspectEnabled(); + const showInspectData = + (isInspectorEnabled || this.isDev) && path !== API_URLS.DYNAMIC_SETTINGS; - if (isInspectorEnabled) { + if (showInspectData) { return { _inspect: this.inspectableEsQueries }; } return {}; } -} + async getInspectEnabled() { + if (this.isInspectorEnabled !== undefined) { + return this.isInspectorEnabled; + } -export function createEsParams(params: T): T { - return params; -} + if (!this.uiSettings) { + return false; + } + + this.isInspectorEnabled = this.uiSettings.client.get(enableInspectEsQueries); + } -function getInspectEnabled(uiSettings?: CoreRequestHandlerContext['uiSettings']) { - if (!uiSettings) { - return false; + async getIndices() { + if (this.heartbeatIndices) { + return this.heartbeatIndices; + } + const settings = await savedObjectsAdapter.getUptimeDynamicSettings(this.savedObjectsClient); + return settings?.heartbeatIndices || ''; } +} - return uiSettings.client.get(enableInspectEsQueries); +export function createEsParams(params: T): T { + return params; } /* eslint-disable no-console */ diff --git a/x-pack/plugins/synthetics/server/legacy_uptime/lib/requests/get_journey_screenshot.ts b/x-pack/plugins/synthetics/server/legacy_uptime/lib/requests/get_journey_screenshot.ts index 083f2562586ce..a7cbc0cb50d33 100644 --- a/x-pack/plugins/synthetics/server/legacy_uptime/lib/requests/get_journey_screenshot.ts +++ b/x-pack/plugins/synthetics/server/legacy_uptime/lib/requests/get_journey_screenshot.ts @@ -63,7 +63,7 @@ export const getJourneyScreenshot: UMElasticsearchQueryFn< const screenshotsOrRefs = (result.body.aggregations?.step.image.hits.hits as ResultType[]) ?? null; - if (screenshotsOrRefs.length === 0) return null; + if (!screenshotsOrRefs || screenshotsOrRefs?.length === 0) return null; return { ...screenshotsOrRefs[0]._source, diff --git a/x-pack/plugins/synthetics/server/legacy_uptime/routes/pings/journey_screenshot_blocks.test.ts b/x-pack/plugins/synthetics/server/legacy_uptime/routes/pings/journey_screenshot_blocks.test.ts index e865ad571edbc..93d23932c3ac5 100644 --- a/x-pack/plugins/synthetics/server/legacy_uptime/routes/pings/journey_screenshot_blocks.test.ts +++ b/x-pack/plugins/synthetics/server/legacy_uptime/routes/pings/journey_screenshot_blocks.test.ts @@ -9,11 +9,20 @@ import { createJourneyScreenshotBlocksRoute } from './journey_screenshot_blocks' import { UMServerLibs } from '../../uptime_server'; describe('journey screenshot blocks route', () => { - let handlerContext: unknown; + let handlerContext: any; let libs: UMServerLibs; + const data: any = []; beforeEach(() => { handlerContext = { - uptimeEsClient: jest.fn(), + uptimeEsClient: { + search: jest.fn().mockResolvedValue({ + body: { + hits: { + hits: data, + }, + }, + }), + }, request: { body: { hashes: ['hash1', 'hash2'], @@ -49,6 +58,32 @@ describe('journey screenshot blocks route', () => { }); it('returns blocks for request', async () => { + handlerContext.uptimeEsClient.search = jest.fn().mockResolvedValue({ + body: { + hits: { + hits: [ + { + _id: 'hash1', + _source: { + synthetics: { + blob: 'blob1', + blob_mime: 'image/jpeg', + }, + }, + }, + { + _id: 'hash2', + _source: { + synthetics: { + blob: 'blob2', + blob_mime: 'image/jpeg', + }, + }, + }, + ], + }, + }, + }); const responseData = [ { id: 'hash1', diff --git a/x-pack/plugins/synthetics/server/legacy_uptime/routes/pings/journey_screenshot_blocks.ts b/x-pack/plugins/synthetics/server/legacy_uptime/routes/pings/journey_screenshot_blocks.ts index 83e77fd8c8bd1..483444e62b1f8 100644 --- a/x-pack/plugins/synthetics/server/legacy_uptime/routes/pings/journey_screenshot_blocks.ts +++ b/x-pack/plugins/synthetics/server/legacy_uptime/routes/pings/journey_screenshot_blocks.ts @@ -8,8 +8,9 @@ import * as t from 'io-ts'; import { isRight } from 'fp-ts/lib/Either'; import { schema } from '@kbn/config-schema'; +import { getJourneyScreenshotBlocks } from '../../lib/requests/get_journey_screenshot_blocks'; import { UMServerLibs } from '../../lib/lib'; -import { UMRestApiRouteFactory } from '../types'; +import { RouteContext, UMRestApiRouteFactory, UptimeRouteContext } from '../types'; import { API_URLS } from '../../../../common/constants'; function isStringArray(data: unknown): data is string[] { @@ -24,22 +25,30 @@ export const createJourneyScreenshotBlocksRoute: UMRestApiRouteFactory = (libs: hashes: schema.arrayOf(schema.string()), }), }, - handler: async ({ request, response, uptimeEsClient }) => { - const { hashes: blockIds } = request.body; + handler: async (routeProps) => { + return await journeyScreenshotBlocksHandler(routeProps); + }, +}); - if (!isStringArray(blockIds)) return response.badRequest(); +export const journeyScreenshotBlocksHandler = async ({ + response, + request, + uptimeEsClient, +}: RouteContext | UptimeRouteContext) => { + const { hashes: blockIds } = request.body; - const result = await libs.requests.getJourneyScreenshotBlocks({ - blockIds, - uptimeEsClient, - }); + if (!isStringArray(blockIds)) return response.badRequest(); - if (result.length === 0) { - return response.notFound(); - } + const result = await getJourneyScreenshotBlocks({ + blockIds, + uptimeEsClient, + }); - return response.ok({ - body: result, - }); - }, -}); + if (result.length === 0) { + return response.notFound(); + } + + return response.ok({ + body: result, + }); +}; diff --git a/x-pack/plugins/synthetics/server/legacy_uptime/routes/pings/journey_screenshots.test.ts b/x-pack/plugins/synthetics/server/legacy_uptime/routes/pings/journey_screenshots.test.ts index 4e0f5fc616f88..7c4a7d61173fc 100644 --- a/x-pack/plugins/synthetics/server/legacy_uptime/routes/pings/journey_screenshots.test.ts +++ b/x-pack/plugins/synthetics/server/legacy_uptime/routes/pings/journey_screenshots.test.ts @@ -12,7 +12,15 @@ describe('journey screenshot route', () => { let handlerContext: any; beforeEach(() => { handlerContext = { - uptimeEsClient: jest.fn(), + uptimeEsClient: { + search: jest.fn().mockResolvedValue({ + body: { + hits: { + hits: [], + }, + }, + }), + }, request: { params: { checkGroup: 'check_group', @@ -63,6 +71,18 @@ describe('journey screenshot route', () => { totalSteps: 3, }; + handlerContext.uptimeEsClient.search = jest.fn().mockResolvedValue({ + body: { + hits: { + total: { + value: 3, + }, + hits: [], + }, + aggregations: { step: { image: { hits: { hits: [{ _source: mock }] } } } }, + }, + }); + const route = createJourneyScreenshotRoute({ requests: { getJourneyScreenshot: jest.fn().mockReturnValue(mock), @@ -93,8 +113,20 @@ describe('journey screenshot route', () => { }, type: 'step/screenshot', }, - totalSteps: 3, }; + + handlerContext.uptimeEsClient.search = jest.fn().mockResolvedValue({ + body: { + hits: { + total: { + value: 3, + }, + hits: [], + }, + aggregations: { step: { image: { hits: { hits: [{ _source: mock }] } } } }, + }, + }); + const route = createJourneyScreenshotRoute({ requests: { getJourneyScreenshot: jest.fn().mockReturnValue(mock), @@ -133,6 +165,17 @@ describe('journey screenshot route', () => { type: 'step/screenshot', }, }; + handlerContext.uptimeEsClient.search = jest.fn().mockResolvedValue({ + body: { + hits: { + total: { + value: 3, + }, + hits: [], + }, + aggregations: { step: { image: { hits: { hits: [{ _source: mock }] } } } }, + }, + }); const route = createJourneyScreenshotRoute({ requests: { getJourneyScreenshot: jest.fn().mockReturnValue(mock), diff --git a/x-pack/plugins/synthetics/server/legacy_uptime/routes/pings/journey_screenshots.ts b/x-pack/plugins/synthetics/server/legacy_uptime/routes/pings/journey_screenshots.ts index 6ae3ae1b45662..7c56b3a26fa0c 100644 --- a/x-pack/plugins/synthetics/server/legacy_uptime/routes/pings/journey_screenshots.ts +++ b/x-pack/plugins/synthetics/server/legacy_uptime/routes/pings/journey_screenshots.ts @@ -8,8 +8,11 @@ import { schema } from '@kbn/config-schema'; import { isRefResult, isFullScreenshot } from '../../../../common/runtime_types/ping/synthetics'; import { UMServerLibs } from '../../lib/lib'; -import { ScreenshotReturnTypesUnion } from '../../lib/requests/get_journey_screenshot'; -import { UMRestApiRouteFactory } from '../types'; +import { + getJourneyScreenshot, + ScreenshotReturnTypesUnion, +} from '../../lib/requests/get_journey_screenshot'; +import { RouteContext, UMRestApiRouteFactory, UptimeRouteContext } from '../types'; import { API_URLS } from '../../../../common/constants'; function getSharedHeaders(stepName: string, totalSteps: number) { @@ -29,32 +32,40 @@ export const createJourneyScreenshotRoute: UMRestApiRouteFactory = (libs: UMServ stepIndex: schema.number(), }), }, - handler: async ({ uptimeEsClient, request, response }) => { - const { checkGroup, stepIndex } = request.params; + handler: async (routeProps) => { + return await journeyScreenshotHandler(routeProps); + }, +}); - const result: ScreenshotReturnTypesUnion | null = await libs.requests.getJourneyScreenshot({ - uptimeEsClient, - checkGroup, - stepIndex, - }); +export const journeyScreenshotHandler = async ({ + response, + request, + uptimeEsClient, +}: RouteContext | UptimeRouteContext) => { + const { checkGroup, stepIndex } = request.params; - if (isFullScreenshot(result) && typeof result.synthetics?.blob !== 'undefined') { - return response.ok({ - body: Buffer.from(result.synthetics.blob, 'base64'), - headers: { - 'content-type': result.synthetics.blob_mime || 'image/png', // falls back to 'image/png' for earlier versions of synthetics - ...getSharedHeaders(result.synthetics.step.name, result.totalSteps), - }, - }); - } else if (isRefResult(result)) { - return response.ok({ - body: { - screenshotRef: result, - }, - headers: getSharedHeaders(result.synthetics.step.name, result.totalSteps), - }); - } + const result: ScreenshotReturnTypesUnion | null = await getJourneyScreenshot({ + uptimeEsClient, + checkGroup, + stepIndex, + }); - return response.notFound(); - }, -}); + if (isFullScreenshot(result) && typeof result.synthetics?.blob !== 'undefined') { + return response.ok({ + body: Buffer.from(result.synthetics.blob, 'base64'), + headers: { + 'content-type': result.synthetics.blob_mime || 'image/png', // falls back to 'image/png' for earlier versions of synthetics + ...getSharedHeaders(result.synthetics.step.name, result.totalSteps), + }, + }); + } else if (isRefResult(result)) { + return response.ok({ + body: { + screenshotRef: result, + }, + headers: getSharedHeaders(result.synthetics.step.name, result.totalSteps), + }); + } + + return response.notFound(); +}; diff --git a/x-pack/plugins/synthetics/server/legacy_uptime/routes/synthetics/last_successful_check.ts b/x-pack/plugins/synthetics/server/legacy_uptime/routes/synthetics/last_successful_check.ts index 3edba46aa39e8..5b1cb0fd33bc2 100644 --- a/x-pack/plugins/synthetics/server/legacy_uptime/routes/synthetics/last_successful_check.ts +++ b/x-pack/plugins/synthetics/server/legacy_uptime/routes/synthetics/last_successful_check.ts @@ -6,11 +6,13 @@ */ import { schema } from '@kbn/config-schema'; +import { getJourneyScreenshot } from '../../lib/requests/get_journey_screenshot'; import { isRefResult, isFullScreenshot } from '../../../../common/runtime_types/ping/synthetics'; import { Ping } from '../../../../common/runtime_types/ping/ping'; import { UMServerLibs } from '../../lib/lib'; -import { UMRestApiRouteFactory } from '../types'; +import { RouteContext, UMRestApiRouteFactory, UptimeRouteContext } from '../types'; import { API_URLS } from '../../../../common/constants'; +import { getLastSuccessfulCheck } from '../../lib/requests/get_last_successful_check'; export const createLastSuccessfulCheckRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => ({ method: 'GET', @@ -23,41 +25,49 @@ export const createLastSuccessfulCheckRoute: UMRestApiRouteFactory = (libs: UMSe location: schema.maybe(schema.string()), }), }, - handler: async ({ uptimeEsClient, request, response }) => { - const { timestamp, monitorId, stepIndex, location } = request.query; - - const check: Ping | null = await libs.requests.getLastSuccessfulCheck({ - uptimeEsClient, - monitorId, - timestamp, - location, - }); - - if (check === null) { - return response.notFound(); - } - - if (!check.monitor.check_group) { - return response.ok({ body: check }); - } - - const screenshot = await libs.requests.getJourneyScreenshot({ - uptimeEsClient, - checkGroup: check.monitor.check_group, - stepIndex, - }); - - if (screenshot === null) { - return response.ok({ body: check }); - } - - if (check.synthetics) { - check.synthetics.isScreenshotRef = isRefResult(screenshot); - check.synthetics.isFullScreenshot = isFullScreenshot(screenshot); - } - - return response.ok({ - body: check, - }); + handler: async (routeProps) => { + return await getLastSuccessfulCheckScreenshot(routeProps); }, }); + +export const getLastSuccessfulCheckScreenshot = async ({ + response, + request, + uptimeEsClient, +}: RouteContext | UptimeRouteContext) => { + const { timestamp, monitorId, stepIndex, location } = request.query; + + const check: Ping | null = await getLastSuccessfulCheck({ + uptimeEsClient, + monitorId, + timestamp, + location, + }); + + if (check === null) { + return response.notFound(); + } + + if (!check.monitor.check_group) { + return response.ok({ body: check }); + } + + const screenshot = await getJourneyScreenshot({ + uptimeEsClient, + checkGroup: check.monitor.check_group, + stepIndex, + }); + + if (screenshot === null) { + return response.ok({ body: check }); + } + + if (check.synthetics) { + check.synthetics.isScreenshotRef = isRefResult(screenshot); + check.synthetics.isFullScreenshot = isFullScreenshot(screenshot); + } + + return response.ok({ + body: check, + }); +}; diff --git a/x-pack/plugins/synthetics/server/legacy_uptime/routes/types.ts b/x-pack/plugins/synthetics/server/legacy_uptime/routes/types.ts index 55286f8ea770e..35ab8e9217eb6 100644 --- a/x-pack/plugins/synthetics/server/legacy_uptime/routes/types.ts +++ b/x-pack/plugins/synthetics/server/legacy_uptime/routes/types.ts @@ -88,6 +88,16 @@ export type SyntheticsRouteWrapper = ( syntheticsMonitorClient: SyntheticsMonitorClient ) => UMKibanaRoute; +export interface UptimeRouteContext { + uptimeEsClient: UptimeEsClient; + context: UptimeRequestHandlerContext; + request: SyntheticsRequest; + response: KibanaResponseFactory; + savedObjectsClient: SavedObjectsClientContract; + server: UptimeServerSetup; + subject?: Subject; +} + /** * This is the contract we specify internally for route handling. */ @@ -99,15 +109,7 @@ export type UMRouteHandler = ({ server, savedObjectsClient, subject, -}: { - uptimeEsClient: UptimeEsClient; - context: UptimeRequestHandlerContext; - request: SyntheticsRequest; - response: KibanaResponseFactory; - savedObjectsClient: SavedObjectsClientContract; - server: UptimeServerSetup; - subject?: Subject; -}) => IKibanaResponse | Promise>; +}: UptimeRouteContext) => IKibanaResponse | Promise>; export interface RouteContext { uptimeEsClient: UptimeEsClient; diff --git a/x-pack/plugins/synthetics/server/legacy_uptime/routes/uptime_route_wrapper.ts b/x-pack/plugins/synthetics/server/legacy_uptime/routes/uptime_route_wrapper.ts index b5a025c885dcf..97c204e90ac99 100644 --- a/x-pack/plugins/synthetics/server/legacy_uptime/routes/uptime_route_wrapper.ts +++ b/x-pack/plugins/synthetics/server/legacy_uptime/routes/uptime_route_wrapper.ts @@ -6,6 +6,7 @@ */ import { KibanaResponse } from '@kbn/core-http-router-server-internal'; +import { checkIndicesReadPrivileges } from '../../synthetics_service/authentication/check_has_privilege'; import { UMKibanaRouteWrapper } from './types'; import { isTestUser, UptimeEsClient } from '../lib/lib'; @@ -23,31 +24,46 @@ export const uptimeRouteWrapper: UMKibanaRouteWrapper = (uptimeRoute, server) => const uptimeEsClient = new UptimeEsClient( coreContext.savedObjects.client, esClient.asCurrentUser, - Boolean(server.isDev && !isTestUser(server)), - coreContext.uiSettings, - request + { + request, + uiSettings: coreContext.uiSettings, + isDev: Boolean(server.isDev && !isTestUser(server)), + } ); server.uptimeEsClient = uptimeEsClient; + try { + const res = await uptimeRoute.handler({ + uptimeEsClient, + savedObjectsClient: coreContext.savedObjects.client, + context, + request, + response, + server, + }); - const res = await uptimeRoute.handler({ - uptimeEsClient, - savedObjectsClient: coreContext.savedObjects.client, - context, - request, - response, - server, - }); + if (res instanceof KibanaResponse) { + return res; + } - if (res instanceof KibanaResponse) { - return res; + return response.ok({ + body: { + ...res, + ...(await uptimeEsClient.getInspectData(uptimeRoute.path)), + }, + }); + } catch (e) { + if (e.statusCode === 403) { + const privileges = await checkIndicesReadPrivileges(uptimeEsClient); + if (!privileges.has_all_requested) { + return response.forbidden({ + body: { + message: `MissingIndicesPrivileges: You do not have permission to read from the ${uptimeEsClient.heartbeatIndices} indices. Please contact your administrator.`, + }, + }); + } + } + throw e; } - - return response.ok({ - body: { - ...res, - ...uptimeEsClient.getInspectData(uptimeRoute.path), - }, - }); }, }); diff --git a/x-pack/plugins/synthetics/server/routes/index.ts b/x-pack/plugins/synthetics/server/routes/index.ts index b394c53f20142..b77d1d13247e0 100644 --- a/x-pack/plugins/synthetics/server/routes/index.ts +++ b/x-pack/plugins/synthetics/server/routes/index.ts @@ -5,7 +5,10 @@ * 2.0. */ -import { createJourneyRoute } from './pings/journeys'; +import { createJourneyScreenshotRoute } from './pings/journey_screenshots'; +import { createJourneyScreenshotBlocksRoute } from './pings/journey_screenshot_blocks'; +import { createLastSuccessfulCheckRoute } from './pings/last_successful_check'; +import { createJourneyFailedStepsRoute, createJourneyRoute } from './pings/journeys'; import { updateDefaultAlertingRoute } from './default_alerts/update_default_alert'; import { syncParamsSyntheticsParamsRoute } from './settings/sync_global_params'; import { editSyntheticsParamsRoute } from './settings/edit_param'; @@ -44,6 +47,7 @@ import { getHasIntegrationMonitorsRoute } from './fleet/get_has_integration_moni import { addSyntheticsParamsRoute } from './settings/add_param'; import { enableDefaultAlertingRoute } from './default_alerts/enable_default_alert'; import { getDefaultAlertingRoute } from './default_alerts/get_default_alert'; +import { createNetworkEventsRoute } from './network_events'; import { addPrivateLocationRoute } from './settings/private_locations/add_private_location'; import { deletePrivateLocationRoute } from './settings/private_locations/delete_private_location'; import { getPrivateLocationsRoute } from './settings/private_locations/get_private_locations'; @@ -80,6 +84,11 @@ export const syntheticsAppRestApiRoutes: SyntheticsRestApiRouteFactory[] = [ getDefaultAlertingRoute, updateDefaultAlertingRoute, createJourneyRoute, + createLastSuccessfulCheckRoute, + createJourneyScreenshotBlocksRoute, + createJourneyFailedStepsRoute, + createNetworkEventsRoute, + createJourneyScreenshotRoute, addPrivateLocationRoute, deletePrivateLocationRoute, getPrivateLocationsRoute, diff --git a/x-pack/plugins/synthetics/server/routes/monitor_cruds/add_monitor.ts b/x-pack/plugins/synthetics/server/routes/monitor_cruds/add_monitor.ts index 0db555a4c48b1..c1c1c16485657 100644 --- a/x-pack/plugins/synthetics/server/routes/monitor_cruds/add_monitor.ts +++ b/x-pack/plugins/synthetics/server/routes/monitor_cruds/add_monitor.ts @@ -13,6 +13,7 @@ import { SavedObjectsErrorHelpers, } from '@kbn/core/server'; import { isValidNamespace } from '@kbn/fleet-plugin/common'; +import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common'; import { getSyntheticsPrivateLocations } from '../../legacy_uptime/lib/saved_objects/private_locations'; import { SyntheticsMonitorClient } from '../../synthetics_service/synthetics_monitor/synthetics_monitor_client'; import { @@ -77,7 +78,7 @@ export const addSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => ({ ); try { - const { id: spaceId } = await server.spaces.spacesService.getActiveSpace(request); + const spaceId = server.spaces?.spacesService.getSpaceId(request) ?? DEFAULT_SPACE_ID; const { errors, newMonitor } = await syncNewMonitor({ normalizedMonitor: validationResult.decodedMonitor, server, @@ -239,12 +240,12 @@ export const syncNewMonitor = async ({ } }; -export const getMonitorNamespace = ( +const getMonitorNamespace = ( server: UptimeServerSetup, request: KibanaRequest, configuredNamespace: string ) => { - const spaceId = server.spaces.spacesService.getSpaceId(request); + const spaceId = server.spaces?.spacesService.getSpaceId(request) ?? DEFAULT_SPACE_ID; const kibanaNamespace = formatKibanaNamespace(spaceId); const namespace = configuredNamespace === DEFAULT_NAMESPACE_STRING ? kibanaNamespace : configuredNamespace; diff --git a/x-pack/plugins/synthetics/server/routes/monitor_cruds/add_monitor_project.ts b/x-pack/plugins/synthetics/server/routes/monitor_cruds/add_monitor_project.ts index 11dbe95952fef..59f798b5bc623 100644 --- a/x-pack/plugins/synthetics/server/routes/monitor_cruds/add_monitor_project.ts +++ b/x-pack/plugins/synthetics/server/routes/monitor_cruds/add_monitor_project.ts @@ -6,6 +6,7 @@ */ import { schema } from '@kbn/config-schema'; import { i18n } from '@kbn/i18n'; +import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common'; import { ProjectMonitor } from '../../../common/runtime_types'; import { SyntheticsRestApiRouteFactory } from '../../legacy_uptime/routes/types'; @@ -51,7 +52,10 @@ export const addSyntheticsProjectMonitorRoute: SyntheticsRestApiRouteFactory = ( } try { - const { id: spaceId } = await server.spaces.spacesService.getActiveSpace(request); + const { id: spaceId } = (await server.spaces?.spacesService.getActiveSpace(request)) ?? { + id: DEFAULT_SPACE_ID, + }; + const encryptedSavedObjectsClient = server.encryptedSavedObjects.getClient(); const pushMonitorFormatter = new ProjectMonitorFormatter({ @@ -75,7 +79,7 @@ export const addSyntheticsProjectMonitorRoute: SyntheticsRestApiRouteFactory = ( } catch (error) { server.logger.error(`Error adding monitors to project ${decodedProjectName}`); if (error.output.statusCode === 404) { - const spaceId = server.spaces.spacesService.getSpaceId(request); + const spaceId = server.spaces?.spacesService.getSpaceId(request) ?? DEFAULT_SPACE_ID; return response.notFound({ body: { message: `Kibana space '${spaceId}' does not exist` } }); } diff --git a/x-pack/plugins/synthetics/server/routes/monitor_cruds/add_monitor_project_legacy.ts b/x-pack/plugins/synthetics/server/routes/monitor_cruds/add_monitor_project_legacy.ts index b68d2ac7aa58c..cfb1ca2f680e3 100644 --- a/x-pack/plugins/synthetics/server/routes/monitor_cruds/add_monitor_project_legacy.ts +++ b/x-pack/plugins/synthetics/server/routes/monitor_cruds/add_monitor_project_legacy.ts @@ -5,6 +5,7 @@ * 2.0. */ import { schema } from '@kbn/config-schema'; +import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common'; import { UMServerLibs } from '../../legacy_uptime/lib/lib'; import { ProjectMonitor } from '../../../common/runtime_types'; @@ -42,7 +43,9 @@ export const addSyntheticsProjectMonitorRouteLegacy: SyntheticsStreamingRouteFac const monitors = (request.body?.monitors as ProjectMonitor[]) || []; try { - const { id: spaceId } = await server.spaces.spacesService.getActiveSpace(request); + const { id: spaceId } = (await server.spaces?.spacesService.getActiveSpace(request)) ?? { + id: DEFAULT_SPACE_ID, + }; const { keep_stale: keepStale, project: projectId } = request.body || {}; const { publicLocations, privateLocations } = await getAllLocations({ @@ -79,7 +82,7 @@ export const addSyntheticsProjectMonitorRouteLegacy: SyntheticsStreamingRouteFac }); } catch (error) { if (error?.output?.statusCode === 404) { - const spaceId = server.spaces.spacesService.getSpaceId(request); + const spaceId = server.spaces?.spacesService.getSpaceId(request) ?? DEFAULT_SPACE_ID; subject?.next(`Unable to create monitors. Kibana space '${spaceId}' does not exist.`); subject?.next({ failedMonitors: monitors.map((m) => m.id) }); } else { diff --git a/x-pack/plugins/synthetics/server/routes/monitor_cruds/bulk_cruds/delete_monitor_bulk.ts b/x-pack/plugins/synthetics/server/routes/monitor_cruds/bulk_cruds/delete_monitor_bulk.ts index f5007986d2508..90130ccaae2dc 100644 --- a/x-pack/plugins/synthetics/server/routes/monitor_cruds/bulk_cruds/delete_monitor_bulk.ts +++ b/x-pack/plugins/synthetics/server/routes/monitor_cruds/bulk_cruds/delete_monitor_bulk.ts @@ -6,6 +6,7 @@ */ import { SavedObjectsClientContract, KibanaRequest } from '@kbn/core/server'; import { SavedObject } from '@kbn/core-saved-objects-server'; +import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common'; import { formatTelemetryDeleteEvent, sendTelemetryEvents, @@ -37,7 +38,10 @@ export const deleteMonitorBulk = async ({ const { logger, telemetry, stackVersion } = server; try { - const { id: spaceId } = await server.spaces.spacesService.getActiveSpace(request); + const { id: spaceId } = (await server.spaces?.spacesService.getActiveSpace(request)) ?? { + id: DEFAULT_SPACE_ID, + }; + const deleteSyncPromise = syntheticsMonitorClient.deleteMonitors( monitors.map((normalizedMonitor) => ({ ...normalizedMonitor.attributes, diff --git a/x-pack/plugins/synthetics/server/routes/monitor_cruds/delete_monitor.ts b/x-pack/plugins/synthetics/server/routes/monitor_cruds/delete_monitor.ts index 2f3b4b2595dc9..2b194ff62d887 100644 --- a/x-pack/plugins/synthetics/server/routes/monitor_cruds/delete_monitor.ts +++ b/x-pack/plugins/synthetics/server/routes/monitor_cruds/delete_monitor.ts @@ -10,6 +10,7 @@ import { SavedObjectsClientContract, SavedObjectsErrorHelpers, } from '@kbn/core/server'; +import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common'; import { deletePermissionError } from '../../synthetics_service/private_location/synthetics_private_location'; import { SyntheticsMonitorClient } from '../../synthetics_service/synthetics_monitor/synthetics_monitor_client'; import { @@ -108,7 +109,7 @@ export const deleteMonitor = async ({ } try { - const spaceId = server.spaces.spacesService.getSpaceId(request); + const spaceId = server.spaces?.spacesService.getSpaceId(request) ?? DEFAULT_SPACE_ID; const deleteSyncPromise = syntheticsMonitorClient.deleteMonitors( [ { diff --git a/x-pack/plugins/synthetics/server/routes/monitor_cruds/edit_monitor.test.ts b/x-pack/plugins/synthetics/server/routes/monitor_cruds/edit_monitor.test.ts index a60eba8868274..2df5e74b93e17 100644 --- a/x-pack/plugins/synthetics/server/routes/monitor_cruds/edit_monitor.test.ts +++ b/x-pack/plugins/synthetics/server/routes/monitor_cruds/edit_monitor.test.ts @@ -109,7 +109,7 @@ describe('syncEditedMonitor', () => { expect(syntheticsService.editConfig).toHaveBeenCalledWith( expect.arrayContaining([ expect.objectContaining({ - id: '7af7e2f0-d5dc-11ec-87ac-bdfdb894c53d', + configId: '7af7e2f0-d5dc-11ec-87ac-bdfdb894c53d', }), ]) ); diff --git a/x-pack/plugins/synthetics/server/routes/monitor_cruds/edit_monitor.ts b/x-pack/plugins/synthetics/server/routes/monitor_cruds/edit_monitor.ts index 09b046b872ed6..90057f6ea7ef3 100644 --- a/x-pack/plugins/synthetics/server/routes/monitor_cruds/edit_monitor.ts +++ b/x-pack/plugins/synthetics/server/routes/monitor_cruds/edit_monitor.ts @@ -13,6 +13,7 @@ import { KibanaRequest, } from '@kbn/core/server'; import { SavedObjectsErrorHelpers } from '@kbn/core/server'; +import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common'; import { getSyntheticsPrivateLocations } from '../../legacy_uptime/lib/saved_objects/private_locations'; import { SyntheticsMonitorClient } from '../../synthetics_service/synthetics_monitor/synthetics_monitor_client'; import { @@ -57,7 +58,7 @@ export const editSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => ( const { monitorId } = request.params; try { - const { id: spaceId } = await server.spaces.spacesService.getActiveSpace(request); + const spaceId = server.spaces?.spacesService.getSpaceId(request) ?? DEFAULT_SPACE_ID; const previousMonitor: SavedObject = await savedObjectsClient.get( syntheticsMonitorType, diff --git a/x-pack/plugins/synthetics/server/routes/network_events/get_network_events.ts b/x-pack/plugins/synthetics/server/routes/network_events/get_network_events.ts new file mode 100644 index 0000000000000..114cb88d508b1 --- /dev/null +++ b/x-pack/plugins/synthetics/server/routes/network_events/get_network_events.ts @@ -0,0 +1,32 @@ +/* + * 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 { schema } from '@kbn/config-schema'; +import { getNetworkEvents } from '../../legacy_uptime/lib/requests/get_network_events'; +import { SYNTHETICS_API_URLS } from '../../../common/constants'; +import { UMServerLibs } from '../../legacy_uptime/uptime_server'; +import { SyntheticsRestApiRouteFactory } from '../../legacy_uptime/routes'; + +export const createNetworkEventsRoute: SyntheticsRestApiRouteFactory = (libs: UMServerLibs) => ({ + method: 'GET', + path: SYNTHETICS_API_URLS.NETWORK_EVENTS, + validate: { + query: schema.object({ + checkGroup: schema.string(), + stepIndex: schema.number(), + }), + }, + handler: async ({ uptimeEsClient, request }): Promise => { + const { checkGroup, stepIndex } = request.query; + + return await getNetworkEvents({ + uptimeEsClient, + checkGroup, + stepIndex, + }); + }, +}); diff --git a/x-pack/plugins/synthetics/server/routes/network_events/index.ts b/x-pack/plugins/synthetics/server/routes/network_events/index.ts new file mode 100644 index 0000000000000..e2b8c871e17bf --- /dev/null +++ b/x-pack/plugins/synthetics/server/routes/network_events/index.ts @@ -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 { createNetworkEventsRoute } from './get_network_events'; diff --git a/x-pack/plugins/synthetics/server/routes/pings/journey_screenshot_blocks.ts b/x-pack/plugins/synthetics/server/routes/pings/journey_screenshot_blocks.ts new file mode 100644 index 0000000000000..6cf0f92b41b40 --- /dev/null +++ b/x-pack/plugins/synthetics/server/routes/pings/journey_screenshot_blocks.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema } from '@kbn/config-schema'; +import { UMServerLibs } from '../../legacy_uptime/uptime_server'; +import { journeyScreenshotBlocksHandler } from '../../legacy_uptime/routes/pings/journey_screenshot_blocks'; +import { SyntheticsRestApiRouteFactory } from '../../legacy_uptime/routes'; +import { SYNTHETICS_API_URLS } from '../../../common/constants'; + +export const createJourneyScreenshotBlocksRoute: SyntheticsRestApiRouteFactory = ( + libs: UMServerLibs +) => ({ + method: 'POST', + path: SYNTHETICS_API_URLS.JOURNEY_SCREENSHOT_BLOCKS, + validate: { + body: schema.object({ + hashes: schema.arrayOf(schema.string()), + }), + }, + handler: async (routeProps) => { + return await journeyScreenshotBlocksHandler(routeProps); + }, +}); diff --git a/x-pack/plugins/synthetics/server/routes/pings/journey_screenshots.ts b/x-pack/plugins/synthetics/server/routes/pings/journey_screenshots.ts new file mode 100644 index 0000000000000..a92cf73f1711b --- /dev/null +++ b/x-pack/plugins/synthetics/server/routes/pings/journey_screenshots.ts @@ -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 { schema } from '@kbn/config-schema'; +import { SyntheticsRestApiRouteFactory } from '../../legacy_uptime/routes'; +import { SYNTHETICS_API_URLS } from '../../../common/constants'; +import { UMServerLibs } from '../../legacy_uptime/uptime_server'; +import { journeyScreenshotHandler } from '../../legacy_uptime/routes/pings/journey_screenshots'; + +export const createJourneyScreenshotRoute: SyntheticsRestApiRouteFactory = ( + libs: UMServerLibs +) => ({ + method: 'GET', + path: SYNTHETICS_API_URLS.JOURNEY_SCREENSHOT, + validate: { + params: schema.object({ + checkGroup: schema.string(), + stepIndex: schema.number(), + }), + }, + handler: async (routeProps) => { + return await journeyScreenshotHandler(routeProps); + }, +}); diff --git a/x-pack/plugins/synthetics/server/routes/pings/journeys.ts b/x-pack/plugins/synthetics/server/routes/pings/journeys.ts index 9ef10fe6bebb2..212f79bd867d8 100644 --- a/x-pack/plugins/synthetics/server/routes/pings/journeys.ts +++ b/x-pack/plugins/synthetics/server/routes/pings/journeys.ts @@ -6,9 +6,13 @@ */ import { schema } from '@kbn/config-schema'; -import { API_URLS, SYNTHETICS_API_URLS } from '../../../common/constants'; +import { getJourneyFailedSteps } from '../../legacy_uptime/lib/requests/get_journey_failed_steps'; +import { SYNTHETICS_API_URLS } from '../../../common/constants'; import { UMServerLibs } from '../../legacy_uptime/uptime_server'; -import { UMRestApiRouteFactory } from '../../legacy_uptime/routes/types'; +import { + SyntheticsRestApiRouteFactory, + UMRestApiRouteFactory, +} from '../../legacy_uptime/routes/types'; import { getJourneyDetails } from '../../queries/get_journey_details'; export const createJourneyRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => ({ @@ -54,9 +58,11 @@ export const createJourneyRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => }, }); -export const createJourneyFailedStepsRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => ({ +export const createJourneyFailedStepsRoute: SyntheticsRestApiRouteFactory = ( + libs: UMServerLibs +) => ({ method: 'GET', - path: API_URLS.JOURNEY_FAILED_STEPS, + path: SYNTHETICS_API_URLS.JOURNEY_FAILED_STEPS, validate: { query: schema.object({ checkGroups: schema.arrayOf(schema.string()), @@ -65,7 +71,7 @@ export const createJourneyFailedStepsRoute: UMRestApiRouteFactory = (libs: UMSer handler: async ({ uptimeEsClient, request, response }): Promise => { const { checkGroups } = request.query; try { - const result = await libs.requests.getJourneyFailedSteps({ + const result = await getJourneyFailedSteps({ uptimeEsClient, checkGroups, }); diff --git a/x-pack/plugins/synthetics/server/routes/pings/last_successful_check.ts b/x-pack/plugins/synthetics/server/routes/pings/last_successful_check.ts new file mode 100644 index 0000000000000..7ebc25adec373 --- /dev/null +++ b/x-pack/plugins/synthetics/server/routes/pings/last_successful_check.ts @@ -0,0 +1,30 @@ +/* + * 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 { schema } from '@kbn/config-schema'; +import { getLastSuccessfulCheckScreenshot } from '../../legacy_uptime/routes/synthetics/last_successful_check'; +import { SyntheticsRestApiRouteFactory } from '../../legacy_uptime/routes'; +import { UMServerLibs } from '../../legacy_uptime/uptime_server'; +import { SYNTHETICS_API_URLS } from '../../../common/constants'; + +export const createLastSuccessfulCheckRoute: SyntheticsRestApiRouteFactory = ( + libs: UMServerLibs +) => ({ + method: 'GET', + path: SYNTHETICS_API_URLS.SYNTHETICS_SUCCESSFUL_CHECK, + validate: { + query: schema.object({ + monitorId: schema.string(), + stepIndex: schema.number(), + timestamp: schema.string(), + location: schema.maybe(schema.string()), + }), + }, + handler: async (routeProps) => { + return await getLastSuccessfulCheckScreenshot(routeProps); + }, +}); diff --git a/x-pack/plugins/synthetics/server/routes/settings/add_param.ts b/x-pack/plugins/synthetics/server/routes/settings/add_param.ts index adb97493a51d8..a563ee83d2d55 100644 --- a/x-pack/plugins/synthetics/server/routes/settings/add_param.ts +++ b/x-pack/plugins/synthetics/server/routes/settings/add_param.ts @@ -6,6 +6,7 @@ */ import { schema } from '@kbn/config-schema'; +import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common'; import { SyntheticsParam } from '../../../common/runtime_types'; import { syntheticsParamType } from '../../../common/types/saved_objects'; import { SyntheticsRestApiRouteFactory } from '../../legacy_uptime/routes/types'; @@ -27,7 +28,7 @@ export const addSyntheticsParamsRoute: SyntheticsRestApiRouteFactory = () => ({ writeAccess: true, handler: async ({ request, server, savedObjectsClient }): Promise => { const { namespaces, ...data } = request.body as SyntheticsParam; - const { id: spaceId } = await server.spaces.spacesService.getActiveSpace(request); + const spaceId = server.spaces?.spacesService.getSpaceId(request) ?? DEFAULT_SPACE_ID; const result = await savedObjectsClient.create(syntheticsParamType, data, { initialNamespaces: (namespaces ?? []).length > 0 ? namespaces : [spaceId], diff --git a/x-pack/plugins/synthetics/server/routes/settings/params.ts b/x-pack/plugins/synthetics/server/routes/settings/params.ts index c75b94186fb85..e713ca7a33ffc 100644 --- a/x-pack/plugins/synthetics/server/routes/settings/params.ts +++ b/x-pack/plugins/synthetics/server/routes/settings/params.ts @@ -6,6 +6,7 @@ */ import { SavedObjectsFindResult } from '@kbn/core-saved-objects-api-server'; +import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common'; import { syntheticsParamType } from '../../../common/types/saved_objects'; import { SyntheticsRestApiRouteFactory } from '../../legacy_uptime/routes/types'; import { SYNTHETICS_API_URLS } from '../../../common/constants'; @@ -17,7 +18,7 @@ export const getSyntheticsParamsRoute: SyntheticsRestApiRouteFactory = () => ({ handler: async ({ savedObjectsClient, request, server }): Promise => { const encryptedSavedObjectsClient = server.encryptedSavedObjects.getClient(); - const spaceId = server.spaces.spacesService.getSpaceId(request); + const spaceId = server.spaces?.spacesService.getSpaceId(request) ?? DEFAULT_SPACE_ID; const canSave = (await server.coreStart?.capabilities.resolveCapabilities(request)).uptime.save ?? false; diff --git a/x-pack/plugins/synthetics/server/routes/settings/sync_global_params.ts b/x-pack/plugins/synthetics/server/routes/settings/sync_global_params.ts index d4b6c833e28e2..7eb7d30356013 100644 --- a/x-pack/plugins/synthetics/server/routes/settings/sync_global_params.ts +++ b/x-pack/plugins/synthetics/server/routes/settings/sync_global_params.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common'; import { getPrivateLocations } from '../../synthetics_service/get_private_locations'; import { SyntheticsRestApiRouteFactory } from '../../legacy_uptime/routes/types'; import { SYNTHETICS_API_URLS } from '../../../common/constants'; @@ -20,7 +21,7 @@ export const syncParamsSyntheticsParamsRoute: SyntheticsRestApiRouteFactory = () request, server, }): Promise => { - const spaceId = server.spaces.spacesService.getSpaceId(request); + const spaceId = server.spaces?.spacesService.getSpaceId(request) ?? DEFAULT_SPACE_ID; const allPrivateLocations = await getPrivateLocations( syntheticsMonitorClient, diff --git a/x-pack/plugins/synthetics/server/routes/synthetics_service/run_once_monitor.ts b/x-pack/plugins/synthetics/server/routes/synthetics_service/run_once_monitor.ts index b7cf5199bfd7c..248d0fecf9987 100644 --- a/x-pack/plugins/synthetics/server/routes/synthetics_service/run_once_monitor.ts +++ b/x-pack/plugins/synthetics/server/routes/synthetics_service/run_once_monitor.ts @@ -5,10 +5,10 @@ * 2.0. */ import { schema } from '@kbn/config-schema'; +import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common'; import { MonitorFields } from '../../../common/runtime_types'; import { SyntheticsRestApiRouteFactory } from '../../legacy_uptime/routes/types'; import { API_URLS } from '../../../common/constants'; -import { formatHeartbeatRequest } from '../../synthetics_service/formatters/format_configs'; import { validateMonitor } from '../monitor_cruds/monitor_validation'; export const runOnceSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => ({ @@ -26,7 +26,7 @@ export const runOnceSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () = const validationResult = validateMonitor(monitor); - const spaceId = server.spaces.spacesService.getSpaceId(request); + const spaceId = server.spaces?.spacesService.getSpaceId(request) ?? DEFAULT_SPACE_ID; if (!validationResult.valid || !validationResult.decodedMonitor) { const { reason: message, details, payload } = validationResult; @@ -37,17 +37,15 @@ export const runOnceSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () = const paramsBySpace = await syntheticsService.getSyntheticsParams({ spaceId }); - const errors = await syntheticsService.runOnceConfigs([ - formatHeartbeatRequest({ - // making it enabled, even if it's disabled in the UI - monitor: { ...validationResult.decodedMonitor, enabled: true }, - monitorId, - heartbeatId: monitorId, - runOnce: true, - testRunId: monitorId, - params: paramsBySpace[spaceId], - }), - ]); + const errors = await syntheticsService.runOnceConfigs({ + // making it enabled, even if it's disabled in the UI + monitor: { ...validationResult.decodedMonitor, enabled: true }, + configId: monitorId, + heartbeatId: monitorId, + runOnce: true, + testRunId: monitorId, + params: paramsBySpace[spaceId], + }); if (errors) { return { errors }; diff --git a/x-pack/plugins/synthetics/server/routes/synthetics_service/test_now_monitor.ts b/x-pack/plugins/synthetics/server/routes/synthetics_service/test_now_monitor.ts index e4cd6d22704c2..929df3a49f962 100644 --- a/x-pack/plugins/synthetics/server/routes/synthetics_service/test_now_monitor.ts +++ b/x-pack/plugins/synthetics/server/routes/synthetics_service/test_now_monitor.ts @@ -6,6 +6,7 @@ */ import { schema } from '@kbn/config-schema'; import { v4 as uuidv4 } from 'uuid'; +import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common'; import { TestNowResponse } from '../../../common/types'; import { ConfigKey, @@ -15,7 +16,6 @@ import { import { SyntheticsRestApiRouteFactory } from '../../legacy_uptime/routes/types'; import { API_URLS } from '../../../common/constants'; import { syntheticsMonitorType } from '../../legacy_uptime/lib/saved_objects/synthetics_monitor'; -import { formatHeartbeatRequest } from '../../synthetics_service/formatters/format_configs'; import { normalizeSecrets } from '../../synthetics_service/utils/secrets'; export const testNowMonitorRoute: SyntheticsRestApiRouteFactory = () => ({ @@ -44,20 +44,18 @@ export const testNowMonitorRoute: SyntheticsRestApiRouteFactory = () => ({ const testRunId = uuidv4(); - const spaceId = server.spaces.spacesService.getSpaceId(request); + const spaceId = server.spaces?.spacesService.getSpaceId(request) ?? DEFAULT_SPACE_ID; const paramsBySpace = await syntheticsService.getSyntheticsParams({ spaceId }); - const errors = await syntheticsService.runOnceConfigs([ - formatHeartbeatRequest({ - // making it enabled, even if it's disabled in the UI - monitor: { ...normalizedMonitor.attributes, enabled: true }, - monitorId, - heartbeatId: (normalizedMonitor.attributes as MonitorFields)[ConfigKey.MONITOR_QUERY_ID], - testRunId, - params: paramsBySpace[spaceId], - }), - ]); + const errors = await syntheticsService.runOnceConfigs({ + // making it enabled, even if it's disabled in the UI + monitor: { ...normalizedMonitor.attributes, enabled: true }, + configId: monitorId, + heartbeatId: (normalizedMonitor.attributes as MonitorFields)[ConfigKey.MONITOR_QUERY_ID], + testRunId, + params: paramsBySpace[spaceId], + }); if (errors && errors?.length > 0) { return { diff --git a/x-pack/plugins/synthetics/server/saved_objects/synthetics_monitor/get_all_monitors.ts b/x-pack/plugins/synthetics/server/saved_objects/synthetics_monitor/get_all_monitors.ts index e0f404ebef875..493c3a2889bdf 100644 --- a/x-pack/plugins/synthetics/server/saved_objects/synthetics_monitor/get_all_monitors.ts +++ b/x-pack/plugins/synthetics/server/saved_objects/synthetics_monitor/get_all_monitors.ts @@ -37,7 +37,7 @@ export const getAllMonitors = async ({ search?: string; filter?: string; } & Pick) => { - const finder = soClient.createPointInTimeFinder({ + const finder = soClient.createPointInTimeFinder({ type: syntheticsMonitorType, perPage: 1000, search, @@ -50,9 +50,7 @@ export const getAllMonitors = async ({ const hits: Array> = []; for await (const result of finder.find()) { - hits.push( - ...(result.saved_objects as Array>) - ); + hits.push(...result.saved_objects); } // no need to wait for it diff --git a/x-pack/plugins/synthetics/server/synthetics_route_wrapper.ts b/x-pack/plugins/synthetics/server/synthetics_route_wrapper.ts index 3e3eb72700ed2..3a13b228ba035 100644 --- a/x-pack/plugins/synthetics/server/synthetics_route_wrapper.ts +++ b/x-pack/plugins/synthetics/server/synthetics_route_wrapper.ts @@ -5,6 +5,8 @@ * 2.0. */ import { KibanaResponse } from '@kbn/core-http-router-server-internal'; +import { checkIndicesReadPrivileges } from './synthetics_service/authentication/check_has_privilege'; +import { SYNTHETICS_INDEX_PATTERN } from '../common/constants'; import { isTestUser, UptimeEsClient } from './legacy_uptime/lib/lib'; import { syntheticsServiceApiKey } from './legacy_uptime/lib/saved_objects/service_api_key'; import { SyntheticsRouteWrapper, SyntheticsStreamingRouteHandler } from './legacy_uptime/routes'; @@ -29,13 +31,11 @@ export const syntheticsRouteWrapper: SyntheticsRouteWrapper = ( // specifically needed for the synthetics service api key generation server.authSavedObjectsClient = savedObjectsClient; - const uptimeEsClient = new UptimeEsClient( - savedObjectsClient, - esClient.asCurrentUser, - false, - coreContext.uiSettings, - request - ); + const uptimeEsClient = new UptimeEsClient(savedObjectsClient, esClient.asCurrentUser, { + request, + isDev: false, + uiSettings: coreContext.uiSettings, + }); server.uptimeEsClient = uptimeEsClient; @@ -62,35 +62,48 @@ export const syntheticsRouteWrapper: SyntheticsRouteWrapper = ( // specifically needed for the synthetics service api key generation server.authSavedObjectsClient = savedObjectsClient; - const uptimeEsClient = new UptimeEsClient( - savedObjectsClient, - esClient.asCurrentUser, - Boolean(server.isDev) && !isTestUser(server), + const uptimeEsClient = new UptimeEsClient(savedObjectsClient, esClient.asCurrentUser, { + request, uiSettings, - request - ); + isDev: Boolean(server.isDev) && !isTestUser(server), + heartbeatIndices: SYNTHETICS_INDEX_PATTERN, + }); server.uptimeEsClient = uptimeEsClient; - const res = await uptimeRoute.handler({ - uptimeEsClient, - savedObjectsClient, - context, - request, - response, - server, - syntheticsMonitorClient, - }); + try { + const res = await uptimeRoute.handler({ + uptimeEsClient, + savedObjectsClient, + context, + request, + response, + server, + syntheticsMonitorClient, + }); + if (res instanceof KibanaResponse) { + return res; + } - if (res instanceof KibanaResponse) { - return res; + return response.ok({ + body: { + ...res, + ...(await uptimeEsClient.getInspectData(uptimeRoute.path)), + }, + }); + } catch (e) { + if (e.statusCode === 403) { + const privileges = await checkIndicesReadPrivileges(uptimeEsClient); + if (!privileges.has_all_requested) { + return response.forbidden({ + body: { + message: + 'MissingIndicesPrivileges: You do not have permission to read from the synthetics-* indices. Please contact your administrator.', + }, + }); + } + } + throw e; } - - return response.ok({ - body: { - ...res, - ...uptimeEsClient.getInspectData(uptimeRoute.path), - }, - }); }, }); diff --git a/x-pack/plugins/synthetics/server/synthetics_service/authentication/check_has_privilege.ts b/x-pack/plugins/synthetics/server/synthetics_service/authentication/check_has_privilege.ts index 56b7ce8b79c62..7a2dcb5446725 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/authentication/check_has_privilege.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/authentication/check_has_privilege.ts @@ -5,9 +5,11 @@ * 2.0. */ +import { SecurityIndexPrivilege } from '@elastic/elasticsearch/lib/api/types'; import { UptimeServerSetup } from '../../legacy_uptime/lib/adapters'; import { getFakeKibanaRequest } from '../utils/fake_kibana_request'; -import { serviceApiKeyPrivileges } from '../get_api_key'; +import { serviceApiKeyPrivileges, syntheticsIndex } from '../get_api_key'; +import { UptimeEsClient } from '../../legacy_uptime/lib/lib'; export const checkHasPrivileges = async ( server: UptimeServerSetup, @@ -22,3 +24,16 @@ export const checkHasPrivileges = async ( }, }); }; + +export const checkIndicesReadPrivileges = async (uptimeEsClient: UptimeEsClient) => { + return await uptimeEsClient.baseESClient.security.hasPrivileges({ + body: { + index: [ + { + names: [syntheticsIndex], + privileges: ['read'] as SecurityIndexPrivilege[], + }, + ], + }, + }); +}; diff --git a/x-pack/plugins/synthetics/server/synthetics_service/formatters/browser.ts b/x-pack/plugins/synthetics/server/synthetics_service/formatters/browser.ts index d9ee08ace167c..1a5e090033ece 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/formatters/browser.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/formatters/browser.ts @@ -5,16 +5,12 @@ * 2.0. */ -import { - Formatter, - commonFormatters, - objectFormatter, - stringToObjectFormatter, - arrayFormatter, -} from './common'; +import { browserFormatters as basicBrowserFormatters } from '../../../common/formatters/browser/formatters'; +import { Formatter, commonFormatters } from './common'; import { BrowserFields, ConfigKey } from '../../../common/runtime_types/monitor_management'; import { DEFAULT_BROWSER_ADVANCED_FIELDS } from '../../../common/constants/monitor_defaults'; import { tlsFormatters } from './tls'; +import { arrayFormatter, objectFormatter, stringToObjectFormatter } from './formatting_utils'; export type BrowserFormatMap = Record; @@ -38,38 +34,21 @@ const throttlingFormatter: Formatter = (fields) => { }; export const browserFormatters: BrowserFormatMap = { - [ConfigKey.METADATA]: (fields) => objectFormatter(fields[ConfigKey.METADATA]), - [ConfigKey.URLS]: null, - [ConfigKey.PORT]: null, - [ConfigKey.ZIP_URL_TLS_VERSION]: (fields) => - arrayFormatter(fields[ConfigKey.ZIP_URL_TLS_VERSION]), - [ConfigKey.SOURCE_ZIP_URL]: null, - [ConfigKey.SOURCE_ZIP_USERNAME]: null, - [ConfigKey.SOURCE_ZIP_PASSWORD]: null, - [ConfigKey.SOURCE_ZIP_FOLDER]: null, - [ConfigKey.SOURCE_ZIP_PROXY_URL]: null, - [ConfigKey.SOURCE_PROJECT_CONTENT]: null, + ...basicBrowserFormatters, + [ConfigKey.METADATA]: objectFormatter, + [ConfigKey.ZIP_URL_TLS_VERSION]: arrayFormatter, [ConfigKey.SOURCE_INLINE]: null, - [ConfigKey.PARAMS]: (fields) => stringToObjectFormatter(fields[ConfigKey.PARAMS] || ''), - [ConfigKey.SCREENSHOTS]: null, - [ConfigKey.SYNTHETICS_ARGS]: (fields) => arrayFormatter(fields[ConfigKey.SYNTHETICS_ARGS]), [ConfigKey.ZIP_URL_TLS_CERTIFICATE_AUTHORITIES]: null, [ConfigKey.ZIP_URL_TLS_CERTIFICATE]: null, [ConfigKey.ZIP_URL_TLS_KEY]: null, [ConfigKey.ZIP_URL_TLS_KEY_PASSPHRASE]: null, [ConfigKey.ZIP_URL_TLS_VERIFICATION_MODE]: null, - [ConfigKey.IS_THROTTLING_ENABLED]: null, - [ConfigKey.THROTTLING_CONFIG]: (fields) => throttlingFormatter(fields), - [ConfigKey.DOWNLOAD_SPEED]: null, - [ConfigKey.UPLOAD_SPEED]: null, - [ConfigKey.LATENCY]: null, + [ConfigKey.THROTTLING_CONFIG]: throttlingFormatter, [ConfigKey.JOURNEY_FILTERS_MATCH]: null, - [ConfigKey.JOURNEY_FILTERS_TAGS]: (fields) => - arrayFormatter(fields[ConfigKey.JOURNEY_FILTERS_TAGS]), - [ConfigKey.IGNORE_HTTPS_ERRORS]: null, - [ConfigKey.PLAYWRIGHT_OPTIONS]: (fields) => - stringToObjectFormatter(fields[ConfigKey.PLAYWRIGHT_OPTIONS] || ''), - [ConfigKey.TEXT_ASSERTION]: null, + [ConfigKey.SYNTHETICS_ARGS]: arrayFormatter, + [ConfigKey.JOURNEY_FILTERS_TAGS]: arrayFormatter, + [ConfigKey.PARAMS]: stringToObjectFormatter, + [ConfigKey.PLAYWRIGHT_OPTIONS]: stringToObjectFormatter, ...commonFormatters, ...tlsFormatters, }; diff --git a/x-pack/plugins/synthetics/server/synthetics_service/formatters/common.test.ts b/x-pack/plugins/synthetics/server/synthetics_service/formatters/common.test.ts index 70bbd0ca402a6..b96c6704294c9 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/formatters/common.test.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/formatters/common.test.ts @@ -5,7 +5,8 @@ * 2.0. */ -import { stringToObjectFormatter } from './common'; +import { ConfigKey } from '../../../common/runtime_types'; +import { stringToObjectFormatter } from './formatting_utils'; describe('common formatters', () => { it.each([ @@ -14,6 +15,6 @@ describe('common formatters', () => { ['{}', undefined], ['{"some": "json"}', { some: 'json' }], ])('formats strings to objects correctly, avoiding errors', (input, expected) => { - expect(stringToObjectFormatter(input)).toEqual(expected); + expect(stringToObjectFormatter({ name: input }, ConfigKey.NAME)).toEqual(expected); }); }); diff --git a/x-pack/plugins/synthetics/server/synthetics_service/formatters/common.ts b/x-pack/plugins/synthetics/server/synthetics_service/formatters/common.ts index ef88a0e7c5686..ec33b2613edf2 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/formatters/common.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/formatters/common.ts @@ -5,52 +5,28 @@ * 2.0. */ -import { CommonFields, ConfigKey, MonitorFields, SourceType } from '../../../common/runtime_types'; - -export type FormattedValue = boolean | string | string[] | Record | null; - -export type Formatter = null | ((fields: Partial) => FormattedValue); +import { arrayFormatter } from './formatting_utils'; +import { commonFormatters as commonBasicFormatters } from '../../../common/formatters/common/formatters'; +import { CommonFields, ConfigKey, MonitorFields } from '../../../common/runtime_types'; + +export type FormattedValue = + | boolean + | number + | string + | string[] + | Record + | null + | Function; + +export type Formatter = + | null + | ((fields: Partial, key: ConfigKey) => FormattedValue) + | Function; export type CommonFormatMap = Record; - export const commonFormatters: CommonFormatMap = { - [ConfigKey.NAME]: null, - [ConfigKey.LOCATIONS]: null, - [ConfigKey.ENABLED]: null, - [ConfigKey.ALERT_CONFIG]: null, - [ConfigKey.MONITOR_TYPE]: null, - [ConfigKey.CONFIG_ID]: null, - [ConfigKey.LOCATIONS]: null, + ...commonBasicFormatters, [ConfigKey.SCHEDULE]: (fields) => `@every ${fields[ConfigKey.SCHEDULE]?.number}${fields[ConfigKey.SCHEDULE]?.unit}`, - [ConfigKey.APM_SERVICE_NAME]: null, - [ConfigKey.TAGS]: (fields) => arrayFormatter(fields[ConfigKey.TAGS]), - [ConfigKey.TIMEOUT]: (fields) => secondsToCronFormatter(fields[ConfigKey.TIMEOUT] || undefined), - [ConfigKey.NAMESPACE]: null, - [ConfigKey.REVISION]: null, - [ConfigKey.MONITOR_SOURCE_TYPE]: (fields) => - fields[ConfigKey.MONITOR_SOURCE_TYPE] || SourceType.UI, - [ConfigKey.FORM_MONITOR_TYPE]: null, - [ConfigKey.JOURNEY_ID]: null, - [ConfigKey.PROJECT_ID]: null, - [ConfigKey.CUSTOM_HEARTBEAT_ID]: null, - [ConfigKey.ORIGINAL_SPACE]: null, - [ConfigKey.CONFIG_HASH]: null, - [ConfigKey.MONITOR_QUERY_ID]: null, -}; - -export const arrayFormatter = (value: string[] = []) => (value.length ? value : null); - -export const secondsToCronFormatter = (value: string = '') => (value ? `${value}s` : null); - -export const objectFormatter = (value: Record = {}) => - Object.keys(value).length ? value : null; - -export const stringToObjectFormatter = (value: string) => { - try { - const obj = JSON.parse(value || '{}'); - return Object.keys(obj).length ? obj : undefined; - } catch { - return undefined; - } + [ConfigKey.TAGS]: arrayFormatter, }; diff --git a/x-pack/plugins/synthetics/server/synthetics_service/formatters/format_configs.test.ts b/x-pack/plugins/synthetics/server/synthetics_service/formatters/format_configs.test.ts index 838eec82f2727..139c9faa91854 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/formatters/format_configs.test.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/formatters/format_configs.test.ts @@ -6,7 +6,11 @@ */ import { omit } from 'lodash'; import { FormattedValue } from './common'; -import { formatMonitorConfig, formatHeartbeatRequest } from './format_configs'; +import { + formatMonitorConfigFields, + formatHeartbeatRequest, + mixParamsWithGlobalParams, +} from './format_configs'; import { ConfigKey, DataStream, @@ -17,6 +21,7 @@ import { SyntheticsMonitor, VerificationMode, } from '../../../common/runtime_types'; +import { loggerMock } from '@kbn/logging-mocks'; const testHTTPConfig: Partial = { type: 'http' as DataStream, @@ -31,7 +36,7 @@ const testHTTPConfig: Partial = { urls: 'https://www.google.com', max_redirects: '0', password: '3z9SBOQWW5F0UrdqLVFqlF6z', - proxy_url: '', + proxy_url: '${proxyUrl}', 'check.response.body.negative': [], 'check.response.body.positive': [], 'response.include_body': 'on_error' as ResponseBodyIndexPolicy, @@ -43,6 +48,7 @@ const testHTTPConfig: Partial = { 'check.request.method': 'GET', 'ssl.verification_mode': VerificationMode.NONE, username: '', + params: '{"proxyUrl":"https://www.google.com"}', }; const testBrowserConfig: Partial = { @@ -82,11 +88,15 @@ const testBrowserConfig: Partial = { }; describe('formatMonitorConfig', () => { + const logger = loggerMock.create(); + describe('http fields', () => { it('sets https keys properly', () => { - const yamlConfig = formatMonitorConfig( + const yamlConfig = formatMonitorConfigFields( Object.keys(testHTTPConfig) as ConfigKey[], - testHTTPConfig + testHTTPConfig, + logger, + { proxyUrl: 'https://www.google.com' } ); expect(yamlConfig).toEqual({ @@ -102,16 +112,22 @@ describe('formatMonitorConfig', () => { timeout: '16s', type: 'http', urls: 'https://www.google.com', + proxy_url: 'https://www.google.com', }); }); it.each([true, false])( 'omits ssl fields when tls is disabled and includes ssl fields when enabled', (isTLSEnabled) => { - const yamlConfig = formatMonitorConfig(Object.keys(testHTTPConfig) as ConfigKey[], { - ...testHTTPConfig, - [ConfigKey.METADATA]: { is_tls_enabled: isTLSEnabled }, - }); + const yamlConfig = formatMonitorConfigFields( + Object.keys(testHTTPConfig) as ConfigKey[], + { + ...testHTTPConfig, + [ConfigKey.METADATA]: { is_tls_enabled: isTLSEnabled }, + }, + logger, + { proxyUrl: 'https://www.google.com' } + ); expect(yamlConfig).toEqual({ 'check.request.method': 'GET', @@ -120,6 +136,7 @@ describe('formatMonitorConfig', () => { max_redirects: '0', name: 'Test', password: '3z9SBOQWW5F0UrdqLVFqlF6z', + proxy_url: 'https://www.google.com', 'response.include_body': 'on_error', 'response.include_headers': true, schedule: '@every 3m', @@ -135,6 +152,7 @@ describe('formatMonitorConfig', () => { describe('browser fields', () => { let formattedBrowserConfig: Record; + const logger = loggerMock.create(); beforeEach(() => { formattedBrowserConfig = { @@ -165,20 +183,27 @@ describe('browser fields', () => { }); it('sets browser keys properly', () => { - const yamlConfig = formatMonitorConfig( + const yamlConfig = formatMonitorConfigFields( Object.keys(testBrowserConfig) as ConfigKey[], - testBrowserConfig + testBrowserConfig, + logger, + { proxyUrl: 'https://www.google.com' } ); expect(yamlConfig).toEqual(formattedBrowserConfig); }); it('does not set empty strings or empty objects for params and playwright options', () => { - const yamlConfig = formatMonitorConfig(Object.keys(testBrowserConfig) as ConfigKey[], { - ...testBrowserConfig, - playwright_options: '{}', - params: '', - }); + const yamlConfig = formatMonitorConfigFields( + Object.keys(testBrowserConfig) as ConfigKey[], + { + ...testBrowserConfig, + playwright_options: '{}', + params: '', + }, + logger, + { proxyUrl: 'https://www.google.com' } + ); expect(yamlConfig).toEqual(omit(formattedBrowserConfig, ['params', 'playwright_options'])); }); @@ -187,9 +212,11 @@ describe('browser fields', () => { testBrowserConfig['throttling.is_enabled'] = false; testBrowserConfig['throttling.upload_speed'] = '3'; - const formattedConfig = formatMonitorConfig( + const formattedConfig = formatMonitorConfigFields( Object.keys(testBrowserConfig) as ConfigKey[], - testBrowserConfig + testBrowserConfig, + logger, + { proxyUrl: 'https://www.google.com' } ); const expected = { @@ -205,9 +232,11 @@ describe('browser fields', () => { it('excludes empty array values', () => { testBrowserConfig['filter_journeys.tags'] = []; - const formattedConfig = formatMonitorConfig( + const formattedConfig = formatMonitorConfigFields( Object.keys(testBrowserConfig) as ConfigKey[], - testBrowserConfig + testBrowserConfig, + logger, + { proxyUrl: 'https://www.google.com' } ); const expected = { @@ -221,9 +250,11 @@ describe('browser fields', () => { it('does not exclude "false" fields', () => { testBrowserConfig.enabled = false; - const formattedConfig = formatMonitorConfig( + const formattedConfig = formatMonitorConfigFields( Object.keys(testBrowserConfig) as ConfigKey[], - testBrowserConfig + testBrowserConfig, + logger, + { proxyUrl: 'https://www.google.com' } ); const expected = { ...formattedConfig, enabled: false }; @@ -236,12 +267,14 @@ describe('formatHeartbeatRequest', () => { it('uses heartbeat id', () => { const monitorId = 'test-monitor-id'; const heartbeatId = 'test-custom-heartbeat-id'; - const actual = formatHeartbeatRequest({ - monitor: testBrowserConfig as SyntheticsMonitor, - monitorId, - heartbeatId, - params: {}, - }); + const actual = formatHeartbeatRequest( + { + monitor: testBrowserConfig as SyntheticsMonitor, + configId: monitorId, + heartbeatId, + }, + '{"a":"param"}' + ); expect(actual).toEqual({ ...testBrowserConfig, id: heartbeatId, @@ -258,12 +291,14 @@ describe('formatHeartbeatRequest', () => { it('uses monitor id when custom heartbeat id is not defined', () => { const monitorId = 'test-monitor-id'; - const actual = formatHeartbeatRequest({ - monitor: testBrowserConfig as SyntheticsMonitor, - monitorId, - heartbeatId: monitorId, - params: {}, - }); + const actual = formatHeartbeatRequest( + { + monitor: testBrowserConfig as SyntheticsMonitor, + configId: monitorId, + heartbeatId: monitorId, + }, + JSON.stringify({ key: 'value' }) + ); expect(actual).toEqual({ ...testBrowserConfig, id: monitorId, @@ -275,6 +310,7 @@ describe('formatHeartbeatRequest', () => { test_run_id: undefined, }, fields_under_root: true, + params: '{"key":"value"}', }); }); @@ -283,9 +319,8 @@ describe('formatHeartbeatRequest', () => { const monitor = { ...testBrowserConfig, project_id: undefined } as SyntheticsMonitor; const actual = formatHeartbeatRequest({ monitor, - monitorId, + configId: monitorId, heartbeatId: monitorId, - params: {}, }); expect(actual).toEqual({ @@ -307,9 +342,8 @@ describe('formatHeartbeatRequest', () => { const monitor = { ...testBrowserConfig, project_id: '' } as SyntheticsMonitor; const actual = formatHeartbeatRequest({ monitor, - monitorId, + configId: monitorId, heartbeatId: monitorId, - params: {}, }); expect(actual).toEqual({ @@ -330,10 +364,9 @@ describe('formatHeartbeatRequest', () => { const monitorId = 'test-monitor-id'; const actual = formatHeartbeatRequest({ monitor: testBrowserConfig as SyntheticsMonitor, - monitorId, + configId: monitorId, runOnce: true, heartbeatId: monitorId, - params: {}, }); expect(actual).toEqual({ @@ -355,10 +388,9 @@ describe('formatHeartbeatRequest', () => { const testRunId = 'beep'; const actual = formatHeartbeatRequest({ monitor: testBrowserConfig as SyntheticsMonitor, - monitorId, + configId: monitorId, testRunId, heartbeatId: monitorId, - params: {}, }); expect(actual).toEqual({ @@ -380,10 +412,9 @@ describe('formatHeartbeatRequest', () => { const testRunId = 'beep'; const actual = formatHeartbeatRequest({ monitor: { ...testBrowserConfig, params: '' } as SyntheticsMonitor, - monitorId, + configId: monitorId, testRunId, heartbeatId: monitorId, - params: {}, }); expect(actual).toEqual({ @@ -401,3 +432,44 @@ describe('formatHeartbeatRequest', () => { }); }); }); + +describe('mixParamsWithGlobalParams', () => { + it('mixes global params with local', () => { + const actual = mixParamsWithGlobalParams( + { + username: 'test-user', + password: 'test-password', + url: 'test-url', + }, + { params: '{"a":"param"}' } as any + ); + expect(actual).toEqual({ + params: { + a: 'param', + password: 'test-password', + url: 'test-url', + username: 'test-user', + }, + str: '{"username":"test-user","password":"test-password","url":"test-url","a":"param"}', + }); + }); + + it('local params gets preference', () => { + const actual = mixParamsWithGlobalParams( + { + username: 'test-user', + password: 'test-password', + url: 'test-url', + }, + { params: '{"username":"superpower-user"}' } as any + ); + expect(actual).toEqual({ + params: { + password: 'test-password', + url: 'test-url', + username: 'superpower-user', + }, + str: '{"username":"superpower-user","password":"test-password","url":"test-url"}', + }); + }); +}); diff --git a/x-pack/plugins/synthetics/server/synthetics_service/formatters/format_configs.ts b/x-pack/plugins/synthetics/server/synthetics_service/formatters/format_configs.ts index 0c2d746690ed3..d9b12c550532f 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/formatters/format_configs.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/formatters/format_configs.ts @@ -6,6 +6,9 @@ */ import { isEmpty, isNil, omitBy } from 'lodash'; +import { Logger } from '@kbn/logging'; +import { PARAMS_KEYS_TO_SKIP } from '../../../common/formatters/format_synthetics_policy'; +import { replaceStringWithParams } from '../../../common/formatters/formatting_utils'; import { BrowserFields, ConfigKey, @@ -39,7 +42,12 @@ const uiToHeartbeatKeyMap = { type YamlKeys = keyof typeof uiToHeartbeatKeyMap; -export const formatMonitorConfig = (configKeys: ConfigKey[], config: Partial) => { +export const formatMonitorConfigFields = ( + configKeys: ConfigKey[], + config: Partial, + logger: Logger, + params: Record +) => { const formattedMonitor = {} as Record; configKeys.forEach((key) => { @@ -50,7 +58,23 @@ export const formatMonitorConfig = (configKeys: ConfigKey[], config: Partial; }; -export const formatHeartbeatRequest = ({ - monitor, - monitorId, - heartbeatId, - runOnce, - testRunId, - params: globalParams, -}: { +export interface ConfigData { monitor: SyntheticsMonitor; - monitorId: string; - heartbeatId: string; + configId: string; + heartbeatId?: string; runOnce?: boolean; testRunId?: string; params: Record; -}): HeartbeatConfig => { +} + +export const formatHeartbeatRequest = ( + { monitor, configId, heartbeatId, runOnce, testRunId }: Omit, + params?: string +): HeartbeatConfig => { const projectId = (monitor as BrowserFields)[ConfigKey.PROJECT_ID]; - let params = { ...(globalParams ?? {}) }; + const heartbeatIdT = heartbeatId ?? monitor[ConfigKey.MONITOR_QUERY_ID]; - let paramsString = ''; + const paramsString = params ?? (monitor as BrowserFields)[ConfigKey.PARAMS]; + + return { + ...monitor, + id: heartbeatIdT, + fields: { + config_id: configId, + 'monitor.project.name': projectId || undefined, + 'monitor.project.id': projectId || undefined, + run_once: runOnce, + test_run_id: testRunId, + }, + fields_under_root: true, + params: monitor.type === 'browser' ? paramsString : '', + }; +}; + +export const mixParamsWithGlobalParams = ( + globalParams: Record, + monitor: SyntheticsMonitor +) => { + let params: Record = { ...(globalParams ?? {}) }; + + const paramsString = ''; try { const monParamsStr = (monitor as BrowserFields)[ConfigKey.PARAMS]; @@ -100,22 +145,14 @@ export const formatHeartbeatRequest = ({ params = { ...params, ...monitorParams }; } - paramsString = isEmpty(params) ? '' : JSON.stringify(params); + if (!isEmpty(params)) { + return { str: JSON.stringify(params), params }; + } else { + return { str: '', params }; + } } catch (e) { // ignore } - return { - ...monitor, - id: heartbeatId, - fields: { - config_id: monitorId, - 'monitor.project.name': projectId || undefined, - 'monitor.project.id': projectId || undefined, - run_once: runOnce, - test_run_id: testRunId, - }, - fields_under_root: true, - params: paramsString, - }; + return { str: paramsString, params }; }; diff --git a/x-pack/plugins/synthetics/server/synthetics_service/formatters/formatting_utils.ts b/x-pack/plugins/synthetics/server/synthetics_service/formatters/formatting_utils.ts new file mode 100644 index 0000000000000..c11595547b05c --- /dev/null +++ b/x-pack/plugins/synthetics/server/synthetics_service/formatters/formatting_utils.ts @@ -0,0 +1,35 @@ +/* + * 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 { ConfigKey, MonitorFields } from '../../../common/runtime_types'; + +type FormatterFn = ( + fields: Partial, + key: ConfigKey +) => string | null | Record | string[]; + +export const arrayFormatter: FormatterFn = (fields, key) => { + const value = (fields[key] as string[]) ?? []; + + return value.length ? value : null; +}; + +export const objectFormatter: FormatterFn = (fields, key) => { + const value = (fields[key] as Record) ?? {}; + + return Object.keys(value).length ? value : null; +}; + +export const stringToObjectFormatter: FormatterFn = (fields, key) => { + const value = fields[key] as string; + try { + const obj = JSON.parse(value || '{}'); + return Object.keys(obj).length ? obj : undefined; + } catch { + return undefined; + } +}; diff --git a/x-pack/plugins/synthetics/server/synthetics_service/formatters/http.ts b/x-pack/plugins/synthetics/server/synthetics_service/formatters/http.ts index 83545eb198ba7..43c1a5e76ea70 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/formatters/http.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/formatters/http.ts @@ -5,34 +5,22 @@ * 2.0. */ -import { Formatter, commonFormatters, objectFormatter, arrayFormatter } from './common'; +import { httpFormatters as basicHttpFormatters } from '../../../common/formatters/http/formatters'; +import { Formatter, commonFormatters } from './common'; import { tlsFormatters } from './tls'; import { ConfigKey, HTTPFields } from '../../../common/runtime_types/monitor_management'; +import { arrayFormatter, objectFormatter } from './formatting_utils'; export type HTTPFormatMap = Record; - export const httpFormatters: HTTPFormatMap = { - [ConfigKey.METADATA]: (fields) => objectFormatter(fields[ConfigKey.METADATA]), - [ConfigKey.URLS]: null, - [ConfigKey.PORT]: null, - [ConfigKey.MAX_REDIRECTS]: null, - [ConfigKey.USERNAME]: null, - [ConfigKey.PASSWORD]: null, - [ConfigKey.PROXY_URL]: null, - [ConfigKey.RESPONSE_BODY_CHECK_NEGATIVE]: (fields) => - arrayFormatter(fields[ConfigKey.RESPONSE_BODY_CHECK_NEGATIVE]), - [ConfigKey.RESPONSE_BODY_CHECK_POSITIVE]: (fields) => - arrayFormatter(fields[ConfigKey.RESPONSE_BODY_CHECK_POSITIVE]), - [ConfigKey.RESPONSE_BODY_INDEX]: null, - [ConfigKey.RESPONSE_HEADERS_CHECK]: (fields) => - objectFormatter(fields[ConfigKey.RESPONSE_HEADERS_CHECK]), - [ConfigKey.RESPONSE_HEADERS_INDEX]: null, - [ConfigKey.RESPONSE_STATUS_CHECK]: (fields) => - arrayFormatter(fields[ConfigKey.RESPONSE_STATUS_CHECK]), + ...basicHttpFormatters, + [ConfigKey.METADATA]: objectFormatter, + [ConfigKey.RESPONSE_BODY_CHECK_NEGATIVE]: arrayFormatter, + [ConfigKey.RESPONSE_BODY_CHECK_POSITIVE]: arrayFormatter, + [ConfigKey.RESPONSE_HEADERS_CHECK]: objectFormatter, + [ConfigKey.RESPONSE_STATUS_CHECK]: arrayFormatter, + [ConfigKey.REQUEST_HEADERS_CHECK]: objectFormatter, [ConfigKey.REQUEST_BODY_CHECK]: (fields) => fields[ConfigKey.REQUEST_BODY_CHECK]?.value || null, - [ConfigKey.REQUEST_HEADERS_CHECK]: (fields) => - objectFormatter(fields[ConfigKey.REQUEST_HEADERS_CHECK]), - [ConfigKey.REQUEST_METHOD_CHECK]: null, ...tlsFormatters, ...commonFormatters, }; diff --git a/x-pack/plugins/synthetics/server/synthetics_service/formatters/icmp.ts b/x-pack/plugins/synthetics/server/synthetics_service/formatters/icmp.ts index 6e71ebfca3ee2..a63c68d088bc1 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/formatters/icmp.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/formatters/icmp.ts @@ -5,13 +5,12 @@ * 2.0. */ -import { Formatter, commonFormatters, secondsToCronFormatter } from './common'; -import { ConfigKey, ICMPFields } from '../../../common/runtime_types/monitor_management'; +import { icmpFormatters as basicICMPFormatters } from '../../../common/formatters/icmp/formatters'; +import { Formatter, commonFormatters } from './common'; +import { ICMPFields } from '../../../common/runtime_types/monitor_management'; export type ICMPFormatMap = Record; - export const icmpFormatters: ICMPFormatMap = { - [ConfigKey.HOSTS]: null, - [ConfigKey.WAIT]: (fields) => secondsToCronFormatter(fields[ConfigKey.WAIT]), ...commonFormatters, + ...basicICMPFormatters, }; diff --git a/x-pack/plugins/synthetics/server/synthetics_service/formatters/index.ts b/x-pack/plugins/synthetics/server/synthetics_service/formatters/index.ts index 1026ff91c9491..972a95814f838 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/formatters/index.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/formatters/index.ts @@ -10,24 +10,9 @@ import { tcpFormatters, TCPFormatMap } from './tcp'; import { icmpFormatters, ICMPFormatMap } from './icmp'; import { browserFormatters, BrowserFormatMap } from './browser'; import { commonFormatters, CommonFormatMap } from './common'; -import { DataStream } from '../../../common/runtime_types'; type Formatters = HTTPFormatMap & TCPFormatMap & ICMPFormatMap & BrowserFormatMap & CommonFormatMap; -interface FormatterMap { - [DataStream.HTTP]: HTTPFormatMap; - [DataStream.ICMP]: ICMPFormatMap; - [DataStream.TCP]: TCPFormatMap; - [DataStream.BROWSER]: BrowserFormatMap; -} - -export const formattersMap: FormatterMap = { - [DataStream.HTTP]: httpFormatters, - [DataStream.ICMP]: icmpFormatters, - [DataStream.TCP]: tcpFormatters, - [DataStream.BROWSER]: browserFormatters, -}; - export const formatters: Formatters = { ...httpFormatters, ...icmpFormatters, diff --git a/x-pack/plugins/synthetics/server/synthetics_service/formatters/tcp.ts b/x-pack/plugins/synthetics/server/synthetics_service/formatters/tcp.ts index 7b89a464039fc..0314f8cd9a00b 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/formatters/tcp.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/formatters/tcp.ts @@ -5,21 +5,15 @@ * 2.0. */ +import { tcpFormatters as baicTCPFormatters } from '../../../common/formatters/tcp/formatters'; import { Formatter, commonFormatters } from './common'; import { tlsFormatters } from './tls'; import { ConfigKey, TCPFields } from '../../../common/runtime_types/monitor_management'; export type TCPFormatMap = Record; - export const tcpFormatters: TCPFormatMap = { - [ConfigKey.METADATA]: null, - [ConfigKey.HOSTS]: null, - [ConfigKey.PORT]: null, - [ConfigKey.PROXY_URL]: null, - [ConfigKey.PROXY_USE_LOCAL_RESOLVER]: null, - [ConfigKey.RESPONSE_RECEIVE_CHECK]: null, - [ConfigKey.REQUEST_SEND_CHECK]: null, - [ConfigKey.URLS]: null, ...tlsFormatters, ...commonFormatters, + ...baicTCPFormatters, + [ConfigKey.METADATA]: null, }; diff --git a/x-pack/plugins/synthetics/server/synthetics_service/formatters/tls.ts b/x-pack/plugins/synthetics/server/synthetics_service/formatters/tls.ts index 3d6faf87643c2..ede084d918f0f 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/formatters/tls.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/formatters/tls.ts @@ -5,7 +5,8 @@ * 2.0. */ -import { arrayFormatter, Formatter } from './common'; +import { arrayFormatter } from './formatting_utils'; +import { Formatter } from './common'; import { ConfigKey, TLSFields } from '../../../common/runtime_types/monitor_management'; type TLSFormatMap = Record; @@ -16,5 +17,5 @@ export const tlsFormatters: TLSFormatMap = { [ConfigKey.TLS_KEY]: null, [ConfigKey.TLS_KEY_PASSPHRASE]: null, [ConfigKey.TLS_VERIFICATION_MODE]: null, - [ConfigKey.TLS_VERSION]: (fields) => arrayFormatter(fields[ConfigKey.TLS_VERSION]), + [ConfigKey.TLS_VERSION]: arrayFormatter, }; diff --git a/x-pack/plugins/synthetics/server/synthetics_service/private_location/synthetics_private_location.test.ts b/x-pack/plugins/synthetics/server/synthetics_service/private_location/synthetics_private_location.test.ts index b39db6a7ba9ac..4d319c714a44b 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/private_location/synthetics_private_location.test.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/private_location/synthetics_private_location.test.ts @@ -108,7 +108,7 @@ describe('SyntheticsPrivateLocation', () => { try { await syntheticsPrivateLocation.createMonitors( - [testConfig], + [{ config: testConfig, globalParams: {} }], {} as unknown as KibanaRequest, savedObjectsClientMock, [mockPrivateLocation], @@ -138,7 +138,7 @@ describe('SyntheticsPrivateLocation', () => { try { await syntheticsPrivateLocation.editMonitors( - [testConfig], + [{ config: testConfig, globalParams: {} }], {} as unknown as KibanaRequest, savedObjectsClientMock, [mockPrivateLocation], @@ -181,7 +181,12 @@ describe('SyntheticsPrivateLocation', () => { }); it('formats monitors stream properly', () => { - const test = formatSyntheticsPolicy(testMonitorPolicy, DataStream.BROWSER, dummyBrowserConfig); + const test = formatSyntheticsPolicy( + testMonitorPolicy, + DataStream.BROWSER, + dummyBrowserConfig, + {} + ); expect(test.formattedPolicy.inputs[3].streams[1]).toStrictEqual({ data_stream: { diff --git a/x-pack/plugins/synthetics/server/synthetics_service/private_location/synthetics_private_location.ts b/x-pack/plugins/synthetics/server/synthetics_service/private_location/synthetics_private_location.ts index 02a1828c1f56c..7ead29564e7ed 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/private_location/synthetics_private_location.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/private_location/synthetics_private_location.ts @@ -47,7 +47,8 @@ export class SyntheticsPrivateLocation { privateLocation: PrivateLocation, savedObjectsClient: SavedObjectsClientContract, newPolicyTemplate: NewPackagePolicy, - spaceId: string + spaceId: string, + globalParams: Record ): NewPackagePolicy | null { if (!savedObjectsClient) { throw new Error('Could not find savedObjectsClient'); @@ -67,13 +68,18 @@ export class SyntheticsPrivateLocation { } newPolicy.namespace = config[ConfigKey.NAMESPACE]; - const { formattedPolicy } = formatSyntheticsPolicy(newPolicy, config.type, { - ...(config as Partial), - config_id: config.fields?.config_id, - location_name: privateLocation.label, - 'monitor.project.id': config.fields?.['monitor.project.name'], - 'monitor.project.name': config.fields?.['monitor.project.name'], - }); + const { formattedPolicy } = formatSyntheticsPolicy( + newPolicy, + config.type, + { + ...(config as Partial), + config_id: config.fields?.config_id, + location_name: privateLocation.label, + 'monitor.project.id': config.fields?.['monitor.project.name'], + 'monitor.project.name': config.fields?.['monitor.project.name'], + }, + globalParams + ); return formattedPolicy; } catch (e) { @@ -93,7 +99,7 @@ export class SyntheticsPrivateLocation { } async createMonitors( - configs: HeartbeatConfig[], + configs: Array<{ config: HeartbeatConfig; globalParams: Record }>, request: KibanaRequest, savedObjectsClient: SavedObjectsClientContract, privateLocations: PrivateLocation[], @@ -112,7 +118,7 @@ export class SyntheticsPrivateLocation { throw new Error(`Unable to create Synthetics package policy for private location`); } - for (const config of configs) { + for (const { config, globalParams } of configs) { try { const { locations } = config; @@ -132,7 +138,8 @@ export class SyntheticsPrivateLocation { location, savedObjectsClient, newPolicyTemplate, - spaceId + spaceId, + globalParams ); if (!newPolicy) { @@ -163,7 +170,7 @@ export class SyntheticsPrivateLocation { } async editMonitors( - configs: HeartbeatConfig[], + configs: Array<{ config: HeartbeatConfig; globalParams: Record }>, request: KibanaRequest, savedObjectsClient: SavedObjectsClientContract, allPrivateLocations: PrivateLocation[], @@ -189,13 +196,13 @@ export class SyntheticsPrivateLocation { const policiesToDelete: string[] = []; const existingPolicies = await this.getExistingPolicies( - configs, + configs.map(({ config }) => config), allPrivateLocations, savedObjectsClient, spaceId ); - for (const config of configs) { + for (const { config, globalParams } of configs) { const { locations } = config; const monitorPrivateLocations = locations.filter((loc) => !loc.isServiceManaged); @@ -211,7 +218,8 @@ export class SyntheticsPrivateLocation { privateLocation, savedObjectsClient, newPolicyTemplate, - spaceId + spaceId, + globalParams ); if (!newPolicy) { diff --git a/x-pack/plugins/synthetics/server/synthetics_service/synthetics_monitor/synthetics_monitor_client.test.ts b/x-pack/plugins/synthetics/server/synthetics_service/synthetics_monitor/synthetics_monitor_client.test.ts index 1a59bb3be7c3a..42aa3791e7ae3 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/synthetics_monitor/synthetics_monitor_client.test.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/synthetics_monitor/synthetics_monitor_client.test.ts @@ -177,7 +177,12 @@ describe('SyntheticsMonitorClient', () => { ); expect(syntheticsService.editConfig).toHaveBeenCalledTimes(1); - expect(syntheticsService.editConfig).toHaveBeenCalledWith(deletePayload); + expect(syntheticsService.editConfig).toHaveBeenCalledWith([ + { + monitor, + configId: id, + }, + ]); expect(syntheticsService.deleteConfigs).toHaveBeenCalledTimes(1); expect(client.privateLocationAPI.editMonitors).toHaveBeenCalledTimes(1); }); @@ -200,45 +205,3 @@ describe('SyntheticsMonitorClient', () => { expect(client.privateLocationAPI.deleteMonitors).toHaveBeenCalledTimes(1); }); }); - -const deletePayload = [ - { - enabled: true, - fields: { - config_id: 'test-id-1', - 'monitor.project.id': undefined, - 'monitor.project.name': undefined, - run_once: undefined, - test_run_id: undefined, - }, - fields_under_root: true, - id: '7af7e2f0-d5dc-11ec-87ac-bdfdb894c53d', - locations: [ - { - geo: { lat: 0, lon: 0 }, - id: 'loc-1', - isServiceManaged: false, - label: 'Location 1', - status: 'ga', - url: 'https://example.com/1', - }, - { - geo: { lat: 0, lon: 0 }, - id: 'loc-2', - isServiceManaged: true, - label: 'Location 2', - status: 'ga', - url: 'https://example.com/2', - }, - ], - max_redirects: '0', - name: 'my mon', - params: '', - password: '', - proxy_url: '', - schedule: { number: '3', unit: 'm' }, - secrets: '{}', - type: 'http', - urls: 'http://google.com', - }, -]; diff --git a/x-pack/plugins/synthetics/server/synthetics_service/synthetics_monitor/synthetics_monitor_client.ts b/x-pack/plugins/synthetics/server/synthetics_service/synthetics_monitor/synthetics_monitor_client.ts index d8d79ea1dfb05..21a57c28f4454 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/synthetics_monitor/synthetics_monitor_client.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/synthetics_monitor/synthetics_monitor_client.ts @@ -15,9 +15,12 @@ import { normalizeSecrets } from '../utils'; import { UptimeServerSetup } from '../../legacy_uptime/lib/adapters'; import { SyntheticsPrivateLocation } from '../private_location/synthetics_private_location'; import { SyntheticsService } from '../synthetics_service'; -import { formatHeartbeatRequest } from '../formatters/format_configs'; import { - ConfigKey, + ConfigData, + formatHeartbeatRequest, + mixParamsWithGlobalParams, +} from '../formatters/format_configs'; +import { MonitorFields, SyntheticsMonitorWithId, HeartbeatConfig, @@ -44,23 +47,30 @@ export class SyntheticsMonitorClient { allPrivateLocations: PrivateLocation[], spaceId: string ) { - const privateConfigs: HeartbeatConfig[] = []; - const publicConfigs: HeartbeatConfig[] = []; + const privateConfigs: Array<{ config: HeartbeatConfig; globalParams: Record }> = + []; + const publicConfigs: ConfigData[] = []; const paramsBySpace = await this.syntheticsService.getSyntheticsParams({ spaceId }); for (const monitorObj of monitors) { const { monitor, id } = monitorObj; - const config = formatHeartbeatRequest({ + const config = { monitor, - monitorId: id, - heartbeatId: monitor[ConfigKey.MONITOR_QUERY_ID], + configId: id, params: paramsBySpace[spaceId], - }); + }; + + const { str: paramsString, params } = mixParamsWithGlobalParams( + paramsBySpace[spaceId], + monitor + ); - const { privateLocations, publicLocations } = this.parseLocations(config); + const formattedConfig = formatHeartbeatRequest(config, paramsString); + + const { privateLocations, publicLocations } = this.parseLocations(formattedConfig); if (privateLocations.length > 0) { - privateConfigs.push(config); + privateConfigs.push({ config: formattedConfig, globalParams: params }); } if (publicLocations.length > 0) { @@ -101,22 +111,30 @@ export class SyntheticsMonitorClient { allPrivateLocations: PrivateLocation[], spaceId: string ) { - const privateConfigs: HeartbeatConfig[] = []; - const publicConfigs: HeartbeatConfig[] = []; - const deletedPublicConfigs: HeartbeatConfig[] = []; + const privateConfigs: Array<{ config: HeartbeatConfig; globalParams: Record }> = + []; + + const publicConfigs: ConfigData[] = []; + const deletedPublicConfigs: ConfigData[] = []; const paramsBySpace = await this.syntheticsService.getSyntheticsParams({ spaceId }); for (const editedMonitor of monitors) { - const editedConfig = formatHeartbeatRequest({ - monitor: editedMonitor.monitor, - monitorId: editedMonitor.id, - heartbeatId: (editedMonitor.monitor as MonitorFields)[ConfigKey.MONITOR_QUERY_ID], + const { str: paramsString, params } = mixParamsWithGlobalParams( + paramsBySpace[spaceId], + editedMonitor.monitor + ); + + const configData = { params: paramsBySpace[spaceId], - }); + monitor: editedMonitor.monitor, + configId: editedMonitor.id, + }; + + const editedConfig = formatHeartbeatRequest(configData, paramsString); const { publicLocations, privateLocations } = this.parseLocations(editedConfig); if (publicLocations.length > 0) { - publicConfigs.push(editedConfig); + publicConfigs.push(configData); } const deletedPublicConfig = this.hasDeletedPublicLocations( @@ -125,11 +143,11 @@ export class SyntheticsMonitorClient { ); if (deletedPublicConfig) { - deletedPublicConfigs.push(deletedPublicConfig); + deletedPublicConfigs.push({ ...deletedPublicConfig, params: paramsBySpace[spaceId] }); } if (privateLocations.length > 0 || this.hasPrivateLocations(editedMonitor.previousMonitor)) { - privateConfigs.push(editedConfig); + privateConfigs.push({ config: editedConfig, globalParams: params }); } } @@ -156,14 +174,14 @@ export class SyntheticsMonitorClient { spaceId: string ) { const privateDeletePromise = this.privateLocationAPI.deleteMonitors( - monitors as SyntheticsMonitorWithId[], + monitors, request, savedObjectsClient, spaceId ); const publicDeletePromise = this.syntheticsService.deleteConfigs( - monitors as SyntheticsMonitorWithId[] + monitors.map((monitor) => ({ monitor, configId: monitor.config_id, params: {} })) ); const [pubicResponse] = await Promise.all([publicDeletePromise, privateDeletePromise]); @@ -191,12 +209,10 @@ export class SyntheticsMonitorClient { const { attributes: normalizedPreviousMonitor } = normalizeSecrets(decryptedPreviousMonitor); normalizedPreviousMonitor.locations = missingPublicLocations; - return formatHeartbeatRequest({ + return { monitor: normalizedPreviousMonitor, - monitorId: normalizedPreviousMonitor.id, - heartbeatId: (normalizedPreviousMonitor as MonitorFields)[ConfigKey.MONITOR_QUERY_ID], - params: {}, - }); + configId: decryptedPreviousMonitor.id, + }; } } @@ -222,19 +238,23 @@ export class SyntheticsMonitorClient { savedObjectsClient: SavedObjectsClientContract; encryptedSavedObjects: EncryptedSavedObjectsPluginStart; }) { - const privateConfigs: HeartbeatConfig[] = []; - const publicConfigs: HeartbeatConfig[] = []; + const privateConfigs: Array<{ config: HeartbeatConfig; globalParams: Record }> = + []; + const publicConfigs: ConfigData[] = []; - const monitors = await this.getAllMonitorConfigs({ encryptedSavedObjects, spaceId }); + const { allConfigs: monitors, paramsBySpace } = await this.getAllMonitorConfigs({ + encryptedSavedObjects, + spaceId, + }); for (const monitor of monitors) { const { publicLocations, privateLocations } = this.parseLocations(monitor); if (publicLocations.length > 0) { - publicConfigs.push(monitor); + publicConfigs.push({ monitor, configId: monitor.config_id, params: {} }); } if (privateLocations.length > 0) { - privateConfigs.push(monitor); + privateConfigs.push({ config: monitor, globalParams: paramsBySpace[monitor.namespace] }); } } if (privateConfigs.length > 0) { @@ -265,7 +285,10 @@ export class SyntheticsMonitorClient { const [paramsBySpace, monitors] = await Promise.all([paramsBySpacePromise, monitorsPromise]); - return this.mixParamsWithMonitors(spaceId, monitors, paramsBySpace); + return { + allConfigs: this.mixParamsWithMonitors(spaceId, monitors, paramsBySpace), + paramsBySpace, + }; } async getAllMonitors({ @@ -309,13 +332,16 @@ export class SyntheticsMonitorClient { for (const monitor of monitors) { const attributes = monitor.attributes as unknown as MonitorFields; + const { str: paramsString } = mixParamsWithGlobalParams(paramsBySpace[spaceId], attributes); + heartbeatConfigs.push( - formatHeartbeatRequest({ - monitor: normalizeSecrets(monitor).attributes, - monitorId: monitor.id, - heartbeatId: attributes[ConfigKey.MONITOR_QUERY_ID], - params: paramsBySpace[spaceId], - }) + formatHeartbeatRequest( + { + monitor: normalizeSecrets(monitor).attributes, + configId: monitor.id, + }, + paramsString + ) ); } diff --git a/x-pack/plugins/synthetics/server/synthetics_service/synthetics_service.test.ts b/x-pack/plugins/synthetics/server/synthetics_service/synthetics_service.test.ts index 7144284e6b4f0..ca8d99a97a86d 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/synthetics_service.test.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/synthetics_service.test.ts @@ -146,7 +146,7 @@ describe('SyntheticsService', () => { (axios as jest.MockedFunction).mockResolvedValue({} as AxiosResponse); - await service.addConfig(payload as HeartbeatConfig); + await service.addConfig({ monitor: payload } as any); expect(axios).toHaveBeenCalledTimes(1); expect(axios).toHaveBeenCalledWith( @@ -165,7 +165,7 @@ describe('SyntheticsService', () => { const payload = getFakePayload([locations[0]]); - await service.editConfig([payload] as HeartbeatConfig[]); + await service.editConfig({ monitor: payload } as any); expect(axios).toHaveBeenCalledTimes(1); expect(axios).toHaveBeenCalledWith( diff --git a/x-pack/plugins/synthetics/server/synthetics_service/synthetics_service.ts b/x-pack/plugins/synthetics/server/synthetics_service/synthetics_service.ts index 7e7b304f5edaa..b1a5ac55dae36 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/synthetics_service.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/synthetics_service.ts @@ -15,6 +15,10 @@ import { TaskManagerStartContract, } from '@kbn/task-manager-plugin/server'; import { Subject } from 'rxjs'; +import { EncryptedSavedObjectsClient } from '@kbn/encrypted-saved-objects-plugin/server'; +import pMap from 'p-map'; +import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common'; +import { ALL_SPACES_ID } from '@kbn/spaces-plugin/common/constants'; import { syntheticsParamType } from '../../common/types/saved_objects'; import { sendErrorTelemetryEvents } from '../routes/telemetry/monitor_upgrade_sender'; import { UptimeServerSetup } from '../legacy_uptime/lib/adapters'; @@ -23,15 +27,19 @@ import { getAPIKeyForSyntheticsService } from './get_api_key'; import { syntheticsMonitorType } from '../legacy_uptime/lib/saved_objects/synthetics_monitor'; import { getEsHosts } from './get_es_hosts'; import { ServiceConfig } from '../../common/config'; -import { ServiceAPIClient } from './service_api_client'; -import { formatHeartbeatRequest, formatMonitorConfig } from './formatters/format_configs'; +import { ServiceAPIClient, ServiceData } from './service_api_client'; +import { + ConfigData, + formatHeartbeatRequest, + formatMonitorConfigFields, + mixParamsWithGlobalParams, +} from './formatters/format_configs'; import { ConfigKey, - HeartbeatConfig, + EncryptedSyntheticsMonitor, MonitorFields, ServiceLocationErrors, ServiceLocations, - SyntheticsMonitor, SyntheticsMonitorWithId, SyntheticsMonitorWithSecrets, SyntheticsParam, @@ -250,7 +258,7 @@ export class SyntheticsService { }; } - async addConfig(config: HeartbeatConfig | HeartbeatConfig[]) { + async addConfig(config: ConfigData | ConfigData[]) { try { const monitors = this.formatConfigs(Array.isArray(config) ? config : [config]); @@ -269,7 +277,7 @@ export class SyntheticsService { } } - async editConfig(monitorConfig: HeartbeatConfig | HeartbeatConfig[], isEdit = true) { + async editConfig(monitorConfig: ConfigData | ConfigData[], isEdit = true) { try { const monitors = this.formatConfigs( Array.isArray(monitorConfig) ? monitorConfig : [monitorConfig] @@ -293,27 +301,24 @@ export class SyntheticsService { async pushConfigs() { const service = this; - const subject = new Subject(); - - subject.subscribe(async (monitorConfigs) => { - try { - const monitors = this.formatConfigs(monitorConfigs); - - if (monitors.length === 0) { - this.logger.debug('No monitor found which can be pushed to service.'); - return null; - } + const subject = new Subject(); - const output = await this.getOutput(); + let output: ServiceData['output'] | null = null; + subject.subscribe(async (monitors) => { + try { if (!output) { - sendErrorTelemetryEvents(service.logger, service.server.telemetry, { - reason: 'API key is not valid.', - message: 'Failed to push configs. API key is not valid.', - type: 'invalidApiKey', - stackVersion: service.server.stackVersion, - }); - return; + output = await this.getOutput(); + + if (!output) { + sendErrorTelemetryEvents(service.logger, service.server.telemetry, { + reason: 'API key is not valid.', + message: 'Failed to push configs. API key is not valid.', + type: 'invalidApiKey', + stackVersion: service.server.stackVersion, + }); + return; + } } this.logger.debug(`${monitors.length} monitors will be pushed to synthetics service.`); @@ -338,7 +343,7 @@ export class SyntheticsService { await this.getMonitorConfigs(subject); } - async runOnceConfigs(configs: HeartbeatConfig[]) { + async runOnceConfigs(configs: ConfigData) { const monitors = this.formatConfigs(configs); if (monitors.length === 0) { return; @@ -360,9 +365,9 @@ export class SyntheticsService { } } - async deleteConfigs(configs: SyntheticsMonitorWithId[]) { - const hasPublicLocations = configs.some(({ locations }) => - locations.some(({ isServiceManaged }) => isServiceManaged) + async deleteConfigs(configs: ConfigData[]) { + const hasPublicLocations = configs.some((config) => + config.monitor.locations.some(({ isServiceManaged }) => isServiceManaged) ); if (hasPublicLocations) { @@ -380,16 +385,31 @@ export class SyntheticsService { } async deleteAllConfigs() { - const subject = new Subject(); + const subject = new Subject(); subject.subscribe(async (monitors) => { - await this.deleteConfigs(monitors); + const hasPublicLocations = monitors.some((config) => + config.locations.some(({ isServiceManaged }) => isServiceManaged) + ); + + if (hasPublicLocations) { + const output = await this.getOutput(); + if (!output) { + return; + } + + const data = { + output, + monitors, + }; + return await this.apiClient.delete(data); + } }); await this.getMonitorConfigs(subject); } - async getMonitorConfigs(subject: Subject) { + async getMonitorConfigs(subject: Subject) { const soClient = this.server.savedObjectsClient; const encryptedClient = this.server.encryptedSavedObjects.getClient(); @@ -399,73 +419,86 @@ export class SyntheticsService { const paramsBySpace = await this.getSyntheticsParams(); - const finder = soClient.createPointInTimeFinder({ + const finder = soClient.createPointInTimeFinder({ type: syntheticsMonitorType, - perPage: 500, - namespaces: ['*'], + perPage: 100, + namespaces: [ALL_SPACES_ID], }); - const start = performance.now(); - for await (const result of finder.find()) { - const encryptedMonitors = result.saved_objects; - - const monitors: Array> = ( - await Promise.all( - encryptedMonitors.map( - (monitor) => - new Promise((resolve) => { - encryptedClient - .getDecryptedAsInternalUser( - syntheticsMonitorType, - monitor.id, - { - namespace: monitor.namespaces?.[0], - } - ) - .then((decryptedMonitor) => resolve(decryptedMonitor)) - .catch((e) => { - this.logger.error(e); - sendErrorTelemetryEvents(this.logger, this.server.telemetry, { - reason: 'Failed to decrypt monitor', - message: e?.message, - type: 'runTaskError', - code: e?.code, - status: e.status, - stackVersion: this.server.stackVersion, - }); - resolve(null); - }); - }) - ) - ) - ).filter((monitor) => monitor !== null) as Array>; - - const end = performance.now(); - const duration = end - start; - - this.logger.debug(`Decrypted ${monitors.length} monitors. Took ${duration} milliseconds`, { - event: { - duration, - }, - monitors: monitors.length, + const monitors = await this.decryptMonitors(result.saved_objects, encryptedClient); + + const configDataList: ConfigData[] = (monitors ?? []).map((monitor) => { + const attributes = monitor.attributes as unknown as MonitorFields; + const monitorSpace = monitor.namespaces?.[0] ?? DEFAULT_SPACE_ID; + + const params = paramsBySpace[monitorSpace]; + + return { + params: { ...params, ...(paramsBySpace?.[ALL_SPACES_ID] ?? {}) }, + monitor: normalizeSecrets(monitor).attributes, + configId: monitor.id, + heartbeatId: attributes[ConfigKey.MONITOR_QUERY_ID], + }; }); - subject.next( - (monitors ?? []).map((monitor) => { - const attributes = monitor.attributes as unknown as MonitorFields; - return formatHeartbeatRequest({ - monitor: normalizeSecrets(monitor).attributes, - monitorId: monitor.id, - heartbeatId: attributes[ConfigKey.MONITOR_QUERY_ID], - params: monitor.namespaces - ? paramsBySpace[monitor.namespaces[0]] - : paramsBySpace.default, - }); - }) - ); + const formattedConfigs = this.formatConfigs(configDataList); + + subject.next(formattedConfigs as MonitorFields[]); } + + await finder.close(); + } + + async decryptMonitors( + monitors: Array>, + encryptedClient: EncryptedSavedObjectsClient + ) { + const start = performance.now(); + + const decryptedMonitors = await pMap( + monitors, + (monitor) => + new Promise((resolve) => { + encryptedClient + .getDecryptedAsInternalUser( + syntheticsMonitorType, + monitor.id, + { + namespace: monitor.namespaces?.[0], + } + ) + .then((decryptedMonitor) => resolve(decryptedMonitor)) + .catch((e) => { + this.logger.error(e); + sendErrorTelemetryEvents(this.logger, this.server.telemetry, { + reason: 'Failed to decrypt monitor', + message: e?.message, + type: 'runTaskError', + code: e?.code, + status: e.status, + stackVersion: this.server.stackVersion, + }); + resolve(null); + }); + }) + ); + + const end = performance.now(); + const duration = end - start; + + this.logger.debug(`Decrypted ${monitors.length} monitors. Took ${duration} milliseconds`, { + event: { + duration, + }, + monitors: monitors.length, + }); + + return decryptedMonitors.filter((monitor) => monitor !== null) as Array< + SavedObject + >; } + async getSyntheticsParams({ spaceId }: { spaceId?: string } = {}) { const encryptedClient = this.server.encryptedSavedObjects.getClient(); @@ -492,13 +525,41 @@ export class SyntheticsService { // no need to wait here finder.close(); + if (paramsBySpace[ALL_SPACES_ID]) { + Object.keys(paramsBySpace).forEach((space) => { + if (space !== ALL_SPACES_ID) { + paramsBySpace[space] = Object.assign(paramsBySpace[space], paramsBySpace['*']); + } + }); + if (spaceId) { + paramsBySpace[spaceId] = { + ...(paramsBySpace?.[spaceId] ?? {}), + ...(paramsBySpace?.[ALL_SPACES_ID] ?? {}), + }; + } + } + return paramsBySpace; } - formatConfigs(configs: SyntheticsMonitorWithId[]) { - return configs.map((config: SyntheticsMonitor) => - formatMonitorConfig(Object.keys(config) as ConfigKey[], config as Partial) - ); + formatConfigs(configData: ConfigData[] | ConfigData) { + const configDataList = Array.isArray(configData) ? configData : [configData]; + + return configDataList.map((config) => { + const { str: paramsString, params } = mixParamsWithGlobalParams( + config.params, + config.monitor + ); + + const asHeartbeatConfig = formatHeartbeatRequest(config, paramsString); + + return formatMonitorConfigFields( + Object.keys(asHeartbeatConfig) as ConfigKey[], + asHeartbeatConfig as Partial, + this.logger, + params ?? {} + ); + }); } } diff --git a/x-pack/plugins/synthetics/tsconfig.json b/x-pack/plugins/synthetics/tsconfig.json index 6cffd71042dee..34b20082a0c07 100644 --- a/x-pack/plugins/synthetics/tsconfig.json +++ b/x-pack/plugins/synthetics/tsconfig.json @@ -72,6 +72,7 @@ "@kbn/core-saved-objects-api-server-mocks", "@kbn/core-saved-objects-server", "@kbn/shared-ux-prompt-not-found", + "@kbn/logging", "@kbn/safer-lodash-set", "@kbn/shared-ux-router", "@kbn/alerts-as-data-utils", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.tsx index ec1f298b58ba0..d27abe0831441 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.tsx @@ -48,6 +48,7 @@ const RuleAdd = ({ initialValues, reloadRules, onSave, + hideInterval, metadata: initialMetadata, filteredRuleTypes, ...props @@ -264,6 +265,7 @@ const RuleAdd = ({ ruleTypeRegistry={ruleTypeRegistry} metadata={metadata} filteredRuleTypes={filteredRuleTypes} + hideInterval={hideInterval} onChangeMetaData={onChangeMetaData} /> diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form.tsx index 40a8d7f5722e8..7105ab930beb4 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form.tsx @@ -97,6 +97,7 @@ interface RuleFormProps> { setHasActionsWithBrokenConnector?: (value: boolean) => void; metadata?: MetaData; filteredRuleTypes?: string[]; + hideInterval?: boolean; connectorFeatureId?: string; onChangeMetaData: (metadata: MetaData) => void; } @@ -114,6 +115,7 @@ export const RuleForm = ({ actionTypeRegistry, metadata, filteredRuleTypes: ruleTypeToFilter, + hideInterval, connectorFeatureId = AlertingConnectorFeatureId, onChangeMetaData, }: RuleFormProps) => { @@ -582,50 +584,52 @@ export const RuleForm = ({ ) : null} - - 0} - error={errors['schedule.interval']} - > - - - 0} - value={ruleInterval || ''} - name="interval" - data-test-subj="intervalInput" - onChange={(e) => { - const value = e.target.value; - if (value === '' || INTEGER_REGEX.test(value)) { - const parsedValue = value === '' ? '' : parseInt(value, 10); - setRuleInterval(parsedValue || undefined); - setScheduleProperty('interval', `${parsedValue}${ruleIntervalUnit}`); - } - }} - /> - - - { - setRuleIntervalUnit(e.target.value); - setScheduleProperty('interval', `${ruleInterval}${e.target.value}`); - }} - data-test-subj="intervalInputUnit" - /> - - - - + {hideInterval !== true && ( + + 0} + error={errors['schedule.interval']} + > + + + 0} + value={ruleInterval || ''} + name="interval" + data-test-subj="intervalInput" + onChange={(e) => { + const value = e.target.value; + if (value === '' || INTEGER_REGEX.test(value)) { + const parsedValue = value === '' ? '' : parseInt(value, 10); + setRuleInterval(parsedValue || undefined); + setScheduleProperty('interval', `${parsedValue}${ruleIntervalUnit}`); + } + }} + /> + + + { + setRuleIntervalUnit(e.target.value); + setScheduleProperty('interval', `${ruleInterval}${e.target.value}`); + }} + data-test-subj="intervalInputUnit" + /> + + + + + )} {canShowActions && defaultActionGroupId && diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts index 05cf37aecad84..763d96105a30a 100644 --- a/x-pack/plugins/triggers_actions_ui/public/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/types.ts @@ -408,6 +408,7 @@ export interface RuleEditProps> { onClose: (reason: RuleFlyoutCloseReason, metadata?: MetaData) => void; /** @deprecated use `onSave` as a callback after an alert is saved*/ reloadRules?: () => Promise; + hideInterval?: boolean; onSave?: (metadata?: MetaData) => Promise; metadata?: MetaData; ruleType?: RuleType; @@ -423,6 +424,7 @@ export interface RuleAddProps> { initialValues?: Partial; /** @deprecated use `onSave` as a callback after an alert is saved*/ reloadRules?: () => Promise; + hideInterval?: boolean; onSave?: (metadata?: MetaData) => Promise; metadata?: MetaData; ruleTypeIndex?: RuleTypeIndex; diff --git a/x-pack/test/api_integration/apis/synthetics/sample_data/test_policy.ts b/x-pack/test/api_integration/apis/synthetics/sample_data/test_policy.ts index 278d01b0bd71d..82e19948779bb 100644 --- a/x-pack/test/api_integration/apis/synthetics/sample_data/test_policy.ts +++ b/x-pack/test/api_integration/apis/synthetics/sample_data/test_policy.ts @@ -14,7 +14,8 @@ export const getTestSyntheticsPolicy = ( id: string, locationName?: string, namespace?: string, - isTLSEnabled?: boolean + isTLSEnabled?: boolean, + proxyUrl?: string ): PackagePolicy => ({ id: '2bfd7da0-22ed-11ed-8c6b-09a2d21dfbc3-27337270-22ed-11ed-8c6b-09a2d21dfbc3-default', version: 'WzE2MjYsMV0=', @@ -53,7 +54,7 @@ export const getTestSyntheticsPolicy = ( 'service.name': { value: '', type: 'text' }, timeout: { value: '3ms', type: 'text' }, max_redirects: { value: '3', type: 'integer' }, - proxy_url: { value: 'http://proxy.com', type: 'text' }, + proxy_url: { value: proxyUrl ?? 'http://proxy.com', type: 'text' }, tags: { value: '["tag1","tag2"]', type: 'yaml' }, username: { value: 'test-username', type: 'text' }, password: { value: 'test', type: 'password' }, @@ -103,7 +104,7 @@ export const getTestSyntheticsPolicy = ( schedule: '@every 5m', timeout: '3ms', max_redirects: 3, - proxy_url: 'http://proxy.com', + proxy_url: proxyUrl ?? 'http://proxy.com', tags: ['tag1', 'tag2'], username: 'test-username', password: 'test', diff --git a/x-pack/test/api_integration/apis/synthetics/sync_global_params.ts b/x-pack/test/api_integration/apis/synthetics/sync_global_params.ts index 73fcffe5a67e0..acb6b4250628d 100644 --- a/x-pack/test/api_integration/apis/synthetics/sync_global_params.ts +++ b/x-pack/test/api_integration/apis/synthetics/sync_global_params.ts @@ -20,7 +20,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; import { getFixtureJson } from '../uptime/rest/helper/get_fixture_json'; import { PrivateLocationTestService } from './services/private_location_test_service'; import { getTestBrowserSyntheticsPolicy } from './sample_data/test_browser_policy'; -import { comparePolicies } from './sample_data/test_policy'; +import { comparePolicies, getTestSyntheticsPolicy } from './sample_data/test_policy'; export default function ({ getService }: FtrProviderContext) { describe('SyncGlobalParams', function () { @@ -31,7 +31,12 @@ export default function ({ getService }: FtrProviderContext) { let testFleetPolicyID: string; let _browserMonitorJson: HTTPFields; let browserMonitorJson: HTTPFields; + + let _httpMonitorJson: HTTPFields; + let httpMonitorJson: HTTPFields; + let newMonitorId: string; + let newHttpMonitorId: string; const testPrivateLocations = new PrivateLocationTestService(getService); const params: Record = {}; @@ -45,11 +50,13 @@ export default function ({ getService }: FtrProviderContext) { .expect(200); _browserMonitorJson = getFixtureJson('browser_monitor'); + _httpMonitorJson = getFixtureJson('http_monitor'); await kServer.savedObjects.clean({ types: [syntheticsParamType] }); }); beforeEach(() => { browserMonitorJson = _browserMonitorJson; + httpMonitorJson = _httpMonitorJson; }); const testPolicyName = 'Fleet test server policy' + Date.now(); @@ -183,6 +190,59 @@ export default function ({ getService }: FtrProviderContext) { ); }); + it('add a http monitor using param', async () => { + const newMonitor = httpMonitorJson; + + newMonitor.locations.push({ + id: testFleetPolicyID, + label: 'Test private location 0', + isServiceManaged: false, + }); + + newMonitor.proxy_url = '${test}'; + + const apiResponse = await supertestAPI + .post(API_URLS.SYNTHETICS_MONITORS) + .set('kbn-xsrf', 'true') + .send(newMonitor); + + expect(apiResponse.body.attributes).eql( + omit( + { + ...newMonitor, + [ConfigKey.MONITOR_QUERY_ID]: apiResponse.body.id, + [ConfigKey.CONFIG_ID]: apiResponse.body.id, + }, + secretKeys + ) + ); + newHttpMonitorId = apiResponse.body.id; + }); + + it('parsed params for previously added http monitors', async () => { + const apiResponse = await supertestAPI.get( + '/api/fleet/package_policies?page=1&perPage=2000&kuery=ingest-package-policies.package.name%3A%20synthetics' + ); + + const packagePolicy = apiResponse.body.items.find( + (pkgPolicy: PackagePolicy) => + pkgPolicy.id === newHttpMonitorId + '-' + testFleetPolicyID + '-default' + ); + + expect(packagePolicy.policy_id).eql(testFleetPolicyID); + + const pPolicy = getTestSyntheticsPolicy( + httpMonitorJson.name, + newHttpMonitorId, + undefined, + undefined, + false, + 'test' + ); + + comparePolicies(packagePolicy, pPolicy); + }); + it('delete all params and sync again', async () => { await kServer.savedObjects.clean({ types: [syntheticsParamType] }); const apiResponseK = await supertestAPI diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/artifact_entries_list.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/artifact_entries_list.ts index b52db3c2c266e..00ff378cde2bf 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/artifact_entries_list.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/artifact_entries_list.ts @@ -22,7 +22,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const esClient = getService('es'); const unzipPromisify = promisify(unzip); - describe('For each artifact list under management', function () { + // FLAKY: https://github.com/elastic/kibana/issues/153855 + describe.skip('For each artifact list under management', function () { let indexedData: IndexedHostsAndAlertsResponse; const checkFleetArtifacts = async ( diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_list.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_list.ts index cc46e4c1ebf90..f25f6be491836 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_list.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_list.ts @@ -81,7 +81,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { return tableData; }; - describe('endpoint list', function () { + // FLAKY: https://github.com/elastic/kibana/issues/153855 + describe.skip('endpoint list', function () { const sleep = (ms = 100) => new Promise((resolve) => setTimeout(resolve, ms)); let indexedData: IndexedHostsAndAlertsResponse; describe('when initially navigating to page', () => { diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_permissions.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_permissions.ts index 4b2abd052043a..f76765c3b67ec 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_permissions.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_permissions.ts @@ -20,7 +20,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const endpointTestResources = getService('endpointTestResources'); const policyTestResources = getService('policyTestResources'); - describe('Endpoint permissions:', () => { + // FLAKY: https://github.com/elastic/kibana/issues/153855 + describe.skip('Endpoint permissions:', () => { let indexedData: IndexedHostsAndAlertsResponse; before(async () => { diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_solution_integrations.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_solution_integrations.ts index 32b2ce196d7e5..8f4e71be82b9b 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_solution_integrations.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_solution_integrations.ts @@ -24,7 +24,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const testSubjects = getService('testSubjects'); const pageObjects = getPageObjects(['common', 'timeline']); - describe('App level Endpoint functionality', () => { + // FLAKY: https://github.com/elastic/kibana/issues/153855 + describe.skip('App level Endpoint functionality', () => { let indexedData: IndexedHostsAndAlertsResponse; let indexedAlerts: IndexedEndpointRuleAlerts; let endpointAgentId: string; diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts index da4b754126383..d46c9a997dcb5 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts @@ -25,7 +25,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const policyTestResources = getService('policyTestResources'); const endpointTestResources = getService('endpointTestResources'); - describe('When on the Endpoint Policy Details Page', function () { + // FLAKY: https://github.com/elastic/kibana/issues/153855 + describe.skip('When on the Endpoint Policy Details Page', function () { let indexedData: IndexedHostsAndAlertsResponse; before(async () => { diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/responder.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/responder.ts index 9081ca2ba096b..bd8d8075cb747 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/responder.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/responder.ts @@ -81,7 +81,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { ); }; - describe('Response Actions Responder', function () { + // FLAKY: https://github.com/elastic/kibana/issues/153855 + describe.skip('Response Actions Responder', function () { let indexedData: IndexedHostsAndAlertsResponse; let endpointAgentId: string; diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/trusted_apps_list.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/trusted_apps_list.ts index 53e1e8f6ad7f2..00c196c1b58fe 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/trusted_apps_list.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/trusted_apps_list.ts @@ -16,7 +16,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const endpointTestResources = getService('endpointTestResources'); const policyTestResources = getService('policyTestResources'); - describe('When on the Trusted Apps list', function () { + // FLAKY: https://github.com/elastic/kibana/issues/153855 + describe.skip('When on the Trusted Apps list', function () { let indexedData: IndexedHostsAndAlertsResponse; before(async () => { const endpointPackage = await policyTestResources.getEndpointPackage(); diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/endpoint_response_actions/execute.ts b/x-pack/test/security_solution_endpoint_api_int/apis/endpoint_response_actions/execute.ts index 413c8bd69e828..8ac14f74afe5c 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/endpoint_response_actions/execute.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/endpoint_response_actions/execute.ts @@ -15,7 +15,8 @@ export default function ({ getService }: FtrProviderContext) { const supertestWithoutAuth = getService('supertestWithoutAuth'); const endpointTestResources = getService('endpointTestResources'); - describe('Endpoint `execute` response action', () => { + // FLAKY: https://github.com/elastic/kibana/issues/153855 + describe.skip('Endpoint `execute` response action', () => { let indexedData: IndexedHostsAndAlertsResponse; let agentId = ''; diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/metadata.ts b/x-pack/test/security_solution_endpoint_api_int/apis/metadata.ts index 48b0107a599aa..70b5a32c24643 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/metadata.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/metadata.ts @@ -37,7 +37,8 @@ export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const endpointTestResources = getService('endpointTestResources'); - describe('test metadata apis', () => { + // FLAKY: https://github.com/elastic/kibana/issues/153855 + describe.skip('test metadata apis', () => { before(async () => { await endpointTestResources.setMetadataTransformFrequency('1s'); });