| // Copyright IBM Corp. 2014, 2026 | |
| // SPDX-License-Identifier: BUSL-1.1 | |
| package logging | |
| import ( | |
| "fmt" | |
| "os" | |
| "runtime/debug" | |
| "strings" | |
| "sync" | |
| "syscall" | |
| "github.com/hashicorp/go-hclog" | |
| ) | |
| // This output is shown if a panic happens. | |
| const panicOutput = ` | |
| !!!!!!!!!!!!!!!!!!!!!!!!!!! TERRAFORM CRASH !!!!!!!!!!!!!!!!!!!!!!!!!!!! | |
| Terraform crashed! This is always indicative of a bug within Terraform. | |
| Please report the crash with Terraform[1] so that we can fix this. | |
| When reporting bugs, please include your terraform version, the stack trace | |
| shown below, and any additional information which may help replicate the issue. | |
| [1]: https://github.com/hashicorp/terraform/issues | |
| !!!!!!!!!!!!!!!!!!!!!!!!!!! TERRAFORM CRASH !!!!!!!!!!!!!!!!!!!!!!!!!!!! | |
| ` | |
| // In case multiple goroutines panic concurrently, ensure only the first one | |
| // recovered by PanicHandler starts printing. | |
| var panicMutex sync.Mutex | |
| // PanicHandler is called to recover from an internal panic in Terraform, and | |
| // augments the standard stack trace with a more user friendly error message. | |
| // PanicHandler must be called as a defered function, and must be the first | |
| // defer called at the start of a new goroutine. | |
| func PanicHandler() { | |
| // Have all managed goroutines checkin here, and prevent them from exiting | |
| // if there's a panic in progress. While this can't lock the entire runtime | |
| // to block progress, we can prevent some cases where Terraform may return | |
| // early before the panic has been printed out. | |
| panicMutex.Lock() | |
| defer panicMutex.Unlock() | |
| recovered := recover() | |
| if recovered == nil { | |
| return | |
| } | |
| // We're aiming to behave as much as possible like the built-in panic | |
| // handler aside from our few intentional exceptions. | |
| // | |
| // One detail is that we want the panic information to definitely go to the | |
| // real stderr even if something else in our process has rudely reassigned | |
| // [`os.Stderr`] to point to something else. Software that's designed to | |
| // monitor the output of Go programs to detect and report panics expects | |
| // the panic message to appear on the real process stderr. | |
| // | |
| // (At the time of writing, the go-plugin Serve function is an example | |
| // of modifying the global os.Stderr, causing it to get routed over a | |
| // plugin-specific stream rather than to the real process stderr. If | |
| // we used os.Stderr here then panics under "terraform rpcapi" would | |
| // end up in the wrong place.) | |
| // | |
| // The following mimics how the standard library (package os) constructs | |
| // os.Stderr in the first place. Technically even this syscall.Stderr | |
| // can be overridden rudely at runtime, but thankfully we've not yet | |
| // encountered anything linked into Terraform that does _that_! | |
| stderr := os.NewFile(uintptr(syscall.Stderr), "/dev/stderr") | |
| if stderr == nil { | |
| // os.NewFile has a few esoteric error cases where it'll return nil, | |
| // in which case we'll just do our best with whatever happens to | |
| // be in os.Stderr right now as a last resort. | |
| stderr = os.Stderr | |
| } | |
| fmt.Fprint(stderr, panicOutput) | |
| fmt.Fprint(stderr, "panic: ", recovered, "\n") | |
| // The following mimics the implementation of debug.PrintStack, but | |
| // without the hard-coded reference to os.Stderr. | |
| stderr.Write(debug.Stack()) | |
| // An exit code of 11 keeps us out of the way of the detailed exitcodes | |
| // from plan, and also happens to be the same code as SIGSEGV which is | |
| // roughly the same type of condition that causes most panics. | |
| os.Exit(11) | |
| } | |
| const pluginPanicOutput = ` | |
| Stack trace from the %[1]s plugin: | |
| %s | |
| Error: The %[1]s plugin crashed! | |
| This is always indicative of a bug within the plugin. It would be immensely | |
| helpful if you could report the crash with the plugin's maintainers so that it | |
| can be fixed. The output above should help diagnose the issue. | |
| ` | |
| // PluginPanics returns a series of provider panics that were collected during | |
| // execution, and formatted for output. | |
| func PluginPanics() []string { | |
| return panics.allPanics() | |
| } | |
| // panicRecorder provides a registry to check for plugin panics that may have | |
| // happened when a plugin suddenly terminates. | |
| type panicRecorder struct { | |
| sync.Mutex | |
| // panics maps the plugin name to the panic output lines received from | |
| // the logger. | |
| panics map[string][]string | |
| // maxLines is the max number of lines we'll record after seeing a | |
| // panic header. Since this is going to be printed in the UI output, we | |
| // don't want to destroy the scrollback. In most cases, the first few lines | |
| // of the stack trace is all that are required. | |
| maxLines int | |
| } | |
| // registerPlugin returns an accumulator function which will accept lines of | |
| // a panic stack trace to collect into an error when requested. | |
| func (p *panicRecorder) registerPlugin(name string) func(string) { | |
| p.Lock() | |
| defer p.Unlock() | |
| // In most cases we shouldn't be starting a plugin if it already | |
| // panicked, but clear out previous entries just in case. | |
| delete(p.panics, name) | |
| count := 0 | |
| // this callback is used by the logger to store panic output | |
| return func(line string) { | |
| p.Lock() | |
| defer p.Unlock() | |
| // stop recording if there are too many lines. | |
| if count > p.maxLines { | |
| return | |
| } | |
| count++ | |
| p.panics[name] = append(p.panics[name], line) | |
| } | |
| } | |
| func (p *panicRecorder) allPanics() []string { | |
| p.Lock() | |
| defer p.Unlock() | |
| var res []string | |
| for name, lines := range p.panics { | |
| if len(lines) == 0 { | |
| continue | |
| } | |
| res = append(res, fmt.Sprintf(pluginPanicOutput, name, strings.Join(lines, "\n"))) | |
| } | |
| return res | |
| } | |
| // logPanicWrapper wraps an hclog.Logger and intercepts and records any output | |
| // that appears to be a panic. | |
| type logPanicWrapper struct { | |
| hclog.Logger | |
| panicRecorder func(string) | |
| inPanic bool | |
| // we need to enable at least error level logging to see stack traces from | |
| // go-plugin, but if the user hasn't requested any logging we will only | |
| // record them and not pass them through. | |
| captureOnly bool | |
| } | |
| // go-plugin will create a new named logger for each plugin binary. | |
| func (l *logPanicWrapper) Named(name string) hclog.Logger { | |
| wrapped := &logPanicWrapper{ | |
| Logger: l.Logger.Named(name), | |
| panicRecorder: panics.registerPlugin(name), | |
| } | |
| if wrapped.Logger.GetLevel() == hclog.Off { | |
| // if we're not logging anything from the provider, we need to set the | |
| // level to error in order to capture stack traces. | |
| wrapped.captureOnly = true | |
| wrapped.Logger.SetLevel(hclog.Error) | |
| } | |
| return wrapped | |
| } | |
| // We only need to implement Error, since that is the level used by go-plugin | |
| // when encountering a stack trace on stderr. | |
| func (l *logPanicWrapper) Error(msg string, args ...interface{}) { | |
| // We don't have access to the binary itself, so guess based on the log | |
| // stream output if this is the start of the traceback. An occasional false | |
| // positive shouldn't be a big deal, since this is only retrieved after an | |
| // error of some sort. | |
| panicPrefix := strings.HasPrefix(msg, "panic: ") || strings.HasPrefix(msg, "fatal error: ") | |
| l.inPanic = l.inPanic || panicPrefix | |
| if l.inPanic && l.panicRecorder != nil { | |
| l.panicRecorder(msg) | |
| } | |
| if !l.captureOnly { | |
| // pass the message through to the real logger implementation if we're | |
| // not just watching for stack traces | |
| l.Logger.Error(msg, args...) | |
| } | |
| } |