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 }