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

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

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

Урок 9 из 25 ~18 мин +20 XP

LogValuer: отложенные и «опасные» значения

Интерфейс slog LogValuer откладывает вычисление атрибута до момента записи: ленивые атрибуты go нужны там, где значение дорого собрать или опасно показывать. Разберём метод LogValue и ResolveValue, типовую обёртку для маскирования токенов на выходе, связь с уровнями на горячем пути и границы: когда отложенное значение не стоит своих строк.

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

У обычного атрибута есть нехорошее свойство: значение вычисляется до того, как slog решит, будет ли запись вообще напечатана. Вызов slog.Info("состояние", slog.String("dump", buildDump(state))) соберёт дамп даже на уровне Error-only, где Info отбрасывается, — и сделает это в каждой итерации горячего цикла. Вторая беда обратная: значение, которое вычислять дёшево, а показывать нельзя. Токен, пароль или персональные данные попадают в атрибуты честным Any — и уезжают в коллектор вместе с записью. Есть и третий, менее очевидный случай: значение, снятое заранее, устарело к моменту печати — атрибут со срезом состояния описывает прошлое, тогда как обёртка снимает его в момент самой записи, ближе к тому, что увидит дежурный. Обе проблемы решает один интерфейс: LogValuer. Тип, реализующий единственный метод LogValue() slog.Value, превращает свои данные в значение в момент записи, и только если запись действительно уходит в обработчик. Это даёт ленивость для дорогих значений и точку контроля для опасных: обёртка с маскированием пишется один раз, а работает для всех вызовов, где токен уже обёрнут.

slog LogValuer: ленивые атрибуты go#

Интерфейс помещается в одну строку: type LogValuer interface { LogValue() slog.Value }. Разбор slog logvaluer начинается с момента вызова: метод срабатывает не там, где создан атрибут slog.Any("key", v), а внутри обработчика, когда запись уже прошла проверку уровня. Оборачивает его функция slog.ResolveValue: она получает Value, видит внутри тип с LogValue и подставляет результат, повторяя раскрытие, пока не останется обычное значение; от зацикленных цепочек защита встроена в стандартную библиотеку. Для вашего кода это значит: дорогой дамп, посчитанный в LogValue, расходуется только на записи, которые реально напечатались. Атрибут для такого типа создают через slog.Any — типизированные конструкторы вроде String не оставляют интерфейсу шанса сыграть, потому что закрывают значение строкой заранее. Ошибку внутри LogValue тоже можно вернуть честно: метод вправе вернуть slog.StringValue с пометкой о сбое, и запись не развалится — панику в методе обработчик превращает в строку-заглушку, но лучше до этого не доводить.

Дорогой дамп считается только при печати

GO
type State struct {
        mu     sync.Mutex
        queues map[string]int
}

func (s State) LogValue() slog.Value {
        s.mu.Lock()
        defer s.mu.Unlock()

        attrs := make([]slog.Attr, 0, len(s.queues))
        for name, depth := range s.queues {
                attrs = append(attrs, slog.Int(name, depth))
        }
        return slog.GroupValue(attrs...)
}

slog.Debug("состояние очередей", slog.Any("state", st))

Атрибут state создаётся на каждом вызове, но блокировка мьютекса и сбор списка сработают только у записи, прошедшей уровень: Debug в проде не тронет мьютекс вовсе. GroupValue собирает группы программно — те же вложенные поля, что и slog.Group из седьмого урока, только из данных.

Маскирование на выходе: токен не доезжает#

Второе применение ценнее первого: тип-обёртка вокруг секрета становится единственной точкой, где секрет встречается с логом. Достаточно, чтобы токен существовал в коде как тип Token, а не сырая строка: тогда метод LogValue этого типа печатает звёздочки той же длины, хэш или первые символы, и никакой вызов Info не сможет напечатать значение целиком, не обёртывая его намеренно. Это надёжнее дисциплины «не логируй токен»: договорённости забываются в спешке инцидента, а тип проверяет компилятор. Подробный разбор того, что вообще нельзя выводить и как это соотносится с требованиями к персональным данным, ждёт в девятнадцатом уроке; здесь — механика. Полезно сочетать маскирование и ленивость: обёртка срабатывает на записи, прошедшей уровень, значит дешёвые отброшенные Debug не тратят время на преобразование. Для записей, которые всё же уйдут, цена — одна конкатенация строк, несравнимая с ценой ротации утечённых ключей и разбора инцидента по тикету, в который дамп кто-то приложил целиком.

