Skip to content

Commit 3d88d90

Browse files
TimPansinolrafeeiumaannamalai
committed
GraphQL Deepest Path Update (#291)
* 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 * Update deepest path behavior * 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]> * GraphQL cleanup changes * Fix broken tests * Add ignored fields and introspection fields * Clean up existing exception tests Co-authored-by: lrafeei <[email protected]> * Fix logic for parsing and transaction naming * Basic async test * Update deepest path behavior * Update branch. * Modify deepest path logic in instrumentation and tests. * Remove asyncio dependency. Co-authored-by: lrafeei <[email protected]> Co-authored-by: Uma Annamalai <[email protected]>
1 parent f510495 commit 3d88d90

File tree

6 files changed

+187
-67
lines changed

6 files changed

+187
-67
lines changed

newrelic/api/graphql_trace.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,6 @@ def finalize_data(self, transaction, exc=None, value=None, tb=None):
5757
# Add attributes
5858
self._add_agent_attribute("graphql.operation.type", self.operation_type)
5959
self._add_agent_attribute("graphql.operation.name", self.operation_name)
60-
self._add_agent_attribute("graphql.operation.deepestPath", self.deepest_path)
6160

6261
# Attach formatted graphql
6362
limit = transaction.settings.agent_limits.sql_query_length_maximum

newrelic/core/attribute.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,6 @@
7777
'graphql.field.path',
7878
'graphql.operation.name',
7979
'graphql.operation.type',
80-
'graphql.operation.deepestPath',
8180
'graphql.operation.query',
8281
))
8382

newrelic/hooks/framework_graphql.py

Lines changed: 67 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ def wrap_executor_context_init(wrapped, instance, args, kwargs):
6969
instance.field_resolver = wrap_resolver(instance.field_resolver)
7070
instance.field_resolver._nr_wrapped = True
7171

72+
7273
return result
7374

7475

@@ -91,65 +92,84 @@ def wrap_execute_operation(wrapped, instance, args, kwargs):
9192
except TypeError:
9293
operation = bind_operation_v2(*args, **kwargs)
9394

94-
operation_name = operation.name
95-
if hasattr(operation_name, "value"):
96-
operation_name = operation_name.value
95+
operation_name = get_node_value(operation, "name")
9796
trace.operation_name = operation_name = operation_name or "<anonymous>"
9897

99-
operation_type = operation.operation
100-
if hasattr(operation_type, "name"):
101-
operation_type = operation_type.name.lower()
98+
operation_type = get_node_value(operation, "operation", "name").lower()
10299
trace.operation_type = operation_type = operation_type or "<unknown>"
103100

104101
if operation.selection_set is not None:
105102
fields = operation.selection_set.selections
106-
try:
107-
deepest_path = traverse_deepest_path(fields)
108-
except:
109-
deepest_path = []
110-
trace.deepest_path = deepest_path = ".".join(deepest_path) or "<unknown>"
103+
# Ignore transactions for introspection queries
104+
for field in fields:
105+
if get_node_value(field, "name") in GRAPHQL_INTROSPECTION_FIELDS:
106+
ignore_transaction()
111107

112-
transaction.set_transaction_name(callable_name(wrapped), "GraphQL", priority=11)
108+
deepest_path = traverse_deepest_unique_path(fields)
109+
trace.deepest_path = deepest_path = ".".join(deepest_path) or ""
113110

111+
transaction.set_transaction_name(callable_name(wrapped), "GraphQL", priority=11)
114112
result = wrapped(*args, **kwargs)
115-
transaction_name = "%s/%s/%s" % (operation_type, operation_name, deepest_path)
113+
transaction_name = "%s/%s/%s" % (operation_type, operation_name, deepest_path) if deepest_path else "%s/%s" % (operation_type, operation_name)
116114
transaction.set_transaction_name(transaction_name, "GraphQL", priority=14)
117115

118116
return result
119117

120118

121-
def traverse_deepest_path(fields):
122-
inputs = deque(((field, deque(), 0) for field in fields))
123-
deepest_path_len = 0
124-
deepest_path = []
125-
126-
while inputs:
127-
field, current_path, current_path_len = inputs.pop()
128-
129-
field_name = field.name
130-
if hasattr(field_name, "value"):
131-
field_name = field_name.value
132-
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))
119+
def get_node_value(field, attr, subattr="value"):
120+
field_name = getattr(field, attr, None)
121+
if hasattr(field_name, subattr):
122+
field_name = getattr(field_name, subattr)
123+
return field_name
150124

