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 Также в библиотеке
JWT токены в Swift: работа с JSON Web Token на iOS/macOS
WebSocket библиотека на C++: полное руководство по выбору и использованию