Каталог разрезов и измеренный род агрегации
- род метрики выводится сверкой минутного слоя с часовым: часовое значение сходится с суммой минутных — накопительная, со средним — мгновенная, иначе `unknown` и свёртка не предлагается вовсе. На живом архиве (123 доставки, 31 метрика) 7 накопительных, 9 мгновенных, противоречащих часов ноль - `GET /api/v1/metrics` под токеном чтения отдаёт единицы, слои с границами и род вместе с основанием измерения; род нигде не хранится — он функция витрины, а витрина функция журнала, устаревать в нём нечему - миграция 00009: покрывающий индекс, чтобы каталог отвечал по учётным колонкам, не разжимая содержимое объектов
This commit is contained in:
+32
-13
@@ -14,14 +14,17 @@ import (
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
|
||||
"git.vakhrushev.me/av/healthlog/internal/catalog"
|
||||
"git.vakhrushev.me/av/healthlog/internal/ingest"
|
||||
)
|
||||
|
||||
// Options — зависимости и настройки транспорта.
|
||||
type Options struct {
|
||||
Ingest *ingest.Service
|
||||
Catalog *catalog.Service
|
||||
Log *slog.Logger
|
||||
WriteTokens []string
|
||||
ReadTokens []string
|
||||
MaxBodyMB int
|
||||
// IngestWriteBudget — сколько отводится маршруту приёма на чтение тела
|
||||
// вместе с отправкой ответа. Ноль означает «полагаться на WriteTimeout
|
||||
@@ -31,8 +34,10 @@ type Options struct {
|
||||
|
||||
type api struct {
|
||||
ingest *ingest.Service
|
||||
catalog *catalog.Service
|
||||
log *slog.Logger
|
||||
writeTokens []string
|
||||
readTokens []string
|
||||
maxBody int64
|
||||
ingestBudget time.Duration
|
||||
}
|
||||
@@ -41,8 +46,10 @@ type api struct {
|
||||
func New(o Options) http.Handler {
|
||||
a := &api{
|
||||
ingest: o.Ingest,
|
||||
catalog: o.Catalog,
|
||||
log: o.Log,
|
||||
writeTokens: o.WriteTokens,
|
||||
readTokens: o.ReadTokens,
|
||||
maxBody: int64(o.MaxBodyMB) << 20,
|
||||
ingestBudget: o.IngestWriteBudget,
|
||||
}
|
||||
@@ -53,7 +60,8 @@ func New(o Options) http.Handler {
|
||||
|
||||
r.Get("/healthz", a.handleHealthz)
|
||||
r.Route("/api/v1", func(r chi.Router) {
|
||||
r.With(a.requireWriteToken).Post("/ingest", a.handleIngest)
|
||||
r.With(requireToken(a.writeTokens)).Post("/ingest", a.handleIngest)
|
||||
r.With(requireToken(a.readTokens)).Get("/metrics", a.handleMetrics)
|
||||
})
|
||||
return r
|
||||
}
|
||||
@@ -64,21 +72,32 @@ func (a *api) handleHealthz(w http.ResponseWriter, _ *http.Request) {
|
||||
_, _ = w.Write([]byte(`{"status":"ok"}`))
|
||||
}
|
||||
|
||||
// requireWriteToken проверяет токен приёма. Пустой список токенов = проверка
|
||||
// requireToken проверяет токен контура. Пустой список токенов = проверка
|
||||
// выключена: локальный запуск в доверенной сети. О выключенной проверке
|
||||
// сервис предупреждает на старте.
|
||||
func (a *api) requireWriteToken(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if len(a.writeTokens) == 0 {
|
||||
//
|
||||
// Проверка ОДНА на оба контура, параметризованная списком. Копия отличалась бы
|
||||
// одним полем и несла бы три решения сразу — сравнение за постоянное время,
|
||||
// «пустой список = выключено» и текст 401; правка любого из них в одном месте
|
||||
// не дала бы ни ошибки компиляции, ни красного теста, а речь о контуре чтения
|
||||
// данных о здоровье.
|
||||
//
|
||||
// Контуры при этом раздельны: списки разные, и токен приёма маршрут чтения не
|
||||
// открывает. Схема строгая — токеном считается только значение после `Bearer `.
|
||||
func requireToken(tokens []string) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if len(tokens) == 0 {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
if !tokenAllowed(bearer(r), tokens) {
|
||||
writeError(w, http.StatusUnauthorized, "неверный или отсутствующий токен")
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
if !tokenAllowed(bearer(r), a.writeTokens) {
|
||||
writeError(w, http.StatusUnauthorized, "неверный или отсутствующий токен")
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func bearer(r *http.Request) string {
|
||||
|
||||
Reference in New Issue
Block a user