Алертинг по логам нужен там, где метрики фиксируют факт проблемы, но не объясняют её причину. Рост 5xx в Prometheus говорит, что API деградировал, а запись с level=error и текстом исключения называет эндпоинт, тип ошибки и trace_id. Логи закрывают три задачи, до которых числовые ряды не достают: отдельные исключения, ошибки бизнес-логики и подозрительные паттерны в тексте событий.
Если логи уже лежат в централизованном хранилище (Graylog, Elasticsearch, Grafana Loki), стоимость алертинга почти нулевая: тот же индекс, те же поля, добавляются правило срабатывания и канал доставки. Централизованный сбор настраивают один раз, дальше из него получают проактивные уведомления вместо архива для разбора инцидентов постфактум.
Есть три сценария, где без алертинга по логам вы слепы: всплеск 5xx на одном сервисе при стабильном общем RPS; повторяющееся исключение после деплоя, которое видно только в тексте сообщения; серия 401 или 403 с одного IP, похожая на перебор паролей. Ни один из них Prometheus в удобном виде не покажет.
Зачем превращать логи в систему алертинга, а не в архив инцидентов
Что алертинг по логам даёт поверх метрик и APM
Метрики это числа с метками, логи это события с контекстом. Счётчик http_requests_total{status="500"} скажет, что выросла доля ошибок, и не скажет, какое поле запроса пришло пустым. Разница по классам задач:
| Вопрос | Метрики и APM | Логи |
|---|---|---|
| Насколько плохо сейчас | Latency, RPS, доля 5xx, насыщение CPU | Error rate, посчитанный по записям level=error |
| Почему сломалось | Только факт деградации | Тип исключения, стек, параметры запроса |
| Кто и откуда | Метки сервиса и инстанса | IP, user-agent, путь, идентификатор пользователя |
| Единичное событие | Смазывается агрегацией | Отдельная запись с полным контекстом |
| Ошибка бизнес-логики при HTTP 200 | Не видна | level=warn или error с текстом о неверном состоянии |
Логи дополняют метрики, а не подменяют их. Держите алерт на error rate в метриках, а разбор причины автоматизируйте через логи: так дежурный получает и сигнал, и контекст в одном уведомлении.
Минимальные предпосылки: что должно быть настроено до алертов
- Централизованный сбор: Graylog, Elasticsearch или Loki принимают логи со всех узлов.
- Структурированные поля: level, service, timestamp, trace_id, host. Без поля level алертинг по логам вырождается в набор регулярных выражений, которые ломаются от любой правки формата.
- Ретенция: окно хранения покрывает период сравнения. Для базовой линии за 7 дней нужны логи минимум за 7 дней.
- Индексация по полям, по которым фильтруете: level, service, status, remote_addr.
- Канал доставки: SMTP для почты, webhook для Slack и Telegram.
Пример структурированной записи, на которую удобно строить запросы:
{
"timestamp": "2026-09-30T21:00:00Z",
"level": "error",
"service": "api",
"host": "api-03",
"trace_id": "8f3c1a2e",
"status": 500,
"message": "Order <uuid> failed after <n> retries"
}
Если приложение пишет неструктурированный текст в stdout, начните с перевода на JSON. Сделать это проще на уровне библиотеки логирования, чем потом разбирать регулярками десятки форматов сообщений.
Как писать запросы для поиска аномалий и всплесков ошибок
Любой алерт по логам состоит из трёх частей: выборка событий, агрегация по окну и условие. Частая ошибка - писать условие по сырым событиям, например count больше нуля по слову error. Такое правило сработает на первом безобидном сообщении. Разберём три группы запросов: всплески, повторяющиеся исключения и подозрительная активность. Шаблоны под EQL и LogQL с интеграцией в Alertmanager собраны в руководстве по алертингу на основе логов для Elasticsearch и Grafana Loki.
Запросы для всплесков ошибок: rate, count_over_time и группировка
В Loki частоту ошибок считают через rate по потоку:
sum(rate({app="api"} |= "level=error" [5m])) by (service)
В Kibana тот же смысл даёт фильтр level:error и агрегация date_histogram с интервалом 1 минута, а в Graylog - поиск level:ERROR с агрегацией count по времени. Окно 5 минут выбрано осознанно: минутное окно даёт рваный график и ложные срабатывания на единичных всплесках, а пять минут сглаживают шум и остаются достаточно быстрыми для реакции дежурного.
Чтобы учесть суточную сезонность, сравнивайте текущее окно с тем же окном сутки назад, а не с предыдущими пятью минутами:
sum(rate({app="api"} |= "level=error" [5m])) by (service)
/
sum(rate({app="api"} |= "level=error" [5m] offset 1d)) by (service)
Группировка by (service) обязательна. Без неё правило считает ошибки по всему кластеру: один упавший сервис из двадцати не перебьёт общий фон, и алерт промолчит. Разбивайте по сервису, а внутри сервиса - по эндпоинту или коду ответа, если трафик большой.
Доля ошибок считается как отношение двух потоков:
sum(rate({app="api"} |= "level=error" [5m]))
/
sum(rate({app="api"} [5m]))
Порог для доли удобнее абсолютного числа: 5% ошибок от запросов означает одно и то же и при 100 RPS, и при 10 000 RPS.
Поиск повторяющихся исключений и топ-N ошибок
Исключения тонут в потоке, если смотреть на них поштучно. Вытаскивайте топ за окно:
topk(10,
sum by (exception_type) (
count_over_time({app="api"} |= "Exception" [1h])
)
)
Если поля exception_type в логах нет, группируйте по нормализованному тексту сообщения. Нормализация нужна потому, что в строку попадают переменные, и каждое сообщение становится уникальным:
was: Order 8f3c1a2e-7b21 failed after 12 retries now: Order <uuid> failed after <n> retries
В Elastic-стеке похожие сообщения схлопывают через дедупликацию по отпечатку: fingerprint-фильтр Logstash создаёт хеш из одного или нескольких полей (по умолчанию message), и этот хеш используется как document ID, чтобы Elasticsearch обновлял существующий документ вместо создания дубликата. Альтернативный приём - генерация уникального ID хешированием набора полей события, например fields: ["event.start_date", "event.data_set", "message"]: при совпадении исходный документ обновляется при индексации. В Graylog группировка событий задаётся через поля события в Event Definition.
Запросы для подозрительной активности: 401/403, 404, аномальные объёмы
Серия неуспешных авторизаций с одного адреса:
sum(count_over_time({app="nginx"} |= " 401 " [5m])) by (remote_addr)
Условие правила: результат больше 20 за окно. Порог подбирайте по своей базовой линии, а не переносите из чужой статьи: для API с мобильными клиентами 20 ошибок авторизации за пять минут с одного IP нормальны при массовом перелогине пользователей.
Всплеск 404 на одном пути:
topk(5,
sum by (path) (count_over_time({app="nginx"} |= " 404 " [10m]))
)
Алертить на каждый 404 бессмысленно: сканеры и боты постоянно перебирают пути. Смотрите на всплеск на одном конкретном пути и на рост числа уникальных путей с одного адреса.
Аномальный объём ответов в байтах:
sum by (remote_addr) (
sum_over_time({app="nginx"} | logfmt | unwrap body_bytes_sent [10m])
)
Пример требует, чтобы nginx писал логи в logfmt или JSON с полем body_bytes_sent. Иначе распарсите нужное поле через pattern или regexp и только потом суммируйте.
Пороговые правила против аномалий по базовой линии: что выбрать
Пороговое правило читается одной строкой: если ошибок больше 100 за 5 минут, шли уведомление. Плюс в простоте и предсказуемости, минус в том, что порог не знает про сезонность и рост трафика. Базовая линия сравнивает текущее значение со статистикой прошлых периодов: если ошибок больше, чем среднее за 7 дней плюс 3 стандартных отклонения, шли уведомление. Такое правило адаптируется само, но требует истории и хуже объясняется дежурному.
| Критерий | Пороговое правило | Базовая линия |
|---|---|---|
| Настройка | Одна константа | Нужна история и выбор окна |
| Сезонность | Не учитывает | Учитывает при сравнении с тем же часом прошлых дней |
| Рост трафика | Порог пересматривают вручную | Подстраивается автоматически |
| Слабые места | С запасом пропускает инциденты, без запаса шумит | При малом числе событий stddev нестабилен |
| Объяснение дежурному | Понятно сразу | Нужен график и значение baseline |
| Когда выбирать | Стабильный трафик, редкие критичные события | Сезонный или растущий трафик, плавающая норма |
Как построить простую базовую линию без ML
Формулы, которой хватает в большинстве случаев:
baseline = avg_over_time(error_rate[7d]) + 3 * stddev_over_time(error_rate[7d])
Здесь error_rate - записанная метрика, то есть recording rule, посчитанный по логам. Loki готового отношения не отдаёт, поэтому либо считайте error rate регулярно и складывайте в Prometheus, либо переносите расчёт в LogQL-метрики и сравнивайте окна через offset.
Z-score даёт то же в явном виде:
z = (current_value - baseline_mean) / baseline_stddev
Правило срабатывает при z больше 3. Для суточной сезонности сравнивайте с тем же часом предыдущих дней, а не со средним за неделю: ночной минимум и вечерний пик дают разные нормы, и общее среднее сместит baseline. Практическое ограничение: при малом числе событий стандартное отклонение нестабильно, поэтому берите окно, где набирается хотя бы сотня событий. На старте базовая линия требует 1-2 недели данных, иначе первые дни будут ложные срабатывания.
Когда порог лучше базовой линии
Если событие в норме не должно происходить вообще, ставьте порог. Потеря подключения к БД, срабатывание OOM-killer, ошибка миграции: любое срабатывание важно, статистика тут не нужна. Редкие события тоже плохо ложатся на базовую линию: чтобы посчитать stddev, нужны данные, а их нет. Простое правило выбора: событие в норме не происходит - порог, событие в норме происходит с разной частотой - базовая линия.
Настройка алертов в Graylog, Kibana и Grafana Loki
Ниже путь для трёх стеков. Названия пунктов меню отличаются между релизами, сверяйтесь со своей версией: точные пути и номера версий в открытой документации приведены не для всех релизов, поэтому ориентируйтесь на свою установку. Общая логика одинакова: запрос, условие, группировка, канал доставки.
Graylog: Event Definitions и уведомления
- Создайте Event Definition: в документации Graylog алерты настраиваются через определение условий события, триггеров и уведомлений.
- Задайте Condition. Filter выбирает события (например, level:ERROR), Aggregation задаёт порог: count больше 100 за 5 минут.
- Заполните поля для группировки: service, host. Эти поля попадут в уведомление и разделят события на независимые алерты.
- Добавьте Notification: Email для почты или HTTP для webhook в Slack.
Пример payload для webhook в Slack:
{
"text": "5xx spike: ${event.source}",
"fields": {"service": "${event.fields.service}"}
}
Graylog не отправит ни одного уведомления, пока у события не настроен Notification. Проверьте, что SMTP прописан в конфигурации сервера, иначе рассылка по почте молча не работает.
Kibana: Rules and Connectors
- Stack Management, затем Rules and Connectors, затем Create rule.
- Выберите тип Log threshold.
- Задайте KQL-запрос, например level:error and service:api, и порог: count больше 100 за 5 минут.
- Создайте Connector: Slack (нужен Incoming Webhook) или Email (нужен SMTP в kibana.yml).
- Привяжите Connector к правилу и сохраните.
Проверить правило можно кнопкой Test: Kibana отправит тестовое уведомление через выбранный Connector, не дожидаясь реального срабатывания. Для правил по логам индексация должна покрывать всё окно проверки, иначе count будет неполным.
Grafana Loki: Alert rules и Contact points
- Alerting, затем Contact points, затем New contact point. Для Telegram понадобятся токен бота и chat_id.
- Notification policies: маршрутизация по метке severity, например critical в Telegram, warning в Slack.
- Alert rules, затем New alert rule: выбираете Loki как источник данных и вставляете LogQL-запрос.
- Условие: sum(rate(...)) by (service) больше 0.05, окно 5 минут, параметр for 5m.
- Проверьте кнопкой Test и посмотрите состояние в Alerting, затем Active alerts.
Пример правила на всплеск ошибок:
sum(rate({app="api"} |= "level=error" [5m])) by (service) > 0.05
Параметр for задаёт, сколько правило должно оставаться в состоянии срабатывания до отправки: без него уведомления будут приходить на каждый короткий всплеск. Шаблоны сообщений, функции reduce и math и работу с метками разбираем в руководстве по продвинутым алертам в Grafana.
Как снизить ложные срабатывания через группировку событий
Тысяча одинаковых ошибок за минуту это одно событие, а не тысяча уведомлений. Отсутствие группировки превращает любой канал в спам и приводит к алертной усталости быстрее, чем неправильно выбранные пороги.
Группировка и дедупликация: одно уведомление вместо тысячи
В Grafana Alerting опция group_by задаёт критерии группировки входящих алертов внутри notification policy, а таймеры (group wait, group interval, repeat interval) определяют, когда и как часто отправляется уведомление. По умолчанию политики уведомлений группируют алерты по правилу алерта, используя метки alertname и grafana_folder, поскольку имена правил не уникальны между папками. Пример настройки группировки по сервису:
group_by: [alertname, service]
В Kibana то же делается через group by field со значением service, в Graylog через поля события в Event Definition. Дедупликация работает по ключу: события с одинаковым набором меток схлопываются в одно уведомление, а не отправляются поштучно. Дополнительно ограничьте частоту: throttle не чаще одного уведомления раз в 5 минут по одной группе.
Отдельная проблема - flapping, когда алерт то срабатывает, то гаснет. Окно for: 5m лечит его: правило отправляет уведомление, только если условие держится всё окно целиком.
Для маршрутизации между Prometheus, Grafana, ELK и Loki под нагрузкой есть разбор схем маршрутизации системных событий и логов.
Подавление на время деплоя и плановых работ
Во время деплоя часть ошибок ожидаема. Ставьте silence заранее и автоматически из CI/CD, а не руками. Silences API в Alertmanager входит в v2 API и позволяет создавать, читать, обновлять и удалять silences, которые подавляют уведомления для совпадающих алертов. Точный путь эндпоинта зависит от вашей инсталляции Grafana, поэтому сверяйтесь с документацией своей версии:
curl -X POST -H "Content-Type: application/json" --data @silence.json \ $GRAFANA_URL/api/alertmanager/grafana/api/v2/silences
В Grafana для регулярных окон есть mute timings: например, подавление предупреждений на время nightly-бэкапа. Provisioning HTTP API Grafana Alerting поддерживает получение одного mute timing, получение всех mute timings и создание нового mute timing. В Kibana аналогичную роль играют maintenance windows. Пример silence на 30 минут при деплое:
{
"matchers": [{"name": "service", "value": "api", "isRegex": false}],
"startsAt": "2026-09-30T21:00:00Z",
"endsAt": "2026-09-30T21:30:00Z",
"createdBy": "ci",
"comment": "deploy api v1.42.0"
}
Silence по сервису и метке деплоя точнее глобального отключения алертов: остальные сервисы продолжают будить дежурного. Готовые правила подавления и группировки для Alertmanager собраны в практическом руководстве по Prometheus и Alertmanager.
Куда отправлять уведомления: почта, Slack, Telegram
| Канал | Для чего подходит | Ограничение |
|---|---|---|
| Email (SMTP) | Аудит, несрочные алерты, отчёты | Медленно, легко теряется в ящике |
| Slack (Incoming Webhook) | Командная работа, разбор в рабочее время | Около одного сообщения в секунду на webhook |
| Telegram (Bot API) | Дежурные, мобильные уведомления | Нужен chat_id, нет тематических потоков |
Маршрутизация по важности: critical идёт в Telegram дежурному, warning в командный Slack, info уходит в почту и никого не будит. Общий чат на все алерты убивает канал за неделю: сообщения про 404 от сканеров смешаются с падением продакшена.
Slack: Incoming Webhook и форматирование сообщений
Создайте Incoming Webhook в настройках приложения Slack и вставьте его в Connector или в Notification. Payload с цветом по важности:
{
"text": "API: 5xx above threshold",
"attachments": [
{"color": "danger", "title": "service=api", "text": "120 errors / 5m"}
]
}
Цвет danger для critical, warning для предупреждений: дежурный оценивает важность без чтения текста. Учитывайте ограничение webhook: Slack допускает не более одного сообщения в секунду на канал, в том числе через incoming webhook, и отдельно указывает лимит 1 сообщение в секунду на webhook URL. Короткие всплески допускаются, но при продолжительном превышении лимита Slack начинает применять rate limiting и возвращает HTTP 429 Too Many Requests с заголовком Retry-After, содержащим число секунд до повторной попытки. Группировка из предыдущего раздела снижает риск упереться в этот лимит.
Telegram: Bot API и chat_id
- Создайте бота через @BotFather и получите токен.
- Напишите боту любое сообщение от своего аккаунта.
- Запросите обновления методом getUpdates и возьмите chat_id из ответа.
- Укажите токен и chat_id в contact point Grafana.
Проверка отправки:
curl -s "$TELEGRAM_API/bot$TELEGRAM_TOKEN/sendMessage" \ -d chat_id=$TELEGRAM_CHAT_ID \ -d text="test alert: api 5xx"
Токен держите в секретах Grafana или в переменных окружения, не в тексте правила: утечка токена отдаёт постороннему возможность писать в ваш рабочий чат. Для нескольких дежурных используйте отдельные contact points и notification policy с привязкой к расписанию on-call.
Проверка и поддержка правил: чек-лист перед продом
Правило, которое не сработало в момент аварии, хуже отсутствующего: на него рассчитывают. Проверяйте алерты до прода.
Как протестировать алерт без реального инцидента
Сгенерируйте событие, которое попадёт в индекс и подойдёт под фильтр:
logger -p user.error "TEST ALERT api 5xx monitor check"
Что проверить после отправки: событие попало в поток с нужным полем level; правило перешло в firing в течение окна for; уведомление дошло до канала. В Grafana перед сохранением используйте Test rule, в Kibana кнопку Test у правила. Тестовые записи помечайте словом TEST, чтобы они не искажали последующие запросы по базовой линии.
Ревизия правил и борьба с алерт-фатигом
Алертная усталость наступает, когда уведомлений больше, чем дежурный способен обработать: он перестаёт открывать алерты, отключает правила, переносит их в отдельный канал. Признак проблемы простой: если правило срабатывает чаще раза в день и не требует действия, его удаляют или понижают severity. Ревизия раз в квартал, удаление мёртвых правил и снижение шума через группировку возвращают доверие к системе. Приёмы подавления шума и эскалации разбирали в статье про умные уведомления и борьбу с шумом.
Второй ориентир для ревизии - SLO: алерт нужен там, где расходуется error budget. Если срабатывание не связано с SLO сервиса и не требует действий, оно занимает чужое внимание.
Типичные ошибки при алертинге по логам
- Алерт на каждый ERROR. Днём это сотни уведомлений. Сравнивайте с нормой: долю ошибок от запросов или всплеск относительно базовой линии.
- Правило без владельца. Непонятно, чей это сервис и к кому идти. Указывайте владельца в метке и в описании правила.
- Правило без runbook. Дежурный ночью не знает, что делать. Добавьте ссылку на инструкцию прямо в текст уведомления.
- Порог без учёта сезонности. Ночной трафик в разы ниже дневного, фиксированный порог будет то молчать, то срабатывать на нормальном пике.
- Все алерты в один общий чат. Канал превращается в ленту, критичное тонет в потоке.
- Нет теста правила. Алерт не срабатывает в аварию, потому что фильтр не совпал с реальным форматом записи.
- Отсутствие группировки. Тысяча событий даёт тысячу уведомлений и блокировку канала.
Перед включением правила в прод ответьте на пять вопросов: что именно оно ловит, при каком условии срабатывает, кому приходит, что делать дежурному и как проверить срабатывание без аварии. Если ответа нет хотя бы на один, правило рано включать.