Repository: open-telemetry/opentelemetry-python

Issue:
WIP: Low cardinality span names + callback for WSGI
This PR fixes #409, and has an ASGI counterpart here: https://github.com/open-telemetry/opentelemetry-python/pull/402/commits/ede28b2cc42551cb57c015f3af0d153efbdf814d


Discussion:

Resolving patch:
diff --git a/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py b/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py
--- a/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py
+++ b/ext/opentelemetry-ext-wsgi/src/opentelemetry/ext/wsgi/__init__.py
@@ -93,12 +93,15 @@ def collect_request_attributes(environ):
 
     result = {
         "component": "http",
-        "http.method": environ["REQUEST_METHOD"],
-        "http.server_name": environ["SERVER_NAME"],
-        "http.scheme": environ["wsgi.url_scheme"],
-        "host.port": int(environ["SERVER_PORT"]),
+        "http.method": environ.get("REQUEST_METHOD"),
+        "http.server_name": environ.get("SERVER_NAME"),
+        "http.scheme": environ.get("wsgi.url_scheme"),
     }
 
+    host_port = environ.get("SERVER_PORT")
+    if host_port is not None:
+        result.update({"host.port": int(host_port)})
+
     setifnotnone(result, "http.host", environ.get("HTTP_HOST"))
     target = environ.get("RAW_URI")
     if target is None:  # Note: `"" or None is None`
