Skip to content

feat: implement log rotation with safe stream swapping and backup management #47

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 13 commits into
base: main
Choose a base branch
from

Conversation

Nuovaxu
Copy link

@Nuovaxu Nuovaxu commented Mar 20, 2025

Description

RotatingCLPFileHandler introduces a size-based log rotation mechanism to prevent excessive file growth while ensuring compatibility with Python’s logging framework. It enforces a 5MB file size limit to reduce I/O overhead and maintain fast access to logs. Before rotation, the system flushes buffered data and writes an EOF marker to ensure log integrity.

Logs follow a timestamp-based naming convention for better traceability. When a file exceeds max_bytes, rotation is triggered, creating a new file while retaining only the latest backup_count logs. Old logs are systematically removed, and a locking mechanism ensures thread safety and uninterrupted operation.

Design for RotatingCLPFileHandler

The RotatingCLPFileHandler extends CLPFileHandler, adding log rotation based on file size. It integrates file rotation and compression mechanisms for efficient log storage and management.

1.1 Class Diagram:

Attributes:

  • filename_prefix: str → Log file prefix.
  • log_dir: Path → Directory for log files.
  • max_bytes: int → Max file size before rotation.
  • backup_count: int → Number of retained backup logs.
  • timestamp_format: str → Format for timestamp in filenames.
  • current_log_file: Path → Path to the active log file.

1.2 Flow Chart:

image

Core Methods

_generate_log_filename()

Creates a new log file name using the current timestamp and the filename_prefix.

  • Ensures unique file naming for each rotation.

_should_rotate()

Checks if the current log file size exceeds the specified max_bytes.

  • Returns True if rotation is needed, otherwise False.

_rotate()

Handles the full rotation process:

  1. Flushes and closes the current ostream.
  2. Writes an EOF marker to signify the end of the compressed stream.
  3. Creates a new log file and reinitializes the handler with the new file.
  4. Removes old log backups exceeding the configured backup_count.

_remove_old_backups()

Manages old log files:

  • Sorts existing log files by modification time.
  • Deletes files exceeding the specified backup_count.

_write(loglevel, msg)

Overrides the _write method of CLPFileHandler to include rotation logic.

  • Before writing a log entry, checks if rotation is needed using _should_rotate().

_get_persistent_machine_uuid()

Retrieves a persistent machine-specific UUID from an environment variable or a system-wide file.
If missing, generates a new UUID (first 8 characters) and saves it for future use.

_open_new_log_file()

Creates and switches to a new log file.

  • Generates a unique filename.
  • Opens it in append mode.
  • Updates self.current_log_file.
  • Reinitializes the handler.

Summary by CodeRabbit

  • New Features

    • Introduced file-based log rotation to automatically manage log size and backup retention.
    • Added a new logging handler with advanced configuration options.
  • Refactor

    • Updated the logging configuration to remove console logging.
    • Enhanced logger initialization to ensure reliable logging across different configuration scenarios, including automatic creation of the logs directory.

Copy link

coderabbitai bot commented Mar 20, 2025

Walkthrough

The changes update the application's logging framework. The console handler has been removed from the configuration and replaced by a file-based handler, CLP_file, which uses the new RotatingCLPFileHandler class for file size-based rotation. In addition, the logger initialization in the application now ensures that a logs directory exists and that the global logger is always set, regardless of configuration loading success.

Changes

File Summary
log_config.yaml Removed the console handler (using logging.StreamHandler), added CLP_file with RotatingCLPFileHandler, and updated the root logger's handlers list.
application/__init__.py Declared logger as global; added creation of the logs directory; ensured logger initialization in both the try and except blocks.
application/log_handlers/handlers.py Introduced the RotatingCLPFileHandler class extending CLPFileHandler, implementing log rotation based on file size, with methods for rotation and backup.

Sequence Diagram(s)

sequenceDiagram
    participant L as Logger
    participant R as RotatingCLPFileHandler
    participant FS as File System

    L ->> R: Log a message
    R ->> R: Check _should_rotate? (file size)
    alt Rotation needed
        R ->> FS: Flush current stream and write EOF
        R ->> FS: Close current log file
        R ->> R: Rotate log (_rotate)
        R ->> FS: Create new log file (with timestamp)
        R ->> FS: Remove old backups if necessary
    end
    R ->> FS: Write log message
