diff --git a/CHANGELOG.md b/CHANGELOG.md index b0683890a0..769365cdaa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#817](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/817)) - `opentelemetry-instrumentation-kafka-python` added kafka-python module instrumentation. ([#814](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/814)) - +- `opentelemetry-instrumentation-falcon` Falcon: Conditionally create SERVER spans + ([#867](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/867)) ### Fixed - `opentelemetry-instrumentation-django` Django: Conditionally create SERVER spans diff --git a/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/__init__.py b/instrumentation/opentelemetry-instrumentation-falcon/src/opentelemetry/instrumentation/falcon/__init__.py index b079d9a656..4c19f9a1d7 100644 --- 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/instrumentation/opentelemetry-instrumentation-falcon/tests/test_falcon.py b/instrumentation/opentelemetry-instrumentation-falcon/tests/test_falcon.py index f971ec68a0..c178a696ca 100644 --- 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 + ) diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/utils.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/utils.py index f5a470d707..100170bf7b 100644 --- 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