From 30e3f6a4b8fa6b6dcb020a40b9b17d0b1c77b835 Mon Sep 17 00:00:00 2001 From: Keepers Date: Tue, 3 Dec 2024 14:03:43 -0700 Subject: [PATCH] move datanode to internal/node (#82) Clues has grown organically into a split between the api that places data in the right spot (clues) and the structure which contains the data (nodes). As we move more behavior into subpackages, and as we look forward to future subpackage additions, it makes the most sense to move the node systems into an internal package so that they can be generically utilized across all clues subpackages without issues like import cycles. This movement is 80% copy/paste, 19% renaming and reorganization, and 1% interface/import adaptation. No logical changes were intended. - copy/paste: datanode.go has moved into /internal/node/node.go. otel.go has moved into /internal/node/otel.go. - reorganization: datanode.go has been broken into multiple files. Discrete concerns (comments, agents, otel) all exist as various files within /node. Generic and basic functionality stays in /node/node.go. - adaptation: OTELConfig now has a user-facing config builder in /clues/otel.go, while the core otel config struct still lives in /internal/node/otel.go. --- best_practices.md | 31 +- clog/builder.go | 10 +- clog/builder_test.go | 26 +- clog/logger.go | 21 + clog/settings.go | 8 +- clog/settings_test.go | 4 +- cluerr/clerr.go | 449 +++++++ cluerr/comments.go | 59 + cluerr/err.go | 414 +++++++ .../err_benchmark_test.go | 103 +- err_fmt_test.go => cluerr/err_fmt_test.go | 494 ++++---- err_test.go => cluerr/err_test.go | 507 ++++---- errcore.go => cluerr/errcore.go | 5 +- cluerr/labels.go | 87 ++ clues.go | 98 +- clues_test.go | 321 ++--- datanode.go | 680 ----------- err.go | 1078 ----------------- internal/node/agents.go | 33 + internal/node/comments.go | 105 ++ internal/node/node.go | 351 ++++++ .../node/node_test.go | 40 +- internal/node/otel.go | 322 +++++ internal/node/stacktrace.go | 67 + internal/tester/tester.go | 180 +++ otel.go | 183 +-- 26 files changed, 2844 insertions(+), 2832 deletions(-) create mode 100644 cluerr/clerr.go create mode 100644 cluerr/comments.go create mode 100644 cluerr/err.go rename err_benchmark_test.go => cluerr/err_benchmark_test.go (80%) rename err_fmt_test.go => cluerr/err_fmt_test.go (77%) rename err_test.go => cluerr/err_test.go (67%) rename errcore.go => cluerr/errcore.go (96%) create mode 100644 cluerr/labels.go delete mode 100644 datanode.go delete mode 100644 err.go create mode 100644 internal/node/agents.go create mode 100644 internal/node/comments.go create mode 100644 internal/node/node.go rename datanode_test.go => internal/node/node_test.go (79%) create mode 100644 internal/node/otel.go create mode 100644 internal/node/stacktrace.go create mode 100644 internal/tester/tester.go diff --git a/best_practices.md b/best_practices.md index 06034d5..12b6c30 100644 --- a/best_practices.md +++ b/best_practices.md @@ -5,15 +5,15 @@ A guide for getting the most out of Clues. ## CTX The clues package can leverage golang's `context.Context` to pack in -metadata that you can later retrieve for logging or observation. These +metadata that you can later retrieve for logging or observation. These additions form a tree, so you're always safe to extend or replace existing state. ### Tracing Calling `AddTrace` or `AddTraceName` will append to an internal property -with the key `clues_trace`. Clues traces form a slice of comma delimited -hashes (or names, when using TraceName). These can be useful for filtering +with the key `clues_trace`. Clues traces form a slice of comma delimited +hashes (or names, when using TraceName). These can be useful for filtering logs to certain process branches. clues.Add() always automatically appends to the trace, so you don't need @@ -57,16 +57,15 @@ for _, user := range users { Errors are the bottom-to-top metadata tracing counterpart to contexts. At minimum, they replicate the creation, wrapping, and stacking of -errors. You can also label clues errors for broad categorization and +errors. You can also label clues errors for broad categorization and add key:value metadata sets, including the full set of clues embedded in a ctx value. - ### Always Stack A single-error Stack ensures clues will append a stacktrace reference -for that point of return. New(), Wrap() and Stack()ing multiple errors -all do this same process. This tip is specific for cases where you'd +for that point of return. New(), Wrap() and Stack()ing multiple errors +all do this same process. This tip is specific for cases where you'd normally `return err`. ```go @@ -122,8 +121,8 @@ if err != nil { ### Log the Core The clues within errors aren't going to show up in a log message -or test output naturally. The formatted value will only include -the message. You can easily extract the full details using ToCore(). +or test output naturally. The formatted value will only include +the message. You can easily extract the full details using ToCore(). ```go // in logs: @@ -135,7 +134,7 @@ assert.NoError(t, err, clues.ToCore(err)) ### Labels Are Not Sentinels Clues errors already support Is and As, which means you can use them -for errors.Is and errors.As calls. This means you shouldn't use +for errors.Is and errors.As calls. This means you shouldn't use labels for the same purpose. ```go @@ -146,26 +145,26 @@ clues.Stack(errByRespCode(resp.StatusCode), err) ``` The best usage of labels is when you want to add _identifiable_ metadata -to an error. This is great for conditions where multiple different -errors can be flagged to get some specific handling. This allows you +to an error. This is great for conditions where multiple different +errors can be flagged to get some specific handling. This allows you to identify the condition without altering the error itself. ```go -// example 1: +// example 1: // doesn't matter what error took place, we want to end in a // certain process state as a categorical result for err := range processCh { if clues.HasLabel(err, mustFailBackup) { - // set the backup state to 'failed' + // set the backup state to 'failed' } } // example 2: -// we can categorically ignore errors based on configuration. +// we can categorically ignore errors based on configuration. for _, err := range processFailures { for _, cat := range config.ignoreErrorCategories { if clues.HasLabel(err, cat) { - processFailures.IgnoreError(err) + processFailures.IgnoreError(err) } } } diff --git a/clog/builder.go b/clog/builder.go index d462ae8..f5139d4 100644 --- a/clog/builder.go +++ b/clog/builder.go @@ -6,6 +6,8 @@ import ( "reflect" "github.com/alcionai/clues" + "github.com/alcionai/clues/cluerr" + "github.com/alcionai/clues/internal/node" "github.com/alcionai/clues/internal/stringify" otellog "go.opentelemetry.io/otel/log" "go.uber.org/zap" @@ -67,11 +69,11 @@ func (b builder) log(l logLevel, msg string) { if b.err != nil { // error values should override context values. - maps.Copy(cv, clues.InErr(b.err).Map()) + maps.Copy(cv, cluerr.CluesIn(b.err).Map()) // attach the error and its labels cv["error"] = b.err - cv["error_labels"] = clues.Labels(b.err) + cv["error_labels"] = cluerr.Labels(b.err) } // finally, make sure we attach the labels and comments @@ -86,7 +88,7 @@ func (b builder) log(l logLevel, msg string) { for k, v := range cv { zsl = zsl.With(k, v) - attr := clues.NewAttribute(k, v) + attr := node.NewAttribute(k, v) record.AddAttributes(attr.KV()) } @@ -94,7 +96,7 @@ func (b builder) log(l logLevel, msg string) { for k, v := range b.with { zsl.With(k, v) - attr := clues.NewAttribute(stringify.Fmt(k)[0], v) + attr := node.NewAttribute(stringify.Fmt(k)[0], v) record.AddAttributes(attr.KV()) } diff --git a/clog/builder_test.go b/clog/builder_test.go index 561cdcb..307dc20 100644 --- a/clog/builder_test.go +++ b/clog/builder_test.go @@ -4,7 +4,7 @@ import ( "context" "testing" - "github.com/alcionai/clues" + "github.com/alcionai/clues/cluerr" "github.com/stretchr/testify/assert" ) @@ -53,17 +53,17 @@ func TestBuilder(t *testing.T) { bld.SkipCaller(1) - // ensure no collision between separate builders - // using the same ctx. - err := clues.New("an error"). - With("fnords", "i have seen them"). - Label("errLabel") + // ensure no collision between separate builders + // using the same ctx. + err := cluerr.New("an error"). + With("fnords", "i have seen them"). + Label("errLabel") - other := CtxErr(ctx, err) - assert.Empty(t, other.with) - assert.Empty(t, other.labels) - assert.Empty(t, other.comments) - assert.ErrorIs(t, other.err, err, clues.ToCore(err)) + other := CtxErr(ctx, err) + assert.Empty(t, other.with) + assert.Empty(t, other.labels) + assert.Empty(t, other.comments) + assert.ErrorIs(t, other.err, err, cluerr.ToCore(err)) other.With("foo", "smarf") assert.Contains(t, other.with, "foo") @@ -126,8 +126,8 @@ func runErrorLogs( func TestGetValue(t *testing.T) { var ( - p1 int = 1 - ps string = "ptr" + p1 = 1 + ps = "ptr" pn any ) diff --git a/clog/logger.go b/clog/logger.go index 1d7e387..da7f66d 100644 --- a/clog/logger.go +++ b/clog/logger.go @@ -2,6 +2,7 @@ package clog import ( "context" + "errors" "os" "sync" "time" @@ -223,6 +224,26 @@ func CtxErr(ctx context.Context, err error) *builder { return nb } +// Singleton is a shorthand for .Ctx(context.Background()). IE: it'll use the singleton +// logger directly; building one if necessary. You should avoid this and use .Ctx or +// .CtxErr if possible. Likelihood is that you're somewhere deep in a func chain that +// doesn't accept a ctx, and you still want to add a quick log; maybe for debugging purposes. +// +// That's fine! Everything should work great. +// +// Unless you call this before initialization. Then it'll panic. We do want you to init +// the logger first, else you'll potentially lose these logs due different buffers. +func Singleton() *builder { + if cloggerton == nil { + panic(errors.New("clog singleton requires prior initialization")) + } + + return &builder{ + ctx: context.Background(), + zsl: cloggerton.zsl, + } +} + // Flush writes out all buffered logs. // Probably good to do before shutting down whatever instance // had initialized the singleton. diff --git a/clog/settings.go b/clog/settings.go index 687c591..f648fa1 100644 --- a/clog/settings.go +++ b/clog/settings.go @@ -6,16 +6,14 @@ import ( "golang.org/x/exp/slices" - "github.com/alcionai/clues" "github.com/alcionai/clues/cecrets" + "github.com/alcionai/clues/cluerr" ) // --------------------------------------------------- // consts // --------------------------------------------------- -const clogLogFileEnv = "CLOG_LOG_FILE" - type logLevel string const ( @@ -84,14 +82,14 @@ func (s Settings) LogToStdOut() Settings { // LogToFile defines a system file to write all logs onto. func (s Settings) LogToFile(pathToFile string) (Settings, error) { if len(pathToFile) == 0 { - return s, clues.New("missing filepath for logging") + return s, cluerr.New("missing filepath for logging") } logdir := filepath.Dir(pathToFile) err := os.MkdirAll(logdir, 0o755) if err != nil { - return s, clues.Wrap(err, "ensuring log file dir exists"). + return s, cluerr.Wrap(err, "ensuring log file dir exists"). With("log_dir", logdir) } diff --git a/clog/settings_test.go b/clog/settings_test.go index 97fdede..e6ce4b3 100644 --- a/clog/settings_test.go +++ b/clog/settings_test.go @@ -4,7 +4,7 @@ import ( "path/filepath" "testing" - "github.com/alcionai/clues" + "github.com/alcionai/clues/cluerr" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -40,7 +40,7 @@ func TestSettings_LogToFile(t *testing.T) { for _, test := range table { t.Run(test.name, func(t *testing.T) { set, err := Settings{}.LogToFile(test.input) - test.expectErr(t, err, clues.ToCore(err)) + test.expectErr(t, err, cluerr.ToCore(err)) assert.Equal(t, test.expectOverride, set.fileOverride) }) } diff --git a/cluerr/clerr.go b/cluerr/clerr.go new file mode 100644 index 0000000..f7b547d --- /dev/null +++ b/cluerr/clerr.go @@ -0,0 +1,449 @@ +package cluerr + +import ( + "context" + + "github.com/alcionai/clues/internal/node" + "github.com/alcionai/clues/internal/stringify" +) + +// ------------------------------------------------------------ +// constructors +// ------------------------------------------------------------ + +// 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 New(msg string) *Err { + return newErr(nil, msg, nil, 1) +} + +// 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 NewWC(ctx context.Context, msg string) *Err { + return newErr(nil, msg, nil, 1).WithClues(ctx) +} + +// 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 Wrap(err error, msg string) *Err { + if isNilErrIface(err) { + return nil + } + + return newErr(err, msg, nil, 1) +} + +// 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 WrapWC(ctx context.Context, err error, msg string) *Err { + if isNilErrIface(err) { + return nil + } + + return newErr(err, msg, nil, 1).WithClues(ctx) +} + +// 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 Stack(errs ...error) *Err { + return makeStack(1, errs...) +} + +// 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 StackWC(ctx context.Context, errs ...error) *Err { + err := makeStack(1, errs...) + + if isNilErrIface(err) { + return nil + } + + return err.WithClues(ctx) +} + +// 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 StackWrap(sentinel, wrapped error, msg string) *Err { + return makeStackWrap(1, sentinel, wrapped, msg) +} + +// 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 StackWrapWC( + ctx context.Context, + sentinel, wrapped error, + msg string, +) *Err { + err := makeStackWrap(1, sentinel, wrapped, msg) + + if isNilErrIface(err) { + return nil + } + + return err.WithClues(ctx) +} + +// 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 *Err) OrNil() error { + if isNilErrIface(err) { + return nil + } + + return err +} + +// ------------------------------------------------------------ +// attributes +// ------------------------------------------------------------ + +// With adds every pair of values as a key,value pair to +// the Err's data map. +func (err *Err) With(kvs ...any) *Err { + if isNilErrIface(err) { + return nil + } + + if len(kvs) > 0 { + err.data = err.data.AddValues(stringify.Normalize(kvs...)) + } + + return err +} + +// WithMap copies the map to the Err's data map. +func (err *Err) WithMap(m map[string]any) *Err { + if isNilErrIface(err) { + return nil + } + + if len(m) > 0 { + err.data = err.data.AddValues(m) + } + + return err +} + +// ------------------------------------------------------------ +// stacktrace +// ------------------------------------------------------------ + +// SkipCaller skips 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 SkipCaller(err error, depth int) *Err { + if isNilErrIface(err) { + return nil + } + + // needed both here and in withTrace() to + // correct for the extra call depth. + if depth < 0 { + depth = 0 + } + + e, ok := err.(*Err) + if !ok { + return newErr(err, "", map[string]any{}, depth+1) + } + + return e.SkipCaller(depth + 1) +} + +// SkipCaller skips 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 *Err) SkipCaller(depth int) *Err { + if isNilErrIface(err) { + return nil + } + + // needed both here and in withTrace() to + // correct for the extra call depth. + if depth < 0 { + depth = 0 + } + + _, _, err.file = node.GetDirAndFile(depth + 1) + err.caller = node.GetCaller(depth + 1) + + return 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 *Err) NoTrace() *Err { + if isNilErrIface(err) { + return nil + } + + err.file = "" + err.caller = "" + + return err +} + +// --------------------------------------------------------------------------- +// helpers +// --------------------------------------------------------------------------- + +// newErr generates a new *Err from the parameters. +// traceDepth should always be `1` or `depth+1`. +func newErr( + e error, + msg string, + m map[string]any, + traceDepth int, +) *Err { + _, _, file := node.GetDirAndFile(traceDepth + 1) + + return &Err{ + e: e, + file: file, + caller: node.GetCaller(traceDepth + 1), + msg: msg, + // no ID needed for err data nodes + data: &node.Node{Values: m}, + } +} + +// tryExtendErr checks if err is an *Err. If it is, it extends the Err +// with a child containing the provided parameters. If not, it creates +// a new Err containing the parameters. +// traceDepth should always be `1` or `depth+1`. +func tryExtendErr( + err error, + msg string, + m map[string]any, + traceDepth int, +) *Err { + if isNilErrIface(err) { + return nil + } + + e, ok := err.(*Err) + if !ok { + e = newErr(err, msg, m, traceDepth+1) + } + + return e +} + +// newStack creates a new *Err containing the provided stack of errors. +// traceDepth should always be `1` or `depth+1`. +func toStack( + e error, + stack []error, + traceDepth int, +) *Err { + _, _, file := node.GetDirAndFile(traceDepth + 1) + + return &Err{ + e: e, + file: file, + caller: node.GetCaller(traceDepth + 1), + stack: stack, + // no ID needed for err nodes + data: &node.Node{}, + } +} + +// makeStack creates a new *Err from the provided stack of errors. +// nil values are filtered out of the errs slice. If all errs are nil, +// returns nil. +// traceDepth should always be `1` or `depth+1`. +func makeStack( + traceDepth int, + errs ...error, +) *Err { + filtered := []error{} + for _, err := range errs { + if !isNilErrIface(err) { + filtered = append(filtered, err) + } + } + + switch len(filtered) { + case 0: + return nil + case 1: + return newErr(filtered[0], "", nil, traceDepth+1) + } + + return toStack(filtered[0], filtered[1:], traceDepth+1) +} + +// makeStackWrap creates a new *Err from the provided pair of sentinal +// and wrapped errors. If sentinel is nil, wraps the wrapped error. +// If wrapped is nil, wraps the sentinel error. If the message is empty, +// returns a stack(sentinel, wrapped). Otherwise, makes a stack headed +// by the sentinel error, and wraps the wrapped error in the message. +func makeStackWrap( + traceDepth int, + sentinel, wrapped error, + msg string, +) *Err { + if isNilErrIface(sentinel) && isNilErrIface(wrapped) { + return nil + } + + if len(msg) == 0 { + return makeStack(traceDepth+1, sentinel, wrapped) + } + + if isNilErrIface(sentinel) { + return newErr(wrapped, msg, nil, traceDepth+1) + } + + if isNilErrIface(wrapped) { + return newErr(sentinel, msg, nil, traceDepth+1) + } + + return makeStack( + 1, + sentinel, + newErr(wrapped, msg, nil, traceDepth+1)) +} diff --git a/cluerr/comments.go b/cluerr/comments.go new file mode 100644 index 0000000..08a5d96 --- /dev/null +++ b/cluerr/comments.go @@ -0,0 +1,59 @@ +package cluerr + +import "github.com/alcionai/clues/internal/node" + +// ------------------------------------------------------------ +// comments +// ------------------------------------------------------------ + +// Comments retrieves all comments in the error. +func (err *Err) Comments() node.CommentHistory { + return Comments(err) +} + +// Comments retrieves all comments in the error. +func Comments(err error) node.CommentHistory { + if isNilErrIface(err) { + return node.CommentHistory{} + } + + ancs := ancestors(err) + result := node.CommentHistory{} + + for _, ancestor := range ancs { + ce, ok := ancestor.(*Err) + if !ok { + continue + } + + result = append(result, ce.data.Comments()...) + } + + return result +} + +// Comment is a 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 *Err) Comment(msg string, vs ...any) *Err { + if isNilErrIface(err) { + return nil + } + + return &Err{ + e: err, + // have to do a new node here, or else comments will duplicate + data: &node.Node{Comment: node.NewComment(1, msg, vs...)}, + } +} diff --git a/cluerr/err.go b/cluerr/err.go new file mode 100644 index 0000000..098e4cb --- /dev/null +++ b/cluerr/err.go @@ -0,0 +1,414 @@ +package cluerr + +import ( + "context" + "errors" + "fmt" + "io" + "reflect" + "strings" + + "github.com/alcionai/clues/internal/node" + "golang.org/x/exp/maps" +) + +// 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), +type Err struct { + // e holds the base error. + e error + + // stack is a chain of errors that this error is stacked onto. + // stacks may contain other stacks, forming a tree. + // Funcs that examine or flatten the tree will walk its structure + // in pre-order traversal. + stack []error + + // the name of the file where the caller func is found. + file string + // the name of the func where the error (or wrapper) was generated. + caller string + + // msg is the message for this error. + msg string + + // labels contains a map of the labels applied + // to this error. Can be used to identify error + // categorization without applying an error type. + labels map[string]struct{} + + // data is the record of contextual data produced, + // presumably, at the time the error is created or wrapped. + data *node.Node +} + +// ------------------------------------------------------------ +// tree operations +// ------------------------------------------------------------ + +// ancestors builds out the ancestor lineage of this +// particular error. This follows standard layout rules +// already established elsewhere: +// * the first entry is the oldest ancestor, the last is +// the current error. +// * Stacked errors get visited before wrapped errors. +func ancestors(err error) []error { + return stackAncestorsOntoSelf(err) +} + +// a recursive function, purely for building out ancestorStack. +func stackAncestorsOntoSelf(err error) []error { + if err == nil { + return []error{} + } + + errs := []error{} + + ce, ok := err.(*Err) + + if ok { + for _, e := range ce.stack { + errs = append(errs, stackAncestorsOntoSelf(e)...) + } + } + + unwrapped := unwrap(err) + + if unwrapped != nil { + errs = append(errs, stackAncestorsOntoSelf(unwrapped)...) + } + + errs = append(errs, err) + + return errs +} + +// ------------------------------------------------------------ +// eror interface compliance and stringers +// ------------------------------------------------------------ + +var _ error = &Err{} + +// Error allows Err to be used as a standard error interface. +func (err *Err) Error() string { + if isNilErrIface(err) { + return "" + } + + msg := []string{} + + if len(err.msg) > 0 { + msg = append(msg, err.msg) + } + + if err.e != nil { + msg = append(msg, err.e.Error()) + } + + for _, se := range err.stack { + msg = append(msg, se.Error()) + } + + return strings.Join(msg, ": ") +} + +// format is the fallback formatting of an error +func format(err error, s fmt.State, verb rune) { + if isNilErrIface(err) { + return + } + + f, ok := err.(fmt.Formatter) + if ok { + f.Format(s, verb) + } else { + write(s, verb, err.Error()) + } +} + +// For all formatting besides %+v, the error printout should closely +// mimic that of err.Error(). +func formatReg(err *Err, s fmt.State, verb rune) { + if isNilErrIface(err) { + return + } + + write(s, verb, err.msg) + + if len(err.msg) > 0 && err.e != nil { + io.WriteString(s, ": ") + } + + format(err.e, s, verb) + + if (len(err.msg) > 0 || err.e != nil) && len(err.stack) > 0 { + io.WriteString(s, ": ") + } + + for _, e := range err.stack { + format(e, s, verb) + } +} + +// in %+v formatting, we output errors FIFO (ie, read from the +// bottom of the stack first). +func formatPlusV(err *Err, s fmt.State, verb rune) { + if isNilErrIface(err) { + return + } + + for i := len(err.stack) - 1; i >= 0; i-- { + e := err.stack[i] + format(e, s, verb) + } + + if len(err.stack) > 0 && err.e != nil { + io.WriteString(s, "\n") + } + + format(err.e, s, verb) + + if (len(err.stack) > 0 || err.e != nil) && len(err.msg) > 0 { + io.WriteString(s, "\n") + } + + write(s, verb, err.msg) + + parts := []string{} + if len(err.caller) > 0 { + parts = append(parts, err.caller) + } + + if len(err.file) > 0 { + parts = append(parts, err.file) + } + + write(s, verb, "\n\t%s", strings.Join(parts, " - ")) +} + +// 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 *Err) Format(s fmt.State, verb rune) { + if isNilErrIface(err) { + return + } + + if verb == 'v' && s.Flag('+') { + formatPlusV(err, s, verb) + return + } + + formatReg(err, s, verb) +} + +func write(s fmt.State, verb rune, msgs ...string) { + if len(msgs) == 0 || len(msgs[0]) == 0 { + return + } + + switch verb { + case 'v': + if s.Flag('+') { + if len(msgs) == 1 { + io.WriteString(s, msgs[0]) + } else if len(msgs[1]) > 0 { + fmt.Fprintf(s, msgs[0], msgs[1]) + } + return + } + + fallthrough + + case 's': + io.WriteString(s, msgs[0]) + + case 'q': + fmt.Fprintf(s, "%q", msgs[0]) + } +} + +// ------------------------------------------------------------ +// common interface compliance +// ------------------------------------------------------------ + +// 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 *Err) Is(target error) bool { + if isNilErrIface(err) { + return false + } + + if errors.Is(err.e, target) { + return true + } + + for _, se := range err.stack { + if errors.Is(se, target) { + return true + } + } + + return false +} + +// 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 *Err) As(target any) bool { + if isNilErrIface(err) { + return false + } + + if errors.As(err.e, target) { + return true + } + + for _, se := range err.stack { + if errors.As(se, target) { + return true + } + } + + return false +} + +// 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 *Err) Unwrap() error { + if isNilErrIface(err) { + return nil + } + + return err.e +} + +// unwrap attempts to unwrap any generic error. +func unwrap(err error) error { + if isNilErrIface(err) { + return nil + } + + if e, ok := err.(*Err); ok { + return e.e + } + + u, ok := err.(interface{ Unwrap() error }) + if !ok { + return nil + } + + ue := u.Unwrap() + return ue +} + +// ------------------------------------------------------------ +// nodes and node attributes +// ------------------------------------------------------------ + +// 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 *Err) WithClues(ctx context.Context) *Err { + if isNilErrIface(err) { + return nil + } + + dn := node.FromCtx(ctx) + e := err.WithMap(dn.Map()) + + return e +} + +// CluesIn returns the structured 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 CluesIn(err error) *node.Node { + if isNilErrIface(err) { + return &node.Node{} + } + + return &node.Node{Values: cluesIn(err)} +} + +func cluesIn(err error) map[string]any { + if isNilErrIface(err) { + return map[string]any{} + } + + if e, ok := err.(*Err); ok { + return e.values() + } + + return cluesIn(unwrap(err)) +} + +// 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 *Err) Values() *node.Node { + if isNilErrIface(err) { + return &node.Node{} + } + + return &node.Node{Values: err.values()} +} + +func (err *Err) values() map[string]any { + if isNilErrIface(err) { + return map[string]any{} + } + + vals := map[string]any{} + maps.Copy(vals, err.data.Map()) + maps.Copy(vals, cluesIn(err.e)) + + for _, se := range err.stack { + maps.Copy(vals, cluesIn(se)) + } + + return vals +} + +// ------------------------------------------------------------ +// helpers +// ------------------------------------------------------------ + +// returns true if the error is nil, or if it is a non-nil interface +// containing a nil value. +func isNilErrIface(err error) bool { + if err == nil { + return true + } + + val := reflect.ValueOf(err) + + return ((val.Kind() == reflect.Pointer || val.Kind() == reflect.Interface) && val.IsNil()) +} diff --git a/err_benchmark_test.go b/cluerr/err_benchmark_test.go similarity index 80% rename from err_benchmark_test.go rename to cluerr/err_benchmark_test.go index 1837e77..c8da209 100644 --- a/err_benchmark_test.go +++ b/cluerr/err_benchmark_test.go @@ -1,4 +1,4 @@ -package clues_test +package cluerr_test import ( "context" @@ -6,87 +6,106 @@ import ( "testing" "github.com/alcionai/clues" + "github.com/alcionai/clues/cluerr" + "golang.org/x/exp/rand" ) +var ( + benchKeys []int64 + benchVals []int64 +) + +const benchSize = 4096 + +func init() { + benchKeys, benchVals = make([]int64, benchSize), make([]int64, benchSize) + for i := 0; i < benchSize; i++ { + benchKeys[i], benchVals[i] = rand.Int63(), rand.Int63() + } + rand.Shuffle(benchSize, func(i, j int) { + benchKeys[i], benchKeys[j] = benchKeys[j], benchKeys[i] + }) +} + func BenchmarkWith_singleConstKConstV(b *testing.B) { - err := clues.New("err") + err := cluerr.New("err") for i := 0; i < b.N; i++ { err = err.With("foo", "bar") } } func BenchmarkWith_singleStaticKStaticV(b *testing.B) { - err := clues.New("err") + err := cluerr.New("err") for i := 0; i < b.N; i++ { err = err.With(benchSize-i, i) } } func BenchmarkWith_singleConstKStaticV(b *testing.B) { - err := clues.New("err") + err := cluerr.New("err") for i := 0; i < b.N; i++ { err = err.With("foo", i) } } func BenchmarkWith_singleStaticKConstV(b *testing.B) { - err := clues.New("err") + err := cluerr.New("err") for i := 0; i < b.N; i++ { err = err.With(i, "bar") } } func BenchmarkWith_singleConstKRandV(b *testing.B) { - err := clues.New("err") + err := cluerr.New("err") for i := 0; i < b.N; i++ { err = err.With("foo", benchVals[i%benchSize]) } } func BenchmarkWith_singleRandKConstV(b *testing.B) { - err := clues.New("err") + err := cluerr.New("err") for i := 0; i < b.N; i++ { err = err.With(benchVals[i%benchSize], "bar") } } func BenchmarkWith_singleRandKRandV(b *testing.B) { - err := clues.New("err") + err := cluerr.New("err") for i := 0; i < b.N; i++ { err = err.With(benchVals[i%benchSize], benchVals[i%benchSize]) } } func BenchmarkWith_multConstKConstV(b *testing.B) { - err := clues.New("err") + err := cluerr.New("err") for i := 0; i < b.N; i++ { err = err.With("foo", "bar", "baz", "qux") } } func BenchmarkWith_multStaticKStaticV(b *testing.B) { - err := clues.New("err") + err := cluerr.New("err") for i := 0; i < b.N; i++ { err = err.With(benchSize-i, i, i-benchSize, i) } } func BenchmarkWith_multConstKStaticV(b *testing.B) { - err := clues.New("err") + err := cluerr.New("err") for i := 0; i < b.N; i++ { err = err.With("foo", i, "baz", -i) } } func BenchmarkWith_multStaticKConstV(b *testing.B) { - err := clues.New("err") + err := cluerr.New("err") for i := 0; i < b.N; i++ { err = err.With(i, "bar", -i, "qux") } } func BenchmarkWith_multConstKRandV(b *testing.B) { - err := clues.New("err") + err := cluerr.New("err") for i := 0; i < b.N; i++ { err = err.With( "foo", benchVals[i%benchSize], @@ -95,7 +114,7 @@ func BenchmarkWith_multConstKRandV(b *testing.B) { } func BenchmarkWith_multRandKConstV(b *testing.B) { - err := clues.New("err") + err := cluerr.New("err") for i := 0; i < b.N; i++ { err = err.With( benchVals[i%benchSize], "bar", @@ -104,7 +123,7 @@ func BenchmarkWith_multRandKConstV(b *testing.B) { } func BenchmarkWith_multRandKRandV(b *testing.B) { - err := clues.New("err") + err := cluerr.New("err") for i := 0; i < b.N; i++ { err = err.With( benchVals[i%benchSize], benchVals[i%benchSize], @@ -113,7 +132,7 @@ func BenchmarkWith_multRandKRandV(b *testing.B) { } func BenchmarkWith_chainConstKConstV(b *testing.B) { - err := clues.New("err") + err := cluerr.New("err") for i := 0; i < b.N; i++ { err = err.With("foo", "bar"). With("baz", "qux") @@ -121,7 +140,7 @@ func BenchmarkWith_chainConstKConstV(b *testing.B) { } func BenchmarkWith_chainStaticKStaticV(b *testing.B) { - err := clues.New("err") + err := cluerr.New("err") for i := 0; i < b.N; i++ { err = err.With(benchSize-i, i). With(i-benchSize, i) @@ -129,7 +148,7 @@ func BenchmarkWith_chainStaticKStaticV(b *testing.B) { } func BenchmarkWith_chainConstKStaticV(b *testing.B) { - err := clues.New("err") + err := cluerr.New("err") for i := 0; i < b.N; i++ { err = err.With("foo", i). With("baz", -i) @@ -137,7 +156,7 @@ func BenchmarkWith_chainConstKStaticV(b *testing.B) { } func BenchmarkWith_chainStaticKConstV(b *testing.B) { - err := clues.New("err") + err := cluerr.New("err") for i := 0; i < b.N; i++ { err = err.With(i, "bar"). With(-i, "qux") @@ -145,7 +164,7 @@ func BenchmarkWith_chainStaticKConstV(b *testing.B) { } func BenchmarkWith_chainConstKRandV(b *testing.B) { - err := clues.New("err") + err := cluerr.New("err") for i := 0; i < b.N; i++ { err = err.With("foo", benchVals[i%benchSize]). With("baz", -benchVals[i%benchSize]) @@ -153,7 +172,7 @@ func BenchmarkWith_chainConstKRandV(b *testing.B) { } func BenchmarkWith_chainRandKConstV(b *testing.B) { - err := clues.New("err") + err := cluerr.New("err") for i := 0; i < b.N; i++ { err = err.With(benchVals[i%benchSize], "bar"). With(-benchVals[i%benchSize], "qux") @@ -161,7 +180,7 @@ func BenchmarkWith_chainRandKConstV(b *testing.B) { } func BenchmarkWith_chainRandKRandV(b *testing.B) { - err := clues.New("err") + err := cluerr.New("err") for i := 0; i < b.N; i++ { err = err.With(benchVals[i%benchSize], benchVals[i%benchSize]). With(-benchVals[i%benchSize], -benchVals[i%benchSize]) @@ -169,7 +188,7 @@ func BenchmarkWith_chainRandKRandV(b *testing.B) { } func BenchmarkWithMap_constKConstV(b *testing.B) { - err := clues.New("err") + err := cluerr.New("err") for i := 0; i < b.N; i++ { m := map[string]any{"foo": "bar", "baz": "qux"} err = err.WithMap(m) @@ -177,7 +196,7 @@ func BenchmarkWithMap_constKConstV(b *testing.B) { } func BenchmarkWithMap_staticKStaticV(b *testing.B) { - err := clues.New("err") + err := cluerr.New("err") for i := 0; i < b.N; i++ { m := map[string]any{ strconv.Itoa(benchSize - i): i, @@ -188,7 +207,7 @@ func BenchmarkWithMap_staticKStaticV(b *testing.B) { } func BenchmarkWithMap_constKStaticV(b *testing.B) { - err := clues.New("err") + err := cluerr.New("err") for i := 0; i < b.N; i++ { m := map[string]any{"foo": i, "baz": -i} err = err.WithMap(m) @@ -196,7 +215,7 @@ func BenchmarkWithMap_constKStaticV(b *testing.B) { } func BenchmarkWithMap_staticKConstV(b *testing.B) { - err := clues.New("err") + err := cluerr.New("err") for i := 0; i < b.N; i++ { m := map[string]any{ strconv.Itoa(i): "bar", @@ -207,7 +226,7 @@ func BenchmarkWithMap_staticKConstV(b *testing.B) { } func BenchmarkWithMap_constKRandV(b *testing.B) { - err := clues.New("err") + err := cluerr.New("err") for i := 0; i < b.N; i++ { m := map[string]any{ "foo": benchVals[i%benchSize], @@ -218,7 +237,7 @@ func BenchmarkWithMap_constKRandV(b *testing.B) { } func BenchmarkWithMap_randKConstV(b *testing.B) { - err := clues.New("err") + err := cluerr.New("err") for i := 0; i < b.N; i++ { m := map[string]any{ strconv.FormatInt(benchVals[i%benchSize], 10): "bar", @@ -229,7 +248,7 @@ func BenchmarkWithMap_randKConstV(b *testing.B) { } func BenchmarkWithMap_randKRandV(b *testing.B) { - err := clues.New("err") + err := cluerr.New("err") for i := 0; i < b.N; i++ { m := map[string]any{ strconv.FormatInt(benchVals[i%benchSize], 10): benchVals[i%benchSize], @@ -240,7 +259,7 @@ func BenchmarkWithMap_randKRandV(b *testing.B) { } func BenchmarkWithClues_constKConstV(b *testing.B) { - err := clues.New("err") + err := cluerr.New("err") ctx := context.Background() for i := 0; i < b.N; i++ { ctx = clues.Add(ctx, "foo", "bar") @@ -249,7 +268,7 @@ func BenchmarkWithClues_constKConstV(b *testing.B) { } func BenchmarkWithClues_staticKStaticV(b *testing.B) { - err := clues.New("err") + err := cluerr.New("err") ctx := context.Background() for i := 0; i < b.N; i++ { ctx = clues.Add(ctx, benchSize-i, i) @@ -258,7 +277,7 @@ func BenchmarkWithClues_staticKStaticV(b *testing.B) { } func BenchmarkWithClues_constKStaticV(b *testing.B) { - err := clues.New("err") + err := cluerr.New("err") ctx := context.Background() for i := 0; i < b.N; i++ { ctx = clues.Add(ctx, "foo", i) @@ -267,7 +286,7 @@ func BenchmarkWithClues_constKStaticV(b *testing.B) { } func BenchmarkWithClues_staticKConstV(b *testing.B) { - err := clues.New("err") + err := cluerr.New("err") ctx := context.Background() for i := 0; i < b.N; i++ { ctx = clues.Add(ctx, i, "bar") @@ -276,7 +295,7 @@ func BenchmarkWithClues_staticKConstV(b *testing.B) { } func BenchmarkWithClues_constKRandV(b *testing.B) { - err := clues.New("err") + err := cluerr.New("err") ctx := context.Background() for i := 0; i < b.N; i++ { ctx = clues.Add(ctx, "foo", benchVals[i%benchSize]) @@ -285,7 +304,7 @@ func BenchmarkWithClues_constKRandV(b *testing.B) { } func BenchmarkWithClues_randKConstV(b *testing.B) { - err := clues.New("err") + err := cluerr.New("err") ctx := context.Background() for i := 0; i < b.N; i++ { ctx = clues.Add(ctx, benchVals[i%benchSize], "bar") @@ -294,7 +313,7 @@ func BenchmarkWithClues_randKConstV(b *testing.B) { } func BenchmarkWithClues_randKRandV(b *testing.B) { - err := clues.New("err") + err := cluerr.New("err") ctx := context.Background() for i := 0; i < b.N; i++ { ctx = clues.Add(ctx, benchVals[i%benchSize], benchVals[i%benchSize]) @@ -303,31 +322,31 @@ func BenchmarkWithClues_randKRandV(b *testing.B) { } func BenchmarkInErr_const(b *testing.B) { - err := clues.New("err") + err := cluerr.New("err") var m map[string]any for i := 0; i < b.N; i++ { err = err.With("foo", "bar") - m = clues.InErr(err).Map() + m = cluerr.CluesIn(err).Map() } _ = m } func BenchmarkInErr_static(b *testing.B) { - err := clues.New("err") + err := cluerr.New("err") var m map[string]any for i := 0; i < b.N; i++ { err = err.With(i, -i) - m = clues.InErr(err).Map() + m = cluerr.CluesIn(err).Map() } _ = m } func BenchmarkInErr_rand(b *testing.B) { - err := clues.New("err") + err := cluerr.New("err") var m map[string]any for i := 0; i < b.N; i++ { err = err.With(benchVals[i%benchSize], benchVals[i%benchSize]) - m = clues.InErr(err).Map() + m = cluerr.CluesIn(err).Map() } _ = m } diff --git a/err_fmt_test.go b/cluerr/err_fmt_test.go similarity index 77% rename from err_fmt_test.go rename to cluerr/err_fmt_test.go index d9ad8d9..c1a1a9c 100644 --- a/err_fmt_test.go +++ b/cluerr/err_fmt_test.go @@ -1,16 +1,15 @@ -package clues_test +package cluerr_test import ( "context" stderr "errors" "fmt" "regexp" - "strings" "testing" "github.com/pkg/errors" - "github.com/alcionai/clues" + "github.com/alcionai/clues/cluerr" ) // if this panics, you added an uneven number of @@ -38,12 +37,6 @@ type checkFmt struct { reExpect *regexp.Regexp } -func prettyStack(s string) string { - s = strings.ReplaceAll(s, "\n", string('\n')) - s = strings.ReplaceAll(s, "\t", " ") - return s -} - func (c checkFmt) check(t *testing.T, err error) { t.Run(c.tmpl, func(t *testing.T) { result := fmt.Sprintf(c.tmpl, err) @@ -85,17 +78,17 @@ func makeOnion(base error, mid, top func(error) error) error { func self(err error) error { return err } var ( - errStd = stderr.New("an error") - errErrs = errors.New("an error") - fmtErrf = fmt.Errorf("an error") - cluErr = clues.New("an error") + errStd = stderr.New("an error") + errErrs = errors.New("an error") + errFmtErrf = fmt.Errorf("an error") + cluErr = cluerr.New("an error") - cluesWrap = func(err error) error { return clues.Wrap(err, "clues wrap") } - cluesPlainStack = func(err error) error { return clues.Stack(err) } - cluesStack = func(err error) error { return clues.Stack(globalSentinel, err) } + cluesWrap = func(err error) error { return cluerr.Wrap(err, "clues wrap") } + cluesPlainStack = func(err error) error { return cluerr.Stack(err) } + cluesStack = func(err error) error { return cluerr.Stack(globalSentinel, err) } ) -const cluesInitFileLoc = `(:\d+|.+/clues/.+go:?\d+)\n` +const cluesInitFileLoc = `(:\d+|.+/cluerr/.+go:?\d+)\n` func TestFmt(t *testing.T) { type expect struct { @@ -127,11 +120,11 @@ func TestFmt(t *testing.T) { plus: plusRE( `an error\n`, "", `errors wrap\n`, "", - `github.com/alcionai/clues_test.TestFmt.func1\n`, `err_fmt_test.go:\d+\n`, - `github.com/alcionai/clues_test.makeOnion.func2\n`, `err_fmt_test.go:\d+\n`, - `github.com/alcionai/clues_test.makeOnion.func3\n`, `err_fmt_test.go:\d+\n`, - `github.com/alcionai/clues_test.makeOnion\n`, `err_fmt_test.go:\d+\n`, - `github.com/alcionai/clues_test.TestFmt\n`, `err_fmt_test.go:\d+\n`, + `github.com/alcionai/clues/cluerr_test.TestFmt.func1\n`, `err_fmt_test.go:\d+\n`, + `github.com/alcionai/clues/cluerr_test.makeOnion.func2\n`, `err_fmt_test.go:\d+\n`, + `github.com/alcionai/clues/cluerr_test.makeOnion.func3\n`, `err_fmt_test.go:\d+\n`, + `github.com/alcionai/clues/cluerr_test.makeOnion\n`, `err_fmt_test.go:\d+\n`, + `github.com/alcionai/clues/cluerr_test.TestFmt\n`, `err_fmt_test.go:\d+\n`, `testing.tRunner\n`, `testing.go:\d+\n`, `runtime.goexit\n`, `runtime/.*:\d+$`, ), @@ -149,17 +142,17 @@ func TestFmt(t *testing.T) { q: `"errors wrap: an error"`, plus: plusRE( `an error\n`, "", - `github.com/alcionai/clues_test.init\n`, cluesInitFileLoc, + `github.com/alcionai/clues/cluerr_test.init\n`, cluesInitFileLoc, `runtime.doInit1?\n`, `proc.go:\d+\n`, `runtime.doInit\n`, `proc.go:\d+\n`, `runtime.main\n`, `proc.go:\d+\n`, `runtime.goexit\n`, `runtime/.*:\d+\n`, `errors wrap\n`, "", - `github.com/alcionai/clues_test.TestFmt.func2\n`, `err_fmt_test.go:\d+\n`, - `github.com/alcionai/clues_test.makeOnion.func2\n`, `err_fmt_test.go:\d+\n`, - `github.com/alcionai/clues_test.makeOnion.func3\n`, `err_fmt_test.go:\d+\n`, - `github.com/alcionai/clues_test.makeOnion\n`, `err_fmt_test.go:\d+\n`, - `github.com/alcionai/clues_test.TestFmt\n`, `err_fmt_test.go:\d+\n`, + `github.com/alcionai/clues/cluerr_test.TestFmt.func2\n`, `err_fmt_test.go:\d+\n`, + `github.com/alcionai/clues/cluerr_test.makeOnion.func2\n`, `err_fmt_test.go:\d+\n`, + `github.com/alcionai/clues/cluerr_test.makeOnion.func3\n`, `err_fmt_test.go:\d+\n`, + `github.com/alcionai/clues/cluerr_test.makeOnion\n`, `err_fmt_test.go:\d+\n`, + `github.com/alcionai/clues/cluerr_test.TestFmt\n`, `err_fmt_test.go:\d+\n`, `testing.tRunner\n`, `testing.go:\d+\n`, `runtime.goexit\n`, `runtime/.*:\d+$`, ), @@ -167,7 +160,7 @@ func TestFmt(t *testing.T) { }, { name: "litmus wrap fmt.Errorf", - onion: makeOnion(fmtErrf, + onion: makeOnion(errFmtErrf, func(err error) error { return errors.Wrap(err, "errors wrap") }, self), expect: expect{ @@ -178,18 +171,18 @@ func TestFmt(t *testing.T) { plus: plusRE( `an error\n`, "", `errors wrap\n`, "", - `github.com/alcionai/clues_test.TestFmt.func3\n`, `err_fmt_test.go:\d+\n`, - `github.com/alcionai/clues_test.makeOnion.func2\n`, `err_fmt_test.go:\d+\n`, - `github.com/alcionai/clues_test.makeOnion.func3\n`, `err_fmt_test.go:\d+\n`, - `github.com/alcionai/clues_test.makeOnion\n`, `err_fmt_test.go:\d+\n`, - `github.com/alcionai/clues_test.TestFmt\n`, `err_fmt_test.go:\d+\n`, + `github.com/alcionai/clues/cluerr_test.TestFmt.func3\n`, `err_fmt_test.go:\d+\n`, + `github.com/alcionai/clues/cluerr_test.makeOnion.func2\n`, `err_fmt_test.go:\d+\n`, + `github.com/alcionai/clues/cluerr_test.makeOnion.func3\n`, `err_fmt_test.go:\d+\n`, + `github.com/alcionai/clues/cluerr_test.makeOnion\n`, `err_fmt_test.go:\d+\n`, + `github.com/alcionai/clues/cluerr_test.TestFmt\n`, `err_fmt_test.go:\d+\n`, `testing.tRunner\n`, `testing.go:\d+\n`, `runtime.goexit\n`, `runtime/.*:\d+$`, ), }, }, { - name: "litmus wrap clues.New", + name: "litmus wrap cluerr.New", onion: makeOnion(cluErr, func(err error) error { return errors.Wrap(err, "errors wrap") }, self), @@ -201,11 +194,11 @@ func TestFmt(t *testing.T) { plus: plusRE( `an error\n`, `err_fmt_test.go:\d+\n`, `errors wrap\n`, "", - `github.com/alcionai/clues_test.TestFmt.func4\n`, `err_fmt_test.go:\d+\n`, - `github.com/alcionai/clues_test.makeOnion.func2\n`, `err_fmt_test.go:\d+\n`, - `github.com/alcionai/clues_test.makeOnion.func3\n`, `err_fmt_test.go:\d+\n`, - `github.com/alcionai/clues_test.makeOnion\n`, `err_fmt_test.go:\d+\n`, - `github.com/alcionai/clues_test.TestFmt\n`, `err_fmt_test.go:\d+\n`, + `github.com/alcionai/clues/cluerr_test.TestFmt.func4\n`, `err_fmt_test.go:\d+\n`, + `github.com/alcionai/clues/cluerr_test.makeOnion.func2\n`, `err_fmt_test.go:\d+\n`, + `github.com/alcionai/clues/cluerr_test.makeOnion.func3\n`, `err_fmt_test.go:\d+\n`, + `github.com/alcionai/clues/cluerr_test.makeOnion\n`, `err_fmt_test.go:\d+\n`, + `github.com/alcionai/clues/cluerr_test.TestFmt\n`, `err_fmt_test.go:\d+\n`, `testing.tRunner\n`, `testing.go:\d+\n`, `runtime.goexit\n`, `runtime/.*:\d+$`, ), @@ -237,7 +230,7 @@ func TestFmt(t *testing.T) { q: `"an error"`, plus: plusRE( `an error\n`, "", - `github.com/alcionai/clues_test.init\n`, cluesInitFileLoc, + `github.com/alcionai/clues/cluerr_test.init\n`, cluesInitFileLoc, `runtime.doInit1?\n`, `proc.go:\d+\n`, `runtime.doInit\n`, `proc.go:\d+\n`, `runtime.main\n`, `proc.go:\d+\n`, @@ -247,7 +240,7 @@ func TestFmt(t *testing.T) { }, { name: "fmt.Errorf", - onion: makeOnion(fmtErrf, self, self), + onion: makeOnion(errFmtErrf, self, self), expect: expect{ v: "an error", hash: `&errors.errorString{s:"an error"}`, @@ -259,7 +252,7 @@ func TestFmt(t *testing.T) { }, }, { - name: "clues.New", + name: "cluerr.New", onion: makeOnion(cluErr, self, self), expect: expect{ v: "an error", @@ -275,7 +268,7 @@ func TestFmt(t *testing.T) { // wrapped error // --------------------------------------------------------------------------- { - name: "clues.Wrap stderr.New", + name: "cluerr.Wrap stderr.New", onion: makeOnion(errStd, cluesWrap, self), expect: expect{ v: "clues wrap: an error", @@ -289,7 +282,7 @@ func TestFmt(t *testing.T) { }, }, { - name: "clues.Wrap errors.New", + name: "cluerr.Wrap errors.New", onion: makeOnion(errErrs, cluesWrap, self), expect: expect{ v: "clues wrap: an error", @@ -298,7 +291,7 @@ func TestFmt(t *testing.T) { q: `"clues wrap": "an error"`, plus: plusRE( `an error\n`, "", - `github.com/alcionai/clues_test.init\n`, cluesInitFileLoc, + `github.com/alcionai/clues/cluerr_test.init\n`, cluesInitFileLoc, `runtime.doInit1?\n`, `proc.go:\d+\n`, `runtime.doInit\n`, `proc.go:\d+\n`, `runtime.main\n`, `proc.go:\d+\n`, @@ -309,7 +302,7 @@ func TestFmt(t *testing.T) { }, { name: "clues.Wrap fmt.Errorf", - onion: makeOnion(fmtErrf, cluesWrap, self), + onion: makeOnion(errFmtErrf, cluesWrap, self), expect: expect{ v: "clues wrap: an error", hash: "clues wrap: an error", @@ -322,7 +315,7 @@ func TestFmt(t *testing.T) { }, }, { - name: "clues.Wrap clues.New", + name: "cluerr.Wrap cluerr.New", onion: makeOnion(cluErr, cluesWrap, self), expect: expect{ v: "clues wrap: an error", @@ -361,7 +354,7 @@ func TestFmt(t *testing.T) { q: `"an error"`, plus: plusRE( `an error\n`, "", - `github.com/alcionai/clues_test.init\n`, cluesInitFileLoc, + `github.com/alcionai/clues/cluerr_test.init\n`, cluesInitFileLoc, `runtime.doInit1?\n`, `proc.go:\d+\n`, `runtime.doInit\n`, `proc.go:\d+\n`, `runtime.main\n`, `proc.go:\d+\n`, @@ -372,7 +365,7 @@ func TestFmt(t *testing.T) { }, { name: "clues.PlainStack fmt.Errorf", - onion: makeOnion(fmtErrf, cluesPlainStack, self), + onion: makeOnion(errFmtErrf, cluesPlainStack, self), expect: expect{ v: "an error", hash: "an error", @@ -384,7 +377,7 @@ func TestFmt(t *testing.T) { }, }, { - name: "clues.PlainStack clues.New", + name: "clues.PlainStack cluerr.New", onion: makeOnion(cluErr, cluesPlainStack, self), expect: expect{ v: "an error", @@ -401,7 +394,7 @@ func TestFmt(t *testing.T) { // stacked sentinel // --------------------------------------------------------------------------- { - name: "clues.Stack stderr.New", + name: "cluerr.Stack stderr.New", onion: makeOnion(errStd, cluesStack, self), expect: expect{ v: "sentinel: an error", @@ -411,7 +404,7 @@ func TestFmt(t *testing.T) { plus: plusRE( `an error\n`, "", `sentinel\n`, "", - `github.com/alcionai/clues_test.init\n`, cluesInitFileLoc, + `github.com/alcionai/clues/cluerr_test.init\n`, cluesInitFileLoc, `runtime.doInit1?\n`, `proc.go:\d+\n`, `runtime.doInit\n`, `proc.go:\d+\n`, `runtime.main\n`, `proc.go:\d+\n`, @@ -421,7 +414,7 @@ func TestFmt(t *testing.T) { }, }, { - name: "clues.Stack errors.New", + name: "cluerr.Stack errors.New", onion: makeOnion(errErrs, cluesStack, self), expect: expect{ v: "sentinel: an error", @@ -430,13 +423,13 @@ func TestFmt(t *testing.T) { q: `"sentinel": "an error"`, plus: plusRE( `an error\n`, "", - `github.com/alcionai/clues_test.init\n`, cluesInitFileLoc, + `github.com/alcionai/clues/cluerr_test.init\n`, cluesInitFileLoc, `runtime.doInit1?\n`, `proc.go:\d+\n`, `runtime.doInit\n`, `proc.go:\d+\n`, `runtime.main\n`, `proc.go:\d+\n`, `runtime.goexit\n`, `runtime/.*:\d+\n`, `sentinel\n`, "", - `github.com/alcionai/clues_test.init\n`, cluesInitFileLoc, + `github.com/alcionai/clues/cluerr_test.init\n`, cluesInitFileLoc, `runtime.doInit1?\n`, `proc.go:\d+\n`, `runtime.doInit\n`, `proc.go:\d+\n`, `runtime.main\n`, `proc.go:\d+\n`, @@ -447,7 +440,7 @@ func TestFmt(t *testing.T) { }, { name: "clues.Stack fmt.Errorf", - onion: makeOnion(fmtErrf, cluesStack, self), + onion: makeOnion(errFmtErrf, cluesStack, self), expect: expect{ v: "sentinel: an error", hash: "sentinel: an error", @@ -456,7 +449,7 @@ func TestFmt(t *testing.T) { plus: plusRE( `an error\n`, "", `sentinel\n`, "", - `github.com/alcionai/clues_test.init\n`, cluesInitFileLoc, + `github.com/alcionai/clues/cluerr_test.init\n`, cluesInitFileLoc, `runtime.doInit1?\n`, `proc.go:\d+\n`, `runtime.doInit\n`, `proc.go:\d+\n`, `runtime.main\n`, `proc.go:\d+\n`, @@ -466,7 +459,7 @@ func TestFmt(t *testing.T) { }, }, { - name: "clues.Stack clues.New", + name: "cluerr.Stack cluerr.New", onion: makeOnion(cluErr, cluesStack, self), expect: expect{ v: "sentinel: an error", @@ -476,7 +469,7 @@ func TestFmt(t *testing.T) { plus: plusRE( `an error\n`, `err_fmt_test.go:\d+\n`, `sentinel\n`, "", - `github.com/alcionai/clues_test.init\n`, cluesInitFileLoc, + `github.com/alcionai/clues/cluerr_test.init\n`, cluesInitFileLoc, `runtime.doInit1?\n`, `proc.go:\d+\n`, `runtime.doInit\n`, `proc.go:\d+\n`, `runtime.main\n`, `proc.go:\d+\n`, @@ -489,7 +482,7 @@ func TestFmt(t *testing.T) { // wrapped, stacked errors // --------------------------------------------------------------------------- { - name: "clues.Wrap clues.Stack stderr.New", + name: "cluerr.Wrap cluerr.Stack stderr.New", onion: makeOnion(errStd, cluesStack, cluesWrap), expect: expect{ v: "clues wrap: sentinel: an error", @@ -499,7 +492,7 @@ func TestFmt(t *testing.T) { plus: plusRE( `an error\n`, "", `sentinel\n`, "", - `github.com/alcionai/clues_test.init\n`, cluesInitFileLoc, + `github.com/alcionai/clues/cluerr_test.init\n`, cluesInitFileLoc, `runtime.doInit1?\n`, `proc.go:\d+\n`, `runtime.doInit\n`, `proc.go:\d+\n`, `runtime.main\n`, `proc.go:\d+\n`, @@ -510,7 +503,7 @@ func TestFmt(t *testing.T) { }, }, { - name: "clues.Wrap clues.Stack errors.New", + name: "cluerr.Wrap cluerr.Stack errors.New", onion: makeOnion(errErrs, cluesStack, cluesWrap), expect: expect{ v: "clues wrap: sentinel: an error", @@ -519,13 +512,13 @@ func TestFmt(t *testing.T) { q: `"clues wrap": "sentinel": "an error"`, plus: plusRE( `an error\n`, "", - `github.com/alcionai/clues_test.init\n`, cluesInitFileLoc, + `github.com/alcionai/clues/cluerr_test.init\n`, cluesInitFileLoc, `runtime.doInit1?\n`, `proc.go:\d+\n`, `runtime.doInit\n`, `proc.go:\d+\n`, `runtime.main\n`, `proc.go:\d+\n`, `runtime.goexit\n`, `runtime/.*:\d+\n`, `sentinel\n`, "", - `github.com/alcionai/clues_test.init\n`, cluesInitFileLoc, + `github.com/alcionai/clues/cluerr_test.init\n`, cluesInitFileLoc, `runtime.doInit1?\n`, `proc.go:\d+\n`, `runtime.doInit\n`, `proc.go:\d+\n`, `runtime.main\n`, `proc.go:\d+\n`, @@ -537,7 +530,7 @@ func TestFmt(t *testing.T) { }, { name: "clues.Wrap clues.Stack fmt.Errorf", - onion: makeOnion(fmtErrf, cluesStack, cluesWrap), + onion: makeOnion(errFmtErrf, cluesStack, cluesWrap), expect: expect{ v: "clues wrap: sentinel: an error", hash: "clues wrap: sentinel: an error", @@ -546,7 +539,7 @@ func TestFmt(t *testing.T) { plus: plusRE( `an error\n`, "", `sentinel\n`, "", - `github.com/alcionai/clues_test.init\n`, cluesInitFileLoc, + `github.com/alcionai/clues/cluerr_test.init\n`, cluesInitFileLoc, `runtime.doInit1?\n`, `proc.go:\d+\n`, `runtime.doInit\n`, `proc.go:\d+\n`, `runtime.main\n`, `proc.go:\d+\n`, @@ -557,7 +550,7 @@ func TestFmt(t *testing.T) { }, }, { - name: "clues.Wrap clues.Stack clues.New", + name: "cluerr.Wrap cluerr.Stack cluerr.New", onion: makeOnion(cluErr, cluesStack, cluesWrap), expect: expect{ v: "clues wrap: sentinel: an error", @@ -567,7 +560,7 @@ func TestFmt(t *testing.T) { plus: plusRE( `an error\n`, `err_fmt_test.go:\d+\n`, `sentinel\n`, "", - `github.com/alcionai/clues_test.init\n`, cluesInitFileLoc, + `github.com/alcionai/clues/cluerr_test.init\n`, cluesInitFileLoc, `runtime.doInit1?\n`, `proc.go:\d+\n`, `runtime.doInit\n`, `proc.go:\d+\n`, `runtime.main\n`, `proc.go:\d+\n`, @@ -581,7 +574,7 @@ func TestFmt(t *testing.T) { // stacked, wrapped errors // --------------------------------------------------------------------------- { - name: "clues.Stack clues.Wrap stderr.New", + name: "cluerr.Stack cluerr.Wrap stderr.New", onion: makeOnion(errStd, cluesWrap, cluesStack), expect: expect{ v: "sentinel: clues wrap: an error", @@ -592,7 +585,7 @@ func TestFmt(t *testing.T) { `an error\n`, "", `clues wrap\n`, `err_fmt_test.go:\d+\n`, `sentinel\n`, "", - `github.com/alcionai/clues_test.init\n`, cluesInitFileLoc, + `github.com/alcionai/clues/cluerr_test.init\n`, cluesInitFileLoc, `runtime.doInit1?\n`, `proc.go:\d+\n`, `runtime.doInit\n`, `proc.go:\d+\n`, `runtime.main\n`, `proc.go:\d+\n`, @@ -602,7 +595,7 @@ func TestFmt(t *testing.T) { }, }, { - name: "clues.Stack clues.Wrap errors.New", + name: "cluerr.Stack cluerr.Wrap errors.New", onion: makeOnion(errErrs, cluesWrap, cluesStack), expect: expect{ v: "sentinel: clues wrap: an error", @@ -611,14 +604,14 @@ func TestFmt(t *testing.T) { q: `"sentinel": "clues wrap": "an error"`, plus: plusRE( `an error\n`, "", - `github.com/alcionai/clues_test.init\n`, cluesInitFileLoc, + `github.com/alcionai/clues/cluerr_test.init\n`, cluesInitFileLoc, `runtime.doInit1?\n`, `proc.go:\d+\n`, `runtime.doInit\n`, `proc.go:\d+\n`, `runtime.main\n`, `proc.go:\d+\n`, `runtime.goexit\n`, `runtime/.*:\d+\n`, `clues wrap\n`, `err_fmt_test.go:\d+\n`, `sentinel\n`, "", - `github.com/alcionai/clues_test.init\n`, cluesInitFileLoc, + `github.com/alcionai/clues/cluerr_test.init\n`, cluesInitFileLoc, `runtime.doInit1?\n`, `proc.go:\d+\n`, `runtime.doInit\n`, `proc.go:\d+\n`, `runtime.main\n`, `proc.go:\d+\n`, @@ -629,7 +622,7 @@ func TestFmt(t *testing.T) { }, { name: "clues.Stack clues.Wrap fmt.Errorf", - onion: makeOnion(fmtErrf, cluesWrap, cluesStack), + onion: makeOnion(errFmtErrf, cluesWrap, cluesStack), expect: expect{ v: "sentinel: clues wrap: an error", hash: "sentinel: clues wrap: an error", @@ -639,7 +632,7 @@ func TestFmt(t *testing.T) { `an error\n`, "", `clues wrap\n`, `err_fmt_test.go:\d+\n`, `sentinel\n`, "", - `github.com/alcionai/clues_test.init\n`, cluesInitFileLoc, + `github.com/alcionai/clues/cluerr_test.init\n`, cluesInitFileLoc, `runtime.doInit1?\n`, `proc.go:\d+\n`, `runtime.doInit\n`, `proc.go:\d+\n`, `runtime.main\n`, `proc.go:\d+\n`, @@ -649,7 +642,7 @@ func TestFmt(t *testing.T) { }, }, { - name: "clues.Stack clues.Wrap clues.New", + name: "cluerr.Stack cluerr.Wrap cluerr.New", onion: makeOnion(cluErr, cluesWrap, cluesStack), expect: expect{ v: "sentinel: clues wrap: an error", @@ -660,7 +653,7 @@ func TestFmt(t *testing.T) { `an error\n`, `err_fmt_test.go:\d+\n`, `clues wrap\n`, `err_fmt_test.go:\d+\n`, `sentinel\n`, "", - `github.com/alcionai/clues_test.init\n`, cluesInitFileLoc, + `github.com/alcionai/clues/cluerr_test.init\n`, cluesInitFileLoc, `runtime.doInit1?\n`, `proc.go:\d+\n`, `runtime.doInit\n`, `proc.go:\d+\n`, `runtime.main\n`, `proc.go:\d+\n`, @@ -674,12 +667,12 @@ func TestFmt(t *testing.T) { // --------------------------------------------------------------------------- { name: "multi-stack stderr.New", - onion: clues.Stack( - clues.Stack( + onion: cluerr.Stack( + cluerr.Stack( stderr.New("top"), stderr.New("mid"), ), - clues.Stack( + cluerr.Stack( globalSentinel, stderr.New("bot"), ), @@ -692,7 +685,7 @@ func TestFmt(t *testing.T) { plus: plusRE( `bot\n`, ``, `sentinel\n`, "", - `github.com/alcionai/clues_test.init\n`, cluesInitFileLoc, + `github.com/alcionai/clues/cluerr_test.init\n`, cluesInitFileLoc, `runtime.doInit1?\n`, `proc.go:\d+\n`, `runtime.doInit\n`, `proc.go:\d+\n`, `runtime.main\n`, `proc.go:\d+\n`, @@ -706,12 +699,12 @@ func TestFmt(t *testing.T) { }, { name: "multi-stack errors.New", - onion: clues.Stack( - clues.Stack( + onion: cluerr.Stack( + cluerr.Stack( errors.New("top"), errors.New("mid"), ), - clues.Stack( + cluerr.Stack( globalSentinel, errors.New("bot"), ), @@ -723,22 +716,22 @@ func TestFmt(t *testing.T) { q: `"top": "mid": "sentinel": "bot"`, plus: plusRE( `bot\n`, ``, - `github.com/alcionai/clues_test.TestFmt\n`, `err_fmt_test.go:\d+\n`, + `github.com/alcionai/clues/cluerr_test.TestFmt\n`, `err_fmt_test.go:\d+\n`, `testing.tRunner\n`, `testing.go:\d+\n`, `runtime.goexit\n`, `runtime/.*:\d+\n`, `sentinel\n`, "", - `github.com/alcionai/clues_test.init\n`, cluesInitFileLoc, + `github.com/alcionai/clues/cluerr_test.init\n`, cluesInitFileLoc, `runtime.doInit1?\n`, `proc.go:\d+\n`, `runtime.doInit\n`, `proc.go:\d+\n`, `runtime.main\n`, `proc.go:\d+\n`, `runtime.goexit\n`, `runtime/.*:\d+\n`, "", `err_fmt_test.go:\d+\n`, `mid\n`, ``, - `github.com/alcionai/clues_test.TestFmt\n`, `err_fmt_test.go:\d+\n`, + `github.com/alcionai/clues/cluerr_test.TestFmt\n`, `err_fmt_test.go:\d+\n`, `testing.tRunner\n`, `testing.go:\d+\n`, `runtime.goexit\n`, `runtime/.*:\d+\n`, `top\n`, ``, - `github.com/alcionai/clues_test.TestFmt\n`, `err_fmt_test.go:\d+\n`, + `github.com/alcionai/clues/cluerr_test.TestFmt\n`, `err_fmt_test.go:\d+\n`, `testing.tRunner\n`, `testing.go:\d+\n`, `runtime.goexit\n`, `runtime/.*:\d+\n`, "", `err_fmt_test.go:\d+\n`, @@ -748,12 +741,12 @@ func TestFmt(t *testing.T) { }, { name: "multi-stack fmt.Errorf", - onion: clues.Stack( - clues.Stack( + onion: cluerr.Stack( + cluerr.Stack( fmt.Errorf("top"), fmt.Errorf("mid"), ), - clues.Stack( + cluerr.Stack( globalSentinel, fmt.Errorf("bot"), ), @@ -766,7 +759,7 @@ func TestFmt(t *testing.T) { plus: plusRE( `bot\n`, ``, `sentinel\n`, "", - `github.com/alcionai/clues_test.init\n`, cluesInitFileLoc, + `github.com/alcionai/clues/cluerr_test.init\n`, cluesInitFileLoc, `runtime.doInit1?\n`, `proc.go:\d+\n`, `runtime.doInit\n`, `proc.go:\d+\n`, `runtime.main\n`, `proc.go:\d+\n`, @@ -779,15 +772,15 @@ func TestFmt(t *testing.T) { }, }, { - name: "multi-stack clues.New", - onion: clues.Stack( - clues.Stack( - clues.New("top"), - clues.New("mid"), + name: "multi-stack cluerr.New", + onion: cluerr.Stack( + cluerr.Stack( + cluerr.New("top"), + cluerr.New("mid"), ), - clues.Stack( + cluerr.Stack( globalSentinel, - clues.New("bot"), + cluerr.New("bot"), ), ), expect: expect{ @@ -798,7 +791,7 @@ func TestFmt(t *testing.T) { plus: plusRE( `bot\n`, `err_fmt_test.go:\d+\n`, `sentinel\n`, "", - `github.com/alcionai/clues_test.init\n`, cluesInitFileLoc, + `github.com/alcionai/clues/cluerr_test.init\n`, cluesInitFileLoc, `runtime.doInit1?\n`, `proc.go:\d+\n`, `runtime.doInit\n`, `proc.go:\d+\n`, `runtime.main\n`, `proc.go:\d+\n`, @@ -816,12 +809,12 @@ func TestFmt(t *testing.T) { // --------------------------------------------------------------------------- { name: "wrapped multi-stack stderr.New", - onion: clues.Stack( - clues.Wrap(clues.Stack( + onion: cluerr.Stack( + cluerr.Wrap(cluerr.Stack( stderr.New("top"), stderr.New("mid"), ), "lhs"), - clues.Wrap(clues.Stack( + cluerr.Wrap(cluerr.Stack( globalSentinel, stderr.New("bot"), ), "rhs"), @@ -834,7 +827,7 @@ func TestFmt(t *testing.T) { plus: plusRE( `bot\n`, ``, `sentinel\n`, "", - `github.com/alcionai/clues_test.init\n`, cluesInitFileLoc, + `github.com/alcionai/clues/cluerr_test.init\n`, cluesInitFileLoc, `runtime.doInit1?\n`, `proc.go:\d+\n`, `runtime.doInit\n`, `proc.go:\d+\n`, `runtime.main\n`, `proc.go:\d+\n`, @@ -850,12 +843,12 @@ func TestFmt(t *testing.T) { }, { name: "wrapped multi-stack errors.New", - onion: clues.Stack( - clues.Wrap(clues.Stack( + onion: cluerr.Stack( + cluerr.Wrap(cluerr.Stack( errors.New("top"), errors.New("mid"), ), "lhs"), - clues.Wrap(clues.Stack( + cluerr.Wrap(cluerr.Stack( globalSentinel, errors.New("bot"), ), "rhs"), @@ -867,11 +860,11 @@ func TestFmt(t *testing.T) { q: `"lhs": "top": "mid": "rhs": "sentinel": "bot"`, plus: plusRE( `bot\n`, ``, - `github.com/alcionai/clues_test.TestFmt\n`, `err_fmt_test.go:\d+\n`, + `github.com/alcionai/clues/cluerr_test.TestFmt\n`, `err_fmt_test.go:\d+\n`, `testing.tRunner\n`, `testing.go:\d+\n`, `runtime.goexit\n`, `runtime/.*:\d+\n`, `sentinel\n`, "", - `github.com/alcionai/clues_test.init\n`, cluesInitFileLoc, + `github.com/alcionai/clues/cluerr_test.init\n`, cluesInitFileLoc, `runtime.doInit1?\n`, `proc.go:\d+\n`, `runtime.doInit\n`, `proc.go:\d+\n`, `runtime.main\n`, `proc.go:\d+\n`, @@ -879,11 +872,11 @@ func TestFmt(t *testing.T) { "", `err_fmt_test.go:\d+\n`, `rhs\n`, `err_fmt_test.go:\d+\n`, `mid\n`, ``, - `github.com/alcionai/clues_test.TestFmt\n`, `err_fmt_test.go:\d+\n`, + `github.com/alcionai/clues/cluerr_test.TestFmt\n`, `err_fmt_test.go:\d+\n`, `testing.tRunner\n`, `testing.go:\d+\n`, `runtime.goexit\n`, `runtime/.*:\d+\n`, `top\n`, ``, - `github.com/alcionai/clues_test.TestFmt\n`, `err_fmt_test.go:\d+\n`, + `github.com/alcionai/clues/cluerr_test.TestFmt\n`, `err_fmt_test.go:\d+\n`, `testing.tRunner\n`, `testing.go:\d+\n`, `runtime.goexit\n`, `runtime/.*:\d+\n`, "", `err_fmt_test.go:\d+\n`, @@ -894,12 +887,12 @@ func TestFmt(t *testing.T) { }, { name: "wrapped multi-stack fmt.Errorf", - onion: clues.Stack( - clues.Wrap(clues.Stack( + onion: cluerr.Stack( + cluerr.Wrap(cluerr.Stack( fmt.Errorf("top"), fmt.Errorf("mid"), ), "lhs"), - clues.Wrap(clues.Stack( + cluerr.Wrap(cluerr.Stack( globalSentinel, fmt.Errorf("bot"), ), "rhs"), @@ -912,7 +905,7 @@ func TestFmt(t *testing.T) { plus: plusRE( `bot\n`, ``, `sentinel\n`, "", - `github.com/alcionai/clues_test.init\n`, cluesInitFileLoc, + `github.com/alcionai/clues/cluerr_test.init\n`, cluesInitFileLoc, `runtime.doInit1?\n`, `proc.go:\d+\n`, `runtime.doInit\n`, `proc.go:\d+\n`, `runtime.main\n`, `proc.go:\d+\n`, @@ -927,15 +920,15 @@ func TestFmt(t *testing.T) { }, }, { - name: "wrapped multi-stack clues.New", - onion: clues.Stack( - clues.Wrap(clues.Stack( - clues.New("top"), - clues.New("mid"), + name: "wrapped multi-stack cluerr.New", + onion: cluerr.Stack( + cluerr.Wrap(cluerr.Stack( + cluerr.New("top"), + cluerr.New("mid"), ), "lhs"), - clues.Wrap(clues.Stack( + cluerr.Wrap(cluerr.Stack( globalSentinel, - clues.New("bot"), + cluerr.New("bot"), ), "rhs"), ), expect: expect{ @@ -946,7 +939,7 @@ func TestFmt(t *testing.T) { plus: plusRE( `bot\n`, `err_fmt_test.go:\d+\n`, `sentinel\n`, "", - `github.com/alcionai/clues_test.init\n`, cluesInitFileLoc, + `github.com/alcionai/clues/cluerr_test.init\n`, cluesInitFileLoc, `runtime.doInit1?\n`, `proc.go:\d+\n`, `runtime.doInit\n`, `proc.go:\d+\n`, `runtime.main\n`, `proc.go:\d+\n`, @@ -982,43 +975,43 @@ func TestFmt(t *testing.T) { // wrapped func bottomWrap(err error) error { - return clues.Wrap(err, "bottom wrap") + return cluerr.Wrap(err, "bottom wrap") } func midWrap(err error) error { - return clues.Wrap(bottomWrap(err), "mid wrap") + return cluerr.Wrap(bottomWrap(err), "mid wrap") } func topWrap(err error) error { - return clues.Wrap(midWrap(err), "top wrap") + return cluerr.Wrap(midWrap(err), "top wrap") } // plain-stacked func bottomPlainStack(err error) error { - return clues.Stack(err) + return cluerr.Stack(err) } func midPlainStack(err error) error { - return clues.Stack(bottomPlainStack(err)) + return cluerr.Stack(bottomPlainStack(err)) } func topPlainStack(err error) error { - return clues.Stack(midPlainStack(err)) + return cluerr.Stack(midPlainStack(err)) } // stacked func bottomStack(err error) error { - return clues.Stack(clues.New("bottom"), err) + return cluerr.Stack(cluerr.New("bottom"), err) } func midStack(err error) error { - return clues.Stack(clues.New("mid"), bottomStack(err)) + return cluerr.Stack(cluerr.New("mid"), bottomStack(err)) } func topStack(err error) error { - return clues.Stack(clues.New("top"), midStack(err)) + return cluerr.Stack(cluerr.New("top"), midStack(err)) } func TestFmt_nestedFuncs(t *testing.T) { @@ -1040,7 +1033,7 @@ func TestFmt_nestedFuncs(t *testing.T) { // wrapped error // --------------------------------------------------------------------------- { - name: "clues.Wrap stderr.New", + name: "cluerr.Wrap stderr.New", fn: topWrap, source: errStd, expect: expect{ @@ -1057,7 +1050,7 @@ func TestFmt_nestedFuncs(t *testing.T) { }, }, { - name: "clues.Wrap errors.New", + name: "cluerr.Wrap errors.New", fn: topWrap, source: errErrs, expect: expect{ @@ -1067,7 +1060,7 @@ func TestFmt_nestedFuncs(t *testing.T) { q: `"top wrap": "mid wrap": "bottom wrap": "an error"`, plus: plusRE( `an error\n`, "", - `github.com/alcionai/clues_test.init\n`, cluesInitFileLoc, + `github.com/alcionai/clues/cluerr_test.init\n`, cluesInitFileLoc, `runtime.doInit1?\n`, `proc.go:\d+\n`, `runtime.doInit\n`, `proc.go:\d+\n`, `runtime.main\n`, `proc.go:\d+\n`, @@ -1079,9 +1072,9 @@ func TestFmt_nestedFuncs(t *testing.T) { }, }, { - name: "clues.Wrap fmt.Errorf", + name: "cluerr.Wrap fmt.Errorf", fn: topWrap, - source: fmtErrf, + source: errFmtErrf, expect: expect{ v: "top wrap: mid wrap: bottom wrap: an error", hash: "top wrap: mid wrap: bottom wrap: an error", @@ -1096,7 +1089,7 @@ func TestFmt_nestedFuncs(t *testing.T) { }, }, { - name: "clues.Wrap clues.New", + name: "cluerr.Wrap cluerr.New", fn: topWrap, source: cluErr, expect: expect{ @@ -1143,7 +1136,7 @@ func TestFmt_nestedFuncs(t *testing.T) { q: `"an error"`, plus: plusRE( `an error\n`, "", - `github.com/alcionai/clues_test.init\n`, cluesInitFileLoc, + `github.com/alcionai/clues/cluerr_test.init\n`, cluesInitFileLoc, `runtime.doInit1?\n`, `proc.go:\d+\n`, `runtime.doInit\n`, `proc.go:\d+\n`, `runtime.main\n`, `proc.go:\d+\n`, @@ -1157,7 +1150,7 @@ func TestFmt_nestedFuncs(t *testing.T) { { name: "clues.PlainStack fmt.Errorf", fn: topPlainStack, - source: fmtErrf, + source: errFmtErrf, expect: expect{ v: "an error", hash: "an error", @@ -1172,7 +1165,7 @@ func TestFmt_nestedFuncs(t *testing.T) { }, }, { - name: "clues.PlainStack clues.New", + name: "clues.PlainStack cluerr.New", fn: topPlainStack, source: cluErr, expect: expect{ @@ -1193,7 +1186,7 @@ func TestFmt_nestedFuncs(t *testing.T) { // stacked tree // --------------------------------------------------------------------------- { - name: "clues.Stack stderr.New", + name: "cluerr.Stack stderr.New", fn: topStack, source: errStd, expect: expect{ @@ -1213,7 +1206,7 @@ func TestFmt_nestedFuncs(t *testing.T) { }, }, { - name: "clues.Stack errors.New", + name: "cluerr.Stack errors.New", fn: topStack, source: errErrs, expect: expect{ @@ -1223,7 +1216,7 @@ func TestFmt_nestedFuncs(t *testing.T) { q: `"top": "mid": "bottom": "an error"`, plus: plusRE( `an error\n`, "", - `github.com/alcionai/clues_test.init\n`, cluesInitFileLoc, + `github.com/alcionai/clues/cluerr_test.init\n`, cluesInitFileLoc, `runtime.doInit1?\n`, `proc.go:\d+\n`, `runtime.doInit\n`, `proc.go:\d+\n`, `runtime.main\n`, `proc.go:\d+\n`, @@ -1238,9 +1231,9 @@ func TestFmt_nestedFuncs(t *testing.T) { }, }, { - name: "clues.Stack fmt.Errorf", + name: "cluerr.Stack fmt.Errorf", fn: topStack, - source: fmtErrf, + source: errFmtErrf, expect: expect{ v: "top: mid: bottom: an error", hash: "top: mid: bottom: an error", @@ -1258,7 +1251,7 @@ func TestFmt_nestedFuncs(t *testing.T) { }, }, { - name: "clues.Stack clues.New", + name: "cluerr.Stack cluerr.New", fn: topStack, source: cluErr, expect: expect{ @@ -1396,32 +1389,32 @@ func TestSkipCaller(t *testing.T) { }) t.Run("wrapWC", func(t *testing.T) { checkFmt{"%+v", "", regexp.MustCompile(test.expectWrapped)}. - check(t, clues.WrapWC(context.Background(), test.tracer(cluErr), "wrap")) + check(t, cluerr.WrapWC(context.Background(), test.tracer(cluErr), "wrap")) }) t.Run("wrap", func(t *testing.T) { checkFmt{"%+v", "", regexp.MustCompile(test.expectWrapped)}. - check(t, clues.Wrap(test.tracer(cluErr), "wrap")) + check(t, cluerr.Wrap(test.tracer(cluErr), "wrap")) }) t.Run("stackWC", func(t *testing.T) { checkFmt{"%+v", "", regexp.MustCompile(test.expectStacked)}. - check(t, clues.StackWC(context.Background(), cluErr, test.tracer(cluErr))) + check(t, cluerr.StackWC(context.Background(), cluErr, test.tracer(cluErr))) }) t.Run("stack", func(t *testing.T) { checkFmt{"%+v", "", regexp.MustCompile(test.expectStacked)}. - check(t, clues.Stack(cluErr, test.tracer(cluErr))) + check(t, cluerr.Stack(cluErr, test.tracer(cluErr))) }) }) } table2 := []struct { name string - tracer func(err *clues.Err) error + tracer func(err *cluerr.Err) error expect string expectWrapped string expectStacked string }{ { - name: "clues.Err 1", - tracer: func(err *clues.Err) error { + name: "cluerr.Err 1", + tracer: func(err *cluerr.Err) error { return cluesWithSkipCaller(err, -1) }, expect: plusRE(`an error\n`, `err_test.go:\d+$`), @@ -1434,8 +1427,8 @@ func TestSkipCaller(t *testing.T) { ``, `err_fmt_test.go:\d+$`), }, { - name: "clues.Err 0", - tracer: func(err *clues.Err) error { + name: "cluerr.Err 0", + tracer: func(err *cluerr.Err) error { return cluesWithSkipCaller(err, 0) }, expect: plusRE(`an error\n`, `err_test.go:\d+$`), @@ -1448,8 +1441,8 @@ func TestSkipCaller(t *testing.T) { ``, `err_fmt_test.go:\d+$`), }, { - name: "clues.Err 1", - tracer: func(err *clues.Err) error { + name: "cluerr.Err 1", + tracer: func(err *cluerr.Err) error { return cluesWithSkipCaller(err, 1) }, expect: plusRE(`an error\n`, `err_fmt_test.go:\d+$`), @@ -1462,8 +1455,8 @@ func TestSkipCaller(t *testing.T) { ``, `err_fmt_test.go:\d+$`), }, { - name: "clues.Err 2", - tracer: func(err *clues.Err) error { + name: "cluerr.Err 2", + tracer: func(err *cluerr.Err) error { return cluesWithSkipCaller(err, 2) }, expect: plusRE(`an error\n`, `err_fmt_test.go:\d+$`), @@ -1476,8 +1469,8 @@ func TestSkipCaller(t *testing.T) { ``, `err_fmt_test.go:\d+$`), }, { - name: "clues.Err 3", - tracer: func(err *clues.Err) error { + name: "cluerr.Err 3", + tracer: func(err *cluerr.Err) error { return cluesWithSkipCaller(err, 3) }, expect: plusRE(`an error\n`, `testing/testing.go:\d+$`), @@ -1490,8 +1483,8 @@ func TestSkipCaller(t *testing.T) { ``, `err_fmt_test.go:\d+$`), }, { - name: "clues.Err 4", - tracer: func(err *clues.Err) error { + name: "cluerr.Err 4", + tracer: func(err *cluerr.Err) error { return cluesWithSkipCaller(err, 4) }, expect: plusRE(`an error\n`, `runtime/.*:\d+$`), @@ -1512,32 +1505,32 @@ func TestSkipCaller(t *testing.T) { }) t.Run("wrapWC", func(t *testing.T) { checkFmt{"%+v", "", regexp.MustCompile(test.expectWrapped)}. - check(t, clues.WrapWC(context.Background(), test.tracer(cluErr), "wrap")) + check(t, cluerr.WrapWC(context.Background(), test.tracer(cluErr), "wrap")) }) t.Run("wrap", func(t *testing.T) { checkFmt{"%+v", "", regexp.MustCompile(test.expectWrapped)}. - check(t, clues.Wrap(test.tracer(cluErr), "wrap")) + check(t, cluerr.Wrap(test.tracer(cluErr), "wrap")) }) t.Run("stackWC", func(t *testing.T) { checkFmt{"%+v", "", regexp.MustCompile(test.expectStacked)}. - check(t, clues.StackWC(context.Background(), cluErr, test.tracer(cluErr))) + check(t, cluerr.StackWC(context.Background(), cluErr, test.tracer(cluErr))) }) t.Run("stack", func(t *testing.T) { checkFmt{"%+v", "", regexp.MustCompile(test.expectStacked)}. - check(t, clues.Stack(cluErr, test.tracer(cluErr))) + check(t, cluerr.Stack(cluErr, test.tracer(cluErr))) }) }) } table3 := []struct { name string - tracer func(err *clues.Err) error + tracer func(err *cluerr.Err) error expect string expectWrapped string expectStacked string }{ { - name: "clues.Err wrapped with generic", - tracer: func(err *clues.Err) error { + name: "cluerr.Err wrapped with generic", + tracer: func(err *cluerr.Err) error { return wrapWithFuncWithGeneric(err) }, expect: plusRE( @@ -1553,8 +1546,8 @@ func TestSkipCaller(t *testing.T) { `an error\n`, `err_fmt_test.go:\d+$`), }, { - name: "clues.Err wrapped with no trace", - tracer: func(err *clues.Err) error { + name: "cluerr.Err wrapped with no trace", + tracer: func(err *cluerr.Err) error { return withNoTrace(err) }, expect: plusRE( @@ -1579,19 +1572,19 @@ func TestSkipCaller(t *testing.T) { }) t.Run("wrapWC", func(t *testing.T) { checkFmt{"%+v", "", regexp.MustCompile(test.expectWrapped)}. - check(t, clues.WrapWC(context.Background(), test.tracer(err), "wrap")) + check(t, cluerr.WrapWC(context.Background(), test.tracer(err), "wrap")) }) t.Run("wrap", func(t *testing.T) { checkFmt{"%+v", "", regexp.MustCompile(test.expectWrapped)}. - check(t, clues.Wrap(test.tracer(err), "wrap")) + check(t, cluerr.Wrap(test.tracer(err), "wrap")) }) t.Run("stackWC", func(t *testing.T) { checkFmt{"%+v", "", regexp.MustCompile(test.expectStacked)}. - check(t, clues.StackWC(context.Background(), err, test.tracer(err))) + check(t, cluerr.StackWC(context.Background(), err, test.tracer(err))) }) t.Run("stack", func(t *testing.T) { checkFmt{"%+v", "", regexp.MustCompile(test.expectStacked)}. - check(t, clues.Stack(err, test.tracer(err))) + check(t, cluerr.Stack(err, test.tracer(err))) }) }) } @@ -1611,14 +1604,14 @@ func TestComment(t *testing.T) { return withCommentWrapper(err, "fisher") }, expect: commentRE( - `withCommentWrapper`, `clues/err_test.go`, `fisher`, - `withCommentWrapper`, `clues/err_test.go`, `fisher - repeat$`), + `withCommentWrapper`, `cluerr/err_test.go`, `fisher`, + `withCommentWrapper`, `cluerr/err_test.go`, `fisher - repeat$`), expectWrapped: commentRE( - `withCommentWrapper`, `clues/err_test.go`, `fisher`, - `withCommentWrapper`, `clues/err_test.go`, `fisher - repeat$`), + `withCommentWrapper`, `cluerr/err_test.go`, `fisher`, + `withCommentWrapper`, `cluerr/err_test.go`, `fisher - repeat$`), expectStacked: commentRE( - `withCommentWrapper`, `clues/err_test.go`, `fisher`, - `withCommentWrapper`, `clues/err_test.go`, `fisher - repeat$`), + `withCommentWrapper`, `cluerr/err_test.go`, `fisher`, + `withCommentWrapper`, `cluerr/err_test.go`, `fisher - repeat$`), }, { name: "error formatted comment", @@ -1626,106 +1619,106 @@ func TestComment(t *testing.T) { return withCommentWrapper(err, "%d", 42) }, expect: commentRE( - `withCommentWrapper`, `clues/err_test.go`, `42`, - `withCommentWrapper`, `clues/err_test.go`, `42 - repeat$`), + `withCommentWrapper`, `cluerr/err_test.go`, `42`, + `withCommentWrapper`, `cluerr/err_test.go`, `42 - repeat$`), expectWrapped: commentRE( - `withCommentWrapper`, `clues/err_test.go`, `42`, - `withCommentWrapper`, `clues/err_test.go`, `42 - repeat$`), + `withCommentWrapper`, `cluerr/err_test.go`, `42`, + `withCommentWrapper`, `cluerr/err_test.go`, `42 - repeat$`), expectStacked: commentRE( - `withCommentWrapper`, `clues/err_test.go`, `42`, - `withCommentWrapper`, `clues/err_test.go`, `42 - repeat$`), + `withCommentWrapper`, `cluerr/err_test.go`, `42`, + `withCommentWrapper`, `cluerr/err_test.go`, `42 - repeat$`), }, } for _, test := range table { t.Run(test.name, func(t *testing.T) { t.Run("plain error", func(t *testing.T) { - comments := clues.Comments(test.commenter(cluErr)).String() + comments := cluerr.Comments(test.commenter(cluErr)).String() commentMatches(t, test.expect, comments) }) t.Run("wrapWC", func(t *testing.T) { - err := clues.WrapWC(context.Background(), test.commenter(cluErr), "wrap") - comments := clues.Comments(err).String() + err := cluerr.WrapWC(context.Background(), test.commenter(cluErr), "wrap") + comments := cluerr.Comments(err).String() commentMatches(t, test.expectWrapped, comments) }) t.Run("wrap", func(t *testing.T) { - err := clues.Wrap(test.commenter(cluErr), "wrap") - comments := clues.Comments(err).String() + err := cluerr.Wrap(test.commenter(cluErr), "wrap") + comments := cluerr.Comments(err).String() commentMatches(t, test.expectWrapped, comments) }) t.Run("stackWC", func(t *testing.T) { - err := clues.StackWC(context.Background(), cluErr, test.commenter(cluErr)) - comments := clues.Comments(err).String() + err := cluerr.StackWC(context.Background(), cluErr, test.commenter(cluErr)) + comments := cluerr.Comments(err).String() commentMatches(t, test.expectStacked, comments) }) t.Run("stack", func(t *testing.T) { - err := clues.Stack(cluErr, test.commenter(cluErr)) - comments := clues.Comments(err).String() + err := cluerr.Stack(cluErr, test.commenter(cluErr)) + comments := cluerr.Comments(err).String() commentMatches(t, test.expectStacked, comments) }) }) } table2 := []struct { name string - commenter func(err *clues.Err) error + commenter func(err *cluerr.Err) error expect string expectWrapped string expectStacked string }{ { - name: "clues.Err flat comment", - commenter: func(err *clues.Err) error { - return cluesWithCommentWrapper(err, "fisher") + name: "cluerr.Err flat comment", + commenter: func(err *cluerr.Err) error { + return cluerrWithCommentWrapper(err, "fisher") }, expect: commentRE( - `cluesWithCommentWrapper`, `clues/err_test.go`, `fisher`, - `cluesWithCommentWrapper`, `clues/err_test.go`, `fisher - repeat$`), + `cluerrWithCommentWrapper`, `cluerr/err_test.go`, `fisher`, + `cluerrWithCommentWrapper`, `cluerr/err_test.go`, `fisher - repeat$`), expectWrapped: commentRE( - `cluesWithCommentWrapper`, `clues/err_test.go`, `fisher`, - `cluesWithCommentWrapper`, `clues/err_test.go`, `fisher - repeat$`), + `cluerrWithCommentWrapper`, `cluerr/err_test.go`, `fisher`, + `cluerrWithCommentWrapper`, `cluerr/err_test.go`, `fisher - repeat$`), expectStacked: commentRE( - `cluesWithCommentWrapper`, `clues/err_test.go`, `fisher`, - `cluesWithCommentWrapper`, `clues/err_test.go`, `fisher - repeat$`), + `cluerrWithCommentWrapper`, `cluerr/err_test.go`, `fisher`, + `cluerrWithCommentWrapper`, `cluerr/err_test.go`, `fisher - repeat$`), }, { - name: "clues.Err formatted comment", - commenter: func(err *clues.Err) error { - return cluesWithCommentWrapper(err, "%d", 42) + name: "cluerr.Err formatted comment", + commenter: func(err *cluerr.Err) error { + return cluerrWithCommentWrapper(err, "%d", 42) }, expect: commentRE( - `cluesWithCommentWrapper`, `clues/err_test.go`, `42`, - `cluesWithCommentWrapper`, `clues/err_test.go`, `42 - repeat$`), + `cluerrWithCommentWrapper`, `cluerr/err_test.go`, `42`, + `cluerrWithCommentWrapper`, `cluerr/err_test.go`, `42 - repeat$`), expectWrapped: commentRE( - `cluesWithCommentWrapper`, `clues/err_test.go`, `42`, - `cluesWithCommentWrapper`, `clues/err_test.go`, `42 - repeat$`), + `cluerrWithCommentWrapper`, `cluerr/err_test.go`, `42`, + `cluerrWithCommentWrapper`, `cluerr/err_test.go`, `42 - repeat$`), expectStacked: commentRE( - `cluesWithCommentWrapper`, `clues/err_test.go`, `42`, - `cluesWithCommentWrapper`, `clues/err_test.go`, `42 - repeat$`), + `cluerrWithCommentWrapper`, `cluerr/err_test.go`, `42`, + `cluerrWithCommentWrapper`, `cluerr/err_test.go`, `42 - repeat$`), }, } for _, test := range table2 { t.Run(test.name, func(t *testing.T) { t.Run("plain error", func(t *testing.T) { - comments := clues.Comments(test.commenter(cluErr)).String() + comments := cluerr.Comments(test.commenter(cluErr)).String() commentMatches(t, test.expect, comments) }) t.Run("wrapWC", func(t *testing.T) { - err := clues.WrapWC(context.Background(), test.commenter(cluErr), "wrap") - comments := clues.Comments(err).String() + err := cluerr.WrapWC(context.Background(), test.commenter(cluErr), "wrap") + comments := cluerr.Comments(err).String() commentMatches(t, test.expectWrapped, comments) }) t.Run("wrap", func(t *testing.T) { - err := clues.Wrap(test.commenter(cluErr), "wrap") - comments := clues.Comments(err).String() + err := cluerr.Wrap(test.commenter(cluErr), "wrap") + comments := cluerr.Comments(err).String() commentMatches(t, test.expectWrapped, comments) }) t.Run("stackWC", func(t *testing.T) { - err := clues.StackWC(context.Background(), cluErr, test.commenter(cluErr)) - comments := clues.Comments(err).String() + err := cluerr.StackWC(context.Background(), cluErr, test.commenter(cluErr)) + comments := cluerr.Comments(err).String() commentMatches(t, test.expectStacked, comments) }) t.Run("stack", func(t *testing.T) { - err := clues.Stack(cluErr, test.commenter(cluErr)) - comments := clues.Comments(err).String() + err := cluerr.Stack(cluErr, test.commenter(cluErr)) + comments := cluerr.Comments(err).String() commentMatches(t, test.expectStacked, comments) }) }) @@ -1735,7 +1728,7 @@ func TestComment(t *testing.T) { func TestErrCore_String(t *testing.T) { table := []struct { name string - core *clues.ErrCore + core *cluerr.ErrCore expectS string expectVPlus string }{ @@ -1747,7 +1740,7 @@ func TestErrCore_String(t *testing.T) { }, { name: "all values", - core: clues. + core: cluerr. New("message"). With("key", "value"). Label("label"). @@ -1757,7 +1750,7 @@ func TestErrCore_String(t *testing.T) { }, { name: "message only", - core: clues. + core: cluerr. New("message"). Core(), expectS: `{"message"}`, @@ -1765,7 +1758,7 @@ func TestErrCore_String(t *testing.T) { }, { name: "labels only", - core: clues. + core: cluerr. New(""). Label("label"). Core(), @@ -1774,7 +1767,7 @@ func TestErrCore_String(t *testing.T) { }, { name: "values only", - core: clues. + core: cluerr. New(""). With("key", "value"). Core(), @@ -1821,3 +1814,40 @@ func TestErrCore_String(t *testing.T) { }) } } + +// --------------------------------------------------------------------------- +// helpers +// --------------------------------------------------------------------------- + +// requires sets of 3 strings +func commentRE(ss ...string) string { + result := "" + + for i := 0; i < len(ss); i += 3 { + result += ss[i] + " - " + result += ss[i+1] + `:\d+\n` + result += `\t` + ss[i+2] + + if len(ss) > i+3 { + result += `\n` + } + } + + return result +} + +func commentMatches( + t *testing.T, + expect, result string, +) { + re := regexp.MustCompile(expect) + + if !re.MatchString(result) { + t.Errorf( + "unexpected comments stack"+ + "\n\nexpected (raw)\n\"%s\""+ + "\n\ngot (raw)\n%#v"+ + "\n\ngot (fmt)\n\"%s\"", + re, result, result) + } +} diff --git a/err_test.go b/cluerr/err_test.go similarity index 67% rename from err_test.go rename to cluerr/err_test.go index a1e9238..eb1c04d 100644 --- a/err_test.go +++ b/cluerr/err_test.go @@ -1,4 +1,4 @@ -package clues_test +package cluerr_test import ( "context" @@ -9,8 +9,21 @@ import ( "golang.org/x/exp/maps" "github.com/alcionai/clues" + "github.com/alcionai/clues/cluerr" + "github.com/alcionai/clues/internal/tester" ) +type msa map[string]any + +func toMSA[T any](m map[string]T) msa { + to := make(msa, len(m)) + for k, v := range m { + to[k] = v + } + + return to +} + type testingError struct{} func (e testingError) Error() string { @@ -30,35 +43,35 @@ func TestStack(t *testing.T) { { name: "SingleNil", getErr: func() error { - return clues.Stack(nil).OrNil() + return cluerr.Stack(nil).OrNil() }, expectNil: true, }, { name: "DoubleNil", getErr: func() error { - return clues.Stack(nil, nil).OrNil() + return cluerr.Stack(nil, nil).OrNil() }, expectNil: true, }, { name: "TripleNil", getErr: func() error { - return clues.Stack(nil, nil, nil).OrNil() + return cluerr.Stack(nil, nil, nil).OrNil() }, expectNil: true, }, { name: "StackNilNil", getErr: func() error { - return clues.Stack(clues.Stack(nil), nil).OrNil() + return cluerr.Stack(cluerr.Stack(nil), nil).OrNil() }, expectNil: true, }, { name: "NilStackNilNil", getErr: func() error { - return clues.Stack(nil, clues.Stack(nil), nil).OrNil() + return cluerr.Stack(nil, cluerr.Stack(nil), nil).OrNil() }, expectNil: true, }, @@ -67,7 +80,7 @@ func TestStack(t *testing.T) { getErr: func() error { var e testingErrorIface - return clues.Stack(nil, e, clues.Stack(nil)).OrNil() + return cluerr.Stack(nil, e, cluerr.Stack(nil)).OrNil() }, expectNil: true, }, @@ -76,7 +89,7 @@ func TestStack(t *testing.T) { getErr: func() error { var e testingErrorIface = testingError{} - return clues.Stack(nil, e, clues.Stack(nil)).OrNil() + return cluerr.Stack(nil, e, cluerr.Stack(nil)).OrNil() }, expectNil: false, }, @@ -85,14 +98,14 @@ func TestStack(t *testing.T) { getErr: func() error { var e testingErrorIface = &testingError{} - return clues.Stack(nil, e, clues.Stack(nil)).OrNil() + return cluerr.Stack(nil, e, cluerr.Stack(nil)).OrNil() }, expectNil: false, }, { name: "NonPointerError", getErr: func() error { - return clues.Stack(nil, testingError{}, clues.Stack(nil)).OrNil() + return cluerr.Stack(nil, testingError{}, cluerr.Stack(nil)).OrNil() }, expectNil: false, }, @@ -120,29 +133,29 @@ func TestHasLabel(t *testing.T) { }{ { name: "multiple stacked clues errors with label on first", - initial: clues.Stack( - clues.New("Labeled").Label(label), - clues.New("NotLabeled")), + initial: cluerr.Stack( + cluerr.New("Labeled").Label(label), + cluerr.New("NotLabeled")), }, { name: "multiple stacked clues errors with label on second", - initial: clues.Stack( - clues.New("NotLabeled"), - clues.New("Labeled").Label(label)), + initial: cluerr.Stack( + cluerr.New("NotLabeled"), + cluerr.New("Labeled").Label(label)), }, { name: "single stacked clues error with label", - initial: clues.Stack(clues.New("Labeled").Label(label)), + initial: cluerr.Stack(cluerr.New("Labeled").Label(label)), }, } for _, test := range table { t.Run(test.name, func(t *testing.T) { - if !clues.HasLabel(test.initial, label) { + if !cluerr.HasLabel(test.initial, label) { t.Errorf( "expected error to have label [%s] but got %v", label, - maps.Keys(clues.Labels(test.initial))) + maps.Keys(cluerr.Labels(test.initial))) } }) } @@ -152,16 +165,16 @@ func TestLabel(t *testing.T) { table := []struct { name string initial error - expect func(*testing.T, *clues.Err) + expect func(*testing.T, *cluerr.Err) }{ {"nil", nil, nil}, {"standard error", errors.New("an error"), nil}, - {"clues error", clues.New("clues error"), nil}, - {"clues error wrapped", fmt.Errorf("%w", clues.New("clues error")), nil}, + {"clues error", cluerr.New("clues error"), nil}, + {"clues error wrapped", fmt.Errorf("%w", cluerr.New("clues error")), nil}, { "clues error with label", - clues.New("clues error").Label("fnords"), - func(t *testing.T, err *clues.Err) { + cluerr.New("clues error").Label("fnords"), + func(t *testing.T, err *cluerr.Err) { if !err.HasLabel("fnords") { t.Error("expected error to have label [fnords]") } @@ -169,8 +182,8 @@ func TestLabel(t *testing.T) { }, { "clues error with label wrapped", - fmt.Errorf("%w", clues.New("clues error").Label("fnords")), - func(t *testing.T, err *clues.Err) { + fmt.Errorf("%w", cluerr.New("clues error").Label("fnords")), + func(t *testing.T, err *cluerr.Err) { if !err.HasLabel("fnords") { t.Error("expected error to have label [fnords]") } @@ -179,12 +192,12 @@ func TestLabel(t *testing.T) { } for _, test := range table { t.Run(test.name, func(t *testing.T) { - if clues.HasLabel(test.initial, "foo") { + if cluerr.HasLabel(test.initial, "foo") { t.Error("new error should have no label") } - err := clues.Label(test.initial, "foo") - if !clues.HasLabel(err, "foo") && test.initial != nil { + err := cluerr.Label(test.initial, "foo") + if !cluerr.HasLabel(err, "foo") && test.initial != nil { t.Error("expected error to have label [foo]") } @@ -196,7 +209,7 @@ func TestLabel(t *testing.T) { } err.Label("bar") - if !clues.HasLabel(err, "bar") { + if !cluerr.HasLabel(err, "bar") { t.Error("expected error to have label [bar]") } @@ -211,11 +224,11 @@ func TestLabels(t *testing.T) { var ( ma = msa{"a": struct{}{}} mab = msa{"a": struct{}{}, "b": struct{}{}} - a = clues.New("a").Label("a") - acopy = clues.New("acopy").Label("a") - b = clues.New("b").Label("b") - wrap = clues.Wrap( - clues.Stack( + a = cluerr.New("a").Label("a") + acopy = cluerr.New("acopy").Label("a") + b = cluerr.New("b").Label("b") + wrap = cluerr.Wrap( + cluerr.Stack( fmt.Errorf("%w", a), fmt.Errorf("%w", b), fmt.Errorf("%w", acopy), @@ -229,30 +242,30 @@ func TestLabels(t *testing.T) { }{ {"nil", nil, msa{}}, {"standard error", errors.New("an error"), msa{}}, - {"unlabeled error", clues.New("clues error"), msa{}}, + {"unlabeled error", cluerr.New("clues error"), msa{}}, {"pkg/errs wrap around labeled error", errors.Wrap(a, "wa"), ma}, - {"clues wrapped", clues.Wrap(a, "wrap"), ma}, - {"clues stacked", clues.Stack(a, b), mab}, - {"clues stacked with copy", clues.Stack(a, b, acopy), mab}, - {"error chain", clues.Stack(b, fmt.Errorf("%w", a), fmt.Errorf("%w", acopy)), mab}, + {"clues wrapped", cluerr.Wrap(a, "wrap"), ma}, + {"clues stacked", cluerr.Stack(a, b), mab}, + {"clues stacked with copy", cluerr.Stack(a, b, acopy), mab}, + {"error chain", cluerr.Stack(b, fmt.Errorf("%w", a), fmt.Errorf("%w", acopy)), mab}, {"error wrap chain", wrap, mab}, } for _, test := range table { t.Run(test.name, func(t *testing.T) { - result := clues.Labels(test.initial) - mustEquals(t, test.expect, toMSA(result), false) + result := cluerr.Labels(test.initial) + tester.MustEquals(t, test.expect, toMSA(result), false) }) } } var ( base = errors.New("an error") - cerr = func() error { return clues.Stack(base) } + cerr = func() error { return cluerr.Stack(base) } werr = func() error { - return fmt.Errorf("%w", clues.Wrap(base, "wrapped error with vals").With("z", 0)) + return fmt.Errorf("%w", cluerr.Wrap(base, "wrapped error with vals").With("z", 0)) } serr = func() error { - return clues.Stack(clues.New("primary").With("z", 0), errors.New("secondary")) + return cluerr.Stack(cluerr.New("primary").With("z", 0), errors.New("secondary")) } ) @@ -288,12 +301,12 @@ func TestWith(t *testing.T) { } for _, test := range table { t.Run(test.name, func(t *testing.T) { - err := clues.With(test.initial, test.k, test.v) + err := cluerr.Stack(test.initial).With(test.k, test.v) for _, kv := range test.with { err = err.With(kv...) } - mustEquals(t, test.expect, clues.InErr(err).Map(), false) - mustEquals(t, test.expect, err.Values().Map(), false) + tester.MustEquals(t, test.expect, cluerr.CluesIn(err).Map(), false) + tester.MustEquals(t, test.expect, err.Values().Map(), false) }) } } @@ -330,10 +343,10 @@ func TestWithMap(t *testing.T) { } for _, test := range table { t.Run(test.name, func(t *testing.T) { - err := clues.WithMap(test.initial, test.kv) + err := cluerr.Stack(test.initial).WithMap(test.kv) err = err.WithMap(test.with) - mustEquals(t, test.expect, clues.InErr(err).Map(), false) - mustEquals(t, test.expect, err.Values().Map(), false) + tester.MustEquals(t, test.expect, cluerr.CluesIn(err).Map(), false) + tester.MustEquals(t, test.expect, err.Values().Map(), false) }) } } @@ -373,10 +386,10 @@ func TestWithClues(t *testing.T) { for _, test := range table { t.Run(test.name, func(t *testing.T) { tctx := clues.AddMap(ctx, test.kv) - err := clues.WithClues(test.initial, tctx) + err := cluerr.Stack(test.initial).WithClues(tctx) err = err.WithMap(test.with) - mustEquals(t, test.expect, clues.InErr(err).Map(), false) - mustEquals(t, test.expect, err.Values().Map(), false) + tester.MustEquals(t, test.expect, cluerr.CluesIn(err).Map(), false) + tester.MustEquals(t, test.expect, err.Values().Map(), false) }) } } @@ -394,9 +407,9 @@ func TestValuePriority(t *testing.T) { // the last addition to a ctx should take priority ctx = clues.Add(ctx, "in-ctx", 2) - err := clues.NewWC(ctx, "err").With("in-err", 1) + err := cluerr.NewWC(ctx, "err").With("in-err", 1) // the first addition to an error should take priority - err = clues.StackWC(ctx, err).With("in-err", 2) + err = cluerr.StackWC(ctx, err).With("in-err", 2) return err }(), @@ -406,9 +419,9 @@ func TestValuePriority(t *testing.T) { name: "last stack wins", err: func() error { ctx := clues.Add(context.Background(), "in-ctx", 1) - err := clues.NewWC(ctx, "last in stack").With("in-err", 1) - err = clues.Stack( - clues.New("first in stack").With("in-err", 2), + err := cluerr.NewWC(ctx, "last in stack").With("in-err", 1) + err = cluerr.Stack( + cluerr.New("first in stack").With("in-err", 2), err) return err }(), @@ -418,7 +431,7 @@ func TestValuePriority(t *testing.T) { name: ".With wins over ctx", err: func() error { ctx := clues.Add(context.Background(), "k", 1) - err := clues.NewWC(ctx, "last in stack").With("k", 2) + err := cluerr.NewWC(ctx, "last in stack").With("k", 2) return err }(), expect: msa{"k": 2}, @@ -426,49 +439,35 @@ func TestValuePriority(t *testing.T) { } for _, test := range table { t.Run(test.name, func(t *testing.T) { - mustEquals(t, test.expect, clues.InErr(test.err).Map(), false) + tester.MustEquals(t, test.expect, cluerr.CluesIn(test.err).Map(), false) }) } } func TestUnwrap(t *testing.T) { e := errors.New("cause") - we := clues.Wrap(e, "outer") + we := cluerr.Wrap(e, "outer") ce := we.Unwrap() if ce != e { t.Errorf("expected result error [%v] to be base error [%v]\n", ce, e) } - ce = clues.Unwrap(we) - if ce != e { - t.Errorf("expected result error [%v] to be base error [%v]\n", ce, e) - } - - se := clues.Stack(e) + se := cluerr.Stack(e) ce = se.Unwrap() if ce != e { t.Errorf("expected result error [%v] to be base error [%v]\n", ce, e) } - - ce = clues.Unwrap(se) - if ce != e { - t.Errorf("expected result error [%v] to be base error [%v]\n", ce, e) - } - - if clues.Unwrap(nil) != nil { - t.Errorf("expected nil unwrap input to return nil") - } } func TestWrapNilStackSlice(t *testing.T) { // an empty slice of errors sl := make([]error, 10) // when stacked - st := clues.Stack(sl...) + st := cluerr.Stack(sl...) // then wrapped - e := clues.Wrap(st, "wrapped") + e := cluerr.Wrap(st, "wrapped") // should contain nil if e.OrNil() != nil { t.Errorf("e.OrNil() <%+v> should be nil", e.OrNil()) @@ -485,65 +484,65 @@ func TestErr_Error(t *testing.T) { }{ { name: "new error", - err: clues.New("new"), + err: cluerr.New("new"), expect: "new", }, { name: "stacked error", - err: clues.Stack(sentinel), + err: cluerr.Stack(sentinel), expect: sentinel.Error(), }, { name: "wrapped new error", - err: clues.Wrap(clues.New("new"), "wrap"), + err: cluerr.Wrap(cluerr.New("new"), "wrap"), expect: "wrap: new", }, { name: "wrapped non-clues error", - err: clues.Wrap(sentinel, "wrap"), + err: cluerr.Wrap(sentinel, "wrap"), expect: "wrap: " + sentinel.Error(), }, { name: "wrapped stacked error", - err: clues.Wrap(clues.Stack(sentinel), "wrap"), + err: cluerr.Wrap(cluerr.Stack(sentinel), "wrap"), expect: "wrap: " + sentinel.Error(), }, { name: "multiple wraps", - err: clues.Wrap(clues.Wrap(clues.New("new"), "wrap"), "wrap2"), + err: cluerr.Wrap(cluerr.Wrap(cluerr.New("new"), "wrap"), "wrap2"), expect: "wrap2: wrap: new", }, { name: "wrap-stack-wrap-new", - err: clues.Wrap(clues.Stack(clues.Wrap(clues.New("new"), "wrap")), "wrap2"), + err: cluerr.Wrap(cluerr.Stack(cluerr.Wrap(cluerr.New("new"), "wrap")), "wrap2"), expect: "wrap2: wrap: new", }, { name: "many stacked errors", - err: clues.Stack(sentinel, errors.New("middle"), errors.New("base")), + err: cluerr.Stack(sentinel, errors.New("middle"), errors.New("base")), expect: sentinel.Error() + ": middle: base", }, { name: "stacked stacks", - err: clues.Stack( - clues.Stack(sentinel, errors.New("left")), - clues.Stack(errors.New("right"), errors.New("base")), + err: cluerr.Stack( + cluerr.Stack(sentinel, errors.New("left")), + cluerr.Stack(errors.New("right"), errors.New("base")), ), expect: sentinel.Error() + ": left: right: base", }, { name: "wrapped stacks", - err: clues.Stack( - clues.Wrap(clues.Stack(errors.New("top"), errors.New("left")), "left-stack"), - clues.Wrap(clues.Stack(errors.New("right"), errors.New("base")), "right-stack"), + err: cluerr.Stack( + cluerr.Wrap(cluerr.Stack(errors.New("top"), errors.New("left")), "left-stack"), + cluerr.Wrap(cluerr.Stack(errors.New("right"), errors.New("base")), "right-stack"), ), expect: "left-stack: top: left: right-stack: right: base", }, { - name: "wrapped stacks, all clues.New", - err: clues.Stack( - clues.Wrap(clues.Stack(clues.New("top"), clues.New("left")), "left-stack"), - clues.Wrap(clues.Stack(clues.New("right"), clues.New("base")), "right-stack"), + name: "wrapped stacks, all cluerr.New", + err: cluerr.Stack( + cluerr.Wrap(cluerr.Stack(cluerr.New("top"), cluerr.New("left")), "left-stack"), + cluerr.Wrap(cluerr.Stack(cluerr.New("right"), cluerr.New("base")), "right-stack"), ), expect: "left-stack: top: left: right-stack: right: base", }, @@ -566,45 +565,45 @@ func TestErrValues_stacks(t *testing.T) { }{ { name: "single err", - err: clues.Stack(clues.New("an err").With("k", "v")), + err: cluerr.Stack(cluerr.New("an err").With("k", "v")), expect: msa{"k": "v"}, }, { name: "two stack", - err: clues.Stack( - clues.New("an err").With("k", "v"), - clues.New("other").With("k2", "v2"), + err: cluerr.Stack( + cluerr.New("an err").With("k", "v"), + cluerr.New("other").With("k2", "v2"), ), expect: msa{"k": "v", "k2": "v2"}, }, { name: "sandvitch", - err: clues.Stack( - clues.New("top").With("k", "v"), + err: cluerr.Stack( + cluerr.New("top").With("k", "v"), errors.New("mid"), - clues.New("base").With("k2", "v2"), + cluerr.New("base").With("k2", "v2"), ), expect: msa{"k": "v", "k2": "v2"}, }, { name: "value collision", - err: clues.Stack( - clues.New("top").With("k", "v"), - clues.New("mid").With("k2", "v2"), - clues.New("base").With("k", "v3"), + err: cluerr.Stack( + cluerr.New("top").With("k", "v"), + cluerr.New("mid").With("k2", "v2"), + cluerr.New("base").With("k", "v3"), ), expect: msa{"k": "v3", "k2": "v2"}, }, { name: "double double", - err: clues.Stack( - clues.Stack( - clues.New("top").With("k", "v"), - clues.New("left").With("k2", "v2"), + err: cluerr.Stack( + cluerr.Stack( + cluerr.New("top").With("k", "v"), + cluerr.New("left").With("k2", "v2"), ), - clues.Stack( - clues.New("right").With("k3", "v3"), - clues.New("base").With("k4", "v4"), + cluerr.Stack( + cluerr.New("right").With("k3", "v3"), + cluerr.New("base").With("k4", "v4"), ), ), expect: msa{ @@ -616,14 +615,14 @@ func TestErrValues_stacks(t *testing.T) { }, { name: "double double collision", - err: clues.Stack( - clues.Stack( - clues.New("top").With("k", "v"), - clues.New("left").With("k2", "v2"), + err: cluerr.Stack( + cluerr.Stack( + cluerr.New("top").With("k", "v"), + cluerr.New("left").With("k2", "v2"), ), - clues.Stack( - clues.New("right").With("k3", "v3"), - clues.New("base").With("k", "v4"), + cluerr.Stack( + cluerr.New("right").With("k3", "v3"), + cluerr.New("base").With("k", "v4"), ), ), expect: msa{ @@ -634,17 +633,17 @@ func TestErrValues_stacks(t *testing.T) { }, { name: "double double animal wrap", - err: clues.Stack( - clues.Wrap( - clues.Stack( - clues.New("top").With("k", "v"), - clues.New("left").With("k2", "v2"), + err: cluerr.Stack( + cluerr.Wrap( + cluerr.Stack( + cluerr.New("top").With("k", "v"), + cluerr.New("left").With("k2", "v2"), ), "left-stack"), - clues.Wrap( - clues.Stack( - clues.New("right").With("k3", "v3"), - clues.New("base").With("k4", "v4"), + cluerr.Wrap( + cluerr.Stack( + cluerr.New("right").With("k3", "v3"), + cluerr.New("base").With("k4", "v4"), ), "right-stack"), ), @@ -657,17 +656,17 @@ func TestErrValues_stacks(t *testing.T) { }, { name: "double double animal wrap collision", - err: clues.Stack( - clues.Wrap( - clues.Stack( - clues.New("top").With("k", "v"), - clues.New("left").With("k2", "v2"), + err: cluerr.Stack( + cluerr.Wrap( + cluerr.Stack( + cluerr.New("top").With("k", "v"), + cluerr.New("left").With("k2", "v2"), ), "left-stack"), - clues.Wrap( - clues.Stack( - clues.New("right").With("k3", "v3"), - clues.New("base").With("k", "v4"), + cluerr.Wrap( + cluerr.Stack( + cluerr.New("right").With("k3", "v3"), + cluerr.New("base").With("k", "v4"), ), "right-stack"), ), @@ -680,24 +679,24 @@ func TestErrValues_stacks(t *testing.T) { } for _, test := range table { t.Run(test.name, func(t *testing.T) { - vs := clues.InErr(test.err) - mustEquals(t, test.expect, vs.Map(), false) + vs := cluerr.CluesIn(test.err) + tester.MustEquals(t, test.expect, vs.Map(), false) }) } } func TestImmutableErrors(t *testing.T) { - err := clues.New("an error").With("k", "v") + err := cluerr.New("an error").With("k", "v") check := msa{"k": "v"} - pre := clues.InErr(err) - mustEquals(t, check, pre.Map(), false) + pre := cluerr.CluesIn(err) + tester.MustEquals(t, check, pre.Map(), false) err2 := err.With("k2", "v2") if _, ok := pre.Map()["k2"]; ok { t.Errorf("previous map should not have been mutated by addition") } - post := clues.InErr(err2) + post := cluerr.CluesIn(err2) if post.Map()["k2"] != "v2" { t.Errorf("new map should contain the added value") } @@ -732,10 +731,10 @@ var ( target = mockTarget{errors.New(tgt)} sentinel = errors.New(stnl) other = errors.New("other") - leftTop = clues.New(lt).With(lt, "v"+lt).Label(lt) - leftBase = clues.New(lb).With(lb, "v"+lb).Label(lb) - rightTop = clues.New(rt).With(rt, "v"+rt).Label(rt) - rightBase = clues.New(rb).With(rb, "v"+rb).Label(rb) + leftTop = cluerr.New(lt).With(lt, "v"+lt).Label(lt) + leftBase = cluerr.New(lb).With(lb, "v"+lb).Label(lb) + rightTop = cluerr.New(rt).With(rt, "v"+rt).Label(rt) + rightBase = cluerr.New(rb).With(rb, "v"+rb).Label(rb) ) var testTable = []struct { @@ -747,79 +746,79 @@ var testTable = []struct { }{ { name: "plain stack", - err: clues.Stack(target, sentinel), + err: cluerr.Stack(target, sentinel), expectMsg: "target: sentinel", expectLabels: msa{}, expectValues: msa{}, }, { name: "plain wrap", - err: clues.Wrap(clues.Stack(target, sentinel), "wrap"), + err: cluerr.Wrap(cluerr.Stack(target, sentinel), "wrap"), expectLabels: msa{}, expectMsg: "wrap: target: sentinel", expectValues: msa{}, }, { name: "two stack; top", - err: clues.Stack(clues.Stack(target, sentinel), other), + err: cluerr.Stack(cluerr.Stack(target, sentinel), other), expectMsg: "target: sentinel: other", expectLabels: msa{}, expectValues: msa{}, }, { name: "two stack; base", - err: clues.Stack(other, clues.Stack(target, sentinel)), + err: cluerr.Stack(other, cluerr.Stack(target, sentinel)), expectMsg: "other: target: sentinel", expectLabels: msa{}, expectValues: msa{}, }, { name: "two wrap", - err: clues.Wrap(clues.Wrap(clues.Stack(target, sentinel), "inner"), "outer"), + err: cluerr.Wrap(cluerr.Wrap(cluerr.Stack(target, sentinel), "inner"), "outer"), expectMsg: "outer: inner: target: sentinel", expectLabels: msa{}, expectValues: msa{}, }, { name: "wrap stack", - err: clues.Wrap(clues.Stack(target, sentinel), "wrap"), + err: cluerr.Wrap(cluerr.Stack(target, sentinel), "wrap"), expectMsg: "wrap: target: sentinel", expectLabels: msa{}, expectValues: msa{}, }, { name: "stackwrap", - err: clues.StackWrap(target, sentinel, "wrap"), + err: cluerr.StackWrap(target, sentinel, "wrap"), expectMsg: "target: wrap: sentinel", expectLabels: msa{}, expectValues: msa{}, }, { name: "stackwrapWC", - err: clues.StackWrapWC(context.Background(), target, sentinel, "wrap"), + err: cluerr.StackWrapWC(context.Background(), target, sentinel, "wrap"), expectMsg: "target: wrap: sentinel", expectLabels: msa{}, expectValues: msa{}, }, { name: "wrap two stack: top", - err: clues.Wrap(clues.Stack(target, sentinel, other), "wrap"), + err: cluerr.Wrap(cluerr.Stack(target, sentinel, other), "wrap"), expectMsg: "wrap: target: sentinel: other", expectLabels: msa{}, expectValues: msa{}, }, { name: "wrap two stack: base", - err: clues.Wrap(clues.Stack(other, target, sentinel), "wrap"), + err: cluerr.Wrap(cluerr.Stack(other, target, sentinel), "wrap"), expectMsg: "wrap: other: target: sentinel", expectLabels: msa{}, expectValues: msa{}, }, { name: "double double stack; left top", - err: clues.Stack( - clues.Stack(target, sentinel, leftBase), - clues.Stack(rightTop, rightBase), + err: cluerr.Stack( + cluerr.Stack(target, sentinel, leftBase), + cluerr.Stack(rightTop, rightBase), ), expectMsg: "target: sentinel: left-base: right-top: right-base", expectLabels: msa{ @@ -835,9 +834,9 @@ var testTable = []struct { }, { name: "double double stack; left base", - err: clues.Stack( - clues.Stack(leftTop, target, sentinel), - clues.Stack(rightTop, rightBase), + err: cluerr.Stack( + cluerr.Stack(leftTop, target, sentinel), + cluerr.Stack(rightTop, rightBase), ), expectMsg: "left-top: target: sentinel: right-top: right-base", expectLabels: msa{ @@ -853,9 +852,9 @@ var testTable = []struct { }, { name: "double double stack; right top", - err: clues.Stack( - clues.Stack(leftTop, leftBase), - clues.Stack(target, sentinel, rightBase), + err: cluerr.Stack( + cluerr.Stack(leftTop, leftBase), + cluerr.Stack(target, sentinel, rightBase), ), expectMsg: "left-top: left-base: target: sentinel: right-base", expectLabels: msa{ @@ -871,9 +870,9 @@ var testTable = []struct { }, { name: "double double animal wrap; right base", - err: clues.Stack( - clues.Wrap(clues.Stack(leftTop, leftBase), "left-stack"), - clues.Wrap(clues.Stack(rightTop, target, sentinel), "right-stack"), + err: cluerr.Stack( + cluerr.Wrap(cluerr.Stack(leftTop, leftBase), "left-stack"), + cluerr.Wrap(cluerr.Stack(rightTop, target, sentinel), "right-stack"), ), expectMsg: "left-stack: left-top: left-base: right-stack: right-top: target: sentinel", expectLabels: msa{ @@ -889,9 +888,9 @@ var testTable = []struct { }, { name: "double double animal wrap; left top", - err: clues.Stack( - clues.Wrap(clues.Stack(target, sentinel, leftBase), "left-stack"), - clues.Wrap(clues.Stack(rightTop, rightBase), "right-stack"), + err: cluerr.Stack( + cluerr.Wrap(cluerr.Stack(target, sentinel, leftBase), "left-stack"), + cluerr.Wrap(cluerr.Stack(rightTop, rightBase), "right-stack"), ), expectMsg: "left-stack: target: sentinel: left-base: right-stack: right-top: right-base", expectLabels: msa{ @@ -907,9 +906,9 @@ var testTable = []struct { }, { name: "double double animal wrap; left base", - err: clues.Stack( - clues.Wrap(clues.Stack(leftTop, target, sentinel), "left-stack"), - clues.Wrap(clues.Stack(rightTop, rightBase), "right-stack"), + err: cluerr.Stack( + cluerr.Wrap(cluerr.Stack(leftTop, target, sentinel), "left-stack"), + cluerr.Wrap(cluerr.Stack(rightTop, rightBase), "right-stack"), ), expectMsg: "left-stack: left-top: target: sentinel: right-stack: right-top: right-base", expectLabels: msa{ @@ -925,9 +924,9 @@ var testTable = []struct { }, { name: "double double animal wrap; right top", - err: clues.Stack( - clues.Wrap(clues.Stack(leftTop, leftBase), "left-stack"), - clues.Wrap(clues.Stack(target, sentinel, rightBase), "right-stack"), + err: cluerr.Stack( + cluerr.Wrap(cluerr.Stack(leftTop, leftBase), "left-stack"), + cluerr.Wrap(cluerr.Stack(target, sentinel, rightBase), "right-stack"), ), expectMsg: "left-stack: left-top: left-base: right-stack: target: sentinel: right-base", expectLabels: msa{ @@ -943,9 +942,9 @@ var testTable = []struct { }, { name: "double double animal wrap; right base", - err: clues.Stack( - clues.Wrap(clues.Stack(leftTop, leftBase), "left-stack"), - clues.Wrap(clues.Stack(rightTop, target, sentinel), "right-stack"), + err: cluerr.Stack( + cluerr.Wrap(cluerr.Stack(leftTop, leftBase), "left-stack"), + cluerr.Wrap(cluerr.Stack(rightTop, target, sentinel), "right-stack"), ), expectMsg: "left-stack: left-top: left-base: right-stack: right-top: target: sentinel", expectLabels: msa{ @@ -970,7 +969,7 @@ func TestIs(t *testing.T) { }) } - notSentinel := clues.New("sentinel") + notSentinel := cluerr.New("sentinel") // NOT Is checks table := []struct { @@ -979,24 +978,24 @@ func TestIs(t *testing.T) { }{ { name: "plain stack", - err: clues.Stack(notSentinel), + err: cluerr.Stack(notSentinel), }, { name: "plain wrap", - err: clues.Wrap(notSentinel, "wrap"), + err: cluerr.Wrap(notSentinel, "wrap"), }, { name: "double double animal wrap", - err: clues.Stack( - clues.Wrap( - clues.Stack( - clues.New("left-top"), - clues.New("left-base"), + err: cluerr.Stack( + cluerr.Wrap( + cluerr.Stack( + cluerr.New("left-top"), + cluerr.New("left-base"), ), "left-stack"), - clues.Wrap( - clues.Stack( - clues.New("right-top"), + cluerr.Wrap( + cluerr.Stack( + cluerr.New("right-top"), notSentinel, ), "right-stack"), @@ -1031,24 +1030,24 @@ func TestAs(t *testing.T) { }{ { name: "plain stack", - err: clues.Stack(notTarget), + err: cluerr.Stack(notTarget), }, { name: "plain wrap", - err: clues.Wrap(notTarget, "wrap"), + err: cluerr.Wrap(notTarget, "wrap"), }, { name: "double double animal wrap", - err: clues.Stack( - clues.Wrap( - clues.Stack( - clues.New("left-top"), - clues.New("left-base"), + err: cluerr.Stack( + cluerr.Wrap( + cluerr.Stack( + cluerr.New("left-top"), + cluerr.New("left-base"), ), "left-stack"), - clues.Wrap( - clues.Stack( - clues.New("right-top"), + cluerr.Wrap( + cluerr.Stack( + cluerr.New("right-top"), notTarget, ), "right-stack"), @@ -1068,29 +1067,29 @@ func TestAs(t *testing.T) { func TestToCore(t *testing.T) { for _, test := range testTable { t.Run(test.name, func(t *testing.T) { - c := clues.ToCore(test.err) + c := cluerr.ToCore(test.err) if test.expectMsg != c.Msg { t.Errorf("expected Msg [%v], got [%v]", test.expectMsg, c.Msg) } - mustEquals(t, test.expectLabels, toMSA(c.Labels), false) - mustEquals(t, test.expectValues, toMSA(c.Values), false) + tester.MustEquals(t, test.expectLabels, toMSA(c.Labels), false) + tester.MustEquals(t, test.expectValues, toMSA(c.Values), false) }) } } func TestStackNils(t *testing.T) { - result := clues.Stack(nil) + result := cluerr.Stack(nil) if result != nil { t.Errorf("expected nil, got [%v]", result) } - e := clues.New("err") - result = clues.Stack(e, nil) + e := cluerr.New("err") + result = cluerr.Stack(e, nil) if result.Error() != e.Error() { t.Errorf("expected [%v], got [%v]", e, result) } - result = clues.Stack(nil, e) + result = cluerr.Stack(nil, e) if result.Error() != e.Error() { t.Errorf("expected [%v], got [%v]", e, result) } @@ -1099,7 +1098,7 @@ func TestStackNils(t *testing.T) { func TestOrNil(t *testing.T) { table := []struct { name string - err *clues.Err + err *cluerr.Err expectNil bool }{ { @@ -1109,12 +1108,12 @@ func TestOrNil(t *testing.T) { }, { name: "nil stack", - err: clues.Stack(nil).With("foo", "bar"), + err: cluerr.Stack(nil).With("foo", "bar"), expectNil: true, }, { name: "nil wrap", - err: clues.Wrap(nil, "msg").With("foo", "bar"), + err: cluerr.Wrap(nil, "msg").With("foo", "bar"), expectNil: true, }, { @@ -1132,81 +1131,24 @@ func TestOrNil(t *testing.T) { } } -type labelCounter map[string]int64 - -func (tla labelCounter) Add(l string, i int64) { - tla[l] = tla[l] + i -} - -var labelTable = []struct { - name string - labels []string - expect map[string]int64 -}{ - { - name: "no labels", - labels: []string{}, - expect: map[string]int64{}, - }, - { - name: "single label", - labels: []string{"un"}, - expect: map[string]int64{ - "un": 1, - }, - }, - { - name: "multiple labels", - labels: []string{"un", "deux"}, - expect: map[string]int64{ - "un": 1, - "deux": 1, - }, - }, - { - name: "duplicated label", - labels: []string{"un", "un"}, - expect: map[string]int64{ - "un": 1, - }, - }, - { - name: "multiple duplicated labels", - labels: []string{"un", "un", "deux", "deux"}, - expect: map[string]int64{ - "un": 1, - "deux": 1, - }, - }, - { - name: "empty string labels", - labels: []string{"", "", "un", "deux"}, - expect: map[string]int64{ - "": 1, - "un": 1, - "deux": 1, - }, - }, -} - // --------------------------------------------------------------------------- // helpers // --------------------------------------------------------------------------- func withSkipCaller(err error, depth int) error { - return clues.WithSkipCaller(err, depth) + return cluerr.SkipCaller(err, depth) } -func cluesWithSkipCaller(err *clues.Err, depth int) error { +func cluesWithSkipCaller(err *cluerr.Err, depth int) error { return err.SkipCaller(depth) } -func wrapWithFuncWithGeneric[E error](err E) *clues.Err { - return clues.Wrap(err, "with-generic") +func wrapWithFuncWithGeneric[E error](err E) *cluerr.Err { + return cluerr.Wrap(err, "with-generic") } -func withNoTrace(err error) *clues.Err { - return clues.Wrap(err, "no-trace").NoTrace() +func withNoTrace(err error) *cluerr.Err { + return cluerr.Wrap(err, "no-trace").NoTrace() } func withCommentWrapper( @@ -1215,13 +1157,14 @@ func withCommentWrapper( vs ...any, ) error { // always add two comments to test that both are saved - return clues. - Comment(err, msg, vs...). + return cluerr. + Stack(err). + Comment(msg, vs...). Comment(msg+" - repeat", vs...) } -func cluesWithCommentWrapper( - err *clues.Err, +func cluerrWithCommentWrapper( + err *cluerr.Err, msg string, vs ...any, ) error { diff --git a/errcore.go b/cluerr/errcore.go similarity index 96% rename from errcore.go rename to cluerr/errcore.go index b6ea874..04dc5e3 100644 --- a/errcore.go +++ b/cluerr/errcore.go @@ -1,9 +1,10 @@ -package clues +package cluerr import ( "fmt" "strings" + "github.com/alcionai/clues/internal/node" "github.com/alcionai/clues/internal/stringify" "golang.org/x/exp/maps" ) @@ -17,7 +18,7 @@ type ErrCore struct { Msg string `json:"msg"` Labels map[string]struct{} `json:"labels"` Values map[string]any `json:"values"` - Comments comments `json:"comments"` + Comments node.CommentHistory `json:"comments"` } // Core transforms the error into an ErrCore. diff --git a/cluerr/labels.go b/cluerr/labels.go new file mode 100644 index 0000000..a6380e5 --- /dev/null +++ b/cluerr/labels.go @@ -0,0 +1,87 @@ +package cluerr + +import "golang.org/x/exp/maps" + +// ------------------------------------------------------------ +// labels +// ------------------------------------------------------------ + +func (err *Err) HasLabel(label string) bool { + if isNilErrIface(err) { + return false + } + + // Check all labels in the error and it's stack since the stack isn't + // traversed separately. If we don't check the stacked labels here we'll skip + // checking them completely. + if _, ok := err.Labels()[label]; ok { + return true + } + + return HasLabel(err.e, label) +} + +func HasLabel(err error, label string) bool { + if isNilErrIface(err) { + return false + } + + if e, ok := err.(*Err); ok { + return e.HasLabel(label) + } + + return HasLabel(unwrap(err), label) +} + +func (err *Err) Label(labels ...string) *Err { + if isNilErrIface(err) { + return nil + } + + if len(err.labels) == 0 { + err.labels = map[string]struct{}{} + } + + for _, label := range labels { + err.labels[label] = struct{}{} + } + + return err +} + +func Label(err error, label string) *Err { + return tryExtendErr(err, "", nil, 1).Label(label) +} + +func (err *Err) Labels() map[string]struct{} { + if isNilErrIface(err) { + return map[string]struct{}{} + } + + labels := map[string]struct{}{} + + for _, se := range err.stack { + maps.Copy(labels, Labels(se)) + } + + if err.e != nil { + maps.Copy(labels, Labels(err.e)) + } + + maps.Copy(labels, err.labels) + + return labels +} + +func Labels(err error) map[string]struct{} { + for err != nil { + e, ok := err.(*Err) + if ok { + return e.Labels() + } + + err = unwrap(err) + } + + return map[string]struct{}{} +} diff --git a/clues.go b/clues.go index 065e668..2b124cb 100644 --- a/clues.go +++ b/clues.go @@ -2,7 +2,9 @@ package clues import ( "context" + "fmt" + "github.com/alcionai/clues/internal/node" "github.com/alcionai/clues/internal/stringify" ) @@ -10,50 +12,59 @@ import ( // persistent client initialization // --------------------------------------------------------------------------- -// 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. +// InitializeOTEL will spin up the OTEL clients that are held by clues, +// Clues will eagerly use these clients 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 Initialize( +// Clues will operate as expected in the event of an error, or if OTEL is not +// initialized. This is a purely optional step. +func InitializeOTEL( ctx context.Context, serviceName string, config OTELConfig, ) (context.Context, error) { - nc := nodeFromCtx(ctx) + nc := node.FromCtx(ctx) - err := nc.init(ctx, serviceName, config) + err := nc.InitOTEL(ctx, serviceName, config.toInternalConfig()) if err != nil { return ctx, err } - return setNodeInCtx(ctx, nc), nil + return node.EmbedInCtx(ctx, nc), nil } // 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 Close(ctx context.Context) error { - nc := nodeFromCtx(ctx) + nc := node.FromCtx(ctx) - if nc.otel != nil { - err := nc.otel.close(ctx) + if nc.OTEL != nil { + err := nc.OTEL.Close(ctx) if err != nil { - return Wrap(err, "closing otel client") + return fmt.Errorf("closing otel client: %w", err) } } return nil } +// --------------------------------------------------------------------------- +// data access +// --------------------------------------------------------------------------- + +// In retrieves the clues structured data from the context. +func In(ctx context.Context) *node.Node { + return node.FromCtx(ctx) +} + // --------------------------------------------------------------------------- // key-value metadata // --------------------------------------------------------------------------- // Add adds all key-value pairs to the clues. func Add(ctx context.Context, kvs ...any) context.Context { - nc := nodeFromCtx(ctx) - return setNodeInCtx(ctx, nc.addValues(stringify.Normalize(kvs...))) + nc := node.FromCtx(ctx) + return node.EmbedInCtx(ctx, nc.AddValues(stringify.Normalize(kvs...))) } // AddMap adds a shallow clone of the map to a namespaced set of clues. @@ -61,14 +72,14 @@ func AddMap[K comparable, V any]( ctx context.Context, m map[K]V, ) context.Context { - nc := nodeFromCtx(ctx) + nc := node.FromCtx(ctx) kvs := make([]any, 0, len(m)*2) for k, v := range m { kvs = append(kvs, k, v) } - return setNodeInCtx(ctx, nc.addValues(stringify.Normalize(kvs...))) + return node.EmbedInCtx(ctx, nc.AddValues(stringify.Normalize(kvs...))) } // --------------------------------------------------------------------------- @@ -82,12 +93,12 @@ func AddMap[K comparable, V any]( // 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 InjectTrace[C traceMapCarrierBase]( +func InjectTrace[C node.TraceMapCarrierBase]( ctx context.Context, mapCarrier C, ) C { - nodeFromCtx(ctx). - injectTrace(ctx, asTraceMapCarrier(mapCarrier)) + node.FromCtx(ctx). + InjectTrace(ctx, node.AsTraceMapCarrier(mapCarrier)) return mapCarrier } @@ -95,12 +106,12 @@ func InjectTrace[C traceMapCarrierBase]( // ReceiveTrace extracts the current trace details from the // headers and adds them to the context. If otel is not // initialized, no-ops. -func ReceiveTrace[C traceMapCarrierBase]( +func ReceiveTrace[C node.TraceMapCarrierBase]( ctx context.Context, mapCarrier C, ) context.Context { - return nodeFromCtx(ctx). - receiveTrace(ctx, asTraceMapCarrier(mapCarrier)) + return node.FromCtx(ctx). + ReceiveTrace(ctx, node.AsTraceMapCarrier(mapCarrier)) } // AddSpan stacks a clues node onto this context and uses the provided @@ -114,27 +125,28 @@ func AddSpan( name string, kvs ...any, ) context.Context { - nc := nodeFromCtx(ctx) + nc := node.FromCtx(ctx) - var node *dataNode + var spanned *node.Node if len(kvs) > 0 { - ctx, node = nc.addSpan(ctx, name) - node.id = name - node = node.addValues(stringify.Normalize(kvs...)) + ctx, spanned = nc.AddSpan(ctx, name) + spanned.ID = name + spanned = spanned.AddValues(stringify.Normalize(kvs...)) } else { - ctx, node = nc.addSpan(ctx, name) - node = node.trace(name) + ctx, spanned = nc.AddSpan(ctx, name) + spanned = spanned.AppendToTree(name) } - return setNodeInCtx(ctx, node) + return node.EmbedInCtx(ctx, spanned) } // CloseSpan closes the current span in the clues node. Should only be called // following a `clues.AddSpan()` call. func CloseSpan(ctx context.Context) context.Context { - nc := nodeFromCtx(ctx).closeSpan(ctx) - return setNodeInCtx(ctx, nc) + return node.EmbedInCtx( + ctx, + node.FromCtx(ctx).CloseSpan(ctx)) } // --------------------------------------------------------------------------- @@ -167,10 +179,10 @@ func AddComment( msg string, vs ...any, ) context.Context { - nc := nodeFromCtx(ctx) - nn := nc.addComment(1, msg, vs...) + nc := node.FromCtx(ctx) + nn := nc.AddComment(1, msg, vs...) - return setNodeInCtx(ctx, nn) + return node.EmbedInCtx(ctx, nn) } // --------------------------------------------------------------------------- @@ -194,27 +206,27 @@ func AddAgent( ctx context.Context, name string, ) context.Context { - nc := nodeFromCtx(ctx) - nn := nc.addAgent(name) + nc := node.FromCtx(ctx) + nn := nc.AddAgent(name) - return setNodeInCtx(ctx, nn) + return node.EmbedInCtx(ctx, nn) } // 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 +// record those values to the node in which it was created. All relayed // values are namespaced to the owning agent. func Relay( ctx context.Context, agent string, vs ...any, ) { - nc := nodeFromCtx(ctx) - ag, ok := nc.agents[agent] + nc := node.FromCtx(ctx) + ag, ok := nc.Agents[agent] if !ok { return } // set values, not add. We don't want agents to own a full clues tree. - ag.data.setValues(stringify.Normalize(vs...)) + ag.Data.SetValues(stringify.Normalize(vs...)) } diff --git a/clues_test.go b/clues_test.go index 0bf0b87..4234bda 100644 --- a/clues_test.go +++ b/clues_test.go @@ -10,7 +10,7 @@ import ( "github.com/alcionai/clues" "github.com/alcionai/clues/cecrets" - "golang.org/x/exp/slices" + "github.com/alcionai/clues/internal/tester" ) func init() { @@ -20,195 +20,34 @@ func init() { }) } -func mapEquals( - t *testing.T, - ctx context.Context, - expect msa, - expectCluesTrace bool, -) { - mustEquals( - t, - expect, - clues.In(ctx).Map(), - expectCluesTrace) -} - -func mustEquals[K comparable, V any]( - t *testing.T, - expect, got map[K]V, - hasCluesTrace bool, -) { - e, g := toMSS(expect), toMSS(got) - - if len(g) > 0 { - if _, ok := g["clues_trace"]; hasCluesTrace && !ok { - t.Errorf( - "expected map to contain key [clues_trace]\ngot: %+v", - g) - } - delete(g, "clues_trace") - } - - if len(e) != len(g) { - t.Errorf( - "expected map of len [%d], received len [%d]\n%s", - len(e), len(g), expectedReceived(expect, got), - ) - } - - for k, v := range e { - if g[k] != v { - t.Errorf( - "expected map to contain key:value [%s: %s]\n%s", - k, v, expectedReceived(expect, got), - ) - } - } - - for k, v := range g { - if e[k] != v { - t.Errorf( - "map contains unexpected key:value [%s: %s]\n%s", - k, v, expectedReceived(expect, got), - ) - } - } -} - -func expectedReceived[K comparable, V any](e, r map[K]V) string { - return fmt.Sprintf( - "expected: %#v\nreceived: %#v\n\n", - e, r) -} - -type mss map[string]string - -func toMSS[K comparable, V any](m map[K]V) mss { - r := mss{} - - for k, v := range m { - ks := fmt.Sprintf("%v", k) - vs := fmt.Sprintf("%v", v) - r[ks] = vs - } - - return r -} - -type msa map[string]any - -func toMSA[T any](m map[string]T) msa { - to := make(msa, len(m)) - for k, v := range m { - to[k] = v - } - - return to -} - -type sa []any - -func (s sa) stringWith(other []any) string { - return fmt.Sprintf( - "\nexpected: %+v\nreceived: %+v\n", - s, other, - ) -} - -func (s sa) equals(t *testing.T, other []any) { - idx := slices.Index(other, "clues_trace") - if idx >= 0 { - other = append(other[:idx], other[idx+2:]...) - } - - if len(s) != len(other) { - t.Errorf( - "expected slice of len [%d], received len [%d]\n%s", - len(s), len(other), s.stringWith(other), - ) - } - - for _, v := range s { - var found bool - for _, o := range other { - if v == o { - found = true - break - } - } - if !found { - t.Errorf("expected slice to contain [%v]\n%s", v, s.stringWith(other)) - } - } - - for _, o := range other { - var found bool - for _, v := range s { - if v == o { - found = true - break - } - } - if !found { - t.Errorf("did not expect slice to contain [%v]\n%s", o, s.stringWith(other)) - } - } -} - -func assert( - t *testing.T, - ctx context.Context, - ns string, - eM, eMns msa, - eS, eSns sa, -) { - vs := clues.In(ctx) - mustEquals(t, eM, vs.Map(), false) - eS.equals(t, vs.Slice()) -} - -func assertMSA( - t *testing.T, - ctx context.Context, - ns string, - eM, eMns msa, - eS, eSns sa, -) { - vs := clues.In(ctx) - mustEquals(t, eM, toMSA(vs.Map()), false) - eS.equals(t, vs.Slice()) -} - -type testCtx struct{} - func TestAdd(t *testing.T) { table := []struct { name string kvs [][]string - expectM msa - expectS sa + expectM tester.MSA + expectS tester.SA }{ - {"single", [][]string{{"k", "v"}}, msa{"k": "v"}, sa{"k", "v"}}, - {"multiple", [][]string{{"a", "1"}, {"b", "2"}}, msa{"a": "1", "b": "2"}, sa{"a", "1", "b", "2"}}, - {"duplicates", [][]string{{"a", "1"}, {"a", "2"}}, msa{"a": "2"}, sa{"a", "2"}}, - {"none", [][]string{}, msa{}, sa{}}, + {"single", [][]string{{"k", "v"}}, tester.MSA{"k": "v"}, tester.SA{"k", "v"}}, + {"multiple", [][]string{{"a", "1"}, {"b", "2"}}, tester.MSA{"a": "1", "b": "2"}, tester.SA{"a", "1", "b", "2"}}, + {"duplicates", [][]string{{"a", "1"}, {"a", "2"}}, tester.MSA{"a": "2"}, tester.SA{"a", "2"}}, + {"none", [][]string{}, tester.MSA{}, tester.SA{}}, } for _, test := range table { t.Run(test.name, func(t *testing.T) { - ctx := context.WithValue(context.Background(), testCtx{}, "instance") - check := msa{} - mustEquals(t, check, clues.In(ctx).Map(), false) + ctx := context.WithValue(context.Background(), tester.StubCtx{}, "instance") + check := tester.MSA{} + tester.MustEquals(t, check, clues.In(ctx).Map(), false) for _, kv := range test.kvs { ctx = clues.Add(ctx, kv[0], kv[1]) check[kv[0]] = kv[1] - mustEquals(t, check, clues.In(ctx).Map(), false) + tester.MustEquals(t, check, clues.In(ctx).Map(), false) } - assert( + tester.AssertEq( t, ctx, "", - test.expectM, msa{}, - test.expectS, sa{}) + test.expectM, tester.MSA{}, + test.expectS, tester.SA{}) }) } } @@ -216,33 +55,33 @@ func TestAdd(t *testing.T) { func TestAddMap(t *testing.T) { table := []struct { name string - ms []msa - expectM msa - expectS sa + ms []tester.MSA + expectM tester.MSA + expectS tester.SA }{ - {"single", []msa{{"k": "v"}}, msa{"k": "v"}, sa{"k", "v"}}, - {"multiple", []msa{{"a": "1"}, {"b": "2"}}, msa{"a": "1", "b": "2"}, sa{"a", "1", "b", "2"}}, - {"duplicate", []msa{{"a": "1"}, {"a": "2"}}, msa{"a": "2"}, sa{"a", "2"}}, - {"none", []msa{}, msa{}, sa{}}, + {"single", []tester.MSA{{"k": "v"}}, tester.MSA{"k": "v"}, tester.SA{"k", "v"}}, + {"multiple", []tester.MSA{{"a": "1"}, {"b": "2"}}, tester.MSA{"a": "1", "b": "2"}, tester.SA{"a", "1", "b", "2"}}, + {"duplicate", []tester.MSA{{"a": "1"}, {"a": "2"}}, tester.MSA{"a": "2"}, tester.SA{"a", "2"}}, + {"none", []tester.MSA{}, tester.MSA{}, tester.SA{}}, } for _, test := range table { t.Run(test.name, func(t *testing.T) { - ctx := context.WithValue(context.Background(), testCtx{}, "instance") - check := msa{} - mustEquals(t, check, clues.In(ctx).Map(), false) + ctx := context.WithValue(context.Background(), tester.StubCtx{}, "instance") + check := tester.MSA{} + tester.MustEquals(t, check, clues.In(ctx).Map(), false) for _, m := range test.ms { ctx = clues.AddMap(ctx, m) for k, v := range m { check[k] = v } - mustEquals(t, check, clues.In(ctx).Map(), false) + tester.MustEquals(t, check, clues.In(ctx).Map(), false) } - assert( + tester.AssertEq( t, ctx, "", - test.expectM, msa{}, - test.expectS, sa{}) + test.expectM, tester.MSA{}, + test.expectS, tester.SA{}) }) } } @@ -252,16 +91,16 @@ func TestAddSpan(t *testing.T) { name string names []string expectTrace string - kvs sa - expectM msa - expectS sa + kvs tester.SA + expectM tester.MSA + expectS tester.SA }{ - {"single", []string{"single"}, "single", nil, msa{}, sa{}}, - {"multiple", []string{"single", "multiple"}, "single,multiple", nil, msa{}, sa{}}, - {"duplicates", []string{"single", "multiple", "multiple"}, "single,multiple,multiple", nil, msa{}, sa{}}, - {"single with kvs", []string{"single"}, "single", sa{"k", "v"}, msa{"k": "v"}, sa{"k", "v"}}, - {"multiple with kvs", []string{"single", "multiple"}, "single,multiple", sa{"k", "v"}, msa{"k": "v"}, sa{"k", "v"}}, - {"duplicates with kvs", []string{"single", "multiple", "multiple"}, "single,multiple,multiple", sa{"k", "v"}, msa{"k": "v"}, sa{"k", "v"}}, + {"single", []string{"single"}, "single", nil, tester.MSA{}, tester.SA{}}, + {"multiple", []string{"single", "multiple"}, "single,multiple", nil, tester.MSA{}, tester.SA{}}, + {"duplicates", []string{"single", "multiple", "multiple"}, "single,multiple,multiple", nil, tester.MSA{}, tester.SA{}}, + {"single with kvs", []string{"single"}, "single", tester.SA{"k", "v"}, tester.MSA{"k": "v"}, tester.SA{"k", "v"}}, + {"multiple with kvs", []string{"single", "multiple"}, "single,multiple", tester.SA{"k", "v"}, tester.MSA{"k": "v"}, tester.SA{"k", "v"}}, + {"duplicates with kvs", []string{"single", "multiple", "multiple"}, "single,multiple,multiple", tester.SA{"k", "v"}, tester.MSA{"k": "v"}, tester.SA{"k", "v"}}, } for _, test := range table { for _, init := range []bool{true, false} { @@ -269,9 +108,9 @@ func TestAddSpan(t *testing.T) { ctx := context.Background() if init { - ictx, err := clues.Initialize(ctx, test.name, clues.OTELConfig{ - GRPCEndpoint: "localhost:4317", - }) + ocfg := clues.OTELConfig{GRPCEndpoint: "localhost:4317"} + + ictx, err := clues.InitializeOTEL(ctx, test.name, ocfg) if err != nil { t.Error("initializing clues", err) return @@ -288,18 +127,18 @@ func TestAddSpan(t *testing.T) { ctx = ictx } - ctx = context.WithValue(ctx, testCtx{}, "instance") - mustEquals(t, msa{}, clues.In(ctx).Map(), false) + ctx = context.WithValue(ctx, tester.StubCtx{}, "instance") + tester.MustEquals(t, tester.MSA{}, clues.In(ctx).Map(), false) for _, name := range test.names { ctx = clues.AddSpan(ctx, name, test.kvs...) defer clues.CloseSpan(ctx) } - assert( + tester.AssertEq( t, ctx, "", - test.expectM, msa{}, - test.expectS, sa{}) + test.expectM, tester.MSA{}, + test.expectS, tester.SA{}) c := clues.In(ctx).Map() if c["clues_trace"] != test.expectTrace { @@ -313,12 +152,12 @@ func TestAddSpan(t *testing.T) { func TestImmutableCtx(t *testing.T) { var ( ctx = context.Background() - testCtx = context.WithValue(ctx, testCtx{}, "instance") - check = msa{} + testCtx = context.WithValue(ctx, tester.StubCtx{}, "instance") + check = tester.MSA{} pre = clues.In(testCtx) preMap = pre.Map() ) - mustEquals(t, check, preMap, false) + tester.MustEquals(t, check, preMap, false) ctx2 := clues.Add(testCtx, "k", "v") if _, ok := preMap["k"]; ok { @@ -326,7 +165,7 @@ func TestImmutableCtx(t *testing.T) { } pre = clues.In(testCtx) - if _, ok := preMap["k"]; ok { + if _, ok := pre.Map()["k"]; ok { t.Errorf("previous map within ctx should not have been mutated by addition") } @@ -342,11 +181,11 @@ func TestImmutableCtx(t *testing.T) { lr = clues.Add(l, "beaux", "regard") ) - mustEquals(t, msa{}, clues.In(ctx).Map(), false) - mustEquals(t, msa{"foo": "bar"}, clues.In(l).Map(), false) - mustEquals(t, msa{"baz": "qux"}, clues.In(r).Map(), false) - mustEquals(t, msa{"foo": "bar", "fnords": "smarf"}, clues.In(ll).Map(), false) - mustEquals(t, msa{"foo": "bar", "beaux": "regard"}, clues.In(lr).Map(), false) + tester.MustEquals(t, tester.MSA{}, clues.In(ctx).Map(), false) + tester.MustEquals(t, tester.MSA{"foo": "bar"}, clues.In(l).Map(), false) + tester.MustEquals(t, tester.MSA{"baz": "qux"}, clues.In(r).Map(), false) + tester.MustEquals(t, tester.MSA{"foo": "bar", "fnords": "smarf"}, clues.In(ll).Map(), false) + tester.MustEquals(t, tester.MSA{"foo": "bar", "beaux": "regard"}, clues.In(lr).Map(), false) } var _ cecrets.Concealer = &safe{} @@ -393,52 +232,52 @@ func TestAdd_concealed(t *testing.T) { table := []struct { name string concealers [][]any - expectM msa - expectS sa + expectM tester.MSA + expectS tester.SA }{ { name: "all hidden", concealers: [][]any{{cecrets.Hide("k"), cecrets.Hide("v")}, {cecrets.Hide("not_k"), cecrets.Hide("not_v")}}, - expectM: msa{"ba3acd7f61e405ca": "509bf4fb69f55ca3", "cc69e8e6a3b991d5": "f669b3b5927161b2"}, - expectS: sa{"ba3acd7f61e405ca", "509bf4fb69f55ca3", "cc69e8e6a3b991d5", "f669b3b5927161b2"}, + expectM: tester.MSA{"ba3acd7f61e405ca": "509bf4fb69f55ca3", "cc69e8e6a3b991d5": "f669b3b5927161b2"}, + expectS: tester.SA{"ba3acd7f61e405ca", "509bf4fb69f55ca3", "cc69e8e6a3b991d5", "f669b3b5927161b2"}, }, { name: "partially hidden", concealers: [][]any{{cecrets.Hide("a"), safe{1}}, {cecrets.Hide(2), safe{"b"}}}, - expectM: msa{"7d2ded59f6a549d7": "1", "cbdd96fab83ece85": `"b"`}, - expectS: sa{"7d2ded59f6a549d7", "1", "cbdd96fab83ece85", `"b"`}, + expectM: tester.MSA{"7d2ded59f6a549d7": "1", "cbdd96fab83ece85": `"b"`}, + expectS: tester.SA{"7d2ded59f6a549d7", "1", "cbdd96fab83ece85", `"b"`}, }, { name: "custom concealer", concealers: [][]any{ {custom{"foo", "bar"}, custom{"baz", "qux"}}, {custom{"fnords", "smarf"}, custom{"beau", "regard"}}}, - expectM: msa{"foo - fcde2b2edba56bf4": "baz - 21f58d27f827d295", "fnords - dd738d92a334bb85": "beau - fe099a0620ce9759"}, - expectS: sa{"foo - fcde2b2edba56bf4", "baz - 21f58d27f827d295", "fnords - dd738d92a334bb85", "beau - fe099a0620ce9759"}, + expectM: tester.MSA{"foo - fcde2b2edba56bf4": "baz - 21f58d27f827d295", "fnords - dd738d92a334bb85": "beau - fe099a0620ce9759"}, + expectS: tester.SA{"foo - fcde2b2edba56bf4", "baz - 21f58d27f827d295", "fnords - dd738d92a334bb85", "beau - fe099a0620ce9759"}, }, { name: "none", concealers: [][]any{}, - expectM: msa{}, - expectS: sa{}, + expectM: tester.MSA{}, + expectS: tester.SA{}, }, } for _, test := range table { t.Run(test.name, func(t *testing.T) { - ctx := context.WithValue(context.Background(), testCtx{}, "instance") - check := msa{} - mustEquals(t, check, toMSA(clues.In(ctx).Map()), false) + ctx := context.WithValue(context.Background(), tester.StubCtx{}, "instance") + check := tester.MSA{} + tester.MustEquals(t, check, tester.ToMSA(clues.In(ctx).Map()), false) for _, cs := range test.concealers { ctx = clues.Add(ctx, cs...) check[concealed(cs[0])] = concealed(cs[1]) - mustEquals(t, check, toMSA(clues.In(ctx).Map()), false) + tester.MustEquals(t, check, tester.ToMSA(clues.In(ctx).Map()), false) } - assertMSA( + tester.AssertMSA( t, ctx, "", - test.expectM, msa{}, - test.expectS, sa{}) + test.expectM, tester.MSA{}, + test.expectS, tester.SA{}) }) } } @@ -574,7 +413,7 @@ func TestAddAgent(t *testing.T) { ctx := context.Background() ctx = clues.Add(ctx, "one", 1) - mapEquals(t, ctx, msa{ + tester.MapEquals(t, ctx, tester.MSA{ "one": 1, }, false) @@ -582,11 +421,11 @@ func TestAddAgent(t *testing.T) { clues.Relay(ctx, "wit", "zero", 0) clues.Relay(ctxWithWit, "wit", "two", 2) - mapEquals(t, ctx, msa{ + tester.MapEquals(t, ctx, tester.MSA{ "one": 1, }, false) - mapEquals(t, ctxWithWit, msa{ + tester.MapEquals(t, ctxWithWit, tester.MSA{ "one": 1, "agents": map[string]map[string]any{ "wit": { @@ -598,11 +437,11 @@ func TestAddAgent(t *testing.T) { ctxWithTim := clues.AddAgent(ctxWithWit, "tim") clues.Relay(ctxWithTim, "tim", "three", 3) - mapEquals(t, ctx, msa{ + tester.MapEquals(t, ctx, tester.MSA{ "one": 1, }, false) - mapEquals(t, ctxWithTim, msa{ + tester.MapEquals(t, ctxWithTim, tester.MSA{ "one": 1, "agents": map[string]map[string]any{ "wit": { @@ -617,12 +456,12 @@ func TestAddAgent(t *testing.T) { ctxWithBob := clues.AddAgent(ctx, "bob") clues.Relay(ctxWithBob, "bob", "four", 4) - mapEquals(t, ctx, msa{ + tester.MapEquals(t, ctx, tester.MSA{ "one": 1, }, false) // should not have changed since its first usage - mapEquals(t, ctxWithWit, msa{ + tester.MapEquals(t, ctxWithWit, tester.MSA{ "one": 1, "agents": map[string]map[string]any{ "wit": { @@ -632,7 +471,7 @@ func TestAddAgent(t *testing.T) { }, false) // should not have changed since its first usage - mapEquals(t, ctxWithTim, msa{ + tester.MapEquals(t, ctxWithTim, tester.MSA{ "one": 1, "agents": map[string]map[string]any{ "wit": { @@ -644,7 +483,7 @@ func TestAddAgent(t *testing.T) { }, }, false) - mapEquals(t, ctxWithBob, msa{ + tester.MapEquals(t, ctxWithBob, tester.MSA{ "one": 1, "agents": map[string]map[string]any{ "bob": { diff --git a/datanode.go b/datanode.go deleted file mode 100644 index 2338e01..0000000 --- a/datanode.go +++ /dev/null @@ -1,680 +0,0 @@ -package clues - -import ( - "context" - "encoding/json" - "fmt" - "net/http" - "path" - "runtime" - "strings" - - "github.com/alcionai/clues/internal/stringify" - "github.com/google/uuid" - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/attribute" - otellog "go.opentelemetry.io/otel/log" - "go.opentelemetry.io/otel/propagation" - "go.opentelemetry.io/otel/trace" - "golang.org/x/exp/maps" - "golang.org/x/exp/slices" -) - -// --------------------------------------------------------------------------- -// data nodes -// --------------------------------------------------------------------------- - -type Adder interface { - Add(key string, n int64) -} - -// dataNodes contain the data tracked by both clues in contexts and in errors. -// -// These nodes compose a tree, such that nodes can walk their ancestry path from -// leaf (the current node) to root (the highest ancestor), but not from root to -// child. This allows clues to establish sets of common ancestor data with unique -// branches for individual descendants, making the addition of new data inherently -// theadsafe. -// -// For collisions during aggregation, distance from the root denotes priority, -// with the root having the lowest priority. IE: if a child overwrites a key -// declared by an ancestor, the child's entry takes priority. -type dataNode struct { - parent *dataNode - - // otel contains the client instance for the in memory otel runtime. It is only - // present if the end user calls the clues initialization step. - otel *otelClient - - // span is the current otel Span. - // Spans are kept separately from the otelClient because we want the client to - // maintain a consistent reference to otel initialization, while the span can - // get replaced at arbitrary points. - span trace.Span - - // ids are optional and are used primarily as tracing markers. - // if empty, the trace for that node will get skipped when building the - // full trace along the node's ancestry path in the tree. - id string - - // values are they arbitrary key:value pairs that appear in clues when callers - // use the Add(ctx, k, v) or err.With(k, v) adders. Each key-value pair added - // to the node is used to produce the final set of Values() in the dataNode, - // with lower nodes in the tree taking priority over higher nodes for any - // collision resolution. - values map[string]any - - // each node can hold a single commment. The history of comments produced - // by the ancestry path through the tree will get concatenated from oldest - // ancestor to the current node to produce the comment history. - comment comment - - // agents act as proxy dataNodes that can relay specific, intentional data - // additions. They're namespaced so that additions to the agents don't accidentally - // clobber other values in the dataNode. This also allows agents to protect - // variations of data from each other, in case users need to compare differences - // on the same keys. That's not the goal for agents, exactly, but it is capable. - agents map[string]*agent -} - -// spawnDescendant generates a new dataNode that is a descendant of the current -// node. A descendant maintains a pointer to its parent, and carries any genetic -// necessities (ie, copies of fields) that must be present for continued functionality. -func (dn *dataNode) spawnDescendant() *dataNode { - agents := maps.Clone(dn.agents) - - if agents == nil && dn.agents != nil { - agents = map[string]*agent{} - } - - return &dataNode{ - parent: dn, - otel: dn.otel, - span: dn.span, - agents: agents, - } -} - -// --------------------------------------------------------------------------- -// setters -// --------------------------------------------------------------------------- - -// addValues adds all entries in the map to the dataNode's values. -// automatically propagates values onto the current span. -func (dn *dataNode) addValues(m map[string]any) *dataNode { - if m == nil { - m = map[string]any{} - } - - spawn := dn.spawnDescendant() - spawn.setValues(m) - spawn.addSpanAttributes(m) - - return spawn -} - -// setValues is a helper called by addValues. -func (dn *dataNode) setValues(m map[string]any) { - if len(m) == 0 { - return - } - - if len(dn.values) == 0 { - dn.values = map[string]any{} - } - - maps.Copy(dn.values, m) -} - -// trace adds a new leaf containing a trace ID and no other values. -func (dn *dataNode) trace(name string) *dataNode { - if name == "" { - name = makeNodeID() - } - - spawn := dn.spawnDescendant() - spawn.id = name - - return spawn -} - -// --------------------------------------------------------------------------- -// getters -// --------------------------------------------------------------------------- - -// lineage runs the fn on every valueNode in the ancestry tree, -// starting at the root and ending at the dataNode. -func (dn *dataNode) lineage(fn func(id string, vs map[string]any)) { - if dn == nil { - return - } - - if dn.parent != nil { - dn.parent.lineage(fn) - } - - fn(dn.id, dn.values) -} - -// 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 In(ctx context.Context) *dataNode { - return nodeFromCtx(ctx) -} - -// Map flattens the tree of dataNode.values into a map. Descendant nodes -// take priority over ancestors in cases of collision. -func (dn *dataNode) Map() map[string]any { - var ( - m = map[string]any{} - nodeIDs = []string{} - ) - - dn.lineage(func(id string, vs map[string]any) { - if len(id) > 0 { - nodeIDs = append(nodeIDs, id) - } - - for k, v := range vs { - m[k] = v - } - }) - - if len(nodeIDs) > 0 { - m["clues_trace"] = strings.Join(nodeIDs, ",") - } - - if len(dn.agents) == 0 { - return m - } - - agentVals := map[string]map[string]any{} - - for _, agent := range dn.agents { - agentVals[agent.id] = agent.data.Map() - } - - m["agents"] = agentVals - - return m -} - -// Slice flattens the tree of dataNode.values into a Slice where all even -// indices contain the keys, and all odd indices contain values. Descendant -// nodes take priority over ancestors in cases of collision. -func (dn *dataNode) Slice() []any { - m := dn.Map() - s := make([]any, 0, 2*len(m)) - - for k, v := range m { - s = append(s, k, v) - } - - return s -} - -// --------------------------------------------------------------------------- -// initialization -// --------------------------------------------------------------------------- - -// init sets up persistent clients in the clues ecosystem such as otel. -// Initialization is NOT required. It is an optional step that end -// users can take if and when they want those clients running in their -// clues instance. -// -// Multiple initializations will no-op. -func (dn *dataNode) init( - ctx context.Context, - name string, - config OTELConfig, -) error { - if dn == nil { - return nil - } - - // if any of these already exist, initialization was previously called. - if dn.otel != nil { - return nil - } - - cli, err := newOTELClient(ctx, name, config) - - dn.otel = cli - - return Stack(err).OrNil() -} - -// --------------------------------------------------------------------------- -// comments -// --------------------------------------------------------------------------- - -type comment struct { - // the func name in which the comment was created. - Caller string - // the name of the file owning the caller. - File string - // the comment message itself. - Message string -} - -// shorthand for checking if an empty comment was generated. -func (c comment) isEmpty() bool { - return len(c.Message) == 0 -} - -// newComment formats the provided values, and grabs the caller and trace -// info according to the depth. Depth is a skip-caller count, and any func -// calling this one should provide either `1` (for itself) or `depth+1` (if -// it was already given a depth value). -func newComment( - depth int, - template string, - values ...any, -) comment { - caller := getCaller(depth + 1) - _, _, parentFileLine := getDirAndFile(depth + 1) - - return comment{ - Caller: caller, - File: parentFileLine, - Message: fmt.Sprintf(template, values...), - } -} - -// addComment creates a new dataNode with a comment but no other properties. -func (dn *dataNode) addComment( - depth int, - msg string, - vs ...any, -) *dataNode { - if len(msg) == 0 { - return dn - } - - spawn := dn.spawnDescendant() - spawn.id = makeNodeID() - spawn.comment = newComment(depth+1, msg, vs...) - - return spawn -} - -// comments allows us to put a stringer on a slice of comments. -type comments []comment - -// String formats the slice of comments as a stack, much like you'd see -// with an error stacktrace. Comments are listed top-to-bottom from first- -// to-last. -// -// The format for each comment in the stack is: -// -// - : -// -func (cs comments) String() string { - result := []string{} - - for _, c := range cs { - result = append(result, c.Caller+" - "+c.File) - result = append(result, "\t"+c.Message) - } - - return strings.Join(result, "\n") -} - -// Comments retrieves the full ancestor comment chain. -// The return value is ordered from the first added comment (closest to -// the root) to the most recent one (closest to the leaf). -func (dn *dataNode) Comments() comments { - result := comments{} - - if !dn.comment.isEmpty() { - result = append(result, dn.comment) - } - - for dn.parent != nil { - dn = dn.parent - if !dn.comment.isEmpty() { - result = append(result, dn.comment) - } - } - - slices.Reverse(result) - - return result -} - -// --------------------------------------------------------------------------- -// agents -// --------------------------------------------------------------------------- - -type agent struct { - // the name of the agent - id string - - // dataNode is used here instead of a basic value map so that - // we can extend the usage of agents in the future by allowing - // the full set of dataNode behavior. We'll need a builder for that, - // but we'll get there eventually. - data *dataNode -} - -// addAgent adds a new named agent to the dataNode. -func (dn *dataNode) addAgent(name string) *dataNode { - spawn := dn.spawnDescendant() - - if len(spawn.agents) == 0 { - spawn.agents = map[string]*agent{} - } - - spawn.agents[name] = &agent{ - id: name, - // no spawn here, this needs an isolated node - data: &dataNode{}, - } - - return spawn -} - -// --------------------------------------------------------------------------- -// ctx handling -// --------------------------------------------------------------------------- - -type cluesCtxKey string - -const defaultCtxKey cluesCtxKey = "default_clues_ctx_key" - -func ctxKey(namespace string) cluesCtxKey { - return cluesCtxKey(namespace) -} - -// nodeFromCtx pulls the datanode within a given namespace out of the context. -func nodeFromCtx(ctx context.Context) *dataNode { - dn := ctx.Value(defaultCtxKey) - - if dn == nil { - return &dataNode{} - } - - return dn.(*dataNode) -} - -// setNodeInCtx embeds the dataNode in the context, and returns the updated context. -func setNodeInCtx(ctx context.Context, dn *dataNode) context.Context { - return context.WithValue(ctx, defaultCtxKey, dn) -} - -// ------------------------------------------------------------ -// span handling -// ------------------------------------------------------------ - -// traceMapCarrierBase defines the structures that support -// otel traceMapCarrier behavior. A traceMapCarrier is used -// to pass and receive traces using message delivery headers -// and other metadata. -type traceMapCarrierBase interface { - map[string]string | http.Header -} - -// asTraceMapCarrier converts a traceMapCarrier interface to -// its propagation package implementation for that structure. -// ie: map becomes a MapCarrier, headers become HeaderCarriers. -func asTraceMapCarrier[C traceMapCarrierBase]( - carrier C, -) propagation.TextMapCarrier { - if carrier == nil { - return propagation.MapCarrier{} - } - - if mss, ok := any(carrier).(map[string]string); ok { - return propagation.MapCarrier(mss) - } - - if hh, ok := any(carrier).(http.Header); ok { - return propagation.HeaderCarrier(hh) - } - - return propagation.MapCarrier{} -} - -// injectTrace adds the current trace details to the provided -// carrier. If otel is not initialized, no-ops. -// -// The carrier data is mutated by this call. -func (dn *dataNode) injectTrace( - ctx context.Context, - carrier propagation.TextMapCarrier, -) { - if dn == nil { - return - } - - otel.GetTextMapPropagator().Inject(ctx, carrier) -} - -// receiveTrace extracts the current trace details from the -// carrier and adds them to the context. If otel is not -// initialized, no-ops. -// -// The carrier data is mutated by this call. -func (dn *dataNode) receiveTrace( - ctx context.Context, - carrier propagation.TextMapCarrier, -) context.Context { - if dn == nil { - return ctx - } - - return otel.GetTextMapPropagator().Extract(ctx, carrier) -} - -// addSpan adds a new otel span. If the otel client is nil, no-ops. -// Attrs can be added to the span with addSpanAttrs. This span will -// continue to be used for that purpose until replaced with another -// span, which will appear in a separate context (and thus a separate, -// dataNode). -func (dn *dataNode) addSpan( - ctx context.Context, - name string, -) (context.Context, *dataNode) { - if dn == nil || dn.otel == nil { - return ctx, dn - } - - ctx, span := dn.otel.tracer.Start(ctx, name) - - spawn := dn.spawnDescendant() - spawn.span = span - - return ctx, spawn -} - -// closeSpan closes the otel span and removes it span from the data node. -// If no span is present, no ops. -func (dn *dataNode) closeSpan(ctx context.Context) *dataNode { - if dn == nil || dn.span == nil { - return dn - } - - dn.span.End() - - spawn := dn.spawnDescendant() - spawn.span = nil - - return spawn -} - -// addSpanAttributes adds the values to the current span. If the span -// is nil (such as if otel wasn't initialized or no span has been generated), -// this call no-ops. -func (dn *dataNode) addSpanAttributes( - values map[string]any, -) { - if dn == nil || dn.span == nil { - return - } - - for k, v := range values { - dn.span.SetAttributes(attribute.String(k, stringify.Marshal(v, false))) - } -} - -// OTELLogger gets the otel logger instance from the otel client. -// Returns nil if otel wasn't initialized. -func (dn *dataNode) OTELLogger() otellog.Logger { - if dn == nil || dn.otel == nil { - return nil - } - - return dn.otel.logger -} - -// --------------------------------------------------------------------------- -// helpers -// --------------------------------------------------------------------------- - -// makeNodeID generates a random hash of 8 characters for use as a node ID. -func makeNodeID() string { - uns := uuid.NewString() - return uns[:4] + uns[len(uns)-4:] -} - -// getDirAndFile retrieves the file and line number of the caller. -// Depth is the skip-caller count. Clues funcs that call this one should -// provide either `1` (if they do not already have a depth value), or `depth+1` -// otherwise`. -// -// formats: -// dir `absolute/os/path/to/parent/folder` -// fileAndLine `:` -// parentAndFileAndLine `/:` -func getDirAndFile( - depth int, -) (dir, fileAndLine, parentAndFileAndLine string) { - _, file, line, _ := runtime.Caller(depth + 1) - dir, file = path.Split(file) - - fileLine := fmt.Sprintf("%s:%d", file, line) - parentFileLine := fileLine - - parent := path.Base(dir) - if len(parent) > 0 { - parentFileLine = path.Join(parent, fileLine) - } - - return dir, fileLine, parentFileLine -} - -// getCaller retrieves the func name of the caller. Depth is the skip-caller -// count. Clues funcs that call this one should provide either `1` (if they -// do not already have a depth value), or `depth+1` otherwise.` -func getCaller(depth int) string { - pc, _, _, ok := runtime.Caller(depth + 1) - if !ok { - return "" - } - - funcPath := runtime.FuncForPC(pc).Name() - - // the funcpath base looks something like this: - // prefix.funcName[...].foo.bar - // with the [...] only appearing for funcs with generics. - base := path.Base(funcPath) - - // so when we split it into parts by '.', we get - // [prefix, funcName[, ], foo, bar] - parts := strings.Split(base, ".") - - // in certain conditions we'll only get the funcName - // itself, without the other parts. In that case, we - // just need to strip the generic portion from the base. - if len(parts) < 2 { - return strings.ReplaceAll(base, "[...]", "") - } - - // in most cases we'll take the 1th index (the func - // name) and trim off the bracket that remains from - // splitting on a period. - return strings.TrimSuffix(parts[1], "[") -} - -// --------------------------------------------------------------------------- -// serialization -// --------------------------------------------------------------------------- - -// nodeCore contains the serializable set of data in a dataNode. -type nodeCore struct { - OTELServiceName string `json:"otelServiceName"` - // TODO: investigate if map[string]string is really the best structure here. - // maybe we can get away with a map[string]any, or a []byte slice? - Values map[string]string `json:"values"` - Comments []comment `json:"comments"` -} - -// Bytes serializes the dataNode to a slice of bytes. -// Only attributes and comments are maintained. All -// values are stringified in the process. -// -// Node hierarchy, clients (such as otel), agents, and -// hooks (such as labelCounter) are all sliced from the -// result. -func (dn *dataNode) Bytes() ([]byte, error) { - if dn == nil { - return []byte{}, nil - } - - var serviceName string - - if dn.otel != nil { - serviceName = dn.otel.serviceName - } - - core := nodeCore{ - OTELServiceName: serviceName, - Values: map[string]string{}, - Comments: dn.Comments(), - } - - for k, v := range dn.Map() { - core.Values[k] = stringify.Marshal(v, false) - } - - return json.Marshal(core) -} - -// FromBytes deserializes the bytes to a new dataNode. -// No clients, agents, or hooks are initialized in this process. -func FromBytes(bs []byte) (*dataNode, error) { - core := nodeCore{} - - err := json.Unmarshal(bs, &core) - if err != nil { - return nil, err - } - - node := dataNode{ - // FIXME: do something with the serialized commments. - // I'm punting on this for now because I want to figure - // out the best middle ground between avoiding a slice of - // comments in each node for serialization sake (they - // are supposed to be one-comment-per-node to use the tree - // for ordering instead of the parameter), and keeping - // the full comment history available. Probably just - // need to introduce a delimiter. - } - - if len(core.Values) > 0 { - node.values = map[string]any{} - } - - for k, v := range core.Values { - node.values[k] = v - } - - if len(core.OTELServiceName) > 0 { - node.otel = &otelClient{ - serviceName: core.OTELServiceName, - } - } - - return &node, nil -} diff --git a/err.go b/err.go deleted file mode 100644 index 465710f..0000000 --- a/err.go +++ /dev/null @@ -1,1078 +0,0 @@ -package clues - -import ( - "context" - "errors" - "fmt" - "io" - "reflect" - "strings" - - "github.com/alcionai/clues/internal/stringify" - "golang.org/x/exp/maps" -) - -// 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), -type Err struct { - // e holds the base error. - e error - - // stack is a chain of errors that this error is stacked onto. - // stacks may contain other stacks, forming a tree. - // Funcs that examine or flatten the tree will walk its structure - // in pre-order traversal. - stack []error - - // the name of the file where the caller func is found. - file string - // the name of the func where the error (or wrapper) was generated. - caller string - - // msg is the message for this error. - msg string - - // labels contains a map of the labels applied - // to this error. Can be used to identify error - // categorization without applying an error type. - labels map[string]struct{} - - // data is the record of contextual data produced, - // presumably, at the time the error is created or wrapped. - data *dataNode -} - -// --------------------------------------------------------------------------- -// constructors -// --------------------------------------------------------------------------- - -// newErr generates a new *Err from the parameters. -// traceDepth should always be `1` or `depth+1`. -func newErr( - e error, - msg string, - m map[string]any, - traceDepth int, -) *Err { - _, _, file := getDirAndFile(traceDepth + 1) - - return &Err{ - e: e, - file: file, - caller: getCaller(traceDepth + 1), - msg: msg, - // no ID needed for err data nodes - data: &dataNode{values: m}, - } -} - -// tryExtendErr checks if err is an *Err. If it is, it extends the Err -// with a child containing the provided parameters. If not, it creates -// a new Err containing the parameters. -// traceDepth should always be `1` or `depth+1`. -func tryExtendErr( - err error, - msg string, - m map[string]any, - traceDepth int, -) *Err { - if isNilErrIface(err) { - return nil - } - - e, ok := err.(*Err) - if !ok { - e = newErr(err, msg, m, traceDepth+1) - } - - return e -} - -// newStack creates a new *Err containing the provided stack of errors. -// traceDepth should always be `1` or `depth+1`. -func toStack( - e error, - stack []error, - traceDepth int, -) *Err { - _, _, file := getDirAndFile(traceDepth + 1) - - return &Err{ - e: e, - file: file, - caller: getCaller(traceDepth + 1), - stack: stack, - // no ID needed for err dataNodes - data: &dataNode{}, - } -} - -// makeStack creates a new *Err from the provided stack of errors. -// nil values are filtered out of the errs slice. If all errs are nil, -// returns nil. -// traceDepth should always be `1` or `depth+1`. -func makeStack( - traceDepth int, - errs ...error, -) *Err { - filtered := []error{} - for _, err := range errs { - if !isNilErrIface(err) { - filtered = append(filtered, err) - } - } - - switch len(filtered) { - case 0: - return nil - case 1: - return newErr(filtered[0], "", nil, traceDepth+1) - } - - return toStack(filtered[0], filtered[1:], traceDepth+1) -} - -// makeStackWrap creates a new *Err from the provided pair of sentinal -// and wrapped errors. If sentinel is nil, wraps the wrapped error. -// If wrapped is nil, wraps the sentinel error. If the message is empty, -// returns a stack(sentinel, wrapped). Otherwise, makes a stack headed -// by the sentinel error, and wraps the wrapped error in the message. -func makeStackWrap( - traceDepth int, - sentinel, wrapped error, - msg string, -) *Err { - if isNilErrIface(sentinel) && isNilErrIface(wrapped) { - return nil - } - - if len(msg) == 0 { - return makeStack(traceDepth+1, sentinel, wrapped) - } - - if isNilErrIface(sentinel) { - return newErr(wrapped, msg, nil, traceDepth+1) - } - - if isNilErrIface(wrapped) { - return newErr(sentinel, msg, nil, traceDepth+1) - } - - return makeStack( - 1, - sentinel, - newErr(wrapped, msg, nil, traceDepth+1)) -} - -// ------------------------------------------------------------ -// getters -// TODO: transform all this to comply with a standard interface -// ------------------------------------------------------------ - -// ancestors builds out the ancestor lineage of this -// particular error. This follows standard layout rules -// already established elsewhere: -// * the first entry is the oldest ancestor, the last is -// the current error. -// * Stacked errors get visited before wrapped errors. -// -// TODO: get other ancestor stack builders to work off of this -// func instead of spreading that handling everywhere. -func ancestors(err error) []error { - return stackAncestorsOntoSelf(err) -} - -// a recursive function, purely for building out ancestorStack. -func stackAncestorsOntoSelf(err error) []error { - if err == nil { - return []error{} - } - - errs := []error{} - - ce, ok := err.(*Err) - - if ok { - for _, e := range ce.stack { - errs = append(errs, stackAncestorsOntoSelf(e)...) - } - } - - unwrapped := Unwrap(err) - - if unwrapped != nil { - errs = append(errs, stackAncestorsOntoSelf(unwrapped)...) - } - - errs = append(errs, err) - - return errs -} - -// 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 InErr(err error) *dataNode { - if isNilErrIface(err) { - return &dataNode{} - } - - return &dataNode{values: inErr(err)} -} - -func inErr(err error) map[string]any { - if isNilErrIface(err) { - return map[string]any{} - } - - if e, ok := err.(*Err); ok { - return e.values() - } - - return inErr(Unwrap(err)) -} - -// ------------------------------------------------------------ -// getters - k:v store -// ------------------------------------------------------------ - -// 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 *Err) Values() *dataNode { - if isNilErrIface(err) { - return &dataNode{} - } - - return &dataNode{values: err.values()} -} - -func (err *Err) values() map[string]any { - if isNilErrIface(err) { - return map[string]any{} - } - - vals := map[string]any{} - maps.Copy(vals, err.data.Map()) - maps.Copy(vals, inErr(err.e)) - - for _, se := range err.stack { - maps.Copy(vals, inErr(se)) - } - - return vals -} - -// ------------------------------------------------------------ -// getters - labels -// ------------------------------------------------------------ - -func (err *Err) HasLabel(label string) bool { - if isNilErrIface(err) { - return false - } - - // Check all labels in the error and it's stack since the stack isn't - // traversed separately. If we don't check the stacked labels here we'll skip - // checking them completely. - if _, ok := err.Labels()[label]; ok { - return true - } - - return HasLabel(err.e, label) -} - -func HasLabel(err error, label string) bool { - if isNilErrIface(err) { - return false - } - - if e, ok := err.(*Err); ok { - return e.HasLabel(label) - } - - return HasLabel(Unwrap(err), label) -} - -func (err *Err) Label(labels ...string) *Err { - if isNilErrIface(err) { - return nil - } - - if len(err.labels) == 0 { - err.labels = map[string]struct{}{} - } - - for _, label := range labels { - err.labels[label] = struct{}{} - } - - return err -} - -func Label(err error, label string) *Err { - return tryExtendErr(err, "", nil, 1).Label(label) -} - -func (err *Err) Labels() map[string]struct{} { - if isNilErrIface(err) { - return map[string]struct{}{} - } - - labels := map[string]struct{}{} - - for _, se := range err.stack { - maps.Copy(labels, Labels(se)) - } - - if err.e != nil { - maps.Copy(labels, Labels(err.e)) - } - - maps.Copy(labels, err.labels) - - return labels -} - -func Labels(err error) map[string]struct{} { - for err != nil { - e, ok := err.(*Err) - if ok { - return e.Labels() - } - - err = Unwrap(err) - } - - return map[string]struct{}{} -} - -// ------------------------------------------------------------ -// getters - comments -// ------------------------------------------------------------ - -// Comments retrieves all comments in the error. -func (err *Err) Comments() comments { - return Comments(err) -} - -// Comments retrieves all comments in the error. -func Comments(err error) comments { - if isNilErrIface(err) { - return comments{} - } - - ancs := ancestors(err) - result := comments{} - - for _, ancestor := range ancs { - ce, ok := ancestor.(*Err) - if !ok { - continue - } - - result = append(result, ce.data.Comments()...) - } - - return result -} - -// ------------------------------------------------------------ -// eror interface compliance and stringers -// ------------------------------------------------------------ - -var _ error = &Err{} - -// Error allows Err to be used as a standard error interface. -func (err *Err) Error() string { - if isNilErrIface(err) { - return "" - } - - msg := []string{} - - if len(err.msg) > 0 { - msg = append(msg, err.msg) - } - - if err.e != nil { - msg = append(msg, err.e.Error()) - } - - for _, se := range err.stack { - msg = append(msg, se.Error()) - } - - return strings.Join(msg, ": ") -} - -// format is the fallback formatting of an error -func format(err error, s fmt.State, verb rune) { - if isNilErrIface(err) { - return - } - - f, ok := err.(fmt.Formatter) - if ok { - f.Format(s, verb) - } else { - write(s, verb, err.Error()) - } -} - -// For all formatting besides %+v, the error printout should closely -// mimic that of err.Error(). -func formatReg(err *Err, s fmt.State, verb rune) { - if isNilErrIface(err) { - return - } - - write(s, verb, err.msg) - - if len(err.msg) > 0 && err.e != nil { - io.WriteString(s, ": ") - } - - format(err.e, s, verb) - - if (len(err.msg) > 0 || err.e != nil) && len(err.stack) > 0 { - io.WriteString(s, ": ") - } - - for _, e := range err.stack { - format(e, s, verb) - } -} - -// in %+v formatting, we output errors FIFO (ie, read from the -// bottom of the stack first). -func formatPlusV(err *Err, s fmt.State, verb rune) { - if isNilErrIface(err) { - return - } - - for i := len(err.stack) - 1; i >= 0; i-- { - e := err.stack[i] - format(e, s, verb) - } - - if len(err.stack) > 0 && err.e != nil { - io.WriteString(s, "\n") - } - - format(err.e, s, verb) - - if (len(err.stack) > 0 || err.e != nil) && len(err.msg) > 0 { - io.WriteString(s, "\n") - } - - write(s, verb, err.msg) - - parts := []string{} - if len(err.caller) > 0 { - parts = append(parts, err.caller) - } - - if len(err.file) > 0 { - parts = append(parts, err.file) - } - - write(s, verb, "\n\t%s", strings.Join(parts, " - ")) -} - -// 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 *Err) Format(s fmt.State, verb rune) { - if isNilErrIface(err) { - return - } - - if verb == 'v' && s.Flag('+') { - formatPlusV(err, s, verb) - return - } - - formatReg(err, s, verb) -} - -func write(s fmt.State, verb rune, msgs ...string) { - if len(msgs) == 0 || len(msgs[0]) == 0 { - return - } - - switch verb { - case 'v': - if s.Flag('+') { - if len(msgs) == 1 { - io.WriteString(s, msgs[0]) - } else if len(msgs[1]) > 0 { - fmt.Fprintf(s, msgs[0], msgs[1]) - } - return - } - - fallthrough - - case 's': - io.WriteString(s, msgs[0]) - - case 'q': - fmt.Fprintf(s, "%q", msgs[0]) - } -} - -// ------------------------------------------------------------ -// common interface compliance -// ------------------------------------------------------------ - -// 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 *Err) Is(target error) bool { - if isNilErrIface(err) { - return false - } - - if errors.Is(err.e, target) { - return true - } - - for _, se := range err.stack { - if errors.Is(se, target) { - return true - } - } - - return false -} - -// 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 *Err) As(target any) bool { - if isNilErrIface(err) { - return false - } - - if errors.As(err.e, target) { - return true - } - - for _, se := range err.stack { - if errors.As(se, target) { - return true - } - } - - return false -} - -// 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 *Err) Unwrap() error { - if isNilErrIface(err) { - return nil - } - - return err.e -} - -// 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. -func Unwrap(err error) error { - if isNilErrIface(err) { - return nil - } - - if e, ok := err.(*Err); ok { - return e.e - } - - u, ok := err.(interface{ Unwrap() error }) - if !ok { - return nil - } - - ue := u.Unwrap() - return ue -} - -// ------------------------------------------------------------ -// constructors -// ------------------------------------------------------------ - -// 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 New(msg string) *Err { - return newErr(nil, msg, nil, 1) -} - -// 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 NewWC(ctx context.Context, msg string) *Err { - return newErr(nil, msg, nil, 1).WithClues(ctx) -} - -// 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 Wrap(err error, msg string) *Err { - if isNilErrIface(err) { - return nil - } - - return newErr(err, msg, nil, 1) -} - -// 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 WrapWC(ctx context.Context, err error, msg string) *Err { - if isNilErrIface(err) { - return nil - } - - return newErr(err, msg, nil, 1).WithClues(ctx) -} - -// 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 Stack(errs ...error) *Err { - return makeStack(1, errs...) -} - -// 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 StackWC(ctx context.Context, errs ...error) *Err { - err := makeStack(1, errs...) - - if isNilErrIface(err) { - return nil - } - - return err.WithClues(ctx) -} - -// 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 StackWrap(sentinel, wrapped error, msg string) *Err { - return makeStackWrap(1, sentinel, wrapped, msg) -} - -// 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 StackWrapWC( - ctx context.Context, - sentinel, wrapped error, - msg string, -) *Err { - err := makeStackWrap(1, sentinel, wrapped, msg) - - if isNilErrIface(err) { - return nil - } - - return err.WithClues(ctx) -} - -// 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 *Err) OrNil() error { - if isNilErrIface(err) { - return nil - } - - return err -} - -// ------------------------------------------------------------ -// builders - clues ctx -// ------------------------------------------------------------ - -// 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 *Err) WithClues(ctx context.Context) *Err { - if isNilErrIface(err) { - return nil - } - - dn := In(ctx) - e := err.WithMap(dn.Map()) - - return e -} - -// 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 WithClues(err error, ctx context.Context) *Err { - if isNilErrIface(err) { - return nil - } - - return WithMap(err, In(ctx).Map()) -} - -// ------------------------------------------------------------ -// builders - k:v store -// ------------------------------------------------------------ - -// With adds every pair of values as a key,value pair to -// the Err's data map. -func (err *Err) With(kvs ...any) *Err { - if isNilErrIface(err) { - return nil - } - - if len(kvs) > 0 { - err.data = err.data.addValues(stringify.Normalize(kvs...)) - } - - return 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 With(err error, kvs ...any) *Err { - return tryExtendErr(err, "", nil, 1).With(kvs...) -} - -// WithMap copies the map to the Err's data map. -func (err *Err) WithMap(m map[string]any) *Err { - if isNilErrIface(err) { - return nil - } - - if len(m) > 0 { - err.data = err.data.addValues(m) - } - - return 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 WithMap(err error, m map[string]any) *Err { - return tryExtendErr(err, "", m, 1).WithMap(m) -} - -// ------------------------------------------------------------ -// builders - tracing -// ------------------------------------------------------------ - -// SkipCaller skips 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 *Err) SkipCaller(depth int) *Err { - if isNilErrIface(err) { - return nil - } - - // needed both here and in withTrace() to - // correct for the extra call depth. - if depth < 0 { - depth = 0 - } - - _, _, err.file = getDirAndFile(depth + 1) - err.caller = getCaller(depth + 1) - - return err -} - -// SkipCaller skips 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 WithSkipCaller(err error, depth int) *Err { - if isNilErrIface(err) { - return nil - } - - // needed both here and in withTrace() to - // correct for the extra call depth. - if depth < 0 { - depth = 0 - } - - e, ok := err.(*Err) - if !ok { - return newErr(err, "", map[string]any{}, depth+1) - } - - return e.SkipCaller(depth + 1) -} - -// 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 *Err) NoTrace() *Err { - if isNilErrIface(err) { - return nil - } - - err.file = "" - err.caller = "" - - return err -} - -// ------------------------------------------------------------ -// builders - comments -// ------------------------------------------------------------ - -// 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 *Err) Comment(msg string, vs ...any) *Err { - if isNilErrIface(err) { - return nil - } - - return &Err{ - e: err, - // have to do a new dataNode here, or else comments will duplicate - data: &dataNode{comment: newComment(1, msg, vs...)}, - } -} - -// 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 Comment(err error, msg string, vs ...any) *Err { - if isNilErrIface(err) { - return nil - } - - return &Err{ - e: err, - // have to do a new dataNode here, or else comments will duplicate - data: &dataNode{comment: newComment(1, msg, vs...)}, - } -} - -// ------------------------------------------------------------ -// helpers -// ------------------------------------------------------------ - -// returns true if the error is nil, or if it is a non-nil interface -// containing a nil value. -func isNilErrIface(err error) bool { - if err == nil { - return true - } - - val := reflect.ValueOf(err) - - return ((val.Kind() == reflect.Pointer || val.Kind() == reflect.Interface) && val.IsNil()) -} diff --git a/internal/node/agents.go b/internal/node/agents.go new file mode 100644 index 0000000..a299634 --- /dev/null +++ b/internal/node/agents.go @@ -0,0 +1,33 @@ +package node + +// --------------------------------------------------------------------------- +// agents +// --------------------------------------------------------------------------- + +type Agent struct { + // the name of the agent + ID string + + // Data is used here instead of a basic value map so that + // we can extend the usage of agents in the future by allowing + // the full set of node behavior. We'll need a builder for that, + // but we'll get there eventually. + Data *Node +} + +// AddAgent adds a new named agent to the node. +func (dn *Node) AddAgent(name string) *Node { + spawn := dn.SpawnDescendant() + + if len(spawn.Agents) == 0 { + spawn.Agents = map[string]*Agent{} + } + + spawn.Agents[name] = &Agent{ + ID: name, + // no spawn here, this needs an isolated node + Data: &Node{}, + } + + return spawn +} diff --git a/internal/node/comments.go b/internal/node/comments.go new file mode 100644 index 0000000..a38a518 --- /dev/null +++ b/internal/node/comments.go @@ -0,0 +1,105 @@ +package node + +import ( + "fmt" + "slices" + "strings" +) + +// --------------------------------------------------------------------------- +// comments +// --------------------------------------------------------------------------- + +type Comment struct { + // the func name in which the comment was created. + Caller string + // the name of the file owning the caller. + File string + // the comment message itself. + Message string +} + +// shorthand for checking if an empty comment was generated. +func (c Comment) IsEmpty() bool { + return len(c.Message) == 0 +} + +// NewComment formats the provided values, and grabs the caller and trace +// info according to the depth. Depth is a skip-caller count, and any func +// calling this one should provide either `1` (for itself) or `depth+1` (if +// it was already given a depth value). +func NewComment( + depth int, + template string, + values ...any, +) Comment { + caller := GetCaller(depth + 1) + _, _, parentFileLine := GetDirAndFile(depth + 1) + + return Comment{ + Caller: caller, + File: parentFileLine, + Message: fmt.Sprintf(template, values...), + } +} + +// AddComment creates a new nodewith a comment but no other properties. +func (dn *Node) AddComment( + depth int, + msg string, + vs ...any, +) *Node { + if len(msg) == 0 { + return dn + } + + spawn := dn.SpawnDescendant() + spawn.ID = randomNodeID() + spawn.Comment = NewComment(depth+1, msg, vs...) + + return spawn +} + +// CommentHistory allows us to put a stringer on a slice of CommentHistory. +type CommentHistory []Comment + +// String formats the slice of comments as a stack, much like you'd see +// with an error stacktrace. Comments are listed top-to-bottom from first- +// to-last. +// +// The format for each comment in the stack is: +// +// - : +// +func (cs CommentHistory) String() string { + result := []string{} + + for _, c := range cs { + result = append(result, c.Caller+" - "+c.File) + result = append(result, "\t"+c.Message) + } + + return strings.Join(result, "\n") +} + +// Comments retrieves the full ancestor comment chain. +// The return value is ordered from the first added comment (closest to +// the root) to the most recent one (closest to the leaf). +func (dn *Node) Comments() CommentHistory { + result := CommentHistory{} + + if !dn.Comment.IsEmpty() { + result = append(result, dn.Comment) + } + + for dn.Parent != nil { + dn = dn.Parent + if !dn.Comment.IsEmpty() { + result = append(result, dn.Comment) + } + } + + slices.Reverse(result) + + return result +} diff --git a/internal/node/node.go b/internal/node/node.go new file mode 100644 index 0000000..59e6ccb --- /dev/null +++ b/internal/node/node.go @@ -0,0 +1,351 @@ +package node + +import ( + "context" + "encoding/json" + "strings" + + "github.com/alcionai/clues/internal/stringify" + "github.com/google/uuid" + "go.opentelemetry.io/otel/trace" + "golang.org/x/exp/maps" +) + +// --------------------------------------------------------------------------- +// data nodes +// --------------------------------------------------------------------------- + +// Node contains the data tracked by both clues in contexts and in errors. +// +// These nodes compose a tree, such that nodes can walk their ancestry path from +// leaf (the current node) to root (the highest ancestor), but not from root to +// child. This allows clues to establish sets of common ancestor data with unique +// branches for individual descendants, making the addition of new data inherently +// theadsafe. +// +// For collisions during aggregation, distance from the root denotes priority, +// with the root having the lowest priority. IE: if a child overwrites a key +// declared by an ancestor, the child's entry takes priority. +type Node struct { + Parent *Node + + // OTEL contains the client instance for the in memory OTEL runtime. It is only + // present if the end user calls the clues initialization step. + OTEL *OTELClient + + // Span is the current otel Span. + // Spans are kept separately from the otelClient because we want the client to + // maintain a consistent reference to otel initialization, while the Span can + // get replaced at arbitrary points. + Span trace.Span + + // ids are optional and are used primarily as tracing markers. + // if empty, the trace for that node will get skipped when building the + // full trace along the node's ancestry path in the tree. + ID string + + // Values are they arbitrary key:value pairs that appear in clues when callers + // use the Add(ctx, k, v) or err.With(k, v) adders. Each key-value pair added + // to the node is used to produce the final set of Values() in the node, + // with lower nodes in the tree taking priority over higher nodes for any + // collision resolution. + Values map[string]any + + // each node can hold a single commment. The history of comments produced + // by the ancestry path through the tree will get concatenated from oldest + // ancestor to the current node to produce the Comment history. + Comment Comment + + // Agents act as proxy node that can relay specific, intentional data + // additions. They're namespaced so that additions to the Agents don't accidentally + // clobber other values in the node. This also allows Agents to protect + // variations of data from each other, in case users need to compare differences + // on the same keys. That's not the goal for Agents, exactly, but it is capable. + Agents map[string]*Agent +} + +// SpawnDescendant generates a new node that is a descendant of the current +// node. A descendant maintains a pointer to its parent, and carries any genetic +// necessities (ie, copies of fields) that must be present for continued functionality. +func (dn *Node) SpawnDescendant() *Node { + agents := maps.Clone(dn.Agents) + + if agents == nil && dn.Agents != nil { + agents = map[string]*Agent{} + } + + return &Node{ + Parent: dn, + OTEL: dn.OTEL, + Span: dn.Span, + Agents: agents, + } +} + +// --------------------------------------------------------------------------- +// setters +// --------------------------------------------------------------------------- + +// AddValues adds all entries in the map to the node's values. +// automatically propagates values onto the current span. +func (dn *Node) AddValues(m map[string]any) *Node { + if m == nil { + m = map[string]any{} + } + + spawn := dn.SpawnDescendant() + spawn.SetValues(m) + spawn.AddSpanAttributes(m) + + return spawn +} + +// SetValues is generally a helper called by addValues. In +// certain corner cases (like agents) it may get called directly. +func (dn *Node) SetValues(m map[string]any) { + if len(m) == 0 { + return + } + + if len(dn.Values) == 0 { + dn.Values = map[string]any{} + } + + maps.Copy(dn.Values, m) +} + +// AppendToTree adds a new leaf with the provided name. +func (dn *Node) AppendToTree(name string) *Node { + if name == "" { + name = randomNodeID() + } + + spawn := dn.SpawnDescendant() + spawn.ID = name + + return spawn +} + +// --------------------------------------------------------------------------- +// getters +// --------------------------------------------------------------------------- + +// RunLineage runs the fn on every valueNode in the ancestry tree, +// starting at the root and ending at the node. +func (dn *Node) RunLineage(fn func(id string, vs map[string]any)) { + if dn == nil { + return + } + + if dn.Parent != nil { + dn.Parent.RunLineage(fn) + } + + fn(dn.ID, dn.Values) +} + +// Map flattens the tree of node.values into a map. Descendant nodes +// take priority over ancestors in cases of collision. +func (dn *Node) Map() map[string]any { + var ( + m = map[string]any{} + nodeIDs = []string{} + ) + + dn.RunLineage(func(id string, vs map[string]any) { + if len(id) > 0 { + nodeIDs = append(nodeIDs, id) + } + + for k, v := range vs { + m[k] = v + } + }) + + if len(nodeIDs) > 0 { + m["clues_trace"] = strings.Join(nodeIDs, ",") + } + + if len(dn.Agents) == 0 { + return m + } + + agentVals := map[string]map[string]any{} + + for _, agent := range dn.Agents { + agentVals[agent.ID] = agent.Data.Map() + } + + m["agents"] = agentVals + + return m +} + +// Slice flattens the tree of node.values into a Slice where all even +// indices contain the keys, and all odd indices contain values. Descendant +// nodes take priority over ancestors in cases of collision. +func (dn *Node) Slice() []any { + m := dn.Map() + s := make([]any, 0, 2*len(m)) + + for k, v := range m { + s = append(s, k, v) + } + + return s +} + +// --------------------------------------------------------------------------- +// initialization +// --------------------------------------------------------------------------- + +// Init sets up persistent clients in the clues ecosystem such as otel. +// Initialization is NOT required. It is an optional step that end +// users can take if and when they want those clients running in their +// clues instance. +// +// Multiple initializations will no-op. +func (dn *Node) InitOTEL( + ctx context.Context, + name string, + config OTELConfig, +) error { + if dn == nil { + return nil + } + + // if any of these already exist, initialization was previously called. + if dn.OTEL != nil { + return nil + } + + cli, err := NewOTELClient(ctx, name, config) + + dn.OTEL = cli + + if err != nil { + return err + } + + return nil +} + +// --------------------------------------------------------------------------- +// ctx handling +// --------------------------------------------------------------------------- + +type CluesCtxKey string + +const defaultCtxKey CluesCtxKey = "default_clues_ctx_key" + +func CtxKey(namespace string) CluesCtxKey { + return CluesCtxKey(namespace) +} + +// FromCtx pulls the node within a given namespace out of the context. +func FromCtx(ctx context.Context) *Node { + dn := ctx.Value(defaultCtxKey) + + if dn == nil { + return &Node{} + } + + return dn.(*Node) +} + +// EmbedInCtx adds the node in the context, and returns the updated context. +func EmbedInCtx(ctx context.Context, dn *Node) context.Context { + return context.WithValue(ctx, defaultCtxKey, dn) +} + +// --------------------------------------------------------------------------- +// helpers +// --------------------------------------------------------------------------- + +// randomNodeID generates a random hash of 8 characters for use as a node ID. +func randomNodeID() string { + uns := uuid.NewString() + return uns[:4] + uns[len(uns)-4:] +} + +// --------------------------------------------------------------------------- +// serialization +// --------------------------------------------------------------------------- + +// nodeCore contains the serializable set of data in a Node. +type nodeCore struct { + OTELServiceName string `json:"otelServiceName"` + // TODO: investigate if map[string]string is really the best structure here. + // maybe we can get away with a map[string]any, or a []byte slice? + Values map[string]string `json:"values"` + Comments []Comment `json:"comments"` +} + +// Bytes serializes the Node to a slice of bytes. +// Only attributes and comments are maintained. All +// values are stringified in the process. +// +// Node hierarchy, clients (such as otel), agents, and +// hooks (such as labelCounter) are all sliced from the +// result. +func (dn *Node) Bytes() ([]byte, error) { + if dn == nil { + return []byte{}, nil + } + + var serviceName string + + if dn.OTEL != nil { + serviceName = dn.OTEL.serviceName + } + + core := nodeCore{ + OTELServiceName: serviceName, + Values: map[string]string{}, + Comments: dn.Comments(), + } + + for k, v := range dn.Map() { + core.Values[k] = stringify.Marshal(v, false) + } + + return json.Marshal(core) +} + +// FromBytes deserializes the bytes to a new Node. +// No clients, agents, or hooks are initialized in this process. +func FromBytes(bs []byte) (*Node, error) { + core := nodeCore{} + + err := json.Unmarshal(bs, &core) + if err != nil { + return nil, err + } + + node := Node{ + // FIXME: do something with the serialized commments. + // I'm punting on this for now because I want to figure + // out the best middle ground between avoiding a slice of + // comments in each node for serialization sake (they + // are supposed to be one-comment-per-node to use the tree + // for ordering instead of the parameter), and keeping + // the full comment history available. Probably just + // need to introduce a delimiter. + } + + if len(core.Values) > 0 { + node.Values = map[string]any{} + } + + for k, v := range core.Values { + node.Values[k] = v + } + + if len(core.OTELServiceName) > 0 { + node.OTEL = &OTELClient{ + serviceName: core.OTELServiceName, + } + } + + return &node, nil +} diff --git a/datanode_test.go b/internal/node/node_test.go similarity index 79% rename from datanode_test.go rename to internal/node/node_test.go index 25f4cec..ccfe5f6 100644 --- a/datanode_test.go +++ b/internal/node/node_test.go @@ -1,4 +1,4 @@ -package clues +package node import ( "context" @@ -12,15 +12,15 @@ import ( // tests // --------------------------------------------------------------------------- -func TestDataNode_Init(t *testing.T) { +func TestNode_Init(t *testing.T) { table := []struct { name string - node *dataNode + node *Node ctx context.Context }{ { name: "nil ctx", - node: &dataNode{}, + node: &Node{}, ctx: nil, }, { @@ -30,14 +30,14 @@ func TestDataNode_Init(t *testing.T) { }, { name: "context.Context", - node: &dataNode{}, + node: &Node{}, ctx: context.Background(), }, } for _, test := range table { t.Run(test.name, func(t *testing.T) { - err := test.node.init(test.ctx, test.name, OTELConfig{}) + err := test.node.InitOTEL(test.ctx, test.name, OTELConfig{}) require.NoError(t, err) }) } @@ -46,14 +46,14 @@ func TestDataNode_Init(t *testing.T) { func TestBytes(t *testing.T) { table := []struct { name string - node func() *dataNode + node func() *Node expectSerialized []byte - expectDeserialized *dataNode + expectDeserialized *Node expectDeserializeErr require.ErrorAssertionFunc }{ { name: "nil", - node: func() *dataNode { + node: func() *Node { return nil }, expectSerialized: []byte{}, @@ -62,25 +62,25 @@ func TestBytes(t *testing.T) { }, { name: "empty", - node: func() *dataNode { - return &dataNode{} + node: func() *Node { + return &Node{} }, expectSerialized: []byte(`{"otelServiceName":"","values":{},"comments":[]}`), - expectDeserialized: &dataNode{}, + expectDeserialized: &Node{}, expectDeserializeErr: require.NoError, }, { name: "with values", - node: func() *dataNode { - return &dataNode{ - otel: &otelClient{ + node: func() *Node { + return &Node{ + OTEL: &OTELClient{ serviceName: "serviceName", }, - values: map[string]any{ + Values: map[string]any{ "fisher": "flannigan", "fitzbog": nil, }, - comment: comment{ + Comment: Comment{ Caller: "i am caller", File: "i am file", Message: "i am message", @@ -90,11 +90,11 @@ func TestBytes(t *testing.T) { expectSerialized: []byte(`{"otelServiceName":"serviceName",` + `"values":{"fisher":"flannigan","fitzbog":""},` + `"comments":[{"Caller":"i am caller","File":"i am file","Message":"i am message"}]}`), - expectDeserialized: &dataNode{ - otel: &otelClient{ + expectDeserialized: &Node{ + OTEL: &OTELClient{ serviceName: "serviceName", }, - values: map[string]any{ + Values: map[string]any{ "fisher": "flannigan", "fitzbog": "", }, diff --git a/internal/node/otel.go b/internal/node/otel.go new file mode 100644 index 0000000..66a2215 --- /dev/null +++ b/internal/node/otel.go @@ -0,0 +1,322 @@ +package node + +import ( + "context" + "fmt" + "net/http" + + "github.com/alcionai/clues/internal/stringify" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" + otellog "go.opentelemetry.io/otel/log" + "go.opentelemetry.io/otel/log/global" + "go.opentelemetry.io/otel/propagation" + "go.opentelemetry.io/otel/sdk/resource" + sdkTrace "go.opentelemetry.io/otel/sdk/trace" + semconv "go.opentelemetry.io/otel/semconv/v1.26.0" + "go.opentelemetry.io/otel/trace" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" +) + +// ------------------------------------------------------------ +// client +// ------------------------------------------------------------ + +type OTELClient struct { + grpcConn *grpc.ClientConn + traceProvider *sdkTrace.TracerProvider + tracer trace.Tracer + logger otellog.Logger + serviceName string +} + +func (cli *OTELClient) Close(ctx context.Context) error { + if cli == nil { + return nil + } + + if cli.traceProvider != nil { + err := cli.traceProvider.ForceFlush(ctx) + if err != nil { + fmt.Println("forcing trace provider flush:", err) + } + + err = cli.traceProvider.Shutdown(ctx) + if err != nil { + return fmt.Errorf("shutting down otel trace provider: %w", err) + } + } + + if cli.grpcConn != nil { + err := cli.grpcConn.Close() + if err != nil { + return fmt.Errorf("closing grpc connection: %w", err) + } + } + + return nil +} + +// ------------------------------------------------------------ +// config +// ------------------------------------------------------------ + +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 +} + +// ------------------------------------------------------------ +// initializers +// ------------------------------------------------------------ + +// NewOTELClient bootstraps the OpenTelemetry pipeline to run against a +// local server instance. If it does not return an error, make sure +// to call the client.Close() method for proper cleanup. +// The service name is used to match traces across backends. +func NewOTELClient( + ctx context.Context, + serviceName string, + config OTELConfig, +) (*OTELClient, error) { + // -- Resource + srvResource, err := resource.New(ctx, resource.WithAttributes( + semconv.ServiceNameKey.String(serviceName))) + if err != nil { + return nil, fmt.Errorf("creating otel resource: %w", err) + } + + // -- Exporter + + conn, err := grpc.NewClient( + config.GRPCEndpoint, + // Note the use of insecure transport here. TLS is recommended in production. + grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + return nil, fmt.Errorf("creating new grpc connection: %w", err) + } + + exporter, err := otlptracegrpc.New(ctx, otlptracegrpc.WithGRPCConn(conn)) + if err != nil { + return nil, fmt.Errorf("creating a trace exporter: %w", err) + } + + // -- TracerProvider + + // Register the trace exporter with a TracerProvider, using a batch + // span processor to aggregate spans before export. + batchSpanProcessor := sdkTrace.NewBatchSpanProcessor(exporter) + + tracerProvider := sdkTrace.NewTracerProvider( + sdkTrace.WithResource(srvResource), + sdkTrace.WithSampler(sdkTrace.AlwaysSample()), + sdkTrace.WithSpanProcessor(batchSpanProcessor), + sdkTrace.WithRawSpanLimits(sdkTrace.SpanLimits{ + AttributeValueLengthLimit: -1, + AttributeCountLimit: -1, + AttributePerEventCountLimit: -1, + AttributePerLinkCountLimit: -1, + EventCountLimit: -1, + LinkCountLimit: -1, + })) + + // set global propagator to traceContext (the default is no-op). + otel.SetTextMapPropagator(propagation.TraceContext{}) + otel.SetTracerProvider(tracerProvider) + + // -- Logger + + // generate a logger provider + logProvider := global.GetLoggerProvider() + + // -- Client + + client := OTELClient{ + grpcConn: conn, + traceProvider: tracerProvider, + tracer: tracerProvider.Tracer(serviceName + "/tracer"), + logger: logProvider.Logger(serviceName), + } + + // Shutdown will flush any remaining spans and shut down the exporter. + return &client, nil +} + +// ------------------------------------------------------------ +// annotations. basically otel's version of With() +// Not currently used; we're just mashing everything in as a +// string right now, same as clues does. +// ------------------------------------------------------------ + +type Annotation struct { + kind string + k string + v any +} + +func NewAttribute(k string, v any) Annotation { + return Annotation{ + kind: "attribute", + k: k, + v: v, + } +} + +func (a Annotation) IsAttribute() bool { + return a.kind == "attribute" +} + +func (a Annotation) KV() otellog.KeyValue { + if a.kind != "attribute" { + return otellog.KeyValue{} + } + + // FIXME: needs extensive type support + switch a.v.(type) { + case int: + return otellog.Int(a.k, a.v.(int)) + case int64: + return otellog.Int64(a.k, a.v.(int64)) + case string: + return otellog.String(a.k, a.v.(string)) + case bool: + return otellog.Bool(a.k, a.v.(bool)) + default: // everything else gets stringified + return otellog.String(a.k, stringify.Marshal(a.v, false)) + } +} + +type Annotationer interface { + IsAttribute() bool + KV() attribute.KeyValue +} + +// ------------------------------------------------------------ +// span handling +// ------------------------------------------------------------ + +// TraceMapCarrierBase defines the structures that support +// otel TraceMapCarrier behavior. A traceMapCarrier is used +// to pass and receive traces using message delivery headers +// and other metadata. +type TraceMapCarrierBase interface { + map[string]string | http.Header +} + +// AsTraceMapCarrier converts a traceMapCarrier interface to +// its propagation package implementation for that structure. +// ie: map becomes a MapCarrier, headers become HeaderCarriers. +func AsTraceMapCarrier[C TraceMapCarrierBase]( + carrier C, +) propagation.TextMapCarrier { + if carrier == nil { + return propagation.MapCarrier{} + } + + if mss, ok := any(carrier).(map[string]string); ok { + return propagation.MapCarrier(mss) + } + + if hh, ok := any(carrier).(http.Header); ok { + return propagation.HeaderCarrier(hh) + } + + return propagation.MapCarrier{} +} + +// injectTrace adds the current trace details to the provided +// carrier. If otel is not initialized, no-ops. +// +// The carrier data is mutated by this call. +func (dn *Node) InjectTrace( + ctx context.Context, + carrier propagation.TextMapCarrier, +) { + if dn == nil { + return + } + + otel.GetTextMapPropagator().Inject(ctx, carrier) +} + +// receiveTrace extracts the current trace details from the +// carrier and adds them to the context. If otel is not +// initialized, no-ops. +// +// The carrier data is mutated by this call. +func (dn *Node) ReceiveTrace( + ctx context.Context, + carrier propagation.TextMapCarrier, +) context.Context { + if dn == nil { + return ctx + } + + return otel.GetTextMapPropagator().Extract(ctx, carrier) +} + +// AddSpan adds a new otel span. If the otel client is nil, no-ops. +// Attrs can be added to the span with addSpanAttrs. This span will +// continue to be used for that purpose until replaced with another +// span, which will appear in a separate context (and thus a separate, +// node). +func (dn *Node) AddSpan( + ctx context.Context, + name string, +) (context.Context, *Node) { + if dn == nil || dn.OTEL == nil { + return ctx, dn + } + + ctx, span := dn.OTEL.tracer.Start(ctx, name) + + spawn := dn.SpawnDescendant() + spawn.Span = span + + return ctx, spawn +} + +// CloseSpan closes the otel span and removes it span from the data node. +// If no span is present, no ops. +func (dn *Node) CloseSpan(ctx context.Context) *Node { + if dn == nil || dn.Span == nil { + return dn + } + + dn.Span.End() + + spawn := dn.SpawnDescendant() + spawn.Span = nil + + return spawn +} + +// AddSpanAttributes adds the values to the current span. If the span +// is nil (such as if otel wasn't initialized or no span has been generated), +// this call no-ops. +func (dn *Node) AddSpanAttributes( + values map[string]any, +) { + if dn == nil || dn.Span == nil { + return + } + + for k, v := range values { + dn.Span.SetAttributes(attribute.String(k, stringify.Marshal(v, false))) + } +} + +// OTELLogger gets the otel logger instance from the otel client. +// Returns nil if otel wasn't initialized. +func (dn *Node) OTELLogger() otellog.Logger { + if dn == nil || dn.OTEL == nil { + return nil + } + + return dn.OTEL.logger +} diff --git a/internal/node/stacktrace.go b/internal/node/stacktrace.go new file mode 100644 index 0000000..21e81f3 --- /dev/null +++ b/internal/node/stacktrace.go @@ -0,0 +1,67 @@ +package node + +import ( + "fmt" + "path" + "runtime" + "strings" +) + +// GetDirAndFile retrieves the file and line number of the caller. +// Depth is the skip-caller count. Clues funcs that call this one should +// provide either `1` (if they do not already have a depth value), or `depth+1` +// otherwise`. +// +// formats: +// dir `absolute/os/path/to/parent/folder` +// fileAndLine `:` +// parentAndFileAndLine `/:` +func GetDirAndFile( + depth int, +) (dir, fileAndLine, parentAndFileAndLine string) { + _, file, line, _ := runtime.Caller(depth + 1) + dir, file = path.Split(file) + + fileLine := fmt.Sprintf("%s:%d", file, line) + parentFileLine := fileLine + + parent := path.Base(dir) + if len(parent) > 0 { + parentFileLine = path.Join(parent, fileLine) + } + + return dir, fileLine, parentFileLine +} + +// GetCaller retrieves the func name of the caller. Depth is the skip-caller +// count. Clues funcs that call this one should provide either `1` (if they +// do not already have a depth value), or `depth+1` otherwise.` +func GetCaller(depth int) string { + pc, _, _, ok := runtime.Caller(depth + 1) + if !ok { + return "" + } + + funcPath := runtime.FuncForPC(pc).Name() + + // the funcpath base looks something like this: + // prefix.funcName[...].foo.bar + // with the [...] only appearing for funcs with generics. + base := path.Base(funcPath) + + // so when we split it into parts by '.', we get + // [prefix, funcName[, ], foo, bar] + parts := strings.Split(base, ".") + + // in certain conditions we'll only get the funcName + // itself, without the other parts. In that case, we + // just need to strip the generic portion from the base. + if len(parts) < 2 { + return strings.ReplaceAll(base, "[...]", "") + } + + // in most cases we'll take the 1th index (the func + // name) and trim off the bracket that remains from + // splitting on a period. + return strings.TrimSuffix(parts[1], "[") +} diff --git a/internal/tester/tester.go b/internal/tester/tester.go new file mode 100644 index 0000000..844d0c1 --- /dev/null +++ b/internal/tester/tester.go @@ -0,0 +1,180 @@ +package tester + +import ( + "context" + "fmt" + "slices" + "testing" + + "github.com/alcionai/clues/internal/node" +) + +func MapEquals( + t *testing.T, + ctx context.Context, + expect MSA, + expectCluesTrace bool, +) { + MustEquals( + t, + expect, + node.FromCtx(ctx).Map(), + expectCluesTrace) +} + +func MustEquals[K comparable, V any]( + t *testing.T, + expect, got map[K]V, + hasCluesTrace bool, +) { + e, g := ToMSS(expect), ToMSS(got) + + if len(g) > 0 { + if _, ok := g["clues_trace"]; hasCluesTrace && !ok { + t.Errorf( + "expected map to contain key [clues_trace]\ngot: %+v", + g) + } + delete(g, "clues_trace") + } + + if len(g) > 0 { + if _, ok := g["clues_trace"]; hasCluesTrace && !ok { + t.Errorf( + "expected map to contain key [clues_trace]\ngot: %+v", + g) + } + delete(g, "clues_trace") + } + + if len(e) != len(g) { + t.Errorf( + "expected map of len [%d], received len [%d]\n%s", + len(e), len(g), expectedReceived(expect, got), + ) + } + + for k, v := range e { + if g[k] != v { + t.Errorf( + "expected map to contain key:value [%s: %s]\n%s", + k, v, expectedReceived(expect, got), + ) + } + } + + for k, v := range g { + if e[k] != v { + t.Errorf( + "map contains unexpected key:value [%s: %s]\n%s", + k, v, expectedReceived(expect, got), + ) + } + } +} + +func expectedReceived[K comparable, V any](e, r map[K]V) string { + return fmt.Sprintf( + "expected: %#v\nreceived: %#v\n\n", + e, r) +} + +type MSS map[string]string + +func ToMSS[K comparable, V any](m map[K]V) MSS { + r := MSS{} + + for k, v := range m { + ks := fmt.Sprintf("%v", k) + vs := fmt.Sprintf("%v", v) + r[ks] = vs + } + + return r +} + +type MSA map[string]any + +func ToMSA[T any](m map[string]T) MSA { + to := make(MSA, len(m)) + for k, v := range m { + to[k] = v + } + + return to +} + +type SA []any + +func (s SA) stringWith(other []any) string { + return fmt.Sprintf( + "\nexpected: %+v\nreceived: %+v\n", + s, other, + ) +} + +func (s SA) equals(t *testing.T, other []any) { + idx := slices.Index(other, "clues_trace") + if idx >= 0 { + other = append(other[:idx], other[idx+2:]...) + } + + if len(s) != len(other) { + t.Errorf( + "expected slice of len [%d], received len [%d]\n%s", + len(s), len(other), s.stringWith(other), + ) + } + + for _, v := range s { + var found bool + for _, o := range other { + if v == o { + found = true + break + } + } + if !found { + t.Errorf("expected slice to contain [%v]\n%s", v, s.stringWith(other)) + } + } + + for _, o := range other { + var found bool + for _, v := range s { + if v == o { + found = true + break + } + } + if !found { + t.Errorf("did not expect slice to contain [%v]\n%s", o, s.stringWith(other)) + } + } +} + +func AssertEq( + t *testing.T, + ctx context.Context, + ns string, + eM, eMns MSA, + eS, eSns SA, +) { + vs := node.FromCtx(ctx) + MustEquals(t, eM, vs.Map(), false) + eS.equals(t, vs.Slice()) +} + +func AssertMSA( + t *testing.T, + ctx context.Context, + ns string, + eM, eMns MSA, + eS, eSns SA, +) { + vs := node.FromCtx(ctx) + MustEquals(t, eM, ToMSA(vs.Map()), false) + eS.equals(t, vs.Slice()) +} + +type StubCtx struct{} diff --git a/otel.go b/otel.go index b92e752..9265fda 100644 --- a/otel.go +++ b/otel.go @@ -1,190 +1,29 @@ package clues import ( - "context" - "fmt" + "errors" - "github.com/alcionai/clues/internal/stringify" - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" - otellog "go.opentelemetry.io/otel/log" - "go.opentelemetry.io/otel/log/global" - "go.opentelemetry.io/otel/propagation" - "go.opentelemetry.io/otel/sdk/resource" - sdkTrace "go.opentelemetry.io/otel/sdk/trace" - semconv "go.opentelemetry.io/otel/semconv/v1.26.0" - "go.opentelemetry.io/otel/trace" - "google.golang.org/grpc" - "google.golang.org/grpc/credentials/insecure" + "github.com/alcionai/clues/internal/node" ) -type otelClient struct { - serviceName string +var ErrMissingOtelGRPCEndpoint = errors.New("missing otel grpc endpoint") - grpcConn *grpc.ClientConn - traceProvider *sdkTrace.TracerProvider - tracer trace.Tracer - logger otellog.Logger -} - -func (cli *otelClient) close(ctx context.Context) error { - if cli == nil { - return nil - } - - if cli.traceProvider != nil { - err := cli.traceProvider.ForceFlush(ctx) - if err != nil { - fmt.Println("forcing trace provider flush:", err) - } - - err = cli.traceProvider.Shutdown(ctx) - if err != nil { - return WrapWC(ctx, err, "shutting down otel trace provider") - } - } - - if cli.grpcConn != nil { - err := cli.grpcConn.Close() - if err != nil { - return WrapWC(ctx, err, "closing grpc connection") - } - } - - return nil -} - -// ------------------------------------------------------------ -// initializers -// ------------------------------------------------------------ +const ( + DefaultOTELGRPCEndpoint = "localhost:4317" +) 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 + // ex: opentelemetry-collector.monitoring.svc.cluster.local:4317 GRPCEndpoint string } -// newOTELClient bootstraps the OpenTelemetry pipeline to run against a -// local server instance. If it does not return an error, make sure -// to call the client.Close() method for proper cleanup. -// The service name is used to match traces across backends. -func newOTELClient( - ctx context.Context, - serviceName string, - config OTELConfig, -) (*otelClient, error) { - // -- Resource - srvResource, err := resource.New(ctx, resource.WithAttributes( - semconv.ServiceNameKey.String(serviceName))) - if err != nil { - return nil, WrapWC(ctx, err, "creating otel resource") - } - - // -- Exporter - - conn, err := grpc.NewClient( - config.GRPCEndpoint, - // Note the use of insecure transport here. TLS is recommended in production. - grpc.WithTransportCredentials(insecure.NewCredentials())) - if err != nil { - return nil, WrapWC(ctx, err, "creating new grpc connection") - } - - exporter, err := otlptracegrpc.New(ctx, otlptracegrpc.WithGRPCConn(conn)) - if err != nil { - return nil, WrapWC(ctx, err, "creating a trace exporter") - } - - // -- TracerProvider - - // Register the trace exporter with a TracerProvider, using a batch - // span processor to aggregate spans before export. - batchSpanProcessor := sdkTrace.NewBatchSpanProcessor(exporter) - - tracerProvider := sdkTrace.NewTracerProvider( - sdkTrace.WithResource(srvResource), - sdkTrace.WithSampler(sdkTrace.AlwaysSample()), - sdkTrace.WithSpanProcessor(batchSpanProcessor), - sdkTrace.WithRawSpanLimits(sdkTrace.SpanLimits{ - AttributeValueLengthLimit: -1, - AttributeCountLimit: -1, - AttributePerEventCountLimit: -1, - AttributePerLinkCountLimit: -1, - EventCountLimit: -1, - LinkCountLimit: -1, - })) - - // set global propagator to traceContext (the default is no-op). - otel.SetTextMapPropagator(propagation.TraceContext{}) - otel.SetTracerProvider(tracerProvider) - - // -- Logger - - // generate a logger provider - logProvider := global.GetLoggerProvider() - - // -- Client - - client := otelClient{ - serviceName: serviceName, - grpcConn: conn, - traceProvider: tracerProvider, - tracer: tracerProvider.Tracer(serviceName + "/tracer"), - logger: logProvider.Logger(serviceName), - } - - // Shutdown will flush any remaining spans and shut down the exporter. - return &client, nil -} - -// ------------------------------------------------------------ -// annotations. basically otel's version of With() -// Not currently used; we're just mashing everything in as a -// string right now, same as clues does. -// ------------------------------------------------------------ - -type annotation struct { - kind string - k string - v any -} - -func NewAttribute(k string, v any) annotation { - return annotation{ - kind: "attribute", - k: k, - v: v, - } -} - -func (a annotation) IsAttribute() bool { - return a.kind == "attribute" -} - -func (a annotation) KV() otellog.KeyValue { - if a.kind != "attribute" { - return otellog.KeyValue{} - } - - // FIXME: needs extensive type support - switch a.v.(type) { - case int: - return otellog.Int(a.k, a.v.(int)) - case int64: - return otellog.Int64(a.k, a.v.(int64)) - case string: - return otellog.String(a.k, a.v.(string)) - case bool: - return otellog.Bool(a.k, a.v.(bool)) - default: // everything else gets stringified - return otellog.String(a.k, stringify.Marshal(a.v, false)) +// clues.OTELConfig is a passthrough to the internal otel config. +func (oc OTELConfig) toInternalConfig() node.OTELConfig { + return node.OTELConfig{ + GRPCEndpoint: oc.GRPCEndpoint, } } - -type Annotationer interface { - IsAttribute() bool - KV() attribute.KeyValue -}