diff --git a/.circleci/config.yml b/.circleci/config.yml index 4eed14944ee..c99a25db642 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -738,6 +738,13 @@ jobs: - run_tox_scenario: pattern: '^gevent_contrib-' + graphql: + <<: *machine_executor + steps: + - run_test: + pattern: "graphql" + snapshot: true + grpc: <<: *machine_executor parallelism: 7 @@ -1143,6 +1150,7 @@ requires_tests: &requires_tests - flask - futures - gevent + - graphql - grpc - httplib - httpx @@ -1241,6 +1249,7 @@ workflows: - flask: *requires_base_venvs - futures: *requires_base_venvs - gevent: *requires_base_venvs + - graphql: *requires_base_venvs - grpc: *requires_base_venvs - httplib: *requires_base_venvs - httpx: *requires_base_venvs diff --git a/ddtrace/_monkey.py b/ddtrace/_monkey.py index 2ee14742f42..ae264125c38 100644 --- a/ddtrace/_monkey.py +++ b/ddtrace/_monkey.py @@ -34,6 +34,7 @@ "algoliasearch": True, "futures": True, "gevent": True, + "graphql": True, "grpc": True, "httpx": True, "mongoengine": True, diff --git a/ddtrace/contrib/graphql/__init__.py b/ddtrace/contrib/graphql/__init__.py new file mode 100644 index 00000000000..f5f520aedb1 --- /dev/null +++ b/ddtrace/contrib/graphql/__init__.py @@ -0,0 +1,57 @@ +""" +This integration instruments ``graphql-core`` queries. + +Enabling +~~~~~~~~ + +The graphql integration is enabled automatically when using +:ref:`ddtrace-run ` or :func:`patch_all() `. + +Or use :func:`patch() ` to manually enable the integration:: + + from ddtrace import patch + patch(graphql=True) + import graphql + ... + +Global Configuration +~~~~~~~~~~~~~~~~~~~~ + +.. py:data:: ddtrace.config.graphql["service"] + + The service name reported by default for graphql instances. + + This option can also be set with the ``DD_SERVICE`` environment + variable. + + Default: ``"graphql"`` + +.. py:data:: ddtrace.config.graphql["resolvers_enabled"] + + To enable ``graphql.resolve`` spans set ``DD_TRACE_GRAPHQL_RESOLVERS_ENABLED`` to True + + Default: ``False`` + + Enabling instrumentation for resolvers will produce a ``graphql.resolve`` span for every graphql field. + For complex graphql queries this could produce large traces. + + +To configure the graphql integration using the +``Pin`` API:: + + from ddtrace import Pin + import graphql + + Pin.override(graphql, service="mygraphql") +""" +from ...internal.utils.importlib import require_modules + + +required_modules = ["graphql"] + +with require_modules(required_modules) as missing_modules: + if not missing_modules: + from .patch import patch + from .patch import unpatch + + __all__ = ["patch", "unpatch"] diff --git a/ddtrace/contrib/graphql/patch.py b/ddtrace/contrib/graphql/patch.py new file mode 100644 index 00000000000..2d72d4f73d2 --- /dev/null +++ b/ddtrace/contrib/graphql/patch.py @@ -0,0 +1,271 @@ +import os +import re +import sys +from typing import TYPE_CHECKING + + +if TYPE_CHECKING: + from typing import Callable + from typing import Dict + from typing import Iterable + from typing import List + from typing import Tuple + from typing import Union + + from ddtrace import Span + +import graphql +from graphql import MiddlewareManager +from graphql.error import GraphQLError +from graphql.execution import ExecutionResult +from graphql.language.source import Source + +from ddtrace import config +from ddtrace.constants import ANALYTICS_SAMPLE_RATE_KEY +from ddtrace.constants import ERROR_MSG +from ddtrace.constants import ERROR_TYPE +from ddtrace.constants import SPAN_MEASURED_KEY +from ddtrace.internal.compat import stringify +from ddtrace.internal.utils import ArgumentError +from ddtrace.internal.utils import get_argument_value +from ddtrace.internal.utils import set_argument_value +from ddtrace.internal.utils.formats import asbool +from ddtrace.internal.utils.version import parse_version +from ddtrace.internal.wrapping import unwrap +from ddtrace.internal.wrapping import wrap +from ddtrace.pin import Pin + +from .. import trace_utils +from ...ext import SpanTypes + + +_graphql_version = parse_version(getattr(graphql, "__version__")) + +if _graphql_version < (3, 0): + from graphql.language.ast import Document +else: + from graphql.language.ast import DocumentNode as Document + + +config._add( + "graphql", + dict( + _default_service="graphql", + resolvers_enabled=asbool(os.getenv("DD_TRACE_GRAPHQL_RESOLVERS_ENABLED", default=False)), + ), +) + + +def patch(): + if getattr(graphql, "_datadog_patch", False): + return + setattr(graphql, "_datadog_patch", True) + Pin().onto(graphql) + + for module_str, func_name, wrapper in _get_patching_candidates(): + _update_patching(wrap, module_str, func_name, wrapper) + + +def unpatch(): + if not getattr(graphql, "_datadog_patch", False) or _graphql_version < (2, 0): + return + + for module_str, func_name, wrapper in _get_patching_candidates(): + _update_patching(unwrap, module_str, func_name, wrapper) + + setattr(graphql, "_datadog_patch", False) + + +def _get_patching_candidates(): + if _graphql_version < (3, 0): + return [ + ("graphql.graphql", "execute_graphql", _traced_query), + ("graphql.language.parser", "parse", _traced_parse), + ("graphql.validation.validation", "validate", _traced_validate), + ("graphql.execution.executor", "execute", _traced_execute), + ] + return [ + ("graphql.graphql", "graphql_impl", _traced_query), + ("graphql.language.parser", "parse", _traced_parse), + ("graphql.validation.validate", "validate", _traced_validate), + ("graphql.execution.execute", "execute", _traced_execute), + ] + + +def _update_patching(operation, module_str, func_name, wrapper): + module = sys.modules[module_str] + func = getattr(module, func_name) + operation(func, wrapper) + + +def _traced_parse(func, args, kwargs): + pin = Pin.get_from(graphql) + if not pin or not pin.enabled(): + return func(*args, **kwargs) + + # If graphql.parse() is called outside graphql.graphql(), graphql.parse will + # be a top level span. Therefore we must explicitly set the service name. + with pin.tracer.trace( + name="graphql.parse", + service=trace_utils.int_service(pin, config.graphql), + span_type=SpanTypes.GRAPHQL, + ): + return func(*args, **kwargs) + + +def _traced_validate(func, args, kwargs): + pin = Pin.get_from(graphql) + if not pin or not pin.enabled(): + return func(*args, **kwargs) + + # If graphql.validate() is called outside graphql.graphql(), graphql.validate will + # be a top level span. Therefore we must explicitly set the service name. + with pin.tracer.trace( + name="graphql.validate", + service=trace_utils.int_service(pin, config.graphql), + span_type=SpanTypes.GRAPHQL, + ) as span: + errors = func(*args, **kwargs) + _set_span_errors(errors, span) + return errors + + +def _traced_execute(func, args, kwargs): + pin = Pin.get_from(graphql) + if not pin or not pin.enabled(): + return func(*args, **kwargs) + + if config.graphql.resolvers_enabled: + # patch resolvers + args, kwargs = _inject_trace_middleware_to_args(_resolver_middleware, args, kwargs) + + # set resource name + if _graphql_version < (3, 0): + document = get_argument_value(args, kwargs, 1, "document_ast") + else: + document = get_argument_value(args, kwargs, 1, "document") + resource = _get_source_str(document) + + with pin.tracer.trace( + name="graphql.execute", + resource=resource, + service=trace_utils.int_service(pin, config.graphql), + span_type=SpanTypes.GRAPHQL, + ) as span: + result = func(*args, **kwargs) + if isinstance(result, ExecutionResult): + # set error tags if the result contains a list of GraphqlErrors, skip if it's a promise + _set_span_errors(result.errors, span) + return result + + +def _traced_query(func, args, kwargs): + pin = Pin.get_from(graphql) + if not pin or not pin.enabled(): + return func(*args, **kwargs) + + # set resource name + source = get_argument_value(args, kwargs, 1, "source") + resource = _get_source_str(source) + + with pin.tracer.trace( + name="graphql.query", + resource=resource, + service=trace_utils.int_service(pin, config.graphql), + span_type=SpanTypes.GRAPHQL, + ) as span: + # mark span as measured and set sample rate + span.set_tag(SPAN_MEASURED_KEY) + sample_rate = config.graphql.get_analytics_sample_rate() + if sample_rate is not None: + span.set_tag(ANALYTICS_SAMPLE_RATE_KEY, sample_rate) + + result = func(*args, **kwargs) + if isinstance(result, ExecutionResult): + # set error tags if the result contains a list of GraphqlErrors, skip if it's a promise + # If the wrapped validate and execute functions return a list of errors we will duplicate + # the span errors here. + _set_span_errors(result.errors, span) + return result + + +def _resolver_middleware(next_middleware, root, info, **args): + """ + trace middleware which wraps the resolvers of graphql fields. + Note - graphql middlewares can not be a partial. It must be a class or a function. + """ + pin = Pin.get_from(graphql) + if not pin or not pin.enabled(): + return next_middleware(root, info, **args) + + with pin.tracer.trace( + name="graphql.resolve", + resource=info.field_name, + span_type=SpanTypes.GRAPHQL, + ): + return next_middleware(root, info, **args) + + +def _inject_trace_middleware_to_args(trace_middleware, args, kwargs): + # type: (Callable, Tuple, Dict) -> Tuple[Tuple, Dict] + """ + Adds a trace middleware to graphql.execute(..., middleware, ...) + """ + middlewares_arg = 8 + if _graphql_version >= (3, 2): + # middleware is the 10th argument graphql.execute(..) version 3.2+ + middlewares_arg = 9 + + # get middlewares from args or kwargs + try: + middlewares = get_argument_value(args, kwargs, middlewares_arg, "middleware") or [] + if isinstance(middlewares, MiddlewareManager): + # First we must get the middlewares iterable from the MiddlewareManager then append + # trace_middleware. For the trace_middleware to be called a new MiddlewareManager will + # need to initialized. This is handled in graphql.execute(): + # https://github.com/graphql-python/graphql-core/blob/v3.2.1/src/graphql/execution/execute.py#L254 + middlewares = middlewares.middlewares # type: Iterable + except ArgumentError: + middlewares = [] + + # Note - graphql middlewares are called in reverse order + # add trace_middleware to the end of the list to wrap the execution of resolver and all middlewares + middlewares = list(middlewares) + [trace_middleware] + + # update args and kwargs to contain trace_middleware + args, kwargs = set_argument_value(args, kwargs, middlewares_arg, "middleware", middlewares) + return args, kwargs + + +def _get_source_str(obj): + # type: (Union[str, Source, Document]) -> str + """ + Parses graphql Documents and Source objects to retrieve + the graphql source input for a request. + """ + if isinstance(obj, str): + source_str = obj + elif isinstance(obj, Source): + source_str = obj.body + elif isinstance(obj, Document): + source_str = obj.loc.source.body + else: + source_str = "" + # remove new lines, tabs and extra whitespace from source_str + return re.sub(r"\s+", " ", source_str).strip() + + +def _set_span_errors(errors, span): + # type: (List[GraphQLError], Span) -> None + if not errors: + # do nothing if the list of graphql errors is empty + return + + span.error = 1 + exc_type_str = "%s.%s" % (GraphQLError.__module__, GraphQLError.__name__) + span._set_str_tag(ERROR_TYPE, exc_type_str) + error_msgs = "\n".join([stringify(error) for error in errors]) + # Since we do not support adding and visualizing multiple tracebacks to one span + # we will not set the error.stack tag on graphql spans. Setting only one traceback + # could be misleading and might obfuscate errors. + span._set_str_tag(ERROR_MSG, error_msgs) diff --git a/ddtrace/ext/__init__.py b/ddtrace/ext/__init__.py index e50c2ca1f7d..a76e96d11e9 100644 --- a/ddtrace/ext/__init__.py +++ b/ddtrace/ext/__init__.py @@ -3,6 +3,7 @@ class SpanTypes(object): CASSANDRA = "cassandra" ELASTICSEARCH = "elasticsearch" GRPC = "grpc" + GRAPHQL = "graphql" HTTP = "http" MONGODB = "mongodb" REDIS = "redis" diff --git a/docs/index.rst b/docs/index.rst index 01cdc44b149..b71a134e193 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -97,6 +97,8 @@ contacting support. +--------------------------------------------------+---------------+----------------+ | :ref:`grpc` | >= 1.12.0 | Yes [5]_ | +--------------------------------------------------+---------------+----------------+ +| :ref:`graphql-core ` | >= 2.0.0 | Yes | ++--------------------------------------------------+---------------+----------------+ | :ref:`httplib` | \* | Yes | +--------------------------------------------------+---------------+----------------+ | :ref:`httpx` | >= 0.14.0 | Yes | diff --git a/docs/integrations.rst b/docs/integrations.rst index 26fc0cae157..de09dbf4729 100644 --- a/docs/integrations.rst +++ b/docs/integrations.rst @@ -189,6 +189,13 @@ gevent .. automodule:: ddtrace.contrib.gevent +.. _graphql: + +graphql +^^^^^^^ +.. automodule:: ddtrace.contrib.graphql + + .. _grpc: Grpc diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt index ec9efe38a25..90ad67bc9a9 100644 --- a/docs/spelling_wordlist.txt +++ b/docs/spelling_wordlist.txt @@ -75,6 +75,7 @@ fastapi formatter gRPC gevent +graphql greenlet greenlets grpc @@ -140,6 +141,7 @@ quickstart redis rediscluster renderers +resolvers repo respawn rq diff --git a/releasenotes/notes/add-graphql-core-support-fd20dcbebfcaa8c7.yaml b/releasenotes/notes/add-graphql-core-support-fd20dcbebfcaa8c7.yaml new file mode 100644 index 00000000000..baa5524561b --- /dev/null +++ b/releasenotes/notes/add-graphql-core-support-fd20dcbebfcaa8c7.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + graphql: add tracing for ``graphql-core>2``. See `the graphql documentation `_ + for more information. diff --git a/riotfile.py b/riotfile.py index 5905c8e3415..966714d4a59 100644 --- a/riotfile.py +++ b/riotfile.py @@ -1427,6 +1427,26 @@ def select_pys(min_version=MIN_PYTHON_VERSION, max_version=MAX_PYTHON_VERSION): ), ], ), + Venv( + name="graphql", + command="pytest {cmdargs} tests/contrib/graphql", + pkgs={"pytest-asyncio": latest}, + venvs=[ + Venv( + pys=select_pys(min_version="3.6", max_version="3.9"), + pkgs={ + # graphql-core<2.2 is not supported in python 3.10 + "graphql-core": ["~=2.0.0", "~=2.1.0"], + }, + ), + Venv( + pys=select_pys(min_version="3.6"), + pkgs={ + "graphql-core": ["~=2.2.0", "~=2.3.0", "~=3.0.0", "~=3.1.0", "~=3.2.0", latest], + }, + ), + ], + ), Venv( name="rq", command="pytest tests/contrib/rq", diff --git a/tests/contrib/graphql/__init__.py b/tests/contrib/graphql/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/contrib/graphql/test_graphql.py b/tests/contrib/graphql/test_graphql.py new file mode 100644 index 00000000000..917f4964167 --- /dev/null +++ b/tests/contrib/graphql/test_graphql.py @@ -0,0 +1,148 @@ +import graphql +import pytest + +from ddtrace import tracer +from ddtrace.contrib.graphql import patch +from ddtrace.contrib.graphql import unpatch +from ddtrace.contrib.graphql.patch import _graphql_version as graphql_version +from tests.utils import override_config +from tests.utils import snapshot + + +@pytest.fixture(autouse=True) +def enable_graphql_patching(): + patch() + yield + unpatch() + + +@pytest.fixture +def enable_graphql_resolvers(): + with override_config("graphql", dict(resolvers_enabled=True)): + yield + + +@pytest.fixture +def test_schema(): + return graphql.GraphQLSchema( + query=graphql.GraphQLObjectType( + name="RootQueryType", + fields={"hello": graphql.GraphQLField(graphql.GraphQLString, None, lambda obj, info: "friend")}, + ) + ) + + +@pytest.fixture +def test_source_str(): + return "{ hello }" + + +@pytest.fixture +def test_middleware(): + def dummy_middleware(next_middleware, root, info, **args): + with tracer.trace("test_middleware"): + return next_middleware(root, info, **args) + + yield dummy_middleware + + +@pytest.fixture +def test_source(test_source_str): + return graphql.Source(test_source_str) + + +@pytest.mark.asyncio +async def test_graphql(test_schema, test_source_str, snapshot_context): + with snapshot_context(): + if graphql_version < (3, 0): + result = graphql.graphql(test_schema, test_source_str) + else: + result = await graphql.graphql(test_schema, test_source_str) + assert result.data == {"hello": "friend"} + + +@pytest.mark.asyncio +async def test_graphql_with_traced_resolver(test_schema, test_source_str, snapshot_context, enable_graphql_resolvers): + with snapshot_context(): + if graphql_version < (3, 0): + result = graphql.graphql(test_schema, test_source_str) + else: + result = await graphql.graphql(test_schema, test_source_str) + assert result.data == {"hello": "friend"} + + +@pytest.mark.asyncio +async def test_graphql_error(test_schema, snapshot_context): + with snapshot_context(ignores=["meta.error.type", "meta.error.msg"]): + if graphql_version < (3, 0): + result = graphql.graphql(test_schema, "{ invalid_schema }") + else: + result = await graphql.graphql(test_schema, "{ invalid_schema }") + assert len(result.errors) == 1 + assert isinstance(result.errors[0], graphql.error.GraphQLError) + assert "Cannot query field" in result.errors[0].message + + +@snapshot(token_override="tests.contrib.graphql.test_graphql.test_graphql") +@pytest.mark.skipif(graphql_version >= (3, 0), reason="graphql version>=3.0 does not return a promise") +def test_graphql_v2_promise(test_schema, test_source_str): + promise = graphql.graphql(test_schema, test_source_str, return_promise=True) + result = promise.get() + assert result.data == {"hello": "friend"} + + +@snapshot( + token_override="tests.contrib.graphql.test_graphql.test_graphql_error", + ignores=["meta.error.type", "meta.error.msg"], +) +@pytest.mark.skipif(graphql_version >= (3, 0), reason="graphql.graphql is NOT async in v2.0") +def test_graphql_error_v2_promise(test_schema): + promise = graphql.graphql(test_schema, "{ invalid_schema }", return_promise=True) + result = promise.get() + assert len(result.errors) == 1 + assert isinstance(result.errors[0], graphql.error.GraphQLError) + assert result.errors[0].message == 'Cannot query field "invalid_schema" on type "RootQueryType".' + + +@snapshot() +@pytest.mark.skipif(graphql_version >= (3, 0), reason="graphql.graphql is NOT async in v2.0") +def test_graphql_v2_with_document(test_schema, test_source_str): + source = graphql.language.source.Source(test_source_str, "GraphQL request") + document_ast = graphql.language.parser.parse(source) + result = graphql.graphql(test_schema, document_ast) + assert result.data == {"hello": "friend"} + + +@snapshot(token_override="tests.contrib.graphql.test_graphql.test_graphql") +@pytest.mark.skipif(graphql_version < (3, 0), reason="graphql.graphql_sync is NOT suppoerted in v2.0") +def test_graphql_sync(test_schema, test_source_str): + result = graphql.graphql_sync(test_schema, test_source_str) + assert result.data == {"hello": "friend"} + + +@snapshot() +def test_graphql_execute_with_middleware(test_schema, test_source_str, test_middleware, enable_graphql_resolvers): + with tracer.trace("test-execute-instrumentation"): + source = graphql.language.source.Source(test_source_str, "GraphQL request") + ast = graphql.language.parser.parse(source) + # execute() can be imported from two modules, ensure both are patched + res1 = graphql.execute(test_schema, ast, middleware=[test_middleware]) + res2 = graphql.execution.execute(test_schema, ast, middleware=[test_middleware]) + assert res1.data == {"hello": "friend"} + assert res2.data == {"hello": "friend"} + + +@snapshot(token_override="tests.contrib.graphql.test_graphql.test_graphql_execute_with_middleware") +@pytest.mark.skipif(graphql_version < (3, 1), reason="graphql.execute_sync is not supported in graphql<3.1") +def test_graphql_execute_sync_with_middlware_manager( + test_schema, test_source_str, test_middleware, enable_graphql_resolvers +): + with tracer.trace("test-execute-instrumentation"): + source = graphql.language.source.Source(test_source_str, "GraphQL request") + ast = graphql.language.parser.parse(source) + middleware_manager = graphql.MiddlewareManager(test_middleware) + # execute_sync() can be imported from two modules, ensure both are patched + res1 = graphql.execute_sync(test_schema, ast, middleware=middleware_manager) + res2 = graphql.execution.execute_sync(test_schema, ast, middleware=middleware_manager) + assert res1.data == {"hello": "friend"} + assert res2.data == {"hello": "friend"} diff --git a/tests/snapshots/tests.contrib.graphql.test_graphql.test_graphql.json b/tests/snapshots/tests.contrib.graphql.test_graphql.test_graphql.json new file mode 100644 index 00000000000..f3379dc6f6f --- /dev/null +++ b/tests/snapshots/tests.contrib.graphql.test_graphql.test_graphql.json @@ -0,0 +1,57 @@ +[[ + { + "name": "graphql.query", + "service": "graphql", + "resource": "{ hello }", + "trace_id": 0, + "span_id": 1, + "parent_id": 0, + "type": "graphql", + "meta": { + "_dd.p.dm": "-0", + "runtime-id": "bc0efee142884beb96fdb35ca56aa3dc" + }, + "metrics": { + "_dd.agent_psr": 1.0, + "_dd.measured": 1, + "_dd.top_level": 1, + "_dd.tracer_kr": 1.0, + "_sampling_priority_v1": 1, + "system.pid": 76591 + }, + "duration": 2409000, + "start": 1658848788443487000 + }, + { + "name": "graphql.parse", + "service": "graphql", + "resource": "graphql.parse", + "trace_id": 0, + "span_id": 2, + "parent_id": 1, + "type": "graphql", + "duration": 147000, + "start": 1658848788444365000 + }, + { + "name": "graphql.validate", + "service": "graphql", + "resource": "graphql.validate", + "trace_id": 0, + "span_id": 3, + "parent_id": 1, + "type": "graphql", + "duration": 674000, + "start": 1658848788444620000 + }, + { + "name": "graphql.execute", + "service": "graphql", + "resource": "{ hello }", + "trace_id": 0, + "span_id": 4, + "parent_id": 1, + "type": "graphql", + "duration": 399000, + "start": 1658848788445460000 + }]] diff --git a/tests/snapshots/tests.contrib.graphql.test_graphql.test_graphql_error.json b/tests/snapshots/tests.contrib.graphql.test_graphql.test_graphql_error.json new file mode 100644 index 00000000000..5de5ef48498 --- /dev/null +++ b/tests/snapshots/tests.contrib.graphql.test_graphql.test_graphql_error.json @@ -0,0 +1,54 @@ +[[ + { + "name": "graphql.query", + "service": "graphql", + "resource": "{ invalid_schema }", + "trace_id": 0, + "span_id": 1, + "parent_id": 0, + "type": "graphql", + "error": 1, + "meta": { + "_dd.p.dm": "-0", + "error.msg": "Cannot query field \"invalid_schema\" on type \"RootQueryType\".", + "error.type": "graphql.error.base.GraphQLError", + "runtime-id": "bc0efee142884beb96fdb35ca56aa3dc" + }, + "metrics": { + "_dd.agent_psr": 1.0, + "_dd.measured": 1, + "_dd.top_level": 1, + "_dd.tracer_kr": 1.0, + "_sampling_priority_v1": 1, + "system.pid": 76591 + }, + "duration": 1244000, + "start": 1658848788529619000 + }, + { + "name": "graphql.parse", + "service": "graphql", + "resource": "graphql.parse", + "trace_id": 0, + "span_id": 2, + "parent_id": 1, + "type": "graphql", + "duration": 112000, + "start": 1658848788529771000 + }, + { + "name": "graphql.validate", + "service": "graphql", + "resource": "graphql.validate", + "trace_id": 0, + "span_id": 3, + "parent_id": 1, + "type": "graphql", + "error": 1, + "meta": { + "error.msg": "Cannot query field \"invalid_schema\" on type \"RootQueryType\".", + "error.type": "graphql.error.base.GraphQLError" + }, + "duration": 768000, + "start": 1658848788530031000 + }]] diff --git a/tests/snapshots/tests.contrib.graphql.test_graphql.test_graphql_execute_with_middleware.json b/tests/snapshots/tests.contrib.graphql.test_graphql.test_graphql_execute_with_middleware.json new file mode 100644 index 00000000000..e7a88b15d06 --- /dev/null +++ b/tests/snapshots/tests.contrib.graphql.test_graphql.test_graphql_execute_with_middleware.json @@ -0,0 +1,106 @@ +[[ + { + "name": "test-execute-instrumentation", + "service": null, + "resource": "test-execute-instrumentation", + "trace_id": 0, + "span_id": 1, + "parent_id": 0, + "meta": { + "_dd.p.dm": "-0", + "runtime-id": "942f610390fa492b9945e108e5aa0f69" + }, + "metrics": { + "_dd.agent_psr": 1.0, + "_dd.top_level": 1, + "_dd.tracer_kr": 1.0, + "_sampling_priority_v1": 1, + "system.pid": 77070 + }, + "duration": 2354000, + "start": 1658848939282326000 + }, + { + "name": "graphql.parse", + "service": "graphql", + "resource": "graphql.parse", + "trace_id": 0, + "span_id": 2, + "parent_id": 1, + "type": "graphql", + "metrics": { + "_dd.top_level": 1 + }, + "duration": 184000, + "start": 1658848939282562000 + }, + { + "name": "graphql.execute", + "service": "graphql", + "resource": "{ hello }", + "trace_id": 0, + "span_id": 3, + "parent_id": 1, + "type": "graphql", + "metrics": { + "_dd.top_level": 1 + }, + "duration": 813000, + "start": 1658848939282899000 + }, + { + "name": "graphql.resolve", + "service": "graphql", + "resource": "hello", + "trace_id": 0, + "span_id": 5, + "parent_id": 3, + "type": "graphql", + "duration": 101000, + "start": 1658848939283224000 + }, + { + "name": "test_middleware", + "service": "graphql", + "resource": "test_middleware", + "trace_id": 0, + "span_id": 7, + "parent_id": 5, + "duration": 30000, + "start": 1658848939283265000 + }, + { + "name": "graphql.execute", + "service": "graphql", + "resource": "{ hello }", + "trace_id": 0, + "span_id": 4, + "parent_id": 1, + "type": "graphql", + "metrics": { + "_dd.top_level": 1 + }, + "duration": 738000, + "start": 1658848939283890000 + }, + { + "name": "graphql.resolve", + "service": "graphql", + "resource": "hello", + "trace_id": 0, + "span_id": 6, + "parent_id": 4, + "type": "graphql", + "duration": 89000, + "start": 1658848939284065000 + }, + { + "name": "test_middleware", + "service": "graphql", + "resource": "test_middleware", + "trace_id": 0, + "span_id": 8, + "parent_id": 6, + "duration": 26000, + "start": 1658848939284102000 + }]] diff --git a/tests/snapshots/tests.contrib.graphql.test_graphql.test_graphql_v2_with_document.json b/tests/snapshots/tests.contrib.graphql.test_graphql.test_graphql_v2_with_document.json new file mode 100644 index 00000000000..4faf87f88c3 --- /dev/null +++ b/tests/snapshots/tests.contrib.graphql.test_graphql.test_graphql_v2_with_document.json @@ -0,0 +1,69 @@ +[[ + { + "name": "graphql.parse", + "service": "graphql", + "resource": "graphql.parse", + "trace_id": 0, + "span_id": 1, + "parent_id": 0, + "type": "graphql", + "meta": { + "_dd.p.dm": "-0", + "runtime-id": "bc0efee142884beb96fdb35ca56aa3dc" + }, + "metrics": { + "_dd.agent_psr": 1.0, + "_dd.top_level": 1, + "_dd.tracer_kr": 1.0, + "_sampling_priority_v1": 1, + "system.pid": 76591 + }, + "duration": 168000, + "start": 1658848788617184000 + }], +[ + { + "name": "graphql.query", + "service": "graphql", + "resource": "{ hello }", + "trace_id": 1, + "span_id": 1, + "parent_id": 0, + "type": "graphql", + "meta": { + "_dd.p.dm": "-0", + "runtime-id": "bc0efee142884beb96fdb35ca56aa3dc" + }, + "metrics": { + "_dd.agent_psr": 1.0, + "_dd.measured": 1, + "_dd.top_level": 1, + "_dd.tracer_kr": 1.0, + "_sampling_priority_v1": 1, + "system.pid": 76591 + }, + "duration": 978000, + "start": 1658848788617517000 + }, + { + "name": "graphql.validate", + "service": "graphql", + "resource": "graphql.validate", + "trace_id": 1, + "span_id": 2, + "parent_id": 1, + "type": "graphql", + "duration": 536000, + "start": 1658848788617622000 + }, + { + "name": "graphql.execute", + "service": "graphql", + "resource": "{ hello }", + "trace_id": 1, + "span_id": 3, + "parent_id": 1, + "type": "graphql", + "duration": 224000, + "start": 1658848788618242000 + }]] diff --git a/tests/snapshots/tests.contrib.graphql.test_graphql.test_graphql_with_traced_resolver.json b/tests/snapshots/tests.contrib.graphql.test_graphql.test_graphql_with_traced_resolver.json new file mode 100644 index 00000000000..a996f35a988 --- /dev/null +++ b/tests/snapshots/tests.contrib.graphql.test_graphql.test_graphql_with_traced_resolver.json @@ -0,0 +1,68 @@ +[[ + { + "name": "graphql.query", + "service": "graphql", + "resource": "{ hello }", + "trace_id": 0, + "span_id": 1, + "parent_id": 0, + "type": "graphql", + "meta": { + "_dd.p.dm": "-0", + "runtime-id": "bc0efee142884beb96fdb35ca56aa3dc" + }, + "metrics": { + "_dd.agent_psr": 1.0, + "_dd.measured": 1, + "_dd.top_level": 1, + "_dd.tracer_kr": 1.0, + "_sampling_priority_v1": 1, + "system.pid": 76591 + }, + "duration": 1456000, + "start": 1658848788502005000 + }, + { + "name": "graphql.parse", + "service": "graphql", + "resource": "graphql.parse", + "trace_id": 0, + "span_id": 2, + "parent_id": 1, + "type": "graphql", + "duration": 96000, + "start": 1658848788502146000 + }, + { + "name": "graphql.validate", + "service": "graphql", + "resource": "graphql.validate", + "trace_id": 0, + "span_id": 3, + "parent_id": 1, + "type": "graphql", + "duration": 540000, + "start": 1658848788502301000 + }, + { + "name": "graphql.execute", + "service": "graphql", + "resource": "{ hello }", + "trace_id": 0, + "span_id": 4, + "parent_id": 1, + "type": "graphql", + "duration": 498000, + "start": 1658848788502935000 + }, + { + "name": "graphql.resolve", + "service": "graphql", + "resource": "hello", + "trace_id": 0, + "span_id": 5, + "parent_id": 4, + "type": "graphql", + "duration": 33000, + "start": 1658848788503087000 + }]]