Skip to content

Add log rotation #312

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 2 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 58 additions & 27 deletions api/logging_config.py
Original file line number Diff line number Diff line change
@@ -1,54 +1,85 @@
import logging
import os
from pathlib import Path
from logging.handlers import RotatingFileHandler


class IgnoreLogChangeDetectedFilter(logging.Filter):
def filter(self, record: logging.LogRecord):
return "Detected file change in" not in record.getMessage()


def setup_logging(format: str = None):
"""
Configure logging for the application.
Reads LOG_LEVEL and LOG_FILE_PATH from environment (defaults: INFO, logs/application.log).
Ensures log directory exists, and configures both file and console handlers.
Configure logging for the application with log rotation.

Environment variables:
LOG_LEVEL: Log level (default: INFO)
LOG_FILE_PATH: Path to log file (default: logs/application.log)
LOG_MAX_SIZE: Max size in MB before rotating (default: 10MB)
LOG_BACKUP_COUNT: Number of backup files to keep (default: 5)

Ensures log directory exists, prevents path traversal, and configures
both rotating file and console handlers.
"""
# Determine log directory and default file path
base_dir = Path(__file__).parent
log_dir = base_dir / "logs"
log_dir.mkdir(parents=True, exist_ok=True)
default_log_file = log_dir / "application.log"

# Get log level and file path from environment
# Get log level from environment
log_level_str = os.environ.get("LOG_LEVEL", "INFO").upper()
log_level = getattr(logging, log_level_str, logging.INFO)
log_file_path = Path(os.environ.get(
"LOG_FILE_PATH", str(default_log_file)))

# ensure log_file_path is within the project's logs directory to prevent path traversal
# Get log file path
log_file_path = Path(os.environ.get("LOG_FILE_PATH", str(default_log_file)))

# Secure path check: must be inside logs/ directory
log_dir_resolved = log_dir.resolve()
resolved_path = log_file_path.resolve()
if not str(resolved_path).startswith(str(log_dir_resolved) + os.sep):
raise ValueError(
f"LOG_FILE_PATH '{log_file_path}' is outside the trusted log directory '{log_dir_resolved}'"
)
# Ensure parent dirs exist for the log file
raise ValueError(f"LOG_FILE_PATH '{log_file_path}' is outside the trusted log directory '{log_dir_resolved}'")

# Ensure parent directories exist
resolved_path.parent.mkdir(parents=True, exist_ok=True)

# Configure logging handlers and format
logging.basicConfig(
level=log_level,
format = format or "%(asctime)s - %(levelname)s - %(name)s - %(filename)s:%(lineno)d - %(message)s",
handlers=[
logging.FileHandler(resolved_path),
logging.StreamHandler()
],
force=True
)

# Ignore log file's change detection
for handler in logging.getLogger().handlers:
handler.addFilter(IgnoreLogChangeDetectedFilter())
# Get max log file size (default: 10MB)
try:
max_mb = int(os.environ.get("LOG_MAX_SIZE", 10)) # 10MB default
max_bytes = max_mb * 1024 * 1024
except (TypeError, ValueError):
max_bytes = 10 * 1024 * 1024 # fallback to 10MB on error

# Initial debug message to confirm configuration
# Get backup count (default: 5)
try:
backup_count = int(os.environ.get("LOG_BACKUP_COUNT", 5))
except ValueError:
backup_count = 5

# Configure format
log_format = format or "%(asctime)s - %(levelname)s - %(name)s - %(filename)s:%(lineno)d - %(message)s"

# Create handlers
file_handler = RotatingFileHandler(resolved_path, maxBytes=max_bytes, backupCount=backup_count, encoding="utf-8")
console_handler = logging.StreamHandler()

# Set format for both handlers
formatter = logging.Formatter(log_format)
file_handler.setFormatter(formatter)
console_handler.setFormatter(formatter)

# Add filter to suppress "Detected file change" messages
file_handler.addFilter(IgnoreLogChangeDetectedFilter())
console_handler.addFilter(IgnoreLogChangeDetectedFilter())

# Apply logging configuration
logging.basicConfig(level=log_level, handlers=[file_handler, console_handler], force=True)

# Log configuration info
logger = logging.getLogger(__name__)
logger.debug(f"Log level set to {log_level_str}, log file: {resolved_path}")
logger.debug(
f"Logging configured: level={log_level_str}, "
f"file={resolved_path}, max_size={max_bytes} bytes, "
f"backup_count={backup_count}"
)