github.com/richardwilkes/toolbox@v1.121.0/log/rotation/rotator.go (about)

     1  // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved.
     2  //
     3  // This Source Code Form is subject to the terms of the Mozilla Public
     4  // License, version 2.0. If a copy of the MPL was not distributed with
     5  // this file, You can obtain one at http://mozilla.org/MPL/2.0/.
     6  //
     7  // This Source Code Form is "Incompatible With Secondary Licenses", as
     8  // defined by the Mozilla Public License, version 2.0.
     9  
    10  // Package rotation provides file rotation when files hit a given size.
    11  package rotation
    12  
    13  import (
    14  	"fmt"
    15  	"io"
    16  	"os"
    17  	"path/filepath"
    18  	"sync"
    19  
    20  	"github.com/richardwilkes/toolbox/errs"
    21  )
    22  
    23  var _ io.WriteCloser = &Rotator{}
    24  
    25  // Rotator holds the rotator data.
    26  type Rotator struct {
    27  	file       *os.File
    28  	path       string
    29  	maxSize    int64
    30  	maxBackups int
    31  	mask       os.FileMode
    32  	size       int64
    33  	lock       sync.Mutex
    34  }
    35  
    36  // New creates a new Rotator with the specified options.
    37  func New(options ...func(*Rotator) error) (*Rotator, error) {
    38  	r := &Rotator{
    39  		path:       DefaultPath(),
    40  		maxSize:    DefaultMaxSize,
    41  		maxBackups: DefaultMaxBackups,
    42  		mask:       0o777,
    43  	}
    44  	for _, option := range options {
    45  		if err := option(r); err != nil {
    46  			return nil, err
    47  		}
    48  	}
    49  	return r, nil
    50  }
    51  
    52  // PathToLog returns the path to the log file that will be used.
    53  func (r *Rotator) PathToLog() string {
    54  	return r.path
    55  }
    56  
    57  // Write implements io.Writer.
    58  func (r *Rotator) Write(b []byte) (int, error) {
    59  	r.lock.Lock()
    60  	defer r.lock.Unlock()
    61  	if r.file == nil {
    62  		fi, err := os.Stat(r.path)
    63  		switch {
    64  		case os.IsNotExist(err):
    65  			if err = os.MkdirAll(filepath.Dir(r.path), 0o755&r.mask); err != nil {
    66  				return 0, errs.Wrap(err)
    67  			}
    68  			file, fErr := os.OpenFile(r.path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o644&r.mask)
    69  			if fErr != nil {
    70  				return 0, errs.Wrap(fErr)
    71  			}
    72  			r.file = file
    73  			r.size = 0
    74  		case err != nil:
    75  			return 0, errs.Wrap(err)
    76  		default:
    77  			var file *os.File
    78  			if file, err = os.OpenFile(r.path, os.O_WRONLY|os.O_APPEND, 0o644&r.mask); err != nil {
    79  				return 0, errs.Wrap(err)
    80  			}
    81  			r.file = file
    82  			r.size = fi.Size()
    83  		}
    84  	}
    85  	writeSize := int64(len(b))
    86  	if r.size+writeSize > r.maxSize {
    87  		if err := r.rotate(); err != nil {
    88  			return 0, err
    89  		}
    90  	}
    91  	n, err := r.file.Write(b)
    92  	if err != nil {
    93  		err = errs.Wrap(err)
    94  	}
    95  	r.size += int64(n)
    96  	return n, err
    97  }
    98  
    99  // Sync commits the current contents of the file to stable storage.
   100  func (r *Rotator) Sync() error {
   101  	r.lock.Lock()
   102  	defer r.lock.Unlock()
   103  	if r.file == nil {
   104  		return nil
   105  	}
   106  	return errs.Wrap(r.file.Sync())
   107  }
   108  
   109  // Close implements io.Closer.
   110  func (r *Rotator) Close() error {
   111  	r.lock.Lock()
   112  	defer r.lock.Unlock()
   113  	if r.file == nil {
   114  		return nil
   115  	}
   116  	file := r.file
   117  	r.file = nil
   118  	return errs.Wrap(file.Close())
   119  }
   120  
   121  func (r *Rotator) rotate() error {
   122  	if r.file != nil {
   123  		err := r.file.Close()
   124  		r.file = nil
   125  		if err != nil {
   126  			return errs.Wrap(err)
   127  		}
   128  	}
   129  	if r.maxBackups < 1 {
   130  		if err := os.Remove(r.path); err != nil && !os.IsNotExist(err) {
   131  			return errs.Wrap(err)
   132  		}
   133  	} else {
   134  		if err := os.Remove(fmt.Sprintf("%s-%d", r.path, r.maxBackups)); err != nil && !os.IsNotExist(err) {
   135  			return errs.Wrap(err)
   136  		}
   137  		for i := r.maxBackups; i > 0; i-- {
   138  			var oldPath string
   139  			if i != 1 {
   140  				oldPath = fmt.Sprintf("%s-%d", r.path, i-1)
   141  			} else {
   142  				oldPath = r.path
   143  			}
   144  			if err := os.Rename(oldPath, fmt.Sprintf("%s-%d", r.path, i)); err != nil && !os.IsNotExist(err) {
   145  				return errs.Wrap(err)
   146  			}
   147  		}
   148  	}
   149  	file, err := os.OpenFile(r.path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o644&r.mask)
   150  	if err != nil {
   151  		return errs.Wrap(err)
   152  	}
   153  	r.file = file
   154  	r.size = 0
   155  	return nil
   156  }