Repository: getsentry/sentry-python

Issue:
Aiohttp support
I don't see a clear way to integrate with aiohttp (server) just yet. I can still use raven at present, but I imagine sentry-python/sentry-sdk is the proposed way forward longer term?


Discussion:
Hi, we currently don't have any integration with aiohttp-server on the new sdk. If anybody is interested in this feature please just vote with reactions on the OP and we'll prioritize accordingly.
@mike-hart what do you have in mind there? Afaik the old SDK also did not have an aiohttp integration (just an aiohttp transport which we do not believe we will provide going forward).
@mitsuhiko - well, right now I'm just looking for a way to replace the error-reporting middleware I have that currently uses raven. Making sure I don't end up "stuck" using an unsupported/non-current framework. Maybe a middleware isn't the best approach, if not then some documentation or a basic recipe would be a great start.

Right now I don't know enough about sentry-sdk to know whether it'll stall an async process while contacting sentry itself, how to replace its transport if it does, whether it's threaded, whether there will be unexpected or undesired outcomes from using its defaults. All I really want is a mechanism to report uncaught exceptions (basically something that would result in a 5xx status code) to Sentry for my service.
@mike-hart calling any of the capture methods on the hub/client/transport are non blocking operations. They enqueue into a background queue.
@mitsuhiko - I note in https://docs.sentry.io/platforms/python/ that there's mention of `httplib_request` hints, is this the mechanism I should be using to add additional information to the reported exception for a service?

Raven had a semi-standard structure (the http context) you'd build to feed to the capture so that you knew URL, query params etc. Is this is the same thing, and are there any documented examples of how to build something similar for `sentry-sdk`? Or am I going down completely the wrong path?
@mike-hart i'm not entirely sure what you mean but i would propose to look at the sanic, flask and django integrations to see how this works. Integrations are pretty straightforward and can emit any kind of data. The structure from raven is very much still there, we just simplified it.
Eg: this is how sanic works: https://github.com/getsentry/sentry-python/blob/master/sentry_sdk/integrations/sanic.py
Thanks, I'll take a further look at the integrations and see if I can come up with something. The mechanism seems a little... invasive (at least when looking at sanic, django, and flask integrations, where they appear to monkey-patch). Is there a reason (say, in sanic's case) not to tie in to the inbuilt error-handler mechanism for example?

Anyway, thanks for the pointers. I'll have a further play around and see if I can get something together, sorry for taking up so much of your time.
We intentionally patch the core handlers now as there were too many issues with working with the individual error handlers that are already in place. Both in reliability and usability. More importantly we already in the past had to do invasive patching for breadcrumbs so the behavior was wildly inconsistent.

