model/chart.go
Rama main · réplica de lectura del repositorio original.
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
}