Skip to content

Commit 402ed88

Browse files
Merge branch 'master' into docs/visualizations
2 parents ad8aa44 + 1f8da9d commit 402ed88

File tree

15 files changed

+248
-50
lines changed

15 files changed

+248
-50
lines changed
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Licensed to Elasticsearch B.V. under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch B.V. licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
import { MetricsCollector } from './types';
21+
22+
const createMock = () => {
23+
const mocked: jest.Mocked<MetricsCollector<any>> = {
24+
collect: jest.fn(),
25+
reset: jest.fn(),
26+
};
27+
28+
mocked.collect.mockResolvedValue({});
29+
30+
return mocked;
31+
};
32+
33+
export const collectorMock = {
34+
create: createMock,
35+
};

src/core/server/metrics/collectors/os.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,4 +57,6 @@ export class OsMetricsCollector implements MetricsCollector<OpsOsMetrics> {
5757

5858
return metrics;
5959
}
60+
61+
public reset() {}
6062
}

src/core/server/metrics/collectors/process.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ export class ProcessMetricsCollector implements MetricsCollector<OpsProcessMetri
4040
uptime_in_millis: process.uptime() * 1000,
4141
};
4242
}
43+
44+
public reset() {}
4345
}
4446

4547
const getEventLoopDelay = (): Promise<number> => {

src/core/server/metrics/collectors/server.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,12 @@ interface ServerResponseTime {
2626
}
2727

2828
export class ServerMetricsCollector implements MetricsCollector<OpsServerMetrics> {
29-
private readonly requests: OpsServerMetrics['requests'] = {
29+
private requests: OpsServerMetrics['requests'] = {
3030
disconnects: 0,
3131
total: 0,
3232
statusCodes: {},
3333
};
34-
private readonly responseTimes: ServerResponseTime = {
34+
private responseTimes: ServerResponseTime = {
3535
count: 0,
3636
total: 0,
3737
max: 0,
@@ -77,4 +77,17 @@ export class ServerMetricsCollector implements MetricsCollector<OpsServerMetrics
7777
concurrent_connections: connections,
7878
};
7979
}
80+
81+
public reset() {
82+
this.requests = {
83+
disconnects: 0,
84+
total: 0,
85+
statusCodes: {},
86+
};
87+
this.responseTimes = {
88+
count: 0,
89+
total: 0,
90+
max: 0,
91+
};
92+
}
8093
}

src/core/server/metrics/collectors/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@
1919

2020
/** Base interface for all metrics gatherers */
2121
export interface MetricsCollector<T> {
22+
/** collect the data currently gathered by the collector */
2223
collect(): Promise<T>;
24+
/** reset the internal state of the collector */
25+
reset(): void;
2326
}
2427

2528
/**

src/core/server/metrics/integration_tests/server_collector.test.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,4 +200,80 @@ describe('ServerMetricsCollector', () => {
200200
metrics = await collector.collect();
201201
expect(metrics.concurrent_connections).toEqual(0);
202202
});
203+
204+
describe('#reset', () => {
205+
it('reset the requests state', async () => {
206+
router.get({ path: '/', validate: false }, async (ctx, req, res) => {
207+
return res.ok({ body: '' });
208+
});
209+
await server.start();
210+
211+
await sendGet('/');
212+
await sendGet('/');
213+
await sendGet('/not-found');
214+
215+
let metrics = await collector.collect();
216+
217+
expect(metrics.requests).toEqual({
218+
total: 3,
219+
disconnects: 0,
220+
statusCodes: {
221+
'200': 2,
222+
'404': 1,
223+
},
224+
});
225+
226+
collector.reset();
227+
metrics = await collector.collect();
228+
229+
expect(metrics.requests).toEqual({
230+
total: 0,
231+
disconnects: 0,
232+
statusCodes: {},
233+
});
234+
235+
await sendGet('/');
236+
await sendGet('/not-found');
237+
238+
metrics = await collector.collect();
239+
240+
expect(metrics.requests).toEqual({
241+
total: 2,
242+
disconnects: 0,
243+
statusCodes: {
244+
'200': 1,
245+
'404': 1,
246+
},
247+
});
248+
});
249+
250+
it('resets the response times', async () => {
251+
router.get({ path: '/no-delay', validate: false }, async (ctx, req, res) => {
252+
return res.ok({ body: '' });
253+
});
254+
router.get({ path: '/500-ms', validate: false }, async (ctx, req, res) => {
255+
await delay(500);
256+
return res.ok({ body: '' });
257+
});
258+
259+
await server.start();
260+
261+
await Promise.all([sendGet('/no-delay'), sendGet('/500-ms')]);
262+
let metrics = await collector.collect();
263+
264+
expect(metrics.response_times.avg_in_millis).toBeGreaterThanOrEqual(250);
265+
expect(metrics.response_times.max_in_millis).toBeGreaterThanOrEqual(500);
266+
267+
collector.reset();
268+
metrics = await collector.collect();
269+
expect(metrics.response_times.avg_in_millis).toBe(0);
270+
expect(metrics.response_times.max_in_millis).toBeGreaterThanOrEqual(0);
271+
272+
await Promise.all([sendGet('/500-ms'), sendGet('/500-ms')]);
273+
metrics = await collector.collect();
274+
275+
expect(metrics.response_times.avg_in_millis).toBeGreaterThanOrEqual(500);
276+
expect(metrics.response_times.max_in_millis).toBeGreaterThanOrEqual(500);
277+
});
278+
});
203279
});

src/core/server/metrics/metrics_service.test.mocks.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,10 @@
1717
* under the License.
1818
*/
1919

20-
export const mockOpsCollector = {
21-
collect: jest.fn(),
22-
};
20+
import { collectorMock } from './collectors/mocks';
21+
22+
export const mockOpsCollector = collectorMock.create();
23+
2324
jest.doMock('./ops_metrics_collector', () => ({
2425
OpsMetricsCollector: jest.fn().mockImplementation(() => mockOpsCollector),
2526
}));

src/core/server/metrics/metrics_service.test.ts

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -57,37 +57,50 @@ describe('MetricsService', () => {
5757
expect(setInterval).toHaveBeenCalledWith(expect.any(Function), testInterval);
5858
});
5959

60-
it('emits the metrics at start', async () => {
60+
it('collects the metrics at every interval', async () => {
6161
mockOpsCollector.collect.mockResolvedValue(dummyMetrics);
6262

63-
const { getOpsMetrics$ } = await metricsService.setup({
64-
http: httpMock,
65-
});
66-
63+
await metricsService.setup({ http: httpMock });
6764
await metricsService.start();
6865

6966
expect(mockOpsCollector.collect).toHaveBeenCalledTimes(1);
70-
expect(
71-
await getOpsMetrics$()
72-
.pipe(take(1))
73-
.toPromise()
74-
).toEqual(dummyMetrics);
67+
68+
jest.advanceTimersByTime(testInterval);
69+
expect(mockOpsCollector.collect).toHaveBeenCalledTimes(2);
70+
71+
jest.advanceTimersByTime(testInterval);
72+
expect(mockOpsCollector.collect).toHaveBeenCalledTimes(3);
7573
});
7674

77-
it('collects the metrics at every interval', async () => {
75+
it('resets the collector after each collection', async () => {
7876
mockOpsCollector.collect.mockResolvedValue(dummyMetrics);
7977

80-
await metricsService.setup({ http: httpMock });
81-
78+
const { getOpsMetrics$ } = await metricsService.setup({ http: httpMock });
8279
await metricsService.start();
8380

81+
// `advanceTimersByTime` only ensure the interval handler is executed
82+
// however the `reset` call is executed after the async call to `collect`
83+
// meaning that we are going to miss the call if we don't wait for the
84+
// actual observable emission that is performed after
85+
const waitForNextEmission = () =>
86+
getOpsMetrics$()
87+
.pipe(take(1))
88+
.toPromise();
89+
8490
expect(mockOpsCollector.collect).toHaveBeenCalledTimes(1);
91+
expect(mockOpsCollector.reset).toHaveBeenCalledTimes(1);
8592

93+
let nextEmission = waitForNextEmission();
8694
jest.advanceTimersByTime(testInterval);
95+
await nextEmission;
8796
expect(mockOpsCollector.collect).toHaveBeenCalledTimes(2);
97+
expect(mockOpsCollector.reset).toHaveBeenCalledTimes(2);
8898

99+
nextEmission = waitForNextEmission();
89100
jest.advanceTimersByTime(testInterval);
101+
await nextEmission;
90102
expect(mockOpsCollector.collect).toHaveBeenCalledTimes(3);
103+
expect(mockOpsCollector.reset).toHaveBeenCalledTimes(3);
91104
});
92105

93106
it('throws when called before setup', async () => {

src/core/server/metrics/metrics_service.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@
1717
* under the License.
1818
*/
1919

20-
import { ReplaySubject } from 'rxjs';
21-
import { first, shareReplay } from 'rxjs/operators';
20+
import { Subject } from 'rxjs';
21+
import { first } from 'rxjs/operators';
2222
import { CoreService } from '../../types';
2323
import { CoreContext } from '../core_context';
2424
import { Logger } from '../logging';
@@ -37,7 +37,7 @@ export class MetricsService
3737
private readonly logger: Logger;
3838
private metricsCollector?: OpsMetricsCollector;
3939
private collectInterval?: NodeJS.Timeout;
40-
private metrics$ = new ReplaySubject<OpsMetrics>(1);
40+
private metrics$ = new Subject<OpsMetrics>();
4141

4242
constructor(private readonly coreContext: CoreContext) {
4343
this.logger = coreContext.logger.get('metrics');
@@ -46,7 +46,7 @@ export class MetricsService
4646
public async setup({ http }: MetricsServiceSetupDeps): Promise<InternalMetricsServiceSetup> {
4747
this.metricsCollector = new OpsMetricsCollector(http.server);
4848

49-
const metricsObservable = this.metrics$.pipe(shareReplay(1));
49+
const metricsObservable = this.metrics$.asObservable();
5050

5151
return {
5252
getOpsMetrics$: () => metricsObservable,
@@ -74,6 +74,7 @@ export class MetricsService
7474
private async refreshMetrics() {
7575
this.logger.debug('Refreshing metrics');
7676
const metrics = await this.metricsCollector!.collect();
77+
this.metricsCollector!.reset();
7778
this.metrics$.next(metrics);
7879
}
7980

src/core/server/metrics/ops_metrics_collector.test.mocks.ts

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,23 +17,19 @@
1717
* under the License.
1818
*/
1919

20-
export const mockOsCollector = {
21-
collect: jest.fn(),
22-
};
20+
import { collectorMock } from './collectors/mocks';
21+
22+
export const mockOsCollector = collectorMock.create();
2323
jest.doMock('./collectors/os', () => ({
2424
OsMetricsCollector: jest.fn().mockImplementation(() => mockOsCollector),
2525
}));
2626

27-
export const mockProcessCollector = {
28-
collect: jest.fn(),
29-
};
27+
export const mockProcessCollector = collectorMock.create();
3028
jest.doMock('./collectors/process', () => ({
3129
ProcessMetricsCollector: jest.fn().mockImplementation(() => mockProcessCollector),
3230
}));
3331

34-
export const mockServerCollector = {
35-
collect: jest.fn(),
36-
};
32+
export const mockServerCollector = collectorMock.create();
3733
jest.doMock('./collectors/server', () => ({
3834
ServerMetricsCollector: jest.fn().mockImplementation(() => mockServerCollector),
3935
}));

0 commit comments

Comments
 (0)