github.com/mailgun/holster/v4@v4.20.0/errors/README.md (about) 1 # Errors 2 Package is a fork of [https://github.com/pkg/errors](https://github.com/pkg/errors) with additional 3 functions for improving the relationship between structured logging and error handling in go. 4 5 ## Adding structured context to an error 6 Wraps the original error while providing structured context data 7 ```go 8 _, err := ioutil.ReadFile(fileName) 9 if err != nil { 10 return errors.WithContext{"file": fileName}.Wrap(err, "read failed") 11 } 12 ``` 13 14 ## Retrieving the structured context 15 Using `errors.WithContext{}` stores the provided context for later retrieval by upstream code or structured logging 16 systems 17 ```go 18 // Pass to logrus as structured logging 19 logrus.WithFields(errors.ToLogrus(err)).Error("open file error") 20 ``` 21 Stack information on the source of the error is also included 22 ```go 23 context := errors.ToMap(err) 24 context == map[string]interface{}{ 25 "file": "my-file.txt", 26 "go-func": "loadFile()", 27 "go-line": 146, 28 "go-file": "with_context_example.go" 29 } 30 ``` 31 32 ## Conforms to the `Causer` interface 33 Errors wrapped with `errors.WithContext{}` are compatible with errors wrapped by `github.com/pkg/errors` 34 ```go 35 switch err := errors.Cause(err).(type) { 36 case *MyError: 37 // handle specifically 38 default: 39 // unknown error 40 } 41 ``` 42 43 ## Proper Usage 44 The context wrapped by `errors.WithContext{}` is not intended to be used to by code to decide how an error should be 45 handled. It is a convenience where the failure is well known, but the context is dynamic. In other words, you know the 46 database returned an unrecoverable query error, but creating a new error type with the details of each query 47 error is overkill **ErrorFetchPage{}, ErrorFetchAll{}, ErrorFetchAuthor{}, etc...** 48 49 As an example 50 ```go 51 func (r *Repository) FetchAuthor(isbn string) (Author, error) { 52 // Returns ErrorNotFound{} if not exist 53 book, err := r.fetchBook(isbn) 54 if err != nil { 55 return nil, errors.WithContext{"isbn": isbn}.Wrap(err, "while fetching book") 56 } 57 // Returns ErrorNotFound{} if not exist 58 author, err := r.fetchAuthorByBook(book) 59 if err != nil { 60 return nil, errors.WithContext{"book": book}.Wrap(err, "while fetching author") 61 } 62 return author, nil 63 } 64 ``` 65 66 You should continue to create and inspect error types 67 ```go 68 type ErrorAuthorNotFound struct {} 69 70 func isNotFound(err error) { 71 _, ok := err.(*ErrorAuthorNotFound) 72 return ok 73 } 74 75 func main() { 76 r := Repository{} 77 author, err := r.FetchAuthor("isbn-213f-23422f52356") 78 if err != nil { 79 // Fetch the original Cause() and determine if the error is recoverable 80 if isNotFound(error.Cause(err)) { 81 author, err := r.AddBook("isbn-213f-23422f52356", "charles", "darwin") 82 } 83 if err != nil { 84 logrus.WithFields(errors.ToLogrus(err)).Errorf("while fetching author - %s", err) 85 os.Exit(1) 86 } 87 } 88 fmt.Printf("Author %+v\n", author) 89 } 90 ``` 91 92 ## Context for concrete error types 93 If the error implements the `errors.HasContext` interface the context can be retrieved 94 ```go 95 context, ok := err.(errors.HasContext) 96 if ok { 97 fmt.Println(context.Context()) 98 } 99 ``` 100 101 This makes it easy for error types to provide their context information. 102 ```go 103 type ErrorBookNotFound struct { 104 ISBN string 105 } 106 // Implements the `HasContext` interface 107 func (e *ErrorBookNotFound) func Context() map[string]interface{} { 108 return map[string]interface{}{ 109 "isbn": e.ISBN, 110 } 111 } 112 ``` 113 Now we can create the error and logrus knows how to retrieve the context 114 115 ```go 116 func (* Repository) FetchBook(isbn string) (*Book, error) { 117 var book Book 118 err := r.db.Query("SELECT * FROM books WHERE isbn = ?").One(&book) 119 if err != nil { 120 return nil, ErrorBookNotFound{ISBN: isbn} 121 } 122 } 123 124 func main() { 125 r := Repository{} 126 book, err := r.FetchBook("isbn-213f-23422f52356") 127 if err != nil { 128 logrus.WithFields(errors.ToLogrus(err)).Errorf("while fetching book - %s", err) 129 os.Exit(1) 130 } 131 fmt.Printf("Book %+v\n", book) 132 } 133 ``` 134 135 136 ## A Complete example 137 The following is a complete example using 138 http://github.com/mailgun/logrus-hooks/kafkahook to marshal the context into ES 139 fields. 140 141 ```go 142 package main 143 144 import ( 145 "log" 146 "io/ioutil" 147 148 "github.com/mailgun/holster/v4/errors" 149 "github.com/mailgun/logrus-hooks/kafkahook" 150 "github.com/sirupsen/logrus" 151 ) 152 153 func OpenWithError(fileName string) error { 154 _, err := ioutil.ReadFile(fileName) 155 if err != nil { 156 // pass the filename up via the error context 157 return errors.WithContext{ 158 "file": fileName, 159 }.Wrap(err, "read failed") 160 } 161 return nil 162 } 163 164 func main() { 165 // Init the kafka hook logger 166 hook, err := kafkahook.New(kafkahook.Config{ 167 Endpoints: []string{"kafka-n01", "kafka-n02"}, 168 Topic: "udplog", 169 }) 170 if err != nil { 171 log.Fatal(err) 172 } 173 174 // Add the hook to logrus 175 logrus.AddHook(hook) 176 177 // Create an error and log it 178 if err := OpenWithError("/tmp/non-existant.file"); err != nil { 179 // This log line will show up in ES with the additional fields 180 // 181 // excText: "read failed" 182 // excValue: "read failed: open /tmp/non-existant.file: no such file or directory" 183 // excType: "*errors.WithContext" 184 // filename: "/src/to/main.go" 185 // funcName: "main()" 186 // lineno: 25 187 // context.file: "/tmp/non-existant.file" 188 // context.domain.id: "some-id" 189 // context.foo: "bar" 190 logrus.WithFields(logrus.Fields{ 191 "domain.id": "some-id", 192 "foo": "bar", 193 "err": err, 194 }).Error("log messge") 195 } 196 } 197 ```