Skip to content

Commit

Permalink
Merge pull request #21 from nhsuk/healthcheck
Browse files Browse the repository at this point in the history
Healthcheck
  • Loading branch information
prembonsall authored Jun 1, 2021
2 parents 7c6ecc3 + 8f20db7 commit 913a521
Show file tree
Hide file tree
Showing 9 changed files with 143 additions and 49 deletions.
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,14 @@ Free-text user comments.
}
```

### Healthcheck

Test the health of the function. Useful for monitoring.

Returns a 503 response if database is unreachable. 200 OK otherwise.

`GET /healthcheck/`

## Contributing

### Tests
Expand Down
4 changes: 2 additions & 2 deletions comments/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
const { handleRequest, requestTypes } = require('../lib/index.js');
const { handleComments } = require('../lib/index.js');

module.exports = (context, req) => handleRequest(context, req, requestTypes.COMMENTS);
module.exports = (context, req) => handleComments(context, req);
18 changes: 18 additions & 0 deletions healthcheck/function.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"bindings": [
{
"authLevel": "anonymous",
"type": "httpTrigger",
"direction": "in",
"name": "req",
"methods": [
"get"
]
},
{
"type": "http",
"direction": "out",
"name": "$return"
}
]
}
3 changes: 3 additions & 0 deletions healthcheck/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const { handleHealthcheck } = require('../lib/index.js');

module.exports = async (context, req) => handleHealthcheck(context, req);
15 changes: 15 additions & 0 deletions lib/database.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,21 @@ const COLLECTION_NAME = 'feedback';

const { validateInitialResponse, validateTextComments } = require('./validation.js');

/**
* @returns {Promise} - A promise which returns true for healthy connection, false for unhealthy.
*/
module.exports.healthcheck = async () => {
try {
const client = await mongoClient.connect(CONNECTION_STRING);
const db = client.db(DB_NAME);
const result = await db.admin().ping();
return result.ok === 1;
} catch (err) {
console.error(err); // eslint-disable-line no-console
return false;
}
};

/**
* @param {Object} data - Data object to be saved into the database
* @returns {Promise} - A promise which resolves to a unique token
Expand Down
69 changes: 37 additions & 32 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,48 +2,53 @@ const database = require('./database.js');
const { HttpError } = require('./validation.js');
const responses = require('./responses.js');

const requestTypes = {
COMMENTS: 'COMMENTS',
SATISFIED: 'SATISFIED',
const handleError = (err) => {
// only return error message in response if it is a HttpError
if (err instanceof HttpError) {
return responses.httpError(err.statusCode, err.message);
}
console.error('500 ERROR:', err.message); // eslint-disable-line no-console
return responses.error500();
};

module.exports.requestTypes = requestTypes;
module.exports.handleHealthcheck = async () => {
const healthy = await database.healthcheck();
if (!healthy) {
return responses.httpError(503, 'Service unavailable');
}

return responses.healthcheckOk();
};

/**
* @param {Object} context - See azure function context documentation
* @param {Object} req - See azure function request documentation
* @param {string} requestType - requestTypes enum value representing which type of request this is
* @returns {Object} response object
*/
module.exports.handleRequest = async (context, req, requestType) => {
module.exports.handleSatisfied = async (context, req) => {
if (!req.body) {
return responses.nodata();
}

let token = req.body.token || null;

try {
if (requestType === requestTypes.SATISFIED) {
token = await database.saveInitialResponse({
isSatisfied: req.body.isSatisfied,
token,
url: req.body.url,
});
}
if (requestType === requestTypes.COMMENTS) {
token = await database.saveTextComments({
comments: req.body.comments,
token,
});
}
token = await database.saveInitialResponse({
isSatisfied: req.body.isSatisfied,
token,
url: req.body.url,
});
} catch (err) {
// only return error message in response if it is a HttpError
if (err instanceof HttpError) {
return responses.httpError(err.statusCode, err.message);
}
context.log('500 ERROR:', err.message);
return responses.error500();
return handleError(err);
}
return responses.ok(token);
};

module.exports.handleComments = async (context, req) => {
if (!req.body) {
return responses.nodata();
}
let token = req.body.token || null;
try {
token = await database.saveTextComments({
comments: req.body.comments,
token,
});
} catch (err) {
return handleError(err);
}
return responses.ok(token);
};
46 changes: 33 additions & 13 deletions lib/index.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* global expect jest */

const { handleRequest, requestTypes } = require('./index.js');
const { handleComments, handleSatisfied, handleHealthcheck } = require('./index.js');
const { ValidationError } = require('./validation.js');
const context = require('../tests/unit/defaultContext.js');

Expand All @@ -16,8 +16,8 @@ describe('http handler for /satisfied/', () => {
const request = {
body: { isSatisfied: true },
};
const response = await handleRequest(context, request, requestTypes.SATISFIED);
expect(response).toEqual({ body: { status: 'ok' } });
const response = await handleSatisfied(context, request);
expect(response).toMatchObject({ body: { status: 'ok' } });
expect(database.saveInitialResponse).toBeCalledWith({ isSatisfied: true, token: null });
});

Expand All @@ -27,8 +27,8 @@ describe('http handler for /satisfied/', () => {
isSatisfied: false,
},
};
const response = await handleRequest(context, request, requestTypes.SATISFIED);
expect(response).toEqual({ body: { status: 'ok' } });
const response = await handleSatisfied(context, request);
expect(response).toMatchObject({ body: { status: 'ok' } });
expect(database.saveInitialResponse).toBeCalledWith({ isSatisfied: false, token: null });
});

Expand All @@ -39,8 +39,8 @@ describe('http handler for /satisfied/', () => {
const request = {
body: { isSatisfied: true },
};
const response = await handleRequest(context, request, requestTypes.SATISFIED);
expect(response).toEqual({ body: { error: 'Something went wrong' }, status: 500 });
const response = await handleSatisfied(context, request);
expect(response).toMatchObject({ body: { error: 'Something went wrong' }, status: 500 });
});

test('returns error when validation fails', async () => {
Expand All @@ -50,14 +50,14 @@ describe('http handler for /satisfied/', () => {
const request = {
body: { isSatisfied: true },
};
const response = await handleRequest(context, request, requestTypes.SATISFIED);
expect(response).toEqual({ body: { error: 'some validation message' }, status: 400 });
const response = await handleSatisfied(context, request);
expect(response).toMatchObject({ body: { error: 'some validation message' }, status: 400 });
});

test('returns error when no body given', async () => {
const request = {};
const response = await handleRequest(context, request, requestTypes.SATISFIED);
expect(response).toEqual({ body: { error: 'Please pass a JSON request body' }, status: 400 });
const response = await handleSatisfied(context, request);
expect(response).toMatchObject({ body: { error: 'Please pass a JSON request body' }, status: 400 });
});
});

Expand All @@ -73,8 +73,28 @@ describe('http handler for /comments/', () => {
token: 'my-token',
},
};
const response = await handleRequest(context, request, requestTypes.COMMENTS);
expect(response).toEqual({ body: { status: 'ok' } });
const response = await handleComments(context, request);
expect(response).toMatchObject({ body: { status: 'ok' } });
expect(database.saveTextComments).toBeCalledWith({ comments: 'Needs more cowbell.', token: 'my-token' });
});
});

describe('http handler for /healthcheck/', () => {
beforeEach(() => {
database.healthcheck.mockClear();
});

test('returns ok when database is available', async () => {
database.healthcheck = jest.fn().mockImplementation(async () => true);
const request = {};
const response = await handleHealthcheck(context, request);
expect(response).toMatchObject({ body: { status: 'ok' } });
});

test('returns 503 when database is unavailable', async () => {
database.healthcheck = jest.fn().mockImplementation(async () => false);
const request = {};
const response = await handleHealthcheck(context, request);
expect(response).toMatchObject({ status: 503 });
});
});
25 changes: 25 additions & 0 deletions lib/responses.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
*
*/

const globalHeaders = {
'Content-Type': 'application/json',
};

/**
* HTTP 200 ok response.
* @param {string} token - UUIDv4 token
Expand All @@ -16,6 +20,9 @@ module.exports.ok = (token) => ({
status: 'ok',
token,
},
headers: {
...globalHeaders,
},
});

/**
Expand All @@ -26,6 +33,9 @@ module.exports.nodata = () => ({
body: {
error: 'Please pass a JSON request body',
},
headers: {
...globalHeaders,
},
status: 400,
});

Expand All @@ -37,6 +47,9 @@ module.exports.error500 = () => ({
body: {
error: 'Something went wrong',
},
headers: {
...globalHeaders,
},
status: 500,
});

Expand All @@ -48,5 +61,17 @@ module.exports.error500 = () => ({
*/
module.exports.httpError = (statusCode, message) => ({
body: { error: message },
headers: {
...globalHeaders,
},
status: statusCode,
});

module.exports.healthcheckOk = () => ({
body: {
status: 'ok',
},
headers: {
...globalHeaders,
},
});
4 changes: 2 additions & 2 deletions satisfied/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
const { handleRequest, requestTypes } = require('../lib/index.js');
const { handleSatisfied } = require('../lib/index.js');

module.exports = async (context, req) => handleRequest(context, req, requestTypes.SATISFIED);
module.exports = async (context, req) => handleSatisfied(context, req);

0 comments on commit 913a521

Please sign in to comment.