151-
return deepest_path
152125

126+
def is_fragment(field):
127+
# Resolve version specific imports
128+
try:
129+
from graphql.language.ast import FragmentSpread, InlineFragment
130+
except ImportError:
131+
from graphql import FragmentSpreadNode as FragmentSpread, InlineFragmentNode as InlineFragment
132+
133+
_fragment_types = (InlineFragment, FragmentSpread)
134+
135+
return isinstance(field, _fragment_types)
136+
137+
def is_named_fragment(field):
138+
# Resolve version specific imports
139+
try:
140+
from graphql.language.ast import NamedType
141+
except ImportError:
142+
from graphql import NamedTypeNode as NamedType
143+
144+
return is_fragment(field) and getattr(field, "type_condition", None) is not None and isinstance(field.type_condition, NamedType)
145+
146+
147+
def traverse_deepest_unique_path(fields):
148+
deepest_path = deque()
149+
150+
while fields is not None and len(fields) > 0:
151+
fields = [f for f in fields if get_node_value(f, "name") not in GRAPHQL_IGNORED_FIELDS]
152+
if len(fields) != 1: # Either selections is empty, or non-unique
153+
return deepest_path
154+
field = fields[0]
155+
156+
field_name = get_node_value(field, "name")
157+
if is_named_fragment(field):
158+
name = get_node_value(field.type_condition, "name")
159+
if name:
160+
deepest_path.append("%s<%s>" % (deepest_path.pop(), name))
161+
elif is_fragment(field):
162+
break
163+
else:
164+
if field_name:
165+
deepest_path.append(field_name)
166+
167+
if field.selection_set is None:
168+
break
169+
else:
170+
fields = field.selection_set.selections
171+
172+
return deepest_path
153173

154174
def bind_get_middleware_resolvers(middlewares):
155175
return middlewares
@@ -176,7 +196,6 @@ def wrap_middleware(wrapped, instance, args, kwargs):
176196
return wrapped(*args, **kwargs)
177197

178198

179-
180199
def bind_get_field_resolver(field_resolver):
181200
return field_resolver
182201

@@ -217,7 +236,9 @@ def wrap_resolver(wrapped, instance, args, kwargs):
217236
transaction = current_transaction()
218237
if transaction is None:
219238
return wrapped(*args, **kwargs)
239+
220240
transaction.set_transaction_name(callable_name(wrapped), "GraphQL", priority=13)
241+
221242
with ErrorTrace(ignore=ignore_graphql_duplicate_exception):
222243
return wrapped(*args, **kwargs)
223244

@@ -250,8 +271,8 @@ def wrap_parse(wrapped, instance, args, kwargs):
250271
transaction = current_transaction()
251272
if transaction is None:
252273
return wrapped(*args, **kwargs)
253-
transaction.set_transaction_name(callable_name(wrapped), "GraphQL", priority=10)
254274

275+
transaction.set_transaction_name(callable_name(wrapped), "GraphQL", priority=10)
255276
with ErrorTrace(ignore=ignore_graphql_duplicate_exception):
256277
return wrapped(*args, **kwargs)
257278

@@ -333,7 +354,7 @@ def wrap_graphql_impl(wrapped, instance, args, kwargs):
333354
trace.statement = graphql_statement(query)
334355
with ErrorTrace(ignore=ignore_graphql_duplicate_exception):
335356
result = wrapped(*args, **kwargs)
336-
#transaction.set_transaction_name(transaction_name, "GraphQL", priority=14)
357+
# transaction.set_transaction_name(transaction_name, "GraphQL", priority=14)
337358
return result
338359

339360

tests/framework_graphql/_target_application.py

Lines changed: 79 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,64 @@
2121
GraphQLObjectType,
2222
GraphQLSchema,
2323
GraphQLString,
24+
GraphQLUnionType,
2425
)
2526

