Skip to content

Commit ec3e16a

Browse files
committed
Feat: Add data transformations.
Signed-off-by: Matt Kruse <[email protected]>
1 parent 03abaa7 commit ec3e16a

12 files changed

+376
-14
lines changed

README.md

+14
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,20 @@ animal_population{name="deer",predator="false"} 456
5050
animal_population{name="lion",predator="true"} 123
5151
animal_population{name="pigeon",predator="false"} 789
5252

53+
## Test transform module
54+
$ curl "http://localhost:7979/probe?module=transform&target=http://localhost:8000/examples/transform-data.json"
55+
56+
# HELP origin_health Health of each origin in the pool
57+
# TYPE origin_health untyped
58+
origin_health{address="10.0.0.1",endpoint_name="origin3",pool_id="2",pool_name="pool2"} 1
59+
origin_health{address="10.0.0.1",endpoint_name="origin4",pool_id="3",pool_name="pool3"} 0
60+
origin_health{address="127.0.0.1",endpoint_name="origin1",pool_id="1",pool_name="pool1"} 1
61+
origin_health{address="192.168.1.1",endpoint_name="origin2",pool_id="1",pool_name="pool1"} 0
62+
# HELP pool_health Health of the pools
63+
# TYPE pool_health untyped
64+
pool_health{pool_id="1",pool_name="pool1"} 1
65+
pool_health{pool_id="2",pool_name="pool2"} 1
66+
pool_health{pool_id="3",pool_name="pool3"} 0
5367

5468
## TEST through prometheus:
5569

config/config.go

+10-8
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
package config
1515

