Skip to content

Commit 3001bd9

Browse files
committed
slog to file when using the tui and support overriding the log file path
closes #101 Signed-off-by: Christopher Petito <[email protected]>
1 parent b63533a commit 3001bd9

File tree

3 files changed

+120
-16
lines changed

3 files changed

+120
-16
lines changed

cmd/root/root.go

Lines changed: 108 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,20 @@ import (
55
"log/slog"
66
"os"
77
"path/filepath"
8+
"strings"
9+
"time"
810

911
"github.com/docker/cagent/internal/config"
1012
"github.com/docker/cagent/internal/telemetry"
1113
"github.com/spf13/cobra"
1214
)
1315

1416
var (
15-
agentName string
16-
debugMode bool
17-
enableOtel bool
17+
agentName string
18+
debugMode bool
19+
enableOtel bool
20+
logFilePath string
21+
logFile *os.File
1822
)
1923

2024
// isFirstRun checks if this is the first time cagent is being run
@@ -47,15 +51,28 @@ func NewRootCmd() *cobra.Command {
4751
Short: "cagent - AI agent runner",
4852
Long: `cagent is a command-line tool for running AI agents`,
4953
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
54+
// Initialize logging before anything else so logs don't break TUI
55+
if err := setupLogging(cmd); err != nil {
56+
// If logging setup fails, fall back to stderr so we still get logs
57+
slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
58+
Level: func() slog.Level {
59+
if debugMode {
60+
return slog.LevelDebug
61+
}
62+
return slog.LevelInfo
63+
}(),
64+
})))
65+
}
5066
if cmd.DisplayName() != "exec" && os.Getenv("CAGENT_HIDE_FEEDBACK_LINK") != "1" {
5167
_, _ = cmd.OutOrStdout().Write([]byte("\nFor any feedback, please visit: " + FeedbackLink + "\n\n"))
5268
}
5369

5470
telemetry.SetGlobalTelemetryDebugMode(debugMode)
55-
if debugMode {
56-
slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
57-
Level: slog.LevelDebug,
58-
})))
71+
return nil
72+
},
73+
PersistentPostRunE: func(cmd *cobra.Command, args []string) error {
74+
if logFile != nil {
75+
_ = logFile.Close()
5976
}
6077
return nil
6178
},
@@ -68,6 +85,7 @@ func NewRootCmd() *cobra.Command {
6885
// Add persistent debug flag available to all commands
6986
cmd.PersistentFlags().BoolVarP(&debugMode, "debug", "d", false, "Enable debug logging")
7087
cmd.PersistentFlags().BoolVarP(&enableOtel, "otel", "o", false, "Enable OpenTelemetry tracing")
88+
cmd.PersistentFlags().StringVar(&logFilePath, "log-file", "", "Path to log file (default: ~/.cagent/cagent.log)")
7189

7290
cmd.AddCommand(NewVersionCmd())
7391
cmd.AddCommand(NewRunCmd())
@@ -110,3 +128,86 @@ We collect anonymous usage data to help improve cagent. To disable:
110128
os.Exit(1)
111129
}
112130
}
131+
132+
// setupLogging configures slog to write to a file instead of stdout/stderr when using the TUI.
133+
// By default, it writes to <dataDir>/logs/cagent-<timestamp>.log. Users can override with --log-file.
134+
func setupLogging(cmd *cobra.Command) error {
135+
// Determine log file path
136+
if logFilePath != "" {
137+
path := logFilePath
138+
if path == "" {
139+
dataDir := config.GetDataDir()
140+
path = filepath.Join(dataDir, "cagent.log")
141+
} else {
142+
if path == "~" || strings.HasPrefix(path, "~/") {
143+
homeDir, err := os.UserHomeDir()
144+
if err == nil {
145+
path = filepath.Join(homeDir, strings.TrimPrefix(path, "~/"))
146+
}
147+
} else if strings.HasPrefix(path, "~\\") { // Windows-style path expansion
148+
homeDir, err := os.UserHomeDir()
149+
if err == nil {
150+
path = filepath.Join(homeDir, strings.TrimPrefix(path, "~\\"))
151+
}
152+
}
153+
}
154+
155+
// Ensure directory exists
156+
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
157+
return err
158+
}
159+
160+
// Open file for appending
161+
f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
162+
if err != nil {
163+
return err
164+
}
165+
logFile = f
166+
167+
// Configure slog default logger
168+
level := slog.LevelInfo
169+
if debugMode {
170+
level = slog.LevelDebug
171+
}
172+
slog.SetDefault(slog.New(slog.NewTextHandler(f, &slog.HandlerOptions{Level: level})))
173+
return nil
174+
}
175+
176+
// Else, decide based on TUI flag: file when TUI is enabled, stderr otherwise
177+
useTUI := false
178+
if cmd != nil && cmd.Name() == "run" {
179+
if f := cmd.Flags().Lookup("tui"); f != nil {
180+
if v, err := cmd.Flags().GetBool("tui"); err == nil {
181+
useTUI = v
182+
}
183+
}
184+
}
185+
if useTUI {
186+
dataDir := config.GetDataDir()
187+
logsDir := filepath.Join(dataDir, "logs")
188+
if err := os.MkdirAll(logsDir, 0o755); err != nil {
189+
return err
190+
}
191+
timestamp := time.Now().Format("20060102-150405")
192+
path := filepath.Join(logsDir, fmt.Sprintf("cagent-%s.log", timestamp))
193+
f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0o644)
194+
if err != nil {
195+
return err
196+
}
197+
logFile = f
198+
level := slog.LevelInfo
199+
if debugMode {
200+
level = slog.LevelDebug
201+
}
202+
slog.SetDefault(slog.New(slog.NewTextHandler(f, &slog.HandlerOptions{Level: level})))
203+
return nil
204+
}
205+
206+
// Default to stderr
207+
level := slog.LevelInfo
208+
if debugMode {
209+
level = slog.LevelDebug
210+
}
211+
slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: level})))
212+
return nil
213+
}

internal/config/paths.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,13 @@ func GetConfigDir() string {
1515
}
1616
return filepath.Join(homeDir, ".config", "cagent")
1717
}
18+
19+
// GetDataDir returns the user's data directory for cagent (caches, content, logs)
20+
// Falls back to temp directory if home directory cannot be determined
21+
func GetDataDir() string {
22+
homeDir, err := os.UserHomeDir()
23+
if err != nil {
24+
return filepath.Join(os.TempDir(), ".cagent")
25+
}
26+
return filepath.Join(homeDir, ".cagent")
27+
}

internal/telemetry/global.go

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package telemetry
33
import (
44
"context"
55
"log/slog"
6-
"os"
76
"sync"
87
)
98

@@ -69,14 +68,8 @@ func ensureGlobalTelemetryInitialized() {
6968
// Use the debug mode set by the root package via --debug flag
7069
debugMode := globalTelemetryDebugMode
7170

72-
// Create logger with appropriate level based on debug mode
73-
logLevel := slog.LevelInfo
74-
if debugMode {
75-
logLevel = slog.LevelDebug
76-
}
77-
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
78-
Level: logLevel,
79-
}))
71+
// Use the global default logger configured by the root command
72+
logger := slog.Default()
8073

8174
// Get telemetry enabled setting
8275
enabled := GetTelemetryEnabled()

0 commit comments

Comments
 (0)