Skip to content

feat(daemon): add daily cleanup timer service for large log files#161

Merged
AnnatarHe merged 4 commits intomainfrom
feat/daemon-cleanup-timer-service
Dec 25, 2025
Merged

feat(daemon): add daily cleanup timer service for large log files#161
AnnatarHe merged 4 commits intomainfrom
feat/daemon-cleanup-timer-service

Conversation

@AnnatarHe
Copy link
Copy Markdown
Contributor

Summary

  • Add a configurable cleanup timer service that runs every 24 hours to automatically remove log files exceeding the configured threshold
  • Add LogCleanup config struct with enabled (default: true) and thresholdMB (default: 100) options
  • Add daemon log path helpers for macOS daemon logs
  • Create shared log cleanup module in model package
  • Refactor gc command to use shared cleanup logic
  • Create CleanupTimerService following HeartbeatResyncService pattern
  • Platform-aware: only cleans daemon logs on macOS (Linux uses systemd journal)

Config Example

# Log cleanup configuration (enabled by default)
[logCleanup]
enabled = true       # set to false to disable
thresholdMB = 100    # files larger than this will be deleted

Test plan

  • Verify daemon builds and starts correctly
  • Verify cleanup timer service logs startup message with threshold
  • Verify shelltime gc command works with configurable threshold
  • Test on macOS: daemon logs are cleaned when exceeding threshold
  • Test config: disable cleanup via enabled = false
  • Test config: custom threshold via thresholdMB

🤖 Generated with Claude Code

Add a configurable cleanup timer service that runs every 24 hours to
automatically remove log files exceeding the configured threshold.

Changes:
- Add LogCleanup config struct with enabled (default: true) and
  thresholdMB (default: 100) options
- Add daemon log path helpers for macOS daemon logs
- Create shared log cleanup module in model package
- Refactor gc command to use shared cleanup logic
- Create CleanupTimerService following HeartbeatResyncService pattern
- Platform-aware: only cleans daemon logs on macOS (Linux uses journal)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello @AnnatarHe, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a robust and configurable system for automatically managing and cleaning up large log files generated by the daemon and CLI. By implementing a daily cleanup timer service and centralizing log management logic, it aims to prevent excessive disk space consumption due to growing log files. The changes ensure that log cleanup is efficient, configurable, and intelligently handles platform-specific logging practices.

Highlights

  • Automated Log Cleanup Service: A new configurable cleanup timer service has been added to the daemon, designed to run every 24 hours and automatically remove log files that exceed a specified size threshold.
  • Configurable Options: The new LogCleanup configuration struct allows users to enable or disable the service and set a thresholdMB (defaulting to 100 MB) for log file size, ensuring flexibility in log management.
  • Shared Log Cleanup Logic: A shared log cleanup module has been created in the model package, centralizing the logic for cleaning both CLI and daemon log files. The existing gc command has been refactored to utilize this new shared logic.
  • Platform-Aware Cleanup: The log cleanup mechanism is platform-aware, specifically targeting daemon logs on macOS. On Linux, daemon logs are typically managed by systemd journal, so this service does not interfere with them.
  • Daemon Log Path Helpers: New helper functions have been introduced to easily retrieve the paths for macOS daemon log files, facilitating their management and cleanup.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a daily cleanup service for large log files, which is a great addition for long-running daemons. The implementation is mostly solid, following existing service patterns. I've identified a few areas for improvement, mainly around code duplication, API correctness, and consistency. My main suggestions involve refactoring the log cleaning logic into a shared helper to reduce duplication and correcting function signatures that incorrectly promise an error return. I've also pointed out a redundant check and the use of magic numbers that could be replaced by constants for better readability. Additionally, a reminder has been added regarding best practices for platform-independent path construction.

Comment thread cmd/daemon/main.go
}

// 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 {

Comment thread commands/gc.go
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.

Comment thread commands/gc.go
Comment on lines +196 to 199
freedBytes, err := model.CleanLargeLogFiles(thresholdBytes, forceCleanLogs)
if err != nil {
slog.Warn("error during log cleanup", slog.Any("err", err))
}
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)

Comment thread daemon/cleanup_timer.go

// 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.

Comment thread daemon/cleanup_timer.go
Comment on lines +82 to +85
freedCLI, err := model.CleanLargeLogFiles(thresholdBytes, false)
if err != nil {
slog.Warn("error during CLI log cleanup", slog.Any("err", err))
}
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)

Comment thread daemon/cleanup_timer.go
Comment on lines +89 to +92
freedDaemon, err := model.CleanDaemonLogFiles(thresholdBytes, false)
if err != nil {
slog.Warn("error during daemon log cleanup", slog.Any("err", err))
}
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)

