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 submetrics #37

Merged
merged 1 commit into from
Dec 3, 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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,9 @@ type Query struct {
Params map[string]interface{}
Interval time.Duration
Timeout time.Duration
DataField string `yaml:"data-field"`
ValueOnError string `yaml:"value-on-error"`
DataField string `yaml:"data-field"`
SubMetrics map[string]string `yaml:"sub-metrics"`
ValueOnError string `yaml:"value-on-error"`
}

// QueryList is a array or Queries
Expand Down
32 changes: 32 additions & 0 deletions config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,38 @@ func Test_loadQueryConfig(t *testing.T) {
},
wantErr: false,
},
{
name: "queries-submetrics",
args: args{
queriesFile: "test-resources/config-test/queries-submetrics.yml",
config: c,
},
want: []*Query{
&Query{
Name: "query_ds_1",
SQL: `select 1 as "sum", 2 as "count" from dual`,
DataSourceRef: "my-ds-1",
Driver: "mysql",
Connection: map[string]interface{}{
"host": "localhost",
"port": 3306,
"user": "root",
"password": "unsecure",
"database": "test",
},
Interval: time.Minute * 15,
Timeout: time.Minute * 5,
Params: nil,
SubMetrics: map[string]string{
"count": "count",
"sum": "sum",
},
ValueOnError: "-1",
DataField: "",
},
},
wantErr: false,
},
{
name: "queries-compatibility",
args: args{
Expand Down
12 changes: 12 additions & 0 deletions examples/example-config-queries.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,15 @@
sql: >
select * from missing_table
interval: 30s

# Sub queries
# This will register two metrics:
# - response_time_count with cnt as value
# - response_time_sum with rt as value
- response_time:
sql: >
select count(*) as cnt, sum(response_time) as rt from requests
sub-metrics:
count: cnt
sum: rt
interval: 30s
86 changes: 57 additions & 29 deletions set.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import (
"encoding/json"
"errors"
"fmt"
"github.com/prometheus/client_golang/prometheus"
"strconv"
"strings"

"github.com/prometheus/client_golang/prometheus"
)

type QueryResult struct {
Expand All @@ -24,11 +25,15 @@ func NewQueryResult(q *Query) *QueryResult {
return r
}

func (r *QueryResult) registerMetric(facets map[string]interface{}) string {
func (r *QueryResult) registerMetric(facets map[string]interface{}, suffix string) string {
labels := prometheus.Labels{}
metricName := r.Query.Name
if suffix != "" {
metricName = fmt.Sprintf("%s_%s", r.Query.Name, suffix)
}

jsonData, _ := json.Marshal(facets)
resultKey := string(jsonData)
resultKey := fmt.Sprintf("%s%s", metricName, string(jsonData))

for k, v := range facets {
labels[k] = strings.ToLower(fmt.Sprintf("%v", v))
Expand All @@ -38,9 +43,9 @@ func (r *QueryResult) registerMetric(facets map[string]interface{}) string {
return resultKey
}

fmt.Println("Registering metric", r.Query.Name, "with facets", resultKey)
fmt.Println("Registering metric", resultKey)
r.Result[resultKey] = prometheus.NewGauge(prometheus.GaugeOpts{
Name: fmt.Sprintf("query_result_%s", r.Query.Name),
Name: fmt.Sprintf("query_result_%s", metricName),
Help: "Result of an SQL query",
ConstLabels: labels,
})
Expand Down Expand Up @@ -75,35 +80,58 @@ func (r *QueryResult) SetMetrics(recs records) (map[string]bool, error) {
return nil, errors.New("There is more than one row in the query result - with a single column")
}

if r.Query.DataField != "" && len(r.Query.SubMetrics) > 0 {
return nil, errors.New("sub-metrics are not compatible with data-field")
}

submetrics := map[string]string{}

if len(r.Query.SubMetrics) > 0 {
submetrics = r.Query.SubMetrics
} else {
submetrics = map[string]string{"": r.Query.DataField}
}

facetsWithResult := make(map[string]bool, 0)
for _, row := range recs {
facet := make(map[string]interface{})
var (
dataVal interface{}
dataFound bool
)
if len(row) > 1 && r.Query.DataField == "" {
return nil, errors.New("Data field not specified for multi-column query")
}
for k, v := range row {
if len(row) > 1 && strings.ToLower(k) != r.Query.DataField { // facet field, add to facets
facet[strings.ToLower(fmt.Sprintf("%v", k))] = v
} else { // this is the actual gauge data
dataVal = v
dataFound = true
for suffix, datafield := range submetrics {
facet := make(map[string]interface{})
var (
dataVal interface{}
dataFound bool
)
for k, v := range row {
if len(row) > 1 && strings.ToLower(k) != datafield { // facet field, add to facets
submetric := false
for _, n := range submetrics {
if strings.ToLower(k) == n {
submetric = true
}
}
// it is a facet field and not a submetric field
if !submetric {
facet[strings.ToLower(fmt.Sprintf("%v", k))] = v
}
} else { // this is the actual gauge data
if dataFound {
return nil, errors.New("Data field not specified for multi-column query")
}
dataVal = v
dataFound = true
}
}
}

if !dataFound {
return nil, errors.New("Data field not found in result set")
}
if !dataFound {
return nil, errors.New("Data field not found in result set")
}

key := r.registerMetric(facet)
err := setValueForResult(r.Result[key], dataVal)
if err != nil {
return nil, err
key := r.registerMetric(facet, suffix)
err := setValueForResult(r.Result[key], dataVal)
if err != nil {
return nil, err
}
facetsWithResult[key] = true
}
facetsWithResult[key] = true
}

return facetsWithResult, nil
Expand All @@ -114,7 +142,7 @@ func (r *QueryResult) RemoveMissingMetrics(facetsWithResult map[string]bool) {
if _, ok := facetsWithResult[key]; ok {
continue
}
fmt.Println("Unregistering metric", r.Query.Name, "with facets", key)
fmt.Println("Unregistering metric", key)
prometheus.Unregister(m)
delete(r.Result, key)
}
Expand Down
146 changes: 146 additions & 0 deletions set_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
package main

import (
"testing"

"github.com/gogo/protobuf/proto"
dto "github.com/prometheus/client_model/go"
)

type testQuerySetOptions struct {
q *QueryResult
rec records
results map[string]string
}

func TestSimpleQuerySet(t *testing.T) {
(&testQuerySetOptions{
q: NewQueryResult(&Query{
Name: "simple_metric",
DataField: "value",
}),
rec: records{
record{
"name": "foo",
"value": 67,
},
},
results: map[string]string{
`simple_metric{"name":"foo"}`: `label: <
name: "name"
value: "foo"
>
gauge: <
value: 67
>
`,
},
}).testQuerySet(t)
}

func TestSingleQuerySet(t *testing.T) {
(&testQuerySetOptions{
q: NewQueryResult(&Query{
Name: "single_metric",
}),
rec: records{
record{
"value": 42,
},
},
results: map[string]string{
"single_metric{}": `gauge: <
value: 42
>
`,
},
}).testQuerySet(t)
}

func TestSubMetrics(t *testing.T) {
(&testQuerySetOptions{
q: NewQueryResult(&Query{
Name: "submetrics_metric",
SubMetrics: map[string]string{
"total": "rt",
"count": "cnt",
},
}),
rec: records{
record{
"rt": 200,
"cnt": 5,
"name": "foo",
},
record{
"rt": 500,
"cnt": 3,
"name": "bar",
},
},
results: map[string]string{
`submetrics_metric_total{"name":"foo"}`: `label: <
name: "name"
value: "foo"
>
gauge: <
value: 200
>
`,
`submetrics_metric_count{"name":"foo"}`: `label: <
name: "name"
value: "foo"
>
gauge: <
value: 5
>
`,
`submetrics_metric_total{"name":"bar"}`: `label: <
name: "name"
value: "bar"
>
gauge: <
value: 500
>
`,
`submetrics_metric_count{"name":"bar"}`: `label: <
name: "name"
value: "bar"
>
gauge: <
value: 3
>
`,
},
}).testQuerySet(t)
}

func (opts *testQuerySetOptions) testQuerySet(t *testing.T) {
_, err := opts.q.SetMetrics(opts.rec)
if err != nil {
t.Errorf("Error while setting metrics: %v", err)
return
}

numRes := len(opts.q.Result)
if numRes != len(opts.results) {
t.Errorf("Bad number of result ; expected: %d, got: %d.", numRes, len(opts.results))
return
}

for k, v := range opts.results {
m := opts.q.Result[k]
if m == nil {
t.Errorf("Can not find metric `%s`.", k)
return
}
metric := &dto.Metric{}
m.Write(metric)
out := proto.MarshalTextString(metric)
if v != out {
t.Errorf("[%s] bad output ; expected: \n`%s`\nGot: \n`%s`\n", k, v, out)
return
}
}

}
5 changes: 5 additions & 0 deletions test-resources/config-test/queries-submetrics.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
- query_ds_1:
sql: select 1 as "sum", 2 as "count" from dual
sub-metrics:
sum: sum
count: count