github.com/hashicorp/go-metrics@v0.5.3/prometheus/prometheus_test.go (about)

     1  package prometheus
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"log"
     7  	"net/http"
     8  	"net/http/httptest"
     9  	"net/url"
    10  	"reflect"
    11  	"strings"
    12  	"testing"
    13  	"time"
    14  
    15  	"github.com/golang/protobuf/proto"
    16  	dto "github.com/prometheus/client_model/go"
    17  
    18  	"github.com/hashicorp/go-metrics"
    19  	"github.com/prometheus/client_golang/prometheus"
    20  	"github.com/prometheus/common/expfmt"
    21  )
    22  
    23  const (
    24  	TestHostname = "test_hostname"
    25  )
    26  
    27  func TestNewPrometheusSinkFrom(t *testing.T) {
    28  	reg := prometheus.NewRegistry()
    29  
    30  	sink, err := NewPrometheusSinkFrom(PrometheusOpts{
    31  		Registerer: reg,
    32  	})
    33  
    34  	if err != nil {
    35  		t.Fatalf("err = %v, want nil", err)
    36  	}
    37  
    38  	//check if register has a sink by unregistering it.
    39  	ok := reg.Unregister(sink)
    40  	if !ok {
    41  		t.Fatalf("Unregister(sink) = false, want true")
    42  	}
    43  }
    44  
    45  func TestNewPrometheusSink(t *testing.T) {
    46  	sink, err := NewPrometheusSink()
    47  	if err != nil {
    48  		t.Fatalf("err = %v, want nil", err)
    49  	}
    50  
    51  	//check if register has a sink by unregistering it.
    52  	ok := prometheus.Unregister(sink)
    53  	if !ok {
    54  		t.Fatalf("Unregister(sink) = false, want true")
    55  	}
    56  }
    57  
    58  // TestMultiplePrometheusSink tests registering multiple sinks on the same registerer with different descriptors
    59  func TestMultiplePrometheusSink(t *testing.T) {
    60  	gaugeDef := GaugeDefinition{
    61  		Name: []string{"my", "test", "gauge"},
    62  		Help: "A gauge for testing? How helpful!",
    63  	}
    64  
    65  	cfg := PrometheusOpts{
    66  		Expiration:         5 * time.Second,
    67  		GaugeDefinitions:   append([]GaugeDefinition{}, gaugeDef),
    68  		SummaryDefinitions: append([]SummaryDefinition{}),
    69  		CounterDefinitions: append([]CounterDefinition{}),
    70  		Name:               "sink1",
    71  	}
    72  
    73  	sink1, err := NewPrometheusSinkFrom(cfg)
    74  	if err != nil {
    75  		t.Fatalf("err = %v, want nil", err)
    76  	}
    77  
    78  	reg := prometheus.DefaultRegisterer
    79  	if reg == nil {
    80  		t.Fatalf("Expected default register to be non nil, got nil.")
    81  	}
    82  
    83  	gaugeDef2 := GaugeDefinition{
    84  		Name: []string{"my2", "test", "gauge"},
    85  		Help: "A gauge for testing? How helpful!",
    86  	}
    87  
    88  	cfg2 := PrometheusOpts{
    89  		Expiration:         15 * time.Second,
    90  		GaugeDefinitions:   append([]GaugeDefinition{}, gaugeDef2),
    91  		SummaryDefinitions: append([]SummaryDefinition{}),
    92  		CounterDefinitions: append([]CounterDefinition{}),
    93  		// commenting out the name to point out that the default name will be used here instead
    94  		// Name:               "sink2",
    95  	}
    96  
    97  	sink2, err := NewPrometheusSinkFrom(cfg2)
    98  	if err != nil {
    99  		t.Fatalf("err = %v, want nil", err)
   100  	}
   101  	//check if register has a sink by unregistering it.
   102  	ok := reg.Unregister(sink1)
   103  	if !ok {
   104  		t.Fatalf("Unregister(sink) = false, want true")
   105  	}
   106  
   107  	//check if register has a sink by unregistering it.
   108  	ok = reg.Unregister(sink2)
   109  	if !ok {
   110  		t.Fatalf("Unregister(sink) = false, want true")
   111  	}
   112  }
   113  
   114  func TestDefinitions(t *testing.T) {
   115  	gaugeDef := GaugeDefinition{
   116  		Name: []string{"my", "test", "gauge"},
   117  		Help: "A gauge for testing? How helpful!",
   118  	}
   119  	summaryDef := SummaryDefinition{
   120  		Name: []string{"my", "test", "summary"},
   121  		Help: "A summary for testing? How helpful!",
   122  	}
   123  	counterDef := CounterDefinition{
   124  		Name: []string{"my", "test", "counter"},
   125  		Help: "A counter for testing? How helpful!",
   126  	}
   127  
   128  	// PrometheusSink config w/ definitions for each metric type
   129  	cfg := PrometheusOpts{
   130  		Expiration:         5 * time.Second,
   131  		GaugeDefinitions:   append([]GaugeDefinition{}, gaugeDef),
   132  		SummaryDefinitions: append([]SummaryDefinition{}, summaryDef),
   133  		CounterDefinitions: append([]CounterDefinition{}, counterDef),
   134  	}
   135  	sink, err := NewPrometheusSinkFrom(cfg)
   136  	if err != nil {
   137  		t.Fatalf("err = %v, want nil", err)
   138  	}
   139  	defer prometheus.Unregister(sink)
   140  
   141  	// We can't just len(x) where x is a sync.Map, so we range over the single item and assert the name in our metric
   142  	// definition matches the key we have for the map entry. Should fail if any metrics exist that aren't defined, or if
   143  	// the defined metrics don't exist.
   144  	sink.gauges.Range(func(key, value interface{}) bool {
   145  		name, _ := flattenKey(gaugeDef.Name, gaugeDef.ConstLabels)
   146  		if name != key {
   147  			t.Fatalf("expected my_test_gauge, got #{name}")
   148  		}
   149  		return true
   150  	})
   151  	sink.summaries.Range(func(key, value interface{}) bool {
   152  		name, _ := flattenKey(summaryDef.Name, summaryDef.ConstLabels)
   153  		if name != key {
   154  			t.Fatalf("expected my_test_summary, got #{name}")
   155  		}
   156  		return true
   157  	})
   158  	sink.counters.Range(func(key, value interface{}) bool {
   159  		name, _ := flattenKey(counterDef.Name, counterDef.ConstLabels)
   160  		if name != key {
   161  			t.Fatalf("expected my_test_counter, got #{name}")
   162  		}
   163  		return true
   164  	})
   165  
   166  	// Set a bunch of values
   167  	sink.SetGauge(gaugeDef.Name, 42)
   168  	sink.AddSample(summaryDef.Name, 42)
   169  	sink.IncrCounter(counterDef.Name, 1)
   170  
   171  	// Prometheus panic should not be propagated
   172  	sink.IncrCounter(counterDef.Name, -1)
   173  
   174  	// Test that the expiry behavior works as expected. First pick a time which
   175  	// is after all the actual updates above.
   176  	timeAfterUpdates := time.Now()
   177  	// Buffer the chan to make sure it doesn't block. We expect only 3 metrics to
   178  	// be produced but give some extra room as this will hang the test if we don't
   179  	// have a big enough buffer.
   180  	ch := make(chan prometheus.Metric, 10)
   181  
   182  	// Collect the metrics as if it's some time in the future, way beyond the 5
   183  	// second expiry.
   184  	sink.collectAtTime(ch, timeAfterUpdates.Add(10*time.Second))
   185  
   186  	// We should see all the metrics desired Expiry behavior
   187  	expectedNum := 3
   188  	for i := 0; i < expectedNum; i++ {
   189  		select {
   190  		case m := <-ch:
   191  			// m is a prometheus.Metric the only thing we can do is Write it to a
   192  			// protobuf type and read from there.
   193  			var pb dto.Metric
   194  			if err := m.Write(&pb); err != nil {
   195  				t.Fatalf("unexpected error reading metric: %s", err)
   196  			}
   197  			desc := m.Desc().String()
   198  			switch {
   199  			case pb.Counter != nil:
   200  				if !strings.Contains(desc, counterDef.Help) {
   201  					t.Fatalf("expected counter to include correct help=%s, but was %s", counterDef.Help, m.Desc().String())
   202  				}
   203  				// Counters should _not_ reset. We could assert not nil too but that
   204  				// would be a bug in prometheus client code so assume it's never nil...
   205  				if *pb.Counter.Value != float64(1) {
   206  					t.Fatalf("expected defined counter to have value 42 after expiring, got %f", *pb.Counter.Value)
   207  				}
   208  			case pb.Gauge != nil:
   209  				if !strings.Contains(desc, gaugeDef.Help) {
   210  					t.Fatalf("expected gauge to include correct help=%s, but was %s", gaugeDef.Help, m.Desc().String())
   211  				}
   212  				// Gauges should _not_ reset. We could assert not nil too but that
   213  				// would be a bug in prometheus client code so assume it's never nil...
   214  				if *pb.Gauge.Value != float64(42) {
   215  					t.Fatalf("expected defined gauge to have value 42 after expiring, got %f", *pb.Gauge.Value)
   216  				}
   217  			case pb.Summary != nil:
   218  				if !strings.Contains(desc, summaryDef.Help) {
   219  					t.Fatalf("expected summary to include correct help=%s, but was %s", summaryDef.Help, m.Desc().String())
   220  				}
   221  				// Summaries should not be reset. Previous behavior here did attempt to
   222  				// reset them by calling Observe(NaN) which results in all values being
   223  				// set to NaN but doesn't actually clear the time window of data
   224  				// predictably so future observations could also end up as NaN until the
   225  				// NaN sample has aged out of the window. Since the summary is already
   226  				// aging out a fixed time window (we fix it a 10 seconds currently for
   227  				// all summaries and it's not affected by Expiration option), there's no
   228  				// point in trying to reset it after "expiry".
   229  				if *pb.Summary.SampleSum != float64(42) {
   230  					t.Fatalf("expected defined summary sum to have value 42 after expiring, got %f", *pb.Summary.SampleSum)
   231  				}
   232  			default:
   233  				t.Fatalf("unexpected metric type %v", pb)
   234  			}
   235  		case <-time.After(100 * time.Millisecond):
   236  			t.Fatalf("Timed out waiting to collect expected metric. Got %d, want %d", i, expectedNum)
   237  		}
   238  	}
   239  }
   240  
   241  func MockGetHostname() string {
   242  	return TestHostname
   243  }
   244  
   245  func fakeServer(q chan string) *httptest.Server {
   246  	handler := func(w http.ResponseWriter, r *http.Request) {
   247  		w.WriteHeader(202)
   248  		w.Header().Set("Content-Type", "application/json")
   249  		defer r.Body.Close()
   250  		dec := expfmt.NewDecoder(r.Body, expfmt.FmtProtoDelim)
   251  		m := &dto.MetricFamily{}
   252  		dec.Decode(m)
   253  		expectedm := &dto.MetricFamily{
   254  			Name: proto.String("default_one_two"),
   255  			Help: proto.String("default_one_two"),
   256  			Type: dto.MetricType_GAUGE.Enum(),
   257  			Metric: []*dto.Metric{
   258  				&dto.Metric{
   259  					Label: []*dto.LabelPair{
   260  						&dto.LabelPair{
   261  							Name:  proto.String("host"),
   262  							Value: proto.String(MockGetHostname()),
   263  						},
   264  					},
   265  					Gauge: &dto.Gauge{
   266  						Value: proto.Float64(42),
   267  					},
   268  				},
   269  			},
   270  		}
   271  		if !reflect.DeepEqual(m, expectedm) {
   272  			msg := fmt.Sprintf("Unexpected samples extracted, got: %+v, want: %+v", m, expectedm)
   273  			q <- errors.New(msg).Error()
   274  		} else {
   275  			q <- "ok"
   276  		}
   277  	}
   278  
   279  	return httptest.NewServer(http.HandlerFunc(handler))
   280  }
   281  
   282  func TestSetGauge(t *testing.T) {
   283  	q := make(chan string)
   284  	server := fakeServer(q)
   285  	defer server.Close()
   286  	u, err := url.Parse(server.URL)
   287  	if err != nil {
   288  		log.Fatal(err)
   289  	}
   290  	host := u.Hostname() + ":" + u.Port()
   291  	sink, err := NewPrometheusPushSink(host, time.Second, "pushtest")
   292  	metricsConf := metrics.DefaultConfig("default")
   293  	metricsConf.HostName = MockGetHostname()
   294  	metricsConf.EnableHostnameLabel = true
   295  	metrics.NewGlobal(metricsConf, sink)
   296  	metrics.SetGauge([]string{"one", "two"}, 42)
   297  	response := <-q
   298  	if response != "ok" {
   299  		t.Fatal(response)
   300  	}
   301  }
   302  
   303  func TestSetPrecisionGauge(t *testing.T) {
   304  	q := make(chan string)
   305  	server := fakeServer(q)
   306  	defer server.Close()
   307  	u, err := url.Parse(server.URL)
   308  	if err != nil {
   309  		log.Fatal(err)
   310  	}
   311  	host := u.Hostname() + ":" + u.Port()
   312  	sink, err := NewPrometheusPushSink(host, time.Second, "pushtest")
   313  	metricsConf := metrics.DefaultConfig("default")
   314  	metricsConf.HostName = MockGetHostname()
   315  	metricsConf.EnableHostnameLabel = true
   316  	metrics.NewGlobal(metricsConf, sink)
   317  	metrics.SetPrecisionGauge([]string{"one", "two"}, 42)
   318  	response := <-q
   319  	if response != "ok" {
   320  		t.Fatal(response)
   321  	}
   322  }
   323  
   324  func TestDefinitionsWithLabels(t *testing.T) {
   325  	gaugeDef := GaugeDefinition{
   326  		Name: []string{"my", "test", "gauge"},
   327  		Help: "A gauge for testing? How helpful!",
   328  	}
   329  	summaryDef := SummaryDefinition{
   330  		Name: []string{"my", "test", "summary"},
   331  		Help: "A summary for testing? How helpful!",
   332  	}
   333  	counterDef := CounterDefinition{
   334  		Name: []string{"my", "test", "counter"},
   335  		Help: "A counter for testing? How helpful!",
   336  	}
   337  
   338  	// PrometheusSink config w/ definitions for each metric type
   339  	cfg := PrometheusOpts{
   340  		Expiration:         5 * time.Second,
   341  		GaugeDefinitions:   append([]GaugeDefinition{}, gaugeDef),
   342  		SummaryDefinitions: append([]SummaryDefinition{}, summaryDef),
   343  		CounterDefinitions: append([]CounterDefinition{}, counterDef),
   344  	}
   345  	sink, err := NewPrometheusSinkFrom(cfg)
   346  	if err != nil {
   347  		t.Fatalf("err =%#v, want nil", err)
   348  	}
   349  	defer prometheus.Unregister(sink)
   350  	if len(sink.help) != 3 {
   351  		t.Fatalf("Expected len(sink.help) to be 3, was %d: %#v", len(sink.help), sink.help)
   352  	}
   353  
   354  	sink.SetGaugeWithLabels(gaugeDef.Name, 42.0, []metrics.Label{
   355  		{Name: "version", Value: "some info"},
   356  	})
   357  	sink.gauges.Range(func(key, value interface{}) bool {
   358  		localGauge := *value.(*gauge)
   359  		if !strings.Contains(localGauge.Desc().String(), gaugeDef.Help) {
   360  			t.Fatalf("expected gauge to include correct help=%s, but was %s", gaugeDef.Help, localGauge.Desc().String())
   361  		}
   362  		return true
   363  	})
   364  
   365  	sink.AddSampleWithLabels(summaryDef.Name, 42.0, []metrics.Label{
   366  		{Name: "version", Value: "some info"},
   367  	})
   368  	sink.summaries.Range(func(key, value interface{}) bool {
   369  		metric := *value.(*summary)
   370  		if !strings.Contains(metric.Desc().String(), summaryDef.Help) {
   371  			t.Fatalf("expected gauge to include correct help=%s, but was %s", summaryDef.Help, metric.Desc().String())
   372  		}
   373  		return true
   374  	})
   375  
   376  	sink.IncrCounterWithLabels(counterDef.Name, 42.0, []metrics.Label{
   377  		{Name: "version", Value: "some info"},
   378  	})
   379  	sink.counters.Range(func(key, value interface{}) bool {
   380  		metric := *value.(*counter)
   381  		if !strings.Contains(metric.Desc().String(), counterDef.Help) {
   382  			t.Fatalf("expected gauge to include correct help=%s, but was %s", counterDef.Help, metric.Desc().String())
   383  		}
   384  		return true
   385  	})
   386  
   387  	// Prometheus panic should not be propagated
   388  	sink.IncrCounterWithLabels(counterDef.Name, -1, []metrics.Label{
   389  		{Name: "version", Value: "some info"},
   390  	})
   391  }
   392  
   393  func TestMetricSinkInterface(t *testing.T) {
   394  	var ps *PrometheusSink
   395  	_ = metrics.MetricSink(ps)
   396  	var pps *PrometheusPushSink
   397  	_ = metrics.MetricSink(pps)
   398  }
   399  
   400  func Test_flattenKey(t *testing.T) {
   401  	testCases := []struct {
   402  		name               string
   403  		inputParts         []string
   404  		inputLabels        []metrics.Label
   405  		expectedOutputKey  string
   406  		expectedOutputHash string
   407  	}{
   408  		{
   409  			name:       "no replacement needed",
   410  			inputParts: []string{"my", "example", "metric"},
   411  			inputLabels: []metrics.Label{
   412  				{Name: "foo", Value: "bar"},
   413  				{Name: "baz", Value: "buz"},
   414  			},
   415  			expectedOutputKey:  "my_example_metric",
   416  			expectedOutputHash: "my_example_metric;foo=bar;baz=buz",
   417  		},
   418  		{
   419  			name:       "key with whitespace",
   420  			inputParts: []string{" my ", " example ", " metric "},
   421  			inputLabels: []metrics.Label{
   422  				{Name: "foo", Value: "bar"},
   423  				{Name: "baz", Value: "buz"},
   424  			},
   425  			expectedOutputKey:  "_my___example___metric_",
   426  			expectedOutputHash: "_my___example___metric_;foo=bar;baz=buz",
   427  		},
   428  		{
   429  			name:       "key with dot",
   430  			inputParts: []string{".my.", ".example.", ".metric."},
   431  			inputLabels: []metrics.Label{
   432  				{Name: "foo", Value: "bar"},
   433  				{Name: "baz", Value: "buz"},
   434  			},
   435  			expectedOutputKey:  "_my___example___metric_",
   436  			expectedOutputHash: "_my___example___metric_;foo=bar;baz=buz",
   437  		},
   438  		{
   439  			name:       "key with dash",
   440  			inputParts: []string{"-my-", "-example-", "-metric-"},
   441  			inputLabels: []metrics.Label{
   442  				{Name: "foo", Value: "bar"},
   443  				{Name: "baz", Value: "buz"},
   444  			},
   445  			expectedOutputKey:  "_my___example___metric_",
   446  			expectedOutputHash: "_my___example___metric_;foo=bar;baz=buz",
   447  		},
   448  		{
   449  			name:       "key with forward slash",
   450  			inputParts: []string{"/my/", "/example/", "/metric/"},
   451  			inputLabels: []metrics.Label{
   452  				{Name: "foo", Value: "bar"},
   453  				{Name: "baz", Value: "buz"},
   454  			},
   455  			expectedOutputKey:  "_my___example___metric_",
   456  			expectedOutputHash: "_my___example___metric_;foo=bar;baz=buz",
   457  		},
   458  		{
   459  			name:       "key with all restricted",
   460  			inputParts: []string{"/my-", ".example ", "metric"},
   461  			inputLabels: []metrics.Label{
   462  				{Name: "foo", Value: "bar"},
   463  				{Name: "baz", Value: "buz"},
   464  			},
   465  			expectedOutputKey:  "_my___example__metric",
   466  			expectedOutputHash: "_my___example__metric;foo=bar;baz=buz",
   467  		},
   468  	}
   469  
   470  	for _, tc := range testCases {
   471  		t.Run(tc.name, func(b *testing.T) {
   472  			actualKey, actualHash := flattenKey(tc.inputParts, tc.inputLabels)
   473  			if actualKey != tc.expectedOutputKey {
   474  				t.Fatalf("expected key %s, got %s", tc.expectedOutputKey, actualKey)
   475  			}
   476  			if actualHash != tc.expectedOutputHash {
   477  				t.Fatalf("expected hash %s, got %s", tc.expectedOutputHash, actualHash)
   478  			}
   479  		})
   480  	}
   481  }