github.com/mongodb/grip@v0.0.0-20240213223901-f906268d82b9/recovery/recovery.go (about)

     1  // Package recovery provides a number of grip-integrated panic
     2  // handling tools for capturing and responding to panics using grip
     3  // loggers.
     4  //
     5  // These handlers are very useful for capturing panic messages that
     6  // might otherwise be lost, as well as providing implementations for
     7  // several established panic handling practices. Nevertheless, this
     8  // assumes that the panic, or an underlying system issue does not
     9  // affect the logging system or its dependencies. For example, panics
    10  // caused by disk-full or out of memory situations are challenging to
    11  // handle with this approach.
    12  //
    13  // All log message are logged with the default standard logger in the
    14  // grip package.
    15  package recovery
    16  
    17  import (
    18  	"os"
    19  	"strings"
    20  
    21  	"github.com/mongodb/grip"
    22  	"github.com/mongodb/grip/level"
    23  	"github.com/mongodb/grip/logging"
    24  	"github.com/mongodb/grip/message"
    25  )
    26  
    27  const killOverrideVarName = "__GRIP_EXIT_OVERRIDE"
    28  
    29  // LogStackTraceAndExit captures a panic, captures and logs a stack
    30  // trace at the Emergency level and then exits.
    31  //
    32  // This operation also attempts to close the underlying log sender.
    33  func LogStackTraceAndExit(opDetails ...string) {
    34  	if p := recover(); p != nil {
    35  		logAndExit(p, logging.MakeGrip(grip.GetSender()), message.MakeFields(getMessage(opDetails)))
    36  	}
    37  }
    38  
    39  // LogStackTraceAndContinue recovers from a panic, and then logs the
    40  // captures a stack trace and logs a structured message at "Alert"
    41  // level without further action.
    42  //
    43  // The "opDetails" argument is optional, and is joined as an
    44  // "operation" field in the log message for providing additional
    45  // context.
    46  //
    47  // Use in a common defer statement, such as:
    48  //
    49  //	defer recovery.LogStackTraceAndContinue("operation")
    50  func LogStackTraceAndContinue(opDetails ...string) {
    51  	if p := recover(); p != nil {
    52  		logAndContinue(p, logging.MakeGrip(grip.GetSender()), message.MakeFields(getMessage(opDetails)))
    53  	}
    54  }
    55  
    56  // HandlePanicWithError is used to convert a panic to an error.
    57  //
    58  // The "opDetails" argument is optional, and is joined as an
    59  // "operation" field in the log message for providing additional
    60  // context.
    61  //
    62  // You must construct a recovery function as in the following example:
    63  //
    64  //	defer func() { err = recovery.HandlePanicWithError(recover(),  err, "op") }()
    65  //
    66  // This defer statement must occur in a function that declares a
    67  // default error return value as in:
    68  //
    69  //	func operation() (err error) {}
    70  func HandlePanicWithError(p interface{}, err error, opDetails ...string) error {
    71  	catcher := grip.NewSimpleCatcher()
    72  	catcher.Add(err)
    73  
    74  	if p != nil {
    75  		perr := panicError(p)
    76  		catcher.Add(perr)
    77  
    78  		handleWithError(perr, err, logging.MakeGrip(grip.GetSender()), message.MakeFields(getMessage(opDetails)))
    79  	}
    80  
    81  	return catcher.Resolve()
    82  }
    83  
    84  // AnnotateMessageWithStackTraceAndContinue logs panics and continues
    85  // and is meant to be used in defer statements like
    86  // LogStackTraceAndContinue.
    87  //
    88  // It takes an interface which it converts to a message.Composer using
    89  // the same rules as logging methods, and annotates those messages
    90  // with the stack trace and panic information.
    91  func AnnotateMessageWithStackTraceAndContinue(m interface{}) {
    92  	if p := recover(); p != nil {
    93  		logAndContinue(p, logging.MakeGrip(grip.GetSender()), message.ConvertToComposer(level.Critical, m))
    94  	}
    95  }
    96  
    97  // SendStackTraceAndContinue is similar to
    98  // AnnotateMessageWithStackTraceAndContinue, but allows you to inject a
    99  // grip.Journaler interface to receive the log message.
   100  func SendStackTraceAndContinue(logger grip.Journaler, m interface{}) {
   101  	if p := recover(); p != nil {
   102  		logAndContinue(p, logger, message.ConvertToComposer(level.Critical, m))
   103  	}
   104  }
   105  
   106  // AnnotateMessageWithStackTraceAndExit logs panics and calls exit
   107  // like LogStackTraceAndExit.
   108  //
   109  // It takes an interface which it converts to a message.Composer using
   110  // the same rules as logging methods, and annotates those messages
   111  // with the stack trace and panic information.
   112  func AnnotateMessageWithStackTraceAndExit(m interface{}) {
   113  	if p := recover(); p != nil {
   114  		logAndExit(p, logging.MakeGrip(grip.GetSender()), message.ConvertToComposer(level.Critical, m))
   115  	}
   116  }
   117  
   118  // SendStackTraceMessageAndExit is similar to
   119  // AnnotateMessageWithStackTraceAndExit, but allows you to inject a
   120  // grip.Journaler interface.
   121  func SendStackTraceMessageAndExit(logger grip.Journaler, m interface{}) {
   122  	if p := recover(); p != nil {
   123  		logAndExit(p, logger, message.ConvertToComposer(level.Critical, m))
   124  	}
   125  }
   126  
   127  // AnnotateMessageWithPanicError processes a panic and converts it
   128  // into an error, combining it with the value of another error. Like,
   129  // HandlePanicWithError, this method is meant to be used in your own
   130  // defer functions.
   131  //
   132  // It takes an interface which it converts to a message.Composer using
   133  // the same rules as logging methods, and annotates those messages
   134  // with the stack trace and panic information.
   135  func AnnotateMessageWithPanicError(p interface{}, err error, m interface{}) error {
   136  	catcher := grip.NewSimpleCatcher()
   137  	catcher.Add(err)
   138  
   139  	if p != nil {
   140  		perr := panicError(p)
   141  		catcher.Add(perr)
   142  
   143  		handleWithError(perr, err, logging.MakeGrip(grip.GetSender()), message.ConvertToComposer(level.Critical, m))
   144  	}
   145  
   146  	return catcher.Resolve()
   147  }
   148  
   149  // SendMessageWithPanicError is similar to
   150  // AnnotateMessageWithPanicError, but allows you to inject a custom
   151  // grip.Jounaler interface to receive the log message.
   152  func SendMessageWithPanicError(p interface{}, err error, logger grip.Journaler, m interface{}) error {
   153  	catcher := grip.NewSimpleCatcher()
   154  	catcher.Add(err)
   155  
   156  	if p != nil {
   157  		perr := panicError(p)
   158  		catcher.Add(perr)
   159  
   160  		handleWithError(perr, err, logger, message.ConvertToComposer(level.Critical, m))
   161  	}
   162  
   163  	return catcher.Resolve()
   164  }
   165  
   166  ////////////////////////////////////////////////////////////////////////
   167  //
   168  // helpers
   169  
   170  func getMessage(details []string) message.Fields {
   171  	m := message.Fields{}
   172  
   173  	if len(details) > 0 {
   174  		m["operation"] = strings.Join(details, " ")
   175  	}
   176  
   177  	return m
   178  }
   179  
   180  func logAndContinue(p interface{}, logger grip.Journaler, msg message.Composer) {
   181  	_ = msg.Annotate("panic", panicString(p))
   182  	_ = msg.Annotate("stack", message.NewStack(3, "").Raw().(message.StackTrace).Frames)
   183  	_ = msg.Annotate(message.FieldsMsgName, "hit panic; recovering")
   184  
   185  	logger.Alert(msg)
   186  }
   187  
   188  func logAndExit(p interface{}, logger grip.Journaler, msg message.Composer) {
   189  	_ = msg.Annotate("panic", panicString(p))
   190  	_ = msg.Annotate("stack", message.NewStack(3, "").Raw().(message.StackTrace).Frames)
   191  	_ = msg.Annotate(message.FieldsMsgName, "hit panic; exiting")
   192  
   193  	// check this env var so that we can avoid exiting in the test.
   194  	if os.Getenv(killOverrideVarName) == "" {
   195  		logger.EmergencyFatal(msg)
   196  	} else {
   197  		logger.Emergency(msg)
   198  	}
   199  }
   200  
   201  func handleWithError(p error, err error, logger grip.Journaler, msg message.Composer) {
   202  	_ = msg.Annotate("panic", p.Error())
   203  	_ = msg.Annotate("stack", message.NewStack(3, "").Raw().(message.StackTrace).Frames)
   204  	_ = msg.Annotate(message.FieldsMsgName, "hit panic; adding error")
   205  
   206  	if err != nil {
   207  		_ = msg.Annotate("error", err.Error())
   208  	}
   209  
   210  	logger.Alert(msg)
   211  }