clues

package module
v0.0.0-...-48ef099 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 29, 2024 License: MIT Imports: 14 Imported by: 39

README

CLUES

PkgGoDev goreportcard

A golang library for tracking runtime variables via ctx, passing them upstream within errors, and retrieving context- and error-bound variables for logging.

Aggregate runtime state in ctx

Track runtime variables by adding them to the context.

func foo(ctx context.Context, someID string) error {
    ctx = clues.Add(ctx, "importantID", someID)
    return bar(ctx, someID)
}

Keep error messages readable and augment your telemetry by packing errors with structured data.

func bar(ctx context.Context, someID string) error {
    ctx = clues.Add(ctx, "importantID", someID)
    err := errors.New("a bad happened")
    if err != nil {
        return clues.Stack(err).WithClues(ctx)
    }
    return nil
}

Retrive structured data from your errors for logging and other telemetry.

func main() {
    err := foo(context.Background(), "importantID")
    if err != nil {
        logger.
            Error("calling foo").
            WithError(err).
            WithAll(clues.InErr(err))
    }
}

Track individual process flows

Each clues addition traces its additions with a tree of IDs, chaining those traces into the "clues_trace" value. This lets you quickly and easily filter logs to a specific process tree.

func iterateOver(ctx context.Context, users []string) {
    // automatically adds "clues_trace":"id_a"
    ctx = clues.Add(ctx, "status", good)
    for i, user := range users {
        // automatically appends another id to "clues_trace": "id_a,id_n"
        ictx := clues.Add(ctx, "currentUser", user, "iter", i)
        err := doSomething(ictx, user)
        if err != nil {
            ictx = clues.Add(ictx, "status", bad)
        }
    }
}

Interoperable with pkg/errors

Clues errors can be wrapped by pkg/errors without slicing out any stored data.

func getIt(someID string) error {
    return clues.New("oh no!").With("importantID", someID)
}

func getItWrapper(someID string) error {
    if err := getIt(someID); err != nil {
        return errors.Wrap(err, "getting the thing")
    }

    return nil
}

func main() {
    err := getItWrapper("id")
    if err != nil {
        fmt.Println("error getting", err, "with vals", clues.InErr(err))
    }
}

Stackable errors

Error stacking lets you embed error sentinels without slicing out the current error's data or relying on err.Error() strings.

var ErrorCommonFailure = "a common failure condition"

func do() error {
    if err := dependency.Do(); err != nil {
        return clues.Stack(ErrorCommonFailure, err)
    }
    
    return nil
}

func main() {
    err := do()
    if errors.Is(err, ErrCommonFailure) {
        // true!
    }
}

Labeling Errors

Rather than build an errors.As-compliant local error to annotate downstream errors, labels allow you to categorize errors with expected qualities.

Augment downstream errors with labels

func foo(ctx context.Context, someID string) error {
    err := externalPkg.DoThing(ctx, someID)
    if err != nil {
        return clues.Wrap(err).Label("retryable")
    }
    return nil
}

Check your labels upstream.

func main() {
    err := foo(context.Background(), "importantID")
    if err != nil {
        if clues.HasLabel(err, "retryable")) {
            err := foo(context.Background(), "importantID")
        }
    }
}

Design

Clues is not the first of its kind: ctx-err-combo packages already exist. Most other packages tend to couple the two notions, packing both into a single handler. This is, in my opinion, an anti-pattern. Errors are not context, and context are not errors. Unifying the two can couple layers together, and your maintenance woes from handling that coupling are not worth the tradeoff in syntactical sugar.

In turn, Clues maintains a clear separation between accumulating data into a context and passing data back in an error. Both handlers operate independent of the other, so you can choose to only use the ctx (accumulate data into the context, but maybe log it instead of returning data in the err) or the err (only pack immedaite details into the error).

References

Similar Art

