logg.go raw

   1  package log
   2  
   3  import (
   4  	"fmt"
   5  	"io"
   6  	"io/ioutil"
   7  	"os"
   8  	"path/filepath"
   9  	"runtime"
  10  	"sort"
  11  	"strings"
  12  	"sync"
  13  	"time"
  14  
  15  	"github.com/davecgh/go-spew/spew"
  16  	"github.com/gookit/color"
  17  	uberatomic "go.uber.org/atomic"
  18  )
  19  
  20  const (
  21  	_Off = iota
  22  	_Fatal
  23  	_Error
  24  	_Chek
  25  	_Warn
  26  	_Info
  27  	_Debug
  28  	_Trace
  29  )
  30  
  31  type (
  32  	// LevelPrinter defines a set of terminal printing primitives that output with
  33  	// extra data, time, log logLevelList, and code location
  34  	LevelPrinter struct {
  35  		// Ln prints lists of interfaces with spaces in between
  36  		Ln func(a ...interface{})
  37  		// F prints like fmt.Println surrounded by log details
  38  		F func(format string, a ...interface{})
  39  		// S prints a spew.Sdump for an interface slice
  40  		S func(a ...interface{})
  41  		// C accepts a function so that the extra computation can be avoided if it is
  42  		// not being viewed
  43  		C func(closure func() string)
  44  		// Chk is a shortcut for printing if there is an error, or returning true
  45  		Chk func(e error) bool
  46  	}
  47  	logLevelList struct {
  48  		Off, Fatal, Error, Check, Warn, Info, Debug, Trace int32
  49  	}
  50  	LevelSpec struct {
  51  		ID        int32
  52  		Name      string
  53  		Colorizer func(format string, a ...interface{}) string
  54  	}
  55  
  56  	// Entry is a log entry to be printed as json to the log file
  57  	Entry struct {
  58  		Time         time.Time
  59  		Level        string
  60  		Package      string
  61  		CodeLocation string
  62  		Text         string
  63  	}
  64  )
  65  
  66  var (
  67  	logger_started = time.Now()
  68  	App            = "   pod"
  69  	AppColorizer   = color.White.Sprint
  70  	// sep is just a convenient shortcut for this very longwinded expression
  71  	sep          = string(os.PathSeparator)
  72  	currentLevel = uberatomic.NewInt32(logLevels.Info)
  73  	// writer can be swapped out for any io.*writer* that you want to use instead of
  74  	// stdout.
  75  	writer io.Writer = os.Stderr
  76  	// allSubsystems stores all of the package subsystem names found in the current
  77  	// application
  78  	allSubsystems []string
  79  	// highlighted is a text that helps visually distinguish a log entry by category
  80  	highlighted = make(map[string]struct{})
  81  	// logFilter specifies a set of packages that will not pr logs
  82  	logFilter = make(map[string]struct{})
  83  	// mutexes to prevent concurrent map accesses
  84  	highlightMx, _logFilterMx sync.Mutex
  85  	// logLevels is a shorthand access that minimises possible Name collisions in the
  86  	// dot import
  87  	logLevels = logLevelList{
  88  		Off:   _Off,
  89  		Fatal: _Fatal,
  90  		Error: _Error,
  91  		Check: _Chek,
  92  		Warn:  _Warn,
  93  		Info:  _Info,
  94  		Debug: _Debug,
  95  		Trace: _Trace,
  96  	}
  97  	// LevelSpecs specifies the id, string name and color-printing function
  98  	LevelSpecs = []LevelSpec{
  99  		{logLevels.Off, "off  ", color.Bit24(0, 0, 0, false).Sprintf},
 100  		{logLevels.Fatal, "fatal", color.Bit24(128, 0, 0, false).Sprintf},
 101  		{logLevels.Error, "error", color.Bit24(255, 0, 0, false).Sprintf},
 102  		{logLevels.Check, "check", color.Bit24(255, 255, 0, false).Sprintf},
 103  		{logLevels.Warn, "warn ", color.Bit24(0, 255, 0, false).Sprintf},
 104  		{logLevels.Info, "info ", color.Bit24(255, 255, 0, false).Sprintf},
 105  		{logLevels.Debug, "debug", color.Bit24(0, 128, 255, false).Sprintf},
 106  		{logLevels.Trace, "trace", color.Bit24(128, 0, 255, false).Sprintf},
 107  	}
 108  	Levels = []string{
 109  		Off,
 110  		Fatal,
 111  		Error,
 112  		Check,
 113  		Warn,
 114  		Info,
 115  		Debug,
 116  		Trace,
 117  	}
 118  	LogChanDisabled = uberatomic.NewBool(true)
 119  	LogChan         chan Entry
 120  )
 121  
 122  const (
 123  	Off   = "off"
 124  	Fatal = "fatal"
 125  	Error = "error"
 126  	Warn  = "warn"
 127  	Info  = "info"
 128  	Check = "check"
 129  	Debug = "debug"
 130  	Trace = "trace"
 131  )
 132  
 133  // AddLogChan adds a channel that log entries are sent to
 134  func AddLogChan() (ch chan Entry) {
 135  	LogChanDisabled.Store(false)
 136  	if LogChan != nil {
 137  		panic("warning warning")
 138  	}
 139  	// L.Writer.Write.Store( false
 140  	LogChan = make(chan Entry)
 141  	return LogChan
 142  }
 143  
 144  // GetLogPrinterSet returns a set of LevelPrinter with their subsystem preloaded
 145  func GetLogPrinterSet(subsystem string) (Fatal, Error, Warn, Info, Debug, Trace LevelPrinter) {
 146  	return _getOnePrinter(_Fatal, subsystem),
 147  		_getOnePrinter(_Error, subsystem),
 148  		_getOnePrinter(_Warn, subsystem),
 149  		_getOnePrinter(_Info, subsystem),
 150  		_getOnePrinter(_Debug, subsystem),
 151  		_getOnePrinter(_Trace, subsystem)
 152  }
 153  
 154  func _getOnePrinter(level int32, subsystem string) LevelPrinter {
 155  	return LevelPrinter{
 156  		Ln:  _ln(level, subsystem),
 157  		F:   _f(level, subsystem),
 158  		S:   _s(level, subsystem),
 159  		C:   _c(level, subsystem),
 160  		Chk: _chk(level, subsystem),
 161  	}
 162  }
 163  
 164  // SetLogLevel sets the log level via a string, which can be truncated down to
 165  // one character, similar to nmcli's argument processor, as the first letter is
 166  // unique. This could be used with a linter to make larger command sets.
 167  func SetLogLevel(l string) {
 168  	if l == "" {
 169  		l = "info"
 170  	}
 171  	// fmt.Fprintln(os.Stderr, "setting log level", l)
 172  	lvl := logLevels.Info
 173  	for i := range LevelSpecs {
 174  		if LevelSpecs[i].Name[:1] == l[:1] {
 175  			lvl = LevelSpecs[i].ID
 176  		}
 177  	}
 178  	currentLevel.Store(lvl)
 179  }
 180  
 181  // SetLogWriter atomically changes the log io.Writer interface
 182  func SetLogWriter(wr io.Writer) {
 183  	// w := unsafe.Pointer(writer)
 184  	// c := unsafe.Pointer(wr)
 185  	// atomic.SwapPointer(&w, c)
 186  	writer = wr
 187  }
 188  
 189  func SetLogWriteToFile(path, appName string) (e error) {
 190  	// copy existing log file to dated log file as we will truncate it per
 191  	// session
 192  	path = filepath.Join(path, "log"+appName)
 193  	if _, e = os.Stat(path); e == nil {
 194  		var b []byte
 195  		b, e = ioutil.ReadFile(path)
 196  		if e == nil {
 197  			ioutil.WriteFile(path+fmt.Sprint(time.Now().Unix()), b, 0600)
 198  		}
 199  	}
 200  	var fileWriter *os.File
 201  	if fileWriter, e = os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC,
 202  		0600); e != nil {
 203  		fmt.Fprintln(os.Stderr, "unable to write log to", path, "error:", e)
 204  		return
 205  	}
 206  	mw := io.MultiWriter(os.Stderr, fileWriter)
 207  	fileWriter.Write([]byte("logging to file '" + path + "'\n"))
 208  	mw.Write([]byte("logging to file '" + path + "'\n"))
 209  	SetLogWriter(mw)
 210  	return
 211  }
 212  
 213  // SortSubsystemsList sorts the list of subsystems, to keep the data read-only,
 214  // call this function right at the top of the main, which runs after
 215  // declarations and main/init. Really this is just here to alert the reader.
 216  func SortSubsystemsList() {
 217  	sort.Strings(allSubsystems)
 218  	// fmt.Fprintln(
 219  	// 	os.Stderr,
 220  	// 	spew.Sdump(allSubsystems),
 221  	// 	spew.Sdump(highlighted),
 222  	// 	spew.Sdump(logFilter),
 223  	// )
 224  }
 225  
 226  // AddLoggerSubsystem adds a subsystem to the list of known subsystems and returns the
 227  // string so it is nice and neat in the package logg.go file
 228  func AddLoggerSubsystem(pathBase string) (subsystem string) {
 229  	// var split []string
 230  	var ok bool
 231  	var file string
 232  	_, file, _, ok = runtime.Caller(1)
 233  	if ok {
 234  		r := strings.Split(file, pathBase)
 235  		// fmt.Fprintln(os.Stderr, version.PathBase, r)
 236  		fromRoot := filepath.Base(file)
 237  		if len(r) > 1 {
 238  			fromRoot = r[1]
 239  		}
 240  		split := strings.Split(fromRoot, "/")
 241  		// fmt.Fprintln(os.Stderr, version.PathBase, "file", file, r, fromRoot, split)
 242  		subsystem = strings.Join(split[:len(split)-1], "/")
 243  		// fmt.Fprintln(os.Stderr, "adding subsystem", subsystem)
 244  		allSubsystems = append(allSubsystems, subsystem)
 245  	}
 246  	return
 247  }
 248  
 249  // StoreHighlightedSubsystems sets the list of subsystems to highlight
 250  func StoreHighlightedSubsystems(highlights []string) (found bool) {
 251  	highlightMx.Lock()
 252  	highlighted = make(map[string]struct{}, len(highlights))
 253  	for i := range highlights {
 254  		highlighted[highlights[i]] = struct{}{}
 255  	}
 256  	highlightMx.Unlock()
 257  	return
 258  }
 259  
 260  // LoadHighlightedSubsystems returns a copy of the map of highlighted subsystems
 261  func LoadHighlightedSubsystems() (o []string) {
 262  	highlightMx.Lock()
 263  	o = make([]string, len(logFilter))
 264  	var counter int
 265  	for i := range logFilter {
 266  		o[counter] = i
 267  		counter++
 268  	}
 269  	highlightMx.Unlock()
 270  	sort.Strings(o)
 271  	return
 272  }
 273  
 274  // StoreSubsystemFilter sets the list of subsystems to filter
 275  func StoreSubsystemFilter(filter []string) {
 276  	_logFilterMx.Lock()
 277  	logFilter = make(map[string]struct{}, len(filter))
 278  	for i := range filter {
 279  		logFilter[filter[i]] = struct{}{}
 280  	}
 281  	_logFilterMx.Unlock()
 282  }
 283  
 284  // LoadSubsystemFilter returns a copy of the map of filtered subsystems
 285  func LoadSubsystemFilter() (o []string) {
 286  	_logFilterMx.Lock()
 287  	o = make([]string, len(logFilter))
 288  	var counter int
 289  	for i := range logFilter {
 290  		o[counter] = i
 291  		counter++
 292  	}
 293  	_logFilterMx.Unlock()
 294  	sort.Strings(o)
 295  	return
 296  }
 297  
 298  // _isHighlighted returns true if the subsystem is in the list to have attention
 299  // getters added to them
 300  func _isHighlighted(subsystem string) (found bool) {
 301  	highlightMx.Lock()
 302  	_, found = highlighted[subsystem]
 303  	highlightMx.Unlock()
 304  	return
 305  }
 306  
 307  // AddHighlightedSubsystem adds a new subsystem Name to the highlighted list
 308  func AddHighlightedSubsystem(hl string) struct{} {
 309  	highlightMx.Lock()
 310  	highlighted[hl] = struct{}{}
 311  	highlightMx.Unlock()
 312  	return struct{}{}
 313  }
 314  
 315  // _isSubsystemFiltered returns true if the subsystem should not pr logs
 316  func _isSubsystemFiltered(subsystem string) (found bool) {
 317  	_logFilterMx.Lock()
 318  	_, found = logFilter[subsystem]
 319  	_logFilterMx.Unlock()
 320  	return
 321  }
 322  
 323  // AddFilteredSubsystem adds a new subsystem Name to the highlighted list
 324  func AddFilteredSubsystem(hl string) struct{} {
 325  	_logFilterMx.Lock()
 326  	logFilter[hl] = struct{}{}
 327  	_logFilterMx.Unlock()
 328  	return struct{}{}
 329  }
 330  
 331  func getTimeText(level int32) string {
 332  	// since := time.Now().Sub(logger_started).Round(time.Millisecond).String()
 333  	// diff := 12 - len(since)
 334  	// if diff > 0 {
 335  	// 	since = strings.Repeat(" ", diff) + since + " "
 336  	// }
 337  	return color.Bit24(99, 99, 99, false).Sprint(time.Now().
 338  		Format(time.StampMilli))
 339  }
 340  
 341  func _ln(level int32, subsystem string) func(a ...interface{}) {
 342  	return func(a ...interface{}) {
 343  		if level <= currentLevel.Load() && !_isSubsystemFiltered(subsystem) {
 344  			printer := fmt.Sprintf
 345  			if _isHighlighted(subsystem) {
 346  				printer = color.Bold.Sprintf
 347  			}
 348  			fmt.Fprintf(
 349  				writer,
 350  				printer(
 351  					"%-58v%s%s%-6v %s\n",
 352  					getLoc(2, level, subsystem),
 353  					getTimeText(level),
 354  					color.Bit24(20, 20, 20, true).
 355  						Sprint(AppColorizer(" "+App)),
 356  					LevelSpecs[level].Colorizer(
 357  						color.Bit24(20, 20, 20, true).
 358  							Sprint(" "+LevelSpecs[level].Name+" "),
 359  					),
 360  					AppColorizer(joinStrings(" ", a...)),
 361  				),
 362  			)
 363  		}
 364  	}
 365  }
 366  
 367  func _f(level int32, subsystem string) func(format string, a ...interface{}) {
 368  	return func(format string, a ...interface{}) {
 369  		if level <= currentLevel.Load() && !_isSubsystemFiltered(subsystem) {
 370  			printer := fmt.Sprintf
 371  			if _isHighlighted(subsystem) {
 372  				printer = color.Bold.Sprintf
 373  			}
 374  			fmt.Fprintf(
 375  				writer,
 376  				printer(
 377  					"%-58v%s%s%-6v %s\n",
 378  					getLoc(2, level, subsystem),
 379  					getTimeText(level),
 380  					color.Bit24(20, 20, 20, true).
 381  						Sprint(AppColorizer(" "+App)),
 382  					LevelSpecs[level].Colorizer(
 383  						color.Bit24(20, 20, 20, true).
 384  							Sprint(" "+LevelSpecs[level].Name+" "),
 385  					),
 386  					AppColorizer(fmt.Sprintf(format, a...)),
 387  				),
 388  			)
 389  		}
 390  	}
 391  }
 392  
 393  func _s(level int32, subsystem string) func(a ...interface{}) {
 394  	return func(a ...interface{}) {
 395  		if level <= currentLevel.Load() && !_isSubsystemFiltered(subsystem) {
 396  			printer := fmt.Sprintf
 397  			if _isHighlighted(subsystem) {
 398  				printer = color.Bold.Sprintf
 399  			}
 400  			fmt.Fprintf(
 401  				writer,
 402  				printer(
 403  					"%-58v%s%s%s%s%s\n",
 404  					getLoc(2, level, subsystem),
 405  					getTimeText(level),
 406  					color.Bit24(20, 20, 20, true).
 407  						Sprint(AppColorizer(" "+App)),
 408  					LevelSpecs[level].Colorizer(
 409  						color.Bit24(20, 20, 20, true).
 410  							Sprint(" "+LevelSpecs[level].Name+" "),
 411  					),
 412  					AppColorizer(
 413  						" spew:",
 414  					),
 415  					fmt.Sprint(
 416  						color.Bit24(20, 20, 20, true).Sprint("\n\n"+spew.Sdump(a)),
 417  						"\n",
 418  					),
 419  				),
 420  			)
 421  		}
 422  	}
 423  }
 424  
 425  func _c(level int32, subsystem string) func(closure func() string) {
 426  	return func(closure func() string) {
 427  		if level <= currentLevel.Load() && !_isSubsystemFiltered(subsystem) {
 428  			printer := fmt.Sprintf
 429  			if _isHighlighted(subsystem) {
 430  				printer = color.Bold.Sprintf
 431  			}
 432  			fmt.Fprintf(
 433  				writer,
 434  				printer(
 435  					"%-58v%s%s%-6v %s\n",
 436  					getLoc(2, level, subsystem),
 437  					getTimeText(level),
 438  					color.Bit24(20, 20, 20, true).
 439  						Sprint(AppColorizer(" "+App)),
 440  					LevelSpecs[level].Colorizer(
 441  						color.Bit24(20, 20, 20, true).
 442  							Sprint(" "+LevelSpecs[level].Name+" "),
 443  					),
 444  					AppColorizer(closure()),
 445  				),
 446  			)
 447  		}
 448  	}
 449  }
 450  
 451  func _chk(level int32, subsystem string) func(e error) bool {
 452  	return func(e error) bool {
 453  		if level <= currentLevel.Load() && !_isSubsystemFiltered(subsystem) {
 454  			if e != nil {
 455  				printer := fmt.Sprintf
 456  				if _isHighlighted(subsystem) {
 457  					printer = color.Bold.Sprintf
 458  				}
 459  				fmt.Fprintf(
 460  					writer,
 461  					printer(
 462  						"%-58v%s%s%-6v %s\n",
 463  						getLoc(2, level, subsystem),
 464  						getTimeText(level),
 465  						color.Bit24(20, 20, 20, true).
 466  							Sprint(AppColorizer(" "+App)),
 467  						LevelSpecs[level].Colorizer(
 468  							color.Bit24(20, 20, 20, true).
 469  								Sprint(" "+LevelSpecs[level].Name+" "),
 470  						),
 471  						LevelSpecs[level].Colorizer(joinStrings(" ", e.Error())),
 472  					),
 473  				)
 474  				return true
 475  			}
 476  		}
 477  		return false
 478  	}
 479  }
 480  
 481  // joinStrings constructs a string from an slice of interface same as Println but
 482  // without the terminal newline
 483  func joinStrings(sep string, a ...interface{}) (o string) {
 484  	for i := range a {
 485  		o += fmt.Sprint(a[i])
 486  		if i < len(a)-1 {
 487  			o += sep
 488  		}
 489  	}
 490  	return
 491  }
 492  
 493  // getLoc calls runtime.Caller and formats as expected by source code editors
 494  // for terminal hyperlinks
 495  //
 496  // Regular expressions and the substitution texts to make these clickable in
 497  // Tilix and other RE hyperlink configurable terminal emulators:
 498  //
 499  // This matches the shortened paths generated in this command and printed at
 500  // the very beginning of the line as this logger prints:
 501  //
 502  // ^((([\/a-zA-Z@0-9-_.]+/)+([a-zA-Z@0-9-_.]+)):([0-9]+))
 503  //
 504  // 		goland --line $5 $GOPATH/src/github.com/p9c/matrjoska/$2
 505  //
 506  // I have used a shell variable there but tilix doesn't expand them,
 507  // so put your GOPATH in manually, and obviously change the repo subpath.
 508  //
 509  //
 510  // Change the path to use with another repository's logging output (
 511  // someone with more time on their hands could probably come up with
 512  // something, but frankly the custom links feature of Tilix has the absolute
 513  // worst UX I have encountered since the 90s...
 514  // Maybe in the future this library will be expanded with a tool that more
 515  // intelligently sets the path, ie from CWD or other cleverness.
 516  //
 517  // This matches full paths anywhere on the commandline delimited by spaces:
 518  //
 519  // ([/](([\/a-zA-Z@0-9-_.]+/)+([a-zA-Z@0-9-_.]+)):([0-9]+))
 520  //
 521  // 		goland --line $5 /$2
 522  //
 523  // Adapt the invocation to open your preferred editor if it has the capability,
 524  // the above is for Jetbrains Goland
 525  //
 526  func getLoc(skip int, level int32, subsystem string) (output string) {
 527  	_, file, line, _ := runtime.Caller(skip)
 528  	defer func() {
 529  		if r := recover(); r != nil {
 530  			fmt.Fprintln(os.Stderr, "getloc panic on subsystem", subsystem, file)
 531  		}
 532  	}()
 533  	split := strings.Split(file, subsystem)
 534  	if len(split) < 2 {
 535  		output = fmt.Sprint(
 536  			color.White.Sprint(subsystem),
 537  			color.Gray.Sprint(
 538  				file, ":", line,
 539  			),
 540  		)
 541  	} else {
 542  		output = fmt.Sprint(
 543  			color.White.Sprint(subsystem),
 544  			color.Gray.Sprint(
 545  				split[1], ":", line,
 546  			),
 547  		)
 548  	}
 549  	return
 550  }
 551  
 552  // DirectionString is a helper function that returns a string that represents the direction of a connection (inbound or outbound).
 553  func DirectionString(inbound bool) string {
 554  	if inbound {
 555  		return "inbound"
 556  	}
 557  	return "outbound"
 558  }
 559  
 560  func PickNoun(n int, singular, plural string) string {
 561  	if n == 1 {
 562  		return singular
 563  	}
 564  	return plural
 565  }
 566  
 567  func FileExists(filePath string) bool {
 568  	_, e := os.Stat(filePath)
 569  	return e == nil
 570  }
 571  
 572  func Caller(comment string, skip int) string {
 573  	_, file, line, _ := runtime.Caller(skip + 1)
 574  	o := fmt.Sprintf("%s: %s:%d", comment, file, line)
 575  	// L.Debug(o)
 576  	return o
 577  }
 578