github.com/openshift-online/ocm-sdk-go@v0.1.473/metrics/handler_wrapper_test.go (about)

     1  /*
     2  Copyright (c) 2021 Red Hat, Inc.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8    http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  // This file contains tests for the metrics transport wrapper.
    18  
    19  package metrics
    20  
    21  import (
    22  	"net/http"
    23  	"net/http/httptest"
    24  
    25  	. "github.com/onsi/ginkgo/v2/dsl/core"  // nolint
    26  	. "github.com/onsi/ginkgo/v2/dsl/table" // nolint
    27  	. "github.com/onsi/gomega"              // nolint
    28  	. "github.com/onsi/gomega/ghttp"        // nolint
    29  
    30  	. "github.com/openshift-online/ocm-sdk-go/testing"
    31  )
    32  
    33  var _ = Describe("Create", func() {
    34  	It("Can't be created without a subsystem", func() {
    35  		wrapper, err := NewHandlerWrapper().
    36  			Build()
    37  		Expect(err).To(HaveOccurred())
    38  		Expect(wrapper).To(BeNil())
    39  		message := err.Error()
    40  		Expect(message).To(ContainSubstring("subsystem"))
    41  		Expect(message).To(ContainSubstring("mandatory"))
    42  	})
    43  })
    44  
    45  var _ = Describe("Metrics", func() {
    46  	var (
    47  		server  *MetricsServer
    48  		wrapper *HandlerWrapper
    49  		handler http.Handler
    50  	)
    51  
    52  	BeforeEach(func() {
    53  		var err error
    54  
    55  		// Start the metrics server:
    56  		server = NewMetricsServer()
    57  
    58  		// Create the wrapper:
    59  		wrapper, err = NewHandlerWrapper().
    60  			Path("/my/path").
    61  			Subsystem("my").
    62  			Registerer(server.Registry()).
    63  			Build()
    64  		Expect(err).ToNot(HaveOccurred())
    65  	})
    66  
    67  	AfterEach(func() {
    68  		// Stop the metrics server:
    69  		server.Close()
    70  	})
    71  
    72  	// Get sends a GET request to the API server.
    73  	var Send = func(method, path string) {
    74  		request := httptest.NewRequest(method, "http://localhost"+path, nil)
    75  		recorder := httptest.NewRecorder()
    76  		handler.ServeHTTP(recorder, request)
    77  	}
    78  
    79  	It("Calls wrapped handler", func() {
    80  		// Prepare the handler:
    81  		called := false
    82  		handler = wrapper.Wrap(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    83  			called = true
    84  		}))
    85  
    86  		// Send the request:
    87  		Send(http.MethodGet, "/api")
    88  
    89  		// Check that the wrapped handler was called:
    90  		Expect(called).To(BeTrue())
    91  	})
    92  
    93  	Describe("Request count", func() {
    94  		It("Honours subsystem", func() {
    95  			// Prepare the handler:
    96  			handler = wrapper.Wrap(RespondWith(http.StatusOK, nil))
    97  
    98  			// Send the request:
    99  			Send(http.MethodGet, "/api")
   100  
   101  			// Verify the metrics:
   102  			metrics := server.Metrics()
   103  			Expect(metrics).To(MatchLine(`^my_request_count\{.*\} .*$`))
   104  		})
   105  
   106  		DescribeTable(
   107  			"Counts correctly",
   108  			func(count int) {
   109  				// Prepare the handler:
   110  				handler = wrapper.Wrap(RespondWith(http.StatusOK, nil))
   111  
   112  				// Send the requests:
   113  				for i := 0; i < count; i++ {
   114  					Send(http.MethodGet, "/api")
   115  				}
   116  
   117  				// Verify the metrics:
   118  				metrics := server.Metrics()
   119  				Expect(metrics).To(MatchLine(`^\w+_request_count\{.*\} %d$`, count))
   120  			},
   121  			Entry("One", 1),
   122  			Entry("Two", 2),
   123  			Entry("Trhee", 3),
   124  		)
   125  
   126  		DescribeTable(
   127  			"Includes method label",
   128  			func(method string) {
   129  				// Prepare the handler:
   130  				handler = wrapper.Wrap(RespondWith(http.StatusOK, nil))
   131  
   132  				// Send the requests:
   133  				Send(method, "/api")
   134  
   135  				// Verify the metrics:
   136  				metrics := server.Metrics()
   137  				Expect(metrics).To(MatchLine(`^\w+_request_count\{.*method="%s".*\} .*$`, method))
   138  			},
   139  			Entry("GET", http.MethodGet),
   140  			Entry("POST", http.MethodPost),
   141  			Entry("PATCH", http.MethodPatch),
   142  			Entry("DELETE", http.MethodDelete),
   143  		)
   144  
   145  		DescribeTable(
   146  			"Includes path label",
   147  			func(path, label string) {
   148  				// Prepare the handler:
   149  				handler = wrapper.Wrap(RespondWith(http.StatusOK, nil))
   150  
   151  				// Send the requests:
   152  				Send(http.MethodGet, path)
   153  
   154  				// Verify the metrics:
   155  				metrics := server.Metrics()
   156  				Expect(metrics).To(MatchLine(`^\w+_request_count\{.*path="%s".*\} .*$`, label))
   157  			},
   158  			Entry(
   159  				"Empty",
   160  				"",
   161  				"/-",
   162  			),
   163  			Entry(
   164  				"One slash",
   165  				"/",
   166  				"/-",
   167  			),
   168  			Entry(
   169  				"Two slashes",
   170  				"//",
   171  				"/-",
   172  			),
   173  			Entry(
   174  				"Tree slashes",
   175  				"///",
   176  				"/-",
   177  			),
   178  			Entry(
   179  				"API root",
   180  				"/api",
   181  				"/api",
   182  			),
   183  			Entry(
   184  				"API root with trailing slash",
   185  				"/api/",
   186  				"/api",
   187  			),
   188  			Entry(
   189  				"Unknown root",
   190  				"/junk/",
   191  				"/-",
   192  			),
   193  			Entry(
   194  				"Service root",
   195  				"/api/clusters_mgmt",
   196  				"/api/clusters_mgmt",
   197  			),
   198  			Entry(
   199  				"Unknown service root",
   200  				"/api/junk",
   201  				"/-",
   202  			),
   203  			Entry(
   204  				"Version root",
   205  				"/api/clusters_mgmt/v1",
   206  				"/api/clusters_mgmt/v1",
   207  			),
   208  			Entry(
   209  				"Unknown version root",
   210  				"/api/junk/v1",
   211  				"/-",
   212  			),
   213  			Entry(
   214  				"Collection",
   215  				"/api/clusters_mgmt/v1/clusters",
   216  				"/api/clusters_mgmt/v1/clusters",
   217  			),
   218  			Entry(
   219  				"Unknown collection",
   220  				"/api/clusters_mgmt/v1/junk",
   221  				"/-",
   222  			),
   223  			Entry(
   224  				"Collection item",
   225  				"/api/clusters_mgmt/v1/clusters/123",
   226  				"/api/clusters_mgmt/v1/clusters/-",
   227  			),
   228  			Entry(
   229  				"Collection item action",
   230  				"/api/clusters_mgmt/v1/clusters/123/hibernate",
   231  				"/api/clusters_mgmt/v1/clusters/-/hibernate",
   232  			),
   233  			Entry(
   234  				"Unknown collection item action",
   235  				"/api/clusters_mgmt/v1/clusters/123/junk",
   236  				"/-",
   237  			),
   238  			Entry(
   239  				"Subcollection",
   240  				"/api/clusters_mgmt/v1/clusters/123/groups",
   241  				"/api/clusters_mgmt/v1/clusters/-/groups",
   242  			),
   243  			Entry(
   244  				"Unknown subcollection",
   245  				"/api/clusters_mgmt/v1/clusters/123/junks",
   246  				"/-",
   247  			),
   248  			Entry(
   249  				"Subcollection item",
   250  				"/api/clusters_mgmt/v1/clusters/123/groups/456",
   251  				"/api/clusters_mgmt/v1/clusters/-/groups/-",
   252  			),
   253  			Entry(
   254  				"Too long",
   255  				"/api/clusters_mgmt/v1/clusters/123/groups/456/junk",
   256  				"/-",
   257  			),
   258  			Entry(
   259  				"Explicitly specified path",
   260  				"/my/path",
   261  				"/my/path",
   262  			),
   263  			Entry(
   264  				"Unknown path",
   265  				"/your/path",
   266  				"/-",
   267  			),
   268  		)
   269  
   270  		DescribeTable(
   271  			"Includes code label",
   272  			func(code int) {
   273  				// Prepare the handler:
   274  				handler = wrapper.Wrap(RespondWith(code, nil))
   275  
   276  				// Send the requests:
   277  				Send(http.MethodGet, "/api")
   278  
   279  				// Verify the metrics:
   280  				metrics := server.Metrics()
   281  				Expect(metrics).To(MatchLine(`^\w+_request_count\{.*code="%d".*\} .*$`, code))
   282  			},
   283  			Entry("200", http.StatusOK),
   284  			Entry("201", http.StatusCreated),
   285  			Entry("202", http.StatusAccepted),
   286  			Entry("401", http.StatusUnauthorized),
   287  			Entry("404", http.StatusNotFound),
   288  			Entry("500", http.StatusInternalServerError),
   289  		)
   290  
   291  		DescribeTable(
   292  			"Includes API service label",
   293  			func(path, label string) {
   294  				// Prepare the handler:
   295  				handler = wrapper.Wrap(RespondWith(http.StatusOK, nil))
   296  
   297  				// Send the requests:
   298  				Send(http.MethodGet, path)
   299  
   300  				// Verify the metrics:
   301  				metrics := server.Metrics()
   302  				Expect(metrics).To(MatchLine(`^\w+_request_count\{.*apiservice="%s".*\} .*$`, label))
   303  			},
   304  			Entry(
   305  				"Empty",
   306  				"",
   307  				"",
   308  			),
   309  			Entry(
   310  				"Root",
   311  				"/",
   312  				"",
   313  			),
   314  			Entry(
   315  				"Clusters root",
   316  				"/api/clusters_mgmt",
   317  				"ocm-clusters-service",
   318  			),
   319  			Entry(
   320  				"Clusters version",
   321  				"/api/clusters_mgmt/v1",
   322  				"ocm-clusters-service",
   323  			),
   324  			Entry(
   325  				"Clusters collection",
   326  				"/api/clusters_mgmt/v1/clusters",
   327  				"ocm-clusters-service",
   328  			),
   329  			Entry(
   330  				"Clusters item",
   331  				"/api/clusters_mgmt/v1/clusters/123",
   332  				"ocm-clusters-service",
   333  			),
   334  			Entry(
   335  				"Accounts root",
   336  				"/api/accounts_mgmt",
   337  				"ocm-accounts-service",
   338  			),
   339  			Entry(
   340  				"Accounts version",
   341  				"/api/accounts_mgmt/v1",
   342  				"ocm-accounts-service",
   343  			),
   344  			Entry(
   345  				"Accounts collection",
   346  				"/api/accounts_mgmt/v1/accounts",
   347  				"ocm-accounts-service",
   348  			),
   349  			Entry(
   350  				"Accounts item",
   351  				"/api/accounts_mgmt/v1/accounts/123",
   352  				"ocm-accounts-service",
   353  			),
   354  			Entry(
   355  				"Logs root",
   356  				"/api/service_logs",
   357  				"ocm-logs-service",
   358  			),
   359  			Entry(
   360  				"Logs version",
   361  				"/api/service_logs/v1",
   362  				"ocm-logs-service",
   363  			),
   364  			Entry(
   365  				"Logs collection",
   366  				"/api/service_logs/v1/accounts",
   367  				"ocm-logs-service",
   368  			),
   369  			Entry(
   370  				"Logs item",
   371  				"/api/service_logs/v1/accounts/123",
   372  				"ocm-logs-service",
   373  			),
   374  			Entry(
   375  				"Future service not in existing service list",
   376  				"/api/future_mgmt/v1/",
   377  				"ocm-future_mgmt"),
   378  		)
   379  	})
   380  
   381  	Describe("Request duration", func() {
   382  		It("Honours subsystem", func() {
   383  			// Prepare the handler:
   384  			handler = wrapper.Wrap(RespondWith(http.StatusOK, nil))
   385  
   386  			// Send the request:
   387  			Send(http.MethodGet, "/api")
   388  
   389  			// Verify the metrics:
   390  			metrics := server.Metrics()
   391  			Expect(metrics).To(MatchLine(`^my_request_duration_bucket\{.*\} .*$`))
   392  			Expect(metrics).To(MatchLine(`^my_request_duration_sum\{.*\} .*$`))
   393  			Expect(metrics).To(MatchLine(`^my_request_duration_count\{.*\} .*$`))
   394  		})
   395  
   396  		It("Honours buckets", func() {
   397  			// Prepare the handler:
   398  			handler = wrapper.Wrap(RespondWith(http.StatusOK, nil))
   399  
   400  			// Send the request:
   401  			Send(http.MethodGet, "/api")
   402  
   403  			// Verify the metrics:
   404  			metrics := server.Metrics()
   405  			Expect(metrics).To(MatchLine(`^\w+_request_duration_bucket\{.*,le="0.1"\} .*$`))
   406  			Expect(metrics).To(MatchLine(`^\w+_request_duration_bucket\{.*,le="1"\} .*$`))
   407  			Expect(metrics).To(MatchLine(`^\w+_request_duration_bucket\{.*,le="10"\} .*$`))
   408  			Expect(metrics).To(MatchLine(`^\w+_request_duration_bucket\{.*,le="30"\} .*$`))
   409  			Expect(metrics).To(MatchLine(`^\w+_request_duration_bucket\{.*,le="\+Inf"\} .*$`))
   410  		})
   411  
   412  		DescribeTable(
   413  			"Counts correctly",
   414  			func(count int) {
   415  				// Prepare the handler:
   416  				handler = wrapper.Wrap(RespondWith(http.StatusOK, nil))
   417  
   418  				// Send the requests:
   419  				for i := 0; i < count; i++ {
   420  					Send(http.MethodGet, "/api")
   421  				}
   422  
   423  				// Verify the metrics:
   424  				metrics := server.Metrics()
   425  				Expect(metrics).To(MatchLine(`^\w+_request_duration_count\{.*\} %d$`, count))
   426  			},
   427  			Entry("One", 1),
   428  			Entry("Two", 2),
   429  			Entry("Trhee", 3),
   430  		)
   431  
   432  		DescribeTable(
   433  			"Includes method label",
   434  			func(method string) {
   435  				// Prepare the handler:
   436  				handler = wrapper.Wrap(RespondWith(http.StatusOK, nil))
   437  
   438  				// Send the requests:
   439  				Send(method, "/api")
   440  
   441  				// Verify the metrics:
   442  				metrics := server.Metrics()
   443  				Expect(metrics).To(MatchLine(`^\w+_request_duration_bucket\{.*method="%s".*\} .*$`, method))
   444  				Expect(metrics).To(MatchLine(`^\w+_request_duration_sum\{.*method="%s".*\} .*$`, method))
   445  				Expect(metrics).To(MatchLine(`^\w+_request_duration_count\{.*method="%s".*\} .*$`, method))
   446  			},
   447  			Entry("GET", http.MethodGet),
   448  			Entry("POST", http.MethodPost),
   449  			Entry("PATCH", http.MethodPatch),
   450  			Entry("DELETE", http.MethodDelete),
   451  		)
   452  
   453  		DescribeTable(
   454  			"Includes path label",
   455  			func(path, label string) {
   456  				// Prepare the handler:
   457  				handler = wrapper.Wrap(RespondWith(http.StatusOK, nil))
   458  
   459  				// Send the requests:
   460  				Send(http.MethodGet, path)
   461  
   462  				// Verify the metrics:
   463  				metrics := server.Metrics()
   464  				Expect(metrics).To(MatchLine(`^\w+_request_duration_bucket\{.*path="%s".*\} .*$`, label))
   465  				Expect(metrics).To(MatchLine(`^\w+_request_duration_sum\{.*path="%s".*\} .*$`, label))
   466  				Expect(metrics).To(MatchLine(`^\w+_request_duration_count\{.*path="%s".*\} .*$`, label))
   467  			},
   468  			Entry(
   469  				"Empty",
   470  				"",
   471  				"/-",
   472  			),
   473  			Entry(
   474  				"One slash",
   475  				"/",
   476  				"/-",
   477  			),
   478  			Entry(
   479  				"Two slashes",
   480  				"//",
   481  				"/-",
   482  			),
   483  			Entry(
   484  				"Tree slashes",
   485  				"///",
   486  				"/-",
   487  			),
   488  			Entry(
   489  				"API root",
   490  				"/api",
   491  				"/api",
   492  			),
   493  			Entry(
   494  				"API root with trailing slash",
   495  				"/api/",
   496  				"/api",
   497  			),
   498  			Entry(
   499  				"Unknown root",
   500  				"/junk/",
   501  				"/-",
   502  			),
   503  			Entry(
   504  				"Service root",
   505  				"/api/clusters_mgmt",
   506  				"/api/clusters_mgmt",
   507  			),
   508  			Entry(
   509  				"Unknown service root",
   510  				"/api/junk",
   511  				"/-",
   512  			),
   513  			Entry(
   514  				"Version root",
   515  				"/api/clusters_mgmt/v1",
   516  				"/api/clusters_mgmt/v1",
   517  			),
   518  			Entry(
   519  				"Unknown version root",
   520  				"/api/junk/v1",
   521  				"/-",
   522  			),
   523  			Entry(
   524  				"Collection",
   525  				"/api/clusters_mgmt/v1/clusters",
   526  				"/api/clusters_mgmt/v1/clusters",
   527  			),
   528  			Entry(
   529  				"Unknown collection",
   530  				"/api/clusters_mgmt/v1/junk",
   531  				"/-",
   532  			),
   533  			Entry(
   534  				"Collection item",
   535  				"/api/clusters_mgmt/v1/clusters/123",
   536  				"/api/clusters_mgmt/v1/clusters/-",
   537  			),
   538  			Entry(
   539  				"Collection item action",
   540  				"/api/clusters_mgmt/v1/clusters/123/hibernate",
   541  				"/api/clusters_mgmt/v1/clusters/-/hibernate",
   542  			),
   543  			Entry(
   544  				"Unknown collection item action",
   545  				"/api/clusters_mgmt/v1/clusters/123/junk",
   546  				"/-",
   547  			),
   548  			Entry(
   549  				"Subcollection",
   550  				"/api/clusters_mgmt/v1/clusters/123/groups",
   551  				"/api/clusters_mgmt/v1/clusters/-/groups",
   552  			),
   553  			Entry(
   554  				"Unknown subcollection",
   555  				"/api/clusters_mgmt/v1/clusters/123/junks",
   556  				"/-",
   557  			),
   558  			Entry(
   559  				"Subcollection item",
   560  				"/api/clusters_mgmt/v1/clusters/123/groups/456",
   561  				"/api/clusters_mgmt/v1/clusters/-/groups/-",
   562  			),
   563  			Entry(
   564  				"Too long",
   565  				"/api/clusters_mgmt/v1/clusters/123/groups/456/junk",
   566  				"/-",
   567  			),
   568  			Entry(
   569  				"Explicitly specified path",
   570  				"/my/path",
   571  				"/my/path",
   572  			),
   573  			Entry(
   574  				"Unknown path",
   575  				"/your/path",
   576  				"/-",
   577  			),
   578  		)
   579  
   580  		DescribeTable(
   581  			"Includes code label",
   582  			func(code int) {
   583  				// Prepare the handler:
   584  				handler = wrapper.Wrap(RespondWith(code, nil))
   585  
   586  				// Send the requests:
   587  				Send(http.MethodGet, "/api")
   588  
   589  				// Verify the metrics:
   590  				metrics := server.Metrics()
   591  				Expect(metrics).To(MatchLine(`^\w+_request_duration_bucket\{.*code="%d".*\} .*$`, code))
   592  				Expect(metrics).To(MatchLine(`^\w+_request_duration_sum\{.*code="%d".*\} .*$`, code))
   593  				Expect(metrics).To(MatchLine(`^\w+_request_duration_count\{.*code="%d".*\} .*$`, code))
   594  			},
   595  			Entry("200", http.StatusOK),
   596  			Entry("201", http.StatusCreated),
   597  			Entry("202", http.StatusAccepted),
   598  			Entry("401", http.StatusUnauthorized),
   599  			Entry("404", http.StatusNotFound),
   600  			Entry("500", http.StatusInternalServerError),
   601  		)
   602  
   603  		DescribeTable(
   604  			"Includes API service label",
   605  			func(path, label string) {
   606  				// Prepare the handler:
   607  				handler = wrapper.Wrap(RespondWith(http.StatusOK, nil))
   608  
   609  				// Send the requests:
   610  				Send(http.MethodGet, path)
   611  
   612  				// Verify the metrics:
   613  				metrics := server.Metrics()
   614  				Expect(metrics).To(MatchLine(`^\w+_request_duration_bucket\{.*apiservice="%s".*\} .*$`, label))
   615  				Expect(metrics).To(MatchLine(`^\w+_request_duration_sum\{.*apiservice="%s".*\} .*$`, label))
   616  				Expect(metrics).To(MatchLine(`^\w+_request_duration_count\{.*apiservice="%s".*\} .*$`, label))
   617  			},
   618  			Entry(
   619  				"Empty",
   620  				"",
   621  				"",
   622  			),
   623  			Entry(
   624  				"Root",
   625  				"/",
   626  				"",
   627  			),
   628  			Entry(
   629  				"Clusters root",
   630  				"/api/clusters_mgmt",
   631  				"ocm-clusters-service",
   632  			),
   633  			Entry(
   634  				"Clusters version",
   635  				"/api/clusters_mgmt/v1",
   636  				"ocm-clusters-service",
   637  			),
   638  			Entry(
   639  				"Clusters collection",
   640  				"/api/clusters_mgmt/v1/clusters",
   641  				"ocm-clusters-service",
   642  			),
   643  			Entry(
   644  				"Clusters item",
   645  				"/api/clusters_mgmt/v1/clusters/123",
   646  				"ocm-clusters-service",
   647  			),
   648  			Entry(
   649  				"Accounts root",
   650  				"/api/accounts_mgmt",
   651  				"ocm-accounts-service",
   652  			),
   653  			Entry(
   654  				"Accounts version",
   655  				"/api/accounts_mgmt/v1",
   656  				"ocm-accounts-service",
   657  			),
   658  			Entry(
   659  				"Accounts collection",
   660  				"/api/accounts_mgmt/v1/accounts",
   661  				"ocm-accounts-service",
   662  			),
   663  			Entry(
   664  				"Accounts item",
   665  				"/api/accounts_mgmt/v1/accounts/123",
   666  				"ocm-accounts-service",
   667  			),
   668  			Entry(
   669  				"Logs root",
   670  				"/api/service_logs",
   671  				"ocm-logs-service",
   672  			),
   673  			Entry(
   674  				"Logs version",
   675  				"/api/service_logs/v1",
   676  				"ocm-logs-service",
   677  			),
   678  			Entry(
   679  				"Logs collection",
   680  				"/api/service_logs/v1/accounts",
   681  				"ocm-logs-service",
   682  			),
   683  			Entry(
   684  				"Logs item",
   685  				"/api/service_logs/v1/accounts/123",
   686  				"ocm-logs-service",
   687  			),
   688  		)
   689  	})
   690  })