Skip to content

Commit bde2b52

Browse files
Add support for Sanic.TouchUp metaclass (#607)
* Add support for Sanic.TouchUp metaclass Starting in version 21.9.0 sanic added a TouchUp metaclass that rewrites methods on the Sanic class effectively undoing our instrumentation wrapping. This adds back our instrumentation wrapping. Co-authored-by: TimPansino <[email protected]> * Change Sanic "test app" name to "test-app" Sanic app names cannot have spaces. * Fix sanic warning * Fix sanic testing setup * Fix sanic response streaming * Fix sanic testing init logic * Add missing py39 tests for sanic Co-authored-by: TimPansino <[email protected]> Co-authored-by: Tim Pansino <[email protected]>
1 parent 2633a4d commit bde2b52

File tree

5 files changed

+114
-69
lines changed

5 files changed

+114
-69
lines changed

newrelic/config.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2544,6 +2544,9 @@ def _process_module_builtin_defaults():
25442544

25452545
_process_module_definition("sanic.app", "newrelic.hooks.framework_sanic", "instrument_sanic_app")
25462546
_process_module_definition("sanic.response", "newrelic.hooks.framework_sanic", "instrument_sanic_response")
2547+
_process_module_definition(
2548+
"sanic.touchup.service", "newrelic.hooks.framework_sanic", "instrument_sanic_touchup_service"
2549+
)
25472550

25482551
_process_module_definition("aiohttp.wsgi", "newrelic.hooks.framework_aiohttp", "instrument_aiohttp_wsgi")
25492552
_process_module_definition("aiohttp.web", "newrelic.hooks.framework_aiohttp", "instrument_aiohttp_web")

newrelic/hooks/framework_sanic.py

Lines changed: 65 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,12 @@
1515
import sys
1616
from inspect import isawaitable
1717

18-
from newrelic.api.web_transaction import web_transaction
19-
from newrelic.api.transaction import current_transaction
20-
from newrelic.api.function_trace import function_trace, FunctionTrace
18+
from newrelic.api.function_trace import FunctionTrace, function_trace
2119
from newrelic.api.time_trace import notice_error
22-
from newrelic.common.object_wrapper import (wrap_function_wrapper,
23-
function_wrapper)
20+
from newrelic.api.transaction import current_transaction
21+
from newrelic.api.web_transaction import web_transaction
2422
from newrelic.common.object_names import callable_name
23+
from newrelic.common.object_wrapper import function_wrapper, wrap_function_wrapper
2524

2625

2726
def _bind_add(uri, methods, handler, *args, **kwargs):
@@ -36,19 +35,20 @@ def _nr_wrapper_handler_(wrapped, instance, args, kwargs):
3635
return wrapped(*args, **kwargs)
3736

3837
name = callable_name(wrapped)
39-
view_class = getattr(wrapped, 'view_class', None)
38+
view_class = getattr(wrapped, "view_class", None)
4039
view = view_class or wrapped
4140
if view_class:
4241
try:
4342
method = args[0].method.lower()
44-
name = callable_name(view_class) + '.' + method
43+
name = callable_name(view_class) + "." + method
4544
view = getattr(view_class, method)
4645
except:
4746
pass
48-
47+
4948
transaction.set_transaction_name(name, priority=3)
5049
import sanic
51-
transaction.add_framework_info(name='Sanic', version=sanic.__version__)
50+
51+
transaction.add_framework_info(name="Sanic", version=sanic.__version__)
5252

5353
with FunctionTrace(name=name, source=view):
5454
return wrapped(*args, **kwargs)
@@ -60,7 +60,7 @@ def _nr_sanic_router_add(wrapped, instance, args, kwargs):
6060

6161
# Cache the callable_name on the handler object
6262
callable_name(handler)
63-
if hasattr(wrapped, 'view_class'):
63+
if hasattr(wrapped, "view_class"):
6464
callable_name(wrapped.view_class)
6565
wrapped_handler = _nr_wrapper_handler_(handler)
6666

@@ -131,7 +131,7 @@ def error_response(wrapped, instance, args, kwargs):
131131
raise
132132
else:
133133
# response can be a response object or a coroutine
134-
if hasattr(response, 'status'):
134+
if hasattr(response, "status"):
135135
notice_error(error=exc_info, status_code=response.status)
136136
else:
137137
notice_error(exc_info)
@@ -144,18 +144,16 @@ def error_response(wrapped, instance, args, kwargs):
144144
def _sanic_app_init(wrapped, instance, args, kwargs):
145145
result = wrapped(*args, **kwargs)
146146

147-
error_handler = getattr(instance, 'error_handler')
148-
if hasattr(error_handler, 'response'):
149-
instance.error_handler.response = error_response(
150-
error_handler.response)
151-
if hasattr(error_handler, 'add'):
152-
error_handler.add = _nr_sanic_error_handlers(
153-
error_handler.add)
147+
error_handler = getattr(instance, "error_handler")
148+
if hasattr(error_handler, "response"):
149+
instance.error_handler.response = error_response(error_handler.response)
150+
if hasattr(error_handler, "add"):
151+
error_handler.add = _nr_sanic_error_handlers(error_handler.add)
154152

155-
router = getattr(instance, 'router')
156-
if hasattr(router, 'add'):
153+
router = getattr(instance, "router")
154+
if hasattr(router, "add"):
157155
router.add = _nr_sanic_router_add(router.add)
158-
if hasattr(router, 'get'):
156+
if hasattr(router, "get"):
159157
# Cache the callable_name on the router.get
160158
callable_name(router.get)
161159
router.get = _nr_sanic_router_get(router.get)
@@ -172,8 +170,7 @@ def _nr_sanic_response_get_headers(wrapped, instance, args, kwargs):
172170
return result
173171

174172
# instance is the response object
175-
cat_headers = transaction.process_response(str(instance.status),
176-
instance.headers.items())
173+
cat_headers = transaction.process_response(str(instance.status), instance.headers.items())
177174

178175
for header_name, header_value in cat_headers:
179176
if header_name not in instance.headers:
@@ -189,27 +186,26 @@ async def _nr_sanic_response_send(wrapped, instance, args, kwargs):
189186
await result
190187

191188
if transaction is None:
192-
return wrapped(*args, **kwargs)
189+
return result
193190

194191
# instance is the response object
195-
cat_headers = transaction.process_response(str(instance.status),
196-
instance.headers.items())
192+
cat_headers = transaction.process_response(str(instance.status), instance.headers.items())
197193

198194
for header_name, header_value in cat_headers:
199195
if header_name not in instance.headers:
200196
instance.headers[header_name] = header_value
201197

202198
return result
203199

200+
204201
def _nr_sanic_response_parse_headers(wrapped, instance, args, kwargs):
205202
transaction = current_transaction()
206203

207204
if transaction is None:
208205
return wrapped(*args, **kwargs)
209206

210207
# instance is the response object
211-
cat_headers = transaction.process_response(str(instance.status),
212-
instance.headers.items())
208+
cat_headers = transaction.process_response(str(instance.status), instance.headers.items())
213209

214210
for header_name, header_value in cat_headers:
215211
if header_name not in instance.headers:
@@ -219,7 +215,7 @@ def _nr_sanic_response_parse_headers(wrapped, instance, args, kwargs):
219215

220216

221217
def _nr_wrapper_middleware_(attach_to):
222-
is_request_middleware = attach_to == 'request'
218+
is_request_middleware = attach_to == "request"
223219

224220
@function_wrapper
225221
def _wrapper(wrapped, instance, args, kwargs):
@@ -238,7 +234,7 @@ def _wrapper(wrapped, instance, args, kwargs):
238234
return _wrapper
239235

240236

241-
def _bind_middleware(middleware, attach_to='request', *args, **kwargs):
237+
def _bind_middleware(middleware, attach_to="request", *args, **kwargs):
242238
return middleware, attach_to
243239

244240

@@ -259,36 +255,55 @@ def _bind_request(request, *args, **kwargs):
259255
def _nr_sanic_transaction_wrapper_(wrapped, instance, args, kwargs):
260256
request = _bind_request(*args, **kwargs)
261257
# If the request is a websocket request do not wrap it
262-
if request.headers.get('upgrade', '').lower() == 'websocket':
258+
if request.headers.get("upgrade", "").lower() == "websocket":
263259
return wrapped(*args, **kwargs)
264260

265261
return web_transaction(
266262
request_method=request.method,
267263
request_path=request.path,
268264
query_string=request.query_string,
269-
headers=request.headers)(wrapped)(*args, **kwargs)
265+
headers=request.headers,
266+
)(wrapped)(*args, **kwargs)
267+
268+
269+
def _nr_wrap_touchup_run(wrapped, instance, args, kwargs):
270+
# TouchUp uses metaprogramming to rewrite methods of classes on startup.
271+
# To properly wrap them we need to catch the call to TouchUp.run and
272+
# reinstrument any methods that were replaced with uninstrumented versions.
273+
274+
result = wrapped(*args, **kwargs)
275+
276+
if "sanic.app" in sys.modules:
277+
module = sys.modules["sanic.app"]
278+
target = args[0]
279+
280+
if isinstance(target, module.Sanic):
281+
# Reinstrument class after metaclass "TouchUp" has finished rewriting methods on the class.
282+
target_cls = module.Sanic
283+
if hasattr(target_cls, "handle_request") and not hasattr(target_cls.handle_request, "__wrapped__"):
284+
wrap_function_wrapper(module, "Sanic.handle_request", _nr_sanic_transaction_wrapper_)
285+
286+
return result
270287

