{ "cells": [ { "cell_type": "markdown", "id": "0edc9361-109f-4951-80bc-349ae0c78715", "metadata": {}, "source": [ "# Failure reporting in the ESA Climate Toolbox\n", "\n", "The ESA Climate Toolbox provides a mechanism whereby unrecoverable failures can be reported automatically to an external server. The user must explicitly consent to this data sharing before it can be activated.\n", "\n", "This notebook demonstrates the mechanism by setting up a simple local reporting server, which receives failure reports and writes them to a file. The notebook then activates automated failure reporting and deliberately raises an error from the toolbox. Finally, the server’s log file is shown to demonstrate that the report was received and logged." ] }, { "cell_type": "markdown", "id": "d7f94d3f-f27d-4fd1-84be-f526e4fe98b8", "metadata": {}, "source": [ "Import some necessary classes and modules, including the `FailureReporter` class." ] }, { "cell_type": "code", "execution_count": 1, "id": "d177e5bc-48f2-4ce0-88d8-2ef558cba43d", "metadata": {}, "outputs": [], "source": [ "import datetime\n", "import json\n", "import pathlib\n", "from http.server import BaseHTTPRequestHandler\n", "from http.server import HTTPServer\n", "import multiprocessing\n", "from esa_climate_toolbox.util.reporting import FailureReporter" ] }, { "cell_type": "markdown", "id": "22313893-dcc8-4a38-a941-9d21a0d0b167", "metadata": {}, "source": [ "Define a minimal server which will receive failure reports, and start it in the background." ] }, { "cell_type": "code", "execution_count": 2, "id": "a521a9c1-a2a9-49be-83bf-6ac65b1e0e5c", "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "127.0.0.1 - - [04/Dec/2025 11:53:17] \"POST / HTTP/1.1\" 200 -\n" ] } ], "source": [ "log_path = pathlib.Path(\"server-log.txt\")\n", "\n", "class ReportRequestHandler(BaseHTTPRequestHandler):\n", "\n", " def do_POST(self):\n", " self.send_response(200)\n", " self.send_header(\"Content-Type\", \"text/plain\")\n", " self.end_headers()\n", " content_length = int(self.headers.get(\"Content-Length\", 0))\n", " content = self.rfile.read(content_length).decode(\"utf-8\")\n", " data = json.loads(content)\n", " with open(log_path, \"a\") as fh:\n", " fh.write(datetime.datetime.now(datetime.UTC).isoformat() + \"\\n\")\n", " for k, v in data.items():\n", " fh.write(k + \":\\n\")\n", " fh.write(\"\\n\".join(v) if isinstance(v, list) else str(v) + \"\\n\")\n", " fh.write(\"\\n\")\n", " self.wfile.write(\"OK\".encode(\"utf-8\"))\n", "\n", "server = HTTPServer((\"127.0.0.1\", 9898), ReportRequestHandler)\n", "\n", "def start_server():\n", " server.serve_forever()\n", "\n", "log_path.unlink(missing_ok=True)\n", "process = multiprocessing.Process(target=start_server)\n", "process.start()" ] }, { "cell_type": "markdown", "id": "40a7b5e0-8eff-45eb-841e-31a83f805cfb", "metadata": {}, "source": [ "Create and activate a `FailureReporter`, which will automatically detect and report any unhandled exceptions involving the ESA Climate Toolbox. The URL of the server which receives the reports can be passed as a parameter or using the environment variable `ESA_CLIMATE_TOOLBOX_FAILURE_REPORTING_SERVER_URL`. The user is asked for their explicit consent before the reporter is activated." ] }, { "cell_type": "code", "execution_count": 3, "id": "e127d7c3-4f4f-4af3-8b84-c9b2911d5c67", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Please enter YES below to consent to reporting.\n" ] }, { "name": "stdin", "output_type": "stream", "text": [ " YES\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "Activating reporting.\n", "IPython reporting activated.\n" ] } ], "source": [ "reporter = FailureReporter(\"http://localhost:9898\")\n", "reporter.activate()" ] }, { "cell_type": "markdown", "id": "2f5b31e0-7e30-45ce-912c-832808c558f0", "metadata": {}, "source": [ "Now test the functionality. The `FailureReporter` class provides a method called `raise_error` to deliberately trigger an error within the toolbox code for testing purposes." ] }, { "cell_type": "code", "execution_count": 4, "id": "43721246-421e-4211-a099-d72f5e9974a0", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Reporting exception.\n" ] }, { "ename": "Exception", "evalue": "This is an error to test the reporting functionality.", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mException\u001b[0m Traceback (most recent call last)", "Cell \u001b[0;32mIn[4], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mFailureReporter\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mraise_error\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n", "File \u001b[0;32m~/projects/esa-cci/esa-climate-toolbox/esa_climate_toolbox/util/reporting.py:89\u001b[0m, in \u001b[0;36mFailureReporter.raise_error\u001b[0;34m()\u001b[0m\n\u001b[1;32m 87\u001b[0m \u001b[38;5;129m@staticmethod\u001b[39m\n\u001b[1;32m 88\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;21mraise_error\u001b[39m():\n\u001b[0;32m---> 89\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mException\u001b[39;00m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mThis is an error to test the reporting functionality.\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n", "\u001b[0;31mException\u001b[0m: This is an error to test the reporting functionality." ] } ], "source": [ "FailureReporter.raise_error()" ] }, { "cell_type": "markdown", "id": "7bf77c57-58b4-40d9-b5cc-c3f7312fdb1d", "metadata": {}, "source": [ "For comparison, trigger an exception not involving the climate toolbox. This will still be reported in the notebook, but will not be reported to the server." ] }, { "cell_type": "code", "execution_count": 5, "id": "bd434fe3-efae-4686-9260-e5ada15f8180", "metadata": {}, "outputs": [ { "ename": "ZeroDivisionError", "evalue": "division by zero", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mZeroDivisionError\u001b[0m Traceback (most recent call last)", "Cell \u001b[0;32mIn[5], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[38;5;241;43m1\u001b[39;49m\u001b[43m \u001b[49m\u001b[38;5;241;43m/\u001b[39;49m\u001b[43m \u001b[49m\u001b[38;5;241;43m0\u001b[39;49m\n", "\u001b[0;31mZeroDivisionError\u001b[0m: division by zero" ] } ], "source": [ "1 / 0" ] }, { "cell_type": "markdown", "id": "d7986117-b17d-4459-836c-35fcd8f78371", "metadata": {}, "source": [ "Show the contents of the server’s log file. Note that the exception from the climate toolbox is logged, but the other exception is not." ] }, { "cell_type": "code", "execution_count": 6, "id": "2f51a422-db71-4a62-a96e-f7c19847a9b1", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "2025-12-04T10:53:17.960178+00:00\n", "toolbox-version:\n", "1.5.1.dev0\n", "exception-class:\n", "Exception\n", "exception-instance:\n", "Exception('This is an error to test the reporting functionality.')\n", "traceback:\n", " File \"/home/tonio/miniconda3/envs/ect/lib/python3.13/site-packages/IPython/core/interactiveshell.py\", line 3577, in run_code\n", " exec(code_obj, self.user_global_ns, self.user_ns)\n", " ~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", "\n", " File \"/tmp/ipykernel_16423/2859698634.py\", line 1, in \n", " FailureReporter.raise_error()\n", " ~~~~~~~~~~~~~~~~~~~~~~~~~~~^^\n", "\n", " File \"/home/tonio/projects/esa-cci/esa-climate-toolbox/esa_climate_toolbox/util/reporting.py\", line 89, in raise_error\n", " raise Exception(\"This is an error to test the reporting functionality.\")\n", "\n" ] } ], "source": [ "with open(log_path, \"r\") as fh:\n", " for line in fh.readlines():\n", " print(line, end=\"\")" ] }, { "cell_type": "markdown", "id": "df09b124-f222-4ac8-ac35-34c692821a14", "metadata": {}, "source": [ "Shut down the server." ] }, { "cell_type": "code", "execution_count": 7, "id": "0e561164-172c-4914-b0fc-937e005e1492", "metadata": {}, "outputs": [], "source": [ "process.kill()\n", "server.socket.close()" ] }, { "cell_type": "code", "execution_count": null, "id": "79be3ed1-4d2f-44a0-8b4c-91bae1708122", "metadata": {}, "outputs": [], "source": [] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "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.13.1" } }, "nbformat": 4, "nbformat_minor": 5 }