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

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

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

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

Ротация: lumberjack рядом с zerolog

Ротация логов на Go рядом с zerolog не требует интеграции: lumberjack — это io.Writer, который сам следит за размером файла и делит его на куски. Разберём связку lumberjack zerolog: MaxSize, MaxBackups и MaxAge, фоновое сжатие, принудительную ротацию по SIGHUP и бюджет диска до инцидента.

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

У файла из прошлого урока есть свойство, которое не заметно в первый месяц: он растёт. Сервис с ровным потоком в пару мегабайт в час за год отложит на диск пятнадцать-двадцать гигабайт, и закончиться это может в самый неподходящий момент — в ночь на чёрную пятницу, когда логи как раз нужны. Классическое решение на уровне системы — демон logrotate: раз в сутки (или по размеру) он переименовывает файл, сжимает старые копии и просит сервис переоткрыть журнал. Работает, но требует конфига вне кода, прав на перезапуск сервисов и аккуратности с переоткрытием. Есть решение проще — держать ротацию в руках самого процесса: библиотека lumberjack встраивается в Go-программу и превращает файл логов в самоподдерживающееся кольцо: переполнился — переименовался, открылся заново, старые куски сжал и почистил. Для zerolog интеграции нет, потому что она не нужна: lumberjack — это io.Writer, и передаётся он в zerolog.New как обычный файл — прямое продолжение приёма из урока про запись в файл. Ни одной строки кода логирования менять не придётся — меняется только writer при старте, и диск перестаёт быть расходником.

lumberjack zerolog: writer с ротацией на лету#

Библиотека живёт по пути gopkg.in/natefinch/lumberjack.v2 и выставляет наружу одну структуру — lumberjack.Logger с полями-настройками. В связке lumberjack zerolog она играет роль writer-а: logger := zerolog.New(lumber).With().Timestamp().Logger() — и всё, дальше обычный код событий. Внутри lumberjack.Logger — мьютекс, открытый файл и текущий размер. Каждый Write, который приходит от zerolog, проверяет: влезет ли эта строка в лимит MaxSize; если нет — файл закрывается, переименовывается с меткой времени в имени, открывается новый с прежним именем, и только затем запись дописывается уже в новый файл. Ротация происходит прямо между записями, на лету, без остановки сервиса и без сигналов. Текущий файл всегда носит имя из Filename — logs/app.log, — а бэкапы получают суффикс с датой и временем. Для системы сбора это значит: хвостить нужно одно и то же имя, а появившиеся рядом файлы с метками — архив, который можно читать, не боясь, что в него пишут. Ровно на этом контракте строятся filebeat, promtail и простой scp-скрипт дежурного.

Zerolog с lumberjack: вся настройка

GO
package main

import (
        "github.com/rs/zerolog"
        "gopkg.in/natefinch/lumberjack.v2"
)

func main() {
        lumber := &lumberjack.Logger{
                Filename:   "logs/app.log",
                MaxSize:    10, // мегабайты
                MaxBackups: 5,
                MaxAge:     30, // дней
                Compress:   true,
        }
        defer lumber.Close()

        logger := zerolog.New(lumber).With().Timestamp().Logger()

        logger.Info().Str("user", "ann").Msg("job done")
}

// logs/app.log — текущий файл, имя не меняется
// logs/app-2024-04-06T12-00-00.000.log — переименован при ротации
// logs/app-2024-04-06T12-00-00.000.log.gz — сжат фоновой горутиной

lumberjack.Logger создаётся с адресом: Write у него на указателе, потому что внутри мьютекс и mutable-размер. Пять полей задают весь жизненный цикл журнала; всё остальное lumberjack решает сам при записи. Логгер zerolog получает lumber как обычный io.Writer и не отличает его от *os.File из прошлого урока.

MaxSize, MaxBackups, MaxAge: дефолты и смысл#

