gplog/gplog.go (263 lines of code) (raw):

package gplog /* * This file contains structs and functions related to logging. */ import ( "fmt" "io" "log" "os" "strings" "sync" "github.com/cloudberrydb/gp-common-go-libs/operating" "github.com/pkg/errors" ) var ( /* * Error code key: * 0: Completed successfully (default value) * 1: Completed, but encountered a non-fatal error (set by logger.Error) * 2: Did not complete, encountered a fatal error (set by logger.Fatal) */ errorCode = 0 // Singleton logger used by any package or utility that calls InitializeLogging logger *GpLogger /* * A mutex for ensuring that concurrent calls to output functions by multiple * goroutines are safe. * * This mutex is a package-level global rather than a member of GpLogger to * avoid any possible error condition caused by calling SetLogger in one * goroutine while another is calling an output function, which could possibly * arise while running tests in parallel. */ logMutex sync.Mutex /* * A function which can customize log file name */ logFileNameFunc LogFileNameFunc exitFunc ExitFunc ) const ( /* * The following constants representing the current logging level, and are * cumulative (that is, setting the log level to Debug will print all Error-, * Info-, and Verbose-level messages in addition to Debug-level messages). * * Log levels for terminal output and logfile output are separate, and can be * set independently. By default, a new logger will have a verbosity of INFO * for terminal output and DEBUG for logfile output. */ LOGERROR = iota LOGINFO LOGVERBOSE LOGDEBUG ) /* * Leveled logging output functions using the above log levels are implemented * below. Info(), Verbose(), and Debug() print messages when the log level is * set at or above the log level matching their names. Warn(), Error(), and * Fatal() always print their messages regardless of the current log level. * * The intended usage of these functions is as follows: * - Info: Messages that should always be written unless the user explicitly * suppresses output, e.g. notifying the user that a step has completed. * - Verbose: More detailed messages that are mostly useful to the user, e.g. * printing information about a function's substeps for progress tracking. * - Debug: More detailed messages that are mostly useful to developers, e.g. * noting that a function has been called with certain arguments. * - Warn: Messages indicating unusual but not incorrect behavior that a user * may want to know, e.g. that certain steps are skipped when using * certain flags. These messages are shown even if output is suppressed. * - Error: Messages indicating that an error has occurred, but that the program * can continue, e.g. one function call in a group failed but others succeeded. * - Fatal: Messages indicating that the program cannot proceed, e.g. the database * cannot be reached. This function will exit the program after printing * the error message. * - FatalWithoutPanic: Same as Fatal, but will not trigger panic. Just exit(1). */ type LogPrefixFunc func(string) string type LogFileNameFunc func(string, string) string type ExitFunc func() type GpLogger struct { logStdout *log.Logger logStderr *log.Logger logFile *log.Logger logFileName string shellVerbosity int fileVerbosity int header string logPrefixFunc LogPrefixFunc } /* * Logger initialization/helper functions */ /* * Multiple calls to InitializeLogging can be made if desired; the first call * will initialize the logger as a singleton and subsequent calls will return * the same Logger instance. */ func InitializeLogging(program string, logdir string) { if logger != nil { return } currentUser, _ := operating.System.CurrentUser() if logdir == "" { logdir = fmt.Sprintf("%s/gpAdminLogs", currentUser.HomeDir) } createLogDirectory(logdir) logfile := GenerateLogFileName(program, logdir) logFileHandle := openLogFile(logfile) logger = NewLogger(os.Stdout, os.Stderr, logFileHandle, logfile, LOGINFO, program) SetExitFunc(defaultExit) } func GenerateLogFileName(program, logdir string) string { var logfile string if logFileNameFunc != nil { logfile = logFileNameFunc(program, logdir) } else { timestamp := operating.System.Now().Format("20060102") logfile = fmt.Sprintf("%s/%s_%s.log", logdir, program, timestamp) } return logfile } func SetLogger(log *GpLogger) { logger = log } // This function should only be used for testing purposes func GetLogger() *GpLogger { return logger } // stdout and stderr are passed in to this function to enable output redirection in tests. func NewLogger(stdout io.Writer, stderr io.Writer, logFile io.Writer, logFileName string, shellVerbosity int, program string, logFileVerbosity ...int) *GpLogger { fileVerbosity := LOGDEBUG // Shell verbosity must always be specified, but file verbosity defaults to LOGDEBUG to encourage more verbose log output. if len(logFileVerbosity) == 1 && logFileVerbosity[0] >= LOGERROR && logFileVerbosity[0] <= LOGDEBUG { fileVerbosity = logFileVerbosity[0] } return &GpLogger{ logStdout: log.New(stdout, "", 0), logStderr: log.New(stderr, "", 0), logFile: log.New(logFile, "", 0), logFileName: logFileName, shellVerbosity: shellVerbosity, fileVerbosity: fileVerbosity, header: GetHeader(program), logPrefixFunc: nil, } } func GetHeader(program string) string { headerFormatStr := "%s:%s:%s:%06d-[%s]:-" // PROGRAMNAME:USERNAME:HOSTNAME:PID-[LOGLEVEL]:- currentUser, _ := operating.System.CurrentUser() user := currentUser.Username host, _ := operating.System.Hostname() pid := operating.System.Getpid() header := fmt.Sprintf(headerFormatStr, program, user, host, pid, "%s") return header } func SetLogPrefixFunc(logPrefixFunc func(string) string) { logger.logPrefixFunc = logPrefixFunc } func SetLogFileNameFunc(fileNameFunc func(string, string) string) { logFileNameFunc = fileNameFunc } func SetExitFunc(pExitFunc func()) { exitFunc = pExitFunc } func defaultLogPrefixFunc(level string) string { logTimestamp := operating.System.Now().Format("20060102:15:04:05") return fmt.Sprintf("%s %s", logTimestamp, fmt.Sprintf(logger.header, level)) } func GetLogPrefix(level string) string { if logger.logPrefixFunc != nil { return logger.logPrefixFunc(level) } return defaultLogPrefixFunc(level) } func GetLogFilePath() string { return logger.logFileName } func GetVerbosity() int { return logger.shellVerbosity } func SetVerbosity(verbosity int) { logger.shellVerbosity = verbosity } func GetLogFileVerbosity() int { return logger.fileVerbosity } func SetLogFileVerbosity(verbosity int) { logger.fileVerbosity = verbosity } func GetErrorCode() int { return errorCode } func SetErrorCode(code int) { errorCode = code } /* * Log output functions, as described above */ func Info(s string, v ...interface{}) { logMutex.Lock() defer logMutex.Unlock() message := GetLogPrefix("INFO") + fmt.Sprintf(s, v...) if logger.fileVerbosity >= LOGINFO { _ = logger.logFile.Output(1, message) } if logger.shellVerbosity >= LOGINFO { _ = logger.logStdout.Output(1, message) } } func Warn(s string, v ...interface{}) { logMutex.Lock() defer logMutex.Unlock() message := GetLogPrefix("WARNING") + fmt.Sprintf(s, v...) _ = logger.logFile.Output(1, message) _ = logger.logStdout.Output(1, message) } func Verbose(s string, v ...interface{}) { logMutex.Lock() defer logMutex.Unlock() message := GetLogPrefix("DEBUG") + fmt.Sprintf(s, v...) if logger.fileVerbosity >= LOGVERBOSE { _ = logger.logFile.Output(1, message) } if logger.shellVerbosity >= LOGVERBOSE { _ = logger.logStdout.Output(1, message) } } func Debug(s string, v ...interface{}) { logMutex.Lock() defer logMutex.Unlock() message := GetLogPrefix("DEBUG") + fmt.Sprintf(s, v...) if logger.fileVerbosity >= LOGDEBUG { _ = logger.logFile.Output(1, message) } if logger.shellVerbosity >= LOGDEBUG { _ = logger.logStdout.Output(1, message) } } func Error(s string, v ...interface{}) { logMutex.Lock() defer logMutex.Unlock() message := GetLogPrefix("ERROR") + fmt.Sprintf(s, v...) errorCode = 1 _ = logger.logFile.Output(1, message) _ = logger.logStderr.Output(1, message) } func Fatal(err error, s string, v ...interface{}) { logMutex.Lock() defer logMutex.Unlock() message := GetLogPrefix("CRITICAL") errorCode = 2 stackTraceStr := "" if err != nil { message += fmt.Sprintf("%v", err) stackTraceStr = formatStackTrace(errors.WithStack(err)) if s != "" { message += ": " } } message += strings.TrimSpace(fmt.Sprintf(s, v...)) _ = logger.logFile.Output(1, message+stackTraceStr) if logger.shellVerbosity >= LOGVERBOSE { abort(message + stackTraceStr) } else { abort(message) } } func FatalOnError(err error, output ...string) { if err != nil { if len(output) == 0 { Fatal(err, "") } else { Fatal(err, output[0]) } } } func FatalWithoutPanic(s string, v ...interface{}) { logMutex.Lock() defer logMutex.Unlock() message := GetLogPrefix("CRITICAL") + fmt.Sprintf(s, v...) errorCode = 2 _ = logger.logFile.Output(1, message) _ = logger.logStderr.Output(1, message) exitFunc() } type stackTracer interface { StackTrace() errors.StackTrace } func formatStackTrace(err error) string { st := err.(stackTracer).StackTrace() message := fmt.Sprintf("%+v", st[1:]) return message } /* * Abort() is for handling critical errors. It panic()s to unwind the call stack * assuming that the panic is caught by a recover() in the main utility. * * log.Fatal() calls Abort() after logging its arguments, so generally that function * should be used instead of calling Abort() directly. */ func abort(output ...interface{}) { errStr := "" if len(output) > 0 { errStr = fmt.Sprintf("%v", output[0]) if len(output) > 1 { errStr = fmt.Sprintf(errStr, output[1:]...) } } panic(errStr) } func openLogFile(filename string) io.WriteCloser { flags := os.O_APPEND | os.O_CREATE | os.O_WRONLY fileHandle, err := operating.System.OpenFileWrite(filename, flags, 0644) if err != nil { abort(err) } return fileHandle } func createLogDirectory(dirname string) { info, err := operating.System.Stat(dirname) if err != nil { if operating.System.IsNotExist(err) { err = operating.System.MkdirAll(dirname, 0755) if err != nil { abort(errors.Errorf("Cannot create log directory %s: %v", dirname, err)) } } else { abort(errors.Errorf("Cannot stat log directory %s: %v", dirname, err)) } } else if !(info.IsDir()) { abort(errors.Errorf("%s is a file, not a directory", dirname)) } } func defaultExit() { os.Exit(1) }