feat: enhance DBLog with error/stack and add extra log formats (v1.0.1) (by AI)

This commit is contained in:
AI Engineer 2026-05-04 01:14:46 +08:00
parent 80aa4aaa49
commit f9e3a2ec5a
8 changed files with 267 additions and 19 deletions

View File

@ -1,5 +1,12 @@
# Changelog # Changelog
## [1.0.1] - 2026-05-04
- **结构增强**: `DBLog` 结构体新增 `Error``CallStacks` 字段,提升数据库错误诊断效率。
- **DB 方法重构**: `Logger.DB` 方法支持可选错误参数,自动处理 `dbError` 类型并记录调用栈。
- **扩展日志支持**: 新增 `TaskLog`, `MonitorLog`, `StatisticLog` 标准结构及其 `Logger` 快捷方法,置于 `extra.go`
- **RequestLog 封装**: `Logger` 新增 `Request` 方法,简化请求日志记录流程。
- **调用栈优化**: 优化 `getCallStacks` 逻辑,确保能正确捕获业务代码和测试代码的调用位置,同时过滤掉日志库内部帧。
## [1.0.0] - 2026-05-02 ## [1.0.0] - 2026-05-02
- **初始版本**: 由 `ssgo/log` 迁移并基于 `apigo.cc/go` 标准重构。 - **初始版本**: 由 `ssgo/log` 迁移并基于 `apigo.cc/go` 标准重构。
- **高性能引擎**: 引入 `LogEntry` 池化与 `sync.Pool` 复用,支持零分配日志对象。 - **高性能引擎**: 引入 `LogEntry` 池化与 `sync.Pool` 复用,支持零分配日志对象。

View File

@ -14,21 +14,41 @@
go get apigo.cc/go/log go get apigo.cc/go/log
``` ```
## 快速开始 ## 标准化日志 API
除了基础的 `Debug`, `Info`, `Warning`, `Error` 外,`go/log` 还提供了一系列针对特定场景优化的标准化日志 API
### 数据库日志 (DB)
自动处理耗时计算、脱敏及错误堆栈捕获。
```go ```go
import "apigo.cc/go/log" // 记录正常 SQL
logger.DB("mysql", dsn, "SELECT * FROM users WHERE id=?", []any{1}, 10.5)
func main() { // 记录带错误的 SQL (自动捕获调用栈并设为 dbError 类型)
// 使用默认 Logger logger.DB("mysql", dsn, "SELECT...", args, usedTime, "table not found")
log.Info("server started", "port", 8080) ```
// 创建带 traceId 的子 Logger ### 请求日志 (Request)
logger := log.New("unique-trace-id") 针对高性能 HTTP 服务设计的结构化日志。
logger.Info("request processed") ```go
req := &log.RequestLog{
// 错误日志带堆栈 Method: "GET",
logger.Error("database failed", "db", "mysql") Path: "/api/user",
// ... 填充其他字段
} }
logger.Request(req)
```
### 任务与监控 (Task / Monitor / Statistic)
```go
// 任务执行日志
logger.Task("CleanCache", 150.2, true, "Success")
// 监控告警日志
logger.Monitor("CPU", 1, "Load too high")
// 业务指标统计
logger.Statistic("Business", "OrderCount", 100)
``` ```
## 配置项 (JSON/YAML) ## 配置项 (JSON/YAML)

19
TEST.md
View File

@ -0,0 +1,19 @@
# Log Performance Test
## Test Environment
- OS: darwin
- Arch: amd64
- CPU: Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz
## Benchmark Results (v1.0.1)
| Benchmark | Iterations | ns/op | B/op | allocs/op |
| :--- | :--- | :--- | :--- | :--- |
| `BenchmarkLogger_RequestLog_Realistic` | 4,937,952 | 270.2 | 72 | 2 |
| `BenchmarkLoggerInfo` | 108,744 | 9,699 | - | - |
| `BenchmarkLoggerAsyncConcurrent` | 121,032 | 8,891 | - | - |
## Summary
- **RequestLog Performance**: High-performance structured logging with minimal allocations.
- **Async Efficiency**: Concurrent logging remains stable and efficient.
- **Object Pooling**: Effective use of `sync.Pool` for `LogEntry` objects reduces GC pressure.

118
extra.go Normal file
View File

@ -0,0 +1,118 @@
package log
import (
"reflect"
"apigo.cc/go/cast"
)
type TaskLog struct {
BaseLog
Task string
UsedTime float32
Success bool
Message string
}
func (t *TaskLog) Reset() {
t.BaseLog.Reset()
t.Task = ""
t.UsedTime = 0
t.Success = false
t.Message = ""
}
func (t *TaskLog) Base() *BaseLog {
return &t.BaseLog
}
type MonitorLog struct {
BaseLog
Target string
Status int
Message string
}
func (m *MonitorLog) Reset() {
m.BaseLog.Reset()
m.Target = ""
m.Status = 0
m.Message = ""
}
func (m *MonitorLog) Base() *BaseLog {
return &m.BaseLog
}
type StatisticLog struct {
BaseLog
Category string
Item string
Value float64
}
func (s *StatisticLog) Reset() {
s.BaseLog.Reset()
s.Category = ""
s.Item = ""
s.Value = 0
}
func (s *StatisticLog) Base() *BaseLog {
return &s.BaseLog
}
func (logger *Logger) Task(taskName string, usedTime float32, success bool, message string, extra ...any) {
if logger.CheckLevel(INFO) {
entry := GetEntry(reflect.TypeOf(&TaskLog{})).(*TaskLog)
logger.fillBase(entry.Base(), LogTypeTask)
entry.Task = taskName
entry.UsedTime = usedTime
entry.Success = success
entry.Message = message
if len(extra) > 0 {
for i := 0; i < len(extra); i += 2 {
if i+1 < len(extra) {
entry.Extra[cast.String(extra[i])] = extra[i+1]
}
}
}
logger.Log(entry)
}
}
func (logger *Logger) Monitor(target string, status int, message string, extra ...any) {
if logger.CheckLevel(INFO) {
entry := GetEntry(reflect.TypeOf(&MonitorLog{})).(*MonitorLog)
logger.fillBase(entry.Base(), LogTypeMonitor)
entry.Target = target
entry.Status = status
entry.Message = message
if len(extra) > 0 {
for i := 0; i < len(extra); i += 2 {
if i+1 < len(extra) {
entry.Extra[cast.String(extra[i])] = extra[i+1]
}
}
}
logger.Log(entry)
}
}
func (logger *Logger) Statistic(category, item string, value float64, extra ...any) {
if logger.CheckLevel(INFO) {
entry := GetEntry(reflect.TypeOf(&StatisticLog{})).(*StatisticLog)
logger.fillBase(entry.Base(), LogTypeStatistic)
entry.Category = category
entry.Item = item
entry.Value = value
if len(extra) > 0 {
for i := 0; i < len(extra); i += 2 {
if i+1 < len(extra) {
entry.Extra[cast.String(extra[i])] = extra[i+1]
}
}
}
logger.Log(entry)
}
}

