Skip to content

Commit 25c28f9

Browse files
committed
Introduce new-style redactable errors.
This change equips the error library with string redaction according to the algorithms and ideas from github.com/cockroachdb/redact. It is backward incompatible in two ways: - it breaks support with Go 1.12. The code is now 1.13+ only. - the `errors.Safe()` function has a new return type. New: - When an error object is passed as formatting argument to `Newf()`, `Wrapf()` and similar (i.e. not as first argument in `Wrap`), it is now attached as secondary payload as per `errors.WithSecondaryError()`. This ensures that more details about these errors are included eventually in Sentry reports. - The full text of the error message, with sensitive bits redacted out, is now included at the head of Sentry reports. - The new `errors.SafeFormatter` interface mimics `errors.Formatter`, with a method `SafeFormatError()`. Its `Printer` argument has `Print()` methods that behave like `redact.SafeWriter`: they distinguish sensitive and non-sensitive error bits. For example: ```go func (myError) SafeFormatError(p errors.Printer) { p.Printf("hello %s", "world") // ^^^^^ safe // ^^^^^^^ unsafe p.Printf("hello %s", errors.Safe("world")) // ^^^^^ safe // ^^^^^^^^^^^^^^^^^^^^^ safe } ``` This method can be implemented *instead of* `FormatError()`; the error library will know how to use it if present. - The new `errors.Formattable()` wrapper transforms any `error` instance into a `fmt.Formatter` which will detail the error adequately, even if the error type does not implement `FormatError`. Example use: ```go fmt.Printf("hello: %v", errors.Formattable(err)) ``` API changes: - `errors.Formatter` continues to be supported for compatibility with Go 1.13's `xerror` proposal. It is however deprecated in favor of `errors.SafeFormatter`. - `errors.Safe()` is now an alias for `redact.Safe()`. It now returns a `redact.SafeValue` instead of a `SafeMessager`. - The `errors.SafeMessager` interface is deprecated. Use the `redact.SafeFormatter` interface, or the other APIs from the `redact` package. - The "complex" constructors `errors.New*()`, `errors.Wrap*()` now use facilities from the `redact` package to distinguish safe and unsafe arguments, instead of the `WithSafeDetails()` wrapper. This makes the chain of errors simpler.
1 parent 7a6984d commit 25c28f9

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

59 files changed

+60316
-1370
lines changed

.travis.yml

-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
language: go
22

33
go:
4-
- 1.12.x
54
- 1.13.x
65
- 1.14.x

README.md

