file.go 6.36 KB
package logger

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"sync"
	"time"
)

type fileLogger struct {
	sync.RWMutex
	fileWriter *os.File

	Filename   string //`json:"filename"`
	Append     bool   `json:"append"`
	MaxLines   int    `json:"maxlines"`
	MaxSize    int    `json:"maxsize"`
	Daily      bool   `json:"daily"`
	MaxDays    int64  `json:"maxdays"`
	Level      string `json:"level"`
	PermitMask string `json:"permit"`

	LogLevel             int
	maxSizeCurSize       int
	maxLinesCurLines     int
	dailyOpenDate        int
	dailyOpenTime        time.Time
	fileNameOnly, suffix string
}

// Init file logger with json config.
// jsonConfig like:
//	{
//	"filename":"log/app.log",
//	"maxlines":10000,
//	"maxsize":1024,
//	"daily":true,
//	"maxdays":15,
//	"rotate":true,
//  	"permit":"0600"
//	}
func (f *fileLogger) Init(jsonConfig string, appName string) error {
	//fmt.Printf("fileLogger Init:%s\n", jsonConfig)
	if len(jsonConfig) == 0 {
		return nil
	}
	err := json.Unmarshal([]byte(jsonConfig), f)
	if err != nil {
		return err
	}
	f.Filename = appName
	if len(f.Filename) == 0 {
		return errors.New("jsonconfig must have filename")
	}
	f.suffix = filepath.Ext(f.Filename)
	f.fileNameOnly = strings.TrimSuffix(f.Filename, f.suffix)
	f.MaxSize *= 1024 * 1024 // 将单位转换成MB
	if f.suffix == "" {
		f.suffix = ".log"
		f.Filename += f.suffix
	}
	if l, ok := LevelMap[f.Level]; ok {
		f.LogLevel = l
	}
	err = f.newFile()
	return err
}

func (f *fileLogger) needCreateFresh(size int, day int) bool {
	return (f.MaxLines > 0 && f.maxLinesCurLines >= f.MaxLines) ||
		(f.MaxSize > 0 && f.maxSizeCurSize+size >= f.MaxSize) ||
		(f.Daily && day != f.dailyOpenDate)

}

// WriteMsg write logger message into file.
func (f *fileLogger) LogWrite(when time.Time, msgText interface{}, level int) error {
	msg, ok := msgText.(string)
	if !ok {
		return nil
	}
	if level > f.LogLevel {
		return nil
	}

	day := when.Day()
	msg += "\n"
	if f.Append {
		f.RLock()
		if f.needCreateFresh(len(msg), day) {
			f.RUnlock()
			f.Lock()
			if f.needCreateFresh(len(msg), day) {
				if err := f.createFreshFile(when); err != nil {
					fmt.Fprintf(os.Stdout, "createFreshFile(%q): %s\n", f.Filename, err)
				}
			}
			f.Unlock()
		} else {
			f.RUnlock()
		}
	}

	f.Lock()
	_, err := f.fileWriter.Write([]byte(msg))
	if err == nil {
		f.maxLinesCurLines++
		f.maxSizeCurSize += len(msg)
	}
	f.Unlock()
	return err
}

func (f *fileLogger) createLogFile() (*os.File, error) {
	// Open the log file
	perm, err := strconv.ParseInt(f.PermitMask, 8, 64)
	if err != nil {
		return nil, err
	}
	fd, err := os.OpenFile(f.Filename, os.O_WRONLY|os.O_APPEND|os.O_CREATE, os.FileMode(perm))
	if err == nil {
		// Make sure file perm is user set perm cause of `os.OpenFile` will obey umask
		os.Chmod(f.Filename, os.FileMode(perm))
	}
	return fd, err
}

func (f *fileLogger) newFile() error {
	file, err := f.createLogFile()
	if err != nil {
		return err
	}
	if f.fileWriter != nil {
		f.fileWriter.Close()
	}
	f.fileWriter = file

	fInfo, err := file.Stat()
	if err != nil {
		return fmt.Errorf("get stat err: %s", err)
	}
	f.maxSizeCurSize = int(fInfo.Size())
	f.dailyOpenTime = time.Now()
	f.dailyOpenDate = f.dailyOpenTime.Day()
	f.maxLinesCurLines = 0
	if f.maxSizeCurSize > 0 {
		count, err := f.lines()
		if err != nil {
			return err
		}
		f.maxLinesCurLines = count
	}
	return nil
}

