diff --git a/CHANGELOG.md b/CHANGELOG.md index 6299212..82c3de1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Complete the `stacktrace` and `json` package READMEs with the APIs introduced in v1.3.0 and the unreleased conditional-capture and formatting features. + - **`ExtractAttrs` drops a redundant dedup map** ([#31]) — the breadth-first traversal no longer keeps a secondary `map[*attributed]bool` to guard against double-counting a shared `*attributed`. The primary `visited` set is keyed on pointer identity (`errptr.Get`), which for an `*attributed` is that same pointer, and is checked-and-set at dequeue time before attrs are collected — so a shared instance reachable through several paths was already deduplicated there, and the second map never actually prevented a duplicate. Removes the per-call map setup and the per-node `attributed` bookkeeping, simplifying the loop body (allocation count is unchanged — escape analysis already stack-allocated both maps). Behavior is identical, as the existing diamond/DAG dedup test confirms. - **`stacktrace` subpackage no longer uses `reflect` or `unsafe`** ([#29], [#35]) — `extractCarrierClassifications` now goes through the public `errx.CarrierClassifications` accessor instead of reflecting into the unexported `carrier.classifications` field, mirroring the migration already done in the `json` subpackage ([#17]). A future rename of the unexported `carrier` type can no longer silently drop classification extraction during stack-trace traversal. No exported APIs or behavior change. - **README overhaul** — examples now lead with the idiomatic single-call variadic form (classification, displayable message, and attributes attached in one `Wrap`/`ClassifyNew`) instead of multi-step builder-style code. Adds a "Why errx" section comparing errx with alternative libraries (`pkg/errors`, `cockroachdb/errors`, `joomcode/errorx`, `morikuni/failure`, and `eris`), plus new sections covering error aggregation (`errx.Join`), attribute redaction via `WithAttributeValueTransformer`, and non-slog logger integration via `ToKVArgs`. diff --git a/json/README.md b/json/README.md index 37c6190..6aadbd0 100644 --- a/json/README.md +++ b/json/README.md @@ -86,6 +86,76 @@ Control whether standard (non-errx) errors in the error chain are included. jsonBytes, _ := errxjson.Marshal(err, errxjson.WithIncludeStandardErrors(false)) ``` +### WithStackTrace + +Include captured stack traces in the serialized output. The default is `true`; pass +`false` to omit the `stack_trace` field. This is separate from `WithMaxStackFrames(0)`, +which means no frame limit. + +```go +jsonBytes, _ := errxjson.Marshal(err, errxjson.WithStackTrace(false)) +``` + +### WithAttributes + +Include structured attributes in the output. The default is `true`; pass `false` to omit +the `attributes` field, for example when attributes may contain sensitive data. + +```go +jsonBytes, _ := errxjson.Marshal(err, errxjson.WithAttributes(false)) +``` + +### WithSentinels + +Include sentinel text in the output. The default is `true`; pass `false` to omit the +`sentinels` field when those messages are internal implementation details. + +```go +jsonBytes, _ := errxjson.Marshal(err, errxjson.WithSentinels(false)) +``` + +### WithAttributeValueTransformer + +Rewrite or redact each attribute value during serialization. The transformer receives the +attribute key and value, and is called only when attributes are included. + +Note that this only transforms values in the `attributes` field, not in `message`. For example, +serializing `errx.Attrs("token", "secret")` with this transformer still includes `"message": "token=secret"`. +Similarly, `WithAttributes(false)` only suppresses the `attributes` field, leaving the message intact. +Neither option guarantees that sensitive values are omitted from the serialized JSON if they are present in the error message. + +```go +jsonBytes, _ := errxjson.Marshal(err, errxjson.WithAttributeValueTransformer( + func(key string, value any) any { + if key == "token" { + return "[redacted]" + } + return value + }, +)) +``` + +### WithStackTraceTrimPaths + +Remove a prefix from each stack frame's file path. An empty prefix (the default) leaves +paths unchanged. + +```go +jsonBytes, _ := errxjson.Marshal(err, + errxjson.WithStackTraceTrimPaths("/home/runner/work/myproject/")) +``` + +### WithMaxMessageBytes + +Limit each serialized error message to at most `n` bytes. When truncation is needed, the +suffix `...(truncated)` is appended without splitting a UTF-8 character. If `n` is 14 +bytes or less (the length of the suffix), a hard cut at the byte limit is used instead +without appending the suffix. The default `0` leaves messages unchanged. + +```go +jsonBytes, _ := errxjson.Marshal(err, errxjson.WithMaxMessageBytes(256)) +``` + ## JSON Structure The serialized error has the following structure: diff --git a/stacktrace/README.md b/stacktrace/README.md index 521692a..5874084 100644 --- a/stacktrace/README.md +++ b/stacktrace/README.md @@ -4,10 +4,14 @@ Optional stack trace support for errx errors. ## Overview -The `stacktrace` package extends `errx` with stack trace capabilities while keeping the core `errx` package minimal and zero-dependency. It provides two usage patterns: +The `stacktrace` package extends `errx` with stack trace capabilities while keeping the +core `errx` package minimal and zero-dependency. It provides three usage patterns: 1. **Per-error opt-in** using `Here()` as a `Classified` -2. **Automatic capture** using `stacktrace.Wrap()` and `stacktrace.Classify()` +2. **Automatic capture** using `stacktrace.Wrap()`, `stacktrace.Classify()` and + `stacktrace.ClassifyNew()` +3. **Conditional capture** using `WrapIf`, `ClassifyIf` or `HereIf` to avoid duplicate traces + when a cause already carries one. ## Installation @@ -42,7 +46,10 @@ Use `stacktrace.Wrap()` or `stacktrace.Classify()` for automatic trace capture: err := stacktrace.Wrap("operation failed", cause, ErrNotFound) // Or with Classify -err := stacktrace.Classify(cause, ErrRetryable) +err = stacktrace.Classify(cause, ErrRetryable) + +// Create and classify a new error +err = stacktrace.ClassifyNew("user record missing", ErrNotFound) ``` ### Extracting Stack Traces @@ -58,6 +65,35 @@ if frames != nil { } ``` +`Extract` returns the first trace in the chain. Use `ExtractAll` when several layers or +branches may carry traces; it returns them outermost first. + +### Conditional Capture + +`WrapIf` and `ClassifyIf` capture a trace only when the cause does not already carry one. +Their `*Depth` variants let callers choose the maximum capture depth. `HereIf` and +`HereIfDepth` return `nil` when a trace is already present, so they can be passed to +`errx.Wrap`, `errx.Classify` or `errx.ClassifyNew` without adding a duplicate. Use +`HasTrace` to check for a non-empty trace before choosing your own behavior. + +```go +err := stacktrace.WrapIf("load user", cause) +if stacktrace.HasTrace(err) { + fmt.Println("trace is available") +} +``` + +### Formatting Traces + +Errors returned by `stacktrace.Wrap`, `Classify` and `ClassifyNew` implement +`fmt.Formatter`. When an error carries a trace, `%+v` prints its message followed by the +stack frames in the `pkg/errors` style; `%v` and `%s` print only the message. `errx.Wrap` +and `errx.Classify` also format traces attached with `stacktrace.Here()`. + +```go +fmt.Printf("%+v\n", err) // message followed by stack frames +``` + ## Integration with errx Features Stack traces work seamlessly with all errx features: @@ -84,14 +120,39 @@ fmt.Println("Has stack trace:", stacktrace.Extract(err) != nil) ### Functions -- `Here() errx.Classified` - Captures the current stack trace as a Classified -- `Extract(err error) []Frame` - Extracts stack frames from an error chain -- `Wrap(text string, cause error, classifications ...errx.Classified) error` - Wraps with automatic trace -- `Classify(cause error, classifications ...errx.Classified) error` - Classifies with automatic trace +- `Here() errx.Classified` captures a trace for use with `errx.Wrap`, `errx.Classify` or + `errx.ClassifyNew`. +- `HereDepth(depth int) errx.Classified` captures up to the requested number of frames. +- `Wrap(text string, cause error, classifications ...errx.Classified) error` and + `WrapDepth(text string, cause error, depth int, classifications ...errx.Classified) error` + wrap an error and capture a trace. +- `Classify(cause error, classifications ...errx.Classified) error` and + `ClassifyDepth(cause error, depth int, classifications ...errx.Classified) error` + add classifications and capture a trace. +- `ClassifyNew(text string, classifications ...errx.Classified) error` and + `ClassifyNewDepth(text string, depth int, classifications ...errx.Classified) error` + create and classify an error with a trace. +- `WrapIf(text string, cause error, classifications ...errx.Classified) error` and + `WrapIfDepth(text string, cause error, depth int, classifications ...errx.Classified) error` + capture only when the cause has no trace. +- `ClassifyIf(cause error, classifications ...errx.Classified) error` and + `ClassifyIfDepth(cause error, depth int, classifications ...errx.Classified) error` + classify and capture only when needed. +- `HereIf(cause error) errx.Classified` and `HereIfDepth(cause error, depth int) errx.Classified` + return `nil` when the cause already carries a trace. +- `Extract(err error) []Frame` returns the first trace in the error chain; + `ExtractAll(err error) [][]Frame` returns all traces, outermost first. +- `HasTrace(err error) bool` reports whether the chain contains a non-empty trace. + +### Constants + +- `DefaultMaxDepth` is 32. Non-positive depth values use this default. +- `MaxDepth` is 256. Larger requested depths are clamped to this ceiling. ### Types -- `Frame` - Represents a single stack frame with `File`, `Line`, and `Function` fields +- `Frame` represents a stack frame with `File`, `Line` and `Function` fields. +- `Tracer` is implemented by errors that expose captured frames through `Frames() []Frame`. ## Performance Considerations