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  }