package main import ( "flag" "fmt" "os" "path/filepath" "strings" "time" "hub.gaomia.site/titor/YunShu/pkg/style" "gopkg.in/yaml.v3" ) type LogLevel string const ( LevelWarn LogLevel = "warn" LevelError LogLevel = "error" LevelInfo LogLevel = "info" LevelOK LogLevel = "ok" ) var logToStderr bool type logEntry struct { Time string `yaml:"time"` Level LogLevel `yaml:"level"` Msg string `yaml:"msg"` Fields map[string]any `yaml:",inline,omitempty"` } func logPath() string { return filepath.Join(ConfigDir(), "log.yml") } func buildFields(keyvals []any) map[string]any { m := make(map[string]any) for i := 0; i < len(keyvals)-1; i += 2 { if key, ok := keyvals[i].(string); ok { m[key] = keyvals[i+1] } } return m } func appendLog(level LogLevel, msg string, keyvals ...any) { path := logPath() var entries []logEntry if data, err := os.ReadFile(path); err == nil && len(data) > 0 { yaml.Unmarshal(data, &entries) } if entries == nil { entries = make([]logEntry, 0) } entry := logEntry{ Time: time.Now().Format(time.RFC3339), Level: level, Msg: msg, } if len(keyvals) > 0 { entry.Fields = buildFields(keyvals) } entries = append(entries, entry) out, err := yaml.Marshal(entries) if err != nil { return } os.WriteFile(path, out, 0644) } func readLogs() ([]logEntry, error) { data, err := os.ReadFile(logPath()) if err != nil { if os.IsNotExist(err) { return nil, nil } return nil, err } if len(strings.TrimSpace(string(data))) == 0 { return nil, nil } var entries []logEntry if err := yaml.Unmarshal(data, &entries); err != nil { return nil, err } return entries, nil } func clearLogs() { os.WriteFile(logPath(), []byte("[]\n"), 0644) } func levelColor(l LogLevel) *style.Style { switch l { case LevelWarn: return style.Yellow case LevelError: return style.Red case LevelInfo: return style.Cyan case LevelOK: return style.Green default: return style.Dim } } func displayLogs(entries []logEntry, filterLevel string, top int) { if len(entries) == 0 { fmt.Println(style.Dim.Render("(暂无日志)")) return } // 按时间倒序 for i, j := 0, len(entries)-1; i < j; i, j = i+1, j-1 { entries[i], entries[j] = entries[j], entries[i] } // 筛选级别 if filterLevel != "" { var filtered []logEntry for _, e := range entries { if string(e.Level) == filterLevel { filtered = append(filtered, e) } } entries = filtered } if top > 0 && top < len(entries) { entries = entries[:top] } for _, e := range entries { ls := levelColor(e.Level) label := ls.Render(fmt.Sprintf("[%-5s]", strings.ToUpper(string(e.Level)))) timeStr := style.Dim.Render(e.Time) fmt.Printf("%s %s %s\n", label, timeStr, e.Msg) for k, v := range e.Fields { fmt.Printf(" %s = %v\n", style.Dim.Render(k), style.Dim.Render(fmt.Sprintf("%v", v))) } } } func followLogs(filterLevel string) { lastCount := 0 first := true for { entries, err := readLogs() if err != nil { time.Sleep(2 * time.Second) continue } n := len(entries) if n == 0 { if first { fmt.Println(style.Dim.Render("等待新日志...")) first = false } time.Sleep(2 * time.Second) continue } if first { fmt.Println(style.Dim.Render("日志监听中... Ctrl+C 退出")) fmt.Println() lastCount = n first = false time.Sleep(2 * time.Second) continue } if n > lastCount { for _, e := range entries[lastCount:] { if filterLevel != "" && string(e.Level) != filterLevel { continue } ls := levelColor(e.Level) label := ls.Render(fmt.Sprintf("[%-5s]", strings.ToUpper(string(e.Level)))) timeStr := style.Dim.Render(e.Time) fmt.Printf("%s %s %s\n", label, timeStr, e.Msg) for k, v := range e.Fields { fmt.Printf(" %s = %v\n", style.Dim.Render(k), style.Dim.Render(fmt.Sprintf("%v", v))) } } } lastCount = n time.Sleep(2 * time.Second) } } func runLogCmd(args []string) { fs := flag.NewFlagSet("log", flag.ExitOnError) top := fs.Int("top", 0, "显示最后 N 条日志") level := fs.String("level", "", "按级别过滤 (warn/error/info/ok)") clear := fs.Bool("clear", false, "清空日志文件") watch := fs.Bool("watch", false, "监听模式,实时输出新日志") fs.Parse(args) if *clear { if *top > 0 || *level != "" || *watch { fmt.Fprintln(os.Stderr, style.Red.Render("--clear 不能与其他选项组合")) os.Exit(1) } clearLogs() fmt.Println(style.Green.Render("日志已清空")) return } if *watch { followLogs(*level) return } entries, err := readLogs() if err != nil { fmt.Fprintln(os.Stderr, style.Red.Render("读取日志失败: "+err.Error())) os.Exit(1) } displayLogs(entries, *level, *top) } func warnLog(msg string, keyvals ...any) { if logToStderr { Log.Warn(msg, keyvals...) } appendLog(LevelWarn, msg, keyvals...) } func errorLog(msg string, keyvals ...any) { if logToStderr { Log.Error(msg, keyvals...) } appendLog(LevelError, msg, keyvals...) } func infoLog(msg string, keyvals ...any) { if logToStderr { Log.Info(msg, keyvals...) } appendLog(LevelInfo, msg, keyvals...) }