Кастомные email-алерты и эскалации: продвинутая маршрутизация инцидентов
Спроектируйте процессы эскалации, поднимающие нужного человека в нужный момент. Гайд по маршрутизации алертов, on-call интеграциям и политикам эскалации.
Проблема эскалации
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 (если поддерживается):
- Настройки домена → Alerting
- Поставьте severity: Critical
- Настройте эскалацию:
- Шаг 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Похожие статьи
Вебхуки и интеграции uptime-мониторинга: соберите свои workflow
Как подключить uptime-мониторинг к вашим системам через вебхуки. Полный гид по автоматизации инцидентов, кастомным уведомлениям и шаблонам интеграций.
Кейс: как мониторинг доступности спас $500K потерянной выручки
Реальный пример того, как проактивный uptime-мониторинг предотвратил катастрофические последствия для бизнеса. Учимся на истории реакции SaaS-компании на.
Как интегрировать мониторинг uptime со Slack: гайд по алертам в реальном времени
Настройте Slack-алерты для простоев сайта за 10 минут. Маршрутизируйте инциденты в #alerts и сократите время реакции с 30 минут до 60 секунд.