📖 Про slogЕщё недавно в Go не было встроенного структурированного логгера (логгера по уровням, со всякими key=value полями, который может писать json в разные системы сбора). Поэтому в большинстве проектов использовались сторонние логгеры (хотя, есть и люди, которые пишут log.Printf("[DEBUG] something happend") и радуются жизни, но я не из таких). Я же, в свою очередь предпочел zerolog.
zerolog предлагает возможность (хотя и не заставляет) прокидывать логгер через контекст. Таким образом вы можете внутри middleware добавлять в логер какие-то поля, такие как request_id, и использовать "расширенный" логгер в хендлере.
Хотя и "фу, context.WithValue", я привык к такому решению и мне оно казалось стандартным допущением.
Однако в стандартной библиотеке появился log/slog. К моему удивлению, функционала для вставки и извлечения логгера из/в контекст не оказалось. Меня это удивило и я, не долго думая, сам реализовал этот функционал. Но потом задумался: "разработчики go ведь не идиоты (хотя я лично знаю много людей, готовых спорить с этим утверждением), как они предлагают использовать логгер?".
slog умеет держать логгер в глобальном atomic.Pointer. И я начал анализировать, а нормально ли использовать глобальный логгер для приложения? С одной стороны весьма удобно, импортировал и юзаешь. Но это немного усложняет тестирование, выходит не очень явно и в целом как-то так себе, глобалы всё-таки...
Но главной причиной отказа от этого решения для меня являлось не понимание того, как же мне заткнуть request_id в логгер и прокинуть в handler логгер вместе с ним. Ведь не буду я в каждом логе руками request_id прописывать.
Хочу подчеркнуть, проблемой было
непонимание. Здесь я углубился: на что способен slog? И был приятно удивлён. После стандартного логгера в python примитивность slog меня огорчила. Ведь всё, что у нас есть, текстовый и json логгер (handler) и пара методов для записи логов. Но в моменте мне открылась сила декораторов. Дело в том, что декорируя Handler, который использует логгер, вы можете сделать
всё. Буквально всё. От цветного вывода в консоль и записи в несколько источников (хоть в telegram сообщения шли), до тех же самых request_id.
Осознав это, я будто познал тайны мироздания...
Вот, как я решил проблему request_id:
type RequestIDLogger struct {
slog.Handler
}
func (l *RequestIDLogger) Handle(ctx context.Context, r slog.Record) error {
request := getRequestID(ctx)
r.AddAttrs(slog.String("request_id", request))
err := l.Handler.Handle(ctx, r)
if err != nil {
return fmt.Errorf("failed to run parent of RequestIDLogger handler: %w", err)
}
return nil
}
Но использовать глобальный логгер я всё равно не захотел, поэтому распихал их по структурам, как обычно в Go делается dependency injection.
Кажется, это наилучший компромисс между всеми подходами.
Теперь middleware генерирует для каждого запроса request_id и добавляет его в контекст, а handler в slog.Logger извлекает его и добавляет при записи логов.
Кстати, slog уже оброс кучей всяких handler. Много интересного можно найти в
awesome slog (думаю, тут есть всё, что вам может пригодиться).
#go #golang #logging #slog