gitlab.com/gitlab-org/labkit@v1.21.0/log/access_logger_test.go (about) 1 package log 2 3 import ( 4 "bytes" 5 "fmt" 6 "io" 7 "net/http" 8 "net/http/httptest" 9 "testing" 10 "time" 11 12 "github.com/stretchr/testify/require" 13 ) 14 15 func TestAccessLogger(t *testing.T) { 16 tests := []struct { 17 name string 18 urlSuffix string 19 body string 20 logMatchers []string 21 options []AccessLoggerOption 22 requestHeaders map[string]string 23 responseHeaders map[string]string 24 handler http.Handler 25 }{ 26 { 27 name: "trivial", 28 body: "hello", 29 logMatchers: []string{ 30 `\btime=\"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}`, 31 `\blevel=info`, 32 `\bmsg=access`, 33 `\bcorrelation_id=\s+`, 34 `\bduration_ms=\d+`, 35 `\bhost="127.0.0.1:\d+"`, 36 `\bmethod=GET`, 37 `\bproto=HTTP/1.1`, 38 `\breferrer=\s+`, 39 `\bremote_addr="127.0.0.1:\d+"`, 40 `\bremote_ip=127.0.0.1`, 41 `\bstatus=200`, 42 `\bsystem=http`, 43 `\buri=/`, 44 `\buser_agent=Go`, 45 `\bwritten_bytes=5`, 46 `\bcontent_type=\s+`, 47 }, 48 }, 49 { 50 name: "senstitive_params", 51 urlSuffix: "?password=123456", 52 logMatchers: []string{ 53 `\buri=\"/\?password=\[FILTERED\]\"`, 54 }, 55 }, 56 { 57 name: "extra_fields", 58 options: []AccessLoggerOption{ 59 WithExtraFields(func(r *http.Request) Fields { 60 return Fields{"testfield": "testvalue"} 61 }), 62 }, 63 logMatchers: []string{ 64 `\btestfield=testvalue\b`, 65 }, 66 }, 67 { 68 name: "excluded_fields", 69 options: []AccessLoggerOption{ 70 WithFieldsExcluded(defaultEnabledFields), 71 }, 72 logMatchers: []string{ 73 `^time=\"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.*level=info msg=access\n$`, 74 }, 75 }, 76 { 77 name: "x_forwarded_for", 78 requestHeaders: map[string]string{ 79 "X-Forwarded-For": "196.7.0.238", 80 }, 81 logMatchers: []string{ 82 `\bremote_ip=196.7.0.238\b`, 83 }, 84 }, 85 { 86 name: "x_forwarded_for_incorrect", 87 requestHeaders: map[string]string{ 88 "X-Forwarded-For": "gitlab.com", 89 }, 90 logMatchers: []string{ 91 `\bremote_ip=127.0.0.1\b`, 92 }, 93 }, 94 { 95 name: "x_forwarded_for_incorrect", 96 requestHeaders: map[string]string{ 97 "X-Forwarded-For": "196.7.238, 197.7.8.9", 98 }, 99 logMatchers: []string{ 100 `\bremote_ip=197.7.8.9\b`, 101 }, 102 }, 103 { 104 name: "x_forwarded_for_not_allowed", 105 options: []AccessLoggerOption{ 106 WithXFFAllowed(func(sip string) bool { return false }), 107 }, 108 requestHeaders: map[string]string{ 109 "X-Forwarded-For": "196.7.0.238", 110 }, 111 logMatchers: []string{ 112 `\bremote_ip=127.0.0.1\b`, 113 }, 114 }, 115 { 116 name: "empty body", 117 logMatchers: []string{ 118 `\bstatus=200\b`, 119 }, 120 }, 121 { 122 name: "content type", 123 body: "hello", 124 responseHeaders: map[string]string{ 125 "Content-Type": "text/plain", 126 }, 127 logMatchers: []string{ 128 `\bcontent_type=text/plain`, 129 }, 130 }, 131 { 132 name: "time to the first byte", 133 body: "ok", 134 logMatchers: []string{ 135 `\bttfb_ms=\d+`, 136 }, 137 }, 138 { 139 name: "time to the first byte, with long delay", 140 body: "yo", 141 logMatchers: []string{ 142 // we expect the delay to be around `10ms` 143 `\bttfb_ms=1\d\b`, 144 }, 145 handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 146 time.Sleep(10 * time.Millisecond) 147 fmt.Fprint(w, "yo") 148 }), 149 }, 150 { 151 name: "time to the first byte, with a slow data transfer", 152 body: "yo", 153 logMatchers: []string{ 154 // we expect the delay to be lower than `10ms` 155 `\bttfb_ms=\d\b`, 156 }, 157 handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 158 w.WriteHeader(http.StatusInternalServerError) 159 time.Sleep(10 * time.Millisecond) 160 fmt.Fprint(w, "yo") 161 }), 162 }, 163 { 164 name: "time to the first byte, with a long processing and slow data transfer", 165 body: "yo", 166 logMatchers: []string{ 167 // we expect the delay to be around `10ms` 168 `\bttfb_ms=1\d\b`, 169 }, 170 handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 171 time.Sleep(10 * time.Millisecond) 172 w.WriteHeader(http.StatusInternalServerError) 173 time.Sleep(20 * time.Millisecond) 174 fmt.Fprint(w, "yo") 175 }), 176 }, 177 } 178 for _, tt := range tests { 179 t.Run(tt.name, func(t *testing.T) { 180 buf := &bytes.Buffer{} 181 182 logger := New() 183 _, err := Initialize(WithLogger(logger), WithWriter(buf)) 184 require.NoError(t, err) 185 186 handler := tt.handler 187 188 if handler == nil { 189 handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 190 for k, v := range tt.responseHeaders { 191 w.Header().Add(k, v) 192 } 193 // This if-statement provides test coverage for the case where the 194 // handler never calls Write or WriteHeader. 195 if len(tt.body) > 0 { 196 fmt.Fprint(w, tt.body) 197 } 198 }) 199 } 200 201 opts := []AccessLoggerOption{WithAccessLogger(logger)} 202 opts = append(opts, tt.options...) 203 handler = AccessLogger(handler, opts...) 204 205 ts := httptest.NewTLSServer(handler) 206 defer ts.Close() 207 208 client := ts.Client() 209 req, err := http.NewRequest("GET", ts.URL+tt.urlSuffix, nil) 210 require.NoError(t, err) 211 212 for k, v := range tt.requestHeaders { 213 req.Header.Add(k, v) 214 } 215 216 res, err := client.Do(req) 217 require.NoError(t, err) 218 219 gotBody, err := io.ReadAll(res.Body) 220 res.Body.Close() 221 222 require.NoError(t, err) 223 require.Equal(t, tt.body, string(gotBody)) 224 225 logString := buf.String() 226 for _, v := range tt.logMatchers { 227 require.Regexp(t, v, logString) 228 } 229 }) 230 } 231 } 232 233 func TestAccessLoggerPanic(t *testing.T) { 234 buf := &bytes.Buffer{} 235 236 logger := New() 237 _, err := Initialize(WithLogger(logger), WithWriter(buf)) 238 require.NoError(t, err) 239 240 var handler http.Handler 241 handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 242 panic("see how the logger handles a panic") 243 }) 244 245 opts := []AccessLoggerOption{WithAccessLogger(logger)} 246 handler = AccessLogger(handler, opts...) 247 248 ts := httptest.NewTLSServer(handler) 249 defer ts.Close() 250 251 client := ts.Client() 252 req, err := http.NewRequest("GET", ts.URL+"/", nil) 253 require.NoError(t, err) 254 255 _, err = client.Do(req) 256 require.Error(t, err, "panic should cause the request to fail with a closed connection") 257 258 require.Regexp(t, `\bstatus=0\b`, buf.String(), "if the handler panics before writing a response header, the status code is undefined, so we expect code 0") 259 } 260 261 func TestUnwrap(t *testing.T) { 262 rw := httptest.NewRecorder() 263 lrw := &loggingResponseWriter{rw: rw} 264 265 require.Same(t, rw, lrw.Unwrap()) 266 }