package logger import ( "os" "path/filepath" "strings" "sync" ) // sink 是日志输出汇集点,把结构化条目分发到内存缓冲、日志文件与界面回调。 type sink struct { store *Store file *fileWriter async *AsyncWriter } var ( sinkMu sync.RWMutex globalSink *sink globalClose func() ) // Init 初始化日志输出:内存缓冲、日志文件与界面回调同时生效。 // // instanceName / instanceID / dataDir 只用于写文件表头,便于把多实例日志 // 复制或汇总后仍能看出是哪台机器的哪个实例。 // 返回的 close 函数必须先调用(保证队列落盘),再由调用方关闭其它资源。 func Init(store *Store, logDir, instanceName, instanceID, dataDir string) (func(), error) { if store == nil { store = NewStore(500) } if level := os.Getenv("HUYA_LOG_LEVEL"); level != "" { SetMinLevel(level) } // 解析为绝对路径,避免打包后工作目录不同导致日志写到意外位置。 logDir = resolveAbsolutePath(logDir) store.SetFilePath("") file, err := newFileWriter(logDir, instanceID) if err != nil { // 日志文件不可用时降级为「仅内存 + 界面」,不影响主流程。 replaceSink(&sink{store: store}, nil) LogEvent("日志", LevelWarn, "日志文件不可用,日志仅保留在内存: %v", err) return func() { closeSink() }, err } file.writeHeader(instanceName, instanceID, dataDir) store.SetFilePath(file.Path()) replaceSink(&sink{ store: store, file: file, async: NewAsyncWriter(file, asyncBufferSize), }, func() { closeSink() }) LogEvent("日志", LevelSuccess, "日志文件已启用: %s", file.Path()) return func() { closeSink() }, nil } func replaceSink(next *sink, closeFn func()) { sinkMu.Lock() previous := globalClose globalSink = next globalClose = closeFn sinkMu.Unlock() if previous != nil { previous() } } func currentSink() *sink { sinkMu.RLock() defer sinkMu.RUnlock() return globalSink } func closeSink() { sinkMu.Lock() current := globalSink globalSink = nil globalClose = nil sinkMu.Unlock() if current == nil { return } if current.async != nil { _ = current.async.Close() } if current.file != nil { _ = current.file.Close() } } func (s *sink) write(entry Entry) { if s == nil { return } if s.store != nil { entry = s.store.Append(entry) } if s.async != nil { _, _ = s.async.Write([]byte(formatLogLine(entry))) } else if s.file != nil { _, _ = s.file.Write([]byte(formatLogLine(entry))) } if emit := currentEmitter(); emit != nil { emit(entry) } } // Dir 返回当前日志目录(无文件日志时返回空串)。 func Dir() string { current := currentSink() if current == nil || current.file == nil { return "" } return current.file.Dir() } // FilePath 返回当前日志文件路径(无文件日志时返回空串)。 func FilePath() string { current := currentSink() if current == nil || current.file == nil { return "" } return current.file.Path() } // Dropped 返回因队列满被丢弃的日志条数。 func Dropped() uint64 { current := currentSink() if current == nil || current.async == nil { return 0 } return current.async.Dropped() } // LevelFromString 把用户输入的等级名归一化,非法值回退为 info。 func LevelFromString(level string) string { return normalizeLevel(strings.TrimSpace(level)) } // resolveAbsolutePath 把相对路径解析为绝对路径,便于日志里直接复制路径排查。 func resolveAbsolutePath(path string) string { if path == "" || filepath.IsAbs(path) { return path } if abs, err := filepath.Abs(path); err == nil { return abs } if exe, err := os.Executable(); err == nil { return filepath.Join(filepath.Dir(exe), path) } return path }