package global import ( "fmt" "io" "log" "os" "time" "github.com/sirupsen/logrus" ) // Logger 是全局的 logrus.Logger 实例,供其他包使用 var Logger *logrus.Logger // Hook 结构体定义了一个日志钩子,用于将日志条目输出到指定的 Writer,并使用指定的 Formatter 格式化日志条目。 // Level 字段是一个包含日志级别的切片,用于指定该钩子应处理的日志级别。 type Hook struct { Writer io.Writer // 日志输出的目标 Writer Formatter logrus.Formatter // 日志的格式化器 Level []logrus.Level // 此钩子应处理的日志级别 } // Fire 方法是 logrus.Hook 接口的实现。当日志条目符合钩子的日志级别时, // 这个方法会被调用。它首先使用 Formatter 将日志条目格式化为字节切片, // 然后将其写入 Writer。此版本增强了日志条目,添加了错误发生的文件名和行号。 func (h *Hook) Fire(entry *logrus.Entry) error { // 如果日志条目有调用者信息,则记录文件名和行号 if entry.HasCaller() { entry.Data["file"] = fmt.Sprintf("%s:%d", entry.Caller.File, entry.Caller.Line) } // 使用 Formatter 格式化日志条目为字节切片 line, err := h.Formatter.Format(entry) if err != nil { return err // 如果格式化出错,返回错误 } h.Writer.Write(line) // 将格式化的日志条目写入 Writer return nil } // Levels 方法返回该钩子支持的日志级别切片。 // logrus 在写入日志时会调用这个方法来决定是否使用这个钩子。 func (h *Hook) Levels() []logrus.Level { return h.Level // 返回支持的日志级别 } // newHook 函数用于创建一个新的 Hook 实例。它接收一个 Writer, // 一个 Formatter 和一个最低的日志级别 level 作为参数。 // 函数会为低于或等于指定级别的所有日志级别创建一个 Hook, // 并返回该 Hook 实例。 func newHook(writer io.Writer, formatter logrus.Formatter, level logrus.Level) *Hook { var levels []logrus.Level // 遍历 logrus 所有的日志级别,如果当前级别低于或等于指定的 level,添加到 levels 切片中 for _, l := range logrus.AllLevels { if l <= level { levels = append(levels, l) } } // 返回新建的 Hook 实例 return &Hook{ Writer: writer, Formatter: formatter, Level: levels, } } // createLogFile 函数用于根据当前日期创建一个新的日志文件。 // 它返回日志文件的文件叠指针、文件名和可能的错误信息。 func createLogFile(logFilePath string) (*os.File, string, error) { // 使用当前日期生成日志文件名,格式为 `log-YYYY-MM-DD-HH-MM-SS.log` currentDate := time.Now().Format("2006-01-02-15-04-05") logFileName := fmt.Sprintf("%s/log-%s.log", logFilePath, currentDate) // 打开或创建日志文件,文件路径由 logFilePath 指定 logFile, err := os.OpenFile(logFileName, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) return logFile, logFileName, err // 返回日志文件叠指针、文件名和可能的错误 } // InitLogs 函数负责初始化 logrus.Logger 对象。它首先检查是否存在指定的日志目录。 // 如果目录不存在,会创建该目录。 func InitLogs(logFilePath string, logLevelStr string) *logrus.Logger { // 检查日志目录是否存在 if _, err := os.Stat(logFilePath); os.IsNotExist(err) { // 目录不存在,创建目录 err := os.MkdirAll(logFilePath, os.ModePerm) if err != nil { fmt.Println("Error creating directory:", err) // 打印创建目录失败的错误信息 return nil } fmt.Println("Directory created:", logFilePath) // 打印成功创建目录的信息 } // 将 logLevelStr 转换为 logrus.Level,失败时默认使用 InfoLevel logLevel, err := logrus.ParseLevel(logLevelStr) if err != nil { logLevel = logrus.InfoLevel log.Printf("Invalid log level: %s. Defaulting to info", logLevelStr) // 输出无效日志级别信息 } // 初始化 Logger 实例,将输出设置为 io.Discard,以强制通过钩子处理所有日志输出 Logger = logrus.New() Logger.SetOutput(io.Discard) // 默认不输出任何日志 // 启用记录调用者信息(文件名和行号) Logger.SetReportCaller(true) // 使用 createLogFile 创建一个新的日志文件 logFile, logFileName, err := createLogFile(logFilePath) if err != nil { log.Printf("Failed to open log file: %s", logFileName) // 打印文件打开失败的信息 panic(err) // 触发 panic } // 添加一个新的 Hook,将日志输出到日志文件中,并使用 JSONFormatter 进行格式化 Logger.AddHook(newHook( logFile, &logrus.JSONFormatter{}, logLevel, )) // 添加一个 Hook,将日志输出到标准错误输出(控制台),使用 TextFormatter 格式化 Logger.AddHook(newHook( os.Stderr, &logrus.TextFormatter{ FullTimestamp: true, // 显示完整时间戳 ForceColors: true, // 强制使用颜色输出 }, logLevel, )) // 启动一个 goroutine,用于每天创建新的日志文件 go func() { // 每 24 小时触发一次 ticker := time.NewTicker(24 * time.Hour) defer ticker.Stop() // 停止 ticker for { <-ticker.C // 等待 ticker 触发 // 关闭当前日志文件 logFile.Close() // 创建新的日志文件 newLogFile, newLogFileName, err := createLogFile(logFilePath) if err != nil { log.Printf("Failed to create new log file: %s", newLogFileName) // 打印创建失败信息 continue // 如果失败,跳过此次循环,等待下一次检查 } logFile = newLogFile // 更新 logFile // 清除所有旧的日志钩子 Logger.ReplaceHooks(make(logrus.LevelHooks)) // 添加新的文件输出 Hook Logger.AddHook(newHook( logFile, &logrus.JSONFormatter{}, logLevel, )) // 添加控制台输出 Hook Logger.AddHook(newHook( os.Stderr, &logrus.TextFormatter{ FullTimestamp: true, ForceColors: true, }, logLevel, )) } }() return Logger // 返回初始化后的 Logger 实例 }