Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 9 из 25 ~18 мин +50 XP

Ротация логов: lumberjack

Связка lumberjack zap решает то, о чём молчат пресеты: файл логов растёт без границ, пока однажды диск не кончится. В этом уроке подключим пакет lumberjack к zap как WriteSyncer, разберём его пять полей конфигурации, посмотрим, что физически происходит при ротации, и где здесь сидят классические ловушки — от прав доступа до «удалённого, но живого» файла.

Содержание урока · 12 разделов

Сервис уходит в прод и месяц пишет логи в 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 не исполняется.

Минимальное подключение

GO
package 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-файлы будут лежать дальше, сколько бы дней им ни исполнилось.

Права доступа: файл создаётся с 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 заданы явно — нулевые значения оставлены только осознанно

Продуктивная конфигурация целиком

GO
w := 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 куда-то не спасёт — записи полетят в переименованный файл, как ни называй.

Соберите конфиг lumberjack

Восстановите поля ротации: файл, лимит размера, возраст архивов и сжатие.

w := zapcore.AddSync(&lumberjack.{ Filename: "logs/app.log", : 50, : 14, : true, }) core := zapcore.NewCore( zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()), w, zapcore.InfoLevel, )

Соберите подключение lumberjack к zap

Расставьте строки: от приёмника до первой записи.

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Проверка: права после ротации

Вы выставили текущему app.log права 0644. Что станет с правами файла после ротации?

Карточки: ротация логов

Термин

lumberjack.Logger

нажмите, чтобы перевернуть
Определение

Ротируемый io.Writer для zap: Filename, MaxSize, MaxBackups, MaxAge, Compress, LocalTime. Не логгер — умный приёмник байтов.

1 / 6

Что дальше по курсу#

Механику приёмников без ротации — 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, кто держит дескриптор, и закончите эксперимент корректной ротацией.

Личные заметкиПусто
Хранятся только в вашем браузере (Zap · Ротация логов: lumberjack)

Вопросы и ответы по уроку

Можно ли ротировать по времени, а не по размеру?

lumberjack.v2 режет только по размеру — MaxAge влияет лишь на чистку старых архивов. Если нужна строго суточная ротация, берите внешние инструменты или форки с таймером, но для лимита объёма размерной ротации обычно достаточно.

Почему не OutputPaths из zap.Config?

OutputPaths принимает пути и схемы, которые zap открывает сам, и держит файлы открытыми до конца жизни логгера — подсовывать туда ротируемый приёмник нельзя. lumberjack подключается через ручное ядро: AddSync поверх lumberjack.Logger в zapcore.NewCore.

Куда деть ошибку, если ротация упала?

Из Write ошибка возвращается в zap и попадает в ErrorOutputPaths. Направьте служебный канал в stderr или отдельный файл, за которым присматривает мониторинг, — в тот же lumberjack-файл его отправлять нельзя.

Термины урока

Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.

Сравните с альтернативами

Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.

Следующая ступень учебного пути

Colly25 уроков~9 чпрактика25 уроков уже доступны

Colly: парсинг и веб-скрапинг на Go

Интерактивный курс Colly — фреймворка веб-скрапинга для Go: селекторы, пагинация, формы, прокси, лимиты, кэш, хранение данных. 25 уроков и итоговый проект.

  • Писать скраперы на Colly от первого Visit до обхода целого сайта
  • Вынимать данные через CSS-селекторы и складывать в структуры Go
  • Настраивать темп: задержки, параллелизм, лимиты на домен
  • Работать с капризными сайтами: заголовки, cookies, прокси