Code

graph: Use a (much) more verbose legend if there's more than one metric.
[sysdb/webui.git] / graph / graph.go
1 //
2 // Copyright (C) 2014-2015 Sebastian 'tokkee' Harl <sh@tokkee.org>
3 // All rights reserved.
4 //
5 // Redistribution and use in source and binary forms, with or without
6 // modification, are permitted provided that the following conditions
7 // are met:
8 // 1. Redistributions of source code must retain the above copyright
9 //    notice, this list of conditions and the following disclaimer.
10 // 2. Redistributions in binary form must reproduce the above copyright
11 //    notice, this list of conditions and the following disclaimer in the
12 //    documentation and/or other materials provided with the distribution.
13 //
14 // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
15 // ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
16 // TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
17 // PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR
18 // CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
19 // EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
20 // PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
21 // OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
22 // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
23 // OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
24 // ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26 // Package graph handles time-series data provided by SysDB. It supports
27 // querying and post-processing of the data.
28 package graph
30 import (
31         "fmt"
32         "time"
34         "github.com/gonum/plot"
35         "github.com/gonum/plot/plotter"
36         "github.com/gonum/plot/plotutil"
37         "github.com/sysdb/go/client"
38         "github.com/sysdb/go/sysdb"
39 )
41 // A Metric represents a single data-source of a graph.
42 type Metric struct {
43         // The unique identifier of the metric.
44         Hostname, Identifier string
46         // Attributes describing details of the metric.
47         Attributes map[string]string
48 }
50 // A Graph represents a single graph. It may reference multiple data-sources.
51 type Graph struct {
52         // Time range of the graph.
53         Start, End time.Time
55         // Content of the graph.
56         Metrics []Metric
57 }
59 type pl struct {
60         *plot.Plot
62         ts int // Index of the current time-series.
63 }
65 func (p *pl) addTimeseries(c *client.Client, metric Metric, start, end time.Time, verbose bool) error {
66         q, err := client.QueryString("TIMESERIES %s.%s START %s END %s",
67                 metric.Hostname, metric.Identifier, start, end)
68         if err != nil {
69                 return fmt.Errorf("Failed to retrieve graph data: %v", err)
70         }
71         res, err := c.Query(q)
72         if err != nil {
73                 return fmt.Errorf("Failed to retrieve graph data: %v", err)
74         }
76         ts, ok := res.(*sysdb.Timeseries)
77         if !ok {
78                 return fmt.Errorf("TIMESERIES did not return a time-series but %T", res)
79         }
81         for name, data := range ts.Data {
82                 pts := make(plotter.XYs, len(data))
83                 for i, p := range data {
84                         pts[i].X = float64(time.Time(p.Timestamp).UnixNano())
85                         pts[i].Y = p.Value
86                 }
88                 l, err := plotter.NewLine(pts)
89                 if err != nil {
90                         return fmt.Errorf("Failed to create line plotter: %v", err)
91                 }
92                 l.LineStyle.Color = plotutil.DarkColors[p.ts%len(plotutil.DarkColors)]
94                 p.Add(l)
95                 if verbose {
96                         p.Legend.Add(fmt.Sprintf("%s %s %s", metric.Hostname, metric.Identifier, name), l)
97                 } else {
98                         p.Legend.Add(name, l)
99                 }
100                 p.ts++
101         }
102         return nil
105 // Plot fetches a graph's time-series data using the specified client and
106 // plots it.
107 func (g *Graph) Plot(c *client.Client) (*plot.Plot, error) {
108         var err error
110         p := &pl{}
111         p.Plot, err = plot.New()
112         if err != nil {
113                 return nil, fmt.Errorf("Failed to create plot: %v", err)
114         }
115         p.Add(plotter.NewGrid())
116         p.X.Tick.Marker = dateTicks{}
118         for _, m := range g.Metrics {
119                 if err := p.addTimeseries(c, m, g.Start, g.End, len(g.Metrics) > 1); err != nil {
120                         return nil, err
121                 }
122         }
123         return p.Plot, nil
126 type dateTicks struct{}
128 func (dateTicks) Ticks(min, max float64) []plot.Tick {
129         // TODO: this is surely not the best we can do
130         // but it'll distribute ticks evenly.
131         ticks := plot.DefaultTicks{}.Ticks(min, max)
132         for i, t := range ticks {
133                 if t.Label == "" {
134                         // Skip minor ticks.
135                         continue
136                 }
137                 ticks[i].Label = time.Unix(0, int64(t.Value)).Format(time.RFC822)
138         }
139         return ticks
142 // vim: set tw=78 sw=4 sw=4 noexpandtab :