Code

Don't try to display metric information after the query failed.
[sysdb/webui.git] / server / query.go
index 16b250c22128551541cf003b5a55de115bb664f7..6f9722c64a6c37130311bf3e15b9586039964764 100644 (file)
@@ -32,6 +32,8 @@ import (
        "fmt"
        "log"
        "strings"
+       "time"
+       "unicode"
 
        "github.com/sysdb/go/proto"
        "github.com/sysdb/go/sysdb"
@@ -42,7 +44,7 @@ func listAll(req request, s *Server) (*page, error) {
                return nil, fmt.Errorf("%s not found", strings.Title(req.cmd))
        }
 
-       res, err := s.query(fmt.Sprintf("LIST %s", req.cmd))
+       res, err := s.query("LIST %s", identifier(req.cmd))
        if err != nil {
                return nil, err
        }
@@ -54,16 +56,41 @@ func lookup(req request, s *Server) (*page, error) {
        if req.r.Method != "POST" {
                return nil, errors.New("Method not allowed")
        }
-       q := proto.EscapeString(req.r.FormValue("query"))
-       if q == "''" {
+       tokens, err := tokenize(req.r.PostForm.Get("query"))
+       if err != nil {
+               return nil, err
+       }
+       if len(tokens) == 0 {
                return nil, errors.New("Empty query")
        }
 
-       res, err := s.query(fmt.Sprintf("LOOKUP hosts MATCHING name =~ %s", q))
+       typ := "hosts"
+       var args string
+       for i, tok := range tokens {
+               if len(args) > 0 {
+                       args += " AND"
+               }
+
+               if fields := strings.SplitN(tok, ":", 2); len(fields) == 2 {
+                       if i == 0 && fields[1] == "" {
+                               typ = fields[0]
+                       } else {
+                               args += fmt.Sprintf(" attribute[%s] = %s",
+                                       proto.EscapeString(fields[0]), proto.EscapeString(fields[1]))
+                       }
+               } else {
+                       args += fmt.Sprintf(" name =~ %s", proto.EscapeString(tok))
+               }
+       }
+
+       res, err := s.query("LOOKUP %s MATCHING"+args, identifier(typ))
        if err != nil {
                return nil, err
        }
-       return tmpl(s.results["hosts"], res)
+       if t, ok := s.results[typ]; ok {
+               return tmpl(t, res)
+       }
+       return nil, fmt.Errorf("Unsupported type %s", typ)
 }
 
 func fetch(req request, s *Server) (*page, error) {
@@ -71,35 +98,184 @@ func fetch(req request, s *Server) (*page, error) {
                return nil, fmt.Errorf("%s not found", strings.Title(req.cmd))
        }
 
-       var q string
+       var res interface{}
+       var err error
        switch req.cmd {
        case "host":
                if len(req.args) != 1 {
                        return nil, fmt.Errorf("%s not found", strings.Title(req.cmd))
                }
-               q = fmt.Sprintf("FETCH host %s", proto.EscapeString(req.args[0]))
+               res, err = s.query("FETCH host %s", req.args[0])
        case "service", "metric":
                if len(req.args) != 2 {
                        return nil, fmt.Errorf("%s not found", strings.Title(req.cmd))
                }
-               host := proto.EscapeString(req.args[0])
-               name := proto.EscapeString(req.args[1])
-               q = fmt.Sprintf("FETCH %s %s.%s", req.cmd, host, name)
+               res, err = s.query("FETCH %s %s.%s", identifier(req.cmd), req.args[0], req.args[1])
+               if err == nil && req.cmd == "metric" {
+                       return metric(req, res, s)
+               }
        default:
                panic("Unknown request: fetch(" + req.cmd + ")")
        }
-
-       res, err := s.query(q)
        if err != nil {
                return nil, err
        }
        return tmpl(s.results[req.cmd], res)
 }
 
-func (s *Server) query(cmd string) (interface{}, error) {
+var datetime = "2006-01-02 15:04:05"
+
+func metric(req request, res interface{}, s *Server) (*page, error) {
+       start := time.Now().Add(-24 * time.Hour)
+       end := time.Now()
+       if req.r.Method == "POST" {
+               var err error
+               // Parse the values first to verify their format.
+               if s := req.r.PostForm.Get("start_date"); s != "" {
+                       if start, err = time.Parse(datetime, s); err != nil {
+                               return nil, fmt.Errorf("Invalid start time %q", s)
+                       }
+               }
+               if e := req.r.PostForm.Get("end_date"); e != "" {
+                       if end, err = time.Parse(datetime, e); err != nil {
+                               return nil, fmt.Errorf("Invalid end time %q", e)
+                       }
+               }
+       }
+
+       p := struct {
+               StartTime string
+               EndTime   string
+               URLStart  string
+               URLEnd    string
+               Data      interface{}
+       }{
+               start.Format(datetime),
+               end.Format(datetime),
+               start.Format(urldate),
+               end.Format(urldate),
+               res,
+       }
+       return tmpl(s.results["metric"], &p)
+}
+
+// tokenize split the string s into its tokens where a token is either a quoted
+// string or surrounded by one or more consecutive whitespace characters.
+func tokenize(s string) ([]string, error) {
+       scan := scanner{}
+       tokens := []string{}
+       start := -1
+       for i, r := range s {
+               if !scan.inField(r) {
+                       if start == -1 {
+                               // Skip leading and consecutive whitespace.
+                               continue
+                       }
+                       tok, err := unescape(s[start:i])
+                       if err != nil {
+                               return nil, err
+                       }
+                       tokens = append(tokens, tok)
+                       start = -1
+               } else if start == -1 {
+                       // Found a new field.
+                       start = i
+               }
+       }
+       if start >= 0 {
+               // Last (or possibly only) field.
+               tok, err := unescape(s[start:])
+               if err != nil {
+                       return nil, err
+               }
+               tokens = append(tokens, tok)
+       }
+
+       if scan.inQuotes {
+               return nil, errors.New("quoted string not terminated")
+       }
+       if scan.escaped {
+               return nil, errors.New("illegal character escape at end of string")
+       }
+       return tokens, nil
+}
+
+func unescape(s string) (string, error) {
+       var unescaped []byte
+       var i, n int
+       for i = 0; i < len(s); i++ {
+               if s[i] != '\\' {
+                       n++
+                       continue
+               }
+
+               if i >= len(s) {
+                       return "", errors.New("illegal character escape at end of string")
+               }
+               if s[i+1] != ' ' && s[i+1] != '"' && s[i+1] != '\\' {
+                       // Allow simple escapes only for now.
+                       return "", fmt.Errorf("illegal character escape \\%c", s[i+1])
+               }
+               if unescaped == nil {
+                       unescaped = []byte(s)
+               }
+               copy(unescaped[n:], s[i+1:])
+       }
+
+       if unescaped != nil {
+               return string(unescaped[:n]), nil
+       }
+       return s, nil
+}
+
+type scanner struct {
+       inQuotes bool
+       escaped  bool
+}
+
+func (s *scanner) inField(r rune) bool {
+       if s.escaped {
+               s.escaped = false
+               return true
+       }
+       if r == '\\' {
+               s.escaped = true
+               return true
+       }
+       if s.inQuotes {
+               if r == '"' {
+                       s.inQuotes = false
+                       return false
+               }
+               return true
+       }
+       if r == '"' {
+               s.inQuotes = true
+               return false
+       }
+       return !unicode.IsSpace(r)
+}
+
+type identifier string
+
+func (s *Server) query(cmd string, args ...interface{}) (interface{}, error) {
        c := <-s.conns
        defer func() { s.conns <- c }()
 
+       for i, arg := range args {
+               switch v := arg.(type) {
+               case identifier:
+                       // Nothing to do.
+               case string:
+                       args[i] = proto.EscapeString(v)
+               case time.Time:
+                       args[i] = v.Format(datetime)
+               default:
+                       panic(fmt.Sprintf("query: invalid type %T", arg))
+               }
+       }
+
+       cmd = fmt.Sprintf(cmd, args...)
        m := &proto.Message{
                Type: proto.ConnectionQuery,
                Raw:  []byte(cmd),