[8.x] [ECO] Add e2e to ensure consistent alert count across APM, Infra and Alerts app (#203845) (#204248)

# Backport

This will backport the following commits from `main` to `8.x`:
- [[ECO] Add e2e to ensure consistent alert count across APM, Infra and
Alerts app (#203845)](https://github.com/elastic/kibana/pull/203845)

<!--- Backport version: 9.4.3 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Irene
Blanco","email":"irene.blanco@elastic.co"},"sourceCommit":{"committedDate":"2024-12-13T15:45:57Z","message":"[ECO]
Add e2e to ensure consistent alert count across APM, Infra and Alerts
app (#203845)\n\n## Summary\r\n\r\nCloses
https://github.com/elastic/kibana/issues/202415.\r\n\r\nWe recently
worked on aligning the alert counts across Entity Inventory,\r\nAPM
(https://github.com/elastic/kibana/issues/201565),
Infra\r\n(https://github.com/elastic/kibana/issues/201567) and the
Alerts app.\r\n\r\nThis PR adds tests to ensure that the alert count
remains consistent\r\nacross all the impacted
pages.\r\n\r\n>[!NOTE]\r\n>Alerts data can't be provided using its own
Synthtrace client as it\r\nhasn't been created yet.\r\nFor these tests,
the data is ingested through the API.\r\n\r\n### How to run in
local\r\n`node
x-pack/plugins/observability_solution/inventory/scripts/test/e2e\r\n--server`\r\n`node
x-pack/plugins/observability_solution/inventory/scripts/test/e2e\r\n--runner
--open`\r\n\r\n### Test
execution\r\n\r\n\r\nhttps://github.com/user-attachments/assets/7f350618-38a2-4e5c-8e8d-8a2166f89ad8","sha":"05e05db07c6703f68143ae48b9170e6a6c459e8c","branchLabelMapping":{"^v9.0.0$":"main","^v8.18.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","v9.0.0","backport:prev-minor","ci:project-deploy-observability","Team:obs-ux-infra_services","v8.18.0"],"title":"[ECO]
Add e2e to ensure consistent alert count across APM, Infra and Alerts
app","number":203845,"url":"https://github.com/elastic/kibana/pull/203845","mergeCommit":{"message":"[ECO]
Add e2e to ensure consistent alert count across APM, Infra and Alerts
app (#203845)\n\n## Summary\r\n\r\nCloses
https://github.com/elastic/kibana/issues/202415.\r\n\r\nWe recently
worked on aligning the alert counts across Entity Inventory,\r\nAPM
(https://github.com/elastic/kibana/issues/201565),
Infra\r\n(https://github.com/elastic/kibana/issues/201567) and the
Alerts app.\r\n\r\nThis PR adds tests to ensure that the alert count
remains consistent\r\nacross all the impacted
pages.\r\n\r\n>[!NOTE]\r\n>Alerts data can't be provided using its own
Synthtrace client as it\r\nhasn't been created yet.\r\nFor these tests,
the data is ingested through the API.\r\n\r\n### How to run in
local\r\n`node
x-pack/plugins/observability_solution/inventory/scripts/test/e2e\r\n--server`\r\n`node
x-pack/plugins/observability_solution/inventory/scripts/test/e2e\r\n--runner
--open`\r\n\r\n### Test
execution\r\n\r\n\r\nhttps://github.com/user-attachments/assets/7f350618-38a2-4e5c-8e8d-8a2166f89ad8","sha":"05e05db07c6703f68143ae48b9170e6a6c459e8c"}},"sourceBranch":"main","suggestedTargetBranches":["8.x"],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/203845","number":203845,"mergeCommit":{"message":"[ECO]
Add e2e to ensure consistent alert count across APM, Infra and Alerts
app (#203845)\n\n## Summary\r\n\r\nCloses
https://github.com/elastic/kibana/issues/202415.\r\n\r\nWe recently
worked on aligning the alert counts across Entity Inventory,\r\nAPM
(https://github.com/elastic/kibana/issues/201565),
Infra\r\n(https://github.com/elastic/kibana/issues/201567) and the
Alerts app.\r\n\r\nThis PR adds tests to ensure that the alert count
remains consistent\r\nacross all the impacted
pages.\r\n\r\n>[!NOTE]\r\n>Alerts data can't be provided using its own
Synthtrace client as it\r\nhasn't been created yet.\r\nFor these tests,
the data is ingested through the API.\r\n\r\n### How to run in
local\r\n`node
x-pack/plugins/observability_solution/inventory/scripts/test/e2e\r\n--server`\r\n`node
x-pack/plugins/observability_solution/inventory/scripts/test/e2e\r\n--runner
--open`\r\n\r\n### Test
execution\r\n\r\n\r\nhttps://github.com/user-attachments/assets/7f350618-38a2-4e5c-8e8d-8a2166f89ad8","sha":"05e05db07c6703f68143ae48b9170e6a6c459e8c"}},{"branch":"8.x","label":"v8.18.0","branchLabelMappingKey":"^v8.18.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->

Co-authored-by: Irene Blanco <irene.blanco@elastic.co>
This commit is contained in:
Kibana Machine 2024-12-14 04:45:28 +11:00 committed by GitHub
parent ca17364f2d
commit 0d1aa2a734
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 457 additions and 3 deletions

View file

@ -17,6 +17,10 @@ import { Logger } from '../utils/create_logger';
export type InfraSynthtraceEsClientOptions = Omit<SynthtraceEsClientOptions, 'pipeline'>;
interface Pipeline {
includeSerialization?: boolean;
}
export class InfraSynthtraceEsClient extends SynthtraceEsClient<InfraDocument> {
constructor(options: { client: Client; logger: Logger } & InfraSynthtraceEsClientOptions) {
super({
@ -30,13 +34,26 @@ export class InfraSynthtraceEsClient extends SynthtraceEsClient<InfraDocument> {
'metrics-aws*',
];
}
getDefaultPipeline(
{
includeSerialization,
}: {
includeSerialization?: boolean;
} = { includeSerialization: true }
) {
return infraPipeline({ includeSerialization });
}
}
function infraPipeline() {
function infraPipeline({ includeSerialization }: Pipeline = { includeSerialization: true }) {
return (base: Readable) => {
const serializationTransform = includeSerialization ? [getSerializeTransform()] : [];
return pipeline(
// @ts-expect-error Some weird stuff here with the type definition for pipeline. We have tests!
base,
getSerializeTransform(),
...serializationTransform,
getRoutingTransform(),
getDedotTransform(),
(err: unknown) => {

View file

@ -115,6 +115,7 @@ export function getServiceColumns({
)}
>
<EuiBadge
data-test-subj="serviceInventoryAlertsBadgeLink"
iconType="warning"
color="danger"
href={link('/services/{serviceName}/alerts', {

View file

@ -277,6 +277,7 @@ export const useHostsTable = () => {
<EuiToolTip position="top" content={TABLE_CONTENT_LABEL.activeAlerts}>
<EuiBadge
iconType="warning"
data-test-subj="hostInventoryAlertsBadgeLink"
color="danger"
onClick={() => {
setProperties({ detailsItemId: row.id === detailsItemId ? null : row.id });

View file

@ -0,0 +1,199 @@
/*
* 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 { apmSynthtrace, entitiesSynthtrace, infraSynthtrace } from '../../../synthtrace';
import {
CONTAINER_ID,
HOST_NAME,
SERVICE_NAME,
generateEntityAlerts,
cleanEntityAlerts,
generateEntities,
generateHosts,
generateTraces,
} from './generate_data';
const start = new Date(Date.now() - 5 * 60000).toISOString();
const end = new Date().toISOString();
const getNumber = (text: string) => text.replace(/\D/g, '');
const verifyNumber = (element: Cypress.Chainable<JQuery<Element>>, alertsCount: string) => {
element.invoke('text').then((testSubjElementCount) => {
expect(getNumber(testSubjElementCount)).to.equal(alertsCount);
});
};
const verifyAlertsTableCount = (alertsCount: string) => {
verifyNumber(cy.getByTestSubj('activeAlertCount'), alertsCount);
verifyNumber(cy.getByTestSubj('toolbar-alerts-count'), alertsCount);
};
describe('Alert count', () => {
beforeEach(() => {
cy.loginAsSuperUser();
cy.intercept('GET', '/internal/entities/managed/enablement', {
fixture: 'eem_enabled.json',
}).as('getEEMStatus');
entitiesSynthtrace.index(
generateEntities({ from: new Date(start).getTime(), to: new Date(end).getTime() })
);
generateEntityAlerts(start);
});
afterEach(() => {
entitiesSynthtrace.clean();
cleanEntityAlerts();
});
describe('When there is entities and signal data', () => {
describe('Service', () => {
before(() => {
apmSynthtrace.index(
generateTraces({ from: new Date(start).getTime(), to: new Date(end).getTime() })
);
});
after(() => {
apmSynthtrace.clean();
});
beforeEach(() => {
cy.visitKibana('/app/inventory');
cy.wait('@getEEMStatus');
cy.contains('service').click();
});
it('Should display the correct alert count in the entity detail views', () => {
cy.getByTestSubj('inventoryAlertsBadgeLink')
.invoke('text')
.then((inventoryAlertsBadgeLinkCount) => {
cy.contains(SERVICE_NAME).click();
cy.url().should('include', `/app/apm/services/${SERVICE_NAME}/overview`);
verifyNumber(
cy.getByTestSubj('alertsTab').get('.euiBadge'),
inventoryAlertsBadgeLinkCount
);
cy.getByTestSubj('alertsTab').click();
cy.url().should('include', `/app/apm/services/${SERVICE_NAME}/alerts`);
cy.getByTestSubj('alert-status-filter-active-button').click();
verifyNumber(cy.getByTestSubj('toolbar-alerts-count'), inventoryAlertsBadgeLinkCount);
});
});
it('Should display the correct alert count in the alerts app', () => {
cy.getByTestSubj('inventoryAlertsBadgeLink')
.invoke('text')
.then((inventoryAlertsBadgeLinkCount) => {
cy.getByTestSubj('inventoryAlertsBadgeLink').click();
cy.url().should('include', `/app/observability/alerts`);
verifyAlertsTableCount(inventoryAlertsBadgeLinkCount);
});
});
it('Should display the correct alert count in the services inventory', () => {
cy.getByTestSubj('inventoryAlertsBadgeLink')
.invoke('text')
.then((inventoryAlertsBadgeLinkCount) => {
cy.visitKibana(`/app/apm/services?rangeFrom=${start}&rangeTo=${end}`);
verifyNumber(
cy.getByTestSubj('serviceInventoryAlertsBadgeLink'),
inventoryAlertsBadgeLinkCount
);
});
});
});
describe('Host', () => {
before(() => {
infraSynthtrace.index(
generateHosts({
from: start,
to: end,
})
);
});
after(() => {
infraSynthtrace.clean();
});
beforeEach(() => {
cy.visitKibana('/app/inventory');
cy.wait('@getEEMStatus');
cy.contains('host').click();
});
it('Should display the correct alert count in the entity detail views', () => {
cy.getByTestSubj('inventoryAlertsBadgeLink')
.invoke('text')
.then((inventoryAlertsBadgeLinkCount) => {
cy.contains(HOST_NAME).click();
cy.url().should('include', `/app/metrics/detail/host/${HOST_NAME}`);
cy.getByTestSubj('hostsView-alert-status-filter-active-button').click();
verifyAlertsTableCount(inventoryAlertsBadgeLinkCount);
});
});
it('Should display the correct alert count in the alerts app', () => {
cy.getByTestSubj('inventoryAlertsBadgeLink')
.invoke('text')
.then((inventoryAlertsBadgeLinkCount) => {
cy.getByTestSubj('inventoryAlertsBadgeLink').click();
cy.url().should('include', `/app/observability/alerts`);
verifyAlertsTableCount(inventoryAlertsBadgeLinkCount);
});
});
it('Should display the correct alert count in the hosts inventory', () => {
cy.getByTestSubj('inventoryAlertsBadgeLink')
.invoke('text')
.then((inventoryAlertsBadgeLinkCount) => {
cy.visitKibana('/app/metrics/hosts');
verifyNumber(
cy.getByTestSubj('hostInventoryAlertsBadgeLink'),
inventoryAlertsBadgeLinkCount
);
});
});
});
describe('Container', () => {
beforeEach(() => {
cy.visitKibana('/app/inventory');
cy.wait('@getEEMStatus');
cy.contains('container').click();
});
it('Should display the correct alert count in the entity detail views', () => {
cy.getByTestSubj('inventoryAlertsBadgeLink')
.invoke('text')
.then((inventoryAlertsBadgeLinkCount) => {
cy.contains(CONTAINER_ID).click();
cy.url().should('include', `/app/metrics/detail/container/${CONTAINER_ID}`);
cy.getByTestSubj('hostsView-alert-status-filter-active-button').click();
verifyAlertsTableCount(inventoryAlertsBadgeLinkCount);
});
});
it('Should display the correct alert count in the alerts app', () => {
cy.getByTestSubj('inventoryAlertsBadgeLink')
.invoke('text')
.then((inventoryAlertsBadgeLinkCount) => {
cy.getByTestSubj('inventoryAlertsBadgeLink').click();
cy.url().should('include', `/app/observability/alerts`);
verifyAlertsTableCount(inventoryAlertsBadgeLinkCount);
});
});
});
});
});

View file

@ -0,0 +1,206 @@
/*
* 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 { entities, apm, timerange, infra } from '@kbn/apm-synthtrace-client';
import { generateLongIdWithSeed } from '@kbn/apm-synthtrace-client/src/lib/utils/generate_id';
const SERVICE_ENTITY_ID = generateLongIdWithSeed('service');
const HOST_ENTITY_ID = generateLongIdWithSeed('host');
const CONTAINER_ENTITY_ID = generateLongIdWithSeed('container');
export const SERVICE_NAME = 'service-entity';
export const HOST_NAME = 'host-entity';
export const CONTAINER_ID = 'container-entity';
const AGENT_NAME = 'agentName';
const ENVIRONMENT = 'ENVIRONMENT_ALL';
export function generateEntities({ from, to }: { from: number; to: number }) {
const service = entities.serviceEntity({
serviceName: SERVICE_NAME,
agentName: [AGENT_NAME],
dataStreamType: ['logs'],
entityId: SERVICE_ENTITY_ID,
});
const host = entities.hostEntity({
hostName: HOST_NAME,
agentName: [AGENT_NAME],
dataStreamType: ['metrics'],
entityId: HOST_ENTITY_ID,
});
const container = entities.containerEntity({
containerId: CONTAINER_ID,
agentName: [AGENT_NAME],
dataStreamType: ['metrics'],
entityId: CONTAINER_ENTITY_ID,
});
const range = timerange(from, to);
return range
.interval('1m')
.rate(1)
.generator((timestamp) => {
return [
service.timestamp(timestamp),
host.timestamp(timestamp),
container.timestamp(timestamp),
];
});
}
export function generateTraces({ from, to }: { from: number; to: number }) {
const synthNodeTraceLogs = apm
.service({
name: SERVICE_NAME,
environment: ENVIRONMENT,
agentName: AGENT_NAME,
})
.instance(HOST_NAME);
const range = timerange(from, to);
return range
.interval('1m')
.rate(1)
.generator((timestamp) => {
return [
synthNodeTraceLogs
.transaction({ transactionName: 't1' })
.timestamp(timestamp)
.duration(1000)
.success(),
];
});
}
export function generateHosts({ from, to }: { from: string; to: string }) {
const range = timerange(from, to);
const hosts: Array<{ hostName: string; cpuValue?: number }> = [
{
hostName: HOST_NAME,
cpuValue: 0.5,
},
];
return range
.interval('30s')
.rate(1)
.generator((timestamp) =>
hosts.flatMap(({ hostName, cpuValue }) => [
infra.host(hostName).cpu({ cpuTotalValue: cpuValue }).timestamp(timestamp),
infra.host(hostName).memory().timestamp(timestamp),
infra.host(hostName).network().timestamp(timestamp),
infra.host(hostName).load().timestamp(timestamp),
infra.host(hostName).filesystem().timestamp(timestamp),
infra.host(hostName).diskio().timestamp(timestamp),
infra.host(hostName).core().timestamp(timestamp),
])
);
}
const alertIndexes = [
'.alerts-observability.apm.alerts-default',
'.alerts-observability.uptime.alerts-default',
'.alerts-observability.metrics.alerts-default',
'.alerts-default.alerts-default',
'.alerts-observability.logs.alerts-default',
'.alerts-observability.slo.alerts-default',
'.alerts-observability.threshold.alerts-default',
];
const entityIdentityFields = [
{
'service.name': SERVICE_NAME,
},
{
'host.name': HOST_NAME,
},
{
'container.id': CONTAINER_ID,
},
];
export const cleanEntityAlerts = () => {
entityIdentityFields.forEach((entityIdentityField) => {
alertIndexes.forEach((index) => {
cy.request({
url: `/api/console/proxy?path=${index}%2F_delete_by_query&method=POST`,
method: 'POST',
body: {
query: {
match: {
...entityIdentityField,
},
},
},
headers: { 'kbn-xsrf': true },
});
});
});
};
export const generateEntityAlerts = (start: string) => {
entityIdentityFields.forEach((entityIdentityField) => {
alertIndexes.forEach((index) => {
cy.request({
url: `/api/console/proxy?path=${index}%2F_doc&method=POST`,
method: 'POST',
body: alert({ entityIdentityField, start, index }),
headers: { 'kbn-xsrf': true },
});
});
});
};
const alert = ({ entityIdentityField, start, index }: any) => {
return {
'processor.event': 'transaction',
'kibana.alert.evaluation.value': 1,
'kibana.alert.evaluation.threshold': 10,
'kibana.alert.reason': 'Test alert reason',
'service.environment': 'ENVIRONMENT',
...entityIdentityField,
'transaction.type': 'request',
'kibana.alert.rule.category': 'Test rule category',
'kibana.alert.rule.consumer': 'apm',
'kibana.alert.rule.name': 'Test rule name',
'kibana.alert.rule.parameters': {
environment: 'ENVIRONMENT',
threshold: 10,
windowSize: 5,
windowUnit: 'm',
},
'kibana.alert.rule.producer': 'apm',
'kibana.alert.rule.revision': 0,
'kibana.alert.rule.rule_type_id': 'apm.transaction_error_rate',
'kibana.alert.rule.tags': ['apm'],
'kibana.alert.rule.uuid': index,
'kibana.space_ids': ['default'],
'@timestamp': start,
'event.action': 'active',
'event.kind': 'signal',
'kibana.alert.rule.execution.timestamp': start,
'kibana.alert.action_group': 'threshold_met',
'kibana.alert.flapping': true,
'kibana.alert.flapping_history': [],
'kibana.alert.maintenance_window_ids': [],
'kibana.alert.consecutive_matches': 1,
'kibana.alert.status': 'active',
'kibana.alert.uuid': index,
'kibana.alert.workflow_status': 'open',
'kibana.alert.duration.us': 3298850000,
'kibana.alert.start': start,
'kibana.alert.time_range': {
gte: start,
},
tags: ['apm'],
'kibana.alert.previous_action_group': 'threshold_met',
};
};

View file

@ -8,8 +8,9 @@ import {
ApmSynthtraceEsClient,
EntitiesSynthtraceEsClient,
LogLevel,
LogsSynthtraceEsClient,
createLogger,
InfraSynthtraceEsClient,
LogsSynthtraceEsClient,
} from '@kbn/apm-synthtrace';
import { createEsClientForTesting } from '@kbn/test';
// eslint-disable-next-line @kbn/imports/no_unresolvable_imports
@ -46,6 +47,12 @@ export function setupNodeEvents(on: Cypress.PluginEvents, config: Cypress.Plugin
refreshAfterIndex: true,
});
const infraSynthtraceEsClient = new InfraSynthtraceEsClient({
client,
logger,
refreshAfterIndex: true,
});
entitiesSynthtraceEsClient.pipeline(
entitiesSynthtraceEsClient.getDefaultPipeline({ includeSerialization: false })
);
@ -58,6 +65,10 @@ export function setupNodeEvents(on: Cypress.PluginEvents, config: Cypress.Plugin
logsSynthtraceEsClient.getDefaultPipeline({ includeSerialization: false })
);
infraSynthtraceEsClient.pipeline(
infraSynthtraceEsClient.getDefaultPipeline({ includeSerialization: false })
);
initPlugin(on, config);
on('task', {
@ -94,6 +105,14 @@ export function setupNodeEvents(on: Cypress.PluginEvents, config: Cypress.Plugin
await logsSynthtraceEsClient.clean();
return null;
},
async 'infraSynthtrace:index'(events: Array<Record<string, any>>) {
await infraSynthtraceEsClient.index(Readable.from(events));
return null;
},
async 'infraSynthtrace:clean'() {
await infraSynthtraceEsClient.clean();
return null;
},
});
on('after:spec', (spec, results) => {

View file

@ -9,6 +9,7 @@ import type {
SynthtraceGenerator,
EntityFields,
ApmFields,
InfraDocument,
} from '@kbn/apm-synthtrace-client';
export const entitiesSynthtrace = {
@ -37,3 +38,13 @@ export const logsSynthtrace = {
),
clean: () => cy.task('logsSynthtrace:clean'),
};
export const infraSynthtrace = {
index: (events: SynthtraceGenerator<InfraDocument>) => {
return cy.task(
'infraSynthtrace:index',
Array.from(events).flatMap((event) => event.serialize())
);
},
clean: () => cy.task('infraSynthtrace:clean'),
};