Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions newrelic/hooks/framework_graphql.py
Original file line number Diff line number Diff line change
Expand Up @@ -381,8 +381,11 @@ def wrap_resolver(wrapped, instance, args, kwargs):
if transaction is None:
return wrapped(*args, **kwargs)

name = callable_name(wrapped)
base_resolver = getattr(wrapped, "_nr_base_resolver", wrapped)

name = callable_name(base_resolver)
transaction.set_transaction_name(name, "GraphQL", priority=13)
trace = FunctionTrace(name, source=base_resolver)

with ErrorTrace(ignore=ignore_graphql_duplicate_exception):
sync_start_time = time.time()
Expand All @@ -391,7 +394,7 @@ def wrap_resolver(wrapped, instance, args, kwargs):
if is_promise(result) and result.is_pending and graphql_version() < (3, 0):
@functools.wraps(wrapped)
def nr_promise_resolver_error_wrapper(v):
with FunctionTrace(name, source=wrapped):
with trace:
with ErrorTrace(ignore=ignore_graphql_duplicate_exception):
try:
return result.get()
Expand All @@ -402,10 +405,10 @@ def nr_promise_resolver_error_wrapper(v):
elif isawaitable(result) and not is_promise(result):
# Grab any async resolvers and wrap with traces
return nr_coro_resolver_error_wrapper(
wrapped, name, ignore_graphql_duplicate_exception, result, transaction
wrapped, name, trace, ignore_graphql_duplicate_exception, result, transaction
)
else:
with FunctionTrace(name, source=wrapped) as trace:
with trace:
trace.start_time = sync_start_time
if is_promise(result) and result.is_rejected:
result.catch(catch_promise_error).get()
Expand Down
4 changes: 2 additions & 2 deletions newrelic/hooks/framework_graphql_py3.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@ async def _nr_coro_execute_name_wrapper():
return _nr_coro_execute_name_wrapper()


def nr_coro_resolver_error_wrapper(wrapped, name, ignore, result, transaction):
def nr_coro_resolver_error_wrapper(wrapped, name, trace, ignore, result, transaction):
@functools.wraps(wrapped)
async def _nr_coro_resolver_error_wrapper():
with FunctionTrace(name, source=wrapped):
with trace:
with ErrorTrace(ignore=ignore):
try:
return await result
Expand Down
14 changes: 11 additions & 3 deletions newrelic/hooks/framework_strawberry.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,16 @@
def framework_details():
import strawberry

return ("Strawberry", getattr(strawberry, "__version__", None))
try:
version = strawberry.__version__
except Exception:
try:
import pkg_resources
version = pkg_resources.get_distribution("strawberry-graphql").version
except Exception:
version = None

return ("Strawberry", version)


def bind_execute(query, *args, **kwargs):
Expand Down Expand Up @@ -104,8 +113,7 @@ def wrap_from_resolver(wrapped, instance, args, kwargs):
else:
if hasattr(field, "base_resolver"):
if hasattr(field.base_resolver, "wrapped_func"):
resolver_name = callable_name(field.base_resolver.wrapped_func)
result = TransactionNameWrapper(result, resolver_name, "GraphQL", priority=13)
result._nr_base_resolver = field.base_resolver.wrapped_func

return result

Expand Down
14 changes: 10 additions & 4 deletions tests/framework_graphql/test_application.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,10 @@ def error_middleware(next, root, info, **args):
error_middleware = [error_middleware]

if six.PY3:
from test_application_async import error_middleware_async, example_middleware_async
try:
from test_application_async import error_middleware_async, example_middleware_async
except ImportError:
from framework_graphql.test_application_async import error_middleware_async, example_middleware_async

example_middleware.append(example_middleware_async)
error_middleware.append(error_middleware_async)
Expand Down Expand Up @@ -141,6 +144,8 @@ def _test():
def test_query_and_mutation(target_application, is_graphql_2):
framework, version, target_application, is_bg, schema_type = target_application

type_annotation = "!" if framework == "Strawberry" else ""

_test_mutation_scoped_metrics = [
("GraphQL/resolve/%s/storage_add" % framework, 1),
("GraphQL/operation/%s/mutation/<anonymous>/storage_add" % framework, 1),
Expand All @@ -158,7 +163,7 @@ def test_query_and_mutation(target_application, is_graphql_2):
"graphql.field.name": "storage_add",
"graphql.field.parentType": "Mutation",
"graphql.field.path": "storage_add",
"graphql.field.returnType": "String",
"graphql.field.returnType": "String" + type_annotation,
}
_expected_query_operation_attributes = {
"graphql.operation.type": "query",
Expand All @@ -168,7 +173,7 @@ def test_query_and_mutation(target_application, is_graphql_2):
"graphql.field.name": "storage",
"graphql.field.parentType": "Query",
"graphql.field.path": "storage",
"graphql.field.returnType": "[String]",
"graphql.field.returnType": "[String%s]%s" % (type_annotation, type_annotation),
}

@validate_code_level_metrics("_target_schema_%s" % schema_type, "resolve_storage_add")
Expand Down Expand Up @@ -432,11 +437,12 @@ def test_field_resolver_metrics_and_attrs(target_application):
framework, version, target_application, is_bg, schema_type = target_application
field_resolver_metrics = [("GraphQL/resolve/%s/hello" % framework, 1)]

type_annotation = "!" if framework == "Strawberry" else ""
graphql_attrs = {
"graphql.field.name": "hello",
"graphql.field.parentType": "Query",
"graphql.field.path": "hello",
"graphql.field.returnType": "String",
"graphql.field.returnType": "String" + type_annotation,
}

