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 }