Repository: open-telemetry/opentelemetry-python-contrib

Issue:
Falcon: Conditionally create SERVER spans
Part of #445 


Discussion:
@ashu658  ^
@owais Can you assign this issue to me?

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
@@ -107,10 +107,10 @@ def response_hook(span, req, resp):
     get_global_response_propagator,
 )
 from opentelemetry.instrumentation.utils import (
+    _start_internal_or_server_span,
     extract_attributes_from_object,
     http_status_to_status_code,
 )
-from opentelemetry.propagate import extract
 from opentelemetry.semconv.trace import SpanAttributes
 from opentelemetry.trace.status import Status
 from opentelemetry.util._time import _time_ns
@@ -195,12 +195,14 @@ def __call__(self, env, start_response):
 
         start_time = _time_ns()
 
-        token = context.attach(extract(env, getter=otel_wsgi.wsgi_getter))
-        span = self._tracer.start_span(
-            otel_wsgi.get_default_span_name(env),
-            kind=trace.SpanKind.SERVER,
+        span, token = _start_internal_or_server_span(
+            tracer=self._tracer,
+            span_name=otel_wsgi.get_default_span_name(env),
             start_time=start_time,
+            context_carrier=env,
+            context_getter=otel_wsgi.wsgi_getter,
         )
+
         if span.is_recording():
             attributes = otel_wsgi.collect_request_attributes(env)
             for key, value in attributes.items():
@@ -216,7 +218,8 @@ def _start_response(status, response_headers, *args, **kwargs):
                 status, response_headers, *args, **kwargs
             )
             activation.__exit__(None, None, None)
-            context.detach(token)
+            if token is not None:
+                context.detach(token)
             return response
 
         try:
@@ -227,7 +230,8 @@ def _start_response(status, response_headers, *args, **kwargs):
                 exc,
                 getattr(exc, "__traceback__", None),
             )
-            context.detach(token)
+            if token is not None:
+                context.detach(token)
             raise
 
 
diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/utils.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/utils.py
--- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/utils.py
+++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/utils.py
@@ -16,9 +16,12 @@
 
 from wrapt import ObjectProxy
 
+from opentelemetry import context, trace
+
 # pylint: disable=unused-import
 # pylint: disable=E0611
 from opentelemetry.context import _SUPPRESS_INSTRUMENTATION_KEY  # noqa: F401
+from opentelemetry.propagate import extract
 from opentelemetry.trace import StatusCode
 
 
@@ -67,3 +70,39 @@ def unwrap(obj, attr: str):
     func = getattr(obj, attr, None)
     if func and isinstance(func, ObjectProxy) and hasattr(func, "__wrapped__"):
         setattr(obj, attr, func.__wrapped__)
+
+
+def _start_internal_or_server_span(
+    tracer, span_name, start_time, context_carrier, context_getter
+):
+    """Returns internal or server span along with the token which can be used by caller to reset context
+
+
+    Args:
+        tracer : tracer in use by given instrumentation library
+        name (string): name of the span
+        start_time : start time of the span
+        context_carrier : object which contains values that are
+            used to construct a Context. This object
+            must be paired with an appropriate getter
+            which understands how to extract a value from it.
+        context_getter : an object which contains a get function that can retrieve zero
+            or more values from the carrier and a keys function that can get all the keys
+            from carrier.
+    """
+
+    token = ctx = span_kind = None
+    if trace.get_current_span() is trace.INVALID_SPAN:
+        ctx = extract(context_carrier, getter=context_getter)
+        token = context.attach(ctx)
+        span_kind = trace.SpanKind.SERVER
+    else:
+        ctx = context.get_current()
+        span_kind = trace.SpanKind.INTERNAL
+    span = tracer.start_span(
+        name=span_name,
+        context=ctx,
+        kind=span_kind,
+        start_time=start_time,
+    )
+    return span, token


Test patch:
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
@@ -16,6 +16,7 @@
 
 from falcon import testing
 
+from opentelemetry import trace
 from opentelemetry.instrumentation.falcon import FalconInstrumentor
 from opentelemetry.instrumentation.propagators import (
     TraceResponsePropagator,
@@ -264,3 +265,18 @@ def test_hooks(self):
         self.assertEqual(
             span.attributes["request_hook_attr"], "value from hook"
         )
+
+
+class TestFalconInstrumentationWrappedWithOtherFramework(TestFalconBase):
+    def test_mark_span_internal_in_presence_of_span_from_other_framework(self):
+        tracer = trace.get_tracer(__name__)
+        with tracer.start_as_current_span(
+            "test", kind=trace.SpanKind.SERVER
+        ) as parent_span:
+            self.client().simulate_request(method="GET", path="/hello")
+            span = self.memory_exporter.get_finished_spans()[0]
+            assert span.status.is_ok
+            self.assertEqual(trace.SpanKind.INTERNAL, span.kind)
+            self.assertEqual(
+                span.parent.span_id, parent_span.get_span_context().span_id
+            )