Централизованные логи почты в ELK/OpenSearch: сбор с нескольких MTA, парсинг Postfix и дашборды доставляемости
Пока логи лежат в /var/log на пяти разных Postfix, вы не управляете доставляемостью, а гадаете. Разбираем сквозной путь: rsyslog → Vector → OpenSearch, боевой grok/VRL под все под-демоны Postfix, сшивку сессии по queue ID и дашборды по deferred/bounce/reject.
EvilMail Team28 июля 2026 г.12 мин чтения
Проблема: почему grep по SSH не масштабируется
Клиент пишет: «Отправил счёт в 14:32, письмо не дошло». У вас пять MTA, логи ротируются раз в сутки, а одно письмо на пути через smtpd → cleanup → qmgr → smtp порождает четыре-шесть строк, связанных между собой только queue ID вроде 4Qk8fS2p9z. Хуже: после релея на следующем хосте queue ID уже другой. Начинается ритуал — ssh edge01, grep, ssh relay01, grep, глаза бегут по времени, а часы на хостах разъехались на две секунды, и порядок строк врёт.
Это не проблема хранения. Логи и так лежат «в одном месте» — на дисках. Проблема в том, что у вас нет реконструкции сессии и нет агрегированной картины по status/dsn
Централизация логов почты в OpenSearch/ELK: парсинг Postfix и дашборды доставляемости — EvilMail Blog
. Пока вы не сшили multi-line сессию Postfix по queue ID в единый запрос и не подняли дашборд delivery rate по relay, вы не управляете доставляемостью — вы гадаете и отвечаете клиенту «сейчас посмотрим».
Дальше — сквозной пайплайн, который закрывает обе боли: централизация и нормализация в поля, по которым строятся ответы клиенту.
Архитектура пайплайна: кто что делает
Разложим по ответственности, потому что 80% кривых инсталляций — это когда парсинг воткнули не туда и гоняют сырьё через полстраны.
Источник — rsyslog/journald на каждом MTA. Их задача: не потерять строку и отдать её дальше по надёжному транспорту.
Shipper — агент на краю. Здесь выбор Filebeat против Vector. Filebeat умеет доставку, но парсит слабо и тянет обработку в Logstash. Vector парсит на месте языком VRL, буферизует на диск и написан на Rust.
Парсинг — grok в Logstash, Ingest pipeline в OpenSearch, или VRL прямо в Vector. Дешевле всего парсить на shipper: вы отправляете в хранилище уже структурированный JSON, а не гоняете гигабайты сырого текста через JVM.
Рекомендация на 2026: OpenSearch 2.x + Vector. OpenSearch — форк Elasticsearch 7.10 под Apache 2.0; форк родился, когда Elastic сменил лицензию на SSPL/Elastic License. Хотя в 2024-м Elastic вернул и AGPLv3, для инфраструктурного логирования OpenSearch остаётся более предсказуемым выбором без лицензионных оговорок и ценового потолка. Logstash на JVM берите только если он у вас уже стоит и вылизан — на голом поле Vector выигрывает по CPU и RAM в разы на том же потоке.
Сбор с нескольких MTA: rsyslog, теги и надёжная доставка
Первое правило: не UDP. Классический syslog-514/UDP тихо теряет строки под нагрузкой, а именно всплеск нагрузки (рассылка, атака) — момент, когда логи критичны. Берите TCP, а лучше RELP (omrelp) — он даёт подтверждённую доставку на уровне приложения.
Фильтруем только Postfix и форвардим в Vector:
# /etc/rsyslog.d/60-postfix-forward.conf
if $programname startswith 'postfix' then {
action(type="omfwd"
target="10.0.0.5" port="5140" protocol="tcp"
action.resumeRetryCount="-1"
queue.type="linkedList" queue.saveOnShutdown="on")
stop
}
Второе правило: тег роли на источнике. Без поля mta_role вы на дашборде не отделите входящий поток от исходящего, и delivery rate превратится в кашу. Проще всего проставить роль на приёме в Vector по имени хоста, но если хостов много и имена нерегулярны — задавайте её структурированным полем через шаблон rsyslog или переменную окружения агента.
Третье правило: буфер на диске у shipper. Когда OpenSearch уходит на ребаланс или падает нода, Vector с buffer.type = disk копит события локально и досылает, вместо того чтобы ронять их на пол:
toml
[sinks.opensearch]
type = "elasticsearch" # совместимый API OpenSearch
inputs = ["postfix_parse"]
endpoints = ["https://10.0.0.9:9200"]
bulk.index = "postfix-logs-%Y.%m.%d"
buffer.type = "disk"
buffer.max_size = 536870912 # 512 МБ на случай простоя приёмника
Парсинг Postfix: grok, которого нет в дефолтах
Главная ловушка новичка — думать, что «формат Postfix» один. На деле это десяток под-демонов с разной грамматикой строки, и готовые grok-паттерны из репозиториев покрывают в лучшем случае smtp. Разберём то, что реально нужно извлечь.
grok работает, но он хрупкий: любое лишнее поле (conn_use=, dsn_orig=) ломает жёсткий шаблон. Надёжнее VRL с parse_key_value, потому что тело строки Postfix после queue ID — это по сути key=value:
Ключевое про кортеж delays=a/b/c/d: a — время до постановки в очередь, b — простой в очереди, c — установка соединения с relay, d — передача письма плюс ответ сервера. Большой c — это DNS или сеть до получателя. Большой b — очередь забита, вы упёрлись в конкуренцию доставки. Это поле — золото для тикетов «письма идут медленно», и без разбивки на компоненты вы такой тикет не закроете.
Не забудьте остальные под-демоны. cleanup даёт связку queue_id → message-id (cleanup[..]: 4Qk8fS2p9z: message-id=<...>) — она понадобится для сшивки через релеи. qmgr даёт from=, size=, nrcpt= и removed. smtpd даёт отказы на приёме: NOQUEUE: reject: RCPT from ... с client= — это ваш поток RBL/HELO/spam-реджектов, отдельная и очень полезная категория.
Сшивка сессии по queue ID
Сырой парсинг даёт вам отдельные события, но письмо — это не событие, это транзакция. Одно письмо = один стабильный message_id (переживает релеи) + несколько queue_id (по одному на MTA).
Прагматичная стратегия: индексируйте всё плоско, а сшивайте на запросе. Внутри одного хоста вся жизнь письма поднимается одним bool-запросом по queue ID:
Поперёк релеев queue ID меняется — здесь спасает message_id из строки cleanup. Замените в запросе term на message_id, и вы увидите письмо на всех MTA сразу, от приёма на edge до status=sent на relay. Именно поэтому message_id обязан быть извлечён и проиндексирован как keyword, а не утоплен в тексте.
Если нужен полноценный готовый «trace» одним документом, а не агрегацией на лету, можно свести события внутри хоста трансформом reduce в Vector по queue_id. Компромисс честный: reduce держит окно в памяти и добавляет задержку до флаша, зато отдаёт в индекс уже собранную транзакцию. Для большинства команд агрегации на запросе достаточно — не усложняйте, пока объём не заставит.
Дашборды доставляемости: что реально смотреть
Красивые графики никого не спасли. Нужны панели, по которым принимают решение:
Delivery rate = sent / (sent + deferred + bounced) по времени и в разбивке по relay. Падение на одном relay при норме на других — проблема конкретного получателя или вашего исходящего IP.
Top bounce reasons — терм-агрегация по dsn (классы 4.x временные, 5.x постоянные) и по тексту reject. 5.1.1 — unknown user (чистите базу), 5.7.1 — политика/спам (репутация), 4.7.0 — троттлинг.
Deferred backlog — счётчик status=deferred во времени. Ровный — норма повторов; растущий — получатель вас тормозит или ваш IP в грейлисте.
Per-destination heat — gmail.com, outlook.com, mail.ru отдельными строками. У крупных провайдеров разное поведение, и усреднённая метрика прячет проблему с конкретным из них.
Reject-поток на `smtpd` — RBL, HELO, spam. Всплеск — либо атака, либо вы сломали конфиг и режете своих.
Пороги, с которых начинают тревожиться: bounce rate > 2% на домен-получатель; одновременный рост deferred и dsn=4.7.0 к gmail.com — почти всегда троттлинг за репутацию IP, пора смотреть Postmaster Tools и прогрев; резкий всплеск smtpd reject — инцидент, а не фон.
Ретеншн, объём и безопасность
Считайте заранее. После парсинга документ в _source — 150–300 байт; MTA средней нагрузки даёт 1–5 млн событий в сутки, то есть примерно 0.5–2 ГБ на MTA в день до сжатия. На пять хостов за 90 дней это сотни гигабайт — управляйте этим политикой ISM в OpenSearch:
rollover при 30 ГБ или 1 дне;
hot 7 дней → warm 30 → delete 90;
индексы postfix-logs-YYYY.MM.dd или data stream.
Отдельно про PII. Поля from, to, message_id — персональные данные, и под GDPR к ним нельзя давать свободный поиск всей команде. Включайте OpenSearch Security с RBAC, ограничивайте поиск по адресам ролью, а где требует регламент — применяйте field masking на адресные поля. Логи доставляемости не должны превращаться в теневую CRM.
Чеклист внедрения
Единый NTP на всех MTA — без него мультихостовая сессия сортируется по времени неверно.
Тег mta_role (edge/relay/mailbox) проставлен на источнике.
Транспорт — RELP или TCP с диск-буфером у shipper, не UDP.
grok/VRL покрывает smtp, smtpd, qmgr, cleanup, а не только доставку.
queue_id и message_id — тип keyword, извлечены явно.
Кортеж delays=a/b/c/d разложен в отдельные числовые поля.
ISM policy настроена: hot 7 → warm 30 → delete 90.