terraform/internal/logging/logging.go
James Bardin 7df949c6c3 add TF_LOG_TRACE to turn off graph trace output
Graph transformations are really only interesting for developers
directly working on graph transformers. Otherwise the complete graph can
be printed once in the trace log for general debugging purposes.
Removing the intermediate graph steps can reduce log output for large
configs by many megabytes per run. On top of the volume of output, the
graph string generation can cause a noticeable impact on performance with
large graphs.
2025-05-22 10:25:55 -04:00

278 lines
6.9 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package logging
import (
"fmt"
"io"
"log"
"os"
"strings"
"syscall"
"github.com/hashicorp/go-hclog"
)
// 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"
envLogFile = "TF_LOG_PATH"
// Allow logging of specific subsystems.
// We only separate core and providers for now, but this could be extended
// to other loggers, like provisioners and remote-state backends.
envLogCore = "TF_LOG_CORE"
envLogProvider = "TF_LOG_PROVIDER"
envLogCloud = "TF_LOG_CLOUD"
envLogStacks = "TF_LOG_STACKS"
// This variable is defined here for consistency, but it is used by the
// graph builder directly to change how much output to generate.
envGraphTrace = "TF_GRAPH_TRACE"
)
var (
// ValidLevels are the log level names that Terraform recognizes.
ValidLevels = []string{"TRACE", "DEBUG", "INFO", "WARN", "ERROR", "OFF"}
// logger is the global hclog logger
logger hclog.Logger
// logWriter is a global writer for logs, to be used with the std log package
logWriter io.Writer
// initialize our cache of panic output from providers
panics = &panicRecorder{
panics: make(map[string][]string),
maxLines: 100,
}
GraphTrace = os.Getenv(envGraphTrace) != ""
)
func init() {
logger = newHCLogger("")
logWriter = logger.StandardWriter(&hclog.StandardLoggerOptions{InferLevels: true})
// set up the default std library logger to use our output
log.SetFlags(0)
log.SetPrefix("")
log.SetOutput(logWriter)
}
// SetupTempLog adds a new log sink which writes all logs to the given file.
func RegisterSink(f *os.File) {
l, ok := logger.(hclog.InterceptLogger)
if !ok {
panic("global logger is not an InterceptLogger")
}
if f == nil {
return
}
l.RegisterSink(hclog.NewSinkAdapter(&hclog.LoggerOptions{
Level: hclog.Trace,
Output: f,
}))
}
// LogOutput return the default global log io.Writer
func LogOutput() io.Writer {
return logWriter
}
// HCLogger returns the default global hclog logger
func HCLogger() hclog.Logger {
return logger
}
// newHCLogger returns a new hclog.Logger instance with the given name
func newHCLogger(name string) hclog.Logger {
logOutput := io.Writer(os.Stderr)
logLevel, json := globalLogLevel()
if logPath := os.Getenv(envLogFile); logPath != "" {
f, err := os.OpenFile(logPath, syscall.O_CREAT|syscall.O_RDWR|syscall.O_APPEND, 0666)
if err != nil {
fmt.Fprintf(os.Stderr, "Error opening log file: %v\n", err)
} else {
logOutput = f
}
}
return hclog.NewInterceptLogger(&hclog.LoggerOptions{
Name: name,
Level: logLevel,
Output: logOutput,
IndependentLevels: true,
JSONFormat: json,
})
}
// NewLogger returns a new logger based in the current global logger, with the
// given name appended.
func NewLogger(name string) hclog.Logger {
if name == "" {
panic("logger name required")
}
return &logPanicWrapper{
Logger: logger.Named(name),
}
}
// NewProviderLogger returns a logger for the provider plugin, possibly with a
// different log level from the global logger.
func NewProviderLogger(prefix string) hclog.Logger {
l := &logPanicWrapper{
Logger: logger.Named(prefix + "provider"),
}
level := providerLogLevel()
logger.Debug("created provider logger", "level", level)
l.SetLevel(level)
return l
}
// NewCloudLogger returns a logger for the cloud plugin, possibly with a
// different log level from the global logger.
func NewCloudLogger() hclog.Logger {
l := &logPanicWrapper{
Logger: logger.Named("cloud"),
}
level := cloudLogLevel()
logger.Debug("created cloud logger", "level", level)
l.SetLevel(level)
return l
}
// NewStacksCLILogger returns a logger for the StacksCLI plugin, possibly with a
// different log level from the global logger.
func NewStacksLogger() hclog.Logger {
l := &logPanicWrapper{
Logger: logger.Named("stacks"),
}
level := stacksLogLevel()
logger.Debug("created stacks logger", "level", level)
l.SetLevel(level)
return l
}
// CurrentLogLevel returns the current log level string based the environment vars
func CurrentLogLevel() string {
ll, _ := globalLogLevel()
return strings.ToUpper(ll.String())
}
func providerLogLevel() hclog.Level {
providerEnvLevel := strings.ToUpper(os.Getenv(envLogProvider))
if providerEnvLevel == "" {
providerEnvLevel = strings.ToUpper(os.Getenv(envLog))
}
return parseLogLevel(providerEnvLevel)
}
func stacksLogLevel() hclog.Level {
pluginEnvLevel := strings.ToUpper(os.Getenv(envLogStacks))
if pluginEnvLevel == "" {
pluginEnvLevel = strings.ToUpper(os.Getenv(envLog))
}
return parseLogLevel(pluginEnvLevel)
}
func cloudLogLevel() hclog.Level {
providerEnvLevel := strings.ToUpper(os.Getenv(envLogCloud))
if providerEnvLevel == "" {
providerEnvLevel = strings.ToUpper(os.Getenv(envLog))
}
return parseLogLevel(providerEnvLevel)
}
func globalLogLevel() (hclog.Level, bool) {
var json bool
envLevel := strings.ToUpper(os.Getenv(envLog))
if envLevel == "" {
envLevel = strings.ToUpper(os.Getenv(envLogCore))
}
if envLevel == "JSON" {
json = true
}
return parseLogLevel(envLevel), json
}
func parseLogLevel(envLevel string) hclog.Level {
if envLevel == "" {
return hclog.Off
}
if envLevel == "JSON" {
envLevel = "TRACE"
}
logLevel := hclog.Trace
if isValidLogLevel(envLevel) {
logLevel = hclog.LevelFromString(envLevel)
} else {
fmt.Fprintf(os.Stderr, "[WARN] Invalid log level: %q. Defaulting to level: TRACE. Valid levels are: %+v",
envLevel, ValidLevels)
}
return logLevel
}
// IsDebugOrHigher returns whether or not the current log level is debug or trace
func IsDebugOrHigher() bool {
level, _ := globalLogLevel()
return level == hclog.Debug || level == hclog.Trace
}
func isValidLogLevel(level string) bool {
for _, l := range ValidLevels {
if level == string(l) {
return true
}
}
return false
}
// PluginOutputMonitor creates an io.Writer that will warn about any writes in
// the default logger. This is used to catch unexpected output from plugins,
// notifying them about the problem as well as surfacing the lost data for
// context.
func PluginOutputMonitor(source string) io.Writer {
return pluginOutputMonitor{
source: source,
log: logger,
}
}
// pluginOutputMonitor is an io.Writer that logs all writes as
// "unexpected data" with the source name.
type pluginOutputMonitor struct {
source string
log hclog.Logger
}
func (w pluginOutputMonitor) Write(d []byte) (int, error) {
// Limit the write size to 1024 bytes We're not expecting any data to come
// through this channel, so accidental writes will usually be stray fmt
// debugging statements and the like, but we want to provide context to the
// provider to indicate what the unexpected data might be.
n := len(d)
if n > 1024 {
d = append(d[:1024], '.', '.', '.')
}
w.log.Warn("unexpected data", w.source, strings.TrimSpace(string(d)))
return n, nil
}