summaryrefslogtreecommitdiffstats
path: root/src/go/collectors/go.d.plugin/modules/weblog
diff options
context:
space:
mode:
Diffstat (limited to '')
l---------src/go/collectors/go.d.plugin/modules/weblog/README.md1
-rw-r--r--src/go/collectors/go.d.plugin/modules/weblog/charts.go890
-rw-r--r--src/go/collectors/go.d.plugin/modules/weblog/collect.go564
-rw-r--r--src/go/collectors/go.d.plugin/modules/weblog/config_schema.json453
-rw-r--r--src/go/collectors/go.d.plugin/modules/weblog/init.go197
-rw-r--r--src/go/collectors/go.d.plugin/modules/weblog/integrations/web_server_log_files.md375
-rw-r--r--src/go/collectors/go.d.plugin/modules/weblog/logline.go617
-rw-r--r--src/go/collectors/go.d.plugin/modules/weblog/logline_test.go669
-rw-r--r--src/go/collectors/go.d.plugin/modules/weblog/metadata.yaml533
-rw-r--r--src/go/collectors/go.d.plugin/modules/weblog/metrics.go188
-rw-r--r--src/go/collectors/go.d.plugin/modules/weblog/parser.go167
-rw-r--r--src/go/collectors/go.d.plugin/modules/weblog/parser_test.go224
-rw-r--r--src/go/collectors/go.d.plugin/modules/weblog/testdata/common.log500
-rw-r--r--src/go/collectors/go.d.plugin/modules/weblog/testdata/config.json64
-rw-r--r--src/go/collectors/go.d.plugin/modules/weblog/testdata/config.yaml39
-rw-r--r--src/go/collectors/go.d.plugin/modules/weblog/testdata/custom.log100
-rw-r--r--src/go/collectors/go.d.plugin/modules/weblog/testdata/custom_time_fields.log72
-rw-r--r--src/go/collectors/go.d.plugin/modules/weblog/testdata/full.log500
-rw-r--r--src/go/collectors/go.d.plugin/modules/weblog/testdata/u_ex221107.log168
-rw-r--r--src/go/collectors/go.d.plugin/modules/weblog/weblog.go168
-rw-r--r--src/go/collectors/go.d.plugin/modules/weblog/weblog_test.go1502
21 files changed, 7991 insertions, 0 deletions
diff --git a/src/go/collectors/go.d.plugin/modules/weblog/README.md b/src/go/collectors/go.d.plugin/modules/weblog/README.md
new file mode 120000
index 000000000..9da3f21c2
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/weblog/README.md
@@ -0,0 +1 @@
+integrations/web_server_log_files.md \ No newline at end of file
diff --git a/src/go/collectors/go.d.plugin/modules/weblog/charts.go b/src/go/collectors/go.d.plugin/modules/weblog/charts.go
new file mode 100644
index 000000000..749a26ce7
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/weblog/charts.go
@@ -0,0 +1,890 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package weblog
+
+import (
+ "errors"
+ "fmt"
+
+ "github.com/netdata/netdata/go/go.d.plugin/agent/module"
+)
+
+type (
+ Charts = module.Charts
+ Chart = module.Chart
+ Dims = module.Dims
+ Dim = module.Dim
+)
+
+const (
+ prioReqTotal = module.Priority + iota
+ prioReqExcluded
+ prioReqType
+
+ prioRespCodesClass
+ prioRespCodes
+ prioRespCodes1xx
+ prioRespCodes2xx
+ prioRespCodes3xx
+ prioRespCodes4xx
+ prioRespCodes5xx
+
+ prioBandwidth
+
+ prioReqProcTime
+ prioRespTimeHist
+ prioUpsRespTime
+ prioUpsRespTimeHist
+
+ prioUniqIP
+
+ prioReqVhost
+ prioReqPort
+ prioReqScheme
+ prioReqMethod
+ prioReqVersion
+ prioReqIPProto
+ prioReqSSLProto
+ prioReqSSLCipherSuite
+
+ prioReqCustomFieldPattern // chart per custom field, alphabetical order
+ prioReqCustomTimeField // chart per custom time field, alphabetical order
+ prioReqCustomTimeFieldHist // histogram chart per custom time field
+ prioReqURLPattern
+ prioURLPatternStats
+
+ prioReqCustomNumericFieldSummary // 3 charts per url pattern, alphabetical order
+)
+
+// NOTE: inconsistency with python web_log
+// TODO: current histogram charts are misleading in netdata
+
+// Requests
+var (
+ reqTotal = Chart{
+ ID: "requests",
+ Title: "Total Requests",
+ Units: "requests/s",
+ Fam: "requests",
+ Ctx: "web_log.requests",
+ Priority: prioReqTotal,
+ Dims: Dims{
+ {ID: "requests", Algo: module.Incremental},
+ },
+ }
+ reqExcluded = Chart{
+ ID: "excluded_requests",
+ Title: "Excluded Requests",
+ Units: "requests/s",
+ Fam: "requests",
+ Ctx: "web_log.excluded_requests",
+ Type: module.Stacked,
+ Priority: prioReqExcluded,
+ Dims: Dims{
+ {ID: "req_unmatched", Name: "unmatched", Algo: module.Incremental},
+ },
+ }
+ // netdata specific grouping
+ reqTypes = Chart{
+ ID: "requests_by_type",
+ Title: "Requests By Type",
+ Units: "requests/s",
+ Fam: "requests",
+ Ctx: "web_log.type_requests",
+ Type: module.Stacked,
+ Priority: prioReqType,
+ Dims: Dims{
+ {ID: "req_type_success", Name: "success", Algo: module.Incremental},
+ {ID: "req_type_bad", Name: "bad", Algo: module.Incremental},
+ {ID: "req_type_redirect", Name: "redirect", Algo: module.Incremental},
+ {ID: "req_type_error", Name: "error", Algo: module.Incremental},
+ },
+ }
+)
+
+// Responses
+var (
+ respCodeClass = Chart{
+ ID: "responses_by_status_code_class",
+ Title: "Responses By Status Code Class",
+ Units: "responses/s",
+ Fam: "responses",
+ Ctx: "web_log.status_code_class_responses",
+ Type: module.Stacked,
+ Priority: prioRespCodesClass,
+ Dims: Dims{
+ {ID: "resp_2xx", Name: "2xx", Algo: module.Incremental},
+ {ID: "resp_5xx", Name: "5xx", Algo: module.Incremental},
+ {ID: "resp_3xx", Name: "3xx", Algo: module.Incremental},
+ {ID: "resp_4xx", Name: "4xx", Algo: module.Incremental},
+ {ID: "resp_1xx", Name: "1xx", Algo: module.Incremental},
+ },
+ }
+ respCodes = Chart{
+ ID: "responses_by_status_code",
+ Title: "Responses By Status Code",
+ Units: "responses/s",
+ Fam: "responses",
+ Ctx: "web_log.status_code_responses",
+ Type: module.Stacked,
+ Priority: prioRespCodes,
+ }
+ respCodes1xx = Chart{
+ ID: "status_code_class_1xx_responses",
+ Title: "Informational Responses By Status Code",
+ Units: "responses/s",
+ Fam: "responses",
+ Ctx: "web_log.status_code_class_1xx_responses",
+ Type: module.Stacked,
+ Priority: prioRespCodes1xx,
+ }
+ respCodes2xx = Chart{
+ ID: "status_code_class_2xx_responses",
+ Title: "Successful Responses By Status Code",
+ Units: "responses/s",
+ Fam: "responses",
+ Ctx: "web_log.status_code_class_2xx_responses",
+ Type: module.Stacked,
+ Priority: prioRespCodes2xx,
+ }
+ respCodes3xx = Chart{
+ ID: "status_code_class_3xx_responses",
+ Title: "Redirects Responses By Status Code",
+ Units: "responses/s",
+ Fam: "responses",
+ Ctx: "web_log.status_code_class_3xx_responses",
+ Type: module.Stacked,
+ Priority: prioRespCodes3xx,
+ }
+ respCodes4xx = Chart{
+ ID: "status_code_class_4xx_responses",
+ Title: "Client Errors Responses By Status Code",
+ Units: "responses/s",
+ Fam: "responses",
+ Ctx: "web_log.status_code_class_4xx_responses",
+ Type: module.Stacked,
+ Priority: prioRespCodes4xx,
+ }
+ respCodes5xx = Chart{
+ ID: "status_code_class_5xx_responses",
+ Title: "Server Errors Responses By Status Code",
+ Units: "responses/s",
+ Fam: "responses",
+ Ctx: "web_log.status_code_class_5xx_responses",
+ Type: module.Stacked,
+ Priority: prioRespCodes5xx,
+ }
+)
+
+// Bandwidth
+var (
+ bandwidth = Chart{
+ ID: "bandwidth",
+ Title: "Bandwidth",
+ Units: "kilobits/s",
+ Fam: "bandwidth",
+ Ctx: "web_log.bandwidth",
+ Type: module.Area,
+ Priority: prioBandwidth,
+ Dims: Dims{
+ {ID: "bytes_received", Name: "received", Algo: module.Incremental, Mul: 8, Div: 1000},
+ {ID: "bytes_sent", Name: "sent", Algo: module.Incremental, Mul: -8, Div: 1000},
+ },
+ }
+)
+
+// Timings
+var (
+ reqProcTime = Chart{
+ ID: "request_processing_time",
+ Title: "Request Processing Time",
+ Units: "milliseconds",
+ Fam: "timings",
+ Ctx: "web_log.request_processing_time",
+ Priority: prioReqProcTime,
+ Dims: Dims{
+ {ID: "req_proc_time_min", Name: "min", Div: 1000},
+ {ID: "req_proc_time_max", Name: "max", Div: 1000},
+ {ID: "req_proc_time_avg", Name: "avg", Div: 1000},
+ },
+ }
+ reqProcTimeHist = Chart{
+ ID: "requests_processing_time_histogram",
+ Title: "Requests Processing Time Histogram",
+ Units: "requests/s",
+ Fam: "timings",
+ Ctx: "web_log.requests_processing_time_histogram",
+ Priority: prioRespTimeHist,
+ }
+)
+
+// Upstream
+var (
+ upsRespTime = Chart{
+ ID: "upstream_response_time",
+ Title: "Upstream Response Time",
+ Units: "milliseconds",
+ Fam: "timings",
+ Ctx: "web_log.upstream_response_time",
+ Priority: prioUpsRespTime,
+ Dims: Dims{
+ {ID: "upstream_resp_time_min", Name: "min", Div: 1000},
+ {ID: "upstream_resp_time_max", Name: "max", Div: 1000},
+ {ID: "upstream_resp_time_avg", Name: "avg", Div: 1000},
+ },
+ }
+ upsRespTimeHist = Chart{
+ ID: "upstream_responses_time_histogram",
+ Title: "Upstream Responses Time Histogram",
+ Units: "responses/s",
+ Fam: "timings",
+ Ctx: "web_log.upstream_responses_time_histogram",
+ Priority: prioUpsRespTimeHist,
+ }
+)
+
+// Clients
+var (
+ uniqIPsCurPoll = Chart{
+ ID: "current_poll_uniq_clients",
+ Title: "Current Poll Unique Clients",
+ Units: "clients",
+ Fam: "client",
+ Ctx: "web_log.current_poll_uniq_clients",
+ Type: module.Stacked,
+ Priority: prioUniqIP,
+ Dims: Dims{
+ {ID: "uniq_ipv4", Name: "ipv4", Algo: module.Absolute},
+ {ID: "uniq_ipv6", Name: "ipv6", Algo: module.Absolute},
+ },
+ }
+)
+
+// Request By N
+var (
+ reqByVhost = Chart{
+ ID: "requests_by_vhost",
+ Title: "Requests By Vhost",
+ Units: "requests/s",
+ Fam: "vhost",
+ Ctx: "web_log.vhost_requests",
+ Type: module.Stacked,
+ Priority: prioReqVhost,
+ }
+ reqByPort = Chart{
+ ID: "requests_by_port",
+ Title: "Requests By Port",
+ Units: "requests/s",
+ Fam: "port",
+ Ctx: "web_log.port_requests",
+ Type: module.Stacked,
+ Priority: prioReqPort,
+ }
+ reqByScheme = Chart{
+ ID: "requests_by_scheme",
+ Title: "Requests By Scheme",
+ Units: "requests/s",
+ Fam: "scheme",
+ Ctx: "web_log.scheme_requests",
+ Type: module.Stacked,
+ Priority: prioReqScheme,
+ Dims: Dims{
+ {ID: "req_http_scheme", Name: "http", Algo: module.Incremental},
+ {ID: "req_https_scheme", Name: "https", Algo: module.Incremental},
+ },
+ }
+ reqByMethod = Chart{
+ ID: "requests_by_http_method",
+ Title: "Requests By HTTP Method",
+ Units: "requests/s",
+ Fam: "http method",
+ Ctx: "web_log.http_method_requests",
+ Type: module.Stacked,
+ Priority: prioReqMethod,
+ }
+ reqByVersion = Chart{
+ ID: "requests_by_http_version",
+ Title: "Requests By HTTP Version",
+ Units: "requests/s",
+ Fam: "http version",
+ Ctx: "web_log.http_version_requests",
+ Type: module.Stacked,
+ Priority: prioReqVersion,
+ }
+ reqByIPProto = Chart{
+ ID: "requests_by_ip_proto",
+ Title: "Requests By IP Protocol",
+ Units: "requests/s",
+ Fam: "ip proto",
+ Ctx: "web_log.ip_proto_requests",
+ Type: module.Stacked,
+ Priority: prioReqIPProto,
+ Dims: Dims{
+ {ID: "req_ipv4", Name: "ipv4", Algo: module.Incremental},
+ {ID: "req_ipv6", Name: "ipv6", Algo: module.Incremental},
+ },
+ }
+ reqBySSLProto = Chart{
+ ID: "requests_by_ssl_proto",
+ Title: "Requests By SSL Connection Protocol",
+ Units: "requests/s",
+ Fam: "ssl conn",
+ Ctx: "web_log.ssl_proto_requests",
+ Type: module.Stacked,
+ Priority: prioReqSSLProto,
+ }
+ reqBySSLCipherSuite = Chart{
+ ID: "requests_by_ssl_cipher_suite",
+ Title: "Requests By SSL Connection Cipher Suite",
+ Units: "requests/s",
+ Fam: "ssl conn",
+ Ctx: "web_log.ssl_cipher_suite_requests",
+ Type: module.Stacked,
+ Priority: prioReqSSLCipherSuite,
+ }
+)
+
+// Request By N Patterns
+var (
+ reqByURLPattern = Chart{
+ ID: "requests_by_url_pattern",
+ Title: "URL Field Requests By Pattern",
+ Units: "requests/s",
+ Fam: "url ptn",
+ Ctx: "web_log.url_pattern_requests",
+ Type: module.Stacked,
+ Priority: prioReqURLPattern,
+ }
+ reqByCustomFieldPattern = Chart{
+ ID: "custom_field_%s_requests_by_pattern",
+ Title: "Custom Field %s Requests By Pattern",
+ Units: "requests/s",
+ Fam: "custom field ptn",
+ Ctx: "web_log.custom_field_pattern_requests",
+ Type: module.Stacked,
+ Priority: prioReqCustomFieldPattern,
+ }
+)
+
+// custom time field
+var (
+ reqByCustomTimeField = Chart{
+ ID: "custom_time_field_%s_summary",
+ Title: `Custom Time Field "%s" Summary`,
+ Units: "milliseconds",
+ Fam: "custom time field",
+ Ctx: "web_log.custom_time_field_summary",
+ Priority: prioReqCustomTimeField,
+ Dims: Dims{
+ {ID: "custom_time_field_%s_time_min", Name: "min", Div: 1000},
+ {ID: "custom_time_field_%s_time_max", Name: "max", Div: 1000},
+ {ID: "custom_time_field_%s_time_avg", Name: "avg", Div: 1000},
+ },
+ }
+ reqByCustomTimeFieldHist = Chart{
+ ID: "custom_time_field_%s_histogram",
+ Title: `Custom Time Field "%s" Histogram`,
+ Units: "observations",
+ Fam: "custom time field",
+ Ctx: "web_log.custom_time_field_histogram",
+ Priority: prioReqCustomTimeFieldHist,
+ }
+)
+
+var (
+ customNumericFieldSummaryChartTmpl = Chart{
+ ID: "custom_numeric_field_%s_summary",
+ Title: "Custom Numeric Field Summary",
+ Units: "",
+ Fam: "custom numeric fields",
+ Ctx: "web_log.custom_numeric_field_%s_summary",
+ Priority: prioReqCustomNumericFieldSummary,
+ Dims: Dims{
+ {ID: "custom_numeric_field_%s_summary_min", Name: "min"},
+ {ID: "custom_numeric_field_%s_summary_max", Name: "max"},
+ {ID: "custom_numeric_field_%s_summary_avg", Name: "avg"},
+ },
+ }
+)
+
+// URL pattern stats
+var (
+ urlPatternRespCodes = Chart{
+ ID: "url_pattern_%s_responses_by_status_code",
+ Title: "Responses By Status Code",
+ Units: "responses/s",
+ Fam: "url ptn %s",
+ Ctx: "web_log.url_pattern_status_code_responses",
+ Type: module.Stacked,
+ Priority: prioURLPatternStats,
+ }
+ urlPatternReqMethods = Chart{
+ ID: "url_pattern_%s_requests_by_http_method",
+ Title: "Requests By HTTP Method",
+ Units: "requests/s",
+ Fam: "url ptn %s",
+ Ctx: "web_log.url_pattern_http_method_requests",
+ Type: module.Stacked,
+ Priority: prioURLPatternStats + 1,
+ }
+ urlPatternBandwidth = Chart{
+ ID: "url_pattern_%s_bandwidth",
+ Title: "Bandwidth",
+ Units: "kilobits/s",
+ Fam: "url ptn %s",
+ Ctx: "web_log.url_pattern_bandwidth",
+ Type: module.Area,
+ Priority: prioURLPatternStats + 2,
+ Dims: Dims{
+ {ID: "url_ptn_%s_bytes_received", Name: "received", Algo: module.Incremental, Mul: 8, Div: 1000},
+ {ID: "url_ptn_%s_bytes_sent", Name: "sent", Algo: module.Incremental, Mul: -8, Div: 1000},
+ },
+ }
+ urlPatternReqProcTime = Chart{
+ ID: "url_pattern_%s_request_processing_time",
+ Title: "Request Processing Time",
+ Units: "milliseconds",
+ Fam: "url ptn %s",
+ Ctx: "web_log.url_pattern_request_processing_time",
+ Priority: prioURLPatternStats + 3,
+ Dims: Dims{
+ {ID: "url_ptn_%s_req_proc_time_min", Name: "min", Div: 1000},
+ {ID: "url_ptn_%s_req_proc_time_max", Name: "max", Div: 1000},
+ {ID: "url_ptn_%s_req_proc_time_avg", Name: "avg", Div: 1000},
+ },
+ }
+)
+
+func newReqProcTimeHistChart(histogram []float64) (*Chart, error) {
+ chart := reqProcTimeHist.Copy()
+ for i, v := range histogram {
+ dim := &Dim{
+ ID: fmt.Sprintf("req_proc_time_hist_bucket_%d", i+1),
+ Name: fmt.Sprintf("%.3f", v),
+ Algo: module.Incremental,
+ }
+ if err := chart.AddDim(dim); err != nil {
+ return nil, err
+ }
+ }
+ if err := chart.AddDim(&Dim{
+ ID: "req_proc_time_hist_count",
+ Name: "+Inf",
+ Algo: module.Incremental,
+ }); err != nil {
+ return nil, err
+ }
+ return chart, nil
+}
+
+func newUpsRespTimeHistChart(histogram []float64) (*Chart, error) {
+ chart := upsRespTimeHist.Copy()
+ for i, v := range histogram {
+ dim := &Dim{
+ ID: fmt.Sprintf("upstream_resp_time_hist_bucket_%d", i+1),
+ Name: fmt.Sprintf("%.3f", v),
+ Algo: module.Incremental,
+ }
+ if err := chart.AddDim(dim); err != nil {
+ return nil, err
+ }
+ }
+ if err := chart.AddDim(&Dim{
+ ID: "upstream_resp_time_hist_count",
+ Name: "+Inf",
+ Algo: module.Incremental,
+ }); err != nil {
+ return nil, err
+ }
+ return chart, nil
+}
+
+func newURLPatternChart(patterns []userPattern) (*Chart, error) {
+ chart := reqByURLPattern.Copy()
+ for _, p := range patterns {
+ dim := &Dim{
+ ID: "req_url_ptn_" + p.Name,
+ Name: p.Name,
+ Algo: module.Incremental,
+ }
+ if err := chart.AddDim(dim); err != nil {
+ return nil, err
+ }
+ }
+ return chart, nil
+}
+
+func newURLPatternRespCodesChart(name string) *Chart {
+ chart := urlPatternRespCodes.Copy()
+ chart.ID = fmt.Sprintf(chart.ID, name)
+ chart.Fam = fmt.Sprintf(chart.Fam, name)
+ return chart
+}
+
+func newURLPatternReqMethodsChart(name string) *Chart {
+ chart := urlPatternReqMethods.Copy()
+ chart.ID = fmt.Sprintf(chart.ID, name)
+ chart.Fam = fmt.Sprintf(chart.Fam, name)
+ return chart
+}
+
+func newURLPatternBandwidthChart(name string) *Chart {
+ chart := urlPatternBandwidth.Copy()
+ chart.ID = fmt.Sprintf(chart.ID, name)
+ chart.Fam = fmt.Sprintf(chart.Fam, name)
+ for _, d := range chart.Dims {
+ d.ID = fmt.Sprintf(d.ID, name)
+ }
+ return chart
+}
+
+func newURLPatternReqProcTimeChart(name string) *Chart {
+ chart := urlPatternReqProcTime.Copy()
+ chart.ID = fmt.Sprintf(chart.ID, name)
+ chart.Fam = fmt.Sprintf(chart.Fam, name)
+ for _, d := range chart.Dims {
+ d.ID = fmt.Sprintf(d.ID, name)
+ }
+ return chart
+}
+
+func newCustomFieldCharts(fields []customField) (Charts, error) {
+ charts := Charts{}
+ for _, f := range fields {
+ chart, err := newCustomFieldChart(f)
+ if err != nil {
+ return nil, err
+ }
+ if err := charts.Add(chart); err != nil {
+ return nil, err
+ }
+ }
+ return charts, nil
+}
+
+func newCustomFieldChart(f customField) (*Chart, error) {
+ chart := reqByCustomFieldPattern.Copy()
+ chart.ID = fmt.Sprintf(chart.ID, f.Name)
+ chart.Title = fmt.Sprintf(chart.Title, f.Name)
+ for _, p := range f.Patterns {
+ dim := &Dim{
+ ID: fmt.Sprintf("custom_field_%s_%s", f.Name, p.Name),
+ Name: p.Name,
+ Algo: module.Incremental,
+ }
+ if err := chart.AddDim(dim); err != nil {
+ return nil, err
+ }
+ }
+ return chart, nil
+}
+
+func newCustomTimeFieldCharts(fields []customTimeField) (Charts, error) {
+ charts := Charts{}
+ for i, f := range fields {
+ chartTime, err := newCustomTimeFieldChart(f)
+ if err != nil {
+ return nil, err
+ }
+ chartTime.Priority += i
+ if err := charts.Add(chartTime); err != nil {
+ return nil, err
+ }
+ if len(f.Histogram) < 1 {
+ continue
+ }
+
+ chartHist, err := newCustomTimeFieldHistChart(f)
+ if err != nil {
+ return nil, err
+ }
+ chartHist.Priority += i
+
+ if err := charts.Add(chartHist); err != nil {
+ return nil, err
+ }
+ }
+ return charts, nil
+}
+
+func newCustomTimeFieldChart(f customTimeField) (*Chart, error) {
+ chart := reqByCustomTimeField.Copy()
+ chart.ID = fmt.Sprintf(chart.ID, f.Name)
+ chart.Title = fmt.Sprintf(chart.Title, f.Name)
+ for _, d := range chart.Dims {
+ d.ID = fmt.Sprintf(d.ID, f.Name)
+ }
+ return chart, nil
+}
+
+func newCustomTimeFieldHistChart(f customTimeField) (*Chart, error) {
+ chart := reqByCustomTimeFieldHist.Copy()
+ chart.ID = fmt.Sprintf(chart.ID, f.Name)
+ chart.Title = fmt.Sprintf(chart.Title, f.Name)
+ for i, v := range f.Histogram {
+ dim := &Dim{
+ ID: fmt.Sprintf("custom_time_field_%s_time_hist_bucket_%d", f.Name, i+1),
+ Name: fmt.Sprintf("%.3f", v),
+ Algo: module.Incremental,
+ }
+ if err := chart.AddDim(dim); err != nil {
+ return nil, err
+ }
+ }
+ if err := chart.AddDim(&Dim{
+ ID: fmt.Sprintf("custom_time_field_%s_time_hist_count", f.Name),
+ Name: "+Inf",
+ Algo: module.Incremental,
+ }); err != nil {
+ return nil, err
+ }
+ return chart, nil
+}
+
+func (w *WebLog) createCharts(line *logLine) error {
+ if line.empty() {
+ return errors.New("empty line")
+ }
+ w.charts = nil
+ // Following charts are created during runtime:
+ // - reqBySSLProto, reqBySSLCipherSuite - it is likely line has no SSL stuff at this moment
+ charts := &Charts{
+ reqTotal.Copy(),
+ reqExcluded.Copy(),
+ }
+ if line.hasVhost() {
+ if err := addVhostCharts(charts); err != nil {
+ return err
+ }
+ }
+ if line.hasPort() {
+ if err := addPortCharts(charts); err != nil {
+ return err
+ }
+ }
+ if line.hasReqScheme() {
+ if err := addSchemeCharts(charts); err != nil {
+ return err
+ }
+ }
+ if line.hasReqClient() {
+ if err := addClientCharts(charts); err != nil {
+ return err
+ }
+ }
+ if line.hasReqMethod() {
+ if err := addMethodCharts(charts, w.URLPatterns); err != nil {
+ return err
+ }
+ }
+ if line.hasReqURL() {
+ if err := addURLCharts(charts, w.URLPatterns); err != nil {
+ return err
+ }
+ }
+ if line.hasReqProto() {
+ if err := addReqProtoCharts(charts); err != nil {
+ return err
+ }
+ }
+ if line.hasRespCode() {
+ if err := addRespCodesCharts(charts, w.GroupRespCodes); err != nil {
+ return err
+ }
+ }
+ if line.hasReqSize() || line.hasRespSize() {
+ if err := addBandwidthCharts(charts, w.URLPatterns); err != nil {
+ return err
+ }
+ }
+ if line.hasReqProcTime() {
+ if err := addReqProcTimeCharts(charts, w.Histogram, w.URLPatterns); err != nil {
+ return err
+ }
+ }
+ if line.hasUpsRespTime() {
+ if err := addUpstreamRespTimeCharts(charts, w.Histogram); err != nil {
+ return err
+ }
+ }
+ if line.hasCustomFields() {
+ if len(w.CustomFields) > 0 {
+ if err := addCustomFieldsCharts(charts, w.CustomFields); err != nil {
+ return err
+ }
+ }
+ if len(w.CustomTimeFields) > 0 {
+ if err := addCustomTimeFieldsCharts(charts, w.CustomTimeFields); err != nil {
+ return err
+ }
+ }
+ if len(w.CustomNumericFields) > 0 {
+ if err := addCustomNumericFieldsCharts(charts, w.CustomNumericFields); err != nil {
+ return err
+ }
+ }
+ }
+
+ w.charts = charts
+
+ return nil
+}
+
+func addVhostCharts(charts *Charts) error {
+ return charts.Add(reqByVhost.Copy())
+}
+
+func addPortCharts(charts *Charts) error {
+ return charts.Add(reqByPort.Copy())
+}
+
+func addSchemeCharts(charts *Charts) error {
+ return charts.Add(reqByScheme.Copy())
+}
+
+func addClientCharts(charts *Charts) error {
+ if err := charts.Add(reqByIPProto.Copy()); err != nil {
+ return err
+ }
+ return charts.Add(uniqIPsCurPoll.Copy())
+}
+
+func addMethodCharts(charts *Charts, patterns []userPattern) error {
+ if err := charts.Add(reqByMethod.Copy()); err != nil {
+ return err
+ }
+
+ for _, p := range patterns {
+ chart := newURLPatternReqMethodsChart(p.Name)
+ if err := charts.Add(chart); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func addURLCharts(charts *Charts, patterns []userPattern) error {
+ if len(patterns) == 0 {
+ return nil
+ }
+ chart, err := newURLPatternChart(patterns)
+ if err != nil {
+ return err
+ }
+ if err := charts.Add(chart); err != nil {
+ return err
+ }
+
+ for _, p := range patterns {
+ chart := newURLPatternRespCodesChart(p.Name)
+ if err := charts.Add(chart); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func addReqProtoCharts(charts *Charts) error {
+ return charts.Add(reqByVersion.Copy())
+}
+
+func addRespCodesCharts(charts *Charts, group bool) error {
+ if err := charts.Add(reqTypes.Copy()); err != nil {
+ return err
+ }
+ if err := charts.Add(respCodeClass.Copy()); err != nil {
+ return err
+ }
+ if !group {
+ return charts.Add(respCodes.Copy())
+ }
+ for _, c := range []Chart{respCodes1xx, respCodes2xx, respCodes3xx, respCodes4xx, respCodes5xx} {
+ if err := charts.Add(c.Copy()); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func addBandwidthCharts(charts *Charts, patterns []userPattern) error {
+ if err := charts.Add(bandwidth.Copy()); err != nil {
+ return err
+ }
+
+ for _, p := range patterns {
+ chart := newURLPatternBandwidthChart(p.Name)
+ if err := charts.Add(chart); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func addReqProcTimeCharts(charts *Charts, histogram []float64, patterns []userPattern) error {
+ if err := charts.Add(reqProcTime.Copy()); err != nil {
+ return err
+ }
+ for _, p := range patterns {
+ chart := newURLPatternReqProcTimeChart(p.Name)
+ if err := charts.Add(chart); err != nil {
+ return err
+ }
+ }
+ if len(histogram) == 0 {
+ return nil
+ }
+ chart, err := newReqProcTimeHistChart(histogram)
+ if err != nil {
+ return err
+ }
+ return charts.Add(chart)
+}
+
+func addUpstreamRespTimeCharts(charts *Charts, histogram []float64) error {
+ if err := charts.Add(upsRespTime.Copy()); err != nil {
+ return err
+ }
+ if len(histogram) == 0 {
+ return nil
+ }
+ chart, err := newUpsRespTimeHistChart(histogram)
+ if err != nil {
+ return err
+ }
+ return charts.Add(chart)
+}
+
+func addCustomFieldsCharts(charts *Charts, fields []customField) error {
+ cs, err := newCustomFieldCharts(fields)
+ if err != nil {
+ return err
+ }
+ return charts.Add(cs...)
+}
+
+func addCustomTimeFieldsCharts(charts *Charts, fields []customTimeField) error {
+ cs, err := newCustomTimeFieldCharts(fields)
+ if err != nil {
+ return err
+ }
+ return charts.Add(cs...)
+}
+
+func addCustomNumericFieldsCharts(charts *module.Charts, fields []customNumericField) error {
+ for _, f := range fields {
+ chart := customNumericFieldSummaryChartTmpl.Copy()
+ chart.ID = fmt.Sprintf(chart.ID, f.Name)
+ chart.Units = f.Units
+ chart.Ctx = fmt.Sprintf(chart.Ctx, f.Name)
+ for _, dim := range chart.Dims {
+ dim.ID = fmt.Sprintf(dim.ID, f.Name)
+ dim.Div = f.Divisor
+ }
+
+ if err := charts.Add(chart); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
diff --git a/src/go/collectors/go.d.plugin/modules/weblog/collect.go b/src/go/collectors/go.d.plugin/modules/weblog/collect.go
new file mode 100644
index 000000000..fd7993f26
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/weblog/collect.go
@@ -0,0 +1,564 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package weblog
+
+import (
+ "fmt"
+ "io"
+ "net/http"
+ "runtime"
+ "strconv"
+ "strings"
+
+ "github.com/netdata/netdata/go/go.d.plugin/pkg/logs"
+ "github.com/netdata/netdata/go/go.d.plugin/pkg/stm"
+
+ "github.com/netdata/netdata/go/go.d.plugin/agent/module"
+)
+
+func (w *WebLog) logPanicStackIfAny() {
+ err := recover()
+ if err == nil {
+ return
+ }
+ w.Errorf("[ERROR] %s\n", err)
+ for depth := 0; ; depth++ {
+ _, file, line, ok := runtime.Caller(depth)
+ if !ok {
+ break
+ }
+ w.Errorf("======> %d: %v:%d", depth, file, line)
+ }
+ panic(err)
+}
+
+func (w *WebLog) collect() (map[string]int64, error) {
+ defer w.logPanicStackIfAny()
+ w.mx.reset()
+
+ var mx map[string]int64
+
+ n, err := w.collectLogLines()
+
+ if n > 0 || err == nil {
+ mx = stm.ToMap(w.mx)
+ }
+ return mx, err
+}
+
+func (w *WebLog) collectLogLines() (int, error) {
+ logOnce := true
+ var n int
+ for {
+ w.line.reset()
+ err := w.parser.ReadLine(w.line)
+ if err != nil {
+ if err == io.EOF {
+ return n, nil
+ }
+ if !logs.IsParseError(err) {
+ return n, err
+ }
+ n++
+ if logOnce {
+ w.Infof("unmatched line: %v (parser: %s)", err, w.parser.Info())
+ logOnce = false
+ }
+ w.collectUnmatched()
+ continue
+ }
+ n++
+ if w.line.empty() {
+ w.collectUnmatched()
+ } else {
+ w.collectLogLine()
+ }
+ }
+}
+
+func (w *WebLog) collectLogLine() {
+ // https://github.com/netdata/netdata/issues/17716
+ if w.line.hasReqProcTime() && w.line.respCode == http.StatusSwitchingProtocols {
+ w.line.reqProcTime = emptyNumber
+ }
+ w.mx.Requests.Inc()
+ w.collectVhost()
+ w.collectPort()
+ w.collectReqScheme()
+ w.collectReqClient()
+ w.collectReqMethod()
+ w.collectReqURL()
+ w.collectReqProto()
+ w.collectRespCode()
+ w.collectReqSize()
+ w.collectRespSize()
+ w.collectReqProcTime()
+ w.collectUpsRespTime()
+ w.collectSSLProto()
+ w.collectSSLCipherSuite()
+ w.collectCustomFields()
+}
+
+func (w *WebLog) collectUnmatched() {
+ w.mx.Requests.Inc()
+ w.mx.ReqUnmatched.Inc()
+}
+
+func (w *WebLog) collectVhost() {
+ if !w.line.hasVhost() {
+ return
+ }
+ c, ok := w.mx.ReqVhost.GetP(w.line.vhost)
+ if !ok {
+ w.addDimToVhostChart(w.line.vhost)
+ }
+ c.Inc()
+}
+
+func (w *WebLog) collectPort() {
+ if !w.line.hasPort() {
+ return
+ }
+ c, ok := w.mx.ReqPort.GetP(w.line.port)
+ if !ok {
+ w.addDimToPortChart(w.line.port)
+ }
+ c.Inc()
+}
+
+func (w *WebLog) collectReqClient() {
+ if !w.line.hasReqClient() {
+ return
+ }
+ if strings.ContainsRune(w.line.reqClient, ':') {
+ w.mx.ReqIPv6.Inc()
+ w.mx.UniqueIPv6.Insert(w.line.reqClient)
+ return
+ }
+ // NOTE: count hostname as IPv4 address
+ w.mx.ReqIPv4.Inc()
+ w.mx.UniqueIPv4.Insert(w.line.reqClient)
+}
+
+func (w *WebLog) collectReqScheme() {
+ if !w.line.hasReqScheme() {
+ return
+ }
+ if w.line.reqScheme == "https" {
+ w.mx.ReqHTTPSScheme.Inc()
+ } else {
+ w.mx.ReqHTTPScheme.Inc()
+ }
+}
+
+func (w *WebLog) collectReqMethod() {
+ if !w.line.hasReqMethod() {
+ return
+ }
+ c, ok := w.mx.ReqMethod.GetP(w.line.reqMethod)
+ if !ok {
+ w.addDimToReqMethodChart(w.line.reqMethod)
+ }
+ c.Inc()
+}
+
+func (w *WebLog) collectReqURL() {
+ if !w.line.hasReqURL() {
+ return
+ }
+ for _, p := range w.urlPatterns {
+ if !p.MatchString(w.line.reqURL) {
+ continue
+ }
+ c, _ := w.mx.ReqURLPattern.GetP(p.name)
+ c.Inc()
+
+ w.collectURLPatternStats(p.name)
+ return
+ }
+}
+
+func (w *WebLog) collectReqProto() {
+ if !w.line.hasReqProto() {
+ return
+ }
+ c, ok := w.mx.ReqVersion.GetP(w.line.reqProto)
+ if !ok {
+ w.addDimToReqVersionChart(w.line.reqProto)
+ }
+ c.Inc()
+}
+
+func (w *WebLog) collectRespCode() {
+ if !w.line.hasRespCode() {
+ return
+ }
+
+ code := w.line.respCode
+ switch {
+ case code >= 100 && code < 300, code == 304, code == 401:
+ w.mx.ReqSuccess.Inc()
+ case code >= 300 && code < 400:
+ w.mx.ReqRedirect.Inc()
+ case code >= 400 && code < 500:
+ w.mx.ReqBad.Inc()
+ case code >= 500 && code < 600:
+ w.mx.ReqError.Inc()
+ }
+
+ switch code / 100 {
+ case 1:
+ w.mx.Resp1xx.Inc()
+ case 2:
+ w.mx.Resp2xx.Inc()
+ case 3:
+ w.mx.Resp3xx.Inc()
+ case 4:
+ w.mx.Resp4xx.Inc()
+ case 5:
+ w.mx.Resp5xx.Inc()
+ }
+
+ codeStr := strconv.Itoa(code)
+ c, ok := w.mx.RespCode.GetP(codeStr)
+ if !ok {
+ w.addDimToRespCodesChart(codeStr)
+ }
+ c.Inc()
+}
+
+func (w *WebLog) collectReqSize() {
+ if !w.line.hasReqSize() {
+ return
+ }
+ w.mx.BytesReceived.Add(float64(w.line.reqSize))
+}
+
+func (w *WebLog) collectRespSize() {
+ if !w.line.hasRespSize() {
+ return
+ }
+ w.mx.BytesSent.Add(float64(w.line.respSize))
+}
+
+func (w *WebLog) collectReqProcTime() {
+ if !w.line.hasReqProcTime() {
+ return
+ }
+ w.mx.ReqProcTime.Observe(w.line.reqProcTime)
+ if w.mx.ReqProcTimeHist == nil {
+ return
+ }
+ w.mx.ReqProcTimeHist.Observe(w.line.reqProcTime)
+}
+
+func (w *WebLog) collectUpsRespTime() {
+ if !w.line.hasUpsRespTime() {
+ return
+ }
+ w.mx.UpsRespTime.Observe(w.line.upsRespTime)
+ if w.mx.UpsRespTimeHist == nil {
+ return
+ }
+ w.mx.UpsRespTimeHist.Observe(w.line.upsRespTime)
+}
+
+func (w *WebLog) collectSSLProto() {
+ if !w.line.hasSSLProto() {
+ return
+ }
+ c, ok := w.mx.ReqSSLProto.GetP(w.line.sslProto)
+ if !ok {
+ w.addDimToSSLProtoChart(w.line.sslProto)
+ }
+ c.Inc()
+}
+
+func (w *WebLog) collectSSLCipherSuite() {
+ if !w.line.hasSSLCipherSuite() {
+ return
+ }
+ c, ok := w.mx.ReqSSLCipherSuite.GetP(w.line.sslCipherSuite)
+ if !ok {
+ w.addDimToSSLCipherSuiteChart(w.line.sslCipherSuite)
+ }
+ c.Inc()
+}
+
+func (w *WebLog) collectURLPatternStats(name string) {
+ v, ok := w.mx.URLPatternStats[name]
+ if !ok {
+ return
+ }
+ if w.line.hasRespCode() {
+ status := strconv.Itoa(w.line.respCode)
+ c, ok := v.RespCode.GetP(status)
+ if !ok {
+ w.addDimToURLPatternRespCodesChart(name, status)
+ }
+ c.Inc()
+ }
+
+ if w.line.hasReqMethod() {
+ c, ok := v.ReqMethod.GetP(w.line.reqMethod)
+ if !ok {
+ w.addDimToURLPatternReqMethodsChart(name, w.line.reqMethod)
+ }
+ c.Inc()
+ }
+
+ if w.line.hasReqSize() {
+ v.BytesReceived.Add(float64(w.line.reqSize))
+ }
+
+ if w.line.hasRespSize() {
+ v.BytesSent.Add(float64(w.line.respSize))
+ }
+ if w.line.hasReqProcTime() {
+ v.ReqProcTime.Observe(w.line.reqProcTime)
+ }
+}
+
+func (w *WebLog) collectCustomFields() {
+ if !w.line.hasCustomFields() {
+ return
+ }
+
+ for _, cv := range w.line.custom.values {
+ _, _ = cv.name, cv.value
+
+ if patterns, ok := w.customFields[cv.name]; ok {
+ for _, pattern := range patterns {
+ if !pattern.MatchString(cv.value) {
+ continue
+ }
+ v, ok := w.mx.ReqCustomField[cv.name]
+ if !ok {
+ break
+ }
+ c, _ := v.GetP(pattern.name)
+ c.Inc()
+ break
+ }
+ } else if histogram, ok := w.customTimeFields[cv.name]; ok {
+ v, ok := w.mx.ReqCustomTimeField[cv.name]
+ if !ok {
+ continue
+ }
+ ctf, err := strconv.ParseFloat(cv.value, 64)
+ if err != nil || !isTimeValid(ctf) {
+ continue
+ }
+ v.Time.Observe(ctf)
+ if histogram != nil {
+ v.TimeHist.Observe(ctf * timeMultiplier(cv.value))
+ }
+ } else if w.customNumericFields[cv.name] {
+ m, ok := w.mx.ReqCustomNumericField[cv.name]
+ if !ok {
+ continue
+ }
+ v, err := strconv.ParseFloat(cv.value, 64)
+ if err != nil {
+ continue
+ }
+ v *= float64(m.multiplier)
+ m.Summary.Observe(v)
+ }
+ }
+}
+
+func (w *WebLog) addDimToVhostChart(vhost string) {
+ chart := w.Charts().Get(reqByVhost.ID)
+ if chart == nil {
+ w.Warningf("add dimension: no '%s' chart", reqByVhost.ID)
+ return
+ }
+ dim := &Dim{
+ ID: "req_vhost_" + vhost,
+ Name: vhost,
+ Algo: module.Incremental,
+ }
+ if err := chart.AddDim(dim); err != nil {
+ w.Warning(err)
+ return
+ }
+ chart.MarkNotCreated()
+}
+
+func (w *WebLog) addDimToPortChart(port string) {
+ chart := w.Charts().Get(reqByPort.ID)
+ if chart == nil {
+ w.Warningf("add dimension: no '%s' chart", reqByPort.ID)
+ return
+ }
+ dim := &Dim{
+ ID: "req_port_" + port,
+ Name: port,
+ Algo: module.Incremental,
+ }
+ if err := chart.AddDim(dim); err != nil {
+ w.Warning(err)
+ return
+ }
+ chart.MarkNotCreated()
+}
+
+func (w *WebLog) addDimToReqMethodChart(method string) {
+ chart := w.Charts().Get(reqByMethod.ID)
+ if chart == nil {
+ w.Warningf("add dimension: no '%s' chart", reqByMethod.ID)
+ return
+ }
+ dim := &Dim{
+ ID: "req_method_" + method,
+ Name: method,
+ Algo: module.Incremental,
+ }
+ if err := chart.AddDim(dim); err != nil {
+ w.Warning(err)
+ return
+ }
+ chart.MarkNotCreated()
+}
+
+func (w *WebLog) addDimToReqVersionChart(version string) {
+ chart := w.Charts().Get(reqByVersion.ID)
+ if chart == nil {
+ w.Warningf("add dimension: no '%s' chart", reqByVersion.ID)
+ return
+ }
+ dim := &Dim{
+ ID: "req_version_" + version,
+ Name: version,
+ Algo: module.Incremental,
+ }
+ if err := chart.AddDim(dim); err != nil {
+ w.Warning(err)
+ return
+ }
+ chart.MarkNotCreated()
+}
+
+func (w *WebLog) addDimToSSLProtoChart(proto string) {
+ chart := w.Charts().Get(reqBySSLProto.ID)
+ if chart == nil {
+ chart = reqBySSLProto.Copy()
+ if err := w.Charts().Add(chart); err != nil {
+ w.Warning(err)
+ return
+ }
+ }
+ dim := &Dim{
+ ID: "req_ssl_proto_" + proto,
+ Name: proto,
+ Algo: module.Incremental,
+ }
+ if err := chart.AddDim(dim); err != nil {
+ w.Warning(err)
+ return
+ }
+ chart.MarkNotCreated()
+}
+
+func (w *WebLog) addDimToSSLCipherSuiteChart(cipher string) {
+ chart := w.Charts().Get(reqBySSLCipherSuite.ID)
+ if chart == nil {
+ chart = reqBySSLCipherSuite.Copy()
+ if err := w.Charts().Add(chart); err != nil {
+ w.Warning(err)
+ return
+ }
+ }
+ dim := &Dim{
+ ID: "req_ssl_cipher_suite_" + cipher,
+ Name: cipher,
+ Algo: module.Incremental,
+ }
+ if err := chart.AddDim(dim); err != nil {
+ w.Warning(err)
+ return
+ }
+ chart.MarkNotCreated()
+}
+
+func (w *WebLog) addDimToRespCodesChart(code string) {
+ chart := w.findRespCodesChart(code)
+ if chart == nil {
+ w.Warning("add dimension: cant find resp codes chart")
+ return
+ }
+ dim := &Dim{
+ ID: "resp_code_" + code,
+ Name: code,
+ Algo: module.Incremental,
+ }
+ if err := chart.AddDim(dim); err != nil {
+ w.Warning(err)
+ return
+ }
+ chart.MarkNotCreated()
+}
+
+func (w *WebLog) addDimToURLPatternRespCodesChart(name, code string) {
+ id := fmt.Sprintf(urlPatternRespCodes.ID, name)
+ chart := w.Charts().Get(id)
+ if chart == nil {
+ w.Warningf("add dimension: no '%s' chart", id)
+ return
+ }
+ dim := &Dim{
+ ID: fmt.Sprintf("url_ptn_%s_resp_code_%s", name, code),
+ Name: code,
+ Algo: module.Incremental,
+ }
+
+ if err := chart.AddDim(dim); err != nil {
+ w.Warning(err)
+ return
+ }
+ chart.MarkNotCreated()
+}
+
+func (w *WebLog) addDimToURLPatternReqMethodsChart(name, method string) {
+ id := fmt.Sprintf(urlPatternReqMethods.ID, name)
+ chart := w.Charts().Get(id)
+ if chart == nil {
+ w.Warningf("add dimension: no '%s' chart", id)
+ return
+ }
+ dim := &Dim{
+ ID: fmt.Sprintf("url_ptn_%s_req_method_%s", name, method),
+ Name: method,
+ Algo: module.Incremental,
+ }
+
+ if err := chart.AddDim(dim); err != nil {
+ w.Warning(err)
+ return
+ }
+ chart.MarkNotCreated()
+}
+
+func (w *WebLog) findRespCodesChart(code string) *Chart {
+ if !w.GroupRespCodes {
+ return w.Charts().Get(respCodes.ID)
+ }
+
+ var id string
+ switch class := code[:1]; class {
+ case "1":
+ id = respCodes1xx.ID
+ case "2":
+ id = respCodes2xx.ID
+ case "3":
+ id = respCodes3xx.ID
+ case "4":
+ id = respCodes4xx.ID
+ case "5":
+ id = respCodes5xx.ID
+ default:
+ return nil
+ }
+ return w.Charts().Get(id)
+}
diff --git a/src/go/collectors/go.d.plugin/modules/weblog/config_schema.json b/src/go/collectors/go.d.plugin/modules/weblog/config_schema.json
new file mode 100644
index 000000000..845eecf46
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/weblog/config_schema.json
@@ -0,0 +1,453 @@
+{
+ "jsonSchema": {
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "type": "object",
+ "properties": {
+ "update_every": {
+ "title": "Update every",
+ "description": "Data collection interval, measured in seconds.",
+ "type": "integer",
+ "minimum": 1,
+ "default": 1
+ },
+ "path": {
+ "title": "Log file",
+ "description": "The file path to the Webserver log file.",
+ "type": "string",
+ "default": "/var/log/nginx/access.log",
+ "pattern": "^$|^/"
+ },
+ "exclude_path": {
+ "title": "Exclude path",
+ "description": "Pattern to exclude log files.",
+ "type": "string",
+ "default": "*.gz"
+ },
+ "histogram": {
+ "title": "Request processing time histogram",
+ "description": "Buckets for the histogram in milliseconds.",
+ "type": [
+ "array",
+ "null"
+ ],
+ "items": {
+ "title": "Bucket",
+ "type": "number",
+ "exclusiveMinimum": 0
+ },
+ "uniqueItems": true
+ },
+ "log_type": {
+ "title": "Log parser",
+ "description": "Type of parser to use for parsing log files.",
+ "type": "string",
+ "enum": [
+ "auto",
+ "csv",
+ "regexp",
+ "json",
+ "ltsv"
+ ],
+ "default": "auto"
+ },
+ "url_patterns": {
+ "title": "URL patterns",
+ "description": "Patterns used to match against the full original request URI. For each pattern, the web log will collect responses by status code, method, bandwidth, and processing time.",
+ "type": [
+ "array",
+ "null"
+ ],
+ "items": {
+ "title": "Patterns",
+ "type": [
+ "object",
+ "null"
+ ],
+ "properties": {
+ "name": {
+ "title": "Dimension",
+ "description": "A unique name used as a dimension name for the pattern.",
+ "type": "string"
+ },
+ "match": {
+ "title": "Pattern",
+ "description": "The [pattern string](https://github.com/netdata/netdata/tree/master/src/go/collectors/go.d.plugin/pkg/matcher#readme) used to match against the full original request URI.",
+ "type": "string"
+ }
+ },
+ "required": [
+ "name",
+ "match"
+ ]
+ },
+ "uniqueItems": true
+ },
+ "custom_fields": {
+ "title": "Custom fields",
+ "description": "Configuration for custom fields. Fild value expected to be string. Patterns used to match against the value of the specified field. For each pattern, the web log will collect responses by status code.",
+ "type": [
+ "array",
+ "null"
+ ],
+ "uniqueItems": true,
+ "items": {
+ "title": "Field configuration",
+ "type": [
+ "object",
+ "null"
+ ],
+ "properties": {
+ "name": {
+ "title": "Field name",
+ "description": "The name of the custom field.",
+ "type": "string"
+ },
+ "patterns": {
+ "title": "Patterns",
+ "description": "",
+ "type": [
+ "array",
+ "null"
+ ],
+ "items": {
+ "title": "User patterns",
+ "type": [
+ "object",
+ "null"
+ ],
+ "properties": {
+ "name": {
+ "title": "Dimension",
+ "description": "A unique name used as a dimension name for the pattern.",
+ "type": "string"
+ },
+ "match": {
+ "title": "Pattern",
+ "description": "The [pattern string](https://github.com/netdata/netdata/tree/master/src/go/collectors/go.d.plugin/pkg/matcher#readme) used to match against the field value.",
+ "type": "string"
+ }
+ },
+ "required": [
+ "name",
+ "match"
+ ]
+ }
+ }
+ },
+ "required": [
+ "name",
+ "patterns"
+ ]
+ }
+ },
+ "custom_time_fields": {
+ "title": "Custom time fields",
+ "description": "Configuration for custom time fields. Field value expected to be numeric and represent time. For each field, the web log will calculate the minimum, average, maximum value, and histogram.",
+ "type": [
+ "array",
+ "null"
+ ],
+ "items": {
+ "title": "Field configuration",
+ "type": [
+ "object",
+ "null"
+ ],
+ "properties": {
+ "name": {
+ "title": "Field mame",
+ "description": "The name of the custom time field.",
+ "type": "string"
+ },
+ "histogram": {
+ "title": "Histogram",
+ "description": "Buckets for the histogram in milliseconds.",
+ "type": [
+ "array",
+ "null"
+ ],
+ "uniqueItems": true,
+ "items": {
+ "title": "Bucket",
+ "type": "number",
+ "exclusiveMinimum": 0
+ },
+ "default": [
+ 0.005,
+ 0.01,
+ 0.025,
+ 0.05,
+ 0.1,
+ 0.25,
+ 0.5,
+ 1,
+ 2.5,
+ 5,
+ 10
+ ]
+ }
+ },
+ "required": [
+ "name"
+ ]
+ }
+ },
+ "custom_numeric_fields": {
+ "title": "Custom numeric field",
+ "description": "Configuration for custom numeric fields. Fild value expected to be numeric. For each field, the web log will calculate the minimum, average, maximum value.",
+ "type": [
+ "array",
+ "null"
+ ],
+ "items": {
+ "title": "Field configuration",
+ "type": [
+ "object",
+ "null"
+ ],
+ "properties": {
+ "name": {
+ "title": "Name",
+ "description": "The name of the custom numeric field.",
+ "type": "string"
+ },
+ "units": {
+ "title": "Units",
+ "description": "The unit label for the vertical axis on charts.",
+ "type": "string"
+ },
+ "multiplier": {
+ "title": "Multiplier",
+ "description": "A value to multiply the field value.",
+ "type": "number",
+ "not": {
+ "const": 0
+ },
+ "default": 1
+ },
+ "divisor": {
+ "title": "Divisor",
+ "description": "A value to divide the field value.",
+ "type": "number",
+ "not": {
+ "const": 0
+ },
+ "default": 1
+ }
+ },
+ "required": [
+ "name",
+ "units",
+ "multiplier",
+ "divisor"
+ ]
+ }
+ }
+ },
+ "required": [
+ "path",
+ "log_type"
+ ],
+ "additionalProperties": false,
+ "patternProperties": {
+ "^name$": {}
+ },
+ "dependencies": {
+ "log_type": {
+ "oneOf": [
+ {
+ "properties": {
+ "log_type": {
+ "const": "auto"
+ }
+ }
+ },
+ {
+ "properties": {
+ "log_type": {
+ "const": "csv"
+ },
+ "csv_config": {
+ "title": "CSV parser configuration",
+ "type": [
+ "object",
+ "null"
+ ],
+ "properties": {
+ "format": {
+ "title": "Format",
+ "description": "Log format.",
+ "type": "string",
+ "default": "$remote_addr - - [$time_local] \"$request\" $status $body_bytes_sent"
+ },
+ "delimiter": {
+ "title": "Delimiter",
+ "description": "Delimiter used to separate fields in the log file. Default: space (' ').",
+ "type": "string",
+ "default": " "
+ }
+ },
+ "required": [
+ "format",
+ "delimiter"
+ ]
+ }
+ },
+ "required": [
+ "csv_config"
+ ]
+ },
+ {
+ "properties": {
+ "log_type": {
+ "const": "regexp"
+ },
+ "regexp_config": {
+ "title": "Regular expression parser configuration",
+ "type": [
+ "object",
+ "null"
+ ],
+ "properties": {
+ "pattern": {
+ "title": "Pattern with named groups",
+ "description": "Regular expression pattern with named groups. Use named groups for known fields.",
+ "type": "string",
+ "default": ""
+ }
+ },
+ "required": [
+ "pattern"
+ ]
+ }
+ },
+ "required": [
+ "regexp_config"
+ ]
+ },
+ {
+ "properties": {
+ "log_type": {
+ "const": "json"
+ },
+ "json_config": {
+ "title": "JSON parser configuration",
+ "type": [
+ "object",
+ "null"
+ ],
+ "properties": {
+ "mapping": {
+ "title": "Field mapping",
+ "description": "Dictionary mapping fields in logs to known fields.",
+ "type": [
+ "object",
+ "null"
+ ],
+ "additionalProperties": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ },
+ {
+ "properties": {
+ "log_type": {
+ "const": "ltsv"
+ },
+ "ltsv_config": {
+ "title": "LTSV parser configuration",
+ "type": [
+ "object",
+ "null"
+ ],
+ "properties": {
+ "field_delimiter": {
+ "title": "Field delimiter",
+ "description": "Delimiter used to separate fields in LTSV logs. Default: tab ('\\t').",
+ "type": "string",
+ "default": "\t"
+ },
+ "value_delimiter": {
+ "title": "Value delimiter",
+ "description": "Delimiter used to separate label-value pairs in LTSV logs.",
+ "type": "string",
+ "default": ":"
+ },
+ "mapping": {
+ "title": "Field mapping",
+ "description": "Dictionary mapping fields in logs to known fields.",
+ "type": [
+ "object",
+ "null"
+ ],
+ "additionalProperties": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ }
+ ]
+ }
+ }
+ },
+ "uiSchema": {
+ "uiOptions": {
+ "fullPage": true
+ },
+ "log_type": {
+ "ui:widget": "radio",
+ "ui:options": {
+ "inline": true
+ }
+ },
+ "custom_fields": {
+ "ui:collapsible": true
+ },
+ "custom_time_fields": {
+ "ui:collapsible": true
+ },
+ "ui:flavour": "tabs",
+ "ui:options": {
+ "tabs": [
+ {
+ "title": "Base",
+ "fields": [
+ "update_every",
+ "path",
+ "exclude_path",
+ "histogram"
+ ]
+ },
+ {
+ "title": "Parser",
+ "fields": [
+ "log_type",
+ "csv_config",
+ "ltsv_config",
+ "regexp_config",
+ "json_config"
+ ]
+ },
+ {
+ "title": "URL patterns",
+ "fields": [
+ "url_patterns"
+ ]
+ },
+ {
+ "title": "Custom fields",
+ "fields": [
+ "custom_fields",
+ "custom_time_fields",
+ "custom_numeric_fields"
+ ]
+ }
+ ]
+ }
+ }
+}
diff --git a/src/go/collectors/go.d.plugin/modules/weblog/init.go b/src/go/collectors/go.d.plugin/modules/weblog/init.go
new file mode 100644
index 000000000..b456c817a
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/weblog/init.go
@@ -0,0 +1,197 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package weblog
+
+import (
+ "errors"
+ "fmt"
+ "strings"
+
+ "github.com/netdata/netdata/go/go.d.plugin/pkg/logs"
+ "github.com/netdata/netdata/go/go.d.plugin/pkg/matcher"
+)
+
+type pattern struct {
+ name string
+ matcher.Matcher
+}
+
+func newPattern(up userPattern) (*pattern, error) {
+ if up.Name == "" || up.Match == "" {
+ return nil, errors.New("empty 'name' or 'match'")
+ }
+
+ m, err := matcher.Parse(up.Match)
+ if err != nil {
+ return nil, err
+ }
+ return &pattern{name: up.Name, Matcher: m}, nil
+}
+
+func (w *WebLog) createURLPatterns() error {
+ if len(w.URLPatterns) == 0 {
+ w.Debug("skipping URL patterns creating, no patterns provided")
+ return nil
+ }
+ w.Debug("starting URL patterns creating")
+ for _, up := range w.URLPatterns {
+ p, err := newPattern(up)
+ if err != nil {
+ return fmt.Errorf("create pattern %+v: %v", up, err)
+ }
+ w.Debugf("created pattern '%s', type '%T', match '%s'", p.name, p.Matcher, up.Match)
+ w.urlPatterns = append(w.urlPatterns, p)
+ }
+ w.Debugf("created %d URL pattern(s)", len(w.URLPatterns))
+ return nil
+}
+
+func (w *WebLog) createCustomFields() error {
+ if len(w.CustomFields) == 0 {
+ w.Debug("skipping custom fields creating, no custom fields provided")
+ return nil
+ }
+
+ w.Debug("starting custom fields creating")
+ w.customFields = make(map[string][]*pattern)
+ for i, cf := range w.CustomFields {
+ if cf.Name == "" {
+ return fmt.Errorf("create custom field: name not set (field %d)", i+1)
+ }
+ for _, up := range cf.Patterns {
+ p, err := newPattern(up)
+ if err != nil {
+ return fmt.Errorf("create field '%s' pattern %+v: %v", cf.Name, up, err)
+ }
+ w.Debugf("created field '%s', pattern '%s', type '%T', match '%s'", cf.Name, p.name, p.Matcher, up.Match)
+ w.customFields[cf.Name] = append(w.customFields[cf.Name], p)
+ }
+ }
+ w.Debugf("created %d custom field(s)", len(w.CustomFields))
+ return nil
+}
+
+func (w *WebLog) createCustomTimeFields() error {
+ if len(w.CustomTimeFields) == 0 {
+ w.Debug("skipping custom time fields creating, no custom time fields provided")
+ return nil
+ }
+
+ w.Debug("starting custom time fields creating")
+ w.customTimeFields = make(map[string][]float64)
+ for i, ctf := range w.CustomTimeFields {
+ if ctf.Name == "" {
+ return fmt.Errorf("create custom field: name not set (field %d)", i+1)
+ }
+ w.customTimeFields[ctf.Name] = ctf.Histogram
+ w.Debugf("created time field '%s', histogram '%v'", ctf.Name, ctf.Histogram)
+ }
+ w.Debugf("created %d custom time field(s)", len(w.CustomTimeFields))
+ return nil
+}
+
+func (w *WebLog) createCustomNumericFields() error {
+ if len(w.CustomNumericFields) == 0 {
+ w.Debug("no custom time fields provided")
+ return nil
+ }
+
+ w.Debugf("creating custom numeric fields for '%+v'", w.CustomNumericFields)
+
+ w.customNumericFields = make(map[string]bool)
+
+ for i := range w.CustomNumericFields {
+ v := w.CustomNumericFields[i]
+ if v.Name == "" {
+ return fmt.Errorf("custom numeric field (%d): 'name' not set", i+1)
+ }
+ if v.Units == "" {
+ return fmt.Errorf("custom numeric field (%s): 'units' not set", v.Name)
+ }
+ if v.Multiplier <= 0 {
+ v.Multiplier = 1
+ }
+ if v.Divisor <= 0 {
+ v.Divisor = 1
+ }
+ w.CustomNumericFields[i] = v
+ w.customNumericFields[v.Name] = true
+ }
+
+ return nil
+}
+
+func (w *WebLog) createLogLine() {
+ w.line = newEmptyLogLine()
+
+ for v := range w.customFields {
+ w.line.custom.fields[v] = struct{}{}
+ }
+ for v := range w.customTimeFields {
+ w.line.custom.fields[v] = struct{}{}
+ }
+ for v := range w.customNumericFields {
+ w.line.custom.fields[v] = struct{}{}
+ }
+}
+
+func (w *WebLog) createLogReader() error {
+ w.Cleanup()
+ w.Debug("starting log reader creating")
+
+ reader, err := logs.Open(w.Path, w.ExcludePath, w.Logger)
+ if err != nil {
+ return fmt.Errorf("creating log reader: %v", err)
+ }
+
+ w.Debugf("created log reader, current file '%s'", reader.CurrentFilename())
+ w.file = reader
+
+ return nil
+}
+
+func (w *WebLog) createParser() error {
+ w.Debug("starting parser creating")
+
+ const readLinesNum = 100
+
+ lines, err := logs.ReadLastLines(w.file.CurrentFilename(), readLinesNum)
+ if err != nil {
+ return fmt.Errorf("failed to read last lines: %v", err)
+ }
+
+ var found bool
+ for _, line := range lines {
+ if line = strings.TrimSpace(line); line == "" {
+ continue
+ }
+ w.Debugf("last line: '%s'", line)
+
+ w.parser, err = w.newParser([]byte(line))
+ if err != nil {
+ w.Debugf("failed to create parser from line: %v", err)
+ continue
+ }
+
+ w.line.reset()
+
+ if err = w.parser.Parse([]byte(line), w.line); err != nil {
+ w.Debugf("failed to parse line: %v", err)
+ continue
+ }
+
+ if err = w.line.verify(); err != nil {
+ w.Debugf("failed to verify line: %v", err)
+ continue
+ }
+
+ found = true
+ break
+ }
+
+ if !found {
+ return fmt.Errorf("failed to create log parser (file '%s')", w.file.CurrentFilename())
+ }
+
+ return nil
+}
diff --git a/src/go/collectors/go.d.plugin/modules/weblog/integrations/web_server_log_files.md b/src/go/collectors/go.d.plugin/modules/weblog/integrations/web_server_log_files.md
new file mode 100644
index 000000000..a433c6dd2
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/weblog/integrations/web_server_log_files.md
@@ -0,0 +1,375 @@
+<!--startmeta
+custom_edit_url: "https://github.com/netdata/netdata/edit/master/src/go/collectors/go.d.plugin/modules/weblog/README.md"
+meta_yaml: "https://github.com/netdata/netdata/edit/master/src/go/collectors/go.d.plugin/modules/weblog/metadata.yaml"
+sidebar_label: "Web server log files"
+learn_status: "Published"
+learn_rel_path: "Collecting Metrics/Web Servers and Web Proxies"
+most_popular: False
+message: "DO NOT EDIT THIS FILE DIRECTLY, IT IS GENERATED BY THE COLLECTOR'S metadata.yaml FILE"
+endmeta-->
+
+# Web server log files
+
+
+<img src="https://netdata.cloud/img/webservers.svg" width="150"/>
+
+
+Plugin: go.d.plugin
+Module: web_log
+
+<img src="https://img.shields.io/badge/maintained%20by-Netdata-%2300ab44" />
+
+## Overview
+
+This collector monitors web servers by parsing their log files.
+
+
+
+
+This collector is supported on all platforms.
+
+This collector supports collecting metrics from multiple instances of this integration, including remote instances.
+
+
+### Default Behavior
+
+#### Auto-Detection
+
+It automatically detects log files of web servers running on localhost.
+
+
+#### Limits
+
+The default configuration for this integration does not impose any limits on data collection.
+
+#### Performance Impact
+
+The default configuration for this integration is not expected to impose a significant performance impact on the system.
+
+
+## Metrics
+
+Metrics grouped by *scope*.
+
+The scope defines the instance that the metric belongs to. An instance is uniquely identified by a set of labels.
+
+
+
+### Per Web server log files instance
+
+These metrics refer to the entire monitored application.
+
+This scope has no labels.
+
+Metrics:
+
+| Metric | Dimensions | Unit |
+|:------|:----------|:----|
+| web_log.requests | requests | requests/s |
+| web_log.excluded_requests | unmatched | requests/s |
+| web_log.type_requests | success, bad, redirect, error | requests/s |
+| web_log.status_code_class_responses | 1xx, 2xx, 3xx, 4xx, 5xx | responses/s |
+| web_log.status_code_class_1xx_responses | a dimension per 1xx code | responses/s |
+| web_log.status_code_class_2xx_responses | a dimension per 2xx code | responses/s |
+| web_log.status_code_class_3xx_responses | a dimension per 3xx code | responses/s |
+| web_log.status_code_class_4xx_responses | a dimension per 4xx code | responses/s |
+| web_log.status_code_class_5xx_responses | a dimension per 5xx code | responses/s |
+| web_log.bandwidth | received, sent | kilobits/s |
+| web_log.request_processing_time | min, max, avg | milliseconds |
+| web_log.requests_processing_time_histogram | a dimension per bucket | requests/s |
+| web_log.upstream_response_time | min, max, avg | milliseconds |
+| web_log.upstream_responses_time_histogram | a dimension per bucket | requests/s |
+| web_log.current_poll_uniq_clients | ipv4, ipv6 | clients |
+| web_log.vhost_requests | a dimension per vhost | requests/s |
+| web_log.port_requests | a dimension per port | requests/s |
+| web_log.scheme_requests | http, https | requests/s |
+| web_log.http_method_requests | a dimension per HTTP method | requests/s |
+| web_log.http_version_requests | a dimension per HTTP version | requests/s |
+| web_log.ip_proto_requests | ipv4, ipv6 | requests/s |
+| web_log.ssl_proto_requests | a dimension per SSL protocol | requests/s |
+| web_log.ssl_cipher_suite_requests | a dimension per SSL cipher suite | requests/s |
+| web_log.url_pattern_requests | a dimension per URL pattern | requests/s |
+| web_log.custom_field_pattern_requests | a dimension per custom field pattern | requests/s |
+
+### Per custom time field
+
+TBD
+
+This scope has no labels.
+
+Metrics:
+
+| Metric | Dimensions | Unit |
+|:------|:----------|:----|
+| web_log.custom_time_field_summary | min, max, avg | milliseconds |
+| web_log.custom_time_field_histogram | a dimension per bucket | observations |
+
+### Per custom numeric field
+
+TBD
+
+This scope has no labels.
+
+Metrics:
+
+| Metric | Dimensions | Unit |
+|:------|:----------|:----|
+| web_log.custom_numeric_field_{{field_name}}_summary | min, max, avg | {{units}} |
+
+### Per URL pattern
+
+TBD
+
+This scope has no labels.
+
+Metrics:
+
+| Metric | Dimensions | Unit |
+|:------|:----------|:----|
+| web_log.url_pattern_status_code_responses | a dimension per pattern | responses/s |
+| web_log.url_pattern_http_method_requests | a dimension per HTTP method | requests/s |
+| web_log.url_pattern_bandwidth | received, sent | kilobits/s |
+| web_log.url_pattern_request_processing_time | min, max, avg | milliseconds |
+
+
+
+## Alerts
+
+
+The following alerts are available:
+
+| Alert name | On metric | Description |
+|:------------|:----------|:------------|
+| [ web_log_1m_unmatched ](https://github.com/netdata/netdata/blob/master/src/health/health.d/web_log.conf) | web_log.excluded_requests | percentage of unparsed log lines over the last minute |
+| [ web_log_1m_requests ](https://github.com/netdata/netdata/blob/master/src/health/health.d/web_log.conf) | web_log.type_requests | ratio of successful HTTP requests over the last minute (1xx, 2xx, 304, 401) |
+| [ web_log_1m_redirects ](https://github.com/netdata/netdata/blob/master/src/health/health.d/web_log.conf) | web_log.type_requests | ratio of redirection HTTP requests over the last minute (3xx except 304) |
+| [ web_log_1m_bad_requests ](https://github.com/netdata/netdata/blob/master/src/health/health.d/web_log.conf) | web_log.type_requests | ratio of client error HTTP requests over the last minute (4xx except 401) |
+| [ web_log_1m_internal_errors ](https://github.com/netdata/netdata/blob/master/src/health/health.d/web_log.conf) | web_log.type_requests | ratio of server error HTTP requests over the last minute (5xx) |
+| [ web_log_web_slow ](https://github.com/netdata/netdata/blob/master/src/health/health.d/web_log.conf) | web_log.request_processing_time | average HTTP response time over the last 1 minute |
+| [ web_log_5m_requests_ratio ](https://github.com/netdata/netdata/blob/master/src/health/health.d/web_log.conf) | web_log.type_requests | ratio of successful HTTP requests over over the last 5 minutes, compared with the previous 5 minutes |
+
+
+## Setup
+
+### Prerequisites
+
+No action required.
+
+### Configuration
+
+#### File
+
+The configuration file name for this integration is `go.d/web_log.conf`.
+
+
+You can edit the configuration file using the `edit-config` script from the
+Netdata [config directory](/docs/netdata-agent/configuration/README.md#the-netdata-config-directory).
+
+```bash
+cd /etc/netdata 2>/dev/null || cd /opt/netdata/etc/netdata
+sudo ./edit-config go.d/web_log.conf
+```
+#### Options
+
+Weblog is aware of how to parse and interpret the following fields (**known fields**):
+
+> [nginx](https://nginx.org/en/docs/varindex.html)
+>
+> [apache](https://httpd.apache.org/docs/current/mod/mod_log_config.html)
+
+| nginx | apache | description |
+|-------------------------|----------|------------------------------------------------------------------------------------------|
+| $host ($http_host) | %v | Name of the server which accepted a request. |
+| $server_port | %p | Port of the server which accepted a request. |
+| $scheme | - | Request scheme. "http" or "https". |
+| $remote_addr | %a (%h) | Client address. |
+| $request | %r | Full original request line. The line is "$request_method $request_uri $server_protocol". |
+| $request_method | %m | Request method. Usually "GET" or "POST". |
+| $request_uri | %U | Full original request URI. |
+| $server_protocol | %H | Request protocol. Usually "HTTP/1.0", "HTTP/1.1", or "HTTP/2.0". |
+| $status | %s (%>s) | Response status code. |
+| $request_length | %I | Bytes received from a client, including request and headers. |
+| $bytes_sent | %O | Bytes sent to a client, including request and headers. |
+| $body_bytes_sent | %B (%b) | Bytes sent to a client, not counting the response header. |
+| $request_time | %D | Request processing time. |
+| $upstream_response_time | - | Time spent on receiving the response from the upstream server. |
+| $ssl_protocol | - | Protocol of an established SSL connection. |
+| $ssl_cipher | - | String of ciphers used for an established SSL connection. |
+
+Notes:
+
+- Apache `%h` logs the IP address if [HostnameLookups](https://httpd.apache.org/docs/2.4/mod/core.html#hostnamelookups) is Off. The web log collector counts hostnames as IPv4 addresses. We recommend either to disable HostnameLookups or use `%a` instead of `%h`.
+- Since httpd 2.0, unlike 1.3, the `%b` and `%B` format strings do not represent the number of bytes sent to the client, but simply the size in bytes of the HTTP response. It will differ, for instance, if the connection is aborted, or if SSL is used. The `%O` format provided by [`mod_logio`](https://httpd.apache.org/docs/2.4/mod/mod_logio.html) will log the actual number of bytes sent over the network.
+- To get `%I` and `%O` working you need to enable `mod_logio` on Apache.
+- NGINX logs URI with query parameters, Apache doesnt.
+- `$request` is parsed into `$request_method`, `$request_uri` and `$server_protocol`. If you have `$request` in your log format, there is no sense to have others.
+- Don't use both `$bytes_sent` and `$body_bytes_sent` (`%O` and `%B` or `%b`). The module does not distinguish between these parameters.
+
+
+<details open><summary>Config options</summary>
+
+| Name | Description | Default | Required |
+|:----|:-----------|:-------|:--------:|
+| update_every | Data collection frequency. | 1 | no |
+| autodetection_retry | Recheck interval in seconds. Zero means no recheck will be scheduled. | 0 | no |
+| path | Path to the web server log file. | | yes |
+| exclude_path | Path to exclude. | *.gz | no |
+| url_patterns | List of URL patterns. | [] | no |
+| url_patterns.name | Used as a dimension name. | | yes |
+| url_patterns.pattern | Used to match against full original request URI. Pattern syntax in [matcher](https://github.com/netdata/netdata/tree/master/src/go/collectors/go.d.plugin/pkg/matcher#supported-format). | | yes |
+| parser | Log parser configuration. | | no |
+| parser.log_type | Log parser type. | auto | no |
+| parser.csv_config | CSV log parser config. | | no |
+| parser.csv_config.delimiter | CSV field delimiter. | , | no |
+| parser.csv_config.format | CSV log format. | | no |
+| parser.ltsv_config | LTSV log parser config. | | no |
+| parser.ltsv_config.field_delimiter | LTSV field delimiter. | \t | no |
+| parser.ltsv_config.value_delimiter | LTSV value delimiter. | : | no |
+| parser.ltsv_config.mapping | LTSV fields mapping to **known fields**. | | yes |
+| parser.json_config | JSON log parser config. | | no |
+| parser.json_config.mapping | JSON fields mapping to **known fields**. | | yes |
+| parser.regexp_config | RegExp log parser config. | | no |
+| parser.regexp_config.pattern | RegExp pattern with named groups. | | yes |
+
+##### url_patterns
+
+"URL pattern" scope metrics will be collected for each URL pattern.
+
+Option syntax:
+
+```yaml
+url_patterns:
+ - name: name1
+ pattern: pattern1
+ - name: name2
+ pattern: pattern2
+```
+
+
+##### parser.log_type
+
+Weblog supports 5 different log parsers:
+
+| Parser type | Description |
+|-------------|-------------------------------------------|
+| auto | Use CSV and auto-detect format |
+| csv | A comma-separated values |
+| json | [JSON](https://www.json.org/json-en.html) |
+| ltsv | [LTSV](http://ltsv.org/) |
+| regexp | Regular expression with named groups |
+
+Syntax:
+
+```yaml
+parser:
+ log_type: auto
+```
+
+If `log_type` parameter set to `auto` (which is default), weblog will try to auto-detect appropriate log parser and log format using the last line of the log file.
+
+- checks if format is `CSV` (using regexp).
+- checks if format is `JSON` (using regexp).
+- assumes format is `CSV` and tries to find appropriate `CSV` log format using predefined list of formats. It tries to parse the line using each of them in the following order (the first one matches is used later):
+
+ ```sh
+ $host:$server_port $remote_addr - - [$time_local] "$request" $status $body_bytes_sent - - $request_length $request_time $upstream_response_time
+ $host:$server_port $remote_addr - - [$time_local] "$request" $status $body_bytes_sent - - $request_length $request_time
+ $host:$server_port $remote_addr - - [$time_local] "$request" $status $body_bytes_sent $request_length $request_time $upstream_response_time
+ $host:$server_port $remote_addr - - [$time_local] "$request" $status $body_bytes_sent $request_length $request_time
+ $host:$server_port $remote_addr - - [$time_local] "$request" $status $body_bytes_sent
+ $remote_addr - - [$time_local] "$request" $status $body_bytes_sent - - $request_length $request_time $upstream_response_time
+ $remote_addr - - [$time_local] "$request" $status $body_bytes_sent - - $request_length $request_time
+ $remote_addr - - [$time_local] "$request" $status $body_bytes_sent $request_length $request_time $upstream_response_time
+ $remote_addr - - [$time_local] "$request" $status $body_bytes_sent $request_length $request_time
+ $remote_addr - - [$time_local] "$request" $status $body_bytes_sent
+ ```
+
+ If you're using the default Apache/NGINX log format, auto-detect will work for you. If it doesn't work you need to set the format manually.
+
+
+##### parser.csv_config.format
+
+
+
+##### parser.ltsv_config.mapping
+
+The mapping is a dictionary where the key is a field, as in logs, and the value is the corresponding **known field**.
+
+> **Note**: don't use `$` and `%` prefixes for mapped field names.
+
+```yaml
+parser:
+ log_type: ltsv
+ ltsv_config:
+ mapping:
+ label1: field1
+ label2: field2
+```
+
+
+##### parser.json_config.mapping
+
+The mapping is a dictionary where the key is a field, as in logs, and the value is the corresponding **known field**.
+
+> **Note**: don't use `$` and `%` prefixes for mapped field names.
+
+```yaml
+parser:
+ log_type: json
+ json_config:
+ mapping:
+ label1: field1
+ label2: field2
+```
+
+
+##### parser.regexp_config.pattern
+
+Use pattern with subexpressions names. These names should be **known fields**.
+
+> **Note**: don't use `$` and `%` prefixes for mapped field names.
+
+Syntax:
+
+```yaml
+parser:
+ log_type: regexp
+ regexp_config:
+ pattern: PATTERN
+```
+
+
+</details>
+
+#### Examples
+There are no configuration examples.
+
+
+
+## Troubleshooting
+
+### Debug Mode
+
+To troubleshoot issues with the `web_log` collector, run the `go.d.plugin` with the debug option enabled. The output
+should give you clues as to why the collector isn't working.
+
+- Navigate to the `plugins.d` directory, usually at `/usr/libexec/netdata/plugins.d/`. If that's not the case on
+ your system, open `netdata.conf` and look for the `plugins` setting under `[directories]`.
+
+ ```bash
+ cd /usr/libexec/netdata/plugins.d/
+ ```
+
+- Switch to the `netdata` user.
+
+ ```bash
+ sudo -u netdata -s
+ ```
+
+- Run the `go.d.plugin` to debug the collector:
+
+ ```bash
+ ./go.d.plugin -d -m web_log
+ ```
+
+
diff --git a/src/go/collectors/go.d.plugin/modules/weblog/logline.go b/src/go/collectors/go.d.plugin/modules/weblog/logline.go
new file mode 100644
index 000000000..5a69593b9
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/weblog/logline.go
@@ -0,0 +1,617 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package weblog
+
+import (
+ "errors"
+ "fmt"
+ "regexp"
+ "strconv"
+ "strings"
+)
+
+// TODO: it is not clear how to handle "-", current handling is not good
+// In general it is:
+// - If a field is unused in a particular entry dash "-" marks the omitted field.
+// In addition to that "-" is used as zero value in:
+// - apache: %b '-' when no bytes are sent.
+//
+// Log Format:
+// - CLF: https://www.w3.org/Daemon/User/Config/Logging.html#common-logfile-format
+// - ELF: https://www.w3.org/TR/WD-logfile.html
+// - Apache CLF: https://httpd.apache.org/docs/trunk/logs.html#common
+
+// Variables:
+// - nginx: http://nginx.org/en/docs/varindex.html
+// - apache: http://httpd.apache.org/docs/current/mod/mod_log_config.html#logformat
+// - IIS: https://learn.microsoft.com/en-us/windows/win32/http/w3c-logging
+
+/*
+| nginx | apache | description |
+|-------------------------|-----------|-----------------------------------------------|
+| $host ($http_host) | %v | Name of the server which accepted a request.
+| $server_port | %p | Port of the server which accepted a request.
+| $scheme | - | Request scheme. "http" or "https".
+| $remote_addr | %a (%h) | Client address.
+| $request | %r | Full original request line. The line is "$request_method $request_uri $server_protocol".
+| $request_method | %m | Request method. Usually "GET" or "POST".
+| $request_uri | %U | Full original request URI.
+| $server_protocol | %H | Request protocol. Usually "HTTP/1.0", "HTTP/1.1", or "HTTP/2.0".
+| $status | %s (%>s) | Response status code.
+| $request_length | %I | Bytes received from a client, including request and headers.
+| $bytes_sent | %O | Bytes sent to a client, including request and headers.
+| $body_bytes_sent | %B (%b) | Bytes sent to a client, not counting the response header.
+| $request_time | %D | Request processing time.
+| $upstream_response_time | - | Time spent on receiving the response from the upstream server.
+| $ssl_protocol | - | Protocol of an established SSL connection.
+| $ssl_cipher | - | String of ciphers used for an established SSL connection.
+*/
+
+var (
+ errEmptyLine = errors.New("empty line")
+ errBadVhost = errors.New("bad vhost")
+ errBadVhostPort = errors.New("bad vhost with port")
+ errBadPort = errors.New("bad port")
+ errBadReqScheme = errors.New("bad req scheme")
+ errBadReqClient = errors.New("bad req client")
+ errBadRequest = errors.New("bad request")
+ errBadReqMethod = errors.New("bad req method")
+ errBadReqURL = errors.New("bad req url")
+ errBadReqProto = errors.New("bad req protocol")
+ errBadReqSize = errors.New("bad req size")
+ errBadRespCode = errors.New("bad resp status code")
+ errBadRespSize = errors.New("bad resp size")
+ errBadReqProcTime = errors.New("bad req processing time")
+ errBadUpsRespTime = errors.New("bad upstream resp time")
+ errBadSSLProto = errors.New("bad ssl protocol")
+ errBadSSLCipherSuite = errors.New("bad ssl cipher suite")
+)
+
+func newEmptyLogLine() *logLine {
+ var l logLine
+ l.custom.fields = make(map[string]struct{})
+ l.custom.values = make([]customValue, 0, 20)
+ l.reset()
+ return &l
+}
+
+type (
+ logLine struct {
+ web
+ custom custom
+ }
+ web struct {
+ vhost string
+ port string
+ reqScheme string
+ reqClient string
+ reqMethod string
+ reqURL string
+ reqProto string
+ reqSize int
+ reqProcTime float64
+ respCode int
+ respSize int
+ upsRespTime float64
+ sslProto string
+ sslCipherSuite string
+ }
+ custom struct {
+ fields map[string]struct{}
+ values []customValue
+ }
+ customValue struct {
+ name string
+ value string
+ }
+)
+
+func (l *logLine) Assign(field string, value string) (err error) {
+ if value == "" {
+ return
+ }
+
+ switch field {
+ case "host", "http_host", "v":
+ err = l.assignVhost(value)
+ case "server_port", "p":
+ err = l.assignPort(value)
+ case "host:$server_port", "v:%p":
+ err = l.assignVhostWithPort(value)
+ case "scheme":
+ err = l.assignReqScheme(value)
+ case "remote_addr", "a", "h":
+ err = l.assignReqClient(value)
+ case "request", "r":
+ err = l.assignRequest(value)
+ case "request_method", "m":
+ err = l.assignReqMethod(value)
+ case "request_uri", "U":
+ err = l.assignReqURL(value)
+ case "server_protocol", "H":
+ err = l.assignReqProto(value)
+ case "status", "s", ">s":
+ err = l.assignRespCode(value)
+ case "request_length", "I":
+ err = l.assignReqSize(value)
+ case "bytes_sent", "body_bytes_sent", "b", "O", "B":
+ err = l.assignRespSize(value)
+ case "request_time", "D":
+ err = l.assignReqProcTime(value)
+ case "upstream_response_time":
+ err = l.assignUpsRespTime(value)
+ case "ssl_protocol":
+ err = l.assignSSLProto(value)
+ case "ssl_cipher":
+ err = l.assignSSLCipherSuite(value)
+ default:
+ err = l.assignCustom(field, value)
+ }
+ if err != nil {
+ err = fmt.Errorf("assign '%s': %w", field, err)
+ }
+ return err
+}
+
+const hyphen = "-"
+
+func (l *logLine) assignVhost(vhost string) error {
+ if vhost == hyphen {
+ return nil
+ }
+ // nginx $host and $http_host returns ipv6 in [], apache not
+ if idx := strings.IndexByte(vhost, ']'); idx > 0 {
+ vhost = vhost[1:idx]
+ }
+ l.vhost = vhost
+ return nil
+}
+
+func (l *logLine) assignPort(port string) error {
+ if port == hyphen {
+ return nil
+ }
+ if !isPortValid(port) {
+ return fmt.Errorf("assign '%s' : %w", port, errBadPort)
+ }
+ l.port = port
+ return nil
+}
+
+func (l *logLine) assignVhostWithPort(vhostPort string) error {
+ if vhostPort == hyphen {
+ return nil
+ }
+ idx := strings.LastIndexByte(vhostPort, ':')
+ if idx == -1 {
+ return fmt.Errorf("assign '%s' : %w", vhostPort, errBadVhostPort)
+ }
+ if err := l.assignPort(vhostPort[idx+1:]); err != nil {
+ return fmt.Errorf("assign '%s' : %w", vhostPort, errBadVhostPort)
+ }
+ if err := l.assignVhost(vhostPort[0:idx]); err != nil {
+ return fmt.Errorf("assign '%s' : %w", vhostPort, errBadVhostPort)
+ }
+ return nil
+}
+
+func (l *logLine) assignReqScheme(scheme string) error {
+ if scheme == hyphen {
+ return nil
+ }
+ if !isSchemeValid(scheme) {
+ return fmt.Errorf("assign '%s' : %w", scheme, errBadReqScheme)
+ }
+ l.reqScheme = scheme
+ return nil
+}
+
+func (l *logLine) assignReqClient(client string) error {
+ if client == hyphen {
+ return nil
+ }
+ l.reqClient = client
+ return nil
+}
+
+func (l *logLine) assignRequest(request string) error {
+ if request == hyphen {
+ return nil
+ }
+ var first, last int
+ if first = strings.IndexByte(request, ' '); first < 0 {
+ return fmt.Errorf("assign '%s': %w", request, errBadRequest)
+ }
+ if last = strings.LastIndexByte(request, ' '); first == last {
+ return fmt.Errorf("assign '%s': %w", request, errBadRequest)
+ }
+ proto := request[last+1:]
+ url := request[first+1 : last]
+ method := request[0:first]
+ if err := l.assignReqMethod(method); err != nil {
+ return err
+ }
+ if err := l.assignReqURL(url); err != nil {
+ return err
+ }
+ return l.assignReqProto(proto)
+}
+
+func (l *logLine) assignReqMethod(method string) error {
+ if method == hyphen {
+ return nil
+ }
+ if !isReqMethodValid(method) {
+ return fmt.Errorf("assign '%s' : %w", method, errBadReqMethod)
+ }
+ l.reqMethod = method
+ return nil
+}
+
+func (l *logLine) assignReqURL(url string) error {
+ if url == hyphen {
+ return nil
+ }
+ if isEmptyString(url) {
+ return fmt.Errorf("assign '%s' : %w", url, errBadReqURL)
+ }
+ l.reqURL = url
+ return nil
+}
+
+func (l *logLine) assignReqProto(proto string) error {
+ if proto == hyphen {
+ return nil
+ }
+ if !isReqProtoValid(proto) {
+ return fmt.Errorf("assign '%s': %w", proto, errBadReqProto)
+ }
+ l.reqProto = proto[5:]
+ return nil
+}
+
+func (l *logLine) assignRespCode(status string) error {
+ if status == hyphen {
+ return nil
+ }
+ v, err := strconv.Atoi(status)
+ if err != nil || !isRespCodeValid(v) {
+ return fmt.Errorf("assign '%s': %w", status, errBadRespCode)
+ }
+ l.respCode = v
+ return nil
+}
+
+func (l *logLine) assignReqSize(size string) error {
+ // apache: can be "-" according web_log py regexp.
+ if size == hyphen {
+ l.reqSize = 0
+ return nil
+ }
+ v, err := strconv.Atoi(size)
+ if err != nil || !isSizeValid(v) {
+ return fmt.Errorf("assign '%s': %w", size, errBadReqSize)
+ }
+ l.reqSize = v
+ return nil
+}
+
+func (l *logLine) assignRespSize(size string) error {
+ // apache: %b. In CLF format, i.e. a '-' rather than a 0 when no bytes are sent.
+ if size == hyphen {
+ l.respSize = 0
+ return nil
+ }
+ v, err := strconv.Atoi(size)
+ if err != nil || !isSizeValid(v) {
+ return fmt.Errorf("assign '%s': %w", size, errBadRespSize)
+ }
+ l.respSize = v
+ return nil
+}
+
+func (l *logLine) assignReqProcTime(time string) error {
+ if time == hyphen {
+ return nil
+ }
+ if time == "0.000" {
+ l.reqProcTime = 0
+ return nil
+ }
+ v, err := strconv.ParseFloat(time, 64)
+ if err != nil || !isTimeValid(v) {
+ return fmt.Errorf("assign '%s': %w", time, errBadReqProcTime)
+ }
+ l.reqProcTime = v * timeMultiplier(time)
+ return nil
+}
+
+func isUpstreamTimeSeparator(r rune) bool { return r == ',' || r == ':' }
+
+func (l *logLine) assignUpsRespTime(time string) error {
+ if time == hyphen {
+ return nil
+ }
+
+ // the upstream response time string can contain multiple values, separated
+ // by commas (in case the request was handled by multiple servers), or colons
+ // (in case the request passed between multiple server groups via an internal redirect)
+ // the individual values should be summed up to obtain the correct amount of time
+ // the request spent in upstream
+ var sum float64
+ for _, val := range strings.FieldsFunc(time, isUpstreamTimeSeparator) {
+ val = strings.TrimSpace(val)
+ v, err := strconv.ParseFloat(val, 64)
+ if err != nil || !isTimeValid(v) {
+ return fmt.Errorf("assign '%s': %w", time, errBadUpsRespTime)
+ }
+
+ sum += v
+ }
+
+ l.upsRespTime = sum * timeMultiplier(time)
+ return nil
+}
+
+func (l *logLine) assignSSLProto(proto string) error {
+ if proto == hyphen {
+ return nil
+ }
+ if !isSSLProtoValid(proto) {
+ return fmt.Errorf("assign '%s': %w", proto, errBadSSLProto)
+ }
+ l.sslProto = proto
+ return nil
+}
+
+func (l *logLine) assignSSLCipherSuite(cipher string) error {
+ if cipher == hyphen {
+ return nil
+ }
+ if strings.IndexByte(cipher, '-') <= 0 && strings.IndexByte(cipher, '_') <= 0 {
+ return fmt.Errorf("assign '%s': %w", cipher, errBadSSLCipherSuite)
+ }
+ l.sslCipherSuite = cipher
+ return nil
+}
+
+func (l *logLine) assignCustom(field, value string) error {
+ if len(l.custom.fields) == 0 || value == hyphen {
+ return nil
+ }
+ if _, ok := l.custom.fields[field]; ok {
+ l.custom.values = append(l.custom.values, customValue{name: field, value: value})
+ }
+ return nil
+}
+
+func (l *logLine) verify() error {
+ if l.empty() {
+ return fmt.Errorf("verify: %w", errEmptyLine)
+ }
+ if l.hasRespCode() && !l.isRespCodeValid() {
+ return fmt.Errorf("verify '%d': %w", l.respCode, errBadRespCode)
+ }
+ if l.hasVhost() && !l.isVhostValid() {
+ return fmt.Errorf("verify '%s': %w", l.vhost, errBadVhost)
+ }
+ if l.hasPort() && !l.isPortValid() {
+ return fmt.Errorf("verify '%s': %w", l.port, errBadPort)
+ }
+ if l.hasReqScheme() && !l.isSchemeValid() {
+ return fmt.Errorf("verify '%s': %w", l.reqScheme, errBadReqScheme)
+ }
+ if l.hasReqClient() && !l.isClientValid() {
+ return fmt.Errorf("verify '%s': %w", l.reqClient, errBadReqClient)
+ }
+ if l.hasReqMethod() && !l.isMethodValid() {
+ return fmt.Errorf("verify '%s': %w", l.reqMethod, errBadReqMethod)
+ }
+ if l.hasReqURL() && !l.isURLValid() {
+ return fmt.Errorf("verify '%s': %w", l.reqURL, errBadReqURL)
+ }
+ if l.hasReqProto() && !l.isProtoValid() {
+ return fmt.Errorf("verify '%s': %w", l.reqProto, errBadReqProto)
+ }
+ if l.hasReqSize() && !l.isReqSizeValid() {
+ return fmt.Errorf("verify '%d': %w", l.reqSize, errBadReqSize)
+ }
+ if l.hasRespSize() && !l.isRespSizeValid() {
+ return fmt.Errorf("verify '%d': %w", l.respSize, errBadRespSize)
+ }
+ if l.hasReqProcTime() && !l.isReqProcTimeValid() {
+ return fmt.Errorf("verify '%f': %w", l.reqProcTime, errBadReqProcTime)
+ }
+ if l.hasUpsRespTime() && !l.isUpsRespTimeValid() {
+ return fmt.Errorf("verify '%f': %w", l.upsRespTime, errBadUpsRespTime)
+ }
+ if l.hasSSLProto() && !l.isSSLProtoValid() {
+ return fmt.Errorf("verify '%s': %w", l.sslProto, errBadSSLProto)
+ }
+ if l.hasSSLCipherSuite() && !l.isSSLCipherSuiteValid() {
+ return fmt.Errorf("verify '%s': %w", l.sslCipherSuite, errBadSSLCipherSuite)
+ }
+ return nil
+}
+
+func (l *logLine) empty() bool { return !l.hasWebFields() && !l.hasCustomFields() }
+func (l *logLine) hasCustomFields() bool { return len(l.custom.values) > 0 }
+func (l *logLine) hasWebFields() bool { return l.web != emptyWebFields }
+func (l *logLine) hasVhost() bool { return !isEmptyString(l.vhost) }
+func (l *logLine) hasPort() bool { return !isEmptyString(l.port) }
+func (l *logLine) hasReqScheme() bool { return !isEmptyString(l.reqScheme) }
+func (l *logLine) hasReqClient() bool { return !isEmptyString(l.reqClient) }
+func (l *logLine) hasReqMethod() bool { return !isEmptyString(l.reqMethod) }
+func (l *logLine) hasReqURL() bool { return !isEmptyString(l.reqURL) }
+func (l *logLine) hasReqProto() bool { return !isEmptyString(l.reqProto) }
+func (l *logLine) hasRespCode() bool { return !isEmptyNumber(l.respCode) }
+func (l *logLine) hasReqSize() bool { return !isEmptyNumber(l.reqSize) }
+func (l *logLine) hasRespSize() bool { return !isEmptyNumber(l.respSize) }
+func (l *logLine) hasReqProcTime() bool { return !isEmptyNumber(int(l.reqProcTime)) }
+func (l *logLine) hasUpsRespTime() bool { return !isEmptyNumber(int(l.upsRespTime)) }
+func (l *logLine) hasSSLProto() bool { return !isEmptyString(l.sslProto) }
+func (l *logLine) hasSSLCipherSuite() bool { return !isEmptyString(l.sslCipherSuite) }
+func (l *logLine) isVhostValid() bool { return reVhost.MatchString(l.vhost) }
+func (l *logLine) isPortValid() bool { return isPortValid(l.port) }
+func (l *logLine) isSchemeValid() bool { return isSchemeValid(l.reqScheme) }
+func (l *logLine) isClientValid() bool { return reClient.MatchString(l.reqClient) }
+func (l *logLine) isMethodValid() bool { return isReqMethodValid(l.reqMethod) }
+func (l *logLine) isURLValid() bool { return !isEmptyString(l.reqURL) }
+func (l *logLine) isProtoValid() bool { return isReqProtoVerValid(l.reqProto) }
+func (l *logLine) isRespCodeValid() bool { return isRespCodeValid(l.respCode) }
+func (l *logLine) isReqSizeValid() bool { return isSizeValid(l.reqSize) }
+func (l *logLine) isRespSizeValid() bool { return isSizeValid(l.respSize) }
+func (l *logLine) isReqProcTimeValid() bool { return isTimeValid(l.reqProcTime) }
+func (l *logLine) isUpsRespTimeValid() bool { return isTimeValid(l.upsRespTime) }
+func (l *logLine) isSSLProtoValid() bool { return isSSLProtoValid(l.sslProto) }
+func (l *logLine) isSSLCipherSuiteValid() bool { return reCipherSuite.MatchString(l.sslCipherSuite) }
+
+func (l *logLine) reset() {
+ l.web = emptyWebFields
+ l.custom.values = l.custom.values[:0]
+}
+
+var (
+ // TODO: reClient doesn't work with %h when HostnameLookups is On.
+ reVhost = regexp.MustCompile(`^[a-zA-Z0-9-:.]+$`)
+ reClient = regexp.MustCompile(`^([\da-f:.]+|localhost)$`)
+ reCipherSuite = regexp.MustCompile(`^[A-Z0-9-_]+$`) // openssl -v
+)
+
+var emptyWebFields = web{
+ vhost: emptyString,
+ port: emptyString,
+ reqScheme: emptyString,
+ reqClient: emptyString,
+ reqMethod: emptyString,
+ reqURL: emptyString,
+ reqProto: emptyString,
+ reqSize: emptyNumber,
+ reqProcTime: emptyNumber,
+ respCode: emptyNumber,
+ respSize: emptyNumber,
+ upsRespTime: emptyNumber,
+ sslProto: emptyString,
+ sslCipherSuite: emptyString,
+}
+
+const (
+ emptyString = "__empty_string__"
+ emptyNumber = -9999
+)
+
+func isEmptyString(s string) bool {
+ return s == emptyString || s == ""
+}
+
+func isEmptyNumber(n int) bool {
+ return n == emptyNumber
+}
+
+func isReqMethodValid(method string) bool {
+ // https://www.iana.org/assignments/http-methods/http-methods.xhtml
+ switch method {
+ case "GET",
+ "ACL",
+ "BASELINE-CONTROL",
+ "BIND",
+ "CHECKIN",
+ "CHECKOUT",
+ "CONNECT",
+ "COPY",
+ "DELETE",
+ "HEAD",
+ "LABEL",
+ "LINK",
+ "LOCK",
+ "MERGE",
+ "MKACTIVITY",
+ "MKCALENDAR",
+ "MKCOL",
+ "MKREDIRECTREF",
+ "MKWORKSPACE",
+ "MOVE",
+ "OPTIONS",
+ "ORDERPATCH",
+ "PATCH",
+ "POST",
+ "PRI",
+ "PROPFIND",
+ "PROPPATCH",
+ "PURGE", // not a standardized HTTP method
+ "PUT",
+ "REBIND",
+ "REPORT",
+ "SEARCH",
+ "TRACE",
+ "UNBIND",
+ "UNCHECKOUT",
+ "UNLINK",
+ "UNLOCK",
+ "UPDATE",
+ "UPDATEREDIRECTREF":
+ return true
+ }
+ return false
+}
+
+func isReqProtoValid(proto string) bool {
+ return len(proto) >= 6 && proto[:5] == "HTTP/" && isReqProtoVerValid(proto[5:])
+}
+
+func isReqProtoVerValid(version string) bool {
+ switch version {
+ case "1.1", "1", "1.0", "2", "2.0", "3", "3.0":
+ return true
+ }
+ return false
+}
+
+func isPortValid(port string) bool {
+ v, err := strconv.Atoi(port)
+ return err == nil && v >= 80 && v <= 49151
+}
+
+func isSchemeValid(scheme string) bool {
+ return scheme == "http" || scheme == "https"
+}
+
+func isRespCodeValid(code int) bool {
+ // rfc7231
+ // Informational responses (100–199),
+ // Successful responses (200–299),
+ // Redirects (300–399),
+ // Client errors (400–499),
+ // Server errors (500–599).
+ return code >= 100 && code <= 600
+}
+
+func isSizeValid(size int) bool {
+ return size >= 0
+}
+
+func isTimeValid(time float64) bool {
+ return time >= 0
+}
+
+func isSSLProtoValid(proto string) bool {
+ if proto == "TLSv1.2" {
+ return true
+ }
+ switch proto {
+ case "TLSv1.3", "SSLv2", "SSLv3", "TLSv1", "TLSv1.1":
+ return true
+ }
+ return false
+}
+
+func timeMultiplier(time string) float64 {
+ // TODO: Change code to detect and modify properly IIS time (in milliseconds)
+ // Convert to microseconds:
+ // - nginx time is in seconds with a milliseconds' resolution.
+ if strings.IndexByte(time, '.') > 0 {
+ return 1e6
+ }
+ // - apache time is in microseconds.
+ return 1
+}
diff --git a/src/go/collectors/go.d.plugin/modules/weblog/logline_test.go b/src/go/collectors/go.d.plugin/modules/weblog/logline_test.go
new file mode 100644
index 000000000..d3055863a
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/weblog/logline_test.go
@@ -0,0 +1,669 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package weblog
+
+import (
+ "errors"
+ "fmt"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+const (
+ emptyStr = ""
+)
+
+var emptyLogLine = *newEmptyLogLine()
+
+func TestLogLine_Assign(t *testing.T) {
+ type subTest struct {
+ input string
+ wantLine logLine
+ wantErr error
+ }
+ type test struct {
+ name string
+ fields []string
+ cases []subTest
+ }
+ tests := []test{
+ {
+ name: "Vhost",
+ fields: []string{
+ "host",
+ "http_host",
+ "v",
+ },
+ cases: []subTest{
+ {input: "1.1.1.1", wantLine: logLine{web: web{vhost: "1.1.1.1"}}},
+ {input: "::1", wantLine: logLine{web: web{vhost: "::1"}}},
+ {input: "[::1]", wantLine: logLine{web: web{vhost: "::1"}}},
+ {input: "1ce:1ce::babe", wantLine: logLine{web: web{vhost: "1ce:1ce::babe"}}},
+ {input: "[1ce:1ce::babe]", wantLine: logLine{web: web{vhost: "1ce:1ce::babe"}}},
+ {input: "localhost", wantLine: logLine{web: web{vhost: "localhost"}}},
+ {input: "debian10.debian", wantLine: logLine{web: web{vhost: "debian10.debian"}}},
+ {input: "my_vhost", wantLine: logLine{web: web{vhost: "my_vhost"}}},
+ {input: emptyStr, wantLine: emptyLogLine},
+ {input: hyphen, wantLine: emptyLogLine},
+ },
+ },
+ {
+ name: "Server Port",
+ fields: []string{
+ "server_port",
+ "p",
+ },
+ cases: []subTest{
+ {input: "80", wantLine: logLine{web: web{port: "80"}}},
+ {input: "8081", wantLine: logLine{web: web{port: "8081"}}},
+ {input: "30000", wantLine: logLine{web: web{port: "30000"}}},
+ {input: emptyStr, wantLine: emptyLogLine},
+ {input: hyphen, wantLine: emptyLogLine},
+ {input: "-1", wantLine: emptyLogLine, wantErr: errBadPort},
+ {input: "0", wantLine: emptyLogLine, wantErr: errBadPort},
+ {input: "50000", wantLine: emptyLogLine, wantErr: errBadPort},
+ },
+ },
+ {
+ name: "Vhost With Port",
+ fields: []string{
+ "host:$server_port",
+ "v:%p",
+ },
+ cases: []subTest{
+ {input: "1.1.1.1:80", wantLine: logLine{web: web{vhost: "1.1.1.1", port: "80"}}},
+ {input: "::1:80", wantLine: logLine{web: web{vhost: "::1", port: "80"}}},
+ {input: "[::1]:80", wantLine: logLine{web: web{vhost: "::1", port: "80"}}},
+ {input: "1ce:1ce::babe:80", wantLine: logLine{web: web{vhost: "1ce:1ce::babe", port: "80"}}},
+ {input: "debian10.debian:81", wantLine: logLine{web: web{vhost: "debian10.debian", port: "81"}}},
+ {input: emptyStr, wantLine: emptyLogLine},
+ {input: hyphen, wantLine: emptyLogLine},
+ {input: "1.1.1.1", wantLine: emptyLogLine, wantErr: errBadVhostPort},
+ {input: "1.1.1.1:", wantLine: emptyLogLine, wantErr: errBadVhostPort},
+ {input: "1.1.1.1 80", wantLine: emptyLogLine, wantErr: errBadVhostPort},
+ {input: "1.1.1.1:20", wantLine: emptyLogLine, wantErr: errBadVhostPort},
+ {input: "1.1.1.1:50000", wantLine: emptyLogLine, wantErr: errBadVhostPort},
+ },
+ },
+ {
+ name: "Scheme",
+ fields: []string{
+ "scheme",
+ },
+ cases: []subTest{
+ {input: "http", wantLine: logLine{web: web{reqScheme: "http"}}},
+ {input: "https", wantLine: logLine{web: web{reqScheme: "https"}}},
+ {input: emptyStr, wantLine: emptyLogLine},
+ {input: hyphen, wantLine: emptyLogLine},
+ {input: "HTTP", wantLine: emptyLogLine, wantErr: errBadReqScheme},
+ {input: "HTTPS", wantLine: emptyLogLine, wantErr: errBadReqScheme},
+ },
+ },
+ {
+ name: "Client",
+ fields: []string{
+ "remote_addr",
+ "a",
+ "h",
+ },
+ cases: []subTest{
+ {input: "1.1.1.1", wantLine: logLine{web: web{reqClient: "1.1.1.1"}}},
+ {input: "debian10", wantLine: logLine{web: web{reqClient: "debian10"}}},
+ {input: emptyStr, wantLine: emptyLogLine},
+ {input: hyphen, wantLine: emptyLogLine},
+ },
+ },
+ {
+ name: "Request",
+ fields: []string{
+ "request",
+ "r",
+ },
+ cases: []subTest{
+ {input: "GET / HTTP/1.0", wantLine: logLine{web: web{reqMethod: "GET", reqURL: "/", reqProto: "1.0"}}},
+ {input: "HEAD /ihs.gif HTTP/1.0", wantLine: logLine{web: web{reqMethod: "HEAD", reqURL: "/ihs.gif", reqProto: "1.0"}}},
+ {input: "POST /ihs.gif HTTP/1.0", wantLine: logLine{web: web{reqMethod: "POST", reqURL: "/ihs.gif", reqProto: "1.0"}}},
+ {input: "PUT /ihs.gif HTTP/1.0", wantLine: logLine{web: web{reqMethod: "PUT", reqURL: "/ihs.gif", reqProto: "1.0"}}},
+ {input: "PATCH /ihs.gif HTTP/1.0", wantLine: logLine{web: web{reqMethod: "PATCH", reqURL: "/ihs.gif", reqProto: "1.0"}}},
+ {input: "DELETE /ihs.gif HTTP/1.0", wantLine: logLine{web: web{reqMethod: "DELETE", reqURL: "/ihs.gif", reqProto: "1.0"}}},
+ {input: "OPTIONS /ihs.gif HTTP/1.0", wantLine: logLine{web: web{reqMethod: "OPTIONS", reqURL: "/ihs.gif", reqProto: "1.0"}}},
+ {input: "TRACE /ihs.gif HTTP/1.0", wantLine: logLine{web: web{reqMethod: "TRACE", reqURL: "/ihs.gif", reqProto: "1.0"}}},
+ {input: "CONNECT ip.cn:443 HTTP/1.1", wantLine: logLine{web: web{reqMethod: "CONNECT", reqURL: "ip.cn:443", reqProto: "1.1"}}},
+ {input: "MKCOL ip.cn:443 HTTP/1.1", wantLine: logLine{web: web{reqMethod: "MKCOL", reqURL: "ip.cn:443", reqProto: "1.1"}}},
+ {input: "PROPFIND ip.cn:443 HTTP/1.1", wantLine: logLine{web: web{reqMethod: "PROPFIND", reqURL: "ip.cn:443", reqProto: "1.1"}}},
+ {input: "MOVE ip.cn:443 HTTP/1.1", wantLine: logLine{web: web{reqMethod: "MOVE", reqURL: "ip.cn:443", reqProto: "1.1"}}},
+ {input: "SEARCH ip.cn:443 HTTP/1.1", wantLine: logLine{web: web{reqMethod: "SEARCH", reqURL: "ip.cn:443", reqProto: "1.1"}}},
+ {input: "GET / HTTP/1.1", wantLine: logLine{web: web{reqMethod: "GET", reqURL: "/", reqProto: "1.1"}}},
+ {input: "GET / HTTP/2", wantLine: logLine{web: web{reqMethod: "GET", reqURL: "/", reqProto: "2"}}},
+ {input: "GET / HTTP/2.0", wantLine: logLine{web: web{reqMethod: "GET", reqURL: "/", reqProto: "2.0"}}},
+ {input: "GET /invalid_version http/1.1", wantLine: logLine{web: web{reqMethod: "GET", reqURL: "/invalid_version", reqProto: emptyString}}, wantErr: errBadReqProto},
+ {input: emptyStr, wantLine: emptyLogLine},
+ {input: hyphen, wantLine: emptyLogLine},
+ {input: "GET no_version", wantLine: emptyLogLine, wantErr: errBadRequest},
+ {input: "GOT / HTTP/2", wantLine: emptyLogLine, wantErr: errBadReqMethod},
+ {input: "get / HTTP/2", wantLine: emptyLogLine, wantErr: errBadReqMethod},
+ {input: "x04\x01\x00P$3\xFE\xEA\x00", wantLine: emptyLogLine, wantErr: errBadRequest},
+ },
+ },
+ {
+ name: "Request HTTP Method",
+ fields: []string{
+ "request_method",
+ "m",
+ },
+ cases: []subTest{
+ {input: "GET", wantLine: logLine{web: web{reqMethod: "GET"}}},
+ {input: "HEAD", wantLine: logLine{web: web{reqMethod: "HEAD"}}},
+ {input: "POST", wantLine: logLine{web: web{reqMethod: "POST"}}},
+ {input: "PUT", wantLine: logLine{web: web{reqMethod: "PUT"}}},
+ {input: "PATCH", wantLine: logLine{web: web{reqMethod: "PATCH"}}},
+ {input: "DELETE", wantLine: logLine{web: web{reqMethod: "DELETE"}}},
+ {input: "OPTIONS", wantLine: logLine{web: web{reqMethod: "OPTIONS"}}},
+ {input: "TRACE", wantLine: logLine{web: web{reqMethod: "TRACE"}}},
+ {input: "CONNECT", wantLine: logLine{web: web{reqMethod: "CONNECT"}}},
+ {input: "MKCOL", wantLine: logLine{web: web{reqMethod: "MKCOL"}}},
+ {input: "PROPFIND", wantLine: logLine{web: web{reqMethod: "PROPFIND"}}},
+ {input: "MOVE", wantLine: logLine{web: web{reqMethod: "MOVE"}}},
+ {input: "SEARCH", wantLine: logLine{web: web{reqMethod: "SEARCH"}}},
+ {input: "PURGE", wantLine: logLine{web: web{reqMethod: "PURGE"}}},
+ {input: emptyStr, wantLine: emptyLogLine},
+ {input: hyphen, wantLine: emptyLogLine},
+ {input: "GET no_version", wantLine: emptyLogLine, wantErr: errBadReqMethod},
+ {input: "GOT / HTTP/2", wantLine: emptyLogLine, wantErr: errBadReqMethod},
+ {input: "get / HTTP/2", wantLine: emptyLogLine, wantErr: errBadReqMethod},
+ },
+ },
+ {
+ name: "Request URL",
+ fields: []string{
+ "request_uri",
+ "U",
+ },
+ cases: []subTest{
+ {input: "/server-status?auto", wantLine: logLine{web: web{reqURL: "/server-status?auto"}}},
+ {input: "/default.html", wantLine: logLine{web: web{reqURL: "/default.html"}}},
+ {input: "10.0.0.1:3128", wantLine: logLine{web: web{reqURL: "10.0.0.1:3128"}}},
+ {input: emptyStr, wantLine: emptyLogLine},
+ {input: hyphen, wantLine: emptyLogLine},
+ },
+ },
+ {
+ name: "Request HTTP Protocol",
+ fields: []string{
+ "server_protocol",
+ "H",
+ },
+ cases: []subTest{
+ {input: "HTTP/1.0", wantLine: logLine{web: web{reqProto: "1.0"}}},
+ {input: "HTTP/1.1", wantLine: logLine{web: web{reqProto: "1.1"}}},
+ {input: "HTTP/2", wantLine: logLine{web: web{reqProto: "2"}}},
+ {input: "HTTP/2.0", wantLine: logLine{web: web{reqProto: "2.0"}}},
+ {input: "HTTP/3", wantLine: logLine{web: web{reqProto: "3"}}},
+ {input: "HTTP/3.0", wantLine: logLine{web: web{reqProto: "3.0"}}},
+ {input: emptyStr, wantLine: emptyLogLine},
+ {input: hyphen, wantLine: emptyLogLine},
+ {input: "1.1", wantLine: emptyLogLine, wantErr: errBadReqProto},
+ {input: "http/1.1", wantLine: emptyLogLine, wantErr: errBadReqProto},
+ },
+ },
+ {
+ name: "Response Status Code",
+ fields: []string{
+ "status",
+ "s",
+ ">s",
+ },
+ cases: []subTest{
+ {input: "100", wantLine: logLine{web: web{respCode: 100}}},
+ {input: "200", wantLine: logLine{web: web{respCode: 200}}},
+ {input: "300", wantLine: logLine{web: web{respCode: 300}}},
+ {input: "400", wantLine: logLine{web: web{respCode: 400}}},
+ {input: "500", wantLine: logLine{web: web{respCode: 500}}},
+ {input: "600", wantLine: logLine{web: web{respCode: 600}}},
+ {input: emptyStr, wantLine: emptyLogLine},
+ {input: hyphen, wantLine: emptyLogLine},
+ {input: "99", wantLine: emptyLogLine, wantErr: errBadRespCode},
+ {input: "601", wantLine: emptyLogLine, wantErr: errBadRespCode},
+ {input: "200 ", wantLine: emptyLogLine, wantErr: errBadRespCode},
+ {input: "0.222", wantLine: emptyLogLine, wantErr: errBadRespCode},
+ {input: "localhost", wantLine: emptyLogLine, wantErr: errBadRespCode},
+ },
+ },
+ {
+ name: "Request Size",
+ fields: []string{
+ "request_length",
+ "I",
+ },
+ cases: []subTest{
+ {input: "15", wantLine: logLine{web: web{reqSize: 15}}},
+ {input: "1000000", wantLine: logLine{web: web{reqSize: 1000000}}},
+ {input: emptyStr, wantLine: emptyLogLine},
+ {input: hyphen, wantLine: logLine{web: web{reqSize: 0}}},
+ {input: "-1", wantLine: emptyLogLine, wantErr: errBadReqSize},
+ {input: "100.222", wantLine: emptyLogLine, wantErr: errBadReqSize},
+ {input: "invalid", wantLine: emptyLogLine, wantErr: errBadReqSize},
+ },
+ },
+ {
+ name: "Response Size",
+ fields: []string{
+ "bytes_sent",
+ "body_bytes_sent",
+ "O",
+ "B",
+ "b",
+ },
+ cases: []subTest{
+ {input: "15", wantLine: logLine{web: web{respSize: 15}}},
+ {input: "1000000", wantLine: logLine{web: web{respSize: 1000000}}},
+ {input: emptyStr, wantLine: emptyLogLine},
+ {input: hyphen, wantLine: logLine{web: web{respSize: 0}}},
+ {input: "-1", wantLine: emptyLogLine, wantErr: errBadRespSize},
+ {input: "100.222", wantLine: emptyLogLine, wantErr: errBadRespSize},
+ {input: "invalid", wantLine: emptyLogLine, wantErr: errBadRespSize},
+ },
+ },
+ {
+ name: "Request Processing Time",
+ fields: []string{
+ "request_time",
+ "D",
+ },
+ cases: []subTest{
+ {input: "100222", wantLine: logLine{web: web{reqProcTime: 100222}}},
+ {input: "100.222", wantLine: logLine{web: web{reqProcTime: 100222000}}},
+ {input: emptyStr, wantLine: emptyLogLine},
+ {input: hyphen, wantLine: emptyLogLine},
+ {input: "-1", wantLine: emptyLogLine, wantErr: errBadReqProcTime},
+ {input: "0.333,0.444,0.555", wantLine: emptyLogLine, wantErr: errBadReqProcTime},
+ {input: "number", wantLine: emptyLogLine, wantErr: errBadReqProcTime},
+ },
+ },
+ {
+ name: "Upstream Response Time",
+ fields: []string{
+ "upstream_response_time",
+ },
+ cases: []subTest{
+ {input: "100222", wantLine: logLine{web: web{upsRespTime: 100222}}},
+ {input: "100.222", wantLine: logLine{web: web{upsRespTime: 100222000}}},
+ {input: "0.100 , 0.400 : 0.200 ", wantLine: logLine{web: web{upsRespTime: 700000}}},
+ {input: emptyStr, wantLine: emptyLogLine},
+ {input: hyphen, wantLine: emptyLogLine},
+ {input: "-1", wantLine: emptyLogLine, wantErr: errBadUpsRespTime},
+ {input: "number", wantLine: emptyLogLine, wantErr: errBadUpsRespTime},
+ },
+ },
+ {
+ name: "SSL Protocol",
+ fields: []string{
+ "ssl_protocol",
+ },
+ cases: []subTest{
+ {input: "SSLv3", wantLine: logLine{web: web{sslProto: "SSLv3"}}},
+ {input: "SSLv2", wantLine: logLine{web: web{sslProto: "SSLv2"}}},
+ {input: "TLSv1", wantLine: logLine{web: web{sslProto: "TLSv1"}}},
+ {input: "TLSv1.1", wantLine: logLine{web: web{sslProto: "TLSv1.1"}}},
+ {input: "TLSv1.2", wantLine: logLine{web: web{sslProto: "TLSv1.2"}}},
+ {input: "TLSv1.3", wantLine: logLine{web: web{sslProto: "TLSv1.3"}}},
+ {input: emptyStr, wantLine: emptyLogLine},
+ {input: hyphen, wantLine: emptyLogLine},
+ {input: "-1", wantLine: emptyLogLine, wantErr: errBadSSLProto},
+ {input: "invalid", wantLine: emptyLogLine, wantErr: errBadSSLProto},
+ },
+ },
+ {
+ name: "SSL Cipher Suite",
+ fields: []string{
+ "ssl_cipher",
+ },
+ cases: []subTest{
+ {input: "ECDHE-RSA-AES256-SHA", wantLine: logLine{web: web{sslCipherSuite: "ECDHE-RSA-AES256-SHA"}}},
+ {input: "DHE-RSA-AES256-SHA", wantLine: logLine{web: web{sslCipherSuite: "DHE-RSA-AES256-SHA"}}},
+ {input: "AES256-SHA", wantLine: logLine{web: web{sslCipherSuite: "AES256-SHA"}}},
+ {input: "PSK-RC4-SHA", wantLine: logLine{web: web{sslCipherSuite: "PSK-RC4-SHA"}}},
+ {input: "TLS_AES_256_GCM_SHA384", wantLine: logLine{web: web{sslCipherSuite: "TLS_AES_256_GCM_SHA384"}}},
+ {input: emptyStr, wantLine: emptyLogLine},
+ {input: hyphen, wantLine: emptyLogLine},
+ {input: "-1", wantLine: emptyLogLine, wantErr: errBadSSLCipherSuite},
+ {input: "invalid", wantLine: emptyLogLine, wantErr: errBadSSLCipherSuite},
+ },
+ },
+ {
+ name: "Custom Fields",
+ fields: []string{
+ "custom",
+ },
+ cases: []subTest{
+ {input: "POST", wantLine: logLine{custom: custom{values: []customValue{{name: "custom", value: "POST"}}}}},
+ {input: "/example.com", wantLine: logLine{custom: custom{values: []customValue{{name: "custom", value: "/example.com"}}}}},
+ {input: "HTTP/1.1", wantLine: logLine{custom: custom{values: []customValue{{name: "custom", value: "HTTP/1.1"}}}}},
+ {input: "0.333,0.444,0.555", wantLine: logLine{custom: custom{values: []customValue{{name: "custom", value: "0.333,0.444,0.555"}}}}},
+ {input: "-1", wantLine: logLine{custom: custom{values: []customValue{{name: "custom", value: "-1"}}}}},
+ {input: "invalid", wantLine: logLine{custom: custom{values: []customValue{{name: "custom", value: "invalid"}}}}},
+ {input: emptyStr, wantLine: emptyLogLine},
+ {input: hyphen, wantLine: emptyLogLine},
+ },
+ },
+ {
+ name: "Custom Fields Not Exist",
+ fields: []string{
+ "custom_field_not_exist",
+ },
+ cases: []subTest{
+ {input: "POST", wantLine: emptyLogLine},
+ {input: "/example.com", wantLine: emptyLogLine},
+ {input: "HTTP/1.1", wantLine: emptyLogLine},
+ {input: "0.333,0.444,0.555", wantLine: emptyLogLine},
+ {input: "-1", wantLine: emptyLogLine},
+ {input: "invalid", wantLine: emptyLogLine},
+ {input: emptyStr, wantLine: emptyLogLine},
+ {input: hyphen, wantLine: emptyLogLine},
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ for _, field := range tt.fields {
+ for i, tc := range tt.cases {
+ name := fmt.Sprintf("[%s:%d]field='%s'|line='%s'", tt.name, i+1, field, tc.input)
+ t.Run(name, func(t *testing.T) {
+
+ line := newEmptyLogLineWithFields()
+ err := line.Assign(field, tc.input)
+
+ if tc.wantErr != nil {
+ require.Error(t, err)
+ assert.Truef(t, errors.Is(err, tc.wantErr), "expected '%v' error, got '%v'", tc.wantErr, err)
+ } else {
+ require.NoError(t, err)
+ }
+
+ expected := prepareLogLine(field, tc.wantLine)
+ assert.Equal(t, expected, *line)
+ })
+ }
+ }
+ }
+}
+
+func TestLogLine_verify(t *testing.T) {
+ type subTest struct {
+ line logLine
+ wantErr error
+ }
+ tests := []struct {
+ name string
+ field string
+ cases []subTest
+ }{
+ {
+ name: "Vhost",
+ field: "host",
+ cases: []subTest{
+ {line: logLine{web: web{vhost: "192.168.0.1"}}},
+ {line: logLine{web: web{vhost: "debian10.debian"}}},
+ {line: logLine{web: web{vhost: "1ce:1ce::babe"}}},
+ {line: logLine{web: web{vhost: "localhost"}}},
+ {line: logLine{web: web{vhost: "invalid_vhost"}}, wantErr: errBadVhost},
+ {line: logLine{web: web{vhost: "http://192.168.0.1/"}}, wantErr: errBadVhost},
+ },
+ },
+ {
+ name: "Server Port",
+ field: "server_port",
+ cases: []subTest{
+ {line: logLine{web: web{port: "80"}}},
+ {line: logLine{web: web{port: "8081"}}},
+ {line: logLine{web: web{port: "79"}}, wantErr: errBadPort},
+ {line: logLine{web: web{port: "50000"}}, wantErr: errBadPort},
+ {line: logLine{web: web{port: "0.0.0.0"}}, wantErr: errBadPort},
+ },
+ },
+ {
+ name: "Scheme",
+ field: "scheme",
+ cases: []subTest{
+ {line: logLine{web: web{reqScheme: "http"}}},
+ {line: logLine{web: web{reqScheme: "https"}}},
+ {line: logLine{web: web{reqScheme: "not_https"}}, wantErr: errBadReqScheme},
+ {line: logLine{web: web{reqScheme: "HTTP"}}, wantErr: errBadReqScheme},
+ {line: logLine{web: web{reqScheme: "HTTPS"}}, wantErr: errBadReqScheme},
+ {line: logLine{web: web{reqScheme: "10"}}, wantErr: errBadReqScheme},
+ },
+ },
+ {
+ name: "Client",
+ field: "remote_addr",
+ cases: []subTest{
+ {line: logLine{web: web{reqClient: "1.1.1.1"}}},
+ {line: logLine{web: web{reqClient: "::1"}}},
+ {line: logLine{web: web{reqClient: "1ce:1ce::babe"}}},
+ {line: logLine{web: web{reqClient: "localhost"}}},
+ {line: logLine{web: web{reqClient: "debian10.debian"}}, wantErr: errBadReqClient},
+ {line: logLine{web: web{reqClient: "invalid"}}, wantErr: errBadReqClient},
+ },
+ },
+ {
+ name: "Request HTTP Method",
+ field: "request_method",
+ cases: []subTest{
+ {line: logLine{web: web{reqMethod: "GET"}}},
+ {line: logLine{web: web{reqMethod: "POST"}}},
+ {line: logLine{web: web{reqMethod: "TRACE"}}},
+ {line: logLine{web: web{reqMethod: "OPTIONS"}}},
+ {line: logLine{web: web{reqMethod: "CONNECT"}}},
+ {line: logLine{web: web{reqMethod: "DELETE"}}},
+ {line: logLine{web: web{reqMethod: "PUT"}}},
+ {line: logLine{web: web{reqMethod: "PATCH"}}},
+ {line: logLine{web: web{reqMethod: "HEAD"}}},
+ {line: logLine{web: web{reqMethod: "MKCOL"}}},
+ {line: logLine{web: web{reqMethod: "PROPFIND"}}},
+ {line: logLine{web: web{reqMethod: "MOVE"}}},
+ {line: logLine{web: web{reqMethod: "SEARCH"}}},
+ {line: logLine{web: web{reqMethod: "Get"}}, wantErr: errBadReqMethod},
+ {line: logLine{web: web{reqMethod: "get"}}, wantErr: errBadReqMethod},
+ },
+ },
+ {
+ name: "Request URL",
+ field: "request_uri",
+ cases: []subTest{
+ {line: logLine{web: web{reqURL: "/"}}},
+ {line: logLine{web: web{reqURL: "/status?full&json"}}},
+ {line: logLine{web: web{reqURL: "/icons/openlogo-75.png"}}},
+ {line: logLine{web: web{reqURL: "status?full&json"}}},
+ {line: logLine{web: web{reqURL: "\"req_url=/ \""}}},
+ {line: logLine{web: web{reqURL: "http://192.168.0.1/"}}},
+ {line: logLine{web: web{reqURL: ""}}},
+ },
+ },
+ {
+ name: "Request HTTP Protocol",
+ field: "server_protocol",
+ cases: []subTest{
+ {line: logLine{web: web{reqProto: "1"}}},
+ {line: logLine{web: web{reqProto: "1.0"}}},
+ {line: logLine{web: web{reqProto: "1.1"}}},
+ {line: logLine{web: web{reqProto: "2.0"}}},
+ {line: logLine{web: web{reqProto: "2"}}},
+ {line: logLine{web: web{reqProto: "0.9"}}, wantErr: errBadReqProto},
+ {line: logLine{web: web{reqProto: "1.1.1"}}, wantErr: errBadReqProto},
+ {line: logLine{web: web{reqProto: "2.2"}}, wantErr: errBadReqProto},
+ {line: logLine{web: web{reqProto: "localhost"}}, wantErr: errBadReqProto},
+ },
+ },
+ {
+ name: "Response Status Code",
+ field: "status",
+ cases: []subTest{
+ {line: logLine{web: web{respCode: 100}}},
+ {line: logLine{web: web{respCode: 200}}},
+ {line: logLine{web: web{respCode: 300}}},
+ {line: logLine{web: web{respCode: 400}}},
+ {line: logLine{web: web{respCode: 500}}},
+ {line: logLine{web: web{respCode: 600}}},
+ {line: logLine{web: web{respCode: -1}}, wantErr: errBadRespCode},
+ {line: logLine{web: web{respCode: 99}}, wantErr: errBadRespCode},
+ {line: logLine{web: web{respCode: 601}}, wantErr: errBadRespCode},
+ },
+ },
+ {
+ name: "Request size",
+ field: "request_length",
+ cases: []subTest{
+ {line: logLine{web: web{reqSize: 0}}},
+ {line: logLine{web: web{reqSize: 100}}},
+ {line: logLine{web: web{reqSize: 1000000}}},
+ {line: logLine{web: web{reqSize: -1}}, wantErr: errBadReqSize},
+ },
+ },
+ {
+ name: "Response size",
+ field: "bytes_sent",
+ cases: []subTest{
+ {line: logLine{web: web{respSize: 0}}},
+ {line: logLine{web: web{respSize: 100}}},
+ {line: logLine{web: web{respSize: 1000000}}},
+ {line: logLine{web: web{respSize: -1}}, wantErr: errBadRespSize},
+ },
+ },
+ {
+ name: "Request Processing Time",
+ field: "request_time",
+ cases: []subTest{
+ {line: logLine{web: web{reqProcTime: 0}}},
+ {line: logLine{web: web{reqProcTime: 100}}},
+ {line: logLine{web: web{reqProcTime: 1000.123}}},
+ {line: logLine{web: web{reqProcTime: -1}}, wantErr: errBadReqProcTime},
+ },
+ },
+ {
+ name: "Upstream Response Time",
+ field: "upstream_response_time",
+ cases: []subTest{
+ {line: logLine{web: web{upsRespTime: 0}}},
+ {line: logLine{web: web{upsRespTime: 100}}},
+ {line: logLine{web: web{upsRespTime: 1000.123}}},
+ {line: logLine{web: web{upsRespTime: -1}}, wantErr: errBadUpsRespTime},
+ },
+ },
+ {
+ name: "SSL Protocol",
+ field: "ssl_protocol",
+ cases: []subTest{
+ {line: logLine{web: web{sslProto: "SSLv3"}}},
+ {line: logLine{web: web{sslProto: "SSLv2"}}},
+ {line: logLine{web: web{sslProto: "TLSv1"}}},
+ {line: logLine{web: web{sslProto: "TLSv1.1"}}},
+ {line: logLine{web: web{sslProto: "TLSv1.2"}}},
+ {line: logLine{web: web{sslProto: "TLSv1.3"}}},
+ {line: logLine{web: web{sslProto: "invalid"}}, wantErr: errBadSSLProto},
+ },
+ },
+ {
+ name: "SSL Cipher Suite",
+ field: "ssl_cipher",
+ cases: []subTest{
+ {line: logLine{web: web{sslCipherSuite: "ECDHE-RSA-AES256-SHA"}}},
+ {line: logLine{web: web{sslCipherSuite: "DHE-RSA-AES256-SHA"}}},
+ {line: logLine{web: web{sslCipherSuite: "AES256-SHA"}}},
+ {line: logLine{web: web{sslCipherSuite: "TLS_AES_256_GCM_SHA384"}}},
+ {line: logLine{web: web{sslCipherSuite: "invalid"}}, wantErr: errBadSSLCipherSuite},
+ },
+ },
+ {
+ name: "Custom Fields",
+ field: "custom",
+ cases: []subTest{
+ {line: logLine{custom: custom{values: []customValue{{name: "custom", value: "POST"}}}}},
+ {line: logLine{custom: custom{values: []customValue{{name: "custom", value: "/example.com"}}}}},
+ {line: logLine{custom: custom{values: []customValue{{name: "custom", value: "0.333,0.444,0.555"}}}}},
+ },
+ },
+ {
+ name: "Empty Line",
+ cases: []subTest{
+ {line: emptyLogLine, wantErr: errEmptyLine},
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ for i, tc := range tt.cases {
+ name := fmt.Sprintf("[%s:%d]field='%s'", tt.name, i+1, tt.field)
+
+ t.Run(name, func(t *testing.T) {
+ line := prepareLogLine(tt.field, tc.line)
+
+ err := line.verify()
+
+ if tc.wantErr != nil {
+ require.Error(t, err)
+ assert.Truef(t, errors.Is(err, tc.wantErr), "expected '%v' error, got '%v'", tc.wantErr, err)
+ } else {
+ assert.NoError(t, err)
+ }
+ })
+ }
+ }
+}
+
+func prepareLogLine(field string, template logLine) logLine {
+ if template.empty() {
+ return *newEmptyLogLineWithFields()
+ }
+
+ line := newEmptyLogLineWithFields()
+ line.reset()
+
+ switch field {
+ case "host", "http_host", "v":
+ line.vhost = template.vhost
+ case "server_port", "p":
+ line.port = template.port
+ case "host:$server_port", "v:%p":
+ line.vhost = template.vhost
+ line.port = template.port
+ case "scheme":
+ line.reqScheme = template.reqScheme
+ case "remote_addr", "a", "h":
+ line.reqClient = template.reqClient
+ case "request", "r":
+ line.reqMethod = template.reqMethod
+ line.reqURL = template.reqURL
+ line.reqProto = template.reqProto
+ case "request_method", "m":
+ line.reqMethod = template.reqMethod
+ case "request_uri", "U":
+ line.reqURL = template.reqURL
+ case "server_protocol", "H":
+ line.reqProto = template.reqProto
+ case "status", "s", ">s":
+ line.respCode = template.respCode
+ case "request_length", "I":
+ line.reqSize = template.reqSize
+ case "bytes_sent", "body_bytes_sent", "b", "O", "B":
+ line.respSize = template.respSize
+ case "request_time", "D":
+ line.reqProcTime = template.reqProcTime
+ case "upstream_response_time":
+ line.upsRespTime = template.upsRespTime
+ case "ssl_protocol":
+ line.sslProto = template.sslProto
+ case "ssl_cipher":
+ line.sslCipherSuite = template.sslCipherSuite
+ default:
+ line.custom.values = template.custom.values
+ }
+ return *line
+}
+
+func newEmptyLogLineWithFields() *logLine {
+ l := newEmptyLogLine()
+ l.custom.fields = map[string]struct{}{"custom": {}}
+ return l
+}
diff --git a/src/go/collectors/go.d.plugin/modules/weblog/metadata.yaml b/src/go/collectors/go.d.plugin/modules/weblog/metadata.yaml
new file mode 100644
index 000000000..1cb4820a3
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/weblog/metadata.yaml
@@ -0,0 +1,533 @@
+plugin_name: go.d.plugin
+modules:
+ - meta:
+ id: collector-go.d.plugin-web_log
+ plugin_name: go.d.plugin
+ module_name: web_log
+ monitored_instance:
+ name: Web server log files
+ link: ""
+ categories:
+ - data-collection.web-servers-and-web-proxies
+ icon_filename: webservers.svg
+ keywords:
+ - webserver
+ - apache
+ - httpd
+ - nginx
+ - lighttpd
+ - logs
+ most_popular: false
+ info_provided_to_referring_integrations:
+ description: ""
+ related_resources:
+ integrations:
+ list: []
+ overview:
+ data_collection:
+ metrics_description: |
+ This collector monitors web servers by parsing their log files.
+ method_description: ""
+ default_behavior:
+ auto_detection:
+ description: |
+ It automatically detects log files of web servers running on localhost.
+ limits:
+ description: ""
+ performance_impact:
+ description: ""
+ additional_permissions:
+ description: ""
+ multi_instance: true
+ supported_platforms:
+ include: []
+ exclude: []
+ setup:
+ prerequisites:
+ list: []
+ configuration:
+ file:
+ name: go.d/web_log.conf
+ options:
+ description: |
+ Weblog is aware of how to parse and interpret the following fields (**known fields**):
+
+ > [nginx](https://nginx.org/en/docs/varindex.html)
+ >
+ > [apache](https://httpd.apache.org/docs/current/mod/mod_log_config.html)
+
+ | nginx | apache | description |
+ |-------------------------|----------|------------------------------------------------------------------------------------------|
+ | $host ($http_host) | %v | Name of the server which accepted a request. |
+ | $server_port | %p | Port of the server which accepted a request. |
+ | $scheme | - | Request scheme. "http" or "https". |
+ | $remote_addr | %a (%h) | Client address. |
+ | $request | %r | Full original request line. The line is "$request_method $request_uri $server_protocol". |
+ | $request_method | %m | Request method. Usually "GET" or "POST". |
+ | $request_uri | %U | Full original request URI. |
+ | $server_protocol | %H | Request protocol. Usually "HTTP/1.0", "HTTP/1.1", or "HTTP/2.0". |
+ | $status | %s (%>s) | Response status code. |
+ | $request_length | %I | Bytes received from a client, including request and headers. |
+ | $bytes_sent | %O | Bytes sent to a client, including request and headers. |
+ | $body_bytes_sent | %B (%b) | Bytes sent to a client, not counting the response header. |
+ | $request_time | %D | Request processing time. |
+ | $upstream_response_time | - | Time spent on receiving the response from the upstream server. |
+ | $ssl_protocol | - | Protocol of an established SSL connection. |
+ | $ssl_cipher | - | String of ciphers used for an established SSL connection. |
+
+ Notes:
+
+ - Apache `%h` logs the IP address if [HostnameLookups](https://httpd.apache.org/docs/2.4/mod/core.html#hostnamelookups) is Off. The web log collector counts hostnames as IPv4 addresses. We recommend either to disable HostnameLookups or use `%a` instead of `%h`.
+ - Since httpd 2.0, unlike 1.3, the `%b` and `%B` format strings do not represent the number of bytes sent to the client, but simply the size in bytes of the HTTP response. It will differ, for instance, if the connection is aborted, or if SSL is used. The `%O` format provided by [`mod_logio`](https://httpd.apache.org/docs/2.4/mod/mod_logio.html) will log the actual number of bytes sent over the network.
+ - To get `%I` and `%O` working you need to enable `mod_logio` on Apache.
+ - NGINX logs URI with query parameters, Apache doesnt.
+ - `$request` is parsed into `$request_method`, `$request_uri` and `$server_protocol`. If you have `$request` in your log format, there is no sense to have others.
+ - Don't use both `$bytes_sent` and `$body_bytes_sent` (`%O` and `%B` or `%b`). The module does not distinguish between these parameters.
+ folding:
+ title: Config options
+ enabled: true
+ list:
+ - name: update_every
+ description: Data collection frequency.
+ default_value: 1
+ required: false
+ - name: autodetection_retry
+ description: Recheck interval in seconds. Zero means no recheck will be scheduled.
+ default_value: 0
+ required: false
+ - name: path
+ description: Path to the web server log file.
+ default_value: ""
+ required: true
+ - name: exclude_path
+ description: Path to exclude.
+ default_value: "*.gz"
+ required: false
+ - name: url_patterns
+ description: List of URL patterns.
+ default_value: "[]"
+ required: false
+ detailed_description: |
+ "URL pattern" scope metrics will be collected for each URL pattern.
+
+ Option syntax:
+
+ ```yaml
+ url_patterns:
+ - name: name1
+ pattern: pattern1
+ - name: name2
+ pattern: pattern2
+ ```
+ - name: url_patterns.name
+ description: Used as a dimension name.
+ default_value: ""
+ required: true
+ - name: url_patterns.pattern
+ description: Used to match against full original request URI. Pattern syntax in [matcher](https://github.com/netdata/netdata/tree/master/src/go/collectors/go.d.plugin/pkg/matcher#supported-format).
+ default_value: ""
+ required: true
+ - name: parser
+ description: Log parser configuration.
+ default_value: ""
+ required: false
+ - name: parser.log_type
+ description: Log parser type.
+ default_value: auto
+ required: false
+ detailed_description: |
+ Weblog supports 5 different log parsers:
+
+ | Parser type | Description |
+ |-------------|-------------------------------------------|
+ | auto | Use CSV and auto-detect format |
+ | csv | A comma-separated values |
+ | json | [JSON](https://www.json.org/json-en.html) |
+ | ltsv | [LTSV](http://ltsv.org/) |
+ | regexp | Regular expression with named groups |
+
+ Syntax:
+
+ ```yaml
+ parser:
+ log_type: auto
+ ```
+
+ If `log_type` parameter set to `auto` (which is default), weblog will try to auto-detect appropriate log parser and log format using the last line of the log file.
+
+ - checks if format is `CSV` (using regexp).
+ - checks if format is `JSON` (using regexp).
+ - assumes format is `CSV` and tries to find appropriate `CSV` log format using predefined list of formats. It tries to parse the line using each of them in the following order (the first one matches is used later):
+
+ ```sh
+ $host:$server_port $remote_addr - - [$time_local] "$request" $status $body_bytes_sent - - $request_length $request_time $upstream_response_time
+ $host:$server_port $remote_addr - - [$time_local] "$request" $status $body_bytes_sent - - $request_length $request_time
+ $host:$server_port $remote_addr - - [$time_local] "$request" $status $body_bytes_sent $request_length $request_time $upstream_response_time
+ $host:$server_port $remote_addr - - [$time_local] "$request" $status $body_bytes_sent $request_length $request_time
+ $host:$server_port $remote_addr - - [$time_local] "$request" $status $body_bytes_sent
+ $remote_addr - - [$time_local] "$request" $status $body_bytes_sent - - $request_length $request_time $upstream_response_time
+ $remote_addr - - [$time_local] "$request" $status $body_bytes_sent - - $request_length $request_time
+ $remote_addr - - [$time_local] "$request" $status $body_bytes_sent $request_length $request_time $upstream_response_time
+ $remote_addr - - [$time_local] "$request" $status $body_bytes_sent $request_length $request_time
+ $remote_addr - - [$time_local] "$request" $status $body_bytes_sent
+ ```
+
+ If you're using the default Apache/NGINX log format, auto-detect will work for you. If it doesn't work you need to set the format manually.
+ - name: parser.csv_config
+ description: CSV log parser config.
+ default_value: ""
+ required: false
+ - name: parser.csv_config.delimiter
+ description: CSV field delimiter.
+ default_value: ","
+ required: false
+ - name: parser.csv_config.format
+ description: CSV log format.
+ default_value: ""
+ required: false
+ detailed_description: ""
+ - name: parser.ltsv_config
+ description: LTSV log parser config.
+ default_value: ""
+ required: false
+ - name: parser.ltsv_config.field_delimiter
+ description: LTSV field delimiter.
+ default_value: "\\t"
+ required: false
+ - name: parser.ltsv_config.value_delimiter
+ description: LTSV value delimiter.
+ default_value: ":"
+ required: false
+ - name: parser.ltsv_config.mapping
+ description: LTSV fields mapping to **known fields**.
+ default_value: ""
+ required: true
+ detailed_description: |
+ The mapping is a dictionary where the key is a field, as in logs, and the value is the corresponding **known field**.
+
+ > **Note**: don't use `$` and `%` prefixes for mapped field names.
+
+ ```yaml
+ parser:
+ log_type: ltsv
+ ltsv_config:
+ mapping:
+ label1: field1
+ label2: field2
+ ```
+ - name: parser.json_config
+ description: JSON log parser config.
+ default_value: ""
+ required: false
+ - name: parser.json_config.mapping
+ description: JSON fields mapping to **known fields**.
+ default_value: ""
+ required: true
+ detailed_description: |
+ The mapping is a dictionary where the key is a field, as in logs, and the value is the corresponding **known field**.
+
+ > **Note**: don't use `$` and `%` prefixes for mapped field names.
+
+ ```yaml
+ parser:
+ log_type: json
+ json_config:
+ mapping:
+ label1: field1
+ label2: field2
+ ```
+ - name: parser.regexp_config
+ description: RegExp log parser config.
+ default_value: ""
+ required: false
+ - name: parser.regexp_config.pattern
+ description: RegExp pattern with named groups.
+ default_value: ""
+ required: true
+ detailed_description: |
+ Use pattern with subexpressions names. These names should be **known fields**.
+
+ > **Note**: don't use `$` and `%` prefixes for mapped field names.
+
+ Syntax:
+
+ ```yaml
+ parser:
+ log_type: regexp
+ regexp_config:
+ pattern: PATTERN
+ ```
+ examples:
+ folding:
+ title: Config
+ enabled: true
+ list: []
+ troubleshooting:
+ problems:
+ list: []
+ alerts:
+ - name: web_log_1m_unmatched
+ metric: web_log.excluded_requests
+ info: percentage of unparsed log lines over the last minute
+ link: https://github.com/netdata/netdata/blob/master/src/health/health.d/web_log.conf
+ - name: web_log_1m_requests
+ metric: web_log.type_requests
+ info: "ratio of successful HTTP requests over the last minute (1xx, 2xx, 304, 401)"
+ link: https://github.com/netdata/netdata/blob/master/src/health/health.d/web_log.conf
+ - name: web_log_1m_redirects
+ metric: web_log.type_requests
+ info: "ratio of redirection HTTP requests over the last minute (3xx except 304)"
+ link: https://github.com/netdata/netdata/blob/master/src/health/health.d/web_log.conf
+ - name: web_log_1m_bad_requests
+ metric: web_log.type_requests
+ info: "ratio of client error HTTP requests over the last minute (4xx except 401)"
+ link: https://github.com/netdata/netdata/blob/master/src/health/health.d/web_log.conf
+ - name: web_log_1m_internal_errors
+ metric: web_log.type_requests
+ info: "ratio of server error HTTP requests over the last minute (5xx)"
+ link: https://github.com/netdata/netdata/blob/master/src/health/health.d/web_log.conf
+ - name: web_log_web_slow
+ metric: web_log.request_processing_time
+ info: average HTTP response time over the last 1 minute
+ link: https://github.com/netdata/netdata/blob/master/src/health/health.d/web_log.conf
+ - name: web_log_5m_requests_ratio
+ metric: web_log.type_requests
+ info: ratio of successful HTTP requests over over the last 5 minutes, compared with the previous 5 minutes
+ link: https://github.com/netdata/netdata/blob/master/src/health/health.d/web_log.conf
+ metrics:
+ folding:
+ title: Metrics
+ enabled: false
+ description: ""
+ availability: []
+ scopes:
+ - name: global
+ description: These metrics refer to the entire monitored application.
+ labels: []
+ metrics:
+ - name: web_log.requests
+ description: Total Requests
+ unit: requests/s
+ chart_type: line
+ dimensions:
+ - name: requests
+ - name: web_log.excluded_requests
+ description: Excluded Requests
+ unit: requests/s
+ chart_type: stacked
+ dimensions:
+ - name: unmatched
+ - name: web_log.type_requests
+ description: Requests By Type
+ unit: requests/s
+ chart_type: stacked
+ dimensions:
+ - name: success
+ - name: bad
+ - name: redirect
+ - name: error
+ - name: web_log.status_code_class_responses
+ description: Responses By Status Code Class
+ unit: responses/s
+ chart_type: stacked
+ dimensions:
+ - name: 1xx
+ - name: 2xx
+ - name: 3xx
+ - name: 4xx
+ - name: 5xx
+ - name: web_log.status_code_class_1xx_responses
+ description: Informational Responses By Status Code
+ unit: responses/s
+ chart_type: stacked
+ dimensions:
+ - name: a dimension per 1xx code
+ - name: web_log.status_code_class_2xx_responses
+ description: Successful Responses By Status Code
+ unit: responses/s
+ chart_type: stacked
+ dimensions:
+ - name: a dimension per 2xx code
+ - name: web_log.status_code_class_3xx_responses
+ description: Redirects Responses By Status Code
+ unit: responses/s
+ chart_type: stacked
+ dimensions:
+ - name: a dimension per 3xx code
+ - name: web_log.status_code_class_4xx_responses
+ description: Client Errors Responses By Status Code
+ unit: responses/s
+ chart_type: stacked
+ dimensions:
+ - name: a dimension per 4xx code
+ - name: web_log.status_code_class_5xx_responses
+ description: Server Errors Responses By Status Code
+ unit: responses/s
+ chart_type: stacked
+ dimensions:
+ - name: a dimension per 5xx code
+ - name: web_log.bandwidth
+ description: Bandwidth
+ unit: kilobits/s
+ chart_type: area
+ dimensions:
+ - name: received
+ - name: sent
+ - name: web_log.request_processing_time
+ description: Request Processing Time
+ unit: milliseconds
+ chart_type: line
+ dimensions:
+ - name: min
+ - name: max
+ - name: avg
+ - name: web_log.requests_processing_time_histogram
+ description: Requests Processing Time Histogram
+ unit: requests/s
+ chart_type: line
+ dimensions:
+ - name: a dimension per bucket
+ - name: web_log.upstream_response_time
+ description: Upstream Response Time
+ unit: milliseconds
+ chart_type: line
+ dimensions:
+ - name: min
+ - name: max
+ - name: avg
+ - name: web_log.upstream_responses_time_histogram
+ description: Upstream Responses Time Histogram
+ unit: requests/s
+ chart_type: line
+ dimensions:
+ - name: a dimension per bucket
+ - name: web_log.current_poll_uniq_clients
+ description: Current Poll Unique Clients
+ unit: clients
+ chart_type: stacked
+ dimensions:
+ - name: ipv4
+ - name: ipv6
+ - name: web_log.vhost_requests
+ description: Requests By Vhost
+ unit: requests/s
+ chart_type: stacked
+ dimensions:
+ - name: a dimension per vhost
+ - name: web_log.port_requests
+ description: Requests By Port
+ unit: requests/s
+ chart_type: stacked
+ dimensions:
+ - name: a dimension per port
+ - name: web_log.scheme_requests
+ description: Requests By Scheme
+ unit: requests/s
+ chart_type: stacked
+ dimensions:
+ - name: http
+ - name: https
+ - name: web_log.http_method_requests
+ description: Requests By HTTP Method
+ unit: requests/s
+ chart_type: stacked
+ dimensions:
+ - name: a dimension per HTTP method
+ - name: web_log.http_version_requests
+ description: Requests By HTTP Version
+ unit: requests/s
+ chart_type: stacked
+ dimensions:
+ - name: a dimension per HTTP version
+ - name: web_log.ip_proto_requests
+ description: Requests By IP Protocol
+ unit: requests/s
+ chart_type: stacked
+ dimensions:
+ - name: ipv4
+ - name: ipv6
+ - name: web_log.ssl_proto_requests
+ description: Requests By SSL Connection Protocol
+ unit: requests/s
+ chart_type: stacked
+ dimensions:
+ - name: a dimension per SSL protocol
+ - name: web_log.ssl_cipher_suite_requests
+ description: Requests By SSL Connection Cipher Suite
+ unit: requests/s
+ chart_type: stacked
+ dimensions:
+ - name: a dimension per SSL cipher suite
+ - name: web_log.url_pattern_requests
+ description: URL Field Requests By Pattern
+ unit: requests/s
+ chart_type: stacked
+ dimensions:
+ - name: a dimension per URL pattern
+ - name: web_log.custom_field_pattern_requests
+ description: Custom Field Requests By Pattern
+ unit: requests/s
+ chart_type: stacked
+ dimensions:
+ - name: a dimension per custom field pattern
+ - name: custom time field
+ description: TBD
+ labels: []
+ metrics:
+ - name: web_log.custom_time_field_summary
+ description: Custom Time Field Summary
+ unit: milliseconds
+ chart_type: line
+ dimensions:
+ - name: min
+ - name: max
+ - name: avg
+ - name: web_log.custom_time_field_histogram
+ description: Custom Time Field Histogram
+ unit: observations
+ chart_type: line
+ dimensions:
+ - name: a dimension per bucket
+ - name: custom numeric field
+ description: TBD
+ labels: []
+ metrics:
+ - name: web_log.custom_numeric_field_{{field_name}}_summary
+ description: Custom Numeric Field Summary
+ unit: '{{units}}'
+ chart_type: line
+ dimensions:
+ - name: min
+ - name: max
+ - name: avg
+ - name: URL pattern
+ description: TBD
+ labels: []
+ metrics:
+ - name: web_log.url_pattern_status_code_responses
+ description: Responses By Status Code
+ unit: responses/s
+ chart_type: line
+ dimensions:
+ - name: a dimension per pattern
+ - name: web_log.url_pattern_http_method_requests
+ description: Requests By HTTP Method
+ unit: requests/s
+ chart_type: line
+ dimensions:
+ - name: a dimension per HTTP method
+ - name: web_log.url_pattern_bandwidth
+ description: Bandwidth
+ unit: kilobits/s
+ chart_type: area
+ dimensions:
+ - name: received
+ - name: sent
+ - name: web_log.url_pattern_request_processing_time
+ description: Request Processing Time
+ unit: milliseconds
+ chart_type: line
+ dimensions:
+ - name: min
+ - name: max
+ - name: avg
diff --git a/src/go/collectors/go.d.plugin/modules/weblog/metrics.go b/src/go/collectors/go.d.plugin/modules/weblog/metrics.go
new file mode 100644
index 000000000..651221a99
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/weblog/metrics.go
@@ -0,0 +1,188 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package weblog
+
+import (
+ "github.com/netdata/netdata/go/go.d.plugin/pkg/metrics"
+)
+
+func newWebLogSummary() metrics.Summary {
+ return &weblogSummary{metrics.NewSummary()}
+}
+
+type weblogSummary struct {
+ metrics.Summary
+}
+
+// WriteTo redefines metrics.Summary.WriteTo
+// TODO: temporary workaround?
+func (s weblogSummary) WriteTo(rv map[string]int64, key string, mul, div int) {
+ s.Summary.WriteTo(rv, key, mul, div)
+ if _, ok := rv[key+"_min"]; !ok {
+ rv[key+"_min"] = 0
+ rv[key+"_max"] = 0
+ rv[key+"_avg"] = 0
+ }
+}
+
+type (
+ metricsData struct {
+ Requests metrics.Counter `stm:"requests"`
+ ReqUnmatched metrics.Counter `stm:"req_unmatched"`
+
+ RespCode metrics.CounterVec `stm:"resp_code"`
+ Resp1xx metrics.Counter `stm:"resp_1xx"`
+ Resp2xx metrics.Counter `stm:"resp_2xx"`
+ Resp3xx metrics.Counter `stm:"resp_3xx"`
+ Resp4xx metrics.Counter `stm:"resp_4xx"`
+ Resp5xx metrics.Counter `stm:"resp_5xx"`
+
+ ReqSuccess metrics.Counter `stm:"req_type_success"`
+ ReqRedirect metrics.Counter `stm:"req_type_redirect"`
+ ReqBad metrics.Counter `stm:"req_type_bad"`
+ ReqError metrics.Counter `stm:"req_type_error"`
+
+ UniqueIPv4 metrics.UniqueCounter `stm:"uniq_ipv4"`
+ UniqueIPv6 metrics.UniqueCounter `stm:"uniq_ipv6"`
+ BytesSent metrics.Counter `stm:"bytes_sent"`
+ BytesReceived metrics.Counter `stm:"bytes_received"`
+ ReqProcTime metrics.Summary `stm:"req_proc_time"`
+ ReqProcTimeHist metrics.Histogram `stm:"req_proc_time_hist"`
+ UpsRespTime metrics.Summary `stm:"upstream_resp_time"`
+ UpsRespTimeHist metrics.Histogram `stm:"upstream_resp_time_hist"`
+
+ ReqVhost metrics.CounterVec `stm:"req_vhost"`
+ ReqPort metrics.CounterVec `stm:"req_port"`
+ ReqMethod metrics.CounterVec `stm:"req_method"`
+ ReqURLPattern metrics.CounterVec `stm:"req_url_ptn"`
+ ReqVersion metrics.CounterVec `stm:"req_version"`
+ ReqSSLProto metrics.CounterVec `stm:"req_ssl_proto"`
+ ReqSSLCipherSuite metrics.CounterVec `stm:"req_ssl_cipher_suite"`
+ ReqHTTPScheme metrics.Counter `stm:"req_http_scheme"`
+ ReqHTTPSScheme metrics.Counter `stm:"req_https_scheme"`
+ ReqIPv4 metrics.Counter `stm:"req_ipv4"`
+ ReqIPv6 metrics.Counter `stm:"req_ipv6"`
+
+ ReqCustomField map[string]metrics.CounterVec `stm:"custom_field"`
+ URLPatternStats map[string]*patternMetrics `stm:"url_ptn"`
+
+ ReqCustomTimeField map[string]*customTimeFieldMetrics `stm:"custom_time_field"`
+ ReqCustomNumericField map[string]*customNumericFieldMetrics `stm:"custom_numeric_field"`
+ }
+ customTimeFieldMetrics struct {
+ Time metrics.Summary `stm:"time"`
+ TimeHist metrics.Histogram `stm:"time_hist"`
+ }
+ customNumericFieldMetrics struct {
+ Summary metrics.Summary `stm:"summary"`
+
+ multiplier int
+ divisor int
+ }
+ patternMetrics struct {
+ RespCode metrics.CounterVec `stm:"resp_code"`
+ ReqMethod metrics.CounterVec `stm:"req_method"`
+ BytesSent metrics.Counter `stm:"bytes_sent"`
+ BytesReceived metrics.Counter `stm:"bytes_received"`
+ ReqProcTime metrics.Summary `stm:"req_proc_time"`
+ }
+)
+
+func newMetricsData(config Config) *metricsData {
+ return &metricsData{
+ ReqVhost: metrics.NewCounterVec(),
+ ReqPort: metrics.NewCounterVec(),
+ ReqMethod: metrics.NewCounterVec(),
+ ReqVersion: metrics.NewCounterVec(),
+ RespCode: metrics.NewCounterVec(),
+ ReqSSLProto: metrics.NewCounterVec(),
+ ReqSSLCipherSuite: metrics.NewCounterVec(),
+ ReqProcTime: newWebLogSummary(),
+ ReqProcTimeHist: metrics.NewHistogram(convHistOptionsToMicroseconds(config.Histogram)),
+ UpsRespTime: newWebLogSummary(),
+ UpsRespTimeHist: metrics.NewHistogram(convHistOptionsToMicroseconds(config.Histogram)),
+ UniqueIPv4: metrics.NewUniqueCounter(true),
+ UniqueIPv6: metrics.NewUniqueCounter(true),
+ ReqURLPattern: newCounterVecFromPatterns(config.URLPatterns),
+ ReqCustomField: newReqCustomField(config.CustomFields),
+ URLPatternStats: newURLPatternStats(config.URLPatterns),
+ ReqCustomTimeField: newReqCustomTimeField(config.CustomTimeFields),
+ ReqCustomNumericField: newReqCustomNumericField(config.CustomNumericFields),
+ }
+}
+
+func (m *metricsData) reset() {
+ m.UniqueIPv4.Reset()
+ m.UniqueIPv6.Reset()
+ m.ReqProcTime.Reset()
+ m.UpsRespTime.Reset()
+ for _, v := range m.URLPatternStats {
+ v.ReqProcTime.Reset()
+ }
+ for _, v := range m.ReqCustomTimeField {
+ v.Time.Reset()
+ }
+ for _, v := range m.ReqCustomNumericField {
+ v.Summary.Reset()
+ }
+}
+
+func newCounterVecFromPatterns(patterns []userPattern) metrics.CounterVec {
+ c := metrics.NewCounterVec()
+ for _, p := range patterns {
+ _, _ = c.GetP(p.Name)
+ }
+ return c
+}
+
+func newURLPatternStats(patterns []userPattern) map[string]*patternMetrics {
+ stats := make(map[string]*patternMetrics)
+ for _, p := range patterns {
+ stats[p.Name] = &patternMetrics{
+ RespCode: metrics.NewCounterVec(),
+ ReqMethod: metrics.NewCounterVec(),
+ ReqProcTime: newWebLogSummary(),
+ }
+ }
+ return stats
+}
+
+func newReqCustomField(fields []customField) map[string]metrics.CounterVec {
+ cf := make(map[string]metrics.CounterVec)
+ for _, f := range fields {
+ cf[f.Name] = newCounterVecFromPatterns(f.Patterns)
+ }
+ return cf
+}
+
+func newReqCustomTimeField(fields []customTimeField) map[string]*customTimeFieldMetrics {
+ cf := make(map[string]*customTimeFieldMetrics)
+ for _, f := range fields {
+ cf[f.Name] = &customTimeFieldMetrics{
+ Time: newWebLogSummary(),
+ TimeHist: metrics.NewHistogram(convHistOptionsToMicroseconds(f.Histogram)),
+ }
+ }
+ return cf
+}
+
+func newReqCustomNumericField(fields []customNumericField) map[string]*customNumericFieldMetrics {
+ rv := make(map[string]*customNumericFieldMetrics)
+ for _, f := range fields {
+ rv[f.Name] = &customNumericFieldMetrics{
+ Summary: newWebLogSummary(),
+ multiplier: f.Multiplier,
+ divisor: f.Divisor,
+ }
+ }
+ return rv
+}
+
+// convert histogram options to microseconds (second => us)
+func convHistOptionsToMicroseconds(histogram []float64) []float64 {
+ var buckets []float64
+ for _, value := range histogram {
+ buckets = append(buckets, value*1e6)
+ }
+ return buckets
+}
diff --git a/src/go/collectors/go.d.plugin/modules/weblog/parser.go b/src/go/collectors/go.d.plugin/modules/weblog/parser.go
new file mode 100644
index 000000000..b152e4129
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/weblog/parser.go
@@ -0,0 +1,167 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package weblog
+
+import (
+ "errors"
+ "fmt"
+ "regexp"
+ "strings"
+
+ "github.com/netdata/netdata/go/go.d.plugin/pkg/logs"
+)
+
+/*
+Default apache log format:
+ - "%v:%p %h %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\"" vhost_combined
+ - "%h %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\"" combined
+ - "%h %l %u %t \"%r\" %>s %O" common
+ - "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-agent}i\" %I %O" Combined I/O (https://httpd.apache.org/docs/2.4/mod/mod_logio.html)
+
+Default nginx log format:
+ - '$remote_addr - $remote_user [$time_local] '
+ '"$request" $status $body_bytes_sent '
+ '"$http_referer" "$http_user_agent"' combined
+
+Netdata recommends:
+ Nginx:
+ - '$remote_addr - $remote_user [$time_local] '
+ '"$request" $status $body_bytes_sent '
+ '$request_length $request_time $upstream_response_time '
+ '"$http_referer" "$http_user_agent"'
+
+ Apache:
+ - "%h %l %u %t \"%r\" %>s %B %I %D \"%{Referer}i\" \"%{User-Agent}i\""
+*/
+
+var (
+ csvCommon = ` $remote_addr - - [$time_local] "$request" $status $body_bytes_sent`
+ csvCustom1 = ` $remote_addr - - [$time_local] "$request" $status $body_bytes_sent $request_length $request_time`
+ csvCustom2 = ` $remote_addr - - [$time_local] "$request" $status $body_bytes_sent $request_length $request_time $upstream_response_time`
+ csvCustom3 = ` $remote_addr - - [$time_local] "$request" $status $body_bytes_sent - - $request_length $request_time`
+ csvCustom4 = ` $remote_addr - - [$time_local] "$request" $status $body_bytes_sent - - $request_length $request_time $upstream_response_time`
+ csvVhostCommon = `$host:$server_port $remote_addr - - [$time_local] "$request" $status $body_bytes_sent`
+ csvVhostCustom1 = `$host:$server_port $remote_addr - - [$time_local] "$request" $status $body_bytes_sent $request_length $request_time`
+ csvVhostCustom2 = `$host:$server_port $remote_addr - - [$time_local] "$request" $status $body_bytes_sent $request_length $request_time $upstream_response_time`
+ csvVhostCustom3 = `$host:$server_port $remote_addr - - [$time_local] "$request" $status $body_bytes_sent - - $request_length $request_time`
+ csvVhostCustom4 = `$host:$server_port $remote_addr - - [$time_local] "$request" $status $body_bytes_sent - - $request_length $request_time $upstream_response_time`
+
+ guessOrder = []string{
+ csvVhostCustom4,
+ csvVhostCustom3,
+ csvVhostCustom2,
+ csvVhostCustom1,
+ csvVhostCommon,
+ csvCustom4,
+ csvCustom3,
+ csvCustom2,
+ csvCustom1,
+ csvCommon,
+ }
+)
+
+func cleanCSVFormat(format string) string { return strings.Join(strings.Fields(format), " ") }
+func cleanApacheLogFormat(format string) string { return strings.ReplaceAll(format, `\`, "") }
+
+const (
+ typeAuto = "auto"
+)
+
+var (
+ reLTSV = regexp.MustCompile(`^[a-zA-Z0-9]+:[^\t]*(\t[a-zA-Z0-9]+:[^\t]*)*$`)
+ reJSON = regexp.MustCompile(`^[[:space:]]*{.*}[[:space:]]*$`)
+)
+
+func (w *WebLog) newParser(record []byte) (logs.Parser, error) {
+ if w.ParserConfig.LogType == typeAuto {
+ w.Debugf("log_type is %s, will try format auto-detection", typeAuto)
+ if len(record) == 0 {
+ return nil, fmt.Errorf("empty line, can't auto-detect format (%s)", w.file.CurrentFilename())
+ }
+ return w.guessParser(record)
+ }
+
+ w.ParserConfig.CSV.Format = cleanApacheLogFormat(w.ParserConfig.CSV.Format)
+ w.Debugf("log_type is %s, skipping auto-detection", w.ParserConfig.LogType)
+ switch w.ParserConfig.LogType {
+ case logs.TypeCSV:
+ w.Debugf("config: %+v", w.ParserConfig.CSV)
+ case logs.TypeLTSV:
+ w.Debugf("config: %+v", w.ParserConfig.LogType)
+ case logs.TypeRegExp:
+ w.Debugf("config: %+v", w.ParserConfig.RegExp)
+ case logs.TypeJSON:
+ w.Debugf("config: %+v", w.ParserConfig.JSON)
+ }
+ return logs.NewParser(w.ParserConfig, w.file)
+}
+
+func (w *WebLog) guessParser(record []byte) (logs.Parser, error) {
+ w.Debug("starting log type auto-detection")
+ if reLTSV.Match(record) {
+ w.Debug("log type is LTSV")
+ return logs.NewLTSVParser(w.ParserConfig.LTSV, w.file)
+ }
+ if reJSON.Match(record) {
+ w.Debug("log type is JSON")
+ return logs.NewJSONParser(w.ParserConfig.JSON, w.file)
+ }
+ w.Debug("log type is CSV")
+ return w.guessCSVParser(record)
+}
+
+func (w *WebLog) guessCSVParser(record []byte) (logs.Parser, error) {
+ w.Debug("starting csv log format auto-detection")
+ w.Debugf("config: %+v", w.ParserConfig.CSV)
+ for _, format := range guessOrder {
+ format = cleanCSVFormat(format)
+ cfg := w.ParserConfig.CSV
+ cfg.Format = format
+
+ w.Debugf("trying format: '%s'", format)
+ parser, err := logs.NewCSVParser(cfg, w.file)
+ if err != nil {
+ return nil, err
+ }
+
+ line := newEmptyLogLine()
+ if err := parser.Parse(record, line); err != nil {
+ w.Debug("parse: ", err)
+ continue
+ }
+
+ if err = line.verify(); err != nil {
+ w.Debug("verify: ", err)
+ continue
+ }
+ return parser, nil
+ }
+ return nil, errors.New("cannot auto-detect log format, use custom log format")
+}
+
+func checkCSVFormatField(field string) (newName string, offset int, valid bool) {
+ if isTimeField(field) {
+ return "", 1, false
+ }
+ if !isFieldValid(field) {
+ return "", 0, false
+ }
+ // remove `$` and `%` to have same field names with regexp parser,
+ // these symbols aren't allowed in sub exp names
+ return field[1:], 0, true
+}
+
+func isTimeField(field string) bool {
+ return field == "[$time_local]" || field == "$time_local" || field == "%t"
+}
+
+func isFieldValid(field string) bool {
+ return len(field) > 1 && (isNginxField(field) || isApacheField(field))
+}
+func isNginxField(field string) bool {
+ return strings.HasPrefix(field, "$")
+}
+
+func isApacheField(field string) bool {
+ return strings.HasPrefix(field, "%")
+}
diff --git a/src/go/collectors/go.d.plugin/modules/weblog/parser_test.go b/src/go/collectors/go.d.plugin/modules/weblog/parser_test.go
new file mode 100644
index 000000000..501df22ae
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/weblog/parser_test.go
@@ -0,0 +1,224 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package weblog
+
+import (
+ "fmt"
+ "testing"
+
+ "github.com/netdata/netdata/go/go.d.plugin/pkg/logs"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestWebLog_guessParser(t *testing.T) {
+ type test = struct {
+ name string
+ inputs []string
+ wantParserType string
+ wantErr bool
+ }
+ tests := []test{
+ {
+ name: "guessed csv",
+ wantParserType: logs.TypeCSV,
+ inputs: []string{
+ `test.example.com:80 88.191.254.20 - - [22/Mar/2009:09:30:31 +0100] "GET / HTTP/1.0" 200 8674 "-" "-" 8674 0.123 0.123,0.321`,
+ `test.example.com:80 88.191.254.20 - - [22/Mar/2009:09:30:31 +0100] "GET / HTTP/1.0" 200 8674 "-" "-" 8674 0.123`,
+ `test.example.com:80 88.191.254.20 - - [22/Mar/2009:09:30:31 +0100] "GET / HTTP/1.0" 200 8674 8674 0.123 0.123,0.321`,
+ `test.example.com:80 88.191.254.20 - - [22/Mar/2009:09:30:31 +0100] "GET / HTTP/1.0" 200 8674 8674 0.123`,
+ `test.example.com:80 88.191.254.20 - - [22/Mar/2009:09:30:31 +0100] "GET / HTTP/1.0" 200 8674`,
+ `88.191.254.20 - - [22/Mar/2009:09:30:31 +0100] "GET / HTTP/1.0" 200 8674 "-" "-" 8674 0.123 0.123,0.321`,
+ `88.191.254.20 - - [22/Mar/2009:09:30:31 +0100] "GET / HTTP/1.0" 200 8674 "-" "-" 8674 0.123`,
+ `88.191.254.20 - - [22/Mar/2009:09:30:31 +0100] "GET / HTTP/1.0" 200 8674 8674 0.123 0.123,0.321`,
+ `88.191.254.20 - - [22/Mar/2009:09:30:31 +0100] "GET / HTTP/1.0" 200 8674 8674 0.123`,
+ `88.191.254.20 - - [22/Mar/2009:09:30:31 +0100] "GET / HTTP/1.0" 200 8674`,
+ },
+ },
+ {
+ name: "guessed ltsv",
+ wantParserType: logs.TypeLTSV,
+ inputs: []string{
+ `field1:test.example.com:80 field2:88.191.254.20 field3:"GET / HTTP/1.0" 200 8674 field4:8674 field5:0.123`,
+ },
+ },
+ {
+ name: "guessed json",
+ wantParserType: logs.TypeJSON,
+ inputs: []string{
+ `{}`,
+ ` {}`,
+ ` {} `,
+ `{"host": "example.com"}`,
+ `{"host": "example.com","time": "2020-08-04T20:23:27+03:00", "upstream_response_time": "0.776", "remote_addr": "1.2.3.4"}`,
+ ` {"host": "example.com","time": "2020-08-04T20:23:27+03:00", "upstream_response_time": "0.776", "remote_addr": "1.2.3.4"} `,
+ },
+ },
+ {
+ name: "unknown",
+ wantErr: true,
+ inputs: []string{
+ `test.example.com 80 88.191.254.20 - - [22/Mar/2009:09:30:31 +0100] "GET / HTTP/1.0" 200 8674`,
+ `test.example.com 88.191.254.20 - - [22/Mar/2009:09:30:31 +0100] "GET / HTTP/1.0" 200 8674`,
+ },
+ },
+ }
+
+ weblog := prepareWebLog()
+
+ for _, tc := range tests {
+ for i, input := range tc.inputs {
+ name := fmt.Sprintf("name=%s,input_num=%d", tc.name, i+1)
+
+ t.Run(name, func(t *testing.T) {
+ p, err := weblog.newParser([]byte(input))
+
+ if tc.wantErr {
+ assert.Error(t, err)
+ } else {
+ require.NoError(t, err)
+ switch tc.wantParserType {
+ default:
+ t.Errorf("unknown parser type: %s", tc.wantParserType)
+ case logs.TypeLTSV:
+ assert.IsType(t, (*logs.LTSVParser)(nil), p)
+ case logs.TypeCSV:
+ require.IsType(t, (*logs.CSVParser)(nil), p)
+ case logs.TypeJSON:
+ require.IsType(t, (*logs.JSONParser)(nil), p)
+ }
+ }
+ })
+ }
+ }
+}
+
+func TestWebLog_guessCSVParser(t *testing.T) {
+ type test = struct {
+ name string
+ inputs []string
+ wantCSVFormat string
+ wantErr bool
+ }
+ tests := []test{
+ {
+ name: "guessed vhost custom4",
+ wantCSVFormat: csvVhostCustom4,
+ inputs: []string{
+ `test.example.com:80 88.191.254.20 - - [22/Mar/2009:09:30:31 +0100] "GET / HTTP/1.0" 200 8674 "-" "-" 8674 0.123 0.123,0.321`,
+ },
+ },
+ {
+ name: "guessed vhost custom3",
+ wantCSVFormat: csvVhostCustom3,
+ inputs: []string{
+ `test.example.com:80 88.191.254.20 - - [22/Mar/2009:09:30:31 +0100] "GET / HTTP/1.0" 200 8674 "-" "-" 8674 0.123`,
+ },
+ },
+ {
+ name: "guessed vhost custom2",
+ wantCSVFormat: csvVhostCustom2,
+ inputs: []string{
+ `test.example.com:80 88.191.254.20 - - [22/Mar/2009:09:30:31 +0100] "GET / HTTP/1.0" 200 8674 8674 0.123 0.123,0.321`,
+ },
+ },
+ {
+ name: "guessed vhost custom1",
+ wantCSVFormat: csvVhostCustom1,
+ inputs: []string{
+ `test.example.com:80 88.191.254.20 - - [22/Mar/2009:09:30:31 +0100] "GET / HTTP/1.0" 200 8674 8674 0.123`,
+ },
+ },
+ {
+ name: "guessed vhost common",
+ wantCSVFormat: csvVhostCommon,
+ inputs: []string{
+ `test.example.com:80 88.191.254.20 - - [22/Mar/2009:09:30:31 +0100] "GET / HTTP/1.0" 200 8674`,
+ },
+ },
+ {
+ name: "guessed custom4",
+ wantCSVFormat: csvCustom4,
+ inputs: []string{
+ `88.191.254.20 - - [22/Mar/2009:09:30:31 +0100] "GET / HTTP/1.0" 200 8674 "-" "-" 8674 0.123 0.123,0.321`,
+ },
+ },
+ {
+ name: "guessed custom3",
+ wantCSVFormat: csvCustom3,
+ inputs: []string{
+ `88.191.254.20 - - [22/Mar/2009:09:30:31 +0100] "GET / HTTP/1.0" 200 8674 "-" "-" 8674 0.123`,
+ },
+ },
+ {
+ name: "guessed custom2",
+ wantCSVFormat: csvCustom2,
+ inputs: []string{
+ `88.191.254.20 - - [22/Mar/2009:09:30:31 +0100] "GET / HTTP/1.0" 200 8674 8674 0.123 0.123,0.321`,
+ },
+ },
+ {
+ name: "guessed custom1",
+ wantCSVFormat: csvCustom1,
+ inputs: []string{
+ `88.191.254.20 - - [22/Mar/2009:09:30:31 +0100] "GET / HTTP/1.0" 200 8674 8674 0.123`,
+ },
+ },
+ {
+ name: "guessed common",
+ wantCSVFormat: csvCommon,
+ inputs: []string{
+ `88.191.254.20 - - [22/Mar/2009:09:30:31 +0100] "GET / HTTP/1.0" 200 8674`,
+ },
+ },
+ {
+ name: "unknown",
+ wantErr: true,
+ inputs: []string{
+ `test.example.com 80 88.191.254.20 - - [22/Mar/2009:09:30:31 +0100] "GET / HTTP/1.0" 200 8674`,
+ `test.example.com 88.191.254.20 - - [22/Mar/2009:09:30:31 +0100] "GET / HTTP/1.0" 200 8674`,
+ },
+ },
+ }
+
+ weblog := prepareWebLog()
+
+ for i, tc := range tests {
+ for _, input := range tc.inputs {
+ name := fmt.Sprintf("name=%s,input_num=%d", tc.name, i+1)
+
+ t.Run(name, func(t *testing.T) {
+ p, err := weblog.guessCSVParser([]byte(input))
+
+ if tc.wantErr {
+ assert.Error(t, err)
+ } else {
+ require.NoError(t, err)
+ assert.Equal(t, cleanCSVFormat(tc.wantCSVFormat), p.(*logs.CSVParser).Config.Format)
+ }
+ })
+ }
+ }
+}
+
+func prepareWebLog() *WebLog {
+ cfg := logs.ParserConfig{
+ LogType: typeAuto,
+ CSV: logs.CSVConfig{
+ Delimiter: " ",
+ CheckField: checkCSVFormatField,
+ },
+ LTSV: logs.LTSVConfig{
+ FieldDelimiter: "\t",
+ ValueDelimiter: ":",
+ },
+ }
+
+ return &WebLog{
+ Config: Config{
+ GroupRespCodes: false,
+ ParserConfig: cfg,
+ },
+ }
+}
diff --git a/src/go/collectors/go.d.plugin/modules/weblog/testdata/common.log b/src/go/collectors/go.d.plugin/modules/weblog/testdata/common.log
new file mode 100644
index 000000000..6860d13e8
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/weblog/testdata/common.log
@@ -0,0 +1,500 @@
+localhost - - [22/Mar/2009:09:30:31 +0100] "POST /example.net HTTP/2" 100 3441
+2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.org HTTP/2.0" 100 4065
+Unmatched! The rat the cat the dog chased killed ate the malt!
+localhost - - [22/Mar/2009:09:30:31 +0100] "POST /example.net HTTP/1.1" 300 3258
+203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.net HTTP/1.1" 201 3189
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.com HTTP/2" 100 3852
+203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.org HTTP/2" 101 4710
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.other HTTP/2.0" 400 4091
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.org HTTP/2.0" 400 4142
+Unmatched! The rat the cat the dog chased killed ate the malt!
+Unmatched! The rat the cat the dog chased killed ate the malt!
+localhost - - [22/Mar/2009:09:30:31 +0100] "POST /example.other HTTP/2" 201 4480
+203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.com HTTP/2" 100 2554
+localhost - - [22/Mar/2009:09:30:31 +0100] "POST /example.com HTTP/2.0" 300 2698
+2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.other HTTP/2" 300 2048
+203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.other HTTP/2.0" 101 4678
+2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.other HTTP/2" 301 1077
+2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.other HTTP/2.0" 300 4949
+203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.net HTTP/2" 300 4170
+2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.org HTTP/2" 400 3962
+localhost - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.org HTTP/2.0" 201 2109
+203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.org HTTP/2" 400 4028
+203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.other HTTP/2.0" 301 2446
+Unmatched! The rat the cat the dog chased killed ate the malt!
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.org HTTP/1.1" 100 1748
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.org HTTP/1.1" 301 4185
+203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.net HTTP/2.0" 200 2775
+203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.net HTTP/2" 401 4280
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.com HTTP/1.1" 401 1592
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.com HTTP/2" 401 2005
+203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.org HTTP/1.1" 101 1867
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.other HTTP/2" 401 4866
+2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.net HTTP/1.1" 201 4371
+2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.other HTTP/1.1" 400 1395
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.org HTTP/2.0" 300 3549
+localhost - - [22/Mar/2009:09:30:31 +0100] "POST /example.org HTTP/1.1" 101 2857
+203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.net HTTP/2.0" 300 3548
+localhost - - [22/Mar/2009:09:30:31 +0100] "GET /example.net HTTP/2.0" 301 4773
+2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.com HTTP/2" 301 4825
+Unmatched! The rat the cat the dog chased killed ate the malt!
+localhost - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.com HTTP/1.1" 400 1039
+203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.com HTTP/2" 101 3619
+203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.com HTTP/2.0" 100 3919
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.org HTTP/2.0" 101 3136
+2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.com HTTP/2" 400 2415
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.org HTTP/1.1" 200 4448
+localhost - - [22/Mar/2009:09:30:31 +0100] "GET /example.com HTTP/1.1" 101 2639
+2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.org HTTP/2.0" 200 3251
+203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.other HTTP/2.0" 400 4026
+203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.net HTTP/2" 100 4450
+localhost - - [22/Mar/2009:09:30:31 +0100] "GET /example.other HTTP/1.1" 100 2267
+Unmatched! The rat the cat the dog chased killed ate the malt!
+2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.org HTTP/2.0" 101 4747
+Unmatched! The rat the cat the dog chased killed ate the malt!
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.com HTTP/1.1" 100 4046
+localhost - - [22/Mar/2009:09:30:31 +0100] "GET /example.other HTTP/2.0" 400 4818
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.net HTTP/2.0" 101 3944
+localhost - - [22/Mar/2009:09:30:31 +0100] "POST /example.net HTTP/2" 100 4152
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.other HTTP/2.0" 101 3407
+2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.org HTTP/2.0" 300 4683
+localhost - - [22/Mar/2009:09:30:31 +0100] "POST /example.net HTTP/2.0" 400 1284
+2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.org HTTP/1.1" 401 1221
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.other HTTP/1.1" 400 2922
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.org HTTP/2.0" 101 4388
+2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.other HTTP/2.0" 401 1636
+Unmatched! The rat the cat the dog chased killed ate the malt!
+2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.com HTTP/2" 100 3518
+203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.com HTTP/2.0" 300 2637
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.net HTTP/2.0" 100 3566
+2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.com HTTP/2.0" 100 3088
+2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.net HTTP/2.0" 301 3379
+2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.net HTTP/1.1" 400 3304
+localhost - - [22/Mar/2009:09:30:31 +0100] "GET /example.com HTTP/2" 201 2772
+203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.net HTTP/2.0" 200 4284
+localhost - - [22/Mar/2009:09:30:31 +0100] "POST /example.com HTTP/2.0" 401 4486
+2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.net HTTP/2" 201 2768
+Unmatched! The rat the cat the dog chased killed ate the malt!
+Unmatched! The rat the cat the dog chased killed ate the malt!
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.other HTTP/1.1" 300 3414
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.net HTTP/2.0" 401 3377
+2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.other HTTP/1.1" 400 3646
+localhost - - [22/Mar/2009:09:30:31 +0100] "GET /example.org HTTP/2.0" 201 1290
+203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.com HTTP/2" 300 2500
+localhost - - [22/Mar/2009:09:30:31 +0100] "POST /example.org HTTP/1.1" 300 4473
+203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.other HTTP/2" 101 1985
+2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.net HTTP/1.1" 400 2607
+2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.net HTTP/2.0" 400 1468
+localhost - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.other HTTP/2" 100 1584
+2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.net HTTP/1.1" 400 4366
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.net HTTP/1.1" 201 3121
+2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.org HTTP/1.1" 201 4888
+2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.org HTTP/2.0" 100 1723
+2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.other HTTP/1.1" 300 3593
+localhost - - [22/Mar/2009:09:30:31 +0100] "POST /example.net HTTP/1.1" 301 3139
+203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.net HTTP/2" 301 1915
+2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.other HTTP/1.1" 400 1381
+203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.org HTTP/2.0" 300 3801
+2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.net HTTP/1.1" 301 4757
+2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.net HTTP/1.1" 400 2553
+localhost - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.net HTTP/2.0" 200 1241
+localhost - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.org HTTP/1.1" 100 3723
+2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.other HTTP/2.0" 400 2236
+localhost - - [22/Mar/2009:09:30:31 +0100] "POST /example.net HTTP/2.0" 100 3375
+localhost - - [22/Mar/2009:09:30:31 +0100] "GET /example.net HTTP/2.0" 100 2941
+Unmatched! The rat the cat the dog chased killed ate the malt!
+localhost - - [22/Mar/2009:09:30:31 +0100] "POST /example.other HTTP/2.0" 201 3199
+Unmatched! The rat the cat the dog chased killed ate the malt!
+localhost - - [22/Mar/2009:09:30:31 +0100] "GET /example.org HTTP/1.1" 300 3117
+2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.other HTTP/2.0" 400 4041
+203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.org HTTP/2.0" 100 1962
+203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.com HTTP/2.0" 100 4868
+localhost - - [22/Mar/2009:09:30:31 +0100] "GET /example.com HTTP/2" 101 2810
+2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.other HTTP/2.0" 300 2858
+localhost - - [22/Mar/2009:09:30:31 +0100] "POST /example.com HTTP/1.1" 301 1398
+2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.com HTTP/1.1" 200 4304
+2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.net HTTP/2" 100 3121
+203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.org HTTP/2" 100 3621
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.other HTTP/2" 201 1922
+Unmatched! The rat the cat the dog chased killed ate the malt!
+localhost - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.org HTTP/2" 101 1857
+2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.org HTTP/2.0" 101 4671
+203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.other HTTP/1.1" 301 4404
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.org HTTP/2" 400 1552
+203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.net HTTP/1.1" 300 1506
+localhost - - [22/Mar/2009:09:30:31 +0100] "POST /example.org HTTP/2" 401 4942
+2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.net HTTP/1.1" 301 1569
+2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.com HTTP/1.1" 101 4946
+2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.org HTTP/2" 101 2884
+203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.org HTTP/2" 301 1487
+2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.org HTTP/2.0" 100 1488
+localhost - - [22/Mar/2009:09:30:31 +0100] "GET /example.org HTTP/2" 300 2931
+203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.net HTTP/2" 100 4186
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.other HTTP/2.0" 100 2110
+2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.other HTTP/2.0" 200 1802
+2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.com HTTP/1.1" 201 3690
+203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.org HTTP/2.0" 300 4811
+Unmatched! The rat the cat the dog chased killed ate the malt!
+2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.org HTTP/1.1" 300 2055
+2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.net HTTP/2.0" 300 3964
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.org HTTP/2.0" 201 4282
+203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.org HTTP/2" 400 4813
+2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.org HTTP/2.0" 401 1438
+localhost - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.other HTTP/2" 100 2254
+localhost - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.net HTTP/2.0" 200 4812
+Unmatched! The rat the cat the dog chased killed ate the malt!
+2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.com HTTP/1.1" 401 1735
+2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.com HTTP/2" 301 1363
+2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.net HTTP/2" 101 3294
+localhost - - [22/Mar/2009:09:30:31 +0100] "POST /example.com HTTP/1.1" 401 4179
+2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.other HTTP/2" 401 1844
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.org HTTP/2" 200 3677
+localhost - - [22/Mar/2009:09:30:31 +0100] "POST /example.org HTTP/2.0" 201 2056
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.net HTTP/2.0" 200 4041
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.org HTTP/2" 101 3850
+2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.org HTTP/1.1" 301 1990
+Unmatched! The rat the cat the dog chased killed ate the malt!
+localhost - - [22/Mar/2009:09:30:31 +0100] "POST /example.other HTTP/2" 200 1729
+2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.org HTTP/2.0" 301 4426
+2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.com HTTP/1.1" 101 1615
+Unmatched! The rat the cat the dog chased killed ate the malt!
+localhost - - [22/Mar/2009:09:30:31 +0100] "POST /example.other HTTP/2" 200 2683
+localhost - - [22/Mar/2009:09:30:31 +0100] "POST /example.org HTTP/2.0" 301 3379
+203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.other HTTP/2" 300 3702
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.other HTTP/2.0" 301 2462
+2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.net HTTP/2.0" 100 4250
+2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.org HTTP/2" 301 1470
+2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.com HTTP/2.0" 200 4572
+203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.other HTTP/2.0" 300 4562
+203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.net HTTP/1.1" 100 4339
+Unmatched! The rat the cat the dog chased killed ate the malt!
+203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.com HTTP/2.0" 301 1565
+2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.net HTTP/2" 100 3779
+Unmatched! The rat the cat the dog chased killed ate the malt!
+localhost - - [22/Mar/2009:09:30:31 +0100] "GET /example.org HTTP/1.1" 100 1372
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.other HTTP/1.1" 201 2457
+2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.com HTTP/2" 201 1455
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.net HTTP/1.1" 100 3573
+203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.other HTTP/2" 400 2048
+2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.org HTTP/2" 300 1723
+203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.net HTTP/1.1" 301 3720
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.net HTTP/1.1" 400 4014
+localhost - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.org HTTP/2" 100 3846
+Unmatched! The rat the cat the dog chased killed ate the malt!
+2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.org HTTP/2.0" 400 1773
+2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.com HTTP/1.1" 201 2261
+203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.other HTTP/1.1" 300 1630
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.other HTTP/2.0" 300 3378
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.org HTTP/2.0" 301 1974
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.com HTTP/1.1" 101 3055
+203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.org HTTP/2" 301 1350
+2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.com HTTP/1.1" 300 2210
+Unmatched! The rat the cat the dog chased killed ate the malt!
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.net HTTP/2.0" 100 2339
+localhost - - [22/Mar/2009:09:30:31 +0100] "GET /example.other HTTP/1.1" 400 2380
+localhost - - [22/Mar/2009:09:30:31 +0100] "GET /example.other HTTP/1.1" 201 3880
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.org HTTP/2.0" 100 1334
+2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.other HTTP/2.0" 300 3683
+localhost - - [22/Mar/2009:09:30:31 +0100] "POST /example.other HTTP/1.1" 200 4519
+2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.com HTTP/1.1" 300 1549
+localhost - - [22/Mar/2009:09:30:31 +0100] "GET /example.org HTTP/2.0" 301 1371
+2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.com HTTP/1.1" 401 1601
+203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.other HTTP/2.0" 301 3826
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.org HTTP/1.1" 101 2260
+2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.net HTTP/2" 200 2497
+Unmatched! The rat the cat the dog chased killed ate the malt!
+Unmatched! The rat the cat the dog chased killed ate the malt!
+2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.net HTTP/2.0" 100 3076
+localhost - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.org HTTP/2.0" 200 3126
+localhost - - [22/Mar/2009:09:30:31 +0100] "POST /example.net HTTP/1.1" 100 2180
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.org HTTP/2.0" 400 3291
+2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.org HTTP/2.0" 100 1268
+203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.com HTTP/1.1" 400 1836
+localhost - - [22/Mar/2009:09:30:31 +0100] "GET /example.net HTTP/2" 101 2953
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.other HTTP/2" 201 4018
+2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.org HTTP/1.1" 301 3686
+2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.other HTTP/1.1" 401 3320
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.com HTTP/2.0" 300 1473
+Unmatched! The rat the cat the dog chased killed ate the malt!
+2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.net HTTP/2.0" 101 3257
+localhost - - [22/Mar/2009:09:30:31 +0100] "GET /example.other HTTP/1.1" 300 3530
+localhost - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.com HTTP/2.0" 201 3109
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.com HTTP/2" 400 4815
+2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.org HTTP/1.1" 100 4414
+203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.com HTTP/2" 401 4290
+2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.net HTTP/2" 100 2060
+2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.net HTTP/2" 100 4651
+Unmatched! The rat the cat the dog chased killed ate the malt!
+Unmatched! The rat the cat the dog chased killed ate the malt!
+2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.org HTTP/1.1" 200 1378
+Unmatched! The rat the cat the dog chased killed ate the malt!
+203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.com HTTP/1.1" 401 2666
+localhost - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.com HTTP/1.1" 400 3376
+localhost - - [22/Mar/2009:09:30:31 +0100] "GET /example.other HTTP/1.1" 200 4009
+Unmatched! The rat the cat the dog chased killed ate the malt!
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.other HTTP/2.0" 400 2307
+203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.net HTTP/2" 100 2928
+localhost - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.com HTTP/2.0" 400 4048
+Unmatched! The rat the cat the dog chased killed ate the malt!
+203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.other HTTP/2.0" 400 3902
+2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.com HTTP/2" 201 1512
+localhost - - [22/Mar/2009:09:30:31 +0100] "POST /example.net HTTP/2" 100 4776
+2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.other HTTP/1.1" 201 4791
+Unmatched! The rat the cat the dog chased killed ate the malt!
+203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.net HTTP/2.0" 201 3219
+203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.com HTTP/2" 401 3020
+203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.net HTTP/1.1" 101 4867
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.other HTTP/2" 401 1276
+2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.org HTTP/1.1" 201 3313
+localhost - - [22/Mar/2009:09:30:31 +0100] "GET /example.other HTTP/1.1" 200 1350
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.com HTTP/2.0" 101 3561
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.net HTTP/2" 201 4382
+2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.net HTTP/2.0" 401 4487
+2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.net HTTP/2.0" 401 4595
+localhost - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.org HTTP/2.0" 301 1727
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.com HTTP/2" 301 4103
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.other HTTP/2" 100 1454
+localhost - - [22/Mar/2009:09:30:31 +0100] "GET /example.net HTTP/1.1" 301 4990
+localhost - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.com HTTP/2.0" 300 3753
+2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.com HTTP/1.1" 401 3445
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.net HTTP/2.0" 101 1295
+2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.net HTTP/1.1" 301 3430
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.org HTTP/1.1" 201 3746
+2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.other HTTP/1.1" 400 3578
+Unmatched! The rat the cat the dog chased killed ate the malt!
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.org HTTP/2.0" 401 1389
+localhost - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.net HTTP/1.1" 200 1889
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.org HTTP/1.1" 200 3680
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.net HTTP/2.0" 300 4623
+203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.other HTTP/2" 300 1016
+203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.net HTTP/1.1" 300 4078
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.com HTTP/1.1" 100 1023
+2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.com HTTP/1.1" 400 4407
+2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.com HTTP/2" 100 4704
+203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.org HTTP/1.1" 401 3575
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.com HTTP/2.0" 300 1013
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.org HTTP/1.1" 400 4512
+2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.net HTTP/2" 100 2563
+203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.other HTTP/1.1" 101 2379
+localhost - - [22/Mar/2009:09:30:31 +0100] "POST /example.net HTTP/2.0" 200 3616
+2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.net HTTP/2" 401 2782
+2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.net HTTP/2" 201 3324
+localhost - - [22/Mar/2009:09:30:31 +0100] "GET /example.other HTTP/2" 300 3157
+203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.org HTTP/2.0" 301 1299
+Unmatched! The rat the cat the dog chased killed ate the malt!
+2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.com HTTP/1.1" 201 3768
+203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.other HTTP/2" 201 1550
+2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.net HTTP/2.0" 200 4683
+2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.com HTTP/2" 401 4689
+localhost - - [22/Mar/2009:09:30:31 +0100] "GET /example.org HTTP/1.1" 300 1400
+2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.other HTTP/2.0" 300 1234
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.org HTTP/2" 101 4018
+localhost - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.com HTTP/1.1" 201 1981
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.com HTTP/2" 201 4646
+2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.com HTTP/2" 201 4767
+2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.com HTTP/2.0" 101 4446
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.com HTTP/1.1" 101 1829
+203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.com HTTP/2" 401 3967
+2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.other HTTP/1.1" 300 4347
+2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.com HTTP/1.1" 400 1753
+2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.other HTTP/2.0" 201 3592
+Unmatched! The rat the cat the dog chased killed ate the malt!
+203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.com HTTP/2" 401 3249
+2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.org HTTP/2.0" 101 1917
+2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.org HTTP/2.0" 101 3295
+2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.net HTTP/2" 201 2958
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.org HTTP/2" 300 1445
+localhost - - [22/Mar/2009:09:30:31 +0100] "POST /example.net HTTP/2" 301 1025
+2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.net HTTP/2.0" 201 2088
+2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.other HTTP/2.0" 300 2029
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.org HTTP/2" 401 1157
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.com HTTP/1.1" 201 4675
+2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.other HTTP/2" 100 4606
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.org HTTP/2.0" 201 1227
+203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.other HTTP/1.1" 300 1869
+203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.org HTTP/1.1" 200 1614
+2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.com HTTP/2" 201 4878
+localhost - - [22/Mar/2009:09:30:31 +0100] "POST /example.other HTTP/1.1" 100 1813
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.net HTTP/1.1" 100 1643
+2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.com HTTP/2.0" 100 3488
+localhost - - [22/Mar/2009:09:30:31 +0100] "GET /example.org HTTP/1.1" 300 1844
+203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.other HTTP/1.1" 300 3527
+2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.org HTTP/2" 100 4655
+2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.org HTTP/1.1" 401 2628
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.com HTTP/2" 300 2380
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.org HTTP/2" 200 1059
+Unmatched! The rat the cat the dog chased killed ate the malt!
+203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.org HTTP/2" 400 4336
+203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.com HTTP/2.0" 100 3951
+2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.net HTTP/1.1" 200 4708
+localhost - - [22/Mar/2009:09:30:31 +0100] "GET /example.other HTTP/1.1" 300 3364
+2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.org HTTP/2.0" 101 2704
+2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.com HTTP/1.1" 201 4399
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.net HTTP/1.1" 100 4365
+Unmatched! The rat the cat the dog chased killed ate the malt!
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.org HTTP/2" 201 3905
+localhost - - [22/Mar/2009:09:30:31 +0100] "POST /example.other HTTP/1.1" 300 3544
+2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.org HTTP/2" 101 2718
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.com HTTP/1.1" 100 1165
+localhost - - [22/Mar/2009:09:30:31 +0100] "POST /example.com HTTP/2" 100 4053
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.other HTTP/2" 300 1351
+2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.org HTTP/2" 101 2537
+2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.other HTTP/2.0" 100 2934
+localhost - - [22/Mar/2009:09:30:31 +0100] "GET /example.net HTTP/2.0" 201 3186
+2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.other HTTP/2" 301 4225
+localhost - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.net HTTP/2.0" 200 3432
+Unmatched! The rat the cat the dog chased killed ate the malt!
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.net HTTP/2" 101 2079
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.org HTTP/2.0" 400 1823
+2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.net HTTP/1.1" 101 3692
+localhost - - [22/Mar/2009:09:30:31 +0100] "GET /example.other HTTP/2.0" 200 2169
+localhost - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.other HTTP/2.0" 300 4244
+2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.other HTTP/1.1" 200 2605
+2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.org HTTP/2" 300 2472
+203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.com HTTP/2.0" 301 1415
+2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.net HTTP/2" 101 3667
+2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.other HTTP/1.1" 301 3214
+2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.org HTTP/2" 201 1689
+203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.other HTTP/2.0" 201 2180
+2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.other HTTP/1.1" 300 1237
+localhost - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.other HTTP/1.1" 100 4821
+localhost - - [22/Mar/2009:09:30:31 +0100] "GET /example.other HTTP/1.1" 201 3739
+2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.com HTTP/2.0" 100 4644
+2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.other HTTP/2" 100 1926
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.com HTTP/2" 400 3835
+203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.org HTTP/1.1" 401 2216
+localhost - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.com HTTP/2" 101 4270
+localhost - - [22/Mar/2009:09:30:31 +0100] "GET /example.org HTTP/2.0" 300 4876
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.org HTTP/1.1" 101 2917
+2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.com HTTP/2" 201 1429
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.com HTTP/2" 400 3952
+203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.other HTTP/1.1" 100 1688
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.com HTTP/2" 201 2935
+Unmatched! The rat the cat the dog chased killed ate the malt!
+203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.other HTTP/2" 300 1968
+2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.other HTTP/2" 100 2139
+localhost - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.com HTTP/2.0" 400 2399
+2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.com HTTP/1.1" 201 3705
+203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.com HTTP/1.1" 100 1810
+203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.com HTTP/2.0" 300 2679
+2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.net HTTP/2.0" 301 3638
+2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.com HTTP/1.1" 200 1078
+Unmatched! The rat the cat the dog chased killed ate the malt!
+localhost - - [22/Mar/2009:09:30:31 +0100] "POST /example.org HTTP/2" 401 1648
+2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.net HTTP/2" 100 4064
+2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.com HTTP/2.0" 300 4981
+2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.com HTTP/2.0" 200 3685
+localhost - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.org HTTP/2.0" 201 1145
+2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.org HTTP/2.0" 300 1766
+2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.com HTTP/2" 401 4867
+localhost - - [22/Mar/2009:09:30:31 +0100] "POST /example.other HTTP/2.0" 101 2972
+localhost - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.org HTTP/2" 101 3389
+203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.com HTTP/2" 300 1911
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.net HTTP/1.1" 301 4083
+203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.net HTTP/2.0" 100 1841
+2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.other HTTP/1.1" 301 3929
+2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.com HTTP/2" 100 2529
+2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.other HTTP/1.1" 301 4904
+2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.net HTTP/2.0" 401 3593
+2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.net HTTP/2.0" 300 3434
+203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.other HTTP/1.1" 201 2610
+2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.net HTTP/2" 301 3577
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.com HTTP/2.0" 301 1099
+2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.other HTTP/2" 401 1355
+203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.net HTTP/1.1" 100 1913
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.net HTTP/1.1" 301 3582
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.com HTTP/2" 401 1974
+2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.net HTTP/1.1" 100 2248
+2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.other HTTP/2" 401 4714
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.com HTTP/1.1" 200 4414
+2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.other HTTP/1.1" 400 4661
+2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.other HTTP/1.1" 200 2206
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.com HTTP/2" 301 4863
+2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.com HTTP/1.1" 100 2792
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.net HTTP/1.1" 100 3458
+localhost - - [22/Mar/2009:09:30:31 +0100] "POST /example.com HTTP/1.1" 401 3559
+2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.com HTTP/2.0" 200 3430
+2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.com HTTP/1.1" 301 3977
+2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.com HTTP/1.1" 400 1199
+2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.com HTTP/2" 100 3822
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.net HTTP/2" 300 1481
+2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.org HTTP/2.0" 100 4760
+203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.org HTTP/2" 101 1228
+203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.net HTTP/2.0" 401 3825
+2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.com HTTP/1.1" 400 2678
+203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.other HTTP/1.1" 201 1750
+Unmatched! The rat the cat the dog chased killed ate the malt!
+203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.other HTTP/2.0" 100 2791
+localhost - - [22/Mar/2009:09:30:31 +0100] "POST /example.org HTTP/1.1" 100 2895
+Unmatched! The rat the cat the dog chased killed ate the malt!
+2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.com HTTP/2" 401 4285
+localhost - - [22/Mar/2009:09:30:31 +0100] "GET /example.org HTTP/1.1" 300 1756
+2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.other HTTP/1.1" 200 3869
+2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.other HTTP/1.1" 300 4503
+2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.other HTTP/2.0" 401 2535
+2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.com HTTP/2" 301 1316
+localhost - - [22/Mar/2009:09:30:31 +0100] "POST /example.com HTTP/2.0" 400 2593
+203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.org HTTP/2" 301 4991
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.net HTTP/1.1" 101 3336
+2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.com HTTP/1.1" 400 2385
+2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.com HTTP/2.0" 400 2640
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.com HTTP/1.1" 401 3748
+localhost - - [22/Mar/2009:09:30:31 +0100] "POST /example.other HTTP/1.1" 401 1633
+2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.other HTTP/2.0" 201 2563
+localhost - - [22/Mar/2009:09:30:31 +0100] "POST /example.org HTTP/1.1" 400 4912
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.other HTTP/2" 300 4293
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.other HTTP/2.0" 201 1866
+localhost - - [22/Mar/2009:09:30:31 +0100] "GET /example.com HTTP/2.0" 200 3271
+203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.org HTTP/2.0" 201 4323
+localhost - - [22/Mar/2009:09:30:31 +0100] "GET /example.com HTTP/2.0" 400 4882
+203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.net HTTP/2" 300 2762
+2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.other HTTP/1.1" 101 1540
+localhost - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.other HTTP/2" 400 3108
+Unmatched! The rat the cat the dog chased killed ate the malt!
+localhost - - [22/Mar/2009:09:30:31 +0100] "POST /example.other HTTP/2" 301 1775
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.com HTTP/1.1" 101 2246
+203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.com HTTP/2.0" 200 2510
+203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.com HTTP/2.0" 300 4898
+2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.com HTTP/1.1" 401 3470
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.net HTTP/2.0" 100 2392
+2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.other HTTP/2.0" 400 1805
+2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.net HTTP/2.0" 100 2343
+2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.org HTTP/2.0" 201 3486
+2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.org HTTP/1.1" 200 4805
+2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.other HTTP/2.0" 401 1072
+localhost - - [22/Mar/2009:09:30:31 +0100] "POST /example.other HTTP/2" 101 1301
+203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.other HTTP/1.1" 300 3148
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.net HTTP/2.0" 301 3699
+2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.com HTTP/1.1" 200 1926
+localhost - - [22/Mar/2009:09:30:31 +0100] "POST /example.com HTTP/2.0" 100 2011
+203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.org HTTP/1.1" 300 2200
+localhost - - [22/Mar/2009:09:30:31 +0100] "GET /example.net HTTP/2" 401 4598
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.net HTTP/2.0" 201 2969
+Unmatched! The rat the cat the dog chased killed ate the malt!
+2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.org HTTP/1.1" 100 3458
+203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.other HTTP/2" 400 3912
+localhost - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.net HTTP/2.0" 301 1370
+203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.org HTTP/1.1" 401 2694
+203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.other HTTP/2" 200 4528
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.net HTTP/2.0" 301 3490
+203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.com HTTP/2" 100 2722
+2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.net HTTP/2" 300 4815
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.org HTTP/2" 300 3511
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.com HTTP/2" 201 1496
+Unmatched! The rat the cat the dog chased killed ate the malt!
+2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.org HTTP/2" 100 4312
+2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.com HTTP/2" 100 3768
+2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.org HTTP/1.1" 101 3636
+2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.net HTTP/2.0" 401 3300
+2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.org HTTP/2" 301 3662
+localhost - - [22/Mar/2009:09:30:31 +0100] "POST /example.org HTTP/2.0" 400 3264
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.net HTTP/2.0" 201 3647
+203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.com HTTP/2.0" 300 1024
+Unmatched! The rat the cat the dog chased killed ate the malt!
+203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.net HTTP/2" 101 1470
+localhost - - [22/Mar/2009:09:30:31 +0100] "POST /example.other HTTP/2" 200 1720
+localhost - - [22/Mar/2009:09:30:31 +0100] "POST /example.com HTTP/2" 301 1130
+2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.org HTTP/2" 401 4736
+localhost - - [22/Mar/2009:09:30:31 +0100] "POST /example.net HTTP/2" 200 1955
+localhost - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.net HTTP/2" 401 4246
+localhost - - [22/Mar/2009:09:30:31 +0100] "GET /example.org HTTP/1.1" 200 3138 \ No newline at end of file
diff --git a/src/go/collectors/go.d.plugin/modules/weblog/testdata/config.json b/src/go/collectors/go.d.plugin/modules/weblog/testdata/config.json
new file mode 100644
index 000000000..80b51736d
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/weblog/testdata/config.json
@@ -0,0 +1,64 @@
+{
+ "update_every": 123,
+ "path": "ok",
+ "exclude_path": "ok",
+ "log_type": "ok",
+ "csv_config": {
+ "fields_per_record": 123,
+ "delimiter": "ok",
+ "trim_leading_space": true,
+ "format": "ok"
+ },
+ "ltsv_config": {
+ "field_delimiter": "ok",
+ "value_delimiter": "ok",
+ "mapping": {
+ "ok": "ok"
+ }
+ },
+ "regexp_config": {
+ "pattern": "ok"
+ },
+ "json_config": {
+ "mapping": {
+ "ok": "ok"
+ }
+ },
+ "url_patterns": [
+ {
+ "name": "ok",
+ "match": "ok"
+ }
+ ],
+ "custom_fields": [
+ {
+ "name": "ok",
+ "patterns": [
+ {
+ "name": "ok",
+ "match": "ok"
+ }
+ ]
+ }
+ ],
+ "custom_time_fields": [
+ {
+ "name": "ok",
+ "histogram": [
+ 123.123
+ ]
+ }
+ ],
+ "custom_numeric_fields": [
+ {
+ "name": "ok",
+ "units": "ok",
+ "multiplier": 123,
+ "divisor": 123
+ }
+ ],
+ "histogram": [
+ 123.123
+ ],
+ "group_response_codes": true
+}
diff --git a/src/go/collectors/go.d.plugin/modules/weblog/testdata/config.yaml b/src/go/collectors/go.d.plugin/modules/weblog/testdata/config.yaml
new file mode 100644
index 000000000..64f60763a
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/weblog/testdata/config.yaml
@@ -0,0 +1,39 @@
+update_every: 123
+path: "ok"
+exclude_path: "ok"
+log_type: "ok"
+csv_config:
+ fields_per_record: 123
+ delimiter: "ok"
+ trim_leading_space: yes
+ format: "ok"
+ltsv_config:
+ field_delimiter: "ok"
+ value_delimiter: "ok"
+ mapping:
+ ok: "ok"
+regexp_config:
+ pattern: "ok"
+json_config:
+ mapping:
+ ok: "ok"
+url_patterns:
+ - name: "ok"
+ match: "ok"
+custom_fields:
+ - name: "ok"
+ patterns:
+ - name: "ok"
+ match: "ok"
+custom_time_fields:
+ - name: "ok"
+ histogram:
+ - 123.123
+custom_numeric_fields:
+ - name: "ok"
+ units: "ok"
+ multiplier: 123
+ divisor: 123
+histogram:
+ - 123.123
+group_response_codes: yes
diff --git a/src/go/collectors/go.d.plugin/modules/weblog/testdata/custom.log b/src/go/collectors/go.d.plugin/modules/weblog/testdata/custom.log
new file mode 100644
index 000000000..f2ea80bdb
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/weblog/testdata/custom.log
@@ -0,0 +1,100 @@
+dark beer
+dark beer
+light wine
+light beer
+dark wine
+dark beer
+Unmatched! The rat the cat the dog chased killed ate the malt!
+light wine
+dark beer
+light wine
+light wine
+dark beer
+dark wine
+dark wine
+light wine
+light beer
+light wine
+light beer
+light beer
+light beer
+dark beer
+light wine
+dark beer
+light beer
+light wine
+dark wine
+dark wine
+light wine
+light beer
+light wine
+dark wine
+light wine
+light wine
+dark beer
+light wine
+Unmatched! The rat the cat the dog chased killed ate the malt!
+light beer
+dark beer
+dark beer
+light beer
+dark beer
+dark wine
+light beer
+light wine
+light beer
+light wine
+Unmatched! The rat the cat the dog chased killed ate the malt!
+dark wine
+dark beer
+light beer
+light wine
+dark beer
+light wine
+dark wine
+Unmatched! The rat the cat the dog chased killed ate the malt!
+light beer
+dark wine
+dark wine
+Unmatched! The rat the cat the dog chased killed ate the malt!
+dark beer
+light wine
+dark wine
+dark wine
+light beer
+dark wine
+dark beer
+light beer
+light wine
+dark beer
+dark beer
+dark beer
+dark beer
+light wine
+light beer
+dark beer
+Unmatched! The rat the cat the dog chased killed ate the malt!
+dark beer
+light beer
+dark wine
+dark beer
+dark beer
+dark beer
+light wine
+light beer
+light beer
+dark beer
+dark beer
+light beer
+Unmatched! The rat the cat the dog chased killed ate the malt!
+light wine
+dark beer
+light wine
+dark beer
+light wine
+light beer
+dark wine
+dark beer
+Unmatched! The rat the cat the dog chased killed ate the malt!
+dark beer
+light beer \ No newline at end of file
diff --git a/src/go/collectors/go.d.plugin/modules/weblog/testdata/custom_time_fields.log b/src/go/collectors/go.d.plugin/modules/weblog/testdata/custom_time_fields.log
new file mode 100644
index 000000000..9d01fb9bc
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/weblog/testdata/custom_time_fields.log
@@ -0,0 +1,72 @@
+121 321
+431 123
+121 321
+121 321
+121 321
+431 123
+121 321
+121 321
+431 123
+121 321
+121 321
+431 123
+121 321
+431 123
+121 321
+431 123
+121 321
+121 321
+121 321
+431 123
+121 321
+431 123
+121 321
+121 321
+431 123
+121 321
+121 321
+121 321
+431 123
+121 321
+121 321
+431 123
+121 321
+121 321
+431 123
+121 321
+431 123
+121 321
+431 123
+121 321
+121 321
+121 321
+431 123
+121 321
+431 123
+121 321
+121 321
+121 321
+431 123
+121 321
+121 321
+121 321
+431 123
+121 321
+121 321
+431 123
+121 321
+121 321
+431 123
+121 321
+431 123
+121 321
+431 123
+121 321
+121 321
+121 321
+431 123
+121 321
+431 123
+121 321
+121 321
+121 321
diff --git a/src/go/collectors/go.d.plugin/modules/weblog/testdata/full.log b/src/go/collectors/go.d.plugin/modules/weblog/testdata/full.log
new file mode 100644
index 000000000..460e62127
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/weblog/testdata/full.log
@@ -0,0 +1,500 @@
+198.51.100.1:82 203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.net HTTP/2" 301 4715 4113 174 465 https TLSv1.2 ECDHE-RSA-AES256-SHA dark beer 230
+Unmatched! The rat the cat the dog chased killed ate the malt!
+2001:db8:1ce::1:82 203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.net HTTP/1.1" 301 1130 1202 409 450 https TLSv1 DHE-RSA-AES256-SHA light beer 230
+198.51.100.1:83 203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.org HTTP/1.1" 201 4020 1217 492 135 https TLSv1.2 PSK-RC4-SHA light wine 230
+test.example.org:82 localhost - - [22/Mar/2009:09:30:31 +0100] "GET /example.other HTTP/2.0" 401 3784 2349 266 63 http TLSv1 ECDHE-RSA-AES256-SHA dark wine 230
+localhost:83 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.net HTTP/1.1" 201 2149 3834 178 197 https TLSv1.1 AES256-SHA dark wine 230
+198.51.100.1:80 2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.com HTTP/1.1" 200 1442 4125 23 197 https TLSv1.3 DHE-RSA-AES256-SHA light wine 230
+test.example.com:82 203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.net HTTP/2.0" 300 4134 3965 259 296 https TLSv1.3 PSK-RC4-SHA dark wine 230
+test.example.com:84 localhost - - [22/Mar/2009:09:30:31 +0100] "GET /example.com HTTP/1.1" 401 1224 3352 135 468 http SSLv2 PSK-RC4-SHA light wine 230
+localhost:82 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.org HTTP/2.0" 200 2504 4754 58 371 http TLSv1.1 DHE-RSA-AES256-SHA dark beer 230
+Unmatched! The rat the cat the dog chased killed ate the malt!
+Unmatched! The rat the cat the dog chased killed ate the malt!
+2001:db8:1ce::1:84 203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.other HTTP/1.1" 200 4898 2787 398 476 http SSLv2 DHE-RSA-AES256-SHA dark beer 230
+test.example.org:83 localhost - - [22/Mar/2009:09:30:31 +0100] "GET /example.other HTTP/2.0" 100 4957 1848 324 158 https TLSv1.2 AES256-SHA dark wine 230
+test.example.org:80 localhost - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.com HTTP/2" 301 1752 1717 75 317 https SSLv3 PSK-RC4-SHA dark wine 230
+Unmatched! The rat the cat the dog chased killed ate the malt!
+test.example.com:82 localhost - - [22/Mar/2009:09:30:31 +0100] "GET /example.com HTTP/2.0" 301 3799 4120 71 17 http TLSv1.3 ECDHE-RSA-AES256-SHA dark beer 230
+198.51.100.1:80 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.com HTTP/1.1" 101 1870 3945 392 323 http TLSv1.1 PSK-RC4-SHA light beer 230
+test.example.com:84 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.other HTTP/2.0" 200 1261 3535 52 271 https TLSv1.1 DHE-RSA-AES256-SHA dark wine 230
+test.example.com:83 2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.org HTTP/1.1" 101 3228 3545 476 168 http TLSv1.1 AES256-SHA light beer 230
+test.example.com:80 203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.other HTTP/2" 300 4731 1574 362 184 https SSLv2 ECDHE-RSA-AES256-SHA light wine 230
+198.51.100.1:80 203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.org HTTP/1.1" 300 4868 1803 23 388 https TLSv1.3 DHE-RSA-AES256-SHA dark beer 230
+Unmatched! The rat the cat the dog chased killed ate the malt!
+test.example.org:83 localhost - - [22/Mar/2009:09:30:31 +0100] "GET /example.other HTTP/1.1" 100 3744 3546 296 437 http SSLv2 DHE-RSA-AES256-SHA light beer 230
+test.example.org:80 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.com HTTP/2.0" 401 4858 1493 151 240 http SSLv2 AES256-SHA light wine 230
+Unmatched! The rat the cat the dog chased killed ate the malt!
+test.example.com:81 2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.other HTTP/2.0" 300 1367 4284 45 443 https TLSv1.1 AES256-SHA light beer 230
+localhost:81 2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.net HTTP/2" 100 4392 4982 143 110 http SSLv3 AES256-SHA light beer 230
+2001:db8:1ce::1:84 localhost - - [22/Mar/2009:09:30:31 +0100] "POST /example.other HTTP/1.1" 101 4606 3311 410 273 https TLSv1 PSK-RC4-SHA dark beer 230
+198.51.100.1:81 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.net HTTP/2.0" 100 1163 1526 10 186 https SSLv2 AES256-SHA light beer 230
+test.example.org:83 localhost - - [22/Mar/2009:09:30:31 +0100] "POST /example.other HTTP/2" 301 3262 3789 144 124 https TLSv1.3 DHE-RSA-AES256-SHA light wine 230
+198.51.100.1:84 203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.org HTTP/2.0" 400 1365 1447 325 186 http TLSv1.2 PSK-RC4-SHA dark beer 230
+Unmatched! The rat the cat the dog chased killed ate the malt!
+2001:db8:1ce::1:84 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.net HTTP/1.1" 301 4546 4409 295 153 http SSLv3 ECDHE-RSA-AES256-SHA light beer 230
+localhost:81 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.other HTTP/2.0" 300 2297 3318 139 227 https TLSv1 ECDHE-RSA-AES256-SHA dark wine 230
+localhost:81 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.net HTTP/1.1" 100 4671 4285 371 7 https SSLv3 ECDHE-RSA-AES256-SHA dark beer 230
+test.example.org:83 203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.other HTTP/2" 400 3651 1135 172 159 https TLSv1.1 DHE-RSA-AES256-SHA light beer 230
+localhost:82 2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.com HTTP/1.1" 101 3958 3959 350 121 https SSLv2 DHE-RSA-AES256-SHA dark beer 230
+localhost:84 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.org HTTP/2.0" 200 1652 3813 190 11 https SSLv3 AES256-SHA dark wine 230
+test.example.org:83 2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.other HTTP/2" 101 1228 2344 251 366 https TLSv1 ECDHE-RSA-AES256-SHA light beer 230
+test.example.org:80 203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.net HTTP/2.0" 200 1860 3118 187 419 https TLSv1 PSK-RC4-SHA light wine 230
+Unmatched! The rat the cat the dog chased killed ate the malt!
+localhost:82 localhost - - [22/Mar/2009:09:30:31 +0100] "POST /example.other HTTP/1.1" 401 4518 3837 18 219 http TLSv1.3 DHE-RSA-AES256-SHA dark beer 230
+localhost:81 2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.net HTTP/2" 201 2108 2472 257 470 http TLSv1.1 PSK-RC4-SHA dark beer 230
+2001:db8:1ce::1:82 localhost - - [22/Mar/2009:09:30:31 +0100] "GET /example.other HTTP/2" 101 2020 1076 262 106 https TLSv1.3 PSK-RC4-SHA light wine 230
+localhost:83 2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.other HTTP/1.1" 100 4815 3052 49 322 https TLSv1.3 DHE-RSA-AES256-SHA light beer 230
+2001:db8:1ce::1:82 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.net HTTP/2" 300 1642 4001 421 194 https TLSv1 PSK-RC4-SHA light wine 230
+Unmatched! The rat the cat the dog chased killed ate the malt!
+2001:db8:1ce::1:84 2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.other HTTP/2" 201 3805 2597 25 187 http TLSv1.1 AES256-SHA dark wine 230
+2001:db8:1ce::1:84 localhost - - [22/Mar/2009:09:30:31 +0100] "POST /example.org HTTP/2.0" 301 3435 1760 474 318 https TLSv1.2 ECDHE-RSA-AES256-SHA light wine 230
+localhost:84 2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.other HTTP/2.0" 101 1911 4082 356 301 https TLSv1 DHE-RSA-AES256-SHA light beer 230
+2001:db8:1ce::1:80 203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.com HTTP/2" 100 2536 1664 115 474 http SSLv3 PSK-RC4-SHA dark beer 230
+Unmatched! The rat the cat the dog chased killed ate the malt!
+test.example.com:82 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.com HTTP/1.1" 401 3757 3987 441 469 http SSLv2 ECDHE-RSA-AES256-SHA dark wine 230
+Unmatched! The rat the cat the dog chased killed ate the malt!
+2001:db8:1ce::1:83 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.com HTTP/1.1" 400 1221 4244 232 421 https TLSv1.1 ECDHE-RSA-AES256-SHA dark wine 230
+localhost:84 203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.com HTTP/1.1" 101 2001 2405 6 140 http TLSv1 DHE-RSA-AES256-SHA light wine 230
+Unmatched! The rat the cat the dog chased killed ate the malt!
+198.51.100.1:81 localhost - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.org HTTP/2.0" 400 4442 4396 64 49 https TLSv1.1 AES256-SHA light beer 230
+2001:db8:1ce::1:81 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.org HTTP/1.1" 401 1461 4623 46 47 https TLSv1.3 ECDHE-RSA-AES256-SHA light beer 230
+Unmatched! The rat the cat the dog chased killed ate the malt!
+test.example.com:84 2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.com HTTP/1.1" 101 4709 2156 249 137 https TLSv1.3 ECDHE-RSA-AES256-SHA dark beer 230
+2001:db8:1ce::1:84 localhost - - [22/Mar/2009:09:30:31 +0100] "GET /example.com HTTP/2" 201 2332 3311 172 266 https TLSv1.1 ECDHE-RSA-AES256-SHA dark beer 230
+2001:db8:1ce::1:80 localhost - - [22/Mar/2009:09:30:31 +0100] "GET /example.net HTTP/2.0" 301 3571 3672 188 389 https SSLv2 AES256-SHA light wine 230
+localhost:84 203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.other HTTP/1.1" 100 1739 3940 403 399 https SSLv3 DHE-RSA-AES256-SHA dark wine 230
+test.example.org:82 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.other HTTP/2.0" 300 2332 3788 473 372 http SSLv3 DHE-RSA-AES256-SHA dark wine 230
+test.example.org:81 203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.org HTTP/2.0" 201 4476 1339 420 120 https TLSv1.3 ECDHE-RSA-AES256-SHA light beer 230
+test.example.org:83 2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.org HTTP/1.1" 101 1040 4417 294 81 http SSLv2 PSK-RC4-SHA dark beer 230
+test.example.org:84 2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.org HTTP/1.1" 200 1908 1611 265 324 http TLSv1 ECDHE-RSA-AES256-SHA light wine 230
+test.example.org:83 localhost - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.net HTTP/2" 300 4725 3638 328 442 https SSLv3 DHE-RSA-AES256-SHA dark wine 230
+198.51.100.1:82 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.org HTTP/1.1" 100 3943 3001 163 391 http TLSv1.1 AES256-SHA light beer 230
+198.51.100.1:84 localhost - - [22/Mar/2009:09:30:31 +0100] "GET /example.org HTTP/2" 101 3635 4361 30 431 https SSLv2 DHE-RSA-AES256-SHA light beer 230
+test.example.com:81 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.net HTTP/2.0" 201 3348 2997 321 462 http TLSv1 PSK-RC4-SHA dark beer 230
+localhost:81 203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.net HTTP/2.0" 101 3213 3414 218 267 http TLSv1.3 PSK-RC4-SHA dark wine 230
+localhost:81 203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.net HTTP/1.1" 400 2845 2448 46 165 https TLSv1 AES256-SHA light beer 230
+test.example.org:82 2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.org HTTP/2.0" 301 2789 1791 227 314 http SSLv3 ECDHE-RSA-AES256-SHA dark wine 230
+2001:db8:1ce::1:81 2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.net HTTP/2" 301 2283 4644 304 402 http TLSv1.1 PSK-RC4-SHA light wine 230
+localhost:81 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.com HTTP/2.0" 201 4748 3274 80 481 http SSLv2 AES256-SHA light beer 230
+localhost:81 localhost - - [22/Mar/2009:09:30:31 +0100] "GET /example.net HTTP/1.1" 300 2327 1772 328 174 http TLSv1 ECDHE-RSA-AES256-SHA light beer 230
+test.example.org:81 localhost - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.net HTTP/1.1" 401 1180 3482 115 138 http SSLv2 DHE-RSA-AES256-SHA dark beer 230
+198.51.100.1:83 203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.other HTTP/2" 300 2758 1482 432 426 http TLSv1.1 PSK-RC4-SHA dark wine 230
+2001:db8:1ce::1:84 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.org HTTP/1.1" 200 4793 3549 258 490 https SSLv3 AES256-SHA light wine 230
+198.51.100.1:83 203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.com HTTP/2.0" 200 4211 3691 49 241 http TLSv1.2 PSK-RC4-SHA light wine 230
+Unmatched! The rat the cat the dog chased killed ate the malt!
+test.example.com:81 203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.other HTTP/2" 201 4853 1043 361 46 http SSLv3 ECDHE-RSA-AES256-SHA dark beer 230
+localhost:82 203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.other HTTP/1.1" 400 1025 3378 28 134 https TLSv1.2 DHE-RSA-AES256-SHA light wine 230
+198.51.100.1:82 203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.org HTTP/2" 400 2124 1528 147 144 http TLSv1.1 DHE-RSA-AES256-SHA dark wine 230
+test.example.com:80 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.other HTTP/2.0" 201 4910 1613 194 385 https TLSv1.1 ECDHE-RSA-AES256-SHA dark beer 230
+2001:db8:1ce::1:81 2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.org HTTP/2" 100 2792 3271 491 104 https SSLv3 DHE-RSA-AES256-SHA dark wine 230
+Unmatched! The rat the cat the dog chased killed ate the malt!
+test.example.com:83 203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.net HTTP/2" 300 4722 4182 344 237 https TLSv1 DHE-RSA-AES256-SHA dark wine 230
+2001:db8:1ce::1:82 2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.org HTTP/1.1" 201 3945 3511 153 388 https TLSv1 PSK-RC4-SHA light beer 230
+test.example.com:81 localhost - - [22/Mar/2009:09:30:31 +0100] "GET /example.com HTTP/2.0" 100 1456 4467 418 70 http TLSv1 ECDHE-RSA-AES256-SHA dark beer 230
+2001:db8:1ce::1:80 localhost - - [22/Mar/2009:09:30:31 +0100] "POST /example.org HTTP/2.0" 401 1307 1537 422 379 http TLSv1 PSK-RC4-SHA light wine 230
+2001:db8:1ce::1:83 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.org HTTP/2.0" 400 4768 2420 95 366 http TLSv1.3 DHE-RSA-AES256-SHA light beer 230
+localhost:80 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.other HTTP/2" 100 4274 4529 296 270 http SSLv3 PSK-RC4-SHA dark beer 230
+test.example.org:82 203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.org HTTP/1.1" 101 1181 3640 182 479 https TLSv1.3 AES256-SHA light wine 230
+198.51.100.1:82 203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.net HTTP/2.0" 400 2101 2029 377 210 http TLSv1.3 AES256-SHA dark wine 230
+test.example.com:83 localhost - - [22/Mar/2009:09:30:31 +0100] "GET /example.com HTTP/2.0" 400 2373 1785 157 373 https SSLv2 DHE-RSA-AES256-SHA dark wine 230
+2001:db8:1ce::1:83 localhost - - [22/Mar/2009:09:30:31 +0100] "GET /example.com HTTP/2.0" 400 4812 4212 89 36 https TLSv1.2 AES256-SHA light wine 230
+198.51.100.1:83 203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.com HTTP/2.0" 201 1421 4737 194 483 https TLSv1.1 AES256-SHA light wine 230
+2001:db8:1ce::1:81 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.other HTTP/2" 200 3485 1976 369 77 https SSLv2 AES256-SHA dark wine 230
+2001:db8:1ce::1:83 localhost - - [22/Mar/2009:09:30:31 +0100] "GET /example.org HTTP/1.1" 100 4414 4356 317 178 http SSLv3 DHE-RSA-AES256-SHA light wine 230
+Unmatched! The rat the cat the dog chased killed ate the malt!
+198.51.100.1:81 203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.other HTTP/2.0" 200 1151 2186 490 362 https TLSv1.3 DHE-RSA-AES256-SHA light wine 230
+2001:db8:1ce::1:81 localhost - - [22/Mar/2009:09:30:31 +0100] "GET /example.com HTTP/2" 400 2991 3256 184 166 https TLSv1.3 PSK-RC4-SHA dark wine 230
+Unmatched! The rat the cat the dog chased killed ate the malt!
+localhost:83 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.other HTTP/2" 300 3872 2708 139 378 http TLSv1.3 PSK-RC4-SHA dark beer 230
+localhost:83 2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.other HTTP/2" 201 2991 3430 178 104 http TLSv1.2 ECDHE-RSA-AES256-SHA light wine 230
+Unmatched! The rat the cat the dog chased killed ate the malt!
+2001:db8:1ce::1:84 203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.com HTTP/1.1" 400 2825 4431 30 249 http TLSv1.3 ECDHE-RSA-AES256-SHA light wine 230
+test.example.org:83 203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.other HTTP/2.0" 100 1319 4859 435 44 http TLSv1.2 ECDHE-RSA-AES256-SHA light beer 230
+Unmatched! The rat the cat the dog chased killed ate the malt!
+localhost:80 localhost - - [22/Mar/2009:09:30:31 +0100] "GET /example.com HTTP/1.1" 300 3962 1663 23 264 https TLSv1.2 DHE-RSA-AES256-SHA dark wine 230
+localhost:80 203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.org HTTP/1.1" 201 4465 2310 493 99 https TLSv1.1 AES256-SHA dark beer 230
+test.example.com:80 203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.com HTTP/2.0" 100 2942 4946 119 27 https TLSv1.1 PSK-RC4-SHA dark wine 230
+test.example.org:82 203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.org HTTP/2" 201 3243 2992 432 260 http TLSv1 AES256-SHA light wine 230
+2001:db8:1ce::1:83 203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.org HTTP/2" 301 2312 3695 112 330 http SSLv2 ECDHE-RSA-AES256-SHA light wine 230
+2001:db8:1ce::1:81 2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.other HTTP/1.1" 400 3118 3248 347 114 https TLSv1 ECDHE-RSA-AES256-SHA light wine 230
+test.example.com:80 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.org HTTP/1.1" 100 3126 4402 19 375 https SSLv3 ECDHE-RSA-AES256-SHA dark beer 230
+2001:db8:1ce::1:83 203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.net HTTP/2.0" 201 2671 2153 195 310 https SSLv2 ECDHE-RSA-AES256-SHA light wine 230
+Unmatched! The rat the cat the dog chased killed ate the malt!
+test.example.com:80 203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.other HTTP/2.0" 401 1582 3558 292 394 http TLSv1.3 PSK-RC4-SHA light wine 230
+test.example.com:81 203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.net HTTP/1.1" 201 4969 4169 281 71 http TLSv1.2 PSK-RC4-SHA dark beer 230
+test.example.org:80 203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.other HTTP/1.1" 200 4531 3111 272 437 https TLSv1.2 DHE-RSA-AES256-SHA dark wine 230
+198.51.100.1:81 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.net HTTP/1.1" 401 1746 4177 224 89 https TLSv1.3 AES256-SHA dark beer 230
+localhost:80 203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.com HTTP/2" 200 4147 4505 454 65 https TLSv1.1 ECDHE-RSA-AES256-SHA light beer 230
+localhost:84 203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.com HTTP/1.1" 300 2235 3397 290 243 https TLSv1.3 DHE-RSA-AES256-SHA dark beer 230
+localhost:82 203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.org HTTP/2.0" 201 1633 3774 146 394 https TLSv1.2 AES256-SHA light wine 230
+2001:db8:1ce::1:80 203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.com HTTP/2" 100 4580 2717 219 305 https TLSv1.3 PSK-RC4-SHA dark beer 230
+test.example.com:82 203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.com HTTP/2.0" 401 1395 3562 303 392 http SSLv2 DHE-RSA-AES256-SHA dark beer 230
+2001:db8:1ce::1:80 localhost - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.com HTTP/1.1" 201 4827 1947 419 323 https TLSv1.2 DHE-RSA-AES256-SHA light beer 230
+2001:db8:1ce::1:83 2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.net HTTP/2.0" 301 1116 4737 55 448 http TLSv1.2 ECDHE-RSA-AES256-SHA light beer 230
+test.example.org:84 203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.org HTTP/2" 401 3130 4303 71 401 https TLSv1.1 DHE-RSA-AES256-SHA light wine 230
+localhost:82 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.com HTTP/2.0" 200 4968 4988 75 411 http TLSv1 AES256-SHA dark wine 230
+198.51.100.1:80 203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.com HTTP/2" 401 1586 4626 58 248 http TLSv1.2 ECDHE-RSA-AES256-SHA dark wine 230
+Unmatched! The rat the cat the dog chased killed ate the malt!
+198.51.100.1:81 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.other HTTP/1.1" 300 2652 2273 379 240 https TLSv1.2 ECDHE-RSA-AES256-SHA dark wine 230
+test.example.org:81 localhost - - [22/Mar/2009:09:30:31 +0100] "POST /example.org HTTP/1.1" 101 2696 1585 383 365 http SSLv2 ECDHE-RSA-AES256-SHA dark beer 230
+localhost:80 localhost - - [22/Mar/2009:09:30:31 +0100] "GET /example.net HTTP/2" 200 4278 2629 350 109 http TLSv1.3 ECDHE-RSA-AES256-SHA light wine 230
+localhost:83 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.com HTTP/2.0" 301 3012 3094 37 44 http SSLv2 PSK-RC4-SHA light beer 230
+localhost:80 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.net HTTP/2" 100 3197 1038 391 416 https TLSv1.2 AES256-SHA dark beer 230
+test.example.org:84 203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.org HTTP/2.0" 100 1842 1947 402 267 https SSLv3 PSK-RC4-SHA light wine 230
+198.51.100.1:81 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.other HTTP/1.1" 200 3365 4296 23 143 https TLSv1.2 AES256-SHA dark beer 230
+2001:db8:1ce::1:80 203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.com HTTP/2.0" 301 3630 4425 343 460 http TLSv1.3 ECDHE-RSA-AES256-SHA light wine 230
+2001:db8:1ce::1:81 localhost - - [22/Mar/2009:09:30:31 +0100] "POST /example.net HTTP/2.0" 101 3175 2967 441 86 http TLSv1.1 ECDHE-RSA-AES256-SHA light wine 230
+198.51.100.1:82 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.other HTTP/1.1" 100 4423 2052 251 81 https TLSv1.3 DHE-RSA-AES256-SHA dark wine 230
+test.example.com:83 localhost - - [22/Mar/2009:09:30:31 +0100] "POST /example.other HTTP/1.1" 400 3440 4089 408 442 https SSLv3 PSK-RC4-SHA dark beer 230
+test.example.org:80 203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.org HTTP/1.1" 100 3827 3457 288 305 http TLSv1 PSK-RC4-SHA dark beer 230
+198.51.100.1:82 203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.com HTTP/1.1" 101 1292 2131 382 334 http TLSv1 ECDHE-RSA-AES256-SHA light wine 230
+198.51.100.1:83 localhost - - [22/Mar/2009:09:30:31 +0100] "POST /example.com HTTP/2" 400 2026 1831 417 123 http TLSv1.1 ECDHE-RSA-AES256-SHA light beer 230
+Unmatched! The rat the cat the dog chased killed ate the malt!
+test.example.com:84 localhost - - [22/Mar/2009:09:30:31 +0100] "POST /example.org HTTP/2" 401 4300 3883 270 160 https TLSv1 PSK-RC4-SHA light wine 230
+localhost:84 2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.net HTTP/2" 300 1360 1687 49 356 https SSLv3 ECDHE-RSA-AES256-SHA dark beer 230
+localhost:81 localhost - - [22/Mar/2009:09:30:31 +0100] "POST /example.org HTTP/1.1" 201 2871 3581 214 269 https TLSv1.1 AES256-SHA dark wine 230
+test.example.org:83 localhost - - [22/Mar/2009:09:30:31 +0100] "POST /example.com HTTP/2.0" 201 4426 4191 74 358 http TLSv1.1 PSK-RC4-SHA light beer 230
+198.51.100.1:84 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.other HTTP/2" 200 3533 2075 370 403 https TLSv1.2 DHE-RSA-AES256-SHA dark wine 230
+test.example.com:82 2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.com HTTP/2.0" 100 3660 3471 272 136 http TLSv1 AES256-SHA light beer 230
+test.example.org:81 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.org HTTP/1.1" 200 1999 3259 277 254 https TLSv1.3 AES256-SHA dark wine 230
+198.51.100.1:81 203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.com HTTP/2.0" 300 3103 2906 200 141 http TLSv1.2 DHE-RSA-AES256-SHA light wine 230
+198.51.100.1:82 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.com HTTP/1.1" 400 4197 4507 159 311 https SSLv3 AES256-SHA dark wine 230
+test.example.com:80 2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.net HTTP/2" 300 1049 4682 464 353 http TLSv1.2 ECDHE-RSA-AES256-SHA dark wine 230
+test.example.com:83 203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.org HTTP/1.1" 200 2163 2112 266 133 https TLSv1.1 ECDHE-RSA-AES256-SHA dark beer 230
+2001:db8:1ce::1:82 localhost - - [22/Mar/2009:09:30:31 +0100] "POST /example.org HTTP/1.1" 400 4310 2281 107 217 https SSLv2 AES256-SHA light beer 230
+198.51.100.1:83 localhost - - [22/Mar/2009:09:30:31 +0100] "GET /example.org HTTP/2.0" 400 4215 2676 425 244 https SSLv3 PSK-RC4-SHA dark beer 230
+2001:db8:1ce::1:84 localhost - - [22/Mar/2009:09:30:31 +0100] "POST /example.org HTTP/2" 200 3707 1631 300 224 http TLSv1 PSK-RC4-SHA light wine 230
+test.example.com:81 2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.other HTTP/2" 201 2082 4603 150 200 http TLSv1.2 DHE-RSA-AES256-SHA dark wine 230
+test.example.com:83 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.other HTTP/2" 301 3547 4120 146 234 https TLSv1.1 PSK-RC4-SHA dark beer 230
+test.example.org:81 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.other HTTP/1.1" 101 1999 2794 47 420 https TLSv1.1 DHE-RSA-AES256-SHA light beer 230
+2001:db8:1ce::1:80 localhost - - [22/Mar/2009:09:30:31 +0100] "GET /example.org HTTP/1.1" 100 2648 4958 389 16 https SSLv3 AES256-SHA light beer 230
+localhost:84 203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.org HTTP/2" 201 1202 2909 26 340 http TLSv1 DHE-RSA-AES256-SHA light wine 230
+localhost:81 203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.com HTTP/1.1" 201 1393 3045 248 421 https TLSv1.1 ECDHE-RSA-AES256-SHA light wine 230
+localhost:81 203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.net HTTP/1.1" 101 2739 4561 61 257 http SSLv3 ECDHE-RSA-AES256-SHA dark beer 230
+localhost:82 localhost - - [22/Mar/2009:09:30:31 +0100] "GET /example.org HTTP/2.0" 301 4127 4190 374 278 https TLSv1 AES256-SHA light beer 230
+Unmatched! The rat the cat the dog chased killed ate the malt!
+localhost:84 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.com HTTP/1.1" 201 3442 1472 366 373 https SSLv2 ECDHE-RSA-AES256-SHA light wine 230
+test.example.org:82 203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.com HTTP/2" 301 1745 1279 207 55 https SSLv3 DHE-RSA-AES256-SHA light beer 230
+test.example.com:81 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.org HTTP/2.0" 400 1462 2721 168 385 https TLSv1.1 DHE-RSA-AES256-SHA light beer 230
+198.51.100.1:82 203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.net HTTP/1.1" 100 1680 2358 342 237 https TLSv1.2 PSK-RC4-SHA light wine 230
+2001:db8:1ce::1:83 localhost - - [22/Mar/2009:09:30:31 +0100] "GET /example.org HTTP/2.0" 101 1242 3123 296 479 https SSLv2 DHE-RSA-AES256-SHA light wine 230
+test.example.com:84 203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.net HTTP/2" 200 1525 4029 39 30 https TLSv1.1 AES256-SHA dark wine 230
+localhost:81 2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.org HTTP/2" 301 4348 4902 121 103 https TLSv1.3 ECDHE-RSA-AES256-SHA light beer 230
+Unmatched! The rat the cat the dog chased killed ate the malt!
+test.example.org:82 203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.com HTTP/2" 201 4992 1046 5 408 https TLSv1.3 ECDHE-RSA-AES256-SHA dark wine 230
+test.example.com:84 2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.other HTTP/2" 401 1331 2834 232 212 https TLSv1.1 DHE-RSA-AES256-SHA dark wine 230
+test.example.com:83 203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.other HTTP/2" 100 1281 3004 261 61 https TLSv1.1 DHE-RSA-AES256-SHA dark beer 230
+localhost:83 2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.other HTTP/1.1" 300 3985 2627 249 397 https SSLv2 PSK-RC4-SHA dark beer 230
+2001:db8:1ce::1:82 203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.org HTTP/1.1" 201 2835 3195 194 308 http TLSv1.2 ECDHE-RSA-AES256-SHA light beer 230
+198.51.100.1:83 2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.com HTTP/2" 101 4413 2887 257 108 https TLSv1 PSK-RC4-SHA light beer 230
+198.51.100.1:84 203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.org HTTP/2" 300 2514 2890 186 53 https TLSv1.3 ECDHE-RSA-AES256-SHA dark beer 230
+198.51.100.1:80 203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.org HTTP/2" 200 2396 3424 101 295 http SSLv2 PSK-RC4-SHA light wine 230
+2001:db8:1ce::1:80 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.com HTTP/2.0" 201 4849 3176 453 302 http TLSv1.1 AES256-SHA dark beer 230
+2001:db8:1ce::1:81 203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.net HTTP/1.1" 200 4191 2809 300 205 https TLSv1 ECDHE-RSA-AES256-SHA dark wine 230
+localhost:83 203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.other HTTP/1.1" 300 2920 1745 421 80 http TLSv1.1 AES256-SHA dark wine 230
+localhost:81 2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.net HTTP/2" 100 3313 1900 226 163 http TLSv1.3 PSK-RC4-SHA light wine 230
+localhost:83 2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.net HTTP/2.0" 401 2298 1179 181 229 https TLSv1 PSK-RC4-SHA dark beer 230
+2001:db8:1ce::1:83 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.com HTTP/1.1" 401 4604 4392 239 20 http SSLv2 ECDHE-RSA-AES256-SHA dark beer 230
+test.example.org:83 2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.com HTTP/2" 401 2077 2339 132 433 https TLSv1.2 ECDHE-RSA-AES256-SHA dark beer 230
+198.51.100.1:83 203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.net HTTP/1.1" 201 4448 2085 496 68 https SSLv3 AES256-SHA light wine 230
+localhost:80 localhost - - [22/Mar/2009:09:30:31 +0100] "POST /example.net HTTP/2.0" 201 3219 2834 226 50 https SSLv3 PSK-RC4-SHA light wine 230
+test.example.org:84 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.com HTTP/2" 101 2908 3137 50 236 http TLSv1 DHE-RSA-AES256-SHA light beer 230
+test.example.org:80 localhost - - [22/Mar/2009:09:30:31 +0100] "GET /example.net HTTP/2.0" 301 4350 1578 469 206 http TLSv1.2 ECDHE-RSA-AES256-SHA light beer 230
+localhost:82 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.other HTTP/1.1" 301 3255 1349 245 492 http TLSv1.3 AES256-SHA dark wine 230
+test.example.com:81 2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.com HTTP/1.1" 201 3960 2563 455 228 http SSLv3 DHE-RSA-AES256-SHA light beer 230
+test.example.org:82 203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.net HTTP/2" 301 3302 1004 184 392 https TLSv1 ECDHE-RSA-AES256-SHA light beer 230
+2001:db8:1ce::1:80 203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.net HTTP/2.0" 401 1565 4150 93 130 https TLSv1 AES256-SHA dark wine 230
+Unmatched! The rat the cat the dog chased killed ate the malt!
+localhost:82 203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.other HTTP/2.0" 401 2251 2071 373 471 http TLSv1.2 AES256-SHA light wine 230
+198.51.100.1:84 2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.com HTTP/1.1" 200 1589 2077 159 389 http TLSv1.1 ECDHE-RSA-AES256-SHA light beer 230
+2001:db8:1ce::1:81 2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.com HTTP/2" 401 1081 2154 103 244 https TLSv1.1 AES256-SHA light wine 230
+198.51.100.1:82 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.com HTTP/2" 101 3824 4262 478 439 https TLSv1 DHE-RSA-AES256-SHA light beer 230
+test.example.org:83 203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.net HTTP/2.0" 200 2123 3904 183 420 https TLSv1.1 DHE-RSA-AES256-SHA light wine 230
+localhost:83 203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.other HTTP/2.0" 100 4324 4867 411 30 https TLSv1.1 ECDHE-RSA-AES256-SHA light beer 230
+198.51.100.1:82 localhost - - [22/Mar/2009:09:30:31 +0100] "GET /example.org HTTP/2.0" 300 2462 3054 286 47 http SSLv3 ECDHE-RSA-AES256-SHA dark wine 230
+2001:db8:1ce::1:82 2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.com HTTP/1.1" 101 3389 4012 81 113 https TLSv1.2 DHE-RSA-AES256-SHA dark beer 230
+test.example.org:82 localhost - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.com HTTP/1.1" 301 1469 3001 134 460 http TLSv1.1 AES256-SHA dark wine 230
+test.example.com:81 2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.com HTTP/1.1" 201 1962 1869 269 191 https SSLv3 ECDHE-RSA-AES256-SHA light beer 230
+2001:db8:1ce::1:84 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.com HTTP/2.0" 400 1807 3457 477 77 https TLSv1 ECDHE-RSA-AES256-SHA dark beer 230
+localhost:84 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.net HTTP/2" 201 2041 2072 464 193 http SSLv3 ECDHE-RSA-AES256-SHA light wine 230
+198.51.100.1:84 203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.org HTTP/2" 301 2731 1114 92 45 http TLSv1.1 DHE-RSA-AES256-SHA light beer 230
+localhost:81 203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.net HTTP/2" 300 4016 4766 425 405 https TLSv1.1 PSK-RC4-SHA light beer 230
+test.example.com:80 2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.other HTTP/1.1" 201 3480 3735 420 338 https TLSv1.1 ECDHE-RSA-AES256-SHA light beer 230
+198.51.100.1:82 2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.com HTTP/2.0" 301 4654 2443 495 322 https TLSv1.3 PSK-RC4-SHA light beer 230
+2001:db8:1ce::1:80 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.org HTTP/2" 301 1575 1083 214 55 http TLSv1.2 AES256-SHA dark wine 230
+localhost:81 203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.other HTTP/2" 101 3791 3173 436 449 http TLSv1 AES256-SHA dark beer 230
+test.example.org:82 localhost - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.org HTTP/2.0" 301 4446 4004 298 459 https TLSv1.1 ECDHE-RSA-AES256-SHA dark beer 230
+test.example.com:82 localhost - - [22/Mar/2009:09:30:31 +0100] "GET /example.com HTTP/2.0" 300 3414 4751 49 391 http TLSv1.2 ECDHE-RSA-AES256-SHA light wine 230
+test.example.org:84 203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.net HTTP/2" 401 2058 2053 250 290 http TLSv1 AES256-SHA dark beer 230
+test.example.com:81 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.net HTTP/2" 200 2115 4533 461 278 https SSLv3 AES256-SHA dark wine 230
+test.example.com:80 203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.org HTTP/2" 300 3872 1292 172 275 https TLSv1.1 AES256-SHA light wine 230
+localhost:82 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.net HTTP/2" 201 4947 4545 50 414 http SSLv3 AES256-SHA dark wine 230
+2001:db8:1ce::1:80 2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.org HTTP/2" 400 1012 3777 305 193 https TLSv1.3 DHE-RSA-AES256-SHA light beer 230
+localhost:84 localhost - - [22/Mar/2009:09:30:31 +0100] "POST /example.org HTTP/2" 201 1862 1381 420 109 http TLSv1.2 ECDHE-RSA-AES256-SHA dark wine 230
+2001:db8:1ce::1:81 203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.org HTTP/2.0" 300 3579 3376 434 67 https TLSv1 PSK-RC4-SHA light beer 230
+Unmatched! The rat the cat the dog chased killed ate the malt!
+test.example.org:81 203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.org HTTP/2" 401 4937 1232 470 280 http TLSv1 DHE-RSA-AES256-SHA light beer 230
+2001:db8:1ce::1:82 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.other HTTP/2.0" 100 4926 4244 82 284 http SSLv2 ECDHE-RSA-AES256-SHA dark wine 230
+2001:db8:1ce::1:82 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.org HTTP/2.0" 100 4783 4925 497 340 http TLSv1 ECDHE-RSA-AES256-SHA light wine 230
+2001:db8:1ce::1:80 localhost - - [22/Mar/2009:09:30:31 +0100] "GET /example.net HTTP/1.1" 100 3308 1377 208 232 http SSLv2 PSK-RC4-SHA light wine 230
+localhost:82 localhost - - [22/Mar/2009:09:30:31 +0100] "GET /example.other HTTP/1.1" 101 4285 4695 426 481 https TLSv1.3 DHE-RSA-AES256-SHA dark wine 230
+localhost:82 203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.other HTTP/2.0" 100 1953 2196 101 129 https SSLv3 PSK-RC4-SHA light beer 230
+localhost:80 2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.com HTTP/1.1" 200 2169 4267 65 181 http TLSv1.3 AES256-SHA light wine 230
+test.example.org:82 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.org HTTP/1.1" 301 1698 1366 116 101 http TLSv1.3 DHE-RSA-AES256-SHA dark beer 230
+test.example.com:80 localhost - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.org HTTP/1.1" 200 3534 4390 114 479 https TLSv1.3 ECDHE-RSA-AES256-SHA dark beer 230
+Unmatched! The rat the cat the dog chased killed ate the malt!
+test.example.org:80 203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.org HTTP/2.0" 101 3583 1060 400 28 http TLSv1.3 PSK-RC4-SHA light wine 230
+test.example.com:84 203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.net HTTP/2.0" 100 3078 4116 60 444 http TLSv1.3 ECDHE-RSA-AES256-SHA dark beer 230
+2001:db8:1ce::1:81 203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.net HTTP/1.1" 401 3975 2201 438 419 http SSLv2 AES256-SHA dark beer 230
+test.example.org:81 203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.org HTTP/1.1" 100 3756 2827 424 411 https TLSv1.3 DHE-RSA-AES256-SHA dark beer 230
+test.example.com:84 203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.other HTTP/1.1" 400 2898 3218 258 198 http SSLv2 PSK-RC4-SHA dark wine 230
+localhost:80 2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.net HTTP/1.1" 201 2076 3000 320 196 http SSLv2 PSK-RC4-SHA light beer 230
+198.51.100.1:83 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.other HTTP/2" 201 1439 4814 47 360 http TLSv1 ECDHE-RSA-AES256-SHA dark wine 230
+test.example.org:80 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.org HTTP/2.0" 301 2871 2870 491 411 https SSLv2 ECDHE-RSA-AES256-SHA dark beer 230
+test.example.com:81 2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.com HTTP/2" 200 2744 3085 11 151 http SSLv3 ECDHE-RSA-AES256-SHA light beer 230
+2001:db8:1ce::1:82 203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.com HTTP/1.1" 101 1241 1752 324 154 https TLSv1.2 DHE-RSA-AES256-SHA dark wine 230
+2001:db8:1ce::1:82 203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.org HTTP/2.0" 301 3834 4235 270 331 https TLSv1.2 PSK-RC4-SHA dark beer 230
+test.example.com:84 203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.org HTTP/2" 200 2431 3778 103 78 http TLSv1.2 PSK-RC4-SHA light beer 230
+198.51.100.1:81 localhost - - [22/Mar/2009:09:30:31 +0100] "GET /example.com HTTP/2" 200 2250 1787 340 132 http TLSv1.3 ECDHE-RSA-AES256-SHA dark beer 230
+2001:db8:1ce::1:81 localhost - - [22/Mar/2009:09:30:31 +0100] "POST /example.com HTTP/1.1" 200 4838 1201 79 10 http SSLv2 AES256-SHA dark beer 230
+2001:db8:1ce::1:84 203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.other HTTP/2.0" 400 2953 1165 492 245 http TLSv1.2 PSK-RC4-SHA light wine 230
+198.51.100.1:83 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.net HTTP/2" 200 2540 3818 490 295 http TLSv1.3 DHE-RSA-AES256-SHA dark beer 230
+test.example.com:83 localhost - - [22/Mar/2009:09:30:31 +0100] "GET /example.net HTTP/2" 400 4469 3199 203 107 https TLSv1.2 ECDHE-RSA-AES256-SHA light wine 230
+test.example.com:81 203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.other HTTP/2" 201 3270 3948 223 443 http TLSv1 ECDHE-RSA-AES256-SHA light wine 230
+2001:db8:1ce::1:81 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.com HTTP/2.0" 300 4902 1169 359 328 http TLSv1.3 PSK-RC4-SHA dark wine 230
+test.example.org:80 localhost - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.other HTTP/1.1" 400 1788 4502 355 220 http TLSv1 PSK-RC4-SHA dark beer 230
+198.51.100.1:82 203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.other HTTP/2.0" 100 1565 2909 127 435 http TLSv1.3 PSK-RC4-SHA light wine 230
+test.example.com:83 localhost - - [22/Mar/2009:09:30:31 +0100] "POST /example.net HTTP/2" 101 4507 2396 259 100 https SSLv3 PSK-RC4-SHA light beer 230
+198.51.100.1:83 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.net HTTP/1.1" 100 3119 2306 387 395 http TLSv1.1 DHE-RSA-AES256-SHA light beer 230
+localhost:82 localhost - - [22/Mar/2009:09:30:31 +0100] "GET /example.com HTTP/2" 100 1473 4928 364 371 https SSLv3 ECDHE-RSA-AES256-SHA light wine 230
+198.51.100.1:83 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.other HTTP/2" 100 1449 3719 390 401 http TLSv1.2 ECDHE-RSA-AES256-SHA dark wine 230
+test.example.com:82 203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.org HTTP/2.0" 301 1897 1428 438 210 http TLSv1.1 ECDHE-RSA-AES256-SHA light wine 230
+localhost:81 2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.net HTTP/2.0" 300 1381 1043 367 453 http TLSv1.2 DHE-RSA-AES256-SHA light wine 230
+localhost:83 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.com HTTP/1.1" 400 3495 2740 375 378 http TLSv1 DHE-RSA-AES256-SHA light beer 230
+2001:db8:1ce::1:84 localhost - - [22/Mar/2009:09:30:31 +0100] "POST /example.com HTTP/2.0" 301 4754 4667 293 56 https SSLv2 ECDHE-RSA-AES256-SHA dark wine 230
+test.example.org:83 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.org HTTP/2.0" 200 3447 3853 454 348 http TLSv1 PSK-RC4-SHA light beer 230
+198.51.100.1:82 203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.com HTTP/2.0" 401 4669 2808 89 235 https TLSv1.3 PSK-RC4-SHA dark beer 230
+Unmatched! The rat the cat the dog chased killed ate the malt!
+localhost:80 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.com HTTP/2.0" 401 3134 1040 401 33 https SSLv3 ECDHE-RSA-AES256-SHA dark beer 230
+2001:db8:1ce::1:80 localhost - - [22/Mar/2009:09:30:31 +0100] "POST /example.net HTTP/2.0" 200 3823 3615 48 110 https SSLv3 PSK-RC4-SHA light beer 230
+test.example.org:81 203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.other HTTP/2.0" 301 2971 3712 2 325 https TLSv1.1 AES256-SHA dark beer 230
+2001:db8:1ce::1:83 203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.org HTTP/1.1" 301 2932 2388 482 302 http SSLv2 AES256-SHA light wine 230
+198.51.100.1:83 2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.net HTTP/2.0" 400 2009 3888 347 59 http TLSv1 AES256-SHA light wine 230
+2001:db8:1ce::1:81 2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.org HTTP/2" 401 4252 3808 285 384 https TLSv1.1 ECDHE-RSA-AES256-SHA dark wine 230
+2001:db8:1ce::1:84 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.net HTTP/2" 301 2664 1505 455 419 https TLSv1.1 DHE-RSA-AES256-SHA light wine 230
+2001:db8:1ce::1:83 203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.com HTTP/2" 400 2474 2102 40 377 http TLSv1.3 PSK-RC4-SHA light beer 230
+test.example.com:81 localhost - - [22/Mar/2009:09:30:31 +0100] "GET /example.com HTTP/1.1" 301 4478 4105 239 420 https TLSv1 DHE-RSA-AES256-SHA light wine 230
+test.example.org:81 localhost - - [22/Mar/2009:09:30:31 +0100] "GET /example.other HTTP/1.1" 201 4461 1737 416 129 https TLSv1.3 PSK-RC4-SHA dark wine 230
+198.51.100.1:84 203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.net HTTP/1.1" 100 2381 2018 34 247 http TLSv1.3 DHE-RSA-AES256-SHA light beer 230
+test.example.com:80 203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.com HTTP/2.0" 101 3138 3141 178 333 http TLSv1.2 DHE-RSA-AES256-SHA dark wine 230
+test.example.com:82 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.com HTTP/2.0" 300 2203 4463 450 497 http TLSv1.3 PSK-RC4-SHA dark wine 230
+Unmatched! The rat the cat the dog chased killed ate the malt!
+test.example.com:81 localhost - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.com HTTP/2.0" 400 3937 4320 30 151 http TLSv1.2 PSK-RC4-SHA dark beer 230
+test.example.com:82 localhost - - [22/Mar/2009:09:30:31 +0100] "GET /example.other HTTP/2" 100 2858 4431 92 38 http SSLv2 PSK-RC4-SHA light beer 230
+2001:db8:1ce::1:83 2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.org HTTP/2" 301 3339 1333 291 479 http TLSv1.2 PSK-RC4-SHA dark wine 230
+test.example.org:81 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.com HTTP/2.0" 201 1799 1725 184 24 http TLSv1 AES256-SHA light wine 230
+localhost:80 2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.net HTTP/1.1" 400 4743 1337 381 494 http SSLv2 DHE-RSA-AES256-SHA dark beer 230
+198.51.100.1:84 203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.com HTTP/2" 300 4542 4411 280 383 http TLSv1 AES256-SHA dark wine 230
+198.51.100.1:80 203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.other HTTP/1.1" 101 3600 2913 361 411 https TLSv1.2 AES256-SHA light wine 230
+198.51.100.1:84 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.org HTTP/1.1" 100 2860 4491 431 82 https TLSv1.3 DHE-RSA-AES256-SHA light wine 230
+localhost:82 2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.org HTTP/2.0" 201 4544 1146 86 146 http TLSv1.2 PSK-RC4-SHA dark wine 230
+Unmatched! The rat the cat the dog chased killed ate the malt!
+198.51.100.1:81 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.other HTTP/2" 401 1412 3023 474 170 https TLSv1.3 DHE-RSA-AES256-SHA dark wine 230
+198.51.100.1:83 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.other HTTP/2" 201 2870 4503 86 428 https TLSv1.2 ECDHE-RSA-AES256-SHA light beer 230
+198.51.100.1:83 localhost - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.org HTTP/1.1" 200 2250 1801 236 283 https TLSv1.2 PSK-RC4-SHA dark beer 230
+test.example.org:81 localhost - - [22/Mar/2009:09:30:31 +0100] "POST /example.com HTTP/1.1" 100 3859 2489 455 150 http SSLv3 PSK-RC4-SHA light beer 230
+Unmatched! The rat the cat the dog chased killed ate the malt!
+test.example.com:81 localhost - - [22/Mar/2009:09:30:31 +0100] "GET /example.net HTTP/2" 400 4322 3740 68 383 http TLSv1 AES256-SHA light wine 230
+localhost:83 localhost - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.net HTTP/1.1" 400 1369 3435 223 363 http TLSv1 AES256-SHA dark beer 230
+test.example.org:80 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.com HTTP/2" 101 1863 1538 81 9 https TLSv1 DHE-RSA-AES256-SHA light beer 230
+localhost:80 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.com HTTP/2" 100 4390 2872 173 68 https TLSv1.3 DHE-RSA-AES256-SHA dark beer 230
+test.example.com:83 203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.com HTTP/1.1" 300 2549 4334 353 127 http TLSv1 AES256-SHA light beer 230
+test.example.com:80 localhost - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.com HTTP/1.1" 300 2314 3541 376 69 https TLSv1.3 ECDHE-RSA-AES256-SHA light beer 230
+test.example.org:80 localhost - - [22/Mar/2009:09:30:31 +0100] "GET /example.org HTTP/2.0" 301 2883 3804 95 80 https TLSv1 PSK-RC4-SHA dark wine 230
+Unmatched! The rat the cat the dog chased killed ate the malt!
+2001:db8:1ce::1:80 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.com HTTP/1.1" 201 3245 4083 153 481 https TLSv1 PSK-RC4-SHA dark beer 230
+localhost:82 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.org HTTP/2" 401 4633 2483 350 196 https TLSv1.3 ECDHE-RSA-AES256-SHA dark beer 230
+198.51.100.1:84 localhost - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.com HTTP/2" 201 1944 2389 217 413 http TLSv1.2 ECDHE-RSA-AES256-SHA dark beer 230
+2001:db8:1ce::1:80 localhost - - [22/Mar/2009:09:30:31 +0100] "GET /example.other HTTP/2.0" 401 4159 4546 294 252 http TLSv1.2 DHE-RSA-AES256-SHA dark wine 230
+test.example.org:84 2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.org HTTP/2" 200 2100 1268 115 431 http SSLv2 ECDHE-RSA-AES256-SHA dark wine 230
+test.example.com:83 localhost - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.com HTTP/2" 301 4386 3222 41 383 http TLSv1.1 AES256-SHA light wine 230
+test.example.org:81 203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.net HTTP/2.0" 200 4859 2780 28 16 https SSLv2 DHE-RSA-AES256-SHA dark beer 230
+2001:db8:1ce::1:80 203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.net HTTP/2.0" 301 1541 2755 114 194 https TLSv1 ECDHE-RSA-AES256-SHA light wine 230
+198.51.100.1:81 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.net HTTP/1.1" 300 2058 3951 312 428 http TLSv1.3 PSK-RC4-SHA dark beer 230
+198.51.100.1:80 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.com HTTP/1.1" 400 3076 4237 341 115 http TLSv1.3 AES256-SHA dark wine 230
+test.example.com:81 203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.org HTTP/2" 300 3384 2583 2 348 http TLSv1.3 ECDHE-RSA-AES256-SHA dark wine 230
+localhost:83 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.net HTTP/1.1" 200 1283 4090 311 39 https SSLv3 DHE-RSA-AES256-SHA dark wine 230
+localhost:81 localhost - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.other HTTP/2" 100 1620 3450 491 119 http TLSv1.1 PSK-RC4-SHA dark beer 230
+test.example.com:81 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.org HTTP/2" 200 3572 3267 95 80 http TLSv1.2 PSK-RC4-SHA dark beer 230
+localhost:80 localhost - - [22/Mar/2009:09:30:31 +0100] "POST /example.net HTTP/1.1" 300 2628 2670 52 307 http TLSv1.1 AES256-SHA dark beer 230
+2001:db8:1ce::1:84 2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.com HTTP/2" 101 3332 4865 246 348 https TLSv1.2 ECDHE-RSA-AES256-SHA light beer 230
+test.example.com:80 203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.com HTTP/1.1" 201 3766 1704 147 217 https SSLv2 DHE-RSA-AES256-SHA dark beer 230
+test.example.com:82 203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.net HTTP/2.0" 200 3763 3904 305 366 http TLSv1.3 DHE-RSA-AES256-SHA dark beer 230
+198.51.100.1:81 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.org HTTP/2" 201 4205 4011 38 144 http SSLv3 DHE-RSA-AES256-SHA light wine 230
+Unmatched! The rat the cat the dog chased killed ate the malt!
+2001:db8:1ce::1:84 203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.net HTTP/2.0" 101 4573 3168 317 94 https TLSv1 PSK-RC4-SHA light beer 230
+localhost:84 2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.other HTTP/2.0" 201 1481 1798 190 170 http TLSv1.1 ECDHE-RSA-AES256-SHA dark wine 230
+test.example.com:84 2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.net HTTP/2" 201 1603 1276 51 465 https SSLv2 PSK-RC4-SHA dark wine 230
+test.example.org:84 2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.org HTTP/2.0" 101 2050 2654 283 287 https TLSv1.1 PSK-RC4-SHA dark wine 230
+198.51.100.1:82 203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.other HTTP/2.0" 201 4943 2143 43 167 http TLSv1.1 ECDHE-RSA-AES256-SHA light beer 230
+198.51.100.1:82 203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.org HTTP/1.1" 401 3854 4082 318 477 https TLSv1.1 ECDHE-RSA-AES256-SHA dark wine 230
+2001:db8:1ce::1:83 203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.com HTTP/2" 100 3235 4635 377 206 http TLSv1.1 AES256-SHA light beer 230
+2001:db8:1ce::1:81 203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.net HTTP/2" 401 4508 2872 185 243 https TLSv1.3 AES256-SHA light beer 230
+test.example.com:80 203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.com HTTP/2.0" 401 4943 3560 48 473 http TLSv1.1 AES256-SHA light beer 230
+test.example.com:81 203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.other HTTP/2" 300 2693 3536 157 430 https SSLv3 DHE-RSA-AES256-SHA dark wine 230
+Unmatched! The rat the cat the dog chased killed ate the malt!
+2001:db8:1ce::1:83 2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.com HTTP/2.0" 300 4321 4966 420 264 http TLSv1.2 DHE-RSA-AES256-SHA dark beer 230
+Unmatched! The rat the cat the dog chased killed ate the malt!
+test.example.org:80 localhost - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.org HTTP/1.1" 101 1470 1279 423 248 https TLSv1 DHE-RSA-AES256-SHA light wine 230
+2001:db8:1ce::1:84 203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.org HTTP/2" 100 2306 4406 237 51 http SSLv2 PSK-RC4-SHA light beer 230
+2001:db8:1ce::1:80 2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.other HTTP/1.1" 301 1766 2834 429 428 https TLSv1.3 DHE-RSA-AES256-SHA dark wine 230
+test.example.org:83 203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.other HTTP/2.0" 300 2997 2317 288 312 http SSLv3 PSK-RC4-SHA dark beer 230
+localhost:84 localhost - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.other HTTP/2.0" 301 2968 1042 124 330 https TLSv1.3 AES256-SHA dark wine 230
+test.example.org:83 localhost - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.org HTTP/2.0" 301 1458 4510 268 136 http SSLv2 ECDHE-RSA-AES256-SHA light wine 230
+test.example.org:82 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.com HTTP/2.0" 301 4830 2063 255 352 http SSLv2 ECDHE-RSA-AES256-SHA dark beer 230
+test.example.org:84 localhost - - [22/Mar/2009:09:30:31 +0100] "POST /example.org HTTP/2" 200 1490 2187 282 484 http TLSv1.3 PSK-RC4-SHA dark wine 230
+localhost:84 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.org HTTP/2.0" 100 1015 2608 460 331 http TLSv1.1 AES256-SHA dark beer 230
+Unmatched! The rat the cat the dog chased killed ate the malt!
+localhost:81 localhost - - [22/Mar/2009:09:30:31 +0100] "POST /example.com HTTP/1.1" 200 4831 1333 57 68 https TLSv1 ECDHE-RSA-AES256-SHA dark wine 230
+localhost:80 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.other HTTP/1.1" 101 2554 1624 18 215 http TLSv1.1 PSK-RC4-SHA dark wine 230
+198.51.100.1:81 203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.net HTTP/2.0" 401 1579 3208 463 31 https TLSv1.1 DHE-RSA-AES256-SHA dark wine 230
+2001:db8:1ce::1:80 localhost - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.com HTTP/1.1" 201 2239 1301 165 27 https SSLv3 ECDHE-RSA-AES256-SHA dark beer 230
+test.example.org:81 203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.other HTTP/2" 200 3874 1581 257 203 http TLSv1.3 AES256-SHA dark beer 230
+localhost:82 203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.other HTTP/1.1" 400 2498 2533 317 269 https TLSv1.1 ECDHE-RSA-AES256-SHA light beer 230
+198.51.100.1:80 203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.net HTTP/1.1" 101 2898 1790 277 180 https TLSv1 DHE-RSA-AES256-SHA light wine 230
+198.51.100.1:80 localhost - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.org HTTP/1.1" 200 2899 2599 70 323 http TLSv1.1 PSK-RC4-SHA dark wine 230
+test.example.org:80 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.other HTTP/2.0" 300 4546 4841 112 34 https SSLv3 ECDHE-RSA-AES256-SHA dark wine 230
+localhost:81 203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.other HTTP/2" 401 4016 3596 394 463 https TLSv1.1 ECDHE-RSA-AES256-SHA dark beer 230
+localhost:83 localhost - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.net HTTP/2" 200 1946 2492 32 123 http SSLv3 DHE-RSA-AES256-SHA light wine 230
+2001:db8:1ce::1:80 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.org HTTP/2" 201 2296 3174 55 473 http TLSv1.3 ECDHE-RSA-AES256-SHA light wine 230
+198.51.100.1:82 localhost - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.net HTTP/2" 201 3037 3632 472 280 https TLSv1.1 DHE-RSA-AES256-SHA light wine 230
+198.51.100.1:80 2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.org HTTP/1.1" 200 1721 1520 211 157 http TLSv1.2 DHE-RSA-AES256-SHA light beer 230
+localhost:80 203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.other HTTP/1.1" 401 4044 3518 390 146 http TLSv1.1 DHE-RSA-AES256-SHA light beer 230
+test.example.com:84 localhost - - [22/Mar/2009:09:30:31 +0100] "GET /example.net HTTP/2.0" 101 4527 3718 95 207 https TLSv1.3 AES256-SHA dark beer 230
+2001:db8:1ce::1:84 203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.org HTTP/2.0" 400 2309 4551 423 304 https TLSv1.1 AES256-SHA dark wine 230
+Unmatched! The rat the cat the dog chased killed ate the malt!
+test.example.com:83 203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.com HTTP/1.1" 401 3864 2883 115 211 https TLSv1.3 PSK-RC4-SHA light wine 230
+test.example.com:83 203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.org HTTP/2" 201 3417 3422 340 242 https SSLv2 PSK-RC4-SHA dark wine 230
+test.example.org:80 localhost - - [22/Mar/2009:09:30:31 +0100] "POST /example.com HTTP/2" 201 4012 2880 45 302 http SSLv2 AES256-SHA light wine 230
+test.example.com:82 localhost - - [22/Mar/2009:09:30:31 +0100] "GET /example.other HTTP/1.1" 300 2834 2781 282 213 http SSLv2 PSK-RC4-SHA dark wine 230
+test.example.org:83 localhost - - [22/Mar/2009:09:30:31 +0100] "POST /example.other HTTP/2" 300 3421 1800 422 72 http TLSv1 ECDHE-RSA-AES256-SHA dark beer 230
+2001:db8:1ce::1:80 203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.com HTTP/2" 300 3052 3602 153 320 https SSLv3 ECDHE-RSA-AES256-SHA light beer 230
+localhost:82 203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.com HTTP/2.0" 400 1578 4720 230 458 https SSLv3 AES256-SHA light wine 230
+test.example.org:80 2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.net HTTP/1.1" 100 1998 3117 220 166 http SSLv2 DHE-RSA-AES256-SHA dark beer 230
+test.example.com:81 203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.org HTTP/2" 100 2041 4031 295 66 http TLSv1.2 DHE-RSA-AES256-SHA light beer 230
+test.example.org:84 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.other HTTP/2.0" 401 4941 3742 174 434 https TLSv1.3 PSK-RC4-SHA dark beer 230
+localhost:84 203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.other HTTP/2.0" 100 1153 2169 24 196 https SSLv2 PSK-RC4-SHA dark wine 230
+test.example.com:80 localhost - - [22/Mar/2009:09:30:31 +0100] "GET /example.net HTTP/1.1" 400 1289 2496 189 98 https SSLv2 PSK-RC4-SHA dark beer 230
+localhost:84 2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.other HTTP/2" 401 4343 2877 90 314 http SSLv2 AES256-SHA light wine 230
+localhost:84 2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.net HTTP/1.1" 200 1203 2163 465 460 https TLSv1.1 PSK-RC4-SHA dark wine 230
+test.example.org:81 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.net HTTP/2.0" 300 2301 3063 36 178 https TLSv1.1 DHE-RSA-AES256-SHA dark beer 230
+test.example.org:80 203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.org HTTP/2" 201 4306 1154 408 297 https TLSv1 AES256-SHA light beer 230
+198.51.100.1:80 2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.org HTTP/1.1" 300 1178 3204 79 101 http SSLv2 DHE-RSA-AES256-SHA dark beer 230
+localhost:84 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.other HTTP/1.1" 200 4431 4442 348 155 http TLSv1.2 DHE-RSA-AES256-SHA light beer 230
+Unmatched! The rat the cat the dog chased killed ate the malt!
+test.example.org:80 203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.org HTTP/1.1" 400 3897 3618 199 149 https TLSv1.3 ECDHE-RSA-AES256-SHA dark wine 230
+2001:db8:1ce::1:80 203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.net HTTP/2" 300 2221 4730 324 338 https SSLv3 AES256-SHA dark beer 230
+198.51.100.1:83 2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.com HTTP/1.1" 100 2030 4453 152 414 https TLSv1 ECDHE-RSA-AES256-SHA light wine 230
+2001:db8:1ce::1:81 localhost - - [22/Mar/2009:09:30:31 +0100] "GET /example.com HTTP/1.1" 301 4937 1625 213 265 https TLSv1.2 PSK-RC4-SHA light beer 230
+localhost:84 203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.org HTTP/2.0" 400 1503 3735 466 485 https TLSv1.2 PSK-RC4-SHA light beer 230
+2001:db8:1ce::1:83 203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.com HTTP/2" 200 3255 2804 105 111 http SSLv3 AES256-SHA dark wine 230
+test.example.com:82 203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.com HTTP/2.0" 300 2376 2896 82 287 https SSLv2 AES256-SHA dark wine 230
+198.51.100.1:82 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.net HTTP/1.1" 100 3525 3376 192 247 https SSLv2 PSK-RC4-SHA light wine 230
+localhost:83 localhost - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.org HTTP/1.1" 300 2813 1800 365 231 https TLSv1.1 PSK-RC4-SHA light wine 230
+localhost:81 localhost - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.com HTTP/2" 201 3589 2334 317 406 https TLSv1.2 PSK-RC4-SHA dark wine 230
+test.example.org:83 2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.net HTTP/1.1" 100 3216 3159 17 344 http TLSv1.3 PSK-RC4-SHA light beer 230
+test.example.org:84 localhost - - [22/Mar/2009:09:30:31 +0100] "POST /example.com HTTP/2.0" 300 4047 2788 196 105 http TLSv1.2 DHE-RSA-AES256-SHA dark wine 230
+2001:db8:1ce::1:80 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.other HTTP/1.1" 301 4253 1092 219 172 https SSLv2 PSK-RC4-SHA light beer 230
+test.example.com:80 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.com HTTP/1.1" 300 2612 4876 113 492 http TLSv1 PSK-RC4-SHA dark beer 230
+test.example.org:82 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.net HTTP/1.1" 400 1039 4957 283 391 https SSLv2 ECDHE-RSA-AES256-SHA dark beer 230
+localhost:80 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.com HTTP/2" 300 2175 1025 349 62 https TLSv1.2 PSK-RC4-SHA light wine 230
+198.51.100.1:82 203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.com HTTP/2" 201 2512 4199 87 90 https TLSv1.2 AES256-SHA dark beer 230
+test.example.com:80 localhost - - [22/Mar/2009:09:30:31 +0100] "POST /example.other HTTP/2.0" 300 3685 3490 288 456 http TLSv1.3 ECDHE-RSA-AES256-SHA light beer 230
+test.example.com:84 203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.com HTTP/1.1" 201 4163 2730 115 186 http TLSv1.3 PSK-RC4-SHA light beer 230
+test.example.org:84 2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.net HTTP/2" 201 4000 1751 482 232 https TLSv1.1 AES256-SHA dark beer 230
+test.example.com:84 203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.org HTTP/2" 301 4544 1246 191 426 http TLSv1.3 AES256-SHA light beer 230
+test.example.org:80 localhost - - [22/Mar/2009:09:30:31 +0100] "GET /example.net HTTP/1.1" 201 2202 1079 44 93 http TLSv1.1 ECDHE-RSA-AES256-SHA dark wine 230
+Unmatched! The rat the cat the dog chased killed ate the malt!
+198.51.100.1:84 203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.org HTTP/1.1" 301 2329 3996 388 386 https SSLv2 DHE-RSA-AES256-SHA dark wine 230
+test.example.org:84 203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.net HTTP/1.1" 101 3564 2870 499 23 https SSLv2 DHE-RSA-AES256-SHA light wine 230
+localhost:84 203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.org HTTP/1.1" 401 3729 1376 161 313 http TLSv1 DHE-RSA-AES256-SHA dark wine 230
+2001:db8:1ce::1:81 localhost - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.com HTTP/2.0" 300 4158 3864 444 149 https TLSv1.2 ECDHE-RSA-AES256-SHA light wine 230
+test.example.com:84 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.net HTTP/2" 301 1809 4286 447 418 http TLSv1 AES256-SHA light beer 230
+198.51.100.1:81 203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.net HTTP/2" 100 1942 2004 497 427 https SSLv2 DHE-RSA-AES256-SHA dark wine 230
+Unmatched! The rat the cat the dog chased killed ate the malt!
+test.example.org:82 203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.other HTTP/2.0" 300 4471 3841 438 176 https TLSv1.3 PSK-RC4-SHA dark beer 230
+test.example.com:82 localhost - - [22/Mar/2009:09:30:31 +0100] "POST /example.net HTTP/1.1" 400 1613 3836 362 432 http SSLv2 ECDHE-RSA-AES256-SHA dark wine 230
+test.example.org:80 localhost - - [22/Mar/2009:09:30:31 +0100] "GET /example.org HTTP/2" 300 4394 2628 344 69 http TLSv1.2 AES256-SHA light wine 230
+localhost:84 localhost - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.com HTTP/1.1" 201 4269 4494 178 149 http SSLv2 ECDHE-RSA-AES256-SHA light wine 230
+198.51.100.1:84 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.org HTTP/2.0" 100 3413 1039 317 109 http TLSv1.1 DHE-RSA-AES256-SHA light wine 230
+test.example.org:83 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.other HTTP/2.0" 201 1110 1662 194 353 https TLSv1.1 AES256-SHA dark wine 230
+2001:db8:1ce::1:84 localhost - - [22/Mar/2009:09:30:31 +0100] "POST /example.other HTTP/1.1" 301 3742 1514 220 406 http TLSv1 DHE-RSA-AES256-SHA dark wine 230
+2001:db8:1ce::1:82 203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.org HTTP/2" 200 2060 4756 406 119 http SSLv3 DHE-RSA-AES256-SHA light wine 230
+2001:db8:1ce::1:82 localhost - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.net HTTP/2" 301 3663 1293 377 420 http TLSv1.2 ECDHE-RSA-AES256-SHA light wine 230
+test.example.com:83 203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.other HTTP/2.0" 301 3708 2360 98 293 https TLSv1.2 DHE-RSA-AES256-SHA dark wine 230
+198.51.100.1:83 203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.net HTTP/1.1" 400 4376 4393 488 173 https SSLv3 PSK-RC4-SHA dark wine 230
+localhost:80 203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.com HTTP/1.1" 100 1129 2917 122 93 http SSLv3 DHE-RSA-AES256-SHA light beer 230
+localhost:81 203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.net HTTP/1.1" 200 4769 2155 492 41 https TLSv1 AES256-SHA dark beer 230
+2001:db8:1ce::1:82 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.org HTTP/1.1" 201 4710 3030 349 392 http TLSv1 AES256-SHA light wine 230
+198.51.100.1:80 203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.com HTTP/2" 100 2642 2759 363 112 http TLSv1.1 DHE-RSA-AES256-SHA light beer 230
+Unmatched! The rat the cat the dog chased killed ate the malt!
+test.example.org:81 203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.net HTTP/1.1" 401 3964 1986 204 377 https SSLv2 DHE-RSA-AES256-SHA light beer 230
+198.51.100.1:81 203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.net HTTP/1.1" 401 1053 3953 284 13 http TLSv1.2 ECDHE-RSA-AES256-SHA light wine 230
+test.example.com:80 localhost - - [22/Mar/2009:09:30:31 +0100] "POST /example.net HTTP/2.0" 301 4436 4981 79 323 https SSLv2 ECDHE-RSA-AES256-SHA dark wine 230
+test.example.com:83 localhost - - [22/Mar/2009:09:30:31 +0100] "GET /example.net HTTP/1.1" 101 3207 2032 206 398 https SSLv3 PSK-RC4-SHA light wine 230
+2001:db8:1ce::1:80 203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.net HTTP/1.1" 200 3938 1928 216 31 https SSLv2 ECDHE-RSA-AES256-SHA light wine 230
+Unmatched! The rat the cat the dog chased killed ate the malt!
+Unmatched! The rat the cat the dog chased killed ate the malt!
+198.51.100.1:81 203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.other HTTP/2.0" 100 2193 1470 144 245 https TLSv1.3 ECDHE-RSA-AES256-SHA light wine 230
+2001:db8:1ce::1:81 203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.net HTTP/2.0" 400 1646 3973 373 78 https TLSv1 ECDHE-RSA-AES256-SHA dark beer 230
+localhost:82 203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.other HTTP/2" 301 3038 3256 361 321 https TLSv1.2 PSK-RC4-SHA dark wine 230
+198.51.100.1:84 203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.net HTTP/2" 401 4535 2424 44 158 http TLSv1.1 ECDHE-RSA-AES256-SHA light wine 230
+localhost:80 localhost - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.net HTTP/2" 301 1366 3163 63 236 http TLSv1 ECDHE-RSA-AES256-SHA light beer 230
+Unmatched! The rat the cat the dog chased killed ate the malt!
+2001:db8:1ce::1:84 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.net HTTP/1.1" 200 4332 3413 59 412 http TLSv1.1 DHE-RSA-AES256-SHA dark wine 230
+2001:db8:1ce::1:84 2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.org HTTP/2.0" 200 3347 4042 218 143 https TLSv1.2 DHE-RSA-AES256-SHA dark beer 230
+2001:db8:1ce::1:84 2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.other HTTP/2" 101 2549 3079 207 113 https TLSv1.3 AES256-SHA light wine 230
+test.example.com:81 localhost - - [22/Mar/2009:09:30:31 +0100] "GET /example.org HTTP/1.1" 101 4605 2701 285 224 http SSLv3 AES256-SHA dark wine 230
+test.example.com:81 203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.other HTTP/1.1" 400 4963 2096 449 476 https SSLv3 AES256-SHA dark beer 230
+Unmatched! The rat the cat the dog chased killed ate the malt!
+Unmatched! The rat the cat the dog chased killed ate the malt!
+2001:db8:1ce::1:82 203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.org HTTP/1.1" 101 4345 2389 145 446 https TLSv1 PSK-RC4-SHA light wine 230
+198.51.100.1:84 203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.com HTTP/1.1" 201 1050 4840 351 106 https TLSv1.2 AES256-SHA dark beer 230
+localhost:83 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.org HTTP/1.1" 300 4089 4457 160 277 http TLSv1.3 PSK-RC4-SHA dark wine 230
+localhost:80 2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.org HTTP/2.0" 100 1766 3641 395 336 http SSLv2 ECDHE-RSA-AES256-SHA light beer 230
+2001:db8:1ce::1:81 2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.net HTTP/2.0" 101 1412 1768 434 79 http SSLv2 ECDHE-RSA-AES256-SHA light wine 230
+localhost:81 203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.other HTTP/2" 300 1912 3209 86 370 https SSLv2 PSK-RC4-SHA dark beer 230
+localhost:84 localhost - - [22/Mar/2009:09:30:31 +0100] "POST /example.org HTTP/2.0" 200 4033 1579 355 409 http TLSv1.3 AES256-SHA dark wine 230
+198.51.100.1:81 2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.com HTTP/2.0" 201 1671 3585 339 63 http TLSv1.3 DHE-RSA-AES256-SHA dark wine 230
+localhost:80 203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.net HTTP/1.1" 400 4248 1510 425 430 https TLSv1.3 ECDHE-RSA-AES256-SHA light wine 230
+test.example.org:84 2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.net HTTP/2.0" 100 4498 1403 239 96 https TLSv1 DHE-RSA-AES256-SHA dark wine 230
+198.51.100.1:83 localhost - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.com HTTP/2.0" 401 2126 4588 167 138 https TLSv1.3 PSK-RC4-SHA light beer 230
+localhost:83 203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.net HTTP/1.1" 101 1279 4755 490 108 http TLSv1.1 ECDHE-RSA-AES256-SHA dark beer 230
+test.example.org:83 203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.other HTTP/1.1" 301 1536 2798 241 305 http SSLv2 AES256-SHA light beer 230
+test.example.com:80 localhost - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.org HTTP/1.1" 400 2593 3461 118 347 https TLSv1 DHE-RSA-AES256-SHA light wine 230
+test.example.com:83 2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.com HTTP/2" 201 2867 3625 418 496 http SSLv2 PSK-RC4-SHA light wine 230
+198.51.100.1:81 203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.net HTTP/1.1" 401 4317 1085 443 410 http SSLv2 AES256-SHA dark wine 230
+2001:db8:1ce::1:82 2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.net HTTP/2.0" 301 1813 4623 250 246 http TLSv1 ECDHE-RSA-AES256-SHA light wine 230
+test.example.com:81 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.org HTTP/2.0" 301 4548 1008 387 9 https SSLv3 AES256-SHA light wine 230
+localhost:80 203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.com HTTP/2.0" 101 4678 4085 210 103 https TLSv1 AES256-SHA light beer 230
+test.example.com:82 2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.net HTTP/2" 401 4897 3938 74 116 http TLSv1.2 PSK-RC4-SHA light beer 230
+test.example.com:80 203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "POST /example.com HTTP/1.1" 200 3022 1961 203 393 http TLSv1.3 ECDHE-RSA-AES256-SHA dark beer 230
+2001:db8:1ce::1:83 203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.other HTTP/1.1" 301 1574 3104 364 165 https TLSv1.2 AES256-SHA light beer 230
+Unmatched! The rat the cat the dog chased killed ate the malt!
+test.example.com:82 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.other HTTP/1.1" 301 2944 3376 68 384 http TLSv1.3 PSK-RC4-SHA light wine 230
+localhost:84 2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.other HTTP/2" 101 4616 4363 17 28 https SSLv2 ECDHE-RSA-AES256-SHA light beer 230
+2001:db8:1ce::1:83 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.com HTTP/2.0" 200 2308 4193 20 257 http SSLv2 PSK-RC4-SHA dark wine 230
+test.example.org:81 203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.other HTTP/2" 300 3503 4056 336 375 https TLSv1.2 DHE-RSA-AES256-SHA dark beer 230
+localhost:82 203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "POST /example.com HTTP/2" 101 4109 2823 250 369 https TLSv1.1 ECDHE-RSA-AES256-SHA dark beer 230
+localhost:81 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.net HTTP/2.0" 300 2069 3457 174 159 http TLSv1.1 ECDHE-RSA-AES256-SHA dark beer 230
+localhost:80 localhost - - [22/Mar/2009:09:30:31 +0100] "GET /example.net HTTP/1.1" 101 2781 3947 414 406 https TLSv1.1 DHE-RSA-AES256-SHA dark beer 230
+198.51.100.1:84 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.net HTTP/1.1" 301 1390 1379 214 31 http TLSv1.3 DHE-RSA-AES256-SHA dark wine 230
+2001:db8:1ce::1:82 2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.com HTTP/1.1" 201 1546 1014 44 351 http TLSv1.1 AES256-SHA light beer 230
+2001:db8:1ce::1:81 2001:db8:2ce:1 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.org HTTP/2.0" 400 1600 4635 219 104 http TLSv1.1 DHE-RSA-AES256-SHA light beer 230
+2001:db8:1ce::1:80 203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.other HTTP/2.0" 201 3604 4845 378 237 http TLSv1.3 ECDHE-RSA-AES256-SHA dark beer 230
+test.example.org:84 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "HEAD /example.net HTTP/2" 400 1409 3810 180 163 https TLSv1.1 ECDHE-RSA-AES256-SHA dark wine 230
+2001:db8:1ce::1:84 2001:db8:2ce:2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.com HTTP/2" 201 1673 1858 43 405 https SSLv2 AES256-SHA light wine 230
+198.51.100.1:82 203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.net HTTP/1.1" 301 4846 3590 105 492 http SSLv2 PSK-RC4-SHA dark beer 230
+test.example.org:81 203.0.113.1 - - [22/Mar/2009:09:30:31 +0100] "GET /example.net HTTP/1.1" 100 4818 2058 362 393 http TLSv1.2 AES256-SHA dark beer 230
+Unmatched! The rat the cat the dog chased killed ate the malt!
+test.example.com:81 203.0.113.2 - - [22/Mar/2009:09:30:31 +0100] "GET /example.other HTTP/2.0" 101 4719 4878 382 257 https TLSv1.1 AES256-SHA light wine 230
diff --git a/src/go/collectors/go.d.plugin/modules/weblog/testdata/u_ex221107.log b/src/go/collectors/go.d.plugin/modules/weblog/testdata/u_ex221107.log
new file mode 100644
index 000000000..38fa91cdc
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/weblog/testdata/u_ex221107.log
@@ -0,0 +1,168 @@
+#Software: Microsoft Internet Information Services 10.0
+#Version: 1.0
+#Date: 2022-11-07 14:29:06
+#Fields: date time s-ip cs-method cs-uri-stem cs-uri-query s-port cs-username c-ip cs(User-Agent) cs(Referer) sc-status sc-substatus sc-win32-status time-taken
+2022-11-07 14:29:06 127.0.0.1 GET /us - 80 - 127.0.0.1 Go-http-client/1.1 - 404 0 2 130
+2022-11-07 14:29:06 127.0.0.1 GET /us - 80 - 127.0.0.1 Go-http-client/1.1 - 404 0 2 1
+2022-11-07 14:29:08 127.0.0.1 GET /status full&json 80 - 127.0.0.1 Go-http-client/1.1 - 404 0 2 0
+2022-11-07 14:29:08 127.0.0.1 GET /status full&json 80 - 127.0.0.1 Go-http-client/1.1 - 404 0 2 0
+2022-11-07 14:29:08 ::1 GET /status full&json 80 - ::1 Go-http-client/1.1 - 404 0 2 0
+2022-11-07 14:29:09 127.0.0.1 GET /server-status auto 80 - 127.0.0.1 Go-http-client/1.1 - 404 0 2 0
+2022-11-07 14:29:09 127.0.0.1 GET /server-status auto 80 - 127.0.0.1 Go-http-client/1.1 - 404 0 2 0
+#Software: Microsoft Internet Information Services 10.0
+#Version: 1.0
+#Date: 2022-11-07 14:55:17
+#Fields: date time s-ip cs-method cs-uri-stem cs-uri-query s-port cs-username c-ip cs(User-Agent) cs(Referer) sc-status sc-substatus sc-win32-status time-taken
+2022-11-07 14:55:17 127.0.0.1 GET /us - 80 - 127.0.0.1 Go-http-client/1.1 - 404 0 2 187
+2022-11-07 14:55:17 127.0.0.1 GET /us - 80 - 127.0.0.1 Go-http-client/1.1 - 404 0 2 0
+2022-11-07 14:55:17 127.0.0.1 GET /server-status format=plain 80 - 127.0.0.1 Go-http-client/1.1 - 404 0 2 0
+2022-11-07 14:55:17 127.0.0.1 GET /server-status format=plain 80 - 127.0.0.1 Go-http-client/1.1 - 404 0 2 1
+2022-11-07 14:55:18 127.0.0.1 GET /basic_status - 80 - 127.0.0.1 Go-http-client/1.1 - 404 0 2 1
+2022-11-07 14:55:18 127.0.0.1 GET /stub_status - 80 - 127.0.0.1 Go-http-client/1.1 - 404 0 2 0
+2022-11-07 14:55:18 127.0.0.1 GET /stub_status - 80 - 127.0.0.1 Go-http-client/1.1 - 404 0 2 1
+2022-11-07 14:55:18 127.0.0.1 GET /nginx_status - 80 - 127.0.0.1 Go-http-client/1.1 - 404 0 2 0
+2022-11-07 14:55:18 127.0.0.1 GET /status - 80 - 127.0.0.1 Go-http-client/1.1 - 404 0 2 0
+2022-11-07 14:55:18 127.0.0.1 GET /status/format/json - 80 - 127.0.0.1 Go-http-client/1.1 - 404 0 2 0
+2022-11-07 14:55:20 127.0.0.1 GET /admin/api.php version=true 80 - 127.0.0.1 Go-http-client/1.1 - 404 0 2 0
+2022-11-07 14:55:20 127.0.0.1 GET /admin/api.php version=true 80 - 127.0.0.1 Go-http-client/1.1 - 404 0 2 0
+2022-11-07 14:55:20 127.0.0.1 GET /server-status auto 80 - 127.0.0.1 Go-http-client/1.1 - 404 0 2 0
+2022-11-07 14:55:20 127.0.0.1 GET /server-status auto 80 - 127.0.0.1 Go-http-client/1.1 - 404 0 2 0
+2022-11-07 14:55:24 127.0.0.1 GET /status full&json 80 - 127.0.0.1 Go-http-client/1.1 - 404 0 2 0
+2022-11-07 14:55:24 127.0.0.1 GET /status full&json 80 - 127.0.0.1 Go-http-client/1.1 - 404 0 2 0
+2022-11-07 14:55:24 ::1 GET /status full&json 80 - ::1 Go-http-client/1.1 - 404 0 2 0
+#Software: Microsoft Internet Information Services 10.0
+#Version: 1.0
+#Date: 2022-11-07 15:42:39
+#Fields: date time s-ip cs-method cs-uri-stem cs-uri-query s-port cs-username c-ip cs(User-Agent) cs(Referer) sc-status sc-substatus sc-win32-status time-taken
+2022-11-07 15:42:39 127.0.0.1 GET /server-status format=plain 80 - 127.0.0.1 Go-http-client/1.1 - 404 0 2 149
+2022-11-07 15:42:39 127.0.0.1 GET /server-status format=plain 80 - 127.0.0.1 Go-http-client/1.1 - 404 0 2 0
+2022-11-07 15:42:39 127.0.0.1 GET /server-status auto 80 - 127.0.0.1 Go-http-client/1.1 - 404 0 2 0
+2022-11-07 15:42:39 127.0.0.1 GET /server-status auto 80 - 127.0.0.1 Go-http-client/1.1 - 404 0 2 0
+2022-11-07 15:42:39 127.0.0.1 GET /status/format/json - 80 - 127.0.0.1 Go-http-client/1.1 - 404 0 2 0
+2022-11-07 15:42:41 127.0.0.1 GET /basic_status - 80 - 127.0.0.1 Go-http-client/1.1 - 404 0 2 0
+2022-11-07 15:42:41 127.0.0.1 GET /stub_status - 80 - 127.0.0.1 Go-http-client/1.1 - 404 0 2 0
+2022-11-07 15:42:41 127.0.0.1 GET /stub_status - 80 - 127.0.0.1 Go-http-client/1.1 - 404 0 2 0
+2022-11-07 15:42:41 127.0.0.1 GET /nginx_status - 80 - 127.0.0.1 Go-http-client/1.1 - 404 0 2 0
+2022-11-07 15:42:41 127.0.0.1 GET /status - 80 - 127.0.0.1 Go-http-client/1.1 - 404 0 2 0
+2022-11-07 15:42:41 127.0.0.1 GET /admin/api.php version=true 80 - 127.0.0.1 Go-http-client/1.1 - 404 0 2 0
+2022-11-07 15:42:41 127.0.0.1 GET /admin/api.php version=true 80 - 127.0.0.1 Go-http-client/1.1 - 404 0 2 0
+2022-11-07 15:42:41 127.0.0.1 GET /us - 80 - 127.0.0.1 Go-http-client/1.1 - 404 0 2 0
+2022-11-07 15:42:41 127.0.0.1 GET /us - 80 - 127.0.0.1 Go-http-client/1.1 - 404 0 2 0
+2022-11-07 15:42:46 127.0.0.1 GET /status full&json 80 - 127.0.0.1 Go-http-client/1.1 - 404 0 2 0
+2022-11-07 15:42:46 127.0.0.1 GET /status full&json 80 - 127.0.0.1 Go-http-client/1.1 - 404 0 2 0
+2022-11-07 15:42:46 ::1 GET /status full&json 80 - ::1 Go-http-client/1.1 - 404 0 2 0
+#Software: Microsoft Internet Information Services 10.0
+#Version: 1.0
+#Date: 2022-11-07 16:47:25
+#Fields: date time s-ip cs-method cs-uri-stem cs-uri-query s-port cs-username c-ip cs(User-Agent) cs(Referer) sc-status sc-substatus sc-win32-status time-taken
+2022-11-07 16:47:25 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT+10.0;+Win64;+x64)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/107.0.0.0+Safari/537.36+Edg/107.0.1418.35 - 304 0 0 256
+2022-11-07 16:47:25 ::1 GET /iisstart.png - 80 - ::1 Mozilla/5.0+(Windows+NT+10.0;+Win64;+x64)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/107.0.0.0+Safari/537.36+Edg/107.0.1418.35 http://localhost/ 304 0 0 2
+2022-11-07 16:47:25 ::1 GET /favicon.ico - 80 - ::1 Mozilla/5.0+(Windows+NT+10.0;+Win64;+x64)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/107.0.0.0+Safari/537.36+Edg/107.0.1418.35 http://localhost/ 404 0 2 16
+2022-11-07 16:48:07 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT+10.0;+Win64;+x64)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/107.0.0.0+Safari/537.36+Edg/107.0.1418.35 - 304 0 0 0
+2022-11-07 16:48:08 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT+10.0;+Win64;+x64)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/107.0.0.0+Safari/537.36+Edg/107.0.1418.35 - 304 0 0 1
+2022-11-07 16:48:08 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT+10.0;+Win64;+x64)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/107.0.0.0+Safari/537.36+Edg/107.0.1418.35 - 304 0 0 0
+2022-11-07 16:48:08 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT+10.0;+Win64;+x64)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/107.0.0.0+Safari/537.36+Edg/107.0.1418.35 - 304 0 0 0
+2022-11-07 16:48:08 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT+10.0;+Win64;+x64)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/107.0.0.0+Safari/537.36+Edg/107.0.1418.35 - 304 0 0 0
+2022-11-07 16:48:08 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT+10.0;+Win64;+x64)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/107.0.0.0+Safari/537.36+Edg/107.0.1418.35 - 304 0 0 0
+2022-11-07 16:48:08 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT+10.0;+Win64;+x64)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/107.0.0.0+Safari/537.36+Edg/107.0.1418.35 - 304 0 0 0
+2022-11-07 16:48:09 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT+10.0;+Win64;+x64)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/107.0.0.0+Safari/537.36+Edg/107.0.1418.35 - 304 0 0 0
+2022-11-07 16:48:09 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT+10.0;+Win64;+x64)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/107.0.0.0+Safari/537.36+Edg/107.0.1418.35 - 304 0 0 0
+2022-11-07 16:49:05 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 0
+2022-11-07 16:49:05 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 0
+2022-11-07 16:49:06 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 0
+2022-11-07 16:49:06 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 0
+2022-11-07 16:49:06 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 3
+2022-11-07 16:49:06 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 0
+2022-11-07 16:49:06 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 0
+2022-11-07 16:49:06 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 0
+2022-11-07 16:49:07 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 0
+2022-11-07 16:49:07 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 0
+2022-11-07 16:49:07 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 0
+2022-11-07 16:49:07 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 0
+2022-11-07 16:49:07 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 0
+2022-11-07 16:49:07 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 0
+2022-11-07 16:49:09 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 0
+2022-11-07 16:49:09 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 0
+2022-11-07 16:49:09 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 0
+2022-11-07 16:49:09 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 0
+2022-11-07 16:49:09 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 0
+2022-11-07 16:49:09 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 0
+2022-11-07 16:49:10 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 0
+2022-11-07 16:49:10 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 0
+2022-11-07 16:49:10 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 0
+2022-11-07 16:49:10 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 0
+2022-11-07 16:49:10 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 4
+2022-11-07 16:49:10 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 0
+2022-11-07 16:49:11 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 0
+2022-11-07 16:49:11 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 0
+2022-11-07 16:49:11 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 0
+2022-11-07 16:49:11 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 0
+2022-11-07 16:49:11 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 0
+2022-11-07 16:49:11 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 0
+2022-11-07 16:49:12 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 0
+2022-11-07 16:49:12 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 0
+2022-11-07 16:49:12 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 0
+2022-11-07 16:49:12 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 0
+2022-11-07 16:49:12 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 0
+2022-11-07 16:49:12 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 0
+2022-11-07 16:49:13 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 0
+2022-11-07 16:49:13 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 0
+2022-11-07 16:49:13 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 2
+2022-11-07 16:49:13 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 0
+2022-11-07 16:49:13 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 0
+2022-11-07 16:49:13 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 0
+2022-11-07 16:49:14 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 0
+2022-11-07 16:49:14 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 0
+2022-11-07 16:49:14 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 0
+2022-11-07 16:49:14 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 2
+2022-11-07 16:49:14 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 0
+2022-11-07 16:49:14 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 0
+2022-11-07 16:49:15 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 0
+2022-11-07 16:49:15 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 0
+2022-11-07 16:49:15 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 0
+2022-11-07 16:49:15 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 0
+2022-11-07 16:49:15 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 0
+2022-11-07 16:49:15 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 0
+2022-11-07 16:49:16 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 0
+2022-11-07 16:49:16 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 0
+2022-11-07 16:49:16 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 0
+2022-11-07 16:49:16 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 0
+2022-11-07 16:49:16 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 0
+2022-11-07 16:49:16 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 0
+2022-11-07 16:49:17 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 0
+2022-11-07 16:49:17 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 0
+2022-11-07 16:49:17 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 2
+2022-11-07 16:49:17 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 0
+2022-11-07 16:49:17 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 0
+2022-11-07 16:49:17 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 0
+2022-11-07 16:49:18 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 0
+2022-11-07 16:49:18 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 2
+2022-11-07 16:49:18 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 0
+2022-11-07 16:49:18 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 31
+2022-11-07 16:49:18 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 0
+2022-11-07 16:49:18 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 0
+2022-11-07 16:49:19 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 8
+2022-11-07 16:49:19 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 0
+2022-11-07 16:49:19 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 0
+2022-11-07 16:49:19 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 0
+2022-11-07 16:49:19 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 0
+2022-11-07 16:49:19 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 0
+2022-11-07 16:49:20 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 0
+2022-11-07 16:49:20 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 0
+2022-11-07 16:49:20 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 0
+2022-11-07 16:49:20 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 0
+2022-11-07 16:49:20 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 0
+2022-11-07 16:49:20 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 0
+2022-11-07 16:49:21 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 0
+2022-11-07 16:49:21 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 0
+2022-11-07 16:49:21 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 0
+2022-11-07 16:49:21 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 0
+2022-11-07 16:49:21 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 0
+2022-11-07 16:49:21 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 0
+2022-11-07 16:49:23 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 0
+2022-11-07 16:49:23 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 0
+2022-11-07 16:49:23 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 0
+2022-11-07 16:49:23 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 0
+2022-11-07 16:49:23 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 0
+2022-11-07 16:49:23 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 0
+2022-11-07 16:49:24 ::1 GET / - 80 - ::1 Mozilla/5.0+(Windows+NT;+Windows+NT+10.0;+en-US)+WindowsPowerShell/5.1.20348.859 - 200 0 0 0
diff --git a/src/go/collectors/go.d.plugin/modules/weblog/weblog.go b/src/go/collectors/go.d.plugin/modules/weblog/weblog.go
new file mode 100644
index 000000000..09a07cc57
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/weblog/weblog.go
@@ -0,0 +1,168 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package weblog
+
+import (
+ _ "embed"
+
+ "github.com/netdata/netdata/go/go.d.plugin/agent/module"
+ "github.com/netdata/netdata/go/go.d.plugin/pkg/logs"
+)
+
+//go:embed "config_schema.json"
+var configSchema string
+
+func init() {
+ module.Register("web_log", module.Creator{
+ JobConfigSchema: configSchema,
+ Create: func() module.Module { return New() },
+ Config: func() any { return &Config{} },
+ })
+}
+
+func New() *WebLog {
+ return &WebLog{
+ Config: Config{
+ ExcludePath: "*.gz",
+ GroupRespCodes: true,
+ ParserConfig: logs.ParserConfig{
+ LogType: typeAuto,
+ CSV: logs.CSVConfig{
+ FieldsPerRecord: -1,
+ Delimiter: " ",
+ TrimLeadingSpace: false,
+ CheckField: checkCSVFormatField,
+ },
+ LTSV: logs.LTSVConfig{
+ FieldDelimiter: "\t",
+ ValueDelimiter: ":",
+ },
+ RegExp: logs.RegExpConfig{},
+ JSON: logs.JSONConfig{},
+ },
+ },
+ }
+}
+
+type (
+ Config struct {
+ UpdateEvery int `yaml:"update_every,omitempty" json:"update_every"`
+ Path string `yaml:"path" json:"path"`
+ ExcludePath string `yaml:"exclude_path,omitempty" json:"exclude_path"`
+ logs.ParserConfig `yaml:",inline" json:""`
+ URLPatterns []userPattern `yaml:"url_patterns,omitempty" json:"url_patterns"`
+ CustomFields []customField `yaml:"custom_fields,omitempty" json:"custom_fields"`
+ CustomTimeFields []customTimeField `yaml:"custom_time_fields,omitempty" json:"custom_time_fields"`
+ CustomNumericFields []customNumericField `yaml:"custom_numeric_fields,omitempty" json:"custom_numeric_fields"`
+ Histogram []float64 `yaml:"histogram,omitempty" json:"histogram"`
+ GroupRespCodes bool `yaml:"group_response_codes" json:"group_response_codes"`
+ }
+ userPattern struct {
+ Name string `yaml:"name" json:"name"`
+ Match string `yaml:"match" json:"match"`
+ }
+ customField struct {
+ Name string `yaml:"name" json:"name"`
+ Patterns []userPattern `yaml:"patterns" json:"patterns"`
+ }
+ customTimeField struct {
+ Name string `yaml:"name" json:"name"`
+ Histogram []float64 `yaml:"histogram" json:"histogram"`
+ }
+ customNumericField struct {
+ Name string `yaml:"name" json:"name"`
+ Units string `yaml:"units" json:"units"`
+ Multiplier int `yaml:"multiplier,omitempty" json:"multiplier"`
+ Divisor int `yaml:"divisor,omitempty" json:"divisor"`
+ }
+)
+
+type WebLog struct {
+ module.Base
+ Config `yaml:",inline" json:""`
+
+ charts *module.Charts
+
+ file *logs.Reader
+ parser logs.Parser
+ line *logLine
+
+ urlPatterns []*pattern
+ customFields map[string][]*pattern
+ customTimeFields map[string][]float64
+ customNumericFields map[string]bool
+
+ mx *metricsData
+}
+
+func (w *WebLog) Configuration() any {
+ return w.Config
+}
+
+func (w *WebLog) Init() error {
+ if err := w.createURLPatterns(); err != nil {
+ w.Errorf("init failed: %v", err)
+ return err
+ }
+
+ if err := w.createCustomFields(); err != nil {
+ w.Errorf("init failed: %v", err)
+ return err
+ }
+
+ if err := w.createCustomTimeFields(); err != nil {
+ w.Errorf("init failed: %v", err)
+ return err
+ }
+
+ if err := w.createCustomNumericFields(); err != nil {
+ w.Errorf("init failed: %v", err)
+ }
+
+ w.createLogLine()
+ w.mx = newMetricsData(w.Config)
+
+ return nil
+}
+
+func (w *WebLog) Check() error {
+ // Note: these inits are here to make auto-detection retry working
+ if err := w.createLogReader(); err != nil {
+ w.Warning("check failed: ", err)
+ return err
+ }
+
+ if err := w.createParser(); err != nil {
+ w.Warning("check failed: ", err)
+ return err
+ }
+
+ if err := w.createCharts(w.line); err != nil {
+ w.Warning("check failed: ", err)
+ return err
+ }
+
+ return nil
+}
+
+func (w *WebLog) Charts() *module.Charts {
+ return w.charts
+}
+
+func (w *WebLog) Collect() map[string]int64 {
+ mx, err := w.collect()
+ if err != nil {
+ w.Error(err)
+ }
+
+ if len(mx) == 0 {
+ return nil
+ }
+ return mx
+}
+
+func (w *WebLog) Cleanup() {
+ if w.file != nil {
+ _ = w.file.Close()
+ }
+}
diff --git a/src/go/collectors/go.d.plugin/modules/weblog/weblog_test.go b/src/go/collectors/go.d.plugin/modules/weblog/weblog_test.go
new file mode 100644
index 000000000..a756b6fb5
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/weblog/weblog_test.go
@@ -0,0 +1,1502 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package weblog
+
+import (
+ "bytes"
+ "fmt"
+ "os"
+ "reflect"
+ "strconv"
+ "strings"
+ "testing"
+
+ "github.com/netdata/netdata/go/go.d.plugin/agent/module"
+ "github.com/netdata/netdata/go/go.d.plugin/pkg/logs"
+ "github.com/netdata/netdata/go/go.d.plugin/pkg/metrics"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+var (
+ dataConfigJSON, _ = os.ReadFile("testdata/config.json")
+ dataConfigYAML, _ = os.ReadFile("testdata/config.yaml")
+
+ dataCommonLog, _ = os.ReadFile("testdata/common.log")
+ dataFullLog, _ = os.ReadFile("testdata/full.log")
+ dataCustomLog, _ = os.ReadFile("testdata/custom.log")
+ dataCustomTimeFieldLog, _ = os.ReadFile("testdata/custom_time_fields.log")
+ dataIISLog, _ = os.ReadFile("testdata/u_ex221107.log")
+)
+
+func Test_testDataIsValid(t *testing.T) {
+ for name, data := range map[string][]byte{
+ "dataConfigJSON": dataConfigJSON,
+ "dataConfigYAML": dataConfigYAML,
+ "dataCommonLog": dataCommonLog,
+ "dataFullLog": dataFullLog,
+ "dataCustomLog": dataCustomLog,
+ "dataCustomTimeFieldLog": dataCustomTimeFieldLog,
+ "dataIISLog": dataIISLog,
+ } {
+ require.NotNil(t, data, name)
+ }
+}
+
+func TestWebLog_ConfigurationSerialize(t *testing.T) {
+ module.TestConfigurationSerialize(t, &WebLog{}, dataConfigJSON, dataConfigYAML)
+}
+
+func TestWebLog_Init(t *testing.T) {
+ weblog := New()
+
+ assert.NoError(t, weblog.Init())
+}
+
+func TestWebLog_Init_ErrorOnCreatingURLPatterns(t *testing.T) {
+ weblog := New()
+ weblog.URLPatterns = []userPattern{{Match: "* !*"}}
+
+ assert.Error(t, weblog.Init())
+}
+
+func TestWebLog_Init_ErrorOnCreatingCustomFields(t *testing.T) {
+ weblog := New()
+ weblog.CustomFields = []customField{{Patterns: []userPattern{{Name: "p1", Match: "* !*"}}}}
+
+ assert.Error(t, weblog.Init())
+}
+
+func TestWebLog_Check(t *testing.T) {
+ weblog := New()
+ defer weblog.Cleanup()
+ weblog.Path = "testdata/common.log"
+ require.NoError(t, weblog.Init())
+
+ assert.NoError(t, weblog.Check())
+}
+
+func TestWebLog_Check_ErrorOnCreatingLogReaderNoLogFile(t *testing.T) {
+ weblog := New()
+ defer weblog.Cleanup()
+ weblog.Path = "testdata/not_exists.log"
+ require.NoError(t, weblog.Init())
+
+ assert.Error(t, weblog.Check())
+}
+
+func TestWebLog_Check_ErrorOnCreatingParserUnknownFormat(t *testing.T) {
+ weblog := New()
+ defer weblog.Cleanup()
+ weblog.Path = "testdata/custom.log"
+ require.NoError(t, weblog.Init())
+
+ assert.Error(t, weblog.Check())
+}
+
+func TestWebLog_Check_ErrorOnCreatingParserEmptyLine(t *testing.T) {
+ weblog := New()
+ defer weblog.Cleanup()
+ weblog.Path = "testdata/custom.log"
+ weblog.ParserConfig.LogType = logs.TypeCSV
+ weblog.ParserConfig.CSV.Format = "$one $two"
+ require.NoError(t, weblog.Init())
+
+ assert.Error(t, weblog.Check())
+}
+
+func TestWebLog_Charts(t *testing.T) {
+ weblog := New()
+ defer weblog.Cleanup()
+ weblog.Path = "testdata/common.log"
+ require.NoError(t, weblog.Init())
+ require.NoError(t, weblog.Check())
+
+ assert.NotNil(t, weblog.Charts())
+}
+
+func TestWebLog_Cleanup(t *testing.T) {
+ New().Cleanup()
+}
+
+func TestWebLog_Collect(t *testing.T) {
+ weblog := prepareWebLogCollectFull(t)
+
+ //m := weblog.Collect()
+ //l := make([]string, 0)
+ //for k := range m {
+ // l = append(l, k)
+ //}
+ //sort.Strings(l)
+ //for _, value := range l {
+ // fmt.Println(fmt.Sprintf("\"%s\": %d,", value, m[value]))
+ //}
+
+ expected := map[string]int64{
+ "bytes_received": 1374096,
+ "bytes_sent": 1373185,
+ "custom_field_drink_beer": 221,
+ "custom_field_drink_wine": 231,
+ "custom_field_side_dark": 231,
+ "custom_field_side_light": 221,
+ "custom_time_field_random_time_field_time_avg": 230,
+ "custom_time_field_random_time_field_time_count": 452,
+ "custom_time_field_random_time_field_time_hist_bucket_1": 452,
+ "custom_time_field_random_time_field_time_hist_bucket_10": 452,
+ "custom_time_field_random_time_field_time_hist_bucket_11": 452,
+ "custom_time_field_random_time_field_time_hist_bucket_2": 452,
+ "custom_time_field_random_time_field_time_hist_bucket_3": 452,
+ "custom_time_field_random_time_field_time_hist_bucket_4": 452,
+ "custom_time_field_random_time_field_time_hist_bucket_5": 452,
+ "custom_time_field_random_time_field_time_hist_bucket_6": 452,
+ "custom_time_field_random_time_field_time_hist_bucket_7": 452,
+ "custom_time_field_random_time_field_time_hist_bucket_8": 452,
+ "custom_time_field_random_time_field_time_hist_bucket_9": 452,
+ "custom_time_field_random_time_field_time_hist_count": 452,
+ "custom_time_field_random_time_field_time_hist_sum": 103960,
+ "custom_time_field_random_time_field_time_max": 230,
+ "custom_time_field_random_time_field_time_min": 230,
+ "custom_time_field_random_time_field_time_sum": 103960,
+ "req_http_scheme": 218,
+ "req_https_scheme": 234,
+ "req_ipv4": 275,
+ "req_ipv6": 177,
+ "req_method_GET": 156,
+ "req_method_HEAD": 150,
+ "req_method_POST": 146,
+ "req_port_80": 96,
+ "req_port_81": 100,
+ "req_port_82": 84,
+ "req_port_83": 85,
+ "req_port_84": 87,
+ "req_proc_time_avg": 244,
+ "req_proc_time_count": 402,
+ "req_proc_time_hist_bucket_1": 402,
+ "req_proc_time_hist_bucket_10": 402,
+ "req_proc_time_hist_bucket_11": 402,
+ "req_proc_time_hist_bucket_2": 402,
+ "req_proc_time_hist_bucket_3": 402,
+ "req_proc_time_hist_bucket_4": 402,
+ "req_proc_time_hist_bucket_5": 402,
+ "req_proc_time_hist_bucket_6": 402,
+ "req_proc_time_hist_bucket_7": 402,
+ "req_proc_time_hist_bucket_8": 402,
+ "req_proc_time_hist_bucket_9": 402,
+ "req_proc_time_hist_count": 402,
+ "req_proc_time_hist_sum": 98312,
+ "req_proc_time_max": 497,
+ "req_proc_time_min": 2,
+ "req_proc_time_sum": 98312,
+ "req_ssl_cipher_suite_AES256-SHA": 101,
+ "req_ssl_cipher_suite_DHE-RSA-AES256-SHA": 111,
+ "req_ssl_cipher_suite_ECDHE-RSA-AES256-SHA": 127,
+ "req_ssl_cipher_suite_PSK-RC4-SHA": 113,
+ "req_ssl_proto_SSLv2": 74,
+ "req_ssl_proto_SSLv3": 57,
+ "req_ssl_proto_TLSv1": 76,
+ "req_ssl_proto_TLSv1.1": 87,
+ "req_ssl_proto_TLSv1.2": 73,
+ "req_ssl_proto_TLSv1.3": 85,
+ "req_type_bad": 49,
+ "req_type_error": 0,
+ "req_type_redirect": 119,
+ "req_type_success": 284,
+ "req_unmatched": 48,
+ "req_url_ptn_com": 120,
+ "req_url_ptn_net": 116,
+ "req_url_ptn_not_match": 0,
+ "req_url_ptn_org": 113,
+ "req_version_1.1": 168,
+ "req_version_2": 143,
+ "req_version_2.0": 141,
+ "req_vhost_198.51.100.1": 81,
+ "req_vhost_2001:db8:1ce::1": 100,
+ "req_vhost_localhost": 102,
+ "req_vhost_test.example.com": 87,
+ "req_vhost_test.example.org": 82,
+ "requests": 500,
+ "resp_1xx": 110,
+ "resp_2xx": 128,
+ "resp_3xx": 119,
+ "resp_4xx": 95,
+ "resp_5xx": 0,
+ "resp_code_100": 60,
+ "resp_code_101": 50,
+ "resp_code_200": 58,
+ "resp_code_201": 70,
+ "resp_code_300": 58,
+ "resp_code_301": 61,
+ "resp_code_400": 49,
+ "resp_code_401": 46,
+ "uniq_ipv4": 3,
+ "uniq_ipv6": 2,
+ "upstream_resp_time_avg": 255,
+ "upstream_resp_time_count": 452,
+ "upstream_resp_time_hist_bucket_1": 452,
+ "upstream_resp_time_hist_bucket_10": 452,
+ "upstream_resp_time_hist_bucket_11": 452,
+ "upstream_resp_time_hist_bucket_2": 452,
+ "upstream_resp_time_hist_bucket_3": 452,
+ "upstream_resp_time_hist_bucket_4": 452,
+ "upstream_resp_time_hist_bucket_5": 452,
+ "upstream_resp_time_hist_bucket_6": 452,
+ "upstream_resp_time_hist_bucket_7": 452,
+ "upstream_resp_time_hist_bucket_8": 452,
+ "upstream_resp_time_hist_bucket_9": 452,
+ "upstream_resp_time_hist_count": 452,
+ "upstream_resp_time_hist_sum": 115615,
+ "upstream_resp_time_max": 497,
+ "upstream_resp_time_min": 7,
+ "upstream_resp_time_sum": 115615,
+ "url_ptn_com_bytes_received": 379864,
+ "url_ptn_com_bytes_sent": 372669,
+ "url_ptn_com_req_method_GET": 38,
+ "url_ptn_com_req_method_HEAD": 39,
+ "url_ptn_com_req_method_POST": 43,
+ "url_ptn_com_req_proc_time_avg": 209,
+ "url_ptn_com_req_proc_time_count": 105,
+ "url_ptn_com_req_proc_time_max": 495,
+ "url_ptn_com_req_proc_time_min": 5,
+ "url_ptn_com_req_proc_time_sum": 22010,
+ "url_ptn_com_resp_code_100": 12,
+ "url_ptn_com_resp_code_101": 15,
+ "url_ptn_com_resp_code_200": 13,
+ "url_ptn_com_resp_code_201": 26,
+ "url_ptn_com_resp_code_300": 16,
+ "url_ptn_com_resp_code_301": 12,
+ "url_ptn_com_resp_code_400": 13,
+ "url_ptn_com_resp_code_401": 13,
+ "url_ptn_net_bytes_received": 349988,
+ "url_ptn_net_bytes_sent": 339867,
+ "url_ptn_net_req_method_GET": 51,
+ "url_ptn_net_req_method_HEAD": 33,
+ "url_ptn_net_req_method_POST": 32,
+ "url_ptn_net_req_proc_time_avg": 254,
+ "url_ptn_net_req_proc_time_count": 104,
+ "url_ptn_net_req_proc_time_max": 497,
+ "url_ptn_net_req_proc_time_min": 10,
+ "url_ptn_net_req_proc_time_sum": 26510,
+ "url_ptn_net_resp_code_100": 16,
+ "url_ptn_net_resp_code_101": 12,
+ "url_ptn_net_resp_code_200": 16,
+ "url_ptn_net_resp_code_201": 14,
+ "url_ptn_net_resp_code_300": 14,
+ "url_ptn_net_resp_code_301": 17,
+ "url_ptn_net_resp_code_400": 14,
+ "url_ptn_net_resp_code_401": 13,
+ "url_ptn_not_match_bytes_received": 0,
+ "url_ptn_not_match_bytes_sent": 0,
+ "url_ptn_not_match_req_proc_time_avg": 0,
+ "url_ptn_not_match_req_proc_time_count": 0,
+ "url_ptn_not_match_req_proc_time_max": 0,
+ "url_ptn_not_match_req_proc_time_min": 0,
+ "url_ptn_not_match_req_proc_time_sum": 0,
+ "url_ptn_org_bytes_received": 331836,
+ "url_ptn_org_bytes_sent": 340095,
+ "url_ptn_org_req_method_GET": 29,
+ "url_ptn_org_req_method_HEAD": 46,
+ "url_ptn_org_req_method_POST": 38,
+ "url_ptn_org_req_proc_time_avg": 260,
+ "url_ptn_org_req_proc_time_count": 102,
+ "url_ptn_org_req_proc_time_max": 497,
+ "url_ptn_org_req_proc_time_min": 2,
+ "url_ptn_org_req_proc_time_sum": 26599,
+ "url_ptn_org_resp_code_100": 15,
+ "url_ptn_org_resp_code_101": 11,
+ "url_ptn_org_resp_code_200": 20,
+ "url_ptn_org_resp_code_201": 16,
+ "url_ptn_org_resp_code_300": 10,
+ "url_ptn_org_resp_code_301": 19,
+ "url_ptn_org_resp_code_400": 13,
+ "url_ptn_org_resp_code_401": 9,
+ }
+
+ mx := weblog.Collect()
+ assert.Equal(t, expected, mx)
+ testCharts(t, weblog, mx)
+}
+
+func TestWebLog_Collect_CommonLogFormat(t *testing.T) {
+ weblog := prepareWebLogCollectCommon(t)
+
+ expected := map[string]int64{
+ "bytes_received": 0,
+ "bytes_sent": 1388056,
+ "req_http_scheme": 0,
+ "req_https_scheme": 0,
+ "req_ipv4": 283,
+ "req_ipv6": 173,
+ "req_method_GET": 159,
+ "req_method_HEAD": 143,
+ "req_method_POST": 154,
+ "req_proc_time_avg": 0,
+ "req_proc_time_count": 0,
+ "req_proc_time_hist_bucket_1": 0,
+ "req_proc_time_hist_bucket_10": 0,
+ "req_proc_time_hist_bucket_11": 0,
+ "req_proc_time_hist_bucket_2": 0,
+ "req_proc_time_hist_bucket_3": 0,
+ "req_proc_time_hist_bucket_4": 0,
+ "req_proc_time_hist_bucket_5": 0,
+ "req_proc_time_hist_bucket_6": 0,
+ "req_proc_time_hist_bucket_7": 0,
+ "req_proc_time_hist_bucket_8": 0,
+ "req_proc_time_hist_bucket_9": 0,
+ "req_proc_time_hist_count": 0,
+ "req_proc_time_hist_sum": 0,
+ "req_proc_time_max": 0,
+ "req_proc_time_min": 0,
+ "req_proc_time_sum": 0,
+ "req_type_bad": 54,
+ "req_type_error": 0,
+ "req_type_redirect": 122,
+ "req_type_success": 280,
+ "req_unmatched": 44,
+ "req_version_1.1": 155,
+ "req_version_2": 147,
+ "req_version_2.0": 154,
+ "requests": 500,
+ "resp_1xx": 130,
+ "resp_2xx": 100,
+ "resp_3xx": 122,
+ "resp_4xx": 104,
+ "resp_5xx": 0,
+ "resp_code_100": 80,
+ "resp_code_101": 50,
+ "resp_code_200": 43,
+ "resp_code_201": 57,
+ "resp_code_300": 70,
+ "resp_code_301": 52,
+ "resp_code_400": 54,
+ "resp_code_401": 50,
+ "uniq_ipv4": 3,
+ "uniq_ipv6": 2,
+ "upstream_resp_time_avg": 0,
+ "upstream_resp_time_count": 0,
+ "upstream_resp_time_hist_bucket_1": 0,
+ "upstream_resp_time_hist_bucket_10": 0,
+ "upstream_resp_time_hist_bucket_11": 0,
+ "upstream_resp_time_hist_bucket_2": 0,
+ "upstream_resp_time_hist_bucket_3": 0,
+ "upstream_resp_time_hist_bucket_4": 0,
+ "upstream_resp_time_hist_bucket_5": 0,
+ "upstream_resp_time_hist_bucket_6": 0,
+ "upstream_resp_time_hist_bucket_7": 0,
+ "upstream_resp_time_hist_bucket_8": 0,
+ "upstream_resp_time_hist_bucket_9": 0,
+ "upstream_resp_time_hist_count": 0,
+ "upstream_resp_time_hist_sum": 0,
+ "upstream_resp_time_max": 0,
+ "upstream_resp_time_min": 0,
+ "upstream_resp_time_sum": 0,
+ }
+
+ mx := weblog.Collect()
+ assert.Equal(t, expected, mx)
+ testCharts(t, weblog, mx)
+}
+
+func TestWebLog_Collect_CustomLogs(t *testing.T) {
+ weblog := prepareWebLogCollectCustom(t)
+
+ expected := map[string]int64{
+ "bytes_received": 0,
+ "bytes_sent": 0,
+ "custom_field_drink_beer": 52,
+ "custom_field_drink_wine": 40,
+ "custom_field_side_dark": 46,
+ "custom_field_side_light": 46,
+ "req_http_scheme": 0,
+ "req_https_scheme": 0,
+ "req_ipv4": 0,
+ "req_ipv6": 0,
+ "req_proc_time_avg": 0,
+ "req_proc_time_count": 0,
+ "req_proc_time_hist_bucket_1": 0,
+ "req_proc_time_hist_bucket_10": 0,
+ "req_proc_time_hist_bucket_11": 0,
+ "req_proc_time_hist_bucket_2": 0,
+ "req_proc_time_hist_bucket_3": 0,
+ "req_proc_time_hist_bucket_4": 0,
+ "req_proc_time_hist_bucket_5": 0,
+ "req_proc_time_hist_bucket_6": 0,
+ "req_proc_time_hist_bucket_7": 0,
+ "req_proc_time_hist_bucket_8": 0,
+ "req_proc_time_hist_bucket_9": 0,
+ "req_proc_time_hist_count": 0,
+ "req_proc_time_hist_sum": 0,
+ "req_proc_time_max": 0,
+ "req_proc_time_min": 0,
+ "req_proc_time_sum": 0,
+ "req_type_bad": 0,
+ "req_type_error": 0,
+ "req_type_redirect": 0,
+ "req_type_success": 0,
+ "req_unmatched": 8,
+ "requests": 100,
+ "resp_1xx": 0,
+ "resp_2xx": 0,
+ "resp_3xx": 0,
+ "resp_4xx": 0,
+ "resp_5xx": 0,
+ "uniq_ipv4": 0,
+ "uniq_ipv6": 0,
+ "upstream_resp_time_avg": 0,
+ "upstream_resp_time_count": 0,
+ "upstream_resp_time_hist_bucket_1": 0,
+ "upstream_resp_time_hist_bucket_10": 0,
+ "upstream_resp_time_hist_bucket_11": 0,
+ "upstream_resp_time_hist_bucket_2": 0,
+ "upstream_resp_time_hist_bucket_3": 0,
+ "upstream_resp_time_hist_bucket_4": 0,
+ "upstream_resp_time_hist_bucket_5": 0,
+ "upstream_resp_time_hist_bucket_6": 0,
+ "upstream_resp_time_hist_bucket_7": 0,
+ "upstream_resp_time_hist_bucket_8": 0,
+ "upstream_resp_time_hist_bucket_9": 0,
+ "upstream_resp_time_hist_count": 0,
+ "upstream_resp_time_hist_sum": 0,
+ "upstream_resp_time_max": 0,
+ "upstream_resp_time_min": 0,
+ "upstream_resp_time_sum": 0,
+ }
+
+ mx := weblog.Collect()
+ assert.Equal(t, expected, mx)
+ testCharts(t, weblog, mx)
+}
+
+func TestWebLog_Collect_CustomTimeFieldsLogs(t *testing.T) {
+ weblog := prepareWebLogCollectCustomTimeFields(t)
+
+ expected := map[string]int64{
+ "bytes_received": 0,
+ "bytes_sent": 0,
+ "custom_time_field_time1_time_avg": 224,
+ "custom_time_field_time1_time_count": 72,
+ "custom_time_field_time1_time_hist_bucket_1": 72,
+ "custom_time_field_time1_time_hist_bucket_10": 72,
+ "custom_time_field_time1_time_hist_bucket_11": 72,
+ "custom_time_field_time1_time_hist_bucket_2": 72,
+ "custom_time_field_time1_time_hist_bucket_3": 72,
+ "custom_time_field_time1_time_hist_bucket_4": 72,
+ "custom_time_field_time1_time_hist_bucket_5": 72,
+ "custom_time_field_time1_time_hist_bucket_6": 72,
+ "custom_time_field_time1_time_hist_bucket_7": 72,
+ "custom_time_field_time1_time_hist_bucket_8": 72,
+ "custom_time_field_time1_time_hist_bucket_9": 72,
+ "custom_time_field_time1_time_hist_count": 72,
+ "custom_time_field_time1_time_hist_sum": 16152,
+ "custom_time_field_time1_time_max": 431,
+ "custom_time_field_time1_time_min": 121,
+ "custom_time_field_time1_time_sum": 16152,
+ "custom_time_field_time2_time_avg": 255,
+ "custom_time_field_time2_time_count": 72,
+ "custom_time_field_time2_time_hist_bucket_1": 72,
+ "custom_time_field_time2_time_hist_bucket_10": 72,
+ "custom_time_field_time2_time_hist_bucket_11": 72,
+ "custom_time_field_time2_time_hist_bucket_2": 72,
+ "custom_time_field_time2_time_hist_bucket_3": 72,
+ "custom_time_field_time2_time_hist_bucket_4": 72,
+ "custom_time_field_time2_time_hist_bucket_5": 72,
+ "custom_time_field_time2_time_hist_bucket_6": 72,
+ "custom_time_field_time2_time_hist_bucket_7": 72,
+ "custom_time_field_time2_time_hist_bucket_8": 72,
+ "custom_time_field_time2_time_hist_bucket_9": 72,
+ "custom_time_field_time2_time_hist_count": 72,
+ "custom_time_field_time2_time_hist_sum": 18360,
+ "custom_time_field_time2_time_max": 321,
+ "custom_time_field_time2_time_min": 123,
+ "custom_time_field_time2_time_sum": 18360,
+ "req_http_scheme": 0,
+ "req_https_scheme": 0,
+ "req_ipv4": 0,
+ "req_ipv6": 0,
+ "req_proc_time_avg": 0,
+ "req_proc_time_count": 0,
+ "req_proc_time_hist_bucket_1": 0,
+ "req_proc_time_hist_bucket_10": 0,
+ "req_proc_time_hist_bucket_11": 0,
+ "req_proc_time_hist_bucket_2": 0,
+ "req_proc_time_hist_bucket_3": 0,
+ "req_proc_time_hist_bucket_4": 0,
+ "req_proc_time_hist_bucket_5": 0,
+ "req_proc_time_hist_bucket_6": 0,
+ "req_proc_time_hist_bucket_7": 0,
+ "req_proc_time_hist_bucket_8": 0,
+ "req_proc_time_hist_bucket_9": 0,
+ "req_proc_time_hist_count": 0,
+ "req_proc_time_hist_sum": 0,
+ "req_proc_time_max": 0,
+ "req_proc_time_min": 0,
+ "req_proc_time_sum": 0,
+ "req_type_bad": 0,
+ "req_type_error": 0,
+ "req_type_redirect": 0,
+ "req_type_success": 0,
+ "req_unmatched": 0,
+ "requests": 72,
+ "resp_1xx": 0,
+ "resp_2xx": 0,
+ "resp_3xx": 0,
+ "resp_4xx": 0,
+ "resp_5xx": 0,
+ "uniq_ipv4": 0,
+ "uniq_ipv6": 0,
+ "upstream_resp_time_avg": 0,
+ "upstream_resp_time_count": 0,
+ "upstream_resp_time_hist_bucket_1": 0,
+ "upstream_resp_time_hist_bucket_10": 0,
+ "upstream_resp_time_hist_bucket_11": 0,
+ "upstream_resp_time_hist_bucket_2": 0,
+ "upstream_resp_time_hist_bucket_3": 0,
+ "upstream_resp_time_hist_bucket_4": 0,
+ "upstream_resp_time_hist_bucket_5": 0,
+ "upstream_resp_time_hist_bucket_6": 0,
+ "upstream_resp_time_hist_bucket_7": 0,
+ "upstream_resp_time_hist_bucket_8": 0,
+ "upstream_resp_time_hist_bucket_9": 0,
+ "upstream_resp_time_hist_count": 0,
+ "upstream_resp_time_hist_sum": 0,
+ "upstream_resp_time_max": 0,
+ "upstream_resp_time_min": 0,
+ "upstream_resp_time_sum": 0,
+ }
+
+ mx := weblog.Collect()
+ assert.Equal(t, expected, mx)
+ testCharts(t, weblog, mx)
+}
+
+func TestWebLog_Collect_CustomNumericFieldsLogs(t *testing.T) {
+ weblog := prepareWebLogCollectCustomNumericFields(t)
+
+ expected := map[string]int64{
+ "bytes_received": 0,
+ "bytes_sent": 0,
+ "custom_numeric_field_numeric1_summary_avg": 224,
+ "custom_numeric_field_numeric1_summary_count": 72,
+ "custom_numeric_field_numeric1_summary_max": 431,
+ "custom_numeric_field_numeric1_summary_min": 121,
+ "custom_numeric_field_numeric1_summary_sum": 16152,
+ "custom_numeric_field_numeric2_summary_avg": 255,
+ "custom_numeric_field_numeric2_summary_count": 72,
+ "custom_numeric_field_numeric2_summary_max": 321,
+ "custom_numeric_field_numeric2_summary_min": 123,
+ "custom_numeric_field_numeric2_summary_sum": 18360,
+ "req_http_scheme": 0,
+ "req_https_scheme": 0,
+ "req_ipv4": 0,
+ "req_ipv6": 0,
+ "req_proc_time_avg": 0,
+ "req_proc_time_count": 0,
+ "req_proc_time_hist_bucket_1": 0,
+ "req_proc_time_hist_bucket_10": 0,
+ "req_proc_time_hist_bucket_11": 0,
+ "req_proc_time_hist_bucket_2": 0,
+ "req_proc_time_hist_bucket_3": 0,
+ "req_proc_time_hist_bucket_4": 0,
+ "req_proc_time_hist_bucket_5": 0,
+ "req_proc_time_hist_bucket_6": 0,
+ "req_proc_time_hist_bucket_7": 0,
+ "req_proc_time_hist_bucket_8": 0,
+ "req_proc_time_hist_bucket_9": 0,
+ "req_proc_time_hist_count": 0,
+ "req_proc_time_hist_sum": 0,
+ "req_proc_time_max": 0,
+ "req_proc_time_min": 0,
+ "req_proc_time_sum": 0,
+ "req_type_bad": 0,
+ "req_type_error": 0,
+ "req_type_redirect": 0,
+ "req_type_success": 0,
+ "req_unmatched": 0,
+ "requests": 72,
+ "resp_1xx": 0,
+ "resp_2xx": 0,
+ "resp_3xx": 0,
+ "resp_4xx": 0,
+ "resp_5xx": 0,
+ "uniq_ipv4": 0,
+ "uniq_ipv6": 0,
+ "upstream_resp_time_avg": 0,
+ "upstream_resp_time_count": 0,
+ "upstream_resp_time_hist_bucket_1": 0,
+ "upstream_resp_time_hist_bucket_10": 0,
+ "upstream_resp_time_hist_bucket_11": 0,
+ "upstream_resp_time_hist_bucket_2": 0,
+ "upstream_resp_time_hist_bucket_3": 0,
+ "upstream_resp_time_hist_bucket_4": 0,
+ "upstream_resp_time_hist_bucket_5": 0,
+ "upstream_resp_time_hist_bucket_6": 0,
+ "upstream_resp_time_hist_bucket_7": 0,
+ "upstream_resp_time_hist_bucket_8": 0,
+ "upstream_resp_time_hist_bucket_9": 0,
+ "upstream_resp_time_hist_count": 0,
+ "upstream_resp_time_hist_sum": 0,
+ "upstream_resp_time_max": 0,
+ "upstream_resp_time_min": 0,
+ "upstream_resp_time_sum": 0,
+ }
+
+ mx := weblog.Collect()
+
+ assert.Equal(t, expected, mx)
+ testCharts(t, weblog, mx)
+}
+
+func TestWebLog_IISLogs(t *testing.T) {
+ weblog := prepareWebLogCollectIISFields(t)
+
+ expected := map[string]int64{
+ "bytes_received": 0,
+ "bytes_sent": 0,
+ "req_http_scheme": 0,
+ "req_https_scheme": 0,
+ "req_ipv4": 38,
+ "req_ipv6": 114,
+ "req_method_GET": 152,
+ "req_port_80": 152,
+ "req_proc_time_avg": 5,
+ "req_proc_time_count": 152,
+ "req_proc_time_hist_bucket_1": 133,
+ "req_proc_time_hist_bucket_10": 145,
+ "req_proc_time_hist_bucket_11": 146,
+ "req_proc_time_hist_bucket_2": 133,
+ "req_proc_time_hist_bucket_3": 133,
+ "req_proc_time_hist_bucket_4": 133,
+ "req_proc_time_hist_bucket_5": 133,
+ "req_proc_time_hist_bucket_6": 133,
+ "req_proc_time_hist_bucket_7": 133,
+ "req_proc_time_hist_bucket_8": 138,
+ "req_proc_time_hist_bucket_9": 143,
+ "req_proc_time_hist_count": 152,
+ "req_proc_time_hist_sum": 799,
+ "req_proc_time_max": 256,
+ "req_proc_time_min": 0,
+ "req_proc_time_sum": 799,
+ "req_type_bad": 42,
+ "req_type_error": 0,
+ "req_type_redirect": 0,
+ "req_type_success": 110,
+ "req_unmatched": 16,
+ "req_vhost_127.0.0.1": 38,
+ "req_vhost_::1": 114,
+ "requests": 168,
+ "resp_1xx": 0,
+ "resp_2xx": 99,
+ "resp_3xx": 11,
+ "resp_4xx": 42,
+ "resp_5xx": 0,
+ "resp_code_200": 99,
+ "resp_code_304": 11,
+ "resp_code_404": 42,
+ "uniq_ipv4": 1,
+ "uniq_ipv6": 1,
+ "upstream_resp_time_avg": 0,
+ "upstream_resp_time_count": 0,
+ "upstream_resp_time_hist_bucket_1": 0,
+ "upstream_resp_time_hist_bucket_10": 0,
+ "upstream_resp_time_hist_bucket_11": 0,
+ "upstream_resp_time_hist_bucket_2": 0,
+ "upstream_resp_time_hist_bucket_3": 0,
+ "upstream_resp_time_hist_bucket_4": 0,
+ "upstream_resp_time_hist_bucket_5": 0,
+ "upstream_resp_time_hist_bucket_6": 0,
+ "upstream_resp_time_hist_bucket_7": 0,
+ "upstream_resp_time_hist_bucket_8": 0,
+ "upstream_resp_time_hist_bucket_9": 0,
+ "upstream_resp_time_hist_count": 0,
+ "upstream_resp_time_hist_sum": 0,
+ "upstream_resp_time_max": 0,
+ "upstream_resp_time_min": 0,
+ "upstream_resp_time_sum": 0,
+ }
+
+ mx := weblog.Collect()
+ assert.Equal(t, expected, mx)
+}
+
+func testCharts(t *testing.T, w *WebLog, mx map[string]int64) {
+ testVhostChart(t, w)
+ testPortChart(t, w)
+ testSchemeChart(t, w)
+ testClientCharts(t, w)
+ testHTTPMethodChart(t, w)
+ testURLPatternChart(t, w)
+ testHTTPVersionChart(t, w)
+ testRespCodeCharts(t, w)
+ testBandwidthChart(t, w)
+ testReqProcTimeCharts(t, w)
+ testUpsRespTimeCharts(t, w)
+ testSSLProtoChart(t, w)
+ testSSLCipherSuiteChart(t, w)
+ testURLPatternStatsCharts(t, w)
+ testCustomFieldCharts(t, w)
+ testCustomTimeFieldCharts(t, w)
+ testCustomNumericFieldCharts(t, w)
+
+ testChartsDimIDs(t, w, mx)
+}
+
+func testChartsDimIDs(t *testing.T, w *WebLog, mx map[string]int64) {
+ for _, chart := range *w.Charts() {
+ for _, dim := range chart.Dims {
+ _, ok := mx[dim.ID]
+ assert.Truef(t, ok, "collected metrics has no data for dim '%s' chart '%s'", dim.ID, chart.ID)
+ }
+ }
+}
+
+func testVhostChart(t *testing.T, w *WebLog) {
+ if len(w.mx.ReqVhost) == 0 {
+ assert.Falsef(t, w.Charts().Has(reqByVhost.ID), "chart '%s' is created", reqByVhost.ID)
+ return
+ }
+
+ chart := w.Charts().Get(reqByVhost.ID)
+ assert.NotNilf(t, chart, "chart '%s' is not created", reqByVhost.ID)
+ if chart == nil {
+ return
+ }
+ for v := range w.mx.ReqVhost {
+ id := "req_vhost_" + v
+ assert.Truef(t, chart.HasDim(id), "chart '%s' has no dim for '%s' vhost, expected '%s'", chart.ID, v, id)
+ }
+}
+
+func testPortChart(t *testing.T, w *WebLog) {
+ if len(w.mx.ReqPort) == 0 {
+ assert.Falsef(t, w.Charts().Has(reqByPort.ID), "chart '%s' is created", reqByPort.ID)
+ return
+ }
+
+ chart := w.Charts().Get(reqByPort.ID)
+ assert.NotNilf(t, chart, "chart '%s' is not created", reqByPort.ID)
+ if chart == nil {
+ return
+ }
+ for v := range w.mx.ReqPort {
+ id := "req_port_" + v
+ assert.Truef(t, chart.HasDim(id), "chart '%s' has no dim for '%s' port, expected '%s'", chart.ID, v, id)
+ }
+}
+
+func testSchemeChart(t *testing.T, w *WebLog) {
+ if w.mx.ReqHTTPScheme.Value() == 0 && w.mx.ReqHTTPSScheme.Value() == 0 {
+ assert.Falsef(t, w.Charts().Has(reqByScheme.ID), "chart '%s' is created", reqByScheme.ID)
+ } else {
+ assert.Truef(t, w.Charts().Has(reqByScheme.ID), "chart '%s' is not created", reqByScheme.ID)
+ }
+}
+
+func testClientCharts(t *testing.T, w *WebLog) {
+ if w.mx.ReqIPv4.Value() == 0 && w.mx.ReqIPv6.Value() == 0 {
+ assert.Falsef(t, w.Charts().Has(reqByIPProto.ID), "chart '%s' is created", reqByIPProto.ID)
+ } else {
+ assert.Truef(t, w.Charts().Has(reqByIPProto.ID), "chart '%s' is not created", reqByIPProto.ID)
+ }
+
+ if w.mx.UniqueIPv4.Value() == 0 && w.mx.UniqueIPv6.Value() == 0 {
+ assert.Falsef(t, w.Charts().Has(uniqIPsCurPoll.ID), "chart '%s' is created", uniqIPsCurPoll.ID)
+ } else {
+ assert.Truef(t, w.Charts().Has(uniqIPsCurPoll.ID), "chart '%s' is not created", uniqIPsCurPoll.ID)
+ }
+}
+
+func testHTTPMethodChart(t *testing.T, w *WebLog) {
+ if len(w.mx.ReqMethod) == 0 {
+ assert.Falsef(t, w.Charts().Has(reqByMethod.ID), "chart '%s' is created", reqByMethod.ID)
+ return
+ }
+
+ chart := w.Charts().Get(reqByMethod.ID)
+ assert.NotNilf(t, chart, "chart '%s' is not created", reqByMethod.ID)
+ if chart == nil {
+ return
+ }
+ for v := range w.mx.ReqMethod {
+ id := "req_method_" + v
+ assert.Truef(t, chart.HasDim(id), "chart '%s' has no dim for '%s' method, expected '%s'", chart.ID, v, id)
+ }
+}
+
+func testURLPatternChart(t *testing.T, w *WebLog) {
+ if isEmptyCounterVec(w.mx.ReqURLPattern) {
+ assert.Falsef(t, w.Charts().Has(reqByURLPattern.ID), "chart '%s' is created", reqByURLPattern.ID)
+ return
+ }
+
+ chart := w.Charts().Get(reqByURLPattern.ID)
+ assert.NotNilf(t, chart, "chart '%s' is not created", reqByURLPattern.ID)
+ if chart == nil {
+ return
+ }
+ for v := range w.mx.ReqURLPattern {
+ id := "req_url_ptn_" + v
+ assert.True(t, chart.HasDim(id), "chart '%s' has no dim for '%s' pattern, expected '%s'", chart.ID, v, id)
+ }
+}
+
+func testHTTPVersionChart(t *testing.T, w *WebLog) {
+ if len(w.mx.ReqVersion) == 0 {
+ assert.Falsef(t, w.Charts().Has(reqByVersion.ID), "chart '%s' is created", reqByVersion.ID)
+ return
+ }
+
+ chart := w.Charts().Get(reqByVersion.ID)
+ assert.NotNilf(t, chart, "chart '%s' is not created", reqByVersion.ID)
+ if chart == nil {
+ return
+ }
+ for v := range w.mx.ReqVersion {
+ id := "req_version_" + v
+ assert.Truef(t, chart.HasDim(id), "chart '%s' has no dim for '%s' version, expected '%s'", chart.ID, v, id)
+ }
+}
+
+func testRespCodeCharts(t *testing.T, w *WebLog) {
+ if isEmptyCounterVec(w.mx.RespCode) {
+ for _, id := range []string{
+ respCodes.ID,
+ respCodes1xx.ID,
+ respCodes2xx.ID,
+ respCodes3xx.ID,
+ respCodes4xx.ID,
+ respCodes5xx.ID,
+ } {
+ assert.Falsef(t, w.Charts().Has(id), "chart '%s' is created", id)
+ }
+ return
+ }
+
+ if !w.GroupRespCodes {
+ chart := w.Charts().Get(respCodes.ID)
+ assert.NotNilf(t, chart, "chart '%s' is not created", respCodes.ID)
+ if chart == nil {
+ return
+ }
+ for v := range w.mx.RespCode {
+ id := "resp_code_" + v
+ assert.Truef(t, chart.HasDim(id), "chart '%s' has no dim for '%s' code, expected '%s'", chart.ID, v, id)
+ }
+ return
+ }
+
+ findCodes := func(class string) (codes []string) {
+ for v := range w.mx.RespCode {
+ if v[:1] == class {
+ codes = append(codes, v)
+ }
+ }
+ return codes
+ }
+
+ var n int
+ ids := []string{
+ respCodes1xx.ID,
+ respCodes2xx.ID,
+ respCodes3xx.ID,
+ respCodes4xx.ID,
+ respCodes5xx.ID,
+ }
+ for i, chartID := range ids {
+ class := strconv.Itoa(i + 1)
+ codes := findCodes(class)
+ n += len(codes)
+ chart := w.Charts().Get(chartID)
+ assert.NotNilf(t, chart, "chart '%s' is not created", chartID)
+ if chart == nil {
+ return
+ }
+ for _, v := range codes {
+ id := "resp_code_" + v
+ assert.Truef(t, chart.HasDim(id), "chart '%s' has no dim for '%s' code, expected '%s'", chartID, v, id)
+ }
+ }
+ assert.Equal(t, len(w.mx.RespCode), n)
+}
+
+func testBandwidthChart(t *testing.T, w *WebLog) {
+ if w.mx.BytesSent.Value() == 0 && w.mx.BytesReceived.Value() == 0 {
+ assert.Falsef(t, w.Charts().Has(bandwidth.ID), "chart '%s' is created", bandwidth.ID)
+ } else {
+ assert.Truef(t, w.Charts().Has(bandwidth.ID), "chart '%s' is not created", bandwidth.ID)
+ }
+}
+
+func testReqProcTimeCharts(t *testing.T, w *WebLog) {
+ if isEmptySummary(w.mx.ReqProcTime) {
+ assert.Falsef(t, w.Charts().Has(reqProcTime.ID), "chart '%s' is created", reqProcTime.ID)
+ } else {
+ assert.Truef(t, w.Charts().Has(reqProcTime.ID), "chart '%s' is not created", reqProcTime.ID)
+ }
+
+ if isEmptyHistogram(w.mx.ReqProcTimeHist) {
+ assert.Falsef(t, w.Charts().Has(reqProcTimeHist.ID), "chart '%s' is created", reqProcTimeHist.ID)
+ } else {
+ assert.Truef(t, w.Charts().Has(reqProcTimeHist.ID), "chart '%s' is not created", reqProcTimeHist.ID)
+ }
+}
+
+func testUpsRespTimeCharts(t *testing.T, w *WebLog) {
+ if isEmptySummary(w.mx.UpsRespTime) {
+ assert.Falsef(t, w.Charts().Has(upsRespTime.ID), "chart '%s' is created", upsRespTime.ID)
+ } else {
+ assert.Truef(t, w.Charts().Has(upsRespTime.ID), "chart '%s' is not created", upsRespTime.ID)
+ }
+
+ if isEmptyHistogram(w.mx.UpsRespTimeHist) {
+ assert.Falsef(t, w.Charts().Has(upsRespTimeHist.ID), "chart '%s' is created", upsRespTimeHist.ID)
+ } else {
+ assert.Truef(t, w.Charts().Has(upsRespTimeHist.ID), "chart '%s' is not created", upsRespTimeHist.ID)
+ }
+}
+
+func testSSLProtoChart(t *testing.T, w *WebLog) {
+ if len(w.mx.ReqSSLProto) == 0 {
+ assert.Falsef(t, w.Charts().Has(reqBySSLProto.ID), "chart '%s' is created", reqBySSLProto.ID)
+ return
+ }
+
+ chart := w.Charts().Get(reqBySSLProto.ID)
+ assert.NotNilf(t, chart, "chart '%s' is not created", reqBySSLProto.ID)
+ if chart == nil {
+ return
+ }
+ for v := range w.mx.ReqSSLProto {
+ id := "req_ssl_proto_" + v
+ assert.Truef(t, chart.HasDim(id), "chart '%s' has no dim for '%s' ssl proto, expected '%s'", chart.ID, v, id)
+ }
+}
+
+func testSSLCipherSuiteChart(t *testing.T, w *WebLog) {
+ if len(w.mx.ReqSSLCipherSuite) == 0 {
+ assert.Falsef(t, w.Charts().Has(reqBySSLCipherSuite.ID), "chart '%s' is created", reqBySSLCipherSuite.ID)
+ return
+ }
+
+ chart := w.Charts().Get(reqBySSLCipherSuite.ID)
+ assert.NotNilf(t, chart, "chart '%s' is not created", reqBySSLCipherSuite.ID)
+ if chart == nil {
+ return
+ }
+ for v := range w.mx.ReqSSLCipherSuite {
+ id := "req_ssl_cipher_suite_" + v
+ assert.Truef(t, chart.HasDim(id), "chart '%s' has no dim for '%s' ssl cipher suite, expected '%s'", chart.ID, v, id)
+ }
+}
+
+func testURLPatternStatsCharts(t *testing.T, w *WebLog) {
+ for _, p := range w.URLPatterns {
+ chartID := fmt.Sprintf(urlPatternRespCodes.ID, p.Name)
+
+ if isEmptyCounterVec(w.mx.RespCode) {
+ assert.Falsef(t, w.Charts().Has(chartID), "chart '%s' is created", chartID)
+ continue
+ }
+
+ chart := w.Charts().Get(chartID)
+ assert.NotNilf(t, chart, "chart '%s' is not created", chartID)
+ if chart == nil {
+ continue
+ }
+
+ stats, ok := w.mx.URLPatternStats[p.Name]
+ assert.Truef(t, ok, "url pattern '%s' has no metric in w.mx.URLPatternStats", p.Name)
+ if !ok {
+ continue
+ }
+ for v := range stats.RespCode {
+ id := fmt.Sprintf("url_ptn_%s_resp_code_%s", p.Name, v)
+ assert.Truef(t, chart.HasDim(id), "chart '%s' has no dim for '%s' code, expected '%s'", chartID, v, id)
+ }
+ }
+
+ for _, p := range w.URLPatterns {
+ id := fmt.Sprintf(urlPatternReqMethods.ID, p.Name)
+ if isEmptyCounterVec(w.mx.ReqMethod) {
+ assert.Falsef(t, w.Charts().Has(id), "chart '%s' is created", id)
+ continue
+ }
+
+ chart := w.Charts().Get(id)
+ assert.NotNilf(t, chart, "chart '%s' is not created", id)
+ if chart == nil {
+ continue
+ }
+
+ stats, ok := w.mx.URLPatternStats[p.Name]
+ assert.Truef(t, ok, "url pattern '%s' has no metric in w.mx.URLPatternStats", p.Name)
+ if !ok {
+ continue
+ }
+ for v := range stats.ReqMethod {
+ dimID := fmt.Sprintf("url_ptn_%s_req_method_%s", p.Name, v)
+ assert.Truef(t, chart.HasDim(dimID), "chart '%s' has no dim for '%s' method, expected '%s'", id, v, dimID)
+ }
+ }
+
+ for _, p := range w.URLPatterns {
+ id := fmt.Sprintf(urlPatternBandwidth.ID, p.Name)
+ if w.mx.BytesSent.Value() == 0 && w.mx.BytesReceived.Value() == 0 {
+ assert.Falsef(t, w.Charts().Has(id), "chart '%s' is created", id)
+ } else {
+ assert.Truef(t, w.Charts().Has(id), "chart '%s' is not created", id)
+ }
+ }
+
+ for _, p := range w.URLPatterns {
+ id := fmt.Sprintf(urlPatternReqProcTime.ID, p.Name)
+ if isEmptySummary(w.mx.ReqProcTime) {
+ assert.Falsef(t, w.Charts().Has(id), "chart '%s' is created", id)
+ } else {
+ assert.Truef(t, w.Charts().Has(id), "chart '%s' is not created", id)
+ }
+ }
+}
+
+func testCustomFieldCharts(t *testing.T, w *WebLog) {
+ for _, cf := range w.CustomFields {
+ id := fmt.Sprintf(reqByCustomFieldPattern.ID, cf.Name)
+ chart := w.Charts().Get(id)
+ assert.NotNilf(t, chart, "chart '%s' is not created", id)
+ if chart == nil {
+ continue
+ }
+
+ for _, p := range cf.Patterns {
+ id := fmt.Sprintf("custom_field_%s_%s", cf.Name, p.Name)
+ assert.True(t, chart.HasDim(id), "chart '%s' has no dim for '%s' pattern, expected '%s'", chart.ID, p, id)
+ }
+ }
+}
+
+func testCustomTimeFieldCharts(t *testing.T, w *WebLog) {
+ for _, cf := range w.CustomTimeFields {
+ id := fmt.Sprintf(reqByCustomTimeField.ID, cf.Name)
+ chart := w.Charts().Get(id)
+ assert.NotNilf(t, chart, "chart '%s' is not created", id)
+ if chart == nil {
+ continue
+ }
+ dimMinID := fmt.Sprintf("custom_time_field_%s_time_min", cf.Name)
+ assert.True(t, chart.HasDim(dimMinID), "chart '%s' has no dim for '%s' name, expected '%s'", chart.ID, cf.Name, dimMinID)
+
+ dimMaxID := fmt.Sprintf("custom_time_field_%s_time_min", cf.Name)
+ assert.True(t, chart.HasDim(dimMaxID), "chart '%s' has no dim for '%s' name, expected '%s'", chart.ID, cf.Name, dimMaxID)
+
+ dimAveID := fmt.Sprintf("custom_time_field_%s_time_min", cf.Name)
+ assert.True(t, chart.HasDim(dimAveID), "chart '%s' has no dim for '%s' name, expected '%s'", chart.ID, cf.Name, dimAveID)
+ }
+}
+
+func testCustomNumericFieldCharts(t *testing.T, w *WebLog) {
+ for _, cf := range w.CustomNumericFields {
+ id := fmt.Sprintf(customNumericFieldSummaryChartTmpl.ID, cf.Name)
+ chart := w.Charts().Get(id)
+ assert.NotNilf(t, chart, "chart '%s' is not created", id)
+ if chart == nil {
+ continue
+ }
+ dimMinID := fmt.Sprintf("custom_numeric_field_%s_summary_min", cf.Name)
+ assert.True(t, chart.HasDim(dimMinID), "chart '%s' has no dim for '%s' name, expected '%s'", chart.ID, cf.Name, dimMinID)
+
+ dimMaxID := fmt.Sprintf("custom_numeric_field_%s_summary_min", cf.Name)
+ assert.True(t, chart.HasDim(dimMaxID), "chart '%s' has no dim for '%s' name, expected '%s'", chart.ID, cf.Name, dimMaxID)
+
+ dimAveID := fmt.Sprintf("custom_numeric_field_%s_summary_min", cf.Name)
+ assert.True(t, chart.HasDim(dimAveID), "chart '%s' has no dim for '%s' name, expected '%s'", chart.ID, cf.Name, dimAveID)
+ }
+}
+
+var (
+ emptySummary = newWebLogSummary()
+ emptyHistogram = metrics.NewHistogram(metrics.DefBuckets)
+)
+
+func isEmptySummary(s metrics.Summary) bool { return reflect.DeepEqual(s, emptySummary) }
+func isEmptyHistogram(h metrics.Histogram) bool { return reflect.DeepEqual(h, emptyHistogram) }
+
+func isEmptyCounterVec(cv metrics.CounterVec) bool {
+ for _, c := range cv {
+ if c.Value() > 0 {
+ return false
+ }
+ }
+ return true
+}
+
+func prepareWebLogCollectFull(t *testing.T) *WebLog {
+ t.Helper()
+ format := strings.Join([]string{
+ "$host:$server_port",
+ "$remote_addr",
+ "-",
+ "-",
+ "$time_local",
+ `"$request"`,
+ "$status",
+ "$body_bytes_sent",
+ "$request_length",
+ "$request_time",
+ "$upstream_response_time",
+ "$scheme",
+ "$ssl_protocol",
+ "$ssl_cipher",
+ "$side",
+ "$drink",
+ "$random_time_field",
+ }, " ")
+
+ cfg := Config{
+ ParserConfig: logs.ParserConfig{
+ LogType: logs.TypeCSV,
+ CSV: logs.CSVConfig{
+ FieldsPerRecord: -1,
+ Delimiter: " ",
+ TrimLeadingSpace: false,
+ Format: format,
+ CheckField: checkCSVFormatField,
+ },
+ },
+ Path: "testdata/full.log",
+ ExcludePath: "",
+ URLPatterns: []userPattern{
+ {Name: "com", Match: "~ com$"},
+ {Name: "org", Match: "~ org$"},
+ {Name: "net", Match: "~ net$"},
+ {Name: "not_match", Match: "* !*"},
+ },
+ CustomFields: []customField{
+ {
+ Name: "side",
+ Patterns: []userPattern{
+ {Name: "dark", Match: "= dark"},
+ {Name: "light", Match: "= light"},
+ },
+ },
+ {
+ Name: "drink",
+ Patterns: []userPattern{
+ {Name: "beer", Match: "= beer"},
+ {Name: "wine", Match: "= wine"},
+ },
+ },
+ },
+ CustomTimeFields: []customTimeField{
+ {
+ Name: "random_time_field",
+ Histogram: metrics.DefBuckets,
+ },
+ },
+ Histogram: metrics.DefBuckets,
+ GroupRespCodes: true,
+ }
+ weblog := New()
+ weblog.Config = cfg
+ require.NoError(t, weblog.Init())
+ require.NoError(t, weblog.Check())
+ defer weblog.Cleanup()
+
+ p, err := logs.NewCSVParser(weblog.ParserConfig.CSV, bytes.NewReader(dataFullLog))
+ require.NoError(t, err)
+ weblog.parser = p
+ return weblog
+}
+
+func prepareWebLogCollectCommon(t *testing.T) *WebLog {
+ t.Helper()
+ format := strings.Join([]string{
+ "$remote_addr",
+ "-",
+ "-",
+ "$time_local",
+ `"$request"`,
+ "$status",
+ "$body_bytes_sent",
+ }, " ")
+
+ cfg := Config{
+ ParserConfig: logs.ParserConfig{
+ LogType: logs.TypeCSV,
+ CSV: logs.CSVConfig{
+ FieldsPerRecord: -1,
+ Delimiter: " ",
+ TrimLeadingSpace: false,
+ Format: format,
+ CheckField: checkCSVFormatField,
+ },
+ },
+ Path: "testdata/common.log",
+ ExcludePath: "",
+ URLPatterns: nil,
+ CustomFields: nil,
+ Histogram: nil,
+ GroupRespCodes: false,
+ }
+
+ weblog := New()
+ weblog.Config = cfg
+ require.NoError(t, weblog.Init())
+ require.NoError(t, weblog.Check())
+ defer weblog.Cleanup()
+
+ p, err := logs.NewCSVParser(weblog.ParserConfig.CSV, bytes.NewReader(dataCommonLog))
+ require.NoError(t, err)
+ weblog.parser = p
+ return weblog
+}
+
+func prepareWebLogCollectCustom(t *testing.T) *WebLog {
+ t.Helper()
+ format := strings.Join([]string{
+ "$side",
+ "$drink",
+ }, " ")
+
+ cfg := Config{
+ ParserConfig: logs.ParserConfig{
+ LogType: logs.TypeCSV,
+ CSV: logs.CSVConfig{
+ FieldsPerRecord: 2,
+ Delimiter: " ",
+ TrimLeadingSpace: false,
+ Format: format,
+ CheckField: checkCSVFormatField,
+ },
+ },
+ CustomFields: []customField{
+ {
+ Name: "side",
+ Patterns: []userPattern{
+ {Name: "dark", Match: "= dark"},
+ {Name: "light", Match: "= light"},
+ },
+ },
+ {
+ Name: "drink",
+ Patterns: []userPattern{
+ {Name: "beer", Match: "= beer"},
+ {Name: "wine", Match: "= wine"},
+ },
+ },
+ },
+ Path: "testdata/custom.log",
+ ExcludePath: "",
+ URLPatterns: nil,
+ Histogram: nil,
+ GroupRespCodes: false,
+ }
+ weblog := New()
+ weblog.Config = cfg
+ require.NoError(t, weblog.Init())
+ require.NoError(t, weblog.Check())
+ defer weblog.Cleanup()
+
+ p, err := logs.NewCSVParser(weblog.ParserConfig.CSV, bytes.NewReader(dataCustomLog))
+ require.NoError(t, err)
+ weblog.parser = p
+ return weblog
+}
+
+func prepareWebLogCollectCustomTimeFields(t *testing.T) *WebLog {
+ t.Helper()
+ format := strings.Join([]string{
+ "$time1",
+ "$time2",
+ }, " ")
+
+ cfg := Config{
+ ParserConfig: logs.ParserConfig{
+ LogType: logs.TypeCSV,
+ CSV: logs.CSVConfig{
+ FieldsPerRecord: 2,
+ Delimiter: " ",
+ TrimLeadingSpace: false,
+ Format: format,
+ CheckField: checkCSVFormatField,
+ },
+ },
+ CustomTimeFields: []customTimeField{
+ {
+ Name: "time1",
+ Histogram: metrics.DefBuckets,
+ },
+ {
+ Name: "time2",
+ Histogram: metrics.DefBuckets,
+ },
+ },
+ Path: "testdata/custom_time_fields.log",
+ ExcludePath: "",
+ URLPatterns: nil,
+ Histogram: nil,
+ GroupRespCodes: false,
+ }
+ weblog := New()
+ weblog.Config = cfg
+ require.NoError(t, weblog.Init())
+ require.NoError(t, weblog.Check())
+ defer weblog.Cleanup()
+
+ p, err := logs.NewCSVParser(weblog.ParserConfig.CSV, bytes.NewReader(dataCustomTimeFieldLog))
+ require.NoError(t, err)
+ weblog.parser = p
+ return weblog
+}
+
+func prepareWebLogCollectCustomNumericFields(t *testing.T) *WebLog {
+ t.Helper()
+ format := strings.Join([]string{
+ "$numeric1",
+ "$numeric2",
+ }, " ")
+
+ cfg := Config{
+ ParserConfig: logs.ParserConfig{
+ LogType: logs.TypeCSV,
+ CSV: logs.CSVConfig{
+ FieldsPerRecord: 2,
+ Delimiter: " ",
+ TrimLeadingSpace: false,
+ Format: format,
+ CheckField: checkCSVFormatField,
+ },
+ },
+ CustomNumericFields: []customNumericField{
+ {
+ Name: "numeric1",
+ Units: "bytes",
+ },
+ {
+ Name: "numeric2",
+ Units: "requests",
+ },
+ },
+ Path: "testdata/custom_time_fields.log",
+ ExcludePath: "",
+ URLPatterns: nil,
+ Histogram: nil,
+ GroupRespCodes: false,
+ }
+ weblog := New()
+ weblog.Config = cfg
+ require.NoError(t, weblog.Init())
+ require.NoError(t, weblog.Check())
+ defer weblog.Cleanup()
+
+ p, err := logs.NewCSVParser(weblog.ParserConfig.CSV, bytes.NewReader(dataCustomTimeFieldLog))
+ require.NoError(t, err)
+ weblog.parser = p
+ return weblog
+}
+
+func prepareWebLogCollectIISFields(t *testing.T) *WebLog {
+ t.Helper()
+ format := strings.Join([]string{
+ "-", // date
+ "-", // time
+ "$host", // s-ip
+ "$request_method", // cs-method
+ "$request_uri", // cs-uri-stem
+ "-", // cs-uri-query
+ "$server_port", // s-port
+ "-", // cs-username
+ "$remote_addr", // c-ip
+ "-", // cs(User-Agent)
+ "-", // cs(Referer)
+ "$status", // sc-status
+ "-", // sc-substatus
+ "-", // sc-win32-status
+ "$request_time", // time-taken
+ }, " ")
+ cfg := Config{
+ ParserConfig: logs.ParserConfig{
+ LogType: logs.TypeCSV,
+ CSV: logs.CSVConfig{
+ // Users can define number of fields
+ FieldsPerRecord: -1,
+ Delimiter: " ",
+ TrimLeadingSpace: false,
+ Format: format,
+ CheckField: checkCSVFormatField,
+ },
+ },
+ Path: "testdata/u_ex221107.log",
+ ExcludePath: "",
+ URLPatterns: nil,
+ Histogram: nil,
+ GroupRespCodes: false,
+ }
+
+ weblog := New()
+ weblog.Config = cfg
+ require.NoError(t, weblog.Init())
+ require.NoError(t, weblog.Check())
+ defer weblog.Cleanup()
+
+ p, err := logs.NewCSVParser(weblog.ParserConfig.CSV, bytes.NewReader(dataIISLog))
+ require.NoError(t, err)
+ weblog.parser = p
+ return weblog
+}
+
+// generateLogs is used to populate 'testdata/full.log'
+//func generateLogs(w io.Writer, num int) error {
+// var (
+// vhost = []string{"localhost", "test.example.com", "test.example.org", "198.51.100.1", "2001:db8:1ce::1"}
+// scheme = []string{"http", "https"}
+// client = []string{"localhost", "203.0.113.1", "203.0.113.2", "2001:db8:2ce:1", "2001:db8:2ce:2"}
+// method = []string{"GET", "HEAD", "POST"}
+// url = []string{"example.other", "example.com", "example.org", "example.net"}
+// version = []string{"1.1", "2", "2.0"}
+// status = []int{100, 101, 200, 201, 300, 301, 400, 401} // no 5xx on purpose
+// sslProto = []string{"TLSv1", "TLSv1.1", "TLSv1.2", "TLSv1.3", "SSLv2", "SSLv3"}
+// sslCipher = []string{"ECDHE-RSA-AES256-SHA", "DHE-RSA-AES256-SHA", "AES256-SHA", "PSK-RC4-SHA"}
+//
+// customField1 = []string{"dark", "light"}
+// customField2 = []string{"beer", "wine"}
+// )
+//
+// var line string
+// for i := 0; i < num; i++ {
+// unmatched := randInt(1, 100) > 90
+// if unmatched {
+// line = "Unmatched! The rat the cat the dog chased killed ate the malt!\n"
+// } else {
+// // test.example.com:80 203.0.113.1 - - "GET / HTTP/1.1" 200 1674 2674 3674 4674 http TLSv1 AES256-SHA dark beer
+// line = fmt.Sprintf(
+// "%s:%d %s - - [22/Mar/2009:09:30:31 +0100] \"%s /%s HTTP/%s\" %d %d %d %d %d %s %s %s %s %s\n",
+// randFromString(vhost),
+// randInt(80, 85),
+// randFromString(client),
+// randFromString(method),
+// randFromString(url),
+// randFromString(version),
+// randFromInt(status),
+// randInt(1000, 5000),
+// randInt(1000, 5000),
+// randInt(1, 500),
+// randInt(1, 500),
+// randFromString(scheme),
+// randFromString(sslProto),
+// randFromString(sslCipher),
+// randFromString(customField1),
+// randFromString(customField2),
+// )
+// }
+// _, err := fmt.Fprint(w, line)
+// if err != nil {
+// return err
+// }
+// }
+// return nil
+//}
+//
+//var r = rand.New(rand.NewSource(time.Now().UnixNano()))
+//
+//func randFromString(s []string) string { return s[r.Intn(len(s))] }
+//func randFromInt(s []int) int { return s[r.Intn(len(s))] }
+//func randInt(min, max int) int { return r.Intn(max-min) + min }