Токен попал в Go-лог: как задать безопасное представление для slog

В обработчик добавили logger.Info с целым объектом подключения. На локальной машине это помогло найти неверный адрес сервиса. В централизованный журнал вместе с адресом ушёл API-токен. Удаление одной строки исправляет эпизод, но похожий объект легко залогировать в другом месте. Полезнее задать ему безопасное представление рядом с типом.

Выберите поля, которые нужны для расследования

Для диагностики подключения обычно достаточно имени зависимости, адреса без секретов и номера попытки. Пароль, токен и пользовательское тело запроса редко помогают понять таймаут. Список разрешённых полей проще контролировать, чем последовательное удаление известных секретных ключей из произвольного объекта. При добавлении нового поля оно не должно автоматически попадать в лог.

В slog тип может реализовать LogValuer. Метод LogValue возвращает значение, которое структурный обработчик использует при разрешении атрибута. Для конфигурации зависимости можно вернуть группу с несколькими публичными полями, а секрет вообще не включать. Для отдельного типа Secret метод возвращает фиксированную маску. Это два полезных уровня: безопасный объект целиком и защита одиночного секрета.

Проверьте результат как JSON

Пример пишет в bytes.Buffer через JSONHandler, затем разбирает полученную строку. Тест проверяет и точное значение безопасных полей, и отсутствие исходного токена во всём выводе. Время и служебные атрибуты не влияют на результат, потому что мы не сравниваем строку целиком. Программе нужен Go 1.21 или новее.

package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"log/slog"
	"strings"
)

type Secret string

func (s Secret) LogValue() slog.Value { return slog.StringValue("[REDACTED]") }

type Dependency struct {
	Name, Host string
	Token      Secret
}

func (d Dependency) LogValue() slog.Value {
	return slog.GroupValue(slog.String("name", d.Name), slog.String("host", d.Host))
}
func main() {
	var b bytes.Buffer
	logger := slog.New(slog.NewJSONHandler(&b, nil))
	d := Dependency{"billing", "billing.internal", Secret("test-only-token-9481")}
	logger.Info("dependency call", "dependency", d, "credential", d.Token)
	var row map[string]any
	if err := json.Unmarshal(b.Bytes(), &row); err != nil {
		panic(err)
	}
	group := row["dependency"].(map[string]any)
	fmt.Println(group["name"], group["host"])
	fmt.Println(row["credential"])
	fmt.Println(strings.Contains(b.String(), string(d.Token)))
}

В результате токен отсутствует, зависимость остаётся узнаваемой, а отдельный Secret превращается в [REDACTED]. Сохранение структуры важно: поиск по dependency.name устойчивее поиска слова внутри произвольной строки. При ошибке внешнего вызова можно добавить request_id и длительность, сохранив возможность связать записи разных этапов.

Не теряйте защиту до вызова логгера

LogValuer срабатывает, когда тип попадает в slog как значение. Если заранее преобразовать Secret в string, информация о типе исчезнет. То же произойдёт при ручном составлении сообщения с исходным токеном. Поэтому форматирование секретов через fmt.Sprintf, конкатенацию и прямую передачу в поле ошибки нужно искать отдельно. Новый тип помогает сделать правильный путь обычным, но не переписывает уже построенные строки.

У адреса тоже может быть секретная часть. URL с паролем, ключом в query или подписью в пути нельзя бездумно добавлять в разрешённые поля. Для журнала полезно хранить отдельно логическое имя сервиса и адрес, построенный из разрешённых частей. Внешнее сообщение ошибки иногда повторяет полный URL: его следует сопоставить с публичной категорией сбоя, а подробности очищать до записи.

HandlerOptions.ReplaceAttr пригодится для общих правил организации: удалить определённые атрибуты, переименовать поле уровня, нормализовать время. Однако общее правило по имени token не найдёт секрет внутри message или пользовательского error. Начинайте с проектирования полей в месте возникновения события, а центральную фильтрацию используйте как дополнительный слой.

Для регрессии добавьте тест на каждый объект, который регулярно передаётся целиком: настройки клиента, сведения о пользователе, входящее событие. Проверьте вложенные группы и указатели. Полезно включить контрольный секрет с уникальной строкой и искать её в результате. Не используйте настоящий ключ в тестовом наборе: тестовые журналы тоже сохраняются в CI.

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

Закрепите набор диагностических полей

Для одного события задайте постоянные имена полей и типы значений. Если duration то строка, то число, поиск медленных вызовов в хранилище логов осложнится. Тест можно проверять на обязательные dependency, request_id и outcome, а не на порядок ключей JSON. При этом высокая кардинальность имеет цену: полный URL и произвольный текст запроса создают множество уникальных значений. Для сводки лучше фиксированное имя операции, например cancel_order. Идентификатор конкретного запроса оставляйте для перехода к его событиям, а публичное имя операции — для сравнения с соседними вызовами.

Продолжите до взаимодействия сервисов

Безопасные поля позволяют перейти от отдельных сообщений к истории операции. В платном курсе микросервисов на Go вы будете читать логи, метрики и трассировки, разбирать HTTP- и gRPC-вызовы и проверять сценарии отказа. Следующая задача — проследить, какой шаг успел завершиться между запросом, транзакцией и доставкой события. Откройте программу курса «Микросервисы на Go: HTTP, PostgreSQL, gRPC, Kafka и Docker».

Продолжить на практике

  • Микросервисы на Go: HTTP, PostgreSQL, gRPC, Kafka и Docker ↗

    От работающего Go API — к взаимодействию сервисов, которому можно доверять. Разберитесь, как проектировать контракты, сохранять согласованность данных и восстанавливать обработку после сбоев. HTTP, PostgreSQL, gRPC, Kafka и Docker — через код и наглядные разборы.