Add Logrotate Support for FM and Sites#399
Conversation
Signed-off-by: Immanuel Raj <iamimmanuelraj@gmail.com>
…isor and system-level log rotation Signed-off-by: Immanuel Raj <iamimmanuelraj@gmail.com>
There was a problem hiding this comment.
Pull request overview
Adds system-level log rotation support to Frappe Manager by configuring Supervisor-managed log limits, introducing a logrotate config generator/installer command, and shipping a migration to apply these changes on upgrade.
Changes:
- Add Supervisor stdout/stderr rotation limits (max bytes + backups) across Frappe services and supervisord itself.
- Introduce a
logrotate.tmplplusfm self logrotateto generate (and optionally install) a logrotate config. - Add a v0.20.0 migration to back up/regenerate Supervisor configs and write the logrotate config for existing installations.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| frappe_manager/templates/supervisor.conf.tmpl | Adds Supervisor log size/backups limits for per-program stdout/stderr logs. |
| frappe_manager/templates/logrotate.tmpl | New logrotate configuration template covering bench/nginx/mariadb/FM CLI logs. |
| frappe_manager/migration_manager/migrations/migrate_0_20_0.py | New v0.20.0 migration to back up and regenerate Supervisor configs and write logrotate.conf. |
| frappe_manager/migration_manager/migration_constants.py | Adds self logrotate to migration whitelist and defines DOCKER_FREE_COMMANDS. |
| frappe_manager/commands/self/logrotate.py | New CLI command to generate/install logrotate config from the template. |
| frappe_manager/commands/self/init.py | Registers the new self logrotate command. |
| frappe_manager/commands/init.py | Adds docker-free command detection to skip Docker daemon checks / first-install image pull. |
| frappe_manager/about.py | Bumps version to 0.20.0.dev0. |
| Docker/frappe/supervisord.conf | Adds supervisord logfile rotation limits. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| current_version = Version(get_current_fm_version()) | ||
| fm_config_manager.version = current_version | ||
| fm_config_manager.export_to_toml() |
There was a problem hiding this comment.
This introduces new behavior that affects command startup (skipping Docker checks based on DOCKER_FREE_COMMANDS), but there doesn’t appear to be test coverage asserting that a docker-free command like self logrotate can run without Docker and without initializing global services. Adding a unit test around app_callback / command dispatch (similar to existing CLI flag tests) would help prevent regressions.
| fm_config_manager.export_to_toml() | |
| fm_config_manager.export_to_toml() | |
| else: | |
| # Docker-free commands must also skip the remaining startup initialization | |
| # (migration/config/bootstrap checks) so they can run without Docker and | |
| # without touching global services. | |
| return |
| is_docker_free = full_command in DOCKER_FREE_COMMANDS | ||
|
|
||
| if not is_docker_free: | ||
| if not DockerClient().server_running(): | ||
| output.exit("Docker daemon not running. Please start docker service") | ||
|
|
||
| if not CLI_FM_CONFIG_PATH.exists(): |
There was a problem hiding this comment.
DOCKER_FREE_COMMANDS currently only skips the initial Docker daemon check and first-install image pull, but ServicesManager.init() / entrypoint_checks() still run later in app_callback for all commands. That will still instantiate DockerClient and attempt to create/pull/start global services, so fm self logrotate will still fail when Docker is unavailable. Consider short-circuiting the ServicesManager initialization/entrypoint checks when is_docker_free is true (and only populate ctx.obj with what the command actually needs).
| output.print(f":white_check_mark: Logrotate config written to [blue]{dest}[/blue]") | ||
|
|
||
| if install: | ||
| system_dest = Path("/etc/logrotate.d/frappe-manager") | ||
| try: | ||
| result = subprocess.run( | ||
| ["sudo", "cp", str(dest), str(system_dest)], | ||
| check=True, | ||
| capture_output=True, | ||
| text=True, | ||
| ) | ||
| output.print( | ||
| f":white_check_mark: Installed to [blue]{system_dest}[/blue]. " | ||
| "Logs will rotate daily." | ||
| ) |
There was a problem hiding this comment.
The success messages embed :white_check_mark: inside the message string instead of using the emoji_code parameter that the OutputHandler API expects. This will likely render the literal text :white_check_mark: in Rich output and JSON logs rather than an emoji code. Pass the emoji via emoji_code=... and keep the message text free of emoji codes (consistent with other commands).
| current_system = platform.system() | ||
| if install and current_system == "Darwin": | ||
| output.error( | ||
| "System-wide logrotate installation is not supported on macOS. " | ||
| "Use newsyslog or install logrotate via Homebrew and manage manually." | ||
| ) | ||
| raise typer.Exit(1) | ||
|
|
There was a problem hiding this comment.
--install is documented as Linux-only, but the current platform guard only blocks macOS (Darwin). On Windows/other platforms this will still try to run sudo cp ... /etc/logrotate.d/... and then print macOS-specific instructions in the non-install path. Please explicitly require platform.system() == 'Linux' for --install, and tailor the non-Linux guidance/error message per platform (or exit with an unsupported-platform error).
| compress | ||
| delaycompress | ||
| missingok | ||
| notifempty | ||
| copytruncate | ||
| su {{ user }} {{ user }} | ||
| } | ||
|
|
||
| # ── Per-site nginx logs ───────────────────────────────────────────────────────── | ||
| {{ frappe_dir }}/sites/*/configs/nginx/logs/*.log { | ||
| daily | ||
| rotate 14 | ||
| compress | ||
| delaycompress | ||
| missingok | ||
| notifempty | ||
| copytruncate | ||
| su {{ user }} {{ user }} | ||
| } | ||
|
|
||
| # ── Global nginx-proxy logs ───────────────────────────────────────────────────── | ||
| {{ frappe_dir }}/services/nginx-proxy/logs/*.log { | ||
| daily | ||
| rotate 14 | ||
| compress | ||
| delaycompress | ||
| missingok | ||
| notifempty | ||
| copytruncate | ||
| su {{ user }} {{ user }} | ||
| } | ||
|
|
||
| # ── Global MariaDB logs ───────────────────────────────────────────────────────── | ||
| {{ frappe_dir }}/services/mariadb/logs/*.log { | ||
| daily | ||
| rotate 14 | ||
| compress | ||
| delaycompress | ||
| missingok | ||
| notifempty | ||
| copytruncate | ||
| su {{ user }} {{ user }} | ||
| } | ||
|
|
||
| # ── FM CLI logs ───────────────────────────────────────────────────────────────── | ||
| # Note: fm.log is also rotated automatically by Python's RotatingFileHandler | ||
| # (10 MB per file, 3 backups). This entry provides an additional daily sweep. | ||
| {{ frappe_dir }}/logs/fm.log { | ||
| daily | ||
| rotate 14 | ||
| compress | ||
| delaycompress | ||
| missingok | ||
| notifempty | ||
| copytruncate | ||
| su {{ user }} {{ user }} | ||
| } |
There was a problem hiding this comment.
This logrotate config rotates the active *.log files daily without dateext, but many of these logs are already rotated by other mechanisms (Supervisor *_logfile_maxbytes produces .log.1/.log.2..., and fm.log is rotated by Python RotatingFileHandler using the same suffix scheme). Running logrotate on the same base filenames risks collisions/overwrites and log loss. Consider adding dateext (and an explicit dateformat) or excluding logs that are already size-rotated, especially {{ frappe_dir }}/logs/fm.log.
Also, su {{ user }} {{ user }} assumes the primary group name equals the username, which isn’t always true; it’s safer to render the user’s primary group explicitly or omit su when rotating files under a user-owned directory.
| compress | |
| delaycompress | |
| missingok | |
| notifempty | |
| copytruncate | |
| su {{ user }} {{ user }} | |
| } | |
| # ── Per-site nginx logs ───────────────────────────────────────────────────────── | |
| {{ frappe_dir }}/sites/*/configs/nginx/logs/*.log { | |
| daily | |
| rotate 14 | |
| compress | |
| delaycompress | |
| missingok | |
| notifempty | |
| copytruncate | |
| su {{ user }} {{ user }} | |
| } | |
| # ── Global nginx-proxy logs ───────────────────────────────────────────────────── | |
| {{ frappe_dir }}/services/nginx-proxy/logs/*.log { | |
| daily | |
| rotate 14 | |
| compress | |
| delaycompress | |
| missingok | |
| notifempty | |
| copytruncate | |
| su {{ user }} {{ user }} | |
| } | |
| # ── Global MariaDB logs ───────────────────────────────────────────────────────── | |
| {{ frappe_dir }}/services/mariadb/logs/*.log { | |
| daily | |
| rotate 14 | |
| compress | |
| delaycompress | |
| missingok | |
| notifempty | |
| copytruncate | |
| su {{ user }} {{ user }} | |
| } | |
| # ── FM CLI logs ───────────────────────────────────────────────────────────────── | |
| # Note: fm.log is also rotated automatically by Python's RotatingFileHandler | |
| # (10 MB per file, 3 backups). This entry provides an additional daily sweep. | |
| {{ frappe_dir }}/logs/fm.log { | |
| daily | |
| rotate 14 | |
| compress | |
| delaycompress | |
| missingok | |
| notifempty | |
| copytruncate | |
| su {{ user }} {{ user }} | |
| } | |
| dateext | |
| dateformat -%Y%m%d | |
| compress | |
| delaycompress | |
| missingok | |
| notifempty | |
| copytruncate | |
| } | |
| # ── Per-site nginx logs ───────────────────────────────────────────────────────── | |
| {{ frappe_dir }}/sites/*/configs/nginx/logs/*.log { | |
| daily | |
| rotate 14 | |
| dateext | |
| dateformat -%Y%m%d | |
| compress | |
| delaycompress | |
| missingok | |
| notifempty | |
| copytruncate | |
| } | |
| # ── Global nginx-proxy logs ───────────────────────────────────────────────────── | |
| {{ frappe_dir }}/services/nginx-proxy/logs/*.log { | |
| daily | |
| rotate 14 | |
| dateext | |
| dateformat -%Y%m%d | |
| compress | |
| delaycompress | |
| missingok | |
| notifempty | |
| copytruncate | |
| } | |
| # ── Global MariaDB logs ───────────────────────────────────────────────────────── | |
| {{ frappe_dir }}/services/mariadb/logs/*.log { | |
| daily | |
| rotate 14 | |
| dateext | |
| dateformat -%Y%m%d | |
| compress | |
| delaycompress | |
| missingok | |
| notifempty | |
| copytruncate | |
| } | |
| # ── FM CLI logs ───────────────────────────────────────────────────────────────── | |
| # Note: fm.log is rotated automatically by Python's RotatingFileHandler | |
| # (10 MB per file, 3 backups), so it is intentionally excluded here to avoid | |
| # suffix collisions and accidental overwrite of rotated files. |
| import multiprocessing | ||
| import os | ||
| import platform | ||
| from pathlib import Path |
There was a problem hiding this comment.
Path is imported but not used in this migration module. If you want to keep lint clean and avoid dead imports, please remove the unused import (or start using it explicitly).
| from pathlib import Path |
This pull request introduces system-level log rotation for Frappe Manager by updating supervisor log file management and adding a new logrotate configuration and command. It also includes a migration to apply these changes to existing installations. The most important changes are grouped below.
Log rotation improvements:
Added
stdout_logfile_maxbytesandstdout_logfile_backups(10MB, 5 backups) to all relevant[program:…]sections insupervisor.confand its template, ensuring supervisor-managed logs are rotated and limited in size. [1] [2] [3] [4] [5] [6] [7] [8]Added a new Jinja2 template
logrotate.tmpland logic to generate a comprehensivelogrotate.confcovering all major Frappe Manager logs (bench, nginx, MariaDB, CLI), supporting daily rotation, compression, and safe log handling.CLI and command enhancements:
Introduced a new
fm self logrotatecommand to generate and optionally install the logrotate config system-wide, with platform-specific instructions and error handling for unsupported operations. [1] [2] [3]Marked
self logrotateas a "docker-free" command, allowing it to run even if Docker is not running, and updated the CLI logic to support this. [1] [2] [3]Migration for existing installations:
migrate_0_20_0.py) to back up old supervisor configs, regenerate them with log rotation settings, and generate the new logrotate configuration for all users upgrading to v0.20.0.Version bump:
0.20.0.dev0to reflect these changes.