Repository: saleor/saleor

Issue:
Add support for OpenTracing
### What I'm trying to achieve
[OpenTracing](https://opentracing.io/) is a vendor-neutral way of instrumenting applications for APM. I would like to be able to use a compatible tracer (like Datadog or Jaeger) to track the performance of Saleor.


### Describe a proposed solution
It seems we could use either https://github.com/opentracing/opentracing-python or https://github.com/opentracing-contrib/python-django to trace:

* Django requests (using a middleware)
* database queries (by either [patching psycopg](https://github.com/DataDog/dd-trace-py/tree/master/ddtrace/contrib/psycopg) or [patching Django](https://github.com/DataDog/dd-trace-py/tree/master/ddtrace/contrib/django)
* Graphene resolvers (using a Graphene middleware, proof of concept below)

### Other solutions I've tried and won't work
Datadog provides a `ddtrace-run` tool that can automatically instrument libraries like Django it does not support Graphene so it's not a good solution for headless deployments.

### Screenshots or mockups
```
import opentracing

class TracingMiddleware:
    @staticmethod
    def resolve(next, root, info, **kwargs):
        span = tracer.start_span(operation_name=info.field_name)
        with opentracing.global_tracer().start_span(operation_name=info.field_name) as span:
            span.set_tag('parent-type', info.parent_type.name)
            return next(root, info, **kwargs)
```


Discussion:
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

Also, Opentracing is supported in Ariadne, maybe this implementation could also help us somehow:
https://github.com/mirumee/ariadne/blob/master/ariadne/contrib/tracing/opentracing.py

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,5 +1,6 @@
 from typing import Optional
 
+import opentracing
 from django.conf import settings
 from django.contrib.auth.models import AnonymousUser
 from django.utils.functional import SimpleLazyObject
@@ -35,6 +36,25 @@ def _jwt_middleware(request):
     return _jwt_middleware
 
 
+def should_trace(info):
+    if info.field_name not in info.parent_type.fields:
+        return False
+
+    return not info.field_name.startswith("__")
+
+
+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")
+            span.set_tag("graphql.parent_type", info.parent_type.name)
+            span.set_tag("graphql.field_name", info.field_name)
+            return next(root, info, **kwargs)
+
+
 def get_service_account(auth_token) -> Optional[ServiceAccount]:
     qs = ServiceAccount.objects.filter(tokens__auth_token=auth_token, is_active=True)
     return qs.first()
diff --git a/saleor/graphql/views.py b/saleor/graphql/views.py
--- a/saleor/graphql/views.py
+++ b/saleor/graphql/views.py
@@ -3,7 +3,9 @@
 import traceback
 from typing import Any, Dict, List, Optional, Tuple, Union
 
+import opentracing
 from django.conf import settings
+from django.db import connection
 from django.http import HttpRequest, HttpResponseNotAllowed, JsonResponse
 from django.shortcuts import render_to_response
 from django.urls import reverse
@@ -26,6 +28,21 @@
 handled_errors_logger = logging.getLogger("saleor.graphql.errors.handled")
 
 
+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
+
+
 class GraphQLView(View):
     # This class is our implementation of `graphene_django.views.GraphQLView`,
     # which was extended to support the following features:
@@ -103,22 +120,27 @@ def handle_query(self, request: HttpRequest):
     def get_response(
         self, request: HttpRequest, data: dict
     ) -> Tuple[Optional[Dict[str, List[Any]]], int]:
-        execution_result = self.execute_graphql_request(request, data)
-        status_code = 200
-        if execution_result:
-            response = {}
-            if execution_result.errors:
-                response["errors"] = [
-                    self.format_error(e) for e in execution_result.errors
-                ]
-            if execution_result.invalid:
-                status_code = 400
+        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)
+            execution_result = self.execute_graphql_request(request, data)
+            status_code = 200
+            if execution_result:
+                response = {}
+                if execution_result.errors:
+                    response["errors"] = [
+                        self.format_error(e) for e in execution_result.errors
+                    ]
+                if execution_result.invalid:
+                    status_code = 400
+                else:
+                    response["data"] = execution_result.data
+                result: Optional[Dict[str, List[Any]]] = response
             else:
-                response["data"] = execution_result.data
-            result: Optional[Dict[str, List[Any]]] = response
-        else:
-            result = None
-        return result, status_code
+                result = None
+            span.set_tag("http.status_code", status_code)
+            return result, status_code
 
     def get_root_value(self):
         return self.root_value
@@ -164,14 +186,15 @@ def execute_graphql_request(self, request: HttpRequest, data: dict):
             # executor is not a valid argument in all backends
             extra_options["executor"] = self.executor
         try:
-            return document.execute(  # type: ignore
-                root=self.get_root_value(),
-                variables=variables,
-                operation_name=operation_name,
-                context=request,
-                middleware=self.middleware,
-                **extra_options,
-            )
+            with connection.execute_wrapper(tracing_wrapper):
+                return document.execute(  # type: ignore
+                    root=self.get_root_value(),
+                    variables=variables,
+                    operation_name=operation_name,
+                    context=request,
+                    middleware=self.middleware,
+                    **extra_options,
+                )
         except Exception as e:
             return ExecutionResult(errors=[e], invalid=True)
 
diff --git a/saleor/settings.py b/saleor/settings.py
--- a/saleor/settings.py
+++ b/saleor/settings.py
@@ -523,6 +523,7 @@ def get_host():
     sentry_sdk.init(dsn=SENTRY_DSN, integrations=[DjangoIntegration()])
 
 GRAPHENE = {
+    "MIDDLEWARE": ("saleor.graphql.middleware.OpentracingGrapheneMiddleware",),
     "RELAY_CONNECTION_ENFORCE_FIRST_OR_LAST": True,
     "RELAY_CONNECTION_MAX_LIMIT": 100,
 }


Test patch: