├── .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 | [![GoDoc](https://godoc.org/github.com/apex/parsers?status.svg)](https://godoc.org/github.com/apex/parsers) 16 | ![](https://img.shields.io/badge/license-MIT-blue.svg) 17 | ![](https://img.shields.io/badge/status-stable-green.svg) 18 | ![](https://github.com/apex/parsers/workflows/Tests/badge.svg) 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 | --------------------------------------------------------------------------------