package log import ( "fmt" "os" "path/filepath" "runtime" "strings" "time" log "github.com/sirupsen/logrus" ) func GetLog(path string, showCodeLine bool) *log.Logger { hook := MyHook{ Field: "line", Skip: 5, Path: path, ShowCodeLine: showCodeLine, } loger := log.New() loger.SetFormatter(&log.TextFormatter{}) loger.AddHook(&hook) return loger } // MyHook ... type MyHook struct { Path string //存储日志的位置 ShowCodeLine bool //输出代码文件名称和日志行 Field string Skip int levels []log.Level } // Levels 只定义 error 和 panic 等级的日志,其他日志等级不会触发 hook func (that *MyHook) Levels() []log.Level { return log.AllLevels } // Fire 将异常日志写入到指定日志文件中 func (that *MyHook) Fire(entry *log.Entry) error { if that.ShowCodeLine { entry.Data[that.Field] = findCaller(that.Skip) } //不需要存储到文件 if that.Path == "" { return nil } //存储到文件 logFilePath := time.Now().Format(that.Path) err := os.MkdirAll(filepath.Dir(logFilePath), os.ModeAppend) if err != nil { return err } //os.Create(logFilePath) f, err := os.OpenFile(logFilePath, os.O_APPEND|os.O_CREATE, 0644) if err != nil { return err } bte, _ := entry.Bytes() _, err = f.Write(bte) if err != nil { return err } err = f.Close() if err != nil { return err } return nil } // 最大框架层数限制 - 超过这个层数后不再跳过,防止误过滤应用层 const maxFrameworkDepth = 10 // isHoTimeFrameworkFile 判断是否是 HoTime 框架文件 // 更精确的匹配:只有明确属于框架的文件才会被跳过 func isHoTimeFrameworkFile(file string) bool { // 1. logrus 日志库内部文件 if strings.HasPrefix(file, "logrus/") { return true } // 2. Go 运行时文件 if strings.HasPrefix(file, "runtime/") { return true } // 3. HoTime 框架核心文件 - 通过包含 "hotime" 或框架特有文件名来识别 // 检查路径中是否包含 hotime 框架标识 lowerFile := strings.ToLower(file) if strings.Contains(lowerFile, "hotime") { // 是 hotime 框架的一部分,检查是否是核心模块 frameworkDirs := []string{"/db/", "/common/", "/code/", "/cache/", "/log/", "/dri/"} for _, dir := range frameworkDirs { if strings.Contains(file, dir) { return true } } // 框架核心文件(在 hotime 根目录下的 .go 文件) if strings.HasSuffix(file, "application.go") || strings.HasSuffix(file, "context.go") || strings.HasSuffix(file, "session.go") || strings.HasSuffix(file, "const.go") || strings.HasSuffix(file, "type.go") || strings.HasSuffix(file, "var.go") || strings.HasSuffix(file, "mime.go") { return true } } // 4. 直接匹配框架核心目录(用于没有完整路径的情况) // 只匹配 "db/xxx.go" 这种在框架核心目录下的文件 frameworkCoreDirs := []string{"db/", "common/", "code/", "cache/"} for _, dir := range frameworkCoreDirs { if strings.HasPrefix(file, dir) { // 额外检查:确保不是用户项目中同名目录 // 框架文件通常有特定的文件名 frameworkFiles := []string{ "query.go", "crud.go", "where.go", "builder.go", "db.go", "dialect.go", "aggregate.go", "transaction.go", "identifier.go", "error.go", "func.go", "map.go", "obj.go", "slice.go", "makecode.go", "template.go", "config.go", "cache.go", "cache_db.go", "cache_memory.go", "cache_redis.go", } for _, f := range frameworkFiles { if strings.HasSuffix(file, f) { return true } } } } return false } // 对caller进行递归查询, 直到找到非框架层产生的第一个调用. // 遍历调用栈,跳过框架层文件,找到应用层代码 // 使用层数限制确保不会误过滤应用层同名目录 func findCaller(skip int) string { frameworkCount := 0 // 连续框架层计数 // 遍历调用栈,找到第一个非框架文件 for i := 0; i < 20; i++ { file, line := getCaller(skip + i) if file == "" { break } if isHoTimeFrameworkFile(file) { frameworkCount++ // 层数限制:如果已经跳过太多层,停止跳过 if frameworkCount >= maxFrameworkDepth { return fmt.Sprintf("%s:%d", file, line) } continue } // 找到非框架文件,返回应用层代码位置 return fmt.Sprintf("%s:%d", file, line) } // 如果找不到应用层,返回最初的调用者 file, line := getCaller(skip) return fmt.Sprintf("%s:%d", file, line) } // 这里其实可以获取函数名称的: fnName := runtime.FuncForPC(pc).Name() // 但是我觉得有 文件名和行号就够定位问题, 因此忽略了caller返回的第一个值:pc // 在标准库log里面我们可以选择记录文件的全路径或者文件名, 但是在使用过程成并发最合适的, // 因为文件的全路径往往很长, 而文件名在多个包中往往有重复, 因此这里选择多取一层, 取到文件所在的上层目录那层. func getCaller(skip int) (string, int) { _, file, line, ok := runtime.Caller(skip) //fmt.Println(file) //fmt.Println(line) if !ok { return "", 0 } n := 0 for i := len(file) - 1; i > 0; i-- { if file[i] == '/' { n++ if n >= 2 { file = file[i+1:] break } } } return file, line }