Привет, Хабр!

Как в 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, и до новых встреч на Хабре.

и... с наступающим Новым Годом! ????

Комментарии (6)


  1. alexandrtumaykin
    27.12.2023 08:19
    +3

    Не понял о чем материал. Статья заканчивается "на полпути" и не раскрыта. А где про slog и другие популярные логеры?


  1. dsh2dsh
    27.12.2023 08:19
    +1

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


  1. sv_kozlov
    27.12.2023 08:19

    С выходом slog имеет смысл писать о нем, либо о переходе на него.


  1. inik23
    27.12.2023 08:19

    Странно что вы и слова не написали о slog который недавно стал частью самого языка


  1. PaNick
    27.12.2023 08:19

    Соглашусь с предыдущими комментаторами. Нужно обязательно написать про slog. Он решает основные проблемы старого логгера. А так из сторонних zerolog мне лично больше зашел


  1. odilovoybek
    27.12.2023 08:19

    Кажись уже давно пора отписываться от OTUS. Штамповка статей от них уже надоела.