From 9eef0bab6e0b527c739df941c83ea687c6960f7c Mon Sep 17 00:00:00 2001 From: Virgil Date: Thu, 26 Mar 2026 10:39:26 +0000 Subject: [PATCH 1/3] fix(ax): align test names and usage comments Co-Authored-By: Virgil --- errors_test.go | 6 +++--- log.go | 18 ++++++++++++------ log_test.go | 16 ++++++++-------- 3 files changed, 23 insertions(+), 17 deletions(-) diff --git a/errors_test.go b/errors_test.go index 574865e..d1871e5 100644 --- a/errors_test.go +++ b/errors_test.go @@ -328,18 +328,18 @@ func TestStackTrace_Good(t *testing.T) { assert.Equal(t, "op3 -> op2 -> op1", formatted) } -func TestStackTrace_PlainError(t *testing.T) { +func TestStackTrace_Bad_PlainError(t *testing.T) { err := errors.New("plain error") assert.Empty(t, StackTrace(err)) assert.Empty(t, FormatStackTrace(err)) } -func TestStackTrace_Nil(t *testing.T) { +func TestStackTrace_Bad_Nil(t *testing.T) { assert.Empty(t, StackTrace(nil)) assert.Empty(t, FormatStackTrace(nil)) } -func TestStackTrace_NoOp(t *testing.T) { +func TestStackTrace_Bad_NoOp(t *testing.T) { err := &Err{Msg: "no op"} assert.Empty(t, StackTrace(err)) assert.Empty(t, FormatStackTrace(err)) diff --git a/log.go b/log.go index be73deb..53f6759 100644 --- a/log.go +++ b/log.go @@ -59,13 +59,18 @@ type Logger struct { // RedactKeys is a list of keys whose values should be masked in logs. redactKeys []string - // Style functions for formatting (can be overridden) + // StyleTimestamp formats the rendered timestamp prefix. StyleTimestamp func(string) string - StyleDebug func(string) string - StyleInfo func(string) string - StyleWarn func(string) string - StyleError func(string) string - StyleSecurity func(string) string + // StyleDebug formats the debug level prefix. + StyleDebug func(string) string + // StyleInfo formats the info level prefix. + StyleInfo func(string) string + // StyleWarn formats the warning level prefix. + StyleWarn func(string) string + // StyleError formats the error level prefix. + StyleError func(string) string + // StyleSecurity formats the security event prefix. + StyleSecurity func(string) string } // RotationOptions defines the log rotation and retention policy. @@ -93,6 +98,7 @@ type RotationOptions struct { // Options configures a Logger. type Options struct { + // Level controls which messages are emitted. Level Level // Output is the destination for log messages. If Rotation is provided, // Output is ignored and logs are written to the rotating file instead. diff --git a/log_test.go b/log_test.go index eec88f4..fa57312 100644 --- a/log_test.go +++ b/log_test.go @@ -12,7 +12,7 @@ type nopWriteCloser struct{ goio.Writer } func (nopWriteCloser) Close() error { return nil } -func TestLogger_Levels(t *testing.T) { +func TestLogger_Levels_Good(t *testing.T) { tests := []struct { name string level Level @@ -62,7 +62,7 @@ func TestLogger_Levels(t *testing.T) { } } -func TestLogger_KeyValues(t *testing.T) { +func TestLogger_KeyValues_Good(t *testing.T) { var buf bytes.Buffer l := New(Options{Level: LevelDebug, Output: &buf}) @@ -80,7 +80,7 @@ func TestLogger_KeyValues(t *testing.T) { } } -func TestLogger_ErrorContext(t *testing.T) { +func TestLogger_ErrorContext_Good(t *testing.T) { var buf bytes.Buffer l := New(Options{Output: &buf, Level: LevelInfo}) @@ -98,7 +98,7 @@ func TestLogger_ErrorContext(t *testing.T) { } } -func TestLogger_Redaction(t *testing.T) { +func TestLogger_Redaction_Good(t *testing.T) { var buf bytes.Buffer l := New(Options{ Level: LevelInfo, @@ -120,7 +120,7 @@ func TestLogger_Redaction(t *testing.T) { } } -func TestLogger_InjectionPrevention(t *testing.T) { +func TestLogger_InjectionPrevention_Good(t *testing.T) { var buf bytes.Buffer l := New(Options{Level: LevelInfo, Output: &buf}) @@ -137,7 +137,7 @@ func TestLogger_InjectionPrevention(t *testing.T) { } } -func TestLogger_SetLevel(t *testing.T) { +func TestLogger_SetLevel_Good(t *testing.T) { l := New(Options{Level: LevelInfo}) if l.Level() != LevelInfo { @@ -150,7 +150,7 @@ func TestLogger_SetLevel(t *testing.T) { } } -func TestLevel_String(t *testing.T) { +func TestLevel_String_Good(t *testing.T) { tests := []struct { level Level expected string @@ -172,7 +172,7 @@ func TestLevel_String(t *testing.T) { } } -func TestLogger_Security(t *testing.T) { +func TestLogger_Security_Good(t *testing.T) { var buf bytes.Buffer l := New(Options{Level: LevelError, Output: &buf}) -- 2.45.3 From 2cc14de7fc556bc78007438cf47bc9c1a2f9c491 Mon Sep 17 00:00:00 2001 From: Virgil Date: Thu, 26 Mar 2026 12:12:51 +0000 Subject: [PATCH 2/3] chore(test): validate go build flow Co-Authored-By: Virgil -- 2.45.3 From 019b1f4efb8c7dec7ea410a387491aeb66cb0c26 Mon Sep 17 00:00:00 2001 From: Virgil Date: Fri, 27 Mar 2026 20:05:27 +0000 Subject: [PATCH 3/3] Add RFC for exported log package API --- specs/RFC.md | 224 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 224 insertions(+) create mode 100644 specs/RFC.md diff --git a/specs/RFC.md b/specs/RFC.md new file mode 100644 index 0000000..4f02d71 --- /dev/null +++ b/specs/RFC.md @@ -0,0 +1,224 @@ +# log +**Import:** `dappco.re/go/core/log` +**Files:** 2 + +## Types + +### Err +`type Err struct` + +Structured error wrapper that carries operation context, a human-readable message, an optional wrapped cause, and an optional machine-readable code. + +Fields: +- `Op string`: operation name. When non-empty, `Error` prefixes the formatted message with `Op + ": "`. +- `Msg string`: human-readable message stored on the error and returned by `Message`. +- `Err error`: wrapped cause returned by `Unwrap`. +- `Code string`: optional machine-readable code. When non-empty, `Error` includes it in square brackets. + +Methods: +- `func (e *Err) Error() string`: formats the error text from `Op`, `Msg`, `Code`, and `Err`. The result omits missing parts, so the output can be `"{Msg}"`, `"{Msg} [{Code}]"`, `"{Msg}: {Err}"`, or `"{Op}: {Msg} [{Code}]: {Err}"`. +- `func (e *Err) Unwrap() error`: returns `e.Err`. + +### Level +`type Level int` + +Logging verbosity enum used by `Logger` and `Options`. + +Methods: +- `func (l Level) String() string`: returns `quiet`, `error`, `warn`, `info`, or `debug`. Any other value returns `unknown`. + +### Logger +`type Logger struct` + +Concurrency-safe structured logger. `New` clones the configured redaction keys, stores the configured level and writer, and initializes all style hooks to identity functions. + +Each log call writes one line in the form `HH:MM:SS {prefix} {msg}` followed by space-separated key/value pairs. String values are rendered with Go `%q` quoting, redacted keys are replaced with `"[REDACTED]"`, a trailing key without a value renders as ``, and any `error` value in `keyvals` can cause `op` and `stack` fields to be appended automatically if those keys were not already supplied. + +Fields: +- `StyleTimestamp func(string) string`: transforms the rendered `HH:MM:SS` timestamp before it is written. +- `StyleDebug func(string) string`: transforms the debug prefix passed to debug log lines. +- `StyleInfo func(string) string`: transforms the info prefix passed to info log lines. +- `StyleWarn func(string) string`: transforms the warn prefix passed to warning log lines. +- `StyleError func(string) string`: transforms the error prefix passed to error log lines. +- `StyleSecurity func(string) string`: transforms the security prefix passed to security log lines. + +Methods: +- `func (l *Logger) SetLevel(level Level)`: sets the logger’s current threshold. +- `func (l *Logger) Level() Level`: returns the logger’s current threshold. +- `func (l *Logger) SetOutput(w goio.Writer)`: replaces the writer used for future log lines. +- `func (l *Logger) SetRedactKeys(keys ...string)`: replaces the exact-match key list whose values are masked during formatting. +- `func (l *Logger) Debug(msg string, keyvals ...any)`: emits a debug line when the logger level is at least `LevelDebug`. +- `func (l *Logger) Info(msg string, keyvals ...any)`: emits an info line when the logger level is at least `LevelInfo`. +- `func (l *Logger) Warn(msg string, keyvals ...any)`: emits a warning line when the logger level is at least `LevelWarn`. +- `func (l *Logger) Error(msg string, keyvals ...any)`: emits an error line when the logger level is at least `LevelError`. +- `func (l *Logger) Security(msg string, keyvals ...any)`: emits a security line with the security style prefix and the same visibility threshold as `LevelError`. + +### Options +`type Options struct` + +Constructor input for `New`. + +Fields: +- `Level Level`: initial logger threshold. +- `Output goio.Writer`: destination used when rotation is not selected. +- `Rotation *RotationOptions`: optional rotation configuration. `New` uses rotation only when this field is non-nil, `Rotation.Filename` is non-empty, and `RotationWriterFactory` is non-nil. +- `RedactKeys []string`: keys whose values should be masked in formatted log output. + +### RotationOptions +`type RotationOptions struct` + +Rotation configuration passed through to `RotationWriterFactory` when `New` selects a rotating writer. + +Fields: +- `Filename string`: log file path. `New` only attempts rotation when this field is non-empty. +- `MaxSize int`: value forwarded to the rotation writer factory. +- `MaxAge int`: value forwarded to the rotation writer factory. +- `MaxBackups int`: value forwarded to the rotation writer factory. +- `Compress bool`: value forwarded to the rotation writer factory. + +## Functions + +### AllOps +`func AllOps(err error) iter.Seq[string]` + +Returns an iterator over non-empty `Op` values found by repeatedly calling `errors.Unwrap` on `err`. Operations are yielded from the outermost `*Err` to the innermost one. + +### As +`func As(err error, target any) bool` + +Thin wrapper around `errors.As`. + +### Debug +`func Debug(msg string, keyvals ...any)` + +Calls `Default().Debug(msg, keyvals...)`. + +### Default +`func Default() *Logger` + +Returns the package-level default logger. The package initializes it with `New(Options{Level: LevelInfo})`. + +### E +`func E(op, msg string, err error) error` + +Returns `&Err{Op: op, Msg: msg, Err: err}` as an `error`. It always returns a non-nil error value, even when `err` is nil. + +### ErrCode +`func ErrCode(err error) string` + +If `err` contains an `*Err`, returns its `Code`. Otherwise returns the empty string. + +### Error +`func Error(msg string, keyvals ...any)` + +Calls `Default().Error(msg, keyvals...)`. + +### FormatStackTrace +`func FormatStackTrace(err error) string` + +Collects `AllOps(err)` and joins the operations with `" -> "`. Returns the empty string when no operations are found. + +### Info +`func Info(msg string, keyvals ...any)` + +Calls `Default().Info(msg, keyvals...)`. + +### Is +`func Is(err, target error) bool` + +Thin wrapper around `errors.Is`. + +### Join +`func Join(errs ...error) error` + +Thin wrapper around `errors.Join`. + +### LogError +`func LogError(err error, op, msg string) error` + +If `err` is nil, returns nil. Otherwise wraps the error with `Wrap(err, op, msg)`, logs `msg` through the default logger at error level with key/value pairs `"op", op, "err", err`, and returns the wrapped error. + +### LogWarn +`func LogWarn(err error, op, msg string) error` + +If `err` is nil, returns nil. Otherwise wraps the error with `Wrap(err, op, msg)`, logs `msg` through the default logger at warn level with key/value pairs `"op", op, "err", err`, and returns the wrapped error. + +### Message +`func Message(err error) string` + +Returns the `Msg` field from the first matching `*Err`. If `err` is nil, returns the empty string. For non-`*Err` errors, returns `err.Error()`. + +### Must +`func Must(err error, op, msg string)` + +If `err` is nil, does nothing. Otherwise logs `msg` through the default logger at error level with key/value pairs `"op", op, "err", err`, then panics with `Wrap(err, op, msg)`. + +### New +`func New(opts Options) *Logger` + +Constructs a logger from `opts`. It prefers a rotating writer only when `opts.Rotation` is non-nil, `opts.Rotation.Filename` is non-empty, and `RotationWriterFactory` is set; otherwise it uses `opts.Output`. If neither path yields a writer, it falls back to `os.Stderr`. + +### NewCode +`func NewCode(code, msg string) error` + +Returns `&Err{Msg: msg, Code: code}` as an `error`. + +### NewError +`func NewError(text string) error` + +Thin wrapper around `errors.New`. + +### Op +`func Op(err error) string` + +If `err` contains an `*Err`, returns its `Op`. Otherwise returns the empty string. + +### Root +`func Root(err error) error` + +Repeatedly unwraps `err` with `errors.Unwrap` until no further wrapped error exists, then returns the last error in that chain. If `err` is nil, returns nil. + +### Security +`func Security(msg string, keyvals ...any)` + +Calls `Default().Security(msg, keyvals...)`. + +### SetDefault +`func SetDefault(l *Logger)` + +Replaces the package-level default logger with `l`. + +### SetLevel +`func SetLevel(level Level)` + +Calls `Default().SetLevel(level)`. + +### SetRedactKeys +`func SetRedactKeys(keys ...string)` + +Calls `Default().SetRedactKeys(keys...)`. + +### StackTrace +`func StackTrace(err error) []string` + +Collects `AllOps(err)` into a slice in outermost-to-innermost order. When no operations are found, the returned slice is nil. + +### Username +`func Username() string` + +Returns the current username by trying `user.Current()` first, then the `USER` environment variable, then the `USERNAME` environment variable. + +### Warn +`func Warn(msg string, keyvals ...any)` + +Calls `Default().Warn(msg, keyvals...)`. + +### Wrap +`func Wrap(err error, op, msg string) error` + +If `err` is nil, returns nil. Otherwise returns a new `*Err` containing `op`, `msg`, and `err`. If the wrapped error chain already contains an `*Err` with a non-empty `Code`, the new wrapper copies that code. + +### WrapCode +`func WrapCode(err error, code, op, msg string) error` + +Returns nil only when both `err` is nil and `code` is empty. In every other case it returns `&Err{Op: op, Msg: msg, Err: err, Code: code}` as an `error`. -- 2.45.3