Проверяем сервисы.
Понимаем сбои.
Создаём небольшой монитор доступности на NestJS.
Мониторинг доступности на NestJS
Создаём сервис, который регулярно проверяет сайты и сохраняет историю ответов.
Uptime Lab — небольшой учебный backend для наблюдения за несколькими веб-сервисами. Он отправляет HTTPS-запросы по расписанию, измеряет время получения ответа и предоставляет результаты через REST API. В отличие от обычного CRUD, здесь есть фоновая работа, сетевые ошибки и защита от пересечения проверок.
В проекте три цели: главный сайт, документация EDU и документация EST. Адреса настраивает оператор сервиса через переменные окружения. Пользователь HTTP API не может передать произвольный URL для проверки.
01. Запуск проекта
Понадобится Node.js 22 LTS или более новая поддерживаемая LTS-версия. Скачайте архив исходников, распакуйте его и установите зависимости. Для сетевых запросов используется встроенный fetch; отдельный HTTP-клиент не нужен.
node --version
cd uptime-monitor-api
npm install
npm run build
npm start
API слушает 127.0.0.1:3002. Первая серия проверок запускается при инициализации, затем планировщик повторяет её раз в минуту. Если один из поддоменов ещё не настроен, его результат будет неуспешным — остальные цели продолжат проверяться.
curl http://127.0.0.1:3002/api/health
curl http://127.0.0.1:3002/api/monitors
Наличие успешного health-check означает, что процесс отвечает. Оно не означает, что все наблюдаемые сайты доступны. Их состояния смотрите в ответе /api/monitors.
02. Архитектура сервиса
MonitorModule объединяет контроллер и сервис. Контроллер отвечает за просмотр состояния, получение истории и ручной запуск. MonitorService содержит список целей, сетевые проверки и ограниченную историю результатов. ScheduleModule включает выполнение периодических задач.
src/
main.ts
app.module.ts
monitor/
monitor.module.ts
monitor.controller.ts
monitor.service.ts
import { Module, Controller, Get } from '@nestjs/common';
import { ScheduleModule } from '@nestjs/schedule';
import { MonitorModule } from './monitor/monitor.module';
@Controller('health')
class HealthController { @Get() check() { return { status: 'ok', service: 'uptime-monitor' }; } }
@Module({ imports: [ScheduleModule.forRoot(), MonitorModule], controllers: [HealthController] })
export class AppModule {}
Каждый Sample содержит момент проверки в UTC, HTTP-код, длительность, признак успеха и текст ошибки. При сетевой ошибке HTTP-код остаётся null: сервер не успел вернуть заголовки ответа. Это отличается от ситуации, когда сервер ответил 503.
03. Настройка целей
Цели перечислены на стороне сервера, а URL могут быть переопределены окружением. Это ограничивает поверхность API: маршрут ручной проверки запускает только заранее известный набор адресов.
PORT=3002
MAIN_URL=https://nestusefullscripts.com/
EDU_URL=https://edu.nestusefullscripts.com/
EST_URL=https://est.nestusefullscripts.com/
cp .env.example .env
node --env-file=.env dist/main.js
Файл .env не загружается автоматически при npm start. Для него используется показанная команда с --env-file. Без файла сервис работает с адресами по умолчанию. Изменения окружения требуют перезапуска процесса.
Разрешён только HTTPS. Используйте проверенные публичные адреса под своим управлением. Возможность редактировать окружение предполагает доверенного оператора; проверка схемы URL сама по себе не защищает от доступа к внутренней сети.
04. HTTP-проверка и таймаут
Проверка отправляет HEAD-запрос. Он получает заголовки ответа без скачивания тела страницы. Успешными считаются только коды 200–299. Редиректы не выполняются, поэтому в настройках следует указывать конечный HTTPS-адрес.
const response = await fetch(target.url, {
method: 'HEAD',
redirect: 'manual',
signal: AbortSignal.timeout(5000),
});
const ok = response.status >= 200 && response.status < 300;
Timeout ограничивает ожидание запроса. Исключения fetch перехватываются и превращаются в результат проверки, а не завершают всю серию. Promise.all запускает три цели параллельно, поэтому один медленный сайт не блокирует начало проверки остальных.
latencyMs измеряется через performance.now и включает DNS, соединение, TLS и ожидание HTTP-заголовков. Это не ICMP ping и не полное время загрузки страницы. Успешный HEAD также не гарантирует работу JavaScript или всех функций сайта.
05. Расписание и пересечение запусков
Декоратор Interval запускает метод каждые 60 000 миллисекунд. Ручная проверка использует тот же механизм, поэтому обе точки входа должны согласовывать доступ к общей серии запросов.
@Interval(60000)
async scheduledCheck() {
if (this.running) return;
await this.checkAll();
}
async checkAll() {
if (this.running) throw new ConflictException('Check already running');
this.running = true;
try {
await Promise.all(this.targets.map(target => this.probe(target)));
return this.list();
} finally { this.running = false; }
}
Если проверка уже выполняется, фоновый запуск пропускается, а ручной получает 409. Блок finally снимает флаг даже при ошибке. Этот флаг работает только в одном процессе. Для нескольких экземпляров приложения потребуется распределённая блокировка или отдельный worker.
06. История и показатели
После каждой проверки результат добавляется в историю цели. Сервис оставляет последние 100 записей. При интервале в минуту это примерно 100 минут наблюдений, если нет ручных запусков и пропусков.
successPercent — доля успешных проверок среди сохранённых записей. Это выборочная статистика, а не точный процент времени доступности за месяц. При перезапуске история и статистика очищаются.
{
"id": "main",
"name": "Main site",
"sampleCount": 10,
"successPercent": 90,
"latest": {
"checkedAt": "2026-10-01T23:00:00.000Z",
"ok": true,
"statusCode": 200,
"latencyMs": 184,
"error": null
}
}
Для постоянного хранения можно создать таблицы monitors и check_results в PostgreSQL. Индекс по monitor_id и checked_at ускорит выборку истории. Определите срок хранения и удаление старых результатов до роста таблицы.
07. REST API и ручной запуск
API предоставляет три маршрута мониторинга и отдельный health-check. Запрос истории принимает идентификатор из набора main, edu и est. Неизвестный идентификатор возвращает 404.
import { Controller, Get, Param, Post } from '@nestjs/common';
import { MonitorService } from './monitor.service';
@Controller('monitors')
export class MonitorController {
constructor(private readonly monitor: MonitorService) {}
@Get() list() { return this.monitor.list(); }
@Get(':id/history') history(@Param('id') id: string) { return this.monitor.history(id); }
@Post('check') async check() { return this.monitor.checkAll(); }
}
curl http://127.0.0.1:3002/api/monitors
curl http://127.0.0.1:3002/api/monitors/main/history
curl -X POST http://127.0.0.1:3002/api/monitors/check
Ручной запуск ждёт завершения всей серии и возвращает обновлённый список целей. По умолчанию POST в NestJS отвечает 201; в этом учебном API декоратор статуса не переопределён. История возвращается от более старых записей к более новым.
08. Полная реализация сервиса
Ниже весь сервис, чтобы можно было сопоставить конфигурацию, планировщик, сетевую проверку и историю. Контроллер не знает, как отправляется HTTPS-запрос: он вызывает методы провайдера.
import { Injectable, OnModuleInit, NotFoundException, ConflictException } from '@nestjs/common';
import { Interval } from '@nestjs/schedule';
type Sample = { checkedAt: string; ok: boolean; statusCode: number | null; latencyMs: number; error: string | null };
type Target = { id: string; name: string; url: string };
@Injectable()
export class MonitorService implements OnModuleInit {
private readonly targets: Target[] = [
{ id: 'main', name: 'Main site', url: process.env.MAIN_URL ?? 'https://nestusefullscripts.com/' },
{ id: 'edu', name: 'Education docs', url: process.env.EDU_URL ?? 'https://edu.nestusefullscripts.com/' },
{ id: 'est', name: 'Backend docs', url: process.env.EST_URL ?? 'https://est.nestusefullscripts.com/' },
];
private readonly samples = new Map<string, Sample[]>();
private running = false;
async onModuleInit() {
for (const target of this.targets) {
const url = new URL(target.url);
if (url.protocol !== 'https:') throw new Error('Monitor URL must use HTTPS');
}
await this.checkAll();
}
list() {
return this.targets.map(target => {
const history = this.samples.get(target.id) ?? [];
return { ...target, latest: history.at(-1) ?? null,
sampleCount: history.length,
successPercent: history.length ? Math.round(history.filter(s => s.ok).length / history.length * 10000) / 100 : null };
});
}
history(id: string) {
if (!this.targets.some(t => t.id === id)) throw new NotFoundException('Monitor not found');
return this.samples.get(id) ?? [];
}
@Interval(60000)
async scheduledCheck() {
if (this.running) return;
await this.checkAll();
}
async checkAll() {
if (this.running) throw new ConflictException('Check already running');
this.running = true;
try {
await Promise.all(this.targets.map(target => this.probe(target)));
return this.list();
} finally { this.running = false; }
}
private async probe(target: Target) {
const started = performance.now();
let statusCode: number | null = null;
let error: string | null = null;
let ok = false;
try {
const response = await fetch(target.url, {
method: 'HEAD', redirect: 'manual', signal: AbortSignal.timeout(5000),
});
statusCode = response.status;
ok = response.status >= 200 && response.status < 300;
if (!ok) error = `HTTP ${response.status}`;
} catch (cause) { error = cause instanceof Error ? cause.message : 'Connection failed'; }
const sample: Sample = { checkedAt: new Date().toISOString(), ok, statusCode,
latencyMs: Math.round(performance.now() - started), error };
const history = this.samples.get(target.id) ?? [];
history.push(sample);
this.samples.set(target.id, history.slice(-100));
}
}
Для тестирования ошибок временно укажите в окружении доверенный тестовый endpoint, который возвращает 503. Проверка должна сохранить HTTP-код и ok: false. Отдельно проверьте таймаут, неизвестный id и очистку истории после перезапуска.
09. Как развивать мониторинг
Добавьте уведомления только при изменении состояния, а не после каждой неудачной проверки. Несколько последовательных ошибок уменьшают число ложных тревог. Восстановление также должно порождать отдельное событие.
Разделите worker и HTTP API, если число целей растёт. Ограничьте параллелизм запросов, чтобы сотни проверок не запускались одновременно. Очередь задач позволяет управлять нагрузкой и повторными попытками.
Для статус-страницы публикуйте только название сервиса, состояние и агрегаты. Не выводите внутренние URL и полный текст сетевых ошибок. Точный SLA требует правил измерения и учёта интервалов между проверками.
Этот монитор показывает доступность из сети сервера. Чтобы оценить доступность из разных стран или провайдеров, используйте несколько независимых точек наблюдения и сравнивайте их результаты.
Исходники и другие руководства
Скачайте проект мониторинга и запустите его отдельно от сайта документации. В архиве есть исходники TypeScript, настройки компиляции, пример окружения и инструкция.
uptime-monitor-api.zipNestJS · HTTPS checks · scheduler · historyСкачать ZIPДругие проекты: каталог учебных курсов и API управления задачами.
Справочные материалы: NestJS Task Scheduling и Node.js fetch.