bosun.org@v0.0.0-20210513094433-e25bc3e69a1f/cmd/bosun/web/chart.go (about)

     1  package web
     2  
     3  import (
     4  	"encoding/base64"
     5  	"encoding/json"
     6  	"fmt"
     7  	"image/color"
     8  	"net/http"
     9  	"strconv"
    10  	"strings"
    11  	"time"
    12  
    13  	"bosun.org/annotate"
    14  	"bosun.org/cmd/bosun/expr"
    15  	"bosun.org/cmd/bosun/sched"
    16  	"bosun.org/metadata"
    17  	"bosun.org/models"
    18  	"bosun.org/opentsdb"
    19  	"github.com/MiniProfiler/go/miniprofiler"
    20  	svg "github.com/ajstarks/svgo"
    21  	"github.com/bradfitz/slice"
    22  	"github.com/gorilla/mux"
    23  	"github.com/vdobler/chart"
    24  	"github.com/vdobler/chart/svgg"
    25  )
    26  
    27  // Graph takes an OpenTSDB request data structure and queries OpenTSDB. Use the
    28  // json parameter to pass JSON. Use the b64 parameter to pass base64-encoded
    29  // JSON.
    30  func Graph(t miniprofiler.Timer, w http.ResponseWriter, r *http.Request) (interface{}, error) {
    31  	j := []byte(r.FormValue("json"))
    32  	if bs := r.FormValue("b64"); bs != "" {
    33  		b, err := base64.StdEncoding.DecodeString(bs)
    34  		if err != nil {
    35  			return nil, err
    36  		}
    37  		j = b
    38  	}
    39  	if len(j) == 0 {
    40  		return nil, fmt.Errorf("either json or b64 required")
    41  	}
    42  	oreq, err := opentsdb.RequestFromJSON(j)
    43  	if err != nil {
    44  		return nil, err
    45  	}
    46  	if ads_v := r.FormValue("autods"); ads_v != "" {
    47  		ads_i, err := strconv.Atoi(ads_v)
    48  		if err != nil {
    49  			return nil, err
    50  		}
    51  		if err := oreq.AutoDownsample(ads_i); err != nil {
    52  			return nil, err
    53  		}
    54  	}
    55  	ar := make(map[int]bool)
    56  	for _, v := range r.Form["autorate"] {
    57  		if i, err := strconv.Atoi(v); err == nil {
    58  			ar[i] = true
    59  		}
    60  	}
    61  	queries := make([]string, len(oreq.Queries))
    62  	var start, end string
    63  	var startT, endT time.Time
    64  	if s, ok := oreq.Start.(string); ok && strings.Contains(s, "-ago") {
    65  		startT, err = opentsdb.ParseTime(s)
    66  		if err != nil {
    67  			return nil, err
    68  		}
    69  		start = strings.TrimSuffix(s, "-ago")
    70  	}
    71  	if s, ok := oreq.End.(string); ok && strings.Contains(s, "-ago") {
    72  		endT, err = opentsdb.ParseTime(s)
    73  		if err != nil {
    74  			return nil, err
    75  		}
    76  		end = strings.TrimSuffix(s, "-ago")
    77  	}
    78  	if start == "" && end == "" {
    79  		s, sok := oreq.Start.(int64)
    80  		e, eok := oreq.End.(int64)
    81  		if sok && eok {
    82  			start = fmt.Sprintf("%vs", e-s)
    83  			startT = time.Unix(s, 0)
    84  			endT = time.Unix(e, 0)
    85  			if err != nil {
    86  				return nil, err
    87  			}
    88  		}
    89  	}
    90  	if endT.Equal(time.Time{}) {
    91  		endT = time.Now().UTC()
    92  	}
    93  	m_units := make(map[string]string)
    94  	for i, q := range oreq.Queries {
    95  		if ar[i] {
    96  
    97  			meta, err := schedule.MetadataMetrics(q.Metric)
    98  			if err != nil {
    99  				return nil, err
   100  			}
   101  			if meta == nil {
   102  				return nil, fmt.Errorf("no metadata for %s: cannot use auto rate", q)
   103  			}
   104  			if meta.Unit != "" {
   105  				m_units[q.Metric] = meta.Unit
   106  			}
   107  			if meta.Rate != "" {
   108  				switch meta.Rate {
   109  				case metadata.Gauge:
   110  					// ignore
   111  				case metadata.Rate:
   112  					q.Rate = true
   113  				case metadata.Counter:
   114  					q.Rate = true
   115  					q.RateOptions = opentsdb.RateOptions{
   116  						Counter:    true,
   117  						ResetValue: 1,
   118  					}
   119  				default:
   120  					return nil, fmt.Errorf("unknown metadata rate: %s", meta.Rate)
   121  				}
   122  			}
   123  		}
   124  		queries[i] = fmt.Sprintf(`q("%v", "%v", "%v")`, q, start, end)
   125  		if !schedule.SystemConf.GetTSDBContext().Version().FilterSupport() {
   126  			if err := schedule.Search.Expand(q); err != nil {
   127  				return nil, err
   128  			}
   129  		}
   130  	}
   131  	var tr opentsdb.ResponseSet
   132  	b, _ := json.MarshalIndent(oreq, "", "  ")
   133  	t.StepCustomTiming("tsdb", "query", string(b), func() {
   134  		h := schedule.SystemConf.GetTSDBHost()
   135  		if h == "" {
   136  			err = fmt.Errorf("tsdbHost not set")
   137  			return
   138  		}
   139  		tr, err = oreq.Query(h)
   140  	})
   141  	if err != nil {
   142  		return nil, err
   143  	}
   144  	cs, err := makeChart(tr, m_units)
   145  	if err != nil {
   146  		return nil, err
   147  	}
   148  	if _, present := r.Form["png"]; present {
   149  		c := chart.ScatterChart{
   150  			Title: fmt.Sprintf("%v - %v", oreq.Start, queries),
   151  		}
   152  		c.XRange.Time = true
   153  		if min, err := strconv.ParseFloat(r.FormValue("min"), 64); err == nil {
   154  			c.YRange.MinMode.Fixed = true
   155  			c.YRange.MinMode.Value = min
   156  		}
   157  		if max, err := strconv.ParseFloat(r.FormValue("max"), 64); err == nil {
   158  			c.YRange.MaxMode.Fixed = true
   159  			c.YRange.MaxMode.Value = max
   160  		}
   161  		for ri, r := range cs {
   162  			pts := make([]chart.EPoint, len(r.Data))
   163  			for idx, v := range r.Data {
   164  				pts[idx].X = v[0]
   165  				pts[idx].Y = v[1]
   166  			}
   167  			slice.Sort(pts, func(i, j int) bool {
   168  				return pts[i].X < pts[j].X
   169  			})
   170  			c.AddData(r.Name, pts, chart.PlotStyleLinesPoints, sched.Autostyle(ri))
   171  		}
   172  		w.Header().Set("Content-Type", "image/svg+xml")
   173  		white := color.RGBA{0xff, 0xff, 0xff, 0xff}
   174  		const width = 800
   175  		const height = 600
   176  		s := svg.New(w)
   177  		s.Start(width, height)
   178  		s.Rect(0, 0, width, height, "fill: #ffffff")
   179  		sgr := svgg.AddTo(s, 0, 0, width, height, "", 12, white)
   180  		c.Plot(sgr)
   181  		s.End()
   182  		return nil, nil
   183  	}
   184  	var a []annotate.Annotation
   185  	warnings := []string{}
   186  	if schedule.SystemConf.AnnotateEnabled() {
   187  		a, err = AnnotateBackend.GetAnnotations(&startT, &endT)
   188  		if err != nil {
   189  			warnings = append(warnings, fmt.Sprintf("unable to get annotations: %v", err))
   190  		}
   191  	}
   192  	return struct {
   193  		Queries     []string
   194  		Series      []*chartSeries
   195  		Annotations []annotate.Annotation
   196  		Warnings    []string
   197  	}{
   198  		queries,
   199  		cs,
   200  		a,
   201  		warnings,
   202  	}, nil
   203  }
   204  
   205  // ExprGraph returns an svg graph.
   206  // The basename of the requested svg file should be a base64 encoded expression.
   207  func ExprGraph(t miniprofiler.Timer, w http.ResponseWriter, r *http.Request) (interface{}, error) {
   208  	vars := mux.Vars(r)
   209  	bs := vars["bs"]
   210  	format := vars["format"]
   211  	b, err := base64.StdEncoding.DecodeString(bs)
   212  	if err != nil {
   213  		return nil, err
   214  	}
   215  	q := string(b)
   216  	if len(q) == 0 {
   217  		return nil, fmt.Errorf("missing expression")
   218  	}
   219  	autods := 1000
   220  	if a := r.FormValue("autods"); a != "" {
   221  		i, err := strconv.Atoi(a)
   222  		if err != nil {
   223  			return nil, err
   224  		}
   225  		autods = i
   226  	}
   227  	now := time.Now().UTC()
   228  	if n := r.FormValue("now"); n != "" {
   229  		i, err := strconv.ParseInt(n, 10, 64)
   230  		if err != nil {
   231  			return nil, err
   232  		}
   233  		now = time.Unix(i, 0).UTC()
   234  	}
   235  	e, err := expr.New(q, schedule.RuleConf.GetFuncs(schedule.SystemConf.EnabledBackends()))
   236  	if err != nil {
   237  		return nil, err
   238  	} else if e.Root.Return() != models.TypeSeriesSet {
   239  		return nil, fmt.Errorf("egraph: requires an expression that returns a series")
   240  	}
   241  	// it may not strictly be necessary to recreate the contexts each time, but we do to be safe
   242  	backends := &expr.Backends{
   243  		TSDBContext:       schedule.SystemConf.GetTSDBContext(),
   244  		GraphiteContext:   schedule.SystemConf.GetGraphiteContext(),
   245  		InfluxConfig:      schedule.SystemConf.GetInfluxContext(),
   246  		ElasticHosts:      schedule.SystemConf.GetElasticContext(),
   247  		AzureMonitor:      schedule.SystemConf.GetAzureMonitorContext(),
   248  		PromConfig:        schedule.SystemConf.GetPromContext(),
   249  		CloudWatchContext: schedule.SystemConf.GetCloudWatchContext(),
   250  	}
   251  	providers := &expr.BosunProviders{
   252  		Cache:     cacheObj,
   253  		Search:    schedule.Search,
   254  		Annotate:  AnnotateBackend,
   255  		Squelched: nil,
   256  		History:   nil,
   257  	}
   258  	res, _, err := e.Execute(backends, providers, t, now, autods, false, "Web: chart creation")
   259  	if err != nil {
   260  		return nil, err
   261  	}
   262  	switch format {
   263  	case "svg":
   264  		if err := schedule.ExprSVG(t, w, 800, 600, "", res.Results); err != nil {
   265  			return nil, err
   266  		}
   267  	case "png":
   268  		if err := schedule.ExprPNG(t, w, 800, 600, "", res.Results); err != nil {
   269  			return nil, err
   270  		}
   271  	}
   272  	return nil, nil
   273  }
   274  
   275  func makeChart(r opentsdb.ResponseSet, m_units map[string]string) ([]*chartSeries, error) {
   276  	var series []*chartSeries
   277  	for _, resp := range r {
   278  		dps := make([][2]float64, 0)
   279  		for k, v := range resp.DPS {
   280  			ki, err := strconv.ParseInt(k, 10, 64)
   281  			if err != nil {
   282  				return nil, err
   283  			}
   284  			dps = append(dps, [2]float64{float64(ki), float64(v)})
   285  		}
   286  		if len(dps) > 0 {
   287  			slice.Sort(dps, func(i, j int) bool {
   288  				return dps[i][0] < dps[j][0]
   289  			})
   290  			name := resp.Metric
   291  			if len(resp.Tags) > 0 {
   292  				name += resp.Tags.String()
   293  			}
   294  			series = append(series, &chartSeries{
   295  				Name:   name,
   296  				Metric: resp.Metric,
   297  				Tags:   resp.Tags,
   298  				Data:   dps,
   299  				Unit:   m_units[resp.Metric],
   300  			})
   301  		}
   302  	}
   303  	return series, nil
   304  }
   305  
   306  type chartSeries struct {
   307  	Name   string
   308  	Metric string
   309  	Tags   opentsdb.TagSet
   310  	Data   [][2]float64
   311  	Unit   string
   312  }