diff --git a/scripts/functional_tests.js b/scripts/functional_tests.js index 685ea8233bec5..3caee022f55b8 100644 --- a/scripts/functional_tests.js +++ b/scripts/functional_tests.js @@ -17,5 +17,4 @@ require('@kbn/test').runTestsCli([ require.resolve('../test/new_visualize_flow/config.ts'), require.resolve('../test/security_functional/config.ts'), require.resolve('../test/functional/config.legacy.ts'), - require.resolve('../test/functional_execution_context/config.ts'), ]); diff --git a/test/functional_execution_context/services.ts b/test/functional_execution_context/services.ts deleted file mode 100644 index b0cf94fedd749..0000000000000 --- a/test/functional_execution_context/services.ts +++ /dev/null @@ -1,11 +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 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { services as functionalServices } from '../functional/services'; - -export const services = functionalServices; diff --git a/test/functional_execution_context/tests/execution_context.ts b/test/functional_execution_context/tests/execution_context.ts deleted file mode 100644 index ad9b4332c9f02..0000000000000 --- a/test/functional_execution_context/tests/execution_context.ts +++ /dev/null @@ -1,374 +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 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ -import type { Ecs, KibanaExecutionContext } from 'kibana/server'; - -import Fs from 'fs/promises'; -import Path from 'path'; -import { isEqual } from 'lodash'; -import type { FtrProviderContext } from '../ftr_provider_context'; - -const logFilePath = Path.resolve(__dirname, '../kibana.log'); - -// to avoid splitting log record containing \n symbol -const endOfLine = /(?<=})\s*\n/; -export default function ({ getService, getPageObjects }: FtrProviderContext) { - const PageObjects = getPageObjects(['common', 'dashboard', 'header', 'home']); - const retry = getService('retry'); - - async function assertLogContains( - description: string, - predicate: (record: Ecs) => boolean - ): Promise { - // logs are written to disk asynchronously. I sacrificed performance to reduce flakiness. - await retry.waitFor(description, async () => { - const logsStr = await Fs.readFile(logFilePath, 'utf-8'); - const normalizedRecords = logsStr - .split(endOfLine) - .filter(Boolean) - .map((s) => JSON.parse(s)); - - return normalizedRecords.some(predicate); - }); - } - - function isExecutionContextLog( - record: string | undefined, - executionContext: KibanaExecutionContext - ) { - if (!record) return false; - try { - const object = JSON.parse(record); - return isEqual(object, executionContext); - } catch (e) { - return false; - } - } - - describe('Execution context service', () => { - before(async () => { - await PageObjects.common.navigateToUrl('home', '/tutorial_directory/sampleData', { - useActualUrl: true, - }); - await PageObjects.home.addSampleDataSet('flights'); - await PageObjects.header.waitUntilLoadingHasFinished(); - }); - - after(async () => { - await PageObjects.common.navigateToUrl('home', '/tutorial_directory/sampleData', { - useActualUrl: true, - }); - await PageObjects.header.waitUntilLoadingHasFinished(); - await PageObjects.home.removeSampleDataSet('flights'); - }); - - describe('discover app', () => { - before(async () => { - await PageObjects.common.navigateToApp('discover'); - await PageObjects.header.waitUntilLoadingHasFinished(); - }); - - it('propagates context for Discover', async () => { - await assertLogContains( - 'execution context propagates to Elasticsearch via "x-opaque-id" header', - (record) => Boolean(record.http?.request?.id?.includes('kibana:application:discover')) - ); - - await assertLogContains('execution context propagates to Kibana logs', (record) => - isExecutionContextLog(record?.message, { - description: 'fetch documents', - id: '', - name: 'discover', - type: 'application', - // discovery doesn't have an URL since one of from the example dataset is not saved separately - url: '/app/discover', - }) - ); - - await assertLogContains('execution context propagates to Kibana logs', (record) => - isExecutionContextLog(record?.message, { - description: 'fetch chart data and total hits', - id: '', - name: 'discover', - type: 'application', - url: '/app/discover', - }) - ); - }); - }); - - describe('dashboard app', () => { - before(async () => { - await PageObjects.common.navigateToApp('dashboard'); - await PageObjects.dashboard.loadSavedDashboard('[Flights] Global Flight Dashboard'); - await PageObjects.dashboard.waitForRenderComplete(); - await PageObjects.header.waitUntilLoadingHasFinished(); - }); - - describe('propagates context for Lens visualizations', () => { - it('lnsXY', async () => { - await assertLogContains( - 'execution context propagates to Elasticsearch via "x-opaque-id" header', - (record) => - Boolean( - record.http?.request?.id?.includes( - 'kibana:application:dashboard:7adfa750-4c81-11e8-b3d7-01146121b73d;lens:lnsXY:086ac2e9-dd16-4b45-92b8-1e43ff7e3f65' - ) - ) - ); - - await assertLogContains('execution context propagates to Kibana logs', (record) => - isExecutionContextLog(record?.message, { - parent: { - type: 'application', - name: 'dashboard', - id: '7adfa750-4c81-11e8-b3d7-01146121b73d', - description: '[Flights] Global Flight Dashboard', - url: '/view/7adfa750-4c81-11e8-b3d7-01146121b73d', - }, - type: 'lens', - name: 'lnsXY', - id: '086ac2e9-dd16-4b45-92b8-1e43ff7e3f65', - description: '[Flights] Flight count', - url: '/app/lens#/edit_by_value', - }) - ); - }); - - it('lnsMetric', async () => { - await assertLogContains( - 'execution context propagates to Elasticsearch via "x-opaque-id" header', - (record) => - Boolean( - record.http?.request?.id?.includes( - 'kibana:application:dashboard:7adfa750-4c81-11e8-b3d7-01146121b73d;lens:lnsMetric:b766e3b8-4544-46ed-99e6-9ecc4847e2a2' - ) - ) - ); - - await assertLogContains('execution context propagates to Kibana logs', (record) => - isExecutionContextLog(record?.message, { - parent: { - type: 'application', - name: 'dashboard', - id: '7adfa750-4c81-11e8-b3d7-01146121b73d', - description: '[Flights] Global Flight Dashboard', - url: '/view/7adfa750-4c81-11e8-b3d7-01146121b73d', - }, - type: 'lens', - name: 'lnsMetric', - id: '2e33ade5-96e5-40b4-b460-493e5d4fa834', - description: '', - url: '/app/lens#/edit_by_value', - }) - ); - }); - - it('lnsDatatable', async () => { - await assertLogContains( - 'execution context propagates to Elasticsearch via "x-opaque-id" header', - (record) => - Boolean( - record.http?.request?.id?.includes( - 'kibana:application:dashboard:7adfa750-4c81-11e8-b3d7-01146121b73d;lens:lnsDatatable:fb86b32f-fb7a-45cf-9511-f366fef51bbd' - ) - ) - ); - - await assertLogContains('execution context propagates to Kibana logs', (record) => - isExecutionContextLog(record?.message, { - parent: { - type: 'application', - name: 'dashboard', - id: '7adfa750-4c81-11e8-b3d7-01146121b73d', - description: '[Flights] Global Flight Dashboard', - url: '/view/7adfa750-4c81-11e8-b3d7-01146121b73d', - }, - type: 'lens', - name: 'lnsDatatable', - id: 'fb86b32f-fb7a-45cf-9511-f366fef51bbd', - description: 'Cities by delay, cancellation', - url: '/app/lens#/edit_by_value', - }) - ); - }); - - it('lnsPie', async () => { - await assertLogContains( - 'execution context propagates to Elasticsearch via "x-opaque-id" header', - (record) => - Boolean( - record.http?.request?.id?.includes( - 'kibana:application:dashboard:7adfa750-4c81-11e8-b3d7-01146121b73d;lens:lnsPie:5d53db36-2d5a-4adc-af7b-cec4c1a294e0' - ) - ) - ); - await assertLogContains('execution context propagates to Kibana logs', (record) => - isExecutionContextLog(record?.message, { - parent: { - type: 'application', - name: 'dashboard', - id: '7adfa750-4c81-11e8-b3d7-01146121b73d', - description: '[Flights] Global Flight Dashboard', - url: '/view/7adfa750-4c81-11e8-b3d7-01146121b73d', - }, - type: 'lens', - name: 'lnsPie', - id: '5d53db36-2d5a-4adc-af7b-cec4c1a294e0', - description: '[Flights] Delay Type', - url: '/app/lens#/edit_by_value', - }) - ); - }); - }); - - it('propagates context for built-in Discover', async () => { - await assertLogContains( - 'execution context propagates to Elasticsearch via "x-opaque-id" header', - (record) => - Boolean( - record.http?.request?.id?.includes( - 'application:dashboard:7adfa750-4c81-11e8-b3d7-01146121b73d;search:discover:571aaf70-4c88-11e8-b3d7-01146121b73d' - ) - ) - ); - await assertLogContains('execution context propagates to Kibana logs', (record) => - isExecutionContextLog(record?.message, { - parent: { - type: 'application', - name: 'dashboard', - id: '7adfa750-4c81-11e8-b3d7-01146121b73d', - description: '[Flights] Global Flight Dashboard', - url: '/view/7adfa750-4c81-11e8-b3d7-01146121b73d', - }, - type: 'search', - name: 'discover', - id: '571aaf70-4c88-11e8-b3d7-01146121b73d', - description: '[Flights] Flight Log', - url: '/app/discover#/view/571aaf70-4c88-11e8-b3d7-01146121b73d', - }) - ); - }); - - it('propagates context for TSVB visualizations', async () => { - await assertLogContains( - 'execution context propagates to Elasticsearch via "x-opaque-id" header', - (record) => - Boolean( - record.http?.request?.id?.includes( - 'kibana:application:dashboard:7adfa750-4c81-11e8-b3d7-01146121b73d;visualization:TSVB:bcb63b50-4c89-11e8-b3d7-01146121b73d' - ) - ) - ); - - await assertLogContains('execution context propagates to Kibana logs', (record) => - isExecutionContextLog(record?.message, { - parent: { - type: 'application', - name: 'dashboard', - id: '7adfa750-4c81-11e8-b3d7-01146121b73d', - description: '[Flights] Global Flight Dashboard', - url: '/view/7adfa750-4c81-11e8-b3d7-01146121b73d', - }, - type: 'visualization', - name: 'TSVB', - id: 'bcb63b50-4c89-11e8-b3d7-01146121b73d', - description: '[Flights] Delays & Cancellations', - url: '/app/visualize#/edit/bcb63b50-4c89-11e8-b3d7-01146121b73d', - }) - ); - }); - - it('propagates context for Vega visualizations', async () => { - await assertLogContains( - 'execution context propagates to Elasticsearch via "x-opaque-id" header', - (record) => - Boolean( - record.http?.request?.id?.includes( - 'kibana:application:dashboard:7adfa750-4c81-11e8-b3d7-01146121b73d;visualization:Vega:ed78a660-53a0-11e8-acbd-0be0ad9d822b' - ) - ) - ); - - await assertLogContains('execution context propagates to Kibana logs', (record) => - isExecutionContextLog(record?.message, { - parent: { - type: 'application', - name: 'dashboard', - id: '7adfa750-4c81-11e8-b3d7-01146121b73d', - description: '[Flights] Global Flight Dashboard', - url: '/view/7adfa750-4c81-11e8-b3d7-01146121b73d', - }, - type: 'visualization', - name: 'Vega', - id: 'ed78a660-53a0-11e8-acbd-0be0ad9d822b', - description: '[Flights] Airport Connections (Hover Over Airport)', - url: '/app/visualize#/edit/ed78a660-53a0-11e8-acbd-0be0ad9d822b', - }) - ); - }); - - it('propagates context for Tag Cloud visualization', async () => { - await assertLogContains( - 'execution context propagates to Elasticsearch via "x-opaque-id" header', - (record) => - Boolean( - record.http?.request?.id?.includes( - 'kibana:application:dashboard:7adfa750-4c81-11e8-b3d7-01146121b73d;visualization:Tag cloud:293b5a30-4c8f-11e8-b3d7-01146121b73d' - ) - ) - ); - - await assertLogContains('execution context propagates to Kibana logs', (record) => - isExecutionContextLog(record?.message, { - parent: { - type: 'application', - name: 'dashboard', - id: '7adfa750-4c81-11e8-b3d7-01146121b73d', - description: '[Flights] Global Flight Dashboard', - url: '/view/7adfa750-4c81-11e8-b3d7-01146121b73d', - }, - type: 'visualization', - name: 'Tag cloud', - id: '293b5a30-4c8f-11e8-b3d7-01146121b73d', - description: '[Flights] Destination Weather', - url: '/app/visualize#/edit/293b5a30-4c8f-11e8-b3d7-01146121b73d', - }) - ); - }); - - it('propagates context for Vertical bar visualization', async () => { - await assertLogContains( - 'execution context propagates to Elasticsearch via "x-opaque-id" header', - (record) => - Boolean( - record.http?.request?.id?.includes( - 'kibana:application:dashboard:7adfa750-4c81-11e8-b3d7-01146121b73d;visualization:Vertical bar:9886b410-4c8b-11e8-b3d7-01146121b73d' - ) - ) - ); - - await assertLogContains('execution context propagates to Kibana logs', (record) => - isExecutionContextLog(record?.message, { - parent: { - type: 'application', - name: 'dashboard', - id: '7adfa750-4c81-11e8-b3d7-01146121b73d', - description: '[Flights] Global Flight Dashboard', - url: '/view/7adfa750-4c81-11e8-b3d7-01146121b73d', - }, - type: 'visualization', - name: 'Vertical bar', - id: '9886b410-4c8b-11e8-b3d7-01146121b73d', - description: '[Flights] Delay Buckets', - url: '/app/visualize#/edit/9886b410-4c8b-11e8-b3d7-01146121b73d', - }) - ); - }); - }); - }); -} diff --git a/x-pack/scripts/functional_tests.js b/x-pack/scripts/functional_tests.js index 43933f6260415..b013b69dcb0ce 100644 --- a/x-pack/scripts/functional_tests.js +++ b/x-pack/scripts/functional_tests.js @@ -92,4 +92,5 @@ require('@kbn/test').runTestsCli([ require.resolve('../test/fleet_functional/config.ts'), require.resolve('../test/examples/config.ts'), require.resolve('../test/performance/config.ts'), + require.resolve('../test/functional_execution_context/config.ts'), ]); diff --git a/test/functional_execution_context/config.ts b/x-pack/test/functional_execution_context/config.ts similarity index 51% rename from test/functional_execution_context/config.ts rename to x-pack/test/functional_execution_context/config.ts index 6e46189073001..f841e8957cde3 100644 --- a/test/functional_execution_context/config.ts +++ b/x-pack/test/functional_execution_context/config.ts @@ -1,16 +1,26 @@ /* * 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 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. */ - -import { FtrConfigProviderContext } from '@kbn/test'; import Path from 'path'; +import { CA_CERT_PATH } from '@kbn/dev-utils'; +import { FtrConfigProviderContext } from '@kbn/test'; +import { logFilePath } from './test_utils'; + +const alertTestPlugin = Path.resolve(__dirname, './fixtures/plugins/alerts'); export default async function ({ readConfigFile }: FtrConfigProviderContext) { - const functionalConfig = await readConfigFile(require.resolve('../functional/config')); + const functionalConfig = await readConfigFile(require.resolve('../../test/functional/config')); + + const servers = { + ...functionalConfig.get('servers'), + elasticsearch: { + ...functionalConfig.get('servers.elasticsearch'), + protocol: 'https', + }, + }; return { ...functionalConfig.getAll(), @@ -19,24 +29,31 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { junit: { reportName: 'Execution Context Functional Tests', }, + servers, + esTestCluster: { + ...functionalConfig.get('esTestCluster'), + ssl: true, + }, kbnTestServer: { ...functionalConfig.get('kbnTestServer'), serverArgs: [ ...functionalConfig.get('kbnTestServer.serverArgs'), + `--plugin-path=${alertTestPlugin}`, + `--elasticsearch.hosts=${servers.elasticsearch.protocol}://${servers.elasticsearch.hostname}:${servers.elasticsearch.port}`, + `--elasticsearch.ssl.certificateAuthorities=${CA_CERT_PATH}`, + '--execution_context.enabled=true', '--logging.appenders.file.type=file', - `--logging.appenders.file.fileName=${Path.resolve(__dirname, './kibana.log')}`, + `--logging.appenders.file.fileName=${logFilePath}`, '--logging.appenders.file.layout.type=json', '--logging.loggers[0].name=elasticsearch.query', '--logging.loggers[0].level=all', - // eslint-disable-next-line prettier/prettier - '--logging.loggers[0].appenders=[\"file\"]', + `--logging.loggers[0].appenders=${JSON.stringify(['file'])}`, '--logging.loggers[1].name=execution_context', '--logging.loggers[1].level=debug', - // eslint-disable-next-line prettier/prettier - '--logging.loggers[1].appenders=[\"file\"]', + `--logging.loggers[1].appenders=${JSON.stringify(['file'])}`, ], }, }; diff --git a/x-pack/test/functional_execution_context/fixtures/plugins/alerts/kibana.json b/x-pack/test/functional_execution_context/fixtures/plugins/alerts/kibana.json new file mode 100644 index 0000000000000..7a51160f20041 --- /dev/null +++ b/x-pack/test/functional_execution_context/fixtures/plugins/alerts/kibana.json @@ -0,0 +1,13 @@ +{ + "id": "alertsFixtures", + "owner": { + "name": "Core team", + "githubTeam": "kibana-core" + }, + "version": "1.0.0", + "kibanaVersion": "kibana", + "configPath": ["xpack"], + "requiredPlugins": ["features", "alerting"], + "server": true, + "ui": false +} diff --git a/x-pack/test/functional_execution_context/fixtures/plugins/alerts/package.json b/x-pack/test/functional_execution_context/fixtures/plugins/alerts/package.json new file mode 100644 index 0000000000000..ac456b01d3493 --- /dev/null +++ b/x-pack/test/functional_execution_context/fixtures/plugins/alerts/package.json @@ -0,0 +1,13 @@ +{ + "name": "alerts-fixtures", + "version": "1.0.0", + "kibana": { + "version": "kibana", + "templateVersion": "1.0.0" + }, + "scripts": { + "kbn": "node ../../../../../../scripts/kbn.js", + "build": "rm -rf './target' && ../../../../../../node_modules/.bin/tsc" + }, + "license": "Elastic License 2.0" +} diff --git a/x-pack/test/functional_execution_context/fixtures/plugins/alerts/server/index.ts b/x-pack/test/functional_execution_context/fixtures/plugins/alerts/server/index.ts new file mode 100644 index 0000000000000..700aee6bfd49d --- /dev/null +++ b/x-pack/test/functional_execution_context/fixtures/plugins/alerts/server/index.ts @@ -0,0 +1,10 @@ +/* + * 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 { FixturePlugin } from './plugin'; + +export const plugin = () => new FixturePlugin(); diff --git a/x-pack/test/functional_execution_context/fixtures/plugins/alerts/server/plugin.ts b/x-pack/test/functional_execution_context/fixtures/plugins/alerts/server/plugin.ts new file mode 100644 index 0000000000000..47a9e4edc30fc --- /dev/null +++ b/x-pack/test/functional_execution_context/fixtures/plugins/alerts/server/plugin.ts @@ -0,0 +1,88 @@ +/* + * 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 { Plugin, CoreSetup } from 'kibana/server'; +import { PluginSetupContract as AlertingPluginSetup } from '../../../../../../plugins/alerting/server/plugin'; +import { EncryptedSavedObjectsPluginStart } from '../../../../../../plugins/encrypted_saved_objects/server'; +import { PluginSetupContract as FeaturesPluginSetup } from '../../../../../../plugins/features/server'; +import { SpacesPluginStart } from '../../../../../../plugins/spaces/server'; +import { SecurityPluginStart } from '../../../../../../plugins/security/server'; + +export interface FixtureSetupDeps { + features: FeaturesPluginSetup; + alerting: AlertingPluginSetup; +} + +export interface FixtureStartDeps { + encryptedSavedObjects: EncryptedSavedObjectsPluginStart; + security?: SecurityPluginStart; + spaces?: SpacesPluginStart; +} + +export class FixturePlugin implements Plugin { + constructor() {} + + public setup(core: CoreSetup, { features, alerting }: FixtureSetupDeps) { + features.registerKibanaFeature({ + id: 'alertsFixture', + name: 'Alerts', + app: ['alerts', 'kibana'], + category: { id: 'foo', label: 'foo' }, + alerting: ['test.executionContext'], + privileges: { + all: { + app: ['alerts', 'kibana'], + savedObject: { + all: ['alert'], + read: [], + }, + alerting: { + rule: { + all: ['test.executionContext'], + }, + }, + ui: [], + }, + read: { + app: ['alerts', 'kibana'], + savedObject: { + all: [], + read: ['alert'], + }, + alerting: { + rule: { + read: ['test.executionContext'], + }, + }, + ui: [], + }, + }, + }); + + alerting.registerType({ + id: 'test.executionContext', + name: 'Test: Query Elasticsearch server', + actionGroups: [ + { + id: 'default', + name: 'Default', + }, + ], + producer: 'alertsFixture', + defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', + isExportable: true, + async executor() { + const [coreStart] = await core.getStartServices(); + await coreStart.elasticsearch.client.asInternalUser.ping(); + }, + }); + } + + public start() {} + public stop() {} +} diff --git a/test/functional_execution_context/ftr_provider_context.ts b/x-pack/test/functional_execution_context/ftr_provider_context.ts similarity index 60% rename from test/functional_execution_context/ftr_provider_context.ts rename to x-pack/test/functional_execution_context/ftr_provider_context.ts index d4ac701735efb..c5aadf858692a 100644 --- a/test/functional_execution_context/ftr_provider_context.ts +++ b/x-pack/test/functional_execution_context/ftr_provider_context.ts @@ -1,13 +1,12 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. */ import { GenericFtrProviderContext } from '@kbn/test'; -import { pageObjects } from '../functional/page_objects'; +import { pageObjects } from '../../test/functional/page_objects'; import { services } from './services'; export type FtrProviderContext = GenericFtrProviderContext; diff --git a/x-pack/test/functional_execution_context/services.ts b/x-pack/test/functional_execution_context/services.ts new file mode 100644 index 0000000000000..e0aaa899deabf --- /dev/null +++ b/x-pack/test/functional_execution_context/services.ts @@ -0,0 +1,10 @@ +/* + * 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 { services as functionalServices } from '../../test/functional/services'; + +export const services = functionalServices; diff --git a/x-pack/test/functional_execution_context/test_utils.ts b/x-pack/test/functional_execution_context/test_utils.ts new file mode 100644 index 0000000000000..94750fa55e964 --- /dev/null +++ b/x-pack/test/functional_execution_context/test_utils.ts @@ -0,0 +1,52 @@ +/* + * 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 Fs from 'fs/promises'; +import Path from 'path'; +import { isEqualWith } from 'lodash'; +import type { Ecs, KibanaExecutionContext } from 'kibana/server'; +import type { RetryService } from '../../../test/common/services/retry'; + +export const logFilePath = Path.resolve(__dirname, './kibana.log'); +export const ANY = Symbol('any'); + +export function isExecutionContextLog( + record: string | undefined, + executionContext: KibanaExecutionContext +) { + if (!record) return false; + try { + const object = JSON.parse(record); + return isEqualWith(object, executionContext, function customizer(obj1: any, obj2: any) { + if (obj2 === ANY) return true; + }); + } catch (e) { + return false; + } +} + +// to avoid splitting log record containing \n symbol +const endOfLine = /(?<=})\s*\n/; +export async function assertLogContains({ + description, + predicate, + retry, +}: { + description: string; + predicate: (record: Ecs) => boolean; + retry: RetryService; +}): Promise { + // logs are written to disk asynchronously. I sacrificed performance to reduce flakiness. + await retry.waitFor(description, async () => { + const logsStr = await Fs.readFile(logFilePath, 'utf-8'); + const normalizedRecords = logsStr + .split(endOfLine) + .filter(Boolean) + .map((s) => JSON.parse(s)); + + return normalizedRecords.some(predicate); + }); +} diff --git a/x-pack/test/functional_execution_context/tests/browser.ts b/x-pack/test/functional_execution_context/tests/browser.ts new file mode 100644 index 0000000000000..9e927dd2bc171 --- /dev/null +++ b/x-pack/test/functional_execution_context/tests/browser.ts @@ -0,0 +1,381 @@ +/* + * 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 type { FtrProviderContext } from '../ftr_provider_context'; +import { assertLogContains, isExecutionContextLog } from '../test_utils'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const PageObjects = getPageObjects(['common', 'dashboard', 'header', 'home']); + const retry = getService('retry'); + + describe('Browser apps', () => { + before(async () => { + await PageObjects.common.navigateToUrl('home', '/tutorial_directory/sampleData', { + useActualUrl: true, + }); + await PageObjects.home.addSampleDataSet('flights'); + await PageObjects.header.waitUntilLoadingHasFinished(); + }); + + after(async () => { + await PageObjects.common.navigateToUrl('home', '/tutorial_directory/sampleData', { + useActualUrl: true, + }); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.home.removeSampleDataSet('flights'); + }); + + describe('discover app', () => { + before(async () => { + await PageObjects.common.navigateToApp('discover'); + await PageObjects.header.waitUntilLoadingHasFinished(); + }); + + it('propagates context for Discover', async () => { + await assertLogContains({ + description: 'execution context propagates to Elasticsearch via "x-opaque-id" header', + predicate: (record) => + Boolean(record.http?.request?.id?.includes('kibana:application:discover')), + retry, + }); + + await assertLogContains({ + description: 'execution context propagates to Kibana logs', + predicate: (record) => + isExecutionContextLog(record?.message, { + description: 'fetch documents', + id: '', + name: 'discover', + type: 'application', + // discovery doesn't have an URL since one of from the example dataset is not saved separately + url: '/app/discover', + }), + retry, + }); + + await assertLogContains({ + description: 'execution context propagates to Kibana logs', + predicate: (record) => + isExecutionContextLog(record?.message, { + description: 'fetch chart data and total hits', + id: '', + name: 'discover', + type: 'application', + url: '/app/discover', + }), + retry, + }); + }); + }); + + describe('dashboard app', () => { + before(async () => { + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.loadSavedDashboard('[Flights] Global Flight Dashboard'); + await PageObjects.dashboard.waitForRenderComplete(); + await PageObjects.header.waitUntilLoadingHasFinished(); + }); + + describe('propagates context for Lens visualizations', () => { + it('lnsXY', async () => { + await assertLogContains({ + description: 'execution context propagates to Elasticsearch via "x-opaque-id" header', + predicate: (record) => + Boolean( + record.http?.request?.id?.includes( + 'kibana:application:dashboard:7adfa750-4c81-11e8-b3d7-01146121b73d;lens:lnsXY:086ac2e9-dd16-4b45-92b8-1e43ff7e3f65' + ) + ), + retry, + }); + + await assertLogContains({ + description: 'execution context propagates to Kibana logs', + predicate: (record) => + isExecutionContextLog(record?.message, { + parent: { + type: 'application', + name: 'dashboard', + id: '7adfa750-4c81-11e8-b3d7-01146121b73d', + description: '[Flights] Global Flight Dashboard', + url: '/view/7adfa750-4c81-11e8-b3d7-01146121b73d', + }, + type: 'lens', + name: 'lnsXY', + id: '086ac2e9-dd16-4b45-92b8-1e43ff7e3f65', + description: '[Flights] Flight count', + url: '/app/lens#/edit_by_value', + }), + retry, + }); + }); + + it('lnsMetric', async () => { + await assertLogContains({ + description: 'execution context propagates to Elasticsearch via "x-opaque-id" header', + predicate: (record) => + Boolean( + record.http?.request?.id?.includes( + 'kibana:application:dashboard:7adfa750-4c81-11e8-b3d7-01146121b73d;lens:lnsMetric:b766e3b8-4544-46ed-99e6-9ecc4847e2a2' + ) + ), + retry, + }); + + await assertLogContains({ + description: 'execution context propagates to Kibana logs', + predicate: (record) => + isExecutionContextLog(record?.message, { + parent: { + type: 'application', + name: 'dashboard', + id: '7adfa750-4c81-11e8-b3d7-01146121b73d', + description: '[Flights] Global Flight Dashboard', + url: '/view/7adfa750-4c81-11e8-b3d7-01146121b73d', + }, + type: 'lens', + name: 'lnsMetric', + id: '2e33ade5-96e5-40b4-b460-493e5d4fa834', + description: '', + url: '/app/lens#/edit_by_value', + }), + retry, + }); + }); + + it('lnsDatatable', async () => { + await assertLogContains({ + description: 'execution context propagates to Elasticsearch via "x-opaque-id" header', + predicate: (record) => + Boolean( + record.http?.request?.id?.includes( + 'kibana:application:dashboard:7adfa750-4c81-11e8-b3d7-01146121b73d;lens:lnsDatatable:fb86b32f-fb7a-45cf-9511-f366fef51bbd' + ) + ), + retry, + }); + + await assertLogContains({ + description: 'execution context propagates to Kibana logs', + predicate: (record) => + isExecutionContextLog(record?.message, { + parent: { + type: 'application', + name: 'dashboard', + id: '7adfa750-4c81-11e8-b3d7-01146121b73d', + description: '[Flights] Global Flight Dashboard', + url: '/view/7adfa750-4c81-11e8-b3d7-01146121b73d', + }, + type: 'lens', + name: 'lnsDatatable', + id: 'fb86b32f-fb7a-45cf-9511-f366fef51bbd', + description: 'Cities by delay, cancellation', + url: '/app/lens#/edit_by_value', + }), + retry, + }); + }); + + it('lnsPie', async () => { + await assertLogContains({ + description: 'execution context propagates to Elasticsearch via "x-opaque-id" header', + predicate: (record) => + Boolean( + record.http?.request?.id?.includes( + 'kibana:application:dashboard:7adfa750-4c81-11e8-b3d7-01146121b73d;lens:lnsPie:5d53db36-2d5a-4adc-af7b-cec4c1a294e0' + ) + ), + retry, + }); + await assertLogContains({ + description: 'execution context propagates to Kibana logs', + predicate: (record) => + isExecutionContextLog(record?.message, { + parent: { + type: 'application', + name: 'dashboard', + id: '7adfa750-4c81-11e8-b3d7-01146121b73d', + description: '[Flights] Global Flight Dashboard', + url: '/view/7adfa750-4c81-11e8-b3d7-01146121b73d', + }, + type: 'lens', + name: 'lnsPie', + id: '5d53db36-2d5a-4adc-af7b-cec4c1a294e0', + description: '[Flights] Delay Type', + url: '/app/lens#/edit_by_value', + }), + retry, + }); + }); + }); + + it('propagates context for built-in Discover', async () => { + await assertLogContains({ + description: 'execution context propagates to Elasticsearch via "x-opaque-id" header', + predicate: (record) => + Boolean( + record.http?.request?.id?.includes( + 'application:dashboard:7adfa750-4c81-11e8-b3d7-01146121b73d;search:discover:571aaf70-4c88-11e8-b3d7-01146121b73d' + ) + ), + retry, + }); + await assertLogContains({ + description: 'execution context propagates to Kibana logs', + predicate: (record) => + isExecutionContextLog(record?.message, { + parent: { + type: 'application', + name: 'dashboard', + id: '7adfa750-4c81-11e8-b3d7-01146121b73d', + description: '[Flights] Global Flight Dashboard', + url: '/view/7adfa750-4c81-11e8-b3d7-01146121b73d', + }, + type: 'search', + name: 'discover', + id: '571aaf70-4c88-11e8-b3d7-01146121b73d', + description: '[Flights] Flight Log', + url: '/app/discover#/view/571aaf70-4c88-11e8-b3d7-01146121b73d', + }), + retry, + }); + }); + + it('propagates context for TSVB visualizations', async () => { + await assertLogContains({ + description: 'execution context propagates to Elasticsearch via "x-opaque-id" header', + predicate: (record) => + Boolean( + record.http?.request?.id?.includes( + 'kibana:application:dashboard:7adfa750-4c81-11e8-b3d7-01146121b73d;visualization:TSVB:bcb63b50-4c89-11e8-b3d7-01146121b73d' + ) + ), + retry, + }); + + await assertLogContains({ + description: 'execution context propagates to Kibana logs', + predicate: (record) => + isExecutionContextLog(record?.message, { + parent: { + type: 'application', + name: 'dashboard', + id: '7adfa750-4c81-11e8-b3d7-01146121b73d', + description: '[Flights] Global Flight Dashboard', + url: '/view/7adfa750-4c81-11e8-b3d7-01146121b73d', + }, + type: 'visualization', + name: 'TSVB', + id: 'bcb63b50-4c89-11e8-b3d7-01146121b73d', + description: '[Flights] Delays & Cancellations', + url: '/app/visualize#/edit/bcb63b50-4c89-11e8-b3d7-01146121b73d', + }), + retry, + }); + }); + + it('propagates context for Vega visualizations', async () => { + await assertLogContains({ + description: 'execution context propagates to Elasticsearch via "x-opaque-id" header', + predicate: (record) => + Boolean( + record.http?.request?.id?.includes( + 'kibana:application:dashboard:7adfa750-4c81-11e8-b3d7-01146121b73d;visualization:Vega:ed78a660-53a0-11e8-acbd-0be0ad9d822b' + ) + ), + retry, + }); + + await assertLogContains({ + description: 'execution context propagates to Kibana logs', + predicate: (record) => + isExecutionContextLog(record?.message, { + parent: { + type: 'application', + name: 'dashboard', + id: '7adfa750-4c81-11e8-b3d7-01146121b73d', + description: '[Flights] Global Flight Dashboard', + url: '/view/7adfa750-4c81-11e8-b3d7-01146121b73d', + }, + type: 'visualization', + name: 'Vega', + id: 'ed78a660-53a0-11e8-acbd-0be0ad9d822b', + description: '[Flights] Airport Connections (Hover Over Airport)', + url: '/app/visualize#/edit/ed78a660-53a0-11e8-acbd-0be0ad9d822b', + }), + retry, + }); + }); + + it('propagates context for Tag Cloud visualization', async () => { + await assertLogContains({ + description: 'execution context propagates to Elasticsearch via "x-opaque-id" header', + predicate: (record) => + Boolean( + record.http?.request?.id?.includes( + 'kibana:application:dashboard:7adfa750-4c81-11e8-b3d7-01146121b73d;visualization:Tag cloud:293b5a30-4c8f-11e8-b3d7-01146121b73d' + ) + ), + retry, + }); + + await assertLogContains({ + description: 'execution context propagates to Kibana logs', + predicate: (record) => + isExecutionContextLog(record?.message, { + parent: { + type: 'application', + name: 'dashboard', + id: '7adfa750-4c81-11e8-b3d7-01146121b73d', + description: '[Flights] Global Flight Dashboard', + url: '/view/7adfa750-4c81-11e8-b3d7-01146121b73d', + }, + type: 'visualization', + name: 'Tag cloud', + id: '293b5a30-4c8f-11e8-b3d7-01146121b73d', + description: '[Flights] Destination Weather', + url: '/app/visualize#/edit/293b5a30-4c8f-11e8-b3d7-01146121b73d', + }), + retry, + }); + }); + + it('propagates context for Vertical bar visualization', async () => { + await assertLogContains({ + description: 'execution context propagates to Elasticsearch via "x-opaque-id" header', + predicate: (record) => + Boolean( + record.http?.request?.id?.includes( + 'kibana:application:dashboard:7adfa750-4c81-11e8-b3d7-01146121b73d;visualization:Vertical bar:9886b410-4c8b-11e8-b3d7-01146121b73d' + ) + ), + retry, + }); + + await assertLogContains({ + description: 'execution context propagates to Kibana logs', + predicate: (record) => + isExecutionContextLog(record?.message, { + parent: { + type: 'application', + name: 'dashboard', + id: '7adfa750-4c81-11e8-b3d7-01146121b73d', + description: '[Flights] Global Flight Dashboard', + url: '/view/7adfa750-4c81-11e8-b3d7-01146121b73d', + }, + type: 'visualization', + name: 'Vertical bar', + id: '9886b410-4c8b-11e8-b3d7-01146121b73d', + description: '[Flights] Delay Buckets', + url: '/app/visualize#/edit/9886b410-4c8b-11e8-b3d7-01146121b73d', + }), + retry, + }); + }); + }); + }); +} diff --git a/test/functional_execution_context/tests/index.ts b/x-pack/test/functional_execution_context/tests/index.ts similarity index 60% rename from test/functional_execution_context/tests/index.ts rename to x-pack/test/functional_execution_context/tests/index.ts index 6dc92f6fb3c8b..6d74a94608671 100644 --- a/test/functional_execution_context/tests/index.ts +++ b/x-pack/test/functional_execution_context/tests/index.ts @@ -1,9 +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 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. */ import { FtrProviderContext } from '../ftr_provider_context'; @@ -11,6 +10,7 @@ import { FtrProviderContext } from '../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('Execution context', function () { this.tags('ciGroup1'); - loadTestFile(require.resolve('./execution_context')); + loadTestFile(require.resolve('./browser')); + loadTestFile(require.resolve('./server')); }); } diff --git a/x-pack/test/functional_execution_context/tests/server.ts b/x-pack/test/functional_execution_context/tests/server.ts new file mode 100644 index 0000000000000..8997c83f4f696 --- /dev/null +++ b/x-pack/test/functional_execution_context/tests/server.ts @@ -0,0 +1,112 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import type { FtrProviderContext } from '../ftr_provider_context'; +import { assertLogContains, isExecutionContextLog, ANY } from '../test_utils'; + +function delay(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +export default function ({ getService }: FtrProviderContext) { + const retry = getService('retry'); + const supertest = getService('supertest'); + const log = getService('log'); + + async function waitForStatus( + id: string, + statuses: Set, + waitMillis: number = 10000 + ): Promise> { + if (waitMillis < 0) { + expect().fail(`waiting for alert ${id} statuses ${Array.from(statuses)} timed out`); + } + + const response = await supertest.get(`/api/alerting/rule/${id}`); + expect(response.status).to.eql(200); + const { status } = response.body.execution_status; + if (statuses.has(status)) return response.body.execution_status; + + log.debug( + `waitForStatus(${Array.from(statuses)} for id:${id}): got ${JSON.stringify( + response.body.execution_status + )}, retrying` + ); + + const WaitForStatusIncrement = 500; + await delay(WaitForStatusIncrement); + return await waitForStatus(id, statuses, waitMillis - WaitForStatusIncrement); + } + + describe('Server-side apps', () => { + it('propagates context for Task and Alerts', async () => { + const { body: createdAlert } = await supertest + .post('/api/alerting/rule') + .set('kbn-xsrf', 'true') + .send({ + enabled: true, + name: 'abc', + tags: ['foo'], + rule_type_id: 'test.executionContext', + consumer: 'alertsFixture', + schedule: { interval: '3s' }, + throttle: '20s', + actions: [], + params: {}, + notify_when: 'onThrottleInterval', + }) + .expect(200); + + const alertId = createdAlert.id; + + await waitForStatus(alertId, new Set(['ok']), 90_000); + + await assertLogContains({ + description: + 'task manager execution context propagates to Elasticsearch via "x-opaque-id" header', + predicate: (record) => + Boolean( + // exclude part with taskId + record.http?.request?.id?.includes( + `kibana:task manager:run alerting:test.executionContext:` + ) + ), + retry, + }); + + await assertLogContains({ + description: + 'alerting execution context propagates to Elasticsearch via "x-opaque-id" header', + predicate: (record) => + Boolean( + record.http?.request?.id?.includes(`alert:execute test.executionContext:${alertId}`) + ), + retry, + }); + + await assertLogContains({ + description: 'execution context propagates to Kibana logs', + predicate: (record) => + isExecutionContextLog(record?.message, { + parent: { + type: 'task manager', + name: 'run alerting:test.executionContext', + // @ts-expect-error. it accepts strings only + id: ANY, + description: 'run task', + }, + type: 'alert', + name: 'execute test.executionContext', + id: alertId, + description: 'execute [test.executionContext] with name [abc] in [default] namespace', + }), + retry, + }); + }); + }); +}