commit 8fec55a50aeba67e23b6183589f59e3d1083c792 Author: RPJosh Date: Sat Apr 1 20:03:16 2023 +0200 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..df1a13b --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/logs \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..6c2d879 --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +## go-logger + +Go module providing simple logging support. + +Take a look at [main.go](main/main.go) for an example. \ No newline at end of file diff --git a/colors.go b/colors.go new file mode 100755 index 0000000..e22beed --- /dev/null +++ b/colors.go @@ -0,0 +1,69 @@ +package logger + +import ( + "fmt" + "os" +) + +// ColorConfig contains configuration options to write +// colored text to the console. +type colorConfig struct { + enableColors bool +} + +// Default ANSI color code definitions. +// The variable contains a function that will be padded by the +// matching color. You can also specify replace values after the string +// for printf. +var ( + colPurple = color("\033[1;35m%s\033[0m") + colPurpleLight = color("\033[0;35m%s\033[0m") + colRed = color("\033[1;31m%s\033[0m") + colYellow = color("\033[1;33m%s\033[0m") + colBlue = color("\033[1;34m%s\033[0m") + colCyan = color("\033[1;36m%s\033[0m") + colGreen = color("\033[0;32m%s\033[0m") +) + +// Color returns a function that pads the string with the given color code +func color(colorString string) func(str string, parameters ...any) string { + return func(str string, parameters ...any) string { + return fmt.Sprintf(colorString, fmt.Sprintf(str, parameters...)) + } +} + +// NewColorConfig prepares and creates a new color config. +// This function could panic because of low level system access +func newColorConfig(enable bool) (conf *colorConfig) { + conf = &colorConfig{} + + // Validate if ANSI codes are supported by the terminal + if enable { + if _, exist := os.LookupEnv("TERMINAL_DISABLE_COLORS"); exist { + return + } else if _, exist := os.LookupEnv("TERMINAL_ENABLE_COLORS"); exist { + conf.enableColors = true + return + } + + conf.enableColors = conf.isColoringSupported() + } + + return +} + +// getColor returns the matching color for the level +func (l Level) getColor() func(str string, parameters ...any) string { + switch l { + case LevelTrace: + return colPurpleLight + case LevelDebug: + return colGreen + case LevelInfo: + return colBlue + case LevelWarning: + return colYellow + default: + return colRed + } +} diff --git a/colors_other.go b/colors_other.go new file mode 100755 index 0000000..ad01876 --- /dev/null +++ b/colors_other.go @@ -0,0 +1,7 @@ +//go:build !unix + +package logger + +func (c colorConfig) isColoringSupported() bool { + return false +} diff --git a/colors_unix.go b/colors_unix.go new file mode 100755 index 0000000..0943213 --- /dev/null +++ b/colors_unix.go @@ -0,0 +1,8 @@ +package logger + +import "os" + +func (c colorConfig) isColoringSupported() bool { + // Check if $TERM variable is set. Almost every terminal does support coloring in linux + return os.Getenv("TERM") != "" +} diff --git a/colors_windows.go b/colors_windows.go new file mode 100755 index 0000000..c6bef73 --- /dev/null +++ b/colors_windows.go @@ -0,0 +1,23 @@ +package logger + +import ( + "os" + + "golang.org/x/sys/windows" +) + +func (c colorConfig) isColoringSupported() bool { + + // In cmd ANSI colors are not supported by default from the beggining on (>16257) → enable explicit support via + // the flag ENABLE_VIRTUAL_TERMINAL_PROCESSING + stdout := windows.Handle(os.Stdout.Fd()) + var originalMode uint32 + + if windows.GetConsoleMode(stdout, &originalMode) == nil { + if windows.SetConsoleMode(stdout, originalMode|windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING) == nil { + return true + } + } + + return false +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..a4a5fce --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module git.rpjosh.de/RPJosh/go-logger + +go 1.20 diff --git a/go.sum b/go.sum new file mode 100755 index 0000000..ba21016 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/logger.go b/logger.go new file mode 100755 index 0000000..8d15907 --- /dev/null +++ b/logger.go @@ -0,0 +1,271 @@ +// logger provides basic logging support for your application. +// Supported log destinations are the console and a log file +package logger + +import ( + "fmt" + "log" + "os" + "runtime" + "strconv" + "strings" + "time" +) + +// Level of the log message +type Level uint8 + +const ( + LevelTrace Level = iota + LevelDebug + LevelInfo + LevelWarning + LevelError + LevelFatal +) + +type Logger struct { + PrintLevel Level + LogLevel Level + LogFilePath string + PrintSource bool + + // Colorizes the log messages for the console. + // Even if you set this to true, the user is able to overwrite this behaviour by + // setting the environment variables "TERMINAL_DISABLE_COLORS" and + // "TERMINAL_ENABLE_COLORS" (to force coloring for "unsupported" terminals) + ColoredOutput 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. + // If you you are using an own wrapper function, you + // have to set this value to one + FuncCallIncrement int + + 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 +// without a Logger struct are called +var dLogger Logger + +func init() { + dLogger = Logger{ + PrintLevel: LevelDebug, + LogLevel: LevelInfo, + LogFilePath: "", + PrintSource: false, + } + + dLogger.setup(false) +} + +// NewLogger creates a new instance of the logger with +// the given configuration. +func NewLogger(logger *Logger) *Logger { + logger.setup(false) + return logger +} + +// NewLoggerWithFile creates a new instance with the given 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. +func NewLoggerWithFile(logger *Logger, file *Logger) *Logger { + logger.logFile = file.logFile + logger.LogFilePath = file.LogFilePath + logger.fileLogger = file.fileLogger + + logger.setup(true) + return logger +} + +// Log logs a message with the given level. As additional parameters you can specify +// replace values for the message. See "fmt.printf()" for more infos. +func (l *Logger) Log(level Level, message string, parameters ...any) { + // This function is needed that "runtime.Caller(2)" is always correct (even on direct call) + l.log(level, message, parameters...) +} + +func (l *Logger) log(level Level, message string, parameters ...any) { + pc, file, line, ok := runtime.Caller(3 + l.FuncCallIncrement) + if !ok { + file = "#unknown" + line = 0 + } + + // Get the name of the level to log + var levelName string + switch level { + case LevelTrace: + levelName = "TRACE" + case LevelDebug: + levelName = "DEBUG" + case LevelInfo: + levelName = "INFO " + case LevelWarning: + levelName = "WARN " + case LevelError: + levelName = "ERROR" + case LevelFatal: + levelName = "FATAL" + } + + if levelName == "" { + message = fmt.Sprintf("Invalid level value given: %d. Original message: ", level) + message + levelName = "WARN " + level = LevelWarning + } + + printMessage := "[" + levelName + "] " + time.Now().Local().Format("2006-01-02 15:04:05") + + 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(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.PrintLevel <= level { + if level == LevelError { + l.consoleLoggerErr.Println(printMessageColored) + } else if level == LevelFatal { + l.consoleLoggerErr.Fatal(printMessageColored) + } else { + l.consoleLogger.Println(printMessageColored) + } + } + +} + +// getColored returns a message padded by with a color code if coloring is supported and specified +func (l *Logger) getColored(message string, color func(str string, parameters ...any) string) string { + if l.colorConf.enableColors { + return color(message) + } + return message +} + +func getSourceMessage(file string, line int, pc uintptr, l Logger) string { + if !l.PrintSource { + return "" + } + + fileName := file[strings.LastIndex(file, "/")+1:] + ":" + strconv.Itoa(line) + + return " (" + fileName + ")" +} + +func (l *Logger) setup(keepFile bool) { + // log.Ldate|log.Ltime|log.Lshortfile + 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())) + } + } else if !keepFile { + l.fileLogger = nil + if l.logFile != nil { + l.logFile.Close() + l.logFile = nil + } + } + + // Functions that could produce a panic + defer func() { + if err := recover(); err != nil { + l.log(LevelDebug, "Panic occured: %s", err) + } + }() + 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.setup(false) +} +func GetGlobalLogger() *Logger { + return &dLogger +} + +func Trace(message string, parameters ...any) { + dLogger.Log(LevelTrace, message, parameters...) +} +func Debug(message string, parameters ...any) { + dLogger.Log(LevelDebug, message, parameters...) +} +func Info(message string, parameters ...any) { + dLogger.Log(LevelInfo, message, parameters...) +} +func Warning(message string, parameters ...any) { + dLogger.Log(LevelWarning, message, parameters...) +} +func Error(message string, parameters ...any) { + dLogger.Log(LevelError, message, parameters...) +} +func Fatal(message string, parameters ...any) { + dLogger.Log(LevelFatal, message, parameters...) +} + +// CloseFile closes the underlaying file to which the logger messages are written. +func CloseFile() { + dLogger.CloseFile() +} + +// GetLevelByName tries to convert the given level name to the represented level code. +// Allowed values are: 'trace', 'debug', 'info', 'warn', 'warning', 'error', 'panic' and 'fatal' +// If an incorrect level name was given an warning is logged and info will be returned +func GetLevelByName(levelName string) Level { + levelName = strings.ToLower(levelName) + switch levelName { + case "trace": + return LevelTrace + case "debug": + return LevelDebug + case "info": + return LevelInfo + case "warn", "warning": + return LevelWarning + case "error": + return LevelError + case "panic", "fatal": + return LevelFatal + + default: + { + Warning("Unable to parse the level name '%s'. Expected 'debug', 'info', 'warn', 'error' or 'fatal'", levelName) + return LevelInfo + } + } +} diff --git a/main/main.go b/main/main.go new file mode 100755 index 0000000..29bfee1 --- /dev/null +++ b/main/main.go @@ -0,0 +1,42 @@ +// main contains a simple example of using this logger package +package main + +import ( + "git.rpjosh.de/RPJosh/go-logger" +) + +func main() { + defer logger.CloseFile() + + // Create a logger configuration + l := &logger.Logger{ + ColoredOutput: true, + PrintSource: true, + LogFilePath: "./logs", + PrintLevel: logger.LevelTrace, + LogLevel: logger.LevelWarning, + } + logger.SetGlobalLogger(l) + + // Printing to the different levels + logger.Trace("You can't find me within %d hours", 5) + logger.Debug("Im a bunny hunter") + logger.Info("That should be a feature.\nOf course!") + logger.Warning("But it would not be safe to use it") + logger.Error("Now it happend") + + // New logger with the same file to log in + lOther := &logger.Logger{ + ColoredOutput: false, + PrintSource: false, + PrintLevel: logger.LevelDebug, + LogLevel: logger.LevelDebug, + } + logger.NewLoggerWithFile(lOther, logger.GetGlobalLogger()) + + lOther.Log(logger.LevelDebug, "Greetings from your brother") + logger.Info("It's a Me, Mario") + lOther.Log(logger.LevelError, "And im your brother luigi") + + logger.Fatal("Bowser enters the room...") +}