374 lines
7.9 KiB
Go
374 lines
7.9 KiB
Go
package logger
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log"
|
|
"log/slog"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"go.uber.org/zap"
|
|
"go.uber.org/zap/zapcore"
|
|
"gopkg.in/natefinch/lumberjack.v2"
|
|
)
|
|
|
|
type Level = zapcore.Level
|
|
|
|
const (
|
|
LevelDebug = zapcore.DebugLevel
|
|
LevelInfo = zapcore.InfoLevel
|
|
LevelWarn = zapcore.WarnLevel
|
|
LevelError = zapcore.ErrorLevel
|
|
LevelFatal = zapcore.FatalLevel
|
|
)
|
|
|
|
type Sink interface {
|
|
WriteLogEvent(event *LogEvent)
|
|
}
|
|
|
|
type LogEvent struct {
|
|
Time time.Time
|
|
Level string
|
|
Component string
|
|
Message string
|
|
LoggerName string
|
|
Fields map[string]any
|
|
}
|
|
|
|
var (
|
|
mu sync.RWMutex
|
|
global *zap.Logger
|
|
sugar *zap.SugaredLogger
|
|
atomicLevel zap.AtomicLevel
|
|
initOptions InitOptions
|
|
currentSink Sink
|
|
stdLogUndo func()
|
|
bootstrapOnce sync.Once
|
|
)
|
|
|
|
func InitBootstrap() {
|
|
bootstrapOnce.Do(func() {
|
|
if err := Init(bootstrapOptions()); err != nil {
|
|
_, _ = fmt.Fprintf(os.Stderr, "logger bootstrap init failed: %v\n", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func Init(options InitOptions) error {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
return initLocked(options)
|
|
}
|
|
|
|
func initLocked(options InitOptions) error {
|
|
normalized := options.normalized()
|
|
zl, al, err := buildLogger(normalized)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
prev := global
|
|
global = zl
|
|
sugar = zl.Sugar()
|
|
atomicLevel = al
|
|
initOptions = normalized
|
|
|
|
bridgeStdLogLocked()
|
|
bridgeSlogLocked()
|
|
|
|
if prev != nil {
|
|
_ = prev.Sync()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func Reconfigure(mutator func(*InitOptions) error) error {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
next := initOptions
|
|
if mutator != nil {
|
|
if err := mutator(&next); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return initLocked(next)
|
|
}
|
|
|
|
func SetLevel(level string) error {
|
|
lv, ok := parseLevel(level)
|
|
if !ok {
|
|
return fmt.Errorf("invalid log level: %s", level)
|
|
}
|
|
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
atomicLevel.SetLevel(lv)
|
|
initOptions.Level = strings.ToLower(strings.TrimSpace(level))
|
|
return nil
|
|
}
|
|
|
|
func CurrentLevel() string {
|
|
mu.RLock()
|
|
defer mu.RUnlock()
|
|
if global == nil {
|
|
return "info"
|
|
}
|
|
return atomicLevel.Level().String()
|
|
}
|
|
|
|
func SetSink(sink Sink) {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
currentSink = sink
|
|
}
|
|
|
|
func L() *zap.Logger {
|
|
mu.RLock()
|
|
defer mu.RUnlock()
|
|
if global != nil {
|
|
return global
|
|
}
|
|
return zap.NewNop()
|
|
}
|
|
|
|
func S() *zap.SugaredLogger {
|
|
mu.RLock()
|
|
defer mu.RUnlock()
|
|
if sugar != nil {
|
|
return sugar
|
|
}
|
|
return zap.NewNop().Sugar()
|
|
}
|
|
|
|
func With(fields ...zap.Field) *zap.Logger {
|
|
return L().With(fields...)
|
|
}
|
|
|
|
func Sync() {
|
|
mu.RLock()
|
|
l := global
|
|
mu.RUnlock()
|
|
if l != nil {
|
|
_ = l.Sync()
|
|
}
|
|
}
|
|
|
|
func bridgeStdLogLocked() {
|
|
if stdLogUndo != nil {
|
|
stdLogUndo()
|
|
stdLogUndo = nil
|
|
}
|
|
|
|
log.SetFlags(0)
|
|
log.SetPrefix("")
|
|
undo, err := zap.RedirectStdLogAt(global.Named("stdlog"), zap.InfoLevel)
|
|
if err != nil {
|
|
_, _ = fmt.Fprintf(os.Stderr, "logger redirect stdlog failed: %v\n", err)
|
|
return
|
|
}
|
|
stdLogUndo = undo
|
|
}
|
|
|
|
func bridgeSlogLocked() {
|
|
slog.SetDefault(slog.New(newSlogZapHandler(global.Named("slog"))))
|
|
}
|
|
|
|
func buildLogger(options InitOptions) (*zap.Logger, zap.AtomicLevel, error) {
|
|
level, _ := parseLevel(options.Level)
|
|
atomic := zap.NewAtomicLevelAt(level)
|
|
|
|
encoderCfg := zapcore.EncoderConfig{
|
|
TimeKey: "time",
|
|
LevelKey: "level",
|
|
NameKey: "logger",
|
|
CallerKey: "caller",
|
|
MessageKey: "msg",
|
|
StacktraceKey: "stacktrace",
|
|
LineEnding: zapcore.DefaultLineEnding,
|
|
EncodeLevel: zapcore.CapitalLevelEncoder,
|
|
EncodeTime: zapcore.ISO8601TimeEncoder,
|
|
EncodeDuration: zapcore.MillisDurationEncoder,
|
|
EncodeCaller: zapcore.ShortCallerEncoder,
|
|
}
|
|
|
|
var enc zapcore.Encoder
|
|
if options.Format == "console" {
|
|
enc = zapcore.NewConsoleEncoder(encoderCfg)
|
|
} else {
|
|
enc = zapcore.NewJSONEncoder(encoderCfg)
|
|
}
|
|
|
|
sinkCore := newSinkCore()
|
|
cores := make([]zapcore.Core, 0, 3)
|
|
|
|
if options.Output.ToStdout {
|
|
infoPriority := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool {
|
|
return lvl >= atomic.Level() && lvl < zapcore.WarnLevel
|
|
})
|
|
errPriority := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool {
|
|
return lvl >= atomic.Level() && lvl >= zapcore.WarnLevel
|
|
})
|
|
cores = append(cores, zapcore.NewCore(enc, zapcore.Lock(os.Stdout), infoPriority))
|
|
cores = append(cores, zapcore.NewCore(enc, zapcore.Lock(os.Stderr), errPriority))
|
|
}
|
|
|
|
if options.Output.ToFile {
|
|
fileCore, filePath, fileErr := buildFileCore(enc, atomic, options)
|
|
if fileErr != nil {
|
|
_, _ = fmt.Fprintf(os.Stderr, "time=%s level=WARN msg=\"日志文件输出初始化失败,降级为仅标准输出\" path=%s err=%v\n",
|
|
time.Now().Format(time.RFC3339Nano),
|
|
filePath,
|
|
fileErr,
|
|
)
|
|
} else {
|
|
cores = append(cores, fileCore)
|
|
}
|
|
}
|
|
|
|
if len(cores) == 0 {
|
|
cores = append(cores, zapcore.NewCore(enc, zapcore.Lock(os.Stdout), atomic))
|
|
}
|
|
|
|
core := zapcore.NewTee(cores...)
|
|
if options.Sampling.Enabled {
|
|
core = zapcore.NewSamplerWithOptions(core, samplingTick(), options.Sampling.Initial, options.Sampling.Thereafter)
|
|
}
|
|
core = sinkCore.Wrap(core)
|
|
|
|
stacktraceLevel, _ := parseStacktraceLevel(options.StacktraceLevel)
|
|
zapOpts := make([]zap.Option, 0, 5)
|
|
if options.Caller {
|
|
zapOpts = append(zapOpts, zap.AddCaller())
|
|
}
|
|
if stacktraceLevel <= zapcore.FatalLevel {
|
|
zapOpts = append(zapOpts, zap.AddStacktrace(stacktraceLevel))
|
|
}
|
|
zapOpts = append(zapOpts, zap.AddCallerSkip(1))
|
|
|
|
logger := zap.New(core, zapOpts...).With(
|
|
zap.String("service", options.ServiceName),
|
|
zap.String("env", options.Environment),
|
|
)
|
|
return logger, atomic, nil
|
|
}
|
|
|
|
func buildFileCore(enc zapcore.Encoder, atomic zap.AtomicLevel, options InitOptions) (zapcore.Core, string, error) {
|
|
filePath := options.Output.FilePath
|
|
if strings.TrimSpace(filePath) == "" {
|
|
filePath = resolveLogFilePath("")
|
|
}
|
|
|
|
dir := filepath.Dir(filePath)
|
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
|
return nil, filePath, err
|
|
}
|
|
lj := &lumberjack.Logger{
|
|
Filename: filePath,
|
|
MaxSize: options.Rotation.MaxSizeMB,
|
|
MaxBackups: options.Rotation.MaxBackups,
|
|
MaxAge: options.Rotation.MaxAgeDays,
|
|
Compress: options.Rotation.Compress,
|
|
LocalTime: options.Rotation.LocalTime,
|
|
}
|
|
return zapcore.NewCore(enc, zapcore.AddSync(lj), atomic), filePath, nil
|
|
}
|
|
|
|
type sinkCore struct {
|
|
core zapcore.Core
|
|
fields []zapcore.Field
|
|
}
|
|
|
|
func newSinkCore() *sinkCore {
|
|
return &sinkCore{}
|
|
}
|
|
|
|
func (s *sinkCore) Wrap(core zapcore.Core) zapcore.Core {
|
|
cp := *s
|
|
cp.core = core
|
|
return &cp
|
|
}
|
|
|
|
func (s *sinkCore) Enabled(level zapcore.Level) bool {
|
|
return s.core.Enabled(level)
|
|
}
|
|
|
|
func (s *sinkCore) With(fields []zapcore.Field) zapcore.Core {
|
|
nextFields := append([]zapcore.Field{}, s.fields...)
|
|
nextFields = append(nextFields, fields...)
|
|
return &sinkCore{
|
|
core: s.core.With(fields),
|
|
fields: nextFields,
|
|
}
|
|
}
|
|
|
|
func (s *sinkCore) Check(entry zapcore.Entry, ce *zapcore.CheckedEntry) *zapcore.CheckedEntry {
|
|
if s.Enabled(entry.Level) {
|
|
return ce.AddCore(entry, s)
|
|
}
|
|
return ce
|
|
}
|
|
|
|
func (s *sinkCore) Write(entry zapcore.Entry, fields []zapcore.Field) error {
|
|
if err := s.core.Write(entry, fields); err != nil {
|
|
return err
|
|
}
|
|
|
|
mu.RLock()
|
|
sink := currentSink
|
|
mu.RUnlock()
|
|
if sink == nil {
|
|
return nil
|
|
}
|
|
|
|
enc := zapcore.NewMapObjectEncoder()
|
|
for _, f := range s.fields {
|
|
f.AddTo(enc)
|
|
}
|
|
for _, f := range fields {
|
|
f.AddTo(enc)
|
|
}
|
|
|
|
event := &LogEvent{
|
|
Time: entry.Time,
|
|
Level: strings.ToLower(entry.Level.String()),
|
|
Component: entry.LoggerName,
|
|
Message: entry.Message,
|
|
LoggerName: entry.LoggerName,
|
|
Fields: enc.Fields,
|
|
}
|
|
sink.WriteLogEvent(event)
|
|
return nil
|
|
}
|
|
|
|
func (s *sinkCore) Sync() error {
|
|
return s.core.Sync()
|
|
}
|
|
|
|
type contextKey string
|
|
|
|
const loggerContextKey contextKey = "ctx_logger"
|
|
|
|
func IntoContext(ctx context.Context, l *zap.Logger) context.Context {
|
|
if ctx == nil {
|
|
ctx = context.Background()
|
|
}
|
|
if l == nil {
|
|
l = L()
|
|
}
|
|
return context.WithValue(ctx, loggerContextKey, l)
|
|
}
|
|
|
|
func FromContext(ctx context.Context) *zap.Logger {
|
|
if ctx == nil {
|
|
return L()
|
|
}
|
|
if l, ok := ctx.Value(loggerContextKey).(*zap.Logger); ok && l != nil {
|
|
return l
|
|
}
|
|
return L()
|
|
}
|