// Package mlog provides a simple wrapper around Logr. package mlog import ( "context" "encoding/json" "fmt" "io/ioutil" "log" "os" "runtime" "strings" "time" "github.com/mattermost/logr/v2" logrcfg "github.com/mattermost/logr/v2/config" ) const ( ShutdownTimeout = time.Second * 15 ) var ( mlogPkg string ) func init() { // Calc current package name pcs := make([]uintptr, 2) _ = runtime.Callers(0, pcs) tmp := runtime.FuncForPC(pcs[1]).Name() mlogPkg = GetPackageName(tmp) } // Type and function aliases from Logr to limit the spread of dependencies throughout Focalboard. type Field = logr.Field type Level = logr.Level type Option = logr.Option type Target = logr.Target type LogRec = logr.LogRec type LogCloner = logr.LogCloner type MetricsCollector = logr.MetricsCollector // Any picks the best supported field type based on type of val. // For best performance when passing a struct (or struct pointer), // implement `logr.LogWriter` on the struct, otherwise reflection // will be used to generate a string representation. var Any = logr.Any // Int64 constructs a field containing a key and Int64 value. var Int64 = logr.Int64 // Int32 constructs a field containing a key and Int32 value. var Int32 = logr.Int32 // Int constructs a field containing a key and Int value. var Int = logr.Int // Uint64 constructs a field containing a key and Uint64 value. var Uint64 = logr.Uint64 // Uint32 constructs a field containing a key and Uint32 value. var Uint32 = logr.Uint32 // Uint constructs a field containing a key and Uint value. var Uint = logr.Uint // Float64 constructs a field containing a key and Float64 value. var Float64 = logr.Float64 // Float32 constructs a field containing a key and Float32 value. var Float32 = logr.Float32 // String constructs a field containing a key and String value. var String = logr.String // Stringer constructs a field containing a key and a fmt.Stringer value. // The fmt.Stringer's `String` method is called lazily. var Stringer = logr.Stringer // Err constructs a field containing a default key ("error") and error value. var Err = logr.Err // NamedErr constructs a field containing a key and error value. var NamedErr = logr.NamedErr // Bool constructs a field containing a key and bool value. var Bool = logr.Bool // Time constructs a field containing a key and time.Time value. var Time = logr.Time // Duration constructs a field containing a key and time.Duration value. var Duration = logr.Duration // Millis constructs a field containing a key and timestamp value. // The timestamp is expected to be milliseconds since Jan 1, 1970 UTC. var Millis = logr.Millis // Array constructs a field containing a key and array value. var Array = logr.Array // Map constructs a field containing a key and map value. var Map = logr.Map // LoggerConfig is a map of LogTarget configurations. type LoggerConfig map[string]logrcfg.TargetCfg func (lc LoggerConfig) append(cfg LoggerConfig) { for k, v := range cfg { lc[k] = v } } // Logger provides a thin wrapper around a Logr instance. This is a struct instead of an interface // so that there are no allocations on the heap each interface method invocation. Normally not // something to be concerned about, but logging calls for disabled levels should have as little CPU // and memory impact as possible. Most of these wrapper calls will be inlined as well. type Logger struct { log *logr.Logger } // NewLogger creates a new Logger instance which can be configured via `(*Logger).Configure`. func NewLogger(options ...Option) *Logger { options = append(options, logr.StackFilter(GetPackageName(mlogPkg))) lgr, _ := logr.New(options...) log := lgr.NewLogger() return &Logger{ log: &log, } } // Configure provides a new configuration for this logger. // Zero or more sources of config can be provided: // cfgFile - path to file containing JSON // cfgEscaped - JSON string probably from ENV var // // For each case JSON containing log targets is provided. Target name collisions are resolved // using the following precedence: // cfgFile > cfgEscaped func (l *Logger) Configure(cfgFile string, cfgEscaped string) error { cfgMap := make(LoggerConfig) // Add config from file if cfgFile != "" { b, err := ioutil.ReadFile(cfgFile) if err != nil { return fmt.Errorf("error reading logger config file %s: %w", cfgFile, err) } var mapCfgFile LoggerConfig if err := json.Unmarshal(b, &mapCfgFile); err != nil { return fmt.Errorf("error decoding logger config file %s: %w", cfgFile, err) } cfgMap.append(mapCfgFile) } // Add config from escaped json string if cfgEscaped != "" { var mapCfgEscaped LoggerConfig if err := json.Unmarshal([]byte(cfgEscaped), &mapCfgEscaped); err != nil { return fmt.Errorf("error decoding logger config as escaped json: %w", err) } cfgMap.append(mapCfgEscaped) } if len(cfgMap) == 0 { return nil } return logrcfg.ConfigureTargets(l.log.Logr(), cfgMap, nil) } // With creates a new Logger with the specified fields. This is a light-weight // operation and can be called on demand. func (l *Logger) With(fields ...Field) *Logger { logWith := l.log.With(fields...) return &Logger{ log: &logWith, } } // IsLevelEnabled returns true only if at least one log target is // configured to emit the specified log level. Use this check when // gathering the log info may be expensive. // // Note, transformations and serializations done via fields are already // lazily evaluated and don't require this check beforehand. func (l *Logger) IsLevelEnabled(level Level) bool { return l.log.IsLevelEnabled(level) } // Log emits the log record for any targets configured for the specified level. func (l *Logger) Log(level Level, msg string, fields ...Field) { l.log.Log(level, msg, fields...) } // LogM emits the log record for any targets configured for the specified levels. // Equivalent to calling `Log` once for each level. func (l *Logger) LogM(levels []Level, msg string, fields ...Field) { l.log.LogM(levels, msg, fields...) } // Convenience method equivalent to calling `Log` with the `Trace` level. func (l *Logger) Trace(msg string, fields ...Field) { l.log.Trace(msg, fields...) } // Convenience method equivalent to calling `Log` with the `Debug` level. func (l *Logger) Debug(msg string, fields ...Field) { l.log.Debug(msg, fields...) } // Convenience method equivalent to calling `Log` with the `Info` level. func (l *Logger) Info(msg string, fields ...Field) { l.log.Info(msg, fields...) } // Convenience method equivalent to calling `Log` with the `Warn` level. func (l *Logger) Warn(msg string, fields ...Field) { l.log.Warn(msg, fields...) } // Convenience method equivalent to calling `Log` with the `Error` level. func (l *Logger) Error(msg string, fields ...Field) { l.log.Error(msg, fields...) } // Convenience method equivalent to calling `Log` with the `Fatal` level, // followed by `os.Exit(1)`. func (l *Logger) Fatal(msg string, fields ...Field) { l.log.Log(logr.Fatal, msg, fields...) _ = l.Shutdown() os.Exit(1) } // HasTargets returns true if at least one log target has been added. func (l *Logger) HasTargets() bool { return l.log.Logr().HasTargets() } // StdLogger creates a standard logger backed by this logger. // All log records are output with the specified level. func (l *Logger) StdLogger(level Level) *log.Logger { return l.log.StdLogger(level) } // RedirectStdLog redirects output from the standard library's package-global logger // to this logger at the specified level and with zero or more Field's. Since this logger already // handles caller annotations, timestamps, etc., it automatically disables the standard // library's annotations and prefixing. // A function is returned that restores the original prefix and flags and resets the standard // library's output to os.Stdout. func (l *Logger) RedirectStdLog(level Level, fields ...Field) func() { return l.log.Logr().RedirectStdLog(level, fields...) } // Shutdown shuts down the logger after making best efforts to flush any // remaining records. func (l *Logger) Shutdown() error { ctx, cancel := context.WithTimeout(context.Background(), ShutdownTimeout) defer cancel() return l.log.Logr().ShutdownWithTimeout(ctx) } // GetPackageName reduces a fully qualified function name to the package name // By sirupsen: https://github.com/sirupsen/logrus/blob/master/entry.go func GetPackageName(f string) string { for { lastPeriod := strings.LastIndex(f, ".") lastSlash := strings.LastIndex(f, "/") if lastPeriod > lastSlash { f = f[:lastPeriod] } else { break } } return f }