starlog/docs/USAGE.md
2026-03-19 16:37:57 +08:00

14 KiB
Raw Blame History

starlog 使用指南

本文档覆盖从接入到生产落地的常见场景。

  • 模块:b612.me/starlog
  • Go 版本:1.16+

1. 快速开始

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 开发环境

log := starlog.NewDevelopment(os.Stdout)
log.SetName("my-service")
log.SetShowStd(true)

特点:

  • 默认 Debug 级别
  • 颜色模式:ColorModeLevelOnly
  • 显示源码位置

2.2 生产环境

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 套用预设

log.ApplyProductionConfig()
// 或
log.ApplyDevelopmentConfig()

3. 输出 API 与换行规则

3.1 输出函数

  • Info/Error/...fmt.Sprint 语义
  • Infof/Errorf/...fmt.Sprintf 语义
  • Infoln/Errorln/...fmt.Sprintln 语义

默认不会自动补 \nln 系列除外)。

3.2 自动补换行

log.SetAutoAppendNewline(true)

行为:

  • 末尾无 \n:自动补一个
  • 末尾有 \n:保持不变

4. 结构化日志

4.1 字段与错误

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 注入

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. 级别过滤

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 颜色模式

log.SetColorMode(starlog.ColorModeOff)
log.SetColorMode(starlog.ColorModeFullLine)
log.SetColorMode(starlog.ColorModeLevelOnly)

6.2 关键词和字段着色

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 风格)

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

log.SetFormatter(starlog.NewTextFormatter())
log.SetFormatter(starlog.NewJSONFormatter())

自定义 formatter

type Formatter interface { Format(*Entry) ([]byte, error) }

7. 输出管道Writer / Sink / MultiSink

7.1 Writer

log.SetWriter(os.Stdout)

7.2 Sink

type Sink interface {
	Write([]byte) error
	Close() error
}

log.SetSink(mySink)

7.3 MultiSink

multi := starlog.NewMultiSink(fileSink, networkSink)
multi.SetContinueOnError(true)
log.SetSink(multi)

stats := multi.GetStats()
_ = stats

8. 按级别分流RouteHandler

示例:info+notice -> brief.logerror+ -> err.log

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 语义

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 主日志轮转(推荐入口)

if err := starlog.StartRotatePolicy(log, policy, 1); err != nil {
	panic(err)
}

内置模板:

_ = 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 管理增强(保留、压缩)

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)
}

模板增强版:

_ = 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

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 自定义轮转策略,同时应用到两种文件

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
}

应用到主日志:

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",
})

应用到分流文件:

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

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. 高频防爆(去重 / 采样 / 限流)

主链路顺序:去重 -> 采样 -> 限流

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. 脱敏

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. 指标观测

snapshot := log.GetMetricsSnapshot()
_ = snapshot

常用统计:

  • GetPendingStats()
  • GetSamplingStats()
  • GetDedupStats()
  • GetRateLimitStats()
  • GetWriteErrorCount()
  • GetAsyncDropCount()

14. 生命周期与退出

语义:

  • Flush():刷写 pending
  • Sync()Flush + 底层 Sync()(若支持)
  • Close():关闭资源,不等待异步队列 drain
  • Shutdown(ctx):等待异步 drain再关闭资源

推荐:

ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
defer log.Shutdown(ctx)

全局 logger 可使用:starlog.Shutdown(ctx)

15. 标准库桥接

stdLogger := log.AsStdlibLoggerWithOptions(starlog.LvInfo,
	starlog.WithStdlibPrefix("http "),
	starlog.WithStdlibFlags(0),
	starlog.WithStdlibTrimNewline(true),
)
stdLogger.Println("server started")

也可以桥接为 io.WriterAsWriter(level) / AsWriterWithOptions(...)

16. 测试工具

16.1 Observer

obs := starlog.NewObserverWithLimit(200)
log.SetEntryHandler(obs)

entries := obs.Entries()
_ = entries

16.2 TestHook

hook := starlog.NewTestHook(log)
defer hook.Close()

log.Info("hello")
if hook.Count() == 0 {
	panic("expect log")
}

17. 配置快照 API并发配置推荐

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. 生产最小模板

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 使用独立轮转策略
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. 常见问题

Q1Infof/Errorf 需要手动加 \n 吗?

默认需要。若开启 SetAutoAppendNewline(true),末尾无 \n 会自动补齐。

Q2异步模式会丢日志吗

  • 显示回调链路:可能因超时或队列满发生丢弃,可通过计数观测
  • writer/sink 主写链路:只要未被过滤(级别/去重/采样/限流)且写入正常,会落盘

Q3新项目应该选 Archive 还是 RotatePolicy

优先 RotatePolicyArchive 作为兼容路径保留。