2026-03-06 09:30:57 +00:00
|
|
|
// Package log provides structured logging and error handling for Core applications.
|
|
|
|
|
//
|
|
|
|
|
// This file implements structured error types and combined log-and-return helpers
|
|
|
|
|
// that simplify common error handling patterns.
|
|
|
|
|
|
|
|
|
|
package log
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"errors"
|
2026-03-09 08:29:57 +00:00
|
|
|
"iter"
|
|
|
|
|
"strings"
|
2026-03-30 06:33:30 +00:00
|
|
|
"time"
|
2026-03-06 09:30:57 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// Err represents a structured error with operational context.
|
|
|
|
|
// It implements the error interface and supports unwrapping.
|
|
|
|
|
type Err struct {
|
|
|
|
|
Op string // Operation being performed (e.g., "user.Save")
|
|
|
|
|
Msg string // Human-readable message
|
|
|
|
|
Err error // Underlying error (optional)
|
|
|
|
|
Code string // Error code (optional, e.g., "VALIDATION_FAILED")
|
2026-03-30 06:33:30 +00:00
|
|
|
// Retryable indicates whether the caller can safely retry this error.
|
|
|
|
|
Retryable bool
|
|
|
|
|
// RetryAfter suggests a delay before retrying when Retryable is true.
|
|
|
|
|
RetryAfter *time.Duration
|
|
|
|
|
// NextAction suggests an alternative path when this error is not directly retryable.
|
|
|
|
|
NextAction string
|
2026-03-06 09:30:57 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Error implements the error interface.
|
|
|
|
|
func (e *Err) Error() string {
|
2026-03-30 13:36:30 +00:00
|
|
|
if e == nil {
|
|
|
|
|
return ""
|
2026-03-06 09:30:57 +00:00
|
|
|
}
|
2026-03-30 13:36:30 +00:00
|
|
|
|
|
|
|
|
body := e.Msg
|
|
|
|
|
if body == "" {
|
2026-03-06 09:30:57 +00:00
|
|
|
if e.Code != "" {
|
2026-03-30 13:36:30 +00:00
|
|
|
body = "[" + e.Code + "]"
|
2026-03-06 09:30:57 +00:00
|
|
|
}
|
2026-03-30 13:36:30 +00:00
|
|
|
} else if e.Code != "" {
|
|
|
|
|
body += " [" + e.Code + "]"
|
2026-03-06 09:30:57 +00:00
|
|
|
}
|
2026-03-30 13:36:30 +00:00
|
|
|
|
|
|
|
|
if e.Err != nil {
|
|
|
|
|
if body != "" {
|
|
|
|
|
body += ": " + e.Err.Error()
|
|
|
|
|
} else {
|
|
|
|
|
body = e.Err.Error()
|
|
|
|
|
}
|
2026-03-06 09:30:57 +00:00
|
|
|
}
|
2026-03-30 13:36:30 +00:00
|
|
|
|
|
|
|
|
if e.Op != "" {
|
|
|
|
|
if body != "" {
|
|
|
|
|
return e.Op + ": " + body
|
|
|
|
|
}
|
|
|
|
|
return e.Op
|
|
|
|
|
}
|
|
|
|
|
return body
|
2026-03-06 09:30:57 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Unwrap returns the underlying error for use with errors.Is and errors.As.
|
|
|
|
|
func (e *Err) Unwrap() error {
|
|
|
|
|
return e.Err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- Error Creation Functions ---
|
|
|
|
|
|
|
|
|
|
// E creates a new Err with operation context.
|
|
|
|
|
// The underlying error can be nil for creating errors without a cause.
|
|
|
|
|
//
|
|
|
|
|
// Example:
|
|
|
|
|
//
|
|
|
|
|
// return log.E("user.Save", "failed to save user", err)
|
|
|
|
|
// return log.E("api.Call", "rate limited", nil) // No underlying cause
|
|
|
|
|
func E(op, msg string, err error) error {
|
|
|
|
|
return &Err{Op: op, Msg: msg, Err: err}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-30 06:33:30 +00:00
|
|
|
// EWithRecovery creates a new Err with operation context and recovery metadata.
|
2026-03-30 13:36:30 +00:00
|
|
|
//
|
|
|
|
|
// return log.EWithRecovery("api.Call", "temporary failure", err, true, &retryAfter, "retry with backoff")
|
2026-03-30 06:33:30 +00:00
|
|
|
func EWithRecovery(op, msg string, err error, retryable bool, retryAfter *time.Duration, nextAction string) error {
|
|
|
|
|
recoveryErr := &Err{
|
|
|
|
|
Op: op,
|
|
|
|
|
Msg: msg,
|
|
|
|
|
Err: err,
|
|
|
|
|
}
|
|
|
|
|
inheritRecovery(recoveryErr, err)
|
|
|
|
|
recoveryErr.Retryable = retryable
|
|
|
|
|
recoveryErr.RetryAfter = retryAfter
|
|
|
|
|
recoveryErr.NextAction = nextAction
|
|
|
|
|
return recoveryErr
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-06 09:30:57 +00:00
|
|
|
// Wrap wraps an error with operation context.
|
|
|
|
|
// Returns nil if err is nil, to support conditional wrapping.
|
|
|
|
|
// Preserves error Code if the wrapped error is an *Err.
|
|
|
|
|
//
|
|
|
|
|
// Example:
|
|
|
|
|
//
|
|
|
|
|
// return log.Wrap(err, "db.Query", "database query failed")
|
|
|
|
|
func Wrap(err error, op, msg string) error {
|
|
|
|
|
if err == nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
2026-04-01 09:49:40 +00:00
|
|
|
wrapped := &Err{Op: op, Msg: msg, Err: err, Code: inheritedCode(err)}
|
2026-03-30 06:33:30 +00:00
|
|
|
inheritRecovery(wrapped, err)
|
|
|
|
|
return wrapped
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// WrapWithRecovery wraps an error with operation context and explicit recovery metadata.
|
2026-03-30 13:36:30 +00:00
|
|
|
//
|
|
|
|
|
// return log.WrapWithRecovery(err, "api.Call", "temporary failure", true, &retryAfter, "retry with backoff")
|
2026-03-30 06:33:30 +00:00
|
|
|
func WrapWithRecovery(err error, op, msg string, retryable bool, retryAfter *time.Duration, nextAction string) error {
|
|
|
|
|
if err == nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
recoveryErr := &Err{
|
|
|
|
|
Op: op,
|
|
|
|
|
Msg: msg,
|
|
|
|
|
Err: err,
|
|
|
|
|
Code: ErrCode(err),
|
|
|
|
|
}
|
|
|
|
|
inheritRecovery(recoveryErr, err)
|
|
|
|
|
recoveryErr.Retryable = retryable
|
|
|
|
|
recoveryErr.RetryAfter = retryAfter
|
|
|
|
|
recoveryErr.NextAction = nextAction
|
|
|
|
|
return recoveryErr
|
2026-03-06 09:30:57 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// WrapCode wraps an error with operation context and error code.
|
|
|
|
|
// Returns nil only if both err is nil AND code is empty.
|
|
|
|
|
// Useful for API errors that need machine-readable codes.
|
|
|
|
|
//
|
|
|
|
|
// Example:
|
|
|
|
|
//
|
|
|
|
|
// return log.WrapCode(err, "VALIDATION_ERROR", "user.Validate", "invalid email")
|
|
|
|
|
func WrapCode(err error, code, op, msg string) error {
|
|
|
|
|
if err == nil && code == "" {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
2026-03-30 06:33:30 +00:00
|
|
|
wrapped := &Err{Op: op, Msg: msg, Err: err, Code: code}
|
|
|
|
|
inheritRecovery(wrapped, err)
|
|
|
|
|
return wrapped
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// WrapCodeWithRecovery wraps an error with operation context, code, and recovery metadata.
|
2026-03-30 13:36:30 +00:00
|
|
|
//
|
|
|
|
|
// return log.WrapCodeWithRecovery(err, "TEMPORARY_UNAVAILABLE", "api.Call", "temporary failure", true, &retryAfter, "retry with backoff")
|
2026-03-30 06:33:30 +00:00
|
|
|
func WrapCodeWithRecovery(err error, code, op, msg string, retryable bool, retryAfter *time.Duration, nextAction string) error {
|
|
|
|
|
if err == nil && code == "" {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
recoveryErr := &Err{
|
|
|
|
|
Op: op,
|
|
|
|
|
Msg: msg,
|
|
|
|
|
Err: err,
|
|
|
|
|
Code: code,
|
|
|
|
|
}
|
|
|
|
|
inheritRecovery(recoveryErr, err)
|
|
|
|
|
recoveryErr.Retryable = retryable
|
|
|
|
|
recoveryErr.RetryAfter = retryAfter
|
|
|
|
|
recoveryErr.NextAction = nextAction
|
|
|
|
|
return recoveryErr
|
2026-03-06 09:30:57 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NewCode creates an error with just code and message (no underlying error).
|
|
|
|
|
// Useful for creating sentinel errors with codes.
|
|
|
|
|
//
|
|
|
|
|
// Example:
|
|
|
|
|
//
|
|
|
|
|
// var ErrNotFound = log.NewCode("NOT_FOUND", "resource not found")
|
|
|
|
|
func NewCode(code, msg string) error {
|
|
|
|
|
return &Err{Msg: msg, Code: code}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-30 06:33:30 +00:00
|
|
|
// NewCodeWithRecovery creates a coded error with recovery metadata.
|
2026-03-30 13:36:30 +00:00
|
|
|
//
|
|
|
|
|
// var ErrTemporary = log.NewCodeWithRecovery("TEMPORARY_UNAVAILABLE", "temporary failure", true, &retryAfter, "retry with backoff")
|
2026-03-30 06:33:30 +00:00
|
|
|
func NewCodeWithRecovery(code, msg string, retryable bool, retryAfter *time.Duration, nextAction string) error {
|
|
|
|
|
return &Err{
|
|
|
|
|
Msg: msg,
|
|
|
|
|
Code: code,
|
|
|
|
|
Retryable: retryable,
|
|
|
|
|
RetryAfter: retryAfter,
|
|
|
|
|
NextAction: nextAction,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// inheritRecovery copies recovery metadata from the first *Err in err's chain.
|
|
|
|
|
func inheritRecovery(dst *Err, err error) {
|
|
|
|
|
if err == nil || dst == nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
var source *Err
|
|
|
|
|
if As(err, &source) {
|
|
|
|
|
dst.Retryable = source.Retryable
|
|
|
|
|
dst.RetryAfter = source.RetryAfter
|
|
|
|
|
dst.NextAction = source.NextAction
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-01 09:49:40 +00:00
|
|
|
// inheritedCode returns the first non-empty code found in an error chain.
|
|
|
|
|
func inheritedCode(err error) string {
|
|
|
|
|
for err != nil {
|
|
|
|
|
if wrapped, ok := err.(*Err); ok && wrapped.Code != "" {
|
|
|
|
|
return wrapped.Code
|
|
|
|
|
}
|
|
|
|
|
err = errors.Unwrap(err)
|
|
|
|
|
}
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-30 06:33:30 +00:00
|
|
|
// RetryAfter returns the first retry-after hint from an error chain, if present.
|
2026-03-30 13:36:30 +00:00
|
|
|
//
|
|
|
|
|
// retryAfter, ok := log.RetryAfter(err)
|
2026-03-30 06:33:30 +00:00
|
|
|
func RetryAfter(err error) (*time.Duration, bool) {
|
2026-04-01 04:41:48 +00:00
|
|
|
for err != nil {
|
|
|
|
|
if wrapped, ok := err.(*Err); ok && wrapped.RetryAfter != nil {
|
2026-03-30 06:33:30 +00:00
|
|
|
return wrapped.RetryAfter, true
|
|
|
|
|
}
|
2026-04-01 04:41:48 +00:00
|
|
|
err = errors.Unwrap(err)
|
2026-03-30 06:33:30 +00:00
|
|
|
}
|
|
|
|
|
return nil, false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// IsRetryable reports whether the error chain contains a retryable Err.
|
2026-03-30 13:36:30 +00:00
|
|
|
//
|
|
|
|
|
// if log.IsRetryable(err) { /* retry the operation */ }
|
2026-03-30 06:33:30 +00:00
|
|
|
func IsRetryable(err error) bool {
|
|
|
|
|
var wrapped *Err
|
|
|
|
|
if As(err, &wrapped) {
|
|
|
|
|
return wrapped.Retryable
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// RecoveryAction returns the first next action from an error chain.
|
2026-03-30 13:36:30 +00:00
|
|
|
//
|
|
|
|
|
// next := log.RecoveryAction(err)
|
2026-03-30 06:33:30 +00:00
|
|
|
func RecoveryAction(err error) string {
|
2026-04-01 04:41:48 +00:00
|
|
|
for err != nil {
|
|
|
|
|
if wrapped, ok := err.(*Err); ok && wrapped.NextAction != "" {
|
|
|
|
|
return wrapped.NextAction
|
|
|
|
|
}
|
|
|
|
|
err = errors.Unwrap(err)
|
2026-03-30 06:33:30 +00:00
|
|
|
}
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-01 04:41:48 +00:00
|
|
|
func retryableHint(err error) bool {
|
|
|
|
|
for err != nil {
|
|
|
|
|
if wrapped, ok := err.(*Err); ok && wrapped.Retryable {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
err = errors.Unwrap(err)
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-06 09:30:57 +00:00
|
|
|
// --- Standard Library Wrappers ---
|
|
|
|
|
|
|
|
|
|
// Is reports whether any error in err's tree matches target.
|
|
|
|
|
// Wrapper around errors.Is for convenience.
|
2026-03-30 00:36:24 +00:00
|
|
|
//
|
|
|
|
|
// if log.Is(err, context.DeadlineExceeded) { /* handle timeout */ }
|
2026-03-06 09:30:57 +00:00
|
|
|
func Is(err, target error) bool {
|
|
|
|
|
return errors.Is(err, target)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// As finds the first error in err's tree that matches target.
|
|
|
|
|
// Wrapper around errors.As for convenience.
|
2026-03-30 00:36:24 +00:00
|
|
|
//
|
|
|
|
|
// var e *log.Err
|
|
|
|
|
// if log.As(err, &e) { /* use e.Code */ }
|
2026-03-06 09:30:57 +00:00
|
|
|
func As(err error, target any) bool {
|
|
|
|
|
return errors.As(err, target)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NewError creates a simple error with the given text.
|
|
|
|
|
// Wrapper around errors.New for convenience.
|
2026-03-30 00:36:24 +00:00
|
|
|
//
|
|
|
|
|
// return log.NewError("invalid state")
|
2026-03-06 09:30:57 +00:00
|
|
|
func NewError(text string) error {
|
|
|
|
|
return errors.New(text)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Join combines multiple errors into one.
|
|
|
|
|
// Wrapper around errors.Join for convenience.
|
2026-03-30 00:36:24 +00:00
|
|
|
//
|
|
|
|
|
// return log.Join(validateErr, persistErr)
|
2026-03-06 09:30:57 +00:00
|
|
|
func Join(errs ...error) error {
|
|
|
|
|
return errors.Join(errs...)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- Error Introspection Helpers ---
|
|
|
|
|
|
|
|
|
|
// Op extracts the operation name from an error.
|
|
|
|
|
// Returns empty string if the error is not an *Err.
|
refactor(ax): AX compliance pass — usage example comments on all exported functions
- Add usage example comments to Logger methods (SetLevel, Level, SetOutput, SetRedactKeys, Debug, Info, Warn, Error, Security)
- Add usage example comments to error introspection (Op, ErrCode, Root, AllOps, StackTrace, FormatStackTrace)
- Add usage example comments to package-level functions (Default, SetDefault)
- Banned imports (fmt, io, os, errors) are LEGITIMATE — this IS the logging/error abstraction layer
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 21:11:16 +01:00
|
|
|
//
|
|
|
|
|
// op := log.Op(err) // e.g. "user.Save"
|
2026-03-06 09:30:57 +00:00
|
|
|
func Op(err error) string {
|
|
|
|
|
var e *Err
|
|
|
|
|
if As(err, &e) {
|
|
|
|
|
return e.Op
|
|
|
|
|
}
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ErrCode extracts the error code from an error.
|
|
|
|
|
// Returns empty string if the error is not an *Err or has no code.
|
refactor(ax): AX compliance pass — usage example comments on all exported functions
- Add usage example comments to Logger methods (SetLevel, Level, SetOutput, SetRedactKeys, Debug, Info, Warn, Error, Security)
- Add usage example comments to error introspection (Op, ErrCode, Root, AllOps, StackTrace, FormatStackTrace)
- Add usage example comments to package-level functions (Default, SetDefault)
- Banned imports (fmt, io, os, errors) are LEGITIMATE — this IS the logging/error abstraction layer
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 21:11:16 +01:00
|
|
|
//
|
|
|
|
|
// code := log.ErrCode(err) // e.g. "VALIDATION_FAILED"
|
2026-03-06 09:30:57 +00:00
|
|
|
func ErrCode(err error) string {
|
|
|
|
|
var e *Err
|
|
|
|
|
if As(err, &e) {
|
|
|
|
|
return e.Code
|
|
|
|
|
}
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Message extracts the message from an error.
|
|
|
|
|
// Returns the error's Error() string if not an *Err.
|
2026-03-30 00:36:24 +00:00
|
|
|
//
|
|
|
|
|
// msg := log.Message(err)
|
2026-03-06 09:30:57 +00:00
|
|
|
func Message(err error) string {
|
|
|
|
|
if err == nil {
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
var e *Err
|
|
|
|
|
if As(err, &e) {
|
|
|
|
|
return e.Msg
|
|
|
|
|
}
|
|
|
|
|
return err.Error()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Root returns the root cause of an error chain.
|
|
|
|
|
// Unwraps until no more wrapped errors are found.
|
refactor(ax): AX compliance pass — usage example comments on all exported functions
- Add usage example comments to Logger methods (SetLevel, Level, SetOutput, SetRedactKeys, Debug, Info, Warn, Error, Security)
- Add usage example comments to error introspection (Op, ErrCode, Root, AllOps, StackTrace, FormatStackTrace)
- Add usage example comments to package-level functions (Default, SetDefault)
- Banned imports (fmt, io, os, errors) are LEGITIMATE — this IS the logging/error abstraction layer
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 21:11:16 +01:00
|
|
|
//
|
|
|
|
|
// cause := log.Root(err)
|
2026-03-06 09:30:57 +00:00
|
|
|
func Root(err error) error {
|
|
|
|
|
if err == nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
for {
|
|
|
|
|
unwrapped := errors.Unwrap(err)
|
|
|
|
|
if unwrapped == nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
err = unwrapped
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-09 08:29:57 +00:00
|
|
|
// AllOps returns an iterator over all operational contexts in the error chain.
|
|
|
|
|
// It traverses the error tree using errors.Unwrap.
|
refactor(ax): AX compliance pass — usage example comments on all exported functions
- Add usage example comments to Logger methods (SetLevel, Level, SetOutput, SetRedactKeys, Debug, Info, Warn, Error, Security)
- Add usage example comments to error introspection (Op, ErrCode, Root, AllOps, StackTrace, FormatStackTrace)
- Add usage example comments to package-level functions (Default, SetDefault)
- Banned imports (fmt, io, os, errors) are LEGITIMATE — this IS the logging/error abstraction layer
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 21:11:16 +01:00
|
|
|
//
|
|
|
|
|
// for op := range log.AllOps(err) { /* "api.Call" → "db.Query" → ... */ }
|
2026-03-09 08:29:57 +00:00
|
|
|
func AllOps(err error) iter.Seq[string] {
|
|
|
|
|
return func(yield func(string) bool) {
|
|
|
|
|
for err != nil {
|
|
|
|
|
if e, ok := err.(*Err); ok {
|
|
|
|
|
if e.Op != "" {
|
|
|
|
|
if !yield(e.Op) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
err = errors.Unwrap(err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-06 09:30:57 +00:00
|
|
|
// StackTrace returns the logical stack trace (chain of operations) from an error.
|
|
|
|
|
// It returns an empty slice if no operational context is found.
|
refactor(ax): AX compliance pass — usage example comments on all exported functions
- Add usage example comments to Logger methods (SetLevel, Level, SetOutput, SetRedactKeys, Debug, Info, Warn, Error, Security)
- Add usage example comments to error introspection (Op, ErrCode, Root, AllOps, StackTrace, FormatStackTrace)
- Add usage example comments to package-level functions (Default, SetDefault)
- Banned imports (fmt, io, os, errors) are LEGITIMATE — this IS the logging/error abstraction layer
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 21:11:16 +01:00
|
|
|
//
|
|
|
|
|
// ops := log.StackTrace(err) // ["api.Call", "db.Query", "sql.Exec"]
|
2026-03-06 09:30:57 +00:00
|
|
|
func StackTrace(err error) []string {
|
|
|
|
|
var stack []string
|
2026-03-09 08:29:57 +00:00
|
|
|
for op := range AllOps(err) {
|
|
|
|
|
stack = append(stack, op)
|
2026-03-06 09:30:57 +00:00
|
|
|
}
|
|
|
|
|
return stack
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// FormatStackTrace returns a pretty-printed logical stack trace.
|
refactor(ax): AX compliance pass — usage example comments on all exported functions
- Add usage example comments to Logger methods (SetLevel, Level, SetOutput, SetRedactKeys, Debug, Info, Warn, Error, Security)
- Add usage example comments to error introspection (Op, ErrCode, Root, AllOps, StackTrace, FormatStackTrace)
- Add usage example comments to package-level functions (Default, SetDefault)
- Banned imports (fmt, io, os, errors) are LEGITIMATE — this IS the logging/error abstraction layer
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 21:11:16 +01:00
|
|
|
//
|
|
|
|
|
// trace := log.FormatStackTrace(err) // "api.Call -> db.Query -> sql.Exec"
|
2026-03-06 09:30:57 +00:00
|
|
|
func FormatStackTrace(err error) string {
|
2026-03-09 08:29:57 +00:00
|
|
|
var ops []string
|
|
|
|
|
for op := range AllOps(err) {
|
|
|
|
|
ops = append(ops, op)
|
2026-03-06 09:30:57 +00:00
|
|
|
}
|
2026-03-09 08:29:57 +00:00
|
|
|
if len(ops) == 0 {
|
|
|
|
|
return ""
|
2026-03-06 09:30:57 +00:00
|
|
|
}
|
2026-03-09 08:29:57 +00:00
|
|
|
return strings.Join(ops, " -> ")
|
2026-03-06 09:30:57 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- Combined Log-and-Return Helpers ---
|
|
|
|
|
|
|
|
|
|
// LogError logs an error at Error level and returns a wrapped error.
|
|
|
|
|
// Reduces boilerplate in error handling paths.
|
|
|
|
|
//
|
|
|
|
|
// Example:
|
|
|
|
|
//
|
|
|
|
|
// // Before
|
|
|
|
|
// if err != nil {
|
|
|
|
|
// log.Error("failed to save", "err", err)
|
|
|
|
|
// return errors.Wrap(err, "user.Save", "failed to save")
|
|
|
|
|
// }
|
|
|
|
|
//
|
|
|
|
|
// // After
|
|
|
|
|
// if err != nil {
|
|
|
|
|
// return log.LogError(err, "user.Save", "failed to save")
|
|
|
|
|
// }
|
|
|
|
|
func LogError(err error, op, msg string) error {
|
|
|
|
|
if err == nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
wrapped := Wrap(err, op, msg)
|
2026-04-01 08:58:37 +00:00
|
|
|
Default().Error(msg, "op", op, "err", err)
|
2026-03-06 09:30:57 +00:00
|
|
|
return wrapped
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// LogWarn logs at Warn level and returns a wrapped error.
|
|
|
|
|
// Use for recoverable errors that should be logged but not treated as critical.
|
|
|
|
|
//
|
|
|
|
|
// Example:
|
|
|
|
|
//
|
|
|
|
|
// return log.LogWarn(err, "cache.Get", "cache miss, falling back to db")
|
|
|
|
|
func LogWarn(err error, op, msg string) error {
|
|
|
|
|
if err == nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
wrapped := Wrap(err, op, msg)
|
2026-04-01 08:58:37 +00:00
|
|
|
Default().Warn(msg, "op", op, "err", err)
|
2026-03-06 09:30:57 +00:00
|
|
|
return wrapped
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Must panics if err is not nil, logging first.
|
|
|
|
|
// Use for errors that should never happen and indicate programmer error.
|
|
|
|
|
//
|
|
|
|
|
// Example:
|
|
|
|
|
//
|
|
|
|
|
// log.Must(Initialize(), "app", "startup failed")
|
|
|
|
|
func Must(err error, op, msg string) {
|
|
|
|
|
if err != nil {
|
2026-03-29 22:34:20 +00:00
|
|
|
wrapped := Wrap(err, op, msg)
|
2026-04-01 08:58:37 +00:00
|
|
|
Default().Error(msg, "op", op, "err", err)
|
2026-03-29 22:34:20 +00:00
|
|
|
panic(wrapped)
|
2026-03-06 09:30:57 +00:00
|
|
|
}
|
|
|
|
|
}
|