diff --git a/bin/slurm_calibrate.sh b/bin/slurm_calibrate.sh index 5786f7ce5737ed6a3be7a2d33658a1e359d5d331..98c45daedcaf5631efc55fc3ffd2c31f7d6c9143 100755 --- a/bin/slurm_calibrate.sh +++ b/bin/slurm_calibrate.sh @@ -26,6 +26,9 @@ echo "hostname: $(hostname)" export CAL_NOTEBOOK_NAME="$notebook" +# This is created by calibrate.py in CAL_WORKING_DIR +setup_logger="setup-logging-nb.py" + # make sure we use agg backend export MPLBACKEND=AGG @@ -40,13 +43,12 @@ then sleep 15 fi -echo "Running notebook" if [ "$caltype" == "CORRECT" ] then - # calparrot stores and repeats calcat queries - ${python_path} -m calparrot -- ${python_path} -m princess ${nb_path} --save + # calparrot stores and repeats calcat queries + ${python_path} -m calparrot -- ${python_path} -m princess ${nb_path} --save --run-before "$setup_logger" else - ${python_path} -m princess ${nb_path} --save + ${python_path} -m princess ${nb_path} --save --run-before "$setup_logger" fi # stop the cluster if requested diff --git a/notebooks/test/test-logging.ipynb b/notebooks/test/test-logging.ipynb new file mode 100644 index 0000000000000000000000000000000000000000..f09b1109e399582ea24e76c9a22d05a0c9873a93 --- /dev/null +++ b/notebooks/test/test-logging.ipynb @@ -0,0 +1,125 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "98e38fec", + "metadata": {}, + "outputs": [], + "source": [ + "in_folder = \"./\" # input folder\n", + "out_folder = \"./\" # output folder" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7fbce574", + "metadata": {}, + "outputs": [], + "source": [ + "import logging\n", + "import warnings\n", + "\n", + "from cal_tools.warnings import CalibrationWarning" + ] + }, + { + "cell_type": "markdown", + "id": "1bf72c57", + "metadata": {}, + "source": [ + "## WARNINGS" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c0d290a1", + "metadata": {}, + "outputs": [], + "source": [ + "class TestCalWarning(CalibrationWarning):\n", + " \"\"\"Base class for custom user warnings\"\"\"\n", + " pass\n", + "\n", + "\n", + "if 1 < 2:\n", + " warnings.warn('This inequality is true!', TestCalWarning)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6a4d4f01", + "metadata": {}, + "outputs": [], + "source": [ + "warnings.warn('This user warning will be ignored', UserWarning)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9b79ea47", + "metadata": {}, + "outputs": [], + "source": [ + "# This wont be logged\n", + "logging.warning(\"This is a warning message using logging standard library. It wont be logged\")" + ] + }, + { + "cell_type": "markdown", + "id": "2b22e2e0", + "metadata": {}, + "source": [ + "## ERRORS" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0f3bfeb7", + "metadata": {}, + "outputs": [], + "source": [ + "# This wont be logged\n", + "logging.error(\"Logging some (ERROR) without failing the notebook. It wont be logged\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c3b87719", + "metadata": {}, + "outputs": [], + "source": [ + "from cal_tools.exceptions import CalibrationError\n", + "\n", + "raise CalibrationError('Testing Calibration Failure!')" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/src/cal_tools/exceptions.py b/src/cal_tools/exceptions.py new file mode 100644 index 0000000000000000000000000000000000000000..5dbcbca65f8d7a81a4193df6821ca041d2df53ac --- /dev/null +++ b/src/cal_tools/exceptions.py @@ -0,0 +1,2 @@ +class CalibrationError(Exception): + pass diff --git a/src/cal_tools/warnings.py b/src/cal_tools/warnings.py new file mode 100644 index 0000000000000000000000000000000000000000..d54686ba0fe5fc61e33d14df1b81b4bd4bb52668 --- /dev/null +++ b/src/cal_tools/warnings.py @@ -0,0 +1,2 @@ +class CalibrationWarning(UserWarning): + pass diff --git a/src/xfel_calibrate/calibrate.py b/src/xfel_calibrate/calibrate.py index 5d2b63c6f4d00a2e717bd86cd95c37e5b56b2a0c..61f35e0a85fcd83b313d237ebbb4602d0efec48a 100755 --- a/src/xfel_calibrate/calibrate.py +++ b/src/xfel_calibrate/calibrate.py @@ -675,6 +675,12 @@ def run(argv=None): cal_work_dir / "pycalib-run-nb.sh" ) + # Copy the setup logger handler + shutil.copy2( + os.path.join(PKG_DIR, "setup_logging.py"), + cal_work_dir / "setup-logging-nb.py" + ) + if nb_details.user_venv: print("Using specified venv:", nb_details.user_venv) python_exe = str(nb_details.user_venv / 'bin' / 'python') diff --git a/src/xfel_calibrate/notebooks.py b/src/xfel_calibrate/notebooks.py index 5ac28ac12340e67410fe62750fd86e95ac8b8efd..97769638974ee8bd60545b4f70699c0cd979bbf4 100644 --- a/src/xfel_calibrate/notebooks.py +++ b/src/xfel_calibrate/notebooks.py @@ -360,6 +360,14 @@ notebooks = { "cluster cores": 1 }, }, + "TEST-LOGGING": { + "notebook": "notebooks/test/test-logging.ipynb", + "concurrency": { + "parameter": None, + "default concurrency": None, + "cluster cores": 1, + }, + }, }, "TEST-RAISES-ERRORS": { "TEST-BAD-KEY": { diff --git a/src/xfel_calibrate/setup_logging.py b/src/xfel_calibrate/setup_logging.py new file mode 100644 index 0000000000000000000000000000000000000000..b9237517c50870e9d492672939d1799611004256 --- /dev/null +++ b/src/xfel_calibrate/setup_logging.py @@ -0,0 +1,94 @@ +import json +import os +import sys +import warnings +from datetime import datetime +from typing import Any, Type + +import IPython + +from cal_tools.warnings import CalibrationWarning + +JOB_ID = os.getenv('SLURM_JOB_ID', 'local') + + +def get_class_hierarchy(cls: Type) -> str: + """Get the full class hierarchy of a class.""" + return ".".join(c.__name__ for c in cls.__mro__ if c != object) + + +def get_log_info( + message: str, + log_type: Type[Exception] | Type[Warning] +) -> dict: + """Create a dictionary with log information.""" + return { + "timestamp": datetime.now().isoformat(), + "message": message, + "class": get_class_hierarchy(log_type), + } + + +def log_error( + exc_type: Type[Exception], + exc_value: Exception, + exc_traceback: Any +) -> None: + """Log error information to file.""" + try: + error_info = get_log_info(str(exc_value), exc_type) + + with open(f"errors_{JOB_ID}.log", "a") as log_file: + log_file.write(json.dumps(error_info) + "\n") + + except Exception as e: + sys.stdout.write(f"Logging failed: {e}\n") + + +original_showwarning = warnings.showwarning + + +def handle_warning( + message: Warning, category: Type[Warning], + # these are needed for `warnings.showwarning` + filename=None, lineno=None, + file=None, line=None, +) -> None: + """Log and display warnings.""" + try: + if issubclass(category, CalibrationWarning): + warning_info = get_log_info(str(message), category) + + with open(f"warnings_{JOB_ID}.log", "a") as log_file: + log_file.write(json.dumps(warning_info) + "\n") + + except Exception as e: + sys.stderr.write(f"Warning logging failed: {e}\n") + finally: + # Ensure warning is displayed in notebook. + original_showwarning(message, category, filename, lineno, file, line) + + +# Get IPython shell +shell = IPython.get_ipython() + +# Store original handlers +original_showtraceback = shell.showtraceback + + +# Custom exception handler for IPython +def custom_showtraceback(*args, **kwargs): + etype, value, tb = sys.exc_info() + log_error(etype, value, tb) + original_showtraceback(*args, **kwargs) + if not args and not kwargs: + return etype, value, tb + + +# Install handlers +shell.showtraceback = custom_showtraceback +warnings.showwarning = handle_warning + +# Ignore all warnings globally, except CalibrationWarning +warnings.filterwarnings("ignore") +warnings.simplefilter("default", CalibrationWarning)