Что такое структурированное логирование и каковы его преимущества и недостатки?

Ответ

Структурированное логирование — это подход к записи логов не в виде простого текста, а в формате с четкой структурой (чаще всего JSON или logfmt), где каждое сообщение представляет собой набор пар «ключ-значение».

Плюсы:

  • Машиночитаемость: Структурированные логи легко парсятся и индексируются автоматизированными системами (ELK, Grafana Loki, Splunk, Datadog). Это упрощает поиск, фильтрацию и агрегацию данных.
  • Контекстуальность: Позволяет добавлять к сообщению произвольные контекстные поля (user_id, request_id, duration_ms), что делает логи гораздо более информативными для отладки и анализа.
  • Надежный поиск: Вместо поиска по подстроке в текстовых логах можно выполнять точные запросы по полям, например, level=error AND user_id=123.

Минусы:

  • Повышенный объем: JSON и другие форматы обычно занимают больше места, чем простой текст, что увеличивает затраты на хранение.
  • Сложнее для быстрого чтения человеком: Необработанные JSON-логи могут быть менее читаемыми в консоли по сравнению с простым текстом. Однако многие инструменты предоставляют "красивый" вывод для разработки.
  • Накладные расходы на производительность: Сериализация данных в JSON или другой формат требует дополнительных ресурсов CPU по сравнению с простой записью строки.

Примеры в Go:

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

Пример с slog (стандартная библиотека):

import (
    "log/slog"
    "os"
)

func main() {
    logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))

    logger.Info("Пользователь вошел в систему",
        slog.String("username", "admin"),
        slog.String("ip_address", "127.0.0.1"),
    )
}

Вывод slog:

{"time":"2023-10-26T15:00:00.123Z","level":"INFO","msg":"Пользователь вошел в систему","username":"admin","ip_address":"127.0.0.1"}

Пример с logrus (популярная библиотека): Примечание: logrus находится в режиме поддержки, новым проектам рекомендуется использовать slog или zerolog.

import "github.com/sirupsen/logrus"

func main() {
    log := logrus.New()
    log.SetFormatter(&logrus.JSONFormatter{})

    log.WithFields(logrus.Fields{
        "user": "admin",
        "ip":   "127.0.0.1",
    }).Info("User logged in") 
}

Ответ 18+ 🔞

А, структурированное логирование, говоришь? Ну это ж, блядь, как раз тот случай, когда из простой хуйни делают целую науку, но, сука, науку полезную!

Смотри, раньше-то мы логи писали как бог на душу положит: строка, да и всё. "Ошибка какая-то, нахуй". А теперь, понимаешь, надо чтобы не только человек, но и какая-нибудь тупая машина в ELK могла это прочитать и сказать: "Э, сабака сука, вот тут юзер admin с айпишником 127.0.0.1 в 15:00:00.123 нас ебал!"

Плюсы, блядь, очевидные:

  • Для машин — мёд: Всё в полях, всё по ключам. Не надо регулярками выковыривать, как гланды. Закинул в систему — она сама всё разложила, проиндексировала и говорит: "Ищи, чё надо?". Хочешь все ошибки за сегодня? Пожалуйста. Хочешь логи конкретного юзера user_id=123? Да похуй, вот тебе.
  • Контекст, ёпта: Раньше в логе было "запрос упал". И хуй поймёшь — чей запрос, на какую хуйню, сколько длился. А теперь можно прилепить к сообщению любые поля: request_id, duration_ms, endpoint. Сразу видно, где собака зарыта, а где просто мартышлюшка накосячила.
  • Поиск, который не сосёт: Вместо того чтобы grep'ом по тексту шариться (а там ещё и формат сообщения мог измениться, блядь!), ты ищешь по полям. level=error AND service=auth. Пиздец как удобно.

Но и минусы, конечно, есть, куда ж без них:

  • Места жрёт, как не в себя: JSON — он же, сука, жирный! Каждое сообщение обрастает кавычками, скобками, названиями полей. Объём логов вырастает в разы, а хранение — это деньги, блядь.
  • Человеку читать — пиздец: Сырой JSON в консоли — это же просто стена текста, глаза сломаешь. Хотя для разработки есть форматтеры, которые это всё красиво раскрашивают.
  • Производительность чуть сосёт: Сериализовать данные в JSON — это не просто строку склеить, тут процессор поработать должен. На высоких нагрузках это может стать бутылочным горлышком, если, конечно, ты не Яндекс, а то у них там овердохуища всего.

Вот, смотри, как в Go с версии 1.21 это делается нахуй правильно, через новый slog:

import (
    "log/slog"
    "os"
)

func main() {
    logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))

    logger.Info("Пользователь вошел в систему",
        slog.String("username", "admin"),
        slog.String("ip_address", "127.0.0.1"),
    )
}

И на выходе получится такая вот красота, от которой любая система мониторинга просто обоссытся от счастья:

{"time":"2023-10-26T15:00:00.123Z","level":"INFO","msg":"Пользователь вошел в систему","username":"admin","ip_address":"127.0.0.1"}

А раньше-то, блядь, использовали logrus (и многие до сих пор используют, хотя он уже, считай, на пенсии):

import "github.com/sirupsen/logrus"

func main() {
    log := logrus.New()
    log.SetFormatter(&logrus.JSONFormatter{})

    log.WithFields(logrus.Fields{
        "user": "admin",
        "ip":   "127.0.0.1",
    }).Info("User logged in") 
}

Короче, суть в чём: если пишешь что-то серьёзнее "Hello, world", то структурированные логи — это не прихоть, а необходимость. Иначе потом, когда всё накроется медным тазом, будешь как Герасим из рассказа: стоять, мычать "Муму" и нихуя не понимать, где искать причину.