Files
huya-go/internal/logger/logger.go
T

157 lines
3.7 KiB
Go

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
}