summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--haminer.go9
-rw-r--r--http_log.go306
-rw-r--r--http_log_test.go45
-rw-r--r--testdata/ParseUDPPacket_test.txt37
4 files changed, 227 insertions, 170 deletions
diff --git a/haminer.go b/haminer.go
index b89b619..ca1f850 100644
--- a/haminer.go
+++ b/haminer.go
@@ -118,9 +118,6 @@ func (h *Haminer) Start() (err error) {
// filter will return true if log is accepted; otherwise it will return false.
func (h *Haminer) filter(halog *HTTPLog) bool {
- if halog == nil {
- return false
- }
if halog.BackendName == `-` {
return false
}
@@ -153,10 +150,8 @@ func (h *Haminer) consume() {
continue
}
- halog = &HTTPLog{}
-
- ok = halog.ParseUDPPacket(packet[:n], h.cfg.RequestHeaders)
- if !ok {
+ halog = ParseUDPPacket(packet[:n], h.cfg.RequestHeaders)
+ if halog == nil {
continue
}
diff --git a/http_log.go b/http_log.go
index bce442f..b90abb5 100644
--- a/http_log.go
+++ b/http_log.go
@@ -50,7 +50,8 @@ const (
type HTTPLog struct {
Timestamp time.Time
- RequestHeaders map[string]string
+ RequestHeaders map[string]string
+ ResponseHeaders map[string]string
ClientIP string
@@ -58,18 +59,19 @@ type HTTPLog struct {
BackendName string
ServerName string
- CookieReq string
- CookieRsp string
- TermState string
-
HTTPMethod string
HTTPURL string
HTTPQuery string
HTTPProto string
tagHTTPURL string
+ CookieReq string
+ CookieRsp string
+ TermState string
+
BytesRead int64
+ HTTPStatus int32
ClientPort int32
TimeReq int32
@@ -86,28 +88,152 @@ type HTTPLog struct {
QueueServer int32
QueueBackend int32
+}
- HTTPStatus int32
+// ParseUDPPacket convert UDP packet (in bytes) to instance of HTTPLog.
+//
+// It will return nil if UDP packet is nil, have zero length, or cannot be
+// parsed (rejected).
+func ParseUDPPacket(packet []byte, reqHeaders []string) (httpLog *HTTPLog) {
+ if len(packet) == 0 {
+ return nil
+ }
+
+ if packet[0] == '<' {
+ var endIdx = bytes.IndexByte(packet, '>')
+ if endIdx < 0 {
+ return nil
+ }
+ packet = packet[endIdx+1:]
+ }
+
+ return Parse(packet, reqHeaders)
+}
+
+// Parse single line of HAProxy log format into HTTPLog.
+//
+// nolint: gocyclo
+func Parse(in []byte, reqHeaders []string) (httpLog *HTTPLog) {
+ in = cleanPrefix(in)
+ if in == nil {
+ return nil
+ }
+
+ var ok bool
+
+ httpLog = &HTTPLog{}
+
+ httpLog.ClientIP, ok = parseToString(in, ':')
+ if !ok {
+ return nil
+ }
+
+ httpLog.ClientPort, ok = parseToInt32(in, ' ')
+ if !ok {
+ return nil
+ }
+
+ // parse timestamp, remove '[' and parse until ']'
+ in = in[1:]
+ ts, ok := parseToString(in, ']')
+ if !ok {
+ return nil
+ }
+
+ var err error
+
+ httpLog.Timestamp, err = time.Parse(`2/Jan/2006:15:04:05.000`, ts)
+ if err != nil {
+ return nil
+ }
+
+ in = in[1:]
+ httpLog.FrontendName, ok = parseToString(in, ' ')
+ if !ok {
+ return nil
+ }
+
+ httpLog.BackendName, ok = parseToString(in, '/')
+ if !ok {
+ return nil
+ }
+
+ httpLog.ServerName, ok = parseToString(in, ' ')
+ if !ok {
+ return nil
+ }
+
+ ok = httpLog.parseConnectionTimes(in)
+ if !ok {
+ return nil
+ }
+
+ httpLog.HTTPStatus, ok = parseToInt32(in, ' ')
+ if !ok {
+ return nil
+ }
+
+ httpLog.BytesRead, ok = parseToInt64(in, ' ')
+ if !ok {
+ return nil
+ }
+
+ httpLog.CookieReq, ok = parseToString(in, ' ')
+ if !ok {
+ return nil
+ }
+ httpLog.CookieRsp, ok = parseToString(in, ' ')
+ if !ok {
+ return nil
+ }
+
+ httpLog.TermState, ok = parseToString(in, ' ')
+ if !ok {
+ return nil
+ }
+
+ ok = httpLog.parseConns(in)
+ if !ok {
+ return nil
+ }
+
+ ok = httpLog.parseQueue(in)
+ if !ok {
+ return nil
+ }
+
+ if len(reqHeaders) > 0 {
+ ok = httpLog.parseRequestHeaders(in, reqHeaders)
+ if !ok {
+ return nil
+ }
+ }
+
+ in = in[1:]
+ ok = httpLog.parseHTTP(in)
+ if !ok {
+ return nil
+ }
+
+ return httpLog
}
-// 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, '[')
+// cleanPrefix will remove `<date-time> <process-name>[pid]: ` prefix which
+// come from systemd/rsyslog in input.
+func cleanPrefix(in []byte) []byte {
+ var start = bytes.IndexByte(in, '[')
if start < 0 {
- return false
+ return nil
}
- end := bytes.IndexByte(in[start:], ']')
+ var end = bytes.IndexByte(in[start:], ']')
if end < 0 {
- return false
+ return nil
}
end = start + end + 3
- copy(in[0:], in[end:])
-
- return true
+ return in[end:]
}
func parseToString(in []byte, sep byte) (string, bool) {
@@ -165,7 +291,7 @@ func parseToInt64(in []byte, sep byte) (int64, bool) {
return v, true
}
-func (httpLog *HTTPLog) parseTimes(in []byte) (ok bool) {
+func (httpLog *HTTPLog) parseConnectionTimes(in []byte) (ok bool) {
httpLog.TimeReq, ok = parseToInt32(in, '/')
if !ok {
return
@@ -287,152 +413,6 @@ func (httpLog *HTTPLog) parseHTTP(in []byte) (ok bool) {
return ok
}
-// Parse will parse one line of HAProxy log format into HTTPLog.
-//
-// nolint: gocyclo
-func (httpLog *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
- httpLog.ClientIP, ok = parseToString(in, ':')
- if !ok {
- return
- }
-
- // parse client port
- httpLog.ClientPort, ok = parseToInt32(in, ' ')
- if !ok {
- return
- }
-
- // parse timestamp, remove '[' and parse until ']'
- in = in[1:]
- ts, ok := parseToString(in, ']')
- if !ok {
- return
- }
-
- httpLog.Timestamp, err = time.Parse(`2/Jan/2006:15:04:05.000`, ts)
- if err != nil {
- return false
- }
-
- // parse frontend name
- in = in[1:]
- httpLog.FrontendName, ok = parseToString(in, ' ')
- if !ok {
- return
- }
-
- // parse backend name
- httpLog.BackendName, ok = parseToString(in, '/')
- if !ok {
- return
- }
-
- // parse server name
- httpLog.ServerName, ok = parseToString(in, ' ')
- if !ok {
- return
- }
-
- // parse times
- ok = httpLog.parseTimes(in)
- if !ok {
- return
- }
-
- // parse HTTP status code
- httpLog.HTTPStatus, ok = parseToInt32(in, ' ')
- if !ok {
- return
- }
-
- // parse bytes read
- httpLog.BytesRead, ok = parseToInt64(in, ' ')
- if !ok {
- return
- }
-
- // parse request cookie
- httpLog.CookieReq, ok = parseToString(in, ' ')
- if !ok {
- return
- }
-
- // parse response cookie
- httpLog.CookieRsp, ok = parseToString(in, ' ')
- if !ok {
- return
- }
-
- // parse termination state
- httpLog.TermState, ok = parseToString(in, ' ')
- if !ok {
- return
- }
-
- // parse number of connections
- ok = httpLog.parseConns(in)
- if !ok {
- return
- }
-
- // parse number of queue state
- ok = httpLog.parseQueue(in)
- if !ok {
- return
- }
-
- if len(reqHeaders) > 0 {
- ok = httpLog.parseRequestHeaders(in, reqHeaders)
- if !ok {
- return
- }
- }
-
- // parse HTTP
- in = in[1:]
- ok = httpLog.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 (httpLog *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 httpLog.Parse(in, reqHeaders)
-}
-
// writeIlp write the HTTP log as Influxdb Line Protocol.
func (httpLog *HTTPLog) writeIlp(out io.Writer) (err error) {
var (
diff --git a/http_log_test.go b/http_log_test.go
new file mode 100644
index 0000000..157ff71
--- /dev/null
+++ b/http_log_test.go
@@ -0,0 +1,45 @@
+// SPDX-FileCopyrightText: 2018 M. Shulhan <ms@kilabit.info>
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package haminer
+
+import (
+ "encoding/json"
+ "testing"
+
+ "git.sr.ht/~shulhan/pakakeh.go/lib/test"
+)
+
+func TestParseUDPPacket(t *testing.T) {
+ var (
+ logp = `TestParseUDPPacket`
+ tdata *test.Data
+ err error
+ )
+ tdata, err = test.LoadData(`testdata/ParseUDPPacket_test.txt`)
+ if err != nil {
+ t.Fatal(logp, err)
+ }
+
+ var listCase = []string{
+ `http_log_0000`,
+ }
+
+ var (
+ httpLog *HTTPLog
+ tag string
+ exp string
+ got []byte
+ )
+ for _, tag = range listCase {
+ httpLog = ParseUDPPacket(tdata.Input[tag], nil)
+
+ got, err = json.MarshalIndent(httpLog, ``, ` `)
+ if err != nil {
+ t.Fatal(logp, err)
+ }
+
+ exp = string(tdata.Output[tag])
+ test.Assert(t, tag, exp, string(got))
+ }
+}
diff --git a/testdata/ParseUDPPacket_test.txt b/testdata/ParseUDPPacket_test.txt
new file mode 100644
index 0000000..9b93015
--- /dev/null
+++ b/testdata/ParseUDPPacket_test.txt
@@ -0,0 +1,37 @@
+Test data for ParseUDPPacket.
+
+>>> http_log_0000
+<134>Mar 17 05:08:28 haproxy[371]: 169.254.63.64:52722 [17/Mar/2024:05:08:28.886] fe-http be-http/be-http2 10/20/30/40/50 200 149 - - ---- 1/1/2/3/4 5/6 "GET / HTTP/1.1"
+
+<<< http_log_0000
+{
+ "Timestamp": "2024-03-17T05:08:28.886Z",
+ "RequestHeaders": null,
+ "ResponseHeaders": null,
+ "ClientIP": "169.254.63.64",
+ "FrontendName": "fe-http",
+ "BackendName": "be-http",
+ "ServerName": "be-http2",
+ "HTTPMethod": "GET",
+ "HTTPURL": "/",
+ "HTTPQuery": "",
+ "HTTPProto": "HTTP/1.1",
+ "CookieReq": "-",
+ "CookieRsp": "-",
+ "TermState": "----",
+ "BytesRead": 149,
+ "HTTPStatus": 200,
+ "ClientPort": 52722,
+ "TimeReq": 10,
+ "TimeWait": 20,
+ "TimeConnect": 30,
+ "TimeRsp": 40,
+ "TimeAll": 50,
+ "ConnActive": 1,
+ "ConnFrontend": 1,
+ "ConnBackend": 2,
+ "ConnServer": 3,
+ "ConnRetries": 4,
+ "QueueServer": 5,
+ "QueueBackend": 6
+}