Framework hooks are essentially events that are emitted that you are able to "hook" into. If you want to add support for Sentry, which is an exception and error tracking solution, then you want to tie into the Exception Hook. When an exception is encountered it will look for your class in the container and execute it.
For the purposes of learning about Framework Hooks, we will walk through how to implement Sentry into a Masonite application by adding it to the Exception Hook.
The Exception Hook is fired when an exception is thrown in an application. Anytime you would normally see the debug view when developing is when this hook will fire. This hook may not be fired if the server does not boot up because of an exception depending on how far into the container the exception is thrown but any normal application exceptions encountered will fire this hook.
The exception hook to tie into is ExceptionHook. This means that the key in the container should end with ExceptionHook and Masonite will call it when the Exception Hook is thrown. We can load things into the container such as:
Notice here that our key ends with ExceptionHook and that we instantiated the object. Let's explore creating this entirly from scratch.
Let's explore how we can simply add Sentry to our application. This should take about 5 minutes.
Let's create a class called
SentryHook and put it into
class SentryHook:def __init__(self):passdef load(self, app):self._app = app
This should be the basic structure for a hook. All hooks require a load method. This load method will always be passed the application container so it always requires that parameter. From here we can do whatever we need to by making objects from the container.
But for this example we actually don't need the container so we can ignore it.
Ok great so now let's add sentry to our application. You can sign up with Sentry.io which will show you the basic 3 lines you need to add Sentry to any Python project.
This should be the finished hook:
from raven import Clientclient = Client( 'https://874..:firstname.lastname@example.org/1234567')class SentryHook:def __init__(self):passdef load(self, app):client.captureException()
That's it. The key in the client should be available through the Sentry.io dashboard.
Now let's walk through how we can simply tie this into the container so it will be called when an exception is thrown in our project.
We can create a new Service Provider to store our hooks so let's make one.
$ craft provider SentryServiceProvider
This will create a new Service Provider inside
app/providers/SentryServiceProvider.py that looks like:
from masonite.provider import ServiceProviderclass SentryServiceProvider(ServiceProvider):def register(self):passdef boot(self):pass
Now let's just add our hook to it:
from masonite.provider import ServiceProviderfrom ..hooks.sentry import SentryHookclass SentryServiceProvider(ServiceProvider):def register(self):self.app.bind('SentryExceptionHook', SentryHook())def boot(self):pass
And finally add the Service Provider to our
PROVIDERS constant in our
from app.providers.SentryServiceProvider import SentryServiceProvider...PROVIDERS = [# Framework ProvidersAppProvider,...ViewProvider,# Optional Framework ProvidersSassProvider,MailProvider,...# Application ProvidersSentryServiceProvider...]...
That's it! Now everytime an exception is thrown, this will run our SentryHook class that we binded into the container and the exception should pop up in our Sentry.io dashboard.
You can build handlers to handle specific exceptions that are thrown by your application. For example if a TemplateNotFound exception is thrown then you can build a special exception handler to catch that and return a special view or a special debug screen.
Exception handlers are simple classes that have a handle method which accepts the exception thrown:
from masonite.request import Requestclass TemplateNotFoundHandler:def __init__(self, request: Request):self.request = requestdef handle(self, exception):pass
The constructor of all exception handlers are resolved by the container so you can hint any dependencies you need. Once the constructor is resolved then the handle method will be called.
Once the handle method is called then Masonite will continue with the rest of the WSGI logic which is just setting the status code, headers and returning a response.
Now that we have our exception handler we will need to register it in the container using a special naming convention. The naming convention is:
ExceptionNameOfErrorHandler. The name of the exception that we want to be catching is called TemplateNotFound so we will need to bind this into the container like so:
from masonite.provider import ServiceProviderfrom somewhere import TemplateNotFoundHandlerclass UserModelProvider(ServiceProvider):def register(self):self.app.bind('ExceptionTemplateNotFoundHandler', TemplateNotFoundHandler)...
While an exception handler will actually handle the incoming exception, exception listeners are a little different.
Masonite can have several listeners registered with the framework that will listen to specific (or all) exceptions and have that exception passed into it if one is raised. It will then perform any logic it needs until an exception handler finally handles the exception.
A listener is a simple class that requires a list of exceptions to listen for. Here is a simple boilerplate of an exception listener:
from masonite.request import Requestfrom .BaseExceptionListener import BaseExceptionListenerclass ExceptionListener(BaseExceptionListener):listens = [ZeroDivisionError]def __init__(self, request: Request):self.request = requestdef handle(self, exception, file, line):# Perform an action
Note that the
__init__ method of a listener is resolved by the container so feel free to type hint anything you need there.
Finally it requires a
handle method which takes 3 arguments. the
exception that was thrown, the
file that the exception was thrown in and the
line the exception was thrown on.
You can either listen to any number of exceptions or all exceptions by passing in a
* to the
class ExceptionListener(BaseExceptionListener):listens = ['*']# ...
A good use case for this would be Masonite Logging package which uses this to log any exceptions.
You can register an exception listener directly to the container with any service provider. For easy use, you can use a
simple bind to the container which will bind the class to the container with the name of the class as the key:
from some.place import LoggerExceptionListenerclass YourProvider:wsgi = Falsedef register(self):self.app.simple(LoggerExceptionListener)
Your listener will now run whenever an exception occurs that your listener is listening to.