конвенции: перенести механизируемое в golangci-lint и internal/archrules
Правило, которое проверяет машина, не должно оставаться прозой: файл конвенций на сотни строк размазывает внимание по тривиальному — модель добросовестно проверит именование полей лога и не дойдёт до формы решения. Включены sloglint (константный msg, стиль ключ-значение), forbidigo (fmt.Print*, os.Getenv, time.Now мимо store.Now), errorlint (сравнение ошибок), depguard (сторонние пакеты ошибок). internal/archrules — сканеры на то, что линтером не выражается: направление зависимостей ядро↔транспорты, AUTOINCREMENT и серверное время в новых миграциях, матчинг ошибки по тексту. Код приведён к правилам: logging.StartCall как единая точка отсчёта длительности внешних вызовов, store.Now вместо time.Now в httpapi и часах воркера, slog.DiscardHandler в тестах. Перенесённое вычеркнуто из docs/conventions/* и openspec/config.yaml — прозой осталось только то, что правилом не выражается. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+12
-30
@@ -8,14 +8,16 @@ OpenSpec-спеках (`### Requirement` с `SHALL`).
|
||||
Краткая выжимка и инварианты — в [CLAUDE.md](../../CLAUDE.md), раздел
|
||||
«Конвенции кода».
|
||||
|
||||
**Механизировано** (`.golangci.yml`): `slog` вместо `fmt.Print*` — `forbidigo`;
|
||||
константный `msg` и стиль ключ-значение — `sloglint`. Ниже — только то, что
|
||||
правилом не выражается.
|
||||
|
||||
## Принципы
|
||||
|
||||
- Только `log/slog`, без `fmt.Println` и прямой записи в stdout.
|
||||
- Структурированный JSON (`slog.JSONHandler`), один формат для dev и prod.
|
||||
- Сообщение (`msg`) — константный шаблон/категория события; данные — в
|
||||
полях (атрибутах `slog`), а не в интерполяции текста.
|
||||
- Каждое поле — отдельный ключ с типизированным значением. Это даёт
|
||||
фильтрацию и агрегацию через `jq`/DuckDB без регулярок.
|
||||
- Сообщение (`msg`) — категория события; данные — в полях. Каждое поле —
|
||||
отдельный ключ с типизированным значением: это даёт фильтрацию и агрегацию
|
||||
через `jq`/DuckDB без регулярок.
|
||||
|
||||
```json
|
||||
{"time":"2026-06-28T11:23:45.123456Z","level":"INFO","msg":"download accepted","capability":"ingest","download_id":"01jz2k7f8q9r3s4t5v6w7x8y9z","infohash":"…","media_type":"movie","title":"Дюна: Часть вторая"}
|
||||
@@ -24,18 +26,8 @@ OpenSpec-спеках (`### Requirement` с `SHALL`).
|
||||
## Сообщение
|
||||
|
||||
- `msg` — короткая константа в нижнем регистре: `download accepted`,
|
||||
`recognition done`, `layout failed`. Без переменных в тексте.
|
||||
- Данные кладём в атрибуты: `slog.Info("download accepted", "download_id",
|
||||
id, "infohash", ih)`.
|
||||
|
||||
```go
|
||||
// Правильно: msg — категория, данные — поля
|
||||
log.Info("download accepted", "download_id", id, "media_type", "movie")
|
||||
|
||||
// Неправильно: данные зашиты в текст, агрегация ломается
|
||||
log.Info(fmt.Sprintf("download %s accepted as movie", id))
|
||||
```
|
||||
|
||||
`recognition done`, `layout failed`. Данные — в атрибутах:
|
||||
`log.Info("download accepted", "download_id", id, "media_type", "movie")`.
|
||||
- `msg` — чистая категория без неймспейс-префикса: `recognition done`, а не
|
||||
`recognize: done`. Подсистему выносим в поле `capability`
|
||||
(`ingest`/`recognition`/`file-layout`/`review`), не в текст.
|
||||
@@ -131,20 +123,10 @@ ctx = logctx.With(ctx, log) // достаём логгер из ctx в кажд
|
||||
|
||||
## Ошибки
|
||||
|
||||
Go-ошибки логируем как атрибут, не как текст сообщения.
|
||||
Go-ошибки логируем как атрибут, не как текст сообщения:
|
||||
`log.Error("layout failed", "error", err, "download_id", id)`. Ключ — `error`
|
||||
(как по умолчанию в zap/zerolog; единый ключ важнее краткости).
|
||||
|
||||
```go
|
||||
// Правильно: msg — категория, ошибка — поле
|
||||
log.Error("layout failed", "error", err, "download_id", id)
|
||||
|
||||
// Неправильно: ошибка зашита в msg, агрегация по событию ломается
|
||||
log.Error(err.Error())
|
||||
```
|
||||
|
||||
Правила:
|
||||
|
||||
- Ошибку передаём полем `"error", err` — не склеиваем в `msg`. Ключ —
|
||||
`error` (как по умолчанию в zap/zerolog; единый ключ важнее краткости).
|
||||
- Идиома Go — **либо лог, либо возврат, не оба**. Промежуточные слои только
|
||||
оборачивают и возвращают (`fmt.Errorf("…: %w", err)`), не логируя —
|
||||
контекст накапливается в цепочке `%w`.
|
||||
|
||||
Reference in New Issue
Block a user