commit ccaedce6f3014cf662acfb95fd1b092019b32329 Author: Thilo Karraß Date: Mon Jan 8 10:51:00 2024 +0100 repo init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0ff8b98 --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +# ---> Go +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib +*/awcli + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +.idea/ +*.yaml +example/example diff --git a/envelope.go b/envelope.go new file mode 100644 index 0000000..0833b37 --- /dev/null +++ b/envelope.go @@ -0,0 +1,51 @@ +package log + +import ( + "fmt" + "time" +) + +type envelope struct { + log logger + + lvl level + + // err is an error attached to the envelope + err error + + args map[string]string +} + +func (e *envelope) Msg(f string, args ...any) { + if e.log.Level() < e.lvl { + return // No logging + } + // formatter and output things here + E := &envelopeData{*e, time.Now()} + msg := fmt.Sprintf(f, args...) + loggers[e.log].format.Output(msg, E) + e.lvl.Hook(msg) +} + +func (e *envelope) Arg(name string, value any) *envelope { + e.args[name] = fmt.Sprintf("%v", value) + return e +} + +func (e *envelope) Err(err error) *envelope { + e.err = err + return e +} + +func (e *envelope) To(l logger) *envelope { + e.log = l + return e +} + +type Fn func(fmt string, args ...any) + +func (e *envelope) If(msg func(Fn)) { + if e.log.Level() >= e.lvl { + msg(e.Msg) + } +} diff --git a/example/example.go b/example/example.go new file mode 100644 index 0000000..803d4af --- /dev/null +++ b/example/example.go @@ -0,0 +1,31 @@ +package main + +import ( + "udico.de/util/log" +) + +var logger = log.Logger("MyLogger").SetLevel(log.TRACE) + +func main() { + log.DefaultLogger.SetLevel(log.DEBUG) + log.WARN.To(logger).Msg("Now doing things :)") + + log.INFO.Msg("hallo %v", "bla") + log.TRACE.To(logger).Arg("a", "b").If(func(msg log.Fn) { + msg("Calculating important things ...") + msg("hi!") + }) + + log.PANIC.To(logger).Msg("Bad things happened :(") + log.NOTICE.Msg("I'm done!") +} + +/* + log.INFO.Msg("...", a) + log.INFO.Ctx(lo).With("field", "").Msg(...) + + log.TRACE.If(func(log logfn) { + // some expensive code goes here + log("something") + }) +*/ diff --git a/formatter.go b/formatter.go new file mode 100644 index 0000000..74e2e43 --- /dev/null +++ b/formatter.go @@ -0,0 +1,49 @@ +package log + +import "time" + +// Formatter ... +// - must be thread safe +// TODO: measure if it's faster to have a goroutine consuming a chan, compared to locking using a mutex. +// +// TODO: what about buffered channels? How/When will the timestamp be added? +// +// -> Answer: should be added to the Envelope right before the call to Output() +// -> There should be an in between layer doing the channel thing calling Output() +type Formatter interface { + Output(message string, e Envelope) +} + +type Envelope interface { + Logger() logger + Level() level + Error() error + Arguments() map[string]string + Time() time.Time +} + +// envelopeData wraps an envelope to match the Envelope interface. +type envelopeData struct { + envelope + t time.Time +} + +func (e envelopeData) Logger() logger { + return e.log +} + +func (e envelopeData) Level() level { + return e.lvl +} + +func (e envelopeData) Error() error { + return e.err +} + +func (e envelopeData) Arguments() map[string]string { + return e.args +} + +func (e envelopeData) Time() time.Time { + return e.t +} diff --git a/formatter_plain.go b/formatter_plain.go new file mode 100644 index 0000000..78b8817 --- /dev/null +++ b/formatter_plain.go @@ -0,0 +1,117 @@ +package log + +import ( + "fmt" + "golang.org/x/term" + "io" + "os" +) + +// ColorMode indicates if colorized output should be used +type ColorMode int + +const ( + // ColorAuto means to automatically detect if a writer is an interactive terminal and + // if so use colorized output. + ColorAuto ColorMode = 0 + + // ColorNever avoids colorized output at all. + ColorNever ColorMode = -1 + + // ColorAlways enforces colorized output even if it is routed into some file. + ColorAlways ColorMode = 1 + + // ColorOn means to enable colorized output. + ColorOn ColorMode = 1 + + // ColorOff means not to use colorized output. + ColorOff ColorMode = -1 +) + +// Colorize indicates if colorized output should be used. +func (c ColorMode) Colorize() bool { + return c > 0 +} + +type PlainFormatter struct { + UseColor ColorMode + + /* avoid checking the output fd on each log message. Only do so if fd changed */ + lastFd int + fdColor ColorMode +} + +func (f *PlainFormatter) resolveColorMode(target io.Writer) ColorMode { + if f.UseColor == ColorAuto { + fd := -1 + if file, ok := target.(*os.File); ok { + fd = int(file.Fd()) + } + + if f.fdColor == ColorAuto || fd != f.lastFd { + // update cache + f.lastFd = fd + if fd > -1 && term.IsTerminal(fd) { + f.fdColor = ColorOn + } else { + f.fdColor = ColorOff + } + } + return f.fdColor + } + return f.UseColor +} + +type levelColorData struct { + Dark string + Light string +} + +/* + fg bg +Black 30/90 40/100 +Red 31 41 +Green 32 42 +Yellow 33 43 +Blue 34 44 +Magenta 35 45 +Cyan 36 46 +White 37 47 +*/ + +var levelColorDatas = []levelColorData{ + /* SILENT */ {Dark: "\033[30;107m", Light: ""}, // never ever log it? + /* PANIC */ {Dark: "\033[93;101m", Light: ""}, + /* FATAL */ {Dark: "\033[30;101m", Light: ""}, + /* ERROR */ {Dark: "\033[91m", Light: ""}, + /* WARN */ {Dark: "\033[33m", Light: ""}, + /* NOTICE */ {Dark: "\033[95m", Light: ""}, + /* INFO */ {Dark: "\033[97m", Light: ""}, + /* DEBUG */ {Dark: "\033[37m", Light: ""}, + /* TRACE */ {Dark: "\033[90m", Light: ""}, +} + +func (f *PlainFormatter) Output(message string, envelope Envelope) { + o := envelope.Logger().Target() + cm := f.resolveColorMode(o) + msg := "" + if cm.Colorize() { + msg += levelColorDatas[envelope.Level()].Dark + } + + msg += envelope.Time().Format("20060102-150405.000000") + " " + if envelope.Logger() != 0 { + msg += "[" + envelope.Logger().String() + "] " + } + msg += envelope.Level().String() + ": " + msg += message + if cm.Colorize() { + msg += "\033[0m" + } + if msg[len(msg)-1] != '\n' { + msg += "\n" + } + // "time (logger) level message args" + + fmt.Fprint(o, msg) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..e751edf --- /dev/null +++ b/go.mod @@ -0,0 +1,7 @@ +module udico.de/util/log + +go 1.21.5 + +require golang.org/x/term v0.16.0 + +require golang.org/x/sys v0.16.0 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e838d28 --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE= +golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= diff --git a/level.go b/level.go new file mode 100644 index 0000000..99035af --- /dev/null +++ b/level.go @@ -0,0 +1,131 @@ +package log + +import ( + "fmt" + "strings" +) + +type level uint8 + +/* +Emergency (emerg): indicates that the system is unusable and requires immediate attention. +Alert (alert): indicates that immediate action is necessary to resolve a critical issue. +Critical (crit): signifies critical conditions in the program that demand intervention to prevent system failure. +Error (error): indicates error conditions that impair some operation but are less severe than critical situations. +Warning (warn): signifies potential issues that may lead to errors or unexpected behavior in the future if not addressed. +Notice (notice): applies to normal but significant conditions that may require monitoring. +Informational (info): includes messages that provide a record of the normal operation of the system. +Debug (debug): intended for logging detailed information about the system for debugging purposes. +*/ + +const ( + // SILENT means no log at all. not even panics. We just don't care about the end of the world. + SILENT level = iota + + // PANIC (also: Emergency) indicates events which cannot be handled gracefully. Program execution will terminate. + PANIC + + // FATAL (also: Critical, Alert) indicates a situation which requires user interaction to resolve. + FATAL + + // ERROR indicates error conditions which may result in malfunction. + ERROR + + // WARN signifies potential issues that may lead to errors or unexpected behavior in the future if not addressed. + WARN + + // NOTICE logs are normal but significant information. + NOTICE + + // INFO indicates normal, informational messages. + INFO + + // DEBUG is intended for logging detailed information for debugging purposes. + DEBUG + + // TRACE is the most detailed level of log output. Might spam your logs. + TRACE +) + +type levelData struct { + Name string + Short string + Hook func(string) +} + +var levelDatas = []levelData{ // No, it's not a map! Indexing into a slice is faster + {Name: "SILENT", Short: "SLT", Hook: nil}, + {Name: "PANIC", Short: "PNC", Hook: func(msg string) { panic(msg) }}, + {Name: "FATAL", Short: "FTL", Hook: nil}, + {Name: "ERROR", Short: "ERR", Hook: nil}, + {Name: "WARN", Short: "WRN", Hook: nil}, + {Name: "NOTICE", Short: "NOT", Hook: nil}, + {Name: "INFO", Short: "INF", Hook: nil}, + {Name: "DEBUG", Short: "DBG", Hook: nil}, + {Name: "TRACE", Short: "TRC", Hook: nil}, +} + +// To sends the resulting log message to a given logger +func (l level) To(log logger) *envelope { + return &envelope{ + log: log, + lvl: l, + err: nil, + args: make(map[string]string), + } +} + +// Msg emits the given message to the default logger. +func (l level) Msg(format string, args ...any) { + l.To(0).Msg(format, args...) +} + +// Err attaches an error to the log message. +func (l level) Err(err error) *envelope { + return l.To(0).Err(err) +} + +// Arg attaches a named argument to the log message. +func (l level) Arg(name string, value any) *envelope { + return l.To(0).Arg(name, value) +} + +// If executes the given function only, if the logger is set to +func (l level) If(msg func(Fn)) { + l.To(0).If(msg) +} + +// Hook executes the associated hook, if any +func (l level) Hook(msg string) { + if levelDatas[l].Hook != nil { + levelDatas[l].Hook(msg) + } +} + +func (l level) String() string { + return levelDatas[l].Name +} + +func ParseLevel(levelstring string) (level, error) { + switch strings.ToLower(levelstring) { + case "silent", "off", "none": + return SILENT, nil + case "panic", "emergency", "emerg": + return PANIC, nil + case "fatal", "crit", "critical", "alert": + return FATAL, nil + case "error": + return ERROR, nil + case "warn", "warning": + return WARN, nil + case "notice": + return NOTICE, nil + case "info": + return INFO, nil + case "debug": + return DEBUG, nil + case "trace": + return TRACE, nil + } + return 0, fmt.Errorf("invalid loglevel '%v'", levelstring) +} diff --git a/logger.go b/logger.go new file mode 100644 index 0000000..2fa1ffa --- /dev/null +++ b/logger.go @@ -0,0 +1,91 @@ +package log + +import ( + "io" + "os" +) + +// logger is just an index into the loggers pool. +type logger int + +const DefaultLogger logger = 0 + +// loggers is the pool of available loggers. The first entry always is empty, +// describing the default logger. +//var loggers []string = make([]string, 1, 10) +//var levels []level = make([]level, 1, 10) +//var targets []io.Writer = make([]io.Writer, 1, 10) + +type loggerData struct { + name string + level level + target io.Writer + format Formatter +} + +var loggers []loggerData = make([]loggerData, 1, 10) + +func init() { + DefaultLogger.SetLevel(INFO) + DefaultLogger.SetTarget(os.Stdout) + DefaultLogger.SetFormatter(&PlainFormatter{}) +} + +// Logger returns a logger with the given name. If there is no logger with this +// name, a new one will be created. +func Logger(name string) logger { + for i, v := range loggers { + if v.name == name { + return logger(i) + } + } + loggers = append(loggers, loggerData{ + name: name, + level: INFO, + target: os.Stdout, + format: &PlainFormatter{}, + }) + return logger(len(loggers) - 1) +} + +// Level returns the log level this logger is set to. +func (l logger) Level() level { + return loggers[l].level +} + +// SetLevel sets the log level for this logger. Returns the logger, which enables chaining during initialization. +func (l logger) SetLevel(lvl level) logger { + loggers[l].level = lvl + return l +} + +func (l logger) Target() io.Writer { + return loggers[l].target +} + +// SetTarget sets the logging target. Returns the logger, which enables chaining during initialization. +func (l logger) SetTarget(t io.Writer) logger { + loggers[l].target = t + return l +} + +func (l logger) Formatter() Formatter { + return loggers[l].format +} + +func (l logger) SetFormatter(f Formatter) logger { + loggers[l].format = f + return l +} + +func (l logger) String() string { + return loggers[l].name +} + +// Configure parses a config string, enabling the configuration of multiple loggers at once. +// [logger:]level[@target][#format] +// target may be a file name +func Configure(config string) error { + + return nil +}