diff --git a/README.md b/README.md index f514a85d..4af13608 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,9 @@ $ curl "http://localhost:7979/probe?module=default&target=http://localhost:8000/ # 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 +# HELP example_regex_value Example of a regex value scrapes in the json +# TYPE example_regex_value gauge +example_regex_value{environment="beta"} 1000 # HELP example_timestamped_value_count Example of a timestamped value scrape in the json # TYPE example_timestamped_value_count untyped example_timestamped_value_count{environment="beta"} 2 diff --git a/cmd/main.go b/cmd/main.go index 08a1d063..f253bafa 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -122,7 +122,7 @@ func probeHandler(w http.ResponseWriter, r *http.Request, logger log.Logger, con registry := prometheus.NewPedanticRegistry() - metrics, err := exporter.CreateMetricsList(config.Modules[module]) + metrics, err := exporter.CreateMetricsList(config.Modules[module], logger) if err != nil { level.Error(logger).Log("msg", "Failed to create metrics list from config", "err", err) } diff --git a/cmd/main_test.go b/cmd/main_test.go index 047648b3..10074ef8 100644 --- a/cmd/main_test.go +++ b/cmd/main_test.go @@ -431,3 +431,40 @@ func TestBodyPostQuery(t *testing.T) { target.Close() } } +func TestRegexResponse(t *testing.T) { + tests := []struct { + name string + ConfigFile string + ServeFile string + ResponseFile string + ShouldSucceed bool + }{ + {"case1_testCorrectResponse", "../test/config/config.yml", "/serve/correctGot.json", "../test/response/expected.txt", true}, + {"case2_testFailResponse", "../test/config/config.yml", "/serve/failGot.json", "../test/response/expected.txt", false}, + {"case3_testInvalidRegex", "../test/config/invalidConfig.yml", "/serve/correctGot.json", "../test/response/expected.txt", false}, + {"case4_testNullVaule", "../test/config/config.yml", "/serve/nullVaule.json", "../test/response/expected.txt", false}, + } + + target := httptest.NewServer(http.FileServer(http.Dir("../test"))) + defer target.Close() + + for i, test := range tests { + c, err := config.LoadConfig(test.ConfigFile) + if err != nil { + t.Fatalf("Failed to load config file %s", test.ConfigFile) + } + + req := httptest.NewRequest("GET", "http://example.com/foo"+"?module=default&target="+target.URL+test.ServeFile, nil) + recorder := httptest.NewRecorder() + probeHandler(recorder, req, log.NewNopLogger(), c) + + resp := recorder.Result() + body, _ := io.ReadAll(resp.Body) + + expected, _ := os.ReadFile(test.ResponseFile) + + if test.ShouldSucceed && cap(body) != cap(expected) { + t.Fatalf("Correct response validation test %d fails unexpectedly.\nGOT:\n%s\nEXPECTED:\n%s", i, body, expected) + } + } +} diff --git a/config/config.go b/config/config.go index 6cd0accb..8e61f079 100644 --- a/config/config.go +++ b/config/config.go @@ -30,6 +30,7 @@ type Metric struct { EpochTimestamp string Help string Values map[string]string + IncludeRegex string } type ScrapeType string diff --git a/examples/config.yml b/examples/config.yml index 9d0745c0..3cbc2f73 100644 --- a/examples/config.yml +++ b/examples/config.yml @@ -30,6 +30,14 @@ modules: active: 1 # static value count: '{.count}' # dynamic value boolean: '{.some_boolean}' + - name: example_regex_value + valuetype: gauge + help: Example of a regex value scrapes in the json + path: '{ .available_memory }' + labels: + environment: beta # static label + regex: ^\d*\.?\d* # only match digits + animals: metrics: diff --git a/examples/data.json b/examples/data.json index 2890657a..d92885d9 100644 --- a/examples/data.json +++ b/examples/data.json @@ -21,5 +21,6 @@ "state": "ACTIVE" } ], - "location": "mars" + "location": "mars", + "available_memory": "1000 MB" } diff --git a/exporter/collector.go b/exporter/collector.go index 4effc10f..8a0eb7d1 100644 --- a/exporter/collector.go +++ b/exporter/collector.go @@ -16,6 +16,8 @@ package exporter import ( "bytes" "encoding/json" + "fmt" + "regexp" "time" "github.com/go-kit/log" @@ -39,6 +41,7 @@ type JSONMetric struct { LabelsJSONPaths []string ValueType prometheus.ValueType EpochTimestampJSONPath string + IncludeRegex *regexp.Regexp } func (mc JSONMetricCollector) Describe(ch chan<- *prometheus.Desc) { @@ -56,7 +59,12 @@ func (mc JSONMetricCollector) Collect(ch chan<- prometheus.Metric) { level.Error(mc.Logger).Log("msg", "Failed to extract value for metric", "path", m.KeyJSONPath, "err", err, "metric", m.Desc) continue } - + if m.IncludeRegex != nil { + if value = m.IncludeRegex.FindString(value); value == "" { + level.Error(mc.Logger).Log("msg", fmt.Sprintf("No matching for this pattern '%s'", m.IncludeRegex.String())) + continue + } + } if floatValue, err := SanitizeValue(value); err == nil { metric := prometheus.MustNewConstMetric( m.Desc, diff --git a/exporter/util.go b/exporter/util.go index 8374ddce..2c2b132e 100644 --- a/exporter/util.go +++ b/exporter/util.go @@ -21,6 +21,7 @@ import ( "math" "net/http" "net/url" + "regexp" "strconv" "strings" "text/template" @@ -74,7 +75,7 @@ func SanitizeIntValue(s string) (int64, error) { return value, fmt.Errorf(resultErr) } -func CreateMetricsList(c config.Module) ([]JSONMetric, error) { +func CreateMetricsList(c config.Module, logger log.Logger) ([]JSONMetric, error) { var ( metrics []JSONMetric valueType prometheus.ValueType @@ -91,10 +92,18 @@ func CreateMetricsList(c config.Module) ([]JSONMetric, error) { switch metric.Type { case config.ValueScrape: var variableLabels, variableLabelsValues []string + var err error + var re *regexp.Regexp for k, v := range metric.Labels { variableLabels = append(variableLabels, k) variableLabelsValues = append(variableLabelsValues, v) } + if metric.IncludeRegex != "" { + if re, err = regexp.Compile(metric.IncludeRegex); err != nil { + level.Error(logger).Log("msg", "invalid regex expression", "err", err) + continue + } + } jsonMetric := JSONMetric{ Type: config.ValueScrape, Desc: prometheus.NewDesc( @@ -107,6 +116,7 @@ func CreateMetricsList(c config.Module) ([]JSONMetric, error) { LabelsJSONPaths: variableLabelsValues, ValueType: valueType, EpochTimestampJSONPath: metric.EpochTimestamp, + IncludeRegex: re, } metrics = append(metrics, jsonMetric) case config.ObjectScrape: diff --git a/test/config/config.yml b/test/config/config.yml new file mode 100644 index 00000000..9bdc396c --- /dev/null +++ b/test/config/config.yml @@ -0,0 +1,9 @@ +--- +modules: + default: + metrics: + - name: available_memory + path: '{.availableMemory}' + help: Available memory in MB + valuetype: gauge + includeregex: ^\d*\.?\d* # only match digits \ No newline at end of file diff --git a/test/config/invalidConfig.yml b/test/config/invalidConfig.yml new file mode 100644 index 00000000..27d1a49f --- /dev/null +++ b/test/config/invalidConfig.yml @@ -0,0 +1,9 @@ +--- +modules: + default: + metrics: + - name: available_memory + path: '{.availableMemory}' + help: Available memory in MB + valuetype: gauge + includeregex: ^\d*\.?\d*) # Invalid regular expression, parentheses do not match \ No newline at end of file diff --git a/test/response/expected.txt b/test/response/expected.txt new file mode 100644 index 00000000..7c9c18bf --- /dev/null +++ b/test/response/expected.txt @@ -0,0 +1,3 @@ +# HELP available_memory Available memory in MB +# TYPE available_memory gauge +available_memory 5 \ No newline at end of file diff --git a/test/serve/correctGot.json b/test/serve/correctGot.json new file mode 100644 index 00000000..8e2351c9 --- /dev/null +++ b/test/serve/correctGot.json @@ -0,0 +1,3 @@ +{ + "availableMemory": "5 MB" +} \ No newline at end of file diff --git a/test/serve/failGot.json b/test/serve/failGot.json new file mode 100644 index 00000000..9de3579d --- /dev/null +++ b/test/serve/failGot.json @@ -0,0 +1,5 @@ +{ + "availableMemory": "1000 MB", + "availableMemory": " 50 GB", + "availableMemory": "1.333285ms" +} \ No newline at end of file diff --git a/test/serve/nullVaule.json b/test/serve/nullVaule.json new file mode 100644 index 00000000..4121edb5 --- /dev/null +++ b/test/serve/nullVaule.json @@ -0,0 +1,3 @@ +{ + "availableMemory": "" +} \ No newline at end of file