# Миграция klog/zap → slog ## Контекст `tools/klog` — тонкая обёртка над `go.uber.org/zap`, которая является точкой входа для логирования во всех сервисах. Модуль: `gitlab.adsw.io/platform/tools/v3`, потребляется 15+ репозиториями через vendor. ## Текущий формат логов (сохраняется) ```json {"level":"info","caller":"main.go:42","msg":"starting service","source":"accounting","date":"2026-05-08T12:34:56.789+03:00"} {"level":"error","caller":"auth.go:28","msg":"fetching user profile","source":"accounting","date":"2026-05-08T12:34:56.789+03:00","error":"context deadline exceeded"} ``` Поля: `level` (строчными), `caller`, `msg`, `source` (при init), `date` (ISO 8601 в каждом вызове через `cookFields()`). Формат воспроизводится через `log/slog` с кастомным JSON handler'ом + `ReplaceAttr` для приведения уровней к нижнему регистру. ## Стратегия Публичный API `klog` **не меняется** — сервисы обновляют vendor и ничего не ломается. Единственное breaking изменение: `SetLogger(*zap.Logger)` → `SetLogger(*slog.Logger)`. Используется только в тестах через `zap.NewNop()`. **Решение:** добавить `SetNopLogger()` — тесты меняют одну строку без импорта zap. --- ## Шаг 0 — tools (делается первым, разблокирует всё остальное) | # | Файл | Что менять | Сложность | |---|------|------------|-----------| | 1 | `tools/klog/log.go` | Переписать на `log/slog` с кастомным JSON handler'ом, сохранить формат, добавить `SetNopLogger()` | Средняя | | 2 | `tools/go.mod` | Удалить `go.uber.org/zap` из `require` | Низкая | | 3 | `tools/vendor/` | `go mod tidy && go mod vendor` | Низкая | После выпустить тег `v3.9.0`. --- ## Шаг 1 — тесты с `klog.SetLogger(zap.NewNop())` (критично) Это **единственные места**, где внешний код завязан на тип `*zap.Logger`. После обновления vendor без этих правок сервисы **не скомпилируются**. Замена везде одинаковая: `klog.SetLogger(zap.NewNop())` → `klog.SetNopLogger()` | # | Репозиторий | Файл | Сложность | |---|-------------|------|-----------| | 4 | `pilot` | `internal/service/agreement_link_test.go` | Низкая | | 5 | `pilot` | `internal/service/application_test.go` | Низкая | | 6 | `pilot` | `internal/service/distro_download_link_test.go` | Низкая | | 7 | `pilot` | `internal/service/distro_test.go` | Низкая | | 8 | `pilot` | `internal/service/download_history_test.go` | Низкая | | 9 | `pilot` | `internal/service/download_link_test.go` | Низкая | | 10 | `pilot` | `internal/service/mail_test.go` | Низкая | | 11 | `hr` | `internal/service/department_test.go` | Низкая | | 12 | `rubytech` | `internal/service/client_test.go` | Низкая | | 13 | `rubytech` | `internal/service/customer_test.go` | Низкая | | 14 | `rubytech` | `internal/service/project_test.go` | Низкая | | 15 | `user` | `internal/private/graph/resolver_test.go` | Низкая | | 16 | `user` | `internal/service/role_test.go` | Низкая | | 17 | `user` | `internal/service/session_test.go` | Низкая | | 18 | `user` | `internal/service/user_test.go` | Низкая | --- ## Шаг 2 — vendor-бамп во всех сервисах (рутина, не требует изменений кода) Для сервисов без `SetLogger(zap.NewNop())` достаточно только обновить vendor. Для `pilot`, `hr`, `rubytech`, `user` — сначала шаг 1, потом vendor-бамп. ```bash go get gitlab.adsw.io/platform/tools/v3@v3.9.0 go mod vendor ``` | # | Репозиторий | Зависимость | Сложность | |---|-------------|-------------|-----------| | 19 | `accounting` | — | Низкая | | 20 | `async` | — | Низкая | | 21 | `document-generator` | — | Низкая | | 22 | `education` | — | Низкая | | 23 | `file-manager` | — | Низкая | | 24 | `hr` | после п.11 | Низкая | | 25 | `mailer` | — | Низкая | | 26 | `partner` | — | Низкая | | 27 | `pilot` | после пп.4–10 | Низкая | | 28 | `private-api-gateway` | — | Низкая | | 29 | `public-api-gateway` | — | Низкая | | 30 | `rubytech` | после пп.12–14 | Низкая | | 31 | `store` | — | Низкая | | 32 | `unisender` | — | Низкая | | 33 | `user` | после пп.15–18 | Низкая | | 34 | `yandex` | — | Низкая | --- ## Шаг 3 — api-gateway (отдельно, не блокирует остальное) `api-gateway` держит `*zap.Logger` как поле структуры `Application` и передаёт его в `log.NewZapLogger()` из библиотеки `jensneuse/abstractlogger`. Это **независимо от klog** и не блокирует миграцию остальных сервисов. | # | Файл | Проблема | Сложность | |---|------|----------|-----------| | 35 | `api-gateway/pkg/application/application.go` | Поле `ZapLogger *zap.Logger` — `abstractlogger` требует zap. Нужно либо оставить zap только здесь, либо заменить `abstractlogger` на slog-совместимый аналог | Высокая | | 36 | `api-gateway/cmd/http/main.go` | `klog.InitLogger()` возвращает `*zap.Logger` сейчас, после миграции вернёт `*slog.Logger` — нужно отвязать инициализацию zap от klog | Средняя | --- ## Порядок выполнения ``` Шаг 0: tools/klog/log.go → slog + SetNopLogger() → тег v3.9.0 │ ├─► Шаг 1: исправить 15 тестовых файлов (pilot, hr, rubytech, user) │ └─► Шаг 2: vendor-бамп в каждом сервисе по мере готовности (в течение месяца) │ └─► Шаг 3: api-gateway — решить вопрос с abstractlogger (отдельно) ``` Шаг 0 и Шаг 1 должны быть выполнены **до** того, как любой сервис обновит vendor.