Содержание урока · 12 разделов
- lumberjack zap: зачем и как подключать
- Поля lumberjack.Logger
- Поля lumberjack.Logger и их значения по умолчанию
- Что происходит при ротации
- Права доступа: файл создаётся с 0600
- Мониторинг размера логов
- Чек-лист эксплуатационных проверок
- Живой дескриптор: почему rm не освобождает место
- Что дальше по курсу
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Сервис уходит в прод и месяц пишет логи в logs/app.log. Ничего не предвещает: файл растёт медленно, диска хватает. Потом начинается шторм — партнёрский API начал отдавать таймауты, ретраи сыпят ошибками, и за ночь app.log съедает остатки раздела. Диск заполняется, база отказывается писать, сервис лежит целиком — не из-за бага, а из-за файла логов, который никто не ограничил. zap сам это чинить не будет: он честно пишет в то, что ему дали, — а открыли вы ему обычный файл. Ротация логов — это про дисциплину этого файла: разрезаем поток на куски ограниченного размера, старое сжимаем и удаляем по возрасту. Стандартный приём в мире zap — пакет lumberjack от Nate Finch: он не логгер, а умный io.Writer, который отдаёт записи zap, а сам следит за размером файла. Разберём подключение, поля, механику ротации и мониторинг.
lumberjack zap: зачем и как подключать#
В седьмом уроке мы задавали приёмники через OutputPaths — это файлы, которые zap открывает и держит открытыми; менять их на лету он не умеет, да и не должен. Ротация логов go — задача уровня файловой системы, и zap принципиально её не тащит в ядро: вместо этого приёмником может быть любой объект с методом Write. Связка lumberjack zap устроена именно так: структура lumberjack.Logger реализует io.Writer, обёртывается в WriteSyncer одной строкой zapcore.AddSync и садится в ядро, которое мы собирали в уроке про zapcore. Получается честное разделение труда: zap решает, что логировать и в каком формате, lumberjack — куда физически кладутся байты и когда файл пора резать. Пара замечаний перед кодом. Первое: lumberjack не читает zap.Config — ручной конфиг из прошлого урока или WrapCore, третьего пути к нему нет. Второе: пакет не ведёт ротацию по времени и не умеет внешних сигналов — только размер, и это надо принимать как дизайн, а не бороться. Установка на вашем окружении: go get gopkg.in/natefinch/lumberjack.v2 — и для запуска примеров, как и со всем библиотечным курсом, понадобится локальный Go, в песочнице проекта zap не исполняется.
Минимальное подключение
GOpackage main
import (
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"gopkg.in/natefinch/lumberjack.v2"
)
func main() {
w := zapcore.AddSync(&lumberjack.Logger{
Filename: "logs/app.log",
})
core := zapcore.NewCore(
zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()),
w,
zapcore.InfoLevel,
)
logger := zap.New(core)
defer logger.Sync()
logger.Info("пишем в ротируемый файл", zap.String("service", "shop"))
}lumberjack.Logger создаётся с единственным обязательным полем Filename — файл logs/app.log появится при первой записи. zapcore.AddSync оборачивает io.Writer в WriteSyncer, добавляя пустой Sync, и ядро собирается как в предыдущем уроке. Важно: каталог logs должен существовать заранее — lumberjack не создаёт папки, при отсутствующем пути первая запись упадёт с ошибкой.
Поля lumberjack.Logger#
Вся конфигурация lumberjack — пять полей структуры, и у каждого есть нулевое значение, которое означает «правило не активно». Filename — базовый файл, в который пишется текущий поток; имя носит всегда именно он, а не бэкапы. MaxSize — предельный размер в мегабайтах: при превышении на очередной записи файл уезжает в архив и открывается новый; ноль или отсутствие поля означает дефолт в 100 МБ. MaxBackups — сколько старых файлов хранить, счёт идёт по архивам, текущий файл не считается; ноль означает хранить всё — и это ловушка новичков, которые читают ноль как «не хранить бэкапов вовсе». MaxAge — максимальный возраст архивов в днях: что старше, то удаляется при следующей ротации, независимо от MaxBackups. Compress — сжимать ли архивы gzip: файл получает суффикс .gz, и типичный лог сжимается в пять-десять раз. Есть и LocalTime — по умолчанию метки в именах архивов ставятся в UTC.
Поля lumberjack.Logger и их значения по умолчанию#
| Поле | Смысл | Нулевое значение |
|---|---|---|
| Filename | имя текущего файла логов | обязательное |
| MaxSize | предел размера в МБ перед ротацией | 100 МБ |
| MaxBackups | сколько архивов хранить | 0 — хранить все |
| MaxAge | максимальный возраст архивов в днях | 0 — не удалять |
| Compress | сжимать архивы gzip (.gz) | false |
| LocalTime | метки времени в именах по локальному времени | false — UTC |
Что происходит при ротации#
Механика простая и потому предсказуемая. Перед каждой записью lumberjack смотрит на текущий размер файла: если свежая запись перевалит за MaxSize, файл закрывается, переименовывается в logs/app-2024-11-02T15-04-05.312.log — в имя зашивается момент ротации, — и открывается новый app.log. Всё это происходит синхронно внутри вызова Write: между записями zap и архивом нет фонового демона, таймера или cron. Отсюда следствие, о котором редко думают: MaxSize проверяется не постфактум, а перед записью, поэтому файл может стать чуть больше лимита, но ровно на одну запись; и отдельная крайность — если одна запись сама длиннее MaxSize мегабайт, lumberjack вернёт ошибку, а не запишет её куском. Пропорции держите разумные: лимит в мегабайтах при записях в сотни байт даёт чистую картину. После ротации в каталоге лежат текущий app.log с постоянным именем и архивы с таймштампами — старое вычищается по MaxBackups и MaxAge уже при следующей ротации, не раньше. И ещё одна деталь на память: удаление архивов случается только в момент ротации — если поток записей встрял, aged-файлы будут лежать дальше, сколько бы дней им ни исполнилось.
Частая ошибка: оставили MaxBackups нулевым, полагая, что бэкапов не будет. Наоборот — ноль означает «архивы не чистить совсем», и каталог за пару месяцев штормов обрастает сотнями файлов. Если ограничиваете объём, задавайте MaxBackups и/или MaxAge явно: например, 7 файлов или 14 дней.
Права доступа: файл создаётся с 0600#
lumberjack создаёт файлы с правами 0600 — читать и писать может только владелец, и это не баг, а консервативный дефолт. В реальной эксплуатации он бьёт больно: агент сбора логов (fluent-bit, promtail) часто работает под другим пользователем и молча не видит файл; скрипт дежурного с sudo требует для каждой проверки. Проблема усугубляется после ротации: вы дали chmod 0644 текущему файлу, а на следующий день он уехал в архив, и на его месте — новый 0600, собиратор снова не видно. Штатной ручки «права на файл» в lumberjack.v2 нет, поэтому варианты такие: гонять сборщик от того же пользователя, что и сервис; при старте выставлять umask, под которым размаскируются права группы; либо забрать каталог под контроль внешней ротации, отказавшись от lumberjack. Решение принимайте осознанно — молча нечитаемые файлы логов всплывают в самый неудобный момент расследования.
Проверка: MaxBackups
Что означает MaxBackups: 0 в конфигурации lumberjack.Logger?
Мониторинг размера логов#
lumberjack ничего не экспортирует наружу — ни метрик, ни событий ротации, он просто режет файлы. Значит, мониторинг размера — ваша забота, и список проверок короткий. Во-первых, df на раздел с логами: даже при ротации архивы суммируются, а MaxAge не сработает, пока не случится следующая ротация — в спокойный сервис без ошибок каталог может молча накапливаться неделями. Во-вторых, du по каталогу логов как метрика: сумма размеров app.log и архивов в алерте — простейший экспортер на shell или Node exporter с textfile-коллектором. В-третьих, сами записи lumberjack: если файл не открылся или ротация упала, lumberjack вернёт ошибку из Write — zap уйдёт с ней в ErrorOutputPaths, и это единственный канал, где сбой ротации виден в логах. Не отправляйте ErrorOutputPaths в тот же lumberjack-файл: смерть диска убьёт оба канала разом. Отдельный вопрос — сжатие: Compress: true стоит CPU на gzip при каждой ротации, зато экономит диск кратно; на нагруженном сервисе это ощутимо в обе стороны, считайте при выборе.
Чек-лист эксплуатационных проверок#
- df на раздел с логами в алерте — заполнение диска ложится на всё, не только на логи
- du -sh по каталогу логов как метрика: рост архива виден до инцидента
- ErrorOutputPaths НЕ в тот же файл: ошибка ротации должна быть видна в живом канале
- сборщик логов запущен под пользователем сервиса или права размаскированы системно
- MaxBackups и MaxAge заданы явно — нулевые значения оставлены только осознанно
Продуктивная конфигурация целиком
GOw := zapcore.AddSync(&lumberjack.Logger{
Filename: "logs/app.log",
MaxSize: 50, // мегабайт на файл
MaxBackups: 7, // хранить 7 архивов
MaxAge: 14, // но не дольше 14 дней
Compress: true, // архивы в .gz
})
core := zapcore.NewCore(
zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()),
w,
zapcore.InfoLevel,
)
logger := zap.New(core, zap.AddCaller(), zap.AddStacktrace(zap.ErrorLevel))
defer logger.Sync()
logger.Error("payment failed", zap.String("order_id", "a-77"))Типовой прод-набор: файл по 50 МБ, семь архивов или четырнадцать дней — что наступит раньше, gzip на архивах. Ядро собирается вручную, как в уроке про zapcore, зато приёмник — ротируемый. AddCaller и AddStacktrace добавлены опциями логгера: без них записи не получат ни файл:строку, ни стеки. Запускать такой пример нужно локально.
Сколько файлов останется в каталоге?
Прочитайте код и выберите, что он выведет:
w := &lumberjack.Logger{
Filename: "logs/app.log",
MaxSize: 1,
MaxBackups: 2,
MaxAge: 0,
Compress: false,
}
// сервис наработал четыре ротации и продолжает писатьЖивой дескриптор: почему rm не освобождает место#
Самая коварная ловушка — попытка починить заполненный диск руками. Открываете dmesg, видите, что app.log огромный, делаете rm logs/app.log — и df не двигается: место всё так же занято. Причина в том, что удаление имени не удаляет данные, пока на файл смотрит открытый дескриптор: процесс держит inode, lumberjack продолжает писать в «удалённый» файл, и место освободится только когда сервис перезапустят или случится ротация. С lumberjack это всё равно безопаснее, чем с классическим logrotate: ротация здесь — переименование собственным процессом, дескриптор переоткрывается внутри, ничего между сервисом и файлом не стоит. Но правило запомните: у долгоживущего процесса файлы сначала закрывают или ротируют, и лишь потом удаляют — lsof по имени покажет, кто держит дескриптор. И то же самое в обратную сторону: mv app.log куда-то не спасёт — записи полетят в переименованный файл, как ни называй.
Внешний logrotate копирует и подрезает файл, после чего сервис обязан переоткрыть дескриптор — обычно через сигнал, которого у Go-сервиса нет. lumberjack управляет файлами сам и для типичного Go-приложения честнее. Внешнюю ротацию берите, когда логи пишет чужой процесс, которым вы не владеете.
Соберите конфиг lumberjack
Восстановите поля ротации: файл, лимит размера, возраст архивов и сжатие.
Соберите подключение lumberjack к zap
Расставьте строки: от приёмника до первой записи.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Проверка: права после ротации
Вы выставили текущему app.log права 0644. Что станет с правами файла после ротации?
Карточки: ротация логов
Что дальше по курсу#
Механику приёмников без ротации — stdout, файлы, мультиплекс и буферизацию — разбирает урок про приёмники вывода: lumberjack садится ровно на то же место WriteSyncer. Ротация режет объём по размеру, но не по смыслу: когда шумят ретраи, файл делится на сто кусков, каждый по 50 МБ, — здесь нужен следующий уровень, сэмплирование логов. Карта курса — на странице курса.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- zap не ротирует файлы сам: приёмником может быть любой io.Writer, и lumberjack — стандартный выбор.
- lumberjack.Logger: Filename, MaxSize (МБ, дефолт 100), MaxBackups (0 — хранить все), MaxAge (дни), Compress (.gz).
- Ротация синхронна внутри Write: файл переименовывается с таймштампом, открывается новый с постоянным именем.
- Файлы создаются с правами 0600 — chmod не переживает ротацию, сборщику нужен тот же пользователь или umask.
- rm не освобождает место при живом дескрипторе: файл закрывают или ротируют до удаления.
- Мониторинг — на вас: df, du как метрика и ErrorOutputPaths как канал сбоев ротации.
- Catalog logs должен существовать до первой записи: lumberjack не создаёт каталоги.
Домашнее задание
Подключите lumberjack к zap локально: файл logs/app.log, MaxSize 1 МБ, MaxBackups 3, MaxAge 7, Compress true. Нагенерируйте записей циклом до двух-трёх ротаций и посмотрите каталог: найдите текущий файл и архивы с таймштампами. Затем удалите текущий файл командой rm и проверьте, что место не освободилось, — разберитесь через lsof, кто держит дескриптор, и закончите эксперимент корректной ротацией.
Личные заметкиПусто
Вопросы и ответы по уроку
Можно ли ротировать по времени, а не по размеру?
lumberjack.v2 режет только по размеру — MaxAge влияет лишь на чистку старых архивов. Если нужна строго суточная ротация, берите внешние инструменты или форки с таймером, но для лимита объёма размерной ротации обычно достаточно.
Почему не OutputPaths из zap.Config?
OutputPaths принимает пути и схемы, которые zap открывает сам, и держит файлы открытыми до конца жизни логгера — подсовывать туда ротируемый приёмник нельзя. lumberjack подключается через ручное ядро: AddSync поверх lumberjack.Logger в zapcore.NewCore.
Куда деть ошибку, если ротация упала?
Из Write ошибка возвращается в zap и попадает в ErrorOutputPaths. Направьте служебный канал в stderr или отдельный файл, за которым присматривает мониторинг, — в тот же lumberjack-файл его отправлять нельзя.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Сравните с альтернативами
Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.
Изучайте дальше
Следующая ступень учебного пути
Colly: парсинг и веб-скрапинг на Go
Интерактивный курс Colly — фреймворка веб-скрапинга для Go: селекторы, пагинация, формы, прокси, лимиты, кэш, хранение данных. 25 уроков и итоговый проект.
- Писать скраперы на Colly от первого Visit до обхода целого сайта
- Вынимать данные через CSS-селекторы и складывать в структуры Go
- Настраивать темп: задержки, параллелизм, лимиты на домен
- Работать с капризными сайтами: заголовки, cookies, прокси