Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lx9FNLG2CqeBbVGGd9bFsP
120 lines
3.7 KiB
Go
120 lines
3.7 KiB
Go
package api
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
// maxGraphPoints bounds the points per series; the metric store caps a range query near 30,000.
|
|
maxGraphPoints = 11_000
|
|
// minRawStep is the default floor of a raw step: one poll interval.
|
|
minRawStep = 60 * time.Second
|
|
// targetPoints is how many points the default step aims for.
|
|
targetPoints = 1000
|
|
// longStepMin is the resolution of the long store's hourly rollup.
|
|
longStepMin = time.Hour
|
|
)
|
|
|
|
var errTooManyPoints = errors.New("too many points")
|
|
|
|
// tierWindow is one stretch of a graph answered by one store: points at
|
|
// from, from+step, ... up to and including last. bound is where the tier
|
|
// officially ends (the join for the older tier of a stitched graph).
|
|
type tierWindow struct {
|
|
tier string
|
|
from time.Time
|
|
last time.Time
|
|
bound time.Time
|
|
step time.Duration
|
|
}
|
|
|
|
// graphPlan says which stores answer which part of [from, to]. Tiers are oldest first.
|
|
type graphPlan struct {
|
|
from, to time.Time // aligned bounds of the whole graph
|
|
tiers []tierWindow
|
|
stitchedAt *time.Time
|
|
}
|
|
|
|
func (p *graphPlan) newest() tierWindow { return p.tiers[len(p.tiers)-1] }
|
|
|
|
// floorTo rounds t down to a multiple of d, counted from the Unix epoch so that
|
|
// panning a graph never shifts its sample times.
|
|
func floorTo(t time.Time, d time.Duration) time.Time {
|
|
ds := int64(d / time.Second)
|
|
return time.Unix(floorDiv(t.Unix(), ds)*ds, 0).UTC()
|
|
}
|
|
|
|
func ceilTo(t time.Time, d time.Duration) time.Time {
|
|
f := floorTo(t, d)
|
|
if f.Equal(t) {
|
|
return f
|
|
}
|
|
return f.Add(d)
|
|
}
|
|
|
|
func floorDiv(a, b int64) int64 {
|
|
q := a / b
|
|
if a%b != 0 && (a < 0) != (b < 0) {
|
|
q--
|
|
}
|
|
return q
|
|
}
|
|
|
|
// defaultStep is max(60 s, range/1000) rounded up to whole minutes, so the
|
|
// same range always lands on the same grid.
|
|
func defaultStep(rng time.Duration) time.Duration {
|
|
return max(minRawStep, ceilDur(rng/targetPoints, minRawStep))
|
|
}
|
|
|
|
func ceilDur(d, unit time.Duration) time.Duration {
|
|
return (d + unit - 1) / unit * unit
|
|
}
|
|
|
|
// planGraph decides the tiers for [from, to] at the given step (0: default).
|
|
// hotStart is where the hot store's retention begins. A range that lies within
|
|
// hot (give or take one step) is answered by raw data alone, a range wholly
|
|
// older than hot by hourly data alone, and one that straddles hotStart is
|
|
// stitched: hourly before the join, raw from it.
|
|
func planGraph(from, to, hotStart time.Time, step time.Duration) (*graphPlan, error) {
|
|
if step <= 0 {
|
|
step = defaultStep(to.Sub(from))
|
|
}
|
|
if n := to.Sub(from) / step; n > maxGraphPoints {
|
|
return nil, fmt.Errorf("%w: %d points at a %s step; the limit is %d, use a larger step", errTooManyPoints, n, step, maxGraphPoints)
|
|
}
|
|
fromRaw, toRaw := floorTo(from, step), floorTo(to, step)
|
|
long := max(longStepMin, ceilDur(step, longStepMin))
|
|
switch {
|
|
case !fromRaw.Add(step).Before(hotStart):
|
|
return &graphPlan{from: fromRaw, to: toRaw, tiers: []tierWindow{
|
|
{tier: tierRaw, from: fromRaw, last: toRaw, bound: toRaw, step: step},
|
|
}}, nil
|
|
case !toRaw.After(hotStart):
|
|
fl, tl := floorTo(from, long), floorTo(to, long)
|
|
return &graphPlan{from: fl, to: tl, tiers: []tierWindow{
|
|
{tier: tier1h, from: fl, last: tl, bound: tl, step: long},
|
|
}}, nil
|
|
}
|
|
join := ceilTo(hotStart, step)
|
|
fl := floorTo(from, long)
|
|
return &graphPlan{from: fl, to: toRaw, stitchedAt: &join, tiers: []tierWindow{
|
|
{tier: tier1h, from: fl, last: floorTo(join.Add(-time.Nanosecond), long), bound: join, step: long},
|
|
{tier: tierRaw, from: join, last: toRaw, bound: toRaw, step: step},
|
|
}}, nil
|
|
}
|
|
|
|
// slots lists the sample times of a tier.
|
|
func (w tierWindow) slots() []int64 {
|
|
if w.last.Before(w.from) {
|
|
return nil
|
|
}
|
|
n := int(w.last.Sub(w.from)/w.step) + 1
|
|
out := make([]int64, 0, n)
|
|
for t := w.from; !t.After(w.last); t = t.Add(w.step) {
|
|
out = append(out, t.Unix())
|
|
}
|
|
return out
|
|
}
|