github.com/swiftstack/proxyfs@v0.0.0-20201223034610-5434d919416e/logger/config.go (about) 1 package logger 2 3 import ( 4 "fmt" 5 "io" 6 "os" 7 "regexp" 8 "strings" 9 10 log "github.com/sirupsen/logrus" 11 12 "github.com/swiftstack/ProxyFS/conf" 13 ) 14 15 // RFC3339 format with microsecond precision 16 // 17 const timeFormat = "2006-01-02T15:04:05.000000Z07:00" 18 19 var logFile *os.File = nil 20 21 // multiWriter groups multiple io.Writers into a single io.Writer. (Our 22 // immediate motivation for this is that logrus's SetOutput expects an 23 // io.Writer, but we might want to log to both the console and a file in 24 // development, and this seems potentially simpler in some aspects than 25 // defining a Hook. We may want to revisit this judgement—again—later. 26 // 27 // Supporting multiple writers is now a full-fledged feature of logger. 28 // 29 type multiWriter struct { 30 writers []io.Writer 31 } 32 33 // The global list of log targets to write to. 34 // 35 // logTargets should probably be protected by a lock or use some clever RCU 36 // update technique, but its really only changed for test cases. 37 // 38 var logTargets multiWriter 39 40 func (mw *multiWriter) addWriter(writer io.Writer) { 41 mw.writers = append(mw.writers, writer) 42 } 43 44 func (mw *multiWriter) Write(p []byte) (n int, err error) { 45 for _, writer := range mw.writers { 46 n, err = writer.Write(p) 47 // regrettably, the first thing that goes wrong determines our return 48 // values 49 if err != nil { 50 return 51 } 52 } 53 return 54 } 55 56 func (mw *multiWriter) Clear() { 57 mw.writers = []io.Writer{} 58 } 59 60 func addLogTarget(writer io.Writer) { 61 logTargets.addWriter(writer) 62 } 63 64 // This is used by LogTarget, which is a logging target that is useful for 65 // capturing the output logged by other packages for testing in test cases. 66 // 67 func (log LogTarget) write(p []byte) (n int, err error) { 68 log.LogBuf.Lock() 69 defer log.LogBuf.Unlock() 70 71 for i := len(log.LogBuf.LogEntries) - 1; i > 0; i-- { 72 log.LogBuf.LogEntries[i] = log.LogBuf.LogEntries[i-1] 73 } 74 log.LogBuf.LogEntries[0] = strings.TrimRight(string(p), " \t\n") 75 76 log.LogBuf.TotalEntries++ 77 return 0, nil 78 } 79 80 // openLogFile is called to open the log file and (re-) int the logger. This 81 // really should be done before using it, but you can log things before calling. 82 // However, they will not appear in the logfile and will not be in the new text 83 // format. 84 // 85 // Config variables that affect logging include: 86 // Logging.LogFilePath string if present, pathname to log file 87 // Logging.LogToConsole bool if present and true, log to console as well as file 88 // Logging.TraceLevelLogging stringslice list of packages where tracing is enabled (name must 89 // also appear in packageTraceSettings) 90 // Logging.DebugLevelLogging stringslice 91 // 92 func openLogFile(confMap conf.ConfMap) (err error) { 93 log.SetFormatter(&log.TextFormatter{DisableColors: true, TimestampFormat: timeFormat}) 94 95 // Fetch log file info, if provided 96 logFilePath, _ := confMap.FetchOptionValueString("Logging", "LogFilePath") 97 if logFilePath != "" { 98 logFile, err = os.OpenFile(logFilePath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) 99 if err != nil { 100 log.Errorf("couldn't open log file '%s': %v", logFilePath, err) 101 return err 102 } 103 } 104 105 // Determine whether we should log to console. Default is false. 106 logToConsole, err := confMap.FetchOptionValueBool("Logging", "LogToConsole") 107 if err != nil { 108 logToConsole = false 109 err = nil 110 } 111 112 log.SetOutput(&logTargets) 113 if logFilePath == "" { 114 addLogTarget(os.Stderr) 115 } else { 116 addLogTarget(logFile) 117 if logToConsole { 118 addLogTarget(os.Stderr) 119 } 120 } 121 122 // NOTE: We always enable max logging in logrus, and either decide in 123 // this package whether to log OR log everything and parse it out of 124 // the logs after the fact 125 log.SetLevel(log.DebugLevel) 126 127 // Fetch trace and debug log settings, if provided 128 traceConfSlice, _ := confMap.FetchOptionValueStringSlice("Logging", "TraceLevelLogging") 129 setTraceLoggingLevel(traceConfSlice) 130 131 debugConfSlice, _ := confMap.FetchOptionValueStringSlice("Logging", "DebugLevelLogging") 132 setDebugLoggingLevel(debugConfSlice) 133 134 Infof("logger opened logfile '%s' (PID %d)", logFilePath, os.Getpid()) 135 return 136 } 137 138 // closeLogFile closes the output log file as part of shutdown or as part of 139 // rotating to a new log file. 140 // 141 func closeLogFile(confMap conf.ConfMap) (err error) { 142 143 Infof("logger closing logfile (PID %d)", os.Getpid()) 144 145 // We open and close our own logfile 146 if logFile != nil { 147 // Sync() flushes data cached in the kernel to disk, which is 148 // really only useful if the OS were to crash soon 149 logFile.Sync() 150 logFile.Close() 151 } 152 logTargets.Clear() 153 err = nil 154 return 155 } 156 157 // Up opens the logfile. This really should be done before using the logfile, 158 // but you can log things before calling. However, they will not appear in 159 // the logfile and will not be in the new text format. 160 // 161 // Config variables that affect logging include: 162 // Logging.LogFilePath string if present, pathname to log file 163 // Logging.LogToConsole bool if present and true, log to console as well as file 164 // Logging.TraceLevelLogging stringslice list of packages where tracing is enabled (name must 165 // also appear in packageTraceSettings) 166 // Logging.DebugLevelLogging stringslice 167 // 168 169 func Up(confMap conf.ConfMap) (err error) { 170 171 err = openLogFile(confMap) 172 if err != nil { 173 return 174 } 175 176 Infof("logger is starting up (PID %d)", os.Getpid()) 177 return 178 } 179 180 func SignaledStart(confMap conf.ConfMap) (err error) { 181 err = nil 182 return 183 } 184 185 func SignaledFinish(confMap conf.ConfMap) (err error) { 186 187 Infof("logger is closing logfile (PID %d)", os.Getpid()) 188 err = closeLogFile(confMap) 189 if nil == err { 190 err = openLogFile(confMap) 191 Infof("logger opened logfile (PID %d)", os.Getpid()) 192 } 193 return 194 } 195 196 func Down(confMap conf.ConfMap) (err error) { 197 198 log.Infof("logger is shutting down (PID %d)", os.Getpid()) 199 200 err = closeLogFile(confMap) 201 return 202 } 203 204 // Parse a log entry captured via LogTarget return the fields as key value pairs 205 // in a map. 206 // 207 // Match log entries look like: 208 // 209 // time="2017-07-27T01:30:46.060080Z" level=info msg="retry.RequestWithRetry(): swiftclient.testRetry.request(1) succeeded after 4 attempts in 0.031 sec" function=RequestWithRetry goroutine=6 package=swiftclient 210 // 211 // time="2017-07-27T02:18:19.214012Z" level=error msg="retry.RequestWithRetry(): swiftclient.testRetry.request(1) failed after 7 attempts in 0.053 sec with retriable error" error="Simulate a retriable errror" function=RequestWithRetry goroutine=6 package=swiftclient 212 // 213 // time="2017-07-27T02:09:32.259383Z" level=error msg="retry.RequestWithRetry(): swiftclient.testRetry.request(1) failed after 6 attempts in 0.054 sec with unretriable error" error="Simulate an unretriable error" function=RequestWithRetry goroutine=20 package=swiftclient 214 // 215 func parseLogEntry(entry string) (fields map[string]string, err error) { 216 var ( 217 matches []string 218 ) 219 220 var fieldRE = regexp.MustCompile( 221 `^time="(?P<time>[-:0-9.ZTt_]+)" level=(?P<level>[a-zA-Z]+) msg="(?P<msg>([^"]|\\")+)" (error="(?P<error>([^"]|\\")+)" )?function=(?P<function>\w+) goroutine=(?P<goroutine>\d+) package=(?P<package>\w+)`) 222 223 matches = fieldRE.FindStringSubmatch(entry) 224 if matches == nil { 225 fmt.Printf("parseLogEntry: log entry not matched by regular expression fieldRE: '%s'\n", entry) 226 err = fmt.Errorf("log entry not matched by regular expression fieldRE: '%s'", entry) 227 return 228 } 229 230 fields = make(map[string]string) 231 for idx, name := range fieldRE.SubexpNames() { 232 if name != "" && matches[idx] != "" { 233 fields[name] = matches[idx] 234 } 235 } 236 237 return 238 } 239 240 // Parse the log entries, starting with the most recent, looking for a message 241 // generated by the function funcName that matches the regular expression 242 // logEntryRE within the most recent maxEntries lines of the log. 243 // 244 // If found, return the parsed fields from the log message, which are a 245 // combination of the fields returned by ParseLogEntry() and the fields in the 246 // passed regular expression (which must use names for the matching parts). 247 // funcName must match the contents of the field "function" returned by 248 // ParseLogEntry(). entryIdx is the index of the entry in the log with 0 being 249 // the most recent. 250 // 251 // If not found, return an error. 252 // 253 // Example regexp from trackedlock/api_test.go that matches: 254 // 255 // msg="trackedlock watcher: *trackedlock.Mutex at 0xc420110000 locked for 2 sec; stack at Lock() call:\ngoroutine 19 [running]:..." 256 // 257 // watcherLogMatch = `^trackedlock watcher: (?P<type>[*a-zA-Z0-9_.]+) at (?P<ptr>0x[0-9a-f]+) locked for (?P<time>\d+) sec; stack at (?P<locker>[a-zA-Z0-9_()]+) call:\\n(?P<lockStack>.*)$` 258 // 259 func parseLogForFunc(logcopy LogTarget, funcName string, logEntryRE *regexp.Regexp, maxEntries int) (fields map[string]string, entryIdx int, err error) { 260 logcopy.LogBuf.Lock() 261 defer logcopy.LogBuf.Unlock() 262 263 if logcopy.LogBuf.TotalEntries < 1 { 264 err = fmt.Errorf("parseLogForFunc(): no log entries") 265 return 266 } 267 268 var logEntry string 269 for entryIdx, logEntry = range logcopy.LogBuf.LogEntries { 270 271 if entryIdx >= maxEntries { 272 err = fmt.Errorf( 273 "parseLogForFunc(): no matching log entry for function '%s' found in %d log entries", 274 funcName, maxEntries) 275 return 276 } 277 if logEntry == "" { 278 err = fmt.Errorf("parseLogForFunc(): exhausted all log entries without finding a match") 279 return 280 } 281 282 fields, err = ParseLogEntry(logEntry) 283 if err != nil { 284 err = fmt.Errorf("parseLogForFunc(): log entry '%s' unparsable by ParseLogEntry(): %v", 285 logEntry, err) 286 return 287 } 288 289 if fields["msg"] == "" { 290 err = fmt.Errorf("parseLogForFunc(): log entry does not contain a 'msg=' string: '%s'", 291 logEntry) 292 return 293 } 294 if fields["function"] != funcName { 295 continue 296 } 297 298 matches := logEntryRE.FindStringSubmatch(fields["msg"]) 299 if matches == nil { 300 continue 301 } 302 303 // we found a matching log entry; copy the fields and we're done! 304 for idx, name := range logEntryRE.SubexpNames() { 305 if name != "" { 306 fields[name] = matches[idx] 307 } 308 } 309 return 310 } 311 312 err = fmt.Errorf("parseLogForFunc(): no matching log entry found: function '%s'", funcName) 313 return 314 }