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.
This commit is contained in:
Krzesimir Nowak
2016-07-14 14:21:01 +02:00
parent 27e0550bd5
commit 69368af796

View File

@@ -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+";<host>", 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;<host>", 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
}