From 118b48fba0ed8578a655b53561cfd2d32d5300aa Mon Sep 17 00:00:00 2001 From: William Banfield Date: Tue, 10 May 2022 18:14:26 -0400 Subject: [PATCH] scripts/metricsgen: add metricsdiff tool --- scripts/metricsgen/metricsdiff/metricsdiff.go | 156 ++++++++++++++++++ .../metricsdiff/metricsdiff_test.go | 64 +++++++ 2 files changed, 220 insertions(+) create mode 100644 scripts/metricsgen/metricsdiff/metricsdiff.go create mode 100644 scripts/metricsgen/metricsdiff/metricsdiff_test.go diff --git a/scripts/metricsgen/metricsdiff/metricsdiff.go b/scripts/metricsgen/metricsdiff/metricsdiff.go new file mode 100644 index 000000000..e3a059014 --- /dev/null +++ b/scripts/metricsgen/metricsdiff/metricsdiff.go @@ -0,0 +1,156 @@ +// metricsdiff is a tool for generating a diff between two different files containing +// prometheus metrics. metricsdiff outputs which metrics have been added, removed, +// or have different sets of labels between the two files. +package main + +import ( + "flag" + "fmt" + "io" + "log" + "os" + "path/filepath" + + dto "github.com/prometheus/client_model/go" + "github.com/prometheus/common/expfmt" +) + +func init() { + flag.Usage = func() { + fmt.Fprintf(os.Stderr, `Usage: %[1]s + +Generate the diff between the sets of prometheus metrics peresent in the two files +in the files. The tool outputs which metrics have been added, removed, or +use different label sets between the two files. + +`, filepath.Base(os.Args[0])) + flag.PrintDefaults() + } +} + +// MetricsDiff contains the set of metrics that were modified between two files +// containing prometheus metrics output. +type MetricsDiff struct { + Adds []string + Removes []string + + LabelDiffs []LabelDiff +} + +// MetricsDiff contains the set of labels that were modified for a single metric +// between two files containing prometheus metrics output. +type LabelDiff struct { + MetricsName string + Adds []string + Removes []string +} + +func main() { + flag.Parse() + if len(os.Args) != 3 { + log.Fatalf("Incorrect number of arguments, expected 2 but received %d", len(os.Args)-1) + } + fa, err := os.Open(os.Args[1]) + if err != nil { + log.Fatalf("Unable to open file %s: %v", os.Args[1], err) + } + fb, err := os.Open(os.Args[2]) + if err != nil { + log.Fatalf("Unable to open file %s: %v", os.Args[2], err) + } + md, err := MetricsDiffFromReaders(fa, fb) + if err != nil { + log.Fatalf("Generating diff: %v", err) + } + fmt.Printf("%s", md) +} + +// MetricsDiffFromReaders parses the metrics present in the readers a and b and +// determines which metrics were added and removed in b. +func MetricsDiffFromReaders(a, b io.Reader) (MetricsDiff, error) { + parser := expfmt.TextParser{} + amf, err := parser.TextToMetricFamilies(a) + if err != nil { + return MetricsDiff{}, err + } + bmf, err := parser.TextToMetricFamilies(b) + if err != nil { + return MetricsDiff{}, err + } + + md := MetricsDiff{} + for name, afamily := range amf { + bfamily, ok := bmf[name] + if !ok { + md.Removes = append(md.Removes, name) + continue + } + + labelsDiff := false + aLabelSet := toSet(afamily.Metric[0].Label) + bLabelSet := toSet(bfamily.Metric[0].Label) + ld := LabelDiff{ + MetricsName: name, + } + for name := range aLabelSet { + _, ok := bLabelSet[name] + if !ok { + ld.Removes = append(ld.Removes, name) + labelsDiff = true + } + } + for name := range bLabelSet { + _, ok := aLabelSet[name] + if !ok { + ld.Adds = append(ld.Adds, name) + labelsDiff = true + } + } + if labelsDiff { + md.LabelDiffs = append(md.LabelDiffs, ld) + } + } + for name := range bmf { + if _, ok := amf[name]; !ok { + md.Adds = append(md.Adds, name) + } + } + return md, nil +} + +func toSet(lps []*dto.LabelPair) map[string]struct{} { + m := make(map[string]struct{}, len(lps)) + for _, pair := range lps { + m[*pair.Name] = struct{}{} + } + return m +} + +func (m MetricsDiff) String() string { + var s string + if len(m.Adds) > 0 { + s += "Adds: \n" + for _, add := range m.Adds { + s += fmt.Sprintf("+++ %s\n", add) + } + } + if len(m.Removes) > 0 { + s += "Removes: \n" + for _, remove := range m.Removes { + s += fmt.Sprintf("--- %s\n", remove) + } + } + if len(m.LabelDiffs) > 0 { + s += "Label Changes: \n" + for _, ld := range m.LabelDiffs { + s += fmt.Sprintf("Label: %s\n", ld.MetricsName) + for _, add := range ld.Adds { + s += fmt.Sprintf("+++ %s", add) + } + for _, remove := range ld.Removes { + s += fmt.Sprintf("--- %s", remove) + } + } + } + return s +} diff --git a/scripts/metricsgen/metricsdiff/metricsdiff_test.go b/scripts/metricsgen/metricsdiff/metricsdiff_test.go new file mode 100644 index 000000000..0bd6d045e --- /dev/null +++ b/scripts/metricsgen/metricsdiff/metricsdiff_test.go @@ -0,0 +1,64 @@ +package main_test + +import ( + "bytes" + "io" + "testing" + + "github.com/stretchr/testify/require" + metricsdiff "github.com/tendermint/tendermint/scripts/metricsgen/metricsdiff" +) + +func TestDiff(t *testing.T) { + for _, tc := range []struct { + name string + aContents string + bContents string + + expectedDiff metricsdiff.MetricsDiff + }{ + { + name: "labels", + aContents: ` + metric_one{label_one="content", label_two="content"} 0 + `, + bContents: ` + metric_one{label_three="content", label_four="content"} 0 + `, + expectedDiff: metricsdiff.MetricsDiff{ + LabelDiffs: []metricsdiff.LabelDiff{ + { + MetricsName: "metric_one", + Adds: []string{"label_three", "label_four"}, + Removes: []string{"label_one", "label_two"}, + }, + }, + }, + }, + { + name: "metrics", + aContents: ` + metric_one{label_one="content"} 0 + `, + bContents: ` + metric_two{label_two="content"} 0 + `, + expectedDiff: metricsdiff.MetricsDiff{ + Adds: []string{"metric_two"}, + Removes: []string{"metric_one"}, + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + bufA := bytes.NewBuffer([]byte{}) + bufB := bytes.NewBuffer([]byte{}) + _, err := io.WriteString(bufA, tc.aContents) + require.NoError(t, err) + _, err = io.WriteString(bufB, tc.bContents) + require.NoError(t, err) + md, err := metricsdiff.MetricsDiffFromReaders(bufA, bufB) + require.NoError(t, err) + require.Equal(t, tc.expectedDiff, md) + }) + } +}