Skip to content

Commit f02b59f

Browse files
Add starlette and fastapi instrumentation. (#5)
1 parent 90363fa commit f02b59f

File tree

13 files changed

+379
-63
lines changed

13 files changed

+379
-63
lines changed

newrelic/api/asgi_application.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
import sys
1516
import functools
1617

1718
import newrelic.packages.asgiref_compatibility as asgiref_compatibility
@@ -247,8 +248,21 @@ def __init__(self, application, scope, receive, send):
247248
if self._settings:
248249
self.capture_params = self._settings.capture_params
249250

251+
def __exit__(self, exc, value, tb):
252+
if getattr(value, "_nr_ignored", False):
253+
exc, value, tb = None, None, None
254+
return super(ASGIWebTransaction, self).__exit__(exc, value, tb)
255+
250256
async def send(self, event):
251-
if event["type"] == "http.response.start":
257+
if (
258+
event["type"] == "http.response.body"
259+
and not event.get("more_body", False)
260+
):
261+
try:
262+
return await self._send(event)
263+
finally:
264+
self.__exit__(*sys.exc_info())
265+
elif event["type"] == "http.response.start":
252266
self.process_response(event["status"], event.get("headers", ()))
253267
return await self._send(event)
254268

newrelic/common/async_proxy.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
import time
1717
import newrelic.packages.six as six
1818

19-
from newrelic.common.coroutine import (is_coroutine_function,
19+
from newrelic.common.coroutine import (is_coroutine_callable,
2020
is_asyncio_coroutine, is_generator_function)
2121
from newrelic.common.object_wrapper import ObjectProxy
2222
from newrelic.core.trace_cache import trace_cache
@@ -159,7 +159,7 @@ def __await__(self):
159159

160160

161161
def async_proxy(wrapped):
162-
if is_coroutine_function(wrapped):
162+
if is_coroutine_callable(wrapped):
163163
return CoroutineProxy
164164
elif is_generator_function(wrapped):
165165
if is_asyncio_coroutine(wrapped):

newrelic/common/async_wrapper.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
import textwrap
1616
import functools
1717
from newrelic.common.coroutine import (
18-
is_coroutine_function,
18+
is_coroutine_callable,
1919
is_asyncio_coroutine,
2020
is_generator_function,
2121
)
@@ -82,7 +82,7 @@ def wrapper(*args, **kwargs):
8282

8383

8484
def async_wrapper(wrapped):
85-
if is_coroutine_function(wrapped):
85+
if is_coroutine_callable(wrapped):
8686
return coroutine_wrapper
8787
elif is_generator_function(wrapped):
8888
if is_asyncio_coroutine(wrapped):

newrelic/common/coroutine.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,7 @@ def is_generator_function(wrapped):
3939

4040
def _iscoroutinefunction_tornado(fn):
4141
return hasattr(fn, '__tornado_coroutine__')
42+
43+
44+
def is_coroutine_callable(wrapped):
45+
return is_coroutine_function(wrapped) or is_coroutine_function(getattr(wrapped, "__call__", None))

newrelic/config.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2113,6 +2113,10 @@ def _process_module_builtin_defaults():
21132113
'newrelic.hooks.framework_falcon',
21142114
'instrument_falcon_routing_util')
21152115

2116+
_process_module_definition('fastapi.routing',
2117+
'newrelic.hooks.framework_fastapi',
2118+
'instrument_fastapi_routing')
2119+
21162120
_process_module_definition('flask.app',
21172121
'newrelic.hooks.framework_flask',
21182122
'instrument_flask_app')
@@ -2359,6 +2363,25 @@ def _process_module_builtin_defaults():
23592363
'newrelic.hooks.external_urllib3',
23602364
'instrument_urllib3_connection')
23612365

2366+
_process_module_definition('starlette.requests',
2367+
'newrelic.hooks.framework_starlette',
2368+
'instrument_starlette_requests')
2369+
_process_module_definition('starlette.routing',
2370+
'newrelic.hooks.framework_starlette',
2371+
'instrument_starlette_routing')
2372+
_process_module_definition('starlette.applications',
2373+
'newrelic.hooks.framework_starlette',
2374+
'instrument_starlette_applications')
2375+
_process_module_definition('starlette.middleware.errors',
2376+
'newrelic.hooks.framework_starlette',
2377+
'instrument_starlette_middleware_errors')
2378+
_process_module_definition('starlette.exceptions',
2379+
'newrelic.hooks.framework_starlette',
2380+
'instrument_starlette_exceptions')
2381+
_process_module_definition('starlette.background',
2382+
'newrelic.hooks.framework_starlette',
2383+
'instrument_starlette_background_task')
2384+
23622385
_process_module_definition('uvicorn.config',
23632386
'newrelic.hooks.adapter_uvicorn',
23642387
'instrument_uvicorn_config')

newrelic/core/trace_cache.py

Lines changed: 61 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,9 @@ def current_task(asyncio):
3838
if not asyncio:
3939
return
4040

41-
current_task = getattr(asyncio, 'current_task', None)
41+
current_task = getattr(asyncio, "current_task", None)
4242
if current_task is None:
43-
current_task = getattr(asyncio.Task, 'current_task', None)
43+
current_task = getattr(asyncio.Task, "current_task", None)
4444

4545
try:
4646
return current_task()
@@ -52,9 +52,9 @@ def all_tasks(asyncio):
5252
if not asyncio:
5353
return
5454

55-
all_tasks = getattr(asyncio, 'all_tasks', None)
55+
all_tasks = getattr(asyncio, "all_tasks", None)
5656
if all_tasks is None:
57-
all_tasks = getattr(asyncio.Task, 'all_tasks', None)
57+
all_tasks = getattr(asyncio.Task, "all_tasks", None)
5858

5959
try:
6060
return all_tasks()
@@ -63,15 +63,14 @@ def all_tasks(asyncio):
6363

6464

6565
def get_event_loop(task):
66-
get_loop = getattr(task, 'get_loop', None)
66+
get_loop = getattr(task, "get_loop", None)
6767
if get_loop:
6868
return get_loop()
6969
else:
70-
return getattr(task, '_loop', None)
70+
return getattr(task, "_loop", None)
7171

7272

7373
class cached_module(object):
74-
7574
def __init__(self, module_path, name=None):
7675
self.module_path = module_path
7776
self.name = name or module_path
@@ -172,9 +171,9 @@ def active_threads(self):
172171
transaction = trace and trace.transaction
173172
if transaction is not None:
174173
if transaction.background_task:
175-
yield transaction, thread_id, 'BACKGROUND', frame
174+
yield transaction, thread_id, "BACKGROUND", frame
176175
else:
177-
yield transaction, thread_id, 'REQUEST', frame
176+
yield transaction, thread_id, "REQUEST", frame
178177
else:
179178
# Note that there may not always be a thread object.
180179
# This is because thread could have been created direct
@@ -183,10 +182,10 @@ def active_threads(self):
183182
# obtain a name for as being 'OTHER'.
184183

185184
thread = threading._active.get(thread_id)
186-
if thread is not None and thread.getName().startswith('NR-'):
187-
yield None, thread_id, 'AGENT', frame
185+
if thread is not None and thread.getName().startswith("NR-"):
186+
yield None, thread_id, "AGENT", frame
188187
else:
189-
yield None, thread_id, 'OTHER', frame
188+
yield None, thread_id, "OTHER", frame
190189

