Логи встраиваемых 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 не вечна. Вот правила, которые мы вынесли кровью:
- Никаких логов на flash по умолчанию. Только RAM-диски.
- Если очень надо писать на диск — используйте журналирующую файловую систему (UBIFS, F2FS) и обязательно ротацию логов.
- Ротацию настраивайте агрессивно:
logrotateдолжен крутить логи хоть каждый час, если они пишутся интенсивно. Держите максимум пару старых файлов. - Отключайте синхронную запись. Для 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% проектов:
- На устройстве — journald в режиме
volatileс буфером 16–32 МБ. - Локально ничего не храним, только оперативка.
- Настроен агрессивный сбор логов с помощью Fluent Bit, который шлёт JSON в центральный Elasticsearch (или просто syslog на сервер).
- На сервере — автоматическая ротация и дашборды в Grafana.
Такая схема пережила уже не одно поколение устройств, и ни разу мы не потеряли критичные логи. И flash-память жива.