Введение: почему логирование в Go — это не просто fmt.Println
Логирование — это глаза и уши любого приложения, работающего в production. В мире Go (Golang) подход к логированию претерпел значительную эволюцию: от простого вывода в консоль через fmt.Println до мощных структурных логгеров, способных выдавать JSON-совместимые записи для систем централизованного сбора логов (ELK, Grafana Loki, Datadog).
В этой статье мы разберем лучшие практики логирования в Go на практике, а не в теории. Вы узнаете, как настроить уровни логирования, использовать структурированные логи, избегать "шума" и правильно обрабатывать ошибки. Все примеры кода готовы к использованию в ваших проектах.
1. Стандартная библиотека log: когда её достаточно, а когда нет
Go предоставляет встроенный пакет log из стандартной библиотеки. Он прост, но имеет существенные ограничения.
Базовый пример
package main
import (
"log"
"os"
)
func main() {
// Настройка вывода в файл
file, _ := os.OpenFile("app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
defer file.Close()
log.SetOutput(file)
log.Println("Приложение запущено")
log.Printf("Пользователь %s авторизован", "admin")
log.Fatal("Критическая ошибка: не удалось подключиться к БД")
}
Плюсы:
- Не требует установки сторонних зависимостей.
- Простота использования для небольших скриптов или CLI-утилит.
Минусы:
- Нет уровней логирования (только Print, Fatal, Panic).
- Формат вывода фиксированный (дата, время, сообщение).
- Нет структурированных полей (ключ-значение).
- Плохо парсится системами сбора логов.
Вывод: для production-сервисов, микросервисной архитектуры или любого приложения, где требуется мониторинг, стандартный log не подходит. Переходите к структурному логированию.
2. Структурное логирование с zerolog: современный стандарт
Библиотека zerolog — один из самых популярных и быстрых логгеров в Go. Она поддерживает уровни логирования, JSON-вывод по умолчанию и минимальное выделение памяти.
Установка
go get -u github.com/rs/zerolog/log
Пример настройки и использования
package main
import (
"os"
"time"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)
func main() {
// Настройка формата времени
zerolog.TimeFieldFormat = time.RFC3339
// Вывод в консоль с цветами (для разработки)
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339})
// Для production — JSON в stdout
// log.Logger = zerolog.New(os.Stdout).With().Timestamp().Logger()
log.Info().Msg("Сервер запущен на порту 8080")
log.Debug().Str("module", "auth").Msg("Попытка входа пользователя")
log.Error().Err(err).Str("user_id", "12345").Msg("Ошибка авторизации")
log.Fatal().Int("attempt", 3).Msg("Превышено количество попыток")
}
Уровни логирования в zerolog
- Trace (
log.Trace()) — для глубокой отладки. - Debug (
log.Debug()) — отладочная информация. - Info (
log.Info()) — обычные события. - Warn (
log.Warn()) — предупреждения (не ошибки). - Error (
log.Error()) — ошибки, требующие внимания. - Fatal (
log.Fatal()) — фатальные ошибки (завершает программу). - Panic (
log.Panic()) — паника с выводом стека.
Лучшая практика: в production установите минимальный уровень Info или Warn, чтобы не захламлять логи debug-сообщениями.
zerolog.SetGlobalLevel(zerolog.InfoLevel)
3. Лучшие практики логирования: структура, контекст и ошибки
Чтобы логи были полезными, а не "шумом", следуйте этим правилам.
3.1. Всегда добавляйте контекст
Лог без контекста — бесполезен. Вместо log.Info().Msg("Ошибка") пишите:
log.Error().
Str("request_id", reqID).
Str("method", "POST").
Int("status", 500).
Err(err).
Msg("Не удалось обработать запрос")
3.2. Не логируйте чувствительные данные
Никогда не выводите пароли, токены, номера кредитных карт или персональные данные (GDPR, 152-ФЗ).
// ПЛОХО
log.Info().Str("password", userPassword).Msg("Логин")
// ХОРОШО
log.Info().Str("user_id", user.ID).Bool("authenticated", true).Msg("Успешный вход")
3.3. Используйте middleware для HTTP-сервера
Автоматически логируйте входящие запросы, статус ответа и время выполнения.
package middleware
import (
"time"
"github.com/rs/zerolog/log"
"net/http"
)
func LoggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
wrapped := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}
next.ServeHTTP(wrapped, r)
log.Info().
Str("method", r.Method).
Str("path", r.URL.Path).
Int("status", wrapped.statusCode).
Dur("duration", time.Since(start)).
Msg("HTTP запрос")
})
}
type responseWriter struct {
http.ResponseWriter
statusCode int
}
func (rw *responseWriter) WriteHeader(code int) {
rw.statusCode = code
rw.ResponseWriter.WriteHeader(code)
}