Loading
sequenceDiagram
    participant App as Application
    participant OS as Operating System
    participant Log as Logger

    App ->> OS: os.makedirs("logs", exist_ok=True)
    alt Configuration loads successfully
        App ->> Log: Initialize global logger with config
    else Configuration load fails
        App ->> Log: Initialize global logger with basicConfig
        Log ->> App: Log error and warning messages
    end
Loading

Poem

I'm a happy rabbit, hopping with delight,
Logs now rotate and keep everything light.
No more console chatter in the night,
Files sing their debug tunes just right.
With whiskers twitching toward a brighter code sight,
I hop along, my path ever bright! 🐇💻

✨ Finishing Touches
  • 📝 Generate Docstrings

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

📜 Review details

Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 462ac75 and 60f0d7d.

📒 Files selected for processing (2)
  • application/logger.py (1 hunks)
  • log_config.yaml (1 hunks)
🧰 Additional context used
🪛 Ruff (0.8.2)
application/logger.py

19-19: Boolean-typed positional argument in function definition

(FBT001)


19-19: Boolean default positional argument in function definition

(FBT002)


47-47: datetime.datetime.now() called without a tz argument

(DTZ005)


90-90: Use a context manager for opening files

(SIM115)


91-91: Do not catch blind exception: Exception

(BLE001)


92-92: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


92-92: Avoid specifying long messages outside the exception class

(TRY003)


119-119: Do not catch blind exception: Exception

(BLE001)

🔇 Additional comments (2)
log_config.yaml (2)

10-16: Confirm overwriting behavior for the file handler.

Currently, the handler is configured with mode: w, causing the log file to be truncated each time the application restarts. If you want to preserve logs across runs, consider mode: a. Otherwise, this configuration is valid for clean-slate logging per run.


18-27: Configuration of RotatingCLPFileHandler appears consistent with your objectives.

The new handler parameters match the file rotation requirements (5MB max, 20 backups). This setup should help prevent large log files and manage retention effectively.

max_bytes: int,
backup_count: int = 5,
mode: str = "ab",
enable_compression: bool = True,
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

Consider replacing the boolean argument with a more descriptive parameter.

Some style guides discourage using boolean-typed parameters because they make function calls less self-explanatory. For instance, you could replace enable_compression: bool = True with an enum or separate methods to increase clarity. However, this is a minor style preference and not strictly required.

🧰 Tools
🪛 Ruff (0.8.2)

19-19: Boolean-typed positional argument in function definition

(FBT001)


19-19: Boolean default positional argument in function definition

(FBT002)

Comment on lines 119 to 120
except Exception as e:
print(f"Error deleting old log file {old_file}: {e}")
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

Catch more targeted exceptions when removing old log files.

Catching Exception is overly broad and may inadvertently hide important errors. Consider using OSError (or a more specific exception) to handle file-related errors.

- except Exception as e:
+ except OSError as e:
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
except Exception as e:
print(f"Error deleting old log file {old_file}: {e}")
except OSError as e:
print(f"Error deleting old log file {old_file}: {e}")
🧰 Tools
🪛 Ruff (0.8.2)

119-119: Do not catch blind exception: Exception

(BLE001)

Comment on lines 90 to 93
new_stream = open(new_log_file, "ab")
except Exception as e:
raise RuntimeError(f"Failed to open new log file {new_log_file}: {e}")

Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

Use more specific exception handling and consider raising exceptions with context.

Catching a broad Exception can make debugging more difficult and may mask unexpected issues. Also, raising with from e preserves the original traceback context. Using a context manager (e.g. with open(...) as f) isn’t straightforward here because you need to keep the stream open after initialization, but you can still refine your exception handling to be more explicit:

 try:
-    new_stream = open(new_log_file, "ab")
-except Exception as e:
-    raise RuntimeError(f"Failed to open new log file {new_log_file}: {e}")
+    new_stream = open(new_log_file, "ab")
+except OSError as err:
+    raise RuntimeError(f"Failed to open new log file {new_log_file}") from err
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
new_stream = open(new_log_file, "ab")
except Exception as e:
raise RuntimeError(f"Failed to open new log file {new_log_file}: {e}")
new_stream = open(new_log_file, "ab")
except OSError as err:
raise RuntimeError(f"Failed to open new log file {new_log_file}") from err
🧰 Tools
🪛 Ruff (0.8.2)

90-90: Use a context manager for opening files

(SIM115)


91-91: Do not catch blind exception: Exception

(BLE001)


92-92: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


92-92: Avoid specifying long messages outside the exception class

(TRY003)

"""
Generate a log filename with the timestamp and prefix.
"""
start_time = datetime.now()
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

Consider specifying a timezone for consistent log timestamps.

Calling datetime.now() without a timezone defaults to local time, which may result in ambiguity if your system’s timezone changes. If you prefer UTC-based logs or consistent timestamps across systems, specify datetime.now(timezone.utc).

- start_time = datetime.now()
+ from datetime import timezone
+ start_time = datetime.now(timezone.utc)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
start_time = datetime.now()
from datetime import timezone
start_time = datetime.now(timezone.utc)
🧰 Tools
🪛 Ruff (0.8.2)

47-47: datetime.datetime.now() called without a tz argument

(DTZ005)

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

📜 Review details

Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a6dd459 and 02a07f3.

📒 Files selected for processing (3)
  • application/__init__.py (1 hunks)
  • application/log_handlers/handlers.py (1 hunks)
  • log_config.yaml (1 hunks)
🧰 Additional context used
🪛 Ruff (0.8.2)
application/log_handlers/handlers.py

20-20: Boolean-typed positional argument in function definition

(FBT001)


20-20: Boolean default positional argument in function definition

(FBT002)


48-48: datetime.datetime.now() called without a tz argument

(DTZ005)


90-90: Use a context manager for opening files

(SIM115)


91-91: Do not catch blind exception: Exception

(BLE001)


92-92: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


92-92: Avoid specifying long messages outside the exception class

(TRY003)


119-119: Do not catch blind exception: Exception

(BLE001)

🔇 Additional comments (6)
application/__init__.py (3)

30-30: Directory creation approach looks good.
No issues with creating the logs directory if it doesn't exist.


36-36: Initialization logic is clear and consistent.
No issues; the logger is properly retrieved as logging.getLogger(__name__) after reading config.


40-40: Fallback initialization matches the main path.
No issues; re-initializing the logger under the fallback scenario is properly handled.

log_config.yaml (1)

10-23: Configuration is coherent.
Your new CLP_file handler definition aligns well with the RotatingCLPFileHandler in the code. The root logger’s handlers are updated accordingly, and the parameter choices (max file size, formatting, compression) appear consistent with your objectives.

application/log_handlers/handlers.py (2)

90-90: Context manager usage may be unnecessary for persistent streams.
Static analysis suggests a context manager here, but since the handler requires a long-lived stream, manually opening and closing might be acceptable. Confirm that the file is properly closed in all execution paths to minimize resource leakage.

🧰 Tools
🪛 Ruff (0.8.2)

90-90: Use a context manager for opening files

(SIM115)


122-128: Rotation logic integration looks solid.
Your _write method properly checks _should_rotate() and triggers _rotate() before writing logs. This ensures the file remains within the specified max size limit.

Comment on lines 104 to 121
def _remove_old_backups(self) -> None:
"""
Remove old log files exceeding the backup count.
"""
if not self.backup_count:
return

log_files = sorted(
self.log_dir.glob(f"{self.filename_prefix}_*.clp.zst"),
key=os.path.getmtime,
)
if len(log_files) > self.backup_count:
for old_file in log_files[:-self.backup_count]:
try:
old_file.unlink()
except Exception as e:
print(f"Error deleting old log file {old_file}: {e}")

Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Restrict broad exception handling or re-raise appropriately.
Catching Exception in _remove_old_backups can hinder troubleshooting for issues like PermissionError. Consider narrowing the exception type or re-raising from the original error.

🧰 Tools
🪛 Ruff (0.8.2)

119-119: Do not catch blind exception: Exception

(BLE001)

Comment on lines 44 to 53
def _generate_log_filename(self) -> Path:
"""
Generate a log filename with the timestamp and prefix.
"""
start_time = datetime.now()
timestamp = start_time.strftime(self.timestamp_format)
self.current_log_file = Path(f"{self.log_dir}/{self.filename_prefix}_{timestamp}.clp.zst")

