├── .github
├── FUNDING.yml
├── ISSUE_TEMPLATE.md
├── PULL_REQUEST_TEMPLATE.md
└── workflows
│ └── test.yml
├── .gitignore
├── History.md
├── LICENSE
├── Readme.md
├── go.mod
├── go.sum
├── parse.go
├── parse_test.go
├── parsers.lde
└── parsers_lde.go
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: tj
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ## Prerequisites
2 |
3 | * [ ] I searched to see if the issue already exists.
4 |
5 | ## Description
6 |
7 | Describe the bug or feature.
8 |
9 | ## Steps to Reproduce
10 |
11 | Describe the steps required to reproduce the issue if applicable.
12 |
13 | ## Slack
14 |
15 | Join us on Slack https://chat.apex.sh/
16 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | Please open an issue and discuss changes before spending time on them, unless the change is trivial or an issue already exists.
2 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | on: [push, pull_request]
2 | name: Tests
3 | jobs:
4 | test:
5 | strategy:
6 | matrix:
7 | go-version: [1.13.x]
8 | platform: [ubuntu-latest, macos-latest, windows-latest]
9 | runs-on: ${{ matrix.platform }}
10 | steps:
11 | - name: Install Go
12 | uses: actions/setup-go@v1
13 | with:
14 | go-version: ${{ matrix.go-version }}
15 | - name: Checkout code
16 | uses: actions/checkout@v1
17 | - name: Test
18 | run: go test ./...
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .envrc
2 |
--------------------------------------------------------------------------------
/History.md:
--------------------------------------------------------------------------------
1 |
2 | v1.2.1 / 2020-09-15
3 | ===================
4 |
5 | * change HerokuScale event to accommodate multiple dynos
6 |
7 | v1.2.0 / 2020-08-28
8 | ===================
9 |
10 | * add HerokuScale event
11 |
12 | v1.1.0 / 2020-08-28
13 | ===================
14 |
15 | * add parsing of Heroku syslog and platform messages. Closes #2
16 |
17 | v1.0.0 / 2020-08-19
18 | ===================
19 |
20 | * add AWSLambdaTimeout event support
21 |
22 | v0.0.2 / 2020-08-17
23 | ===================
24 |
25 | * add test for empty line
26 | * add support for Lambda init duration REPORT logs
27 | * fix race condition
28 |
29 | v0.0.1 / 2020-08-12
30 | ===================
31 |
32 | * change to assume newline is stripped
33 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License
2 |
3 | Copyright (c) 2020 TJ Holowaychuk tj@tjholowaychuk.com
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining
6 | a copy of this software and associated documentation files (the
7 | 'Software'), to deal in the Software without restriction, including
8 | without limitation the rights to use, copy, modify, merge, publish,
9 | distribute, sublicense, and/or sell copies of the Software, and to
10 | permit persons to whom the Software is furnished to do so, subject to
11 | the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be
14 | included in all copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23 |
24 |
--------------------------------------------------------------------------------
/Readme.md:
--------------------------------------------------------------------------------
1 | # Parsers
2 |
3 | Go log format parsers, generated by [Ldetool](https://github.com/sirkon/ldetool) producing performant & memory efficient parsers.
4 |
5 | ## Formats
6 |
7 | Currently it supports the following formats:
8 |
9 | - AWS Lambda
10 | - Syslog (rfc5424)
11 | - Heroku platform messages
12 |
13 | ---
14 |
15 | [](https://godoc.org/github.com/apex/parsers)
16 | 
17 | 
18 | 
19 |
20 | ## Sponsors
21 |
22 | Sponsored by my [GitHub sponsors](https://github.com/sponsors/tj):
23 |
24 | [
](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/0)
25 | [
](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/1)
26 | [
](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/2)
27 | [
](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/3)
28 | [
](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/4)
29 | [
](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/5)
30 | [
](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/6)
31 | [
](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/7)
32 | [
](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/8)
33 | [
](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/9)
34 | [
](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/10)
35 | [
](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/11)
36 | [
](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/12)
37 | [
](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/13)
38 | [
](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/14)
39 | [
](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/15)
40 | [
](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/16)
41 | [
](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/17)
42 | [
](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/18)
43 | [
](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/19)
44 | [
](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/20)
45 | [
](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/21)
46 | [
](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/22)
47 | [
](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/23)
48 | [
](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/24)
49 | [
](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/25)
50 | [
](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/26)
51 | [
](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/27)
52 | [
](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/28)
53 | [
](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/29)
54 | [
](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/30)
55 | [
](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/31)
56 | [
](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/32)
57 | [
](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/33)
58 | [
](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/34)
59 | [
](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/35)
60 | [
](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/36)
61 | [
](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/37)
62 | [
](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/38)
63 | [
](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/39)
64 | [
](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/40)
65 | [
](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/41)
66 | [
](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/42)
67 | [
](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/43)
68 | [
](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/44)
69 | [
](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/45)
70 | [
](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/46)
71 | [
](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/47)
72 | [
](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/48)
73 | [
](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/49)
74 | [
](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/50)
75 | [
](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/51)
76 | [
](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/52)
77 | [
](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/53)
78 | [
](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/54)
79 | [
](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/55)
80 | [
](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/56)
81 | [
](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/57)
82 | [
](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/58)
83 | [
](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/59)
84 | [
](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/60)
85 | [
](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/61)
86 | [
](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/62)
87 | [
](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/63)
88 | [
](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/64)
89 | [
](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/65)
90 | [
](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/66)
91 | [
](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/67)
92 | [
](https://sponsors-api-u2fftug6kq-uc.a.run.app/sponsor/profile/68)
93 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/apex/parsers
2 |
3 | go 1.14
4 |
5 | require github.com/tj/assert v0.0.3
6 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
4 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
5 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
6 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
7 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
8 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
9 | github.com/tj/assert v0.0.3 h1:Df/BlaZ20mq6kuai7f5z2TvPFiwC3xaWJSDQNiIS3Rk=
10 | github.com/tj/assert v0.0.3/go.mod h1:Ne6X72Q+TB1AteidzQncjw9PabbMp4PBMZ1k+vd1Pvk=
11 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
12 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
13 | gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c h1:grhR+C34yXImVGp7EzNk+DTIk+323eIUWOmEevy6bDo=
14 | gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
15 |
--------------------------------------------------------------------------------
/parse.go:
--------------------------------------------------------------------------------
1 | //go:generate ldetool --package parsers --go-string parsers.lde
2 |
3 | package parsers
4 |
5 | // Event is the interface used to extract an event from a log line.
6 | type Event interface {
7 | Extract(line string) (bool, error)
8 | }
9 |
10 | // Parse a log line from any source. Typically it's best to use a targeted parser
11 | // such as ParseLambda() or ParseHeroku(). Returns true if an event was successfully parsed.
12 | func Parse(line string) (Event, bool) {
13 | events := []Event{
14 | &AWSLambdaStart{},
15 | &AWSLambdaReportInit{},
16 | &AWSLambdaReport{},
17 | &AWSLambdaEnd{},
18 | &AWSLambdaTimeout{},
19 | &Syslog{},
20 | }
21 |
22 | for _, e := range events {
23 | if ok, _ := e.Extract(line); ok {
24 | return e, true
25 | }
26 | }
27 |
28 | return nil, false
29 | }
30 |
31 | // ParseLambda parses a log line from AWS Lambda. Returns true if an event was successfully parsed.
32 | func ParseLambda(line string) (Event, bool) {
33 | events := []Event{
34 | &AWSLambdaStart{},
35 | &AWSLambdaReportInit{},
36 | &AWSLambdaReport{},
37 | &AWSLambdaEnd{},
38 | &AWSLambdaTimeout{},
39 | }
40 |
41 | for _, e := range events {
42 | if ok, _ := e.Extract(line); ok {
43 | return e, true
44 | }
45 | }
46 |
47 | return nil, false
48 | }
49 |
50 | // ParseHeroku parses a log line from Heroku. You should first parse the syslog line from Heroku
51 | // using Syslog, and then ParseHeroku() for the platform specific message.
52 | // Returns true if an event was successfully parsed.
53 | func ParseHeroku(line string) (Event, bool) {
54 | events := []Event{
55 | &HerokuDeploy{},
56 | &HerokuRollback{},
57 | &HerokuBuild{},
58 | &HerokuRelease{},
59 | &HerokuProcessExit{},
60 | &HerokuProcessStart{},
61 | &HerokuStateChange{},
62 | &HerokuProcessListening{},
63 | &HerokuConfigSet{},
64 | &HerokuConfigRemove{},
65 | &HerokuScale{},
66 | }
67 |
68 | for _, e := range events {
69 | if ok, _ := e.Extract(line); ok {
70 | return e, true
71 | }
72 | }
73 |
74 | return nil, false
75 | }
76 |
--------------------------------------------------------------------------------
/parse_test.go:
--------------------------------------------------------------------------------
1 | package parsers_test
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/tj/assert"
7 |
8 | "github.com/apex/parsers"
9 | )
10 |
11 | var cases = []struct {
12 | Label string
13 | Input string
14 | Output parsers.Event
15 | }{
16 | {
17 | Label: "Lambda start",
18 | Input: "START RequestId: f7172574-5884-44d9-95f4-7438fb83e9b0 Version: 26",
19 | Output: &parsers.AWSLambdaStart{
20 | RequestID: "f7172574-5884-44d9-95f4-7438fb83e9b0",
21 | Version: "26",
22 | },
23 | },
24 | {
25 | Label: "Lambda start with $LATEST",
26 | Input: "START RequestId: f7172574-5884-44d9-95f4-7438fb83e9b0 Version: $LATEST",
27 | Output: &parsers.AWSLambdaStart{
28 | RequestID: "f7172574-5884-44d9-95f4-7438fb83e9b0",
29 | Version: "$LATEST",
30 | },
31 | },
32 | {
33 | Label: "Lambda end",
34 | Input: "END RequestId: f7172574-5884-44d9-95f4-7438fb83e9b0",
35 | Output: &parsers.AWSLambdaEnd{
36 | RequestID: "f7172574-5884-44d9-95f4-7438fb83e9b0",
37 | },
38 | },
39 | {
40 | Label: "Lambda report",
41 | Input: "REPORT RequestId: 136f2f48-069e-4808-8d73-b31c4d97e146\tDuration: 7.80 ms\tBilled Duration: 100 ms\tMemory Size: 512 MB\tMax Memory Used: 115 MB\t",
42 | Output: &parsers.AWSLambdaReport{
43 | RequestID: "136f2f48-069e-4808-8d73-b31c4d97e146",
44 | Duration: 7.8,
45 | BilledDuration: 100,
46 | MemorySize: 512,
47 | MaxMemoryUsed: 115,
48 | },
49 | },
50 | {
51 | Label: "Lambda report with init duration",
52 | Input: "REPORT RequestId: 136f2f48-069e-4808-8d73-b31c4d97e146\tDuration: 7.80 ms\tBilled Duration: 100 ms\tMemory Size: 512 MB\tMax Memory Used: 115 MB\tInit Duration: 185.62 ms\t",
53 | Output: &parsers.AWSLambdaReportInit{
54 | RequestID: "136f2f48-069e-4808-8d73-b31c4d97e146",
55 | Duration: 7.8,
56 | BilledDuration: 100,
57 | InitDuration: 185.62,
58 | MemorySize: 512,
59 | MaxMemoryUsed: 115,
60 | },
61 | },
62 | {
63 | Label: "Lambda timeout",
64 | Input: "2020-08-19T09:20:47.075Z 8173dbda-4443-4bcd-8d4c-33704efa0f05 Task timed out after 30.03 seconds",
65 | Output: &parsers.AWSLambdaTimeout{
66 | Timestamp: "2020-08-19T09:20:47.075Z",
67 | RequestID: "8173dbda-4443-4bcd-8d4c-33704efa0f05",
68 | Duration: 30.03,
69 | },
70 | },
71 | {
72 | Label: "Heroku syslog",
73 | Input: "<45>1 2020-08-28T10:38:06.285004+00:00 host app api - Some random message here",
74 | Output: &parsers.Syslog{
75 | Priority: 45,
76 | SyslogVersion: 1,
77 | Timestamp: "2020-08-28T10:38:06.285004+00:00",
78 | Hostname: "host",
79 | Appname: "app",
80 | ProcID: "api",
81 | MsgID: "-",
82 | Message: "Some random message here",
83 | },
84 | },
85 | {
86 | Label: "Unmatched",
87 | Input: `{ "some": "json" }`,
88 | Output: nil,
89 | },
90 | {
91 | Label: "Empty",
92 | Input: ``,
93 | Output: nil,
94 | },
95 | }
96 |
97 | // Test parsing.
98 | func TestParse(t *testing.T) {
99 | for _, c := range cases {
100 | t.Run(c.Label, func(t *testing.T) {
101 | v, _ := parsers.Parse(c.Input)
102 | assert.Equal(t, c.Output, v)
103 | })
104 | }
105 | }
106 |
107 | var herokuCases = []struct {
108 | Label string
109 | Input string
110 | Output parsers.Event
111 | }{
112 | {
113 | Label: "Heroku deployment",
114 | Input: "Deploy 059375fe by user tj@apex.sh",
115 | Output: &parsers.HerokuDeploy{
116 | Commit: "059375fe",
117 | User: "tj@apex.sh",
118 | },
119 | },
120 | {
121 | Label: "Heroku release",
122 | Input: "Release v16 created by user tj@apex.sh",
123 | Output: &parsers.HerokuRelease{
124 | Version: "v16",
125 | User: "tj@apex.sh",
126 | },
127 | },
128 | {
129 | Label: "Heroku rollback",
130 | Input: "Rollback to v11 by user tj@apex.sh",
131 | Output: &parsers.HerokuRollback{
132 | Version: "v11",
133 | User: "tj@apex.sh",
134 | },
135 | },
136 | {
137 | Label: "Heroku build start",
138 | Input: "Build started by user tj@apex.sh",
139 | Output: &parsers.HerokuBuild{
140 | User: "tj@apex.sh",
141 | },
142 | },
143 | {
144 | Label: "Heroku state change",
145 | Input: "State changed from starting to crashed",
146 | Output: &parsers.HerokuStateChange{
147 | From: "starting",
148 | To: "crashed",
149 | },
150 | },
151 | {
152 | Label: "Heroku process exit",
153 | Input: "Process exited with status 143",
154 | Output: &parsers.HerokuProcessExit{
155 | Status: 143,
156 | },
157 | },
158 | {
159 | Label: "Heroku starting process",
160 | Input: "Starting process with command `node index.js`",
161 | Output: &parsers.HerokuProcessStart{
162 | Command: "node index.js",
163 | },
164 | },
165 | {
166 | Label: "Heroku listening",
167 | Input: "Listening on 55766",
168 | Output: &parsers.HerokuProcessListening{
169 | Port: 55766,
170 | },
171 | },
172 | {
173 | Label: "Heroku set env var",
174 | Input: "Set FOO config vars by user tj@apex.sh",
175 | Output: &parsers.HerokuConfigSet{
176 | Variables: "FOO",
177 | User: "tj@apex.sh",
178 | },
179 | },
180 | {
181 | Label: "Heroku set env vars",
182 | Input: "Set FOO, BAR config vars by user tj@apex.sh",
183 | Output: &parsers.HerokuConfigSet{
184 | Variables: "FOO, BAR",
185 | User: "tj@apex.sh",
186 | },
187 | },
188 | {
189 | Label: "Heroku remove env vars",
190 | Input: "Remove FOO config vars by user tj@apex.sh",
191 | Output: &parsers.HerokuConfigRemove{
192 | Variables: "FOO",
193 | User: "tj@apex.sh",
194 | },
195 | },
196 | {
197 | Label: "Heroku scale 0 free",
198 | Input: "Scaled to web@0:Free by user tj@apex.sh",
199 | Output: &parsers.HerokuScale{
200 | Dynos: "web@0:Free",
201 | User: "tj@apex.sh",
202 | },
203 | },
204 | {
205 | Label: "Heroku scale 1 free",
206 | Input: "Scaled to web@1:Free by user tj@apex.sh",
207 | Output: &parsers.HerokuScale{
208 | Dynos: "web@1:Free",
209 | User: "tj@apex.sh",
210 | },
211 | },
212 | {
213 | Label: "Heroku scale multiple",
214 | Input: "Scaled to web@1:Free worker@0:Free by user tj@apex.sh",
215 | Output: &parsers.HerokuScale{
216 | Dynos: "web@1:Free worker@0:Free",
217 | User: "tj@apex.sh",
218 | },
219 | },
220 | }
221 |
222 | // Test parsing Heroku messages.
223 | func TestParseHeroku(t *testing.T) {
224 | for _, c := range herokuCases {
225 | t.Run(c.Label, func(t *testing.T) {
226 | v, _ := parsers.ParseHeroku(c.Input)
227 | assert.Equal(t, c.Output, v)
228 | })
229 | }
230 | }
231 |
232 | // Benchmark parsing.
233 | func BenchmarkParse(b *testing.B) {
234 | for i := 0; i < b.N; i++ {
235 | s := "REPORT RequestId: 136f2f48-069e-4808-8d73-b31c4d97e146\tDuration: 7.80 ms\tBilled Duration: 100 ms\tMemory Size: 512 MB\tMax Memory Used: 115 MB\t\n"
236 | _, ok := parsers.Parse(s)
237 | if !ok {
238 | b.Fatal("failed parsing")
239 | }
240 | }
241 | }
242 |
243 | // Benchmark parsing.
244 | func BenchmarkParseHeroku(b *testing.B) {
245 | for i := 0; i < b.N; i++ {
246 | s := "Set FOO, BAR config vars by user tj@apex.sh"
247 | _, ok := parsers.ParseHeroku(s)
248 | if !ok {
249 | b.Fatal("failed parsing")
250 | }
251 | }
252 | }
253 |
--------------------------------------------------------------------------------
/parsers.lde:
--------------------------------------------------------------------------------
1 |
2 | # AWSLambdaStart event.
3 | AWSLambdaStart =
4 | ^"START RequestId: " RequestID(string) " Version: " Version(string);
5 |
6 | # AWSLambdaEnd event.
7 | AWSLambdaEnd =
8 | ^"END RequestId: " RequestID(string);
9 |
10 | # AWSLambdaReport event.
11 | AWSLambdaReport =
12 | ^"REPORT RequestId: " RequestID(string) "\tDuration: " Duration(float64) " ms\tBilled Duration: " BilledDuration(float64) " ms\tMemory Size: " MemorySize(int) " MB\tMax Memory Used: " MaxMemoryUsed(int) " MB\t";
13 |
14 | # AWSLambdaReportInit event.
15 | AWSLambdaReportInit =
16 | ^"REPORT RequestId: " RequestID(string) "\tDuration: " Duration(float64) " ms\tBilled Duration: " BilledDuration(float64) " ms\tMemory Size: " MemorySize(int) " MB\tMax Memory Used: " MaxMemoryUsed(int) " MB\tInit Duration: " InitDuration(float64) " ms\t";
17 |
18 | # AWSLambdaTimeout event.
19 | AWSLambdaTimeout =
20 | Timestamp(string) " " RequestID(string) " Task timed out after " Duration(float64) " seconds";
21 |
22 | # Syslog event.
23 | Syslog =
24 | ^"<" Priority(int) ">" SyslogVersion(int) " " Timestamp(string) " " Hostname(string) " " Appname(string) " " ProcID(string) " " MsgID(string) " " Message(string);
25 |
26 | # HerokuDeploy event.
27 | HerokuDeploy =
28 | ^"Deploy " Commit(string) " by user " User(string);
29 |
30 | # HerokuRelease event.
31 | HerokuRelease =
32 | ^"Release " Version(string) " created by user " User(string);
33 |
34 | # HerokuRollback event.
35 | HerokuRollback =
36 | ^"Rollback to " Version(string) " by user " User(string);
37 |
38 | # HerokuBuild event.
39 | HerokuBuild =
40 | ^"Build started by user " User(string);
41 |
42 | # HerokuStateChange event.
43 | HerokuStateChange =
44 | ^"State changed from " From(string) " to " To(string);
45 |
46 | # HerokuProcessExit event.
47 | HerokuProcessExit =
48 | ^"Process exited with status " Status(int);
49 |
50 | # HerokuProcessStart event.
51 | HerokuProcessStart =
52 | ^"Starting process with command `" Command(string) "`";
53 |
54 | # HerokuProcessListening event.
55 | HerokuProcessListening =
56 | ^"Listening on " Port(int);
57 |
58 | # HerokuConfigSet event.
59 | HerokuConfigSet =
60 | ^"Set " Variables(string) " config vars by user " User(string);
61 |
62 | # HerokuConfigRemove event.
63 | HerokuConfigRemove =
64 | ^"Remove " Variables(string) " config vars by user " User(string);
65 |
66 | # HerokuScale event.
67 | HerokuScale =
68 | ^"Scaled to " Dynos(string) " by user " User(string);
--------------------------------------------------------------------------------
/parsers_lde.go:
--------------------------------------------------------------------------------
1 | // Code generated by ldetool --package parsers --go-string parsers.lde. DO NOT EDIT.
2 |
3 | package parsers
4 |
5 | import (
6 | "fmt"
7 | "strconv"
8 | "strings"
9 | )
10 |
11 | var constBslashTDurationColonSpace = "\tDuration: "
12 | var constBuildSpaceStartedSpaceBySpaceUserSpace = "Build started by user "
13 | var constDeploySpace = "Deploy "
14 | var constENDSpaceRequestIDColonSpace = "END RequestId: "
15 | var constLess = "<"
16 | var constListeningSpaceOnSpace = "Listening on "
17 | var constMore = ">"
18 | var constProcessSpaceExitedSpaceWithSpaceStatusSpace = "Process exited with status "
19 | var constREPORTSpaceRequestIDColonSpace = "REPORT RequestId: "
20 | var constReleaseSpace = "Release "
21 | var constRemoveSpace = "Remove "
22 | var constRollbackSpaceToSpace = "Rollback to "
23 | var constSTARTSpaceRequestIDColonSpace = "START RequestId: "
24 | var constScaledSpaceToSpace = "Scaled to "
25 | var constSetSpace = "Set "
26 | var constSpace = " "
27 | var constSpaceBySpaceUserSpace = " by user "
28 | var constSpaceConfigSpaceVarsSpaceBySpaceUserSpace = " config vars by user "
29 | var constSpaceCreatedSpaceBySpaceUserSpace = " created by user "
30 | var constSpaceMBBslashT = " MB\t"
31 | var constSpaceMBBslashTInitSpaceDurationColonSpace = " MB\tInit Duration: "
32 | var constSpaceMBBslashTMaxSpaceMemorySpaceUsedColonSpace = " MB\tMax Memory Used: "
33 | var constSpaceMsBslashT = " ms\t"
34 | var constSpaceMsBslashTBilledSpaceDurationColonSpace = " ms\tBilled Duration: "
35 | var constSpaceMsBslashTMemorySpaceSizeColonSpace = " ms\tMemory Size: "
36 | var constSpaceSeconds = " seconds"
37 | var constSpaceTaskSpaceTimedSpaceOutSpaceAfterSpace = " Task timed out after "
38 | var constSpaceToSpace = " to "
39 | var constSpaceVersionColonSpace = " Version: "
40 | var constStartingSpaceProcessSpaceWithSpaceCommandSpace = "Starting process with command `"
41 | var constStateSpaceChangedSpaceFromSpace = "State changed from "
42 | var constUnrecognizedSequence = "`"
43 |
44 | // AWSLambdaStart event.
45 | type AWSLambdaStart struct {
46 | Rest string
47 | RequestID string
48 | Version string
49 | }
50 |
51 | // Extract ...
52 | func (p *AWSLambdaStart) Extract(line string) (bool, error) {
53 | p.Rest = line
54 | var pos int
55 |
56 | // Checks if the rest starts with `"START RequestId: "` and pass it
57 | if strings.HasPrefix(p.Rest, constSTARTSpaceRequestIDColonSpace) {
58 | p.Rest = p.Rest[len(constSTARTSpaceRequestIDColonSpace):]
59 | } else {
60 | return false, nil
61 | }
62 |
63 | // Take until " Version: " as RequestID(string)
64 | pos = strings.Index(p.Rest, constSpaceVersionColonSpace)
65 | if pos >= 0 {
66 | p.RequestID = p.Rest[:pos]
67 | p.Rest = p.Rest[pos+len(constSpaceVersionColonSpace):]
68 | } else {
69 | return false, nil
70 | }
71 |
72 | // Take the rest as Version(string)
73 | p.Version = p.Rest
74 | p.Rest = p.Rest[len(p.Rest):]
75 | return true, nil
76 | }
77 |
78 | // AWSLambdaEnd event.
79 | type AWSLambdaEnd struct {
80 | Rest string
81 | RequestID string
82 | }
83 |
84 | // Extract ...
85 | func (p *AWSLambdaEnd) Extract(line string) (bool, error) {
86 | p.Rest = line
87 |
88 | // Checks if the rest starts with `"END RequestId: "` and pass it
89 | if strings.HasPrefix(p.Rest, constENDSpaceRequestIDColonSpace) {
90 | p.Rest = p.Rest[len(constENDSpaceRequestIDColonSpace):]
91 | } else {
92 | return false, nil
93 | }
94 |
95 | // Take the rest as RequestID(string)
96 | p.RequestID = p.Rest
97 | p.Rest = p.Rest[len(p.Rest):]
98 | return true, nil
99 | }
100 |
101 | // AWSLambdaReport event.
102 | type AWSLambdaReport struct {
103 | Rest string
104 | RequestID string
105 | Duration float64
106 | BilledDuration float64
107 | MemorySize int
108 | MaxMemoryUsed int
109 | }
110 |
111 | // Extract ...
112 | func (p *AWSLambdaReport) Extract(line string) (bool, error) {
113 | p.Rest = line
114 | var err error
115 | var pos int
116 | var tmp string
117 | var tmpFloat float64
118 | var tmpInt int64
119 |
120 | // Checks if the rest starts with `"REPORT RequestId: "` and pass it
121 | if strings.HasPrefix(p.Rest, constREPORTSpaceRequestIDColonSpace) {
122 | p.Rest = p.Rest[len(constREPORTSpaceRequestIDColonSpace):]
123 | } else {
124 | return false, nil
125 | }
126 |
127 | // Take until "\tDuration: " as RequestID(string)
128 | pos = strings.Index(p.Rest, constBslashTDurationColonSpace)
129 | if pos >= 0 {
130 | p.RequestID = p.Rest[:pos]
131 | p.Rest = p.Rest[pos+len(constBslashTDurationColonSpace):]
132 | } else {
133 | return false, nil
134 | }
135 |
136 | // Take until " ms\tBilled Duration: " as Duration(float64)
137 | pos = strings.Index(p.Rest, constSpaceMsBslashTBilledSpaceDurationColonSpace)
138 | if pos >= 0 {
139 | tmp = p.Rest[:pos]
140 | p.Rest = p.Rest[pos+len(constSpaceMsBslashTBilledSpaceDurationColonSpace):]
141 | } else {
142 | return false, nil
143 | }
144 | if tmpFloat, err = strconv.ParseFloat(tmp, 64); err != nil {
145 | return false, fmt.Errorf("parsing `%s` into field Duration(float64): %s", tmp, err)
146 | }
147 | p.Duration = float64(tmpFloat)
148 |
149 | // Take until " ms\tMemory Size: " as BilledDuration(float64)
150 | pos = strings.Index(p.Rest, constSpaceMsBslashTMemorySpaceSizeColonSpace)
151 | if pos >= 0 {
152 | tmp = p.Rest[:pos]
153 | p.Rest = p.Rest[pos+len(constSpaceMsBslashTMemorySpaceSizeColonSpace):]
154 | } else {
155 | return false, nil
156 | }
157 | if tmpFloat, err = strconv.ParseFloat(tmp, 64); err != nil {
158 | return false, fmt.Errorf("parsing `%s` into field BilledDuration(float64): %s", tmp, err)
159 | }
160 | p.BilledDuration = float64(tmpFloat)
161 |
162 | // Take until " MB\tMax Memory Used: " as MemorySize(int)
163 | pos = strings.Index(p.Rest, constSpaceMBBslashTMaxSpaceMemorySpaceUsedColonSpace)
164 | if pos >= 0 {
165 | tmp = p.Rest[:pos]
166 | p.Rest = p.Rest[pos+len(constSpaceMBBslashTMaxSpaceMemorySpaceUsedColonSpace):]
167 | } else {
168 | return false, nil
169 | }
170 | if tmpInt, err = strconv.ParseInt(tmp, 10, 64); err != nil {
171 | return false, fmt.Errorf("parsing `%s` into field MemorySize(int): %s", tmp, err)
172 | }
173 | p.MemorySize = int(tmpInt)
174 |
175 | // Take until " MB\t" as MaxMemoryUsed(int)
176 | pos = strings.Index(p.Rest, constSpaceMBBslashT)
177 | if pos >= 0 {
178 | tmp = p.Rest[:pos]
179 | p.Rest = p.Rest[pos+len(constSpaceMBBslashT):]
180 | } else {
181 | return false, nil
182 | }
183 | if tmpInt, err = strconv.ParseInt(tmp, 10, 64); err != nil {
184 | return false, fmt.Errorf("parsing `%s` into field MaxMemoryUsed(int): %s", tmp, err)
185 | }
186 | p.MaxMemoryUsed = int(tmpInt)
187 |
188 | return true, nil
189 | }
190 |
191 | // AWSLambdaReportInit event.
192 | type AWSLambdaReportInit struct {
193 | Rest string
194 | RequestID string
195 | Duration float64
196 | BilledDuration float64
197 | MemorySize int
198 | MaxMemoryUsed int
199 | InitDuration float64
200 | }
201 |
202 | // Extract ...
203 | func (p *AWSLambdaReportInit) Extract(line string) (bool, error) {
204 | p.Rest = line
205 | var err error
206 | var pos int
207 | var tmp string
208 | var tmpFloat float64
209 | var tmpInt int64
210 |
211 | // Checks if the rest starts with `"REPORT RequestId: "` and pass it
212 | if strings.HasPrefix(p.Rest, constREPORTSpaceRequestIDColonSpace) {
213 | p.Rest = p.Rest[len(constREPORTSpaceRequestIDColonSpace):]
214 | } else {
215 | return false, nil
216 | }
217 |
218 | // Take until "\tDuration: " as RequestID(string)
219 | pos = strings.Index(p.Rest, constBslashTDurationColonSpace)
220 | if pos >= 0 {
221 | p.RequestID = p.Rest[:pos]
222 | p.Rest = p.Rest[pos+len(constBslashTDurationColonSpace):]
223 | } else {
224 | return false, nil
225 | }
226 |
227 | // Take until " ms\tBilled Duration: " as Duration(float64)
228 | pos = strings.Index(p.Rest, constSpaceMsBslashTBilledSpaceDurationColonSpace)
229 | if pos >= 0 {
230 | tmp = p.Rest[:pos]
231 | p.Rest = p.Rest[pos+len(constSpaceMsBslashTBilledSpaceDurationColonSpace):]
232 | } else {
233 | return false, nil
234 | }
235 | if tmpFloat, err = strconv.ParseFloat(tmp, 64); err != nil {
236 | return false, fmt.Errorf("parsing `%s` into field Duration(float64): %s", tmp, err)
237 | }
238 | p.Duration = float64(tmpFloat)
239 |
240 | // Take until " ms\tMemory Size: " as BilledDuration(float64)
241 | pos = strings.Index(p.Rest, constSpaceMsBslashTMemorySpaceSizeColonSpace)
242 | if pos >= 0 {
243 | tmp = p.Rest[:pos]
244 | p.Rest = p.Rest[pos+len(constSpaceMsBslashTMemorySpaceSizeColonSpace):]
245 | } else {
246 | return false, nil
247 | }
248 | if tmpFloat, err = strconv.ParseFloat(tmp, 64); err != nil {
249 | return false, fmt.Errorf("parsing `%s` into field BilledDuration(float64): %s", tmp, err)
250 | }
251 | p.BilledDuration = float64(tmpFloat)
252 |
253 | // Take until " MB\tMax Memory Used: " as MemorySize(int)
254 | pos = strings.Index(p.Rest, constSpaceMBBslashTMaxSpaceMemorySpaceUsedColonSpace)
255 | if pos >= 0 {
256 | tmp = p.Rest[:pos]
257 | p.Rest = p.Rest[pos+len(constSpaceMBBslashTMaxSpaceMemorySpaceUsedColonSpace):]
258 | } else {
259 | return false, nil
260 | }
261 | if tmpInt, err = strconv.ParseInt(tmp, 10, 64); err != nil {
262 | return false, fmt.Errorf("parsing `%s` into field MemorySize(int): %s", tmp, err)
263 | }
264 | p.MemorySize = int(tmpInt)
265 |
266 | // Take until " MB\tInit Duration: " as MaxMemoryUsed(int)
267 | pos = strings.Index(p.Rest, constSpaceMBBslashTInitSpaceDurationColonSpace)
268 | if pos >= 0 {
269 | tmp = p.Rest[:pos]
270 | p.Rest = p.Rest[pos+len(constSpaceMBBslashTInitSpaceDurationColonSpace):]
271 | } else {
272 | return false, nil
273 | }
274 | if tmpInt, err = strconv.ParseInt(tmp, 10, 64); err != nil {
275 | return false, fmt.Errorf("parsing `%s` into field MaxMemoryUsed(int): %s", tmp, err)
276 | }
277 | p.MaxMemoryUsed = int(tmpInt)
278 |
279 | // Take until " ms\t" as InitDuration(float64)
280 | pos = strings.Index(p.Rest, constSpaceMsBslashT)
281 | if pos >= 0 {
282 | tmp = p.Rest[:pos]
283 | p.Rest = p.Rest[pos+len(constSpaceMsBslashT):]
284 | } else {
285 | return false, nil
286 | }
287 | if tmpFloat, err = strconv.ParseFloat(tmp, 64); err != nil {
288 | return false, fmt.Errorf("parsing `%s` into field InitDuration(float64): %s", tmp, err)
289 | }
290 | p.InitDuration = float64(tmpFloat)
291 |
292 | return true, nil
293 | }
294 |
295 | // AWSLambdaTimeout event.
296 | type AWSLambdaTimeout struct {
297 | Rest string
298 | Timestamp string
299 | RequestID string
300 | Duration float64
301 | }
302 |
303 | // Extract ...
304 | func (p *AWSLambdaTimeout) Extract(line string) (bool, error) {
305 | p.Rest = line
306 | var err error
307 | var pos int
308 | var tmp string
309 | var tmpFloat float64
310 |
311 | // Take until " " as Timestamp(string)
312 | pos = strings.Index(p.Rest, constSpace)
313 | if pos >= 0 {
314 | p.Timestamp = p.Rest[:pos]
315 | p.Rest = p.Rest[pos+len(constSpace):]
316 | } else {
317 | return false, nil
318 | }
319 |
320 | // Take until " Task timed out after " as RequestID(string)
321 | pos = strings.Index(p.Rest, constSpaceTaskSpaceTimedSpaceOutSpaceAfterSpace)
322 | if pos >= 0 {
323 | p.RequestID = p.Rest[:pos]
324 | p.Rest = p.Rest[pos+len(constSpaceTaskSpaceTimedSpaceOutSpaceAfterSpace):]
325 | } else {
326 | return false, nil
327 | }
328 |
329 | // Take until " seconds" as Duration(float64)
330 | pos = strings.Index(p.Rest, constSpaceSeconds)
331 | if pos >= 0 {
332 | tmp = p.Rest[:pos]
333 | p.Rest = p.Rest[pos+len(constSpaceSeconds):]
334 | } else {
335 | return false, nil
336 | }
337 | if tmpFloat, err = strconv.ParseFloat(tmp, 64); err != nil {
338 | return false, fmt.Errorf("parsing `%s` into field Duration(float64): %s", tmp, err)
339 | }
340 | p.Duration = float64(tmpFloat)
341 |
342 | return true, nil
343 | }
344 |
345 | // Syslog event.
346 | type Syslog struct {
347 | Rest string
348 | Priority int
349 | SyslogVersion int
350 | Timestamp string
351 | Hostname string
352 | Appname string
353 | ProcID string
354 | MsgID string
355 | Message string
356 | }
357 |
358 | // Extract ...
359 | func (p *Syslog) Extract(line string) (bool, error) {
360 | p.Rest = line
361 | var err error
362 | var pos int
363 | var tmp string
364 | var tmpInt int64
365 |
366 | // Checks if the rest starts with `"<"` and pass it
367 | if strings.HasPrefix(p.Rest, constLess) {
368 | p.Rest = p.Rest[len(constLess):]
369 | } else {
370 | return false, nil
371 | }
372 |
373 | // Take until ">" as Priority(int)
374 | pos = strings.Index(p.Rest, constMore)
375 | if pos >= 0 {
376 | tmp = p.Rest[:pos]
377 | p.Rest = p.Rest[pos+len(constMore):]
378 | } else {
379 | return false, nil
380 | }
381 | if tmpInt, err = strconv.ParseInt(tmp, 10, 64); err != nil {
382 | return false, fmt.Errorf("parsing `%s` into field Priority(int): %s", tmp, err)
383 | }
384 | p.Priority = int(tmpInt)
385 |
386 | // Take until " " as SyslogVersion(int)
387 | pos = strings.Index(p.Rest, constSpace)
388 | if pos >= 0 {
389 | tmp = p.Rest[:pos]
390 | p.Rest = p.Rest[pos+len(constSpace):]
391 | } else {
392 | return false, nil
393 | }
394 | if tmpInt, err = strconv.ParseInt(tmp, 10, 64); err != nil {
395 | return false, fmt.Errorf("parsing `%s` into field SyslogVersion(int): %s", tmp, err)
396 | }
397 | p.SyslogVersion = int(tmpInt)
398 |
399 | // Take until " " as Timestamp(string)
400 | pos = strings.Index(p.Rest, constSpace)
401 | if pos >= 0 {
402 | p.Timestamp = p.Rest[:pos]
403 | p.Rest = p.Rest[pos+len(constSpace):]
404 | } else {
405 | return false, nil
406 | }
407 |
408 | // Take until " " as Hostname(string)
409 | pos = strings.Index(p.Rest, constSpace)
410 | if pos >= 0 {
411 | p.Hostname = p.Rest[:pos]
412 | p.Rest = p.Rest[pos+len(constSpace):]
413 | } else {
414 | return false, nil
415 | }
416 |
417 | // Take until " " as Appname(string)
418 | pos = strings.Index(p.Rest, constSpace)
419 | if pos >= 0 {
420 | p.Appname = p.Rest[:pos]
421 | p.Rest = p.Rest[pos+len(constSpace):]
422 | } else {
423 | return false, nil
424 | }
425 |
426 | // Take until " " as ProcID(string)
427 | pos = strings.Index(p.Rest, constSpace)
428 | if pos >= 0 {
429 | p.ProcID = p.Rest[:pos]
430 | p.Rest = p.Rest[pos+len(constSpace):]
431 | } else {
432 | return false, nil
433 | }
434 |
435 | // Take until " " as MsgID(string)
436 | pos = strings.Index(p.Rest, constSpace)
437 | if pos >= 0 {
438 | p.MsgID = p.Rest[:pos]
439 | p.Rest = p.Rest[pos+len(constSpace):]
440 | } else {
441 | return false, nil
442 | }
443 |
444 | // Take the rest as Message(string)
445 | p.Message = p.Rest
446 | p.Rest = p.Rest[len(p.Rest):]
447 | return true, nil
448 | }
449 |
450 | // HerokuDeploy event.
451 | type HerokuDeploy struct {
452 | Rest string
453 | Commit string
454 | User string
455 | }
456 |
457 | // Extract ...
458 | func (p *HerokuDeploy) Extract(line string) (bool, error) {
459 | p.Rest = line
460 | var pos int
461 |
462 | // Checks if the rest starts with `"Deploy "` and pass it
463 | if strings.HasPrefix(p.Rest, constDeploySpace) {
464 | p.Rest = p.Rest[len(constDeploySpace):]
465 | } else {
466 | return false, nil
467 | }
468 |
469 | // Take until " by user " as Commit(string)
470 | pos = strings.Index(p.Rest, constSpaceBySpaceUserSpace)
471 | if pos >= 0 {
472 | p.Commit = p.Rest[:pos]
473 | p.Rest = p.Rest[pos+len(constSpaceBySpaceUserSpace):]
474 | } else {
475 | return false, nil
476 | }
477 |
478 | // Take the rest as User(string)
479 | p.User = p.Rest
480 | p.Rest = p.Rest[len(p.Rest):]
481 | return true, nil
482 | }
483 |
484 | // HerokuRelease event.
485 | type HerokuRelease struct {
486 | Rest string
487 | Version string
488 | User string
489 | }
490 |
491 | // Extract ...
492 | func (p *HerokuRelease) Extract(line string) (bool, error) {
493 | p.Rest = line
494 | var pos int
495 |
496 | // Checks if the rest starts with `"Release "` and pass it
497 | if strings.HasPrefix(p.Rest, constReleaseSpace) {
498 | p.Rest = p.Rest[len(constReleaseSpace):]
499 | } else {
500 | return false, nil
501 | }
502 |
503 | // Take until " created by user " as Version(string)
504 | pos = strings.Index(p.Rest, constSpaceCreatedSpaceBySpaceUserSpace)
505 | if pos >= 0 {
506 | p.Version = p.Rest[:pos]
507 | p.Rest = p.Rest[pos+len(constSpaceCreatedSpaceBySpaceUserSpace):]
508 | } else {
509 | return false, nil
510 | }
511 |
512 | // Take the rest as User(string)
513 | p.User = p.Rest
514 | p.Rest = p.Rest[len(p.Rest):]
515 | return true, nil
516 | }
517 |
518 | // HerokuRollback event.
519 | type HerokuRollback struct {
520 | Rest string
521 | Version string
522 | User string
523 | }
524 |
525 | // Extract ...
526 | func (p *HerokuRollback) Extract(line string) (bool, error) {
527 | p.Rest = line
528 | var pos int
529 |
530 | // Checks if the rest starts with `"Rollback to "` and pass it
531 | if strings.HasPrefix(p.Rest, constRollbackSpaceToSpace) {
532 | p.Rest = p.Rest[len(constRollbackSpaceToSpace):]
533 | } else {
534 | return false, nil
535 | }
536 |
537 | // Take until " by user " as Version(string)
538 | pos = strings.Index(p.Rest, constSpaceBySpaceUserSpace)
539 | if pos >= 0 {
540 | p.Version = p.Rest[:pos]
541 | p.Rest = p.Rest[pos+len(constSpaceBySpaceUserSpace):]
542 | } else {
543 | return false, nil
544 | }
545 |
546 | // Take the rest as User(string)
547 | p.User = p.Rest
548 | p.Rest = p.Rest[len(p.Rest):]
549 | return true, nil
550 | }
551 |
552 | // HerokuBuild event.
553 | type HerokuBuild struct {
554 | Rest string
555 | User string
556 | }
557 |
558 | // Extract ...
559 | func (p *HerokuBuild) Extract(line string) (bool, error) {
560 | p.Rest = line
561 |
562 | // Checks if the rest starts with `"Build started by user "` and pass it
563 | if strings.HasPrefix(p.Rest, constBuildSpaceStartedSpaceBySpaceUserSpace) {
564 | p.Rest = p.Rest[len(constBuildSpaceStartedSpaceBySpaceUserSpace):]
565 | } else {
566 | return false, nil
567 | }
568 |
569 | // Take the rest as User(string)
570 | p.User = p.Rest
571 | p.Rest = p.Rest[len(p.Rest):]
572 | return true, nil
573 | }
574 |
575 | // HerokuStateChange event.
576 | type HerokuStateChange struct {
577 | Rest string
578 | From string
579 | To string
580 | }
581 |
582 | // Extract ...
583 | func (p *HerokuStateChange) Extract(line string) (bool, error) {
584 | p.Rest = line
585 | var pos int
586 |
587 | // Checks if the rest starts with `"State changed from "` and pass it
588 | if strings.HasPrefix(p.Rest, constStateSpaceChangedSpaceFromSpace) {
589 | p.Rest = p.Rest[len(constStateSpaceChangedSpaceFromSpace):]
590 | } else {
591 | return false, nil
592 | }
593 |
594 | // Take until " to " as From(string)
595 | pos = strings.Index(p.Rest, constSpaceToSpace)
596 | if pos >= 0 {
597 | p.From = p.Rest[:pos]
598 | p.Rest = p.Rest[pos+len(constSpaceToSpace):]
599 | } else {
600 | return false, nil
601 | }
602 |
603 | // Take the rest as To(string)
604 | p.To = p.Rest
605 | p.Rest = p.Rest[len(p.Rest):]
606 | return true, nil
607 | }
608 |
609 | // HerokuProcessExit event.
610 | type HerokuProcessExit struct {
611 | Rest string
612 | Status int
613 | }
614 |
615 | // Extract ...
616 | func (p *HerokuProcessExit) Extract(line string) (bool, error) {
617 | p.Rest = line
618 | var err error
619 | var tmpInt int64
620 |
621 | // Checks if the rest starts with `"Process exited with status "` and pass it
622 | if strings.HasPrefix(p.Rest, constProcessSpaceExitedSpaceWithSpaceStatusSpace) {
623 | p.Rest = p.Rest[len(constProcessSpaceExitedSpaceWithSpaceStatusSpace):]
624 | } else {
625 | return false, nil
626 | }
627 |
628 | // Take the rest as Status(int)
629 | if tmpInt, err = strconv.ParseInt(p.Rest, 10, 64); err != nil {
630 | return false, fmt.Errorf("parsing `%s` into field Status(int): %s", p.Rest, err)
631 | }
632 | p.Status = int(tmpInt)
633 | p.Rest = p.Rest[len(p.Rest):]
634 | return true, nil
635 | }
636 |
637 | // HerokuProcessStart event.
638 | type HerokuProcessStart struct {
639 | Rest string
640 | Command string
641 | }
642 |
643 | // Extract ...
644 | func (p *HerokuProcessStart) Extract(line string) (bool, error) {
645 | p.Rest = line
646 | var pos int
647 |
648 | // Checks if the rest starts with `"Starting process with command `"` and pass it
649 | if strings.HasPrefix(p.Rest, constStartingSpaceProcessSpaceWithSpaceCommandSpace) {
650 | p.Rest = p.Rest[len(constStartingSpaceProcessSpaceWithSpaceCommandSpace):]
651 | } else {
652 | return false, nil
653 | }
654 |
655 | // Take until "`" as Command(string)
656 | pos = strings.Index(p.Rest, constUnrecognizedSequence)
657 | if pos >= 0 {
658 | p.Command = p.Rest[:pos]
659 | p.Rest = p.Rest[pos+len(constUnrecognizedSequence):]
660 | } else {
661 | return false, nil
662 | }
663 |
664 | return true, nil
665 | }
666 |
667 | // HerokuProcessListening event.
668 | type HerokuProcessListening struct {
669 | Rest string
670 | Port int
671 | }
672 |
673 | // Extract ...
674 | func (p *HerokuProcessListening) Extract(line string) (bool, error) {
675 | p.Rest = line
676 | var err error
677 | var tmpInt int64
678 |
679 | // Checks if the rest starts with `"Listening on "` and pass it
680 | if strings.HasPrefix(p.Rest, constListeningSpaceOnSpace) {
681 | p.Rest = p.Rest[len(constListeningSpaceOnSpace):]
682 | } else {
683 | return false, nil
684 | }
685 |
686 | // Take the rest as Port(int)
687 | if tmpInt, err = strconv.ParseInt(p.Rest, 10, 64); err != nil {
688 | return false, fmt.Errorf("parsing `%s` into field Port(int): %s", p.Rest, err)
689 | }
690 | p.Port = int(tmpInt)
691 | p.Rest = p.Rest[len(p.Rest):]
692 | return true, nil
693 | }
694 |
695 | // HerokuConfigSet event.
696 | type HerokuConfigSet struct {
697 | Rest string
698 | Variables string
699 | User string
700 | }
701 |
702 | // Extract ...
703 | func (p *HerokuConfigSet) Extract(line string) (bool, error) {
704 | p.Rest = line
705 | var pos int
706 |
707 | // Checks if the rest starts with `"Set "` and pass it
708 | if strings.HasPrefix(p.Rest, constSetSpace) {
709 | p.Rest = p.Rest[len(constSetSpace):]
710 | } else {
711 | return false, nil
712 | }
713 |
714 | // Take until " config vars by user " as Variables(string)
715 | pos = strings.Index(p.Rest, constSpaceConfigSpaceVarsSpaceBySpaceUserSpace)
716 | if pos >= 0 {
717 | p.Variables = p.Rest[:pos]
718 | p.Rest = p.Rest[pos+len(constSpaceConfigSpaceVarsSpaceBySpaceUserSpace):]
719 | } else {
720 | return false, nil
721 | }
722 |
723 | // Take the rest as User(string)
724 | p.User = p.Rest
725 | p.Rest = p.Rest[len(p.Rest):]
726 | return true, nil
727 | }
728 |
729 | // HerokuConfigRemove event.
730 | type HerokuConfigRemove struct {
731 | Rest string
732 | Variables string
733 | User string
734 | }
735 |
736 | // Extract ...
737 | func (p *HerokuConfigRemove) Extract(line string) (bool, error) {
738 | p.Rest = line
739 | var pos int
740 |
741 | // Checks if the rest starts with `"Remove "` and pass it
742 | if strings.HasPrefix(p.Rest, constRemoveSpace) {
743 | p.Rest = p.Rest[len(constRemoveSpace):]
744 | } else {
745 | return false, nil
746 | }
747 |
748 | // Take until " config vars by user " as Variables(string)
749 | pos = strings.Index(p.Rest, constSpaceConfigSpaceVarsSpaceBySpaceUserSpace)
750 | if pos >= 0 {
751 | p.Variables = p.Rest[:pos]
752 | p.Rest = p.Rest[pos+len(constSpaceConfigSpaceVarsSpaceBySpaceUserSpace):]
753 | } else {
754 | return false, nil
755 | }
756 |
757 | // Take the rest as User(string)
758 | p.User = p.Rest
759 | p.Rest = p.Rest[len(p.Rest):]
760 | return true, nil
761 | }
762 |
763 | // HerokuScale event.
764 | type HerokuScale struct {
765 | Rest string
766 | Dynos string
767 | User string
768 | }
769 |
770 | // Extract ...
771 | func (p *HerokuScale) Extract(line string) (bool, error) {
772 | p.Rest = line
773 | var pos int
774 |
775 | // Checks if the rest starts with `"Scaled to "` and pass it
776 | if strings.HasPrefix(p.Rest, constScaledSpaceToSpace) {
777 | p.Rest = p.Rest[len(constScaledSpaceToSpace):]
778 | } else {
779 | return false, nil
780 | }
781 |
782 | // Take until " by user " as Dynos(string)
783 | pos = strings.Index(p.Rest, constSpaceBySpaceUserSpace)
784 | if pos >= 0 {
785 | p.Dynos = p.Rest[:pos]
786 | p.Rest = p.Rest[pos+len(constSpaceBySpaceUserSpace):]
787 | } else {
788 | return false, nil
789 | }
790 |
791 | // Take the rest as User(string)
792 | p.User = p.Rest
793 | p.Rest = p.Rest[len(p.Rest):]
794 | return true, nil
795 | }
796 |
--------------------------------------------------------------------------------