| package logger |
|
|
| import ( |
| "context" |
| "fmt" |
| "io" |
| "log" |
| "log/slog" |
| "os" |
| "path/filepath" |
| "strings" |
| "sync" |
| "sync/atomic" |
| "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 atomic.Pointer[zap.Logger] |
| sugar atomic.Pointer[zap.SugaredLogger] |
| atomicLevel zap.AtomicLevel |
| initOptions InitOptions |
| currentSink atomic.Value |
| stdLogUndo func() |
| bootstrapOnce sync.Once |
| ) |
|
|
| type sinkState struct { |
| sink Sink |
| } |
|
|
| 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.Load() |
| global.Store(zl) |
| sugar.Store(zl.Sugar()) |
| atomicLevel = al |
| initOptions = normalized |
|
|
| bridgeSlogLocked() |
| bridgeStdLogLocked() |
|
|
| 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.Load() == nil { |
| return "info" |
| } |
| return atomicLevel.Level().String() |
| } |
|
|
| func SetSink(sink Sink) { |
| currentSink.Store(sinkState{sink: sink}) |
| } |
|
|
| func loadSink() Sink { |
| v := currentSink.Load() |
| if v == nil { |
| return nil |
| } |
| state, ok := v.(sinkState) |
| if !ok { |
| return nil |
| } |
| return state.sink |
| } |
|
|
| |
| |
| func WriteSinkEvent(level, component, message string, fields map[string]any) { |
| sink := loadSink() |
| if sink == nil { |
| return |
| } |
|
|
| level = strings.ToLower(strings.TrimSpace(level)) |
| if level == "" { |
| level = "info" |
| } |
| component = strings.TrimSpace(component) |
| message = strings.TrimSpace(message) |
| if message == "" { |
| return |
| } |
|
|
| eventFields := make(map[string]any, len(fields)+1) |
| for k, v := range fields { |
| eventFields[k] = v |
| } |
| if component != "" { |
| if _, ok := eventFields["component"]; !ok { |
| eventFields["component"] = component |
| } |
| } |
|
|
| sink.WriteLogEvent(&LogEvent{ |
| Time: time.Now(), |
| Level: level, |
| Component: component, |
| Message: message, |
| LoggerName: component, |
| Fields: eventFields, |
| }) |
| } |
|
|
| func L() *zap.Logger { |
| if l := global.Load(); l != nil { |
| return l |
| } |
| return zap.NewNop() |
| } |
|
|
| func S() *zap.SugaredLogger { |
| if s := sugar.Load(); s != nil { |
| return s |
| } |
| return zap.NewNop().Sugar() |
| } |
|
|
| func With(fields ...zap.Field) *zap.Logger { |
| return L().With(fields...) |
| } |
|
|
| func Sync() { |
| l := global.Load() |
| if l != nil { |
| _ = l.Sync() |
| } |
| } |
|
|
| func bridgeStdLogLocked() { |
| if stdLogUndo != nil { |
| stdLogUndo() |
| stdLogUndo = nil |
| } |
|
|
| prevFlags := log.Flags() |
| prevPrefix := log.Prefix() |
| prevWriter := log.Writer() |
|
|
| log.SetFlags(0) |
| log.SetPrefix("") |
| base := global.Load() |
| if base == nil { |
| base = zap.NewNop() |
| } |
| log.SetOutput(newStdLogBridge(base.Named("stdlog"))) |
|
|
| stdLogUndo = func() { |
| log.SetOutput(prevWriter) |
| log.SetFlags(prevFlags) |
| log.SetPrefix(prevPrefix) |
| } |
| } |
|
|
| func bridgeSlogLocked() { |
| base := global.Load() |
| if base == nil { |
| base = zap.NewNop() |
| } |
| slog.SetDefault(slog.New(newSlogZapHandler(base.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 { |
| |
| |
| ce = s.core.Check(entry, ce) |
| if ce != nil { |
| ce = ce.AddCore(entry, s) |
| } |
| return ce |
| } |
|
|
| func (s *sinkCore) Write(entry zapcore.Entry, fields []zapcore.Field) error { |
| |
| |
| sink := loadSink() |
| 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 stdLogBridge struct { |
| logger *zap.Logger |
| } |
|
|
| func newStdLogBridge(l *zap.Logger) io.Writer { |
| if l == nil { |
| l = zap.NewNop() |
| } |
| return &stdLogBridge{logger: l} |
| } |
|
|
| func (b *stdLogBridge) Write(p []byte) (int, error) { |
| msg := normalizeStdLogMessage(string(p)) |
| if msg == "" { |
| return len(p), nil |
| } |
|
|
| level := inferStdLogLevel(msg) |
| entry := b.logger.WithOptions(zap.AddCallerSkip(4)) |
|
|
| switch level { |
| case LevelDebug: |
| entry.Debug(msg, zap.Bool("legacy_stdlog", true)) |
| case LevelWarn: |
| entry.Warn(msg, zap.Bool("legacy_stdlog", true)) |
| case LevelError, LevelFatal: |
| entry.Error(msg, zap.Bool("legacy_stdlog", true)) |
| default: |
| entry.Info(msg, zap.Bool("legacy_stdlog", true)) |
| } |
| return len(p), nil |
| } |
|
|
| func normalizeStdLogMessage(raw string) string { |
| msg := strings.TrimSpace(strings.ReplaceAll(raw, "\n", " ")) |
| if msg == "" { |
| return "" |
| } |
| return strings.Join(strings.Fields(msg), " ") |
| } |
|
|
| func inferStdLogLevel(msg string) Level { |
| lower := strings.ToLower(strings.TrimSpace(msg)) |
| if lower == "" { |
| return LevelInfo |
| } |
|
|
| if strings.HasPrefix(lower, "[debug]") || strings.HasPrefix(lower, "debug:") { |
| return LevelDebug |
| } |
| if strings.HasPrefix(lower, "[warn]") || strings.HasPrefix(lower, "[warning]") || strings.HasPrefix(lower, "warn:") || strings.HasPrefix(lower, "warning:") { |
| return LevelWarn |
| } |
| if strings.HasPrefix(lower, "[error]") || strings.HasPrefix(lower, "error:") || strings.HasPrefix(lower, "fatal:") || strings.HasPrefix(lower, "panic:") { |
| return LevelError |
| } |
|
|
| if strings.Contains(lower, " failed") || strings.Contains(lower, "error") || strings.Contains(lower, "panic") || strings.Contains(lower, "fatal") { |
| return LevelError |
| } |
| if strings.Contains(lower, "warning") || strings.Contains(lower, "warn") || strings.Contains(lower, " queue full") || strings.Contains(lower, "fallback") { |
| return LevelWarn |
| } |
| return LevelInfo |
| } |
|
|
| |
| func LegacyPrintf(component, format string, args ...any) { |
| msg := normalizeStdLogMessage(fmt.Sprintf(format, args...)) |
| if msg == "" { |
| return |
| } |
|
|
| initialized := global.Load() != nil |
| if !initialized { |
| |
| log.Print(msg) |
| return |
| } |
|
|
| l := L() |
| if component != "" { |
| l = l.With(zap.String("component", component)) |
| } |
| l = l.WithOptions(zap.AddCallerSkip(1)) |
|
|
| switch inferStdLogLevel(msg) { |
| case LevelDebug: |
| l.Debug(msg, zap.Bool("legacy_printf", true)) |
| case LevelWarn: |
| l.Warn(msg, zap.Bool("legacy_printf", true)) |
| case LevelError, LevelFatal: |
| l.Error(msg, zap.Bool("legacy_printf", true)) |
| default: |
| l.Info(msg, zap.Bool("legacy_printf", true)) |
| } |
| } |
|
|
| 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() |
| } |
|
|