return self.current_log_file

Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

Use a timezone-aware or UTC time for consistency.
Using datetime.now() without specifying a timezone can lead to ambiguous log timestamps if servers run in different timezones. Consider using datetime.now(tz=...) or datetime.utcnow() where appropriate.

🧰 Tools
🪛 Ruff (0.8.2)

48-48: datetime.datetime.now() called without a tz argument

(DTZ005)

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (4)
application/log_handlers/handlers.py (4)

14-24: 🧹 Nitpick (assertive)

Consider refactoring boolean default arguments to keyword-only.

Static analysis flags the boolean default argument enable_compression=True. Converting it to a keyword-only argument clarifies its optional nature and avoids confusion in call signatures.

 def __init__(
     self,
     filename_prefix: str,
     log_dir: Path,
     max_bytes: int,
     backup_count: int = 20,
     mode: str = "ab",
-    enable_compression: bool = True,
+    *,
+    enable_compression: bool = True,
     timestamp_format: str = "%Y-%m-%d_%H-%M-%S",
     loglevel_timeout: Optional[CLPLogLevelTimeout] = None,
 ) -> None:
🧰 Tools
🪛 Ruff (0.8.2)

21-21: Boolean-typed positional argument in function definition

(FBT001)


21-21: Boolean default positional argument in function definition

(FBT002)


92-92: 🧹 Nitpick (assertive)

Use a timezone-aware or UTC time for consistency.

Using datetime.now() without specifying a timezone can lead to ambiguous log timestamps if servers run in different timezones. Consider using datetime.now(tz=...) or datetime.utcnow() where appropriate.

-    timestamp = datetime.now().strftime(self.timestamp_format)
+    # Use UTC for consistent timestamps regardless of server timezone
+    timestamp = datetime.now(datetime.UTC).strftime(self.timestamp_format)
🧰 Tools
🪛 Ruff (0.8.2)

92-92: datetime.datetime.now() called without a tz argument

(DTZ005)


140-144: 🛠️ Refactor suggestion

Use context manager and improve exception handling.

The current implementation has several issues that could be improved:

  1. Not using a context manager for file operations
  2. Catching a blind Exception which can mask specific errors
  3. Not preserving the exception traceback with from e
-        # Initialize the new stream
-        try:
-            new_stream = open(new_log_file, "ab")
-        except Exception as e:
-            raise RuntimeError(f"Failed to open new log file {new_log_file}: {e}")
+        # Initialize the new stream
+        try:
+            new_stream = open(new_log_file, "ab")
+        except (IOError, PermissionError, OSError) as e:
+            raise RuntimeError(f"Failed to open new log file {new_log_file}") from e

Alternatively, use a context manager approach for future implementation:

try:
    with open(new_log_file, "ab") as new_stream:
        # Store a reference to the file object
        self.stream = new_stream
        # Initialize with the stream
        self.init(new_stream)
        # Prevent the context manager from closing the file
        new_stream.detach()
except (IOError, PermissionError, OSError) as e:
    raise RuntimeError(f"Failed to open new log file {new_log_file}") from e
🧰 Tools
🪛 Ruff (0.8.2)

141-141: Use a context manager for opening files

(SIM115)


142-142: Do not catch blind exception: Exception

(BLE001)


143-143: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


143-143: Avoid specifying long messages outside the exception class

(TRY003)


172-173: 🛠️ Refactor suggestion

Restrict broad exception handling or re-raise appropriately.

Catching Exception in _remove_old_backups can hinder troubleshooting for issues like PermissionError. Consider narrowing the exception type or re-raising from the original error.

                 try:
                     old_file.unlink()
-                except Exception as e:
+                except (IOError, PermissionError, OSError) as e:
                     print(f"Error deleting old log file {old_file}: {e}")
🧰 Tools
🪛 Ruff (0.8.2)

172-172: Do not catch blind exception: Exception

(BLE001)

📜 Review details

Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 02a07f3 and 6342c69.

📒 Files selected for processing (2)
  • application/log_handlers/handlers.py (1 hunks)
  • log_config.yaml (1 hunks)
