Codebase list golang-github-go-kit-kit / 7529000 metrics / multi_test.go
7529000

Tree @7529000 (Download .tar.gz)

multi_test.go @7529000raw · history · blame

package metrics_test

import (
	stdexpvar "expvar"
	"fmt"
	"io/ioutil"
	"math"
	"net/http"
	"net/http/httptest"
	"regexp"
	"strconv"
	"strings"
	"testing"

	stdprometheus "github.com/prometheus/client_golang/prometheus"

	"github.com/go-kit/kit/metrics"
	"github.com/go-kit/kit/metrics/expvar"
	"github.com/go-kit/kit/metrics/prometheus"
	"github.com/go-kit/kit/metrics/teststat"
)

func TestMultiWith(t *testing.T) {
	c := metrics.NewMultiCounter(
		"multifoo",
		expvar.NewCounter("foo"),
		prometheus.NewCounter(stdprometheus.CounterOpts{
			Namespace: "test",
			Subsystem: "multi_with",
			Name:      "bar",
			Help:      "Bar counter.",
		}, []string{"a"}),
	)

	c.Add(1)
	c.With(metrics.Field{Key: "a", Value: "1"}).Add(2)
	c.Add(3)

	if want, have := strings.Join([]string{
		`# HELP test_multi_with_bar Bar counter.`,
		`# TYPE test_multi_with_bar counter`,
		`test_multi_with_bar{a="1"} 2`,
		`test_multi_with_bar{a="unknown"} 4`,
	}, "\n"), scrapePrometheus(t); !strings.Contains(have, want) {
		t.Errorf("Prometheus metric stanza not found or incorrect\n%s", have)
	}
}

func TestMultiCounter(t *testing.T) {
	metrics.NewMultiCounter(
		"multialpha",
		expvar.NewCounter("alpha"),
		prometheus.NewCounter(stdprometheus.CounterOpts{
			Namespace: "test",
			Subsystem: "multi_counter",
			Name:      "beta",
			Help:      "Beta counter.",
		}, []string{"a"}),
	).With(metrics.Field{Key: "a", Value: "b"}).Add(123)

	if want, have := "123", stdexpvar.Get("alpha").String(); want != have {
		t.Errorf("expvar: want %q, have %q", want, have)
	}

	if want, have := strings.Join([]string{
		`# HELP test_multi_counter_beta Beta counter.`,
		`# TYPE test_multi_counter_beta counter`,
		`test_multi_counter_beta{a="b"} 123`,
	}, "\n"), scrapePrometheus(t); !strings.Contains(have, want) {
		t.Errorf("Prometheus metric stanza not found or incorrect\n%s", have)
	}
}

func TestMultiGauge(t *testing.T) {
	g := metrics.NewMultiGauge(
		"multidelta",
		expvar.NewGauge("delta"),
		prometheus.NewGauge(stdprometheus.GaugeOpts{
			Namespace: "test",
			Subsystem: "multi_gauge",
			Name:      "kappa",
			Help:      "Kappa gauge.",
		}, []string{"a"}),
	)

	f := metrics.Field{Key: "a", Value: "aaa"}
	g.With(f).Set(34)

	if want, have := "34", stdexpvar.Get("delta").String(); want != have {
		t.Errorf("expvar: want %q, have %q", want, have)
	}
	if want, have := strings.Join([]string{
		`# HELP test_multi_gauge_kappa Kappa gauge.`,
		`# TYPE test_multi_gauge_kappa gauge`,
		`test_multi_gauge_kappa{a="aaa"} 34`,
	}, "\n"), scrapePrometheus(t); !strings.Contains(have, want) {
		t.Errorf("Prometheus metric stanza not found or incorrect\n%s", have)
	}

	g.With(f).Add(-40)

	if want, have := "-6", stdexpvar.Get("delta").String(); want != have {
		t.Errorf("expvar: want %q, have %q", want, have)
	}
	if want, have := strings.Join([]string{
		`# HELP test_multi_gauge_kappa Kappa gauge.`,
		`# TYPE test_multi_gauge_kappa gauge`,
		`test_multi_gauge_kappa{a="aaa"} -6`,
	}, "\n"), scrapePrometheus(t); !strings.Contains(have, want) {
		t.Errorf("Prometheus metric stanza not found or incorrect\n%s", have)
	}
}