191190
# Now yield up those corresponding to greenlets. Right now only
192191
# doing this for greenlets in which any active transactions are
@@ -202,11 +201,9 @@ def active_threads(self):
202201
gr = transaction._greenlet()
203202
if gr and gr.gr_frame is not None:
204203
if transaction.background_task:
205-
yield (transaction, thread_id,
206-
'BACKGROUND', gr.gr_frame)
204+
yield (transaction, thread_id, "BACKGROUND", gr.gr_frame)
207205
else:
208-
yield (transaction, thread_id,
209-
'REQUEST', gr.gr_frame)
206+
yield (transaction, thread_id, "REQUEST", gr.gr_frame)
210207

211208
def prepare_for_root(self):
212209
"""Updates the cache state so that a new root can be created if the
@@ -217,7 +214,7 @@ def prepare_for_root(self):
217214
if not trace:
218215
return None
219216

220-
if not hasattr(trace, '_task'):
217+
if not hasattr(trace, "_task"):
221218
return trace
222219

223220
task = current_task(self.asyncio)
@@ -243,15 +240,16 @@ def save_trace(self, trace):
243240

244241
if thread_id in self._cache:
245242
cache_root = self._cache[thread_id].root
246-
if (cache_root and cache_root is not trace.root and
247-
not cache_root.exited):
243+
if cache_root and cache_root is not trace.root and not cache_root.exited:
248244
# Cached trace exists and has a valid root still
249-
_logger.error('Runtime instrumentation error. Attempt to '
250-
'save a trace from an inactive transaction. '
251-
'Report this issue to New Relic support.\n%s',
252-
''.join(traceback.format_stack()[:-1]))
245+
_logger.error(
246+
"Runtime instrumentation error. Attempt to "
247+
"save a trace from an inactive transaction. "
248+
"Report this issue to New Relic support.\n%s",
249+
"".join(traceback.format_stack()[:-1]),
250+
)
253251

254-
raise TraceCacheActiveTraceError('transaction already active')
252+
raise TraceCacheActiveTraceError("transaction already active")
255253

256254
self._cache[thread_id] = trace
257255

@@ -266,21 +264,39 @@ def save_trace(self, trace):
266264

267265
trace._greenlet = None
268266

269-
if hasattr(sys, '_current_frames'):
267+
if hasattr(sys, "_current_frames"):
270268
if thread_id not in sys._current_frames():
271269
if self.greenlet:
272270
trace._greenlet = weakref.ref(self.greenlet.getcurrent())
273271

274-
if self.asyncio and not hasattr(trace, '_task'):
272+
if self.asyncio and not hasattr(trace, "_task"):
275273
task = current_task(self.asyncio)
276274
trace._task = task
277275

276+
def thread_start(self, trace):
277+
current_thread_id = self.current_thread_id()
278+
if current_thread_id not in self._cache:
279+
self._cache[current_thread_id] = trace
280+
else:
281+
_logger.error(
282+
"Runtime instrumentation error. An active "
283+
"trace already exists in the cache on thread_id %s. Report "
284+
"this issue to New Relic support.\n ", current_thread_id
285+
)
286+
return None
287+
288+
return current_thread_id
289+
290+
def thread_stop(self, thread_id):
291+
if thread_id:
292+
self._cache.pop(thread_id, None)
293+
278294
def pop_current(self, trace):
279295
"""Restore the trace's parent under the thread ID of the current
280296
executing thread."""
281297

282-
if hasattr(trace, '_task'):
283-
delattr(trace, '_task')
298+
if hasattr(trace, "_task"):
299+
delattr(trace, "_task")
284300

285301
thread_id = trace.thread_id
286302
parent = trace.parent
@@ -294,7 +310,7 @@ def complete_root(self, root):
294310
295311
"""
296312

297-
if hasattr(root, '_task'):
313+
if hasattr(root, "_task"):
298314
if root.has_outstanding_children():
299315
task_ids = (id(task) for task in all_tasks(self.asyncio))
300316

