Привет, Хабр!
Как в Golang логирование поживает? Рассмотрим этот вопрос в статье.
Рассмотрим основные библиотеки и подходы.
Логирование в Go
Cтандартная библиотека log
дает вам все необходимое для логирования без внешних зависимостей.
Для начала работы с log
достаточно импортировать пакет и использовать его функции:
import "log"
func main() {
log.Println("This is a log message!")
}
Этот код выведет сообщение вместе с датой и временем его записи. Выглядит достаточно просто.
Функции log
:
Print, Printf, Println: Print
выводит сообщение, Printf
позволяет форматировать вывод (подобно fmt.Printf
), а Println
добавляет новую строку в конце.
Fatal, Fatalf, Fatalln: Эти функции работают как Print
-функции, но после вывода сообщения вызывают os.Exit(1)
, завершая программу.
Panic, Panicf, Panicln: Похожи на Fatal
-функции, но вместо завершения программы вызывают панику.
С помощью логгера также можно настроить префикс сообщений, формат времени и определить, куда будут выводиться логи:
SetFlags: Определяет форматирование вывода. Например, log.Ldate | log.Ltime
добавит дату и время к каждому сообщению.
SetPrefix: Устанавливает префикс для каждого сообщения.
SetOutput: Позволяет перенаправить вывод логов в любой io.Writer
, будь то файл, буфер или HTTP-ответ.
Часто логи нужно сохранять в файлы для последующего анализа. Это делается путем перенаправления вывода логгера:
file, err := os.OpenFile("app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
log.Fatal("Failed to open log file:", err)
}
log.SetOutput(file)
Теперь все логи будут аккуратно сохраняться в app.log
.
Также в логере существуют стандартные уровни логирования: Debug, Info, Warn, Error.
Библиотека Logrus
Logrus полностью совместима со стандартным интерфейсом логгера Go, но предоставляет гораздо больше функциональности.
Logrus поддерживает различные уровни логирования, такие как Debug, Info, Warn, Error, Fatal и Panic:
log.Debug("Debug message")
log.Info("Info message")
log.Warn("Warning message")
log.Error("Error message")
log.Fatal("Fatal message") // вызовет os.Exit(1) после логирования
log.Panic("Panic message") // вызовет панику после логирования
Logrus умеет логировать структурированные данные:
log.WithFields(log.Fields{
"username": "johndoe",
"id": 123,
}).Info("User details")
Также в лоргусе можно настраиват формат вывода логов.Можно выбрать между встроенными форматтерами, такими как JSON и текст, или создать свой собственный:
// JSON форматтер
log.SetFormatter(&log.JSONFormatter{})
// текстовый форматтер с настройками
log.SetFormatter(&log.TextFormatter{
FullTimestamp: true,
})
Можно определить, куда будут отправляться ваши логи, используя метод SetOutput
. Это может быть любой объект, который реализует интерфейс io.Writer
, например, файл, стандартный вывод или удаленный сервер:
file, err := os.OpenFile("logrus.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err == nil {
log.SetOutput(file)
} else {
log.Info("Failed to log to file, using default stderr")
}
Еще есть "hooks" — специальные функции, которые вызываются при каждом логировании сообщения:
log.AddHook(MyCustomHook{})
Можно настроить уровень логирования, чтобы контролировать, какие сообщения фактически записываются:
log.SetLevel(log.WarnLevel)
Библиотека Zap
Zap предлагает API, который позволяет быстро записывать логи, и предоставляет множество опций для настройки.
Как и большинство логгеров, Zap предлагает различные уровни логирования: Debug, Info, Warn, Error, DPanic, Panic, и Fatal:
logger.Debug("Debug message")
logger.Info("Info message")
logger.Warn("Warning message")
logger.Error("Error message")
logger.DPanic("DPanic message")
logger.Panic("Panic message")
logger.Fatal("Fatal message")
Zap также позволяет легко добавлять контекстные поля к вашим логам:
logger.Info("Failed to fetch URL",
zap.String("url", url),
zap.Int("attempt", 3),
zap.Duration("backoff", time.Second),
)
Есть два основных типа логгеров: Logger
и SugaredLogger
. Logger
предлагает более высокую производительность, в то время как SugaredLogger
предлагает более удобный, но менее производительный API:
logger, _ := zap.NewProduction()
defer logger.Sync() // flushes buffer, if any
// Sugared логгер
sugar := logger.Sugar()
sugar.Infow("failed to fetch URL",
"url", url,
"attempt", 3,
"backoff", time.Second,
)
C помощью Zap можно легко настраивать определение уровня логирования, форматирование и место назначения логов:
cfg := zap.Config{
Encoding: "json",
Level: zap.NewAtomicLevelAt(zap.InfoLevel),
OutputPaths: []string{"stdout", "/tmp/logs"},
ErrorOutputPaths: []string{"stderr"},
EncoderConfig: zapcore.EncoderConfig{
MessageKey: "msg",
LevelKey: "level",
TimeKey: "ts",
EncodeTime: zapcore.ISO8601TimeEncoder,
...
},
}
logger, _ := cfg.Build()
Zap позволяет создавать пользовательские encoder'ы для определения, как логи будут сериализованы, и WriteSyncer'ы для определения, куда они будут записаны:
encoderConfig := zapcore.EncoderConfig{...}
core := zapcore.NewCore(
zapcore.NewJSONEncoder(encoderConfig),
zapcore.AddSync(os.Stdout),
zap.DebugLevel,
)
logger := zap.New(core)
Примеры реализации
На Lorgus:
package main
import (
"os"
"github.com/sirupsen/logrus"
)
var log = logrus.New()
func init() {
// установим уровень логирования
log.SetLevel(logrus.DebugLevel)
// установим форматирование логов в джейсоне
log.SetFormatter(&logrus.JSONFormatter{})
// установим вывод логов в файл
file, err := os.OpenFile("app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err == nil {
log.SetOutput(file)
} else {
log.Info("Не удалось открыть файл логов, используется стандартный stderr")
}
}
func main() {
// пример лога
log.WithFields(logrus.Fields{
"username": "Иван Иванов",
"id": 42,
}).Info("Новый пользователь зарегистрирован")
simulateError()
}
func simulateError() {
// представим, что здесь произошла ошибка
err := "Сервер базы данных не отвечает"
log.WithFields(logrus.Fields{
"event": "connect_database",
"error": err,
}).Error("Ошибка при подключении к базе данных")
}
На Zap:
package main
import (
"go.uber.org/zap"
)
var logger *zap.Logger
func init() {
var err error
logger, err = zap.NewProduction() // Или NewDevelopment для более подробного логирования
if err != nil {
panic(err) // Не удалось создать логгер
}
defer logger.Sync() // все асинхронные логи будут записаны перед выходом
}
func main() {
// пример логирования
logger.Info("Приложение запущено")
simulateError()
simulateDebug()
}
func simulateError()
// к примеру здесь произошло логирование
err := "Сервер базы данных не отвечает"
logger.Error("Ошибка при подключении к базе данных",
zap.String("event", "connect_database"),
zap.String("error", err),
)
}
func simulateDebug() {
// отладочное сообщение
feature := "Новая фича"
logger.Debug("Отладочная информация",
zap.String("feature", feature),
)
}
В микросервисах
Для начала нужно то, чтобы сервисы логируют данные в едином формате, предпочтительно в JSON, для упрощения парсинга и анализа.
Для отслеживания запросов через разные сервисы можно использовать Correlation ID — уникальный идентификатор, который передается между сервисами и записывается в логи.
Когда запрос входит в вашу систему (например, через API-шлюз), вы должны сгенерировать уникальный Correlation ID, если он еще не существует. Затем этот ID должен передаваться от сервиса к сервису.
package main
import (
"net/http"
"github.com/google/uuid"
)
func main() {
http.HandleFunc("/", handler)
http.ListenAndServe(":8080", nil)
}
func handler(w http.ResponseWriter, r *http.Request) {
// чекаем, есть ли Correlation ID в запросе
cid := r.Header.Get("X-Correlation-ID")
if cid == "" {
// если нет, генерируем новый
cid = uuid.New().String()
}
// логируем с Correlation ID
log.WithFields(logrus.Fields{
"correlation_id": cid,
}).Info("Запрос получен")
// добавляем Correlation ID в ответ
w.Header().Set("X-Correlation-ID", cid)
}
Когда один сервис вызывает другой, он должен передать Correlation ID в вызове. Это можно сделать, добавив ID в заголовки HTTP-запроса или в тело сообщения, если вы используете другой протокол:
func callAnotherService(cid string) {
// ... код для вызова другого сервиса ...
req, _ := http.NewRequest("GET", "http://another-service", nil)
req.Header.Set("X-Correlation-ID", cid)
// ... отправка запроса ...
}
С lorgus базовое логирование микросервсиов может выглядеть так:
package main
import (
"github.com/sirupsen/logrus"
"os"
)
func main() {
log := logrus.New()
log.Formatter = &logrus.JSONFormatter{} // Устанавливаем формат логов в JSON
// добавляем Hook для отправки логов в Logstash или другую систему
log.AddHook(NewMyLogstashHook())
// запрос с Correlation ID
correlationID := "12345"
log.WithFields(logrus.Fields{
"correlation_id": correlationID,
"event": "request_started",
}).Info("Начало обработки запроса")
// ..здесь бизнес логинка....
log.WithFields(logrus.Fields{
"correlation_id": correlationID,
"event": "request_finished",
}).Info("Запрос обработан")
}
// NewMyLogstashHook - создает новый Hook для отправки логов в Logstash или другую систему
func NewMyLogstashHook() logrus.Hook {
// логика подключения к Logstash или другой системе
// можно использовать logrus-logstash-hook или аналогичные библиотеки
return &MyLogstashHook{}
}
type MyLogstashHook struct {
// ... параметры подключения ...
}
func (hook *MyLogstashHook) Fire(entry *logrus.Entry) error {
// логика отправки лога
// к примеру, формируем JSON и отправляем его в Logstash
return nil
}
func (hook *MyLogstashHook) Levels() []logrus.Level {
// указываем, для каких уровней логирования активен этот Hook
return logrus.AllLevels
}
Больше про инфраструктуру и логирование эксперты из OTUS рассказывают в рамках практических онлайн-курсов.
Логирование может значительно упростить процесс отладки и мониторинга, а также повысить надежность приложений.
Keep coding, keep improving, и до новых встреч на Хабре.
и... с наступающим Новым Годом!