Skip to content

Commit a6b32ab

Browse files
authored
[APM] Catch health status error from ML (#80131)
Closes #80119.
1 parent 51e93df commit a6b32ab

File tree

9 files changed

+136
-49
lines changed

9 files changed

+136
-49
lines changed

x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
* or more contributor license agreements. Licensed under the Elastic License;
44
* you may not use this file except in compliance with the Elastic License.
55
*/
6+
import { Logger } from '@kbn/logging';
67
import { joinByKey } from '../../../../common/utils/join_by_key';
78
import { PromiseReturnType } from '../../../../typings/common';
89
import { Setup, SetupTimeRange } from '../../helpers/setup_request';
@@ -23,9 +24,11 @@ export type ServicesItemsProjection = ReturnType<typeof getServicesProjection>;
2324
export async function getServicesItems({
2425
setup,
2526
searchAggregatedTransactions,
27+
logger,
2628
}: {
2729
setup: ServicesItemsSetup;
2830
searchAggregatedTransactions: boolean;
31+
logger: Logger;
2932
}) {
3033
const params = {
3134
projection: getServicesProjection({
@@ -49,7 +52,10 @@ export async function getServicesItems({
4952
getTransactionRates(params),
5053
getTransactionErrorRates(params),
5154
getEnvironments(params),
52-
getHealthStatuses(params, setup.uiFilters.environment),
55+
getHealthStatuses(params, setup.uiFilters.environment).catch((err) => {
56+
logger.error(err);
57+
return [];
58+
}),
5359
]);
5460

5561
const allMetrics = [

x-pack/plugins/apm/server/lib/services/get_services/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
*/
66

77
import { isEmpty } from 'lodash';
8+
import { Logger } from '@kbn/logging';
89
import { PromiseReturnType } from '../../../../typings/common';
910
import { Setup, SetupTimeRange } from '../../helpers/setup_request';
1011
import { hasHistoricalAgentData } from './has_historical_agent_data';
@@ -16,14 +17,17 @@ export type ServiceListAPIResponse = PromiseReturnType<typeof getServices>;
1617
export async function getServices({
1718
setup,
1819
searchAggregatedTransactions,
20+
logger,
1921
}: {
2022
setup: Setup & SetupTimeRange;
2123
searchAggregatedTransactions: boolean;
24+
logger: Logger;
2225
}) {
2326
const [items, hasLegacyData] = await Promise.all([
2427
getServicesItems({
2528
setup,
2629
searchAggregatedTransactions,
30+
logger,
2731
}),
2832
getLegacyDataStatus(setup),
2933
]);

x-pack/plugins/apm/server/lib/services/queries.test.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,11 @@ describe('services queries', () => {
4747

4848
it('fetches the service items', async () => {
4949
mock = await inspectSearchParams((setup) =>
50-
getServicesItems({ setup, searchAggregatedTransactions: false })
50+
getServicesItems({
51+
setup,
52+
searchAggregatedTransactions: false,
53+
logger: {} as any,
54+
})
5155
);
5256

5357
const allParams = mock.spy.mock.calls.map((call) => call[0]);

x-pack/plugins/apm/server/routes/services.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,11 @@ export const servicesRoute = createRoute(() => ({
3030
setup
3131
);
3232

33-
const services = await getServices({ setup, searchAggregatedTransactions });
33+
const services = await getServices({
34+
setup,
35+
searchAggregatedTransactions,
36+
logger: context.logger,
37+
});
3438

3539
return services;
3640
},

x-pack/test/apm_api_integration/common/authentication.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export enum ApmUser {
1414
apmReadUser = 'apm_read_user',
1515
apmWriteUser = 'apm_write_user',
1616
apmAnnotationsWriteUser = 'apm_annotations_write_user',
17+
apmReadUserWithoutMlAccess = 'apm_read_user_without_ml_access',
1718
}
1819

1920
const roles = {
@@ -27,6 +28,15 @@ const roles = {
2728
},
2829
],
2930
},
31+
[ApmUser.apmReadUserWithoutMlAccess]: {
32+
kibana: [
33+
{
34+
base: [],
35+
feature: { apm: ['read'] },
36+
spaces: ['*'],
37+
},
38+
],
39+
},
3040
[ApmUser.apmWriteUser]: {
3141
kibana: [
3242
{
@@ -63,6 +73,9 @@ const users = {
6373
[ApmUser.apmReadUser]: {
6474
roles: ['apm_user', ApmUser.apmReadUser],
6575
},
76+
[ApmUser.apmReadUserWithoutMlAccess]: {
77+
roles: ['apm_user', ApmUser.apmReadUserWithoutMlAccess],
78+
},
6679
[ApmUser.apmWriteUser]: {
6780
roles: ['apm_user', ApmUser.apmWriteUser],
6881
},

x-pack/test/apm_api_integration/common/config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@ export function createTestConfig(settings: Settings) {
6363
servers.kibana,
6464
ApmUser.apmAnnotationsWriteUser
6565
),
66+
supertestAsApmReadUserWithoutMlAccess: supertestAsApmUser(
67+
servers.kibana,
68+
ApmUser.apmReadUserWithoutMlAccess
69+
),
6670
},
6771
junit: {
6872
reportName: name,

x-pack/test/apm_api_integration/trial/tests/service_maps/__snapshots__/service_maps.snap

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

x-pack/test/apm_api_integration/trial/tests/service_maps/service_maps.ts

Lines changed: 46 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import { FtrProviderContext } from '../../../common/ftr_provider_context';
1414

1515
export default function serviceMapsApiTests({ getService }: FtrProviderContext) {
1616
const supertest = getService('supertest');
17+
const supertestAsApmReadUserWithoutMlAccess = getService('supertestAsApmReadUserWithoutMlAccess');
18+
1719
const esArchiver = getService('esArchiver');
1820

1921
const archiveName = 'apm_8.0.0';
@@ -128,34 +130,35 @@ export default function serviceMapsApiTests({ getService }: FtrProviderContext)
128130
before(() => esArchiver.load(archiveName));
129131
after(() => esArchiver.unload(archiveName));
130132

131-
let response: PromiseReturnType<typeof supertest.get>;
133+
describe('with the default apm user', () => {
134+
let response: PromiseReturnType<typeof supertest.get>;
132135

133-
before(async () => {
134-
response = await supertest.get(`/api/apm/service-map?start=${start}&end=${end}`);
135-
});
136+
before(async () => {
137+
response = await supertest.get(`/api/apm/service-map?start=${start}&end=${end}`);
138+
});
136139

137-
it('returns service map elements with anomaly stats', () => {
138-
expect(response.status).to.be(200);
139-
const dataWithAnomalies = response.body.elements.filter(
140-
(el: { data: { serviceAnomalyStats?: {} } }) => !isEmpty(el.data.serviceAnomalyStats)
141-
);
140+
it('returns service map elements with anomaly stats', () => {
141+
expect(response.status).to.be(200);
142+
const dataWithAnomalies = response.body.elements.filter(
143+
(el: { data: { serviceAnomalyStats?: {} } }) => !isEmpty(el.data.serviceAnomalyStats)
144+
);
142145

143-
expect(dataWithAnomalies).to.not.empty();
146+
expect(dataWithAnomalies).to.not.empty();
144147

145-
dataWithAnomalies.forEach(({ data }: any) => {
146-
expect(
147-
Object.values(data.serviceAnomalyStats).filter((value) => isEmpty(value))
148-
).to.not.empty();
148+
dataWithAnomalies.forEach(({ data }: any) => {
149+
expect(
150+
Object.values(data.serviceAnomalyStats).filter((value) => isEmpty(value))
151+
).to.not.empty();
152+
});
149153
});
150-
});
151154

152-
it('returns the correct anomaly stats', () => {
153-
const dataWithAnomalies = response.body.elements.filter(
154-
(el: { data: { serviceAnomalyStats?: {} } }) => !isEmpty(el.data.serviceAnomalyStats)
155-
);
155+
it('returns the correct anomaly stats', () => {
156+
const dataWithAnomalies = response.body.elements.filter(
157+
(el: { data: { serviceAnomalyStats?: {} } }) => !isEmpty(el.data.serviceAnomalyStats)
158+
);
156159

157-
expectSnapshot(dataWithAnomalies.length).toMatchInline(`5`);
158-
expectSnapshot(dataWithAnomalies.slice(0, 3)).toMatchInline(`
160+
expectSnapshot(dataWithAnomalies.length).toMatchInline(`5`);
161+
expectSnapshot(dataWithAnomalies.slice(0, 3)).toMatchInline(`
159162
Array [
160163
Object {
161164
"data": Object {
@@ -203,7 +206,28 @@ export default function serviceMapsApiTests({ getService }: FtrProviderContext)
203206
]
204207
`);
205208

206-
expectSnapshot(response.body).toMatch();
209+
expectSnapshot(response.body).toMatch();
210+
});
211+
});
212+
213+
describe('with a user that does not have access to ML', () => {
214+
let response: PromiseReturnType<typeof supertest.get>;
215+
216+
before(async () => {
217+
response = await supertestAsApmReadUserWithoutMlAccess.get(
218+
`/api/apm/service-map?start=${start}&end=${end}`
219+
);
220+
});
221+
222+
it('returns service map elements without anomaly stats', () => {
223+
expect(response.status).to.be(200);
224+
225+
const dataWithAnomalies = response.body.elements.filter(
226+
(el: { data: { serviceAnomalyStats?: {} } }) => !isEmpty(el.data.serviceAnomalyStats)
227+
);
228+
229+
expect(dataWithAnomalies).to.be.empty();
230+
});
207231
});
208232
});
209233
});

x-pack/test/apm_api_integration/trial/tests/services/top_services.ts

Lines changed: 51 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import archives_metadata from '../../../common/archives_metadata';
1212

1313
export default function ApiTest({ getService }: FtrProviderContext) {
1414
const supertest = getService('supertest');
15+
const supertestAsApmReadUserWithoutMlAccess = getService('supertestAsApmReadUserWithoutMlAccess');
1516
const esArchiver = getService('esArchiver');
1617

1718
const archiveName = 'apm_8.0.0';
@@ -29,35 +30,36 @@ export default function ApiTest({ getService }: FtrProviderContext) {
2930
before(() => esArchiver.load(archiveName));
3031
after(() => esArchiver.unload(archiveName));
3132

32-
describe('and fetching a list of services', () => {
33-
let response: PromiseReturnType<typeof supertest.get>;
34-
before(async () => {
35-
response = await supertest.get(
36-
`/api/apm/services?start=${start}&end=${end}&uiFilters=${uiFilters}`
37-
);
38-
});
33+
describe('with the default APM read user', () => {
34+
describe('and fetching a list of services', () => {
35+
let response: PromiseReturnType<typeof supertest.get>;
36+
before(async () => {
37+
response = await supertest.get(
38+
`/api/apm/services?start=${start}&end=${end}&uiFilters=${uiFilters}`
39+
);
40+
});
3941

40-
it('the response is successful', () => {
41-
expect(response.status).to.eql(200);
42-
});
42+
it('the response is successful', () => {
43+
expect(response.status).to.eql(200);
44+
});
4345

44-
it('there is at least one service', () => {
45-
expect(response.body.items.length).to.be.greaterThan(0);
46-
});
46+
it('there is at least one service', () => {
47+
expect(response.body.items.length).to.be.greaterThan(0);
48+
});
4749

48-
it('some items have a health status set', () => {
49-
// Under the assumption that the loaded archive has
50-
// at least one APM ML job, and the time range is longer
51-
// than 15m, at least one items should have a health status
52-
// set. Note that we currently have a bug where healthy
53-
// services report as unknown (so without any health status):
54-
// https://github.com/elastic/kibana/issues/77083
50+
it('some items have a health status set', () => {
51+
// Under the assumption that the loaded archive has
52+
// at least one APM ML job, and the time range is longer
53+
// than 15m, at least one items should have a health status
54+
// set. Note that we currently have a bug where healthy
55+
// services report as unknown (so without any health status):
56+
// https://github.com/elastic/kibana/issues/77083
5557

56-
const healthStatuses = response.body.items.map((item: any) => item.healthStatus);
58+
const healthStatuses = response.body.items.map((item: any) => item.healthStatus);
5759

58-
expect(healthStatuses.filter(Boolean).length).to.be.greaterThan(0);
60+
expect(healthStatuses.filter(Boolean).length).to.be.greaterThan(0);
5961

60-
expectSnapshot(healthStatuses).toMatchInline(`
62+
expectSnapshot(healthStatuses).toMatchInline(`
6163
Array [
6264
"healthy",
6365
undefined,
@@ -69,6 +71,32 @@ export default function ApiTest({ getService }: FtrProviderContext) {
6971
"healthy",
7072
]
7173
`);
74+
});
75+
});
76+
});
77+
78+
describe('with a user that does not have access to ML', () => {
79+
let response: PromiseReturnType<typeof supertest.get>;
80+
before(async () => {
81+
response = await supertestAsApmReadUserWithoutMlAccess.get(
82+
`/api/apm/services?start=${start}&end=${end}&uiFilters=${uiFilters}`
83+
);
84+
});
85+
86+
it('the response is successful', () => {
87+
expect(response.status).to.eql(200);
88+
});
89+
90+
it('there is at least one service', () => {
91+
expect(response.body.items.length).to.be.greaterThan(0);
92+
});
93+
94+
it('contains no health statuses', () => {
95+
const definedHealthStatuses = response.body.items
96+
.map((item: any) => item.healthStatus)
97+
.filter(Boolean);
98+
99+
expect(definedHealthStatuses.length).to.be(0);
72100
});
73101
});
74102
});

0 commit comments

Comments
 (0)