DevGang
Авторизоваться

Где разместить логгер в Golang? 

Ведение журнала является неотъемлемой частью любого приложения. Однако правильное расположение и использование логгера в структуре проекта вызывает вопросы даже у опытных разработчиков.

Есть несколько способов сделать это, из которых я отдаю предпочтение одному. Я объясню почему.

При разработке приложения разработчик выбирает один из нескольких явных вариантов:

  1. сохранить регистратор в глобальной переменной;
  2. получить логгер из библиотеки логирования;
  3. добавить логгер в структуры данных;
  4. явно передать регистратор в вызове функции.

В этой статье я буду использовать регистратор Zap в качестве примера, но вы можете использовать любой.

Сохраните регистратор в глобальной переменной

Это самый простой вариант, который приходит на ум.

var l, _ = zap.NewProduction()

Он хорош своей простотой, но его реализация может быть неэффективной:

  1. использование глобальных переменных увеличивает связность в программе, ухудшая структуру кода;
  2. глобальный логгер неудобно подстраивать под конкретный контекст, например — добавлять в логгер общие поля для набора сообщений;
  3. определение глобальной области действия может оказаться сложной задачей для приложения с несколькими исполняемыми файлами.

Получить регистратор из библиотеки журналов

Эта опция почти аналогична глобальной переменной, только переменная скрыта внутри импортируемого модуля.

l := zap.L()

Это немного удобнее в использовании, потому что вам не нужно искать глобальную переменную в коде и импортировать ее. С другой стороны, при подключении внешних зависимостей возможен конфликт версии библиотеки.

Добавьте регистратор в структуры данных

Это довольно распространенная альтернатива, используемая многими разработчиками. Однако его использование добавляет к структурам элемент, который относится не к структуре данных, а к их инструментированию.

type UserRepository struct {
    l *zap.Logger
    /*
    other fields
    */
}

func (ur *UserRepository) SaveUser(ctx context.Context, u user.User) error {
    ur.l.Info("saving user")
    // ...
}

Другими словами, логирование связано не с данными, а с операциями, поэтому имеет смысл вести его в функциях и методах, а не в структурах.

Явно передать регистратор в вызове функции

Получается, что самое логичное место для логгера — в самих методах или функциях. Однако передача регистратора в каждую функцию, очевидно, приводит к огромному усложнению кода:

  1. Добавление параметра к функции ухудшит читабельность подписи;
  2. явная передача логгера по цепочке вызовов добавляет много копипаста;
  3. функции с фиксированной сигнатурой (например, обработчик HTTP) потребуют, чтобы вы передали регистратору обходные пути.
func CreateUser(ctx context.Context, u user.User, l *zap.Logger) error {
    l = l.With(zap.String("userID", u.ID))

    l.Info("user creation started")

    if err := SaveUser(ctx, u, l); err != nil {
       l.Error("user creation failed", zap.Error(err))
       return err
    }
    l.Info("user creation done")
    return nil
}

Это делает подход неприменимым на практике. Но есть ли способ совместить плюсы разных подходов, избегая минусов?

Беспроигрышный контекст

Мне как разработчику хотелось бы передать логгер функции без добавления новых параметров в ее сигнатуру. Но эти требования кажутся противоречивыми.

К счастью, в Golang есть сущность, которая часто передается каждой функции и выполняет множество утилитарных задач — это context.Context.

Context в Go решает очень важные проблемы:

  1. изящное прерывание обработки при завершении работы приложения;
  2. ограничение времени выполнения функции;
  3. управление асинхронными операциями;
  4. выход из бесконечных циклов;
  5. распределенная трассировка.

Как видим, эти задачи чисто утилитарные, то есть не относятся к бизнес-логике приложения. Такая же задача логирования (если мы не говорим о специальных видах логирования, таких как журнал аудита).

Таким образом, передача журнала через контекст не нарушит сигнатуру функции (поскольку большинство функций уже принимают контекст в качестве первого параметра, а другие от этого выиграют). Это можно сделать следующим образом.

package log

import (
    "context"
    "go.uber.org/zap"
)

type ctxLogger struct{}

// ContextWithLogger adds logger to context
func ContextWithLogger(ctx context.Context, l *zap.Logger) context.Context {
    return context.WithValue(ctx, ctxLogger{}, l)
}

// LoggerFromContext returns logger from context
func LoggerFromContext(ctx context.Context) *zap.Logger {
    if l, ok := ctx.Value(ctxLogger{}).(*zap.Logger); ok {
        return l
    }
    return zap.L()
}

Это значительно упростит использование логгера и даст дополнительные преимущества:

  1. добавить в лог общие поля по цепочке вызовов, например добавление userID из запроса;
  2. внедрить промежуточное ПО для http/grpc, которое будет добавлять в лог информацию о запросе;
  3. интегрировать журнал с другими инструментами, использующими контекст, такими как распределенная трассировка.

Кроме того, подход решает проблему доступа к глобальным данным — теперь логгер всегда локален по отношению к вызываемой функции, и его можно использовать без ограничений.

func CreateUser(ctx context.Context, u user.User) error {
    l := log.LoggerFromContext(ctx).With(zap.String("userID", u.ID))

    l.Info("user creation started")

    ctx = log.ContextWithLogger(ctx, l)
    if err := SaveUser(ctx, u); err != nil {
       l.Error("user creation failed", zap.Error(err))
       return err
    }
    l.Info("user creation done")
    return nil
}

На практике не всегда удобно использовать такой подход, но он покрывает примерно 90%-95% случаев в серверном приложении. Для остальных случаев можно использовать другой подход из перечисленных выше.

Это также удобно в командной работе, когда разные члены команды могут добавлять контекст вызова в регистратор на разных уровнях приложения, когда они работают над своими изменениями. Это в конечном итоге будет объединено в информативные и полезные записи, с которыми легко работать.

#Golang
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

Vladimir Shaitan - Видео блог о frontend разработке и не только

Посмотреть