logs.go 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. package cmd
  2. import (
  3. "context"
  4. "encoding/json"
  5. "fmt"
  6. "io"
  7. "os"
  8. "path/filepath"
  9. "slices"
  10. "time"
  11. "charm.land/log/v2"
  12. "github.com/charmbracelet/colorprofile"
  13. "github.com/charmbracelet/crush/internal/config"
  14. "github.com/charmbracelet/x/term"
  15. "github.com/nxadm/tail"
  16. "github.com/spf13/cobra"
  17. )
  18. const defaultTailLines = 1000
  19. var logsCmd = &cobra.Command{
  20. Use: "logs",
  21. Short: "View crush logs",
  22. Long: `View the logs generated by Crush. This command allows you to see the log output for debugging and monitoring.`,
  23. RunE: func(cmd *cobra.Command, args []string) error {
  24. cwd, err := cmd.Flags().GetString("cwd")
  25. if err != nil {
  26. return fmt.Errorf("failed to get current working directory: %v", err)
  27. }
  28. dataDir, err := cmd.Flags().GetString("data-dir")
  29. if err != nil {
  30. return fmt.Errorf("failed to get data directory: %v", err)
  31. }
  32. follow, err := cmd.Flags().GetBool("follow")
  33. if err != nil {
  34. return fmt.Errorf("failed to get follow flag: %v", err)
  35. }
  36. tailLines, err := cmd.Flags().GetInt("tail")
  37. if err != nil {
  38. return fmt.Errorf("failed to get tail flag: %v", err)
  39. }
  40. log.SetLevel(log.DebugLevel)
  41. log.SetOutput(os.Stdout)
  42. if !term.IsTerminal(os.Stdout.Fd()) {
  43. log.SetColorProfile(colorprofile.NoTTY)
  44. }
  45. cfg, err := config.Load(cwd, dataDir, false)
  46. if err != nil {
  47. return fmt.Errorf("failed to load configuration: %v", err)
  48. }
  49. logsFile := filepath.Join(cfg.Options.DataDirectory, "logs", "crush.log")
  50. _, err = os.Stat(logsFile)
  51. if os.IsNotExist(err) {
  52. log.Warn("Looks like you are not in a crush project. No logs found.")
  53. return nil
  54. }
  55. if follow {
  56. return followLogs(cmd.Context(), logsFile, tailLines)
  57. }
  58. return showLogs(logsFile, tailLines)
  59. },
  60. }
  61. func init() {
  62. logsCmd.Flags().BoolP("follow", "f", false, "Follow log output")
  63. logsCmd.Flags().IntP("tail", "t", defaultTailLines, "Show only the last N lines default: 1000 for performance")
  64. }
  65. func followLogs(ctx context.Context, logsFile string, tailLines int) error {
  66. t, err := tail.TailFile(logsFile, tail.Config{
  67. Follow: false,
  68. ReOpen: false,
  69. Logger: tail.DiscardingLogger,
  70. })
  71. if err != nil {
  72. return fmt.Errorf("failed to tail log file: %v", err)
  73. }
  74. var lines []string
  75. for line := range t.Lines {
  76. if line.Err != nil {
  77. continue
  78. }
  79. lines = append(lines, line.Text)
  80. if len(lines) > tailLines {
  81. lines = lines[len(lines)-tailLines:]
  82. }
  83. }
  84. t.Stop()
  85. for _, line := range lines {
  86. printLogLine(line)
  87. }
  88. if len(lines) == tailLines {
  89. fmt.Fprintf(os.Stderr, "\nShowing last %d lines. Full logs available at: %s\n", tailLines, logsFile)
  90. fmt.Fprintf(os.Stderr, "Following new log entries...\n\n")
  91. }
  92. t, err = tail.TailFile(logsFile, tail.Config{
  93. Follow: true,
  94. ReOpen: true,
  95. Logger: tail.DiscardingLogger,
  96. Location: &tail.SeekInfo{Offset: 0, Whence: io.SeekEnd},
  97. })
  98. if err != nil {
  99. return fmt.Errorf("failed to tail log file: %v", err)
  100. }
  101. defer t.Stop()
  102. for {
  103. select {
  104. case line := <-t.Lines:
  105. if line.Err != nil {
  106. continue
  107. }
  108. printLogLine(line.Text)
  109. case <-ctx.Done():
  110. return nil
  111. }
  112. }
  113. }
  114. func showLogs(logsFile string, tailLines int) error {
  115. t, err := tail.TailFile(logsFile, tail.Config{
  116. Follow: false,
  117. ReOpen: false,
  118. Logger: tail.DiscardingLogger,
  119. MaxLineSize: 0,
  120. })
  121. if err != nil {
  122. return fmt.Errorf("failed to tail log file: %v", err)
  123. }
  124. defer t.Stop()
  125. var lines []string
  126. for line := range t.Lines {
  127. if line.Err != nil {
  128. continue
  129. }
  130. lines = append(lines, line.Text)
  131. if len(lines) > tailLines {
  132. lines = lines[len(lines)-tailLines:]
  133. }
  134. }
  135. for _, line := range lines {
  136. printLogLine(line)
  137. }
  138. if len(lines) == tailLines {
  139. fmt.Fprintf(os.Stderr, "\nShowing last %d lines. Full logs available at: %s\n", tailLines, logsFile)
  140. }
  141. return nil
  142. }
  143. func printLogLine(lineText string) {
  144. var data map[string]any
  145. if err := json.Unmarshal([]byte(lineText), &data); err != nil {
  146. return
  147. }
  148. msg := data["msg"]
  149. level := data["level"]
  150. otherData := []any{}
  151. keys := []string{}
  152. for k := range data {
  153. keys = append(keys, k)
  154. }
  155. slices.Sort(keys)
  156. for _, k := range keys {
  157. switch k {
  158. case "msg", "level", "time":
  159. continue
  160. case "source":
  161. source, ok := data[k].(map[string]any)
  162. if !ok {
  163. continue
  164. }
  165. sourceFile := fmt.Sprintf("%s:%d", source["file"], int(source["line"].(float64)))
  166. otherData = append(otherData, "source", sourceFile)
  167. default:
  168. otherData = append(otherData, k, data[k])
  169. }
  170. }
  171. log.SetTimeFunction(func(_ time.Time) time.Time {
  172. // parse the timestamp from the log line if available
  173. t, err := time.Parse(time.RFC3339, data["time"].(string))
  174. if err != nil {
  175. return time.Now() // fallback to current time if parsing fails
  176. }
  177. return t
  178. })
  179. switch level {
  180. case "INFO":
  181. log.Info(msg, otherData...)
  182. case "DEBUG":
  183. log.Debug(msg, otherData...)
  184. case "ERROR":
  185. log.Error(msg, otherData...)
  186. case "WARN":
  187. log.Warn(msg, otherData...)
  188. default:
  189. log.Info(msg, otherData...)
  190. }
  191. }