Логи встраиваемых Linux-систем: особенности сбора

Когда я перешел с «голых» микроконтроллеров на встраиваемый Linux, первое время испытывал культурный шок. С одной стороны — это почти настоящий сервер: bash, systemd, сеть. С другой — у тебя 64 мегабайта оперативки, медленная flash-память с ограниченным ресурсом циклов перезаписи и полное отсутствие клавиатуры с монитором.

И тут встаёт вопрос: а как, собственно, собирать логи? Если просто включить syslog как на десктопе, устройство умрёт через полгода. Если выключить всё — вы ослепнете.

В этой статье я расскажу, как мы в наших проектах настраивали сбор логов для встраиваемых Linux-систем. Без воды, с реальными граблями и конфигами.

В чём принципиальная разница

Начнём с главного: встраиваемая система — это не урезанный сервер. У неё другие приоритеты.

Память. Если на сервере 32 гигабайта ОЗУ — норм, на встройке 128 мегабайт — уже шикарно. А бывает и 64, и 32 . Логи не должны жрать оперативку.

Диск. Ваша флешка (часто просто SD-карта или впаянный NAND) имеет ресурс. Типичная MLC-флешка переживает 3000–10000 циклов перезаписи. Если вы пишете логи в /var/log как бешеный, устройство умрёт не от старости, а от износа памяти .

Автономность. Система работает годами без перезагрузки и без доступа человека. Логи должны либо не забивать всё, либо уезжать наружу.

Безопасность. Если устройство взломают, злоумышленник в первую очередь почистит логи. Хранить их только локально — значит дать ему фору .

Анатомия логов в embedded Linux

В современном встраиваемом Linux есть два основных подхода: классический syslog и systemd-journald.

Классика: BusyBox syslogd

Если ваша система собирается через Buildroot или OpenWrt, у вас скорее всего BusyBox. И у него есть встроенный syslogd .

Самый правильный режим для embedded — логирование в кольцевой буфер в оперативной памяти:

syslogd -C 64

Флаг -C 64 означает: выделить 64 килобайта под кольцевой буфер в RAM. Логи пишутся в память, по кругу. Чтобы их прочитать, используем logread .

Плюсы: - Нет записи на диск — флешка живёт вечно. - Не надо заботиться о ротации — старые записи затираются новыми. - Быстро.

Минусы: - При перезагрузке всё теряется. - Размер буфера ограничен.

Современный подход: systemd-journald

Если у вас система пожирнее (например, Yocto с systemd), то логи собирает journald. Он тоже умеет писать в память.

В /etc/systemd/journald.conf выставляем:

Storage=volatile SystemMaxUse=16M RuntimeMaxUse=16M ForwardToSyslog=no

Это заставляет journald хранить логи только в /run/log/journal (то есть в оперативке) и ограничивает объём 16 мегабайтами . Перезагрузка — логи ушли.

Но у journald есть киллер-фича: он хранит логи в бинарном формате с полной структурированной информацией — PID, юнит, приоритет, временные метки. Это потом пригодится для разбора.

Куда девать логи, если память не резиновая?

Хранить только в RAM — рискованно. Перезагрузилось устройство ночью — и вы никогда не узнаете, почему. Поэтому логи нужно эвакуировать.

Вариант 1: удалённый syslog-сервер

Самый простой и надёжный способ — слать логи по сети на центральный сервер. Используем протокол syslog.

На стороне устройства (клиент): ```bash

Для BusyBox syslogd

syslogd -R logserver.local:514 -L

Для rsyslog

echo ". @@logserver.local:514" >> /etc/rsyslog.conf ```

Двойной знак @@ означает TCP. Если поставить один @ — будет UDP, быстрее, но ненадёжнее .

На стороне сервера (лог-сервер): ```bash

/etc/rsyslog.conf

$ModLoad imtcp $InputTCPServerRun 514

$template RemoteLogs,"/var/log/remote/%HOSTNAME%/%PROGRAMNAME%.log" . ?RemoteLogs & ~ ```

Я использую такую схему уже лет пять. Работает безотказно. Единственное — следите, чтобы сервер был доступен постоянно. Если сеть падает, логи забивают локальный буфер.

Вариант 2: умный агрегатор с буферизацией

Для ответственных систем мы ставим на устройство Fluent Bit . Это легковесный агент (несколько мегабайт), который умеет:

  • Забирать логи из journald или файлов.
  • Буферизовать их на случай обрыва сети (на диск или в память).
  • Пакетно отправлять в Elasticsearch, Kafka или просто HTTP-эндоинт.