1616
import (
17+
"github.com/prometheus-community/json_exporter/transformers"
1718
"os"
1819

1920
pconfig "github.com/prometheus/common/config"
@@ -22,14 +23,15 @@ import (
2223

2324
// Metric contains values that define a metric
2425
type Metric struct {
25-
Name string
26-
Path string
27-
Labels map[string]string
28-
Type ScrapeType
29-
ValueType ValueType
30-
EpochTimestamp string
31-
Help string
32-
Values map[string]string
26+
Name string
27+
Path string
28+
Labels map[string]string
29+
Type ScrapeType
30+
ValueType ValueType
31+
EpochTimestamp string
32+
Help string
33+
Values map[string]string
34+
Transformations []transformers.TransformationConfig
3335
}
3436

3537
type ScrapeType string

examples/config.yml

+31
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,37 @@ modules:
4343
values:
4444
population: '{ .population }'
4545

46+
transform:
47+
metrics:
48+
- name: origin
49+
transformations:
50+
- type: jq
51+
query: |-
52+
.result[] | .name as $poolName | .id as $poolId | .origins[] | ( .name as $name | .healthy as $endpointHealth | {endpointName: $name, endpointHealthy: .healthy, poolName: $poolName, address:.address, poolId:$poolId, address:.address} )
53+
help: Health of each origin in the pool
54+
path: '{ [*] }'
55+
type: object
56+
labels:
57+
pool_id: '{.poolId}'
58+
pool_name: '{.poolName}'
59+
address: '{.address}'
60+
endpoint_name: '{.endpointName}'
61+
values:
62+
health: '{.endpointHealthy}' # Extract only the `healthy` field
63+
64+
65+
- name: pool
66+
type: object
67+
help: Health of the pools
68+
path: '{.result[*]}'
69+
labels:
70+
pool_name: '{.name}'
71+
pool_id: '{.id}'
72+
values:
73+
health: '{.healthy}'
74+
75+
76+
4677
## HTTP connection configurations can be set in 'modules.<module_name>.http_client_config' field. For full http client config parameters, ref: https://pkg.go.dev/github.com/prometheus/common/config?tab=doc#HTTPClientConfig
4778
#
4879
# http_client_config:

examples/prometheus.yml

+15
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,18 @@ scrape_configs:
3232
## Location of the json exporter's real <hostname>:<port>
3333
replacement: host.docker.internal:7979 # equivalent to "localhost:7979"
3434

35+
## Gather metrics from transform-data JSON source using the 'transform' module
36+
- job_name: json_transform
37+
metrics_path: /probe
38+
params:
39+
module: [transform] # Use the 'transform' module
40+
static_configs:
41+
- targets:
42+
- http://localhost:8000/examples/transform-data.json
43+
relabel_configs:
44+
- source_labels: [__address__]
45+
target_label: __param_target
46+
- source_labels: [__param_target]
47+
target_label: instance
48+
- target_label: __address__
49+
replacement: host.docker.internal:7979 # json_exporter instance

examples/transform-data.json

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
{
2+
"result": [
3+
{
4+
"name": "pool1",
5+
"id": "1",
6+
"healthy": true,
7+
"origins": [
8+
{
9+
"name": "origin1",
10+
"address": "127.0.0.1",
11+
"healthy": true
12+
},
13+
{
14+
"name": "origin2",
15+
"address": "192.168.1.1",
16+
"healthy": false
17+
}
18+
]
19+
},
20+
{
21+
"name": "pool2",
22+
"id": "2",
23+
"healthy": true,
24+
"origins": [
25+
{
26+
"name": "origin3",
27+
"address": "10.0.0.1",
28+
"healthy": true
29+
}
30+
]
31+
},
32+
{
33+
"name": "pool3",
34+
"id": "3",
35+
"healthy": false,
36+
"origins": [
37+
{
38+
"name": "origin4",
39+
"address": "10.0.0.1",
40+
"healthy": false
41+
}
42+
]
43+
}
44+
]
45+
}

exporter/collector.go

+20-6
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package exporter
1616
import (
1717
"bytes"
1818
"encoding/json"
19+
"github.com/prometheus-community/json_exporter/transformers"
1920
"time"
2021

2122
"github.com/go-kit/log"
@@ -39,6 +40,7 @@ type JSONMetric struct {
3940
LabelsJSONPaths []string
4041
ValueType prometheus.ValueType
4142
EpochTimestampJSONPath string
43+
Transformers []transformers.Transformer
4244
}
4345

4446
func (mc JSONMetricCollector) Describe(ch chan<- *prometheus.Desc) {
@@ -49,11 +51,22 @@ func (mc JSONMetricCollector) Describe(ch chan<- *prometheus.Desc) {
4951

5052
func (mc JSONMetricCollector) Collect(ch chan<- prometheus.Metric) {
5153
for _, m := range mc.JSONMetrics {
54+
55+
jsonData := mc.Data
56+
for _, transformer := range m.Transformers {
57+
transformedData, err := transformer.Transform(jsonData)
58+
if err != nil {
59+
level.Error(mc.Logger).Log("msg", "Transformation failed", "err", err)
60+
continue
61+
}
62+
jsonData = transformedData
63+
}
64+
5265
switch m.Type {
5366
case config.ValueScrape:
54-
value, err := extractValue(mc.Logger, mc.Data, m.KeyJSONPath, false)
67+
value, err := extractValue(mc.Logger, jsonData, m.KeyJSONPath, false)
5568
if err != nil {
56-
level.Error(mc.Logger).Log("msg", "Failed to extract value for metric", "path", m.KeyJSONPath, "err", err, "metric", m.Desc)
69+
level.Error(mc.Logger).Log("msg", "Failed to extract value for metric", "path", m.KeyJSONPath, "err", err, "metric", m.Desc, "data", jsonData)
5770
continue
5871
}
5972

@@ -62,16 +75,17 @@ func (mc JSONMetricCollector) Collect(ch chan<- prometheus.Metric) {
6275
m.Desc,
6376
m.ValueType,
6477
floatValue,
65-
extractLabels(mc.Logger, mc.Data, m.LabelsJSONPaths)...,
78+
extractLabels(mc.Logger, jsonData, m.LabelsJSONPaths)...,
6679
)
67-
ch <- timestampMetric(mc.Logger, m, mc.Data, metric)
80+
ch <- timestampMetric(mc.Logger, m, jsonData, metric)
6881
} else {
69-
level.Error(mc.Logger).Log("msg", "Failed to convert extracted value to float64", "path", m.KeyJSONPath, "value", value, "err", err, "metric", m.Desc)
82+
level.Error(mc.Logger).Log("msg", "Failed to convert extracted value to float64", "path", m.KeyJSONPath, "value", value, "err", err, "metric", m.Desc) // was getting error here!
7083
continue
7184
}
7285

7386
case config.ObjectScrape:
74-
values, err := extractValue(mc.Logger, mc.Data, m.KeyJSONPath, true)
87+
values, err := extractValue(mc.Logger, jsonData, m.KeyJSONPath, true)
88+
7589
if err != nil {
7690
level.Error(mc.Logger).Log("msg", "Failed to extract json objects for metric", "err", err, "metric", m.Desc)
7791
continue

exporter/util.go

+19
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"context"
1818
"errors"
1919
"fmt"
20+
"github.com/prometheus-community/json_exporter/transformers"
2021
"io"
2122
"math"
2223
"net/http"
@@ -80,6 +81,22 @@ func CreateMetricsList(c config.Module) ([]JSONMetric, error) {
8081
valueType prometheus.ValueType
8182
)
8283
for _, metric := range c.Metrics {
84+
metricTransfomers := []transformers.Transformer{}
85+
for _, tConfig := range metric.Transformations {
86+
transformer, err := transformers.NewTransformer(tConfig) // Use the package reference here
87+
if err != nil {
88+
return nil, err
89+
}
90+
metricTransfomers = append(metricTransfomers, transformer)
91+
}
92+
switch metric.ValueType {
93+
case config.ValueTypeGauge:
94+
valueType = prometheus.GaugeValue
95+
case config.ValueTypeCounter:
96+
valueType = prometheus.CounterValue
97+
default:
98+
valueType = prometheus.UntypedValue
99+
}
83100
switch metric.ValueType {
84101
case config.ValueTypeGauge:
85102
valueType = prometheus.GaugeValue
@@ -107,6 +124,7 @@ func CreateMetricsList(c config.Module) ([]JSONMetric, error) {
107124
LabelsJSONPaths: variableLabelsValues,
108125
ValueType: valueType,
109126
EpochTimestampJSONPath: metric.EpochTimestamp,
127+
Transformers: metricTransfomers, // Add transformers here
110128
}
111129
metrics = append(metrics, jsonMetric)
112130
case config.ObjectScrape:
@@ -130,6 +148,7 @@ func CreateMetricsList(c config.Module) ([]JSONMetric, error) {
130148
LabelsJSONPaths: variableLabelsValues,
131149
ValueType: valueType,
132150
EpochTimestampJSONPath: metric.EpochTimestamp,
151+
Transformers: metricTransfomers, // Add transformers here
133152
}
134153
metrics = append(metrics, jsonMetric)
135154
}

go.mod

+2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ require (
66
github.com/Masterminds/sprig/v3 v3.2.3
77
github.com/alecthomas/kingpin/v2 v2.4.0
88
github.com/go-kit/log v0.2.1
9+
github.com/itchyny/gojq v0.12.16
910
github.com/prometheus/client_golang v1.19.1
1011
github.com/prometheus/common v0.55.0
1112
github.com/prometheus/exporter-toolkit v0.11.0
@@ -24,6 +25,7 @@ require (
2425
github.com/google/uuid v1.3.0 // indirect
2526
github.com/huandu/xstrings v1.3.3 // indirect
2627
github.com/imdario/mergo v0.3.11 // indirect
28+
github.com/itchyny/timefmt-go v0.1.6 // indirect
2729
github.com/jpillora/backoff v1.0.0 // indirect
2830
github.com/mitchellh/copystructure v1.0.0 // indirect
2931
github.com/mitchellh/reflectwalk v1.0.0 // indirect

go.sum

+4
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ github.com/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4
3131
github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
3232
github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA=
3333
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
34+
github.com/itchyny/gojq v0.12.16 h1:yLfgLxhIr/6sJNVmYfQjTIv0jGctu6/DgDoivmxTr7g=
35+
github.com/itchyny/gojq v0.12.16/go.mod h1:6abHbdC2uB9ogMS38XsErnfqJ94UlngIJGlRAIj4jTM=
36+
github.com/itchyny/timefmt-go v0.1.6 h1:ia3s54iciXDdzWzwaVKXZPbiXzxxnv1SPGFfM/myJ5Q=
37+
github.com/itchyny/timefmt-go v0.1.6/go.mod h1:RRDZYC5s9ErkjQvTvvU7keJjxUYzIISJGxm9/mAERQg=
3438
github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
3539
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
3640
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=

transformers/jq_transformer.go

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
// Copyright 2020 The Prometheus Authors
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at
5+
//
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
package transformers
15+
16+
import (
17+
"encoding/json"
18+
"github.com/itchyny/gojq"
19+
)
20+
21+
// JQTransformer struct for jq transformation
22+
type JQTransformer struct {
23+
Query string
24+
}
25+
26+
// NewJQTransformer creates a new JQTransformer with a given query
27+
func NewJQTransformer(query string) JQTransformer {
28+
return JQTransformer{Query: query}
29+
}
30+
31+
// Transform applies the jq filter transformation to the input data
32+
func (jq JQTransformer) Transform(data []byte) ([]byte, error) {
33+
return applyJQFilter(data, jq.Query)
34+
}
35+
36+
// applyJQFilter uses gojq to apply a jq transformation to the input data
37+
func applyJQFilter(jsonData []byte, jqQuery string) ([]byte, error) {
38+
var input interface{}
39+
if err := json.Unmarshal(jsonData, &input); err != nil {
40+
return nil, err
41+
}
42+
43+
query, err := gojq.Parse(jqQuery)
44+
if err != nil {
45+
return nil, err
46+
}
47+
48+
iter := query.Run(input)
49+
var results []interface{}
50+
for {
51+
v, ok := iter.Next()
52+
if !ok {
53+
break
54+
}
55+
if err, ok := v.(error); ok {
56+
return nil, err
57+
}
58+
results = append(results, v)
59+
}
60+
61+
// Convert the transformed result back to JSON []byte
62+
transformedJSON, err := json.Marshal(results)
63+
if err != nil {
64+
return nil, err
65+
}
66+
67+
return transformedJSON, nil
68+
}

transformers/transformers.go

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Copyright 2020 The Prometheus Authors
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at
5+
//
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
package transformers
15+
16+
import "fmt"
17+
18+
type Transformer interface {
19+
Transform(data []byte) ([]byte, error)
20+
}
21+
22+
type TransformationConfig struct {
23+
Type string
24+
Query string
25+
}
26+
27+
func NewTransformer(config TransformationConfig) (Transformer, error) {
28+
switch config.Type {
29+
case "jq":
30+
return NewJQTransformer(config.Query), nil
31+
default:
32+
return nil, fmt.Errorf("unsupported transformer type: %s", config.Type)
33+
}
34+
}

0 commit comments

Comments
 (0)