Repository: open-telemetry/opentelemetry-python

Issue:
Calling trace.get_tracer before initializing tracing turns entire tracing pipeline into a noop pipeline.
Calling `trace.get_tracer()` before setting a global tracer provider effectively disables all tracing for the process.

* `opentelemetry.ap.trace.get_tracer_provider()` checks if a global tracer provider is set or not and if not, it sets the global provider to the default implementation from the api package.
* Calling `opentelemetry.trace.get_tracer()` internally calls `get_tracer_provider()` meaning if it is called before a `set_tracer_provider()` call, it ends up setting the global provider to the default tracer provider.
* This means subsequent calls to set_provider will not do anything and no spans will be exported.
* Depending on which framework the user is instrumenting or how logging is set up, this may or may not provide helpful hints.

**Steps to reproduce**

Example project: https://github.com/owais/otel-django-test

* tracing initialized here: https://github.com/owais/otel-django-test/blob/main/djtest/wsgi.py#L17
* trace.get_tracer() call ends up getting called before tracing is setup. https://github.com/owais/otel-django-test/blob/main/djtest/views.py#L6

***Steps***
* Run the project as `./manage.py runserver 8000`
* Make requests to `localhost:8000/hello`.
* No spans are printed to console.

***Fix***
* Either move `trace.get_tracer()` to inside the view or move init_tracing from wsgi.py to manage.py to fix the issue.

**Possible fixes**

1. Document this and highlight it as a caveat, and hope users don't get tripped by it.
2. Partial fix would be for `trace.set_tracer_provider()` to accept new tracer provider instances and replace the old one *if* the old one was an instance of the default tracer provider. This would be a partial fix because it would make the trace pipeline and any tracers fetched after the pipeline was setup functional but it would still render the tracers fetched before the pipeline was setup non-functional as they'd export spans to the default tracer provider.
3. Patch the default tracer to actually be a proxy to a real tracer. May be the default tracer can lazily "upgrade" itself to a real tracer if an actual tracer provider was set, otherwise continue to be a noop implementation. 



Discussion:
> * trace.get_tracer() call ends up getting called before tracing is setup. https://github.com/owais/otel-django-test/blob/main/djtest/views.py#L6

Any idea why this happens before the setup in wsgi.py? I thought that would be the first thing to run
Closing in favour of #1159

Let's continue the discussion there.

Resolving patch:
diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py
--- a/opentelemetry-api/src/opentelemetry/trace/__init__.py
+++ b/opentelemetry-api/src/opentelemetry/trace/__init__.py
@@ -74,6 +74,7 @@
 """
 
 
+import os
 from abc import ABC, abstractmethod
 from contextlib import contextmanager
 from enum import Enum
@@ -220,6 +221,21 @@ def get_tracer(
         return _DefaultTracer()
 
 
+class ProxyTracerProvider(TracerProvider):
+    def get_tracer(
+        self,
+        instrumenting_module_name: str,
+        instrumenting_library_version: str = "",
+    ) -> "Tracer":
+        if _TRACER_PROVIDER:
+            return _TRACER_PROVIDER.get_tracer(
+                instrumenting_module_name, instrumenting_library_version
+            )
+        return ProxyTracer(
+            instrumenting_module_name, instrumenting_library_version
+        )
+
+
 class Tracer(ABC):
     """Handles span creation and in-process context propagation.
 
@@ -349,6 +365,40 @@ def start_as_current_span(
         """
 
 
+class ProxyTracer(Tracer):
+    # pylint: disable=W0222,signature-differs
+    def __init__(
+        self,
+        instrumenting_module_name: str,
+        instrumenting_library_version: str,
+    ):
+        self._instrumenting_module_name = instrumenting_module_name
+        self._instrumenting_library_version = instrumenting_library_version
+        self._real_tracer: Optional[Tracer] = None
+        self._noop_tracer = _DefaultTracer()
+
+    @property
+    def _tracer(self) -> Tracer:
+        if self._real_tracer:
+            return self._real_tracer
+
+        if _TRACER_PROVIDER:
+            self._real_tracer = _TRACER_PROVIDER.get_tracer(
+                self._instrumenting_module_name,
+                self._instrumenting_library_version,
+            )
+            return self._real_tracer
+        return self._noop_tracer
+
+    def start_span(self, *args, **kwargs) -> Span:  # type: ignore
+        return self._tracer.start_span(*args, **kwargs)  # type: ignore
+
+    def start_as_current_span(  # type: ignore
+        self, *args, **kwargs
+    ) -> Span:
+        return self._tracer.start_as_current_span(*args, **kwargs)  # type: ignore
+
+
 class _DefaultTracer(Tracer):
     """The default Tracer, used when no Tracer implementation is available.
 
@@ -387,6 +437,7 @@ def start_as_current_span(
 
 
 _TRACER_PROVIDER = None
+_PROXY_TRACER_PROVIDER = None
 
 
 def get_tracer(
@@ -425,9 +476,18 @@ def set_tracer_provider(tracer_provider: TracerProvider) -> None:
 
 def get_tracer_provider() -> TracerProvider:
     """Gets the current global :class:`~.TracerProvider` object."""
-    global _TRACER_PROVIDER  # pylint: disable=global-statement
+    # pylint: disable=global-statement
+    global _TRACER_PROVIDER
+    global _PROXY_TRACER_PROVIDER
 
     if _TRACER_PROVIDER is None:
+        # if a global tracer provider has not been set either via code or env
+        # vars, return a proxy tracer provider
+        if OTEL_PYTHON_TRACER_PROVIDER not in os.environ:
+            if not _PROXY_TRACER_PROVIDER:
+                _PROXY_TRACER_PROVIDER = ProxyTracerProvider()
+            return _PROXY_TRACER_PROVIDER
+
         _TRACER_PROVIDER = cast(  # type: ignore
             "TracerProvider",
             _load_provider(OTEL_PYTHON_TRACER_PROVIDER, "tracer_provider"),


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -10,7 +10,7 @@ env:
   # Otherwise, set variable to the commit of your branch on
   # opentelemetry-python-contrib which is compatible with these Core repo
   # changes.
-  CONTRIB_REPO_SHA: fbd7e07d0b3f9be37ddfdbc8b12b35f0a8d71ede
+  CONTRIB_REPO_SHA: 1064da4bf2afaa0fb84fa10f573b211efeba72bc
 
 jobs:
   build:
diff --git a/opentelemetry-api/tests/trace/test_proxy.py b/opentelemetry-api/tests/trace/test_proxy.py
new file mode 100644
--- /dev/null
+++ b/opentelemetry-api/tests/trace/test_proxy.py
@@ -0,0 +1,72 @@
+# Copyright The OpenTelemetry Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# pylint: disable=W0212,W0222,W0221
+
+import unittest
+
+from opentelemetry import trace
+from opentelemetry.trace.span import INVALID_SPAN_CONTEXT, NonRecordingSpan
+
+
+class TestProvider(trace._DefaultTracerProvider):
+    def get_tracer(
+        self, instrumentation_module_name, instrumentaiton_library_version=None
+    ):
+        return TestTracer()
+
+
+class TestTracer(trace._DefaultTracer):
+    def start_span(self, *args, **kwargs):
+        return TestSpan(INVALID_SPAN_CONTEXT)
+
+
+class TestSpan(NonRecordingSpan):
+    pass
+
+
+class TestProxy(unittest.TestCase):
+    def test_proxy_tracer(self):
+        original_provider = trace._TRACER_PROVIDER
+
+        provider = trace.get_tracer_provider()
+        # proxy provider
+        self.assertIsInstance(provider, trace.ProxyTracerProvider)
+
+        # provider returns proxy tracer
+        tracer = provider.get_tracer("proxy-test")
+        self.assertIsInstance(tracer, trace.ProxyTracer)
+
+        with tracer.start_span("span1") as span:
+            self.assertIsInstance(span, trace.NonRecordingSpan)
+
+        with tracer.start_as_current_span("span2") as span:
+            self.assertIsInstance(span, trace.NonRecordingSpan)
+
+        # set a real provider
+        trace.set_tracer_provider(TestProvider())
+
+        # tracer provider now returns real instance
+        self.assertIsInstance(trace.get_tracer_provider(), TestProvider)
+
+        # references to the old provider still work but return real tracer now
+        real_tracer = provider.get_tracer("proxy-test")
+        self.assertIsInstance(real_tracer, TestTracer)
+
+        # reference to old proxy tracer now delegates to a real tracer and
+        # creates real spans
+        with tracer.start_span("") as span:
+            self.assertIsInstance(span, TestSpan)
+
+        trace._TRACER_PROVIDER = original_provider