mirror of
https://github.com/charmbracelet/crush.git
synced 2026-05-30 18:47:33 +00:00
* refactor(config): introduce ConfigStore and Scope for better config management This makes config.Config immutable and introduces a ConfigStore that manages the config and provides helper methods for accessing config values with proper scoping (global, workspace). This allows us to avoid passing around mutable config objects and ensures that all parts of the code are accessing the most up-to-date config values. It also lays the groundwork for future features like per-workspace config overrides. * fixt: lint
217 lines
4.9 KiB
Go
217 lines
4.9 KiB
Go
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.Config().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"]
|
|
var otherData []any
|
|
var 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...)
|
|
}
|
|
}
|