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