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  }