NNest Useful ScriptsDOCS
ФОНОВЫЕ ЗАДАЧИ · СЕТЬ · НАБЛЮДЕНИЕ

Проверяем сервисы.
Понимаем сбои.

Создаём небольшой монитор доступности на NestJS.

Мониторинг доступности на NestJS

Создаём сервис, который регулярно проверяет сайты и сохраняет историю ответов.

Uptime Lab — небольшой учебный backend для наблюдения за несколькими веб-сервисами. Он отправляет HTTPS-запросы по расписанию, измеряет время получения ответа и предоставляет результаты через REST API. В отличие от обычного CRUD, здесь есть фоновая работа, сетевые ошибки и защита от пересечения проверок.

В проекте три цели: главный сайт, документация EDU и документация EST. Адреса настраивает оператор сервиса через переменные окружения. Пользователь HTTP API не может передать произвольный URL для проверки.

60 секундИнтервал проверок
5 секундТаймаут каждой цели
100 записейИстория каждой цели
Планировщик запускает проверки HTTPS, результаты сохраняются в истории и доступны через REST API
Это документация, а не действующая статус-страница. Реальные проверки выполняются только после отдельного запуска скачиваемого backend.

01. Запуск проекта

Понадобится Node.js 22 LTS или более новая поддерживаемая LTS-версия. Скачайте архив исходников, распакуйте его и установите зависимости. Для сетевых запросов используется встроенный fetch; отдельный HTTP-клиент не нужен.

Terminal
node --version
cd uptime-monitor-api
npm install
npm run build
npm start

API слушает 127.0.0.1:3002. Первая серия проверок запускается при инициализации, затем планировщик повторяет её раз в минуту. Если один из поддоменов ещё не настроен, его результат будет неуспешным — остальные цели продолжат проверяться.

Terminal
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
app.module.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: маршрут ручной проверки запускает только заранее известный набор адресов.

.env.example
PORT=3002
MAIN_URL=https://nestusefullscripts.com/
EDU_URL=https://edu.nestusefullscripts.com/
EST_URL=https://est.nestusefullscripts.com/
Terminal
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
  }
}
Классификация результатов: 2xx означает успех, остальные HTTP-коды и сетевые ошибки означают неуспех

Для постоянного хранения можно создать таблицы monitors и check_results в PostgreSQL. Индекс по monitor_id и checked_at ускорит выборку истории. Определите срок хранения и удаление старых результатов до роста таблицы.

07. REST API и ручной запуск

API предоставляет три маршрута мониторинга и отдельный health-check. Запрос истории принимает идентификатор из набора main, edu и est. Неизвестный идентификатор возвращает 404.

monitor.controller.ts
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(); }
}
Terminal
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 декоратор статуса не переопределён. История возвращается от более старых записей к более новым.

В примере нет авторизации. Перед публикацией защитите ручной запуск и доступ к результатам. Для локального изучения сервис привязан к 127.0.0.1.

08. Полная реализация сервиса

Ниже весь сервис, чтобы можно было сопоставить конфигурацию, планировщик, сетевую проверку и историю. Контроллер не знает, как отправляется HTTPS-запрос: он вызывает методы провайдера.

monitor.service.ts
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.