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