Наше приложение Promyze — это стандартное веб-приложение с фронтендом ReactJS и стеком Node/Express/Mongoose/MongoDB для нашего API. Недавно мы задумались о создании публичной страницы состояния для наших пользователей, чтобы они знали, работают ли наши сервисы или столкнулись с проблемами. На рынке существует множество инструментов мониторинга. В нашем случае мы выбрали MonSpark, поскольку он довольно прост в использовании и отвечает нашим требованиям: интеграция со Slack и публичные и приватные страницы статуса (для наших внутренних команд). Мы рассмотрим конфигурацию MonSpark в одном из следующих постов, а пока сосредоточимся на настройке конечной точки API HealthCheck.
NB: Мы не утверждаем, что это правильный способ сделать это. Существует множество реализаций, и та, которую мы представляем здесь, может иметь некоторые недостатки: мы просто делимся своими мыслями 😉
Зачем нужен этот мониторинг и что мониторить?
Мониторинг имеет решающее значение в разработке программного обеспечения, и, к сожалению, я думаю, что многие команды не инвестируют в эту тему. Если в вашей системе произошел серьезный сбой или некоторые сервисы не работают, мы должны быть первыми, кто это заметит: не наши клиенты. Более того, настроить мониторинг сегодня довольно просто благодаря множеству существующих инструментов.
В нашем контексте мы считаем, что наш API работает, если:
- Наш сервер нод запущен
- Экспресс-фреймворк запущен
- Наша база данных доступна и может быть запрошена.
Поэтому нам нужна конечная точка, которая удовлетворяет этим требованиям. Может случиться так, что сервер express запущен, открывая ваш API, но соединение с базой данных не работает. Поэтому нам нужна полная картина, чтобы убедиться, что с API все в порядке.
Как проводить мониторинг?
Я читал много статей в блогах, в которых предлагается такое решение, которое отлично работает:
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;
Нам не хватало части, связанной с базой данных. На примере корневой точки мы решили возвращать код 200, только если мы сможем запросить коллекцию MongoDB и найти в ней 1 элемент. Вот и все.
В принципе, реализация выглядит следующим образом, обратите внимание, что мы не добавили полный код, но вы легко поймете логику.
// 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();
}
});
Обратите внимание, что вызов «findOneAndUpdate» используется для создания первого элемента в коллекции. Очевидно, что это можно было бы поместить в один файл, тем более что логика здесь очень проста. Но мы стараемся сохранить шестиугольную архитектуру в нашем приложении, так что да, у нас очень маленький шестиугольник для HealthCheck! 🙂 .
Влияние на базу данных?
Мы можем подумать, что выполнение «бесполезных» запросов может перегрузить базу данных. Честно говоря, если мы не можем позволить себе этот простой запрос на выделенной коллекции, один раз в минуту… Я думаю, у нас есть более серьезные проблемы, которые нужно решить в первую очередь! Мы можем даже пойти дальше и запросить некоторые реальные бизнес-данные.
Время ответа конечной точки HealthCheck также будет полезно для обнаружения проблем с нашей базой данных в случае, если соединение имеет проблемы с медлительностью. Мы можем настроить наш инструмент мониторинга на параметры таймаута, чтобы получать уведомление, если время ответа превышает, например, 10 секунд.
Добавьте уровень безопасности
В зависимости от того, как вы развернули свое приложение, ваша конечная точка может быть публичной или нет. Под публичной я подразумеваю, что кто-то вроде меня может пинговать вашу конечную точку. Даже если эта конечная точка не должна быть указана на вашем сайте, кто-то все равно может знать о ее существовании и проводить атаки на нее. Существует несколько стратегий, одна из них — добавление закрытого ключа в качестве заголовка.
В нашем контексте мы добавляем заголовок под названием код 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;
}
Конечно, этот подход может быть адаптирован таким же образом для SQL-движка или любой другой базы данных.
На этом все, и не стесняйтесь делиться с нами своими методами и советами 🙂