Миграция с локальных логов на централизованную систему: пошаговый план и подводные камни | AdminWiki

Миграция с локальных логов на централизованную систему: пошаговый план и подводные камни

01 октября 2026 15 мин. чтения

Переход с локальных логов на централизованную систему укладывается в пять этапов: инвентаризация источников, выбор приёмника, пилот на одном сервисе, волновое масштабирование и проверка критериев готовности. Основное время уходит на подготовку: точный список источников и понимание форматов экономят неделю отладки чужих конфигов.

Два подводных камня ломают миграцию чаще остальных. Первый: несогласованные часовые пояса и форматы 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-01billing-api/var/log/app/app.logplain text800 MiB30 днейкоманда платежейвысокий
web-03nginxaccess_log, error_logcombined3.2 GiB14 днейинфраструктурасредний
db-02postgresqljournaldsyslog-подобный120 MiB7 днейDBAсредний
k8s-node-05все подыstdout контейнеровJSON2.5 GiB3 дня на дискеплатформавысокий

Оценка роста важнее текущего объёма: поделите суточный объём на число активных пользователей и умножьте на ожидаемый рост нагрузки, чтобы приёмник не упёрся в диск на второй неделе. Отдельно отметьте источники, где логи содержат персональные данные или секреты: для них заранее решите, что вырезается на агенте.

Выбор приёмника логов: критерии и сравнение

Критерии выбора привязываются к вашей инфраструктуре, а не к популярности инструмента. Пройдите по списку честно.

  • Объём и рост. Считайте в гигабайтах в сутки и в гигабайтах горячего хранения за 14 и 90 дней.
  • Тип запросов. Нужен полнотекстовый поиск и агрегации или достаточно выборки по меткам и фильтра по строке.
  • Retention и стоимость. Горячий, тёплый и холодный ярусы, выгрузка в объектное хранилище, цена гигабайта и трафика.
  • Компетенции команды. Сколько человеко-часов в месяц вы готовы тратить на эксплуатацию кластера.
  • Безопасность. Аутентификация, RBAC на уровне потоков и полей, TLS на всём пути, аудит доступа к логам.
  • Интеграции. Алертинг, дашборды, выгрузка в SIEM, готовые агенты для вашего стека.

Сравнительная таблица популярных решений

РешениеЛицензияСложность запускаПоискАлертингТипичный сценарий
Elasticsearch (ELK) или OpenSearchElastic License 2.0, SSPL и AGPLv3 у Elasticsearch в зависимости от версии; OpenSearch под Apache 2.0высокая: кластер, шарды, JVM heap, ILMполнотекст, агрегации, сложные запросывстроенный в Kibana и OpenSearch Dashboards, плюс внешние правилакрупная инфраструктура, аудит, разбор инцидентов по тексту
Grafana LokiAGPLv3средняя: один бинарник плюс объектное хранилищепо меткам, полнотекста по содержимому нетGrafana Alerting по LogQLуже есть Grafana и Prometheus, нужен недорогой сбор
Graylogоткрытая база плюс коммерческая Enterprise-редакциясредняя: сервер, MongoDB, поисковый бэкендполнотекст по индексам Elasticsearch или OpenSearchвстроенный, с правилами обработки потоковкоманда хочет готовый веб-интерфейс и правила без разработки
Fluent Bit и FluentdApache 2.0низкая на уровне агента, растёт с парсерамизависит от приёмниканет, это транспортагент в Kubernetes, DaemonSet или sidecar
VectorMPL 2.0низкая: конфиг как пайплайнзависит от приёмниканет, это транспортзамена агента с высокой пропускной способностью, маршрутизация в несколько хранилищ

Условия лицензий и состав редакций меняются, поэтому перед выбором сверяйте их на сайте проекта для нужной версии. Практические ориентиры такие: до нескольких десятков гигабайт в сутки и наличие Grafana делают Loki самым дешёвым вариантом; требования к полнотекстовому поиску, аудиту и сложным агрегациям ведут к OpenSearch или Elasticsearch; команде без выделенного инженера по хранилищам проще стартовать с Graylog. Агенты и приёмники удобно разделять: Vector или Fluent Bit собирают и маршрутизируют, хранилище отвечает за индексацию и выдачу. Разбор конвейера и ролей компонентов с примерами конфигов есть в статье про архитектуру централизованного сбора логов.

Пилот на одном сервисе: пошаговый план

Пилот проверяет не инструмент, а процесс: сколько времени занимает подключение одного источника и где ломается пайплайн. Возьмите сервис, который не участвует в обработке платежей и не служит источником для аудита безопасности.

  1. Выберите источник с понятным форматом и объёмом до 1-2 ГиБ в сутки.
  2. Зафиксируйте метрики успеха: задержка доставки p95, расхождение числа событий, размер индекса за сутки, стоимость гигабайта.
  3. Поднимите приёмник в отдельном контуре: свой namespace, отдельный индекс или тенант, отдельные креды агента.
  4. Установите агент и соберите конфиг под конкретный формат логов.
  5. Настройте парсинг и проверьте, что поля извлекаются, а timestamp берётся из события, а не из времени чтения файла.
  6. Включите мониторинг агента и приёмника с алертами на отставание и потери.
  7. Прогоните тест на сбой: остановите приёмник на 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, нулевые потери, сверка полноты, проверенное восстановление из снапшота. Пока эти числа не сходятся, локальные файлы остаются дешёвой страховкой.

Поделиться: