# starlog 使用指南 本文档覆盖从接入到生产落地的常见场景。 - 模块:`b612.me/starlog` - Go 版本:`1.16+` ## 1. 快速开始 ```go package main import ( "os" "b612.me/starlog" ) func main() { log := starlog.NewStarlog(os.Stdout) log.SetName("demo") log.SetShowStd(true) log.SetColorMode(starlog.ColorModeLevelOnly) log.Info("service start") log.WithField("user_id", 42).Info("login ok") } ``` ## 2. 推荐初始化方式 ### 2.1 开发环境 ```go log := starlog.NewDevelopment(os.Stdout) log.SetName("my-service") log.SetShowStd(true) ``` 特点: - 默认 `Debug` 级别 - 颜色模式:`ColorModeLevelOnly` - 显示源码位置 ### 2.2 生产环境 ```go log := starlog.NewProduction(nil) log.SetName("my-service") if err := starlog.SetLogFile("./logs/app.log", log, true); err != nil { panic(err) } ``` 特点: - 默认 `Info` 级别 - 默认 `JSONFormatter` - 默认不直出控制台 ### 2.3 对现有 logger 套用预设 ```go log.ApplyProductionConfig() // 或 log.ApplyDevelopmentConfig() ``` ## 3. 输出 API 与换行规则 ### 3.1 输出函数 - `Info/Error/...`:`fmt.Sprint` 语义 - `Infof/Errorf/...`:`fmt.Sprintf` 语义 - `Infoln/Errorln/...`:`fmt.Sprintln` 语义 默认不会自动补 `\n`(`ln` 系列除外)。 ### 3.2 自动补换行 ```go log.SetAutoAppendNewline(true) ``` 行为: - 末尾无 `\n`:自动补一个 - 末尾有 `\n`:保持不变 ## 4. 结构化日志 ### 4.1 字段与错误 ```go log.WithField("order_id", "O-1001").Info("create order") log.WithFields(starlog.Fields{ "trace_id": "t-001", "module": "payment", }).WithError(err).Error("charge failed") ``` ### 4.2 Context 注入 ```go log.SetContextFieldExtractor(func(ctx context.Context) starlog.Fields { traceID, _ := ctx.Value("trace_id").(string) if traceID == "" { return nil } return starlog.Fields{"trace_id": traceID} }) log.WithContext(ctx).Info("request done") log.InfoContext(ctx, "request done") ``` ## 5. 级别过滤 ```go log.SetLevel(starlog.LvInfo) if log.IsLevelEnabled(starlog.LvDebug) { log.Debug("expensive debug") } lv, err := starlog.ParseLevel("warn") // LvWarning _ = lv _ = err ``` `SetStdErrLevel` 只控制 stdout/stderr 分流阈值,不是日志过滤阈值。 ## 6. 显示、颜色与格式化 ### 6.1 颜色模式 ```go log.SetColorMode(starlog.ColorModeOff) log.SetColorMode(starlog.ColorModeFullLine) log.SetColorMode(starlog.ColorModeLevelOnly) ``` ### 6.2 关键词和字段着色 ```go log.SetKeywordColor("timeout", []starlog.Attr{starlog.FgRed, starlog.Bold}) log.SetShowFieldColor(true) log.SetFieldKeyColor([]starlog.Attr{starlog.FgHiBlue}) log.SetFieldTypeColor(starlog.FieldTypeNumber, []starlog.Attr{starlog.FgYellow}) log.SetFieldValueColor("user_id", []starlog.Attr{starlog.FgCyan}) ``` ### 6.3 关键词预设(Moba 风格) ```go log.ApplyKeywordPreset(starlog.KeywordPresetMobaLite) // 覆盖现有关键词映射 // 或 log.MergeKeywordPreset(starlog.KeywordPresetMobaFull) // 在现有映射上合并 // 可选匹配模式(默认关闭,兼容旧行为) log.SetKeywordMatchOptions(starlog.KeywordMatchOptions{ IgnoreCase: true, // 忽略大小写 WholeWord: true, // 仅匹配完整单词 }) ``` 说明: - `MobaLite`:常用词高亮(如 `error/warn/true/false/success`) - `MobaFull`:在 Lite 基础上增加更多运行态词汇 - 预设后仍可继续 `SetKeywordColor(...)` 覆盖单个关键词颜色 ### 6.4 Formatter ```go log.SetFormatter(starlog.NewTextFormatter()) log.SetFormatter(starlog.NewJSONFormatter()) ``` 自定义 formatter: ```go type Formatter interface { Format(*Entry) ([]byte, error) } ``` ## 7. 输出管道(Writer / Sink / MultiSink) ### 7.1 Writer ```go log.SetWriter(os.Stdout) ``` ### 7.2 Sink ```go type Sink interface { Write([]byte) error Close() error } log.SetSink(mySink) ``` ### 7.3 MultiSink ```go multi := starlog.NewMultiSink(fileSink, networkSink) multi.SetContinueOnError(true) log.SetSink(multi) stats := multi.GetStats() _ = stats ``` ## 8. 按级别分流(RouteHandler) 示例:`info+notice -> brief.log`,`error+ -> err.log`。 ```go brief, _ := os.OpenFile("./logs/brief.log", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644) errf, _ := os.OpenFile("./logs/err.log", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644) route := starlog.NewRouteHandler( starlog.Route{ Name: "brief", Match: starlog.MatchLevels(starlog.LvInfo, starlog.LvNotice), Formatter: starlog.NewTextFormatter(), Sink: starlog.NewWriterSink(brief), }, starlog.Route{ Name: "error", Match: starlog.MatchAtLeast(starlog.LvError), Formatter: starlog.NewJSONFormatter(), Sink: starlog.NewWriterSink(errf), }, ) log.SetEntryHandler(route) ``` 如果要“主日志 + 分流日志”同时保留,用 `ChainHandler` 组合 handler。 ## 9. 轮转与归档 ### 9.1 `RotatePolicy` 语义 ```go type RotatePolicy interface { ShouldRotate(starlog.FileInfo, *starlog.Entry) bool NextPath(string, time.Time) string } // 可选扩展接口 type RotateArchivePathProvider interface { ArchivePath(string, time.Time) string } ``` 语义说明: - `ShouldRotate`:判定是否触发轮转 - `NextPath`:返回“被切走旧日志”的归档路径 - 若策略实现了 `ArchivePath`,框架优先使用 `ArchivePath` - 若 `ArchivePath` 返回空字符串,回退到 `NextPath` ### 9.2 主日志轮转(推荐入口) ```go if err := starlog.StartRotatePolicy(log, policy, 1); err != nil { panic(err) } ``` 内置模板: ```go _ = starlog.StartRotateByTime(log, 24*time.Hour, 10) _ = starlog.StartRotateBySize(log, 200*1024*1024, 5) _ = starlog.StartRotateByTimeSize(log, 24*time.Hour, 200*1024*1024, 5) ``` ### 9.3 管理增强(保留、压缩) ```go opts := starlog.RotateManageOptions{ MaxBackups: 7, MaxAge: 7 * 24 * time.Hour, Compress: true, Pattern: "20060102-150405", } if err := starlog.StartManagedRotatePolicy(log, policy, 1, opts); err != nil { panic(err) } ``` 模板增强版: ```go _ = starlog.StartManagedRotateByTime(log, 24*time.Hour, 10, opts) _ = starlog.StartManagedRotateBySize(log, 200*1024*1024, 5, opts) _ = starlog.StartManagedRotateByTimeSize(log, 24*time.Hour, 200*1024*1024, 5, opts) ``` ### 9.4 RouteHandler 分流文件轮转 分流 sink 可直接使用轮转 sink: ```go debugSink, err := starlog.NewManagedRotateByTimeSink( "./logs/debug.log", true, 24*time.Hour, 30*time.Second, starlog.RotateManageOptions{MaxBackups: 30, Compress: true, Pattern: "20060102"}, ) if err != nil { panic(err) } route := starlog.NewRouteHandler( starlog.Route{ Name: "debug-info", Match: starlog.MatchLevels(starlog.LvDebug, starlog.LvInfo), Formatter: starlog.NewTextFormatter(), Sink: debugSink, }, ) log.SetEntryHandler(route) ``` ### 9.5 自定义轮转策略,同时应用到两种文件 ```go type HourOrSizePolicy struct { HourInterval time.Duration MaxBytes int64 } func (p HourOrSizePolicy) ShouldRotate(fi starlog.FileInfo, _ *starlog.Entry) bool { if p.MaxBytes > 0 && fi.Size >= p.MaxBytes { return true } if p.HourInterval > 0 && fi.ModTime.Add(p.HourInterval).Before(time.Now()) { return true } return false } func (p HourOrSizePolicy) NextPath(path string, now time.Time) string { ext := filepath.Ext(path) base := strings.TrimSuffix(path, ext) return base + "." + now.Format("20060102-150405") + ext } ``` 应用到主日志: ```go mainLog := starlog.NewProduction(nil) _ = starlog.SetLogFile("./logs/app.log", mainLog, true) policy := HourOrSizePolicy{HourInterval: 6 * time.Hour, MaxBytes: 300 * 1024 * 1024} _ = starlog.StartManagedRotatePolicy(mainLog, policy, 1, starlog.RotateManageOptions{ MaxBackups: 20, MaxAge: 14 * 24 * time.Hour, Compress: true, Pattern: "20060102-150405", }) ``` 应用到分流文件: ```go debugSink, _ := starlog.NewManagedRotatePolicySink( "./logs/debug.log", true, policy, 30*time.Second, starlog.RotateManageOptions{ MaxBackups: 30, MaxAge: 30 * 24 * time.Hour, Compress: true, Pattern: "20060102-150405", }, ) route := starlog.NewRouteHandler( starlog.Route{ Name: "debug-info", Match: starlog.MatchLevels(starlog.LvDebug, starlog.LvInfo), Formatter: starlog.NewTextFormatter(), Sink: debugSink, }, ) mainLog.SetEntryHandler(route) ``` ### 9.6 兼容入口 `StartArchive` 仍可使用,但新代码建议优先 `StartRotatePolicy` / `StartManagedRotatePolicy`。 ## 10. 异步、回调与 pending ```go log.SetHandler(func(data starlog.LogData) { // 异步显示回调 }) log.SetAsyncFallbackToSync(true) log.SetAsyncHandlerTimeout(100 * time.Millisecond) log.SetEntryHandler(myHandler) log.SetEntryHandlerTimeout(200 * time.Millisecond) log.SetPendingWriteLimit(1024) log.SetPendingDropPolicy(starlog.PendingDropOldest) ``` 可观测项: - `GetAsyncDropCount()` - `GetPendingStats()` ## 11. 高频防爆(去重 / 采样 / 限流) 主链路顺序:`去重 -> 采样 -> 限流`。 ```go log.SetDedupConfig(starlog.DedupConfig{ Enable: true, Levels: []int{starlog.LvInfo, starlog.LvNotice}, Window: 2 * time.Second, Scope: starlog.DedupScopeByKey, }) log.SetSamplingConfig(starlog.SamplingConfig{ Enable: true, Levels: []int{starlog.LvInfo}, Rate: 0.2, Scope: starlog.SamplingScopeByKey, }) log.SetRateLimitConfig(starlog.RateLimitConfig{ Enable: true, Levels: []int{starlog.LvInfo}, Rate: 50, Burst: 100, Scope: starlog.RateLimitScopeByKey, }) ``` ## 12. 脱敏 ```go log.AddRedactRule(starlog.NewSensitiveFieldRule("[MASK]", "password", "token")) log.AddRedactRule(starlog.NewMessageRegexRule(regexp.MustCompile(`\b1\d{10}\b`), "[PHONE]")) log.SetRedactFailMode(starlog.RedactFailMaskAll) ``` 失败策略: - `RedactFailMaskAll`(推荐) - `RedactFailOpen` - `RedactFailDrop` ## 13. 指标观测 ```go snapshot := log.GetMetricsSnapshot() _ = snapshot ``` 常用统计: - `GetPendingStats()` - `GetSamplingStats()` - `GetDedupStats()` - `GetRateLimitStats()` - `GetWriteErrorCount()` - `GetAsyncDropCount()` ## 14. 生命周期与退出 语义: - `Flush()`:刷写 pending - `Sync()`:`Flush + 底层 Sync()`(若支持) - `Close()`:关闭资源,不等待异步队列 drain - `Shutdown(ctx)`:等待异步 drain,再关闭资源 推荐: ```go ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() defer log.Shutdown(ctx) ``` 全局 logger 可使用:`starlog.Shutdown(ctx)`。 ## 15. 标准库桥接 ```go stdLogger := log.AsStdlibLoggerWithOptions(starlog.LvInfo, starlog.WithStdlibPrefix("http "), starlog.WithStdlibFlags(0), starlog.WithStdlibTrimNewline(true), ) stdLogger.Println("server started") ``` 也可以桥接为 `io.Writer`:`AsWriter(level)` / `AsWriterWithOptions(...)`。 ## 16. 测试工具 ### 16.1 Observer ```go obs := starlog.NewObserverWithLimit(200) log.SetEntryHandler(obs) entries := obs.Entries() _ = entries ``` ### 16.2 TestHook ```go hook := starlog.NewTestHook(log) defer hook.Close() log.Info("hello") if hook.Count() == 0 { panic("expect log") } ``` ## 17. 配置快照 API(并发配置推荐) ```go log.UpdateConfig(func(cfg *starlog.Config) { cfg.Level = starlog.LvInfo cfg.ShowColor = false cfg.AutoAppendNewline = true cfg.Sampling = starlog.SamplingConfig{Enable: true, Rate: 0.3} cfg.Dedup = starlog.DedupConfig{Enable: true, Window: time.Second} }) ``` 配套: - `GetConfig()` - `ApplyConfig(cfg)` - `DefaultConfig()` ## 18. 生产最小模板 ```go func NewProdLogger(path string) *starlog.StarLogger { log := starlog.NewProduction(nil) log.SetName("svc") log.SetAutoAppendNewline(true) if err := starlog.SetLogFile(path, log, true); err != nil { panic(err) } _ = starlog.StartManagedRotateBySize(log, 200*1024*1024, 5, starlog.RotateManageOptions{ MaxBackups: 14, MaxAge: 14 * 24 * time.Hour, Compress: true, Pattern: "20060102-150405", }) return log } ``` ## 19. 完整实战示例(主日志 + 协程分隔 + 双文件轮转) 覆盖目标: 1. 使用 `starlog` 2. 协程中使用新 `LOG` 分隔符(`NewFlag()` 自动随机) 3. 设置主日志文件 4. 主日志使用压缩轮转 5. `debug/info` 额外保存到 `debug.log` 6. `debug.log` 使用独立轮转策略 ```go package main import ( "context" "os" "path/filepath" "sync" "time" "b612.me/starlog" ) func main() { _ = os.MkdirAll("./logs", 0755) mainLog := starlog.NewProduction(nil) mainLog.SetName("app-main") mainLog.SetAutoAppendNewline(true) mainPath := filepath.Clean("./logs/app.log") if err := starlog.SetLogFile(mainPath, mainLog, true); err != nil { panic(err) } if err := starlog.StartManagedRotateBySize(mainLog, 200*1024*1024, 5, starlog.RotateManageOptions{ MaxBackups: 14, MaxAge: 14 * 24 * time.Hour, Compress: true, Pattern: "20060102-150405", }); err != nil { panic(err) } debugPath := filepath.Clean("./logs/debug.log") debugSink, err := starlog.NewManagedRotateByTimeSink(debugPath, true, 24*time.Hour, 30*time.Second, starlog.RotateManageOptions{ MaxBackups: 30, MaxAge: 30 * 24 * time.Hour, Compress: true, Pattern: "20060102", }) if err != nil { panic(err) } route := starlog.NewRouteHandler( starlog.Route{ Name: "debug-info-to-debug-log", Match: starlog.MatchLevels(starlog.LvDebug, starlog.LvInfo), Formatter: starlog.NewTextFormatter(), Sink: debugSink, }, ) mainLog.SetEntryHandler(route) var wg sync.WaitGroup for i := 0; i < 3; i++ { wg.Add(1) go func(worker int) { defer wg.Done() workerLog := mainLog.NewFlag() workerLog.WithField("worker", worker).Debug("worker start") workerLog.WithField("worker", worker).Info("worker running") if worker == 1 { workerLog.WithField("worker", worker).Error("worker failed") } }(i) } wg.Wait() ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() _ = mainLog.Shutdown(ctx) } ``` ## 20. 常见问题 ### Q1:`Infof/Errorf` 需要手动加 `\n` 吗? 默认需要。若开启 `SetAutoAppendNewline(true)`,末尾无 `\n` 会自动补齐。 ### Q2:异步模式会丢日志吗? - 显示回调链路:可能因超时或队列满发生丢弃,可通过计数观测 - writer/sink 主写链路:只要未被过滤(级别/去重/采样/限流)且写入正常,会落盘 ### Q3:新项目应该选 `Archive` 还是 `RotatePolicy`? 优先 `RotatePolicy`;`Archive` 作为兼容路径保留。