Comment thread model/log_cleanup.go
Comment on lines +35 to +86
// CleanLargeLogFiles checks CLI log files and removes those exceeding the size threshold.
// thresholdBytes: size threshold in bytes
// If force is true, removes all log files regardless of size.
func CleanLargeLogFiles(thresholdBytes int64, force bool) (int64, error) {
logFiles := []string{
GetLogFilePath(),
GetHeartbeatLogFilePath(),
GetSyncPendingFilePath(),
}

var totalFreed int64
for _, filePath := range logFiles {
freed, err := CleanLogFile(filePath, thresholdBytes, 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
}

// CleanDaemonLogFiles cleans daemon-specific log files.
// On macOS, daemon logs go to ~/.shelltime/logs/shelltime-daemon.{log,err}
// On Linux, daemon logs go to systemd journal and can't be cleaned from here.
// thresholdBytes: size threshold in bytes
// If force is true, removes all log files regardless of size.
func CleanDaemonLogFiles(thresholdBytes int64, force bool) (int64, error) {
// Only clean daemon logs on macOS (darwin)
// On Linux, daemon uses systemd journal which is managed by journald
if runtime.GOOS != "darwin" {
return 0, nil
}

logFiles := []string{
GetDaemonLogFilePath(),
GetDaemonErrFilePath(),
}

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

return totalFreed, nil
}
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 functions CleanLargeLogFiles and CleanDaemonLogFiles contain duplicate logic for iterating over a list of files and cleaning them. This can be extracted into a private helper function to improve maintainability.

Additionally, both functions are declared to return an error, but they always return nil, making the error check at call sites redundant. The function signatures should be changed to only return (int64, error) to reflect this. The callers in commands/gc.go and daemon/cleanup_timer.go will need to be updated to no longer check for an error.

When constructing file paths, such as those returned by GetLogFilePath() and similar functions, ensure that filepath.Join is used for combining segments and os.UserHomeDir() for the home directory to maintain platform independence.

func cleanFiles(filePaths []string, thresholdBytes int64, force bool, warnMsg string) int64 {
	var totalFreed int64
	for _, filePath := range filePaths {
		freed, err := CleanLogFile(filePath, thresholdBytes, force)
		if err != nil {
			slog.Warn(warnMsg, slog.String("file", filePath), slog.Any("err", err))
			continue
		}
		totalFreed += freed
	}
	return totalFreed
}

// CleanLargeLogFiles checks CLI log files and removes those exceeding the size threshold.
// thresholdBytes: size threshold in bytes
// If force is true, removes all log files regardless of size.
func CleanLargeLogFiles(thresholdBytes int64, force bool) (int64, error) {
	logFiles := []string{
		GetLogFilePath(),
		GetHeartbeatLogFilePath(),
		GetSyncPendingFilePath(),
	}
	return cleanFiles(logFiles, thresholdBytes, force, "failed to clean log file"), nil
}

// CleanDaemonLogFiles cleans daemon-specific log files.
// On macOS, daemon logs go to ~/.shelltime/logs/shelltime-daemon.{log,err}
// On Linux, daemon logs go to systemd journal and can't be cleaned from here.
// thresholdBytes: size threshold in bytes
// If force is true, removes all log files regardless of size.
func CleanDaemonLogFiles(thresholdBytes int64, force bool) (int64, error) {
	// Only clean daemon logs on macOS (darwin)
	// On Linux, daemon uses systemd journal which is managed by journald
	if runtime.GOOS != "darwin" {
		return 0, nil
	}

	logFiles := []string{
		GetDaemonLogFilePath(),
		GetDaemonErrFilePath(),
	}
	return cleanFiles(logFiles, thresholdBytes, force, "failed to clean daemon log file"), nil
}
References
  1. For platform-independent paths, use filepath.Join to combine segments and os.UserHomeDir() to get the home directory, rather than hardcoding path separators or environment variables like $HOME.

AnnatarHe and others added 3 commits December 25, 2025 22:27
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add comprehensive tests for:
- LogCleanup defaults (enabled=true, thresholdMB=100)
- LogCleanup custom values
- LogCleanup partial config (only enabled or only threshold)
- LogCleanup merge from local config
- CodeTracking merge from local config

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The gc command now requires LogCleanup config to be present.
Update the mock to include proper LogCleanup defaults.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@codecov
Copy link
Copy Markdown

codecov Bot commented Dec 25, 2025

Codecov Report

❌ Patch coverage is 14.75410% with 104 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
daemon/cleanup_timer.go 0.00% 47 Missing ⚠️
model/log_cleanup.go 0.00% 40 Missing ⚠️
cmd/daemon/main.go 0.00% 8 Missing ⚠️
model/path.go 0.00% 6 Missing ⚠️
commands/gc.go 50.00% 2 Missing and 1 partial ⚠️
Flag Coverage Δ
unittests 20.44% <14.75%> (-0.20%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
model/config.go 61.76% <100.00%> (+8.89%) ⬆️
commands/gc.go 42.99% <50.00%> (-0.76%) ⬇️
model/path.go 0.00% <0.00%> (ø)
cmd/daemon/main.go 0.00% <0.00%> (ø)
model/log_cleanup.go 0.00% <0.00%> (ø)
daemon/cleanup_timer.go 0.00% <0.00%> (ø)

... and 1 file with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@AnnatarHe AnnatarHe merged commit c6d7429 into main Dec 25, 2025
2 of 3 checks passed
@AnnatarHe AnnatarHe deleted the feat/daemon-cleanup-timer-service branch December 25, 2025 14:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant