Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 7 из 25 ~16 мин +20 XP

Группы: WithGroup и вложенные поля

Группы в slog — способ дать полям пространство имён: конструкция slog Group собирает несколько атрибутов под одним ключом, а WithGroup назначает такую приставку всему, что логгер напишет дальше. Разберём оба способа, как вложенные поля лога выглядят в JSON и в текстовом выводе, что группируется, а что остаётся на верхнем уровне и где проходит разумная граница вложенности.

Содержание урока · 9 разделов

Плоский набор ключей хорошо служит до первого конфликта имён. Сервис логирует и HTTP-запросы, и пользователей, и метрики кэша — и вот поле id в одной записи означает номер запроса, в другой номер пользователя, а в третьей версию схемы. Структурные поля решают типизацией, но не пространством имён: ключ один, а смыслов у него несколько. В slog для этого есть группы — атрибуты, собранные под общим ключом, — и два способа их заводить. Точечный: slog.Group внутри вызова Info оборачивает конкретный набор полей. Сквозной: logger.WithGroup(name) возвращает логгер, у которого каждая последующая запись получает общий префикс. Группы переводят вывод из плоского списка в иерархию: user.id и request.id перестают спорить за одно имя, а дашборды и алерты находят нужное поле по пути. В этом уроке — механика обоих способов, вид вывода в двух форматах, границы групп и соглашения, после которых вложенные поля перестают быть сюрпризом для того, кто будет читать их в три ночи.

slog Group и WithGroup: два способа собрать группу#

Разбор slog group начинается с функции Group: она принимает имя и любое число атрибутов и возвращает Attr, который обработчик развернёт в подобъект. Её место — внутри вызова записи, рядом с обычными атрибутами: slog.Info("запрос обработан", slog.Group("user", slog.Int("id", 7), slog.String("role", "admin"))). Второй способ — метод WithGroup на логгере: logger.WithGroup("http") возвращает новый логгер, у которого все последующие атрибуты получат группу http. Разница в охвате: Group действует на один вызов и расставляет поля локально, WithGroup — на весь остаток жизни конкретного логгера, поэтому его место в конструировании, рядом с With, а не в горячем коде обработчика. Способы сочетаются: логгер с WithGroup("http") внутри записи получает ещё и slog.Group("user"), и в выводе поля окажутся на пути http.user.id. Как и With, метод WithGroup не меняет родителя — исходный логгер продолжает писать без префикса, а дочерние логгеры можно наслаивать друг на друга.

Сквозная группа и локальная группа в одной записи

GO
package main

import (
        "log/slog"
        "os"
)

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

        logger.Info("запрос обработан",
                slog.Group("user",
                        slog.Int("id", 7),
                        slog.String("role", "admin")),
                slog.Int("status", 200))
}

Группа http назначена через WithGroup, поэтому и status, и вложенная группа user попадают под неё. В JSON это объект http с полями user.id, user.role и status; msg, time и level остаются на верхнем уровне. Тот же код с TextHandler напечатает http.user.id=7 через точечную нотацию.

Вложенные поля лога: как группа выглядит в выводе#

Формат определяет, как именно вложенные поля лога доедут до читателя. JSONHandler разворачивает группу в настоящий вложенный объект: поля оказываются на путях вида http.user.id, и парсер коллектора читает их как дерево — это главная причина заводить группы в сервисах, чьи логи собирает Loki или ELK. TextHandler группирует иначе: вложенности нет, вместо неё точечная нотация — путь склеивается в один ключ http.user.id=7, потому что текстовый формат не умеет вложенные объекты. Формы разные, путь один, поэтому схемы полей, о которых пойдёт речь в семнадцатом уроке, переносятся между форматами без переделки. Есть и правила отбрасывания, одинаковые для штатных обработчиков. Группа, в которой не осталось ни одного непустого атрибута, в вывод не попадает вовсе: условия часто порождают пустые ветки, и лог не раздувается секциями-пустышками. Атрибут с пустым ключом внутри группы отбрасывается так же, как и на верхнем уровне. Свой Handler из двенадцатого урока вправе вести себя иначе — это его контракт, но до поры до времени держитесь поведения штатных.

Группа user с полями id и role в двух форматах#

ФорматКак выглядитЧто важно
JSONHandlerобъект: http.user.id и http.user.role вложеныколлектор читает поля как дерево
TextHandlerточечная нотация: http.user.id=7 http.user.role=adminвложенности нет, путь склеен в ключ
пустая группав выводе отсутствуетштатные обработчики отбрасывают её целиком
атрибут с пустым ключомв выводе отсутствуетправило то же, что и на верхнем уровне

Границы групп: что попадает, а что нет#

WithGroup влияет на атрибуты, добавленные после него, и только на них. Служебные поля записи — время, уровень, сообщение и, при AddSource, место вызова — остаются на верхнем уровне в любом формате: дежурному нужно, чтобы msg и level находились поиском по одному и тому же пути во всех строках, а не в разных ветках дерева. Метод WithGroup с пустой строкой — операция без действия: он возвращает тот же логгер без группы, чем удобно пользоваться в шаблонном коде, где префикс приходит переменной, которая иногда пуста. Отсюда же практическая граница: вложенность глубже двух уровней превращает путь в строку, которую сложно напечатать в фильтре, а конфликт имён решается и на одном уровне, если подсистемы названы аккуратно. Ещё одна граница — порядок конструирования: группа применяется к атрибутам, добавленным после WithGroup, но не к тем, что логгер унаследовал от родительского With до разветвления; вспомните это, когда поле неожиданно окажется не в той ветке.

Цепочка групп для вложенной подсистемы

GO
httpLogger := base.WithGroup("http")
reqLogger := httpLogger.WithGroup("request")

reqLogger.Info("обработан",
        slog.Int("status", 200),
        slog.Group("timing",
                slog.Duration("total", elapsed)))

Два WithGroup подряд образуют путь http.request, а Group внутри записи добавляет ветку timing. Итоговые пути в JSON: http.request.status и http.request.timing.total. Если elapsed равен нулю и группа timing осталась бы без атрибутов, штатный обработчик её отбросил бы — ветка не появится в выводе.

Практика: группы по подсистемам#

Соглашение, которое держится в проектах без усилий: одна группа верхнего уровня на подсистему и не больше одной вложенной. http, db, cache, auth — четыре префикса закрывают большинство сервисов, а внутри них пути второго уровня вроде http.request или db.query уточняют контекст. Такое дерево читается в фильтрах и не спорит с полями соседей: db.id и http.id живут раздельно, хотя ключ называется одинаково. Второе правило — называть группы именами подсистем, а не именами типов: группа user про доменную сущность уместна, а группа string, названная по типу значения, лишняя. Третье — не дублировать имена атрибутов внутри одной группы: поле id в двух разных группах — норма, поле id и рядом второй id в одной — глюк для парсера. Схему полей полезно держать рядом с кодом, потому что дежурный, строящий фильтр, смотрит в неё, а не в исходники; подробнее о схеме для сборщиков — в семнадцатом уроке. А про то, как логгер с префиксами доехал бы до конкретного запроса целиком, — восьмой урок про With и логгер на запрос.

Пара сценариев, где новички спотыкаются о группы. Первый: поле добавлено до WithGroup, и оно «не попало» под префикс — это не баг, а порядок конструирования: атрибуты и группы накапливаются цепочкой, и всё, что создано до ветвления, остаётся на общем пути. Лечится переупорядочиванием: сначала префикс, потом поля. Второй сценарий: ожидание, что WithGroup повлияет на уже существующие записи — не повлияет, группы живут на конкретном логгере и его потомках, родительскую запись никто не переписывает задним числом. Третий: путь вида http.http.id при двух одинаковых WithGroup выглядит как двойная работа — это знак, что middleware наслоился дважды, и тот же детектор дубликатов, о котором пойдёт речь в восьмом уроке, здесь применим. Общий вывод: группы — часть контракта вывода, а не украшение; их переименовывают с тем же уведомлением потребителей, что и обычные имена полей.

Правила групп#

  • slog.Group собирает атрибуты под одним ключом в конкретном вызове, WithGroup — назначает префикс логгеру на всё дальнейшее.
  • WithGroup и With не меняют родителя: исходный логгер продолжает писать без префикса.
  • msg, level, time и source группами не накрываются — служебные поля остаются на верхнем уровне.
  • WithGroup с пустой строкой возвращает тот же логгер: удобно для префикса, приходящего переменной.
  • Пустая группа и атрибут с пустым ключом отбрасываются штатными обработчиками.
  • Одна группа на подсистему, вложенность до второго уровня: глубже путь становится нечитаемым в фильтрах.

