[kbn/journeys] fixes to run journeys against ESS cluster (#166923)

## Summary

I had to change `waitForRender` since `page.waitForFunction` tries to
run a script on page and it is not working due to CSP settings on Cloud.
Instead of injecting a script, we use a classical API to find
elements/attributes in the DOM.

Since `PUT /internal/core/_settings` is merged in 8.11.0, journeys run
on Cloud with on-fly labels update is supported starting deployments
8.11.0+. I added error message for 404 code just in case someone runs it
on earlier version.

`many_fields_discover` journey was update since on Cloud the data view
used by scenario is not selected by default.

How it works:

Create a deployment with QAF and re-configure it for journey run:
```
export EC_DEPLOYMENT_NAME=my-run-8.11
qaf elastic-cloud deployments create --stack-version 8.11.0-SNAPSHOT --environment staging --region gcp-us-central1
qaf elastic-cloud deployments configure-for-performance-journeys
```

Run any journey, e.g. many_fields_discover
```
TEST_CLOUD=1 TEST_ES_URL=https://username:pswd@es_url:443 TEST_KIBANA_URL=https://username:pswd@kibana-ur_url node scripts/functional_test_runner --config x-pack/performance/journeys/many_fields_discover.ts
```

You should see a log about labels being updated:

```
Updating telemetry & APM labels: {"testJobId":"local-a3272047-6724-44d1-9a61-5c79781b06a1","testBuildId":"local-d8edbace-f441-4ba9-ac83-5909be3acf2a","journeyName":"many_fields_discover","ftrConfig":"x-pack/performance/journeys/many_fields_discover.ts"}
```

And then able to find APM logs for the journey in
[Ops](https://kibana-ops-e2e-perf.kb.us-central1.gcp.cloud.es.io:9243/app/apm/services?comparisonEnabled=true&environment=ENVIRONMENT_ALL&kuery=labels.testJobId%20%3A%20%22local-d79a878c-cc7a-423b-b884-c9b6b1a8d781%22&latencyAggregationType=avg&offset=1d&rangeFrom=now-24h%2Fh&rangeTo=now&serviceGroup=&transactionType=request)
cluster
This commit is contained in:
Dzmitry Lemechko 2023-09-28 12:06:00 +02:00 committed by GitHub
parent fdf0ab763b
commit c48cc24617
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 158 additions and 60 deletions

View file

@ -11,7 +11,7 @@ tags: ['kibana', 'onboarding', 'setup', 'performance', 'development', 'telemetry
As a way to better understand user experience with Kibana in cloud, we support running performance journeys against As a way to better understand user experience with Kibana in cloud, we support running performance journeys against
Cloud deployments. Cloud deployments.
The process takes a few steps: The process takes a few steps:
- Create a cloud deployment - Create a cloud deployment (8.11.0+ is supported)
- Re-configure deployment with APM enabled and reporting metrics to the monitoring cluster - Re-configure deployment with APM enabled and reporting metrics to the monitoring cluster
- Create a user with `superuser` role to run tests with - Create a user with `superuser` role to run tests with
- Checkout the branch that matches your cloud deployment version - Checkout the branch that matches your cloud deployment version
@ -35,7 +35,7 @@ Navigate to `Advanced Edit` page and change `Deployment Configuration` by adding
``` ```
"user_settings_override_json": { "user_settings_override_json": {
"tracing.apm.enabled": "true", "tracing.apm.enabled": "true",
"tracing.apm.environment": "development", "tracing.apm.agent.environment": "development",
"tracing.apm.agent.service_name": "elasticsearch", "tracing.apm.agent.service_name": "elasticsearch",
"tracing.apm.agent.server_url": "<SERVER_URL>", "tracing.apm.agent.server_url": "<SERVER_URL>",
"tracing.apm.agent.metrics_interval": "120s", "tracing.apm.agent.metrics_interval": "120s",
@ -50,6 +50,7 @@ Navigate to `Advanced Edit` page and change `Deployment Configuration` by adding
``` ```
"user_settings_override_json": { "user_settings_override_json": {
"coreApp.allowDynamicConfigOverrides": true,
"elastic.apm.active": true, "elastic.apm.active": true,
"elastic.apm.breakdownMetrics": false, "elastic.apm.breakdownMetrics": false,
"elastic.apm.captureBody": "all", "elastic.apm.captureBody": "all",
@ -74,8 +75,28 @@ Note: DEPLOYMENT_ID and YOUR_JOURNEY_NAME values are optional labels to find the
Save changes and make sure cluster is restarted successfully. Save changes and make sure cluster is restarted successfully.
### Use QAF to prepare the deployment
The quickest way to prepare ESS deployment is to use [QAF](https://github.com/elastic/qaf):
- Make sure to add `~/.elastic/cloud.json` and ~/.elastic/cloud-admin.json with Cloud API (to create deployment) & Cloud Admin API (to modify it) keys
```
{
"api_key": {
"production": "<PROD KEY>",
"staging": "<STAGING KEY>",
"qa": "<QA KEY>"
}
}
```
- Create deployment and modify it
```
export EC_DEPLOYMENT_NAME=kibana-perf-8.11
qaf elastic-cloud deployments create --stack-version 8.11.0-SNAPSHOT --environment staging --region gcp-us-central1
qaf elastic-cloud deployments configure-for-performance-journeys
```
### Run the journey ### Run the journey
Make sure you have created user with `superuser` role and the Kibana repo branch is matching your deployment version. Make sure the Kibana repo branch is matching your deployment version.
Set env variables to run FTR against your cloud deployment: Set env variables to run FTR against your cloud deployment:
``` ```
@ -90,4 +111,6 @@ Run your journey with the command:
node scripts/functional_test_runner.js --config x-pack/performance/journeys/$YOUR_JOURNEY_NAME.ts` node scripts/functional_test_runner.js --config x-pack/performance/journeys/$YOUR_JOURNEY_NAME.ts`
``` ```
APM & Telemetry labels will be updated on the fly and metrics/traces should be available in Telemetry Staging and kibana-ops-e2e-perf cluster.

View file

@ -12,14 +12,9 @@ import { Page } from 'playwright';
import callsites from 'callsites'; import callsites from 'callsites';
import { ToolingLog } from '@kbn/tooling-log'; import { ToolingLog } from '@kbn/tooling-log';
import { FtrConfigProvider } from '@kbn/test'; import { FtrConfigProvider } from '@kbn/test';
import { import { FtrProviderContext } from '../services/ftr_context_provider';
FtrProviderContext, import { Es, KibanaServer, Retry, Auth } from '../services';
KibanaServer,
Es,
RetryService,
} from '@kbn/ftr-common-functional-services';
import { Auth } from '../services/auth';
import { InputDelays } from '../services/input_delays'; import { InputDelays } from '../services/input_delays';
import { KibanaUrl } from '../services/kibana_url'; import { KibanaUrl } from '../services/kibana_url';
@ -37,7 +32,7 @@ export interface BaseStepCtx {
kbnUrl: KibanaUrl; kbnUrl: KibanaUrl;
kibanaServer: KibanaServer; kibanaServer: KibanaServer;
es: Es; es: Es;
retry: RetryService; retry: Retry;
auth: Auth; auth: Auth;
} }
@ -141,7 +136,7 @@ export class Journey<CtxExt extends object> {
getService('kibanaServer'), getService('kibanaServer'),
getService('es'), getService('es'),
getService('retry'), getService('retry'),
new Auth(getService('config'), getService('log'), getService('kibanaServer')), getService('auth'),
this.config this.config
).initMochaSuite(this.#steps); ).initMochaSuite(this.#steps);
} }

View file

@ -11,7 +11,7 @@ import Path from 'path';
import { v4 as uuidV4 } from 'uuid'; import { v4 as uuidV4 } from 'uuid';
import { REPO_ROOT } from '@kbn/repo-info'; import { REPO_ROOT } from '@kbn/repo-info';
import type { FtrConfigProviderContext, FtrConfigProvider } from '@kbn/test'; import type { FtrConfigProviderContext, FtrConfigProvider } from '@kbn/test';
import { commonFunctionalServices } from '@kbn/ftr-common-functional-services'; import { services } from '../services';
import { AnyStep } from './journey'; import { AnyStep } from './journey';
import { JourneyConfig } from './journey_config'; import { JourneyConfig } from './journey_config';
@ -66,7 +66,7 @@ export function makeFtrConfigProvider(
bail: true, bail: true,
}, },
services: commonFunctionalServices, services,
pageObjects: {}, pageObjects: {},
servicesRequiredForTestAnalysis: ['performance', 'journeyConfig'], servicesRequiredForTestAnalysis: ['performance', 'journeyConfig'],

View file

@ -9,20 +9,19 @@
import Url from 'url'; import Url from 'url';
import { inspect, format } from 'util'; import { inspect, format } from 'util';
import { setTimeout } from 'timers/promises'; import { setTimeout } from 'timers/promises';
import * as Rx from 'rxjs'; import * as Rx from 'rxjs';
import apmNode from 'elastic-apm-node'; import apmNode from 'elastic-apm-node';
import playwright, { ChromiumBrowser, Page, BrowserContext, CDPSession, Request } from 'playwright'; import playwright, { ChromiumBrowser, Page, BrowserContext, CDPSession, Request } from 'playwright';
import { asyncMap, asyncForEach } from '@kbn/std'; import { asyncMap, asyncForEach } from '@kbn/std';
import { ToolingLog } from '@kbn/tooling-log'; import { ToolingLog } from '@kbn/tooling-log';
import { Config } from '@kbn/test'; import { Config } from '@kbn/test';
import { EsArchiver, KibanaServer, Es, RetryService } from '@kbn/ftr-common-functional-services';
import { import {
ELASTIC_HTTP_VERSION_HEADER, ELASTIC_HTTP_VERSION_HEADER,
X_ELASTIC_INTERNAL_ORIGIN_REQUEST, X_ELASTIC_INTERNAL_ORIGIN_REQUEST,
} from '@kbn/core-http-common'; } from '@kbn/core-http-common';
import { Auth } from '../services/auth'; import { AxiosError } from 'axios';
import { Auth, Es, EsArchiver, KibanaServer, Retry } from '../services';
import { getInputDelays } from '../services/input_delays'; import { getInputDelays } from '../services/input_delays';
import { KibanaUrl } from '../services/kibana_url'; import { KibanaUrl } from '../services/kibana_url';
@ -40,7 +39,7 @@ export class JourneyFtrHarness {
private readonly esArchiver: EsArchiver, private readonly esArchiver: EsArchiver,
private readonly kibanaServer: KibanaServer, private readonly kibanaServer: KibanaServer,
private readonly es: Es, private readonly es: Es,
private readonly retry: RetryService, private readonly retry: Retry,
private readonly auth: Auth, private readonly auth: Auth,
private readonly journeyConfig: JourneyConfig<any> private readonly journeyConfig: JourneyConfig<any>
) { ) {
@ -63,15 +62,24 @@ export class JourneyFtrHarness {
private async updateTelemetryAndAPMLabels(labels: { [k: string]: string }) { private async updateTelemetryAndAPMLabels(labels: { [k: string]: string }) {
this.log.info(`Updating telemetry & APM labels: ${JSON.stringify(labels)}`); this.log.info(`Updating telemetry & APM labels: ${JSON.stringify(labels)}`);
await this.kibanaServer.request({ try {
path: '/internal/core/_settings', await this.kibanaServer.request({
method: 'PUT', path: '/internal/core/_settings',
headers: { method: 'PUT',
[ELASTIC_HTTP_VERSION_HEADER]: '1', headers: {
[X_ELASTIC_INTERNAL_ORIGIN_REQUEST]: 'ftr', [ELASTIC_HTTP_VERSION_HEADER]: '1',
}, [X_ELASTIC_INTERNAL_ORIGIN_REQUEST]: 'ftr',
body: { telemetry: { labels } }, },
}); body: { telemetry: { labels } },
});
} catch (error) {
const statusCode = (error as AxiosError).response?.status;
if (statusCode === 404) {
throw new Error(
`Failed to update labels, supported Kibana version is 8.11.0+ and must be started with "coreApp.allowDynamicConfigOverrides:true"`
);
} else throw error;
}
} }
private async setupApm() { private async setupApm() {
@ -385,7 +393,7 @@ export class JourneyFtrHarness {
} }
const isServerlessProject = !!this.config.get('serverless'); const isServerlessProject = !!this.config.get('serverless');
const kibanaPage = getNewPageObject(isServerlessProject, page, this.log); const kibanaPage = getNewPageObject(isServerlessProject, page, this.log, this.retry);
this.#_ctx = this.journeyConfig.getExtendedStepCtx({ this.#_ctx = this.journeyConfig.getExtendedStepCtx({
kibanaPage, kibanaPage,

View file

@ -10,9 +10,7 @@ import Url from 'url';
import { format } from 'util'; import { format } from 'util';
import axios, { AxiosResponse } from 'axios'; import axios, { AxiosResponse } from 'axios';
import { ToolingLog } from '@kbn/tooling-log'; import { FtrService } from './ftr_context_provider';
import { Config } from '@kbn/test';
import { KibanaServer } from '@kbn/ftr-common-functional-services';
export interface Credentials { export interface Credentials {
username: string; username: string;
@ -22,12 +20,10 @@ export interface Credentials {
function extractCookieValue(authResponse: AxiosResponse) { function extractCookieValue(authResponse: AxiosResponse) {
return authResponse.headers['set-cookie']?.[0].toString().split(';')[0].split('sid=')[1] ?? ''; return authResponse.headers['set-cookie']?.[0].toString().split(';')[0].split('sid=')[1] ?? '';
} }
export class Auth { export class AuthService extends FtrService {
constructor( private readonly config = this.ctx.getService('config');
private readonly config: Config, private readonly log = this.ctx.getService('log');
private readonly log: ToolingLog, private readonly kibanaServer = this.ctx.getService('kibanaServer');
private readonly kibanaServer: KibanaServer
) {}
public async login(credentials?: Credentials) { public async login(credentials?: Credentials) {
const baseUrl = new URL( const baseUrl = new URL(

View file

@ -0,0 +1,20 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 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 { Client } from '@elastic/elasticsearch';
import { createEsClientForFtrConfig, ProvidedType } from '@kbn/test';
import { FtrProviderContext } from './ftr_context_provider';
export function EsProvider({ getService }: FtrProviderContext): Client {
const config = getService('config');
return createEsClientForFtrConfig(config);
}
export type Es = ProvidedType<typeof EsProvider>;

View file

@ -0,0 +1,13 @@
/*
* 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 { GenericFtrProviderContext, GenericFtrService } from '@kbn/test';
import { services } from '.';
export type FtrProviderContext = GenericFtrProviderContext<typeof services, {}>;
export class FtrService extends GenericFtrService<FtrProviderContext> {}

View file

@ -0,0 +1,28 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 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 { commonFunctionalServices, RetryService } from '@kbn/ftr-common-functional-services';
import { EsArchiverProvider } from '@kbn/ftr-common-functional-services/services/es_archiver';
import { KibanaServerProvider } from '@kbn/ftr-common-functional-services/services/kibana_server';
import { ProvidedType } from '@kbn/test';
import { EsProvider } from './es';
import { AuthService } from './auth';
export const services = {
es: EsProvider,
kibanaServer: commonFunctionalServices.kibanaServer,
esArchiver: commonFunctionalServices.esArchiver,
retry: commonFunctionalServices.retry,
auth: AuthService,
};
export type EsArchiver = ProvidedType<typeof EsArchiverProvider>;
export type KibanaServer = ProvidedType<typeof KibanaServerProvider>;
export type Es = ProvidedType<typeof EsProvider>;
export type Auth = AuthService;
export type Retry = RetryService;

View file

@ -8,9 +8,10 @@
import { ToolingLog } from '@kbn/tooling-log'; import { ToolingLog } from '@kbn/tooling-log';
import { Page } from 'playwright'; import { Page } from 'playwright';
import { Retry } from '..';
import { KibanaPage } from './kibana_page'; import { KibanaPage } from './kibana_page';
import { ProjectPage } from './project_page'; import { ProjectPage } from './project_page';
export function getNewPageObject(isServerless: boolean, page: Page, log: ToolingLog) { export function getNewPageObject(isServerless: boolean, page: Page, log: ToolingLog, retry: Retry) {
return isServerless ? new ProjectPage(page, log) : new KibanaPage(page, log); return isServerless ? new ProjectPage(page, log, retry) : new KibanaPage(page, log, retry);
} }

View file

@ -9,6 +9,7 @@
import { subj } from '@kbn/test-subj-selector'; import { subj } from '@kbn/test-subj-selector';
import { ToolingLog } from '@kbn/tooling-log'; import { ToolingLog } from '@kbn/tooling-log';
import { Page } from 'playwright'; import { Page } from 'playwright';
import { Retry } from '..';
interface WaitForRenderArgs { interface WaitForRenderArgs {
expectedItemsCount: number; expectedItemsCount: number;
@ -19,10 +20,12 @@ interface WaitForRenderArgs {
export class KibanaPage { export class KibanaPage {
readonly page: Page; readonly page: Page;
readonly log: ToolingLog; readonly log: ToolingLog;
readonly retry: Retry;
constructor(page: Page, log: ToolingLog) { constructor(page: Page, log: ToolingLog, retry: Retry) {
this.page = page; this.page = page;
this.log = log; this.log = log;
this.retry = retry;
} }
async waitForHeader() { async waitForHeader() {
@ -36,25 +39,32 @@ export class KibanaPage {
} }
async waitForRender({ expectedItemsCount, itemLocator, checkAttribute }: WaitForRenderArgs) { async waitForRender({ expectedItemsCount, itemLocator, checkAttribute }: WaitForRenderArgs) {
try { // we can't use `page.waitForFunction` because of CSP while testing on Cloud
await this.page.waitForFunction( await this.retry.waitFor(
function renderCompleted(args: WaitForRenderArgs) { `rendering of ${expectedItemsCount} elements with selector ${itemLocator} is completed`,
const renderingItems = Array.from(document.querySelectorAll(args.itemLocator)); async () => {
const allItemsLoaded = renderingItems.length === args.expectedItemsCount; const renderingItems = await this.page.$$(itemLocator);
return allItemsLoaded if (renderingItems.length === expectedItemsCount) {
? renderingItems.every((e) => e.getAttribute(args.checkAttribute) === 'true') // all components are loaded, checking if all are rendered
: false; const renderStatuses = await Promise.all(
}, renderingItems.map(async (item) => {
{ expectedItemsCount, itemLocator, checkAttribute } return (await item.getAttribute(checkAttribute)) === 'true';
); })
} catch (err) { );
const loaded = await this.page.$$(itemLocator); const rendered = renderStatuses.filter((isRendered) => isRendered === true);
const rendered = await this.page.$$(`${itemLocator}[${checkAttribute}="true"]`); this.log.debug(
this.log.error( `waitForRender: ${rendered.length} out of ${expectedItemsCount} are rendered...`
`'waitForRendering' failed: loaded - ${loaded.length}, rendered - ${rendered.length}, expected count - ${expectedItemsCount}` );
); return rendered.length === expectedItemsCount;
throw err; } else {
} // not all components are loaded yet
this.log.debug(
`waitForRender: ${renderingItems.length} out of ${expectedItemsCount} are loaded...`
);
return false;
}
}
);
} }
async waitForVisualizations(count: number) { async waitForVisualizations(count: number) {

View file

@ -13,7 +13,11 @@ export const journey = new Journey({
esArchives: ['test/functional/fixtures/es_archiver/many_fields'], esArchives: ['test/functional/fixtures/es_archiver/many_fields'],
}) })
.step('Go to Discover Page', async ({ page, kbnUrl, kibanaPage }) => { .step('Go to Discover Page', async ({ page, kbnUrl, kibanaPage }) => {
await page.goto(kbnUrl.get(`/app/discover`)); await page.goto(
kbnUrl.get(
`/app/discover#/?_g=(filters:!(),refreshInterval:(pause:!t,value:60000),time:(from:now-15m,to:now))&_a=(columns:!(),filters:!(),index:'35796250-bb09-11ec-a8e4-a9868e049a39',interval:auto,query:(language:kuery,query:''),sort:!())`
)
);
await kibanaPage.waitForHeader(); await kibanaPage.waitForHeader();
await page.waitForSelector('[data-test-subj="discoverDocTable"][data-render-complete="true"]'); await page.waitForSelector('[data-test-subj="discoverDocTable"][data-render-complete="true"]');
await page.waitForSelector(subj('globalLoadingIndicator-hidden')); await page.waitForSelector(subj('globalLoadingIndicator-hidden'));