MM-27744 disable Zap for unit tests. Zap has no concept of shutdown or close. Zap is only shutdown when the app exits. Not a problem for console logging, but when creating a new Zap logger that outputs to files on every unit test, that leaves no easy way to clean up until process exit. Depending on what else is running this can exhaust all file handles and cause unit tests to fail. Zap is now disabled unit tests and uses Logr instead, regardless of config settings. `make test-server` peak file handle usage dropped from ~5K to less than 100.
326 строки
9.5 KiB
Go
326 строки
9.5 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package mlog
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"os"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/mattermost/logr"
|
|
"go.uber.org/zap"
|
|
"go.uber.org/zap/zapcore"
|
|
"gopkg.in/natefinch/lumberjack.v2"
|
|
)
|
|
|
|
const (
|
|
// Very verbose messages for debugging specific issues
|
|
LevelDebug = "debug"
|
|
// Default log level, informational
|
|
LevelInfo = "info"
|
|
// Warnings are messages about possible issues
|
|
LevelWarn = "warn"
|
|
// Errors are messages about things we know are problems
|
|
LevelError = "error"
|
|
|
|
// DefaultFlushTimeout is the default amount of time mlog.Flush will wait
|
|
// before timing out.
|
|
DefaultFlushTimeout = time.Second * 5
|
|
)
|
|
|
|
var (
|
|
// disableZap is set when Zap should be disabled and Logr used instead.
|
|
// This is needed for unit testing as Zap has no shutdown capabilities
|
|
// and holds file handles until process exit. Currently unit test create
|
|
// many server instances, and thus many Zap log files.
|
|
// This flag will be removed when Zap is permanently replaced.
|
|
disableZap int32
|
|
)
|
|
|
|
// Type and function aliases from zap to limit the libraries scope into MM code
|
|
type Field = zapcore.Field
|
|
|
|
var Int64 = zap.Int64
|
|
var Int32 = zap.Int32
|
|
var Int = zap.Int
|
|
var Uint32 = zap.Uint32
|
|
var String = zap.String
|
|
var Any = zap.Any
|
|
var Err = zap.Error
|
|
var NamedErr = zap.NamedError
|
|
var Bool = zap.Bool
|
|
var Duration = zap.Duration
|
|
|
|
type TargetInfo logr.TargetInfo
|
|
|
|
type LoggerConfiguration struct {
|
|
EnableConsole bool
|
|
ConsoleJson bool
|
|
ConsoleLevel string
|
|
EnableFile bool
|
|
FileJson bool
|
|
FileLevel string
|
|
FileLocation string
|
|
}
|
|
|
|
type Logger struct {
|
|
zap *zap.Logger
|
|
consoleLevel zap.AtomicLevel
|
|
fileLevel zap.AtomicLevel
|
|
logrLogger *logr.Logger
|
|
}
|
|
|
|
func getZapLevel(level string) zapcore.Level {
|
|
switch level {
|
|
case LevelInfo:
|
|
return zapcore.InfoLevel
|
|
case LevelWarn:
|
|
return zapcore.WarnLevel
|
|
case LevelDebug:
|
|
return zapcore.DebugLevel
|
|
case LevelError:
|
|
return zapcore.ErrorLevel
|
|
default:
|
|
return zapcore.InfoLevel
|
|
}
|
|
}
|
|
|
|
func makeEncoder(json bool) zapcore.Encoder {
|
|
encoderConfig := zap.NewProductionEncoderConfig()
|
|
if json {
|
|
return zapcore.NewJSONEncoder(encoderConfig)
|
|
}
|
|
|
|
encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
|
|
return zapcore.NewConsoleEncoder(encoderConfig)
|
|
}
|
|
|
|
func NewLogger(config *LoggerConfiguration) *Logger {
|
|
cores := []zapcore.Core{}
|
|
logger := &Logger{
|
|
consoleLevel: zap.NewAtomicLevelAt(getZapLevel(config.ConsoleLevel)),
|
|
fileLevel: zap.NewAtomicLevelAt(getZapLevel(config.FileLevel)),
|
|
logrLogger: newLogr(),
|
|
}
|
|
|
|
if config.EnableConsole {
|
|
writer := zapcore.Lock(os.Stderr)
|
|
core := zapcore.NewCore(makeEncoder(config.ConsoleJson), writer, logger.consoleLevel)
|
|
cores = append(cores, core)
|
|
}
|
|
|
|
if config.EnableFile {
|
|
if atomic.LoadInt32(&disableZap) != 0 {
|
|
t := &LogTarget{
|
|
Type: "file",
|
|
Format: "json",
|
|
Levels: mlogLevelToLogrLevels(config.FileLevel),
|
|
MaxQueueSize: DefaultMaxTargetQueue,
|
|
Options: []byte(fmt.Sprintf(`{"Filename":"%s", "MaxSizeMB":%d, "Compress":%t}`,
|
|
config.FileLocation, 100, true)),
|
|
}
|
|
if !config.FileJson {
|
|
t.Format = "plain"
|
|
}
|
|
if tgt, err := NewLogrTarget("mlogFile", t); err == nil {
|
|
logger.logrLogger.Logr().AddTarget(tgt)
|
|
} else {
|
|
Error("error creating mlogFile", Err(err))
|
|
}
|
|
} else {
|
|
writer := zapcore.AddSync(&lumberjack.Logger{
|
|
Filename: config.FileLocation,
|
|
MaxSize: 100,
|
|
Compress: true,
|
|
})
|
|
|
|
core := zapcore.NewCore(makeEncoder(config.FileJson), writer, logger.fileLevel)
|
|
cores = append(cores, core)
|
|
}
|
|
}
|
|
|
|
combinedCore := zapcore.NewTee(cores...)
|
|
|
|
logger.zap = zap.New(combinedCore,
|
|
zap.AddCaller(),
|
|
)
|
|
return logger
|
|
}
|
|
|
|
func (l *Logger) ChangeLevels(config *LoggerConfiguration) {
|
|
l.consoleLevel.SetLevel(getZapLevel(config.ConsoleLevel))
|
|
l.fileLevel.SetLevel(getZapLevel(config.FileLevel))
|
|
}
|
|
|
|
func (l *Logger) SetConsoleLevel(level string) {
|
|
l.consoleLevel.SetLevel(getZapLevel(level))
|
|
}
|
|
|
|
func (l *Logger) With(fields ...Field) *Logger {
|
|
newlogger := *l
|
|
newlogger.zap = newlogger.zap.With(fields...)
|
|
if newlogger.logrLogger != nil {
|
|
ll := newlogger.logrLogger.WithFields(zapToLogr(fields))
|
|
newlogger.logrLogger = &ll
|
|
}
|
|
return &newlogger
|
|
}
|
|
|
|
func (l *Logger) StdLog(fields ...Field) *log.Logger {
|
|
return zap.NewStdLog(l.With(fields...).zap.WithOptions(getStdLogOption()))
|
|
}
|
|
|
|
// StdLogAt returns *log.Logger which writes to supplied zap logger at required level.
|
|
func (l *Logger) StdLogAt(level string, fields ...Field) (*log.Logger, error) {
|
|
return zap.NewStdLogAt(l.With(fields...).zap.WithOptions(getStdLogOption()), getZapLevel(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 accordingly
|
|
func (l *Logger) StdLogWriter() io.Writer {
|
|
newLogger := *l
|
|
newLogger.zap = newLogger.zap.WithOptions(zap.AddCallerSkip(4), getStdLogOption())
|
|
f := newLogger.Info
|
|
return &loggerWriter{f}
|
|
}
|
|
|
|
func (l *Logger) WithCallerSkip(skip int) *Logger {
|
|
newlogger := *l
|
|
newlogger.zap = newlogger.zap.WithOptions(zap.AddCallerSkip(skip))
|
|
return &newlogger
|
|
}
|
|
|
|
// Made for the plugin interface, wraps mlog in a simpler interface
|
|
// at the cost of performance
|
|
func (l *Logger) Sugar() *SugarLogger {
|
|
return &SugarLogger{
|
|
wrappedLogger: l,
|
|
zapSugar: l.zap.Sugar(),
|
|
}
|
|
}
|
|
|
|
func (l *Logger) Debug(message string, fields ...Field) {
|
|
l.zap.Debug(message, fields...)
|
|
if isLevelEnabled(l.logrLogger, logr.Debug) {
|
|
l.logrLogger.WithFields(zapToLogr(fields)).Debug(message)
|
|
}
|
|
}
|
|
|
|
func (l *Logger) Info(message string, fields ...Field) {
|
|
l.zap.Info(message, fields...)
|
|
if isLevelEnabled(l.logrLogger, logr.Info) {
|
|
l.logrLogger.WithFields(zapToLogr(fields)).Info(message)
|
|
}
|
|
}
|
|
|
|
func (l *Logger) Warn(message string, fields ...Field) {
|
|
l.zap.Warn(message, fields...)
|
|
if isLevelEnabled(l.logrLogger, logr.Warn) {
|
|
l.logrLogger.WithFields(zapToLogr(fields)).Warn(message)
|
|
}
|
|
}
|
|
|
|
func (l *Logger) Error(message string, fields ...Field) {
|
|
l.zap.Error(message, fields...)
|
|
if isLevelEnabled(l.logrLogger, logr.Error) {
|
|
l.logrLogger.WithFields(zapToLogr(fields)).Error(message)
|
|
}
|
|
}
|
|
|
|
func (l *Logger) Critical(message string, fields ...Field) {
|
|
l.zap.Error(message, fields...)
|
|
if isLevelEnabled(l.logrLogger, logr.Error) {
|
|
l.logrLogger.WithFields(zapToLogr(fields)).Error(message)
|
|
}
|
|
}
|
|
|
|
func (l *Logger) Log(level LogLevel, message string, fields ...Field) {
|
|
l.logrLogger.WithFields(zapToLogr(fields)).Log(logr.Level(level), message)
|
|
}
|
|
|
|
func (l *Logger) LogM(levels []LogLevel, message string, fields ...Field) {
|
|
var logger *logr.Logger
|
|
for _, lvl := range levels {
|
|
if isLevelEnabled(l.logrLogger, logr.Level(lvl)) {
|
|
// don't create logger with fields unless at least one level is active.
|
|
if logger == nil {
|
|
l := l.logrLogger.WithFields(zapToLogr(fields))
|
|
logger = &l
|
|
}
|
|
logger.Log(logr.Level(lvl), message)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (l *Logger) Flush(cxt context.Context) error {
|
|
return l.logrLogger.Logr().FlushWithTimeout(cxt)
|
|
}
|
|
|
|
// ShutdownAdvancedLogging stops the logger from accepting new log records and tries to
|
|
// flush queues within the context timeout. Once complete all targets are shutdown
|
|
// and any resources released.
|
|
func (l *Logger) ShutdownAdvancedLogging(cxt context.Context) error {
|
|
err := l.logrLogger.Logr().ShutdownWithTimeout(cxt)
|
|
l.logrLogger = newLogr()
|
|
return err
|
|
}
|
|
|
|
// ConfigAdvancedLoggingConfig (re)configures advanced logging based on the
|
|
// specified log targets. This is the easiest way to get the advanced logger
|
|
// configured via a config source such as file.
|
|
func (l *Logger) ConfigAdvancedLogging(targets LogTargetCfg) error {
|
|
if err := l.ShutdownAdvancedLogging(context.Background()); err != nil {
|
|
Error("error shutting down previous logger", Err(err))
|
|
}
|
|
|
|
err := logrAddTargets(l.logrLogger, targets)
|
|
return err
|
|
}
|
|
|
|
// AddTarget adds one or more logr.Target to the advanced logger. This is the preferred method
|
|
// to add custom targets or provide configuration that cannot be expressed via a
|
|
// config source.
|
|
func (l *Logger) AddTarget(targets ...logr.Target) error {
|
|
return l.logrLogger.Logr().AddTarget(targets...)
|
|
}
|
|
|
|
// RemoveTargets selectively removes targets that were previously added to this logger instance
|
|
// using the passed in filter function. The filter function should return true to remove the target
|
|
// and false to keep it.
|
|
func (l *Logger) RemoveTargets(ctx context.Context, f func(ti TargetInfo) bool) error {
|
|
// Use locally defined TargetInfo type so we don't spread Logr dependencies.
|
|
fc := func(tic logr.TargetInfo) bool {
|
|
return f(TargetInfo(tic))
|
|
}
|
|
return l.logrLogger.Logr().RemoveTargets(ctx, fc)
|
|
}
|
|
|
|
// EnableMetrics enables metrics collection by supplying a MetricsCollector.
|
|
// The MetricsCollector provides counters and gauges that are updated by log targets.
|
|
func (l *Logger) EnableMetrics(collector logr.MetricsCollector) error {
|
|
return l.logrLogger.Logr().SetMetricsCollector(collector)
|
|
}
|
|
|
|
// DisableZap is called to disable Zap, and Logr will be used instead. Any Logger
|
|
// instances created after this call will only use Logr.
|
|
//
|
|
// This is needed for unit testing as Zap has no shutdown capabilities
|
|
// and holds file handles until process exit. Currently unit tests create
|
|
// many server instances, and thus many Zap log file handles.
|
|
//
|
|
// This method will be removed when Zap is permanently replaced.
|
|
func DisableZap() {
|
|
atomic.StoreInt32(&disableZap, 1)
|
|
}
|
|
|
|
// EnableZap re-enables Zap such that any Logger instances created after this
|
|
// call will allow Zap targets.
|
|
func EnableZap() {
|
|
atomic.StoreInt32(&disableZap, 0)
|
|
}
|