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 }