zerolog для Go: полное руководство с примерами

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

zerolog для Go: Полное руководство по логированию с нулевым аллоцированием

В мире высоконагруженных Go-приложений производительность имеет первостепенное значение. Каждая лишняя аллокация памяти может привести к увеличению времени работы сборщика мусора (GC) и снижению пропускной способности сервиса. Традиционные библиотеки логирования, такие как log из стандартной библиотеки или популярный logrus, хоть и удобны, но создают значительную нагрузку на кучу (heap), выделяя память под каждый вызов логгера. Именно здесь на помощь приходит zerolog — библиотека логирования для Go, спроектированная с фокусом на нулевое выделение памяти (zero allocation).

zerolog предоставляет разработчикам мощный, гибкий и невероятно быстрый API для структурированного логирования в формате JSON. В отличие от многих аналогов, zerolog использует цепочку вызовов методов (fluent API) и избегает интерфейсов и рефлексии в критических путях, что позволяет достичь производительности, близкой к ручному форматированию строк. Это делает его идеальным выбором для микросервисов, API-шлюзов, прокси-серверов и любого другого ПО, где важна каждая микросекунда и каждый байт памяти.

Библиотека не только быстрая, но и чрезвычайно удобная. Она поддерживает уровни логирования (debug, info, warn, error, fatal, panic), контекстное логирование с помощью Logger.With(), интеграцию с контекстом Go (context.Context), а также гибкую настройку вывода (консольный или JSON). В этой статье мы подробно разберем, как установить zerolog, освоим его основные методы на практических примерах и рассмотрим лучшие практики для достижения максимальной производительности.

Установка

Установка zerolog, как и большинства Go-библиотек, производится с помощью менеджера пакетов go get. Выполните следующую команду в корне вашего проекта (там, где находится файл go.mod):

go get -u github.com/rs/zerolog/log

После выполнения этой команды библиотека будет добавлена в ваш go.mod файл, и вы сможете импортировать её в своем коде.

Быстрый старт: Минимальный рабочий пример

Давайте начнем с самого простого примера, который демонстрирует базовое использование zerolog для вывода сообщения в формате JSON.

package main

import (
    "github.com/rs/zerolog"
    "github.com/rs/zerolog/log"
)

func main() {
    // Устанавливаем глобальный уровень логирования на Debug.
    // По умолчанию уровень Info.
    zerolog.SetGlobalLevel(zerolog.DebugLevel)

    // Простое логирование строки.
    log.Print("Hello, zerolog!")

    // Логирование с уровнем Info.
    log.Info().Msg("Сервис запущен успешно")

    // Логирование с уровнем Warn.
    log.Warn().Msg("Обнаружен потенциально медленный запрос")

    // Логирование с уровнем Error.
    log.Error().Msg("Не удалось подключиться к базе данных")
}

При запуске этого кода вы увидите в консоли JSON-строки, содержащие время, уровень, сообщение и другую служебную информацию. Обратите внимание на цепочку вызовов: log.Info().Msg("..."). Это и есть тот самый fluent API, который делает код лаконичным.

Основные методы и функции

zerolog предоставляет богатый набор методов для гибкого и производительного логирования. Рассмотрим 8 ключевых из них.

1. log.Info(), log.Debug(), log.Warn(), log.Error(), log.Fatal(), log.Panic()

Сигнатура: func Info() *Event (и аналогично для других уровней)

Что делает: Эти методы создают новое событие логирования с соответствующим уровнем. Они возвращают указатель на структуру Event, которую затем можно дополнить полями с помощью других методов (например, Str(), Int()) и завершить вызовом Msg() или Send(). Fatal() завершает программу с кодом 1 после записи лога, а Panic() вызывает панику.

Пример:

package main

import (
    "github.com/rs/zerolog/log"
)

func main() {
    // Debug: для отладочной информации
    log.Debug().Msg("Это отладочное сообщение")

    // Info: для стандартной информации о работе приложения
    log.Info().Msg("Пользователь вошел в систему")

    // Warn: для предупреждений, которые не являются ошибками
    log.Warn().Msg("Используется устаревший API")

    // Error: для ошибок, которые не прерывают работу приложения
    log.Error().Msg("Не удалось найти файл конфигурации")

    // Fatal: для критических ошибок, после которых приложение завершает работу
    // log.Fatal().Msg("Критическая ошибка инициализации")

    // Panic: для ситуаций, которые должны вызвать панику
    // log.Panic().Msg("Неожиданное состояние")
}

2. Event.Str()

Сигнатура: func (e *Event) Str(key string, val string) *Event

Что делает: Добавляет строковое поле (ключ-значение) в текущее событие логирования. Возвращает указатель на тот же Event, что позволяет выстраивать цепочки вызовов.

Пример:

package main

import (
    "github.com/rs/zerolog/log"
)

func main() {
    log.Info().
        Str("user_id", "12345").
        Str("action", "login").
        Msg("Пользователь выполнил действие")
    // Вывод: {"level":"info","user_id":"12345","action":"login","time":"...","message":"Пользователь выполнил действие"}
}

3. Event.Int(), Event.Int64(), Event.Uint()

Сигнатура: func (e *Event) Int(key string, val int) *Event (и аналогично для других числовых типов)

Что делает: Добавляет целочисленное поле в событие. Поддерживаются все основные типы: Int, Int8, Int16, Int32, Int64, Uint, Uint8 и т.д.

Пример:

package main

import (
    "github.com/rs/zerolog/log"
)

func main() {
    log.Info().
        Int("http_status", 200).
        Int64("response_time_ms", 42).
        Msg("HTTP запрос обработан")
}

4. Event.Bool()

