218 lines
5.3 KiB
Go
218 lines
5.3 KiB
Go
|
|
// 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"
|
||
|
|
"fmt"
|
||
|
|
)
|
||
|
|
|
||
|
|
// 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")
|
||
|
|
}
|
||
|
|
|
||
|
|
// Error implements the error interface.
|
||
|
|
func (e *Err) Error() string {
|
||
|
|
if e.Err != nil {
|
||
|
|
if e.Code != "" {
|
||
|
|
return fmt.Sprintf("%s: %s [%s]: %v", e.Op, e.Msg, e.Code, e.Err)
|
||
|
|
}
|
||
|
|
return fmt.Sprintf("%s: %s: %v", e.Op, e.Msg, e.Err)
|
||
|
|
}
|
||
|
|
if e.Code != "" {
|
||
|
|
return fmt.Sprintf("%s: %s [%s]", e.Op, e.Msg, e.Code)
|
||
|
|
}
|
||
|
|
return fmt.Sprintf("%s: %s", e.Op, e.Msg)
|
||
|
|
}
|
||
|
|
|
||
|
|
// 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.
|
||
|
|
// If err is nil, returns nil to support conditional wrapping.
|
||
|
|
//
|
||
|
|
// Example:
|
||
|
|
//
|
||
|
|
// return log.E("user.Save", "failed to save user", err)
|
||
|
|
func E(op, msg string, err error) error {
|
||
|
|
if err == nil {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
return &Err{Op: op, Msg: msg, Err: err}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Wrap wraps an error with operation context.
|
||
|
|
// Alias for E() for semantic clarity when wrapping existing errors.
|
||
|
|
//
|
||
|
|
// Example:
|
||
|
|
//
|
||
|
|
// return log.Wrap(err, "db.Query", "database query failed")
|
||
|
|
func Wrap(err error, op, msg string) error {
|
||
|
|
return E(op, msg, err)
|
||
|
|
}
|
||
|
|
|
||
|
|
// WrapCode wraps an error with operation context and error code.
|
||
|
|
// 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 {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
return &Err{Op: op, Msg: msg, Err: err, Code: code}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 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}
|
||
|
|
}
|
||
|
|
|
||
|
|
// --- Standard Library Wrappers ---
|
||
|
|
|
||
|
|
// Is reports whether any error in err's tree matches target.
|
||
|
|
// Wrapper around errors.Is for convenience.
|
||
|
|
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.
|
||
|
|
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.
|
||
|
|
func NewError(text string) error {
|
||
|
|
return errors.New(text)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Join combines multiple errors into one.
|
||
|
|
// Wrapper around errors.Join for convenience.
|
||
|
|
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.
|
||
|
|
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.
|
||
|
|
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.
|
||
|
|
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.
|
||
|
|
func Root(err error) error {
|
||
|
|
if err == nil {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
for {
|
||
|
|
unwrapped := errors.Unwrap(err)
|
||
|
|
if unwrapped == nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
err = unwrapped
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// --- 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)
|
||
|
|
defaultLogger.Error(msg, "op", op, "err", err)
|
||
|
|
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)
|
||
|
|
defaultLogger.Warn(msg, "op", op, "err", err)
|
||
|
|
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 {
|
||
|
|
defaultLogger.Error(msg, "op", op, "err", err)
|
||
|
|
panic(Wrap(err, op, msg))
|
||
|
|
}
|
||
|
|
}
|