├── .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 [](https://goreportcard.com/report/github.com/snwfdhmp/errlog) [](https://github.com/sindresorhus/awesome) [](http://godoc.org/github.com/snwfdhmp/errlog) [](https://github.com/snwfdhmp/errlog/issues) [](https://github.com/snwfdhmp/errlog/LICENSE)
2 |
3 | 
4 |
5 |
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 | 
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 | 
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 | 
176 |
177 | ## Documentation
178 |
179 | Documentation can be found here : [](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 | [](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 |
--------------------------------------------------------------------------------