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