26-
libraries = [
27+
28+
books = [
2729
{
2830
"id": 1,
29-
"name": "NYC Public Library",
30-
"book": [{"name": "A", "author": "B", "id": 1}, {"name": "C", "author": "D", "id": 2}],
31+
"name": 'Python Agent: The Book',
32+
"isbn": 'a-fake-isbn',
33+
"author": 'Sentient Bits',
34+
"branch": 'riverside'
35+
},
36+
{
37+
"id": 2,
38+
"name": 'Ollies for O11y: A Sk8er\'s Guide to Observability',
39+
"isbn": 'a-second-fake-isbn',
40+
"author": 'Faux Hawk',
41+
"branch": 'downtown'
42+
},
43+
{
44+
"id": 3,
45+
"name": '[Redacted]',
46+
"isbn": 'a-third-fake-isbn',
47+
"author": 'Closed Telemetry',
48+
"branch": 'riverside'
49+
},
50+
]
51+
52+
magazines = [
53+
{
54+
"id": 1,
55+
"name": 'Reli Updates Weekly',
56+
"issue": 1,
57+
"branch": 'riverside'
58+
},
59+
{
60+
"id": 2,
61+
"name": 'Reli Updates Weekly',
62+
"issue": 2,
63+
"branch": 'downtown'
64+
},
65+
{
66+
"id": 3,
67+
"name": 'Node Weekly',
68+
"issue": 1,
69+
"branch": 'riverside'
3170
},
32-
{"id": 2, "name": "Portland Public Library", "book": [{"name": "E", "author": "F", "id": 1}]},
3371
]
3472

73+
74+
libraries = ['riverside', 'downtown']
75+
libraries = [{
76+
"id": i + 1,
77+
"branch": branch,
78+
"magazine": [m for m in magazines if m["branch"] == branch],
79+
"book": [b for b in books if b["branch"] == branch],
80+
} for i, branch in enumerate(libraries)]
81+
3582
storage = []
3683

3784

@@ -45,22 +92,41 @@ def resolve_storage_add(parent, info, string):
4592
def resolve_storage(parent, info):
4693
return storage
4794

95+
def resolve_search(parent, info, contains):
96+
search_books = [b for b in books if contains in b["name"]]
97+
search_magazines = [m for m in magazines if contains in m["name"]]
98+
return search_books + search_magazines
99+
48100

49101
Book = GraphQLObjectType(
50102
"Book",
51103
{
52104
"id": GraphQLField(GraphQLInt),
53105
"name": GraphQLField(GraphQLString),
106+
"isbn": GraphQLField(GraphQLString),
54107
"author": GraphQLField(GraphQLString),
108+
"branch": GraphQLField(GraphQLString),
55109
},
56110
)
57111

112+
Magazine = GraphQLObjectType(
113+
"Magazine",
114+
{
115+
"id": GraphQLField(GraphQLInt),
116+
"name": GraphQLField(GraphQLString),
117+
"issue": GraphQLField(GraphQLInt),
118+
"branch": GraphQLField(GraphQLString),
119+
},
120+
)
121+
122+
58123
Library = GraphQLObjectType(
59124
"Library",
60125
{
61126
"id": GraphQLField(GraphQLInt),
62127
"name": GraphQLField(GraphQLString),
63128
"book": GraphQLField(GraphQLList(Book)),
129+
"magazine": GraphQLField(GraphQLList(Book)),
64130
},
65131
)
66132

@@ -86,6 +152,10 @@ def resolve_error(root, info):
86152
resolver=resolve_library,
87153
args={"index": GraphQLArgument(GraphQLNonNull(GraphQLInt))},
88154
)
155+
search_field = GraphQLField(
156+
GraphQLList(GraphQLUnionType("Item", (Book, Magazine), resolve_type=resolve_search)),
157+
args={"contains": GraphQLArgument(GraphQLNonNull(GraphQLString))},
158+
)
89159
echo_field = GraphQLField(
90160
GraphQLString,
91161
resolver=resolve_echo,
@@ -111,6 +181,10 @@ def resolve_error(root, info):
111181
resolve=resolve_library,
112182
args={"index": GraphQLArgument(GraphQLNonNull(GraphQLInt))},
113183
)
184+
search_field = GraphQLField(
185+
GraphQLList(GraphQLUnionType("Item", (Book, Magazine), resolve_type=resolve_search)),
186+
args={"contains": GraphQLArgument(GraphQLNonNull(GraphQLString))},
187+
)
114188
echo_field = GraphQLField(
115189
GraphQLString,
116190
resolve=resolve_echo,
@@ -135,6 +209,7 @@ def resolve_error(root, info):
135209
fields={
136210
"hello": hello_field,
137211
"library": library_field,
212+
"search": search_field,
138213
"echo": echo_field,
139214
"storage": storage_field,
140215
"error": error_field,

0 commit comments

Comments
 (0)