🧰 Additional context used
🪛 Ruff (0.8.2)
application/log_handlers/handlers.py

21-21: Boolean-typed positional argument in function definition

(FBT001)


21-21: Boolean default positional argument in function definition

(FBT002)


69-69: Unnecessary open mode parameters

Remove open mode parameters

(UP015)


92-92: datetime.datetime.now() called without a tz argument

(DTZ005)


141-141: Use a context manager for opening files

(SIM115)


142-142: Do not catch blind exception: Exception

(BLE001)


143-143: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


143-143: Avoid specifying long messages outside the exception class

(TRY003)


172-172: Do not catch blind exception: Exception

(BLE001)

🔇 Additional comments (4)
log_config.yaml (1)

10-19: LGTM! Clean configuration for the new rotating log handler.

The configuration for CLP_file is well-structured with appropriate parameters for log rotation:

  • Max file size of 5MB provides a good balance for log management
  • 20 backup files is a reasonable number for history retention
  • Compression is enabled to optimize storage usage
  • Timestamp format is clear and sortable
application/log_handlers/handlers.py (3)

105-113: LGTM! Clean and error-resistant rotation check.

The rotation check logic is straightforward and properly handles the case where the file doesn't exist.


175-181: LGTM! Clean implementation of write method with rotation check.

The write method override is concise and correctly checks if rotation is needed before writing to the log file.


9-182: Great implementation of the rotating log handler.

Overall, this is a well-designed implementation of a rotating log handler with:

  • Clear separation of concerns (checking, rotating, cleanup)
  • Thread-safety during rotation with proper lock handling
  • Robust UUID generation for machine identification
  • Appropriate error handling in most cases

The minor issues flagged above should be addressed, but the core functionality is solid.

🧰 Tools
🪛 Ruff (0.8.2)

21-21: Boolean-typed positional argument in function definition

(FBT001)


21-21: Boolean default positional argument in function definition

(FBT002)


69-69: Unnecessary open mode parameters

Remove open mode parameters

(UP015)


92-92: datetime.datetime.now() called without a tz argument

(DTZ005)


141-141: Use a context manager for opening files

(SIM115)


142-142: Do not catch blind exception: Exception

(BLE001)


143-143: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


143-143: Avoid specifying long messages outside the exception class

(TRY003)


172-172: Do not catch blind exception: Exception

(BLE001)


root:
level: DEBUG
handlers: [console]
handlers: [CLP_file]
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

Consider documenting the removal of console logging.

The change from console to file-based logging improves log persistence, but users accustomed to seeing logs in the terminal may be surprised. Consider documenting this change in user-facing documentation.

…dle the case that someone manually deletes log files
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (2)
application/log_handlers/handlers.py (2)

92-92: 🧹 Nitpick (assertive)

Consider using timezone-aware timestamps for logs.

Using datetime.now() without specifying a timezone can lead to ambiguous timestamps if servers run in different timezones.

- timestamp = datetime.now().strftime(self.timestamp_format)
+ timestamp = datetime.now(tz=datetime.timezone.utc).strftime(self.timestamp_format)
🧰 Tools
🪛 Ruff (0.8.2)

92-92: datetime.datetime.now() called without a tz argument

(DTZ005)


179-179: 🛠️ Refactor suggestion

Avoid catching overly broad Exception.

Catching a blind Exception can mask issues like PermissionError. Use narrow exception types or re-raise with the original error context for better troubleshooting.

-except Exception as e:
-    print(f"Error deleting old log file {old_file}: {e}")
+except OSError as e:
+    logger.error(f"Error deleting old log file {old_file}", exc_info=e)
🧰 Tools
🪛 Ruff (0.8.2)

179-179: Do not catch blind exception: Exception

(BLE001)

📜 Review details

Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6342c69 and bb274ab.

📒 Files selected for processing (2)
  • application/__init__.py (1 hunks)
  • application/log_handlers/handlers.py (1 hunks)
🧰 Additional context used
🪛 Ruff (0.8.2)
application/log_handlers/handlers.py

21-21: Boolean-typed positional argument in function definition

(FBT001)


21-21: Boolean default positional argument in function definition

(FBT002)


69-69: Unnecessary open mode parameters

Remove open mode parameters

(UP015)


