From 69368af7961ee18baf19cbd96be9b69e70be4785 Mon Sep 17 00:00:00 2001 From: Krzesimir Nowak Date: Thu, 14 Jul 2016 14:21:01 +0200 Subject: [PATCH] Make the iowait example plugin a controller too It exposes a button that allows switching between showing an iowait statistics and an idle statistics. When the button is pressed it should be replaced with other button. The button is shown in the host node. This is a rather nasty case as it shows several problems: - Button control races - The way the NodeControl currently works creates races between plugins adding buttons to the same node. This is because NodeControls are not really merged, but rather one of the two are chosen based on a NodeControls' timestamps, so the older one is thrown away entirely. In the end GUI can switch randomly between showing controls from one plugin or from another. - Showing outdated statistics - When pressing the button to switch to show the other statistics, the old ones are still shown for several seconds. - Slowness of the updates in GUI - Pressing the button yields no immediate reaction. Changes happen after several seconds. Probably related to the previous point. --- examples/plugins/iowait/main.go | 174 +++++++++++++++++++++++++++----- 1 file changed, 146 insertions(+), 28 deletions(-) diff --git a/examples/plugins/iowait/main.go b/examples/plugins/iowait/main.go index a77b899fb..486d38a33 100644 --- a/examples/plugins/iowait/main.go +++ b/examples/plugins/iowait/main.go @@ -1,6 +1,7 @@ package main import ( + "encoding/json" "flag" "fmt" "log" @@ -52,6 +53,7 @@ func main() { plugin := &Plugin{HostID: *hostID} http.HandleFunc("/report", plugin.Report) + http.HandleFunc("/control", plugin.Control) if err := http.Serve(listener, nil); err != nil { log.Printf("error: %v", err) } @@ -59,63 +61,180 @@ func main() { // Plugin groups the methods a plugin needs type Plugin struct { - HostID string + HostID string + iowaitMode bool } // Report is called by scope when a new report is needed. It is part of the // "reporter" interface, which all plugins must implement. func (p *Plugin) Report(w http.ResponseWriter, r *http.Request) { log.Println(r.URL.String()) - now := time.Now() - nowISO := now.Format(time.RFC3339) - value, err := iowait() + metric, metricTemplate, err := p.metricsSnippets() if err != nil { log.Printf("error: %v", err) http.Error(w, err.Error(), http.StatusInternalServerError) return } - fmt.Fprintf(w, `{ + topologyControl, nodeControl := p.controlsSnippets() + rpt := fmt.Sprintf(`{ "Host": { "nodes": { %q: { - "metrics": { - "iowait": { - "samples": [ {"date": %q, "value": %f} ], - "min": 0, - "max": 100 - } - } + "metrics": { %s }, + "controls": { %s } } }, - "metric_templates": { - "iowait": { - "id": "iowait", - "label": "IO Wait", - "format": "percent", - "priority": 0.1 - } - } + "metric_templates": { %s }, + "controls": { %s } }, "Plugins": [ { "id": "iowait", "label": "iowait", "description": "Adds a graph of CPU IO Wait to hosts", - "interfaces": ["reporter"], + "interfaces": ["reporter", "controller"], "api_version": "1" } ] - }`, p.HostID+";", nowISO, value) + }`, p.getTopologyHost(), metric, nodeControl, metricTemplate, topologyControl) + fmt.Fprintf(w, "%s", rpt) +} + +// Request is just a trimmed down xfer.Request +type Request struct { + NodeID string + Control string +} + +// Control is called by scope when a control is activated. It is part +// of the "controller" interface. +func (p *Plugin) Control(w http.ResponseWriter, r *http.Request) { + log.Println(r.URL.String()) + xreq := Request{} + err := json.NewDecoder(r.Body).Decode(&xreq) if err != nil { - log.Printf("error: %v", err) + log.Printf("Bad request: %v", err) + w.WriteHeader(http.StatusBadRequest) + return } + thisNodeID := p.getTopologyHost() + if xreq.NodeID != thisNodeID { + log.Printf("Bad nodeID, expected %q, got %q", thisNodeID, xreq.NodeID) + w.WriteHeader(http.StatusBadRequest) + return + } + expectedControlID, _, _ := p.controlDetails() + if expectedControlID != xreq.Control { + log.Printf("Bad control, expected %q, got %q", expectedControlID, xreq.Control) + w.WriteHeader(http.StatusBadRequest) + return + } + p.iowaitMode = !p.iowaitMode + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, "{}") +} + +func (p *Plugin) getTopologyHost() string { + return fmt.Sprintf("%s;", p.HostID) +} + +// Get the metrics and metric_templates JSON snippets +func (p *Plugin) metricsSnippets() (string, string, error) { + id, name := p.metricIDAndName() + value, err := p.metricValue() + if err != nil { + return "", "", err + } + nowISO := rfcNow() + metric := fmt.Sprintf(` + %q: { + "samples": [ {"date": %q, "value": %f} ], + "min": 0, + "max": 100 + } +`, id, nowISO, value) + metricTemplate := fmt.Sprintf(` + %q: { + "id": %q, + "label": %q, + "format": "percent", + "priority": 0.1 + } +`, id, id, name) + return metric, metricTemplate, nil +} + +// Get the topology controls and node's controls JSON snippet +func (p *Plugin) controlsSnippets() (string, string) { + id, human, icon := p.controlDetails() + nowISO := rfcNow() + topologyControl := fmt.Sprintf(` + %q: { + "id": %q, + "human": %q, + "icon": %q, + "rank": 1 + } +`, id, id, human, icon) + nodeControl := fmt.Sprintf(` + "timestamp": %q, + "controls": [%q] +`, nowISO, id) + return topologyControl, nodeControl +} + +func rfcNow() string { + now := time.Now() + return now.Format(time.RFC3339) +} + +func (p *Plugin) metricIDAndName() (string, string) { + if p.iowaitMode { + return "iowait", "IO Wait" + } + return "idle", "Idle" +} + +func (p *Plugin) metricValue() (float64, error) { + if p.iowaitMode { + return iowait() + } + return idle() +} + +func (p *Plugin) controlDetails() (string, string, string) { + if p.iowaitMode { + return "switchToIdle", "Switch to idle", "fa-beer" + } + return "switchToIOWait", "Switch to IO wait", "fa-hourglass" } // Get the latest iowait value func iowait() (float64, error) { + return iostatValue(3) +} + +func idle() (float64, error) { + return iostatValue(5) +} + +func iostatValue(idx int) (float64, error) { + values, err := iostat() + if err != nil { + return 0, err + } + if idx >= len(values) { + return 0, fmt.Errorf("invalid iostat field index %d", idx) + } + + return strconv.ParseFloat(values[idx], 64) +} + +// Get the latest iostat values +func iostat() ([]string, error) { out, err := exec.Command("iostat", "-c").Output() if err != nil { - return 0, fmt.Errorf("iowait: %v", err) + return nil, fmt.Errorf("iowait: %v", err) } // Linux 4.2.0-25-generic (a109563eab38) 04/01/16 _x86_64_(4 CPU) @@ -124,13 +243,12 @@ func iowait() (float64, error) { // 2.37 0.00 1.58 0.01 0.00 96.04 lines := strings.Split(string(out), "\n") if len(lines) < 4 { - return 0, fmt.Errorf("iowait: unexpected output: %q", out) + return nil, fmt.Errorf("iowait: unexpected output: %q", out) } values := strings.Fields(lines[3]) if len(values) != 6 { - return 0, fmt.Errorf("iowait: unexpected output: %q", out) + return nil, fmt.Errorf("iowait: unexpected output: %q", out) } - - return strconv.ParseFloat(values[3], 64) + return values, nil }