允许不同级别的写入info

This commit is contained in:
yun
2026-10-05 19:30:40 +08:00
parent 7e1b474823
commit a9adc11fad
6 changed files with 600 additions and 32 deletions
+11 -2
View File
@@ -108,8 +108,17 @@ func (l *Logger) logger(ctx context.Context, event string, v ...any) {
l.reportError(fmt.Errorf("loggerx: 写入日志失败: %w", err))
}
if l.option.errorToInfo && event == "error" {
if _, err := l.write("info", line1); err != nil {
// allLevelToInfo:除 info 之外的所有级别(debug/warn/error)都额外往同一个
// info 日志里再写一份,info.log 因此成为「一个文件看全量」的汇总文件。
// event 只会是包内固定的四个字面量(info/debug/warn/error,见 loggerx.go),
// 所以 event != "info" 恰好等价于「其它所有级别」
//
// 两个细节:
// - 复制出来的行保留原始 level 字段,info.log 里依然能区分级别;
// - 走 writeFileOnly 而不是 write:镜像不再经过额外驱动,
// 否则控制台/自定义 writer 上每条非 info 日志都会重复输出两遍
if l.option.allLevelToInfo && event != "info" {
if _, err := l.writeFileOnly("info", line1); err != nil {
l.reportError(fmt.Errorf("loggerx: 写入 info 日志失败: %w", err))
}
}
+483
View File
@@ -0,0 +1,483 @@
package loggerx_test
import (
"bytes"
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"sync"
"testing"
"github.com/yuninks/loggerx"
)
// levelsOf 解析 JSON 日志的每一行,返回「level -> 出现次数」
func levelsOf(t *testing.T, raw string) map[string]int {
t.Helper()
out := make(map[string]int)
for _, line := range splitLines(raw) {
var fd struct {
Level string `json:"level"`
}
if err := json.Unmarshal([]byte(line), &fd); err != nil {
t.Fatalf("日志行不是合法 JSON: %q: %v", line, err)
}
out[fd.Level]++
}
return out
}
// splitLines 去掉空行,只保留有效日志行
func splitLines(raw string) []string {
var out []string
for _, line := range strings.Split(raw, "\n") {
if strings.TrimSpace(line) != "" {
out = append(out, line)
}
}
return out
}
// 开启 SetAllLevelToInfo 后:debug / warn / error 都要同时进 info.log,
// 各原级别文件仍只写自己那一条;info 本身不重复写
func TestAllLevelToInfoMirrorsEveryLevel(t *testing.T) {
l, dir := newTestLogger(t, loggerx.SetAllLevelToInfo())
ctx := context.Background()
l.Debug(ctx, "M-DEBUG")
l.Info(ctx, "M-INFO")
l.Warn(ctx, "M-WARN")
l.Error(ctx, "M-ERROR")
info := syncAndRead(t, l, dir, "info")
for _, ev := range []string{"debug", "info", "warn", "error"} {
marker := "M-" + strings.ToUpper(ev)
if n := strings.Count(info, marker); n != 1 {
t.Errorf("info.log 中 %s 出现 %d 次,期望 1 次\n%s", marker, n, info)
}
}
// 副本必须保留原始 level,而不是统一改写成 info
want := map[string]int{"debug": 1, "info": 1, "warn": 1, "error": 1}
got := levelsOf(t, info)
for lv, n := range want {
if got[lv] != n {
t.Errorf("info.log 中 level=%s 的行数 = %d,期望 %d(实际 %v)", lv, got[lv], n, got)
}
}
if len(got) != len(want) {
t.Errorf("info.log 出现了预期外的 level: %v", got)
}
// 原级别文件不受影响:各写各的,且只写一条
for _, ev := range []string{"debug", "warn", "error"} {
own := syncAndRead(t, l, dir, ev)
marker := "M-" + strings.ToUpper(ev)
if n := strings.Count(own, marker); n != 1 {
t.Errorf("%s.log 中 %s 出现 %d 次,期望 1 次\n%s", ev, marker, n, own)
}
for _, other := range []string{"M-DEBUG", "M-INFO", "M-WARN", "M-ERROR"} {
if other == marker {
continue
}
if strings.Contains(own, other) {
t.Errorf("%s.log 混入了 %s\n%s", ev, other, own)
}
}
}
}
// 未开启选项时(默认):info.log 只放 info,其它级别不得混入
func TestAllLevelToInfoOffByDefault(t *testing.T) {
l, dir := newTestLogger(t)
ctx := context.Background()
l.Debug(ctx, "N-DEBUG")
l.Info(ctx, "N-INFO")
l.Warn(ctx, "N-WARN")
l.Error(ctx, "N-ERROR")
info := syncAndRead(t, l, dir, "info")
for _, marker := range []string{"N-DEBUG", "N-WARN", "N-ERROR"} {
if strings.Contains(info, marker) {
t.Errorf("未开启 SetAllLevelToInfo 时 info.log 不应包含 %s\n%s", marker, info)
}
}
if n := strings.Count(info, "N-INFO"); n != 1 {
t.Errorf("info.log 中 info 日志出现 %d 次,期望 1 次\n%s", n, info)
}
}
// 副本只落文件:控制台 / 自定义驱动上每条日志只能出现一次(同步与异步都成立)
func TestAllLevelToInfoDoesNotDuplicateDrivers(t *testing.T) {
for _, mode := range []struct {
name string
opts []loggerx.Option
}{
{name: "sync"},
{name: "async", opts: []loggerx.Option{loggerx.SetWriteAsync()}},
} {
t.Run(mode.name, func(t *testing.T) {
var buf bytes.Buffer
opts := append([]loggerx.Option{loggerx.SetAllLevelToInfo(), loggerx.SetExtraDriver(&buf)}, mode.opts...)
l, dir := newTestLogger(t, opts...)
ctx := context.Background()
l.Debug(ctx, "D-DEBUG")
l.Info(ctx, "D-INFO")
l.Warn(ctx, "D-WARN")
l.Error(ctx, "D-ERROR")
// 异步模式下驱动由消费协程写,必须先 Close 再读 buf,否则是数据竞争
if err := l.Close(); err != nil {
t.Fatalf("Close: %v", err)
}
info := syncAndRead(t, l, dir, "info")
out := buf.String()
for _, marker := range []string{"D-DEBUG", "D-INFO", "D-WARN", "D-ERROR"} {
if n := strings.Count(out, marker); n != 1 {
t.Errorf("额外驱动里 %s 出现 %d 次,期望 1 次\n%s", marker, n, out)
}
if !strings.Contains(info, marker) {
t.Errorf("info.log 里缺少 %s\n%s", marker, info)
}
}
})
}
}
// 异步 + channel 组合下镜像同样成立,且不能串 channel
func TestAllLevelToInfoAsyncChannel(t *testing.T) {
dir := t.TempDir()
l := loggerx.NewLogger(context.Background(),
loggerx.SetDir(dir),
loggerx.SetWriteAsync(),
loggerx.SetAllLevelToInfo(),
)
ctx := context.Background()
l.Channel("sub").Error(ctx, "A-SUBERR")
l.Channel("sub").Warn(ctx, "A-SUBWARN")
l.Channel("sub").Info(ctx, "A-SUBINFO")
l.Error(ctx, "A-ROOTERR")
if err := l.Close(); err != nil {
t.Fatalf("Close: %v", err)
}
subInfo := readLog(t, filepath.Join(dir, "sub"), "info")
for _, marker := range []string{"A-SUBERR", "A-SUBWARN", "A-SUBINFO"} {
if n := strings.Count(subInfo, marker); n != 1 {
t.Errorf("sub/info.log 中 %s 出现 %d 次,期望 1 次\n%s", marker, n, subInfo)
}
}
// error 仍然落在自己的文件里
subErr := readLog(t, filepath.Join(dir, "sub"), "error")
if n := strings.Count(subErr, "A-SUBERR"); n != 1 {
t.Errorf("sub/error.log 中 A-SUBERR 出现 %d 次,期望 1 次\n%s", n, subErr)
}
// 根 channel 的 info.log 不能混入 sub 的内容
rootInfo := readLog(t, dir, "info")
if n := strings.Count(rootInfo, "A-ROOTERR"); n != 1 {
t.Errorf("根 info.log 中 A-ROOTERR 出现 %d 次,期望 1 次\n%s", n, rootInfo)
}
if strings.Contains(rootInfo, "A-SUBERR") {
t.Errorf("根 info.log 混入了 sub channel 的日志\n%s", rootInfo)
}
}
// 级别过滤优先:被 SetMinLevel 丢弃的日志不会因为它出现在 info.log 里
func TestAllLevelToInfoRespectsMinLevel(t *testing.T) {
l, dir := newTestLogger(t, loggerx.SetAllLevelToInfo(), loggerx.SetMinLevel(loggerx.LevelWarn))
ctx := context.Background()
l.Debug(ctx, "F-DEBUG")
l.Info(ctx, "F-INFO")
l.Warn(ctx, "F-WARN")
l.Error(ctx, "F-ERROR")
info := syncAndRead(t, l, dir, "info")
for _, marker := range []string{"F-DEBUG", "F-INFO"} {
if strings.Contains(info, marker) {
t.Errorf("低于 MinLevel 的 %s 不应出现在 info.log\n%s", marker, info)
}
}
for _, marker := range []string{"F-WARN", "F-ERROR"} {
if n := strings.Count(info, marker); n != 1 {
t.Errorf("info.log 中 %s 出现 %d 次,期望 1 次\n%s", marker, n, info)
}
}
}
// text 格式下镜像同样是一行一条
func TestAllLevelToInfoTextFormat(t *testing.T) {
l, dir := newTestLogger(t, loggerx.SetAllLevelToInfo(), loggerx.SetFormat(loggerx.FormatText))
ctx := context.Background()
l.Warn(ctx, "T-WARN")
l.Error(ctx, "T-ERROR")
info := syncAndRead(t, l, dir, "info")
lines := splitLines(info)
if len(lines) != 2 {
t.Fatalf("info.log 行数 = %d,期望 2\n%s", len(lines), info)
}
for i, marker := range []string{"T-WARN", "T-ERROR"} {
if !strings.Contains(lines[i], marker) {
t.Errorf("info.log 第 %d 行缺少 %s: %s", i+1, marker, lines[i])
}
}
if !strings.Contains(lines[1], "level=error") {
t.Errorf("text 副本应保留原始 level: %s", lines[1])
}
}
// 并发下镜像不能丢、不能重复:info.log 的条数必须是四个级别之和
func TestAllLevelToInfoConcurrentExactCounts(t *testing.T) {
const (
workers = 8
rounds = 60
)
l, dir := newTestLogger(t, loggerx.SetAllLevelToInfo())
var wg sync.WaitGroup
for w := 0; w < workers; w++ {
wg.Add(1)
go func(w int) {
defer wg.Done()
ctx := context.Background()
for i := 0; i < rounds; i++ {
l.Debugf(ctx, "C-DEBUG-%d-%d", w, i)
l.Infof(ctx, "C-INFO-%d-%d", w, i)
l.Warnf(ctx, "C-WARN-%d-%d", w, i)
l.Errorf(ctx, "C-ERROR-%d-%d", w, i)
}
}(w)
}
wg.Wait()
// 原级别文件:debug/warn/error 各自只放自己那 rounds*workers 条
for _, ev := range []string{"debug", "warn", "error"} {
got := syncAndRead(t, l, dir, ev)
if n := len(splitLines(got)); n != workers*rounds {
t.Errorf("%s.log 行数 = %d,期望 %d", ev, n, workers*rounds)
}
}
// info.log:info 本身 + debug/warn/error 三份副本 = 4 倍
info := syncAndRead(t, l, dir, "info")
if n := len(splitLines(info)); n != 4*workers*rounds {
t.Errorf("info.log 行数 = %d,期望 %d", n, 4*workers*rounds)
}
for _, ev := range []string{"DEBUG", "INFO", "WARN", "ERROR"} {
if n := strings.Count(info, "C-"+ev+"-"); n != workers*rounds {
t.Errorf("info.log 中 %s 出现 %d 次,期望 %d", ev, n, workers*rounds)
}
}
}
// 异步 + 并发:镜像任务不能丢、不能串级别文件
func TestAllLevelToInfoAsyncConcurrentCounts(t *testing.T) {
const (
workers = 4
rounds = 50
)
dir := t.TempDir()
l := loggerx.NewLogger(context.Background(),
loggerx.SetDir(dir),
loggerx.SetWriteAsync(),
loggerx.SetAllLevelToInfo(),
)
var wg sync.WaitGroup
for w := 0; w < workers; w++ {
wg.Add(1)
go func(w int) {
defer wg.Done()
ctx := context.Background()
for i := 0; i < rounds; i++ {
l.Warnf(ctx, "Q-WARN-%d-%d", w, i)
l.Errorf(ctx, "Q-ERROR-%d-%d", w, i)
}
}(w)
}
wg.Wait()
if err := l.Close(); err != nil {
t.Fatalf("Close: %v", err)
}
for _, ev := range []string{"warn", "error"} {
own := readLog(t, dir, ev)
if n := strings.Count(own, "Q-"+strings.ToUpper(ev)+"-"); n != workers*rounds {
t.Errorf("%s.log 中自身日志出现 %d 次,期望 %d", ev, n, workers*rounds)
}
}
info := readLog(t, dir, "info")
for _, ev := range []string{"WARN", "ERROR"} {
if n := strings.Count(info, "Q-"+ev+"-"); n != workers*rounds {
t.Errorf("info.log 中 %s 出现 %d 次,期望 %d", ev, n, workers*rounds)
}
}
if n := len(splitLines(info)); n != 2*workers*rounds {
t.Errorf("info.log 行数 = %d,期望 %d", n, 2*workers*rounds)
}
}
// readLevelAll 读取某级别在目录下的全部文件(含大小切割产生的 _N 归档)
func readLevelAll(t *testing.T, dir, event string) string {
t.Helper()
files, err := filepath.Glob(filepath.Join(dir, "*_"+event+"*.log"))
if err != nil {
t.Fatalf("glob %s: %v", event, err)
}
var sb strings.Builder
for _, f := range files {
b, err := os.ReadFile(f)
if err != nil {
t.Fatalf("读取 %s: %v", f, err)
}
sb.Write(b)
}
return sb.String()
}
// 开启大小切割时镜像同样要正常滚动归档,一条都不能丢、也不能重复
func TestAllLevelToInfoWithSizeSplit(t *testing.T) {
dir := t.TempDir()
l := loggerx.NewLogger(context.Background(),
loggerx.SetDir(dir),
loggerx.SetAllLevelToInfo(),
loggerx.SetSizeSplit(2*1024), // 很小,强制滚动多次
loggerx.SetCompress(false), // 不压缩,便于直接读归档
)
t.Cleanup(func() {
if err := l.Close(); err != nil {
t.Errorf("Close 返回错误: %v", err)
}
})
const n = 200
ctx := context.Background()
for i := 0; i < n; i++ {
l.Warnf(ctx, "S-WARN-%03d", i)
l.Infof(ctx, "S-INFO-%03d", i)
}
if err := l.MustSync(); err != nil {
t.Fatalf("MustSync: %v", err)
}
info := readLevelAll(t, dir, "info")
warn := readLevelAll(t, dir, "warn")
for i := 0; i < n; i++ {
if got := strings.Count(info, fmt.Sprintf("S-WARN-%03d", i)); got != 1 {
t.Fatalf("info.log 中 S-WARN-%03d 出现 %d 次,期望 1 次", i, got)
}
if got := strings.Count(info, fmt.Sprintf("S-INFO-%03d", i)); got != 1 {
t.Fatalf("info.log 中 S-INFO-%03d 出现 %d 次,期望 1 次", i, got)
}
if got := strings.Count(warn, fmt.Sprintf("S-WARN-%03d", i)); got != 1 {
t.Fatalf("warn.log 中 S-WARN-%03d 出现 %d 次,期望 1 次", i, got)
}
}
if got, want := len(splitLines(info)), 2*n; got != want {
t.Errorf("info.log 总行数 = %d,期望 %d", got, want)
}
if got, want := len(splitLines(warn)), n; got != want {
t.Errorf("warn.log 总行数 = %d,期望 %d", got, want)
}
}
// 异步模式下镜像写入失败必须上报(否则 info.log 写不进去是静默的),
// 且不能影响原级别文件落盘
func TestAllLevelToInfoAsyncReportsMirrorFailure(t *testing.T) {
dir := t.TempDir()
// 用一个同名目录占住 info.log:镜像必然打不开,而 warn.log 正常
if err := os.Mkdir(filepath.Join(dir, "info.log"), 0o755); err != nil {
t.Fatalf("mkdir info.log: %v", err)
}
reported := make(chan error, 32)
l := loggerx.NewLogger(context.Background(),
loggerx.SetDir(dir),
loggerx.SetFileSplit(loggerx.FileSplitNone),
loggerx.SetWriteAsync(),
loggerx.SetAllLevelToInfo(),
loggerx.SetErrorHandler(func(err error) {
select {
case reported <- err:
default:
}
}),
)
l.Warn(context.Background(), "R-WARN")
if err := l.Close(); err != nil {
t.Fatalf("Close: %v", err)
}
select {
case err := <-reported:
if !strings.Contains(err.Error(), "info") {
t.Errorf("上报的错误与 info 镜像无关: %v", err)
}
default:
t.Error("异步模式下 info 镜像写入失败没有被上报")
}
if !strings.Contains(readLog(t, dir, "warn"), "R-WARN") {
t.Error("镜像失败不应影响 warn.log 落盘")
}
}
// SetPrintFile(false):没有文件可镜像,选项不能引发重复驱动输出,也不能建出 info.log
func TestAllLevelToInfoWithoutFile(t *testing.T) {
for _, mode := range []struct {
name string
opts []loggerx.Option
}{
{name: "sync"},
{name: "async", opts: []loggerx.Option{loggerx.SetWriteAsync()}},
} {
t.Run(mode.name, func(t *testing.T) {
var buf bytes.Buffer
dir := t.TempDir()
opts := append([]loggerx.Option{
loggerx.SetDir(dir),
loggerx.SetPrintFile(false),
loggerx.SetAllLevelToInfo(),
loggerx.SetExtraDriver(&buf),
}, mode.opts...)
l := loggerx.NewLogger(context.Background(), opts...)
l.Error(context.Background(), "P-ERROR")
l.Warn(context.Background(), "P-WARN")
// 异步模式下驱动由消费协程写,必须先 Close 再读 buf
if err := l.Close(); err != nil {
t.Fatalf("Close: %v", err)
}
out := buf.String()
if n := strings.Count(out, "P-ERROR"); n != 1 {
t.Errorf("驱动里 P-ERROR 出现 %d 次,期望 1 次\n%s", n, out)
}
if n := strings.Count(out, "P-WARN"); n != 1 {
t.Errorf("驱动里 P-WARN 出现 %d 次,期望 1 次\n%s", n, out)
}
files, err := filepath.Glob(filepath.Join(dir, "*_info.log"))
if err != nil {
t.Fatalf("glob: %v", err)
}
if len(files) != 0 {
t.Errorf("SetPrintFile(false) 时不应产生文件,实际有 %v", files)
}
})
}
}
+1 -1
View File
@@ -20,7 +20,7 @@ func TestLogger(t *testing.T) {
l := loggerx.NewLogger(
context.Background(),
loggerx.SetErrorToInfo(),
loggerx.SetAllLevelToInfo(),
loggerx.SetToConsole(),
loggerx.SetTimeZone(time.UTC),
// loggerx.SetExtraDriver(b, Print{}),
+39 -25
View File
@@ -8,28 +8,28 @@ import (
)
type loggerOption struct {
prefix string // 日志前缀
format Format // json / text
dir string // 文件目录
isGinLog bool
isGid bool
isPrintFile bool
asGlobalLog bool // 是否接管全局 stdlib log(默认 false)
writeType writeType // 是否异步罗盘
traceField string // trace字段
errorToInfo bool // 错误日志是否写入info日志
days int // 日志保存天数
drivers []io.Writer // 文件落盘驱动器
fileSplit FileSplit // 文件切割规则
sizeSplit int // 根据文件大小切割(字节,<=0 不切割)
compress bool // 归档文件是否压缩为 .gz
compressLvl int // gzip 压缩级别
flushEvery time.Duration // 定时刷盘间隔,0 表示不开启
minLevel Level // 最低输出级别
onError func(error) // 内部错误回调
timeZone *time.Location // 时区
escapeHTML bool
expandData map[string]string // 扩展字段
prefix string // 日志前缀
format Format // json / text
dir string // 文件目录
isGinLog bool
isGid bool
isPrintFile bool
asGlobalLog bool // 是否接管全局 stdlib log(默认 false)
writeType writeType // 是否异步罗盘
traceField string // trace字段
allLevelToInfo bool // 非 info 级别的日志是否同时写入 info 日志
days int // 日志保存天数
drivers []io.Writer // 文件落盘驱动器
fileSplit FileSplit // 文件切割规则
sizeSplit int // 根据文件大小切割(字节,<=0 不切割)
compress bool // 归档文件是否压缩为 .gz
compressLvl int // gzip 压缩级别
flushEvery time.Duration // 定时刷盘间隔,0 表示不开启
minLevel Level // 最低输出级别
onError func(error) // 内部错误回调
timeZone *time.Location // 时区
escapeHTML bool
expandData map[string]string // 扩展字段
}
type writeType uint8
@@ -95,10 +95,24 @@ func SetToConsole() Option {
}
}
// 错误日志是否写入info日志
func SetErrorToInfo() Option {
// SetAllLevelToInfo 开启后,除 info 以外的所有级别(debug / warn / error)
// 都会额外往同一个 channel 的 info 日志里再写一份,
// info.log 因此成为「一个文件看全量日志」的汇总文件。
//
// 具体行为:
// - 各原级别文件照旧写(warn 进 warn.log、error 进 error.log),info.log 是副本;
// - 副本保留原始 level 字段,info.log 里仍能按级别区分和过滤;
// - info 本身不重复写,不会在 info.log 里出现两行同样的 info;
// - 副本只落文件,不会再走一遍额外驱动(控制台/自定义 writer),避免重复输出;
// - 级别过滤(SetMinLevel)仍然先生效:被丢弃的日志不会因为它而出现在 info.log 里;
// - SetPrintFile(false) 时该选项无意义(没有文件可写)。
//
// 代价:开启后 info.log 的体积约等于所有级别日志之和(保留期内磁盘占用同样成倍
// 增长);异步模式下每条非 info 日志会多占一个队列槽位。
// 按大小/时间切割与过期清理仍然照常生效。
func SetAllLevelToInfo() Option {
return func(o *loggerOption) {
o.errorToInfo = true
o.allLevelToInfo = true
}
}
+26 -1
View File
@@ -32,7 +32,7 @@ log := loggerx.NewLogger(ctx,
loggerx.SetMinLevel(loggerx.LevelInfo), // 最低输出级别,默认 LevelDebug(全输出)
loggerx.SetErrorHandler(onLogError), // 日志库自身故障回调(磁盘满/句柄失效等)
loggerx.SetTraceField("trace_id"), // trace 字段名,默认 trace_id
loggerx.SetErrorToInfo(), // error 是否同时写入 info 日志
loggerx.SetAllLevelToInfo(), // debug/warn/error 是否同时写入 info 日志
loggerx.SetExpandData("app", "order"), // 每条日志追加固定字段
loggerx.SetExtraDriver(f, hooks), // 额外落盘驱动(实现 io.Writer 即可)
loggerx.SetPrintFile(false), // 不写文件,只走驱动
@@ -172,6 +172,31 @@ loggerx.SetMinLevel(loggerx.LevelInfo) // 生产常用:Debug 直接丢弃,
loggerx.SetMinLevel(loggerx.LevelOff) // 全关
```
### 全量日志汇总到一个文件(SetAllLevelToInfo)
开启后,除 `info` 以外的所有级别(`debug` / `warn` / `error`)都会额外往同一个 channel 的
`info` 日志里再写一份,`info.log` 就是「一个文件看全量」的汇总文件:
```go
log := loggerx.NewLogger(ctx, loggerx.SetDir("./log"), loggerx.SetAllLevelToInfo())
log.Info(ctx, "只进 info.log")
log.Warn(ctx, "进 warn.log,同时进 info.log")
log.Error(ctx, "进 error.log,同时进 info.log")
```
行为细节:
- **原级别文件照旧写**:`warn.log` / `error.log` 内容不变,`info.log` 里是副本
- **副本保留原始 level 字段**,因此在 `info.log` 里仍然能按级别区分和过滤(不会统一改写成 `info`)
- **`info` 本身不重复写**,`info.log` 里不会出现两行相同的 info
- **副本只落文件**,不会再走一遍额外驱动(`SetToConsole` / `SetExtraDriver`),控制台不会出现重复行
- **级别过滤优先**:被 `SetMinLevel` 丢弃的日志不会因为它出现在 `info.log` 里
- **`SetPrintFile(false)` 时该选项无意义**(没有文件可写)
- **代价**:`info.log` 体积约等于所有级别日志之和;异步模式下每条非 info 日志会多占一个
队列槽位(队列容量 1000,非 info 级别的有效缓冲因此减半)
- 按时间/大小切割、过期清理照常生效
### 把日志库自身的故障暴露出来
磁盘满、句柄失效、归档失败、刷盘超时这类问题默认只体现在返回值里,而调用方通常忽略返回值。
+40 -3
View File
@@ -15,12 +15,39 @@ const closeDrainTimeout = 5 * time.Second
// 写入,需要判断同步还是异步
func (l *Logger) write(event string, b []byte) (n int, err error) {
if l.toAsync(event, b) {
if l.toAsync(event, b, false) {
return len(b), nil
}
return l.store(event, b)
}
// writeFileOnly 只写文件,不写额外驱动
//
// 用于 allLevelToInfo 的镜像写入:这条日志的内容已经在原级别文件里走过一次驱动了,
// 再走一遍会让控制台/自定义 writer 出现完全重复的两行
func (l *Logger) writeFileOnly(event string, b []byte) (n int, err error) {
if !l.option.isPrintFile {
// 不落文件时镜像没有去处:直接返回,连队列和内存拷贝都省掉
return len(b), nil
}
if l.toAsync(event, b, true) {
return len(b), nil
}
return l.storeFileOnlyTo(l.channel, event, b)
}
// storeFileOnlyTo 把日志只写进指定 channel 的文件,跳过全部驱动
// 句柄缓冲不是并发安全的,因此和 storeFileTo 一样必须在 writeMu 内写
func (l *Logger) storeFileOnlyTo(channel, event string, b []byte) (n int, err error) {
if !l.option.isPrintFile {
// 不落文件时该选项没有意义:镜像的唯一去处就是文件,直接当成功
return len(b), nil
}
l.writeMu.Lock()
defer l.writeMu.Unlock()
return l.storeFileTo(channel, event, b)
}
// 实际的存储
// 必须满足 io.Writer 契约:成功时 n == len(b),否则调用方(log / io.MultiWriter)
// 会认为发生了短写并把日志吞掉
@@ -219,11 +246,14 @@ type cacheData struct {
Channel string
Event string
Data []byte
// FileOnly 只落文件、不写额外驱动(allLevelToInfo 的镜像任务用)
FileOnly bool
}
// toAsync 尝试异步写入
// fileOnly 表示这条任务只落文件、不走驱动(见 writeFileOnly)
// 返回 true 表示已经交给异步队列,返回 false 表示需要同步写入
func (l *Logger) toAsync(event string, b []byte) bool {
func (l *Logger) toAsync(event string, b []byte, fileOnly bool) bool {
if l.writeType == writeTypeSync || // 指定同步模式
(l.writeType == writeTypeDefault && l.option.writeType != writeTypeAsync) { // 默认同步模式
return false
@@ -259,7 +289,7 @@ func (l *Logger) toAsync(event string, b []byte) bool {
data := make([]byte, len(b))
copy(data, b)
ch <- cacheData{Channel: l.channel, Event: event, Data: data}
ch <- cacheData{Channel: l.channel, Event: event, Data: data, FileOnly: fileOnly}
return true
}
@@ -285,6 +315,13 @@ func (l *Logger) consumeOne(val cacheData) {
}
}()
// 按任务里记的 channel 落盘,避免多个 channel 的日志混到根目录
if val.FileOnly {
// 镜像只落文件;失败必须上报,否则异步模式下 info.log 写不进去是静默的
if _, err := l.storeFileOnlyTo(val.Channel, val.Event, val.Data); err != nil {
l.reportError(fmt.Errorf("loggerx: 写入 info 日志失败: %w", err))
}
return
}
_, _ = l.storeTo(val.Channel, val.Event, val.Data)
}