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 "strings"
33 "time"
35 "github.com/gonum/plot"
36 "github.com/gonum/plot/plotter"
37 "github.com/gonum/plot/plotutil"
38 "github.com/sysdb/go/client"
39 "github.com/sysdb/go/sysdb"
40 )
42 // A Metric represents a single data-source of a graph.
43 type Metric struct {
44 // The unique identifier of the metric.
45 Hostname, Identifier string
47 // Attributes describing details of the metric.
48 Attributes map[string]string
50 ts *sysdb.Timeseries
51 }
53 // A Graph represents a single graph. It may reference multiple data-sources.
54 type Graph struct {
55 // Time range of the graph.
56 Start, End time.Time
58 // Content of the graph.
59 Metrics []Metric
61 // List of attributes to group by.
62 GroupBy []string
63 }
65 type pl struct {
66 *plot.Plot
68 ts int // Index of the current time-series.
69 }
71 func queryTimeseries(c *client.Client, metric Metric, start, end time.Time) (*sysdb.Timeseries, error) {
72 q, err := client.QueryString("TIMESERIES %s.%s START %s END %s",
73 metric.Hostname, metric.Identifier, start, end)
74 if err != nil {
75 return nil, fmt.Errorf("Failed to retrieve graph data: %v", err)
76 }
77 res, err := c.Query(q)
78 if err != nil {
79 return nil, fmt.Errorf("Failed to retrieve graph data: %v", err)
80 }
82 ts, ok := res.(*sysdb.Timeseries)
83 if !ok {
84 return nil, fmt.Errorf("TIMESERIES did not return a time-series but %T", res)
85 }
86 return ts, nil
87 }
89 func (p *pl) addTimeseries(c *client.Client, metric Metric, verbose bool) error {
90 for name, data := range metric.ts.Data {
91 pts := make(plotter.XYs, len(data))
92 for i, p := range data {
93 pts[i].X = float64(time.Time(p.Timestamp).UnixNano())
94 pts[i].Y = p.Value
95 }
97 l, err := plotter.NewLine(pts)
98 if err != nil {
99 return fmt.Errorf("Failed to create line plotter: %v", err)
100 }
101 l.LineStyle.Color = plotutil.DarkColors[p.ts%len(plotutil.DarkColors)]
103 p.Add(l)
104 if verbose {
105 p.Legend.Add(fmt.Sprintf("%s %s %s", metric.Hostname, metric.Identifier, name), l)
106 } else {
107 p.Legend.Add(name, l)
108 }
109 p.ts++
110 }
111 return nil
112 }
114 // sum is an aggregation function that adds ts2 to ts1.
115 func sum(ts1, ts2 *sysdb.Timeseries) error {
116 if !ts1.Start.Equal(ts1.Start) || !ts1.End.Equal(ts2.End) {
117 return fmt.Errorf("Timeseries cover different ranges: [%s, %s] != [%s, %s]",
118 ts1.Start, ts1.End, ts2.Start, ts2.End)
119 }
120 if len(ts1.Data) != len(ts2.Data) {
121 return fmt.Errorf("Incompatible time-series: %v != %v", ts1.Data, ts2.Data)
122 }
124 for name := range ts1.Data {
125 if len(ts1.Data[name]) != len(ts2.Data[name]) {
126 return fmt.Errorf("Time-series %q is not aligned", name)
127 }
128 for i := range ts1.Data[name] {
129 if !ts1.Data[name][i].Timestamp.Equal(ts2.Data[name][i].Timestamp) {
130 return fmt.Errorf("Time-series %q is not aligned", name)
131 }
132 ts1.Data[name][i].Value += ts2.Data[name][i].Value
133 }
134 }
135 return nil
136 }
138 func (g *Graph) group(c *client.Client, start, end time.Time) ([]Metric, error) {
139 if len(g.GroupBy) == 0 {
140 for i, m := range g.Metrics {
141 var err error
142 if g.Metrics[i].ts, err = queryTimeseries(c, m, g.Start, g.End); err != nil {
143 return nil, err
144 }
145 }
146 return g.Metrics, nil
147 }
149 groups := make(map[string][]Metric)
150 for _, m := range g.Metrics {
151 var key string
152 for _, g := range g.GroupBy {
153 key += "\x00" + m.Attributes[g]
154 }
155 groups[key] = append(groups[key], m)
156 }
158 var metrics []Metric
159 for name, group := range groups {
160 ts, err := queryTimeseries(c, group[0], g.Start, g.End)
161 if err != nil {
162 return nil, err
163 }
164 host := group[0].Hostname
165 for _, m := range group[1:] {
166 ts2, err := queryTimeseries(c, m, g.Start, g.End)
167 if err != nil {
168 return nil, err
169 }
170 if err := sum(ts, ts2); err != nil {
171 return nil, err
172 }
173 if host != "" && host != m.Hostname {
174 host = ""
175 }
176 }
178 metrics = append(metrics, Metric{
179 Hostname: host,
180 Identifier: strings.Replace(name[1:], "\x00", "-", -1),
181 ts: ts,
182 })
183 }
184 return metrics, nil
185 }
187 // Plot fetches a graph's time-series data using the specified client and
188 // plots it.
189 func (g *Graph) Plot(c *client.Client) (*plot.Plot, error) {
190 var err error
192 p := &pl{}
193 p.Plot, err = plot.New()
194 if err != nil {
195 return nil, fmt.Errorf("Failed to create plot: %v", err)
196 }
197 p.Add(plotter.NewGrid())
198 p.X.Tick.Marker = dateTicks{}
200 metrics, err := g.group(c, g.Start, g.End)
201 if err != nil {
202 return nil, err
203 }
204 for _, m := range metrics {
205 if err := p.addTimeseries(c, m, len(g.Metrics) > 1); err != nil {
206 return nil, err
207 }
208 }
209 return p.Plot, nil
210 }
212 type dateTicks struct{}
214 func (dateTicks) Ticks(min, max float64) []plot.Tick {
215 // TODO: this is surely not the best we can do
216 // but it'll distribute ticks evenly.
217 ticks := plot.DefaultTicks{}.Ticks(min, max)
218 for i, t := range ticks {
219 if t.Label == "" {
220 // Skip minor ticks.
221 continue
222 }
223 ticks[i].Label = time.Unix(0, int64(t.Value)).Format(time.RFC822)
224 }
225 return ticks
226 }
228 // vim: set tw=78 sw=4 sw=4 noexpandtab :