├── .github └── FUNDING.yml ├── .gitignore ├── LICENSE ├── README.md ├── defaults.go ├── errlog.go ├── examples ├── basic │ └── basic.go ├── custom │ └── custom.go ├── disabled │ └── disabled.go ├── failingLineFar │ └── failingLineFar.go └── stackTrace │ └── stackTrace.go ├── logger.go ├── regexp.go └── utils.go /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: snwfdhmp 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | examples/local-test -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Martin Joly 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Errlog: reduce debugging time while programming [![Go Report Card](https://goreportcard.com/badge/github.com/snwfdhmp/errlog)](https://goreportcard.com/report/github.com/snwfdhmp/errlog) [![Awesome](https://cdn.rawgit.com/sindresorhus/awesome/d7305f38d29fed78fa85652e3a63e154dd8e8829/media/badge.svg)](https://github.com/sindresorhus/awesome) [![Documentation](https://godoc.org/github.com/snwfdhmp/errlog?status.svg)](http://godoc.org/github.com/snwfdhmp/errlog) [![GitHub issues](https://img.shields.io/github/issues/snwfdhmp/errlog.svg)](https://github.com/snwfdhmp/errlog/issues) [![license](https://img.shields.io/github/license/snwfdhmp/errlog.svg?maxAge=6000)](https://github.com/snwfdhmp/errlog/LICENSE) 2 | 3 | ![Example](https://i.imgur.com/Ulf1RGw.png) 4 | 5 | Buy Me A Coffee 6 | 7 | ## Introduction 8 | 9 | Use errlog to improve error logging and **speed up debugging while you create amazing code** : 10 | 11 | - Highlight source code 12 | - **Detect and point out** which func call is causing the fail 13 | - Pretty stack trace 14 | - **No-op mode** for production 15 | - Easy implementation, adaptable logger 16 | - Plug to any current project without changing you or your teammates habits 17 | - Plug to **your current logging system** 18 | 19 | |Go to| 20 | |---| 21 | |[Get started](#get-started)| 22 | |[Documentation](#documentation)| 23 | |[Examples](#example)| 24 | |[Tweaking](#tweak-as-you-need)| 25 | |[Feedbacks](#feedbacks)| 26 | |[Contributions](#contributions)| 27 | |[License](#license-information)| 28 | |[Contributors](#contributors)| 29 | 30 | ## Get started 31 | 32 | ### Install 33 | 34 | ```shell 35 | go get github.com/snwfdhmp/errlog 36 | ``` 37 | 38 | ### Usage 39 | 40 | Replace your `if err != nil` with `if errlog.Debug(err)` to add debugging informations. 41 | 42 | ```golang 43 | func someFunc() { 44 | //... 45 | if errlog.Debug(err) { // will debug & pass if err != nil, will ignore if err == nil 46 | return 47 | } 48 | } 49 | ``` 50 | 51 | In production, call `errlog.DefaultLogger.Disable(true)` to enable no-op (equivalent to `if err != nil`) 52 | 53 | ## Tweak as you need 54 | 55 | You can configure your own logger with the following options : 56 | 57 | ```golang 58 | type Config struct { 59 | PrintFunc func(format string, data ...interface{}) //Printer func (eg: fmt.Printf) 60 | LinesBefore int //How many lines to print *before* the error line when printing source code 61 | LinesAfter int //How many lines to print *after* the error line when printing source code 62 | PrintStack bool //Shall we print stack trace ? yes/no 63 | PrintSource bool //Shall we print source code along ? yes/no 64 | PrintError bool //Shall we print the error of Debug(err) ? yes/no 65 | ExitOnDebugSuccess bool //Shall we os.Exit(1) after Debug has finished logging everything ? (doesn't happen when err is nil). Will soon be replaced by ExitFunc to enable panic-ing the current goroutine. (if you need this quick, please open an issue) 66 | } 67 | ``` 68 | 69 | > As we don't yet update automatically this README immediately when we add new features, this definition may be outdated. (Last update: 2019/08/07) 70 | > [See the struct definition in godoc.org](https://godoc.org/github.com/snwfdhmp/errlog#Config) for the up to date definition 71 | 72 | 73 | ## Example 74 | 75 | ### Try yourself 76 | 77 | | Name and link | Description | 78 | | --- | --- | 79 | | [Basic](examples/basic/basic.go) | standard usage, quick setup 80 | | [Custom](examples/custom/custom.go) | guided configuration for fulfilling your needs | 81 | | [Disabled](examples/disabled/disabled.go) | how to disable the logging & debugging (eg: for production use) | 82 | | [Failing line far away](examples/failingLineFar/failingLineFar.go) | example of finding the func call that caused the error while it is lines away from the errlog.Debug call | 83 | | [Pretty stack trace](examples/stackTrace/stackTrace.go) | pretty stack trace printing instead of debugging. | 84 | 85 | ### Just read 86 | 87 | #### Basic example 88 | 89 | > Note that in the example, you will see some unuseful func. Those are made to generate additional stack trace levels for the sake of example 90 | 91 | We're going to use this sample program : 92 | 93 | ```golang 94 | func main() { 95 | fmt.Println("Program start") 96 | 97 | wrapingFunc() //call to our important function 98 | 99 | fmt.Println("Program end") 100 | } 101 | 102 | func wrapingFunc() { 103 | someBigFunction() // call some func 104 | } 105 | 106 | func someBigFunction() { 107 | someDumbFunction() // just random calls 108 | someSmallFunction() // just random calls 109 | someDumbFunction() // just random calls 110 | 111 | // Here it can fail, so instead of `if err != nil` we use `errlog.Debug(err)` 112 | if err := someNastyFunction(); errlog.Debug(err) { 113 | return 114 | } 115 | 116 | someSmallFunction() // just random calls 117 | someDumbFunction() // just random calls 118 | } 119 | 120 | func someSmallFunction() { 121 | _ = fmt.Sprintf("I do things !") 122 | } 123 | 124 | func someNastyFunction() error { 125 | return errors.New("I'm failing for some reason") // simulate an error 126 | } 127 | 128 | func someDumbFunction() bool { 129 | return false // just random things 130 | } 131 | ``` 132 | 133 | 134 | #### Output 135 | 136 | ![Console Output examples/basic.go](https://i.imgur.com/tOkDgwP.png) 137 | 138 | 139 | We are able to **detect and point out which line is causing the error**. 140 | 141 | ### Custom Configuration Example 142 | 143 | Let's see what we can do with a **custom configuration.** 144 | 145 | ```golang 146 | debug := errlog.NewLogger(&errlog.Config{ 147 | // PrintFunc is of type `func (format string, data ...interface{})` 148 | // so you can easily implement your own logger func. 149 | // In this example, logrus is used, but any other logger can be used. 150 | // Beware that you should add '\n' at the end of format string when printing. 151 | PrintFunc: logrus.Printf, 152 | PrintSource: true, //Print the failing source code 153 | LinesBefore: 2, //Print 2 lines before failing line 154 | LinesAfter: 1, //Print 1 line after failing line 155 | PrintError: true, //Print the error 156 | PrintStack: false, //Don't print the stack trace 157 | ExitOnDebugSuccess: true, //Exit if err 158 | }) 159 | ``` 160 | 161 | > Please note: This definition may be outdated. (Last update: 2019/08/07) 162 | > [See the struct definition in godoc.org](https://godoc.org/github.com/snwfdhmp/errlog#Config) for the up to date definition 163 | 164 | #### Output 165 | 166 | ![Console Output examples/custom.go](https://i.imgur.com/vh2iEnS.png) 167 | 168 | 169 | ### When the failing func call is a few lines away 170 | 171 | Even when the func call is a few lines away, there is no problem for finding it. 172 | 173 | #### Output 174 | 175 | ![Source Example: error earlier in the code](https://i.imgur.com/wPBrYqs.png) 176 | 177 | ## Documentation 178 | 179 | Documentation can be found here : [![Documentation](https://godoc.org/github.com/snwfdhmp/errlog?status.svg)](http://godoc.org/github.com/snwfdhmp/errlog) 180 | 181 | ## Feedbacks 182 | 183 | Feel free to open an issue for any feedback or suggestion. 184 | 185 | I fix process issues quickly. 186 | 187 | ## Contributions 188 | 189 | We are happy to collaborate with you : 190 | 191 | - Ask for a new feature: [Open an issue](https://github.com/snwfdhmp/errlog/issues/new) 192 | - Add your feature: [Open a PR](https://github.com/snwfdhmp/errlog/compare) 193 | 194 | When submitting a PR, please apply Effective Go best practices. For more information: https://golang.org/doc/effective_go.html 195 | 196 | ## License information 197 | 198 | Click the following badge to open LICENSE information. 199 | 200 | [![license](https://img.shields.io/github/license/snwfdhmp/errlog.svg?maxAge=60000)](https://github.com/snwfdhmp/errlog/LICENSE) 201 | 202 | ## Contributors 203 | 204 | ### Major 205 | 206 | - [snwfdhmp](https://github.com/snwfdhmp): Author and maintainer 207 | - [chemidy](https://github.com/chemidy): Added important badges 208 | 209 | ### Minor fixes 210 | 211 | - [orisano](https://github.com/orisano) 212 | - [programmingman](https://github.com/programmingman) 213 | -------------------------------------------------------------------------------- /defaults.go: -------------------------------------------------------------------------------- 1 | package errlog 2 | 3 | import "fmt" 4 | 5 | var ( 6 | //DefaultLoggerPrintFunc is fmt.Printf without return values 7 | DefaultLoggerPrintFunc = func(format string, data ...interface{}) { 8 | fmt.Printf(format+"\n", data...) 9 | } 10 | 11 | //DefaultLogger logger implements default configuration for a logger 12 | DefaultLogger = &logger{ 13 | config: &Config{ 14 | PrintFunc: DefaultLoggerPrintFunc, 15 | LinesBefore: 4, 16 | LinesAfter: 2, 17 | PrintStack: false, 18 | PrintSource: true, 19 | PrintError: true, 20 | ExitOnDebugSuccess: false, 21 | }, 22 | } 23 | ) 24 | -------------------------------------------------------------------------------- /errlog.go: -------------------------------------------------------------------------------- 1 | // Package errlog provides a simple object to enhance Go source code debugging 2 | // 3 | // Example result: 4 | // 5 | // 6 | // $ go run myfailingapp.go 7 | // Program starting 8 | // error in main.main: something failed here 9 | // line 13 of /Users/snwfdhmp/go/src/github.com/snwfdhmp/sandbox/testerr.go 10 | // 9: func main() { 11 | // 10: fmt.Println("Program starting") 12 | // 11: err := errors.New("something failed here") 13 | // 12: 14 | // 13: errlog.Debug(err) 15 | // 14: 16 | // 15: fmt.Println("End of the program") 17 | // 16: } 18 | // exit status 1 19 | // 20 | // 21 | // You can configure your own logger with these options : 22 | // 23 | // 24 | // type Config struct { 25 | // LinesBefore int 26 | // LinesAfter int 27 | // PrintStack bool 28 | // PrintSource bool 29 | // PrintError bool 30 | // ExitOnDebugSuccess bool 31 | // } 32 | // 33 | // 34 | // Example : 35 | // 36 | // 37 | // debug := errlog.NewLogger(&errlog.Config{ 38 | // LinesBefore: 2, 39 | // LinesAfter: 1, 40 | // PrintError: true, 41 | // PrintSource: true, 42 | // PrintStack: false, 43 | // ExitOnDebugSuccess: true, 44 | // }) 45 | // 46 | // // ... 47 | // if err != nil { 48 | // debug.Debug(err) 49 | // return 50 | // } 51 | // 52 | // Outputs : 53 | // 54 | // Error in main.someBigFunction(): I'm failing for no reason 55 | // line 41 of /Users/snwfdhmp/go/src/github.com/snwfdhmp/sandbox/testerr.go:41 56 | // 33: func someBigFunction() { 57 | // ... 58 | // 40: if err := someNastyFunction(); err != nil { 59 | // 41: debug.Debug(err) 60 | // 42: return 61 | // 43: } 62 | // exit status 1 63 | // 64 | package errlog 65 | 66 | import ( 67 | "github.com/sirupsen/logrus" 68 | "github.com/spf13/afero" 69 | ) 70 | 71 | var ( 72 | debugMode = false 73 | fs = afero.NewOsFs() //fs is at package level because I think it needn't be scoped to loggers 74 | ) 75 | 76 | //SetDebugMode sets debug mode to On if toggle==true or Off if toggle==false. It changes log level an so displays more logs about whats happening. Useful for debugging. 77 | func SetDebugMode(toggle bool) { 78 | if toggle { 79 | logrus.SetLevel(logrus.DebugLevel) 80 | } else { 81 | logrus.SetLevel(logrus.InfoLevel) 82 | } 83 | debugMode = toggle 84 | } 85 | 86 | //Debug is a shortcut for DefaultLogger.Debug. 87 | func Debug(uErr error) bool { 88 | DefaultLogger.Overload(1) // Prevents from adding this func to the stack trace 89 | return DefaultLogger.Debug(uErr) 90 | } 91 | 92 | //PrintStack pretty prints the current stack trace 93 | func PrintStack() { 94 | DefaultLogger.printStack(parseStackTrace(1)) 95 | } 96 | 97 | //PrintRawStack prints the current stack trace unparsed 98 | func PrintRawStack() { 99 | DefaultLogger.Printf("%#v", parseStackTrace(1)) 100 | } 101 | 102 | //PrintStackMinus prints the current stack trace minus the amount of depth in parameter 103 | func PrintStackMinus(depthToRemove int) { 104 | DefaultLogger.printStack(parseStackTrace(1 + depthToRemove)) 105 | } 106 | -------------------------------------------------------------------------------- /examples/basic/basic.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/snwfdhmp/errlog" 8 | ) 9 | 10 | func main() { 11 | fmt.Println("Example start") 12 | 13 | wrappingFunc() 14 | 15 | fmt.Println("Example end") 16 | } 17 | 18 | func wrappingFunc() { 19 | someBigFunction() 20 | } 21 | 22 | func someBigFunction() { 23 | someDumbFunction() 24 | 25 | someSmallFunction() 26 | 27 | someDumbFunction() 28 | 29 | if err := someNastyFunction(); errlog.Debug(err) { 30 | return 31 | } 32 | 33 | someSmallFunction() 34 | 35 | someDumbFunction() 36 | } 37 | 38 | func someSmallFunction() { 39 | _ = fmt.Sprintf("I do things !") 40 | } 41 | 42 | func someNastyFunction() error { 43 | return errors.New("I'm failing for some reason") 44 | } 45 | 46 | func someDumbFunction() bool { 47 | return false 48 | } 49 | -------------------------------------------------------------------------------- /examples/custom/custom.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/sirupsen/logrus" 7 | "github.com/snwfdhmp/errlog" 8 | ) 9 | 10 | var ( 11 | debug = errlog.NewLogger(&errlog.Config{ 12 | PrintFunc: logrus.Errorf, 13 | LinesBefore: 6, 14 | LinesAfter: 3, 15 | PrintError: true, 16 | PrintSource: true, 17 | PrintStack: false, 18 | ExitOnDebugSuccess: true, 19 | }) 20 | ) 21 | 22 | func main() { 23 | logrus.Print("Start of the program") 24 | 25 | wrapingFunc() 26 | 27 | logrus.Print("End of the program") 28 | } 29 | 30 | func wrapingFunc() { 31 | someBigFunction() 32 | } 33 | 34 | func someBigFunction() { 35 | someDumbFunction() 36 | 37 | someSmallFunction() 38 | 39 | someDumbFunction() 40 | 41 | if err := someNastyFunction(); debug.Debug(err) { 42 | return 43 | } 44 | 45 | someSmallFunction() 46 | 47 | someDumbFunction() 48 | } 49 | 50 | func someSmallFunction() { 51 | logrus.Print("I do things !") 52 | } 53 | 54 | func someNastyFunction() error { 55 | return errors.New("I'm failing for no reason") 56 | } 57 | 58 | func someDumbFunction() bool { 59 | return false 60 | } 61 | -------------------------------------------------------------------------------- /examples/disabled/disabled.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/snwfdhmp/errlog" 8 | ) 9 | 10 | func init() { 11 | errlog.DefaultLogger.Disable(true) 12 | } 13 | 14 | func main() { 15 | fmt.Println("Example start") 16 | 17 | wrapingFunc() 18 | 19 | fmt.Println("Example end") 20 | } 21 | 22 | func wrapingFunc() { 23 | someBigFunction() 24 | } 25 | 26 | func someBigFunction() { 27 | someDumbFunction() 28 | 29 | someSmallFunction() 30 | 31 | someDumbFunction() 32 | 33 | if err := someNastyFunction(); errlog.Debug(err) { 34 | return 35 | } 36 | 37 | someSmallFunction() 38 | 39 | someDumbFunction() 40 | } 41 | 42 | func someSmallFunction() { 43 | _ = fmt.Sprintf("I do things !") 44 | } 45 | 46 | func someNastyFunction() error { 47 | return errors.New("I'm failing for some reason") 48 | } 49 | 50 | func someDumbFunction() bool { 51 | return false 52 | } 53 | -------------------------------------------------------------------------------- /examples/failingLineFar/failingLineFar.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/sirupsen/logrus" 7 | "github.com/snwfdhmp/errlog" 8 | ) 9 | 10 | var ( 11 | debug = errlog.NewLogger(&errlog.Config{ 12 | PrintFunc: logrus.Errorf, 13 | LinesBefore: 6, 14 | LinesAfter: 3, 15 | PrintError: true, 16 | PrintSource: true, 17 | PrintStack: true, 18 | ExitOnDebugSuccess: true, 19 | }) 20 | ) 21 | 22 | func main() { 23 | logrus.Print("Start of the program") 24 | 25 | wrapingFunc() 26 | 27 | logrus.Print("End of the program") 28 | } 29 | 30 | func wrapingFunc() { 31 | someBigFunction() 32 | } 33 | 34 | func someBigFunction() { 35 | someDumbFunction() 36 | 37 | someSmallFunction() 38 | err := someNastyFunction() 39 | someDumbFunction() 40 | 41 | if debug.Debug(err) { 42 | return 43 | } 44 | 45 | someSmallFunction() 46 | 47 | someDumbFunction() 48 | } 49 | 50 | func someSmallFunction() { 51 | logrus.Print("I do things !") 52 | } 53 | 54 | func someNastyFunction() error { 55 | return errors.New("I'm failing for no reason") 56 | } 57 | 58 | func someDumbFunction() bool { 59 | return false 60 | } 61 | -------------------------------------------------------------------------------- /examples/stackTrace/stackTrace.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/snwfdhmp/errlog" 5 | ) 6 | 7 | func main() { 8 | errlog.PrintRawStack() 9 | } 10 | -------------------------------------------------------------------------------- /logger.go: -------------------------------------------------------------------------------- 1 | package errlog 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | 7 | "github.com/fatih/color" 8 | "github.com/sirupsen/logrus" 9 | "github.com/spf13/afero" 10 | ) 11 | 12 | var ( 13 | gopath = os.Getenv("GOPATH") 14 | ) 15 | 16 | //Logger interface allows to log an error, or to print source code lines. Check out NewLogger function to learn more about Logger objects and Config. 17 | type Logger interface { 18 | // Debug wraps up Logger debugging funcs related to an error 19 | // If the given error is nil, it returns immediately 20 | // It relies on Logger.Config to determine what will be printed or executed 21 | // It returns whether err != nil 22 | Debug(err error) bool 23 | //PrintSource prints lines based on given opts (see PrintSourceOptions type definition) 24 | PrintSource(lines []string, opts PrintSourceOptions) 25 | //DebugSource debugs a source file 26 | DebugSource(filename string, lineNumber int) 27 | //SetConfig replaces current config with the given one 28 | SetConfig(cfg *Config) 29 | //Config returns current config 30 | Config() *Config 31 | //Disable is used to disable Logger (every call to this Logger will perform NO-OP (no operation)) and return instantly 32 | //Use Disable(true) to disable and Disable(false) to enable again 33 | Disable(bool) 34 | } 35 | 36 | //Config holds the configuration for a logger 37 | type Config struct { 38 | PrintFunc func(format string, data ...interface{}) //Printer func (eg: fmt.Printf) 39 | LinesBefore int //How many lines to print *before* the error line when printing source code 40 | LinesAfter int //How many lines to print *after* the error line when printing source code 41 | PrintStack bool //Shall we print stack trace ? yes/no 42 | PrintSource bool //Shall we print source code along ? yes/no 43 | PrintError bool //Shall we print the error of Debug(err) ? yes/no 44 | ExitOnDebugSuccess bool //Shall we os.Exit(1) after Debug has finished logging everything ? (doesn't happen when err is nil) 45 | DisableStackIndentation bool //Shall we print stack vertically instead of indented 46 | Mode int 47 | } 48 | 49 | // PrintSourceOptions represents config for (*logger).PrintSource func 50 | type PrintSourceOptions struct { 51 | FuncLine int 52 | StartLine int 53 | EndLine int 54 | Highlighted map[int][]int //map[lineIndex][columnstart, columnEnd] of chars to highlight 55 | } 56 | 57 | //logger holds logger object, implementing Logger interface 58 | type logger struct { 59 | config *Config //config for the logger 60 | stackDepthOverload int //stack depth to ignore when reading stack 61 | } 62 | 63 | //NewLogger creates a new logger struct with given config 64 | func NewLogger(cfg *Config) Logger { 65 | l := logger{ 66 | config: cfg, 67 | stackDepthOverload: 0, 68 | } 69 | 70 | l.Doctor() 71 | 72 | return &l 73 | } 74 | 75 | // Debug wraps up Logger debugging funcs related to an error 76 | // If the given error is nil, it returns immediately 77 | // It relies on Logger.Config to determine what will be printed or executed 78 | func (l *logger) Debug(uErr error) bool { 79 | if l.config.Mode == ModeDisabled { 80 | return uErr != nil 81 | } 82 | l.Doctor() 83 | if uErr == nil { 84 | return false 85 | } 86 | 87 | stLines := parseStackTrace(1 + l.stackDepthOverload) 88 | if stLines == nil || len(stLines) < 1 { 89 | l.Printf("Error: %s", uErr) 90 | l.Printf("Errlog tried to debug the error but the stack trace seems empty. If you think this is an error, please open an issue at https://github.com/snwfdhmp/errlog/issues/new and provide us logs to investigate.") 91 | return true 92 | } 93 | 94 | if l.config.PrintError { 95 | l.Printf("Error in %s: %s", stLines[0].CallingObject, color.YellowString(uErr.Error())) 96 | } 97 | 98 | if l.config.PrintSource { 99 | l.DebugSource(stLines[0].SourcePathRef, stLines[0].SourceLineRef) 100 | } 101 | 102 | if l.config.PrintStack { 103 | l.Printf("Stack trace:") 104 | l.printStack(stLines) 105 | } 106 | 107 | if l.config.ExitOnDebugSuccess { 108 | os.Exit(1) 109 | } 110 | 111 | l.stackDepthOverload = 0 112 | 113 | return true 114 | } 115 | 116 | //DebugSource prints certain lines of source code of a file for debugging, using (*logger).config as configurations 117 | func (l *logger) DebugSource(filepath string, debugLineNumber int) { 118 | filepathShort := filepath 119 | if gopath != "" { 120 | filepathShort = strings.Replace(filepath, gopath+"/src/", "", -1) 121 | } 122 | 123 | b, err := afero.ReadFile(fs, filepath) 124 | if err != nil { 125 | l.Printf("errlog: cannot read file '%s': %s. If sources are not reachable in this environment, you should set PrintSource=false in logger config.", filepath, err) 126 | return 127 | // l.Debug(err) 128 | } 129 | lines := strings.Split(string(b), "\n") 130 | 131 | // set line range to print based on config values and debugLineNumber 132 | minLine := debugLineNumber - l.config.LinesBefore 133 | maxLine := debugLineNumber + l.config.LinesAfter 134 | 135 | //delete blank lines from range and clean range if out of lines range 136 | deleteBlankLinesFromRange(lines, &minLine, &maxLine) 137 | 138 | //free some memory from unused values 139 | lines = lines[:maxLine+1] 140 | 141 | //find func line and adjust minLine if below 142 | funcLine := findFuncLine(lines, debugLineNumber) 143 | if funcLine > minLine { 144 | minLine = funcLine + 1 145 | } 146 | 147 | //try to find failing line if any 148 | failingLineIndex, columnStart, columnEnd := findFailingLine(lines, funcLine, debugLineNumber) 149 | 150 | if failingLineIndex != -1 { 151 | l.Printf("line %d of %s:%d", failingLineIndex+1, filepathShort, failingLineIndex+1) 152 | } else { 153 | l.Printf("error in %s (failing line not found, stack trace says func call is at line %d)", filepathShort, debugLineNumber) 154 | } 155 | 156 | l.PrintSource(lines, PrintSourceOptions{ 157 | FuncLine: funcLine, 158 | Highlighted: map[int][]int{ 159 | failingLineIndex: {columnStart, columnEnd}, 160 | }, 161 | StartLine: minLine, 162 | EndLine: maxLine, 163 | }) 164 | } 165 | 166 | // PrintSource prints source code based on opts 167 | func (l *logger) PrintSource(lines []string, opts PrintSourceOptions) { 168 | //print func on first line 169 | if opts.FuncLine != -1 && opts.FuncLine < opts.StartLine { 170 | l.Printf("%s", color.RedString("%d: %s", opts.FuncLine+1, lines[opts.FuncLine])) 171 | if opts.FuncLine < opts.StartLine-1 { // append blank line if minLine is not next line 172 | l.Printf("%s", color.YellowString("...")) 173 | } 174 | } 175 | 176 | for i := opts.StartLine; i < opts.EndLine; i++ { 177 | if _, ok := opts.Highlighted[i]; !ok || len(opts.Highlighted[i]) != 2 { 178 | l.Printf("%d: %s", i+1, color.YellowString(lines[i])) 179 | continue 180 | } 181 | 182 | hlStart := max(opts.Highlighted[i][0], 0) //highlight column start 183 | hlEnd := min(opts.Highlighted[i][1], len(lines)-1) //highlight column end 184 | l.Printf("%d: %s%s%s", i+1, color.YellowString(lines[i][:hlStart]), color.RedString(lines[i][hlStart:hlEnd+1]), color.YellowString(lines[i][hlEnd+1:])) 185 | } 186 | } 187 | 188 | func (l *logger) Doctor() (neededDoctor bool) { 189 | neededDoctor = false 190 | 191 | if l.config.PrintFunc == nil { 192 | neededDoctor = true 193 | logrus.Debug("PrintFunc not set for this logger. Replacing with DefaultLoggerPrintFunc.") 194 | l.config.PrintFunc = DefaultLoggerPrintFunc 195 | } 196 | 197 | if l.config.LinesBefore < 0 { 198 | neededDoctor = true 199 | logrus.Debugf("LinesBefore is '%d' but should not be <0. Setting to 0.", l.config.LinesBefore) 200 | l.config.LinesBefore = 0 201 | } 202 | 203 | if l.config.LinesAfter < 0 { 204 | neededDoctor = true 205 | logrus.Debugf("LinesAfters is '%d' but should not be <0. Setting to 0.", l.config.LinesAfter) 206 | l.config.LinesAfter = 0 207 | } 208 | 209 | if neededDoctor && !debugMode { 210 | logrus.Warn("errlog: Doctor() has detected and fixed some problems on your logger configuration. It might have modified your configuration. Check logs by enabling debug. 'errlog.SetDebugMode(true)'.") 211 | } 212 | 213 | return 214 | } 215 | 216 | func (l *logger) printStack(stLines []StackTraceItem) { 217 | for i := len(stLines) - 1; i >= 0; i-- { 218 | padding := "" 219 | if !l.config.DisableStackIndentation { 220 | for j := 0; j < len(stLines)-1-i; j++ { 221 | padding += " " 222 | } 223 | } 224 | l.Printf("%s (%s:%d)", stLines[i].CallingObject, stLines[i].SourcePathRef, stLines[i].SourceLineRef) 225 | } 226 | } 227 | 228 | //Printf is the function used to log 229 | func (l *logger) Printf(format string, data ...interface{}) { 230 | l.config.PrintFunc(format, data...) 231 | } 232 | 233 | //Overload adds depths to remove when parsing next stack trace 234 | func (l *logger) Overload(amount int) { 235 | l.stackDepthOverload += amount 236 | } 237 | 238 | func (l *logger) SetConfig(cfg *Config) { 239 | l.config = cfg 240 | l.Doctor() 241 | } 242 | 243 | func (l *logger) Config() *Config { 244 | return l.config 245 | } 246 | 247 | func (l *logger) SetMode(mode int) bool { 248 | if !isIntInSlice(mode, enabledModes) { 249 | return false 250 | } 251 | l.Config().Mode = mode 252 | return true 253 | } 254 | 255 | func (l *logger) Disable(shouldDisable bool) { 256 | if shouldDisable { 257 | l.Config().Mode = ModeDisabled 258 | } else { 259 | l.Config().Mode = ModeEnabled 260 | } 261 | } 262 | 263 | const ( 264 | // ModeDisabled represents the disabled mode (NO-OP) 265 | ModeDisabled = iota + 1 266 | // ModeEnabled represents the enabled mode (Print) 267 | ModeEnabled 268 | ) 269 | 270 | var ( 271 | enabledModes = []int{ModeDisabled, ModeEnabled} 272 | ) 273 | -------------------------------------------------------------------------------- /regexp.go: -------------------------------------------------------------------------------- 1 | package errlog 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "runtime/debug" 7 | "strconv" 8 | "strings" 9 | 10 | "github.com/fatih/color" 11 | "github.com/sirupsen/logrus" 12 | ) 13 | 14 | var ( 15 | /* 16 | Note for contributors/users : these regexp have been made by me, taking my own source code as example for finding the right one to use. 17 | I use gofmt for source code formatting, that means this will work on most cases. 18 | Unfortunately, I didn't check against other code formatting tools, so it may require some evolution. 19 | Feel free to create an issue or send a PR. 20 | */ 21 | regexpParseStack = regexp.MustCompile(`((?:(?:[a-zA-Z._-]+)[/])*(?:[*a-zA-Z0-9_]*\.)+[a-zA-Z0-9_]+)\(((?:(?:0x[0-9a-f]+)|(?:...)[,\s]*)+)*\)[\s]+([/:\-a-zA-Z0-9\._]+)[:]([0-9]+)[\s](?:\+0x([0-9a-f]+))*`) 22 | regexpHexNumber = regexp.MustCompile(`0x[0-9a-f]+`) 23 | regexpFuncLine = regexp.MustCompile(`^func[\s][a-zA-Z0-9]+[(](.*)[)][\s]*{`) 24 | regexpParseDebugLineFindFunc = regexp.MustCompile(`[\.]Debug[\(](.*)[/)]`) 25 | regexpParseDebugLineParseVarName = regexp.MustCompile(`[\.]Debug[\(](.*)\)`) 26 | regexpFindVarDefinition = func(varName string) *regexp.Regexp { 27 | return regexp.MustCompile(fmt.Sprintf(`%s[\s\:]*={1}([\s]*[a-zA-Z0-9\._]+)`, varName)) 28 | } 29 | ) 30 | 31 | // StackTraceItem represents parsed information of a stack trace item 32 | type StackTraceItem struct { 33 | CallingObject string 34 | Args []string 35 | SourcePathRef string 36 | SourceLineRef int 37 | MysteryNumber int64 // don't know what this is, no documentation found, if you know please let me know via a PR ! 38 | } 39 | 40 | func parseStackTrace(deltaDepth int) []StackTraceItem { 41 | fmt.Println(string(debug.Stack())) 42 | return parseAnyStackTrace(string(debug.Stack()), deltaDepth) 43 | } 44 | 45 | func parseAnyStackTrace(stackStr string, deltaDepth int) []StackTraceItem { 46 | stackArr := strings.Split(stackStr, "\n") 47 | if len(stackArr) < 2*(2+deltaDepth) { 48 | return nil 49 | } 50 | stack := strings.Join(stackArr[2*(2+deltaDepth):], "\n") //get stack trace and reduce to desired size 51 | parsedRes := regexpParseStack.FindAllStringSubmatch(stack, -1) 52 | 53 | sti := make([]StackTraceItem, len(parsedRes)) 54 | for i := range parsedRes { 55 | args := regexpHexNumber.FindAllString(parsedRes[i][2], -1) 56 | srcLine, err := strconv.Atoi(parsedRes[i][4]) 57 | if Debug(err) { 58 | srcLine = -1 59 | } 60 | 61 | mysteryNumberStr := parsedRes[i][5] 62 | mysteryNumber := int64(-25) 63 | if mysteryNumberStr != "" { 64 | mysteryNumber, err = strconv.ParseInt(parsedRes[i][5], 16, 32) 65 | if Debug(err) { 66 | mysteryNumber = -1 67 | } 68 | } 69 | 70 | sti[i] = StackTraceItem{ 71 | CallingObject: parsedRes[i][1], 72 | Args: args, 73 | SourcePathRef: parsedRes[i][3], 74 | SourceLineRef: srcLine, 75 | MysteryNumber: mysteryNumber, 76 | } 77 | } 78 | 79 | return sti 80 | } 81 | 82 | //findFuncLine finds line where func is declared 83 | func findFuncLine(lines []string, lineNumber int) int { 84 | for i := lineNumber; i > 0; i-- { 85 | if regexpFuncLine.Match([]byte(lines[i])) { 86 | return i 87 | } 88 | } 89 | 90 | return -1 91 | } 92 | 93 | //findFailingLine finds line where is defined, if Debug() is present on lines[debugLine]. funcLine serves as max 94 | func findFailingLine(lines []string, funcLine int, debugLine int) (failingLineIndex, columnStart, columnEnd int) { 95 | failingLineIndex = -1 //init error flag 96 | 97 | //find var name 98 | reMatches := regexpParseDebugLineParseVarName.FindStringSubmatch(lines[debugLine-1]) 99 | if len(reMatches) < 2 { 100 | return 101 | } 102 | varName := reMatches[1] 103 | 104 | //build regexp for finding var definition 105 | reFindVar := regexpFindVarDefinition(varName) 106 | 107 | //start to search for var definition 108 | for i := debugLine; i >= funcLine && i > 0; i-- { // going reverse from debug line to funcLine 109 | logrus.Debugf("%d: %s", i, lines[i]) // print line for debug 110 | 111 | // early skipping some cases 112 | if strings.Trim(lines[i], " \n\t") == "" { // skip if line is blank 113 | logrus.Debugf(color.BlueString("%d: ignoring blank line", i)) 114 | continue 115 | } else if len(lines[i]) >= 2 && lines[i][:2] == "//" { // skip if line is a comment line (note: comments of type '/*' can be stopped inline and code may be placed after it, therefore we should pass line if '/*' starts the line) 116 | logrus.Debugf(color.BlueString("%d: ignoring comment line", i)) 117 | continue 118 | } 119 | 120 | //search for var definition 121 | index := reFindVar.FindStringSubmatchIndex(lines[i]) 122 | if index == nil { //if not found, continue searching with next line 123 | logrus.Debugf(color.BlueString("%d: var definition not found for '%s' (regexp no match).", i, varName)) 124 | continue 125 | } 126 | // At that point we found our definition 127 | 128 | failingLineIndex = i //store the ressult 129 | columnStart = index[0] //store columnStart 130 | 131 | //now lets walk to columnEnd (because regexp is really bad at doing this) 132 | //for this purpose, we count brackets from first opening, and stop when openedBrackets == closedBrackets 133 | openedBrackets, closedBrackets := 0, 0 134 | for j := index[1]; j < len(lines[i]); j++ { 135 | if lines[i][j] == '(' { 136 | openedBrackets++ 137 | } else if lines[i][j] == ')' { 138 | closedBrackets++ 139 | } 140 | if openedBrackets == closedBrackets { // that means every opened brackets are now closed (the first/last one is the one from the func call) 141 | columnEnd = j // so we found our column end 142 | return // so return the result 143 | } 144 | } 145 | 146 | if columnEnd == 0 { //columnEnd was not found 147 | logrus.Debugf("Fixing value of columnEnd (0). Defaulting to end of failing line.") 148 | columnEnd = len(lines[i]) - 1 149 | } 150 | return 151 | } 152 | 153 | return 154 | } 155 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package errlog 2 | 3 | import "strings" 4 | 5 | func isIntInSlice(i int, s []int) bool { 6 | for vi := range s { 7 | if s[vi] == i { 8 | return true 9 | } 10 | } 11 | return false 12 | } 13 | 14 | //deleteBlankLinesFromRange increments and decrements respectively start and end so they are not representing an empty line (in slice lines) 15 | func deleteBlankLinesFromRange(lines []string, start, end *int) { 16 | //clean from out of range values 17 | (*start) = max(*start, 0) 18 | (*end) = min(*end, len(lines)-1) 19 | 20 | //clean leading blank lines 21 | for (*start) <= (*end) { 22 | if strings.Trim(lines[(*start)], " \n\t") != "" { 23 | break 24 | } 25 | (*start)++ 26 | } 27 | 28 | //clean trailing blank lines 29 | for (*end) >= (*start) { 30 | if strings.Trim(lines[(*end)], " \n\t") != "" { 31 | break 32 | } 33 | (*end)-- 34 | } 35 | } 36 | 37 | func max(a, b int) int { 38 | if a > b { 39 | return a 40 | } 41 | return b 42 | } 43 | 44 | func min(a, b int) int { 45 | if a < b { 46 | return a 47 | } 48 | return b 49 | } 50 | --------------------------------------------------------------------------------