Конфиг минимальный: ``` [SERVICE] Flush 5 Log_Level info

[INPUT] Name systemd Tag host.journal

[OUTPUT] Name es Match * Host elastic.local Port 9200 ```

Fluent Bit создан для встраиваемых систем — у него маленький footprint и он написан на C, без интерпретаторов .

Вариант 3: хитрый гибрид

В одном проекте мы делали так:

  • Основные логи (ошибки, авторизация) уходили на сервер немедленно по syslog TCP.
  • Отладочные логи (debug-уровень) писались только в RAM-буфер.
  • При падении устройства специальный скрипт перед ребутом скидывал RAM-лог на сервер по HTTP.

Звучит сложно, но реализуется просто: скрипт-враппер, который дёргает logread и шлёт POST-запрос.

Как не угробить flash-память

Я уже упоминал, что flash не вечна. Вот правила, которые мы вынесли кровью:

  1. Никаких логов на flash по умолчанию. Только RAM-диски.
  2. Если очень надо писать на диск — используйте журналирующую файловую систему (UBIFS, F2FS) и обязательно ротацию логов.
  3. Ротацию настраивайте агрессивно: logrotate должен крутить логи хоть каждый час, если они пишутся интенсивно. Держите максимум пару старых файлов.
  4. Отключайте синхронную запись. Для syslog-ng есть опция sync(n), где n — количество сообщений перед сбросом на диск. Чем выше n, тем меньше записи, но выше риск потерять при крахе.

В одном промышленном контроллере мы вообще вынесли логи на внешнюю SD-карту, а внутреннюю eMMC оставили только для системы. SD-карта дешевле, и заменить её проще, чем перепаивать чип.

Структурированные логи — это must have

Когда у вас 50 устройств в поле, читать плоские текстовые логи — адский труд. Поэтому мы давно перешли на JSON.

Пример того, что шлёт устройство: json {"ts":"2025-06-10T08:23:45Z","host":"sensor-12","level":"error","evt":"modbus_read_failed","addr":247,"code":2}

Почему JSON: - Его автоматом парсят Elasticsearch, Loki и даже банальный jq. - Можно добавлять поля без слома обратной совместимости. - Удобно фильтровать и агрегировать.

Как генерировать JSON на устройстве? Легко. Если у вас приложение на Python — используйте стандартный модуль json. На C — есть парсеры вроде jsmn или генераторы типа frozen. Но чаще всего мы пишем просто snprintf в буфер — для пары десятков полей оверхед минимален.

Вот пример из продакшена (C на ESP32 с Linux): c void log_event(const char *evt, int value) { char buf[256]; snprintf(buf, sizeof(buf), "{\"ts\":%lu,\"evt\":\"%s\",\"val\":%d}\n", time(NULL), evt, value); fputs(buf, stderr); // stderr уходит в systemd }

Трейсинг — следующий уровень

Когда логи структурированы, следующий шаг — связать их в цепочки запросов. Для этого добавляем trace_id и span_id в каждое сообщение .

В распределённой системе (условный шлюз + десяток датчиков) это позволяет отследить путь конкретной команды. Мы используем OpenTelemetry-совместимый формат, хотя на самих устройствах не ставим агентов — просто генерируем ID и шлём их в логах.

Безопасность логов

Не буду много философствовать, перечислю только базовые принципы :

  • Логи должны уезжать с устройства. Если устройство украдут или взломают, логи на сервере уцелеют.
  • По возможности используйте TLS для передачи. Rsyslog и Fluent Bit это умеют.
  • Не пишите секреты. Никаких паролей, токенов, ключей в логах. Фильтруйте на этапе генерации.
  • Подписывайте критичные логи. Для финансовых или медицинских устройств иногда требуется доказывать, что лог не подделан. Это уже экзотика, но бывает.

Что в итоге

Сбор логов на встраиваемых Linux-системах — это компромисс между памятью, живучестью flash и диагностической ценностью.

За эти годы я выработал для себя шаблон, который применяю в 90% проектов:

  1. На устройстве — journald в режиме volatile с буфером 16–32 МБ.
  2. Локально ничего не храним, только оперативка.
  3. Настроен агрессивный сбор логов с помощью Fluent Bit, который шлёт JSON в центральный Elasticsearch (или просто syslog на сервер).
  4. На сервере — автоматическая ротация и дашборды в Grafana.

Такая схема пережила уже не одно поколение устройств, и ни разу мы не потеряли критичные логи. И flash-память жива.