diff --git a/README.md b/README.md index 190f3be..9bb6b3c 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,11 @@ A simple HTTP server that reads RRD files and responds to requests from Grafana [![CircleCI](https://img.shields.io/circleci/project/github/doublemarket/grafana-rrd-server.svg)](https://github.com/doublemarket/grafana-rrd-server) [![GitHub release](https://img.shields.io/github/release/doublemarket/grafana-rrd-server.svg)](https://github.com/doublemarket/grafana-rrd-server/releases) +This server supports all endpoints (urls) defined in the Grafana Simple JSON Datasource plugin documentation](https://grafana.net/plugins/grafana-simple-json-datasource) but: + +- The `/annotation` endpoint always returns the same result `{"message":"annotations"}`. (the endpoint exists but no features are implemented) +- You can use `*` as a wildcard in the `target` values (but not for `ds`) for the `/query` endpoint. + # Requirement - librrd-dev (rrdtool) diff --git a/rrdserver.go b/rrdserver.go index 44bf7fd..06af729 100644 --- a/rrdserver.go +++ b/rrdserver.go @@ -13,6 +13,7 @@ import ( "strings" "time" + "github.com/mattn/go-zglob" "github.com/ziutek/rrd" ) @@ -128,43 +129,50 @@ func query(w http.ResponseWriter, r *http.Request) { var result []QueryResponse for _, target := range queryRequest.Targets { - var points [][]float64 ds := target.Target[strings.LastIndex(target.Target, ":")+1 : len(target.Target)] rrdDsRep := regexp.MustCompile(`:` + ds + `$`) - fPath := rrdDsRep.ReplaceAllString(target.Target, "") - fPath = config.Server.RrdPath + strings.Replace(fPath, ":", "/", -1) + ".rrd" - if _, err := os.Stat(fPath); err != nil { - fmt.Println("File", fPath, "does not exist") - continue - } - infoRes, err := rrd.Info(fPath) - if err != nil { - fmt.Println("ERROR: Cannot retrieve information from ", fPath) - fmt.Println(err) - } - lastUpdate := time.Unix(int64(infoRes["last_update"].(uint)), 0) - if to.After(lastUpdate) && lastUpdate.After(from) { - to = lastUpdate - } - fetchRes, err := rrd.Fetch(fPath, "AVERAGE", from, to, time.Duration(config.Server.Step)*time.Second) - if err != nil { - fmt.Println("ERROR: Cannot retrieve time series data from ", fPath) - fmt.Println(err) - } - timestamp := fetchRes.Start - dsIndex := int(infoRes["ds.index"].(map[string]interface{})[ds].(uint)) - // The last point is likely to contain wrong data (mostly a big number) - // RowCnt-1 is for ignoring the last point (temporary solution) - for i := 0; i < fetchRes.RowCnt-1; i++ { - value := fetchRes.ValueAt(dsIndex, i) - if !math.IsNaN(value) { - points = append(points, []float64{value, float64(timestamp.Unix()) * 1000}) + fileSearchPath := rrdDsRep.ReplaceAllString(target.Target, "") + fileSearchPath = config.Server.RrdPath + strings.Replace(fileSearchPath, ":", "/", -1) + ".rrd" + + fileNameArray, _ := zglob.Glob(fileSearchPath) + for _, filePath := range fileNameArray { + var points [][]float64 + if _, err = os.Stat(filePath); err != nil { + fmt.Println("File", filePath, "does not exist") + continue } - timestamp = timestamp.Add(fetchRes.Step) - } - defer fetchRes.FreeValues() + infoRes, err := rrd.Info(filePath) + if err != nil { + fmt.Println("ERROR: Cannot retrieve information from ", filePath) + fmt.Println(err) + } + lastUpdate := time.Unix(int64(infoRes["last_update"].(uint)), 0) + if to.After(lastUpdate) && lastUpdate.After(from) { + to = lastUpdate + } + fetchRes, err := rrd.Fetch(filePath, "AVERAGE", from, to, time.Duration(config.Server.Step)*time.Second) + if err != nil { + fmt.Println("ERROR: Cannot retrieve time series data from ", filePath) + fmt.Println(err) + } + timestamp := fetchRes.Start + dsIndex := int(infoRes["ds.index"].(map[string]interface{})[ds].(uint)) + // The last point is likely to contain wrong data (mostly a big number) + // RowCnt-1 is for ignoring the last point (temporary solution) + for i := 0; i < fetchRes.RowCnt-1; i++ { + value := fetchRes.ValueAt(dsIndex, i) + if !math.IsNaN(value) { + points = append(points, []float64{value, float64(timestamp.Unix()) * 1000}) + } + timestamp = timestamp.Add(fetchRes.Step) + } + defer fetchRes.FreeValues() - result = append(result, QueryResponse{Target: target.Target, DataPoints: points}) + extractedTarget := strings.Replace(filePath, ".rrd", "", -1) + extractedTarget = strings.Replace(extractedTarget, config.Server.RrdPath, "", -1) + extractedTarget = strings.Replace(extractedTarget, "/", ":", -1) + ":" + ds + result = append(result, QueryResponse{Target: extractedTarget, DataPoints: points}) + } } json, err := json.Marshal(result) if err != nil { diff --git a/rrdserver_test.go b/rrdserver_test.go index 7100e66..140f7f7 100644 --- a/rrdserver_test.go +++ b/rrdserver_test.go @@ -108,7 +108,8 @@ func TestQuery(t *testing.T) { "intervalMs":60000, "targets":[ {"target":"sample:ClientJobsIdle","refId":"A","hide":false,"type":"timeserie"}, - {"target":"sample:ClientJobsRunning","refId":"B","hide":false,"type":"timeserie"} + {"target":"sample:ClientJobsRunning","refId":"B","hide":false,"type":"timeserie"}, + {"target":"percent-*:value","refId":"B","hide":false,"type":"timeserie"} ], "format":"json", "maxDataPoints":1812 @@ -141,10 +142,31 @@ func TestQuery(t *testing.T) { t.Fatalf("Error at decoding JSON response. %v", err) } + clientJobsIdleExists := false + percentIdleExists := false + percentUserExists := false for _, v := range qrs { if len(v.Target) < 0 { t.Fatalf("Response is empty.") } + if v.Target == "sample:ClientJobsIdle" { + clientJobsIdleExists = true + } + if v.Target == "sample:percent-idle:value" { + percentIdleExists = true + } + if v.Target == "sample:percent-user:value" { + percentUserExists = true + } + } + if !clientJobsIdleExists { + t.Fatal("sample:ClientJobsIdle isn't contained in the response.") + } + if !percentIdleExists { + t.Fatal("sample:percent-idle:value isn't contained in the response.") + } + if !percentUserExists { + t.Fatal("sample:percent-user:value isn't contained in the response.") } } diff --git a/sample/percent-idle.rrd b/sample/percent-idle.rrd new file mode 100644 index 0000000..87b51f1 Binary files /dev/null and b/sample/percent-idle.rrd differ diff --git a/sample/percent-user.rrd b/sample/percent-user.rrd new file mode 100644 index 0000000..c74932d Binary files /dev/null and b/sample/percent-user.rrd differ