[ML] Log pattern analysis functional tests (#160171)

Adds functional tests for the Log Pattern Analysis AIOPs page and
Discover integration.

ML app:
Selects a message field, checks category counts are correct, selects
bother "filter in" and "filter out" buttons which redirect to Discover
and apply a filter. Checks the total doc counts in discover match the
selected category.

In Discover:
Selects a message field which opens the flyout, checks category counts
are correct, selects bother "filter in" and "filter out" buttons which
close the flyout and apply a filter. Checks the total doc counts in
discover match the selected category.
This commit is contained in:
James Gowdy 2023-06-30 09:49:14 +01:00 committed by GitHub
parent 079763bfff
commit 3dfbe8ab98
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 376 additions and 18 deletions

View file

@ -171,6 +171,7 @@ export const CategoryTable: FC<Props> = ({
description: labels.singleSelect.in,
icon: 'plusInCircle',
type: 'icon',
'data-test-subj': 'aiopsLogPatternsActionFilterInButton',
onClick: (category) => openInDiscover(QUERY_MODE.INCLUDE, category),
},
{
@ -178,6 +179,7 @@ export const CategoryTable: FC<Props> = ({
description: labels.singleSelect.out,
icon: 'minusInCircle',
type: 'icon',
'data-test-subj': 'aiopsLogPatternsActionFilterOutButton',
onClick: (category) => openInDiscover(QUERY_MODE.EXCLUDE, category),
},
],
@ -232,6 +234,7 @@ export const CategoryTable: FC<Props> = ({
onTableChange={onTableChange}
pagination={pagination}
sorting={sorting}
data-test-subj="aiopsLogPatternsTable"
rowProps={(category) => {
return enableRowActions
? {

View file

@ -30,7 +30,7 @@ export const TableHeader: FC<Props> = ({
<>
<EuiFlexGroup gutterSize="none" alignItems="center" css={{ minHeight: euiTheme.euiSizeXL }}>
<EuiFlexItem>
<EuiText size="s">
<EuiText size="s" data-test-subj="aiopsLogPatternsFoundCount">
<FormattedMessage
id="xpack.aiops.logCategorization.counts"
defaultMessage="{count} patterns found"

View file

@ -198,7 +198,7 @@ export const LogCategorizationFlyout: FC<LogCategorizationPageProps> = ({
<EuiFlexGroup gutterSize="s" alignItems="center">
<EuiFlexItem grow={false}>
<EuiTitle size="m">
<h2 id="flyoutTitle">
<h2 id="flyoutTitle" data-test-subj="mlJobSelectorFlyoutTitle">
<FormattedMessage
id="xpack.aiops.categorizeFlyout.title"
defaultMessage="Pattern analysis of {name}"
@ -216,7 +216,7 @@ export const LogCategorizationFlyout: FC<LogCategorizationPageProps> = ({
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutHeader>
<EuiFlyoutBody data-test-subj={'mlJobSelectorFlyoutBody'}>
<EuiFlyoutBody data-test-subj="mlJobSelectorFlyoutBody">
{loading === true ? <LoadingCategorization onClose={onClose} /> : null}
<InformationText

View file

@ -230,7 +230,7 @@ export const LogCategorizationPage: FC = () => {
};
return (
<EuiPageBody data-test-subj="aiopsLogCategorizationPage" paddingSize="none" panelled={false}>
<EuiPageBody data-test-subj="aiopsLogPatternAnalysisPage" paddingSize="none" panelled={false}>
<PageHeader />
<EuiSpacer />
<EuiFlexGroup gutterSize="none">
@ -258,6 +258,7 @@ export const LogCategorizationPage: FC = () => {
onChange={onFieldChange}
selectedOptions={selectedField === undefined ? undefined : [{ label: selectedField }]}
singleSelection={{ asPlainText: true }}
data-test-subj="aiopsLogPatternAnalysisCategoryField"
/>
</EuiFormRow>
</EuiFlexItem>
@ -268,6 +269,7 @@ export const LogCategorizationPage: FC = () => {
onClick={() => {
loadCategories();
}}
data-test-subj="aiopsLogPatternAnalysisRunButton"
>
<FormattedMessage
id="xpack.aiops.logCategorization.runButton"

View file

@ -221,13 +221,11 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
);
await ml.testExecution.logTestStep('check log pattern analysis page loaded correctly');
await aiops.logPatternAnalysisPageProvider.assertLogCategorizationPageExists();
await aiops.logPatternAnalysisPageProvider.assertTotalDocumentCount(
await aiops.logPatternAnalysisPage.assertLogPatternAnalysisPageExists();
await aiops.logPatternAnalysisPage.assertTotalDocumentCount(
testData.action.expected.totalDocCount
);
await aiops.logPatternAnalysisPageProvider.assertQueryInput(
testData.action.expected.queryBar
);
await aiops.logPatternAnalysisPage.assertQueryInput(testData.action.expected.queryBar);
}
}
});

View file

@ -31,5 +31,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) {
loadTestFile(require.resolve('./explain_log_rate_spikes'));
loadTestFile(require.resolve('./change_point_detection'));
loadTestFile(require.resolve('./log_pattern_analysis'));
loadTestFile(require.resolve('./log_pattern_analysis_in_discover'));
});
}

View file

@ -0,0 +1,102 @@
/*
* 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 { FtrProviderContext } from '../../ftr_provider_context';
export default function ({ getPageObjects, getService }: FtrProviderContext) {
const elasticChart = getService('elasticChart');
const esArchiver = getService('esArchiver');
const aiops = getService('aiops');
const browser = getService('browser');
const retry = getService('retry');
const ml = getService('ml');
const selectedField = '@message';
const totalDocCount = 14005;
async function retrySwitchTab(tabIndex: number, seconds: number) {
await retry.tryForTime(seconds * 1000, async () => {
await browser.switchTab(tabIndex);
});
}
describe('log pattern analysis', async function () {
let tabsCount = 1;
afterEach(async () => {
if (tabsCount > 1) {
await browser.closeCurrentWindow();
await retrySwitchTab(0, 10);
tabsCount--;
}
});
before(async () => {
await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/logstash_functional');
await ml.testResources.createIndexPatternIfNeeded('logstash-*', '@timestamp');
await ml.testResources.setKibanaTimeZoneToUTC();
await ml.securityUI.loginAsMlPowerUser();
});
after(async () => {
await ml.testResources.deleteIndexPatternByTitle('logstash-*');
});
it(`loads the log pattern analysis page and filters in patterns in discover`, async () => {
// Start navigation from the base of the ML app.
await ml.navigation.navigateToMl();
await elasticChart.setNewChartUiDebugFlag(true);
await aiops.logPatternAnalysisPage.navigateToIndexPatternSelection();
await ml.jobSourceSelection.selectSourceForLogPatternAnalysisDetection('logstash-*');
await aiops.logPatternAnalysisPage.assertLogPatternAnalysisPageExists();
await aiops.logPatternAnalysisPage.clickUseFullDataButton(totalDocCount);
await aiops.logPatternAnalysisPage.selectCategoryField(selectedField);
await aiops.logPatternAnalysisPage.clickRunButton();
await aiops.logPatternAnalysisPage.assertTotalCategoriesFound(3);
await aiops.logPatternAnalysisPage.assertCategoryTableRows(3);
// get category count from the first row
const categoryCount = await aiops.logPatternAnalysisPage.getCategoryCountFromTable(0);
await aiops.logPatternAnalysisPage.clickFilterInButton(0);
retrySwitchTab(1, 10);
tabsCount++;
await aiops.logPatternAnalysisPage.assertDiscoverDocCountExists();
// ensure the discover doc count is equal to the category count
await aiops.logPatternAnalysisPage.assertDiscoverDocCount(categoryCount);
});
it(`loads the log pattern analysis page and filters out patterns in discover`, async () => {
// Start navigation from the base of the ML app.
await ml.navigation.navigateToMl();
await elasticChart.setNewChartUiDebugFlag(true);
await aiops.logPatternAnalysisPage.navigateToIndexPatternSelection();
await ml.jobSourceSelection.selectSourceForLogPatternAnalysisDetection('logstash-*');
await aiops.logPatternAnalysisPage.assertLogPatternAnalysisPageExists();
await aiops.logPatternAnalysisPage.clickUseFullDataButton(totalDocCount);
await aiops.logPatternAnalysisPage.selectCategoryField(selectedField);
await aiops.logPatternAnalysisPage.clickRunButton();
await aiops.logPatternAnalysisPage.assertTotalCategoriesFound(3);
await aiops.logPatternAnalysisPage.assertCategoryTableRows(3);
// get category count from the first row
const categoryCount = await aiops.logPatternAnalysisPage.getCategoryCountFromTable(0);
await aiops.logPatternAnalysisPage.clickFilterOutButton(0);
retrySwitchTab(1, 10);
tabsCount++;
await aiops.logPatternAnalysisPage.assertDiscoverDocCountExists();
// ensure the discover doc count is equal to the total doc count minus category count
await aiops.logPatternAnalysisPage.assertDiscoverDocCount(totalDocCount - categoryCount);
});
});
}

View file

@ -0,0 +1,109 @@
/*
* 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 { FtrProviderContext } from '../../ftr_provider_context';
export default function ({ getPageObjects, getService }: FtrProviderContext) {
const esArchiver = getService('esArchiver');
const aiops = getService('aiops');
const browser = getService('browser');
const retry = getService('retry');
const ml = getService('ml');
const PageObjects = getPageObjects(['common', 'timePicker', 'discover']);
const selectedField = '@message';
const totalDocCount = 14005;
async function retrySwitchTab(tabIndex: number, seconds: number) {
await retry.tryForTime(seconds * 1000, async () => {
await browser.switchTab(tabIndex);
});
}
describe('log pattern analysis', async function () {
let tabsCount = 1;
afterEach(async () => {
if (tabsCount > 1) {
await browser.closeCurrentWindow();
await retrySwitchTab(0, 10);
tabsCount--;
}
});
before(async () => {
await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/logstash_functional');
await ml.testResources.createIndexPatternIfNeeded('logstash-*', '@timestamp');
await ml.testResources.setKibanaTimeZoneToUTC();
await ml.securityUI.loginAsMlPowerUser();
});
after(async () => {
await ml.testResources.deleteIndexPatternByTitle('logstash-*');
});
it(`loads the log pattern analysis flyout and shows patterns in discover`, async () => {
await ml.navigation.navigateToDiscoverViaAppsMenu();
await PageObjects.timePicker.pauseAutoRefresh();
await PageObjects.timePicker.setAbsoluteRange(
'Sep 20, 2015 @ 00:00:00.000',
'Sep 22, 2015 @ 23:50:13.253'
);
await PageObjects.discover.selectIndexPattern('logstash-*');
await aiops.logPatternAnalysisPage.assertDiscoverDocCount(totalDocCount);
await aiops.logPatternAnalysisPage.clickDiscoverField(selectedField);
await aiops.logPatternAnalysisPage.clickDiscoverMenuAnalyzeButton(selectedField);
await aiops.logPatternAnalysisPage.assertLogPatternAnalysisFlyoutExists();
await aiops.logPatternAnalysisPage.assertLogPatternAnalysisFlyoutTitle(selectedField);
await aiops.logPatternAnalysisPage.assertTotalCategoriesFound(3);
await aiops.logPatternAnalysisPage.assertCategoryTableRows(3);
// get category count from the first row
const categoryCount = await aiops.logPatternAnalysisPage.getCategoryCountFromTable(0);
await aiops.logPatternAnalysisPage.clickFilterInButton(0);
await aiops.logPatternAnalysisPage.assertLogPatternAnalysisFlyoutDoesNotExist();
await aiops.logPatternAnalysisPage.assertDiscoverDocCountExists();
// ensure the discover doc count is equal to the category count
await aiops.logPatternAnalysisPage.assertDiscoverDocCount(categoryCount);
});
it(`loads the log pattern analysis flyout and hides patterns in discover`, async () => {
await ml.navigation.navigateToDiscoverViaAppsMenu();
await PageObjects.timePicker.pauseAutoRefresh();
await PageObjects.timePicker.setAbsoluteRange(
'Sep 20, 2015 @ 00:00:00.000',
'Sep 22, 2015 @ 23:50:13.253'
);
await aiops.logPatternAnalysisPage.assertDiscoverDocCount(totalDocCount);
await aiops.logPatternAnalysisPage.clickDiscoverField(selectedField);
await aiops.logPatternAnalysisPage.clickDiscoverMenuAnalyzeButton(selectedField);
await aiops.logPatternAnalysisPage.assertLogPatternAnalysisFlyoutExists();
await aiops.logPatternAnalysisPage.assertLogPatternAnalysisFlyoutTitle(selectedField);
await aiops.logPatternAnalysisPage.assertTotalCategoriesFound(3);
await aiops.logPatternAnalysisPage.assertCategoryTableRows(3);
// get category count from the first row
const categoryCount = await aiops.logPatternAnalysisPage.getCategoryCountFromTable(0);
await aiops.logPatternAnalysisPage.clickFilterOutButton(0);
await aiops.logPatternAnalysisPage.assertLogPatternAnalysisFlyoutDoesNotExist();
await aiops.logPatternAnalysisPage.assertDiscoverDocCountExists();
// ensure the discover doc count is equal to the total count minus the category count
await aiops.logPatternAnalysisPage.assertDiscoverDocCount(totalDocCount - categoryCount);
});
});
}

View file

@ -22,7 +22,7 @@ export const kibanaLogsDataViewTestData: TestData = {
expected: {
queryBar:
'clientip:30.156.16.164 AND host.keyword:elastic-elastic-elastic.org AND ip:30.156.16.163 AND response.keyword:404 AND machine.os.keyword:win xp AND geo.dest:IN AND geo.srcdest:US\\:IN',
totalDocCount: '100',
totalDocCount: 100,
},
},
expected: {

View file

@ -12,7 +12,7 @@ interface TestDataTableActionLogPatternAnalysis {
tableRowId: string;
expected: {
queryBar: string;
totalDocCount: string;
totalDocCount: number;
};
}

View file

@ -21,7 +21,7 @@ export function AiopsProvider(context: FtrProviderContext) {
const explainLogRateSpikesAnalysisGroupsTable =
ExplainLogRateSpikesAnalysisGroupsTableProvider(context);
const explainLogRateSpikesDataGenerator = ExplainLogRateSpikesDataGeneratorProvider(context);
const logPatternAnalysisPageProvider = LogPatternAnalysisPageProvider(context);
const logPatternAnalysisPage = LogPatternAnalysisPageProvider(context);
const tableService = MlTableServiceProvider(context);
@ -33,6 +33,6 @@ export function AiopsProvider(context: FtrProviderContext) {
explainLogRateSpikesAnalysisTable,
explainLogRateSpikesAnalysisGroupsTable,
explainLogRateSpikesDataGenerator,
logPatternAnalysisPageProvider,
logPatternAnalysisPage,
};
}

View file

@ -12,11 +12,31 @@ import type { FtrProviderContext } from '../../ftr_provider_context';
export function LogPatternAnalysisPageProvider({ getService, getPageObject }: FtrProviderContext) {
const retry = getService('retry');
const testSubjects = getService('testSubjects');
const comboBox = getService('comboBox');
return {
async assertLogCategorizationPageExists() {
async assertLogPatternAnalysisPageExists() {
await retry.tryForTime(30 * 1000, async () => {
await testSubjects.existOrFail('aiopsLogCategorizationPage');
await testSubjects.existOrFail('aiopsLogPatternAnalysisPage');
});
},
async navigateToIndexPatternSelection() {
await testSubjects.click('mlMainTab logCategorization');
await testSubjects.existOrFail('mlPageSourceSelection');
},
async clickUseFullDataButton(expectedDocCount: number) {
await retry.tryForTime(30 * 1000, async () => {
await testSubjects.clickWhenNotDisabledWithoutRetry('mlDatePickerButtonUseFullData');
await testSubjects.clickWhenNotDisabledWithoutRetry('superDatePickerApplyTimeButton');
await this.assertTotalDocumentCount(expectedDocCount);
});
},
async clickRunButton() {
await testSubjects.clickWhenNotDisabled('aiopsLogPatternAnalysisRunButton', {
timeout: 5000,
});
},
@ -29,12 +49,130 @@ export function LogPatternAnalysisPageProvider({ getService, getPageObject }: Ft
);
},
async assertTotalDocumentCount(expectedFormattedTotalDocCount: string) {
async assertTotalDocumentCount(expectedFormattedTotalDocCount: number) {
await retry.tryForTime(5000, async () => {
const docCount = await testSubjects.getVisibleText('aiopsTotalDocCount');
expect(docCount).to.eql(
const formattedDocCount = Number(docCount.replaceAll(',', ''));
expect(formattedDocCount).to.eql(
expectedFormattedTotalDocCount,
`Expected total document count to be '${expectedFormattedTotalDocCount}' (got '${docCount}')`
`Expected total document count to be '${expectedFormattedTotalDocCount}' (got '${formattedDocCount}')`
);
});
},
async assertTotalCategoriesFound(expectedCategoryCount: number) {
const expectedText = `${expectedCategoryCount} patterns found`;
await retry.tryForTime(5000, async () => {
const actualText = await testSubjects.getVisibleText('aiopsLogPatternsFoundCount');
expect(actualText).to.eql(
expectedText,
`Expected patterns found count to be '${expectedText}' (got '${actualText}')`
);
});
},
async assertCategoryTableRows(expectedCategoryCount: number) {
await retry.tryForTime(5000, async () => {
const tableListContainer = await testSubjects.find('aiopsLogPatternsTable');
const rows = await tableListContainer.findAllByClassName('euiTableRow');
expect(rows.length).to.eql(
expectedCategoryCount,
`Expected number of rows in table to be '${expectedCategoryCount}' (got '${rows.length}')`
);
});
},
async selectCategoryField(value: string) {
await comboBox.set(`aiopsLogPatternAnalysisCategoryField > comboBoxInput`, value);
await this.assertCategoryFieldSelection(value);
},
async assertCategoryFieldSelection(expectedIdentifier: string) {
const comboBoxSelectedOptions = await comboBox.getComboBoxSelectedOptions(
`aiopsLogPatternAnalysisCategoryField > comboBoxInput`
);
const expectedOptions = [expectedIdentifier];
expect(comboBoxSelectedOptions).to.eql(
expectedOptions,
`Expected a category field to be '${expectedOptions}' (got '${comboBoxSelectedOptions}')`
);
},
async clickFilterInButton(rowIndex: number) {
this.clickFilterButtons('in', rowIndex);
},
async clickFilterOutButton(rowIndex: number) {
this.clickFilterButtons('out', rowIndex);
},
async clickFilterButtons(buttonType: 'in' | 'out', rowIndex: number) {
const tableListContainer = await testSubjects.find('aiopsLogPatternsTable', 5000);
const rows = await tableListContainer.findAllByClassName('euiTableRow');
const button = await rows[rowIndex].findByTestSubject(
buttonType === 'in'
? 'aiopsLogPatternsActionFilterInButton'
: 'aiopsLogPatternsActionFilterOutButton'
);
button.click();
},
async getCategoryCountFromTable(rowIndex: number) {
const tableListContainer = await testSubjects.find('aiopsLogPatternsTable', 5000);
const rows = await tableListContainer.findAllByClassName('euiTableRow');
const row = rows[rowIndex];
const cells = await row.findAllByClassName('euiTableRowCell');
return Number(await cells[0].getVisibleText());
},
async assertDiscoverDocCountExists() {
await retry.tryForTime(30 * 1000, async () => {
await testSubjects.existOrFail('unifiedHistogramQueryHits');
});
},
async assertDiscoverDocCount(expectedDocCount: number) {
await retry.tryForTime(5000, async () => {
const docCount = await testSubjects.getVisibleText('unifiedHistogramQueryHits');
const formattedDocCount = docCount.replaceAll(',', '');
expect(formattedDocCount).to.eql(
expectedDocCount,
`Expected discover document count to be '${expectedDocCount}' (got '${formattedDocCount}')`
);
});
},
async clickDiscoverField(fieldName: string) {
await testSubjects.clickWhenNotDisabled(`dscFieldListPanelField-${fieldName}`, {
timeout: 5000,
});
},
async clickDiscoverMenuAnalyzeButton(fieldName: string) {
await testSubjects.clickWhenNotDisabled(`fieldCategorize-${fieldName}`, {
timeout: 5000,
});
},
async assertLogPatternAnalysisFlyoutExists() {
await retry.tryForTime(30 * 1000, async () => {
await testSubjects.existOrFail('mlJobSelectorFlyoutBody');
});
},
async assertLogPatternAnalysisFlyoutDoesNotExist() {
await retry.tryForTime(30 * 1000, async () => {
await testSubjects.missingOrFail('mlJobSelectorFlyoutBody');
});
},
async assertLogPatternAnalysisFlyoutTitle(fieldName: string) {
await retry.tryForTime(30 * 1000, async () => {
const title = await testSubjects.getVisibleText('mlJobSelectorFlyoutTitle');
const expectedTitle = `Pattern analysis of ${fieldName}`;
expect(title).to.eql(
expectedTitle,
`Expected flyout title to be '${expectedTitle}' (got '${title}')`
);
});
},

View file

@ -50,5 +50,9 @@ export function MachineLearningJobSourceSelectionProvider({ getService }: FtrPro
async selectSourceForChangePointDetection(sourceName: string) {
await this.selectSource(sourceName, 'aiopsChangePointDetectionPage');
},
async selectSourceForLogPatternAnalysisDetection(sourceName: string) {
await this.selectSource(sourceName, 'aiopsLogPatternAnalysisPage');
},
};
}