Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support wildcard for target in /query #23

Merged
merged 2 commits into from Apr 25, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.md
Expand Up @@ -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)
Expand Down
74 changes: 41 additions & 33 deletions rrdserver.go
Expand Up @@ -13,6 +13,7 @@ import (
"strings"
"time"

"github.com/mattn/go-zglob"
"github.com/ziutek/rrd"
)

Expand Down Expand Up @@ -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 {
Expand Down
24 changes: 23 additions & 1 deletion rrdserver_test.go
Expand Up @@ -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
Expand Down Expand Up @@ -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.")
}
}

Expand Down
Binary file added sample/percent-idle.rrd
Binary file not shown.
Binary file added sample/percent-user.rrd
Binary file not shown.