httpapi: точки метрики за период отдаются одним запросом
- `GET /api/v1/metrics/{name}?from&to&layer` — ряд точек за период; конверт
объявляет слой, измеренный род, его применимость к отданному ряду и границу
окна измерения, а сам ряд собирается из одного слоя, выбранного по охвату
точек внутри периода
- use-case вынесен в `internal/points`, чтение — одним входом `store.ReadSeries`
под одной транзакцией; правило выбора слоя остаётся в домене и приходит в
хранилище колбэком
- `writeJSON` перестал экранировать HTML-символы и перестал глушить отказ
записи: дословность содержимого точки иначе не удерживается, а оборванное
тело уходило под видом успешного `200`
This commit is contained in:
+40
-16
@@ -147,7 +147,12 @@ func closeEnough(a, b float64) bool {
|
||||
return math.Abs(a-b)/math.Max(math.Abs(a), math.Abs(b)) <= tolerance
|
||||
}
|
||||
|
||||
func clipMetric(metric string) string {
|
||||
// ClipMetric обрезает имя метрики для записи лога.
|
||||
//
|
||||
// Экспортирована потому, что предел один на всех, кто пишет имя метрики в лог:
|
||||
// имя приходит из тела дословно при пределе приёма в 64 МиБ, а запись
|
||||
// повторяется на каждый запрос. Второй предел разошёлся бы с первым молча.
|
||||
func ClipMetric(metric string) string {
|
||||
if len(metric) <= maxMetricInLog {
|
||||
return metric
|
||||
}
|
||||
@@ -252,12 +257,38 @@ func (s *Service) Version(ctx context.Context) (string, error) {
|
||||
s.log.DebugContext(ctx, "state version unavailable", "capability", "query", "error", err)
|
||||
return "", err
|
||||
}
|
||||
return stamp(version, store.Now().Add(horizonSlack)), nil
|
||||
return Stamp(version, Horizon()), nil
|
||||
}
|
||||
|
||||
// stamp склеивает версию витрины с горизонтом. Пустая версия остаётся пустой:
|
||||
// Horizon — верхняя граница окна измерения на текущий момент.
|
||||
//
|
||||
// Экспортирована потому, что горизонт нужен ВСЕМ, кто объявляет измеренный род:
|
||||
// маршрут точек снимает род и метку с одного горизонта, иначе метка подтвердит
|
||||
// неизменность ответа, в котором род уже перевернулся ходом часов.
|
||||
func Horizon() time.Time { return store.Now().Add(horizonSlack) }
|
||||
|
||||
// MeasureWindow — окно измерения рода для заданного горизонта.
|
||||
//
|
||||
// Одно на всех потребителей измерения. Второй экземпляр параметров разошёлся бы
|
||||
// с первым молча, а вердикт зависит от каждого из них: слои сверки, размер окна
|
||||
// и оба структурных порога уходят в предварительный отбор хранилища.
|
||||
func MeasureWindow(horizon time.Time) store.CatalogWindow {
|
||||
return store.CatalogWindow{
|
||||
Fine: string(hae.LayerMinute),
|
||||
Coarse: string(hae.LayerHour),
|
||||
Hours: Window,
|
||||
Horizon: horizon,
|
||||
CoarsePoints: coarsePoints,
|
||||
MinFinePoints: minFinePoints,
|
||||
}
|
||||
}
|
||||
|
||||
// Stamp склеивает версию витрины с горизонтом. Пустая версия остаётся пустой:
|
||||
// подписывать нечем — значит нечем, и горизонт этого не меняет.
|
||||
func stamp(version string, horizon time.Time) string {
|
||||
//
|
||||
// Экспортирована по той же причине, что и Horizon: правило «метка строится из
|
||||
// всего, от чего зависит ответ» держится ровно до тех пор, пока склейка одна.
|
||||
func Stamp(version string, horizon time.Time) string {
|
||||
if version == "" {
|
||||
return ""
|
||||
}
|
||||
@@ -287,19 +318,12 @@ func New(st *store.Store, log *slog.Logger) *Service {
|
||||
// её хранилище — двумя пробами вокруг чтения. Порядок проб там же и объяснён:
|
||||
// версия, снятая после чтения, пометила бы устаревший снимок свежей меткой.
|
||||
func (s *Service) Metrics(ctx context.Context) (Snapshot, error) {
|
||||
horizon := store.Now().Add(horizonSlack)
|
||||
horizon := Horizon()
|
||||
|
||||
var snap store.CatalogSnapshot
|
||||
version, err := s.store.VersionedRead(ctx, func(ctx context.Context) error {
|
||||
var err error
|
||||
snap, err = s.store.ReadCatalog(ctx, store.CatalogWindow{
|
||||
Fine: string(hae.LayerMinute),
|
||||
Coarse: string(hae.LayerHour),
|
||||
Hours: Window,
|
||||
Horizon: horizon,
|
||||
CoarsePoints: coarsePoints,
|
||||
MinFinePoints: minFinePoints,
|
||||
})
|
||||
snap, err = s.store.ReadCatalog(ctx, MeasureWindow(horizon))
|
||||
return err
|
||||
})
|
||||
if err != nil { //nolint:nestif // ветка одна, вложенность даёт лог по адресату
|
||||
@@ -331,7 +355,7 @@ func (s *Service) Metrics(ctx context.Context) (Snapshot, error) {
|
||||
if basis.Conflicting > 0 {
|
||||
s.log.WarnContext(ctx, "aggregation style conflict",
|
||||
"capability", "query",
|
||||
"metric", clipMetric(group.metric),
|
||||
"metric", ClipMetric(group.metric),
|
||||
"hours", basis.Hours,
|
||||
"compared", basis.Compared,
|
||||
"agreeing", basis.Agreeing,
|
||||
@@ -344,7 +368,7 @@ func (s *Service) Metrics(ctx context.Context) (Snapshot, error) {
|
||||
if to := group.latest(); to.After(horizon) {
|
||||
s.log.WarnContext(ctx, "future data",
|
||||
"capability", "query",
|
||||
"metric", clipMetric(group.metric),
|
||||
"metric", ClipMetric(group.metric),
|
||||
"last_ts", store.FormatTime(to),
|
||||
"horizon", store.FormatTime(horizon))
|
||||
}
|
||||
@@ -362,7 +386,7 @@ func (s *Service) Metrics(ctx context.Context) (Snapshot, error) {
|
||||
// механизм не окупается вовсе.
|
||||
s.log.DebugContext(ctx, "catalog unsigned", "capability", "query")
|
||||
}
|
||||
return Snapshot{Version: stamp(version, horizon), Metrics: out}, nil
|
||||
return Snapshot{Version: Stamp(version, horizon), Metrics: out}, nil
|
||||
}
|
||||
|
||||
type metricGroup struct {
|
||||
|
||||
Reference in New Issue
Block a user