Nova Uptime
Гайдыincident-responseescalationon-call

Кастомные email-алерты и эскалации: продвинутая маршрутизация инцидентов

Спроектируйте процессы эскалации, поднимающие нужного человека в нужный момент. Гайд по маршрутизации алертов, on-call интеграциям и политикам эскалации.

SN
Sumit Nova Uptime
28 февраля 2026 г. · 8 min read
Share:

Проблема эскалации

3 часа ночи. Сайт лёг. Нужен кто-то, кто отреагирует немедленно.

Без эскалации:

  • Алерт уходит в on-call Slack-канал
  • Никто не видит (все спят, уведомления Slack выключены)
  • Через 45 минут жалобы клиентов кого-то будят
  • MTTR: 45 минут

С умной эскалацией:

  • Алерт уходит в Slack (низкий приоритет)
  • Если нет подтверждения за 2 минуты → SMS основному дежурному
  • Если нет ответа за 5 минут → звонок резервному
  • MTTR: 5 минут

Этот гайд покажет, как построить умные процессы эскалации.

Понимаем серьёзность инцидента

Сначала классифицируем инциденты по серьёзности:

Уровень 1: критичный (пейдж сразу)#

  • Прод-сайт лежит (бьёт по выручке)
  • Сбой платёжного шлюза
  • API возвращает 5xx
  • Репликация БД лежит

Действия:

  • Slack #critical-incidents (всегда мониторится)
  • SMS основному дежурному
  • Звонок, если нет ответа на SMS
  • Автоматическое создание тикета в Jira
  • Обновление статус-страницы

Уровень 2: warning (пейдж в рабочее время)#

  • Деградация времени отклика
  • Ошибки некритичных сервисов
  • Проблемы доставляемости email
  • Медленные запросы к БД

Действия:

  • Slack #alerts (проверяется в рабочие часы)
  • Создание тикета в Jira
  • Email-дайджест в конце дня

Уровень 3: info (только лог)#

  • Истечение домена через 30 дней
  • Истечение SSL через 90 дней
  • Еженедельный отчёт по трендам
  • Некритичный порог метрики

Действия:

  • Еженедельный email-дайджест
  • Уведомление в дашборде (без пейджа)

Создаём политику эскалации

Шаг 1: задайте on-call ротации#

Создайте расписание, кто на дежурстве и когда:

Пн–Пт 9:00–17:00: Алиса (основной), Боб (резерв)
Пн–Пт 17:00–9:00: Чарли (основной), Диана (резерв)
Сб–Вс 24 ч: Ева (основной), Фрэнк (резерв)
Праздники: Джордж (на дежурстве весь день)

Шаг 2: задайте время эскалации#

T+0:    Алерт идёт в Slack
T+2 мин: Нет подтверждения → SMS основному
T+5 мин: Нет ответа → звонок основному
T+10 мин: Нет ответа → SMS резервному
T+15 мин: Всё ещё нет ответа → пейдж всей команды

Шаг 3: внедрите логику эскалации#

В Nova Uptime (если поддерживается):

  1. Настройки домена → Alerting
  2. Поставьте severity: Critical
  3. Настройте эскалацию:
    • Шаг 1: Slack #critical-incidents
    • Шаг 2 (2 мин): SMS дежурному
    • Шаг 3 (5 мин): звонок
    • Шаг 4 (10 мин): вся команда

Через webhook + кастомную систему:

async function handleCriticalIncident({ domain, detectedAt }) {
  const oncall = await getOnCallEngineer(new Date());

  // Шаг 1: мгновенный Slack-алерт
  const slackMessage = await postToSlack({
    channel: '#critical-incidents',
    text: `🚨 CRITICAL: ${domain} is down`,
    blocks: [
      {
        type: 'section',
        text: {
          type: 'mrkdwn',
          text: `@${oncall.slackHandle}: ${domain} is down. Acknowledge with reaction.`
        }
      }
    ]
  });

  // Шаг 2: ждём 2 минуты подтверждения
  const acknowledged = await waitForAcknowledgment(slackMessage, 2 * 60 * 1000);

  if (!acknowledged) {
    // Шаг 2: отправляем SMS
    await sendSMS({
      to: oncall.phone,
      message: `CRITICAL: ${domain} down. Reply OK to acknowledge.`
    });
  }

  // Шаг 3: ждём всего 5 минут
  const smsAcknowledged = await waitForSMS(oncall.phone, 5 * 60 * 1000);

  if (!smsAcknowledged) {
    // Шаг 3: телефонный звонок
    await makePhoneCall({
      to: oncall.phone,
      message: `Critical incident. Website ${domain} is down. Press 1 to acknowledge.`
    });
  }

  // ... продолжаем цепочку эскалации
}

Продвинуто: умная маршрутизация алертов

Паттерн 1: маршрутизация по времени суток#

Разные люди дежурят в разное время.

