Skip to content

Commit

Permalink
feature: count json objects with same value
Browse files Browse the repository at this point in the history
if we want to count json objects with same values we can use
countbylabel type in the metric configuration

Signed-off-by: Parsa <[email protected]>
  • Loading branch information
Parsa committed Apr 28, 2022
1 parent 1614ee8 commit 30d4492
Show file tree
Hide file tree
Showing 7 changed files with 98 additions and 7 deletions.
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,15 @@ metrics:
count: '{.count}' # dynamic value
boolean: '{.some_boolean}'

- name: example_count
type: countbylabel
help: Example of count json labels and
path: '{.values[*].state}'
labels:
environment: beta # static label
state: '{}' # dynamic label
mincount: 1

headers:
X-Dummy: my-test-header

Expand All @@ -68,6 +77,8 @@ Serving HTTP on 0.0.0.0 port 8000 ...
$ ./json_exporter --config.file examples/config.yml &

$ curl "http://localhost:7979/probe?target=http://localhost:8000/examples/data.json" | grep ^example
example_count{environment="beta",state="ACTIVE"} 2
example_count{environment="beta",state="INACTIVE"} 1
example_global_value{environment="beta",location="planet-mars"} 1234
example_value_active{environment="beta",id="id-A"} 1
example_value_active{environment="beta",id="id-C"} 1
Expand Down
17 changes: 11 additions & 6 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,21 @@ import (

// Metric contains values that define a metric
type Metric struct {
Name string
Path string
Labels map[string]string
Type MetricType
Help string
Values map[string]string
Name string
Path string
Labels map[string]string
Type MetricType
Help string
Values map[string]string
Mincount int
}

type MetricType string

const (
ValueScrape MetricType = "value" // default
ObjectScrape MetricType = "object"
CountScrape MetricType = "countbylabel"
)

// Config contains metrics and headers defining a configuration
Expand Down Expand Up @@ -69,6 +71,9 @@ func LoadConfig(configPath string) (Config, error) {
if config.Metrics[i].Help == "" {
config.Metrics[i].Help = config.Metrics[i].Name
}
if !(config.Metrics[i].Mincount > 0) {
config.Metrics[i].Mincount = 1
}
}

return config, nil
Expand Down
9 changes: 9 additions & 0 deletions examples/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,15 @@ metrics:
count: '{.count}' # dynamic value
boolean: '{.some_boolean}'

- name: example_count
type: countbylabel
help: Example of count json labels and
path: '{.values[*].state}'
labels:
environment: beta # static label
state: '{}' # dynamic label
mincount: 1

headers:
X-Dummy: my-test-header

Expand Down
39 changes: 38 additions & 1 deletion exporter/collector.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ type JSONMetric struct {
KeyJSONPath string
ValueJSONPath string
LabelsJSONPaths []string
Mincount int
}

func (mc JSONMetricCollector) Describe(ch chan<- *prometheus.Desc) {
Expand All @@ -44,7 +45,7 @@ func (mc JSONMetricCollector) Describe(ch chan<- *prometheus.Desc) {

func (mc JSONMetricCollector) Collect(ch chan<- prometheus.Metric) {
for _, m := range mc.JSONMetrics {
if m.ValueJSONPath == "" { // ScrapeType is 'value'
if m.ValueJSONPath == "" && m.Mincount == 0 { // ScrapeType is 'value'
value, err := extractValue(mc.Logger, mc.Data, m.KeyJSONPath, false)
if err != nil {
level.Error(mc.Logger).Log("msg", "Failed to extract value for metric", "path", m.KeyJSONPath, "err", err, "metric", m.Desc)
Expand All @@ -63,6 +64,42 @@ func (mc JSONMetricCollector) Collect(ch chan<- prometheus.Metric) {
level.Error(mc.Logger).Log("msg", "Failed to convert extracted value to float64", "path", m.KeyJSONPath, "value", value, "err", err, "metric", m.Desc)
continue
}
} else if m.Mincount > 0 { // ScrapeType is 'countbylabel'
values, err := extractValue(mc.Logger, mc.Data, m.KeyJSONPath, true)
if err != nil {
level.Error(mc.Logger).Log("msg", "Failed to extract json objects for metric", "err", err, "metric", m.Desc)
continue
}

var jsonData []interface{}
counts := make(map[interface{}]int)

if err := json.Unmarshal([]byte(values), &jsonData); err == nil {
for _, data := range jsonData {
counts[data]++
}
for data, count := range counts {
if count >= m.Mincount {
jdata, err := json.Marshal(data)
if err != nil {
level.Error(mc.Logger).Log("msg", "Failed to marshal data to json", "path", m.ValueJSONPath, "err", err, "metric", m.Desc, "data", data)
continue
}
if err != nil {
level.Error(mc.Logger).Log("msg", "Failed to extract value for metric", "path", m.ValueJSONPath, "err", err, "metric", m.Desc)
continue
}

ch <- prometheus.MustNewConstMetric(
m.Desc,
prometheus.UntypedValue,
float64(count),
extractLabels(mc.Logger, jdata, m.LabelsJSONPaths)...,
)
}
}
}

} else { // ScrapeType is 'object'
values, err := extractValue(mc.Logger, mc.Data, m.KeyJSONPath, true)
if err != nil {
Expand Down
18 changes: 18 additions & 0 deletions exporter/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,24 @@ func CreateMetricsList(c config.Config) ([]JSONMetric, error) {
}
metrics = append(metrics, jsonMetric)
}
case config.CountScrape:
var variableLabels, variableLabelsValues []string
for k, v := range metric.Labels {
variableLabels = append(variableLabels, k)
variableLabelsValues = append(variableLabelsValues, v)
}
jsonMetric := JSONMetric{
Desc: prometheus.NewDesc(
metric.Name,
metric.Help,
variableLabels,
nil,
),
KeyJSONPath: metric.Path,
Mincount: metric.Mincount,
LabelsJSONPaths: variableLabelsValues,
}
metrics = append(metrics, jsonMetric)
default:
return nil, fmt.Errorf("Unknown metric type: '%s', for metric: '%s'", metric.Type, metric.Name)
}
Expand Down
7 changes: 7 additions & 0 deletions test/config/good.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,10 @@ metrics:
count: '{.count}' # dynamic value
boolean: '{.some_boolean}'

- name: example_count
type: countbylabel
help: Example of count object from a json
path: '{.values[*].state}'
labels:
environment: beta # static label
state: '{}' # dynamic label
4 changes: 4 additions & 0 deletions test/response/good.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# HELP example_count Example of count object from a json
# TYPE example_count untyped
example_count{environment="beta",state="ACTIVE"} 2
example_count{environment="beta",state="INACTIVE"} 1
# HELP example_global_value Example of a top-level global value scrape in the json
# TYPE example_global_value untyped
example_global_value{environment="beta",location="planet-mars"} 1234
Expand Down

0 comments on commit 30d4492

Please sign in to comment.