diff --git a/log.go b/log.go new file mode 100644 index 000000000..d655d8d39 --- /dev/null +++ b/log.go @@ -0,0 +1,29 @@ +package main + +import ( + "io" + "os" +) + +// These are the environmental variables that determine if we log, and if +// we log whether or not the log should go to a file. +const EnvLog = "TF_LOG" +const EnvLogFile = "TF_LOG_PATH" + +// logOutput determines where we should send logs (if anywhere). +func logOutput() (logOutput io.Writer, err error) { + logOutput = nil + if os.Getenv(EnvLog) != "" { + logOutput = os.Stderr + + if logPath := os.Getenv(EnvLogFile); logPath != "" { + var err error + logOutput, err = os.Create(logPath) + if err != nil { + return nil, err + } + } + } + + return +} diff --git a/main.go b/main.go index 6a2d88aab..0831428c4 100644 --- a/main.go +++ b/main.go @@ -6,7 +6,9 @@ import ( "log" "os" + "github.com/ActiveState/tail" "github.com/mitchellh/cli" + "github.com/mitchellh/panicwrap" ) func main() { @@ -14,7 +16,79 @@ func main() { } func realMain() int { - log.SetOutput(ioutil.Discard) + var wrapConfig panicwrap.WrapConfig + + if !panicwrap.Wrapped(&wrapConfig) { + // Determine where logs should go in general (requested by the user) + logWriter, err := logOutput() + if err != nil { + fmt.Fprintf(os.Stderr, "Couldn't setup log output: %s", err) + return 1 + } + + // We always send logs to a temporary file that we use in case + // there is a panic. Otherwise, we delete it. + logTempFile, err := ioutil.TempFile("", "terraform-log") + if err != nil { + fmt.Fprintf(os.Stderr, "Couldn't setup logging tempfile: %s", err) + return 1 + } + logTempFile.Close() + defer os.Remove(logTempFile.Name()) + + // Tell the logger to log to this file + os.Setenv(EnvLog, "1") + os.Setenv(EnvLogFile, logTempFile.Name()) + + if logWriter != nil { + // Start tailing the file beforehand to get the data + t, err := tail.TailFile(logTempFile.Name(), tail.Config{ + Follow: true, + Logger: tail.DiscardingLogger, + MustExist: true, + }) + if err != nil { + fmt.Fprintf(os.Stderr, "Couldn't setup logging tempfile: %s", err) + return 1 + } + go func() { + for line := range t.Lines { + logWriter.Write([]byte(line.Text + "\n")) + } + }() + } + + // Create the configuration for panicwrap and wrap our executable + wrapConfig.Handler = panicHandler(logTempFile) + exitStatus, err := panicwrap.Wrap(&wrapConfig) + if err != nil { + fmt.Fprintf(os.Stderr, "Couldn't start Terraform: %s", err) + return 1 + } + + // If >= 0, we're the parent, so just exit + if exitStatus >= 0 { + return exitStatus + } + + // We're the child, so just close the tempfile we made in order to + // save file handles since the tempfile is only used by the parent. + logTempFile.Close() + } + + // Call the real main + return wrappedMain() +} + +func wrappedMain() int { + logOutput, err := logOutput() + if err != nil { + fmt.Fprintf(os.Stderr, "Error starting Terraform: %s", err) + return 1 + } + if logOutput != nil { + log.SetOutput(logOutput) + } // Get the command line args. We shortcut "--version" and "-v" to // just show the version. diff --git a/panic.go b/panic.go new file mode 100644 index 000000000..d8e4b9eb9 --- /dev/null +++ b/panic.go @@ -0,0 +1,63 @@ +package main + +import ( + "fmt" + "io" + "os" + "strings" + + "github.com/mitchellh/panicwrap" +) + +// This output is shown if a panic happens. +const panicOutput = ` + +!!!!!!!!!!!!!!!!!!!!!!!!!!! TERRAFORM CRASH !!!!!!!!!!!!!!!!!!!!!!!!!!!! + +Terraform crashed! This is always indicative of a bug within Terraform. +A crash log has been placed at "crash.log" relative to your current +working directory. It would be immensely helpful if you could please +report the crash with Terraform[1] so that we can fix this. + +[1]: https://github.com/hashicorp/terraform/issues + +!!!!!!!!!!!!!!!!!!!!!!!!!!! TERRAFORM CRASH !!!!!!!!!!!!!!!!!!!!!!!!!!!! +` + +// panicHandler is what is called by panicwrap when a panic is encountered +// within Terraform. It is guaranteed to run after the resulting process has +// exited so we can take the log file, add in the panic, and store it +// somewhere locally. +func panicHandler(logF *os.File) panicwrap.HandlerFunc { + return func(m string) { + // Right away just output this thing on stderr so that it gets + // shown in case anything below fails. + fmt.Fprintf(os.Stderr, fmt.Sprintf("%s\n", m)) + + // Create the crash log file where we'll write the logs + f, err := os.Create("crash.log") + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to create crash log file: %s", err) + return + } + defer f.Close() + + // Seek the log file back to the beginning + if _, err = logF.Seek(0, 0); err != nil { + fmt.Fprintf(os.Stderr, "Failed to seek log file for crash: %s", err) + return + } + + // Copy the contents to the crash file. This will include + // the panic that just happened. + if _, err = io.Copy(f, logF); err != nil { + fmt.Fprintf(os.Stderr, "Failed to write crash log: %s", err) + return + } + + // Tell the user a crash occurred in some helpful way that + // they'll hopefully notice. + fmt.Printf("\n\n") + fmt.Println(strings.TrimSpace(panicOutput)) + } +}