log.go raw

   1  // Package lol (log of location) is a simple logging library that prints a high
   2  // precision unix timestamp and the source location of a log print to make
   3  // tracing errors simpler. Includes a set of logging levels and the ability to
   4  // filter out higher log levels for a more quiet output.
   5  package lol
   6  
   7  import (
   8  	"fmt"
   9  	"io"
  10  	"os"
  11  	"runtime"
  12  	"sync/atomic"
  13  	"time"
  14  
  15  	"github.com/davecgh/go-spew/spew"
  16  )
  17  
  18  const (
  19  	Off = iota
  20  	Fatal
  21  	Error
  22  	Warn
  23  	Info
  24  	Debug
  25  	Trace
  26  )
  27  
  28  var LevelNames = []string{
  29  	"off",
  30  	"fatal",
  31  	"error",
  32  	"warn",
  33  	"info",
  34  	"debug",
  35  	"trace",
  36  }
  37  
  38  type (
  39  	// LevelPrinter defines a set of terminal printing primitives that output
  40  	// with extra data, time, log logLevelList, and code location
  41  
  42  	// Ln prints lists of server with spaces in between
  43  	Ln func(a ...interface{})
  44  	// F prints like fmt.Println surrounded []byte log details
  45  	F func(format string, a ...interface{})
  46  	// S prints a spew.Sdump for an enveloper slice
  47  	S func(a ...interface{})
  48  	// C accepts a function so that the extra computation can be avoided if it is not being
  49  	// viewed
  50  	C func(closure func() string)
  51  	// Chk is a shortcut for printing if there is an error, or returning true
  52  	Chk func(e error) bool
  53  	// Err is a pass-through function that uses fmt.Errorf to construct an error and returns the
  54  	// error after printing it to the log
  55  	Err func(format string, a ...any) error
  56  
  57  	// LevelPrinter is the set of log printers on each log level.
  58  	LevelPrinter struct {
  59  		Ln
  60  		F
  61  		S
  62  		C
  63  		Chk
  64  		Err
  65  	}
  66  
  67  	// LevelSpec is the name, ID and Colorizer for a log level.
  68  	LevelSpec struct {
  69  		ID        int
  70  		Name      string
  71  		Colorizer func(a ...any) string
  72  	}
  73  
  74  	// Entry is a log entry to be printed as json to the log file
  75  	Entry struct {
  76  		Time         time.Time
  77  		Level        string
  78  		Package      string
  79  		CodeLocation string
  80  		Text         string
  81  	}
  82  )
  83  
  84  var (
  85  	// Writer can be swapped out for any io.*Writer* that you want to use instead of stdout.
  86  	Writer io.Writer = os.Stderr
  87  
  88  	// LevelSpecs specifies the id, string name and color-printing function
  89  	LevelSpecs = []LevelSpec{
  90  		{Off, " ", NoSprint},
  91  		{Fatal, "â˜ ī¸ ", fmt.Sprint},
  92  		{Error, "🚨 ", fmt.Sprint},
  93  		{Warn, "âš ī¸ ", fmt.Sprint},
  94  		{Info, "â„šī¸ ", fmt.Sprint},
  95  		{Debug, "🔎 ", fmt.Sprint},
  96  		{Trace, "đŸ‘ģ ", fmt.Sprint},
  97  	}
  98  )
  99  
 100  // NoSprint is a noop for sprint (it returns nothing no matter what is given to it).
 101  func NoSprint(_ ...any) string { return "" }
 102  
 103  // Log is a set of log printers for the various Level items.
 104  type Log struct {
 105  	F, E, W, I, D, T LevelPrinter
 106  }
 107  
 108  // Check is the set of log levels for a Check operation (prints an error if the error is not
 109  // nil).
 110  type Check struct {
 111  	F, E, W, I, D, T Chk
 112  }
 113  
 114  // Errorf prints an error that is also returned as an error, so the error is logged at the site.
 115  type Errorf struct {
 116  	F, E, W, I, D, T Err
 117  }
 118  
 119  // Logger is a collection of things that creates a logger, including levels.
 120  type Logger struct {
 121  	*Log
 122  	*Check
 123  	*Errorf
 124  }
 125  
 126  // Level is the level that the logger is printing at.
 127  var Level atomic.Int32
 128  
 129  // Main is the main logger.
 130  var Main = &Logger{}
 131  
 132  func init() {
 133  	// Main = &Logger{}
 134  	Main.Log, Main.Check, Main.Errorf = New(os.Stderr, 2)
 135  	ll := os.Getenv("LOG_LEVEL")
 136  	if ll == "" {
 137  		SetLogLevel("info")
 138  	} else {
 139  		for i := range LevelNames {
 140  			if ll == LevelNames[i] {
 141  				SetLoggers(i)
 142  				return
 143  			}
 144  		}
 145  		SetLoggers(Info)
 146  	}
 147  }
 148  
 149  // SetLoggers configures a log level.
 150  func SetLoggers(level int) {
 151  	Main.Log.T.F("log level %s", LevelSpecs[level].Colorizer(LevelNames[level]))
 152  	Level.Store(int32(level))
 153  }
 154  
 155  // GetLogLevel returns the log level number of a string log level.
 156  func GetLogLevel(level string) (i int) {
 157  	for i = range LevelNames {
 158  		if level == LevelNames[i] {
 159  			return i
 160  		}
 161  	}
 162  	return Info
 163  }
 164  
 165  // SetLogLevel sets the log level of the logger.
 166  func SetLogLevel(level string) {
 167  	for i := range LevelNames {
 168  		if level == LevelNames[i] {
 169  			SetLoggers(i)
 170  			return
 171  		}
 172  	}
 173  	SetLoggers(Trace)
 174  }
 175  
 176  // JoinStrings joins together anything into a set of strings with space separating the items.
 177  func JoinStrings(a ...any) (s string) {
 178  	for i := range a {
 179  		s += fmt.Sprint(a[i])
 180  		if i < len(a)-1 {
 181  			s += " "
 182  		}
 183  	}
 184  	return
 185  }
 186  
 187  var msgCol = fmt.Sprint
 188  
 189  // GetPrinter returns a full logger that writes to the provided io.Writer.
 190  func GetPrinter(l int32, writer io.Writer, skip int) LevelPrinter {
 191  	return LevelPrinter{
 192  		Ln: func(a ...interface{}) {
 193  			if Level.Load() < l {
 194  				return
 195  			}
 196  			_, _ = fmt.Fprintf(
 197  				writer,
 198  				"%s%s%s %s\n",
 199  				msgCol(TimeStamper()),
 200  				LevelSpecs[l].Colorizer(LevelSpecs[l].Name),
 201  				JoinStrings(a...),
 202  				msgCol(GetLoc(skip)),
 203  			)
 204  		},
 205  		F: func(format string, a ...interface{}) {
 206  			if Level.Load() < l {
 207  				return
 208  			}
 209  			_, _ = fmt.Fprintf(
 210  				writer,
 211  				"%s%s%s %s\n",
 212  				msgCol(TimeStamper()),
 213  				LevelSpecs[l].Colorizer(LevelSpecs[l].Name),
 214  				fmt.Sprintf(format, a...),
 215  				msgCol(GetLoc(skip)),
 216  			)
 217  		},
 218  		S: func(a ...interface{}) {
 219  			if Level.Load() < l {
 220  				return
 221  			}
 222  			_, _ = fmt.Fprintf(
 223  				writer,
 224  				"%s%s%s %s\n",
 225  				msgCol(TimeStamper()),
 226  				LevelSpecs[l].Colorizer(LevelSpecs[l].Name),
 227  				spew.Sdump(a...),
 228  				msgCol(GetLoc(skip)),
 229  			)
 230  		},
 231  		C: func(closure func() string) {
 232  			if Level.Load() < l {
 233  				return
 234  			}
 235  			_, _ = fmt.Fprintf(
 236  				writer,
 237  				"%s%s%s %s\n",
 238  				msgCol(TimeStamper()),
 239  				LevelSpecs[l].Colorizer(LevelSpecs[l].Name),
 240  				closure(),
 241  				msgCol(GetLoc(skip)),
 242  			)
 243  		},
 244  		Chk: func(e error) bool {
 245  			if Level.Load() < l {
 246  				return e != nil
 247  			}
 248  			if e != nil {
 249  				_, _ = fmt.Fprintf(
 250  					writer,
 251  					"%s%s%s %s\n",
 252  					msgCol(TimeStamper()),
 253  					LevelSpecs[l].Colorizer(LevelSpecs[l].Name),
 254  					e.Error(),
 255  					msgCol(GetLoc(skip)),
 256  				)
 257  				return true
 258  			}
 259  			return false
 260  		},
 261  		Err: func(format string, a ...interface{}) error {
 262  			if Level.Load() >= l {
 263  				_, _ = fmt.Fprintf(
 264  					writer,
 265  					"%s%s%s %s\n",
 266  					msgCol(TimeStamper()),
 267  					LevelSpecs[l].Colorizer(LevelSpecs[l].Name),
 268  					fmt.Sprintf(format, a...),
 269  					msgCol(GetLoc(skip)),
 270  				)
 271  			}
 272  			return fmt.Errorf(format, a...)
 273  		},
 274  	}
 275  }
 276  
 277  // GetNullPrinter is a logger that doesn't log.
 278  func GetNullPrinter() LevelPrinter {
 279  	return LevelPrinter{
 280  		Ln:  func(a ...interface{}) {},
 281  		F:   func(format string, a ...interface{}) {},
 282  		S:   func(a ...interface{}) {},
 283  		C:   func(closure func() string) {},
 284  		Chk: func(e error) bool { return e != nil },
 285  		Err: func(
 286  			format string, a ...interface{},
 287  		) error {
 288  			return fmt.Errorf(format, a...)
 289  		},
 290  	}
 291  }
 292  
 293  // New creates a new logger with all the levels and things.
 294  func New(writer io.Writer, skip int) (l *Log, c *Check, errorf *Errorf) {
 295  	if writer == nil {
 296  		writer = Writer
 297  	}
 298  	l = &Log{
 299  		T: GetPrinter(Trace, writer, skip),
 300  		D: GetPrinter(Debug, writer, skip),
 301  		I: GetPrinter(Info, writer, skip),
 302  		W: GetPrinter(Warn, writer, skip),
 303  		E: GetPrinter(Error, writer, skip),
 304  		F: GetPrinter(Fatal, writer, skip),
 305  	}
 306  	c = &Check{
 307  		F: l.F.Chk,
 308  		E: l.E.Chk,
 309  		W: l.W.Chk,
 310  		I: l.I.Chk,
 311  		D: l.D.Chk,
 312  		T: l.T.Chk,
 313  	}
 314  	errorf = &Errorf{
 315  		F: l.F.Err,
 316  		E: l.E.Err,
 317  		W: l.W.Err,
 318  		I: l.I.Err,
 319  		D: l.D.Err,
 320  		T: l.T.Err,
 321  	}
 322  	return
 323  }
 324  
 325  // TimeStamper generates the timestamp for logs.
 326  func TimeStamper() (s string) {
 327  	s = fmt.Sprint(time.Now().UnixMicro())
 328  	return
 329  }
 330  
 331  // GetLoc returns the code location of the caller.
 332  func GetLoc(skip int) (output string) {
 333  	_, file, line, _ := runtime.Caller(skip)
 334  	output = fmt.Sprintf("%s:%d", file, line)
 335  	return
 336  }
 337