golang實現(xiàn)實時監(jiān)聽文件并自動切換目錄
應用程序使用golang開發(fā),日志采用zap進行記錄,每天會根據(jù)日期自動創(chuàng)建文件夾存放當天日志記錄(log.log、error.log)如下圖所示,如何實時記錄日志內(nèi)容,進行持久化入庫,并且自動根據(jù)日期切換文件夾監(jiān)聽。

解決方案
采用fsnotify來實現(xiàn),fsnotify 是 Go 語言中的一個庫,用于監(jiān)控文件系統(tǒng)事件,例如文件或目錄的創(chuàng)建、刪除、修改等。它提供了一個跨平臺的文件系統(tǒng)通知接口,允許你監(jiān)聽文件系統(tǒng)的變化并采取相應的措施。
需要注意的是需要設(shè)計數(shù)據(jù)庫或者緩存來存儲解析日志的offset,不然會出現(xiàn)如果程序重新啟動,會重復解析日志文件的問題。
核心代碼
package watch
import (
"bufio"
"fmt"
"github.com/fsnotify/fsnotify"
"go.uber.org/zap"
"io"
"os"
"path/filepath"
"time"
)
// 存儲已處理的位置
func saveProcessedOffset(fullFileName, logLevel string, offset int64) {
logRunRecord := system.SysRunLogWatchRecord{}
// 嘗試從數(shù)據(jù)庫中找到匹配的記錄
global.DB.Where(system.SysRunLogWatchRecord{FullFileName: fullFileName, LogLevel: logLevel}).First(&logRunRecord)
// 如果找到了匹配的記錄,則更新 offset 值
if logRunRecord.ID > 0 {
logRunRecord.ProcessedOffset = offset
global.DB.Save(&logRunRecord)
} else {
// 沒有找到匹配的記錄,插入新記錄
newLogRecord := system.SysRunLogWatchRecord{
FullFileName: fullFileName,
LogLevel: logLevel,
ProcessedOffset: offset,
}
global.DB.Create(&newLogRecord)
}
}
// 從存儲中讀取已處理的位置
func readProcessedOffset(fullFileName, logLevel string) int64 {
var logRunRecord system.SysRunLogWatchRecord
global.DB.Where("full_file_name = ? and log_level = ?", fullFileName, logLevel).First(&logRunRecord)
return logRunRecord.ProcessedOffset
}
// WatchSysRuntimeLogIncrement 檢測日志
func WatchSysRuntimeLogIncrement(logPath, logLevel string) {
watcher, err := fsnotify.NewWatcher()
if err != nil {
global.LOG.Error("fsnotify watch error:", zap.Error(err))
}
defer watcher.Close()
ticker := time.NewTicker(1 * time.Minute)
global.LOG.Info("啟動一個定時器,每分鐘檢查一次時間并切換目錄")
defer ticker.Stop()
directory := getCurrentLogDirectory(logPath)
initCurrentErrorLog(fmt.Sprintf("%s/%s.log", directory, logLevel))
err = watcher.Add(directory)
if err != nil {
global.LOG.Error("watcher Add error", zap.Error(err))
}
var currentReadFile *os.File
// 在循環(huán)外打開文件
logFilePath := fmt.Sprintf("%s/%s.log", directory, logLevel)
// 當前正在解析的日志文件
currentReadFile, err = os.Open(logFilePath)
if err != nil {
global.LOG.Error("無法打開文件:", zap.Error(err))
return
}
var processedOffset int64
for {
select {
case <-ticker.C:
currentDate := time.Now().Format("2006-01-02")
newDirectory := fmt.Sprintf("%s%s", logPath, currentDate)
if newDirectory != directory {
if _, err := os.Stat(fmt.Sprintf("%s/%s.log", newDirectory, logLevel)); err == nil {
watcher.Remove(directory)
directory = newDirectory
err := watcher.Add(newDirectory)
if err != nil {
global.LOG.Error("無法監(jiān)控新目錄:", zap.Error(err))
}
global.LOG.Info("開始監(jiān)控新文件:" + newDirectory)
//監(jiān)控新文件的時候,關(guān)閉舊文件
currentReadFile.Close()
logFilePath := fmt.Sprintf("%s/%s.log", directory, logLevel)
currentReadFile, err = os.Open(logFilePath)
processedOffset = 0
global.LOG.Info("文件已經(jīng)發(fā)生變化,新文件為:" + logFilePath)
} else {
global.LOG.Info("新文件不存在,繼續(xù)監(jiān)控舊文件")
processedOffset = readProcessedOffset(fmt.Sprintf("%s/%s.log", directory, logLevel), logLevel)
}
} else {
processedOffset = readProcessedOffset(fmt.Sprintf("%s/%s.log", directory, logLevel), logLevel)
}
case event, ok := <-watcher.Events:
if !ok {
return
}
if event.Op&fsnotify.Write == fsnotify.Write {
global.LOG.Info("解析文件:" + logFilePath)
if err != nil {
global.LOG.Error("無法打開文件:", zap.Error(err))
continue
}
currentReadFile.Seek(processedOffset, io.SeekStart)
scanner := bufio.NewScanner(currentReadFile)
for scanner.Scan() {
line := scanner.Text()
runLog := system.SystemRunningLog{
Description: logLevel,
ModuleName: logPath,
Operation: line,
}
err = global.DB.Create(&runLog).Error
if err != nil {
global.LOG.Error("存儲運行日志錯誤", zap.Error(err))
}
}
if err := scanner.Err(); err != nil {
global.LOG.Error("讀取文件時發(fā)生錯誤", zap.Error(err))
}
// 更新已處理的位置
processedOffset, err = currentReadFile.Seek(0, io.SeekEnd)
if err != nil {
global.LOG.Error("無法獲取文件偏移量:", zap.Error(err))
}
// 將已處理的位置存儲到文件中
saveProcessedOffset(fmt.Sprintf("%s/%s.log", directory, logLevel), logLevel, processedOffset)
}
case err, ok := <-watcher.Errors:
if !ok {
return
}
global.LOG.Error("錯誤事件", zap.Error(err))
}
}
}
// 獲取當前日期并構(gòu)建日志目錄路徑
func getCurrentLogDirectory(logPath string) string {
currentDate := time.Now().Format("2006-01-02")
return fmt.Sprintf("%s%s", logPath, currentDate)
}
// 初始文件
func initCurrentErrorLog(errorLogPath string) {
// 判斷文件是否存在
_, err := os.Stat(errorLogPath)
if os.IsNotExist(err) {
// 文件不存在,創(chuàng)建文件夾和文件
err := os.MkdirAll(filepath.Dir(errorLogPath), os.ModePerm)
if err != nil {
global.LOG.Error("os.MkdirAll error:", zap.Error(err))
return
}
file, err := os.Create(errorLogPath)
if err != nil {
global.LOG.Error("os.Create error:", zap.Error(err))
return
}
defer file.Close()
global.LOG.Info("error.log 文件已經(jīng)存在,開始監(jiān)控:" + errorLogPath)
} else if err == nil {
global.LOG.Info("error.log 文件已經(jīng)存在,開始監(jiān)控:" + errorLogPath)
} else {
global.LOG.Error("os.IsNotExist error:", zap.Error(err))
return
}
}其中WatchSysRuntimeLogIncrement方法傳入日志路徑和需要解析的日志文件名稱(例如info)后綴默認.log,執(zhí)行此方法即可實現(xiàn)邏輯
以上就是golang實現(xiàn)實時監(jiān)聽文件并自動切換目錄的詳細內(nèi)容,更多關(guān)于golang實時監(jiān)聽文件的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
解決golang時間字符串轉(zhuǎn)time.Time的坑
這篇文章主要介紹了解決golang時間字符串轉(zhuǎn)time.Time的坑,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2021-04-04

