- internal/logging: LOG_LEVEL/LOG_FORMAT parsing and slog handler - request_id middleware (X-Request-ID or generated, echoed) + debug access log - httpx registers errors; middleware logs internal causes with request_id - health excluded, objects/sse debug-only, no query/body/secrets in logs - new observability capability spec; README/.env.example docs
7.2 KiB
Design
Context
См. proposal.md — Why. Текущее состояние, важное для подхода:
- Логирование инициализируется в
backend/cmd/server/main.go:34жёстко (slog.NewJSONHandler(os.Stdout, LevelInfo)), без чтения конфига. - Роутер —
gin.New()+gin.Recovery()(internal/app/app.go:94); access-логов,request_idи middleware корреляции нет. Есть толькоauth.Middlewareна группе/api(app.go:122). httpx.RespondError/Abort(internal/httpx/httpx.go:55-71) для не-AppErrorотвечают generic500и не логируют причину.slogуже используется точечно (internal/db/db.go,internal/photos/worker.go), версия Go 1.26,gin v1.12.
Goals / Non-Goals
Goals:
- Управляемые уровень и формат логов без изменения кода.
- Один след на запрос:
request_idво всех строках, порождённых этим запросом. - Видимость причины внутренних ошибок в логах.
- Отсутствие утечки секретов (токены, пароли, query, тела, подписанные URL).
Non-Goals:
- Доменные (операционные) логи в сервисах (
album.created,photo.uploaded) — слой C, не входит. - Метрики, трейсинг, экспорт во внешние системы.
- Изменение CLI-команд (
migrate,reconcile,backfill-exif). - Изменение формата ответов API.
Decisions
D1. Конфигурация уровня и формата
Новые env LOG_LEVEL (debug|info|warn|error, default info) и LOG_FORMAT
(json|text, default json). Значения валидируются в config.Load; недопустимое
значение — ошибка старта. main.go строит handler из конфига.
- Альтернатива: булев
GALO_DEBUG— отклонено, слишком грубо (нет warn/error отдельно) и не даёт формат. - Новый пакет
internal/loggingинкапсулирует разбор уровня и сборку handler (NewHandler(format, level)), чтобы не размазыватьslog-детали поconfigиmain.
D2. Идентификатор запроса
Middleware (B) берёт X-Request-ID из входящего запроса, если он корректной формы
(ограничение длины и допустимые символы), иначе генерирует новый. request_id:
- кладётся в
context.Contextзапроса (logging.WithRequestID); - возвращается клиенту в заголовке ответа
X-Request-ID.
Так связка «запрос клиента ↔ строка в логе» работает без доступа к внутреннему состоянию.
- Альтернатива: не принимать клиентский id и всегда генерировать — проще, но теряет сквозную корреляцию с внешним прокси. Выбрано «принять валидный, иначе сгенерировать».
D3. Логирование внутренних ошибок
httpx.RespondError/Abort перестают молча выдавать 500: для ошибки, не являющейся
*AppError, они вызывают c.Error(err) (gin накапливает, не пишет ответ). Middleware (B)
после c.Next() записывает все собранные ошибки на уровне error с причиной и request_id.
- Классификация:
*AppErrorсо статусом< 500→debug;*AppErrorсо статусом>= 500и любая не-AppError→error. - Альтернатива: логировать прямо в
httpxчерезslog.ErrorContext— меньше правок, но два независимых места логирования и дублированиеrequest_id. Выбрана централизация в middleware. log/slogне имеет контекстных атрибутов «из коробки»;request_idподставляется явно изlogging.RequestID(ctx).
D4. Access-лог
Один middleware в app.Router() до групп маршрутов; после c.Next() пишет строку
уровня debug: method, route (c.FullPath(), без query), status, latency,
bytes, client_ip, user_id (auth.UserID, если установлен), request_id.
user_idдоступен, т.к. middleware оборачиваетauth.Middlewareи читает контекст после завершения цепочки.
D5. Уровни (сводно)
| Событие | Уровень |
|---|---|
| Старт, бэкенды, миграции, storage ready (как сейчас) | info |
Причина внутренней ошибки (не-AppError, 5xx) |
error |
| Access-строка запроса | debug |
Отказ 4xx (AppError < 500) |
debug |
D6. Фильтрация и приватность
/api/health— всегда исключён из access-лога./api/objects/*и/api/sse/*— access-логи только наdebug(объём/долгие соединения).- Никогда не логируются: query-строка целиком (там
?token=для SSE), подписанные URL, заголовокAuthorization, тела запросов, пароли.
Risks / Trade-offs
- [Рост объёма на
debugот/api/objects/*и SSE] -> фильтры из D6; наinfoобъём не растёт (access/4xx —debug). - [Спуфинг
X-Request-IDклиентом] -> принимаем только валидную форму с ограничением длины; иначе генерируем. - [Ложная latency для SSE (весь срок соединения)] -> SSE-строки только на
debug; поле трактуется как длительность соединения. - [Утечка секретов через логи] -> D6 запрещает query/body/Authorization; явные поля — только route и метаданные.
- [Двойной ответ при
c.Error] ->c.Errorтолько регистрирует ошибку, ответ уже отправленhttpx; middleware лишь читаетc.Errors.
Migration Plan
Изменение кода и конфигурации, без миграции данных. При отсутствии новых env поведение
соответствует LOG_LEVEL=info, LOG_FORMAT=json — обычные логи почти не меняются. Для
диагностики выставить LOG_LEVEL=debug. Откат — вернуть прежний жёсткий handler.
Open Questions
Нет.