@validate_transaction_metrics(
Expand Down
216 changes: 55 additions & 161 deletions tests/framework_strawberry/_target_application.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,185 +12,79 @@
# See the License for the specific language governing permissions and
# limitations under the License.

from typing import List, Union

import strawberry.mutation
import strawberry.type
from strawberry import Schema, field
from strawberry.asgi import GraphQL
from strawberry.schema.config import StrawberryConfig
from strawberry.types.types import Optional


@strawberry.type
class Author:
first_name: str
last_name: str


@strawberry.type
class Book:
id: int
name: str
isbn: str
author: Author
branch: str


@strawberry.type
class Magazine:
id: int
name: str
issue: int
branch: str


@strawberry.type
class Library:
id: int
branch: str
magazine: List[Magazine]
book: List[Book]
import asyncio
import json
import pytest

from _target_schema_sync import target_schema as target_schema_sync, target_asgi_application as target_asgi_application_sync
from _target_schema_async import target_schema as target_schema_async, target_asgi_application as target_asgi_application_async

Item = Union[Book, Magazine]
Storage = List[str]

def run_sync(schema):
def _run_sync(query, middleware=None):
from graphql.language.source import Source

authors = [
Author(
first_name="New",
last_name="Relic",
),
Author(
first_name="Bob",
last_name="Smith",
),
Author(
first_name="Leslie",
last_name="Jones",
),
]

books = [
Book(
id=1,
name="Python Agent: The Book",
isbn="a-fake-isbn",
author=authors[0],
branch="riverside",
),
Book(
id=2,
name="Ollies for O11y: A Sk8er's Guide to Observability",
isbn="a-second-fake-isbn",
author=authors[1],
branch="downtown",
),
Book(
id=3,
name="[Redacted]",
isbn="a-third-fake-isbn",
author=authors[2],
branch="riverside",
),
]
if middleware is not None:
pytest.skip("Middleware not supported in Strawberry.")

magazines = [
Magazine(id=1, name="Reli Updates Weekly", issue=1, branch="riverside"),
Magazine(id=2, name="Reli: The Forgotten Years", issue=2, branch="downtown"),
Magazine(id=3, name="Node Weekly", issue=1, branch="riverside"),
]
response = schema.execute_sync(query)

if isinstance(query, str) and "error" not in query or isinstance(query, Source) and "error" not in query.body:
assert not response.errors
else:
assert response.errors

libraries = ["riverside", "downtown"]
libraries = [
Library(
id=i + 1,
branch=branch,
magazine=[m for m in magazines if m.branch == branch],
book=[b for b in books if b.branch == branch],
)
for i, branch in enumerate(libraries)
]
return response.data
return _run_sync

storage = []

def run_async(schema):
def _run_async(query, middleware=None):
from graphql.language.source import Source

def resolve_hello():
return "Hello!"
if middleware is not None:
pytest.skip("Middleware not supported in Strawberry.")

loop = asyncio.get_event_loop()
response = loop.run_until_complete(schema.execute(query))

async def resolve_hello_async():
return "Hello!"
if isinstance(query, str) and "error" not in query or isinstance(query, Source) and "error" not in query.body:
assert not response.errors
else:
assert response.errors

return response.data
return _run_async

def resolve_echo(echo: str):
return echo

def run_asgi(app):
def _run_asgi(query, middleware=None):
if middleware is not None:
pytest.skip("Middleware not supported in Strawberry.")

def resolve_library(index: int):
return libraries[index]
response = app.make_request(
"POST", "/", body=json.dumps({"query": query}), headers={"Content-Type": "application/json"}
)
body = json.loads(response.body.decode("utf-8"))

if not isinstance(query, str) or "error" in query:
try:
assert response.status != 200
except AssertionError:
assert body["errors"]
else:
assert response.status == 200
assert "errors" not in body or not body["errors"]

def resolve_storage_add(string: str):
storage.add(string)
return storage
return body["data"]
return _run_asgi


def resolve_storage():
return storage


def resolve_error():
raise RuntimeError("Runtime Error!")


def resolve_search(contains: str):
search_books = [b for b in books if contains in b.name]
search_magazines = [m for m in magazines if contains in m.name]
return search_books + search_magazines


@strawberry.type
class Query:
library: Library = field(resolver=resolve_library)
hello: str = field(resolver=resolve_hello)
hello_async: str = field(resolver=resolve_hello_async)
search: List[Item] = field(resolver=resolve_search)
echo: str = field(resolver=resolve_echo)
storage: Storage = field(resolver=resolve_storage)
error: Optional[str] = field(resolver=resolve_error)
error_non_null: str = field(resolver=resolve_error)

def resolve_library(self, info, index):
return libraries[index]

def resolve_storage(self, info):
return storage

def resolve_search(self, info, contains):
search_books = [b for b in books if contains in b.name]
search_magazines = [m for m in magazines if contains in m.name]
return search_books + search_magazines

def resolve_hello(self, info):
return "Hello!"

def resolve_echo(self, info, echo):
return echo

def resolve_error(self, info) -> str:
raise RuntimeError("Runtime Error!")


@strawberry.type
class Mutation:
@strawberry.mutation
def storage_add(self, string: str) -> str:
storage.append(string)
return str(string)


_target_application = Schema(query=Query, mutation=Mutation, config=StrawberryConfig(auto_camel_case=False))
_target_asgi_application = GraphQL(_target_application)
target_application = {
"sync-sync": run_sync(target_schema_sync),
"async-sync": run_async(target_schema_sync),
"asgi-sync": run_asgi(target_asgi_application_sync),
"async-async": run_async(target_schema_async),
"asgi-async": run_asgi(target_asgi_application_async),
}
Loading