From 3148a1dd11fe67062335079c7298cd76c2562b9c Mon Sep 17 00:00:00 2001 From: "Christiane (Tina) Heiligers" Date: Thu, 3 Feb 2022 12:19:44 -0700 Subject: [PATCH 1/9] Appends the saved objects documents count to the CoreUsageData service (#124308) (#124563) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> (cherry picked from commit ad7c8de75aef8c5890ea4db4e08d1e465cdd8db7) # Conflicts: # docs/development/core/server/kibana-plugin-core-server.md # docs/development/core/server/kibana-plugin-core-server.mergesavedobjectmigrationmaps.md --- ...re-server.mergesavedobjectmigrationmaps.md | 21 ++++++++++++++ .../core_usage_data_service.mock.ts | 1 + .../core_usage_data_service.test.ts | 12 ++++++++ .../core_usage_data_service.ts | 28 +++++++++++++++++-- src/core/server/core_usage_data/types.ts | 1 + src/core/server/server.api.md | 1 + .../collectors/core/core_usage_collector.ts | 10 +++++-- src/plugins/telemetry/schema/oss_plugins.json | 10 +++++-- 8 files changed, 77 insertions(+), 7 deletions(-) create mode 100644 docs/development/core/server/kibana-plugin-core-server.mergesavedobjectmigrationmaps.md diff --git a/docs/development/core/server/kibana-plugin-core-server.mergesavedobjectmigrationmaps.md b/docs/development/core/server/kibana-plugin-core-server.mergesavedobjectmigrationmaps.md new file mode 100644 index 0000000000000..68cd580b57882 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.mergesavedobjectmigrationmaps.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [mergeSavedObjectMigrationMaps](./kibana-plugin-core-server.mergesavedobjectmigrationmaps.md) + +## mergeSavedObjectMigrationMaps variable + +Merges two saved object migration maps. + +If there is a migration for a given version on only one of the maps, that migration function will be used: + +mergeSavedObjectMigrationMaps({ '1.2.3': f }, { '4.5.6': g }) -> { '1.2.3': f, '4.5.6': g } + +If there is a migration for a given version on both maps, the migrations will be composed: + +mergeSavedObjectMigrationMaps({ '1.2.3': f }, { '1.2.3': g }) -> { '1.2.3': (doc, context) => f(g(doc, context), context) } + +Signature: + +```typescript +mergeSavedObjectMigrationMaps: (map1: SavedObjectMigrationMap, map2: SavedObjectMigrationMap) => SavedObjectMigrationMap +``` diff --git a/src/core/server/core_usage_data/core_usage_data_service.mock.ts b/src/core/server/core_usage_data/core_usage_data_service.mock.ts index 941ac5afacb40..d4ba6176cc78b 100644 --- a/src/core/server/core_usage_data/core_usage_data_service.mock.ts +++ b/src/core/server/core_usage_data/core_usage_data_service.mock.ts @@ -138,6 +138,7 @@ const createStartContractMock = () => { alias: 'test_index', primaryStoreSizeBytes: 1, storeSizeBytes: 1, + savedObjectsDocsCount: 1, }, ], legacyUrlAliases: { diff --git a/src/core/server/core_usage_data/core_usage_data_service.test.ts b/src/core/server/core_usage_data/core_usage_data_service.test.ts index 89d83cfdee2b8..bdaa8ae58a807 100644 --- a/src/core/server/core_usage_data/core_usage_data_service.test.ts +++ b/src/core/server/core_usage_data/core_usage_data_service.test.ts @@ -177,6 +177,11 @@ describe('CoreUsageDataService', () => { }, ], } as any); + elasticsearch.client.asInternalUser.count.mockResolvedValueOnce({ + body: { + count: '15', + }, + } as any); elasticsearch.client.asInternalUser.cat.indices.mockResolvedValueOnce({ body: [ { @@ -188,6 +193,11 @@ describe('CoreUsageDataService', () => { }, ], } as any); + elasticsearch.client.asInternalUser.count.mockResolvedValueOnce({ + body: { + count: '10', + }, + } as any); elasticsearch.client.asInternalUser.search.mockResolvedValueOnce({ body: { hits: { total: { value: 6 } }, @@ -343,6 +353,7 @@ describe('CoreUsageDataService', () => { "docsCount": 10, "docsDeleted": 10, "primaryStoreSizeBytes": 2000, + "savedObjectsDocsCount": "15", "storeSizeBytes": 1000, }, Object { @@ -350,6 +361,7 @@ describe('CoreUsageDataService', () => { "docsCount": 20, "docsDeleted": 20, "primaryStoreSizeBytes": 4000, + "savedObjectsDocsCount": "10", "storeSizeBytes": 2000, }, ], diff --git a/src/core/server/core_usage_data/core_usage_data_service.ts b/src/core/server/core_usage_data/core_usage_data_service.ts index 73f63d4d634df..609e7af3946fe 100644 --- a/src/core/server/core_usage_data/core_usage_data_service.ts +++ b/src/core/server/core_usage_data/core_usage_data_service.ts @@ -133,11 +133,11 @@ export class CoreUsageDataService implements CoreService()) .values() - ).map((index) => { + ).map(async (index) => { // The _cat/indices API returns the _index_ and doesn't return a way // to map back from the index to the alias. So we have to make an API - // call for every alias - return elasticsearch.client.asInternalUser.cat + // call for every alias. The document count is the lucene document count. + const catIndicesResults = await elasticsearch.client.asInternalUser.cat .indices({ index, format: 'JSON', @@ -145,6 +145,7 @@ export class CoreUsageDataService implements CoreService { const stats = body[0]; + return { alias: kibanaOrTaskManagerIndex(index, this.kibanaConfig!.index), docsCount: stats['docs.count'] ? parseInt(stats['docs.count'], 10) : 0, @@ -155,6 +156,27 @@ export class CoreUsageDataService implements CoreService/_count API to get the number of saved objects + // to monitor if the cluster will hit the scalling limit of saved object migrations + const savedObjectsCounts = await elasticsearch.client.asInternalUser + .count({ + index, + }) + .then(({ body }) => { + return { + savedObjectsDocsCount: body.count ? body.count : 0, + }; + }); + this.logger.debug( + `Lucene documents count ${catIndicesResults.docsCount} from index ${catIndicesResults.alias}` + ); + this.logger.debug( + `Saved objects documents count ${savedObjectsCounts.savedObjectsDocsCount} from index ${catIndicesResults.alias}` + ); + return { + ...catIndicesResults, + ...savedObjectsCounts, + }; }) ); } diff --git a/src/core/server/core_usage_data/types.ts b/src/core/server/core_usage_data/types.ts index 7d0e9fd362d29..17eade436551d 100644 --- a/src/core/server/core_usage_data/types.ts +++ b/src/core/server/core_usage_data/types.ts @@ -177,6 +177,7 @@ export interface CoreServicesUsageData { docsDeleted: number; storeSizeBytes: number; primaryStoreSizeBytes: number; + savedObjectsDocsCount: number; }[]; legacyUrlAliases: { activeCount: number; diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 00a1cbc44061d..75ec18a04f54e 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -416,6 +416,7 @@ export interface CoreServicesUsageData { docsDeleted: number; storeSizeBytes: number; primaryStoreSizeBytes: number; + savedObjectsDocsCount: number; }[]; legacyUrlAliases: { activeCount: number; diff --git a/src/plugins/kibana_usage_collection/server/collectors/core/core_usage_collector.ts b/src/plugins/kibana_usage_collection/server/collectors/core/core_usage_collector.ts index b1cf0ecd2213e..a208832baf719 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/core/core_usage_collector.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/core/core_usage_collector.ts @@ -355,14 +355,14 @@ export function getCoreUsageCollector( type: 'long', _meta: { description: - 'The number of documents in the index, including hidden nested documents.', + 'The number of lucene documents in the index, including hidden nested documents.', }, }, docsDeleted: { type: 'long', _meta: { description: - 'The number of deleted documents in the index, including hidden nested documents.', + 'The number of deleted lucene documents in the index, including hidden nested documents.', }, }, alias: { @@ -382,6 +382,12 @@ export function getCoreUsageCollector( description: 'The size in bytes of the index, for primaries and replicas.', }, }, + savedObjectsDocsCount: { + type: 'long', + _meta: { + description: 'The number of saved objects documents in the index.', + }, + }, }, }, legacyUrlAliases: { diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index d78adb7e22958..b1d11acb0b225 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -6248,13 +6248,13 @@ "docsCount": { "type": "long", "_meta": { - "description": "The number of documents in the index, including hidden nested documents." + "description": "The number of lucene documents in the index, including hidden nested documents." } }, "docsDeleted": { "type": "long", "_meta": { - "description": "The number of deleted documents in the index, including hidden nested documents." + "description": "The number of deleted lucene documents in the index, including hidden nested documents." } }, "alias": { @@ -6274,6 +6274,12 @@ "_meta": { "description": "The size in bytes of the index, for primaries and replicas." } + }, + "savedObjectsDocsCount": { + "type": "long", + "_meta": { + "description": "The number of saved objects documents in the index." + } } } } From fb43c093d68e671a7ad4aa598fe1ab0a4c9013d9 Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Thu, 3 Feb 2022 14:20:06 -0500 Subject: [PATCH 2/9] [Index Management] Fix cluster_nodes API test (#124564) --- .../apis/management/index_management/cluster_nodes.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/test/api_integration/apis/management/index_management/cluster_nodes.ts b/x-pack/test/api_integration/apis/management/index_management/cluster_nodes.ts index 30c12bf33d763..e885b677aaffb 100644 --- a/x-pack/test/api_integration/apis/management/index_management/cluster_nodes.ts +++ b/x-pack/test/api_integration/apis/management/index_management/cluster_nodes.ts @@ -19,7 +19,7 @@ export default function ({ getService }: FtrProviderContext) { it('should fetch the nodes plugins', async () => { const { body } = await getNodesPlugins().expect(200); - expect(body).eql([]); + expect(Array.isArray(body)).to.be(true); }); }); } From 98d688a93c85f3d259d0445dfa2647ccb7d0e56b Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Thu, 3 Feb 2022 14:41:32 -0500 Subject: [PATCH 3/9] [7.17] [Fleet] Use a docker registry for fleet integration test (#124405) (#124569) * [Fleet] Use a docker registry for fleet integration test (#124405) (cherry picked from commit 4f6be55f0e6322168d2afd9a7b4ff4d3ea04f230) # Conflicts: # x-pack/plugins/fleet/server/integration_tests/reset_preconfiguration.test.ts * Fix merge typo --- .../docker_registry_helper.ts | 69 +++++++++++++++++++ .../reset_preconfiguration.test.ts | 5 ++ 2 files changed, 74 insertions(+) create mode 100644 x-pack/plugins/fleet/server/integration_tests/docker_registry_helper.ts diff --git a/x-pack/plugins/fleet/server/integration_tests/docker_registry_helper.ts b/x-pack/plugins/fleet/server/integration_tests/docker_registry_helper.ts new file mode 100644 index 0000000000000..bb34dc3258d05 --- /dev/null +++ b/x-pack/plugins/fleet/server/integration_tests/docker_registry_helper.ts @@ -0,0 +1,69 @@ +/* + * 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 { spawn } from 'child_process'; +import type { ChildProcess } from 'child_process'; + +import fetch from 'node-fetch'; + +const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +export function useDockerRegistry() { + const packageRegistryPort = process.env.FLEET_PACKAGE_REGISTRY_PORT || '8081'; + + if (!packageRegistryPort.match(/^[0-9]{4}/)) { + throw new Error('Invalid FLEET_PACKAGE_REGISTRY_PORT'); + } + + let dockerProcess: ChildProcess | undefined; + async function startDockerRegistryServer() { + const dockerImage = `docker.elastic.co/package-registry/distribution@sha256:de952debe048d903fc73e8a4472bb48bb95028d440cba852f21b863d47020c61`; + + const args = ['run', '--rm', '-p', `${packageRegistryPort}:8080`, dockerImage]; + + dockerProcess = spawn('docker', args, { stdio: 'inherit' }); + + let isExited = dockerProcess.exitCode !== null; + dockerProcess.once('exit', () => { + isExited = true; + }); + + let retries = 0; + while (!isExited && retries++ <= 20) { + try { + const res = await fetch(`http://localhost:${packageRegistryPort}/`); + if (res.status === 200) { + return; + } + } catch (err) { + // swallow errors + } + + await delay(3000); + } + + dockerProcess.kill(); + throw new Error('Unable to setup docker registry'); + } + + async function cleanupDockerRegistryServer() { + if (dockerProcess && !dockerProcess.killed) { + dockerProcess.kill(); + } + } + + beforeAll(async () => { + jest.setTimeout(5 * 60 * 1000); // 5 minutes timeout + await startDockerRegistryServer(); + }); + + afterAll(async () => { + await cleanupDockerRegistryServer(); + }); + + return `http://localhost:${packageRegistryPort}`; +} diff --git a/x-pack/plugins/fleet/server/integration_tests/reset_preconfiguration.test.ts b/x-pack/plugins/fleet/server/integration_tests/reset_preconfiguration.test.ts index 042341212bd13..87b47a915b19f 100644 --- a/x-pack/plugins/fleet/server/integration_tests/reset_preconfiguration.test.ts +++ b/x-pack/plugins/fleet/server/integration_tests/reset_preconfiguration.test.ts @@ -11,6 +11,8 @@ import * as kbnTestServer from 'src/core/test_helpers/kbn_server'; import type { AgentPolicySOAttributes } from '../types'; +import { useDockerRegistry } from './docker_registry_helper'; + const logFilePath = Path.join(__dirname, 'logs.log'); type Root = ReturnType; @@ -36,6 +38,8 @@ describe.skip('Fleet preconfiguration rest', () => { let esServer: kbnTestServer.TestElasticsearchUtils; let kbnServer: kbnTestServer.TestKibanaUtils; + const registryUrl = useDockerRegistry(); + const startServers = async () => { const { startES } = kbnTestServer.createTestServers({ adjustTimeout: (t) => jest.setTimeout(t), @@ -53,6 +57,7 @@ describe.skip('Fleet preconfiguration rest', () => { { xpack: { fleet: { + registryUrl, // Preconfigure two policies test-12345 and test-456789 agentPolicies: [ { From 6e4de5c6af0adda9869a7f0cfa85c1a3da8e6645 Mon Sep 17 00:00:00 2001 From: Spencer Date: Thu, 3 Feb 2022 19:23:09 -0800 Subject: [PATCH 4/9] [7.17] [ci-stats] send test results to ci-stats service (#123740) (#124651) * [ci-stats] send test results to ci-stats service (#123740) * [ci-stats] send test results to ci-stats service * move export to export type (cherry picked from commit cc0380a4613b27129a888dde673bd2d412d62b73) # Conflicts: # packages/kbn-dev-utils/BUILD.bazel # packages/kbn-test/BUILD.bazel * fix bad merge conflict resolution --- package.json | 1 + .../ci_stats_reporter/ci_stats_reporter.ts | 105 +++++++++--- .../ci_stats_test_group_types.ts | 90 ++++++++++ .../src/ci_stats_reporter/index.ts | 1 + packages/kbn-pm/dist/index.js | 49 +++++- packages/kbn-test/BUILD.bazel | 9 +- packages/kbn-test/jest-preset.js | 6 + .../kbn-test/jest_integration/jest-preset.js | 6 + .../functional_test_runner.ts | 6 +- .../src/functional_test_runner/index.ts | 3 +- .../__fixtures__/failure_hooks/config.js | 1 + .../__fixtures__/simple_project/config.js | 3 + .../lib/config/config.ts | 2 + .../lib/config/schema.ts | 1 + .../lib/failure_metadata.test.ts | 61 ------- .../lib/failure_metadata.ts | 93 ----------- .../src/functional_test_runner/lib/index.ts | 2 +- .../functional_test_runner/lib/lifecycle.ts | 8 + .../mocha/reporter/ci_stats_ftr_reporter.ts | 157 ++++++++++++++++++ .../lib/mocha/reporter/reporter.js | 22 ++- .../lib/test_metadata.ts | 41 +++++ .../functional_test_runner/public_types.ts | 8 +- .../src/jest/ci_stats_jest_reporter.ts | 120 +++++++++++++ packages/kbn-test/src/mocha/index.ts | 2 +- .../functional/services/common/screenshots.ts | 42 ++++- 25 files changed, 629 insertions(+), 210 deletions(-) create mode 100644 packages/kbn-dev-utils/src/ci_stats_reporter/ci_stats_test_group_types.ts delete mode 100644 packages/kbn-test/src/functional_test_runner/lib/failure_metadata.test.ts delete mode 100644 packages/kbn-test/src/functional_test_runner/lib/failure_metadata.ts create mode 100644 packages/kbn-test/src/functional_test_runner/lib/mocha/reporter/ci_stats_ftr_reporter.ts create mode 100644 packages/kbn-test/src/functional_test_runner/lib/test_metadata.ts create mode 100644 packages/kbn-test/src/jest/ci_stats_jest_reporter.ts diff --git a/package.json b/package.json index c60412dd7896d..793f8d3257ea1 100644 --- a/package.json +++ b/package.json @@ -447,6 +447,7 @@ "@emotion/babel-preset-css-prop": "^11.2.0", "@emotion/jest": "^11.3.0", "@istanbuljs/schema": "^0.1.2", + "@jest/console": "^26.6.2", "@jest/reporters": "^26.6.2", "@kbn/babel-code-parser": "link:bazel-bin/packages/kbn-babel-code-parser", "@kbn/babel-preset": "link:bazel-bin/packages/kbn-babel-preset", diff --git a/packages/kbn-dev-utils/src/ci_stats_reporter/ci_stats_reporter.ts b/packages/kbn-dev-utils/src/ci_stats_reporter/ci_stats_reporter.ts index 3dff5acdc228a..f16cdcc80f286 100644 --- a/packages/kbn-dev-utils/src/ci_stats_reporter/ci_stats_reporter.ts +++ b/packages/kbn-dev-utils/src/ci_stats_reporter/ci_stats_reporter.ts @@ -11,16 +11,28 @@ import Os from 'os'; import Fs from 'fs'; import Path from 'path'; import crypto from 'crypto'; + import execa from 'execa'; -import Axios from 'axios'; +import Axios, { AxiosRequestConfig } from 'axios'; // @ts-expect-error not "public", but necessary to prevent Jest shimming from breaking things import httpAdapter from 'axios/lib/adapters/http'; import { ToolingLog } from '../tooling_log'; import { parseConfig, Config } from './ci_stats_config'; +import type { CiStatsTestGroupInfo, CiStatsTestRun } from './ci_stats_test_group_types'; const BASE_URL = 'https://ci-stats.kibana.dev'; +/** Container for metadata that can be attached to different ci-stats objects */ +export interface CiStatsMetadata { + /** + * Arbitrary key-value pairs which can be attached to CiStatsTiming and CiStatsMetric + * objects stored in the ci-stats service + */ + [key: string]: string | string[] | number | boolean | undefined; +} + +/** A ci-stats metric record */ export interface CiStatsMetric { /** Top-level categorization for the metric, e.g. "page load bundle size" */ group: string; @@ -40,13 +52,7 @@ export interface CiStatsMetric { meta?: CiStatsMetadata; } -export interface CiStatsMetadata { - /** - * Arbitrary key-value pairs which can be attached to CiStatsTiming and CiStatsMetric - * objects stored in the ci-stats service - */ - [key: string]: string | string[] | number | boolean | undefined; -} +/** A ci-stats timing event */ export interface CiStatsTiming { /** Top-level categorization for the timing, e.g. "scripts/foo", process type, etc. */ group: string; @@ -58,13 +64,7 @@ export interface CiStatsTiming { meta?: CiStatsMetadata; } -interface ReqOptions { - auth: boolean; - path: string; - body: any; - bodyDesc: string; -} - +/** Options for reporting timings to ci-stats */ export interface TimingsOptions { /** list of timings to record */ timings: CiStatsTiming[]; @@ -74,10 +74,41 @@ export interface TimingsOptions { kibanaUuid?: string | null; } +/** Options for reporting metrics to ci-stats */ export interface MetricsOptions { /** Default metadata to add to each metric */ defaultMeta?: CiStatsMetadata; } + +/** Options for reporting tests to ci-stats */ +export interface CiStatsReportTestsOptions { + /** + * Information about the group of tests that were run + */ + group: CiStatsTestGroupInfo; + /** + * Information about each test that ran, including failure information + */ + testRuns: CiStatsTestRun[]; +} + +/* @internal */ +interface ReportTestsResponse { + buildId: string; + groupId: string; + testRunCount: number; +} + +/* @internal */ +interface ReqOptions { + auth: boolean; + path: string; + body: any; + bodyDesc: string; + query?: AxiosRequestConfig['params']; +} + +/** Object that helps report data to the ci-stats service */ export class CiStatsReporter { /** * Create a CiStatsReporter by inspecting the ENV for the necessary config @@ -86,7 +117,7 @@ export class CiStatsReporter { return new CiStatsReporter(parseConfig(log), log); } - constructor(private config: Config | undefined, private log: ToolingLog) {} + constructor(private readonly config: Config | undefined, private readonly log: ToolingLog) {} /** * Determine if CI_STATS is explicitly disabled by the environment. To determine @@ -165,7 +196,7 @@ export class CiStatsReporter { this.log.debug('CIStatsReporter committerHash: %s', defaultMeta.committerHash); - return await this.req({ + return !!(await this.req({ auth: !!buildId, path: '/v1/timings', body: { @@ -175,7 +206,7 @@ export class CiStatsReporter { timings, }, bodyDesc: timings.length === 1 ? `${timings.length} timing` : `${timings.length} timings`, - }); + })); } /** @@ -188,12 +219,11 @@ export class CiStatsReporter { } const buildId = this.config?.buildId; - if (!buildId) { - throw new Error(`CiStatsReporter can't be authorized without a buildId`); + throw new Error(`metrics can't be reported without a buildId`); } - return await this.req({ + return !!(await this.req({ auth: true, path: '/v1/metrics', body: { @@ -204,6 +234,30 @@ export class CiStatsReporter { bodyDesc: `metrics: ${metrics .map(({ group, id, value }) => `[${group}/${id}=${value}]`) .join(' ')}`, + })); + } + + /** + * Send test reports to ci-stats + */ + async reportTests({ group, testRuns }: CiStatsReportTestsOptions) { + if (!this.config?.buildId || !this.config?.apiToken) { + throw new Error( + 'unable to report tests unless buildId is configured and auth config available' + ); + } + + return await this.req({ + auth: true, + path: '/v1/test_group', + query: { + buildId: this.config?.buildId, + }, + bodyDesc: `[${group.name}/${group.type}] test groups with ${testRuns.length} tests`, + body: [ + JSON.stringify({ group }), + ...testRuns.map((testRun) => JSON.stringify({ testRun })), + ].join('\n'), }); } @@ -241,7 +295,7 @@ export class CiStatsReporter { } } - private async req({ auth, body, bodyDesc, path }: ReqOptions) { + private async req({ auth, body, bodyDesc, path, query }: ReqOptions) { let attempt = 0; const maxAttempts = 5; @@ -251,23 +305,24 @@ export class CiStatsReporter { Authorization: `token ${this.config.apiToken}`, }; } else if (auth) { - throw new Error('this.req() shouldnt be called with auth=true if this.config is defined'); + throw new Error('this.req() shouldnt be called with auth=true if this.config is not defined'); } while (true) { attempt += 1; try { - await Axios.request({ + const resp = await Axios.request({ method: 'POST', url: path, baseURL: BASE_URL, headers, data: body, + params: query, adapter: httpAdapter, }); - return true; + return resp.data; } catch (error) { if (!error?.request) { // not an axios error, must be a usage error that we should notify user about diff --git a/packages/kbn-dev-utils/src/ci_stats_reporter/ci_stats_test_group_types.ts b/packages/kbn-dev-utils/src/ci_stats_reporter/ci_stats_test_group_types.ts new file mode 100644 index 0000000000000..147d4e19325b2 --- /dev/null +++ b/packages/kbn-dev-utils/src/ci_stats_reporter/ci_stats_test_group_types.ts @@ -0,0 +1,90 @@ +/* + * 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 { CiStatsMetadata } from './ci_stats_reporter'; + +export type CiStatsTestResult = 'fail' | 'pass' | 'skip'; +export type CiStatsTestType = + | 'after all hook' + | 'after each hook' + | 'before all hook' + | 'before each hook' + | 'test'; + +export interface CiStatsTestRun { + /** + * ISO-8601 formatted datetime representing when the tests started running + */ + startTime: string; + /** + * Duration of the tests in milliseconds + */ + durationMs: number; + /** + * A sequence number, this is used to order the tests in a specific test run + */ + seq: number; + /** + * The type of this "test run", usually this is just "test" but when reporting issues in hooks it can be set to the type of hook + */ + type: CiStatsTestType; + /** + * "fail", "pass" or "skip", the result of the tests + */ + result: CiStatsTestResult; + /** + * The list of suite names containing this test, the first being the outermost suite + */ + suites: string[]; + /** + * The name of this specific test run + */ + name: string; + /** + * Relative path from the root of the repo contianing this test + */ + file: string; + /** + * Error message if the test failed + */ + error?: string; + /** + * Debug output/stdout produced by the test + */ + stdout?: string; + /** + * Screenshots captured during the test run + */ + screenshots?: Array<{ + name: string; + base64Png: string; + }>; +} + +export interface CiStatsTestGroupInfo { + /** + * ISO-8601 formatted datetime representing when the group of tests started running + */ + startTime: string; + /** + * The number of miliseconds that the tests ran for + */ + durationMs: number; + /** + * The type of tests run in this group, any value is valid but test groups are groupped by type in the UI so use something consistent + */ + type: string; + /** + * The name of this specific group (within the "type") + */ + name: string; + /** + * Arbitrary metadata associated with this group. We currently look for a ciGroup metadata property for highlighting that when appropriate + */ + meta: CiStatsMetadata; +} diff --git a/packages/kbn-dev-utils/src/ci_stats_reporter/index.ts b/packages/kbn-dev-utils/src/ci_stats_reporter/index.ts index 318a2921517f1..cf80d06613dbf 100644 --- a/packages/kbn-dev-utils/src/ci_stats_reporter/index.ts +++ b/packages/kbn-dev-utils/src/ci_stats_reporter/index.ts @@ -10,3 +10,4 @@ export * from './ci_stats_reporter'; export type { Config } from './ci_stats_config'; export * from './ship_ci_stats_cli'; export { getTimeReporter } from './report_time'; +export * from './ci_stats_test_group_types'; diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js index 146c3fe9f8d58..d41bbe2480ebc 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -9048,7 +9048,9 @@ var _ci_stats_config = __webpack_require__(218); */ // @ts-expect-error not "public", but necessary to prevent Jest shimming from breaking things const BASE_URL = 'https://ci-stats.kibana.dev'; +/** Container for metadata that can be attached to different ci-stats objects */ +/** Object that helps report data to the ci-stats service */ class CiStatsReporter { /** * Create a CiStatsReporter by inspecting the ENV for the necessary config @@ -9145,7 +9147,7 @@ class CiStatsReporter { totalMem: _os.default.totalmem() }; this.log.debug('CIStatsReporter committerHash: %s', defaultMeta.committerHash); - return await this.req({ + return !!(await this.req({ auth: !!buildId, path: '/v1/timings', body: { @@ -9155,7 +9157,7 @@ class CiStatsReporter { timings }, bodyDesc: timings.length === 1 ? `${timings.length} timing` : `${timings.length} timings` - }); + })); } /** * Report metrics data to the ci-stats service. If running outside of CI this method @@ -9173,10 +9175,10 @@ class CiStatsReporter { const buildId = (_this$config4 = this.config) === null || _this$config4 === void 0 ? void 0 : _this$config4.buildId; if (!buildId) { - throw new Error(`CiStatsReporter can't be authorized without a buildId`); + throw new Error(`metrics can't be reported without a buildId`); } - return await this.req({ + return !!(await this.req({ auth: true, path: '/v1/metrics', body: { @@ -9189,6 +9191,35 @@ class CiStatsReporter { id, value }) => `[${group}/${id}=${value}]`).join(' ')}` + })); + } + /** + * Send test reports to ci-stats + */ + + + async reportTests({ + group, + testRuns + }) { + var _this$config5, _this$config6, _this$config7; + + if (!((_this$config5 = this.config) !== null && _this$config5 !== void 0 && _this$config5.buildId) || !((_this$config6 = this.config) !== null && _this$config6 !== void 0 && _this$config6.apiToken)) { + throw new Error('unable to report tests unless buildId is configured and auth config available'); + } + + return await this.req({ + auth: true, + path: '/v1/test_group', + query: { + buildId: (_this$config7 = this.config) === null || _this$config7 === void 0 ? void 0 : _this$config7.buildId + }, + bodyDesc: `[${group.name}/${group.type}] test groups with ${testRuns.length} tests`, + body: [JSON.stringify({ + group + }), ...testRuns.map(testRun => JSON.stringify({ + testRun + }))].join('\n') }); } /** @@ -9238,7 +9269,8 @@ class CiStatsReporter { auth, body, bodyDesc, - path + path, + query }) { let attempt = 0; const maxAttempts = 5; @@ -9249,22 +9281,23 @@ class CiStatsReporter { Authorization: `token ${this.config.apiToken}` }; } else if (auth) { - throw new Error('this.req() shouldnt be called with auth=true if this.config is defined'); + throw new Error('this.req() shouldnt be called with auth=true if this.config is not defined'); } while (true) { attempt += 1; try { - await _axios.default.request({ + const resp = await _axios.default.request({ method: 'POST', url: path, baseURL: BASE_URL, headers, data: body, + params: query, adapter: _http.default }); - return true; + return resp.data; } catch (error) { var _error$response; diff --git a/packages/kbn-test/BUILD.bazel b/packages/kbn-test/BUILD.bazel index e8b3348c22663..bc2314dabe54d 100644 --- a/packages/kbn-test/BUILD.bazel +++ b/packages/kbn-test/BUILD.bazel @@ -41,8 +41,10 @@ RUNTIME_DEPS = [ "//packages/kbn-std", "//packages/kbn-utils", "@npm//@elastic/elasticsearch", - "@npm//axios", "@npm//@babel/traverse", + "@npm//@jest/console", + "@npm//@jest/reporters", + "@npm//axios", "@npm//chance", "@npm//del", "@npm//enzyme", @@ -56,7 +58,6 @@ RUNTIME_DEPS = [ "@npm//jest-cli", "@npm//jest-snapshot", "@npm//jest-styled-components", - "@npm//@jest/reporters", "@npm//joi", "@npm//mustache", "@npm//parse-link-header", @@ -77,6 +78,10 @@ TYPES_DEPS = [ "//packages/kbn-i18n", "//packages/kbn-utils", "@npm//@elastic/elasticsearch", + "@npm//@jest/console", + "@npm//@jest/reporters", + "@npm//axios", + "@npm//elastic-apm-node", "@npm//del", "@npm//form-data", "@npm//jest", diff --git a/packages/kbn-test/jest-preset.js b/packages/kbn-test/jest-preset.js index db64f070b37d9..a4886676e1b86 100644 --- a/packages/kbn-test/jest-preset.js +++ b/packages/kbn-test/jest-preset.js @@ -54,6 +54,12 @@ module.exports = { rootDirectory: '.', }, ], + [ + '@kbn/test/target_node/jest/ci_stats_jest_reporter', + { + testGroupType: 'Jest Unit Tests', + }, + ], ], // The paths to modules that run some code to configure or set up the testing environment before each test diff --git a/packages/kbn-test/jest_integration/jest-preset.js b/packages/kbn-test/jest_integration/jest-preset.js index 7504dec9e7a20..be007262477d3 100644 --- a/packages/kbn-test/jest_integration/jest-preset.js +++ b/packages/kbn-test/jest_integration/jest-preset.js @@ -21,6 +21,12 @@ module.exports = { reporters: [ 'default', ['@kbn/test/target_node/jest/junit_reporter', { reportName: 'Jest Integration Tests' }], + [ + '@kbn/test/target_node/jest/ci_stats_jest_reporter', + { + testGroupType: 'Jest Integration Tests', + }, + ], ], coverageReporters: !!process.env.CI ? [['json', { file: 'jest-integration.json' }]] diff --git a/packages/kbn-test/src/functional_test_runner/functional_test_runner.ts b/packages/kbn-test/src/functional_test_runner/functional_test_runner.ts index 27445e68f9537..527c89c969a08 100644 --- a/packages/kbn-test/src/functional_test_runner/functional_test_runner.ts +++ b/packages/kbn-test/src/functional_test_runner/functional_test_runner.ts @@ -13,7 +13,7 @@ import { Suite, Test } from './fake_mocha_types'; import { Lifecycle, LifecyclePhase, - FailureMetadata, + TestMetadata, readConfigFile, ProviderCollection, readProviderSpec, @@ -27,7 +27,7 @@ import { export class FunctionalTestRunner { public readonly lifecycle = new Lifecycle(); - public readonly failureMetadata = new FailureMetadata(this.lifecycle); + public readonly testMetadata = new TestMetadata(this.lifecycle); private closed = false; private readonly esVersion: EsVersion; @@ -181,7 +181,7 @@ export class FunctionalTestRunner { const coreProviders = readProviderSpec('Service', { lifecycle: () => this.lifecycle, log: () => this.log, - failureMetadata: () => this.failureMetadata, + testMetadata: () => this.testMetadata, config: () => config, dockerServers: () => dockerServers, esVersion: () => this.esVersion, diff --git a/packages/kbn-test/src/functional_test_runner/index.ts b/packages/kbn-test/src/functional_test_runner/index.ts index 1718b5f7a4bc5..e67e72fd5801a 100644 --- a/packages/kbn-test/src/functional_test_runner/index.ts +++ b/packages/kbn-test/src/functional_test_runner/index.ts @@ -7,7 +7,8 @@ */ export { FunctionalTestRunner } from './functional_test_runner'; -export { readConfigFile, Config, EsVersion } from './lib'; +export { readConfigFile, Config, EsVersion, Lifecycle, LifecyclePhase } from './lib'; +export type { ScreenshotRecord } from './lib'; export { runFtrCli } from './cli'; export * from './lib/docker_servers'; export * from './public_types'; diff --git a/packages/kbn-test/src/functional_test_runner/integration_tests/__fixtures__/failure_hooks/config.js b/packages/kbn-test/src/functional_test_runner/integration_tests/__fixtures__/failure_hooks/config.js index 0b9cfd88b4cbb..b6aa2669be681 100644 --- a/packages/kbn-test/src/functional_test_runner/integration_tests/__fixtures__/failure_hooks/config.js +++ b/packages/kbn-test/src/functional_test_runner/integration_tests/__fixtures__/failure_hooks/config.js @@ -35,6 +35,7 @@ export default function () { }, mochaReporter: { captureLogOutput: false, + sendToCiStats: false, }, }; } diff --git a/packages/kbn-test/src/functional_test_runner/integration_tests/__fixtures__/simple_project/config.js b/packages/kbn-test/src/functional_test_runner/integration_tests/__fixtures__/simple_project/config.js index 7163058b78523..4c87b53b5753b 100644 --- a/packages/kbn-test/src/functional_test_runner/integration_tests/__fixtures__/simple_project/config.js +++ b/packages/kbn-test/src/functional_test_runner/integration_tests/__fixtures__/simple_project/config.js @@ -10,4 +10,7 @@ import { resolve } from 'path'; export default () => ({ testFiles: [resolve(__dirname, 'tests.js')], + mochaReporter: { + sendToCiStats: false, + }, }); diff --git a/packages/kbn-test/src/functional_test_runner/lib/config/config.ts b/packages/kbn-test/src/functional_test_runner/lib/config/config.ts index 1d4af9c33fb79..d6248b9628e73 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/config/config.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/config/config.ts @@ -20,6 +20,7 @@ interface Options { } export class Config { + public readonly path: string; private [$values]: Record; constructor(options: Options) { @@ -29,6 +30,7 @@ export class Config { throw new TypeError('path is a required option'); } + this.path = path; const { error, value } = schema.validate(settings, { abortEarly: false, context: { diff --git a/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts b/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts index a9ceaa643a60f..e51ebc4538343 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts @@ -152,6 +152,7 @@ export const schema = Joi.object() mochaReporter: Joi.object() .keys({ captureLogOutput: Joi.boolean().default(!!process.env.CI), + sendToCiStats: Joi.boolean().default(!!process.env.CI), }) .default(), diff --git a/packages/kbn-test/src/functional_test_runner/lib/failure_metadata.test.ts b/packages/kbn-test/src/functional_test_runner/lib/failure_metadata.test.ts deleted file mode 100644 index b40f6a5c83688..0000000000000 --- a/packages/kbn-test/src/functional_test_runner/lib/failure_metadata.test.ts +++ /dev/null @@ -1,61 +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 { Lifecycle } from './lifecycle'; -import { FailureMetadata } from './failure_metadata'; -import { Test } from '../fake_mocha_types'; - -it('collects metadata for the current test', async () => { - const lifecycle = new Lifecycle(); - const failureMetadata = new FailureMetadata(lifecycle); - - const test1 = {} as Test; - await lifecycle.beforeEachRunnable.trigger(test1); - failureMetadata.add({ foo: 'bar' }); - - expect(failureMetadata.get(test1)).toMatchInlineSnapshot(` - Object { - "foo": "bar", - } - `); - - const test2 = {} as Test; - await lifecycle.beforeEachRunnable.trigger(test2); - failureMetadata.add({ test: 2 }); - - expect(failureMetadata.get(test1)).toMatchInlineSnapshot(` - Object { - "foo": "bar", - } - `); - expect(failureMetadata.get(test2)).toMatchInlineSnapshot(` - Object { - "test": 2, - } - `); -}); - -it('adds messages to the messages state', () => { - const lifecycle = new Lifecycle(); - const failureMetadata = new FailureMetadata(lifecycle); - - const test1 = {} as Test; - lifecycle.beforeEachRunnable.trigger(test1); - failureMetadata.addMessages(['foo', 'bar']); - failureMetadata.addMessages(['baz']); - - expect(failureMetadata.get(test1)).toMatchInlineSnapshot(` - Object { - "messages": Array [ - "foo", - "bar", - "baz", - ], - } - `); -}); diff --git a/packages/kbn-test/src/functional_test_runner/lib/failure_metadata.ts b/packages/kbn-test/src/functional_test_runner/lib/failure_metadata.ts deleted file mode 100644 index a766c73f4c727..0000000000000 --- a/packages/kbn-test/src/functional_test_runner/lib/failure_metadata.ts +++ /dev/null @@ -1,93 +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 Path from 'path'; - -import { REPO_ROOT } from '@kbn/utils'; - -import { Lifecycle } from './lifecycle'; - -interface Metadata { - [key: string]: unknown; -} - -export class FailureMetadata { - // mocha's global types mean we can't import Mocha or it will override the global jest types.............. - private currentRunnable?: any; - private readonly allMetadata = new Map(); - - constructor(lifecycle: Lifecycle) { - if (!process.env.GCS_UPLOAD_PREFIX && process.env.CI) { - throw new Error( - 'GCS_UPLOAD_PREFIX environment variable is not set and must always be set on CI' - ); - } - - lifecycle.beforeEachRunnable.add((runnable) => { - this.currentRunnable = runnable; - }); - } - - add(metadata: Metadata | ((current: Metadata) => Metadata)) { - if (!this.currentRunnable) { - throw new Error('no current runnable to associate metadata with'); - } - - const current = this.allMetadata.get(this.currentRunnable); - this.allMetadata.set(this.currentRunnable, { - ...current, - ...(typeof metadata === 'function' ? metadata(current || {}) : metadata), - }); - } - - addMessages(messages: string[]) { - this.add((current) => ({ - messages: [...(Array.isArray(current.messages) ? current.messages : []), ...messages], - })); - } - - /** - * @param name Name to label the URL with - * @param repoPath absolute path, within the repo, that will be uploaded - */ - addScreenshot(name: string, repoPath: string) { - const prefix = process.env.GCS_UPLOAD_PREFIX; - - if (!prefix) { - return; - } - - const slash = prefix.endsWith('/') ? '' : '/'; - const urlPath = Path.relative(REPO_ROOT, repoPath) - .split(Path.sep) - .map((c) => encodeURIComponent(c)) - .join('/'); - - if (urlPath.startsWith('..')) { - throw new Error( - `Only call addUploadLink() with paths that are within the repo root, received ${repoPath} and repo root is ${REPO_ROOT}` - ); - } - - const url = `https://storage.googleapis.com/${prefix}${slash}${urlPath}`; - const screenshot = { - name, - url, - }; - - this.add((current) => ({ - screenshots: [...(Array.isArray(current.screenshots) ? current.screenshots : []), screenshot], - })); - - return screenshot; - } - - get(runnable: any) { - return this.allMetadata.get(runnable); - } -} diff --git a/packages/kbn-test/src/functional_test_runner/lib/index.ts b/packages/kbn-test/src/functional_test_runner/lib/index.ts index 98b5fec0597e4..e387fd156fe8a 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/index.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/index.ts @@ -12,7 +12,7 @@ export { readConfigFile, Config } from './config'; export { readProviderSpec, ProviderCollection } from './providers'; // @internal export { runTests, setupMocha } from './mocha'; -export { FailureMetadata } from './failure_metadata'; +export * from './test_metadata'; export * from './docker_servers'; export { SuiteTracker } from './suite_tracker'; diff --git a/packages/kbn-test/src/functional_test_runner/lib/lifecycle.ts b/packages/kbn-test/src/functional_test_runner/lib/lifecycle.ts index 17dcaa8d7447d..e683ec23a8d84 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/lifecycle.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/lifecycle.ts @@ -11,15 +11,23 @@ import { LifecyclePhase } from './lifecycle_phase'; import { Suite, Test } from '../fake_mocha_types'; export class Lifecycle { + /** lifecycle phase that will run handlers once before tests execute */ public readonly beforeTests = new LifecyclePhase<[Suite]>({ singular: true, }); + /** lifecycle phase that runs handlers before each runnable (test and hooks) */ public readonly beforeEachRunnable = new LifecyclePhase<[Test]>(); + /** lifecycle phase that runs handlers before each suite */ public readonly beforeTestSuite = new LifecyclePhase<[Suite]>(); + /** lifecycle phase that runs handlers before each test */ public readonly beforeEachTest = new LifecyclePhase<[Test]>(); + /** lifecycle phase that runs handlers after each suite */ public readonly afterTestSuite = new LifecyclePhase<[Suite]>(); + /** lifecycle phase that runs handlers after a test fails */ public readonly testFailure = new LifecyclePhase<[Error, Test]>(); + /** lifecycle phase that runs handlers after a hook fails */ public readonly testHookFailure = new LifecyclePhase<[Error, Test]>(); + /** lifecycle phase that runs handlers at the very end of execution */ public readonly cleanup = new LifecyclePhase<[]>({ singular: true, }); diff --git a/packages/kbn-test/src/functional_test_runner/lib/mocha/reporter/ci_stats_ftr_reporter.ts b/packages/kbn-test/src/functional_test_runner/lib/mocha/reporter/ci_stats_ftr_reporter.ts new file mode 100644 index 0000000000000..61eb7eccce430 --- /dev/null +++ b/packages/kbn-test/src/functional_test_runner/lib/mocha/reporter/ci_stats_ftr_reporter.ts @@ -0,0 +1,157 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 * as Path from 'path'; + +import { REPO_ROOT } from '@kbn/utils'; +import { CiStatsReporter, CiStatsReportTestsOptions, CiStatsTestType } from '@kbn/dev-utils'; + +import { Config } from '../../config'; +import { Runner } from '../../../fake_mocha_types'; +import { TestMetadata, ScreenshotRecord } from '../../test_metadata'; +import { Lifecycle } from '../../lifecycle'; +import { getSnapshotOfRunnableLogs } from '../../../../mocha'; + +interface Suite { + _beforeAll: Runnable[]; + _beforeEach: Runnable[]; + _afterEach: Runnable[]; + _afterAll: Runnable[]; +} + +interface Runnable { + isFailed(): boolean; + isPending(): boolean; + duration?: number; + titlePath(): string[]; + file: string; + title: string; + parent: Suite; + _screenshots?: ScreenshotRecord[]; +} + +function getHookType(hook: Runnable): CiStatsTestType { + if (hook.parent._afterAll.includes(hook)) { + return 'after all hook'; + } + if (hook.parent._afterEach.includes(hook)) { + return 'after each hook'; + } + if (hook.parent._beforeEach.includes(hook)) { + return 'before each hook'; + } + if (hook.parent._beforeAll.includes(hook)) { + return 'before all hook'; + } + + throw new Error(`unable to determine hook type, hook is not owned by it's parent`); +} + +export function setupCiStatsFtrTestGroupReporter({ + config, + lifecycle, + runner, + testMetadata, + reporter, +}: { + config: Config; + lifecycle: Lifecycle; + runner: Runner; + testMetadata: TestMetadata; + reporter: CiStatsReporter; +}) { + let startMs: number | undefined; + runner.on('start', () => { + startMs = Date.now(); + }); + + const start = Date.now(); + const group: CiStatsReportTestsOptions['group'] = { + startTime: new Date(start).toJSON(), + durationMs: 0, + type: config.path.startsWith('x-pack') ? 'X-Pack Functional Tests' : 'Functional Tests', + name: Path.relative(REPO_ROOT, config.path), + meta: { + ciGroup: config.get('suiteTags.include').find((t: string) => t.startsWith('ciGroup')), + tags: [ + ...config.get('suiteTags.include'), + ...config.get('suiteTags.exclude').map((t: string) => `-${t}`), + ].filter((t) => !t.startsWith('ciGroup')), + }, + }; + + const testRuns: CiStatsReportTestsOptions['testRuns'] = []; + function trackRunnable( + runnable: Runnable, + { error, type }: { error?: Error; type: CiStatsTestType } + ) { + testRuns.push({ + startTime: new Date(Date.now() - (runnable.duration ?? 0)).toJSON(), + durationMs: runnable.duration ?? 0, + seq: testRuns.length + 1, + file: Path.relative(REPO_ROOT, runnable.file), + name: runnable.title, + suites: runnable.titlePath().slice(0, -1), + result: runnable.isFailed() ? 'fail' : runnable.isPending() ? 'skip' : 'pass', + type, + error: error?.stack, + stdout: getSnapshotOfRunnableLogs(runnable), + screenshots: testMetadata.getScreenshots(runnable).map((s) => ({ + base64Png: s.base64Png, + name: s.name, + })), + }); + } + + const errors = new Map(); + runner.on('fail', (test: Runnable, error: Error) => { + errors.set(test, error); + }); + + runner.on('hook end', (hook: Runnable) => { + if (hook.isFailed()) { + const error = errors.get(hook); + if (!error) { + throw new Error(`no error recorded for failed hook`); + } + + trackRunnable(hook, { + type: getHookType(hook), + error, + }); + } + }); + + runner.on('test end', (test: Runnable) => { + const error = errors.get(test); + if (test.isFailed() && !error) { + throw new Error('no error recorded for failed test'); + } + + trackRunnable(test, { + type: 'test', + error, + }); + }); + + runner.on('end', () => { + if (!startMs) { + throw new Error('startMs was not defined'); + } + + // update the durationMs + group.durationMs = Date.now() - startMs; + }); + + lifecycle.cleanup.add(async () => { + await reporter.reportTests({ + group, + testRuns, + }); + }); +} diff --git a/packages/kbn-test/src/functional_test_runner/lib/mocha/reporter/reporter.js b/packages/kbn-test/src/functional_test_runner/lib/mocha/reporter/reporter.js index d6045b71bf3a7..84299cba14eaa 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/mocha/reporter/reporter.js +++ b/packages/kbn-test/src/functional_test_runner/lib/mocha/reporter/reporter.js @@ -9,7 +9,7 @@ import { format } from 'util'; import Mocha from 'mocha'; -import { ToolingLogTextWriter } from '@kbn/dev-utils'; +import { ToolingLogTextWriter, CiStatsReporter } from '@kbn/dev-utils'; import moment from 'moment'; import { recordLog, snapshotLogsForRunnable, setupJUnitReportGeneration } from '../../../../mocha'; @@ -17,11 +17,13 @@ import * as colors from './colors'; import * as symbols from './symbols'; import { ms } from './ms'; import { writeEpilogue } from './write_epilogue'; +import { setupCiStatsFtrTestGroupReporter } from './ci_stats_ftr_reporter'; export function MochaReporterProvider({ getService }) { const log = getService('log'); const config = getService('config'); - const failureMetadata = getService('failureMetadata'); + const lifecycle = getService('lifecycle'); + const testMetadata = getService('testMetadata'); let originalLogWriters; let reporterCaptureStartTime; @@ -45,9 +47,23 @@ export function MochaReporterProvider({ getService }) { if (config.get('junit.enabled') && config.get('junit.reportName')) { setupJUnitReportGeneration(runner, { reportName: config.get('junit.reportName'), - getTestMetadata: (t) => failureMetadata.get(t), }); } + + if (config.get('mochaReporter.sendToCiStats')) { + const reporter = CiStatsReporter.fromEnv(log); + if (!reporter.hasBuildConfig()) { + log.warning('ci-stats reporter config is not available so test results will not be sent'); + } else { + setupCiStatsFtrTestGroupReporter({ + reporter, + config, + lifecycle, + runner, + testMetadata, + }); + } + } } onStart = () => { diff --git a/packages/kbn-test/src/functional_test_runner/lib/test_metadata.ts b/packages/kbn-test/src/functional_test_runner/lib/test_metadata.ts new file mode 100644 index 0000000000000..5789231f87044 --- /dev/null +++ b/packages/kbn-test/src/functional_test_runner/lib/test_metadata.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { Lifecycle } from './lifecycle'; + +export interface ScreenshotRecord { + name: string; + base64Png: string; + baselinePath?: string; + failurePath?: string; +} + +export class TestMetadata { + // mocha's global types mean we can't import Mocha or it will override the global jest types.............. + private currentRunnable?: any; + + constructor(lifecycle: Lifecycle) { + lifecycle.beforeEachRunnable.add((runnable) => { + this.currentRunnable = runnable; + }); + } + + addScreenshot(screenshot: ScreenshotRecord) { + this.currentRunnable._screenshots = (this.currentRunnable._screenshots || []).concat( + screenshot + ); + } + + getScreenshots(test: any): ScreenshotRecord[] { + if (!test || typeof test !== 'object' || !test._screenshots) { + return []; + } + + return test._screenshots.slice(); + } +} diff --git a/packages/kbn-test/src/functional_test_runner/public_types.ts b/packages/kbn-test/src/functional_test_runner/public_types.ts index 6cb6d5adf4b19..426fdda74d313 100644 --- a/packages/kbn-test/src/functional_test_runner/public_types.ts +++ b/packages/kbn-test/src/functional_test_runner/public_types.ts @@ -8,10 +8,10 @@ import type { ToolingLog } from '@kbn/dev-utils'; -import type { Config, Lifecycle, FailureMetadata, DockerServersService, EsVersion } from './lib'; +import type { Config, Lifecycle, TestMetadata, DockerServersService, EsVersion } from './lib'; import type { Test, Suite } from './fake_mocha_types'; -export { Lifecycle, Config, FailureMetadata }; +export { Lifecycle, Config, TestMetadata }; export interface AsyncInstance { /** @@ -57,7 +57,7 @@ export interface GenericFtrProviderContext< * @param serviceName */ hasService( - serviceName: 'config' | 'log' | 'lifecycle' | 'failureMetadata' | 'dockerServers' | 'esVersion' + serviceName: 'config' | 'log' | 'lifecycle' | 'testMetadata' | 'dockerServers' | 'esVersion' ): true; hasService(serviceName: K): serviceName is K; hasService(serviceName: string): serviceName is Extract; @@ -71,7 +71,7 @@ export interface GenericFtrProviderContext< getService(serviceName: 'log'): ToolingLog; getService(serviceName: 'lifecycle'): Lifecycle; getService(serviceName: 'dockerServers'): DockerServersService; - getService(serviceName: 'failureMetadata'): FailureMetadata; + getService(serviceName: 'testMetadata'): TestMetadata; getService(serviceName: 'esVersion'): EsVersion; getService(serviceName: T): ServiceMap[T]; diff --git a/packages/kbn-test/src/jest/ci_stats_jest_reporter.ts b/packages/kbn-test/src/jest/ci_stats_jest_reporter.ts new file mode 100644 index 0000000000000..94675d87a3a24 --- /dev/null +++ b/packages/kbn-test/src/jest/ci_stats_jest_reporter.ts @@ -0,0 +1,120 @@ +/* + * 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 * as Path from 'path'; + +import getopts from 'getopts'; +import { CiStatsReporter, ToolingLog, CiStatsReportTestsOptions } from '@kbn/dev-utils'; +import type { Config } from '@jest/types'; +import { BaseReporter, Test, TestResult } from '@jest/reporters'; +import { ConsoleBuffer } from '@jest/console'; + +type LogEntry = ConsoleBuffer[0]; + +interface ReporterOptions { + testGroupType: string; +} + +function formatConsoleLine({ type, message, origin }: LogEntry) { + const originLines = origin.split('\n'); + + return `console.${type}: ${message}${originLines[0] ? `\n ${originLines[0]}` : ''}`; +} + +/** + * Jest reporter that reports tests to CI Stats + * @class JestJUnitReporter + */ + +// eslint-disable-next-line import/no-default-export +export default class CiStatsJestReporter extends BaseReporter { + private reporter: CiStatsReporter | undefined; + private readonly testGroupType: string; + private readonly reportName: string; + private readonly rootDir: string; + private startTime: number | undefined; + + private group: CiStatsReportTestsOptions['group'] | undefined; + private readonly testRuns: CiStatsReportTestsOptions['testRuns'] = []; + + constructor(config: Config.GlobalConfig, options: ReporterOptions) { + super(); + + this.rootDir = config.rootDir; + this.testGroupType = options?.testGroupType; + if (!this.testGroupType) { + throw new Error('missing testGroupType reporter option'); + } + + const configArg = getopts(process.argv).config; + if (typeof configArg !== 'string') { + throw new Error('expected to find a single --config arg'); + } + this.reportName = configArg; + } + + async onRunStart() { + const reporter = CiStatsReporter.fromEnv( + new ToolingLog({ + level: 'info', + writeTo: process.stdout, + }) + ); + + if (!reporter.hasBuildConfig()) { + return; + } + + this.startTime = Date.now(); + this.reporter = reporter; + this.group = { + name: this.reportName, + type: this.testGroupType, + startTime: new Date(this.startTime).toJSON(), + meta: {}, + durationMs: 0, + }; + } + + async onTestFileResult(_: Test, testResult: TestResult) { + if (!this.reporter || !this.group) { + return; + } + + let elapsedTime = 0; + for (const t of testResult.testResults) { + const startTime = new Date(testResult.perfStats.start + elapsedTime).toJSON(); + elapsedTime += t.duration ?? 0; + this.testRuns.push({ + startTime, + durationMs: t.duration ?? 0, + seq: this.testRuns.length + 1, + file: Path.relative(this.rootDir, testResult.testFilePath), + name: t.title, + result: t.status === 'failed' ? 'fail' : t.status === 'passed' ? 'pass' : 'skip', + suites: t.ancestorTitles, + type: 'test', + error: t.failureMessages.join('\n\n'), + stdout: testResult.console?.map(formatConsoleLine).join('\n'), + }); + } + } + + async onRunComplete() { + if (!this.reporter || !this.group || !this.testRuns.length || !this.startTime) { + return; + } + + this.group.durationMs = Date.now() - this.startTime; + + await this.reporter.reportTests({ + group: this.group, + testRuns: this.testRuns, + }); + } +} diff --git a/packages/kbn-test/src/mocha/index.ts b/packages/kbn-test/src/mocha/index.ts index 4ada51c7ae013..1be65d60a9842 100644 --- a/packages/kbn-test/src/mocha/index.ts +++ b/packages/kbn-test/src/mocha/index.ts @@ -11,7 +11,7 @@ export { setupJUnitReportGeneration } from './junit_report_generation'; // @ts-ignore not typed yet // @internal -export { recordLog, snapshotLogsForRunnable } from './log_cache'; +export { recordLog, snapshotLogsForRunnable, getSnapshotOfRunnableLogs } from './log_cache'; // @ts-ignore not typed yet // @internal export { escapeCdata } from './xml'; diff --git a/test/functional/services/common/screenshots.ts b/test/functional/services/common/screenshots.ts index 0f2ab8e6edfbe..d5f901300941f 100644 --- a/test/functional/services/common/screenshots.ts +++ b/test/functional/services/common/screenshots.ts @@ -22,7 +22,7 @@ const writeFileAsync = promisify(writeFile); export class ScreenshotsService extends FtrService { private readonly log = this.ctx.getService('log'); private readonly config = this.ctx.getService('config'); - private readonly failureMetadata = this.ctx.getService('failureMetadata'); + private readonly testMetadata = this.ctx.getService('testMetadata'); private readonly browser = this.ctx.getService('browser'); private readonly SESSION_DIRECTORY = resolve(this.config.get('screenshots.directory'), 'session'); @@ -51,11 +51,17 @@ export class ScreenshotsService extends FtrService { async compareAgainstBaseline(name: string, updateBaselines: boolean, el?: WebElementWrapper) { this.log.debug('compareAgainstBaseline'); const sessionPath = resolve(this.SESSION_DIRECTORY, `${name}.png`); - await this.capture(sessionPath, el); - const baselinePath = resolve(this.BASELINE_DIRECTORY, `${name}.png`); const failurePath = resolve(this.FAILURE_DIRECTORY, `${name}.png`); + await this.capture({ + path: sessionPath, + name, + el, + baselinePath, + failurePath, + }); + if (updateBaselines) { this.log.debug('Updating baseline snapshot'); // Make the directory if it doesn't exist @@ -76,22 +82,42 @@ export class ScreenshotsService extends FtrService { async take(name: string, el?: WebElementWrapper, subDirectories: string[] = []) { const path = resolve(this.SESSION_DIRECTORY, ...subDirectories, `${name}.png`); - await this.capture(path, el); - this.failureMetadata.addScreenshot(name, path); + await this.capture({ path, name, el }); } async takeForFailure(name: string, el?: WebElementWrapper) { const path = resolve(this.FAILURE_DIRECTORY, `${name}.png`); - await this.capture(path, el); - this.failureMetadata.addScreenshot(`failure[${name}]`, path); + await this.capture({ + path, + name: `failure[${name}]`, + el, + }); } - private async capture(path: string, el?: WebElementWrapper) { + private async capture({ + path, + el, + name, + baselinePath, + failurePath, + }: { + path: string; + name: string; + el?: WebElementWrapper; + baselinePath?: string; + failurePath?: string; + }) { try { this.log.info(`Taking screenshot "${path}"`); const screenshot = await (el ? el.takeScreenshot() : this.browser.takeScreenshot()); await mkdirAsync(dirname(path), { recursive: true }); await writeFileAsync(path, screenshot, 'base64'); + this.testMetadata.addScreenshot({ + name, + base64Png: Buffer.isBuffer(screenshot) ? screenshot.toString('base64') : screenshot, + baselinePath, + failurePath, + }); } catch (err) { this.log.error('SCREENSHOT FAILED'); this.log.error(err); From 161e981af214fc98880d58eb06e15224da9ddc56 Mon Sep 17 00:00:00 2001 From: Esteban Beltran Date: Fri, 4 Feb 2022 11:12:52 +0100 Subject: [PATCH 5/9] [Cases] Fix user action content pushing the sidebar beyond the screen limits (#123050) (#124659) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../cases/public/components/markdown_editor/renderer.tsx | 1 + .../public/components/user_action_tree/user_action_markdown.tsx | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/cases/public/components/markdown_editor/renderer.tsx b/x-pack/plugins/cases/public/components/markdown_editor/renderer.tsx index 6a91dda97a892..ae08d39a53e7a 100644 --- a/x-pack/plugins/cases/public/components/markdown_editor/renderer.tsx +++ b/x-pack/plugins/cases/public/components/markdown_editor/renderer.tsx @@ -31,6 +31,7 @@ const MarkdownRendererComponent: React.FC = ({ children, disableLinks }) {children} diff --git a/x-pack/plugins/cases/public/components/user_action_tree/user_action_markdown.tsx b/x-pack/plugins/cases/public/components/user_action_tree/user_action_markdown.tsx index f7a6932b35856..24fac6f849d58 100644 --- a/x-pack/plugins/cases/public/components/user_action_tree/user_action_markdown.tsx +++ b/x-pack/plugins/cases/public/components/user_action_tree/user_action_markdown.tsx @@ -112,7 +112,7 @@ export const UserActionMarkdown = forwardRef ) : ( - + {content} ); From c199a85b336e6dca12785b07f6cc2dfd4a5e8c46 Mon Sep 17 00:00:00 2001 From: Robert Oskamp Date: Fri, 4 Feb 2022 16:02:47 +0100 Subject: [PATCH 6/9] [7.17] [ML] Functional tests - always refresh job list before filtering (#123195) (#124685) * [ML] Functional tests - always refresh job list before filtering (#123195) This PR stabilizes tests that are filtering the anomaly detection job list by making sure the list is always refreshed before filtering. (cherry picked from commit a3359b7d154ec2190cf70c61bbecb8b2d9fff135) # Conflicts: # x-pack/test/functional/apps/ml/stack_management_jobs/import_jobs.ts * Fix backport merge issues --- .../functional/apps/ml/anomaly_detection/advanced_job.ts | 2 -- .../apps/ml/anomaly_detection/aggregated_scripted_job.ts | 2 -- .../functional/apps/ml/anomaly_detection/annotations.ts | 7 ------- .../apps/ml/anomaly_detection/anomaly_explorer.ts | 1 - .../apps/ml/anomaly_detection/categorization_job.ts | 3 --- .../functional/apps/ml/anomaly_detection/date_nanos_job.ts | 1 - .../test/functional/apps/ml/anomaly_detection/forecasts.ts | 1 - .../apps/ml/anomaly_detection/multi_metric_job.ts | 2 -- .../functional/apps/ml/anomaly_detection/population_job.ts | 2 -- .../apps/ml/anomaly_detection/saved_search_job.ts | 1 - .../apps/ml/anomaly_detection/single_metric_job.ts | 3 --- .../single_metric_job_without_datafeed_start.ts | 1 - .../apps/ml/anomaly_detection/single_metric_viewer.ts | 2 -- .../apps/ml/stack_management_jobs/import_jobs.ts | 3 +-- .../apps/ml/stack_management_jobs/manage_spaces.ts | 4 ++-- x-pack/test/functional/services/ml/job_table.ts | 1 + .../apps/ml_docs/anomaly_detection/custom_urls.ts | 1 - .../apps/ml_docs/anomaly_detection/geographic_data.ts | 2 -- .../apps/ml_docs/anomaly_detection/mapping_anomalies.ts | 1 - .../apps/ml_docs/anomaly_detection/population_analysis.ts | 1 - 20 files changed, 4 insertions(+), 37 deletions(-) diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/advanced_job.ts b/x-pack/test/functional/apps/ml/anomaly_detection/advanced_job.ts index 9a0bd824ccd90..1fc4c87619f18 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/advanced_job.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection/advanced_job.ts @@ -433,7 +433,6 @@ export default function ({ getService }: FtrProviderContext) { await ml.testExecution.logTestStep( 'job creation displays the created job in the job list' ); - await ml.jobTable.refreshJobList(); await ml.jobTable.filterWithSearchString(testData.jobId, 1); await ml.testExecution.logTestStep( @@ -649,7 +648,6 @@ export default function ({ getService }: FtrProviderContext) { await ml.testExecution.logTestStep( 'job cloning displays the created job in the job list' ); - await ml.jobTable.refreshJobList(); await ml.jobTable.filterWithSearchString(testData.jobIdClone, 1); await ml.testExecution.logTestStep( diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/aggregated_scripted_job.ts b/x-pack/test/functional/apps/ml/anomaly_detection/aggregated_scripted_job.ts index 332bc1f59d141..25261e4d05ca1 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/aggregated_scripted_job.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection/aggregated_scripted_job.ts @@ -396,7 +396,6 @@ export default function ({ getService }: FtrProviderContext) { await ml.testExecution.logTestStep( 'check that the single metric viewer button is enabled' ); - await ml.jobTable.waitForJobsToLoad(); await ml.jobTable.filterWithSearchString(testData.jobConfig.job_id, 1); await ml.jobTable.assertJobActionSingleMetricViewerButtonEnabled( @@ -442,7 +441,6 @@ export default function ({ getService }: FtrProviderContext) { await ml.testExecution.logTestStep( 'check that the single metric viewer button is disabled' ); - await ml.jobTable.waitForJobsToLoad(); await ml.jobTable.filterWithSearchString(testData.jobConfig.job_id, 1); await ml.jobTable.assertJobActionSingleMetricViewerButtonEnabled( diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/annotations.ts b/x-pack/test/functional/apps/ml/anomaly_detection/annotations.ts index 2acb16a4cff89..f7cae920106de 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/annotations.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection/annotations.ts @@ -58,7 +58,6 @@ export default function ({ getService }: FtrProviderContext) { await ml.navigation.navigateToMl(); await ml.navigation.navigateToJobManagement(); - await ml.jobTable.waitForJobsToLoad(); await ml.jobTable.filterWithSearchString(jobId, 1); await ml.jobTable.clickOpenJobInSingleMetricViewerButton(jobId); @@ -90,7 +89,6 @@ export default function ({ getService }: FtrProviderContext) { await ml.testExecution.logTestStep('should display created annotation in job list'); await ml.navigation.navigateToJobManagement(); - await ml.jobTable.waitForJobsToLoad(); await ml.jobTable.filterWithSearchString(jobId, 1); await ml.jobTable.openAnnotationsTab(jobId); await ml.jobAnnotations.assertAnnotationExists({ @@ -125,7 +123,6 @@ export default function ({ getService }: FtrProviderContext) { await ml.navigation.navigateToMl(); await ml.navigation.navigateToJobManagement(); - await ml.jobTable.waitForJobsToLoad(); await ml.jobTable.filterWithSearchString(jobId, 1); await ml.jobTable.openAnnotationsTab(jobId); await ml.jobAnnotations.assertAnnotationContentById( @@ -179,7 +176,6 @@ export default function ({ getService }: FtrProviderContext) { await ml.testExecution.logTestStep('should display edited annotation in job list'); await ml.navigation.navigateToJobManagement(); - await ml.jobTable.waitForJobsToLoad(); await ml.jobTable.filterWithSearchString(jobId, 1); await ml.jobTable.openAnnotationsTab(jobId); await ml.jobAnnotations.assertAnnotationContentById(annotationId, expectedEditedAnnotation); @@ -200,7 +196,6 @@ export default function ({ getService }: FtrProviderContext) { await ml.navigation.navigateToMl(); await ml.navigation.navigateToJobManagement(); - await ml.jobTable.waitForJobsToLoad(); await ml.jobTable.filterWithSearchString(jobId, 1); await ml.jobTable.openAnnotationsTab(jobId); @@ -221,7 +216,6 @@ export default function ({ getService }: FtrProviderContext) { await ml.navigation.navigateToMl(); await ml.navigation.navigateToJobManagement(); - await ml.jobTable.waitForJobsToLoad(); await ml.jobTable.filterWithSearchString(jobId, 1); await ml.jobTable.clickOpenJobInSingleMetricViewerButton(jobId); @@ -257,7 +251,6 @@ export default function ({ getService }: FtrProviderContext) { await ml.testExecution.logTestStep('does not show the deleted annotation in job list'); await ml.navigation.navigateToJobManagement(); - await ml.jobTable.waitForJobsToLoad(); await ml.jobTable.filterWithSearchString(jobId, 1); await ml.jobTable.openAnnotationsTab(jobId); await ml.jobAnnotations.assertAnnotationsRowMissing(annotationId); diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/anomaly_explorer.ts b/x-pack/test/functional/apps/ml/anomaly_detection/anomaly_explorer.ts index 341217b029d66..915ec4e3b3abf 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/anomaly_explorer.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection/anomaly_explorer.ts @@ -102,7 +102,6 @@ export default function ({ getService }: FtrProviderContext) { await ml.navigation.navigateToJobManagement(); await ml.testExecution.logTestStep('open job in anomaly explorer'); - await ml.jobTable.waitForJobsToLoad(); await ml.jobTable.filterWithSearchString(testData.jobConfig.job_id, 1); await ml.jobTable.clickOpenJobInAnomalyExplorerButton(testData.jobConfig.job_id); diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/categorization_job.ts b/x-pack/test/functional/apps/ml/anomaly_detection/categorization_job.ts index 7c07acd7e7185..f8360804d7e83 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/categorization_job.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection/categorization_job.ts @@ -202,7 +202,6 @@ export default function ({ getService }: FtrProviderContext) { await ml.navigation.navigateToMl(); await ml.navigation.navigateToJobManagement(); - await ml.jobTable.waitForJobsToLoad(); await ml.jobTable.filterWithSearchString(jobId, 1); await ml.testExecution.logTestStep( @@ -318,7 +317,6 @@ export default function ({ getService }: FtrProviderContext) { await ml.navigation.navigateToMl(); await ml.navigation.navigateToJobManagement(); - await ml.jobTable.waitForJobsToLoad(); await ml.jobTable.filterWithSearchString(jobIdClone, 1); await ml.testExecution.logTestStep( @@ -350,7 +348,6 @@ export default function ({ getService }: FtrProviderContext) { await ml.testExecution.logTestStep( 'job deletion does not display the deleted job in the job list any more' ); - await ml.jobTable.waitForJobsToLoad(); await ml.jobTable.filterWithSearchString(jobIdClone, 0); await ml.testExecution.logTestStep( diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/date_nanos_job.ts b/x-pack/test/functional/apps/ml/anomaly_detection/date_nanos_job.ts index fb60534b87aa0..0401d13780403 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/date_nanos_job.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection/date_nanos_job.ts @@ -313,7 +313,6 @@ export default function ({ getService }: FtrProviderContext) { await ml.testExecution.logTestStep( 'job creation displays the created job in the job list' ); - await ml.jobTable.refreshJobList(); await ml.jobTable.filterWithSearchString(testData.jobId, 1); await ml.testExecution.logTestStep( diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/forecasts.ts b/x-pack/test/functional/apps/ml/anomaly_detection/forecasts.ts index 19f8e508af7e0..4f97f9f48dcdb 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/forecasts.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection/forecasts.ts @@ -63,7 +63,6 @@ export default function ({ getService }: FtrProviderContext) { await ml.navigation.navigateToJobManagement(); await ml.testExecution.logTestStep('open job in single metric viewer'); - await ml.jobTable.waitForJobsToLoad(); await ml.jobTable.filterWithSearchString(JOB_CONFIG.job_id, 1); await ml.jobTable.clickOpenJobInSingleMetricViewerButton(JOB_CONFIG.job_id); diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/multi_metric_job.ts b/x-pack/test/functional/apps/ml/anomaly_detection/multi_metric_job.ts index 9f4982338288a..4786f51bdc414 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/multi_metric_job.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection/multi_metric_job.ts @@ -205,7 +205,6 @@ export default function ({ getService }: FtrProviderContext) { await ml.navigation.navigateToMl(); await ml.navigation.navigateToJobManagement(); - await ml.jobTable.waitForJobsToLoad(); await ml.jobTable.filterWithSearchString(jobId, 1); await ml.testExecution.logTestStep( @@ -338,7 +337,6 @@ export default function ({ getService }: FtrProviderContext) { await ml.navigation.navigateToMl(); await ml.navigation.navigateToJobManagement(); - await ml.jobTable.waitForJobsToLoad(); await ml.jobTable.filterWithSearchString(jobIdClone, 1); await ml.testExecution.logTestStep( diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/population_job.ts b/x-pack/test/functional/apps/ml/anomaly_detection/population_job.ts index 4335d99bbf1af..f522f5ebefd9a 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/population_job.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection/population_job.ts @@ -231,7 +231,6 @@ export default function ({ getService }: FtrProviderContext) { await ml.navigation.navigateToMl(); await ml.navigation.navigateToJobManagement(); - await ml.jobTable.waitForJobsToLoad(); await ml.jobTable.filterWithSearchString(jobId, 1); await ml.testExecution.logTestStep( @@ -375,7 +374,6 @@ export default function ({ getService }: FtrProviderContext) { await ml.navigation.navigateToMl(); await ml.navigation.navigateToJobManagement(); - await ml.jobTable.waitForJobsToLoad(); await ml.jobTable.filterWithSearchString(jobIdClone, 1); await ml.testExecution.logTestStep( diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/saved_search_job.ts b/x-pack/test/functional/apps/ml/anomaly_detection/saved_search_job.ts index c5945f0d91a02..f314052035ff1 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/saved_search_job.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection/saved_search_job.ts @@ -411,7 +411,6 @@ export default function ({ getService }: FtrProviderContext) { await ml.navigation.navigateToMl(); await ml.navigation.navigateToJobManagement(); - await ml.jobTable.waitForJobsToLoad(); await ml.jobTable.filterWithSearchString(testData.jobId, 1); await ml.testExecution.logTestStep( diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/single_metric_job.ts b/x-pack/test/functional/apps/ml/anomaly_detection/single_metric_job.ts index 35d2a01ead8f0..8464abdf511ed 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/single_metric_job.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection/single_metric_job.ts @@ -184,7 +184,6 @@ export default function ({ getService }: FtrProviderContext) { await ml.navigation.navigateToMl(); await ml.navigation.navigateToJobManagement(); - await ml.jobTable.waitForJobsToLoad(); await ml.jobTable.filterWithSearchString(jobId, 1); await ml.testExecution.logTestStep( @@ -301,7 +300,6 @@ export default function ({ getService }: FtrProviderContext) { await ml.navigation.navigateToMl(); await ml.navigation.navigateToJobManagement(); - await ml.jobTable.waitForJobsToLoad(); await ml.jobTable.filterWithSearchString(jobIdClone, 1); await ml.testExecution.logTestStep( @@ -333,7 +331,6 @@ export default function ({ getService }: FtrProviderContext) { await ml.testExecution.logTestStep( 'job deletion does not display the deleted job in the job list any more' ); - await ml.jobTable.waitForJobsToLoad(); await ml.jobTable.filterWithSearchString(jobIdClone, 0); await ml.testExecution.logTestStep( diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/single_metric_job_without_datafeed_start.ts b/x-pack/test/functional/apps/ml/anomaly_detection/single_metric_job_without_datafeed_start.ts index 70438069f3447..8d08fb84a8f55 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/single_metric_job_without_datafeed_start.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection/single_metric_job_without_datafeed_start.ts @@ -135,7 +135,6 @@ export default function ({ getService }: FtrProviderContext) { await ml.jobWizardCommon.assertCreateJobButtonExists(); await ml.jobWizardCommon.createJobWithoutDatafeedStart(); - await ml.jobTable.waitForJobsToLoad(); await ml.jobTable.filterWithSearchString(jobId, 1); await ml.testExecution.logTestStep( diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/single_metric_viewer.ts b/x-pack/test/functional/apps/ml/anomaly_detection/single_metric_viewer.ts index 661b9a700ef3f..0a04201eeef81 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/single_metric_viewer.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection/single_metric_viewer.ts @@ -64,7 +64,6 @@ export default function ({ getService }: FtrProviderContext) { await ml.navigation.navigateToJobManagement(); await ml.testExecution.logTestStep('open job in single metric viewer'); - await ml.jobTable.waitForJobsToLoad(); await ml.jobTable.filterWithSearchString(JOB_CONFIG.job_id, 1); await ml.jobTable.clickOpenJobInSingleMetricViewerButton(JOB_CONFIG.job_id); @@ -152,7 +151,6 @@ export default function ({ getService }: FtrProviderContext) { await ml.navigation.navigateToJobManagement(); await ml.testExecution.logTestStep('open job in single metric viewer'); - await ml.jobTable.waitForJobsToLoad(); await ml.jobTable.filterWithSearchString(jobConfig.job_id, 1); await ml.jobTable.clickOpenJobInSingleMetricViewerButton(jobConfig.job_id); diff --git a/x-pack/test/functional/apps/ml/stack_management_jobs/import_jobs.ts b/x-pack/test/functional/apps/ml/stack_management_jobs/import_jobs.ts index 4fb1b03599577..044e318cd92de 100644 --- a/x-pack/test/functional/apps/ml/stack_management_jobs/import_jobs.ts +++ b/x-pack/test/functional/apps/ml/stack_management_jobs/import_jobs.ts @@ -75,9 +75,8 @@ export default function ({ getService }: FtrProviderContext) { it('ensures jobs have been imported', async () => { if (testData.expected.jobType === 'anomaly-detector') { await ml.navigation.navigateToStackManagementJobsListPageAnomalyDetectionTab(); - await ml.jobTable.refreshJobList(); for (const id of testData.expected.jobIds) { - await ml.jobTable.filterWithSearchString(id); + await ml.jobTable.filterWithSearchString(id, 1); } for (const id of testData.expected.skippedJobIds) { await ml.jobTable.filterWithSearchString(id, 0); diff --git a/x-pack/test/functional/apps/ml/stack_management_jobs/manage_spaces.ts b/x-pack/test/functional/apps/ml/stack_management_jobs/manage_spaces.ts index 3eea2ac125043..6cf704e966d38 100644 --- a/x-pack/test/functional/apps/ml/stack_management_jobs/manage_spaces.ts +++ b/x-pack/test/functional/apps/ml/stack_management_jobs/manage_spaces.ts @@ -174,7 +174,7 @@ export default function ({ getService }: FtrProviderContext) { await ml.navigation.navigateToStackManagementJobsListPage(); // AD - await ml.jobTable.filterWithSearchString(testData.adJobId); + await ml.jobTable.filterWithSearchString(testData.adJobId, 1); await ml.stackManagementJobs.assertADJobRowSpaces(testData.adJobId, [ testData.initialSpace, ]); @@ -218,7 +218,7 @@ export default function ({ getService }: FtrProviderContext) { // AD await ml.navigation.navigateToStackManagementJobsListPageAnomalyDetectionTab(); - await ml.jobTable.filterWithSearchString(testData.adJobId); + await ml.jobTable.filterWithSearchString(testData.adJobId, 1); await ml.stackManagementJobs.assertADJobRowSpaces(testData.adJobId, expectedJobRowSpaces); // DFA diff --git a/x-pack/test/functional/services/ml/job_table.ts b/x-pack/test/functional/services/ml/job_table.ts index 40ef733bc5e06..aec834702324f 100644 --- a/x-pack/test/functional/services/ml/job_table.ts +++ b/x-pack/test/functional/services/ml/job_table.ts @@ -239,6 +239,7 @@ export function MachineLearningJobTableProvider( public async filterWithSearchString(filter: string, expectedRowCount: number = 1) { await this.waitForJobsToLoad(); + await this.refreshJobList(); const searchBar = await testSubjects.find('mlJobListSearchBar'); const searchBarInput = await searchBar.findByTagName('input'); await searchBarInput.clearValueWithKeyboard(); diff --git a/x-pack/test/screenshot_creation/apps/ml_docs/anomaly_detection/custom_urls.ts b/x-pack/test/screenshot_creation/apps/ml_docs/anomaly_detection/custom_urls.ts index a823bff61dbdf..6906fa5dfc38a 100644 --- a/x-pack/test/screenshot_creation/apps/ml_docs/anomaly_detection/custom_urls.ts +++ b/x-pack/test/screenshot_creation/apps/ml_docs/anomaly_detection/custom_urls.ts @@ -86,7 +86,6 @@ export default function ({ getService }: FtrProviderContext) { await ml.navigation.navigateToJobManagement(); await ml.testExecution.logTestStep('open job in anomaly explorer'); - await ml.jobTable.waitForJobsToLoad(); await ml.jobTable.filterWithSearchString(ecommerceJobConfig.job_id, 1); await ml.jobTable.clickOpenJobInAnomalyExplorerButton(ecommerceJobConfig.job_id); await ml.commonUI.waitForMlLoadingIndicatorToDisappear(); diff --git a/x-pack/test/screenshot_creation/apps/ml_docs/anomaly_detection/geographic_data.ts b/x-pack/test/screenshot_creation/apps/ml_docs/anomaly_detection/geographic_data.ts index 24a92612a7595..868e649950ba5 100644 --- a/x-pack/test/screenshot_creation/apps/ml_docs/anomaly_detection/geographic_data.ts +++ b/x-pack/test/screenshot_creation/apps/ml_docs/anomaly_detection/geographic_data.ts @@ -229,7 +229,6 @@ export default function ({ getPageObject, getService }: FtrProviderContext) { await elasticChart.setNewChartUiDebugFlag(true); await ml.testExecution.logTestStep('open job in anomaly explorer'); - await ml.jobTable.waitForJobsToLoad(); await ml.jobTable.filterWithSearchString(ecommerceGeoJobConfig.job_id, 1); await ml.jobTable.clickOpenJobInAnomalyExplorerButton(ecommerceGeoJobConfig.job_id); await ml.commonUI.waitForMlLoadingIndicatorToDisappear(); @@ -260,7 +259,6 @@ export default function ({ getPageObject, getService }: FtrProviderContext) { await elasticChart.setNewChartUiDebugFlag(true); await ml.testExecution.logTestStep('open job in anomaly explorer'); - await ml.jobTable.waitForJobsToLoad(); await ml.jobTable.filterWithSearchString(weblogGeoJobConfig.job_id, 1); await ml.jobTable.clickOpenJobInAnomalyExplorerButton(weblogGeoJobConfig.job_id); await ml.commonUI.waitForMlLoadingIndicatorToDisappear(); diff --git a/x-pack/test/screenshot_creation/apps/ml_docs/anomaly_detection/mapping_anomalies.ts b/x-pack/test/screenshot_creation/apps/ml_docs/anomaly_detection/mapping_anomalies.ts index 6d653e0aed43e..e1859c5ae0a76 100644 --- a/x-pack/test/screenshot_creation/apps/ml_docs/anomaly_detection/mapping_anomalies.ts +++ b/x-pack/test/screenshot_creation/apps/ml_docs/anomaly_detection/mapping_anomalies.ts @@ -114,7 +114,6 @@ export default function ({ getPageObject, getService }: FtrProviderContext) { await ml.navigation.navigateToJobManagement(); await ml.testExecution.logTestStep('open job in anomaly explorer'); - await ml.jobTable.waitForJobsToLoad(); await ml.jobTable.filterWithSearchString(weblogVectorJobConfig.job_id, 1); await ml.jobTable.clickOpenJobInAnomalyExplorerButton(weblogVectorJobConfig.job_id); await ml.commonUI.waitForMlLoadingIndicatorToDisappear(); diff --git a/x-pack/test/screenshot_creation/apps/ml_docs/anomaly_detection/population_analysis.ts b/x-pack/test/screenshot_creation/apps/ml_docs/anomaly_detection/population_analysis.ts index 235b78ca8a662..23ef7ce03ce09 100644 --- a/x-pack/test/screenshot_creation/apps/ml_docs/anomaly_detection/population_analysis.ts +++ b/x-pack/test/screenshot_creation/apps/ml_docs/anomaly_detection/population_analysis.ts @@ -82,7 +82,6 @@ export default function ({ getService }: FtrProviderContext) { await elasticChart.setNewChartUiDebugFlag(true); await ml.testExecution.logTestStep('open job in anomaly explorer'); - await ml.jobTable.waitForJobsToLoad(); await ml.jobTable.filterWithSearchString(populationJobConfig.job_id, 1); await ml.jobTable.clickOpenJobInAnomalyExplorerButton(populationJobConfig.job_id); await ml.commonUI.waitForMlLoadingIndicatorToDisappear(); From 10997900d526da77ce80e7a4b57913cab3d0eea9 Mon Sep 17 00:00:00 2001 From: Lukas Olson Date: Fri, 4 Feb 2022 09:59:34 -0700 Subject: [PATCH 7/9] Add enable_fields_emulation flag to search requests (#123267) * Add enable_fields_emulation flag to search requests * Move parameter to ese strategy * Update search source to not fetch fields if size is 0 * Don't send flag if specifying fields and _source * Update maps functional test with search source changes * Add flag to other search strategies * Fix ESE usage of params * Fix tests * Other fixes * Fix linting Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../search_source/search_source.test.ts | 34 ++++++--- .../search/search_source/search_source.ts | 6 ++ .../data/server/search/routes/call_msearch.ts | 8 +- .../eql_search/eql_search_strategy.ts | 3 +- .../es_search/es_search_strategy.test.ts | 3 + .../es_search/es_search_strategy.ts | 2 +- .../strategies/es_search/request_utils.ts | 16 +++- .../ese_search/ese_search_strategy.ts | 3 +- .../ese_search/request_utils.test.ts | 75 +++++++++++++++---- .../strategies/ese_search/request_utils.ts | 11 ++- .../functional/apps/maps/mvt_super_fine.js | 2 +- 11 files changed, 127 insertions(+), 36 deletions(-) diff --git a/src/plugins/data/common/search/search_source/search_source.test.ts b/src/plugins/data/common/search/search_source/search_source.test.ts index 8ef457ac586c5..72869e2555d8a 100644 --- a/src/plugins/data/common/search/search_source/search_source.test.ts +++ b/src/plugins/data/common/search/search_source/search_source.test.ts @@ -157,6 +157,22 @@ describe('SearchSource', () => { expect(request.runtime_mappings).toEqual(runtimeFields); }); + test('except when size is 0', async () => { + const runtimeFields = { runtime_field: runtimeFieldDef }; + searchSource.setField('size', 0).setField('index', { + ...indexPattern, + getComputedFields: () => ({ + storedFields: ['hello'], + scriptFields: { world: {} }, + docvalueFields: ['@timestamp'], + runtimeFields, + }), + } as unknown as IndexPattern); + + const request = searchSource.getSearchRequestBody(); + expect(request.fields).toBeUndefined(); + }); + test('never includes docvalue_fields', async () => { searchSource.setField('index', { ...indexPattern, @@ -211,25 +227,21 @@ describe('SearchSource', () => { expect(request.fields).toEqual(['c', { field: 'a', format: 'date_time' }]); }); - test('allows you to override computed fields if you provide a format', async () => { - const indexPatternFields = indexPattern.fields; - indexPatternFields.getByType = (type) => { - return []; - }; - searchSource.setField('index', { + test('does not allow any field info when size is 0', async () => { + searchSource.setField('size', 0).setField('index', { ...indexPattern, - fields: indexPatternFields, getComputedFields: () => ({ storedFields: [], scriptFields: {}, - docvalueFields: [{ field: 'hello', format: 'date_time' }], + docvalueFields: [{ field: 'a', format: 'date_time' }], }), } as unknown as IndexPattern); - searchSource.setField('fields', [{ field: 'hello', format: 'strict_date_time' }]); + searchSource.setField('fields', ['c']); + searchSource.setField('fieldsFromSource', ['a', 'b', 'd']); const request = searchSource.getSearchRequestBody(); - expect(request).toHaveProperty('fields'); - expect(request.fields).toEqual([{ field: 'hello', format: 'strict_date_time' }]); + expect(request).not.toHaveProperty('_source'); + expect(request).not.toHaveProperty('fields'); }); test('injects a date format for computed docvalue fields if none is provided', async () => { diff --git a/src/plugins/data/common/search/search_source/search_source.ts b/src/plugins/data/common/search/search_source/search_source.ts index 0691925ed50f7..8a7973ff08f8f 100644 --- a/src/plugins/data/common/search/search_source/search_source.ts +++ b/src/plugins/data/common/search/search_source/search_source.ts @@ -838,6 +838,12 @@ export class SearchSource { body.fields = filteredDocvalueFields; } + // If we aren't requesting any documents, there isn't any reason to request any field information + if (body.size === 0) { + delete body._source; + delete body.fields; + } + // If sorting by _score, build queries in the "must" clause instead of "filter" clause to enable scoring const filtersInMustClause = (body.sort ?? []).some((sort: EsQuerySortValue[]) => sort.hasOwnProperty('_score') diff --git a/src/plugins/data/server/search/routes/call_msearch.ts b/src/plugins/data/server/search/routes/call_msearch.ts index 4a7db9517c688..bbcb6c2f3732e 100644 --- a/src/plugins/data/server/search/routes/call_msearch.ts +++ b/src/plugins/data/server/search/routes/call_msearch.ts @@ -60,8 +60,12 @@ export function getCallMsearch(dependencies: CallMsearchDependencies) { const config = await globalConfig$.pipe(first()).toPromise(); const timeout = getShardTimeout(config); - // trackTotalHits is not supported by msearch - const { track_total_hits: _, ...defaultParams } = await getDefaultSearchParams(uiSettings); + // track_total_hits/enable_fields_emulation are not supported by msearch + const { + track_total_hits: tth, + enable_fields_emulation: efe, + ...defaultParams + } = await getDefaultSearchParams(uiSettings); try { const promise = esClient.asCurrentUser.msearch( diff --git a/src/plugins/data/server/search/strategies/eql_search/eql_search_strategy.ts b/src/plugins/data/server/search/strategies/eql_search/eql_search_strategy.ts index 4c75d62f12190..c2bc62c40e8de 100644 --- a/src/plugins/data/server/search/strategies/eql_search/eql_search_strategy.ts +++ b/src/plugins/data/server/search/strategies/eql_search/eql_search_strategy.ts @@ -42,7 +42,8 @@ export const eqlSearchStrategyProvider = ( const search = async () => { const { track_total_hits: _, ...defaultParams } = await getDefaultSearchParams( - uiSettingsClient + uiSettingsClient, + request.params?.body ); const params = id ? getDefaultAsyncGetParams(null, options) diff --git a/src/plugins/data/server/search/strategies/es_search/es_search_strategy.test.ts b/src/plugins/data/server/search/strategies/es_search/es_search_strategy.test.ts index bbbc99d157fe0..aebc3dcc8404b 100644 --- a/src/plugins/data/server/search/strategies/es_search/es_search_strategy.test.ts +++ b/src/plugins/data/server/search/strategies/es_search/es_search_strategy.test.ts @@ -74,6 +74,7 @@ describe('ES search strategy', () => { ...params, ignore_unavailable: true, track_total_hits: true, + enable_fields_emulation: true, }); done(); }); @@ -89,6 +90,7 @@ describe('ES search strategy', () => { expect(mockApiCaller.mock.calls[0][0]).toEqual({ ...params, track_total_hits: true, + enable_fields_emulation: true, }); done(); }); @@ -126,6 +128,7 @@ describe('ES search strategy', () => { expect(mockApiCaller.mock.calls[0][0]).toEqual({ ...params, track_total_hits: true, + enable_fields_emulation: true, }); expect(mockedApiCaller.abort).toBeCalled(); }); diff --git a/src/plugins/data/server/search/strategies/es_search/es_search_strategy.ts b/src/plugins/data/server/search/strategies/es_search/es_search_strategy.ts index c24aa37082bd8..d508a91dae6fe 100644 --- a/src/plugins/data/server/search/strategies/es_search/es_search_strategy.ts +++ b/src/plugins/data/server/search/strategies/es_search/es_search_strategy.ts @@ -39,7 +39,7 @@ export const esSearchStrategyProvider = ( try { const config = await config$.pipe(first()).toPromise(); const params = { - ...(await getDefaultSearchParams(uiSettingsClient)), + ...(await getDefaultSearchParams(uiSettingsClient, request.params?.body)), ...getShardTimeout(config), ...request.params, }; diff --git a/src/plugins/data/server/search/strategies/es_search/request_utils.ts b/src/plugins/data/server/search/strategies/es_search/request_utils.ts index 15cad34065ddc..2123b3dba5247 100644 --- a/src/plugins/data/server/search/strategies/es_search/request_utils.ts +++ b/src/plugins/data/server/search/strategies/es_search/request_utils.ts @@ -7,6 +7,7 @@ */ import type { TransportRequestPromise } from '@elastic/elasticsearch/lib/Transport'; +import { SearchRequest } from '@elastic/elasticsearch/api/types'; import type { Search } from '@elastic/elasticsearch/api/requestParams'; import type { IUiSettingsClient, SharedGlobalConfig } from 'kibana/server'; import { UI_SETTINGS } from '../../../../common'; @@ -17,18 +18,29 @@ export function getShardTimeout(config: SharedGlobalConfig): Pick + uiSettingsClient: Pick, + body: SearchRequest['body'] = {} ): Promise< - Pick + Pick & { + enable_fields_emulation: boolean; + } > { const maxConcurrentShardRequests = await uiSettingsClient.get( UI_SETTINGS.COURIER_MAX_CONCURRENT_SHARD_REQUESTS ); + + // Specifying specific fields from both "_source" and "fields' while emulating the fields API will throw errors in ES + // See https://github.com/elastic/elasticsearch/pull/75745 + const hasFields = Array.isArray(body?.fields) && body?.fields.length > 0; + const hasSourceFields = body?.hasOwnProperty('_source') && typeof body?._source !== 'boolean'; + const enableFieldsEmulation = !(hasFields && hasSourceFields); + return { max_concurrent_shard_requests: maxConcurrentShardRequests > 0 ? maxConcurrentShardRequests : undefined, ignore_unavailable: true, // Don't fail if the index/indices don't exist track_total_hits: true, + enable_fields_emulation: enableFieldsEmulation, }; } diff --git a/src/plugins/data/server/search/strategies/ese_search/ese_search_strategy.ts b/src/plugins/data/server/search/strategies/ese_search/ese_search_strategy.ts index 75a4ddf051418..029136384fe81 100644 --- a/src/plugins/data/server/search/strategies/ese_search/ese_search_strategy.ts +++ b/src/plugins/data/server/search/strategies/ese_search/ese_search_strategy.ts @@ -64,6 +64,7 @@ export const enhancedEsSearchStrategyProvider = ( ...(await getDefaultAsyncSubmitParams( uiSettingsClient, searchSessionsClient.getConfig(), + request.params?.body, options )), ...request.params, @@ -110,7 +111,7 @@ export const enhancedEsSearchStrategyProvider = ( const querystring = { ...getShardTimeout(legacyConfig), ...(await getIgnoreThrottled(uiSettingsClient)), - ...(await getDefaultSearchParams(uiSettingsClient)), + ...(await getDefaultSearchParams(uiSettingsClient, request.params?.body)), ...params, }; diff --git a/src/plugins/data/server/search/strategies/ese_search/request_utils.test.ts b/src/plugins/data/server/search/strategies/ese_search/request_utils.test.ts index 91b323de7c07b..61c8b61f7b434 100644 --- a/src/plugins/data/server/search/strategies/ese_search/request_utils.test.ts +++ b/src/plugins/data/server/search/strategies/ese_search/request_utils.test.ts @@ -56,7 +56,7 @@ describe('request utils', () => { const mockConfig = getMockSearchSessionsConfig({ defaultExpiration: moment.duration(3, 'd'), }); - const params = await getDefaultAsyncSubmitParams(mockUiSettingsClient, mockConfig, {}); + const params = await getDefaultAsyncSubmitParams(mockUiSettingsClient, mockConfig, {}, {}); expect(params).toHaveProperty('keep_alive', '1m'); }); @@ -67,9 +67,14 @@ describe('request utils', () => { const mockConfig = getMockSearchSessionsConfig({ defaultExpiration: moment.duration(3, 'd'), }); - const params = await getDefaultAsyncSubmitParams(mockUiSettingsClient, mockConfig, { - sessionId: 'foo', - }); + const params = await getDefaultAsyncSubmitParams( + mockUiSettingsClient, + mockConfig, + {}, + { + sessionId: 'foo', + } + ); expect(params).toHaveProperty('keep_alive', '259200000ms'); }); @@ -81,9 +86,14 @@ describe('request utils', () => { defaultExpiration: moment.duration(3, 'd'), enabled: false, }); - const params = await getDefaultAsyncSubmitParams(mockUiSettingsClient, mockConfig, { - sessionId: 'foo', - }); + const params = await getDefaultAsyncSubmitParams( + mockUiSettingsClient, + mockConfig, + {}, + { + sessionId: 'foo', + } + ); expect(params).toHaveProperty('keep_alive', '1m'); }); @@ -92,9 +102,14 @@ describe('request utils', () => { [UI_SETTINGS.SEARCH_INCLUDE_FROZEN]: false, }); const mockConfig = getMockSearchSessionsConfig({}); - const params = await getDefaultAsyncSubmitParams(mockUiSettingsClient, mockConfig, { - sessionId: 'foo', - }); + const params = await getDefaultAsyncSubmitParams( + mockUiSettingsClient, + mockConfig, + {}, + { + sessionId: 'foo', + } + ); expect(params).toHaveProperty('keep_on_completion', true); }); @@ -106,11 +121,45 @@ describe('request utils', () => { defaultExpiration: moment.duration(3, 'd'), enabled: false, }); - const params = await getDefaultAsyncSubmitParams(mockUiSettingsClient, mockConfig, { - sessionId: 'foo', - }); + const params = await getDefaultAsyncSubmitParams( + mockUiSettingsClient, + mockConfig, + {}, + { + sessionId: 'foo', + } + ); expect(params).toHaveProperty('keep_on_completion', false); }); + + test('Sends `enable_fields_emulation: true` for BWC with CCS if not specifying both fields and _source', async () => { + const mockUiSettingsClient = getMockUiSettingsClient({ + [UI_SETTINGS.SEARCH_INCLUDE_FROZEN]: false, + }); + const mockConfig = getMockSearchSessionsConfig({ + defaultExpiration: moment.duration(3, 'd'), + enabled: false, + }); + const params = await getDefaultAsyncSubmitParams(mockUiSettingsClient, mockConfig, {}, {}); + expect(params).toHaveProperty('enable_fields_emulation', true); + }); + + test('Sends `enable_fields_emulation: false` if specifying both fields and _source', async () => { + const mockUiSettingsClient = getMockUiSettingsClient({ + [UI_SETTINGS.SEARCH_INCLUDE_FROZEN]: false, + }); + const mockConfig = getMockSearchSessionsConfig({ + defaultExpiration: moment.duration(3, 'd'), + enabled: false, + }); + const params = await getDefaultAsyncSubmitParams( + mockUiSettingsClient, + mockConfig, + { fields: ['foo'], _source: { excludes: ['bar'] } }, + {} + ); + expect(params).toHaveProperty('enable_fields_emulation', false); + }); }); describe('getDefaultAsyncGetParams', () => { diff --git a/src/plugins/data/server/search/strategies/ese_search/request_utils.ts b/src/plugins/data/server/search/strategies/ese_search/request_utils.ts index f8fb54cfd870b..589f502047d54 100644 --- a/src/plugins/data/server/search/strategies/ese_search/request_utils.ts +++ b/src/plugins/data/server/search/strategies/ese_search/request_utils.ts @@ -12,6 +12,7 @@ import { AsyncSearchSubmit, Search, } from '@elastic/elasticsearch/api/requestParams'; +import { SearchRequest } from '@elastic/elasticsearch/api/types'; import { ISearchOptions, UI_SETTINGS } from '../../../../common'; import { getDefaultSearchParams } from '../es_search'; import { SearchSessionsConfigSchema } from '../../../../config'; @@ -32,7 +33,8 @@ export async function getIgnoreThrottled( export async function getDefaultAsyncSubmitParams( uiSettingsClient: Pick, searchSessionsConfig: SearchSessionsConfigSchema | null, - options: ISearchOptions + body: SearchRequest['body'] = {}, + options: ISearchOptions = {} ): Promise< Pick< AsyncSearchSubmit, @@ -44,7 +46,9 @@ export async function getDefaultAsyncSubmitParams( | 'ignore_unavailable' | 'track_total_hits' | 'keep_on_completion' - > + > & { + enable_fields_emulation: boolean; + } > { const useSearchSessions = searchSessionsConfig?.enabled && !!options.sessionId; @@ -64,8 +68,7 @@ export async function getDefaultAsyncSubmitParams( // The initial keepalive is as defined in defaultExpiration if search sessions are used or 1m otherwise. keep_alive: keepAlive, ...(await getIgnoreThrottled(uiSettingsClient)), - ...(await getDefaultSearchParams(uiSettingsClient)), - // If search sessions are used, set the initial expiration time. + ...(await getDefaultSearchParams(uiSettingsClient, body)), }; } diff --git a/x-pack/test/functional/apps/maps/mvt_super_fine.js b/x-pack/test/functional/apps/maps/mvt_super_fine.js index dcd2923cb9335..fd60e0a489ed6 100644 --- a/x-pack/test/functional/apps/maps/mvt_super_fine.js +++ b/x-pack/test/functional/apps/maps/mvt_super_fine.js @@ -34,7 +34,7 @@ export default function ({ getPageObjects, getService }) { //Source should be correct expect( mapboxStyle.sources[MB_VECTOR_SOURCE_ID].tiles[0].startsWith( - `/api/maps/mvt/getGridTile/{z}/{x}/{y}.pbf?geometryFieldName=geo.coordinates&index=logstash-*&requestBody=(_source:(excludes:!()),aggs:(gridSplit:(aggs:(gridCentroid:(geo_centroid:(field:geo.coordinates)),max_of_bytes:(max:(field:bytes))),geotile_grid:(bounds:!n,field:geo.coordinates,precision:!n,shard_size:65535,size:65535))),fields:!((field:'@timestamp',format:date_time),(field:'relatedContent.article:modified_time',format:date_time),(field:'relatedContent.article:published_time',format:date_time),(field:utc_time,format:date_time)),query:(bool:(filter:!((range:('@timestamp':(format:strict_date_optional_time,gte:'2015-09-20T00:00:00.000Z',lte:'2015-09-20T01:00:00.000Z')))),must:!(),must_not:!(),should:!())),runtime_mappings:(),script_fields:(hour_of_day:(script:(lang:painless,source:'doc[!'@timestamp!'].value.getHour()'))),size:0,stored_fields:!('*'))&requestType=grid&geoFieldType=geo_point` + `/api/maps/mvt/getGridTile/{z}/{x}/{y}.pbf?geometryFieldName=geo.coordinates&index=logstash-*&requestBody=(aggs:(gridSplit:(aggs:(gridCentroid:(geo_centroid:(field:geo.coordinates)),max_of_bytes:(max:(field:bytes))),geotile_grid:(bounds:!n,field:geo.coordinates,precision:!n,shard_size:65535,size:65535))),query:(bool:(filter:!((range:('@timestamp':(format:strict_date_optional_time,gte:'2015-09-20T00:00:00.000Z',lte:'2015-09-20T01:00:00.000Z')))),must:!(),must_not:!(),should:!())),runtime_mappings:(),script_fields:(hour_of_day:(script:(lang:painless,source:'doc[!'@timestamp!'].value.getHour()'))),size:0,stored_fields:!('*'))&requestType=grid&geoFieldType=geo_point` ) ).to.equal(true); From 15b5cad04c96e3e89fc703421b08c91a69ac9d3b Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Fri, 4 Feb 2022 17:33:39 +0000 Subject: [PATCH 8/9] [File data visualizer] Removing file upload retries (#123696) (#123725) (cherry picked from commit 3797b78ce722bcefc617ef233c2917cf6905f126) # Conflicts: # x-pack/plugins/file_upload/server/import_data.ts Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/file_upload/server/analyze_file.tsx | 11 +++++++---- x-pack/plugins/file_upload/server/import_data.ts | 4 ++-- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/file_upload/server/analyze_file.tsx b/x-pack/plugins/file_upload/server/analyze_file.tsx index 2239697083492..a2a5f3835f910 100644 --- a/x-pack/plugins/file_upload/server/analyze_file.tsx +++ b/x-pack/plugins/file_upload/server/analyze_file.tsx @@ -14,10 +14,13 @@ export async function analyzeFile( overrides: InputOverrides ): Promise { overrides.explain = overrides.explain === undefined ? 'true' : overrides.explain; - const { body } = await client.asInternalUser.textStructure.findStructure({ - body: data, - ...overrides, - }); + const { body } = await client.asInternalUser.textStructure.findStructure( + { + body: data, + ...overrides, + }, + { maxRetries: 0 } + ); const { hasOverrides, reducedOverrides } = formatOverrides(overrides); diff --git a/x-pack/plugins/file_upload/server/import_data.ts b/x-pack/plugins/file_upload/server/import_data.ts index deb170974ced8..af2e05d8af196 100644 --- a/x-pack/plugins/file_upload/server/import_data.ts +++ b/x-pack/plugins/file_upload/server/import_data.ts @@ -103,7 +103,7 @@ export function importDataProvider({ asCurrentUser }: IScopedClusterClient) { body.settings = settings; } - await asCurrentUser.indices.create({ index, body }); + await asCurrentUser.indices.create({ index, body }, { maxRetries: 0 }); } async function indexData(index: string, pipelineId: string, data: InputData) { @@ -119,7 +119,7 @@ export function importDataProvider({ asCurrentUser }: IScopedClusterClient) { settings.pipeline = pipelineId; } - const { body: resp } = await asCurrentUser.bulk(settings); + const { body: resp } = await asCurrentUser.bulk(settings, { maxRetries: 0 }); if (resp.errors) { throw resp; } else { From 0399402f0b0916208740ea22f61ad9a92847cd5b Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Mon, 7 Feb 2022 01:55:20 -0500 Subject: [PATCH 9/9] Change deprecation warning for elasticsearch.username (#124717) --- src/core/server/elasticsearch/elasticsearch_config.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/core/server/elasticsearch/elasticsearch_config.ts b/src/core/server/elasticsearch/elasticsearch_config.ts index 96b3bdcad1d2e..5108418593623 100644 --- a/src/core/server/elasticsearch/elasticsearch_config.ts +++ b/src/core/server/elasticsearch/elasticsearch_config.ts @@ -165,10 +165,11 @@ const deprecations: ConfigDeprecationProvider = () => [ manualSteps: [ i18n.translate('core.deprecations.elasticsearchUsername.manualSteps1', { defaultMessage: - 'Use the elasticsearch-service-tokens CLI tool to create a new service account token for the "elastic/kibana" service account.', + 'Use Kibana Dev Tools to create a service account token using the API: "POST /_security/service/elastic/kibana/credential/token"', }), i18n.translate('core.deprecations.elasticsearchUsername.manualSteps2', { - defaultMessage: 'Add the "elasticsearch.serviceAccountToken" setting to kibana.yml.', + defaultMessage: + 'Copy the returned token.value and add it as the "elasticsearch.serviceAccountToken" setting to kibana.yml.', }), i18n.translate('core.deprecations.elasticsearchUsername.manualSteps3', { defaultMessage: