Skip to content

Commit 108d2ed

Browse files
committed
GraphQL Obfuscated Queries (#279)
* Format * Add query obfuscation testing * Add query as agent attribute for graphql * Fix inheritance of graphql traces * Fix graphql2 tests * Move graphql operation trace to capture sql
1 parent b2a0088 commit 108d2ed

File tree

7 files changed

+225
-36
lines changed

7 files changed

+225
-36
lines changed

newrelic/api/graphql_trace.py

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@
1414

1515
import functools
1616

17-
from newrelic.common.async_wrapper import async_wrapper
1817
from newrelic.api.time_trace import TimeTrace, current_trace
18+
from newrelic.api.transaction import current_transaction
19+
from newrelic.common.async_wrapper import async_wrapper
1920
from newrelic.common.object_wrapper import FunctionWrapper, wrap_object
2021
from newrelic.core.graphql_node import GraphQLOperationNode, GraphQLResolverNode
2122

@@ -31,17 +32,39 @@ def __init__(self, **kwargs):
3132
self.operation_name = None
3233
self.operation_type = None
3334
self.deepest_path = None
35+
self.graphql = None
36+
self.graphql_format = None
37+
self.statement = None
3438

3539
def __repr__(self):
3640
return '<%s %s>' % (self.__class__.__name__, dict())
3741

38-
def finalize_data(self, *args, **kwargs):
42+
@property
43+
def formatted(self):
44+
if not self.statement:
45+
return "<unknown>"
46+
47+
transaction = current_transaction()
48+
49+
# Record SQL settings
50+
settings = transaction.settings
51+
tt = settings.transaction_tracer
52+
self.graphql_format = tt.record_sql
53+
54+
return self.statement.formatted(self.graphql_format)
55+
56+
def finalize_data(self, transaction, exc=None, value=None, tb=None):
57+
# Add attributes
3958
self._add_agent_attribute("graphql.operation.type", self.operation_type or "<unknown>")
4059
self._add_agent_attribute("graphql.operation.name", self.operation_name or "<anonymous>")
4160
self._add_agent_attribute("graphql.operation.deepestPath", self.deepest_path or "<unknown>")
4261

43-
return super(GraphQLOperationTrace, self).finalize_data(*args, **kwargs)
62+
# Attach formatted graphql
63+
limit = transaction.settings.agent_limits.sql_query_length_maximum
64+
self.graphql = graphql = self.formatted[:limit]
65+
self._add_agent_attribute("graphql.operation.query", graphql)
4466

67+
return super(GraphQLOperationTrace, self).finalize_data(transaction, exc=None, value=None, tb=None)
4568

4669
def create_node(self):
4770
return GraphQLOperationNode(
@@ -56,6 +79,7 @@ def create_node(self):
5679
operation_name=self.operation_name,
5780
operation_type=self.operation_type,
5881
deepest_path=self.deepest_path,
82+
graphql=self.graphql,
5983
)
6084

6185

@@ -87,18 +111,19 @@ def wrap_graphql_operation_trace(module, object_path):
87111
wrap_object(module, object_path, GraphQLOperationTraceWrapper)
88112

89113

90-
class GraphQLResolverTrace(GraphQLOperationTrace):
114+
class GraphQLResolverTrace(TimeTrace):
91115
def __init__(self, field_name=None, **kwargs):
92116
super(GraphQLResolverTrace, self).__init__(**kwargs)
93117
self.field_name = field_name
94118

119+
def __repr__(self):
120+
return '<%s %s>' % (self.__class__.__name__, dict())
95121

96122
def finalize_data(self, *args, **kwargs):
97123
self._add_agent_attribute("graphql.field.name", self.field_name)
98124

99125
return super(GraphQLResolverTrace, self).finalize_data(*args, **kwargs)
100126

101-
102127
def create_node(self):
103128
return GraphQLResolverNode(
104129
field_name=self.field_name,

newrelic/config.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2377,6 +2377,11 @@ def _process_module_builtin_defaults():
23772377
"gluon.contrib.memcache.memcache", "newrelic.hooks.memcache_memcache"
23782378
)
23792379

2380+
_process_module_definition(
2381+
"graphql.graphql",
2382+
"newrelic.hooks.framework_graphql",
2383+
"instrument_graphql",
2384+
)
23802385
_process_module_definition(
23812386
"graphql.execution.execute",
23822387
"newrelic.hooks.framework_graphql",

newrelic/core/graphql_node.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,13 @@
1717
import newrelic.core.trace_node
1818

1919
from newrelic.core.metric import TimeMetric
20-
2120
from newrelic.core.node_mixin import GenericNodeMixin
2221

2322

2423
_GraphQLOperationNode = namedtuple('_GraphQLNode',
25-
['operation_type', 'operation_name', 'deepest_path',
26-
'children', 'start_time', 'end_time', 'duration', 'exclusive',
27-
'guid', 'agent_attributes', 'user_attributes'])
24+
['operation_type', 'operation_name', 'deepest_path', 'graphql',
25+
'children', 'start_time', 'end_time', 'duration', 'exclusive', 'guid',
26+
'agent_attributes', 'user_attributes'])
2827

2928
_GraphQLResolverNode = namedtuple('_GraphQLNode',
3029
['field_name', 'children', 'start_time', 'end_time', 'duration',

newrelic/core/graphql_utils.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
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+
"""GraphQL utilities that wrap SQL utilities for reuse."""
16+
17+
import weakref
18+
19+
from newrelic.core.database_utils import SQLStatement
20+
21+
22+
class GraphQLStyle(object):
23+
"""Helper class to initialize SQLStatement instances."""
24+
25+
quoting_style = "single+double"
26+
27+
28+
class GraphQLStatement(SQLStatement):
29+
"""Wrap SQLStatements to allow usage with GraphQL."""
30+
31+
def __init__(self, graphql):
32+
super(GraphQLStatement, self).__init__(graphql, GraphQLStyle())
33+
# Preset unapplicable fields to empty
34+
self._operation = ""
35+
self._target = ""
36+
37+
38+
_graphql_statements = weakref.WeakValueDictionary()
39+
40+
41+
def graphql_statement(graphql):
42+
result = _graphql_statements.get(graphql, None)
43+
44+
if result is not None:
45+
return result
46+
47+
result = GraphQLStatement(graphql)
48+
49+
_graphql_statements[graphql] = result
50+
51+
return result

newrelic/hooks/framework_graphql.py

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

15-
from newrelic.api.time_trace import notice_error, current_trace
1615
from newrelic.api.error_trace import ErrorTrace, ErrorTraceWrapper
1716
from newrelic.api.function_trace import FunctionTrace, FunctionTraceWrapper
1817
from newrelic.api.graphql_trace import GraphQLResolverTrace, GraphQLOperationTrace
18+
from newrelic.api.time_trace import notice_error, current_trace
1919
from newrelic.api.transaction import current_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
22+
from newrelic.core.graphql_utils import graphql_statement
2223
from collections import deque
2324
from copy import copy
2425

2526

27+
def graphql_version():
28+
from graphql import __version__ as version
29+
return tuple(int(v) for v in version.split("."))
30+
31+
2632
def ignore_graphql_duplicate_exception(exc, val, tb):
2733
from graphql.error import GraphQLError
2834

@@ -67,17 +73,6 @@ def ignore_graphql_duplicate_exception(exc, val, tb):
6773
return None # Follow original exception matching rules
6874

6975

70-
def wrap_execute(wrapped, instance, args, kwargs):
71-
transaction = current_transaction()
72-
if transaction is None:
73-
return wrapped(*args, **kwargs)
74-
75-
transaction.set_transaction_name(callable_name(wrapped), priority=1)
76-
with GraphQLOperationTrace():
77-
with ErrorTrace(ignore=ignore_graphql_duplicate_exception):
78-
return wrapped(*args, **kwargs)
79-
80-
8176
def wrap_executor_context_init(wrapped, instance, args, kwargs):
8277
result = wrapped(*args, **kwargs)
8378

@@ -250,10 +245,7 @@ def wrap_resolve_field(wrapped, instance, args, kwargs):
250245
if transaction is None:
251246
return wrapped(*args, **kwargs)
252247

253-
from graphql import __version__ as version
254-
version = tuple(int(v) for v in version.split("."))
255-
256-
if version <= (3, 0, 0):
248+
if graphql_version() <= (3, 0, 0):
257249
bind_resolve_field = bind_resolve_field_v2
258250
else:
259251
bind_resolve_field = bind_resolve_field_v3
@@ -272,9 +264,46 @@ def wrap_resolve_field(wrapped, instance, args, kwargs):
272264
return wrapped(*args, **kwargs)
273265

274266

267+
def bind_graphql_impl_query(schema, source, *args, **kwargs):
268+
return source
269+
270+
271+
def bind_execute_graphql_query(
272+
schema,
273+
request_string="",
274+
root=None,
275+
context=None,
276+
variables=None,
277+
operation_name=None,
278+
middleware=None,
279+
backend=None,
280+
**execute_options):
281+
282+
return request_string
283+
284+
285+
def wrap_graphql_impl(wrapped, instance, args, kwargs):
286+
transaction = current_transaction()
287+
288+
if not transaction:
289+
return wrapped(*args, **kwargs)
290+
291+
if graphql_version() <= (3, 0, 0):
292+
bind_query = bind_execute_graphql_query
293+
else:
294+
bind_query = bind_graphql_impl_query
295+
296+
query = bind_query(*args, **kwargs)
297+
if hasattr(query, "body"):
298+
query = query.body
299+
300+
with GraphQLOperationTrace() as trace:
301+
trace.statement = graphql_statement(query)
302+
with ErrorTrace(ignore=ignore_graphql_duplicate_exception):
303+
return wrapped(*args, **kwargs)
304+
305+
275306
def instrument_graphql_execute(module):
276-
if hasattr(module, "execute"):
277-
wrap_function_wrapper(module, "execute", wrap_execute)
278307
if hasattr(module, "get_field_def"):
279308
wrap_function_wrapper(module, "get_field_def", wrap_get_field_def)
280309
if hasattr(module, "ExecutionContext"):
@@ -323,3 +352,9 @@ def instrument_graphql_error_located_error(module):
323352

324353
def instrument_graphql_validate(module):
325354
wrap_function_wrapper(module, "validate", wrap_validate)
355+
356+
def instrument_graphql(module):
357+
if hasattr(module, "graphql_impl"):
358+
wrap_function_wrapper(module, "graphql_impl", wrap_graphql_impl)
359+
if hasattr(module, "execute_graphql"):
360+
wrap_function_wrapper(module, "execute_graphql", wrap_graphql_impl)

tests/framework_graphql/_target_application.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,10 @@ def resolve_hello(root, info):
6868
return "Hello!"
6969

7070

71+
def resolve_echo(root, info, echo):
72+
return echo
73+
74+
7175
def resolve_error(root, info):
7276
raise RuntimeError("Runtime Error!")
7377

@@ -79,6 +83,11 @@ def resolve_error(root, info):
7983
resolver=resolve_library,
8084
args={"index": GraphQLArgument(GraphQLNonNull(GraphQLInt))},
8185
)
86+
echo_field = GraphQLField(
87+
GraphQLString,
88+
resolver=resolve_echo,
89+
args={"echo": GraphQLArgument(GraphQLNonNull(GraphQLString))},
90+
)
8291
storage_field = GraphQLField(
8392
Storage,
8493
resolver=resolve_storage,
@@ -99,6 +108,11 @@ def resolve_error(root, info):
99108
resolve=resolve_library,
100109
args={"index": GraphQLArgument(GraphQLNonNull(GraphQLInt))},
101110
)
111+
echo_field = GraphQLField(
112+
GraphQLString,
113+
resolve=resolve_echo,
114+
args={"echo": GraphQLArgument(GraphQLNonNull(GraphQLString))},
115+
)
102116
storage_field = GraphQLField(
103117
Storage,
104118
resolve=resolve_storage,
@@ -117,10 +131,11 @@ def resolve_error(root, info):
117131
name="Query",
118132
fields={
119133
"hello": hello_field,
134+
"library": library_field,
135+
"echo": echo_field,
120136
"storage": storage_field,
121137
"error": error_field,
122138
"error_non_null": error_non_null_field,
123-
"library": library_field,
124139
},
125140
)
126141

0 commit comments

Comments
 (0)