clues

package module
v0.0.0-...-f551729 Latest Latest
Warning

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

Go to latest
Published: Nov 11, 2024 License: MIT Imports: 26 Imported by: 40

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

This section is empty.

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 AddAgent

func AddAgent(
	ctx context.Context,
	name string,
) context.Context

AddAgent adds an agent with a given name to the context. What's an agent? It's a special case data adder that you can spawn to collect clues for you. Unlike standard clues additions, you have to tell the agent exactly what data you want it to Relay() for you.

Agents are recorded in the current clues node and all of its descendants. Data relayed by the agent will appear as part of the standard data map, namespaced by each agent.

Agents are specifically handy in a certain set of uncommon cases where retrieving clues is otherwise difficult to do, such as working with middleware that doesn't allow control over error creation. In these cases your only option is to relay that data back to some prior clues node.

func AddComment

func AddComment(
	ctx context.Context,
	msg string,
	vs ...any,
) context.Context

AddComment adds a long form comment to the clues.

Comments are special case additions to the context. They're here to, well, let you add comments! Why? Because sometimes it's not sufficient to have a log let you know that a line of code was reached. Even a bunch of clues to describe system state may not be enough. Sometimes what you need in order to debug the situation is a long-form explanation (you do already add that to your code, don't you?). Or, even better, a linear history of long-form explanations, each one building on the prior (which you can't easily do in code).

Should you transfer all your comments to clues? Absolutely not. But in cases where extra explantion is truly important to debugging production, when all you've got are some logs and (maybe if you're lucky) a span trace? Those are the ones you want.

Unlike other additions, which are added as top-level key:value pairs to the context, comments are all held as a single array of additions, persisted in order of appearance, and prefixed by the file and line in which they appeared. This means comments are always added to the context and never clobber each other, regardless of their location. IE: don't add them to a loop.

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 AddSpan

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

AddSpan stacks a clues node onto this context and uses the provided name for the trace id, instead of a randomly generated hash. AddSpan can be called without additional values if you only want to add a trace marker. The assumption is that an otel span is generated and attached to the node. Callers should always follow this addition with a closing `defer clues.CloseSpan(ctx)`.

func Close

func Close(ctx context.Context) error

Close will flush all buffered data waiting to be read. If Initialize was not called, this call is a no-op. Should be called in a defer after initializing.

func CloseSpan

func CloseSpan(ctx context.Context) context.Context

CloseSpan closes the current span in the clues node. Should only be called following a `clues.AddSpan()` call.

func Comments

func Comments(err error) comments

Comments retrieves all comments in the error.

func FromBytes

func FromBytes(bs []byte) (*dataNode, error)

FromBytes deserializes the bytes to a new dataNode. No clients, agents, or hooks are initialized in this process.

func HasLabel

func HasLabel(err error, label string) bool

func In

func In(ctx context.Context) *dataNode

In returns the default dataNode from the context. TODO: turn return an interface instead of a dataNode, have dataNodes and errors both comply with that wrapper.

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. TODO: remove this in favor of a type-independent In() that returns an interface which both dataNodes and Err comply with.

func Initialize

func Initialize(
	ctx context.Context,
	serviceName string,
	config OTELConfig,
) (context.Context, error)

Initialize will spin up any persistent clients that are held by clues, such as OTEL communication. Clues will use these optimistically in the background to provide additional telemetry hook-ins.

Clues will operate as expected in the event of an error, or if initialization is not called. This is a purely optional step.

func InjectTrace

func InjectTrace[C traceMapCarrierBase](
	ctx context.Context,
	mapCarrier C,
) C

InjectTrace adds the current trace details to the provided headers. If otel is not initialized, no-ops.

The mapCarrier is mutated by this request. The passed reference is returned mostly as a quality-of-life step so that callers don't need to declare the map outside of this call.

func Labels

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

func NewAttribute

func NewAttribute(k string, v any) annotation

func ReceiveTrace

func ReceiveTrace[C traceMapCarrierBase](
	ctx context.Context,
	mapCarrier C,
) context.Context

ReceiveTrace extracts the current trace details from the headers and adds them to the context. If otel is not initialized, no-ops.

func Relay

func Relay(
	ctx context.Context,
	agent string,
	vs ...any,
)

Relay adds all key-value pairs to the provided agent. The agent will record those values to the dataNode in which it was created. All relayed values are namespaced to the owning agent.

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 Annotationer

type Annotationer interface {
	IsAttribute() bool
	KV() attribute.KeyValue
}

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 Comment

func Comment(err error, msg string, vs ...any) *Err

Comments are special case additions to the error. They're here to, well, let you add comments! Why? Because sometimes it's not sufficient to have an error message describe what that error really means. Even a bunch of clues to describe system state may not be enough. Sometimes what you need in order to debug the situation is a long-form explanation (you do already add that to your code, don't you?). Or, even better, a linear history of long-form explanations, each one building on the prior (which you can't easily do in code).

