capture_log.go 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  1. package capture
  2. import (
  3. "fmt"
  4. "io"
  5. "log"
  6. "os"
  7. "time"
  8. "github.com/sirupsen/logrus"
  9. )
  10. // Logger 是全局的 logrus.Logger 实例,供其他包使用
  11. var Logger *logrus.Logger
  12. // Hook 结构体定义了一个日志钩子,用于将日志条目输出到指定的 Writer,并使用指定的 Formatter 格式化日志条目。
  13. // Level 字段是一个包含日志级别的切片,用于指定该钩子应处理的日志级别。
  14. type Hook struct {
  15. Writer io.Writer // 日志输出的目标 Writer
  16. Formatter logrus.Formatter // 日志的格式化器
  17. Level []logrus.Level // 此钩子应处理的日志级别
  18. }
  19. // Fire 方法是 logrus.Hook 接口的实现。当日志条目符合钩子的日志级别时,
  20. // 这个方法会被调用。它首先使用 Formatter 将日志条目格式化为字节切片,
  21. // 然后将其写入 Writer。此版本增强了日志条目,添加了错误发生的文件名和行号。
  22. func (h *Hook) Fire(entry *logrus.Entry) error {
  23. // 如果日志条目有调用者信息,则记录文件名和行号
  24. if entry.HasCaller() {
  25. entry.Data["file"] = fmt.Sprintf("%s:%d", entry.Caller.File, entry.Caller.Line)
  26. }
  27. // 使用 Formatter 格式化日志条目为字节切片
  28. line, err := h.Formatter.Format(entry)
  29. if err != nil {
  30. return err // 如果格式化出错,返回错误
  31. }
  32. h.Writer.Write(line) // 将格式化的日志条目写入 Writer
  33. return nil
  34. }
  35. // Levels 方法返回该钩子支持的日志级别切片。
  36. // logrus 在写入日志时会调用这个方法来决定是否使用这个钩子。
  37. func (h *Hook) Levels() []logrus.Level {
  38. return h.Level // 返回支持的日志级别
  39. }
  40. // newHook 函数用于创建一个新的 Hook 实例。它接收一个 Writer,
  41. // 一个 Formatter 和一个最低的日志级别 level 作为参数。
  42. // 函数会为低于或等于指定级别的所有日志级别创建一个 Hook,
  43. // 并返回该 Hook 实例。
  44. func newHook(writer io.Writer, formatter logrus.Formatter, level logrus.Level) *Hook {
  45. var levels []logrus.Level
  46. // 遍历 logrus 所有的日志级别,如果当前级别低于或等于指定的 level,添加到 levels 切片中
  47. for _, l := range logrus.AllLevels {
  48. if l <= level {
  49. levels = append(levels, l)
  50. }
  51. }
  52. // 返回新建的 Hook 实例
  53. return &Hook{
  54. Writer: writer,
  55. Formatter: formatter,
  56. Level: levels,
  57. }
  58. }
  59. // createLogFile 函数用于根据当前日期创建一个新的日志文件。
  60. // 它返回日志文件的文件叠指针、文件名和可能的错误信息。
  61. func createLogFile(logFilePath string) (*os.File, string, error) {
  62. // 使用当前日期生成日志文件名,格式为 `log-YYYY-MM-DD-HH-MM-SS.log`
  63. currentDate := time.Now().Format("2006-01-02-15-04-05")
  64. logFileName := fmt.Sprintf("%s/log-%s.log", logFilePath, currentDate)
  65. // 打开或创建日志文件,文件路径由 logFilePath 指定
  66. logFile, err := os.OpenFile(logFileName, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
  67. return logFile, logFileName, err // 返回日志文件叠指针、文件名和可能的错误
  68. }
  69. // InitLogs 函数负责初始化 logrus.Logger 对象。它首先检查是否存在指定的日志目录。
  70. // 如果目录不存在,会创建该目录。
  71. func InitLogs(logFilePath string, logLevelStr string) *logrus.Logger {
  72. // 检查日志目录是否存在
  73. if _, err := os.Stat(logFilePath); os.IsNotExist(err) {
  74. // 目录不存在,创建目录
  75. err := os.MkdirAll(logFilePath, os.ModePerm)
  76. if err != nil {
  77. fmt.Println("Error creating directory:", err) // 打印创建目录失败的错误信息
  78. return nil
  79. }
  80. fmt.Println("Directory created:", logFilePath) // 打印成功创建目录的信息
  81. }
  82. // 将 logLevelStr 转换为 logrus.Level,失败时默认使用 InfoLevel
  83. logLevel, err := logrus.ParseLevel(logLevelStr)
  84. if err != nil {
  85. logLevel = logrus.InfoLevel
  86. log.Printf("Invalid log level: %s. Defaulting to info", logLevelStr) // 输出无效日志级别信息
  87. }
  88. // 初始化 Logger 实例,将输出设置为 io.Discard,以强制通过钩子处理所有日志输出
  89. Logger = logrus.New()
  90. Logger.SetOutput(io.Discard) // 默认不输出任何日志
  91. // 启用记录调用者信息(文件名和行号)
  92. Logger.SetReportCaller(true)
  93. // 使用 createLogFile 创建一个新的日志文件
  94. logFile, logFileName, err := createLogFile(logFilePath)
  95. if err != nil {
  96. log.Printf("Failed to open log file: %s", logFileName) // 打印文件打开失败的信息
  97. panic(err) // 触发 panic
  98. }
  99. // 添加一个新的 Hook,将日志输出到日志文件中,并使用 JSONFormatter 进行格式化
  100. Logger.AddHook(newHook(
  101. logFile,
  102. &logrus.JSONFormatter{},
  103. logLevel,
  104. ))
  105. // 添加一个 Hook,将日志输出到标准错误输出(控制台),使用 TextFormatter 格式化
  106. Logger.AddHook(newHook(
  107. os.Stderr,
  108. &logrus.TextFormatter{
  109. FullTimestamp: true, // 显示完整时间戳
  110. ForceColors: true, // 强制使用颜色输出
  111. },
  112. logLevel,
  113. ))
  114. // 启动一个 goroutine,用于每天创建新的日志文件
  115. go func() {
  116. // 每 24 小时触发一次
  117. ticker := time.NewTicker(24 * time.Hour)
  118. defer ticker.Stop() // 停止 ticker
  119. for {
  120. <-ticker.C // 等待 ticker 触发
  121. // 关闭当前日志文件
  122. logFile.Close()
  123. // 创建新的日志文件
  124. newLogFile, newLogFileName, err := createLogFile(logFilePath)
  125. if err != nil {
  126. log.Printf("Failed to create new log file: %s", newLogFileName) // 打印创建失败信息
  127. continue // 如果失败,跳过此次循环,等待下一次检查
  128. }
  129. logFile = newLogFile // 更新 logFile
  130. // 清除所有旧的日志钩子
  131. Logger.ReplaceHooks(make(logrus.LevelHooks))
  132. // 添加新的文件输出 Hook
  133. Logger.AddHook(newHook(
  134. logFile,
  135. &logrus.JSONFormatter{},
  136. logLevel,
  137. ))
  138. // 添加控制台输出 Hook
  139. Logger.AddHook(newHook(
  140. os.Stderr,
  141. &logrus.TextFormatter{
  142. FullTimestamp: true,
  143. ForceColors: true,
  144. },
  145. logLevel,
  146. ))
  147. }
  148. }()
  149. return Logger // 返回初始化后的 Logger 实例
  150. }