github.com/jxskiss/gopkg/v2@v2.14.9-0.20240514120614-899f3e7952b4/zlog/context.go (about)

     1  package zlog
     2  
     3  import (
     4  	"context"
     5  
     6  	"go.uber.org/zap"
     7  	"go.uber.org/zap/zapcore"
     8  )
     9  
    10  type ctxKey int
    11  
    12  const (
    13  	fieldsKey ctxKey = iota
    14  	loggerKey
    15  )
    16  
    17  // CtxHandler customizes a logger's behavior at runtime dynamically.
    18  type CtxHandler struct {
    19  
    20  	// ChangeLevel returns a non-nil Level if it wants to change
    21  	// the logger's logging level according to ctx.
    22  	// It returns nil to keep the logger's logging level as-is.
    23  	ChangeLevel func(ctx context.Context) *Level
    24  
    25  	// WithCtx is called by Logger.Ctx, SugaredLogger.Ctx and the global
    26  	// function WithCtx to check ctx for context-aware information.
    27  	// It returns CtxResult to customize the logger's behavior.
    28  	WithCtx func(ctx context.Context) CtxResult
    29  }
    30  
    31  // CtxResult holds information get from a context.
    32  type CtxResult struct {
    33  	// Non-nil Level changes the logger's logging level.
    34  	Level *Level
    35  
    36  	// Fields will be added to the logger as additional fields.
    37  	Fields []zap.Field
    38  }
    39  
    40  // AddFields add logging fields to ctx which can be retrieved by
    41  // GetFields, WithCtx.
    42  // Duplicate fields override the old ones in ctx.
    43  func AddFields(ctx context.Context, fields ...zap.Field) context.Context {
    44  	if len(fields) == 0 {
    45  		return ctx
    46  	}
    47  	old, ok := ctx.Value(fieldsKey).([]zap.Field)
    48  	if ok {
    49  		fields = appendFields(old, fields)
    50  	}
    51  	return context.WithValue(ctx, fieldsKey, fields)
    52  }
    53  
    54  // GetFields returns the logging fields added to ctx by AddFields.
    55  func GetFields(ctx context.Context) []zap.Field {
    56  	fs, ok := ctx.Value(fieldsKey).([]zap.Field)
    57  	if ok {
    58  		return fs[:len(fs):len(fs)] // clip
    59  	}
    60  	return nil
    61  }
    62  
    63  // WithLogger returns a new context.Context with logger attached,
    64  // which can be retrieved by WithCtx.
    65  func WithLogger[T Logger | SugaredLogger | *zap.Logger | *zap.SugaredLogger](
    66  	ctx context.Context, logger T) context.Context {
    67  	ctx = context.WithValue(ctx, loggerKey, logger)
    68  	return ctx
    69  }
    70  
    71  func getLoggerFromCtx(ctx context.Context) any {
    72  	return ctx.Value(loggerKey)
    73  }
    74  
    75  // Ctx creates a child logger, it calls CtxHandler.WithCtx to get CtxResult
    76  // from ctx, adds CtxResult.Fields to the child logger and changes
    77  // the logger's level to CtxResult.Level, if it is not nil.
    78  func (l Logger) Ctx(ctx context.Context, extra ...zap.Field) Logger {
    79  	if ctx == nil {
    80  		return l.With(extra...)
    81  	}
    82  	var fields []zap.Field
    83  	logger := l
    84  	ctxFunc := globals.Props.cfg.CtxHandler.WithCtx
    85  	if ctxFunc != nil {
    86  		ctxResult := ctxFunc(ctx)
    87  		fields = ctxResult.Fields
    88  		if ctxResult.Level != nil {
    89  			logger = logger.WithOptions(zap.WrapCore(changeLevel(*ctxResult.Level)))
    90  		}
    91  	}
    92  	fields = appendFields(fields, GetFields(ctx))
    93  	fields = appendFields(fields, extra)
    94  	if len(fields) > 0 {
    95  		logger = logger.With(fields...)
    96  	}
    97  	return logger
    98  }
    99  
   100  // Ctx creates a child logger, it calls CtxHandler.WithCtx to get CtxResult
   101  // from ctx, adds CtxResult.Fields to the child logger and changes
   102  // the logger's level to CtxResult.Level, if it is not nil.
   103  func (s SugaredLogger) Ctx(ctx context.Context, extra ...zap.Field) SugaredLogger {
   104  	ctxFunc := globals.Props.cfg.CtxHandler.WithCtx
   105  	if (ctx == nil || ctxFunc == nil) && len(extra) == 0 {
   106  		return s
   107  	}
   108  	return s.Desugar().Ctx(ctx, extra...).Sugar()
   109  }
   110  
   111  // WithCtx creates a child logger and customizes its behavior using context
   112  // data (e.g. adding fields, dynamically changing level, etc.)
   113  //
   114  // If ctx is created by WithLogger, it carries a logger instance,
   115  // this function uses that logger as a base to create the child logger,
   116  // else it calls Logger.Ctx to build the child logger with contextual fields
   117  // and optional dynamic level from ctx.
   118  //
   119  // Also see WithLogger, CtxHandler and CtxResult for more details.
   120  func WithCtx(ctx context.Context, extra ...zap.Field) Logger {
   121  	if ctx == nil {
   122  		return With(extra...)
   123  	}
   124  	if lg := getLoggerFromCtx(ctx); lg != nil {
   125  		var logger Logger
   126  		switch x := lg.(type) {
   127  		case Logger:
   128  			logger = x
   129  		case SugaredLogger:
   130  			logger = x.Desugar()
   131  		case *zap.Logger:
   132  			logger = Logger{Logger: x}
   133  		case *zap.SugaredLogger:
   134  			logger = Logger{Logger: x.Desugar()}
   135  		}
   136  		if logger.Logger != nil {
   137  			return logger.With(extra...)
   138  		}
   139  	}
   140  	return L().Ctx(ctx, extra...)
   141  }
   142  
   143  //nolint:predeclared
   144  func appendFields(old []zap.Field, new []zap.Field) []zap.Field {
   145  	if len(new) == 0 {
   146  		return old
   147  	}
   148  	result := make([]zap.Field, len(old), len(old)+len(new))
   149  	copy(result, old)
   150  
   151  	// check namespace
   152  	nsIdx := 0
   153  	for i := len(result) - 1; i >= 0; i-- {
   154  		if result[i].Type == zapcore.NamespaceType {
   155  			nsIdx = i + 1
   156  			break
   157  		}
   158  	}
   159  
   160  	var hasNewNamespace bool
   161  loop:
   162  	for _, f := range new {
   163  		if !hasNewNamespace {
   164  			if f.Type == zapcore.NamespaceType {
   165  				hasNewNamespace = true
   166  				result = append(result, f)
   167  				continue loop
   168  			}
   169  			for i := nsIdx; i < len(result); i++ {
   170  				if result[i].Key == f.Key {
   171  					result[i] = f
   172  					continue loop
   173  				}
   174  			}
   175  		}
   176  		result = append(result, f)
   177  	}
   178  	return result
   179  }