Log 实例封装

package comm
 
import (
	"fmt"
	"os"
	"path/filepath"
	"runtime"
	"time"
 
	rotatelogs "github.com/lestrrat-go/file-rotatelogs"
	"github.com/sirupsen/logrus"
)
 
// 全局日志记录器
var Log *logrus.Logger
 
func init() {
	if Log != nil {
		return
	}
 
	// 获取日志文件路径
	logFilePath := getLogFilePath()
 
	// 创建一个新的 TextFormatter 实例
	formatter := &logrus.TextFormatter{
		CallerPrettyfier: func(f *runtime.Frame) (string, string) {
			_, filename := filepath.Split(f.File)
 
			return fmt.Sprintf("%s()", f.Function), fmt.Sprintf("%s:%d", filename, f.Line)
		},
		TimestampFormat:        "2006-01-02 15:04:05",
		FullTimestamp:          true,
		DisableLevelTruncation: true,
	}
 
	Log = logrus.New()
	Log.SetFormatter(formatter)
	Log.SetReportCaller(true)
	Log.SetLevel(logrus.InfoLevel)
 
	// 创建并添加Hook,根据不同模块名记录到不同文件
	Log.AddHook(NewModuleHook(logFilePath))
}
 
// getLogFilePath 根据不同操作系统和模块名生成日志文件路径
func getLogFilePath() string {
	var logFilePath string
 
	switch runtime.GOOS {
	case "windows":
		logFilePath = filepath.Join("d:", "fgap_log")
	case "linux":
		logFilePath = filepath.Join("/home/wuhy/FGAP/", "fgap_log")
	default:
		fmt.Println("getLogFilePath OS not supported")
	}
 
	os.MkdirAll(logFilePath, 0755)
	return logFilePath
}
 
// ModuleHook 根据模块将日志记录到不同的文件中
type ModuleHook struct {
	LogFilePath  string
	ModuleLevels []logrus.Level
}
 
// NewModuleHook 创建一个ModuleHook实例
func NewModuleHook(logFilePath string) *ModuleHook {
	return &ModuleHook{
		LogFilePath:  logFilePath,
		ModuleLevels: []logrus.Level{logrus.InfoLevel, logrus.ErrorLevel},
	}
}
 
// Fire 实现Hook接口的Fire方法,用于实际记录日志
func (hook *ModuleHook) Fire(entry *logrus.Entry) error {
	module, ok := entry.Data["module"].(string)
	if !ok {
		module = "default"
	}
 
	filename := filepath.Join(hook.LogFilePath, module, module+".log")
	writer, err := rotatelogs.New(
		filename+".%Y%m%d%H%M%S",
		rotatelogs.WithLinkName(filename),
		rotatelogs.WithMaxAge(time.Duration(86400)*time.Second),
		rotatelogs.WithRotationTime(time.Duration(604800)*time.Second),
	)
	if err != nil {
		return err
	}
 
	logger := logrus.New()
	logger.SetFormatter(&logrus.JSONFormatter{})
	logger.SetOutput(writer)
	logger.SetLevel(entry.Level)
 
	logger.WithField("module", module).Log(entry.Level, entry.Message)
	return nil
}
 
// Levels 实现Hook接口的Levels方法,返回支持的日志级别
func (hook *ModuleHook) Levels() []logrus.Level {
	return hook.ModuleLevels
}

如何调用写入日志

comm.Log.WithField("module", "transfer_ftp").Errorf("sendConn.Write failed, err:%v", err)
comm.Log.WithField("module", "transfer_ftp").Infof("停止接收UDP数据 PolicyName:%s Port:%+v", s.PolicyName, s.Port)

如何将 Logrus 记录到 Influx 数据库

写入 Influx

记录到 Influx 之后查询就很方便了

package main
 
import (
	"fmt"
	"os"
	"runtime"
	"time"
 
	"github.com/influxdata/influxdb-client-go/v2"
	"github.com/influxdata/influxdb-client-go/v2/api"
	"github.com/sirupsen/logrus"
)
 
type InfluxDBHook struct {
	client   influxdb.Client
	writeAPI api.WriteAPI
	module   string
}
 
func NewInfluxDBHook(url, token, org, bucket, module string) (*InfluxDBHook, error) {
	client := influxdb.NewClient(url, token)
	writeAPI := client.WriteAPI(org, bucket)
 
	return &InfluxDBHook{
		client:   client,
		writeAPI: writeAPI,
		module:   module,
	}, nil
}
 
func (hook *InfluxDBHook) Fire(entry *logrus.Entry) error {
	funcName, funcFile, funcLine := getCallerInfo()
	point := influxdb2.NewPoint("logs",
		map[string]string{
			"level":   entry.Level.String(),
			"module":  hook.module,
			"func":    funcName,
			"file":    funcFile,
			"line":    fmt.Sprintf("%d", funcLine),
			"message": entry.Message,
		},
		map[string]interface{}{},
		time.Now())
	hook.writeAPI.WritePoint(point)
 
	return nil
}
 
func (hook *InfluxDBHook) Levels() []logrus.Level {
	return logrus.AllLevels
}
 
func getCallerInfo() (string, string, int) {
	pc, file, line, ok := runtime.Caller(3) // Skip 3 levels to get caller info
	if !ok {
		return "unknown", "unknown", 0
	}
	fn := runtime.FuncForPC(pc)
	if fn == nil {
		return "unknown", file, line
	}
	return fn.Name(), file, line
}
 
func main() {
	hook, err := NewInfluxDBHook("http://localhost:8086", "your-token", "your-org", "your-bucket", "your-module")
	if err != nil {
		fmt.Printf("Failed to create InfluxDB hook: %s\n", err)
		return
	}
	defer hook.client.Close()
 
	logger := logrus.New()
	logger.SetOutput(os.Stdout)
	logger.SetLevel(logrus.DebugLevel)
	logger.AddHook(hook)
 
	logger.Info("This is an info message")
	logger.Warn("This is a warning message")
}

