diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5443bc16dbc..b0821906d18 100644 --- 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/CHANGELOG.md b/CHANGELOG.md index b22da9c9b95..d7061532b09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1721](https://github.com/open-telemetry/opentelemetry-python/pull/1721)) - Update bootstrap cmd to use exact version when installing instrumentation packages. ([#1722](https://github.com/open-telemetry/opentelemetry-python/pull/1722)) +- Added ProxyTracerProvider and ProxyTracer implementations to allow fetching provider + and tracer instances before a global provider is set up. + ([#1726](https://github.com/open-telemetry/opentelemetry-python/pull/1726)) ## [1.0.0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.0.0) - 2021-03-26 diff --git a/opentelemetry-api/src/opentelemetry/trace/__init__.py b/opentelemetry-api/src/opentelemetry/trace/__init__.py index 59ae2173049..d06a09c20a2 100644 --- 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"), diff --git a/opentelemetry-api/tests/trace/test_proxy.py b/opentelemetry-api/tests/trace/test_proxy.py new file mode 100644 index 00000000000..57759676fe3 --- /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