Files
prevara/pkg/log/logg.go

576 lines
15 KiB
Go

package log
import (
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"runtime"
"sort"
"strings"
"sync"
"time"
"github.com/davecgh/go-spew/spew"
"github.com/gookit/color"
uberatomic "go.uber.org/atomic"
)
const (
_Off = iota
_Fatal
_Error
_Chek
_Warn
_Info
_Debug
_Trace
)
type (
// LevelPrinter defines a set of terminal printing primitives that output with
// extra data, time, log logLevelList, and code location
LevelPrinter struct {
// Ln prints lists of interfaces with spaces in between
Ln func(a ...interface{})
// F prints like fmt.Println surrounded by log details
F func(format string, a ...interface{})
// S prints a spew.Sdump for an interface 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
}
logLevelList struct {
Off, Fatal, Error, Check, Warn, Info, Debug, Trace int32
}
LevelSpec struct {
ID int32
Name string
Colorizer func(format string, a ...interface{}) 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 (
logger_started = time.Now()
App = " pod"
AppColorizer = color.White.Sprint
// sep is just a convenient shortcut for this very longwinded expression
sep = string(os.PathSeparator)
currentLevel = uberatomic.NewInt32(logLevels.Info)
// writer can be swapped out for any io.*writer* that you want to use instead of
// stdout.
writer io.Writer = os.Stderr
// allSubsystems stores all of the package subsystem names found in the current
// application
allSubsystems []string
// highlighted is a text that helps visually distinguish a log entry by category
highlighted = make(map[string]struct{})
// logFilter specifies a set of packages that will not pr logs
logFilter = make(map[string]struct{})
// mutexes to prevent concurrent map accesses
highlightMx, _logFilterMx sync.Mutex
// logLevels is a shorthand access that minimises possible Name collisions in the
// dot import
logLevels = logLevelList{
Off: _Off,
Fatal: _Fatal,
Error: _Error,
Check: _Chek,
Warn: _Warn,
Info: _Info,
Debug: _Debug,
Trace: _Trace,
}
// LevelSpecs specifies the id, string name and color-printing function
LevelSpecs = []LevelSpec{
{logLevels.Off, "off ", color.Bit24(0, 0, 0, false).Sprintf},
{logLevels.Fatal, "fatal", color.Bit24(128, 0, 0, false).Sprintf},
{logLevels.Error, "error", color.Bit24(255, 0, 0, false).Sprintf},
{logLevels.Check, "check", color.Bit24(255, 255, 0, false).Sprintf},
{logLevels.Warn, "warn ", color.Bit24(0, 255, 0, false).Sprintf},
{logLevels.Info, "info ", color.Bit24(255, 255, 0, false).Sprintf},
{logLevels.Debug, "debug", color.Bit24(0, 128, 255, false).Sprintf},
{logLevels.Trace, "trace", color.Bit24(128, 0, 255, false).Sprintf},
}
Levels = []string{
Off,
Fatal,
Error,
Check,
Warn,
Info,
Debug,
Trace,
}
LogChanDisabled = uberatomic.NewBool(true)
LogChan chan Entry
)
const (
Off = "off"
Fatal = "fatal"
Error = "error"
Warn = "warn"
Info = "info"
Check = "check"
Debug = "debug"
Trace = "trace"
)
// AddLogChan adds a channel that log entries are sent to
func AddLogChan() (ch chan Entry) {
LogChanDisabled.Store(false)
if LogChan != nil {
panic("warning warning")
}
// L.Writer.Write.Store( false
LogChan = make(chan Entry)
return LogChan
}
// GetLogPrinterSet returns a set of LevelPrinter with their subsystem preloaded
func GetLogPrinterSet(subsystem string) (Fatal, Error, Warn, Info, Debug, Trace LevelPrinter) {
return _getOnePrinter(_Fatal, subsystem),
_getOnePrinter(_Error, subsystem),
_getOnePrinter(_Warn, subsystem),
_getOnePrinter(_Info, subsystem),
_getOnePrinter(_Debug, subsystem),
_getOnePrinter(_Trace, subsystem)
}
func _getOnePrinter(level int32, 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),
}
}
// SetLogLevel sets the log level via a string, which can be truncated down to
// one character, similar to nmcli's argument processor, as the first letter is
// unique. This could be used with a linter to make larger command sets.
func SetLogLevel(l string) {
if l == "" {
l = "info"
}
// fmt.Fprintln(os.Stderr, "setting log level", l)
lvl := logLevels.Info
for i := range LevelSpecs {
if LevelSpecs[i].Name[:1] == l[:1] {
lvl = LevelSpecs[i].ID
}
}
currentLevel.Store(lvl)
}
// SetLogWriter atomically changes the log io.Writer interface
func SetLogWriter(wr io.Writer) {
// w := unsafe.Pointer(writer)
// c := unsafe.Pointer(wr)
// atomic.SwapPointer(&w, c)
writer = wr
}
func SetLogWriteToFile(path, appName string) (e error) {
// copy existing log file to dated log file as we will truncate it per
// session
path = filepath.Join(path, "log"+appName)
if _, e = os.Stat(path); e == nil {
var b []byte
b, e = ioutil.ReadFile(path)
if e == nil {
ioutil.WriteFile(path+fmt.Sprint(time.Now().Unix()), b, 0600)
}
}
var fileWriter *os.File
if fileWriter, e = os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC,
0600); e != nil {
fmt.Fprintln(os.Stderr, "unable to write log to", path, "error:", e)
return
}
mw := io.MultiWriter(os.Stderr, fileWriter)
fileWriter.Write([]byte("logging to file '" + path + "'\n"))
mw.Write([]byte("logging to file '" + path + "'\n"))
SetLogWriter(mw)
return
}
// 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)
// fmt.Fprintln(
// os.Stderr,
// spew.Sdump(allSubsystems),
// spew.Sdump(highlighted),
// spew.Sdump(logFilter),
// )
}
// AddLoggerSubsystem adds a subsystem to the list of known subsystems and returns the
// string so it is nice and neat in the package logg.go file
func AddLoggerSubsystem(pathBase string) (subsystem string) {
// var split []string
var ok bool
var file string
_, file, _, ok = runtime.Caller(1)
if ok {
r := strings.Split(file, pathBase)
// fmt.Fprintln(os.Stderr, version.PathBase, r)
fromRoot := filepath.Base(file)
if len(r) > 1 {
fromRoot = r[1]
}
split := strings.Split(fromRoot, "/")
// fmt.Fprintln(os.Stderr, version.PathBase, "file", file, r, fromRoot, split)
subsystem = strings.Join(split[:len(split)-1], "/")
// fmt.Fprintln(os.Stderr, "adding subsystem", subsystem)
allSubsystems = append(allSubsystems, subsystem)
}
return
}
// StoreHighlightedSubsystems sets the list of subsystems to highlight
func StoreHighlightedSubsystems(highlights []string) (found bool) {
highlightMx.Lock()
highlighted = make(map[string]struct{}, len(highlights))
for i := range highlights {
highlighted[highlights[i]] = struct{}{}
}
highlightMx.Unlock()
return
}
// LoadHighlightedSubsystems returns a copy of the map of highlighted subsystems
func LoadHighlightedSubsystems() (o []string) {
highlightMx.Lock()
o = make([]string, len(logFilter))
var counter int
for i := range logFilter {
o[counter] = i
counter++
}
highlightMx.Unlock()
sort.Strings(o)
return
}
// StoreSubsystemFilter sets the list of subsystems to filter
func StoreSubsystemFilter(filter []string) {
_logFilterMx.Lock()
logFilter = make(map[string]struct{}, len(filter))
for i := range filter {
logFilter[filter[i]] = struct{}{}
}
_logFilterMx.Unlock()
}
// LoadSubsystemFilter returns a copy of the map of filtered subsystems
func LoadSubsystemFilter() (o []string) {
_logFilterMx.Lock()
o = make([]string, len(logFilter))
var counter int
for i := range logFilter {
o[counter] = i
counter++
}
_logFilterMx.Unlock()
sort.Strings(o)
return
}
// _isHighlighted returns true if the subsystem is in the list to have attention
// getters added to them
func _isHighlighted(subsystem string) (found bool) {
highlightMx.Lock()
_, found = highlighted[subsystem]
highlightMx.Unlock()
return
}
// AddHighlightedSubsystem adds a new subsystem Name to the highlighted list
func AddHighlightedSubsystem(hl string) struct{} {
highlightMx.Lock()
highlighted[hl] = struct{}{}
highlightMx.Unlock()
return struct{}{}
}
// _isSubsystemFiltered returns true if the subsystem should not pr logs
func _isSubsystemFiltered(subsystem string) (found bool) {
_logFilterMx.Lock()
_, found = logFilter[subsystem]
_logFilterMx.Unlock()
return
}
// AddFilteredSubsystem adds a new subsystem Name to the highlighted list
func AddFilteredSubsystem(hl string) struct{} {
_logFilterMx.Lock()
logFilter[hl] = struct{}{}
_logFilterMx.Unlock()
return struct{}{}
}
func getTimeText(level int32) string {
// since := time.Now().Sub(logger_started).Round(time.Millisecond).String()
// diff := 12 - len(since)
// if diff > 0 {
// since = strings.Repeat(" ", diff) + since + " "
// }
return color.Bit24(99, 99, 99, false).Sprint(time.Now().
Format(time.StampMilli))
}
func _ln(level int32, subsystem string) func(a ...interface{}) {
return func(a ...interface{}) {
if level <= currentLevel.Load() && !_isSubsystemFiltered(subsystem) {
printer := fmt.Sprintf
if _isHighlighted(subsystem) {
printer = color.Bold.Sprintf
}
fmt.Fprintf(
writer,
printer(
"%-58v%s%s%-6v %s\n",
getLoc(2, level, subsystem),
getTimeText(level),
color.Bit24(20, 20, 20, true).
Sprint(AppColorizer(" "+App)),
LevelSpecs[level].Colorizer(
color.Bit24(20, 20, 20, true).
Sprint(" "+LevelSpecs[level].Name+" "),
),
AppColorizer(joinStrings(" ", a...)),
),
)
}
}
}
func _f(level int32, subsystem string) func(format string, a ...interface{}) {
return func(format string, a ...interface{}) {
if level <= currentLevel.Load() && !_isSubsystemFiltered(subsystem) {
printer := fmt.Sprintf
if _isHighlighted(subsystem) {
printer = color.Bold.Sprintf
}
fmt.Fprintf(
writer,
printer(
"%-58v%s%s%-6v %s\n",
getLoc(2, level, subsystem),
getTimeText(level),
color.Bit24(20, 20, 20, true).
Sprint(AppColorizer(" "+App)),
LevelSpecs[level].Colorizer(
color.Bit24(20, 20, 20, true).
Sprint(" "+LevelSpecs[level].Name+" "),
),
AppColorizer(fmt.Sprintf(format, a...)),
),
)
}
}
}
func _s(level int32, subsystem string) func(a ...interface{}) {
return func(a ...interface{}) {
if level <= currentLevel.Load() && !_isSubsystemFiltered(subsystem) {
printer := fmt.Sprintf
if _isHighlighted(subsystem) {
printer = color.Bold.Sprintf
}
fmt.Fprintf(
writer,
printer(
"%-58v%s%s%s%s%s\n",
getLoc(2, level, subsystem),
getTimeText(level),
color.Bit24(20, 20, 20, true).
Sprint(AppColorizer(" "+App)),
LevelSpecs[level].Colorizer(
color.Bit24(20, 20, 20, true).
Sprint(" "+LevelSpecs[level].Name+" "),
),
AppColorizer(
" spew:",
),
fmt.Sprint(
color.Bit24(20, 20, 20, true).Sprint("\n\n"+spew.Sdump(a)),
"\n",
),
),
)
}
}
}
func _c(level int32, subsystem string) func(closure func() string) {
return func(closure func() string) {
if level <= currentLevel.Load() && !_isSubsystemFiltered(subsystem) {
printer := fmt.Sprintf
if _isHighlighted(subsystem) {
printer = color.Bold.Sprintf
}
fmt.Fprintf(
writer,
printer(
"%-58v%s%s%-6v %s\n",
getLoc(2, level, subsystem),
getTimeText(level),
color.Bit24(20, 20, 20, true).
Sprint(AppColorizer(" "+App)),
LevelSpecs[level].Colorizer(
color.Bit24(20, 20, 20, true).
Sprint(" "+LevelSpecs[level].Name+" "),
),
AppColorizer(closure()),
),
)
}
}
}
func _chk(level int32, subsystem string) func(e error) bool {
return func(e error) bool {
if level <= currentLevel.Load() && !_isSubsystemFiltered(subsystem) {
if e != nil {
printer := fmt.Sprintf
if _isHighlighted(subsystem) {
printer = color.Bold.Sprintf
}
fmt.Fprintf(
writer,
printer(
"%-58v%s%s%-6v %s\n",
getLoc(2, level, subsystem),
getTimeText(level),
color.Bit24(20, 20, 20, true).
Sprint(AppColorizer(" "+App)),
LevelSpecs[level].Colorizer(
color.Bit24(20, 20, 20, true).
Sprint(" "+LevelSpecs[level].Name+" "),
),
LevelSpecs[level].Colorizer(joinStrings(" ", e.Error())),
),
)
return true
}
}
return false
}
}
// joinStrings constructs a string from an slice of interface same as Println but
// without the terminal newline
func joinStrings(sep string, a ...interface{}) (o string) {
for i := range a {
o += fmt.Sprint(a[i])
if i < len(a)-1 {
o += sep
}
}
return
}
// 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]+))
//
// 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.
//
// Change the path to use with another repository's logging output (
// someone with more time on their hands could probably come up with
// something, but frankly the custom links feature of Tilix has the absolute
// worst UX I have encountered since the 90s...
// Maybe in the future this library will be expanded with a tool that more
// intelligently sets the path, ie from CWD or other cleverness.
//
// This matches full paths anywhere on the commandline delimited by spaces:
//
// ([/](([\/a-zA-Z@0-9-_.]+/)+([a-zA-Z@0-9-_.]+)):([0-9]+))
//
// goland --line $5 /$2
//
// Adapt the invocation to open your preferred editor if it has the capability,
// the above is for Jetbrains Goland
func getLoc(skip int, level int32, 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(
color.White.Sprint(subsystem),
color.Gray.Sprint(
split[1], ":", line,
),
)
}
return
}
// DirectionString is a helper function that returns a string that represents the direction of a connection (inbound or outbound).
func DirectionString(inbound bool) string {
if inbound {
return "inbound"
}
return "outbound"
}
func PickNoun(n int, singular, plural string) string {
if n == 1 {
return singular
}
return plural
}
func FileExists(filePath string) bool {
_, e := os.Stat(filePath)
return e == nil
}
func Caller(comment string, skip int) string {
_, file, line, _ := runtime.Caller(skip + 1)
o := fmt.Sprintf("%s: %s:%d", comment, file, line)
// L.Debug(o)
return o
}