github.com/phuslu/log@v1.0.100/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)