@@ -149,12 +152,8 @@ def add_response_attributes(
 
 
 def get_default_span_name(environ):
-    """Calculates a (generic) span name for an incoming HTTP request based on the PEP3333 conforming WSGI environ."""
-
-    # TODO: Update once
-    #  https://github.com/open-telemetry/opentelemetry-specification/issues/270
-    #  is resolved
-    return environ.get("PATH_INFO", "/")
+    """Default implementation for name_callback, returns HTTP {METHOD_NAME}."""
+    return "HTTP {}".format(environ.get("REQUEST_METHOD", "")).strip()
 
 
 class OpenTelemetryMiddleware:
@@ -165,11 +164,15 @@ class OpenTelemetryMiddleware:
 
     Args:
         wsgi: The WSGI application callable to forward requests to.
+        name_callback: Callback which calculates a generic span name for an
+                       incoming HTTP request based on the PEP3333 WSGI environ.
+                       Optional: Defaults to get_default_span_name.
     """
 
-    def __init__(self, wsgi):
+    def __init__(self, wsgi, name_callback=get_default_span_name):
         self.wsgi = wsgi
         self.tracer = trace.get_tracer(__name__, __version__)
+        self.name_callback = name_callback
 
     @staticmethod
     def _create_start_response(span, start_response):
@@ -191,7 +194,7 @@ def __call__(self, environ, start_response):
         token = context.attach(
             propagators.extract(get_header_from_environ, environ)
         )
-        span_name = get_default_span_name(environ)
+        span_name = self.name_callback(environ)
 
         span = self.tracer.start_span(
             span_name,


Test patch:
diff --git a/ext/opentelemetry-ext-flask/tests/test_programmatic.py b/ext/opentelemetry-ext-flask/tests/test_programmatic.py
--- a/ext/opentelemetry-ext-flask/tests/test_programmatic.py
+++ b/ext/opentelemetry-ext-flask/tests/test_programmatic.py
@@ -118,7 +118,7 @@ def test_404(self):
         resp.close()
         span_list = self.memory_exporter.get_finished_spans()
         self.assertEqual(len(span_list), 1)
-        self.assertEqual(span_list[0].name, "/bye")
+        self.assertEqual(span_list[0].name, "HTTP POST")
         self.assertEqual(span_list[0].kind, trace.SpanKind.SERVER)
         self.assertEqual(span_list[0].attributes, expected_attrs)
 
diff --git a/ext/opentelemetry-ext-pyramid/tests/test_programmatic.py b/ext/opentelemetry-ext-pyramid/tests/test_programmatic.py
--- a/ext/opentelemetry-ext-pyramid/tests/test_programmatic.py
+++ b/ext/opentelemetry-ext-pyramid/tests/test_programmatic.py
@@ -101,7 +101,7 @@ def test_404(self):
         resp.close()
         span_list = self.memory_exporter.get_finished_spans()
         self.assertEqual(len(span_list), 1)
-        self.assertEqual(span_list[0].name, "/bye")
+        self.assertEqual(span_list[0].name, "HTTP POST")
         self.assertEqual(span_list[0].kind, trace.SpanKind.SERVER)
         self.assertEqual(span_list[0].attributes, expected_attrs)
 
diff --git a/ext/opentelemetry-ext-wsgi/tests/test_wsgi_middleware.py b/ext/opentelemetry-ext-wsgi/tests/test_wsgi_middleware.py
--- a/ext/opentelemetry-ext-wsgi/tests/test_wsgi_middleware.py
+++ b/ext/opentelemetry-ext-wsgi/tests/test_wsgi_middleware.py
@@ -74,7 +74,9 @@ def error_wsgi(environ, start_response):
 
 
 class TestWsgiApplication(WsgiTestBase):
-    def validate_response(self, response, error=None):
+    def validate_response(
+        self, response, error=None, span_name="HTTP GET", http_method="GET"
+    ):
         while True:
             try:
                 value = next(response)
@@ -95,23 +97,22 @@ def validate_response(self, response, error=None):
 
         span_list = self.memory_exporter.get_finished_spans()
         self.assertEqual(len(span_list), 1)
-        self.assertEqual(span_list[0].name, "/")
+        self.assertEqual(span_list[0].name, span_name)
         self.assertEqual(span_list[0].kind, trace_api.SpanKind.SERVER)
-        self.assertEqual(
-            span_list[0].attributes,
-            {
-                "component": "http",
-                "http.method": "GET",
-                "http.server_name": "127.0.0.1",
-                "http.scheme": "http",
-                "host.port": 80,
-                "http.host": "127.0.0.1",
-                "http.flavor": "1.0",
-                "http.url": "http://127.0.0.1/",
-                "http.status_text": "OK",
-                "http.status_code": 200,
-            },
-        )
+        expected_attributes = {
+            "component": "http",
+            "http.server_name": "127.0.0.1",
+            "http.scheme": "http",
+            "host.port": 80,
+            "http.host": "127.0.0.1",
+            "http.flavor": "1.0",
+            "http.url": "http://127.0.0.1/",
+            "http.status_text": "OK",
+            "http.status_code": 200,
+        }
+        if http_method is not None:
+            expected_attributes["http.method"] = http_method
+        self.assertEqual(span_list[0].attributes, expected_attributes)
 
     def test_basic_wsgi_call(self):
         app = otel_wsgi.OpenTelemetryMiddleware(simple_wsgi)
@@ -147,6 +148,27 @@ def test_wsgi_exc_info(self):
         response = app(self.environ, self.start_response)
         self.validate_response(response, error=ValueError)
 
+    def test_override_span_name(self):
+        """Test that span_names can be overwritten by our callback function."""
+        span_name = "Dymaxion"
+
+        def get_predefined_span_name(scope):
+            # pylint: disable=unused-argument
+            return span_name
+
+        app = otel_wsgi.OpenTelemetryMiddleware(
+            simple_wsgi, name_callback=get_predefined_span_name
+        )
+        response = app(self.environ, self.start_response)
+        self.validate_response(response, span_name=span_name)
+
+    def test_default_span_name_missing_request_method(self):
+        """Test that default span_names with missing request method."""
+        self.environ.pop("REQUEST_METHOD")
+        app = otel_wsgi.OpenTelemetryMiddleware(simple_wsgi)
+        response = app(self.environ, self.start_response)
+        self.validate_response(response, span_name="HTTP", http_method=None)
+
 
 class TestWsgiAttributes(unittest.TestCase):
     def setUp(self):