Generally the builtin error handlers are typically compromising so that they can still render error pages which swallows quite a few errors we are still interested in.
So this seems to have a lot of support. Is this something that used to work just fine with raven for you? Is there a third-party package that enabled this for you?
@untitaker if your question is what do people use within `aiohttp`, I can answer for myself - [raven-aiohttp](https://github.com/getsentry/raven-aiohttp) and hope `sentry-python` will support `aiohttp` as it is critical feature and it's absence prevents me from using `sentry-python`.
@decaz how does its absence *prevent* you from using the new SDK? Surely spawning threads like the new SDK does would still work even if it's not the optimal way to do things? I haven't tried it but it should work out of the box.

Note that as far as I can tell this is only about the transport used internally. Perhaps I don't have the full context as to why people use raven-aiohttp in the first place
I think people use raven-aiohttp because new SDK does not contain integration with aiohttp. And yes, this is also about the transport.
@untitaker - For me, it would just be nice if aiohttp (and related asyncio-based systems) had some kind of first-class support is all. Instead of each of us coming up with out own (possibly wrong) recipe to integrate Sentry.

We're not _prevented_ from using sentry-python, we just don't necessarily have the confidence to integrate something as invasively as this appears to require in such a manner that it's done properly. Doing it properly is pretty high on my list when it comes to adding an exception handling framework to my services, especially if that service might be handling hundreds of open connections in-process, and doubly-so to avoid reporting on the wrong async stream/connection as being the cause.
@mike-hart I understand all of your points. I am just very confused about statements like this:

>I think people use raven-aiohttp because new SDK does not contain integration with aiohttp.

raven-aiohttp has nothing but a custom transport.


> raven-aiohttp has nothing but a custom transport.

You might want to ask @decaz more about that in particular, but my take on it is this:
That transport is the way to avoid the Sentry client stalling the eventloop. I suspect most people just add the `client.captureException` call in a piece of middleware post-request and be done with it. If something more specific is needed by them then I guess they would go from there (doing something more aggressive), adding context details, breadcrumbs etc. etc.
I meant that if a user who is using raven-aiohttp meets the SDK and sees that it does not have integration with aiohttp, then he probably will not think about transport or anything else, for him it looks like a library without support aiohttp, nothing more.
>That transport is the way to avoid the Sentry client stalling the eventloop

The eventloop is not stalled either way unless you explicitly use a blocking transport, which this SDK does not provide. The default transport in this SDK just spawns a new thread which should not be a problem even with async code.

>I suspect most people just add the client.captureException call in a piece of middleware post-request and be done with it. If something more specific is needed by them then I guess they would go from there (doing something more aggressive), adding context details, breadcrumbs etc. etc.

Exactly. I don't know of prior art that does this out of the box for aiohttp.
I guess part of this is that people love to hate threads 😉 

More seriously though, some proper integration would be lovely. There are a number of asyncio-based frameworks now becoming much more popular, and getting it right would be useful. aiohttp and perhaps starlette might be good ones to start on so that people know how to do it correctly.

Resolving patch:
diff --git a/sentry_sdk/integrations/aiohttp.py b/sentry_sdk/integrations/aiohttp.py
new file mode 100644
--- /dev/null
+++ b/sentry_sdk/integrations/aiohttp.py
@@ -0,0 +1,101 @@
+import sys
+import weakref
+
+from sentry_sdk._compat import reraise
+from sentry_sdk.hub import Hub
+from sentry_sdk.integrations import Integration
+from sentry_sdk.integrations.logging import ignore_logger
+from sentry_sdk.integrations._wsgi_common import _filter_headers
+from sentry_sdk.utils import capture_internal_exceptions, event_from_exception
+
+import asyncio
+from aiohttp.web import Application, HTTPError
+
+
+class AioHttpIntegration(Integration):
+    identifier = "aiohttp"
+
+    @staticmethod
+    def setup_once():
+        if sys.version_info < (3, 7):
+            # We better have contextvars or we're going to leak state between
+            # requests.
+            raise RuntimeError(
+                "The aiohttp integration for Sentry requires Python 3.7+"
+            )
+
+        ignore_logger("aiohttp.server")
+
+        old_handle = Application._handle
+
+        async def sentry_app_handle(self, request, *args, **kwargs):
+            async def inner():
+                hub = Hub.current
+                if hub.get_integration(AioHttpIntegration) is None:
+                    return old_handle(self, request, *args, **kwargs)
+
+                weak_request = weakref.ref(request)
+
+                with Hub(Hub.current) as hub:
+                    with hub.configure_scope() as scope:
+                        scope.add_event_processor(_make_request_processor(weak_request))
+
+                    try:
+                        response = await old_handle(self, request)
+                    except HTTPError:
+                        raise
+                    except Exception:
+                        reraise(*_capture_exception(hub))
+
+                    return response
+
+            return await asyncio.create_task(inner())
+
+        Application._handle = sentry_app_handle
+
+
+def _make_request_processor(weak_request):
+    def aiohttp_processor(event, hint):
+        request = weak_request()
+        if request is None:
+            return event
+
+        with capture_internal_exceptions():
+            # TODO: Figure out what to do with request body. Methods on request
+            # are async, but event processors are not.
+
+            request_info = event.setdefault("request", {})
+
+            if "url" not in request_info:
+                request_info["url"] = "%s://%s%s" % (
+                    request.scheme,
+                    request.host,
+                    request.path,
+                )
+
+            if "query_string" not in request_info:
+                request_info["query_string"] = request.query_string
+
+            if "method" not in request_info:
+                request_info["method"] = request.method
+
+            if "env" not in request_info:
+                request_info["env"] = {"REMOTE_ADDR": request.remote}
+
+            if "headers" not in request_info:
+                request_info["headers"] = _filter_headers(dict(request.headers))
+
+        return event
+
+    return aiohttp_processor
+
+
+def _capture_exception(hub):
+    exc_info = sys.exc_info()
+    event, hint = event_from_exception(
+        exc_info,
+        client_options=hub.client.options,
+        mechanism={"type": "aiohttp", "handled": False},
+    )
+    hub.capture_event(event, hint=hint)
+    return exc_info


Test patch:
diff --git a/tests/integrations/aiohttp/__init__.py b/tests/integrations/aiohttp/__init__.py
new file mode 100644
--- /dev/null
+++ b/tests/integrations/aiohttp/__init__.py
@@ -0,0 +1,3 @@
+import pytest
+
+aiohttp = pytest.importorskip("aiohttp")
diff --git a/tests/integrations/aiohttp/test_aiohttp.py b/tests/integrations/aiohttp/test_aiohttp.py
new file mode 100644
--- /dev/null
+++ b/tests/integrations/aiohttp/test_aiohttp.py
@@ -0,0 +1,55 @@
+from aiohttp import web
+
+from sentry_sdk.integrations.aiohttp import AioHttpIntegration
+
+
+async def test_basic(sentry_init, aiohttp_client, loop, capture_events):
+    sentry_init(integrations=[AioHttpIntegration()])
+
+    async def hello(request):
+        1 / 0
+
+    app = web.Application()
+    app.router.add_get("/", hello)
+
+    events = capture_events()
+
+    client = await aiohttp_client(app)
+    resp = await client.get("/")
+    assert resp.status == 500
+
+    event, = events
+
+    exception, = event["exception"]["values"]
+    assert exception["type"] == "ZeroDivisionError"
+    request = event["request"]
+    host = request["headers"]["Host"]
+
+    assert request["env"] == {"REMOTE_ADDR": "127.0.0.1"}
+    assert request["method"] == "GET"
+    assert request["query_string"] == ""
+    assert request["url"] == f"http://{host}/"
+    assert request["headers"] == {
+        "Accept": "*/*",
+        "Accept-Encoding": "gzip, deflate",
+        "Host": host,
+        "User-Agent": request["headers"]["User-Agent"],
+    }
+
+
+async def test_403_not_captured(sentry_init, aiohttp_client, loop, capture_events):
+    sentry_init(integrations=[AioHttpIntegration()])
+
+    async def hello(request):
+        raise web.HTTPForbidden()
+
+    app = web.Application()
+    app.router.add_get("/", hello)
+
+    events = capture_events()
+
+    client = await aiohttp_client(app)
+    resp = await client.get("/")
+    assert resp.status == 403
+
+    assert not events