Логирование в Go приложениях: лучшие практики для начинающих
Логирование — это не просто запись сообщений в консоль. В продакшене логи помогают отслеживать ошибки, анализировать производительность и понимать поведение системы. В этой статье мы разберем лучшие практики логирования в Go (Golang) для начинающих: от стандартной библиотеки до современных структурированных подходов.
Почему логирование важно в Go?
Go — язык для создания высоконагруженных серверных приложений. Без качественного логирования вы не сможете:
- Быстро локализовать баги в production-среде
- Понять последовательность событий при сбое
- Провести аудит безопасности
- Оптимизировать производительность
Хорошее логирование должно быть: структурированным, уровнем (debug, info, warn, error), контекстным (содержать ID запроса, имя функции) и управляемым (ротация, вывод в файл).
1. Начинаем со стандартной библиотеки log
Go предоставляет встроенный пакет log — простой и достаточный для небольших проектов. Вот как он работает:
package main
import (
"log"
"os"
)
func main() {
// Базовая настройка: вывод в stderr
log.SetOutput(os.Stderr)
log.SetPrefix("INFO: ")
log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)
log.Println("Приложение запущено")
log.Printf("Пользователь %s авторизован", "admin")
log.Fatal("Критическая ошибка") // завершает программу с кодом 1
}
Ключевые возможности log:
log.Println()— вывод строкиlog.Printf()— форматированный выводlog.Fatal()— вывод + завершение программыlog.Panic()— вывод + паника
Но у стандартного логгера есть недостатки: нет уровней (debug, info, error), нет структурированного вывода (JSON), нет ротации файлов.
2. Добавляем уровни логирования
Для production-приложений критически важно разделять логи по уровням. Вот минимальная реализация с использованием стандартного пакета:
package main
import (
"log"
"os"
)
type Logger struct {
debug *log.Logger
info *log.Logger
warn *log.Logger
error *log.Logger
}
func NewLogger() *Logger {
flags := log.Ldate | log.Ltime | log.Lshortfile
return &Logger{
debug: log.New(os.Stdout, "DEBUG: ", flags),
info: log.New(os.Stdout, "INFO: ", flags),
warn: log.New(os.Stderr, "WARN: ", flags),
error: log.New(os.Stderr, "ERROR: ", flags),
}
}
func (l *Logger) Debug(msg string, args ...interface{}) {
l.debug.Printf(msg, args...)
}
func (l *Logger) Info(msg string, args ...interface{}) {
l.info.Printf(msg, args...)
}
func (l *Logger) Error(msg string, args ...interface{}) {
l.error.Printf(msg, args...)
}
func main() {
log := NewLogger()
log.Info("Сервер запущен на порту %d", 8080)
log.Debug("Запрос от %s", "192.168.1.1")
log.Error("Не удалось подключиться к БД: %v", "timeout")
}
Совет: В production используйте os.Stderr для ошибок и os.Stdout для обычных логов — это упрощает сбор логов через systemd или Docker.
3. Структурированное логирование (JSON)
В современных системах логи часто собираются в централизованные хранилища (Elasticsearch, Loki). Для этого нужен структурированный вывод, например в JSON. Популярные библиотеки:
- logrus — гибкий, с поддержкой форматтеров
- zap — сверхбыстрый, от Uber
- zerolog — легковесный и быстрый
Пример с logrus:
package main
import (
"os"
"github.com/sirupsen/logrus"
)
func main() {
log := logrus.New()
log.SetOutput(os.Stdout)
log.SetLevel(logrus.DebugLevel)
log.SetFormatter(&logrus.JSONFormatter{})
log.WithFields(logrus.Fields{
"user_id": 12345,
"action": "login",
"ip": "10.0.0.1",
}).Info("Пользователь вошел в систему")
log.WithField("error", "connection refused").Error("Ошибка подключения к БД")
}
Преимущества JSON-логов:
- Машиночитаемый формат — легко парсить
- Поддержка вложенных полей и метаданных
- Совместимость с ELK-стеком и Grafana Loki
Пример вывода:
{"action":"login","ip":"10.0.0.1","level":"info","msg":"Пользователь вошел в систему","time":"2025-03-15T10:30:00Z","user_id":12345}
4. Ротация логов и работа с файлами
Если вы пишете логи в файл, без ротации файл будет расти бесконечно. Лучшие практики:
- Используйте lumberjack для автоматической ротации
- Настройте максимальный размер файла (например, 100MB)
- Установите количество архивов (например, 7)
- Не забывайте про сжатие старых логов
Пример с lumberjack и logrus:
package main
import (
"github.com/sirupsen/logrus"
"gopkg.in/natefinch/lumberjack.v2"
)
func main() {
log := logrus.New()
// Настройка ротации
log.SetOutput(&lumberjack.Logger{
Filename: "/var/log/myapp/app.log",
MaxSize: 100, // мегабайт
MaxBackups: 7, // количество архивов
MaxAge: 30, // дней
Compress: true,
})
log.Info("Логи будут автоматически ротироваться")
log.Warn("Файл не превысит 100MB")
}
Важно: В контейнерах (Docker) обычно выводят логи в stdout/st