Fault is most similar in design to this package, and also attempts to maintain separation between errors and contexts. The differences are largely syntactical: Fault prefers a composable interface with decorator packages. I like to keep error production as terse as possible, thus preferring a more populated interface of methods over the decorator design.

References

Documentation

Index

Constants

View Source
const (
	SHA256 hashAlg = iota
	HMAC_SHA256
	Plaintext
	Flatmask
)

Variables

This section is empty.

Functions

func Add

func Add(ctx context.Context, kvs ...any) context.Context

Add adds all key-value pairs to the clues.

func AddLabelCounter

func AddLabelCounter(ctx context.Context, counter Adder) context.Context

AddLabelCounter embeds an Adder interface into this context. Any already embedded Adder will get replaced. When adding Labels to a clues.Err the LabelCounter will use the label as the key for the Add call, and increment the count of that label by one.

func AddMap

func AddMap[K comparable, V any](ctx context.Context, m map[K]V) context.Context

AddMap adds a shallow clone of the map to a namespaced set of clues.

func AddMapTo

func AddMapTo[K comparable, V any](ctx context.Context, namespace string, m map[K]V) context.Context

AddMapTo adds a shallow clone of the map to a namespaced set of clues.

func AddTo

func AddTo(ctx context.Context, namespace string, kvs ...any) context.Context

AddTo adds all key-value pairs to a namespaced set of clues.

func AddTrace

func AddTrace(ctx context.Context) context.Context

AddTrace stacks a clues node onto this context. Adding a node ensures that this point in code is identified by an ID, which can later be used to correlate and isolate logs to certain trace branches. AddTrace is only needed for layers that don't otherwise call Add() or similar functions, since those funcs already attach a new node.

func AddTraceName

func AddTraceName(
	ctx context.Context,
	name string,
	kvs ...any,
) context.Context

AddTraceName stacks a clues node onto this context and uses the provided name for the trace id, instead of a randomly generated hash. AddTraceName can be called without additional values if you only want to add a trace marker.

func AddTraceNameTo

func AddTraceNameTo(
	ctx context.Context,
	name, namespace string,
	kvs ...any,
) context.Context

AddTraceNameTo stacks a clues node onto this context and uses the provided name for the trace id, instead of a randomly generated hash. AddTraceNameTo can be called without additional values if you only want to add a trace marker.

func AddTraceTo

func AddTraceTo(ctx context.Context, namespace string) context.Context

AddTraceTo stacks a clues node onto this context within the specified namespace. Adding a node ensures that a point in code is identified by an ID, which can later be used to correlate and isolate logs to certain trace branches. AddTraceTo is only needed for layers that don't otherwise call AddTo() or similar functions, since those funcs already attach a new node.

func Conceal

func Conceal(a any) string

Conceal runs the currently configured hashing algorithm on the parameterized value.

func ConcealWith

func ConcealWith(alg hashAlg, s string) string

Conceal runs one of clues' hashing algorithms on the provided string.

func HasLabel

func HasLabel(err error, label string) bool

func Hide

func Hide(a any) secret

Hide embeds the value in a secret struct where the Conceal() call contains a truncated hash of value. The hash function defaults to SHA256, but can be changed through configuration.

func HideAll

func HideAll(a ...any) []secret

HideAll is a quality-of-life wrapper for transforming multiple values to secret structs.

func In

func In(ctx context.Context) *dataNode

In returns the map of values in the default namespace.

func InErr

func InErr(err error) *dataNode

InErr returns the map of contextual values in the error. Each error in the stack is unwrapped and all maps are unioned. In case of collision, lower level error data take least priority.

func InNamespace

func InNamespace(ctx context.Context, namespace string) *dataNode

InNamespace returns the map of values in the given namespace.

func Labels

func Labels(err error) map[string]struct{}

func Mask

func Mask(a any) secret

Mask embeds the value in a secret struct where the Conceal() call always returns a flat string: "***"

func SetHasher

