hotime/log/logrus.go
hoteas 3d83c41905 feat(cache): 增加批量操作支持以提升性能
- 在 HoTimeCache 中新增 SessionsGet、SessionsSet 和 SessionsDelete 方法,支持批量获取、设置和删除 Session 缓存
- 优化缓存逻辑,减少数据库写入次数,提升性能
- 更新文档,详细说明批量操作的使用方法和性能对比
- 添加调试日志记录,便于追踪批量操作的执行情况
2026-01-30 17:51:43 +08:00

193 lines
5.3 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 日志库内部文件(支持带版本号的路径,如 logrus@v1.8.1/entry.go
if strings.HasPrefix(file, "logrus/") || 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)
if !ok {
return "", 0
}
n := 0
for i := len(file) - 1; i > 0; i-- {
if file[i] == '/' || file[i] == '\\' { // 同时处理 / 和 \ 路径分隔符
n++
if n >= 2 {
file = file[i+1:]
break
}
}
}
return file, line
}