mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[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:
parent
743ddd48cb
commit
8b7fa0d3f8
21 changed files with 317 additions and 9 deletions
|
@ -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
|
||||
|
|
17
.buildkite/pipelines/pull_request/slo_plugin_e2e.yml
Normal file
17
.buildkite/pipelines/pull_request/slo_plugin_e2e.yml
Normal 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
|
|
@ -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') ||
|
||||
|
|
16
.buildkite/scripts/steps/functional/slo_plugin_e2e.sh
Executable file
16
.buildkite/scripts/steps/functional/slo_plugin_e2e.sh
Executable 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}\"}
|
|
@ -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';
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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],
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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';
|
|
@ -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();
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
|
@ -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();
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
18
x-pack/plugins/observability_solution/slo/e2e/tsconfig.json
Normal file
18
x-pack/plugins/observability_solution/slo/e2e/tsconfig.json
Normal 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",
|
||||
]
|
||||
}
|
|
@ -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) {
|
||||
|
|
13
x-pack/plugins/observability_solution/slo/scripts/e2e.js
Normal file
13
x-pack/plugins/observability_solution/slo/scripts/e2e.js
Normal 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);
|
|
@ -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',
|
||||
|
|
|
@ -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';
|
Loading…
Add table
Add a link
Reference in a new issue