github.com/jxskiss/gopkg/v2@v2.14.9-0.20240514120614-899f3e7952b4/easy/recover.go (about)

     1  package easy
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"log"
     7  
     8  	"github.com/jxskiss/gopkg/v2/internal"
     9  )
    10  
    11  var recoverToError = NewRecoverFunc(func(_ context.Context, _ *PanicError) {})
    12  
    13  // PanicError represents an captured panic error.
    14  type PanicError struct {
    15  	Exception  any
    16  	Location   string
    17  	Stacktrace []byte
    18  }
    19  
    20  func newPanicError(skip int, e any) *PanicError {
    21  	panicLoc, frames := internal.IdentifyPanic(skip + 1)
    22  	stack := internal.FormatFrames(frames)
    23  	return &PanicError{
    24  		Exception:  e,
    25  		Location:   panicLoc,
    26  		Stacktrace: stack,
    27  	}
    28  }
    29  
    30  func (p *PanicError) Unwrap() error {
    31  	if err, ok := p.Exception.(error); ok {
    32  		return err
    33  	}
    34  	return fmt.Errorf("%v", p.Exception)
    35  }
    36  
    37  func (p *PanicError) Error() string {
    38  	return fmt.Sprintf("panic: %v, location: %v", p.Exception, p.Location)
    39  }
    40  
    41  func (p *PanicError) Format(f fmt.State, c rune) {
    42  	if c == 'v' && f.Flag('+') {
    43  		fmt.Fprintf(f, "panic: %v, location: %v\n%s\n", p.Exception, p.Location, p.Stacktrace)
    44  	} else {
    45  		fmt.Fprint(f, p.Error())
    46  	}
    47  }
    48  
    49  // Safe returns a wrapper function with panic recover.
    50  //
    51  // Note that if panic happens, the wrapped function does not log messages,
    52  // instead it will be returned as a `*PanicError`, the caller take
    53  // responsibility to log the panic messages.
    54  func Safe(f func()) func() error {
    55  	return func() (err error) {
    56  		defer recoverToError(context.Background(), &err)
    57  		f()
    58  		return
    59  	}
    60  }
    61  
    62  // Safe1 returns a wrapper function with panic recover.
    63  //
    64  // Note that if panic or error happens, the wrapped function does not log
    65  // messages, instead it will be returned as an error, the caller take
    66  // responsibility to log the panic or error messages.
    67  func Safe1(f func() error) func() error {
    68  	return func() (err error) {
    69  		defer recoverToError(context.Background(), &err)
    70  		err = f()
    71  		return
    72  	}
    73  }
    74  
    75  // Recover recovers panics.
    76  // If panic occurred, it prints an error log.
    77  // If err is not nil, it will be set to a `*PanicError`.
    78  //
    79  // Note that this function should not be wrapped by another function,
    80  // instead it should be called directly by the `defer` statement,
    81  // else it won't work as you may expect.
    82  func Recover(errp *error) {
    83  	e := recover()
    84  	if e == nil {
    85  		return
    86  	}
    87  	pErr := newPanicError(0, e)
    88  	if errp != nil {
    89  		*errp = pErr
    90  	}
    91  	log.Printf("[Error] %+v", pErr)
    92  }
    93  
    94  // NewRecoverFunc returns a function which recovers panics.
    95  // It accepts a panicErr handler function which may be used to log the
    96  // panic error and context information.
    97  //
    98  // Note that the returned function should not be wrapped by another
    99  // function, instead it should be called directly by the `defer` statement,
   100  // else it won't work as you may expect.
   101  //
   102  // Example:
   103  //
   104  //	var Recover = NewRecoverFunc(func(ctx context.Context, panicErr *easy.PanicError) {
   105  //		serviceName := getServiceName(ctx)
   106  //
   107  //		// emit metrics
   108  //		metrics.Emit("panic", 1, metrics.Tag("service", serviceName))
   109  //
   110  //		// print log
   111  //		log.Printf("[Error] %+v", panicErr)
   112  //
   113  //		// or check the panic details
   114  //		// mylog.Logger(ctx).Errorf("catch panic: %v\nlocation: %s\nstacktrace: %s",
   115  //		//	panicErr.Exception, panicErr.Location, panicErr.Stacktrace)
   116  //	})
   117  //
   118  //	// Use the recover function somewhere.
   119  //	func SomeFunction(ctx context.Context) (err error) {
   120  //		defer Recover(ctx, &err)
   121  //		// do something ...
   122  //	}
   123  func NewRecoverFunc[T any](f func(ctx T, panicErr *PanicError)) func(ctx T, errp *error) {
   124  	return func(ctx T, errp *error) {
   125  		e := recover()
   126  		if e == nil {
   127  			return
   128  		}
   129  		pErr := newPanicError(0, e)
   130  		if errp != nil {
   131  			*errp = pErr
   132  		}
   133  		f(ctx, pErr)
   134  	}
   135  }
   136  
   137  // IdentifyPanic reports the panic location when a panic happens.
   138  // It should be called directly after `recover()`, not wrapped by
   139  // another function, else it returns incorrect location.
   140  // Use IdentifyPanicSkip for wrapping.
   141  func IdentifyPanic() string {
   142  	loc, _ := internal.IdentifyPanic(1)
   143  	return loc
   144  }
   145  
   146  // IdentifyPanicSkip is similar to IdentifyPanic, except that
   147  // it accepts a param skip for wrapping usecase.
   148  func IdentifyPanicSkip(skip int) string {
   149  	loc, _ := internal.IdentifyPanic(skip + 1)
   150  	return loc
   151  }
   152  
   153  // EnsureError ensures the given value (should be non-nil) is an error.
   154  // If it's not an error, `fmt.Errorf("%v", v)` will be used to convert it.
   155  func EnsureError(v any) error {
   156  	if v == nil {
   157  		return nil
   158  	}
   159  	err, ok := v.(error)
   160  	if !ok {
   161  		err = fmt.Errorf("%v", v)
   162  	}
   163  	return err
   164  }
   165  
   166  // PanicOnError fires a panic if any of the args is non-nil error.
   167  func PanicOnError(args ...any) {
   168  	for _, arg := range args {
   169  		if err, ok := arg.(error); ok && err != nil {
   170  			panic(err)
   171  		}
   172  	}
   173  }