-
Notifications
You must be signed in to change notification settings - Fork 14
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
base: main
Are you sure you want to change the base?
Changes from 11 commits
fcc2d76
2d6129e
74e7f21
bfb69cc
4b46a72
f86f53d
a52c34b
60f0d7d
54bee53
a6dd459
02a07f3
6342c69
bb274ab
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,128 @@ | ||
import os | ||
from pathlib import Path | ||
from typing import Optional | ||
from clp_logging.handlers import CLPFileHandler, CLPLogLevelTimeout, EOF_CHAR, FLUSH_FRAME | ||
from datetime import datetime | ||
|
||
|
||
class RotatingCLPFileHandler(CLPFileHandler): | ||
""" | ||
Extends `CLPFileHandler` to support file size-based log rotation. | ||
""" | ||
|
||
def __init__( | ||
self, | ||
filename_prefix: str, | ||
log_dir: Path, | ||
max_bytes: int, | ||
backup_count: int = 20, | ||
mode: str = "ab", | ||
enable_compression: bool = True, | ||
timestamp_format: str = "%Y-%m-%d_%H-%M-%S", | ||
loglevel_timeout: Optional[CLPLogLevelTimeout] = None, | ||
) -> None: | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
self.filename_prefix = filename_prefix | ||
self.log_dir = Path(log_dir) | ||
self.max_bytes = max_bytes | ||
self.backup_count = backup_count | ||
self.timestamp_format = timestamp_format | ||
|
||
# Ensure log directory exists | ||
os.makedirs(log_dir, exist_ok=True) | ||
|
||
# Initialize the base filename with a timestamp | ||
self.current_log_file = self._generate_log_filename() | ||
|
||
super().__init__( | ||
fpath=self.current_log_file, | ||
mode=mode, | ||
enable_compression=enable_compression, | ||
timestamp_format=timestamp_format, | ||
loglevel_timeout=loglevel_timeout, | ||
) | ||
|
||
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 | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick (assertive) Use a timezone-aware or UTC time for consistency. 🧰 Tools🪛 Ruff (0.8.2)48-48: (DTZ005) |
||
def _should_rotate(self) -> bool: | ||
""" | ||
Check if the current file exceeds the maximum size. | ||
""" | ||
try: | ||
return self.current_log_file.stat().st_size >= self.max_bytes | ||
except FileNotFoundError: | ||
return False | ||
|
||
def _rotate(self) -> None: | ||
""" | ||
Perform log rotation by finalizing the current stream, creating a new log file, | ||
and reinitializing the handler with the new stream. | ||
|
||
This method is responsible for: | ||
1. Flushing and writing any remaining buffered data in the current stream. | ||
2. Writing a logical end-of-file (EOF) marker to signal the end of the log stream. | ||
3. Closing the current stream to release any associated resources. | ||
4. Generating a new log file name based on the current timestamp. | ||
5. Opening a new stream for the new log file and reinitializing the handler to use it. | ||
6. Removing old log files if the number of backups exceeds the specified backup count. | ||
|
||
Thread safety is ensured by acquiring and releasing the handler's lock during the | ||
transition between streams. | ||
""" | ||
try: | ||
self.ostream.flush(FLUSH_FRAME) | ||
self.ostream.write(EOF_CHAR) | ||
finally: | ||
self.ostream.close() | ||
|
||
# Generate a new log filename | ||
new_log_file = self._generate_log_filename() | ||
|
||
# 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}") | ||
|
||
self.acquire() | ||
try: | ||
self.stream = new_stream | ||
self.init(new_stream) | ||
finally: | ||
self.release() | ||
|
||
# Remove old backups | ||
self._remove_old_backups() | ||
|
||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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}") | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Restrict broad exception handling or re-raise appropriately. 🧰 Tools🪛 Ruff (0.8.2)119-119: Do not catch blind exception: (BLE001) |
||
def _write(self, loglevel: int, msg: str) -> None: | ||
""" | ||
Override `_write` to add rotation logic. | ||
""" | ||
if self._should_rotate(): | ||
self._rotate() | ||
super()._write(loglevel, msg) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,12 +7,17 @@ formatters: | |
datefmt: '%Y-%m-%d %H:%M:%S' | ||
|
||
handlers: | ||
console: | ||
class: logging.StreamHandler | ||
CLP_file: | ||
class: application.log_handlers.handlers.RotatingCLPFileHandler | ||
level: DEBUG | ||
formatter: default | ||
stream: ext://sys.stderr | ||
filename_prefix: 'ictrl' | ||
log_dir: 'logs' | ||
max_bytes: 5242880 # Rotate after 5 MB | ||
backup_count: 20 # Keep 20 backup files | ||
enable_compression: True | ||
timestamp_format: '%Y-%m-%d' | ||
|
||
root: | ||
level: DEBUG | ||
handlers: [console] | ||
handlers: [CLP_file] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
There was a problem hiding this comment.
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.