+38-26
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ Table of contents:
4646
| wrappers to attach secondary causes | | | ||
4747
| wrappers to attach [`logtags`](https://github.com/cockroachdb/logtags) details from `context.Context` | | | ||
4848
| `errors.FormatError()`, `Formatter`, `Printer` | | | (under construction) ||
49+
| `errors.SafeFormatError()`, `SafeFormatter` | | | ||
4950

5051
"Forward compatibility" above refers to the ability of this library to
5152
recognize and properly handle network communication of error types it
@@ -201,17 +202,17 @@ return errors.Wrap(foo(), "foo")
201202
- `WithMessage(error, string) error`, `WithMessagef(error, string, ...interface{}) error`: message prefix.
202203
- when to use: probably never. Use `errors.Wrap()`/`errors.Wrapf()` instead.
203204
- what it does: adds a message prefix.
204-
- how to access the detail: `Error()`, regular Go formatting. Not included in Sentry reports.
205+
- how to access the detail: `Error()`, regular Go formatting, Sentry Report.
205206

206207
- `WithDetail(error, string) error`, `WithDetailf(error, string, ...interface{}) error`, user-facing detail with contextual information.
207208
- **when to use: need to embark a message string to output when the error is presented to a human.**
208209
- what it does: captures detail strings.
209-
- how to access the detail: `errors.GetAllDetails()`, `errors.FlattenDetails()` (all details are preserved), format with `%+v`.
210+
- how to access the detail: `errors.GetAllDetails()`, `errors.FlattenDetails()` (all details are preserved), format with `%+v`. Not included in Sentry reports.
210211

211212
- `WithHint(error, string) error`, `WithHintf(error, string, ...interface{}) error`: user-facing detail with suggestion for action to take.
212213
- **when to use: need to embark a message string to output when the error is presented to a human.**
213214
- what it does: captures hint strings.
214-
- how to access the detail: `errors.GetAllHints()`, `errors.FlattenHints()` (hints are de-duplicated), format with `%+v`.
215+
- how to access the detail: `errors.GetAllHints()`, `errors.FlattenHints()` (hints are de-duplicated), format with `%+v`. Not included in Sentry reports.
215216

216217
- `WithIssueLink(error, IssueLink) error`: annotate an error with an URL and arbitrary string.
217218
- **when to use: to refer (human) users to some external resources.**
@@ -260,7 +261,7 @@ considered to be PII-free, and thus included in Sentry reports automatically:
260261
- the `format string` argument of `Newf`, `AssertionFailedf`, etc (the constructors ending with `...f()`),
261262
- the *type* of the additional arguments passed to the `...f()` constructors,
262263
- the *value of specific argument types* passed to the `...f()` constructors, when known to be PII-safe.
263-
For details of which arguments are considered PII-free, see the [`Redact()` function](safedetails/redact.go).
264+
For details of which arguments are considered PII-free, see the [`redact` package](https://github.com/cockroachdb/redact).
264265

265266
It is possible to opt additional in to Sentry reporting, using either of the following methods:
266267

@@ -293,7 +294,7 @@ If your error type is a wrapper, you should implement a `Format()`
293294
method that redirects to `errors.FormatError()`, otherwise `%+v` will
294295
not work. Additionally, if your type has a payload not otherwise
295296
visible via `Error()`, you may want to implement
296-
`errors.Formatter`. See [making `%+v` work with your
297+
`errors.SafeFormatter`. See [making `%+v` work with your
297298
type](#Making-v-work-with-your-type) below for details.
298299

299300
Finally, you may want your new error type to be portable across
@@ -445,8 +446,8 @@ In short:
445446
type, this will disable the recursive application of the `%+v` flag
446447
to the causes chained from your wrapper.)
447448
448-
- You may optionally implement the `errors.Formatter` interface:
449-
`FormatError(p errors.Printer) (next error)`. This is optional, but
449+
- You may optionally implement the `errors.SafeFormatter` interface:
450+
`SafeFormatError(p errors.Printer) (next error)`. This is optional, but
450451
should be done when some details are not included by `Error()` and
451452
should be emitted upon `%+v`.
452453
@@ -458,11 +459,11 @@ achieves this as follows:
458459
func (w *withHTTPCode) Format(s fmt.State, verb rune) { errors.FormatError(w, s, verb) }
459460
460461
// FormatError() formats the error.
461-
func (w *withHTTPCode) FormatError(p errors.Printer) (next error) {
462+
func (w *withHTTPCode) SafeFormatError(p errors.Printer) (next error) {
462463
// Note: no need to print out the cause here!
463464
// FormatError() knows how to do this automatically.
464465
if p.Detail() {
465-
p.Printf("http code: %d", w.code)
466+
p.Printf("http code: %d", errors.Safe(w.code))
466467
}
467468
return w.cause
468469
}
@@ -492,22 +493,22 @@ proposal](https://go.googlesource.com/proposal/+/master/design/29934-error-value
492493

493494
## Error composition (summary)
494495

495-
| Constructor | Composes |
496-
|------------------------------------|--------------------------------------------------------------------------------------------------|
497-
| `New` | `NewWithDepth` (see below) |
498-
| `Errorf` | = `Newf` |
499-
| `Newf` | `NewWithDepthf` (see below) |
500-
| `WithMessage` | = `pkgErr.WithMessage` |
501-
| `Wrap` | `WrapWithDepth` (see below) |
502-
| `Wrapf` | `WrapWithDepthf` (see below) |
503-
| `AssertionFailed` | `AssertionFailedWithDepthf` (see below) |
504-
| `NewWithDepth` | `goErr.New` + `WithStackDepth` (see below) |
505-
| `NewWithDepthf` | `fmt.Errorf` + `WithSafeDetails` + `WithStackDepth` |
506-
| `WithMessagef` | `pkgErr.WithMessagef` + `WithSafeDetails` |
507-
| `WrapWithDepth` | `WithMessage` + `WithStackDepth` |
508-
| `WrapWithDepthf` | `WithMessage` + `WithStackDepth` + `WithSafeDetails` |
509-
| `AssertionFailedWithDepthf` | `fmt.Errorf` + `WithStackDepth` + `WithSafeDetails` + `WithAssertionFailure` |
510-
| `NewAssertionErrorWithWrappedErrf` | `HandledWithMessagef` (barrier) + `WithStackDepth` + `WithSafeDetails` + `WithAssertionFailure` |
496+
| Constructor | Composes |
497+
|------------------------------------|-----------------------------------------------------------------------------------|
498+
| `New` | `NewWithDepth` (see below) |
499+
| `Errorf` | = `Newf` |
500+
| `Newf` | `NewWithDepthf` (see below) |
501+
| `WithMessage` | custom wrapper with message prefix and knowledge of safe strings |
502+
| `Wrap` | `WrapWithDepth` (see below) |
503+
| `Wrapf` | `WrapWithDepthf` (see below) |
504+
| `AssertionFailed` | `AssertionFailedWithDepthf` (see below) |
505+
| `NewWithDepth` | custom leaf with knowledge of safe strings + `WithStackDepth` (see below) |
506+
| `NewWithDepthf` | custom leaf with knowledge of safe strings + `WithSafeDetails` + `WithStackDepth` |
507+
| `WithMessagef` | custom wrapper with message prefix and knowledge of safe strings |
508+
| `WrapWithDepth` | `WithMessage` + `WithStackDepth` |
509+
| `WrapWithDepthf` | `WithMessagef` + `WithStackDepth` |
510+
| `AssertionFailedWithDepthf` | `NewWithDepthf` + `WithAssertionFailure` |
511+
| `NewAssertionErrorWithWrappedErrf` | `HandledWithMessagef` (barrier) + `WrapWithDepthf` + `WithAssertionFailure` |
511512

512513
## API (not constructing error objects)
513514

@@ -519,6 +520,13 @@ func Cause(err error) error // compatibility
519520
func Unwrap(err error) error // compatibility
520521
type Wrapper interface { ... } // compatibility
521522
523+
// Error formatting.
524+
type Formatter interface { ... } // compatibility, not recommended
525+
type SafeFormatter interface { ... }
526+
type Printer interface { ... }
527+
func FormatError(err error, s fmt.State, verb rune)
528+
func Formattable(err error) fmt.Formatter
529+
522530
// Identify errors.
523531
func Is(err, reference error) bool
524532
func IsAny(err error, references ...error) bool
@@ -553,10 +561,14 @@ func GetReportableStackTrace(err error) *ReportableStackTrace
553561
type SafeDetailPayload struct { ... }
554562
func GetAllSafeDetails(err error) []SafeDetailPayload
555563
func GetSafeDetails(err error) (payload SafeDetailPayload)
564+
565+
// Obsolete APIs.
556566
type SafeMessager interface { ... }
557-
func Safe(v interface{}) SafeMessager
558567
func Redact(r interface{}) string
559568
569+
// Aliases redact.Safe.
570+
func Safe(v interface{}) SafeMessager
571+
560572
// Assertion failures.
561573
func HasAssertionFailure(err error) bool
562574
func IsAssertionFailure(err error) bool

assert/assert.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ type withAssertionFailure struct {
6262

6363
var _ error = (*withAssertionFailure)(nil)
6464
var _ fmt.Formatter = (*withAssertionFailure)(nil)
65-
var _ errbase.Formatter = (*withAssertionFailure)(nil)
65+
var _ errbase.SafeFormatter = (*withAssertionFailure)(nil)
6666

6767
// ErrorHint implements the hintdetail.ErrorHinter interface.
6868
func (w *withAssertionFailure) ErrorHint() string {
@@ -77,9 +77,9 @@ func (w *withAssertionFailure) Cause() error { return w.cause }
7777
func (w *withAssertionFailure) Unwrap() error { return w.cause }
7878

7979
func (w *withAssertionFailure) Format(s fmt.State, verb rune) { errbase.FormatError(w, s, verb) }
80-
func (w *withAssertionFailure) FormatError(p errbase.Printer) error {
80+
func (w *withAssertionFailure) SafeFormatError(p errbase.Printer) error {
8181
if p.Detail() {
82-
p.Print("assertion failure")
82+
p.Printf("assertion failure")
8383
}
8484
return w.cause
8585
}

barriers/barriers.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ type barrierError struct {
7575

7676
var _ error = (*barrierError)(nil)
7777
var _ errbase.SafeDetailer = (*barrierError)(nil)
78-
var _ errbase.Formatter = (*barrierError)(nil)
78+
var _ errbase.SafeFormatter = (*barrierError)(nil)
7979
var _ fmt.Formatter = (*barrierError)(nil)
8080

8181
// barrierError is an error.
@@ -94,8 +94,8 @@ func (e *barrierError) SafeDetails() []string {
9494
// Printing a barrier reveals the details.
9595
func (e *barrierError) Format(s fmt.State, verb rune) { errbase.FormatError(e, s, verb) }
9696

97-
func (e *barrierError) FormatError(p errbase.Printer) (next error) {
98-
p.Printf(e.msg)
97+
func (e *barrierError) SafeFormatError(p errbase.Printer) (next error) {
98+
p.Print(e.msg)
9999
if p.Detail() {
100100
p.Printf("-- cause hidden behind barrier\n%+v", e.maskedErr)
101101
}

contexttags/contexttags.go

+17-6
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ import (
1818
"context"
1919

2020
"github.com/cockroachdb/errors/errbase"
21-
"github.com/cockroachdb/errors/safedetails"
2221
"github.com/cockroachdb/logtags"
22+
"github.com/cockroachdb/redact"
2323
)
2424

2525
// WithContextTags captures the k/v pairs stored in the context via the
@@ -88,15 +88,26 @@ func convertToStringsOnly(b *logtags.Buffer) (res *logtags.Buffer) {
8888

8989
func redactTags(b *logtags.Buffer) []string {
9090
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
9199
for i, t := range b.Get() {
92-
res[i] = t.Key()
100+
k := t.Key()
93101
v := t.Value()
102+
eq := empty
103+
var val interface{} = empty
94104
if v != nil {
95-
if len(res[i]) > 1 {
96-
res[i] += "="
105+
if len(k) > 1 {
106+
eq = "="
97107
}
98-
res[i] += safedetails.Redact(v)
108+
val = v
99109
}
110+
res := redact.Sprintf("%s%s%v", redact.Safe(k), eq, val)
111+
fn(i, res)
100112
}
101-
return res
102113
}

contexttags/contexttags_test.go

+8-4
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
"github.com/cockroachdb/errors/markers"
2828
"github.com/cockroachdb/errors/testutils"
2929
"github.com/cockroachdb/logtags"
30+
"github.com/cockroachdb/redact"
3031
)
3132

3233
func TestWithContext(t *testing.T) {
@@ -87,7 +88,7 @@ func TestWithContext(t *testing.T) {
8788
tt.Run("remote", func(tt testutils.T) { theTest(tt, newErr) })
8889
}
8990

90-
func TestTagRedaction(t *testing.T) {
91+
func TestTagRedactionInSafeDetails(t *testing.T) {
9192
tt := testutils.T{T: t}
9293

9394
// Create an example context with decoration.
@@ -105,10 +106,13 @@ func TestTagRedaction(t *testing.T) {
105106
ctx2 = logtags.AddTag(ctx2, "planet1", "universe")
106107
ctx2 = logtags.AddTag(ctx2, "planet2", errors.Safe("universe"))
107108

108-
// This will be our reference expected value.
109+
// rm is what's left over after redaction.
110+
rm := string(redact.RedactableBytes(redact.RedactedMarker()).StripMarkers())
111+
112+
// This will be our reference expected value in "safe details" strings.
109113
refStrings := [][]string{
110-
[]string{"planet1=string:<redacted>", "planet2=universe"},
111-
[]string{"foo1=int:<redacted>", "xint:<redacted>", "bar1", "foo2=123", "y456", "bar2"},
114+
[]string{"planet1=" + rm, "planet2=universe"},
115+
[]string{"foo1=" + rm, "x" + rm, "bar1", "foo2=123", "y456", "bar2"},
112116
}
113117

114118
// Construct the error object.

contexttags/with_context.go

+11-3
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"github.com/cockroachdb/errors/errbase"
2222
"github.com/cockroachdb/errors/errorspb"
2323
"github.com/cockroachdb/logtags"
24+
"github.com/cockroachdb/redact"
2425
"github.com/gogo/protobuf/proto"
2526
)
2627

@@ -39,8 +40,8 @@ type withContext struct {
3940

4041
var _ error = (*withContext)(nil)
4142
var _ errbase.SafeDetailer = (*withContext)(nil)
43+
var _ errbase.SafeFormatter = (*withContext)(nil)
4244
var _ fmt.Formatter = (*withContext)(nil)
43-
var _ errbase.Formatter = (*withContext)(nil)
4445

4546
// withContext is an error. The original error message is preserved.
4647
func (w *withContext) Error() string { return w.cause.Error() }
@@ -52,9 +53,16 @@ func (w *withContext) Unwrap() error { return w.cause }
5253
// Printing a withContext reveals the tags.
5354
func (w *withContext) Format(s fmt.State, verb rune) { errbase.FormatError(w, s, verb) }
5455

55-
func (w *withContext) FormatError(p errbase.Printer) error {
56+
func (w *withContext) SafeFormatError(p errbase.Printer) error {
5657
if p.Detail() && w.tags != nil {
57-
p.Printf("tags: [%s]", w.tags.String())
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("]")
5866
}
5967
return w.cause
6068
}

domains/domains.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ func HandledInDomainWithMessage(err error, domain Domain, msg string) error {
9393
// Handled creates a handled error in the implicit domain (see
9494
// PackageDomain() below) of its caller.
9595
//
96-
// See the documentation of `errors.Handled()` for details.
96+
// See the documentation of `barriers.Handled()` for details.
9797
func Handled(err error) error {
9898
return HandledInDomain(err, PackageDomainAtDepth(1))
9999
}

domains/with_domain.go

+4-3
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"fmt"
2020

2121
"github.com/cockroachdb/errors/errbase"
22+
"github.com/cockroachdb/redact"
2223
"github.com/gogo/protobuf/proto"
2324
)
2425

@@ -36,7 +37,7 @@ var _ error = (*withDomain)(nil)
3637
var _ errbase.SafeDetailer = (*withDomain)(nil)
3738
var _ errbase.TypeKeyMarker = (*withDomain)(nil)
3839
var _ fmt.Formatter = (*withDomain)(nil)
39-
var _ errbase.Formatter = (*withDomain)(nil)
40+
var _ errbase.SafeFormatter = (*withDomain)(nil)
4041

4142
// withDomain is an error. The original error message is preserved.
4243
func (e *withDomain) Error() string { return e.cause.Error() }
@@ -58,9 +59,9 @@ func (e *withDomain) SafeDetails() []string {
5859

5960
func (e *withDomain) Format(s fmt.State, verb rune) { errbase.FormatError(e, s, verb) }
6061

61-
func (e *withDomain) FormatError(p errbase.Printer) error {
62+
func (e *withDomain) SafeFormatError(p errbase.Printer) error {
6263
if p.Detail() {
63-
p.Print(e.domain)
64+
p.Print(redact.Safe(e.domain))
6465
}
6566
return e.cause
6667
}

errbase/encode.go

+3
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,9 @@ func getTypeDetails(
211211

212212
// TypeKeyMarker can be implemented by errors that wish to extend
213213
// their type name as seen by GetTypeKey().
214+
//
215+
// Note: the key marker is considered safe for reporting and
216+
// is included in sentry reports.
214217
type TypeKeyMarker interface {
215218
ErrorKeyMarker() string
216219
}

0 commit comments

Comments
 (0)