Skip to content

Commit 84ab0c0

Browse files
authored
feat(api-health): Added /health endpoint to check Wildduck API health during runtime ZMS-120 (#607)
* add health api endpoint to check health of API * fixes and add graylog logging * round timestamp, cast to string. Use mongodb ping instead of topology.isConnected check * add timeout to redis commands so that the health api endpoint will return a value
1 parent 01b22ec commit 84ab0c0

File tree

2 files changed

+131
-0
lines changed

2 files changed

+131
-0
lines changed

api.js

+2
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ const dkimRoutes = require('./lib/api/dkim');
4747
const certsRoutes = require('./lib/api/certs');
4848
const webhooksRoutes = require('./lib/api/webhooks');
4949
const settingsRoutes = require('./lib/api/settings');
50+
const healthRoutes = require('./lib/api/health');
5051
const { SettingsHandler } = require('./lib/settings-handler');
5152

5253
let userHandler;
@@ -561,6 +562,7 @@ module.exports = done => {
561562
certsRoutes(db, server);
562563
webhooksRoutes(db, server);
563564
settingsRoutes(db, server, settingsHandler);
565+
healthRoutes(db, server, loggelf);
564566

565567
if (process.env.NODE_ENV === 'test') {
566568
server.get(

lib/api/health.js

+129
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
'use strict';
2+
3+
const Joi = require('joi');
4+
const tools = require('../tools');
5+
const { successRes } = require('../schemas/response/general-schemas');
6+
7+
module.exports = (db, server, loggelf) => {
8+
server.get(
9+
{
10+
path: '/health',
11+
summary: 'Check the health of the API',
12+
description: 'Check the status of the WildDuck API service, that is if db is connected and readable/writable, same for redis.',
13+
tags: ['Health'],
14+
validationObjs: {
15+
requestBody: {},
16+
queryParams: {},
17+
pathParams: {},
18+
response: {
19+
200: {
20+
description: 'Success',
21+
model: Joi.object({ success: successRes })
22+
},
23+
500: {
24+
description: 'Failed',
25+
model: Joi.object({ success: successRes, message: Joi.string().required().description('Error message specifying what went wrong') })
26+
}
27+
}
28+
}
29+
},
30+
tools.responseWrapper(async (req, res) => {
31+
res.charSet('utf-8');
32+
33+
const currentTimestamp = Math.round(Date.now() / 1000).toString();
34+
35+
// 1) test that mongoDb is up
36+
try {
37+
const pingResult = await db.database.command({ ping: 1 });
38+
39+
if (!pingResult.ok) {
40+
res.status(500);
41+
return res.json({
42+
success: false,
43+
message: 'DB is down'
44+
});
45+
}
46+
} catch (err) {
47+
loggelf({
48+
short_message: '[HEALTH] MongoDb is down. MongoDb is not connected. PING not ok'
49+
});
50+
51+
res.status(500);
52+
return res.json({
53+
success: false,
54+
message: 'DB is down'
55+
});
56+
}
57+
58+
// 2) test that mongoDb is writeable
59+
60+
try {
61+
const insertData = await db.database.collection('health').insertOne({ [`${currentTimestamp}`]: 'testWrite' });
62+
await db.database.collection('health').deleteOne({ _id: insertData.insertedId });
63+
} catch (err) {
64+
loggelf({
65+
short_message:
66+
'[HEALTH] could not write to MongoDb. MongoDB is not writeable, cannot write document to collection `health` and delete the document at that path.'
67+
});
68+
69+
res.status(500);
70+
return res.json({
71+
success: false,
72+
message: 'Could not write to DB'
73+
});
74+
}
75+
76+
// 3) test redis PING
77+
try {
78+
// Redis might try to reconnect causing a situation where given ping() command might never return a value, add a fixed timeout
79+
await promiseRaceTimeoutWrapper(db.redis.ping(), 10000);
80+
} catch (err) {
81+
loggelf({
82+
short_message: '[HEALTH] Redis is down. PING to Redis failed.'
83+
});
84+
85+
res.status(500);
86+
return res.json({
87+
success: false,
88+
message: 'Redis is down'
89+
});
90+
}
91+
92+
// 4) test if redis is writeable
93+
try {
94+
await promiseRaceTimeoutWrapper(db.redis.hset('health', `${currentTimestamp}`, `${currentTimestamp}`), 10000);
95+
96+
const data = await promiseRaceTimeoutWrapper(db.redis.hget(`health`, `${currentTimestamp}`), 10000);
97+
98+
if (data !== `${currentTimestamp}`) {
99+
throw Error('Received data is not the same!');
100+
}
101+
102+
await promiseRaceTimeoutWrapper(db.redis.hdel('health', `${currentTimestamp}`), 10000);
103+
} catch (err) {
104+
loggelf({
105+
short_message:
106+
'[HEALTH] Redis is not writeable/readable. Could not set hashkey `health` in redis, failed to get the key and/or delete the key.'
107+
});
108+
109+
res.status(500);
110+
return res.json({
111+
success: false,
112+
message: 'Redis is not writeable/readable'
113+
});
114+
}
115+
116+
res.status(200);
117+
return res.json({ success: true });
118+
})
119+
);
120+
};
121+
122+
async function promiseRaceTimeoutWrapper(promise, timeout) {
123+
return Promise.race([
124+
promise,
125+
new Promise((_resolve, reject) => {
126+
setTimeout(() => reject(new Error('Async call timed out!')), timeout);
127+
})
128+
]);
129+
}

0 commit comments

Comments
 (0)