Ctrl K
ComunidadGuías…

model/chart.go

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

Archivo original ↗Preguntar a KiBot
package model

import (
	"encoding/json"
	"fmt"
	"sort"
	"strings"

	"github.com/coroot/coroot/timeseries"
)

type Annotation struct {
	Name string          `json:"name"`
	X1   timeseries.Time `json:"x1"`
	X2   timeseries.Time `json:"x2"`
	Icon string          `json:"icon"`
	Link *RouterLink     `json:"link,omitempty"`
}

type SeriesData interface {
	IsEmpty() bool
	Get() *timeseries.TimeSeries
	Reduce(timeseries.F) float32
}

type Series struct {
	Name      string `json:"name"`
	Title     string `json:"title,omitempty"`
	Color     string `json:"color,omitempty"`
	Fill      bool   `json:"fill,omitempty"`
	Threshold string `json:"threshold,omitempty"`

	Data  SeriesData `json:"data"`
	Value string     `json:"value"`
}

func (s *Series) UnmarshalJSON(data []byte) error {
	type Alias Series

	aux := &struct {
		Data json.RawMessage `json:"data"`
		*Alias
	}{
		Alias: (*Alias)(s),
	}

	if err := json.Unmarshal(data, &aux); err != nil {
		return err
	}
	var ts timeseries.TimeSeries
	if err := json.Unmarshal(aux.Data, &ts); err != nil {
		return err
	}
	s.Data = &ts
	return nil
}

type SeriesList struct {
	series []*Series

	topN int
	topF timeseries.F

	histogram   []HistogramBucket
	percentiles []float32
}

func (sl SeriesList) IsEmpty() bool {
	return len(sl.series) == 0 && (len(sl.histogram) == 0 || len(sl.percentiles) == 0)
}

func (sl SeriesList) MarshalJSON() ([]byte, error) {
	ss := sl.series
	switch {
	case sl.topN > 0 && sl.topF != nil:
		ss = topN(ss, sl.topN, sl.topF)
	case len(sl.histogram) > 0 && len(sl.percentiles) > 0:
		for _, q := range sl.percentiles {
			ss = append(ss, &Series{
				Name: fmt.Sprintf("p%d", int(q*100)),
				Data: Quantile(sl.histogram, q),
			})
		}
	}
	return json.Marshal(ss)
}

func (sl *SeriesList) UnmarshalJSON(data []byte) error {
	var ss []*Series
	if err := json.Unmarshal(data, &ss); err != nil {
		return err
	}
	sl.series = ss
	sl.topN = 0
	sl.topF = nil
	sl.histogram = nil
	sl.percentiles = nil
	return nil
}

type Chart struct {
	Ctx timeseries.Context `json:"ctx"`

	Title         string       `json:"title"`
	Series        SeriesList   `json:"series"`
	Threshold     *Series      `json:"threshold"`
	Featured      bool         `json:"featured"`
	IsStacked     bool         `json:"stacked"`
	IsSorted      bool         `json:"sorted"`
	IsColumn      bool         `json:"column"`
	ColorShift    int          `json:"color_shift"`
	Annotations   []Annotation `json:"annotations"`
	DrillDownLink *RouterLink  `json:"drill_down_link"`
	HideLegend    bool         `json:"hide_legend"`

	widget *Widget
}

func (ch *Chart) Widget() *Widget {
	if ch == nil {
		return nil
	}
	return ch.widget
}

func NewChart(ctx timeseries.Context, title string) *Chart {
	if ctx.Truncated {
		title += " (truncated range)"
	}
	return &Chart{Ctx: ctx, Title: title}
}

func (ch *Chart) IsEmpty() bool {
	if ch == nil {
		return true
	}
	return ch.Series.IsEmpty() && (ch.Threshold == nil || ch.Threshold.Data.IsEmpty())
}

func (ch *Chart) Stacked() *Chart {
	if ch == nil {
		return nil
	}
	ch.IsStacked = true
	return ch
}

func (ch *Chart) Sorted() *Chart {
	if ch == nil {
		return nil
	}
	ch.IsSorted = true
	return ch
}

func (ch *Chart) Column() *Chart {
	if ch == nil {
		return nil
	}
	ch.IsColumn = true
	ch.IsStacked = true
	return ch
}

func (ch *Chart) Legend(on bool) *Chart {
	if ch == nil {
		return nil
	}
	ch.HideLegend = !on
	return ch
}

func (ch *Chart) ShiftColors() *Chart {
	if ch == nil {
		return nil
	}
	ch.ColorShift = 1
	return ch
}

