[SLO] Synthetics based SLO e2e tests (#183637)

## Summary

Setting up Elastic/Synthetics based slo e2e tests !!

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Shahzad 2024-05-20 11:49:22 +02:00 committed by GitHub
parent 743ddd48cb
commit 8b7fa0d3f8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 317 additions and 9 deletions

View file

@ -63,6 +63,7 @@ disabled:
- x-pack/plugins/observability_solution/synthetics/e2e/synthetics/synthetics_run.ts
- x-pack/plugins/observability_solution/exploratory_view/e2e/synthetics_run.ts
- x-pack/plugins/observability_solution/ux/e2e/synthetics_run.ts
- x-pack/plugins/observability_solution/slo/e2e/synthetics_run.ts
# Configs that exist but weren't running in CI when this file was introduced
- x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/config.ts

View file

@ -0,0 +1,17 @@
steps:
- command: .buildkite/scripts/steps/functional/slo_plugin_e2e.sh
label: 'SLO Plugin @elastic/synthetics Tests'
agents:
queue: n2-4-spot
depends_on:
- build
- quick_checks
timeout_in_minutes: 30
artifact_paths:
- 'x-pack/plugins/observability_solution/slo/e2e/.journeys/**/*'
retry:
automatic:
- exit_status: '-1'
limit: 3
- exit_status: '*'
limit: 1

View file

@ -139,6 +139,10 @@ const uploadPipeline = (pipelineContent: string | object) => {
pipeline.push(getPipeline('.buildkite/pipelines/pull_request/ux_plugin_e2e.yml'));
}
if (await doAnyChangesMatch([/^x-pack\/plugins\/observability_solution/])) {
pipeline.push(getPipeline('.buildkite/pipelines/pull_request/slo_plugin_e2e.yml'));
}
if (
GITHUB_PR_LABELS.includes('ci:deploy-cloud') ||
GITHUB_PR_LABELS.includes('ci:cloud-deploy') ||

View file

@ -0,0 +1,16 @@
#!/usr/bin/env bash
set -euo pipefail
source .buildkite/scripts/common/util.sh
.buildkite/scripts/bootstrap.sh
.buildkite/scripts/download_build_artifacts.sh
export JOB=kibana-ux-plugin-synthetics
echo "--- SLO @elastic/synthetics Tests"
cd "$XPACK_DIR"
node plugins/observability_solution/slo/scripts/e2e.js --kibana-install-dir "$KIBANA_BUILD_LOCATION" ${GREP:+--grep \"${GREP}\"}

View file

@ -19,3 +19,4 @@ export { cli } from './src/cli';
export { generate } from './src/generate';
export { cleanup } from './src/cleanup';
export { createConfig, readConfig } from './src/lib/create_config';
export { DEFAULTS } from './src/constants';

View file

@ -6,14 +6,15 @@
*/
import { ToolingLog } from '@kbn/tooling-log';
import { CliOptions } from './types';
import { cliOptionsToPartialConfig } from './lib/cli_to_partial_config';
import { createConfig, readConfig } from './lib/create_config';
import { getEsClient } from './lib/get_es_client';
import { parseCliOptions } from './lib/parse_cli_options';
import { run } from './run';
export async function cli() {
const options = parseCliOptions();
export async function cli(cliOptions?: CliOptions) {
const options = cliOptions ?? parseCliOptions();
const partialConfig = options.config
? await readConfig(options.config)
: cliOptionsToPartialConfig(options);

View file

@ -34,5 +34,5 @@ export const DEFAULTS = {
EVENT_TEMPLATE: 'good',
REDUCE_WEEKEND_TRAFFIC_BY: 0,
EPHEMERAL_PROJECT_IDS: 0,
ALIGN_EVENTS_TO_INTERVAL: 0,
ALIGN_EVENTS_TO_INTERVAL: true,
};

View file

@ -14,7 +14,7 @@ export function cliOptionsToPartialConfig(options: CliOptions) {
const schedule: Schedule = {
template: options.eventTemplate,
start: options.lookback,
end: false,
end: options.scheduleEnd ?? false,
};
const decodedDataset = DatasetRT.decode(options.dataset);

View file

@ -62,7 +62,7 @@ export function createConfig(partialConfig: PartialConfig = {}) {
concurrency: DEFAULTS.CONCURRENCY,
reduceWeekendTrafficBy: DEFAULTS.REDUCE_WEEKEND_TRAFFIC_BY,
ephemeralProjectIds: DEFAULTS.EPHEMERAL_PROJECT_IDS,
alignEventsToInterval: DEFAULTS.ALIGN_EVENTS_TO_INTERVAL === 1,
alignEventsToInterval: DEFAULTS.ALIGN_EVENTS_TO_INTERVAL,
...(partialConfig.indexing ?? {}),
},
schedule: partialConfig.schedule ?? [schedule],

View file

@ -52,7 +52,7 @@ export async function indexSchedule(config: Config, client: Client, logger: Tool
logger.info(
`Indexing "${schedule.template}" events from ${startTs.toISOString()} to ${
end === false ? 'indefinatly' : end.toISOString()
end === false ? 'indefinitely' : end.toISOString()
}`
);
await createEvents(

View file

@ -159,7 +159,7 @@ export interface Point {
}
export interface CliOptions {
config: string;
config?: string;
lookback: string;
eventsPerCycle: number;
payloadSize: number;
@ -170,7 +170,7 @@ export interface CliOptions {
elasticsearchHost: string;
elasticsearchUsername: string;
elasticsearchPassword: string;
elasticsearchApiKey: undefined | string;
elasticsearchApiKey?: undefined | string;
kibanaUrl: string;
kibanaUsername: string;
kibanaPassword: string;
@ -179,4 +179,5 @@ export interface CliOptions {
reduceWeekendTrafficBy: number;
ephemeralProjectIds: number;
alignEventsToInterval: boolean;
scheduleEnd?: string;
}

View file

@ -0,0 +1,8 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export * from './slos_overview.journey';

View file

@ -0,0 +1,50 @@
/*
* 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 { journey, step, before, expect } from '@elastic/synthetics';
import { RetryService } from '@kbn/ftr-common-functional-services';
import { SLoDataService } from '../services/slo_data_service';
import { sloAppPageProvider } from '../page_objects/slo_app';
journey(`SLOsOverview`, async ({ page, params }) => {
const sloApp = sloAppPageProvider({ page, kibanaUrl: params.kibanaUrl });
const dataService = new SLoDataService({
kibanaUrl: params.kibanaUrl,
elasticsearchUrl: params.elasticsearchUrl,
getService: params.getService,
});
const retry: RetryService = params.getService('retry');
before(async () => {
await dataService.generateSloData();
await dataService.addSLO();
});
step('Go to slos overview', async () => {
await sloApp.navigateToOverview(true);
});
step('validate data retention tab', async () => {
await retry.tryWithRetries(
'check if slos are displayed',
async () => {
await page.waitForSelector('text="Test Stack SLO"');
const cards = await page.locator('text="Test Stack SLO"').all();
expect(cards.length > 5).toBeTruthy();
},
{
retryCount: 50,
retryDelay: 20000,
timeout: 60 * 20000,
},
async () => {
await page.getByTestId('querySubmitButton').click();
}
);
});
});

View file

@ -0,0 +1,33 @@
/*
* 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 { Page } from '@elastic/synthetics';
import { loginPageProvider } from '@kbn/synthetics-plugin/e2e/page_objects/login';
import { utilsPageProvider } from '@kbn/synthetics-plugin/e2e/page_objects/utils';
import { recordVideo } from '@kbn/synthetics-plugin/e2e/helpers/record_video';
export function sloAppPageProvider({ page, kibanaUrl }: { page: Page; kibanaUrl: string }) {
page.setDefaultTimeout(60 * 1000);
recordVideo(page);
return {
...loginPageProvider({
page,
username: 'elastic',
password: 'changeme',
}),
...utilsPageProvider({ page }),
async navigateToOverview(doLogin = false) {
await page.goto(`${kibanaUrl}/app/slo`, {
waitUntil: 'networkidle',
});
if (doLogin) {
await this.loginToKibana();
}
},
};
}

View file

@ -0,0 +1,94 @@
/*
* 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 { KbnClient } from '@kbn/test';
import { cli, DEFAULTS } from '@kbn/data-forge';
export class SLoDataService {
kibanaUrl: string;
elasticsearchUrl: string;
params: Record<string, any>;
requester: KbnClient['requester'];
constructor(params: Record<string, any>) {
this.kibanaUrl = params.kibanaUrl;
this.elasticsearchUrl = params.elasticsearchUrl;
this.requester = params.getService('kibanaServer').requester;
this.params = params;
}
async generateSloData({
lookback = 'now-1d',
eventsPerCycle = 50,
}: {
lookback?: string;
eventsPerCycle?: number;
} = {}) {
await cli({
kibanaUrl: this.kibanaUrl,
elasticsearchHost: this.elasticsearchUrl,
lookback: DEFAULTS.LOOKBACK,
eventsPerCycle: DEFAULTS.EVENTS_PER_CYCLE,
payloadSize: DEFAULTS.PAYLOAD_SIZE,
concurrency: DEFAULTS.CONCURRENCY,
indexInterval: 10_000,
dataset: 'fake_stack',
scenario: DEFAULTS.SCENARIO,
elasticsearchUsername: DEFAULTS.ELASTICSEARCH_USERNAME,
elasticsearchPassword: DEFAULTS.ELASTICSEARCH_PASSWORD,
kibanaUsername: DEFAULTS.KIBANA_USERNAME,
kibanaPassword: DEFAULTS.KIBANA_PASSWORD,
installKibanaAssets: true,
eventTemplate: DEFAULTS.EVENT_TEMPLATE,
reduceWeekendTrafficBy: DEFAULTS.REDUCE_WEEKEND_TRAFFIC_BY,
ephemeralProjectIds: DEFAULTS.EPHEMERAL_PROJECT_IDS,
alignEventsToInterval: DEFAULTS.ALIGN_EVENTS_TO_INTERVAL,
scheduleEnd: 'now+10m',
}).then((res) => {
// eslint-disable-next-line no-console
console.log(res);
});
}
async addSLO() {
const example = {
name: 'Test Stack SLO',
description: '',
indicator: {
type: 'sli.kql.custom',
params: {
index: 'kbn-data-forge-fake_stack.admin-console-*',
filter: '',
good: 'log.level : "INFO" ',
total: '',
timestampField: '@timestamp',
},
},
budgetingMethod: 'occurrences',
timeWindow: {
duration: '30d',
type: 'rolling',
},
objective: {
target: 0.99,
},
tags: [],
groupBy: ['user.id'],
};
try {
const { data } = await this.requester.request({
description: 'get monitor by id',
path: '/api/observability/slos',
body: example,
method: 'POST',
});
return data;
} catch (e) {
console.error(e);
}
}
}

View file

@ -0,0 +1,35 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { FtrConfigProviderContext } from '@kbn/test';
import { SyntheticsRunner, argv } from '@kbn/synthetics-plugin/e2e';
const { headless, grep, bail: pauseOnError } = argv;
async function runE2ETests({ readConfigFile }: FtrConfigProviderContext) {
const kibanaConfig = await readConfigFile(require.resolve('@kbn/synthetics-plugin/e2e/config'));
return {
...kibanaConfig.getAll(),
testRunner: async ({ getService }: any) => {
const syntheticsRunner = new SyntheticsRunner(getService, {
headless,
match: grep,
pauseOnError,
});
await syntheticsRunner.setup();
await syntheticsRunner.loadTestFiles(async () => {
require('./journeys');
});
await syntheticsRunner.run();
},
};
}
// eslint-disable-next-line import/no-default-export
export default runE2ETests;

View file

@ -0,0 +1,18 @@
{
"extends": "../../../../../tsconfig.base.json",
"exclude": ["tmp", "target/**/*"],
"include": ["**/*"],
"compilerOptions": {
"outDir": "target/types",
"types": [ "node"],
"isolatedModules": false,
},
"kbn_references": [
{ "path": "../../../../test/tsconfig.json" },
{ "path": "../../../../../test/tsconfig.json" },
"@kbn/test",
"@kbn/ftr-common-functional-services",
"@kbn/synthetics-plugin/e2e",
"@kbn/data-forge",
]
}

View file

@ -108,6 +108,7 @@ export function SloCardItem({ slo, rules, activeAlerts, historicalSummary, refet
return (
<>
<EuiPanel
className="sloCardItem"
panelRef={containerRef as React.Ref<HTMLDivElement>}
onMouseOver={() => {
if (!isMouseOver) {

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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
/* eslint-disable no-console */
const { executeSyntheticsRunner } = require('@kbn/synthetics-plugin/scripts/base_e2e');
const path = require('path');
const e2eDir = path.join(__dirname, '../e2e');
executeSyntheticsRunner(e2eDir);

View file

@ -112,7 +112,11 @@ export class SyntheticsRunner {
let results: PromiseType<ReturnType<typeof syntheticsRun>> = {};
for (let i = 0; i < noOfRuns; i++) {
results = await syntheticsRun({
params: { kibanaUrl: this.kibanaUrl, getService: this.getService },
params: {
kibanaUrl: this.kibanaUrl,
getService: this.getService,
elasticsearchUrl: this.elasticsearchUrl,
},
playwrightOptions: {
headless: headless ?? !CI,
testIdAttribute: 'data-test-subj',

View file

@ -0,0 +1,11 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export { SyntheticsRunner } from './helpers/synthetics_runner';
export { argv } from './helpers/parse_args_params';
export { loginPageProvider } from './page_objects/login';
export { utilsPageProvider } from './page_objects/utils';