package impl_localfile import ( "fmt" "log" "os" "path/filepath" "strconv" "sync" "time" ) const ( con_FILE_SUFFIX = "txt" defaultPanicNum = 10 // 默认支持的连续panic次数 defaultPanicIntervals = 5 // 秒 fileMaxSize = 1 << 27 // 128M ) type fileLog struct { logPath string // fileDir + fileName 用于判断是否需要切换日志文件 fileDir string fileName string lv levelType msgChan chan logObject f *os.File // 如果出现未知panic并一直发生,那么应该要真正的panic panicNum int panicTime time.Time // 当前已经写入的大小 curWriteSize int64 } func newLog(logPath string, lv levelType) *fileLog { f := &fileLog{ logPath: logPath, fileDir: "", fileName: "", msgChan: make(chan logObject, 10240), lv: lv, f: nil, } go f.loop() return f } func (f *fileLog) loop() { defer func() { if r := recover(); r != nil { // 将错误输出,而不是记录到文件,是因为可能导致死循环 fmt.Println("log file lv:", f.lv, " err:", r) // 超过间隔时间重置 if time.Since(f.panicTime)/time.Second > defaultPanicIntervals { f.panicNum = 0 f.panicTime = time.Now() } // 这里处理连续panic 也是防止循环调用形成死循环 f.panicNum++ if f.panicNum >= defaultPanicNum { panic(r) } } go f.loop() }() for logItem := range f.msgChan { f.writeLog(logItem) } } func (f *fileLog) writeLog(logObj logObject) { // 由于不使用直接 logUtil 调用,logUtilPlus 存在终端打印的控制,所以 logUtil 只作为纯文件日志组件 // if PrintImportantLog && (logObj.level == warn || logObj.level == _error || logObj.level == fatal) { // fmt.Println(logObj.logInfo) // } // 检查是否需要去切换文件或者创建文件 f.checkFileAndLoop(logObj.level, logObj.ifIncludeHour) f.curWriteSize += int64(len(logObj.logInfo)) _, _ = f.f.WriteString(logObj.logInfo) } // 检查文件是否需要切换 func (f *fileLog) checkFileAndLoop(level levelType, ifIncludeHour bool) { // 获取当前时间 now := time.Now() fileAbsoluteDirectory := filepath.Join(f.logPath, strconv.Itoa(now.Year()), strconv.Itoa(int(now.Month()))) fileName := "" if ifIncludeHour { fileName = fmt.Sprintf("%s.%s.%s", time.Now().Format("2006-01-02-15"), level, con_FILE_SUFFIX) } else { fileName = fmt.Sprintf("%s.%s.%s", time.Now().Format("2006-01-02"), level, con_FILE_SUFFIX) } // 说明已经存在, 检查文件大小,并切换 if f.f != nil && f.fileName == fileName && f.fileDir == fileAbsoluteDirectory { if f.curWriteSize < fileMaxSize { return } // 大小超额切换文件 f.switchFile() return } // 创建文件夹 if f.fileDir != fileAbsoluteDirectory { if err := os.MkdirAll(fileAbsoluteDirectory, os.ModePerm|os.ModeTemporary); err != nil { log.Println("make dir all is err :", err) } f.fileDir = fileAbsoluteDirectory } // 创建文件 if f.fileName != fileName { f.fileName = fileName } // 到这里说明要切换了,关闭之前的file if f.f != nil { _ = f.f.Close() } // 正常切换文件 f.createFile() } func (f *fileLog) switchFile() { // 关闭文件 _ = f.f.Close() // 重命名 curFileName := filepath.Join(f.fileDir, f.fileName) newFileName := filepath.Join(f.fileDir, fmt.Sprintf("%s.%s.%s", f.fileName, time.Now().Format("15-04-05.999"), con_FILE_SUFFIX)) err := os.Rename(curFileName, newFileName) if err != nil { log.Println(err) } // 再次创建文件 f.createFile() } func (f *fileLog) createFile() { // 得到最终的文件绝对路径 fileAbsolutePath := filepath.Join(f.fileDir, f.fileName) // 打开文件(如果文件存在就以读写模式打开,并追加写入;如果文件不存在就创建,然后以写模式打开。) file, err := os.OpenFile(fileAbsolutePath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, os.ModePerm|os.ModeTemporary) if err != nil { log.Println("open file is err :", err) return } f.f = file stat, _ := file.Stat() f.curWriteSize = stat.Size() } func (f *fileLog) SetLogPath(logPath string) { f.logPath = logPath } // Close 最后等待 3s 钟 func (f *fileLog) Close(wg *sync.WaitGroup, waitFinish bool) { ticker := time.NewTicker(3 * time.Second) defer func() { ticker.Stop() if f.f != nil { _ = f.f.Close() } wg.Done() }() for { if !waitFinish || len(f.msgChan) == 0 { break } select { case <-ticker.C: fmt.Println("wait close log file timeout:3s") return default: // 1ms 写入文件500条,最多等待 15ms 检查一次就行 time.Sleep(15 * time.Millisecond) } } }