~pixelherodev/ikiru

882be31b0fe6945ce4c31d173dfa48a60cdd4ddd — Noam Preil 2 months ago c7bbdff master
Working node-independent dashboards
4 files changed, 78 insertions(+), 5 deletions(-)

M example/example.yaml
M main.go
M prometheus.go
M templates/dashboard.html
M example/example.yaml => example/example.yaml +14 -2
@@ 6,6 6,7 @@ linked: []
rows:
    - categories:
        - {
            width: 12,
            open: true,
            name: "bucket test",
            rows: [


@@ 31,6 32,17 @@ rows:
                            width: 12,
                        }
                    ]
                }, {
                    metrics: [
                        {
                            queries: [
                                "rate(node_network_receive_bytes_total{device=\"eth0\"}[5m]) / 1024 / 1024"
                            ],
                            title: "Node-independent test",
                            width: 12,
                            duration: 500,
                        },
                    ]
                }
            ]
        }


@@ 52,8 64,8 @@ rows:
                            ],
                            title: "network status",
                            duration: 120,
                            format: "png",
                            width: 6,
                            format: "svg",
                            width: 12,
                        },

                    ],

M main.go => main.go +26 -1
@@ 11,6 11,7 @@ import (
	"log"
	"net/http"
	"os/exec"
	"strconv"
	"strings"
	"time"
)


@@ 50,12 51,17 @@ type Metric struct {
	Renderer string        `yaml:"renderer"`
	Type     string        `yaml:"type"`
	Duration time.Duration `yaml:"duration"`
	// Not in the YAML; this is the list of nodes returned by the Prometheus query
	// For node-independent dashboards, the current node is sent as a POST header,
	// With other options exposed via a drop-down.
	Nodes []string
	// Not in the YAML; this caches the resultant image for the template
	Result struct {
		Error  error
		Width  int
		Height int
		Image  template.HTML
		Index  uint64
	}
}



@@ 71,6 77,8 @@ func (m *Metric) UnmarshalYAML(unmarshal func(interface{}) error) error {
	m.Title = ""
	m.Result.Error = errors.New("Not yet generated!")
	m.Result.Image = ""
	m.Result.Index = 0
	m.Nodes = nil
	type plain Metric
	return unmarshal((*plain)(m))
}


@@ 81,16 89,33 @@ func generateHook(dash Dashboard, gnuplotPath string, static string) {
		if err != nil {
			fmt.Fprintf(w, "Error: %s\n", err)
		}
		abs_metricdex := 0
		for rowdex := range dash.Rows {
			for catdex := range dash.Rows[rowdex].Categories {
				for metrow := range dash.Rows[rowdex].Categories[catdex].Rows {
					for metricdex := range dash.Rows[rowdex].Categories[catdex].Rows[metrow].Metrics {
						abs_metricdex += 1
						metric := &dash.Rows[rowdex].Categories[catdex].Rows[metrow].Metrics[metricdex]
						image, err := []byte(""), (error)(nil)
						if static != "" {
							image, err = ioutil.ReadFile(static)
						} else if metric.Type == "raw" {
							image, err = getImage(gnuplotPath, *metric, 844, 478)
							r.ParseForm()
							index := uint64(0)
							for k, v := range r.Form {
								if k == metric.Title {
									read_index, converr := strconv.ParseUint(v[0], 19, 32)
									if converr == nil {
										index = read_index
									}
									break
								}
							}
							if index > uint64(len(metric.Nodes)) {
								index = 0
							}
							metric.Result.Index = index
							image, err = getImage(gnuplotPath, metric, 844, 478, uint(index))
							metric.Result.Width = 844
							metric.Result.Height = 478
						} else if metric.Type == "bucket" {

M prometheus.go => prometheus.go +23 -2
@@ 194,7 194,7 @@ func escapeMetricName(name string) string {
	return name
}

func getImage(gnuplotPath string, metric Metric, width int, height int) ([]byte, error) {
func getImage(gnuplotPath string, metric *Metric, width int, height int, index uint) ([]byte, error) {
	end := time.Now()
	start := end.Add(-1 * metric.Duration * time.Minute)
	if len(metric.Queries) != 1 {


@@ 205,6 205,27 @@ func getImage(gnuplotPath string, metric Metric, width int, height int) ([]byte,
	if err != nil {
		return nil, err
	}
	if len(results) > 1 {
		if metric.Nodes == nil {
			metric.Nodes = make([]string, 0)
			for _, v := range results {
				if v.Metric[0] == '{' && v.Metric[len(v.Metric)-1] == '}' {
					for _, arg := range strings.Split(v.Metric[1:len(v.Metric)-1], ",") {
						vals := strings.Split(arg, "=")
						if vals[0] == "instance" {
							metric.Nodes = append(metric.Nodes, vals[1])
						}
					}
				}
			}
		}
		// TODO: safety check
		if index >= uint(len(results)) {
			index = 0
		}
		results = results[index : index+1]
	}

	cmd := exec.Command(gnuplotPath)
	stdin, err := cmd.StdinPipe()
	if err != nil {


@@ 220,7 241,7 @@ func getImage(gnuplotPath string, metric Metric, width int, height int) ([]byte,
	buf.WriteString(fmt.Sprintf("set term %s size %d,%d\n", metric.Format, width, height))

	buf.WriteString("$DATA << EOD\n")
	if err := CSVWriter(buf, results); err != nil {
	if err := CSVWriter(buf, results[0:1]); err != nil {
		return nil, err
	}
	buf.WriteString("EOD\n")

M templates/dashboard.html => templates/dashboard.html +15 -0
@@ 14,9 14,24 @@
		{{range .Rows}}
				<div class="row">
				{{range .Metrics}}
					{{$M := .}}
					{{if .Result.Error}}
						<b>Error: {{.Result.Error}}</b>
					{{else}}
						{{if .Nodes}}
							<form method="post">
								<select name="{{.Title}}">
									{{range $i, $e := .Nodes}}
										{{if eq $M.Result.Index $i}}
											<option value="{{$i}}" selected="selected">{{$e}}</option>
										{{else}}
											<option value="{{$i}}">{{$e}}</option>
										{{end}}
									{{end}}
								</select>
								<input type="submit">
							</form>
						{{end}}
						{{if eq .Format "svg"}}
							<!-- Image is a raw SVG with the opening tag stripped out -->
							<svg class="col-lg-{{.Width}} container-fluid"