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

fix(sanic): support version 21.x (#2240) #2379

Merged
merged 28 commits into from
Jun 17, 2021
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
bc71dfc
fix(sanic): support latest version (#2240)
nizox Apr 29, 2021
dea009e
add a release note
nizox May 3, 2021
17595e3
add more tests
nizox May 4, 2021
1a4ab2e
More tests
nizox May 5, 2021
8091f4e
Merge branch 'master' into sanic-21
nizox May 5, 2021
648571d
Merge branch 'master' into sanic-21
P403n1x87 May 6, 2021
5b83784
rewrite release note
nizox May 6, 2021
e389068
Merge branch 'master' into sanic-21
Yun-Kim May 7, 2021
a2bedd3
Merge branch 'master' into sanic-21
Kyle-Verhoog May 7, 2021
3109fef
Merge branch 'master' into sanic-21
brettlangdon May 19, 2021
c3792bc
Bump pytest-sanic to 1.7.1
nizox May 20, 2021
f432f49
Merge branch 'master' into sanic-21
Yun-Kim May 20, 2021
0b0d273
Fix tests & compat with older versions
nizox May 20, 2021
4072192
Merge branch 'master' into sanic-21
nizox May 21, 2021
294ef9d
Merge branch 'master' into sanic-21
Kyle-Verhoog May 24, 2021
f7f3982
Lock deps versions in tox
nizox May 25, 2021
acd92dc
Merge branch 'master' into sanic-21
brettlangdon May 25, 2021
8f3bcca
Merge branch 'master' into sanic-21
mergify[bot] May 25, 2021
6b7308b
Merge branch 'master' into sanic-21
nizox May 27, 2021
ee7660e
Merge branch 'master' into sanic-21
mergify[bot] May 27, 2021
b493b69
Merge branch 'master' into sanic-21
mergify[bot] May 28, 2021
18762a9
Merge branch 'master' into sanic-21
mergify[bot] May 28, 2021
1c3d97b
Merge branch 'master' into sanic-21
mergify[bot] May 28, 2021
a62caa7
Merge branch 'master' into sanic-21
nizox Jun 15, 2021
37abff0
Merge branch 'master' into sanic-21
mergify[bot] Jun 15, 2021
70067b4
Merge branch 'master' into sanic-21
nizox Jun 16, 2021
cc4b600
Merge branch 'master' into sanic-21
mergify[bot] Jun 16, 2021
491bf63
Merge branch 'master' into sanic-21
brettlangdon Jun 17, 2021
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
122 changes: 82 additions & 40 deletions ddtrace/contrib/sanic/patch.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from ddtrace import config
from ddtrace.constants import ANALYTICS_SAMPLE_RATE_KEY
from ddtrace.ext import SpanTypes
from ddtrace.pin import Pin
from ddtrace.utils.wrappers import unwrap as _u
from ddtrace.vendor import wrapt
from ddtrace.vendor.wrapt import wrap_function_wrapper as _w
Expand All @@ -18,34 +19,37 @@

config._add("sanic", dict(_default_service="sanic", distributed_tracing=True))

SANIC_PRE_21 = None


def update_span(span, response):
if isinstance(response, sanic.response.BaseHTTPResponse):
status_code = response.status
response_headers = response.headers
else:
# invalid response causes ServerError exception which must be handled
status_code = 500
response_headers = None
trace_utils.set_http_meta(span, config.sanic, status_code=status_code, response_headers=response_headers)


def _wrap_response_callback(span, callback):
# wrap response callbacks (either sync or async function) to set span tags
# based on response and finish span before returning response

def update_span(response):
if isinstance(response, sanic.response.BaseHTTPResponse):
status_code = response.status
response_headers = response.headers
else:
# invalid response causes ServerError exception which must be handled
status_code = 500
response_headers = None
trace_utils.set_http_meta(span, config.sanic, status_code=status_code, response_headers=response_headers)
span.finish()
Yun-Kim marked this conversation as resolved.
Show resolved Hide resolved
# Only for sanic 20 and older
# Wrap response callbacks (either sync or async function) to set HTTP
# response span tags

@wrapt.function_wrapper
def wrap_sync(wrapped, instance, args, kwargs):
r = wrapped(*args, **kwargs)
response = args[0]
update_span(response)
update_span(span, response)
return r

@wrapt.function_wrapper
async def wrap_async(wrapped, instance, args, kwargs):
r = await wrapped(*args, **kwargs)
response = args[0]
update_span(response)
update_span(span, response)
return r

if asyncio.iscoroutinefunction(callback):
Expand All @@ -54,6 +58,18 @@ async def wrap_async(wrapped, instance, args, kwargs):
return wrap_sync(callback)


async def patch_request_respond(wrapped, instance, args, kwargs):
# Only for sanic 21 and newer
# Wrap the framework response to set HTTP response span tags
response = await wrapped(*args, **kwargs)
pin = Pin._find(instance.ctx)
if pin is not None and pin.enabled():
span = pin.tracer.current_span()
if span is not None:
update_span(span, response)
return response


def _get_path(request):
"""Get path and replace path parameter values with names if route exists."""
path = request.path
Expand All @@ -66,57 +82,83 @@ def _get_path(request):
return path


async def patch_run_request_middleware(wrapped, instance, args, kwargs):
# Set span resource from the framework request
request = args[0]
pin = Pin._find(request.ctx)
if pin is not None and pin.enabled():
span = pin.tracer.current_span()
if span is not None:
span.resource = "{} {}".format(request.method, _get_path(request))
return await wrapped(*args, **kwargs)


def patch():
"""Patch the instrumented methods."""
global SANIC_PRE_21

if getattr(sanic, "__datadog_patch", False):
return
setattr(sanic, "__datadog_patch", True)

SANIC_PRE_21 = sanic.__version__[:2] < "21"

_w("sanic", "Sanic.handle_request", patch_handle_request)
_w("sanic", "Sanic._run_request_middleware", patch_run_request_middleware)
if not SANIC_PRE_21:
_w(sanic.request, "Request.respond", patch_request_respond)


def unpatch():
"""Unpatch the instrumented methods."""
_u(sanic.Sanic, "handle_request")
_u(sanic.Sanic, "_run_request_middleware")
if not SANIC_PRE_21:
_u(sanic.request.Request, "respond")
if not getattr(sanic, "__datadog_patch", False):
return
setattr(sanic, "__datadog_patch", False)


async def patch_handle_request(wrapped, instance, args, kwargs):
"""Wrapper for Sanic.handle_request"""
request = kwargs.get("request", args[0])
write_callback = kwargs.get("write_callback", args[1])
stream_callback = kwargs.get("stream_callback", args[2])

def unwrap(request, write_callback=None, stream_callback=None, **kwargs):
return request, write_callback, stream_callback, kwargs

request, write_callback, stream_callback, new_kwargs = unwrap(*args, **kwargs)

if request.scheme not in ("http", "https"):
return await wrapped(request, write_callback, stream_callback, **kwargs)
return await wrapped(*args, **kwargs)

resource = "{} {}".format(request.method, _get_path(request))
pin = Pin()
pin.onto(request.ctx)

headers = request.headers.copy()

trace_utils.activate_distributed_headers(ddtrace.tracer, int_config=config.sanic, request_headers=headers)

span = ddtrace.tracer.trace(
with pin.tracer.trace(
"sanic.request",
service=trace_utils.int_service(None, config.sanic),
resource=resource,
span_type=SpanTypes.WEB,
)
sample_rate = config.sanic.get_analytics_sample_rate(use_global_config=True)
if sample_rate is not None:
span.set_tag(ANALYTICS_SAMPLE_RATE_KEY, sample_rate)

method = request.method
url = "{scheme}://{host}{path}".format(scheme=request.scheme, host=request.host, path=request.path)
query_string = request.query_string
if isinstance(query_string, bytes):
query_string = query_string.decode()
trace_utils.set_http_meta(span, config.sanic, method=method, url=url, query=query_string, request_headers=headers)

if write_callback is not None:
write_callback = _wrap_response_callback(span, write_callback)
if stream_callback is not None:
stream_callback = _wrap_response_callback(span, stream_callback)

return await wrapped(request, write_callback, stream_callback, **kwargs)
) as span:
sample_rate = config.sanic.get_analytics_sample_rate(use_global_config=True)
if sample_rate is not None:
span.set_tag(ANALYTICS_SAMPLE_RATE_KEY, sample_rate)

method = request.method
url = "{scheme}://{host}{path}".format(scheme=request.scheme, host=request.host, path=request.path)
query_string = request.query_string
if isinstance(query_string, bytes):
query_string = query_string.decode()
trace_utils.set_http_meta(
span, config.sanic, method=method, url=url, query=query_string, request_headers=headers
)

if write_callback is not None:
new_kwargs["write_callback"] = _wrap_response_callback(span, write_callback)
if stream_callback is not None:
new_kwargs["stream_callback"] = _wrap_response_callback(span, stream_callback)

return await wrapped(request, **new_kwargs)
4 changes: 4 additions & 0 deletions releasenotes/notes/fix-sanic-21-2d24b817e010ed84.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
fixes:
- |
sanic: update instrumentation to support the latest framework version.
nizox marked this conversation as resolved.
Show resolved Hide resolved
10 changes: 7 additions & 3 deletions tests/contrib/sanic/test_sanic.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import pytest
from sanic import Sanic
from sanic.config import DEFAULT_CONFIG
from sanic.exceptions import ServerError
from sanic.response import json
from sanic.response import stream
Expand Down Expand Up @@ -38,8 +39,11 @@ async def _response_text(response):
return resp_text


@pytest.yield_fixture
@pytest.fixture
def app(tracer):
# Sanic 20.12 and newer prevent loading multiple applications
# with the same name if register is True.
DEFAULT_CONFIG["REGISTER"] = False
app = Sanic(__name__)

@tracer.wrap()
Expand Down Expand Up @@ -283,7 +287,7 @@ async def test_multiple_requests(tracer, client, test_spans):
async def test_invalid_response_type_str(tracer, client, test_spans):
response = await client.get("/invalid")
assert _response_status(response) == 500
assert await _response_text(response) == "Invalid response type"
assert (await _response_text(response)).startswith("Invalid response type")

spans = test_spans.pop_traces()
assert len(spans) == 1
Expand All @@ -301,7 +305,7 @@ async def test_invalid_response_type_str(tracer, client, test_spans):
async def test_invalid_response_type_empty(tracer, client, test_spans):
response = await client.get("/empty")
assert _response_status(response) == 500
assert await _response_text(response) == "Invalid response type"
assert (await _response_text(response)).startswith("Invalid response type")

spans = test_spans.pop_traces()
assert len(spans) == 1
Expand Down
4 changes: 3 additions & 1 deletion tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ envlist =
pyramid_contrib{,_autopatch}-py{27,35,36,37,38,39}-pyramid{17,18,19,110,}-webtest
redis_contrib-py{27,35,36,37,38,39}-redis{210,30,32,33,34,35,}
rediscluster_contrib-py{27,35,36,37,38,39}-rediscluster{135,136,200,}-redis210
sanic_contrib-py{37,38,39}-sanic{1906,1909,1912,2003,2006}
sanic_contrib-py{37,38,39}-sanic{1906,1909,1912,2003,2006,2103}
nizox marked this conversation as resolved.
Show resolved Hide resolved
sqlite3_contrib-py{27,35,36,37,38,39}-sqlite3
tornado_contrib-py{27,35,36,37,38,39}-tornado{44,45}
tornado_contrib-py{37,38,39}-tornado{50,51,60,}
Expand Down Expand Up @@ -311,6 +311,8 @@ deps =
sanic1912: sanic~=19.12.0
sanic2003: sanic~=20.3.0
sanic2006: sanic~=20.6.0
sanic2103: sanic~=21.3.0
sanic2103: httpx
sanic: sanic
sqlalchemy: sqlalchemy
sslmodules3: aiohttp
Expand Down