metrics/metrics_test.go
Aliaksandr Valialkin 17878c4c4e
Make UnregisterSet() less error-prone to use
Previously it was expected that the user calls s.UnregisterAllMetrics() after calling UnregisterSet(s).
This led to many subtle memory leak bugs like https://github.com/VictoriaMetrics/VictoriaMetrics/issues/6247 .

Solve this issue by adding `destroySet bool` arg to UnregisterSet(), so it automatically calls
s.UnregisterAllMetrics() if destroySet=true.

This changes UnregisterSet() function signature, so users need to update UnregisterSet() calls across their code bases
after upgrading to the new version of github.com/VictoriaMetrics/metrics package. This is OK, since this allows
fixing subtle memory leak bugs like https://github.com/VictoriaMetrics/VictoriaMetrics/issues/6247 .
2024-07-15 10:35:32 +02:00

297 lines
8.1 KiB
Go

package metrics
import (
"bytes"
"fmt"
"io"
"strings"
"testing"
"time"
)
func TestWriteMetrics(t *testing.T) {
t.Run("gauge_uint64", func(t *testing.T) {
var bb bytes.Buffer
WriteGaugeUint64(&bb, "foo", 123)
sExpected := "foo 123\n"
if s := bb.String(); s != sExpected {
t.Fatalf("unexpected value; got\n%s\nwant\n%s", s, sExpected)
}
ExposeMetadata(true)
bb.Reset()
WriteGaugeUint64(&bb, "foo", 123)
sExpected = "# HELP foo\n# TYPE foo gauge\nfoo 123\n"
ExposeMetadata(false)
if s := bb.String(); s != sExpected {
t.Fatalf("unexpected value; got\n%s\nwant\n%s", s, sExpected)
}
})
t.Run("gauge_float64", func(t *testing.T) {
var bb bytes.Buffer
WriteGaugeFloat64(&bb, "foo", 1.23)
sExpected := "foo 1.23\n"
if s := bb.String(); s != sExpected {
t.Fatalf("unexpected value; got\n%s\nwant\n%s", s, sExpected)
}
ExposeMetadata(true)
bb.Reset()
WriteGaugeFloat64(&bb, "foo", 1.23)
sExpected = "# HELP foo\n# TYPE foo gauge\nfoo 1.23\n"
ExposeMetadata(false)
if s := bb.String(); s != sExpected {
t.Fatalf("unexpected value; got\n%s\nwant\n%s", s, sExpected)
}
})
t.Run("counter_uint64", func(t *testing.T) {
var bb bytes.Buffer
WriteCounterUint64(&bb, "foo_total", 123)
sExpected := "foo_total 123\n"
if s := bb.String(); s != sExpected {
t.Fatalf("unexpected value; got\n%s\nwant\n%s", s, sExpected)
}
ExposeMetadata(true)
bb.Reset()
WriteCounterUint64(&bb, "foo_total", 123)
sExpected = "# HELP foo_total\n# TYPE foo_total counter\nfoo_total 123\n"
ExposeMetadata(false)
if s := bb.String(); s != sExpected {
t.Fatalf("unexpected value; got\n%s\nwant\n%s", s, sExpected)
}
})
t.Run("counter_float64", func(t *testing.T) {
var bb bytes.Buffer
WriteCounterFloat64(&bb, "foo_total", 1.23)
sExpected := "foo_total 1.23\n"
if s := bb.String(); s != sExpected {
t.Fatalf("unexpected value; got\n%s\nwant\n%s", s, sExpected)
}
ExposeMetadata(true)
bb.Reset()
WriteCounterFloat64(&bb, "foo_total", 1.23)
sExpected = "# HELP foo_total\n# TYPE foo_total counter\nfoo_total 1.23\n"
ExposeMetadata(false)
if s := bb.String(); s != sExpected {
t.Fatalf("unexpected value; got\n%s\nwant\n%s", s, sExpected)
}
})
}
func TestGetDefaultSet(t *testing.T) {
s := GetDefaultSet()
if s != defaultSet {
t.Fatalf("GetDefaultSet must return defaultSet=%p, but returned %p", defaultSet, s)
}
}
func TestUnregisterAllMetrics(t *testing.T) {
for j := 0; j < 3; j++ {
for i := 0; i < 10; i++ {
_ = NewCounter(fmt.Sprintf("counter_%d", i))
_ = NewSummary(fmt.Sprintf("summary_%d", i))
_ = NewHistogram(fmt.Sprintf("histogram_%d", i))
_ = NewGauge(fmt.Sprintf("gauge_%d", i), func() float64 { return 0 })
}
if mns := ListMetricNames(); len(mns) == 0 {
t.Fatalf("unexpected empty list of metrics on iteration %d", j)
}
UnregisterAllMetrics()
if mns := ListMetricNames(); len(mns) != 0 {
t.Fatalf("unexpected metric names after UnregisterAllMetrics call on iteration %d: %q", j, mns)
}
}
}
func TestRegisterMetricsWriter(t *testing.T) {
RegisterMetricsWriter(func(w io.Writer) {
WriteCounterUint64(w, `counter{label="abc"}`, 1234)
WriteGaugeFloat64(w, `gauge{a="b",c="d"}`, -34.43)
})
var bb bytes.Buffer
WritePrometheus(&bb, false)
data := bb.String()
UnregisterAllMetrics()
expectedLine := fmt.Sprintf(`counter{label="abc"} 1234` + "\n")
if !strings.Contains(data, expectedLine) {
t.Fatalf("missing %q in\n%s", expectedLine, data)
}
expectedLine = fmt.Sprintf(`gauge{a="b",c="d"} -34.43` + "\n")
if !strings.Contains(data, expectedLine) {
t.Fatalf("missing %q in\n%s", expectedLine, data)
}
}
func TestRegisterUnregisterSet(t *testing.T) {
const metricName = "metric_from_set"
const metricValue = 123
s := NewSet()
c := s.NewCounter(metricName)
c.Set(metricValue)
RegisterSet(s)
var bb bytes.Buffer
WritePrometheus(&bb, false)
data := bb.String()
expectedLine := fmt.Sprintf("%s %d\n", metricName, metricValue)
if !strings.Contains(data, expectedLine) {
t.Fatalf("missing %q in\n%s", expectedLine, data)
}
UnregisterSet(s, true)
bb.Reset()
WritePrometheus(&bb, false)
data = bb.String()
if strings.Contains(data, expectedLine) {
t.Fatalf("unepected %q in\n%s", expectedLine, data)
}
}
func TestInvalidName(t *testing.T) {
f := func(name string) {
t.Helper()
expectPanic(t, fmt.Sprintf("NewCounter(%q)", name), func() { NewCounter(name) })
expectPanic(t, fmt.Sprintf("NewGauge(%q)", name), func() { NewGauge(name, func() float64 { return 0 }) })
expectPanic(t, fmt.Sprintf("NewSummary(%q)", name), func() { NewSummary(name) })
expectPanic(t, fmt.Sprintf("GetOrCreateCounter(%q)", name), func() { GetOrCreateCounter(name) })
expectPanic(t, fmt.Sprintf("GetOrCreateGauge(%q)", name), func() { GetOrCreateGauge(name, func() float64 { return 0 }) })
expectPanic(t, fmt.Sprintf("GetOrCreateSummary(%q)", name), func() { GetOrCreateSummary(name) })
expectPanic(t, fmt.Sprintf("GetOrCreateHistogram(%q)", name), func() { GetOrCreateHistogram(name) })
}
f("")
f("foo{")
f("foo}")
f("foo{bar")
f("foo{bar=")
f(`foo{bar="`)
f(`foo{bar="baz`)
f(`foo{bar="baz"`)
f(`foo{bar="baz",`)
f(`foo{bar="baz",}`)
}
func TestDoubleRegister(t *testing.T) {
t.Run("NewCounter", func(t *testing.T) {
name := "NewCounterDoubleRegister"
NewCounter(name)
expectPanic(t, name, func() { NewCounter(name) })
})
t.Run("NewGauge", func(t *testing.T) {
name := "NewGaugeDoubleRegister"
NewGauge(name, func() float64 { return 0 })
expectPanic(t, name, func() { NewGauge(name, func() float64 { return 0 }) })
})
t.Run("NewSummary", func(t *testing.T) {
name := "NewSummaryDoubleRegister"
NewSummary(name)
expectPanic(t, name, func() { NewSummary(name) })
})
t.Run("NewHistogram", func(t *testing.T) {
name := "NewHistogramDoubleRegister"
NewHistogram(name)
expectPanic(t, name, func() { NewSummary(name) })
})
}
func TestGetOrCreateNotCounter(t *testing.T) {
name := "GetOrCreateNotCounter"
NewSummary(name)
expectPanic(t, name, func() { GetOrCreateCounter(name) })
}
func TestGetOrCreateNotGauge(t *testing.T) {
name := "GetOrCreateNotGauge"
NewCounter(name)
expectPanic(t, name, func() { GetOrCreateGauge(name, func() float64 { return 0 }) })
}
func TestGetOrCreateNotSummary(t *testing.T) {
name := "GetOrCreateNotSummary"
NewCounter(name)
expectPanic(t, name, func() { GetOrCreateSummary(name) })
}
func TestGetOrCreateNotHistogram(t *testing.T) {
name := "GetOrCreateNotHistogram"
NewCounter(name)
expectPanic(t, name, func() { GetOrCreateHistogram(name) })
}
func TestWritePrometheusSerial(t *testing.T) {
if err := testWritePrometheus(); err != nil {
t.Fatal(err)
}
}
func TestWritePrometheusConcurrent(t *testing.T) {
if err := testConcurrent(testWritePrometheus); err != nil {
t.Fatal(err)
}
}
func testWritePrometheus() error {
var bb bytes.Buffer
WritePrometheus(&bb, false)
resultWithoutProcessMetrics := bb.String()
bb.Reset()
WritePrometheus(&bb, true)
resultWithProcessMetrics := bb.String()
if len(resultWithProcessMetrics) <= len(resultWithoutProcessMetrics) {
return fmt.Errorf("result with process metrics must contain more data than the result without process metrics; got\n%q\nvs\n%q",
resultWithProcessMetrics, resultWithoutProcessMetrics)
}
return nil
}
func expectPanic(t *testing.T, context string, f func()) {
t.Helper()
defer func() {
t.Helper()
if r := recover(); r == nil {
t.Fatalf("expecting panic in %s", context)
}
}()
f()
}
func testConcurrent(f func() error) error {
const concurrency = 5
resultsCh := make(chan error, concurrency)
for i := 0; i < concurrency; i++ {
go func() {
resultsCh <- f()
}()
}
for i := 0; i < concurrency; i++ {
select {
case err := <-resultsCh:
if err != nil {
return fmt.Errorf("unexpected error: %s", err)
}
case <-time.After(time.Second * 5):
return fmt.Errorf("timeout")
}
}
return nil
}
func testMarshalTo(t *testing.T, m metric, prefix, resultExpected string) {
t.Helper()
var bb bytes.Buffer
m.marshalTo(prefix, &bb)
result := bb.String()
if result != resultExpected {
t.Fatalf("unexpected marshaled metric;\ngot\n%q\nwant\n%q", result, resultExpected)
}
}