func (ch *Chart) AddAnnotation(annotations ...Annotation) *Chart {
	if ch == nil {
		return nil
	}
	ch.Annotations = append(ch.Annotations, annotations...)
	return ch
}

func (ch *Chart) AddSeries(name string, data SeriesData, color ...string) *Chart {
	if ch == nil {
		return nil
	}
	if data.IsEmpty() {
		return ch
	}
	var ns *Series
	for _, s := range ch.Series.series {
		if s.Name == name {
			ns = s
			break
		}
	}
	if ns == nil {
		ns = &Series{Name: name}
		ch.Series.series = append(ch.Series.series, ns)
	}
	ns.Data = data
	if len(color) > 0 {
		ns.Color = color[0]
	}
	return ch
}

func (ch *Chart) AddMany(series map[string]SeriesData, topN int, topF timeseries.F) *Chart {
	if ch == nil {
		return nil
	}
	for name, data := range series {
		ch.AddSeries(name, data)
	}
	ch.Series.topN = topN
	ch.Series.topF = topF
	return ch
}

func (ch *Chart) PercentilesFrom(histogram []HistogramBucket, percentiles ...float32) *Chart {
	if ch == nil {
		return nil
	}
	ch.Series.histogram = histogram
	ch.Series.percentiles = percentiles
	return ch
}

func (ch *Chart) SetThreshold(name string, data SeriesData) *Chart {
	if ch == nil {
		return nil
	}
	if data.IsEmpty() {
		return ch
	}
	ch.Threshold = &Series{Name: name, Color: "black", Data: data}
	return ch
}

func (ch *Chart) Feature() *Chart {
	if ch == nil {
		return nil
	}
	ch.Featured = true
	return ch
}

func (ch *Chart) Group(name string, priority int) *Chart {
	if ch == nil {
		return nil
	}
	if ch.widget != nil {
		ch.widget.Group = name
		ch.widget.groupPriority = priority
	}
	return ch
}

type ChartGroup struct {
	ctx    timeseries.Context
	Title  string   `json:"title"`
	Charts []*Chart `json:"charts"`

	widget *Widget
}

func (cg *ChartGroup) Widget() *Widget {
	if cg == nil {
		return nil
	}
	return cg.widget
}

func (cg *ChartGroup) Group(name string, priority int) *ChartGroup {
	if cg == nil {
		return nil
	}
	if cg.widget != nil {
		cg.widget.Group = name
		cg.widget.groupPriority = priority
	}
	return cg
}

func NewChartGroup(ctx timeseries.Context, title string) *ChartGroup {
	if ctx.Truncated {
		title += " (truncated range)"
		ctx.Truncated = false
	}
	return &ChartGroup{ctx: ctx, Title: title}
}

func (cg *ChartGroup) MarshalJSON() ([]byte, error) {
	autoFeatureChart(cg.Charts)
	return json.Marshal(struct {
		Title  string   `json:"title"`
		Charts []*Chart `json:"charts"`
	}{
		Title:  cg.Title,
		Charts: cg.Charts,
	})
}

func (cg *ChartGroup) GetOrCreateChart(title string) *Chart {
	if cg == nil {
		return nil
	}
	for _, ch := range cg.Charts {
		if ch.Title == title {
			return ch
		}
	}
	ch := NewChart(cg.ctx, title)
	ch.widget = cg.widget
	cg.Charts = append(cg.Charts, ch)
	return ch
}

type Heatmap struct {
	Ctx timeseries.Context `json:"ctx"`

	Title  string     `json:"title"`
	Series SeriesList `json:"series"`

	Annotations []Annotation `json:"annotations"`

	DrillDownLink *RouterLink `json:"drill_down_link"`

	widget *Widget
}

func (hm *Heatmap) Widget() *Widget {
	if hm == nil {
		return nil
	}
	return hm.widget
}

func NewHeatmap(ctx timeseries.Context, title string) *Heatmap {
	if r := ctx.Step % 60; r != 0 {
		ctx.Step += 60 - r
	}
	return &Heatmap{Ctx: ctx, Title: title}
}

func (hm *Heatmap) AddSeries(name, title string, data SeriesData, threshold string, value string) *Heatmap {
	if hm == nil {
		return nil
	}
	if data.IsEmpty() {
		return hm
	}
	s := &Series{Name: name, Title: title, Data: data, Threshold: threshold, Value: value}
	hm.Series.series = append(hm.Series.series, s)
	return hm
}

func (hm *Heatmap) AddAnnotation(annotations ...Annotation) *Heatmap {
	if hm == nil {
		return nil
	}
	hm.Annotations = append(hm.Annotations, annotations...)
	return hm
}

