---
title: "Ротация логов: lumberjack"
description: "lumberjack zap: ротация логов go — MaxSize, MaxBackups, MaxAge, Compress, права доступа 0600, мониторинг размера. Подключаем lumberjack к zap как WriteSyncer."
course: "Zap: структурное логирование на Go"
url: "https://golangmaster.ru/courses/zap/lessons/09-rotaciya-lumberjack/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 18
---

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

*Курс «Zap: структурное логирование на Go», урок 9 из 25. ~18 минут. Обновлено 21.09.2026. Источник: https://golangmaster.ru/courses/zap/lessons/09-rotaciya-lumberjack/*

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

Сервис уходит в прод и месяц пишет логи в 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-файлы будут лежать дальше, сколько бы дней им ни исполнилось.

> **MaxBackups: 0 — это хранить ВСЁ.** Частая ошибка: оставили MaxBackups нулевым, полагая, что бэкапов не будет. Наоборот — ноль означает «архивы не чистить совсем», и каталог за пару месяцев штормов обрастает сотнями файлов. Если ограничиваете объём, задавайте MaxBackups и/или MaxAge явно: например, 7 файлов или 14 дней.

### Права доступа: файл создаётся с 0600

lumberjack создаёт файлы с правами 0600 — читать и писать может только владелец, и это не баг, а консервативный дефолт. В реальной эксплуатации он бьёт больно: агент сбора логов (fluent-bit, promtail) часто работает под другим пользователем и молча не видит файл; скрипт дежурного с sudo требует для каждой проверки. Проблема усугубляется после ротации: вы дали chmod 0644 текущему файлу, а на следующий день он уехал в архив, и на его месте — новый 0600, собиратор снова не видно. Штатной ручки «права на файл» в lumberjack.v2 нет, поэтому варианты такие: гонять сборщик от того же пользователя, что и сервис; при старте выставлять umask, под которым размаскируются права группы; либо забрать каталог под контроль внешней ротации, отказавшись от lumberjack. Решение принимайте осознанно — молча нечитаемые файлы логов всплывают в самый неудобный момент расследования.

### Проверка: MaxBackups

**Вопрос:** Что означает MaxBackups: 0 в конфигурации lumberjack.Logger?

- Старые файлы не хранятся вообще — ротация без архивов
- ✅ Старые файлы хранятся все, чистки архивов нет
- Файл ротируется каждые 0 мегабайт, то есть на каждой записи
- Конфигурация считается ошибочной и ротация отключается

Ноль у MaxBackups — «лимита нет»: все архивы остаются на диске, пока их не срежет MaxAge, а если и он нулевой, то не режется ничто. Чтобы ограничить каталог, задавайте MaxBackups или MaxAge явно.

### Мониторинг размера логов

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 добавлены опциями логгера: без них записи не получат ни файл:строку, ни стеки. Запускать такой пример нужно локально.

### Сколько файлов останется в каталоге?

```go
w := &lumberjack.Logger{
        Filename:   "logs/app.log",
        MaxSize:    1,
        MaxBackups: 2,
        MaxAge:     0,
        Compress:   false,
}
// сервис наработал четыре ротации и продолжает писать
```

**Что выведет программа?**

- Один: текущий app.log — архивы не хранятся
- ✅ Три: текущий app.log и два свежайших архива
- Пять: текущий и все четыре архива
- Четыре: текущий и три архива, старейший удалён

MaxBackups: 2 держит ровно два последних архива — при каждой ротации лишние удаляются, MaxAge нулевой не добавляет чистки по возрасту. Итого в каталоге два архива с таймштампами плюс текущий app.log.

### Живой дескриптор: почему rm не освобождает место

