Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make it easy to handle and report exceptions #3893

Closed
MarcSkovMadsen opened this issue Sep 27, 2022 · 5 comments · Fixed by #3896
Closed

Make it easy to handle and report exceptions #3893

MarcSkovMadsen opened this issue Sep 27, 2022 · 5 comments · Fixed by #3896
Labels
type: feature A major new feature

Comments

@MarcSkovMadsen
Copy link
Collaborator

MarcSkovMadsen commented Sep 27, 2022

Request

Provide functionality to make exception handling and reporting easy

Motivation

One of the things that make it hard to make Panel applications work efficiently is exception handling. Panel provides no functionality or guidance to make this easy.

As a consequence I have seen so many apps that just fails without the end user knowing anything about this. The end user does not know if the code actually executed or not because they don't see anything changing. As a consequence the users start distrusting the app and the developers do not get any feedback from users.

The developers can look into the logs and even setup automated monitoring (if they have access). But it requires infrastructure work to get there.

For me this seems just as important as the nice deferred_load functionality you just implemented @philippjfr 👍

Solutions

  1. Document how to handle exceptions inside individual callbacks and report to a TextArea or via a notification.
  2. Document how to build some reusable functionality to handle and report exceptions.
  3. Provide built in functionality to make this easy [PREFERRED]

My proposal

  • enable developers to configure Panel via pn.extension to catch_and_report_exceptions.
    • The catched exceptions should be logged to server and notified to client by default. But this should be configurable.
    • enable developers to pn.bind and/ or pn.depend to a config.exception parameter for custom error handling.

Concept Code

I don't know exactly how to implement this, for now I had to make this a context handler with config.exception_handler():. I believe this can be avoid in the final solution.

But this shows the principle.

notification-principle.mp4
import param
import logging
import panel as pn
from contextlib import contextmanager

class Config(param.Parameterized):
    exception = param.ClassSelector(class_=Exception)

    log_exception_to_server = param.Boolean(default=True)
    log_exception_to_client = param.Boolean(default=True)

    @contextmanager
    def exception_handler(self):
        try:
            yield None
        except Exception as ex:
            self.exception=ex
            if self.log_exception_to_server:
                logging.error("Error", exc_info=ex)
            if self.log_exception_to_client:
                pn.state.notifications.error(f'{ex}')


config = Config()

# ***** General config object above ****
# ***** General app below ****

import panel as pn

pn.extension(notifications=True, template="fast") # catch_and_handle_errors=True

button = pn.widgets.Button(name="Raise Exception").servable()

def _raise_exception(_):
    with config.exception_handler():
        raise ValueError("Some button error")

button.on_click(_raise_exception)

slider = pn.widgets.IntSlider(value=0, start=0, end=5, name="Raise exception on 3").servable()

@pn.depends(value=slider, watch=True)
def _raise_exception_2(value):
    with config.exception_handler():
        if value==3:
            raise ValueError("Some slider error")

logs = pn.widgets.Terminal(height=300, sizing_mode="stretch_width").servable()
@pn.depends(exception=config.param.exception, watch=True)
def _handle_exception(exception):
    logs.write(f"{exception}\n")
@MarcSkovMadsen MarcSkovMadsen added the type: feature A major new feature label Sep 27, 2022
@MarcSkovMadsen MarcSkovMadsen added this to the Version 0.14.0 milestone Sep 27, 2022
@maximlt
Copy link
Member

maximlt commented Sep 27, 2022

Just referencing this Bokeh issue as I found it by chance shortly after reading this issue: bokeh/bokeh#6884 (dating back to 2017 🙃 )

@maximlt maximlt modified the milestones: Version 0.14.0, next Sep 27, 2022
@maximlt
Copy link
Member

maximlt commented Sep 27, 2022

@MarcSkovMadsen given that Panel 0.14.0 is about to be released, I'm moving this to another milestone.

@MarcSkovMadsen
Copy link
Collaborator Author

MarcSkovMadsen commented Sep 28, 2022

This is very close to the working solution I would be able to create. Would make a world of a difference for me.

  • Handles loading errors. By default in the same way as panel serve ... --autoreload does
  • Handles event errors. By default via logging to the server and showing a notification in the client.
  • Allows the user to specify custom handlers for the two above cases.
  • Does not implement setting an exception parameter that the developer could .bind or depends to. That would also be nice.
exception-handling.mp4
import html
import logging
import sys
import traceback

import bokeh
import panel as pn
import param
from panel.pane import HTML
from panel.reactive import state
from panel.util import edit_readonly

# ***** General Functionality ********

def _process_bokeh_event(self, event: 'Event') -> None:
    self._log('received bokeh event %s', event)
    busy = pn.state.busy
    with edit_readonly(pn.state):
        pn.state.busy = True
    try:
        self._process_event(event)
    except Exception as ex:
        if hasattr(pn.state, "onevent_exception_handler"):
            pn.state.onevent_exception_handler(ex)
        else:
            raise Exception from ex
    finally:
        self._log('finished processing bokeh event %s', event)
        with edit_readonly(pn.state):
            pn.state.busy = busy

pn.reactive.Syncable._process_bokeh_event=_process_bokeh_event

def handle_exception(handler, e):
    from bokeh.application.handlers.handler import handle_exception
    
    bokeh.application.handlers.code_runner.handle_exception = handle_exception
    if hasattr(pn.state, "onload_exception_handler"):
        pn.state.onload_exception_handler(handler, e)
    else:
        handle_exception(handler, e)

if not pn.config.autoreload:
    bokeh.application.handlers.code_runner.handle_exception = handle_exception

# ***** Default Exception Handlers *******

def _onload_exception_handler(handler, e):
    logging.error("Error", exc_info=e)

    from panel.pane import HTML
    tb = html.escape(traceback.format_exc())

    # Serve error
    HTML(
        f'<b>{type(e).__name__}</b>: {e}</br><pre style="overflow-y: scroll">{tb}</pre>',
        css_classes=['alert', 'alert-danger'], sizing_mode='stretch_width'
    ).servable()

def _onevent_exception_handler(ex):
    logging.error("Error", exc_info=ex)
    if pn.config.notifications:
        # whether duration should be =0 or >0 can be debated
        pn.state.notifications.send(f"Error. {ex}", background="red", icon="🐛", duration=3000)

# Don't know if pn.state or pn.config is the right place for these
pn.state.onload_exception_handler = _onload_exception_handler
pn.state.onevent_exception_handler = _onevent_exception_handler

# ***** Test App ********

import random

import panel as pn

pn.extension(notifications=True)
pn.state.notifications.servable() # Needed pre panel 0.14

coin = random.randint(0, 1)
if coin:
    raise ValueError(f"Something went wrong. Could not load the application")

button = pn.widgets.Button(name="Raise Exception").servable()

def raise_exception(_=None):
    raise ValueError(f"Something went wrong when clicking the button")

button.on_click(raise_exception)

@hoxbro
Copy link
Member

hoxbro commented Sep 28, 2022

Just to note that I have implemented something like this in holoviz/lumen#322

@MarcSkovMadsen
Copy link
Collaborator Author

Just a note. My solution above also needs to modify pn.reactive.Syncable._process_events to work.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type: feature A major new feature
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants