github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/logging/logging.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package logging
     5  
     6  import (
     7  	"fmt"
     8  	"io"
     9  	"log"
    10  	"os"
    11  	"strings"
    12  	"syscall"
    13  
    14  	// go.etcd.io/etcd imports capnslog, which calls log.SetOutput in its
    15  	// init() function, so importing it here means that our log.SetOutput
    16  	// wins. this is fixed in coreos v3.5, which is not released yet. See
    17  	// https://github.com/etcd-io/etcd/issues/12498 for more information.
    18  	_ "github.com/coreos/pkg/capnslog"
    19  	"github.com/hashicorp/go-hclog"
    20  )
    21  
    22  // These are the environmental variables that determine if we log, and if
    23  // we log whether or not the log should go to a file.
    24  const (
    25  	envLog     = "TF_LOG"
    26  	envLogFile = "TF_LOG_PATH"
    27  
    28  	// Allow logging of specific subsystems.
    29  	// We only separate core and providers for now, but this could be extended
    30  	// to other loggers, like provisioners and remote-state backends.
    31  	envLogCore     = "TF_LOG_CORE"
    32  	envLogProvider = "TF_LOG_PROVIDER"
    33  	envLogCloud    = "TF_LOG_CLOUD"
    34  )
    35  
    36  var (
    37  	// ValidLevels are the log level names that Terraform recognizes.
    38  	ValidLevels = []string{"TRACE", "DEBUG", "INFO", "WARN", "ERROR", "OFF"}
    39  
    40  	// logger is the global hclog logger
    41  	logger hclog.Logger
    42  
    43  	// logWriter is a global writer for logs, to be used with the std log package
    44  	logWriter io.Writer
    45  
    46  	// initialize our cache of panic output from providers
    47  	panics = &panicRecorder{
    48  		panics:   make(map[string][]string),
    49  		maxLines: 100,
    50  	}
    51  )
    52  
    53  func init() {
    54  	logger = newHCLogger("")
    55  	logWriter = logger.StandardWriter(&hclog.StandardLoggerOptions{InferLevels: true})
    56  
    57  	// set up the default std library logger to use our output
    58  	log.SetFlags(0)
    59  	log.SetPrefix("")
    60  	log.SetOutput(logWriter)
    61  }
    62  
    63  // SetupTempLog adds a new log sink which writes all logs to the given file.
    64  func RegisterSink(f *os.File) {
    65  	l, ok := logger.(hclog.InterceptLogger)
    66  	if !ok {
    67  		panic("global logger is not an InterceptLogger")
    68  	}
    69  
    70  	if f == nil {
    71  		return
    72  	}
    73  
    74  	l.RegisterSink(hclog.NewSinkAdapter(&hclog.LoggerOptions{
    75  		Level:  hclog.Trace,
    76  		Output: f,
    77  	}))
    78  }
    79  
    80  // LogOutput return the default global log io.Writer
    81  func LogOutput() io.Writer {
    82  	return logWriter
    83  }
    84  
    85  // HCLogger returns the default global hclog logger
    86  func HCLogger() hclog.Logger {
    87  	return logger
    88  }
    89  
    90  // newHCLogger returns a new hclog.Logger instance with the given name
    91  func newHCLogger(name string) hclog.Logger {
    92  	logOutput := io.Writer(os.Stderr)
    93  	logLevel, json := globalLogLevel()
    94  
    95  	if logPath := os.Getenv(envLogFile); logPath != "" {
    96  		f, err := os.OpenFile(logPath, syscall.O_CREAT|syscall.O_RDWR|syscall.O_APPEND, 0666)
    97  		if err != nil {
    98  			fmt.Fprintf(os.Stderr, "Error opening log file: %v\n", err)
    99  		} else {
   100  			logOutput = f
   101  		}
   102  	}
   103  
   104  	return hclog.NewInterceptLogger(&hclog.LoggerOptions{
   105  		Name:              name,
   106  		Level:             logLevel,
   107  		Output:            logOutput,
   108  		IndependentLevels: true,
   109  		JSONFormat:        json,
   110  	})
   111  }
   112  
   113  // NewLogger returns a new logger based in the current global logger, with the
   114  // given name appended.
   115  func NewLogger(name string) hclog.Logger {
   116  	if name == "" {
   117  		panic("logger name required")
   118  	}
   119  	return &logPanicWrapper{
   120  		Logger: logger.Named(name),
   121  	}
   122  }
   123  
   124  // NewProviderLogger returns a logger for the provider plugin, possibly with a
   125  // different log level from the global logger.
   126  func NewProviderLogger(prefix string) hclog.Logger {
   127  	l := &logPanicWrapper{
   128  		Logger: logger.Named(prefix + "provider"),
   129  	}
   130  
   131  	level := providerLogLevel()
   132  	logger.Debug("created provider logger", "level", level)
   133  
   134  	l.SetLevel(level)
   135  	return l
   136  }
   137  
   138  // NewCloudLogger returns a logger for the cloud plugin, possibly with a
   139  // different log level from the global logger.
   140  func NewCloudLogger() hclog.Logger {
   141  	l := &logPanicWrapper{
   142  		Logger: logger.Named("cloud"),
   143  	}
   144  
   145  	level := cloudLogLevel()
   146  	logger.Debug("created cloud logger", "level", level)
   147  
   148  	l.SetLevel(level)
   149  	return l
   150  }
   151  
   152  // CurrentLogLevel returns the current log level string based the environment vars
   153  func CurrentLogLevel() string {
   154  	ll, _ := globalLogLevel()
   155  	return strings.ToUpper(ll.String())
   156  }
   157  
   158  func providerLogLevel() hclog.Level {
   159  	providerEnvLevel := strings.ToUpper(os.Getenv(envLogProvider))
   160  	if providerEnvLevel == "" {
   161  		providerEnvLevel = strings.ToUpper(os.Getenv(envLog))
   162  	}
   163  
   164  	return parseLogLevel(providerEnvLevel)
   165  }
   166  
   167  func cloudLogLevel() hclog.Level {
   168  	providerEnvLevel := strings.ToUpper(os.Getenv(envLogCloud))
   169  	if providerEnvLevel == "" {
   170  		providerEnvLevel = strings.ToUpper(os.Getenv(envLog))
   171  	}
   172  
   173  	return parseLogLevel(providerEnvLevel)
   174  }
   175  
   176  func globalLogLevel() (hclog.Level, bool) {
   177  	var json bool
   178  	envLevel := strings.ToUpper(os.Getenv(envLog))
   179  	if envLevel == "" {
   180  		envLevel = strings.ToUpper(os.Getenv(envLogCore))
   181  	}
   182  	if envLevel == "JSON" {
   183  		json = true
   184  	}
   185  	return parseLogLevel(envLevel), json
   186  }
   187  
   188  func parseLogLevel(envLevel string) hclog.Level {
   189  	if envLevel == "" {
   190  		return hclog.Off
   191  	}
   192  	if envLevel == "JSON" {
   193  		envLevel = "TRACE"
   194  	}
   195  
   196  	logLevel := hclog.Trace
   197  	if isValidLogLevel(envLevel) {
   198  		logLevel = hclog.LevelFromString(envLevel)
   199  	} else {
   200  		fmt.Fprintf(os.Stderr, "[WARN] Invalid log level: %q. Defaulting to level: TRACE. Valid levels are: %+v",
   201  			envLevel, ValidLevels)
   202  	}
   203  
   204  	return logLevel
   205  }
   206  
   207  // IsDebugOrHigher returns whether or not the current log level is debug or trace
   208  func IsDebugOrHigher() bool {
   209  	level, _ := globalLogLevel()
   210  	return level == hclog.Debug || level == hclog.Trace
   211  }
   212  
   213  func isValidLogLevel(level string) bool {
   214  	for _, l := range ValidLevels {
   215  		if level == string(l) {
   216  			return true
   217  		}
   218  	}
   219  
   220  	return false
   221  }
   222  
   223  // PluginOutputMonitor creates an io.Writer that will warn about any writes in
   224  // the default logger. This is used to catch unexpected output from plugins,
   225  // notifying them about the problem as well as surfacing the lost data for
   226  // context.
   227  func PluginOutputMonitor(source string) io.Writer {
   228  	return pluginOutputMonitor{
   229  		source: source,
   230  		log:    logger,
   231  	}
   232  }
   233  
   234  // pluginOutputMonitor is an io.Writer that logs all writes as
   235  // "unexpected data" with the source name.
   236  type pluginOutputMonitor struct {
   237  	source string
   238  	log    hclog.Logger
   239  }
   240  
   241  func (w pluginOutputMonitor) Write(d []byte) (int, error) {
   242  	// Limit the write size to 1024 bytes We're not expecting any data to come
   243  	// through this channel, so accidental writes will usually be stray fmt
   244  	// debugging statements and the like, but we want to provide context to the
   245  	// provider to indicate what the unexpected data might be.
   246  	n := len(d)
   247  	if n > 1024 {
   248  		d = append(d[:1024], '.', '.', '.')
   249  	}
   250  
   251  	w.log.Warn("unexpected data", w.source, strings.TrimSpace(string(d)))
   252  	return n, nil
   253  }