Files
indra/pkg/proc/log/log.go
2023-06-09 07:12:53 +01:00

417 lines
10 KiB
Go

package log
import (
"fmt"
"github.com/davecgh/go-spew/spew"
"github.com/gookit/color"
"go.uber.org/atomic"
"io"
"os"
"path/filepath"
"runtime"
"sort"
"strings"
"sync"
"time"
)
// The LogLevel settings used in proc
const (
Off LogLevel = iota
Fatal
Error
Check
Warn
Info
Debug
Trace
)
var (
// LevelSpecs specifies the id, string name and color-printing function
LevelSpecs = map[LogLevel]LevelSpec{
Off: gLS(Off, 0, 0, 0),
Fatal: gLS(Fatal, 255, 0, 0),
Error: gLS(Error, 255, 128, 0),
Check: gLS(Check, 255, 255, 0),
Warn: gLS(Warn, 255, 255, 0),
Info: gLS(Info, 0, 255, 0),
Debug: gLS(Debug, 0, 128, 255),
Trace: gLS(Trace, 128, 0, 255),
}
// LocTimeStampFormat is a custom time format that provides millisecond precision.
LocTimeStampFormat = "2006-01-02T15:04:05.000000"
// LvlStr is a map that provides the uniform width strings that are printed
// to identify the LogLevel of a log entry.
LvlStr = LevelMap{
Off: "off ",
Fatal: "fatal",
Error: "error",
Warn: "warn ",
Info: "info ",
Check: "check",
Debug: "debug",
Trace: "trace",
}
// log is your generic Logger creation invocation that uses the version data
// in version.go that provides the current compilation path prefix for making
// relative paths for log printing code locations.
log = GetLogger()
lvlStrs = map[string]LogLevel{
"off": Off,
"fatal": Fatal,
"error": Error,
"check": Check,
"warn": Warn,
"info": Info,
"debug": Debug,
"trace": Trace,
}
startTime = time.Now()
timeStampFormat = "15:04:05.000"
tty io.Writer = os.Stderr
file *os.File
path string
writer = tty
writerMx sync.Mutex
logLevel = Info
// App is the name of the application. Change this at the beginning of
// an application main.
App atomic.String
Longest atomic.Uint32
// allSubsystems stores all package subsystem names found in the current
// application.
allSubsystems []string
)
type (
LevelMap map[LogLevel]string
// LogLevel is a code representing a scale of importance and context for log
// entries.
LogLevel int32
// Println prints lists of interfaces with spaces in between
Println func(a ...interface{})
// Printf prints like fmt.Println surrounded by log details
Printf func(format string, a ...interface{})
// Prints prints a spew.Sdump for an interface slice
Prints func(a ...interface{})
// Printc accepts a function so that the extra computation can be avoided if
// it is not being viewed
Printc func(closure func() string)
// Chk is a shortcut for printing if there is an error, or returning true
Chk func(e error) bool
// LevelPrinter defines a set of terminal printing primitives that output
// with extra data, time, level, and code location
LevelPrinter struct {
Ln Println
// F prints like fmt.Println surrounded by log details
F Printf
// S uses spew.dump to show the content of a variable
S Prints
// C accepts a function so that the extra computation can be avoided if
// it is not being viewed
C Printc
// Chk is a shortcut for printing if there is an error, or returning
// true
Chk Chk
}
// LevelSpec is a key pair of log level and the text colorizer used
// for it.
LevelSpec struct {
Name string
Colorizer func(format string, a ...interface{}) string
}
// Logger is a set of log printers for the various LogLevel items.
Logger struct {
F, E, W, I, D, T LevelPrinter
}
)
// Add adds a subsystem to the list of known subsystems and returns the
// string.
func Add() (subsystem string) {
writerMx.Lock()
defer writerMx.Unlock()
_, f, _, _ := runtime.Caller(1)
subsystemPath := filepath.Dir(f)
allSubsystems = append(allSubsystems, subsystemPath)
sortSubsystemsList()
return
}
func GetAllSubsystems() (o []string) {
writerMx.Lock()
defer writerMx.Unlock()
o = make([]string, len(allSubsystems))
for i := range allSubsystems {
o[i] = allSubsystems[i]
}
return
}
func GetLevelByString(lvl string, def LogLevel) (ll LogLevel) {
var exists bool
if ll, exists = lvlStrs[lvl]; !exists {
return def
}
return ll
}
func GetLevelName(ll LogLevel) string {
return strings.TrimSpace(LvlStr[ll])
}
// GetLoc calls runtime.Caller and formats as expected by source code editors
// for terminal hyperlinks
//
// Regular expressions and the substitution texts to make these clickable in
// Tilix and other RE hyperlink configurable terminal emulators:
//
// This matches the shortened paths generated in this command and printed at
// the very beginning of the line as this logger prints:
/*
^((([\/a-zA-Z@0-9-_.]+/)+([a-zA-Z@0-9-_.]+)):([0-9]+))
/usr/local/bin/goland --line $5 $GOPATH/src/github.com/p9c/matrjoska/$2
*/
// I have used a shell variable there but tilix doesn't expand them,
// so put your GOPATH in manually, and obviously change the repo subpath.
func GetLoc(skip int, subsystem string) (output string) {
_, file, line, _ := runtime.Caller(skip)
defer func() {
if r := recover(); r != nil {
fmt.Fprintln(
os.Stderr, "getloc panic on subsystem",
subsystem, file,
)
}
}()
//split := strings.Split(file, subsystem)
//if len(split) < 2 {
// output = fmt.Sprint(
// color.White.Sprint(subsystem),
// color.Gray.Sprint(
// file, ":", line,
// ),
// )
//} else {
output = fmt.Sprint(
subsystem,
file, ":", line,
)
//}
return
}
func GetLogLevel() (l LogLevel) {
writerMx.Lock()
defer writerMx.Unlock()
l = logLevel
return
}
// GetLogger returns a set of LevelPrinter with their subsystem preloaded
func GetLogger() (l *Logger) {
ss := Add()
return &Logger{
getOnePrinter(Fatal, ss),
getOnePrinter(Error, ss),
getOnePrinter(Warn, ss),
getOnePrinter(Info, ss),
getOnePrinter(Debug, ss),
getOnePrinter(Trace, ss),
}
}
func SetLogFilePath(p string) (err error) {
writerMx.Lock()
defer writerMx.Unlock()
if file != nil {
err = StopLogToFile()
if err != nil {
return err
}
path = p
err = StartLogToFile()
} else {
path = p
}
return
}
func SetLogLevel(l LogLevel) {
writerMx.Lock()
defer writerMx.Unlock()
logLevel = l
}
// SetTimeStampFormat sets a custom timeStampFormat for the logger
func SetTimeStampFormat(format string) {
timeStampFormat = format
}
func StartLogToFile() (err error) {
writerMx.Lock()
defer writerMx.Unlock()
file, err = os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0600)
if err != nil {
fmt.Fprintf(os.Stderr, "%s\n", path)
return err
}
writer = io.MultiWriter(tty, file)
return
}
func StopLogToFile() (err error) {
writerMx.Lock()
defer writerMx.Unlock()
writer = tty
if file != nil {
// err = file.Close()
file = nil
}
return
}
func (l LevelMap) String() (s string) {
ss := make([]string, len(l))
for i := range l {
ss[i] = strings.TrimSpace(l[i])
}
return strings.Join(ss, " ")
}
func _c(level LogLevel, subsystem string) Printc {
return func(closure func() string) {
logPrint(level, subsystem, closure)()
}
}
func _chk(level LogLevel, subsystem string) Chk {
return func(e error) (is bool) {
if e != nil {
logPrint(level,
subsystem,
joinStrings(
" ",
e))()
is = true
}
return
}
}
func _f(level LogLevel, subsystem string) Printf {
return func(format string, a ...interface{}) {
logPrint(
level, subsystem, func() string {
return fmt.Sprintf(format, a...)
},
)()
}
}
// The collection of the different types of log print functions,
// includes spew.Dump, closure and error check printers.
func _ln(l LogLevel, ss string) Println {
return func(a ...interface{}) {
logPrint(l, ss, joinStrings(" ", a...))()
}
}
func _s(level LogLevel, subsystem string) Prints {
return func(a ...interface{}) {
text := "spew:\n"
if s, ok := a[0].(string); ok {
text = strings.TrimSpace(s) + "\n"
a = a[1:]
}
logPrint(
level, subsystem, func() string {
return color.Gray.Sprint(text + spew.Sdump(a...))
},
)()
}
}
// gLS is a helper to make more compact declarations of LevelSpec names and
// colors by using the LogLevel LvlStr map.
func gLS(lvl LogLevel, r, g, b byte) LevelSpec {
return LevelSpec{
Name: LvlStr[lvl],
Colorizer: color.Bit24(r, g, b, false).Sprintf,
}
}
func getOnePrinter(level LogLevel, subsystem string) LevelPrinter {
return LevelPrinter{
Ln: _ln(level, subsystem),
F: _f(level, subsystem),
S: _s(level, subsystem),
C: _c(level, subsystem),
Chk: _chk(level, subsystem),
}
}
// getTimeText is a helper that returns the current time with the
// timeStampFormat that is configured.
func getTimeText(tsf string) string { return time.Now().Format(tsf) }
// joinStrings constructs a string from a slice of interface same as Println but
// without the terminal newline
func joinStrings(sep string, a ...interface{}) func() (o string) {
return func() (o string) {
for i := range a {
o += fmt.Sprint(a[i])
if i < len(a)-1 {
o += sep
}
}
return
}
}
// logPrint is the generic log printing function that provides the base
// format for log entries.
func logPrint(
level LogLevel,
subsystem string,
printFunc func() string,
) func() {
return func() {
writerMx.Lock()
defer writerMx.Unlock()
if level > logLevel {
return
}
tsf := timeStampFormat
timeText := getTimeText(tsf)
loc := GetLoc(3, subsystem)
if int(Longest.Load()) < len(loc) {
Longest.Store(uint32(len(loc)))
}
loc = color.OpItalic.Sprint(color.OpUnderscore.Sprint(loc)) + strings.Repeat(" ", int(Longest.Load())-len(loc)+1)
formatString := "%s%-6v %s %s %s"
timeText = time.Now().Format("2006-01-02 15:04:05.999999999 UTC+0700")
var app string
if len(App.Load()) > 0 {
app = fmt.Sprint(App.Load(), " ")
}
s := fmt.Sprintf(
formatString,
app,
LevelSpecs[level].Colorizer(
LevelSpecs[level].Name,
),
LevelSpecs[level].Colorizer(loc),
printFunc(),
LevelSpecs[level].Colorizer(timeText),
)
s = strings.TrimSuffix(s, "\n")
fmt.Fprintln(writer, s)
}
}
// sortSubsystemsList sorts the list of subsystems, to keep the data read-only,
// call this function right at the top of the main, which runs after
// declarations and main/init. Really this is just here to alert the reader.
func sortSubsystemsList() { sort.Strings(allSubsystems) }