github.com/pingcap/tiflow@v0.0.0-20240520035814-5bf52d54e205/pkg/logutil/log.go (about)

     1  // Copyright 2020 PingCAP, 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  	"io"
    20  	"os"
    21  	"strconv"
    22  	"strings"
    23  
    24  	"github.com/IBM/sarama"
    25  	"github.com/gin-gonic/gin"
    26  	"github.com/go-sql-driver/mysql"
    27  	"github.com/pingcap/errors"
    28  	"github.com/pingcap/log"
    29  	"go.uber.org/zap"
    30  	"go.uber.org/zap/zapcore"
    31  	"google.golang.org/grpc/grpclog"
    32  )
    33  
    34  // globalP is the global ZapProperties in log
    35  var globalP *log.ZapProperties
    36  
    37  const (
    38  	defaultLogLevel   = "info"
    39  	defaultLogMaxDays = 7
    40  	defaultLogMaxSize = 512 // MB
    41  )
    42  
    43  // Config serializes log related config in toml/json.
    44  type Config struct {
    45  	// Log level.
    46  	Level string `toml:"level" json:"level"`
    47  	// Log filename, leave empty to disable file log.
    48  	File string `toml:"file" json:"file"`
    49  	// Max size for a single file, in MB.
    50  	FileMaxSize int `toml:"max-size" json:"max-size"`
    51  	// Max log keep days, default is never deleting.
    52  	FileMaxDays int `toml:"max-days" json:"max-days"`
    53  	// Maximum number of old log files to retain.
    54  	FileMaxBackups int `toml:"max-backups" json:"max-backups"`
    55  	// ZapInternalErrOutput specify where the internal error of zap logger should be send to.
    56  	ZapInternalErrOutput string `toml:"error-output" json:"error-output"`
    57  }
    58  
    59  // Adjust adjusts config
    60  func (cfg *Config) Adjust() {
    61  	if len(cfg.Level) == 0 {
    62  		cfg.Level = defaultLogLevel
    63  	}
    64  	if cfg.Level == "warning" {
    65  		cfg.Level = "warn"
    66  	}
    67  	if cfg.FileMaxSize == 0 {
    68  		cfg.FileMaxSize = defaultLogMaxSize
    69  	}
    70  	if cfg.FileMaxDays == 0 {
    71  		cfg.FileMaxDays = defaultLogMaxDays
    72  	}
    73  }
    74  
    75  // SetLogLevel changes TiCDC log level dynamically.
    76  func SetLogLevel(level string) error {
    77  	oldLevel := log.GetLevel()
    78  	if strings.EqualFold(oldLevel.String(), level) {
    79  		return nil
    80  	}
    81  	var lv zapcore.Level
    82  	err := lv.UnmarshalText([]byte(level))
    83  	if err != nil {
    84  		return errors.Trace(err)
    85  	}
    86  	log.SetLevel(lv)
    87  	return nil
    88  }
    89  
    90  // loggerOp is the op for logger control
    91  type loggerOp struct {
    92  	isInitGRPCLogger   bool
    93  	isInitSaramaLogger bool
    94  	isInitMySQLLogger  bool
    95  	output             zapcore.WriteSyncer
    96  }
    97  
    98  func (op *loggerOp) applyOpts(opts []LoggerOpt) {
    99  	for _, opt := range opts {
   100  		opt(op)
   101  	}
   102  }
   103  
   104  // LoggerOpt is the logger option
   105  type LoggerOpt func(*loggerOp)
   106  
   107  // WithInitGRPCLogger enables grpc logger initialization when initializes global logger
   108  func WithInitGRPCLogger() LoggerOpt {
   109  	return func(op *loggerOp) {
   110  		op.isInitGRPCLogger = true
   111  	}
   112  }
   113  
   114  // WithInitSaramaLogger enables sarama logger initialization when initializes global logger
   115  func WithInitSaramaLogger() LoggerOpt {
   116  	return func(op *loggerOp) {
   117  		op.isInitSaramaLogger = true
   118  	}
   119  }
   120  
   121  // WithInitMySQLLogger enables mysql logger initialization when initializes global logger
   122  func WithInitMySQLLogger() LoggerOpt {
   123  	return func(op *loggerOp) {
   124  		op.isInitMySQLLogger = true
   125  	}
   126  }
   127  
   128  // WithOutputWriteSyncer will replace the WriteSyncer of global logger with customized WriteSyncer
   129  // Easy for test when using zaptest.Buffer as WriteSyncer
   130  func WithOutputWriteSyncer(output zapcore.WriteSyncer) LoggerOpt {
   131  	return func(op *loggerOp) {
   132  		op.output = output
   133  	}
   134  }
   135  
   136  // InitLogger initializes logger
   137  func InitLogger(cfg *Config, opts ...LoggerOpt) error {
   138  	var op loggerOp
   139  	op.applyOpts(opts)
   140  
   141  	pclogConfig := &log.Config{
   142  		Level: cfg.Level,
   143  		File: log.FileLogConfig{
   144  			Filename:   cfg.File,
   145  			MaxSize:    cfg.FileMaxSize,
   146  			MaxDays:    cfg.FileMaxDays,
   147  			MaxBackups: cfg.FileMaxBackups,
   148  		},
   149  		ErrorOutputPath: cfg.ZapInternalErrOutput,
   150  	}
   151  
   152  	var lg *zap.Logger
   153  	var err error
   154  	if op.output == nil {
   155  		lg, globalP, err = log.InitLogger(pclogConfig)
   156  	} else {
   157  		lg, globalP, err = log.InitLoggerWithWriteSyncer(pclogConfig, op.output, nil)
   158  	}
   159  	if err != nil {
   160  		return err
   161  	}
   162  
   163  	// Do not log stack traces at all, as we'll get the stack trace from the
   164  	// error itself.
   165  	lg = lg.WithOptions(zap.AddStacktrace(zap.DPanicLevel))
   166  	log.ReplaceGlobals(lg, globalP)
   167  
   168  	return initOptionalComponent(&op, cfg)
   169  }
   170  
   171  // initOptionalComponent initializes some optional components
   172  func initOptionalComponent(op *loggerOp, cfg *Config) error {
   173  	var level zapcore.Level
   174  	if op.isInitGRPCLogger || op.isInitSaramaLogger {
   175  		err := level.UnmarshalText([]byte(cfg.Level))
   176  		if err != nil {
   177  			return errors.Trace(err)
   178  		}
   179  	}
   180  
   181  	if op.isInitGRPCLogger {
   182  		if err := initGRPCLogger(level); err != nil {
   183  			return err
   184  		}
   185  	}
   186  
   187  	if op.isInitSaramaLogger {
   188  		if err := initSaramaLogger(level); err != nil {
   189  			return err
   190  		}
   191  	}
   192  
   193  	if op.isInitMySQLLogger {
   194  		if err := initMySQLLogger(); err != nil {
   195  			return err
   196  		}
   197  	}
   198  
   199  	return nil
   200  }
   201  
   202  // ZapErrorFilter wraps zap.Error, if err is in given filterErrors, it will be set to nil
   203  func ZapErrorFilter(err error, filterErrors ...error) zap.Field {
   204  	cause := errors.Cause(err)
   205  	for _, ferr := range filterErrors {
   206  		if cause == ferr {
   207  			return zap.Error(nil)
   208  		}
   209  	}
   210  	return zap.Error(err)
   211  }
   212  
   213  // initMySQLLogger setup logger used in mysql lib
   214  func initMySQLLogger() error {
   215  	// MySQL lib only prints error logs.
   216  	level := zapcore.ErrorLevel
   217  	logger, err := zap.NewStdLogAt(log.L().With(zap.String("component", "[mysql]")), level)
   218  	if err != nil {
   219  		return errors.Trace(err)
   220  	}
   221  	return mysql.SetLogger(logger)
   222  }
   223  
   224  // initSaramaLogger hacks logger used in sarama lib
   225  func initSaramaLogger(level zapcore.Level) error {
   226  	// only available less than info level
   227  	if !zapcore.InfoLevel.Enabled(level) {
   228  		logger, err := zap.NewStdLogAt(log.L().With(zap.String("component", "sarama")), level)
   229  		if err != nil {
   230  			return errors.Trace(err)
   231  		}
   232  		sarama.Logger = logger
   233  	}
   234  	return nil
   235  }
   236  
   237  type loggerWriter struct {
   238  	logFunc func(msg string, fields ...zap.Field)
   239  }
   240  
   241  func (l *loggerWriter) Write(p []byte) (int, error) {
   242  	p = bytes.TrimSpace(p)
   243  	l.logFunc(string(p))
   244  	return len(p), nil
   245  }
   246  
   247  func levelToFunc(logger *zap.Logger, level zapcore.Level) (func(string, ...zap.Field), error) {
   248  	switch level {
   249  	case zap.DebugLevel:
   250  		return logger.Debug, nil
   251  	case zap.InfoLevel:
   252  		return logger.Info, nil
   253  	case zap.WarnLevel:
   254  		return logger.Warn, nil
   255  	case zap.ErrorLevel:
   256  		return logger.Error, nil
   257  	case zap.DPanicLevel:
   258  		return logger.DPanic, nil
   259  	case zap.PanicLevel:
   260  		return logger.Panic, nil
   261  	case zap.FatalLevel:
   262  		return logger.Fatal, nil
   263  	}
   264  	return nil, errors.Errorf("unrecognized level: %q", level)
   265  }
   266  
   267  func initGRPCLogger(level zapcore.Level) error {
   268  	// Inherit Golang gRPC verbosity setting.
   269  	// https://github.com/grpc/grpc-go/blob/v1.26.0/grpclog/loggerv2.go#L134-L138
   270  	var v int
   271  	vLevel := os.Getenv("GRPC_GO_LOG_VERBOSITY_LEVEL")
   272  	if vl, err := strconv.Atoi(vLevel); err == nil {
   273  		v = vl
   274  	}
   275  	if v <= 0 && level.Enabled(zapcore.ErrorLevel) {
   276  		// Sometimes gRPC log is very verbose.
   277  		level = zapcore.ErrorLevel
   278  		return nil
   279  	}
   280  
   281  	logger := log.L().With(zap.String("component", "grpc"))
   282  	// For gRPC 1.26.0, logging call stack:
   283  	//
   284  	// github.com/pingcap/tiflow/pkg/util.levelToFunc.func1
   285  	// github.com/pingcap/tiflow/pkg/util.(*grpcLoggerWriter).Write
   286  	// log.(*Logger).Output
   287  	// log.(*Logger).Printf
   288  	// google.golang.org/grpc/grpclog.(*loggerT).Infof
   289  	// google.golang.org/grpc/grpclog.Infof(...)
   290  	// Caller
   291  	logger = logger.WithOptions(zap.AddCallerSkip(5))
   292  	logFunc, err := levelToFunc(logger, level)
   293  	if err != nil {
   294  		return err
   295  	}
   296  	writer := &loggerWriter{logFunc: logFunc}
   297  	grpclog.SetLoggerV2(grpclog.NewLoggerV2WithVerbosity(writer, writer, writer, v))
   298  	return nil
   299  }
   300  
   301  // ErrorFilterContextCanceled log the msg and fields but do nothing if fields have cancel error
   302  func ErrorFilterContextCanceled(logger *zap.Logger, msg string, fields ...zap.Field) {
   303  	for _, field := range fields {
   304  		switch field.Type {
   305  		case zapcore.StringType:
   306  			if field.Key == "error" && strings.Contains(field.String, context.Canceled.Error()) {
   307  				return
   308  			}
   309  		case zapcore.ErrorType:
   310  			err, ok := field.Interface.(error)
   311  			if ok && errors.Cause(err) == context.Canceled {
   312  				return
   313  			}
   314  		}
   315  	}
   316  
   317  	logger.WithOptions(zap.AddCallerSkip(1)).Error(msg, fields...)
   318  }
   319  
   320  // ShortError contructs a field which only records the error message without the
   321  // verbose text (i.e. excludes the stack trace).
   322  func ShortError(err error) zap.Field {
   323  	if err == nil {
   324  		return zap.Skip()
   325  	}
   326  	return zap.String("error", err.Error())
   327  }
   328  
   329  // WithComponent return a logger with specified component scope
   330  func WithComponent(component string) *zap.Logger {
   331  	return log.L().With(zap.String("component", component))
   332  }
   333  
   334  // InitGinLogWritter initialize loggers for Gin.
   335  func InitGinLogWritter() io.Writer {
   336  	currentLevel := log.GetLevel()
   337  	if currentLevel == zapcore.DebugLevel {
   338  		gin.SetMode(gin.DebugMode)
   339  	} else {
   340  		gin.SetMode(gin.ReleaseMode)
   341  	}
   342  	logger := WithComponent("gin")
   343  	logFunc, _ := levelToFunc(logger, currentLevel)
   344  	gin.DefaultWriter = &loggerWriter{logFunc: logFunc}
   345  	logFunc, _ = levelToFunc(logger, zapcore.ErrorLevel)
   346  	gin.DefaultErrorWriter = &loggerWriter{logFunc: logFunc}
   347  	return gin.DefaultErrorWriter
   348  }