Самая коварная ловушка — попытка починить заполненный диск руками. Открываете dmesg, видите, что app.log огромный, делаете rm logs/app.log — и df не двигается: место всё так же занято. Причина в том, что удаление имени не удаляет данные, пока на файл смотрит открытый дескриптор: процесс держит inode, lumberjack продолжает писать в «удалённый» файл, и место освободится только когда сервис перезапустят или случится ротация. С lumberjack это всё равно безопаснее, чем с классическим logrotate: ротация здесь — переименование собственным процессом, дескриптор переоткрывается внутри, ничего между сервисом и файлом не стоит. Но правило запомните: у долгоживущего процесса файлы сначала закрывают или ротируют, и лишь потом удаляют — lsof по имени покажет, кто держит дескриптор. И то же самое в обратную сторону: mv app.log куда-то не спасёт — записи полетят в переименованный файл, как ни называй.

> **lumberjack или logrotate?.** Внешний logrotate копирует и подрезает файл, после чего сервис обязан переоткрыть дескриптор — обычно через сигнал, которого у Go-сервиса нет. lumberjack управляет файлами сам и для типичного Go-приложения честнее. Внешнюю ротацию берите, когда логи пишет чужой процесс, которым вы не владеете.

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

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

```go
w := zapcore.AddSync(&lumberjack.___{
        Filename: "logs/app.log",
        ___:      50,
        ___:      14,
        ___:      true,
})

core := zapcore.NewCore(
        zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()),
        w,
        zapcore.InfoLevel,
)
```

Подсказка: Тип lumberjack.Logger; размер в мегабайтах — MaxSize; возраст в днях — MaxAge; gzip — Compress.

Правильные ответы в пропусках: `Logger`, `MaxSize`, `MaxAge`, `Compress`

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

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

Правильный порядок строк:

1. `os.MkdirAll("logs", 0o755)`
2. `w := zapcore.AddSync(&lumberjack.Logger{Filename: "logs/app.log", MaxSize: 50})`
3. `core := zapcore.NewCore(zapcore.NewJSONEncoder(cfg), w, zapcore.InfoLevel)`
4. `logger := zap.New(core)`
5. `defer logger.Sync()`
6. `logger.Info("service started")`

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

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

- Наследуются — новый файл тоже 0644
- ✅ Новый файл создастся с 0600 — как все файлы lumberjack
- Права возьмутся из umask процесса
- Файл уедет в архив вместе с правами, новый не создастся

lumberjack при открытии нового файла ставит 0600 независимо от старого. Ручной chmod не переживает ротацию — если права критичны, меняйте umask процесса или запускайте сборщик под тем же пользователем.

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

- **lumberjack.Logger** — Ротируемый io.Writer для zap: Filename, MaxSize, MaxBackups, MaxAge, Compress, LocalTime. Не логгер — умный приёмник байтов.
- **MaxSize** — Предел размера текущего файла в МБ; проверяется перед записью, дефолт 100. Одна запись длиннее лимита вернёт ошибку.
- **MaxBackups** — Сколько архивов хранить; 0 — хранить все. Считаются только архивы, текущий файл не в счёт.
- **MaxAge** — Максимальный возраст архивов в днях; 0 — не удалять по возрасту. Срабатывает при следующей ротации.
- **Механика ротации** — Перед записью: файл закрыт, переименован в app-<таймштамп>.log, открыт новый app.log. Синхронно внутри Write, без демонов.
- **Права файлов** — lumberjack создаёт файлы с 0600; ручной chmod не переживает ротацию. Сборщику логов — тот же пользователь или umask.

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

Механику приёмников без ротации — stdout, файлы, мультиплекс и буферизацию — разбирает урок про [приёмники вывода](/courses/zap/lessons/08-vyvody-vhodnye/): lumberjack садится ровно на то же место WriteSyncer. Ротация режет объём по размеру, но не по смыслу: когда шумят ретраи, файл делится на сто кусков, каждый по 50 МБ, — здесь нужен следующий уровень, [сэмплирование логов](/courses/zap/lessons/11-sampling/). Карта курса — на [странице курса](/courses/zap/).

## Коротко: что запомнить

- 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-файл его отправлять нельзя.
