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)) } 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() }