Skip to main content

gva cutter 日志切割

你的 Cutter 实现了一个简单的日志切割功能,通过时间和自定义参数来生成日志文件名,并在每次写入时根据生成的文件名打开文件句柄进行写入。下面是对你代码的一些优化和改进建议:

优化建议

  1. 减少文件打开和关闭的频率

    • 当前实现中,每次写入都会打开和关闭文件句柄,这样会导致频繁的文件操作,影响性能。可以考虑在第一次写入时打开文件句柄,并在特定条件下(如文件大小或时间)才进行切割。
  2. 日志切割策略

    • 目前的实现没有具体的日志切割策略,比如按文件大小或时间间隔切割。可以考虑增加这些策略,使日志切割更加灵活。
  3. 错误处理

    • 在某些情况下,错误处理可以更加细致,例如在写入文件失败时应该记录错误日志或采取其他措施。
  4. 代码结构调整

    • 可以将文件名生成逻辑和文件操作逻辑分开,使代码更加清晰和可维护。

改进后的代码

下面是改进后的 Cutter 实现,增加了按时间间隔切割日志的功能,并优化了文件打开和关闭的频率:

package internal

import (
"os"
"path/filepath"
"sync"
"time"
)

// Cutter 实现 io.Writer 接口,用于日志切割
type Cutter struct {
level string // 日志级别
layout string // 时间格式
formats []string // 自定义参数
director string // 日志文件夹
file *os.File // 文件句柄
mutex *sync.RWMutex // 读写锁
lastCut time.Time // 上次切割时间
interval time.Duration // 日志切割间隔
}

// CutterOption 是用于配置 Cutter 的选项函数
type CutterOption func(*Cutter)

// CutterWithLayout 设置时间格式
func CutterWithLayout(layout string) CutterOption {
return func(c *Cutter) {
c.layout = layout
}
}

// CutterWithFormats 设置格式化参数
func CutterWithFormats(format ...string) CutterOption {
return func(c *Cutter) {
if len(format) > 0 {
c.formats = format
}
}
}

// CutterWithInterval 设置日志切割间隔
func CutterWithInterval(interval time.Duration) CutterOption {
return func(c *Cutter) {
c.interval = interval
}
}

// NewCutter 创建一个新的 Cutter 实例
func NewCutter(director string, level string, options ...CutterOption) *Cutter {
cutter := &Cutter{
level: level,
director: director,
mutex: new(sync.RWMutex),
interval: 24 * time.Hour, // 默认按天切割
}
for _, option := range options {
option(cutter)
}
return cutter
}

// Write 实现 io.Writer 接口,写入日志
func (c *Cutter) Write(bytes []byte) (n int, err error) {
c.mutex.Lock()
defer c.mutex.Unlock()

// 检查是否需要切割日志
if c.file == nil || time.Since(c.lastCut) >= c.interval {
if c.file != nil {
_ = c.file.Close()
}
err = c.rotateFile()
if err != nil {
return 0, err
}
}

return c.file.Write(bytes)
}

