package log import ( "errors" "fmt" "io" stdLog "log" "os" "runtime" "strings" "time" ) var ( // logger is the variable to which the code can write its output. logger *stdLog.Logger // The current absolute path to backend/ is stored in filePrefix; this is // removed from the filename because the logging doesn't require any of the other junk. filePrefix string // logLevel is the variable that decides at which log level the logger will output // to the logger. logLevel LogLevel ) type LogLevel uint8 const ( DebugLevel LogLevel = iota InfoLevel WarnLevel ErrorLevel FatalLevel ) func init() { // Make sure that logger is always initalized, this is to still be able to use the logger // even when logger isn't yet initilazed with the correct values. initializeLogger(os.Stdout) // A quick and dirty way to get the backend/ absolute folder _, filename, _, _ := runtime.Caller(0) filePrefix = strings.TrimSuffix(filename, "server/log/log.go") if filePrefix == filename { // in case the source code file is moved, we can not trim the suffix, the code above should also be updated. panic("unable to detect correct package prefix, please update file: " + filename) } } // initializeLogger takes a input of multiple writers, which to all writers will be logged to. func initializeLogger(w ...io.Writer) { logger = stdLog.New(io.MultiWriter(w...), "", 0) } func stringToLogLevel(input string) (LogLevel, error) { switch strings.ToLower(input) { case "fatal": return FatalLevel, nil case "error": return ErrorLevel, nil case "warn": return WarnLevel, nil case "info": return InfoLevel, nil } return FatalLevel, errors.New("invalid log level value") } // Config takes the ini config and correctly adjust the logger variable. func Config(outputs []string, outputFile, level string) error { newLogLevel, err := stringToLogLevel(level) if err != nil { return err } logLevel = newLogLevel isFileLog := false isConsoleLog := false for _, output := range outputs { switch output { case "file": isFileLog = true case "console": isConsoleLog = true } } writers := []io.Writer{} if isConsoleLog { writers = append(writers, os.Stdout) } if isFileLog { f, err := os.OpenFile(outputFile, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0o644) if err != nil { return fmt.Errorf("cannot open log output's file %q: %v", outputFile, err) } writers = append(writers, f) } initializeLogger(writers...) return nil } // the Skipper struct can be passed as firt argument to adjust the skip offset. type Skipper struct { // SkipOffset will be added on top of the existing "2" skips. SkipOffset int } func log(msg, prefix string, print bool, args ...interface{}) string { caller := "?()" skipOffset := 2 // Check if the first argument is Skipper and add the skipoffset accordingly. if len(args) > 0 { if skip, ok := args[0].(Skipper); ok { skipOffset += skip.SkipOffset args = args[1:] } } pc, filename, line, ok := runtime.Caller(skipOffset) if ok { // Get caller function name. fn := runtime.FuncForPC(pc) if fn != nil { caller = fn.Name() + "()" // Remove prefix of binary's name. lastIndex := strings.LastIndexByte(caller, '.') if lastIndex > 0 && len(caller) > lastIndex+1 { caller = caller[lastIndex+1:] } } } filename = strings.TrimPrefix(filename, filePrefix) // Don't output long file names. if len(filename) > 40 { filename = "..." + filename[len(filename)-40:] } now := time.Now() year, month, day := now.Date() hour, min, sec := now.Clock() // Output message: // DATE TIME FILENAME:LINE:CALLER PREFIX: MSG prefixedMessage := fmt.Sprintf("%d/%02d/%02d %02d:%02d:%02d %s:%d:%s %s: %s", year, month, day, hour, min, sec, filename, line, caller, prefix, msg) // Only print the message if it has been requested. if print { if len(args) > 0 { logger.Printf(prefixedMessage+"\n", args...) } else { logger.Println(prefixedMessage) } return "" } else { return fmt.Sprintf(prefixedMessage, args...) } } // Debug logs a message with the DEBUG prefix. func Debug(msg string, args ...interface{}) { if logLevel > DebugLevel { return } log(msg, "DEBUG", true, args...) } // Info logs a message with the INFO prefix. func Info(msg string, args ...interface{}) { if logLevel > InfoLevel { return } log(msg, "INFO", true, args...) } // Warn logs a message with the WARN prefix. func Warn(msg string, args ...interface{}) { if logLevel > WarnLevel { return } log(msg, "WARN", true, args...) } // Error logs a message with the ERROR prefix. func Error(msg string, args ...interface{}) { if logLevel > ErrorLevel { return } log(msg, "ERROR", true, args...) } // Fatal logs a message with the FATAL prefix and then exit the program. func Fatal(msg string, args ...interface{}) { log(msg, "FATAL", true, args...) os.Exit(1) }