Unlike other additions, which are added as top-level key:value pairs to the context, the whole history of comments gets retained, persisted in order of appearance and prefixed by the file and line in which they appeared. This means comments are always added to the error and never clobber each other, regardless of their location.

func Label

func Label(err error, label string) *Err

func New

func New(msg string) *Err

New creates an *Err with the provided Msg.

If you have a `ctx` containing other clues data, it is recommended that you call `NewWC(ctx, msg)` to ensure that data gets added to the error.

The returned *Err is an error-compliant builder that can aggregate additional data using funcs like With(...) or Label(...).

func NewWC

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

NewWC creates an *Err with the provided Msg, and additionally extracts all of the clues data in the context into the error.

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

The returned *Err is an error-compliant builder that can aggregate additional data using funcs like With(...) or Label(...).

func Stack

func Stack(errs ...error) *Err

Stack composes a stack of one or more errors. The first message in the parameters is considered the "most recent". Ex: a construction like clues.Stack(errFoo, io.EOF, errSmarf), the resulting Error message would be "foo: end-of-file: smarf".

Unwrapping a Stack follows the same order. This allows callers to inject sentinel errors into error chains (ex: clues.Stack(io.EOF, myErr)) without losing errors.Is or errors.As checks on lower errors.

If given a single error, Stack acts as a thin wrapper around the error to provide an *Err, giving the caller access to all the builder funcs and error tracing. It is always recommended that callers `return clues.Stack(err)` instead of the plain `return err`.

The returned *Err is an error-compliant builder that can aggregate additional data using funcs like With(...) or Label(...).

Stack can be given one or more `nil` error values. Nil errors will be automatically filtered from the retained stack of errors. Ex: clues.Stack(errFoo, nil, errSmarf) == clues.Stack(errFoo, errSmarf). If all input errors are nil, stack will return nil. To avoid golang footguns when returning nil structs as interfaces (such as error), callers should always return Stack().OrNil() in cases where the input error could be nil.

func StackWC

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

StackWC composes a stack of one or more errors. The first message in the parameters is considered the "most recent". Ex: a construction like clues.StackWC(errFoo, io.EOF, errSmarf), the resulting Error message would be "foo: end-of-file: smarf".

Unwrapping a Stack follows the same order. This allows callers to inject sentinel errors into error chains (ex: clues.StackWC(io.EOF, myErr)) without losing errors.Is or errors.As checks on lower errors.

If given a single error, Stack acts as a thin wrapper around the error to provide an *Err, giving the caller access to all the builder funcs and error tracing. It is always recommended that callers `return clues.StackWC(err)` instead of the plain `return err`.

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

The returned *Err is an error-compliant builder that can aggregate additional data using funcs like With(...) or Label(...).

Stack can be given one or more `nil` error values. Nil errors will be automatically filtered from the retained stack of errors. Ex: clues.StackWC(ctx, errFoo, nil, errSmarf) == clues.StackWC(ctx, errFoo, errSmarf). If all input errors are nil, stack will return nil. To avoid golang footguns when returning nil structs as interfaces (such as error), callers should always return StackWC().OrNil() in cases where the input error could be nil.

func StackWrap

func StackWrap(sentinel, wrapped error, msg string) *Err

StackWrap is a quality-of-life shorthand for a common usage of clues errors: clues.Stack(sentinel, clues.Wrap(myErr, "my message")). The result follows all standard behavior of stacked and wrapped errors.

The returned *Err is an error-compliant builder that can aggregate additional data using funcs like With(...) or Label(...).

StackWrap can be given one or more `nil` error values. Nil errors will be automatically filtered from the retained stack of errors. Ex: clues.StackWrap(errFoo, nil, "msg") == clues.Wrap(errFoo, "msg"). If both input errors are nil, StackWrap will return nil. To avoid golang footguns when returning nil structs as interfaces (such as error), callers should always return StackWrap().OrNil() in cases where the input errors could be nil.

func StackWrapWC

func StackWrapWC(
	ctx context.Context,
	sentinel, wrapped error,
	msg string,
) *Err

StackWrapWC is a quality-of-life shorthand for a common usage of clues errors: clues.Stack(sentinel, clues.Wrap(myErr, "my message")).WithClues(ctx). The result follows all standard behavior of stacked and wrapped errors.

The returned *Err is an error-compliant builder that can aggregate additional data using funcs like With(...) or Label(...).

StackWrapWC can be given one or more `nil` error values. Nil errors will be automatically filtered from the retained stack of errors. Ex: clues.StackWrapWC(ctx, errFoo, nil, "msg") == clues.WrapWC(ctx, errFoo, "msg"). If both input errors are nil, StackWrap will return nil. To avoid golang footguns when returning nil structs as interfaces (such as error), callers should always return StackWrap().OrNil() in cases where the input errors could be nil.

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, a new *Err is generated containing the original err.

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 WithSkipCaller

func WithSkipCaller(err error, depth int) *Err

