sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/pkg/metrics/http_test.go (about)

     1  /*
     2  Copyright 2019 The Kubernetes Authors.
     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  package metrics
    18  
    19  import (
    20  	"net/http"
    21  	"net/http/httptest"
    22  	"reflect"
    23  	"strings"
    24  	"testing"
    25  	"time"
    26  
    27  	"github.com/prometheus/client_golang/prometheus/testutil"
    28  	"k8s.io/utils/diff"
    29  	"sigs.k8s.io/prow/pkg/simplifypath"
    30  )
    31  
    32  func TestPowersOfTwoBetween(t *testing.T) {
    33  	var testCases = []struct {
    34  		name     string
    35  		min, max float64
    36  		powers   []float64
    37  	}{
    38  		{
    39  			name:   "bounds are powers",
    40  			min:    2,
    41  			max:    32,
    42  			powers: []float64{2, 4, 8, 16, 32},
    43  		},
    44  		{
    45  			name:   "bounds are integers",
    46  			min:    1,
    47  			max:    33,
    48  			powers: []float64{1, 2, 4, 8, 16, 32, 33},
    49  		},
    50  		{
    51  			name:   "bounds are <1",
    52  			min:    0.05,
    53  			max:    0.5,
    54  			powers: []float64{0.05, 0.0625, 0.125, 0.25, 0.5},
    55  		},
    56  	}
    57  	for _, testCase := range testCases {
    58  		t.Run(testCase.name, func(t *testing.T) {
    59  			if actual, expected := powersOfTwoBetween(testCase.min, testCase.max), testCase.powers; !reflect.DeepEqual(actual, expected) {
    60  				t.Errorf("%s: got incorrect powers between (%v,%v): %s", testCase.name, testCase.min, testCase.max, diff.ObjectReflectDiff(actual, expected))
    61  			}
    62  		})
    63  	}
    64  }
    65  
    66  func TestWriteHeader(t *testing.T) {
    67  	testcases := []struct {
    68  		name       string
    69  		statusCode int
    70  	}{
    71  		{
    72  			"StatusOK",
    73  			http.StatusOK,
    74  		},
    75  		{
    76  			"StatusNotFound",
    77  			http.StatusNotFound,
    78  		},
    79  	}
    80  	for _, tc := range testcases {
    81  		t.Run(tc.name, func(t *testing.T) {
    82  			rr := httptest.NewRecorder()
    83  			trw := &traceResponseWriter{ResponseWriter: rr, statusCode: http.StatusOK}
    84  			trw.WriteHeader(tc.statusCode)
    85  			if rr.Code != tc.statusCode {
    86  				t.Errorf("mismatch in response headers: expected %s, got %s", http.StatusText(tc.statusCode), http.StatusText(rr.Code))
    87  			}
    88  			if trw.statusCode != tc.statusCode {
    89  				t.Errorf("mismatch in TraceResponseWriter headers: expected %s, got %s", http.StatusText(tc.statusCode), http.StatusText(trw.statusCode))
    90  			}
    91  		})
    92  
    93  	}
    94  
    95  }
    96  
    97  func TestWrite(t *testing.T) {
    98  	testcases := []struct {
    99  		name         string
   100  		responseBody string
   101  	}{
   102  		{
   103  			"SimpleText for trace",
   104  			"Simple text to traceResponseWriter.size",
   105  		},
   106  	}
   107  	for _, tc := range testcases {
   108  		t.Run(tc.name, func(t *testing.T) {
   109  			rr := httptest.NewRecorder()
   110  			trw := &traceResponseWriter{ResponseWriter: rr, statusCode: http.StatusOK}
   111  			resp := []byte(tc.responseBody)
   112  			_, err := trw.Write(resp)
   113  			if err != nil {
   114  				t.Fatalf("failed to write to TraceResponseWriter")
   115  			}
   116  			if rr.Body.String() != tc.responseBody {
   117  				t.Errorf("mismatch in response body: expected %s, got %s", tc.responseBody, rr.Body.String())
   118  			}
   119  			if trw.size != len(resp) {
   120  				t.Errorf("mismatch in TraceResponseWriter size: expected %d, got %d", len(resp), trw.size)
   121  			}
   122  		})
   123  	}
   124  }
   125  
   126  func TestRecordError(t *testing.T) {
   127  	testcases := []struct {
   128  		name          string
   129  		namespace     string
   130  		expectedError string
   131  		expectedCount int
   132  		expectedOut   string
   133  	}{
   134  		{
   135  			name:          "Simple Error String",
   136  			namespace:     "testnamespace",
   137  			expectedError: "sample error message to ensure proper working",
   138  			expectedOut: `# HELP testnamespace_error_rate number of errors, sorted by label/type.
   139  					   # TYPE testnamespace_error_rate counter
   140  					   testnamespace_error_rate{error="sample error message to ensure proper working"} 1
   141  					   `,
   142  		},
   143  	}
   144  	for _, tc := range testcases {
   145  		t.Run(tc.name, func(t *testing.T) {
   146  			errorRate := ErrorRate(tc.namespace)
   147  			RecordError(tc.expectedError, errorRate)
   148  			if err := testutil.CollectAndCompare(errorRate, strings.NewReader(tc.expectedOut)); err != nil {
   149  				t.Errorf("unexpected metrics for ErrorRate:\n%s", err)
   150  			}
   151  
   152  		})
   153  	}
   154  }
   155  
   156  func oneByteWriter(t *testing.T) http.HandlerFunc {
   157  	return func(w http.ResponseWriter, r *http.Request) {
   158  		oneByteLength := []byte{'1'}
   159  		_, err := w.Write(oneByteLength)
   160  		if err != nil {
   161  			t.Fatalf("failed to write to TraceResponseWriter: %v", err)
   162  		}
   163  	}
   164  }
   165  
   166  func halfSecLatency(_ time.Time) time.Duration {
   167  	return time.Millisecond * 500
   168  }
   169  func TestHandleWithMetricsCustomTimer(t *testing.T) {
   170  	testcases := []struct {
   171  		name                    string
   172  		namespace               string
   173  		customTimer             func(time.Time) time.Duration
   174  		dummyWriter             http.HandlerFunc
   175  		expectedResponseTimeOut string
   176  		expectedResponseSizeOut string
   177  	}{
   178  		{
   179  			name:        "Simple call to dummy handler with 0.5 sec latency and 1 byte response",
   180  			namespace:   "testnamespace",
   181  			customTimer: halfSecLatency,
   182  			dummyWriter: oneByteWriter(t),
   183  			expectedResponseTimeOut: `
   184              # HELP testnamespace_http_request_duration_seconds http request duration in seconds
   185              # TYPE testnamespace_http_request_duration_seconds histogram
   186              testnamespace_http_request_duration_seconds_bucket{method="GET",path="",status="200",user_agent="",le="0.0001"} 0
   187              testnamespace_http_request_duration_seconds_bucket{method="GET",path="",status="200",user_agent="",le="0.0001220703125"} 0
   188              testnamespace_http_request_duration_seconds_bucket{method="GET",path="",status="200",user_agent="",le="0.000244140625"} 0
   189              testnamespace_http_request_duration_seconds_bucket{method="GET",path="",status="200",user_agent="",le="0.00048828125"} 0
   190              testnamespace_http_request_duration_seconds_bucket{method="GET",path="",status="200",user_agent="",le="0.0009765625"} 0
   191              testnamespace_http_request_duration_seconds_bucket{method="GET",path="",status="200",user_agent="",le="0.001953125"} 0
   192              testnamespace_http_request_duration_seconds_bucket{method="GET",path="",status="200",user_agent="",le="0.00390625"} 0
   193              testnamespace_http_request_duration_seconds_bucket{method="GET",path="",status="200",user_agent="",le="0.0078125"} 0
   194              testnamespace_http_request_duration_seconds_bucket{method="GET",path="",status="200",user_agent="",le="0.015625"} 0
   195              testnamespace_http_request_duration_seconds_bucket{method="GET",path="",status="200",user_agent="",le="0.03125"} 0
   196              testnamespace_http_request_duration_seconds_bucket{method="GET",path="",status="200",user_agent="",le="0.0625"} 0
   197              testnamespace_http_request_duration_seconds_bucket{method="GET",path="",status="200",user_agent="",le="0.125"} 0
   198              testnamespace_http_request_duration_seconds_bucket{method="GET",path="",status="200",user_agent="",le="0.25"} 0
   199              testnamespace_http_request_duration_seconds_bucket{method="GET",path="",status="200",user_agent="",le="0.5"} 1
   200              testnamespace_http_request_duration_seconds_bucket{method="GET",path="",status="200",user_agent="",le="1"} 1
   201              testnamespace_http_request_duration_seconds_bucket{method="GET",path="",status="200",user_agent="",le="2"} 1
   202              testnamespace_http_request_duration_seconds_bucket{method="GET",path="",status="200",user_agent="",le="+Inf"} 1
   203              testnamespace_http_request_duration_seconds_sum{method="GET",path="",status="200",user_agent=""} 0.5
   204              testnamespace_http_request_duration_seconds_count{method="GET",path="",status="200",user_agent=""} 1
   205  			`,
   206  			expectedResponseSizeOut: `
   207  			# HELP testnamespace_http_response_size_bytes http response size in bytes
   208              # TYPE testnamespace_http_response_size_bytes histogram
   209              testnamespace_http_response_size_bytes_bucket{method="GET",path="",status="200",user_agent="",le="256"} 1
   210              testnamespace_http_response_size_bytes_bucket{method="GET",path="",status="200",user_agent="",le="512"} 1
   211              testnamespace_http_response_size_bytes_bucket{method="GET",path="",status="200",user_agent="",le="1024"} 1
   212              testnamespace_http_response_size_bytes_bucket{method="GET",path="",status="200",user_agent="",le="2048"} 1
   213              testnamespace_http_response_size_bytes_bucket{method="GET",path="",status="200",user_agent="",le="4096"} 1
   214              testnamespace_http_response_size_bytes_bucket{method="GET",path="",status="200",user_agent="",le="8192"} 1
   215              testnamespace_http_response_size_bytes_bucket{method="GET",path="",status="200",user_agent="",le="16384"} 1
   216              testnamespace_http_response_size_bytes_bucket{method="GET",path="",status="200",user_agent="",le="32768"} 1
   217              testnamespace_http_response_size_bytes_bucket{method="GET",path="",status="200",user_agent="",le="65536"} 1
   218              testnamespace_http_response_size_bytes_bucket{method="GET",path="",status="200",user_agent="",le="+Inf"} 1
   219              testnamespace_http_response_size_bytes_sum{method="GET",path="",status="200",user_agent=""} 1
   220              testnamespace_http_response_size_bytes_count{method="GET",path="",status="200",user_agent=""} 1
   221  			`,
   222  		},
   223  	}
   224  	for _, tc := range testcases {
   225  		t.Run(tc.name, func(t *testing.T) {
   226  			httpResponseSize := HttpResponseSize(tc.namespace, 256, 65536)
   227  			httpRequestDuration := HttpRequestDuration(tc.namespace, 0.0001, 2)
   228  
   229  			simplifier := simplifypath.NewSimplifier(simplifypath.L(""))
   230  			handler := traceHandlerWithCustomTimer(simplifier, httpRequestDuration, httpResponseSize, tc.customTimer)(tc.dummyWriter)
   231  			rr := httptest.NewRecorder()
   232  			req, err := http.NewRequest("GET", "http://example.com", nil)
   233  			if err != nil {
   234  				t.Errorf("error while creating dummy request: %v", err)
   235  			}
   236  			handler.ServeHTTP(rr, req)
   237  			if err := testutil.CollectAndCompare(httpResponseSize, strings.NewReader(tc.expectedResponseSizeOut)); err != nil {
   238  				t.Errorf("unexpected metrics for HTTPResponseSize:\n%s", err)
   239  			}
   240  			if err := testutil.CollectAndCompare(httpRequestDuration, strings.NewReader(tc.expectedResponseTimeOut)); err != nil {
   241  				t.Errorf("unexpected metrics for HTTPRequestDuration:\n%s", err)
   242  			}
   243  		})
   244  	}
   245  }