Files
av 29ca8d415c httpapi: точки метрики за период отдаются одним запросом
- `GET /api/v1/metrics/{name}?from&to&layer` — ряд точек за период; конверт
  объявляет слой, измеренный род, его применимость к отданному ряду и границу
  окна измерения, а сам ряд собирается из одного слоя, выбранного по охвату
  точек внутри периода
- use-case вынесен в `internal/points`, чтение — одним входом `store.ReadSeries`
  под одной транзакцией; правило выбора слоя остаётся в домене и приходит в
  хранилище колбэком
- `writeJSON` перестал экранировать HTML-символы и перестал глушить отказ
  записи: дословность содержимого точки иначе не удерживается, а оборванное
  тело уходило под видом успешного `200`
2026-08-04 18:46:45 +03:00

13 KiB
Raw Permalink Blame History

Критерии приёмки (из постановки, переживают удаление файла задачи)

Задача docs/tasks/items/read-api-points-period.md, «Точки за период».

  • К1. «вес за год» отвечается одним запросом без доступа к файлу базы — оракул: запрос к поднятому сервису на живом архиве. Подмена оракула на этой машине: рабочая база ./data принадлежит живому контейнеру, читать её мимо task up/task run запрещено. Вместо неё сервис поднимается в worktree на своём порту и своей базе, наполненной реальными пакетами internal/hae/testdata. Подмена названа строкой и не выдаётся за исходный оракул: она проверяет маршрут целиком (HTTP, токен, разбор параметров, чтение витрины, форма ответа), но не объём живого архива.
  • К2. в ответе всегда видны layer, aggregation и last_hourоракул: тест на форме ответа.
  • К3. метрика, у которой род не измерен, отдаётся без свёртки и говорит об этом, а не молчит и не досчитывает — оракул: тест на метрике с неизвестным родом.

Приёмочные критерии из рубрики (проход review-rubric, профиль design)

Свойства узла «HTTP-обработчик чтения временного ряда», порождённые до чтения предложения. Проверяются наравне с К1–К3.

  • Р1. Род, объявленный в ответе, применим к отданному ряду: сочетания, из которого клиент выведет разрешённой операцию, запрещённую инвариантом, в конверте нет.
  • Р2. Ответ есть функция того, что уже произошло, а не момента взгляда: всё, что способно измениться без коммита в базу (горизонт, параметры запроса), входит в область действия метки; повтор того же запроса при том же состоянии и тех же часах даёт побайтово тот же ответ.
  • Р3. Ряд собран ровно из одного слоя, слой назван всегда, правило выбора детерминировано на любом входе; предикат выбора слоя и предикат отбора точек используют одну границу.
  • Р4. Отсутствие предела размера — осознанное решение, стоящее на замере того режима, ради которого предел заводится, а не на замере доступного корпуса.
  • Р5. Период — полуинтервал; точка на границе попадает ровно в один из двух соседних ответов; правило принадлежности интервальной точки названо.
  • Р6. Пустой результат — 200, пустая коллекция списком, метаданные на месте; «данных нет» отличимо от «ресурса нет» без второго запроса.
  • Р7. Невозможный запрос отвергается до чтения витрины, кодом 4xx, и текст отказа не содержит значений из запроса.
  • Р8. Время нормализовано и однозначно; значения точки уезжают дословно — без переименования, пересчёта, отбрасывания и экранирования.
  • Р9. Ответ не выглядит полнее, чем он есть: «данных не было» отличимо от «слой выбран по охвату меньше периода» без пересчёта точек.
  • Р10. Выборка идёт по существующему индексу без полного скана витрины, context протянут до драйвера, отмена клиента не считается отказом.
  • Р11. Транспорт не несёт доменной логики; доменный тип до сериализации не доезжает.
  • Р12. Ни значения здоровья, ни токен не доводятся до лога выше DEBUG и до тела отказа ни одним путём.

1. Чтение витрины

  • 1.1 store.ReadSeries(ctx, window, pick)один вход, одна транзакция чтения: охваты слоёв, точки выбранного слоя, объекты окна измерения. Правило выбора слоя приходит функцией-параметром и остаётся в домене
  • 1.2 Охваты слоёв: min(first_ts), max(last_ts), охваты по GROUP BY layer в границах часов [trunc(from), trunc(to)] — по покрывающему индексу bucket_catalog, без чтения содержимого (Р10)
  • 1.3 Точки выбранного слоя: объекты по точному префиксу первичного ключа, разжатие, отбор по началу координаты до [from, to)
  • 1.4 Окно измерения одной метрики: переиспользует commonHours и readHourPairs, второго правила отбора не заводит
  • 1.5 Тесты хранилища: точка 10:59 из объекта 10:00 не теряется; точка ровно на from есть, ровно на to — нет; период короче часа, где крупный слой имеет объект без точек внутри, а мелкий — точки (Р3); пустой период

