diff --git a/server/query.go b/server/query.go
index 16b250c22128551541cf003b5a55de115bb664f7..6f9722c64a6c37130311bf3e15b9586039964764 100644 (file)
--- a/server/query.go
+++ b/server/query.go
"fmt"
"log"
"strings"
+ "time"
+ "unicode"
"github.com/sysdb/go/proto"
"github.com/sysdb/go/sysdb"
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
}
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) {
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),