Code

6d8fddf252d8159e7c3facbf5b7e24b43d5120f5
[sysdb/webui.git] / server / query.go
1 //
2 // Copyright (C) 2014 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 server
28 // Helper functions for handling queries.
30 import (
31         "errors"
32         "fmt"
33         "strings"
34         "time"
35         "unicode"
37         "github.com/sysdb/go/client"
38         "github.com/sysdb/go/proto"
39 )
41 func listAll(req request, s *Server) (*page, error) {
42         if len(req.args) != 0 {
43                 return nil, fmt.Errorf("%s not found", strings.Title(req.cmd))
44         }
46         q, err := client.QueryString("LIST %s", client.Identifier(req.cmd))
47         if err != nil {
48                 return nil, err
49         }
50         res, err := s.c.Query(q)
51         if err != nil {
52                 return nil, err
53         }
54         // the template *must* exist
55         return tmpl(s.results[req.cmd], res)
56 }
58 func lookup(req request, s *Server) (*page, error) {
59         if req.r.Method != "POST" {
60                 return nil, errors.New("Method not allowed")
61         }
62         tokens, err := tokenize(req.r.PostForm.Get("query"))
63         if err != nil {
64                 return nil, err
65         }
66         if len(tokens) == 0 {
67                 return nil, errors.New("Empty query")
68         }
70         typ := "hosts"
71         var args string
72         for i, tok := range tokens {
73                 if len(args) > 0 {
74                         args += " AND"
75                 }
77                 if fields := strings.SplitN(tok, ":", 2); len(fields) == 2 {
78                         // Query: [<type>:] [<sibling-type>.]<attribute>:<value> ...
79                         if i == 0 && fields[1] == "" {
80                                 typ = fields[0]
81                         } else if elems := strings.Split(fields[0], "."); len(elems) > 1 {
82                                 objs := elems[:len(elems)-1]
83                                 for _, o := range objs {
84                                         if o != "host" && o != "service" && o != "metric" {
85                                                 return nil, fmt.Errorf("Invalid object type %q", o)
86                                         }
87                                 }
88                                 args += fmt.Sprintf(" %s.attribute[%s] = %s",
89                                         strings.Join(objs, "."), proto.EscapeString(elems[len(elems)-1]),
90                                         proto.EscapeString(fields[1]))
91                         } else {
92                                 args += fmt.Sprintf(" attribute[%s] = %s",
93                                         proto.EscapeString(fields[0]), proto.EscapeString(fields[1]))
94                         }
95                 } else {
96                         args += fmt.Sprintf(" name =~ %s", proto.EscapeString(tok))
97                 }
98         }
100         q, err := client.QueryString("LOOKUP %s MATCHING"+args, client.Identifier(typ))
101         if err != nil {
102                 return nil, err
103         }
104         res, err := s.c.Query(q)
105         if err != nil {
106                 return nil, err
107         }
108         if t, ok := s.results[typ]; ok {
109                 return tmpl(t, res)
110         }
111         return nil, fmt.Errorf("Unsupported type %s", typ)
114 func fetch(req request, s *Server) (*page, error) {
115         if len(req.args) == 0 {
116                 return nil, fmt.Errorf("%s not found", strings.Title(req.cmd))
117         }
119         var q string
120         var err error
121         switch req.cmd {
122         case "host":
123                 if len(req.args) != 1 {
124                         return nil, fmt.Errorf("%s not found", strings.Title(req.cmd))
125                 }
126                 q, err = client.QueryString("FETCH host %s", req.args[0])
127         case "service", "metric":
128                 if len(req.args) != 2 {
129                         return nil, fmt.Errorf("%s not found", strings.Title(req.cmd))
130                 }
131                 q, err = client.QueryString("FETCH %s %s.%s", client.Identifier(req.cmd), req.args[0], req.args[1])
132         default:
133                 panic("Unknown request: fetch(" + req.cmd + ")")
134         }
135         if err != nil {
136                 return nil, err
137         }
139         res, err := s.c.Query(q)
140         if err != nil {
141                 return nil, err
142         }
143         if req.cmd == "metric" {
144                 return metric(req, res, s)
145         }
146         return tmpl(s.results[req.cmd], res)
149 var datetime = "2006-01-02 15:04:05"
151 func metric(req request, res interface{}, s *Server) (*page, error) {
152         start := time.Now().Add(-24 * time.Hour)
153         end := time.Now()
154         if req.r.Method == "POST" {
155                 var err error
156                 // Parse the values first to verify their format.
157                 if s := req.r.PostForm.Get("start_date"); s != "" {
158                         if start, err = time.Parse(datetime, s); err != nil {
159                                 return nil, fmt.Errorf("Invalid start time %q", s)
160                         }
161                 }
162                 if e := req.r.PostForm.Get("end_date"); e != "" {
163                         if end, err = time.Parse(datetime, e); err != nil {
164                                 return nil, fmt.Errorf("Invalid end time %q", e)
165                         }
166                 }
167         }
169         p := struct {
170                 StartTime string
171                 EndTime   string
172                 URLStart  string
173                 URLEnd    string
174                 Data      interface{}
175         }{
176                 start.Format(datetime),
177                 end.Format(datetime),
178                 start.Format(urldate),
179                 end.Format(urldate),
180                 res,
181         }
182         return tmpl(s.results["metric"], &p)
185 // tokenize split the string s into its tokens where a token is either a quoted
186 // string or surrounded by one or more consecutive whitespace characters.
187 func tokenize(s string) ([]string, error) {
188         scan := scanner{}
189         tokens := []string{}
190         start := -1
191         for i, r := range s {
192                 if !scan.inField(r) {
193                         if start == -1 {
194                                 // Skip leading and consecutive whitespace.
195                                 continue
196                         }
197                         tok, err := unescape(s[start:i])
198                         if err != nil {
199                                 return nil, err
200                         }
201                         tokens = append(tokens, tok)
202                         start = -1
203                 } else if start == -1 {
204                         // Found a new field.
205                         start = i
206                 }
207         }
208         if start >= 0 {
209                 // Last (or possibly only) field.
210                 tok, err := unescape(s[start:])
211                 if err != nil {
212                         return nil, err
213                 }
214                 tokens = append(tokens, tok)
215         }
217         if scan.inQuotes {
218                 return nil, errors.New("quoted string not terminated")
219         }
220         if scan.escaped {
221                 return nil, errors.New("illegal character escape at end of string")
222         }
223         return tokens, nil
226 func unescape(s string) (string, error) {
227         var unescaped []byte
228         var i, n int
229         for i = 0; i < len(s); i++ {
230                 if s[i] != '\\' {
231                         n++
232                         continue
233                 }
235                 if i >= len(s) {
236                         return "", errors.New("illegal character escape at end of string")
237                 }
238                 if s[i+1] != ' ' && s[i+1] != '"' && s[i+1] != '\\' {
239                         // Allow simple escapes only for now.
240                         return "", fmt.Errorf("illegal character escape \\%c", s[i+1])
241                 }
242                 if unescaped == nil {
243                         unescaped = []byte(s)
244                 }
245                 copy(unescaped[n:], s[i+1:])
246         }
248         if unescaped != nil {
249                 return string(unescaped[:n]), nil
250         }
251         return s, nil
254 type scanner struct {
255         inQuotes bool
256         escaped  bool
259 func (s *scanner) inField(r rune) bool {
260         if s.escaped {
261                 s.escaped = false
262                 return true
263         }
264         if r == '\\' {
265                 s.escaped = true
266                 return true
267         }
268         if s.inQuotes {
269                 if r == '"' {
270                         s.inQuotes = false
271                         return false
272                 }
273                 return true
274         }
275         if r == '"' {
276                 s.inQuotes = true
277                 return false
278         }
279         return !unicode.IsSpace(r)
282 // vim: set tw=78 sw=4 sw=4 noexpandtab :