package httpapi import ( "net/http" "git.vakhrushev.me/av/healthlog/internal/catalog" ) // catalogResponse — оболочка ответа каталога. // // Объект, а не голый массив: список метрик — не единственное, что каталогу // когда-нибудь придётся отдать, а массив верхнего уровня расширить нечем. type catalogResponse struct { Metrics []catalog.Metric `json:"metrics"` } // handleMetrics отдаёт каталог разрезов с измеренным родом агрегации. // // Список приходит из домена уже непустым срезом: nil сериализуется в `null`, и // пустая витрина отдавала бы клиенту `"metrics": null` вместо `[]`. Тест, // сравнивающий разобранные структуры, этого не увидел бы — потому приёмочная // проверка сравнивает байты ответа. Второй страховки здесь нет намеренно: // подстраховка поверх подстраховки прячет отказ первой. // // Условный запрос стоит ПОСЛЕ проверки токена (её ставит роутер) и ДО сборки // снимка: в этом весь смысл — самый частый запрос потребителя есть повтор // неизменившегося, и он не должен стоить ни снимка, ни разжатия точек. // scopeMetrics — область действия метки каталога. Ответ маршрута не зависит от // параметров запроса, поэтому область постоянна; у точек и MCP на её месте // будет канонизированная форма запроса. const scopeMetrics = "metrics" func (a *api) handleMetrics(w http.ResponseWriter, r *http.Request) { // Values, а не Get: `If-None-Match` клиент вправе прислать несколькими // строками, и `Get` увидел бы только первую — часть меток осталась бы // нерассмотренной. if cond := r.Header.Values("If-None-Match"); len(cond) > 0 { // Отказ пробы глушится намеренно: он означает лишь, что условного // ответа не будет, — а настоящий отказ базы всплывёт сборкой каталога // строкой ниже и будет назван ею один раз. // Версию спрашиваем У КАТАЛОГА, а не у хранилища: ответ есть функция не // только состояния витрины, и что ещё в него входит, знает домен. Read // API точек ответит здесь же своей версией, включающей параметры // запроса, — транспорту эти правила знать незачем. if version, err := a.catalog.Version(r.Context()); err == nil { if tag := etag(scopeMetrics, version); notModified(cond, tag) { writeNotModified(w, tag) return } } } snap, err := a.catalog.Metrics(r.Context()) if err != nil { // Исход операции логирует доменный слой, транспорт только переводит его // в ответ. Наружу уходит человекочитаемое сообщение, а не текст ошибки: // в нём имена колонок и форма запроса. writeError(w, http.StatusInternalServerError, "каталог не собрался") return } // Версии может не быть — витрина изменилась, пока ответ собирался. Тогда // ответ уходит без метки: это ровно поведение до появления условного // запроса, то есть деградация в безопасную сторону. setReadHeaders(w, etag(scopeMetrics, snap.Version)) writeJSON(w, http.StatusOK, catalogResponse{Metrics: snap.Metrics}) }