Skip to content

Commit cea37d2

Browse files
lrafeeiTimPansinoumaannamalai
authored
Graphql server instrumentation (#499)
* Add GraphQL Server Sanic Instrumentation * Co-authored-by: Timothy Pansino <[email protected]> Co-authored-by: Uma Annamalai <[email protected]> * Add co-authors Co-authored-by: Timothy Pansino <[email protected]> Co-authored-by: Uma Annamalai <[email protected]> * Comment out Copyright notice message Co-authored-by: Timothy Pansino <[email protected]> Co-authored-by: Uma Annamalai <[email protected]> Co-authored-by: Timothy Pansino <[email protected]> Co-authored-by: Uma Annamalai <[email protected]>
1 parent 2656a3b commit cea37d2

File tree

6 files changed

+227
-1
lines changed

6 files changed

+227
-1
lines changed

newrelic/config.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2153,6 +2153,12 @@ def _process_module_builtin_defaults():
21532153
"instrument_flask_rest",
21542154
)
21552155

2156+
_process_module_definition(
2157+
"graphql_server",
2158+
"newrelic.hooks.component_graphqlserver",
2159+
"instrument_graphqlserver",
2160+
)
2161+
21562162
# _process_module_definition('web.application',
21572163
# 'newrelic.hooks.framework_webpy')
21582164
# _process_module_definition('web.template',
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
from newrelic.api.asgi_application import wrap_asgi_application
2+
from newrelic.api.error_trace import ErrorTrace
3+
from newrelic.api.graphql_trace import GraphQLOperationTrace
4+
from newrelic.api.transaction import current_transaction
5+
from newrelic.api.transaction_name import TransactionNameWrapper
6+
from newrelic.common.object_names import callable_name
7+
from newrelic.common.object_wrapper import wrap_function_wrapper
8+
from newrelic.core.graphql_utils import graphql_statement
9+
from newrelic.hooks.framework_graphql import (
10+
framework_version as graphql_framework_version,
11+
)
12+
from newrelic.hooks.framework_graphql import ignore_graphql_duplicate_exception
13+
14+
def framework_details():
15+
import graphql_server
16+
return ("GraphQLServer", getattr(graphql_server, "__version__", None))
17+
18+
def bind_query(schema, params, *args, **kwargs):
19+
return getattr(params, "query", None)
20+
21+
22+
def wrap_get_response(wrapped, instance, args, kwargs):
23+
transaction = current_transaction()
24+
25+
if not transaction:
26+
return wrapped(*args, **kwargs)
27+
28+
try:
29+
query = bind_query(*args, **kwargs)
30+
except TypeError:
31+
return wrapped(*args, **kwargs)
32+
33+
framework = framework_details()
34+
transaction.add_framework_info(name=framework[0], version=framework[1])
35+
transaction.add_framework_info(name="GraphQL", version=graphql_framework_version())
36+
37+
if hasattr(query, "body"):
38+
query = query.body
39+
40+
transaction.set_transaction_name(callable_name(wrapped), "GraphQL", priority=10)
41+
42+
with GraphQLOperationTrace() as trace:
43+
trace.product = "GraphQLServer"
44+
trace.statement = graphql_statement(query)
45+
with ErrorTrace(ignore=ignore_graphql_duplicate_exception):
46+
return wrapped(*args, **kwargs)
47+
48+
def instrument_graphqlserver(module):
49+
wrap_function_wrapper(module, "get_response", wrap_get_response)
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
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 sanic import Sanic
16+
from graphql_server.sanic import GraphQLView
17+
from testing_support.asgi_testing import AsgiTest
18+
19+
from graphql import GraphQLObjectType, GraphQLString, GraphQLSchema, GraphQLField
20+
21+
22+
def resolve_hello(root, info):
23+
return "Hello!"
24+
25+
hello_field = GraphQLField(GraphQLString, resolve=resolve_hello)
26+
query = GraphQLObjectType(
27+
name="Query",
28+
fields={
29+
"hello": hello_field,
30+
},
31+
)
32+
33+
app = Sanic(name="SanicGraphQL")
34+
routes = [
35+
app.add_route(GraphQLView.as_view(schema=GraphQLSchema(query=query)), "/graphql"),
36+
]
37+
38+
target_application = AsgiTest(app)
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
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 testing_support.fixtures import (
16+
code_coverage_fixture,
17+
collector_agent_registration_fixture,
18+
collector_available_fixture,
19+
)
20+
21+
_coverage_source = [
22+
"newrelic.hooks.component_graphqlserver",
23+
]
24+
25+
code_coverage = code_coverage_fixture(source=_coverage_source)
26+
27+
_default_settings = {
28+
"transaction_tracer.explain_threshold": 0.0,
29+
"transaction_tracer.transaction_threshold": 0.0,
30+
"transaction_tracer.stack_trace_threshold": 0.0,
31+
"debug.log_data_collector_payloads": True,
32+
"debug.record_transaction_failure": True,
33+
}
34+
35+
collector_agent_registration = collector_agent_registration_fixture(
36+
app_name="Python Agent Test (component_graphqlserver)",
37+
default_settings=_default_settings,
38+
)
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
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+
import json
16+
17+
import pytest
18+
from testing_support.fixtures import dt_enabled, validate_transaction_metrics
19+
from testing_support.validators.validate_span_events import validate_span_events
20+
from testing_support.validators.validate_transaction_count import (
21+
validate_transaction_count,
22+
)
23+
24+
25+
@pytest.fixture(scope="session")
26+
def target_application():
27+
import _test_graphql
28+
29+
return _test_graphql.target_application
30+
31+
32+
@dt_enabled
33+
def test_graphql_metrics_and_attrs(target_application):
34+
from graphql import __version__ as graphql_version
35+
from graphql_server import __version__ as graphql_server_version
36+
from sanic import __version__ as sanic_version
37+
38+
FRAMEWORK_METRICS = [
39+
("Python/Framework/GraphQL/%s" % graphql_version, 1),
40+
("Python/Framework/GraphQLServer/%s" % graphql_server_version, 1),
41+
("Python/Framework/Sanic/%s" % sanic_version, 1),
42+
]
43+
_test_scoped_metrics = [
44+
("GraphQL/resolve/GraphQLServer/hello", 1),
45+
("GraphQL/operation/GraphQLServer/query/<anonymous>/hello", 1),
46+
("Function/graphql_server.sanic.graphqlview:GraphQLView.post", 1),
47+
]
48+
_test_unscoped_metrics = [
49+
("GraphQL/all", 1),
50+
("GraphQL/GraphQLServer/all", 1),
51+
("GraphQL/allWeb", 1),
52+
("GraphQL/GraphQLServer/allWeb", 1),
53+
] + _test_scoped_metrics
54+
55+
_expected_query_operation_attributes = {
56+
"graphql.operation.type": "query",
57+
"graphql.operation.name": "<anonymous>",
58+
"graphql.operation.query": "{ hello }",
59+
}
60+
_expected_query_resolver_attributes = {
61+
"graphql.field.name": "hello",
62+
"graphql.field.parentType": "Query",
63+
"graphql.field.path": "hello",
64+
"graphql.field.returnType": "String",
65+
}
66+
67+
@validate_span_events(exact_agents=_expected_query_operation_attributes)
68+
@validate_span_events(exact_agents=_expected_query_resolver_attributes)
69+
@validate_transaction_metrics(
70+
"query/<anonymous>/hello",
71+
"GraphQL",
72+
scoped_metrics=_test_scoped_metrics,
73+
rollup_metrics=_test_unscoped_metrics + FRAMEWORK_METRICS,
74+
)
75+
def _test():
76+
response = target_application.make_request(
77+
"POST", "/graphql", body=json.dumps({"query": "{ hello }"}), headers={"Content-Type": "application/json"}
78+
)
79+
assert response.status == 200
80+
assert "Hello!" in response.body.decode("utf-8")
81+
82+
_test()
83+
84+
85+
@validate_transaction_count(0)
86+
def test_ignored_introspection_transactions(target_application):
87+
response = target_application.make_request(
88+
"POST", "/graphql", body=json.dumps({"query": "{ __schema { types { name } } }"}), headers={"Content-Type": "application/json"}
89+
)
90+
assert response.status == 200

tox.ini

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ envlist =
6060
python-component_djangorestframework-py27-djangorestframework0300,
6161
python-component_djangorestframework-{py36,py37,py38,py39,py310}-djangorestframeworklatest,
6262
python-component_flask_rest-{py27,py36,py37,py38,py39,pypy,pypy3},
63+
python-component_graphqlserver-{py36,py37,py38,py39,py310},
6364
python-component_tastypie-{py27,pypy}-tastypie0143,
6465
python-component_tastypie-{py36,py37,py38,py39,pypy3}-tastypie{0143,latest},
6566
python-coroutines_asyncio-{py36,py37,py38,py39,py310,pypy3},
@@ -147,7 +148,7 @@ envlist =
147148
usefixtures =
148149
collector_available_fixture
149150
collector_agent_registration
150-
code_coverage
151+
; code_coverage
151152

152153
[testenv]
153154
deps =
@@ -182,6 +183,9 @@ deps =
182183
component_flask_rest: flask-restful
183184
component_flask_rest: flask-restplus
184185
component_flask_rest: flask-restx
186+
component_graphqlserver: graphql-server[sanic]==3.0.0b5
187+
component_graphqlserver: sanic>20
188+
component_graphqlserver: jinja2
185189
component_tastypie-tastypie0143: django-tastypie<0.14.4
186190
component_tastypie-{py27,pypy}-tastypie0143: django<1.12
187191
component_tastypie-{py36,py37,py38,py39,pypy3}-tastypie0143: django<3.0.1
@@ -359,6 +363,7 @@ changedir =
359363
application_gearman: tests/application_gearman
360364
component_djangorestframework: tests/component_djangorestframework
361365
component_flask_rest: tests/component_flask_rest
366+
component_graphqlserver: tests/component_graphqlserver
362367
component_tastypie: tests/component_tastypie
363368
coroutines_asyncio: tests/coroutines_asyncio
364369
cross_agent: tests/cross_agent

0 commit comments

Comments
 (0)