92-92: datetime.datetime.now() called without a tz argument

(DTZ005)


116-116: Use a context manager for opening files

(SIM115)


117-117: Do not catch blind exception: Exception

(BLE001)


118-118: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


118-118: Avoid specifying long messages outside the exception class

(TRY003)


179-179: Do not catch blind exception: Exception

(BLE001)

🔇 Additional comments (3)
application/log_handlers/handlers.py (2)

14-24: Consider making enable_compression a keyword-only argument.

This mirrors a prior suggestion about refactoring boolean default arguments to keyword-only for clarity and avoidance of parameter confusion.

🧰 Tools
🪛 Ruff (0.8.2)

21-21: Boolean-typed positional argument in function definition

(FBT001)


21-21: Boolean default positional argument in function definition

(FBT002)


182-191: LGTM for rotation logic and concurrency handling.

The locking mechanisms (acquire() and release()) ensure thread safety during writes and log rotations. The approach looks well-structured.

application/__init__.py (1)

35-39: Validation: The fallback ensures a logger is always defined.

This approach guards against logging failures by defining the logger in both the try and except blocks.

Comment on lines +45 to +85
@staticmethod
def _get_persistent_machine_uuid() -> str:
"""
Retrieves a persistent machine-specific UUID:
- First checks the `iCtrl_LOG_UUID` environment variable.
- If not found, checks a system-wide file (`/etc/iCtrl_machine_log_uuid` or `~/.iCtrl_machine_log_uuid`).
- If still not found, generates a new UUID, saves it, and returns only the first 8 characters.
"""
env_var_name = "iCtrl_LOG_UUID"

# 1️⃣ Check if UUID exists in an environment variable
env_uuid = os.getenv(env_var_name)
if env_uuid:
return env_uuid.strip()[:8] # Limit to first 8 characters

# 2️⃣ Define a persistent file path
if os.name == "nt": # Windows
uuid_file = Path(os.getenv("APPDATA", "C:/ProgramData")) / "iCtrl_machine_log_uuid.txt"
else: # Linux/macOS
uuid_file = Path(
"/etc/iCtrl_machine_log_uuid") if os.geteuid() == 0 else Path.home() / ".iCtrl_machine_log_uuid"

# 3️⃣ Check if the file exists and read the UUID
if uuid_file.exists():
with open(uuid_file, "r") as f:
machine_uuid = f.read().strip()[:8] # Limit to first 8 characters
os.environ[env_var_name] = machine_uuid # Store in environment for future use
return machine_uuid

# 4️⃣ Generate a new UUID and store only the first 8 characters
new_uuid = str(uuid.uuid4())[:8]

try:
with open(uuid_file, "w") as f:
f.write(new_uuid)
os.environ[env_var_name] = new_uuid # Store in environment
except PermissionError:
print(f"Warning: Could not save machine UUID to {uuid_file}. Using in-memory only.")

return new_uuid

Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

Use logging instead of printing errors to maintain consistency.

At line 82, a print statement warns about permission issues. For consistency and configurability in production environments, consider using logger.warning(...) instead of printing to stdout.

