mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 01:13:23 -04:00
[RAM] Add integration tests for all-space global logs view (#145192)
## Summary Closes #144732 Adds functional tests for: - Global logs api with `namespace` - Global logs kpi with `namespace` - Error log api with `namespace` - Showing/hiding the logs switch based on space existence and user permissions Also: - Fixes the query backend for `getActionErrorLogWithAuth` so that it actually successfully filters down to just a single specified alert `id` instead of returning all action error logs from everything. Writing a test for this functionality revealed that it was broken. ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios
This commit is contained in:
parent
bca45a1245
commit
4956ea0b7a
8 changed files with 468 additions and 7 deletions
|
@ -529,6 +529,7 @@ export function getQueryBodyWithAuthFilter(
|
|||
) {
|
||||
const { namespaces, type, authFilter } = opts;
|
||||
const { start, end, filter } = queryOptions ?? {};
|
||||
const ids = 'ids' in opts ? opts.ids : [];
|
||||
|
||||
const namespaceQuery = (namespaces ?? [undefined]).map((namespace) =>
|
||||
getNamespaceQuery(namespace)
|
||||
|
@ -586,6 +587,43 @@ export function getQueryBodyWithAuthFilter(
|
|||
},
|
||||
];
|
||||
|
||||
if (ids.length) {
|
||||
musts.push({
|
||||
bool: {
|
||||
should: {
|
||||
bool: {
|
||||
must: [
|
||||
{
|
||||
nested: {
|
||||
path: 'kibana.saved_objects',
|
||||
query: {
|
||||
bool: {
|
||||
must: [
|
||||
{
|
||||
terms: {
|
||||
// default maximum of 65,536 terms, configurable by index.max_terms_count
|
||||
'kibana.saved_objects.id': ids,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
range: {
|
||||
'kibana.version': {
|
||||
gte: LEGACY_ID_CUTOFF_VERSION,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (start) {
|
||||
musts.push({
|
||||
range: {
|
||||
|
|
|
@ -428,7 +428,7 @@ export const RuleEventLogListTable = <T extends RuleEventLogListOptions>(
|
|||
/>
|
||||
</EuiFlexItem>
|
||||
{hasAllSpaceSwitch && areMultipleSpacesAccessible && (
|
||||
<EuiFlexItem>
|
||||
<EuiFlexItem data-test-subj="showAllSpacesSwitch">
|
||||
<EuiSwitch
|
||||
label={ALL_SPACES_LABEL}
|
||||
checked={showFromAllSpaces}
|
||||
|
|
|
@ -0,0 +1,120 @@
|
|||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
|
||||
import { Spaces } from '../../../scenarios';
|
||||
import {
|
||||
getUrlPrefix,
|
||||
ObjectRemover,
|
||||
getTestRuleData,
|
||||
getEventLog,
|
||||
ESTestIndexTool,
|
||||
} from '../../../../common/lib';
|
||||
import { FtrProviderContext } from '../../../../common/ftr_provider_context';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function createGetActionErrorLogTests({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const retry = getService('retry');
|
||||
const es = getService('es');
|
||||
const esTestIndexTool = new ESTestIndexTool(es, retry);
|
||||
|
||||
const dateStart = new Date(Date.now() - 600000).toISOString();
|
||||
|
||||
describe('getActionErrorLog', () => {
|
||||
const objectRemover = new ObjectRemover(supertest);
|
||||
|
||||
beforeEach(async () => {
|
||||
await esTestIndexTool.destroy();
|
||||
await esTestIndexTool.setup();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await objectRemover.removeAll();
|
||||
});
|
||||
|
||||
it('gets action error logs from an alternate space', async () => {
|
||||
const { body: createdConnector } = await supertest
|
||||
.post(`${getUrlPrefix(Spaces[1].id)}/api/actions/connector`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
name: 'connector that throws',
|
||||
connector_type_id: 'test.throw',
|
||||
config: {},
|
||||
secrets: {},
|
||||
})
|
||||
.expect(200);
|
||||
objectRemover.add(Spaces[1].id, createdConnector.id, 'action', 'actions');
|
||||
|
||||
// Create 2 rules, and then only pull the logs for one of them
|
||||
let watchedRuleId;
|
||||
for (let i = 0; i < 2; i++) {
|
||||
const { body: createdRule } = await supertest
|
||||
.post(`${getUrlPrefix(Spaces[1].id)}/api/alerting/rule`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send(
|
||||
getTestRuleData({
|
||||
rule_type_id: 'test.cumulative-firing',
|
||||
actions: [
|
||||
{
|
||||
id: createdConnector.id,
|
||||
group: 'default',
|
||||
params: {},
|
||||
},
|
||||
],
|
||||
})
|
||||
)
|
||||
.expect(200);
|
||||
objectRemover.add(Spaces[1].id, createdRule.id, 'rule', 'alerting');
|
||||
await waitForEvents(createdRule.id, 'alerting', new Map([['execute', { gte: 1 }]]));
|
||||
await waitForEvents(createdRule.id, 'actions', new Map([['execute', { gte: 1 }]]));
|
||||
|
||||
if (i === 0) watchedRuleId = createdRule.id;
|
||||
}
|
||||
|
||||
const response = await supertest.get(
|
||||
`${getUrlPrefix(
|
||||
Spaces[0].id
|
||||
)}/internal/alerting/rule/${watchedRuleId}/_action_error_log?date_start=${dateStart}&with_auth=true&namespace=${
|
||||
Spaces[1].id
|
||||
}`
|
||||
);
|
||||
expect(response.body.totalErrors).to.eql(1);
|
||||
expect(response.body.errors.length).to.eql(1);
|
||||
|
||||
for (const errors of response.body.errors) {
|
||||
expect(errors.type).to.equal('actions');
|
||||
expect(errors.message).to.equal(
|
||||
`action execution failure: test.throw:${createdConnector.id}: connector that throws - an error occurred while running the action: this action is intended to fail; retry: true`
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
async function waitForEvents(
|
||||
id: string,
|
||||
provider: string,
|
||||
actions: Map<
|
||||
string,
|
||||
{
|
||||
gte: number;
|
||||
}
|
||||
>
|
||||
) {
|
||||
await retry.try(async () => {
|
||||
return await getEventLog({
|
||||
getService,
|
||||
spaceId: Spaces[1].id,
|
||||
type: 'alert',
|
||||
id,
|
||||
provider,
|
||||
actions,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
|
@ -20,7 +20,7 @@ export default function getGlobalExecutionKpiTests({ getService }: FtrProviderCo
|
|||
describe('getGlobalExecutionKpi', () => {
|
||||
const objectRemover = new ObjectRemover(supertest);
|
||||
|
||||
after(() => objectRemover.removeAll());
|
||||
afterEach(() => objectRemover.removeAll());
|
||||
|
||||
it('should return KPI only from the current space', async () => {
|
||||
const startTime = new Date().toISOString();
|
||||
|
@ -42,8 +42,9 @@ export default function getGlobalExecutionKpiTests({ getService }: FtrProviderCo
|
|||
const ruleId = response.body.id;
|
||||
objectRemover.add(spaceId, ruleId, 'rule', 'alerting');
|
||||
|
||||
const spaceId2 = UserAtSpaceScenarios[4].space.id;
|
||||
const response2 = await supertest
|
||||
.post(`${getUrlPrefix(spaceId)}/api/alerting/rule`)
|
||||
.post(`${getUrlPrefix(spaceId2)}/api/alerting/rule`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send(
|
||||
getTestRuleData({
|
||||
|
@ -55,12 +56,12 @@ export default function getGlobalExecutionKpiTests({ getService }: FtrProviderCo
|
|||
|
||||
expect(response2.status).to.eql(200);
|
||||
const ruleId2 = response2.body.id;
|
||||
objectRemover.add(spaceId, ruleId2, 'rule', 'alerting');
|
||||
objectRemover.add(spaceId2, ruleId2, 'rule', 'alerting');
|
||||
|
||||
await retry.try(async () => {
|
||||
// break AAD
|
||||
await supertest
|
||||
.put(`${getUrlPrefix(spaceId)}/api/alerts_fixture/saved_object/alert/${ruleId2}`)
|
||||
.put(`${getUrlPrefix(spaceId2)}/api/alerts_fixture/saved_object/alert/${ruleId2}`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
attributes: {
|
||||
|
@ -74,7 +75,7 @@ export default function getGlobalExecutionKpiTests({ getService }: FtrProviderCo
|
|||
// there can be a successful execute before the error one
|
||||
const someEvents = await getEventLog({
|
||||
getService,
|
||||
spaceId,
|
||||
spaceId: spaceId2,
|
||||
type: 'alert',
|
||||
id: ruleId2,
|
||||
provider: 'alerting',
|
||||
|
@ -148,15 +149,155 @@ export default function getGlobalExecutionKpiTests({ getService }: FtrProviderCo
|
|||
'success',
|
||||
'unknown',
|
||||
'failure',
|
||||
'warning',
|
||||
'activeAlerts',
|
||||
'newAlerts',
|
||||
'recoveredAlerts',
|
||||
'erroredActions',
|
||||
'triggeredActions',
|
||||
]);
|
||||
// it should be above 1 since we have two rule running
|
||||
expect(kpiLogs.success).to.be.above(1);
|
||||
expect(kpiLogs.failure).to.be.above(0);
|
||||
});
|
||||
|
||||
it('should return KPI from all spaces in the namespaces param', async () => {
|
||||
const startTime = new Date().toISOString();
|
||||
|
||||
const spaceId = UserAtSpaceScenarios[1].space.id;
|
||||
const user = UserAtSpaceScenarios[1].user;
|
||||
const response = await supertest
|
||||
.post(`${getUrlPrefix(spaceId)}/api/alerting/rule`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send(
|
||||
getTestRuleData({
|
||||
rule_type_id: 'test.noop',
|
||||
schedule: { interval: '1s' },
|
||||
throttle: null,
|
||||
})
|
||||
);
|
||||
|
||||
expect(response.status).to.eql(200);
|
||||
const ruleId = response.body.id;
|
||||
objectRemover.add(spaceId, ruleId, 'rule', 'alerting');
|
||||
|
||||
const spaceId2 = UserAtSpaceScenarios[4].space.id;
|
||||
const response2 = await supertest
|
||||
.post(`${getUrlPrefix(spaceId2)}/api/alerting/rule`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send(
|
||||
getTestRuleData({
|
||||
rule_type_id: 'test.noop',
|
||||
schedule: { interval: '1s' },
|
||||
throttle: null,
|
||||
})
|
||||
);
|
||||
|
||||
expect(response2.status).to.eql(200);
|
||||
const ruleId2 = response2.body.id;
|
||||
objectRemover.add(spaceId2, ruleId2, 'rule', 'alerting');
|
||||
|
||||
await retry.try(async () => {
|
||||
// break AAD
|
||||
await supertest
|
||||
.put(`${getUrlPrefix(spaceId2)}/api/alerts_fixture/saved_object/alert/${ruleId2}`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
attributes: {
|
||||
name: 'bar',
|
||||
},
|
||||
})
|
||||
.expect(200);
|
||||
});
|
||||
|
||||
await retry.try(async () => {
|
||||
// there can be a successful execute before the error one
|
||||
const someEvents = await getEventLog({
|
||||
getService,
|
||||
spaceId: spaceId2,
|
||||
type: 'alert',
|
||||
id: ruleId2,
|
||||
provider: 'alerting',
|
||||
actions: new Map([['execute', { gte: 1 }]]),
|
||||
});
|
||||
const errorEvents = someEvents.filter(
|
||||
(event) => event?.kibana?.alerting?.status === 'error'
|
||||
);
|
||||
expect(errorEvents.length).to.be.above(0);
|
||||
});
|
||||
|
||||
await retry.try(async () => {
|
||||
// there can be a successful execute before the error one
|
||||
const logResponse = await supertestWithoutAuth
|
||||
.get(
|
||||
`${getUrlPrefix(
|
||||
spaceId
|
||||
)}/internal/alerting/_global_execution_logs?date_start=${startTime}&date_end=9999-12-31T23:59:59Z&per_page=50&page=1`
|
||||
)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.auth(user.username, user.password);
|
||||
expect(logResponse.statusCode).to.be(200);
|
||||
expect(logResponse.body.data.length).to.be.above(1);
|
||||
});
|
||||
|
||||
await retry.try(async () => {
|
||||
// break AAD
|
||||
await supertest
|
||||
.put(`${getUrlPrefix(spaceId)}/api/alerts_fixture/saved_object/alert/${ruleId}`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
attributes: {
|
||||
name: 'bar',
|
||||
},
|
||||
})
|
||||
.expect(200);
|
||||
});
|
||||
|
||||
await retry.try(async () => {
|
||||
// there can be a successful execute before the error one
|
||||
const someEvents = await getEventLog({
|
||||
getService,
|
||||
spaceId,
|
||||
type: 'alert',
|
||||
id: ruleId,
|
||||
provider: 'alerting',
|
||||
actions: new Map([['execute', { gte: 1 }]]),
|
||||
});
|
||||
const errorEvents = someEvents.filter(
|
||||
(event) => event?.kibana?.alerting?.status === 'error'
|
||||
);
|
||||
expect(errorEvents.length).to.be.above(0);
|
||||
});
|
||||
|
||||
const kpiLogs = await retry.try(async () => {
|
||||
// there can be a successful execute before the error one
|
||||
const logResponse = await supertestWithoutAuth
|
||||
.get(
|
||||
`${getUrlPrefix(
|
||||
spaceId
|
||||
)}/internal/alerting/_global_execution_kpi?date_start=${startTime}&date_end=9999-12-31T23:59:59Z&namespaces=${JSON.stringify(
|
||||
[spaceId, spaceId2]
|
||||
)}`
|
||||
)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.auth(user.username, user.password);
|
||||
expect(logResponse.statusCode).to.be(200);
|
||||
|
||||
return logResponse.body;
|
||||
});
|
||||
|
||||
expect(Object.keys(kpiLogs)).to.eql([
|
||||
'success',
|
||||
'unknown',
|
||||
'failure',
|
||||
'warning',
|
||||
'activeAlerts',
|
||||
'newAlerts',
|
||||
'recoveredAlerts',
|
||||
'erroredActions',
|
||||
'triggeredActions',
|
||||
]);
|
||||
expect(kpiLogs.success).to.be.above(1);
|
||||
expect(kpiLogs.failure).to.be.above(2);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -78,5 +78,66 @@ export default function globalExecutionLogTests({ getService }: FtrProviderConte
|
|||
const allLogsSpace0 = sanitizedLogs.every((l: any) => l.rule_id === alertId);
|
||||
expect(allLogsSpace0).to.be(true);
|
||||
});
|
||||
|
||||
it('should return logs from multiple spaces when passed the namespaces param', async () => {
|
||||
const startTime = new Date().toISOString();
|
||||
|
||||
const spaceId = UserAtSpaceScenarios[1].space.id;
|
||||
const user = UserAtSpaceScenarios[1].user;
|
||||
const response = await supertest
|
||||
.post(`${getUrlPrefix(spaceId)}/api/alerting/rule`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send(
|
||||
getTestRuleData({
|
||||
rule_type_id: 'test.noop',
|
||||
schedule: { interval: '1s' },
|
||||
throttle: null,
|
||||
})
|
||||
);
|
||||
|
||||
expect(response.status).to.eql(200);
|
||||
const alertId = response.body.id;
|
||||
objectRemover.add(spaceId, alertId, 'rule', 'alerting');
|
||||
|
||||
const spaceId2 = UserAtSpaceScenarios[4].space.id;
|
||||
const response2 = await supertest
|
||||
.post(`${getUrlPrefix(spaceId2)}/api/alerting/rule`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send(
|
||||
getTestRuleData({
|
||||
rule_type_id: 'test.noop',
|
||||
schedule: { interval: '1s' },
|
||||
throttle: null,
|
||||
})
|
||||
);
|
||||
|
||||
expect(response2.status).to.eql(200);
|
||||
const alertId2 = response2.body.id;
|
||||
objectRemover.add(spaceId2, alertId2, 'rule', 'alerting');
|
||||
|
||||
const logs = await retry.try(async () => {
|
||||
// there can be a successful execute before the error one
|
||||
const logResponse = await supertestWithoutAuth
|
||||
.get(
|
||||
`${getUrlPrefix(
|
||||
spaceId
|
||||
)}/internal/alerting/_global_execution_logs?date_start=${startTime}&date_end=9999-12-31T23:59:59Z&per_page=50&page=1&namespaces=${JSON.stringify(
|
||||
[spaceId, spaceId2]
|
||||
)}`
|
||||
)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.auth(user.username, user.password);
|
||||
expect(logResponse.statusCode).to.be(200);
|
||||
|
||||
return logResponse.body.data;
|
||||
});
|
||||
|
||||
// Filter out any excess logs from rules not created by this test
|
||||
const sanitizedLogs = logs.filter((l: any) => [alertId, alertId2].includes(l.rule_id));
|
||||
const allLogsSpaces0and1 = sanitizedLogs.every((l: any) =>
|
||||
[alertId, alertId2].includes(l.rule_id)
|
||||
);
|
||||
expect(allLogsSpaces0and1).to.be(true);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -45,6 +45,8 @@ export default function alertingTests({ loadTestFile, getService }: FtrProviderC
|
|||
loadTestFile(require.resolve('./excluded'));
|
||||
loadTestFile(require.resolve('./snooze'));
|
||||
loadTestFile(require.resolve('./global_execution_log'));
|
||||
loadTestFile(require.resolve('./get_global_execution_kpi'));
|
||||
loadTestFile(require.resolve('./get_action_error_log'));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -14,5 +14,6 @@ export default ({ loadTestFile, getService }: FtrProviderContext) => {
|
|||
loadTestFile(require.resolve('./alert_create_flyout'));
|
||||
loadTestFile(require.resolve('./details'));
|
||||
loadTestFile(require.resolve('./connectors'));
|
||||
loadTestFile(require.resolve('./logs_list'));
|
||||
});
|
||||
};
|
||||
|
|
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
import { Role, User } from '../../../cases_api_integration/common/lib/authentication/types';
|
||||
import {
|
||||
createUsersAndRoles,
|
||||
deleteUsersAndRoles,
|
||||
} from '../../../cases_api_integration/common/lib/authentication';
|
||||
|
||||
const SPACE2 = {
|
||||
id: 'space-2',
|
||||
name: 'Space 2',
|
||||
disabledFeatures: [],
|
||||
};
|
||||
const ONLY_S2_ROLE: Role = {
|
||||
name: 'only_s2',
|
||||
privileges: {
|
||||
elasticsearch: {
|
||||
indices: [
|
||||
{
|
||||
names: ['*'],
|
||||
privileges: ['all'],
|
||||
},
|
||||
],
|
||||
},
|
||||
kibana: [
|
||||
{
|
||||
base: ['all'],
|
||||
feature: {},
|
||||
spaces: [SPACE2.id],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const ONLY_S2_USER: User = {
|
||||
username: 'only_s2_user',
|
||||
password: 'changeme',
|
||||
roles: [ONLY_S2_ROLE.name],
|
||||
};
|
||||
|
||||
export default ({ getPageObjects, getService }: FtrProviderContext) => {
|
||||
const testSubjects = getService('testSubjects');
|
||||
const pageObjects = getPageObjects(['common', 'triggersActionsUI', 'header', 'security']);
|
||||
const spaces = getService('spaces');
|
||||
|
||||
async function refreshLogsList() {
|
||||
await testSubjects.click('logsTab');
|
||||
}
|
||||
|
||||
describe('logs list', function () {
|
||||
before(async () => {
|
||||
await createUsersAndRoles(getService, [ONLY_S2_USER], [ONLY_S2_ROLE]);
|
||||
await pageObjects.common.navigateToApp('triggersActions');
|
||||
await testSubjects.click('logsTab');
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await deleteUsersAndRoles(getService, [ONLY_S2_USER], [ONLY_S2_ROLE]);
|
||||
await spaces.delete(SPACE2.id);
|
||||
await pageObjects.security.forceLogout();
|
||||
});
|
||||
|
||||
it('should not show the logs space switch when only one space exists', async () => {
|
||||
const spacesSwitchExists = await testSubjects.exists('showAllSpacesSwitch');
|
||||
expect(spacesSwitchExists).to.be(false);
|
||||
});
|
||||
|
||||
it('should show the logs space switch when multiple spaces are accessible', async () => {
|
||||
await spaces.create(SPACE2);
|
||||
|
||||
await refreshLogsList();
|
||||
const spacesSwitch = await testSubjects.find('showAllSpacesSwitch');
|
||||
expect(spacesSwitch).not.to.be(undefined);
|
||||
const switchControl = await spacesSwitch.findByCssSelector('button');
|
||||
expect(await switchControl.getAttribute('aria-checked')).to.be('false');
|
||||
await switchControl.click();
|
||||
expect(await switchControl.getAttribute('aria-checked')).to.be('true');
|
||||
});
|
||||
|
||||
it('should not show the logs space switch when multiple spaces exist but only one is accessible', async () => {
|
||||
await pageObjects.security.forceLogout();
|
||||
await pageObjects.security.login(ONLY_S2_USER.username, ONLY_S2_USER.password);
|
||||
|
||||
await pageObjects.common.navigateToApp('triggersActions', { basePath: `/s/${SPACE2.id}` });
|
||||
await testSubjects.click('logsTab');
|
||||
|
||||
const spacesSwitchExists = await testSubjects.exists('showAllSpacesSwitch');
|
||||
expect(spacesSwitchExists).to.be(false);
|
||||
});
|
||||
});
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue