// Package logging 负责服务端日志的落盘:按「放行 / 拦截」两类分流到不同目录, // 各自按天 + 按大小轮转并保留固定份数,写盘失败时降级为只写控制台。 // // 单独成包是为了让 httpapi 与 middleware 共用同一份「拦截点」捕获与类别标记, // 又不会互相依赖(两边都只依赖本包)。 package logging import ( "fmt" "path/filepath" "runtime" "strings" ) // 类别标记:日志按「是不是我们主动拦截的」分流,而不是只看级别—— // DEBUG 里既有常规访问日志,也有拦截上下文,只按级别分不开。 const ( // ChannelKey 是类别标记的属性名;拦截类日志都带上它。 ChannelKey = "log.channel" // ChannelInterception 是拦截类的取值。 ChannelInterception = "interception" ) // sourceRoot 是本包所在模块的根目录(…/server);用它把编译期路径裁成 // 「internal/httpapi/xxx.go」这样的仓库内相对路径,日志里不出现开发机的目录结构。 // 用 -trimpath 构建时 file 本来就是相对路径,裁剪会自然退化成原样。 var sourceRoot = func() string { _, file, _, ok := runtime.Caller(0) if !ok { return "" } // 本文件位于 /internal/logging/origin.go,向上三级即 。 return filepath.Dir(filepath.Dir(filepath.Dir(file))) }() // relativeSourcePath 把编译期文件路径裁成仓库内相对路径;裁不动时只留文件名。 func relativeSourcePath(file string) string { if sourceRoot != "" { if rel := strings.TrimPrefix(file, sourceRoot+string(filepath.Separator)); rel != file { return rel } } return filepath.Base(file) } // shortFuncName 尽量给出「(*server).createEvent」「RequireWrite」这样的短名。 // 编译器会把闭包按内联链写成 (*server).write.(*server).write.RequireWrite.func1.func4, // 直接取最后一段会得到一串噪音,这里只保留「有意义的那个函数名 + 第一个闭包序号」。 func shortFuncName(full string) string { if i := strings.LastIndex(full, "/"); i >= 0 { full = full[i+1:] } rest := full if i := strings.Index(rest, "."); i >= 0 { rest = rest[i+1:] } parts := strings.Split(rest, ".") if len(parts) == 1 { return rest } // 跳过尾部的闭包序号,落回真正定义它的那个函数。 owner := len(parts) - 1 for owner > 0 && strings.HasPrefix(parts[owner], "func") { owner-- } name := parts[owner] // 闭包序号只留第一个:够区分同一个函数里的多个闭包。 if owner+1 < len(parts) && strings.HasPrefix(parts[owner+1], "func") { name += "." + parts[owner+1] } // 方法带上接收者,「(*server).createEvent」比「createEvent」好找。 if owner > 0 && strings.HasPrefix(parts[owner-1], "(*") { name = parts[owner-1] + "." + name } return name } // FormatOrigin 拼出「internal/middleware/middleware.go:123 RequireRole」。 func FormatOrigin(file string, line int, fn string) string { return fmt.Sprintf("%s:%d %s", relativeSourcePath(file), line, shortFuncName(fn)) } // Origin 捕获调用点(skip 从本函数自己那层算起),产出「文件:行 函数名」。 // 日志里必须能指出是哪段代码拦下的,否则现场只有一句中文提示,定位不到校验代码。 func Origin(skip int) string { pc, file, line, ok := runtime.Caller(skip) if !ok { return "unknown" } fn := "unknown" if f := runtime.FuncForPC(pc); f != nil { fn = f.Name() } return FormatOrigin(file, line, fn) }