Files
lol/log.go

337 lines
7.3 KiB
Go
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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
}