Ctrl K
ComunidadGuías…

internal/waste/meters.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package waste

import (
	"context"
	"errors"
	"fmt"
	"strconv"
	"sync"
	"time"

	"github.com/sophotechlabs/spinoza/internal/api"
	"github.com/sophotechlabs/spinoza/internal/safe"
)

const (
	liveSource    = "metrics-server"
	liveWindow    = "right now"
	promSource    = "prometheus"
	sampledSource = "spinoza's own samples"

	windowPodBudget = 200
	windowReaders   = 8
	milliPerCore    = 1000
)

type Sampler interface {
	Metrics(ctx context.Context) api.Metrics
}

type Historian interface {
	MetricHistory(ctx context.Context, namespace, pod string, span time.Duration) (api.MetricHistory, error)
}

type live struct {
	from Sampler
}

func RightNow(from Sampler) Meter {
	return &live{from: from}
}

func (lv *live) Usage(ctx context.Context, pods []Ref) (Reading, error) {
	held := lv.from.Metrics(ctx)
	if held.Error != "" {
		return Reading{}, errors.New(held.Error)
	}
	out := make(map[string]api.ResourceUsage, len(pods))
	for _, ref := range pods {
		key := ref.Namespace + "/" + ref.Name
		use, ok := held.Pods[key]
		if !ok {
			continue
		}
		out[key] = use
	}
	if len(out) == 0 {
		return Reading{}, errors.New("metrics-server named none of the pods this asked about")
	}
	return Reading{Pods: out, Source: liveSource, Window: liveWindow}, nil
}

type window struct {
	from   Historian
	span   time.Duration
	budget int
}

func OverAWindow(from Historian, span time.Duration) Meter {
	return &window{from: from, span: span, budget: windowPodBudget}
}

type sampled struct {
	key    string
	source string
	use    api.ResourceUsage
	ok     bool
	err    error
}

func (wd *window) Usage(ctx context.Context, pods []Ref) (Reading, error) {
	if len(pods) > wd.budget {
		return Reading{}, fmt.Errorf(
			"a window is read one pod at a time, and this asks about %d pods against the %d it will read; "+
				"ask about one namespace",
			len(pods), wd.budget,
		)
	}
	out := Reading{Pods: make(map[string]api.ResourceUsage, len(pods)), Window: windowWords(wd.span)}
	var last error
	for _, one := range wd.readAll(ctx, pods) {
		if !one.ok {
			last = one.err
			continue
		}
		if out.Source == "" {
			out.Source = one.source
		}
		out.Pods[one.key] = one.use
	}
	if len(out.Pods) > 0 {
		return out, nil
	}
	if last == nil {
		last = errors.New("no pod had a history to read")
	}
	return Reading{}, last
}

func (wd *window) readAll(ctx context.Context, pods []Ref) []sampled {
	out := make([]sampled, len(pods))
	gate := make(chan struct{}, windowReaders)
	var group sync.WaitGroup
	group.Add(len(pods))
	for at, ref := range pods {
		gate <- struct{}{}
		safe.Go("reading a pod's usage over a window", func() {
			defer group.Done()
			defer func() { <-gate }()
			out[at] = wd.one(ctx, ref)
		})
	}
	group.Wait()
	return out
}

func (wd *window) one(ctx context.Context, ref Ref) sampled {
	key := ref.Namespace + "/" + ref.Name
	history, err := wd.from.MetricHistory(ctx, ref.Namespace, ref.Name, wd.span)
	if err != nil {
		return sampled{err: err}
	}
	if len(history.CPU) == 0 && len(history.Memory) == 0 {
		return sampled{err: errors.New(key + " has no measured history")}
	}
	return sampled{key: key, source: sourceOf(history), use: meanOf(history), ok: true}
}

func sourceOf(history api.MetricHistory) string {
	if history.Sampled {
		return sampledSource
	}
	if history.Source != "" {
		return history.Source
	}
	return promSource
}

func meanOf(history api.MetricHistory) api.ResourceUsage {
	return api.ResourceUsage{
		CPUMilli: int64(mean(history.CPU) * milliPerCore),
		MemoryMi: int64(mean(history.Memory)) / bytesPerMi,
	}
}

func mean(points []api.MetricPoint) float64 {
	if len(points) == 0 {
		return 0
	}
	var total float64
	for _, point := range points {
		total += point.Value
	}
	return total / float64(len(points))
}

func windowWords(span time.Duration) string {
	if span <= 0 {
		return liveWindow
	}
	seconds := int64(span / time.Second)
	if seconds < 60 {
		return "the last " + counted(seconds, "second")
	}
	minutes := seconds / 60
	if minutes < 60 {
		return "the last " + counted(minutes, "minute")
	}
	hours := minutes / 60
	if hours < 24 {
		return "the last " + counted(hours, "hour")
	}
	return "the last " + counted(hours/24, "day")
}

func counted(count int64, noun string) string {
	if count == 1 {
		return noun
	}
	return strconv.FormatInt(count, 10) + " " + noun + "s"
}