Repository: saleor/saleor

Issue:
OpenTracing segments are orphans instead of children of the root span
Saleor is using `start_span` instead of `start_active_span` which is resulting to having a trace for each span instead of a segment, making the tracing close to useless.

On top, we should add a test(s) using MockTracer to ensure it does not break in the future.


Discussion:

Resolving patch:
diff --git a/saleor/graphql/middleware.py b/saleor/graphql/middleware.py
--- a/saleor/graphql/middleware.py
+++ b/saleor/graphql/middleware.py
@@ -1,6 +1,7 @@
 from typing import Optional
 
-import opentracing
+import opentracing as ot
+import opentracing.tags as ot_tags
 from django.conf import settings
 from django.contrib.auth.models import AnonymousUser
 from django.utils.functional import SimpleLazyObject
@@ -46,10 +47,12 @@ def should_trace(info):
 class OpentracingGrapheneMiddleware:
     @staticmethod
     def resolve(next, root, info, **kwargs):
-        with opentracing.global_tracer().start_span(
-            operation_name=info.operation.operation
-        ) as span:
-            span.set_tag("component", "graphql")
+        operation = (
+            f"{info.operation.operation}.{info.parent_type.name}.{info.field_name}"
+        )
+        with ot.global_tracer().start_active_span(operation_name=operation) as scope:
+            span = scope.span
+            span.set_tag(ot_tags.COMPONENT, "graphql")
             span.set_tag("graphql.parent_type", info.parent_type.name)
             span.set_tag("graphql.field_name", info.field_name)
             return next(root, info, **kwargs)
diff --git a/saleor/graphql/views.py b/saleor/graphql/views.py
--- a/saleor/graphql/views.py
+++ b/saleor/graphql/views.py
@@ -3,9 +3,11 @@
 import traceback
 from typing import Any, Dict, List, Optional, Tuple, Union
 
-import opentracing
+import opentracing as ot
+import opentracing.tags as ot_tags
 from django.conf import settings
 from django.db import connection
+from django.db.backends.postgresql.base import DatabaseWrapper
 from django.http import HttpRequest, HttpResponseNotAllowed, JsonResponse
 from django.shortcuts import render_to_response
 from django.urls import reverse
@@ -29,18 +31,14 @@
 
 
 def tracing_wrapper(execute, sql, params, many, context):
-    with opentracing.global_tracer().start_span(operation_name="query") as span:
-        span.set_tag("component", "db")
-        span.set_tag("db.statement", sql)
-        try:
-            result = execute(sql, params, many, context)
-        except Exception as e:
-            span.set_tag("error", True)
-            span.set_tag("error.object", e)
-            raise
-        else:
-            span.set_tag("error", False)
-            return result
+    conn: DatabaseWrapper = context["connection"]
+    operation = f"{conn.alias} {conn.display_name}"
+    with ot.global_tracer().start_active_span(operation_name=operation) as scope:
+        span = scope.span
+        span.set_tag(ot_tags.COMPONENT, "db")
+        span.set_tag(ot_tags.DATABASE_STATEMENT, sql)
+        span.set_tag(ot_tags.DATABASE_TYPE, conn.display_name)
+        return execute(sql, params, many, context)
 
 
 class GraphQLView(View):
@@ -80,7 +78,7 @@ def __init__(
     def dispatch(self, request, *args, **kwargs):
         # Handle options method the GraphQlView restricts it.
         if request.method == "GET":
-            if settings.DEBUG:
+            if settings.PLAYGROUND_ENABLED:
                 return render_to_response("graphql/playground.html")
             return HttpResponseNotAllowed(["OPTIONS", "POST"])
 
@@ -120,10 +118,12 @@ def handle_query(self, request: HttpRequest):
     def get_response(
         self, request: HttpRequest, data: dict
     ) -> Tuple[Optional[Dict[str, List[Any]]], int]:
-        with opentracing.global_tracer().start_span(operation_name="request") as span:
-            span.set_tag("component", "http")
-            span.set_tag("http.method", request.method)
-            span.set_tag("http.path", request.path)
+        with ot.global_tracer().start_active_span(operation_name="request") as scope:
+            span = scope.span
+            span.set_tag(ot_tags.COMPONENT, "http")
+            span.set_tag(ot_tags.HTTP_METHOD, request.method)
+            span.set_tag(ot_tags.HTTP_URL, request.get_full_path())
+
             execution_result = self.execute_graphql_request(request, data)
             status_code = 200
             if execution_result:
@@ -139,7 +139,8 @@ def get_response(
                 result: Optional[Dict[str, List[Any]]] = response
             else:
                 result = None
-            span.set_tag("http.status_code", status_code)
+
+            span.set_tag(ot_tags.HTTP_STATUS_CODE, status_code)
             return result, status_code
 
     def get_root_value(self):
diff --git a/saleor/settings.py b/saleor/settings.py
--- a/saleor/settings.py
+++ b/saleor/settings.py
@@ -346,9 +346,7 @@ def get_bool_from_env(name, default_value):
 # note: having multiple currencies is not supported yet
 AVAILABLE_CURRENCIES = [DEFAULT_CURRENCY]
 
-COUNTRIES_OVERRIDE = {
-    "EU": "European Union",
-}
+COUNTRIES_OVERRIDE = {"EU": "European Union"}
 
 OPENEXCHANGERATES_API_KEY = os.environ.get("OPENEXCHANGERATES_API_KEY")
 
@@ -391,6 +389,8 @@ def get_host():
 
 TEST_RUNNER = "tests.runner.PytestTestRunner"
 
+PLAYGROUND_ENABLED = get_bool_from_env("PLAYGROUND_ENABLED", True)
+
 ALLOWED_HOSTS = get_list(os.environ.get("ALLOWED_HOSTS", "localhost,127.0.0.1"))
 ALLOWED_GRAPHQL_ORIGINS = os.environ.get("ALLOWED_GRAPHQL_ORIGINS", "*")
 


Test patch:
diff --git a/tests/api/test_view.py b/tests/api/test_view.py
--- a/tests/api/test_view.py
+++ b/tests/api/test_view.py
@@ -52,16 +52,11 @@ def test_batch_queries(category, product, api_client):
     assert data["category"]["name"] == category.name
 
 
-def test_graphql_view_get_in_non_debug_mode(client):
+@pytest.mark.parametrize("playground_on, status", [(True, 200), (False, 405)])
+def test_graphql_view_get_enabled_or_disabled(client, settings, playground_on, status):
+    settings.PLAYGROUND_ENABLED = playground_on
     response = client.get(API_PATH)
-    assert response.status_code == 405
-
-
-@override_settings(DEBUG=True)
-def test_graphql_view_get_in_debug_mode(client):
-    response = client.get(API_PATH)
-    assert response.status_code == 200
-    assert response.templates[0].name == "graphql/playground.html"
+    assert response.status_code == status
 
 
 def test_graphql_view_options(client):