diff --git a/server/query.go b/server/query.go
index edeae731baa5f9519618e25e2e9321dae2c7591b..ad645bd1dbf139887de5f86a8e26bb5af1556b96 100644 (file)
--- a/server/query.go
+++ b/server/query.go
import (
"errors"
"fmt"
- "log"
"strings"
+ "time"
+ "unicode"
+ "github.com/sysdb/go/client"
"github.com/sysdb/go/proto"
- "github.com/sysdb/go/sysdb"
)
func listAll(req request, s *Server) (*page, error) {
return nil, fmt.Errorf("%s not found", strings.Title(req.cmd))
}
- res, err := s.query("LIST %s", identifier(req.cmd))
+ q, err := client.QueryString("LIST %s", client.Identifier(req.cmd))
+ if err != nil {
+ return nil, err
+ }
+ res, err := s.c.Query(q)
if err != nil {
return nil, err
}
if req.r.Method != "POST" {
return nil, errors.New("Method not allowed")
}
- q := req.r.FormValue("query")
- if q == "''" {
- return nil, errors.New("Empty query")
+ raw, err := parseQuery(req.r.PostForm.Get("query"))
+ if err != nil {
+ return nil, err
+ }
+
+ if raw.typ == "" {
+ raw.typ = "hosts"
+ }
+ var args string
+ for name, value := range raw.args {
+ if len(args) > 0 {
+ args += " AND"
+ }
+
+ if name == "name" {
+ args += fmt.Sprintf(" name =~ %s", value)
+ } else {
+ args += fmt.Sprintf(" %s = %s", name, value)
+ }
}
- res, err := s.query("LOOKUP hosts MATCHING name =~ %s", q)
+ q, err := client.QueryString("LOOKUP %s MATCHING"+args, client.Identifier(raw.typ))
if err != nil {
return nil, err
}
- return tmpl(s.results["hosts"], res)
+ res, err := s.c.Query(q)
+ if err != nil {
+ return nil, err
+ }
+ if t, ok := s.results[raw.typ]; ok {
+ return tmpl(t, res)
+ }
+ return nil, fmt.Errorf("Unsupported type %s", raw.typ)
}
func fetch(req request, s *Server) (*page, error) {
return nil, fmt.Errorf("%s not found", strings.Title(req.cmd))
}
- var res interface{}
+ var q string
var err error
switch req.cmd {
case "host":
if len(req.args) != 1 {
return nil, fmt.Errorf("%s not found", strings.Title(req.cmd))
}
- res, err = s.query("FETCH host %s", req.args[0])
+ q, err = client.QueryString("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))
}
- res, err = s.query("FETCH %s %s.%s", identifier(req.cmd), req.args[0], req.args[1])
+ q, err = client.QueryString("FETCH %s %s.%s", client.Identifier(req.cmd), req.args[0], req.args[1])
default:
panic("Unknown request: fetch(" + req.cmd + ")")
}
if err != nil {
return nil, err
}
+
+ res, err := s.c.Query(q)
+ if err != nil {
+ return nil, err
+ }
+ if req.cmd == "metric" {
+ return metric(req, res, s)
+ }
return tmpl(s.results[req.cmd], res)
}
-type identifier string
+func graphs(req request, s *Server) (*page, error) {
+ p := struct {
+ Query, Metrics string
+ QueryOptions string
+ GroupBy []string
+ Attributes map[string]bool
+ }{
+ Query: req.r.PostForm.Get("metrics-query"),
+ GroupBy: req.r.PostForm["group-by"],
+ }
+
+ if req.r.Method == "POST" {
+ p.Metrics = p.Query
+ if len(p.GroupBy) > 0 {
+ p.QueryOptions += "/g=" + strings.Join(p.GroupBy, ",")
+ }
+
+ metrics, err := s.queryMetrics(p.Query)
+ if err != nil {
+ return nil, err
+ }
+ p.Attributes = make(map[string]bool)
+ for _, m := range metrics {
+ for a := range m.Attributes {
+ var checked bool
+ for _, g := range p.GroupBy {
+ if a == g {
+ checked = true
+ break
+ }
+ }
+ p.Attributes[a] = checked
+ }
+ }
+ }
+ return tmpl(s.results["graphs"], &p)
+}
-func (s *Server) query(cmd string, args ...interface{}) (interface{}, error) {
- c := <-s.conns
- defer func() { s.conns <- c }()
+var datetime = "2006-01-02 15:04:05"
- for i, arg := range args {
- switch v := arg.(type) {
- case identifier:
- // Nothing to do.
- case string:
- args[i] = proto.EscapeString(v)
- default:
- panic(fmt.Sprintf("query: invalid type %T", arg))
+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)
+}
+
+type query struct {
+ typ string
+ args map[string]string
+}
+
+func (q *query) arg(name, value string) error {
+ if _, ok := q.args[name]; ok {
+ return fmt.Errorf("Duplicate key %q", name)
+ }
+ q.args[name] = proto.EscapeString(value)
+ return nil
+}
+
+func (q *query) attr(parent, name, value string) error {
+ var k string
+ if parent != "" {
+ k = fmt.Sprintf("%s.attribute[%s]", parent, proto.EscapeString(name))
+ } else {
+ k = fmt.Sprintf("attribute[%s]", proto.EscapeString(name))
}
- cmd = fmt.Sprintf(cmd, args...)
- m := &proto.Message{
- Type: proto.ConnectionQuery,
- Raw: []byte(cmd),
+ return q.arg(k, value)
+}
+
+func parseQuery(s string) (*query, error) {
+ tokens, err := tokenize(s)
+ if err != nil {
+ return nil, err
}
- if err := c.Send(m); err != nil {
- return nil, fmt.Errorf("Query %q: %v", cmd, err)
+ if len(tokens) == 0 {
+ return nil, errors.New("Empty query")
}
- for {
- m, err := c.Receive()
+ q := &query{args: make(map[string]string)}
+ for i, tok := range tokens {
+ if fields := strings.SplitN(tok, ":", 2); len(fields) == 2 {
+ // Query: [<type>:] [<sibling-type>.]<attribute>:<value> ...
+ if i == 0 && fields[1] == "" {
+ q.typ = fields[0]
+ } else if elems := strings.Split(fields[0], "."); len(elems) > 1 {
+ objs := elems[:len(elems)-1]
+ for _, o := range objs {
+ if o != "host" && o != "service" && o != "metric" {
+ return nil, fmt.Errorf("Invalid object type %q", o)
+ }
+ }
+ if err := q.attr(strings.Join(objs, "."), elems[len(elems)-1], fields[1]); err != nil {
+ return nil, err
+ }
+ } else {
+ if err := q.attr("", fields[0], fields[1]); err != nil {
+ return nil, err
+ }
+ }
+ } else {
+ if err := q.arg("name", tok); err != nil {
+ return nil, err
+ }
+ }
+ }
+ return q, nil
+}
+
+// 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, fmt.Errorf("Failed to receive server response: %v", err)
+ return nil, err
}
- if m.Type == proto.ConnectionLog {
- log.Println(string(m.Raw[4:]))
+ 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
- } else if m.Type == proto.ConnectionError {
- return nil, errors.New(string(m.Raw))
}
- t, err := m.DataType()
- if err != nil {
- return nil, fmt.Errorf("Failed to unmarshal response: %v", err)
- }
-
- var res interface{}
- switch t {
- case proto.HostList:
- var hosts []sysdb.Host
- err = proto.Unmarshal(m, &hosts)
- res = hosts
- case proto.Host:
- var host sysdb.Host
- err = proto.Unmarshal(m, &host)
- res = host
- case proto.Timeseries:
- var ts sysdb.Timeseries
- err = proto.Unmarshal(m, &ts)
- res = ts
- default:
- return nil, fmt.Errorf("Unsupported data type %d", t)
+ if i >= len(s) {
+ return "", errors.New("illegal character escape at end of string")
}
- if err != nil {
- return nil, fmt.Errorf("Failed to unmarshal response: %v", err)
+ 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 res, nil
+ return true
+ }
+ if r == '"' {
+ s.inQuotes = true
+ return false
}
+ return !unicode.IsSpace(r)
}
// vim: set tw=78 sw=4 sw=4 noexpandtab :