github.com/netdata/go.d.plugin@v0.58.1/modules/weblog/logline_test.go (about) 1 // SPDX-License-Identifier: GPL-3.0-or-later 2 3 package weblog 4 5 import ( 6 "errors" 7 "fmt" 8 "testing" 9 10 "github.com/stretchr/testify/assert" 11 "github.com/stretchr/testify/require" 12 ) 13 14 const ( 15 emptyStr = "" 16 ) 17 18 var emptyLogLine = *newEmptyLogLine() 19 20 func TestLogLine_Assign(t *testing.T) { 21 type subTest struct { 22 input string 23 wantLine logLine 24 wantErr error 25 } 26 type test struct { 27 name string 28 fields []string 29 cases []subTest 30 } 31 tests := []test{ 32 { 33 name: "Vhost", 34 fields: []string{ 35 "host", 36 "http_host", 37 "v", 38 }, 39 cases: []subTest{ 40 {input: "1.1.1.1", wantLine: logLine{web: web{vhost: "1.1.1.1"}}}, 41 {input: "::1", wantLine: logLine{web: web{vhost: "::1"}}}, 42 {input: "[::1]", wantLine: logLine{web: web{vhost: "::1"}}}, 43 {input: "1ce:1ce::babe", wantLine: logLine{web: web{vhost: "1ce:1ce::babe"}}}, 44 {input: "[1ce:1ce::babe]", wantLine: logLine{web: web{vhost: "1ce:1ce::babe"}}}, 45 {input: "localhost", wantLine: logLine{web: web{vhost: "localhost"}}}, 46 {input: "debian10.debian", wantLine: logLine{web: web{vhost: "debian10.debian"}}}, 47 {input: "my_vhost", wantLine: logLine{web: web{vhost: "my_vhost"}}}, 48 {input: emptyStr, wantLine: emptyLogLine}, 49 {input: hyphen, wantLine: emptyLogLine}, 50 }, 51 }, 52 { 53 name: "Server Port", 54 fields: []string{ 55 "server_port", 56 "p", 57 }, 58 cases: []subTest{ 59 {input: "80", wantLine: logLine{web: web{port: "80"}}}, 60 {input: "8081", wantLine: logLine{web: web{port: "8081"}}}, 61 {input: "30000", wantLine: logLine{web: web{port: "30000"}}}, 62 {input: emptyStr, wantLine: emptyLogLine}, 63 {input: hyphen, wantLine: emptyLogLine}, 64 {input: "-1", wantLine: emptyLogLine, wantErr: errBadPort}, 65 {input: "0", wantLine: emptyLogLine, wantErr: errBadPort}, 66 {input: "50000", wantLine: emptyLogLine, wantErr: errBadPort}, 67 }, 68 }, 69 { 70 name: "Vhost With Port", 71 fields: []string{ 72 "host:$server_port", 73 "v:%p", 74 }, 75 cases: []subTest{ 76 {input: "1.1.1.1:80", wantLine: logLine{web: web{vhost: "1.1.1.1", port: "80"}}}, 77 {input: "::1:80", wantLine: logLine{web: web{vhost: "::1", port: "80"}}}, 78 {input: "[::1]:80", wantLine: logLine{web: web{vhost: "::1", port: "80"}}}, 79 {input: "1ce:1ce::babe:80", wantLine: logLine{web: web{vhost: "1ce:1ce::babe", port: "80"}}}, 80 {input: "debian10.debian:81", wantLine: logLine{web: web{vhost: "debian10.debian", port: "81"}}}, 81 {input: emptyStr, wantLine: emptyLogLine}, 82 {input: hyphen, wantLine: emptyLogLine}, 83 {input: "1.1.1.1", wantLine: emptyLogLine, wantErr: errBadVhostPort}, 84 {input: "1.1.1.1:", wantLine: emptyLogLine, wantErr: errBadVhostPort}, 85 {input: "1.1.1.1 80", wantLine: emptyLogLine, wantErr: errBadVhostPort}, 86 {input: "1.1.1.1:20", wantLine: emptyLogLine, wantErr: errBadVhostPort}, 87 {input: "1.1.1.1:50000", wantLine: emptyLogLine, wantErr: errBadVhostPort}, 88 }, 89 }, 90 { 91 name: "Scheme", 92 fields: []string{ 93 "scheme", 94 }, 95 cases: []subTest{ 96 {input: "http", wantLine: logLine{web: web{reqScheme: "http"}}}, 97 {input: "https", wantLine: logLine{web: web{reqScheme: "https"}}}, 98 {input: emptyStr, wantLine: emptyLogLine}, 99 {input: hyphen, wantLine: emptyLogLine}, 100 {input: "HTTP", wantLine: emptyLogLine, wantErr: errBadReqScheme}, 101 {input: "HTTPS", wantLine: emptyLogLine, wantErr: errBadReqScheme}, 102 }, 103 }, 104 { 105 name: "Client", 106 fields: []string{ 107 "remote_addr", 108 "a", 109 "h", 110 }, 111 cases: []subTest{ 112 {input: "1.1.1.1", wantLine: logLine{web: web{reqClient: "1.1.1.1"}}}, 113 {input: "debian10", wantLine: logLine{web: web{reqClient: "debian10"}}}, 114 {input: emptyStr, wantLine: emptyLogLine}, 115 {input: hyphen, wantLine: emptyLogLine}, 116 }, 117 }, 118 { 119 name: "Request", 120 fields: []string{ 121 "request", 122 "r", 123 }, 124 cases: []subTest{ 125 {input: "GET / HTTP/1.0", wantLine: logLine{web: web{reqMethod: "GET", reqURL: "/", reqProto: "1.0"}}}, 126 {input: "HEAD /ihs.gif HTTP/1.0", wantLine: logLine{web: web{reqMethod: "HEAD", reqURL: "/ihs.gif", reqProto: "1.0"}}}, 127 {input: "POST /ihs.gif HTTP/1.0", wantLine: logLine{web: web{reqMethod: "POST", reqURL: "/ihs.gif", reqProto: "1.0"}}}, 128 {input: "PUT /ihs.gif HTTP/1.0", wantLine: logLine{web: web{reqMethod: "PUT", reqURL: "/ihs.gif", reqProto: "1.0"}}}, 129 {input: "PATCH /ihs.gif HTTP/1.0", wantLine: logLine{web: web{reqMethod: "PATCH", reqURL: "/ihs.gif", reqProto: "1.0"}}}, 130 {input: "DELETE /ihs.gif HTTP/1.0", wantLine: logLine{web: web{reqMethod: "DELETE", reqURL: "/ihs.gif", reqProto: "1.0"}}}, 131 {input: "OPTIONS /ihs.gif HTTP/1.0", wantLine: logLine{web: web{reqMethod: "OPTIONS", reqURL: "/ihs.gif", reqProto: "1.0"}}}, 132 {input: "TRACE /ihs.gif HTTP/1.0", wantLine: logLine{web: web{reqMethod: "TRACE", reqURL: "/ihs.gif", reqProto: "1.0"}}}, 133 {input: "CONNECT ip.cn:443 HTTP/1.1", wantLine: logLine{web: web{reqMethod: "CONNECT", reqURL: "ip.cn:443", reqProto: "1.1"}}}, 134 {input: "MKCOL ip.cn:443 HTTP/1.1", wantLine: logLine{web: web{reqMethod: "MKCOL", reqURL: "ip.cn:443", reqProto: "1.1"}}}, 135 {input: "PROPFIND ip.cn:443 HTTP/1.1", wantLine: logLine{web: web{reqMethod: "PROPFIND", reqURL: "ip.cn:443", reqProto: "1.1"}}}, 136 {input: "MOVE ip.cn:443 HTTP/1.1", wantLine: logLine{web: web{reqMethod: "MOVE", reqURL: "ip.cn:443", reqProto: "1.1"}}}, 137 {input: "SEARCH ip.cn:443 HTTP/1.1", wantLine: logLine{web: web{reqMethod: "SEARCH", reqURL: "ip.cn:443", reqProto: "1.1"}}}, 138 {input: "GET / HTTP/1.1", wantLine: logLine{web: web{reqMethod: "GET", reqURL: "/", reqProto: "1.1"}}}, 139 {input: "GET / HTTP/2", wantLine: logLine{web: web{reqMethod: "GET", reqURL: "/", reqProto: "2"}}}, 140 {input: "GET / HTTP/2.0", wantLine: logLine{web: web{reqMethod: "GET", reqURL: "/", reqProto: "2.0"}}}, 141 {input: "GET /invalid_version http/1.1", wantLine: logLine{web: web{reqMethod: "GET", reqURL: "/invalid_version", reqProto: emptyString}}, wantErr: errBadReqProto}, 142 {input: emptyStr, wantLine: emptyLogLine}, 143 {input: hyphen, wantLine: emptyLogLine}, 144 {input: "GET no_version", wantLine: emptyLogLine, wantErr: errBadRequest}, 145 {input: "GOT / HTTP/2", wantLine: emptyLogLine, wantErr: errBadReqMethod}, 146 {input: "get / HTTP/2", wantLine: emptyLogLine, wantErr: errBadReqMethod}, 147 {input: "x04\x01\x00P$3\xFE\xEA\x00", wantLine: emptyLogLine, wantErr: errBadRequest}, 148 }, 149 }, 150 { 151 name: "Request HTTP Method", 152 fields: []string{ 153 "request_method", 154 "m", 155 }, 156 cases: []subTest{ 157 {input: "GET", wantLine: logLine{web: web{reqMethod: "GET"}}}, 158 {input: "HEAD", wantLine: logLine{web: web{reqMethod: "HEAD"}}}, 159 {input: "POST", wantLine: logLine{web: web{reqMethod: "POST"}}}, 160 {input: "PUT", wantLine: logLine{web: web{reqMethod: "PUT"}}}, 161 {input: "PATCH", wantLine: logLine{web: web{reqMethod: "PATCH"}}}, 162 {input: "DELETE", wantLine: logLine{web: web{reqMethod: "DELETE"}}}, 163 {input: "OPTIONS", wantLine: logLine{web: web{reqMethod: "OPTIONS"}}}, 164 {input: "TRACE", wantLine: logLine{web: web{reqMethod: "TRACE"}}}, 165 {input: "CONNECT", wantLine: logLine{web: web{reqMethod: "CONNECT"}}}, 166 {input: "MKCOL", wantLine: logLine{web: web{reqMethod: "MKCOL"}}}, 167 {input: "PROPFIND", wantLine: logLine{web: web{reqMethod: "PROPFIND"}}}, 168 {input: "MOVE", wantLine: logLine{web: web{reqMethod: "MOVE"}}}, 169 {input: "SEARCH", wantLine: logLine{web: web{reqMethod: "SEARCH"}}}, 170 {input: emptyStr, wantLine: emptyLogLine}, 171 {input: hyphen, wantLine: emptyLogLine}, 172 {input: "GET no_version", wantLine: emptyLogLine, wantErr: errBadReqMethod}, 173 {input: "GOT / HTTP/2", wantLine: emptyLogLine, wantErr: errBadReqMethod}, 174 {input: "get / HTTP/2", wantLine: emptyLogLine, wantErr: errBadReqMethod}, 175 }, 176 }, 177 { 178 name: "Request URL", 179 fields: []string{ 180 "request_uri", 181 "U", 182 }, 183 cases: []subTest{ 184 {input: "/server-status?auto", wantLine: logLine{web: web{reqURL: "/server-status?auto"}}}, 185 {input: "/default.html", wantLine: logLine{web: web{reqURL: "/default.html"}}}, 186 {input: "10.0.0.1:3128", wantLine: logLine{web: web{reqURL: "10.0.0.1:3128"}}}, 187 {input: emptyStr, wantLine: emptyLogLine}, 188 {input: hyphen, wantLine: emptyLogLine}, 189 }, 190 }, 191 { 192 name: "Request HTTP Protocol", 193 fields: []string{ 194 "server_protocol", 195 "H", 196 }, 197 cases: []subTest{ 198 {input: "HTTP/1.0", wantLine: logLine{web: web{reqProto: "1.0"}}}, 199 {input: "HTTP/1.1", wantLine: logLine{web: web{reqProto: "1.1"}}}, 200 {input: "HTTP/2", wantLine: logLine{web: web{reqProto: "2"}}}, 201 {input: "HTTP/2.0", wantLine: logLine{web: web{reqProto: "2.0"}}}, 202 {input: "HTTP/3", wantLine: logLine{web: web{reqProto: "3"}}}, 203 {input: "HTTP/3.0", wantLine: logLine{web: web{reqProto: "3.0"}}}, 204 {input: emptyStr, wantLine: emptyLogLine}, 205 {input: hyphen, wantLine: emptyLogLine}, 206 {input: "1.1", wantLine: emptyLogLine, wantErr: errBadReqProto}, 207 {input: "http/1.1", wantLine: emptyLogLine, wantErr: errBadReqProto}, 208 }, 209 }, 210 { 211 name: "Response Status Code", 212 fields: []string{ 213 "status", 214 "s", 215 ">s", 216 }, 217 cases: []subTest{ 218 {input: "100", wantLine: logLine{web: web{respCode: 100}}}, 219 {input: "200", wantLine: logLine{web: web{respCode: 200}}}, 220 {input: "300", wantLine: logLine{web: web{respCode: 300}}}, 221 {input: "400", wantLine: logLine{web: web{respCode: 400}}}, 222 {input: "500", wantLine: logLine{web: web{respCode: 500}}}, 223 {input: "600", wantLine: logLine{web: web{respCode: 600}}}, 224 {input: emptyStr, wantLine: emptyLogLine}, 225 {input: hyphen, wantLine: emptyLogLine}, 226 {input: "99", wantLine: emptyLogLine, wantErr: errBadRespCode}, 227 {input: "601", wantLine: emptyLogLine, wantErr: errBadRespCode}, 228 {input: "200 ", wantLine: emptyLogLine, wantErr: errBadRespCode}, 229 {input: "0.222", wantLine: emptyLogLine, wantErr: errBadRespCode}, 230 {input: "localhost", wantLine: emptyLogLine, wantErr: errBadRespCode}, 231 }, 232 }, 233 { 234 name: "Request Size", 235 fields: []string{ 236 "request_length", 237 "I", 238 }, 239 cases: []subTest{ 240 {input: "15", wantLine: logLine{web: web{reqSize: 15}}}, 241 {input: "1000000", wantLine: logLine{web: web{reqSize: 1000000}}}, 242 {input: emptyStr, wantLine: emptyLogLine}, 243 {input: hyphen, wantLine: logLine{web: web{reqSize: 0}}}, 244 {input: "-1", wantLine: emptyLogLine, wantErr: errBadReqSize}, 245 {input: "100.222", wantLine: emptyLogLine, wantErr: errBadReqSize}, 246 {input: "invalid", wantLine: emptyLogLine, wantErr: errBadReqSize}, 247 }, 248 }, 249 { 250 name: "Response Size", 251 fields: []string{ 252 "bytes_sent", 253 "body_bytes_sent", 254 "O", 255 "B", 256 "b", 257 }, 258 cases: []subTest{ 259 {input: "15", wantLine: logLine{web: web{respSize: 15}}}, 260 {input: "1000000", wantLine: logLine{web: web{respSize: 1000000}}}, 261 {input: emptyStr, wantLine: emptyLogLine}, 262 {input: hyphen, wantLine: logLine{web: web{respSize: 0}}}, 263 {input: "-1", wantLine: emptyLogLine, wantErr: errBadRespSize}, 264 {input: "100.222", wantLine: emptyLogLine, wantErr: errBadRespSize}, 265 {input: "invalid", wantLine: emptyLogLine, wantErr: errBadRespSize}, 266 }, 267 }, 268 { 269 name: "Request Processing Time", 270 fields: []string{ 271 "request_time", 272 "D", 273 }, 274 cases: []subTest{ 275 {input: "100222", wantLine: logLine{web: web{reqProcTime: 100222}}}, 276 {input: "100.222", wantLine: logLine{web: web{reqProcTime: 100222000}}}, 277 {input: emptyStr, wantLine: emptyLogLine}, 278 {input: hyphen, wantLine: emptyLogLine}, 279 {input: "-1", wantLine: emptyLogLine, wantErr: errBadReqProcTime}, 280 {input: "0.333,0.444,0.555", wantLine: emptyLogLine, wantErr: errBadReqProcTime}, 281 {input: "number", wantLine: emptyLogLine, wantErr: errBadReqProcTime}, 282 }, 283 }, 284 { 285 name: "Upstream Response Time", 286 fields: []string{ 287 "upstream_response_time", 288 }, 289 cases: []subTest{ 290 {input: "100222", wantLine: logLine{web: web{upsRespTime: 100222}}}, 291 {input: "100.222", wantLine: logLine{web: web{upsRespTime: 100222000}}}, 292 {input: "0.100 , 0.400 : 0.200 ", wantLine: logLine{web: web{upsRespTime: 700000}}}, 293 {input: emptyStr, wantLine: emptyLogLine}, 294 {input: hyphen, wantLine: emptyLogLine}, 295 {input: "-1", wantLine: emptyLogLine, wantErr: errBadUpsRespTime}, 296 {input: "number", wantLine: emptyLogLine, wantErr: errBadUpsRespTime}, 297 }, 298 }, 299 { 300 name: "SSL Protocol", 301 fields: []string{ 302 "ssl_protocol", 303 }, 304 cases: []subTest{ 305 {input: "SSLv3", wantLine: logLine{web: web{sslProto: "SSLv3"}}}, 306 {input: "SSLv2", wantLine: logLine{web: web{sslProto: "SSLv2"}}}, 307 {input: "TLSv1", wantLine: logLine{web: web{sslProto: "TLSv1"}}}, 308 {input: "TLSv1.1", wantLine: logLine{web: web{sslProto: "TLSv1.1"}}}, 309 {input: "TLSv1.2", wantLine: logLine{web: web{sslProto: "TLSv1.2"}}}, 310 {input: "TLSv1.3", wantLine: logLine{web: web{sslProto: "TLSv1.3"}}}, 311 {input: emptyStr, wantLine: emptyLogLine}, 312 {input: hyphen, wantLine: emptyLogLine}, 313 {input: "-1", wantLine: emptyLogLine, wantErr: errBadSSLProto}, 314 {input: "invalid", wantLine: emptyLogLine, wantErr: errBadSSLProto}, 315 }, 316 }, 317 { 318 name: "SSL Cipher Suite", 319 fields: []string{ 320 "ssl_cipher", 321 }, 322 cases: []subTest{ 323 {input: "ECDHE-RSA-AES256-SHA", wantLine: logLine{web: web{sslCipherSuite: "ECDHE-RSA-AES256-SHA"}}}, 324 {input: "DHE-RSA-AES256-SHA", wantLine: logLine{web: web{sslCipherSuite: "DHE-RSA-AES256-SHA"}}}, 325 {input: "AES256-SHA", wantLine: logLine{web: web{sslCipherSuite: "AES256-SHA"}}}, 326 {input: "PSK-RC4-SHA", wantLine: logLine{web: web{sslCipherSuite: "PSK-RC4-SHA"}}}, 327 {input: "TLS_AES_256_GCM_SHA384", wantLine: logLine{web: web{sslCipherSuite: "TLS_AES_256_GCM_SHA384"}}}, 328 {input: emptyStr, wantLine: emptyLogLine}, 329 {input: hyphen, wantLine: emptyLogLine}, 330 {input: "-1", wantLine: emptyLogLine, wantErr: errBadSSLCipherSuite}, 331 {input: "invalid", wantLine: emptyLogLine, wantErr: errBadSSLCipherSuite}, 332 }, 333 }, 334 { 335 name: "Custom Fields", 336 fields: []string{ 337 "custom", 338 }, 339 cases: []subTest{ 340 {input: "POST", wantLine: logLine{custom: custom{values: []customValue{{name: "custom", value: "POST"}}}}}, 341 {input: "/example.com", wantLine: logLine{custom: custom{values: []customValue{{name: "custom", value: "/example.com"}}}}}, 342 {input: "HTTP/1.1", wantLine: logLine{custom: custom{values: []customValue{{name: "custom", value: "HTTP/1.1"}}}}}, 343 {input: "0.333,0.444,0.555", wantLine: logLine{custom: custom{values: []customValue{{name: "custom", value: "0.333,0.444,0.555"}}}}}, 344 {input: "-1", wantLine: logLine{custom: custom{values: []customValue{{name: "custom", value: "-1"}}}}}, 345 {input: "invalid", wantLine: logLine{custom: custom{values: []customValue{{name: "custom", value: "invalid"}}}}}, 346 {input: emptyStr, wantLine: emptyLogLine}, 347 {input: hyphen, wantLine: emptyLogLine}, 348 }, 349 }, 350 { 351 name: "Custom Fields Not Exist", 352 fields: []string{ 353 "custom_field_not_exist", 354 }, 355 cases: []subTest{ 356 {input: "POST", wantLine: emptyLogLine}, 357 {input: "/example.com", wantLine: emptyLogLine}, 358 {input: "HTTP/1.1", wantLine: emptyLogLine}, 359 {input: "0.333,0.444,0.555", wantLine: emptyLogLine}, 360 {input: "-1", wantLine: emptyLogLine}, 361 {input: "invalid", wantLine: emptyLogLine}, 362 {input: emptyStr, wantLine: emptyLogLine}, 363 {input: hyphen, wantLine: emptyLogLine}, 364 }, 365 }, 366 } 367 368 for _, tt := range tests { 369 for _, field := range tt.fields { 370 for i, tc := range tt.cases { 371 name := fmt.Sprintf("[%s:%d]field='%s'|line='%s'", tt.name, i+1, field, tc.input) 372 t.Run(name, func(t *testing.T) { 373 374 line := newEmptyLogLineWithFields() 375 err := line.Assign(field, tc.input) 376 377 if tc.wantErr != nil { 378 require.Error(t, err) 379 assert.Truef(t, errors.Is(err, tc.wantErr), "expected '%v' error, got '%v'", tc.wantErr, err) 380 } else { 381 require.NoError(t, err) 382 } 383 384 expected := prepareLogLine(field, tc.wantLine) 385 assert.Equal(t, expected, *line) 386 }) 387 } 388 } 389 } 390 } 391 392 func TestLogLine_verify(t *testing.T) { 393 type subTest struct { 394 line logLine 395 wantErr error 396 } 397 tests := []struct { 398 name string 399 field string 400 cases []subTest 401 }{ 402 { 403 name: "Vhost", 404 field: "host", 405 cases: []subTest{ 406 {line: logLine{web: web{vhost: "192.168.0.1"}}}, 407 {line: logLine{web: web{vhost: "debian10.debian"}}}, 408 {line: logLine{web: web{vhost: "1ce:1ce::babe"}}}, 409 {line: logLine{web: web{vhost: "localhost"}}}, 410 {line: logLine{web: web{vhost: "invalid_vhost"}}, wantErr: errBadVhost}, 411 {line: logLine{web: web{vhost: "http://192.168.0.1/"}}, wantErr: errBadVhost}, 412 }, 413 }, 414 { 415 name: "Server Port", 416 field: "server_port", 417 cases: []subTest{ 418 {line: logLine{web: web{port: "80"}}}, 419 {line: logLine{web: web{port: "8081"}}}, 420 {line: logLine{web: web{port: "79"}}, wantErr: errBadPort}, 421 {line: logLine{web: web{port: "50000"}}, wantErr: errBadPort}, 422 {line: logLine{web: web{port: "0.0.0.0"}}, wantErr: errBadPort}, 423 }, 424 }, 425 { 426 name: "Scheme", 427 field: "scheme", 428 cases: []subTest{ 429 {line: logLine{web: web{reqScheme: "http"}}}, 430 {line: logLine{web: web{reqScheme: "https"}}}, 431 {line: logLine{web: web{reqScheme: "not_https"}}, wantErr: errBadReqScheme}, 432 {line: logLine{web: web{reqScheme: "HTTP"}}, wantErr: errBadReqScheme}, 433 {line: logLine{web: web{reqScheme: "HTTPS"}}, wantErr: errBadReqScheme}, 434 {line: logLine{web: web{reqScheme: "10"}}, wantErr: errBadReqScheme}, 435 }, 436 }, 437 { 438 name: "Client", 439 field: "remote_addr", 440 cases: []subTest{ 441 {line: logLine{web: web{reqClient: "1.1.1.1"}}}, 442 {line: logLine{web: web{reqClient: "::1"}}}, 443 {line: logLine{web: web{reqClient: "1ce:1ce::babe"}}}, 444 {line: logLine{web: web{reqClient: "localhost"}}}, 445 {line: logLine{web: web{reqClient: "debian10.debian"}}, wantErr: errBadReqClient}, 446 {line: logLine{web: web{reqClient: "invalid"}}, wantErr: errBadReqClient}, 447 }, 448 }, 449 { 450 name: "Request HTTP Method", 451 field: "request_method", 452 cases: []subTest{ 453 {line: logLine{web: web{reqMethod: "GET"}}}, 454 {line: logLine{web: web{reqMethod: "POST"}}}, 455 {line: logLine{web: web{reqMethod: "TRACE"}}}, 456 {line: logLine{web: web{reqMethod: "OPTIONS"}}}, 457 {line: logLine{web: web{reqMethod: "CONNECT"}}}, 458 {line: logLine{web: web{reqMethod: "DELETE"}}}, 459 {line: logLine{web: web{reqMethod: "PUT"}}}, 460 {line: logLine{web: web{reqMethod: "PATCH"}}}, 461 {line: logLine{web: web{reqMethod: "HEAD"}}}, 462 {line: logLine{web: web{reqMethod: "MKCOL"}}}, 463 {line: logLine{web: web{reqMethod: "PROPFIND"}}}, 464 {line: logLine{web: web{reqMethod: "MOVE"}}}, 465 {line: logLine{web: web{reqMethod: "SEARCH"}}}, 466 {line: logLine{web: web{reqMethod: "Get"}}, wantErr: errBadReqMethod}, 467 {line: logLine{web: web{reqMethod: "get"}}, wantErr: errBadReqMethod}, 468 }, 469 }, 470 { 471 name: "Request URL", 472 field: "request_uri", 473 cases: []subTest{ 474 {line: logLine{web: web{reqURL: "/"}}}, 475 {line: logLine{web: web{reqURL: "/status?full&json"}}}, 476 {line: logLine{web: web{reqURL: "/icons/openlogo-75.png"}}}, 477 {line: logLine{web: web{reqURL: "status?full&json"}}}, 478 {line: logLine{web: web{reqURL: "\"req_url=/ \""}}}, 479 {line: logLine{web: web{reqURL: "http://192.168.0.1/"}}}, 480 {line: logLine{web: web{reqURL: ""}}}, 481 }, 482 }, 483 { 484 name: "Request HTTP Protocol", 485 field: "server_protocol", 486 cases: []subTest{ 487 {line: logLine{web: web{reqProto: "1"}}}, 488 {line: logLine{web: web{reqProto: "1.0"}}}, 489 {line: logLine{web: web{reqProto: "1.1"}}}, 490 {line: logLine{web: web{reqProto: "2.0"}}}, 491 {line: logLine{web: web{reqProto: "2"}}}, 492 {line: logLine{web: web{reqProto: "0.9"}}, wantErr: errBadReqProto}, 493 {line: logLine{web: web{reqProto: "1.1.1"}}, wantErr: errBadReqProto}, 494 {line: logLine{web: web{reqProto: "2.2"}}, wantErr: errBadReqProto}, 495 {line: logLine{web: web{reqProto: "localhost"}}, wantErr: errBadReqProto}, 496 }, 497 }, 498 { 499 name: "Response Status Code", 500 field: "status", 501 cases: []subTest{ 502 {line: logLine{web: web{respCode: 100}}}, 503 {line: logLine{web: web{respCode: 200}}}, 504 {line: logLine{web: web{respCode: 300}}}, 505 {line: logLine{web: web{respCode: 400}}}, 506 {line: logLine{web: web{respCode: 500}}}, 507 {line: logLine{web: web{respCode: 600}}}, 508 {line: logLine{web: web{respCode: -1}}, wantErr: errBadRespCode}, 509 {line: logLine{web: web{respCode: 99}}, wantErr: errBadRespCode}, 510 {line: logLine{web: web{respCode: 601}}, wantErr: errBadRespCode}, 511 }, 512 }, 513 { 514 name: "Request size", 515 field: "request_length", 516 cases: []subTest{ 517 {line: logLine{web: web{reqSize: 0}}}, 518 {line: logLine{web: web{reqSize: 100}}}, 519 {line: logLine{web: web{reqSize: 1000000}}}, 520 {line: logLine{web: web{reqSize: -1}}, wantErr: errBadReqSize}, 521 }, 522 }, 523 { 524 name: "Response size", 525 field: "bytes_sent", 526 cases: []subTest{ 527 {line: logLine{web: web{respSize: 0}}}, 528 {line: logLine{web: web{respSize: 100}}}, 529 {line: logLine{web: web{respSize: 1000000}}}, 530 {line: logLine{web: web{respSize: -1}}, wantErr: errBadRespSize}, 531 }, 532 }, 533 { 534 name: "Request Processing Time", 535 field: "request_time", 536 cases: []subTest{ 537 {line: logLine{web: web{reqProcTime: 0}}}, 538 {line: logLine{web: web{reqProcTime: 100}}}, 539 {line: logLine{web: web{reqProcTime: 1000.123}}}, 540 {line: logLine{web: web{reqProcTime: -1}}, wantErr: errBadReqProcTime}, 541 }, 542 }, 543 { 544 name: "Upstream Response Time", 545 field: "upstream_response_time", 546 cases: []subTest{ 547 {line: logLine{web: web{upsRespTime: 0}}}, 548 {line: logLine{web: web{upsRespTime: 100}}}, 549 {line: logLine{web: web{upsRespTime: 1000.123}}}, 550 {line: logLine{web: web{upsRespTime: -1}}, wantErr: errBadUpsRespTime}, 551 }, 552 }, 553 { 554 name: "SSL Protocol", 555 field: "ssl_protocol", 556 cases: []subTest{ 557 {line: logLine{web: web{sslProto: "SSLv3"}}}, 558 {line: logLine{web: web{sslProto: "SSLv2"}}}, 559 {line: logLine{web: web{sslProto: "TLSv1"}}}, 560 {line: logLine{web: web{sslProto: "TLSv1.1"}}}, 561 {line: logLine{web: web{sslProto: "TLSv1.2"}}}, 562 {line: logLine{web: web{sslProto: "TLSv1.3"}}}, 563 {line: logLine{web: web{sslProto: "invalid"}}, wantErr: errBadSSLProto}, 564 }, 565 }, 566 { 567 name: "SSL Cipher Suite", 568 field: "ssl_cipher", 569 cases: []subTest{ 570 {line: logLine{web: web{sslCipherSuite: "ECDHE-RSA-AES256-SHA"}}}, 571 {line: logLine{web: web{sslCipherSuite: "DHE-RSA-AES256-SHA"}}}, 572 {line: logLine{web: web{sslCipherSuite: "AES256-SHA"}}}, 573 {line: logLine{web: web{sslCipherSuite: "TLS_AES_256_GCM_SHA384"}}}, 574 {line: logLine{web: web{sslCipherSuite: "invalid"}}, wantErr: errBadSSLCipherSuite}, 575 }, 576 }, 577 { 578 name: "Custom Fields", 579 field: "custom", 580 cases: []subTest{ 581 {line: logLine{custom: custom{values: []customValue{{name: "custom", value: "POST"}}}}}, 582 {line: logLine{custom: custom{values: []customValue{{name: "custom", value: "/example.com"}}}}}, 583 {line: logLine{custom: custom{values: []customValue{{name: "custom", value: "0.333,0.444,0.555"}}}}}, 584 }, 585 }, 586 { 587 name: "Empty Line", 588 cases: []subTest{ 589 {line: emptyLogLine, wantErr: errEmptyLine}, 590 }, 591 }, 592 } 593 594 for _, tt := range tests { 595 for i, tc := range tt.cases { 596 name := fmt.Sprintf("[%s:%d]field='%s'", tt.name, i+1, tt.field) 597 598 t.Run(name, func(t *testing.T) { 599 line := prepareLogLine(tt.field, tc.line) 600 601 err := line.verify() 602 603 if tc.wantErr != nil { 604 require.Error(t, err) 605 assert.Truef(t, errors.Is(err, tc.wantErr), "expected '%v' error, got '%v'", tc.wantErr, err) 606 } else { 607 assert.NoError(t, err) 608 } 609 }) 610 } 611 } 612 } 613 614 func prepareLogLine(field string, template logLine) logLine { 615 if template.empty() { 616 return *newEmptyLogLineWithFields() 617 } 618 619 line := newEmptyLogLineWithFields() 620 line.reset() 621 622 switch field { 623 case "host", "http_host", "v": 624 line.vhost = template.vhost 625 case "server_port", "p": 626 line.port = template.port 627 case "host:$server_port", "v:%p": 628 line.vhost = template.vhost 629 line.port = template.port 630 case "scheme": 631 line.reqScheme = template.reqScheme 632 case "remote_addr", "a", "h": 633 line.reqClient = template.reqClient 634 case "request", "r": 635 line.reqMethod = template.reqMethod 636 line.reqURL = template.reqURL 637 line.reqProto = template.reqProto 638 case "request_method", "m": 639 line.reqMethod = template.reqMethod 640 case "request_uri", "U": 641 line.reqURL = template.reqURL 642 case "server_protocol", "H": 643 line.reqProto = template.reqProto 644 case "status", "s", ">s": 645 line.respCode = template.respCode 646 case "request_length", "I": 647 line.reqSize = template.reqSize 648 case "bytes_sent", "body_bytes_sent", "b", "O", "B": 649 line.respSize = template.respSize 650 case "request_time", "D": 651 line.reqProcTime = template.reqProcTime 652 case "upstream_response_time": 653 line.upsRespTime = template.upsRespTime 654 case "ssl_protocol": 655 line.sslProto = template.sslProto 656 case "ssl_cipher": 657 line.sslCipherSuite = template.sslCipherSuite 658 default: 659 line.custom.values = template.custom.values 660 } 661 return *line 662 } 663 664 func newEmptyLogLineWithFields() *logLine { 665 l := newEmptyLogLine() 666 l.custom.fields = map[string]struct{}{"custom": {}} 667 return l 668 }