forked from toolshed/abra
We were running behind and there were quite some deprecations to update. This was mostly in the upstream copy/pasta package but seems quite minimal.
413 lines
9.6 KiB
Go
413 lines
9.6 KiB
Go
package log
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"runtime"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/charmbracelet/lipgloss"
|
|
"github.com/muesli/termenv"
|
|
)
|
|
|
|
// ErrMissingValue is returned when a key is missing a value.
|
|
var ErrMissingValue = fmt.Errorf("missing value")
|
|
|
|
// LoggerOption is an option for a logger.
|
|
type LoggerOption = func(*Logger)
|
|
|
|
// Logger is a Logger that implements Logger.
|
|
type Logger struct {
|
|
w io.Writer
|
|
b bytes.Buffer
|
|
mu *sync.RWMutex
|
|
re *lipgloss.Renderer
|
|
|
|
isDiscard uint32
|
|
|
|
level int64
|
|
prefix string
|
|
timeFunc TimeFunction
|
|
timeFormat string
|
|
callerOffset int
|
|
callerFormatter CallerFormatter
|
|
formatter Formatter
|
|
|
|
reportCaller bool
|
|
reportTimestamp bool
|
|
|
|
fields []interface{}
|
|
|
|
helpers *sync.Map
|
|
styles *Styles
|
|
}
|
|
|
|
// Logf logs a message with formatting.
|
|
func (l *Logger) Logf(level Level, format string, args ...interface{}) {
|
|
l.Log(level, fmt.Sprintf(format, args...))
|
|
}
|
|
|
|
// Log logs the given message with the given keyvals for the given level.
|
|
func (l *Logger) Log(level Level, msg interface{}, keyvals ...interface{}) {
|
|
if atomic.LoadUint32(&l.isDiscard) != 0 {
|
|
return
|
|
}
|
|
|
|
// check if the level is allowed
|
|
if atomic.LoadInt64(&l.level) > int64(level) {
|
|
return
|
|
}
|
|
|
|
var frame runtime.Frame
|
|
if l.reportCaller {
|
|
// Skip log.log, the caller, and any offset added.
|
|
frames := l.frames(l.callerOffset + 2)
|
|
for {
|
|
f, more := frames.Next()
|
|
_, helper := l.helpers.Load(f.Function)
|
|
if !helper || !more {
|
|
// Found a frame that wasn't a helper function.
|
|
// Or we ran out of frames to check.
|
|
frame = f
|
|
break
|
|
}
|
|
}
|
|
}
|
|
l.handle(level, l.timeFunc(time.Now()), []runtime.Frame{frame}, msg, keyvals...)
|
|
}
|
|
|
|
func (l *Logger) handle(level Level, ts time.Time, frames []runtime.Frame, msg interface{}, keyvals ...interface{}) {
|
|
var kvs []interface{}
|
|
if l.reportTimestamp && !ts.IsZero() {
|
|
kvs = append(kvs, TimestampKey, ts)
|
|
}
|
|
|
|
_, ok := l.styles.Levels[level]
|
|
if ok {
|
|
kvs = append(kvs, LevelKey, level)
|
|
}
|
|
|
|
if l.reportCaller && len(frames) > 0 && frames[0].PC != 0 {
|
|
file, line, fn := l.location(frames)
|
|
if file != "" {
|
|
caller := l.callerFormatter(file, line, fn)
|
|
kvs = append(kvs, CallerKey, caller)
|
|
}
|
|
}
|
|
|
|
if l.prefix != "" {
|
|
kvs = append(kvs, PrefixKey, l.prefix)
|
|
}
|
|
|
|
if msg != nil {
|
|
if m := fmt.Sprint(msg); m != "" {
|
|
kvs = append(kvs, MessageKey, m)
|
|
}
|
|
}
|
|
|
|
// append logger fields
|
|
kvs = append(kvs, l.fields...)
|
|
if len(l.fields)%2 != 0 {
|
|
kvs = append(kvs, ErrMissingValue)
|
|
}
|
|
|
|
// append the rest
|
|
kvs = append(kvs, keyvals...)
|
|
if len(keyvals)%2 != 0 {
|
|
kvs = append(kvs, ErrMissingValue)
|
|
}
|
|
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
switch l.formatter {
|
|
case LogfmtFormatter:
|
|
l.logfmtFormatter(kvs...)
|
|
case JSONFormatter:
|
|
l.jsonFormatter(kvs...)
|
|
case TextFormatter:
|
|
fallthrough
|
|
default:
|
|
l.textFormatter(kvs...)
|
|
}
|
|
|
|
// WriteTo will reset the buffer
|
|
l.b.WriteTo(l.w) //nolint: errcheck
|
|
}
|
|
|
|
// Helper marks the calling function as a helper
|
|
// and skips it for source location information.
|
|
// It's the equivalent of testing.TB.Helper().
|
|
func (l *Logger) Helper() {
|
|
l.helper(1)
|
|
}
|
|
|
|
func (l *Logger) helper(skip int) {
|
|
var pcs [1]uintptr
|
|
// Skip runtime.Callers, and l.helper
|
|
n := runtime.Callers(skip+2, pcs[:])
|
|
frames := runtime.CallersFrames(pcs[:n])
|
|
frame, _ := frames.Next()
|
|
l.helpers.LoadOrStore(frame.Function, struct{}{})
|
|
}
|
|
|
|
// frames returns the runtime.Frames for the caller.
|
|
func (l *Logger) frames(skip int) *runtime.Frames {
|
|
// Copied from testing.T
|
|
const maxStackLen = 50
|
|
var pc [maxStackLen]uintptr
|
|
|
|
// Skip runtime.Callers, and l.frame
|
|
n := runtime.Callers(skip+2, pc[:])
|
|
frames := runtime.CallersFrames(pc[:n])
|
|
return frames
|
|
}
|
|
|
|
func (l *Logger) location(frames []runtime.Frame) (file string, line int, fn string) {
|
|
if len(frames) == 0 {
|
|
return "", 0, ""
|
|
}
|
|
f := frames[0]
|
|
return f.File, f.Line, f.Function
|
|
}
|
|
|
|
// Cleanup a path by returning the last n segments of the path only.
|
|
func trimCallerPath(path string, n int) string {
|
|
// lovely borrowed from zap
|
|
// nb. To make sure we trim the path correctly on Windows too, we
|
|
// counter-intuitively need to use '/' and *not* os.PathSeparator here,
|
|
// because the path given originates from Go stdlib, specifically
|
|
// runtime.Caller() which (as of Mar/17) returns forward slashes even on
|
|
// Windows.
|
|
//
|
|
// See https://github.com/golang/go/issues/3335
|
|
// and https://github.com/golang/go/issues/18151
|
|
//
|
|
// for discussion on the issue on Go side.
|
|
|
|
// Return the full path if n is 0.
|
|
if n <= 0 {
|
|
return path
|
|
}
|
|
|
|
// Find the last separator.
|
|
idx := strings.LastIndexByte(path, '/')
|
|
if idx == -1 {
|
|
return path
|
|
}
|
|
|
|
for i := 0; i < n-1; i++ {
|
|
// Find the penultimate separator.
|
|
idx = strings.LastIndexByte(path[:idx], '/')
|
|
if idx == -1 {
|
|
return path
|
|
}
|
|
}
|
|
|
|
return path[idx+1:]
|
|
}
|
|
|
|
// SetReportTimestamp sets whether the timestamp should be reported.
|
|
func (l *Logger) SetReportTimestamp(report bool) {
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
l.reportTimestamp = report
|
|
}
|
|
|
|
// SetReportCaller sets whether the caller location should be reported.
|
|
func (l *Logger) SetReportCaller(report bool) {
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
l.reportCaller = report
|
|
}
|
|
|
|
// GetLevel returns the current level.
|
|
func (l *Logger) GetLevel() Level {
|
|
l.mu.RLock()
|
|
defer l.mu.RUnlock()
|
|
return Level(l.level)
|
|
}
|
|
|
|
// SetLevel sets the current level.
|
|
func (l *Logger) SetLevel(level Level) {
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
atomic.StoreInt64(&l.level, int64(level))
|
|
}
|
|
|
|
// GetPrefix returns the current prefix.
|
|
func (l *Logger) GetPrefix() string {
|
|
l.mu.RLock()
|
|
defer l.mu.RUnlock()
|
|
return l.prefix
|
|
}
|
|
|
|
// SetPrefix sets the current prefix.
|
|
func (l *Logger) SetPrefix(prefix string) {
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
l.prefix = prefix
|
|
}
|
|
|
|
// SetTimeFormat sets the time format.
|
|
func (l *Logger) SetTimeFormat(format string) {
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
l.timeFormat = format
|
|
}
|
|
|
|
// SetTimeFunction sets the time function.
|
|
func (l *Logger) SetTimeFunction(f TimeFunction) {
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
l.timeFunc = f
|
|
}
|
|
|
|
// SetOutput sets the output destination.
|
|
func (l *Logger) SetOutput(w io.Writer) {
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
if w == nil {
|
|
w = os.Stderr
|
|
}
|
|
l.w = w
|
|
var isDiscard uint32
|
|
if w == io.Discard {
|
|
isDiscard = 1
|
|
}
|
|
atomic.StoreUint32(&l.isDiscard, isDiscard)
|
|
// Reuse cached renderers
|
|
if v, ok := registry.Load(w); ok {
|
|
l.re = v.(*lipgloss.Renderer)
|
|
} else {
|
|
l.re = lipgloss.NewRenderer(w, termenv.WithColorCache(true))
|
|
registry.Store(w, l.re)
|
|
}
|
|
}
|
|
|
|
// SetFormatter sets the formatter.
|
|
func (l *Logger) SetFormatter(f Formatter) {
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
l.formatter = f
|
|
}
|
|
|
|
// SetCallerFormatter sets the caller formatter.
|
|
func (l *Logger) SetCallerFormatter(f CallerFormatter) {
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
l.callerFormatter = f
|
|
}
|
|
|
|
// SetCallerOffset sets the caller offset.
|
|
func (l *Logger) SetCallerOffset(offset int) {
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
l.callerOffset = offset
|
|
}
|
|
|
|
// SetColorProfile force sets the underlying Lip Gloss renderer color profile
|
|
// for the TextFormatter.
|
|
func (l *Logger) SetColorProfile(profile termenv.Profile) {
|
|
l.re.SetColorProfile(profile)
|
|
}
|
|
|
|
// SetStyles sets the logger styles for the TextFormatter.
|
|
func (l *Logger) SetStyles(s *Styles) {
|
|
if s == nil {
|
|
s = DefaultStyles()
|
|
}
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
l.styles = s
|
|
}
|
|
|
|
// With returns a new logger with the given keyvals added.
|
|
func (l *Logger) With(keyvals ...interface{}) *Logger {
|
|
var st Styles
|
|
l.mu.Lock()
|
|
sl := *l
|
|
st = *l.styles
|
|
l.mu.Unlock()
|
|
sl.b = bytes.Buffer{}
|
|
sl.mu = &sync.RWMutex{}
|
|
sl.helpers = &sync.Map{}
|
|
sl.fields = append(make([]interface{}, 0, len(l.fields)+len(keyvals)), l.fields...)
|
|
sl.fields = append(sl.fields, keyvals...)
|
|
sl.styles = &st
|
|
return &sl
|
|
}
|
|
|
|
// WithPrefix returns a new logger with the given prefix.
|
|
func (l *Logger) WithPrefix(prefix string) *Logger {
|
|
sl := l.With()
|
|
sl.SetPrefix(prefix)
|
|
return sl
|
|
}
|
|
|
|
// Debug prints a debug message.
|
|
func (l *Logger) Debug(msg interface{}, keyvals ...interface{}) {
|
|
l.Log(DebugLevel, msg, keyvals...)
|
|
}
|
|
|
|
// Info prints an info message.
|
|
func (l *Logger) Info(msg interface{}, keyvals ...interface{}) {
|
|
l.Log(InfoLevel, msg, keyvals...)
|
|
}
|
|
|
|
// Warn prints a warning message.
|
|
func (l *Logger) Warn(msg interface{}, keyvals ...interface{}) {
|
|
l.Log(WarnLevel, msg, keyvals...)
|
|
}
|
|
|
|
// Error prints an error message.
|
|
func (l *Logger) Error(msg interface{}, keyvals ...interface{}) {
|
|
l.Log(ErrorLevel, msg, keyvals...)
|
|
}
|
|
|
|
// Fatal prints a fatal message and exits.
|
|
func (l *Logger) Fatal(msg interface{}, keyvals ...interface{}) {
|
|
l.Log(FatalLevel, msg, keyvals...)
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Print prints a message with no level.
|
|
func (l *Logger) Print(msg interface{}, keyvals ...interface{}) {
|
|
l.Log(noLevel, msg, keyvals...)
|
|
}
|
|
|
|
// Debugf prints a debug message with formatting.
|
|
func (l *Logger) Debugf(format string, args ...interface{}) {
|
|
l.Log(DebugLevel, fmt.Sprintf(format, args...))
|
|
}
|
|
|
|
// Infof prints an info message with formatting.
|
|
func (l *Logger) Infof(format string, args ...interface{}) {
|
|
l.Log(InfoLevel, fmt.Sprintf(format, args...))
|
|
}
|
|
|
|
// Warnf prints a warning message with formatting.
|
|
func (l *Logger) Warnf(format string, args ...interface{}) {
|
|
l.Log(WarnLevel, fmt.Sprintf(format, args...))
|
|
}
|
|
|
|
// Errorf prints an error message with formatting.
|
|
func (l *Logger) Errorf(format string, args ...interface{}) {
|
|
l.Log(ErrorLevel, fmt.Sprintf(format, args...))
|
|
}
|
|
|
|
// Fatalf prints a fatal message with formatting and exits.
|
|
func (l *Logger) Fatalf(format string, args ...interface{}) {
|
|
l.Log(FatalLevel, fmt.Sprintf(format, args...))
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Printf prints a message with no level and formatting.
|
|
func (l *Logger) Printf(format string, args ...interface{}) {
|
|
l.Log(noLevel, fmt.Sprintf(format, args...))
|
|
}
|