View File

@ -25,3 +25,40 @@ func TestDesensitization(t *testing.T) {
} }
logger.Log(data) // 应该在输出中脱敏 logger.Log(data) // 应该在输出中脱敏
} }
func TestDBLog(t *testing.T) {
logger := NewLogger(Config{
Level: "debug",
})
// 测试普通 DB 日志
logger.DB("mysql", "dsn...", "SELECT * FROM users", []any{1}, 10.5)
// 测试 DB 错误日志
logger.DBError("connection lost", "mysql", "dsn...", "SELECT * FROM users", []any{1}, 10.5)
// 测试合并后的 DB 方法带错误
logger.DB("mysql", "dsn...", "SELECT * FROM users", []any{1}, 10.5, "another error")
}
func TestRequestLog(t *testing.T) {
logger := NewLogger(Config{
Level: "debug",
})
req := &RequestLog{
Method: "GET",
Path: "/api/user",
}
logger.Request(req)
}
func TestExtraLogs(t *testing.T) {
logger := NewLogger(Config{
Level: "debug",
})
logger.Task("CleanCache", 150.2, true, "Success clean")
logger.Monitor("CPU", 1, "Normal")
logger.Statistic("Business", "OrderCount", 100)
}

View File

@ -308,3 +308,38 @@ func (logger *Logger) fillBase(base *BaseLog, logType string) {
base.ServerName = serverName base.ServerName = serverName
base.ServerIp = serverIp base.ServerIp = serverIp
} }
func (logger *Logger) DB(dbType, dsn, query string, args []any, usedTime float32, errStr ...string) {
logType := LogTypeDb
level := INFO
var e string
if len(errStr) > 0 && errStr[0] != "" {
logType = LogTypeDbError
level = ERROR
e = errStr[0]
}
if logger.CheckLevel(level) {
entry := GetEntry(reflect.TypeOf(&DBLog{})).(*DBLog)
logger.fillBase(entry.Base(), logType)
entry.DbType = dbType
entry.Dsn = dsn
entry.Query = query
entry.QueryArgs = cast.MustToJSON(args)
entry.UsedTime = usedTime
if e != "" {
entry.Error = e
entry.CallStacks = getCallStacks(logger.truncations)
}
logger.Log(entry)
}
}
func (logger *Logger) DBError(errStr, dbType, dsn, query string, args []any, usedTime float32) {
logger.DB(dbType, dsn, query, args, usedTime, errStr)
}
func (logger *Logger) Request(entry *RequestLog) {
logger.fillBase(entry.Base(), LogTypeRequest)
logger.Log(entry)
}

View File

@ -115,6 +115,8 @@ type DBLog struct {
Query string Query string
QueryArgs string QueryArgs string
UsedTime float32 UsedTime float32
Error string
CallStacks []string
} }
func (d *DBLog) Reset() { func (d *DBLog) Reset() {
@ -124,6 +126,8 @@ func (d *DBLog) Reset() {
d.Query = "" d.Query = ""
d.QueryArgs = "" d.QueryArgs = ""
d.UsedTime = 0 d.UsedTime = 0
d.Error = ""
d.CallStacks = d.CallStacks[:0]
} }
func (d *DBLog) Base() *BaseLog { func (d *DBLog) Base() *BaseLog {

View File

@ -138,13 +138,21 @@ func getCallStacks(truncations []string) []string {
if strings.Contains(file, "/go/src/") { if strings.Contains(file, "/go/src/") {
continue continue
} }
if strings.Contains(file, "/log/") { // 注意这里的路径匹配,迁移后是 /log/ // 只有在 logger.go, extra.go 等核心实现文件中的帧才被认为是 "inLogger"
// 这样可以保留测试文件 (xxx_test.go) 的调用栈
isLogInternal := (strings.Contains(file, "/log/logger.go") ||
strings.Contains(file, "/log/utility.go") ||
strings.Contains(file, "/log/standard.go") ||
strings.Contains(file, "/log/extra.go"))
if isLogInternal {
if inLogger { if inLogger {
continue continue
} }
} else { } else {
inLogger = false inLogger = false
} }
if truncations != nil { if truncations != nil {
for _, truncation := range truncations { for _, truncation := range truncations {
if pos := strings.Index(file, truncation); pos != -1 { if pos := strings.Index(file, truncation); pos != -1 {