271288

272289
def instrument_sanic_app(module):
273-
wrap_function_wrapper(module, 'Sanic.handle_request',
274-
_nr_sanic_transaction_wrapper_)
275-
wrap_function_wrapper(module, 'Sanic.__init__',
276-
_sanic_app_init)
277-
wrap_function_wrapper(module, 'Sanic.register_middleware',
278-
_nr_sanic_register_middleware_)
279-
if hasattr(module.Sanic, 'register_named_middleware'):
280-
wrap_function_wrapper(module, 'Sanic.register_named_middleware',
281-
_nr_sanic_register_middleware_)
290+
wrap_function_wrapper(module, "Sanic.handle_request", _nr_sanic_transaction_wrapper_)
291+
wrap_function_wrapper(module, "Sanic.__init__", _sanic_app_init)
292+
wrap_function_wrapper(module, "Sanic.register_middleware", _nr_sanic_register_middleware_)
293+
if hasattr(module.Sanic, "register_named_middleware"):
294+
wrap_function_wrapper(module, "Sanic.register_named_middleware", _nr_sanic_register_middleware_)
282295

283296

284297
def instrument_sanic_response(module):
285-
if hasattr(module.BaseHTTPResponse, 'send'):
286-
wrap_function_wrapper(module, 'BaseHTTPResponse.send',
287-
_nr_sanic_response_send)
298+
if hasattr(module.BaseHTTPResponse, "send"):
299+
wrap_function_wrapper(module, "BaseHTTPResponse.send", _nr_sanic_response_send)
288300
else:
289-
if hasattr(module.BaseHTTPResponse, 'get_headers'):
290-
wrap_function_wrapper(module, 'BaseHTTPResponse.get_headers',
291-
_nr_sanic_response_get_headers)
292-
if hasattr(module.BaseHTTPResponse, '_parse_headers'):
293-
wrap_function_wrapper(module, 'BaseHTTPResponse._parse_headers',
294-
_nr_sanic_response_parse_headers)
301+
if hasattr(module.BaseHTTPResponse, "get_headers"):
302+
wrap_function_wrapper(module, "BaseHTTPResponse.get_headers", _nr_sanic_response_get_headers)
303+
if hasattr(module.BaseHTTPResponse, "_parse_headers"):
304+
wrap_function_wrapper(module, "BaseHTTPResponse._parse_headers", _nr_sanic_response_parse_headers)
305+
306+
307+
def instrument_sanic_touchup_service(module):
308+
if hasattr(module, "TouchUp") and hasattr(module.TouchUp, "run"):
309+
wrap_function_wrapper(module.TouchUp, "run", _nr_wrap_touchup_run)

tests/framework_sanic/_target_application.py

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,18 @@
1515
from sanic import Blueprint, Sanic
1616
from sanic.exceptions import NotFound, SanicException, ServerError
1717
from sanic.handlers import ErrorHandler
18-
from sanic.response import json, stream
18+
from sanic.response import json
1919
from sanic.router import Router
2020
from sanic.views import HTTPMethodView
2121

2222

23+
try:
24+
# Old style response streaming
25+
from sanic.response import stream
26+
except ImportError:
27+
stream = None
28+
29+
2330
class MethodView(HTTPMethodView):
2431
async def get(self, request):
2532
return json({"hello": "world"})
@@ -93,7 +100,7 @@ def get(self, *args):
93100
error_handler = CustomErrorHandler()
94101

95102
router = CustomRouter()
96-
app = Sanic(name="test app", error_handler=error_handler, router=router)
103+
app = Sanic(name="test-app", error_handler=error_handler, router=router)
97104
router.app = app
98105
blueprint = Blueprint("test_bp")
99106

@@ -139,13 +146,25 @@ async def blueprint_middleware(request):
139146
app.register_middleware(request_middleware)
140147

141148

149+
async def do_streaming(request):
150+
if stream is not None:
151+
# Old style response streaming
152+
async def streaming_fn(response):
153+
response.write("foo")
154+
response.write("bar")
155+
156+
return stream(streaming_fn)
157+
else:
158+
# New style response streaming
159+
response = await request.respond(content_type="text/plain")
160+
await response.send("foo")
161+
await response.send("bar")
162+
await response.eof()
163+
164+
142165
@app.route("/streaming")
143166
async def streaming(request):
144-
async def streaming_fn(response):
145-
response.write("foo")
146-
response.write("bar")
147-
148-
return stream(streaming_fn)
167+
return await do_streaming(request)
149168

