github.com/mendersoftware/go-lib-micro@v0.0.0-20240304135804-e8e39c59b148/log/log.go (about)

     1  // Copyright 2023 Northern.tech AS
     2  //
     3  //    Licensed under the Apache License, Version 2.0 (the "License");
     4  //    you may not use this file except in compliance with the License.
     5  //    You may obtain a copy of the License at
     6  //
     7  //        http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  //    Unless required by applicable law or agreed to in writing, software
    10  //    distributed under the License is distributed on an "AS IS" BASIS,
    11  //    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  //    See the License for the specific language governing permissions and
    13  //    limitations under the License.
    14  
    15  // Package log provides a thin wrapper over logrus, with a definition
    16  // of a global root logger, its setup functions and convenience wrappers.
    17  //
    18  // The wrappers are introduced to reduce verbosity:
    19  // - logrus.Fields becomes log.Ctx
    20  // - logrus.WithFields becomes log.F(), defined on a Logger type
    21  //
    22  // The usage scenario in a multilayer app is as follows:
    23  // - a new Logger is created in the upper layer with an initial context (request id, api method...)
    24  // - it is passed to lower layer which automatically includes the context, and can further enrich it
    25  // - result - logs across layers are tied together with a common context
    26  //
    27  // Note on concurrency:
    28  // - all Loggers in fact point to the single base log, which serializes logging with its mutexes
    29  // - all context is copied - each layer operates on an independent copy
    30  
    31  package log
    32  
    33  import (
    34  	"context"
    35  	"fmt"
    36  	"io"
    37  	"os"
    38  	"path"
    39  	"runtime"
    40  	"strconv"
    41  	"strings"
    42  	"time"
    43  
    44  	"github.com/sirupsen/logrus"
    45  )
    46  
    47  var (
    48  	// log is a global logger instance
    49  	Log = logrus.New()
    50  )
    51  
    52  const (
    53  	envLogFormat        = "LOG_FORMAT"
    54  	envLogLevel         = "LOG_LEVEL"
    55  	envLogDisableCaller = "LOG_DISABLE_CALLER_CONTEXT"
    56  
    57  	logFormatJSON    = "json"
    58  	logFormatJSONAlt = "ndjson"
    59  
    60  	logFieldCaller    = "caller"
    61  	logFieldCallerFmt = "%s@%s:%d"
    62  
    63  	pkgSirupsen = "github.com/sirupsen/logrus"
    64  )
    65  
    66  type loggerContextKeyType int
    67  
    68  const (
    69  	loggerContextKey loggerContextKeyType = 0
    70  )
    71  
    72  // ContextLogger interface for components which support
    73  // logging with context, via setting a logger to an exisiting one,
    74  // thereby inheriting its context.
    75  type ContextLogger interface {
    76  	UseLog(l *Logger)
    77  }
    78  
    79  // init initializes the global logger to sane defaults.
    80  func init() {
    81  	var opts Options
    82  	switch strings.ToLower(os.Getenv(envLogFormat)) {
    83  	case logFormatJSON, logFormatJSONAlt:
    84  		opts.Format = FormatJSON
    85  	default:
    86  		opts.Format = FormatConsole
    87  	}
    88  	opts.Level = Level(logrus.InfoLevel)
    89  	if lvl := os.Getenv(envLogLevel); lvl != "" {
    90  		logLevel, err := logrus.ParseLevel(lvl)
    91  		if err == nil {
    92  			opts.Level = Level(logLevel)
    93  		}
    94  	}
    95  	opts.TimestampFormat = time.RFC3339
    96  	opts.DisableCaller, _ = strconv.ParseBool(os.Getenv(envLogDisableCaller))
    97  	Configure(opts)
    98  
    99  	Log.ExitFunc = func(int) {}
   100  }
   101  
   102  type Level logrus.Level
   103  
   104  const (
   105  	LevelPanic = Level(logrus.PanicLevel)
   106  	LevelFatal = Level(logrus.FatalLevel)
   107  	LevelError = Level(logrus.ErrorLevel)
   108  	LevelWarn  = Level(logrus.WarnLevel)
   109  	LevelInfo  = Level(logrus.InfoLevel)
   110  	LevelDebug = Level(logrus.DebugLevel)
   111  	LevelTrace = Level(logrus.TraceLevel)
   112  )
   113  
   114  type Format int
   115  
   116  const (
   117  	FormatConsole Format = iota
   118  	FormatJSON
   119  )
   120  
   121  type Options struct {
   122  	TimestampFormat string
   123  
   124  	Level Level
   125  
   126  	DisableCaller bool
   127  
   128  	Format Format
   129  
   130  	Output io.Writer
   131  }
   132  
   133  func Configure(opts Options) {
   134  	Log = logrus.New()
   135  
   136  	if opts.Output != nil {
   137  		Log.SetOutput(opts.Output)
   138  	}
   139  	Log.SetLevel(logrus.Level(opts.Level))
   140  
   141  	if !opts.DisableCaller {
   142  		Log.AddHook(ContextHook{})
   143  	}
   144  
   145  	var formatter logrus.Formatter
   146  
   147  	switch opts.Format {
   148  	case FormatConsole:
   149  		formatter = &logrus.TextFormatter{
   150  			FullTimestamp:   true,
   151  			TimestampFormat: opts.TimestampFormat,
   152  		}
   153  	case FormatJSON:
   154  		formatter = &logrus.JSONFormatter{
   155  			TimestampFormat: opts.TimestampFormat,
   156  		}
   157  	}
   158  	Log.Formatter = formatter
   159  }
   160  
   161  // Setup allows to override the global logger setup.
   162  func Setup(debug bool) {
   163  	if debug {
   164  		Log.Level = logrus.DebugLevel
   165  	}
   166  }
   167  
   168  // Ctx short for log context, alias for the more verbose logrus.Fields.
   169  type Ctx map[string]interface{}
   170  
   171  // Logger is a wrapper for logrus.Entry.
   172  type Logger struct {
   173  	*logrus.Entry
   174  }
   175  
   176  // New returns a new Logger with a given context, derived from the global Log.
   177  func New(ctx Ctx) *Logger {
   178  	return NewFromLogger(Log, ctx)
   179  }
   180  
   181  // NewEmpty returns a new logger with empty context
   182  func NewEmpty() *Logger {
   183  	return New(Ctx{})
   184  }
   185  
   186  // NewFromLogger returns a new Logger derived from a given logrus.Logger,
   187  // instead of the global one.
   188  func NewFromLogger(log *logrus.Logger, ctx Ctx) *Logger {
   189  	return &Logger{log.WithFields(logrus.Fields(ctx))}
   190  }
   191  
   192  // NewFromLogger returns a new Logger derived from a given logrus.Logger,
   193  // instead of the global one.
   194  func NewFromEntry(log *logrus.Entry, ctx Ctx) *Logger {
   195  	return &Logger{log.WithFields(logrus.Fields(ctx))}
   196  }
   197  
   198  // F returns a new Logger enriched with new context fields.
   199  // It's a less verbose wrapper over logrus.WithFields.
   200  func (l *Logger) F(ctx Ctx) *Logger {
   201  	return &Logger{l.Entry.WithFields(logrus.Fields(ctx))}
   202  }
   203  
   204  func (l *Logger) Level() logrus.Level {
   205  	return l.Entry.Logger.Level
   206  }
   207  
   208  type ContextHook struct {
   209  }
   210  
   211  func (hook ContextHook) Levels() []logrus.Level {
   212  	return logrus.AllLevels
   213  }
   214  
   215  func FmtCaller(caller runtime.Frame) string {
   216  	return fmt.Sprintf(
   217  		logFieldCallerFmt,
   218  		path.Base(caller.Function),
   219  		path.Base(caller.File),
   220  		caller.Line,
   221  	)
   222  }
   223  
   224  func (hook ContextHook) Fire(entry *logrus.Entry) error {
   225  	const (
   226  		minCallDepth = 6 // logrus.Logger.Log
   227  		maxCallDepth = 8 // logrus.Logger.<Level>f
   228  	)
   229  	var pcs [1 + maxCallDepth - minCallDepth]uintptr
   230  	if _, ok := entry.Data[logFieldCaller]; !ok {
   231  		// We don't know how deep we are in the callstack since the hook can be fired
   232  		// at different levels. Search between depth 6 -> 8.
   233  		i := runtime.Callers(minCallDepth, pcs[:])
   234  		frames := runtime.CallersFrames(pcs[:i])
   235  		var caller *runtime.Frame
   236  		for frame, _ := frames.Next(); frame.PC != 0; frame, _ = frames.Next() {
   237  			if !strings.HasPrefix(frame.Function, pkgSirupsen) {
   238  				caller = &frame
   239  				break
   240  			}
   241  		}
   242  		if caller != nil {
   243  			entry.Data[logFieldCaller] = FmtCaller(*caller)
   244  		}
   245  	}
   246  	return nil
   247  }
   248  
   249  // WithCallerContext returns a new logger with caller set to the parent caller
   250  // context. The skipParents select how many caller contexts to skip, a value of
   251  // 0 sets the context to the caller of this function.
   252  func (l *Logger) WithCallerContext(skipParents int) *Logger {
   253  	const calleeDepth = 2
   254  	var pc [1]uintptr
   255  	newEntry := l
   256  	i := runtime.Callers(calleeDepth+skipParents, pc[:])
   257  	frame, _ := runtime.CallersFrames(pc[:i]).
   258  		Next()
   259  	if frame.Func != nil {
   260  		newEntry = &Logger{Entry: l.Dup()}
   261  		newEntry.Data[logFieldCaller] = FmtCaller(frame)
   262  	}
   263  	return newEntry
   264  }
   265  
   266  // Grab an instance of Logger that may have been passed in context.Context.
   267  // Returns the logger or creates a new instance if none was found in ctx. Since
   268  // Logger is based on logrus.Entry, if logger instance from context is any of
   269  // logrus.Logger, logrus.Entry, necessary adaption will be applied.
   270  func FromContext(ctx context.Context) *Logger {
   271  	l := ctx.Value(loggerContextKey)
   272  	if l == nil {
   273  		return New(Ctx{})
   274  	}
   275  
   276  	switch v := l.(type) {
   277  	case *Logger:
   278  		return v
   279  	case *logrus.Entry:
   280  		return NewFromEntry(v, Ctx{})
   281  	case *logrus.Logger:
   282  		return NewFromLogger(v, Ctx{})
   283  	default:
   284  		return New(Ctx{})
   285  	}
   286  }
   287  
   288  // WithContext adds logger to context `ctx` and returns the resulting context.
   289  func WithContext(ctx context.Context, log *Logger) context.Context {
   290  	return context.WithValue(ctx, loggerContextKey, log)
   291  }