package logging import ( "fmt" "io" "log" "os" "strings" "syscall" // go.etcd.io/etcd imports capnslog, which calls log.SetOutput in its // init() function, so importing it here means that our log.SetOutput // wins. this is fixed in coreos v3.5, which is not released yet. See // https://github.com/etcd-io/etcd/issues/12498 for more information. _ "github.com/coreos/pkg/capnslog" "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" ) 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, } ) 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 } // 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 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 }