150169

151170
# Fake websocket endpoint to enable websockets on the server
@@ -200,17 +219,11 @@ async def async_error(request):
200219

201220
@blueprint.route("/blueprint")
202221
async def blueprint_route(request):
203-
async def streaming_fn(response):
204-
response.write("foo")
205-
206-
return stream(streaming_fn)
207-
222+
return await do_streaming(request)
208223

209224
app.blueprint(blueprint)
210225
app.add_route(MethodView.as_view(), "/method_view")
211226

212-
if not getattr(router, "finalized", True):
213-
router.finalize()
214227

215228
if __name__ == "__main__":
216229
app.run(host="127.0.0.1", port=8000)

tests/framework_sanic/conftest.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,10 +80,13 @@ async def send(*args, **kwargs):
8080
proto = MockProtocol(loop=loop, app=app)
8181
proto.recv_buffer = bytearray()
8282
http = Http(proto)
83+
84+
if hasattr(http, "init_for_request"):
85+
http.init_for_request()
86+
8387
http.stage = Stage.HANDLER
8488
http.response_func = http.http1_response_header
8589
_request.stream = http
86-
pass
8790
except ImportError:
8891
pass
8992

@@ -123,6 +126,15 @@ def request(app, method, url, headers=None):
123126
if loop is None:
124127
loop = asyncio.new_event_loop()
125128

129+
if not getattr(app.router, "finalized", True):
130+
# Handle startup if the router hasn't been finalized.
131+
# Older versions don't have this requirement or variable so
132+
# the default should be True.
133+
if hasattr(app, "_startup"):
134+
loop.run_until_complete(app._startup())
135+
else:
136+
app.router.finalize()
137+
126138
coro = create_request_coroutine(app, method, url, headers, loop)
127139
loop.run_until_complete(coro)
128140
return RESPONSES.pop()

tox.ini

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -134,8 +134,8 @@ envlist =
134134
python-framework_pyramid-{pypy,py27,py38}-Pyramid0104,
135135
python-framework_pyramid-{pypy,py27,pypy37,py37,py38,py39,py310}-Pyramid0110-cornice,
136136
python-framework_pyramid-{py37,py38,py39,py310,pypy37}-Pyramidmaster,
137-
python-framework_sanic-{py38,pypy37}-sanic{190301,1906,1812,1912,200904,210300},
138-
python-framework_sanic-{py37,py38,py310,pypy37}-saniclatest,
137+
python-framework_sanic-{py38,pypy37}-sanic{190301,1906,1812,1912,200904,210300,2109,2112,2203},
138+
python-framework_sanic-{py37,py38,py39,py310,pypy37}-saniclatest,
139139
python-framework_starlette-{py310,pypy37}-starlette{0014,0015,0019},
140140
python-framework_starlette-{py37,py38,py39,py310,pypy37}-starlettelatest,
141141
python-framework_strawberry-{py37,py38,py39,py310}-strawberrylatest,
@@ -319,8 +319,10 @@ deps =
319319
framework_sanic-sanic1912: sanic<19.13
320320
framework_sanic-sanic200904: sanic<20.9.5
321321
framework_sanic-sanic210300: sanic<21.3.1
322-
; Temporarily test older sanic version until issues are resolved
323-
framework_sanic-saniclatest: sanic<21.9.0
322+
framework_sanic-sanic2109: sanic<21.10
323+
framework_sanic-sanic2112: sanic<21.13
324+
framework_sanic-sanic2203: sanic<22.4
325+
framework_sanic-saniclatest: sanic
324326
framework_sanic-sanic{1812,190301,1906}: aiohttp
325327
framework_starlette: graphene<3
326328
framework_starlette-starlette0014: starlette<0.15

0 commit comments

Comments
 (0)