main: Log about reliance on GODEBUG settings

The Go runtime provides a number of configuration knobs that subtly change
its behavior, and we cannot control which of these is available over time
as we change which version of Go we're building with.

It's therefore possible that an OpenTofu user might be intentionally or
unintentionally relying on one of these settings for OpenTofu to work on
their system, in which case they would be broken if they upgraded to a
newer version of OpenTofu which uses a different Go version that no longer
supports that setting.

These log lines are intended to help us more quickly notice that
possibility if someone opens a bug report describing an unexpected behavior
change after upgrading to a new OpenTofu minor release series. We can ask
the reporter to share "TF_LOG=debug" output from both the previous and new
releases to compare, and then these log lines should appear in the older
version's output so we can search the Go codebase and issue tracker for
each of the mentioned names to learn if the handling of that setting has
changed between Go versions.

Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
This commit is contained in:
Martin Atkins
2025-12-17 10:32:04 -08:00
parent 4f99815163
commit f21bd00a1b
2 changed files with 59 additions and 0 deletions

View File

@@ -380,6 +380,12 @@ func realMain() int {
return 1
}
// We might generate some additional log lines if OpenTofu relied on any
// non-default Go runtime behaviors enabled by GODEBUG settings, because
// they might be relevant when trying to reproduce certain problems for
// debugging or bug reporting purposes.
logGodebugUsage()
// if we are exiting with a non-zero code, check if it was caused by any
// plugins crashing
if exitCode != 0 {

View File

@@ -6,9 +6,62 @@
package main
import (
"log"
"runtime/metrics"
"strings"
"github.com/opentofu/opentofu/internal/logging"
"github.com/opentofu/opentofu/version"
)
var Version = version.Version
var VersionPrerelease = version.Prerelease
// logGodebugUsage produces extra DEBUG log lines if the Go runtime's metrics
// suggest that code that was run so far relied on any non-default "GODEBUG"
// settings, which could be helpful in reproducing a bug report if the
// behavior differs based on such a setting.
//
// For this to be useful it must be run just before we're about to exit, after
// we've already performed all of the requested work.
func logGodebugUsage() {
// These constants reflect the documented conventions from the
// runtime/metrics package, so we can filter for only the metrics that
// are relevant to this function.
const godebugMetricPrefix = "/godebug/non-default-behavior/"
const godebugMetricSuffix = ":events"
if !logging.IsDebugOrHigher() {
// No point in doing any of this work if the log lines are going to
// be filtered out anyway.
return
}
metricDescs := metrics.All()
for _, metric := range metricDescs {
if !metric.Cumulative || metric.Kind != metrics.KindUint64 {
// godebug counters are always cumulative uint64, so this quickly
// filters some irrelevant metrics before we do any string
// comparisons.
continue
}
if !strings.HasPrefix(metric.Name, godebugMetricPrefix) {
continue // irrelevant metric
}
if !strings.HasSuffix(metric.Name, godebugMetricSuffix) {
continue // irrelevant metric
}
// The metrics API is designed for applications that want to periodically
// re-extract the same set of metrics in a long-running application by
// reusing a preallocated buffer, but we don't have that need here and so
// we'll just read one metric at a time into a single-element array.
var samples [1]metrics.Sample
samples[0].Name = metric.Name
metrics.Read(samples[:])
if count := samples[0].Value.Uint64(); count != 0 {
name := metric.Name[len(godebugMetricPrefix) : len(metric.Name)-len(godebugMetricSuffix)]
log.Printf("[DEBUG] Relied on GODEBUG %q %d times during this execution; behavior might change in future OpenTofu versions", name, count)
}
}
}