├── .gitignore ├── CHANGELOG ├── LICENSE ├── Makefile ├── README.md ├── go.mod ├── go.sum ├── parsercommon ├── parsercommon.go └── parsercommon_test.go ├── rfc3164 ├── example_test.go ├── rfc3164.go └── rfc3164_test.go ├── rfc5424 ├── example_test.go ├── rfc5424.go └── rfc5424_test.go ├── syslogparser.go └── syslogparser_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | *.test 2 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | v1.1.0: 2 | 3 | - Fix panic when parsing tag (35ba9b8) 4 | 5 | v1.0.0: 6 | 7 | - Initial release (d8748a1) 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2021, Jérôme RENARD 2 | 3 | Redistribution and use in source and binary forms, with or without modification, 4 | are permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, 7 | this list of conditions and the following disclaimer. 8 | 9 | 2. Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | 3. Neither the name of the copyright holder nor the names of its contributors may 14 | be used to endorse or promote products derived from this software without 15 | specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 18 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 19 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 20 | IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 21 | INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 22 | NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, 23 | OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 24 | WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 25 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 26 | POSSIBILITY OF SUCH DAMAGE. 27 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GO ?= $(shell which go) 2 | 3 | GO_PKGS ?= $(shell $(GO) list ./...) 4 | 5 | GO_TEST_PKGS ?= $(shell test -f go.mod && $(GO) list -f \ 6 | '{{ if or .TestGoFiles .XTestGoFiles }}{{ .ImportPath }}{{ end }}' \ 7 | $(GO_PKGS)) 8 | 9 | GO_TEST_TIMEOUT ?= 15s 10 | 11 | GO_BENCH=go test -bench=. -benchmem 12 | 13 | all: lint test benchmark 14 | 15 | test: 16 | $(GO) test \ 17 | -race \ 18 | -timeout $(GO_TEST_TIMEOUT) \ 19 | $(GO_TEST_PKGS) 20 | 21 | #FIXME 22 | benchmark: 23 | $(GO_BENCH) 24 | cd rfc3164 && $(GO_BENCH) 25 | cd rfc5424 && $(GO_BENCH) 26 | cd parsercommon && $(GO_BENCH) 27 | 28 | lint: 29 | golangci-lint run ./... 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Syslogparser 2 | ============ 3 | 4 | This is a syslog parser for the Go programming language. 5 | 6 | https://pkg.go.dev/github.com/jeromer/syslogparser 7 | 8 | Installing 9 | ---------- 10 | 11 | go get github.com/jeromer/syslogparser 12 | 13 | Supported RFCs 14 | -------------- 15 | 16 | - [RFC 3164][RFC 3164] 17 | - [RFC 5424][RFC 5424] 18 | 19 | Not all features described in RFCs above are supported but only the most 20 | part of it. For exaple `SDID`s are not supported in [RFC 5424][RFC 5424] and 21 | `STRUCTURED-DATA` are parsed as a whole string. 22 | 23 | This parser should solve 80% of use cases. If your use cases are in the 24 | 20% remaining ones I would recommend you to fully test what you want to 25 | achieve and provide a patch if you want. 26 | 27 | Parsing an RFC 3164 syslog message 28 | ---------------------------------- 29 | 30 | b := "<34>Oct 11 22:14:15 mymachine su: 'su root' failed for lonvick on /dev/pts/8" 31 | buff := []byte(b) 32 | 33 | p := rfc3164.NewParser(buff) 34 | err := p.Parse() 35 | if err != nil { 36 | panic(err) 37 | } 38 | 39 | for k, v := range p.Dump() { 40 | fmt.Println(k, ":", v) 41 | } 42 | 43 | You should see 44 | 45 | timestamp : 2013-10-11 22:14:15 +0000 UTC 46 | hostname : mymachine 47 | tag : su 48 | content : 'su root' failed for lonvick on /dev/pts/8 49 | priority : 34 50 | facility : 4 51 | severity : 2 52 | 53 | Parsing an RFC 5424 syslog message 54 | ---------------------------------- 55 | 56 | b := `<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...` 57 | buff := []byte(b) 58 | 59 | p := rfc5424.NewParser(buff) 60 | err := p.Parse() 61 | if err != nil { 62 | panic(err) 63 | } 64 | 65 | for k, v := range p.Dump() { 66 | fmt.Println(k, ":", v) 67 | } 68 | 69 | You should see 70 | 71 | version : 1 72 | timestamp : 2003-10-11 22:14:15.003 +0000 UTC 73 | app_name : evntslog 74 | msg_id : ID47 75 | message : An application event log entry... 76 | priority : 165 77 | facility : 20 78 | severity : 5 79 | hostname : mymachine.example.com 80 | proc_id : - 81 | structured_data : [exampleSDID@32473 iut="3" eventSource="Application" eventID="1011"] 82 | 83 | Detecting message format 84 | ------------------------ 85 | 86 | You can use the `DetectRFC()` function. Like this: 87 | 88 | b := []byte(`<165>1 2003-10-11T22:14:15.003Z ...`) 89 | rfc, err := syslogparser.DetectRFC(b) 90 | if err != nil { 91 | panic(err) 92 | } 93 | 94 | switch rfc { 95 | case RFC_UNKNOWN: 96 | fmt.Println("unknown") 97 | case RFC_3164: 98 | fmt.Println("3164") 99 | case RFC_5424: 100 | fmt.Println("5424") 101 | } 102 | 103 | Running tests 104 | ------------- 105 | 106 | Run `make test` 107 | 108 | Running benchmarks 109 | ------------------ 110 | 111 | Run `make benchmark` 112 | 113 | go test -bench=. -benchmem 114 | goos: linux 115 | goarch: amd64 116 | pkg: github.com/jeromer/syslogparser 117 | BenchmarkDetectRFC-8 81994480 14.7 ns/op 0 B/op 0 allocs/op 118 | PASS 119 | ok github.com/jeromer/syslogparser 2.145s 120 | 121 | cd rfc3164 && go test -bench=. -benchmem 122 | goos: linux 123 | goarch: amd64 124 | pkg: github.com/jeromer/syslogparser/rfc3164 125 | BenchmarkParseTimestamp-8 2823901 416 ns/op 16 B/op 1 allocs/op 126 | BenchmarkParseHostname-8 34796552 35.4 ns/op 16 B/op 1 allocs/op 127 | BenchmarkParseTag-8 20954252 59.3 ns/op 8 B/op 1 allocs/op 128 | BenchmarkParseHeader-8 2276569 596 ns/op 80 B/op 3 allocs/op 129 | BenchmarkParsemessage-8 6751579 192 ns/op 104 B/op 4 allocs/op 130 | BenchmarkParseFull-8 1445076 838 ns/op 336 B/op 10 allocs/op 131 | PASS 132 | 133 | ok github.com/jeromer/syslogparser/rfc3164 9.601s 134 | cd rfc5424 && go test -bench=. -benchmem 135 | goos: linux 136 | goarch: amd64 137 | pkg: github.com/jeromer/syslogparser/rfc5424 138 | BenchmarkParseTimestamp-8 790478 1488 ns/op 432 B/op 21 allocs/op 139 | BenchmarkParseHeader-8 1000000 1043 ns/op 336 B/op 18 allocs/op 140 | BenchmarkParseFull-8 980828 1306 ns/op 672 B/op 21 allocs/op 141 | PASS 142 | ok github.com/jeromer/syslogparser/rfc5424 4.356s 143 | 144 | 145 | [RFC 5424]: https://tools.ietf.org/html/rfc5424 146 | [RFC 3164]: https://tools.ietf.org/html/rfc3164 147 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/jeromer/syslogparser 2 | 3 | go 1.14 4 | 5 | require github.com/stretchr/testify v1.7.0 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 5 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 6 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 7 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 8 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 9 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 10 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 11 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 12 | -------------------------------------------------------------------------------- /parsercommon/parsercommon.go: -------------------------------------------------------------------------------- 1 | package parsercommon 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | ) 8 | 9 | const ( 10 | NO_VERSION = -1 11 | ) 12 | 13 | var ( 14 | ErrEOL = &ParserError{"End of log line"} 15 | ErrNoSpace = &ParserError{"No space found"} 16 | 17 | ErrPriorityNoStart = &ParserError{"No start char found for priority"} 18 | ErrPriorityEmpty = &ParserError{"Priority field empty"} 19 | ErrPriorityNoEnd = &ParserError{"No end char found for priority"} 20 | ErrPriorityTooShort = &ParserError{"Priority field too short"} 21 | ErrPriorityTooLong = &ParserError{"Priority field too long"} 22 | ErrPriorityNonDigit = &ParserError{"Non digit found in priority"} 23 | 24 | ErrVersionNotFound = &ParserError{"Can not find version"} 25 | 26 | ErrTimestampUnknownFormat = &ParserError{"Timestamp format unknown"} 27 | 28 | ErrHostnameNotFound = &ParserError{"Hostname not found"} 29 | ) 30 | 31 | type ParserError struct { 32 | ErrorString string 33 | } 34 | 35 | type Priority struct { 36 | P int 37 | F Facility 38 | S Severity 39 | } 40 | 41 | type Facility struct { 42 | Value int 43 | } 44 | 45 | type Severity struct { 46 | Value int 47 | } 48 | 49 | // https://tools.ietf.org/html/rfc3164#section-4.1 50 | func ParsePriority(buff []byte, cursor *int, l int) (*Priority, error) { 51 | if l <= 0 { 52 | return nil, ErrPriorityEmpty 53 | } 54 | 55 | if buff[*cursor] != '<' { 56 | return nil, ErrPriorityNoStart 57 | } 58 | 59 | i := 1 60 | priDigit := 0 61 | 62 | for i < l { 63 | if i >= 5 { 64 | return nil, ErrPriorityTooLong 65 | } 66 | 67 | c := buff[i] 68 | 69 | if c == '>' { 70 | if i == 1 { 71 | return nil, ErrPriorityTooShort 72 | } 73 | 74 | *cursor = i + 1 75 | 76 | return NewPriority(priDigit), nil 77 | } 78 | 79 | if IsDigit(c) { 80 | v, e := strconv.Atoi(string(c)) 81 | if e != nil { 82 | return nil, e 83 | } 84 | 85 | priDigit = (priDigit * 10) + v 86 | } else { 87 | return nil, ErrPriorityNonDigit 88 | } 89 | 90 | i++ 91 | } 92 | 93 | return nil, ErrPriorityNoEnd 94 | } 95 | 96 | // https://tools.ietf.org/html/rfc5424#section-6.2.2 97 | func ParseVersion(buff []byte, cursor *int, l int) (int, error) { 98 | if *cursor >= l { 99 | return NO_VERSION, ErrVersionNotFound 100 | } 101 | 102 | c := buff[*cursor] 103 | *cursor++ 104 | 105 | // XXX : not a version, not an error though as RFC 3164 does not support it 106 | if !IsDigit(c) { 107 | return NO_VERSION, nil 108 | } 109 | 110 | v, e := strconv.Atoi(string(c)) 111 | if e != nil { 112 | *cursor-- 113 | return NO_VERSION, e 114 | } 115 | 116 | return v, nil 117 | 118 | } 119 | 120 | func IsDigit(c byte) bool { 121 | return c >= '0' && c <= '9' 122 | } 123 | 124 | func NewPriority(p int) *Priority { 125 | // The Priority value is calculated by first multiplying the Facility 126 | // number by 8 and then adding the numerical value of the Severity. 127 | 128 | return &Priority{ 129 | P: p, 130 | F: Facility{Value: p / 8}, 131 | S: Severity{Value: p % 8}, 132 | } 133 | } 134 | 135 | func FindNextSpace(buff []byte, from int, l int) (int, error) { 136 | var to int 137 | 138 | for to = from; to < l; to++ { 139 | if buff[to] == ' ' { 140 | to++ 141 | return to, nil 142 | } 143 | } 144 | 145 | return 0, ErrNoSpace 146 | } 147 | 148 | func Parse2Digits(buff []byte, cursor *int, l int, min int, max int, e error) (int, error) { 149 | digitLen := 2 150 | 151 | if *cursor+digitLen > l { 152 | return 0, ErrEOL 153 | } 154 | 155 | sub := string(buff[*cursor : *cursor+digitLen]) 156 | 157 | *cursor += digitLen 158 | 159 | i, err := strconv.Atoi(sub) 160 | if err != nil { 161 | return 0, e 162 | } 163 | 164 | if i >= min && i <= max { 165 | return i, nil 166 | } 167 | 168 | return 0, e 169 | } 170 | 171 | func ParseHostname(buff []byte, cursor *int, l int) (string, error) { 172 | from := *cursor 173 | var to int 174 | 175 | for to = from; to < l; to++ { 176 | if buff[to] == ' ' { 177 | break 178 | } 179 | } 180 | 181 | hostname := buff[from:to] 182 | 183 | *cursor = to 184 | 185 | return string(hostname), nil 186 | } 187 | 188 | func ShowCursorPos(buff []byte, cursor int) { 189 | fmt.Println(string(buff)) 190 | padding := strings.Repeat("-", cursor) 191 | fmt.Println(padding + "↑\n") 192 | } 193 | 194 | func (err *ParserError) Error() string { 195 | return err.ErrorString 196 | } 197 | -------------------------------------------------------------------------------- /parsercommon/parsercommon_test.go: -------------------------------------------------------------------------------- 1 | package parsercommon 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestParsePriority(t *testing.T) { 10 | testCases := []struct { 11 | description string 12 | input []byte 13 | expectedPri *Priority 14 | expectedCursorPos int 15 | expectedErr error 16 | }{ 17 | { 18 | description: "empty priority", 19 | input: []byte(""), 20 | expectedPri: nil, 21 | expectedCursorPos: 0, 22 | expectedErr: ErrPriorityEmpty, 23 | }, 24 | { 25 | description: "no start", 26 | input: []byte("7>"), 27 | expectedPri: nil, 28 | expectedCursorPos: 0, 29 | expectedErr: ErrPriorityNoStart, 30 | }, 31 | { 32 | description: "no end", 33 | input: []byte("<77"), 34 | expectedPri: nil, 35 | expectedCursorPos: 0, 36 | expectedErr: ErrPriorityNoEnd, 37 | }, 38 | { 39 | description: "too short", 40 | input: []byte("<>"), 41 | expectedPri: nil, 42 | expectedCursorPos: 0, 43 | expectedErr: ErrPriorityTooShort, 44 | }, 45 | { 46 | description: "too long", 47 | input: []byte("<1233>"), 48 | expectedPri: nil, 49 | expectedCursorPos: 0, 50 | expectedErr: ErrPriorityTooLong, 51 | }, 52 | { 53 | description: "no digits", 54 | input: []byte("<7a8>"), 55 | expectedPri: nil, 56 | expectedCursorPos: 0, 57 | expectedErr: ErrPriorityNonDigit, 58 | }, 59 | { 60 | description: "all good", 61 | input: []byte("<190>"), 62 | expectedPri: NewPriority(190), 63 | expectedCursorPos: 5, 64 | expectedErr: nil, 65 | }, 66 | } 67 | 68 | for _, tc := range testCases { 69 | cursor := 0 70 | 71 | obtained, err := ParsePriority( 72 | tc.input, &cursor, len(tc.input), 73 | ) 74 | 75 | require.Equal( 76 | t, tc.expectedPri, obtained, tc.description, 77 | ) 78 | 79 | require.Equal( 80 | t, tc.expectedCursorPos, cursor, tc.description, 81 | ) 82 | 83 | require.Equal( 84 | t, tc.expectedErr, err, tc.description, 85 | ) 86 | } 87 | } 88 | 89 | func TestNewPriority(t *testing.T) { 90 | require.Equal( 91 | t, 92 | &Priority{ 93 | P: 165, 94 | F: Facility{Value: 20}, 95 | S: Severity{Value: 5}, 96 | }, 97 | NewPriority(165), 98 | ) 99 | } 100 | 101 | func TestParseVersion(t *testing.T) { 102 | testCases := []struct { 103 | description string 104 | input []byte 105 | expectedVersion int 106 | expectedCursorPos int 107 | expectedErr error 108 | }{ 109 | { 110 | description: "not found", 111 | input: []byte("<123>"), 112 | expectedVersion: NO_VERSION, 113 | expectedCursorPos: 5, 114 | expectedErr: ErrVersionNotFound, 115 | }, 116 | { 117 | description: "non digit", 118 | input: []byte("<123>a"), 119 | expectedVersion: NO_VERSION, 120 | expectedCursorPos: 6, 121 | expectedErr: nil, 122 | }, 123 | { 124 | description: "all good", 125 | input: []byte("<123>1"), 126 | expectedVersion: 1, 127 | expectedCursorPos: 6, 128 | expectedErr: nil, 129 | }, 130 | } 131 | 132 | for _, tc := range testCases { 133 | cursor := 5 134 | 135 | obtained, err := ParseVersion( 136 | tc.input, &cursor, len(tc.input), 137 | ) 138 | 139 | require.Equal( 140 | t, tc.expectedVersion, obtained, tc.description, 141 | ) 142 | 143 | require.Equal( 144 | t, tc.expectedCursorPos, cursor, tc.description, 145 | ) 146 | 147 | require.Equal( 148 | t, tc.expectedErr, err, tc.description, 149 | ) 150 | } 151 | } 152 | 153 | func TestParseHostname(t *testing.T) { 154 | testCases := []struct { 155 | description string 156 | input []byte 157 | expectedHostname string 158 | expectedCursorPos int 159 | }{ 160 | { 161 | description: "invalid", 162 | input: []byte("foo name"), 163 | expectedHostname: "foo", 164 | expectedCursorPos: 3, 165 | }, 166 | { 167 | description: "valid", 168 | input: []byte("ubuntu11.somehost.com" + " "), 169 | expectedHostname: "ubuntu11.somehost.com", 170 | expectedCursorPos: len("ubuntu11.somehost.com"), 171 | }, 172 | } 173 | 174 | for _, tc := range testCases { 175 | cursor := 0 176 | 177 | obtained, err := ParseHostname( 178 | tc.input, &cursor, len(tc.input), 179 | ) 180 | 181 | require.Equal( 182 | t, tc.expectedHostname, obtained, tc.description, 183 | ) 184 | 185 | require.Equal( 186 | t, tc.expectedCursorPos, cursor, tc.description, 187 | ) 188 | 189 | require.Nil( 190 | t, err, 191 | ) 192 | } 193 | } 194 | 195 | func TestFindNextSpace(t *testing.T) { 196 | testCases := []struct { 197 | description string 198 | input []byte 199 | expectedCursorPos int 200 | expectedErr error 201 | }{ 202 | { 203 | description: "no space", 204 | input: []byte("aaaaaa"), 205 | expectedCursorPos: 0, 206 | expectedErr: ErrNoSpace, 207 | }, 208 | { 209 | description: "space found", 210 | input: []byte("foo bar baz"), 211 | expectedCursorPos: 4, 212 | expectedErr: nil, 213 | }, 214 | } 215 | 216 | for _, tc := range testCases { 217 | obtained, err := FindNextSpace( 218 | tc.input, 0, len(tc.input), 219 | ) 220 | 221 | require.Equal( 222 | t, tc.expectedCursorPos, obtained, tc.description, 223 | ) 224 | 225 | require.Equal( 226 | t, tc.expectedErr, err, tc.description, 227 | ) 228 | } 229 | } 230 | 231 | func BenchmarkParsePriority(b *testing.B) { 232 | buff := []byte("<190>") 233 | var start int 234 | l := len(buff) 235 | 236 | for i := 0; i < b.N; i++ { 237 | start = 0 238 | _, err := ParsePriority(buff, &start, l) 239 | if err != nil { 240 | panic(err) 241 | } 242 | } 243 | } 244 | 245 | func BenchmarkParseVersion(b *testing.B) { 246 | buff := []byte("<123>1") 247 | start := 5 248 | l := len(buff) 249 | 250 | for i := 0; i < b.N; i++ { 251 | start = 0 252 | _, err := ParseVersion(buff, &start, l) 253 | if err != nil { 254 | panic(err) 255 | } 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /rfc3164/example_test.go: -------------------------------------------------------------------------------- 1 | package rfc3164_test 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/jeromer/syslogparser/rfc3164" 7 | ) 8 | 9 | func ExampleNewParser() { 10 | b := "<34>Oct 11 22:14:15 mymachine su: 'su root' failed for lonvick on /dev/pts/8" 11 | buff := []byte(b) 12 | 13 | p := rfc3164.NewParser(buff) 14 | err := p.Parse() 15 | if err != nil { 16 | panic(err) 17 | } 18 | 19 | for k, v := range p.Dump() { 20 | fmt.Println(k, ":", v) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /rfc3164/rfc3164.go: -------------------------------------------------------------------------------- 1 | package rfc3164 2 | 3 | import ( 4 | "bytes" 5 | "math" 6 | "time" 7 | 8 | "github.com/jeromer/syslogparser" 9 | "github.com/jeromer/syslogparser/parsercommon" 10 | ) 11 | 12 | const ( 13 | // according to https://tools.ietf.org/html/rfc3164#section-4.1 14 | // "The total length of the packet MUST be 1024 bytes or less" 15 | // However we will accept a bit more while protecting from exhaustion 16 | MAX_PACKET_LEN = 2048 17 | ) 18 | 19 | type Parser struct { 20 | buff []byte 21 | cursor int 22 | l int 23 | priority *parsercommon.Priority 24 | version int 25 | header *header 26 | message *message 27 | location *time.Location 28 | hostname string 29 | customTag string 30 | customTimestampFormat string 31 | } 32 | 33 | type header struct { 34 | timestamp time.Time 35 | hostname string 36 | } 37 | 38 | type message struct { 39 | tag string 40 | content string 41 | } 42 | 43 | func NewParser(buff []byte) *Parser { 44 | return &Parser{ 45 | buff: buff, 46 | cursor: 0, 47 | location: time.UTC, 48 | l: int( 49 | math.Min( 50 | float64(len(buff)), 51 | MAX_PACKET_LEN, 52 | ), 53 | ), 54 | } 55 | } 56 | 57 | // Forces a priority for this parser. Priority will not be parsed. 58 | func (p *Parser) WithPriority(pri *parsercommon.Priority) { 59 | p.priority = pri 60 | } 61 | 62 | // Forces a location. UTC will be used otherwise. 63 | func (p *Parser) WithLocation(l *time.Location) { 64 | p.location = l 65 | } 66 | 67 | // Forces a hostname. Hostname will not be parsed 68 | func (p *Parser) WithHostname(h string) { 69 | p.hostname = h 70 | } 71 | 72 | // Forces a tag. Tag will not be parsed 73 | func (p *Parser) WithTag(t string) { 74 | p.customTag = t 75 | } 76 | 77 | // Forces a given time format. 78 | // Refer to pkg/time layouts for more informations 79 | // By default the following formats will be tried in order: 80 | // Jan 02 15:04:05 81 | // Jan 2 15:04:05 82 | // The timezone MUST be specified using WithLocation() and 83 | // not using WithTimestampFormat 84 | func (p *Parser) WithTimestampFormat(s string) { 85 | p.customTimestampFormat = s 86 | } 87 | 88 | // DEPRECATED. Use WithLocation() instead 89 | func (p *Parser) Location(location *time.Location) { 90 | p.WithLocation(location) 91 | } 92 | 93 | // DEPRECATED. Use WithHostname() instead 94 | func (p *Parser) Hostname(hostname string) { 95 | p.WithHostname(hostname) 96 | } 97 | 98 | func (p *Parser) Parse() error { 99 | p.version = parsercommon.NO_VERSION 100 | 101 | pri, err := p.parsePriority() 102 | if err != nil { 103 | return err 104 | } 105 | 106 | p.priority = pri 107 | 108 | hdr, err := p.parseHeader() 109 | if err != nil { 110 | return err 111 | } 112 | 113 | p.header = hdr 114 | 115 | if p.buff[p.cursor] == ' ' { 116 | p.cursor++ 117 | } 118 | 119 | msg, err := p.parsemessage() 120 | if err != parsercommon.ErrEOL { 121 | return err 122 | } 123 | 124 | p.message = msg 125 | 126 | return nil 127 | } 128 | 129 | func (p *Parser) Dump() syslogparser.LogParts { 130 | return syslogparser.LogParts{ 131 | "timestamp": p.header.timestamp, 132 | "hostname": p.header.hostname, 133 | "tag": p.message.tag, 134 | "content": p.message.content, 135 | "priority": p.priority.P, 136 | "facility": p.priority.F.Value, 137 | "severity": p.priority.S.Value, 138 | } 139 | } 140 | 141 | func (p *Parser) parsePriority() (*parsercommon.Priority, error) { 142 | if p.priority != nil { 143 | return p.priority, nil 144 | } 145 | 146 | return parsercommon.ParsePriority( 147 | p.buff, &p.cursor, p.l, 148 | ) 149 | } 150 | 151 | // HEADER: TIMESTAMP + HOSTNAME (or IP) 152 | // https://tools.ietf.org/html/rfc3164#section-4.1.2 153 | func (p *Parser) parseHeader() (*header, error) { 154 | var err error 155 | 156 | if p.buff[p.cursor] == ' ' { 157 | p.cursor++ 158 | } 159 | 160 | ts, err := p.parseTimestamp() 161 | if err != nil { 162 | return nil, err 163 | } 164 | 165 | h, err := p.parseHostname() 166 | if err != nil { 167 | return nil, err 168 | } 169 | 170 | hdr := &header{ 171 | timestamp: ts, 172 | hostname: h, 173 | } 174 | 175 | return hdr, nil 176 | } 177 | 178 | // MSG: TAG + CONTENT 179 | // https://tools.ietf.org/html/rfc3164#section-4.1.3 180 | func (p *Parser) parsemessage() (*message, error) { 181 | var err error 182 | 183 | tag, err := p.parseTag() 184 | if err != nil { 185 | return nil, err 186 | } 187 | 188 | content, err := p.parseContent() 189 | if err != parsercommon.ErrEOL { 190 | return nil, err 191 | } 192 | 193 | msg := &message{ 194 | tag: tag, 195 | content: content, 196 | } 197 | 198 | return msg, err 199 | } 200 | 201 | // https://tools.ietf.org/html/rfc3164#section-4.1.2 202 | func (p *Parser) parseTimestamp() (time.Time, error) { 203 | var ts time.Time 204 | var err error 205 | var tsFmtLen int 206 | var sub []byte 207 | 208 | tsFmts := []string{ 209 | "Jan 02 15:04:05", 210 | "Jan 2 15:04:05", 211 | } 212 | 213 | if p.customTimestampFormat != "" { 214 | tsFmts = []string{ 215 | p.customTimestampFormat, 216 | } 217 | } 218 | 219 | found := false 220 | for _, tsFmt := range tsFmts { 221 | tsFmtLen = len(tsFmt) 222 | 223 | if p.cursor+tsFmtLen > p.l { 224 | continue 225 | } 226 | 227 | sub = p.buff[p.cursor : tsFmtLen+p.cursor] 228 | ts, err = time.ParseInLocation( 229 | tsFmt, string(sub), p.location, 230 | ) 231 | 232 | if err == nil { 233 | found = true 234 | break 235 | } 236 | } 237 | 238 | if !found { 239 | p.cursor = tsFmtLen 240 | 241 | // XXX : If the timestamp is invalid we try to push the cursor one byte 242 | // XXX : further, in case it is a space 243 | if (p.cursor < p.l) && (p.buff[p.cursor] == ' ') { 244 | p.cursor++ 245 | } 246 | 247 | return ts, parsercommon.ErrTimestampUnknownFormat 248 | } 249 | 250 | fixTimestampIfNeeded(&ts) 251 | 252 | p.cursor += tsFmtLen 253 | 254 | if (p.cursor < p.l) && (p.buff[p.cursor] == ' ') { 255 | p.cursor++ 256 | } 257 | 258 | return ts, nil 259 | } 260 | 261 | func (p *Parser) parseHostname() (string, error) { 262 | if p.hostname != "" { 263 | return p.hostname, nil 264 | } 265 | 266 | return parsercommon.ParseHostname( 267 | p.buff, &p.cursor, p.l, 268 | ) 269 | } 270 | 271 | // http://tools.ietf.org/html/rfc3164#section-4.1.3 272 | func (p *Parser) parseTag() (string, error) { 273 | if p.customTag != "" { 274 | return p.customTag, nil 275 | } 276 | 277 | var b byte 278 | var tag []byte 279 | var err error 280 | var enough bool 281 | 282 | previous := p.cursor 283 | 284 | // "The TAG is a string of ABNF alphanumeric characters that MUST NOT exceed 32 characters." 285 | to := int( 286 | math.Min( 287 | float64(p.l), 288 | float64(p.cursor+32), 289 | ), 290 | ) 291 | 292 | for p.cursor < to { 293 | b = p.buff[p.cursor] 294 | 295 | if b == ' ' { 296 | p.cursor++ 297 | break 298 | } 299 | 300 | if b == '[' || b == ']' || b == ':' || enough { 301 | enough = true 302 | p.cursor++ 303 | continue 304 | } 305 | 306 | tag = append(tag, b) 307 | p.cursor++ 308 | } 309 | 310 | if len(tag) == 0 { 311 | p.cursor = previous 312 | } 313 | 314 | return string(tag), err 315 | } 316 | 317 | func (p *Parser) parseContent() (string, error) { 318 | if p.cursor > p.l { 319 | return "", parsercommon.ErrEOL 320 | } 321 | 322 | content := bytes.Trim( 323 | p.buff[p.cursor:p.l], " ", 324 | ) 325 | 326 | p.cursor += len(content) 327 | 328 | return string(content), parsercommon.ErrEOL 329 | } 330 | 331 | func fixTimestampIfNeeded(ts *time.Time) { 332 | now := time.Now() 333 | y := ts.Year() 334 | 335 | if ts.Year() == 0 { 336 | y = now.Year() 337 | } 338 | 339 | newTs := time.Date( 340 | y, ts.Month(), ts.Day(), 341 | ts.Hour(), ts.Minute(), ts.Second(), ts.Nanosecond(), 342 | ts.Location(), 343 | ) 344 | 345 | *ts = newTs 346 | } 347 | -------------------------------------------------------------------------------- /rfc3164/rfc3164_test.go: -------------------------------------------------------------------------------- 1 | package rfc3164 2 | 3 | import ( 4 | "bytes" 5 | "strings" 6 | "testing" 7 | "time" 8 | 9 | "github.com/jeromer/syslogparser" 10 | "github.com/jeromer/syslogparser/parsercommon" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | var ( 15 | // XXX : corresponds to the length of the last tried timestamp format 16 | // XXX : Jan 2 15:04:05 17 | lastTriedTimestampLen = 15 18 | ) 19 | 20 | func TestParserValid(t *testing.T) { 21 | buff := []byte( 22 | "<34>Oct 11 22:14:15 mymachine very.large.syslog.message.tag: 'su root' failed for lonvick on /dev/pts/8", 23 | ) 24 | 25 | p := NewParser(buff) 26 | 27 | require.Equal( 28 | t, 29 | &Parser{ 30 | buff: buff, 31 | cursor: 0, 32 | l: len(buff), 33 | location: time.UTC, 34 | }, 35 | p, 36 | ) 37 | 38 | err := p.Parse() 39 | 40 | require.Nil( 41 | t, err, 42 | ) 43 | 44 | require.Equal( 45 | t, 46 | syslogparser.LogParts{ 47 | "timestamp": time.Date( 48 | time.Now().Year(), 49 | time.October, 50 | 11, 22, 14, 15, 0, 51 | time.UTC, 52 | ), 53 | "hostname": "mymachine", 54 | "tag": "very.large.syslog.message.tag", 55 | "content": "'su root' failed for lonvick on /dev/pts/8", 56 | "priority": 34, 57 | "facility": 4, 58 | "severity": 2, 59 | }, 60 | p.Dump(), 61 | ) 62 | } 63 | 64 | func TestParserWithPriority(t *testing.T) { 65 | buff := []byte( 66 | "Oct 11 22:14:15 mymachine very.large.syslog.message.tag: 'su root' failed for lonvick on /dev/pts/8", 67 | ) 68 | 69 | pri := parsercommon.NewPriority(0) 70 | 71 | p := NewParser(buff) 72 | p.WithPriority(pri) 73 | 74 | require.Equal( 75 | t, 76 | &Parser{ 77 | buff: buff, 78 | cursor: 0, 79 | l: len(buff), 80 | location: time.UTC, 81 | priority: pri, 82 | }, 83 | p, 84 | ) 85 | 86 | err := p.Parse() 87 | 88 | require.Nil( 89 | t, err, 90 | ) 91 | 92 | require.Equal( 93 | t, 94 | syslogparser.LogParts{ 95 | "timestamp": time.Date( 96 | time.Now().Year(), 97 | time.October, 98 | 11, 22, 14, 15, 0, 99 | time.UTC, 100 | ), 101 | "hostname": "mymachine", 102 | "tag": "very.large.syslog.message.tag", 103 | "content": "'su root' failed for lonvick on /dev/pts/8", 104 | "priority": 0, 105 | "facility": 0, 106 | "severity": 0, 107 | }, 108 | p.Dump(), 109 | ) 110 | } 111 | 112 | func TestParserWithHostname(t *testing.T) { 113 | buff := []byte( 114 | "<30>Jun 23 13:17:42 chronyd[1119]: Selected source 192.168.65.1", 115 | ) 116 | 117 | p := NewParser(buff) 118 | p.WithHostname("dummy") 119 | 120 | err := p.Parse() 121 | require.Nil(t, err) 122 | 123 | require.Equal( 124 | t, 125 | syslogparser.LogParts{ 126 | "timestamp": time.Date( 127 | time.Now().Year(), 128 | time.June, 129 | 23, 13, 17, 42, 0, 130 | time.UTC, 131 | ), 132 | "hostname": "dummy", 133 | "tag": "chronyd", 134 | "content": "Selected source 192.168.65.1", 135 | "priority": 30, 136 | "facility": 3, 137 | "severity": 6, 138 | }, 139 | p.Dump(), 140 | ) 141 | } 142 | 143 | func TestParserWithTag(t *testing.T) { 144 | buff := []byte( 145 | "<30>Jun 23 13:17:42 localhost Selected source 192.168.65.1", 146 | ) 147 | 148 | tag := "chronyd" 149 | p := NewParser(buff) 150 | p.WithTag(tag) 151 | 152 | err := p.Parse() 153 | require.Nil(t, err) 154 | 155 | require.Equal( 156 | t, 157 | syslogparser.LogParts{ 158 | "timestamp": time.Date( 159 | time.Now().Year(), 160 | time.June, 161 | 23, 13, 17, 42, 0, 162 | time.UTC, 163 | ), 164 | "hostname": "localhost", 165 | "tag": "chronyd", 166 | "content": "Selected source 192.168.65.1", 167 | "priority": 30, 168 | "facility": 3, 169 | "severity": 6, 170 | }, 171 | p.Dump(), 172 | ) 173 | } 174 | 175 | func TestParserWithLocation(t *testing.T) { 176 | buff := []byte( 177 | "<30>Jun 23 13:17:42 localhost foo: Selected source 192.168.65.1", 178 | ) 179 | 180 | loc, err := time.LoadLocation("America/New_York") 181 | require.Nil(t, err) 182 | 183 | p := NewParser(buff) 184 | p.WithLocation(loc) 185 | 186 | err = p.Parse() 187 | require.Nil(t, err) 188 | 189 | require.Equal( 190 | t, 191 | syslogparser.LogParts{ 192 | "timestamp": time.Date( 193 | time.Now().Year(), 194 | time.June, 195 | 23, 13, 17, 42, 0, 196 | loc, 197 | ), 198 | "hostname": "localhost", 199 | "tag": "foo", 200 | "content": "Selected source 192.168.65.1", 201 | "priority": 30, 202 | "facility": 3, 203 | "severity": 6, 204 | }, 205 | p.Dump(), 206 | ) 207 | } 208 | 209 | func TestParserWithTimestampFormat(t *testing.T) { 210 | buff := []byte( 211 | "<30>2006-01-02T15:04:05 localhost foo: Selected source 192.168.65.1", 212 | ) 213 | 214 | p := NewParser(buff) 215 | p.WithTimestampFormat( 216 | "2006-01-02T15:04:05", 217 | ) 218 | 219 | err := p.Parse() 220 | require.Nil(t, err) 221 | 222 | require.Equal( 223 | t, 224 | syslogparser.LogParts{ 225 | "timestamp": time.Date( 226 | 2006, time.January, 2, 227 | 15, 4, 5, 0, 228 | time.UTC, 229 | ), 230 | "hostname": "localhost", 231 | "tag": "foo", 232 | "content": "Selected source 192.168.65.1", 233 | "priority": 30, 234 | "facility": 3, 235 | "severity": 6, 236 | }, 237 | p.Dump(), 238 | ) 239 | } 240 | 241 | func TestParserWithPriorityHostnameTag(t *testing.T) { 242 | buff := []byte( 243 | "Oct 11 22:14:15 'su root' failed for lonvick on /dev/pts/8", 244 | ) 245 | 246 | pri := parsercommon.NewPriority(0) 247 | h := "mymachine" 248 | tag := "foo" 249 | 250 | p := NewParser(buff) 251 | p.WithPriority(pri) 252 | p.WithHostname(h) 253 | p.WithTag(tag) 254 | 255 | require.Equal( 256 | t, 257 | &Parser{ 258 | buff: buff, 259 | cursor: 0, 260 | l: len(buff), 261 | location: time.UTC, 262 | priority: pri, 263 | hostname: h, 264 | customTag: tag, 265 | }, 266 | p, 267 | ) 268 | 269 | err := p.Parse() 270 | 271 | require.Nil( 272 | t, err, 273 | ) 274 | 275 | require.Equal( 276 | t, 277 | syslogparser.LogParts{ 278 | "timestamp": time.Date( 279 | time.Now().Year(), 280 | time.October, 281 | 11, 22, 14, 15, 0, 282 | time.UTC, 283 | ), 284 | "hostname": h, 285 | "tag": tag, 286 | "content": "'su root' failed for lonvick on /dev/pts/8", 287 | "priority": 0, 288 | "facility": 0, 289 | "severity": 0, 290 | }, 291 | p.Dump(), 292 | ) 293 | } 294 | 295 | func TestParseHeader(t *testing.T) { 296 | date := time.Date( 297 | time.Now().Year(), 298 | time.October, 299 | 11, 22, 14, 15, 0, 300 | time.UTC, 301 | ) 302 | 303 | testCases := []struct { 304 | description string 305 | input string 306 | expectedHdr *header 307 | expectedCursorPos int 308 | expectedErr error 309 | }{ 310 | { 311 | description: "valid headers", 312 | input: "Oct 11 22:14:15 mymachine ", 313 | expectedHdr: &header{ 314 | hostname: "mymachine", 315 | timestamp: date, 316 | }, 317 | expectedCursorPos: 25, 318 | expectedErr: nil, 319 | }, 320 | { 321 | description: "valid headers with prepended space", 322 | input: " Oct 11 22:14:15 mymachine ", 323 | expectedHdr: &header{ 324 | hostname: "mymachine", 325 | timestamp: date, 326 | }, 327 | expectedCursorPos: 26, 328 | expectedErr: nil, 329 | }, 330 | { 331 | description: "invalid timestamp", 332 | input: "Oct 34 32:72:82 mymachine ", 333 | expectedHdr: nil, 334 | expectedCursorPos: lastTriedTimestampLen + 1, 335 | expectedErr: parsercommon.ErrTimestampUnknownFormat, 336 | }, 337 | } 338 | 339 | for _, tc := range testCases { 340 | p := NewParser([]byte(tc.input)) 341 | obtained, err := p.parseHeader() 342 | 343 | require.Equal( 344 | t, tc.expectedErr, err, tc.description, 345 | ) 346 | 347 | require.Equal( 348 | t, tc.expectedHdr, obtained, tc.description, 349 | ) 350 | 351 | require.Equal( 352 | t, tc.expectedCursorPos, p.cursor, tc.description, 353 | ) 354 | } 355 | } 356 | 357 | func TestParsemessage(t *testing.T) { 358 | content := "foo bar baz blah quux" 359 | 360 | buff := []byte("sometag[123]: " + content) 361 | 362 | msg := &message{ 363 | tag: "sometag", 364 | content: content, 365 | } 366 | 367 | p := NewParser(buff) 368 | obtained, err := p.parsemessage() 369 | 370 | require.Equal( 371 | t, parsercommon.ErrEOL, err, 372 | ) 373 | 374 | require.Equal( 375 | t, msg, obtained, 376 | ) 377 | 378 | require.Equal( 379 | t, len(buff), p.cursor, 380 | ) 381 | } 382 | 383 | func TestParseTimestamp(t *testing.T) { 384 | testCases := []struct { 385 | description string 386 | input string 387 | expectedTS time.Time 388 | expectedCursorPos int 389 | expectedErr error 390 | }{ 391 | { 392 | description: "invalid", 393 | input: "Oct 34 32:72:82", 394 | expectedCursorPos: lastTriedTimestampLen, 395 | expectedErr: parsercommon.ErrTimestampUnknownFormat, 396 | }, 397 | { 398 | description: "trailing space", 399 | input: "Oct 11 22:14:15 ", 400 | expectedTS: time.Date( 401 | time.Now().Year(), 402 | time.October, 403 | 11, 22, 14, 15, 0, 404 | time.UTC, 405 | ), 406 | expectedCursorPos: 16, 407 | expectedErr: nil, 408 | }, 409 | { 410 | description: "one digit for month", 411 | input: "Oct 1 22:14:15", 412 | expectedTS: time.Date( 413 | time.Now().Year(), 414 | time.October, 415 | 1, 22, 14, 15, 0, 416 | time.UTC, 417 | ), 418 | expectedCursorPos: 15, 419 | expectedErr: nil, 420 | }, 421 | { 422 | description: "valid timestamp", 423 | input: "Oct 11 22:14:15", 424 | expectedTS: time.Date( 425 | time.Now().Year(), 426 | time.October, 427 | 11, 22, 14, 15, 0, 428 | time.UTC, 429 | ), 430 | expectedCursorPos: 15, 431 | expectedErr: nil, 432 | }, 433 | } 434 | 435 | for _, tc := range testCases { 436 | p := NewParser([]byte(tc.input)) 437 | obtained, err := p.parseTimestamp() 438 | 439 | require.Equal( 440 | t, tc.expectedTS, obtained, tc.description, 441 | ) 442 | 443 | require.Equal( 444 | t, tc.expectedCursorPos, p.cursor, tc.description, 445 | ) 446 | 447 | require.Equal( 448 | t, tc.expectedErr, err, tc.description, 449 | ) 450 | } 451 | } 452 | 453 | func TestParseTag(t *testing.T) { 454 | testCases := []struct { 455 | description string 456 | input string 457 | expectedTag string 458 | expectedCursorPos int 459 | expectedErr error 460 | }{ 461 | { 462 | description: "with pid", 463 | input: "apache2[10]:", 464 | expectedTag: "apache2", 465 | expectedCursorPos: 12, 466 | expectedErr: nil, 467 | }, 468 | { 469 | description: "without pid", 470 | input: "apache2:", 471 | expectedTag: "apache2", 472 | expectedCursorPos: 8, 473 | expectedErr: nil, 474 | }, 475 | { 476 | description: "trailing space", 477 | input: "apache2: ", 478 | expectedTag: "apache2", 479 | expectedCursorPos: 9, 480 | expectedErr: nil, 481 | }, 482 | { 483 | description: "super long", 484 | input: strings.Repeat("a", 50) + "", 485 | expectedTag: strings.Repeat("a", 32), 486 | expectedCursorPos: 32, 487 | expectedErr: nil, 488 | }, 489 | } 490 | 491 | for _, tc := range testCases { 492 | p := NewParser([]byte(tc.input)) 493 | obtained, err := p.parseTag() 494 | 495 | require.Equal( 496 | t, obtained, tc.expectedTag, tc.description, 497 | ) 498 | 499 | require.Equal( 500 | t, tc.expectedCursorPos, p.cursor, tc.description, 501 | ) 502 | 503 | require.Equal( 504 | t, tc.expectedErr, err, tc.description, 505 | ) 506 | } 507 | } 508 | 509 | func TestParseContent(t *testing.T) { 510 | buff := []byte(" foo bar baz quux ") 511 | content := string(bytes.Trim(buff, " ")) 512 | 513 | p := NewParser(buff) 514 | obtained, err := p.parseContent() 515 | 516 | require.Equal( 517 | t, err, parsercommon.ErrEOL, 518 | ) 519 | 520 | require.Equal( 521 | t, content, obtained, 522 | ) 523 | 524 | require.Equal( 525 | t, len(content), p.cursor, 526 | ) 527 | } 528 | 529 | func TestParseMessageSizeChecks(t *testing.T) { 530 | start := "<34>Oct 11 22:14:15 mymachine su: " 531 | msg := start + strings.Repeat("a", MAX_PACKET_LEN) 532 | 533 | p := NewParser([]byte(msg)) 534 | err := p.Parse() 535 | fields := p.Dump() 536 | 537 | require.Nil( 538 | t, err, 539 | ) 540 | 541 | require.Len( 542 | t, 543 | fields["content"], 544 | MAX_PACKET_LEN-len(start), 545 | ) 546 | 547 | // --- 548 | 549 | msg = start + "hello" 550 | p = NewParser([]byte(msg)) 551 | err = p.Parse() 552 | fields = p.Dump() 553 | 554 | require.Nil( 555 | t, err, 556 | ) 557 | 558 | require.Equal( 559 | t, "hello", fields["content"], 560 | ) 561 | } 562 | 563 | func TestParseWithoutTag(t *testing.T) { 564 | buff := []byte("<30>Jun 23 13:17:42 127.0.0.1 java.lang.NullPointerException") 565 | 566 | p := NewParser(buff) 567 | 568 | err := p.Parse() 569 | require.Nil(t, err) 570 | 571 | now := time.Now() 572 | 573 | obtained := p.Dump() 574 | expected := syslogparser.LogParts{ 575 | "timestamp": time.Date( 576 | now.Year(), time.June, 23, 577 | 13, 17, 42, 0, time.UTC, 578 | ), 579 | "hostname": "127.0.0.1", 580 | "tag": "java.lang.NullPointerException", 581 | "content": "", 582 | "priority": 30, 583 | "facility": 3, 584 | "severity": 6, 585 | } 586 | 587 | require.Equal( 588 | t, expected, obtained, 589 | ) 590 | } 591 | 592 | func BenchmarkParseTimestamp(b *testing.B) { 593 | buff := []byte("Oct 11 22:14:15") 594 | 595 | p := NewParser(buff) 596 | 597 | for i := 0; i < b.N; i++ { 598 | _, err := p.parseTimestamp() 599 | if err != nil { 600 | panic(err) 601 | } 602 | 603 | p.cursor = 0 604 | } 605 | } 606 | 607 | func BenchmarkParseHostname(b *testing.B) { 608 | buff := []byte("gimli.local") 609 | 610 | p := NewParser(buff) 611 | 612 | for i := 0; i < b.N; i++ { 613 | _, err := p.parseHostname() 614 | if err != nil { 615 | panic(err) 616 | } 617 | 618 | p.cursor = 0 619 | } 620 | } 621 | 622 | func BenchmarkParseTag(b *testing.B) { 623 | buff := []byte("apache2[10]:") 624 | 625 | p := NewParser(buff) 626 | 627 | for i := 0; i < b.N; i++ { 628 | _, err := p.parseTag() 629 | if err != nil { 630 | panic(err) 631 | } 632 | 633 | p.cursor = 0 634 | } 635 | } 636 | 637 | func BenchmarkParseHeader(b *testing.B) { 638 | buff := []byte("Oct 11 22:14:15 mymachine ") 639 | 640 | p := NewParser(buff) 641 | 642 | for i := 0; i < b.N; i++ { 643 | _, err := p.parseHeader() 644 | if err != nil { 645 | panic(err) 646 | } 647 | 648 | p.cursor = 0 649 | } 650 | } 651 | 652 | func BenchmarkParsemessage(b *testing.B) { 653 | buff := []byte("sometag[123]: foo bar baz blah quux") 654 | 655 | p := NewParser(buff) 656 | 657 | for i := 0; i < b.N; i++ { 658 | _, err := p.parsemessage() 659 | if err != parsercommon.ErrEOL { 660 | panic(err) 661 | } 662 | 663 | p.cursor = 0 664 | } 665 | } 666 | 667 | func BenchmarkParseFull(b *testing.B) { 668 | msg := "<34>Oct 11 22:14:15 mymachine su: 'su root' failed for lonvick on /dev/pts/8" 669 | 670 | for i := 0; i < b.N; i++ { 671 | p := NewParser( 672 | []byte(msg), 673 | ) 674 | 675 | err := p.Parse() 676 | if err != nil { 677 | panic(err) 678 | } 679 | } 680 | } 681 | 682 | func TestBenchmarkParseTimestamp(t *testing.T) { 683 | type args struct { 684 | b *testing.B 685 | } 686 | tests := []struct { 687 | name string 688 | args args 689 | }{ 690 | // TODO: Add test cases. 691 | } 692 | for _, tt := range tests { 693 | t.Run(tt.name, func(t *testing.T) { 694 | BenchmarkParseTimestamp(tt.args.b) 695 | }) 696 | } 697 | } 698 | 699 | func TestBenchmarkParseHostname(t *testing.T) { 700 | type args struct { 701 | b *testing.B 702 | } 703 | tests := []struct { 704 | name string 705 | args args 706 | }{ 707 | // TODO: Add test cases. 708 | } 709 | for _, tt := range tests { 710 | t.Run(tt.name, func(t *testing.T) { 711 | BenchmarkParseHostname(tt.args.b) 712 | }) 713 | } 714 | } 715 | 716 | func TestBenchmarkParseTag(t *testing.T) { 717 | type args struct { 718 | b *testing.B 719 | } 720 | tests := []struct { 721 | name string 722 | args args 723 | }{ 724 | // TODO: Add test cases. 725 | } 726 | for _, tt := range tests { 727 | t.Run(tt.name, func(t *testing.T) { 728 | BenchmarkParseTag(tt.args.b) 729 | }) 730 | } 731 | } 732 | 733 | func TestBenchmarkParseHeader(t *testing.T) { 734 | type args struct { 735 | b *testing.B 736 | } 737 | tests := []struct { 738 | name string 739 | args args 740 | }{ 741 | // TODO: Add test cases. 742 | } 743 | for _, tt := range tests { 744 | t.Run(tt.name, func(t *testing.T) { 745 | BenchmarkParseHeader(tt.args.b) 746 | }) 747 | } 748 | } 749 | 750 | func TestBenchmarkParsemessage(t *testing.T) { 751 | type args struct { 752 | b *testing.B 753 | } 754 | tests := []struct { 755 | name string 756 | args args 757 | }{ 758 | // TODO: Add test cases. 759 | } 760 | for _, tt := range tests { 761 | t.Run(tt.name, func(t *testing.T) { 762 | BenchmarkParsemessage(tt.args.b) 763 | }) 764 | } 765 | } 766 | 767 | func TestBenchmarkParseFull(t *testing.T) { 768 | type args struct { 769 | b *testing.B 770 | } 771 | tests := []struct { 772 | name string 773 | args args 774 | }{ 775 | // TODO: Add test cases. 776 | } 777 | for _, tt := range tests { 778 | t.Run(tt.name, func(t *testing.T) { 779 | BenchmarkParseFull(tt.args.b) 780 | }) 781 | } 782 | } 783 | -------------------------------------------------------------------------------- /rfc5424/example_test.go: -------------------------------------------------------------------------------- 1 | package rfc5424_test 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/jeromer/syslogparser/rfc5424" 7 | ) 8 | 9 | func ExampleNewParser() { 10 | b := `<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...` 11 | buff := []byte(b) 12 | 13 | p := rfc5424.NewParser(buff) 14 | err := p.Parse() 15 | if err != nil { 16 | panic(err) 17 | } 18 | 19 | for k, v := range p.Dump() { 20 | fmt.Println(k, ":", v) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /rfc5424/rfc5424.go: -------------------------------------------------------------------------------- 1 | package rfc5424 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "math" 7 | "strconv" 8 | "time" 9 | 10 | "github.com/jeromer/syslogparser" 11 | "github.com/jeromer/syslogparser/parsercommon" 12 | ) 13 | 14 | const ( 15 | NILVALUE = '-' 16 | 17 | // according to https://tools.ietf.org/html/rfc5424#section-6.1 18 | // the length of the packet MUST be 2048 bytes or less. 19 | // However we will accept a bit more while protecting from exhaustion 20 | MAX_PACKET_LEN = 3048 21 | ) 22 | 23 | var ( 24 | ErrYearInvalid = &parsercommon.ParserError{ErrorString: "Invalid year in timestamp"} 25 | ErrMonthInvalid = &parsercommon.ParserError{ErrorString: "Invalid month in timestamp"} 26 | ErrDayInvalid = &parsercommon.ParserError{ErrorString: "Invalid day in timestamp"} 27 | ErrHourInvalid = &parsercommon.ParserError{ErrorString: "Invalid hour in timestamp"} 28 | ErrMinuteInvalid = &parsercommon.ParserError{ErrorString: "Invalid minute in timestamp"} 29 | ErrSecondInvalid = &parsercommon.ParserError{ErrorString: "Invalid second in timestamp"} 30 | ErrSecFracInvalid = &parsercommon.ParserError{ErrorString: "Invalid fraction of second in timestamp"} 31 | ErrTimeZoneInvalid = &parsercommon.ParserError{ErrorString: "Invalid time zone in timestamp"} 32 | ErrInvalidTimeFormat = &parsercommon.ParserError{ErrorString: "Invalid time format"} 33 | ErrInvalidAppName = &parsercommon.ParserError{ErrorString: "Invalid app name"} 34 | ErrInvalidProcId = &parsercommon.ParserError{ErrorString: "Invalid proc ID"} 35 | ErrInvalidMsgId = &parsercommon.ParserError{ErrorString: "Invalid msg ID"} 36 | ErrNoStructuredData = &parsercommon.ParserError{ErrorString: "No structured data"} 37 | ) 38 | 39 | type Parser struct { 40 | buff []byte 41 | cursor int 42 | l int 43 | header *header 44 | structuredData string 45 | message string 46 | 47 | tmpHostname string 48 | tmpPriority *parsercommon.Priority 49 | } 50 | 51 | type header struct { 52 | priority *parsercommon.Priority 53 | version int 54 | timestamp time.Time 55 | hostname string 56 | appName string 57 | procId string 58 | msgId string 59 | } 60 | 61 | type partialTime struct { 62 | hour int 63 | minute int 64 | seconds int 65 | secFrac float64 66 | } 67 | 68 | type fullTime struct { 69 | pt *partialTime 70 | loc *time.Location 71 | } 72 | 73 | type fullDate struct { 74 | year int 75 | month int 76 | day int 77 | } 78 | 79 | func NewParser(buff []byte) *Parser { 80 | return &Parser{ 81 | buff: buff, 82 | cursor: 0, 83 | l: int( 84 | math.Min( 85 | float64(len(buff)), 86 | MAX_PACKET_LEN, 87 | ), 88 | ), 89 | } 90 | } 91 | 92 | // Forces a priority for this parser. Priority will not be parsed. 93 | func (p *Parser) WithPriority(pri *parsercommon.Priority) { 94 | p.tmpPriority = pri 95 | } 96 | 97 | // Noop as RFC5424 syslog always has a timezone 98 | func (p *Parser) WithLocation(l *time.Location) {} 99 | 100 | // Noop as RFC5424 is strict about timestamp format 101 | func (p *Parser) WithTimestampFormat(s string) {} 102 | 103 | // Forces a hostname. Hostname will not be parsed 104 | func (p *Parser) WithHostname(h string) { 105 | p.tmpHostname = h 106 | } 107 | 108 | // Noop as RFC5424 as no tag per se: 109 | // The TAG has been split into APP-NAME, PROCID, and MSGID. 110 | // Ref: https://tools.ietf.org/html/rfc5424#appendix-A.1 111 | func (p *Parser) WithTag(t string) { 112 | } 113 | 114 | // DEPRECATED. Use WithLocation() instead 115 | func (p *Parser) Location(location *time.Location) { 116 | } 117 | 118 | func (p *Parser) Parse() error { 119 | hdr, err := p.parseHeader() 120 | if err != nil { 121 | return err 122 | } 123 | 124 | p.header = hdr 125 | 126 | sd, err := p.parseStructuredData() 127 | if err != nil { 128 | return err 129 | } 130 | 131 | p.structuredData = sd 132 | p.cursor++ 133 | 134 | if p.cursor < p.l { 135 | p.message = string( 136 | bytes.Trim( 137 | p.buff[p.cursor:p.l], " ", 138 | ), 139 | ) 140 | } 141 | 142 | return nil 143 | } 144 | 145 | func (p *Parser) Dump() syslogparser.LogParts { 146 | return syslogparser.LogParts{ 147 | "priority": p.header.priority.P, 148 | "facility": p.header.priority.F.Value, 149 | "severity": p.header.priority.S.Value, 150 | "version": p.header.version, 151 | "timestamp": p.header.timestamp, 152 | "hostname": p.header.hostname, 153 | "app_name": p.header.appName, 154 | "proc_id": p.header.procId, 155 | "msg_id": p.header.msgId, 156 | "structured_data": p.structuredData, 157 | "message": p.message, 158 | } 159 | } 160 | 161 | // HEADER = PRI VERSION SP TIMESTAMP SP HOSTNAME SP APP-NAME SP PROCID SP MSGID 162 | func (p *Parser) parseHeader() (*header, error) { 163 | pri, err := p.parsePriority() 164 | if err != nil { 165 | return nil, err 166 | } 167 | 168 | ver, err := p.parseVersion() 169 | if err != nil { 170 | return nil, err 171 | } 172 | 173 | p.cursor++ 174 | 175 | ts, err := p.parseTimestamp() 176 | if err != nil { 177 | return nil, err 178 | } 179 | 180 | p.cursor++ 181 | 182 | host, err := p.parseHostname() 183 | if err != nil { 184 | return nil, err 185 | } 186 | 187 | // cursor is moved in p.parseHostname() 188 | 189 | appName, err := p.parseAppName() 190 | if err != nil { 191 | return nil, err 192 | } 193 | 194 | p.cursor++ 195 | 196 | procId, err := p.parseProcId() 197 | if err != nil { 198 | return nil, err 199 | } 200 | 201 | p.cursor++ 202 | 203 | msgId, err := p.parseMsgId() 204 | if err != nil { 205 | return nil, err 206 | } 207 | 208 | p.cursor++ 209 | 210 | hdr := &header{ 211 | version: ver, 212 | timestamp: *ts, 213 | priority: pri, 214 | hostname: host, 215 | procId: procId, 216 | msgId: msgId, 217 | appName: appName, 218 | } 219 | 220 | return hdr, nil 221 | } 222 | 223 | func (p *Parser) parsePriority() (*parsercommon.Priority, error) { 224 | if p.tmpPriority != nil { 225 | return p.tmpPriority, nil 226 | } 227 | 228 | return parsercommon.ParsePriority( 229 | p.buff, &p.cursor, p.l, 230 | ) 231 | } 232 | 233 | func (p *Parser) parseVersion() (int, error) { 234 | return parsercommon.ParseVersion(p.buff, &p.cursor, p.l) 235 | } 236 | 237 | // https://tools.ietf.org/html/rfc5424#section-6.2.3 238 | func (p *Parser) parseTimestamp() (*time.Time, error) { 239 | if p.buff[p.cursor] == NILVALUE { 240 | p.cursor++ 241 | return new(time.Time), nil 242 | } 243 | 244 | fd, err := parseFullDate( 245 | p.buff, &p.cursor, p.l, 246 | ) 247 | 248 | if err != nil { 249 | return nil, err 250 | } 251 | 252 | if p.buff[p.cursor] != 'T' { 253 | return nil, ErrInvalidTimeFormat 254 | } 255 | 256 | p.cursor++ 257 | 258 | ft, err := parseFullTime( 259 | p.buff, &p.cursor, p.l, 260 | ) 261 | 262 | if err != nil { 263 | return nil, parsercommon.ErrTimestampUnknownFormat 264 | } 265 | 266 | nSec, err := toNSec( 267 | ft.pt.secFrac, 268 | ) 269 | 270 | if err != nil { 271 | return nil, err 272 | } 273 | 274 | ts := time.Date( 275 | fd.year, 276 | time.Month(fd.month), 277 | fd.day, 278 | ft.pt.hour, 279 | ft.pt.minute, 280 | ft.pt.seconds, 281 | nSec, 282 | ft.loc, 283 | ) 284 | 285 | return &ts, nil 286 | } 287 | 288 | // HOSTNAME = NILVALUE / 1*255PRINTUSASCII 289 | func (p *Parser) parseHostname() (string, error) { 290 | if p.tmpHostname != "" { 291 | return p.tmpHostname, nil 292 | } 293 | 294 | h, err := parsercommon.ParseHostname(p.buff, &p.cursor, p.l) 295 | 296 | p.cursor++ 297 | 298 | return h, err 299 | } 300 | 301 | // APP-NAME = NILVALUE / 1*48PRINTUSASCII 302 | func (p *Parser) parseAppName() (string, error) { 303 | return parseUpToLen(p.buff, &p.cursor, p.l, 48, ErrInvalidAppName) 304 | } 305 | 306 | // PROCID = NILVALUE / 1*128PRINTUSASCII 307 | func (p *Parser) parseProcId() (string, error) { 308 | return parseUpToLen(p.buff, &p.cursor, p.l, 128, ErrInvalidProcId) 309 | } 310 | 311 | // MSGID = NILVALUE / 1*32PRINTUSASCII 312 | func (p *Parser) parseMsgId() (string, error) { 313 | return parseUpToLen( 314 | p.buff, &p.cursor, p.l, 32, ErrInvalidMsgId, 315 | ) 316 | } 317 | 318 | func (p *Parser) parseStructuredData() (string, error) { 319 | return parseStructuredData(p.buff, &p.cursor, p.l) 320 | } 321 | 322 | // ---------------------------------------------- 323 | // https://tools.ietf.org/html/rfc5424#section-6 324 | // ---------------------------------------------- 325 | 326 | // XXX : bind them to Parser ? 327 | 328 | // FULL-DATE : DATE-FULLYEAR "-" DATE-MONTH "-" DATE-MDAY 329 | func parseFullDate(buff []byte, cursor *int, l int) (fullDate, error) { 330 | var fd fullDate 331 | 332 | year, err := parseYear(buff, cursor, l) 333 | if err != nil { 334 | return fd, err 335 | } 336 | 337 | if buff[*cursor] != '-' { 338 | return fd, parsercommon.ErrTimestampUnknownFormat 339 | } 340 | 341 | *cursor++ 342 | 343 | month, err := parseMonth(buff, cursor, l) 344 | if err != nil { 345 | return fd, err 346 | } 347 | 348 | if buff[*cursor] != '-' { 349 | return fd, parsercommon.ErrTimestampUnknownFormat 350 | } 351 | 352 | *cursor++ 353 | 354 | day, err := parseDay(buff, cursor, l) 355 | if err != nil { 356 | return fd, err 357 | } 358 | 359 | fd = fullDate{ 360 | year: year, 361 | month: month, 362 | day: day, 363 | } 364 | 365 | return fd, nil 366 | } 367 | 368 | // DATE-FULLYEAR = 4DIGIT 369 | func parseYear(buff []byte, cursor *int, l int) (int, error) { 370 | yearLen := 4 371 | 372 | if *cursor+yearLen > l { 373 | return 0, parsercommon.ErrEOL 374 | } 375 | 376 | // XXX : we do not check for a valid year (ie. 1999, 2013 etc) 377 | // XXX : we only checks the format is correct 378 | sub := string(buff[*cursor : *cursor+yearLen]) 379 | 380 | *cursor += yearLen 381 | 382 | year, err := strconv.Atoi(sub) 383 | if err != nil { 384 | return 0, ErrYearInvalid 385 | } 386 | 387 | return year, nil 388 | } 389 | 390 | // DATE-MONTH = 2DIGIT ; 01-12 391 | func parseMonth(buff []byte, cursor *int, l int) (int, error) { 392 | return parsercommon.Parse2Digits(buff, cursor, l, 1, 12, ErrMonthInvalid) 393 | } 394 | 395 | // DATE-MDAY = 2DIGIT ; 01-28, 01-29, 01-30, 01-31 based on month/year 396 | func parseDay(buff []byte, cursor *int, l int) (int, error) { 397 | // XXX : this is a relaxed constraint 398 | // XXX : we do not check if valid regarding February or leap years 399 | // XXX : we only checks that day is in range [01 -> 31] 400 | // XXX : in other words this function will not rant if you provide Feb 31th 401 | return parsercommon.Parse2Digits(buff, cursor, l, 1, 31, ErrDayInvalid) 402 | } 403 | 404 | // FULL-TIME = PARTIAL-TIME TIME-OFFSET 405 | func parseFullTime(buff []byte, cursor *int, l int) (*fullTime, error) { 406 | pt, err := parsePartialTime(buff, cursor, l) 407 | if err != nil { 408 | return nil, err 409 | } 410 | 411 | loc, err := parseTimeOffset(buff, cursor, l) 412 | if err != nil { 413 | return nil, err 414 | } 415 | 416 | ft := &fullTime{ 417 | pt: pt, 418 | loc: loc, 419 | } 420 | 421 | return ft, nil 422 | } 423 | 424 | // PARTIAL-TIME = TIME-HOUR ":" TIME-MINUTE ":" TIME-SECOND[TIME-SECFRAC] 425 | func parsePartialTime(buff []byte, cursor *int, l int) (*partialTime, error) { 426 | hour, minute, err := getHourMinute( 427 | buff, cursor, l, 428 | ) 429 | 430 | if err != nil { 431 | return nil, err 432 | } 433 | 434 | if buff[*cursor] != ':' { 435 | return nil, ErrInvalidTimeFormat 436 | } 437 | 438 | *cursor++ 439 | 440 | // ---- 441 | 442 | seconds, err := parseSecond( 443 | buff, cursor, l, 444 | ) 445 | 446 | if err != nil { 447 | return nil, err 448 | } 449 | 450 | pt := &partialTime{ 451 | hour: hour, 452 | minute: minute, 453 | seconds: seconds, 454 | } 455 | 456 | // ---- 457 | 458 | if buff[*cursor] != '.' { 459 | return pt, nil 460 | } 461 | 462 | *cursor++ 463 | 464 | secFrac, err := parseSecFrac( 465 | buff, cursor, l, 466 | ) 467 | 468 | if err != nil { 469 | return pt, nil 470 | } 471 | 472 | pt.secFrac = secFrac 473 | 474 | return pt, nil 475 | } 476 | 477 | // TIME-HOUR = 2DIGIT ; 00-23 478 | func parseHour(buff []byte, cursor *int, l int) (int, error) { 479 | return parsercommon.Parse2Digits(buff, cursor, l, 0, 23, ErrHourInvalid) 480 | } 481 | 482 | // TIME-MINUTE = 2DIGIT ; 00-59 483 | func parseMinute(buff []byte, cursor *int, l int) (int, error) { 484 | return parsercommon.Parse2Digits(buff, cursor, l, 0, 59, ErrMinuteInvalid) 485 | } 486 | 487 | // TIME-SECOND = 2DIGIT ; 00-59 488 | func parseSecond(buff []byte, cursor *int, l int) (int, error) { 489 | return parsercommon.Parse2Digits(buff, cursor, l, 0, 59, ErrSecondInvalid) 490 | } 491 | 492 | // TIME-SECFRAC = "." 1*6DIGIT 493 | func parseSecFrac(buff []byte, cursor *int, l int) (float64, error) { 494 | maxDigitLen := 6 495 | 496 | max := *cursor + maxDigitLen 497 | from := *cursor 498 | to := 0 499 | 500 | for to = from; to < max; to++ { 501 | if to >= l { 502 | break 503 | } 504 | 505 | c := buff[to] 506 | if !parsercommon.IsDigit(c) { 507 | break 508 | } 509 | } 510 | 511 | sub := string(buff[from:to]) 512 | if len(sub) == 0 { 513 | return 0, ErrSecFracInvalid 514 | } 515 | 516 | secFrac, err := strconv.ParseFloat("0."+sub, 64) 517 | *cursor = to 518 | if err != nil { 519 | return 0, ErrSecFracInvalid 520 | } 521 | 522 | return secFrac, nil 523 | } 524 | 525 | // TIME-OFFSET = "Z" / TIME-NUMOFFSET 526 | func parseTimeOffset(buff []byte, cursor *int, l int) (*time.Location, error) { 527 | 528 | if buff[*cursor] == 'Z' { 529 | *cursor++ 530 | return time.UTC, nil 531 | } 532 | 533 | return parseNumericalTimeOffset(buff, cursor, l) 534 | } 535 | 536 | // TIME-NUMOFFSET = ("+" / "-") TIME-HOUR ":" TIME-MINUTE 537 | func parseNumericalTimeOffset(buff []byte, cursor *int, l int) (*time.Location, error) { 538 | var loc = new(time.Location) 539 | 540 | sign := buff[*cursor] 541 | 542 | if (sign != '+') && (sign != '-') { 543 | return loc, ErrTimeZoneInvalid 544 | } 545 | 546 | *cursor++ 547 | 548 | hour, minute, err := getHourMinute(buff, cursor, l) 549 | if err != nil { 550 | return loc, err 551 | } 552 | 553 | tzStr := fmt.Sprintf("%s%02d:%02d", string(sign), hour, minute) 554 | tmpTs, err := time.Parse("-07:00", tzStr) 555 | if err != nil { 556 | return loc, err 557 | } 558 | 559 | return tmpTs.Location(), nil 560 | } 561 | 562 | func getHourMinute(buff []byte, cursor *int, l int) (int, int, error) { 563 | hour, err := parseHour(buff, cursor, l) 564 | if err != nil { 565 | return 0, 0, err 566 | } 567 | 568 | if buff[*cursor] != ':' { 569 | return 0, 0, ErrInvalidTimeFormat 570 | } 571 | 572 | *cursor++ 573 | 574 | minute, err := parseMinute(buff, cursor, l) 575 | if err != nil { 576 | return 0, 0, err 577 | } 578 | 579 | return hour, minute, nil 580 | } 581 | 582 | func toNSec(sec float64) (int, error) { 583 | _, frac := math.Modf(sec) 584 | fracStr := strconv.FormatFloat(frac, 'f', 9, 64) 585 | fracInt, err := strconv.Atoi(fracStr[2:]) 586 | if err != nil { 587 | return 0, err 588 | } 589 | 590 | return fracInt, nil 591 | } 592 | 593 | // ------------------------------------------------ 594 | // https://tools.ietf.org/html/rfc5424#section-6.3 595 | // ------------------------------------------------ 596 | 597 | func parseStructuredData(buff []byte, cursor *int, l int) (string, error) { 598 | var sdData string 599 | var found bool 600 | 601 | if buff[*cursor] == NILVALUE { 602 | *cursor++ 603 | return "-", nil 604 | } 605 | 606 | if buff[*cursor] != '[' { 607 | return sdData, ErrNoStructuredData 608 | } 609 | 610 | from := *cursor 611 | to := 0 612 | 613 | for to = from; to < l; to++ { 614 | if found { 615 | break 616 | } 617 | 618 | b := buff[to] 619 | 620 | if b == ']' { 621 | switch t := to + 1; { 622 | case t == l: 623 | found = true 624 | case t <= l && buff[t] == ' ': 625 | found = true 626 | } 627 | } 628 | } 629 | 630 | if found { 631 | *cursor = to 632 | return string(buff[from:to]), nil 633 | } 634 | 635 | return sdData, ErrNoStructuredData 636 | } 637 | 638 | func parseUpToLen(buff []byte, cursor *int, l int, maxLen int, e error) (string, error) { 639 | var to int 640 | var found bool 641 | var result string 642 | 643 | max := *cursor + maxLen 644 | 645 | for to = *cursor; (to < max) && (to < l); to++ { 646 | if buff[to] == ' ' { 647 | found = true 648 | break 649 | } 650 | } 651 | 652 | if found { 653 | result = string(buff[*cursor:to]) 654 | } 655 | 656 | *cursor = to 657 | 658 | if found { 659 | return result, nil 660 | } 661 | 662 | return "", e 663 | } 664 | -------------------------------------------------------------------------------- /rfc5424/rfc5424_test.go: -------------------------------------------------------------------------------- 1 | package rfc5424 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "testing" 7 | "time" 8 | 9 | "github.com/jeromer/syslogparser" 10 | "github.com/jeromer/syslogparser/parsercommon" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func TestParser(t *testing.T) { 15 | tmpTZ, err := time.Parse("-07:00", "-07:00") 16 | require.Nil(t, err) 17 | 18 | testCases := []struct { 19 | description string 20 | input string 21 | expectedParts syslogparser.LogParts 22 | }{ 23 | { 24 | description: "no STRUCTURED-DATA 1/2", 25 | input: "<34>1 2003-10-11T22:14:15.003Z mymachine.example.com su - ID47 - 'su root' failed for lonvick on /dev/pts/8", 26 | expectedParts: syslogparser.LogParts{ 27 | "priority": 34, 28 | "facility": 4, 29 | "severity": 2, 30 | "version": 1, 31 | "timestamp": time.Date( 32 | 2003, time.October, 11, 33 | 22, 14, 15, 3*10e5, 34 | time.UTC, 35 | ), 36 | "hostname": "mymachine.example.com", 37 | "app_name": "su", 38 | "proc_id": "-", 39 | "msg_id": "ID47", 40 | "structured_data": "-", 41 | "message": "'su root' failed for lonvick on /dev/pts/8", 42 | }, 43 | }, 44 | { 45 | description: "no STRUCTURED_DATA 2/2", 46 | input: "<165>1 2003-08-24T05:14:15.000003-07:00 192.0.2.1 myproc 8710 - - %% It's time to make the do-nuts.", 47 | expectedParts: syslogparser.LogParts{ 48 | "priority": 165, 49 | "facility": 20, 50 | "severity": 5, 51 | "version": 1, 52 | "timestamp": time.Date( 53 | 2003, time.August, 24, 54 | 5, 14, 15, 3*10e2, 55 | tmpTZ.Location(), 56 | ), 57 | "hostname": "192.0.2.1", 58 | "app_name": "myproc", 59 | "proc_id": "8710", 60 | "msg_id": "-", 61 | "structured_data": "-", 62 | "message": "%% It's time to make the do-nuts.", 63 | }, 64 | }, 65 | { 66 | description: "with STRUCTURED_DATA", 67 | input: `<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...`, 68 | expectedParts: syslogparser.LogParts{ 69 | "priority": 165, 70 | "facility": 20, 71 | "severity": 5, 72 | "version": 1, 73 | "timestamp": time.Date( 74 | 2003, time.October, 11, 75 | 22, 14, 15, 3*10e5, 76 | time.UTC, 77 | ), 78 | "hostname": "mymachine.example.com", 79 | "app_name": "evntslog", 80 | "proc_id": "-", 81 | "msg_id": "ID47", 82 | "structured_data": `[exampleSDID@32473 iut="3" eventSource="Application" eventID="1011"]`, 83 | "message": "An application event log entry...", 84 | }, 85 | }, 86 | { 87 | description: "STRUCTURED_DATA only", 88 | input: `<165>1 2003-10-11T22:14:15.003Z mymachine.example.com evntslog - ID47 [exampleSDID@32473 iut="3" eventSource= "Application" eventID="1011"][examplePriority@32473 class="high"]`, 89 | expectedParts: syslogparser.LogParts{ 90 | "priority": 165, 91 | "facility": 20, 92 | "severity": 5, 93 | "version": 1, 94 | "timestamp": time.Date( 95 | 2003, time.October, 11, 96 | 22, 14, 15, 3*10e5, 97 | time.UTC, 98 | ), 99 | "hostname": "mymachine.example.com", 100 | "app_name": "evntslog", 101 | "proc_id": "-", 102 | "msg_id": "ID47", 103 | "structured_data": `[exampleSDID@32473 iut="3" eventSource= "Application" eventID="1011"][examplePriority@32473 class="high"]`, 104 | "message": "", 105 | }, 106 | }, 107 | } 108 | 109 | for _, tc := range testCases { 110 | buff := []byte(tc.input) 111 | 112 | p := NewParser(buff) 113 | require.Equal( 114 | t, 115 | &Parser{ 116 | buff: buff, 117 | cursor: 0, 118 | l: len(tc.input), 119 | }, 120 | p, 121 | tc.description, 122 | ) 123 | 124 | err := p.Parse() 125 | require.Nil(t, err) 126 | 127 | obtained := p.Dump() 128 | for k, v := range obtained { 129 | require.Equal( 130 | t, tc.expectedParts[k], v, tc.description, 131 | ) 132 | } 133 | } 134 | } 135 | 136 | func TestParseWithHostname(t *testing.T) { 137 | buff := []byte( 138 | "<34>1 2003-10-11T22:14:15.003Z su - ID47 - 'su root' failed for lonvick on /dev/pts/8", 139 | ) 140 | 141 | p := NewParser(buff) 142 | p.WithHostname("mymachine.example.com") 143 | 144 | require.Equal( 145 | t, 146 | &Parser{ 147 | buff: buff, 148 | cursor: 0, 149 | l: len(buff), 150 | tmpHostname: "mymachine.example.com", 151 | }, 152 | p, 153 | ) 154 | 155 | err := p.Parse() 156 | require.Nil(t, err) 157 | 158 | require.Equal( 159 | t, syslogparser.LogParts{ 160 | "priority": 34, 161 | "facility": 4, 162 | "severity": 2, 163 | "version": 1, 164 | "timestamp": time.Date( 165 | 2003, time.October, 11, 166 | 22, 14, 15, 3*10e5, 167 | time.UTC, 168 | ), 169 | "hostname": "mymachine.example.com", 170 | "app_name": "su", 171 | "proc_id": "-", 172 | "msg_id": "ID47", 173 | "structured_data": "-", 174 | "message": "'su root' failed for lonvick on /dev/pts/8", 175 | }, p.Dump(), 176 | ) 177 | } 178 | 179 | func TestParseWithPriority(t *testing.T) { 180 | buff := []byte( 181 | "1 2003-10-11T22:14:15.003Z mymachine.example.com su - ID47 - 'su root' failed for lonvick on /dev/pts/8", 182 | ) 183 | 184 | pri := parsercommon.NewPriority(34) 185 | 186 | p := NewParser(buff) 187 | p.WithPriority(pri) 188 | 189 | require.Equal( 190 | t, 191 | &Parser{ 192 | buff: buff, 193 | cursor: 0, 194 | l: len(buff), 195 | tmpPriority: pri, 196 | }, 197 | p, 198 | ) 199 | 200 | err := p.Parse() 201 | require.Nil(t, err) 202 | 203 | require.Equal( 204 | t, syslogparser.LogParts{ 205 | "priority": 34, 206 | "facility": 4, 207 | "severity": 2, 208 | "version": 1, 209 | "timestamp": time.Date( 210 | 2003, time.October, 11, 211 | 22, 14, 15, 3*10e5, 212 | time.UTC, 213 | ), 214 | "hostname": "mymachine.example.com", 215 | "app_name": "su", 216 | "proc_id": "-", 217 | "msg_id": "ID47", 218 | "structured_data": "-", 219 | "message": "'su root' failed for lonvick on /dev/pts/8", 220 | }, p.Dump(), 221 | ) 222 | } 223 | 224 | func TestParseWithPriorityAndHostname(t *testing.T) { 225 | buff := []byte( 226 | "1 2003-10-11T22:14:15.003Z su - ID47 - 'su root' failed for lonvick on /dev/pts/8", 227 | ) 228 | 229 | pri := parsercommon.NewPriority(34) 230 | 231 | p := NewParser(buff) 232 | p.WithPriority(pri) 233 | p.WithHostname("mymachine.example.com") 234 | 235 | require.Equal( 236 | t, 237 | &Parser{ 238 | buff: buff, 239 | cursor: 0, 240 | l: len(buff), 241 | tmpHostname: "mymachine.example.com", 242 | tmpPriority: pri, 243 | }, 244 | p, 245 | ) 246 | 247 | err := p.Parse() 248 | require.Nil(t, err) 249 | 250 | require.Equal( 251 | t, syslogparser.LogParts{ 252 | "priority": 34, 253 | "facility": 4, 254 | "severity": 2, 255 | "version": 1, 256 | "timestamp": time.Date( 257 | 2003, time.October, 11, 258 | 22, 14, 15, 3*10e5, 259 | time.UTC, 260 | ), 261 | "hostname": "mymachine.example.com", 262 | "app_name": "su", 263 | "proc_id": "-", 264 | "msg_id": "ID47", 265 | "structured_data": "-", 266 | "message": "'su root' failed for lonvick on /dev/pts/8", 267 | }, p.Dump(), 268 | ) 269 | } 270 | 271 | func TestParseHeader(t *testing.T) { 272 | ts := time.Date(2003, time.October, 11, 22, 14, 15, 3*10e5, time.UTC) 273 | tsString := "2003-10-11T22:14:15.003Z" 274 | hostname := "mymachine.example.com" 275 | appName := "su" 276 | procId := "123" 277 | msgId := "ID47" 278 | nilValue := string(NILVALUE) 279 | headerFmt := "<165>1 %s %s %s %s %s " 280 | pri := &parsercommon.Priority{ 281 | P: 165, 282 | F: parsercommon.Facility{Value: 20}, 283 | S: parsercommon.Severity{Value: 5}, 284 | } 285 | testCases := []struct { 286 | description string 287 | input string 288 | expectedHdr *header 289 | }{ 290 | { 291 | description: "HEADER complete", 292 | input: fmt.Sprintf(headerFmt, tsString, hostname, appName, procId, msgId), 293 | expectedHdr: &header{ 294 | priority: pri, 295 | version: 1, 296 | timestamp: ts, 297 | hostname: hostname, 298 | appName: appName, 299 | procId: procId, 300 | msgId: msgId, 301 | }, 302 | }, 303 | { 304 | description: "TIMESTAMP as NILVALUE", 305 | input: fmt.Sprintf(headerFmt, nilValue, hostname, appName, procId, msgId), 306 | expectedHdr: &header{ 307 | priority: pri, 308 | version: 1, 309 | timestamp: time.Time{}, 310 | hostname: hostname, 311 | appName: appName, 312 | procId: procId, 313 | msgId: msgId, 314 | }, 315 | }, 316 | { 317 | description: "HOSTNAME as NILVALUE", 318 | input: fmt.Sprintf(headerFmt, tsString, nilValue, appName, procId, msgId), 319 | expectedHdr: &header{ 320 | priority: pri, 321 | version: 1, 322 | timestamp: ts, 323 | hostname: nilValue, 324 | appName: appName, 325 | procId: procId, 326 | msgId: msgId, 327 | }, 328 | }, 329 | { 330 | description: "APP-NAME as NILVALUE", 331 | input: fmt.Sprintf(headerFmt, tsString, hostname, nilValue, procId, msgId), 332 | expectedHdr: &header{ 333 | priority: pri, 334 | version: 1, 335 | timestamp: ts, 336 | hostname: hostname, 337 | appName: nilValue, 338 | procId: procId, 339 | msgId: msgId, 340 | }, 341 | }, 342 | { 343 | description: "PROCID as NILVALUE", 344 | input: fmt.Sprintf(headerFmt, tsString, hostname, appName, nilValue, msgId), 345 | expectedHdr: &header{ 346 | priority: pri, 347 | version: 1, 348 | timestamp: ts, 349 | hostname: hostname, 350 | appName: appName, 351 | procId: nilValue, 352 | msgId: msgId, 353 | }, 354 | }, 355 | { 356 | description: "MSGID as NILVALUE", 357 | input: fmt.Sprintf(headerFmt, tsString, hostname, appName, procId, nilValue), 358 | expectedHdr: &header{ 359 | priority: pri, 360 | version: 1, 361 | timestamp: ts, 362 | hostname: hostname, 363 | appName: appName, 364 | procId: procId, 365 | msgId: nilValue, 366 | }, 367 | }, 368 | } 369 | 370 | for _, tc := range testCases { 371 | p := NewParser([]byte(tc.input)) 372 | obtained, err := p.parseHeader() 373 | 374 | require.Nil( 375 | t, err, tc.description, 376 | ) 377 | 378 | require.Equal( 379 | t, tc.expectedHdr, obtained, tc.description, 380 | ) 381 | 382 | require.Equal( 383 | t, len(tc.input), p.cursor, tc.description, 384 | ) 385 | } 386 | } 387 | 388 | func TestParseTimestamp(t *testing.T) { 389 | tz := "-04:00" 390 | tmpTZ, err := time.Parse("-07:00", tz) 391 | require.Nil(t, err) 392 | require.NotNil(t, tmpTZ) 393 | 394 | dt1 := time.Date( 395 | 1985, time.April, 12, 396 | 23, 20, 50, 52*10e6, 397 | time.UTC, 398 | ) 399 | 400 | dt2 := time.Date( 401 | 1985, time.April, 12, 402 | 19, 20, 50, 52*10e6, 403 | tmpTZ.Location(), 404 | ) 405 | 406 | dt3 := time.Date( 407 | 2003, time.October, 11, 408 | 22, 14, 15, 3*10e5, 409 | time.UTC, 410 | ) 411 | 412 | dt4 := time.Date( 413 | 2003, time.August, 24, 414 | 5, 14, 15, 3*10e2, 415 | tmpTZ.Location(), 416 | ) 417 | testCases := []struct { 418 | description string 419 | input string 420 | expectedTS *time.Time 421 | expectedCursorPos int 422 | expectedErr error 423 | }{ 424 | { 425 | description: "UTC timestamp", 426 | input: "1985-04-12T23:20:50.52Z", 427 | expectedTS: &dt1, 428 | expectedCursorPos: 23, 429 | expectedErr: nil, 430 | }, 431 | { 432 | description: "numeric timezone", 433 | input: "1985-04-12T19:20:50.52" + tz, 434 | expectedTS: &dt2, 435 | expectedCursorPos: 28, 436 | expectedErr: nil, 437 | }, 438 | { 439 | description: "timestamp with ms", 440 | input: "2003-10-11T22:14:15.003Z", 441 | expectedTS: &dt3, 442 | expectedCursorPos: 24, 443 | expectedErr: nil, 444 | }, 445 | { 446 | description: "timestamp with us", 447 | input: "2003-08-24T05:14:15.000003" + tz, 448 | expectedTS: &dt4, 449 | expectedCursorPos: 32, 450 | expectedErr: nil, 451 | }, 452 | { 453 | description: "timestamp with ns", 454 | input: "2003-08-24T05:14:15.000000003-07:00", 455 | expectedCursorPos: 26, 456 | expectedTS: nil, 457 | expectedErr: parsercommon.ErrTimestampUnknownFormat, 458 | }, 459 | { 460 | description: "nil timestamp", 461 | input: "-", 462 | expectedCursorPos: 1, 463 | expectedTS: nil, 464 | expectedErr: nil, 465 | }, 466 | } 467 | 468 | for _, tc := range testCases { 469 | p := NewParser([]byte(tc.input)) 470 | obtained, err := p.parseTimestamp() 471 | 472 | require.Equal( 473 | t, tc.expectedErr, err, tc.description, 474 | ) 475 | 476 | require.Equal( 477 | t, 478 | tc.expectedCursorPos, 479 | p.cursor, 480 | tc.description, 481 | ) 482 | 483 | if tc.expectedErr != nil { 484 | require.Nil( 485 | t, obtained, tc.description, 486 | ) 487 | 488 | continue 489 | } 490 | 491 | if tc.description == "nil timestamp" { 492 | continue 493 | } 494 | 495 | tfmt := time.RFC3339Nano 496 | require.Equal( 497 | t, 498 | tc.expectedTS.Format(tfmt), 499 | obtained.Format(tfmt), 500 | tc.description, 501 | ) 502 | } 503 | } 504 | 505 | func TestParseYear(t *testing.T) { 506 | testCases := []struct { 507 | description string 508 | input string 509 | expectedYear int 510 | expectedCursorPos int 511 | expectedErr error 512 | }{ 513 | { 514 | description: "invalid year", 515 | input: "1a2b", 516 | expectedYear: 0, 517 | expectedCursorPos: 4, 518 | expectedErr: ErrYearInvalid, 519 | }, 520 | { 521 | description: "year too short", 522 | input: "123", 523 | expectedYear: 0, 524 | expectedCursorPos: 0, 525 | expectedErr: parsercommon.ErrEOL, 526 | }, 527 | { 528 | description: "valid", 529 | input: "2013", 530 | expectedYear: 2013, 531 | expectedCursorPos: 4, 532 | expectedErr: nil, 533 | }, 534 | } 535 | 536 | for _, tc := range testCases { 537 | cursor := 0 538 | obtained, err := parseYear( 539 | []byte(tc.input), 540 | &cursor, 541 | len(tc.input), 542 | ) 543 | 544 | require.Equal( 545 | t, tc.expectedYear, obtained, tc.description, 546 | ) 547 | 548 | require.Equal( 549 | t, tc.expectedErr, err, tc.description, 550 | ) 551 | 552 | require.Equal( 553 | t, tc.expectedCursorPos, cursor, tc.description, 554 | ) 555 | } 556 | } 557 | 558 | func TestParseMonth(t *testing.T) { 559 | testCases := []struct { 560 | description string 561 | input string 562 | expectedMonth int 563 | expectedCursorPos int 564 | expectedErr error 565 | }{ 566 | { 567 | description: "invalid string", 568 | input: "ab", 569 | expectedMonth: 0, 570 | expectedCursorPos: 2, 571 | expectedErr: ErrMonthInvalid, 572 | }, 573 | { 574 | description: "invalid range 1/2", 575 | input: "00", 576 | expectedMonth: 0, 577 | expectedCursorPos: 2, 578 | expectedErr: ErrMonthInvalid, 579 | }, 580 | { 581 | description: "invalid range 2/2", 582 | input: "13", 583 | expectedMonth: 0, 584 | expectedCursorPos: 2, 585 | expectedErr: ErrMonthInvalid, 586 | }, 587 | { 588 | description: "too short", 589 | input: "1", 590 | expectedMonth: 0, 591 | expectedCursorPos: 0, 592 | expectedErr: parsercommon.ErrEOL, 593 | }, 594 | { 595 | description: "valid", 596 | input: "02", 597 | expectedMonth: 2, 598 | expectedCursorPos: 2, 599 | expectedErr: nil, 600 | }, 601 | } 602 | 603 | for _, tc := range testCases { 604 | cursor := 0 605 | obtained, err := parseMonth( 606 | []byte(tc.input), 607 | &cursor, 608 | len(tc.input), 609 | ) 610 | 611 | require.Equal( 612 | t, tc.expectedMonth, obtained, tc.description, 613 | ) 614 | 615 | require.Equal( 616 | t, tc.expectedErr, err, tc.description, 617 | ) 618 | 619 | require.Equal( 620 | t, tc.expectedCursorPos, cursor, tc.description, 621 | ) 622 | } 623 | } 624 | 625 | func TestParseDay(t *testing.T) { 626 | testCases := []struct { 627 | description string 628 | input string 629 | expectedDay int 630 | expectedCursorPos int 631 | expectedErr error 632 | }{ 633 | { 634 | description: "invalid string", 635 | input: "ab", 636 | expectedDay: 0, 637 | expectedCursorPos: 2, 638 | expectedErr: ErrDayInvalid, 639 | }, 640 | { 641 | description: "too short", 642 | input: "1", 643 | expectedDay: 0, 644 | expectedCursorPos: 0, 645 | expectedErr: parsercommon.ErrEOL, 646 | }, 647 | { 648 | description: "invalid range 1/2", 649 | input: "00", 650 | expectedDay: 0, 651 | expectedCursorPos: 2, 652 | expectedErr: ErrDayInvalid, 653 | }, 654 | { 655 | description: "invalid range 2/2", 656 | input: "32", 657 | expectedDay: 0, 658 | expectedCursorPos: 2, 659 | expectedErr: ErrDayInvalid, 660 | }, 661 | { 662 | description: "valid", 663 | input: "02", 664 | expectedDay: 2, 665 | expectedCursorPos: 2, 666 | expectedErr: nil, 667 | }, 668 | } 669 | 670 | for _, tc := range testCases { 671 | cursor := 0 672 | obtained, err := parseDay( 673 | []byte(tc.input), 674 | &cursor, 675 | len(tc.input), 676 | ) 677 | 678 | require.Equal( 679 | t, tc.expectedDay, obtained, tc.description, 680 | ) 681 | 682 | require.Equal( 683 | t, tc.expectedErr, err, tc.description, 684 | ) 685 | 686 | require.Equal( 687 | t, tc.expectedCursorPos, cursor, tc.description, 688 | ) 689 | } 690 | } 691 | 692 | func TestParseFullDate(t *testing.T) { 693 | testCases := []struct { 694 | description string 695 | input string 696 | expectedDate fullDate 697 | expectedCursorPos int 698 | expectedErr error 699 | }{ 700 | { 701 | description: "invalid separator 1/2", 702 | input: "2013+10-28", 703 | expectedDate: fullDate{}, 704 | expectedCursorPos: 4, 705 | expectedErr: parsercommon.ErrTimestampUnknownFormat, 706 | }, 707 | { 708 | description: "invalid separator 2/2", 709 | input: "2013-10+28", 710 | expectedDate: fullDate{}, 711 | expectedCursorPos: 7, 712 | expectedErr: parsercommon.ErrTimestampUnknownFormat, 713 | }, 714 | { 715 | description: "valid", 716 | input: "2013-10-28", 717 | expectedDate: fullDate{2013, 10, 28}, 718 | expectedCursorPos: 10, 719 | expectedErr: nil, 720 | }, 721 | } 722 | 723 | for _, tc := range testCases { 724 | cursor := 0 725 | obtained, err := parseFullDate( 726 | []byte(tc.input), 727 | &cursor, 728 | len(tc.input), 729 | ) 730 | 731 | require.Equal( 732 | t, tc.expectedErr, err, tc.description, 733 | ) 734 | 735 | require.Equal( 736 | t, tc.expectedDate, obtained, tc.description, 737 | ) 738 | 739 | require.Equal( 740 | t, tc.expectedCursorPos, cursor, tc.description, 741 | ) 742 | } 743 | } 744 | 745 | func TestParseHour(t *testing.T) { 746 | testCases := []struct { 747 | description string 748 | input string 749 | expectedHour int 750 | expectedCursorPos int 751 | expectedErr error 752 | }{ 753 | { 754 | description: "invalid", 755 | input: "azer", 756 | expectedHour: 0, 757 | expectedCursorPos: 2, 758 | expectedErr: ErrHourInvalid, 759 | }, 760 | { 761 | description: "too short", 762 | input: "1", 763 | expectedHour: 0, 764 | expectedCursorPos: 0, 765 | expectedErr: parsercommon.ErrEOL, 766 | }, 767 | { 768 | description: "invalid range 1/2", 769 | input: "-1", 770 | expectedHour: 0, 771 | expectedCursorPos: 2, 772 | expectedErr: ErrHourInvalid, 773 | }, 774 | { 775 | description: "invalid range 2/2", 776 | input: "24", 777 | expectedHour: 0, 778 | expectedCursorPos: 2, 779 | expectedErr: ErrHourInvalid, 780 | }, 781 | { 782 | description: "valid", 783 | input: "12", 784 | expectedHour: 12, 785 | expectedCursorPos: 2, 786 | expectedErr: nil, 787 | }, 788 | } 789 | 790 | for _, tc := range testCases { 791 | cursor := 0 792 | obtained, err := parseHour( 793 | []byte(tc.input), 794 | &cursor, 795 | len(tc.input), 796 | ) 797 | 798 | require.Equal( 799 | t, tc.expectedHour, obtained, tc.description, 800 | ) 801 | 802 | require.Equal( 803 | t, tc.expectedErr, err, tc.description, 804 | ) 805 | 806 | require.Equal( 807 | t, tc.expectedCursorPos, cursor, tc.description, 808 | ) 809 | } 810 | } 811 | 812 | func TestParseMinute(t *testing.T) { 813 | testCases := []struct { 814 | description string 815 | input string 816 | expectedMinute int 817 | expectedCursorPos int 818 | expectedErr error 819 | }{ 820 | { 821 | description: "invalid", 822 | input: "azer", 823 | expectedMinute: 0, 824 | expectedCursorPos: 2, 825 | expectedErr: ErrMinuteInvalid, 826 | }, 827 | { 828 | description: "too short", 829 | input: "1", 830 | expectedMinute: 0, 831 | expectedCursorPos: 0, 832 | expectedErr: parsercommon.ErrEOL, 833 | }, 834 | { 835 | description: "invalid range 1/2", 836 | input: "-1", 837 | expectedMinute: 0, 838 | expectedCursorPos: 2, 839 | expectedErr: ErrMinuteInvalid, 840 | }, 841 | { 842 | description: "invalid range 2/2", 843 | input: "60", 844 | expectedMinute: 0, 845 | expectedCursorPos: 2, 846 | expectedErr: ErrMinuteInvalid, 847 | }, 848 | { 849 | description: "valid", 850 | input: "12", 851 | expectedMinute: 12, 852 | expectedCursorPos: 2, 853 | expectedErr: nil, 854 | }, 855 | } 856 | 857 | for _, tc := range testCases { 858 | cursor := 0 859 | obtained, err := parseMinute( 860 | []byte(tc.input), 861 | &cursor, 862 | len(tc.input), 863 | ) 864 | 865 | require.Equal( 866 | t, tc.expectedMinute, obtained, tc.description, 867 | ) 868 | 869 | require.Equal( 870 | t, tc.expectedErr, err, tc.description, 871 | ) 872 | 873 | require.Equal( 874 | t, tc.expectedCursorPos, cursor, tc.description, 875 | ) 876 | } 877 | } 878 | 879 | func TestParseSecond(t *testing.T) { 880 | testCases := []struct { 881 | description string 882 | input string 883 | expectedSecond int 884 | expectedCursorPos int 885 | expectedErr error 886 | }{ 887 | { 888 | description: "invalid", 889 | input: "azer", 890 | expectedSecond: 0, 891 | expectedCursorPos: 2, 892 | expectedErr: ErrSecondInvalid, 893 | }, 894 | { 895 | description: "too short", 896 | input: "1", 897 | expectedSecond: 0, 898 | expectedCursorPos: 0, 899 | expectedErr: parsercommon.ErrEOL, 900 | }, 901 | { 902 | description: "invalid range 1/2", 903 | input: "-1", 904 | expectedSecond: 0, 905 | expectedCursorPos: 2, 906 | expectedErr: ErrSecondInvalid, 907 | }, 908 | { 909 | description: "invalid range 2/2", 910 | input: "60", 911 | expectedSecond: 0, 912 | expectedCursorPos: 2, 913 | expectedErr: ErrSecondInvalid, 914 | }, 915 | { 916 | description: "valid", 917 | input: "12", 918 | expectedSecond: 12, 919 | expectedCursorPos: 2, 920 | expectedErr: nil, 921 | }, 922 | } 923 | 924 | for _, tc := range testCases { 925 | cursor := 0 926 | obtained, err := parseSecond( 927 | []byte(tc.input), 928 | &cursor, 929 | len(tc.input), 930 | ) 931 | 932 | require.Equal( 933 | t, tc.expectedSecond, obtained, tc.description, 934 | ) 935 | 936 | require.Equal( 937 | t, tc.expectedErr, err, tc.description, 938 | ) 939 | 940 | require.Equal( 941 | t, tc.expectedCursorPos, cursor, tc.description, 942 | ) 943 | } 944 | } 945 | 946 | func TestParseSecFrac(t *testing.T) { 947 | testCases := []struct { 948 | description string 949 | input string 950 | expectedSecFrac float64 951 | expectedCursorPos int 952 | expectedErr error 953 | }{ 954 | { 955 | description: "invalid", 956 | input: "azerty", 957 | expectedSecFrac: 0, 958 | expectedCursorPos: 0, 959 | expectedErr: ErrSecFracInvalid, 960 | }, 961 | { 962 | description: "nanoseconds", 963 | input: "123456789", 964 | expectedSecFrac: 0.123456, 965 | expectedCursorPos: 6, 966 | expectedErr: nil, 967 | }, 968 | { 969 | description: "valid 1/4", 970 | input: "0", 971 | expectedSecFrac: 0, 972 | expectedCursorPos: 1, 973 | expectedErr: nil, 974 | }, 975 | { 976 | description: "valid 2/4", 977 | input: "52", 978 | expectedSecFrac: 0.52, 979 | expectedCursorPos: 2, 980 | expectedErr: nil, 981 | }, 982 | { 983 | description: "valid 3/4", 984 | input: "003", 985 | expectedSecFrac: 0.003, 986 | expectedCursorPos: 3, 987 | expectedErr: nil, 988 | }, 989 | { 990 | description: "valid 4/4", 991 | input: "000003", 992 | expectedSecFrac: 0.000003, 993 | expectedCursorPos: 6, 994 | expectedErr: nil, 995 | }, 996 | } 997 | 998 | for _, tc := range testCases { 999 | cursor := 0 1000 | obtained, err := parseSecFrac( 1001 | []byte(tc.input), 1002 | &cursor, 1003 | len(tc.input), 1004 | ) 1005 | 1006 | require.Equal( 1007 | t, tc.expectedSecFrac, obtained, tc.description, 1008 | ) 1009 | 1010 | require.Equal( 1011 | t, tc.expectedErr, err, tc.description, 1012 | ) 1013 | 1014 | require.Equal( 1015 | t, tc.expectedCursorPos, cursor, tc.description, 1016 | ) 1017 | } 1018 | } 1019 | 1020 | func TestParseNumericalTimeOffset(t *testing.T) { 1021 | buff := []byte("+02:00") 1022 | cursor := 0 1023 | l := len(buff) 1024 | 1025 | tmpTs, err := time.Parse("-07:00", string(buff)) 1026 | require.Nil(t, err) 1027 | 1028 | obtained, err := parseNumericalTimeOffset( 1029 | buff, &cursor, l, 1030 | ) 1031 | 1032 | require.Nil(t, err) 1033 | 1034 | expected := tmpTs.Location() 1035 | require.Equal(t, expected, obtained) 1036 | require.Equal(t, 6, cursor) 1037 | } 1038 | 1039 | func TestParseTimeOffset(t *testing.T) { 1040 | buff := []byte("Z") 1041 | cursor := 0 1042 | l := len(buff) 1043 | 1044 | obtained, err := parseTimeOffset( 1045 | buff, &cursor, l, 1046 | ) 1047 | 1048 | require.Nil(t, err) 1049 | require.Equal(t, time.UTC, obtained) 1050 | require.Equal(t, 1, cursor) 1051 | } 1052 | 1053 | func TestGetHourMin(t *testing.T) { 1054 | buff := []byte("12:34") 1055 | cursor := 0 1056 | l := len(buff) 1057 | 1058 | expectedH := 12 1059 | expectedM := 34 1060 | 1061 | obtainedH, obtainedM, err := getHourMinute( 1062 | buff, &cursor, l, 1063 | ) 1064 | 1065 | require.Nil(t, err) 1066 | require.Equal(t, expectedH, obtainedH) 1067 | require.Equal(t, expectedM, obtainedM) 1068 | require.Equal(t, l, cursor) 1069 | } 1070 | 1071 | func TestParsePartialTime(t *testing.T) { 1072 | buff := []byte("05:14:15.000003") 1073 | cursor := 0 1074 | l := len(buff) 1075 | 1076 | obtained, err := parsePartialTime( 1077 | buff, &cursor, l, 1078 | ) 1079 | 1080 | expected := &partialTime{ 1081 | hour: 5, 1082 | minute: 14, 1083 | seconds: 15, 1084 | secFrac: 0.000003, 1085 | } 1086 | 1087 | require.Nil(t, err) 1088 | require.Equal(t, expected, obtained) 1089 | require.Equal(t, l, cursor) 1090 | } 1091 | 1092 | func TestParseFullTime(t *testing.T) { 1093 | tz := "-02:00" 1094 | buff := []byte("05:14:15.000003" + tz) 1095 | cursor := 0 1096 | l := len(buff) 1097 | 1098 | tmpTs, err := time.Parse("-07:00", string(tz)) 1099 | require.Nil(t, err) 1100 | 1101 | obtained, err := parseFullTime( 1102 | buff, &cursor, l, 1103 | ) 1104 | 1105 | expected := &fullTime{ 1106 | pt: &partialTime{ 1107 | hour: 5, 1108 | minute: 14, 1109 | seconds: 15, 1110 | secFrac: 0.000003, 1111 | }, 1112 | loc: tmpTs.Location(), 1113 | } 1114 | 1115 | require.Nil(t, err) 1116 | require.Equal(t, expected, obtained) 1117 | require.Equal(t, 21, cursor) 1118 | } 1119 | 1120 | func TestToNSec(t *testing.T) { 1121 | testCases := map[float64]int{ 1122 | 0.52: 520000000, 1123 | 0.003: 3000000, 1124 | 0.000003: 3000, 1125 | } 1126 | 1127 | for src, expected := range testCases { 1128 | obtained, err := toNSec(src) 1129 | require.Nil(t, err) 1130 | require.Equal(t, expected, obtained) 1131 | } 1132 | } 1133 | 1134 | func TestParseAppName(t *testing.T) { 1135 | testCases := []struct { 1136 | description string 1137 | input string 1138 | expectedAppName string 1139 | expectedCursorPos int 1140 | expectedErr error 1141 | }{ 1142 | { 1143 | description: "valid", 1144 | input: "su ", 1145 | expectedAppName: "su", 1146 | expectedCursorPos: 2, 1147 | expectedErr: nil, 1148 | }, 1149 | { 1150 | description: "too long", 1151 | input: "suuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuu ", 1152 | expectedAppName: "", 1153 | expectedCursorPos: 48, 1154 | expectedErr: ErrInvalidAppName, 1155 | }, 1156 | } 1157 | 1158 | for _, tc := range testCases { 1159 | p := NewParser([]byte(tc.input)) 1160 | obtained, err := p.parseAppName() 1161 | 1162 | require.Equal( 1163 | t, tc.expectedErr, err, tc.description, 1164 | ) 1165 | 1166 | require.Equal( 1167 | t, tc.expectedAppName, obtained, tc.description, 1168 | ) 1169 | 1170 | require.Equal( 1171 | t, tc.expectedCursorPos, p.cursor, tc.description, 1172 | ) 1173 | } 1174 | } 1175 | 1176 | func TestParseProcID(t *testing.T) { 1177 | testCases := []struct { 1178 | description string 1179 | input string 1180 | expectedProcID string 1181 | expectedCursorPos int 1182 | expectedErr error 1183 | }{ 1184 | { 1185 | description: "valid", 1186 | input: "123foo ", 1187 | expectedProcID: "123foo", 1188 | expectedCursorPos: 6, 1189 | expectedErr: nil, 1190 | }, 1191 | { 1192 | description: "too long", 1193 | input: strings.Repeat("a", 129), 1194 | expectedProcID: "", 1195 | expectedCursorPos: 128, 1196 | expectedErr: ErrInvalidProcId, 1197 | }, 1198 | } 1199 | 1200 | for _, tc := range testCases { 1201 | p := NewParser([]byte(tc.input)) 1202 | obtained, err := p.parseProcId() 1203 | 1204 | require.Equal( 1205 | t, tc.expectedErr, err, tc.description, 1206 | ) 1207 | 1208 | require.Equal( 1209 | t, tc.expectedProcID, obtained, tc.description, 1210 | ) 1211 | 1212 | require.Equal( 1213 | t, tc.expectedCursorPos, p.cursor, tc.description, 1214 | ) 1215 | } 1216 | } 1217 | 1218 | func TestParseMsgID(t *testing.T) { 1219 | testCases := []struct { 1220 | description string 1221 | input string 1222 | expectedMsgID string 1223 | expectedCursorPos int 1224 | expectedErr error 1225 | }{ 1226 | { 1227 | description: "valid", 1228 | input: "123foo ", 1229 | expectedMsgID: "123foo", 1230 | expectedCursorPos: 6, 1231 | expectedErr: nil, 1232 | }, 1233 | { 1234 | description: "too long", 1235 | input: strings.Repeat("a", 33), 1236 | expectedMsgID: "", 1237 | expectedCursorPos: 32, 1238 | expectedErr: ErrInvalidMsgId, 1239 | }, 1240 | } 1241 | 1242 | for _, tc := range testCases { 1243 | p := NewParser([]byte(tc.input)) 1244 | obtained, err := p.parseMsgId() 1245 | 1246 | require.Equal( 1247 | t, tc.expectedErr, err, tc.description, 1248 | ) 1249 | 1250 | require.Equal( 1251 | t, tc.expectedMsgID, obtained, tc.description, 1252 | ) 1253 | 1254 | require.Equal( 1255 | t, tc.expectedCursorPos, p.cursor, tc.description, 1256 | ) 1257 | } 1258 | } 1259 | 1260 | func TestParseStructuredData(t *testing.T) { 1261 | testCases := []struct { 1262 | description string 1263 | input string 1264 | expectedData string 1265 | expectedCursorPos int 1266 | expectedErr error 1267 | }{ 1268 | { 1269 | description: "nil", 1270 | input: "-", 1271 | expectedData: "-", 1272 | expectedCursorPos: 1, 1273 | expectedErr: nil, 1274 | }, 1275 | { 1276 | description: "single", 1277 | input: `[exampleSDID@32473 iut="3" eventSource="Application"eventID="1011"]`, 1278 | expectedData: `[exampleSDID@32473 iut="3" eventSource="Application"eventID="1011"]`, 1279 | expectedCursorPos: 67, 1280 | expectedErr: nil, 1281 | }, 1282 | { 1283 | description: "multiple", 1284 | input: `[exampleSDID@32473 iut="3" eventSource="Application"eventID="1011"][examplePriority@32473 class="high"]`, 1285 | expectedData: `[exampleSDID@32473 iut="3" eventSource="Application"eventID="1011"][examplePriority@32473 class="high"]`, 1286 | expectedCursorPos: 103, 1287 | expectedErr: nil, 1288 | }, 1289 | { 1290 | description: "multiple invalid", 1291 | input: `[exampleSDID@32473 iut="3" eventSource="Application"eventID="1011"] [examplePriority@32473 class="high"]`, 1292 | expectedData: `[exampleSDID@32473 iut="3" eventSource="Application"eventID="1011"]`, 1293 | expectedCursorPos: 67, 1294 | expectedErr: nil, 1295 | }, 1296 | } 1297 | 1298 | for _, tc := range testCases { 1299 | cursor := 0 1300 | obtained, err := parseStructuredData( 1301 | []byte(tc.input), 1302 | &cursor, 1303 | len(tc.input), 1304 | ) 1305 | 1306 | require.Equal( 1307 | t, tc.expectedErr, err, tc.description, 1308 | ) 1309 | 1310 | require.Equal( 1311 | t, tc.expectedData, obtained, tc.description, 1312 | ) 1313 | 1314 | require.Equal( 1315 | t, tc.expectedCursorPos, cursor, tc.description, 1316 | ) 1317 | } 1318 | } 1319 | 1320 | func TestParseMessageSizeChecks(t *testing.T) { 1321 | start := `<165>1 2003-10-11T22:14:15.003Z mymachine.example.com evntslog - ID47 [exampleSDID@32473 iut="3" eventSource="Application" eventID="1011"] ` 1322 | msg := start + strings.Repeat("a", MAX_PACKET_LEN) 1323 | 1324 | p := NewParser([]byte(msg)) 1325 | err := p.Parse() 1326 | fields := p.Dump() 1327 | 1328 | require.Nil( 1329 | t, err, 1330 | ) 1331 | 1332 | require.Len( 1333 | t, 1334 | fields["message"], 1335 | MAX_PACKET_LEN-len(start), 1336 | ) 1337 | 1338 | // --- 1339 | 1340 | msg = start + " hello " 1341 | p = NewParser([]byte(msg)) 1342 | err = p.Parse() 1343 | fields = p.Dump() 1344 | 1345 | require.Nil(t, err) 1346 | require.Equal(t, "hello", fields["message"]) 1347 | } 1348 | 1349 | func BenchmarkParseTimestamp(b *testing.B) { 1350 | buff := []byte("2003-08-24T05:14:15.000003-07:00") 1351 | 1352 | p := NewParser(buff) 1353 | 1354 | for i := 0; i < b.N; i++ { 1355 | _, err := p.parseTimestamp() 1356 | if err != nil { 1357 | panic(err) 1358 | } 1359 | 1360 | p.cursor = 0 1361 | } 1362 | } 1363 | 1364 | func BenchmarkParseHeader(b *testing.B) { 1365 | buff := []byte( 1366 | "<165>1 2003-10-11T22:14:15.003Z mymachine.example.com su 123 ID47 ", 1367 | ) 1368 | 1369 | p := NewParser(buff) 1370 | 1371 | for i := 0; i < b.N; i++ { 1372 | _, err := p.parseHeader() 1373 | if err != nil { 1374 | panic(err) 1375 | } 1376 | 1377 | p.cursor = 0 1378 | } 1379 | } 1380 | 1381 | func BenchmarkParseFull(b *testing.B) { 1382 | 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...` 1383 | 1384 | for i := 0; i < b.N; i++ { 1385 | p := NewParser( 1386 | []byte(msg), 1387 | ) 1388 | 1389 | err := p.Parse() 1390 | if err != nil { 1391 | panic(err) 1392 | } 1393 | } 1394 | } 1395 | -------------------------------------------------------------------------------- /syslogparser.go: -------------------------------------------------------------------------------- 1 | // Package syslogparser implements functions to parsing RFC3164 or RFC5424 syslog messages. 2 | // syslogparser provides one subpackage per RFC with an example usage for which RFC. 3 | package syslogparser 4 | 5 | import ( 6 | "time" 7 | 8 | "github.com/jeromer/syslogparser/parsercommon" 9 | ) 10 | 11 | type RFC uint8 12 | 13 | const ( 14 | RFC_UNKNOWN = iota 15 | RFC_3164 16 | RFC_5424 17 | ) 18 | 19 | type LogParts map[string]interface{} 20 | 21 | type LogParser interface { 22 | Parse() error 23 | Dump() LogParts 24 | WithTimestampFormat(string) 25 | WithLocation(*time.Location) 26 | WithHostname(string) 27 | WithTag(string) 28 | } 29 | 30 | func DetectRFC(buff []byte) (RFC, error) { 31 | max := 10 32 | var v int 33 | var err error 34 | 35 | for i := 0; i < max; i++ { 36 | if buff[i] == '>' && i < max { 37 | x := i + 1 38 | 39 | v, err = parsercommon.ParseVersion( 40 | buff, &x, max, 41 | ) 42 | 43 | break 44 | } 45 | } 46 | 47 | if err != nil { 48 | return RFC_UNKNOWN, err 49 | } 50 | 51 | if v == parsercommon.NO_VERSION { 52 | return RFC_3164, nil 53 | } 54 | 55 | return RFC_5424, nil 56 | } 57 | -------------------------------------------------------------------------------- /syslogparser_test.go: -------------------------------------------------------------------------------- 1 | package syslogparser 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestDetectRFC_3164(t *testing.T) { 10 | p, err := DetectRFC( 11 | []byte( 12 | "<34>Oct 11 22:14:15 ...", 13 | ), 14 | ) 15 | 16 | require.Nil(t, err) 17 | require.Equal(t, p, RFC(RFC_3164)) 18 | } 19 | 20 | func TestDetectRFC_5424(t *testing.T) { 21 | p, err := DetectRFC( 22 | []byte( 23 | "<165>1 2003-10-11T22:14:15.003Z ...", 24 | ), 25 | ) 26 | 27 | require.Nil(t, err) 28 | require.Equal(t, p, RFC(RFC_5424)) 29 | } 30 | 31 | func BenchmarkDetectRFC(b *testing.B) { 32 | buff := []byte( 33 | "<165>1 2003-10-11T22:14:15.003Z ...", 34 | ) 35 | 36 | for i := 0; i < b.N; i++ { 37 | _, err := DetectRFC(buff) 38 | if err != nil { 39 | panic(err) 40 | } 41 | } 42 | } 43 | --------------------------------------------------------------------------------