logrotate

package module
v1.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 9, 2026 License: MIT Imports: 11 Imported by: 0

README

logrotate

logrotate 是一个 Go 日志文件切割包,实现 io.WriteCloser,可以作为标准库 logslogzaplogrus 等日志库的文件输出端。

它只负责日志文件写入、切割、压缩和清理,不负责日志格式化、日志级别、字段编码或采样。

支持的切割方式:

  • 按文件大小切割:通过 MaxSize 控制单个日志文件最大 MB 数。
  • 按天切割,活跃文件名不变:通过 Daily 跨天轮转,当前写入文件仍是固定 Filename
  • 按天切割,活跃文件名带日期:通过 DailyFilename 写入 app-2026-06-09.log 这类日期文件。
  • 大小和按天组合切割:MaxSize 可以和 Daily / DailyFilename 同时生效。
  • 手动切割:通过 Rotate() 主动轮转,适合配合 SIGHUP
  • 显式启动:通过 Open() 初始化文件并同步清理历史积压,通过 Cleanup() 单独触发清理。
  • 旧日志清理:通过 MaxBackups 限制旧文件数量,通过 MaxAge 限制保留天数。
  • 旧日志压缩:通过 Compress 将轮转后的旧日志压缩为 .gz

安装

go get github.com/gtkit/logrotate

快速上手

配合标准库 log
package main

import (
	"log"

	"github.com/gtkit/logrotate"
)

func main() {
	w := &logrotate.Logger{
		Filename:   "/var/log/myapp/app.log",
		MaxSize:    500, // MB
		MaxBackups: 7,
		MaxAge:     30, // 天
		Compress:   true,
	}
	defer w.Close()

	log.SetOutput(w)
	log.Println("service started")
}
配合 slog
package main

import (
	"log/slog"

	"github.com/gtkit/logrotate"
)

func main() {
	w := &logrotate.Logger{
		Filename: "/var/log/myapp/app.log",
		MaxSize:  200,
		Compress: true,
	}
	defer w.Close()

	logger := slog.New(slog.NewJSONHandler(w, nil))
	logger.Info("service started", "module", "api")
}
配合 zap
package main

import (
	"time"

	"go.uber.org/zap"
	"go.uber.org/zap/zapcore"

	"github.com/gtkit/logrotate"
)

func main() {
	w := &logrotate.Logger{
		Filename:      "/var/log/myapp/app.log",
		DailyFilename: true,
		MaxSize:       500,
		MaxBackups:    14,
		MaxAge:        30,
		Location:      time.Local,
		Compress:      true,
	}
	if err := w.Open(); err != nil {
		panic(err)
	}
	defer w.Close()

	encoderCfg := zap.NewProductionEncoderConfig()
	core := zapcore.NewCore(
		zapcore.NewJSONEncoder(encoderCfg),
		zapcore.AddSync(w),
		zap.InfoLevel,
	)
	logger := zap.New(core)
	defer logger.Sync()

	logger.Info("service started", zap.String("module", "api"))
}
配合 logrus
package main

import (
	"github.com/sirupsen/logrus"

	"github.com/gtkit/logrotate"
)

func main() {
	w := &logrotate.Logger{
		Filename:   "/var/log/myapp/app.log",
		Daily:      true,
		MaxSize:    500,
		MaxBackups: 14,
		MaxAge:     30,
		LocalTime:  true,
		Compress:   true,
	}
	defer w.Close()

	logger := logrus.New()
	logger.SetOutput(w)
	logger.SetFormatter(&logrus.JSONFormatter{})

	logger.WithField("module", "api").Info("service started")
}

常用配置

w := &logrotate.Logger{
	Filename:      "/var/log/myapp/app.log",
	MaxSize:       500,
	MaxBackups:    7,
	MaxAge:        30,
	Location:      time.Local,
	Compress:      true,
	Daily:         true,
	DailyFilename: false,
}
字段 默认值 说明
Filename os.TempDir() 下的 <进程名>-logrotate.log 当前活跃日志文件路径
MaxSize 100 单个日志文件最大大小,单位 MB
MaxAge 0 旧日志最长保留天数;0 表示不按时间删除
MaxBackups 0 最多保留旧日志数量;0 表示不按数量删除
LocalTime false 轮转时间和日期文件名默认使用 UTC;设为 true 后使用本地时间
Location nil 指定轮转时间、日期文件名和清理截止时间使用的时区;非空时优先于 LocalTime
Compress false 是否用 gzip 压缩旧日志
Daily false 是否在跨天时轮转当前日志文件
DailyFilename false 当前活跃日志文件名是否带日期
Now nil 当前时间源;为空时使用 time.Now,主要用于测试
OnError nil 后台清理或压缩失败时的错误回调

