Skip to content

Commit

Permalink
feat(falcon): Update of Falcon Integration (#1733)
Browse files Browse the repository at this point in the history
Update Falcon Integration to support Falcon 3.x

---------

Co-authored-by: bartolootrit <bartolootrit@users.noreply.github.com>
  • Loading branch information
antonpirker and bartolootrit committed Feb 17, 2023
1 parent f21fc0f commit f62c83d
Show file tree
Hide file tree
Showing 8 changed files with 141 additions and 85 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/test-integration-falcon.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["2.7","3.5","3.6","3.7","3.8","3.9","3.10","3.11"]
python-version: ["2.7","3.5","3.6","3.7","3.8","3.9"]
# python3.6 reached EOL and is no longer being supported on
# new versions of hosted runners on Github Actions
# ubuntu-20.04 is the last version that supported python3.6
Expand Down
60 changes: 43 additions & 17 deletions sentry_sdk/integrations/falcon.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,29 @@

from sentry_sdk._types import EventProcessor

# In Falcon 3.0 `falcon.api_helpers` is renamed to `falcon.app_helpers`
# and `falcon.API` to `falcon.App`

try:
import falcon # type: ignore
import falcon.api_helpers # type: ignore

from falcon import __version__ as FALCON_VERSION
except ImportError:
raise DidNotEnable("Falcon not installed")

try:
import falcon.app_helpers # type: ignore

falcon_helpers = falcon.app_helpers
falcon_app_class = falcon.App
FALCON3 = True
except ImportError:
import falcon.api_helpers # type: ignore

falcon_helpers = falcon.api_helpers
falcon_app_class = falcon.API
FALCON3 = False


class FalconRequestExtractor(RequestExtractor):
def env(self):
Expand Down Expand Up @@ -58,16 +73,27 @@ def raw_data(self):
else:
return None

def json(self):
# type: () -> Optional[Dict[str, Any]]
try:
return self.request.media
except falcon.errors.HTTPBadRequest:
# NOTE(jmagnusson): We return `falcon.Request._media` here because
# falcon 1.4 doesn't do proper type checking in
# `falcon.Request.media`. This has been fixed in 2.0.
# Relevant code: https://github.com/falconry/falcon/blob/1.4.1/falcon/request.py#L953
return self.request._media
if FALCON3:

def json(self):
# type: () -> Optional[Dict[str, Any]]
try:
return self.request.media
except falcon.errors.HTTPBadRequest:
return None

else:

def json(self):
# type: () -> Optional[Dict[str, Any]]
try:
return self.request.media
except falcon.errors.HTTPBadRequest:
# NOTE(jmagnusson): We return `falcon.Request._media` here because
# falcon 1.4 doesn't do proper type checking in
# `falcon.Request.media`. This has been fixed in 2.0.
# Relevant code: https://github.com/falconry/falcon/blob/1.4.1/falcon/request.py#L953
return self.request._media


class SentryFalconMiddleware(object):
Expand Down Expand Up @@ -120,7 +146,7 @@ def setup_once():

def _patch_wsgi_app():
# type: () -> None
original_wsgi_app = falcon.API.__call__
original_wsgi_app = falcon_app_class.__call__

def sentry_patched_wsgi_app(self, env, start_response):
# type: (falcon.API, Any, Any) -> Any
Expand All @@ -135,12 +161,12 @@ def sentry_patched_wsgi_app(self, env, start_response):

return sentry_wrapped(env, start_response)

falcon.API.__call__ = sentry_patched_wsgi_app
falcon_app_class.__call__ = sentry_patched_wsgi_app


def _patch_handle_exception():
# type: () -> None
original_handle_exception = falcon.API._handle_exception
original_handle_exception = falcon_app_class._handle_exception

def sentry_patched_handle_exception(self, *args):
# type: (falcon.API, *Any) -> Any
Expand Down Expand Up @@ -170,12 +196,12 @@ def sentry_patched_handle_exception(self, *args):

return was_handled

falcon.API._handle_exception = sentry_patched_handle_exception
falcon_app_class._handle_exception = sentry_patched_handle_exception


def _patch_prepare_middleware():
# type: () -> None
original_prepare_middleware = falcon.api_helpers.prepare_middleware
original_prepare_middleware = falcon_helpers.prepare_middleware

def sentry_patched_prepare_middleware(
middleware=None, independent_middleware=False
Expand All @@ -187,7 +213,7 @@ def sentry_patched_prepare_middleware(
middleware = [SentryFalconMiddleware()] + (middleware or [])
return original_prepare_middleware(middleware, independent_middleware)

falcon.api_helpers.prepare_middleware = sentry_patched_prepare_middleware
falcon_helpers.prepare_middleware = sentry_patched_prepare_middleware


def _exception_leads_to_http_5xx(ex):
Expand Down
1 change: 1 addition & 0 deletions test-requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ jsonschema==3.2.0
pyrsistent==0.16.0 # TODO(py3): 0.17.0 requires python3, see https://github.com/tobgu/pyrsistent/issues/205
executing
asttokens
responses
ipdb
121 changes: 68 additions & 53 deletions tests/integrations/httpx/test_httpx.py
Original file line number Diff line number Diff line change
@@ -1,68 +1,83 @@
import asyncio

import pytest
import httpx
import responses

from sentry_sdk import capture_message, start_transaction
from sentry_sdk.integrations.httpx import HttpxIntegration


def test_crumb_capture_and_hint(sentry_init, capture_events):
@pytest.mark.parametrize(
"httpx_client",
(httpx.Client(), httpx.AsyncClient()),
)
def test_crumb_capture_and_hint(sentry_init, capture_events, httpx_client):
def before_breadcrumb(crumb, hint):
crumb["data"]["extra"] = "foo"
return crumb

sentry_init(integrations=[HttpxIntegration()], before_breadcrumb=before_breadcrumb)
clients = (httpx.Client(), httpx.AsyncClient())
for i, c in enumerate(clients):
with start_transaction():
events = capture_events()

url = "https://httpbin.org/status/200"
if not asyncio.iscoroutinefunction(c.get):
response = c.get(url)
else:
response = asyncio.get_event_loop().run_until_complete(c.get(url))

assert response.status_code == 200
capture_message("Testing!")

(event,) = events
# send request twice so we need get breadcrumb by index
crumb = event["breadcrumbs"]["values"][i]
assert crumb["type"] == "http"
assert crumb["category"] == "httplib"
assert crumb["data"] == {
"url": url,
"method": "GET",
"http.fragment": "",
"http.query": "",
"status_code": 200,
"reason": "OK",
"extra": "foo",
}


def test_outgoing_trace_headers(sentry_init):

url = "http://example.com/"
responses.add(responses.GET, url, status=200)

with start_transaction():
events = capture_events()

if asyncio.iscoroutinefunction(httpx_client.get):
response = asyncio.get_event_loop().run_until_complete(
httpx_client.get(url)
)
else:
response = httpx_client.get(url)

assert response.status_code == 200
capture_message("Testing!")

(event,) = events

crumb = event["breadcrumbs"]["values"][0]
assert crumb["type"] == "http"
assert crumb["category"] == "httplib"
assert crumb["data"] == {
"url": url,
"method": "GET",
"http.fragment": "",
"http.query": "",
"status_code": 200,
"reason": "OK",
"extra": "foo",
}


@pytest.mark.parametrize(
"httpx_client",
(httpx.Client(), httpx.AsyncClient()),
)
def test_outgoing_trace_headers(sentry_init, httpx_client):
sentry_init(traces_sample_rate=1.0, integrations=[HttpxIntegration()])
clients = (httpx.Client(), httpx.AsyncClient())
for i, c in enumerate(clients):
with start_transaction(
name="/interactions/other-dogs/new-dog",
op="greeting.sniff",
# make trace_id difference between transactions
trace_id=f"012345678901234567890123456789{i}",
) as transaction:
url = "https://httpbin.org/status/200"
if not asyncio.iscoroutinefunction(c.get):
response = c.get(url)
else:
response = asyncio.get_event_loop().run_until_complete(c.get(url))

request_span = transaction._span_recorder.spans[-1]
assert response.request.headers[
"sentry-trace"
] == "{trace_id}-{parent_span_id}-{sampled}".format(
trace_id=transaction.trace_id,
parent_span_id=request_span.span_id,
sampled=1,

url = "http://example.com/"
responses.add(responses.GET, url, status=200)

with start_transaction(
name="/interactions/other-dogs/new-dog",
op="greeting.sniff",
trace_id="01234567890123456789012345678901",
) as transaction:
if asyncio.iscoroutinefunction(httpx_client.get):
response = asyncio.get_event_loop().run_until_complete(
httpx_client.get(url)
)
else:
response = httpx_client.get(url)

request_span = transaction._span_recorder.spans[-1]
assert response.request.headers[
"sentry-trace"
] == "{trace_id}-{parent_span_id}-{sampled}".format(
trace_id=transaction.trace_id,
parent_span_id=request_span.span_id,
sampled=1,
)
6 changes: 3 additions & 3 deletions tests/integrations/opentelemetry/test_span_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,14 +212,14 @@ def test_update_span_with_otel_data_http_method2():
"http.status_code": 429,
"http.status_text": "xxx",
"http.user_agent": "curl/7.64.1",
"http.url": "https://httpbin.org/status/403?password=123&username=test@example.com&author=User123&auth=1234567890abcdef",
"http.url": "https://example.com/status/403?password=123&username=test@example.com&author=User123&auth=1234567890abcdef",
}

span_processor = SentrySpanProcessor()
span_processor._update_span_with_otel_data(sentry_span, otel_span)

assert sentry_span.op == "http.server"
assert sentry_span.description == "GET https://httpbin.org/status/403"
assert sentry_span.description == "GET https://example.com/status/403"
assert sentry_span._tags["http.status_code"] == "429"
assert sentry_span.status == "resource_exhausted"

Expand All @@ -229,7 +229,7 @@ def test_update_span_with_otel_data_http_method2():
assert sentry_span._data["http.user_agent"] == "curl/7.64.1"
assert (
sentry_span._data["http.url"]
== "https://httpbin.org/status/403?password=123&username=test@example.com&author=User123&auth=1234567890abcdef"
== "https://example.com/status/403?password=123&username=test@example.com&author=User123&auth=1234567890abcdef"
)


Expand Down
9 changes: 7 additions & 2 deletions tests/integrations/requests/test_requests.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import pytest
import responses

requests = pytest.importorskip("requests")

Expand All @@ -8,17 +9,21 @@

def test_crumb_capture(sentry_init, capture_events):
sentry_init(integrations=[StdlibIntegration()])

url = "http://example.com/"
responses.add(responses.GET, url, status=200)

events = capture_events()

response = requests.get("https://httpbin.org/status/418")
response = requests.get(url)
capture_message("Testing!")

(event,) = events
(crumb,) = event["breadcrumbs"]["values"]
assert crumb["type"] == "http"
assert crumb["category"] == "httplib"
assert crumb["data"] == {
"url": "https://httpbin.org/status/418",
"url": url,
"method": "GET",
"http.fragment": "",
"http.query": "",
Expand Down
Loading

0 comments on commit f62c83d

Please sign in to comment.