From b235de39f6d25d7e418d8238267f6559abb052b5 Mon Sep 17 00:00:00 2001 From: Beth Probert Date: Wed, 17 Jun 2026 12:22:12 +0100 Subject: [PATCH 1/4] Update customise_server to poll the debug flag in the LT server instead of re-reading the value from CLI arguments. --- src/openflexure_microscope_server/server/__init__.py | 12 ++++++------ tests/unit_tests/test_server_cli.py | 12 +++++++++--- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/openflexure_microscope_server/server/__init__.py b/src/openflexure_microscope_server/server/__init__.py index 3735f312..01028715 100644 --- a/src/openflexure_microscope_server/server/__init__.py +++ b/src/openflexure_microscope_server/server/__init__.py @@ -72,7 +72,7 @@ def set_shutdown_function(shutdown_function: Callable[[], None]) -> None: def customise_server( - server: lt.ThingServer, application_config: OFMApplicationData, debug: bool = False + server: lt.ThingServer, application_config: OFMApplicationData ) -> None: """Customise the server with additional endpoints, debug mode etc.""" if DEVELOPER_MODE: @@ -88,8 +88,9 @@ def customise_server( add_v2_endpoints(server) add_static_files(server, application_config.data_folder) - # Configure logging to DEBUG if requested in CLI args. - if debug: + # Configure logging to DEBUG if LT server is set up + # with debug = true + if server.debug: lt.logs.configure_thing_logger(logging.DEBUG) # Add an endpoint to get the logs - (directly calling the FastAPI decorator) @@ -119,9 +120,8 @@ def serve_from_cli(argv: Optional[list[str]] = None) -> None: application_config = OFMApplicationData(**lt_config.application_config) configure_logging(application_config.log_folder) - server = lt.ThingServer.from_config(lt_config) - debug = bool(args.debug) - customise_server(server, application_config, debug) + server = lt.ThingServer.from_config(lt_config, args.debug) + customise_server(server, application_config) def shutdown_call() -> None: try: diff --git a/tests/unit_tests/test_server_cli.py b/tests/unit_tests/test_server_cli.py index 4c71a1c3..b7837781 100644 --- a/tests/unit_tests/test_server_cli.py +++ b/tests/unit_tests/test_server_cli.py @@ -109,14 +109,20 @@ def test_failed_customise(mocker): # An that it has the error to display assert str(fallback_app._context.error) == "Can't touch this" - def test_debug_mode(mocker): """Test that --debug flag triggers lt.logs.configure_thing_logger.""" - # Mock the LabThings function that returns the server + + # Use a side_effect to map the CLI debug arg to the mock server's debug attribute + def mock_from_config(_, debug_flag): + mock_server = mocker.Mock() + mock_server.debug = debug_flag # This will now be correctly True or False + return mock_server + mocker.patch( "openflexure_microscope_server.server.lt.ThingServer.from_config", - return_value=mocker.Mock(), + side_effect=mock_from_config, ) + # Mock customisation dependencies to avoid side effects mocker.patch.object(ofm_server, "add_v2_endpoints") mocker.patch.object(ofm_server, "add_static_files") From b1202723a1aec990e3902979fb9e2b267ad61272 Mon Sep 17 00:00:00 2001 From: Beth Probert Date: Wed, 17 Jun 2026 13:02:05 +0100 Subject: [PATCH 2/4] Fix ruff Add debug test Add info log message for sanity Add more useful logs Edit configure_logging to change OFM level to debug if set in CLI Update log message Fix boot and int mix up Fix string Change string Update logging string again try changing OFM handler level Change log slice Fix syntax error Add in OFMHandler logic, update webapp to show in logging tab and fix ruff --- src/openflexure_microscope_server/logging.py | 18 ++++++++++++++---- .../server/__init__.py | 10 +++------- tests/unit_tests/test_server_cli.py | 1 + .../tabContentComponents/loggingContent.vue | 2 +- 4 files changed, 19 insertions(+), 12 deletions(-) diff --git a/src/openflexure_microscope_server/logging.py b/src/openflexure_microscope_server/logging.py index a32061b7..0d4375f4 100644 --- a/src/openflexure_microscope_server/logging.py +++ b/src/openflexure_microscope_server/logging.py @@ -23,17 +23,26 @@ LOGGER = logging.getLogger(__name__) OFM_LOG_FILE: Optional[str] = None -def configure_logging(log_folder: str) -> None: +def configure_logging(log_folder: str, debug: bool = False) -> None: """Configure logging for the server while it is running. - This modifies the root logger to have a rotating file handler and + Params: + - log_folder: This modifies the root logger to have a rotating file handler and adds a custom handler that prints and stores all records except ``uvicorn.access`` logs. + - debug: This modifies the root logger to change its logging level. It is + set to True by starting the server with the cli argument `--debug`. It is important not to let Uvicorn override these settings. + """ root_logger = logging.getLogger() - root_logger.setLevel(logging.INFO) + + if debug: + root_logger.setLevel(logging.DEBUG) + else: + root_logger.setLevel(logging.INFO) + # Explicitly make OFM_LOG_FILE a global so it can be updated based on log settings # This requires silencing PLW0603 which disallows globals. global OFM_LOG_FILE # noqa: PLW0603 @@ -44,6 +53,7 @@ def configure_logging(log_folder: str) -> None: ofm_format_str = "[%(asctime)s] [%(levelname)s] %(message)s" OFM_HANDLER.setFormatter(logging.Formatter(ofm_format_str)) OFM_HANDLER.addFilter(UvicornAccessFilter()) + OFM_HANDLER.level = root_logger.level root_logger.addHandler(OFM_HANDLER) try: @@ -63,7 +73,7 @@ def configure_logging(log_folder: str) -> None: except PermissionError as e: LOGGER.error(f"Cannot create log file at {OFM_LOG_FILE}: {e}") - LOGGER.info("OFM server root logger has been set up at INFO level") + LOGGER.info("OFM server root logger has been set up at %s level", logging.getLevelName(root_logger.level)) def retrieve_log() -> PlainTextResponse: diff --git a/src/openflexure_microscope_server/server/__init__.py b/src/openflexure_microscope_server/server/__init__.py index 01028715..2e1849bf 100644 --- a/src/openflexure_microscope_server/server/__init__.py +++ b/src/openflexure_microscope_server/server/__init__.py @@ -88,11 +88,6 @@ def customise_server( add_v2_endpoints(server) add_static_files(server, application_config.data_folder) - # Configure logging to DEBUG if LT server is set up - # with debug = true - if server.debug: - lt.logs.configure_thing_logger(logging.DEBUG) - # Add an endpoint to get the logs - (directly calling the FastAPI decorator) server.app.get(str(server.api_prefix.rstrip("/")) + "/log/")(retrieve_log) server.app.get(str(server.api_prefix.rstrip("/")) + "/logfile/")( @@ -114,13 +109,14 @@ def serve_from_cli(argv: Optional[list[str]] = None) -> None: server = None try: lt_config = _full_config_from_args(args) + debug = bool(args.debug) # Validate our application data if lt_config.application_config is None: raise ValueError("No application configuration was supplied.") application_config = OFMApplicationData(**lt_config.application_config) - configure_logging(application_config.log_folder) + configure_logging(application_config.log_folder, debug) - server = lt.ThingServer.from_config(lt_config, args.debug) + server = lt.ThingServer.from_config(lt_config, debug) customise_server(server, application_config) def shutdown_call() -> None: diff --git a/tests/unit_tests/test_server_cli.py b/tests/unit_tests/test_server_cli.py index b7837781..b9eda4f3 100644 --- a/tests/unit_tests/test_server_cli.py +++ b/tests/unit_tests/test_server_cli.py @@ -109,6 +109,7 @@ def test_failed_customise(mocker): # An that it has the error to display assert str(fallback_app._context.error) == "Can't touch this" + def test_debug_mode(mocker): """Test that --debug flag triggers lt.logs.configure_thing_logger.""" diff --git a/webapp/src/components/tabContentComponents/loggingContent.vue b/webapp/src/components/tabContentComponents/loggingContent.vue index 21b0421d..56cb3ee3 100644 --- a/webapp/src/components/tabContentComponents/loggingContent.vue +++ b/webapp/src/components/tabContentComponents/loggingContent.vue @@ -121,7 +121,7 @@ export default { ...mapState(useSettingsStore, ["baseUri"]), filteredLevels: function () { let cutoffIndex = this.allLevels.indexOf(this.filterLevel); - return this.allLevels.slice(cutoffIndex, -1); + return this.allLevels.slice(cutoffIndex); }, filteredItems: function () { var items = []; From cf6fd4ccea5ecb67b30f0dd86d17e01f74618d3a Mon Sep 17 00:00:00 2001 From: Beth Probert Date: Wed, 17 Jun 2026 16:42:25 +0100 Subject: [PATCH 3/4] Update to cover new logic. Updatee READMEs and Guides with --debug arg information --- README.md | 2 +- apidocs/dev/README.md | 4 ++ apidocs/dev/simulation_guide.md | 2 + src/openflexure_microscope_server/logging.py | 5 ++- tests/unit_tests/test_logging.py | 28 ++++++++++++ tests/unit_tests/test_server_cli.py | 47 +++++++++----------- 6 files changed, 59 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 4af9d1bf..52ebcd0f 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ More information is also available in the [handbook](https://gitlab.com/openflex ## Running directly -The Python package provides a command `ofm-microscope-server` that runs the server. This is what is used by `systemd` to run the service. You will need to provide some command-line arguments, see the output of `--help` for an up to date list. See `/etc/systemd/system/openflexure-microscope-server.service` for the command line used to run the server by default on the Raspberry Pi. In general, you are likely to want to specify a configuration file with `-c`, a host (`--host 0.0.0.0` to serve on all addresses) and a port (`--port 5000`). The `--fallback` option will allow the server to start *even if the hardware specified in the configuration file can't load*. This serves an error page, rather than have the server fail. In the future, it should redirect users to a way to fix their configuration. +The Python package provides a command `ofm-microscope-server` that runs the server. This is what is used by `systemd` to run the service. You will need to provide some command-line arguments, see the output of `--help` for an up to date list. See `/etc/systemd/system/openflexure-microscope-server.service` for the command line used to run the server by default on the Raspberry Pi. In general, you are likely to want to specify a configuration file with `-c`, a host (`--host 0.0.0.0` to serve on all addresses) and a port (`--port 5000`). The `--fallback` option will allow the server to start *even if the hardware specified in the configuration file can't load*. This serves an error page, rather than have the server fail. In the future, it should redirect users to a way to fix their configuration. The `--debug` option spins up both the OpenFlexure Microscope server and the LabThings server in `DEBUG` mode, providing more verbose logging information. It is also possible to run the server in simulation mode. See Developer guidelines below. diff --git a/apidocs/dev/README.md b/apidocs/dev/README.md index bd68d26a..e24986b3 100644 --- a/apidocs/dev/README.md +++ b/apidocs/dev/README.md @@ -71,6 +71,8 @@ sudo -u openflexure-ws application/openflexure-microscope-server/.venv/bin/openf ``` +> Note: If you would like more detailed logging information from both the OpenFlexure Microscope server and the LabThings server, you can add the `--debug` argument to the end of the above command to activate `DEBUG` level logs. + ### Running the simulation server on other platforms To start the simulation server: @@ -81,6 +83,8 @@ To start the simulation server: `openflexure-microscope-server --fallback -c ./ofm_config_simulation.json` +> Note: If you would like more detailed logging information from both the OpenFlexure Microscope server and the LabThings server, you can add the `--debug` argument to the end of the above command to activate `DEBUG` level logs. + * Open the address displayed in the terminal in any browser. * To close the simulation server, return to the terminal and press `ctrl + c`. diff --git a/apidocs/dev/simulation_guide.md b/apidocs/dev/simulation_guide.md index cf7997ae..a79d6e6e 100644 --- a/apidocs/dev/simulation_guide.md +++ b/apidocs/dev/simulation_guide.md @@ -27,6 +27,8 @@ The simulation server GUI is a copy of the GUI you would find on the hardware wi This can fail with an error that the chosen port is already in use. In that case, append `--port 8081` +> Note: If you would like more detailed logging information from both the OpenFlexure Microscope server and the LabThings server, you can add the `--debug` argument to the end of the above command to activate `DEBUG` level logs. + --- ## Video Walkthrough diff --git a/src/openflexure_microscope_server/logging.py b/src/openflexure_microscope_server/logging.py index 0d4375f4..aa68741a 100644 --- a/src/openflexure_microscope_server/logging.py +++ b/src/openflexure_microscope_server/logging.py @@ -73,7 +73,10 @@ def configure_logging(log_folder: str, debug: bool = False) -> None: except PermissionError as e: LOGGER.error(f"Cannot create log file at {OFM_LOG_FILE}: {e}") - LOGGER.info("OFM server root logger has been set up at %s level", logging.getLevelName(root_logger.level)) + LOGGER.info( + "OFM server root logger has been set up at %s level", + logging.getLevelName(root_logger.level), + ) def retrieve_log() -> PlainTextResponse: diff --git a/tests/unit_tests/test_logging.py b/tests/unit_tests/test_logging.py index 56de711f..0f961c23 100644 --- a/tests/unit_tests/test_logging.py +++ b/tests/unit_tests/test_logging.py @@ -206,3 +206,31 @@ def test_uvicorn_error_only_says_error_on_error( assert name in log_txt for name in names_not_in_log: assert name not in log_txt + + +@pytest.mark.parametrize( + ("debug_flag", "expected_level"), + [ + (False, logging.INFO), + (True, logging.DEBUG), + ], +) +def test_configure_logging_debug_mode(debug_flag, expected_level): + """Check that debug flag sets the correct root and handler logging levels.""" + # Reset handler at start of test + ofm_logging.OFM_HANDLER = ofm_logging.OFMHandler() + + with tmp_logging_dir() as tmpdir: + # Run configuration with the parameterised debug flag + ofm_logging.configure_logging(tmpdir, debug=debug_flag) + + root_logger = logging.getLogger() + + # Assert the root logger level was set correctly + assert root_logger.level == expected_level + # Assert the custom OFM handler level was synced to the root logger + assert ofm_logging.OFM_HANDLER.level == expected_level + + # Cleanup: Remove the OFM_HANDLER from the root logger so it doesn't + # interfere with other tests in the suite. + root_logger.removeHandler(ofm_logging.OFM_HANDLER) diff --git a/tests/unit_tests/test_server_cli.py b/tests/unit_tests/test_server_cli.py index b9eda4f3..90a5bfda 100644 --- a/tests/unit_tests/test_server_cli.py +++ b/tests/unit_tests/test_server_cli.py @@ -112,42 +112,35 @@ def test_failed_customise(mocker): def test_debug_mode(mocker): """Test that --debug flag triggers lt.logs.configure_thing_logger.""" - - # Use a side_effect to map the CLI debug arg to the mock server's debug attribute - def mock_from_config(_, debug_flag): - mock_server = mocker.Mock() - mock_server.debug = debug_flag # This will now be correctly True or False - return mock_server - - mocker.patch( + # Mock the LabThings server creation so we can inspect its arguments + mock_from_config = mocker.patch( "openflexure_microscope_server.server.lt.ThingServer.from_config", - side_effect=mock_from_config, + return_value=mocker.Mock(), ) - # Mock customisation dependencies to avoid side effects - mocker.patch.object(ofm_server, "add_v2_endpoints") - mocker.patch.object(ofm_server, "add_static_files") - # Mock logging to avoid side effects and allow checking calls - mocker.patch.object(ofm_server, "configure_logging") - mocker.patch.object(ofm_server, "retrieve_log") - mocker.patch.object(ofm_server, "retrieve_log_from_file") - # Mock uvicorn.run to avoid starting a server + # Mock configure_logging to inspect its arguments and prevent side effects + mock_configure_logging = mocker.patch.object(ofm_server, "configure_logging") + + # Mock other dependencies to avoid side effects (file writing, server starting) + mocker.patch.object(ofm_server, "customise_server") mocker.patch("openflexure_microscope_server.server.uvicorn.run") - # Mock the target function - mock_configure_thing_logger = mocker.patch( - "openflexure_microscope_server.server.lt.logs.configure_thing_logger" - ) - # 1. Run with --debug ofm_server.serve_from_cli(["-c", SIM_CONFIG, "--debug"]) - # Verify it was called with DEBUG level - mock_configure_thing_logger.assert_called_once_with(logging.DEBUG) + # Verify configure_logging was called with debug=True (the second argument) + assert mock_configure_logging.call_args.args[1] is True + # Verify ThingServer.from_config was called with debug=True (the second argument) + assert mock_from_config.call_args.args[1] is True + + # Reset the mocks for the next run + mock_configure_logging.reset_mock() + mock_from_config.reset_mock() # 2. Run without --debug - mock_configure_thing_logger.reset_mock() ofm_server.serve_from_cli(["-c", SIM_CONFIG]) - # Verify it was NOT called - mock_configure_thing_logger.assert_not_called() + # Verify configure_logging was called with debug=False + assert mock_configure_logging.call_args.args[1] is False + # Verify ThingServer.from_config was called with debug=False + assert mock_from_config.call_args.args[1] is False From 05b802b53e7cc13eeeb56bade9c5c1ddae1ee25d Mon Sep 17 00:00:00 2001 From: Beth Probert Date: Wed, 17 Jun 2026 17:11:47 +0100 Subject: [PATCH 4/4] Fix docstring for document generation --- src/openflexure_microscope_server/logging.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/openflexure_microscope_server/logging.py b/src/openflexure_microscope_server/logging.py index aa68741a..38ad3848 100644 --- a/src/openflexure_microscope_server/logging.py +++ b/src/openflexure_microscope_server/logging.py @@ -31,7 +31,7 @@ def configure_logging(log_folder: str, debug: bool = False) -> None: adds a custom handler that prints and stores all records except ``uvicorn.access`` logs. - debug: This modifies the root logger to change its logging level. It is - set to True by starting the server with the cli argument `--debug`. + set to True by starting the server with the cli argument ``--debug``. It is important not to let Uvicorn override these settings.