查询以及显示

package main
 
import (
	"fmt"
	"log"
	"net/http"
	"strings"
 
	"github.com/gin-gonic/gin"
	"github.com/influxdata/influxdb-client-go/v2"
	"github.com/influxdata/influxdb-client-go/v2/api"
)
 
func main() {
	r := gin.Default()
	r.GET("/logs", getLogs)
	r.Run(":8080")
}
 
func getLogs(c *gin.Context) {
	url := "http://localhost:8086"
	token :*** "your-token"
	org := "your-org"
	bucket := "your-bucket"
 
	client := influxdb.NewClient(url, token)
	queryAPI := client.QueryAPI(org)
 
	query := fmt.Sprintf(`from(bucket:"%s")
	|> range(start: -1h)
	|> filter(fn: (r) => r["_measurement"] == "logs")
	|> filter(fn: (r) => r["module"] == "%s" or r["func"] == "%s" or r["level"] == "%s" or strings.contains(value: r["message"], search: "%s"))`,
		bucket, c.Query("module"), c.Query("func"), c.Query("level"), c.Query("message"))
 
	result, err := queryAPI.Query(query)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}
 
	var logs []map[string]interface{}
	for result.Next() {
		record := result.Record()
		log := make(map[string]interface{})
		for k, v := range record.Values() {
			log[k] = v
		}
		logs = append(logs, log)
	}
 
	c.JSON(http.StatusOK, logs)
}
<template>
  <div>
    <el-input v-model="module" placeholder="Module"></el-input>
    <el-input v-model="func" placeholder="Function"></el-input>
    <el-input v-model="level" placeholder="Level"></el-input>
    <el-input v-model="message" placeholder="Message"></el-input>
    <el-button @click="searchLogs">Search</el-button>
    <el-table :data="logs" style="width: 100%">
      <el-table-column prop="time" label="Time"></el-table-column>
      <el-table-column prop="module" label="Module"></el-table-column>
      <el-table-column prop="func" label="Function"></el-table-column>
      <el-table-column prop="level" label="Level"></el-table-column>
      <el-table-column prop="message" label="Message"></el-table-column>
    </el-table>
  </div>
</template>
 
<script>
import { ref } from 'vue'
import axios from 'axios'
 
export default {
  setup() {
    const module = ref('')
    const func = ref('')
    const level = ref('')
    const message = ref('')
    const logs = ref([])
 
    const searchLogs = async () => {
      try {
        const response = await axios.get('/logs', {
          params: {
            module: module.value,
            func: func.value,
            level: level.value,
            message: message.value
          }
        })
        logs.value = response.data
      } catch (error) {
        console.error(error)
      }
    }
 
    return { module, func, level, message, logs, searchLogs }
  }
}
</script>
 
<style>
/* Add your custom styles here */
</style>

如何做日志轮转

image.png 写成上面的样子会导致总是报错,提示日志已经存在,那是由于在每次记录日志的时候都去创建日志,当然会有问题。正确的是下面的写法,只做一次初始化,后面获取初始化的内容

package comm
 
import (
    "path/filepath"
    "sync"
    "time"
 
    rotatelogs "github.com/lestrrat-go/file-rotatelogs"
    "github.com/sirupsen/logrus"
)
 
// ModuleHook 根据模块将日志记录到不同的文件中
type ModuleHook struct {
    LogFilePath  string
    Loggers      map[string]*logrus.Logger
    lock         sync.Mutex
    ModuleLevels []logrus.Level
}
 
// NewModuleHook 创建一个ModuleHook实例
func NewModuleHook(logFilePath string) *ModuleHook {
    return &ModuleHook{
        LogFilePath:  logFilePath,
        Loggers:      make(map[string]*logrus.Logger),
        ModuleLevels: []logrus.Level{logrus.InfoLevel, logrus.ErrorLevel},
    }
}
 
// GetLogger 如果存在则返回对应的logger,否则创建一个新的
func (hook *ModuleHook) GetLogger(module string) (*logrus.Logger, error) {
    hook.lock.Lock()
    defer hook.lock.Unlock()
    if logger, ok := hook.Loggers[module]; ok {
        return logger, nil
    }
 
    filename := filepath.Join(hook.LogFilePath, module, module+".log")
    writer, err := rotatelogs.New(
        filename+".%Y%m%d%H%M%S",
        rotatelogs.WithLinkName(filename),
        rotatelogs.WithMaxAge(time.Duration(86400)*time.Second),
        rotatelogs.WithRotationTime(time.Duration(604800)*time.Second),
    )
    if err != nil {
        return nil, err
    }
 
    logger := logrus.New()
    logger.SetFormatter(&logrus.JSONFormatter{})
    logger.SetOutput(writer)
    hook.Loggers[module] = logger // 在字典中存储新的logger,以便稍后使用
    return logger, nil
}
 
// Fire 实现Hook接口的Fire方法,用于实际记录日志
func (hook *ModuleHook) Fire(entry *logrus.Entry) error {
    module, ok := entry.Data["module"].(string)
    if !ok {
        module = "default"
    }
 
    logger, err := hook.GetLogger(module)
    if err != nil {
        return err // 如果有错误(例如,无法创建日志轮询),则返回
    }
 
    logger.SetLevel(entry.Level)
    logger.WithField("module", module).Log(entry.Level, entry.Message)
    return nil
}
 
// Levels 实现Hook接口的Levels方法,返回支持的日志级别
func (hook *ModuleHook) Levels() []logrus.Level {
    return hook.ModuleLevels
}