diff --git a/README.ja.md b/README.ja.md index e9e6d07..15c53e4 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)の設定を生成するコマンドを提供しており、すべてのldcronログファイルを自動的にローテーションできます。 + +```bash +# newsyslog設定を生成・インストール(初回のみ、sudo必要) +ldcron log setup-rotation | sudo tee /etc/newsyslog.d/com.ldcron.conf +``` + +生成される設定は、各ログファイルが1MBを超えた時点でローテーションし、gzip圧縮したアーカイブを3世代保持します。プロセスへのシグナル送信は不要です(launchdはジョブ実行ごとにログファイルを開き直すため)。newsyslogはシステムのlaunchdジョブにより毎時自動実行されるため、追加のスケジュール設定は不要です。 + --- ## ファイル配置 diff --git a/README.md b/README.md index af4c598..f742bf6 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) 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 +(launchd reopens log files on each job execution). 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..6f6fd51 --- /dev/null +++ b/cmd/log.go @@ -0,0 +1,56 @@ +package cmd + +import ( + "fmt" + "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 { + dir, err := logDirPath() + if err != nil { + return err + } + 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=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 new file mode 100644 index 0000000..38eb000 --- /dev/null +++ b/cmd/log_test.go @@ -0,0 +1,41 @@ +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) + t.Cleanup(func() { cmd.SetOut(nil) }) + + 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) + } + if !strings.Contains(output, "644") { + t.Errorf("output should contain mode 644, got:\n%s", output) + } +} 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) } 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