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.
248 строки
6.4 KiB
Go
248 строки
6.4 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package mlog
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
|
|
"github.com/hashicorp/go-multierror"
|
|
"github.com/mattermost/logr"
|
|
logrFmt "github.com/mattermost/logr/format"
|
|
"github.com/mattermost/logr/target"
|
|
"go.uber.org/zap/zapcore"
|
|
)
|
|
|
|
const (
|
|
DefaultMaxTargetQueue = 1000
|
|
DefaultSysLogPort = 514
|
|
)
|
|
|
|
type LogLevel struct {
|
|
ID logr.LevelID
|
|
Name string
|
|
Stacktrace bool
|
|
}
|
|
|
|
type LogTarget struct {
|
|
Type string // one of "console", "file", "tcp", "syslog", "none".
|
|
Format string // one of "json", "plain"
|
|
Levels []LogLevel
|
|
Options json.RawMessage
|
|
MaxQueueSize int
|
|
}
|
|
|
|
type LogTargetCfg map[string]*LogTarget
|
|
type LogrCleanup func() error
|
|
|
|
func newLogr() *logr.Logger {
|
|
lgr := &logr.Logr{}
|
|
lgr.OnExit = func(int) {}
|
|
lgr.OnPanic = func(interface{}) {}
|
|
lgr.OnLoggerError = onLoggerError
|
|
lgr.OnQueueFull = onQueueFull
|
|
lgr.OnTargetQueueFull = onTargetQueueFull
|
|
|
|
logger := lgr.NewLogger()
|
|
return &logger
|
|
}
|
|
|
|
func logrAddTargets(logger *logr.Logger, targets LogTargetCfg) error {
|
|
lgr := logger.Logr()
|
|
var errs error
|
|
for name, t := range targets {
|
|
target, err := NewLogrTarget(name, t)
|
|
if err != nil {
|
|
errs = multierror.Append(err)
|
|
continue
|
|
}
|
|
if target != nil {
|
|
target.SetName(name)
|
|
lgr.AddTarget(target)
|
|
}
|
|
}
|
|
return errs
|
|
}
|
|
|
|
// NewLogrTarget creates a `logr.Target` based on a target config.
|
|
// Can be used when parsing custom config files, or when programmatically adding
|
|
// built-in targets. Use `mlog.AddTarget` to add custom targets.
|
|
func NewLogrTarget(name string, t *LogTarget) (logr.Target, error) {
|
|
formatter, err := newFormatter(name, t.Format)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
filter, err := newFilter(name, t.Levels)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if t.MaxQueueSize == 0 {
|
|
t.MaxQueueSize = DefaultMaxTargetQueue
|
|
}
|
|
|
|
switch t.Type {
|
|
case "console":
|
|
return newConsoleTarget(name, t, filter, formatter)
|
|
case "file":
|
|
return newFileTarget(name, t, filter, formatter)
|
|
case "syslog":
|
|
return newSyslogTarget(name, t, filter, formatter)
|
|
case "tcp":
|
|
return newTCPTarget(name, t, filter, formatter)
|
|
case "none":
|
|
return nil, nil
|
|
}
|
|
return nil, fmt.Errorf("invalid type '%s' for target %s", t.Type, name)
|
|
}
|
|
|
|
func newFilter(name string, levels []LogLevel) (logr.Filter, error) {
|
|
filter := &logr.CustomFilter{}
|
|
for _, lvl := range levels {
|
|
filter.Add(logr.Level(lvl))
|
|
}
|
|
return filter, nil
|
|
}
|
|
|
|
func newFormatter(name string, format string) (logr.Formatter, error) {
|
|
switch format {
|
|
case "json", "":
|
|
return &logrFmt.JSON{}, nil
|
|
case "plain":
|
|
return &logrFmt.Plain{Delim: " | "}, nil
|
|
default:
|
|
return nil, fmt.Errorf("invalid format '%s' for target %s", format, name)
|
|
}
|
|
}
|
|
|
|
func newConsoleTarget(name string, t *LogTarget, filter logr.Filter, formatter logr.Formatter) (logr.Target, error) {
|
|
type consoleOptions struct {
|
|
Out string `json:"Out"`
|
|
}
|
|
options := &consoleOptions{}
|
|
if err := json.Unmarshal(t.Options, options); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var w io.Writer
|
|
switch options.Out {
|
|
case "stdout", "":
|
|
w = os.Stdout
|
|
case "stderr":
|
|
w = os.Stderr
|
|
default:
|
|
return nil, fmt.Errorf("invalid out '%s' for target %s", options.Out, name)
|
|
}
|
|
|
|
newTarget := target.NewWriterTarget(filter, formatter, w, t.MaxQueueSize)
|
|
return newTarget, nil
|
|
}
|
|
|
|
func newFileTarget(name string, t *LogTarget, filter logr.Filter, formatter logr.Formatter) (logr.Target, error) {
|
|
type fileOptions struct {
|
|
Filename string `json:"Filename"`
|
|
MaxSize int `json:"MaxSizeMB"`
|
|
MaxAge int `json:"MaxAgeDays"`
|
|
MaxBackups int `json:"MaxBackups"`
|
|
Compress bool `json:"Compress"`
|
|
}
|
|
options := &fileOptions{}
|
|
if err := json.Unmarshal(t.Options, options); err != nil {
|
|
return nil, err
|
|
}
|
|
return newFileTargetWithOpts(name, t, target.FileOptions(*options), filter, formatter)
|
|
}
|
|
|
|
func newFileTargetWithOpts(name string, t *LogTarget, opts target.FileOptions, filter logr.Filter, formatter logr.Formatter) (logr.Target, error) {
|
|
if opts.Filename == "" {
|
|
return nil, fmt.Errorf("missing 'Filename' option for target %s", name)
|
|
}
|
|
if err := checkFileWritable(opts.Filename); err != nil {
|
|
return nil, fmt.Errorf("error writing to 'Filename' for target %s: %w", name, err)
|
|
}
|
|
|
|
newTarget := target.NewFileTarget(filter, formatter, opts, t.MaxQueueSize)
|
|
return newTarget, nil
|
|
}
|
|
|
|
func newSyslogTarget(name string, t *LogTarget, filter logr.Filter, formatter logr.Formatter) (logr.Target, error) {
|
|
options := &SyslogParams{}
|
|
if err := json.Unmarshal(t.Options, options); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if options.IP == "" {
|
|
return nil, fmt.Errorf("missing 'IP' option for target %s", name)
|
|
}
|
|
if options.Port == 0 {
|
|
options.Port = DefaultSysLogPort
|
|
}
|
|
return NewSyslogTarget(filter, formatter, options, t.MaxQueueSize)
|
|
}
|
|
|
|
func newTCPTarget(name string, t *LogTarget, filter logr.Filter, formatter logr.Formatter) (logr.Target, error) {
|
|
options := &TcpParams{}
|
|
if err := json.Unmarshal(t.Options, options); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if options.IP == "" {
|
|
return nil, fmt.Errorf("missing 'IP' option for target %s", name)
|
|
}
|
|
if options.Port == 0 {
|
|
return nil, fmt.Errorf("missing 'Port' option for target %s", name)
|
|
}
|
|
return NewTcpTarget(filter, formatter, options, t.MaxQueueSize)
|
|
}
|
|
|
|
func checkFileWritable(filename string) error {
|
|
// try opening/creating the file for writing
|
|
file, err := os.OpenFile(filename, os.O_RDWR|os.O_APPEND|os.O_CREATE, 0600)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
file.Close()
|
|
return nil
|
|
}
|
|
|
|
func isLevelEnabled(logger *logr.Logger, level logr.Level) bool {
|
|
if logger == nil || logger.Logr() == nil {
|
|
return false
|
|
}
|
|
|
|
status := logger.Logr().IsLevelEnabled(level)
|
|
return status.Enabled
|
|
}
|
|
|
|
// zapToLogr converts Zap fields to Logr fields.
|
|
// This will not be needed once Logr is used for all logging.
|
|
func zapToLogr(zapFields []Field) logr.Fields {
|
|
encoder := zapcore.NewMapObjectEncoder()
|
|
for _, zapField := range zapFields {
|
|
zapField.AddTo(encoder)
|
|
}
|
|
return logr.Fields(encoder.Fields)
|
|
}
|
|
|
|
// mlogLevelToLogrLevel converts a mlog logger level to
|
|
// an array of discrete Logr levels.
|
|
func mlogLevelToLogrLevels(level string) []LogLevel {
|
|
levels := make([]LogLevel, 0)
|
|
levels = append(levels, LvlError, LvlPanic, LvlFatal, LvlStdLog)
|
|
|
|
switch level {
|
|
case LevelDebug:
|
|
levels = append(levels, LvlDebug)
|
|
fallthrough
|
|
case LevelInfo:
|
|
levels = append(levels, LvlInfo)
|
|
fallthrough
|
|
case LevelWarn:
|
|
levels = append(levels, LvlWarn)
|
|
}
|
|
return levels
|
|
}
|