Skip to content

Commit 49c95c3

Browse files
committed
Strawberry Instrumentation (#357)
* Add strawberry instrumentation * [Mega-Linter] Apply linters fixes * [Mega-Linter] Apply linters fixes * Fix instrumentation issues * More instrumentation fixes * Safer wrap_from_resolver argument binding * Fix failing assertions * Fix strawberry types Co-authored-by: TimPansino <[email protected]>
1 parent 9e77b92 commit 49c95c3

File tree

4 files changed

+157
-14
lines changed

4 files changed

+157
-14
lines changed

newrelic/config.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2454,6 +2454,24 @@ def _process_module_builtin_defaults():
24542454
"instrument_starlette_background_task",
24552455
)
24562456

2457+
_process_module_definition(
2458+
"strawberry.asgi",
2459+
"newrelic.hooks.framework_strawberry",
2460+
"instrument_strawberry_asgi",
2461+
)
2462+
2463+
_process_module_definition(
2464+
"strawberry.schema.schema",
2465+
"newrelic.hooks.framework_strawberry",
2466+
"instrument_strawberry_schema",
2467+
)
2468+
2469+
_process_module_definition(
2470+
"strawberry.schema.schema_converter",
2471+
"newrelic.hooks.framework_strawberry",
2472+
"instrument_strawberry_schema_converter",
2473+
)
2474+
24572475
_process_module_definition("uvicorn.config", "newrelic.hooks.adapter_uvicorn", "instrument_uvicorn_config")
24582476

24592477
_process_module_definition("sanic.app", "newrelic.hooks.framework_sanic", "instrument_sanic_app")
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
# Copyright 2010 New Relic, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from newrelic.api.asgi_application import wrap_asgi_application
16+
from newrelic.api.error_trace import ErrorTrace
17+
from newrelic.api.graphql_trace import GraphQLOperationTrace
18+
from newrelic.api.transaction import current_transaction
19+
from newrelic.api.transaction_name import TransactionNameWrapper
20+
from newrelic.common.object_names import callable_name
21+
from newrelic.common.object_wrapper import wrap_function_wrapper
22+
from newrelic.core.graphql_utils import graphql_statement
23+
from newrelic.hooks.framework_graphql import (
24+
framework_version as graphql_framework_version,
25+
)
26+
from newrelic.hooks.framework_graphql import ignore_graphql_duplicate_exception
27+
28+
29+
def framework_details():
30+
import strawberry
31+
32+
return ("Strawberry", getattr(strawberry, "__version__", None))
33+
34+
35+
def bind_execute(query, *args, **kwargs):
36+
return query
37+
38+
39+
def wrap_execute_sync(wrapped, instance, args, kwargs):
40+
transaction = current_transaction()
41+
42+
if not transaction:
43+
return wrapped(*args, **kwargs)
44+
45+
try:
46+
query = bind_execute(*args, **kwargs)
47+
except TypeError:
48+
return wrapped(*args, **kwargs)
49+
50+
framework = framework_details()
51+
transaction.add_framework_info(name=framework[0], version=framework[1])
52+
transaction.add_framework_info(name="GraphQL", version=graphql_framework_version())
53+
54+
if hasattr(query, "body"):
55+
query = query.body
56+
57+
transaction.set_transaction_name(callable_name(wrapped), "GraphQL", priority=10)
58+
59+
with GraphQLOperationTrace() as trace:
60+
trace.statement = graphql_statement(query)
61+
with ErrorTrace(ignore=ignore_graphql_duplicate_exception):
62+
return wrapped(*args, **kwargs)
63+
64+
65+
async def wrap_execute(wrapped, instance, args, kwargs):
66+
transaction = current_transaction()
67+
68+
if not transaction:
69+
return await wrapped(*args, **kwargs)
70+
71+
try:
72+
query = bind_execute(*args, **kwargs)
73+
except TypeError:
74+
return await wrapped(*args, **kwargs)
75+
76+
framework = framework_details()
77+
transaction.add_framework_info(name=framework[0], version=framework[1])
78+
transaction.add_framework_info(name="GraphQL", version=graphql_framework_version())
79+
80+
if hasattr(query, "body"):
81+
query = query.body
82+
83+
transaction.set_transaction_name(callable_name(wrapped), "GraphQL", priority=10)
84+
85+
with GraphQLOperationTrace() as trace:
86+
trace.statement = graphql_statement(query)
87+
with ErrorTrace(ignore=ignore_graphql_duplicate_exception):
88+
return await wrapped(*args, **kwargs)
89+
90+
91+
def bind_from_resolver(field, *args, **kwargs):
92+
return field
93+
94+
95+
def wrap_from_resolver(wrapped, instance, args, kwargs):
96+
result = wrapped(*args, **kwargs)
97+
98+
try:
99+
field = bind_from_resolver(*args, **kwargs)
100+
except TypeError:
101+
pass
102+
else:
103+
if hasattr(field, "base_resolver"):
104+
if hasattr(field.base_resolver, "wrapped_func"):
105+
resolver_name = callable_name(field.base_resolver.wrapped_func)
106+
result = TransactionNameWrapper(result, resolver_name, "GraphQL", priority=13)
107+
108+
return result
109+
110+
111+
def instrument_strawberry_schema(module):
112+
if hasattr(module, "Schema"):
113+
if hasattr(module.Schema, "execute"):
114+
wrap_function_wrapper(module, "Schema.execute", wrap_execute)
115+
if hasattr(module.Schema, "execute_sync"):
116+
wrap_function_wrapper(module, "Schema.execute_sync", wrap_execute_sync)
117+
118+
119+
def instrument_strawberry_asgi(module):
120+
if hasattr(module, "GraphQL"):
121+
wrap_asgi_application(module, "GraphQL.__call__", framework=framework_details())
122+
123+
124+
def instrument_strawberry_schema_converter(module):
125+
if hasattr(module, "GraphQLCoreConverter"):
126+
if hasattr(module.GraphQLCoreConverter, "from_resolver"):
127+
wrap_function_wrapper(module, "GraphQLCoreConverter.from_resolver", wrap_from_resolver)

tests/framework_ariadne/test_application.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -248,8 +248,8 @@ def test_exception_in_middleware(app, graphql_run):
248248
def _test():
249249
from graphql import MiddlewareManager
250250

251-
ok, response = graphql_run(app, query, middleware=MiddlewareManager(error_middleware))
252-
assert not ok and response["errors"]
251+
_, response = graphql_run(app, query, middleware=MiddlewareManager(error_middleware))
252+
assert response["errors"]
253253

254254
_test()
255255

@@ -296,8 +296,8 @@ def test_exception_in_resolver(app, graphql_run, field):
296296
@validate_transaction_errors(errors=_test_runtime_error)
297297
@background_task()
298298
def _test():
299-
ok, response = graphql_run(app, query)
300-
assert not ok and response["errors"]
299+
_, response = graphql_run(app, query)
300+
assert response["errors"]
301301

302302
_test()
303303

@@ -352,8 +352,8 @@ def test_exception_in_validation(app, graphql_run, is_graphql_2, query, exc_clas
352352
@validate_transaction_errors(errors=[exc_class])
353353
@background_task()
354354
def _test():
355-
ok, response = graphql_run(app, query)
356-
assert not ok and response["errors"]
355+
_, response = graphql_run(app, query)
356+
assert response["errors"]
357357

358358
_test()
359359

tests/framework_strawberry/_target_application.py

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

15-
from typing import Union
15+
from typing import List, Union
1616

1717
import strawberry.mutation
1818
import strawberry.type
19-
from strawberry import Schema, field, union
19+
from strawberry import Schema, field
2020
from strawberry.asgi import GraphQL
2121
from strawberry.schema.config import StrawberryConfig
2222
from strawberry.types.types import Optional
@@ -49,14 +49,12 @@ class Magazine:
4949
class Library:
5050
id: int
5151
branch: str
52-
magazine: list[Magazine]
53-
book: list[Book]
52+
magazine: List[Magazine]
53+
book: List[Book]
5454

5555

5656
Item = Union[Book, Magazine]
57-
58-
59-
Storage = list[str]
57+
Storage = List[str]
6058

6159

6260
authors = [
@@ -159,7 +157,7 @@ class Query:
159157
library: Library = field(resolver=resolve_library)
160158
hello: str = field(resolver=resolve_hello)
161159
hello_async: str = field(resolver=resolve_hello_async)
162-
search: list[Item] = field(resolver=resolve_search)
160+
search: List[Item] = field(resolver=resolve_search)
163161
echo: str = field(resolver=resolve_echo)
164162
storage: Storage = field(resolver=resolve_storage)
165163
error: Optional[str] = field(resolver=resolve_error)

0 commit comments

Comments
 (0)