- `GET /api/v1/metrics/{name}?from&to&layer` — ряд точек за период; конверт
объявляет слой, измеренный род, его применимость к отданному ряду и границу
окна измерения, а сам ряд собирается из одного слоя, выбранного по охвату
точек внутри периода
- use-case вынесен в `internal/points`, чтение — одним входом `store.ReadSeries`
под одной транзакцией; правило выбора слоя остаётся в домене и приходит в
хранилище колбэком
- `writeJSON` перестал экранировать HTML-символы и перестал глушить отказ
записи: дословность содержимого точки иначе не удерживается, а оборванное
тело уходило под видом успешного `200`
31 KiB
points Specification
Purpose
Ряд точек одной метрики за период — то, ради чего собирается витрина. Каталог
отвечает «что у тебя есть», этот маршрут — «дай значения». Форма запроса и его
разбор, правило выбора слоя, состав конверта и объявление измеренного рода
вместе с границей окна измерения живут здесь; общие правила читающих маршрутов —
в capability read-api.
Requirements
Requirement: Точки метрики за период отдаются одним запросом
Система SHALL отдавать значения одной метрики за запрошенный период по
GET /api/v1/metrics/{name} с обязательными параметрами from и to, не
требуя от потребителя доступа к файлу базы и не требуя второго запроса за
смыслом отданных чисел.
Имя метрики берётся из пути ровно один раз декодированным и далее
дословно: система его не нормализует и не сверяет со списком известных — имена
приходят из тела доставки и хранилищу не принадлежат. Повторное декодирование
MUST NOT происходить: имя, само содержащее процентную последовательность
(a%41b), после второго декодирования становится именем другой метрики, и
маршрут отвечает 200 с её данными. Имя, которое путём не
выражается (пустое), маршрутом недостижимо; каталог такую метрику показывает, и
это названная цена адресации именем в пути, а не молчание.
Маршрут закрыт токеном чтения тем же контуром, что и каталог, и MUST NOT
перехватывать маршрут каталога GET /api/v1/metrics.
Scenario: Период запрошен
- GIVEN метрика, у которой в витрине есть объекты внутри периода
- WHEN потребитель запрашивает
GET /api/v1/metrics/{name}?from=…&to=…с действующим токеном чтения - THEN ответ
200несёт значения этой метрики за этот период
Scenario: Каталог остаётся достижим
- WHEN потребитель запрашивает
GET /api/v1/metrics - THEN отвечает каталог, а не маршрут точек
Scenario: Имя метрики закодировано в пути
- GIVEN метрика, чьё имя содержит символ, требующий процентного кодирования
- WHEN потребитель запрашивает её точки, закодировав имя
- THEN отвечают точки этой метрики, а не пустой ряд
Scenario: Имя метрики само содержит процентную последовательность
- GIVEN метрики с именами
a%41bиaAbв витрине - WHEN потребитель запрашивает
a%41b, закодировав имя - THEN отвечают точки
a%41b, а не точкиaAb
Scenario: Токен чтения отсутствует
- WHEN запрос точек приходит без действующего токена чтения при непустом списке токенов чтения
- THEN ответ
401, и тело ответа собрано объявленным типом транспорта
Requirement: Конверт ответа объявляет слой, род свёртки, его применимость и границу окна измерения
Система SHALL сопровождать точки конвертом, в котором ВСЕГДА присутствуют поля
metric, from, to, layer, bucket, aggregation и points, а объект
aggregation MUST всегда нести поля style, applicable и last_hour. Поле,
которому нечего сообщить, MUST уезжать как null и MUST NOT исчезать из ответа
и MUST NOT подменяться нулевым значением своего типа.
Смысл полей:
metric— имя метрики, как оно пришло путём после декодирования;fromиto— фактически применённые границы периода, нормализованные к UTC в RFC 3339;layer— слой, из которого собран ряд;bucket— сетка свёртки;null, когда свёртки не было;aggregation.style— измеренный род метрики (cumulative/instant/unknown) тем же правилом и тем же окном, что у каталога;aggregation.applicable— применим ли объявленный род к отданному ряду;aggregation.last_hour— ярлык самого свежего часа окна измерения;null, когда окно пусто;points— ряд, пустой коллекцией[], а неnull.
last_hour обязателен именно потому, что окно измерения считается в общих
часах, а не в часах календаря: выключенная минутная автоматизация HAE
останавливает пополнение общих часов, окно замирает и продолжает объявлять род.
Без last_hour у клиента нет ни одного способа это увидеть.
Scenario: Свёртки не было
- WHEN маршрут отвечает на запрос без сетки
- THEN в ответе присутствуют
metric,from,to,layer,bucket,aggregationсо всеми тремя полями иpoints, причёмbucketравенnull
Scenario: Окно измерения замерло
- GIVEN метрика, чьи самые свежие общие часы старше конца запрошенного периода
- WHEN маршрут отвечает
- THEN
aggregation.last_hourназывает ярлык самого свежего общего часа, а не конец периода и не текущее время
Scenario: Окна измерения нет вовсе
- GIVEN метрика, у которой нет ни одного общего часа минутного и часового слоёв
- WHEN маршрут отвечает
- THEN
aggregation.styleравен"unknown", аaggregation.last_hourравенnull
Requirement: Род, объявленный в ответе, едет вместе со своей применимостью к отданному ряду
Система SHALL объявлять aggregation.applicable равным false всегда, когда
объявленный род нельзя применить к отданному ряду, и MUST считать неприменимым
род cumulative на нижнем слое HAE (raw), род unknown и любой род при
отсутствии выбранного слоя. Система MUST NOT досчитывать свёртку по
неизмеренному роду и MUST NOT выражать неизвестность отсутствием поля.
Поле существует потому, что род — свойство метрики, а слой — свойство
отданного ряда, и их сочетание бывает опасным: нижний слой HAE это
интерполяция, а не сэмплы, и сумма по нему завышает втрое. Конверт, объявляющий
cumulative рядом с рядом из raw и молчащий о неприменимости, приглашает
потребителя сложить интерполяцию самостоятельно — система при этом не
складывает ничего, а решение у потребителя уже принято по завышенному числу.
Scenario: Род метрики не измерен
- GIVEN метрика, у которой род агрегации не измерен
- WHEN потребитель запрашивает её точки за период
- THEN ответ
200несёт точки как есть,aggregation.styleравен"unknown", аaggregation.applicableравенfalse
Scenario: Накопительная метрика отдана нижним слоем
- GIVEN метрика с измеренным родом
cumulative, ряд которой собран из слояraw - WHEN маршрут отвечает
- THEN
aggregation.styleравен"cumulative", аaggregation.applicableравенfalse
Scenario: Род применим
- GIVEN метрика с измеренным родом, ряд которой собран из слоя
minute,hourилиsample - WHEN маршрут отвечает
- THEN
aggregation.applicableравенtrue
Requirement: Слой выбирается по охвату точек внутри периода и не меняется внутри ответа
Система SHALL собирать ряд ровно из одного слоя и MUST NOT склеивать в одном
ответе точки разных слоёв. При отсутствии параметра layer система SHALL брать
слой с наибольшим охватом внутри запрошенного периода, а при равном охвате —
самый мелкий слой в порядке sample, raw, minute, hour, day.
Охват меряется метками точек, а не часами объектов. Охват слоя есть длина
пересечения отрезка [первая метка слоя, последняя метка слоя] с запрошенным
периодом; слой с пустым пересечением из выбора MUST выбывать. Мера названа
именно так потому, что объекты адресуются часом, а ряд отбирается точной меткой:
на периоде короче часа множество «слоёв с объектами» и множество «слоёв с
точками» расходятся, и слой, выбранный по часам, отдал бы пустой ряд при
непустых данных соседнего слоя.
Охват сравнивается между слоями, а не с запрошенным периодом: границы данных законно короче запроса и законно имеют дыры внутри.
Заданный параметр layer отменяет правило целиком: система SHALL отдавать
запрошенный разрез, в том числе пустым, и SHALL называть его в ответе.
Scenario: Мелкий слой охватывает меньше крупного
- GIVEN метрика, у которой внутри периода нижний слой покрывает несколько дней, а часовой — весь период
- WHEN потребитель запрашивает период без параметра
layer - THEN ряд собран из часового слоя, и
layerназывает его
Scenario: Охваты равны
- GIVEN метрика, у которой два слоя охватывают внутри периода одно и то же
- WHEN потребитель запрашивает период без параметра
layer - THEN ряд собран из более мелкого слоя
Scenario: Период короче часа
- GIVEN период внутри одного часа, в котором у крупного слоя есть объект без единой точки внутри периода, а у мелкого — точки внутри периода
- WHEN потребитель запрашивает период без параметра
layer - THEN ряд собран из мелкого слоя и не пуст
Scenario: Слой задан явно
- WHEN потребитель задаёт
layerявно - THEN ряд собран из этого слоя, даже если другой слой охватывает период
шире, и
layerв ответе равен запрошенному
Scenario: Имя слоя незнакомо
- WHEN параметр
layerприсутствует, а его значение не является одним изsample,raw,minute,hour,day - THEN ответ
400, и умолчание молча не подставляется
Scenario: Значение слоя пусто
- WHEN параметр
layerприсутствует с пустым значением - THEN ответ
400, а не автоматический выбор слоя
Requirement: Период задаётся явно и разбирается строго
Система SHALL требовать оба параметра from и to, SHALL принимать их только в
формате RFC 3339 с явным смещением зоны и SHALL толковать период как
полуинтервал [from, to). Отсутствующий параметр, неразбираемое значение,
значение без явной зоны, from >= to и граница, чей год после приведения к UTC
выходит за диапазон 1–9999, MUST давать 400 с человекочитаемым сообщением,
которое MUST NOT содержать значений из запроса, и MUST NOT подменяться
умолчанием.
Граница за пределами четырёхзначного года отвергается потому, что объекты
адресуются строкой RFC 3339 и границы сравниваются лексикографически:
9999-12-31T23:00:00-07:00 становится 10000-01-01T06:00:00Z, который как
строка меньше любой настоящей метки, — и запрос молча отдал бы пустой ряд при
непустых данных.
Полуинтервал взят потому, что соседние окна обязаны склеиваться без двойного счёта граничной точки. Явная зона обязательна потому, что вопрос «в какой зоне считать сутки» в проекте открыт: принять голую дату значило бы выбрать зону за клиента молча.
Параметр bucket этой версией маршрута не поддержан: система SHALL отвечать
400 на его присутствие с любым значением и MUST NOT игнорировать его молча.
Молчаливое игнорирование дало бы клиенту, попросившему суточную сетку, полный
минутный ряд — зеркало того самого промаха, ради которого соседняя задача
различает «указали сетку» как информацию и как защиту. Прочие незнакомые
параметры запроса система игнорирует.
Scenario: Параметр периода отсутствует
- WHEN в запросе нет
fromили нетto - THEN ответ
400, и период умолчанием не подставляется
Scenario: Метка времени без зоны
- WHEN значение
fromилиtoзаписано без явного смещения зоны - THEN ответ
400
Scenario: Границы периода вывернуты
- WHEN
fromне раньшеto - THEN ответ
400
Scenario: Граница выходит за четырёхзначный год
- WHEN граница периода после приведения к UTC попадает в год за пределами диапазона 1–9999
- THEN ответ
400, а не200с пустым рядом
Scenario: Запрошена сетка свёртки
- WHEN в запросе присутствует параметр
bucket - THEN ответ
400, называющий, что свёртка ещё не поддержана
Scenario: Точка стоит ровно на границе
- GIVEN точки с метками ровно в
fromи ровно вto - WHEN маршрут отвечает
- THEN точка на
fromв ответе есть, а точка наto— нет
Requirement: Значение точки уезжает дословно, нормализовано только время
Система SHALL отдавать каждую точку объектом {ts, ts_end, tz_offset, units, values}, где values MUST быть содержимым точки ровно в том виде, в каком его
сохранило хранилище, без переименования полей, пересчёта единиц, отбрасывания
незнакомого и без экранирования. ts и ts_end MUST быть нормализованными
к UTC метками начала и конца координаты точки; у точки-измерения ts_end равен
ts. tz_offset и units MUST принадлежать самой точке: смещение исходной
зоны — её собственное, единицы — того объекта, из которого точка прочитана.
Точки MUST идти по возрастанию (ts, ts_end). Порядок этим определён
однозначно: пара (начало, конец) внутри одного слоя одной метрики есть ключ
идентичности, и двух точек с равной парой в витрине не существует.
Принадлежность точки периоду определяется её началом: точка-интервал,
начавшаяся раньше from, в ответ не входит, даже если её конец лежит внутри
периода. Правило то же, каким час объекта берётся по началу точки; цена названа
вслух — запрос «сон за ночь с полуночи» не увидит эпизод, начавшийся до неё.
ts_end присутствует потому, что идентичность точки — интервал, а не метка: под
одной меткой лежит до трёх записей сна, и конверт с одним ts предлагал бы
клиенту различать их, разбирая дословное содержимое.
Scenario: Точка-интервал
- GIVEN точка, у которой конец координаты отличается от начала
- WHEN маршрут отвечает
- THEN
ts_endотличается отtsи называет конец координаты
Scenario: Содержимое не переписывается
- WHEN точка уезжает клиенту
- THEN
valuesпобайтово совпадает с сохранённым содержимым точки
Scenario: Содержимое несёт символы, которые сериализатор склонен экранировать
- GIVEN точка, чьё содержимое несёт
&,<или> - WHEN маршрут отвечает
- THEN эти символы уезжают как есть, а не escape-последовательностями
Requirement: Пустота периода — успех, а не отсутствие ресурса
Система SHALL отвечать 200 на запрос метрики, у которой нет данных в
запрошенном периоде, и MUST отдавать points пустым списком. Система MUST NOT
отвечать 404 по признаку «нет данных».
layer равен null только тогда, когда слой выбирала система и выбирать
было не из чего. Заданный клиентом layer уезжает в ответе всегда, даже когда
ряд пуст: иначе клиент, спросивший разрез поимённо, не отличил бы «этого разреза
за период нет» от «маршрут проигнорировал параметр».
Различать опечатку в имени метрики и честную пустоту — работа каталога: список имён маршруту точек не принадлежит.
Scenario: Данных за период нет и слой не задан
- WHEN у метрики нет ни одной точки внутри периода и параметр
layerне задан - THEN ответ
200,pointsравен[],layerравенnull
Scenario: Данных за период нет, а слой задан
- WHEN у метрики нет точек запрошенного слоя внутри периода
- THEN ответ
200,pointsравен[],layerравен запрошенному
Scenario: Имени метрики в витрине нет вовсе
- WHEN запрошено имя метрики, которого в витрине нет
- THEN ответ
200с пустымpoints, а не404
Requirement: Ответ снимается одним снимком витрины
Система SHALL читать охваты слоёв, точки выбранного слоя и объекты окна измерения в одной транзакции чтения. Смесь «слой выбран до коммита свёртки, точки прочитаны после» и «род измерен на третьем состоянии» дала бы ответ, внутренне противоречивый и неотличимый от обычного свежего; пара проб версии такой ответ обнаруживает, но не предотвращает — она снимает метку, а тело всё равно уезжает.
Scenario: Свёртка коммитит во время чтения
- WHEN фоновая свёртка коммитит в витрину между выбором слоя и чтением точек
- THEN ответ собран из одного снимка витрины, а не из двух состояний
Requirement: Метка ответа включает канонизированную форму запроса и горизонт измерения
Система SHALL помечать ответ меткой, область действия которой MUST быть функцией канонизированной формы запроса — имени метрики, границ периода и слоя — и которая MUST включать горизонт измерения, огрублённый до часа, наравне с версией витрины. При невозможности подписать ответ система SHALL отдавать его без метки, а не отказом.
Область MUST быть ограничена по длине и не выносить значения запроса наружу. Имя метрики приходит из чужого тела дословно и ничем не ограничено: положенное в метку как есть, оно даёт заголовок в тысячи байт, а кавычка внутри имени по HTTP кончает метку — разбор обрежет её там, и условный запрос по такой метрике не сработает никогда. Тот же вход в лог уезжает обрезанным, и у метки предел обязан быть по той же причине.
Канонизация границ MUST сохранять точность, по которой отбираются точки: два запроса, различающиеся долей секунды, дают разные ряды, и одна метка на них подтвердила бы неизменность чужого набора данных.
Горизонт входит в метку по той же причине, по какой он входит в метку каталога:
род объявляется по окну, ограниченному сверху текущее время + запас, и метка
из будущего, лежащая в витрине, въезжает в окно без единого коммита, меняя
aggregation.style и last_hour. Метка без горизонта подтвердила бы
неизменность ответа, в котором род уже перевернулся. Форма запроса входит в
область действия потому, что ответ этого маршрута есть функция параметров: метка
без них однажды подтвердила бы неизменность чужого набора данных. Род и метка
MUST сниматься с одного горизонта.
Цена названа: один полный ответ в час на потребителя при неизменившейся витрине.
Scenario: Запросы различаются параметром
- WHEN два запроса при одном состоянии витрины различаются метрикой, границей периода или слоем
- THEN метки их ответов различны
Scenario: Запросы различаются только формой записи
- WHEN два запроса при одном состоянии витрины задают одно и то же разной записью смещения зоны
- THEN метки их ответов совпадают
Scenario: Границы различаются долей секунды
- WHEN два запроса при одном состоянии витрины различаются границей периода на долю секунды и дают разные ряды
- THEN метки их ответов различны
Scenario: Имя метрики длинное или содержит кавычку
- WHEN запрошена метрика, чьё имя длиной в тысячи символов или содержит кавычку
- THEN метка ответа остаётся короткой и не содержит имени метрики
Scenario: Горизонт перешёл через час
- WHEN горизонт измерения перешёл через границу часа при неизменной версии витрины
- THEN метка ответа изменилась
Scenario: Витрина изменилась во время чтения
- WHEN пробы версии витрины разошлись
- THEN ответ уходит целиком и без метки
Requirement: Исход маршрута точек наблюдаем, а данные о здоровье в лог не уезжают
Система SHALL писать один логирующий чекпоинт исхода на доменной границе
маршрута и MUST NOT доводить до записи лога значения точек, содержимое values
и границы запроса; имя метрики MUST быть обрезано по тому же пределу, что у
каталога. Отмена запроса клиентом и занятость базы MUST писаться уровнем
DEBUG, а настоящий отказ хранилища — уровнем ERROR.
Предупреждения измерения — противоречащий род и данные, помеченные будущим, —
остаются привилегией каталога: маршрут точек их MUST NOT повторять. Агент
опрашивает по расписанию, и WARN на каждый опрос обесценил бы уровень ровно
так же, как обесценила бы его строка на каждый 304.
Scenario: Клиент оборвал запрос
- WHEN потребитель обрывает запрос точек по своему тайм-ауту
- THEN запись об этом уходит уровнем
DEBUG, а неERROR
Scenario: Метрика с противоречащим родом запрошена многократно
- WHEN потребитель повторно запрашивает точки метрики, у которой род противоречив
- THEN маршрут точек предупреждений об этом не пишет
Scenario: Тело ответа оборвалось на середине
- WHEN запись тела ответа отказала после того, как код ответа уже отдан
- THEN отказ записи оставляет собственный чекпоинт и не проходит молча под видом успешного ответа