| package testutils |
| |
| import ( |
| "fmt" |
| "io" |
| "net/http/httptest" |
| "sort" |
| "strings" |
| |
| "github.com/prometheus/client_golang/prometheus" |
| "github.com/prometheus/client_golang/prometheus/promhttp" |
| "github.com/stretchr/testify/require" |
| |
| "go.skia.org/infra/go/sktest" |
| "go.skia.org/infra/go/util" |
| ) |
| |
| // GetRecordedMetric returns the value that prometheus is reporting. |
| // Using this allows for unit tests to check that metrics are properly |
| // being calculated and sent. This approach was found to be less awkward |
| // than using mocks and is decently performant. |
| // See datahopper/bot_metrics/bots_test.go for an example use. |
| func GetRecordedMetric(t sktest.TestingT, metricName string, tags map[string]string) string { |
| req := httptest.NewRequest("GET", "/metrics", nil) |
| rw := httptest.NewRecorder() |
| promhttp.HandlerFor(prometheus.DefaultRegisterer.(*prometheus.Registry), promhttp.HandlerOpts{ |
| ErrorLog: nil, |
| ErrorHandling: promhttp.PanicOnError, |
| DisableCompression: true, |
| }).ServeHTTP(rw, req) |
| resp := rw.Result() |
| defer util.Close(resp.Body) |
| b, err := io.ReadAll(resp.Body) |
| require.NoError(t, err) |
| // b at this point looks like: |
| // # HELP go_gc_duration_seconds A summary of the GC invocation durations. |
| // # TYPE go_gc_duration_seconds summary |
| // go_gc_duration_seconds{quantile="0"} 0 |
| // go_gc_duration_seconds{quantile="0.5"} 0 |
| // go_gc_duration_seconds{quantile="1"} 0 |
| // go_gc_duration_seconds_sum 0 |
| // # ... |
| metric := metricName + stringifyTags(tags) |
| for _, s := range strings.Split(string(b), "\n") { |
| if strings.HasPrefix(s, metric) { |
| split := strings.Split(s, " ") |
| return split[len(split)-1] |
| } |
| } |
| return "Could not find anything for " + metric |
| } |
| |
| // stringifyTags takes the given tags and returns them as would match the prometheus query |
| // format (e.g. `{key1="value1",key2="value2"}`) or "" if the map is empty. |
| func stringifyTags(tags map[string]string) string { |
| if len(tags) == 0 { |
| // Metrics w/o tags are stored on a line that look like: |
| // go_goroutines 10 |
| // So we don't want to append {} otherwise, nothing will match. |
| return "" |
| } |
| // Prometheus always puts tags/labels in order by key value |
| // https://github.com/prometheus/client_golang/blob/94ff84a9a6ebb5e6eb9172897c221a64df3443bc/prometheus/desc.go#L106 |
| // We do the same for tests |
| keys := []string{} |
| for k := range tags { |
| keys = append(keys, k) |
| } |
| sort.Strings(keys) |
| labelStrings := []string{} |
| for _, k := range keys { |
| labelStrings = append(labelStrings, fmt.Sprintf(`%s="%s"`, k, tags[k])) |
| } |
| return "{" + strings.Join(labelStrings, ",") + "}" |
| } |