async function getOnCallEngineer(timestamp) {
  const hour = new Date(timestamp).getHours();
  const dayOfWeek = new Date(timestamp).getDay();

  // Рабочее время (9:00–17:00 в будни)
  if (dayOfWeek >= 1 && dayOfWeek <= 5 && hour >= 9 && hour < 17) {
    return {
      name: 'Alice',
      slackHandle: 'alice',
      phone: '+1-555-0100',
      email: 'alice@company.com'
    };
  }

  // Вне рабочих часов (будни)
  if (dayOfWeek >= 1 && dayOfWeek <= 5 && (hour < 9 || hour >= 17)) {
    return {
      name: 'Charlie',
      slackHandle: 'charlie',
      phone: '+1-555-0102',
      email: 'charlie@company.com'
    };
  }

  // Выходные
  if (dayOfWeek === 0 || dayOfWeek === 6) {
    return {
      name: 'Eve',
      slackHandle: 'eve',
      phone: '+1-555-0104',
      email: 'eve@company.com'
    };
  }
}

Паттерн 2: маршрутизация по домену#

Разные команды владеют разными доменами.

async function getTeamForDomain(domain) {
  // Engineering владеет api.*, backend.*
  if (domain.startsWith('api.') || domain.startsWith('backend.')) {
    return 'engineering';
  }

  // Infrastructure владеет server, monitoring, infra
  if (domain.includes('server') || domain.includes('monitor')) {
    return 'infrastructure';
  }

  // Support владеет клиент-фейсинг доменами
  if (domain.startsWith('support.') || domain.startsWith('customer.')) {
    return 'support';
  }

  // По умолчанию: DevOps
  return 'devops';
}

async function handleIncident({ domain, severity }) {
  const team = await getTeamForDomain(domain);
  const oncall = await getOnCallEngineer(team, new Date());

  if (severity === 'critical') {
    await escalate(oncall);
  }
}

Паттерн 3: маршрутизация по типу инцидента#

Разная экспертиза для разных сбоев.

async function getExpertForIncident(domain, incidentType) {
  if (incidentType === 'database_down') {
    // Эксперта по БД
    return await getOnCallExpert('database');
  } else if (incidentType === 'api_errors') {
    // Лида по API
    return await getOnCallExpert('backend');
  } else if (incidentType === 'email_delivery_failing') {
    // Email ops
    return await getOnCallExpert('email');
  } else if (incidentType === 'ssl_expired') {
    // Security
    return await getOnCallExpert('security');
  }

  // По умолчанию: дежурный
  return await getOnCallEngineer(new Date());
}

Паттерн 4: условная эскалация#

Разные пути эскалации на основе свойств инцидента.

async function escalateIncident({ domain, severity, duration }) {
  const oncall = await getOnCallEngineer(new Date());

  if (severity === 'critical' && duration > 5 * 60 * 1000) {
    // Critical >5 минут: агрессивная эскалация
    await Promise.all([
      postSlack({ channel: '#critical-incidents', text: 'CRITICAL ESCALATION' }),
      sendSMS(oncall.phone),
      makePhoneCall(oncall.phone),
      pageBackup(oncall.backup)
    ]);
  } else if (severity === 'critical') {
    // Critical, но недавно: мягкая эскалация
    await Promise.all([
      postSlack({ channel: '#critical-incidents' }),
      sendSMS(oncall.phone)
    ]);
  } else if (severity === 'warning') {
    // Просто лог
    await postSlack({ channel: '#alerts' });
  }
}

Интеграция с PagerDuty#

Для серьёзного управления on-call интегрируйтесь с PagerDuty:

const PagerDutyClient = require('pagerduty');

async function pageOnCallVia PagerDuty(domain, severity) {
  const client = new PagerDutyClient({
    token: process.env.PAGERDUTY_TOKEN
  });

  // Создание инцидента
  const incident = await client.incidents.create({
    type: 'incident_reference',
    incident: {
      type: 'incident',
      title: `${domain} is down`,
      body: {
        type: 'incident_body',
        description: `Website ${domain} is down. Response: Down. Severity: ${severity}`
      },
      urgency: severity === 'critical' ? 'high' : 'low',
      service: {
        id: process.env.PAGERDUTY_SERVICE_ID,
        type: 'service_reference'
      }
    }
  });

  console.log(`Created PagerDuty incident: ${incident.id}`);
}

Подтверждение и handoff#

Паттерны подтверждения

Дежурный должен подтвердить инцидент:

// Через реакцию в Slack
async function waitForSlackAcknowledgment(messageId, maxWait) {
  const startTime = Date.now();

  while (Date.now() - startTime < maxWait) {
    // Опрашиваем реакции
    const reactions = await getSlackMessageReactions(messageId);

    if (reactions.includes('white_check_mark')) {
      return true; // Подтверждено
    }

    await sleep(10 * 1000); // Проверяем каждые 10 секунд
  }

  return false; // Не подтверждено за лимит ожидания
}

