157 lines
3.7 KiB
Go
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
|
|
}
|