Пять полей структуры делятся на два типа: пороги и уборка. MaxSize — максимальный размер текущего файла в мегабайтах перед ротацией; дефолт 100 МБ. Внимание к единицам: поле целое и измеряется именно мегабайтами — MaxSize: 10 это десять мегабайт, и наивное «10 для десяти килобайт» сделает файл в тысячу раз больше задуманного. MaxBackups — сколько старых файлов хранить; дефолт 0 означает «хранить все», что вместе с нулевым MaxAge ведёт к бесконечному росту архива — именно поэтому число стоит задавать всегда. MaxAge — предельный возраст бэкапа в днях; дефолт 0 — не удалять по возрасту вовсе. Compress включает gzip для переименованных файлов; дефолт false — бэкапы лежат сырыми. LocalTime определяет, в какой зоне штампуется имя бэкапа: дефолт — UTC, и это разумный дефолт для серверов, но при сверке с локальными журналами удобно LocalTime: true. Нюанс уборки: удаление старых и сжатие выполняются фоново, после ротации — не блокируя запись; поэтому сжатый .gz может появиться чуть позже самого переименования.

Механика внутри полезна для понимания границ. Write блокируется мьютексом — параллельные записи из горутин сериализуются, и размер не врёт. Если одна запись сама больше MaxSize, lumberjack вернёт ошибку вида write length 2000000 exceeds maximum file size 1000000 — строки логов обычно крошечные, но протечёт это место, только если кто-то логирует гигантский payload одним куском; отдельная тема — гигантские поля, у zerolog они разбираются в уроке про поля и секреты. Каталог под журнал создаётся автоматически с правами 0755, сам файл — 0600: только владелец процесса пишет и читает, что резко снижает интерес посторонних к вашим логам. Права нового файла lumberjack копирует с прежнего журнала, если тот уже существовал, — ручной chmod переживает ротации. Имена бэкапов формируются из текущего времени в формате с точностью до миллисекунд: app-2024-04-06T12-00-00.000.log — сортировка по имени равна сортировке по времени, и любые скрипты очистки работают без парсинга; метка в имени — это момент самой ротации, а не время первой записи, по именам видно, когда именно журнал делился на куски. Всё это происходит синхронно с записью; уборка — сжатие и удаление лишних — уезжает в отдельную горутину и догоняет по мере сил, так что всплеск ротации не тормозит горячий путь.

Правила ротации одним списком#

  • MaxSize измеряется мегабайтами: 10 — это 10 МБ; проверьте единицы, прежде чем удивляться размеру файлов
  • MaxBackups: 0 хранит все бэкапы — задайте число, диск конечен
  • MaxAge: 0 не удаляет по возрасту; для журналов с ретеншеном задайте оба порога
  • Compress жмёт только переименованные бэкапы — текущий файл всегда сырой
  • имена бэкапов штампуются в UTC; для сверки с локальными журналами — LocalTime: true
  • бюджет места примерно MaxSize × (MaxBackups + 1) — считайте его до инцидента, а не после
  • одна запись больше MaxSize вернёт ошибку: не опускайте лимит ниже самой большой строки лога

Арифметика диска: бюджет до инцидента#

Ротация превращает неограниченный рост в константу, и эту константу стоит назвать до того, как диск назовёт её сам. Верхняя граница занятого места — MaxSize, умноженный на количество текущих и резервных файлов: MaxSize: 100 при MaxBackups: 5 — это до шестисот мегабайт в худшем случае, Compress отыгрывает ещё в разы на текстовых JSON-логах. Пороги работают независимо: по документации lumberjack файлы старше MaxAge дней удаляются независимо от MaxBackups, поэтому мягкий ретеншен задаёт MaxAge, а жёсткий потолок места — именно MaxBackups. Дальше обратная задача: сколько пишет ваш сервис? Оцените средний размер записи — у zerolog со структурными полями это обычно двести-четыреста байт, — умножьте на события в час и получите мегабайты в сутки; от них вверх возьмите MaxSize. Если сервис на пике пишет гигабайты в час, сначала уберите объём, а не наращивайте диск: прореживание потока из урока про sampling режет повторяющиеся записи до того, как они доедут до writer-а, и ротация становится техникой, а не спасением. Худшее, что может случиться без этой арифметики, — не переполнение диска само по себе, а то, что вместе с логами место кончится у базы данных, живущей на той же машине.

Принудительная ротация по SIGHUP

GO
package main

import (
        "os"
        "os/signal"
        "syscall"

        "github.com/rs/zerolog"
        "gopkg.in/natefinch/lumberjack.v2"
)