-except PermissionError:
-    print(f"Warning: Could not save machine UUID to {uuid_file}. Using in-memory only.")
+except PermissionError as err:
+    logger.warning(f"Could not save machine UUID to {uuid_file}. Using in-memory only. {err}")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@staticmethod
def _get_persistent_machine_uuid() -> str:
"""
Retrieves a persistent machine-specific UUID:
- First checks the `iCtrl_LOG_UUID` environment variable.
- If not found, checks a system-wide file (`/etc/iCtrl_machine_log_uuid` or `~/.iCtrl_machine_log_uuid`).
- If still not found, generates a new UUID, saves it, and returns only the first 8 characters.
"""
env_var_name = "iCtrl_LOG_UUID"
# 1️⃣ Check if UUID exists in an environment variable
env_uuid = os.getenv(env_var_name)
if env_uuid:
return env_uuid.strip()[:8] # Limit to first 8 characters
# 2️⃣ Define a persistent file path
if os.name == "nt": # Windows
uuid_file = Path(os.getenv("APPDATA", "C:/ProgramData")) / "iCtrl_machine_log_uuid.txt"
else: # Linux/macOS
uuid_file = Path(
"/etc/iCtrl_machine_log_uuid") if os.geteuid() == 0 else Path.home() / ".iCtrl_machine_log_uuid"
# 3️⃣ Check if the file exists and read the UUID
if uuid_file.exists():
with open(uuid_file, "r") as f:
machine_uuid = f.read().strip()[:8] # Limit to first 8 characters
os.environ[env_var_name] = machine_uuid # Store in environment for future use
return machine_uuid
# 4️⃣ Generate a new UUID and store only the first 8 characters
new_uuid = str(uuid.uuid4())[:8]
try:
with open(uuid_file, "w") as f:
f.write(new_uuid)
os.environ[env_var_name] = new_uuid # Store in environment
except PermissionError:
print(f"Warning: Could not save machine UUID to {uuid_file}. Using in-memory only.")
return new_uuid
@staticmethod
def _get_persistent_machine_uuid() -> str:
"""
Retrieves a persistent machine-specific UUID:
- First checks the `iCtrl_LOG_UUID` environment variable.
- If not found, checks a system-wide file (`/etc/iCtrl_machine_log_uuid` or `~/.iCtrl_machine_log_uuid`).
- If still not found, generates a new UUID, saves it, and returns only the first 8 characters.
"""
env_var_name = "iCtrl_LOG_UUID"
# 1️⃣ Check if UUID exists in an environment variable
env_uuid = os.getenv(env_var_name)
if env_uuid:
return env_uuid.strip()[:8] # Limit to first 8 characters
# 2️⃣ Define a persistent file path
if os.name == "nt": # Windows
uuid_file = Path(os.getenv("APPDATA", "C:/ProgramData")) / "iCtrl_machine_log_uuid.txt"
else: # Linux/macOS
uuid_file = Path(
"/etc/iCtrl_machine_log_uuid") if os.geteuid() == 0 else Path.home() / ".iCtrl_machine_log_uuid"
# 3️⃣ Check if the file exists and read the UUID
if uuid_file.exists():
with open(uuid_file, "r") as f:
machine_uuid = f.read().strip()[:8] # Limit to first 8 characters
os.environ[env_var_name] = machine_uuid # Store in environment for future use
return machine_uuid
# 4️⃣ Generate a new UUID and store only the first 8 characters
new_uuid = str(uuid.uuid4())[:8]
try:
with open(uuid_file, "w") as f:
f.write(new_uuid)
os.environ[env_var_name] = new_uuid # Store in environment
except PermissionError as err:
logger.warning(f"Could not save machine UUID to {uuid_file}. Using in-memory only. {err}")
return new_uuid
🧰 Tools
🪛 Ruff (0.8.2)

69-69: Unnecessary open mode parameters

Remove open mode parameters

(UP015)

Comment on lines +116 to +118
new_stream = open(new_log_file, "ab")
except Exception as e:
raise RuntimeError(f"Failed to open new log file {new_log_file}: {e}")
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Use a context manager and attach root cause when re-raising.

  1. Simplify file operations by using a with statement.
  2. Add from e when re-raising to preserve the original traceback.
-try:
-    new_stream = open(new_log_file, "ab")
-except Exception as e:
-    raise RuntimeError(f"Failed to open new log file {new_log_file}: {e}")
+try:
+    with open(new_log_file, "ab") as new_stream:
+        self.acquire()
+        try:
+            self.stream = new_stream
+            self.init(new_stream)
+            self.current_log_file = new_log_file
+        finally:
+            self.release()
+except (PermissionError, OSError) as e:
+    raise RuntimeError(f"Failed to open new log file {new_log_file}") from e

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
🪛 Ruff (0.8.2)

116-116: Use a context manager for opening files

(SIM115)


117-117: Do not catch blind exception: Exception

(BLE001)


118-118: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


118-118: Avoid specifying long messages outside the exception class

(TRY003)

@@ -26,15 +26,17 @@
from werkzeug.exceptions import HTTPException
from werkzeug.serving import WSGIRequestHandler

logger = logging.getLogger(__name__)
global logger
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

Consider removing unnecessary global logger.

When a variable is declared at the module level, it's already accessible within the module. Using global logger here may be unnecessary and can be removed for cleaner code.

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.

2 participants