Обёртка-маска для токена

GO
type Token string

func (t Token) LogValue() slog.Value {
        s := string(t)
        if len(s) == 0 {
                return slog.StringValue("пусто")
        }
        return slog.StringValue(strings.Repeat("*", len(s)))
}

slog.Info("запрос авторизован",
        slog.Any("token", tok),
        slog.String("user", login))

В записи появится поле token той же длины, что и оригинал, из звёздочек: формат сохраняется для проверки «токен вообще был», а значение — нет. Достаточно, чтобы тип tok был Token: свой LogValue вызовется сам, маскировать руками в каждом вызове не нужно.

ResolveValue и границы отложенности#

Механика раскрытия стоит одной страницы понимания. Когда обработчик собирается писать атрибут, он вызывает slog.ResolveValue: для обычных значений возвращается то же Value, для типа с LogValue — результат метода, при необходимости с продолжением, если внутри снова прячется LogValuer. Глубина цепочки ограничена внутренней защитой от зацикливания, поэтому взаимно ссылающиеся обёртки не повесят процесс — в записи окажется заглушка. Отсюда две границы. Первая: ленивость начинается с Any — атрибуты, созданные через String или Int, уже содержат готовое значение, и LogValue у них не спросит. Вторая: LogValue не отменяет стоимость создания самого атрибута — slog.Any("key", st) обёртывает указатель или структуру мгновенно и дёшево, но если вы для него предварительно считаете готовую строку, ленивость уже потеряна по дороге в вызов. Правило простое: в Any уходит сам объект с методом, а вся тяжёлая работа переезжает внутрь LogValue. На горячем пути это сочетается с Enabled из четырнадцатого урока: проверка уровня до сборки полей экономит ещё больше.

Где отложенное значение окупается#

СитуацияЧто даёт LogValuerТиповая обёртка
секрет в атрибутемаскирование на выходе: звёздочки или хэшToken, Secret с методом LogValue
дорогой дампсбор только для записей, прошедших уровеньState с сбором среза в LogValue
значение под мьютексомблокировка лишь при реальной печатиметод с Lock и GroupValue
составное значениеподобъект с полями по даннымslog.GroupValue(attrs...)

Ещё одна точка, где отложенные значения показывают характер, — тесты. Захват логов в память из двадцатого урока получает записи уже после раскрытия: тест видит звёздочки, а не исходный токен, и это удобно проверять утверждением «в выводе нет подстроки исходного значения». Такой тест дешевле целой политики безопасности и работает как страховка: новый атрибут с секретом, забытый без обёртки, падает на первой же проверке. Для дежурных ценность другая: звёздочки сохраняют длину, поэтому по записи видно, что токен был и его формат совпал с ожидаемым, — вопрос «а был ли там вообще заголовок авторизации» снимается без запроса к инженеру. И последнее: не стоит складывать в LogValue работу с побочными эффектами — запросы в сеть, записи в файлы, чтение окружения. Отложенное значение должно быть детерминированной функцией от данных, иначе лог начинает зависеть от окружения, а тест — от порядка вызовов.

Когда LogValuer не нужен#

Честная граница: обёртки пишут для значений с поведением — дорого посчитать, опасно показать или неудобно сериализовать. Для обычных полей интерфейс лишний: slog.Int("port", 8080) короче и читается без прыжка в определение типа. Не нужен он и там, где значение уже готово и безопасно: длительность, счётчик, код ответа. Есть и цена у самого приёма: читатель вызова Info больше не видит, что именно напечатается, — за значением надо идти к типу; методы со скрытым поведением хуже методов с явными аргументами, и злоупотреблять этим не стоит. Разумная конвенция проекта: типы с LogValue заводят для секретов, больших дампах и значениях с мьютексом, а список таких типов держат коротким — это документируется рядом со схемой полей. Всё остальное остаётся обычными атрибутами из четвёртого урока: типизированные конструкторы, Any для случаев «вне списка» и группы для вложенности.

