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 }