2. Use-case «ряд точек» (internal/points)

  • 2.1 Пакет internal/points: тип запроса, тип ответа, Service над store и catalog
  • 2.2 Правило выбора слоя: охват = длина пересечения [первая метка, последняя метка] слоя с периодом; пустое пересечение выбывает; наибольший охват, при равенстве — самый мелкий; явный слой отменяет правило и всегда уезжает в ответ (Р3)
  • 2.3 Род через catalog.Measure, применимость — false при unknown, при cumulative на слое raw и при отсутствии выбранного слоя (Р1)
  • 2.4 Метка ответа: версия витрины + горизонт, огрублённый до часа (catalog.Stamp, не второй экземпляр) + канонизированная форма запроса; род и метка снимаются с одного горизонта (Р2)
  • 2.5 Логирующий чекпоинт исхода один и на доменной границе; отмена и занятость базы — DEBUG, настоящий отказ — ERROR; значений точек и границ запроса в записи нет, имя метрики обрезано; предупреждения измерения маршрут точек не повторяет (Р12)
  • 2.6 Тесты домена: охваты различаются; охваты равны; ни одного слоя; явный слой пуст; период короче часа; применимость рода на четырёх слоях

3. Транспорт и форма провода

  • 3.1 Маршрут GET /api/v1/metrics/{name} под токеном чтения; тест, что он не перехватывает GET /api/v1/metrics; имя метрики берётся процентно декодированным
  • 3.2 Разбор параметров: from/to обязательны, RFC 3339 с явной зоной, from < to, layer из словаря, присутствие bucket400; каждый отказ до чтения витрины, с человекочитаемым сообщением без значений из запроса (Р7)
  • 3.3 Форма провода точек по образцу catalog.go: типы *Wire с json-тегами, перевод присваиванием поле в поле, valuesjson.RawMessage
  • 3.4 Строка в таблице образцов wire_internal_test.go (Р11)
  • 3.5 writeJSON перестаёт экранировать HTML-символы (общее правило read-api); тест на значении точки с &, <, > (Р8)
  • 3.6 Метка и Cache-Control через существующий setReadHeaders; тесты: различие меток по каждому параметру по очереди, совпадение при эквивалентной записи времени, различие при переходе горизонта через час (Р2)
  • 3.7 Байтовое утверждение формы ответа на фиксированной витрине с часами в прошлом — ни одно поле литерала не зависит от хода часов (прецедент docs/review.md, 2026-08-03)
  • 3.8 Тесты маршрута: К2 (все поля конверта присутствуют всегда), К3 (род unknown назван словом, applicable: false), точка-интервал (ts_end), дословность values, пустой период без слоя (layer: null), пустой период с явным слоем (layer эхом), незнакомая метрика, 401

4. Документация

  • 4.1 docs/architecture.md, раздел «Read API»: форма ответа точек (aggregation объектом, applicable, ts_end), правило выбора слоя формулировкой из спеки, строка маршрута с пометкой о неподдержанном bucket, строка points в таблице компонентов
  • 4.2 config.example.toml: строка про read_tokens называет точки рядом с каталогом

5. Верификация

  • 5.1 task gate зелёный
  • 5.2 Поведенческая верификация: свой config.toml в worktree (порт :18080, база и архив в ./tmp/), наполнение реальными пакетами internal/hae/testdata через маршрут приёма, затем К1 — метрика за год одним запросом; рабочий ./data не трогается
  • 5.3 Замер цены маршрута числом на трёх режимах, включая худший (Р4, прецедент docs/review.md 2026-08-04 «замер снят на корпусе, где измеряемого случая не бывает»): редкая метрика за год; плотная метрика за сутки в minute; плотная метрика за неделю в raw. Корпус собирается размножением реальных точек testdata, а не литералами; метод записывается рядом с числом
  • 5.4 task verify:busy — зелёный (fold 24.08 с, replay 23.60 с). task verify:archive не прогнан: в worktree нет ./data, а живой архив основного репозитория трогать запрещено; триггер прогона (изменение правила разбора, идентичности или слияния) не сработал — маршрут только читает витрину. Названо строкой в границах покрытия, а не замолчано