func main() {
        lumber := &lumberjack.Logger{Filename: "app.log", MaxSize: 50}
        defer lumber.Close()

        logger := zerolog.New(lumber).With().Timestamp().Logger()

        sig := make(chan os.Signal, 1)
        signal.Notify(sig, syscall.SIGHUP)

        go func() {
                for range sig {
                        if err := lumber.Rotate(); err != nil {
                                logger.Error().Err(err).Msg("rotate failed")
                        }
                }
        }()

        logger.Info().Msg("ready: SIGHUP rotates the log")
        select {}
}

Rotate() закрывает текущий файл, переименовывает его и открывает новый — та же механика, что и автоматическая ротация, но по требованию. Классический повод — SIGHUP от внешнего logrotate или скрипта завершения смены. Close в defer закрывает журнал при остановке; ошибка ротации логируется тем же zerolog с Err.

Чего lumberjack не делает#

Границы библиотеки важнее её возможностей, потому что их узнают по факту. Lumberjack не пишет по сети: это файловый writer, и для Loki или Kafka нужен сборщик-агент, читающий каталог. Он не ротирует по дням автоматически: порог у него один — размер, а календарную разбивку делают своим кодом — меняйте Filename на dated-имя и вызывайте Rotate() в полночь, если формат дневных журналов обязателен. Он ничего не знает о содержимом: не режет спам, не разметит поля, не пришлёт алерт — всё это уровни логгера и сэмплеры zerolog. Наконец, он не координируется с соседними процессами: два сервиса на один Filename сообща будут устраивать гонку за файл — каждому сервису свой путь, это дешёвая дисциплина. Для типового сервиса всё перечисленное не нужно: размерный порог, уборка, сжатие покрывают девять случаев из десяти, а остальное — работа агента на стороне читателя. Честный вывод: lumberjack решает ровно одну задачу — диск не переполнится, — и решает её без движения со стороны zerolog. Проверить связку руками можно за пять минут, и это стоит сделать локально: песочница сайта — тематическая программа на чистом Go, файловые операции и сигналы в ней недоступны. Соберите программу из первого примера с намеренно крошечным лимитом — MaxSize: 1, — закиньте в цикл тысячу записей и посмотрите ls logs/: увидите свежие бэкапы с метками в именах и одиночный app.log, в который пишется прямо сейчас. Затем выполните gzip -t на бэкапе, чтобы убедиться в валидности сжатия, и подождите пару секунд с MaxBackups: 2 — лишние куски исчезнут фоном. На этой же программе проверьте tail -F против tail -f: запустите хвост, сгенерируйте ротацию и посмотрите, кто из них продолжит показывать новые записи. Пятиминутный прогон надолго снимает вопросы «а как оно выглядит».

Поля lumberjack.Logger: дефолты и смысл#

ПолеТипДефолтЧто делает
Filenamestringtemp-lumberjack.log в os.TempDir()имя текущего файла журнала
MaxSizeint100порог ротации в мегабайтах
MaxBackupsint0 — хранить всесколько старых файлов держать
MaxAgeint0 — не удалятьпредельный возраст бэкапа в днях
Compressboolfalsegzip для переименованных бэкапов
LocalTimeboolfalse — UTCзона временной метки в имени бэкапа

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

Ротация — последний кирпич локального хранения: в уроке про запись в файл лежит базовая связка с os.OpenFile, а здесь она обросла жизненным циклом. Дальше два пути: продовая эксплуатация — что мониторить и куда отдавать записи, разберёт урок про логи на проде, — и экономия потока до writer-а: сэмплеры из урока о прореживании убирают повторяющийся спам так, что ротация успевает дышать. Практический тест на сегодня: соберите сервис из урока с MaxSize: 1 и MaxBackups: 2, прогоните пару тысяч записей и посчитайте занятое место в каталоге. Совпало с формулой MaxSize × (MaxBackups + 1)? Диск теперь под контролем, и это главный результат урока.

Проверка: место lumberjack в zerolog

Как lumberjack подключается к zerolog?

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

Что означает MaxBackups: 0 в lumberjack.Logger?

Какое имя получит бэкап после ротации?

Прочитайте код и выберите, что он выведет:

