package prometheus import ( "io/ioutil" "math" "math/rand" "net/http" "net/http/httptest" "reflect" "regexp" "strconv" "strings" "testing" stdprometheus "github.com/prometheus/client_golang/prometheus" "github.com/go-kit/kit/metrics/teststat" ) func TestCounter(t *testing.T) { s := httptest.NewServer(stdprometheus.UninstrumentedHandler()) defer s.Close() scrape := func() string { resp, _ := http.Get(s.URL) buf, _ := ioutil.ReadAll(resp.Body) return string(buf) } namespace, subsystem, name := "ns", "ss", "foo" re := regexp.MustCompile(namespace + `_` + subsystem + `_` + name + `{alpha="alpha-value",beta="beta-value"} ([0-9\.]+)`) counter := NewCounterFrom(stdprometheus.CounterOpts{ Namespace: namespace, Subsystem: subsystem, Name: name, Help: "This is the help string.", }, []string{"alpha", "beta"}).With("beta", "beta-value", "alpha", "alpha-value") // order shouldn't matter value := func() float64 { matches := re.FindStringSubmatch(scrape()) f, _ := strconv.ParseFloat(matches[1], 64) return f } if err := teststat.TestCounter(counter, value); err != nil { t.Fatal(err) } } func TestGauge(t *testing.T) { s := httptest.NewServer(stdprometheus.UninstrumentedHandler()) defer s.Close() scrape := func() string { resp, _ := http.Get(s.URL) buf, _ := ioutil.ReadAll(resp.Body) return string(buf) } namespace, subsystem, name := "aaa", "bbb", "ccc" re := regexp.MustCompile(namespace + `_` + subsystem + `_` + name + `{foo="bar"} ([0-9\.]+)`) gauge := NewGaugeFrom(stdprometheus.GaugeOpts{ Namespace: namespace, Subsystem: subsystem, Name: name, Help: "This is a different help string.", }, []string{"foo"}).With("foo", "bar") value := func() float64 { matches := re.FindStringSubmatch(scrape()) f, _ := strconv.ParseFloat(matches[1], 64) return f } if err := teststat.TestGauge(gauge, value); err != nil { t.Fatal(err) } } func TestSummary(t *testing.T) { s := httptest.NewServer(stdprometheus.UninstrumentedHandler()) defer s.Close() scrape := func() string { resp, _ := http.Get(s.URL) buf, _ := ioutil.ReadAll(resp.Body) return string(buf) } namespace, subsystem, name := "test", "prometheus", "summary" re50 := regexp.MustCompile(namespace + `_` + subsystem + `_` + name + `{a="a",b="b",quantile="0.5"} ([0-9\.]+)`) re90 := regexp.MustCompile(namespace + `_` + subsystem + `_` + name + `{a="a",b="b",quantile="0.9"} ([0-9\.]+)`) re99 := regexp.MustCompile(namespace + `_` + subsystem + `_` + name + `{a="a",b="b",quantile="0.99"} ([0-9\.]+)`) summary := NewSummaryFrom(stdprometheus.SummaryOpts{ Namespace: namespace, Subsystem: subsystem, Name: name, Help: "This is the help string for the summary.", }, []string{"a", "b"}).With("b", "b").With("a", "a") quantiles := func() (float64, float64, float64, float64) { buf := scrape() match50 := re50.FindStringSubmatch(buf) p50, _ := strconv.ParseFloat(match50[1], 64) match90 := re90.FindStringSubmatch(buf) p90, _ := strconv.ParseFloat(match90[1], 64) match99 := re99.FindStringSubmatch(buf) p99, _ := strconv.ParseFloat(match99[1], 64) p95 := p90 + ((p99 - p90) / 2) // Prometheus, y u no p95??? :< #yolo return p50, p90, p95, p99 } if err := teststat.TestHistogram(summary, quantiles, 0.01); err != nil { t.Fatal(err) } } func TestHistogram(t *testing.T) { // Prometheus reports histograms as a count of observations that fell into // each predefined bucket, with the bucket value representing a global upper // limit. That is, the count monotonically increases over the buckets. This // requires a different strategy to test. s := httptest.NewServer(stdprometheus.UninstrumentedHandler()) defer s.Close() scrape := func() string { resp, _ := http.Get(s.URL) buf, _ := ioutil.ReadAll(resp.Body) return string(buf) } namespace, subsystem, name := "test", "prometheus", "histogram" re := regexp.MustCompile(namespace + `_` + subsystem + `_` + name + `_bucket{x="1",le="([0-9]+|\+Inf)"} ([0-9\.]+)`) numStdev := 3 bucketMin := (teststat.Mean - (numStdev * teststat.Stdev)) bucketMax := (teststat.Mean + (numStdev * teststat.Stdev)) if bucketMin < 0 { bucketMin = 0 } bucketCount := 10 bucketDelta := (bucketMax - bucketMin) / bucketCount buckets := []float64{} for i := bucketMin; i <= bucketMax; i += bucketDelta { buckets = append(buckets, float64(i)) } histogram := NewHistogramFrom(stdprometheus.HistogramOpts{ Namespace: namespace, Subsystem: subsystem, Name: name, Help: "This is the help string for the histogram.", Buckets: buckets, }, []string{"x"}).With("x", "1") // Can't TestHistogram, because Prometheus Histograms don't dynamically // compute quantiles. Instead, they fill up buckets. So, let's populate the // histogram kind of manually. teststat.PopulateNormalHistogram(histogram, rand.Int()) // Then, we use ExpectedObservationsLessThan to validate. for _, line := range strings.Split(scrape(), "\n") { match := re.FindStringSubmatch(line) if match == nil { continue } bucket, _ := strconv.ParseInt(match[1], 10, 64) have, _ := strconv.ParseInt(match[2], 10, 64) want := teststat.ExpectedObservationsLessThan(bucket) if match[1] == "+Inf" { want = int64(teststat.Count) // special case } // Unfortunately, we observe experimentally that Prometheus is quite // imprecise at the extremes. I'm setting a very high tolerance for now. // It would be great to dig in and figure out whether that's a problem // with my Expected calculation, or in Prometheus. tolerance := 0.25 if delta := math.Abs(float64(want) - float64(have)); (delta / float64(want)) > tolerance { t.Errorf("Bucket %d: want %d, have %d (%.1f%%)", bucket, want, have, (100.0 * delta / float64(want))) } } } func TestInconsistentLabelCardinality(t *testing.T) { defer func() { x := recover() if x == nil { t.Fatal("expected panic, got none") } err, ok := x.(error) if !ok { t.Fatalf("expected error, got %s", reflect.TypeOf(x)) } if want, have := "inconsistent label cardinality", err.Error(); want != have { t.Fatalf("want %q, have %q", want, have) } }() NewCounterFrom(stdprometheus.CounterOpts{ Namespace: "test", Subsystem: "inconsistent_label_cardinality", Name: "foobar", Help: "This is the help string for the metric.", }, []string{"a", "b"}).With( "a", "1", "b", "2", "c", "KABOOM!", ).Add(123) }