SkipCaller skips <depth> callers when constructing the error trace stack. The caller is the file, line, and func where the *clues.Err was generated.

A depth of 0 performs no skips, and returns the same caller info as if SkipCaller was not called. 1 skips the immediate parent, etc.

Error traces are already generated for the location where clues.Wrap or clues.Stack was called. This func 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 extends an error with the provided message. It is a replacement for `errors.Wrap`, and complies with all golang unwrapping behavior.

If you have a `ctx` containing other clues data, it is recommended that you call `WrapWC(ctx, err, msg)` to ensure that data gets added to the error.

The returned *Err is an error-compliant builder that can aggregate additional data using funcs like With(...) or Label(...). There is no Wrapf func in clues; we prefer that callers use Wrap().With() instead.

Wrap can be given a `nil` error value, and will return a nil *Err. To avoid golang footguns when returning nil structs as interfaces (such as error), callers should always return Wrap().OrNil() in cases where the input error could be nil.

func WrapWC

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

WrapWC extends an error with the provided message. It is a replacement for `errors.Wrap`, and complies with all golang unwrapping behavior.

WrapWC is equivalent to clues.Wrap(err, "msg").WithClues(ctx).

If you have a `ctx` containing other clues data, it is recommended that you call `WrapWC(ctx, err, msg)` to ensure that data gets added to the error.

The returned *Err is an error-compliant builder that can aggregate additional data using funcs like With(...) or Label(...). There is no WrapWCf func in clues; we prefer that callers use WrapWC().With() instead.

Wrap can be given a `nil` error value, and will return a nil *Err. To avoid golang footguns when returning nil structs as interfaces (such as error), callers should always return WrapWC().OrNil() in cases where the input error could be nil.

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.stack. This allows clues to Stack() multiple error pointers without failing the otherwise linear errors.As check.

func (*Err) Comment

func (err *Err) Comment(msg string, vs ...any) *Err

Comments are special case additions to the error. They're here to, well, let you add comments! Why? Because sometimes it's not sufficient to have an error message describe what that error really means. Even a bunch of clues to describe system state may not be enough. Sometimes what you need in order to debug the situation is a long-form explanation (you do already add that to your code, don't you?). Or, even better, a linear history of long-form explanations, each one building on the prior (which you can't easily do in code).

Unlike other additions, which are added as top-level key:value pairs to the context, the whole history of comments gets retained, persisted in order of appearance and prefixed by the file and line in which they appeared. This means comments are always added to the error and never clobber each other, regardless of their location.

func (*Err) Comments

func (err *Err) Comments() comments

Comments retrieves all comments in the error.

func (*Err) Core

func (err *Err) Core() *ErrCore

Core transforms the error into an ErrCore. ErrCore is a minimized version of an Err{}. It produces a concrete, storable version of the clues error data. Rather than expose the underlying error structure that's used for building metadata, an error core synthesizes the hierarchical storage of errors and data nodes into a flat, easily consumed set of properties.

func (*Err) Error

func (err *Err) Error() string

Error allows Err to be used as a standard error interface.

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.stack. This allows clues to Stack() 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) NoTrace

func (err *Err) NoTrace() *Err

NoTrace prevents the error from appearing in the trace stack. This is particularly useful for global sentinels that get stacked or wrapped into other error cases.

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 should use it to ensure the error value to produce is properly nil whenever your wrapped or stacked error values could also possibly be nil.

ie: ``` return clues.Stack(maybeNilErrValue).OrNil() // or return clues.Wrap(maybeNilErrValue, "msg").OrNil() ```

func (*Err) SkipCaller

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

SkipCaller skips <depth> callers when constructing the error trace stack. The caller is the file, line, and func where the *clues.Err was generated.

A depth of 0 performs no skips, and returns the same caller info as if SkipCaller was not called. 1 skips the immediate parent, etc.

Error traces are already generated for the location where clues.Wrap or clues.Stack was called. This func is for cases where Wrap or Stack calls are handled in a helper func and are not reporting the actual error origin.

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.

type ErrCore

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

ErrCore is a minimized version of an Err{}. It produces a concrete, storable version of the clues error data. Rather than expose the underlying error structure that's used for building metadata, an error core synthesizes the hierarchical storage of errors and data nodes into a flat, easily consumed set of properties.

func ToCore

func ToCore(err error) *ErrCore

ToCore transforms the error into an ErrCore. ErrCore is a minimized version of an Err{}. It produces a concrete, storable version of the clues error data. Rather than expose the underlying error structure that's used for building metadata, an error core synthesizes the hierarchical storage of errors and data nodes into a flat, easily consumed set of properties.

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 OTELConfig

type OTELConfig struct {
	// specify the endpoint location to use for grpc communication.
	// If empty, no telemetry exporter will be generated.
	// ex: localhost:4317
	// ex: 0.0.0.0:4317
	GRPCEndpoint string
}

Directories

Path Synopsis
internal

Jump to

Keyboard shortcuts

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