Compare commits

...

5 Commits
v1.3.1 ... main

Author SHA1 Message Date
AI Engineer
1870d84974 chore: infrastructure alignment and doc sync (by AICoder) 2026-05-16 01:27:48 +08:00
AI Engineer
6bc0aa4c0a chore: infrastructure alignment and doc sync (by checkall) 2026-05-14 21:51:48 +08:00
AI Engineer
3d0fc5e93f feat: align with new starter.Service interface, reporting queue and drop status (by AI) 2026-05-13 11:11:12 +08:00
AI Engineer
1f816dc8b3 feat: add log.As and logger.As for frictionless error handling 2026-05-13 01:07:42 +08:00
AI Engineer
a2b1055f5d rename 2026-05-13 00:29:17 +08:00
13 changed files with 141 additions and 74 deletions

View File

@ -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 对象池、文件句柄与丢弃计数。

View File

@ -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`

View File

@ -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...)
}

View File

@ -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)

View File

@ -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
View File

@ -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
View File

@ -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=

View File

@ -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)
}
}

View File

@ -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 {

View File

@ -28,8 +28,7 @@ func TestLoggerReliability(t *testing.T) {
}
wg.Wait()
Stop()
Wait()
WriterService.Stop(nil)
file, err := os.Open(logFile)
if err != nil {

View File

@ -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 {

View File

@ -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)

View File

@ -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()