From f9e3a2ec5ad5d682d6ab52a7482b0da6155e9aa5 Mon Sep 17 00:00:00 2001 From: AI Engineer Date: Mon, 4 May 2026 01:14:46 +0800 Subject: [PATCH] feat: enhance DBLog with error/stack and add extra log formats (v1.0.1) (by AI) --- CHANGELOG.md | 7 +++ README.md | 46 ++++++++++++++------ TEST.md | 19 +++++++++ extra.go | 118 +++++++++++++++++++++++++++++++++++++++++++++++++++ log_test.go | 37 ++++++++++++++++ logger.go | 35 +++++++++++++++ standard.go | 14 +++--- utility.go | 10 ++++- 8 files changed, 267 insertions(+), 19 deletions(-) create mode 100644 extra.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 40eec11..a4f8c84 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # 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 - **初始版本**: 由 `ssgo/log` 迁移并基于 `apigo.cc/go` 标准重构。 - **高性能引擎**: 引入 `LogEntry` 池化与 `sync.Pool` 复用,支持零分配日志对象。 diff --git a/README.md b/README.md index f794991..476a1dd 100644 --- a/README.md +++ b/README.md @@ -14,21 +14,41 @@ go get apigo.cc/go/log ``` -## 快速开始 -```go -import "apigo.cc/go/log" +## 标准化日志 API -func main() { - // 使用默认 Logger - log.Info("server started", "port", 8080) - - // 创建带 traceId 的子 Logger - logger := log.New("unique-trace-id") - logger.Info("request processed") - - // 错误日志带堆栈 - logger.Error("database failed", "db", "mysql") +除了基础的 `Debug`, `Info`, `Warning`, `Error` 外,`go/log` 还提供了一系列针对特定场景优化的标准化日志 API: + +### 数据库日志 (DB) +自动处理耗时计算、脱敏及错误堆栈捕获。 +```go +// 记录正常 SQL +logger.DB("mysql", dsn, "SELECT * FROM users WHERE id=?", []any{1}, 10.5) + +// 记录带错误的 SQL (自动捕获调用栈并设为 dbError 类型) +logger.DB("mysql", dsn, "SELECT...", args, usedTime, "table not found") +``` + +### 请求日志 (Request) +针对高性能 HTTP 服务设计的结构化日志。 +```go +req := &log.RequestLog{ + Method: "GET", + 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) diff --git a/TEST.md b/TEST.md index e69de29..272b53a 100644 --- a/TEST.md +++ b/TEST.md @@ -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. diff --git a/extra.go b/extra.go new file mode 100644 index 0000000..68e46fa --- /dev/null +++ b/extra.go @@ -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) + } +} diff --git a/log_test.go b/log_test.go index 9d9102b..aa9c330 100644 --- a/log_test.go +++ b/log_test.go @@ -25,3 +25,40 @@ func TestDesensitization(t *testing.T) { } 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) +} diff --git a/logger.go b/logger.go index 9dd8c8b..7e692c7 100644 --- a/logger.go +++ b/logger.go @@ -308,3 +308,38 @@ func (logger *Logger) fillBase(base *BaseLog, logType string) { base.ServerName = serverName 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) +} diff --git a/standard.go b/standard.go index 76e99a1..e797faa 100644 --- a/standard.go +++ b/standard.go @@ -110,11 +110,13 @@ func (e *ErrorLog) Base() *BaseLog { type DBLog struct { BaseLog - DbType string - Dsn string - Query string - QueryArgs string - UsedTime float32 + DbType string + Dsn string + Query string + QueryArgs string + UsedTime float32 + Error string + CallStacks []string } func (d *DBLog) Reset() { @@ -124,6 +126,8 @@ func (d *DBLog) Reset() { d.Query = "" d.QueryArgs = "" d.UsedTime = 0 + d.Error = "" + d.CallStacks = d.CallStacks[:0] } func (d *DBLog) Base() *BaseLog { diff --git a/utility.go b/utility.go index 46e22b4..7819463 100644 --- a/utility.go +++ b/utility.go @@ -138,13 +138,21 @@ func getCallStacks(truncations []string) []string { if strings.Contains(file, "/go/src/") { 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 { continue } } else { inLogger = false } + if truncations != nil { for _, truncation := range truncations { if pos := strings.Index(file, truncation); pos != -1 {