347 lines
7.8 KiB
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])
|
|
}
|