// Package lol (log of location) is a simple logging library that prints a high // precision unix timestamp and the source location of a log print to make // tracing errors simpler. Includes a set of logging levels and the ability to // filter out higher log levels for a more quiet output. package lol import ( "fmt" "io" "os" "runtime" "sync/atomic" "time" "github.com/davecgh/go-spew/spew" ) const ( Off = iota Fatal Error Warn Info Debug Trace ) var LevelNames = []string{ "off", "fatal", "error", "warn", "info", "debug", "trace", } type ( // LevelPrinter defines a set of terminal printing primitives that output // with extra data, time, log logLevelList, and code location // Ln prints lists of server with spaces in between Ln func(a ...interface{}) // F prints like fmt.Println surrounded []byte log details F func(format string, a ...interface{}) // S prints a spew.Sdump for an enveloper slice S func(a ...interface{}) // C accepts a function so that the extra computation can be avoided if it is not being // viewed C func(closure func() string) // Chk is a shortcut for printing if there is an error, or returning true Chk func(e error) bool // Err is a pass-through function that uses fmt.Errorf to construct an error and returns the // error after printing it to the log Err func(format string, a ...any) error // LevelPrinter is the set of log printers on each log level. LevelPrinter struct { Ln F S C Chk Err } // LevelSpec is the name, ID and Colorizer for a log level. LevelSpec struct { ID int Name string Colorizer func(a ...any) string } // Entry is a log entry to be printed as json to the log file Entry struct { Time time.Time Level string Package string CodeLocation string Text string } ) var ( // Writer can be swapped out for any io.*Writer* that you want to use instead of stdout. Writer io.Writer = os.Stderr // LevelSpecs specifies the id, string name and color-printing function LevelSpecs = []LevelSpec{ {Off, " ", NoSprint}, {Fatal, "â˜ ī¸ ", fmt.Sprint}, {Error, "🚨 ", fmt.Sprint}, {Warn, "âš ī¸ ", fmt.Sprint}, {Info, "â„šī¸ ", fmt.Sprint}, {Debug, "🔎 ", fmt.Sprint}, {Trace, "đŸ‘ģ ", fmt.Sprint}, } ) // NoSprint is a noop for sprint (it returns nothing no matter what is given to it). func NoSprint(_ ...any) string { return "" } // Log is a set of log printers for the various Level items. type Log struct { F, E, W, I, D, T LevelPrinter } // Check is the set of log levels for a Check operation (prints an error if the error is not // nil). type Check struct { F, E, W, I, D, T Chk } // Errorf prints an error that is also returned as an error, so the error is logged at the site. type Errorf struct { F, E, W, I, D, T Err } // Logger is a collection of things that creates a logger, including levels. type Logger struct { *Log *Check *Errorf } // Level is the level that the logger is printing at. var Level atomic.Int32 // Main is the main logger. var Main = &Logger{} func init() { // Main = &Logger{} Main.Log, Main.Check, Main.Errorf = New(os.Stderr, 2) ll := os.Getenv("LOG_LEVEL") if ll == "" { SetLogLevel("info") } else { for i := range LevelNames { if ll == LevelNames[i] { SetLoggers(i) return } } SetLoggers(Info) } } // SetLoggers configures a log level. func SetLoggers(level int) { Main.Log.T.F("log level %s", LevelSpecs[level].Colorizer(LevelNames[level])) Level.Store(int32(level)) } // GetLogLevel returns the log level number of a string log level. func GetLogLevel(level string) (i int) { for i = range LevelNames { if level == LevelNames[i] { return i } } return Info } // SetLogLevel sets the log level of the logger. func SetLogLevel(level string) { for i := range LevelNames { if level == LevelNames[i] { SetLoggers(i) return } } SetLoggers(Trace) } // JoinStrings joins together anything into a set of strings with space separating the items. func JoinStrings(a ...any) (s string) { for i := range a { s += fmt.Sprint(a[i]) if i < len(a)-1 { s += " " } } return } var msgCol = fmt.Sprint // GetPrinter returns a full logger that writes to the provided io.Writer. func GetPrinter(l int32, writer io.Writer, skip int) LevelPrinter { return LevelPrinter{ Ln: func(a ...interface{}) { if Level.Load() < l { return } _, _ = fmt.Fprintf( writer, "%s%s%s %s\n", msgCol(TimeStamper()), LevelSpecs[l].Colorizer(LevelSpecs[l].Name), JoinStrings(a...), msgCol(GetLoc(skip)), ) }, F: func(format string, a ...interface{}) { if Level.Load() < l { return } _, _ = fmt.Fprintf( writer, "%s%s%s %s\n", msgCol(TimeStamper()), LevelSpecs[l].Colorizer(LevelSpecs[l].Name), fmt.Sprintf(format, a...), msgCol(GetLoc(skip)), ) }, S: func(a ...interface{}) { if Level.Load() < l { return } _, _ = fmt.Fprintf( writer, "%s%s%s %s\n", msgCol(TimeStamper()), LevelSpecs[l].Colorizer(LevelSpecs[l].Name), spew.Sdump(a...), msgCol(GetLoc(skip)), ) }, C: func(closure func() string) { if Level.Load() < l { return } _, _ = fmt.Fprintf( writer, "%s%s%s %s\n", msgCol(TimeStamper()), LevelSpecs[l].Colorizer(LevelSpecs[l].Name), closure(), msgCol(GetLoc(skip)), ) }, Chk: func(e error) bool { if Level.Load() < l { return e != nil } if e != nil { _, _ = fmt.Fprintf( writer, "%s%s%s %s\n", msgCol(TimeStamper()), LevelSpecs[l].Colorizer(LevelSpecs[l].Name), e.Error(), msgCol(GetLoc(skip)), ) return true } return false }, Err: func(format string, a ...interface{}) error { if Level.Load() >= l { _, _ = fmt.Fprintf( writer, "%s%s%s %s\n", msgCol(TimeStamper()), LevelSpecs[l].Colorizer(LevelSpecs[l].Name), fmt.Sprintf(format, a...), msgCol(GetLoc(skip)), ) } return fmt.Errorf(format, a...) }, } } // GetNullPrinter is a logger that doesn't log. func GetNullPrinter() LevelPrinter { return LevelPrinter{ Ln: func(a ...interface{}) {}, F: func(format string, a ...interface{}) {}, S: func(a ...interface{}) {}, C: func(closure func() string) {}, Chk: func(e error) bool { return e != nil }, Err: func( format string, a ...interface{}, ) error { return fmt.Errorf(format, a...) }, } } // New creates a new logger with all the levels and things. func New(writer io.Writer, skip int) (l *Log, c *Check, errorf *Errorf) { if writer == nil { writer = Writer } l = &Log{ T: GetPrinter(Trace, writer, skip), D: GetPrinter(Debug, writer, skip), I: GetPrinter(Info, writer, skip), W: GetPrinter(Warn, writer, skip), E: GetPrinter(Error, writer, skip), F: GetPrinter(Fatal, writer, skip), } c = &Check{ F: l.F.Chk, E: l.E.Chk, W: l.W.Chk, I: l.I.Chk, D: l.D.Chk, T: l.T.Chk, } errorf = &Errorf{ F: l.F.Err, E: l.E.Err, W: l.W.Err, I: l.I.Err, D: l.D.Err, T: l.T.Err, } return } // TimeStamper generates the timestamp for logs. func TimeStamper() (s string) { s = fmt.Sprint(time.Now().UnixMicro()) return } // GetLoc returns the code location of the caller. func GetLoc(skip int) (output string) { _, file, line, _ := runtime.Caller(skip) output = fmt.Sprintf("%s:%d", file, line) return }