// Через SMS
async function waitForSmsAcknowledgment(phone, maxWait) {
  const startTime = Date.now();

  while (Date.now() - startTime < maxWait) {
    // Опрашиваем SMS-ответы
    const responses = await getSmsResponses(phone);

    if (responses.some(m => m.text.toUpperCase().includes('OK'))) {
      return true; // Подтверждено
    }

    await sleep(5 * 1000); // Каждые 5 секунд
  }

  return false; // Не подтверждено
}

Handoff между командами#

Когда один дежурный передаёт другому:

async function handoffIncident(incident, fromEngineer, toEngineer) {
  // Обновляем инцидент
  incident.assignedTo = toEngineer;
  incident.handoffAt = new Date();
  await incident.save();

  // Уведомляем обоих
  await sendMessage({
    to: fromEngineer.slack,
    text: `Handing off ${incident.domain} to ${toEngineer.name}`
  });

  await sendMessage({
    to: toEngineer.slack,
    text: `Taking over incident: ${incident.domain}. See: ${incident.dashboard}`
  });

  // Обновляем статус-страницу
  await updateStatusPage({
    message: `Working with ${toEngineer.name}'s team on investigation`
  });
}

Измеряем эффективность эскалации

Отслеживайте метрики:

async function analyzeEscalationMetrics() {
  const incidents = await Incident.find({
    createdAfter: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000) // последние 30 дней
  });

  const metrics = {
    totalIncidents: incidents.length,
    avgTimeToAcknowledgment: calculateAvg(
      incidents.map(i => i.acknowledgedAt - i.alertedAt)
    ),
    avgTimeToEscalation: calculateAvg(
      incidents.map(i => i.escalatedAt - i.alertedAt)
    ),
    escalationRate: incidents.filter(i => i.escalatedAt).length / incidents.length,
    avgMTTR: calculateAvg(
      incidents.map(i => i.resolvedAt - i.alertedAt)
    )
  };

  console.log('Escalation Metrics (Last 30 days):');
  console.log(`Total Incidents: ${metrics.totalIncidents}`);
  console.log(`Avg Time to Acknowledgment: ${metrics.avgTimeToAcknowledgment / 60}m`);
  console.log(`Escalation Rate: ${(metrics.escalationRate * 100).toFixed(1)}%`);
  console.log(`Avg MTTR: ${metrics.avgMTTR / 60}m`);
}

Частые ошибки эскалации

Ошибка 1: Alert fatigue ведёт к игнору#

Проблема: слишком много алертов → команда перестаёт реагировать → реальные проблемы пропускаются

Решение: жёсткие пороги. Пейджите только за реально критичное.

Ошибка 2: слишком агрессивная эскалация#

Проблема: пейдж всей команды на каждый инцидент → выгорание → люди уходят

Решение: эскалируйте постепенно. Дайте основному 5 минут до пейджа резерва.

Ошибка 3: слишком медленная эскалация#

Проблема: критичный инцидент висит без внимания 30 минут → большой урон

Решение: для критичных инцидентов эскалируйте за 2–5 минут.

Ошибка 4: нет процесса handoff#

Проблема: основной дежурный не знал, что ему передают → хаос

Решение: явно сообщайте о передаче через Slack/email.

Резюме: чек-лист настройки эскалации

  • ✅ Определите уровни серьёзности (Critical/Warning/Info)
  • ✅ Создайте расписание on-call ротации
  • ✅ Задайте время эскалации (2 мин → 5 мин → 10 мин → все)
  • ✅ Настройте каналы эскалации (Slack → SMS → звонок → all-hands)
  • ✅ Настройте маршрутизацию по времени суток
  • ✅ Маршрутизация по владению доменом/команде
  • ✅ Маршрутизация по типу инцидента (БД vs API vs email)
  • ✅ Интеграция с PagerDuty (если применимо)
  • ✅ Механизмы подтверждения (реакция в Slack, ответ на SMS)
  • ✅ Процедуры handoff
  • ✅ Отслеживание метрик эскалации
  • ✅ Ежемесячный обзор эффективности

Начните сегодня

Начните просто: только Slack + SMS. Добавляйте сложность (PagerDuty, условную маршрутизацию) по мере роста объёма инцидентов.

Зафиксируйте политику эскалации в командной wiki. Поделитесь со всеми. Тестируйте раз в квартал, чтобы всё продолжало работать.

Политика эскалации — это разница между «инцидент закрыт за 5 минут» и «outage длился 3 часа». Вкладывайтесь в то, чтобы это было сделано правильно.

Monitor Your Website Before It Goes Down

Get uptime monitoring, SSL tracking, domain expiry alerts, and email health checks. Free plan — no credit card required.

Start Monitoring Free

Похожие статьи