Files
weave-scope/examples/plugins/iowait/main.go
Krzesimir Nowak 9e092f1a4a Switch to LatestMap-style node controls
This allows plugins to add controls to nodes that already have some
controls set by other plugin. Previously only the last plugin that
sets the controls in the node would have its controls visible. That
was because of NodeControls' Merge function that actually weren't
merging data from two inputs, but rather returning data that was newer
and discarding the older one.
2016-08-12 17:15:43 +02:00

385 lines
8.1 KiB
Go

package main
import (
"encoding/json"
"flag"
"fmt"
"log"
"net"
"net/http"
"os"
"os/exec"
"os/signal"
"strconv"
"strings"
"sync"
"time"
)
func main() {
hostname, _ := os.Hostname()
var (
addr = flag.String("addr", "/var/run/scope/plugins/iowait.sock", "unix socket to listen for connections on")
hostID = flag.String("hostname", hostname, "hostname of the host running this plugin")
)
flag.Parse()
log.Printf("Starting on %s...\n", *hostID)
// Check we can get the iowait for the system
_, err := iowait()
if err != nil {
log.Fatal(err)
}
os.Remove(*addr)
interrupt := make(chan os.Signal, 1)
signal.Notify(interrupt, os.Interrupt)
go func() {
<-interrupt
os.Remove(*addr)
os.Exit(0)
}()
listener, err := net.Listen("unix", *addr)
if err != nil {
log.Fatal(err)
}
defer func() {
listener.Close()
os.Remove(*addr)
}()
log.Printf("Listening on: unix://%s", *addr)
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)
}
}
// Plugin groups the methods a plugin needs
type Plugin struct {
HostID string
lock sync.Mutex
iowaitMode bool
}
type request struct {
NodeID string
Control string
}
type response struct {
ShortcutReport *report `json:"shortcutReport,omitempty"`
}
type report struct {
Host topology
Plugins []pluginSpec
}
type topology struct {
Nodes map[string]node `json:"nodes"`
MetricTemplates map[string]metricTemplate `json:"metric_templates"`
Controls map[string]control `json:"controls"`
}
type node struct {
Metrics map[string]metric `json:"metrics"`
LatestControls map[string]controlEntry `json:"latestControls,omitempty"`
}
type metric struct {
Samples []sample `json:"samples,omitempty"`
Min float64 `json:"min"`
Max float64 `json:"max"`
}
type sample struct {
Date time.Time `json:"date"`
Value float64 `json:"value"`
}
type controlEntry struct {
Timestamp time.Time `json:"timestamp"`
Value controlData `json:"value"`
}
type controlData struct {
Dead bool `json:"dead"`
}
type metricTemplate struct {
ID string `json:"id"`
Label string `json:"label,omitempty"`
Format string `json:"format,omitempty"`
Priority float64 `json:"priority,omitempty"`
}
type control struct {
ID string `json:"id"`
Human string `json:"human"`
Icon string `json:"icon"`
Rank int `json:"rank"`
}
type pluginSpec struct {
ID string `json:"id"`
Label string `json:"label"`
Description string `json:"description,omitempty"`
Interfaces []string `json:"interfaces"`
APIVersion string `json:"api_version,omitempty"`
}
func (p *Plugin) makeReport() (*report, error) {
metrics, err := p.metrics()
if err != nil {
return nil, err
}
rpt := &report{
Host: topology{
Nodes: map[string]node{
p.getTopologyHost(): {
Metrics: metrics,
LatestControls: p.latestControls(),
},
},
MetricTemplates: p.metricTemplates(),
Controls: p.controls(),
},
Plugins: []pluginSpec{
{
ID: "iowait",
Label: "iowait",
Description: "Adds a graph of CPU IO Wait to hosts",
Interfaces: []string{"reporter", "controller"},
APIVersion: "1",
},
},
}
return rpt, nil
}
func (p *Plugin) metrics() (map[string]metric, error) {
value, err := p.metricValue()
if err != nil {
return nil, err
}
id, _ := p.metricIDAndName()
metrics := map[string]metric{
id: {
Samples: []sample{
{
Date: time.Now(),
Value: value,
},
},
Min: 0,
Max: 100,
},
}
return metrics, nil
}
func (p *Plugin) latestControls() map[string]controlEntry {
ts := time.Now()
ctrls := map[string]controlEntry{}
for _, details := range p.allControlDetails() {
ctrls[details.id] = controlEntry{
Timestamp: ts,
Value: controlData{
Dead: details.dead,
},
}
}
return ctrls
}
func (p *Plugin) metricTemplates() map[string]metricTemplate {
id, name := p.metricIDAndName()
return map[string]metricTemplate{
id: {
ID: id,
Label: name,
Format: "percent",
Priority: 0.1,
},
}
}
func (p *Plugin) controls() map[string]control {
ctrls := map[string]control{}
for _, details := range p.allControlDetails() {
ctrls[details.id] = control{
ID: details.id,
Human: details.human,
Icon: details.icon,
Rank: 1,
}
}
return ctrls
}
// 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) {
p.lock.Lock()
defer p.lock.Unlock()
log.Println(r.URL.String())
rpt, err := p.makeReport()
if err != nil {
log.Printf("error: %v", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
raw, err := json.Marshal(*rpt)
if err != nil {
log.Printf("error: %v", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
w.Write(raw)
}
// 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) {
p.lock.Lock()
defer p.lock.Unlock()
log.Println(r.URL.String())
xreq := request{}
err := json.NewDecoder(r.Body).Decode(&xreq)
if err != nil {
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
rpt, err := p.makeReport()
if err != nil {
log.Printf("error: %v", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
res := response{ShortcutReport: rpt}
raw, err := json.Marshal(res)
if err != nil {
log.Printf("error: %v", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
w.Write(raw)
}
func (p *Plugin) getTopologyHost() string {
return fmt.Sprintf("%s;<host>", p.HostID)
}
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()
}
type controlDetails struct {
id string
human string
icon string
dead bool
}
func (p *Plugin) allControlDetails() []controlDetails {
return []controlDetails{
{
id: "switchToIdle",
human: "Switch to idle",
icon: "fa-beer",
dead: !p.iowaitMode,
},
{
id: "switchToIOWait",
human: "Switch to IO wait",
icon: "fa-hourglass",
dead: p.iowaitMode,
},
}
}
func (p *Plugin) controlDetails() (string, string, string) {
for _, details := range p.allControlDetails() {
if !details.dead {
return details.id, details.human, details.icon
}
}
return "", "", ""
}
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 nil, fmt.Errorf("iowait: %v", err)
}
// Linux 4.2.0-25-generic (a109563eab38) 04/01/16 _x86_64_(4 CPU)
//
// avg-cpu: %user %nice %system %iowait %steal %idle
// 2.37 0.00 1.58 0.01 0.00 96.04
lines := strings.Split(string(out), "\n")
if len(lines) < 4 {
return nil, fmt.Errorf("iowait: unexpected output: %q", out)
}
values := strings.Fields(lines[3])
if len(values) != 6 {
return nil, fmt.Errorf("iowait: unexpected output: %q", out)
}
return values, nil
}