github.com/oarkflow/log@v1.0.78/file.go (about) 1 package log 2 3 import ( 4 "crypto/md5" 5 "io" 6 "os" 7 "path/filepath" 8 "runtime" 9 "sort" 10 "strconv" 11 "strings" 12 "sync" 13 "time" 14 ) 15 16 // FileWriter is an Writer that writes to the specified filename. 17 // 18 // Backups use the log file name given to FileWriter, in the form 19 // `name.timestamp.ext` where name is the filename without the extension, 20 // timestamp is the time at which the log was rotated formatted with the 21 // time.Time format of `2006-01-02T15-04-05` and the extension is the 22 // original extension. For example, if your FileWriter.Filename is 23 // `/var/log/foo/server.log`, a backup created at 6:30pm on Nov 11 2016 would 24 // use the filename `/var/log/foo/server.2016-11-04T18-30-00.log` 25 // 26 // # Cleaning Up Old Log Files 27 // 28 // Whenever a new logfile gets created, old log files may be deleted. The most 29 // recent files according to filesystem modified time will be retained, up to a 30 // number equal to MaxBackups (or all of them if MaxBackups is 0). Note that the 31 // time encoded in the timestamp is the rotation time, which may differ from the 32 // last time that file was written to . 33 type FileWriter struct { 34 // Filename is the file to write logs to. Backup log files will be retained 35 // in the same directory. 36 Filename string 37 38 // MaxSize is the maximum size in bytes of the log file before it gets rotated. 39 MaxSize int64 40 41 // MaxBackups is the maximum number of old log files to retain. The default 42 // is to retain all old log files 43 MaxBackups int 44 45 // make aligncheck happy 46 mu sync.Mutex 47 size int64 48 file *os.File 49 50 // FileMode represents the file's mode and permission bits. The default 51 // mode is 0644 52 FileMode os.FileMode 53 54 // TimeFormat specifies the time format of filename, uses `2006-01-02T15-04-05` as default format. 55 // If set with `TimeFormatUnix`, `TimeFormatUnixMs`, times are formated as UNIX timestamp. 56 TimeFormat string 57 58 // LocalTime determines if the time used for formatting the timestamps in 59 // log files is the computer's local time. The default is to use UTC time. 60 LocalTime bool 61 62 // HostName determines if the hostname used for formatting in log files. 63 HostName bool 64 65 // ProcessID determines if the pid used for formatting in log files. 66 ProcessID bool 67 68 // EnsureFolder ensures the file directory creation before writing. 69 EnsureFolder bool 70 71 // Header specifies an optional header function of log file after rotation, 72 Header func(fileinfo os.FileInfo) []byte 73 74 // Cleaner specifies an optional cleanup function of log backups after rotation, 75 // if not set, the default behavior is to delete more than MaxBackups log files. 76 Cleaner func(filename string, maxBackups int, matches []os.FileInfo) 77 } 78 79 // WriteEntry implements Writer. If a write would cause the log file to be larger 80 // than MaxSize, the file is closed, rotate to include a timestamp of the 81 // current time, and update symlink with log name file to the new file. 82 func (w *FileWriter) WriteEntry(e *Entry) (n int, err error) { 83 w.mu.Lock() 84 n, err = w.write(e.buf) 85 w.mu.Unlock() 86 return 87 } 88 89 // Write implements io.Writer. If a write would cause the log file to be larger 90 // than MaxSize, the file is closed, rotate to include a timestamp of the 91 // current time, and update symlink with log name file to the new file. 92 func (w *FileWriter) Write(p []byte) (n int, err error) { 93 w.mu.Lock() 94 n, err = w.write(p) 95 w.mu.Unlock() 96 return 97 } 98 99 func (w *FileWriter) write(p []byte) (n int, err error) { 100 if w.file == nil { 101 if w.Filename == "" { 102 n, err = os.Stderr.Write(p) 103 return 104 } 105 if w.EnsureFolder { 106 err = os.MkdirAll(filepath.Dir(w.Filename), 0755) 107 if err != nil { 108 return 109 } 110 } 111 err = w.create() 112 if err != nil { 113 return 114 } 115 } 116 117 n, err = w.file.Write(p) 118 if err != nil { 119 return 120 } 121 122 w.size += int64(n) 123 if w.MaxSize > 0 && w.size > w.MaxSize && w.Filename != "" { 124 err = w.rotate() 125 } 126 127 return 128 } 129 130 // Close implements io.Closer, and closes the current logfile. 131 func (w *FileWriter) Close() (err error) { 132 w.mu.Lock() 133 if w.file != nil { 134 err = w.file.Close() 135 w.file = nil 136 w.size = 0 137 } 138 w.mu.Unlock() 139 return 140 } 141 142 // Rotate causes Logger to close the existing log file and immediately create a 143 // new one. This is a helper function for applications that want to initiate 144 // rotations outside of the normal rotation rules, such as in response to 145 // SIGHUP. After rotating, this initiates compression and removal of old log 146 // files according to the configuration. 147 func (w *FileWriter) Rotate() (err error) { 148 w.mu.Lock() 149 err = w.rotate() 150 w.mu.Unlock() 151 return 152 } 153 154 func (w *FileWriter) rotate() (err error) { 155 var file *os.File 156 file, err = os.OpenFile(w.fileargs(timeNow())) 157 if err != nil { 158 return err 159 } 160 if w.file != nil { 161 w.file.Close() 162 } 163 w.file = file 164 w.size = 0 165 166 if w.Header != nil { 167 st, err := file.Stat() 168 if err != nil { 169 return err 170 } 171 if b := w.Header(st); b != nil { 172 n, err := w.file.Write(b) 173 w.size += int64(n) 174 if err != nil { 175 return nil 176 } 177 } 178 } 179 180 go func(newname string) { 181 os.Remove(w.Filename) 182 if !w.ProcessID { 183 _ = os.Symlink(filepath.Base(newname), w.Filename) 184 } 185 186 uid, _ := strconv.Atoi(os.Getenv("SUDO_UID")) 187 gid, _ := strconv.Atoi(os.Getenv("SUDO_GID")) 188 if uid != 0 && gid != 0 && os.Geteuid() == 0 { 189 _ = os.Lchown(w.Filename, uid, gid) 190 _ = os.Chown(newname, uid, gid) 191 } 192 193 dir := filepath.Dir(w.Filename) 194 dirfile, err := os.Open(dir) 195 if err != nil { 196 return 197 } 198 infos, err := dirfile.Readdir(-1) 199 dirfile.Close() 200 if err != nil { 201 return 202 } 203 204 base, ext := filepath.Base(w.Filename), filepath.Ext(w.Filename) 205 prefix, extgz := base[:len(base)-len(ext)]+".", ext+".gz" 206 exclude := prefix + "error" + ext 207 208 matches := make([]os.FileInfo, 0) 209 for _, info := range infos { 210 name := info.Name() 211 if name != base && name != exclude && 212 strings.HasPrefix(name, prefix) && 213 (strings.HasSuffix(name, ext) || strings.HasSuffix(name, extgz)) { 214 matches = append(matches, info) 215 } 216 } 217 sort.Slice(matches, func(i, j int) bool { 218 return matches[i].ModTime().Unix() < matches[j].ModTime().Unix() 219 }) 220 221 if w.Cleaner != nil { 222 w.Cleaner(w.Filename, w.MaxBackups, matches) 223 } else { 224 for i := 0; i < len(matches)-w.MaxBackups-1; i++ { 225 os.Remove(filepath.Join(dir, matches[i].Name())) 226 } 227 } 228 }(w.file.Name()) 229 230 return 231 } 232 233 func (w *FileWriter) create() (err error) { 234 w.file, err = os.OpenFile(w.fileargs(timeNow())) 235 if err != nil { 236 return err 237 } 238 w.size = 0 239 st, err := w.file.Stat() 240 if err == nil { 241 w.size = st.Size() 242 } 243 244 if w.size == 0 && w.Header != nil { 245 if b := w.Header(st); b != nil { 246 n, err := w.file.Write(b) 247 w.size += int64(n) 248 if err != nil { 249 return err 250 } 251 } 252 } 253 254 os.Remove(w.Filename) 255 if !w.ProcessID { 256 _ = os.Symlink(filepath.Base(w.file.Name()), w.Filename) 257 } 258 259 return 260 } 261 262 // fileargs returns a new filename, flag, perm based on the original name and the given time. 263 func (w *FileWriter) fileargs(now time.Time) (filename string, flag int, perm os.FileMode) { 264 if !w.LocalTime { 265 now = now.UTC() 266 } 267 268 // filename 269 ext := filepath.Ext(w.Filename) 270 prefix := w.Filename[0 : len(w.Filename)-len(ext)] 271 switch w.TimeFormat { 272 case "": 273 filename = prefix + now.Format(".2006-01-02T15-04-05") 274 case TimeFormatUnix: 275 filename = prefix + "." + strconv.FormatInt(now.Unix(), 10) 276 case TimeFormatUnixMs: 277 filename = prefix + "." + strconv.FormatInt(now.UnixNano()/1000000, 10) 278 default: 279 filename = prefix + "." + now.Format(w.TimeFormat) 280 } 281 if w.HostName { 282 if w.ProcessID { 283 filename += "." + hostname + "-" + strconv.Itoa(pid) + ext 284 } else { 285 filename += "." + hostname + ext 286 } 287 } else { 288 if w.ProcessID { 289 filename += "." + strconv.Itoa(pid) + ext 290 } else { 291 filename += ext 292 } 293 } 294 295 // flag 296 flag = os.O_APPEND | os.O_CREATE | os.O_WRONLY 297 298 // perm 299 perm = w.FileMode 300 if perm == 0 { 301 perm = 0644 302 } 303 304 return 305 } 306 307 var hostname, machine = func() (string, [16]byte) { 308 // host 309 host, err := os.Hostname() 310 if err != nil || strings.HasPrefix(host, "localhost") { 311 host = "localhost-" + strconv.FormatInt(int64(Fastrandn(1000000)), 10) 312 } 313 // seed files 314 var files []string 315 switch runtime.GOOS { 316 case "linux": 317 files = []string{"/etc/machine-id", "/proc/self/cpuset"} 318 case "freebsd": 319 files = []string{"/etc/hostid"} 320 } 321 // append seed to hostname 322 data := []byte(host) 323 for _, file := range files { 324 if b, err := os.ReadFile(file); err == nil { 325 data = append(data, b...) 326 } 327 } 328 // md5 digest 329 hex := md5.Sum(data) 330 331 return host, hex 332 }() 333 334 var pid = os.Getpid() 335 336 var _ Writer = (*FileWriter)(nil) 337 var _ io.Writer = (*FileWriter)(nil)