From 2eacfe65f0b4dcefc65060642aa5db92403f188d Mon Sep 17 00:00:00 2001 From: Sebastian Harl Date: Thu, 14 May 2015 12:42:51 +0200 Subject: [PATCH] Add rudimentary support for dynamic graphs based on a metrics query. A new page now allows to query metrics using the simple query language of the webUI. All returned metrics will be thrown into a single graph. --- server/graph.go | 55 ++++++++++++++++++++++++++++++++++++++++--- server/query.go | 18 +++++++++++++- server/server.go | 3 ++- static/style/main.css | 7 ++++++ templates/graphs.tmpl | 12 ++++++++++ templates/main.tmpl | 1 + 6 files changed, 91 insertions(+), 5 deletions(-) create mode 100644 templates/graphs.tmpl diff --git a/server/graph.go b/server/graph.go index 7509485..ddc8c4d 100644 --- a/server/graph.go +++ b/server/graph.go @@ -35,6 +35,7 @@ import ( "time" "github.com/gonum/plot/vg" + "github.com/sysdb/go/sysdb" "github.com/sysdb/webui/graph" ) @@ -61,11 +62,20 @@ func (s *Server) graph(w http.ResponseWriter, req request) { return } } + g := &graph.Graph{ - Start: start, - End: end, - Metrics: []graph.Metric{{Hostname: req.args[0], Identifier: req.args[1]}}, + Start: start, + End: end, + } + if req.args[0] == "q" { + if g.Metrics, err = s.queryMetrics(req.args[1]); err != nil { + s.badrequest(w, fmt.Errorf("Failed to query metrics: %v", err)) + return + } + } else { + g.Metrics = []graph.Metric{{Hostname: req.args[0], Identifier: req.args[1]}} } + p, err := g.Plot(s.c) if err != nil { s.internal(w, err) @@ -88,4 +98,43 @@ func (s *Server) graph(w http.ResponseWriter, req request) { io.Copy(w, &buf) } +func (s *Server) queryMetrics(q string) ([]graph.Metric, error) { + raw, err := parseQuery(q) + if err != nil { + return nil, err + } + if raw.typ != "" && raw.typ != "metrics" { + return nil, fmt.Errorf("Invalid object type %q for graphs", raw.typ) + } + + 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.c.Query("LOOKUP metrics MATCHING" + args) + if err != nil { + return nil, err + } + hosts, ok := res.([]sysdb.Host) + if !ok { + return nil, fmt.Errorf("LOOKUP did not return a list of hosts but %T", res) + } + var metrics []graph.Metric + for _, h := range hosts { + for _, m := range h.Metrics { + metrics = append(metrics, graph.Metric{Hostname: h.Name, Identifier: m.Name}) + } + } + return metrics, nil +} + // vim: set tw=78 sw=4 sw=4 noexpandtab : diff --git a/server/query.go b/server/query.go index 962e3ef..bde450f 100644 --- a/server/query.go +++ b/server/query.go @@ -64,6 +64,9 @@ func lookup(req request, s *Server) (*page, error) { return nil, err } + if raw.typ == "" { + raw.typ = "hosts" + } var args string for name, value := range raw.args { if len(args) > 0 { @@ -126,6 +129,19 @@ func fetch(req request, s *Server) (*page, error) { return tmpl(s.results[req.cmd], res) } +func graphs(req request, s *Server) (*page, error) { + p := struct { + Query, Metrics string + }{ + Query: req.r.PostForm.Get("metrics-query"), + } + + if req.r.Method == "POST" { + p.Metrics = p.Query + } + return tmpl(s.results["graphs"], &p) +} + var datetime = "2006-01-02 15:04:05" func metric(req request, res interface{}, s *Server) (*page, error) { @@ -195,7 +211,7 @@ func parseQuery(s string) (*query, error) { return nil, errors.New("Empty query") } - q := &query{typ: "hosts", args: make(map[string]string)} + q := &query{args: make(map[string]string)} for i, tok := range tokens { if fields := strings.SplitN(tok, ":", 2); len(fields) == 2 { // Query: [:] [.]: ... diff --git a/server/server.go b/server/server.go index 8d0d72d..1f5ea94 100644 --- a/server/server.go +++ b/server/server.go @@ -79,7 +79,7 @@ func New(addr, user string, cfg Config) (*Server, error) { if s.main, err = cfg.parse("main.tmpl"); err != nil { return nil, err } - types := []string{"host", "hosts", "service", "services", "metric", "metrics"} + types := []string{"graphs", "host", "hosts", "service", "services", "metric", "metrics"} for _, t := range types { s.results[t], err = cfg.parse(t + ".tmpl") if err != nil { @@ -120,6 +120,7 @@ var content = map[string]func(request, *Server) (*page, error){ "": index, // Queries + "graphs": graphs, "host": fetch, "service": fetch, "metric": fetch, diff --git a/static/style/main.css b/static/style/main.css index 4127602..1bcae86 100644 --- a/static/style/main.css +++ b/static/style/main.css @@ -173,6 +173,13 @@ input[type=text].datetime { padding: 0px 3px; } +input[type=text].query { + width: 25em; + height: 25px; + border: 1px solid #000; + padding: 0px 3px; +} + button { background-color: #000; color: #fff; diff --git a/templates/graphs.tmpl b/templates/graphs.tmpl new file mode 100644 index 0000000..43ec622 --- /dev/null +++ b/templates/graphs.tmpl @@ -0,0 +1,12 @@ +
+

Graphs

+
+ + +

+{{if .Metrics}} + +{{end}} +

 

+
diff --git a/templates/main.tmpl b/templates/main.tmpl index 254a22a..e6049eb 100644 --- a/templates/main.tmpl +++ b/templates/main.tmpl @@ -38,6 +38,7 @@ Hosts Services Metrics + Graphs
-- 2.30.2