Skip to content

Commit d1bcc83

Browse files
authored
Add graphene-django instrumentation (#1451)
* Add graphene-django instrumentation * Increase naming priority * Remove unused import * Add sychronous schema tests * Clean up test files * Remove commented out code * Megalinter fixes * Add operation & resolver tests * Refine tests * MegaLinter fixes * Suggested reviewer changes * Megalinter fixes
1 parent e32dcbe commit d1bcc83

File tree

14 files changed

+728
-1
lines changed

14 files changed

+728
-1
lines changed

newrelic/config.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2700,6 +2700,10 @@ def _process_module_builtin_defaults():
27002700
"graphene.types.schema", "newrelic.hooks.framework_graphene", "instrument_graphene_types_schema"
27012701
)
27022702

2703+
_process_module_definition(
2704+
"graphene_django.views", "newrelic.hooks.component_graphenedjango", "instrument_graphene_django_views"
2705+
)
2706+
27032707
_process_module_definition("graphql.graphql", "newrelic.hooks.framework_graphql", "instrument_graphql")
27042708
_process_module_definition(
27052709
"graphql.execution.execute", "newrelic.hooks.framework_graphql", "instrument_graphql_execute"
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
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 functools
16+
import sys
17+
from inspect import isawaitable
18+
19+
from newrelic.api.error_trace import ErrorTrace
20+
from newrelic.api.graphql_trace import GraphQLOperationTrace
21+
from newrelic.api.transaction import current_transaction
22+
from newrelic.common.object_names import callable_name
23+
from newrelic.common.object_wrapper import wrap_function_wrapper
24+
from newrelic.common.package_version_utils import get_package_version
25+
from newrelic.common.signature import bind_args
26+
from newrelic.core.graphql_utils import graphql_statement
27+
from newrelic.hooks.framework_graphql import GRAPHQL_VERSION, ignore_graphql_duplicate_exception
28+
29+
GRAPHENE_DJANGO_VERSION = get_package_version("graphene_django")
30+
graphene_django_version_tuple = tuple(map(int, GRAPHENE_DJANGO_VERSION.split(".")))
31+
32+
33+
# Implementation from `newrelic.hooks.framework_graphql_py3`
34+
def nr_coro_execute_graphql_request_wrapper(wrapped, trace, ignore, result):
35+
@functools.wraps(wrapped)
36+
async def _nr_coro_execute_graphql_request_wrapper():
37+
try:
38+
with ErrorTrace(ignore=ignore):
39+
result_ = await result
40+
except:
41+
trace.__exit__(*sys.exc_info())
42+
raise
43+
else:
44+
trace.__exit__(None, None, None)
45+
return result_
46+
47+
return _nr_coro_execute_graphql_request_wrapper()
48+
49+
50+
def wrap_GraphQLView_execute_graphql_request(wrapped, instance, args, kwargs):
51+
transaction = current_transaction()
52+
53+
if not transaction:
54+
return wrapped(*args, **kwargs)
55+
56+
transaction.add_framework_info(name="GraphQL", version=GRAPHQL_VERSION)
57+
transaction.add_framework_info(name="GrapheneDjango", version=GRAPHENE_DJANGO_VERSION)
58+
bound_args = bind_args(wrapped, args, kwargs)
59+
60+
try:
61+
schema = instance.schema.graphql_schema
62+
query = bound_args.get("query", None)
63+
except TypeError:
64+
return wrapped(*args, **kwargs)
65+
66+
transaction.set_transaction_name(callable_name(wrapped), "GraphQL", priority=11)
67+
68+
trace = GraphQLOperationTrace()
69+
70+
trace.statement = graphql_statement(query)
71+
trace.product = "GrapheneDjango"
72+
73+
# Handle Schemas created from frameworks
74+
if hasattr(schema, "_nr_framework"):
75+
framework = schema._nr_framework
76+
transaction.add_framework_info(name=framework[0], version=framework[1])
77+
78+
# Trace must be manually started and stopped to ensure it exists prior to and during the entire duration of the query.
79+
# Otherwise subsequent instrumentation will not be able to find an operation trace and will have issues.
80+
trace.__enter__()
81+
try:
82+
with ErrorTrace(ignore=ignore_graphql_duplicate_exception):
83+
result = wrapped(*args, **kwargs)
84+
except Exception:
85+
# Execution finished synchronously, exit immediately.
86+
trace.__exit__(*sys.exc_info())
87+
raise
88+
else:
89+
trace.set_transaction_name(priority=14)
90+
if isawaitable(result):
91+
# Asynchronous implementations
92+
# Return a coroutine that handles closing the operation trace
93+
return nr_coro_execute_graphql_request_wrapper(wrapped, trace, ignore_graphql_duplicate_exception, result)
94+
else:
95+
# Execution finished synchronously, exit immediately.
96+
trace.__exit__(None, None, None)
97+
return result
98+
99+
100+
def instrument_graphene_django_views(module):
101+
if hasattr(module, "GraphQLView") and hasattr(module.GraphQLView, "execute_graphql_request"):
102+
wrap_function_wrapper(module, "GraphQLView.execute_graphql_request", wrap_GraphQLView_execute_graphql_request)
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
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+
16+
import webtest
17+
from wsgi import application
18+
19+
_target_application = webtest.TestApp(application)
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
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 dummy_app.models import AuthorModel, BookModel, LibraryModel, MagazineModel
16+
from graphene import Field, Int, List, NonNull, ObjectType, Schema, String, Union
17+
from graphene import Mutation as GrapheneMutation
18+
from graphene_django import DjangoObjectType
19+
20+
21+
class Author(DjangoObjectType):
22+
class Meta:
23+
model = AuthorModel
24+
fields = "__all__"
25+
26+
27+
class Book(DjangoObjectType):
28+
class Meta:
29+
model = BookModel
30+
fields = "__all__"
31+
32+
33+
class Magazine(DjangoObjectType):
34+
class Meta:
35+
model = MagazineModel
36+
fields = "__all__"
37+
38+
39+
class Item(Union):
40+
class Meta:
41+
types = (Book, Magazine)
42+
43+
44+
class Library(DjangoObjectType):
45+
class Meta:
46+
model = LibraryModel
47+
fields = "__all__"
48+
49+
50+
Storage = List(String)
51+
52+
53+
authors = [
54+
Author(first_name="New", last_name="Relic"),
55+
Author(first_name="Bob", last_name="Smith"),
56+
Author(first_name="Leslie", last_name="Jones"),
57+
]
58+
59+
books = [
60+
Book(id=1, name="Python Agent: The Book", isbn="a-fake-isbn", author=authors[0], branch="riverside"),
61+
Book(
62+
id=2,
63+
name="Ollies for O11y: A Sk8er's Guide to Observability",
64+
isbn="a-second-fake-isbn",
65+
author=authors[1],
66+
branch="downtown",
67+
),
68+
Book(id=3, name="[Redacted]", isbn="a-third-fake-isbn", author=authors[2], branch="riverside"),
69+
]
70+
71+
magazines = [
72+
Magazine(id=1, name="Reli Updates Weekly", issue=1, branch="riverside"),
73+
Magazine(id=2, name="Reli Updates Weekly", issue=2, branch="downtown"),
74+
Magazine(id=3, name="Node Weekly", issue=1, branch="riverside"),
75+
]
76+
77+
78+
libraries = ["riverside", "downtown"]
79+
libraries = [
80+
Library(
81+
id=i + 1,
82+
branch=branch,
83+
magazine=[m for m in magazines if m.branch == branch],
84+
book=[b for b in books if b.branch == branch],
85+
)
86+
for i, branch in enumerate(libraries)
87+
]
88+
89+
storage = []
90+
91+
92+
def resolve_library(self, info, index):
93+
return libraries[index]
94+
95+
96+
def resolve_storage(self, info):
97+
return [storage.pop()]
98+
99+
100+
def resolve_search(self, info, contains):
101+
search_books = [b for b in books if contains in b.name]
102+
search_magazines = [m for m in magazines if contains in m.name]
103+
return search_books + search_magazines
104+
105+
106+
def resolve_hello(self, info):
107+
return "Hello!"
108+
109+
110+
def resolve_echo(self, info, echo):
111+
return echo
112+
113+
114+
def resolve_error(self, info):
115+
raise RuntimeError("Runtime Error!")
116+
117+
118+
def resolve_storage_add(self, info, string):
119+
storage.append(string)
120+
return StorageAdd(string=string)
121+
122+
123+
class StorageAdd(GrapheneMutation):
124+
class Arguments:
125+
string = String(required=True)
126+
127+
string = String()
128+
mutate = resolve_storage_add
129+
130+
131+
class Query(ObjectType):
132+
library = Field(Library, index=Int(required=True), resolver=resolve_library)
133+
hello = String(resolver=resolve_hello)
134+
search = Field(List(Item), contains=String(required=True), resolver=resolve_search)
135+
echo = Field(String, echo=String(required=True), resolver=resolve_echo)
136+
storage = Field(Storage, resolver=resolve_storage)
137+
error = String(resolver=resolve_error)
138+
error_non_null = Field(NonNull(String), resolver=resolve_error)
139+
error_middleware = String(resolver=resolve_hello)
140+
141+
142+
class Mutation(ObjectType):
143+
storage_add = StorageAdd.Field()
144+
145+
146+
target_schema = Schema(query=Query, mutation=Mutation, auto_camelcase=False)
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
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 pytest
16+
from testing_support.fixtures import collector_agent_registration_fixture, collector_available_fixture
17+
18+
_default_settings = {
19+
"package_reporting.enabled": False, # Turn off package reporting for testing as it causes slow downs.
20+
"transaction_tracer.explain_threshold": 0.0,
21+
"transaction_tracer.transaction_threshold": 0.0,
22+
"transaction_tracer.stack_trace_threshold": 0.0,
23+
"debug.log_data_collector_payloads": True,
24+
"debug.record_transaction_failure": True,
25+
"debug.log_autorum_middleware": True,
26+
}
27+
28+
collector_agent_registration = collector_agent_registration_fixture(
29+
app_name="Python Agent Test (framework_graphene-django)", default_settings=_default_settings
30+
)
31+
32+
33+
@pytest.fixture(scope="session")
34+
def wsgi_app():
35+
from _target_application import _target_application as wsgi_application
36+
37+
return wsgi_application
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
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+
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
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 django.apps import AppConfig
16+
17+
18+
class LibraryConfig(AppConfig):
19+
default_auto_field = "django.db.models.BigAutoField"
20+
name = "dummy_app"
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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 django.db import models
16+
17+
18+
class AuthorModel(models.Model):
19+
first_name = models.CharField(max_length=100)
20+
last_name = models.CharField(max_length=100)
21+
22+
23+
class BookModel(models.Model):
24+
id = models.IntegerField(primary_key=True)
25+
name = models.CharField(max_length=100)
26+
isbn = models.CharField(max_length=100)
27+
author = models.ForeignKey(AuthorModel, on_delete=models.CASCADE)
28+
branch = models.CharField(max_length=100)
29+
30+
31+
class MagazineModel(models.Model):
32+
id = models.IntegerField(primary_key=True)
33+
name = models.CharField(max_length=100)
34+
issue = models.IntegerField()
35+
branch = models.CharField(max_length=100)
36+
37+
38+
class LibraryModel(models.Model):
39+
id = models.IntegerField(primary_key=True)
40+
branch = models.CharField(max_length=100)
41+
magazine = models.ManyToManyField(MagazineModel)
42+
book = models.ManyToManyField(BookModel)

0 commit comments

Comments
 (0)