├── .github └── workflows │ └── ci.yaml ├── .gitignore ├── LICENSE ├── Makefile.update-protos ├── README.md ├── assert ├── assert.go └── assert_test.go ├── assert_api.go ├── barriers ├── barriers.go └── barriers_test.go ├── barriers_api.go ├── contexttags ├── contexttags.go ├── contexttags_test.go └── with_context.go ├── contexttags_api.go ├── domains ├── domains.go ├── domains_test.go ├── internal │ └── dummy_domain.go └── with_domain.go ├── domains_api.go ├── errbase ├── adapters.go ├── adapters_errno.go ├── adapters_errno_test.go ├── adapters_test.go ├── decode.go ├── encode.go ├── encode_test.go ├── err_string_ownership_test.go ├── format_error.go ├── format_error_internal_test.go ├── formatter.go ├── internal │ ├── unknown.pb.go │ └── unknown.proto ├── migrations.go ├── migrations_test.go ├── opaque.go ├── opaque_test.go ├── oserror_go116.go ├── oserror_pre116.go ├── safe_details.go ├── stack_format_test.go ├── unknown_type_test.go ├── unwrap.go └── unwrap_test.go ├── errbase_api.go ├── errorspb ├── errors.go ├── errors.pb.go ├── errors.proto ├── hintdetail.pb.go ├── hintdetail.proto ├── markers.go ├── markers.pb.go ├── markers.proto ├── tags.pb.go ├── tags.proto ├── testing.go ├── testing.pb.go └── testing.proto ├── errutil ├── as.go ├── as_test.go ├── assertions.go ├── doc.go ├── format_error_special.go ├── message.go ├── message_test.go ├── redactable.go └── utilities.go ├── errutil_api.go ├── errutil_api_test.go ├── extgrpc ├── ext_grpc.go ├── ext_grpc.pb.go ├── ext_grpc.proto └── ext_grpc_test.go ├── exthttp ├── ext_http.go ├── ext_http.pb.go ├── ext_http.proto └── ext_http_test.go ├── fmttests ├── datadriven_test.go ├── datadriven_test_go116.go ├── datadriven_test_pre116.go ├── format_error_test.go └── testdata │ └── format │ ├── leaves │ ├── leaves-via-network │ ├── opaque │ ├── wrap-fmt │ ├── wrap-fmt-via-network │ ├── wrap-goerr │ ├── wrap-goerr-via-network │ ├── wrap-newf │ ├── wrap-newf-via-network │ ├── wrap-nofmt │ ├── wrap-nofmt-via-network │ ├── wrap-pkgerr │ └── wrap-pkgerr-via-network ├── go.mod ├── go.sum ├── grpc ├── client_test.go ├── echoer.pb.go ├── echoer.proto ├── main_test.go ├── middleware │ ├── client.go │ └── server.go ├── server_test.go └── status │ └── status.go ├── hintdetail ├── hintdetail.go ├── hintdetail_test.go ├── with_detail.go └── with_hint.go ├── hintdetail_api.go ├── issuelink ├── issuelink.go ├── issuelink_test.go ├── unimplemented_error.go ├── unimplemented_test.go └── with_issuelink.go ├── issuelink_api.go ├── join ├── join.go └── join_test.go ├── markers ├── example_has_type_test.go ├── internal │ ├── unknown.pb.go │ └── unknown.proto ├── markers.go └── markers_test.go ├── markers_api.go ├── oserror ├── oserror.go ├── oserror_test.go └── oserror_unix_test.go ├── report ├── report.go ├── report_test.go └── reportables.go ├── report_api.go ├── safedetails ├── redact.go ├── redact_test.go ├── safedetails.go ├── safedetails_test.go └── with_safedetails.go ├── safedetails_api.go ├── secondary ├── secondary.go ├── secondary_test.go └── with_secondary.go ├── secondary_api.go ├── stdstrings └── strings.go ├── telemetrykeys ├── telemetrykeys.go ├── telemetrykeys_test.go └── with_telemetry.go ├── telemetrykeys_api.go ├── testutils ├── simplecheck.go └── simplecheck_test.go ├── withstack ├── internal │ └── run.go ├── one_line_source.go ├── one_line_source_test.go ├── reportable.go ├── reportable_test.go ├── stack.go └── withstack.go └── withstack_api.go /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | go: 15 | - "1.23" 16 | - "1.24" 17 | steps: 18 | - uses: actions/checkout@v3 19 | 20 | - name: Set up Go (${{ matrix.go }} 21 | uses: actions/setup-go@v4 22 | with: 23 | go-version: ${{ matrix.go }} 24 | 25 | - name: Build (${{ matrix.go }}) 26 | run: go build ./... 27 | build-and-test: 28 | runs-on: ubuntu-latest 29 | strategy: 30 | matrix: 31 | go: 32 | - "1.23" 33 | - "1.24" 34 | steps: 35 | - uses: actions/checkout@v3 36 | 37 | - name: Set up Go (${{ matrix.go }} 38 | uses: actions/setup-go@v4 39 | with: 40 | go-version: ${{ matrix.go }} 41 | 42 | - name: Build (${{ matrix.go }}) 43 | run: go build ./... 44 | 45 | - name: Test (${{ matrix.go }}) 46 | run: go test ./... 47 | 48 | - name: Tidy (${{ matrix.go }}) 49 | run: go mod tidy 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | -------------------------------------------------------------------------------- /Makefile.update-protos: -------------------------------------------------------------------------------- 1 | # This makefile can be used to-regenerate the protobuf files. 2 | # 3 | # Prerequisites: 4 | # "protoc" from https://github.com/protocolbuffers/protobuf 5 | # go get github.com/cockroachdb/protoc-gen-gogoroach 6 | # go get github.com/gogo/protobuf/types 7 | # go get github.com/gogo/protobuf/protoc-gen-gogo 8 | # 9 | # Note: as of 2021-04-13, we like to use a custom protoc-gen-gogo 10 | # with additional options, to stabilize the marshalled 11 | # encoding of objects (so that they are deterministic 12 | # across marshal/unmarshal cycles) and reduce the memory footprint 13 | # of objects: 14 | # 15 | # vanity.TurnOnStable_MarshalerAll, 16 | # vanity.TurnOffGoUnrecognizedAll, 17 | # vanity.TurnOffGoUnkeyedAll, 18 | # vanity.TurnOffGoSizecacheAll, 19 | # 20 | # Until this is resolved, the "go get" commands above are not 21 | # adequate; instead: 22 | # 23 | # 1. set the PATH env var to point to CockroachDB's `bin` 24 | # sub-directory (after a successful CockroachDB build), where a 25 | # suitable version of protoc-gen-gogoroach is provided. 26 | # 27 | # 2. run `make -f Makefile.update-protos` with this PATH active. 28 | 29 | export SHELL := env PWD=$(CURDIR) bash 30 | 31 | PROTOS := $(wildcard \ 32 | errbase/internal/*.proto \ 33 | errorspb/*.proto \ 34 | extgrpc/*.proto \ 35 | exthttp/*.proto \ 36 | grpc/*.proto \ 37 | markers/internal/*.proto \ 38 | ) 39 | GO_SOURCES = $(PROTOS:.proto=.pb.go) 40 | 41 | SED = sed 42 | SED_INPLACE := $(shell $(SED) --version 2>&1 | grep -q GNU && echo -i || echo "-i ''") 43 | 44 | all: $(PROTOS) 45 | set -e; for dir in $(sort $(dir $(PROTOS))); do \ 46 | protoc \ 47 | -I. \ 48 | -I$$GOPATH/src/ \ 49 | -I$$GOPATH/src/github.com \ 50 | -I$$GOPATH/src/github.com/cockroachdb/errors \ 51 | -I$$GOPATH/src/github.com/gogo/protobuf \ 52 | -I$$GOPATH/src/github.com/gogo/protobuf/protobuf \ 53 | --gogoroach_out=Mgoogle/protobuf/any.proto=github.com/gogo/protobuf/types,plugins=grpc,import_prefix=:. \ 54 | $$dir/*.proto; \ 55 | done 56 | $(SED) $(SED_INPLACE) -E \ 57 | -e '/import _ /d' \ 58 | -e 's!import (fmt|math) "github.com/(fmt|math)"! !g' \ 59 | -e 's!github.com/((bytes|encoding/binary|errors|fmt|io|math|github\.com|(google\.)?golang\.org)([^a-z]|$$))!\1!g' \ 60 | -e 's!golang.org/x/net/context!context!g' \ 61 | $(GO_SOURCES) 62 | gofmt -s -w $(GO_SOURCES) 63 | -------------------------------------------------------------------------------- /assert/assert.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | package assert 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | 21 | "github.com/cockroachdb/errors/errbase" 22 | "github.com/cockroachdb/errors/markers" 23 | "github.com/cockroachdb/errors/stdstrings" 24 | "github.com/gogo/protobuf/proto" 25 | ) 26 | 27 | // WithAssertionFailure decorates the error with an assertion failure marker. 28 | // This is not intended to be used directly (see AssertionFailed() for 29 | // further decoration). 30 | // 31 | // Detail is shown: 32 | // - when formatting with `%+v`. 33 | // - in Sentry reports. 34 | func WithAssertionFailure(err error) error { 35 | if err == nil { 36 | return nil 37 | } 38 | return &withAssertionFailure{cause: err} 39 | } 40 | 41 | // HasAssertionFailure returns true if the error or any of its causes 42 | // is an assertion failure annotation. 43 | func HasAssertionFailure(err error) bool { 44 | _, ok := markers.If(err, func(err error) (v interface{}, ok bool) { 45 | v, ok = err.(*withAssertionFailure) 46 | return 47 | }) 48 | return ok 49 | } 50 | 51 | // IsAssertionFailure returns true if the error (not its causes) is an 52 | // assertion failure annotation. Consider using markers.If or 53 | // HasAssertionFailure to test both the error and its causes. 54 | func IsAssertionFailure(err error) bool { 55 | _, ok := err.(*withAssertionFailure) 56 | return ok 57 | } 58 | 59 | type withAssertionFailure struct { 60 | cause error 61 | } 62 | 63 | var _ error = (*withAssertionFailure)(nil) 64 | var _ fmt.Formatter = (*withAssertionFailure)(nil) 65 | var _ errbase.SafeFormatter = (*withAssertionFailure)(nil) 66 | 67 | // ErrorHint implements the hintdetail.ErrorHinter interface. 68 | func (w *withAssertionFailure) ErrorHint() string { 69 | return AssertionErrorHint + stdstrings.IssueReferral 70 | } 71 | 72 | // AssertionErrorHint is the hint emitted upon assertion failures. 73 | const AssertionErrorHint = `You have encountered an unexpected error.` 74 | 75 | func (w *withAssertionFailure) Error() string { return w.cause.Error() } 76 | func (w *withAssertionFailure) Cause() error { return w.cause } 77 | func (w *withAssertionFailure) Unwrap() error { return w.cause } 78 | 79 | func (w *withAssertionFailure) Format(s fmt.State, verb rune) { errbase.FormatError(w, s, verb) } 80 | func (w *withAssertionFailure) SafeFormatError(p errbase.Printer) error { 81 | if p.Detail() { 82 | p.Printf("assertion failure") 83 | } 84 | return w.cause 85 | } 86 | 87 | func decodeAssertFailure( 88 | _ context.Context, cause error, _ string, _ []string, _ proto.Message, 89 | ) error { 90 | return &withAssertionFailure{cause: cause} 91 | } 92 | 93 | func init() { 94 | errbase.RegisterWrapperDecoder(errbase.GetTypeKey((*withAssertionFailure)(nil)), decodeAssertFailure) 95 | } 96 | -------------------------------------------------------------------------------- /assert/assert_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | package assert_test 16 | 17 | import ( 18 | "context" 19 | goErr "errors" 20 | "fmt" 21 | "strings" 22 | "testing" 23 | 24 | "github.com/cockroachdb/errors/assert" 25 | "github.com/cockroachdb/errors/errbase" 26 | "github.com/cockroachdb/errors/markers" 27 | "github.com/cockroachdb/errors/testutils" 28 | "github.com/pkg/errors" 29 | ) 30 | 31 | func TestAssert(t *testing.T) { 32 | tt := testutils.T{T: t} 33 | 34 | baseErr := errors.New("world") 35 | err := errors.Wrap(assert.WithAssertionFailure(baseErr), "hello") 36 | 37 | tt.Check(markers.Is(err, baseErr)) 38 | 39 | tt.Check(assert.HasAssertionFailure(err)) 40 | 41 | if _, ok := markers.If(err, func(err error) (interface{}, bool) { return nil, assert.IsAssertionFailure(err) }); !ok { 42 | t.Error("woops") 43 | } 44 | 45 | tt.CheckEqual(err.Error(), "hello: world") 46 | 47 | enc := errbase.EncodeError(context.Background(), err) 48 | newErr := errbase.DecodeError(context.Background(), enc) 49 | 50 | tt.Check(markers.Is(newErr, baseErr)) 51 | 52 | tt.Check(assert.HasAssertionFailure(newErr)) 53 | 54 | if _, ok := markers.If(newErr, func(err error) (interface{}, bool) { return nil, assert.IsAssertionFailure(err) }); !ok { 55 | t.Error("woops") 56 | } 57 | 58 | tt.CheckEqual(newErr.Error(), "hello: world") 59 | } 60 | 61 | func TestFormat(t *testing.T) { 62 | tt := testutils.T{t} 63 | 64 | baseErr := goErr.New("woo") 65 | const woo = `woo` 66 | const waawoo = `waa: woo` 67 | testCases := []struct { 68 | name string 69 | err error 70 | expFmtSimple string 71 | expFmtVerbose string 72 | }{ 73 | {"assert", 74 | assert.WithAssertionFailure(baseErr), 75 | woo, ` 76 | woo 77 | (1) assertion failure 78 | Wraps: (2) woo 79 | Error types: (1) *assert.withAssertionFailure (2) *errors.errorString`}, 80 | 81 | {"assert + wrapper", 82 | assert.WithAssertionFailure(&werrFmt{baseErr, "waa"}), 83 | waawoo, ` 84 | waa: woo 85 | (1) assertion failure 86 | Wraps: (2) waa 87 | | -- this is waa's 88 | | multi-line payload 89 | Wraps: (3) woo 90 | Error types: (1) *assert.withAssertionFailure (2) *assert_test.werrFmt (3) *errors.errorString`}, 91 | 92 | {"wrapper + assert", 93 | &werrFmt{assert.WithAssertionFailure(baseErr), "waa"}, 94 | waawoo, ` 95 | waa: woo 96 | (1) waa 97 | | -- this is waa's 98 | | multi-line payload 99 | Wraps: (2) assertion failure 100 | Wraps: (3) woo 101 | Error types: (1) *assert_test.werrFmt (2) *assert.withAssertionFailure (3) *errors.errorString`}, 102 | } 103 | 104 | for _, test := range testCases { 105 | tt.Run(test.name, func(tt testutils.T) { 106 | err := test.err 107 | 108 | // %s is simple formatting 109 | tt.CheckStringEqual(fmt.Sprintf("%s", err), test.expFmtSimple) 110 | 111 | // %v is simple formatting too, for compatibility with the past. 112 | tt.CheckStringEqual(fmt.Sprintf("%v", err), test.expFmtSimple) 113 | 114 | // %q is always like %s but quotes the result. 115 | ref := fmt.Sprintf("%q", test.expFmtSimple) 116 | tt.CheckStringEqual(fmt.Sprintf("%q", err), ref) 117 | 118 | // %+v is the verbose mode. 119 | refV := strings.TrimPrefix(test.expFmtVerbose, "\n") 120 | spv := fmt.Sprintf("%+v", err) 121 | tt.CheckStringEqual(spv, refV) 122 | }) 123 | } 124 | } 125 | 126 | type werrFmt struct { 127 | cause error 128 | msg string 129 | } 130 | 131 | var _ errbase.Formatter = (*werrFmt)(nil) 132 | 133 | func (e *werrFmt) Error() string { return fmt.Sprintf("%s: %v", e.msg, e.cause) } 134 | func (e *werrFmt) Unwrap() error { return e.cause } 135 | func (e *werrFmt) Format(s fmt.State, verb rune) { errbase.FormatError(e, s, verb) } 136 | func (e *werrFmt) FormatError(p errbase.Printer) error { 137 | p.Print(e.msg) 138 | if p.Detail() { 139 | p.Printf("-- this is %s's\nmulti-line payload", e.msg) 140 | } 141 | return e.cause 142 | } 143 | -------------------------------------------------------------------------------- /assert_api.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | package errors 16 | 17 | import "github.com/cockroachdb/errors/assert" 18 | 19 | // WithAssertionFailure decorates the error with an assertion failure marker. 20 | // This is not intended to be used directly (see AssertionFailed() for 21 | // further decoration). 22 | // 23 | // Detail is shown: 24 | // - when formatting with `%+v`. 25 | // - in Sentry reports. 26 | func WithAssertionFailure(err error) error { return assert.WithAssertionFailure(err) } 27 | 28 | // HasAssertionFailure returns true if the error or any of its causes 29 | // is an assertion failure annotation. 30 | func HasAssertionFailure(err error) bool { return assert.HasAssertionFailure(err) } 31 | 32 | // IsAssertionFailure returns true if the error (not its causes) is an 33 | // assertion failure annotation. Consider using markers.If or 34 | // HasAssertionFailure to test both the error and its causes. 35 | func IsAssertionFailure(err error) bool { return assert.IsAssertionFailure(err) } 36 | -------------------------------------------------------------------------------- /barriers_api.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | package errors 16 | 17 | import "github.com/cockroachdb/errors/barriers" 18 | 19 | // Handled swallows the provided error and hides it from the 20 | // Cause()/Unwrap() interface, and thus the Is() facility that 21 | // identifies causes. However, it retains it for the purpose of 22 | // printing the error out (e.g. for troubleshooting). The error 23 | // message is preserved in full. 24 | // 25 | // Detail is shown: 26 | // - via `errors.GetSafeDetails()`, shows details from hidden error. 27 | // - when formatting with `%+v`. 28 | // - in Sentry reports. 29 | func Handled(err error) error { return barriers.Handled(err) } 30 | 31 | // HandledWithMessage is like Handled except the message is overridden. 32 | // This can be used e.g. to hide message details or to prevent 33 | // downstream code to make assertions on the message's contents. 34 | func HandledWithMessage(err error, msg string) error { return barriers.HandledWithMessage(err, msg) } 35 | -------------------------------------------------------------------------------- /contexttags/contexttags.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | package contexttags 16 | 17 | import ( 18 | "context" 19 | 20 | "github.com/cockroachdb/errors/errbase" 21 | "github.com/cockroachdb/logtags" 22 | "github.com/cockroachdb/redact" 23 | ) 24 | 25 | // WithContextTags captures the k/v pairs stored in the context via the 26 | // `logtags` package and annotates them on the error. 27 | // 28 | // Only the strong representation of values remains available. This is 29 | // because the library cannot guarantee that the underlying value is 30 | // preserved across the network. To avoid creating a stateful interface 31 | // (where the user code needs to know whether an error has traveled 32 | // through the network or not), the library restricts access to the 33 | // value part as strings. See GetContextTags() below. 34 | // 35 | // Detail is shown: 36 | // - via `errors.GetSafeDetails()`. 37 | // - via `GetContextTags()` below. 38 | // - when formatting with `%+v`. 39 | // - in Sentry reports. 40 | func WithContextTags(err error, ctx context.Context) error { 41 | if err == nil { 42 | return nil 43 | } 44 | tags := logtags.FromContext(ctx) 45 | if tags == nil { 46 | return err 47 | } 48 | return &withContext{cause: err, tags: tags} 49 | } 50 | 51 | // GetContextTags retrieves the k/v pairs stored in the error. 52 | // The sets are returned from outermost to innermost level of cause. 53 | // The returned logtags.Buffer only know about the string 54 | // representation of the values originally captured by the error. 55 | func GetContextTags(err error) (res []*logtags.Buffer) { 56 | for e := err; e != nil; e = errbase.UnwrapOnce(e) { 57 | if w, ok := e.(*withContext); ok { 58 | b := w.tags 59 | // Ensure that the buffer does not contain any non-string. 60 | if hasNonStringValue(b) { 61 | b = convertToStringsOnly(b) 62 | } 63 | res = append(res, b) 64 | } 65 | } 66 | return res 67 | } 68 | 69 | func hasNonStringValue(b *logtags.Buffer) bool { 70 | for _, t := range b.Get() { 71 | v := t.Value() 72 | if v == nil { 73 | return true 74 | } 75 | if _, ok := v.(string); !ok { 76 | return true 77 | } 78 | } 79 | return false 80 | } 81 | 82 | func convertToStringsOnly(b *logtags.Buffer) (res *logtags.Buffer) { 83 | for _, t := range b.Get() { 84 | res = res.Add(t.Key(), t.ValueStr()) 85 | } 86 | return res 87 | } 88 | 89 | func redactTags(b *logtags.Buffer) []string { 90 | res := make([]string, len(b.Get())) 91 | redactableTagsIterate(b, func(i int, r redact.RedactableString) { 92 | res[i] = r.Redact().StripMarkers() 93 | }) 94 | return res 95 | } 96 | 97 | func redactableTagsIterate(b *logtags.Buffer, fn func(i int, s redact.RedactableString)) { 98 | var empty redact.SafeString 99 | for i, t := range b.Get() { 100 | k := t.Key() 101 | v := t.Value() 102 | eq := empty 103 | var val interface{} = empty 104 | if v != nil { 105 | if len(k) > 1 { 106 | eq = "=" 107 | } 108 | val = v 109 | } 110 | res := redact.Sprintf("%s%s%v", redact.Safe(k), eq, val) 111 | fn(i, res) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /contexttags/with_context.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | package contexttags 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | 21 | "github.com/cockroachdb/errors/errbase" 22 | "github.com/cockroachdb/errors/errorspb" 23 | "github.com/cockroachdb/logtags" 24 | "github.com/cockroachdb/redact" 25 | "github.com/gogo/protobuf/proto" 26 | ) 27 | 28 | type withContext struct { 29 | cause error 30 | // tags stores the context k/v pairs, non-redacted. 31 | // The errors library only gives access to the string representation 32 | // of the value part. This is because the network encoding of 33 | // a withContext instance only stores the string. 34 | tags *logtags.Buffer 35 | // redactedTags stores the context k/v pairs, redacted. 36 | // When this is defined, SafeDetails() uses it. Otherwise, it 37 | // re-redact tags above. 38 | redactedTags []string 39 | } 40 | 41 | var _ error = (*withContext)(nil) 42 | var _ errbase.SafeDetailer = (*withContext)(nil) 43 | var _ errbase.SafeFormatter = (*withContext)(nil) 44 | var _ fmt.Formatter = (*withContext)(nil) 45 | 46 | // withContext is an error. The original error message is preserved. 47 | func (w *withContext) Error() string { return w.cause.Error() } 48 | 49 | // the cause is reachable. 50 | func (w *withContext) Cause() error { return w.cause } 51 | func (w *withContext) Unwrap() error { return w.cause } 52 | 53 | // Printing a withContext reveals the tags. 54 | func (w *withContext) Format(s fmt.State, verb rune) { errbase.FormatError(w, s, verb) } 55 | 56 | func (w *withContext) SafeFormatError(p errbase.Printer) error { 57 | if p.Detail() && w.tags != nil { 58 | p.Printf("tags: [") 59 | redactableTagsIterate(w.tags, func(i int, r redact.RedactableString) { 60 | if i > 0 { 61 | p.Printf(",") 62 | } 63 | p.Print(r) 64 | }) 65 | p.Printf("]") 66 | } 67 | return w.cause 68 | } 69 | 70 | // SafeDetails implements the errbase.SafeDetailer interface. 71 | func (w *withContext) SafeDetails() []string { 72 | if w.redactedTags != nil { 73 | return w.redactedTags 74 | } 75 | return redactTags(w.tags) 76 | } 77 | 78 | func encodeWithContext(_ context.Context, err error) (string, []string, proto.Message) { 79 | w := err.(*withContext) 80 | p := &errorspb.TagsPayload{} 81 | for _, t := range w.tags.Get() { 82 | p.Tags = append(p.Tags, errorspb.TagPayload{Tag: t.Key(), Value: t.ValueStr()}) 83 | } 84 | return "", w.SafeDetails(), p 85 | } 86 | 87 | func decodeWithContext( 88 | _ context.Context, cause error, _ string, redactedTags []string, payload proto.Message, 89 | ) error { 90 | m, ok := payload.(*errorspb.TagsPayload) 91 | if !ok { 92 | // If this ever happens, this means some version of the library 93 | // (presumably future) changed the payload type, and we're 94 | // receiving this here. In this case, give up and let 95 | // DecodeError use the opaque type. 96 | return nil 97 | } 98 | if len(m.Tags) == 0 && len(redactedTags) == 0 { 99 | // There are no tags stored. Either there are no tags stored, or 100 | // we received some new version of the protobuf message which does 101 | // things differently. Again, use the opaque type. 102 | return nil 103 | } 104 | // Convert the k/v pairs. 105 | var b *logtags.Buffer 106 | for _, t := range m.Tags { 107 | b = b.Add(t.Tag, t.Value) 108 | } 109 | return &withContext{cause: cause, tags: b, redactedTags: redactedTags} 110 | } 111 | 112 | func init() { 113 | errbase.RegisterWrapperEncoder(errbase.GetTypeKey((*withContext)(nil)), encodeWithContext) 114 | errbase.RegisterWrapperDecoder(errbase.GetTypeKey((*withContext)(nil)), decodeWithContext) 115 | } 116 | -------------------------------------------------------------------------------- /contexttags_api.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | package errors 16 | 17 | import ( 18 | "context" 19 | 20 | "github.com/cockroachdb/errors/contexttags" 21 | "github.com/cockroachdb/logtags" 22 | ) 23 | 24 | // WithContextTags captures the k/v pairs stored in the context via the 25 | // `logtags` package and annotates them on the error. 26 | // 27 | // Only the strong representation of values remains available. This is 28 | // because the library cannot guarantee that the underlying value is 29 | // preserved across the network. To avoid creating a stateful interface 30 | // (where the user code needs to know whether an error has traveled 31 | // through the network or not), the library restricts access to the 32 | // value part as strings. See GetContextTags() below. 33 | // 34 | // Detail is shown: 35 | // - via `errors.GetSafeDetails()`. 36 | // - via `GetContextTags()` below. 37 | // - when formatting with `%+v`. 38 | // - in Sentry reports. 39 | func WithContextTags(err error, ctx context.Context) error { 40 | return contexttags.WithContextTags(err, ctx) 41 | } 42 | 43 | // GetContextTags retrieves the k/v pairs stored in the error. 44 | // The sets are returned from outermost to innermost level of cause. 45 | // The returned logtags.Buffer only know about the string 46 | // representation of the values originally captured by the error. 47 | func GetContextTags(err error) []*logtags.Buffer { return contexttags.GetContextTags(err) } 48 | -------------------------------------------------------------------------------- /domains/domains.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | package domains 16 | 17 | import ( 18 | "errors" 19 | "fmt" 20 | "path/filepath" 21 | "runtime" 22 | 23 | "github.com/cockroachdb/errors/barriers" 24 | "github.com/cockroachdb/errors/errbase" 25 | ) 26 | 27 | // Domain is the type of a domain annotation. 28 | type Domain string 29 | 30 | // NoDomain is the domain of errors that don't originate 31 | // from a barrier. 32 | const NoDomain Domain = "error domain: " 33 | 34 | // GetDomain extracts the domain of the given error, or NoDomain if 35 | // the error's cause does not have a domain annotation. 36 | func GetDomain(err error) Domain { 37 | for { 38 | if b, ok := err.(*withDomain); ok { 39 | return b.domain 40 | } 41 | // Recurse to the cause. 42 | if c := errbase.UnwrapOnce(err); c != nil { 43 | err = c 44 | continue 45 | } 46 | break 47 | } 48 | return NoDomain 49 | } 50 | 51 | // WithDomain wraps an error so that it appears to come from the given domain. 52 | // 53 | // Domain is shown: 54 | // - via `errors.GetSafeDetails()`. 55 | // - when formatting with `%+v`. 56 | // - in Sentry reports. 57 | func WithDomain(err error, domain Domain) error { 58 | if err == nil { 59 | return nil 60 | } 61 | return &withDomain{cause: err, domain: domain} 62 | } 63 | 64 | // New creates an error in the implicit domain (see PackageDomain() below) 65 | // of its caller. 66 | // 67 | // Domain is shown: 68 | // - via `errors.GetSafeDetails()`. 69 | // - when formatting with `%+v`. 70 | // - in Sentry reports. 71 | func New(msg string) error { 72 | return WithDomain(errors.New(msg), PackageDomainAtDepth(1)) 73 | } 74 | 75 | // Newf/Errorf with format and args can be implemented similarly. 76 | 77 | // HandledInDomain creates an error in the given domain and retains 78 | // the details of the given original error as context for 79 | // debugging. The original error is hidden and does not become a 80 | // "cause" for the new error. The original's error _message_ 81 | // is preserved. 82 | // 83 | // See the documentation of `WithDomain()` and `errors.Handled()` for details. 84 | func HandledInDomain(err error, domain Domain) error { 85 | return WithDomain(barriers.Handled(err), domain) 86 | } 87 | 88 | // HandledInDomainWithMessage is like HandledWithMessage but with a domain. 89 | func HandledInDomainWithMessage(err error, domain Domain, msg string) error { 90 | return WithDomain(barriers.HandledWithMessage(err, msg), domain) 91 | } 92 | 93 | // Handled creates a handled error in the implicit domain (see 94 | // PackageDomain() below) of its caller. 95 | // 96 | // See the documentation of `barriers.Handled()` for details. 97 | func Handled(err error) error { 98 | return HandledInDomain(err, PackageDomainAtDepth(1)) 99 | } 100 | 101 | // Handledf with format and args can be implemented similarly. 102 | 103 | // NotInDomain returns true if and only if the error's 104 | // domain is not one of the specified domains. 105 | func NotInDomain(err error, domains ...Domain) bool { 106 | return notInDomainInternal(GetDomain(err), domains...) 107 | } 108 | 109 | func notInDomainInternal(d Domain, domains ...Domain) bool { 110 | for _, given := range domains { 111 | if d == given { 112 | return false 113 | } 114 | } 115 | return true 116 | } 117 | 118 | // EnsureNotInDomain checks whether the error is in the given domain(s). 119 | // If it is, the given constructor if provided is called to construct 120 | // an alternate error. If no error constructor is provided, 121 | // a new barrier is constructed automatically using the first 122 | // provided domain as new domain. The original error message 123 | // is preserved. 124 | func EnsureNotInDomain( 125 | err error, constructor func(originalDomain Domain, err error) error, forbiddenDomains ...Domain, 126 | ) error { 127 | if err == nil { 128 | return nil 129 | } 130 | 131 | // Is the error already in the wanted domains? 132 | errDomain := GetDomain(err) 133 | if notInDomainInternal(errDomain, forbiddenDomains...) { 134 | // No: no-op. 135 | return err 136 | } 137 | return constructor(errDomain, err) 138 | } 139 | 140 | // PackageDomain returns an error domain that represents the 141 | // package of its caller. 142 | func PackageDomain() Domain { 143 | return PackageDomainAtDepth(1) 144 | } 145 | 146 | // PackageDomainAtDepth returns an error domain that describes the 147 | // package at the given call depth. 148 | func PackageDomainAtDepth(depth int) Domain { 149 | _, f, _, _ := runtime.Caller(1 + depth) 150 | return Domain("error domain: pkg " + filepath.Dir(f)) 151 | } 152 | 153 | // NamedDomain returns an error domain identified by the given string. 154 | func NamedDomain(domainName string) Domain { 155 | return Domain(fmt.Sprintf("error domain: %q", domainName)) 156 | } 157 | -------------------------------------------------------------------------------- /domains/internal/dummy_domain.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | package internal 16 | 17 | import ( 18 | "errors" 19 | 20 | "github.com/cockroachdb/errors/domains" 21 | ) 22 | 23 | // ThisDomain is a helper for tests. 24 | var ThisDomain = domains.PackageDomain() 25 | 26 | // NewError is a helper for tests. 27 | func NewError(msg string) error { 28 | return domains.WithDomain(errors.New(msg), ThisDomain) 29 | } 30 | -------------------------------------------------------------------------------- /domains/with_domain.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | package domains 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | 21 | "github.com/cockroachdb/errors/errbase" 22 | "github.com/cockroachdb/redact" 23 | "github.com/gogo/protobuf/proto" 24 | ) 25 | 26 | // withDomain is a wrapper type that adds a domain annotation to an 27 | // error. 28 | type withDomain struct { 29 | // Mandatory: error cause 30 | cause error 31 | // Mandatory: domain. This also must be free of PII 32 | // as it will be reported in "safe details". 33 | domain Domain 34 | } 35 | 36 | var _ error = (*withDomain)(nil) 37 | var _ errbase.SafeDetailer = (*withDomain)(nil) 38 | var _ errbase.TypeKeyMarker = (*withDomain)(nil) 39 | var _ fmt.Formatter = (*withDomain)(nil) 40 | var _ errbase.SafeFormatter = (*withDomain)(nil) 41 | 42 | // withDomain is an error. The original error message is preserved. 43 | func (e *withDomain) Error() string { return e.cause.Error() } 44 | 45 | // the cause is reachable. 46 | func (e *withDomain) Cause() error { return e.cause } 47 | func (e *withDomain) Unwrap() error { return e.cause } 48 | 49 | // ErrorKeyMarker implements the TypeNameMarker interface. 50 | // The full type name of barriers is extended with the domain as extra marker. 51 | // This ensures that domain-annotated errors appear to be of different types 52 | // for the purpose of Is(). 53 | func (e *withDomain) ErrorKeyMarker() string { return string(e.domain) } 54 | 55 | // SafeDetails reports the domain. 56 | func (e *withDomain) SafeDetails() []string { 57 | return []string{string(e.domain)} 58 | } 59 | 60 | func (e *withDomain) Format(s fmt.State, verb rune) { errbase.FormatError(e, s, verb) } 61 | 62 | func (e *withDomain) SafeFormatError(p errbase.Printer) error { 63 | if p.Detail() { 64 | p.Print(redact.Safe(e.domain)) 65 | } 66 | return e.cause 67 | } 68 | 69 | // A domain-annotated error is decoded exactly. 70 | func decodeWithDomain( 71 | _ context.Context, cause error, _ string, details []string, _ proto.Message, 72 | ) error { 73 | if len(details) == 0 { 74 | // decoding failure: expecting at least one detail string 75 | // (the one that carries the domain string). 76 | return nil 77 | } 78 | return &withDomain{cause: cause, domain: Domain(details[0])} 79 | } 80 | 81 | func init() { 82 | tn := errbase.GetTypeKey((*withDomain)(nil)) 83 | errbase.RegisterWrapperDecoder(tn, decodeWithDomain) 84 | } 85 | -------------------------------------------------------------------------------- /domains_api.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | package errors 16 | 17 | import "github.com/cockroachdb/errors/domains" 18 | 19 | // Domain is the type of a domain annotation. 20 | type Domain = domains.Domain 21 | 22 | // NoDomain is the domain of errors that don't originate 23 | // from a barrier. 24 | const NoDomain Domain = domains.NoDomain 25 | 26 | // NamedDomain returns an error domain identified by the given string. 27 | func NamedDomain(domainName string) Domain { return domains.NamedDomain(domainName) } 28 | 29 | // PackageDomain returns an error domain that represents the 30 | // package of its caller. 31 | func PackageDomain() Domain { return domains.PackageDomainAtDepth(1) } 32 | 33 | // PackageDomainAtDepth returns an error domain that describes the 34 | // package at the given call depth. 35 | func PackageDomainAtDepth(depth int) Domain { return domains.PackageDomainAtDepth(depth) } 36 | 37 | // WithDomain wraps an error so that it appears to come from the given domain. 38 | // 39 | // Domain is shown: 40 | // - via `errors.GetSafeDetails()`. 41 | // - when formatting with `%+v`. 42 | // - in Sentry reports. 43 | func WithDomain(err error, domain Domain) error { return domains.WithDomain(err, domain) } 44 | 45 | // NotInDomain returns true if and only if the error's 46 | // domain is not one of the specified domains. 47 | func NotInDomain(err error, doms ...Domain) bool { return domains.NotInDomain(err, doms...) } 48 | 49 | // EnsureNotInDomain checks whether the error is in the given domain(s). 50 | // If it is, the given constructor if provided is called to construct 51 | // an alternate error. If no error constructor is provided, 52 | // a new barrier is constructed automatically using the first 53 | // provided domain as new domain. The original error message 54 | // is preserved. 55 | func EnsureNotInDomain(err error, constructor DomainOverrideFn, forbiddenDomains ...Domain) error { 56 | return domains.EnsureNotInDomain(err, constructor, forbiddenDomains...) 57 | } 58 | 59 | // DomainOverrideFn is the type of the callback function passed to EnsureNotInDomain(). 60 | type DomainOverrideFn = func(originalDomain Domain, err error) error 61 | 62 | // HandledInDomain creates an error in the given domain and retains 63 | // the details of the given original error as context for 64 | // debugging. The original error is hidden and does not become a 65 | // "cause" for the new error. The original's error _message_ 66 | // is preserved. 67 | // 68 | // See the documentation of `WithDomain()` and `errors.Handled()` for details. 69 | func HandledInDomain(err error, domain Domain) error { return domains.HandledInDomain(err, domain) } 70 | 71 | // HandledInDomainWithMessage is like HandledWithMessage but with a domain. 72 | func HandledInDomainWithMessage(err error, domain Domain, msg string) error { 73 | return domains.HandledInDomainWithMessage(err, domain, msg) 74 | } 75 | 76 | // GetDomain extracts the domain of the given error, or NoDomain if 77 | // the error's cause does not have a domain annotation. 78 | func GetDomain(err error) Domain { return domains.GetDomain(err) } 79 | -------------------------------------------------------------------------------- /errbase/adapters_errno.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | // +build !plan9 16 | 17 | package errbase 18 | 19 | import ( 20 | "context" 21 | "os" 22 | "runtime" 23 | "syscall" 24 | 25 | "github.com/cockroachdb/errors/errorspb" 26 | "github.com/gogo/protobuf/proto" 27 | ) 28 | 29 | const thisArch = runtime.GOOS + ":" + runtime.GOARCH 30 | 31 | func encodeErrno(_ context.Context, err error) (msg string, safe []string, payload proto.Message) { 32 | e := err.(syscall.Errno) 33 | payload = &errorspb.ErrnoPayload{ 34 | OrigErrno: int64(e), 35 | Arch: thisArch, 36 | IsPermission: e.Is(os.ErrPermission), 37 | IsExist: e.Is(os.ErrExist), 38 | IsNotExist: e.Is(os.ErrNotExist), 39 | IsTimeout: e.Timeout(), 40 | IsTemporary: e.Temporary(), 41 | } 42 | return e.Error(), []string{e.Error()}, payload 43 | } 44 | 45 | func decodeErrno(_ context.Context, msg string, _ []string, payload proto.Message) error { 46 | m, ok := payload.(*errorspb.ErrnoPayload) 47 | if !ok { 48 | // If this ever happens, this means some version of the library 49 | // (presumably future) changed the payload type, and we're 50 | // receiving this here. In this case, give up and let 51 | // DecodeError use the opaque type. 52 | return nil 53 | } 54 | if m.Arch != thisArch { 55 | // The errno object is coming from a different platform. We'll 56 | // keep it opaque here. 57 | return &OpaqueErrno{msg: msg, details: m} 58 | } 59 | return syscall.Errno(m.OrigErrno) 60 | } 61 | 62 | func init() { 63 | pKey := GetTypeKey(syscall.Errno(0)) 64 | RegisterLeafEncoder(pKey, encodeErrno) 65 | RegisterLeafDecoder(pKey, decodeErrno) 66 | } 67 | -------------------------------------------------------------------------------- /errbase/adapters_errno_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | // +build !plan9 16 | 17 | package errbase_test 18 | 19 | import ( 20 | "context" 21 | "reflect" 22 | "syscall" 23 | "testing" 24 | 25 | "github.com/cockroachdb/errors/errbase" 26 | "github.com/cockroachdb/errors/errorspb" 27 | "github.com/cockroachdb/errors/oserror" 28 | "github.com/cockroachdb/errors/testutils" 29 | "github.com/gogo/protobuf/types" 30 | ) 31 | 32 | func TestAdaptErrno(t *testing.T) { 33 | tt := testutils.T{T: t} 34 | 35 | // Arbitrary values of errno on a given platform are preserved 36 | // exactly when decoded on the same platform. 37 | origErr := syscall.Errno(123) 38 | newErr := network(t, origErr) 39 | tt.Check(reflect.DeepEqual(newErr, origErr)) 40 | 41 | // Common values of errno preserve their properties 42 | // across a network encode/decode even though they 43 | // may not decode to the same type. 44 | for i := 0; i < 2000; i++ { 45 | origErr := syscall.Errno(i) 46 | enc := errbase.EncodeError(context.Background(), origErr) 47 | 48 | // Trick the decoder into thinking the error comes from a different platform. 49 | details := &enc.Error.(*errorspb.EncodedError_Leaf).Leaf.Details 50 | var d types.DynamicAny 51 | if err := types.UnmarshalAny(details.FullDetails, &d); err != nil { 52 | t.Fatal(err) 53 | } 54 | errnoDetails := d.Message.(*errorspb.ErrnoPayload) 55 | errnoDetails.Arch = "OTHER" 56 | any, err := types.MarshalAny(errnoDetails) 57 | if err != nil { 58 | t.Fatal(err) 59 | } 60 | details.FullDetails = any 61 | 62 | // Now decode the error. This produces an OpaqueErrno payload. 63 | dec := errbase.DecodeError(context.Background(), enc) 64 | if _, ok := dec.(*errbase.OpaqueErrno); !ok { 65 | t.Fatalf("expected OpaqueErrno, got %T", dec) 66 | } 67 | 68 | // Now check that the properties have been preserved properly. 69 | tt.CheckEqual(oserror.IsPermission(origErr), oserror.IsPermission(dec)) 70 | tt.CheckEqual(oserror.IsExist(origErr), oserror.IsExist(dec)) 71 | tt.CheckEqual(oserror.IsNotExist(origErr), oserror.IsNotExist(dec)) 72 | tt.CheckEqual(oserror.IsTimeout(origErr), oserror.IsTimeout(dec)) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /errbase/encode_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | package errbase_test 16 | 17 | import ( 18 | "testing" 19 | 20 | "github.com/cockroachdb/errors/errbase" 21 | "github.com/cockroachdb/errors/testutils" 22 | ) 23 | 24 | type myE struct{ marker string } 25 | 26 | func (e *myE) Error() string { return "woo" } 27 | 28 | func (e *myE) ErrorKeyMarker() string { return e.marker } 29 | 30 | var _ errbase.TypeKeyMarker = (*myE)(nil) 31 | 32 | // This test shows how the extended type marker changes the visible 33 | // type and thus the identity of an error. 34 | func TestTypeName(t *testing.T) { 35 | err1 := &myE{"woo"} 36 | err2 := &myE{""} 37 | 38 | tn1 := errbase.GetTypeMark(err1) 39 | tn2 := errbase.GetTypeMark(err2) 40 | 41 | tt := testutils.T{T: t} 42 | 43 | tt.Check(!tn1.Equals(tn2)) 44 | tt.CheckEqual(tn1.FamilyName, tn2.FamilyName) 45 | tt.Check(tn1.Extension != tn2.Extension) 46 | } 47 | -------------------------------------------------------------------------------- /errbase/err_string_ownership_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | package errbase_test 16 | 17 | import ( 18 | "context" 19 | goErr "errors" 20 | "fmt" 21 | "testing" 22 | 23 | "github.com/cockroachdb/errors/errbase" 24 | "github.com/cockroachdb/errors/errorspb" 25 | "github.com/cockroachdb/errors/testutils" 26 | ) 27 | 28 | func genEncoded(mt errorspb.MessageType) errorspb.EncodedError { 29 | return errorspb.EncodedError{ 30 | Error: &errorspb.EncodedError_Wrapper{ 31 | Wrapper: &errorspb.EncodedWrapper{ 32 | Cause: errorspb.EncodedError{ 33 | Error: &errorspb.EncodedError_Leaf{ 34 | Leaf: &errorspb.EncodedErrorLeaf{ 35 | Message: "leaf-error-msg", 36 | }, 37 | }, 38 | }, 39 | Message: "wrapper-error-msg: leaf-error-msg: extra info", 40 | Details: errorspb.EncodedErrorDetails{}, 41 | MessageType: mt, 42 | }, 43 | }, 44 | } 45 | } 46 | 47 | func TestDecodeOldVersion(t *testing.T) { 48 | tt := testutils.T{T: t} 49 | 50 | errOldEncoded := genEncoded(errorspb.MessageType_PREFIX) 51 | errOldDecoded := errbase.DecodeError(context.Background(), errOldEncoded) 52 | // Ensure that we will continue to just concat leaf after wrapper 53 | // with older errors for backward compatibility. 54 | tt.CheckEqual(errOldDecoded.Error(), "wrapper-error-msg: leaf-error-msg: extra info: leaf-error-msg") 55 | 56 | // Check to ensure that when flag is present, we interpret things correctly. 57 | errNewEncoded := genEncoded(errorspb.MessageType_FULL_MESSAGE) 58 | errNewDecoded := errbase.DecodeError(context.Background(), errNewEncoded) 59 | tt.CheckEqual(errNewDecoded.Error(), "wrapper-error-msg: leaf-error-msg: extra info") 60 | } 61 | 62 | func TestEncodeDecodeNewVersion(t *testing.T) { 63 | tt := testutils.T{T: t} 64 | errNewEncoded := errbase.EncodeError( 65 | context.Background(), 66 | fmt.Errorf( 67 | "wrapper-error-msg: %w: extra info", 68 | goErr.New("leaf-error-msg"), 69 | ), 70 | ) 71 | 72 | errNew := errorspb.EncodedError{ 73 | Error: &errorspb.EncodedError_Wrapper{ 74 | Wrapper: &errorspb.EncodedWrapper{ 75 | Cause: errorspb.EncodedError{ 76 | Error: &errorspb.EncodedError_Leaf{ 77 | Leaf: &errorspb.EncodedErrorLeaf{ 78 | Message: "leaf-error-msg", 79 | Details: errorspb.EncodedErrorDetails{ 80 | OriginalTypeName: "errors/*errors.errorString", 81 | ErrorTypeMark: errorspb.ErrorTypeMark{FamilyName: "errors/*errors.errorString", Extension: ""}, 82 | ReportablePayload: nil, 83 | FullDetails: nil, 84 | }, 85 | }, 86 | }, 87 | }, 88 | Message: "wrapper-error-msg: leaf-error-msg: extra info", 89 | Details: errorspb.EncodedErrorDetails{ 90 | OriginalTypeName: "fmt/*fmt.wrapError", 91 | ErrorTypeMark: errorspb.ErrorTypeMark{FamilyName: "fmt/*fmt.wrapError", Extension: ""}, 92 | ReportablePayload: nil, 93 | FullDetails: nil, 94 | }, 95 | MessageType: errorspb.MessageType_FULL_MESSAGE, 96 | }, 97 | }, 98 | } 99 | 100 | tt.CheckDeepEqual(errNewEncoded, errNew) 101 | newErr := errbase.DecodeError(context.Background(), errNew) 102 | 103 | // New version correctly decodes error 104 | tt.CheckEqual(newErr.Error(), "wrapper-error-msg: leaf-error-msg: extra info") 105 | } 106 | -------------------------------------------------------------------------------- /errbase/formatter.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | // This file is taken from golang.org/x/xerrors, 16 | // at commit 3ee3066db522c6628d440a3a91c4abdd7f5ef22f (2019-05-10). 17 | // From the original code: 18 | // Copyright 2018 The Go Authors. All rights reserved. 19 | // Use of this source code is governed by a BSD-style 20 | // license that can be found in the LICENSE file. 21 | 22 | package errbase 23 | 24 | // A Formatter formats error messages. 25 | // 26 | // NB: Consider implementing SafeFormatter instead. This will ensure 27 | // that error displays can distinguish bits that are PII-safe. 28 | type Formatter interface { 29 | error 30 | 31 | // FormatError prints the receiver's first error. 32 | // The return value decides what happens in the case 33 | // FormatError() is used to produce a "short" message, 34 | // eg. when it is used to implement Error(): 35 | // 36 | // - if it returns nil, then the short message 37 | // contains no more than that produced for this error, 38 | // even if the error has a further causal chain. 39 | // 40 | // - if it returns non-nil, then the short message 41 | // contains the value printed by this error, 42 | // followed by that of its causal chain. 43 | // (e.g. thiserror: itscause: furthercause) 44 | // 45 | // Note that all the causal chain is reported in verbose reports in 46 | // any case. 47 | FormatError(p Printer) (next error) 48 | } 49 | 50 | // SafeFormatter is implemented by error leaf or wrapper types that want 51 | // to separate safe and non-safe information when printed out. 52 | // 53 | // When multiple errors are chained (e.g. via errors.Wrap), intermediate 54 | // layers in the error that do not implement SafeError are considered 55 | // “unsafe” 56 | type SafeFormatter interface { 57 | // SafeFormatError prints the receiver's first error. 58 | // 59 | // The provided Printer behaves like a redact.SafePrinter its 60 | // Print() and Printf() methods conditionally add redaction markers 61 | // around unsafe bits. 62 | // 63 | // The return value of SafeFormatError() decides what happens in the 64 | // case the method is used to produce a "short" message, eg. when it 65 | // is used to implement Error(): 66 | // 67 | // - if it returns nil, then the short message 68 | // contains no more than that produced for this error, 69 | // even if the error has a further causal chain. 70 | // 71 | // - if it returns non-nil, then the short message 72 | // contains the value printed by this error, 73 | // followed by that of its causal chain. 74 | // (e.g. thiserror: itscause: furthercause) 75 | // 76 | // Note that all the causal chain is reported in verbose reports in 77 | // any case. 78 | SafeFormatError(p Printer) (next error) 79 | } 80 | 81 | // A Printer formats error messages. 82 | // 83 | // The most common implementation of Printer is the one provided by package fmt 84 | // during Printf (as of Go 1.13). Localization packages such as golang.org/x/text/message 85 | // typically provide their own implementations. 86 | type Printer interface { 87 | // Print appends args to the message output. 88 | Print(args ...interface{}) 89 | 90 | // Printf writes a formatted string. 91 | Printf(format string, args ...interface{}) 92 | 93 | // Detail reports whether error detail is requested. 94 | // After the first call to Detail, all text written to the Printer 95 | // is formatted as additional detail, or ignored when 96 | // detail has not been requested. 97 | // If Detail returns false, the caller can avoid printing the detail at all. 98 | Detail() bool 99 | } 100 | -------------------------------------------------------------------------------- /errbase/internal/unknown.proto: -------------------------------------------------------------------------------- 1 | // This file is used only for unknown_type_test.go 2 | 3 | syntax = "proto3"; 4 | package cockroach.errors.errbase.internal; 5 | option go_package = "internal"; 6 | 7 | message MyPayload { 8 | int32 val = 1; 9 | } 10 | 11 | -------------------------------------------------------------------------------- /errbase/opaque.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | package errbase 16 | 17 | import ( 18 | "fmt" 19 | 20 | "github.com/cockroachdb/errors/errorspb" 21 | "github.com/cockroachdb/redact" 22 | ) 23 | 24 | // opaqueLeaf is used when receiving an unknown leaf type. 25 | // Its important property is that if it is communicated 26 | // back to some network system that _does_ know about 27 | // the type, the original object can be restored. 28 | type opaqueLeaf struct { 29 | msg string 30 | details errorspb.EncodedErrorDetails 31 | } 32 | 33 | // opaqueLeafCauses is used when receiving an unknown multi-cause 34 | // wrapper type. Its important property is that if it is communicated 35 | // back to some network system that _does_ know about the type, the 36 | // original object can be restored. We encode multi-cause errors as 37 | // leaf nodes over the network, in order to support backwards 38 | // compatibility with existing single-cause wrapper messages. 39 | // 40 | // This struct *must* be initialized with a non-nil causes value in 41 | // order to comply with go stdlib expectations for `Unwrap()`. 42 | type opaqueLeafCauses struct { 43 | opaqueLeaf 44 | causes []error 45 | } 46 | 47 | var _ error = (*opaqueLeaf)(nil) 48 | var _ SafeDetailer = (*opaqueLeaf)(nil) 49 | var _ fmt.Formatter = (*opaqueLeaf)(nil) 50 | var _ SafeFormatter = (*opaqueLeaf)(nil) 51 | 52 | var _ error = (*opaqueLeafCauses)(nil) 53 | var _ SafeDetailer = (*opaqueLeafCauses)(nil) 54 | var _ fmt.Formatter = (*opaqueLeafCauses)(nil) 55 | var _ SafeFormatter = (*opaqueLeafCauses)(nil) 56 | 57 | // opaqueWrapper is used when receiving an unknown wrapper type. 58 | // Its important property is that if it is communicated 59 | // back to some network system that _does_ know about 60 | // the type, the original object can be restored. 61 | type opaqueWrapper struct { 62 | cause error 63 | prefix string 64 | details errorspb.EncodedErrorDetails 65 | messageType MessageType 66 | } 67 | 68 | var _ error = (*opaqueWrapper)(nil) 69 | var _ SafeDetailer = (*opaqueWrapper)(nil) 70 | var _ fmt.Formatter = (*opaqueWrapper)(nil) 71 | var _ SafeFormatter = (*opaqueWrapper)(nil) 72 | 73 | func (e *opaqueLeaf) Error() string { return e.msg } 74 | 75 | func (e *opaqueWrapper) Error() string { 76 | if e.messageType == FullMessage { 77 | return e.prefix 78 | } 79 | if e.prefix == "" { 80 | return e.cause.Error() 81 | } 82 | return fmt.Sprintf("%s: %s", e.prefix, e.cause) 83 | } 84 | 85 | // the opaque wrapper is a wrapper. 86 | func (e *opaqueWrapper) Cause() error { return e.cause } 87 | func (e *opaqueWrapper) Unwrap() error { return e.cause } 88 | 89 | func (e *opaqueLeaf) SafeDetails() []string { return e.details.ReportablePayload } 90 | func (e *opaqueWrapper) SafeDetails() []string { return e.details.ReportablePayload } 91 | 92 | func (e *opaqueLeaf) Format(s fmt.State, verb rune) { FormatError(e, s, verb) } 93 | func (e *opaqueLeafCauses) Format(s fmt.State, verb rune) { FormatError(e, s, verb) } 94 | func (e *opaqueWrapper) Format(s fmt.State, verb rune) { FormatError(e, s, verb) } 95 | 96 | // opaqueLeafCauses is a multi-cause wrapper 97 | func (e *opaqueLeafCauses) Unwrap() []error { return e.causes } 98 | 99 | func (e *opaqueLeaf) SafeFormatError(p Printer) (next error) { 100 | p.Print(e.msg) 101 | if p.Detail() { 102 | p.Printf("\n(opaque error leaf)") 103 | p.Printf("\ntype name: %s", redact.Safe(e.details.OriginalTypeName)) 104 | for i, d := range e.details.ReportablePayload { 105 | p.Printf("\nreportable %d:\n%s", redact.Safe(i), redact.Safe(d)) 106 | } 107 | if e.details.FullDetails != nil { 108 | p.Printf("\npayload type: %s", redact.Safe(e.details.FullDetails.TypeUrl)) 109 | } 110 | } 111 | return nil 112 | } 113 | 114 | func (e *opaqueWrapper) SafeFormatError(p Printer) (next error) { 115 | if len(e.prefix) > 0 { 116 | // We use the condition if len(msg) > 0 because 117 | // otherwise an empty string would cause a "redactable 118 | // empty string" to be emitted (something that looks like "<>") 119 | // and the error formatting code only cleanly elides 120 | // the prefix properly if the output string is completely empty. 121 | p.Print(e.prefix) 122 | } 123 | if p.Detail() { 124 | p.Printf("\n(opaque error wrapper)") 125 | p.Printf("\ntype name: %s", redact.Safe(e.details.OriginalTypeName)) 126 | for i, d := range e.details.ReportablePayload { 127 | p.Printf("\nreportable %d:\n%s", redact.Safe(i), redact.Safe(d)) 128 | } 129 | if e.details.FullDetails != nil { 130 | p.Printf("\npayload type: %s", redact.Safe(e.details.FullDetails.TypeUrl)) 131 | } 132 | } 133 | if e.messageType == FullMessage { 134 | return nil 135 | } 136 | return e.cause 137 | } 138 | -------------------------------------------------------------------------------- /errbase/opaque_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | package errbase 16 | 17 | import ( 18 | "context" 19 | "errors" 20 | "fmt" 21 | "testing" 22 | 23 | "github.com/cockroachdb/errors/testutils" 24 | "github.com/kr/pretty" 25 | ) 26 | 27 | func TestUnknownWrapperTraversalWithMessageOverride(t *testing.T) { 28 | // Simulating scenario where the new field on the opaque wrapper is dropped 29 | // in the middle of the chain by a node running an older version. 30 | 31 | origErr := fmt.Errorf("this is a wrapped err %w with a non-prefix wrap msg", errors.New("hello")) 32 | t.Logf("start err: %# v", pretty.Formatter(origErr)) 33 | 34 | // Encode the error, this will use the encoder. 35 | enc := EncodeError(context.Background(), origErr) 36 | t.Logf("encoded: %# v", pretty.Formatter(enc)) 37 | 38 | newErr := DecodeError(context.Background(), enc) 39 | t.Logf("decoded: %# v", pretty.Formatter(newErr)) 40 | 41 | // simulate node not knowing about `messageType` field 42 | newErr.(*opaqueWrapper).messageType = Prefix 43 | 44 | // Encode it again, to simulate the error passed on to another system. 45 | enc2 := EncodeError(context.Background(), newErr) 46 | t.Logf("encoded2: %# v", pretty.Formatter(enc)) 47 | 48 | // Then decode again. 49 | newErr2 := DecodeError(context.Background(), enc2) 50 | t.Logf("decoded: %# v", pretty.Formatter(newErr2)) 51 | 52 | tt := testutils.T{T: t} 53 | 54 | // We expect to see an erroneous `: hello` because our 55 | // error passes through a node which drops the new protobuf 56 | // field. 57 | tt.CheckEqual(newErr2.Error(), origErr.Error()+": hello") 58 | } 59 | -------------------------------------------------------------------------------- /errbase/oserror_go116.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | // +build go1.16 16 | 17 | package errbase 18 | 19 | import "io/fs" 20 | 21 | func registerOsPathErrorMigration() { 22 | // The os.PathError type was migrated to io.fs.PathError in Go 1.16. 23 | RegisterTypeMigration("os", "*os.PathError", &fs.PathError{}) 24 | } 25 | -------------------------------------------------------------------------------- /errbase/oserror_pre116.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | // +build !go1.16 16 | 17 | package errbase 18 | 19 | func registerOsPathErrorMigration() {} 20 | -------------------------------------------------------------------------------- /errbase/safe_details.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | package errbase 16 | 17 | import ( 18 | "fmt" 19 | 20 | "github.com/cockroachdb/errors/errorspb" 21 | pkgErr "github.com/pkg/errors" 22 | ) 23 | 24 | // SafeDetailer is an interface that can be implemented by errors that 25 | // can provide PII-free additional strings suitable for reporting or 26 | // telemetry. 27 | type SafeDetailer interface { 28 | SafeDetails() []string 29 | } 30 | 31 | // GetAllSafeDetails collects the safe details from the given error object 32 | // and all its causes. 33 | // The details are collected from outermost to innermost level of cause. 34 | func GetAllSafeDetails(err error) []SafeDetailPayload { 35 | var details []SafeDetailPayload 36 | for ; err != nil; err = UnwrapOnce(err) { 37 | details = append(details, GetSafeDetails(err)) 38 | } 39 | return details 40 | } 41 | 42 | // GetSafeDetails collects the safe details from the given error 43 | // object. If it is a wrapper, only the details from the wrapper are 44 | // returned. 45 | func GetSafeDetails(err error) (payload SafeDetailPayload) { 46 | origTypeName, famName, ext := getTypeDetails(err, false /*onlyFamily*/) 47 | payload.OriginalTypeName = origTypeName 48 | payload.ErrorTypeMark = errorspb.ErrorTypeMark{ 49 | FamilyName: famName, 50 | Extension: ext, 51 | } 52 | payload.SafeDetails = getDetails(err) 53 | return 54 | } 55 | 56 | func getDetails(err error) []string { 57 | if sd, ok := err.(SafeDetailer); ok { 58 | return sd.SafeDetails() 59 | } 60 | // For convenience, we also know how to extract stack traces 61 | // in the style of github.com/pkg/errors. 62 | if st, ok := err.(interface{ StackTrace() pkgErr.StackTrace }); ok { 63 | return []string{fmt.Sprintf("%+v", st.StackTrace())} 64 | } 65 | return nil 66 | } 67 | 68 | // SafeDetailPayload captures the safe strings for one 69 | // level of wrapping. 70 | type SafeDetailPayload struct { 71 | // OriginalTypeName is the concrete type of the error that the details 72 | // are coming from. 73 | OriginalTypeName string 74 | // ErrorTypeMark is the mark of the error that the details are 75 | // coming from. This may contain a different type name than 76 | // OriginalTypeName in case an error type was migrated. 77 | ErrorTypeMark errorspb.ErrorTypeMark 78 | // SafeDetails are the PII-free strings. 79 | SafeDetails []string 80 | } 81 | 82 | // Fill can be used to concatenate multiple SafeDetailPayloads. 83 | func (s *SafeDetailPayload) Fill(slice []string) []string { 84 | if len(s.SafeDetails) == 0 { 85 | return slice 86 | } 87 | slice = append(slice, fmt.Sprintf("details for %s::%s:", 88 | s.ErrorTypeMark.FamilyName, s.ErrorTypeMark.Extension)) 89 | for _, sd := range s.SafeDetails { 90 | slice = append(slice, " "+sd) 91 | } 92 | return slice 93 | } 94 | -------------------------------------------------------------------------------- /errbase/stack_format_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | package errbase_test 16 | 17 | import ( 18 | "fmt" 19 | "regexp" 20 | "strings" 21 | "testing" 22 | 23 | "github.com/cockroachdb/errors/errbase" 24 | "github.com/cockroachdb/errors/testutils" 25 | pkgErr "github.com/pkg/errors" 26 | ) 27 | 28 | func TestSimplifyStacks(t *testing.T) { 29 | leaf := func() error { 30 | return pkgErr.New("hello world") 31 | } 32 | wrapper := func() error { 33 | err := leaf() 34 | return pkgErr.WithStack(err) 35 | } 36 | errWrapper := wrapper() 37 | t.Logf("error: %+v", errWrapper) 38 | 39 | t.Run("low level API", func(t *testing.T) { 40 | tt := testutils.T{t} 41 | // Extract the stack trace from the leaf. 42 | errLeaf := errbase.UnwrapOnce(errWrapper) 43 | leafP, ok := errLeaf.(errbase.StackTraceProvider) 44 | if !ok { 45 | t.Fatal("leaf error does not provide stack trace") 46 | } 47 | leafT := leafP.StackTrace() 48 | spv := fmtClean(leafT) 49 | t.Logf("-- leaf trace --%+v", spv) 50 | if !strings.Contains(spv, "TestSimplifyStacks") { 51 | t.Fatalf("expected test function in trace, got:%v", spv) 52 | } 53 | leafLines := strings.Split(spv, "\n") 54 | 55 | // Extract the stack trace from the wrapper. 56 | wrapperP, ok := errWrapper.(errbase.StackTraceProvider) 57 | if !ok { 58 | t.Fatal("wrapper error does not provide stack trace") 59 | } 60 | wrapperT := wrapperP.StackTrace() 61 | spv = fmtClean(wrapperT) 62 | t.Logf("-- wrapper trace --%+v", spv) 63 | wrapperLines := strings.Split(spv, "\n") 64 | 65 | // Sanity check before we verify the result. 66 | tt.Check(len(wrapperLines) > 0) 67 | tt.CheckDeepEqual(wrapperLines[3:], leafLines[5:]) 68 | 69 | // Elide the suffix and verify that we arrive to the same result. 70 | simplified, hasElided := errbase.ElideSharedStackTraceSuffix(leafT, wrapperT) 71 | spv = fmtClean(simplified) 72 | t.Logf("-- simplified (%v) --%+v", hasElided, spv) 73 | simplifiedLines := strings.Split(spv, "\n") 74 | tt.CheckDeepEqual(simplifiedLines, wrapperLines[0:3]) 75 | }) 76 | 77 | t.Run("high level API", func(t *testing.T) { 78 | tt := testutils.T{t} 79 | 80 | spv := fmtClean(errbase.Formattable(errWrapper)) 81 | tt.CheckStringEqual(spv, `hello world 82 | (1) 83 | -- stack trace: 84 | | github.com/cockroachdb/errors/errbase_test.TestSimplifyStacks.func2 85 | | : 86 | | [...repeated from below...] 87 | Wraps: (2) hello world 88 | | github.com/cockroachdb/errors/errbase_test.TestSimplifyStacks.func1 89 | | : 90 | | github.com/cockroachdb/errors/errbase_test.TestSimplifyStacks.func2 91 | | : 92 | | github.com/cockroachdb/errors/errbase_test.TestSimplifyStacks 93 | | : 94 | | testing.tRunner 95 | | : 96 | | runtime.goexit 97 | | : 98 | Error types: (1) *errors.withStack (2) *errors.fundamental`) 99 | }) 100 | } 101 | 102 | func fmtClean(x interface{}) string { 103 | spv := fmt.Sprintf("%+v", x) 104 | spv = fileref.ReplaceAllString(spv, ":") 105 | spv = strings.ReplaceAll(spv, "\t", "") 106 | return spv 107 | } 108 | 109 | var fileref = regexp.MustCompile(`([a-zA-Z0-9\._/@-]*\.(?:go|s):\d+)`) 110 | -------------------------------------------------------------------------------- /errbase/unwrap.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | package errbase 16 | 17 | // Sadly the go 2/1.13 design for errors has promoted the name 18 | // `Unwrap()` for the method that accesses the cause, whilst the 19 | // ecosystem has already chosen `Cause()`. In order to unwrap 20 | // reliably, we must thus support both. 21 | // 22 | // See: https://github.com/golang/go/issues/31778 23 | 24 | // UnwrapOnce accesses the direct cause of the error if any, otherwise 25 | // returns nil. 26 | // 27 | // It supports both errors implementing causer (`Cause()` method, from 28 | // github.com/pkg/errors) and `Wrapper` (`Unwrap()` method, from the 29 | // Go 2 error proposal). 30 | // 31 | // UnwrapOnce treats multi-errors (those implementing the 32 | // `Unwrap() []error` interface as leaf-nodes since they cannot 33 | // reasonably be iterated through to a single cause. These errors 34 | // are typically constructed as a result of `fmt.Errorf` which results 35 | // in a `wrapErrors` instance that contains an interpolated error 36 | // string along with a list of causes. 37 | // 38 | // The go stdlib does not define output on `Unwrap()` for a multi-cause 39 | // error, so we default to nil here. 40 | func UnwrapOnce(err error) (cause error) { 41 | switch e := err.(type) { 42 | case interface{ Cause() error }: 43 | return e.Cause() 44 | case interface{ Unwrap() error }: 45 | return e.Unwrap() 46 | } 47 | return nil 48 | } 49 | 50 | // UnwrapAll accesses the root cause object of the error. 51 | // If the error has no cause (leaf error), it is returned directly. 52 | // UnwrapAll treats multi-errors as leaf nodes. 53 | func UnwrapAll(err error) error { 54 | for { 55 | if cause := UnwrapOnce(err); cause != nil { 56 | err = cause 57 | continue 58 | } 59 | break 60 | } 61 | return err 62 | } 63 | 64 | // UnwrapMulti access the slice of causes that an error contains, if it is a 65 | // multi-error. 66 | func UnwrapMulti(err error) []error { 67 | if me, ok := err.(interface{ Unwrap() []error }); ok { 68 | return me.Unwrap() 69 | } 70 | return nil 71 | } 72 | -------------------------------------------------------------------------------- /errbase/unwrap_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | package errbase_test 16 | 17 | import ( 18 | "errors" 19 | "fmt" 20 | "testing" 21 | 22 | "github.com/cockroachdb/errors/errbase" 23 | "github.com/cockroachdb/errors/testutils" 24 | pkgErr "github.com/pkg/errors" 25 | ) 26 | 27 | // This test demonstrates how to use errbase.UnwrapOnce and errbase.UnwrapAll to 28 | // access causes. 29 | func TestUnwrap(t *testing.T) { 30 | tt := testutils.T{T: t} 31 | 32 | err := errors.New("hello") 33 | 34 | tt.CheckEqual(errbase.UnwrapOnce(err), nil) 35 | tt.CheckEqual(errbase.UnwrapAll(err), err) 36 | 37 | // WithMessage is guaranteed to add just one layer of wrapping. 38 | err2 := pkgErr.WithMessage(err, "woo") 39 | 40 | tt.CheckEqual(errbase.UnwrapOnce(err2), err) 41 | tt.CheckEqual(errbase.UnwrapAll(err2), err) 42 | 43 | err3 := pkgErr.WithMessage(err2, "woo") 44 | 45 | tt.CheckEqual(errbase.UnwrapOnce(err3), err2) 46 | tt.CheckEqual(errbase.UnwrapAll(err3), err) 47 | } 48 | 49 | // This test demonstrates how errbase.UnwrapOnce/errbase.UnwrapAll are able to use 50 | // either Cause() or errbase.Unwrap(). 51 | func TestMixedErrorWrapping(t *testing.T) { 52 | tt := testutils.T{T: t} 53 | 54 | err := errors.New("hello") 55 | err2 := pkgErr.WithMessage(err, "woo") 56 | err3 := &myWrapper{cause: err2} 57 | 58 | tt.CheckEqual(errbase.UnwrapOnce(err3), err2) 59 | tt.CheckEqual(errbase.UnwrapAll(err3), err) 60 | } 61 | 62 | func TestMultiErrorUnwrap(t *testing.T) { 63 | tt := testutils.T{T: t} 64 | 65 | err := errors.New("hello") 66 | err2 := pkgErr.WithMessage(err, "woo") 67 | err3 := fmt.Errorf("%w %w", err, err2) 68 | 69 | tt.CheckEqual(errbase.UnwrapOnce(err3), nil) 70 | tt.CheckEqual(errbase.UnwrapAll(err3), err3) 71 | tt.CheckDeepEqual(errbase.UnwrapMulti(err3), []error{err, err2}) 72 | } 73 | 74 | type myWrapper struct{ cause error } 75 | 76 | func (w *myWrapper) Error() string { return w.cause.Error() } 77 | func (w *myWrapper) Unwrap() error { return w.cause } 78 | -------------------------------------------------------------------------------- /errorspb/errors.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | package errorspb 16 | 17 | // IsSet returns true if the EncodedError contains an error, or false if it is 18 | // empty. 19 | func (m *EncodedError) IsSet() bool { 20 | return m.Error != nil 21 | } 22 | -------------------------------------------------------------------------------- /errorspb/hintdetail.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package cockroach.errorspb; 3 | option go_package = "errorspb"; 4 | 5 | message StringPayload { 6 | string msg = 1; 7 | } 8 | -------------------------------------------------------------------------------- /errorspb/markers.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | package errorspb 16 | 17 | func (m ErrorTypeMark) Equals(o ErrorTypeMark) bool { 18 | return m.FamilyName == o.FamilyName && m.Extension == o.Extension 19 | } 20 | -------------------------------------------------------------------------------- /errorspb/markers.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package cockroach.errorspb; 3 | option go_package = "errorspb"; 4 | 5 | import "errorspb/errors.proto"; 6 | import "gogoproto/gogo.proto"; 7 | 8 | // MarkPayload is the error payload for a forced marker. 9 | // See errors/markers/markers.go and the RFC on 10 | // error handling for details. 11 | message MarkPayload { 12 | string msg = 1; 13 | repeated ErrorTypeMark types = 2 [(gogoproto.nullable) = false]; 14 | } 15 | -------------------------------------------------------------------------------- /errorspb/tags.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package cockroach.errorspb; 3 | option go_package = "errorspb"; 4 | 5 | import "gogoproto/gogo.proto"; 6 | 7 | // TagsPayload is the error payload for a WithContext 8 | // marker. 9 | // See errors/contexttags/withcontext.go and the RFC on 10 | // error handling for details. 11 | message TagsPayload { 12 | repeated TagPayload tags = 1 [(gogoproto.nullable) = false]; 13 | } 14 | 15 | message TagPayload { 16 | string tag = 1; 17 | string value = 2; 18 | } 19 | -------------------------------------------------------------------------------- /errorspb/testing.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | package errorspb 16 | 17 | // Error implements the error interface. 18 | func (t *TestError) Error() string { return "test error" } 19 | -------------------------------------------------------------------------------- /errorspb/testing.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package cockroach.errorspb; 3 | option go_package = "errorspb"; 4 | 5 | // TestError is meant for use in testing only. 6 | message TestError{} 7 | -------------------------------------------------------------------------------- /errutil/as.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Go Authors. All rights reserved. 2 | // Copyright 2019 The Cockroach Authors. 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 13 | // implied. See the License for the specific language governing 14 | // permissions and limitations under the License. 15 | 16 | package errutil 17 | 18 | import ( 19 | "reflect" 20 | 21 | "github.com/cockroachdb/errors/errbase" 22 | ) 23 | 24 | // As finds the first error in err's chain that matches the type to which target 25 | // points, and if so, sets the target to its value and returns true. An error 26 | // matches a type if it is assignable to the target type, or if it has a method 27 | // As(interface{}) bool such that As(target) returns true. As will panic if target 28 | // is not a non-nil pointer to a type which implements error or is of interface type. 29 | // 30 | // The As method should set the target to its value and return true if err 31 | // matches the type to which target points. 32 | // 33 | // Note: this implementation differs from that of xerrors as follows: 34 | // - it also supports recursing through causes with Cause(). 35 | // - if it detects an API use error, its panic object is a valid error. 36 | func As(err error, target interface{}) bool { 37 | if target == nil { 38 | panic(AssertionFailedf("errors.As: target cannot be nil")) 39 | } 40 | 41 | // We use introspection for now, of course when/if Go gets generics 42 | // all this can go away. 43 | val := reflect.ValueOf(target) 44 | typ := val.Type() 45 | if typ.Kind() != reflect.Ptr || val.IsNil() { 46 | panic(AssertionFailedf("errors.As: target must be a non-nil pointer, found %T", target)) 47 | } 48 | if e := typ.Elem(); e.Kind() != reflect.Interface && !e.Implements(errorType) { 49 | panic(AssertionFailedf("errors.As: *target must be interface or implement error, found %T", target)) 50 | } 51 | 52 | targetType := typ.Elem() 53 | for c := err; c != nil; c = errbase.UnwrapOnce(c) { 54 | if reflect.TypeOf(c).AssignableTo(targetType) { 55 | val.Elem().Set(reflect.ValueOf(c)) 56 | return true 57 | } 58 | if x, ok := c.(interface{ As(interface{}) bool }); ok && x.As(target) { 59 | return true 60 | } 61 | 62 | // If at any point in the single cause chain including the top, 63 | // we encounter a multi-cause chain, recursively explore it. 64 | for _, cause := range errbase.UnwrapMulti(c) { 65 | if As(cause, target) { 66 | return true 67 | } 68 | } 69 | } 70 | 71 | return false 72 | } 73 | 74 | var errorType = reflect.TypeOf((*error)(nil)).Elem() 75 | -------------------------------------------------------------------------------- /errutil/as_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | package errutil_test 16 | 17 | import ( 18 | goErr "errors" 19 | "fmt" 20 | "testing" 21 | 22 | "github.com/cockroachdb/errors" 23 | "github.com/cockroachdb/errors/testutils" 24 | ) 25 | 26 | func TestAs(t *testing.T) { 27 | tt := testutils.T{t} 28 | 29 | refErr := &myType{msg: "woo"} 30 | 31 | // Check we can fish the leaf back. 32 | var mySlot *myType 33 | tt.Check(errors.As(refErr, &mySlot)) 34 | tt.Check(errors.Is(mySlot, refErr)) 35 | 36 | // Check we can fish it even if behind something else. 37 | // Note: this would fail with xerrors.As() because 38 | // Wrap() uses github.com/pkg/errors which implements 39 | // Cause() but not Unwrap(). 40 | // This may change with https://github.com/pkg/errors/pull/206. 41 | wErr := errors.Wrap(refErr, "hidden") 42 | mySlot = nil 43 | tt.Check(errors.As(wErr, &mySlot)) 44 | tt.Check(errors.Is(mySlot, refErr)) 45 | 46 | // Check we can fish the wrapper back. 47 | refwErr := &myWrapper{cause: errors.New("world"), msg: "hello"} 48 | var mywSlot *myWrapper 49 | tt.Check(errors.As(refwErr, &mywSlot)) 50 | tt.Check(errors.Is(mywSlot, refwErr)) 51 | 52 | // Check that it works even if behind something else. 53 | wwErr := errors.Wrap(refwErr, "hidden") 54 | mywSlot = nil 55 | tt.Check(errors.As(wwErr, &mywSlot)) 56 | tt.Check(errors.Is(mywSlot, refwErr)) 57 | 58 | // Check that it works even if hidden in wrapError 59 | multiWrapErr := fmt.Errorf("test %w test", errors.Wrap(refwErr, "hidden")) 60 | mywSlot = nil 61 | tt.Check(errors.As(multiWrapErr, &mywSlot)) 62 | tt.Check(errors.Is(mywSlot, refwErr)) 63 | 64 | // Check that it works even if hidden in multi-cause wrapErrors 65 | multiWrapErr = fmt.Errorf("error: %w and %w", errors.Wrap(refwErr, "hidden"), errors.New("world")) 66 | mywSlot = nil 67 | tt.Check(errors.As(multiWrapErr, &mywSlot)) 68 | tt.Check(errors.Is(mywSlot, refwErr)) 69 | 70 | // Check that it works even if hidden in custom multi-error 71 | multiWrapErr = &myMultiWrapper{ 72 | causes: []error{errors.Wrap(refwErr, "hidden"), errors.New("world")}, 73 | msg: "errors", 74 | } 75 | mywSlot = nil 76 | tt.Check(errors.As(multiWrapErr, &mywSlot)) 77 | tt.Check(errors.Is(mywSlot, refwErr)) 78 | 79 | // Check that it works even if hidden in a multi-level multi-cause chain 80 | multiWrapErr = fmt.Errorf("error: %w and %w", 81 | &myMultiWrapper{ 82 | causes: []error{errors.New("ignoreme"), errors.New("also ignore")}, 83 | msg: "red herring", 84 | }, &myMultiWrapper{ 85 | causes: []error{errors.Wrap(refwErr, "hidden"), errors.New("world")}, 86 | msg: "errors", 87 | }) 88 | mywSlot = nil 89 | tt.Check(errors.As(multiWrapErr, &mywSlot)) 90 | tt.Check(errors.Is(mywSlot, refwErr)) 91 | } 92 | 93 | type myType struct{ msg string } 94 | 95 | func (m *myType) Error() string { return m.msg } 96 | 97 | type myWrapper struct { 98 | cause error 99 | msg string 100 | } 101 | 102 | func (m *myWrapper) Error() string { return fmt.Sprintf("%s: %v", m.msg, m.cause) } 103 | 104 | type myMultiWrapper struct { 105 | causes []error 106 | msg string 107 | } 108 | 109 | func (m *myMultiWrapper) Error() string { return fmt.Sprintf("%s: %v", m.msg, goErr.Join(m.causes...)) } 110 | 111 | func (m *myMultiWrapper) Unwrap() []error { 112 | return m.causes 113 | } 114 | -------------------------------------------------------------------------------- /errutil/assertions.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | package errutil 16 | 17 | import ( 18 | "github.com/cockroachdb/errors/assert" 19 | "github.com/cockroachdb/errors/barriers" 20 | "github.com/cockroachdb/errors/withstack" 21 | ) 22 | 23 | // AssertionFailedf creates an internal error. 24 | // 25 | // Detail is shown: 26 | // - via `errors.GetSafeDetails()`, shows redacted strings. 27 | // - when formatting with `%+v`. 28 | // - in Sentry reports. 29 | func AssertionFailedf(format string, args ...interface{}) error { 30 | return AssertionFailedWithDepthf(1, format, args...) 31 | } 32 | 33 | // AssertionFailedWithDepthf creates an internal error 34 | // with a stack trace collected at the specified depth. 35 | // See the doc of `AssertionFailedf()` for more details. 36 | func AssertionFailedWithDepthf(depth int, format string, args ...interface{}) error { 37 | err := NewWithDepthf(1+depth, format, args...) 38 | err = assert.WithAssertionFailure(err) 39 | return err 40 | } 41 | 42 | // HandleAsAssertionFailure hides an error and turns it into 43 | // an assertion failure. Both details from the original error and the 44 | // context of the caller are preserved. The original error is not 45 | // visible as cause any more. The original error message is preserved. 46 | // See the doc of `AssertionFailedf()` for more details. 47 | func HandleAsAssertionFailure(origErr error) error { 48 | return HandleAsAssertionFailureDepth(1, origErr) 49 | } 50 | 51 | // HandleAsAssertionFailureDepth is like HandleAsAssertionFailure but 52 | // the depth at which the call stack is captured can be specified. 53 | func HandleAsAssertionFailureDepth(depth int, origErr error) error { 54 | err := barriers.Handled(origErr) 55 | err = withstack.WithStackDepth(err, 1+depth) 56 | err = assert.WithAssertionFailure(err) 57 | return err 58 | } 59 | 60 | // NewAssertionErrorWithWrappedErrf wraps an error and turns it into 61 | // an assertion error. Both details from the original error and the 62 | // context of the caller are preserved. The original error is not 63 | // visible as cause any more. The original error message is preserved. 64 | // See the doc of `AssertionFailedf()` for more details. 65 | func NewAssertionErrorWithWrappedErrf(origErr error, format string, args ...interface{}) error { 66 | return NewAssertionErrorWithWrappedErrDepthf(1, origErr, format, args...) 67 | } 68 | 69 | // NewAssertionErrorWithWrappedErrDepthf is like 70 | // NewAssertionErrorWithWrappedErrf but the depth at which the call 71 | // stack is captured can be specified. 72 | // See the doc of `AssertionFailedf()` for more details. 73 | func NewAssertionErrorWithWrappedErrDepthf( 74 | depth int, origErr error, format string, args ...interface{}, 75 | ) error { 76 | err := barriers.Handled(origErr) 77 | err = WrapWithDepthf(depth+1, err, format, args...) 78 | err = assert.WithAssertionFailure(err) 79 | return err 80 | } 81 | -------------------------------------------------------------------------------- /errutil/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | // Package errutil combines primitives from the library, the Go errors 16 | // package and github.com/pkg/errors. It aims to serve as drop-in 17 | // replacement to github.com/pkg/errors and replace the legacy 18 | // implementation of `pgerrors`. 19 | package errutil 20 | -------------------------------------------------------------------------------- /errutil/format_error_special.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | package errutil 16 | 17 | import ( 18 | "context" 19 | "net" 20 | "os" 21 | "runtime" 22 | "syscall" 23 | 24 | "github.com/cockroachdb/errors/errbase" 25 | "github.com/cockroachdb/errors/markers" 26 | "github.com/cockroachdb/redact" 27 | ) 28 | 29 | func init() { 30 | errbase.RegisterSpecialCasePrinter(specialCaseFormat) 31 | } 32 | 33 | func specialCaseFormat(err error, p errbase.Printer, isLeaf bool) (handled bool, next error) { 34 | if isLeaf && markers.IsAny(err, 35 | context.DeadlineExceeded, 36 | context.Canceled, 37 | os.ErrInvalid, 38 | os.ErrPermission, 39 | os.ErrExist, 40 | os.ErrNotExist, 41 | os.ErrClosed, 42 | os.ErrNoDeadline) { 43 | p.Print(redact.Safe(err.Error())) 44 | return true, nil 45 | } 46 | 47 | switch v := err.(type) { 48 | // The following two types are safe too. 49 | case runtime.Error: 50 | p.Print(redact.Safe(v.Error())) 51 | return true, err 52 | case syscall.Errno: 53 | p.Print(redact.Safe(v.Error())) 54 | return true, err 55 | case *os.SyscallError: 56 | p.Print(redact.Safe(v.Syscall)) 57 | return true, err 58 | case *os.PathError: 59 | p.Printf("%s %s", redact.Safe(v.Op), v.Path) 60 | return true, err 61 | case *os.LinkError: 62 | p.Printf("%s %s %s", redact.Safe(v.Op), v.Old, v.New) 63 | return true, err 64 | case *net.OpError: 65 | p.Print(redact.Safe(v.Op)) 66 | if v.Net != "" { 67 | p.Printf(" %s", redact.Safe(v.Net)) 68 | } 69 | if v.Source != nil { 70 | p.Printf(" %s", v.Source) 71 | } 72 | if v.Addr != nil { 73 | if v.Source != nil { 74 | p.Printf(" ->") 75 | } 76 | p.Printf(" %s", v.Addr) 77 | } 78 | return true, err 79 | case redact.SafeMessager: 80 | // Backward-compatibility with previous versions 81 | // of the errors library: if an error type implements 82 | // SafeMessage(), use that instead of its error message. 83 | p.Print(redact.Safe(v.SafeMessage())) 84 | // It also short-cuts any further causes. 85 | return true, nil 86 | } 87 | return false, nil 88 | } 89 | -------------------------------------------------------------------------------- /errutil/message.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | package errutil 16 | 17 | import "github.com/cockroachdb/redact" 18 | 19 | // WithMessage annotates err with a new message. 20 | // If err is nil, WithMessage returns nil. 21 | // The message is considered safe for reporting 22 | // and is included in Sentry reports. 23 | func WithMessage(err error, message string) error { 24 | if err == nil { 25 | return nil 26 | } 27 | return &withPrefix{ 28 | cause: err, 29 | prefix: redact.Sprint(redact.Safe(message)), 30 | } 31 | } 32 | 33 | // WithMessagef annotates err with the format specifier. 34 | // If err is nil, WithMessagef returns nil. 35 | // The message is formatted as per redact.Sprintf, 36 | // to separate safe and unsafe strings for Sentry reporting. 37 | func WithMessagef(err error, format string, args ...interface{}) error { 38 | if err == nil { 39 | return nil 40 | } 41 | return &withPrefix{ 42 | cause: err, 43 | prefix: redact.Sprintf(format, args...), 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /errutil_api_test.go: -------------------------------------------------------------------------------- 1 | package errors_test 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/cockroachdb/errors" 9 | "github.com/cockroachdb/errors/testutils" 10 | ) 11 | 12 | func TestUnwrap(t *testing.T) { 13 | tt := testutils.T{t} 14 | 15 | e := fmt.Errorf("foo %w %w", fmt.Errorf("bar"), fmt.Errorf("baz")) 16 | 17 | // Compatibility with go 1.20: Unwrap() on a multierror returns nil 18 | // (per API documentation) 19 | tt.Check(errors.Unwrap(e) == nil) 20 | } 21 | 22 | // More detailed testing of Join is in datadriven_test.go. Here we make 23 | // sure that the public API includes the stacktrace wrapper. 24 | func TestJoin(t *testing.T) { 25 | e := errors.Join(errors.New("abc123"), errors.New("def456")) 26 | printed := fmt.Sprintf("%+v", e) 27 | expected := `Error types: (1) *withstack.withStack (2) *join.joinError (3) *withstack.withStack (4) *errutil.leafError (5) *withstack.withStack (6) *errutil.leafError` 28 | if !strings.Contains(printed, expected) { 29 | t.Errorf("Expected: %s to contain: %s", printed, expected) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /extgrpc/ext_grpc.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package cockroach.errors.extgrpc; 3 | option go_package = "extgrpc"; 4 | 5 | message EncodedGrpcCode { 6 | uint32 code = 1; 7 | } 8 | -------------------------------------------------------------------------------- /exthttp/ext_http.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | package exthttp 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | 21 | "github.com/cockroachdb/errors" 22 | "github.com/cockroachdb/errors/errbase" 23 | "github.com/cockroachdb/errors/markers" 24 | "github.com/gogo/protobuf/proto" 25 | ) 26 | 27 | // This file demonstrates how to add a wrapper type not otherwise 28 | // known to the rest of the library. 29 | 30 | // withHTTPCode is our wrapper type. 31 | type withHTTPCode struct { 32 | cause error 33 | code int 34 | } 35 | 36 | // WrapWithHTTPCode adds a HTTP code to an existing error. 37 | func WrapWithHTTPCode(err error, code int) error { 38 | if err == nil { 39 | return nil 40 | } 41 | return &withHTTPCode{cause: err, code: code} 42 | } 43 | 44 | // GetHTTPCode retrieves the HTTP code from a stack of causes. 45 | func GetHTTPCode(err error, defaultCode int) int { 46 | if v, ok := markers.If(err, func(err error) (interface{}, bool) { 47 | if w, ok := err.(*withHTTPCode); ok { 48 | return w.code, true 49 | } 50 | return nil, false 51 | }); ok { 52 | return v.(int) 53 | } 54 | return defaultCode 55 | } 56 | 57 | // it's an error. 58 | func (w *withHTTPCode) Error() string { return w.cause.Error() } 59 | 60 | // it's also a wrapper. 61 | func (w *withHTTPCode) Cause() error { return w.cause } 62 | func (w *withHTTPCode) Unwrap() error { return w.cause } 63 | 64 | // it knows how to format itself. 65 | func (w *withHTTPCode) Format(s fmt.State, verb rune) { errors.FormatError(w, s, verb) } 66 | 67 | // SafeFormatter implements errors.SafeFormatter. 68 | // Note: see the documentation of errbase.SafeFormatter for details 69 | // on how to implement this. In particular beware of not emitting 70 | // unsafe strings. 71 | func (w *withHTTPCode) SafeFormatError(p errors.Printer) (next error) { 72 | if p.Detail() { 73 | p.Printf("http code: %d", w.code) 74 | } 75 | return w.cause 76 | } 77 | 78 | // it's an encodable error. 79 | func encodeWithHTTPCode(_ context.Context, err error) (string, []string, proto.Message) { 80 | w := err.(*withHTTPCode) 81 | details := []string{fmt.Sprintf("HTTP %d", w.code)} 82 | payload := &EncodedHTTPCode{Code: uint32(w.code)} 83 | return "", details, payload 84 | } 85 | 86 | // it's a decodable error. 87 | func decodeWithHTTPCode( 88 | _ context.Context, cause error, _ string, _ []string, payload proto.Message, 89 | ) error { 90 | wp := payload.(*EncodedHTTPCode) 91 | return &withHTTPCode{cause: cause, code: int(wp.Code)} 92 | } 93 | 94 | func init() { 95 | errbase.RegisterWrapperEncoder(errbase.GetTypeKey((*withHTTPCode)(nil)), encodeWithHTTPCode) 96 | errbase.RegisterWrapperDecoder(errbase.GetTypeKey((*withHTTPCode)(nil)), decodeWithHTTPCode) 97 | } 98 | -------------------------------------------------------------------------------- /exthttp/ext_http.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package cockroach.errors.exthttp; 3 | option go_package = "exthttp"; 4 | 5 | message EncodedHTTPCode { 6 | uint32 code = 1; 7 | } 8 | -------------------------------------------------------------------------------- /exthttp/ext_http_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | package exthttp_test 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | "testing" 21 | 22 | "github.com/cockroachdb/errors" 23 | "github.com/cockroachdb/errors/exthttp" 24 | "github.com/cockroachdb/errors/testutils" 25 | ) 26 | 27 | func TestHTTP(t *testing.T) { 28 | err := fmt.Errorf("hello") 29 | err = exthttp.WrapWithHTTPCode(err, 302) 30 | 31 | // Simulate a network transfer. 32 | enc := errors.EncodeError(context.Background(), err) 33 | otherErr := errors.DecodeError(context.Background(), enc) 34 | 35 | tt := testutils.T{T: t} 36 | 37 | // Error is preserved through the network. 38 | tt.CheckDeepEqual(otherErr, err) 39 | 40 | // It's possible to extract the HTTP code. 41 | tt.CheckEqual(exthttp.GetHTTPCode(otherErr, 100), 302) 42 | 43 | // If there are multiple codes, the most recent one wins. 44 | otherErr = exthttp.WrapWithHTTPCode(otherErr, 404) 45 | tt.CheckEqual(exthttp.GetHTTPCode(otherErr, 100), 404) 46 | 47 | // The code is hidden when the error is printed with %v. 48 | tt.CheckStringEqual(fmt.Sprintf("%v", err), `hello`) 49 | // The code appears when the error is printed verbosely. 50 | tt.CheckStringEqual(fmt.Sprintf("%+v", err), `hello 51 | (1) http code: 302 52 | Wraps: (2) hello 53 | Error types: (1) *exthttp.withHTTPCode (2) *errors.errorString`) 54 | } 55 | -------------------------------------------------------------------------------- /fmttests/datadriven_test_go116.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | // +build go1.16 16 | 17 | package fmttests 18 | 19 | func fakeGo116(s string) string { 20 | // We are at 1.16 already. Nothing to fake. 21 | return s 22 | } 23 | -------------------------------------------------------------------------------- /fmttests/datadriven_test_pre116.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | // +build !go1.16 16 | 17 | package fmttests 18 | 19 | import "strings" 20 | 21 | func fakeGo116(s string) string { 22 | // In Go 1.16, the canonical type for os.PathError is io/fs/*fs.PathError. 23 | // So when running the tests with a pre-1.16 runtime, the strings 24 | // emitted by printing out the error objects don't match the output 25 | // expected in the tests, which was generated with go 1.16. 26 | s = strings.ReplaceAll(s, "os/*os.PathError (*::)", "io/fs/*fs.PathError (os/*os.PathError::)") 27 | s = strings.ReplaceAll(s, " *os.PathError", " *fs.PathError") 28 | s = strings.ReplaceAll(s, "\n*os.PathError", "\n*fs.PathError") 29 | s = strings.ReplaceAll(s, "&os.PathError", "&fs.PathError") 30 | return s 31 | } 32 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/cockroachdb/errors 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.23.8 6 | 7 | require ( 8 | github.com/cockroachdb/datadriven v1.0.2 9 | github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b 10 | github.com/cockroachdb/redact v1.1.5 11 | github.com/getsentry/sentry-go v0.27.0 12 | github.com/gogo/googleapis v1.4.1 // gogoproto 1.2-compatible, for CRDB 13 | github.com/gogo/protobuf v1.3.2 14 | github.com/gogo/status v1.1.0 15 | github.com/hydrogen18/memlistener v1.0.0 16 | github.com/kr/pretty v0.3.1 17 | github.com/pkg/errors v0.9.1 18 | github.com/stretchr/testify v1.8.2 19 | google.golang.org/grpc v1.56.3 20 | google.golang.org/protobuf v1.33.0 21 | ) 22 | 23 | require ( 24 | github.com/davecgh/go-spew v1.1.1 // indirect 25 | github.com/golang/protobuf v1.5.3 // indirect 26 | github.com/kr/text v0.2.0 // indirect 27 | github.com/pmezard/go-difflib v1.0.0 // indirect 28 | github.com/rogpeppe/go-internal v1.9.0 // indirect 29 | golang.org/x/net v0.38.0 // indirect 30 | golang.org/x/sys v0.31.0 // indirect 31 | golang.org/x/text v0.23.0 // indirect 32 | google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect 33 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect 34 | gopkg.in/yaml.v3 v3.0.1 // indirect 35 | ) 36 | -------------------------------------------------------------------------------- /grpc/client_test.go: -------------------------------------------------------------------------------- 1 | package grpc 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/cockroachdb/errors" 10 | "github.com/cockroachdb/errors/grpc/status" 11 | "github.com/cockroachdb/errors/testutils" 12 | "google.golang.org/grpc/codes" 13 | ) 14 | 15 | func TestGrpc(t *testing.T) { 16 | 17 | tt := testutils.T{T: t} 18 | 19 | // A successful call should return the message, a nil error, and the status code should evaluate to codes.OK 20 | resp, err := Client.Echo(context.Background(), &EchoRequest{Text: "hello"}) 21 | tt.Assert(err == nil) 22 | tt.Assert(resp.Reply == "echoing: hello") 23 | tt.Assert(status.Code(err) == codes.OK) 24 | 25 | // A sentinel error should be detectable across grpc boundaries 26 | // A failed call that does not have a status specified should evaluate to codes.Unknown 27 | _, err = Client.Echo(context.Background(), &EchoRequest{Text: "noecho"}) 28 | tt.Assert(err != nil) 29 | tt.Assert(errors.Is(err, ErrCantEcho)) 30 | tt.Assert(status.Code(err) == codes.Unknown) 31 | 32 | // A wrapped error should be unwrappable after crossing grpc boundaries 33 | _, err = Client.Echo(context.Background(), &EchoRequest{Text: "really_long_message"}) 34 | tt.Assert(err != nil) 35 | tt.Assert(err.Error() == "really_long_message is too long: text is too long") 36 | tt.Assert(errors.Is(err, ErrTooLong)) 37 | tt.Assert(errors.UnwrapAll(err).Error() == "text is too long") 38 | 39 | // A failed call with a specified status should evaluate correctly after crossing a grpc boundary 40 | _, err = Client.Echo(context.Background(), &EchoRequest{Text: "reverse"}) 41 | tt.Assert(err != nil) 42 | tt.Assert(err.Error() == "reverse is not implemented") 43 | tt.Assert(status.Code(err) == codes.Unimplemented) 44 | 45 | // Sentinel error and status code in the same response 46 | // Printing the error out with detail should include the grpc status 47 | _, err = Client.Echo(context.Background(), &EchoRequest{Text: "internal"}) 48 | tt.Assert(err != nil) 49 | tt.Assert(err.Error() == "there was a problem: internal error!") 50 | tt.Assert(status.Code(err) == codes.Internal) 51 | tt.Assert(errors.Is(err, ErrInternal)) 52 | spv := fmt.Sprintf("%+v", err) 53 | t.Logf("spv:\n%s", spv) 54 | tt.Assert(strings.Contains(spv, "gRPC code: Internal")) 55 | } 56 | -------------------------------------------------------------------------------- /grpc/echoer.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package grpc; 4 | 5 | service Echoer { 6 | rpc Echo (EchoRequest) returns (EchoReply) {} 7 | } 8 | 9 | message EchoRequest { 10 | string text = 1; 11 | } 12 | 13 | message EchoReply { 14 | string reply = 1; 15 | } 16 | -------------------------------------------------------------------------------- /grpc/main_test.go: -------------------------------------------------------------------------------- 1 | package grpc 2 | 3 | import ( 4 | "net" 5 | "os" 6 | "testing" 7 | "time" 8 | 9 | "google.golang.org/grpc" 10 | 11 | "github.com/cockroachdb/errors/grpc/middleware" 12 | "github.com/hydrogen18/memlistener" 13 | ) 14 | 15 | var ( 16 | Client EchoerClient 17 | ) 18 | 19 | func TestMain(m *testing.M) { 20 | 21 | srv := &EchoServer{} 22 | 23 | lis := memlistener.NewMemoryListener() 24 | 25 | grpcServer := grpc.NewServer(grpc.UnaryInterceptor(middleware.UnaryServerInterceptor)) 26 | RegisterEchoerServer(grpcServer, srv) 27 | 28 | go grpcServer.Serve(lis) 29 | 30 | dialOpts := []grpc.DialOption{ 31 | grpc.WithDialer(func(target string, d time.Duration) (net.Conn, error) { 32 | return lis.Dial("", "") 33 | }), 34 | grpc.WithInsecure(), 35 | grpc.WithUnaryInterceptor(middleware.UnaryClientInterceptor), 36 | } 37 | 38 | clientConn, err := grpc.Dial("", dialOpts...) 39 | if err != nil { 40 | panic(err) 41 | } 42 | 43 | Client = NewEchoerClient(clientConn) 44 | 45 | code := m.Run() 46 | 47 | grpcServer.Stop() 48 | clientConn.Close() 49 | 50 | os.Exit(code) 51 | } 52 | -------------------------------------------------------------------------------- /grpc/middleware/client.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/cockroachdb/errors" 7 | "github.com/gogo/status" 8 | 9 | "google.golang.org/grpc" 10 | ) 11 | 12 | func UnaryClientInterceptor( 13 | ctx context.Context, 14 | method string, 15 | req interface{}, 16 | reply interface{}, 17 | cc *grpc.ClientConn, 18 | invoker grpc.UnaryInvoker, 19 | opts ...grpc.CallOption, 20 | ) error { 21 | err := invoker(ctx, method, req, reply, cc, opts...) 22 | 23 | st := status.Convert(err) 24 | var reconstituted error 25 | for _, det := range st.Details() { 26 | switch t := det.(type) { 27 | case *errors.EncodedError: 28 | reconstituted = errors.DecodeError(ctx, *t) 29 | } 30 | } 31 | 32 | if reconstituted != nil { 33 | err = reconstituted 34 | } 35 | 36 | return err 37 | } 38 | -------------------------------------------------------------------------------- /grpc/middleware/server.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/cockroachdb/errors" 7 | "github.com/cockroachdb/errors/extgrpc" 8 | "github.com/gogo/status" 9 | 10 | "google.golang.org/grpc" 11 | ) 12 | 13 | func UnaryServerInterceptor( 14 | ctx context.Context, 15 | req interface{}, 16 | info *grpc.UnaryServerInfo, 17 | handler grpc.UnaryHandler, 18 | ) (interface{}, error) { 19 | resp, err := handler(ctx, req) 20 | if err == nil { 21 | return resp, err 22 | } 23 | 24 | st, ok := status.FromError(err) 25 | if !ok { 26 | code := extgrpc.GetGrpcCode(err) 27 | st = status.New(code, err.Error()) 28 | enc := errors.EncodeError(ctx, err) 29 | st, err = st.WithDetails(&enc) 30 | if err != nil { 31 | 32 | // https://jbrandhorst.com/post/grpc-errors/ 33 | // "If this errored, it will always error 34 | // here, so better panic so we can figure 35 | // out why than have this silently passing." 36 | // 37 | // More specifically, an error here is from ptypes.MarshalAny(detail), which probably 38 | // means that your proto.Message is not registered with gogoproto. (Make sure that 39 | // your error's .pb.go file imports "github.com/gogo/protobuf/proto".) 40 | // 41 | // By panicking, we either take down the service or (if it has a recovery middleware) cause 42 | // the call to fail dramatically. Either case will draw attention to get it fixed. 43 | // 44 | // If we simply returned an errors.AssertionFailed, our entire error stack would vanish 45 | // as it crosses the network boundary. A client would receive a grpc status with code.Internal, 46 | // and the stringification of the error. This change in behavior could induce subtle bugs 47 | // in the client since none of the usual errors are being returned. 48 | // 49 | // We could also log the error here via whatever appropriate mechanism, but the truth is 50 | // that the service was seriously misconfigured and shouldn't be running at all. 51 | // 52 | panic(err) 53 | } 54 | } 55 | 56 | return resp, st.Err() 57 | } 58 | -------------------------------------------------------------------------------- /grpc/server_test.go: -------------------------------------------------------------------------------- 1 | package grpc 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/cockroachdb/errors" 7 | "github.com/cockroachdb/errors/grpc/status" 8 | 9 | "google.golang.org/grpc/codes" 10 | ) 11 | 12 | var ErrCantEcho = errors.New("unable to echo") 13 | var ErrTooLong = errors.New("text is too long") 14 | var ErrInternal = errors.New("internal error!") 15 | 16 | type EchoServer struct { 17 | } 18 | 19 | func (srv *EchoServer) Echo(ctx context.Context, req *EchoRequest) (*EchoReply, error) { 20 | msg := req.Text 21 | switch { 22 | case msg == "noecho": 23 | return nil, ErrCantEcho 24 | case len(msg) > 10: 25 | return nil, errors.WithMessage(ErrTooLong, msg+" is too long") 26 | case msg == "reverse": 27 | return nil, status.Error(codes.Unimplemented, "reverse is not implemented") 28 | case msg == "internal": 29 | return nil, status.WrapErr(codes.Internal, "there was a problem", ErrInternal) 30 | } 31 | return &EchoReply{ 32 | Reply: "echoing: " + msg, 33 | }, nil 34 | } 35 | -------------------------------------------------------------------------------- /grpc/status/status.go: -------------------------------------------------------------------------------- 1 | package status 2 | 3 | import ( 4 | "github.com/cockroachdb/errors" 5 | "github.com/cockroachdb/errors/extgrpc" 6 | 7 | "google.golang.org/grpc/codes" 8 | ) 9 | 10 | func Error(c codes.Code, msg string) error { 11 | return extgrpc.WrapWithGrpcCode(errors.New(msg), c) 12 | } 13 | 14 | func Errorf(c codes.Code, format string, args ...interface{}) error { 15 | return extgrpc.WrapWithGrpcCode(errors.Newf(format, args...), c) 16 | } 17 | 18 | func WrapErr(c codes.Code, msg string, err error) error { 19 | return extgrpc.WrapWithGrpcCode(errors.WrapWithDepth(1, err, msg), c) 20 | } 21 | 22 | func WrapErrf(c codes.Code, err error, format string, args ...interface{}) error { 23 | return extgrpc.WrapWithGrpcCode(errors.WrapWithDepthf(1, err, format, args...), c) 24 | } 25 | 26 | func Code(err error) codes.Code { 27 | return extgrpc.GetGrpcCode(err) 28 | } 29 | -------------------------------------------------------------------------------- /hintdetail/hintdetail.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | package hintdetail 16 | 17 | import ( 18 | "bytes" 19 | "fmt" 20 | 21 | "github.com/cockroachdb/errors/errbase" 22 | ) 23 | 24 | // WithHint decorates an error with a textual hint. 25 | // The hint may contain PII and thus will not reportable. 26 | // The suggested use case for hint is to relay information to end users. 27 | // 28 | // Hint is shown: 29 | // - when formatting with `%+v`. 30 | // - with `GetAllHints()` / `FlattenHints()` below. 31 | // 32 | // Note: the hint does not appear in the main error message returned 33 | // with Error(). Use GetAllHints() or FlattenHints() to retrieve it. 34 | func WithHint(err error, msg string) error { 35 | if err == nil { 36 | return nil 37 | } 38 | 39 | return &withHint{cause: err, hint: msg} 40 | } 41 | 42 | // WithHintf is a helper that formats the hint. 43 | func WithHintf(err error, format string, args ...interface{}) error { 44 | if err == nil { 45 | return nil 46 | } 47 | 48 | return &withHint{cause: err, hint: fmt.Sprintf(format, args...)} 49 | } 50 | 51 | // GetAllHints retrieves the hints from the error using in post-order 52 | // traversal. The hints are de-duplicated. Assertion failures, issue 53 | // links and unimplemented errors are detected and receive standard 54 | // hints. 55 | func GetAllHints(err error) []string { 56 | return getAllHintsInternal(err, nil, make(map[string]struct{})) 57 | } 58 | 59 | // FlattenHints retrieves the hints as per GetAllHints() and 60 | // concatenates them into a single string. 61 | func FlattenHints(err error) string { 62 | var b bytes.Buffer 63 | sep := "" 64 | for _, h := range GetAllHints(err) { 65 | b.WriteString(sep) 66 | b.WriteString(h) 67 | sep = "\n--\n" 68 | } 69 | return b.String() 70 | } 71 | 72 | func getAllHintsInternal(err error, hints []string, seen map[string]struct{}) []string { 73 | if c := errbase.UnwrapOnce(err); c != nil { 74 | hints = getAllHintsInternal(c, hints, seen) 75 | } 76 | 77 | hint := "" 78 | if w, ok := err.(ErrorHinter); ok { 79 | hint = w.ErrorHint() 80 | } 81 | 82 | if hint != "" { 83 | // De-duplicate hints. 84 | if _, ok := seen[hint]; !ok { 85 | hints = append(hints, hint) 86 | seen[hint] = struct{}{} 87 | } 88 | } 89 | return hints 90 | } 91 | 92 | // ErrorHinter is implemented by types that can provide 93 | // user-informing detail strings. This is implemented by withHint 94 | // here, withIssueLink, assertionFailure and pgerror.Error. 95 | type ErrorHinter interface { 96 | ErrorHint() string 97 | } 98 | 99 | // WithDetail decorates an error with a textual detail. 100 | // The detail may contain PII and thus will not reportable. 101 | // The suggested use case for detail is to augment errors with information 102 | // useful for debugging. 103 | // 104 | // Detail is shown: 105 | // - when formatting with `%+v`. 106 | // - with `GetAllDetails()` / `FlattenDetails()` below. 107 | // 108 | // Note: the detail does not appear in the main error message returned 109 | // with Error(). Use GetAllDetails() or FlattenDetails() to retrieve 110 | // it. 111 | func WithDetail(err error, msg string) error { 112 | if err == nil { 113 | return nil 114 | } 115 | 116 | return &withDetail{cause: err, detail: msg} 117 | } 118 | 119 | // WithDetailf is a helper that formats the detail string. 120 | func WithDetailf(err error, format string, args ...interface{}) error { 121 | if err == nil { 122 | return nil 123 | } 124 | 125 | return &withDetail{cause: err, detail: fmt.Sprintf(format, args...)} 126 | } 127 | 128 | // GetAllDetails retrieves the details from the error using in post-order 129 | // traversal. 130 | func GetAllDetails(err error) []string { 131 | return getAllDetailsInternal(err, nil) 132 | } 133 | 134 | // FlattenDetails retrieves the details as per GetAllDetails() and 135 | // concatenates them into a single string. 136 | func FlattenDetails(err error) string { 137 | var b bytes.Buffer 138 | sep := "" 139 | for _, h := range GetAllDetails(err) { 140 | b.WriteString(sep) 141 | b.WriteString(h) 142 | sep = "\n--\n" 143 | } 144 | return b.String() 145 | } 146 | 147 | func getAllDetailsInternal(err error, details []string) []string { 148 | if c := errbase.UnwrapOnce(err); c != nil { 149 | details = getAllDetailsInternal(c, details) 150 | } 151 | if w, ok := err.(ErrorDetailer); ok { 152 | d := w.ErrorDetail() 153 | if d != "" { 154 | details = append(details, w.ErrorDetail()) 155 | } 156 | } 157 | return details 158 | } 159 | 160 | // ErrorDetailer is implemented by types that can provide 161 | // user-informing detail strings. This is implemented by withDetail 162 | // here and pgerror.Error. 163 | type ErrorDetailer interface { 164 | ErrorDetail() string 165 | } 166 | -------------------------------------------------------------------------------- /hintdetail/with_detail.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | package hintdetail 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | 21 | "github.com/cockroachdb/errors/errbase" 22 | "github.com/cockroachdb/errors/errorspb" 23 | "github.com/gogo/protobuf/proto" 24 | ) 25 | 26 | type withDetail struct { 27 | cause error 28 | detail string 29 | } 30 | 31 | var _ error = (*withDetail)(nil) 32 | var _ ErrorDetailer = (*withDetail)(nil) 33 | var _ fmt.Formatter = (*withDetail)(nil) 34 | var _ errbase.Formatter = (*withDetail)(nil) 35 | 36 | func (w *withDetail) ErrorDetail() string { return w.detail } 37 | func (w *withDetail) Error() string { return w.cause.Error() } 38 | func (w *withDetail) Cause() error { return w.cause } 39 | func (w *withDetail) Unwrap() error { return w.cause } 40 | 41 | func (w *withDetail) Format(s fmt.State, verb rune) { errbase.FormatError(w, s, verb) } 42 | 43 | func (w *withDetail) FormatError(p errbase.Printer) error { 44 | if p.Detail() { 45 | p.Print(w.detail) 46 | } 47 | return w.cause 48 | } 49 | 50 | func encodeWithDetail(_ context.Context, err error) (string, []string, proto.Message) { 51 | w := err.(*withDetail) 52 | return "", nil, &errorspb.StringPayload{Msg: w.detail} 53 | } 54 | 55 | func decodeWithDetail( 56 | _ context.Context, cause error, _ string, _ []string, payload proto.Message, 57 | ) error { 58 | m, ok := payload.(*errorspb.StringPayload) 59 | if !ok { 60 | // If this ever happens, this means some version of the library 61 | // (presumably future) changed the payload type, and we're 62 | // receiving this here. In this case, give up and let 63 | // DecodeError use the opaque type. 64 | return nil 65 | } 66 | return &withDetail{cause: cause, detail: m.Msg} 67 | } 68 | 69 | func init() { 70 | errbase.RegisterWrapperEncoder(errbase.GetTypeKey((*withDetail)(nil)), encodeWithDetail) 71 | errbase.RegisterWrapperDecoder(errbase.GetTypeKey((*withDetail)(nil)), decodeWithDetail) 72 | } 73 | -------------------------------------------------------------------------------- /hintdetail/with_hint.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | package hintdetail 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | 21 | "github.com/cockroachdb/errors/errbase" 22 | "github.com/cockroachdb/errors/errorspb" 23 | "github.com/gogo/protobuf/proto" 24 | ) 25 | 26 | type withHint struct { 27 | cause error 28 | hint string 29 | } 30 | 31 | var _ error = (*withHint)(nil) 32 | var _ ErrorHinter = (*withHint)(nil) 33 | var _ fmt.Formatter = (*withHint)(nil) 34 | var _ errbase.Formatter = (*withHint)(nil) 35 | 36 | func (w *withHint) ErrorHint() string { return w.hint } 37 | func (w *withHint) Error() string { return w.cause.Error() } 38 | func (w *withHint) Cause() error { return w.cause } 39 | func (w *withHint) Unwrap() error { return w.cause } 40 | 41 | func (w *withHint) Format(s fmt.State, verb rune) { errbase.FormatError(w, s, verb) } 42 | 43 | func (w *withHint) FormatError(p errbase.Printer) error { 44 | if p.Detail() { 45 | p.Print(w.hint) 46 | } 47 | return w.cause 48 | } 49 | 50 | func encodeWithHint(_ context.Context, err error) (string, []string, proto.Message) { 51 | w := err.(*withHint) 52 | return "", nil, &errorspb.StringPayload{Msg: w.hint} 53 | } 54 | 55 | func decodeWithHint( 56 | _ context.Context, cause error, _ string, _ []string, payload proto.Message, 57 | ) error { 58 | m, ok := payload.(*errorspb.StringPayload) 59 | if !ok { 60 | // If this ever happens, this means some version of the library 61 | // (presumably future) changed the payload type, and we're 62 | // receiving this here. In this case, give up and let 63 | // DecodeError use the opaque type. 64 | return nil 65 | } 66 | return &withHint{cause: cause, hint: m.Msg} 67 | } 68 | 69 | func init() { 70 | errbase.RegisterWrapperEncoder(errbase.GetTypeKey((*withHint)(nil)), encodeWithHint) 71 | errbase.RegisterWrapperDecoder(errbase.GetTypeKey((*withHint)(nil)), decodeWithHint) 72 | } 73 | -------------------------------------------------------------------------------- /hintdetail_api.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | package errors 16 | 17 | import "github.com/cockroachdb/errors/hintdetail" 18 | 19 | // ErrorHinter is implemented by types that can provide 20 | // user-informing detail strings. This is implemented by withHint 21 | // here, withIssueLink, assertionFailure and pgerror.Error. 22 | type ErrorHinter = hintdetail.ErrorHinter 23 | 24 | // ErrorDetailer is implemented by types that can provide 25 | // user-informing detail strings. 26 | type ErrorDetailer = hintdetail.ErrorDetailer 27 | 28 | // WithHint decorates an error with a textual hint. 29 | // The hint may contain PII and thus will not reportable. 30 | // The suggested use case for hint is to relay information to end users. 31 | // 32 | // Hint is shown: 33 | // - when formatting with `%+v`. 34 | // - with `GetAllHints()` / `FlattenHints()` below. 35 | // 36 | // Note: the hint does not appear in the main error message returned 37 | // with Error(). Use GetAllHints() or FlattenHints() to retrieve it. 38 | func WithHint(err error, msg string) error { return hintdetail.WithHint(err, msg) } 39 | 40 | // WithHintf is a helper that formats the hint. 41 | // See the documentation of WithHint() for details. 42 | func WithHintf(err error, format string, args ...interface{}) error { 43 | return hintdetail.WithHintf(err, format, args...) 44 | } 45 | 46 | // WithDetail decorates an error with a textual detail. 47 | // The detail may contain PII and thus will not reportable. 48 | // The suggested use case for detail is to augment errors with information 49 | // useful for debugging. 50 | // 51 | // Detail is shown: 52 | // - when formatting with `%+v`. 53 | // - with `GetAllDetails()` / `FlattenDetails()` below. 54 | // 55 | // Note: the detail does not appear in the main error message returned 56 | // with Error(). Use GetAllDetails() or FlattenDetails() to retrieve 57 | // it. 58 | func WithDetail(err error, msg string) error { return hintdetail.WithDetail(err, msg) } 59 | 60 | // WithDetailf is a helper that formats the detail string. 61 | // See the documentation of WithDetail() for details. 62 | func WithDetailf(err error, format string, args ...interface{}) error { 63 | return hintdetail.WithDetailf(err, format, args...) 64 | } 65 | 66 | // GetAllHints retrieves the hints from the error using in post-order 67 | // traversal. The hints are de-duplicated. Assertion failures, issue 68 | // links and unimplemented errors are detected and receive standard 69 | // hints. 70 | func GetAllHints(err error) []string { return hintdetail.GetAllHints(err) } 71 | 72 | // FlattenHints retrieves the hints as per GetAllHints() and 73 | // concatenates them into a single string. 74 | func FlattenHints(err error) string { return hintdetail.FlattenHints(err) } 75 | 76 | // GetAllDetails retrieves the details from the error using in post-order 77 | // traversal. 78 | func GetAllDetails(err error) []string { return hintdetail.GetAllDetails(err) } 79 | 80 | // FlattenDetails retrieves the details as per GetAllDetails() and 81 | // concatenates them into a single string. 82 | func FlattenDetails(err error) string { return hintdetail.FlattenDetails(err) } 83 | -------------------------------------------------------------------------------- /issuelink/issuelink.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | package issuelink 16 | 17 | import ( 18 | "fmt" 19 | 20 | "github.com/cockroachdb/errors/errbase" 21 | "github.com/cockroachdb/errors/markers" 22 | ) 23 | 24 | // WithIssueLink adds an annotation to a know issue 25 | // on a web issue tracker. 26 | // 27 | // The url and detail strings may contain PII and will 28 | // be considered reportable. 29 | // 30 | // Detail is shown: 31 | // - via `errors.GetSafeDetails()` 32 | // - when formatting with `%+v`. 33 | // - in Sentry reports. 34 | // - via `errors.GetAllHints()` / `errors.FlattenHints()` 35 | func WithIssueLink(err error, issue IssueLink) error { 36 | if err == nil { 37 | return nil 38 | } 39 | return &withIssueLink{cause: err, IssueLink: issue} 40 | } 41 | 42 | // HasIssueLink returns true iff the error or one of its 43 | // causes has a linked issue payload. 44 | func HasIssueLink(err error) bool { 45 | _, ok := markers.If(err, func(err error) (v interface{}, ok bool) { 46 | v, ok = err.(*withIssueLink) 47 | return 48 | }) 49 | return ok 50 | } 51 | 52 | // IsIssueLink returns true iff the error (not its 53 | // causes) has a linked issue payload. 54 | func IsIssueLink(err error) bool { 55 | _, ok := err.(*withIssueLink) 56 | return ok 57 | } 58 | 59 | // GetAllIssueLinks retrieves the linked issue carried 60 | // by the error or its direct causes. 61 | func GetAllIssueLinks(err error) (issues []IssueLink) { 62 | for ; err != nil; err = errbase.UnwrapOnce(err) { 63 | if issue, ok := GetIssueLink(err); ok { 64 | issues = append(issues, issue) 65 | } 66 | } 67 | return 68 | } 69 | 70 | // UnimplementedError creates a new leaf error that indicates that 71 | // some feature was not (yet) implemented. 72 | // 73 | // Detail is shown: 74 | // - via `errors.GetSafeDetails()` 75 | // - when formatting with `%+v`. 76 | // - in Sentry reports. 77 | // - via `errors.GetAllHints()` / `errors.FlattenHints()` 78 | func UnimplementedError(issueLink IssueLink, msg string) error { 79 | return &unimplementedError{IssueLink: issueLink, msg: msg} 80 | } 81 | 82 | // UnimplementedErrorf creates a new leaf error that indicates that 83 | // some feature was not (yet) implemented. The message is formatted. 84 | func UnimplementedErrorf(issueLink IssueLink, format string, args ...interface{}) error { 85 | return &unimplementedError{IssueLink: issueLink, msg: fmt.Sprintf(format, args...)} 86 | } 87 | 88 | // IsUnimplementedError returns iff if err is an unimplemented error. 89 | func IsUnimplementedError(err error) bool { 90 | _, ok := err.(*unimplementedError) 91 | return ok 92 | } 93 | 94 | // HasUnimplementedError returns iff if err or its cause is an 95 | // unimplemented error. 96 | func HasUnimplementedError(err error) bool { 97 | _, ok := errbase.UnwrapAll(err).(*unimplementedError) 98 | return ok 99 | } 100 | 101 | // GetIssueLink retrieves the linked issue annotation carried 102 | // by the error, or false if there is no such annotation. 103 | func GetIssueLink(err error) (IssueLink, bool) { 104 | switch w := err.(type) { 105 | case *withIssueLink: 106 | return w.IssueLink, true 107 | case *unimplementedError: 108 | return w.IssueLink, true 109 | } 110 | return IssueLink{}, false 111 | } 112 | 113 | // IssueLink is the payload for a linked issue annotation. 114 | type IssueLink struct { 115 | // URL to the issue on a tracker. 116 | IssueURL string 117 | // Annotation that characterizes a sub-issue. 118 | Detail string 119 | } 120 | -------------------------------------------------------------------------------- /issuelink/unimplemented_error.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | package issuelink 16 | 17 | import ( 18 | "bytes" 19 | "context" 20 | "fmt" 21 | 22 | "github.com/cockroachdb/errors/errbase" 23 | "github.com/cockroachdb/redact" 24 | "github.com/gogo/protobuf/proto" 25 | ) 26 | 27 | type unimplementedError struct { 28 | // For now, msg is non-reportable. 29 | msg string 30 | IssueLink 31 | } 32 | 33 | var _ error = (*unimplementedError)(nil) 34 | var _ fmt.Formatter = (*unimplementedError)(nil) 35 | var _ errbase.SafeFormatter = (*unimplementedError)(nil) 36 | var _ errbase.SafeDetailer = (*unimplementedError)(nil) 37 | 38 | func (w *unimplementedError) Error() string { return w.msg } 39 | func (w *unimplementedError) SafeDetails() []string { 40 | return []string{w.IssueURL, w.Detail} 41 | } 42 | 43 | // ErrorHint implements the hintdetail.ErrorHinter interface. 44 | func (w *unimplementedError) ErrorHint() string { 45 | var hintText bytes.Buffer 46 | hintText.WriteString(UnimplementedErrorHint) 47 | maybeAppendReferral(&hintText, w.IssueLink) 48 | return hintText.String() 49 | } 50 | 51 | // UnimplementedErrorHint is the hint emitted upon unimplemented errors. 52 | const UnimplementedErrorHint = `You have attempted to use a feature that is not yet implemented.` 53 | 54 | func (w *unimplementedError) Format(s fmt.State, verb rune) { errbase.FormatError(w, s, verb) } 55 | 56 | func (w *unimplementedError) SafeFormatError(p errbase.Printer) error { 57 | // For now, msg is non-reportable. 58 | p.Print(w.msg) 59 | if p.Detail() { 60 | // But the details are. 61 | p.Printf("unimplemented") 62 | if w.IssueURL != "" { 63 | p.Printf("\nissue: %s", redact.Safe(w.IssueURL)) 64 | } 65 | if w.Detail != "" { 66 | p.Printf("\ndetail: %s", redact.Safe(w.Detail)) 67 | } 68 | } 69 | return nil 70 | } 71 | 72 | func decodeUnimplementedError( 73 | _ context.Context, msg string, details []string, _ proto.Message, 74 | ) error { 75 | var issueLink IssueLink 76 | if len(details) > 0 { 77 | issueLink.IssueURL = details[0] 78 | } 79 | if len(details) > 1 { 80 | issueLink.Detail = details[1] 81 | } 82 | return &unimplementedError{msg: msg, IssueLink: issueLink} 83 | } 84 | 85 | func init() { 86 | errbase.RegisterLeafDecoder(errbase.GetTypeKey((*unimplementedError)(nil)), decodeUnimplementedError) 87 | } 88 | -------------------------------------------------------------------------------- /issuelink/unimplemented_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | package issuelink_test 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | "strings" 21 | "testing" 22 | 23 | "github.com/cockroachdb/errors/errbase" 24 | "github.com/cockroachdb/errors/issuelink" 25 | "github.com/cockroachdb/errors/markers" 26 | "github.com/cockroachdb/errors/testutils" 27 | "github.com/pkg/errors" 28 | ) 29 | 30 | func TestUnimplementedError(t *testing.T) { 31 | tt := testutils.T{T: t} 32 | 33 | err := issuelink.UnimplementedError(issuelink.IssueLink{IssueURL: "123", Detail: "foo"}, "world") 34 | 35 | err = errors.Wrap(err, "hello") 36 | 37 | theTest := func(tt testutils.T, err error) { 38 | tt.Check(issuelink.HasUnimplementedError(err)) 39 | tt.Check(issuelink.IsUnimplementedError(errbase.UnwrapAll(err))) 40 | if _, ok := markers.If(err, func(err error) (interface{}, bool) { return nil, issuelink.IsUnimplementedError(err) }); !ok { 41 | t.Error("woops") 42 | } 43 | 44 | details := issuelink.GetAllIssueLinks(err) 45 | tt.CheckDeepEqual(details, []issuelink.IssueLink{ 46 | {IssueURL: "123", Detail: "foo"}, 47 | }) 48 | 49 | errV := fmt.Sprintf("%+v", err) 50 | tt.Check(strings.Contains(errV, "issue: 123")) 51 | tt.Check(strings.Contains(errV, "detail: foo")) 52 | } 53 | 54 | tt.Run("local", func(tt testutils.T) { theTest(tt, err) }) 55 | 56 | enc := errbase.EncodeError(context.Background(), err) 57 | newErr := errbase.DecodeError(context.Background(), enc) 58 | 59 | tt.Run("remote", func(tt testutils.T) { theTest(tt, newErr) }) 60 | 61 | } 62 | 63 | func TestFormatUnimp(t *testing.T) { 64 | tt := testutils.T{t} 65 | 66 | link := issuelink.IssueLink{IssueURL: "http://mysite"} 67 | const woo = `woo` 68 | const waawoo = `waa: woo` 69 | testCases := []struct { 70 | name string 71 | err error 72 | expFmtSimple string 73 | expFmtVerbose string 74 | }{ 75 | {"unimp", 76 | issuelink.UnimplementedError(link, "woo"), 77 | woo, ` 78 | woo 79 | (1) woo 80 | | unimplemented 81 | | issue: http://mysite 82 | Error types: (1) *issuelink.unimplementedError`}, 83 | {"unimp-details", 84 | issuelink.UnimplementedError(issuelink.IssueLink{IssueURL: "http://mysite", Detail: "see more"}, "woo"), 85 | woo, ` 86 | woo 87 | (1) woo 88 | | unimplemented 89 | | issue: http://mysite 90 | | detail: see more 91 | Error types: (1) *issuelink.unimplementedError`}, 92 | 93 | {"wrapper + unimp", 94 | &werrFmt{issuelink.UnimplementedError(link, "woo"), "waa"}, 95 | waawoo, ` 96 | waa: woo 97 | (1) waa 98 | | -- this is waa's 99 | | multi-line payload 100 | Wraps: (2) woo 101 | | unimplemented 102 | | issue: http://mysite 103 | Error types: (1) *issuelink_test.werrFmt (2) *issuelink.unimplementedError`}, 104 | } 105 | 106 | for _, test := range testCases { 107 | tt.Run(test.name, func(tt testutils.T) { 108 | err := test.err 109 | 110 | // %s is simple formatting 111 | tt.CheckStringEqual(fmt.Sprintf("%s", err), test.expFmtSimple) 112 | 113 | // %v is simple formatting too, for compatibility with the past. 114 | tt.CheckStringEqual(fmt.Sprintf("%v", err), test.expFmtSimple) 115 | 116 | // %q is always like %s but quotes the result. 117 | ref := fmt.Sprintf("%q", test.expFmtSimple) 118 | tt.CheckStringEqual(fmt.Sprintf("%q", err), ref) 119 | 120 | // %+v is the verbose mode. 121 | refV := strings.TrimPrefix(test.expFmtVerbose, "\n") 122 | spv := fmt.Sprintf("%+v", err) 123 | tt.CheckStringEqual(spv, refV) 124 | }) 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /issuelink/with_issuelink.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | package issuelink 16 | 17 | import ( 18 | "bytes" 19 | "context" 20 | "fmt" 21 | 22 | "github.com/cockroachdb/errors/errbase" 23 | "github.com/cockroachdb/errors/stdstrings" 24 | "github.com/cockroachdb/redact" 25 | "github.com/gogo/protobuf/proto" 26 | ) 27 | 28 | type withIssueLink struct { 29 | cause error 30 | IssueLink 31 | } 32 | 33 | var _ error = (*withIssueLink)(nil) 34 | var _ errbase.SafeDetailer = (*withIssueLink)(nil) 35 | var _ fmt.Formatter = (*withIssueLink)(nil) 36 | var _ errbase.SafeFormatter = (*withIssueLink)(nil) 37 | 38 | func (w *withIssueLink) Error() string { return w.cause.Error() } 39 | func (w *withIssueLink) Cause() error { return w.cause } 40 | func (w *withIssueLink) Unwrap() error { return w.cause } 41 | 42 | func (w *withIssueLink) SafeDetails() []string { 43 | return []string{w.IssueURL, w.Detail} 44 | } 45 | 46 | // ErrorHint implements the hintdetail.ErrorHinter interface. 47 | func (w *withIssueLink) ErrorHint() string { 48 | var hintText bytes.Buffer 49 | maybeAppendReferral(&hintText, w.IssueLink) 50 | return hintText.String() 51 | } 52 | 53 | func maybeAppendReferral(buf *bytes.Buffer, link IssueLink) { 54 | if link.IssueURL != "" { 55 | // If there is a URL, refer to that. 56 | if buf.Len() > 0 { 57 | buf.WriteByte('\n') 58 | } 59 | fmt.Fprintf(buf, "See: %s", link.IssueURL) 60 | } else { 61 | // No URL: tell the user to send details. 62 | buf.WriteString(stdstrings.IssueReferral) 63 | } 64 | } 65 | 66 | func (w *withIssueLink) Format(s fmt.State, verb rune) { errbase.FormatError(w, s, verb) } 67 | 68 | func (w *withIssueLink) SafeFormatError(p errbase.Printer) error { 69 | if p.Detail() { 70 | sep := redact.SafeString("") 71 | if w.IssueURL != "" { 72 | p.Printf("issue: %s", redact.Safe(w.IssueURL)) 73 | sep = "\n" 74 | } 75 | if w.Detail != "" { 76 | p.Printf("%sdetail: %s", sep, redact.Safe(w.Detail)) 77 | } 78 | } 79 | return w.cause 80 | } 81 | 82 | func decodeWithIssueLink( 83 | _ context.Context, cause error, _ string, details []string, _ proto.Message, 84 | ) error { 85 | var issueLink IssueLink 86 | if len(details) > 0 { 87 | issueLink.IssueURL = details[0] 88 | } 89 | if len(details) > 1 { 90 | issueLink.Detail = details[1] 91 | } 92 | return &withIssueLink{cause: cause, IssueLink: issueLink} 93 | } 94 | 95 | func init() { 96 | errbase.RegisterWrapperDecoder(errbase.GetTypeKey((*withIssueLink)(nil)), decodeWithIssueLink) 97 | } 98 | -------------------------------------------------------------------------------- /issuelink_api.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | package errors 16 | 17 | import "github.com/cockroachdb/errors/issuelink" 18 | 19 | // WithIssueLink adds an annotation to a know issue 20 | // on a web issue tracker. 21 | // 22 | // The url and detail strings may contain PII and will 23 | // be considered reportable. 24 | // 25 | // Detail is shown: 26 | // - via `errors.GetSafeDetails()` 27 | // - when formatting with `%+v`. 28 | // - in Sentry reports. 29 | // - via `errors.GetAllHints()` / `errors.FlattenHints()` 30 | func WithIssueLink(err error, issue IssueLink) error { return issuelink.WithIssueLink(err, issue) } 31 | 32 | // IssueLink is the payload for a linked issue annotation. 33 | type IssueLink = issuelink.IssueLink 34 | 35 | // UnimplementedError creates a new leaf error that indicates that 36 | // some feature was not (yet) implemented. 37 | // 38 | // Detail is shown: 39 | // - via `errors.GetSafeDetails()` 40 | // - when formatting with `%+v`. 41 | // - in Sentry reports. 42 | // - via `errors.GetAllHints()` / `errors.FlattenHints()` 43 | func UnimplementedError(issueLink IssueLink, msg string) error { 44 | return issuelink.UnimplementedError(issueLink, msg) 45 | } 46 | 47 | // UnimplementedErrorf creates a new leaf error that indicates that 48 | // some feature was not (yet) implemented. The message is formatted. 49 | func UnimplementedErrorf(issueLink IssueLink, format string, args ...interface{}) error { 50 | return issuelink.UnimplementedErrorf(issueLink, format, args...) 51 | } 52 | 53 | // GetAllIssueLinks retrieves the linked issue carried 54 | // by the error or its direct causes. 55 | func GetAllIssueLinks(err error) (issues []IssueLink) { return issuelink.GetAllIssueLinks(err) } 56 | 57 | // HasIssueLink returns true iff the error or one of its 58 | // causes has a linked issue payload. 59 | func HasIssueLink(err error) bool { return issuelink.HasIssueLink(err) } 60 | 61 | // IsIssueLink returns true iff the error (not its 62 | // causes) has a linked issue payload. 63 | func IsIssueLink(err error) bool { return issuelink.IsIssueLink(err) } 64 | 65 | // HasUnimplementedError returns iff if err or its cause is an 66 | // unimplemented error. 67 | func HasUnimplementedError(err error) bool { return issuelink.HasUnimplementedError(err) } 68 | 69 | // IsUnimplementedError returns iff if err is an unimplemented error. 70 | func IsUnimplementedError(err error) bool { return issuelink.IsUnimplementedError(err) } 71 | 72 | // UnimplementedErrorHint is the hint emitted upon unimplemented errors. 73 | const UnimplementedErrorHint = issuelink.UnimplementedErrorHint 74 | -------------------------------------------------------------------------------- /join/join.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | package join 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | 21 | "github.com/cockroachdb/errors/errbase" 22 | "github.com/cockroachdb/redact" 23 | "github.com/gogo/protobuf/proto" 24 | ) 25 | 26 | // Join returns an error that wraps the given errors. 27 | // Any nil error values are discarded. 28 | // Join returns nil if errs contains no non-nil values. 29 | // The error formats as the concatenation of the strings obtained 30 | // by calling the Error method of each element of errs, with a newline 31 | // between each string. 32 | func Join(errs ...error) error { 33 | n := 0 34 | for _, err := range errs { 35 | if err != nil { 36 | n++ 37 | } 38 | } 39 | if n == 0 { 40 | return nil 41 | } 42 | e := &joinError{ 43 | errs: make([]error, 0, n), 44 | } 45 | for _, err := range errs { 46 | if err != nil { 47 | e.errs = append(e.errs, err) 48 | } 49 | } 50 | return e 51 | } 52 | 53 | type joinError struct { 54 | errs []error 55 | } 56 | 57 | var _ error = (*joinError)(nil) 58 | var _ fmt.Formatter = (*joinError)(nil) 59 | var _ errbase.SafeFormatter = (*joinError)(nil) 60 | 61 | func (e *joinError) Error() string { 62 | return redact.Sprint(e).StripMarkers() 63 | } 64 | 65 | func (e *joinError) Unwrap() []error { 66 | return e.errs 67 | } 68 | 69 | func (e *joinError) SafeFormatError(p errbase.Printer) error { 70 | for i, err := range e.errs { 71 | if i > 0 { 72 | p.Print("\n") 73 | } 74 | p.Print(err) 75 | } 76 | return nil 77 | } 78 | 79 | func (e *joinError) Format(s fmt.State, verb rune) { 80 | errbase.FormatError(e, s, verb) 81 | } 82 | 83 | func init() { 84 | errbase.RegisterMultiCauseEncoder( 85 | errbase.GetTypeKey(&joinError{}), 86 | func( 87 | ctx context.Context, 88 | err error, 89 | ) (msg string, safeDetails []string, payload proto.Message) { 90 | return "", nil, nil 91 | }, 92 | ) 93 | errbase.RegisterMultiCauseDecoder( 94 | errbase.GetTypeKey(&joinError{}), 95 | func( 96 | ctx context.Context, 97 | causes []error, 98 | msgPrefix string, 99 | safeDetails []string, 100 | payload proto.Message, 101 | ) error { 102 | return Join(causes...) 103 | }, 104 | ) 105 | } 106 | -------------------------------------------------------------------------------- /join/join_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | package join 16 | 17 | import ( 18 | "errors" 19 | "testing" 20 | 21 | "github.com/cockroachdb/errors/safedetails" 22 | "github.com/cockroachdb/redact" 23 | ) 24 | 25 | func TestJoin(t *testing.T) { 26 | e := Join(errors.New("abc123"), errors.New("def456")) 27 | expected := "abc123\ndef456" 28 | if e.Error() != expected { 29 | t.Errorf("Expected: %s; Got: %s", expected, e.Error()) 30 | } 31 | 32 | e = Join(errors.New("abc123"), nil, errors.New("def456"), nil) 33 | if e.Error() != expected { 34 | t.Errorf("Expected: %s; Got: %s", expected, e.Error()) 35 | } 36 | 37 | e = Join(nil, nil, nil) 38 | if e != nil { 39 | t.Errorf("expected nil error") 40 | } 41 | 42 | e = Join( 43 | errors.New("information"), 44 | safedetails.WithSafeDetails(errors.New("detailed error"), "trace_id: %d", redact.Safe(1234)), 45 | ) 46 | printed := redact.Sprintf("%+v", e) 47 | expectedR := redact.RedactableString(`‹information› 48 | (1) ‹information› 49 | | ‹detailed error› 50 | Wraps: (2) trace_id: 1234 51 | └─ Wraps: (3) ‹detailed error› 52 | Wraps: (4) ‹information› 53 | Error types: (1) *join.joinError (2) *safedetails.withSafeDetails (3) *errors.errorString (4) *errors.errorString`) 54 | if printed != expectedR { 55 | t.Errorf("Expected: %s; Got: %s", expectedR, printed) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /markers/example_has_type_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | package markers_test 16 | 17 | import ( 18 | "fmt" 19 | "net" 20 | 21 | "github.com/cockroachdb/errors/markers" 22 | "github.com/pkg/errors" 23 | ) 24 | 25 | type ExampleError struct{ msg string } 26 | 27 | func (e *ExampleError) Error() string { return e.msg } 28 | 29 | func ExampleHasType() { 30 | base := &ExampleError{"world"} 31 | err := errors.Wrap(base, "hello") 32 | fmt.Println(markers.HasType(err, (*ExampleError)(nil))) 33 | fmt.Println(markers.HasType(err, nil)) 34 | fmt.Println(markers.HasType(err, (*net.AddrError)(nil))) 35 | 36 | // Output: 37 | // 38 | // true 39 | // false 40 | // false 41 | } 42 | 43 | func ExampleHasInterface() { 44 | base := &net.AddrError{ 45 | Addr: "ndn", 46 | Err: "ndn doesn't really exists :(", 47 | } 48 | err := errors.Wrap(base, "bummer") 49 | fmt.Println(markers.HasInterface(err, (*net.Error)(nil))) 50 | func() { 51 | defer func() { 52 | if r := recover(); r != nil { 53 | fmt.Println("*net.AddrError is not a pointer to an interface type so the call panics") 54 | } 55 | }() 56 | fmt.Println(markers.HasInterface(err, (*net.AddrError)(nil))) 57 | }() 58 | 59 | // Output: 60 | // 61 | // true 62 | // *net.AddrError is not a pointer to an interface type so the call panics 63 | } 64 | -------------------------------------------------------------------------------- /markers/internal/unknown.proto: -------------------------------------------------------------------------------- 1 | // This file is used only for unknown_type_test.go 2 | 3 | syntax = "proto3"; 4 | package cockroach.errors.markers.internal; 5 | option go_package = "internal"; 6 | 7 | message MyPayload { 8 | int32 val = 1; 9 | } 10 | -------------------------------------------------------------------------------- /markers_api.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | package errors 16 | 17 | import "github.com/cockroachdb/errors/markers" 18 | 19 | // Is determines whether one of the causes of the given error or any 20 | // of its causes is equivalent to some reference error. 21 | // 22 | // As in the Go standard library, an error is considered to match a 23 | // reference error if it is equal to that target or if it implements a 24 | // method Is(error) bool such that Is(reference) returns true. 25 | // 26 | // Note: the inverse is not true - making an Is(reference) method 27 | // return false does not imply that errors.Is() also returns 28 | // false. Errors can be equal because their network equality marker is 29 | // the same. To force errors to appear different to Is(), use 30 | // errors.Mark(). 31 | // 32 | // Note: if any of the error types has been migrated from a previous 33 | // package location or a different type, ensure that 34 | // RegisterTypeMigration() was called prior to Is(). 35 | func Is(err, reference error) bool { return markers.Is(err, reference) } 36 | 37 | // HasType returns true iff err contains an error whose concrete type 38 | // matches that of referenceType. 39 | func HasType(err, referenceType error) bool { return markers.HasType(err, referenceType) } 40 | 41 | // HasInterface returns true if err contains an error which implements the 42 | // interface pointed to by referenceInterface. The type of referenceInterface 43 | // must be a pointer to an interface type. If referenceInterface is not a 44 | // pointer to an interface, this function will panic. 45 | func HasInterface(err error, referenceInterface interface{}) bool { 46 | return markers.HasInterface(err, referenceInterface) 47 | } 48 | 49 | // If iterates on the error's causal chain and returns a predicate's 50 | // return value the first time the predicate returns true. 51 | // 52 | // Note: if any of the error types has been migrated from a previous 53 | // package location or a different type, ensure that 54 | // RegisterTypeMigration() was called prior to If(). 55 | func If(err error, pred func(err error) (interface{}, bool)) (interface{}, bool) { 56 | return markers.If(err, pred) 57 | } 58 | 59 | // IsAny is like Is except that multiple references are compared. 60 | // 61 | // Note: if any of the error types has been migrated from a previous 62 | // package location or a different type, ensure that 63 | // RegisterTypeMigration() was called prior to IsAny(). 64 | func IsAny(err error, references ...error) bool { return markers.IsAny(err, references...) } 65 | 66 | // Mark creates an explicit mark for the given error, using 67 | // the same mark as some reference error. 68 | // 69 | // Note: if any of the error types has been migrated from a previous 70 | // package location or a different type, ensure that 71 | // RegisterTypeMigration() was called prior to Mark(). 72 | func Mark(err error, reference error) error { return markers.Mark(err, reference) } 73 | -------------------------------------------------------------------------------- /oserror/oserror.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | package oserror 16 | 17 | import ( 18 | "os" 19 | 20 | "github.com/cockroachdb/errors" 21 | "github.com/cockroachdb/errors/errbase" 22 | ) 23 | 24 | // Portable analogs of some common system call errors. 25 | // 26 | // Errors returned from this package may be tested against these errors 27 | // with errors.Is. 28 | var ( 29 | ErrInvalid = os.ErrInvalid 30 | ErrPermission = os.ErrPermission 31 | ErrExist = os.ErrExist 32 | ErrNotExist = os.ErrNotExist 33 | ErrClosed = os.ErrClosed 34 | ) 35 | 36 | // IsPermission returns a boolean indicating whether the error is 37 | // known to report that permission is denied. It is satisfied by 38 | // ErrPermission as well as some syscall errors. 39 | // 40 | // This function differs from os.IsPermission() in that it 41 | // can identify an error through wrapping layers. 42 | func IsPermission(err error) bool { 43 | // errors.Is() is not able to peek through os.SyscallError, 44 | // whereas os.IsPermission() can. Conversely, os.IsPermission() 45 | // cannot peek through Unwrap, whereas errors.Is() can. So we 46 | // need to try both. 47 | if errors.Is(err, ErrPermission) || os.IsPermission(errors.UnwrapAll(err)) { 48 | return true 49 | } 50 | // If a syscall errno representing ErrPermission was encoded on a 51 | // different platform, and decoded here, then it will show up as 52 | // neither a syscall errno here nor an ErrPermission; instead it 53 | // shows up as an OpaqueErrno. We test this here. 54 | if o := (*errbase.OpaqueErrno)(nil); errors.As(err, &o) { 55 | return o.Is(ErrPermission) 56 | } 57 | return false 58 | } 59 | 60 | // IsExist returns a boolean indicating whether the error is known to report 61 | // that a file or directory already exists. It is satisfied by ErrExist as 62 | // well as some syscall errors. 63 | // 64 | // This function differs from os.IsExist() in that it 65 | // can identify an error through wrapping layers. 66 | func IsExist(err error) bool { 67 | // errors.Is() is not able to peek through os.SyscallError, 68 | // whereas os.IsExist() can. Conversely, os.IsExist() 69 | // cannot peek through Unwrap, whereas errors.Is() can. So we 70 | // need to try both. 71 | if errors.Is(err, ErrExist) || os.IsExist(errors.UnwrapAll(err)) { 72 | return true 73 | } 74 | // If a syscall errno representing ErrExist was encoded on a 75 | // different platform, and decoded here, then it will show up as 76 | // neither a syscall errno here nor an ErrExist; instead it 77 | // shows up as an OpaqueErrno. We test this here. 78 | if o := (*errbase.OpaqueErrno)(nil); errors.As(err, &o) { 79 | return o.Is(ErrExist) 80 | } 81 | return false 82 | } 83 | 84 | // IsNotExist returns a boolean indicating whether the error is known to 85 | // report that a file or directory does not exist. It is satisfied by 86 | // ErrNotExist as well as some syscall errors. 87 | // 88 | // This function differs from os.IsNotExist() in that it 89 | // can identify an error through wrapping layers. 90 | func IsNotExist(err error) bool { 91 | // errors.Is() is not able to peek through os.SyscallError, 92 | // whereas os.IsNotExist() can. Conversely, os.IsNotExist() 93 | // cannot peek through Unwrap, whereas errors.Is() can. So we 94 | // need to try both. 95 | if errors.Is(err, ErrNotExist) || os.IsNotExist(errors.UnwrapAll(err)) { 96 | return true 97 | } 98 | // If a syscall errno representing ErrNotExist was encoded on a 99 | // different platform, and decoded here, then it will show up as 100 | // neither a syscall errno here nor an ErrNotExist; instead it 101 | // shows up as an OpaqueErrno. We test this here. 102 | if o := (*errbase.OpaqueErrno)(nil); errors.As(err, &o) { 103 | return o.Is(ErrNotExist) 104 | } 105 | return false 106 | } 107 | 108 | // IsTimeout returns a boolean indicating whether the error is known 109 | // to report that a timeout occurred. 110 | // 111 | // This function differs from os.IsTimeout() in that it 112 | // can identify an error through wrapping layers. 113 | func IsTimeout(err error) bool { 114 | // os.IsTimeout() cannot peek through Unwrap. We need errors.If() 115 | // for that. 116 | _, ok := errors.If(err, func(err error) (interface{}, bool) { 117 | return nil, os.IsTimeout(err) 118 | }) 119 | return ok 120 | } 121 | -------------------------------------------------------------------------------- /oserror/oserror_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | package oserror 16 | 17 | import ( 18 | "os" 19 | "testing" 20 | 21 | "github.com/cockroachdb/errors" 22 | "github.com/cockroachdb/errors/testutils" 23 | ) 24 | 25 | func TestErrorPredicates(t *testing.T) { 26 | tt := testutils.T{T: t} 27 | 28 | tt.Check(IsPermission(errors.Wrap(os.ErrPermission, "woo"))) 29 | tt.Check(IsExist(errors.Wrap(os.ErrExist, "woo"))) 30 | tt.Check(IsNotExist(errors.Wrap(os.ErrNotExist, "woo"))) 31 | tt.Check(IsTimeout(errors.Wrap(&myTimeout{}, "woo"))) 32 | } 33 | 34 | type myTimeout struct{} 35 | 36 | func (t *myTimeout) Error() string { return "timeout" } 37 | func (t *myTimeout) Timeout() bool { return true } 38 | -------------------------------------------------------------------------------- /oserror/oserror_unix_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | // +build aix darwin dragonfly freebsd linux netbsd openbsd solaris 16 | 17 | package oserror 18 | 19 | import ( 20 | "syscall" 21 | "testing" 22 | 23 | "github.com/cockroachdb/errors" 24 | "github.com/cockroachdb/errors/testutils" 25 | ) 26 | 27 | func TestErrorPredicatesUnix(t *testing.T) { 28 | tt := testutils.T{T: t} 29 | 30 | tt.Check(IsPermission(errors.Wrap(syscall.EACCES, "woo"))) 31 | tt.Check(IsExist(errors.Wrap(syscall.ENOTEMPTY, "woo"))) 32 | tt.Check(IsNotExist(errors.Wrap(syscall.ENOENT, "woo"))) 33 | tt.Check(IsTimeout(errors.Wrap(syscall.EAGAIN, "woo"))) 34 | } 35 | -------------------------------------------------------------------------------- /report/reportables.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | package report 16 | 17 | import ( 18 | "bytes" 19 | "fmt" 20 | 21 | "github.com/getsentry/sentry-go" 22 | ) 23 | 24 | // StackTrace is an object suitable for inclusion in errors that can 25 | // ultimately be reported with ReportInternalError() or similar. 26 | type StackTrace = sentry.Stacktrace 27 | 28 | // PrintStackTrace produces a human-readable partial representation of 29 | // the stack trace. 30 | func PrintStackTrace(s *StackTrace) string { 31 | var buf bytes.Buffer 32 | for i := len(s.Frames) - 1; i >= 0; i-- { 33 | f := s.Frames[i] 34 | fmt.Fprintf(&buf, "%s:%d: in %s()\n", f.Filename, f.Lineno, f.Function) 35 | } 36 | return buf.String() 37 | } 38 | -------------------------------------------------------------------------------- /report_api.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | package errors 16 | 17 | import ( 18 | "github.com/cockroachdb/errors/report" 19 | "github.com/getsentry/sentry-go" 20 | ) 21 | 22 | // BuildSentryReport builds the components of a sentry report. This 23 | // can be used instead of ReportError() below to use additional custom 24 | // conditions in the reporting or add additional reporting tags. 25 | // 26 | // The Sentry Event is populated for maximal utility when exploited in 27 | // the Sentry.io web interface and database. 28 | // 29 | // A Sentry report is displayed visually in the Sentry UI as follows: 30 | // 31 | //////////////// 32 | // Title: (1) some prefix in bold (2) one line for a stack trace 33 | // (3) a single-line subtitle 34 | // 35 | // (4) the tags, as a tag soup (concatenated in a single paragraph, 36 | // unsorted) 37 | // 38 | // (5) a "message" 39 | // 40 | // (6) zero or more "exceptions", each composed of: 41 | // (7) a bold title 42 | // (8) some freeform text 43 | // (9) a stack trace 44 | // 45 | // (10) metadata fields: environment, arch, etc 46 | // 47 | // (11) "Additional data" fields 48 | // 49 | // (12) SDK version 50 | ///////////////// 51 | // 52 | // These visual items map to the Sentry Event object as follows: 53 | // 54 | // (1) the Type field of the 1st Exception object, if any 55 | // otherwise the Message field 56 | // (2) the topmost entry from the Stacktrace field of the 1st Exception object, if any 57 | // (3) the Value field of the 1st Exception object, if any, unwrapped as a single line 58 | // (4) the Tags field 59 | // (5) the Message field 60 | // (7) the Type field (same as (1) for 1st exception) 61 | // (8) the Value field (same as (3) for 1st exception) 62 | // (9) the Stacktrace field (input to (2) on 1st exception) 63 | // (10) the other fields on the Event object 64 | // (11) the Extra field 65 | // 66 | // (Note how the top-level title fields (1) (3) are unrelated to the 67 | // Message field in the event, which is surprising!) 68 | // 69 | // Given this mapping, an error object is decomposed as follows: 70 | // 71 | // (1)/(7): : () 72 | // (3)/(8): : 73 | // (4): not populated in this function, caller is to manage this 74 | // (5): detailed structure of the entire error object, with references to "additional data" 75 | // and additional "exception" objects 76 | // (9): generated from innermost stack trace 77 | // (6): every exception object after the 1st reports additional stack trace contexts 78 | // (11): "additional data" populated from safe detail payloads 79 | // 80 | // If there is no stack trace in the error, a synthetic Exception 81 | // object is still produced to provide visual detail in the Sentry UI. 82 | // 83 | // Note that if a layer in the error has both a stack trace (ie 84 | // provides the `StackTrace()` interface) and also safe details 85 | // (`SafeDetails()`) other than the stack trace, only the stack trace 86 | // is included in the Sentry report. This does not affect error types 87 | // provided by the library, but could impact error types defined by 88 | // 3rd parties. This limitation may be lifted in a later version. 89 | // 90 | func BuildSentryReport(err error) (*sentry.Event, map[string]interface{}) { 91 | return report.BuildSentryReport(err) 92 | } 93 | 94 | // ReportError reports the given error to Sentry. The caller is responsible for 95 | // checking whether telemetry is enabled, and calling the sentry.Flush() 96 | // function to wait for the report to be uploaded. (By default, 97 | // Sentry submits reports asynchronously.) 98 | // 99 | // Note: an empty 'eventID' can be returned which signifies that the error was 100 | // not reported. This can occur when Sentry client hasn't been properly 101 | // configured or Sentry client decided to not report the error (due to 102 | // configured sampling rate, callbacks, Sentry's event processors, etc). 103 | func ReportError(err error) string { return report.ReportError(err) } 104 | -------------------------------------------------------------------------------- /safedetails/redact.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | package safedetails 16 | 17 | import "github.com/cockroachdb/redact" 18 | 19 | // Redact returns a redacted version of the supplied item that is safe to use in 20 | // anonymized reporting. 21 | // 22 | // NB: this interface is obsolete. Use redact.Sprint() directly. 23 | func Redact(r interface{}) string { 24 | return redact.Sprint(r).Redact().StripMarkers() 25 | } 26 | -------------------------------------------------------------------------------- /safedetails/redact_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | package safedetails_test 16 | 17 | import ( 18 | "context" 19 | "errors" 20 | "net" 21 | "os" 22 | "regexp" 23 | "runtime" 24 | "testing" 25 | 26 | "github.com/cockroachdb/errors/safedetails" 27 | "github.com/cockroachdb/errors/testutils" 28 | "github.com/cockroachdb/redact" 29 | ) 30 | 31 | func TestRedact(t *testing.T) { 32 | errSentinel := (error)(struct{ error }{}) 33 | 34 | // rm is what's left over after redaction. 35 | rm := string(redact.RedactableBytes(redact.RedactedMarker()).StripMarkers()) 36 | 37 | testData := []struct { 38 | obj interface{} 39 | expected string 40 | }{ 41 | // Redacting non-error values. 42 | 43 | {123, rm}, 44 | {"secret", rm}, 45 | 46 | // Redacting SafeMessagers. 47 | 48 | {mySafer{}, `hello`}, 49 | 50 | {safedetails.Safe(123), `123`}, 51 | 52 | {mySafeError{}, `hello`}, 53 | 54 | {&werrFmt{mySafeError{}, "unseen"}, rm + `: hello`}, 55 | 56 | // Redacting errors. 57 | 58 | // Unspecial cases, get redacted. 59 | {errors.New("secret"), rm}, 60 | 61 | // Special cases, unredacted. 62 | {os.ErrInvalid, `invalid argument`}, 63 | {os.ErrPermission, `permission denied`}, 64 | {os.ErrExist, `file already exists`}, 65 | {os.ErrNotExist, `file does not exist`}, 66 | {os.ErrClosed, `file already closed`}, 67 | {os.ErrNoDeadline, `file type does not support deadline`}, 68 | 69 | {context.Canceled, `context canceled`}, 70 | {context.DeadlineExceeded, `context deadline exceeded`}, 71 | 72 | {makeTypeAssertionErr(), `interface conversion: interface {} is nil, not int`}, 73 | 74 | {errSentinel, // explodes if Error() called 75 | `%!v(PANIC=SafeFormatter method: runtime error: invalid memory address or nil pointer dereference)`}, 76 | 77 | {&werrFmt{&werrFmt{os.ErrClosed, "unseen"}, "unsung"}, 78 | rm + `: ` + rm + `: file already closed`}, 79 | 80 | // Special cases, get partly redacted. 81 | 82 | {os.NewSyscallError("rename", os.ErrNotExist), 83 | `rename: file does not exist`}, 84 | 85 | {&os.PathError{Op: "rename", Path: "secret", Err: os.ErrNotExist}, 86 | `rename ` + rm + `: file does not exist`}, 87 | 88 | {&os.LinkError{ 89 | Op: "moo", 90 | Old: "sec", 91 | New: "cret", 92 | Err: os.ErrNotExist, 93 | }, 94 | `moo ` + rm + ` ` + rm + `: file does not exist`}, 95 | 96 | {&net.OpError{ 97 | Op: "write", 98 | Net: "tcp", 99 | Source: &net.IPAddr{IP: net.IP("sensitive-source")}, 100 | Addr: &net.IPAddr{IP: net.IP("sensitive-addr")}, 101 | Err: errors.New("not safe"), 102 | }, `write tcp ` + rm + ` -> ` + rm + `: ` + rm}, 103 | } 104 | 105 | tt := testutils.T{T: t} 106 | 107 | for _, tc := range testData { 108 | s := safedetails.Redact(tc.obj) 109 | s = fileref.ReplaceAllString(s, "...path...") 110 | 111 | tt.CheckStringEqual(s, tc.expected) 112 | } 113 | } 114 | 115 | var fileref = regexp.MustCompile(`([a-zA-Z0-9\._/@-]*\.(?:go|s):\d+)`) 116 | 117 | // makeTypeAssertionErr returns a runtime.Error with the message: 118 | // interface conversion: interface {} is nil, not int 119 | func makeTypeAssertionErr() (result runtime.Error) { 120 | defer func() { 121 | e := recover() 122 | result = e.(runtime.Error) 123 | }() 124 | var x interface{} 125 | _ = x.(int) 126 | return nil 127 | } 128 | 129 | type mySafer struct{} 130 | 131 | func (mySafer) SafeMessage() string { return "hello" } 132 | 133 | type mySafeError struct{} 134 | 135 | func (mySafeError) SafeMessage() string { return "hello" } 136 | func (mySafeError) Error() string { return "helloerr" } 137 | -------------------------------------------------------------------------------- /safedetails/safedetails.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | package safedetails 16 | 17 | import ( 18 | "reflect" 19 | 20 | "github.com/cockroachdb/redact" 21 | ) 22 | 23 | // WithSafeDetails annotates an error with the given reportable details. 24 | // The format is made available as a PII-free string, alongside 25 | // with a PII-free representation of every additional argument. 26 | // Arguments can be reported as-is (without redaction) by wrapping 27 | // them using the Safe() function. 28 | // 29 | // If the format is empty and there are no arguments, the 30 | // error argument is returned unchanged. 31 | // 32 | // Detail is shown: 33 | // - via `errors.GetSafeDetails()` 34 | // - when formatting with `%+v`. 35 | // - in Sentry reports. 36 | func WithSafeDetails(err error, format string, args ...interface{}) error { 37 | if err == nil { 38 | return nil 39 | } 40 | if len(format) == 0 && len(args) == 0 { 41 | return err 42 | } 43 | details := []string{ 44 | redact.Sprintf(format, args...).Redact().StripMarkers(), 45 | } 46 | return &withSafeDetails{cause: err, safeDetails: details} 47 | } 48 | 49 | var refSafeType = reflect.TypeOf(redact.Safe("")) 50 | 51 | // SafeMessager is implemented by objects which have a way of 52 | // representing themselves suitably redacted for anonymized reporting. 53 | // 54 | // NB: this interface is obsolete. Use redact.SafeFormatter instead. 55 | type SafeMessager = redact.SafeMessager 56 | 57 | // Safe wraps the given object into an opaque struct that implements 58 | // SafeMessager: its contents can be included as-is in PII-free 59 | // strings in error objects and reports. 60 | // 61 | // NB: this is obsolete. Use redact.Safe instead. 62 | func Safe(v interface{}) redact.SafeValue { 63 | return redact.Safe(v) 64 | } 65 | -------------------------------------------------------------------------------- /safedetails/with_safedetails.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | package safedetails 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | 21 | "github.com/cockroachdb/errors/errbase" 22 | "github.com/cockroachdb/redact" 23 | "github.com/gogo/protobuf/proto" 24 | ) 25 | 26 | type withSafeDetails struct { 27 | cause error 28 | 29 | safeDetails []string 30 | } 31 | 32 | func (e *withSafeDetails) SafeDetails() []string { 33 | return e.safeDetails 34 | } 35 | 36 | var _ fmt.Formatter = (*withSafeDetails)(nil) 37 | var _ errbase.SafeFormatter = (*withSafeDetails)(nil) 38 | 39 | // Printing a withSecondary reveals the details. 40 | func (e *withSafeDetails) Format(s fmt.State, verb rune) { errbase.FormatError(e, s, verb) } 41 | 42 | // SafeFormatError implements errbase.SafeFormatter. 43 | func (e *withSafeDetails) SafeFormatError(p errbase.Printer) error { 44 | if p.Detail() { 45 | comma := redact.SafeString("") 46 | if len(e.safeDetails) != 1 { 47 | plural := redact.SafeString("s") 48 | if len(e.safeDetails) == 1 { 49 | plural = "" 50 | } 51 | p.Printf("%d safe detail%s enclosed", redact.Safe(len(e.safeDetails)), plural) 52 | comma = "\n" 53 | } 54 | // We hide the details from %+v; they are included 55 | // during Sentry reporting. 56 | for _, s := range e.safeDetails { 57 | p.Printf("%s%s", comma, redact.Safe(s)) 58 | comma = "\n" 59 | } 60 | } 61 | return e.cause 62 | } 63 | 64 | func (e *withSafeDetails) Error() string { return e.cause.Error() } 65 | func (e *withSafeDetails) Cause() error { return e.cause } 66 | func (e *withSafeDetails) Unwrap() error { return e.cause } 67 | 68 | func decodeWithSafeDetails( 69 | _ context.Context, cause error, _ string, safeDetails []string, _ proto.Message, 70 | ) error { 71 | return &withSafeDetails{cause: cause, safeDetails: safeDetails} 72 | } 73 | 74 | func init() { 75 | tn := errbase.GetTypeKey((*withSafeDetails)(nil)) 76 | errbase.RegisterWrapperDecoder(tn, decodeWithSafeDetails) 77 | // Note: no encoder needed, the default implementation is suitable. 78 | } 79 | -------------------------------------------------------------------------------- /safedetails_api.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | package errors 16 | 17 | import ( 18 | "github.com/cockroachdb/errors/safedetails" 19 | "github.com/cockroachdb/redact" 20 | ) 21 | 22 | // WithSafeDetails annotates an error with the given reportable details. 23 | // The format is made available as a PII-free string, alongside 24 | // with a PII-free representation of every additional argument. 25 | // Arguments can be reported as-is (without redaction) by wrapping 26 | // them using the Safe() function. 27 | // 28 | // If the format is empty and there are no arguments, the 29 | // error argument is returned unchanged. 30 | // 31 | // Detail is shown: 32 | // - via `errors.GetSafeDetails()` 33 | // - when formatting with `%+v`. 34 | // - in Sentry reports. 35 | func WithSafeDetails(err error, format string, args ...interface{}) error { 36 | return safedetails.WithSafeDetails(err, format, args...) 37 | } 38 | 39 | // SafeMessager aliases redact.SafeMessager. 40 | // 41 | // NB: this is obsolete. Use redact.SafeFormatter or 42 | // errors.SafeFormatter instead. 43 | type SafeMessager = redact.SafeMessager 44 | 45 | // Safe wraps the given object into an opaque struct that implements 46 | // SafeMessager: its contents can be included as-is in PII-free 47 | // strings in error objects and reports. 48 | // 49 | // NB: this is obsolete. Use redact.Safe instead. 50 | func Safe(v interface{}) redact.SafeValue { return safedetails.Safe(v) } 51 | 52 | // Redact returns a redacted version of the supplied item that is safe to use in 53 | // anonymized reporting. 54 | // 55 | // NB: this interface is obsolete. Use redact.Sprint() directly. 56 | func Redact(r interface{}) string { return safedetails.Redact(r) } 57 | -------------------------------------------------------------------------------- /secondary/secondary.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | package secondary 16 | 17 | // WithSecondaryError enhances the error given as first argument with 18 | // an annotation that carries the error given as second argument. The 19 | // second error does not participate in cause analysis (Is, etc) and 20 | // is only revealed when printing out the error or collecting safe 21 | // (PII-free) details for reporting. 22 | // 23 | // If additionalErr is nil, the first error is returned as-is. 24 | // 25 | // Tip: consider using CombineErrors() below in the general case. 26 | // 27 | // Detail is shown: 28 | // - via `errors.GetSafeDetails()`, shows details from secondary error. 29 | // - when formatting with `%+v`. 30 | // - in Sentry reports. 31 | func WithSecondaryError(err error, additionalErr error) error { 32 | if err == nil || additionalErr == nil { 33 | return err 34 | } 35 | return &withSecondaryError{cause: err, secondaryError: additionalErr} 36 | } 37 | 38 | // CombineErrors returns err, or, if err is nil, otherErr. 39 | // if err is non-nil, otherErr is attached as secondary error. 40 | // See the documentation of `WithSecondaryError()` for details. 41 | func CombineErrors(err error, otherErr error) error { 42 | if err == nil { 43 | return otherErr 44 | } 45 | return WithSecondaryError(err, otherErr) 46 | } 47 | -------------------------------------------------------------------------------- /secondary/with_secondary.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | package secondary 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | 21 | "github.com/cockroachdb/errors/errbase" 22 | "github.com/gogo/protobuf/proto" 23 | ) 24 | 25 | type withSecondaryError struct { 26 | cause error 27 | 28 | // secondaryError is an additional error payload that provides 29 | // additional context towards troubleshooting. 30 | secondaryError error 31 | } 32 | 33 | var _ error = (*withSecondaryError)(nil) 34 | var _ errbase.SafeDetailer = (*withSecondaryError)(nil) 35 | var _ fmt.Formatter = (*withSecondaryError)(nil) 36 | var _ errbase.SafeFormatter = (*withSecondaryError)(nil) 37 | 38 | // SafeDetails reports the PII-free details from the secondary error. 39 | func (e *withSecondaryError) SafeDetails() []string { 40 | var details []string 41 | for err := e.secondaryError; err != nil; err = errbase.UnwrapOnce(err) { 42 | sd := errbase.GetSafeDetails(err) 43 | details = sd.Fill(details) 44 | } 45 | return details 46 | } 47 | 48 | // Printing a withSecondary reveals the details. 49 | func (e *withSecondaryError) Format(s fmt.State, verb rune) { errbase.FormatError(e, s, verb) } 50 | 51 | func (e *withSecondaryError) SafeFormatError(p errbase.Printer) (next error) { 52 | if p.Detail() { 53 | p.Printf("secondary error attachment\n%+v", e.secondaryError) 54 | } 55 | return e.cause 56 | } 57 | 58 | func (e *withSecondaryError) Error() string { return e.cause.Error() } 59 | func (e *withSecondaryError) Cause() error { return e.cause } 60 | func (e *withSecondaryError) Unwrap() error { return e.cause } 61 | 62 | func encodeWithSecondaryError(ctx context.Context, err error) (string, []string, proto.Message) { 63 | e := err.(*withSecondaryError) 64 | enc := errbase.EncodeError(ctx, e.secondaryError) 65 | return "", nil, &enc 66 | } 67 | 68 | func decodeWithSecondaryError( 69 | ctx context.Context, cause error, _ string, _ []string, payload proto.Message, 70 | ) error { 71 | enc, ok := payload.(*errbase.EncodedError) 72 | if !ok { 73 | // If this ever happens, this means some version of the library 74 | // (presumably future) changed the payload type, and we're 75 | // receiving this here. In this case, give up and let 76 | // DecodeError use the opaque type. 77 | return nil 78 | } 79 | return &withSecondaryError{ 80 | cause: cause, 81 | secondaryError: errbase.DecodeError(ctx, *enc), 82 | } 83 | } 84 | 85 | func init() { 86 | tn := errbase.GetTypeKey((*withSecondaryError)(nil)) 87 | errbase.RegisterWrapperDecoder(tn, decodeWithSecondaryError) 88 | errbase.RegisterWrapperEncoder(tn, encodeWithSecondaryError) 89 | } 90 | -------------------------------------------------------------------------------- /secondary_api.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | package errors 16 | 17 | import "github.com/cockroachdb/errors/secondary" 18 | 19 | // WithSecondaryError enhances the error given as first argument with 20 | // an annotation that carries the error given as second argument. The 21 | // second error does not participate in cause analysis (Is, etc) and 22 | // is only revealed when printing out the error or collecting safe 23 | // (PII-free) details for reporting. 24 | // 25 | // If additionalErr is nil, the first error is returned as-is. 26 | // 27 | // Tip: consider using CombineErrors() below in the general case. 28 | // 29 | // Detail is shown: 30 | // - via `errors.GetSafeDetails()`, shows details from secondary error. 31 | // - when formatting with `%+v`. 32 | // - in Sentry reports. 33 | func WithSecondaryError(err error, additionalErr error) error { 34 | return secondary.WithSecondaryError(err, additionalErr) 35 | } 36 | 37 | // CombineErrors returns err, or, if err is nil, otherErr. 38 | // if err is non-nil, otherErr is attached as secondary error. 39 | // See the documentation of `WithSecondaryError()` for details. 40 | func CombineErrors(err, otherErr error) error { 41 | return secondary.CombineErrors(err, otherErr) 42 | } 43 | -------------------------------------------------------------------------------- /stdstrings/strings.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | package stdstrings 16 | 17 | // IssueReferral is the additional hint text provided to ask users for further actions. 18 | const IssueReferral = ` 19 | 20 | Please check the public issue tracker to check whether this problem is 21 | already tracked. If you cannot find it there, please report the error 22 | with details by creating a new issue. 23 | 24 | If you would rather not post publicly, please contact us directly 25 | using the support form. 26 | 27 | We appreciate your feedback. 28 | ` 29 | -------------------------------------------------------------------------------- /telemetrykeys/telemetrykeys.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | package telemetrykeys 16 | 17 | import "github.com/cockroachdb/errors/errbase" 18 | 19 | // WithTelemetry annotates err with the given telemetry key(s). 20 | // The telemetry keys must be PII-free. 21 | // 22 | // Detail is shown: 23 | // - via `errors.GetSafeDetails()`. 24 | // - via `GetTelemetryKeys()` below. 25 | // - when formatting with `%+v`. 26 | // - in Sentry reports. 27 | func WithTelemetry(err error, keys ...string) error { 28 | if err == nil { 29 | return nil 30 | } 31 | 32 | return &withTelemetry{cause: err, keys: keys} 33 | } 34 | 35 | // GetTelemetryKeys retrieves the (de-duplicated) set of 36 | // all telemetry keys present in the direct causal chain 37 | // of the error. The keys may not be sorted. 38 | func GetTelemetryKeys(err error) []string { 39 | keys := map[string]struct{}{} 40 | for ; err != nil; err = errbase.UnwrapOnce(err) { 41 | if w, ok := err.(*withTelemetry); ok { 42 | for _, k := range w.keys { 43 | keys[k] = struct{}{} 44 | } 45 | } 46 | } 47 | res := make([]string, 0, len(keys)) 48 | for k := range keys { 49 | res = append(res, k) 50 | } 51 | return res 52 | } 53 | -------------------------------------------------------------------------------- /telemetrykeys/telemetrykeys_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | package telemetrykeys_test 16 | 17 | import ( 18 | "context" 19 | goErr "errors" 20 | "fmt" 21 | "sort" 22 | "strings" 23 | "testing" 24 | 25 | "github.com/cockroachdb/errors/errbase" 26 | "github.com/cockroachdb/errors/markers" 27 | "github.com/cockroachdb/errors/telemetrykeys" 28 | "github.com/cockroachdb/errors/testutils" 29 | "github.com/pkg/errors" 30 | ) 31 | 32 | func TestTelemetry(t *testing.T) { 33 | tt := testutils.T{T: t} 34 | 35 | baseErr := errors.New("world") 36 | err := errors.Wrap( 37 | telemetrykeys.WithTelemetry( 38 | telemetrykeys.WithTelemetry( 39 | baseErr, 40 | "a", "b"), 41 | "b", "c"), 42 | "hello") 43 | 44 | tt.Check(markers.Is(err, baseErr)) 45 | tt.CheckStringEqual(err.Error(), "hello: world") 46 | 47 | keys := telemetrykeys.GetTelemetryKeys(err) 48 | sort.Strings(keys) 49 | tt.CheckDeepEqual(keys, []string{"a", "b", "c"}) 50 | 51 | errV := fmt.Sprintf("%+v", err) 52 | tt.Check(strings.Contains(errV, `keys: [a b]`)) 53 | tt.Check(strings.Contains(errV, `keys: [b c]`)) 54 | 55 | enc := errbase.EncodeError(context.Background(), err) 56 | newErr := errbase.DecodeError(context.Background(), enc) 57 | 58 | tt.Check(markers.Is(newErr, baseErr)) 59 | tt.CheckStringEqual(newErr.Error(), "hello: world") 60 | 61 | keys = telemetrykeys.GetTelemetryKeys(newErr) 62 | sort.Strings(keys) 63 | tt.CheckDeepEqual(keys, []string{"a", "b", "c"}) 64 | 65 | errV = fmt.Sprintf("%+v", newErr) 66 | tt.Check(strings.Contains(errV, `keys: [a b]`)) 67 | tt.Check(strings.Contains(errV, `keys: [b c]`)) 68 | } 69 | 70 | func TestFormat(t *testing.T) { 71 | tt := testutils.T{t} 72 | 73 | baseErr := goErr.New("woo") 74 | const woo = `woo` 75 | const waawoo = `waa: woo` 76 | testCases := []struct { 77 | name string 78 | err error 79 | expFmtSimple string 80 | expFmtVerbose string 81 | }{ 82 | {"keys", 83 | telemetrykeys.WithTelemetry(baseErr, "a", "b"), 84 | woo, ` 85 | woo 86 | (1) keys: [a b] 87 | Wraps: (2) woo 88 | Error types: (1) *telemetrykeys.withTelemetry (2) *errors.errorString`}, 89 | 90 | {"keys + wrapper", 91 | telemetrykeys.WithTelemetry(&werrFmt{baseErr, "waa"}, "a", "b"), 92 | waawoo, ` 93 | waa: woo 94 | (1) keys: [a b] 95 | Wraps: (2) waa 96 | | -- this is waa's 97 | | multi-line payload 98 | Wraps: (3) woo 99 | Error types: (1) *telemetrykeys.withTelemetry (2) *telemetrykeys_test.werrFmt (3) *errors.errorString`}, 100 | 101 | {"wrapper + keys", 102 | &werrFmt{telemetrykeys.WithTelemetry(baseErr, "a", "b"), "waa"}, 103 | waawoo, ` 104 | waa: woo 105 | (1) waa 106 | | -- this is waa's 107 | | multi-line payload 108 | Wraps: (2) keys: [a b] 109 | Wraps: (3) woo 110 | Error types: (1) *telemetrykeys_test.werrFmt (2) *telemetrykeys.withTelemetry (3) *errors.errorString`}, 111 | } 112 | 113 | for _, test := range testCases { 114 | tt.Run(test.name, func(tt testutils.T) { 115 | err := test.err 116 | 117 | // %s is simple formatting 118 | tt.CheckStringEqual(fmt.Sprintf("%s", err), test.expFmtSimple) 119 | 120 | // %v is simple formatting too, for compatibility with the past. 121 | tt.CheckStringEqual(fmt.Sprintf("%v", err), test.expFmtSimple) 122 | 123 | // %q is always like %s but quotes the result. 124 | ref := fmt.Sprintf("%q", test.expFmtSimple) 125 | tt.CheckStringEqual(fmt.Sprintf("%q", err), ref) 126 | 127 | // %+v is the verbose mode. 128 | refV := strings.TrimPrefix(test.expFmtVerbose, "\n") 129 | spv := fmt.Sprintf("%+v", err) 130 | tt.CheckStringEqual(spv, refV) 131 | }) 132 | } 133 | } 134 | 135 | type werrFmt struct { 136 | cause error 137 | msg string 138 | } 139 | 140 | var _ errbase.Formatter = (*werrFmt)(nil) 141 | 142 | func (e *werrFmt) Error() string { return fmt.Sprintf("%s: %v", e.msg, e.cause) } 143 | func (e *werrFmt) Unwrap() error { return e.cause } 144 | func (e *werrFmt) Format(s fmt.State, verb rune) { errbase.FormatError(e, s, verb) } 145 | func (e *werrFmt) FormatError(p errbase.Printer) error { 146 | p.Print(e.msg) 147 | if p.Detail() { 148 | p.Printf("-- this is %s's\nmulti-line payload", e.msg) 149 | } 150 | return e.cause 151 | } 152 | -------------------------------------------------------------------------------- /telemetrykeys/with_telemetry.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | package telemetrykeys 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | "strings" 21 | 22 | "github.com/cockroachdb/errors/errbase" 23 | "github.com/cockroachdb/redact" 24 | "github.com/gogo/protobuf/proto" 25 | ) 26 | 27 | type withTelemetry struct { 28 | cause error 29 | keys []string 30 | } 31 | 32 | var _ error = (*withTelemetry)(nil) 33 | var _ errbase.SafeDetailer = (*withTelemetry)(nil) 34 | var _ fmt.Formatter = (*withTelemetry)(nil) 35 | var _ errbase.SafeFormatter = (*withTelemetry)(nil) 36 | 37 | func (w *withTelemetry) Error() string { return w.cause.Error() } 38 | func (w *withTelemetry) Cause() error { return w.cause } 39 | func (w *withTelemetry) Unwrap() error { return w.cause } 40 | 41 | func (w *withTelemetry) SafeDetails() []string { return w.keys } 42 | 43 | func (w *withTelemetry) Format(s fmt.State, verb rune) { errbase.FormatError(w, s, verb) } 44 | 45 | func (w *withTelemetry) SafeFormatError(p errbase.Printer) (next error) { 46 | if p.Detail() { 47 | p.Printf("keys: [%s]", redact.Safe(strings.Join(w.keys, " "))) 48 | } 49 | return w.cause 50 | } 51 | 52 | func decodeWithTelemetry( 53 | _ context.Context, cause error, _ string, keys []string, _ proto.Message, 54 | ) error { 55 | return &withTelemetry{cause: cause, keys: keys} 56 | } 57 | 58 | func init() { 59 | errbase.RegisterWrapperDecoder(errbase.GetTypeKey((*withTelemetry)(nil)), decodeWithTelemetry) 60 | } 61 | -------------------------------------------------------------------------------- /telemetrykeys_api.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | package errors 16 | 17 | import "github.com/cockroachdb/errors/telemetrykeys" 18 | 19 | // WithTelemetry annotates err with the given telemetry key(s). 20 | // The telemetry keys must be PII-free. 21 | // 22 | // Detail is shown: 23 | // - via `errors.GetSafeDetails()`. 24 | // - via `GetTelemetryKeys()` below. 25 | // - when formatting with `%+v`. 26 | // - in Sentry reports. 27 | func WithTelemetry(err error, keys ...string) error { return telemetrykeys.WithTelemetry(err, keys...) } 28 | 29 | // GetTelemetryKeys retrieves the (de-duplicated) set of 30 | // all telemetry keys present in the direct causal chain 31 | // of the error. The keys may not be sorted. 32 | func GetTelemetryKeys(err error) []string { return telemetrykeys.GetTelemetryKeys(err) } 33 | -------------------------------------------------------------------------------- /testutils/simplecheck_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | package testutils 16 | 17 | import "testing" 18 | 19 | func id(x int) int { return x } 20 | 21 | func TestAssert(t *testing.T) { 22 | tt := T{T: t} 23 | 24 | tt.Run("the-test", func(t T) { 25 | t.Check(id(1) == 1) 26 | t.CheckEqual(1, id(1)) 27 | t.CheckDeepEqual(1, id(1)) 28 | t.Assert(id(1) == 1) 29 | t.AssertEqual(1, id(1)) 30 | t.AssertDeepEqual(1, id(1)) 31 | t.CheckRegexpEqual("hello", "h.*o") 32 | }) 33 | } 34 | -------------------------------------------------------------------------------- /withstack/internal/run.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | package internal 16 | 17 | // Run is used in tests. 18 | func Run(callback func() error) error { 19 | return Run2(callback) 20 | } 21 | 22 | // Run2 is used in tests. 23 | func Run2(callback func() error) error { 24 | return callback() 25 | } 26 | -------------------------------------------------------------------------------- /withstack/one_line_source.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | package withstack 16 | 17 | import ( 18 | "fmt" 19 | "path/filepath" 20 | "strings" 21 | 22 | "github.com/cockroachdb/errors/errbase" 23 | ) 24 | 25 | // GetOneLineSource extracts the file/line/function information 26 | // of the topmost caller in the innermost recorded stack trace. 27 | // The filename is simplified to remove the path prefix. 28 | // This is used e.g. to populate the "source" field in 29 | // PostgreSQL errors. 30 | func GetOneLineSource(err error) (file string, line int, fn string, ok bool) { 31 | // We want the innermost entry: start by recursing. 32 | if c := errbase.UnwrapOnce(err); c != nil { 33 | if file, line, fn, ok = GetOneLineSource(c); ok { 34 | return 35 | } 36 | } 37 | // If we reach this point, we haven't found anything in the cause so 38 | // far. Look at the current level. 39 | 40 | // If we have a stack trace in the style of github.com/pkg/errors 41 | // (either from there or our own withStack), use it. 42 | if st, ok := err.(errbase.StackTraceProvider); ok { 43 | return getOneLineSourceFromPkgStack(st.StackTrace()) 44 | } 45 | 46 | // If we have flattened a github.com/pkg/errors-style stack 47 | // trace to a string, it will happen in the error's safe details 48 | // and we need to parse it. 49 | if sd, ok := err.(errbase.SafeDetailer); ok { 50 | details := sd.SafeDetails() 51 | if len(details) > 0 { 52 | switch errbase.GetTypeKey(err) { 53 | case pkgFundamental, pkgWithStackName, ourWithStackName: 54 | return getOneLineSourceFromPrintedStack(details[0]) 55 | } 56 | } 57 | } 58 | 59 | // No conversion available - no stack trace. 60 | return "", 0, "", false 61 | } 62 | 63 | func getOneLineSourceFromPkgStack( 64 | st errbase.StackTrace, 65 | ) (file string, line int, fn string, ok bool) { 66 | if len(st) > 0 { 67 | st = st[:1] 68 | // Note: the stack trace logic changed between go 1.11 and 1.12. 69 | // Trying to analyze the frame PCs point-wise will cause 70 | // the output to change between the go versions. 71 | stS := fmt.Sprintf("%+v", st) 72 | return getOneLineSourceFromPrintedStack(stS) 73 | } 74 | return "", 0, "", false 75 | } 76 | 77 | func getOneLineSourceFromPrintedStack(st string) (file string, line int, fn string, ok bool) { 78 | // We only need 3 lines: the function/file/line info will be on the first two lines. 79 | // See parsePrintedStack() for details. 80 | lines := strings.SplitN(strings.TrimSpace(st), "\n", 3) 81 | if len(lines) > 0 { 82 | _, file, line, fnName := parsePrintedStackEntry(lines, 0) 83 | if fnName != "unknown" { 84 | _, fn = functionName(fnName) 85 | } 86 | return filepath.Base(file), line, fn, true 87 | } 88 | return "", 0, "", false 89 | } 90 | -------------------------------------------------------------------------------- /withstack/one_line_source_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | package withstack_test 16 | 17 | import ( 18 | "context" 19 | "errors" 20 | "strings" 21 | "testing" 22 | 23 | "github.com/cockroachdb/errors/errbase" 24 | "github.com/cockroachdb/errors/testutils" 25 | "github.com/cockroachdb/errors/withstack" 26 | pkgErr "github.com/pkg/errors" 27 | ) 28 | 29 | func TestOneLineSource(t *testing.T) { 30 | tt := testutils.T{T: t} 31 | 32 | simpleErr := errors.New("hello") 33 | testData := []error{ 34 | withstack.WithStack(simpleErr), 35 | errbase.DecodeError(context.Background(), errbase.EncodeError(context.Background(), withstack.WithStack(simpleErr))), 36 | pkgErr.WithStack(simpleErr), 37 | errbase.DecodeError(context.Background(), errbase.EncodeError(context.Background(), pkgErr.WithStack(simpleErr))), 38 | pkgErr.New("woo"), 39 | errbase.DecodeError(context.Background(), errbase.EncodeError(context.Background(), pkgErr.New("woo"))), 40 | } 41 | 42 | for _, err := range testData { 43 | tt.Run(err.Error(), func(tt testutils.T) { 44 | file, line, fn, ok := withstack.GetOneLineSource(err) 45 | tt.CheckEqual(ok, true) 46 | tt.CheckEqual(file, "one_line_source_test.go") 47 | tt.CheckEqual(fn, "TestOneLineSource") 48 | tt.Check(line > 21) 49 | if tt.Failed() { 50 | tt.Logf("looking at: %+v", err) 51 | } 52 | }) 53 | } 54 | } 55 | 56 | func TestOneLineSourceInner(t *testing.T) { 57 | tt := testutils.T{T: t} 58 | 59 | // makeErr creates an error where the source context is not this 60 | // test function. 61 | simpleErr := makeErr() 62 | 63 | // Make the error wrapped to add additional source context. The rest 64 | // of the test below will check that GetOneLineSource retrieves the 65 | // innermost context, not this one. 66 | testData := []error{ 67 | withstack.WithStack(simpleErr), 68 | errbase.DecodeError(context.Background(), errbase.EncodeError(context.Background(), withstack.WithStack(simpleErr))), 69 | pkgErr.WithStack(simpleErr), 70 | errbase.DecodeError(context.Background(), errbase.EncodeError(context.Background(), pkgErr.WithStack(simpleErr))), 71 | } 72 | 73 | for _, err := range testData { 74 | file, line, fn, ok := withstack.GetOneLineSource(err) 75 | tt.CheckEqual(ok, true) 76 | tt.CheckEqual(file, "reportable_test.go") 77 | tt.Check(strings.HasPrefix(fn, "makeErr")) 78 | tt.Check(line > 21) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /withstack/reportable_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | package withstack_test 16 | 17 | import ( 18 | "context" 19 | "errors" 20 | "strings" 21 | "testing" 22 | 23 | "github.com/cockroachdb/errors/errbase" 24 | "github.com/cockroachdb/errors/testutils" 25 | "github.com/cockroachdb/errors/withstack" 26 | "github.com/cockroachdb/errors/withstack/internal" 27 | "github.com/kr/pretty" 28 | pkgErr "github.com/pkg/errors" 29 | ) 30 | 31 | func TestReportableStackTrace(t *testing.T) { 32 | baseErr := errors.New("hello") 33 | 34 | t.Run("pkgErr", func(t *testing.T) { 35 | err := internal.Run(func() error { return pkgErr.WithStack(baseErr) }) 36 | t.Run("local", func(t *testing.T) { 37 | checkStackTrace(t, err, 0) 38 | }) 39 | enc := errbase.EncodeError(context.Background(), err) 40 | err = errbase.DecodeError(context.Background(), enc) 41 | t.Run("remote", func(t *testing.T) { 42 | checkStackTrace(t, err, 0) 43 | }) 44 | }) 45 | 46 | t.Run("pkgFundamental", func(t *testing.T) { 47 | err := internal.Run(func() error { return pkgErr.New("hello") }) 48 | t.Run("local", func(t *testing.T) { 49 | checkStackTrace(t, err, 0) 50 | }) 51 | enc := errbase.EncodeError(context.Background(), err) 52 | err = errbase.DecodeError(context.Background(), enc) 53 | t.Run("remote", func(t *testing.T) { 54 | checkStackTrace(t, err, 0) 55 | }) 56 | }) 57 | 58 | t.Run("withStack", func(t *testing.T) { 59 | err := internal.Run(func() error { return withstack.WithStack(baseErr) }) 60 | t.Run("local", func(t *testing.T) { 61 | checkStackTrace(t, err, 0) 62 | }) 63 | enc := errbase.EncodeError(context.Background(), err) 64 | err = errbase.DecodeError(context.Background(), enc) 65 | t.Run("remote", func(t *testing.T) { 66 | checkStackTrace(t, err, 0) 67 | }) 68 | }) 69 | 70 | t.Run("withStack depth", func(t *testing.T) { 71 | err := internal.Run(makeErr) 72 | checkStackTrace(t, err, 1) 73 | }) 74 | t.Run("withStack nontrival depth", func(t *testing.T) { 75 | err := internal.Run(makeErr3) 76 | checkStackTrace(t, err, 0) 77 | }) 78 | } 79 | 80 | func makeErr() error { return makeErr2() } 81 | func makeErr2() error { return withstack.WithStack(errors.New("")) } 82 | 83 | func makeErr3() error { return makeErr4() } 84 | func makeErr4() error { return withstack.WithStackDepth(errors.New(""), 1) } 85 | 86 | func checkStackTrace(t *testing.T, err error, expectedDepth int) { 87 | tt := testutils.T{T: t} 88 | 89 | t.Logf("looking at err %# v", pretty.Formatter(err)) 90 | 91 | r := withstack.GetReportableStackTrace(err) 92 | tt.Assert(r != nil) 93 | 94 | // We're expecting the Run() functions in second position. 95 | tt.Assert(len(r.Frames) >= expectedDepth+2) 96 | 97 | for i, f := range r.Frames { 98 | t.Logf("frame %d:", i) 99 | t.Logf("absolute path: %s", f.AbsPath) 100 | t.Logf("file: %s", f.Filename) 101 | t.Logf("line: %d", f.Lineno) 102 | t.Logf("module: %s", f.Module) 103 | t.Logf("function: %s", f.Function) 104 | } 105 | 106 | // The reportable frames are in reversed order. For the test, 107 | // we want to look at them in the "good" order. 108 | for i, j := 0, len(r.Frames)-1; i < j; i, j = i+1, j-1 { 109 | r.Frames[i], r.Frames[j] = r.Frames[j], r.Frames[i] 110 | } 111 | 112 | for i := expectedDepth; i < expectedDepth+2; i++ { 113 | f := r.Frames[i] 114 | tt.Check(strings.Contains(f.Filename, "/errors/") || strings.Contains(f.Filename, "/errors@")) 115 | 116 | tt.Check(strings.HasSuffix(f.AbsPath, f.Filename)) 117 | 118 | switch i { 119 | case expectedDepth: 120 | tt.Check(strings.HasSuffix(f.Filename, "reportable_test.go")) 121 | 122 | case expectedDepth + 1, expectedDepth + 2: 123 | tt.Check(strings.HasSuffix(f.Filename, "internal/run.go")) 124 | 125 | tt.Check(strings.HasSuffix(f.Module, "withstack/internal")) 126 | 127 | tt.Check(strings.HasPrefix(f.Function, "Run")) 128 | } 129 | } 130 | 131 | // Check that Run2() is after Run() in the source code. 132 | tt.Check(r.Frames[expectedDepth+1].Lineno != 0 && 133 | r.Frames[expectedDepth+2].Lineno != 0 && 134 | (r.Frames[expectedDepth+1].Lineno > r.Frames[expectedDepth+2].Lineno)) 135 | } 136 | -------------------------------------------------------------------------------- /withstack/stack.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | package withstack 16 | 17 | import ( 18 | "fmt" 19 | "runtime" 20 | 21 | "github.com/cockroachdb/errors/errbase" 22 | ) 23 | 24 | // stack represents a stack of program counters. 25 | // This mirrors the (non-exported) type of the same name in github.com/pkg/errors. 26 | type stack []uintptr 27 | 28 | // Format mirrors the code in github.com/pkg/errors. 29 | func (s *stack) Format(st fmt.State, verb rune) { 30 | switch verb { 31 | case 'v': 32 | switch { 33 | case st.Flag('+'): 34 | for _, pc := range *s { 35 | f := errbase.StackFrame(pc) 36 | fmt.Fprintf(st, "\n%+v", f) 37 | } 38 | } 39 | } 40 | } 41 | 42 | // StackTrace mirrors the code in github.com/pkg/errors. 43 | func (s *stack) StackTrace() errbase.StackTrace { 44 | f := make([]errbase.StackFrame, len(*s)) 45 | for i := 0; i < len(f); i++ { 46 | f[i] = errbase.StackFrame((*s)[i]) 47 | } 48 | return f 49 | } 50 | 51 | // callers mirrors the code in github.com/pkg/errors, 52 | // but makes the depth customizable. 53 | func callers(depth int) *stack { 54 | const numFrames = 32 55 | var pcs [numFrames]uintptr 56 | n := runtime.Callers(2+depth, pcs[:]) 57 | var st stack = pcs[0:n] 58 | return &st 59 | } 60 | -------------------------------------------------------------------------------- /withstack/withstack.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | package withstack 16 | 17 | import ( 18 | "fmt" 19 | 20 | "github.com/cockroachdb/errors/errbase" 21 | ) 22 | 23 | // This file mirrors the WithStack functionality from 24 | // github.com/pkg/errors. We would prefer to reuse the withStack 25 | // struct from that package directly (the library recognizes it well) 26 | // unfortunately github.com/pkg/errors does not enable client code to 27 | // customize the depth at which the stack trace is captured. 28 | 29 | // WithStack annotates err with a stack trace at the point WithStack was called. 30 | // 31 | // Detail is shown: 32 | // - via `errors.GetSafeDetails()` 33 | // - when formatting with `%+v`. 34 | // - in Sentry reports. 35 | // - when innermost stack capture, with `errors.GetOneLineSource()`. 36 | func WithStack(err error) error { 37 | // Skip the frame of WithStack itself, this mirrors the behavior 38 | // of WithStack() in github.com/pkg/errors. 39 | return WithStackDepth(err, 1) 40 | } 41 | 42 | // WithStackDepth annotates err with a stack trace starting from the 43 | // given call depth. The value zero identifies the caller 44 | // of WithStackDepth itself. 45 | // See the documentation of WithStack() for more details. 46 | func WithStackDepth(err error, depth int) error { 47 | if err == nil { 48 | return nil 49 | } 50 | return &withStack{cause: err, stack: callers(depth + 1)} 51 | } 52 | 53 | type withStack struct { 54 | cause error 55 | 56 | *stack 57 | } 58 | 59 | var _ error = (*withStack)(nil) 60 | var _ fmt.Formatter = (*withStack)(nil) 61 | var _ errbase.SafeFormatter = (*withStack)(nil) 62 | var _ errbase.SafeDetailer = (*withStack)(nil) 63 | 64 | func (w *withStack) Error() string { return w.cause.Error() } 65 | func (w *withStack) Cause() error { return w.cause } 66 | func (w *withStack) Unwrap() error { return w.cause } 67 | 68 | // Format implements the fmt.Formatter interface. 69 | func (w *withStack) Format(s fmt.State, verb rune) { errbase.FormatError(w, s, verb) } 70 | 71 | // SafeFormatError implements the errbase.SafeFormatter interface. 72 | func (w *withStack) SafeFormatError(p errbase.Printer) error { 73 | if p.Detail() { 74 | p.Printf("attached stack trace") 75 | } 76 | // We do not print the stack trace ourselves - errbase.FormatError() 77 | // does this for us. 78 | return w.cause 79 | } 80 | 81 | // SafeDetails implements the errbase.SafeDetailer interface. 82 | func (w *withStack) SafeDetails() []string { 83 | return []string{fmt.Sprintf("%+v", w.StackTrace())} 84 | } 85 | -------------------------------------------------------------------------------- /withstack_api.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Cockroach Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | // implied. See the License for the specific language governing 13 | // permissions and limitations under the License. 14 | 15 | package errors 16 | 17 | import "github.com/cockroachdb/errors/withstack" 18 | 19 | // This file mirrors the WithStack functionality from 20 | // github.com/pkg/errors. We would prefer to reuse the withStack 21 | // struct from that package directly (the library recognizes it well) 22 | // unfortunately github.com/pkg/errors does not enable client code to 23 | // customize the depth at which the stack trace is captured. 24 | 25 | // WithStack annotates err with a stack trace at the point WithStack was called. 26 | // 27 | // Detail is shown: 28 | // - via `errors.GetSafeDetails()` 29 | // - when formatting with `%+v`. 30 | // - in Sentry reports. 31 | // - when innermost stack capture, with `errors.GetOneLineSource()`. 32 | func WithStack(err error) error { return withstack.WithStackDepth(err, 1) } 33 | 34 | // WithStackDepth annotates err with a stack trace starting from the 35 | // given call depth. The value zero identifies the caller 36 | // of WithStackDepth itself. 37 | // See the documentation of WithStack() for more details. 38 | func WithStackDepth(err error, depth int) error { return withstack.WithStackDepth(err, depth+1) } 39 | 40 | // ReportableStackTrace aliases the type of the same name in the sentry 41 | // package. This is used by SendReport(). 42 | type ReportableStackTrace = withstack.ReportableStackTrace 43 | 44 | // GetOneLineSource extracts the file/line/function information 45 | // of the topmost caller in the innermost recorded stack trace. 46 | // The filename is simplified to remove the path prefix. 47 | // 48 | // This is used e.g. to populate the "source" field in 49 | // PostgreSQL errors in CockroachDB. 50 | func GetOneLineSource(err error) (file string, line int, fn string, ok bool) { 51 | return withstack.GetOneLineSource(err) 52 | } 53 | 54 | // GetReportableStackTrace extracts a stack trace embedded in the 55 | // given error in the format suitable for Sentry reporting. 56 | // 57 | // This supports: 58 | // - errors generated by github.com/pkg/errors (either generated 59 | // locally or after transfer through the network), 60 | // - errors generated with WithStack() in this package, 61 | // - any other error that implements a StackTrace() method 62 | // returning a StackTrace from github.com/pkg/errors. 63 | // 64 | // Note: Sentry wants the oldest call frame first, so 65 | // the entries are reversed in the result. 66 | func GetReportableStackTrace(err error) *ReportableStackTrace { 67 | return withstack.GetReportableStackTrace(err) 68 | } 69 | --------------------------------------------------------------------------------