Сигнатура: func (e *Event) Bool(key string, val bool) *Event

Что делает: Добавляет булево поле в событие.

Пример:

package main

import (
    "github.com/rs/zerolog/log"
)

func main() {
    log.Info().
        Bool("is_cached", true).
        Msg("Данные получены из кэша")
}

5. Event.Err() / Event.AnErr()

Сигнатура: func (e *Event) Err(err error) *Event и func (e *Event) AnErr(key string, err error) *Event

Что делает: Err(err) добавляет ошибку с ключом "error". AnErr(key, err) добавляет ошибку с произвольным ключом. Если ошибка равна nil, поле не добавляется, что очень удобно.

Пример:

package main

import (
    "errors"
    "github.com/rs/zerolog/log"
)

func main() {
    err := errors.New("connection refused")
    log.Error().
        Err(err).
        Msg("Не удалось выполнить запрос к внешнему сервису")

    // Использование AnErr для именованной ошибки
    log.Error().
        AnErr("db_error", err).
        Msg("Ошибка базы данных")
}

6. Event.Dict()

Сигнатура: func (e *Event) Dict(key string, dict *Dict) *Event

Что делает: Позволяет вложить в лог-событие целый JSON-объект (словарь). Это полезно для структурирования сложных данных. Словарь создается с помощью zerolog.Dict().

Пример:

package main

import (
    "github.com/rs/zerolog"
    "github.com/rs/zerolog/log"
)

func main() {
    log.Info().
        Dict("user", zerolog.Dict().
            Str("id", "user_007").
            Str("role", "admin").
            Bool("active", true),
        ).
        Msg("Информация о пользователе")
    // Вывод: {"level":"info","user":{"id":"user_007","role":"admin","active":true},"time":"...","message":"Информация о пользователе"}
}

7. Logger.With()

Сигнатура: func (l Logger) With() *Context

Что делает: Создает новый контекст логгера, к которому можно добавить поля, которые будут автоматически включены во все последующие сообщения этого логгера. Это ключевой метод для контекстного логирования, например, для добавления request_id ко всем логам в рамках одного HTTP-запроса.

Пример:

package main

import (
    "github.com/rs/zerolog/log"
)

func main() {
    // Создаем дочерний логгер с контекстными полями
    requestLogger := log.With().
        Str("request_id", "req-abc-123").
        Str("service", "user-api").
        Logger()

    // Все сообщения от requestLogger будут содержать эти поля
    requestLogger.Info().Msg("Обработка запроса начата")
    requestLogger.Warn().Msg("Медленный SQL запрос")
    requestLogger.Error().Msg("Ошибка валидации")
}

8. log.Ctx() / Logger.WithContext()

Сигнатура: func Ctx(ctx context.Context) *Logger и func (l Logger) WithContext(ctx context.Context) context.Context

Что делает: Позволяет интегрировать zerolog с контекстом Go (context.Context). WithContext сохраняет логгер в контекст, а Ctx извлекает его оттуда. Это стандартный способ передачи логгера через middleware.

Пример:

package main

import (
    "context"
    "github.com/rs/zerolog"
    "github.com/rs/zerolog/log"
)

func main() {
    // Создаем логгер с контекстным полем
    l := log.With().Str("component", "main").Logger()
    // Сохраняем его в контекст
    ctx := l.WithContext(context.Background())

    // В другой функции извлекаем логгер из контекста
    processRequest(ctx)
}

func processRequest(ctx context.Context) {
    // Извлекаем логгер. Если его нет, возвращается глобальный log.Logger
    logger := zerolog.Ctx(ctx)
    logger.Info().Msg("Функция processRequest вызвана")
}

Реальный практический пример: HTTP-сервер с логированием

Давайте создадим небольшой, но полноценный HTTP-сервер, который демонстрирует все основные возможности zerolog: контекстное логирование, middleware для добавления ID запроса, обработка ошибок и производительный вывод.

package main

import (
    "context"
    "encoding/json"
    "fmt"
    "math/rand"
    "net/http"
    "os"
    "time"

    "github.com/google/uuid"
    "github.com/rs/zerolog"
    "github.com/rs/zerolog/log"
)

// User представляет модель пользователя.
type User struct {
    ID   string `json:"id"`
    Name string `json:"name"`
    Age  int    `json:"age"`
}

// responseWriter обертка для захвата статус-кода ответа.
type responseWriter struct {
    http.ResponseWriter
    statusCode int
}

func newResponseWriter(w http.ResponseWriter) *responseWriter {
    return &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}
}

func (rw *responseWriter) WriteHeader(code int) {
    rw.statusCode = code
    rw.ResponseWriter.WriteHeader(code)
}

// Middleware для добавления request_id и времени выполнения.
func loggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()

        // Генерируем уникальный ID для запроса
        requestID := uuid.New().String()

        // Создаем дочерний логгер с контекстом запроса
        requestLogger := log.With().
            Str("request_id", requestID).
            Str("method", r.Method).
            Str("path", r.URL.Path).
            Str("remote_addr", r.RemoteAddr).
            Logger()

        // Сохраняем логгер в контекст запроса
        ctx := requestLogger.WithContext(r.Context())
        r = r.WithContext(ctx)

        // Оборачиваем ResponseWriter для захвата статуса
        rw := newResponseWriter(w)

        // Вызываем следующий обработчик
        next.ServeHTTP(rw, r)

        // Логируем завершение запроса
        duration := time.Since(start)
        requestLogger.Info().
            Int("status_code", rw.statusCode).
            Dur("duration_ms", duration).
            Msg("HTTP запрос обработан")
    })
}

// Обработчик для GET /api/users
func getUsersHandler(w                    

Рекомендаций