From ffaadba216079eb499120a868c60ae83aa2c8b28 Mon Sep 17 00:00:00 2001 From: mversaggi Date: Sun, 16 Nov 2025 23:35:25 -0500 Subject: [PATCH 1/3] NEX-7: Configure Flask app logger and add logging to app startup --- app.py | 70 +++++++++++++++++++++++++++++++++++++----- tests/conftest.py | 7 +++-- tests/constants.py | 2 ++ tests/unit/app_test.py | 9 +++--- 4 files changed, 73 insertions(+), 15 deletions(-) create mode 100644 tests/constants.py diff --git a/app.py b/app.py index 6af3b7b..542e1b8 100644 --- a/app.py +++ b/app.py @@ -1,30 +1,84 @@ +import os +import logging + from flask import Flask +from logging.handlers import RotatingFileHandler +from typing import Mapping -def create_app(): +def create_app(config: Mapping): """ - Creates and configures the Flask application instance. Intended for use by both production and test code. + Creates and configures the Flask application instance using the provided config mapping. """ app = Flask(__name__) + + app.config.from_mapping(config) + configure_logging(app) + app.logger.info("Creating Flask application instance") + # TODO: Secure this before deploying into production app.secret_key = "cUrR33_ChI_#" # Configure app - app.config.from_mapping( - { - "FLASK_ENV": "development", - } - ) + app.logger.debug(f"Using application configuration {config}") # Register routes via Flask blueprint from routes import app_blueprint + app.logger.info("Registering server endpoints") app.register_blueprint(app_blueprint) return app -app = create_app() +def configure_logging(app: Flask): + """ + Configure logging for the Flask application. Attempts to get the log level from a LOG_LEVEL environment variable, + defaults to INFO if environment variable not set. + """ + + # Get log level from env-var, default to INFO if env-var invalid or not set + log_level = logging.getLevelName(os.getenv("LOG_LEVEL")) + + if log_level is None or isinstance(log_level, str): + log_level = logging.INFO + + app.logger.setLevel(log_level) + + # Remove default Flask handlers to avoid duplicate logs + app.logger.handlers.clear() + + # Create and configure formatter + detailed_formatter = logging.Formatter( + "[%(asctime)s] %(levelname)s in %(module)s: %(message)s", "%H:%M:%S,%f %Y-%m-%d" + ) + + # Create and configure console handler for development logging + console_handler = logging.StreamHandler() + console_handler.setLevel(logging.DEBUG) + console_handler.setFormatter(detailed_formatter) + app.logger.addHandler(console_handler) + + # Add file handler with rotation for longer-running production deployments, omit during testing + if not bool(app.config["TESTING"]): + # Create logs directory if it doesn't exist + if not os.path.exists("logs"): + os.mkdir("logs") + + # Max 10MB per file, keep 10 backup files + file_handler = RotatingFileHandler( + "logs/netex.log", maxBytes=10 * 1024 * 1024, backupCount=10 # 10MB + ) + file_handler.setLevel(logging.INFO) + file_handler.setFormatter(detailed_formatter) + app.logger.addHandler(file_handler) + if __name__ == "__main__": + # TODO: Support environment- and file-based configuration + app = create_app( + { + "FLASK_ENV": "development", + } + ) app.run(debug=True) diff --git a/tests/conftest.py b/tests/conftest.py index bb536d9..fc9d52a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,6 +3,7 @@ import pytest from app import create_app +from tests import constants # TODO: Explain why this is needed for tests sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) @@ -10,9 +11,9 @@ @pytest.fixture(scope="class") def test_client(): - flask_app = create_app() - - flask_app.config["TESTING"] = True + flask_app = create_app( + {"FLASK_ENV": "development", "TESTING": constants.SET_TESTING_CONFIG} + ) # Create a test client using the Flask application configured for testing with flask_app.test_client() as test_client: diff --git a/tests/constants.py b/tests/constants.py new file mode 100644 index 0000000..0231e35 --- /dev/null +++ b/tests/constants.py @@ -0,0 +1,2 @@ +# Adds the "TESTING" config to the Flask app if True +SET_TESTING_CONFIG = True \ No newline at end of file diff --git a/tests/unit/app_test.py b/tests/unit/app_test.py index caffd97..83db786 100644 --- a/tests/unit/app_test.py +++ b/tests/unit/app_test.py @@ -1,5 +1,6 @@ from app import create_app from http import HTTPMethod +from tests import constants class TestApp: @@ -12,11 +13,11 @@ def test_app_contains_index_route(self): WHEN create_app() is called THEN a Flask app with an index route ("/") supporting GET and POST methods should be returned. """ - app = create_app() + app = create_app({"TESTING": constants.SET_TESTING_CONFIG}) # Assert configs assert app is not None - assert app.config["FLASK_ENV"] == "development" + assert app.config["TESTING"] == constants.SET_TESTING_CONFIG assert app.secret_key is not None url_map = app.url_map @@ -26,7 +27,7 @@ def test_app_contains_index_route(self): index_rule = routes[0] assert index_rule.rule == "/" - + assert HTTPMethod.GET in index_rule.methods assert HTTPMethod.POST in index_rule.methods @@ -35,7 +36,7 @@ def test_app_contains_summary_route(self): WHEN create_app() is called THEN a Flask app with a "/summary/{netcdf_filename} route supporting only the GET method should be returned. """ - app = create_app() + app = create_app({"TESTING": True}) url_map = app.url_map # Assert summary route is registered and supports GET method From 50dc10aacca41886fd73f5826e04c2c74268ba80 Mon Sep 17 00:00:00 2001 From: mversaggi Date: Sun, 16 Nov 2025 23:44:42 -0500 Subject: [PATCH 2/3] NEX-7: Add debug logging to routes --- routes.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/routes.py b/routes.py index 8d91616..5c4cba7 100644 --- a/routes.py +++ b/routes.py @@ -1,12 +1,19 @@ import humanize import xarray -from flask import Blueprint, redirect, render_template, request, session, url_for +from flask import ( + Blueprint, + current_app, + redirect, + render_template, + request, + session, + url_for, +) from http import HTTPMethod from io import BytesIO - app_blueprint = Blueprint("netex", __name__) FILE_SESSION_KEY = "netcdf_file" @@ -17,6 +24,9 @@ def index(): if request.method == HTTPMethod.POST: netcdf_file = request.files["netcdf_file"] + current_app.logger.debug( + f"Received NetCDF summary request for file '{netcdf_file.filename}'" + ) if netcdf_file: # Get file size netcdf_file.seek(0, 2) @@ -34,6 +44,7 @@ def index(): url_for("netex.summary", netcdf_filename=netcdf_file.filename) ) + current_app.logger.debug("Received GET request for index page") return render_template("index.html.jinja") @@ -41,6 +52,9 @@ def index(): def summary(netcdf_filename): summary = session["summary_text"] file_size = session["file_size"] + + current_app.logger.debug("Showing summary for file '{netcdf_filename}'") + return render_template( "summary.html.jinja", file_name=netcdf_filename, From aa4c0147f8c0cd8870bddeb93af63e8759047f62 Mon Sep 17 00:00:00 2001 From: mversaggi Date: Tue, 18 Nov 2025 09:48:53 -0500 Subject: [PATCH 3/3] NEX-7: Add logs directory to .gitignore, update docs in app.py, fix log timestamp format --- .gitignore | 1 + app.py | 12 ++++++++++-- routes.py | 2 +- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 2b61a0c..0dff4bc 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ __pycache__ # Pytest folders .pytest_cache +logs reports # Node.js diff --git a/app.py b/app.py index 542e1b8..554cac4 100644 --- a/app.py +++ b/app.py @@ -9,6 +9,8 @@ def create_app(config: Mapping): """ Creates and configures the Flask application instance using the provided config mapping. + + :param config: The config mapping containing configuration key-value pairs """ app = Flask(__name__) @@ -35,6 +37,8 @@ def configure_logging(app: Flask): """ Configure logging for the Flask application. Attempts to get the log level from a LOG_LEVEL environment variable, defaults to INFO if environment variable not set. + + :param app: The Flask app instance to configure logging on """ # Get log level from env-var, default to INFO if env-var invalid or not set @@ -45,12 +49,16 @@ def configure_logging(app: Flask): app.logger.setLevel(log_level) - # Remove default Flask handlers to avoid duplicate logs + # Clear default handler + # + # TODO: Configure logging before creating the Flask app instance to prevent default handler from being added. + # TODO: See https://flask.palletsprojects.com/en/stable/logging/ app.logger.handlers.clear() # Create and configure formatter detailed_formatter = logging.Formatter( - "[%(asctime)s] %(levelname)s in %(module)s: %(message)s", "%H:%M:%S,%f %Y-%m-%d" + "[%(asctime)s.%(msecs)03d] %(levelname)s (%(module)s): %(message)s", + "%Y-%m-%d, %H:%M:%S" ) # Create and configure console handler for development logging diff --git a/routes.py b/routes.py index 5c4cba7..c090e22 100644 --- a/routes.py +++ b/routes.py @@ -53,7 +53,7 @@ def summary(netcdf_filename): summary = session["summary_text"] file_size = session["file_size"] - current_app.logger.debug("Showing summary for file '{netcdf_filename}'") + current_app.logger.debug(f"Showing summary for file '{netcdf_filename}'") return render_template( "summary.html.jinja",