httpapi: точки метрики за период отдаются одним запросом
- `GET /api/v1/metrics/{name}?from&to&layer` — ряд точек за период; конверт
объявляет слой, измеренный род, его применимость к отданному ряду и границу
окна измерения, а сам ряд собирается из одного слоя, выбранного по охвату
точек внутри периода
- use-case вынесен в `internal/points`, чтение — одним входом `store.ReadSeries`
под одной транзакцией; правило выбора слоя остаётся в домене и приходит в
хранилище колбэком
- `writeJSON` перестал экранировать HTML-символы и перестал глушить отказ
записи: дословность содержимого точки иначе не удерживается, а оборванное
тело уходило под видом успешного `200`
This commit is contained in:
+61
-15
@@ -230,7 +230,8 @@ capability**, и здесь стоит ссылка, а не пересказ т
|
||||
| `replay` | проигрывание журнала в витрину: состав, порядок, отчёт | [`reindex`](../openspec/specs/reindex/spec.md) |
|
||||
| `catalog` | каталог разрезов и измерение рода агрегации | [`catalog`](../openspec/specs/catalog/spec.md) |
|
||||
| `store` | SQLite: доставки, часовые объекты, тренировки, записи | [`storage`](../openspec/specs/storage/spec.md) |
|
||||
| `httpapi` | приём, read API и **форма провода** ответов чтения | [`ingest`](../openspec/specs/ingest/spec.md), [`catalog`](../openspec/specs/catalog/spec.md), [`read-api`](../openspec/specs/read-api/spec.md) |
|
||||
| `points` | ряд точек метрики за период: выбор слоя, применимость рода | [`points`](../openspec/specs/points/spec.md) |
|
||||
| `httpapi` | приём, read API и **форма провода** ответов чтения | [`ingest`](../openspec/specs/ingest/spec.md), [`catalog`](../openspec/specs/catalog/spec.md), [`read-api`](../openspec/specs/read-api/spec.md), [`points`](../openspec/specs/points/spec.md) |
|
||||
|
||||
## Приём
|
||||
|
||||
@@ -835,7 +836,7 @@ hour метки выровнены на час heart_rate 00:00:00
|
||||
доставки той же автоматизации; если её не было, берём **надёжный** заголовок
|
||||
(`Minutes` → `minute`, `Hours` → `hour`). Иначе точки не сохраняются вовсе:
|
||||
молчаливый `raw` создал бы призрачный разрез, который поедет в каталог и в
|
||||
правило Read API «самый мелкий слой, покрывающий диапазон».
|
||||
правило Read API выбора слоя (см. «Read API»).
|
||||
|
||||
Слово «предшествующей» в третьем пункте несёт вес: слой обязан быть функцией от
|
||||
**префикса журнала**. Наследование от последней доставки вообще делает свёртку
|
||||
@@ -1446,7 +1447,7 @@ MongoDB, и так просилось из слова «перезаписыва
|
||||
|
||||
```
|
||||
GET /api/v1/metrics каталог: имя, units, род, слои с диапазонами
|
||||
GET /api/v1/metrics/{name}?from&to&bucket&layer точки метрики, при желании свёрнутые
|
||||
GET /api/v1/metrics/{name}?from&to&layer точки метрики за период (bucket — соседняя задача, пока 400)
|
||||
GET /api/v1/workouts?from&to заголовки тренировок
|
||||
GET /api/v1/workouts/{id} тренировка целиком, с маршрутом
|
||||
GET /api/v1/records/{kind}?from&to прочие секции
|
||||
@@ -1497,9 +1498,21 @@ GET /healthz
|
||||
законно есть дыры. Поэтому правило выбора слоя опирается на фактические объекты
|
||||
запрошенного диапазона, а не на каталожную пару границ.
|
||||
|
||||
Параметр `layer` выбирает разрез. Если он не указан — берём **самый мелкий
|
||||
слой, покрывающий весь запрошенный диапазон**. Молча переключать слой на
|
||||
границе периода нельзя: ряд поедет незаметно для клиента.
|
||||
Параметр `layer` выбирает разрез. Если он не указан — берём слой с **наибольшим
|
||||
охватом внутри запрошенного периода**, а при равном охвате самый мелкий (порядок
|
||||
`sample` → `raw` → `minute` → `hour` → `day`). Молча переключать слой на границе
|
||||
периода нельзя: ряд поедет незаметно для клиента, и ряд из одного ответа всегда
|
||||
собран из одного слоя.
|
||||
|
||||
**Охват — длина пересечения** отрезка «первая метка слоя … последняя метка слоя»
|
||||
с периодом; слой с пустым пересечением выбывает. Меряется он метками **точек**,
|
||||
а не часами объектов. Почему прежняя формулировка («самый мелкий, покрывающий
|
||||
весь диапазон») пересмотрена, почему мера именно такая и во что она обошлась —
|
||||
[ADR-2026-08-04-sloy-vybiraetsya-po-ohvatu-tochek](adr/ADR-2026-08-04-sloy-vybiraetsya-po-ohvatu-tochek.md).
|
||||
|
||||
Словарь слоёв при этом **один** (`hae.Layers`): из него выводятся и порядок, и
|
||||
перечень слоёв в выборке охватов, и проверка параметра запроса, и текст отказа
|
||||
клиенту.
|
||||
|
||||
### Условный запрос
|
||||
|
||||
@@ -1578,21 +1591,54 @@ GET /healthz
|
||||
Нормализованная оболочка, сырое содержимое:
|
||||
|
||||
```json
|
||||
{"layer": "minute", "bucket": "hour", "aggregation": "sum",
|
||||
{"metric": "heart_rate",
|
||||
"from": "2026-07-31T00:00:00Z", "to": "2026-08-01T00:00:00Z",
|
||||
"layer": "minute", "bucket": null,
|
||||
"aggregation": {"style": "instant", "applicable": true,
|
||||
"last_hour": "2026-08-02T14:00:00Z"},
|
||||
"points": [
|
||||
{"ts": "2026-07-31T09:00:00Z", "tz_offset": 10800, "units": "count",
|
||||
"values": {"qty": 812}}
|
||||
{"ts": "2026-07-31T09:00:00Z", "ts_end": "2026-07-31T09:00:00Z",
|
||||
"tz_offset": 10800, "units": "count", "values": {"qty": 812}}
|
||||
]}
|
||||
```
|
||||
|
||||
`layer`, `bucket` и `aggregation` присутствуют всегда, даже когда свёртки не
|
||||
было (`"bucket": null`): клиент не должен выводить их наличием или
|
||||
отсутствием поля.
|
||||
Все поля присутствуют ВСЕГДА, даже когда сообщить нечего: клиент не должен
|
||||
выводить исход наличием или отсутствием поля. `bucket` равен `null`, когда
|
||||
свёртки не было; `layer` — `null`, когда слой выбирала система и выбирать было
|
||||
не из чего (явно запрошенный слой уезжает всегда, в том числе при пустом ряде).
|
||||
|
||||
`aggregation` — **объект, а не строка**. Строка называла бы только применённую
|
||||
свёртку, а инвариант требует, чтобы клиент видел ещё и основание (решение и
|
||||
разбор чужих API —
|
||||
[ADR-2026-08-04-otvet-tochek-nesyot-rod-i-ego-primenimost](adr/ADR-2026-08-04-otvet-tochek-nesyot-rod-i-ego-primenimost.md)):
|
||||
|
||||
- `style` — измеренный род метрики, тот же словарь, что у каталога;
|
||||
- `applicable` — применим ли род к **отданному ряду**. Род есть свойство
|
||||
метрики, слой — свойство ряда, и сочетание `{"layer": "raw", "style":
|
||||
"cumulative"}` законно и штатно: оно приглашает потребителя сложить
|
||||
интерполяцию самому и завысить втрое. Система при этом не складывает ничего —
|
||||
а потребитель об инварианте не знает;
|
||||
- `last_hour` — ярлык самого свежего часа окна измерения. Окно считается в
|
||||
**общих** часах, а не в часах календаря: выключенная минутная автоматизация
|
||||
HAE останавливает их пополнение, окно замирает и продолжает объявлять род.
|
||||
Это единственный след.
|
||||
|
||||
`ts_end` — конец координаты точки; у точки-измерения равен `ts`. Он есть потому,
|
||||
что идентичность точки — интервал, а не метка: под одной меткой лежит до трёх
|
||||
записей сна, и конверт с одним `ts` предлагал бы клиенту различать их, разбирая
|
||||
дословное содержимое.
|
||||
|
||||
Принадлежность точки периоду определяется её **началом** — тем же правилом,
|
||||
каким час объекта берётся по началу. Цена названа: «сон за ночь с полуночи» не
|
||||
увидит эпизод, начавшийся в 23:40.
|
||||
|
||||
Время приведено к единому виду, значения отданы как пришли: ни
|
||||
переименований, ни пересчёта единиц. Метрик у Apple много и они разные —
|
||||
семантику разбирает клиент по имени метрики. Полная нормализация означала бы,
|
||||
что каждая новая метрика требует правки коллектора, а незнакомая теряется.
|
||||
переименований, ни пересчёта единиц, ни экранирования (сериализатор ответа
|
||||
HTML-символы не экранирует — иначе `&` в имени источника уезжал бы как
|
||||
`\u0026`, и обещание дословности переставало быть правдой). Метрик у Apple
|
||||
много и они разные — семантику разбирает клиент по имени метрики. Полная
|
||||
нормализация означала бы, что каждая новая метрика требует правки коллектора,
|
||||
а незнакомая теряется.
|
||||
|
||||
### Форма провода
|
||||
|
||||
|
||||
Reference in New Issue
Block a user