lumber := &lumberjack.Logger{
        Filename: "app.log",
        MaxSize:  1,
}

// файл дописался до порога — запись вызвала ротацию

Что вернёт Write, если запись больше MaxSize?

Прочитайте код и выберите, что он выведет:

lumber := &lumberjack.Logger{Filename: "app.log", MaxSize: 1}

big := make([]byte, 2<<20) // 2 МБ одной записью

_, err := lumber.Write(big)

Соберите связку zerolog с lumberjack

Восстановите программу: пакет-путь, структура, пороги и закрытие.

package import ( "github.com/rs/zerolog" "gopkg.in/natefinch/.v2" ) func main() { lumber := &.{ Filename: "logs/app.log", : 10, MaxBackups: 5, : 30, Compress: true, } defer lumber.() logger := zerolog.New(lumber).With().Timestamp().Logger() logger.Info().Msg("job done") }

Программа с ротацией

Расставьте строки в правильном порядке: импорты, writer с ротацией, логгер и запись.

Строки

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

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

Карточки: ротация

Термин

lumberjack.Logger

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

io.Writer с встроенной ротацией: порог размера, число и возраст бэкапов, gzip. Передаётся в zerolog.New как обычный writer.

1 / 6

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

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

  • Lumberjack — io.Writer с ротацией: передаётся в zerolog.New без единой строки интеграции.
  • Ротация происходит на лету при записи: превышение MaxSize — переименовать, открыть новый, писать дальше.
  • Дефолты опасны для долгоживущего сервиса: MaxBackups: 0 и MaxAge: 0 хранят архив вечно.
  • Бюджет места — примерно MaxSize × (MaxBackups + 1); Compress отыгрывает в разы на текстовых логах.
  • Сжатие и удаление бэкапов идут фоново после ротации и не тормозят запись.
  • Rotate() принудительно делит журнал — классика SIGHUP; Close закрывает файл при остановке.
  • Tail -f отрывается после ротации: следите командой tail -F или агентом, знающим переоткрытие.
  • Чего нет: сетевой отправки, дневной ротации и фильтрации спама — это работа агентов и сэмплеров zerolog.

Домашнее задание

Соберите программу урока с MaxSize: 1 и MaxBackups: 2, прогоните цикл из пары тысяч записей и разберите каталог logs: имена бэкапов, число файлов, занятое место против формулы. Включите Compress и повторите — сравните размеры. Затем подайте процессу SIGHUP и убедитесь, что Rotate создал свежий кусок без записей-двойников. Наконец, запустите tail -f и tail -F рядом с ротацией и запишите для себя, какой из них пережил переименование.

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

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

Как настроить ротацию логов zerolog?

Передайте в zerolog.New писателя lumberjack: lumber := &lumberjack.Logger{Filename: "logs/app.log", MaxSize: 10, MaxBackups: 5, MaxAge: 30, Compress: true}. Файл будет делиться на куски по 10 МБ, останутся пять последних, бэкапы старше 30 дней удалятся, а свежие — сожмутся gzip.

Lumberjack или внешний logrotate — что выбрать?

Lumberjack проще: ротация внутри процесса, без конфигов и сигналов, и работает везде, где живёт бинарник. Внешний logrotate берёт верх, когда политика единая для многих сервисов и языков, когда нужен дневной формат журналов или постобработка после ротации — postrotate-скрипты он умеет, lumberjack о внешних сигналах не знает. Их можно совмещать: lumberjack ведёт размер, logrotate остаётся для остальной системы.

Почему после ротации tail -f перестал показывать новые записи?

Tail -f привязан к inode открытого файла, а lumberjack переименовал его и открыл новый под старым именем — хвост остался читать архив. Tail -F переоткрывает файл по имени и переживает ротацию; агенты вроде filebeat и promtail делают то же самое из коробки.

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

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

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

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

Валидация в Go: go-playground/validator

Курс validator для Go: теги required/min/max, email, dive, вложенные структуры, свои валидаторы, Gin и Echo, русские сообщения, тесты. 25 уроков.

  • Валидировать структуры тегами binding и validate
  • Понимать ValidationErrors и писать понятные сообщения
  • Работать с вложенностью, срезами и картами через dive
  • Писать свои функции-валидаторы и StructLevel-правила