github.com/best4tires/kit@v1.0.5/log/rotate/writer.go (about)

     1  package rotate
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"os"
     7  	"path/filepath"
     8  
     9  	"github.com/best4tires/kit/log/entry"
    10  )
    11  
    12  const (
    13  	// KB is one kilobyte
    14  	KB int = 1024
    15  	// MB is one megabyte
    16  	MB int = KB * 1024
    17  	// GB is one gigabyte
    18  	GB int = MB * 1024
    19  )
    20  
    21  // Option is the type for a rotate.Writer create option
    22  type Option func(w *Writer) error
    23  
    24  // WithFileSize defines the maximum size for a log-file before it gets rotated
    25  func WithFileSize(s int) Option {
    26  	return func(w *Writer) error {
    27  		if s < KB || s > 10*GB {
    28  			return fmt.Errorf("invalid file-size: %d", s)
    29  		}
    30  		w.fileSize = s
    31  		return nil
    32  	}
    33  }
    34  
    35  // WithFileCount defines the maximum number of files before old files will be deleted
    36  func WithFileCount(c int) Option {
    37  	return func(w *Writer) error {
    38  		if c < 2 {
    39  			return fmt.Errorf("invalid file-count: %d", c)
    40  		}
    41  		w.fileCount = c
    42  		return nil
    43  	}
    44  }
    45  
    46  // WithFileOpErrHandler defines an error-handler for file operations
    47  func WithFileOpErrHandler(h func(err error)) Option {
    48  	return func(w *Writer) error {
    49  		w.onFileOpErr = h
    50  		return nil
    51  	}
    52  }
    53  
    54  // Writer implements the log.Writer interface.
    55  // It performs a log-file rotation based on the provided parameters
    56  type Writer struct {
    57  	file        io.WriteCloser
    58  	fileDir     string
    59  	fileBase    string
    60  	fileSize    int
    61  	fileCount   int
    62  	currSize    int
    63  	onFileOpErr func(error)
    64  	msgC        chan string
    65  	doneC       chan struct{}
    66  	stopC       chan struct{}
    67  }
    68  
    69  // NewWriter creates a new rotate.Writer
    70  func NewWriter(path string, opts ...Option) (*Writer, error) {
    71  	err := os.MkdirAll(filepath.Dir(path), os.ModePerm)
    72  	if err != nil {
    73  		return nil, fmt.Errorf("os.mkdirall %q: %w", filepath.Dir(path), err)
    74  	}
    75  	f, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
    76  	if err != nil {
    77  		return nil, fmt.Errorf("open-file %q: %w", path, err)
    78  	}
    79  	w := &Writer{
    80  		file:        f,
    81  		fileDir:     filepath.Dir(path),
    82  		fileBase:    filepath.Base(path),
    83  		fileSize:    1 * MB,
    84  		fileCount:   5,
    85  		currSize:    0,
    86  		onFileOpErr: func(err error) { panic(err) },
    87  		msgC:        make(chan string, 10000),
    88  		doneC:       make(chan struct{}),
    89  		stopC:       make(chan struct{}),
    90  	}
    91  	for _, opt := range opts {
    92  		err := opt(w)
    93  		if err != nil {
    94  			return nil, err
    95  		}
    96  	}
    97  	go func() {
    98  		defer close(w.doneC)
    99  		for {
   100  			select {
   101  			case <-w.stopC:
   102  				return
   103  			case s := <-w.msgC:
   104  				n, _ := w.file.Write([]byte(s + "\n"))
   105  				w.currSize += n
   106  				if w.currSize >= w.fileSize {
   107  					w.rotate()
   108  				}
   109  			}
   110  		}
   111  	}()
   112  	return w, nil
   113  }
   114  
   115  // Close closes the writer and all related resources
   116  func (w *Writer) Close() {
   117  	close(w.stopC)
   118  	<-w.doneC
   119  	w.file.Close()
   120  }
   121  
   122  // Write writes an entry to the current log-file
   123  func (w *Writer) Write(e entry.Entry) {
   124  	w.msgC <- fmt.Sprintf("%s [%s] [%s] [%s] %s", e.Time.Format("2006-01-02T15:04:05.000"), e.Program, e.Component, e.Level, e.Message)
   125  }
   126  
   127  func (w *Writer) rotate() {
   128  	handleErr := func(err error) {
   129  		if err != nil {
   130  			w.onFileOpErr(err)
   131  		}
   132  	}
   133  	w.file.Close()
   134  	pattern := filepath.Join(w.fileDir, w.fileBase+".*")
   135  	matches, _ := filepath.Glob(pattern)
   136  
   137  	//remove oldest
   138  	for len(matches) > w.fileCount {
   139  		last := matches[len(matches)-1]
   140  		err := os.Remove(last)
   141  		handleErr(err)
   142  		matches = matches[:len(matches)-1]
   143  	}
   144  
   145  	//rename others
   146  	for i := len(matches) - 1; i >= 0; i-- {
   147  		new := filepath.Join(w.fileDir, w.fileBase+fmt.Sprintf(".%03d", i+2))
   148  		err := os.Rename(matches[i], new)
   149  		handleErr(err)
   150  	}
   151  	err := os.Rename(filepath.Join(w.fileDir, w.fileBase), filepath.Join(w.fileDir, w.fileBase+".001"))
   152  	handleErr(err)
   153  
   154  	f, err := os.OpenFile(filepath.Join(w.fileDir, w.fileBase), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
   155  	handleErr(err)
   156  	w.file = f
   157  	w.currSize = 0
   158  }