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

feat: added fastapi integration #26

Merged
merged 2 commits into from
Nov 27, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ Python errors Catcher module for [Hawk.so](https://hawk.so).

Register an account and get a new project token.

If you want to connect specific frameworks see [Flask integration](./docs/flask.md), [FastAPI integration](./docs/fastapi.md).

### Install module

Install `hawkcatcher` from PyPI.
Expand Down Expand Up @@ -104,7 +106,7 @@ Parameters:

## Requirements

- Python \>= 3.5
- Python \>= 3.9
- requests

## Links
Expand Down
100 changes: 100 additions & 0 deletions docs/fastapi.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# Flask integration

This extension adds support for the [FastAPI](https://fastapi.tiangolo.com/) web framework.

## Installation

```bash
pip install hawkcatcher[fastapi]
```

import Catcher module to your project.

```python
from hawkcatcher.modules.fastapi import HawkFastapi
```

```python
app = FastAPI()

hawk = HawkFastapi(
'app_instance': app,
'token': '1234567-abcd-8901-efgh-123456789012'
)
```

Now all global fastapi errors would be sent to Hawk.

### Try-except

If you want to catch errors in try-except blocks see [this](../README.md#try-except)

## Manual sending

You can send any error to Hawk. See [this](../README.md#manual-sending)

### Event context

See [this](../README.md#event-context)

### Affected user

See [this](../README.md#affected-user)

### Addons

When some event handled by FastAPI Catcher, it adds some addons to the event data for Hawk.

| name | type | description |
| --------- | ---- | --------------- |
| `url` | str | Request URL |
| `method` | str | Request method |
| `headers` | dict | Request headers |
| `cookies` | dict | Request cookies |
| `params` | dict | Request params |

## Init params

To init Hawk Catcher just pass a project token and FastAPI app instance.

```python
app = FastAPI()

hawk = HawkFastapi(
'app_instance': app,
'token': '1234567-abcd-8901-efgh-123456789012'
)
```

### Additional params

If you need to use custom Hawk server then pass a dictionary with params.

```python
hawk = HawkFastapi({
'app_instance': app,
'token': '1234567-abcd-8901-efgh-123456789012',
'collector_endpoint': 'https://<id>.k1.hawk.so',
})
```

Parameters:

| name | type | required | description |
| -------------------- | ------------------------- | ------------ | ------------------------------------------------------------------------------ |
| `app_instance` | FastAPI | **required** | FastAPI app instance |
| `token` | str | **required** | Your project's Integration Token |
| `release` | str | optional | Release name for Suspected Commits feature |
| `collector_endpoint` | string | optional | Collector endpoint for sending event to |
| `context` | dict | optional | Additional context to be send with every event |
| `before_send` | Callable[[dict], None] | optional | This Method allows you to filter any data you don't want sending to Hawk |
| `set_user` | Callable[[Request], User] | optional | This Method allows you to set user for every request by fastapi request object |
| `with_addons` | bool | optional | Add framework addons to event data |
neSpecc marked this conversation as resolved.
Show resolved Hide resolved

## Requirements

See [this](../README.md#requirements)

And for fastapi you need:

- fastapi
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ name = "hawkcatcher"
authors = [{ name = "CodeX Team", email = "team@codex.so" }]
description = "Python errors Catcher module for Hawk."
readme = "README.md"
requires-python = ">=3.5"
requires-python = ">=3.9"
classifiers = [
"Intended Audience :: Developers",
"Topic :: Software Development :: Bug Tracking",
Expand All @@ -19,6 +19,7 @@ classifiers = [
]
[project.optional-dependencies]
flask = ["flask"]
fastapi = ["starlette"]
[tool.hatch.version]
path = "src/hawkcatcher/__init__.py"
[project.urls]
Expand Down
29 changes: 24 additions & 5 deletions src/hawkcatcher/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

import hawkcatcher
from hawkcatcher.errors import InvalidHawkToken
from hawkcatcher.types import HawkCatcherSettings
from hawkcatcher.types import HawkCatcherSettings, Addons, User


class Hawk:
Expand Down Expand Up @@ -49,7 +49,7 @@ def get_params(settings) -> Union[HawkCatcherSettings, None]:
'context': settings.get('context', None)
}

def handler(self, exc_cls: type, exc: Exception, tb: traceback, context=None, user=None, addons=None):
def handler(self, exc_cls: type, exc: Exception, tb: traceback, context=None, user=None):
"""
Catch, prepare and send error

Expand All @@ -70,6 +70,7 @@ def handler(self, exc_cls: type, exc: Exception, tb: traceback, context=None, us
ex_message = traceback.format_exception_only(exc_cls, exc)[-1]
ex_message = ex_message.strip()
backtrace = tb and Hawk.parse_traceback(tb)
addons = self._set_addons()

if not (type(context) is dict):
context = {
Expand Down Expand Up @@ -108,7 +109,7 @@ def send_to_collector(self, event):
except Exception as e:
print('[Hawk] Can\'t send error cause of %s' % e)

def send(self, event: Exception = None, context=None, user=None, addons=None):
def send(self, event: Exception = None, context=None, user=None):
"""
Method for manually send error to Hawk
:param event: event to send
Expand All @@ -119,9 +120,27 @@ def send(self, event: Exception = None, context=None, user=None, addons=None):
exc_cls, exc, tb = sys.exc_info()

if event is not None:
self.handler(type(event), event, tb, context, user, addons)
self.handler(type(event), event, tb, context, user)
else:
self.handler(exc_cls, exc, tb, context, user, addons)
self.handler(exc_cls, exc, tb, context, user)

def _set_addons(self) -> Union[Addons, None]:
"""
Set framework addons to send with error
"""
return None

def _set_user(self, request) -> Union[User, None]:
"""
Set user information by set_user callback
"""
user = None

if self.params.get('set_user') is not None:
user = self.params['set_user'](request)

return user


@staticmethod
def parse_traceback(tb):
Expand Down
13 changes: 13 additions & 0 deletions src/hawkcatcher/modules/fastapi/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from .fastapi import HawkFastapi
from .types import HawkCatcherSettings
from .types import FastapiSettings

hawk = HawkFastapi()


def init(*args, **kwargs):
hawk.init(*args, **kwargs)


def send(*args, **kwargs):
hawk.send(*args, **kwargs)
96 changes: 96 additions & 0 deletions src/hawkcatcher/modules/fastapi/fastapi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
from hawkcatcher.types import HawkCatcherSettings
from ...core import Hawk
from hawkcatcher.modules.fastapi.types import FastapiSettings, FastapiAddons
from starlette.types import ASGIApp, Receive, Scope, Send
from starlette.requests import Request
from starlette.middleware.base import BaseHTTPMiddleware
from typing import Union
from hawkcatcher.errors import ModuleError
import asyncio
from contextvars import ContextVar
from fastapi import Request
import asyncio

# Variable for saving current request, work with async tasks
current_request: ContextVar[Union[Request, None]] = ContextVar("current_request", default=None)


# class for catching errors in fastapi app
class HawkFastapi(Hawk):
params: FastapiSettings = {}

def init(self, settings: Union[str, FastapiSettings] = None):
self.params = self.get_params(settings)

if self.params.get('app_instance') is None:
raise ModuleError('Fastapi app instance not passed to HawkFastapi')

self.params.get('app_instance').add_middleware(self._get_starlette_middleware())

def _get_starlette_middleware(self):
"""
Create middleware for starlette to identify request exception and storing current request for manual sending
"""

# Create method to use it in middleware class with Hawk class context
def send_func(err):
return self.send(err)

class StarletteMiddleware:
def __init__(self, app: ASGIApp):
self.app = app

async def __call__(self, scope: Scope, receive: Receive, send: Send):
if scope["type"] == "http":
request = Request(scope, receive, send)
current_request.set(request)
try:
await self.app(scope, receive, send)
except Exception as err:
return send_func(err)
else:
await self.app(scope, receive, send)
return None

return StarletteMiddleware

def send(self, event: Exception = None, context=None, user=None):
"""
Method for manually send error to Hawk, make it async for starlette
:param exception: exception
:param context: additional context to send with error
:param user: user information who faced with that event
"""

request = current_request.get()

if user is None and request is not None:
user = self._set_user(request)

return super().send(event, context, user)

def _set_addons(self) -> Union[FastapiAddons, None]:
request = current_request.get()

if request is None:
return None

return {
'url': str(request.url),
'method': request.method,
'headers': dict(request.headers),
'cookies': dict(request.cookies),
'params': dict(request.query_params)
}

@staticmethod
def get_params(settings) -> FastapiSettings | None:
hawk_params = Hawk.get_params(settings)

if hawk_params is None:
return None

return {
**hawk_params,
'app_instance': settings.get('app_instance'),
}
19 changes: 19 additions & 0 deletions src/hawkcatcher/modules/fastapi/types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from hawkcatcher.types import HawkCatcherSettings, User, Addons
from typing import Callable, TypedDict
from starlette.applications import Starlette
from fastapi import Request

class FastapiAddons(TypedDict):
url: str # url of request
method: str # method of request
headers: dict # headers of request
cookies: dict # cookies of request
params: dict # request params

class Addons(Addons):
fastapi: FastapiAddons

class FastapiSettings(HawkCatcherSettings[Request]):
"""Settings for Fastapi catcher for errors tracking"""

app_instance: Starlette # Fastapi app instance to add catching
Loading