配置字段应在首次 WriteRotateClose 前设置好。开始使用后不要并发修改这些字段。

切割模式

1. 只按大小切割
w := &logrotate.Logger{
	Filename:   "/var/log/myapp/app.log",
	MaxSize:    500,
	MaxBackups: 5,
	MaxAge:     14,
	Compress:   true,
}

/var/log/myapp/app.log 即将超过 500 MB 时,会生成带时间戳的备份文件:

app-2026-06-09T10-30-00.000.log
app-2026-06-09T10-30-00.000.log.gz

活跃文件始终还是:

app.log
2. 按天切割,活跃文件名不变
w := &logrotate.Logger{
	Filename: "/var/log/myapp/app.log",
	Daily:    true,
	MaxSize:  500,
}

跨天后,旧的 app.log 会被改名为带时间戳的备份文件,新日志继续写入新的 app.log

适合希望日志采集器始终读取固定文件名的场景。

3. 活跃文件名带日期
w := &logrotate.Logger{
	Filename:      "/var/log/myapp/app.log",
	DailyFilename: true,
	MaxSize:       500,
}

实际活跃文件会变成:

app-2026-06-09.log
app-2026-06-10.log

每天写入当天对应文件。MaxSize 仍然生效,同一天内文件过大时会继续生成时间戳备份。

适合按日期直接查看日志文件的场景。

清理旧日志

MaxBackupsMaxAge 可以同时使用。

w := &logrotate.Logger{
	Filename:   "/var/log/myapp/app.log",
	MaxSize:    500,
	MaxBackups: 10,
	MaxAge:     30,
	Compress:   true,
}

含义:

  • MaxBackups: 10:最多保留 10 组旧日志。
  • MaxAge: 30:删除时间戳超过 30 天的旧日志。
  • Compress: true:轮转后的旧日志会压缩为 .gz

如果 MaxBackupsMaxAge 都是 0,不会自动删除旧日志。

清理判断使用文件名中的日期或时间戳,不使用文件的修改时间。这样日志保留规则不受复制、解压、备份恢复或手工 touch 影响。按天文件使用 app-2006-01-02.log 里的日期;按大小备份使用 app-2006-01-02T15-04-05.000.log 里的时间戳。

默认清理和压缩在后台执行,不阻塞 Write() / Rotate()。如果应用需要启动时立即回收历史积压文件,调用 Open()

w := &logrotate.Logger{
	Filename:      "/var/log/myapp/app.log",
	DailyFilename: true,
	MaxAge:        30,
	MaxBackups:    14,
	Compress:      true,
	OnError: func(err error) {
		log.Printf("rotate cleanup: %v", err)
	},
}
if err := w.Open(); err != nil {
	log.Fatal(err)
}
defer w.Close()

只需要同步清理、不需要打开当前日志文件时,调用 Cleanup()。后台清理或压缩失败会传给 OnError;显式 Cleanup() 的失败直接通过返回值暴露。

手动轮转

可以在收到 SIGHUP 时主动切割日志。

package main

import (
	"log"
	"os"
	"os/signal"
	"syscall"

	"github.com/gtkit/logrotate"
)

func main() {
	w := &logrotate.Logger{
		Filename: "/var/log/myapp/app.log",
		MaxSize:  500,
	}
	defer w.Close()

	log.SetOutput(w)

	ch := make(chan os.Signal, 1)
	signal.Notify(ch, syscall.SIGHUP)

	go func() {
		for range ch {
			if err := w.Rotate(); err != nil {
				log.Printf("rotate log: %v", err)
			}
		}
	}()

	select {}
}

在业务 logger 项目中使用

如果业务 logger 项目只需要文件切割能力,不需要重复实现按天切割。直接把 logrotate.Logger 作为底层 io.Writer 注入即可。

示例:

func NewFileWriter(path string) *logrotate.Logger {
	return &logrotate.Logger{
		Filename:      path,
		DailyFilename: true,
		MaxSize:       500,
		MaxBackups:    14,
		MaxAge:        30,
		Location:      time.Local,
		Compress:      true,
	}
}