func (f *fileLogger) lines() (int, error) {
	fd, err := os.Open(f.Filename)
	if err != nil {
		return 0, err
	}
	defer fd.Close()

	buf := make([]byte, 32768) // 32k
	count := 0
	lineSep := []byte{'\n'}

	for {
		c, err := fd.Read(buf)
		if err != nil && err != io.EOF {
			return count, err
		}

		count += bytes.Count(buf[:c], lineSep)

		if err == io.EOF {
			break
		}
	}

	return count, nil
}

// new file name like  xx.2013-01-01.001.log
func (f *fileLogger) createFreshFile(logTime time.Time) error {
	// file exists
	// Find the next available number
	num := 1
	fName := ""
	rotatePerm, err := strconv.ParseInt(f.PermitMask, 8, 64)
	if err != nil {
		return err
	}

	_, err = os.Lstat(f.Filename)
	if err != nil {
		// 初始日志文件不存在,无需创建新文件
		goto RESTART_LOGGER
	}
	// 日期变了, 说明跨天,重命名时需要保存为昨天的日期
	if f.dailyOpenDate != logTime.Day() {
		for ; err == nil && num <= 999; num++ {
			fName = f.fileNameOnly + fmt.Sprintf(".%s.%03d%s", f.dailyOpenTime.Format("2006-01-02"), num, f.suffix)
			_, err = os.Lstat(fName)
		}
	} else { //如果仅仅是文件大小或行数达到了限制,仅仅变更后缀序号即可
		for ; err == nil && num <= 999; num++ {
			fName = f.fileNameOnly + fmt.Sprintf(".%s.%03d%s", logTime.Format("2006-01-02"), num, f.suffix)
			_, err = os.Lstat(fName)
		}
	}

	if err == nil {
		return fmt.Errorf("Cannot find free log number to rename %s", f.Filename)
	}
	f.fileWriter.Close()

	// 当创建新文件标记为true时
	// 当日志文件超过最大限制行
	// 当日志文件超过最大限制字节
	// 当日志文件隔天更新标记为true时
	// 将旧文件重命名,然后创建新文件
	err = os.Rename(f.Filename, fName)
	if err != nil {
		fmt.Fprintf(os.Stdout, "os.Rename %s to %s err:%s\n", f.Filename, fName, err.Error())
		goto RESTART_LOGGER
	}

	err = os.Chmod(fName, os.FileMode(rotatePerm))

RESTART_LOGGER:

	startLoggerErr := f.newFile()
	go f.deleteOldLog()

	if startLoggerErr != nil {
		return fmt.Errorf("Rotate StartLogger: %s", startLoggerErr)
	}
	if err != nil {
		return fmt.Errorf("Rotate: %s", err)
	}
	return nil
}

func (f *fileLogger) deleteOldLog() {
	dir := filepath.Dir(f.Filename)
	filepath.Walk(dir, func(path string, info os.FileInfo, err error) (returnErr error) {
		defer func() {
			if r := recover(); r != nil {
				fmt.Fprintf(os.Stdout, "Unable to delete old log '%s', error: %v\n", path, r)
			}
		}()

		if info == nil {
			return
		}

		if f.MaxDays != -1 && !info.IsDir() && info.ModTime().Add(24*time.Hour*time.Duration(f.MaxDays)).Before(time.Now()) {
			if strings.HasPrefix(filepath.Base(path), filepath.Base(f.fileNameOnly)) &&
				strings.HasSuffix(filepath.Base(path), f.suffix) {
				os.Remove(path)
			}
		}
		return
	})
}

func (f *fileLogger) Destroy() {
	f.fileWriter.Close()
}

func init() {
	Register(AdapterFile, &fileLogger{
		Daily:      true,
		MaxDays:    7,
		Append:     true,
		LogLevel:   LevelDebug,
		PermitMask: "0777",
		MaxLines:   10,
		MaxSize:    10 * 1024 * 1024,
	})
}