538 lines
17 KiB
Go
538 lines
17 KiB
Go
package logger
|
||
|
||
import (
|
||
"errors"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
"sync"
|
||
"sync/atomic"
|
||
|
||
"github.com/EthanCodeCraft/xlgo-core/config"
|
||
|
||
"go.uber.org/zap"
|
||
"go.uber.org/zap/zapcore"
|
||
"gopkg.in/natefinch/lumberjack.v2"
|
||
)
|
||
|
||
var (
|
||
// 内部 atomic 存储——四个 logger 的真实源(H7 修复)。
|
||
// 所有包级函数(Info/Debug/.../APILog/DBLog/Sync)经 atomic Load 读取,
|
||
// 使请求 goroutine 不与 Init/Close 重新装配全局变量竞争。
|
||
// 原实现用 m.mu(实例锁)保护包级全局变量,锁与被保护对象作用域错配,
|
||
// 读侧无锁裸读 → 热重载 re-Init/Close 与请求日志存在数据竞争。
|
||
loggerPtr atomic.Pointer[zap.Logger]
|
||
sugarPtr atomic.Pointer[zap.SugaredLogger]
|
||
apiLogPtr atomic.Pointer[zap.Logger]
|
||
dbLogPtr atomic.Pointer[zap.Logger]
|
||
|
||
// defaultLoggerPtr 是包级 facade 使用的默认 manager 原子快照。
|
||
// 保留导出的 DefaultLogger *LogManager 作为兼容变量;SetDefaultLogManager
|
||
// 只替换这个内部指针,避免 facade 裸读/裸写导出变量。
|
||
defaultLoggerPtr atomic.Pointer[LogManager]
|
||
globalLogGeneration uint64
|
||
|
||
// globalMu 串行化包级 logger/writer 的发布与关闭。
|
||
// 不能仅依赖 LogManager.mu:多个 LogManager 实例并发 Init/Close 时,
|
||
// 实例锁无法保护 Logger/fileWriters 这类包级状态。
|
||
globalMu sync.Mutex
|
||
|
||
// Logger 全局通用日志实例(兼容别名)。Init 之前为 Nop,调用安全。
|
||
//
|
||
// Deprecated: 此导出变量由 Init/Close 在 globalMu 下同步维护,但直接读它在 re-Init/Close
|
||
// 期间非并发安全(L-E)。请改用包级函数 Info/Debug/Warn/Error/...(经 atomic 读取,
|
||
// 并发安全)。保留仅为向后兼容,未来大版本可能移除。
|
||
Logger = zap.NewNop()
|
||
|
||
// fileWriters 持有所有 lumberjack 实例引用,
|
||
// Close() 调用时显式释放文件句柄。
|
||
// 必要性:lumberjack 不依赖 GC 关闭文件,进程长跑或测试场景下
|
||
// 不显式关闭会持有句柄导致 Windows 上无法删除日志目录。
|
||
// 仅在 globalMu 下访问(Init/Close),无读侧竞争。
|
||
fileWriters []*lumberjack.Logger
|
||
)
|
||
|
||
func init() {
|
||
// 初始化 atomic 存储为 Nop,保证包级函数在任何时刻 Load 均非 nil。
|
||
nop := zap.NewNop()
|
||
loggerPtr.Store(nop)
|
||
sugarPtr.Store(nop.Sugar())
|
||
apiLogPtr.Store(nop)
|
||
dbLogPtr.Store(nop)
|
||
defaultLoggerPtr.Store(DefaultLogger)
|
||
}
|
||
|
||
// currentLogger 返回通用 logger 的 atomic 快照(永不 nil)。
|
||
func currentLogger() *zap.Logger {
|
||
if l := loggerPtr.Load(); l != nil {
|
||
return l
|
||
}
|
||
return zap.NewNop() // 防御:init 后不可达
|
||
}
|
||
|
||
// currentSugar 返回 sugared logger 的 atomic 快照(永不 nil)。
|
||
func currentSugar() *zap.SugaredLogger {
|
||
if s := sugarPtr.Load(); s != nil {
|
||
return s
|
||
}
|
||
return zap.NewNop().Sugar() // 防御:init 后不可达
|
||
}
|
||
|
||
// currentAPILog 返回 API logger 的 atomic 快照(永不 nil)。
|
||
func currentAPILog() *zap.Logger {
|
||
if l := apiLogPtr.Load(); l != nil {
|
||
return l
|
||
}
|
||
return zap.NewNop()
|
||
}
|
||
|
||
// currentDBLog 返回 DB logger 的 atomic 快照(永不 nil)。
|
||
func currentDBLog() *zap.Logger {
|
||
if l := dbLogPtr.Load(); l != nil {
|
||
return l
|
||
}
|
||
return zap.NewNop()
|
||
}
|
||
|
||
// LogManager 日志管理器(#10)。照 database.Manager 模式:
|
||
// 实例化 + DefaultLogger 全局默认 + 包级 facade 代理。
|
||
// 包级 Logger/sugar/apiLog/dbLog 由 Init 同步维护,下游 8 个包零改动。
|
||
type LogManager struct {
|
||
mu sync.Mutex
|
||
cfg *config.Config
|
||
// level 是所有 core 共享的 AtomicLevel,支持运行期 SetLevel 热切换(M19)。
|
||
// Init 前为 nil,SetLevel 守卫之。
|
||
level zap.AtomicLevel
|
||
generation uint64
|
||
}
|
||
|
||
// DefaultSnapshot 是默认 logger 全局状态的 opaque 快照。
|
||
// App 初始化用它实现“先安装新 logger,后续失败可恢复旧 logger”的事务边界。
|
||
type DefaultSnapshot struct {
|
||
manager *LogManager
|
||
logger *zap.Logger
|
||
sugar *zap.SugaredLogger
|
||
apiLog *zap.Logger
|
||
dbLog *zap.Logger
|
||
writers []*lumberjack.Logger
|
||
generation uint64
|
||
}
|
||
|
||
// DefaultLogger 默认日志管理器,包级 facade 代理到它。
|
||
//
|
||
// Deprecated: 直接替换该导出变量(logger.DefaultLogger = m)不会更新包级 facade
|
||
// 使用的 atomic 快照,也不是并发安全的替换方式。请用 SetDefaultLogManager(m)。
|
||
// 直接调用 DefaultLogger.Init/Close/SetLevel/GetLevel 仍保留兼容,并受 LogManager
|
||
// 自身锁保护。
|
||
var DefaultLogger = NewLogManager()
|
||
|
||
// NewLogManager 创建日志管理器实例。
|
||
func NewLogManager() *LogManager { return &LogManager{} }
|
||
|
||
// GetDefaultLogManager 返回全局默认 LogManager(atomic 读取,并发安全)。
|
||
func GetDefaultLogManager() *LogManager {
|
||
if m := defaultLoggerPtr.Load(); m != nil {
|
||
return m
|
||
}
|
||
if DefaultLogger != nil && defaultLoggerPtr.CompareAndSwap(nil, DefaultLogger) {
|
||
return DefaultLogger
|
||
}
|
||
m := NewLogManager()
|
||
if defaultLoggerPtr.CompareAndSwap(nil, m) {
|
||
return m
|
||
}
|
||
return defaultLoggerPtr.Load()
|
||
}
|
||
|
||
// SetLevel 运行期热切换日志级别(M19)。需先 Init;未 Init 时返回 false。
|
||
// 影响所有 core(app/api/db/console)——它们共享同一 AtomicLevel。
|
||
func (m *LogManager) SetLevel(l zapcore.Level) bool {
|
||
m.mu.Lock()
|
||
defer m.mu.Unlock()
|
||
if m.level == (zap.AtomicLevel{}) {
|
||
return false
|
||
}
|
||
m.level.SetLevel(l)
|
||
return true
|
||
}
|
||
|
||
// GetLevel 返回当前日志级别(未 Init 返回 InfoLevel)。
|
||
func (m *LogManager) GetLevel() zapcore.Level {
|
||
m.mu.Lock()
|
||
defer m.mu.Unlock()
|
||
if m.level == (zap.AtomicLevel{}) {
|
||
return zapcore.InfoLevel
|
||
}
|
||
return m.level.Level()
|
||
}
|
||
|
||
// SetLevel 包级 facade:运行期热切换默认日志级别(M19)。未 Init 时无操作返回 false。
|
||
func SetLevel(l zapcore.Level) bool { return GetDefaultLogManager().SetLevel(l) }
|
||
|
||
// GetLevel 包级 facade:返回默认日志当前级别。
|
||
func GetLevel() zapcore.Level { return GetDefaultLogManager().GetLevel() }
|
||
|
||
// SetDefaultLogManager 提升指定 LogManager 为全局默认(atomic.Store,并发安全)。
|
||
func SetDefaultLogManager(m *LogManager) {
|
||
if m != nil {
|
||
defaultLoggerPtr.Store(m)
|
||
}
|
||
}
|
||
|
||
// SnapshotDefault 返回默认 logger 全局状态快照。
|
||
// 快照仅用于 App 初始化回滚;调用方成功提交后必须 CloseDefaultSnapshot 释放旧 writers。
|
||
func SnapshotDefault() *DefaultSnapshot {
|
||
globalMu.Lock()
|
||
defer globalMu.Unlock()
|
||
return &DefaultSnapshot{
|
||
manager: GetDefaultLogManager(),
|
||
logger: currentLogger(),
|
||
sugar: currentSugar(),
|
||
apiLog: currentAPILog(),
|
||
dbLog: currentDBLog(),
|
||
writers: append([]*lumberjack.Logger(nil), fileWriters...),
|
||
generation: globalLogGeneration,
|
||
}
|
||
}
|
||
|
||
// RestoreDefaultSnapshot 恢复 SnapshotDefault 捕获的默认 logger 状态,并关闭当前新 writers。
|
||
func RestoreDefaultSnapshot(s *DefaultSnapshot) error {
|
||
if s == nil {
|
||
return nil
|
||
}
|
||
globalMu.Lock()
|
||
newLogger := currentLogger()
|
||
newAPILog := currentAPILog()
|
||
newDBLog := currentDBLog()
|
||
newWriters := fileWriters
|
||
loggerPtr.Store(s.logger)
|
||
sugarPtr.Store(s.sugar)
|
||
apiLogPtr.Store(s.apiLog)
|
||
dbLogPtr.Store(s.dbLog)
|
||
Logger = s.logger
|
||
fileWriters = append([]*lumberjack.Logger(nil), s.writers...)
|
||
globalLogGeneration = s.generation
|
||
globalMu.Unlock()
|
||
if s.manager != nil {
|
||
SetDefaultLogManager(s.manager)
|
||
}
|
||
return errors.Join(syncLoggers(newLogger, newAPILog, newDBLog), closeFileWriters(newWriters))
|
||
}
|
||
|
||
// CloseDefaultSnapshot 关闭 SnapshotDefault 捕获的旧 writers;用于新 logger 已提交成功后释放旧资源。
|
||
func CloseDefaultSnapshot(s *DefaultSnapshot) error {
|
||
if s == nil {
|
||
return nil
|
||
}
|
||
return errors.Join(syncLoggers(s.logger, s.apiLog, s.dbLog), closeFileWriters(s.writers))
|
||
}
|
||
|
||
// Init 初始化日志。
|
||
//
|
||
// 三个 logger 的分流策略:
|
||
// - Logger(通用):写 console + logs/app.log
|
||
// - APILog() :写 console + logs/api.log
|
||
// - DBLog() :写 console + logs/database.log
|
||
//
|
||
// 关键修复(v1.0.3):旧实现把 apiCore 和 dbCore 都 Tee 进通用 Logger,
|
||
// 导致每条 logger.Info(...) 都会同时落到 api.log + database.log + console
|
||
// 三份,磁盘占用翻倍且分流形同虚设。新实现通用 Logger 只走独立的 app.log。
|
||
func (m *LogManager) Init(cfg *config.Config) error {
|
||
return m.init(cfg, true)
|
||
}
|
||
|
||
// InitPreservingPrevious 初始化并发布 logger,但暂不关闭被替换的旧 writers。
|
||
// App.Init 使用该方法配合 SnapshotDefault,在后续 hook 失败时可恢复旧 logger。
|
||
func (m *LogManager) InitPreservingPrevious(cfg *config.Config) error {
|
||
return m.init(cfg, false)
|
||
}
|
||
|
||
func (m *LogManager) init(cfg *config.Config, closePrevious bool) error {
|
||
if cfg == nil {
|
||
return errors.New("logger: 配置为空")
|
||
}
|
||
if err := validateLogConfig(cfg.Log); err != nil {
|
||
return err
|
||
}
|
||
|
||
m.mu.Lock()
|
||
defer m.mu.Unlock()
|
||
|
||
// 确保日志目录存在(0750:owner+group 可访问,与 storage 目录权限一致)
|
||
if err := os.MkdirAll(cfg.Log.Dir, 0o750); err != nil {
|
||
return err
|
||
}
|
||
|
||
// 日志编码器配置
|
||
encoderConfig := zapcore.EncoderConfig{
|
||
TimeKey: "time",
|
||
LevelKey: "level",
|
||
NameKey: "logger",
|
||
CallerKey: "caller",
|
||
FunctionKey: zapcore.OmitKey,
|
||
MessageKey: "msg",
|
||
StacktraceKey: "stacktrace",
|
||
LineEnding: zapcore.DefaultLineEnding,
|
||
EncodeLevel: zapcore.LowercaseLevelEncoder,
|
||
EncodeTime: zapcore.ISO8601TimeEncoder,
|
||
EncodeDuration: zapcore.SecondsDurationEncoder,
|
||
EncodeCaller: zapcore.ShortCallerEncoder,
|
||
}
|
||
|
||
// 根据运行模式设置日志级别(M19:用 AtomicLevel 支持运行期 SetLevel 热切换)
|
||
level := zap.NewAtomicLevelAt(zapcore.DebugLevel)
|
||
if cfg.IsProduction() {
|
||
level = zap.NewAtomicLevelAt(zapcore.InfoLevel)
|
||
}
|
||
|
||
jsonEncoder := zapcore.NewJSONEncoder(encoderConfig)
|
||
consoleEncoder := zapcore.NewConsoleEncoder(encoderConfig)
|
||
consoleCore := zapcore.NewCore(consoleEncoder, zapcore.AddSync(os.Stdout), level)
|
||
|
||
// 通用日志独立文件,避免与 api/db 日志重复写入
|
||
appWriter := newRotatingWriter(cfg.Log, "app.log")
|
||
apiWriter := newRotatingWriter(cfg.Log, "api.log")
|
||
dbWriter := newRotatingWriter(cfg.Log, "database.log")
|
||
|
||
appCore := zapcore.NewCore(jsonEncoder, zapcore.AddSync(appWriter), level)
|
||
apiCore := zapcore.NewCore(jsonEncoder, zapcore.AddSync(apiWriter), level)
|
||
dbCore := zapcore.NewCore(jsonEncoder, zapcore.AddSync(dbWriter), level)
|
||
|
||
// 三个 logger 各写自己的文件 + console,互不 Tee
|
||
newLogger := zap.New(
|
||
zapcore.NewTee(appCore, consoleCore),
|
||
zap.AddCaller(), zap.AddCallerSkip(1),
|
||
)
|
||
newAPILog := zap.New(
|
||
zapcore.NewTee(apiCore, consoleCore),
|
||
zap.AddCaller(), zap.AddCallerSkip(1),
|
||
)
|
||
newDBLog := zap.New(
|
||
zapcore.NewTee(dbCore, consoleCore),
|
||
zap.AddCaller(), zap.AddCallerSkip(1),
|
||
)
|
||
|
||
// 全部构造成功后再原子替换全局变量,避免半初始化状态。
|
||
// 先发布新 logger,再关闭旧 writer,避免请求 goroutine 在替换窗口内
|
||
// 通过包级函数拿到仍指向已关闭 writer 的旧 logger。
|
||
// H7:四个 logger 经 atomic.Pointer Store,读侧(请求 goroutine)无锁原子 load;
|
||
// fileWriters 仅在 globalMu 下访问。Logger 兼容别名同步维护。
|
||
globalMu.Lock()
|
||
oldWriters := fileWriters
|
||
globalLogGeneration++
|
||
m.generation = globalLogGeneration
|
||
loggerPtr.Store(newLogger)
|
||
sugarPtr.Store(newLogger.Sugar())
|
||
apiLogPtr.Store(newAPILog)
|
||
dbLogPtr.Store(newDBLog)
|
||
Logger = newLogger
|
||
fileWriters = []*lumberjack.Logger{appWriter, apiWriter, dbWriter}
|
||
globalMu.Unlock()
|
||
|
||
m.level = level
|
||
m.cfg = cfg
|
||
|
||
if closePrevious {
|
||
return closeFileWriters(oldWriters)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// Init 包级 facade:初始化日志(代理到 DefaultLogger)。
|
||
func Init(cfg *config.Config) error {
|
||
return GetDefaultLogManager().Init(cfg)
|
||
}
|
||
|
||
func validateLogConfig(cfg config.LogConfig) error {
|
||
if strings.TrimSpace(cfg.Dir) == "" {
|
||
return errors.New("logger: 日志目录为空")
|
||
}
|
||
if cfg.MaxSize < 0 {
|
||
return errors.New("logger: MaxSize 不能为负数")
|
||
}
|
||
if cfg.MaxBackups < 0 {
|
||
return errors.New("logger: MaxBackups 不能为负数")
|
||
}
|
||
if cfg.MaxAge < 0 {
|
||
return errors.New("logger: MaxAge 不能为负数")
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// newRotatingWriter 创建带 lumberjack 滚动归档的 writer
|
||
func newRotatingWriter(cfg config.LogConfig, filename string) *lumberjack.Logger {
|
||
return &lumberjack.Logger{
|
||
Filename: filepath.Join(cfg.Dir, filename),
|
||
MaxSize: cfg.MaxSize,
|
||
MaxBackups: cfg.MaxBackups,
|
||
MaxAge: cfg.MaxAge,
|
||
Compress: cfg.Compress,
|
||
}
|
||
}
|
||
|
||
// closeFileWriters 关闭传入的 lumberjack writer,并聚合关闭错误。
|
||
func closeFileWriters(writers []*lumberjack.Logger) error {
|
||
var errs []error
|
||
for _, w := range writers {
|
||
if w != nil {
|
||
if err := w.Close(); err != nil {
|
||
errs = append(errs, err)
|
||
}
|
||
}
|
||
}
|
||
return errors.Join(errs...)
|
||
}
|
||
|
||
func syncLoggers(loggers ...*zap.Logger) error {
|
||
var errs []error
|
||
for _, l := range loggers {
|
||
if l == nil {
|
||
continue
|
||
}
|
||
if err := l.Sync(); err != nil && !isHarmlessSyncError(err) {
|
||
errs = append(errs, err)
|
||
}
|
||
}
|
||
return errors.Join(errs...)
|
||
}
|
||
|
||
// Sync 同步全部 logger 缓冲到底层 writer。
|
||
//
|
||
// 注意:在 Windows / 部分 *nix 平台上对 stdout/stderr 调用 Sync 会返回
|
||
// "invalid argument" / "inappropriate ioctl for device",属于 zap 已知行为,
|
||
// 这里把这类错误识别并忽略,只返回真实的写入失败。
|
||
func (m *LogManager) Sync() error {
|
||
return syncLoggers(currentLogger(), currentAPILog(), currentDBLog())
|
||
}
|
||
|
||
// Sync 包级 facade:同步全部 logger 缓冲(代理到 DefaultLogger)。
|
||
func Sync() error {
|
||
return GetDefaultLogManager().Sync()
|
||
}
|
||
|
||
// Close 关闭日志文件句柄,重置全局 logger 为 Nop。
|
||
// 通常由 App.Shutdown 在 Sync 之后调用;测试场景需要清理临时目录时也应调用。
|
||
//
|
||
// 调用后再次写日志不会 panic(fall back to nop logger),但不会写入文件。
|
||
// 如需重新启用,请再次调用 Init。
|
||
func (m *LogManager) Close() error {
|
||
m.mu.Lock()
|
||
globalMu.Lock()
|
||
if m.generation == 0 || m.generation != globalLogGeneration {
|
||
m.level = zap.AtomicLevel{}
|
||
m.cfg = nil
|
||
m.generation = 0
|
||
globalMu.Unlock()
|
||
m.mu.Unlock()
|
||
return nil
|
||
}
|
||
oldLogger := currentLogger()
|
||
oldAPILog := currentAPILog()
|
||
oldDBLog := currentDBLog()
|
||
oldWriters := fileWriters
|
||
|
||
// H7:重置为 Nop 经 atomic Store,与读侧一致;Logger 兼容别名同步。
|
||
nop := zap.NewNop()
|
||
loggerPtr.Store(nop)
|
||
sugarPtr.Store(nop.Sugar())
|
||
apiLogPtr.Store(nop)
|
||
dbLogPtr.Store(nop)
|
||
Logger = nop
|
||
fileWriters = nil
|
||
globalMu.Unlock()
|
||
|
||
m.level = zap.AtomicLevel{}
|
||
m.cfg = nil
|
||
m.generation = 0
|
||
m.mu.Unlock()
|
||
|
||
return errors.Join(
|
||
syncLoggers(oldLogger, oldAPILog, oldDBLog),
|
||
closeFileWriters(oldWriters),
|
||
)
|
||
}
|
||
|
||
// Close 包级 facade:关闭日志文件句柄,重置为 Nop(代理到 DefaultLogger)。
|
||
func Close() error {
|
||
return GetDefaultLogManager().Close()
|
||
}
|
||
|
||
// isHarmlessSyncError 识别 stdout/stderr Sync 在不同平台返回的预期错误。
|
||
// 这些错误来自 console core,对真实 writer 无影响,可安全忽略。
|
||
func isHarmlessSyncError(err error) bool {
|
||
if err == nil {
|
||
return true
|
||
}
|
||
msg := err.Error()
|
||
for _, sub := range []string{
|
||
"invalid argument", // Linux stdout
|
||
"inappropriate ioctl for device", // macOS stdout
|
||
"bad file descriptor", // Windows stdout
|
||
} {
|
||
if strings.Contains(msg, sub) {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
// Debug 调试日志
|
||
func Debug(msg string, fields ...zap.Field) {
|
||
currentLogger().Debug(msg, fields...)
|
||
}
|
||
|
||
// Info 信息日志
|
||
func Info(msg string, fields ...zap.Field) {
|
||
currentLogger().Info(msg, fields...)
|
||
}
|
||
|
||
// Warn 警告日志
|
||
func Warn(msg string, fields ...zap.Field) {
|
||
currentLogger().Warn(msg, fields...)
|
||
}
|
||
|
||
// Error 错误日志
|
||
func Error(msg string, fields ...zap.Field) {
|
||
currentLogger().Error(msg, fields...)
|
||
}
|
||
|
||
// Fatal 致命错误日志(仅供应用层使用,框架内部禁止调用)
|
||
func Fatal(msg string, fields ...zap.Field) {
|
||
currentLogger().Fatal(msg, fields...)
|
||
}
|
||
|
||
// Debugf 格式化调试日志
|
||
func Debugf(template string, args ...any) {
|
||
currentSugar().Debugf(template, args...)
|
||
}
|
||
|
||
// Infof 格式化信息日志
|
||
func Infof(template string, args ...any) {
|
||
currentSugar().Infof(template, args...)
|
||
}
|
||
|
||
// Warnf 格式化警告日志
|
||
func Warnf(template string, args ...any) {
|
||
currentSugar().Warnf(template, args...)
|
||
}
|
||
|
||
// Errorf 格式化错误日志
|
||
func Errorf(template string, args ...any) {
|
||
currentSugar().Errorf(template, args...)
|
||
}
|
||
|
||
// Fatalf 格式化致命错误日志(仅供应用层使用,框架内部禁止调用)
|
||
func Fatalf(template string, args ...any) {
|
||
currentSugar().Fatalf(template, args...)
|
||
}
|
||
|
||
// APILog 返回 API 专用日志器(写 logs/api.log + console)
|
||
func APILog() *zap.Logger {
|
||
return currentAPILog()
|
||
}
|
||
|
||
// DBLog 返回数据库专用日志器(写 logs/database.log + console)
|
||
func DBLog() *zap.Logger {
|
||
return currentDBLog()
|
||
}
|