Skip to content

Commit f510495

Browse files
TimPansinolrafeeiumaannamalai
committed
GraphQL Cleanup (#290)
* GraphQL cleanup changes * Fix broken tests * Add ignored fields and introspection fields * Clean up to_source object function * Clean up existing exception tests Co-authored-by: lrafeei <[email protected]> * Fix default values in trace * Fix logic for parsing and transaction naming * Basic async test *   Fix failing tests and update txn group. * Update non-null resolver test. * Update non-null error case. * Seperate async testing to new file. * Drop support for graphql 2.1 (3 year support policy). * Remove 2.1 from test matrix. Co-authored-by: lrafeei <[email protected]> Co-authored-by: Uma Annamalai <[email protected]>
1 parent 765611e commit f510495

File tree

9 files changed

+399
-103
lines changed

9 files changed

+399
-103
lines changed

newrelic/api/graphql_trace.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,9 @@ def __init__(self, **kwargs):
2929
parent = kwargs['parent']
3030
super(GraphQLOperationTrace, self).__init__(parent)
3131

32-
self.operation_name = None
33-
self.operation_type = None
34-
self.deepest_path = None
32+
self.operation_name = "<anonymous>"
33+
self.operation_type = "<unknown>"
34+
self.deepest_path = "<unknown>"
3535
self.graphql = None
3636
self.graphql_format = None
3737
self.statement = None
@@ -44,7 +44,7 @@ def formatted(self):
4444
if not self.statement:
4545
return "<unknown>"
4646

47-
transaction = current_transaction()
47+
transaction = current_transaction(active_only=False)
4848

4949
# Record SQL settings
5050
settings = transaction.settings
@@ -55,9 +55,9 @@ def formatted(self):
5555

5656
def finalize_data(self, transaction, exc=None, value=None, tb=None):
5757
# Add attributes
58-
self._add_agent_attribute("graphql.operation.type", self.operation_type or "<unknown>")
59-
self._add_agent_attribute("graphql.operation.name", self.operation_name or "<anonymous>")
60-
self._add_agent_attribute("graphql.operation.deepestPath", self.deepest_path or "<unknown>")
58+
self._add_agent_attribute("graphql.operation.type", self.operation_type)
59+
self._add_agent_attribute("graphql.operation.name", self.operation_name)
60+
self._add_agent_attribute("graphql.operation.deepestPath", self.deepest_path)
6161

6262
# Attach formatted graphql
6363
limit = transaction.settings.agent_limits.sql_query_length_maximum

newrelic/config.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2407,6 +2407,11 @@ def _process_module_builtin_defaults():
24072407
"newrelic.hooks.framework_graphql",
24082408
"instrument_graphql_error_located_error",
24092409
)
2410+
_process_module_definition(
2411+
"graphql.language.parser",
2412+
"newrelic.hooks.framework_graphql",
2413+
"instrument_graphql_parser",
2414+
)
24102415
_process_module_definition(
24112416
"graphql.validation.validate",
24122417
"newrelic.hooks.framework_graphql",

newrelic/core/graphql_node.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -96,9 +96,9 @@ def time_metrics(self, stats, root, parent):
9696
class GraphQLOperationNode(_GraphQLOperationNode, GraphQLNodeMixin):
9797
@property
9898
def name(self):
99-
operation_type = self.operation_type or "<unknown>"
100-
operation_name = self.operation_name or "<anonymous>"
101-
deepest_path = self.deepest_path or "<unknown>"
99+
operation_type = self.operation_type
100+
operation_name = self.operation_name
101+
deepest_path = self.deepest_path
102102
product = self.product
103103

104104
name = 'GraphQL/operation/%s/%s/%s/%s' % (product, operation_type,
@@ -112,9 +112,9 @@ def time_metrics(self, stats, root, parent):
112112
113113
"""
114114

115-
operation_type = self.operation_type or "<unknown>"
116-
operation_name = self.operation_name or "<anonymous>"
117-
deepest_path = self.deepest_path or "<unknown>"
115+
operation_type = self.operation_type
116+
operation_name = self.operation_name
117+
deepest_path = self.deepest_path
118118
product = self.product
119119

120120
# Determine the scoped metric

newrelic/hooks/framework_graphql.py

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

15-
from newrelic.api.error_trace import ErrorTrace, ErrorTraceWrapper
16-
from newrelic.api.function_trace import FunctionTrace, FunctionTraceWrapper
15+
from newrelic.api.error_trace import ErrorTrace
16+
from newrelic.api.function_trace import FunctionTrace
1717
from newrelic.api.graphql_trace import GraphQLResolverTrace, GraphQLOperationTrace
1818
from newrelic.api.time_trace import notice_error, current_trace
19-
from newrelic.api.transaction import current_transaction
19+
from newrelic.api.transaction import current_transaction, ignore_transaction
2020
from newrelic.common.object_names import callable_name, parse_exc_info
2121
from newrelic.common.object_wrapper import function_wrapper, wrap_function_wrapper
2222
from newrelic.core.graphql_utils import graphql_statement
2323
from collections import deque
2424
from copy import copy
2525

2626

27+
GRAPHQL_IGNORED_FIELDS = frozenset(("id", "__typename"))
28+
GRAPHQL_INTROSPECTION_FIELDS = frozenset(("__schema", "__type"))
29+
30+
2731
def graphql_version():
2832
from graphql import __version__ as version
2933
return tuple(int(v) for v in version.split("."))
@@ -45,7 +49,7 @@ def ignore_graphql_duplicate_exception(exc, val, tb):
4549
_, _, fullnames, message = parse_exc_info((None, val.original_error, None))
4650
fullname = fullnames[0]
4751
for error in transaction._errors:
48-
if error.type == fullname and error.message == message:
52+
if error.message == message:
4953
return True
5054

5155
return None # Follow original exception matching rules
@@ -76,39 +80,48 @@ def bind_operation_v2(exe_context, operation, root_value):
7680

7781

7882
def wrap_execute_operation(wrapped, instance, args, kwargs):
83+
transaction = current_transaction()
7984
trace = current_trace()
8085

81-
if trace:
86+
if not transaction:
87+
return wrapped(*args, **kwargs)
88+
89+
try:
90+
operation = bind_operation_v3(*args, **kwargs)
91+
except TypeError:
92+
operation = bind_operation_v2(*args, **kwargs)
93+
94+
operation_name = operation.name
95+
if hasattr(operation_name, "value"):
96+
operation_name = operation_name.value
97+
trace.operation_name = operation_name = operation_name or "<anonymous>"
98+
99+
operation_type = operation.operation
100+
if hasattr(operation_type, "name"):
101+
operation_type = operation_type.name.lower()
102+
trace.operation_type = operation_type = operation_type or "<unknown>"
103+
104+
if operation.selection_set is not None:
105+
fields = operation.selection_set.selections
82106
try:
83-
operation = bind_operation_v3(*args, **kwargs)
84-
except TypeError:
85-
operation = bind_operation_v2(*args, **kwargs)
86-
87-
operation_name = operation.name
88-
if hasattr(operation_name, "value"):
89-
operation_name = operation_name.value
90-
trace.operation_name = operation_name
91-
92-
operation_type = operation.operation
93-
if hasattr(operation_type, "name"):
94-
operation_type = operation_type.name
95-
trace.operation_type = operation_type.lower()
96-
97-
if operation.selection_set is not None:
98-
fields = operation.selection_set.selections
99-
try:
100-
deepest_path = traverse_deepest_path(fields)
101-
except:
102-
deepest_path = []
103-
trace.deepest_path = ".".join(deepest_path)
107+
deepest_path = traverse_deepest_path(fields)
108+
except:
109+
deepest_path = []
110+
trace.deepest_path = deepest_path = ".".join(deepest_path) or "<unknown>"
104111

105-
return wrapped(*args, **kwargs)
112+
transaction.set_transaction_name(callable_name(wrapped), "GraphQL", priority=11)
113+
114+
result = wrapped(*args, **kwargs)
115+
transaction_name = "%s/%s/%s" % (operation_type, operation_name, deepest_path)
116+
transaction.set_transaction_name(transaction_name, "GraphQL", priority=14)
117+
118+
return result
106119

107120

108121
def traverse_deepest_path(fields):
109122
inputs = deque(((field, deque(), 0) for field in fields))
110123
deepest_path_len = 0
111-
deepest_path = None
124+
deepest_path = []
112125

113126
while inputs:
114127
field, current_path, current_path_len = inputs.pop()
@@ -117,16 +130,23 @@ def traverse_deepest_path(fields):
117130
if hasattr(field_name, "value"):
118131
field_name = field_name.value
119132

120-
current_path.append(field_name)
121-
current_path_len += 1
122-
123-
if field.selection_set is None or len(field.selection_set.selections) == 0:
124-
if deepest_path_len < current_path_len:
125-
deepest_path = current_path
126-
deepest_path_len = current_path_len
127-
else:
128-
for inner_field in field.selection_set.selections:
129-
inputs.append((inner_field, copy(current_path), current_path_len))
133+
if field_name in GRAPHQL_INTROSPECTION_FIELDS:
134+
# If an introspection query is identified, ignore the transaction completely.
135+
# This matches the logic used in Apollo server for identifying introspection queries.
136+
ignore_transaction()
137+
return []
138+
elif field_name not in GRAPHQL_IGNORED_FIELDS:
139+
# Only add to the current path for non-ignored values.
140+
current_path.append(field_name)
141+
current_path_len += 1
142+
143+
if field.selection_set is None or len(field.selection_set.selections) == 0:
144+
if deepest_path_len < current_path_len:
145+
deepest_path = current_path
146+
deepest_path_len = current_path_len
147+
else:
148+
for inner_field in field.selection_set.selections:
149+
inputs.append((inner_field, copy(current_path), current_path_len))
130150

131151
return deepest_path
132152

@@ -137,12 +157,25 @@ def bind_get_middleware_resolvers(middlewares):
137157

138158
def wrap_get_middleware_resolvers(wrapped, instance, args, kwargs):
139159
middlewares = bind_get_middleware_resolvers(*args, **kwargs)
140-
middlewares = [FunctionTraceWrapper(ErrorTraceWrapper(m, ignore=ignore_graphql_duplicate_exception)) if not hasattr(m, "_nr_wrapped") else m for m in middlewares]
160+
middlewares = [wrap_middleware(m) if not hasattr(m, "_nr_wrapped") else m for m in middlewares]
141161
for m in middlewares:
142162
m._nr_wrapped = True
143163

144164
return wrapped(middlewares)
145165

166+
@function_wrapper
167+
def wrap_middleware(wrapped, instance, args, kwargs):
168+
transaction = current_transaction()
169+
if transaction is None:
170+
return wrapped(*args, **kwargs)
171+
172+
name = callable_name(wrapped)
173+
transaction.set_transaction_name(name, 'GraphQL', priority=12)
174+
with FunctionTrace(name):
175+
with ErrorTrace(ignore=ignore_graphql_duplicate_exception):
176+
return wrapped(*args, **kwargs)
177+
178+
146179

147180
def bind_get_field_resolver(field_resolver):
148181
return field_resolver
@@ -182,14 +215,11 @@ def wrap_executor_execute(wrapped, instance, args, kwargs):
182215
@function_wrapper
183216
def wrap_resolver(wrapped, instance, args, kwargs):
184217
transaction = current_transaction()
185-
# Prevent double wrapping using _nr_wrapped attr
186218
if transaction is None:
187219
return wrapped(*args, **kwargs)
188-
189-
transaction.set_transaction_name(callable_name(wrapped), priority=2)
190-
with FunctionTrace(callable_name(wrapped)):
191-
with ErrorTrace(ignore=ignore_graphql_duplicate_exception):
192-
return wrapped(*args, **kwargs)
220+
transaction.set_transaction_name(callable_name(wrapped), "GraphQL", priority=13)
221+
with ErrorTrace(ignore=ignore_graphql_duplicate_exception):
222+
return wrapped(*args, **kwargs)
193223

194224

195225
def wrap_error_handler(wrapped, instance, args, kwargs):
@@ -198,6 +228,13 @@ def wrap_error_handler(wrapped, instance, args, kwargs):
198228

199229

200230
def wrap_validate(wrapped, instance, args, kwargs):
231+
transaction = current_transaction()
232+
if transaction is None:
233+
return wrapped(*args, **kwargs)
234+
235+
transaction.set_transaction_name(callable_name(wrapped),"GraphQL", priority=10)
236+
237+
# Run and collect errors
201238
errors = wrapped(*args, **kwargs)
202239

203240
# Raise errors and immediately catch them so we can record them
@@ -209,6 +246,15 @@ def wrap_validate(wrapped, instance, args, kwargs):
209246

210247
return errors
211248

249+
def wrap_parse(wrapped, instance, args, kwargs):
250+
transaction = current_transaction()
251+
if transaction is None:
252+
return wrapped(*args, **kwargs)
253+
transaction.set_transaction_name(callable_name(wrapped), "GraphQL", priority=10)
254+
255+
with ErrorTrace(ignore=ignore_graphql_duplicate_exception):
256+
return wrapped(*args, **kwargs)
257+
212258

213259
def bind_resolve_field_v3(parent_type, source, field_nodes, path):
214260
return parent_type, field_nodes, path
@@ -233,13 +279,14 @@ def wrap_resolve_field(wrapped, instance, args, kwargs):
233279
field_name = field_asts[0].name.value
234280

235281
with GraphQLResolverTrace(field_name) as trace:
236-
trace._add_agent_attribute("graphql.field.parentType", parent_type.name)
237-
if isinstance(field_path, list):
238-
trace._add_agent_attribute("graphql.field.path", field_path[0])
239-
else:
240-
trace._add_agent_attribute("graphql.field.path", field_path.key)
282+
with ErrorTrace(ignore=ignore_graphql_duplicate_exception):
283+
trace._add_agent_attribute("graphql.field.parentType", parent_type.name)
284+
if isinstance(field_path, list):
285+
trace._add_agent_attribute("graphql.field.path", field_path[0])
286+
else:
287+
trace._add_agent_attribute("graphql.field.path", field_path.key)
241288

242-
return wrapped(*args, **kwargs)
289+
return wrapped(*args, **kwargs)
243290

244291

245292
def bind_graphql_impl_query(schema, source, *args, **kwargs):
@@ -280,10 +327,14 @@ def wrap_graphql_impl(wrapped, instance, args, kwargs):
280327
if hasattr(query, "body"):
281328
query = query.body
282329

330+
transaction.set_transaction_name(callable_name(wrapped), "GraphQL", priority=10)
331+
283332
with GraphQLOperationTrace() as trace:
284333
trace.statement = graphql_statement(query)
285334
with ErrorTrace(ignore=ignore_graphql_duplicate_exception):
286-
return wrapped(*args, **kwargs)
335+
result = wrapped(*args, **kwargs)
336+
#transaction.set_transaction_name(transaction_name, "GraphQL", priority=14)
337+
return result
287338

288339

289340
def instrument_graphql_execute(module):
@@ -336,8 +387,13 @@ def instrument_graphql_error_located_error(module):
336387
def instrument_graphql_validate(module):
337388
wrap_function_wrapper(module, "validate", wrap_validate)
338389

390+
339391
def instrument_graphql(module):
340392
if hasattr(module, "graphql_impl"):
341393
wrap_function_wrapper(module, "graphql_impl", wrap_graphql_impl)
342394
if hasattr(module, "execute_graphql"):
343395
wrap_function_wrapper(module, "execute_graphql", wrap_graphql_impl)
396+
397+
398+
def instrument_graphql_parser(module):
399+
wrap_function_wrapper(module, "parse", wrap_parse)

tests/framework_graphql/_target_application.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,11 @@
2525

2626
libraries = [
2727
{
28+
"id": 1,
2829
"name": "NYC Public Library",
29-
"book": [{"name": "A", "author": "B"}, {"name": "C", "author": "D"}],
30+
"book": [{"name": "A", "author": "B", "id": 1}, {"name": "C", "author": "D", "id": 2}],
3031
},
31-
{"name": "Portland Public Library", "book": [{"name": "E", "author": "F"}]},
32+
{"id": 2, "name": "Portland Public Library", "book": [{"name": "E", "author": "F", "id": 1}]},
3233
]
3334

3435
storage = []
@@ -48,6 +49,7 @@ def resolve_storage(parent, info):
4849
Book = GraphQLObjectType(
4950
"Book",
5051
{
52+
"id": GraphQLField(GraphQLInt),
5153
"name": GraphQLField(GraphQLString),
5254
"author": GraphQLField(GraphQLString),
5355
},
@@ -56,6 +58,7 @@ def resolve_storage(parent, info):
5658
Library = GraphQLObjectType(
5759
"Library",
5860
{
61+
"id": GraphQLField(GraphQLInt),
5962
"name": GraphQLField(GraphQLString),
6063
"book": GraphQLField(GraphQLList(Book)),
6164
},

tests/framework_graphql/conftest.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
# limitations under the License.
1414

1515
import pytest
16+
import six
1617
from testing_support.fixtures import (
1718
code_coverage_fixture,
1819
collector_agent_registration_fixture,
@@ -44,3 +45,9 @@ def app():
4445
from _target_application import _target_application
4546

4647
return _target_application
48+
49+
50+
if six.PY2:
51+
collect_ignore = [
52+
'test_application_async.py'
53+
]

0 commit comments

Comments
 (0)