2021-10-16 14:11:32 -07:00
|
|
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
|
|
// See LICENSE.txt for license information.
|
|
|
|
|
|
|
|
// Package mlog provides a simple wrapper around Logr.
|
|
|
|
package mlog
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"encoding/json"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"io/ioutil"
|
|
|
|
"log"
|
|
|
|
"os"
|
|
|
|
"strings"
|
|
|
|
"sync/atomic"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/mattermost/logr/v2"
|
|
|
|
logrcfg "github.com/mattermost/logr/v2/config"
|
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
ShutdownTimeout = time.Second * 15
|
|
|
|
FlushTimeout = time.Second * 15
|
|
|
|
DefaultMaxQueueSize = 1000
|
|
|
|
DefaultMetricsUpdateFreqMillis = 15000
|
|
|
|
)
|
|
|
|
|
|
|
|
type LoggerIFace interface {
|
|
|
|
IsLevelEnabled(Level) bool
|
|
|
|
Debug(string, ...Field)
|
|
|
|
Info(string, ...Field)
|
|
|
|
Warn(string, ...Field)
|
|
|
|
Error(string, ...Field)
|
|
|
|
Critical(string, ...Field)
|
|
|
|
Log(Level, string, ...Field)
|
|
|
|
LogM([]Level, string, ...Field)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Type and function aliases from Logr to limit the spread of dependencies.
|
|
|
|
type Field = logr.Field
|
|
|
|
type Level = logr.Level
|
|
|
|
type Option = logr.Option
|
|
|
|
type Target = logr.Target
|
|
|
|
type TargetInfo = logr.TargetInfo
|
|
|
|
type LogRec = logr.LogRec
|
|
|
|
type LogCloner = logr.LogCloner
|
|
|
|
type MetricsCollector = logr.MetricsCollector
|
|
|
|
type TargetCfg = logrcfg.TargetCfg
|
2021-12-11 15:05:15 -08:00
|
|
|
type TargetFactory = logrcfg.TargetFactory
|
|
|
|
type FormatterFactory = logrcfg.FormatterFactory
|
|
|
|
type Factories = logrcfg.Factories
|
2021-10-16 14:11:32 -07:00
|
|
|
type Sugar = logr.Sugar
|
|
|
|
|
|
|
|
// LoggerConfiguration is a map of LogTarget configurations.
|
|
|
|
type LoggerConfiguration map[string]TargetCfg
|
|
|
|
|
|
|
|
func (lc LoggerConfiguration) Append(cfg LoggerConfiguration) {
|
|
|
|
for k, v := range cfg {
|
|
|
|
lc[k] = v
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (lc LoggerConfiguration) toTargetCfg() map[string]logrcfg.TargetCfg {
|
|
|
|
tcfg := make(map[string]logrcfg.TargetCfg)
|
|
|
|
for k, v := range lc {
|
|
|
|
tcfg[k] = v
|
|
|
|
}
|
|
|
|
return tcfg
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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 = func(key string, s fmt.Stringer) logr.Field {
|
|
|
|
if s == nil {
|
|
|
|
return Field{Key: key, Type: logr.StringType, String: ""}
|
|
|
|
}
|
|
|
|
return Field{Key: key, Type: logr.StringType, String: s.String()}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Err constructs a field containing a default key ("error") and error value.
|
|
|
|
var Err = func(err error) logr.Field {
|
|
|
|
return NamedErr("error", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// NamedErr constructs a field containing a key and error value.
|
|
|
|
var NamedErr = func(key string, err error) logr.Field {
|
|
|
|
if err == nil {
|
|
|
|
return Field{Key: key, Type: logr.StringType, String: ""}
|
|
|
|
}
|
|
|
|
return Field{Key: key, Type: logr.StringType, String: err.Error()}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
|
|
|
// 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
|
|
|
|
lockConfig *int32
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewLogger creates a new Logger instance which can be configured via `(*Logger).Configure`.
|
|
|
|
// Some options with invalid values can cause an error to be returned, however `NewLogger()`
|
|
|
|
// using just defaults never errors.
|
|
|
|
func NewLogger(options ...Option) (*Logger, error) {
|
|
|
|
options = append(options, logr.StackFilter(logr.GetPackageName("NewLogger")))
|
|
|
|
|
|
|
|
lgr, err := logr.New(options...)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
log := lgr.NewLogger()
|
|
|
|
var lockConfig int32
|
|
|
|
|
|
|
|
return &Logger{
|
|
|
|
log: &log,
|
|
|
|
lockConfig: &lockConfig,
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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
|
2021-12-11 15:05:15 -08:00
|
|
|
//
|
|
|
|
// An optional set of factories can be provided which will be called to create any target
|
|
|
|
// types or formatters not built-in.
|
|
|
|
func (l *Logger) Configure(cfgFile string, cfgEscaped string, factories *Factories) error {
|
2021-10-16 14:11:32 -07:00
|
|
|
if atomic.LoadInt32(l.lockConfig) != 0 {
|
|
|
|
return ErrConfigurationLock
|
|
|
|
}
|
|
|
|
|
|
|
|
cfgMap := make(LoggerConfiguration)
|
|
|
|
|
|
|
|
// 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 LoggerConfiguration
|
|
|
|
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 LoggerConfiguration
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2021-12-11 15:05:15 -08:00
|
|
|
return logrcfg.ConfigureTargets(l.log.Logr(), cfgMap.toTargetCfg(), factories)
|
2021-10-16 14:11:32 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
// ConfigureTargets provides a new configuration for this logger via a `LoggerConfig` map.
|
|
|
|
// Typically `mlog.Configure` is used instead which accepts JSON formatted configuration.
|
2021-12-11 15:05:15 -08:00
|
|
|
// An optional set of factories can be provided which will be called to create any target
|
|
|
|
// types or formatters not built-in.
|
|
|
|
func (l *Logger) ConfigureTargets(cfg LoggerConfiguration, factories *Factories) error {
|
2021-10-16 14:11:32 -07:00
|
|
|
if atomic.LoadInt32(l.lockConfig) != 0 {
|
|
|
|
return ErrConfigurationLock
|
|
|
|
}
|
2021-12-11 15:05:15 -08:00
|
|
|
return logrcfg.ConfigureTargets(l.log.Logr(), cfg.toTargetCfg(), factories)
|
2021-10-16 14:11:32 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
// LockConfiguration disallows further configuration changes until `UnlockConfiguration`
|
|
|
|
// is called. The previous locked stated is returned.
|
|
|
|
func (l *Logger) LockConfiguration() bool {
|
|
|
|
old := atomic.SwapInt32(l.lockConfig, 1)
|
|
|
|
return old != 0
|
|
|
|
}
|
|
|
|
|
|
|
|
// UnlockConfiguration allows configuration changes. The previous locked stated is returned.
|
|
|
|
func (l *Logger) UnlockConfiguration() bool {
|
|
|
|
old := atomic.SwapInt32(l.lockConfig, 0)
|
|
|
|
return old != 0
|
|
|
|
}
|
|
|
|
|
|
|
|
// IsConfigurationLocked returns the current state of the configuration lock.
|
|
|
|
func (l *Logger) IsConfigurationLocked() bool {
|
|
|
|
return atomic.LoadInt32(l.lockConfig) != 0
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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,
|
|
|
|
lockConfig: l.lockConfig,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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 `Critical` level.
|
|
|
|
func (l *Logger) Critical(msg string, fields ...Field) {
|
|
|
|
l.log.Log(LvlCritical, 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)
|
|
|
|
}
|
|
|
|
|
|
|
|
// StdLogWriter returns a writer that can be hooked up to the output of a golang standard logger
|
|
|
|
// anything written will be interpreted as log entries and passed to this logger.
|
|
|
|
func (l *Logger) StdLogWriter() io.Writer {
|
|
|
|
return &logWriter{
|
|
|
|
logger: l,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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...)
|
|
|
|
}
|
|
|
|
|
|
|
|
// RemoveTargets safely removes one or more targets based on the filtering method.
|
|
|
|
// `f` should return true to delete the target, false to keep it.
|
|
|
|
// When removing a target, best effort is made to write any queued log records before
|
2022-03-12 10:41:07 -08:00
|
|
|
// closing, with ctx determining how much time can be spent in total.
|
2021-10-16 14:11:32 -07:00
|
|
|
// Note, keep the timeout short since this method blocks certain logging operations.
|
|
|
|
func (l *Logger) RemoveTargets(ctx context.Context, f func(ti TargetInfo) bool) error {
|
|
|
|
return l.log.Logr().RemoveTargets(ctx, f)
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetMetricsCollector sets (or resets) the metrics collector to be used for gathering
|
|
|
|
// metrics for all targets. Only targets added after this call will use the collector.
|
|
|
|
//
|
|
|
|
// To ensure all targets use a collector, use the `SetMetricsCollector` option when
|
|
|
|
// creating the Logger instead, or configure/reconfigure the Logger after calling this method.
|
|
|
|
func (l *Logger) SetMetricsCollector(collector MetricsCollector, updateFrequencyMillis int64) {
|
|
|
|
l.log.Logr().SetMetricsCollector(collector, updateFrequencyMillis)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Sugar creates a new `Logger` with a less structured API. Any fields are preserved.
|
|
|
|
func (l *Logger) Sugar(fields ...Field) Sugar {
|
|
|
|
return l.log.Sugar(fields...)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Flush forces all targets to write out any queued log records with a default timeout.
|
|
|
|
func (l *Logger) Flush() error {
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), FlushTimeout)
|
|
|
|
defer cancel()
|
|
|
|
return l.log.Logr().FlushWithTimeout(ctx)
|
|
|
|
}
|
|
|
|
|
2022-03-12 10:41:07 -08:00
|
|
|
// Flush forces all targets to write out any queued log records with the specified timeout.
|
2021-10-16 14:11:32 -07:00
|
|
|
func (l *Logger) FlushWithTimeout(ctx context.Context) error {
|
|
|
|
return l.log.Logr().FlushWithTimeout(ctx)
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Shutdown shuts down the logger after making best efforts to flush any
|
|
|
|
// remaining records.
|
|
|
|
func (l *Logger) ShutdownWithTimeout(ctx context.Context) error {
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2021-12-11 15:05:15 -08:00
|
|
|
// ShouldQuote returns true if val contains any characters that might be unsafe
|
|
|
|
// when injecting log output into an aggregator, viewer or report.
|
|
|
|
// Returning true means that val should be surrounded by quotation marks before being
|
|
|
|
// output into logs.
|
|
|
|
func ShouldQuote(val string) bool {
|
|
|
|
for _, c := range val {
|
|
|
|
if !((c >= '0' && c <= '9') ||
|
|
|
|
(c >= 'a' && c <= 'z') ||
|
|
|
|
(c >= 'A' && c <= 'Z') ||
|
|
|
|
c == '-' || c == '.' || c == '_' || c == '/' || c == '@' || c == '^' || c == '+') {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2021-10-16 14:11:32 -07:00
|
|
|
type logWriter struct {
|
|
|
|
logger *Logger
|
|
|
|
}
|
|
|
|
|
|
|
|
func (lw *logWriter) Write(p []byte) (int, error) {
|
|
|
|
lw.logger.Info(string(p))
|
|
|
|
return len(p), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// ErrConfigurationLock is returned when one of a logger's configuration APIs is called
|
|
|
|
// while the configuration is locked.
|
|
|
|
var ErrConfigurationLock = errors.New("configuration is locked")
|