Fix metrics test structure
Peter Bourgon
9 years ago
0 | 0 | package expvar_test |
1 | 1 | |
2 | 2 | import ( |
3 | stdexpvar "expvar" | |
4 | "fmt" | |
5 | "math" | |
6 | "math/rand" | |
7 | "strconv" | |
8 | 3 | "testing" |
9 | 4 | |
10 | "github.com/peterbourgon/gokit/metrics" | |
11 | 5 | "github.com/peterbourgon/gokit/metrics/expvar" |
6 | "github.com/peterbourgon/gokit/metrics/teststat" | |
12 | 7 | ) |
13 | 8 | |
14 | 9 | func TestHistogramQuantiles(t *testing.T) { |
17 | 12 | h := expvar.NewHistogram(metricName, 0, 100, 3, quantiles...) |
18 | 13 | |
19 | 14 | const seed, mean, stdev int64 = 424242, 50, 10 |
20 | populateNormalHistogram(t, h, seed, mean, stdev) | |
21 | assertNormalHistogram(t, metricName, mean, stdev, quantiles) | |
15 | teststat.PopulateNormalHistogram(t, h, seed, mean, stdev) | |
16 | teststat.AssertExpvarNormalHistogram(t, metricName, mean, stdev, quantiles) | |
22 | 17 | } |
23 | ||
24 | func populateNormalHistogram(t *testing.T, h metrics.Histogram, seed int64, mean, stdev int64) { | |
25 | rand.Seed(seed) | |
26 | for i := 0; i < 1234; i++ { | |
27 | sample := int64(rand.NormFloat64()*float64(stdev) + float64(mean)) | |
28 | h.Observe(sample) | |
29 | } | |
30 | } | |
31 | ||
32 | func assertNormalHistogram(t *testing.T, metricName string, mean, stdev int64, quantiles []int) { | |
33 | const tolerance int = 2 | |
34 | for _, quantile := range quantiles { | |
35 | want := normalValueAtQuantile(mean, stdev, quantile) | |
36 | s := stdexpvar.Get(fmt.Sprintf("%s_p%02d", metricName, quantile)).String() | |
37 | have, err := strconv.Atoi(s) | |
38 | if err != nil { | |
39 | t.Fatal(err) | |
40 | } | |
41 | if int(math.Abs(float64(want)-float64(have))) > tolerance { | |
42 | t.Errorf("quantile %d: want %d, have %d", quantile, want, have) | |
43 | } | |
44 | } | |
45 | } | |
46 | ||
47 | // https://en.wikipedia.org/wiki/Normal_distribution#Quantile_function | |
48 | func normalValueAtQuantile(mean, stdev int64, quantile int) int64 { | |
49 | return int64(float64(mean) + float64(stdev)*math.Sqrt2*erfinv(2*(float64(quantile)/100)-1)) | |
50 | } | |
51 | ||
52 | // https://stackoverflow.com/questions/5971830/need-code-for-inverse-error-function | |
53 | func erfinv(y float64) float64 { | |
54 | if y < -1.0 || y > 1.0 { | |
55 | panic("invalid input") | |
56 | } | |
57 | ||
58 | var ( | |
59 | a = [4]float64{0.886226899, -1.645349621, 0.914624893, -0.140543331} | |
60 | b = [4]float64{-2.118377725, 1.442710462, -0.329097515, 0.012229801} | |
61 | c = [4]float64{-1.970840454, -1.624906493, 3.429567803, 1.641345311} | |
62 | d = [2]float64{3.543889200, 1.637067800} | |
63 | ) | |
64 | ||
65 | const y0 = 0.7 | |
66 | var x, z float64 | |
67 | ||
68 | if math.Abs(y) == 1.0 { | |
69 | x = -y * math.Log(0.0) | |
70 | } else if y < -y0 { | |
71 | z = math.Sqrt(-math.Log((1.0 + y) / 2.0)) | |
72 | x = -(((c[3]*z+c[2])*z+c[1])*z + c[0]) / ((d[1]*z+d[0])*z + 1.0) | |
73 | } else { | |
74 | if y < y0 { | |
75 | z = y * y | |
76 | 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) | |
77 | } else { | |
78 | z = math.Sqrt(-math.Log((1.0 - y) / 2.0)) | |
79 | x = (((c[3]*z+c[2])*z+c[1])*z + c[0]) / ((d[1]*z+d[0])*z + 1.0) | |
80 | } | |
81 | x = x - (math.Erf(x)-y)/(2.0/math.SqrtPi*math.Exp(-x*x)) | |
82 | x = x - (math.Erf(x)-y)/(2.0/math.SqrtPi*math.Exp(-x*x)) | |
83 | } | |
84 | ||
85 | return x | |
86 | } |
0 | 0 | package metrics_test |
1 | 1 | |
2 | 2 | import ( |
3 | "expvar" | |
3 | stdexpvar "expvar" | |
4 | "fmt" | |
5 | "io/ioutil" | |
6 | "math" | |
7 | "math/rand" | |
8 | "net/http" | |
9 | "net/http/httptest" | |
10 | "regexp" | |
11 | "strconv" | |
4 | 12 | "strings" |
5 | 13 | "testing" |
6 | 14 | |
15 | stdprometheus "github.com/prometheus/client_golang/prometheus" | |
16 | ||
7 | 17 | "github.com/peterbourgon/gokit/metrics" |
18 | "github.com/peterbourgon/gokit/metrics/expvar" | |
19 | "github.com/peterbourgon/gokit/metrics/prometheus" | |
8 | 20 | ) |
9 | 21 | |
10 | 22 | func TestMultiWith(t *testing.T) { |
11 | 23 | c := metrics.NewMultiCounter( |
12 | metrics.NewExpvarCounter("foo"), | |
13 | metrics.NewPrometheusCounter("test", "multi_with", "bar", "Bar counter.", []string{"a"}), | |
24 | expvar.NewCounter("foo"), | |
25 | prometheus.NewCounter("test", "multi_with", "bar", "Bar counter.", []string{"a"}), | |
14 | 26 | ) |
15 | 27 | |
16 | 28 | c.Add(1) |
29 | 41 | |
30 | 42 | func TestMultiCounter(t *testing.T) { |
31 | 43 | metrics.NewMultiCounter( |
32 | metrics.NewExpvarCounter("alpha"), | |
33 | metrics.NewPrometheusCounter("test", "multi_counter", "beta", "Beta counter.", []string{}), | |
44 | expvar.NewCounter("alpha"), | |
45 | prometheus.NewCounter("test", "multi_counter", "beta", "Beta counter.", []string{}), | |
34 | 46 | ).Add(123) |
35 | 47 | |
36 | if want, have := "123", expvar.Get("alpha").String(); want != have { | |
48 | if want, have := "123", stdexpvar.Get("alpha").String(); want != have { | |
37 | 49 | t.Errorf("expvar: want %q, have %q", want, have) |
38 | 50 | } |
39 | 51 | |
48 | 60 | |
49 | 61 | func TestMultiGauge(t *testing.T) { |
50 | 62 | g := metrics.NewMultiGauge( |
51 | metrics.NewExpvarGauge("delta"), | |
52 | metrics.NewPrometheusGauge("test", "multi_gauge", "kappa", "Kappa gauge.", []string{}), | |
63 | expvar.NewGauge("delta"), | |
64 | prometheus.NewGauge("test", "multi_gauge", "kappa", "Kappa gauge.", []string{}), | |
53 | 65 | ) |
54 | 66 | |
55 | 67 | g.Set(34) |
56 | 68 | |
57 | if want, have := "34", expvar.Get("delta").String(); want != have { | |
69 | if want, have := "34", stdexpvar.Get("delta").String(); want != have { | |
58 | 70 | t.Errorf("expvar: want %q, have %q", want, have) |
59 | 71 | } |
60 | 72 | if want, have := strings.Join([]string{ |
67 | 79 | |
68 | 80 | g.Add(-40) |
69 | 81 | |
70 | if want, have := "-6", expvar.Get("delta").String(); want != have { | |
82 | if want, have := "-6", stdexpvar.Get("delta").String(); want != have { | |
71 | 83 | t.Errorf("expvar: want %q, have %q", want, have) |
72 | 84 | } |
73 | 85 | if want, have := strings.Join([]string{ |
82 | 94 | func TestMultiHistogram(t *testing.T) { |
83 | 95 | quantiles := []int{50, 90, 99} |
84 | 96 | h := metrics.NewMultiHistogram( |
85 | metrics.NewExpvarHistogram("omicron", 0, 100, 3, quantiles...), | |
86 | metrics.NewPrometheusHistogram("test", "multi_histogram", "nu", "Nu histogram.", []string{}), | |
97 | expvar.NewHistogram("omicron", 0, 100, 3, quantiles...), | |
98 | prometheus.NewHistogram("test", "multi_histogram", "nu", "Nu histogram.", []string{}), | |
87 | 99 | ) |
88 | 100 | |
89 | 101 | const seed, mean, stdev int64 = 123, 50, 10 |
91 | 103 | assertExpvarNormalHistogram(t, "omicron", mean, stdev, quantiles) |
92 | 104 | assertPrometheusNormalHistogram(t, "test_multi_histogram_nu", mean, stdev) |
93 | 105 | } |
106 | ||
107 | func populateNormalHistogram(t *testing.T, h metrics.Histogram, seed int64, mean, stdev int64) { | |
108 | rand.Seed(seed) | |
109 | for i := 0; i < 1234; i++ { | |
110 | sample := int64(rand.NormFloat64()*float64(stdev) + float64(mean)) | |
111 | h.Observe(sample) | |
112 | } | |
113 | } | |
114 | ||
115 | func assertExpvarNormalHistogram(t *testing.T, metricName string, mean, stdev int64, quantiles []int) { | |
116 | const tolerance int = 2 | |
117 | for _, quantile := range quantiles { | |
118 | want := normalValueAtQuantile(mean, stdev, quantile) | |
119 | s := stdexpvar.Get(fmt.Sprintf("%s_p%02d", metricName, quantile)).String() | |
120 | have, err := strconv.Atoi(s) | |
121 | if err != nil { | |
122 | t.Fatal(err) | |
123 | } | |
124 | if int(math.Abs(float64(want)-float64(have))) > tolerance { | |
125 | t.Errorf("quantile %d: want %d, have %d", quantile, want, have) | |
126 | } | |
127 | } | |
128 | } | |
129 | ||
130 | func assertPrometheusNormalHistogram(t *testing.T, metricName string, mean, stdev int64) { | |
131 | scrape := scrapePrometheus(t) | |
132 | const tolerance int = 5 // Prometheus approximates higher quantiles badly -_-; | |
133 | for quantileInt, quantileStr := range map[int]string{50: "0.5", 90: "0.9", 99: "0.99"} { | |
134 | want := normalValueAtQuantile(mean, stdev, quantileInt) | |
135 | have := getPrometheusQuantile(t, scrape, metricName, quantileStr) | |
136 | if int(math.Abs(float64(want)-float64(have))) > tolerance { | |
137 | t.Errorf("%q: want %d, have %d", quantileStr, want, have) | |
138 | } | |
139 | } | |
140 | } | |
141 | ||
142 | // https://en.wikipedia.org/wiki/Normal_distribution#Quantile_function | |
143 | func normalValueAtQuantile(mean, stdev int64, quantile int) int64 { | |
144 | return int64(float64(mean) + float64(stdev)*math.Sqrt2*erfinv(2*(float64(quantile)/100)-1)) | |
145 | } | |
146 | ||
147 | // https://stackoverflow.com/questions/5971830/need-code-for-inverse-error-function | |
148 | func erfinv(y float64) float64 { | |
149 | if y < -1.0 || y > 1.0 { | |
150 | panic("invalid input") | |
151 | } | |
152 | ||
153 | var ( | |
154 | a = [4]float64{0.886226899, -1.645349621, 0.914624893, -0.140543331} | |
155 | b = [4]float64{-2.118377725, 1.442710462, -0.329097515, 0.012229801} | |
156 | c = [4]float64{-1.970840454, -1.624906493, 3.429567803, 1.641345311} | |
157 | d = [2]float64{3.543889200, 1.637067800} | |
158 | ) | |
159 | ||
160 | const y0 = 0.7 | |
161 | var x, z float64 | |
162 | ||
163 | if math.Abs(y) == 1.0 { | |
164 | x = -y * math.Log(0.0) | |
165 | } else if y < -y0 { | |
166 | z = math.Sqrt(-math.Log((1.0 + y) / 2.0)) | |
167 | x = -(((c[3]*z+c[2])*z+c[1])*z + c[0]) / ((d[1]*z+d[0])*z + 1.0) | |
168 | } else { | |
169 | if y < y0 { | |
170 | z = y * y | |
171 | 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) | |
172 | } else { | |
173 | z = math.Sqrt(-math.Log((1.0 - y) / 2.0)) | |
174 | x = (((c[3]*z+c[2])*z+c[1])*z + c[0]) / ((d[1]*z+d[0])*z + 1.0) | |
175 | } | |
176 | x = x - (math.Erf(x)-y)/(2.0/math.SqrtPi*math.Exp(-x*x)) | |
177 | x = x - (math.Erf(x)-y)/(2.0/math.SqrtPi*math.Exp(-x*x)) | |
178 | } | |
179 | ||
180 | return x | |
181 | } | |
182 | ||
183 | func scrapePrometheus(t *testing.T) string { | |
184 | server := httptest.NewServer(stdprometheus.UninstrumentedHandler()) | |
185 | defer server.Close() | |
186 | ||
187 | resp, err := http.Get(server.URL) | |
188 | if err != nil { | |
189 | t.Fatal(err) | |
190 | } | |
191 | defer resp.Body.Close() | |
192 | ||
193 | buf, err := ioutil.ReadAll(resp.Body) | |
194 | if err != nil { | |
195 | t.Fatal(err) | |
196 | } | |
197 | ||
198 | return strings.TrimSpace(string(buf)) | |
199 | } | |
200 | ||
201 | func getPrometheusQuantile(t *testing.T, scrape, name, quantileStr string) int { | |
202 | matches := regexp.MustCompile(name+`{quantile="`+quantileStr+`"} ([0-9]+)`).FindAllStringSubmatch(scrape, -1) | |
203 | if len(matches) < 1 { | |
204 | t.Fatalf("%q: quantile %q not found in scrape", name, quantileStr) | |
205 | } | |
206 | if len(matches[0]) < 2 { | |
207 | t.Fatalf("%q: quantile %q not found in scrape", name, quantileStr) | |
208 | } | |
209 | i, err := strconv.Atoi(matches[0][1]) | |
210 | if err != nil { | |
211 | t.Fatal(err) | |
212 | } | |
213 | return i | |
214 | } |
0 | 0 | package prometheus_test |
1 | 1 | |
2 | 2 | import ( |
3 | "io/ioutil" | |
4 | "math" | |
5 | "math/rand" | |
6 | "net/http" | |
7 | "net/http/httptest" | |
8 | "regexp" | |
9 | "strconv" | |
10 | 3 | "strings" |
11 | 4 | "testing" |
12 | 5 | |
13 | stdprometheus "github.com/prometheus/client_golang/prometheus" | |
14 | ||
15 | 6 | "github.com/peterbourgon/gokit/metrics" |
16 | 7 | "github.com/peterbourgon/gokit/metrics/prometheus" |
8 | "github.com/peterbourgon/gokit/metrics/teststat" | |
17 | 9 | ) |
18 | 10 | |
19 | 11 | func TestPrometheusLabelBehavior(t *testing.T) { |
26 | 18 | `# TYPE test_prometheus_label_behavior_foobar counter`, |
27 | 19 | `test_prometheus_label_behavior_foobar{unused_key="unknown",used_key="declared"} 1`, |
28 | 20 | `test_prometheus_label_behavior_foobar{unused_key="unknown",used_key="unknown"} 1`, |
29 | }, "\n"), scrapePrometheus(t); !strings.Contains(have, want) { | |
21 | }, "\n"), teststat.ScrapePrometheus(t); !strings.Contains(have, want) { | |
30 | 22 | t.Errorf("metric stanza not found or incorrect\n%s", have) |
31 | 23 | } |
32 | 24 | } |
39 | 31 | `# HELP test_prometheus_counter_foobar Lorem ipsum.`, |
40 | 32 | `# TYPE test_prometheus_counter_foobar counter`, |
41 | 33 | `test_prometheus_counter_foobar 3`, |
42 | }, "\n"), scrapePrometheus(t); !strings.Contains(have, want) { | |
34 | }, "\n"), teststat.ScrapePrometheus(t); !strings.Contains(have, want) { | |
43 | 35 | t.Errorf("metric stanza not found or incorrect\n%s", have) |
44 | 36 | } |
45 | 37 | c.Add(3) |
48 | 40 | `# HELP test_prometheus_counter_foobar Lorem ipsum.`, |
49 | 41 | `# TYPE test_prometheus_counter_foobar counter`, |
50 | 42 | `test_prometheus_counter_foobar 10`, |
51 | }, "\n"), scrapePrometheus(t); !strings.Contains(have, want) { | |
43 | }, "\n"), teststat.ScrapePrometheus(t); !strings.Contains(have, want) { | |
52 | 44 | t.Errorf("metric stanza not found or incorrect\n%s", have) |
53 | 45 | } |
54 | 46 | } |
60 | 52 | `# HELP test_prometheus_gauge_foobar Dolor sit.`, |
61 | 53 | `# TYPE test_prometheus_gauge_foobar gauge`, |
62 | 54 | `test_prometheus_gauge_foobar 42`, |
63 | }, "\n"), scrapePrometheus(t); !strings.Contains(have, want) { | |
55 | }, "\n"), teststat.ScrapePrometheus(t); !strings.Contains(have, want) { | |
64 | 56 | t.Errorf("metric stanza not found or incorrect\n%s", have) |
65 | 57 | } |
66 | 58 | c.Add(-43) |
68 | 60 | `# HELP test_prometheus_gauge_foobar Dolor sit.`, |
69 | 61 | `# TYPE test_prometheus_gauge_foobar gauge`, |
70 | 62 | `test_prometheus_gauge_foobar -1`, |
71 | }, "\n"), scrapePrometheus(t); !strings.Contains(have, want) { | |
63 | }, "\n"), teststat.ScrapePrometheus(t); !strings.Contains(have, want) { | |
72 | 64 | t.Errorf("metric stanza not found or incorrect\n%s", have) |
73 | 65 | } |
74 | 66 | } |
77 | 69 | h := prometheus.NewHistogram("test", "prometheus_histogram", "foobar", "Qwerty asdf.", []string{}) |
78 | 70 | |
79 | 71 | const mean, stdev int64 = 50, 10 |
80 | populateNormalHistogram(t, h, 34, mean, stdev) | |
81 | assertNormalHistogram(t, "test_prometheus_histogram_foobar", mean, stdev) | |
72 | teststat.PopulateNormalHistogram(t, h, 34, mean, stdev) | |
73 | teststat.AssertPrometheusNormalHistogram(t, "test_prometheus_histogram_foobar", mean, stdev) | |
82 | 74 | } |
83 | ||
84 | func populateNormalHistogram(t *testing.T, h metrics.Histogram, seed int64, mean, stdev int64) { | |
85 | rand.Seed(seed) | |
86 | for i := 0; i < 1234; i++ { | |
87 | sample := int64(rand.NormFloat64()*float64(stdev) + float64(mean)) | |
88 | h.Observe(sample) | |
89 | } | |
90 | } | |
91 | ||
92 | func assertNormalHistogram(t *testing.T, metricName string, mean, stdev int64) { | |
93 | scrape := scrapePrometheus(t) | |
94 | const tolerance int = 5 // Prometheus approximates higher quantiles badly -_-; | |
95 | for quantileInt, quantileStr := range map[int]string{50: "0.5", 90: "0.9", 99: "0.99"} { | |
96 | want := normalValueAtQuantile(mean, stdev, quantileInt) | |
97 | have := getPrometheusQuantile(t, scrape, metricName, quantileStr) | |
98 | if int(math.Abs(float64(want)-float64(have))) > tolerance { | |
99 | t.Errorf("%q: want %d, have %d", quantileStr, want, have) | |
100 | } | |
101 | } | |
102 | } | |
103 | ||
104 | // https://en.wikipedia.org/wiki/Normal_distribution#Quantile_function | |
105 | func normalValueAtQuantile(mean, stdev int64, quantile int) int64 { | |
106 | return int64(float64(mean) + float64(stdev)*math.Sqrt2*erfinv(2*(float64(quantile)/100)-1)) | |
107 | } | |
108 | ||
109 | // https://stackoverflow.com/questions/5971830/need-code-for-inverse-error-function | |
110 | func erfinv(y float64) float64 { | |
111 | if y < -1.0 || y > 1.0 { | |
112 | panic("invalid input") | |
113 | } | |
114 | ||
115 | var ( | |
116 | a = [4]float64{0.886226899, -1.645349621, 0.914624893, -0.140543331} | |
117 | b = [4]float64{-2.118377725, 1.442710462, -0.329097515, 0.012229801} | |
118 | c = [4]float64{-1.970840454, -1.624906493, 3.429567803, 1.641345311} | |
119 | d = [2]float64{3.543889200, 1.637067800} | |
120 | ) | |
121 | ||
122 | const y0 = 0.7 | |
123 | var x, z float64 | |
124 | ||
125 | if math.Abs(y) == 1.0 { | |
126 | x = -y * math.Log(0.0) | |
127 | } else if y < -y0 { | |
128 | z = math.Sqrt(-math.Log((1.0 + y) / 2.0)) | |
129 | x = -(((c[3]*z+c[2])*z+c[1])*z + c[0]) / ((d[1]*z+d[0])*z + 1.0) | |
130 | } else { | |
131 | if y < y0 { | |
132 | z = y * y | |
133 | 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) | |
134 | } else { | |
135 | z = math.Sqrt(-math.Log((1.0 - y) / 2.0)) | |
136 | x = (((c[3]*z+c[2])*z+c[1])*z + c[0]) / ((d[1]*z+d[0])*z + 1.0) | |
137 | } | |
138 | x = x - (math.Erf(x)-y)/(2.0/math.SqrtPi*math.Exp(-x*x)) | |
139 | x = x - (math.Erf(x)-y)/(2.0/math.SqrtPi*math.Exp(-x*x)) | |
140 | } | |
141 | ||
142 | return x | |
143 | } | |
144 | ||
145 | func scrapePrometheus(t *testing.T) string { | |
146 | server := httptest.NewServer(stdprometheus.UninstrumentedHandler()) | |
147 | defer server.Close() | |
148 | ||
149 | resp, err := http.Get(server.URL) | |
150 | if err != nil { | |
151 | t.Fatal(err) | |
152 | } | |
153 | defer resp.Body.Close() | |
154 | ||
155 | buf, err := ioutil.ReadAll(resp.Body) | |
156 | if err != nil { | |
157 | t.Fatal(err) | |
158 | } | |
159 | ||
160 | return strings.TrimSpace(string(buf)) | |
161 | } | |
162 | ||
163 | func getPrometheusQuantile(t *testing.T, scrape, name, quantileStr string) int { | |
164 | matches := regexp.MustCompile(name+`{quantile="`+quantileStr+`"} ([0-9]+)`).FindAllStringSubmatch(scrape, -1) | |
165 | if len(matches) < 1 { | |
166 | t.Fatalf("%q: quantile %q not found in scrape", name, quantileStr) | |
167 | } | |
168 | if len(matches[0]) < 2 { | |
169 | t.Fatalf("%q: quantile %q not found in scrape", name, quantileStr) | |
170 | } | |
171 | i, err := strconv.Atoi(matches[0][1]) | |
172 | if err != nil { | |
173 | t.Fatal(err) | |
174 | } | |
175 | return i | |
176 | } |
3 | 3 | "testing" |
4 | 4 | |
5 | 5 | "github.com/peterbourgon/gokit/metrics" |
6 | "github.com/peterbourgon/gokit/metrics/expvar" | |
6 | 7 | ) |
7 | 8 | |
8 | 9 | func TestScaledHistogram(t *testing.T) { |
11 | 12 | metricName := "test_scaled_histogram" |
12 | 13 | |
13 | 14 | var h metrics.Histogram |
14 | h = metrics.NewExpvarHistogram(metricName, 0, 1000, 3, quantiles...) | |
15 | h = expvar.NewHistogram(metricName, 0, 1000, 3, quantiles...) | |
15 | 16 | h = metrics.NewScaledHistogram(h, scale) |
16 | 17 | |
17 | 18 | const seed, mean, stdev = 333, 500, 100 // input values |
0 | // Package teststat contains helper functions for statistical testing of | |
1 | // metrics implementations. | |
2 | package teststat | |
3 | ||
4 | import ( | |
5 | "math" | |
6 | "math/rand" | |
7 | "testing" | |
8 | ||
9 | "github.com/peterbourgon/gokit/metrics" | |
10 | ) | |
11 | ||
12 | // PopulateNormalHistogram populates the Histogram with a normal distribution | |
13 | // of observations. | |
14 | func PopulateNormalHistogram(t *testing.T, h metrics.Histogram, seed int64, mean, stdev int64) { | |
15 | rand.Seed(seed) | |
16 | for i := 0; i < 1234; i++ { | |
17 | sample := int64(rand.NormFloat64()*float64(stdev) + float64(mean)) | |
18 | h.Observe(sample) | |
19 | } | |
20 | } | |
21 | ||
22 | // https://en.wikipedia.org/wiki/Normal_distribution#Quantile_function | |
23 | func normalValueAtQuantile(mean, stdev int64, quantile int) int64 { | |
24 | return int64(float64(mean) + float64(stdev)*math.Sqrt2*erfinv(2*(float64(quantile)/100)-1)) | |
25 | } | |
26 | ||
27 | // https://stackoverflow.com/questions/5971830/need-code-for-inverse-error-function | |
28 | func erfinv(y float64) float64 { | |
29 | if y < -1.0 || y > 1.0 { | |
30 | panic("invalid input") | |
31 | } | |
32 | ||
33 | var ( | |
34 | a = [4]float64{0.886226899, -1.645349621, 0.914624893, -0.140543331} | |
35 | b = [4]float64{-2.118377725, 1.442710462, -0.329097515, 0.012229801} | |
36 | c = [4]float64{-1.970840454, -1.624906493, 3.429567803, 1.641345311} | |
37 | d = [2]float64{3.543889200, 1.637067800} | |
38 | ) | |
39 | ||
40 | const y0 = 0.7 | |
41 | var x, z float64 | |
42 | ||
43 | if math.Abs(y) == 1.0 { | |
44 | x = -y * math.Log(0.0) | |
45 | } else if y < -y0 { | |
46 | z = math.Sqrt(-math.Log((1.0 + y) / 2.0)) | |
47 | x = -(((c[3]*z+c[2])*z+c[1])*z + c[0]) / ((d[1]*z+d[0])*z + 1.0) | |
48 | } else { | |
49 | if y < y0 { | |
50 | z = y * y | |
51 | 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) | |
52 | } else { | |
53 | z = math.Sqrt(-math.Log((1.0 - y) / 2.0)) | |
54 | x = (((c[3]*z+c[2])*z+c[1])*z + c[0]) / ((d[1]*z+d[0])*z + 1.0) | |
55 | } | |
56 | x = x - (math.Erf(x)-y)/(2.0/math.SqrtPi*math.Exp(-x*x)) | |
57 | x = x - (math.Erf(x)-y)/(2.0/math.SqrtPi*math.Exp(-x*x)) | |
58 | } | |
59 | ||
60 | return x | |
61 | } |
0 | package teststat | |
1 | ||
2 | import ( | |
3 | "expvar" | |
4 | "fmt" | |
5 | "math" | |
6 | "strconv" | |
7 | "testing" | |
8 | ) | |
9 | ||
10 | // AssertExpvarNormalHistogram ensures the expvar Histogram referenced by | |
11 | // metricName abides a normal distribution. | |
12 | func AssertExpvarNormalHistogram(t *testing.T, metricName string, mean, stdev int64, quantiles []int) { | |
13 | const tolerance int = 2 | |
14 | for _, quantile := range quantiles { | |
15 | want := normalValueAtQuantile(mean, stdev, quantile) | |
16 | s := expvar.Get(fmt.Sprintf("%s_p%02d", metricName, quantile)).String() | |
17 | have, err := strconv.Atoi(s) | |
18 | if err != nil { | |
19 | t.Fatal(err) | |
20 | } | |
21 | if int(math.Abs(float64(want)-float64(have))) > tolerance { | |
22 | t.Errorf("quantile %d: want %d, have %d", quantile, want, have) | |
23 | } | |
24 | } | |
25 | } |
0 | package teststat | |
1 | ||
2 | import ( | |
3 | "io/ioutil" | |
4 | "math" | |
5 | "net/http" | |
6 | "net/http/httptest" | |
7 | "regexp" | |
8 | "strconv" | |
9 | "strings" | |
10 | "testing" | |
11 | ||
12 | "github.com/prometheus/client_golang/prometheus" | |
13 | ) | |
14 | ||
15 | // ScrapePrometheus returns the text encoding of the current state of | |
16 | // Prometheus. | |
17 | func ScrapePrometheus(t *testing.T) string { | |
18 | server := httptest.NewServer(prometheus.UninstrumentedHandler()) | |
19 | defer server.Close() | |
20 | ||
21 | resp, err := http.Get(server.URL) | |
22 | if err != nil { | |
23 | t.Fatal(err) | |
24 | } | |
25 | defer resp.Body.Close() | |
26 | ||
27 | buf, err := ioutil.ReadAll(resp.Body) | |
28 | if err != nil { | |
29 | t.Fatal(err) | |
30 | } | |
31 | ||
32 | return strings.TrimSpace(string(buf)) | |
33 | } | |
34 | ||
35 | // AssertPrometheusNormalHistogram ensures the Prometheus Histogram referenced | |
36 | // by metricName abides a normal distribution. | |
37 | func AssertPrometheusNormalHistogram(t *testing.T, metricName string, mean, stdev int64) { | |
38 | scrape := ScrapePrometheus(t) | |
39 | const tolerance int = 5 // Prometheus approximates higher quantiles badly -_-; | |
40 | for quantileInt, quantileStr := range map[int]string{50: "0.5", 90: "0.9", 99: "0.99"} { | |
41 | want := normalValueAtQuantile(mean, stdev, quantileInt) | |
42 | have := getPrometheusQuantile(t, scrape, metricName, quantileStr) | |
43 | if int(math.Abs(float64(want)-float64(have))) > tolerance { | |
44 | t.Errorf("%q: want %d, have %d", quantileStr, want, have) | |
45 | } | |
46 | } | |
47 | } | |
48 | ||
49 | func getPrometheusQuantile(t *testing.T, scrape, name, quantileStr string) int { | |
50 | matches := regexp.MustCompile(name+`{quantile="`+quantileStr+`"} ([0-9]+)`).FindAllStringSubmatch(scrape, -1) | |
51 | if len(matches) < 1 { | |
52 | t.Fatalf("%q: quantile %q not found in scrape", name, quantileStr) | |
53 | } | |
54 | if len(matches[0]) < 2 { | |
55 | t.Fatalf("%q: quantile %q not found in scrape", name, quantileStr) | |
56 | } | |
57 | i, err := strconv.Atoi(matches[0][1]) | |
58 | if err != nil { | |
59 | t.Fatal(err) | |
60 | } | |
61 | return i | |
62 | } |
5 | 5 | "time" |
6 | 6 | |
7 | 7 | "github.com/peterbourgon/gokit/metrics" |
8 | "github.com/peterbourgon/gokit/metrics/expvar" | |
8 | 9 | ) |
9 | 10 | |
10 | 11 | func TestTimeHistogram(t *testing.T) { |
11 | 12 | const metricName string = "test_time_histogram" |
12 | 13 | quantiles := []int{50, 90, 99} |
13 | h0 := metrics.NewExpvarHistogram(metricName, 0, 200, 3, quantiles...) | |
14 | h0 := expvar.NewHistogram(metricName, 0, 200, 3, quantiles...) | |
14 | 15 | h := metrics.NewTimeHistogram(h0, time.Millisecond) |
15 | 16 | const seed, mean, stdev int64 = 321, 100, 20 |
16 | 17 |