Переход с локальных логов на централизованную систему укладывается в пять этапов: инвентаризация источников, выбор приёмника, пилот на одном сервисе, волновое масштабирование и проверка критериев готовности. Основное время уходит на подготовку: точный список источников и понимание форматов экономят неделю отладки чужих конфигов.
Два подводных камня ломают миграцию чаще остальных. Первый: несогласованные часовые пояса и форматы timestamp, из-за которых события выстраиваются в неверном порядке. Второй: дубликаты в переходный период, когда старая и новая схемы сбора работают параллельно. Оба снимаются заранее: время унифицируется по UTC, доставка делается идемпотентной на стороне приёмника.
Ниже рабочий план: команды для инвентаризации, таблица сравнения приёмников, конфиги Filebeat, Promtail и Logstash, разбор дублей и таймзон, чек-листы для пилота и для полного отказа от локального хранения. Синтаксис агентов зависит от версии, поэтому конфиг проверяйте на стенде и сверяйтесь с документацией вашей сборки.
Почему локальные логи перестают работать
Локальное хранение держится до первого крупного инцидента. Дальше проявляются одни и те же ограничения.
- Поиск растягивается на все хосты. Чтобы найти request_id, приходится обходить серверы по SSH.
- Ротация съедает нужное окно. Пока разбираетесь, logrotate уже удалил файл с событием.
- Отказ диска стирает историю. Логи не восстановить вместе с данными.
- Корреляция между сервисами почти невозможна. События одного запроса лежат в четырёх местах без общей метки.
- Права размазаны по машинам. Доступ к логам выдаётся через SSH, аудит доступа не собирается.
Поиск по десяти серверам выглядит так:
for h in $(cat hosts.txt); do echo "== $h" ssh "$h" "grep -h 'request_id=8f3c1d' /var/log/app/*.log" done
Обход занимает минуты, и всё это время инцидент продолжается. Если приложение пишет в stdout контейнера, а политика Docker log driver стоит по умолчанию, файлы на хосте растут до перезапуска контейнера и уносят место на диске вместе с ротацией.
Отдельный случай, который часто всплывает на разборе: systemd-journald без каталога /var/log/journal держит журнал в /run/log/journal, то есть в памяти. Перезагрузка стирает весь диапазон. Проверяется парой команд:
ls /var/log/journal 2>/dev/null journalctl --disk-usage
Централизация закрывает эти боли: один поисковый запрос вместо обхода хостов, единые права, общий retention и дашборды. Чем централизованное логирование отличается от локального хранения и какие архитектуры сбора применяют сейчас, мы разбирали в материале про выбор архитектуры логирования. Переход не сводится к установке агента: нужны инвентарь, приёмник, пилот и план отката.
Инвентаризация источников логов
Инвентарь отвечает на три вопроса: где лежат логи, сколько их и кто отвечает за источник. Без этого объём хранения и список агентов берутся с потолка, а половина сервисов обнаруживается уже после отключения локальных файлов.
Соберите базовую картину по каждому хосту:
find /var/log -type f -size +1M -printf '%s\t%p\n' | sort -rn | head -50
du -sh /var/log/* 2>/dev/null | sort -h | tail -20
find /var/log -type f -mtime -1 -printf '%s\n' | awk '{s+=$1} END {printf "%.1f MiB/sutki\n", s/1048576}'
journalctl --list-boots --no-pager | tail -5
ls /etc/rsyslog.d/ /etc/logrotate.d/
docker ps --format '{{.Names}}' | while read c; do echo "$c $(docker inspect -f '{{.LogPath}}' $c)"; done
Для Nginx и приложений с собственными конфигами полезно пройти по директивам: nginx -T | grep -i log покажет все пути и форматы, а grep -rn "syslog\|journal\|log_path" /etc/myapp/ найдёт нетиповые способы отправки, включая прямую запись в удалённый syslog.
Фиксируйте по каждому источнику одинаковый набор параметров, иначе инвентарь быстро превратится в набор заметок.
| Хост | Сервис | Источник | Формат | Объём в сутки | Retention | Ответственный | Приоритет |
|---|---|---|---|---|---|---|---|
| app-01 | billing-api | /var/log/app/app.log | plain text | 800 MiB | 30 дней | команда платежей | высокий |
| web-03 | nginx | access_log, error_log | combined | 3.2 GiB | 14 дней | инфраструктура | средний |
| db-02 | postgresql | journald | syslog-подобный | 120 MiB | 7 дней | DBA | средний |
| k8s-node-05 | все поды | stdout контейнеров | JSON | 2.5 GiB | 3 дня на диске | платформа | высокий |
Оценка роста важнее текущего объёма: поделите суточный объём на число активных пользователей и умножьте на ожидаемый рост нагрузки, чтобы приёмник не упёрся в диск на второй неделе. Отдельно отметьте источники, где логи содержат персональные данные или секреты: для них заранее решите, что вырезается на агенте.
Выбор приёмника логов: критерии и сравнение
Критерии выбора привязываются к вашей инфраструктуре, а не к популярности инструмента. Пройдите по списку честно.
- Объём и рост. Считайте в гигабайтах в сутки и в гигабайтах горячего хранения за 14 и 90 дней.
- Тип запросов. Нужен полнотекстовый поиск и агрегации или достаточно выборки по меткам и фильтра по строке.
- Retention и стоимость. Горячий, тёплый и холодный ярусы, выгрузка в объектное хранилище, цена гигабайта и трафика.
- Компетенции команды. Сколько человеко-часов в месяц вы готовы тратить на эксплуатацию кластера.
- Безопасность. Аутентификация, RBAC на уровне потоков и полей, TLS на всём пути, аудит доступа к логам.
- Интеграции. Алертинг, дашборды, выгрузка в SIEM, готовые агенты для вашего стека.
Сравнительная таблица популярных решений
| Решение | Лицензия | Сложность запуска | Поиск | Алертинг | Типичный сценарий |
|---|---|---|---|---|---|
| Elasticsearch (ELK) или OpenSearch | Elastic License 2.0, SSPL и AGPLv3 у Elasticsearch в зависимости от версии; OpenSearch под Apache 2.0 | высокая: кластер, шарды, JVM heap, ILM | полнотекст, агрегации, сложные запросы | встроенный в Kibana и OpenSearch Dashboards, плюс внешние правила | крупная инфраструктура, аудит, разбор инцидентов по тексту |
| Grafana Loki | AGPLv3 | средняя: один бинарник плюс объектное хранилище | по меткам, полнотекста по содержимому нет | Grafana Alerting по LogQL | уже есть Grafana и Prometheus, нужен недорогой сбор |
| Graylog | открытая база плюс коммерческая Enterprise-редакция | средняя: сервер, MongoDB, поисковый бэкенд | полнотекст по индексам Elasticsearch или OpenSearch | встроенный, с правилами обработки потоков | команда хочет готовый веб-интерфейс и правила без разработки |
| Fluent Bit и Fluentd | Apache 2.0 | низкая на уровне агента, растёт с парсерами | зависит от приёмника | нет, это транспорт | агент в Kubernetes, DaemonSet или sidecar |
| Vector | MPL 2.0 | низкая: конфиг как пайплайн | зависит от приёмника | нет, это транспорт | замена агента с высокой пропускной способностью, маршрутизация в несколько хранилищ |
Условия лицензий и состав редакций меняются, поэтому перед выбором сверяйте их на сайте проекта для нужной версии. Практические ориентиры такие: до нескольких десятков гигабайт в сутки и наличие Grafana делают Loki самым дешёвым вариантом; требования к полнотекстовому поиску, аудиту и сложным агрегациям ведут к OpenSearch или Elasticsearch; команде без выделенного инженера по хранилищам проще стартовать с Graylog. Агенты и приёмники удобно разделять: Vector или Fluent Bit собирают и маршрутизируют, хранилище отвечает за индексацию и выдачу. Разбор конвейера и ролей компонентов с примерами конфигов есть в статье про архитектуру централизованного сбора логов.
Пилот на одном сервисе: пошаговый план
Пилот проверяет не инструмент, а процесс: сколько времени занимает подключение одного источника и где ломается пайплайн. Возьмите сервис, который не участвует в обработке платежей и не служит источником для аудита безопасности.
- Выберите источник с понятным форматом и объёмом до 1-2 ГиБ в сутки.
- Зафиксируйте метрики успеха: задержка доставки p95, расхождение числа событий, размер индекса за сутки, стоимость гигабайта.
- Поднимите приёмник в отдельном контуре: свой namespace, отдельный индекс или тенант, отдельные креды агента.
- Установите агент и соберите конфиг под конкретный формат логов.
- Настройте парсинг и проверьте, что поля извлекаются, а timestamp берётся из события, а не из времени чтения файла.
- Включите мониторинг агента и приёмника с алертами на отставание и потери.
- Прогоните тест на сбой: остановите приёмник на 15 минут и убедитесь, что события доехали после возврата связи.
Настройка сбора логов на примере Filebeat
Минимальный конфиг для текстового лога приложения с метками окружения и сервиса:
filebeat.inputs:
- type: filestream
id: billing-api
enabled: true
paths:
- /var/log/app/*.log
fields:
service: billing-api
env: staging
fields_under_root: true
parsers:
- multiline:
type: pattern
pattern: '^\d{4}-\d{2}-\d{2}'
negate: true
match: after
processors:
- add_host_metadata: ~
- drop_fields:
fields: ["agent.ephemeral_id", "ecs.version"]
output.elasticsearch:
hosts: ["https://es-node-01.internal:9200"]
index: "logs-app-%{[service]}-%{+yyyy.MM.dd}"
api_key: "${ES_API_KEY}"
ssl.verification_mode: full
logging.level: info
logging.to_files: true
logging.files:
path: /var/log/filebeat
keepfiles: 7
Параметр multiline нужен для стектрейсов Java и Python: без него одна ошибка превращается в двадцать отдельных событий и ломает поиск. Директиву fields_under_root ставьте осознанно, чтобы метка service попала в верхний уровень документа и по ней строились фильтры. Проверка конфига выполняется до перезапуска службы:
filebeat test config -c /etc/filebeat/filebeat.yml filebeat test output -c /etc/filebeat/filebeat.yml systemctl restart filebeat
Для Loki тот же источник описывается через Promtail или Grafana Alloy:
server:
http_listen_port: 9080
clients:
- url: http://loki.internal:3100/loki/api/v1/push
scrape_configs:
- job_name: billing-api
static_configs:
- targets: [localhost]
labels:
service: billing-api
env: staging
__path__: /var/log/app/*.log
pipeline_stages:
- json:
expressions:
level: level
request_id: request_id
- labels:
level:
Проверка доставки и парсинга
Проверяйте не факт запуска агента, а наличие конкретных событий. Для Elasticsearch:
curl -s "https://es-node-01.internal:9200/_cat/indices/logs-app-*?v&h=index,docs.count,store.size" -H "Authorization: ApiKey $ES_API_KEY"
curl -s -X GET "https://es-node-01.internal:9200/logs-app-billing-api-*/_count" \
-H "Content-Type: application/json" -H "Authorization: ApiKey $ES_API_KEY" \
-d '{"query":{"term":{"service":"billing-api"}}}'
Для Loki запрос по потоку:
curl -s -G "http://loki.internal:3100/loki/api/v1/query_range" \
--data-urlencode 'query={service="billing-api"}' \
--data-urlencode 'limit=5'
Парсинг удобно разбирать grok-паттерном в Logstash, если источник отдаёт текст:
filter {
grok {
match => { "message" => "%{TIMESTAMP_ISO8601:ts} %{LOGLEVEL:level} \\[%{DATA:service}\\] %{GREEDYDATA:msg}" }
tag_on_failure => ["_grokparsefailure"]
}
date {
match => ["ts", "ISO8601"]
target => "@timestamp"
}
if "_grokparsefailure" in [tags] { drop {} }
}
Структурированные логи снимают большую часть этой работы: поиск по request_id, фильтры по level и дашборды по service начинают работать без регулярных выражений. Обязательный набор полей и план перехода без остановки сервисов мы собрали в статье про переход от plain text к JSON.
Чек-лист пилота:
- timestamp в документе совпадает со временем из строки лога, а не со временем чтения файла;
- в событиях есть service, env, level и request_id там, где приложение их пишет;
- число событий в приёмнике расходится с числом строк в файле не более чем на 0,1% на сопоставимом интервале;
- в логах агента нет ошибок отправки и отказов по правам;
- потребление CPU и памяти агентом укладывается в 1-2% ресурсов хоста;
- тест с остановкой приёмника на 15 минут не привёл к потере событий.
Подводный камень 1: разные форматы времени и часовые пояса
Самая частая причина «логов нет» и «порядок событий неверный» это время. Сервер в Europe/Moscow, сервер в UTC и приложение, которое пишет локальное время без смещения, дают сдвиг в три часа и разрушенную корреляцию по request_id. При переходе на летнее время в регионах, где его отменяли или возвращали, сдвиг меняется в середине истории.
Правило простое: все хосты живут в UTC, все события пишутся в RFC 3339 с указанием смещения, а часовой пояс пользователя применяется только при выводе.
| Как писать не стоит | Почему плохо | Как писать правильно |
|---|---|---|
| 2026-10-01 10:15:32 | нет смещения, неизвестна таймзона хоста | 2026-10-01T07:15:32.412Z |
| 01/Oct/2026:10:15:32 (без offset) | формат без зоны, парсер подставит время сервера | 01/Oct/2026:10:15:32 +0300 |
| 1759300532 | точность секунды, порядок событий внутри секунды теряется | epoch в миллисекундах или ISO 8601 с миллисекундами |
Синхронизация времени настраивается на всех хостах одинаково:
timedatectl set-timezone UTC timedatectl set-ntp true timedatectl status chronyc sources -v chronyc tracking timedatectl timesync-status
В выводе chronyc tracking смотрите строку System time и значение Root dispersion: рост dispersion означает, что источник времени недоступен и часы расходятся. Если приложение уже пишет локальное время без смещения, исправить это на агенте можно, но лучше править приложение: любой промежуточный костыль придётся поддерживать годами.
date {
match => ["ts", "yyyy-MM-dd HH:mm:ss"]
timezone => "Europe/Moscow"
target => "@timestamp"
}
В Promtail то же самое решается стадией timestamp:
- timestamp:
source: ts
format: RFC3339
location: "Europe/Moscow"
После настройки проверьте результат: откройте события одного request_id и убедитесь, что порядок в приёмнике совпадает с порядком в исходном файле. Расхождение на несколько секунд обычно означает, что timestamp всё ещё берётся из времени чтения файла.
Подводный камень 2: дублирование событий при параллельной работе
Пока старая схема сбора и новая работают одновременно, каждая строка рискует попасть в хранилище дважды. Источники дублей предсказуемы:
- два транспорта на хосте: rsyslog продолжает пересылать логи туда же, куда пишет Filebeat;
- пересекающиеся маски путей, когда один и тот же файл попадает в два инпута, в том числе через симлинк;
- повторная отправка после сбоя: доставка «минимум один раз» на уровне транспорта даёт копию события;
- перекрытие чтения при ротации, когда агент дочитывает старый inode и уже открыл новый файл;
- несколько реплик приложения, пишущих в один файл без идентификатора экземпляра.
На время перехода держите на хосте один активный транспорт. Отключайте старый сбор явно, а не «когда-нибудь потом»:
systemctl stop rsyslog-forwarder.service systemctl disable rsyslog-forwarder.service filebeat test config -c /etc/filebeat/filebeat.yml
Дедупликация строится на идемпотентности записи. В Logstash добавьте fingerprint и используйте его как идентификатор документа:
filter {
fingerprint {
source => ["host.name", "log.file.path", "message"]
target => "[@metadata][fp]"
method => "SHA256"
concatenate_sources => true
}
}
output {
elasticsearch {
hosts => ["https://es-node-01.internal:9200"]
index => "logs-app-%{[service]}-%{+yyyy.MM.dd}"
document_id => "%{[@metadata][fp]}"
api_key => "${ES_API_KEY}"
}
}
Одинаковый _id превращает повторную запись в обновление того же документа, и вторая копия не появляется. Ограничение метода: он работает, пока состав полей в fingerprint стабилен, а агент не переписывает message форматированием. Для Loki такого механизма нет, поэтому дедупликацию делают до записи: transform dedupe в Vector по набору полей host, file, message или фильтрация на стороне агента.
Проверка дублей выполняется сравнением двух чисел: общего количества событий и количества уникальных отпечатков за тот же интервал.
curl -s -X GET "https://es-node-01.internal:9200/logs-app-billing-api-*/_search" \
-H "Content-Type: application/json" -H "Authorization: ApiKey $ES_API_KEY" \
-d '{"size":0,"track_total_hits":true,"aggs":{"uniq":{"cardinality":{"field":"event.hash","precision_threshold":40000}}}}'
Дополнительно тегируйте поток источника: поле pipeline со значениями old и new позволит отделить события переходного периода и позже удалить устаревший контур одним запросом. Для приложений с высокой ценой потери событий разумно временно увеличить окно поиска и хранить сутки сырых данных с обеих сторон, чтобы сравнить полноту.
Сохранение исторических данных и обратная совместимость
Историю нельзя терять при переключении: она нужна для разбора старых инцидентов, аудита и разбирательств с клиентами. Работайте по трём направлениям одновременно.
Архивация локальных файлов. Перед отключением соберите логи в сжатые архивы. Сжатие zstd даёт хороший баланс скорости и размера, а проверка содержимого обязательна.
tar --use-compress-program='zstd -19 -T0' -cf /mnt/archive/app-2026-09.tar.zst /var/log/app/ tar -tf /mnt/archive/app-2026-09.tar.zst | wc -l sha256sum /mnt/archive/app-2026-09.tar.zst > /mnt/archive/app-2026-09.tar.zst.sha256
Ротация вместо удаления. Пока переход не завершён, локальные файлы остаются буфером. Конфиг logrotate с ежедневной ротацией, сжатием и задержкой сжатия сохраняет последний файл доступным для grep:
/var/log/app/*.log {
daily
rotate 30
compress
delaycompress
missingok
notifempty
dateext
dateformat -%Y%m%d
sharedscripts
postrotate
systemctl reload app.service >/dev/null 2>&1 || true
endscript
}
Загрузка истории в приёмник. Старые логи можно разобрать пакетно и записать в отдельные индексы с меткой historical. Индексы прошлых периодов помечайте только для чтения, чтобы политики жизненного цикла не применялись к ним вместе с текущими данными.
curl -X PUT "https://es-node-01.internal:9200/logs-app-2025.12/_settings" \
-H "Content-Type: application/json" -H "Authorization: ApiKey $ES_API_KEY" \
-d '{"index.blocks.write": true}'
Для кластера Elasticsearch историю удобно держать снапшотами в объектном хранилище: снапшот занимает дешевле горячих шардов и восстанавливается по требованию. Стратегии ротации, ярусы hot, warm и cold и настройка ILM подробно разобраны в статье про хранение и ротацию логов в проде.
Обратная совместимость касается не только данных, но и привычек команды. Сохраните пути к файлам и возможность привычного grep на переходный период, оставьте старый парсер для legacy-формата рядом с новым, не удаляйте дашборды старой системы до момента, когда новые подтвердят те же цифры. Если сервис пишет и текст, и JSON в разные файлы, обрабатывайте оба потока одним пайплайном с общим набором полей, чтобы запросы по service и level работали одинаково.
Масштабирование на остальные сервисы
После пилота подключайте сервисы волнами, а не одним коммитом. Автоматизация установки агента экономит время и снижает риск расхождения конфигов между хостами.
- hosts: log_agents
serial: "10%"
max_failure_percentage: 10
roles:
- role: filebeat
vars:
filebeat_output: "logstash.internal:5044"
filebeat_extra_fields:
env: production
Параметр serial ограничивает волну десятью процентами хостов, а max_failure_percentage останавливает прогон, если ошибок стало больше допустимого. Критичные сервисы выносите в отдельную волну и ставьте её последней. Для Kubernetes используйте DaemonSet с Vector или Fluent Bit и отдельные настройки для подов, которые пишут особенно объёмно.
Следите за приёмником во время каждой волны. Значимые метрики: входящий поток и глубина очереди у агента (events.acked, events.dropped, queue depth), скорость индексации и отказы записи у хранилища, утилизация CPU и диска, потребление heap при Java-стеке. Рост задержки или появление отказов это сигнал остановить волну и разобраться, а не добавлять следующие хосты.
Сглаживать пики помогает буферизация: дисковая очередь на агенте позволяет пережить недоступность приёмника, а брокер вроде Kafka развязывает приём и запись при скачках потока. Отдельная проблема масштабирования это рост числа полей: динамический маппинг на произвольном JSON быстро создаёт тысячи полей и деградирует индексацию. Ограничьте набор полей шаблоном индекса и явно перечислите типы для ключевых полей.
Критерии готовности к полному отказу от локального хранения
Отключать локальные логи можно тогда, когда централизованный контур подтверждён измерениями, а не ощущением. Пройдите по чек-листу.
- Все источники из инвентаря подключены, статус по каждому подтверждён владельцем сервиса.
- Отказ приёмника на 15-30 минут не приводит к потере событий: дисковая очередь агента не переполняется, после восстановления связи события доезжают.
- Задержка доставки p95 укладывается в согласованный предел, например 30 секунд для операционных логов.
- Полнота подтверждена сверкой числа событий с числом строк в источниках на выборке сервисов.
- Настроены алерты на отставание агента, переполнение диска, отказы записи и рост задержки.
- Резервное копирование проверено восстановлением снапшота на тестовом кластере, а не только созданием копии.
- Права доступа, RBAC и шифрование трафика настроены; доступ к логам попадает в аудит.
- Retention согласован с требованиями хранения, стоимость гигабайта посчитана и укладывается в бюджет.
- Есть runbook: что делать, если логи перестали поступать, кому звонить и как откатиться на локальный сбор.
Что смотреть в цифрах, чтобы критерии не остались декларацией:
| Метрика | Где брать | Ориентир |
|---|---|---|
| Задержка доставки p95 | разница между timestamp события и временем записи в хранилище | до 30 секунд для операционных логов |
| Доля потерянных событий | events.dropped у агента, отказы записи у приёмника | 0 в штатном режиме |
| Расхождение полноты | сверка count по источнику и приёмнику | не более 0,1% на сопоставимом интервале |
| Глубина очереди | queue depth агента и брокера | не растёт монотонно в течение часа |
| Стоимость хранения | объём индексов и архивов на гигабайт | в пределах бюджета, с запасом на рост |
Отключение делайте постепенно: сначала некритичные сервисы, где потеря нескольких часов логов не влияет на разбор инцидентов, и только потом критичный контур. На переходный период оставьте локальный буфер и ротацию, а перед сокращением объёма записи проверьте, что политика логирования не вырезала нужные события: критерии, что стоит писать, а что только создаёт шум, разобраны в материале про критерии логирования.
Заключение
Начните с инвентаря: таблица источников с путями, форматами и суточным объёмом сразу показывает, сколько гигабайт придётся хранить и какие сервисы дадут основной поток. Дальше выберите приёмник по типу запросов и компетенциям команды, поднимите его в отдельном контуре и подключите один сервис.
Разберитесь с двумя вещами до масштабирования: переведите все хосты в UTC с синхронизацией по chrony и сделайте запись идемпотентной через fingerprint или отказ от второго транспорта на хосте. Эти два шага снимают большинство сложных разборов, которые всплывают уже после переключения.
Масштабирование ведите волнами по десять процентов хостов с автоматизацией установки агента, следите за задержкой и глубиной очереди на каждой волне. Полный отказ от локального хранения подтверждайте метриками: задержка p95, нулевые потери, сверка полноты, проверенное восстановление из снапшота. Пока эти числа не сходятся, локальные файлы остаются дешёвой страховкой.