From fd2112c12dd28f7110b671b090ea51cfcc07143c Mon Sep 17 00:00:00 2001 From: quinna-h Date: Fri, 24 Jan 2025 17:13:10 -0500 Subject: [PATCH 01/24] add span event to errors --- ddtrace/contrib/internal/graphql/patch.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/ddtrace/contrib/internal/graphql/patch.py b/ddtrace/contrib/internal/graphql/patch.py index fe538303a52..120c4792b0e 100644 --- a/ddtrace/contrib/internal/graphql/patch.py +++ b/ddtrace/contrib/internal/graphql/patch.py @@ -293,7 +293,7 @@ def _set_span_errors(errors: List[GraphQLError], span: Span) -> None: if not errors: # do nothing if the list of graphql errors is empty return - + breakpoint() span.error = 1 exc_type_str = "%s.%s" % (GraphQLError.__module__, GraphQLError.__name__) span.set_tag_str(ERROR_TYPE, exc_type_str) @@ -301,7 +301,15 @@ def _set_span_errors(errors: List[GraphQLError], span: Span) -> None: # 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. + locations = [ + f"{err_location.formatted['line']}:{err_location.formatted['column']}" for err_location in errors[0].locations + ] + locations = " ".join(locations) span.set_tag_str(ERROR_MSG, error_msgs) + span._add_event( + name="dd.graphql.query.error", + attributes={"message": errors[0].message, "locations": locations, "path": errors[0].path}, + ) def _set_span_operation_tags(span, document): From 16430b3336f8138142b806a177627708f896cc71 Mon Sep 17 00:00:00 2001 From: quinna-h Date: Fri, 24 Jan 2025 17:14:48 -0500 Subject: [PATCH 02/24] wip --- ddtrace/contrib/internal/graphql/patch.py | 1 - 1 file changed, 1 deletion(-) diff --git a/ddtrace/contrib/internal/graphql/patch.py b/ddtrace/contrib/internal/graphql/patch.py index 120c4792b0e..e5397d0ef50 100644 --- a/ddtrace/contrib/internal/graphql/patch.py +++ b/ddtrace/contrib/internal/graphql/patch.py @@ -293,7 +293,6 @@ def _set_span_errors(errors: List[GraphQLError], span: Span) -> None: if not errors: # do nothing if the list of graphql errors is empty return - breakpoint() span.error = 1 exc_type_str = "%s.%s" % (GraphQLError.__module__, GraphQLError.__name__) span.set_tag_str(ERROR_TYPE, exc_type_str) From 8ff140fe0679c69db24679bdd882943b0f305ff9 Mon Sep 17 00:00:00 2001 From: quinna-h Date: Mon, 27 Jan 2025 18:39:29 -0500 Subject: [PATCH 03/24] wip --- ddtrace/contrib/internal/graphql/patch.py | 77 ++++++++++++++++++++--- 1 file changed, 67 insertions(+), 10 deletions(-) diff --git a/ddtrace/contrib/internal/graphql/patch.py b/ddtrace/contrib/internal/graphql/patch.py index e5397d0ef50..0ba4cbc4dcf 100644 --- a/ddtrace/contrib/internal/graphql/patch.py +++ b/ddtrace/contrib/internal/graphql/patch.py @@ -1,6 +1,7 @@ import os import re import sys +import traceback from typing import TYPE_CHECKING from typing import List @@ -18,6 +19,7 @@ import graphql from graphql import MiddlewareManager + from graphql.error import GraphQLError from graphql.execution import ExecutionResult from graphql.language.source import Source @@ -26,6 +28,7 @@ from ddtrace.constants import _ANALYTICS_SAMPLE_RATE_KEY from ddtrace.constants import _SPAN_MEASURED_KEY from ddtrace.constants import ERROR_MSG +from ddtrace.constants import ERROR_STACK from ddtrace.constants import ERROR_TYPE from ddtrace.contrib import trace_utils from ddtrace.ext import SpanTypes @@ -68,7 +71,6 @@ def get_version(): _GRAPHQL_OPERATION_TYPE = "graphql.operation.type" _GRAPHQL_OPERATION_NAME = "graphql.operation.name" - def patch(): if getattr(graphql, "_datadog_patch", False): return @@ -289,27 +291,82 @@ def _get_source_str(obj): return re.sub(r"\s+", " ", source_str).strip() +# def _validate_error_extensions(error: GraphQLError, extensions: str | None) -> Dict: +# # Parsing: handle `\` and `\\` +# # `field1\,` should match `field1,` +# # `field1\\` should match `field1\` +# if not extensions: +# return {} + +# # split on un-escaped commas +# pattern = r'(? 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_tag_str(ERROR_TYPE, exc_type_str) error_msgs = "\n".join([str(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. - locations = [ - f"{err_location.formatted['line']}:{err_location.formatted['column']}" for err_location in errors[0].locations - ] - locations = " ".join(locations) span.set_tag_str(ERROR_MSG, error_msgs) - span._add_event( - name="dd.graphql.query.error", - attributes={"message": errors[0].message, "locations": locations, "path": errors[0].path}, - ) - + for error in errors: + # [{'line': 1, 'column': 17} {'line': 3, 'column': 5}] -> '1:17' '3:5' + locations = [ + f"{err_location.formatted['line']}:{err_location.formatted['column']}" for err_location in error.locations] + locations = " ".join(locations) + + # breakpoint() + attributes={"message": error.message, + "type": span.get_tag("error.type"), + "locations": locations, + } + + if error.__traceback__: + stacktrace = "".join( + traceback.format_exception( + type(error), error, error.__traceback__, limit=config._span_traceback_max_size + ) + ) + attributes["stacktrace"] = stacktrace + span.set_tag_str(ERROR_STACK, stacktrace) + + + if error.path is not None: + path = ",".join([str(path_obj) for path_obj in error.path]) + attributes["path"] = path + + # TODO: handle user extensions + # if os.environ.get("DD_TRACE_GRAPHQL_ERROR_EXTENSIONS") is not None: + # extensions = os.environ.get("DD_TRACE_GRAPHQL_ERROR_EXTENSIONS") + + # error_extensions = _validate_error_extensions(error, extensions) + # if error_extensions: + # attributes["extensions"] = str(error_extensions) + span._add_event( + name="dd.graphql.query.error", + attributes=attributes, + ) def _set_span_operation_tags(span, document): operation_def = graphql.get_operation_ast(document) From 260237e1875046d073d96b8df6145b5ad7c590df Mon Sep 17 00:00:00 2001 From: quinna-h Date: Wed, 29 Jan 2025 13:57:48 -0500 Subject: [PATCH 04/24] add fields --- ddtrace/contrib/internal/graphql/patch.py | 59 ++++++++++------------- 1 file changed, 25 insertions(+), 34 deletions(-) diff --git a/ddtrace/contrib/internal/graphql/patch.py b/ddtrace/contrib/internal/graphql/patch.py index 0ba4cbc4dcf..2a1034c65c3 100644 --- a/ddtrace/contrib/internal/graphql/patch.py +++ b/ddtrace/contrib/internal/graphql/patch.py @@ -4,6 +4,7 @@ import traceback from typing import TYPE_CHECKING from typing import List +from typing import Dict from ddtrace.internal.schema.span_attribute_schema import SpanDirection from ddtrace.trace import Span @@ -291,30 +292,25 @@ def _get_source_str(obj): return re.sub(r"\s+", " ", source_str).strip() -# def _validate_error_extensions(error: GraphQLError, extensions: str | None) -> Dict: -# # Parsing: handle `\` and `\\` -# # `field1\,` should match `field1,` -# # `field1\\` should match `field1\` -# if not extensions: -# return {} +def _validate_error_extensions(error: GraphQLError, extensions: str | None) -> Dict: + # Validate user-provided extensions + if not extensions: + return {} -# # split on un-escaped commas -# pattern = r'(? None: @@ -331,11 +327,7 @@ def _set_span_errors(errors: List[GraphQLError], span: Span) -> None: # could be misleading and might obfuscate errors. span.set_tag_str(ERROR_MSG, error_msgs) for error in errors: - # [{'line': 1, 'column': 17} {'line': 3, 'column': 5}] -> '1:17' '3:5' - locations = [ - f"{err_location.formatted['line']}:{err_location.formatted['column']}" for err_location in error.locations] - locations = " ".join(locations) - + locations = " ".join(f"{loc.formatted['line']}:{loc.formatted['column']}" for loc in error.locations) # breakpoint() attributes={"message": error.message, "type": span.get_tag("error.type"), @@ -356,13 +348,12 @@ def _set_span_errors(errors: List[GraphQLError], span: Span) -> None: path = ",".join([str(path_obj) for path_obj in error.path]) attributes["path"] = path - # TODO: handle user extensions - # if os.environ.get("DD_TRACE_GRAPHQL_ERROR_EXTENSIONS") is not None: - # extensions = os.environ.get("DD_TRACE_GRAPHQL_ERROR_EXTENSIONS") + if os.environ.get("DD_TRACE_GRAPHQL_ERROR_EXTENSIONS") is not None: + extensions = os.environ.get("DD_TRACE_GRAPHQL_ERROR_EXTENSIONS") - # error_extensions = _validate_error_extensions(error, extensions) - # if error_extensions: - # attributes["extensions"] = str(error_extensions) + error_extensions = _validate_error_extensions(error, extensions) + if error_extensions: + attributes["extensions"] = str(error_extensions) span._add_event( name="dd.graphql.query.error", attributes=attributes, From e64f04aede2a596b18692db6ed0df618a1418b71 Mon Sep 17 00:00:00 2001 From: quinna-h Date: Wed, 29 Jan 2025 17:21:59 -0500 Subject: [PATCH 05/24] add test --- ddtrace/contrib/internal/graphql/patch.py | 1 - tests/contrib/graphql/test_graphql.py | 40 +++++++++++++++++++++-- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/ddtrace/contrib/internal/graphql/patch.py b/ddtrace/contrib/internal/graphql/patch.py index 2a1034c65c3..d273a01d5e5 100644 --- a/ddtrace/contrib/internal/graphql/patch.py +++ b/ddtrace/contrib/internal/graphql/patch.py @@ -328,7 +328,6 @@ def _set_span_errors(errors: List[GraphQLError], span: Span) -> None: span.set_tag_str(ERROR_MSG, error_msgs) for error in errors: locations = " ".join(f"{loc.formatted['line']}:{loc.formatted['column']}" for loc in error.locations) - # breakpoint() attributes={"message": error.message, "type": span.get_tag("error.type"), "locations": locations, diff --git a/tests/contrib/graphql/test_graphql.py b/tests/contrib/graphql/test_graphql.py index 7072b35bc81..d21066d61ba 100644 --- a/tests/contrib/graphql/test_graphql.py +++ b/tests/contrib/graphql/test_graphql.py @@ -7,6 +7,8 @@ from ddtrace.contrib.internal.graphql.patch import patch from ddtrace.contrib.internal.graphql.patch import unpatch from ddtrace.trace import tracer +from graphql import build_schema +from graphql import graphql_sync from tests.utils import override_config from tests.utils import snapshot @@ -73,13 +75,45 @@ async def test_graphql_with_traced_resolver(test_schema, test_source_str, snapsh assert result.data == {"hello": "friend"} + +def resolve_fail(root, info): + undefined_var = None + return undefined_var.property + + +def test_graphql_fail(enable_graphql_resolvers): + query = """ + query { + fail + } + """ + + resolvers = { + "Query": { + "fail": resolve_fail, + } + } + schema_definition = """ + type Query { + fail: String + } + """ + + test_schema = build_schema(schema_definition) + result = graphql_sync(test_schema, query, root_value=None, field_resolver=lambda _type, _field: resolvers[_type.name][_field.name]) + + assert result.errors is not None + assert len(result.errors) == 1 + assert isinstance(result.errors[0], graphql.error.GraphQLError) + assert "'NoneType' object has no attribute 'name'" in result.errors[0].message + @pytest.mark.asyncio async def test_graphql_error(test_schema, snapshot_context): with snapshot_context(ignores=["meta.error.type", "meta.error.message"]): if graphql_version < (3, 0): - result = graphql.graphql(test_schema, "{ invalid_schema }") + result = graphql.graphql(test_schema, "query my_query{ invalid_schema }") else: - result = await graphql.graphql(test_schema, "{ invalid_schema }") + result = await graphql.graphql(test_schema, "query my_query{ invalid_schema }") assert len(result.errors) == 1 assert isinstance(result.errors[0], graphql.error.GraphQLError) assert "Cannot query field" in result.errors[0].message @@ -99,7 +133,7 @@ def test_graphql_v2_promise(test_schema, test_source_str): ) @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) + promise = graphql.graphql(test_schema, "query my_query{ invalid_schema }", return_promise=True) result = promise.get() assert len(result.errors) == 1 assert isinstance(result.errors[0], graphql.error.GraphQLError) From 7b98a6a7fde8980fa251a242db7335315c463504 Mon Sep 17 00:00:00 2001 From: quinna-h Date: Thu, 30 Jan 2025 10:20:04 -0500 Subject: [PATCH 06/24] update tests --- tests/contrib/graphql/test_graphql.py | 3 +- ...aphql.test_graphql.test_graphql_error.json | 135 ++++++++--------- ...raphql.test_graphql.test_graphql_fail.json | 138 ++++++++++++++++++ 3 files changed, 209 insertions(+), 67 deletions(-) create mode 100644 tests/snapshots/tests.contrib.graphql.test_graphql.test_graphql_fail.json diff --git a/tests/contrib/graphql/test_graphql.py b/tests/contrib/graphql/test_graphql.py index d21066d61ba..88deb38227d 100644 --- a/tests/contrib/graphql/test_graphql.py +++ b/tests/contrib/graphql/test_graphql.py @@ -81,6 +81,7 @@ def resolve_fail(root, info): return undefined_var.property +@snapshot(ignores=["meta.error.type", "meta.error.message", "meta.events"]) def test_graphql_fail(enable_graphql_resolvers): query = """ query { @@ -109,7 +110,7 @@ def test_graphql_fail(enable_graphql_resolvers): @pytest.mark.asyncio async def test_graphql_error(test_schema, snapshot_context): - with snapshot_context(ignores=["meta.error.type", "meta.error.message"]): + with snapshot_context(ignores=["meta.error.type", "meta.error.message", "meta.events"]): if graphql_version < (3, 0): result = graphql.graphql(test_schema, "query my_query{ invalid_schema }") else: 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 index 4b95ed96c9e..21f6c933024 100644 --- a/tests/snapshots/tests.contrib.graphql.test_graphql.test_graphql_error.json +++ b/tests/snapshots/tests.contrib.graphql.test_graphql.test_graphql_error.json @@ -1,68 +1,71 @@ -[[ - { - "name": "graphql.request", - "service": "graphql", - "resource": "{ invalid_schema }", - "trace_id": 0, - "span_id": 1, - "parent_id": 0, - "type": "graphql", - "error": 1, - "meta": { - "_dd.base_service": "tests.contrib.graphql", - "_dd.p.dm": "-0", - "_dd.p.tid": "654a694400000000", - "component": "graphql", - "error.message": "Cannot query field 'invalid_schema' on type 'RootQueryType'.\n\nGraphQL request:1:3\n1 | { invalid_schema }\n | ^", - "error.type": "graphql.error.graphql_error.GraphQLError", - "language": "python", - "runtime-id": "13a9a43400594de89a6aa537a3cb7b8e" +[ + [ + { + "name": "graphql.request", + "service": "graphql", + "resource": "query my_query{ invalid_schema }", + "trace_id": 0, + "span_id": 1, + "parent_id": 0, + "type": "graphql", + "error": 1, + "meta": { + "_dd.base_service": "tests.contrib.graphql", + "_dd.p.dm": "-0", + "_dd.p.tid": "679b96df00000000", + "component": "graphql", + "error.message": "Cannot query field 'invalid_schema' on type 'RootQueryType'.\n\nGraphQL request:1:17\n1 | query my_query{ invalid_schema }\n | ^", + "error.type": "graphql.error.graphql_error.GraphQLError", + "events": "[{\"name\": \"dd.graphql.query.error\", \"time_unix_nano\": 1738249951802662000, \"attributes\": {\"message\": \"Cannot query field 'invalid_schema' on type 'RootQueryType'.\", \"type\": \"graphql.error.graphql_error.GraphQLError\", \"locations\": \"1:17\"}}]", + "language": "python", + "runtime-id": "9a4c224e6fdd49cea07a7a3954fd5db7" + }, + "metrics": { + "_dd.measured": 1, + "_dd.top_level": 1, + "_dd.tracer_kr": 1.0, + "_sampling_priority_v1": 1, + "process_id": 26822 + }, + "duration": 1221000, + "start": 1738249951801442000 }, - "metrics": { - "_dd.measured": 1, - "_dd.top_level": 1, - "_dd.tracer_kr": 1.0, - "_sampling_priority_v1": 1, - "process_id": 50292 + { + "name": "graphql.parse", + "service": "graphql", + "resource": "graphql.parse", + "trace_id": 0, + "span_id": 2, + "parent_id": 1, + "type": "graphql", + "error": 0, + "meta": { + "_dd.base_service": "tests.contrib.graphql", + "component": "graphql", + "graphql.source": "query my_query{ invalid_schema }" + }, + "duration": 143000, + "start": 1738249951801693000 }, - "duration": 950458, - "start": 1692710417176600596 - }, - { - "name": "graphql.parse", - "service": "graphql", - "resource": "graphql.parse", - "trace_id": 0, - "span_id": 2, - "parent_id": 1, - "type": "graphql", - "error": 0, - "meta": { - "_dd.base_service": "tests.contrib.graphql", - "_dd.p.tid": "654a694400000000", - "component": "graphql", - "graphql.source": "{ invalid_schema }" - }, - "duration": 85750, - "start": 1692710417176824346 - }, - { - "name": "graphql.validate", - "service": "graphql", - "resource": "graphql.validate", - "trace_id": 0, - "span_id": 3, - "parent_id": 1, - "type": "graphql", - "error": 1, - "meta": { - "_dd.base_service": "tests.contrib.graphql", - "_dd.p.tid": "654a694400000000", - "component": "graphql", - "error.message": "Cannot query field 'invalid_schema' on type 'RootQueryType'.\n\nGraphQL request:1:3\n1 | { invalid_schema }\n | ^", - "error.type": "graphql.error.graphql_error.GraphQLError", - "graphql.source": "{ invalid_schema }" - }, - "duration": 564292, - "start": 1692710417176948721 - }]] + { + "name": "graphql.validate", + "service": "graphql", + "resource": "graphql.validate", + "trace_id": 0, + "span_id": 3, + "parent_id": 1, + "type": "graphql", + "error": 1, + "meta": { + "_dd.base_service": "tests.contrib.graphql", + "component": "graphql", + "error.message": "Cannot query field 'invalid_schema' on type 'RootQueryType'.\n\nGraphQL request:1:17\n1 | query my_query{ invalid_schema }\n | ^", + "error.type": "graphql.error.graphql_error.GraphQLError", + "events": "[{\"name\": \"dd.graphql.query.error\", \"time_unix_nano\": 1738249951802627000, \"attributes\": {\"message\": \"Cannot query field 'invalid_schema' on type 'RootQueryType'.\", \"type\": \"graphql.error.graphql_error.GraphQLError\", \"locations\": \"1:17\"}}]", + "graphql.source": "query my_query{ invalid_schema }" + }, + "duration": 745000, + "start": 1738249951801884000 + } + ] +] \ No newline at end of file diff --git a/tests/snapshots/tests.contrib.graphql.test_graphql.test_graphql_fail.json b/tests/snapshots/tests.contrib.graphql.test_graphql.test_graphql_fail.json new file mode 100644 index 00000000000..b644d1844d9 --- /dev/null +++ b/tests/snapshots/tests.contrib.graphql.test_graphql.test_graphql_fail.json @@ -0,0 +1,138 @@ +[[ + { + "name": "graphql.parse", + "service": "graphql", + "resource": "graphql.parse", + "trace_id": 0, + "span_id": 1, + "parent_id": 0, + "type": "graphql", + "error": 0, + "meta": { + "_dd.base_service": "tests.contrib.graphql", + "_dd.p.dm": "-0", + "_dd.p.tid": "679b976d00000000", + "component": "graphql", + "graphql.source": "type Query { fail: String }", + "language": "python", + "runtime-id": "596dfe80a9184851a69f62836436abe5" + }, + "metrics": { + "_dd.top_level": 1, + "_dd.tracer_kr": 1.0, + "_sampling_priority_v1": 1, + "process_id": 27630 + }, + "duration": 257000, + "start": 1738250093732371000 + }], +[ + { + "name": "graphql.request", + "service": "graphql", + "resource": "query { fail }", + "trace_id": 1, + "span_id": 1, + "parent_id": 0, + "type": "graphql", + "error": 1, + "meta": { + "_dd.base_service": "tests.contrib.graphql", + "_dd.p.dm": "-0", + "_dd.p.tid": "679b976d00000000", + "component": "graphql", + "error.message": "'NoneType' object has no attribute 'name'\n\nGraphQL request:3:7\n2 | query {\n3 | fail\n | ^\n4 | }", + "error.stack": "Traceback (most recent call last):\n File \"/Users/quinna.halim/dd-trace-py/.riot/venv_py31013_mock_pytest_pytest-mock_coverage_pytest-cov_opentracing_hypothesis6451_pytest-asyncio0211_graphql-core~320_pytest-randomly/lib/python3.10/site-packages/graphql/execution/execute.py\", line 521, in execute_field\n result = resolve_fn(source, info, **args)\n File \"/Users/quinna.halim/dd-trace-py/ddtrace/contrib/internal/graphql/patch.py\", line 243, in _resolver_middleware\n return next_middleware(root, info, **args)\n File \"/Users/quinna.halim/dd-trace-py/tests/contrib/graphql/test_graphql.py\", line 104, in \n result = graphql_sync(test_schema, query, root_value=None, field_resolver=lambda _type, _field: resolvers[_type.name][_field.name])\ngraphql.error.graphql_error.GraphQLError: 'NoneType' object has no attribute 'name'\n\nGraphQL request:3:7\n2 | query {\n3 | fail\n | ^\n4 | }\n", + "error.type": "graphql.error.graphql_error.GraphQLError", + "events": "[{\"name\": \"dd.graphql.query.error\", \"time_unix_nano\": 1738250093735707000, \"attributes\": {\"message\": \"'NoneType' object has no attribute 'name'\", \"type\": \"graphql.error.graphql_error.GraphQLError\", \"locations\": \"3:7\", \"stacktrace\": \"Traceback (most recent call last):\\n File \\\"/Users/quinna.halim/dd-trace-py/.riot/venv_py31013_mock_pytest_pytest-mock_coverage_pytest-cov_opentracing_hypothesis6451_pytest-asyncio0211_graphql-core~320_pytest-randomly/lib/python3.10/site-packages/graphql/execution/execute.py\\\", line 521, in execute_field\\n result = resolve_fn(source, info, **args)\\n File \\\"/Users/quinna.halim/dd-trace-py/ddtrace/contrib/internal/graphql/patch.py\\\", line 243, in _resolver_middleware\\n return next_middleware(root, info, **args)\\n File \\\"/Users/quinna.halim/dd-trace-py/tests/contrib/graphql/test_graphql.py\\\", line 104, in \\n result = graphql_sync(test_schema, query, root_value=None, field_resolver=lambda _type, _field: resolvers[_type.name][_field.name])\\ngraphql.error.graphql_error.GraphQLError: 'NoneType' object has no attribute 'name'\\n\\nGraphQL request:3:7\\n2 | query {\\n3 | fail\\n | ^\\n4 | }\\n\", \"path\": \"fail\"}}]", + "language": "python", + "runtime-id": "596dfe80a9184851a69f62836436abe5" + }, + "metrics": { + "_dd.measured": 1, + "_dd.top_level": 1, + "_dd.tracer_kr": 1.0, + "_sampling_priority_v1": 1, + "process_id": 27630 + }, + "duration": 1681000, + "start": 1738250093734027000 + }, + { + "name": "graphql.parse", + "service": "graphql", + "resource": "graphql.parse", + "trace_id": 1, + "span_id": 2, + "parent_id": 1, + "type": "graphql", + "error": 0, + "meta": { + "_dd.base_service": "tests.contrib.graphql", + "component": "graphql", + "graphql.source": "query { fail }" + }, + "duration": 128000, + "start": 1738250093734164000 + }, + { + "name": "graphql.validate", + "service": "graphql", + "resource": "graphql.validate", + "trace_id": 1, + "span_id": 3, + "parent_id": 1, + "type": "graphql", + "error": 0, + "meta": { + "_dd.base_service": "tests.contrib.graphql", + "component": "graphql", + "graphql.source": "query { fail }" + }, + "duration": 493000, + "start": 1738250093734336000 + }, + { + "name": "graphql.execute", + "service": "graphql", + "resource": "query { fail }", + "trace_id": 1, + "span_id": 4, + "parent_id": 1, + "type": "graphql", + "error": 1, + "meta": { + "_dd.base_service": "tests.contrib.graphql", + "component": "graphql", + "error.message": "'NoneType' object has no attribute 'name'\n\nGraphQL request:3:7\n2 | query {\n3 | fail\n | ^\n4 | }", + "error.stack": "Traceback (most recent call last):\n File \"/Users/quinna.halim/dd-trace-py/.riot/venv_py31013_mock_pytest_pytest-mock_coverage_pytest-cov_opentracing_hypothesis6451_pytest-asyncio0211_graphql-core~320_pytest-randomly/lib/python3.10/site-packages/graphql/execution/execute.py\", line 521, in execute_field\n result = resolve_fn(source, info, **args)\n File \"/Users/quinna.halim/dd-trace-py/ddtrace/contrib/internal/graphql/patch.py\", line 243, in _resolver_middleware\n return next_middleware(root, info, **args)\n File \"/Users/quinna.halim/dd-trace-py/tests/contrib/graphql/test_graphql.py\", line 104, in \n result = graphql_sync(test_schema, query, root_value=None, field_resolver=lambda _type, _field: resolvers[_type.name][_field.name])\ngraphql.error.graphql_error.GraphQLError: 'NoneType' object has no attribute 'name'\n\nGraphQL request:3:7\n2 | query {\n3 | fail\n | ^\n4 | }\n", + "error.type": "graphql.error.graphql_error.GraphQLError", + "events": "[{\"name\": \"dd.graphql.query.error\", \"time_unix_nano\": 1738250093735623000, \"attributes\": {\"message\": \"'NoneType' object has no attribute 'name'\", \"type\": \"graphql.error.graphql_error.GraphQLError\", \"locations\": \"3:7\", \"stacktrace\": \"Traceback (most recent call last):\\n File \\\"/Users/quinna.halim/dd-trace-py/.riot/venv_py31013_mock_pytest_pytest-mock_coverage_pytest-cov_opentracing_hypothesis6451_pytest-asyncio0211_graphql-core~320_pytest-randomly/lib/python3.10/site-packages/graphql/execution/execute.py\\\", line 521, in execute_field\\n result = resolve_fn(source, info, **args)\\n File \\\"/Users/quinna.halim/dd-trace-py/ddtrace/contrib/internal/graphql/patch.py\\\", line 243, in _resolver_middleware\\n return next_middleware(root, info, **args)\\n File \\\"/Users/quinna.halim/dd-trace-py/tests/contrib/graphql/test_graphql.py\\\", line 104, in \\n result = graphql_sync(test_schema, query, root_value=None, field_resolver=lambda _type, _field: resolvers[_type.name][_field.name])\\ngraphql.error.graphql_error.GraphQLError: 'NoneType' object has no attribute 'name'\\n\\nGraphQL request:3:7\\n2 | query {\\n3 | fail\\n | ^\\n4 | }\\n\", \"path\": \"fail\"}}]", + "graphql.operation.type": "query", + "graphql.source": "query { fail }" + }, + "metrics": { + "_dd.measured": 1 + }, + "duration": 761000, + "start": 1738250093734863000 + }, + { + "name": "graphql.resolve", + "service": "graphql", + "resource": "fail", + "trace_id": 1, + "span_id": 5, + "parent_id": 4, + "type": "graphql", + "error": 1, + "meta": { + "_dd.base_service": "tests.contrib.graphql", + "component": "graphql", + "error.message": "'NoneType' object has no attribute 'name'", + "error.stack": "Traceback (most recent call last):\n File \"/Users/quinna.halim/dd-trace-py/ddtrace/contrib/internal/graphql/patch.py\", line 243, in _resolver_middleware\n return next_middleware(root, info, **args)\n File \"/Users/quinna.halim/dd-trace-py/tests/contrib/graphql/test_graphql.py\", line 104, in \n result = graphql_sync(test_schema, query, root_value=None, field_resolver=lambda _type, _field: resolvers[_type.name][_field.name])\nAttributeError: 'NoneType' object has no attribute 'name'\n", + "error.type": "builtins.AttributeError" + }, + "duration": 424000, + "start": 1738250093734961000 + }]] From 2b509a704601b11594e344d2d137e4f02d62dec5 Mon Sep 17 00:00:00 2001 From: quinna-h Date: Thu, 30 Jan 2025 16:52:28 -0500 Subject: [PATCH 07/24] wip --- ddtrace/contrib/internal/graphql/patch.py | 39 +++++++++++++---------- tests/contrib/graphene/test_graphene.py | 35 +++++++++++++++++++- 2 files changed, 57 insertions(+), 17 deletions(-) diff --git a/ddtrace/contrib/internal/graphql/patch.py b/ddtrace/contrib/internal/graphql/patch.py index d273a01d5e5..1f9bade29dc 100644 --- a/ddtrace/contrib/internal/graphql/patch.py +++ b/ddtrace/contrib/internal/graphql/patch.py @@ -4,7 +4,6 @@ import traceback from typing import TYPE_CHECKING from typing import List -from typing import Dict from ddtrace.internal.schema.span_attribute_schema import SpanDirection from ddtrace.trace import Span @@ -20,7 +19,6 @@ import graphql from graphql import MiddlewareManager - from graphql.error import GraphQLError from graphql.execution import ExecutionResult from graphql.language.source import Source @@ -72,6 +70,7 @@ def get_version(): _GRAPHQL_OPERATION_TYPE = "graphql.operation.type" _GRAPHQL_OPERATION_NAME = "graphql.operation.name" + def patch(): if getattr(graphql, "_datadog_patch", False): return @@ -292,17 +291,17 @@ def _get_source_str(obj): return re.sub(r"\s+", " ", source_str).strip() -def _validate_error_extensions(error: GraphQLError, extensions: str | None) -> Dict: +def _validate_error_extensions(error: GraphQLError, extensions: str | None, attributes: Dict) -> Tuple[Dict, Dict]: # Validate user-provided extensions if not extensions: - return {} - - fields = [e.strip() for e in extensions.split(',')] + return {}, attributes + + fields = [e.strip() for e in extensions.split(",")] error_extensions = {} for field in fields: if field in error.extensions: # validate extensions formatting - # All extensions values MUST be stringified, EXCEPT for numeric values and + # All extensions values MUST be stringified, EXCEPT for numeric values and # boolean values, which remain in their original type. if isinstance(error.extensions[field], (int, float, bool)): error_extensions[field] = error.extensions[field] @@ -310,7 +309,14 @@ def _validate_error_extensions(error: GraphQLError, extensions: str | None) -> D # q: could this be `None`? error_extensions[field] = str(error.extensions[field]) - return error_extensions + # Additional validation for Apollo Server attributes + if field == "stacktrace": + attributes["type"] = error.extensions[field].split(":")[0] + attributes["stacktrace"] = "\n".join(error.extensions[field]) + elif field == "code": + attributes["code"] = error.extensions[field] + + return error_extensions, attributes def _set_span_errors(errors: List[GraphQLError], span: Span) -> None: @@ -328,21 +334,21 @@ def _set_span_errors(errors: List[GraphQLError], span: Span) -> None: span.set_tag_str(ERROR_MSG, error_msgs) for error in errors: locations = " ".join(f"{loc.formatted['line']}:{loc.formatted['column']}" for loc in error.locations) - attributes={"message": error.message, - "type": span.get_tag("error.type"), - "locations": locations, + attributes = { + "message": error.message, + "type": span.get_tag("error.type"), + "locations": locations, } if error.__traceback__: stacktrace = "".join( - traceback.format_exception( - type(error), error, error.__traceback__, limit=config._span_traceback_max_size - ) + traceback.format_exception( + type(error), error, error.__traceback__, limit=config._span_traceback_max_size ) + ) attributes["stacktrace"] = stacktrace span.set_tag_str(ERROR_STACK, stacktrace) - if error.path is not None: path = ",".join([str(path_obj) for path_obj in error.path]) attributes["path"] = path @@ -350,7 +356,7 @@ def _set_span_errors(errors: List[GraphQLError], span: Span) -> None: if os.environ.get("DD_TRACE_GRAPHQL_ERROR_EXTENSIONS") is not None: extensions = os.environ.get("DD_TRACE_GRAPHQL_ERROR_EXTENSIONS") - error_extensions = _validate_error_extensions(error, extensions) + error_extensions, attributes = _validate_error_extensions(error, extensions, attributes) if error_extensions: attributes["extensions"] = str(error_extensions) span._add_event( @@ -358,6 +364,7 @@ def _set_span_errors(errors: List[GraphQLError], span: Span) -> None: attributes=attributes, ) + def _set_span_operation_tags(span, document): operation_def = graphql.get_operation_ast(document) if not operation_def: diff --git a/tests/contrib/graphene/test_graphene.py b/tests/contrib/graphene/test_graphene.py index 5dca40212ee..6d55a38f61f 100644 --- a/tests/contrib/graphene/test_graphene.py +++ b/tests/contrib/graphene/test_graphene.py @@ -1,4 +1,7 @@ +import os + import graphene +import graphql import pytest from ddtrace.contrib.internal.graphql.patch import patch @@ -26,6 +29,23 @@ def resolve_patron(root, info): raise Exception("exception was raised in a graphene query") +class Query(graphene.ObjectType): + user = graphene.String(id=graphene.ID()) + + def resolve_user(self, info, id): + if id != "123": + raise graphql.error.GraphQLError( + "User not found", + extensions={ + "code": "USER_NOT_FOUND", + "timestamp": "2025-01-30T12:34:56Z", + "status": 404, + "retryable": False, + }, + ) + return "John Doe" + + @pytest.fixture(autouse=True) def enable_graphql_patching(): patch() @@ -69,6 +89,19 @@ def test_schema_execute(test_schema, test_source_str): assert result.data == {"patron": {"id": "1", "name": "Syrus", "age": 27}} +@pytest.mark.snapshot( + ignores=["meta.events", "meta.error.stack"], variants={"v2": graphene.VERSION < (3,), "": graphene.VERSION >= (3,)} +) +@pytest.mark.snapshot +def test_schema_failing_extensions(): + schema = graphene.Schema(query=Query) + os.environ["DD_TRACE_GRAPHQL_ERROR_EXTENSIONS"] = "code, status" + + query_string = '{ user(id: "999") }' + result = schema.execute(query_string) + assert result.errors + + @pytest.mark.asyncio @pytest.mark.snapshot(token="tests.contrib.graphene.test_graphene.test_schema_execute") @pytest.mark.skipif(graphene.VERSION < (3, 0, 0), reason="execute_async is only supported in graphene>=3.0") @@ -95,7 +128,7 @@ async def test_schema_execute_async_with_resolvers(test_schema, test_source_str, @pytest.mark.snapshot( - ignores=["meta.error.stack"], variants={"v2": graphene.VERSION < (3,), "": graphene.VERSION >= (3,)} + ignores=["meta.events", "meta.error.stack"], variants={"v2": graphene.VERSION < (3,), "": graphene.VERSION >= (3,)} ) def test_schema_failing_execute(failing_schema, test_source_str, enable_graphql_resolvers): result = failing_schema.execute(test_source_str) From d6a5fad0f4f5485a24d3c1fd6340b94e31f7c988 Mon Sep 17 00:00:00 2001 From: quinna-h Date: Thu, 30 Jan 2025 17:51:02 -0500 Subject: [PATCH 08/24] formatting --- tests/contrib/graphene/test_graphene.py | 22 +++++++++------------- tests/contrib/graphql/test_graphql.py | 8 ++++++-- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/tests/contrib/graphene/test_graphene.py b/tests/contrib/graphene/test_graphene.py index 6d55a38f61f..fd8fae94522 100644 --- a/tests/contrib/graphene/test_graphene.py +++ b/tests/contrib/graphene/test_graphene.py @@ -89,19 +89,6 @@ def test_schema_execute(test_schema, test_source_str): assert result.data == {"patron": {"id": "1", "name": "Syrus", "age": 27}} -@pytest.mark.snapshot( - ignores=["meta.events", "meta.error.stack"], variants={"v2": graphene.VERSION < (3,), "": graphene.VERSION >= (3,)} -) -@pytest.mark.snapshot -def test_schema_failing_extensions(): - schema = graphene.Schema(query=Query) - os.environ["DD_TRACE_GRAPHQL_ERROR_EXTENSIONS"] = "code, status" - - query_string = '{ user(id: "999") }' - result = schema.execute(query_string) - assert result.errors - - @pytest.mark.asyncio @pytest.mark.snapshot(token="tests.contrib.graphene.test_graphene.test_schema_execute") @pytest.mark.skipif(graphene.VERSION < (3, 0, 0), reason="execute_async is only supported in graphene>=3.0") @@ -127,6 +114,15 @@ async def test_schema_execute_async_with_resolvers(test_schema, test_source_str, assert result.data == {"patron": {"id": "1", "name": "Syrus", "age": 27}} +@pytest.mark.snapshot +def test_schema_failing_extensions(test_schema, test_source_str, enable_graphql_resolvers): + schema = graphene.Schema(query=Query) + os.environ["DD_TRACE_GRAPHQL_ERROR_EXTENSIONS"] = "code, status" + query_string = '{ user(id: "999") }' + result = schema.execute(query_string) + assert result.errors + + @pytest.mark.snapshot( ignores=["meta.events", "meta.error.stack"], variants={"v2": graphene.VERSION < (3,), "": graphene.VERSION >= (3,)} ) diff --git a/tests/contrib/graphql/test_graphql.py b/tests/contrib/graphql/test_graphql.py index 88deb38227d..0d80f9dbe32 100644 --- a/tests/contrib/graphql/test_graphql.py +++ b/tests/contrib/graphql/test_graphql.py @@ -1,6 +1,8 @@ import os import graphql +from graphql import build_schema +from graphql import graphql_sync import pytest from ddtrace.contrib.internal.graphql.patch import _graphql_version as graphql_version @@ -75,7 +77,6 @@ async def test_graphql_with_traced_resolver(test_schema, test_source_str, snapsh assert result.data == {"hello": "friend"} - def resolve_fail(root, info): undefined_var = None return undefined_var.property @@ -101,13 +102,16 @@ def test_graphql_fail(enable_graphql_resolvers): """ test_schema = build_schema(schema_definition) - result = graphql_sync(test_schema, query, root_value=None, field_resolver=lambda _type, _field: resolvers[_type.name][_field.name]) + result = graphql_sync( + test_schema, query, root_value=None, field_resolver=lambda _type, _field: resolvers[_type.name][_field.name] + ) assert result.errors is not None assert len(result.errors) == 1 assert isinstance(result.errors[0], graphql.error.GraphQLError) assert "'NoneType' object has no attribute 'name'" in result.errors[0].message + @pytest.mark.asyncio async def test_graphql_error(test_schema, snapshot_context): with snapshot_context(ignores=["meta.error.type", "meta.error.message", "meta.events"]): From 23cdad508d1bc4d2a7735cb97a65b952a9345077 Mon Sep 17 00:00:00 2001 From: quinna-h Date: Mon, 3 Feb 2025 20:10:01 -0500 Subject: [PATCH 09/24] add noqa --- tests/contrib/graphene/test_graphene.py | 4 +- ...aphene.test_schema_failing_extensions.json | 110 ++++++++++++++ ...aphql.test_graphql.test_graphql_error.json | 135 +++++++++--------- 3 files changed, 178 insertions(+), 71 deletions(-) create mode 100644 tests/snapshots/tests.contrib.graphene.test_graphene.test_schema_failing_extensions.json diff --git a/tests/contrib/graphene/test_graphene.py b/tests/contrib/graphene/test_graphene.py index fd8fae94522..50e1ccdc98c 100644 --- a/tests/contrib/graphene/test_graphene.py +++ b/tests/contrib/graphene/test_graphene.py @@ -32,7 +32,7 @@ def resolve_patron(root, info): class Query(graphene.ObjectType): user = graphene.String(id=graphene.ID()) - def resolve_user(self, info, id): + def resolve_user(self, info, id): # noqa: A002 if id != "123": raise graphql.error.GraphQLError( "User not found", @@ -114,7 +114,7 @@ async def test_schema_execute_async_with_resolvers(test_schema, test_source_str, assert result.data == {"patron": {"id": "1", "name": "Syrus", "age": 27}} -@pytest.mark.snapshot +@pytest.mark.snapshot(ignores=["meta.events", "meta.error.stack"]) def test_schema_failing_extensions(test_schema, test_source_str, enable_graphql_resolvers): schema = graphene.Schema(query=Query) os.environ["DD_TRACE_GRAPHQL_ERROR_EXTENSIONS"] = "code, status" diff --git a/tests/snapshots/tests.contrib.graphene.test_graphene.test_schema_failing_extensions.json b/tests/snapshots/tests.contrib.graphene.test_graphene.test_schema_failing_extensions.json new file mode 100644 index 00000000000..11391a3d342 --- /dev/null +++ b/tests/snapshots/tests.contrib.graphene.test_graphene.test_schema_failing_extensions.json @@ -0,0 +1,110 @@ +[[ + { + "name": "graphql.request", + "service": "graphql", + "resource": "{ user(id: \"999\") }", + "trace_id": 0, + "span_id": 1, + "parent_id": 0, + "type": "graphql", + "error": 1, + "meta": { + "_dd.base_service": "tests.contrib.graphene", + "_dd.p.dm": "-0", + "_dd.p.tid": "67a1663a00000000", + "component": "graphql", + "error.message": "User not found\n\nGraphQL request:1:3\n1 | { user(id: \"999\") }\n | ^", + "error.stack": "Traceback (most recent call last):\n File \"/Users/quinna.halim/dd-trace-py/.riot/venv_py3130_mock_pytest_pytest-mock_coverage_pytest-cov_opentracing_hypothesis6451_graphene~300_pytest-asyncio0211_graphql-relay_pytest-randomly/lib/python3.13/site-packages/graphql/execution/execute.py\", line 617, in resolve_field\n result = resolve_fn(source, info, **args)\n File \"/Users/quinna.halim/dd-trace-py/ddtrace/contrib/internal/graphql/patch.py\", line 242, in _resolver_middleware\n return next_middleware(root, info, **args)\n File \"/Users/quinna.halim/dd-trace-py/tests/contrib/graphene/test_graphene.py\", line 37, in resolve_user\n raise graphql.error.GraphQLError(\n ...<7 lines>...\n )\ngraphql.error.graphql_error.GraphQLError: User not found\n\nGraphQL request:1:3\n1 | { user(id: \"999\") }\n | ^\n", + "error.type": "graphql.error.graphql_error.GraphQLError", + "events": "[{\"name\": \"dd.graphql.query.error\", \"time_unix_nano\": 1738630714389656000, \"attributes\": {\"message\": \"User not found\", \"type\": \"graphql.error.graphql_error.GraphQLError\", \"locations\": \"1:3\", \"stacktrace\": \"Traceback (most recent call last):\\n File \\\"/Users/quinna.halim/dd-trace-py/.riot/venv_py3130_mock_pytest_pytest-mock_coverage_pytest-cov_opentracing_hypothesis6451_graphene~300_pytest-asyncio0211_graphql-relay_pytest-randomly/lib/python3.13/site-packages/graphql/execution/execute.py\\\", line 617, in resolve_field\\n result = resolve_fn(source, info, **args)\\n File \\\"/Users/quinna.halim/dd-trace-py/ddtrace/contrib/internal/graphql/patch.py\\\", line 242, in _resolver_middleware\\n return next_middleware(root, info, **args)\\n File \\\"/Users/quinna.halim/dd-trace-py/tests/contrib/graphene/test_graphene.py\\\", line 37, in resolve_user\\n raise graphql.error.GraphQLError(\\n ...<7 lines>...\\n )\\ngraphql.error.graphql_error.GraphQLError: User not found\\n\\nGraphQL request:1:3\\n1 | { user(id: \\\"999\\\") }\\n | ^\\n\", \"path\": \"user\", \"code\": \"USER_NOT_FOUND\", \"extensions\": \"{'code': 'USER_NOT_FOUND', 'status': 404}\"}}]", + "language": "python", + "runtime-id": "eee5cdadb35249afb9f5d6e1c304be24" + }, + "metrics": { + "_dd.measured": 1, + "_dd.top_level": 1, + "_dd.tracer_kr": 1.0, + "_sampling_priority_v1": 1, + "process_id": 43930 + }, + "duration": 3813000, + "start": 1738630714385845000 + }, + { + "name": "graphql.parse", + "service": "graphql", + "resource": "graphql.parse", + "trace_id": 0, + "span_id": 2, + "parent_id": 1, + "type": "graphql", + "error": 0, + "meta": { + "_dd.base_service": "tests.contrib.graphene", + "component": "graphql", + "graphql.source": "{ user(id: \"999\") }" + }, + "duration": 180000, + "start": 1738630714386234000 + }, + { + "name": "graphql.validate", + "service": "graphql", + "resource": "graphql.validate", + "trace_id": 0, + "span_id": 3, + "parent_id": 1, + "type": "graphql", + "error": 0, + "meta": { + "_dd.base_service": "tests.contrib.graphene", + "component": "graphql", + "graphql.source": "{ user(id: \"999\") }" + }, + "duration": 888000, + "start": 1738630714386488000 + }, + { + "name": "graphql.execute", + "service": "graphql", + "resource": "{ user(id: \"999\") }", + "trace_id": 0, + "span_id": 4, + "parent_id": 1, + "type": "graphql", + "error": 1, + "meta": { + "_dd.base_service": "tests.contrib.graphene", + "component": "graphql", + "error.message": "User not found\n\nGraphQL request:1:3\n1 | { user(id: \"999\") }\n | ^", + "error.stack": "Traceback (most recent call last):\n File \"/Users/quinna.halim/dd-trace-py/.riot/venv_py3130_mock_pytest_pytest-mock_coverage_pytest-cov_opentracing_hypothesis6451_graphene~300_pytest-asyncio0211_graphql-relay_pytest-randomly/lib/python3.13/site-packages/graphql/execution/execute.py\", line 617, in resolve_field\n result = resolve_fn(source, info, **args)\n File \"/Users/quinna.halim/dd-trace-py/ddtrace/contrib/internal/graphql/patch.py\", line 242, in _resolver_middleware\n return next_middleware(root, info, **args)\n File \"/Users/quinna.halim/dd-trace-py/tests/contrib/graphene/test_graphene.py\", line 37, in resolve_user\n raise graphql.error.GraphQLError(\n ...<7 lines>...\n )\ngraphql.error.graphql_error.GraphQLError: User not found\n\nGraphQL request:1:3\n1 | { user(id: \"999\") }\n | ^\n", + "error.type": "graphql.error.graphql_error.GraphQLError", + "events": "[{\"name\": \"dd.graphql.query.error\", \"time_unix_nano\": 1738630714389350000, \"attributes\": {\"message\": \"User not found\", \"type\": \"graphql.error.graphql_error.GraphQLError\", \"locations\": \"1:3\", \"stacktrace\": \"Traceback (most recent call last):\\n File \\\"/Users/quinna.halim/dd-trace-py/.riot/venv_py3130_mock_pytest_pytest-mock_coverage_pytest-cov_opentracing_hypothesis6451_graphene~300_pytest-asyncio0211_graphql-relay_pytest-randomly/lib/python3.13/site-packages/graphql/execution/execute.py\\\", line 617, in resolve_field\\n result = resolve_fn(source, info, **args)\\n File \\\"/Users/quinna.halim/dd-trace-py/ddtrace/contrib/internal/graphql/patch.py\\\", line 242, in _resolver_middleware\\n return next_middleware(root, info, **args)\\n File \\\"/Users/quinna.halim/dd-trace-py/tests/contrib/graphene/test_graphene.py\\\", line 37, in resolve_user\\n raise graphql.error.GraphQLError(\\n ...<7 lines>...\\n )\\ngraphql.error.graphql_error.GraphQLError: User not found\\n\\nGraphQL request:1:3\\n1 | { user(id: \\\"999\\\") }\\n | ^\\n\", \"path\": \"user\", \"code\": \"USER_NOT_FOUND\", \"extensions\": \"{'code': 'USER_NOT_FOUND', 'status': 404}\"}}]", + "graphql.operation.type": "query", + "graphql.source": "{ user(id: \"999\") }" + }, + "metrics": { + "_dd.measured": 1 + }, + "duration": 1921000, + "start": 1738630714387431000 + }, + { + "name": "graphql.resolve", + "service": "graphql", + "resource": "user", + "trace_id": 0, + "span_id": 5, + "parent_id": 4, + "type": "graphql", + "error": 1, + "meta": { + "_dd.base_service": "tests.contrib.graphene", + "component": "graphql", + "error.message": "User not found", + "error.stack": "Traceback (most recent call last):\n File \"/Users/quinna.halim/dd-trace-py/ddtrace/contrib/internal/graphql/patch.py\", line 242, in _resolver_middleware\n return next_middleware(root, info, **args)\n File \"/Users/quinna.halim/dd-trace-py/tests/contrib/graphene/test_graphene.py\", line 37, in resolve_user\n raise graphql.error.GraphQLError(\n ...<7 lines>...\n )\ngraphql.error.graphql_error.GraphQLError: User not found\n", + "error.type": "graphql.error.graphql_error.GraphQLError" + }, + "duration": 1152000, + "start": 1738630714387593000 + }]] 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 index 21f6c933024..c19d700c1a3 100644 --- a/tests/snapshots/tests.contrib.graphql.test_graphql.test_graphql_error.json +++ b/tests/snapshots/tests.contrib.graphql.test_graphql.test_graphql_error.json @@ -1,71 +1,68 @@ -[ - [ - { - "name": "graphql.request", - "service": "graphql", - "resource": "query my_query{ invalid_schema }", - "trace_id": 0, - "span_id": 1, - "parent_id": 0, - "type": "graphql", - "error": 1, - "meta": { - "_dd.base_service": "tests.contrib.graphql", - "_dd.p.dm": "-0", - "_dd.p.tid": "679b96df00000000", - "component": "graphql", - "error.message": "Cannot query field 'invalid_schema' on type 'RootQueryType'.\n\nGraphQL request:1:17\n1 | query my_query{ invalid_schema }\n | ^", - "error.type": "graphql.error.graphql_error.GraphQLError", - "events": "[{\"name\": \"dd.graphql.query.error\", \"time_unix_nano\": 1738249951802662000, \"attributes\": {\"message\": \"Cannot query field 'invalid_schema' on type 'RootQueryType'.\", \"type\": \"graphql.error.graphql_error.GraphQLError\", \"locations\": \"1:17\"}}]", - "language": "python", - "runtime-id": "9a4c224e6fdd49cea07a7a3954fd5db7" - }, - "metrics": { - "_dd.measured": 1, - "_dd.top_level": 1, - "_dd.tracer_kr": 1.0, - "_sampling_priority_v1": 1, - "process_id": 26822 - }, - "duration": 1221000, - "start": 1738249951801442000 +[[ + { + "name": "graphql.request", + "service": "graphql", + "resource": "query my_query{ invalid_schema }", + "trace_id": 0, + "span_id": 1, + "parent_id": 0, + "type": "graphql", + "error": 1, + "meta": { + "_dd.base_service": "tests.contrib.graphql", + "_dd.p.dm": "-0", + "_dd.p.tid": "679b96df00000000", + "component": "graphql", + "error.message": "Cannot query field 'invalid_schema' on type 'RootQueryType'.\n\nGraphQL request:1:17\n1 | query my_query{ invalid_schema }\n | ^", + "error.type": "graphql.error.graphql_error.GraphQLError", + "events": "[{\"name\": \"dd.graphql.query.error\", \"time_unix_nano\": 1738249951802662000, \"attributes\": {\"message\": \"Cannot query field 'invalid_schema' on type 'RootQueryType'.\", \"type\": \"graphql.error.graphql_error.GraphQLError\", \"locations\": \"1:17\"}}]", + "language": "python", + "runtime-id": "9a4c224e6fdd49cea07a7a3954fd5db7" }, - { - "name": "graphql.parse", - "service": "graphql", - "resource": "graphql.parse", - "trace_id": 0, - "span_id": 2, - "parent_id": 1, - "type": "graphql", - "error": 0, - "meta": { - "_dd.base_service": "tests.contrib.graphql", - "component": "graphql", - "graphql.source": "query my_query{ invalid_schema }" - }, - "duration": 143000, - "start": 1738249951801693000 + "metrics": { + "_dd.measured": 1, + "_dd.top_level": 1, + "_dd.tracer_kr": 1.0, + "_sampling_priority_v1": 1, + "process_id": 26822 }, - { - "name": "graphql.validate", - "service": "graphql", - "resource": "graphql.validate", - "trace_id": 0, - "span_id": 3, - "parent_id": 1, - "type": "graphql", - "error": 1, - "meta": { - "_dd.base_service": "tests.contrib.graphql", - "component": "graphql", - "error.message": "Cannot query field 'invalid_schema' on type 'RootQueryType'.\n\nGraphQL request:1:17\n1 | query my_query{ invalid_schema }\n | ^", - "error.type": "graphql.error.graphql_error.GraphQLError", - "events": "[{\"name\": \"dd.graphql.query.error\", \"time_unix_nano\": 1738249951802627000, \"attributes\": {\"message\": \"Cannot query field 'invalid_schema' on type 'RootQueryType'.\", \"type\": \"graphql.error.graphql_error.GraphQLError\", \"locations\": \"1:17\"}}]", - "graphql.source": "query my_query{ invalid_schema }" - }, - "duration": 745000, - "start": 1738249951801884000 - } - ] -] \ No newline at end of file + "duration": 1221000, + "start": 1738249951801442000 + }, + { + "name": "graphql.parse", + "service": "graphql", + "resource": "graphql.parse", + "trace_id": 0, + "span_id": 2, + "parent_id": 1, + "type": "graphql", + "error": 0, + "meta": { + "_dd.base_service": "tests.contrib.graphql", + "component": "graphql", + "graphql.source": "query my_query{ invalid_schema }" + }, + "duration": 143000, + "start": 1738249951801693000 + }, + { + "name": "graphql.validate", + "service": "graphql", + "resource": "graphql.validate", + "trace_id": 0, + "span_id": 3, + "parent_id": 1, + "type": "graphql", + "error": 1, + "meta": { + "_dd.base_service": "tests.contrib.graphql", + "component": "graphql", + "error.message": "Cannot query field 'invalid_schema' on type 'RootQueryType'.\n\nGraphQL request:1:17\n1 | query my_query{ invalid_schema }\n | ^", + "error.type": "graphql.error.graphql_error.GraphQLError", + "events": "[{\"name\": \"dd.graphql.query.error\", \"time_unix_nano\": 1738249951802627000, \"attributes\": {\"message\": \"Cannot query field 'invalid_schema' on type 'RootQueryType'.\", \"type\": \"graphql.error.graphql_error.GraphQLError\", \"locations\": \"1:17\"}}]", + "graphql.source": "query my_query{ invalid_schema }" + }, + "duration": 745000, + "start": 1738249951801884000 + }]] From 1b61fd5708d83d5f6085dfd10a57a087f389c9e1 Mon Sep 17 00:00:00 2001 From: quinna-h Date: Tue, 4 Feb 2025 11:30:45 -0500 Subject: [PATCH 10/24] formatting --- ddtrace/contrib/internal/graphql/patch.py | 4 +++- tests/contrib/graphene/test_graphene.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/ddtrace/contrib/internal/graphql/patch.py b/ddtrace/contrib/internal/graphql/patch.py index 1f9bade29dc..4050a24002b 100644 --- a/ddtrace/contrib/internal/graphql/patch.py +++ b/ddtrace/contrib/internal/graphql/patch.py @@ -13,10 +13,10 @@ from typing import Callable # noqa:F401 from typing import Dict # noqa:F401 from typing import Iterable # noqa:F401 + from typing import List from typing import Tuple # noqa:F401 from typing import Union # noqa:F401 - import graphql from graphql import MiddlewareManager from graphql.error import GraphQLError @@ -24,6 +24,7 @@ from graphql.language.source import Source from ddtrace import config +from ddtrace._trace.span import Span from ddtrace.constants import _ANALYTICS_SAMPLE_RATE_KEY from ddtrace.constants import _SPAN_MEASURED_KEY from ddtrace.constants import ERROR_MSG @@ -34,6 +35,7 @@ from ddtrace.internal.constants import COMPONENT from ddtrace.internal.schema import schematize_service_name from ddtrace.internal.schema import schematize_url_operation +from ddtrace.internal.schema.span_attribute_schema import SpanDirection from ddtrace.internal.utils import ArgumentError from ddtrace.internal.utils import get_argument_value from ddtrace.internal.utils import set_argument_value diff --git a/tests/contrib/graphene/test_graphene.py b/tests/contrib/graphene/test_graphene.py index 50e1ccdc98c..dbfee8cc611 100644 --- a/tests/contrib/graphene/test_graphene.py +++ b/tests/contrib/graphene/test_graphene.py @@ -32,7 +32,7 @@ def resolve_patron(root, info): class Query(graphene.ObjectType): user = graphene.String(id=graphene.ID()) - def resolve_user(self, info, id): # noqa: A002 + def resolve_user(self, info, id): # noqa: A002 if id != "123": raise graphql.error.GraphQLError( "User not found", From 2fced067947db4b7e2a3ee50c3259167c251a2ff Mon Sep 17 00:00:00 2001 From: quinna-h Date: Tue, 4 Feb 2025 11:51:30 -0500 Subject: [PATCH 11/24] type checking --- ddtrace/contrib/internal/graphql/patch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ddtrace/contrib/internal/graphql/patch.py b/ddtrace/contrib/internal/graphql/patch.py index 4050a24002b..4338b4d9f20 100644 --- a/ddtrace/contrib/internal/graphql/patch.py +++ b/ddtrace/contrib/internal/graphql/patch.py @@ -293,7 +293,7 @@ def _get_source_str(obj): return re.sub(r"\s+", " ", source_str).strip() -def _validate_error_extensions(error: GraphQLError, extensions: str | None, attributes: Dict) -> Tuple[Dict, Dict]: +def _validate_error_extensions(error: GraphQLError, extensions: Optional[str], attributes: Dict) -> Tuple[Dict, Dict]: # Validate user-provided extensions if not extensions: return {}, attributes From 3cfc20c7b948d1798a1c3c1bdcbc16f726e128b9 Mon Sep 17 00:00:00 2001 From: quinna-h Date: Tue, 4 Feb 2025 12:05:58 -0500 Subject: [PATCH 12/24] update snapshot decorator arguments --- tests/contrib/graphql/test_graphql.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/contrib/graphql/test_graphql.py b/tests/contrib/graphql/test_graphql.py index 0d80f9dbe32..1e6c11896b0 100644 --- a/tests/contrib/graphql/test_graphql.py +++ b/tests/contrib/graphql/test_graphql.py @@ -82,7 +82,7 @@ def resolve_fail(root, info): return undefined_var.property -@snapshot(ignores=["meta.error.type", "meta.error.message", "meta.events"]) +@snapshot(ignores=["meta.error.type", "meta.error.message", "meta.error.stack", "meta.events"]) def test_graphql_fail(enable_graphql_resolvers): query = """ query { From f9a74b8753051f18d72a3d10eefdba31e5579c83 Mon Sep 17 00:00:00 2001 From: quinna-h Date: Wed, 5 Feb 2025 12:55:43 -0500 Subject: [PATCH 13/24] update error extensions --- ddtrace/contrib/internal/graphql/patch.py | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/ddtrace/contrib/internal/graphql/patch.py b/ddtrace/contrib/internal/graphql/patch.py index 4338b4d9f20..50ad0205409 100644 --- a/ddtrace/contrib/internal/graphql/patch.py +++ b/ddtrace/contrib/internal/graphql/patch.py @@ -294,7 +294,11 @@ def _get_source_str(obj): def _validate_error_extensions(error: GraphQLError, extensions: Optional[str], attributes: Dict) -> Tuple[Dict, Dict]: - # Validate user-provided extensions + """Validate user-provided extensions + All extensions values MUST be stringified, EXCEPT for numeric values and + boolean values, which remain in their original type. + """ + if not extensions: return {}, attributes @@ -302,22 +306,11 @@ def _validate_error_extensions(error: GraphQLError, extensions: Optional[str], a error_extensions = {} for field in fields: if field in error.extensions: - # validate extensions formatting - # All extensions values MUST be stringified, EXCEPT for numeric values and - # boolean values, which remain in their original type. if isinstance(error.extensions[field], (int, float, bool)): error_extensions[field] = error.extensions[field] else: - # q: could this be `None`? error_extensions[field] = str(error.extensions[field]) - # Additional validation for Apollo Server attributes - if field == "stacktrace": - attributes["type"] = error.extensions[field].split(":")[0] - attributes["stacktrace"] = "\n".join(error.extensions[field]) - elif field == "code": - attributes["code"] = error.extensions[field] - return error_extensions, attributes From dc9fc1ec5863fbc80a06768d4f14e11c64a26331 Mon Sep 17 00:00:00 2001 From: quinna-h Date: Thu, 6 Feb 2025 12:39:11 -0500 Subject: [PATCH 14/24] formatting --- ddtrace/contrib/internal/graphql/patch.py | 23 ++++++++--------------- tests/contrib/graphql/test_graphql.py | 2 -- 2 files changed, 8 insertions(+), 17 deletions(-) diff --git a/ddtrace/contrib/internal/graphql/patch.py b/ddtrace/contrib/internal/graphql/patch.py index 50ad0205409..0b3c7a4d790 100644 --- a/ddtrace/contrib/internal/graphql/patch.py +++ b/ddtrace/contrib/internal/graphql/patch.py @@ -2,20 +2,13 @@ import re import sys import traceback -from typing import TYPE_CHECKING -from typing import List - -from ddtrace.internal.schema.span_attribute_schema import SpanDirection -from ddtrace.trace import Span - - -if TYPE_CHECKING: # pragma: no cover - from typing import Callable # noqa:F401 - from typing import Dict # noqa:F401 - from typing import Iterable # noqa:F401 - from typing import List - from typing import Tuple # noqa:F401 - from typing import Union # noqa:F401 +from typing import Callable # noqa:F401 +from typing import Dict # noqa:F401 +from typing import Iterable # noqa:F401 +from typing import List # noqa:F401 +from typing import Optional # noqa:F401 +from typing import Tuple # noqa:F401 +from typing import Union # noqa:F401 import graphql from graphql import MiddlewareManager @@ -24,7 +17,6 @@ from graphql.language.source import Source from ddtrace import config -from ddtrace._trace.span import Span from ddtrace.constants import _ANALYTICS_SAMPLE_RATE_KEY from ddtrace.constants import _SPAN_MEASURED_KEY from ddtrace.constants import ERROR_MSG @@ -44,6 +36,7 @@ from ddtrace.internal.wrapping import unwrap from ddtrace.internal.wrapping import wrap from ddtrace.trace import Pin +from ddtrace.trace import Span _graphql_version_str = graphql.__version__ diff --git a/tests/contrib/graphql/test_graphql.py b/tests/contrib/graphql/test_graphql.py index 1e6c11896b0..ab3321aad8b 100644 --- a/tests/contrib/graphql/test_graphql.py +++ b/tests/contrib/graphql/test_graphql.py @@ -9,8 +9,6 @@ from ddtrace.contrib.internal.graphql.patch import patch from ddtrace.contrib.internal.graphql.patch import unpatch from ddtrace.trace import tracer -from graphql import build_schema -from graphql import graphql_sync from tests.utils import override_config from tests.utils import snapshot From 6ab3b785dfdfe9d93b7f4f2b524d6c62edea33c7 Mon Sep 17 00:00:00 2001 From: quinna-h Date: Thu, 6 Feb 2025 12:56:52 -0500 Subject: [PATCH 15/24] add release note --- ...hql-error-span-events-add-extensions-5eece423cc8ff93e.yaml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 releasenotes/notes/graphql-error-span-events-add-extensions-5eece423cc8ff93e.yaml diff --git a/releasenotes/notes/graphql-error-span-events-add-extensions-5eece423cc8ff93e.yaml b/releasenotes/notes/graphql-error-span-events-add-extensions-5eece423cc8ff93e.yaml new file mode 100644 index 00000000000..d557a7b1fc9 --- /dev/null +++ b/releasenotes/notes/graphql-error-span-events-add-extensions-5eece423cc8ff93e.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + graphql: Add user provided extensions to graphql error span events. \ No newline at end of file From 2215d7d3971d231be1323b554b9505be424d79b3 Mon Sep 17 00:00:00 2001 From: quinna-h Date: Thu, 6 Feb 2025 12:57:33 -0500 Subject: [PATCH 16/24] format --- ...aphql-error-span-events-add-extensions-5eece423cc8ff93e.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/releasenotes/notes/graphql-error-span-events-add-extensions-5eece423cc8ff93e.yaml b/releasenotes/notes/graphql-error-span-events-add-extensions-5eece423cc8ff93e.yaml index d557a7b1fc9..67eb95aaefe 100644 --- a/releasenotes/notes/graphql-error-span-events-add-extensions-5eece423cc8ff93e.yaml +++ b/releasenotes/notes/graphql-error-span-events-add-extensions-5eece423cc8ff93e.yaml @@ -1,4 +1,4 @@ --- features: - | - graphql: Add user provided extensions to graphql error span events. \ No newline at end of file + graphql: Add user provided extensions to graphql error span events. From d9d1ee31afee4d45f185f3b5beab58a22919489a Mon Sep 17 00:00:00 2001 From: quinna-h Date: Thu, 13 Feb 2025 12:09:43 -0500 Subject: [PATCH 17/24] formatting --- ddtrace/contrib/internal/graphql/patch.py | 5 ++--- tests/contrib/graphql/test_graphql.py | 1 - 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/ddtrace/contrib/internal/graphql/patch.py b/ddtrace/contrib/internal/graphql/patch.py index 2c1afffa07b..e461b8e062f 100644 --- a/ddtrace/contrib/internal/graphql/patch.py +++ b/ddtrace/contrib/internal/graphql/patch.py @@ -2,9 +2,9 @@ import re import sys import traceback - from typing import TYPE_CHECKING from typing import List +from typing import Optional from ddtrace.internal.schema.span_attribute_schema import SpanDirection from ddtrace.trace import Span @@ -17,6 +17,7 @@ from typing import Tuple # noqa:F401 from typing import Union # noqa:F401 + import graphql from graphql import MiddlewareManager from graphql.error import GraphQLError @@ -34,7 +35,6 @@ from ddtrace.internal.constants import COMPONENT from ddtrace.internal.schema import schematize_service_name from ddtrace.internal.schema import schematize_url_operation -from ddtrace.internal.schema.span_attribute_schema import SpanDirection from ddtrace.internal.utils import ArgumentError from ddtrace.internal.utils import get_argument_value from ddtrace.internal.utils import set_argument_value @@ -43,7 +43,6 @@ from ddtrace.internal.wrapping import unwrap from ddtrace.internal.wrapping import wrap from ddtrace.trace import Pin -from ddtrace.trace import Span _graphql_version_str = graphql.__version__ diff --git a/tests/contrib/graphql/test_graphql.py b/tests/contrib/graphql/test_graphql.py index d199a0a9bda..674c022a8e4 100644 --- a/tests/contrib/graphql/test_graphql.py +++ b/tests/contrib/graphql/test_graphql.py @@ -80,7 +80,6 @@ def resolve_fail(root, info): return undefined_var.property - @snapshot(ignores=["meta.error.stack", "meta.events"]) def test_graphql_fail(enable_graphql_resolvers): query = """ From a908db5908474927ae6764163b353d44a7bb4e9e Mon Sep 17 00:00:00 2001 From: quinna-h Date: Thu, 13 Feb 2025 12:20:10 -0500 Subject: [PATCH 18/24] update release note --- ...aphql-error-span-events-add-extensions-5eece423cc8ff93e.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/releasenotes/notes/graphql-error-span-events-add-extensions-5eece423cc8ff93e.yaml b/releasenotes/notes/graphql-error-span-events-add-extensions-5eece423cc8ff93e.yaml index 67eb95aaefe..122dd7475d1 100644 --- a/releasenotes/notes/graphql-error-span-events-add-extensions-5eece423cc8ff93e.yaml +++ b/releasenotes/notes/graphql-error-span-events-add-extensions-5eece423cc8ff93e.yaml @@ -1,4 +1,4 @@ --- features: - | - graphql: Add user provided extensions to graphql error span events. + graphql: Add user provided extensions to graphql error span events through the environment variable `DD_TRACE_GRAPHQL_ERROR_EXTENSIONS`. From 531786ce95f8953e744b6e02693f9129bcb50351 Mon Sep 17 00:00:00 2001 From: quinna-h Date: Thu, 13 Feb 2025 12:45:49 -0500 Subject: [PATCH 19/24] update --- ddtrace/contrib/internal/graphql/patch.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ddtrace/contrib/internal/graphql/patch.py b/ddtrace/contrib/internal/graphql/patch.py index e461b8e062f..da3c868fc63 100644 --- a/ddtrace/contrib/internal/graphql/patch.py +++ b/ddtrace/contrib/internal/graphql/patch.py @@ -3,8 +3,10 @@ import sys import traceback from typing import TYPE_CHECKING +from typing import Dict from typing import List from typing import Optional +from typing import Tuple from ddtrace.internal.schema.span_attribute_schema import SpanDirection from ddtrace.trace import Span @@ -12,9 +14,7 @@ if TYPE_CHECKING: # pragma: no cover from typing import Callable # noqa:F401 - from typing import Dict # noqa:F401 from typing import Iterable # noqa:F401 - from typing import Tuple # noqa:F401 from typing import Union # noqa:F401 From 6b9c4358311345d69b978c1dc5d2da5a5edcb2ca Mon Sep 17 00:00:00 2001 From: quinna-h Date: Fri, 14 Feb 2025 11:36:15 -0500 Subject: [PATCH 20/24] format --- ddtrace/contrib/internal/graphql/patch.py | 35 ++++++++++++----------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/ddtrace/contrib/internal/graphql/patch.py b/ddtrace/contrib/internal/graphql/patch.py index da3c868fc63..5a8c3e508f8 100644 --- a/ddtrace/contrib/internal/graphql/patch.py +++ b/ddtrace/contrib/internal/graphql/patch.py @@ -6,7 +6,6 @@ from typing import Dict from typing import List from typing import Optional -from typing import Tuple from ddtrace.internal.schema.span_attribute_schema import SpanDirection from ddtrace.trace import Span @@ -59,14 +58,24 @@ def get_version(): return _graphql_version_str +def _parse_error_extensions(error_extensions: Optional[str]): + """Parse the user provided error extensions.""" + if error_extensions is not None: + fields = [e.strip() for e in error_extensions.split(",")] + return fields + return None + + config._add( "graphql", dict( _default_service=schematize_service_name("graphql"), resolvers_enabled=asbool(os.getenv("DD_TRACE_GRAPHQL_RESOLVERS_ENABLED", default=False)), + _error_extensions=_parse_error_extensions(os.getenv("DD_TRACE_GRAPHQL_ERROR_EXTENSIONS")), ), ) + _GRAPHQL_SOURCE = "graphql.source" _GRAPHQL_OPERATION_TYPE = "graphql.operation.type" _GRAPHQL_OPERATION_NAME = "graphql.operation.name" @@ -292,25 +301,20 @@ def _get_source_str(obj): return re.sub(r"\s+", " ", source_str).strip() -def _validate_error_extensions(error: GraphQLError, extensions: Optional[str], attributes: Dict) -> Tuple[Dict, Dict]: - """Validate user-provided extensions +def _validate_error_extensions(error: GraphQLError, error_extension_fields: List) -> Dict: + """Validate user-provided extensions format and return the formatted extensions. All extensions values MUST be stringified, EXCEPT for numeric values and boolean values, which remain in their original type. """ - - if not extensions: - return {}, attributes - - fields = [e.strip() for e in extensions.split(",")] error_extensions = {} - for field in fields: + for field in error_extension_fields: if field in error.extensions: if isinstance(error.extensions[field], (int, float, bool)): error_extensions[field] = error.extensions[field] else: error_extensions[field] = str(error.extensions[field]) - return error_extensions, attributes + return error_extensions def _set_span_errors(errors: List[GraphQLError], span: Span) -> None: @@ -347,12 +351,11 @@ def _set_span_errors(errors: List[GraphQLError], span: Span) -> None: path = ",".join([str(path_obj) for path_obj in error.path]) attributes["path"] = path - if os.environ.get("DD_TRACE_GRAPHQL_ERROR_EXTENSIONS") is not None: - extensions = os.environ.get("DD_TRACE_GRAPHQL_ERROR_EXTENSIONS") - - error_extensions, attributes = _validate_error_extensions(error, extensions, attributes) - if error_extensions: - attributes["extensions"] = str(error_extensions) + error_extension_fields = config.graphql._error_extensions + if error_extension_fields is not None: + extensions = _validate_error_extensions(error, error_extension_fields) + if extensions: + attributes["extensions"] = extensions span._add_event( name="dd.graphql.query.error", From 4acf6570b4b57214f5d90a72342c77d267b1231c Mon Sep 17 00:00:00 2001 From: quinna-h Date: Fri, 14 Feb 2025 13:31:12 -0500 Subject: [PATCH 21/24] add comment --- ddtrace/contrib/_graphql.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ddtrace/contrib/_graphql.py b/ddtrace/contrib/_graphql.py index d4d8d769d61..e22aef69407 100644 --- a/ddtrace/contrib/_graphql.py +++ b/ddtrace/contrib/_graphql.py @@ -35,6 +35,12 @@ Enabling instrumentation for resolvers will produce a ``graphql.resolve`` span for every graphql field. For complex graphql queries this could produce large traces. +.. py:data:: ddtrace.config.graphql["_error_extensions"] + + Enable setting user-provided error extensions on span events for graphql errors. + + Default: ``None`` + To configure the graphql integration using the ``Pin`` API:: From d9d9261af8f7307f7d90a347ad4ef899c2c540f2 Mon Sep 17 00:00:00 2001 From: quinna-h Date: Fri, 14 Feb 2025 15:20:57 -0500 Subject: [PATCH 22/24] update release note --- ...aphql-error-span-events-add-extensions-5eece423cc8ff93e.yaml | 2 +- tests/contrib/graphene/test_graphene.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/releasenotes/notes/graphql-error-span-events-add-extensions-5eece423cc8ff93e.yaml b/releasenotes/notes/graphql-error-span-events-add-extensions-5eece423cc8ff93e.yaml index 122dd7475d1..9f31ee186d2 100644 --- a/releasenotes/notes/graphql-error-span-events-add-extensions-5eece423cc8ff93e.yaml +++ b/releasenotes/notes/graphql-error-span-events-add-extensions-5eece423cc8ff93e.yaml @@ -1,4 +1,4 @@ --- features: - | - graphql: Add user provided extensions to graphql error span events through the environment variable `DD_TRACE_GRAPHQL_ERROR_EXTENSIONS`. + graphql: Adds user-provided extensions to graphql error span events through the `DD_TRACE_GRAPHQL_ERROR_EXTENSIONS` environment variable. This is disabled by default; when set it allows users to capture their extensions. diff --git a/tests/contrib/graphene/test_graphene.py b/tests/contrib/graphene/test_graphene.py index 1ae25dfef4d..dbf8f4941d7 100644 --- a/tests/contrib/graphene/test_graphene.py +++ b/tests/contrib/graphene/test_graphene.py @@ -121,6 +121,7 @@ def test_schema_failing_extensions(test_schema, test_source_str, enable_graphql_ query_string = '{ user(id: "999") }' result = schema.execute(query_string) assert result.errors + assert result.errors[0].extensions is not None @pytest.mark.snapshot( From d2c9a39878ed8baa3b1e5a4cce71c40ef2225d2b Mon Sep 17 00:00:00 2001 From: quinna-h Date: Fri, 14 Feb 2025 15:56:22 -0500 Subject: [PATCH 23/24] update test and snapshot --- tests/contrib/graphene/test_graphene.py | 11 +++- ...aphene.test_schema_failing_extensions.json | 59 +++++++------------ 2 files changed, 28 insertions(+), 42 deletions(-) diff --git a/tests/contrib/graphene/test_graphene.py b/tests/contrib/graphene/test_graphene.py index dbf8f4941d7..fb77c74f917 100644 --- a/tests/contrib/graphene/test_graphene.py +++ b/tests/contrib/graphene/test_graphene.py @@ -1,5 +1,3 @@ -import os - import graphene import graphql import pytest @@ -114,10 +112,17 @@ async def test_schema_execute_async_with_resolvers(test_schema, test_source_str, assert result.data == {"patron": {"id": "1", "name": "Syrus", "age": 27}} +@pytest.mark.subprocess(env=dict(DD_TRACE_GRAPHQL_ERROR_EXTENSIONS="code, status")) @pytest.mark.snapshot(ignores=["meta.events", "meta.error.stack"]) def test_schema_failing_extensions(test_schema, test_source_str, enable_graphql_resolvers): + import graphene + + from ddtrace.contrib.internal.graphql.patch import patch + from tests.contrib.graphene.test_graphene import Query + + patch() + schema = graphene.Schema(query=Query) - os.environ["DD_TRACE_GRAPHQL_ERROR_EXTENSIONS"] = "code, status" query_string = '{ user(id: "999") }' result = schema.execute(query_string) assert result.errors diff --git a/tests/snapshots/tests.contrib.graphene.test_graphene.test_schema_failing_extensions.json b/tests/snapshots/tests.contrib.graphene.test_graphene.test_schema_failing_extensions.json index 11391a3d342..8ba76e0cffc 100644 --- a/tests/snapshots/tests.contrib.graphene.test_graphene.test_schema_failing_extensions.json +++ b/tests/snapshots/tests.contrib.graphene.test_graphene.test_schema_failing_extensions.json @@ -9,26 +9,26 @@ "type": "graphql", "error": 1, "meta": { - "_dd.base_service": "tests.contrib.graphene", + "_dd.base_service": "ddtrace_subprocess_dir", "_dd.p.dm": "-0", - "_dd.p.tid": "67a1663a00000000", + "_dd.p.tid": "67afad1f00000000", "component": "graphql", "error.message": "User not found\n\nGraphQL request:1:3\n1 | { user(id: \"999\") }\n | ^", - "error.stack": "Traceback (most recent call last):\n File \"/Users/quinna.halim/dd-trace-py/.riot/venv_py3130_mock_pytest_pytest-mock_coverage_pytest-cov_opentracing_hypothesis6451_graphene~300_pytest-asyncio0211_graphql-relay_pytest-randomly/lib/python3.13/site-packages/graphql/execution/execute.py\", line 617, in resolve_field\n result = resolve_fn(source, info, **args)\n File \"/Users/quinna.halim/dd-trace-py/ddtrace/contrib/internal/graphql/patch.py\", line 242, in _resolver_middleware\n return next_middleware(root, info, **args)\n File \"/Users/quinna.halim/dd-trace-py/tests/contrib/graphene/test_graphene.py\", line 37, in resolve_user\n raise graphql.error.GraphQLError(\n ...<7 lines>...\n )\ngraphql.error.graphql_error.GraphQLError: User not found\n\nGraphQL request:1:3\n1 | { user(id: \"999\") }\n | ^\n", + "error.stack": "Traceback (most recent call last):\n\n File \"/Users/quinna.halim/dd-trace-py/.riot/venv_py31013_mock_pytest_pytest-mock_coverage_pytest-cov_opentracing_hypothesis6451_graphene~300_pytest-asyncio0211_graphql-relay_pytest-randomly/lib/python3.10/site-packages/graphql/execution/execute.py\", line 617, in resolve_field\n result = resolve_fn(source, info, **args)\n\n File \"/Users/quinna.halim/dd-trace-py/tests/contrib/graphene/test_graphene.py\", line 37, in resolve_user\n raise graphql.error.GraphQLError(\n\ngraphql.error.graphql_error.GraphQLError: User not found\n\nGraphQL request:1:3\n1 | { user(id: \"999\") }\n | ^\n", "error.type": "graphql.error.graphql_error.GraphQLError", - "events": "[{\"name\": \"dd.graphql.query.error\", \"time_unix_nano\": 1738630714389656000, \"attributes\": {\"message\": \"User not found\", \"type\": \"graphql.error.graphql_error.GraphQLError\", \"locations\": \"1:3\", \"stacktrace\": \"Traceback (most recent call last):\\n File \\\"/Users/quinna.halim/dd-trace-py/.riot/venv_py3130_mock_pytest_pytest-mock_coverage_pytest-cov_opentracing_hypothesis6451_graphene~300_pytest-asyncio0211_graphql-relay_pytest-randomly/lib/python3.13/site-packages/graphql/execution/execute.py\\\", line 617, in resolve_field\\n result = resolve_fn(source, info, **args)\\n File \\\"/Users/quinna.halim/dd-trace-py/ddtrace/contrib/internal/graphql/patch.py\\\", line 242, in _resolver_middleware\\n return next_middleware(root, info, **args)\\n File \\\"/Users/quinna.halim/dd-trace-py/tests/contrib/graphene/test_graphene.py\\\", line 37, in resolve_user\\n raise graphql.error.GraphQLError(\\n ...<7 lines>...\\n )\\ngraphql.error.graphql_error.GraphQLError: User not found\\n\\nGraphQL request:1:3\\n1 | { user(id: \\\"999\\\") }\\n | ^\\n\", \"path\": \"user\", \"code\": \"USER_NOT_FOUND\", \"extensions\": \"{'code': 'USER_NOT_FOUND', 'status': 404}\"}}]", + "events": "[{\"name\": \"dd.graphql.query.error\", \"time_unix_nano\": 1739566367698511000, \"attributes\": {\"message\": \"User not found\", \"type\": \"graphql.error.graphql_error.GraphQLError\", \"locations\": [\"1:3\"], \"stacktrace\": \"Traceback (most recent call last):\\n\\n File \\\"/Users/quinna.halim/dd-trace-py/.riot/venv_py31013_mock_pytest_pytest-mock_coverage_pytest-cov_opentracing_hypothesis6451_graphene~300_pytest-asyncio0211_graphql-relay_pytest-randomly/lib/python3.10/site-packages/graphql/execution/execute.py\\\", line 617, in resolve_field\\n result = resolve_fn(source, info, **args)\\n\\n File \\\"/Users/quinna.halim/dd-trace-py/tests/contrib/graphene/test_graphene.py\\\", line 37, in resolve_user\\n raise graphql.error.GraphQLError(\\n\\ngraphql.error.graphql_error.GraphQLError: User not found\\n\\nGraphQL request:1:3\\n1 | { user(id: \\\"999\\\") }\\n | ^\\n\", \"path\": \"user\", \"extensions\": {\"code\": \"USER_NOT_FOUND\", \"status\": 404}}}]", "language": "python", - "runtime-id": "eee5cdadb35249afb9f5d6e1c304be24" + "runtime-id": "ca05af4f165a4763a02c53075fe1b37b" }, "metrics": { "_dd.measured": 1, "_dd.top_level": 1, "_dd.tracer_kr": 1.0, "_sampling_priority_v1": 1, - "process_id": 43930 + "process_id": 24906 }, - "duration": 3813000, - "start": 1738630714385845000 + "duration": 924000, + "start": 1739566367697588000 }, { "name": "graphql.parse", @@ -40,12 +40,12 @@ "type": "graphql", "error": 0, "meta": { - "_dd.base_service": "tests.contrib.graphene", + "_dd.base_service": "ddtrace_subprocess_dir", "component": "graphql", "graphql.source": "{ user(id: \"999\") }" }, - "duration": 180000, - "start": 1738630714386234000 + "duration": 60000, + "start": 1739566367697707000 }, { "name": "graphql.validate", @@ -57,12 +57,12 @@ "type": "graphql", "error": 0, "meta": { - "_dd.base_service": "tests.contrib.graphene", + "_dd.base_service": "ddtrace_subprocess_dir", "component": "graphql", "graphql.source": "{ user(id: \"999\") }" }, - "duration": 888000, - "start": 1738630714386488000 + "duration": 376000, + "start": 1739566367697791000 }, { "name": "graphql.execute", @@ -74,37 +74,18 @@ "type": "graphql", "error": 1, "meta": { - "_dd.base_service": "tests.contrib.graphene", + "_dd.base_service": "ddtrace_subprocess_dir", "component": "graphql", "error.message": "User not found\n\nGraphQL request:1:3\n1 | { user(id: \"999\") }\n | ^", - "error.stack": "Traceback (most recent call last):\n File \"/Users/quinna.halim/dd-trace-py/.riot/venv_py3130_mock_pytest_pytest-mock_coverage_pytest-cov_opentracing_hypothesis6451_graphene~300_pytest-asyncio0211_graphql-relay_pytest-randomly/lib/python3.13/site-packages/graphql/execution/execute.py\", line 617, in resolve_field\n result = resolve_fn(source, info, **args)\n File \"/Users/quinna.halim/dd-trace-py/ddtrace/contrib/internal/graphql/patch.py\", line 242, in _resolver_middleware\n return next_middleware(root, info, **args)\n File \"/Users/quinna.halim/dd-trace-py/tests/contrib/graphene/test_graphene.py\", line 37, in resolve_user\n raise graphql.error.GraphQLError(\n ...<7 lines>...\n )\ngraphql.error.graphql_error.GraphQLError: User not found\n\nGraphQL request:1:3\n1 | { user(id: \"999\") }\n | ^\n", + "error.stack": "Traceback (most recent call last):\n\n File \"/Users/quinna.halim/dd-trace-py/.riot/venv_py31013_mock_pytest_pytest-mock_coverage_pytest-cov_opentracing_hypothesis6451_graphene~300_pytest-asyncio0211_graphql-relay_pytest-randomly/lib/python3.10/site-packages/graphql/execution/execute.py\", line 617, in resolve_field\n result = resolve_fn(source, info, **args)\n\n File \"/Users/quinna.halim/dd-trace-py/tests/contrib/graphene/test_graphene.py\", line 37, in resolve_user\n raise graphql.error.GraphQLError(\n\ngraphql.error.graphql_error.GraphQLError: User not found\n\nGraphQL request:1:3\n1 | { user(id: \"999\") }\n | ^\n", "error.type": "graphql.error.graphql_error.GraphQLError", - "events": "[{\"name\": \"dd.graphql.query.error\", \"time_unix_nano\": 1738630714389350000, \"attributes\": {\"message\": \"User not found\", \"type\": \"graphql.error.graphql_error.GraphQLError\", \"locations\": \"1:3\", \"stacktrace\": \"Traceback (most recent call last):\\n File \\\"/Users/quinna.halim/dd-trace-py/.riot/venv_py3130_mock_pytest_pytest-mock_coverage_pytest-cov_opentracing_hypothesis6451_graphene~300_pytest-asyncio0211_graphql-relay_pytest-randomly/lib/python3.13/site-packages/graphql/execution/execute.py\\\", line 617, in resolve_field\\n result = resolve_fn(source, info, **args)\\n File \\\"/Users/quinna.halim/dd-trace-py/ddtrace/contrib/internal/graphql/patch.py\\\", line 242, in _resolver_middleware\\n return next_middleware(root, info, **args)\\n File \\\"/Users/quinna.halim/dd-trace-py/tests/contrib/graphene/test_graphene.py\\\", line 37, in resolve_user\\n raise graphql.error.GraphQLError(\\n ...<7 lines>...\\n )\\ngraphql.error.graphql_error.GraphQLError: User not found\\n\\nGraphQL request:1:3\\n1 | { user(id: \\\"999\\\") }\\n | ^\\n\", \"path\": \"user\", \"code\": \"USER_NOT_FOUND\", \"extensions\": \"{'code': 'USER_NOT_FOUND', 'status': 404}\"}}]", + "events": "[{\"name\": \"dd.graphql.query.error\", \"time_unix_nano\": 1739566367698466000, \"attributes\": {\"message\": \"User not found\", \"type\": \"graphql.error.graphql_error.GraphQLError\", \"locations\": [\"1:3\"], \"stacktrace\": \"Traceback (most recent call last):\\n\\n File \\\"/Users/quinna.halim/dd-trace-py/.riot/venv_py31013_mock_pytest_pytest-mock_coverage_pytest-cov_opentracing_hypothesis6451_graphene~300_pytest-asyncio0211_graphql-relay_pytest-randomly/lib/python3.10/site-packages/graphql/execution/execute.py\\\", line 617, in resolve_field\\n result = resolve_fn(source, info, **args)\\n\\n File \\\"/Users/quinna.halim/dd-trace-py/tests/contrib/graphene/test_graphene.py\\\", line 37, in resolve_user\\n raise graphql.error.GraphQLError(\\n\\ngraphql.error.graphql_error.GraphQLError: User not found\\n\\nGraphQL request:1:3\\n1 | { user(id: \\\"999\\\") }\\n | ^\\n\", \"path\": \"user\", \"extensions\": {\"code\": \"USER_NOT_FOUND\", \"status\": 404}}}]", "graphql.operation.type": "query", "graphql.source": "{ user(id: \"999\") }" }, "metrics": { "_dd.measured": 1 }, - "duration": 1921000, - "start": 1738630714387431000 - }, - { - "name": "graphql.resolve", - "service": "graphql", - "resource": "user", - "trace_id": 0, - "span_id": 5, - "parent_id": 4, - "type": "graphql", - "error": 1, - "meta": { - "_dd.base_service": "tests.contrib.graphene", - "component": "graphql", - "error.message": "User not found", - "error.stack": "Traceback (most recent call last):\n File \"/Users/quinna.halim/dd-trace-py/ddtrace/contrib/internal/graphql/patch.py\", line 242, in _resolver_middleware\n return next_middleware(root, info, **args)\n File \"/Users/quinna.halim/dd-trace-py/tests/contrib/graphene/test_graphene.py\", line 37, in resolve_user\n raise graphql.error.GraphQLError(\n ...<7 lines>...\n )\ngraphql.error.graphql_error.GraphQLError: User not found\n", - "error.type": "graphql.error.graphql_error.GraphQLError" - }, - "duration": 1152000, - "start": 1738630714387593000 - }]] + "duration": 279000, + "start": 1739566367698188000 + }]] From 1a61c15fbfe676471160284273dfc14cefc3bbdd Mon Sep 17 00:00:00 2001 From: quinna-h Date: Tue, 18 Feb 2025 13:31:23 -0500 Subject: [PATCH 24/24] change extensions keys --- ddtrace/contrib/internal/graphql/patch.py | 4 +-- tests/contrib/graphene/test_graphene.py | 1 - ...aphene.test_schema_failing_extensions.json | 30 +++++++++---------- 3 files changed, 17 insertions(+), 18 deletions(-) diff --git a/ddtrace/contrib/internal/graphql/patch.py b/ddtrace/contrib/internal/graphql/patch.py index 5a8c3e508f8..f172a5e1342 100644 --- a/ddtrace/contrib/internal/graphql/patch.py +++ b/ddtrace/contrib/internal/graphql/patch.py @@ -355,8 +355,8 @@ def _set_span_errors(errors: List[GraphQLError], span: Span) -> None: if error_extension_fields is not None: extensions = _validate_error_extensions(error, error_extension_fields) if extensions: - attributes["extensions"] = extensions - + for key in extensions: + attributes[f"extensions.{key}"] = extensions[key] span._add_event( name="dd.graphql.query.error", attributes=attributes, diff --git a/tests/contrib/graphene/test_graphene.py b/tests/contrib/graphene/test_graphene.py index fb77c74f917..c1aa07d5904 100644 --- a/tests/contrib/graphene/test_graphene.py +++ b/tests/contrib/graphene/test_graphene.py @@ -126,7 +126,6 @@ def test_schema_failing_extensions(test_schema, test_source_str, enable_graphql_ query_string = '{ user(id: "999") }' result = schema.execute(query_string) assert result.errors - assert result.errors[0].extensions is not None @pytest.mark.snapshot( diff --git a/tests/snapshots/tests.contrib.graphene.test_graphene.test_schema_failing_extensions.json b/tests/snapshots/tests.contrib.graphene.test_graphene.test_schema_failing_extensions.json index 8ba76e0cffc..b1112f8d3c6 100644 --- a/tests/snapshots/tests.contrib.graphene.test_graphene.test_schema_failing_extensions.json +++ b/tests/snapshots/tests.contrib.graphene.test_graphene.test_schema_failing_extensions.json @@ -11,24 +11,24 @@ "meta": { "_dd.base_service": "ddtrace_subprocess_dir", "_dd.p.dm": "-0", - "_dd.p.tid": "67afad1f00000000", + "_dd.p.tid": "67b4cea400000000", "component": "graphql", "error.message": "User not found\n\nGraphQL request:1:3\n1 | { user(id: \"999\") }\n | ^", - "error.stack": "Traceback (most recent call last):\n\n File \"/Users/quinna.halim/dd-trace-py/.riot/venv_py31013_mock_pytest_pytest-mock_coverage_pytest-cov_opentracing_hypothesis6451_graphene~300_pytest-asyncio0211_graphql-relay_pytest-randomly/lib/python3.10/site-packages/graphql/execution/execute.py\", line 617, in resolve_field\n result = resolve_fn(source, info, **args)\n\n File \"/Users/quinna.halim/dd-trace-py/tests/contrib/graphene/test_graphene.py\", line 37, in resolve_user\n raise graphql.error.GraphQLError(\n\ngraphql.error.graphql_error.GraphQLError: User not found\n\nGraphQL request:1:3\n1 | { user(id: \"999\") }\n | ^\n", + "error.stack": "Traceback (most recent call last):\n\n File \"/Users/quinna.halim/dd-trace-py/.riot/venv_py3127_mock_pytest_pytest-mock_coverage_pytest-cov_opentracing_hypothesis6451_graphene~300_pytest-asyncio0211_graphql-relay_pytest-randomly/lib/python3.12/site-packages/graphql/execution/execute.py\", line 617, in resolve_field\n result = resolve_fn(source, info, **args)\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n File \"/Users/quinna.halim/dd-trace-py/tests/contrib/graphene/test_graphene.py\", line 35, in resolve_user\n raise graphql.error.GraphQLError(\n\ngraphql.error.graphql_error.GraphQLError: User not found\n\nGraphQL request:1:3\n1 | { user(id: \"999\") }\n | ^\n", "error.type": "graphql.error.graphql_error.GraphQLError", - "events": "[{\"name\": \"dd.graphql.query.error\", \"time_unix_nano\": 1739566367698511000, \"attributes\": {\"message\": \"User not found\", \"type\": \"graphql.error.graphql_error.GraphQLError\", \"locations\": [\"1:3\"], \"stacktrace\": \"Traceback (most recent call last):\\n\\n File \\\"/Users/quinna.halim/dd-trace-py/.riot/venv_py31013_mock_pytest_pytest-mock_coverage_pytest-cov_opentracing_hypothesis6451_graphene~300_pytest-asyncio0211_graphql-relay_pytest-randomly/lib/python3.10/site-packages/graphql/execution/execute.py\\\", line 617, in resolve_field\\n result = resolve_fn(source, info, **args)\\n\\n File \\\"/Users/quinna.halim/dd-trace-py/tests/contrib/graphene/test_graphene.py\\\", line 37, in resolve_user\\n raise graphql.error.GraphQLError(\\n\\ngraphql.error.graphql_error.GraphQLError: User not found\\n\\nGraphQL request:1:3\\n1 | { user(id: \\\"999\\\") }\\n | ^\\n\", \"path\": \"user\", \"extensions\": {\"code\": \"USER_NOT_FOUND\", \"status\": 404}}}]", + "events": "[{\"name\": \"dd.graphql.query.error\", \"time_unix_nano\": 1739902628776246000, \"attributes\": {\"message\": \"User not found\", \"type\": \"graphql.error.graphql_error.GraphQLError\", \"locations\": [\"1:3\"], \"stacktrace\": \"Traceback (most recent call last):\\n\\n File \\\"/Users/quinna.halim/dd-trace-py/.riot/venv_py3127_mock_pytest_pytest-mock_coverage_pytest-cov_opentracing_hypothesis6451_graphene~300_pytest-asyncio0211_graphql-relay_pytest-randomly/lib/python3.12/site-packages/graphql/execution/execute.py\\\", line 617, in resolve_field\\n result = resolve_fn(source, info, **args)\\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\\n\\n File \\\"/Users/quinna.halim/dd-trace-py/tests/contrib/graphene/test_graphene.py\\\", line 35, in resolve_user\\n raise graphql.error.GraphQLError(\\n\\ngraphql.error.graphql_error.GraphQLError: User not found\\n\\nGraphQL request:1:3\\n1 | { user(id: \\\"999\\\") }\\n | ^\\n\", \"path\": \"user\", \"extensions.code\": \"USER_NOT_FOUND\", \"extensions.status\": 404}}]", "language": "python", - "runtime-id": "ca05af4f165a4763a02c53075fe1b37b" + "runtime-id": "8cbbf6dde3964c80b5a6a70abc93ac87" }, "metrics": { "_dd.measured": 1, "_dd.top_level": 1, "_dd.tracer_kr": 1.0, "_sampling_priority_v1": 1, - "process_id": 24906 + "process_id": 48668 }, - "duration": 924000, - "start": 1739566367697588000 + "duration": 1010000, + "start": 1739902628775236000 }, { "name": "graphql.parse", @@ -44,8 +44,8 @@ "component": "graphql", "graphql.source": "{ user(id: \"999\") }" }, - "duration": 60000, - "start": 1739566367697707000 + "duration": 124000, + "start": 1739902628775328000 }, { "name": "graphql.validate", @@ -61,8 +61,8 @@ "component": "graphql", "graphql.source": "{ user(id: \"999\") }" }, - "duration": 376000, - "start": 1739566367697791000 + "duration": 309000, + "start": 1739902628775473000 }, { "name": "graphql.execute", @@ -77,15 +77,15 @@ "_dd.base_service": "ddtrace_subprocess_dir", "component": "graphql", "error.message": "User not found\n\nGraphQL request:1:3\n1 | { user(id: \"999\") }\n | ^", - "error.stack": "Traceback (most recent call last):\n\n File \"/Users/quinna.halim/dd-trace-py/.riot/venv_py31013_mock_pytest_pytest-mock_coverage_pytest-cov_opentracing_hypothesis6451_graphene~300_pytest-asyncio0211_graphql-relay_pytest-randomly/lib/python3.10/site-packages/graphql/execution/execute.py\", line 617, in resolve_field\n result = resolve_fn(source, info, **args)\n\n File \"/Users/quinna.halim/dd-trace-py/tests/contrib/graphene/test_graphene.py\", line 37, in resolve_user\n raise graphql.error.GraphQLError(\n\ngraphql.error.graphql_error.GraphQLError: User not found\n\nGraphQL request:1:3\n1 | { user(id: \"999\") }\n | ^\n", + "error.stack": "Traceback (most recent call last):\n\n File \"/Users/quinna.halim/dd-trace-py/.riot/venv_py3127_mock_pytest_pytest-mock_coverage_pytest-cov_opentracing_hypothesis6451_graphene~300_pytest-asyncio0211_graphql-relay_pytest-randomly/lib/python3.12/site-packages/graphql/execution/execute.py\", line 617, in resolve_field\n result = resolve_fn(source, info, **args)\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n File \"/Users/quinna.halim/dd-trace-py/tests/contrib/graphene/test_graphene.py\", line 35, in resolve_user\n raise graphql.error.GraphQLError(\n\ngraphql.error.graphql_error.GraphQLError: User not found\n\nGraphQL request:1:3\n1 | { user(id: \"999\") }\n | ^\n", "error.type": "graphql.error.graphql_error.GraphQLError", - "events": "[{\"name\": \"dd.graphql.query.error\", \"time_unix_nano\": 1739566367698466000, \"attributes\": {\"message\": \"User not found\", \"type\": \"graphql.error.graphql_error.GraphQLError\", \"locations\": [\"1:3\"], \"stacktrace\": \"Traceback (most recent call last):\\n\\n File \\\"/Users/quinna.halim/dd-trace-py/.riot/venv_py31013_mock_pytest_pytest-mock_coverage_pytest-cov_opentracing_hypothesis6451_graphene~300_pytest-asyncio0211_graphql-relay_pytest-randomly/lib/python3.10/site-packages/graphql/execution/execute.py\\\", line 617, in resolve_field\\n result = resolve_fn(source, info, **args)\\n\\n File \\\"/Users/quinna.halim/dd-trace-py/tests/contrib/graphene/test_graphene.py\\\", line 37, in resolve_user\\n raise graphql.error.GraphQLError(\\n\\ngraphql.error.graphql_error.GraphQLError: User not found\\n\\nGraphQL request:1:3\\n1 | { user(id: \\\"999\\\") }\\n | ^\\n\", \"path\": \"user\", \"extensions\": {\"code\": \"USER_NOT_FOUND\", \"status\": 404}}}]", + "events": "[{\"name\": \"dd.graphql.query.error\", \"time_unix_nano\": 1739902628776152000, \"attributes\": {\"message\": \"User not found\", \"type\": \"graphql.error.graphql_error.GraphQLError\", \"locations\": [\"1:3\"], \"stacktrace\": \"Traceback (most recent call last):\\n\\n File \\\"/Users/quinna.halim/dd-trace-py/.riot/venv_py3127_mock_pytest_pytest-mock_coverage_pytest-cov_opentracing_hypothesis6451_graphene~300_pytest-asyncio0211_graphql-relay_pytest-randomly/lib/python3.12/site-packages/graphql/execution/execute.py\\\", line 617, in resolve_field\\n result = resolve_fn(source, info, **args)\\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\\n\\n File \\\"/Users/quinna.halim/dd-trace-py/tests/contrib/graphene/test_graphene.py\\\", line 35, in resolve_user\\n raise graphql.error.GraphQLError(\\n\\ngraphql.error.graphql_error.GraphQLError: User not found\\n\\nGraphQL request:1:3\\n1 | { user(id: \\\"999\\\") }\\n | ^\\n\", \"path\": \"user\", \"extensions.code\": \"USER_NOT_FOUND\", \"extensions.status\": 404}}]", "graphql.operation.type": "query", "graphql.source": "{ user(id: \"999\") }" }, "metrics": { "_dd.measured": 1 }, - "duration": 279000, - "start": 1739566367698188000 + "duration": 354000, + "start": 1739902628775799000 }]]