Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1870d84974 | ||
|
|
6bc0aa4c0a | ||
|
|
3d0fc5e93f | ||
|
|
1f816dc8b3 | ||
|
|
a2b1055f5d |
@ -1,5 +1,11 @@
|
||||
# Changelog
|
||||
|
||||
## [1.3.2] - 2026-05-13
|
||||
- **功能增强: 引入摩擦消除工具 `As`**:
|
||||
- **泛型支持**: 新增全局泛型函数 `log.As[T](v T, err error) T`,仿照 `cast.As` 设计,自动记录错误并返回零值,极大简化了带 error 返回值的函数链式调用。
|
||||
- **Logger 扩展**: `Logger` 结构体新增 `As(v any, err error) any` 方法,支持实例级别的错误捕获与自动记录。
|
||||
- **调用栈优化**: 优化了 `GetCallStacks` 逻辑,自动跳过 `default_logger.go` 中的内部帧,确保 `log.As` 记录的错误位置精准指向业务代码。
|
||||
|
||||
## [1.3.1] - 2026-05-12
|
||||
- **架构升级: 引入 LoggerService**:
|
||||
- **解耦重构**: 重构全局变量管理,引入 `loggerService` 结构体,集中化管理异步写入协程、Writers 对象池、文件句柄与丢弃计数。
|
||||
|
||||
@ -89,10 +89,14 @@ export LOG_FILE=console
|
||||
1. **分级记录**
|
||||
* `Debug`, `Info`, `Warning`, `Error` —— 标准日志方法,支持 `message` + 变长 `extra` 参数。
|
||||
|
||||
2. **通用记录 (`Log`)**
|
||||
2. **摩擦消除 (`As`)**
|
||||
* `As(v, err)` —— 仿照 `cast.As`,忽略错误并返回零值,但会自动将错误记录到日志中。支持全局调用 (`log.As`) 或实例调用 (`logger.As`)。
|
||||
* **优势**: 在类型转换或快速赋值场景下,无需繁琐的 `if err != nil` 判断,同时确保异常被记录。
|
||||
|
||||
3. **通用记录 (`Log`)**
|
||||
* `Log(LogEntry)` —— 记录自定义结构的日志。
|
||||
|
||||
3. **独立可视化工具 (`logv`)**
|
||||
4. **独立可视化工具 (`logv`)**
|
||||
* **安装**: `go install apigo.cc/go/log/logv@latest`
|
||||
* **使用**: `tail -f app.log | logv` 或 `tail -f app.log | logv -json`。
|
||||
|
||||
|
||||
@ -30,3 +30,35 @@ func SetDefaultName(name string) {
|
||||
DefaultLogger.SetName(name)
|
||||
}
|
||||
}
|
||||
|
||||
// As 仿照 cast.As,使用 DefaultLogger 记录错误并返回零值 (消除摩擦)
|
||||
func As[T any](v T, err error) T {
|
||||
if err != nil {
|
||||
if DefaultLogger != nil {
|
||||
DefaultLogger.Error(err.Error())
|
||||
}
|
||||
var zero T
|
||||
return zero
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// Debug 记录一条调试级别日志
|
||||
func Debug(message string, extra ...any) {
|
||||
DefaultLogger.Debug(message, extra...)
|
||||
}
|
||||
|
||||
// Info 记录一条信息级别日志
|
||||
func Info(message string, extra ...any) {
|
||||
DefaultLogger.Info(message, extra...)
|
||||
}
|
||||
|
||||
// Warning 记录一条警告级别日志
|
||||
func Warning(message string, extra ...any) {
|
||||
DefaultLogger.Warning(message, extra...)
|
||||
}
|
||||
|
||||
// Error 记录一条错误级别日志
|
||||
func Error(message string, extra ...any) {
|
||||
DefaultLogger.Error(message, extra...)
|
||||
}
|
||||
|
||||
@ -97,7 +97,7 @@ func (w *esWriter) Run() {
|
||||
w.lock.Unlock()
|
||||
|
||||
// 超过100条数据 或 过了1秒 发送数据
|
||||
if queueLen > 100 || (queueLen > 0 && (now > w.last || !LoggerService.Running.Load())) {
|
||||
if queueLen > 100 || (queueLen > 0 && (now > w.last || !WriterService.Running.Load())) {
|
||||
w.lock.Lock()
|
||||
sendings := w.queue
|
||||
w.queue = make([]string, 0)
|
||||
|
||||
@ -20,6 +20,7 @@ func TestSplitTag(t *testing.T) {
|
||||
File: logFile,
|
||||
SplitTag: splitTag,
|
||||
}
|
||||
log.WriterService.Start(nil, nil)
|
||||
logger := log.NewLogger(conf)
|
||||
|
||||
// 1. 记录第一条日志
|
||||
|
||||
16
go.mod
16
go.mod
@ -3,17 +3,17 @@ module apigo.cc/go/log
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
apigo.cc/go/cast v1.3.0
|
||||
apigo.cc/go/config v1.3.0
|
||||
apigo.cc/go/file v1.3.0
|
||||
apigo.cc/go/id v1.3.0
|
||||
apigo.cc/go/shell v1.3.0
|
||||
apigo.cc/go/cast v1.3.3
|
||||
apigo.cc/go/config v1.3.1
|
||||
apigo.cc/go/file v1.3.2
|
||||
apigo.cc/go/id v1.3.1
|
||||
apigo.cc/go/shell v1.3.1
|
||||
)
|
||||
|
||||
require (
|
||||
apigo.cc/go/encoding v1.3.0 // indirect
|
||||
apigo.cc/go/rand v1.3.0 // indirect
|
||||
apigo.cc/go/safe v1.3.0 // indirect
|
||||
apigo.cc/go/encoding v1.3.1 // indirect
|
||||
apigo.cc/go/rand v1.3.1 // indirect
|
||||
apigo.cc/go/safe v1.3.1 // indirect
|
||||
golang.org/x/crypto v0.51.0 // indirect
|
||||
golang.org/x/sys v0.44.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
|
||||
24
go.sum
24
go.sum
@ -1,11 +1,19 @@
|
||||
apigo.cc/go/cast v1.3.0 h1:ZTcLYijkqZjSWSCSpJUWMfzJYeJKbwKxquKkPrFsROQ=
|
||||
apigo.cc/go/config v1.3.0 h1:TwI3bv3D+BJrAnFx+o62HQo3FarY2Ge3SCGsKchFYGg=
|
||||
apigo.cc/go/encoding v1.3.0 h1:8jqNHoZBR8vOU/BGsLFebfp1Txa1UxDRpd7YwzIFLJs=
|
||||
apigo.cc/go/file v1.3.0 h1:xG9FcY3Rv6Br83r9pq9QsIXFrplx4g8ITOkHSzfzXRg=
|
||||
apigo.cc/go/id v1.3.0 h1:Tr2Yj0Rl19lfwW5wBTJ407o/zgo2oVRLE20WWEgJzdE=
|
||||
apigo.cc/go/rand v1.3.0 h1:k+UFAhMySwXf+dq8Om9TniZV6fm6gAE0evbrqMEdwQU=
|
||||
apigo.cc/go/safe v1.3.0 h1:uctdAUsphT9p60Tk4oS5xPCe0NoIdOHfsYv4PNS0Rok=
|
||||
apigo.cc/go/shell v1.3.0 h1:hdxuYPN/7T2BuM/Ja8AjVUhbRqU/wpi8OjcJVziJ0nw=
|
||||
apigo.cc/go/cast v1.3.3 h1:aln5eDR5DZVWVzZ/y5SJh1gQNgWv2sT82I25NaO9g34=
|
||||
apigo.cc/go/cast v1.3.3/go.mod h1:lGlwImiOvHxG7buyMWhFzcdvQzmSaoKbmr7bcDfUpHk=
|
||||
apigo.cc/go/config v1.3.1 h1:wZzUh4oL+fGD6SayVgX6prLPMsniM25etWFcEH8XzIE=
|
||||
apigo.cc/go/config v1.3.1/go.mod h1:7KHz/1WmtBLM762Lln/TaXh2dmlMvJTLhnlk33zbS3U=
|
||||
apigo.cc/go/encoding v1.3.1 h1:y8O58KYAyulkThg1O2ji2BqjnFoSvk42sit9I3z+K7Y=
|
||||
apigo.cc/go/encoding v1.3.1/go.mod h1:xAJk5b83VZ31mXMTnyp0dfMoBKfT/AHDn0u+cQfojgY=
|
||||
apigo.cc/go/file v1.3.2 h1:pu4oiDyiqgj3/eykfnJf+/6+A9v/Z0b3ClP5XK+lwG4=
|
||||
apigo.cc/go/file v1.3.2/go.mod h1:vci4h0Pz94mV6dkniQkuyBYERVYeq7/LX4jJVuCg9hs=
|
||||
apigo.cc/go/id v1.3.1 h1:pkqi6VeWyQoHuIu0Zbx/RRxIAdM61Js0j6cY1M9XVCk=
|
||||
apigo.cc/go/id v1.3.1/go.mod h1:P2/vl3tyW3US+ayOFSMoPIOCulNLBngNYPhXJC/Z7J4=
|
||||
apigo.cc/go/rand v1.3.1 h1:7FvsI6PtQ5XrWER0dTiLVo0p7GIxRidT/TBKhVy93j8=
|
||||
apigo.cc/go/rand v1.3.1/go.mod h1:mZ/4Soa3bk+XvDaqPWJuUe1bfEi4eThBj1XmEAuYxsk=
|
||||
apigo.cc/go/safe v1.3.1 h1:irTCqPAC97gGsX/Lw5AzLelDt1xXLEZIAaVhLELWe9Q=
|
||||
apigo.cc/go/safe v1.3.1/go.mod h1:XdOpBhN2vkImalaykYXXmEpczqWa1y3ah6/Q72cdRqE=
|
||||
apigo.cc/go/shell v1.3.1 h1:M8oD0b2HcJuCC6frQFx11b3UTcTx3lATX8XK+YXSVm8=
|
||||
apigo.cc/go/shell v1.3.1/go.mod h1:ZMdJjpCpWdvsHKUXlelh/AxsV/nWdkH/k3lISfzMdUw=
|
||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
|
||||
26
log_test.go
26
log_test.go
@ -1,6 +1,7 @@
|
||||
package log_test
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"apigo.cc/go/log"
|
||||
@ -86,3 +87,28 @@ func TestExtraLogs(t *testing.T) {
|
||||
|
||||
logger.Info("Extra log test", "key", "value")
|
||||
}
|
||||
|
||||
func TestAs(t *testing.T) {
|
||||
// 1. 测试 log.As (使用 DefaultLogger)
|
||||
val1 := log.As(strconv.Atoi("123"))
|
||||
if val1 != 123 {
|
||||
t.Errorf("log.As expected 123, got %v", val1)
|
||||
}
|
||||
|
||||
val2 := log.As(strconv.Atoi("abc"))
|
||||
if val2 != 0 {
|
||||
t.Errorf("log.As expected 0, got %v", val2)
|
||||
}
|
||||
|
||||
// 2. 测试 logger.As (方法)
|
||||
logger := log.NewLogger(log.Config{Level: "debug"})
|
||||
val3 := logger.As(strconv.Atoi("456")).(int)
|
||||
if val3 != 456 {
|
||||
t.Errorf("logger.As expected 456, got %v", val3)
|
||||
}
|
||||
|
||||
val4 := logger.As(strconv.Atoi("def")).(int)
|
||||
if val4 != 0 {
|
||||
t.Errorf("logger.As expected 0, got %v", val4)
|
||||
}
|
||||
}
|
||||
|
||||
32
logger.go
32
logger.go
@ -78,29 +78,27 @@ func NewLogger(conf Config) *Logger {
|
||||
if m, ok := writerMakers[writerName]; ok {
|
||||
if w := m(&conf); w != nil {
|
||||
logger.writer = w
|
||||
LoggerService.WriterLock.Lock()
|
||||
cur := LoggerService.Writers.Load().([]Writer)
|
||||
WriterService.WriterLock.Lock()
|
||||
cur := WriterService.Writers.Load().([]Writer)
|
||||
newW := append(cur, w)
|
||||
LoggerService.Writers.Store(newW)
|
||||
LoggerService.WriterLock.Unlock()
|
||||
Start()
|
||||
WriterService.Writers.Store(newW)
|
||||
WriterService.WriterLock.Unlock()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if conf.SplitTag != "" {
|
||||
LoggerService.FilesLock.RLock()
|
||||
logger.file = LoggerService.Files[conf.File+conf.SplitTag]
|
||||
LoggerService.FilesLock.RUnlock()
|
||||
WriterService.FilesLock.RLock()
|
||||
logger.file = WriterService.Files[conf.File+conf.SplitTag]
|
||||
WriterService.FilesLock.RUnlock()
|
||||
if logger.file == nil {
|
||||
logger.file = &FileWriter{
|
||||
fileName: conf.File,
|
||||
splitTag: conf.SplitTag,
|
||||
}
|
||||
LoggerService.FilesLock.Lock()
|
||||
LoggerService.Files[conf.File+conf.SplitTag] = logger.file
|
||||
LoggerService.FilesLock.Unlock()
|
||||
WriterService.FilesLock.Lock()
|
||||
WriterService.Files[conf.File+conf.SplitTag] = logger.file
|
||||
WriterService.FilesLock.Unlock()
|
||||
}
|
||||
Start()
|
||||
} else {
|
||||
fp, err := os.OpenFile(conf.File, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
|
||||
if err == nil {
|
||||
@ -124,7 +122,7 @@ func (logger *Logger) asyncWrite(entry LogEntry) {
|
||||
}
|
||||
|
||||
func (logger *Logger) writeBuf(entry LogEntry, buf []byte) {
|
||||
if LoggerService.Running.Load() {
|
||||
if WriterService.Running.Load() {
|
||||
writeAsync(logPayload{
|
||||
entry: entry,
|
||||
buf: buf,
|
||||
@ -256,6 +254,14 @@ func (logger *Logger) GetTraceId() string {
|
||||
return logger.traceId
|
||||
}
|
||||
|
||||
// As 仿照 cast.As,忽略错误并返回零值,但会将错误记录到日志中 (消除摩擦)
|
||||
func (logger *Logger) As(v any, err error) any {
|
||||
if err != nil {
|
||||
logger.Error(err.Error())
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func (logger *Logger) CheckLevel(logLevel LevelType) bool {
|
||||
settedLevel := logger.level
|
||||
if settedLevel == 0 {
|
||||
|
||||
@ -28,8 +28,7 @@ func TestLoggerReliability(t *testing.T) {
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
Stop()
|
||||
Wait()
|
||||
WriterService.Stop(nil)
|
||||
|
||||
file, err := os.Open(logFile)
|
||||
if err != nil {
|
||||
|
||||
@ -69,6 +69,7 @@ func getCallStacks(truncations []string) []string {
|
||||
isLogInternal := (strings.Contains(file, "/log/logger.go") ||
|
||||
strings.Contains(file, "/log/utility.go") ||
|
||||
strings.Contains(file, "/log/standard.go") ||
|
||||
strings.Contains(file, "/log/default_logger.go") ||
|
||||
strings.Contains(file, "/log/extra.go"))
|
||||
|
||||
if isLogInternal {
|
||||
|
||||
@ -181,12 +181,12 @@ func TestCallStacksViewable(t *testing.T) {
|
||||
wd, _ := os.Getwd()
|
||||
entry := &CallStackLog{
|
||||
BaseLog: log.BaseLog{
|
||||
LogType: "error",
|
||||
LogType: "test_error",
|
||||
LogTime: 1714896000000000000,
|
||||
},
|
||||
CallStacks: []string{wd + "/main.go:10", "/usr/local/go/src/runtime/panic.go:100"},
|
||||
}
|
||||
log.RegisterType("error", entry)
|
||||
log.RegisterType("test_error", entry)
|
||||
|
||||
line := string(log.Marshal(entry, nil))
|
||||
out := log.Viewable(line)
|
||||
|
||||
60
writer.go
60
writer.go
@ -22,8 +22,8 @@ type logPayload struct {
|
||||
file *FileWriter // 目标文件 Writer
|
||||
}
|
||||
|
||||
// loggerService manages the background writing of log entries.
|
||||
type loggerService struct {
|
||||
// writerService manages the background writing of log entries.
|
||||
type writerService struct {
|
||||
Running atomic.Bool
|
||||
StopChan chan bool
|
||||
LogChannel chan logPayload
|
||||
@ -36,8 +36,8 @@ type loggerService struct {
|
||||
}
|
||||
|
||||
var (
|
||||
// LoggerService is the global instance of loggerService.
|
||||
LoggerService = &loggerService{}
|
||||
// WriterService is the global instance of defaultService.
|
||||
WriterService = &writerService{}
|
||||
)
|
||||
|
||||
// ConsoleWriter 控制台写入器
|
||||
@ -52,9 +52,9 @@ func (w *ConsoleWriter) Run() {
|
||||
}
|
||||
|
||||
func init() {
|
||||
LoggerService.LogChannel = make(chan logPayload, 10000)
|
||||
LoggerService.Writers.Store([]Writer{})
|
||||
LoggerService.Files = make(map[string]*FileWriter)
|
||||
WriterService.LogChannel = make(chan logPayload, 10000)
|
||||
WriterService.Writers.Store([]Writer{})
|
||||
WriterService.Files = make(map[string]*FileWriter)
|
||||
|
||||
RegisterWriterMaker("console", func(conf *Config) Writer {
|
||||
return &ConsoleWriter{}
|
||||
@ -66,14 +66,14 @@ func writeAsync(payload logPayload) {
|
||||
defer func() {
|
||||
recover()
|
||||
}()
|
||||
if !LoggerService.Running.Load() {
|
||||
if !WriterService.Running.Load() {
|
||||
return
|
||||
}
|
||||
select {
|
||||
case LoggerService.LogChannel <- payload:
|
||||
case WriterService.LogChannel <- payload:
|
||||
default:
|
||||
// 丢弃或处理过载
|
||||
dropped := LoggerService.Dropped.Add(1)
|
||||
dropped := WriterService.Dropped.Add(1)
|
||||
if dropped%1000 == 1 {
|
||||
if DefaultLogger != nil {
|
||||
// 注意:这里可能会产生递归调用,但 select default 保证了不会死锁
|
||||
@ -85,29 +85,11 @@ func writeAsync(payload logPayload) {
|
||||
|
||||
// GetDroppedLogs 获取被丢弃的日志数量
|
||||
func GetDroppedLogs() uint64 {
|
||||
return LoggerService.Dropped.Load()
|
||||
}
|
||||
|
||||
// Start 启动写入器 (兼容旧 API)
|
||||
func Start() {
|
||||
_ = LoggerService.Start(nil, nil)
|
||||
}
|
||||
|
||||
// Stop 停止写入器 (兼容旧 API)
|
||||
func Stop() {
|
||||
_ = LoggerService.Stop(nil)
|
||||
}
|
||||
|
||||
// Wait 等待写入器停止 (兼容旧 API)
|
||||
func Wait() {
|
||||
if LoggerService.StopChan != nil {
|
||||
<-LoggerService.StopChan
|
||||
LoggerService.StopChan = nil
|
||||
}
|
||||
return WriterService.Dropped.Load()
|
||||
}
|
||||
|
||||
// Start implements starter.Service interface.
|
||||
func (s *loggerService) Start(_ context.Context, _ *Logger) error {
|
||||
func (s *writerService) Start(_ context.Context, _ *Logger) error {
|
||||
if !s.Running.CompareAndSwap(false, true) {
|
||||
return nil
|
||||
}
|
||||
@ -117,7 +99,7 @@ func (s *loggerService) Start(_ context.Context, _ *Logger) error {
|
||||
}
|
||||
|
||||
// Stop implements starter.Service interface.
|
||||
func (s *loggerService) Stop(_ context.Context) error {
|
||||
func (s *writerService) Stop(_ context.Context) error {
|
||||
if s.Running.CompareAndSwap(true, false) {
|
||||
close(s.LogChannel)
|
||||
if s.StopChan != nil {
|
||||
@ -128,12 +110,14 @@ func (s *loggerService) Stop(_ context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Health implements starter.Service interface.
|
||||
func (s *loggerService) Health() error {
|
||||
return nil
|
||||
// Status implements starter.Service interface.
|
||||
func (s *writerService) Status() (string, error) {
|
||||
if s.Running.Load() {
|
||||
return fmt.Sprintf("queue: %d, dropped: %d", len(s.LogChannel), s.Dropped.Load()), nil
|
||||
}
|
||||
|
||||
func (s *loggerService) writerRunner() {
|
||||
return "stopped", nil
|
||||
}
|
||||
func (s *writerService) writerRunner() {
|
||||
ticker := time.NewTicker(200 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
|
||||
@ -173,7 +157,7 @@ func (s *loggerService) writerRunner() {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *loggerService) processLog(payload logPayload) {
|
||||
func (s *writerService) processLog(payload logPayload) {
|
||||
// 精准路由:根据包裹信息决定写入目标
|
||||
if payload.writer != nil {
|
||||
payload.writer.Log(payload.entry, payload.buf)
|
||||
@ -182,7 +166,7 @@ func (s *loggerService) processLog(payload logPayload) {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *loggerService) flushWriters() {
|
||||
func (s *writerService) flushWriters() {
|
||||
curWriters, _ := s.Writers.Load().([]Writer)
|
||||
for _, w := range curWriters {
|
||||
w.Run()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user