Repository: strawberry-graphql/strawberry

Issue:
Tracing - ignore introspection and default resolvers
We just merged support for Apollo tracing, but we can improve it to not trace introspection queries and default resolvers (tracing all the resolvers will probably be too expensive, and also it is not a useful information).

The check should be done in both async and non-async `resolve`: https://github.com/strawberry-graphql/strawberry/blob/master/strawberry/extensions/tracing/apollo.py#L132



Discussion:
It is easy to figure out with the self, _next and root what is being done? I can help here.
@flpStrri we migth need to add an attribute on the resolver, here's where we fallback to gettattr: https://github.com/strawberry-graphql/strawberry/blob/master/strawberry/resolvers.py#L50-L52

this is our version of default resolver, it might not be easy to understand if it is default resolver though (unless we can access `field.base_resolver`), so we might need a bit of restructuring of the code, happy to pair on this if needed

for introspection I think we can check the field name in `info` :)
Would be great. You ping me at Discord so we can set a time? Now that JetBrains released their [pair programming extension](https://blog.jetbrains.com/blog/2020/09/28/code-with-me-eap/), it will be much better!

Resolving patch:
diff --git a/strawberry/extensions/tracing/apollo.py b/strawberry/extensions/tracing/apollo.py
--- a/strawberry/extensions/tracing/apollo.py
+++ b/strawberry/extensions/tracing/apollo.py
@@ -4,23 +4,13 @@
 from datetime import datetime
 from inspect import isawaitable
 
-from graphql import GraphQLResolveInfo
 from strawberry.extensions import Extension
 from strawberry.types.execution import ExecutionContext
 
+from .utils import get_path_from_info, should_skip_tracing
 
-DATETIME_FORMAT = "%Y-%m-%dT%H:%M:%S.%fZ"
-
-
-def get_path_from_info(info: GraphQLResolveInfo) -> typing.List[str]:
-    path = info.path
-    elements = []
-
-    while path:
-        elements.append(path.key)
-        path = path.prev
 
-    return elements[::-1]
+DATETIME_FORMAT = "%Y-%m-%dT%H:%M:%S.%fZ"
 
 
 @dataclasses.dataclass
@@ -130,6 +120,14 @@ def get_results(self):
         return {"tracing": self.stats.to_json()}
 
     async def resolve(self, _next, root, info, *args, **kwargs):
+        if should_skip_tracing(_next, info):
+            result = _next(root, info, *args, **kwargs)
+
+            if isawaitable(result):
+                result = await result  # pragma: no cover
+
+            return result
+
         start_timestamp = self.now()
 
         resolver_stats = ApolloResolverStats(
@@ -155,6 +153,9 @@ async def resolve(self, _next, root, info, *args, **kwargs):
 
 class ApolloTracingExtensionSync(ApolloTracingExtension):
     def resolve(self, _next, root, info, *args, **kwargs):
+        if should_skip_tracing(_next, info):
+            return _next(root, info, *args, **kwargs)
+
         start_timestamp = self.now()
 
         resolver_stats = ApolloResolverStats(
diff --git a/strawberry/extensions/tracing/utils.py b/strawberry/extensions/tracing/utils.py
new file mode 100644
--- /dev/null
+++ b/strawberry/extensions/tracing/utils.py
@@ -0,0 +1,39 @@
+from typing import Callable, List, Union
+
+from graphql import GraphQLResolveInfo
+from strawberry.resolvers import is_default_resolver
+
+
+def is_instrospection_key(key: Union[str, int]) -> bool:
+    # from: https://spec.graphql.org/June2018/#sec-Schema
+    # > All types and directives defined within a schema must not have a name which
+    # > begins with "__" (two underscores), as this is used exclusively
+    # > by GraphQL’s introspection system.
+
+    return str(key).startswith("__")
+
+
+def is_instrospection_field(info: GraphQLResolveInfo) -> bool:
+    path = info.path
+
+    while path:
+        if is_instrospection_key(path.key):
+            return True
+        path = path.prev
+
+    return False
+
+
+def should_skip_tracing(resolver: Callable, info: GraphQLResolveInfo) -> bool:
+    return is_instrospection_field(info) or is_default_resolver(resolver)
+
+
+def get_path_from_info(info: GraphQLResolveInfo) -> List[str]:
+    path = info.path
+    elements = []
+
+    while path:
+        elements.append(path.key)
+        path = path.prev
+
+    return elements[::-1]
diff --git a/strawberry/resolvers.py b/strawberry/resolvers.py
--- a/strawberry/resolvers.py
+++ b/strawberry/resolvers.py
@@ -6,6 +6,11 @@
 from .utils.inspect import get_func_args
 
 
+def is_default_resolver(func: Callable) -> bool:
+    """Check whether the function is a default resolver or a user provided one."""
+    return getattr(func, "_is_default", False)
+
+
 def get_resolver(field: FieldDefinition) -> Callable:
     def _check_permissions(source, info, **kwargs):
         """
@@ -58,4 +63,6 @@ def _resolver(source, info, **kwargs):
 
         return result
 
+    _resolver._is_default = not field.base_resolver  # type: ignore
+
     return _resolver


Test patch:
diff --git a/tests/schema/extensions/test_apollo.py b/tests/schema/extensions/test_apollo.py
--- a/tests/schema/extensions/test_apollo.py
+++ b/tests/schema/extensions/test_apollo.py
@@ -2,6 +2,7 @@
 
 import strawberry
 from freezegun import freeze_time
+from graphql.utilities import get_introspection_query
 from strawberry.extensions.tracing.apollo import (
     ApolloTracingExtension,
     ApolloTracingExtensionSync,
@@ -54,14 +55,6 @@ def person(self, info) -> Person:
                         "startOffset": 0,
                         "duration": 0,
                     },
-                    {
-                        "path": ["person", "name"],
-                        "field_name": "name",
-                        "parentType": "Person",
-                        "returnType": "String!",
-                        "startOffset": 0,
-                        "duration": 0,
-                    },
                 ]
             },
             "validation": {"startOffset": 0, "duration": 0},
@@ -83,6 +76,10 @@ class Person:
 
     @strawberry.type
     class Query:
+        @strawberry.field
+        def example(self, info) -> str:
+            return "Hi"
+
         @strawberry.field
         async def person(self, info) -> Person:
             return Person()
@@ -91,6 +88,7 @@ async def person(self, info) -> Person:
 
     query = """
         query {
+            example
             person {
                 name
             }
@@ -110,18 +108,18 @@ async def person(self, info) -> Person:
             "execution": {
                 "resolvers": [
                     {
-                        "path": ["person"],
-                        "field_name": "person",
+                        "duration": 0,
+                        "field_name": "example",
                         "parentType": "Query",
-                        "returnType": "Person!",
+                        "path": ["example"],
+                        "returnType": "String!",
                         "startOffset": 0,
-                        "duration": 0,
                     },
                     {
-                        "path": ["person", "name"],
-                        "field_name": "name",
-                        "parentType": "Person",
-                        "returnType": "String!",
+                        "path": ["person"],
+                        "field_name": "person",
+                        "parentType": "Query",
+                        "returnType": "Person!",
                         "startOffset": 0,
                         "duration": 0,
                     },
@@ -131,3 +129,72 @@ async def person(self, info) -> Person:
             "parsing": {"startOffset": 0, "duration": 0},
         }
     }
+
+
+@freeze_time("20120114 12:00:01")
+def test_should_not_trace_introspection_sync_queries(mocker):
+    mocker.patch(
+        "strawberry.extensions.tracing.apollo.time.perf_counter_ns", return_value=0
+    )
+
+    @strawberry.type
+    class Person:
+        name: str = "Jess"
+
+    @strawberry.type
+    class Query:
+        @strawberry.field
+        def person(self, info) -> Person:
+            return Person()
+
+    schema = strawberry.Schema(query=Query, extensions=[ApolloTracingExtensionSync])
+
+    result = schema.execute_sync(get_introspection_query())
+
+    assert not result.errors
+    assert result.extensions == {
+        "tracing": {
+            "version": 1,
+            "startTime": "2012-01-14T12:00:01.000000Z",
+            "endTime": "2012-01-14T12:00:01.000000Z",
+            "duration": 0,
+            "execution": {"resolvers": []},
+            "validation": {"startOffset": 0, "duration": 0},
+            "parsing": {"startOffset": 0, "duration": 0},
+        }
+    }
+
+
+@pytest.mark.asyncio
+@freeze_time("20120114 12:00:01")
+async def test_should_not_trace_introspection_async_queries(mocker):
+    mocker.patch(
+        "strawberry.extensions.tracing.apollo.time.perf_counter_ns", return_value=0
+    )
+
+    @strawberry.type
+    class Person:
+        name: str = "Jess"
+
+    @strawberry.type
+    class Query:
+        @strawberry.field
+        async def person(self, info) -> Person:
+            return Person()
+
+    schema = strawberry.Schema(query=Query, extensions=[ApolloTracingExtension])
+
+    result = await schema.execute(get_introspection_query())
+
+    assert not result.errors
+    assert result.extensions == {
+        "tracing": {
+            "version": 1,
+            "startTime": "2012-01-14T12:00:01.000000Z",
+            "endTime": "2012-01-14T12:00:01.000000Z",
+            "duration": 0,
+            "execution": {"resolvers": []},
+            "validation": {"startOffset": 0, "duration": 0},
+            "parsing": {"startOffset": 0, "duration": 0},
+        }
+    }