上层 logger 继续负责:

  • 日志级别。
  • 文本或 JSON 格式。
  • trace id、request id 等字段。
  • 多输出端分发。

logrotate 负责:

  • 文件写入。
  • 按大小切割。
  • 按天切割。
  • 旧文件压缩。
  • 旧文件清理。

并发与进程约束

  • Logger 可以被多个 goroutine 并发写入。
  • 并发写入会被串行化,以保证文件大小统计、轮转判断和写入顺序一致。
  • Write()Rotate()Close() 之间需串行调用:Close()Rotate() 期间不要并发调用 Write()Close() 期间也不要并发调用 Rotate()
  • Logger 内部持有锁,首次使用后不得复制;请始终通过指针(*logrotate.Logger)传递和使用。
  • 配置字段必须在首次使用前设置完成,运行中不要并发修改。
  • 旧日志的压缩和清理在后台异步执行,不阻塞 WriteClose() 只等待本实例已调度的后台任务完成后才返回,不等待其他 Logger 实例的后台任务。
  • Open() 会初始化当前文件并同步执行清理;如果清理历史积压文件失败,错误会直接返回。
  • Close() 不会使 Logger 进入终止状态;之后再次 Write 会按现有配置重新打开日志文件。
  • 同一份日志文件只能由一个进程写入。多个进程使用相同 Filename 会导致不正确的轮转行为。

API 概览

方法 说明
Open() error 打开或创建当前活跃文件,并同步执行一次旧日志清理/压缩
Write(p []byte) (int, error) 写入日志内容,必要时自动轮转
Rotate() error 立即手动轮转当前日志文件
Cleanup() error 同步执行一次旧日志清理/压缩,并返回失败原因
CurrentFilename() string 返回当前活跃日志文件路径
Sync() error 将当前文件的内核缓冲刷写到磁盘,满足 zapcore.WriteSyncer 等需要 Sync 的接口
Close() error 关闭当前打开的日志文件,并等待本实例已调度的后台清理/压缩完成后才返回;不会终止后续写入

文件命名规则

假设配置为:

Filename: "/var/log/myapp/app.log"

按大小或 Daily 轮转时,备份文件格式为:

/var/log/myapp/app-2026-06-09T10-30-00.000.log

启用压缩后:

/var/log/myapp/app-2026-06-09T10-30-00.000.log.gz

启用 DailyFilename 后,活跃文件格式为:

/var/log/myapp/app-2026-06-09.log

致谢与来源

本包基于 lumberjack(作者 Nate Finch,MIT 许可证)改造,在其文件切割、压缩、清理能力之上新增了按天切割(Daily / DailyFilename)等功能。感谢原作者的工作。

License

Documentation

Overview

Package logrotate 提供日志文件写入、切割、压缩和清理能力。

使用方式:

import "github.com/gtkit/logrotate"

logrotate 只负责日志文件输出端的管理,不负责日志格式化、日志级别或字段编码。 它可以作为任何接收 io.Writer 的日志库输出目标,例如标准库 log、slog、 zap 和 logrus。

同一份日志文件只能由一个进程写入。多个进程使用相同配置写入同一文件会导致 不正确的轮转行为。

使用约束与已知限制

以下都是有意的设计取舍或该类库的固有约束:

  • 配置字段不可热改:导出字段必须在首次 Open/Write/Rotate/Cleanup 之前设置好, 运行中不要与这些操作并发修改,否则会发生 data race。需要不同配置请新建 Logger。
  • Open 是同步的:它会同步清理并压缩历史积压文件,积压多时启动会变慢,属于预期行为。 不想承担该开销时可以不调用 Open,让首次 Write 走后台异步清理。
  • 后台清理是全局单 worker:所有 Logger 实例共享一个后台清理/压缩 goroutine,多实例 同时压缩大文件会排队。副作用是 Close 等待本实例已调度的后台任务时,若该 worker 正 忙于其他实例的压缩,本实例的 Close 也会一并等待后返回。
  • 保留判断只看文件名,不看 modTime:MaxAge 与 MaxBackups 按文件名中的日期/时间戳判断, 不符合本包命名规则的文件会被忽略,既不会被清理也不会被误删。迁移老目录前请确认文件名 符合规则,否则旧文件不会被自动回收。
  • 同一份日志文件只能单进程写:本包不加文件锁,多进程共用同一 Filename 会导致轮转竞争。
  • 同步清理无超时:Open 与 Close 的清理/压缩没有超时控制,磁盘 IO 病态时理论上会阻塞, 属于已知边界。
