Repository: open-telemetry/opentelemetry-python-contrib

Issue:
Improve Falcon OpenTelemetry Instrumentation for Hug Web Framework
**Is your feature request related to a problem?**
We are extensively using the [Hug](https://hugapi.github.io/hug/) web framework in our organization. Since Hug is built on top of Falcon, we have been using the `falcon-instrumentation` library to instrument our applications. However, this approach results in spans that are difficult to interpret, as shown in the screenshot below: 

<img width="536" alt="Screen Shot 2023-03-22 at 11 03 16" src="https://user-images.githubusercontent.com/83533845/226852876-6456db91-e9fc-481d-9fc9-c195e2c49d39.png">

**Describe the solution you'd like**
We believe that better instrumentation support for Hug can be achieved through one of the following options:
- Develop a dedicated `hug-instrumentation` library, which would be largely similar (about 90%) to the existing falcon-instrumentation .
- Create a `hug-instrumentation` library that depends on falcon-instrumentation and overrides some functions. However, this solution might be less maintainable and less readable.
- Introduce a configuration option in the `falcon-instrumentation` library that enhances its compatibility with Hug.

**Additional context**
I have not personally tried the Falcon framework to see how spans are named there, but I assume they are similar to those of other frameworks. I would appreciate any input from project contributors on this matter and am willing to explore the issue further. What are your thoughts on the best approach to improve OpenTelemetry instrumentation for the Hug web framework?



Discussion:

Resolving patch:
diff --git a/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/__init__.py b/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/__init__.py
--- a/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/__init__.py
+++ b/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/__init__.py
@@ -428,7 +428,6 @@ def process_resource(self, req, resp, resource, params):
 
         resource_name = resource.__class__.__name__
         span.set_attribute("falcon.resource", resource_name)
-        span.update_name(f"{resource_name}.on_{req.method.lower()}")
 
     def process_response(
         self, req, resp, resource, req_succeeded=None
@@ -483,6 +482,12 @@ def process_response(
                 response_headers = resp.headers
 
             if span.is_recording() and span.kind == trace.SpanKind.SERVER:
+                # Check if low-cardinality route is available as per semantic-conventions
+                if req.uri_template:
+                    span.update_name(f"{req.method} {req.uri_template}")
+                else:
+                    span.update_name(f"{req.method}")
+
                 custom_attributes = (
                     otel_wsgi.collect_custom_response_headers_attributes(
                         response_headers.items()


Test patch:
diff --git a/instrumentation/opentelemetry-instrumentation-falcon/tests/app.py b/instrumentation/opentelemetry-instrumentation-falcon/tests/app.py
--- a/instrumentation/opentelemetry-instrumentation-falcon/tests/app.py
+++ b/instrumentation/opentelemetry-instrumentation-falcon/tests/app.py
@@ -61,6 +61,13 @@ def on_get(self, _, resp):
         resp.set_header("my-secret-header", "my-secret-value")
 
 
+class UserResource:
+    def on_get(self, req, resp, user_id):
+        # pylint: disable=no-member
+        resp.status = falcon.HTTP_200
+        resp.body = f"Hello user {user_id}"
+
+
 def make_app():
     _parsed_falcon_version = package_version.parse(falcon.__version__)
     if _parsed_falcon_version < package_version.parse("3.0.0"):
@@ -76,4 +83,6 @@ def make_app():
     app.add_route(
         "/test_custom_response_headers", CustomResponseHeaderResource()
     )
+    app.add_route("/user/{user_id}", UserResource())
+
     return app
diff --git a/instrumentation/opentelemetry-instrumentation-falcon/tests/test_falcon.py b/instrumentation/opentelemetry-instrumentation-falcon/tests/test_falcon.py
--- a/instrumentation/opentelemetry-instrumentation-falcon/tests/test_falcon.py
+++ b/instrumentation/opentelemetry-instrumentation-falcon/tests/test_falcon.py
@@ -110,7 +110,7 @@ def _test_method(self, method):
         spans = self.memory_exporter.get_finished_spans()
         self.assertEqual(len(spans), 1)
         span = spans[0]
-        self.assertEqual(span.name, f"HelloWorldResource.on_{method.lower()}")
+        self.assertEqual(span.name, f"{method} /hello")
         self.assertEqual(span.status.status_code, StatusCode.UNSET)
         self.assertEqual(
             span.status.description,
@@ -145,7 +145,7 @@ def test_404(self):
         spans = self.memory_exporter.get_finished_spans()
         self.assertEqual(len(spans), 1)
         span = spans[0]
-        self.assertEqual(span.name, "GET /does-not-exist")
+        self.assertEqual(span.name, "GET")
         self.assertEqual(span.status.status_code, StatusCode.UNSET)
         self.assertSpanHasAttributes(
             span,
@@ -177,7 +177,7 @@ def test_500(self):
         spans = self.memory_exporter.get_finished_spans()
         self.assertEqual(len(spans), 1)
         span = spans[0]
-        self.assertEqual(span.name, "ErrorResource.on_get")
+        self.assertEqual(span.name, "GET /error")
         self.assertFalse(span.status.is_ok)
         self.assertEqual(span.status.status_code, StatusCode.ERROR)
         self.assertEqual(
@@ -206,6 +206,33 @@ def test_500(self):
                 span.attributes[SpanAttributes.NET_PEER_IP], "127.0.0.1"
             )
 
+    def test_url_template(self):
+        self.client().simulate_get("/user/123")
+        spans = self.memory_exporter.get_finished_spans()
+        self.assertEqual(len(spans), 1)
+        span = spans[0]
+        self.assertEqual(span.name, "GET /user/{user_id}")
+        self.assertEqual(span.status.status_code, StatusCode.UNSET)
+        self.assertEqual(
+            span.status.description,
+            None,
+        )
+        self.assertSpanHasAttributes(
+            span,
+            {
+                SpanAttributes.HTTP_METHOD: "GET",
+                SpanAttributes.HTTP_SERVER_NAME: "falconframework.org",
+                SpanAttributes.HTTP_SCHEME: "http",
+                SpanAttributes.NET_HOST_PORT: 80,
+                SpanAttributes.HTTP_HOST: "falconframework.org",
+                SpanAttributes.HTTP_TARGET: "/",
+                SpanAttributes.NET_PEER_PORT: "65133",
+                SpanAttributes.HTTP_FLAVOR: "1.1",
+                "falcon.resource": "UserResource",
+                SpanAttributes.HTTP_STATUS_CODE: 200,
+            },
+        )
+
     def test_uninstrument(self):
         self.client().simulate_get(path="/hello")
         spans = self.memory_exporter.get_finished_spans()