diff --git a/README.md b/README.md index 75db454e..c52a62be 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,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 f00bbb77..8cbf1417 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -121,7 +121,7 @@ func probeHandler(w http.ResponseWriter, r *http.Request, logger *slog.Logger, c registry := prometheus.NewPedanticRegistry() - metrics, err := exporter.CreateMetricsList(config.Modules[module]) + metrics, err := exporter.CreateMetricsList(config.Modules[module], logger) if err != nil { logger.Error("Failed to create metrics list from config", "err", err) } diff --git a/cmd/main_test.go b/cmd/main_test.go index 9499d386..a1f3fb7c 100644 --- a/cmd/main_test.go +++ b/cmd/main_test.go @@ -470,3 +470,28 @@ func TestBodyPostQuery(t *testing.T) { target.Close() } } +func TestInvalidRegexConfig(t *testing.T) { + _, err := config.LoadConfig("../test/config/invalidConfig.yml") + if err != nil { + return + } + + target := httptest.NewServer(http.FileServer(http.Dir("../test"))) + defer target.Close() + + c, err := config.LoadConfig("../test/config/invalidConfig.yml") + if err != nil { + t.Fatalf("Failed to load config file") + } + + req := httptest.NewRequest("GET", "http://example.com/foo"+"?module=default&target="+target.URL+"/serve/good.json", nil) + recorder := httptest.NewRecorder() + logBuffer := strings.Builder{} + promslogConfig := &promslog.Config{Writer: &logBuffer} + logger := promslog.New(promslogConfig) + probeHandler(recorder, req, logger, c) + + if logBuffer.Len() == 0 { + t.Fatal("Expected error log for invalid regex, but got none") + } +} diff --git a/config/config.go b/config/config.go index b9cade47..d94dac67 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 AllowMissingKey bool `yaml:"allow_missing_key,omitempty"` } diff --git a/examples/config.yml b/examples/config.yml index b70599c1..f3e0d510 100644 --- a/examples/config.yml +++ b/examples/config.yml @@ -30,6 +30,13 @@ 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 - name: example_missing_key path: '{ .missing_key }' allow_missing_key: true 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 9fdd0c3f..35394857 100644 --- a/exporter/collector.go +++ b/exporter/collector.go @@ -17,6 +17,7 @@ import ( "bytes" "encoding/json" "log/slog" + "regexp" "time" "github.com/prometheus-community/json_exporter/config" @@ -38,6 +39,7 @@ type JSONMetric struct { LabelsJSONPaths []string ValueType prometheus.ValueType EpochTimestampJSONPath string + IncludeRegex *regexp.Regexp AllowMissingKey bool } @@ -59,7 +61,12 @@ func (mc JSONMetricCollector) Collect(ch chan<- prometheus.Metric) { if missing { continue } - + if m.IncludeRegex != nil { + if value = m.IncludeRegex.FindString(value); value == "" { + mc.Logger.Error("No matching for this pattern", "pattern", 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 ffdf5fd3..f40444a6 100644 --- a/exporter/util.go +++ b/exporter/util.go @@ -22,6 +22,7 @@ import ( "math" "net/http" "net/url" + "regexp" "strconv" "strings" "text/template" @@ -73,7 +74,7 @@ func SanitizeIntValue(s string) (int64, error) { return value, errors.New(resultErr) } -func CreateMetricsList(c config.Module) ([]JSONMetric, error) { +func CreateMetricsList(c config.Module, logger *slog.Logger) ([]JSONMetric, error) { var ( metrics []JSONMetric valueType prometheus.ValueType @@ -90,10 +91,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 { + logger.Error("Invalid regex expression", "err", err) + continue + } + } jsonMetric := JSONMetric{ Type: config.ValueScrape, Desc: prometheus.NewDesc( @@ -106,6 +115,7 @@ func CreateMetricsList(c config.Module) ([]JSONMetric, error) { LabelsJSONPaths: variableLabelsValues, ValueType: valueType, EpochTimestampJSONPath: metric.EpochTimestamp, + IncludeRegex: re, AllowMissingKey: metric.AllowMissingKey, } metrics = append(metrics, jsonMetric) diff --git a/test/config/good.yml b/test/config/good.yml index 9bde7320..8556d240 100644 --- a/test/config/good.yml +++ b/test/config/good.yml @@ -22,3 +22,9 @@ modules: active: 1 # static value count: '{.count}' # dynamic value boolean: '{.some_boolean}' + + - name: example_regex_value + path: '{.availableMemory}' + help: Example of a regex filtered value scrape in the json + valuetype: gauge + includeregex: ^\d*\.?\d* 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/good.txt b/test/response/good.txt index d9b1ca1e..149b7c2e 100644 --- a/test/response/good.txt +++ b/test/response/good.txt @@ -1,6 +1,9 @@ # HELP example_global_value Example of a top-level global value scrape in the json # TYPE example_global_value gauge example_global_value{environment="beta",location="planet-mars"} 1234 +# HELP example_regex_value Example of a regex filtered value scrape in the json +# TYPE example_regex_value gauge +example_regex_value 5 # HELP example_value_active Example of sub-level value scrapes from a json # TYPE example_value_active counter example_value_active{environment="beta",id="id-A"} 1 diff --git a/test/serve/good.json b/test/serve/good.json index 93dea69a..67d2e5c0 100644 --- a/test/serve/good.json +++ b/test/serve/good.json @@ -20,5 +20,6 @@ "state": "ACTIVE" } ], - "location": "mars" + "location": "mars", + "availableMemory": "5 MB" }