aboutsummaryrefslogtreecommitdiff
path: root/http_log.go
diff options
context:
space:
mode:
authorShulhan <ms@kilabit.info>2022-08-17 13:32:00 +0700
committerShulhan <ms@kilabit.info>2022-08-17 13:32:00 +0700
commit05b4830d0fd5aaec157139d88beffa1cf0ce0615 (patch)
tree1138313a4d8bc045f698c94da68a213a3025732c /http_log.go
parent4e45545e0832d9d3a8c0311a87d82e81d3684cbe (diff)
downloadhaminer-05b4830d0fd5aaec157139d88beffa1cf0ce0615.tar.xz
all: rename struct type Halog to HttpLog
Halog contains parsed HTTP log, so its make more readable if we rename the type name.
Diffstat (limited to 'http_log.go')
-rw-r--r--http_log.go390
1 files changed, 390 insertions, 0 deletions
diff --git a/http_log.go b/http_log.go
new file mode 100644
index 0000000..bbaf225
--- /dev/null
+++ b/http_log.go
@@ -0,0 +1,390 @@
+// Copyright 2018, M. Shulhan (ms@kilabit.info). All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package haminer
+
+import (
+ "bytes"
+ "strconv"
+ "strings"
+ "time"
+)
+
+// HttpLog contains the mapping of haproxy HTTP log format to Go struct.
+//
+// Reference: https://cbonte.github.io/haproxy-dconv/1.7/configuration.html#8.2.3
+type HttpLog struct { // nolint: maligned
+ Timestamp time.Time
+
+ ClientIP string
+ ClientPort int32
+
+ FrontendName string
+ BackendName string
+ ServerName string
+
+ TimeReq int32
+ TimeWait int32
+ TimeConnect int32
+ TimeRsp int32
+ TimeAll int32
+
+ BytesRead int64
+
+ CookieReq string
+ CookieRsp string
+
+ TermState string
+
+ ConnActive int32
+ ConnFrontend int32
+ ConnBackend int32
+ ConnServer int32
+ ConnRetries int32
+
+ QueueServer int32
+ QueueBackend int32
+
+ RequestHeaders map[string]string
+
+ HTTPStatus int32
+ HTTPMethod string
+ HTTPURL string
+ HTTPQuery string
+ HTTPProto string
+
+ tagHTTPURL string
+}
+
+// cleanPrefix will remove `<date-time> <process-name>[pid]: ` prefix (which
+// come from systemd/rsyslog) in input.
+func cleanPrefix(in []byte) bool {
+ start := bytes.IndexByte(in, '[')
+ if start < 0 {
+ return false
+ }
+
+ end := bytes.IndexByte(in[start:], ']')
+ if end < 0 {
+ return false
+ }
+
+ end = start + end + 3
+
+ copy(in[0:], in[end:])
+
+ return true
+}
+
+func parseToString(in []byte, sep byte) (string, bool) {
+ end := bytes.IndexByte(in, sep)
+ if end < 0 {
+ return "", false
+ }
+
+ v := string(in[:end])
+ copy(in, in[end+1:])
+
+ return v, true
+}
+
+func parseToInt32(in []byte, sep byte) (int32, bool) {
+ end := bytes.IndexByte(in, sep)
+ if end < 0 {
+ return 0, false
+ }
+
+ v, err := strconv.Atoi(string(in[:end]))
+ if err != nil {
+ return 0, false
+ }
+
+ copy(in, in[end+1:])
+
+ return int32(v), true
+}
+
+func parseToInt64(in []byte, sep byte) (int64, bool) {
+ end := bytes.IndexByte(in, sep)
+ if end < 0 {
+ return 0, false
+ }
+
+ v, err := strconv.ParseInt(string(in[:end]), 10, 64)
+ if err != nil {
+ return 0, false
+ }
+
+ copy(in, in[end+1:])
+
+ return v, true
+}
+
+func (halog *HttpLog) parseTimes(in []byte) (ok bool) {
+ halog.TimeReq, ok = parseToInt32(in, '/')
+ if !ok {
+ return
+ }
+
+ halog.TimeWait, ok = parseToInt32(in, '/')
+ if !ok {
+ return
+ }
+
+ halog.TimeConnect, ok = parseToInt32(in, '/')
+ if !ok {
+ return
+ }
+
+ halog.TimeRsp, ok = parseToInt32(in, '/')
+ if !ok {
+ return
+ }
+
+ halog.TimeAll, ok = parseToInt32(in, ' ')
+ if !ok {
+ return
+ }
+
+ return
+}
+
+func (halog *HttpLog) parseConns(in []byte) (ok bool) {
+ halog.ConnActive, ok = parseToInt32(in, '/')
+ if !ok {
+ return
+ }
+
+ halog.ConnFrontend, ok = parseToInt32(in, '/')
+ if !ok {
+ return
+ }
+
+ halog.ConnBackend, ok = parseToInt32(in, '/')
+ if !ok {
+ return
+ }
+
+ halog.ConnServer, ok = parseToInt32(in, '/')
+ if !ok {
+ return
+ }
+
+ halog.ConnRetries, ok = parseToInt32(in, ' ')
+ if !ok {
+ return
+ }
+
+ return
+}
+
+func (halog *HttpLog) parseQueue(in []byte) (ok bool) {
+ halog.QueueServer, ok = parseToInt32(in, '/')
+ if !ok {
+ return
+ }
+
+ halog.QueueBackend, ok = parseToInt32(in, ' ')
+
+ return
+}
+
+// parserRequestHeaders parse the request header values in log file.
+// The request headers start with '{' and end with '}'.
+// Each header is separated by '|'.
+func (halog *HttpLog) parseRequestHeaders(in []byte, reqHeaders []string) (ok bool) {
+ if in[0] != '{' {
+ // Skip if we did not find the beginning.
+ return true
+ }
+
+ end := bytes.IndexByte(in, '}')
+ // Either '}' not found or its empty as in '{}'.
+ if end <= 1 {
+ return
+ }
+
+ sep := []byte{'|'}
+ bheaders := bytes.Split(in[1:end], sep)
+
+ if len(reqHeaders) != len(bheaders) {
+ return
+ }
+
+ halog.RequestHeaders = make(map[string]string)
+ for x, name := range reqHeaders {
+ halog.RequestHeaders[name] = string(bheaders[x])
+ }
+
+ copy(in, in[end+2:])
+
+ return true
+}
+
+func (halog *HttpLog) parseHTTP(in []byte) (ok bool) {
+ halog.HTTPMethod, ok = parseToString(in, ' ')
+ if !ok {
+ return
+ }
+
+ v, ok := parseToString(in, ' ')
+ if !ok {
+ return
+ }
+ urlQuery := strings.SplitN(v, "?", 2)
+ halog.HTTPURL = urlQuery[0]
+ if len(urlQuery) == 2 {
+ halog.HTTPQuery = urlQuery[1]
+ }
+
+ halog.HTTPProto, ok = parseToString(in, '"')
+
+ return ok
+}
+
+// Parse will parse one line of HAProxy log format into HttpLog.
+//
+// nolint: gocyclo
+func (halog *HttpLog) Parse(in []byte, reqHeaders []string) (ok bool) {
+ var err error
+
+ // Remove prefix from systemd/rsyslog
+ ok = cleanPrefix(in)
+ if !ok {
+ return
+ }
+
+ // parse client IP
+ halog.ClientIP, ok = parseToString(in, ':')
+ if !ok {
+ return
+ }
+
+ // parse client port
+ halog.ClientPort, ok = parseToInt32(in, ' ')
+ if !ok {
+ return
+ }
+
+ // parse timestamp, remove '[' and parse until ']'
+ in = in[1:]
+ ts, ok := parseToString(in, ']')
+ if !ok {
+ return
+ }
+
+ halog.Timestamp, err = time.Parse("2/Jan/2006:15:04:05.000", ts)
+ if err != nil {
+ return false
+ }
+
+ // parse frontend name
+ in = in[1:]
+ halog.FrontendName, ok = parseToString(in, ' ')
+ if !ok {
+ return
+ }
+
+ // parse backend name
+ halog.BackendName, ok = parseToString(in, '/')
+ if !ok {
+ return
+ }
+
+ // parse server name
+ halog.ServerName, ok = parseToString(in, ' ')
+ if !ok {
+ return
+ }
+
+ // parse times
+ ok = halog.parseTimes(in)
+ if !ok {
+ return
+ }
+
+ // parse HTTP status code
+ halog.HTTPStatus, ok = parseToInt32(in, ' ')
+ if !ok {
+ return
+ }
+
+ // parse bytes read
+ halog.BytesRead, ok = parseToInt64(in, ' ')
+ if !ok {
+ return
+ }
+
+ // parse request cookie
+ halog.CookieReq, ok = parseToString(in, ' ')
+ if !ok {
+ return
+ }
+
+ // parse response cookie
+ halog.CookieRsp, ok = parseToString(in, ' ')
+ if !ok {
+ return
+ }
+
+ // parse termination state
+ halog.TermState, ok = parseToString(in, ' ')
+ if !ok {
+ return
+ }
+
+ // parse number of connections
+ ok = halog.parseConns(in)
+ if !ok {
+ return
+ }
+
+ // parse number of queue state
+ ok = halog.parseQueue(in)
+ if !ok {
+ return
+ }
+
+ if len(reqHeaders) > 0 {
+ ok = halog.parseRequestHeaders(in, reqHeaders)
+ if !ok {
+ return
+ }
+ }
+
+ // parse HTTP
+ in = in[1:]
+ ok = halog.parseHTTP(in)
+
+ return ok
+}
+
+// ParseUDPPacket will convert UDP packet (in bytes) to instance of
+// HttpLog.
+//
+// It will return nil and false if UDP packet is nil, have zero length, or
+// cannot be parsed (rejected).
+func (halog *HttpLog) ParseUDPPacket(packet []byte, reqHeaders []string) bool {
+ if len(packet) == 0 {
+ return false
+ }
+
+ var (
+ endIdx int
+ in []byte
+ )
+
+ if packet[0] == '<' {
+ endIdx = bytes.IndexByte(packet, '>')
+ if endIdx < 0 {
+ return false
+ }
+
+ in = packet[endIdx+1:]
+ } else {
+ in = packet
+ }
+
+ return halog.Parse(in, reqHeaders)
+}