Связи с другими уроками: четвёртый урок про атрибуты объясняет, из чего собирается любой Attr, включая групповой. Следующий шаг по конструированию логгера — восьмой урок про With и логгер на запрос, а сквозные поля запроса целиком разбирает пятнадцатый урок про request ID. Для сборщиков вроде Loki и ELK схему полей с группами фиксирует семнадцатый урок. Карта курса — на странице курса.

Проверка: TextHandler и группы

logger := slog.New(slog.NewTextHandler(os.Stdout, nil)).WithGroup("http"); logger.Info("готово", slog.Int("code", 200)). Как выглядит строка вывода?

Проверка: пустые группы

logger.WithGroup("http").Info("готово") — что попадёт в вывод, если после WithGroup не было добавлено ни одного атрибута?

Какой JSON получится?

Прочитайте код и выберите, что он выведет:

logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)).
        WithGroup("http")
logger.Info("готово", slog.Group("user",
        slog.Int("id", 7), slog.String("role", "admin")))

Соберите вложенные поля

Допишите: назначить сквозную группу, собрать локальную группу user и типизированный атрибут с числом.

base := slog.New(slog.NewJSONHandler(os.Stdout, nil)) logger := base.("http") logger.Info("запрос обработан", slog.("user", slog.("id", 7), slog.String("role", "admin")))

Цепочка от базового логгера до вложенной записи

Расставьте строки по порядку: базовый логгер, группа http, атрибут-группа, запись с ней, вложенная группа, запись из неё.

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Карточки: группы

Термин

slog.Group

нажмите, чтобы перевернуть
Определение

Attr, собирающий несколько атрибутов под одним ключом внутри конкретного вызова записи.

1 / 6

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

Коротко: что запомнить

  • slog.Group оборачивает атрибуты в один вызов, WithGroup назначает префикс всему дальнейшему выводу логгера.
  • В JSON группа — вложенный объект, в текстовом формате — плоский ключ с точками: путь поля одинаков.
  • Служебные поля time, level, msg и source не попадают под группы.
  • WithGroup с пустой строкой — без действия, пустая группа отбрасывается штатными обработчиками.
  • Группы решают конфликты имён: db.id и http.id живут раздельно при одном ключе.
  • Соглашение: одна группа на подсистему, вложенность не глубже второго уровня.

Домашнее задание

Соберите локально логгер с WithGroup("db") и записью, где внутри есть slog.Group("query", ...) с тремя полями. Проверьте вывод в обоих форматах и найдите путь каждого поля. Затем добавьте вторую группу через ещё один WithGroup и убедитесь, что пути склеились, а служебные поля остались на верхнем уровне.

Личные заметкиПусто
Хранятся только в вашем браузере (Slog · Группы: WithGroup и вложенные поля)

Вопросы и ответы по уроку

Group или WithGroup: что выбрать?

Group — когда нужно собрать несколько полей под общим ключом в одной записи, например user или timing. WithGroup — когда префикс должен носить весь логгер, например подсистема db или http. В боевом коде WithGroup живёт при конструировании, Group — в точке записи.

Влияют ли группы на производительность?

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

Почему поле оказалось не там, где ждёт дашборд?

Скорее всего, логгер получил WithGroup после того, как схема полей была зафиксирована, — путь сменился с user.id на http.user.id. Путь поля — часть контракта с теми, кто читает логи: при изменении групп обновляйте схему и предупреждайте потребителей вывода.

Термины урока

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

Следующая ступень учебного пути

pgx25 уроков~12 чпрактика25 уроков уже доступны

PostgreSQL в Go: драйвер pgx с нуля до продакшена

Курс pgx для Go: подключение к PostgreSQL, pgxpool, сканирование структур, транзакции, Batch, COPY, JSONB, LISTEN/NOTIFY и тесты. 25 уроков.

  • Подключаться к PostgreSQL через pgx и pgxpool
  • Сканировать строки в структуры и справляться с NULL
  • Работать с типами Postgres: UUID, JSONB, массивы, timestamptz
  • Проводить транзакции и массовые вставки через Batch и COPY