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 }