service.xpcool.com/internal/service/admin/base/log/log.go
夏犀麟 9010592c9e fix(build): 修正 .gitignore 的 log/ 规则误伤源码包,恢复 HEAD 可编译
问题:.gitignore 第 27 行的 `log/` 未锚定根目录,会匹配任意层级的
log 目录,把下面 4 个**源码包**一并忽略:
  - api/admin/base/log
  - api/admin/system/log
  - internal/service/admin/base/log
  - internal/service/admin/system/log

而引用它们的 internal/controller/admin/log.go 已在库中,于是
「已提交的代码 import 了不存在(未提交)的包」——干净检出后
`go build .` 直接失败:
  no required module provides package service.xpcool.com/api/admin/base/log

因为 service 仓库没有 CI 自动构建,这个问题一直没有暴露,
影响是:新克隆/新设备无法编译,任何干净检出构建都会失败。

修复:
1. 规则改为 `/log/`,只忽略仓库根目录下的运行日志输出目录
   (config.dev.yaml 的 logger.path: "log"),不再误伤同名源码包;
2. 补交被忽略期间漏掉的 4 个 log 包,恢复 HEAD 完整可编译。
2026-09-14 02:34:46 +08:00

130 lines
3.7 KiB
Go
Raw 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 admin_base_log 提供服务器日志文件管理服务:列出日志目录下的文件、读取文件尾部。
//
// 日志目录取自配置项 logger.path默认 "log",相对项目根目录)。
// 安全约束:只允许访问日志目录内的文件,文件名做路径穿越校验,禁止读取目录外内容。
package admin_base_log
import (
"context"
"os"
"path/filepath"
"strings"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"service.xpcool.com/internal/model/dto"
)
// ILogManage 服务器日志管理服务接口。
type ILogManage interface {
// Files 列出日志目录下的全部日志文件。
Files(context.Context) (dir string, files []*dto.LogFile, err error)
// Tail 读取指定日志文件的尾部若干行,可按关键字过滤。
Tail(ctx context.Context, file string, lines int, keyword string) ([]string, error)
}
var localLogManage ILogManage
// LogManage 获取服务器日志管理服务实例。
func LogManage() ILogManage {
if localLogManage == nil {
panic("LogManage 实现未注册")
}
return localLogManage
}
// RegisterLogManage 注册服务器日志管理服务实现。
func RegisterLogManage(i ILogManage) { localLogManage = i }
// NewLogManage 创建服务器日志管理服务实例。
func NewLogManage() ILogManage { return &logManage{} }
type logManage struct{}
// logDir 返回日志目录的绝对路径。
func (s *logManage) logDir(ctx context.Context) string {
dir := g.Cfg().MustGet(ctx, "logger.path", "log").String()
if dir == "" {
dir = "log"
}
if abs, err := filepath.Abs(dir); err == nil {
return abs
}
return dir
}
// Files 列出日志目录下的全部日志文件(不递归子目录)。
func (s *logManage) Files(ctx context.Context) (string, []*dto.LogFile, error) {
dir := s.logDir(ctx)
entries, err := os.ReadDir(dir)
if err != nil {
if os.IsNotExist(err) {
// 日志目录尚未创建属正常情况(服务还没写日志),返回空列表而非报错。
return dir, []*dto.LogFile{}, nil
}
return "", nil, gerror.Wrap(err, "读取日志目录失败")
}
files := make([]*dto.LogFile, 0, len(entries))
for _, e := range entries {
if e.IsDir() {
continue
}
info, err := e.Info()
if err != nil {
continue
}
files = append(files, &dto.LogFile{
Name: e.Name(),
Path: filepath.Join(dir, e.Name()),
Size: info.Size(),
ModTime: info.ModTime().Format("2006-01-02 15:04:05"),
})
}
return dir, files, nil
}
// Tail 读取指定日志文件的尾部 lines 行keyword 非空时只返回包含该关键字的行。
func (s *logManage) Tail(ctx context.Context, file string, lines int, keyword string) ([]string, error) {
if lines <= 0 {
lines = 200
}
dir := s.logDir(ctx)
// 路径穿越防护:文件名不允许包含路径分隔符与上跳符。
if file == "" || strings.ContainsAny(file, `/\`) || strings.Contains(file, "..") {
return nil, gerror.New("非法的日志文件名")
}
full := filepath.Join(dir, file)
data, err := os.ReadFile(full)
if err != nil {
if os.IsNotExist(err) {
return nil, gerror.New("日志文件不存在")
}
return nil, gerror.Wrap(err, "读取日志文件失败")
}
all := strings.Split(strings.ReplaceAll(string(data), "\r\n", "\n"), "\n")
// 末尾通常是空行,去掉以免占用一行额度。
if n := len(all); n > 0 && all[n-1] == "" {
all = all[:n-1]
}
// 关键字过滤。
if keyword != "" {
filtered := make([]string, 0, len(all))
for _, l := range all {
if strings.Contains(l, keyword) {
filtered = append(filtered, l)
}
}
all = filtered
}
// 只取尾部 lines 行。
if len(all) > lines {
all = all[len(all)-lines:]
}
return all, nil
}