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]) }