From d6ea4c568920e31dee1c5ccf671025673155ac8b Mon Sep 17 00:00:00 2001 From: guppy0130 Date: Mon, 17 Feb 2025 11:58:10 -0600 Subject: [PATCH] opentelemetry: use semconv attributes * use attributes defined in opentelemetry's semconv * stop setting span attribute `component`, as that's been deprecated * fixes #3769 --- RELEASE.md | 3 +++ .../extensions/tracing/opentelemetry.py | 27 ++++++++++++++++--- tests/schema/extensions/test_opentelemetry.py | 20 +++++++++++--- 3 files changed, 43 insertions(+), 7 deletions(-) create mode 100644 RELEASE.md diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 0000000000..52f8edb733 --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,3 @@ +Release type: patch + +Use OTEL's semconv attribute registry for span attributes. Stops setting `component`, which has been deprecated. diff --git a/strawberry/extensions/tracing/opentelemetry.py b/strawberry/extensions/tracing/opentelemetry.py index 56c301b7ca..cfa6e43c5e 100644 --- a/strawberry/extensions/tracing/opentelemetry.py +++ b/strawberry/extensions/tracing/opentelemetry.py @@ -11,6 +11,7 @@ ) from opentelemetry import trace +from opentelemetry.semconv._incubating.attributes import graphql_attributes from opentelemetry.trace import SpanKind from strawberry.extensions import LifecycleStep, SchemaExtension @@ -60,11 +61,16 @@ def on_operation(self) -> Generator[None, None, None]: self._span_holder[LifecycleStep.OPERATION] = self._tracer.start_span( span_name, kind=SpanKind.SERVER ) - self._span_holder[LifecycleStep.OPERATION].set_attribute("component", "graphql") + + # set the name if we have it. if we don't, we might populate it after parsing. + if self._operation_name: + self._span_holder[LifecycleStep.OPERATION].set_attribute( + graphql_attributes.GRAPHQL_OPERATION_NAME, self._operation_name + ) if self.execution_context.query: self._span_holder[LifecycleStep.OPERATION].set_attribute( - "query", self.execution_context.query + graphql_attributes.GRAPHQL_DOCUMENT, self.execution_context.query ) yield @@ -76,6 +82,22 @@ def on_operation(self) -> Generator[None, None, None]: if not self._operation_name and self.execution_context.operation_name: span_name = f"GraphQL Query: {self.execution_context.operation_name}" self._span_holder[LifecycleStep.OPERATION].update_name(span_name) + self._span_holder[LifecycleStep.OPERATION].set_attribute( + graphql_attributes.GRAPHQL_OPERATION_NAME, + self.execution_context.operation_name, + ) + + # likewise for the operation type; we'll know it for sure after parsing. + # note that this means ``self.execution_context.operation_type`` must + # be kept in sync with ``graphql_attributes.GraphqlOperationTypeValues``. + if self.execution_context.operation_type: + self._span_holder[LifecycleStep.OPERATION].set_attribute( + graphql_attributes.GRAPHQL_OPERATION_TYPE, + graphql_attributes.GraphqlOperationTypeValues( + self.execution_context.operation_type.value.lower() + ).value, + ) + self._span_holder[LifecycleStep.OPERATION].end() def on_validate(self) -> Generator[None, None, None]: @@ -139,7 +161,6 @@ def convert_list_or_tuple_to_allowed_types(self, value: Iterable) -> str: def add_tags(self, span: Span, info: GraphQLResolveInfo, kwargs: Any) -> None: graphql_path = ".".join(map(str, get_path_from_info(info))) - span.set_attribute("component", "graphql") span.set_attribute("graphql.parentType", info.parent_type.name) span.set_attribute("graphql.path", graphql_path) diff --git a/tests/schema/extensions/test_opentelemetry.py b/tests/schema/extensions/test_opentelemetry.py index 94f22ff396..40584bb258 100644 --- a/tests/schema/extensions/test_opentelemetry.py +++ b/tests/schema/extensions/test_opentelemetry.py @@ -2,6 +2,12 @@ from unittest.mock import MagicMock import pytest +from opentelemetry.semconv._incubating.attributes.graphql_attributes import ( + GRAPHQL_DOCUMENT, + GRAPHQL_OPERATION_NAME, + GRAPHQL_OPERATION_TYPE, + GraphqlOperationTypeValues, +) from opentelemetry.trace import SpanKind from pytest_mock import MockerFixture @@ -66,12 +72,14 @@ async def test_opentelemetry_sync_uses_global_tracer(global_tracer_mock): def _instrumentation_stages(mocker, query): return [ mocker.call("GraphQL Query", kind=SpanKind.SERVER), - mocker.call().set_attribute("component", "graphql"), - mocker.call().set_attribute("query", query), + mocker.call().set_attribute(GRAPHQL_DOCUMENT, query), mocker.call("GraphQL Parsing", context=mocker.ANY), mocker.call().end(), mocker.call("GraphQL Validation", context=mocker.ANY), mocker.call().end(), + mocker.call().set_attribute( + GRAPHQL_OPERATION_TYPE, GraphqlOperationTypeValues.QUERY.value + ), mocker.call().end(), ] @@ -101,7 +109,6 @@ async def test_open_tracing(global_tracer_mock, mocker): [ mocker.call("GraphQL Resolving: person", context=mocker.ANY), mocker.call().__enter__(), - mocker.call().__enter__().set_attribute("component", "graphql"), mocker.call().__enter__().set_attribute("graphql.parentType", "Query"), mocker.call().__enter__().set_attribute("graphql.path", "person"), mocker.call().__exit__(None, None, None), @@ -126,6 +133,7 @@ async def test_open_tracing_uses_operation_name(global_tracer_mock, mocker): [ # if operation_name is supplied it is added to this span's tag mocker.call("GraphQL Query: Example", kind=SpanKind.SERVER), + mocker.call().set_attribute(GRAPHQL_OPERATION_NAME, "Example"), *_instrumentation_stages(mocker, query)[1:], ] ) @@ -154,12 +162,16 @@ def generate_trace(*args: str, **kwargs: Any): await schema.execute(query) + # if operation_name is in the query, it is added to this span's name tracers[0].update_name.assert_has_calls( [ - # if operation_name is supplied it is added to this span's tag mocker.call("GraphQL Query: Example"), ] ) + # and the span's attributes + tracers[0].set_attribute.assert_has_calls( + [mocker.call(GRAPHQL_OPERATION_NAME, "Example")] + ) @pytest.mark.asyncio