hostSync/cmd/log.go

347 lines
7.8 KiB
Go

package cmd
import (
"bufio"
"fmt"
"io"
"os"
"path/filepath"
"sort"
"strings"
"time"
"github.com/evil7/hostsync/config"
"github.com/evil7/hostsync/utils"
"github.com/spf13/cobra"
)
var (
logLines int
logFollow bool
logLevel string
)
// logCmd 日志命令
var logCmd = &cobra.Command{
Use: "log",
Short: "查看和管理日志",
Long: `查看和管理HostSync的日志文件。
示例:
hostsync log # 查看最近50行日志
hostsync log -n 100 # 查看最近100行日志
hostsync log -f # 实时跟踪日志
hostsync log --level error # 只显示错误级别的日志
hostsync log list # 列出所有日志文件`,
Run: runLogShow,
}
// logListCmd 列出日志文件命令
var logListCmd = &cobra.Command{
Use: "list",
Short: "列出所有日志文件",
Long: `列出日志目录中的所有日志文件及其大小和修改时间。`,
Run: runLogList,
}
// logClearCmd 清理日志命令
var logClearCmd = &cobra.Command{
Use: "clear",
Short: "清理旧日志文件",
Long: `清理日志目录中的旧日志备份文件,保留当前日志和最近的备份。`,
Run: runLogClear,
}
func init() {
logCmd.Flags().IntVarP(&logLines, "lines", "n", 50, "显示的行数")
logCmd.Flags().BoolVarP(&logFollow, "follow", "f", false, "实时跟踪日志输出")
logCmd.Flags().StringVar(&logLevel, "level", "", "过滤日志级别 (debug,info,warning,error)")
logCmd.AddCommand(logListCmd)
logCmd.AddCommand(logClearCmd)
}
func runLogShow(cmd *cobra.Command, args []string) {
// 初始化配置以获取日志路径
config.Init()
if config.AppConfig == nil || config.AppConfig.LogPath == "" {
utils.LogError("未找到日志配置")
return
}
logFile := filepath.Join(config.AppConfig.LogPath, "hostsync.log")
// 检查日志文件是否存在
if _, err := os.Stat(logFile); os.IsNotExist(err) {
utils.LogInfo("日志文件尚未创建")
utils.LogInfo("日志文件路径: %s", logFile)
utils.LogInfo("执行一些命令后,日志文件将自动创建")
utils.LogInfo("例如: hostsync list, hostsync add test example.com 等")
return
}
if logFollow {
// 实时跟踪模式
followLog(logFile)
} else {
// 显示最近的日志
showRecentLog(logFile, logLines)
}
}
func runLogList(cmd *cobra.Command, args []string) {
// 初始化配置
config.Init()
if config.AppConfig == nil || config.AppConfig.LogPath == "" {
utils.LogError("未找到日志配置")
return
}
logDir := config.AppConfig.LogPath
// 读取日志目录
files, err := os.ReadDir(logDir)
if err != nil {
utils.LogError("读取日志目录失败: %v", err)
return
}
// 过滤和排序日志文件
var logFiles []os.FileInfo
for _, file := range files {
if strings.HasPrefix(file.Name(), "hostsync.log") {
info, err := file.Info()
if err == nil {
logFiles = append(logFiles, info)
}
}
}
if len(logFiles) == 0 {
utils.LogInfo("未找到任何日志文件")
return
}
// 按修改时间排序
sort.Slice(logFiles, func(i, j int) bool {
return logFiles[i].ModTime().After(logFiles[j].ModTime())
})
// 显示日志文件列表
utils.LogInfo("日志目录: %s", logDir)
utils.LogInfo("")
// 准备表格数据
headers := []string{"文件名", "大小", "修改时间", "状态"}
var rows [][]string
for _, file := range logFiles {
size := formatFileSize(file.Size())
modTime := file.ModTime().Format("2006-01-02 15:04:05")
// 确定文件状态
var status string
if file.Name() == "hostsync.log" {
status = "当前"
} else {
status = "备份"
}
rows = append(rows, []string{file.Name(), size, modTime, status})
}
// 设置列宽
columnWidths := []int{35, 12, 20, 10}
utils.FormatTable(headers, rows, columnWidths)
}
func runLogClear(cmd *cobra.Command, args []string) {
// 初始化配置
config.Init()
if config.AppConfig == nil || config.AppConfig.LogPath == "" {
utils.LogError("未找到日志配置")
return
}
logDir := config.AppConfig.LogPath
// 读取日志目录
files, err := os.ReadDir(logDir)
if err != nil {
utils.LogError("读取日志目录失败: %v", err)
return
}
// 查找备份日志文件(不删除主日志文件)
var backupFiles []string
for _, file := range files {
if strings.HasPrefix(file.Name(), "hostsync.log.") {
backupFiles = append(backupFiles, file.Name())
}
}
if len(backupFiles) == 0 {
utils.LogInfo("没有需要清理的备份日志文件")
return
}
// 删除备份文件
deletedCount := 0
for _, fileName := range backupFiles {
filePath := filepath.Join(logDir, fileName)
if err := os.Remove(filePath); err != nil {
utils.LogWarning("删除文件失败 %s: %v", fileName, err)
} else {
deletedCount++
utils.LogInfo("删除备份日志文件: %s", fileName)
}
}
utils.LogSuccess("已清理 %d 个备份日志文件", deletedCount)
}
func showRecentLog(logFile string, lines int) {
file, err := os.Open(logFile)
if err != nil {
utils.LogError("打开日志文件失败: %v", err)
return
}
defer file.Close()
// 读取文件的最后N行
logLines := readLastLines(file, lines)
// 过滤日志级别
if logLevel != "" {
logLines = filterLogLevel(logLines, logLevel)
}
if len(logLines) == 0 {
utils.LogInfo("没有找到匹配的日志记录")
return
}
utils.LogInfo("最近 %d 行日志 (%s):", len(logLines), logFile)
utils.LogInfo("")
for _, line := range logLines {
utils.LogResult("%s\n", line)
}
}
func followLog(logFile string) {
utils.LogInfo("实时跟踪日志文件: %s", logFile)
utils.LogInfo("按 Ctrl+C 停止跟踪")
utils.LogInfo("")
file, err := os.Open(logFile)
if err != nil {
utils.LogError("打开日志文件失败: %v", err)
return
}
defer file.Close()
// 移动到文件末尾
file.Seek(0, io.SeekEnd)
scanner := bufio.NewScanner(file)
for {
for scanner.Scan() {
line := scanner.Text()
if logLevel == "" || strings.Contains(strings.ToUpper(line), strings.ToUpper("["+logLevel+"]")) {
utils.LogResult("%s\n", line)
}
}
// 等待新内容
time.Sleep(100 * time.Millisecond)
}
}
func readLastLines(file *os.File, lines int) []string {
// 获取文件大小
stat, err := file.Stat()
if err != nil {
return nil
}
fileSize := stat.Size()
if fileSize == 0 {
return nil
}
// 从文件末尾开始读取
var result []string
var buffer []byte
bufferSize := int64(4096)
position := fileSize
for position > 0 && len(result) < lines {
// 计算读取位置
readSize := bufferSize
if position < bufferSize {
readSize = position
}
position -= readSize
// 读取数据
buffer = make([]byte, readSize)
_, err := file.ReadAt(buffer, position)
if err != nil && err != io.EOF {
break
}
// 分割行
text := string(buffer)
if position > 0 {
// 如果不是文件开头,去掉第一行(可能不完整)
if idx := strings.Index(text, "\n"); idx >= 0 {
text = text[idx+1:]
}
}
textLines := strings.Split(text, "\n")
// 反向添加行
for i := len(textLines) - 1; i >= 0; i-- {
if len(textLines[i]) > 0 {
result = append([]string{textLines[i]}, result...)
if len(result) >= lines {
break
}
}
}
}
// 返回指定数量的行
if len(result) > lines {
result = result[len(result)-lines:]
}
return result
}
func filterLogLevel(lines []string, level string) []string {
var filtered []string
levelUpper := strings.ToUpper("[" + level + "]")
for _, line := range lines {
if strings.Contains(strings.ToUpper(line), levelUpper) {
filtered = append(filtered, line)
}
}
return filtered
}
func formatFileSize(bytes int64) string {
const unit = 1024
if bytes < unit {
return fmt.Sprintf("%d B", bytes)
}
div, exp := int64(unit), 0
for n := bytes / unit; n >= unit; n /= unit {
div *= unit
exp++
}
return fmt.Sprintf("%.1f %cB", float64(bytes)/float64(div), "KMGTPE"[exp])
}