github.com/jaredpalmer/terraform@v1.1.0-alpha20210908.0.20210911170307-88705c943a03/internal/logging/panic.go (about)

     1  package logging
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"io/ioutil"
     7  	"os"
     8  	"strings"
     9  	"sync"
    10  
    11  	"github.com/hashicorp/go-hclog"
    12  	"github.com/mitchellh/panicwrap"
    13  )
    14  
    15  // This output is shown if a panic happens.
    16  const panicOutput = `
    17  
    18  !!!!!!!!!!!!!!!!!!!!!!!!!!! TERRAFORM CRASH !!!!!!!!!!!!!!!!!!!!!!!!!!!!
    19  
    20  Terraform crashed! This is always indicative of a bug within Terraform.
    21  A crash log has been placed at %[1]q relative to your current
    22  working directory. It would be immensely helpful if you could please
    23  report the crash with Terraform[1] so that we can fix this.
    24  
    25  When reporting bugs, please include your terraform version. That
    26  information is available on the first line of crash.log. You can also
    27  get it by running 'terraform --version' on the command line.
    28  
    29  SECURITY WARNING: the %[1]q file that was created may contain 
    30  sensitive information that must be redacted before it is safe to share 
    31  on the issue tracker.
    32  
    33  [1]: https://github.com/hashicorp/terraform/issues
    34  
    35  !!!!!!!!!!!!!!!!!!!!!!!!!!! TERRAFORM CRASH !!!!!!!!!!!!!!!!!!!!!!!!!!!!
    36  `
    37  
    38  // panicHandler is what is called by panicwrap when a panic is encountered
    39  // within Terraform. It is guaranteed to run after the resulting process has
    40  // exited so we can take the log file, add in the panic, and store it
    41  // somewhere locally.
    42  func PanicHandler(tmpLogPath string) panicwrap.HandlerFunc {
    43  	return func(m string) {
    44  		// Create the crash log file where we'll write the logs
    45  		f, err := ioutil.TempFile(".", "crash.*.log")
    46  		if err != nil {
    47  			fmt.Fprintf(os.Stderr, "Failed to create crash log file: %s", err)
    48  			return
    49  		}
    50  		defer f.Close()
    51  
    52  		tmpLog, err := os.Open(tmpLogPath)
    53  		if err != nil {
    54  			fmt.Fprintf(os.Stderr, "Failed to open log file %q: %v\n", tmpLogPath, err)
    55  			return
    56  		}
    57  		defer tmpLog.Close()
    58  
    59  		// Copy the contents to the crash file. This will include
    60  		// the panic that just happened.
    61  		if _, err = io.Copy(f, tmpLog); err != nil {
    62  			fmt.Fprintf(os.Stderr, "Failed to write crash log: %s", err)
    63  			return
    64  		}
    65  
    66  		// add the trace back to the log
    67  		f.WriteString("\n" + m)
    68  
    69  		// Tell the user a crash occurred in some helpful way that
    70  		// they'll hopefully notice.
    71  		fmt.Printf("\n\n")
    72  		fmt.Printf(panicOutput, f.Name())
    73  	}
    74  }
    75  
    76  const pluginPanicOutput = `
    77  Stack trace from the %[1]s plugin:
    78  
    79  %s
    80  
    81  Error: The %[1]s plugin crashed!
    82  
    83  This is always indicative of a bug within the plugin. It would be immensely
    84  helpful if you could report the crash with the plugin's maintainers so that it
    85  can be fixed. The output above should help diagnose the issue.
    86  `
    87  
    88  // PluginPanics returns a series of provider panics that were collected during
    89  // execution, and formatted for output.
    90  func PluginPanics() []string {
    91  	return panics.allPanics()
    92  }
    93  
    94  // panicRecorder provides a registry to check for plugin panics that may have
    95  // happened when a plugin suddenly terminates.
    96  type panicRecorder struct {
    97  	sync.Mutex
    98  
    99  	// panics maps the plugin name to the panic output lines received from
   100  	// the logger.
   101  	panics map[string][]string
   102  
   103  	// maxLines is the max number of lines we'll record after seeing a
   104  	// panic header. Since this is going to be printed in the UI output, we
   105  	// don't want to destroy the scrollback. In most cases, the first few lines
   106  	// of the stack trace is all that are required.
   107  	maxLines int
   108  }
   109  
   110  // registerPlugin returns an accumulator function which will accept lines of
   111  // a panic stack trace to collect into an error when requested.
   112  func (p *panicRecorder) registerPlugin(name string) func(string) {
   113  	p.Lock()
   114  	defer p.Unlock()
   115  
   116  	// In most cases we shouldn't be starting a plugin if it already
   117  	// panicked, but clear out previous entries just in case.
   118  	delete(p.panics, name)
   119  
   120  	count := 0
   121  
   122  	// this callback is used by the logger to store panic output
   123  	return func(line string) {
   124  		p.Lock()
   125  		defer p.Unlock()
   126  
   127  		// stop recording if there are too many lines.
   128  		if count > p.maxLines {
   129  			return
   130  		}
   131  		count++
   132  
   133  		p.panics[name] = append(p.panics[name], line)
   134  	}
   135  }
   136  
   137  func (p *panicRecorder) allPanics() []string {
   138  	p.Lock()
   139  	defer p.Unlock()
   140  
   141  	var res []string
   142  	for name, lines := range p.panics {
   143  		if len(lines) == 0 {
   144  			continue
   145  		}
   146  
   147  		res = append(res, fmt.Sprintf(pluginPanicOutput, name, strings.Join(lines, "\n")))
   148  	}
   149  	return res
   150  }
   151  
   152  // logPanicWrapper wraps an hclog.Logger and intercepts and records any output
   153  // that appears to be a panic.
   154  type logPanicWrapper struct {
   155  	hclog.Logger
   156  	panicRecorder func(string)
   157  	inPanic       bool
   158  }
   159  
   160  // go-plugin will create a new named logger for each plugin binary.
   161  func (l *logPanicWrapper) Named(name string) hclog.Logger {
   162  	return &logPanicWrapper{
   163  		Logger:        l.Logger.Named(name),
   164  		panicRecorder: panics.registerPlugin(name),
   165  	}
   166  }
   167  
   168  // we only need to implement Debug, since that is the default output level used
   169  // by go-plugin when encountering unstructured output on stderr.
   170  func (l *logPanicWrapper) Debug(msg string, args ...interface{}) {
   171  	// We don't have access to the binary itself, so guess based on the stderr
   172  	// output if this is the start of the traceback. An occasional false
   173  	// positive shouldn't be a big deal, since this is only retrieved after an
   174  	// error of some sort.
   175  
   176  	panicPrefix := strings.HasPrefix(msg, "panic: ") || strings.HasPrefix(msg, "fatal error: ")
   177  
   178  	l.inPanic = l.inPanic || panicPrefix
   179  
   180  	if l.inPanic && l.panicRecorder != nil {
   181  		l.panicRecorder(msg)
   182  	}
   183  
   184  	// If we have logging turned on, we need to prevent panicwrap from seeing
   185  	// this as a core panic. This can be done by obfuscating the panic error
   186  	// line.
   187  	if panicPrefix {
   188  		colon := strings.Index(msg, ":")
   189  		msg = strings.ToUpper(msg[:colon]) + msg[colon:]
   190  	}
   191  
   192  	l.Logger.Debug(msg, args...)
   193  }