repo init

This commit is contained in:
Thilo Karraß 2024-01-08 10:51:00 +01:00
commit ccaedce6f3
9 changed files with 502 additions and 0 deletions

21
.gitignore vendored Normal file
View File

@ -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

51
envelope.go Normal file
View File

@ -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)
}
}

31
example/example.go Normal file
View File

@ -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")
})
*/

49
formatter.go Normal file
View File

@ -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
}

117
formatter_plain.go Normal file
View File

@ -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)
}

7
go.mod Normal file
View File

@ -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

4
go.sum Normal file
View File

@ -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=

131
level.go Normal file
View File

@ -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)
}

91
logger.go Normal file
View File

@ -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
}