func SetHasher(sc HashCfg)

SetHasher sets the hashing configuration used in all clues concealer structs, and clues.Conceal() and clues.Hash() calls.

func Unwrap

func Unwrap(err error) error

Unwrap provides compatibility for Go 1.13 error chains. Unwrap returns the Unwrap()ped base error, if it implements the unwrapper interface:

type unwrapper interface {
       Unwrap() error
}

If the error does not implement Unwrap, returns the error.

Types

type Adder

type Adder interface {
	Add(key string, n int64)
}

type Concealer

type Concealer interface {
	Conceal() string
	// Concealers also need to comply with Format
	// It's a bit overbearing, but complying with Concealer
	// doesn't provide guarantees that the variable won't
	// pass into fmt.Printf("%v") and skip the whole hash.
	// This is for your protection, too.
	Format(fs fmt.State, verb rune)
	// PlainStringer is the opposite of conceal.
	// Useful for if you want to retrieve the raw value of a secret.
	PlainString() string
}

type Err

type Err struct {
	// contains filtered or unexported fields
}

Err augments an error with labels (a categorization system) and data (a map of contextual data used to record the state of the process at the time the error occurred, primarily for use in upstream logging and other telemetry),

func Label

func Label(err error, label string) *Err

func New

func New(msg string) *Err

func NewWC

func NewWC(ctx context.Context, msg string) *Err

NewWC is equivalent to clues.New("msg").WithClues(ctx)

func Stack

func Stack(errs ...error) *Err

Stack returns the error as a clues.Err. If additional errors are provided, the entire stack is flattened and returned as a single error chain. All messages and stored structure is aggregated into the returned err.

Ex: Stack(sentinel, errors.New("base")).Error() => "sentinel: base"

func StackWC

func StackWC(ctx context.Context, errs ...error) *Err

StackWC is equivalent to clues.Stack(errs...).WithClues(ctx)

func With

func With(err error, kvs ...any) *Err

With adds every two values as a key,value pair to the Err's data map. If err is not an *Err intance, returns the error wrapped into an *Err struct.

func WithClues

func WithClues(err error, ctx context.Context) *Err

WithClues is syntactical-sugar that assumes you're using the clues package to store structured data in the context. The values in the default namespace are retrieved and added to the error.

clues.WithClues(err, ctx) adds the same data as clues.WithMap(err, clues.Values(ctx)).

If the context contains a clues LabelCounter, that counter is passed to the error. WithClues must always be called first in order to count labels.

func WithMap

func WithMap(err error, m map[string]any) *Err

WithMap copies the map to the Err's data map. If err is not an *Err intance, returns the error wrapped into an *Err struct.

func WithTrace

func WithTrace(err error, depth int) *Err

WithTrace sets the error trace to a certain depth. A depth of 0 traces to the func where WithTrace is called. 1 sets the trace to its parent, etc. Error traces are already generated for the location where clues.Wrap or clues.Stack was called. This call is for cases where Wrap or Stack calls are handled in a helper func and are not reporting the actual error origin. If err is not an *Err intance, returns the error wrapped into an *Err struct.

func Wrap

func Wrap(err error, msg string) *Err

Wrap returns a clues.Err with a new message wrapping the old error.

func WrapWC

func WrapWC(ctx context.Context, err error, msg string) *Err

WrapWC is equivalent to clues.Wrap(err, "msg").WithClues(ctx) Wrap returns a clues.Err with a new message wrapping the old error.

func (*Err) As

func (err *Err) As(target any) bool

As overrides the standard As check for Err.e, allowing us to check the conditional for both Err.e and Err.next. This allows clues to Stack() maintain multiple error pointers without failing the otherwise linear errors.As check.

func (*Err) Core

func (err *Err) Core() *ErrCore

Core transforms the Err to an ErrCore, flattening all the errors in the stack into a single struct.

func (*Err) Error