Example

使用标准库 log 时,在应用启动时把 Logger 传给 SetOutput 即可。

log.SetOutput(&Logger{
	Filename:   "/var/log/myapp/foo.log",
	MaxSize:    500, // MB
	MaxBackups: 3,
	MaxAge:     28,   // 天
	Compress:   true, // 默认关闭
})

Index

Examples

Constants

View Source
const Version = "v1.1.0"

Variables

This section is empty.

Functions

This section is empty.

Types

type Logger

type Logger struct {
	// Filename 是当前活跃日志文件路径。备份日志会保存在同一目录。
	// 为空时使用 os.TempDir() 下的 <进程名>-logrotate.log。
	Filename string `json:"filename" yaml:"filename"`

	// MaxSize 是单个日志文件轮转前的最大大小,单位 MB。默认值为 100。
	MaxSize int `json:"maxsize" yaml:"maxsize"`

	// MaxAge 是旧日志最多保留天数,基于文件名里的时间戳计算。
	// 一天按 24 小时计算,可能与夏令时、闰秒等日历边界不完全一致。
	// 默认不按时间删除旧日志。
	MaxAge int `json:"maxage" yaml:"maxage"`

	// MaxBackups 是最多保留的旧日志数量。默认保留全部旧日志,但 MaxAge 仍可能删除它们。
	MaxBackups int `json:"maxbackups" yaml:"maxbackups"`

	// LocalTime 控制备份时间戳和日期文件名是否使用本地时间。默认使用 UTC。
	LocalTime bool `json:"localtime" yaml:"localtime"`

	// Compress 控制轮转后的旧日志是否使用 gzip 压缩。默认不压缩。
	Compress bool `json:"compress" yaml:"compress"`

	// Daily 控制是否在日期变化时轮转日志文件。日期边界默认按 UTC 判断,
	// LocalTime 为 true 时按本地时区判断。默认不按天轮转。
	Daily bool `json:"daily" yaml:"daily"`

	// DailyFilename 控制活跃日志文件名是否包含当前日期,格式为 name-2006-01-02.ext。
	// 日期默认使用 UTC,LocalTime 为 true 时使用本地时区。MaxSize 在每天的文件内仍然生效。
	DailyFilename bool `json:"dailyfilename" yaml:"dailyfilename"`

	// Now 返回当前时间。为空时使用 time.Now。
	// 主要用于测试或需要固定业务时间源的场景。请在首次使用前设置。
	Now func() time.Time `json:"-" yaml:"-"`

	// Location 控制备份时间戳、日期文件名、日期边界和 MaxAge 截止时间使用的时区。
	// 非空时优先于 LocalTime;为空且 LocalTime 为 true 时使用 time.Local;否则使用 UTC。
	Location *time.Location `json:"-" yaml:"-"`

	// OnError 接收后台清理、压缩或 panic recover 产生的错误。
	// 为空时后台错误保持非致命并被忽略。Cleanup 返回的同步错误不会重复调用 OnError。
	OnError func(error) `json:"-" yaml:"-"`
	// contains filtered or unexported fields
}

Logger 是写入指定日志文件的 io.WriteCloser。

Logger 会在首次 Write 时打开或创建日志文件。如果文件已存在且小于 MaxSize 兆字节,Logger 会追加写入该文件。如果文件大小大于或等于 MaxSize,Logger 会在文件扩展名前加入当前时间戳作为备份文件名,并用原始文件名创建新的日志文件。

当一次写入会导致当前日志文件超过 MaxSize 时,Logger 会关闭当前文件、重命名为 备份文件,并用原始文件名创建新文件。因此 Filename 始终表示当前活跃日志文件。

如果 Daily 为 true,Logger 会在日期变化时轮转当前文件。日期边界默认使用 UTC, LocalTime 为 true 时使用本地时间。MaxSize 仍然生效,因此可以同时按天和按大小 轮转。

如果 DailyFilename 为 true,活跃日志文件名会包含当前日期,格式为 `name-2006-01-02.ext`。跨天时 Logger 会关闭旧日期文件,并开始写入新日期文件。 MaxSize 在每天的文件内仍然生效,按大小生成的备份仍使用普通时间戳格式。

