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  }