From d8748a145de99c32aac9aa27f4984044a39902c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Renard?= Date: Wed, 24 Feb 2021 08:03:11 +0100 Subject: [PATCH] v1 (#22) --- Makefile | 8 +- README.md | 31 +- go.sum | 1 + parsercommon/parsercommon.go | 196 ++++++++++ parsercommon/parsercommon_test.go | 257 ++++++++++++ rfc3164/example_test.go | 4 +- rfc3164/rfc3164.go | 206 ++++++---- rfc3164/rfc3164_test.go | 405 +++++++++++++++---- rfc5424/example_test.go | 4 +- rfc5424/rfc5424.go | 231 +++++++---- rfc5424/rfc5424_test.go | 628 ++++++++++++++++++++---------- syslogparser.go | 213 +--------- syslogparser_test.go | 283 +------------- 13 files changed, 1545 insertions(+), 922 deletions(-) create mode 100644 parsercommon/parsercommon.go create mode 100644 parsercommon/parsercommon_test.go diff --git a/Makefile b/Makefile index c6dcac4..78b9a8e 100644 --- a/Makefile +++ b/Makefile @@ -10,7 +10,7 @@ GO_TEST_TIMEOUT ?= 15s GO_BENCH=go test -bench=. -benchmem -all: test +all: lint test benchmark test: $(GO) test \ @@ -18,8 +18,12 @@ test: -timeout $(GO_TEST_TIMEOUT) \ $(GO_TEST_PKGS) -#XXX: ugly +#FIXME benchmark: $(GO_BENCH) cd rfc3164 && $(GO_BENCH) cd rfc5424 && $(GO_BENCH) + cd parsercommon && $(GO_BENCH) + +lint: + golangci-lint run ./... diff --git a/README.md b/README.md index 1a792a5..204c3ae 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,8 @@ Syslogparser This is a syslog parser for the Go programming language. +https://pkg.go.dev/github.com/jeromer/syslogparser + Installing ---------- @@ -112,34 +114,33 @@ Run `make benchmark` goos: linux goarch: amd64 pkg: github.com/jeromer/syslogparser - BenchmarkParsePriority-8 41772079 31.2 ns/op 0 B/op 0 allocs/op - BenchmarkParseVersion-8 270007530 4.45 ns/op 0 B/op 0 allocs/op - BenchmarkDetectRFC-8 78742269 16.2 ns/op 0 B/op 0 allocs/op + BenchmarkDetectRFC-8 81994480 14.7 ns/op 0 B/op 0 allocs/op PASS - ok github.com/jeromer/syslogparser 5.257s + ok github.com/jeromer/syslogparser 2.145s cd rfc3164 && go test -bench=. -benchmem goos: linux goarch: amd64 pkg: github.com/jeromer/syslogparser/rfc3164 - BenchmarkParseTimestamp-8 2693362 467 ns/op 16 B/op 1 allocs/op - BenchmarkParseHostname-8 34919636 32.8 ns/op 16 B/op 1 allocs/op - BenchmarkParseTag-8 20970715 56.0 ns/op 8 B/op 1 allocs/op - BenchmarkParseHeader-8 2549106 478 ns/op 32 B/op 2 allocs/op - BenchmarkParsemessage-8 8280796 143 ns/op 72 B/op 3 allocs/op - BenchmarkParseFull-8 8070195 139 ns/op 120 B/op 3 allocs/op + BenchmarkParseTimestamp-8 2823901 416 ns/op 16 B/op 1 allocs/op + BenchmarkParseHostname-8 34796552 35.4 ns/op 16 B/op 1 allocs/op + BenchmarkParseTag-8 20954252 59.3 ns/op 8 B/op 1 allocs/op + BenchmarkParseHeader-8 2276569 596 ns/op 80 B/op 3 allocs/op + BenchmarkParsemessage-8 6751579 192 ns/op 104 B/op 4 allocs/op + BenchmarkParseFull-8 1445076 838 ns/op 336 B/op 10 allocs/op PASS - ok github.com/jeromer/syslogparser/rfc3164 8.428s + ok github.com/jeromer/syslogparser/rfc3164 9.601s cd rfc5424 && go test -bench=. -benchmem goos: linux goarch: amd64 pkg: github.com/jeromer/syslogparser/rfc5424 - BenchmarkParseTimestamp-8 846019 1385 ns/op 352 B/op 18 allocs/op - BenchmarkParseHeader-8 1424103 840 ns/op 106 B/op 12 allocs/op - BenchmarkParseFull-8 1444834 825 ns/op 112 B/op 12 allocs/op + BenchmarkParseTimestamp-8 790478 1488 ns/op 432 B/op 21 allocs/op + BenchmarkParseHeader-8 1000000 1043 ns/op 336 B/op 18 allocs/op + BenchmarkParseFull-8 980828 1306 ns/op 672 B/op 21 allocs/op PASS - ok github.com/jeromer/syslogparser/rfc5424 6.195s + ok github.com/jeromer/syslogparser/rfc5424 4.356s + [RFC 5424]: https://tools.ietf.org/html/rfc5424 [RFC 3164]: https://tools.ietf.org/html/rfc3164 diff --git a/go.sum b/go.sum index b380ae4..acb88a4 100644 --- a/go.sum +++ b/go.sum @@ -5,6 +5,7 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/parsercommon/parsercommon.go b/parsercommon/parsercommon.go new file mode 100644 index 0000000..27e8a58 --- /dev/null +++ b/parsercommon/parsercommon.go @@ -0,0 +1,196 @@ +package parsercommon + +import ( + "fmt" + "strconv" + "strings" +) + +const ( + NO_VERSION = -1 +) + +var ( + ErrEOL = &ParserError{"End of log line"} + ErrNoSpace = &ParserError{"No space found"} + + ErrPriorityNoStart = &ParserError{"No start char found for priority"} + ErrPriorityEmpty = &ParserError{"Priority field empty"} + ErrPriorityNoEnd = &ParserError{"No end char found for priority"} + ErrPriorityTooShort = &ParserError{"Priority field too short"} + ErrPriorityTooLong = &ParserError{"Priority field too long"} + ErrPriorityNonDigit = &ParserError{"Non digit found in priority"} + + ErrVersionNotFound = &ParserError{"Can not find version"} + + ErrTimestampUnknownFormat = &ParserError{"Timestamp format unknown"} + + ErrHostnameNotFound = &ParserError{"Hostname not found"} +) + +type ParserError struct { + ErrorString string +} + +type Priority struct { + P int + F Facility + S Severity +} + +type Facility struct { + Value int +} + +type Severity struct { + Value int +} + +// https://tools.ietf.org/html/rfc3164#section-4.1 +func ParsePriority(buff []byte, cursor *int, l int) (*Priority, error) { + if l <= 0 { + return nil, ErrPriorityEmpty + } + + if buff[*cursor] != '<' { + return nil, ErrPriorityNoStart + } + + i := 1 + priDigit := 0 + + for i < l { + if i >= 5 { + return nil, ErrPriorityTooLong + } + + c := buff[i] + + if c == '>' { + if i == 1 { + return nil, ErrPriorityTooShort + } + + *cursor = i + 1 + + return NewPriority(priDigit), nil + } + + if IsDigit(c) { + v, e := strconv.Atoi(string(c)) + if e != nil { + return nil, e + } + + priDigit = (priDigit * 10) + v + } else { + return nil, ErrPriorityNonDigit + } + + i++ + } + + return nil, ErrPriorityNoEnd +} + +// https://tools.ietf.org/html/rfc5424#section-6.2.2 +func ParseVersion(buff []byte, cursor *int, l int) (int, error) { + if *cursor >= l { + return NO_VERSION, ErrVersionNotFound + } + + c := buff[*cursor] + *cursor++ + + // XXX : not a version, not an error though as RFC 3164 does not support it + if !IsDigit(c) { + return NO_VERSION, nil + } + + v, e := strconv.Atoi(string(c)) + if e != nil { + *cursor-- + return NO_VERSION, e + } + + return v, nil + +} + +func IsDigit(c byte) bool { + return c >= '0' && c <= '9' +} + +func NewPriority(p int) *Priority { + // The Priority value is calculated by first multiplying the Facility + // number by 8 and then adding the numerical value of the Severity. + + return &Priority{ + P: p, + F: Facility{Value: p / 8}, + S: Severity{Value: p % 8}, + } +} + +func FindNextSpace(buff []byte, from int, l int) (int, error) { + var to int + + for to = from; to < l; to++ { + if buff[to] == ' ' { + to++ + return to, nil + } + } + + return 0, ErrNoSpace +} + +func Parse2Digits(buff []byte, cursor *int, l int, min int, max int, e error) (int, error) { + digitLen := 2 + + if *cursor+digitLen > l { + return 0, ErrEOL + } + + sub := string(buff[*cursor : *cursor+digitLen]) + + *cursor += digitLen + + i, err := strconv.Atoi(sub) + if err != nil { + return 0, e + } + + if i >= min && i <= max { + return i, nil + } + + return 0, e +} + +func ParseHostname(buff []byte, cursor *int, l int) (string, error) { + from := *cursor + var to int + + for to = from; to < l; to++ { + if buff[to] == ' ' { + break + } + } + + hostname := buff[from:to] + + *cursor = to + + return string(hostname), nil +} + +func ShowCursorPos(buff []byte, cursor int) { + fmt.Println(string(buff)) + padding := strings.Repeat("-", cursor) + fmt.Println(padding + "↑\n") +} + +func (err *ParserError) Error() string { + return err.ErrorString +} diff --git a/parsercommon/parsercommon_test.go b/parsercommon/parsercommon_test.go new file mode 100644 index 0000000..ee0ec30 --- /dev/null +++ b/parsercommon/parsercommon_test.go @@ -0,0 +1,257 @@ +package parsercommon + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestParsePriority(t *testing.T) { + testCases := []struct { + description string + input []byte + expectedPri *Priority + expectedCursorPos int + expectedErr error + }{ + { + description: "empty priority", + input: []byte(""), + expectedPri: nil, + expectedCursorPos: 0, + expectedErr: ErrPriorityEmpty, + }, + { + description: "no start", + input: []byte("7>"), + expectedPri: nil, + expectedCursorPos: 0, + expectedErr: ErrPriorityNoStart, + }, + { + description: "no end", + input: []byte("<77"), + expectedPri: nil, + expectedCursorPos: 0, + expectedErr: ErrPriorityNoEnd, + }, + { + description: "too short", + input: []byte("<>"), + expectedPri: nil, + expectedCursorPos: 0, + expectedErr: ErrPriorityTooShort, + }, + { + description: "too long", + input: []byte("<1233>"), + expectedPri: nil, + expectedCursorPos: 0, + expectedErr: ErrPriorityTooLong, + }, + { + description: "no digits", + input: []byte("<7a8>"), + expectedPri: nil, + expectedCursorPos: 0, + expectedErr: ErrPriorityNonDigit, + }, + { + description: "all good", + input: []byte("<190>"), + expectedPri: NewPriority(190), + expectedCursorPos: 5, + expectedErr: nil, + }, + } + + for _, tc := range testCases { + cursor := 0 + + obtained, err := ParsePriority( + tc.input, &cursor, len(tc.input), + ) + + require.Equal( + t, tc.expectedPri, obtained, tc.description, + ) + + require.Equal( + t, tc.expectedCursorPos, cursor, tc.description, + ) + + require.Equal( + t, tc.expectedErr, err, tc.description, + ) + } +} + +func TestNewPriority(t *testing.T) { + require.Equal( + t, + &Priority{ + P: 165, + F: Facility{Value: 20}, + S: Severity{Value: 5}, + }, + NewPriority(165), + ) +} + +func TestParseVersion(t *testing.T) { + testCases := []struct { + description string + input []byte + expectedVersion int + expectedCursorPos int + expectedErr error + }{ + { + description: "not found", + input: []byte("<123>"), + expectedVersion: NO_VERSION, + expectedCursorPos: 5, + expectedErr: ErrVersionNotFound, + }, + { + description: "non digit", + input: []byte("<123>a"), + expectedVersion: NO_VERSION, + expectedCursorPos: 6, + expectedErr: nil, + }, + { + description: "all good", + input: []byte("<123>1"), + expectedVersion: 1, + expectedCursorPos: 6, + expectedErr: nil, + }, + } + + for _, tc := range testCases { + cursor := 5 + + obtained, err := ParseVersion( + tc.input, &cursor, len(tc.input), + ) + + require.Equal( + t, tc.expectedVersion, obtained, tc.description, + ) + + require.Equal( + t, tc.expectedCursorPos, cursor, tc.description, + ) + + require.Equal( + t, tc.expectedErr, err, tc.description, + ) + } +} + +func TestParseHostname(t *testing.T) { + testCases := []struct { + description string + input []byte + expectedHostname string + expectedCursorPos int + }{ + { + description: "invalid", + input: []byte("foo name"), + expectedHostname: "foo", + expectedCursorPos: 3, + }, + { + description: "valid", + input: []byte("ubuntu11.somehost.com" + " "), + expectedHostname: "ubuntu11.somehost.com", + expectedCursorPos: len("ubuntu11.somehost.com"), + }, + } + + for _, tc := range testCases { + cursor := 0 + + obtained, err := ParseHostname( + tc.input, &cursor, len(tc.input), + ) + + require.Equal( + t, tc.expectedHostname, obtained, tc.description, + ) + + require.Equal( + t, tc.expectedCursorPos, cursor, tc.description, + ) + + require.Nil( + t, err, + ) + } +} + +func TestFindNextSpace(t *testing.T) { + testCases := []struct { + description string + input []byte + expectedCursorPos int + expectedErr error + }{ + { + description: "no space", + input: []byte("aaaaaa"), + expectedCursorPos: 0, + expectedErr: ErrNoSpace, + }, + { + description: "space found", + input: []byte("foo bar baz"), + expectedCursorPos: 4, + expectedErr: nil, + }, + } + + for _, tc := range testCases { + obtained, err := FindNextSpace( + tc.input, 0, len(tc.input), + ) + + require.Equal( + t, tc.expectedCursorPos, obtained, tc.description, + ) + + require.Equal( + t, tc.expectedErr, err, tc.description, + ) + } +} + +func BenchmarkParsePriority(b *testing.B) { + buff := []byte("<190>") + var start int + l := len(buff) + + for i := 0; i < b.N; i++ { + start = 0 + _, err := ParsePriority(buff, &start, l) + if err != nil { + panic(err) + } + } +} + +func BenchmarkParseVersion(b *testing.B) { + buff := []byte("<123>1") + start := 5 + l := len(buff) + + for i := 0; i < b.N; i++ { + start = 0 + _, err := ParseVersion(buff, &start, l) + if err != nil { + panic(err) + } + } +} diff --git a/rfc3164/example_test.go b/rfc3164/example_test.go index 418dd6a..f67a2c4 100644 --- a/rfc3164/example_test.go +++ b/rfc3164/example_test.go @@ -16,5 +16,7 @@ func ExampleNewParser() { panic(err) } - fmt.Println(p.Dump()) + for k, v := range p.Dump() { + fmt.Println(k, ":", v) + } } diff --git a/rfc3164/rfc3164.go b/rfc3164/rfc3164.go index 3fc3831..f2bf434 100644 --- a/rfc3164/rfc3164.go +++ b/rfc3164/rfc3164.go @@ -2,22 +2,32 @@ package rfc3164 import ( "bytes" + "math" "time" "github.com/jeromer/syslogparser" + "github.com/jeromer/syslogparser/parsercommon" +) + +const ( + // according to https://tools.ietf.org/html/rfc3164#section-4.1 + // "The total length of the packet MUST be 1024 bytes or less" + // However we will accept a bit more while protecting from exhaustion + MAX_PACKET_LEN = 2048 ) type Parser struct { - buff []byte - cursor int - l int - priority syslogparser.Priority - version int - header header - message rfc3164message - location *time.Location - hostname string - ParsePriority bool + buff []byte + cursor int + l int + priority *parsercommon.Priority + version int + header *header + message *message + location *time.Location + hostname string + customTag string + customTimestampFormat string } type header struct { @@ -25,120 +35,165 @@ type header struct { hostname string } -type rfc3164message struct { +type message struct { tag string content string } func NewParser(buff []byte) *Parser { return &Parser{ - buff: buff, - cursor: 0, - l: len(buff), - location: time.UTC, - ParsePriority: true, + buff: buff, + cursor: 0, + location: time.UTC, + l: int( + math.Min( + float64(len(buff)), + MAX_PACKET_LEN, + ), + ), } } +// Forces a priority for this parser. Priority will not be parsed. +func (p *Parser) WithPriority(pri *parsercommon.Priority) { + p.priority = pri +} + +// Forces a location. UTC will be used otherwise. +func (p *Parser) WithLocation(l *time.Location) { + p.location = l +} + +// Forces a hostname. Hostname will not be parsed +func (p *Parser) WithHostname(h string) { + p.hostname = h +} + +// Forces a tag. Tag will not be parsed +func (p *Parser) WithTag(t string) { + p.customTag = t +} + +// Forces a given time format. +// Refer to pkg/time layouts for more informations +// By default the following formats will be tried in order: +// Jan 02 15:04:05 +// Jan 2 15:04:05 +// The timezone MUST be specified using WithLocation() and +// not using WithTimestampFormat +func (p *Parser) WithTimestampFormat(s string) { + p.customTimestampFormat = s +} + +// DEPRECATED. Use WithLocation() instead func (p *Parser) Location(location *time.Location) { - p.location = location + p.WithLocation(location) } +// DEPRECATED. Use WithHostname() instead func (p *Parser) Hostname(hostname string) { - p.hostname = hostname + p.WithHostname(hostname) } func (p *Parser) Parse() error { - if p.ParsePriority { - pri, err := p.parsePriority() - if err != nil { - return err - } - p.priority = pri - } else { - p.priority = syslogparser.Priority{ - P: 0, - F: syslogparser.Facility{Value: 0}, - S: syslogparser.Severity{Value: 0}, - } + p.version = parsercommon.NO_VERSION + + pri, err := p.parsePriority() + if err != nil { + return err } + p.priority = pri + hdr, err := p.parseHeader() if err != nil { return err } + p.header = hdr + if p.buff[p.cursor] == ' ' { p.cursor++ } msg, err := p.parsemessage() - if err != syslogparser.ErrEOL { + if err != parsercommon.ErrEOL { return err } - p.version = syslogparser.NO_VERSION - p.header = hdr p.message = msg return nil } func (p *Parser) Dump() syslogparser.LogParts { - parts := syslogparser.LogParts{ + return syslogparser.LogParts{ "timestamp": p.header.timestamp, "hostname": p.header.hostname, "tag": p.message.tag, "content": p.message.content, + "priority": p.priority.P, + "facility": p.priority.F.Value, + "severity": p.priority.S.Value, } - if p.ParsePriority { - parts["priority"] = p.priority.P - parts["facility"] = p.priority.F.Value - parts["severity"] = p.priority.S.Value - } - return parts } -func (p *Parser) parsePriority() (syslogparser.Priority, error) { - return syslogparser.ParsePriority(p.buff, &p.cursor, p.l) +func (p *Parser) parsePriority() (*parsercommon.Priority, error) { + if p.priority != nil { + return p.priority, nil + } + + return parsercommon.ParsePriority( + p.buff, &p.cursor, p.l, + ) } -func (p *Parser) parseHeader() (header, error) { - hdr := header{} +// HEADER: TIMESTAMP + HOSTNAME (or IP) +// https://tools.ietf.org/html/rfc3164#section-4.1.2 +func (p *Parser) parseHeader() (*header, error) { var err error + if p.buff[p.cursor] == ' ' { + p.cursor++ + } + ts, err := p.parseTimestamp() if err != nil { - return hdr, err + return nil, err } - hostname, err := p.parseHostname() + h, err := p.parseHostname() if err != nil { - return hdr, err + return nil, err } - hdr.timestamp = ts - hdr.hostname = hostname + hdr := &header{ + timestamp: ts, + hostname: h, + } return hdr, nil } -func (p *Parser) parsemessage() (rfc3164message, error) { - msg := rfc3164message{} +// MSG: TAG + CONTENT +// https://tools.ietf.org/html/rfc3164#section-4.1.3 +func (p *Parser) parsemessage() (*message, error) { var err error tag, err := p.parseTag() if err != nil { - return msg, err + return nil, err } content, err := p.parseContent() - if err != syslogparser.ErrEOL { - return msg, err + if err != parsercommon.ErrEOL { + return nil, err } - msg.tag = tag - msg.content = content + msg := &message{ + tag: tag, + content: content, + } return msg, err } @@ -155,6 +210,12 @@ func (p *Parser) parseTimestamp() (time.Time, error) { "Jan 2 15:04:05", } + if p.customTimestampFormat != "" { + tsFmts = []string{ + p.customTimestampFormat, + } + } + found := false for _, tsFmt := range tsFmts { tsFmtLen = len(tsFmt) @@ -164,7 +225,10 @@ func (p *Parser) parseTimestamp() (time.Time, error) { } sub = p.buff[p.cursor : tsFmtLen+p.cursor] - ts, err = time.ParseInLocation(tsFmt, string(sub), p.location) + ts, err = time.ParseInLocation( + tsFmt, string(sub), p.location, + ) + if err == nil { found = true break @@ -180,7 +244,7 @@ func (p *Parser) parseTimestamp() (time.Time, error) { p.cursor++ } - return ts, syslogparser.ErrTimestampUnknownFormat + return ts, parsercommon.ErrTimestampUnknownFormat } fixTimestampIfNeeded(&ts) @@ -197,13 +261,19 @@ func (p *Parser) parseTimestamp() (time.Time, error) { func (p *Parser) parseHostname() (string, error) { if p.hostname != "" { return p.hostname, nil - } else { - return syslogparser.ParseHostname(p.buff, &p.cursor, p.l) } + + return parsercommon.ParseHostname( + p.buff, &p.cursor, p.l, + ) } // http://tools.ietf.org/html/rfc3164#section-4.1.3 func (p *Parser) parseTag() (string, error) { + if p.customTag != "" { + return p.customTag, nil + } + var b byte var endOfTag bool var bracketOpen bool @@ -246,13 +316,16 @@ func (p *Parser) parseTag() (string, error) { func (p *Parser) parseContent() (string, error) { if p.cursor > p.l { - return "", syslogparser.ErrEOL + return "", parsercommon.ErrEOL } - content := bytes.Trim(p.buff[p.cursor:p.l], " ") + content := bytes.Trim( + p.buff[p.cursor:p.l], " ", + ) + p.cursor += len(content) - return string(content), syslogparser.ErrEOL + return string(content), parsercommon.ErrEOL } func fixTimestampIfNeeded(ts *time.Time) { @@ -263,8 +336,11 @@ func fixTimestampIfNeeded(ts *time.Time) { y = now.Year() } - newTs := time.Date(y, ts.Month(), ts.Day(), ts.Hour(), ts.Minute(), - ts.Second(), ts.Nanosecond(), ts.Location()) + newTs := time.Date( + y, ts.Month(), ts.Day(), + ts.Hour(), ts.Minute(), ts.Second(), ts.Nanosecond(), + ts.Location(), + ) *ts = newTs } diff --git a/rfc3164/rfc3164_test.go b/rfc3164/rfc3164_test.go index 3dcd3ea..2b6d6ae 100644 --- a/rfc3164/rfc3164_test.go +++ b/rfc3164/rfc3164_test.go @@ -2,44 +2,47 @@ package rfc3164 import ( "bytes" + "strings" "testing" "time" "github.com/jeromer/syslogparser" - "github.com/stretchr/testify/suite" + "github.com/jeromer/syslogparser/parsercommon" + "github.com/stretchr/testify/require" ) -type RFC3164TestSuite struct { - suite.Suite -} - var ( // XXX : corresponds to the length of the last tried timestamp format // XXX : Jan 2 15:04:05 lastTriedTimestampLen = 15 ) -func (s *RFC3164TestSuite) TestParser_Valid() { +func TestParser_Valid(t *testing.T) { buff := []byte( "<34>Oct 11 22:14:15 mymachine very.large.syslog.message.tag: 'su root' failed for lonvick on /dev/pts/8", ) p := NewParser(buff) - s.Require().Equal( - p, + + require.Equal( + t, &Parser{ - buff: buff, - cursor: 0, - l: len(buff), - location: time.UTC, - ParsePriority: true, + buff: buff, + cursor: 0, + l: len(buff), + location: time.UTC, }, + p, ) err := p.Parse() - s.Require().Nil(err) - s.Require().Equal( - p.Dump(), + + require.Nil( + t, err, + ) + + require.Equal( + t, syslogparser.LogParts{ "timestamp": time.Date( time.Now().Year(), @@ -54,32 +57,40 @@ func (s *RFC3164TestSuite) TestParser_Valid() { "facility": 4, "severity": 2, }, + p.Dump(), ) } -func (s *RFC3164TestSuite) TestParser_WithoutPriority() { +func TestParser_WithPriority(t *testing.T) { buff := []byte( "Oct 11 22:14:15 mymachine very.large.syslog.message.tag: 'su root' failed for lonvick on /dev/pts/8", ) + pri := parsercommon.NewPriority(0) + p := NewParser(buff) - p.ParsePriority = false + p.WithPriority(pri) - s.Require().Equal( - p, + require.Equal( + t, &Parser{ - buff: buff, - cursor: 0, - l: len(buff), - location: time.UTC, - ParsePriority: false, + buff: buff, + cursor: 0, + l: len(buff), + location: time.UTC, + priority: pri, }, + p, ) err := p.Parse() - s.Require().Nil(err) - s.Require().Equal( - p.Dump(), + + require.Nil( + t, err, + ) + + require.Equal( + t, syslogparser.LogParts{ "timestamp": time.Date( time.Now().Year(), @@ -90,23 +101,59 @@ func (s *RFC3164TestSuite) TestParser_WithoutPriority() { "hostname": "mymachine", "tag": "very.large.syslog.message.tag", "content": "'su root' failed for lonvick on /dev/pts/8", + "priority": 0, + "facility": 0, + "severity": 0, }, + p.Dump(), ) } -func (s *RFC3164TestSuite) TestParseWithout_Hostname() { +func TestParser_WithHostname(t *testing.T) { buff := []byte( "<30>Jun 23 13:17:42 chronyd[1119]: Selected source 192.168.65.1", ) p := NewParser(buff) - p.Hostname("testhost") + p.WithHostname("dummy") err := p.Parse() - s.Require().Nil(err) + require.Nil(t, err) - s.Require().Equal( + require.Equal( + t, + syslogparser.LogParts{ + "timestamp": time.Date( + time.Now().Year(), + time.June, + 23, 13, 17, 42, 0, + time.UTC, + ), + "hostname": "dummy", + "tag": "chronyd", + "content": "Selected source 192.168.65.1", + "priority": 30, + "facility": 3, + "severity": 6, + }, p.Dump(), + ) +} + +func TestParser_WithTag(t *testing.T) { + buff := []byte( + "<30>Jun 23 13:17:42 localhost Selected source 192.168.65.1", + ) + + tag := "chronyd" + p := NewParser(buff) + p.WithTag(tag) + + err := p.Parse() + require.Nil(t, err) + + require.Equal( + t, syslogparser.LogParts{ "timestamp": time.Date( time.Now().Year(), @@ -114,45 +161,178 @@ func (s *RFC3164TestSuite) TestParseWithout_Hostname() { 23, 13, 17, 42, 0, time.UTC, ), - "hostname": "testhost", + "hostname": "localhost", "tag": "chronyd", "content": "Selected source 192.168.65.1", "priority": 30, "facility": 3, "severity": 6, }, + p.Dump(), ) } -func (s *RFC3164TestSuite) TestParseHeader() { +func TestParser_WithLocation(t *testing.T) { + buff := []byte( + "<30>Jun 23 13:17:42 localhost foo: Selected source 192.168.65.1", + ) + + loc, err := time.LoadLocation("America/New_York") + require.Nil(t, err) + + p := NewParser(buff) + p.WithLocation(loc) + + err = p.Parse() + require.Nil(t, err) + + require.Equal( + t, + syslogparser.LogParts{ + "timestamp": time.Date( + time.Now().Year(), + time.June, + 23, 13, 17, 42, 0, + loc, + ), + "hostname": "localhost", + "tag": "foo", + "content": "Selected source 192.168.65.1", + "priority": 30, + "facility": 3, + "severity": 6, + }, + p.Dump(), + ) +} + +func TestParser_WithTimestampFormat(t *testing.T) { + buff := []byte( + "<30>2006-01-02T15:04:05 localhost foo: Selected source 192.168.65.1", + ) + + p := NewParser(buff) + p.WithTimestampFormat( + "2006-01-02T15:04:05", + ) + + err := p.Parse() + require.Nil(t, err) + + require.Equal( + t, + syslogparser.LogParts{ + "timestamp": time.Date( + 2006, time.January, 2, + 15, 4, 5, 0, + time.UTC, + ), + "hostname": "localhost", + "tag": "foo", + "content": "Selected source 192.168.65.1", + "priority": 30, + "facility": 3, + "severity": 6, + }, + p.Dump(), + ) +} + +func TestParser_WithPriorityHostnameTag(t *testing.T) { + buff := []byte( + "Oct 11 22:14:15 'su root' failed for lonvick on /dev/pts/8", + ) + + pri := parsercommon.NewPriority(0) + h := "mymachine" + tag := "foo" + + p := NewParser(buff) + p.WithPriority(pri) + p.WithHostname(h) + p.WithTag(tag) + + require.Equal( + t, + &Parser{ + buff: buff, + cursor: 0, + l: len(buff), + location: time.UTC, + priority: pri, + hostname: h, + customTag: tag, + }, + p, + ) + + err := p.Parse() + + require.Nil( + t, err, + ) + + require.Equal( + t, + syslogparser.LogParts{ + "timestamp": time.Date( + time.Now().Year(), + time.October, + 11, 22, 14, 15, 0, + time.UTC, + ), + "hostname": h, + "tag": tag, + "content": "'su root' failed for lonvick on /dev/pts/8", + "priority": 0, + "facility": 0, + "severity": 0, + }, + p.Dump(), + ) +} + +func TestParseHeader(t *testing.T) { + date := time.Date( + time.Now().Year(), + time.October, + 11, 22, 14, 15, 0, + time.UTC, + ) + testCases := []struct { description string input string - expectedHdr header + expectedHdr *header expectedCursorPos int expectedErr error }{ { description: "valid headers", input: "Oct 11 22:14:15 mymachine ", - expectedHdr: header{ - hostname: "mymachine", - timestamp: time.Date( - time.Now().Year(), - time.October, - 11, 22, 14, 15, 0, - time.UTC, - ), + expectedHdr: &header{ + hostname: "mymachine", + timestamp: date, }, expectedCursorPos: 25, expectedErr: nil, }, + { + description: "valid headers with prepended space", + input: " Oct 11 22:14:15 mymachine ", + expectedHdr: &header{ + hostname: "mymachine", + timestamp: date, + }, + expectedCursorPos: 26, + expectedErr: nil, + }, { description: "invalid timestamp", input: "Oct 34 32:72:82 mymachine ", - expectedHdr: header{}, + expectedHdr: nil, expectedCursorPos: lastTriedTimestampLen + 1, - expectedErr: syslogparser.ErrTimestampUnknownFormat, + expectedErr: parsercommon.ErrTimestampUnknownFormat, }, } @@ -160,18 +340,26 @@ func (s *RFC3164TestSuite) TestParseHeader() { p := NewParser([]byte(tc.input)) obtained, err := p.parseHeader() - s.Require().Equal(err, tc.expectedErr, tc.description) - s.Require().Equal(obtained, tc.expectedHdr, tc.description) - s.Require().Equal(p.cursor, tc.expectedCursorPos, tc.description) + require.Equal( + t, tc.expectedErr, err, tc.description, + ) + + require.Equal( + t, tc.expectedHdr, obtained, tc.description, + ) + + require.Equal( + t, tc.expectedCursorPos, p.cursor, tc.description, + ) } } -func (s *RFC3164TestSuite) TestParsemessage_Valid() { +func TestParsemessage_Valid(t *testing.T) { content := "foo bar baz blah quux" buff := []byte("sometag[123]: " + content) - hdr := rfc3164message{ + msg := &message{ tag: "sometag", content: content, } @@ -179,12 +367,20 @@ func (s *RFC3164TestSuite) TestParsemessage_Valid() { p := NewParser(buff) obtained, err := p.parsemessage() - s.Require().Equal(err, syslogparser.ErrEOL) - s.Require().Equal(obtained, hdr) - s.Require().Equal(p.cursor, len(buff)) + require.Equal( + t, parsercommon.ErrEOL, err, + ) + + require.Equal( + t, msg, obtained, + ) + + require.Equal( + t, len(buff), p.cursor, + ) } -func (s *RFC3164TestSuite) TestParseTimestamp() { +func TestParseTimestamp(t *testing.T) { testCases := []struct { description string input string @@ -196,7 +392,7 @@ func (s *RFC3164TestSuite) TestParseTimestamp() { description: "invalid", input: "Oct 34 32:72:82", expectedCursorPos: lastTriedTimestampLen, - expectedErr: syslogparser.ErrTimestampUnknownFormat, + expectedErr: parsercommon.ErrTimestampUnknownFormat, }, { description: "trailing space", @@ -240,19 +436,21 @@ func (s *RFC3164TestSuite) TestParseTimestamp() { p := NewParser([]byte(tc.input)) obtained, err := p.parseTimestamp() - s.Require().Equal( - obtained, tc.expectedTS, tc.description, + require.Equal( + t, tc.expectedTS, obtained, tc.description, ) - s.Require().Equal( - p.cursor, tc.expectedCursorPos, tc.description, + + require.Equal( + t, tc.expectedCursorPos, p.cursor, tc.description, ) - s.Require().Equal( - err, tc.expectedErr, tc.description, + + require.Equal( + t, tc.expectedErr, err, tc.description, ) } } -func (s *RFC3164TestSuite) TestParseTag() { +func TestParseTag(t *testing.T) { testCases := []struct { description string input string @@ -287,30 +485,72 @@ func (s *RFC3164TestSuite) TestParseTag() { p := NewParser([]byte(tc.input)) obtained, err := p.parseTag() - s.Require().Equal( - obtained, tc.expectedTag, tc.description, + require.Equal( + t, obtained, tc.expectedTag, tc.description, ) - s.Require().Equal( - p.cursor, tc.expectedCursorPos, tc.description, + require.Equal( + t, tc.expectedCursorPos, p.cursor, tc.description, ) - s.Require().Equal( - err, tc.expectedErr, tc.description, + require.Equal( + t, tc.expectedErr, err, tc.description, ) } } -func (s *RFC3164TestSuite) TestParseContent_Valid() { +func TestParseContent_Valid(t *testing.T) { buff := []byte(" foo bar baz quux ") content := string(bytes.Trim(buff, " ")) p := NewParser(buff) obtained, err := p.parseContent() - s.Require().Equal(err, syslogparser.ErrEOL) - s.Require().Equal(obtained, content) - s.Require().Equal(p.cursor, len(content)) + require.Equal( + t, err, parsercommon.ErrEOL, + ) + + require.Equal( + t, content, obtained, + ) + + require.Equal( + t, len(content), p.cursor, + ) +} + +func TestParseMessageSizeChecks(t *testing.T) { + start := "<34>Oct 11 22:14:15 mymachine su: " + msg := start + strings.Repeat("a", MAX_PACKET_LEN) + + p := NewParser([]byte(msg)) + err := p.Parse() + fields := p.Dump() + + require.Nil( + t, err, + ) + + require.Len( + t, + fields["content"], + MAX_PACKET_LEN-len(start), + ) + + // --- + + msg = start + "hello" + p = NewParser([]byte(msg)) + err = p.Parse() + fields = p.Dump() + + require.Nil( + t, err, + ) + + require.Equal( + t, "hello", fields["content"], + ) } func BenchmarkParseTimestamp(b *testing.B) { @@ -380,7 +620,7 @@ func BenchmarkParsemessage(b *testing.B) { for i := 0; i < b.N; i++ { _, err := p.parsemessage() - if err != syslogparser.ErrEOL { + if err != parsercommon.ErrEOL { panic(err) } @@ -391,21 +631,14 @@ func BenchmarkParsemessage(b *testing.B) { func BenchmarkParseFull(b *testing.B) { msg := "<34>Oct 11 22:14:15 mymachine su: 'su root' failed for lonvick on /dev/pts/8" - p := NewParser([]byte(msg)) - for i := 0; i < b.N; i++ { - _, err := p.parsemessage() - if err != syslogparser.ErrEOL { + p := NewParser( + []byte(msg), + ) + + err := p.Parse() + if err != nil { panic(err) } - - p.cursor = 0 } - -} - -func TestRFC3164TestSuite(t *testing.T) { - suite.Run( - t, new(RFC3164TestSuite), - ) } diff --git a/rfc5424/example_test.go b/rfc5424/example_test.go index ed3600a..8a0eec7 100644 --- a/rfc5424/example_test.go +++ b/rfc5424/example_test.go @@ -16,5 +16,7 @@ func ExampleNewParser() { panic(err) } - fmt.Println(p.Dump()) + for k, v := range p.Dump() { + fmt.Println(k, ":", v) + } } diff --git a/rfc5424/rfc5424.go b/rfc5424/rfc5424.go index ed5a09d..dbee9f4 100644 --- a/rfc5424/rfc5424.go +++ b/rfc5424/rfc5424.go @@ -1,45 +1,55 @@ package rfc5424 import ( + "bytes" "fmt" "math" "strconv" "time" "github.com/jeromer/syslogparser" + "github.com/jeromer/syslogparser/parsercommon" ) const ( NILVALUE = '-' + + // according to https://tools.ietf.org/html/rfc5424#section-6.1 + // the length of the packet MUST be 2048 bytes or less. + // However we will accept a bit more while protecting from exhaustion + MAX_PACKET_LEN = 3048 ) var ( - ErrYearInvalid = &syslogparser.ParserError{ErrorString: "Invalid year in timestamp"} - ErrMonthInvalid = &syslogparser.ParserError{ErrorString: "Invalid month in timestamp"} - ErrDayInvalid = &syslogparser.ParserError{ErrorString: "Invalid day in timestamp"} - ErrHourInvalid = &syslogparser.ParserError{ErrorString: "Invalid hour in timestamp"} - ErrMinuteInvalid = &syslogparser.ParserError{ErrorString: "Invalid minute in timestamp"} - ErrSecondInvalid = &syslogparser.ParserError{ErrorString: "Invalid second in timestamp"} - ErrSecFracInvalid = &syslogparser.ParserError{ErrorString: "Invalid fraction of second in timestamp"} - ErrTimeZoneInvalid = &syslogparser.ParserError{ErrorString: "Invalid time zone in timestamp"} - ErrInvalidTimeFormat = &syslogparser.ParserError{ErrorString: "Invalid time format"} - ErrInvalidAppName = &syslogparser.ParserError{ErrorString: "Invalid app name"} - ErrInvalidProcId = &syslogparser.ParserError{ErrorString: "Invalid proc ID"} - ErrInvalidMsgId = &syslogparser.ParserError{ErrorString: "Invalid msg ID"} - ErrNoStructuredData = &syslogparser.ParserError{ErrorString: "No structured data"} + ErrYearInvalid = &parsercommon.ParserError{ErrorString: "Invalid year in timestamp"} + ErrMonthInvalid = &parsercommon.ParserError{ErrorString: "Invalid month in timestamp"} + ErrDayInvalid = &parsercommon.ParserError{ErrorString: "Invalid day in timestamp"} + ErrHourInvalid = &parsercommon.ParserError{ErrorString: "Invalid hour in timestamp"} + ErrMinuteInvalid = &parsercommon.ParserError{ErrorString: "Invalid minute in timestamp"} + ErrSecondInvalid = &parsercommon.ParserError{ErrorString: "Invalid second in timestamp"} + ErrSecFracInvalid = &parsercommon.ParserError{ErrorString: "Invalid fraction of second in timestamp"} + ErrTimeZoneInvalid = &parsercommon.ParserError{ErrorString: "Invalid time zone in timestamp"} + ErrInvalidTimeFormat = &parsercommon.ParserError{ErrorString: "Invalid time format"} + ErrInvalidAppName = &parsercommon.ParserError{ErrorString: "Invalid app name"} + ErrInvalidProcId = &parsercommon.ParserError{ErrorString: "Invalid proc ID"} + ErrInvalidMsgId = &parsercommon.ParserError{ErrorString: "Invalid msg ID"} + ErrNoStructuredData = &parsercommon.ParserError{ErrorString: "No structured data"} ) type Parser struct { buff []byte cursor int l int - header header + header *header structuredData string message string + + tmpHostname string + tmpPriority *parsercommon.Priority } type header struct { - priority syslogparser.Priority + priority *parsercommon.Priority version int timestamp time.Time hostname string @@ -56,7 +66,7 @@ type partialTime struct { } type fullTime struct { - pt partialTime + pt *partialTime loc *time.Location } @@ -70,12 +80,39 @@ func NewParser(buff []byte) *Parser { return &Parser{ buff: buff, cursor: 0, - l: len(buff), + l: int( + math.Min( + float64(len(buff)), + MAX_PACKET_LEN, + ), + ), } } +// Forces a priority for this parser. Priority will not be parsed. +func (p *Parser) WithPriority(pri *parsercommon.Priority) { + p.tmpPriority = pri +} + +// Noop as RFC5424 syslog always has a timezone +func (p *Parser) WithLocation(l *time.Location) {} + +// Noop as RFC5424 is strict about timestamp format +func (p *Parser) WithTimestampFormat(s string) {} + +// Forces a hostname. Hostname will not be parsed +func (p *Parser) WithHostname(h string) { + p.tmpHostname = h +} + +// Noop as RFC5424 as no tag per se: +// The TAG has been split into APP-NAME, PROCID, and MSGID. +// Ref: https://tools.ietf.org/html/rfc5424#appendix-A.1 +func (p *Parser) WithTag(t string) { +} + +// DEPRECATED. Use WithLocation() instead func (p *Parser) Location(location *time.Location) { - // Ignore as RFC5424 syslog always has a timezone } func (p *Parser) Parse() error { @@ -95,7 +132,11 @@ func (p *Parser) Parse() error { p.cursor++ if p.cursor < p.l { - p.message = string(p.buff[p.cursor:]) + p.message = string( + bytes.Trim( + p.buff[p.cursor:p.l], " ", + ), + ) } return nil @@ -118,105 +159,119 @@ func (p *Parser) Dump() syslogparser.LogParts { } // HEADER = PRI VERSION SP TIMESTAMP SP HOSTNAME SP APP-NAME SP PROCID SP MSGID -func (p *Parser) parseHeader() (header, error) { - hdr := header{} - +func (p *Parser) parseHeader() (*header, error) { pri, err := p.parsePriority() if err != nil { - return hdr, err + return nil, err } - hdr.priority = pri - ver, err := p.parseVersion() if err != nil { - return hdr, err + return nil, err } - hdr.version = ver + p.cursor++ ts, err := p.parseTimestamp() if err != nil { - return hdr, err + return nil, err } - hdr.timestamp = ts p.cursor++ host, err := p.parseHostname() if err != nil { - return hdr, err + return nil, err } - hdr.hostname = host - p.cursor++ + // cursor is moved in p.parseHostname() appName, err := p.parseAppName() if err != nil { - return hdr, err + return nil, err } - hdr.appName = appName p.cursor++ procId, err := p.parseProcId() if err != nil { - return hdr, nil + return nil, err } - hdr.procId = procId p.cursor++ msgId, err := p.parseMsgId() if err != nil { - return hdr, nil + return nil, err } - hdr.msgId = msgId p.cursor++ + hdr := &header{ + version: ver, + timestamp: *ts, + priority: pri, + hostname: host, + procId: procId, + msgId: msgId, + appName: appName, + } + return hdr, nil } -func (p *Parser) parsePriority() (syslogparser.Priority, error) { - return syslogparser.ParsePriority(p.buff, &p.cursor, p.l) +func (p *Parser) parsePriority() (*parsercommon.Priority, error) { + if p.tmpPriority != nil { + return p.tmpPriority, nil + } + + return parsercommon.ParsePriority( + p.buff, &p.cursor, p.l, + ) } func (p *Parser) parseVersion() (int, error) { - return syslogparser.ParseVersion(p.buff, &p.cursor, p.l) + return parsercommon.ParseVersion(p.buff, &p.cursor, p.l) } // https://tools.ietf.org/html/rfc5424#section-6.2.3 -func (p *Parser) parseTimestamp() (time.Time, error) { - var ts time.Time - +func (p *Parser) parseTimestamp() (*time.Time, error) { if p.buff[p.cursor] == NILVALUE { p.cursor++ - return ts, nil + return new(time.Time), nil } - fd, err := parseFullDate(p.buff, &p.cursor, p.l) + fd, err := parseFullDate( + p.buff, &p.cursor, p.l, + ) + if err != nil { - return ts, err + return nil, err } if p.buff[p.cursor] != 'T' { - return ts, ErrInvalidTimeFormat + return nil, ErrInvalidTimeFormat } p.cursor++ - ft, err := parseFullTime(p.buff, &p.cursor, p.l) + ft, err := parseFullTime( + p.buff, &p.cursor, p.l, + ) + if err != nil { - return ts, syslogparser.ErrTimestampUnknownFormat + return nil, parsercommon.ErrTimestampUnknownFormat } - nSec, err := toNSec(ft.pt.secFrac) + nSec, err := toNSec( + ft.pt.secFrac, + ) + if err != nil { - return ts, err + return nil, err } - ts = time.Date( + ts := time.Date( fd.year, time.Month(fd.month), fd.day, @@ -227,12 +282,20 @@ func (p *Parser) parseTimestamp() (time.Time, error) { ft.loc, ) - return ts, nil + return &ts, nil } // HOSTNAME = NILVALUE / 1*255PRINTUSASCII func (p *Parser) parseHostname() (string, error) { - return syslogparser.ParseHostname(p.buff, &p.cursor, p.l) + if p.tmpHostname != "" { + return p.tmpHostname, nil + } + + h, err := parsercommon.ParseHostname(p.buff, &p.cursor, p.l) + + p.cursor++ + + return h, err } // APP-NAME = NILVALUE / 1*48PRINTUSASCII @@ -247,7 +310,9 @@ func (p *Parser) parseProcId() (string, error) { // MSGID = NILVALUE / 1*32PRINTUSASCII func (p *Parser) parseMsgId() (string, error) { - return parseUpToLen(p.buff, &p.cursor, p.l, 32, ErrInvalidMsgId) + return parseUpToLen( + p.buff, &p.cursor, p.l, 32, ErrInvalidMsgId, + ) } func (p *Parser) parseStructuredData() (string, error) { @@ -270,7 +335,7 @@ func parseFullDate(buff []byte, cursor *int, l int) (fullDate, error) { } if buff[*cursor] != '-' { - return fd, syslogparser.ErrTimestampUnknownFormat + return fd, parsercommon.ErrTimestampUnknownFormat } *cursor++ @@ -281,7 +346,7 @@ func parseFullDate(buff []byte, cursor *int, l int) (fullDate, error) { } if buff[*cursor] != '-' { - return fd, syslogparser.ErrTimestampUnknownFormat + return fd, parsercommon.ErrTimestampUnknownFormat } *cursor++ @@ -305,7 +370,7 @@ func parseYear(buff []byte, cursor *int, l int) (int, error) { yearLen := 4 if *cursor+yearLen > l { - return 0, syslogparser.ErrEOL + return 0, parsercommon.ErrEOL } // XXX : we do not check for a valid year (ie. 1999, 2013 etc) @@ -324,7 +389,7 @@ func parseYear(buff []byte, cursor *int, l int) (int, error) { // DATE-MONTH = 2DIGIT ; 01-12 func parseMonth(buff []byte, cursor *int, l int) (int, error) { - return syslogparser.Parse2Digits(buff, cursor, l, 1, 12, ErrMonthInvalid) + return parsercommon.Parse2Digits(buff, cursor, l, 1, 12, ErrMonthInvalid) } // DATE-MDAY = 2DIGIT ; 01-28, 01-29, 01-30, 01-31 based on month/year @@ -333,24 +398,22 @@ func parseDay(buff []byte, cursor *int, l int) (int, error) { // XXX : we do not check if valid regarding February or leap years // XXX : we only checks that day is in range [01 -> 31] // XXX : in other words this function will not rant if you provide Feb 31th - return syslogparser.Parse2Digits(buff, cursor, l, 1, 31, ErrDayInvalid) + return parsercommon.Parse2Digits(buff, cursor, l, 1, 31, ErrDayInvalid) } // FULL-TIME = PARTIAL-TIME TIME-OFFSET -func parseFullTime(buff []byte, cursor *int, l int) (fullTime, error) { - var ft fullTime - +func parseFullTime(buff []byte, cursor *int, l int) (*fullTime, error) { pt, err := parsePartialTime(buff, cursor, l) if err != nil { - return ft, err + return nil, err } loc, err := parseTimeOffset(buff, cursor, l) if err != nil { - return ft, err + return nil, err } - ft = fullTime{ + ft := &fullTime{ pt: pt, loc: loc, } @@ -359,28 +422,32 @@ func parseFullTime(buff []byte, cursor *int, l int) (fullTime, error) { } // PARTIAL-TIME = TIME-HOUR ":" TIME-MINUTE ":" TIME-SECOND[TIME-SECFRAC] -func parsePartialTime(buff []byte, cursor *int, l int) (partialTime, error) { - var pt partialTime +func parsePartialTime(buff []byte, cursor *int, l int) (*partialTime, error) { + hour, minute, err := getHourMinute( + buff, cursor, l, + ) - hour, minute, err := getHourMinute(buff, cursor, l) if err != nil { - return pt, err + return nil, err } if buff[*cursor] != ':' { - return pt, ErrInvalidTimeFormat + return nil, ErrInvalidTimeFormat } *cursor++ // ---- - seconds, err := parseSecond(buff, cursor, l) + seconds, err := parseSecond( + buff, cursor, l, + ) + if err != nil { - return pt, err + return nil, err } - pt = partialTime{ + pt := &partialTime{ hour: hour, minute: minute, seconds: seconds, @@ -394,10 +461,14 @@ func parsePartialTime(buff []byte, cursor *int, l int) (partialTime, error) { *cursor++ - secFrac, err := parseSecFrac(buff, cursor, l) + secFrac, err := parseSecFrac( + buff, cursor, l, + ) + if err != nil { return pt, nil } + pt.secFrac = secFrac return pt, nil @@ -405,17 +476,17 @@ func parsePartialTime(buff []byte, cursor *int, l int) (partialTime, error) { // TIME-HOUR = 2DIGIT ; 00-23 func parseHour(buff []byte, cursor *int, l int) (int, error) { - return syslogparser.Parse2Digits(buff, cursor, l, 0, 23, ErrHourInvalid) + return parsercommon.Parse2Digits(buff, cursor, l, 0, 23, ErrHourInvalid) } // TIME-MINUTE = 2DIGIT ; 00-59 func parseMinute(buff []byte, cursor *int, l int) (int, error) { - return syslogparser.Parse2Digits(buff, cursor, l, 0, 59, ErrMinuteInvalid) + return parsercommon.Parse2Digits(buff, cursor, l, 0, 59, ErrMinuteInvalid) } // TIME-SECOND = 2DIGIT ; 00-59 func parseSecond(buff []byte, cursor *int, l int) (int, error) { - return syslogparser.Parse2Digits(buff, cursor, l, 0, 59, ErrSecondInvalid) + return parsercommon.Parse2Digits(buff, cursor, l, 0, 59, ErrSecondInvalid) } // TIME-SECFRAC = "." 1*6DIGIT @@ -432,7 +503,7 @@ func parseSecFrac(buff []byte, cursor *int, l int) (float64, error) { } c := buff[to] - if !syslogparser.IsDigit(c) { + if !parsercommon.IsDigit(c) { break } } diff --git a/rfc5424/rfc5424_test.go b/rfc5424/rfc5424_test.go index 20daf47..6fd0f7d 100644 --- a/rfc5424/rfc5424_test.go +++ b/rfc5424/rfc5424_test.go @@ -7,16 +7,13 @@ import ( "time" "github.com/jeromer/syslogparser" - "github.com/stretchr/testify/suite" + "github.com/jeromer/syslogparser/parsercommon" + "github.com/stretchr/testify/require" ) -type RFC5424TestSuite struct { - suite.Suite -} - -func (s *RFC5424TestSuite) TestParser() { +func TestParser(t *testing.T) { tmpTZ, err := time.Parse("-07:00", "-07:00") - s.Require().Nil(err) + require.Nil(t, err) testCases := []struct { description string @@ -113,29 +110,165 @@ func (s *RFC5424TestSuite) TestParser() { buff := []byte(tc.input) p := NewParser(buff) - s.Require().Equal( - p, + require.Equal( + t, &Parser{ buff: buff, cursor: 0, l: len(tc.input), }, + p, tc.description, ) err := p.Parse() - s.Require().Nil(err) + require.Nil(t, err) obtained := p.Dump() for k, v := range obtained { - s.Require().Equal( - v, tc.expectedParts[k], tc.description, + require.Equal( + t, tc.expectedParts[k], v, tc.description, ) } } } -func (s *RFC5424TestSuite) TestParseHeader() { +func TestParseWithHostname(t *testing.T) { + buff := []byte( + "<34>1 2003-10-11T22:14:15.003Z su - ID47 - 'su root' failed for lonvick on /dev/pts/8", + ) + + p := NewParser(buff) + p.WithHostname("mymachine.example.com") + + require.Equal( + t, + &Parser{ + buff: buff, + cursor: 0, + l: len(buff), + tmpHostname: "mymachine.example.com", + }, + p, + ) + + err := p.Parse() + require.Nil(t, err) + + require.Equal( + t, syslogparser.LogParts{ + "priority": 34, + "facility": 4, + "severity": 2, + "version": 1, + "timestamp": time.Date( + 2003, time.October, 11, + 22, 14, 15, 3*10e5, + time.UTC, + ), + "hostname": "mymachine.example.com", + "app_name": "su", + "proc_id": "-", + "msg_id": "ID47", + "structured_data": "-", + "message": "'su root' failed for lonvick on /dev/pts/8", + }, p.Dump(), + ) +} + +func TestParseWithPriority(t *testing.T) { + buff := []byte( + "1 2003-10-11T22:14:15.003Z mymachine.example.com su - ID47 - 'su root' failed for lonvick on /dev/pts/8", + ) + + pri := parsercommon.NewPriority(34) + + p := NewParser(buff) + p.WithPriority(pri) + + require.Equal( + t, + &Parser{ + buff: buff, + cursor: 0, + l: len(buff), + tmpPriority: pri, + }, + p, + ) + + err := p.Parse() + require.Nil(t, err) + + require.Equal( + t, syslogparser.LogParts{ + "priority": 34, + "facility": 4, + "severity": 2, + "version": 1, + "timestamp": time.Date( + 2003, time.October, 11, + 22, 14, 15, 3*10e5, + time.UTC, + ), + "hostname": "mymachine.example.com", + "app_name": "su", + "proc_id": "-", + "msg_id": "ID47", + "structured_data": "-", + "message": "'su root' failed for lonvick on /dev/pts/8", + }, p.Dump(), + ) +} + +func TestParseWithPriorityAndHostname(t *testing.T) { + buff := []byte( + "1 2003-10-11T22:14:15.003Z su - ID47 - 'su root' failed for lonvick on /dev/pts/8", + ) + + pri := parsercommon.NewPriority(34) + + p := NewParser(buff) + p.WithPriority(pri) + p.WithHostname("mymachine.example.com") + + require.Equal( + t, + &Parser{ + buff: buff, + cursor: 0, + l: len(buff), + tmpHostname: "mymachine.example.com", + tmpPriority: pri, + }, + p, + ) + + err := p.Parse() + require.Nil(t, err) + + require.Equal( + t, syslogparser.LogParts{ + "priority": 34, + "facility": 4, + "severity": 2, + "version": 1, + "timestamp": time.Date( + 2003, time.October, 11, + 22, 14, 15, 3*10e5, + time.UTC, + ), + "hostname": "mymachine.example.com", + "app_name": "su", + "proc_id": "-", + "msg_id": "ID47", + "structured_data": "-", + "message": "'su root' failed for lonvick on /dev/pts/8", + }, p.Dump(), + ) +} + +func TestParseHeader(t *testing.T) { ts := time.Date(2003, time.October, 11, 22, 14, 15, 3*10e5, time.UTC) tsString := "2003-10-11T22:14:15.003Z" hostname := "mymachine.example.com" @@ -144,20 +277,20 @@ func (s *RFC5424TestSuite) TestParseHeader() { msgId := "ID47" nilValue := string(NILVALUE) headerFmt := "<165>1 %s %s %s %s %s " - pri := syslogparser.Priority{ + pri := &parsercommon.Priority{ P: 165, - F: syslogparser.Facility{Value: 20}, - S: syslogparser.Severity{Value: 5}, + F: parsercommon.Facility{Value: 20}, + S: parsercommon.Severity{Value: 5}, } testCases := []struct { description string input string - expectedHdr header + expectedHdr *header }{ { description: "HEADER complete", input: fmt.Sprintf(headerFmt, tsString, hostname, appName, procId, msgId), - expectedHdr: header{ + expectedHdr: &header{ priority: pri, version: 1, timestamp: ts, @@ -170,10 +303,10 @@ func (s *RFC5424TestSuite) TestParseHeader() { { description: "TIMESTAMP as NILVALUE", input: fmt.Sprintf(headerFmt, nilValue, hostname, appName, procId, msgId), - expectedHdr: header{ + expectedHdr: &header{ priority: pri, version: 1, - timestamp: *new(time.Time), + timestamp: time.Time{}, hostname: hostname, appName: appName, procId: procId, @@ -183,7 +316,7 @@ func (s *RFC5424TestSuite) TestParseHeader() { { description: "HOSTNAME as NILVALUE", input: fmt.Sprintf(headerFmt, tsString, nilValue, appName, procId, msgId), - expectedHdr: header{ + expectedHdr: &header{ priority: pri, version: 1, timestamp: ts, @@ -196,7 +329,7 @@ func (s *RFC5424TestSuite) TestParseHeader() { { description: "APP-NAME as NILVALUE", input: fmt.Sprintf(headerFmt, tsString, hostname, nilValue, procId, msgId), - expectedHdr: header{ + expectedHdr: &header{ priority: pri, version: 1, timestamp: ts, @@ -209,7 +342,7 @@ func (s *RFC5424TestSuite) TestParseHeader() { { description: "PROCID as NILVALUE", input: fmt.Sprintf(headerFmt, tsString, hostname, appName, nilValue, msgId), - expectedHdr: header{ + expectedHdr: &header{ priority: pri, version: 1, timestamp: ts, @@ -222,7 +355,7 @@ func (s *RFC5424TestSuite) TestParseHeader() { { description: "MSGID as NILVALUE", input: fmt.Sprintf(headerFmt, tsString, hostname, appName, procId, nilValue), - expectedHdr: header{ + expectedHdr: &header{ priority: pri, version: 1, timestamp: ts, @@ -238,74 +371,81 @@ func (s *RFC5424TestSuite) TestParseHeader() { p := NewParser([]byte(tc.input)) obtained, err := p.parseHeader() - s.Require().Nil( - err, tc.description, + require.Nil( + t, err, tc.description, ) - s.Require().Equal( - obtained, tc.expectedHdr, tc.description, + require.Equal( + t, tc.expectedHdr, obtained, tc.description, ) - s.Require().Equal( - p.cursor, len(tc.input), tc.description, + require.Equal( + t, len(tc.input), p.cursor, tc.description, ) } } -func (s *RFC5424TestSuite) TestParseTimestamp() { +func TestParseTimestamp(t *testing.T) { tz := "-04:00" tmpTZ, err := time.Parse("-07:00", tz) - s.Require().Nil(err) - s.Require().NotNil(tmpTZ) + require.Nil(t, err) + require.NotNil(t, tmpTZ) + + dt1 := time.Date( + 1985, time.April, 12, + 23, 20, 50, 52*10e6, + time.UTC, + ) + dt2 := time.Date( + 1985, time.April, 12, + 19, 20, 50, 52*10e6, + tmpTZ.Location(), + ) + + dt3 := time.Date( + 2003, time.October, 11, + 22, 14, 15, 3*10e5, + time.UTC, + ) + + dt4 := time.Date( + 2003, time.August, 24, + 5, 14, 15, 3*10e2, + tmpTZ.Location(), + ) testCases := []struct { description string input string - expectedTS time.Time + expectedTS *time.Time expectedCursorPos int expectedErr error }{ { - description: "UTC timestamp", - input: "1985-04-12T23:20:50.52Z", - expectedTS: time.Date( - 1985, time.April, 12, - 23, 20, 50, 52*10e6, - time.UTC, - ), + description: "UTC timestamp", + input: "1985-04-12T23:20:50.52Z", + expectedTS: &dt1, expectedCursorPos: 23, expectedErr: nil, }, { - description: "numeric timezone", - input: "1985-04-12T19:20:50.52" + tz, - expectedTS: time.Date( - 1985, time.April, 12, - 19, 20, 50, 52*10e6, - tmpTZ.Location(), - ), + description: "numeric timezone", + input: "1985-04-12T19:20:50.52" + tz, + expectedTS: &dt2, expectedCursorPos: 28, expectedErr: nil, }, { - description: "timestamp with ms", - input: "2003-10-11T22:14:15.003Z", - expectedTS: time.Date( - 2003, time.October, 11, - 22, 14, 15, 3*10e5, - time.UTC, - ), + description: "timestamp with ms", + input: "2003-10-11T22:14:15.003Z", + expectedTS: &dt3, expectedCursorPos: 24, expectedErr: nil, }, { - description: "timestamp with us", - input: "2003-08-24T05:14:15.000003" + tz, - expectedTS: time.Date( - 2003, time.August, 24, - 5, 14, 15, 3*10e2, - tmpTZ.Location(), - ), + description: "timestamp with us", + input: "2003-08-24T05:14:15.000003" + tz, + expectedTS: &dt4, expectedCursorPos: 32, expectedErr: nil, }, @@ -313,12 +453,14 @@ func (s *RFC5424TestSuite) TestParseTimestamp() { description: "timestamp with ns", input: "2003-08-24T05:14:15.000000003-07:00", expectedCursorPos: 26, - expectedErr: syslogparser.ErrTimestampUnknownFormat, + expectedTS: nil, + expectedErr: parsercommon.ErrTimestampUnknownFormat, }, { description: "nil timestamp", input: "-", expectedCursorPos: 1, + expectedTS: nil, expectedErr: nil, }, } @@ -327,24 +469,40 @@ func (s *RFC5424TestSuite) TestParseTimestamp() { p := NewParser([]byte(tc.input)) obtained, err := p.parseTimestamp() - s.Require().Equal( - err, tc.expectedErr, tc.description, + require.Equal( + t, tc.expectedErr, err, tc.description, ) - tfmt := time.RFC3339Nano - s.Require().Equal( - obtained.Format(tfmt), - tc.expectedTS.Format(tfmt), + require.Equal( + t, + tc.expectedCursorPos, + p.cursor, tc.description, ) - s.Require().Equal( - p.cursor, tc.expectedCursorPos, tc.description, + if tc.expectedErr != nil { + require.Nil( + t, obtained, tc.description, + ) + + continue + } + + if tc.description == "nil timestamp" { + continue + } + + tfmt := time.RFC3339Nano + require.Equal( + t, + tc.expectedTS.Format(tfmt), + obtained.Format(tfmt), + tc.description, ) } } -func (s *RFC5424TestSuite) TestParseYear() { +func TestParseYear(t *testing.T) { testCases := []struct { description string input string @@ -364,7 +522,7 @@ func (s *RFC5424TestSuite) TestParseYear() { input: "123", expectedYear: 0, expectedCursorPos: 0, - expectedErr: syslogparser.ErrEOL, + expectedErr: parsercommon.ErrEOL, }, { description: "valid", @@ -383,19 +541,21 @@ func (s *RFC5424TestSuite) TestParseYear() { len(tc.input), ) - s.Require().Equal( - obtained, tc.expectedYear, tc.description, + require.Equal( + t, tc.expectedYear, obtained, tc.description, ) - s.Require().Equal( - err, tc.expectedErr, tc.description, + + require.Equal( + t, tc.expectedErr, err, tc.description, ) - s.Require().Equal( - cursor, tc.expectedCursorPos, tc.description, + + require.Equal( + t, tc.expectedCursorPos, cursor, tc.description, ) } } -func (s *RFC5424TestSuite) TestParseMonth() { +func TestParseMonth(t *testing.T) { testCases := []struct { description string input string @@ -429,7 +589,7 @@ func (s *RFC5424TestSuite) TestParseMonth() { input: "1", expectedMonth: 0, expectedCursorPos: 0, - expectedErr: syslogparser.ErrEOL, + expectedErr: parsercommon.ErrEOL, }, { description: "valid", @@ -448,19 +608,21 @@ func (s *RFC5424TestSuite) TestParseMonth() { len(tc.input), ) - s.Require().Equal( - obtained, tc.expectedMonth, tc.description, + require.Equal( + t, tc.expectedMonth, obtained, tc.description, ) - s.Require().Equal( - err, tc.expectedErr, tc.description, + + require.Equal( + t, tc.expectedErr, err, tc.description, ) - s.Require().Equal( - cursor, tc.expectedCursorPos, tc.description, + + require.Equal( + t, tc.expectedCursorPos, cursor, tc.description, ) } } -func (s *RFC5424TestSuite) TestParseDay() { +func TestParseDay(t *testing.T) { testCases := []struct { description string input string @@ -480,7 +642,7 @@ func (s *RFC5424TestSuite) TestParseDay() { input: "1", expectedDay: 0, expectedCursorPos: 0, - expectedErr: syslogparser.ErrEOL, + expectedErr: parsercommon.ErrEOL, }, { description: "invalid range 1/2", @@ -513,19 +675,21 @@ func (s *RFC5424TestSuite) TestParseDay() { len(tc.input), ) - s.Require().Equal( - obtained, tc.expectedDay, tc.description, + require.Equal( + t, tc.expectedDay, obtained, tc.description, ) - s.Require().Equal( - err, tc.expectedErr, tc.description, + + require.Equal( + t, tc.expectedErr, err, tc.description, ) - s.Require().Equal( - cursor, tc.expectedCursorPos, tc.description, + + require.Equal( + t, tc.expectedCursorPos, cursor, tc.description, ) } } -func (s *RFC5424TestSuite) TestParseFullDate() { +func TestParseFullDate(t *testing.T) { testCases := []struct { description string input string @@ -538,14 +702,14 @@ func (s *RFC5424TestSuite) TestParseFullDate() { input: "2013+10-28", expectedDate: fullDate{}, expectedCursorPos: 4, - expectedErr: syslogparser.ErrTimestampUnknownFormat, + expectedErr: parsercommon.ErrTimestampUnknownFormat, }, { description: "invalid separator 2/2", input: "2013-10+28", expectedDate: fullDate{}, expectedCursorPos: 7, - expectedErr: syslogparser.ErrTimestampUnknownFormat, + expectedErr: parsercommon.ErrTimestampUnknownFormat, }, { description: "valid", @@ -564,19 +728,21 @@ func (s *RFC5424TestSuite) TestParseFullDate() { len(tc.input), ) - s.Require().Equal( - err, tc.expectedErr, tc.description, + require.Equal( + t, tc.expectedErr, err, tc.description, ) - s.Require().Equal( - obtained, tc.expectedDate, tc.description, + + require.Equal( + t, tc.expectedDate, obtained, tc.description, ) - s.Require().Equal( - cursor, tc.expectedCursorPos, tc.description, + + require.Equal( + t, tc.expectedCursorPos, cursor, tc.description, ) } } -func (s *RFC5424TestSuite) TestParseHour() { +func TestParseHour(t *testing.T) { testCases := []struct { description string input string @@ -596,7 +762,7 @@ func (s *RFC5424TestSuite) TestParseHour() { input: "1", expectedHour: 0, expectedCursorPos: 0, - expectedErr: syslogparser.ErrEOL, + expectedErr: parsercommon.ErrEOL, }, { description: "invalid range 1/2", @@ -629,19 +795,21 @@ func (s *RFC5424TestSuite) TestParseHour() { len(tc.input), ) - s.Require().Equal( - obtained, tc.expectedHour, tc.description, + require.Equal( + t, tc.expectedHour, obtained, tc.description, ) - s.Require().Equal( - err, tc.expectedErr, tc.description, + + require.Equal( + t, tc.expectedErr, err, tc.description, ) - s.Require().Equal( - cursor, tc.expectedCursorPos, tc.description, + + require.Equal( + t, tc.expectedCursorPos, cursor, tc.description, ) } } -func (s *RFC5424TestSuite) TestParseMinute() { +func TestParseMinute(t *testing.T) { testCases := []struct { description string input string @@ -661,7 +829,7 @@ func (s *RFC5424TestSuite) TestParseMinute() { input: "1", expectedMinute: 0, expectedCursorPos: 0, - expectedErr: syslogparser.ErrEOL, + expectedErr: parsercommon.ErrEOL, }, { description: "invalid range 1/2", @@ -694,19 +862,21 @@ func (s *RFC5424TestSuite) TestParseMinute() { len(tc.input), ) - s.Require().Equal( - obtained, tc.expectedMinute, tc.description, + require.Equal( + t, tc.expectedMinute, obtained, tc.description, ) - s.Require().Equal( - err, tc.expectedErr, tc.description, + + require.Equal( + t, tc.expectedErr, err, tc.description, ) - s.Require().Equal( - cursor, tc.expectedCursorPos, tc.description, + + require.Equal( + t, tc.expectedCursorPos, cursor, tc.description, ) } } -func (s *RFC5424TestSuite) TestParseSecond() { +func TestParseSecond(t *testing.T) { testCases := []struct { description string input string @@ -726,7 +896,7 @@ func (s *RFC5424TestSuite) TestParseSecond() { input: "1", expectedSecond: 0, expectedCursorPos: 0, - expectedErr: syslogparser.ErrEOL, + expectedErr: parsercommon.ErrEOL, }, { description: "invalid range 1/2", @@ -759,19 +929,21 @@ func (s *RFC5424TestSuite) TestParseSecond() { len(tc.input), ) - s.Require().Equal( - obtained, tc.expectedSecond, tc.description, + require.Equal( + t, tc.expectedSecond, obtained, tc.description, ) - s.Require().Equal( - err, tc.expectedErr, tc.description, + + require.Equal( + t, tc.expectedErr, err, tc.description, ) - s.Require().Equal( - cursor, tc.expectedCursorPos, tc.description, + + require.Equal( + t, tc.expectedCursorPos, cursor, tc.description, ) } } -func (s *RFC5424TestSuite) TestParseSecFrac() { +func TestParseSecFrac(t *testing.T) { testCases := []struct { description string input string @@ -831,46 +1003,54 @@ func (s *RFC5424TestSuite) TestParseSecFrac() { len(tc.input), ) - s.Require().Equal( - obtained, tc.expectedSecFrac, tc.description, + require.Equal( + t, tc.expectedSecFrac, obtained, tc.description, ) - s.Require().Equal( - err, tc.expectedErr, tc.description, + + require.Equal( + t, tc.expectedErr, err, tc.description, ) - s.Require().Equal( - cursor, tc.expectedCursorPos, tc.description, + + require.Equal( + t, tc.expectedCursorPos, cursor, tc.description, ) } } -func (s *RFC5424TestSuite) TestParseNumericalTimeOffset() { +func TestParseNumericalTimeOffset(t *testing.T) { buff := []byte("+02:00") cursor := 0 l := len(buff) + tmpTs, err := time.Parse("-07:00", string(buff)) - s.Require().Nil(err) + require.Nil(t, err) + + obtained, err := parseNumericalTimeOffset( + buff, &cursor, l, + ) - obtained, err := parseNumericalTimeOffset(buff, &cursor, l) - s.Require().Nil(err) + require.Nil(t, err) expected := tmpTs.Location() - s.Require().Equal(obtained, expected) - - s.Require().Equal(cursor, 6) + require.Equal(t, expected, obtained) + require.Equal(t, 6, cursor) } -func (s *RFC5424TestSuite) TestParseTimeOffset() { +func TestParseTimeOffset(t *testing.T) { buff := []byte("Z") cursor := 0 l := len(buff) - obtained, err := parseTimeOffset(buff, &cursor, l) - s.Require().Nil(err) - s.Require().Equal(obtained, time.UTC) - s.Require().Equal(cursor, 1) + obtained, err := parseTimeOffset( + buff, &cursor, l, + ) + + require.Nil(t, err) + require.Equal(t, time.UTC, obtained) + require.Equal(t, 1, cursor) } -func (s *RFC5424TestSuite) TestGetHourMin() { +func TestGetHourMin(t *testing.T) { buff := []byte("12:34") cursor := 0 l := len(buff) @@ -882,14 +1062,13 @@ func (s *RFC5424TestSuite) TestGetHourMin() { buff, &cursor, l, ) - s.Require().Nil(err) - s.Require().Equal(obtainedH, expectedH) - s.Require().Equal(obtainedM, expectedM) - - s.Require().Equal(cursor, l) + require.Nil(t, err) + require.Equal(t, expectedH, obtainedH) + require.Equal(t, expectedM, obtainedM) + require.Equal(t, l, cursor) } -func (s *RFC5424TestSuite) TestParsePartialTime() { +func TestParsePartialTime(t *testing.T) { buff := []byte("05:14:15.000003") cursor := 0 l := len(buff) @@ -898,33 +1077,33 @@ func (s *RFC5424TestSuite) TestParsePartialTime() { buff, &cursor, l, ) - expected := partialTime{ + expected := &partialTime{ hour: 5, minute: 14, seconds: 15, secFrac: 0.000003, } - s.Require().Nil(err) - s.Require().Equal(obtained, expected) - s.Require().Equal(cursor, l) + require.Nil(t, err) + require.Equal(t, expected, obtained) + require.Equal(t, l, cursor) } -func (s *RFC5424TestSuite) TestParseFullTime() { +func TestParseFullTime(t *testing.T) { tz := "-02:00" buff := []byte("05:14:15.000003" + tz) cursor := 0 l := len(buff) tmpTs, err := time.Parse("-07:00", string(tz)) - s.Require().Nil(err) + require.Nil(t, err) obtained, err := parseFullTime( buff, &cursor, l, ) - expected := fullTime{ - pt: partialTime{ + expected := &fullTime{ + pt: &partialTime{ hour: 5, minute: 14, seconds: 15, @@ -933,12 +1112,12 @@ func (s *RFC5424TestSuite) TestParseFullTime() { loc: tmpTs.Location(), } - s.Require().Nil(err) - s.Require().Equal(obtained, expected) - s.Require().Equal(cursor, 21) + require.Nil(t, err) + require.Equal(t, expected, obtained) + require.Equal(t, 21, cursor) } -func (s *RFC5424TestSuite) TestToNSec() { +func TestToNSec(t *testing.T) { testCases := map[float64]int{ 0.52: 520000000, 0.003: 3000000, @@ -947,12 +1126,12 @@ func (s *RFC5424TestSuite) TestToNSec() { for src, expected := range testCases { obtained, err := toNSec(src) - s.Require().Nil(err) - s.Require().Equal(obtained, expected) + require.Nil(t, err) + require.Equal(t, expected, obtained) } } -func (s *RFC5424TestSuite) TestParseAppName() { +func TestParseAppName(t *testing.T) { testCases := []struct { description string input string @@ -980,19 +1159,21 @@ func (s *RFC5424TestSuite) TestParseAppName() { p := NewParser([]byte(tc.input)) obtained, err := p.parseAppName() - s.Require().Equal( - err, tc.expectedErr, tc.description, + require.Equal( + t, tc.expectedErr, err, tc.description, ) - s.Require().Equal( - obtained, tc.expectedAppName, tc.description, + + require.Equal( + t, tc.expectedAppName, obtained, tc.description, ) - s.Require().Equal( - p.cursor, tc.expectedCursorPos, tc.description, + + require.Equal( + t, tc.expectedCursorPos, p.cursor, tc.description, ) } } -func (s *RFC5424TestSuite) TestParseProcID() { +func TestParseProcID(t *testing.T) { testCases := []struct { description string input string @@ -1020,19 +1201,21 @@ func (s *RFC5424TestSuite) TestParseProcID() { p := NewParser([]byte(tc.input)) obtained, err := p.parseProcId() - s.Require().Equal( - err, tc.expectedErr, tc.description, + require.Equal( + t, tc.expectedErr, err, tc.description, ) - s.Require().Equal( - obtained, tc.expectedProcID, tc.description, + + require.Equal( + t, tc.expectedProcID, obtained, tc.description, ) - s.Require().Equal( - p.cursor, tc.expectedCursorPos, tc.description, + + require.Equal( + t, tc.expectedCursorPos, p.cursor, tc.description, ) } } -func (s *RFC5424TestSuite) TestParseMsgID() { +func TestParseMsgID(t *testing.T) { testCases := []struct { description string input string @@ -1060,19 +1243,21 @@ func (s *RFC5424TestSuite) TestParseMsgID() { p := NewParser([]byte(tc.input)) obtained, err := p.parseMsgId() - s.Require().Equal( - err, tc.expectedErr, tc.description, + require.Equal( + t, tc.expectedErr, err, tc.description, ) - s.Require().Equal( - obtained, tc.expectedMsgID, tc.description, + + require.Equal( + t, tc.expectedMsgID, obtained, tc.description, ) - s.Require().Equal( - p.cursor, tc.expectedCursorPos, tc.description, + + require.Equal( + t, tc.expectedCursorPos, p.cursor, tc.description, ) } } -func (s *RFC5424TestSuite) TestParseStructuredData() { +func TestParseStructuredData(t *testing.T) { testCases := []struct { description string input string @@ -1118,18 +1303,49 @@ func (s *RFC5424TestSuite) TestParseStructuredData() { len(tc.input), ) - s.Require().Equal( - err, tc.expectedErr, tc.description, + require.Equal( + t, tc.expectedErr, err, tc.description, ) - s.Require().Equal( - obtained, tc.expectedData, tc.description, + + require.Equal( + t, tc.expectedData, obtained, tc.description, ) - s.Require().Equal( - cursor, tc.expectedCursorPos, tc.description, + + require.Equal( + t, tc.expectedCursorPos, cursor, tc.description, ) } } +func TestParseMessageSizeChecks(t *testing.T) { + start := `<165>1 2003-10-11T22:14:15.003Z mymachine.example.com evntslog - ID47 [exampleSDID@32473 iut="3" eventSource="Application" eventID="1011"] ` + msg := start + strings.Repeat("a", MAX_PACKET_LEN) + + p := NewParser([]byte(msg)) + err := p.Parse() + fields := p.Dump() + + require.Nil( + t, err, + ) + + require.Len( + t, + fields["message"], + MAX_PACKET_LEN-len(start), + ) + + // --- + + msg = start + " hello " + p = NewParser([]byte(msg)) + err = p.Parse() + fields = p.Dump() + + require.Nil(t, err) + require.Equal(t, "hello", fields["message"]) +} + func BenchmarkParseTimestamp(b *testing.B) { buff := []byte("2003-08-24T05:14:15.000003-07:00") @@ -1146,7 +1362,9 @@ func BenchmarkParseTimestamp(b *testing.B) { } func BenchmarkParseHeader(b *testing.B) { - buff := []byte("<165>1 2003-10-11T22:14:15.003Z mymachine.example.com su 123 ID47") + buff := []byte( + "<165>1 2003-10-11T22:14:15.003Z mymachine.example.com su 123 ID47 ", + ) p := NewParser(buff) @@ -1163,20 +1381,14 @@ func BenchmarkParseHeader(b *testing.B) { func BenchmarkParseFull(b *testing.B) { msg := `<165>1 2003-10-11T22:14:15.003Z mymachine.example.com evntslog - ID47 [exampleSDID@32473 iut="3" eventSource="Application" eventID="1011"] An application event log entry...` - p := NewParser([]byte(msg)) - for i := 0; i < b.N; i++ { - _, err := p.parseHeader() + p := NewParser( + []byte(msg), + ) + + err := p.Parse() if err != nil { panic(err) } - - p.cursor = 0 } } - -func TestRFC5424TestSuite(t *testing.T) { - suite.Run( - t, new(RFC5424TestSuite), - ) -} diff --git a/syslogparser.go b/syslogparser.go index d49e97d..6671af7 100644 --- a/syslogparser.go +++ b/syslogparser.go @@ -1,33 +1,11 @@ +// Package syslogparser implements functions to parsing RFC3164 or RFC5424 syslog messages. +// syslogparser provides one subpackage per RFC with an example usage for which RFC. package syslogparser import ( - "fmt" - "strconv" - "strings" "time" -) - -const ( - PRI_PART_START = '<' - PRI_PART_END = '>' - NO_VERSION = -1 -) - -var ( - ErrEOL = &ParserError{"End of log line"} - ErrNoSpace = &ParserError{"No space found"} - - ErrPriorityNoStart = &ParserError{"No start char found for priority"} - ErrPriorityEmpty = &ParserError{"Priority field empty"} - ErrPriorityNoEnd = &ParserError{"No end char found for priority"} - ErrPriorityTooShort = &ParserError{"Priority field too short"} - ErrPriorityTooLong = &ParserError{"Priority field too long"} - ErrPriorityNonDigit = &ParserError{"Non digit found in priority"} - - ErrVersionNotFound = &ParserError{"Can not find version"} - - ErrTimestampUnknownFormat = &ParserError{"Timestamp format unknown"} + "github.com/jeromer/syslogparser/parsercommon" ) type RFC uint8 @@ -38,180 +16,15 @@ const ( RFC_5424 ) +type LogParts map[string]interface{} + type LogParser interface { Parse() error Dump() LogParts - Location(*time.Location) -} - -type ParserError struct { - ErrorString string -} - -type Priority struct { - P int - F Facility - S Severity -} - -type Facility struct { - Value int -} - -type Severity struct { - Value int -} - -type LogParts map[string]interface{} - -// https://tools.ietf.org/html/rfc3164#section-4.1 -func ParsePriority(buff []byte, cursor *int, l int) (Priority, error) { - pri := newPriority(0) - - if l <= 0 { - return pri, ErrPriorityEmpty - } - - if buff[*cursor] != PRI_PART_START { - return pri, ErrPriorityNoStart - } - - i := 1 - priDigit := 0 - - for i < l { - if i >= 5 { - return pri, ErrPriorityTooLong - } - - c := buff[i] - - if c == PRI_PART_END { - if i == 1 { - return pri, ErrPriorityTooShort - } - - *cursor = i + 1 - return newPriority(priDigit), nil - } - - if IsDigit(c) { - v, e := strconv.Atoi(string(c)) - if e != nil { - return pri, e - } - - priDigit = (priDigit * 10) + v - } else { - return pri, ErrPriorityNonDigit - } - - i++ - } - - return pri, ErrPriorityNoEnd -} - -// https://tools.ietf.org/html/rfc5424#section-6.2.2 -func ParseVersion(buff []byte, cursor *int, l int) (int, error) { - if *cursor >= l { - return NO_VERSION, ErrVersionNotFound - } - - c := buff[*cursor] - *cursor++ - - // XXX : not a version, not an error though as RFC 3164 does not support it - if !IsDigit(c) { - return NO_VERSION, nil - } - - v, e := strconv.Atoi(string(c)) - if e != nil { - *cursor-- - return NO_VERSION, e - } - - return v, nil -} - -func IsDigit(c byte) bool { - return c >= '0' && c <= '9' -} - -// TODO: this should return a *Priority -func newPriority(p int) Priority { - // The Priority value is calculated by first multiplying the Facility - // number by 8 and then adding the numerical value of the Severity. - - return Priority{ - P: p, - F: Facility{Value: p / 8}, - S: Severity{Value: p % 8}, - } -} - -func FindNextSpace(buff []byte, from int, l int) (int, error) { - var to int - - for to = from; to < l; to++ { - if buff[to] == ' ' { - to++ - return to, nil - } - } - - return 0, ErrNoSpace -} - -func Parse2Digits(buff []byte, cursor *int, l int, min int, max int, e error) (int, error) { - digitLen := 2 - - if *cursor+digitLen > l { - return 0, ErrEOL - } - - sub := string(buff[*cursor : *cursor+digitLen]) - - *cursor += digitLen - - i, err := strconv.Atoi(sub) - if err != nil { - return 0, e - } - - if i >= min && i <= max { - return i, nil - } - - return 0, e -} - -func ParseHostname(buff []byte, cursor *int, l int) (string, error) { - from := *cursor - var to int - - for to = from; to < l; to++ { - if buff[to] == ' ' { - break - } - } - - hostname := buff[from:to] - - *cursor = to - - return string(hostname), nil -} - -func ShowCursorPos(buff []byte, cursor int) { - fmt.Println(string(buff)) - padding := strings.Repeat("-", cursor) - fmt.Println(padding + "↑\n") -} - -func (err *ParserError) Error() string { - return err.ErrorString + WithTimestampFormat(string) + WithLocation(*time.Location) + WithHostname(string) + WithTag(string) } func DetectRFC(buff []byte) (RFC, error) { @@ -222,7 +35,11 @@ func DetectRFC(buff []byte) (RFC, error) { for i := 0; i < max; i++ { if buff[i] == '>' && i < max { x := i + 1 - v, err = ParseVersion(buff, &x, max) + + v, err = parsercommon.ParseVersion( + buff, &x, max, + ) + break } } @@ -231,7 +48,7 @@ func DetectRFC(buff []byte) (RFC, error) { return RFC_UNKNOWN, err } - if v == NO_VERSION { + if v == parsercommon.NO_VERSION { return RFC_3164, nil } diff --git a/syslogparser_test.go b/syslogparser_test.go index 83abd61..91bd42c 100644 --- a/syslogparser_test.go +++ b/syslogparser_test.go @@ -3,278 +3,35 @@ package syslogparser import ( "testing" - "github.com/stretchr/testify/suite" + "github.com/stretchr/testify/require" ) -type CommonTestSuite struct { - suite.Suite -} - -func (s *CommonTestSuite) TestParsePriority() { - testCases := []struct { - description string - input []byte - expectedPri Priority - expectedCursorPos int - expectedErr error - }{ - { - description: "empty priority", - input: []byte(""), - expectedPri: newPriority(0), - expectedCursorPos: 0, - expectedErr: ErrPriorityEmpty, - }, - { - description: "no start", - input: []byte("7>"), - expectedPri: newPriority(0), - expectedCursorPos: 0, - expectedErr: ErrPriorityNoStart, - }, - { - description: "no end", - input: []byte("<77"), - expectedPri: newPriority(0), - expectedCursorPos: 0, - expectedErr: ErrPriorityNoEnd, - }, - { - description: "too short", - input: []byte("<>"), - expectedPri: newPriority(0), - expectedCursorPos: 0, - expectedErr: ErrPriorityTooShort, - }, - { - description: "too long", - input: []byte("<1233>"), - expectedPri: newPriority(0), - expectedCursorPos: 0, - expectedErr: ErrPriorityTooLong, - }, - { - description: "no digits", - input: []byte("<7a8>"), - expectedPri: newPriority(0), - expectedCursorPos: 0, - expectedErr: ErrPriorityNonDigit, - }, - { - description: "all good", - input: []byte("<190>"), - expectedPri: newPriority(190), - expectedCursorPos: 5, - expectedErr: nil, - }, - } - - for _, tc := range testCases { - cursor := 0 - - obtained, err := ParsePriority( - tc.input, &cursor, len(tc.input), - ) - - s.Require().Equal( - obtained, tc.expectedPri, tc.description, - ) - - s.Require().Equal( - cursor, tc.expectedCursorPos, tc.description, - ) - - s.Require().Equal( - err, tc.expectedErr, tc.description, - ) - } -} - -func (s *CommonTestSuite) TestNewPriority() { - s.Require().Equal( - newPriority(165), - Priority{ - P: 165, - F: Facility{Value: 20}, - S: Severity{Value: 5}, - }, +func TestDetectRFC_3164(t *testing.T) { + p, err := DetectRFC( + []byte( + "<34>Oct 11 22:14:15 ...", + ), ) -} - -func (s *CommonTestSuite) TestParseVersion() { - testCases := []struct { - description string - input []byte - expectedVersion int - expectedCursorPos int - expectedErr error - }{ - { - description: "not found", - input: []byte("<123>"), - expectedVersion: NO_VERSION, - expectedCursorPos: 5, - expectedErr: ErrVersionNotFound, - }, - { - description: "non digit", - input: []byte("<123>a"), - expectedVersion: NO_VERSION, - expectedCursorPos: 6, - expectedErr: nil, - }, - { - description: "all good", - input: []byte("<123>1"), - expectedVersion: 1, - expectedCursorPos: 6, - expectedErr: nil, - }, - } - for _, tc := range testCases { - cursor := 5 - - obtained, err := ParseVersion( - tc.input, &cursor, len(tc.input), - ) - - s.Require().Equal( - obtained, tc.expectedVersion, tc.description, - ) - - s.Require().Equal( - cursor, tc.expectedCursorPos, tc.description, - ) - - s.Require().Equal( - err, tc.expectedErr, tc.description, - ) - } + require.Nil(t, err) + require.Equal(t, p, RFC(RFC_3164)) } -func (s *CommonTestSuite) TestParseHostname() { - testCases := []struct { - description string - input []byte - expectedHostname string - expectedCursorPos int - }{ - { - description: "invalid", - input: []byte("foo name"), - expectedHostname: "foo", - expectedCursorPos: 3, - }, - { - description: "valid", - input: []byte("ubuntu11.somehost.com" + " "), - expectedHostname: "ubuntu11.somehost.com", - expectedCursorPos: len("ubuntu11.somehost.com"), - }, - } - - for _, tc := range testCases { - cursor := 0 - - obtained, err := ParseHostname( - tc.input, &cursor, len(tc.input), - ) - - s.Require().Equal( - obtained, tc.expectedHostname, tc.description, - ) - - s.Require().Equal( - cursor, tc.expectedCursorPos, tc.description, - ) - - s.Require().Nil(err) - } -} - -func (s *CommonTestSuite) TestDetectRFC_3164() { - p, err := DetectRFC([]byte("<34>Oct 11 22:14:15 ...")) - - s.Require().Nil(err) - s.Require().Equal(p, RFC(RFC_3164)) -} - -func (s *CommonTestSuite) TestDetectRFC_5424() { +func TestDetectRFC_5424(t *testing.T) { p, err := DetectRFC( - []byte("<165>1 2003-10-11T22:14:15.003Z ..."), + []byte( + "<165>1 2003-10-11T22:14:15.003Z ...", + ), ) - s.Require().Nil(err) - s.Require().Equal(p, RFC(RFC_5424)) -} - -func (s *CommonTestSuite) TestFindNextSpace() { - testCases := []struct { - description string - input []byte - expectedCursorPos int - expectedErr error - }{ - { - description: "no space", - input: []byte("aaaaaa"), - expectedCursorPos: 0, - expectedErr: ErrNoSpace, - }, - { - description: "space found", - input: []byte("foo bar baz"), - expectedCursorPos: 4, - expectedErr: nil, - }, - } - - for _, tc := range testCases { - obtained, err := FindNextSpace( - tc.input, 0, len(tc.input), - ) - - s.Require().Equal( - obtained, tc.expectedCursorPos, tc.description, - ) - - s.Require().Equal( - err, tc.expectedErr, tc.description, - ) - } -} - -func BenchmarkParsePriority(b *testing.B) { - buff := []byte("<190>") - var start int - l := len(buff) - - for i := 0; i < b.N; i++ { - start = 0 - _, err := ParsePriority(buff, &start, l) - if err != nil { - panic(err) - } - } -} - -func BenchmarkParseVersion(b *testing.B) { - buff := []byte("<123>1") - start := 5 - l := len(buff) - - for i := 0; i < b.N; i++ { - start = 0 - _, err := ParseVersion(buff, &start, l) - if err != nil { - panic(err) - } - } + require.Nil(t, err) + require.Equal(t, p, RFC(RFC_5424)) } func BenchmarkDetectRFC(b *testing.B) { - buff := []byte("<165>1 2003-10-11T22:14:15.003Z ...") + buff := []byte( + "<165>1 2003-10-11T22:14:15.003Z ...", + ) for i := 0; i < b.N; i++ { _, err := DetectRFC(buff) @@ -283,9 +40,3 @@ func BenchmarkDetectRFC(b *testing.B) { } } } - -func TestCommonTestSuite(t *testing.T) { - suite.Run( - t, new(CommonTestSuite), - ) -}