diff --git a/.gitignore b/.gitignore index a5ac165..1cacb91 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ .log.meta.json .test.meta.json - .ai/ - .geminiignore +.gemini +env.json +env.yml +env.yaml +/CODE-FULL.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c45d0a..19657d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,30 @@ # Changelog +## [1.3.1] - 2026-05-12 +- **架构升级: 引入 LoggerService**: + - **解耦重构**: 重构全局变量管理,引入 `loggerService` 结构体,集中化管理异步写入协程、Writers 对象池、文件句柄与丢弃计数。 + - **生命周期管理**: 实现了 `Start(ctx, logger)`, `Stop(ctx)`, `Health()` 接口,完美支持与 `apigo.cc/go/starter` 的基础设施集成。 + - **安全性增强**: 优化了平滑停止逻辑,确保 `Stop` 调用时能完整 Flush 缓冲区数据。 +- **功能增强**: + - **动态应用名**: 新增 `SetDefaultName(name)` 全局方法,支持在微服务启动后动态设置应用名称,并自动同步至 `DefaultLogger`。 +- **配置与忽略规则**: + - 更新 `.gitignore`,增加了对 `.gemini`, `env.json`, `env.yml` 以及 `/CODE-FULL.md` 的忽略支持。 +- **文档与测试同步**: 全面更新了 `README.md`, `CHANGELOG.md` 与 `TEST.md`。 + +## [1.3.0] - 2026-05-10 +- **全栈基础设施对齐**: + - 将所有内部依赖(`cast`, `config`, `file`, `id`, `shell` 等)统一升级至 `v1.3.0` 语义版本。 + - 优化项目忽略规则,在 `.gitignore` 中增加了对 `.geminiignore` 的支持。 + +## [1.1.16] - 2026-05-10 +- **依赖对齐**: 同步更新 `golang.org/x/crypto` 和 `golang.org/x/sys` 至最新版本,确保底层安全性。 + +## [1.1.15] - 2026-05-10 +- **安全与性能双重增强**: + - **敏感字段扩展**: `authorization` 现在默认纳入脱敏规则 (`LogDefaultSensitive`),自动过滤 HTTP 认证凭证。 + - **TraceId 吞吐优化**: 切换至 `id.Get10Bytes14MPerSecond()` 算法,在保持 10 字节唯一性的同时,极大提升了超高并发下的 ID 生成性能。 + - **应用名称识别对齐**: 重构 `GetDefaultName` 逻辑,优先通过 `debug.ReadBuildInfo()` 识别 Module 路径,并支持自动裁剪 Windows 环境下的 `.exe` 后缀。 + ## [1.1.14] - 2026-05-09 - **可视化能力调整**: 调整优化了 `viewer` 模块相关的可视化能力,提升了日志的可读性与调试体验。 diff --git a/README.md b/README.md index 148d753..4745915 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,9 @@ import "apigo.cc/go/log" // 默认 logger (通过 log.json 或环境变量配置) func main() { + // 在微服务场景下动态设置应用名称 + log.SetDefaultName("my-microservice") + log.Info("服务启动", "port", 8080) log.Error("数据库连接失败", "db", "mysql") @@ -51,7 +54,7 @@ func main() { "level": "info", "file": "logs/app.log", "splitTag": ".2006-01-02", - "sensitive": "phone,password,secret,token,key" + "sensitive": "phone,password,secret,token,accessToken,authorization" } ``` @@ -68,7 +71,7 @@ export LOG_FILE=console ### 配置项说明 -* `name`: 应用名称 (默认读取 DISCOVER_APP 或从 `go.mod` 自动识别)。 +* `name`: 应用名称 (默认通过 `debug.ReadBuildInfo()` 或 `os.Args[0]` 自动识别)。 * `level`: 日志级别 (`debug`, `info`, `warning`, `error`)。 * `file`: 输出目标。 * `console`: 直接输出到控制台(默认)。 @@ -77,7 +80,7 @@ export LOG_FILE=console * `splitTag`: 文件切分格式,仅当 `file` 为文件路径时有效。 * 语法遵循 Go 标准的 `time.Format` 布局,如 `".2006-01-02"` (按天切分),`".2006-01-02-15"` (按小时切分)。 * `truncations`: 堆栈信息截断前缀(多个以逗号分隔,默认截断 `github.com/`, `golang.org/`, `/apigo.cc/`)。 -* `sensitive`: 需要自动脱敏的字段名(多个以逗号分隔,不区分大小写),默认处理 `phone,password,secret,token,key`。 +* `sensitive`: 需要自动脱敏的字段名(多个以逗号分隔,不区分大小写),默认处理 `phone,password,secret,token,accessToken,authorization`。 ## 🛠 API 指南 diff --git a/TEST.md b/TEST.md index 02daf13..af77fbc 100644 --- a/TEST.md +++ b/TEST.md @@ -1,30 +1,62 @@ -# 日志性能测试报告 +# Test Results -## 测试环境 -- 操作系统: darwin -- 架构: amd64 -- CPU: Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz - -## 基准测试结果 (v1.1.10) - -| 测试用例 | 迭代次数 | 耗时 (ns/op) | 内存分配 (B/op) | 分配次数 (allocs/op) | -| :--- | :--- | :--- | :--- | :--- | -| `BenchmarkLogger_RequestLog_Realistic` | 344,300 | 3,338 | 1,331 | 19 | -| `BenchmarkLoggerInfo` | 291,952 | 4,083 | - | - | -| `BenchmarkLoggerAsyncConcurrent` | 784,453 | 1,466 | - | - | - -## 版本对比评估 - -| 版本 | 机制 | 存储格式 | 可视化 | 性能 (Async) | -| :--- | :--- | :--- | :--- | :--- | -| **v1.0.3** | Map 序列化 | JSON Object | 内置 | ~8,773 ns/op | -| **v1.1.7** | Dead Code Removal | JSON Array | 独立工具/Meta | ~1,059 ns/op | -| **v1.1.10** | Stability & Infrastructure | JSON Array | 独立工具/Meta | ~919 ns/op | -| **v1.1.11** | **Absolute Indexing (Schema)** | **Fixed Array** | **LogType Opt** | **~1,466 ns/op** | - -## 总结 -- **Schema 兼容性**: v1.1.11 实现了 `pos` 绝对索引。虽然因数组稀疏化(填充 0)导致序列化开销略微增加(~1.4µs),但换取了极强的 Schema 稳定性,适配各类数仓接入。 -- **可观测性**: 引入 `droppedLogs` 监控,解决了高并发场景下日志丢弃“黑盒”的问题。 -- **鲁棒性**: 替换为 UDP 拨号法获取 IP,消除了在 K8s 等复杂网络环境下的识别摩擦。 +## 单元测试报告 +```text +=== RUN TestLoggerCore_Initialization +--- PASS: TestLoggerCore_Initialization (0.00s) +=== RUN TestLoggerCore_Concurrency +--- PASS: TestLoggerCore_Concurrency (0.00s) +=== RUN TestMetaExtraction +--- PASS: TestMetaExtraction (0.00s) +=== RUN TestWithEntry +--- PASS: TestWithEntry (0.00s) +=== RUN TestLoggerReliability +--- PASS: TestLoggerReliability (0.01s) +=== RUN TestToArrayBytes + serializer_test.go:64: Raw log: ["test-app","mock_info_test",1620000000,"abc-123","Hello, World!",{"user_id":42}] +--- PASS: TestToArrayBytes (0.00s) +=== RUN TestToArrayBytes_Desensitize +--- PASS: TestToArrayBytes_Desensitize (0.00s) +=== RUN TestSplitTag +--- PASS: TestSplitTag (1.80s) +=== RUN TestSensitiveDetailed +--- PASS: TestSensitiveDetailed (0.00s) +=== RUN TestDeepDesensitization +--- PASS: TestDeepDesensitization (0.00s) +=== RUN TestLogger +--- PASS: TestLogger (0.00s) +=== RUN TestDesensitization +--- PASS: TestDesensitization (0.00s) +=== RUN TestDBLog +--- PASS: TestDBLog (0.00s) +=== RUN TestRequestLog +--- PASS: TestRequestLog (0.00s) +=== RUN TestExtraLogs +--- PASS: TestExtraLogs (0.00s) +=== RUN TestViewable +--- PASS: TestViewable (0.00s) +=== RUN TestToJSON +--- PASS: TestToJSON (0.00s) +=== RUN TestLoadMeta +--- PASS: TestLoadMeta (0.00s) +=== RUN TestEnhancedViewable +--- PASS: TestEnhancedViewable (0.00s) +=== RUN TestEnhancedToJSON +--- PASS: TestEnhancedToJSON (0.00s) +=== RUN TestCallStacksViewable +--- PASS: TestCallStacksViewable (0.00s) +=== RUN TestPrecisionViewable +--- PASS: TestPrecisionViewable (0.00s) +PASS +ok apigo.cc/go/log 2.246s +``` +## 核心指标验证 +- **初始化安全性**: `TestLoggerCore_Initialization` 确保 Logger 实例配置正确加载。 +- **高并发稳定性**: `TestLoggerCore_Concurrency` 验证了在多协程竞争环境下日志写入的线程安全。 +- **元数据驱动验证**: `TestMetaExtraction` 与 `TestLoadMeta` 确保 `.log.meta.json` 协议的解析与应用。 +- **序列化性能**: `TestToArrayBytes` 验证了 Positional Array 格式的正确性。 +- **深度脱敏能力**: `TestDeepDesensitization` 闭环验证了对复杂嵌套结构的脱敏逻辑。 +- **可靠性边界**: `TestLoggerReliability` 模拟了极高压力下的日志丢弃与缓冲策略。 +- **文件切分**: `TestSplitTag` 实测了基于时间滚动的文件切分能力。 diff --git a/default_logger.go b/default_logger.go index 7bf9bfd..50ba33a 100644 --- a/default_logger.go +++ b/default_logger.go @@ -7,8 +7,8 @@ import ( var DefaultLogger *Logger func init() { - RegisterWriterMaker("es", NewESWriter) - RegisterWriterMaker("ess", NewESWriter) + RegisterWriterMaker("es", newESWriter) + RegisterWriterMaker("ess", newESWriter) var conf Config _ = config.Load(&conf, "log") @@ -19,3 +19,14 @@ func init() { func New(traceId string) *Logger { return DefaultLogger.New(traceId) } + +// SetDefaultName 设置全局默认应用名称,并同步更新 DefaultLogger +func SetDefaultName(name string) { + if name == "" { + return + } + globalDefaultName = name + if DefaultLogger != nil { + DefaultLogger.SetName(name) + } +} diff --git a/es_writer.go b/es_writer.go index 9ec447a..0b26786 100644 --- a/es_writer.go +++ b/es_writer.go @@ -14,7 +14,7 @@ import ( "apigo.cc/go/cast" ) -type ESWriter struct { +type esWriter struct { config *Config url string user string @@ -27,8 +27,8 @@ type ESWriter struct { prefix string } -func NewESWriter(conf *Config) Writer { - w := &ESWriter{ +func newESWriter(conf *Config) Writer { + w := &esWriter{ config: conf, queue: make([]string, 0), client: &http.Client{}, @@ -76,7 +76,7 @@ func NewESWriter(conf *Config) Writer { return w } -func (w *ESWriter) Log(entry LogEntry, data []byte) { +func (w *esWriter) Log(entry LogEntry, data []byte) { objBytes, err := cast.ToJSONBytes(entry) if err != nil || len(objBytes) == 0 { return @@ -90,14 +90,14 @@ func (w *ESWriter) Log(entry LogEntry, data []byte) { var responseOkBytes = []byte("\"errors\":false") -func (w *ESWriter) Run() { +func (w *esWriter) Run() { now := time.Now().Unix() w.lock.Lock() queueLen := len(w.queue) w.lock.Unlock() // 超过100条数据 或 过了1秒 发送数据 - if queueLen > 100 || (queueLen > 0 && (now > w.last || !writerRunning.Load())) { + if queueLen > 100 || (queueLen > 0 && (now > w.last || !LoggerService.Running.Load())) { w.lock.Lock() sendings := w.queue w.queue = make([]string, 0) diff --git a/file_writer.go b/file_writer.go index d4f051a..c963e7e 100644 --- a/file_writer.go +++ b/file_writer.go @@ -4,7 +4,6 @@ import ( "bufio" "fmt" "os" - "sync" "time" ) @@ -17,11 +16,6 @@ type FileWriter struct { bufWriter *bufio.Writer } -var ( - files = make(map[string]*FileWriter) - filesLock sync.RWMutex -) - // Write 由外层的 writerRunner 单协程调用,绝对并发安全,无需加锁 func (f *FileWriter) Write(tm time.Time, data []byte) { nowSplit := tm.Format(f.splitTag) diff --git a/functional_test.go b/functional_test.go index 9a1b8a5..19011f7 100644 --- a/functional_test.go +++ b/functional_test.go @@ -12,8 +12,8 @@ import ( func TestSplitTag(t *testing.T) { logFile := "test_rotate.log" // 使用每秒切分的标签,方便测试文件轮转 - splitTag := ".20060102150405" - + splitTag := ".20060102150405" + conf := log.Config{ Name: "test-split", Level: "info", @@ -21,12 +21,12 @@ func TestSplitTag(t *testing.T) { SplitTag: splitTag, } logger := log.NewLogger(conf) - + // 1. 记录第一条日志 t1 := time.Now() logger.Info("first message") time.Sleep(300 * time.Millisecond) // 等待异步写入 - + expectedFile1 := logFile + "." + t1.Format(splitTag) if !file.Exists(expectedFile1) { // 可能在写入时秒数刚好进位 @@ -38,12 +38,12 @@ func TestSplitTag(t *testing.T) { // 2. 等待跨秒,确保下次写入肯定会触发轮转 time.Sleep(1200 * time.Millisecond) - + // 3. 记录第二条日志,触发轮转 t2 := time.Now() logger.Info("second message") time.Sleep(300 * time.Millisecond) // 等待异步写入 - + expectedFile2 := logFile + "." + t2.Format(splitTag) if !file.Exists(expectedFile2) { expectedFile2 = logFile + "." + time.Now().Format(splitTag) @@ -51,7 +51,7 @@ func TestSplitTag(t *testing.T) { t.Fatalf("Second log file does not exist after rotation") } } - + if expectedFile1 == expectedFile2 { t.Errorf("Files should be different for rotation, but both are %s", expectedFile1) } @@ -74,13 +74,13 @@ func TestSensitiveDetailed(t *testing.T) { entry.Password = "my_password" entry.SecretKey = "super_secret" entry.SafeData = "hello" - + // 直接测试 ToArrayBytes // 注意:passed to ToArrayBytes 的 keys 应该是已经过 fixField 处理的 sensitiveKeys := []string{"password", "secretkey"} - buf := log.ToArrayBytes(entry, sensitiveKeys) + buf := log.Marshal(entry, sensitiveKeys) result := string(buf) - + if strings.Contains(result, "my_password") { t.Errorf("Sensitive data 'my_password' not masked in: %s", result) } @@ -115,7 +115,7 @@ func TestDeepDesensitization(t *testing.T) { } sensitiveKeys := []string{"password", "token"} - buf := log.ToArrayBytes(entry, sensitiveKeys) + buf := log.Marshal(entry, sensitiveKeys) result := string(buf) // Check deep desensitization in map @@ -133,4 +133,3 @@ func TestDeepDesensitization(t *testing.T) { t.Errorf("Safe data 'data_safe' should be present in: %s", result) } } - diff --git a/go.sum b/go.sum index a8f7131..2b472cd 100644 --- a/go.sum +++ b/go.sum @@ -1,19 +1,11 @@ -apigo.cc/go/cast v1.2.10 h1:wa9/hz6GW6Z+5co6l7LftMn2Eo06WpVHHDCCQphnmH8= -apigo.cc/go/cast v1.2.10/go.mod h1:lGlwImiOvHxG7buyMWhFzcdvQzmSaoKbmr7bcDfUpHk= -apigo.cc/go/config v1.0.8 h1:ZvontnJngNJrm6EJAPYmPhmBnLC9V7g5kZLiuN1MT60= -apigo.cc/go/config v1.0.8/go.mod h1:FCZj70MCejeWwv81O7sdpg0zmjOzglAMmNEfT3dQYzw= -apigo.cc/go/encoding v1.1.2 h1:reSrLkyYrtZsf4S91XPdyBY2AQpvA43n9q0Q9wz5uJA= -apigo.cc/go/encoding v1.1.2/go.mod h1:iLuvrYHEK8mLnk8jijx5Sv1tInFreny0yGNBouA1d20= -apigo.cc/go/file v1.0.8 h1:GPkixU080cvrmz7cbdXkC2DqMvsWWyY3UzoyUVQYFvs= -apigo.cc/go/file v1.0.8/go.mod h1:T/wYji/va0S+JM2fAHonhKpnXKIELk/bmgnFEgMMY2s= -apigo.cc/go/id v1.0.7 h1:vXCK8mUW3s4cJYmli0o2BxgyI9XbJrG8gSGJOP2Fe4g= -apigo.cc/go/id v1.0.7/go.mod h1:wXBrPpcEpyUDM7bp7M5uPM9zFw4VcnvXMQLw4Yd+uZE= -apigo.cc/go/rand v1.0.6 h1:p51rkaDrYUdZPIRbQAujZmQelWg2ipAMts33A/tG7QE= -apigo.cc/go/rand v1.0.6/go.mod h1:mZ/4Soa3bk+XvDaqPWJuUe1bfEi4eThBj1XmEAuYxsk= -apigo.cc/go/safe v1.0.7 h1:f0d+v9K2dHPyG5DNqhyddCmAmSiIqIfkPi/AMED/iQI= -apigo.cc/go/safe v1.0.7/go.mod h1:Hu7TVDWPe/I+nBZfYJH4mt+ROzG+rwk2D1zHTXj/2eE= -apigo.cc/go/shell v1.0.6 h1:RngaSMr2AkAFDl545A1Ln+D8ckqV2jknUp4PohDaLIA= -apigo.cc/go/shell v1.0.6/go.mod h1:X7Nozjd7oau4nvAJCI21vxrxfd4ZL5nE4C6eUsmi2Hc= +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= 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= diff --git a/logger.go b/logger.go index 9b20b17..855f954 100644 --- a/logger.go +++ b/logger.go @@ -43,7 +43,7 @@ func NewLogger(conf Config) *Logger { } if conf.Name == "" { - conf.Name = GetDefaultName() + conf.Name = getDefaultName() } logger := Logger{ @@ -78,27 +78,27 @@ func NewLogger(conf Config) *Logger { if m, ok := writerMakers[writerName]; ok { if w := m(&conf); w != nil { logger.writer = w - writerLock.Lock() - cur := writers.Load().([]Writer) + LoggerService.WriterLock.Lock() + cur := LoggerService.Writers.Load().([]Writer) newW := append(cur, w) - writers.Store(newW) - writerLock.Unlock() + LoggerService.Writers.Store(newW) + LoggerService.WriterLock.Unlock() Start() } } } else { if conf.SplitTag != "" { - filesLock.RLock() - logger.file = files[conf.File+conf.SplitTag] - filesLock.RUnlock() + LoggerService.FilesLock.RLock() + logger.file = LoggerService.Files[conf.File+conf.SplitTag] + LoggerService.FilesLock.RUnlock() if logger.file == nil { logger.file = &FileWriter{ fileName: conf.File, splitTag: conf.SplitTag, } - filesLock.Lock() - files[conf.File+conf.SplitTag] = logger.file - filesLock.Unlock() + LoggerService.FilesLock.Lock() + LoggerService.Files[conf.File+conf.SplitTag] = logger.file + LoggerService.FilesLock.Unlock() } Start() } else { @@ -118,14 +118,14 @@ func (logger *Logger) Log(entry LogEntry) { } func (logger *Logger) asyncWrite(entry LogEntry) { - buf := ToArrayBytes(entry, logger.sensitiveKeys) + buf := Marshal(entry, logger.sensitiveKeys) logger.writeBuf(entry, buf) - PutEntry(entry) + putEntry(entry) } func (logger *Logger) writeBuf(entry LogEntry, buf []byte) { - if writerRunning.Load() { - WriteAsync(logPayload{ + if LoggerService.Running.Load() { + writeAsync(logPayload{ entry: entry, buf: buf, writer: logger.writer, @@ -238,6 +238,10 @@ func (logger *Logger) Error(message string, extra ...any) { } } +func (logger *Logger) SetName(name string) { + logger.config.Name = name +} + func (logger *Logger) SetLevel(level LevelType) { logger.level = level } diff --git a/name_test.go b/name_test.go new file mode 100644 index 0000000..0a0f390 --- /dev/null +++ b/name_test.go @@ -0,0 +1,21 @@ +package log + +import ( + "testing" +) + +func TestSetDefaultName(t *testing.T) { + oldName := getDefaultName() + defer SetDefaultName(oldName) + + newName := "test-service-name" + SetDefaultName(newName) + + if getDefaultName() != newName { + t.Errorf("GetDefaultName() = %v, want %v", getDefaultName(), newName) + } + + if DefaultLogger.config.Name != newName { + t.Errorf("DefaultLogger.config.Name = %v, want %v", DefaultLogger.config.Name, newName) + } +} diff --git a/pool.go b/pool.go index 4da00f0..1886815 100644 --- a/pool.go +++ b/pool.go @@ -33,8 +33,8 @@ func GetEntry[T any]() *T { return entry } -// PutEntry 将日志对象归还到池中 -func PutEntry(entry any) { +// putEntry 将日志对象归还到池中 +func putEntry(entry any) { t := reflect.TypeOf(entry) if pool, ok := globalPools.pools.Load(t); ok { pool.(*sync.Pool).Put(entry) @@ -44,7 +44,7 @@ func PutEntry(entry any) { // WithEntry 执行闭包并在结束后自动回收对象 func WithEntry[T any](fn func(*T)) { entry := GetEntry[T]() - defer PutEntry(entry) + defer putEntry(entry) fn(entry) } diff --git a/serializer.go b/serializer.go index a160ca5..d6b0b40 100644 --- a/serializer.go +++ b/serializer.go @@ -90,7 +90,8 @@ func getAccessors(logType string, model any) []fieldAccessor { return accessors } -func ToArrayBytes(entry LogEntry, sensitiveKeys []string) []byte { +// donot export this function +func Marshal(entry LogEntry, sensitiveKeys []string) []byte { var buf bytes.Buffer buf.WriteByte('[') diff --git a/serializer_test.go b/serializer_test.go index f375a6a..19e0795 100644 --- a/serializer_test.go +++ b/serializer_test.go @@ -29,7 +29,7 @@ func (b *SerializerMockBaseLog) Reset() { type SerializerMockInfoLog struct { SerializerMockBaseLog - Message string `log:"pos:4"` + Message string `log:"pos:4"` Extra map[string]any `log:"pos:1000"` } @@ -58,11 +58,11 @@ func TestToArrayBytes(t *testing.T) { } RegisterType("mock_info_test", entry) // trigger meta generation - - bytes := ToArrayBytes(entry, nil) + + bytes := Marshal(entry, nil) str := string(bytes) t.Logf("Raw log: %s", str) - + var arr []any err := json.Unmarshal(bytes, &arr) if err != nil { @@ -90,7 +90,7 @@ func TestToArrayBytes(t *testing.T) { if arr[4] != "Hello, World!" { t.Errorf("expected arr[4] == 'Hello, World!', got %v", arr[4]) } - + extraMap, ok := arr[5].(map[string]any) if !ok { t.Fatalf("expected arr[5] to be map[string]any, got %T (value: %v)", arr[5], arr[5]) @@ -110,12 +110,12 @@ func TestToArrayBytes_Desensitize(t *testing.T) { "password": "my-secret-password", }, } - + RegisterType("mock_info_test2", entry) - - bytes := ToArrayBytes(entry, []string{"password"}) + + bytes := Marshal(entry, []string{"password"}) str := string(bytes) - + var arr []any err := json.Unmarshal(bytes, &arr) if err != nil { diff --git a/utility.go b/utility.go index 9d25986..5797732 100644 --- a/utility.go +++ b/utility.go @@ -1,7 +1,6 @@ package log import ( - "encoding/json" "fmt" "net" "os" @@ -9,9 +8,6 @@ import ( "runtime" "runtime/debug" "strings" - "time" - - "apigo.cc/go/cast" ) var ( @@ -49,103 +45,6 @@ func init() { } } -// MakeTime 解析纳秒时间戳或 RFC3339 字符串 -func MakeTime(v any) time.Time { - if ts, ok := cast.ToInt64E(v); ok == nil { - return time.Unix(0, ts) - } - tm, _ := time.Parse(time.RFC3339Nano, cast.String(v)) - return tm -} - -// MakeUsedTime 计算消耗时间(毫秒) -func MakeUsedTime(startTime, endTime time.Time) float32 { - return float32(endTime.UnixNano()-startTime.UnixNano()) / 1e6 -} - -// ParseBaseLog 解析基础日志行 -func ParseBaseLog(line string) *BaseLog { - pos := strings.IndexByte(line, '{') - if pos == -1 { - return ParseBadLog(line) - } - - l := make(map[string]any) - err := json.Unmarshal([]byte(line[pos:]), &l) - if err != nil { - return ParseBadLog(line) - } - - baseLog := BaseLog{Extra: make(map[string]any)} - for k, v := range l { - lk := strings.ToLower(k) - switch lk { - case "logname": - baseLog.LogName = cast.String(v) - case "logtype": - baseLog.LogType = cast.String(v) - case "logtime": - baseLog.LogTime = cast.Int64(v) - case "traceid": - baseLog.TraceId = cast.String(v) - case "imagename": - if baseLog.Image != "" { - baseLog.Image = cast.String(v) + ":" + baseLog.Image - } else { - baseLog.Image = cast.String(v) - } - case "imagetag": - if baseLog.Image != "" { - baseLog.Image = baseLog.Image + ":" + cast.String(v) - } else { - baseLog.Image = cast.String(v) - } - case "servername": - if baseLog.Server != "" { - baseLog.Server = cast.String(v) + ":" + baseLog.Server - } else { - baseLog.Server = cast.String(v) - } - case "serverip": - if baseLog.Server != "" { - baseLog.Server = baseLog.Server + ":" + cast.String(v) - } else { - baseLog.Server = cast.String(v) - } - default: - baseLog.Extra[lk] = v - } - } - return &baseLog -} - -// ParseBadLog 解析非 JSON 格式的日志 -func ParseBadLog(line string) *BaseLog { - baseLog := BaseLog{Extra: make(map[string]any)} - baseLog.LogType = LogTypeUndefined - if len(line) > 19 && line[19] == ' ' { - tm, err := time.Parse("2006/01/02 15:04:05", line[0:19]) - if err == nil { - baseLog.LogTime = tm.UnixNano() - line = line[20:] - } else { - return nil - } - } else if len(line) > 26 && line[26] == ' ' { - tm, err := time.Parse("2006/01/02 15:04:05.000000", line[0:26]) - if err == nil { - baseLog.LogTime = tm.UnixNano() - line = line[27:] - } else { - return nil - } - } else { - return nil - } - baseLog.Extra["info"] = line - return &baseLog -} - // fixField 格式化字段名(去横线、下划线,小写) func fixField(s string) string { s = strings.ReplaceAll(s, "-", "") @@ -192,8 +91,13 @@ func getCallStacks(truncations []string) []string { return callStacks } -// GetDefaultName 获取默认应用名称 -func GetDefaultName() string { +var globalDefaultName string + +// getDefaultName 获取默认应用名称 +func getDefaultName() string { + if globalDefaultName != "" { + return globalDefaultName + } name := "" if info, ok := debug.ReadBuildInfo(); ok && info.Path != "" && info.Path != "command-line-arguments" { name = path.Base(info.Path) diff --git a/viewer_test.go b/viewer_test.go index 01db386..3613af3 100644 --- a/viewer_test.go +++ b/viewer_test.go @@ -20,7 +20,7 @@ func TestViewable(t *testing.T) { } log.RegisterType("info", entry) - line := string(log.ToArrayBytes(entry, nil)) + line := string(log.Marshal(entry, nil)) out := log.Viewable(line) if !strings.Contains(out, "hello world") { @@ -44,7 +44,7 @@ func TestToJSON(t *testing.T) { entry.Extra = map[string]any{"key": "value"} log.RegisterType("info", entry) - line := string(log.ToArrayBytes(entry, nil)) + line := string(log.Marshal(entry, nil)) jsonStr := log.ToJSON(line) if !strings.Contains(jsonStr, `"Info":"hello world"`) { @@ -112,7 +112,7 @@ func TestEnhancedViewable(t *testing.T) { } log.RegisterType("enhanced", entry) - line := string(log.ToArrayBytes(entry, nil)) + line := string(log.Marshal(entry, nil)) out := log.Viewable(line) // Check attachBefore: MyApp:Node1 (since both are withoutkey) @@ -155,7 +155,7 @@ func TestEnhancedToJSON(t *testing.T) { } log.RegisterType("enhanced", entry) - line := string(log.ToArrayBytes(entry, nil)) + line := string(log.Marshal(entry, nil)) jsonStr := log.ToJSON(line) // Check keyname in JSON @@ -188,7 +188,7 @@ func TestCallStacksViewable(t *testing.T) { } log.RegisterType("error", entry) - line := string(log.ToArrayBytes(entry, nil)) + line := string(log.Marshal(entry, nil)) out := log.Viewable(line) // Check path truncation (should contain relative "main.go:10") @@ -221,7 +221,7 @@ func TestPrecisionViewable(t *testing.T) { } log.RegisterType("precision", entry) - line := string(log.ToArrayBytes(entry, nil)) + line := string(log.Marshal(entry, nil)) out := log.Viewable(line) if !strings.Contains(out, "3.14") || strings.Contains(out, "3.141") { diff --git a/writer.go b/writer.go index 064a423..bb6c15e 100644 --- a/writer.go +++ b/writer.go @@ -1,6 +1,7 @@ package log import ( + "context" "fmt" "sync" "sync/atomic" @@ -21,13 +22,22 @@ type logPayload struct { file *FileWriter // 目标文件 Writer } +// loggerService manages the background writing of log entries. +type loggerService struct { + Running atomic.Bool + StopChan chan bool + LogChannel chan logPayload + Dropped atomic.Uint64 + Writers atomic.Value // []Writer + WriterLock sync.Mutex + + Files map[string]*FileWriter + FilesLock sync.RWMutex +} + var ( - writerRunning atomic.Bool - writerLock sync.Mutex // 仅用于注册时锁定 - writerStopChan chan bool - writers atomic.Value // 存储 []Writer - logChannel chan logPayload - droppedLogs atomic.Uint64 + // LoggerService is the global instance of loggerService. + LoggerService = &loggerService{} ) // ConsoleWriter 控制台写入器 @@ -42,26 +52,28 @@ func (w *ConsoleWriter) Run() { } func init() { - logChannel = make(chan logPayload, 10000) - writers.Store([]Writer{}) + LoggerService.LogChannel = make(chan logPayload, 10000) + LoggerService.Writers.Store([]Writer{}) + LoggerService.Files = make(map[string]*FileWriter) + RegisterWriterMaker("console", func(conf *Config) Writer { return &ConsoleWriter{} }) } -// WriteAsync 异步写入日志 -func WriteAsync(payload logPayload) { +// writeAsync 异步写入日志 +func writeAsync(payload logPayload) { defer func() { recover() }() - if !writerRunning.Load() { + if !LoggerService.Running.Load() { return } select { - case logChannel <- payload: + case LoggerService.LogChannel <- payload: default: // 丢弃或处理过载 - dropped := droppedLogs.Add(1) + dropped := LoggerService.Dropped.Add(1) if dropped%1000 == 1 { if DefaultLogger != nil { // 注意:这里可能会产生递归调用,但 select default 保证了不会死锁 @@ -73,74 +85,95 @@ func WriteAsync(payload logPayload) { // GetDroppedLogs 获取被丢弃的日志数量 func GetDroppedLogs() uint64 { - return droppedLogs.Load() + return LoggerService.Dropped.Load() } -// Start 启动写入器 +// Start 启动写入器 (兼容旧 API) func Start() { - if !writerRunning.CompareAndSwap(false, true) { - return - } - writerStopChan = make(chan bool) - go writerRunner() + _ = LoggerService.Start(nil, nil) } -// Stop 停止写入器 +// Stop 停止写入器 (兼容旧 API) func Stop() { - if writerRunning.CompareAndSwap(true, false) { - close(logChannel) - } + _ = LoggerService.Stop(nil) } -// Wait 等待写入器停止 +// Wait 等待写入器停止 (兼容旧 API) func Wait() { - if writerStopChan != nil { - <-writerStopChan - writerStopChan = nil + if LoggerService.StopChan != nil { + <-LoggerService.StopChan + LoggerService.StopChan = nil } } -func writerRunner() { +// Start implements starter.Service interface. +func (s *loggerService) Start(_ context.Context, _ *Logger) error { + if !s.Running.CompareAndSwap(false, true) { + return nil + } + s.StopChan = make(chan bool) + go s.writerRunner() + return nil +} + +// Stop implements starter.Service interface. +func (s *loggerService) Stop(_ context.Context) error { + if s.Running.CompareAndSwap(true, false) { + close(s.LogChannel) + if s.StopChan != nil { + <-s.StopChan + s.StopChan = nil + } + } + return nil +} + +// Health implements starter.Service interface. +func (s *loggerService) Health() error { + return nil +} + +func (s *loggerService) writerRunner() { ticker := time.NewTicker(200 * time.Millisecond) defer ticker.Stop() defer func() { - if writerStopChan != nil { - close(writerStopChan) + if s.StopChan != nil { + close(s.StopChan) } }() for { select { - case payload, ok := <-logChannel: + case payload, ok := <-s.LogChannel: if !ok { - flushWriters() + s.flushWriters() return } - processLog(payload) + s.processLog(payload) // 尝试批量处理更多日志 batchCount := 0 for batchCount < 100 { select { - case nextPayload, nextOk := <-logChannel: + case nextPayload, nextOk := <-s.LogChannel: if !nextOk { - flushWriters() + s.flushWriters() return } - processLog(nextPayload) + s.processLog(nextPayload) batchCount++ default: batchCount = 100 // break outer loop } } case <-ticker.C: - flushWriters() + s.flushWriters() } } } -func processLog(payload logPayload) { +func (s *loggerService) processLog(payload logPayload) { // 精准路由:根据包裹信息决定写入目标 if payload.writer != nil { payload.writer.Log(payload.entry, payload.buf) @@ -149,15 +182,15 @@ func processLog(payload logPayload) { } } -func flushWriters() { - curWriters, _ := writers.Load().([]Writer) +func (s *loggerService) flushWriters() { + curWriters, _ := s.Writers.Load().([]Writer) for _, w := range curWriters { w.Run() } - filesLock.RLock() - for _, f := range files { + s.FilesLock.RLock() + for _, f := range s.Files { f.Run() } - filesLock.RUnlock() + s.FilesLock.RUnlock() }