| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534 |
- 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
- // OpsSystemLogSkipField keeps an event in the standard logger while
- // preventing the database-backed Ops system-log sink from indexing it.
- OpsSystemLogSkipField = "ops_system_log_skip"
- )
- 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 // sinkState
- 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
- }
- // WriteSinkEvent 直接写入日志 sink,不经过全局日志级别门控。
- // 用于需要“可观测性入库”与“业务输出级别”解耦的场景(例如 ops 系统日志索引)。
- 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 {
- // Delegate to inner core (tee) so each sub-core's level enabler is respected.
- // Then add ourselves for sink forwarding only.
- 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 {
- // Only handle sink forwarding — the inner cores write via their own
- // Write methods (added to CheckedEntry by s.core.Check above).
- 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
- }
- // LegacyPrintf 用于平滑迁移历史的 printf 风格日志到结构化 logger。
- func LegacyPrintf(component, format string, args ...any) {
- msg := normalizeStdLogMessage(fmt.Sprintf(format, args...))
- if msg == "" {
- return
- }
- initialized := global.Load() != nil
- if !initialized {
- // 在日志系统未初始化前,回退到标准库 log,避免测试/工具链丢日志。
- 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()
- }
|