github.com/gitbundle/modules@v0.0.0-20231025071548-85b91c5c3b01/log/file.go (about) 1 // Copyright 2023 The GitBundle Inc. All rights reserved. 2 // Copyright 2017 The Gitea Authors. All rights reserved. 3 // Use of this source code is governed by a MIT-style 4 // license that can be found in the LICENSE file. 5 6 // Copyright 2014 The Gogs Authors. All rights reserved. 7 // Use of this source code is governed by a MIT-style 8 // license that can be found in the LICENSE file. 9 10 package log 11 12 import ( 13 "bufio" 14 "compress/gzip" 15 "errors" 16 "fmt" 17 "os" 18 "path/filepath" 19 "strings" 20 "sync" 21 "time" 22 23 "github.com/gitbundle/modules/json" 24 "github.com/gitbundle/modules/util" 25 ) 26 27 // FileLogger implements LoggerProvider. 28 // It writes messages by lines limit, file size limit, or time frequency. 29 type FileLogger struct { 30 WriterLogger 31 mw *MuxWriter 32 // The opened file 33 Filename string `json:"filename"` 34 35 // Rotate at size 36 Maxsize int `json:"maxsize"` 37 maxsizeCursize int 38 39 // Rotate daily 40 Daily bool `json:"daily"` 41 Maxdays int64 `json:"maxdays"` 42 dailyOpenDate int 43 44 Rotate bool `json:"rotate"` 45 46 Compress bool `json:"compress"` 47 CompressionLevel int `json:"compressionLevel"` 48 49 startLock sync.Mutex // Only one log can write to the file 50 } 51 52 // MuxWriter an *os.File writer with locker. 53 type MuxWriter struct { 54 mu sync.Mutex 55 fd *os.File 56 owner *FileLogger 57 } 58 59 // Write writes to os.File. 60 func (mw *MuxWriter) Write(b []byte) (int, error) { 61 mw.mu.Lock() 62 defer mw.mu.Unlock() 63 mw.owner.docheck(len(b)) 64 return mw.fd.Write(b) 65 } 66 67 // Close the internal writer 68 func (mw *MuxWriter) Close() error { 69 return mw.fd.Close() 70 } 71 72 // SetFd sets os.File in writer. 73 func (mw *MuxWriter) SetFd(fd *os.File) { 74 if mw.fd != nil { 75 mw.fd.Close() 76 } 77 mw.fd = fd 78 } 79 80 // NewFileLogger create a FileLogger returning as LoggerProvider. 81 func NewFileLogger() LoggerProvider { 82 log := &FileLogger{ 83 Filename: "", 84 Maxsize: 1 << 28, // 256 MB 85 Daily: true, 86 Maxdays: 7, 87 Rotate: true, 88 Compress: true, 89 CompressionLevel: gzip.DefaultCompression, 90 } 91 log.Level = TRACE 92 // use MuxWriter instead direct use os.File for lock write when rotate 93 log.mw = new(MuxWriter) 94 log.mw.owner = log 95 96 return log 97 } 98 99 // Init file logger with json config. 100 // config like: 101 // 102 // { 103 // "filename":"log/gogs.log", 104 // "maxsize":1<<30, 105 // "daily":true, 106 // "maxdays":15, 107 // "rotate":true 108 // } 109 func (log *FileLogger) Init(config string) error { 110 if err := json.Unmarshal([]byte(config), log); err != nil { 111 return fmt.Errorf("Unable to parse JSON: %v", err) 112 } 113 if len(log.Filename) == 0 { 114 return errors.New("config must have filename") 115 } 116 // set MuxWriter as Logger's io.Writer 117 log.NewWriterLogger(log.mw) 118 return log.StartLogger() 119 } 120 121 // StartLogger start file logger. create log file and set to locker-inside file writer. 122 func (log *FileLogger) StartLogger() error { 123 fd, err := log.createLogFile() 124 if err != nil { 125 return err 126 } 127 log.mw.SetFd(fd) 128 return log.initFd() 129 } 130 131 func (log *FileLogger) docheck(size int) { 132 log.startLock.Lock() 133 defer log.startLock.Unlock() 134 if log.Rotate && ((log.Maxsize > 0 && log.maxsizeCursize >= log.Maxsize) || 135 (log.Daily && time.Now().Day() != log.dailyOpenDate)) { 136 if err := log.DoRotate(); err != nil { 137 fmt.Fprintf(os.Stderr, "FileLogger(%q): %s\n", log.Filename, err) 138 return 139 } 140 } 141 log.maxsizeCursize += size 142 } 143 144 func (log *FileLogger) createLogFile() (*os.File, error) { 145 // Open the log file 146 return os.OpenFile(log.Filename, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0o660) 147 } 148 149 func (log *FileLogger) initFd() error { 150 fd := log.mw.fd 151 finfo, err := fd.Stat() 152 if err != nil { 153 return fmt.Errorf("get stat: %v", err) 154 } 155 log.maxsizeCursize = int(finfo.Size()) 156 log.dailyOpenDate = time.Now().Day() 157 return nil 158 } 159 160 // DoRotate means it need to write file in new file. 161 // new file name like xx.log.2013-01-01.2 162 func (log *FileLogger) DoRotate() error { 163 _, err := os.Lstat(log.Filename) 164 if err == nil { // file exists 165 // Find the next available number 166 num := 1 167 fname := "" 168 for ; err == nil && num <= 999; num++ { 169 fname = log.Filename + fmt.Sprintf(".%s.%03d", time.Now().Format("2006-01-02"), num) 170 _, err = os.Lstat(fname) 171 if log.Compress && err != nil { 172 _, err = os.Lstat(fname + ".gz") 173 } 174 } 175 // return error if the last file checked still existed 176 if err == nil { 177 return fmt.Errorf("rotate: cannot find free log number to rename %s", log.Filename) 178 } 179 180 fd := log.mw.fd 181 fd.Close() 182 183 // close fd before rename 184 // Rename the file to its newfound home 185 if err = util.Rename(log.Filename, fname); err != nil { 186 return fmt.Errorf("Rotate: %v", err) 187 } 188 189 if log.Compress { 190 go compressOldLogFile(fname, log.CompressionLevel) 191 } 192 193 // re-start logger 194 if err = log.StartLogger(); err != nil { 195 return fmt.Errorf("Rotate StartLogger: %v", err) 196 } 197 198 go log.deleteOldLog() 199 } 200 201 return nil 202 } 203 204 func compressOldLogFile(fname string, compressionLevel int) error { 205 reader, err := os.Open(fname) 206 if err != nil { 207 return err 208 } 209 defer reader.Close() 210 buffer := bufio.NewReader(reader) 211 fw, err := os.OpenFile(fname+".gz", os.O_WRONLY|os.O_CREATE, 0o660) 212 if err != nil { 213 return err 214 } 215 defer fw.Close() 216 zw, err := gzip.NewWriterLevel(fw, compressionLevel) 217 if err != nil { 218 return err 219 } 220 defer zw.Close() 221 _, err = buffer.WriteTo(zw) 222 if err != nil { 223 zw.Close() 224 fw.Close() 225 util.Remove(fname + ".gz") 226 return err 227 } 228 reader.Close() 229 return util.Remove(fname) 230 } 231 232 func (log *FileLogger) deleteOldLog() { 233 dir := filepath.Dir(log.Filename) 234 _ = filepath.Walk(dir, func(path string, info os.FileInfo, err error) (returnErr error) { 235 defer func() { 236 if r := recover(); r != nil { 237 returnErr = fmt.Errorf("Unable to delete old log '%s', error: %+v", path, r) 238 } 239 }() 240 241 if !info.IsDir() && info.ModTime().Unix() < (time.Now().Unix()-60*60*24*log.Maxdays) { 242 if strings.HasPrefix(filepath.Base(path), filepath.Base(log.Filename)) { 243 if err := util.Remove(path); err != nil { 244 returnErr = fmt.Errorf("Failed to remove %s: %v", path, err) 245 } 246 } 247 } 248 return returnErr 249 }) 250 } 251 252 // Content returns the content accumulated in the content provider 253 func (log *FileLogger) Content() (string, error) { 254 b, err := os.ReadFile(log.Filename) 255 if err != nil { 256 return "", err 257 } 258 return string(b), nil 259 } 260 261 // Flush flush file logger. 262 // there are no buffering messages in file logger in memory. 263 // flush file means sync file from disk. 264 func (log *FileLogger) Flush() { 265 _ = log.mw.fd.Sync() 266 } 267 268 // ReleaseReopen releases and reopens log files 269 func (log *FileLogger) ReleaseReopen() error { 270 closingErr := log.mw.fd.Close() 271 startingErr := log.StartLogger() 272 if startingErr != nil { 273 if closingErr != nil { 274 return fmt.Errorf("Error during closing: %v Error during starting: %v", closingErr, startingErr) 275 } 276 return startingErr 277 } 278 return closingErr 279 } 280 281 // GetName returns the default name for this implementation 282 func (log *FileLogger) GetName() string { 283 return "file" 284 } 285 286 func init() { 287 Register("file", NewFileLogger) 288 }