备份文件使用 Logger 的日志文件名生成,格式为 `name-timestamp.ext`。其中 name 是不含扩展名的文件名,timestamp 是轮转时间,格式为 `2006-01-02T15-04-05.000`, ext 是原始扩展名。例如 Filename 为 `/var/log/foo/server.log` 时,备份文件可能是 `/var/log/foo/server-2026-06-09T10-30-00.000.log`。

清理旧日志文件

每次创建新日志文件后,Logger 可能会删除旧日志。按文件名里的时间戳排序后,最多 保留 MaxBackups 组旧日志;MaxBackups 为 0 时不按数量删除。时间戳早于 MaxAge 天的旧日志会被删除;MaxAge 为 0 时不按时间删除。文件名里的时间是轮转时间, 可能不同于该文件最后一次写入的时间。

如果 MaxBackups 和 MaxAge 都是 0,不会自动删除旧日志。

Logger 会串行化并发写入,以保证大小统计、轮转判断和写入顺序一致。请在首次使用前 设置好导出的配置字段,使用过程中不要与 Write、Rotate 或 Close 并发修改这些字段。

Logger 内部持有锁,首次使用后不得复制;请始终通过指针(*Logger)传递和使用。

func (*Logger) Cleanup added in v1.1.0

func (l *Logger) Cleanup() error

Cleanup 同步执行一次旧日志压缩和清理,并返回遇到的第一个错误。

Cleanup 不通过后台 worker 调度,也不会为返回的同步错误调用 OnError。需要启动即回收 历史积压文件时,应用可以在完成配置后显式调用 Cleanup 或 Open。

func (*Logger) Close

func (l *Logger) Close() error

Close 实现 io.Closer,关闭当前打开的日志文件,并等待本 Logger 已调度的后台 清理与压缩任务全部完成后才返回。

因此 Close 返回后,由本 Logger 触发的旧日志删除和压缩都已结束处理(清理或压缩 自身的失败会被忽略,不保证一定成功),不会有清理 goroutine 在 Close 之后继续运行。 Close 不会使 Logger 进入终止状态,之后的 Write 可以按现有配置重新打开日志文件。 Close 期间不要并发调用 Write 或 Rotate。

func (*Logger) CurrentFilename added in v1.1.0

func (l *Logger) CurrentFilename() string

CurrentFilename 返回当前活跃日志文件路径。

DailyFilename 关闭时返回 Filename 或默认文件名;DailyFilename 开启时返回当前日期对应 的活跃文件名。若内部已经切换到某个日期文件,则返回该文件名。

func (*Logger) Open added in v1.1.0

func (l *Logger) Open() error

Open 打开或创建当前活跃日志文件,并同步执行一次旧日志清理/压缩。

调用 Open 可在应用启动时初始化目录、创建当前文件并清理历史积压文件,而无需等待首次 Write。Open 不会使 Logger 进入终止状态;Close 后仍可再次 Open 或 Write。 Open 期间不要并发调用 Write、Rotate 或 Close。

func (*Logger) Rotate

func (l *Logger) Rotate() error

Rotate 关闭现有日志文件并立即创建新文件。 应用可以用它在正常轮转规则之外主动触发轮转,例如响应 SIGHUP。 轮转后,Logger 会按配置触发旧日志压缩和清理。

Example

响应 SIGHUP 时可以主动轮转日志文件。

l := &Logger{}
log.SetOutput(l)
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGHUP)

go func() {
	for {
		<-c
		l.Rotate()
	}
}()

func (*Logger) Sync added in v1.0.1

func (l *Logger) Sync() error

Sync 将当前日志文件的内核缓冲刷写到磁盘。

它让 Logger 满足 zap 的 zapcore.WriteSyncer 等需要 Sync 的接口,可在需要持久化 保证的场景显式调用。当前没有打开的文件时返回 nil。

func (*Logger) Write

func (l *Logger) Write(p []byte) (n int, err error)

Write 实现 io.Writer。写入会导致当前日志文件超过 MaxSize 时,Logger 会关闭当前文件、 将其重命名为带时间戳的备份文件,并用原始文件名创建新日志文件。 如果单次写入长度超过 MaxSize,Write 返回错误。

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL