| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216 |
- package cmd
- import (
- "context"
- "encoding/json"
- "fmt"
- "io"
- "os"
- "path/filepath"
- "slices"
- "time"
- "charm.land/log/v2"
- "github.com/charmbracelet/colorprofile"
- "github.com/charmbracelet/crush/internal/config"
- "github.com/charmbracelet/x/term"
- "github.com/nxadm/tail"
- "github.com/spf13/cobra"
- )
- const defaultTailLines = 1000
- var logsCmd = &cobra.Command{
- Use: "logs",
- Short: "View crush logs",
- Long: `View the logs generated by Crush. This command allows you to see the log output for debugging and monitoring.`,
- RunE: func(cmd *cobra.Command, args []string) error {
- cwd, err := cmd.Flags().GetString("cwd")
- if err != nil {
- return fmt.Errorf("failed to get current working directory: %v", err)
- }
- dataDir, err := cmd.Flags().GetString("data-dir")
- if err != nil {
- return fmt.Errorf("failed to get data directory: %v", err)
- }
- follow, err := cmd.Flags().GetBool("follow")
- if err != nil {
- return fmt.Errorf("failed to get follow flag: %v", err)
- }
- tailLines, err := cmd.Flags().GetInt("tail")
- if err != nil {
- return fmt.Errorf("failed to get tail flag: %v", err)
- }
- log.SetLevel(log.DebugLevel)
- log.SetOutput(os.Stdout)
- if !term.IsTerminal(os.Stdout.Fd()) {
- log.SetColorProfile(colorprofile.NoTTY)
- }
- cfg, err := config.Load(cwd, dataDir, false)
- if err != nil {
- return fmt.Errorf("failed to load configuration: %v", err)
- }
- logsFile := filepath.Join(cfg.Options.DataDirectory, "logs", "crush.log")
- _, err = os.Stat(logsFile)
- if os.IsNotExist(err) {
- log.Warn("Looks like you are not in a crush project. No logs found.")
- return nil
- }
- if follow {
- return followLogs(cmd.Context(), logsFile, tailLines)
- }
- return showLogs(logsFile, tailLines)
- },
- }
- func init() {
- logsCmd.Flags().BoolP("follow", "f", false, "Follow log output")
- logsCmd.Flags().IntP("tail", "t", defaultTailLines, "Show only the last N lines default: 1000 for performance")
- }
- func followLogs(ctx context.Context, logsFile string, tailLines int) error {
- t, err := tail.TailFile(logsFile, tail.Config{
- Follow: false,
- ReOpen: false,
- Logger: tail.DiscardingLogger,
- })
- if err != nil {
- return fmt.Errorf("failed to tail log file: %v", err)
- }
- var lines []string
- for line := range t.Lines {
- if line.Err != nil {
- continue
- }
- lines = append(lines, line.Text)
- if len(lines) > tailLines {
- lines = lines[len(lines)-tailLines:]
- }
- }
- t.Stop()
- for _, line := range lines {
- printLogLine(line)
- }
- if len(lines) == tailLines {
- fmt.Fprintf(os.Stderr, "\nShowing last %d lines. Full logs available at: %s\n", tailLines, logsFile)
- fmt.Fprintf(os.Stderr, "Following new log entries...\n\n")
- }
- t, err = tail.TailFile(logsFile, tail.Config{
- Follow: true,
- ReOpen: true,
- Logger: tail.DiscardingLogger,
- Location: &tail.SeekInfo{Offset: 0, Whence: io.SeekEnd},
- })
- if err != nil {
- return fmt.Errorf("failed to tail log file: %v", err)
- }
- defer t.Stop()
- for {
- select {
- case line := <-t.Lines:
- if line.Err != nil {
- continue
- }
- printLogLine(line.Text)
- case <-ctx.Done():
- return nil
- }
- }
- }
- func showLogs(logsFile string, tailLines int) error {
- t, err := tail.TailFile(logsFile, tail.Config{
- Follow: false,
- ReOpen: false,
- Logger: tail.DiscardingLogger,
- MaxLineSize: 0,
- })
- if err != nil {
- return fmt.Errorf("failed to tail log file: %v", err)
- }
- defer t.Stop()
- var lines []string
- for line := range t.Lines {
- if line.Err != nil {
- continue
- }
- lines = append(lines, line.Text)
- if len(lines) > tailLines {
- lines = lines[len(lines)-tailLines:]
- }
- }
- for _, line := range lines {
- printLogLine(line)
- }
- if len(lines) == tailLines {
- fmt.Fprintf(os.Stderr, "\nShowing last %d lines. Full logs available at: %s\n", tailLines, logsFile)
- }
- return nil
- }
- func printLogLine(lineText string) {
- var data map[string]any
- if err := json.Unmarshal([]byte(lineText), &data); err != nil {
- return
- }
- msg := data["msg"]
- level := data["level"]
- otherData := []any{}
- keys := []string{}
- for k := range data {
- keys = append(keys, k)
- }
- slices.Sort(keys)
- for _, k := range keys {
- switch k {
- case "msg", "level", "time":
- continue
- case "source":
- source, ok := data[k].(map[string]any)
- if !ok {
- continue
- }
- sourceFile := fmt.Sprintf("%s:%d", source["file"], int(source["line"].(float64)))
- otherData = append(otherData, "source", sourceFile)
- default:
- otherData = append(otherData, k, data[k])
- }
- }
- log.SetTimeFunction(func(_ time.Time) time.Time {
- // parse the timestamp from the log line if available
- t, err := time.Parse(time.RFC3339, data["time"].(string))
- if err != nil {
- return time.Now() // fallback to current time if parsing fails
- }
- return t
- })
- switch level {
- case "INFO":
- log.Info(msg, otherData...)
- case "DEBUG":
- log.Debug(msg, otherData...)
- case "ERROR":
- log.Error(msg, otherData...)
- case "WARN":
- log.Warn(msg, otherData...)
- default:
- log.Info(msg, otherData...)
- }
- }
|