// rotateFile 切割日志文件
func (c *Cutter) rotateFile() error {
filename := c.generateFilename()
director := filepath.Dir(filename)
err := os.MkdirAll(director, os.ModePerm)
if err != nil {
return err
}

c.file, err = os.OpenFile(filename, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
if err != nil {
return err
}

c.lastCut = time.Now()
return nil
}

// generateFilename 生成日志文件名
func (c *Cutter) generateFilename() string {
length := len(c.formats)
values := make([]string, 0, 3+length)
values = append(values, c.director)
if c.layout != "" {
values = append(values, time.Now().Format(c.layout))
}
for _, format := range c.formats {
values = append(values, format)
}
values = append(values, c.level+".log")
return filepath.Join(values...)
}

// Sync 同步日志文件
func (c *Cutter) Sync() error {
c.mutex.Lock()
defer c.mutex.Unlock()

if c.file != nil {
return c.file.Sync()
}
return nil
}

解释改进点

  1. 减少文件打开和关闭的频率

    • Write 方法中,只有当文件句柄为空或达到切割间隔时才会进行文件切割和重新打开文件句柄。
  2. 日志切割策略

    • 增加了按时间间隔切割日志的功能,可以通过 CutterWithInterval 选项函数设置切割间隔。
  3. 错误处理

    • rotateFile 方法中,如果创建文件夹或打开文件失败,会返回错误。
  4. 代码结构调整

    • 将文件名生成逻辑抽象到了 generateFilename 方法中,使得 rotateFile 方法更加简洁。

通过这样的改进,你的 Cutter 实现变得更加高效、灵活和易于维护。

日志切割中,为什么使用锁

在实现日志切割功能时,使用锁(如 sync.RWMutex)是为了确保多线程环境下的并发安全。具体来说,锁的使用主要有以下几个原因:

1. 确保文件句柄的并发安全

在多线程环境中,多个 goroutine 可能会同时尝试写入日志文件。如果不使用锁来保护文件句柄的访问,可能会导致数据竞争和文件损坏。使用锁可以确保在任何时刻,只有一个 goroutine 能够操作文件句柄,从而避免并发问题。

2. 确保文件切割的并发安全

日志切割操作涉及到关闭当前文件句柄、生成新文件名、创建新文件等一系列步骤。这些操作必须是原子性的,以避免在切割过程中其他 goroutine 进行写操作,导致文件状态不一致。使用锁可以确保这些操作在一个 goroutine 完成之前,其他 goroutine 无法进行干扰。

3. 确保数据一致性

在执行写操作时,如果没有锁的保护,可能会出现部分日志写入旧文件,部分日志写入新文件的情况。通过使用锁,可以确保在切割操作完成之前,所有的写操作都被正确地同步到文件中,保证日志数据的一致性。

代码示例

以下是改进后的代码,展示了如何使用锁来确保并发安全:

package internal

import (
"os"
"path/filepath"
"sync"
"time"
)

// Cutter 实现 io.Writer 接口,用于日志切割
type Cutter struct {
level string // 日志级别
layout string // 时间格式
formats []string // 自定义参数
director string // 日志文件夹
file *os.File // 文件句柄
mutex *sync.RWMutex // 读写锁
lastCut time.Time // 上次切割时间
interval time.Duration // 日志切割间隔
}

// CutterOption 是用于配置 Cutter 的选项函数
type CutterOption func(*Cutter)

// CutterWithLayout 设置时间格式
func CutterWithLayout(layout string) CutterOption {
return func(c *Cutter) {
c.layout = layout
}
}

// CutterWithFormats 设置格式化参数
func CutterWithFormats(format ...string) CutterOption {
return func(c *Cutter) {
if len(format) > 0 {
c.formats = format
}
}
}

// CutterWithInterval 设置日志切割间隔
func CutterWithInterval(interval time.Duration) CutterOption {
return func(c *Cutter) {
c.interval = interval
}
}

// NewCutter 创建一个新的 Cutter 实例
func NewCutter(director string, level string, options ...CutterOption) *Cutter {
cutter := &Cutter{
level: level,
director: director,
mutex: new(sync.RWMutex),
interval: 24 * time.Hour, // 默认按天切割
}
for _, option := range options {
option(cutter)
}
return cutter
}

// Write 实现 io.Writer 接口,写入日志
func (c *Cutter) Write(bytes []byte) (n int, err error) {
c.mutex.Lock()
defer c.mutex.Unlock()

// 检查是否需要切割日志
if c.file == nil || time.Since(c.lastCut) >= c.interval {
if c.file != nil {
_ = c.file.Close()
}
err = c.rotateFile()
if err != nil {
return 0, err
}
}

return c.file.Write(bytes)
}

// rotateFile 切割日志文件
func (c *Cutter) rotateFile() error {
filename := c.generateFilename()
director := filepath.Dir(filename)
err := os.MkdirAll(director, os.ModePerm)
if err != nil {
return err
}

c.file, err = os.OpenFile(filename, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
if err != nil {
return err
}

c.lastCut = time.Now()
return nil
}

// generateFilename 生成日志文件名
func (c *Cutter) generateFilename() string {
length := len(c.formats)
values := make([]string, 0, 3+length)
values = append(values, c.director)
if c.layout != "" {
values = append(values, time.Now().Format(c.layout))
}
for _, format := range c.formats {
values = append(values, format)
}
values = append(values, c.level+".log")
return filepath.Join(values...)
}

// Sync 同步日志文件
func (c *Cutter) Sync() error {
c.mutex.Lock()
defer c.mutex.Unlock()

if c.file != nil {
return c.file.Sync()
}
return nil
}

具体锁的使用

  1. 初始化锁

    • NewCutter 函数中,初始化了一个 sync.RWMutex 实例。
  2. 写操作加锁

    • Write 方法中,使用 c.mutex.Lock()defer c.mutex.Unlock() 确保在整个写操作过程中只有一个 goroutine 可以操作文件句柄。
  3. 文件切割加锁

    • rotateFile 方法中,虽然没有显式的锁操作,但由于 rotateFile 是在 Write 方法内部调用的,而 Write 方法已经加锁,因此 rotateFile 的执行是线程安全的。
  4. 同步操作加锁

    • Sync 方法中,同样使用了 c.mutex.Lock()defer c.mutex.Unlock() 确保在同步操作过程中文件句柄的并发安全。

通过这些锁的使用,可以确保在多线程环境下,日志写入和切割操作的并发安全,避免数据竞争和文件损坏。