github.com/whtcorpsinc/MilevaDB-Prod@v0.0.0-20211104133533-f57f4be3b597/soliton/logutil/log.go (about)

     1  // Copyright 2020 WHTCORPS INC, Inc.
     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  // See the License for the specific language governing permissions and
    12  // limitations under the License.
    13  
    14  package logutil
    15  
    16  import (
    17  	"bytes"
    18  	"context"
    19  	"fmt"
    20  	"os"
    21  	"path/filepath"
    22  	"runtime"
    23  	"sort"
    24  	"strings"
    25  	"time"
    26  
    27  	"github.com/opentracing/opentracing-go"
    28  	tlog "github.com/opentracing/opentracing-go/log"
    29  	log "github.com/sirupsen/logrus"
    30  	"github.com/whtcorpsinc/errors"
    31  	zaplog "github.com/whtcorpsinc/log"
    32  	"go.uber.org/zap"
    33  	"go.uber.org/zap/zapembedded"
    34  	"gopkg.in/natefinch/lumberjack.v2"
    35  )
    36  
    37  const (
    38  	defaultLogTimeFormat = "2006/01/02 15:04:05.000"
    39  	// DefaultLogMaxSize is the default size of log files.
    40  	DefaultLogMaxSize = 300 // MB
    41  	// DefaultLogFormat is the default format of the log.
    42  	DefaultLogFormat = "text"
    43  	defaultLogLevel  = log.InfoLevel
    44  	// DefaultSlowThreshold is the default slow log threshold in millisecond.
    45  	DefaultSlowThreshold = 300
    46  	// DefaultQueryLogMaxLen is the default max length of the query in the log.
    47  	DefaultQueryLogMaxLen = 4096
    48  	// DefaultRecordCausetInSlowLog is the default value for whether enable log query plan in the slow log.
    49  	DefaultRecordCausetInSlowLog = 1
    50  	// DefaultMilevaDBEnableSlowLog enables MilevaDB to log slow queries.
    51  	DefaultMilevaDBEnableSlowLog = true
    52  )
    53  
    54  // EmptyFileLogConfig is an empty FileLogConfig.
    55  var EmptyFileLogConfig = FileLogConfig{}
    56  
    57  // FileLogConfig serializes file log related config in toml/json.
    58  type FileLogConfig struct {
    59  	zaplog.FileLogConfig
    60  }
    61  
    62  // NewFileLogConfig creates a FileLogConfig.
    63  func NewFileLogConfig(maxSize uint) FileLogConfig {
    64  	return FileLogConfig{FileLogConfig: zaplog.FileLogConfig{
    65  		MaxSize: int(maxSize),
    66  	},
    67  	}
    68  }
    69  
    70  // LogConfig serializes log related config in toml/json.
    71  type LogConfig struct {
    72  	zaplog.Config
    73  
    74  	// SlowQueryFile filename, default to File log config on empty.
    75  	SlowQueryFile string
    76  }
    77  
    78  // NewLogConfig creates a LogConfig.
    79  func NewLogConfig(level, format, slowQueryFile string, fileCfg FileLogConfig, disableTimestamp bool, opts ...func(*zaplog.Config)) *LogConfig {
    80  	c := &LogConfig{
    81  		Config: zaplog.Config{
    82  			Level:            level,
    83  			Format:           format,
    84  			DisableTimestamp: disableTimestamp,
    85  			File:             fileCfg.FileLogConfig,
    86  		},
    87  		SlowQueryFile: slowQueryFile,
    88  	}
    89  	for _, opt := range opts {
    90  		opt(&c.Config)
    91  	}
    92  	return c
    93  }
    94  
    95  // isSKippedPackageName tests wether path name is on log library calling stack.
    96  func isSkippedPackageName(name string) bool {
    97  	return strings.Contains(name, "github.com/sirupsen/logrus") ||
    98  		strings.Contains(name, "github.com/embeddedos/pkg/capnslog")
    99  }
   100  
   101  // modifyHook injects file name and line pos into log entry.
   102  type contextHook struct{}
   103  
   104  // Fire implements logrus.Hook interface
   105  // https://github.com/sirupsen/logrus/issues/63
   106  func (hook *contextHook) Fire(entry *log.Entry) error {
   107  	pc := make([]uintptr, 4)
   108  	cnt := runtime.Callers(8, pc)
   109  
   110  	for i := 0; i < cnt; i++ {
   111  		fu := runtime.FuncForPC(pc[i] - 1)
   112  		name := fu.Name()
   113  		if !isSkippedPackageName(name) {
   114  			file, line := fu.FileLine(pc[i] - 1)
   115  			entry.Data["file"] = filepath.Base(file)
   116  			entry.Data["line"] = line
   117  			break
   118  		}
   119  	}
   120  	return nil
   121  }
   122  
   123  // Levels implements logrus.Hook interface.
   124  func (hook *contextHook) Levels() []log.Level {
   125  	return log.AllLevels
   126  }
   127  
   128  func stringToLogLevel(level string) log.Level {
   129  	switch strings.ToLower(level) {
   130  	case "fatal":
   131  		return log.FatalLevel
   132  	case "error":
   133  		return log.ErrorLevel
   134  	case "warn", "warning":
   135  		return log.WarnLevel
   136  	case "debug":
   137  		return log.DebugLevel
   138  	case "info":
   139  		return log.InfoLevel
   140  	}
   141  	return defaultLogLevel
   142  }
   143  
   144  // textFormatter is for compatibility with ngaut/log
   145  type textFormatter struct {
   146  	DisableTimestamp bool
   147  	EnableEntryOrder bool
   148  }
   149  
   150  // Format implements logrus.Formatter
   151  func (f *textFormatter) Format(entry *log.Entry) ([]byte, error) {
   152  	var b *bytes.Buffer
   153  	if entry.Buffer != nil {
   154  		b = entry.Buffer
   155  	} else {
   156  		b = &bytes.Buffer{}
   157  	}
   158  
   159  	if !f.DisableTimestamp {
   160  		fmt.Fprintf(b, "%s ", entry.Time.Format(defaultLogTimeFormat))
   161  	}
   162  	if file, ok := entry.Data["file"]; ok {
   163  		fmt.Fprintf(b, "%s:%v:", file, entry.Data["line"])
   164  	}
   165  	fmt.Fprintf(b, " [%s] %s", entry.Level.String(), entry.Message)
   166  
   167  	if f.EnableEntryOrder {
   168  		keys := make([]string, 0, len(entry.Data))
   169  		for k := range entry.Data {
   170  			if k != "file" && k != "line" {
   171  				keys = append(keys, k)
   172  			}
   173  		}
   174  		sort.Strings(keys)
   175  		for _, k := range keys {
   176  			fmt.Fprintf(b, " %v=%v", k, entry.Data[k])
   177  		}
   178  	} else {
   179  		for k, v := range entry.Data {
   180  			if k != "file" && k != "line" {
   181  				fmt.Fprintf(b, " %v=%v", k, v)
   182  			}
   183  		}
   184  	}
   185  
   186  	b.WriteByte('\n')
   187  
   188  	return b.Bytes(), nil
   189  }
   190  
   191  const (
   192  	// SlowLogTimeFormat is the time format for slow log.
   193  	SlowLogTimeFormat = time.RFC3339Nano
   194  	// OldSlowLogTimeFormat is the first version of the the time format for slow log, This is use for compatibility.
   195  	OldSlowLogTimeFormat = "2006-01-02-15:04:05.999999999 -0700"
   196  )
   197  
   198  type slowLogFormatter struct{}
   199  
   200  func (f *slowLogFormatter) Format(entry *log.Entry) ([]byte, error) {
   201  	var b *bytes.Buffer
   202  	if entry.Buffer != nil {
   203  		b = entry.Buffer
   204  	} else {
   205  		b = &bytes.Buffer{}
   206  	}
   207  
   208  	fmt.Fprintf(b, "# Time: %s\n", entry.Time.Format(SlowLogTimeFormat))
   209  	fmt.Fprintf(b, "%s\n", entry.Message)
   210  	return b.Bytes(), nil
   211  }
   212  
   213  func stringToLogFormatter(format string, disableTimestamp bool) log.Formatter {
   214  	switch strings.ToLower(format) {
   215  	case "text":
   216  		return &textFormatter{
   217  			DisableTimestamp: disableTimestamp,
   218  		}
   219  	default:
   220  		return &textFormatter{}
   221  	}
   222  }
   223  
   224  // initFileLog initializes file based logging options.
   225  func initFileLog(cfg *zaplog.FileLogConfig, logger *log.Logger) error {
   226  	if st, err := os.Stat(cfg.Filename); err == nil {
   227  		if st.IsDir() {
   228  			return errors.New("can't use directory as log file name")
   229  		}
   230  	}
   231  	if cfg.MaxSize == 0 {
   232  		cfg.MaxSize = DefaultLogMaxSize
   233  	}
   234  
   235  	// use lumberjack to logrotate
   236  	output := &lumberjack.Logger{
   237  		Filename:   cfg.Filename,
   238  		MaxSize:    cfg.MaxSize,
   239  		MaxBackups: cfg.MaxBackups,
   240  		MaxAge:     cfg.MaxDays,
   241  		LocalTime:  true,
   242  	}
   243  
   244  	if logger == nil {
   245  		log.SetOutput(output)
   246  	} else {
   247  		logger.Out = output
   248  	}
   249  	return nil
   250  }
   251  
   252  // SlowQueryLogger is used to log slow query, InitLogger will modify it according to config file.
   253  var SlowQueryLogger = log.StandardLogger()
   254  
   255  // SlowQueryZapLogger is used to log slow query, InitZapLogger will modify it according to config file.
   256  var SlowQueryZapLogger = zaplog.L()
   257  
   258  // InitLogger initializes FIDel's logger.
   259  func InitLogger(cfg *LogConfig) error {
   260  	log.SetLevel(stringToLogLevel(cfg.Level))
   261  	log.AddHook(&contextHook{})
   262  
   263  	if cfg.Format == "" {
   264  		cfg.Format = DefaultLogFormat
   265  	}
   266  	formatter := stringToLogFormatter(cfg.Format, cfg.DisableTimestamp)
   267  	log.SetFormatter(formatter)
   268  
   269  	if len(cfg.File.Filename) != 0 {
   270  		if err := initFileLog(&cfg.File, nil); err != nil {
   271  			return errors.Trace(err)
   272  		}
   273  	}
   274  
   275  	if len(cfg.SlowQueryFile) != 0 {
   276  		SlowQueryLogger = log.New()
   277  		tmp := cfg.File
   278  		tmp.Filename = cfg.SlowQueryFile
   279  		if err := initFileLog(&tmp, SlowQueryLogger); err != nil {
   280  			return errors.Trace(err)
   281  		}
   282  		SlowQueryLogger.Formatter = &slowLogFormatter{}
   283  	}
   284  
   285  	return nil
   286  }
   287  
   288  // InitZapLogger initializes a zap logger with cfg.
   289  func InitZapLogger(cfg *LogConfig) error {
   290  	gl, props, err := zaplog.InitLogger(&cfg.Config, zap.AddStacktrace(zapembedded.FatalLevel))
   291  	if err != nil {
   292  		return errors.Trace(err)
   293  	}
   294  	zaplog.ReplaceGlobals(gl, props)
   295  
   296  	if len(cfg.SlowQueryFile) != 0 {
   297  		sqfCfg := zaplog.FileLogConfig{
   298  			MaxSize:  cfg.File.MaxSize,
   299  			Filename: cfg.SlowQueryFile,
   300  		}
   301  		sqCfg := &zaplog.Config{
   302  			Level:            cfg.Level,
   303  			Format:           cfg.Format,
   304  			DisableTimestamp: cfg.DisableTimestamp,
   305  			File:             sqfCfg,
   306  		}
   307  		sqLogger, _, err := zaplog.InitLogger(sqCfg)
   308  		if err != nil {
   309  			return errors.Trace(err)
   310  		}
   311  		SlowQueryZapLogger = sqLogger
   312  	} else {
   313  		SlowQueryZapLogger = gl
   314  	}
   315  
   316  	return nil
   317  }
   318  
   319  // SetLevel sets the zap logger's level.
   320  func SetLevel(level string) error {
   321  	l := zap.NewAtomicLevel()
   322  	if err := l.UnmarshalText([]byte(level)); err != nil {
   323  		return errors.Trace(err)
   324  	}
   325  	zaplog.SetLevel(l.Level())
   326  	return nil
   327  }
   328  
   329  type ctxLogKeyType struct{}
   330  
   331  var ctxLogKey = ctxLogKeyType{}
   332  
   333  // Logger gets a contextual logger from current context.
   334  // contextual logger will output common fields from context.
   335  func Logger(ctx context.Context) *zap.Logger {
   336  	if ctxlogger, ok := ctx.Value(ctxLogKey).(*zap.Logger); ok {
   337  		return ctxlogger
   338  	}
   339  	return zaplog.L()
   340  }
   341  
   342  // BgLogger is alias of `logutil.BgLogger()`
   343  func BgLogger() *zap.Logger {
   344  	return zaplog.L()
   345  }
   346  
   347  // WithConnID attaches connId to context.
   348  func WithConnID(ctx context.Context, connID uint32) context.Context {
   349  	var logger *zap.Logger
   350  	if ctxLogger, ok := ctx.Value(ctxLogKey).(*zap.Logger); ok {
   351  		logger = ctxLogger
   352  	} else {
   353  		logger = zaplog.L()
   354  	}
   355  	return context.WithValue(ctx, ctxLogKey, logger.With(zap.Uint32("conn", connID)))
   356  }
   357  
   358  // WithKeyValue attaches key/value to context.
   359  func WithKeyValue(ctx context.Context, key, value string) context.Context {
   360  	var logger *zap.Logger
   361  	if ctxLogger, ok := ctx.Value(ctxLogKey).(*zap.Logger); ok {
   362  		logger = ctxLogger
   363  	} else {
   364  		logger = zaplog.L()
   365  	}
   366  	return context.WithValue(ctx, ctxLogKey, logger.With(zap.String(key, value)))
   367  }
   368  
   369  // TraceEventKey presents the TraceEventKey in span log.
   370  const TraceEventKey = "event"
   371  
   372  // Event records event in current tracing span.
   373  func Event(ctx context.Context, event string) {
   374  	if span := opentracing.SpanFromContext(ctx); span != nil && span.Tracer() != nil {
   375  		span.LogFields(tlog.String(TraceEventKey, event))
   376  	}
   377  }
   378  
   379  // Eventf records event in current tracing span with format support.
   380  func Eventf(ctx context.Context, format string, args ...interface{}) {
   381  	if span := opentracing.SpanFromContext(ctx); span != nil && span.Tracer() != nil {
   382  		span.LogFields(tlog.String(TraceEventKey, fmt.Sprintf(format, args...)))
   383  	}
   384  }
   385  
   386  // SetTag sets tag ekv-pair in current tracing span
   387  func SetTag(ctx context.Context, key string, value interface{}) {
   388  	if span := opentracing.SpanFromContext(ctx); span != nil && span.Tracer() != nil {
   389  		span.SetTag(key, value)
   390  	}
   391  }