feat: enhance DBLog with error/stack and add extra log formats (v1.0.1) (by AI)
This commit is contained in:
parent
80aa4aaa49
commit
f9e3a2ec5a
@ -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` 复用,支持零分配日志对象。
|
||||
|
||||
46
README.md
46
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)
|
||||
|
||||
19
TEST.md
19
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.
|
||||
118
extra.go
Normal file
118
extra.go
Normal 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)
|
||||
}
|
||||
}
|
||||
37
log_test.go
37
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)
|
||||
}
|
||||
|
||||
35
logger.go
35
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)
|
||||
}
|
||||
|
||||
14
standard.go
14
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 {
|
||||
|
||||
10
utility.go
10
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 {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user