diff --git a/.gitignore b/.gitignore index df1a13b..241045b 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1 @@ -/logs \ No newline at end of file +/logs* \ No newline at end of file diff --git a/file.go b/file.go new file mode 100644 index 0000000..3c69d0a --- /dev/null +++ b/file.go @@ -0,0 +1,132 @@ +package logger + +import ( + "fmt" + "log" + "os" + "strings" + "sync" + "time" +) + +// FileLogger contains configuration options specific to logging into a file. +// It is enabled if the FilePath != "" +type FileLogger struct { + + // Minimum log level for logging into a file + Level Level + + // Absolute or relative path to log files to + Path string + + // With this option the path of the log file will be appended with the current date + // so that an own log file for each day is used. The format of the date is 'YYYYMMDD' + AppendDate bool + + // Internal dependency used to synchronize the access the log file + fileSync sync.RWMutex + // Additional file sync that is used during writing to the log file + fileSyncWrite sync.RWMutex + + logger *log.Logger + file *os.File + + // Upper logger struct + rootLogger *Logger +} + +// CloseFile closes the file that is currently used for logging messages to +// a file +func (l *FileLogger) CloseFile() { + l.fileSync.Lock() + + if l.file != nil { + l.file.Close() + l.file = nil + l.logger = nil + } + + l.fileSync.Unlock() +} + +// openFile tries to open the file that is configured inside the loggers fild +// "LogFilePath" +func (l *FileLogger) openFile() { + l.fileSync.Lock() + + path := l.getFilePath() + file, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err == nil { + l.logger = log.New(file, "", 0) + l.file = file + } else { + l.rootLogger.Log(LevelError, fmt.Sprintf("Cannot access the log file '%s'\n%s", path, err.Error())) + } + + l.fileSync.Unlock() +} + +// writeToFile writes the given message to the opened log file +func (l *FileLogger) writeToFile(message string, level Level) { + l.fileSync.RLock() + l.fileSyncWrite.RLock() + + // When append date is enabled we need to check if file path is still accurate + if l.AppendDate { + currentPath := l.getFilePath() + + if l.file.Name() != currentPath { + // The file path is not up-to-date anymore → update the log file + l.fileSync.RUnlock() + l.fileSyncWrite.RUnlock() + + l.fileSyncWrite.Lock() + // The syncWriter is now locked. So check again the file name against the current path because the file could already be changed + // in the time framew between locking and checking + if l.file.Name() != currentPath { + l.CloseFile() + l.openFile() + } + l.fileSyncWrite.Unlock() + + // Lock previous locks again + l.fileSync.RLock() + l.fileSyncWrite.RLock() + } + } + + l.logger.Println(message) + l.file.Sync() + + l.fileSync.RUnlock() + l.fileSyncWrite.RUnlock() + + // Close the file because for fatal log level the program is going to be exited + if level == LevelFatal { + l.CloseFile() + } +} + +// getFilePath returns the path to use for the log file +func (l *FileLogger) getFilePath() string { + path := strings.ReplaceAll(l.Path, "\\", "/") + + // Append the current date to the log path when enabled + if l.AppendDate { + lastSlash := strings.LastIndex(path, "/") + if lastSlash != -1 && (lastSlash+1) < len(path) { + path = path + "." + getFileDate() + } else if lastSlash == -1 { + path = path + "." + getFileDate() + } else { + path += getFileDate() + } + } + + return path +} + +// getFileDate returns the current date formatted as the log files path name +func getFileDate() string { + return time.Now().Format("2006-01-02") +} diff --git a/logger.go b/logger.go index 3cfc975..1e39f7f 100755 --- a/logger.go +++ b/logger.go @@ -13,10 +13,9 @@ import ( ) type Logger struct { - PrintLevel Level - LogLevel Level - LogFilePath string - PrintSource bool + + // Minimum log level for printing to the console (stdout and stderr) + Level Level // Colorizes the log messages for the console. // Even if you set this to true the user is able to overwrite this behaviour by @@ -24,6 +23,9 @@ type Logger struct { // "TERMINAL_ENABLE_COLORS" (to force coloring for "unsupported" terminals) ColoredOutput bool + // Whether to print the file and line number of the invoking (calling line) + PrintSource bool + // While logging, the file and line number of the // invoking (calling) line can be printed out. // This defines an offset that is applied to the call stack. @@ -31,11 +33,12 @@ type Logger struct { // have to set this value to one FuncCallIncrement int + // Configuration options for logging into a file + File *FileLogger + colorConf colorConfig consoleLogger *log.Logger consoleLoggerErr *log.Logger - fileLogger *log.Logger - logFile *os.File } // Globally available logging instance. This will be uesed if log functions @@ -44,9 +47,11 @@ var dLogger Logger func init() { dLogger = Logger{ - PrintLevel: LevelDebug, - LogLevel: LevelInfo, - LogFilePath: "", + Level: LevelDebug, + File: &FileLogger{ + Level: LevelInfo, + Path: "", + }, PrintSource: false, } @@ -64,11 +69,11 @@ func NewLogger(logger *Logger) *Logger { // configuration. // Instead of opening a new file to write the log messages to, // the old file reference of the other logger will be used internal. -// This enables you to writhe to the same file with different log configurations. +// This enables you to write to the same file with different log configurations. func NewLoggerWithFile(logger *Logger, file *Logger) *Logger { - logger.logFile = file.logFile - logger.LogFilePath = file.LogFilePath - logger.fileLogger = file.fileLogger + logger.File.file = file.File.file + logger.File.Path = file.File.Path + logger.File.logger = file.File.logger logger.setup(true) return logger @@ -92,24 +97,19 @@ func (l *Logger) log(level Level, message string, parameters ...any) { var levelName = fmt.Sprintf("%-5s", level) printMessage := "[" + levelName + "] " + time.Now().Local().Format("2006-01-02 15:04:05") + - getSourceMessage(file, line, pc, *l) + " - " + fmt.Sprintf(message, parameters...) + getSourceMessage(file, line, pc, l) + " - " + fmt.Sprintf(message, parameters...) printMessageColored := l.getColored("["+levelName+"] ", level.getColor()) + l.getColored(time.Now().Local().Format("2006-01-02 15:04:05"), colCyan) + - l.getColored(getSourceMessage(file, line, pc, *l), colPurple) + " - " + + l.getColored(getSourceMessage(file, line, pc, l), colPurple) + " - " + l.getColored(fmt.Sprintf(message, parameters...), level.getColor()) - if l.LogLevel <= level && l.fileLogger != nil { - l.fileLogger.Println(printMessage) - l.logFile.Sync() - - if level == LevelFatal { - l.CloseFile() - } + if l.File.Level <= level && l.File.logger != nil { + l.File.writeToFile(printMessage, level) } - if l.PrintLevel <= level { + if l.Level <= level { if level == LevelError { l.consoleLoggerErr.Println(printMessageColored) } else if level == LevelFatal { @@ -129,7 +129,7 @@ func (l *Logger) getColored(message string, color func(str string, parameters .. return message } -func getSourceMessage(file string, line int, pc uintptr, l Logger) string { +func getSourceMessage(file string, line int, pc uintptr, l *Logger) string { if !l.PrintSource { return "" } @@ -144,20 +144,10 @@ func (l *Logger) setup(keepFile bool) { l.consoleLogger = log.New(os.Stdout, "", 0) l.consoleLoggerErr = log.New(os.Stderr, "", 0) - if strings.TrimSpace(l.LogFilePath) != "" && !keepFile { - file, err := os.OpenFile(l.LogFilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) - if err == nil { - l.fileLogger = log.New(file, "", 0) - l.logFile = file - } else { - l.Log(LevelError, fmt.Sprintf("Cannot access the log file '%s'\n%s", l.LogFilePath, err.Error())) - } + if strings.TrimSpace(l.File.Path) != "" && !keepFile { + l.File.openFile() } else if !keepFile { - l.fileLogger = nil - if l.logFile != nil { - l.logFile.Close() - l.logFile = nil - } + l.File.CloseFile() } // Functions that could produce a panic @@ -169,18 +159,10 @@ func (l *Logger) setup(keepFile bool) { l.colorConf = *newColorConfig(l.ColoredOutput) } -func (l *Logger) CloseFile() { - if l.logFile != nil { - l.logFile.Close() - l.logFile = nil - l.fileLogger = nil - } -} - // SetGlobalLogger updates the global default logger with a custom one. // You can create one via the Logger struct. func SetGlobalLogger(l *Logger) { - dLogger = *l + dLogger = *l // nolint: golint dLogger.setup(false) } func GetGlobalLogger() *Logger { @@ -210,28 +192,28 @@ func Fatal(message string, parameters ...any) { // Available methods for each logger per logging level // -func (l Logger) Trace(message string, parameters ...any) { +func (l *Logger) Trace(message string, parameters ...any) { l.Log(LevelTrace, message, parameters...) } -func (l Logger) Debug(message string, parameters ...any) { +func (l *Logger) Debug(message string, parameters ...any) { l.Log(LevelDebug, message, parameters...) } -func (l Logger) Info(message string, parameters ...any) { +func (l *Logger) Info(message string, parameters ...any) { l.Log(LevelInfo, message, parameters...) } -func (l Logger) Warning(message string, parameters ...any) { +func (l *Logger) Warning(message string, parameters ...any) { l.Log(LevelWarning, message, parameters...) } -func (l Logger) Error(message string, parameters ...any) { +func (l *Logger) Error(message string, parameters ...any) { l.Log(LevelError, message, parameters...) } -func (l Logger) Fatal(message string, parameters ...any) { +func (l *Logger) Fatal(message string, parameters ...any) { l.Log(LevelFatal, message, parameters...) } // CloseFile closes the underlaying file to which the logger messages are written. func CloseFile() { - dLogger.CloseFile() + dLogger.File.CloseFile() } // GetLoggerFromEnv returns a logging instance configured @@ -252,9 +234,10 @@ func CloseFile() { // - Tracing disabled func GetLoggerFromEnv(defaultLogger *Logger) *Logger { defaultLogger.ColoredOutput = getEnvBool("LOGGER_COLOREDOUTPUT", defaultLogger.ColoredOutput) - defaultLogger.PrintLevel = GetLevelByName(getEnvString("LOGGER_PRINTLEVEL", defaultLogger.PrintLevel.String())) - defaultLogger.LogLevel = GetLevelByName(getEnvString("LOGGER_LOGLEVEL", defaultLogger.LogLevel.String())) - defaultLogger.LogFilePath = getEnvString("LOGGER_LOGFILEPATH", defaultLogger.LogFilePath) + defaultLogger.Level = GetLevelByName(getEnvString("LOGGER_LEVEL", defaultLogger.Level.String())) + defaultLogger.File.Level = GetLevelByName(getEnvString("LOGGER_FILE_LEVEL", defaultLogger.File.Level.String())) + defaultLogger.File.Path = getEnvString("LOGGER_FILE_PATH", defaultLogger.File.Path) + defaultLogger.File.AppendDate = getEnvBool("LOGGER_FILE_APPENDDATE", defaultLogger.File.AppendDate) defaultLogger.PrintSource = getEnvBool("LOGGER_PRINTSOURCE", defaultLogger.PrintSource) return NewLogger(defaultLogger) } diff --git a/main/main.go b/main/main.go index 29bfee1..e0e67f6 100755 --- a/main/main.go +++ b/main/main.go @@ -12,9 +12,12 @@ func main() { l := &logger.Logger{ ColoredOutput: true, PrintSource: true, - LogFilePath: "./logs", - PrintLevel: logger.LevelTrace, - LogLevel: logger.LevelWarning, + Level: logger.LevelTrace, + File: &logger.FileLogger{ + Level: logger.LevelWarning, + Path: "./logs", + AppendDate: true, + }, } logger.SetGlobalLogger(l) @@ -29,8 +32,10 @@ func main() { lOther := &logger.Logger{ ColoredOutput: false, PrintSource: false, - PrintLevel: logger.LevelDebug, - LogLevel: logger.LevelDebug, + Level: logger.LevelDebug, + File: &logger.FileLogger{ + Level: logger.LevelDebug, + }, } logger.NewLoggerWithFile(lOther, logger.GetGlobalLogger())