func TestMultiHistogram(t *testing.T) {
	quantiles := []int{50, 90, 99}
	h := metrics.NewMultiHistogram(
		"multiomicron",
		expvar.NewHistogram("omicron", 0, 100, 3, quantiles...),
		prometheus.NewSummary(stdprometheus.SummaryOpts{
			Namespace: "test",
			Subsystem: "multi_histogram",
			Name:      "nu",
			Help:      "Nu histogram.",
		}, []string{}),
	)

	const seed, mean, stdev int64 = 123, 50, 10
	teststat.PopulateNormalHistogram(t, h, seed, mean, stdev)
	assertExpvarNormalHistogram(t, "omicron", mean, stdev, quantiles)
	assertPrometheusNormalHistogram(t, `test_multi_histogram_nu`, mean, stdev)
}

func assertExpvarNormalHistogram(t *testing.T, metricName string, mean, stdev int64, quantiles []int) {
	const tolerance int = 2
	for _, quantile := range quantiles {
		want := normalValueAtQuantile(mean, stdev, quantile)
		s := stdexpvar.Get(fmt.Sprintf("%s_p%02d", metricName, quantile)).String()
		have, err := strconv.Atoi(s)
		if err != nil {
			t.Fatal(err)
		}
		if int(math.Abs(float64(want)-float64(have))) > tolerance {
			t.Errorf("quantile %d: want %d, have %d", quantile, want, have)
		}
	}
}

func assertPrometheusNormalHistogram(t *testing.T, metricName string, mean, stdev int64) {
	scrape := scrapePrometheus(t)
	const tolerance int = 5 // Prometheus approximates higher quantiles badly -_-;
	for quantileInt, quantileStr := range map[int]string{50: "0.5", 90: "0.9", 99: "0.99"} {
		want := normalValueAtQuantile(mean, stdev, quantileInt)
		have := getPrometheusQuantile(t, scrape, metricName, quantileStr)
		if int(math.Abs(float64(want)-float64(have))) > tolerance {
			t.Errorf("%q: want %d, have %d", quantileStr, want, have)
		}
	}
}

// https://en.wikipedia.org/wiki/Normal_distribution#Quantile_function
func normalValueAtQuantile(mean, stdev int64, quantile int) int64 {
	return int64(float64(mean) + float64(stdev)*math.Sqrt2*erfinv(2*(float64(quantile)/100)-1))
}

// https://stackoverflow.com/questions/5971830/need-code-for-inverse-error-function
func erfinv(y float64) float64 {
	if y < -1.0 || y > 1.0 {
		panic("invalid input")
	}

	var (
		a = [4]float64{0.886226899, -1.645349621, 0.914624893, -0.140543331}
		b = [4]float64{-2.118377725, 1.442710462, -0.329097515, 0.012229801}
		c = [4]float64{-1.970840454, -1.624906493, 3.429567803, 1.641345311}
		d = [2]float64{3.543889200, 1.637067800}
	)

	const y0 = 0.7
	var x, z float64

	if math.Abs(y) == 1.0 {
		x = -y * math.Log(0.0)
	} else if y < -y0 {
		z = math.Sqrt(-math.Log((1.0 + y) / 2.0))
		x = -(((c[3]*z+c[2])*z+c[1])*z + c[0]) / ((d[1]*z+d[0])*z + 1.0)
	} else {
		if y < y0 {
			z = y * y
			x = y * (((a[3]*z+a[2])*z+a[1])*z + a[0]) / ((((b[3]*z+b[3])*z+b[1])*z+b[0])*z + 1.0)
		} else {
			z = math.Sqrt(-math.Log((1.0 - y) / 2.0))
			x = (((c[3]*z+c[2])*z+c[1])*z + c[0]) / ((d[1]*z+d[0])*z + 1.0)
		}
		x = x - (math.Erf(x)-y)/(2.0/math.SqrtPi*math.Exp(-x*x))
		x = x - (math.Erf(x)-y)/(2.0/math.SqrtPi*math.Exp(-x*x))
	}

	return x
}

func scrapePrometheus(t *testing.T) string {
	server := httptest.NewServer(stdprometheus.UninstrumentedHandler())
	defer server.Close()

	resp, err := http.Get(server.URL)
	if err != nil {
		t.Fatal(err)
	}
	defer resp.Body.Close()

	buf, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		t.Fatal(err)
	}

	return strings.TrimSpace(string(buf))
}

func getPrometheusQuantile(t *testing.T, scrape, name, quantileStr string) int {
	re := name + `{quantile="` + quantileStr + `"} ([0-9]+)`
	matches := regexp.MustCompile(re).FindAllStringSubmatch(scrape, -1)
	if len(matches) < 1 {
		t.Fatalf("%q: quantile %q not found in scrape (%s)", name, quantileStr, re)
	}
	if len(matches[0]) < 2 {
		t.Fatalf("%q: quantile %q not found in scrape (%s)", name, quantileStr, re)
	}
	i, err := strconv.Atoi(matches[0][1])
	if err != nil {
		t.Fatal(err)
	}
	return i
}