Our application Packmind is a standard Web application with a ReactJS frontend and a Node/Express/Mongoose/MongoDB stack for our API. We were recently in the reflection of building a public status page for our users to let them know whether our services were up or facing troubles. There are many monitoring tools on the market. In our case, we chose MonSpark, as it’s quite simple to use and meets our requirements: integration with Slack and public and private status pages (for our internal teams). We’ll cover the configuration of MonSpark in a later post, but so far, we focus on setting an API HealthCheck endpoint.
NB: We do not pretend this is the right way to do it. There are plenty of implementations, the one we present here may have some flaws: we just share our thoughts ;)
Why this monitoring and what to monitor?
Monitoring is crucial in software development, and unfortunately, I think that many teams don’t invest in that topic. If your system has a major outage or some services are down, we should be the first one to observe that: not our customers. Moreover, setting monitoring is quite easy today with the number of existing tools.
In our context, we consider that our API is up if:
- Our node server is running
- The express framework has started
- Our database is available and can be queried
So we wanted an endpoint that fills those requirements. It might happen that the express server gets started, exposing your API, but the database connection is not working. So we need the whole picture to make sure the API is fine.
How to monitor?
I’ve read many blog posts that suggest this kind of solution that works fine:
const express = require("express");
const router = express.Router({});
router.get('/healthcheck', async (_req, res, _next) => {
res.status(200).send({'message':'OK');
});
// export router with all routes included
module.exports = router;
We were missing the database part. Using this example of a root point, we chose to return a 200 code only if we could query a MongoDB collection and find 1 element in it. That’s it.
Basically, the implementation looks like this, please note we didn’t add the full code, but you’ll easily understand the logic.
// Healtcheck.ts
export class HealthCheck {
constructor(public event: string) {}
}
// HealthCheckMongo.ts
const HealthCheckSchema = new mongoose.Schema(
{
event: String,
},
{
collection: 'HealthCheck',
minimize: false,
},
);
export default mongoose.model('HealthCheck', HealthCheckSchema);
// HealtcheckRepositoryMongo.ts
async getOrCreate(): Promise<HealthCheck> {
const data = await this.model.findOneAndUpdate({"event" : "check"},
{"event" : "check"}, {
new: true,
upsert: true,
});
return data;
}
//server.ts
router.get('/healthcheck', async (_req, res, _next) => {
try {
const healthCheckData: HealthCheck = await this._healthCheckRepo.getOrCreate();
const isUp: boolean = healthCheckData !== undefined;
if (isUp) {
res.status(200).end();
} else {
res.status(502).end();
}
} catch(error) {
res.status(502).end();
}
});
Note that the call “findOneAndUpdate” is used to create the first element in the collection. You could clearly put this in a single file, especially because the logic is very straightforward here. But we try to keep our hexagonal architecture consistent in our application, so yes, we have a very little hexagon for HealthCheck! 🙂
Impact on database?
We could think that executing “useless” queries can overwhelm the database. Honestly, if we can’t afford this simple query on a dedicated collection, once per minute… I think we’ve got bigger problems to solve first! We could even go further and query some real business data.
The response time of the HealthCheck endpoint will also be useful to detect issues with our database in case the connection has slowness issues. We can tune our monitoring tool to adjust the timeout settings, to be notified if the answer time goes over 10 seconds for instance.
Add a security layer
Depending on how you’ve deployed your application, your endpoint might be public or not. By public, I mean that someone like me could ping your endpoint. Even though this endpoint is not supposed to be listed on your website, someone could still be aware of its existence and runs attacks on it. Several strategies exist, one of them is to add a private key as a header.
In our context, we add a header called code PRIVATE_AUTH_HEADER_KEY:
router.get('/', privateKeyMiddleware, async (_req, res, _next) => {
res.status(200).send({'message':'OK');
});
function privateAuthMiddleware(req: Request, res: Response, next: NextFunction) {
const key = req.headers[PRIVATE_AUTH_HEADER_KEY];
if (key && key === getPrivateAuthKey()) {
return next();
}
return res.sendStatus(401);
}
function getPrivateAuthKey(): string {
return process.env.PRIVATE_AUTH_KEY || PRIVATE_AUTH_KEY.default;
}
Of course, this approach can be adapted in the same way for a SQL engine or any other database.
That’s it, and feel free to share with us your methods and tips :)
Top comments (0)