httpapi: точки метрики за период отдаются одним запросом
- `GET /api/v1/metrics/{name}?from&to&layer` — ряд точек за период; конверт
объявляет слой, измеренный род, его применимость к отданному ряду и границу
окна измерения, а сам ряд собирается из одного слоя, выбранного по охвату
точек внутри периода
- use-case вынесен в `internal/points`, чтение — одним входом `store.ReadSeries`
под одной транзакцией; правило выбора слоя остаётся в домене и приходит в
хранилище колбэком
- `writeJSON` перестал экранировать HTML-символы и перестал глушить отказ
записи: дословность содержимого точки иначе не удерживается, а оборванное
тело уходило под видом успешного `200`
This commit is contained in:
@@ -16,12 +16,14 @@ import (
|
||||
|
||||
"git.vakhrushev.me/av/healthlog/internal/catalog"
|
||||
"git.vakhrushev.me/av/healthlog/internal/ingest"
|
||||
"git.vakhrushev.me/av/healthlog/internal/points"
|
||||
)
|
||||
|
||||
// Options — зависимости и настройки транспорта.
|
||||
type Options struct {
|
||||
Ingest *ingest.Service
|
||||
Catalog *catalog.Service
|
||||
Points *points.Service
|
||||
Log *slog.Logger
|
||||
WriteTokens []string
|
||||
ReadTokens []string
|
||||
@@ -35,6 +37,7 @@ type Options struct {
|
||||
type api struct {
|
||||
ingest *ingest.Service
|
||||
catalog *catalog.Service
|
||||
points *points.Service
|
||||
log *slog.Logger
|
||||
writeTokens []string
|
||||
readTokens []string
|
||||
@@ -47,6 +50,7 @@ func New(o Options) http.Handler {
|
||||
a := &api{
|
||||
ingest: o.Ingest,
|
||||
catalog: o.Catalog,
|
||||
points: o.Points,
|
||||
log: o.Log,
|
||||
writeTokens: o.WriteTokens,
|
||||
readTokens: o.ReadTokens,
|
||||
@@ -62,6 +66,10 @@ func New(o Options) http.Handler {
|
||||
r.Route("/api/v1", func(r chi.Router) {
|
||||
r.With(requireToken(a.writeTokens)).Post("/ingest", a.handleIngest)
|
||||
r.With(requireToken(a.readTokens)).Get("/metrics", a.handleMetrics)
|
||||
// Маршрут точек стоит РЯДОМ с каталогом, а не поверх него: у chi
|
||||
// литеральный `/metrics` и шаблон `/metrics/{metric}` — разные узлы, и
|
||||
// каталог остаётся достижим. Утверждается это тестом, а не верой.
|
||||
r.With(requireToken(a.readTokens)).Get("/metrics/{metric}", a.handlePoints)
|
||||
})
|
||||
return r
|
||||
}
|
||||
@@ -152,10 +160,27 @@ func routePattern(r *http.Request) string {
|
||||
return r.URL.Path
|
||||
}
|
||||
|
||||
func writeJSON(w http.ResponseWriter, status int, v any) {
|
||||
// writeJSON — единственный сериализатор тел ответа.
|
||||
//
|
||||
// Экранирование HTML ВЫКЛЮЧЕНО, и это не косметика. `encoding/json` по
|
||||
// умолчанию превращает `&`, `<` и `>` в `\u0026`, `\u003c`, `\u003e`; на
|
||||
// маршруте, отдающем дословно сохранённое содержимое точки, это прямо ломает
|
||||
// обещание дословности — имя источника приходит с телефона пользовательской
|
||||
// строкой и законно содержит `&`. Хранилище этот же капкан уже проходило и
|
||||
// обезвредило тем же способом (`store.encodePayload`).
|
||||
//
|
||||
// Правило общее для всех читающих маршрутов намеренно: механизм один, и
|
||||
// решать его заново на каждом маршруте значило бы завести второй способ.
|
||||
// Отказ записи ВОЗВРАЩАЕТСЯ, а не глушится: код ответа отдан до сериализации,
|
||||
// поэтому оборванное на середине тело снаружи неотличимо от успеха, а
|
||||
// `accessLog` честно напишет `200`. Кто из вызывающих обязан об этом сказать —
|
||||
// решает он сам; глушить молча нельзя ни одному.
|
||||
func writeJSON(w http.ResponseWriter, status int, v any) error {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
_ = json.NewEncoder(w).Encode(v)
|
||||
enc := json.NewEncoder(w)
|
||||
enc.SetEscapeHTML(false)
|
||||
return enc.Encode(v)
|
||||
}
|
||||
|
||||
// errorWire — форма провода тела отказа, общая для всех маршрутов.
|
||||
@@ -173,5 +198,7 @@ type errorWire struct {
|
||||
// writeError отдаёт человекочитаемое сообщение, а не текст ошибки: в тексте
|
||||
// имена колонок и форма запроса.
|
||||
func writeError(w http.ResponseWriter, status int, msg string) {
|
||||
writeJSON(w, status, errorWire{Error: msg})
|
||||
// Тело отказа — десятки байт: оборваться на нём нечему, и сообщать о
|
||||
// таком обрыве было бы шумом.
|
||||
_ = writeJSON(w, status, errorWire{Error: msg})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user