github.com/kaydxh/golang@v0.0.131/pkg/file-rotate/rotate_file.go (about) 1 /* 2 *Copyright (c) 2022, kaydxh 3 * 4 *Permission is hereby granted, free of charge, to any person obtaining a copy 5 *of this software and associated documentation files (the "Software"), to deal 6 *in the Software without restriction, including without limitation the rights 7 *to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 *copies of the Software, and to permit persons to whom the Software is 9 *furnished to do so, subject to the following conditions: 10 * 11 *The above copyright notice and this permission notice shall be included in all 12 *copies or substantial portions of the Software. 13 * 14 *THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 *IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 *FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 *AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 *LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 *OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 *SOFTWARE. 21 */ 22 package rotatefile 23 24 import ( 25 "context" 26 "fmt" 27 "io" 28 "os" 29 "path/filepath" 30 "regexp" 31 "sort" 32 "strconv" 33 "strings" 34 "sync" 35 "time" 36 37 os_ "github.com/kaydxh/golang/go/os" 38 filepath_ "github.com/kaydxh/golang/go/path/filepath" 39 time_ "github.com/kaydxh/golang/go/time" 40 cleanup_ "github.com/kaydxh/golang/pkg/file-cleanup" 41 ) 42 43 type EventCallbackFunc func(ctx context.Context, path string) 44 45 type RotateFiler struct { 46 file *os.File 47 filedir string 48 curFilepath string 49 seq uint64 50 linkpath string 51 mu sync.Mutex 52 opts struct { 53 prefixName string 54 fileTimeLayout string //default "20060102150405" ,take effect if rotateInterval > 0 55 56 subfixName string 57 //maxAge is the maximum number of time to retain old files, 0 is unlimited 58 maxAge time.Duration 59 //maxCount is the maximum number to retain old files, 0 is unlimited 60 maxCount int64 61 62 //rotate file when file size larger than rotateSize 63 rotateSize int64 64 //rotate file in rotateInterval 65 rotateInterval time.Duration 66 syncInterval time.Duration 67 rotateCallbackFunc EventCallbackFunc 68 } 69 } 70 71 func NewRotateFiler(filedir string, options ...RotateFilerOption) (*RotateFiler, error) { 72 r := &RotateFiler{ 73 filedir: filedir, 74 } 75 r.ApplyOptions(options...) 76 77 if r.linkpath == "" { 78 r.linkpath = filepath.Base(os.Args[0]) + ".log" 79 } 80 81 // if need rotate file with rotateInterval, set default timelayout 82 if r.opts.rotateInterval > 0 { 83 if r.opts.fileTimeLayout == "" { 84 r.opts.fileTimeLayout = time_.ShortTimeFormat 85 } 86 } 87 88 if r.opts.rotateCallbackFunc != nil { 89 if r.opts.syncInterval == 0 { 90 r.opts.syncInterval = 30 * time.Second 91 } 92 go r.watch() 93 } 94 95 return r, nil 96 } 97 98 // /data/log/1%%%AA20160304 -> /data/log/1*A20160304* 99 func globFromFileTimeLayout(filePath string) string { 100 regexps := []*regexp.Regexp{ 101 regexp.MustCompile(`%[%+A-Za-z]`), 102 regexp.MustCompile(`\*+`), 103 } 104 105 for _, re := range regexps { 106 filePath = re.ReplaceAllString(filePath, "*") 107 } 108 return filePath + "*" 109 } 110 111 func (f *RotateFiler) Write(p []byte) (file *os.File, n int, err error) { 112 f.mu.Lock() 113 defer f.mu.Unlock() 114 115 out, err := f.getWriterNolock(int64(len(p))) 116 if err != nil { 117 return nil, 0, err 118 } 119 120 n, err = out.Write(p) 121 return f.file, n, err 122 } 123 124 func (f *RotateFiler) WriteBytesLine(p [][]byte) (file *os.File, n int, err error) { 125 126 var data []byte 127 for _, d := range p { 128 data = append(data, d...) 129 data = append(data, '\n') 130 } 131 return f.Write(data) 132 } 133 134 func (f *RotateFiler) generateRotateFilename() string { 135 if f.opts.rotateInterval > 0 { 136 now := time.Now() 137 return time_.TruncateToUTCString(now, f.opts.rotateInterval, f.opts.fileTimeLayout) 138 } 139 return "" 140 } 141 142 func (f *RotateFiler) watch() { 143 timer := time.NewTicker(f.opts.syncInterval) 144 defer timer.Stop() 145 146 for { 147 select { 148 case <-timer.C: 149 func() { 150 f.mu.Lock() 151 defer f.mu.Unlock() 152 f.getWriterNolock(0) 153 }() 154 } 155 } 156 } 157 158 func (f *RotateFiler) getWriterNolock(length int64) (io.Writer, error) { 159 basename := f.generateRotateFilename() 160 filename := f.opts.prefixName + basename + f.opts.subfixName 161 if filename == "" { 162 filename = "default.log" 163 } 164 165 // first rotate log file, maybe /data/logs/logs.test20210917230000.log 166 filePath := filepath.Join(f.filedir, filename) 167 globPath := filepath.Join(filepath.Dir(filePath), f.opts.prefixName) 168 169 // current log file, maybe /data/logs/logs.test20210917230000.log.1 170 if f.curFilepath == "" { 171 f.curFilepath, _ = f.getCurSeqFilename(globPath) 172 f.seq = f.extractSeq(f.curFilepath) 173 } 174 175 // if curFilePath is different rotated time with filename, need reset curFilePath 176 if !strings.Contains(f.curFilepath, filename) { 177 f.curFilepath = filePath 178 f.seq = 0 179 } 180 181 rotated := false 182 183 fi, err := os.Stat(f.curFilepath) 184 if err != nil { 185 if !os.IsNotExist(err) { 186 return nil, fmt.Errorf("failed to get file info, err: %v", err) 187 } 188 //file is not exist, think just like rotating file 189 rotated = true 190 } 191 192 //rotate file by size 193 if err == nil && f.opts.rotateSize > 0 && (fi.Size()+length) > f.opts.rotateSize { 194 195 f.curFilepath, err = f.generateNextSeqFilename(filePath) 196 if err != nil { 197 return nil, fmt.Errorf("failed to generate rotate file name by seq, err: %v", err) 198 } 199 200 rotated = true 201 } 202 203 if f.file == nil || rotated { 204 fn, err := os_.OpenFile(f.curFilepath, true) 205 if err != nil { 206 return nil, fmt.Errorf("failed to create file: %v, err: %v", f.curFilepath, err) 207 } 208 209 if f.file != nil { 210 //callback 211 if f.opts.rotateCallbackFunc != nil { 212 f.opts.rotateCallbackFunc(context.Background(), f.file.Name()) 213 } 214 f.file.Close() 215 } 216 f.file = fn 217 218 f.seq = f.extractSeq(f.curFilepath) 219 220 os_.SymLink(f.curFilepath, f.linkpath) 221 222 globFile := globFromFileTimeLayout(globPath) 223 224 go cleanup_.FileCleanup(globFile, cleanup_.WithMaxAge(f.opts.maxAge), cleanup_.WithMaxCount(f.opts.maxCount)) 225 } 226 227 return f.file, nil 228 } 229 230 //filename like foo foo.1 foo.2 ... 231 func (f *RotateFiler) generateNextSeqFilename(filePath string) (string, error) { 232 233 var newFilePath string 234 seq := f.seq 235 236 for { 237 if seq == 0 { 238 newFilePath = filePath 239 } else { 240 newFilePath = fmt.Sprintf("%s.%d", filePath, seq) 241 } 242 243 _, err := os.Stat(newFilePath) 244 if os.IsNotExist(err) { 245 f.seq = seq 246 return newFilePath, nil 247 } 248 if err != nil { 249 return "", err 250 } 251 //file exist, need to get next seq filename 252 seq++ 253 } 254 255 } 256 257 // globPath: log/logs.test 258 // globFile: [log/logs.test20211008081908.log log/logs.test20211008081908.log.1 log/logs.test20211008081908.log.2] 259 func (f *RotateFiler) getCurSeqFilename(globPath string) (string, error) { 260 261 globFile := globFromFileTimeLayout(globPath) 262 matches, err := filepath_.Glob(globFile) 263 if err != nil { 264 return "", err 265 } 266 if len(matches) == 0 { 267 return globPath, nil 268 } 269 270 sort.Sort(cleanup_.RotatedFiles(matches)) 271 return matches[len(matches)-1], nil 272 } 273 274 func (f *RotateFiler) extractSeq(filePath string) uint64 { 275 if filePath == "" { 276 return 0 277 } 278 279 ext := filepath.Ext(filePath) 280 if ext == "" { 281 return 0 282 } 283 284 seq, err := strconv.ParseUint(ext[1:], 10, 64) 285 if err != nil { 286 return 0 287 } 288 289 return seq 290 291 }