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 }