Skip to content
This repository has been archived by the owner on May 3, 2024. It is now read-only.

Commit

Permalink
refactor: migrating metrics server to Fastify (#803)
Browse files Browse the repository at this point in the history
* feat(deps): upgrade to react 17

BREAKING CHANGE: Upgrade from React 16 to 17

* feat(server): drop node 12 support

BREAKING CHANGE: minimum supported node version is 16

* test(modules): fix dep resolution error

* chore(release): 6.0.0-rc.0

* chore(bundle-size-action): wider strip-hash capture

* chore(deps): update packages to latest compatible versions

* chore(babel): update packages

* chore(commitlint): update

* chore(rollup-plugins): update

* chore(acorn): uninstall

* chore(babel-preset-amex): update to 4

* chore(body-parser): update

* chore(dev-deps): update

* chore(holocron): update 1.3.0

* chore(redux): update 4.2.0

* chore(core-js): update 3.23.3

* chore(deps): run npm update

* chore(husky): update to 8.x

* chore(chalk): downgrade to non esm version

* chore(webdriverio): update 7.x

* feat(dockerfile): update node version to 16.15.1

* chore(deps): update supertest

* fix(node): set min version 16.15.1

* chore(deps): dedupe

* test(createRequestHtmlFragment): more reliable error message

* chore(jest): upgrade 28.1.2

* fix(helmet): disable breaking headers (#780)

* feat(deps): upgrade to react 17

BREAKING CHANGE: Upgrade from React 16 to 17

* feat(server): drop node 12 support

BREAKING CHANGE: minimum supported node version is 16

* test(modules): fix dep resolution error

* chore(release): 6.0.0-rc.0

* chore(bundle-size-action): wider strip-hash capture

* chore(deps): update packages to latest compatible versions

* chore(babel): update packages

* chore(commitlint): update

* chore(rollup-plugins): update

* chore(acorn): uninstall

* chore(babel-preset-amex): update to 4

* chore(body-parser): update

* chore(dev-deps): update

* chore(holocron): update 1.3.0

* chore(redux): update 4.2.0

* chore(core-js): update 3.23.3

* chore(deps): run npm update

* chore(husky): update to 8.x

* chore(chalk): downgrade to non esm version

* chore(webdriverio): update 7.x

* feat(dockerfile): update node version to 16.15.1

* chore(deps): update supertest

* fix(node): set min version 16.15.1

* chore(deps): dedupe

* test(createRequestHtmlFragment): more reliable error message

* chore(jest): upgrade 28.1.2

* fix(helmet): disable breaking headers (#780)

* chore(changelog): correct 5.15.0

* feat: running app through fastify

* chore: fixing unit tests

* chore: fixed unit testing

* fix: tests

* feat: added rate limiter in metrics api

* chore: minor adjustments

* feat: metrics server fastify migration

* refactor: migrated logging middleware to fastify as plugin

* test: added missing test for coverage

* refactor: added rate limit plugin

* refactor: converted hardcoded route into decorator

* refactor: removed health check middleware

* chore: feedback

Co-authored-by: Jamie King <jamie.king@aexp.com>
Co-authored-by: Jonny Adshead <JAdshead@users.noreply.github.com>
Co-authored-by: Jonathan Adshead <jonathan.adshead@aexp.com>
  • Loading branch information
4 people authored Sep 2, 2022
1 parent 1b2dca1 commit a0fc9ed
Show file tree
Hide file tree
Showing 10 changed files with 1,386 additions and 597 deletions.
67 changes: 23 additions & 44 deletions __tests__/server/metricsServer.spec.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2019 American Express Travel Related Services Company, Inc.
* Copyright 2022 American Express Travel Related Services Company, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -22,25 +22,42 @@ describe('metricsServer', () => {
jest.spyOn(console, 'warn').mockImplementation(() => {});

let client;
let helmet;
let healthCheck;
let logging;

async function load() {
jest.resetModules();

jest.mock('prom-client');
jest.mock('../../src/server/utils/logging/serverMiddleware', () => jest.fn((req, res, next) => next()));
jest.mock('../../src/server/middleware/healthCheck');
jest.mock('prom-client', () => ({
collectDefaultMetrics: jest.fn(),
register: {
metrics: jest.fn(async () => 'unit testing'),
contentType: 'unit/testing',
},
}));
jest.mock('@fastify/helmet', () => jest.fn((_fastify, _opts, done) => done()));
jest.mock('../../src/server/utils/logging/fastifyPlugin', () => jest.fn((_fastify, _opts, done) => done()));
jest.mock('../../src/server/plugins/healthCheck', () => jest.fn((_fastify, _opts, done) => done()));

client = require('prom-client');
logging = require('../../src/server/utils/logging/serverMiddleware');
healthCheck = require('../../src/server/middleware/healthCheck').default;
helmet = require('@fastify/helmet');
logging = require('../../src/server/utils/logging/fastifyPlugin');
healthCheck = require('../../src/server/plugins/healthCheck');

const fastify = await require('../../src/server/metricsServer').default();

return fastify;
}

it('registers the required plugins', async () => {
await load();

expect(logging).toHaveBeenCalledTimes(1);
expect(healthCheck).toHaveBeenCalledTimes(1);
expect(helmet).toHaveBeenCalledTimes(1);
});

describe('metrics setup', () => {
it('collects default metrics', async () => {
await load();
Expand Down Expand Up @@ -69,44 +86,6 @@ describe('metricsServer', () => {
expect(response.body).toEqual('');
expect(response.headers['content-type']).toEqual('text/plain; charset=utf-8');
});

it('should log the request', async () => {
logging.mockClear();
process.env.NODE_ENV = 'production';

const metricsServer = await load();
expect(logging).not.toHaveBeenCalled();

await metricsServer.inject({
method: 'GET',
url: '/doesnt-exist',
});
expect(logging).toHaveBeenCalledTimes(1);
});
});

describe('/im-up', () => {
it('should run health checks', async () => {
const instance = await load();
await instance.inject({
method: 'GET',
url: '/im-up',
});

expect(healthCheck).toBeCalled();
});

it('should log the request', async () => {
logging.mockClear();

const instance = await load();
await instance.inject({
method: 'GET',
url: '/im-up',
});

expect(logging).toHaveBeenCalledTimes(1);
});
});

describe('/metrics', () => {
Expand Down
54 changes: 0 additions & 54 deletions __tests__/server/middleware/__snapshots__/healthCheck.spec.js.snap

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2019 American Express Travel Related Services Company, Inc.
* Copyright 2022 American Express Travel Related Services Company, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -16,11 +16,12 @@

import pidusage from 'pidusage';
import { getModule } from 'holocron';
import Fastify from 'fastify';
import healthCheck, {
getTickDelay,
checkForRootModule,
verifyThresholds,
} from '../../../src/server/middleware/healthCheck';
} from '../../../src/server/plugins/healthCheck';
import { getModuleMapHealth } from '../../../src/server/utils/pollModuleMap';

jest.mock('holocron', () => ({
Expand All @@ -41,12 +42,6 @@ jest.mock('../../../src/server/utils/pollModuleMap', () => ({

describe('healthCheck', () => {
const { hrtime } = process;
const req = {};
const res = {
status: jest.fn(),
json: jest.fn(),
sendStatus: jest.fn(),
};

beforeEach(() => {
jest.clearAllMocks();
Expand Down Expand Up @@ -145,61 +140,119 @@ describe('healthCheck', () => {
});
});

describe('middleware', () => {
describe('plugin', () => {
const buildFastifyAndMakeRequest = async () => {
const fastify = Fastify();

await fastify.register(healthCheck);

fastify.get('/im-up', async (_request, reply) => {
await reply.healthReport();
});

await fastify.ready();

const response = await fastify.inject({
method: 'GET',
url: '/im-up',
});

return response;
};

it('should return a 200 when all is good', async () => {
pidusage.mockImplementationOnce((pid, cb) => cb(undefined, {
cpu: 80,
memory: 1.4e9,
}));
})
);
getModule.mockReturnValueOnce(() => 0);
getModuleMapHealth.mockReturnValueOnce(true);
await healthCheck(req, res);
expect(res.sendStatus).not.toHaveBeenCalled();
expect(res.status).toHaveBeenCalledTimes(1);
expect(res.status).toHaveBeenCalledWith(200);
expect(res.json).toHaveBeenCalledTimes(1);
expect(res.json.mock.calls[0][0]).toMatchSnapshot();
const response = await buildFastifyAndMakeRequest();
expect(response.statusCode).toBe(200);
expect(await response.json()).toMatchInlineSnapshot(`
Object {
"holocron": Object {
"moduleMapHealthy": true,
"rootModuleExists": true,
},
"process": Object {
"cpu": 80,
"memory": 1400000000,
"tickDelay": Array [
0,
100,
],
},
}
`);
});

it('should return a 207 when the module map is not healthy', async () => {
pidusage.mockImplementationOnce((pid, cb) => cb(undefined, {
cpu: 80,
memory: 1.4e9,
}));
})
);
getModule.mockReturnValueOnce(() => 0);
getModuleMapHealth.mockReturnValueOnce(false);
await healthCheck(req, res);
expect(res.sendStatus).not.toHaveBeenCalled();
expect(res.status).toHaveBeenCalledTimes(1);
expect(res.status).toHaveBeenCalledWith(207);
expect(res.json).toHaveBeenCalledTimes(1);
expect(res.json.mock.calls[0][0]).toMatchSnapshot();
const response = await buildFastifyAndMakeRequest();
expect(response.statusCode).toBe(207);
expect(await response.json()).toMatchInlineSnapshot(`
Object {
"holocron": Object {
"moduleMapHealthy": false,
"rootModuleExists": true,
"status": 500,
},
"process": Object {
"cpu": 80,
"memory": 1400000000,
"status": 200,
"tickDelay": Array [
0,
100,
],
},
}
`);
});

it('should return a 503 when any threshold has been passed', async () => {
pidusage.mockImplementationOnce((pid, cb) => cb(undefined, {
cpu: 80.1,
memory: 1.4e9,
}));
})
);
getModule.mockReturnValueOnce(() => 0);
getModuleMapHealth.mockReturnValueOnce(true);
await healthCheck(req, res);
expect(res.sendStatus).not.toHaveBeenCalled();
expect(res.status).toHaveBeenCalledTimes(1);
expect(res.status).toHaveBeenCalledWith(503);
expect(res.json).toHaveBeenCalledTimes(1);
expect(res.json.mock.calls[0][0]).toMatchSnapshot();
const response = await buildFastifyAndMakeRequest();
expect(response.statusCode).toBe(503);
expect(await response.json()).toMatchInlineSnapshot(`
Object {
"holocron": Object {
"moduleMapHealthy": true,
"rootModuleExists": true,
},
"process": Object {
"cpu": 80.1,
"memory": 1400000000,
"tickDelay": Array [
0,
100,
],
},
}
`);
});

it('should return a 500 if it can\'t get the stats', async () => {
it("should return a 500 if it can't get the stats", async () => {
pidusage.mockImplementationOnce((pid, cb) => cb(new Error('no stats')));
getModule.mockReturnValueOnce(() => 0);
getModuleMapHealth.mockReturnValueOnce(true);
await healthCheck(req, res);
expect(res.status).not.toHaveBeenCalled();
expect(res.json).not.toHaveBeenCalled();
expect(res.sendStatus).toHaveBeenCalledTimes(1);
expect(res.sendStatus).toHaveBeenCalledWith(500);
const response = await buildFastifyAndMakeRequest();
expect(response.statusCode).toBe(500);
expect(response.body).toMatchInlineSnapshot('""');
});
});
});
2 changes: 1 addition & 1 deletion __tests__/server/ssrServer.spec.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2019 American Express Travel Related Services Company, Inc.
* Copyright 2022 American Express Travel Related Services Company, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down
Loading

0 comments on commit a0fc9ed

Please sign in to comment.