func (err *Err) Error() string

func (*Err) Format

func (err *Err) Format(s fmt.State, verb rune)

Format ensures stack traces are printed appropariately.

%s    same as err.Error()
%v    equivalent to %s

Format accepts flags that alter the printing of some verbs, as follows:

%+v   Prints filename, function, and line number for each error in the stack.

func (*Err) HasLabel

func (err *Err) HasLabel(label string) bool

func (*Err) Is

func (err *Err) Is(target error) bool

Is overrides the standard Is check for Err.e, allowing us to check the conditional for both Err.e and Err.next. This allows clues to Stack() maintain multiple error pointers without failing the otherwise linear errors.Is check.

func (*Err) Label

func (err *Err) Label(labels ...string) *Err

func (*Err) Labels

func (err *Err) Labels() map[string]struct{}

func (*Err) OrNil

func (err *Err) OrNil() error

OrNil is a workaround for golang's infamous "an interface holding a nil value is not nil" gotcha. You can use it at the end of error formatting chains to ensure a correct nil return value.

func (*Err) Unwrap

func (err *Err) Unwrap() error

Unwrap provides compatibility for Go 1.13 error chains. Unwrap returns the Unwrap()ped base error, if it implements the unwrapper interface:

type unwrapper interface {
       Unwrap() error
}

If the error does not implement Unwrap, returns the base error.

func (*Err) Values

func (err *Err) Values() *dataNode

Values returns a copy of all of the contextual data in the error. Each error in the stack is unwrapped and all maps are unioned. In case of collision, lower level error data take least priority.

func (*Err) With

func (err *Err) With(kvs ...any) *Err

With adds every pair of values as a key,value pair to the Err's data map.

func (*Err) WithClues

func (err *Err) WithClues(ctx context.Context) *Err

WithClues is syntactical-sugar that assumes you're using the clues package to store structured data in the context. The values in the default namespace are retrieved and added to the error.

clues.Stack(err).WithClues(ctx) adds the same data as clues.Stack(err).WithMap(clues.Values(ctx)).

If the context contains a clues LabelCounter, that counter is passed to the error. WithClues must always be called first in order to count labels.

func (*Err) WithMap

func (err *Err) WithMap(m map[string]any) *Err

WithMap copies the map to the Err's data map.

func (*Err) WithTrace

func (err *Err) WithTrace(depth int) *Err

WithTrace sets the error trace to a certain depth. A depth of 0 traces to the func where WithTrace is called. 1 sets the trace to its parent, etc. Error traces are already generated for the location where clues.Wrap or clues.Stack was called. This call is for cases where Wrap or Stack calls are handled in a helper func and are not reporting the actual error origin.

type ErrCore

type ErrCore struct {
	Msg    string              `json:"msg"`
	Labels map[string]struct{} `json:"labels"`
	Values map[string]any      `json:"values"`
}

ErrCore is a minimized version of an Err{}. Primarily intended for serializing a flattened version of the error stack

func ToCore

func ToCore(err error) *ErrCore

ToCore transforms the Err to an ErrCore, flattening all the errors in the stack into a single struct

func (*ErrCore) Format

func (ec *ErrCore) Format(s fmt.State, verb rune)

Format provides cleaner printing of an ErrCore struct.

%s    only populated values are printed, without printing the property name.
%v    same as %s.

Format accepts flags that alter the printing of some verbs, as follows:

%+v    prints the full struct, including empty values and property names.

func (*ErrCore) String

func (ec *ErrCore) String() string

type HashCfg

type HashCfg struct {
	HashAlg hashAlg
	HMACKey []byte
}

func DefaultHash

func DefaultHash() HashCfg

DefaultHash creates a secrets configuration using the HMAC_SHA256 hash with a random key. This value is already set upon initialization of the package.

func NoHash

func NoHash() HashCfg

NoHash provides a secrets configuration with no hashing or masking of values.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL