diff --git a/app/__init__.py b/app/__init__.py index a286e3e..78ebbbd 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -5,6 +5,8 @@ from flask_migrate import Migrate from flask_mail import Mail from config import config import os +import logging +from logging.handlers import RotatingFileHandler db = SQLAlchemy() login_manager = LoginManager() @@ -25,6 +27,46 @@ def create_app(config_name='default'): login_manager.login_message = 'Please log in to access this page.' login_manager.login_message_category = 'info' + # ── Logging setup ──────────────────────────────────────────────────────── + # Configure root logger so that logger.info/error calls in all modules + # (notifications.py, issues.py, etc.) actually write output. + # Writes to stdout (captured by journalctl/gunicorn) AND a rotating file. + if not app.debug or os.environ.get('LOG_TO_FILE'): + log_level = logging.INFO + + formatter = logging.Formatter( + '[%(asctime)s] %(levelname)s in %(module)s: %(message)s', + datefmt='%Y-%m-%d %H:%M:%S', + ) + + # Stream handler — always on; journalctl captures stdout + stream_handler = logging.StreamHandler() + stream_handler.setLevel(log_level) + stream_handler.setFormatter(formatter) + + # Rotating file handler — keeps 5 × 5 MB log files + log_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'logs') + os.makedirs(log_dir, exist_ok=True) + file_handler = RotatingFileHandler( + os.path.join(log_dir, 'jqc.log'), + maxBytes=5 * 1024 * 1024, + backupCount=5, + ) + file_handler.setLevel(log_level) + file_handler.setFormatter(formatter) + + # Apply to both the Flask app logger and the root logger so all + # getLogger(__name__) calls in sub-modules are captured. + app.logger.setLevel(log_level) + app.logger.addHandler(stream_handler) + app.logger.addHandler(file_handler) + + root_logger = logging.getLogger() + root_logger.setLevel(log_level) + if not root_logger.handlers: + root_logger.addHandler(stream_handler) + root_logger.addHandler(file_handler) + # Register csrf_token() as an app-wide Jinja2 global so templates that # render manual forms (no WTForms object) can still inject the CSRF token. from flask_wtf.csrf import generate_csrf diff --git a/config.py b/config.py index 8843a7e..38a015b 100644 --- a/config.py +++ b/config.py @@ -52,11 +52,18 @@ class Config: DIGEST_SECRET = os.environ.get('DIGEST_SECRET') MAIL_SERVER = os.environ.get('MAIL_SERVER') - MAIL_PORT = int(os.environ.get('MAIL_PORT') or 587) - MAIL_USE_TLS = os.environ.get('MAIL_USE_TLS', 'true').lower() in ('true', 'on', '1') MAIL_USERNAME = os.environ.get('MAIL_USERNAME') MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD') + # ── SSL vs STARTTLS selection ──────────────────────────────────────────── + # Port 465 = implicit SSL → MAIL_USE_SSL=True, MAIL_USE_TLS=False + # Port 587 = STARTTLS → MAIL_USE_SSL=False, MAIL_USE_TLS=True + # The two flags are mutually exclusive; setting both True breaks Flask-Mail. + _mail_port = int(os.environ.get('MAIL_PORT') or 587) + MAIL_PORT = _mail_port + MAIL_USE_SSL = _mail_port == 465 + MAIL_USE_TLS = not MAIL_USE_SSL # STARTTLS only when NOT using implicit SSL + class DevelopmentConfig(Config): DEBUG = True