func (hm *Heatmap) IsEmpty() bool {
	if hm == nil {
		return true
	}
	return hm.Series.IsEmpty()
}

func autoFeatureChart(charts []*Chart) {
	if len(charts) < 2 {
		return
	}
	for _, ch := range charts {
		if ch.Featured {
			return
		}
	}
	type weight struct {
		i int
		w float32
	}
	weights := make([]weight, 0, len(charts))
	for i, ch := range charts {
		var w float32
		for _, s := range ch.Series.series {
			w += s.Data.Reduce(timeseries.NanSum)
		}
		weights = append(weights, weight{i: i, w: w})
	}
	sort.Slice(weights, func(i, j int) bool {
		return weights[i].w > weights[j].w
	})
	if weights[0].w/weights[1].w > 1.2 {
		charts[weights[0].i].Featured = true
	}
}

func topN(ss []*Series, n int, by timeseries.F) []*Series {
	type weighted struct {
		*Series
		weight float32
	}
	sortable := make([]weighted, 0, len(ss))
	for _, s := range ss {
		w := s.Data.Reduce(by)
		if !timeseries.IsNaN(w) {
			sortable = append(sortable, weighted{Series: s, weight: w})
		}
	}
	sort.Slice(sortable, func(i, j int) bool {
		return sortable[i].weight > sortable[j].weight
	})
	res := make([]*Series, 0, n+1)
	other := timeseries.NewAggregate(timeseries.NanSum)
	for i, s := range sortable {
		if (i + 1) < n {
			res = append(res, s.Series)
		} else {
			other.Add(s.Data.Get())
		}
	}
	if otherTs := other.Get(); !otherTs.IsEmpty() {
		res = append(res, &Series{Name: "other", Data: otherTs, Color: "grey"})
	}
	return res
}

func EventsToAnnotations(events []*ApplicationEvent, ctx timeseries.Context) []Annotation {
	if len(events) == 0 {
		return nil
	}

	type annotation struct {
		start  timeseries.Time
		end    timeseries.Time
		events []*ApplicationEvent
	}
	var annotations []*annotation
	getLast := func() *annotation {
		if len(annotations) == 0 {
			return nil
		}
		return annotations[len(annotations)-1]
	}
	for _, e := range events {
		last := getLast()
		if last == nil || e.Start.Sub(last.start) > 3*ctx.Step {
			a := &annotation{start: e.Start, end: e.End, events: []*ApplicationEvent{e}}
			annotations = append(annotations, a)
			continue
		}
		last.events = append(last.events, e)
		last.end = e.End
	}

	res := make([]Annotation, 0, len(annotations))
	for _, a := range annotations {
		sort.Slice(a.events, func(i, j int) bool {
			return a.events[i].Type < a.events[j].Type
		})
		icon := ""
		var link *RouterLink
		var msgs []string
		count := map[string]int{}
		for _, e := range a.events {
			i, msg := "", ""
			switch e.Type {
			case ApplicationEventTypeRollout:
				msg = "deployment " + e.Details
				i = "mdi-swap-horizontal-circle-outline"
			case ApplicationEventTypeSwitchover:
				msg = "switchover " + e.Details
				i = "mdi-database-sync-outline"
			case ApplicationEventTypeInstanceUp:
				msg = e.Details + " is up"
				i = "mdi-alert-octagon-outline"
			case ApplicationEventTypeInstanceDown:
				msg = e.Details + " is down"
				i = "mdi-alert-octagon-outline"
			case ApplicationEventTypeDbChange:
				msg = e.Details + " changed"
				i = "mdi-database-cog-outline"
			}
			if msg != "" {
				if count[msg] == 0 {
					msgs = append(msgs, msg)
				}
				count[msg]++
			}
			if icon == "" {
				icon = i
			}
			if link == nil {
				link = e.Link
			}
		}
		for i, msg := range msgs {
			if n := count[msg]; n > 1 {
				msgs[i] = fmt.Sprintf("%s (×%d)", msg, n)
			}
		}
		res = append(res, Annotation{
			Name: strings.Join(msgs, "<br>"),
			X1:   a.start,
			X2:   a.end,
			Icon: icon,
			Link: link,
		})
	}
	return res
}

func IncidentsToAnnotations(incidents []*ApplicationIncident, ctx timeseries.Context) []Annotation {
	res := make([]Annotation, 0, len(incidents))
	for _, i := range incidents {
		a := Annotation{Name: "incident", X1: i.OpenedAt, X2: i.ResolvedAt}
		if a.X2.IsZero() {
			a.X2 = ctx.To
		}
		res = append(res, a)
	}
	return res
}