Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Performance Test service and config refactor #128258

Merged
merged 16 commits into from
Apr 12, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,6 @@ sleep 120

cd "$XPACK_DIR"

jobId=$(npx uuid)
export TEST_JOB_ID="$jobId"

journeys=("ecommerce_dashboard" "flight_dashboard" "web_logs_dashboard" "promotion_tracking_dashboard")

for i in "${journeys[@]}"; do
Expand Down
7 changes: 5 additions & 2 deletions x-pack/test/performance/config.playwright.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,11 @@ export default async function ({ readConfigFile, log }: FtrConfigProviderContext

const testFiles = [require.resolve('./tests/playwright')];

const testJobId = process.env.TEST_JOB_ID ?? uuid();
log.info(`👷 JOB ID ${testJobId}👷`);
const testBuildId = process.env.BUILDKITE_BUILD_ID ?? `local-${uuid()}`;
const testJobId = process.env.BUILDKITE_JOB_ID ?? `local-${uuid()}`;
const executionId = uuid();

log.info(` 👷‍♀️ BUILD ID ${testBuildId}\n 👷 JOB ID ${testJobId}\n 👷‍♂️ EXECUTION ID:${executionId}`);

return {
testFiles,
Expand Down
62 changes: 62 additions & 0 deletions x-pack/test/performance/services/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import axios, { AxiosResponse } from 'axios';
import Url from 'url';
import { FtrService, FtrProviderContext } from '../ftr_provider_context';

export interface Credentials {
username: string;
password: string;
}

function extractCookieValue(authResponse: AxiosResponse) {
return authResponse.headers['set-cookie'][0].toString().split(';')[0].split('sid=')[1] as string;
}
export class AuthService extends FtrService {
private readonly kibanaServer = this.ctx.getService('kibanaServer');
private readonly config = this.ctx.getService('config');

constructor(ctx: FtrProviderContext) {
super(ctx);
}

public async login({ username, password }: Credentials) {
const headers = {
'content-type': 'application/json',
'kbn-version': await this.kibanaServer.version.get(),
'sec-fetch-mode': 'cors',
'sec-fetch-site': 'same-origin',
};

const baseUrl = Url.format({
protocol: this.config.get('servers.kibana.protocol'),
hostname: this.config.get('servers.kibana.hostname'),
port: this.config.get('servers.kibana.port'),
});

const loginUrl = baseUrl + '/internal/security/login';
const provider = baseUrl.includes('localhost') ? 'basic' : 'cloud-basic';

const authBody = {
providerType: 'basic',
providerName: provider,
currentURL: `${baseUrl}/login?next=%2F`,
params: { username, password },
};

const authResponse = await axios.post(loginUrl, authBody, { headers });

return {
name: 'sid',
value: extractCookieValue(authResponse),
url: baseUrl,
};
}
}

export const AuthProvider = (ctx: FtrProviderContext) => new AuthService(ctx);
2 changes: 2 additions & 0 deletions x-pack/test/performance/services/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import { services as functionalServices } from '../../functional/services';
import { PerformanceTestingService } from './performance';
import { InputDelaysProvider } from './input_delays';
import { AuthProvider } from './auth';

export const services = {
es: functionalServices.es,
Expand All @@ -16,4 +17,5 @@ export const services = {
retry: functionalServices.retry,
performance: PerformanceTestingService,
inputDelays: InputDelaysProvider,
auth: AuthProvider,
};
193 changes: 80 additions & 113 deletions x-pack/test/performance/services/performance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,52 +10,46 @@
import Url from 'url';
import { inspect } from 'util';
import apm, { Span, Transaction } from 'elastic-apm-node';
import { setTimeout } from 'timers/promises';
import playwright, { ChromiumBrowser, Page, BrowserContext } from 'playwright';
import playwright, { ChromiumBrowser, Page, BrowserContext, CDPSession } from 'playwright';
import { FtrService, FtrProviderContext } from '../ftr_provider_context';

type StorageState = Awaited<ReturnType<BrowserContext['storageState']>>;

apm.start({
secretToken: 'Q5q5rWQEw6tKeirBpw',
serverUrl: 'https://2fad4006bf784bb8a54e52f4a5862609.apm.us-west1.gcp.cloud.es.io:443',
serviceName: 'functional test runner',
serverUrl: 'https://kibana-ops-e2e-perf.apm.us-central1.gcp.cloud.es.io:443',
secretToken: 'CTs9y3cvcfq13bQqsB',
});

interface StepCtx {
export interface StepCtx {
page: Page;
kibanaUrl: string;
}

type StepFn = (ctx: StepCtx) => Promise<void>;
type Steps = Array<{ name: string; fn: StepFn }>;
export type Steps = Array<{ name: string; handler: StepFn }>;

export class PerformanceTestingService extends FtrService {
private readonly auth = this.ctx.getService('auth');
private readonly config = this.ctx.getService('config');
private readonly lifecycle = this.ctx.getService('lifecycle');
private readonly inputDelays = this.ctx.getService('inputDelays');

private browser: ChromiumBrowser | undefined;
private storageState: StorageState | undefined;
private currentSpanStack: Array<Span | null> = [];
private currentTransaction: Transaction | undefined | null;
private currentTransaction: Transaction | undefined | null = undefined;

constructor(ctx: FtrProviderContext) {
super(ctx);
}

this.lifecycle.beforeTests.add(async () => {
await this.withTransaction('Journey setup', async () => {
await this.getStorageState();
});
});

this.lifecycle.cleanup.add(async () => {
apm.flush();
await setTimeout(5000);
await this.browser?.close();
private getKibanaUrl() {
return Url.format({
protocol: this.config.get('servers.kibana.protocol'),
hostname: this.config.get('servers.kibana.hostname'),
port: this.config.get('servers.kibana.port'),
});
}

private async withTransaction<T>(name: string, block: () => Promise<T>) {
try {
if (this.currentTransaction !== undefined) {
if (this.currentTransaction) {
throw new Error(
`Transaction already started, make sure you end transaction ${this.currentTransaction?.name}`
);
Expand Down Expand Up @@ -105,42 +99,6 @@ export class PerformanceTestingService extends FtrService {
?.traceparent;
}

private async getStorageState() {
if (this.storageState) {
return this.storageState;
}

await this.withSpan('initial login', undefined, async () => {
const kibanaUrl = Url.format({
protocol: this.config.get('servers.kibana.protocol'),
hostname: this.config.get('servers.kibana.hostname'),
port: this.config.get('servers.kibana.port'),
});

const browser = await this.getBrowserInstance();
const context = await browser.newContext();
const page = await context.newPage();
await this.interceptBrowserRequests(page);
await page.goto(`${kibanaUrl}`);

const usernameLocator = page.locator('[data-test-subj=loginUsername]');
const passwordLocator = page.locator('[data-test-subj=loginPassword]');
const submitButtonLocator = page.locator('[data-test-subj=loginSubmit]');

await usernameLocator?.type('elastic', { delay: this.inputDelays.TYPING });
await passwordLocator?.type('changeme', { delay: this.inputDelays.TYPING });
await submitButtonLocator?.click({ delay: this.inputDelays.MOUSE_CLICK });

await page.waitForSelector('#headerUserMenu');

this.storageState = await page.context().storageState();
await page.close();
await context.close();
});

return this.storageState;
}

private async getBrowserInstance() {
if (this.browser) {
return this.browser;
Expand Down Expand Up @@ -179,70 +137,79 @@ export class PerformanceTestingService extends FtrService {
});
}

public makePage(journeyName: string) {
const steps: Steps = [];

it(journeyName, async () => {
await this.withTransaction(`Journey ${journeyName}`, async () => {
const browser = await this.getBrowserInstance();
const context = await browser.newContext({
viewport: { width: 1600, height: 1200 },
storageState: await this.getStorageState(),
});

const page = await context.newPage();
page.on('console', (message) => {
(async () => {
try {
const args = await Promise.all(
message.args().map(async (handle) => handle.jsonValue())
);
public runUserJourney(
journeyName: string,
steps: Steps,
{ requireAuth }: { requireAuth: boolean }
) {
return this.withTransaction(`Journey ${journeyName}`, async () => {
const browser = await this.getBrowserInstance();
const viewport = { width: 1600, height: 1200 };
const context = await browser.newContext({ viewport });

const { url, lineNumber, columnNumber } = message.location();
if (!requireAuth) {
const cookie = await this.auth.login({ username: 'elastic', password: 'changeme' });
await context.addCookies([cookie]);
}

const location = `${url},${lineNumber},${columnNumber}`;
const page = await context.newPage();
if (!process.env.NO_BROWSER_LOG) {
page.on('console', this.onConsoleEvent());
}
const client = await this.sendCDPCommands(context, page);

const text = args.length
? args.map((arg) => (typeof arg === 'string' ? arg : inspect(arg))).join(' ')
: message.text();
await this.interceptBrowserRequests(page);
await this.handleSteps(steps, page);
await this.tearDown(page, client, context);
});
}

console.log(`[console.${message.type()}]`, text);
console.log(' ', location);
} catch (e) {
console.error('Failed to evaluate console.log line', e);
}
})();
});
const client = await this.sendCDPCommands(context, page);
private async tearDown(page: Page, client: CDPSession, context: BrowserContext) {
if (page) {
apm.flush();
await client.detach();
await page.close();
await context.close();
}
}

await this.interceptBrowserRequests(page);
public async shutdownBrowser() {
if (this.browser) {
await (await this.getBrowserInstance()).close();
}
}

private async handleSteps(steps: Array<{ name: string; handler: StepFn }>, page: Page) {
for (const step of steps) {
await this.withSpan(`step: ${step.name}`, 'step', async () => {
try {
for (const step of steps) {
await this.withSpan(`step: ${step.name}`, 'step', async () => {
try {
await step.fn({ page });
} catch (e) {
const error = new Error(`Step [${step.name}] failed: ${e.message}`);
error.stack = e.stack;
throw error;
}
});
}
} finally {
if (page) {
await client.detach();
await page.close();
await context.close();
}
await step.handler({ page, kibanaUrl: this.getKibanaUrl() });
} catch (e) {
const error = new Error(`Step [${step.name}] failed: ${e.message}`);
error.stack = e.stack;
}
});
});
}
}

private onConsoleEvent() {
return async (message: playwright.ConsoleMessage) => {
try {
const args = await Promise.all(message.args().map(async (handle) => handle.jsonValue()));

const { url, lineNumber, columnNumber } = message.location();

return {
step: (name: string, fn: StepFn) => {
steps.push({ name, fn });
},
const location = `${url},${lineNumber},${columnNumber}`;

const text = args.length
? args.map((arg) => (typeof arg === 'string' ? arg : inspect(arg, false, null))).join(' ')
: message.text();

console.log(`[console.${message.type()}]`, text);
console.log(' ', location);
} catch (e) {
console.error('Failed to evaluate console.log line', e);
}
};
}
}
Loading