Code

graph: Sort group names after grouping to ensure consistent output.
[sysdb/webui.git] / graph / graph.go
index 7985dc47f0daba74ff952c48d50cf830b1445c8f..17de57d8e48552d4672d6883e064b68e9c476041 100644 (file)
@@ -29,6 +29,8 @@ package graph
 
 import (
        "fmt"
+       "sort"
+       "strings"
        "time"
 
        "github.com/gonum/plot"
@@ -45,6 +47,8 @@ type Metric struct {
 
        // Attributes describing details of the metric.
        Attributes map[string]string
+
+       ts *sysdb.Timeseries
 }
 
 // A Graph represents a single graph. It may reference multiple data-sources.
@@ -54,6 +58,9 @@ type Graph struct {
 
        // Content of the graph.
        Metrics []Metric
+
+       // List of attributes to group by.
+       GroupBy []string
 }
 
 type pl struct {
@@ -62,23 +69,26 @@ type pl struct {
        ts int // Index of the current time-series.
 }
 
-func (p *pl) addTimeseries(c *client.Client, metric Metric, start, end time.Time) error {
+func queryTimeseries(c *client.Client, metric Metric, start, end time.Time) (*sysdb.Timeseries, error) {
        q, err := client.QueryString("TIMESERIES %s.%s START %s END %s",
                metric.Hostname, metric.Identifier, start, end)
        if err != nil {
-               return fmt.Errorf("Failed to retrieve graph data: %v", err)
+               return nil, fmt.Errorf("Failed to retrieve graph data: %v", err)
        }
        res, err := c.Query(q)
        if err != nil {
-               return fmt.Errorf("Failed to retrieve graph data: %v", err)
+               return nil, fmt.Errorf("Failed to retrieve graph data: %v", err)
        }
 
        ts, ok := res.(*sysdb.Timeseries)
        if !ok {
-               return fmt.Errorf("TIMESERIES did not return a time-series but %T", res)
+               return nil, fmt.Errorf("TIMESERIES did not return a time-series but %T", res)
        }
+       return ts, nil
+}
 
-       for name, data := range ts.Data {
+func (p *pl) addTimeseries(c *client.Client, metric Metric, verbose bool) error {
+       for name, data := range metric.ts.Data {
                pts := make(plotter.XYs, len(data))
                for i, p := range data {
                        pts[i].X = float64(time.Time(p.Timestamp).UnixNano())
@@ -92,12 +102,95 @@ func (p *pl) addTimeseries(c *client.Client, metric Metric, start, end time.Time
                l.LineStyle.Color = plotutil.DarkColors[p.ts%len(plotutil.DarkColors)]
 
                p.Add(l)
-               p.Legend.Add(name, l)
+               if verbose {
+                       p.Legend.Add(fmt.Sprintf("%s %s %s", metric.Hostname, metric.Identifier, name), l)
+               } else {
+                       p.Legend.Add(name, l)
+               }
                p.ts++
        }
        return nil
 }
 
+// sum is an aggregation function that adds ts2 to ts1.
+func sum(ts1, ts2 *sysdb.Timeseries) error {
+       if !ts1.Start.Equal(ts1.Start) || !ts1.End.Equal(ts2.End) {
+               return fmt.Errorf("Timeseries cover different ranges: [%s, %s] != [%s, %s]",
+                       ts1.Start, ts1.End, ts2.Start, ts2.End)
+       }
+       if len(ts1.Data) != len(ts2.Data) {
+               return fmt.Errorf("Incompatible time-series: %v != %v", ts1.Data, ts2.Data)
+       }
+
+       for name := range ts1.Data {
+               if len(ts1.Data[name]) != len(ts2.Data[name]) {
+                       return fmt.Errorf("Time-series %q is not aligned", name)
+               }
+               for i := range ts1.Data[name] {
+                       if !ts1.Data[name][i].Timestamp.Equal(ts2.Data[name][i].Timestamp) {
+                               return fmt.Errorf("Time-series %q is not aligned", name)
+                       }
+                       ts1.Data[name][i].Value += ts2.Data[name][i].Value
+               }
+       }
+       return nil
+}
+
+func (g *Graph) group(c *client.Client, start, end time.Time) ([]Metric, error) {
+       if len(g.GroupBy) == 0 {
+               for i, m := range g.Metrics {
+                       var err error
+                       if g.Metrics[i].ts, err = queryTimeseries(c, m, g.Start, g.End); err != nil {
+                               return nil, err
+                       }
+               }
+               return g.Metrics, nil
+       }
+
+       names := make([]string, 0)
+       groups := make(map[string][]Metric)
+       for _, m := range g.Metrics {
+               var key string
+               for _, g := range g.GroupBy {
+                       key += "\x00" + m.Attributes[g]
+               }
+               if _, ok := groups[key]; !ok {
+                       names = append(names, key)
+               }
+               groups[key] = append(groups[key], m)
+       }
+       sort.Strings(names)
+
+       var metrics []Metric
+       for _, name := range names {
+               group := groups[name]
+               ts, err := queryTimeseries(c, group[0], g.Start, g.End)
+               if err != nil {
+                       return nil, err
+               }
+               host := group[0].Hostname
+               for _, m := range group[1:] {
+                       ts2, err := queryTimeseries(c, m, g.Start, g.End)
+                       if err != nil {
+                               return nil, err
+                       }
+                       if err := sum(ts, ts2); err != nil {
+                               return nil, err
+                       }
+                       if host != "" && host != m.Hostname {
+                               host = ""
+                       }
+               }
+
+               metrics = append(metrics, Metric{
+                       Hostname:   host,
+                       Identifier: strings.Replace(name[1:], "\x00", "-", -1),
+                       ts:         ts,
+               })
+       }
+       return metrics, nil
+}
+
 // Plot fetches a graph's time-series data using the specified client and
 // plots it.
 func (g *Graph) Plot(c *client.Client) (*plot.Plot, error) {
@@ -111,8 +204,12 @@ func (g *Graph) Plot(c *client.Client) (*plot.Plot, error) {
        p.Add(plotter.NewGrid())
        p.X.Tick.Marker = dateTicks{}
 
-       for _, m := range g.Metrics {
-               if err := p.addTimeseries(c, m, g.Start, g.End); err != nil {
+       metrics, err := g.group(c, g.Start, g.End)
+       if err != nil {
+               return nil, err
+       }
+       for _, m := range metrics {
+               if err := p.addTimeseries(c, m, len(g.Metrics) > 1); err != nil {
                        return nil, err
                }
        }