From dd0b30df51385cc4bc17736c8fbe4c7ad4341a59 Mon Sep 17 00:00:00 2001 From: Julian Stirling Date: Sun, 6 Apr 2025 15:19:39 +0100 Subject: [PATCH 1/3] Stop uvicorn.run disabling existing loggers --- .../server/__init__.py | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/openflexure_microscope_server/server/__init__.py b/src/openflexure_microscope_server/server/__init__.py index 1aa0a4cf..eff6fc48 100644 --- a/src/openflexure_microscope_server/server/__init__.py +++ b/src/openflexure_microscope_server/server/__init__.py @@ -30,7 +30,16 @@ def serve_from_cli(argv: Optional[list[str]] = None): config = cli.config_from_args(args) server = cli.server_from_config(config) customise_server(server) - uvicorn.run(server.app, host=args.host, port=args.port) + uvicorn.run( + server.app, + host=args.host, + port=args.port, + log_config={ + "version": 1, + "disable_existing_loggers": False, + }, + ) + except BaseException as e: if args.fallback: print(f"Error: {e}") @@ -40,6 +49,14 @@ def serve_from_cli(argv: Optional[list[str]] = None): app.labthings_config = config app.labthings_server = server app.labthings_error = e - uvicorn.run(app, host=args.host, port=args.port) + uvicorn.run( + app, + host=args.host, + port=args.port, + log_config={ + "version": 1, + "disable_existing_loggers": False, + }, + ) else: raise e From 70d9e2f75827ab9630de799c395355653facfe89 Mon Sep 17 00:00:00 2001 From: Julian Stirling Date: Mon, 7 Apr 2025 23:16:00 +0100 Subject: [PATCH 2/3] Add custom log handler for sending logs to UI --- src/openflexure_microscope_server/logging.py | 74 +++++++++++++++++-- .../tabContentComponents/loggingContent.vue | 5 +- 2 files changed, 73 insertions(+), 6 deletions(-) diff --git a/src/openflexure_microscope_server/logging.py b/src/openflexure_microscope_server/logging.py index 84c253c5..6afe04ef 100644 --- a/src/openflexure_microscope_server/logging.py +++ b/src/openflexure_microscope_server/logging.py @@ -11,6 +11,7 @@ OFM_LOG_FILE = os.path.join(OFM_LOG_FOLDER, "openflexure_microscope.log") def configure_logging(): root_logger = logging.getLogger() root_logger.setLevel(logging.INFO) + try: if not os.path.exists(OFM_LOG_FOLDER): os.makedirs(OFM_LOG_FOLDER) @@ -20,10 +21,13 @@ def configure_logging(): maxBytes=1000000, backupCount=10, ) - handler.setFormatter( - logging.Formatter("[%(asctime)s] [%(levelname)s] %(message)s") - ) + format_str = "[%(asctime)s] [%(levelname)s] <%(name)s> %(message)s" + handler.setFormatter(logging.Formatter(format_str)) root_logger.addHandler(handler) + ofm_format_str = "[%(asctime)s] [%(levelname)s] %(message)s" + OFM_HANDLER.setFormatter(logging.Formatter(ofm_format_str)) + root_logger.addHandler(OFM_HANDLER) + except PermissionError as e: logging.warning(f"Cannot create log file at {OFM_LOG_FILE}: {e}") logging.info("") @@ -33,5 +37,65 @@ def configure_logging(): def retrieve_log() -> PlainTextResponse: - """The most recent 1Mb of logs from the server""" - return FileResponse(OFM_LOG_FILE, media_type=PlainTextResponse.media_type) + """ + Returns logs since we started running the server, up to a maxiumum of + 250 messages. This log is the one shown in the UI and on the logging page. + + It does not include any of the `uvicorn.access` logs as these are emmitted + every time any API route is called. + + All logs, including `uvicorn.access` are logged to the OFM_LOG_FILE (see above) + ths is the best place to get logs about crashes. + """ + return PlainTextResponse(OFM_HANDLER.log_history) + + +class OFMHandler(logging.Handler): + """ + A child class of logging.Handler. This class handles storing the most recent + logs for access by the server. + """ + + def __init__(self, level=logging.INFO, max_logs=250): + super().__init__(level=level) + self._log = [] + self._max_logs = max_logs + + def append_record(self, record): + """ + Use the built in formatter to format the record, then save + it to an array. Pop any in excess of the mamimum number of logs + """ + self._log.append(self.format(record)) + while len(self._log) > self._max_logs: + self._log.pop(0) + + def emit(self, record): + """ + Emit will save the logged record to the log + """ + + # Basic filter for now that simply stops uvicorn.access logs + # These are the logs each time an API endpoint is accessed + # This is only the log for the UI. + if record.name.startswith("uvicorn.access"): + return + try: + if record.levelno >= self.level: + self.append_record(record) + except RecursionError as e: + raise e + except BaseException: + # Never let the logger crash unless there is somehow recursion. + # Use built in logging error handler + self.handleError(record) + + @property + def log_history(self): + """ + Return the log history up to the maximum number of logs + """ + return "\n".join(self._log) + + +OFM_HANDLER = OFMHandler() diff --git a/webapp/src/components/tabContentComponents/loggingContent.vue b/webapp/src/components/tabContentComponents/loggingContent.vue index 85547734..d74a27f5 100644 --- a/webapp/src/components/tabContentComponents/loggingContent.vue +++ b/webapp/src/components/tabContentComponents/loggingContent.vue @@ -30,7 +30,10 @@
- Download Log File
From 3a4715169451563aa29d8f43a17e8eb6a312860a Mon Sep 17 00:00:00 2001 From: Julian Stirling Date: Mon, 7 Apr 2025 23:38:09 +0100 Subject: [PATCH 3/3] Serve logfile and logs for UI seperately --- .gitlab/issue_templates/Bug.md | 4 ++-- src/openflexure_microscope_server/logging.py | 14 +++++++++++++- .../server/__init__.py | 5 +++-- .../tabContentComponents/loggingContent.vue | 9 ++++++--- 4 files changed, 24 insertions(+), 8 deletions(-) diff --git a/.gitlab/issue_templates/Bug.md b/.gitlab/issue_templates/Bug.md index 000b3627..86300792 100644 --- a/.gitlab/issue_templates/Bug.md +++ b/.gitlab/issue_templates/Bug.md @@ -22,8 +22,8 @@ To access your microscope log, either: * Run `ofm log` on your microscope, and copy/paste the output here -* Go to `http://:5000/log`, download the log file, and attach it here using the "Attach a File" button below - * In most setups, `http://microscope.local:5000/log` should work +* Go to `http://:5000/logfile`, download the log file, and attach it here using the "Attach a File" button below + * In most setups, `http://microscope.local:5000/logfile` should work ## Additional details diff --git a/src/openflexure_microscope_server/logging.py b/src/openflexure_microscope_server/logging.py index 6afe04ef..9082e092 100644 --- a/src/openflexure_microscope_server/logging.py +++ b/src/openflexure_microscope_server/logging.py @@ -2,7 +2,7 @@ import logging from logging.handlers import RotatingFileHandler import os -from fastapi.responses import FileResponse, PlainTextResponse +from fastapi.responses import PlainTextResponse OFM_LOG_FOLDER = "/var/openflexure/logs/" OFM_LOG_FILE = os.path.join(OFM_LOG_FOLDER, "openflexure_microscope.log") @@ -50,6 +50,18 @@ def retrieve_log() -> PlainTextResponse: return PlainTextResponse(OFM_HANDLER.log_history) +def retrieve_log_from_file() -> PlainTextResponse: + """ + Returns the full log from the file for downloading. + + Note this is read and then sent as otherwise it causes a RuntimeError if it + is written to while sending through FileResponse + """ + with open(OFM_LOG_FILE, "r", encoding="utf-8") as logfile: + full_log = logfile.read() + return PlainTextResponse(full_log) + + class OFMHandler(logging.Handler): """ A child class of logging.Handler. This class handles storing the most recent diff --git a/src/openflexure_microscope_server/server/__init__.py b/src/openflexure_microscope_server/server/__init__.py index eff6fc48..7c938362 100644 --- a/src/openflexure_microscope_server/server/__init__.py +++ b/src/openflexure_microscope_server/server/__init__.py @@ -6,7 +6,7 @@ from labthings_fastapi.server import cli, ThingServer import uvicorn from .serve_static_files import add_static_files from .legacy_api import add_v2_endpoints -from ..logging import configure_logging, retrieve_log +from ..logging import configure_logging, retrieve_log, retrieve_log_from_file def customise_server(server: ThingServer): @@ -18,8 +18,9 @@ def customise_server(server: ThingServer): except RuntimeError: print("Failed to add static files - you will have to do without them!") - # Add an endpoint to get the log + # Add an endpoint to get the logs - (directly calling the FastAPI decorator) server.app.get("/log/")(retrieve_log) + server.app.get("/logfile/")(retrieve_log_from_file) def serve_from_cli(argv: Optional[list[str]] = None): diff --git a/webapp/src/components/tabContentComponents/loggingContent.vue b/webapp/src/components/tabContentComponents/loggingContent.vue index d74a27f5..2b7488c7 100644 --- a/webapp/src/components/tabContentComponents/loggingContent.vue +++ b/webapp/src/components/tabContentComponents/loggingContent.vue @@ -33,7 +33,7 @@ Download Log File @@ -128,9 +128,12 @@ export default { return items; }, - logFileURI: function() { + logURI: function() { return `${this.$store.getters.baseUri}/log/`; }, + logFileURI: function() { + return `${this.$store.getters.baseUri}/logfile/`; + }, pagedItems: function() { let startIndex = (this.page - 1) * this.maxitems; return this.filteredItems.slice(startIndex, startIndex + this.maxitems); @@ -153,7 +156,7 @@ export default { } }, async updateLogs() { - let response = await axios.get(this.logFileURI); + let response = await axios.get(this.logURI); let lines = response.data.split("\n"); let logs = []; let regexp = /\[(.+)\] \[(.+)\] (.*)$/;