Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions cmd/daemon/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,18 @@ func main() {
defer syncCircuitBreakerService.Stop()
}

// Start cleanup timer service if enabled (enabled by default)
if cfg.LogCleanup != nil && cfg.LogCleanup.Enabled != nil && *cfg.LogCleanup.Enabled {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The ReadConfigFile function ensures that cfg.LogCleanup and cfg.LogCleanup.Enabled are initialized with default values and are never nil. Therefore, this check can be simplified to just check the boolean value.

Suggested change
if cfg.LogCleanup != nil && cfg.LogCleanup.Enabled != nil && *cfg.LogCleanup.Enabled {
if *cfg.LogCleanup.Enabled {

cleanupTimerService := daemon.NewCleanupTimerService(cfg)
if err := cleanupTimerService.Start(ctx); err != nil {
slog.Error("Failed to start cleanup timer service", slog.Any("err", err))
} else {
slog.Info("Cleanup timer service started",
slog.Int64("thresholdMB", cfg.LogCleanup.ThresholdMB))
defer cleanupTimerService.Stop()
}
}

go daemon.SocketTopicProcessor(msg)

// Start CCUsage service if enabled (v1 - ccusage CLI based)
Expand Down
58 changes: 9 additions & 49 deletions commands/gc.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ import (
"go.opentelemetry.io/otel/trace"
)

const logFileSizeThreshold int64 = 50 * 1024 * 1024 // 50 MB

var GCCommand *cli.Command = &cli.Command{
Name: "gc",
Usage: "clean internal storage",
Expand All @@ -34,52 +32,6 @@ var GCCommand *cli.Command = &cli.Command{
Action: commandGC,
}

// cleanLogFile removes a log file if it exceeds the threshold or if force is true.
// Returns the size of the deleted file (0 if not deleted or file doesn't exist).
func cleanLogFile(filePath string, threshold int64, force bool) (int64, error) {
info, err := os.Stat(filePath)
if os.IsNotExist(err) {
return 0, nil
}
if err != nil {
return 0, fmt.Errorf("failed to stat file %s: %w", filePath, err)
}

fileSize := info.Size()
if !force && fileSize < threshold {
return 0, nil
}

if err := os.Remove(filePath); err != nil {
return 0, fmt.Errorf("failed to remove file %s: %w", filePath, err)
}

slog.Info("cleaned log file", slog.String("file", filePath), slog.Int64("size_bytes", fileSize))
return fileSize, nil
}

// cleanLargeLogFiles checks all log files and removes those exceeding the size threshold.
// If force is true, removes all log files regardless of size.
func cleanLargeLogFiles(force bool) (int64, error) {
logFiles := []string{
model.GetLogFilePath(),
model.GetHeartbeatLogFilePath(),
model.GetSyncPendingFilePath(),
}

var totalFreed int64
for _, filePath := range logFiles {
freed, err := cleanLogFile(filePath, logFileSizeThreshold, force)
if err != nil {
slog.Warn("failed to clean log file", slog.String("file", filePath), slog.Any("err", err))
continue
}
totalFreed += freed
}

return totalFreed, nil
}

// backupAndWriteFile backs up the existing file and writes new content.
func backupAndWriteFile(filePath string, content []byte) error {
backupFile := filePath + ".bak"
Expand Down Expand Up @@ -231,9 +183,17 @@ func commandGC(c *cli.Context) error {
return nil
}

// Get config for threshold
cfg, err := configService.ReadConfigFile(ctx)
if err != nil {
slog.Warn("failed to read config, using default threshold", slog.Any("err", err))
cfg.LogCleanup = &model.LogCleanup{ThresholdMB: 100}
}
thresholdBytes := cfg.LogCleanup.ThresholdMB * 1024 * 1024
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The magic number multiplication * 1024 * 1024 to convert megabytes to bytes is also used in daemon/cleanup_timer.go. To improve readability and avoid duplication, consider defining a shared constant for this conversion, for example const BytesPerMB = 1024 * 1024 in the model package, and using it in both places.


// Clean log files: force clean if --withLog, otherwise only clean large files
forceCleanLogs := c.Bool("withLog")
freedBytes, err := cleanLargeLogFiles(forceCleanLogs)
freedBytes, err := model.CleanLargeLogFiles(thresholdBytes, forceCleanLogs)
if err != nil {
slog.Warn("error during log cleanup", slog.Any("err", err))
}
Comment on lines +196 to 199
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Following the proposed change in model/log_cleanup.go to have CleanLargeLogFiles no longer return an error, this error handling block becomes redundant and can be removed.

Suggested change
freedBytes, err := model.CleanLargeLogFiles(thresholdBytes, forceCleanLogs)
if err != nil {
slog.Warn("error during log cleanup", slog.Any("err", err))
}
freedBytes, _ := model.CleanLargeLogFiles(thresholdBytes, forceCleanLogs)

Expand Down
5 changes: 5 additions & 0 deletions commands/track_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,11 +122,16 @@ func (s *trackTestSuite) TestTrackWithSendData() {
}))
defer server.Close()
cs := model.NewMockConfigService(s.T())
truthy := true
mockedConfig := model.ShellTimeConfig{
Token: "TOKEN001",
APIEndpoint: server.URL,
FlushCount: 7,
GCTime: 8,
LogCleanup: &model.LogCleanup{
Enabled: &truthy,
ThresholdMB: 100,
},
}
cs.On("ReadConfigFile", mock.Anything).Return(mockedConfig, nil)
model.UserShellTimeConfig = mockedConfig
Expand Down
103 changes: 103 additions & 0 deletions daemon/cleanup_timer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package daemon

import (
"context"
"log/slog"
"sync"
"time"

"github.com/malamtime/cli/model"
)

const (
// CleanupInterval is the interval for log cleanup (24 hours)
CleanupInterval = 24 * time.Hour
)

// CleanupTimerService handles periodic cleanup of large log files
type CleanupTimerService struct {
config model.ShellTimeConfig
ticker *time.Ticker
stopChan chan struct{}
wg sync.WaitGroup
}

// NewCleanupTimerService creates a new cleanup timer service
func NewCleanupTimerService(config model.ShellTimeConfig) *CleanupTimerService {
return &CleanupTimerService{
config: config,
stopChan: make(chan struct{}),
}
}

// Start begins the periodic cleanup job
func (s *CleanupTimerService) Start(ctx context.Context) error {
s.ticker = time.NewTicker(CleanupInterval)
s.wg.Add(1)

go func() {
defer s.wg.Done()

// NOTE: Do not run at startup, only on timer
// This avoids slowing daemon startup and prevents cleanup on restart loops

for {
select {
case <-s.ticker.C:
s.cleanup(ctx)
case <-s.stopChan:
return
case <-ctx.Done():
return
}
}
}()

slog.Info("Cleanup timer service started",
slog.Duration("interval", CleanupInterval),
slog.Int64("thresholdMB", s.config.LogCleanup.ThresholdMB))
return nil
}

// Stop stops the cleanup service
func (s *CleanupTimerService) Stop() {
if s.ticker != nil {
s.ticker.Stop()
}
close(s.stopChan)
s.wg.Wait()
slog.Info("Cleanup timer service stopped")
}

// cleanup performs the log cleanup
func (s *CleanupTimerService) cleanup(ctx context.Context) {
thresholdBytes := s.config.LogCleanup.ThresholdMB * 1024 * 1024
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The magic number multiplication * 1024 * 1024 to convert megabytes to bytes is also used in commands/gc.go. To improve readability and avoid duplication, consider defining a shared constant for this conversion, for example const BytesPerMB = 1024 * 1024 in the model package, and using it in both places.


slog.Debug("Starting scheduled log cleanup",
slog.Int64("thresholdMB", s.config.LogCleanup.ThresholdMB))

var totalFreed int64

// Clean CLI log files
freedCLI, err := model.CleanLargeLogFiles(thresholdBytes, false)
if err != nil {
slog.Warn("error during CLI log cleanup", slog.Any("err", err))
}
Comment on lines +82 to +85
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Following the proposed change in model/log_cleanup.go to have CleanLargeLogFiles no longer return an error, this error handling block becomes redundant and can be removed.

Suggested change
freedCLI, err := model.CleanLargeLogFiles(thresholdBytes, false)
if err != nil {
slog.Warn("error during CLI log cleanup", slog.Any("err", err))
}
freedCLI, _ := model.CleanLargeLogFiles(thresholdBytes, false)

totalFreed += freedCLI

// Clean daemon log files (macOS only)
freedDaemon, err := model.CleanDaemonLogFiles(thresholdBytes, false)
if err != nil {
slog.Warn("error during daemon log cleanup", slog.Any("err", err))
}
Comment on lines +89 to +92
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Following the proposed change in model/log_cleanup.go to have CleanDaemonLogFiles no longer return an error, this error handling block becomes redundant and can be removed.

Suggested change
freedDaemon, err := model.CleanDaemonLogFiles(thresholdBytes, false)
if err != nil {
slog.Warn("error during daemon log cleanup", slog.Any("err", err))
}
freedDaemon, _ := model.CleanDaemonLogFiles(thresholdBytes, false)

totalFreed += freedDaemon

if totalFreed > 0 {
slog.Info("scheduled log cleanup completed",
slog.Int64("totalFreedBytes", totalFreed),
slog.Int64("cliFreedBytes", freedCLI),
slog.Int64("daemonFreedBytes", freedDaemon))
} else {
slog.Debug("scheduled log cleanup completed, no files exceeded threshold")
}
}
24 changes: 24 additions & 0 deletions model/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,18 @@ func mergeConfig(base, local *ShellTimeConfig) {
if local.CCOtel != nil {
base.CCOtel = local.CCOtel
}
if local.LogCleanup != nil {
base.LogCleanup = local.LogCleanup
}
if local.SocketPath != "" {
base.SocketPath = local.SocketPath
}
if local.CodeTracking != nil {
base.CodeTracking = local.CodeTracking
}
if local.LogCleanup != nil {
base.LogCleanup = local.LogCleanup
}
}

func (cs *configService) ReadConfigFile(ctx context.Context, opts ...ReadConfigOption) (config ShellTimeConfig, err error) {
Expand Down Expand Up @@ -177,6 +186,21 @@ func (cs *configService) ReadConfigFile(ctx context.Context, opts ...ReadConfigO
config.SocketPath = DefaultSocketPath
}

// Initialize LogCleanup with defaults if not present (enabled by default with 100MB threshold)
if config.LogCleanup == nil {
config.LogCleanup = &LogCleanup{
Enabled: &truthy,
ThresholdMB: 100,
}
} else {
if config.LogCleanup.Enabled == nil {
config.LogCleanup.Enabled = &truthy
}
if config.LogCleanup.ThresholdMB == 0 {
config.LogCleanup.ThresholdMB = 100
}
}

// Save to cache
cs.mu.Lock()
cs.cachedConfig = &config
Expand Down
Loading