From 2298cfceb6e6d0516e795333b5f0f0d41e6e271c Mon Sep 17 00:00:00 2001 From: s4na Date: Sat, 7 Mar 2026 15:35:52 +0900 Subject: [PATCH 1/3] feat: add `ldcron log setup-rotation` for newsyslog-based log rotation Log files grow indefinitely because launchd has no built-in rotation. Add a `log setup-rotation` subcommand that prints a newsyslog(8) config snippet using the G (glob) flag to cover all ldcron log files. Users install it once with: ldcron log setup-rotation | sudo tee /etc/newsyslog.d/com.ldcron.conf The config rotates logs at 1 MB, keeps 3 gzip archives, and requires no process signaling (GNB flags). newsyslog already runs hourly via a system launchd job, so no extra scheduling is needed. Co-Authored-By: Claude Opus 4.6 --- README.ja.md | 11 ++++++++++ README.md | 16 ++++++++++++++ cmd/log.go | 55 +++++++++++++++++++++++++++++++++++++++++++++++++ cmd/log_test.go | 37 +++++++++++++++++++++++++++++++++ cmd/root.go | 6 ++++++ 5 files changed, 125 insertions(+) create mode 100644 cmd/log.go create mode 100644 cmd/log_test.go diff --git a/README.ja.md b/README.ja.md index e9e6d07..7a235ff 100644 --- a/README.ja.md +++ b/README.ja.md @@ -239,6 +239,17 @@ tail -f ~/Library/Logs/ldcron/a1b2c3d4.log tail -n 100 ~/Library/Logs/ldcron/a1b2c3d4.log ``` +### ログローテーション + +ログファイルはデフォルトでは無限に増え続けます。ldcronは[newsyslog(8)](x-man-page://newsyslog)の設定を生成するコマンドを提供しており、すべてのldcronログファイルを自動的にローテーションできます。 + +```bash +# newsyslog設定を生成・インストール(初回のみ、sudo必要) +ldcron log setup-rotation | sudo tee /etc/newsyslog.d/com.ldcron.conf +``` + +生成される設定は、各ログファイルが1MBを超えた時点でローテーションし、gzip圧縮したアーカイブを3世代保持します。newsyslogはシステムのlaunchdジョブにより毎時自動実行されるため、追加のスケジュール設定は不要です。 + --- ## ファイル配置 diff --git a/README.md b/README.md index af4c598..bf08b95 100644 --- a/README.md +++ b/README.md @@ -239,6 +239,22 @@ tail -f ~/Library/Logs/ldcron/a1b2c3d4.log tail -n 100 ~/Library/Logs/ldcron/a1b2c3d4.log ``` +### Log rotation + +Log files grow indefinitely by default. ldcron provides a command to generate +a [newsyslog(8)](x-man-page://newsyslog) configuration that automatically +rotates all ldcron log files. + +```bash +# Generate and install the newsyslog config (one-time setup, requires sudo) +ldcron log setup-rotation | sudo tee /etc/newsyslog.d/com.ldcron.conf +``` + +The generated configuration rotates each log file when it exceeds 1 MB, +keeps 3 compressed (gzip) archives, and requires no process signaling. +newsyslog runs automatically every hour via a system launchd job, so no +additional scheduling is needed. + --- ## File locations diff --git a/cmd/log.go b/cmd/log.go new file mode 100644 index 0000000..6664112 --- /dev/null +++ b/cmd/log.go @@ -0,0 +1,55 @@ +package cmd + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/spf13/cobra" +) + +var logCmd = &cobra.Command{ + Use: "log", + Short: "Manage job log files", + SilenceUsage: true, + RunE: func(cmd *cobra.Command, args []string) error { + return cmd.Help() + }, +} + +var logSetupRotationCmd = &cobra.Command{ + Use: "setup-rotation", + Short: "Print newsyslog configuration for log rotation", + Long: `Print a newsyslog(8) configuration snippet that rotates ldcron log files. + +The generated configuration uses a glob pattern to cover all ldcron log files +under ~/Library/Logs/ldcron/. newsyslog is a macOS system service that runs +every hour and rotates log files according to /etc/newsyslog.d/*.conf. + +To install: + ldcron log setup-rotation | sudo tee /etc/newsyslog.d/com.ldcron.conf + +The configuration rotates logs when they exceed 1 MB, keeps 3 compressed +archives, and requires no process signaling.`, + SilenceUsage: true, + RunE: runLogSetupRotation, +} + +func init() { + logCmd.AddCommand(logSetupRotationCmd) +} + +func runLogSetupRotation(cmd *cobra.Command, _ []string) error { + home, err := os.UserHomeDir() + if err != nil { + return fmt.Errorf("failed to get home directory: %w", err) + } + logPattern := filepath.Join(home, "Library", "Logs", "ldcron", "*.log") + + w := cmd.OutOrStdout() + _, _ = fmt.Fprintf(w, "# ldcron log rotation — managed by ldcron\n") + _, _ = fmt.Fprintf(w, "# See newsyslog.conf(5) for format details.\n") + _, _ = fmt.Fprintf(w, "# logfilename\t\t\t\t\t\tmode\tcount\tsize\twhen\tflags\n") + _, _ = fmt.Fprintf(w, "%s\t644\t3\t1024\t*\tGNB\n", logPattern) + return nil +} diff --git a/cmd/log_test.go b/cmd/log_test.go new file mode 100644 index 0000000..58a1101 --- /dev/null +++ b/cmd/log_test.go @@ -0,0 +1,37 @@ +package cmd + +import ( + "bytes" + "os" + "path/filepath" + "strings" + "testing" +) + +func TestLogSetupRotation(t *testing.T) { + home, err := os.UserHomeDir() + if err != nil { + t.Fatal(err) + } + wantPattern := filepath.Join(home, "Library", "Logs", "ldcron", "*.log") + + var buf bytes.Buffer + cmd := logSetupRotationCmd + cmd.SetOut(&buf) + + if err := cmd.RunE(cmd, nil); err != nil { + t.Fatalf("unexpected error: %v", err) + } + + output := buf.String() + + if !strings.Contains(output, wantPattern) { + t.Errorf("output should contain log pattern %q, got:\n%s", wantPattern, output) + } + if !strings.Contains(output, "GNB") { + t.Errorf("output should contain flags GNB, got:\n%s", output) + } + if !strings.Contains(output, "1024") { + t.Errorf("output should contain size 1024, got:\n%s", output) + } +} diff --git a/cmd/root.go b/cmd/root.go index 5054acb..c00764d 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -34,6 +34,7 @@ func init() { rootCmd.AddCommand(listCmd) rootCmd.AddCommand(removeCmd) rootCmd.AddCommand(runCmd) + rootCmd.AddCommand(logCmd) // Save the default subcommand help, then set a custom help for the root command. defaultHelp := rootCmd.HelpFunc() @@ -76,6 +77,7 @@ Commands: list List all registered jobs remove Delete a job by ID run Run a job immediately + log setup-rotation Print newsyslog config for log rotation Cron expression format (minute hour day month weekday): @@ -84,6 +86,10 @@ Cron expression format (minute hour day month weekday): "0 9 * * 1-5" Weekdays (Mon–Fri) at 9:00 "30 8 1 * *" 1st of every month at 8:30 +Log rotation: + + ldcron log setup-rotation | sudo tee /etc/newsyslog.d/com.ldcron.conf + Flags: -h, --help Show this help message From 607ec66004cdd891b6e98db8eb43ff306d9c5bb5 Mon Sep 17 00:00:00 2001 From: s4na Date: Sat, 7 Mar 2026 15:46:30 +0900 Subject: [PATCH 2/3] refactor: address review feedback for log rotation - Extract logDirPath() in paths.go to share path construction with logDir() and log.go, avoiding duplication - Add t.Cleanup to reset cobra command output in log_test.go to prevent test interference - Add inline comment explaining GNB newsyslog flags - Sync README.ja.md with README.md (add "no process signaling" note) Co-Authored-By: Claude Opus 4.6 --- README.ja.md | 2 +- cmd/log.go | 8 ++++---- cmd/log_test.go | 1 + cmd/paths.go | 11 +++++++++-- 4 files changed, 15 insertions(+), 7 deletions(-) diff --git a/README.ja.md b/README.ja.md index 7a235ff..9392533 100644 --- a/README.ja.md +++ b/README.ja.md @@ -248,7 +248,7 @@ tail -n 100 ~/Library/Logs/ldcron/a1b2c3d4.log ldcron log setup-rotation | sudo tee /etc/newsyslog.d/com.ldcron.conf ``` -生成される設定は、各ログファイルが1MBを超えた時点でローテーションし、gzip圧縮したアーカイブを3世代保持します。newsyslogはシステムのlaunchdジョブにより毎時自動実行されるため、追加のスケジュール設定は不要です。 +生成される設定は、各ログファイルが1MBを超えた時点でローテーションし、gzip圧縮したアーカイブを3世代保持します。プロセスへのシグナル送信は不要です(launchdはジョブ実行ごとにログファイルを開き直すため)。newsyslogはシステムのlaunchdジョブにより毎時自動実行されるため、追加のスケジュール設定は不要です。 --- diff --git a/cmd/log.go b/cmd/log.go index 6664112..1d51226 100644 --- a/cmd/log.go +++ b/cmd/log.go @@ -2,7 +2,6 @@ package cmd import ( "fmt" - "os" "path/filepath" "github.com/spf13/cobra" @@ -40,16 +39,17 @@ func init() { } func runLogSetupRotation(cmd *cobra.Command, _ []string) error { - home, err := os.UserHomeDir() + dir, err := logDirPath() if err != nil { - return fmt.Errorf("failed to get home directory: %w", err) + return err } - logPattern := filepath.Join(home, "Library", "Logs", "ldcron", "*.log") + logPattern := filepath.Join(dir, "*.log") w := cmd.OutOrStdout() _, _ = fmt.Fprintf(w, "# ldcron log rotation — managed by ldcron\n") _, _ = fmt.Fprintf(w, "# See newsyslog.conf(5) for format details.\n") _, _ = fmt.Fprintf(w, "# logfilename\t\t\t\t\t\tmode\tcount\tsize\twhen\tflags\n") + // Flags: G=glob pattern, N=no signal to any process, B=no rotation message in log _, _ = fmt.Fprintf(w, "%s\t644\t3\t1024\t*\tGNB\n", logPattern) return nil } diff --git a/cmd/log_test.go b/cmd/log_test.go index 58a1101..1edc799 100644 --- a/cmd/log_test.go +++ b/cmd/log_test.go @@ -18,6 +18,7 @@ func TestLogSetupRotation(t *testing.T) { var buf bytes.Buffer cmd := logSetupRotationCmd cmd.SetOut(&buf) + t.Cleanup(func() { cmd.SetOut(nil) }) if err := cmd.RunE(cmd, nil); err != nil { t.Fatalf("unexpected error: %v", err) diff --git a/cmd/paths.go b/cmd/paths.go index eccea6b..e7a2a74 100644 --- a/cmd/paths.go +++ b/cmd/paths.go @@ -14,12 +14,19 @@ func launchAgentsDir() (string, error) { return filepath.Join(home, "Library", "LaunchAgents"), nil } -func logDir() (string, error) { +func logDirPath() (string, error) { home, err := os.UserHomeDir() if err != nil { return "", fmt.Errorf("failed to get home directory: %w", err) } - dir := filepath.Join(home, "Library", "Logs", "ldcron") + return filepath.Join(home, "Library", "Logs", "ldcron"), nil +} + +func logDir() (string, error) { + dir, err := logDirPath() + if err != nil { + return "", err + } if err := os.MkdirAll(dir, 0o755); err != nil { return "", fmt.Errorf("failed to create log directory: %w", err) } From 0f9538b6e67236dafcad82015e51d99ccb21c7e8 Mon Sep 17 00:00:00 2001 From: s4na Date: Sat, 7 Mar 2026 15:52:09 +0900 Subject: [PATCH 3/3] refactor: address second review feedback - Improve B flag comment to clarify rotation-marker suppression - Add mode 644 assertion to log_test.go - Remove x-man-page:// URLs from READMEs (not functional on GitHub) - Sync README.md and README.ja.md: add launchd reopen explanation to English version to match Japanese version Co-Authored-By: Claude Opus 4.6 --- README.ja.md | 2 +- README.md | 10 +++++----- cmd/log.go | 3 ++- cmd/log_test.go | 3 +++ 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/README.ja.md b/README.ja.md index 9392533..15c53e4 100644 --- a/README.ja.md +++ b/README.ja.md @@ -241,7 +241,7 @@ tail -n 100 ~/Library/Logs/ldcron/a1b2c3d4.log ### ログローテーション -ログファイルはデフォルトでは無限に増え続けます。ldcronは[newsyslog(8)](x-man-page://newsyslog)の設定を生成するコマンドを提供しており、すべてのldcronログファイルを自動的にローテーションできます。 +ログファイルはデフォルトでは無限に増え続けます。ldcronはnewsyslog(8)の設定を生成するコマンドを提供しており、すべてのldcronログファイルを自動的にローテーションできます。 ```bash # newsyslog設定を生成・インストール(初回のみ、sudo必要) diff --git a/README.md b/README.md index bf08b95..f742bf6 100644 --- a/README.md +++ b/README.md @@ -242,8 +242,7 @@ tail -n 100 ~/Library/Logs/ldcron/a1b2c3d4.log ### Log rotation Log files grow indefinitely by default. ldcron provides a command to generate -a [newsyslog(8)](x-man-page://newsyslog) configuration that automatically -rotates all ldcron log files. +a newsyslog(8) configuration that automatically rotates all ldcron log files. ```bash # Generate and install the newsyslog config (one-time setup, requires sudo) @@ -251,9 +250,10 @@ ldcron log setup-rotation | sudo tee /etc/newsyslog.d/com.ldcron.conf ``` The generated configuration rotates each log file when it exceeds 1 MB, -keeps 3 compressed (gzip) archives, and requires no process signaling. -newsyslog runs automatically every hour via a system launchd job, so no -additional scheduling is needed. +keeps 3 compressed (gzip) archives, and requires no process signaling +(launchd reopens log files on each job execution). newsyslog runs +automatically every hour via a system launchd job, so no additional +scheduling is needed. --- diff --git a/cmd/log.go b/cmd/log.go index 1d51226..6f6fd51 100644 --- a/cmd/log.go +++ b/cmd/log.go @@ -49,7 +49,8 @@ func runLogSetupRotation(cmd *cobra.Command, _ []string) error { _, _ = fmt.Fprintf(w, "# ldcron log rotation — managed by ldcron\n") _, _ = fmt.Fprintf(w, "# See newsyslog.conf(5) for format details.\n") _, _ = fmt.Fprintf(w, "# logfilename\t\t\t\t\t\tmode\tcount\tsize\twhen\tflags\n") - // Flags: G=glob pattern, N=no signal to any process, B=no rotation message in log + // Flags: G=glob pattern, N=no signal to any process, + // B=don't insert a rotation-marker line into the new log file _, _ = fmt.Fprintf(w, "%s\t644\t3\t1024\t*\tGNB\n", logPattern) return nil } diff --git a/cmd/log_test.go b/cmd/log_test.go index 1edc799..38eb000 100644 --- a/cmd/log_test.go +++ b/cmd/log_test.go @@ -35,4 +35,7 @@ func TestLogSetupRotation(t *testing.T) { if !strings.Contains(output, "1024") { t.Errorf("output should contain size 1024, got:\n%s", output) } + if !strings.Contains(output, "644") { + t.Errorf("output should contain mode 644, got:\n%s", output) + } }