github.com/m3db/m3@v1.5.1-0.20231129193456-75a402aa583b/src/x/instrument/config_test.go (about)

     1  // Copyright (c) 2020 Uber Technologies, Inc.
     2  //
     3  // Permission is hereby granted, free of charge, to any person obtaining a copy
     4  // of this software and associated documentation files (the "Software"), to deal
     5  // in the Software without restriction, including without limitation the rights
     6  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
     7  // copies of the Software, and to permit persons to whom the Software is
     8  // furnished to do so, subject to the following conditions:
     9  //
    10  // The above copyright notice and this permission notice shall be included in
    11  // all copies or substantial portions of the Software.
    12  //
    13  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    14  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    15  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    16  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    17  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    18  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    19  // THE SOFTWARE.
    20  
    21  package instrument
    22  
    23  import (
    24  	"encoding/json"
    25  	"fmt"
    26  	"net"
    27  	"net/http"
    28  	"testing"
    29  
    30  	"github.com/sergi/go-diff/diffmatchpatch"
    31  
    32  	xjson "github.com/m3db/m3/src/x/json"
    33  
    34  	"github.com/gogo/protobuf/jsonpb"
    35  	extprom "github.com/prometheus/client_golang/prometheus"
    36  	"github.com/prometheus/common/expfmt"
    37  	"github.com/stretchr/testify/require"
    38  )
    39  
    40  func TestPrometheusDefaults(t *testing.T) {
    41  	cfg := newConfiguration()
    42  
    43  	_, closer, reporters, err := cfg.NewRootScopeAndReporters(
    44  		NewRootScopeAndReportersOptions{})
    45  	require.NoError(t, err)
    46  	require.NotNil(t, reporters.PrometheusReporter)
    47  
    48  	defer closer.Close()
    49  
    50  	// Make sure populated default histogram buckets.
    51  	numDefaultBuckets := DefaultHistogramTimerHistogramBuckets().Len()
    52  	require.True(t, numDefaultBuckets > 0)
    53  	require.Equal(t, numDefaultBuckets, len(cfg.PrometheusReporter.DefaultHistogramBuckets))
    54  
    55  	// Make sure populated default summary objectives buckets.
    56  	numQuantiles := len(DefaultSummaryQuantileObjectives())
    57  	require.True(t, numQuantiles > 0)
    58  	require.Equal(t, numQuantiles, len(cfg.PrometheusReporter.DefaultSummaryObjectives))
    59  }
    60  
    61  func TestPrometheusExternalRegistries(t *testing.T) {
    62  	extReg1 := PrometheusExternalRegistry{
    63  		Registry: extprom.NewRegistry(),
    64  		SubScope: "ext1",
    65  	}
    66  	extReg2 := PrometheusExternalRegistry{
    67  		Registry: extprom.NewRegistry(),
    68  		SubScope: "ext2",
    69  	}
    70  
    71  	cfg, listener := startMetricsEndpoint(t)
    72  
    73  	scope, closer, reporters, err := cfg.NewRootScopeAndReporters(
    74  		NewRootScopeAndReportersOptions{
    75  			PrometheusHandlerListener: listener,
    76  			PrometheusExternalRegistries: []PrometheusExternalRegistry{
    77  				extReg1,
    78  				extReg2,
    79  			},
    80  		})
    81  	require.NoError(t, err)
    82  	require.NotNil(t, reporters.PrometheusReporter)
    83  
    84  	foo := scope.Tagged(map[string]string{
    85  		"test": t.Name(),
    86  	}).Counter("foo")
    87  	foo.Inc(3)
    88  
    89  	bar := extprom.NewCounterVec(extprom.CounterOpts{
    90  		Name: "bar",
    91  		Help: "bar help",
    92  	}, []string{
    93  		"test",
    94  	}).With(map[string]string{
    95  		"test": t.Name(),
    96  	})
    97  	extReg1.Registry.MustRegister(bar)
    98  	bar.Inc()
    99  
   100  	baz := extprom.NewCounterVec(extprom.CounterOpts{
   101  		Name: "baz",
   102  		Help: "baz help",
   103  	}, []string{
   104  		"test",
   105  	}).With(map[string]string{
   106  		"test": t.Name(),
   107  	})
   108  	extReg2.Registry.MustRegister(baz)
   109  	baz.Inc()
   110  	baz.Inc()
   111  
   112  	// Wait for report.
   113  	mClosers, ok := closer.(metricsClosers)
   114  	require.True(t, ok)
   115  	require.NoError(t, mClosers.reporterCloser.Close())
   116  
   117  	expected := map[string]xjson.Map{
   118  		"foo": {
   119  			"name": "foo",
   120  			"help": "foo counter",
   121  			"type": "COUNTER",
   122  			"metric": xjson.Array{
   123  				xjson.Map{
   124  					"counter": xjson.Map{"value": 3},
   125  					"label": xjson.Array{
   126  						xjson.Map{
   127  							"name":  "test",
   128  							"value": t.Name(),
   129  						},
   130  					},
   131  				},
   132  			},
   133  		},
   134  		"ext1_bar": {
   135  			"name": "ext1_bar",
   136  			"help": "bar help",
   137  			"type": "COUNTER",
   138  			"metric": xjson.Array{
   139  				xjson.Map{
   140  					"counter": xjson.Map{"value": 1},
   141  					"label": xjson.Array{
   142  						xjson.Map{
   143  							"name":  "test",
   144  							"value": t.Name(),
   145  						},
   146  					},
   147  				},
   148  			},
   149  		},
   150  		"ext2_baz": {
   151  			"name": "ext2_baz",
   152  			"help": "baz help",
   153  			"type": "COUNTER",
   154  			"metric": xjson.Array{
   155  				xjson.Map{
   156  					"counter": xjson.Map{"value": 2},
   157  					"label": xjson.Array{
   158  						xjson.Map{
   159  							"name":  "test",
   160  							"value": t.Name(),
   161  						},
   162  					},
   163  				},
   164  			},
   165  		},
   166  	}
   167  
   168  	assertMetricsEqual(t, listener, expected)
   169  	require.NoError(t, mClosers.Close())
   170  }
   171  
   172  func TestCommonLabelsAdded(t *testing.T) {
   173  	extReg1 := PrometheusExternalRegistry{
   174  		Registry: extprom.NewRegistry(),
   175  		SubScope: "ext1",
   176  	}
   177  
   178  	cfg, listener := startMetricsEndpoint(t)
   179  
   180  	scope, closer, reporters, err := cfg.NewRootScopeAndReporters(
   181  		NewRootScopeAndReportersOptions{
   182  			PrometheusHandlerListener:    listener,
   183  			PrometheusExternalRegistries: []PrometheusExternalRegistry{extReg1},
   184  			CommonLabels:                 map[string]string{"commonLabel": "commonLabelValue"},
   185  		})
   186  	require.NoError(t, err)
   187  	require.NotNil(t, reporters.PrometheusReporter)
   188  
   189  	foo := scope.Counter("foo")
   190  	foo.Inc(3)
   191  
   192  	bar := extprom.NewCounter(extprom.CounterOpts{Name: "bar", Help: "bar help"})
   193  	extReg1.Registry.MustRegister(bar)
   194  	bar.Inc()
   195  
   196  	// Wait for report.
   197  	mClosers, ok := closer.(metricsClosers)
   198  	require.True(t, ok)
   199  	require.NoError(t, mClosers.reporterCloser.Close())
   200  
   201  	expected := map[string]xjson.Map{
   202  		"foo": {
   203  			"name": "foo",
   204  			"help": "foo counter",
   205  			"type": "COUNTER",
   206  			"metric": xjson.Array{
   207  				xjson.Map{
   208  					"counter": xjson.Map{"value": 3},
   209  					"label": xjson.Array{
   210  						xjson.Map{
   211  							"name":  "commonLabel",
   212  							"value": "commonLabelValue",
   213  						},
   214  					},
   215  				},
   216  			},
   217  		},
   218  		"ext1_bar": {
   219  			"name": "ext1_bar",
   220  			"help": "bar help",
   221  			"type": "COUNTER",
   222  			"metric": xjson.Array{
   223  				xjson.Map{
   224  					"counter": xjson.Map{"value": 1},
   225  					"label": xjson.Array{
   226  						xjson.Map{
   227  							"name":  "commonLabel",
   228  							"value": "commonLabelValue",
   229  						},
   230  					},
   231  				},
   232  			},
   233  		},
   234  	}
   235  
   236  	assertMetricsEqual(t, listener, expected)
   237  	require.NoError(t, mClosers.Close())
   238  }
   239  
   240  func startMetricsEndpoint(t *testing.T) (MetricsConfiguration, net.Listener) {
   241  	cfg := newConfiguration()
   242  	listener, err := net.Listen("tcp", "127.0.0.1:0")
   243  	require.NoError(t, err)
   244  	return cfg, listener
   245  }
   246  
   247  func newConfiguration() MetricsConfiguration {
   248  	sanitization := PrometheusMetricSanitization
   249  	extended := DetailedExtendedMetrics
   250  	cfg := MetricsConfiguration{
   251  		Sanitization: &sanitization,
   252  		SamplingRate: 1,
   253  		PrometheusReporter: &PrometheusConfiguration{
   254  			HandlerPath:   "/metrics",
   255  			ListenAddress: "0.0.0.0:0",
   256  			TimerType:     "histogram",
   257  		},
   258  		ExtendedMetrics: &extended,
   259  	}
   260  	return cfg
   261  }
   262  
   263  func assertMetricsEqual(t *testing.T, listener net.Listener, expected map[string]xjson.Map) {
   264  	url := fmt.Sprintf("http://%s/metrics", listener.Addr().String()) //nolint
   265  	resp, err := http.Get(url)                                        //nolint
   266  	require.NoError(t, err)
   267  	require.Equal(t, http.StatusOK, resp.StatusCode)
   268  
   269  	defer resp.Body.Close()
   270  
   271  	var parser expfmt.TextParser
   272  	metricFamilies, err := parser.TextToMetricFamilies(resp.Body)
   273  	require.NoError(t, err)
   274  
   275  	expectMatch := len(expected)
   276  	actualMatch := 0
   277  	for k, v := range metricFamilies {
   278  		data, err := (&jsonpb.Marshaler{}).MarshalToString(v)
   279  		require.NoError(t, err)
   280  		// Turn this on for debugging:
   281  		// fmt.Printf("metric received: key=%s, value=%s\n", k, data)
   282  
   283  		expect, ok := expected[k]
   284  		if !ok {
   285  			continue
   286  		}
   287  
   288  		// Mark matched.
   289  		delete(expected, k)
   290  
   291  		expectJSON := mustPrettyJSONMap(t, expect)
   292  		actualJSON := mustPrettyJSONString(t, data)
   293  
   294  		require.Equal(t, expectJSON, actualJSON,
   295  			diff(expectJSON, actualJSON))
   296  	}
   297  
   298  	var remaining []string
   299  	for k := range expected {
   300  		remaining = append(remaining, k)
   301  	}
   302  
   303  	t.Logf("matched expected metrics: expected=%d, actual=%d",
   304  		expectMatch, actualMatch)
   305  
   306  	require.Equal(t, 0, len(remaining),
   307  		fmt.Sprintf("did not match expected metrics: %v", remaining))
   308  }
   309  
   310  func mustPrettyJSONMap(t *testing.T, value xjson.Map) string {
   311  	pretty, err := json.MarshalIndent(value, "", "  ")
   312  	require.NoError(t, err)
   313  	return string(pretty)
   314  }
   315  
   316  func mustPrettyJSONString(t *testing.T, str string) string {
   317  	var unmarshalled map[string]interface{}
   318  	err := json.Unmarshal([]byte(str), &unmarshalled)
   319  	require.NoError(t, err)
   320  	pretty, err := json.MarshalIndent(unmarshalled, "", "  ")
   321  	require.NoError(t, err)
   322  	return string(pretty)
   323  }
   324  
   325  func diff(expected, actual string) string {
   326  	dmp := diffmatchpatch.New()
   327  	diffs := dmp.DiffMain(expected, actual, false)
   328  	return dmp.DiffPrettyText(diffs)
   329  }