@@ -319,17 +335,19 @@ def complete_root(self, root):
319335
if thread_id not in self._cache:
320336
thread_id = self.current_thread_id()
321337
if thread_id not in self._cache:
322-
raise TraceCacheNoActiveTraceError('no active trace')
338+
raise TraceCacheNoActiveTraceError("no active trace")
323339

324340
current = self._cache.get(thread_id)
325341

326342
if root is not current:
327-
_logger.error('Runtime instrumentation error. Attempt to '
328-
'drop the root when it is not the current '
329-
'trace. Report this issue to New Relic support.\n%s',
330-
''.join(traceback.format_stack()[:-1]))
343+
_logger.error(
344+
"Runtime instrumentation error. Attempt to "
345+
"drop the root when it is not the current "
346+
"trace. Report this issue to New Relic support.\n%s",
347+
"".join(traceback.format_stack()[:-1]),
348+
)
331349

332-
raise RuntimeError('not the current trace')
350+
raise RuntimeError("not the current trace")
333351

334352
del self._cache[thread_id]
335353
root._greenlet = None
@@ -354,26 +372,28 @@ def record_event_loop_wait(self, start_time, end_time):
354372
roots = set()
355373
seen = set()
356374

357-
task = getattr(transaction.root_span, '_task', None)
375+
task = getattr(transaction.root_span, "_task", None)
358376
loop = get_event_loop(task)
359377

360378
for trace in self._cache.values():
361379
if trace in seen:
362380
continue
363381

364382
# If the trace is on a different transaction and it's asyncio
365-
if (trace.transaction is not transaction and
366-
getattr(trace, '_task', None) is not None and
367-
get_event_loop(trace._task) is loop and
368-
trace._is_leaf()):
383+
if (
384+
trace.transaction is not transaction
385+
and getattr(trace, "_task", None) is not None
386+
and get_event_loop(trace._task) is loop
387+
and trace._is_leaf()
388+
):
369389
trace.exclusive -= duration
370390
roots.add(trace.root)
371391
seen.add(trace)
372392

373393
seen = None
374394

375395
for root in roots:
376-
guid = '%016x' % random.getrandbits(64)
396+
guid = "%016x" % random.getrandbits(64)
377397
node = LoopNode(
378398
fetch_name=fetch_name,
379399
start_time=start_time,

newrelic/hooks/adapter_gunicorn.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,12 @@
1616

1717
from newrelic.api.wsgi_application import WSGIApplicationWrapper
1818
from newrelic.common.object_wrapper import wrap_out_function
19-
from newrelic.common.coroutine import (is_coroutine_function,
19+
from newrelic.common.coroutine import (is_coroutine_callable,
2020
is_asyncio_coroutine)
2121

2222

2323
def is_coroutine(fn):
24-
return is_coroutine_function(fn) or is_asyncio_coroutine(fn)
24+
return is_coroutine_callable(fn) or is_asyncio_coroutine(fn)
2525

2626

2727
def _nr_wrapper_Application_wsgi_(application):

newrelic/hooks/framework_aiohttp.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from newrelic.api.function_trace import function_trace
2020
from newrelic.api.transaction import current_transaction, ignore_transaction
2121
from newrelic.api.web_transaction import web_transaction
22-
from newrelic.common.async_wrapper import is_coroutine_function, async_wrapper
22+
from newrelic.common.async_wrapper import is_coroutine_callable, async_wrapper
2323
from newrelic.common.object_names import callable_name
2424
from newrelic.common.object_wrapper import (wrap_function_wrapper,
2525
function_wrapper, ObjectProxy)
@@ -219,7 +219,7 @@ def _nr_aiohttp_add_cat_headers_(wrapped, instance, args, kwargs):
219219
tmp = instance.headers
220220
instance.headers = HeaderProxy(tmp, cat_headers)
221221

222-
if is_coroutine_function(wrapped):
222+
if is_coroutine_callable(wrapped):
223223
@asyncio.coroutine
224224
def new_coro():
225225
try:

0 commit comments

Comments
 (0)