Правила LogValuer#

  • LogValue вызывается обработчиком для записи, прошедшей уровень: отброшенные записи не считают дампы.
  • Атрибут для типа с LogValue создают через slog.Any: String и Int закрывают значение до интерфейса.
  • Раскрытием занимается slog.ResolveValue, цепочки ограничены защитой от зацикливания.
  • Секреты живут в типе с LogValue с первого дня: обёртка-маска одна, а вызовов записи сотни.
  • Панику внутри LogValue обработчик превращает в заглушку — но тяжёлую работу лучше делать без паник.
  • Для обычных готовых полей интерфейс не нужен: Int, String и Duration короче и прозрачнее.

Связи с другими уроками: четвёртый урок про атрибуты объясняет, какие конструкторы закрывают значение до интерфейса, а девятнадцатый урок про секреты собирает полный каталог того, что нельзя выводить. Горячий путь и проверку Enabled разбирает четырнадцатый урок про производительность, проверку масок тестами — двадцатый урок, а Handler, который вызывает ResolveValue, — двенадцатый урок про свой Handler. Карта курса — на странице курса.

Проверка: момент вызова LogValue

Сервис пишет на уровне Info, Debug отключён уровнем обработчика. Вызов slog.Debug("состояние", slog.Any("state", st)) с LogValuer-типом st. Когда выполнится метод st.LogValue()?

Проверка: маскирование

type Token string с LogValue, возвращающим звёздочки той же длины. В записи slog.Any("token", tok) при tok равном sk-live-9f3ac2. Что окажется в поле token?

Что напечатает обработчик?

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

type Secret string

func (s Secret) LogValue() slog.Value {
        return slog.StringValue(strings.Repeat("*", len(string(s))))
}

pass := Secret("hunter2")
slog.Info("вход", slog.Any("pass", pass), slog.Int("attempt", 3))

Соберите тип-маску

Допишите метод интерфейса, возврат значения и создание атрибута для типа с LogValue.

type Token string func (t Token) () slog.Value { return slog.(strings.Repeat("*", len(string(t)))) } slog.Info("аутентификация", slog.("token", tok))

Тип-маска от определения до вызова

Расставьте строки по порядку: тип, начало метода, возврат значения, конец метода, создание значения, запись с ним.

Строки

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

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

Карточки: LogValuer

Термин

LogValuer

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

Интерфейс с методом LogValue() slog.Value: тип сам решает, каким значением предстать в записи.

1 / 6

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

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

Вывод

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

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

  • LogValuer откладывает вычисление атрибута до обработчика: дорогие значения платятся только за напечатанные записи.
  • Атрибуты для таких типов создают через slog.Any — типизированные конструкторы закрывают значение заранее.
  • slog.ResolveValue раскрывает обёртки, включая цепочки, с защитой от зацикливания.
  • Обёртка-маска вокруг секрета печатает звёздочки или хэш: тип контролирует вывод вместо дисциплины вызовов.
  • Панику в LogValue обработчик превращает в заглушку, но тяжёлую работу делают без паник.
  • Для готовых и безопасных полей интерфейс излишен: обычные конструкторы короче и прозрачнее.

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

Заведите локально тип Secret с LogValue, печатающим первые два символа и звёздочки по остальной длине, и проверьте вывод на токенах разной длины, включая пустую строку. Затем добавьте тип State с GroupValue внутри и убедитесь через LevelWarn у обработчика, что Debug-запись со State не тратит время на сбор — поставьте печать маркера в метод и понаблюдайте.

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

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

LogValuer или просто не логировать секрет?

Не логировать — дисциплина, которая ломается в спешке инцидента; обёртка — механика, проверяемая компилятором: секрет живёт в собственном типе и в лог попадает уже преобразованным. Оба подхода сочетают: тип-маска для обязательных полей и запрет на вывод остального — в девятнадцатом уроке.

Можно ли вернуть из LogValue несколько полей?

Да: верните slog.GroupValue(attrs...) — атрибут развернётся в группу, как slog.Group из седьмого урока. Так печатают состояние структур: map с очередями превращается в подобъект с полями по ключам, а на текстовом формате — в пути с точками.

Что произойдёт при панике внутри LogValue?

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

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

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

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

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