[APM] Catch health status error from ML (#80131)

Closes #80119.
This commit is contained in:
Dario Gieselaar 2020-10-12 20:16:51 +02:00 committed by GitHub
parent 51e93dfe64
commit a6b32ab0a2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 141 additions and 54 deletions

View file

@ -3,6 +3,7 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { Logger } from '@kbn/logging';
import { joinByKey } from '../../../../common/utils/join_by_key';
import { PromiseReturnType } from '../../../../typings/common';
import { Setup, SetupTimeRange } from '../../helpers/setup_request';
@ -23,9 +24,11 @@ export type ServicesItemsProjection = ReturnType<typeof getServicesProjection>;
export async function getServicesItems({
setup,
searchAggregatedTransactions,
logger,
}: {
setup: ServicesItemsSetup;
searchAggregatedTransactions: boolean;
logger: Logger;
}) {
const params = {
projection: getServicesProjection({
@ -49,7 +52,10 @@ export async function getServicesItems({
getTransactionRates(params),
getTransactionErrorRates(params),
getEnvironments(params),
getHealthStatuses(params, setup.uiFilters.environment),
getHealthStatuses(params, setup.uiFilters.environment).catch((err) => {
logger.error(err);
return [];
}),
]);
const allMetrics = [

View file

@ -5,6 +5,7 @@
*/
import { isEmpty } from 'lodash';
import { Logger } from '@kbn/logging';
import { PromiseReturnType } from '../../../../typings/common';
import { Setup, SetupTimeRange } from '../../helpers/setup_request';
import { hasHistoricalAgentData } from './has_historical_agent_data';
@ -16,14 +17,17 @@ export type ServiceListAPIResponse = PromiseReturnType<typeof getServices>;
export async function getServices({
setup,
searchAggregatedTransactions,
logger,
}: {
setup: Setup & SetupTimeRange;
searchAggregatedTransactions: boolean;
logger: Logger;
}) {
const [items, hasLegacyData] = await Promise.all([
getServicesItems({
setup,
searchAggregatedTransactions,
logger,
}),
getLegacyDataStatus(setup),
]);

View file

@ -47,7 +47,11 @@ describe('services queries', () => {
it('fetches the service items', async () => {
mock = await inspectSearchParams((setup) =>
getServicesItems({ setup, searchAggregatedTransactions: false })
getServicesItems({
setup,
searchAggregatedTransactions: false,
logger: {} as any,
})
);
const allParams = mock.spy.mock.calls.map((call) => call[0]);

View file

@ -30,7 +30,11 @@ export const servicesRoute = createRoute(() => ({
setup
);
const services = await getServices({ setup, searchAggregatedTransactions });
const services = await getServices({
setup,
searchAggregatedTransactions,
logger: context.logger,
});
return services;
},

View file

@ -14,6 +14,7 @@ export enum ApmUser {
apmReadUser = 'apm_read_user',
apmWriteUser = 'apm_write_user',
apmAnnotationsWriteUser = 'apm_annotations_write_user',
apmReadUserWithoutMlAccess = 'apm_read_user_without_ml_access',
}
const roles = {
@ -27,6 +28,15 @@ const roles = {
},
],
},
[ApmUser.apmReadUserWithoutMlAccess]: {
kibana: [
{
base: [],
feature: { apm: ['read'] },
spaces: ['*'],
},
],
},
[ApmUser.apmWriteUser]: {
kibana: [
{
@ -63,6 +73,9 @@ const users = {
[ApmUser.apmReadUser]: {
roles: ['apm_user', ApmUser.apmReadUser],
},
[ApmUser.apmReadUserWithoutMlAccess]: {
roles: ['apm_user', ApmUser.apmReadUserWithoutMlAccess],
},
[ApmUser.apmWriteUser]: {
roles: ['apm_user', ApmUser.apmWriteUser],
},

View file

@ -63,6 +63,10 @@ export function createTestConfig(settings: Settings) {
servers.kibana,
ApmUser.apmAnnotationsWriteUser
),
supertestAsApmReadUserWithoutMlAccess: supertestAsApmUser(
servers.kibana,
ApmUser.apmReadUserWithoutMlAccess
),
},
junit: {
reportName: name,

View file

@ -1046,7 +1046,7 @@ Array [
]
`;
exports[`Service Maps with a trial license when there is data with anomalies returns the correct anomaly stats 3`] = `
exports[`Service Maps with a trial license when there is data with anomalies with the default apm user returns the correct anomaly stats 3`] = `
Object {
"elements": Array [
Object {

View file

@ -14,6 +14,8 @@ import { FtrProviderContext } from '../../../common/ftr_provider_context';
export default function serviceMapsApiTests({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
const supertestAsApmReadUserWithoutMlAccess = getService('supertestAsApmReadUserWithoutMlAccess');
const esArchiver = getService('esArchiver');
const archiveName = 'apm_8.0.0';
@ -128,34 +130,35 @@ export default function serviceMapsApiTests({ getService }: FtrProviderContext)
before(() => esArchiver.load(archiveName));
after(() => esArchiver.unload(archiveName));
let response: PromiseReturnType<typeof supertest.get>;
describe('with the default apm user', () => {
let response: PromiseReturnType<typeof supertest.get>;
before(async () => {
response = await supertest.get(`/api/apm/service-map?start=${start}&end=${end}`);
});
it('returns service map elements with anomaly stats', () => {
expect(response.status).to.be(200);
const dataWithAnomalies = response.body.elements.filter(
(el: { data: { serviceAnomalyStats?: {} } }) => !isEmpty(el.data.serviceAnomalyStats)
);
expect(dataWithAnomalies).to.not.empty();
dataWithAnomalies.forEach(({ data }: any) => {
expect(
Object.values(data.serviceAnomalyStats).filter((value) => isEmpty(value))
).to.not.empty();
before(async () => {
response = await supertest.get(`/api/apm/service-map?start=${start}&end=${end}`);
});
});
it('returns the correct anomaly stats', () => {
const dataWithAnomalies = response.body.elements.filter(
(el: { data: { serviceAnomalyStats?: {} } }) => !isEmpty(el.data.serviceAnomalyStats)
);
it('returns service map elements with anomaly stats', () => {
expect(response.status).to.be(200);
const dataWithAnomalies = response.body.elements.filter(
(el: { data: { serviceAnomalyStats?: {} } }) => !isEmpty(el.data.serviceAnomalyStats)
);
expectSnapshot(dataWithAnomalies.length).toMatchInline(`5`);
expectSnapshot(dataWithAnomalies.slice(0, 3)).toMatchInline(`
expect(dataWithAnomalies).to.not.empty();
dataWithAnomalies.forEach(({ data }: any) => {
expect(
Object.values(data.serviceAnomalyStats).filter((value) => isEmpty(value))
).to.not.empty();
});
});
it('returns the correct anomaly stats', () => {
const dataWithAnomalies = response.body.elements.filter(
(el: { data: { serviceAnomalyStats?: {} } }) => !isEmpty(el.data.serviceAnomalyStats)
);
expectSnapshot(dataWithAnomalies.length).toMatchInline(`5`);
expectSnapshot(dataWithAnomalies.slice(0, 3)).toMatchInline(`
Array [
Object {
"data": Object {
@ -203,7 +206,28 @@ export default function serviceMapsApiTests({ getService }: FtrProviderContext)
]
`);
expectSnapshot(response.body).toMatch();
expectSnapshot(response.body).toMatch();
});
});
describe('with a user that does not have access to ML', () => {
let response: PromiseReturnType<typeof supertest.get>;
before(async () => {
response = await supertestAsApmReadUserWithoutMlAccess.get(
`/api/apm/service-map?start=${start}&end=${end}`
);
});
it('returns service map elements without anomaly stats', () => {
expect(response.status).to.be(200);
const dataWithAnomalies = response.body.elements.filter(
(el: { data: { serviceAnomalyStats?: {} } }) => !isEmpty(el.data.serviceAnomalyStats)
);
expect(dataWithAnomalies).to.be.empty();
});
});
});
});

View file

@ -12,6 +12,7 @@ import archives_metadata from '../../../common/archives_metadata';
export default function ApiTest({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
const supertestAsApmReadUserWithoutMlAccess = getService('supertestAsApmReadUserWithoutMlAccess');
const esArchiver = getService('esArchiver');
const archiveName = 'apm_8.0.0';
@ -29,10 +30,55 @@ export default function ApiTest({ getService }: FtrProviderContext) {
before(() => esArchiver.load(archiveName));
after(() => esArchiver.unload(archiveName));
describe('and fetching a list of services', () => {
describe('with the default APM read user', () => {
describe('and fetching a list of services', () => {
let response: PromiseReturnType<typeof supertest.get>;
before(async () => {
response = await supertest.get(
`/api/apm/services?start=${start}&end=${end}&uiFilters=${uiFilters}`
);
});
it('the response is successful', () => {
expect(response.status).to.eql(200);
});
it('there is at least one service', () => {
expect(response.body.items.length).to.be.greaterThan(0);
});
it('some items have a health status set', () => {
// Under the assumption that the loaded archive has
// at least one APM ML job, and the time range is longer
// than 15m, at least one items should have a health status
// set. Note that we currently have a bug where healthy
// services report as unknown (so without any health status):
// https://github.com/elastic/kibana/issues/77083
const healthStatuses = response.body.items.map((item: any) => item.healthStatus);
expect(healthStatuses.filter(Boolean).length).to.be.greaterThan(0);
expectSnapshot(healthStatuses).toMatchInline(`
Array [
"healthy",
undefined,
"healthy",
undefined,
"healthy",
"healthy",
"healthy",
"healthy",
]
`);
});
});
});
describe('with a user that does not have access to ML', () => {
let response: PromiseReturnType<typeof supertest.get>;
before(async () => {
response = await supertest.get(
response = await supertestAsApmReadUserWithoutMlAccess.get(
`/api/apm/services?start=${start}&end=${end}&uiFilters=${uiFilters}`
);
});
@ -45,30 +91,12 @@ export default function ApiTest({ getService }: FtrProviderContext) {
expect(response.body.items.length).to.be.greaterThan(0);
});
it('some items have a health status set', () => {
// Under the assumption that the loaded archive has
// at least one APM ML job, and the time range is longer
// than 15m, at least one items should have a health status
// set. Note that we currently have a bug where healthy
// services report as unknown (so without any health status):
// https://github.com/elastic/kibana/issues/77083
it('contains no health statuses', () => {
const definedHealthStatuses = response.body.items
.map((item: any) => item.healthStatus)
.filter(Boolean);
const healthStatuses = response.body.items.map((item: any) => item.healthStatus);
expect(healthStatuses.filter(Boolean).length).to.be.greaterThan(0);
expectSnapshot(healthStatuses).toMatchInline(`
Array [
"healthy",
undefined,
"healthy",
undefined,
"healthy",
"healthy",
"healthy",
"healthy",
]
`);
expect(definedHealthStatuses.length).to.be(0);
});
});
});