diff --git a/README.md b/README.md index 684680c0fb..fcedf05e02 100644 --- a/README.md +++ b/README.md @@ -52,15 +52,15 @@ pip install -e ./ext/opentelemetry-ext-{integration} ```python from opentelemetry import trace from opentelemetry.context import Context -from opentelemetry.sdk.trace import Tracer +from opentelemetry.sdk.trace import TracerSource from opentelemetry.sdk.trace.export import ConsoleSpanExporter from opentelemetry.sdk.trace.export import SimpleExportSpanProcessor -trace.set_preferred_tracer_implementation(lambda T: Tracer()) -tracer = trace.tracer() -tracer.add_span_processor( +trace.set_preferred_tracer_source_implementation(lambda T: TracerSource()) +trace.tracer_source().add_span_processor( SimpleExportSpanProcessor(ConsoleSpanExporter()) ) +tracer = trace.tracer_source().get_tracer(__name__) with tracer.start_as_current_span('foo'): with tracer.start_as_current_span('bar'): with tracer.start_as_current_span('baz'): diff --git a/examples/basic_tracer/tracer.py b/examples/basic_tracer/tracer.py index 69ee0a1602..4b392fd1ea 100755 --- a/examples/basic_tracer/tracer.py +++ b/examples/basic_tracer/tracer.py @@ -18,7 +18,7 @@ from opentelemetry import trace from opentelemetry.context import Context -from opentelemetry.sdk.trace import Tracer +from opentelemetry.sdk.trace import TracerSource from opentelemetry.sdk.trace.export import ( BatchExportSpanProcessor, ConsoleSpanExporter, @@ -37,13 +37,17 @@ # The preferred tracer implementation must be set, as the opentelemetry-api # defines the interface with a no-op implementation. -trace.set_preferred_tracer_implementation(lambda T: Tracer()) -tracer = trace.tracer() +trace.set_preferred_tracer_source_implementation(lambda T: TracerSource()) + +# We tell OpenTelemetry who it is that is creating spans. In this case, we have +# no real name (no setup.py), so we make one up. If we had a version, we would +# also specify it here. +tracer = trace.tracer_source().get_tracer(__name__) # SpanExporter receives the spans and send them to the target location. span_processor = BatchExportSpanProcessor(exporter) -tracer.add_span_processor(span_processor) +trace.tracer_source().add_span_processor(span_processor) with tracer.start_as_current_span("foo"): with tracer.start_as_current_span("bar"): with tracer.start_as_current_span("baz"): diff --git a/examples/http/server.py b/examples/http/server.py index 63973da74d..68e3d952b0 100755 --- a/examples/http/server.py +++ b/examples/http/server.py @@ -22,7 +22,7 @@ from opentelemetry import trace from opentelemetry.ext import http_requests from opentelemetry.ext.wsgi import OpenTelemetryMiddleware -from opentelemetry.sdk.trace import Tracer +from opentelemetry.sdk.trace import TracerSource from opentelemetry.sdk.trace.export import ( BatchExportSpanProcessor, ConsoleSpanExporter, @@ -41,17 +41,17 @@ # The preferred tracer implementation must be set, as the opentelemetry-api # defines the interface with a no-op implementation. -trace.set_preferred_tracer_implementation(lambda T: Tracer()) -tracer = trace.tracer() +trace.set_preferred_tracer_source_implementation(lambda T: TracerSource()) +tracer = trace.tracer_source().get_tracer(__name__) # SpanExporter receives the spans and send them to the target location. span_processor = BatchExportSpanProcessor(exporter) -tracer.add_span_processor(span_processor) +trace.tracer_source().add_span_processor(span_processor) # Integrations are the glue that binds the OpenTelemetry API and the # frameworks and libraries that are used together, automatically creating # Spans and propagating context as appropriate. -http_requests.enable(tracer) +http_requests.enable(trace.tracer_source()) app = flask.Flask(__name__) app.wsgi_app = OpenTelemetryMiddleware(app.wsgi_app) diff --git a/examples/http/tracer_client.py b/examples/http/tracer_client.py index dde25b9bb3..746608db3b 100755 --- a/examples/http/tracer_client.py +++ b/examples/http/tracer_client.py @@ -20,7 +20,7 @@ from opentelemetry import trace from opentelemetry.ext import http_requests -from opentelemetry.sdk.trace import Tracer +from opentelemetry.sdk.trace import TracerSource from opentelemetry.sdk.trace.export import ( BatchExportSpanProcessor, ConsoleSpanExporter, @@ -39,15 +39,15 @@ # The preferred tracer implementation must be set, as the opentelemetry-api # defines the interface with a no-op implementation. -trace.set_preferred_tracer_implementation(lambda T: Tracer()) -tracer = trace.tracer() +trace.set_preferred_tracer_source_implementation(lambda T: TracerSource()) +tracer_source = trace.tracer_source() # SpanExporter receives the spans and send them to the target location. span_processor = BatchExportSpanProcessor(exporter) -tracer.add_span_processor(span_processor) +tracer_source.add_span_processor(span_processor) # Integrations are the glue that binds the OpenTelemetry API and the # frameworks and libraries that are used together, automatically creating # Spans and propagating context as appropriate. -http_requests.enable(tracer) +http_requests.enable(tracer_source) response = requests.get(url="http://127.0.0.1:5000/") diff --git a/examples/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py b/examples/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py index 85df625efe..ae484dd30e 100644 --- a/examples/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py +++ b/examples/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py @@ -17,13 +17,14 @@ the requests library to perform downstream requests """ import flask +import pkg_resources import requests import opentelemetry.ext.http_requests from opentelemetry import propagators, trace from opentelemetry.ext.flask import instrument_app from opentelemetry.sdk.context.propagation.b3_format import B3Format -from opentelemetry.sdk.trace import Tracer +from opentelemetry.sdk.trace import TracerSource def configure_opentelemetry(flask_app: flask.Flask): @@ -45,7 +46,7 @@ def configure_opentelemetry(flask_app: flask.Flask): # the preferred implementation of these objects must be set, # as the opentelemetry-api defines the interface with a no-op # implementation. - trace.set_preferred_tracer_implementation(lambda _: Tracer()) + trace.set_preferred_tracer_source_implementation(lambda _: TracerSource()) # Next, we need to configure how the values that are used by # traces and metrics are propagated (such as what specific headers # carry this value). @@ -56,7 +57,7 @@ def configure_opentelemetry(flask_app: flask.Flask): # Integrations are the glue that binds the OpenTelemetry API # and the frameworks and libraries that are used together, automatically # creating Spans and propagating context as appropriate. - opentelemetry.ext.http_requests.enable(trace.tracer()) + opentelemetry.ext.http_requests.enable(trace.tracer_source()) instrument_app(flask_app) @@ -67,7 +68,11 @@ def configure_opentelemetry(flask_app: flask.Flask): def hello(): # emit a trace that measures how long the # sleep takes - with trace.tracer().start_as_current_span("example-request"): + version = pkg_resources.get_distribution( + "opentelemetry-example-app" + ).version + tracer = trace.tracer_source().get_tracer(__name__, version) + with tracer.start_as_current_span("example-request"): requests.get("http://www.example.com") return "hello" diff --git a/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py b/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py index cce038ccb5..ce11b18d63 100644 --- a/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py +++ b/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py @@ -7,6 +7,7 @@ import opentelemetry.ext.wsgi as otel_wsgi from opentelemetry import propagators, trace +from opentelemetry.ext.flask.version import __version__ from opentelemetry.util import time_ns logger = logging.getLogger(__name__) @@ -60,7 +61,7 @@ def _before_flask_request(): otel_wsgi.get_header_from_environ, environ ) - tracer = trace.tracer() + tracer = trace.tracer_source().get_tracer(__name__, __version__) attributes = otel_wsgi.collect_request_attributes(environ) if flask_request.url_rule: diff --git a/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/__init__.py b/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/__init__.py index f05202c055..4f5a18cf9e 100644 --- a/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/__init__.py +++ b/ext/opentelemetry-ext-http-requests/src/opentelemetry/ext/http_requests/__init__.py @@ -24,6 +24,7 @@ from opentelemetry import propagators from opentelemetry.context import Context +from opentelemetry.ext.http_requests.version import __version__ from opentelemetry.trace import SpanKind @@ -32,7 +33,7 @@ # if the SDK/tracer is already using `requests` they may, in theory, bypass our # instrumentation when using `import from`, etc. (currently we only instrument # a instance method so the probability for that is very low). -def enable(tracer): +def enable(tracer_source): """Enables tracing of all requests calls that go through :code:`requests.session.Session.request` (this includes :code:`requests.get`, etc.).""" @@ -47,6 +48,8 @@ def enable(tracer): # Guard against double instrumentation disable() + tracer = tracer_source.get_tracer(__name__, __version__) + wrapped = Session.request @functools.wraps(wrapped) diff --git a/ext/opentelemetry-ext-http-requests/tests/test_requests_integration.py b/ext/opentelemetry-ext-http-requests/tests/test_requests_integration.py index 2a02e1916a..35cf3110f3 100644 --- a/ext/opentelemetry-ext-http-requests/tests/test_requests_integration.py +++ b/ext/opentelemetry-ext-http-requests/tests/test_requests_integration.py @@ -16,6 +16,7 @@ import unittest from unittest import mock +import pkg_resources import requests import urllib3 @@ -28,7 +29,16 @@ class TestRequestsIntegration(unittest.TestCase): # TODO: Copy & paste from test_wsgi_middleware def setUp(self): self.span_attrs = {} - self.tracer = trace.tracer() + self.tracer_source = trace.TracerSource() + self.tracer = trace.Tracer() + self.get_tracer_patcher = mock.patch.object( + self.tracer_source, + "get_tracer", + autospec=True, + spec_set=True, + return_value=self.tracer, + ) + self.get_tracer = self.get_tracer_patcher.start() self.span_context_manager = mock.MagicMock() self.span = mock.create_autospec(trace.Span, spec_set=True) self.span_context_manager.__enter__.return_value = self.span @@ -45,7 +55,6 @@ def setspanattr(key, value): spec_set=True, return_value=self.span_context_manager, ) - self.start_as_current_span = self.start_span_patcher.start() mocked_response = requests.models.Response() mocked_response.status_code = 200 @@ -57,12 +66,21 @@ def setspanattr(key, value): spec_set=True, return_value=mocked_response, ) + + self.start_as_current_span = self.start_span_patcher.start() self.send = self.send_patcher.start() - opentelemetry.ext.http_requests.enable(self.tracer) + opentelemetry.ext.http_requests.enable(self.tracer_source) + distver = pkg_resources.get_distribution( + "opentelemetry-ext-http-requests" + ).version + self.get_tracer.assert_called_with( + opentelemetry.ext.http_requests.__name__, distver + ) def tearDown(self): opentelemetry.ext.http_requests.disable() + self.get_tracer_patcher.stop() self.send_patcher.stop() self.start_span_patcher.stop() @@ -70,7 +88,7 @@ def test_basic(self): url = "https://www.example.org/foo/bar?x=y#top" requests.get(url=url) self.assertEqual(1, len(self.send.call_args_list)) - self.tracer.start_as_current_span.assert_called_with( + self.tracer.start_as_current_span.assert_called_with( # pylint:disable=no-member "/foo/bar", kind=trace.SpanKind.CLIENT ) self.span_context_manager.__enter__.assert_called_with() @@ -96,11 +114,12 @@ def test_invalid_url(self): with self.assertRaises(exception_type): requests.post(url=url) + call_args = ( + self.tracer.start_as_current_span.call_args # pylint:disable=no-member + ) self.assertTrue( - self.tracer.start_as_current_span.call_args[0][0].startswith( - " bool: INVALID_SPAN = DefaultSpan(INVALID_SPAN_CONTEXT) +class TracerSource: + # pylint:disable=no-self-use,unused-argument + def get_tracer( + self, + instrumenting_module_name: str, + instrumenting_library_version: str = "", + ) -> "Tracer": + """Returns a `Tracer` for use by the given instrumentation library. + + For any two calls it is undefined whether the same or different + `Tracer` instances are returned, even for different library names. + + This function may return different `Tracer` types (e.g. a no-op tracer + vs. a functional tracer). + + Args: + instrumenting_module_name: The name of the instrumenting module + (usually just ``__name__``). + + This should *not* be the name of the module that is + instrumented but the name of the module doing the instrumentation. + E.g., instead of ``"requests"``, use + ``"opentelemetry.ext.http_requests"``. + + instrumenting_library_version: Optional. The version string of the + instrumenting library. Usually this should be the same as + ``pkg_resources.get_distribution(instrumenting_library_name).version``. + """ + return Tracer() + + class Tracer: """Handles span creation and in-process context propagation. @@ -522,43 +560,46 @@ def use_span( # the following type definition should be replaced with # from opentelemetry.util.loader import ImplementationFactory ImplementationFactory = typing.Callable[ - [typing.Type[Tracer]], typing.Optional[Tracer] + [typing.Type[TracerSource]], typing.Optional[TracerSource] ] -_TRACER = None # type: typing.Optional[Tracer] -_TRACER_FACTORY = None # type: typing.Optional[ImplementationFactory] +_TRACER_SOURCE = None # type: typing.Optional[TracerSource] +_TRACER_SOURCE_FACTORY = None # type: typing.Optional[ImplementationFactory] -def tracer() -> Tracer: - """Gets the current global :class:`~.Tracer` object. +def tracer_source() -> TracerSource: + """Gets the current global :class:`~.TracerSource` object. If there isn't one set yet, a default will be loaded. """ - global _TRACER, _TRACER_FACTORY # pylint:disable=global-statement + global _TRACER_SOURCE, _TRACER_SOURCE_FACTORY # pylint:disable=global-statement - if _TRACER is None: + if _TRACER_SOURCE is None: # pylint:disable=protected-access - _TRACER = loader._load_impl(Tracer, _TRACER_FACTORY) - del _TRACER_FACTORY + _TRACER_SOURCE = loader._load_impl( + TracerSource, _TRACER_SOURCE_FACTORY + ) + del _TRACER_SOURCE_FACTORY - return _TRACER + return _TRACER_SOURCE -def set_preferred_tracer_implementation( +def set_preferred_tracer_source_implementation( factory: ImplementationFactory, ) -> None: - """Set the factory to be used to create the tracer. + """Set the factory to be used to create the tracer source. See :mod:`opentelemetry.util.loader` for details. This function may not be called after a tracer is already loaded. Args: - factory: Callback that should create a new :class:`Tracer` instance. + factory: Callback that should create a new :class:`TracerSource` + instance. """ - global _TRACER_FACTORY # pylint:disable=global-statement + global _TRACER_SOURCE_FACTORY # pylint:disable=global-statement - if _TRACER: - raise RuntimeError("Tracer already loaded.") + if _TRACER_SOURCE: + raise RuntimeError("TracerSource already loaded.") - _TRACER_FACTORY = factory + _TRACER_SOURCE_FACTORY = factory diff --git a/opentelemetry-api/src/opentelemetry/util/loader.py b/opentelemetry-api/src/opentelemetry/util/loader.py index 3ae5a52fc5..b65c822ab9 100644 --- a/opentelemetry-api/src/opentelemetry/util/loader.py +++ b/opentelemetry-api/src/opentelemetry/util/loader.py @@ -15,7 +15,8 @@ """ The OpenTelemetry loader module is mainly used internally to load the -implementation for global objects like :func:`opentelemetry.trace.tracer`. +implementation for global objects like +:func:`opentelemetry.trace.tracer_source`. .. _loader-factory: @@ -27,7 +28,7 @@ def my_factory_for_t(api_type: typing.Type[T]) -> typing.Optional[T]: That function is called with e.g., the type of the global object it should create as an argument (e.g. the type object -:class:`opentelemetry.trace.Tracer`) and should return an instance of that type +:class:`opentelemetry.trace.TracerSource`) and should return an instance of that type (such that ``instanceof(my_factory_for_t(T), T)`` is true). Alternatively, it may return ``None`` to indicate that the no-op default should be used. @@ -36,16 +37,16 @@ def my_factory_for_t(api_type: typing.Type[T]) -> typing.Optional[T]: 1. If the environment variable :samp:`OPENTELEMETRY_PYTHON_IMPLEMENTATION_{getter-name}` (e.g., - ``OPENTELEMETRY_PYTHON_IMPLEMENTATION_TRACER``) is set to an nonempty - value, an attempt is made to import a module with that name and use a - factory function named ``get_opentelemetry_implementation`` in it. - 2. Otherwise, the same is tried with the environment - variable ``OPENTELEMETRY_PYTHON_IMPLEMENTATION_DEFAULT``. + ``OPENTELEMETRY_PYTHON_IMPLEMENTATION_TRACERSOURCE``) is set to an + nonempty value, an attempt is made to import a module with that name and + use a factory function named ``get_opentelemetry_implementation`` in it. + 2. Otherwise, the same is tried with the environment variable + ``OPENTELEMETRY_PYTHON_IMPLEMENTATION_DEFAULT``. 3. Otherwise, if a :samp:`set_preferred_{}_implementation` was called (e.g. - :func:`opentelemetry.trace.set_preferred_tracer_implementation`), the - callback set there is used (that is, the environment variables override - the callback set in code). + :func:`opentelemetry.trace.set_preferred_tracer_source_implementation`), + the callback set there is used (that is, the environment variables + override the callback set in code). 4. Otherwise, if :func:`set_preferred_default_implementation` was called, the callback set there is used. 5. Otherwise, an attempt is made to import and use the OpenTelemetry SDK. diff --git a/opentelemetry-api/tests/mypysmoke.py b/opentelemetry-api/tests/mypysmoke.py index 7badc13b69..3f652adca1 100644 --- a/opentelemetry-api/tests/mypysmoke.py +++ b/opentelemetry-api/tests/mypysmoke.py @@ -15,5 +15,5 @@ import opentelemetry.trace -def dummy_check_mypy_returntype() -> opentelemetry.trace.Tracer: - return opentelemetry.trace.tracer() +def dummy_check_mypy_returntype() -> opentelemetry.trace.TracerSource: + return opentelemetry.trace.tracer_source() diff --git a/opentelemetry-api/tests/test_implementation.py b/opentelemetry-api/tests/test_implementation.py index 60bf9dd9fa..cd126229f9 100644 --- a/opentelemetry-api/tests/test_implementation.py +++ b/opentelemetry-api/tests/test_implementation.py @@ -26,7 +26,8 @@ class TestAPIOnlyImplementation(unittest.TestCase): """ def test_tracer(self): - tracer = trace.Tracer() + tracer_source = trace.TracerSource() + tracer = tracer_source.get_tracer(__name__) with tracer.start_span("test") as span: self.assertEqual(span.get_context(), trace.INVALID_SPAN_CONTEXT) self.assertEqual(span, trace.INVALID_SPAN) diff --git a/opentelemetry-api/tests/test_loader.py b/opentelemetry-api/tests/test_loader.py index 970b615963..8ac397afcb 100644 --- a/opentelemetry-api/tests/test_loader.py +++ b/opentelemetry-api/tests/test_loader.py @@ -21,18 +21,18 @@ from opentelemetry import trace from opentelemetry.util import loader -DUMMY_TRACER = None +DUMMY_TRACER_SOURCE = None -class DummyTracer(trace.Tracer): +class DummyTracerSource(trace.TracerSource): pass def get_opentelemetry_implementation(type_): - global DUMMY_TRACER # pylint:disable=global-statement - assert type_ is trace.Tracer - DUMMY_TRACER = DummyTracer() - return DUMMY_TRACER + global DUMMY_TRACER_SOURCE # pylint:disable=global-statement + assert type_ is trace.TracerSource + DUMMY_TRACER_SOURCE = DummyTracerSource() + return DUMMY_TRACER_SOURCE # pylint:disable=redefined-outer-name,protected-access,unidiomatic-typecheck @@ -43,30 +43,32 @@ def setUp(self): reload(loader) reload(trace) - # Need to reload self, otherwise DummyTracer will have the wrong base - # class after reloading `trace`. + # Need to reload self, otherwise DummyTracerSource will have the wrong + # base class after reloading `trace`. reload(sys.modules[__name__]) def test_get_default(self): - tracer = trace.tracer() - self.assertIs(type(tracer), trace.Tracer) + tracer_source = trace.tracer_source() + self.assertIs(type(tracer_source), trace.TracerSource) def test_preferred_impl(self): - trace.set_preferred_tracer_implementation( + trace.set_preferred_tracer_source_implementation( get_opentelemetry_implementation ) - tracer = trace.tracer() - self.assertIs(tracer, DUMMY_TRACER) + tracer_source = trace.tracer_source() + self.assertIs(tracer_source, DUMMY_TRACER_SOURCE) # NOTE: We use do_* + *_ methods because subtest wouldn't run setUp, # which we require here. def do_test_preferred_impl(self, setter: Callable[[Any], Any]) -> None: setter(get_opentelemetry_implementation) - tracer = trace.tracer() - self.assertIs(tracer, DUMMY_TRACER) + tracer_source = trace.tracer_source() + self.assertIs(tracer_source, DUMMY_TRACER_SOURCE) def test_preferred_impl_with_tracer(self): - self.do_test_preferred_impl(trace.set_preferred_tracer_implementation) + self.do_test_preferred_impl( + trace.set_preferred_tracer_source_implementation + ) def test_preferred_impl_with_default(self): self.do_test_preferred_impl( @@ -74,16 +76,16 @@ def test_preferred_impl_with_default(self): ) def test_try_set_again(self): - self.assertTrue(trace.tracer()) - # Try setting after the tracer has already been created: + self.assertTrue(trace.tracer_source()) + # Try setting after the tracer_source has already been created: with self.assertRaises(RuntimeError) as einfo: - trace.set_preferred_tracer_implementation( + trace.set_preferred_tracer_source_implementation( get_opentelemetry_implementation ) self.assertIn("already loaded", str(einfo.exception)) def do_test_get_envvar(self, envvar_suffix: str) -> None: - global DUMMY_TRACER # pylint:disable=global-statement + global DUMMY_TRACER_SOURCE # pylint:disable=global-statement # Test is not runnable with this! self.assertFalse(sys.flags.ignore_environment) @@ -91,15 +93,15 @@ def do_test_get_envvar(self, envvar_suffix: str) -> None: envname = "OPENTELEMETRY_PYTHON_IMPLEMENTATION_" + envvar_suffix os.environ[envname] = __name__ try: - tracer = trace.tracer() - self.assertIs(tracer, DUMMY_TRACER) + tracer_source = trace.tracer_source() + self.assertIs(tracer_source, DUMMY_TRACER_SOURCE) finally: - DUMMY_TRACER = None + DUMMY_TRACER_SOURCE = None del os.environ[envname] - self.assertIs(type(tracer), DummyTracer) + self.assertIs(type(tracer_source), DummyTracerSource) def test_get_envvar_tracer(self): - return self.do_test_get_envvar("TRACER") + return self.do_test_get_envvar("TRACERSOURCE") def test_get_envvar_default(self): return self.do_test_get_envvar("DEFAULT") diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 140626aa94..3035ae7ef9 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -41,8 +41,8 @@ class SpanProcessor: invocations. Span processors can be registered directly using - :func:`Tracer.add_span_processor` and they are invoked in the same order - as they were registered. + :func:`TracerSource.add_span_processor` and they are invoked + in the same order as they were registered. """ def on_start(self, span: "Span") -> None: @@ -137,6 +137,7 @@ def __init__( links: Sequence[trace_api.Link] = (), kind: trace_api.SpanKind = trace_api.SpanKind.INTERNAL, span_processor: SpanProcessor = SpanProcessor(), + instrumentation_info: "InstrumentationInfo" = None, set_status_on_exception: bool = True, ) -> None: @@ -172,6 +173,7 @@ def __init__( self.end_time = None # type: Optional[int] self.start_time = None # type: Optional[int] + self.instrumentation_info = instrumentation_info def __repr__(self): return '{}(name="{}", context={})'.format( @@ -326,6 +328,46 @@ def generate_trace_id() -> int: return random.getrandbits(128) +class InstrumentationInfo: + """Immutable information about an instrumentation library module. + + See `TracerSource.get_tracer` for the meaning of the properties. + """ + + __slots__ = ("_name", "_version") + + def __init__(self, name: str, version: str): + self._name = name + self._version = version + + def __repr__(self): + return "{}({}, {})".format( + type(self).__name__, self._name, self._version + ) + + def __hash__(self): + return hash((self._name, self._version)) + + def __eq__(self, value): + return type(value) is type(self) and (self._name, self._version) == ( + value._name, + value._version, + ) + + def __lt__(self, value): + if type(value) is not type(self): + return NotImplemented + return (self._name, self._version) < (value._name, value._version) + + @property + def version(self) -> str: + return self._version + + @property + def name(self) -> str: + return self._name + + class Tracer(trace_api.Tracer): """See `opentelemetry.trace.Tracer`. @@ -337,23 +379,15 @@ class Tracer(trace_api.Tracer): def __init__( self, - name: str = "", - sampler: sampling.Sampler = trace_api.sampling.ALWAYS_ON, - shutdown_on_exit: bool = True, + source: "TracerSource", + instrumentation_info: InstrumentationInfo, ) -> None: - slot_name = "current_span" - if name: - slot_name = "{}.current_span".format(name) - self._current_span_slot = Context.register_slot(slot_name) - self._active_span_processor = MultiSpanProcessor() - self.sampler = sampler - self._atexit_handler = None - if shutdown_on_exit: - self._atexit_handler = atexit.register(self.shutdown) + self.source = source + self.instrumentation_info = instrumentation_info def get_current_span(self): """See `opentelemetry.trace.Tracer.get_current_span`.""" - return self._current_span_slot.get() + return self.source.get_current_span() def start_as_current_span( self, @@ -411,7 +445,7 @@ def start_span( # pylint: disable=too-many-locals # exported. # The sampler may also add attributes to the newly-created span, e.g. # to include information about the sampling decision. - sampling_decision = self.sampler.should_sample( + sampling_decision = self.source.sampler.should_sample( parent_context, context.trace_id, context.span_id, @@ -431,11 +465,12 @@ def start_span( # pylint: disable=too-many-locals name=name, context=context, parent=parent, - sampler=self.sampler, + sampler=self.source.sampler, attributes=span_attributes, - span_processor=self._active_span_processor, + span_processor=self.source._active_span_processor, # pylint:disable=protected-access kind=kind, links=links, + instrumentation_info=self.instrumentation_info, set_status_on_exception=set_status_on_exception, ) span.start(start_time=start_time) @@ -449,18 +484,56 @@ def use_span( ) -> Iterator[trace_api.Span]: """See `opentelemetry.trace.Tracer.use_span`.""" try: - span_snapshot = self._current_span_slot.get() - self._current_span_slot.set(span) + span_snapshot = self.source.get_current_span() + self.source._current_span_slot.set( # pylint:disable=protected-access + span + ) try: yield span finally: - self._current_span_slot.set(span_snapshot) + self.source._current_span_slot.set( # pylint:disable=protected-access + span_snapshot + ) finally: if end_on_exit: span.end() + +class TracerSource(trace_api.TracerSource): + def __init__( + self, + sampler: sampling.Sampler = trace_api.sampling.ALWAYS_ON, + shutdown_on_exit: bool = True, + ): + # TODO: How should multiple TracerSources behave? Should they get their own contexts? + # This could be done by adding `str(id(self))` to the slot name. + self._current_span_slot = Context.register_slot("current_span") + self._active_span_processor = MultiSpanProcessor() + self.sampler = sampler + self._atexit_handler = None + if shutdown_on_exit: + self._atexit_handler = atexit.register(self.shutdown) + + def get_tracer( + self, + instrumenting_module_name: str, + instrumenting_library_version: str = "", + ) -> "trace_api.Tracer": + if not instrumenting_module_name: # Reject empty strings too. + instrumenting_module_name = "ERROR:MISSING MODULE NAME" + logger.error("get_tracer called with missing module name.") + return Tracer( + self, + InstrumentationInfo( + instrumenting_module_name, instrumenting_library_version + ), + ) + + def get_current_span(self) -> Span: + return self._current_span_slot.get() + def add_span_processor(self, span_processor: SpanProcessor) -> None: - """Registers a new :class:`SpanProcessor` for this `Tracer`. + """Registers a new :class:`SpanProcessor` for this `TracerSource`. The span processors are invoked in the same order they are registered. """ @@ -475,6 +548,3 @@ def shutdown(self): if self._atexit_handler is not None: atexit.unregister(self._atexit_handler) self._atexit_handler = None - - -tracer = Tracer() diff --git a/opentelemetry-sdk/tests/test_implementation.py b/opentelemetry-sdk/tests/test_implementation.py index 9aaa5fc35a..d8d6bae139 100644 --- a/opentelemetry-sdk/tests/test_implementation.py +++ b/opentelemetry-sdk/tests/test_implementation.py @@ -28,7 +28,7 @@ class TestSDKImplementation(unittest.TestCase): """ def test_tracer(self): - tracer = trace.Tracer() + tracer = trace.TracerSource().get_tracer(__name__) with tracer.start_span("test") as span: self.assertNotEqual(span.get_context(), INVALID_SPAN_CONTEXT) self.assertNotEqual(span, INVALID_SPAN) diff --git a/opentelemetry-sdk/tests/trace/export/test_export.py b/opentelemetry-sdk/tests/trace/export/test_export.py index 9ad65aea88..54fdee2629 100644 --- a/opentelemetry-sdk/tests/trace/export/test_export.py +++ b/opentelemetry-sdk/tests/trace/export/test_export.py @@ -44,13 +44,14 @@ def shutdown(self): class TestSimpleExportSpanProcessor(unittest.TestCase): def test_simple_span_processor(self): - tracer = trace.Tracer() + tracer_source = trace.TracerSource() + tracer = tracer_source.get_tracer(__name__) spans_names_list = [] my_exporter = MySpanExporter(destination=spans_names_list) span_processor = export.SimpleExportSpanProcessor(my_exporter) - tracer.add_span_processor(span_processor) + tracer_source.add_span_processor(span_processor) with tracer.start_as_current_span("foo"): with tracer.start_as_current_span("bar"): @@ -68,13 +69,14 @@ def test_simple_span_processor_no_context(self): SpanProcessors should act on a span's start and end events whether or not it is ever the active span. """ - tracer = trace.Tracer() + tracer_source = trace.TracerSource() + tracer = tracer_source.get_tracer(__name__) spans_names_list = [] my_exporter = MySpanExporter(destination=spans_names_list) span_processor = export.SimpleExportSpanProcessor(my_exporter) - tracer.add_span_processor(span_processor) + tracer_source.add_span_processor(span_processor) with tracer.start_span("foo"): with tracer.start_span("bar"): diff --git a/opentelemetry-sdk/tests/trace/export/test_in_memory_span_exporter.py b/opentelemetry-sdk/tests/trace/export/test_in_memory_span_exporter.py index b52d148c1b..5c5194053b 100644 --- a/opentelemetry-sdk/tests/trace/export/test_in_memory_span_exporter.py +++ b/opentelemetry-sdk/tests/trace/export/test_in_memory_span_exporter.py @@ -24,62 +24,40 @@ class TestInMemorySpanExporter(unittest.TestCase): - def test_get_finished_spans(self): - tracer = trace.Tracer() - - memory_exporter = InMemorySpanExporter() - span_processor = export.SimpleExportSpanProcessor(memory_exporter) - tracer.add_span_processor(span_processor) - - with tracer.start_as_current_span("foo"): - with tracer.start_as_current_span("bar"): - with tracer.start_as_current_span("xxx"): + def setUp(self): + self.tracer_source = trace.TracerSource() + self.tracer = self.tracer_source.get_tracer(__name__) + self.memory_exporter = InMemorySpanExporter() + span_processor = export.SimpleExportSpanProcessor(self.memory_exporter) + self.tracer_source.add_span_processor(span_processor) + self.exec_scenario() + + def exec_scenario(self): + with self.tracer.start_as_current_span("foo"): + with self.tracer.start_as_current_span("bar"): + with self.tracer.start_as_current_span("xxx"): pass - span_list = memory_exporter.get_finished_spans() + def test_get_finished_spans(self): + span_list = self.memory_exporter.get_finished_spans() spans_names_list = [span.name for span in span_list] self.assertListEqual(["xxx", "bar", "foo"], spans_names_list) def test_clear(self): - tracer = trace.Tracer() - - memory_exporter = InMemorySpanExporter() - span_processor = export.SimpleExportSpanProcessor(memory_exporter) - tracer.add_span_processor(span_processor) - - with tracer.start_as_current_span("foo"): - with tracer.start_as_current_span("bar"): - with tracer.start_as_current_span("xxx"): - pass - - memory_exporter.clear() - span_list = memory_exporter.get_finished_spans() + self.memory_exporter.clear() + span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 0) def test_shutdown(self): - tracer = trace.Tracer() - - memory_exporter = InMemorySpanExporter() - span_processor = export.SimpleExportSpanProcessor(memory_exporter) - tracer.add_span_processor(span_processor) - - with tracer.start_as_current_span("foo"): - with tracer.start_as_current_span("bar"): - with tracer.start_as_current_span("xxx"): - pass - - span_list = memory_exporter.get_finished_spans() + span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 3) - memory_exporter.shutdown() + self.memory_exporter.shutdown() # after shutdown no new spans are accepted - with tracer.start_as_current_span("foo"): - with tracer.start_as_current_span("bar"): - with tracer.start_as_current_span("xxx"): - pass + self.exec_scenario() - span_list = memory_exporter.get_finished_spans() + span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 3) def test_return_code(self): diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 9ec68feeb0..98a7bb100e 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -24,21 +24,26 @@ from opentelemetry.util import time_ns +def new_tracer() -> trace_api.Tracer: + return trace.TracerSource().get_tracer(__name__) + + class TestTracer(unittest.TestCase): def test_extends_api(self): - tracer = trace.Tracer() + tracer = new_tracer() + self.assertIsInstance(tracer, trace.Tracer) self.assertIsInstance(tracer, trace_api.Tracer) def test_shutdown(self): - tracer = trace.Tracer() + tracer_source = trace.TracerSource() mock_processor1 = mock.Mock(spec=trace.SpanProcessor) - tracer.add_span_processor(mock_processor1) + tracer_source.add_span_processor(mock_processor1) mock_processor2 = mock.Mock(spec=trace.SpanProcessor) - tracer.add_span_processor(mock_processor2) + tracer_source.add_span_processor(mock_processor2) - tracer.shutdown() + tracer_source.shutdown() self.assertEqual(mock_processor1.shutdown.call_count, 1) self.assertEqual(mock_processor2.shutdown.call_count, 1) @@ -58,8 +63,8 @@ def print_shutdown_count(): # creating the tracer atexit.register(print_shutdown_count) -tracer = trace.Tracer({tracer_parameters}) -tracer.add_span_processor(mock_processor) +tracer_source = trace.TracerSource({tracer_parameters}) +tracer_source.add_span_processor(mock_processor) {tracer_shutdown} """ @@ -72,7 +77,7 @@ def run_general_code(shutdown_on_exit, explicit_shutdown): tracer_parameters = "shutdown_on_exit=False" if explicit_shutdown: - tracer_shutdown = "tracer.shutdown()" + tracer_shutdown = "tracer_source.shutdown()" return subprocess.check_output( [ @@ -103,7 +108,7 @@ def run_general_code(shutdown_on_exit, explicit_shutdown): class TestTracerSampling(unittest.TestCase): def test_default_sampler(self): - tracer = trace.Tracer() + tracer = new_tracer() # Check that the default tracer creates real spans via the default # sampler @@ -113,8 +118,8 @@ def test_default_sampler(self): self.assertIsInstance(child_span, trace.Span) def test_sampler_no_sampling(self): - tracer = trace.Tracer() - tracer.sampler = sampling.ALWAYS_OFF + tracer_source = trace.TracerSource(sampling.ALWAYS_OFF) + tracer = tracer_source.get_tracer(__name__) # Check that the default tracer creates no-op spans if the sampler # decides not to sampler @@ -132,15 +137,68 @@ def test_start_span_invalid_spancontext(self): Invalid span contexts should also not be added as a parent. This eliminates redundant error handling logic in exporters. """ - tracer = trace.Tracer("test_start_span_invalid_spancontext") + tracer = new_tracer() new_span = tracer.start_span( "root", parent=trace_api.INVALID_SPAN_CONTEXT ) self.assertTrue(new_span.context.is_valid()) self.assertIsNone(new_span.parent) + def test_instrumentation_info(self): + tracer_source = trace.TracerSource() + tracer1 = tracer_source.get_tracer("instr1") + tracer2 = tracer_source.get_tracer("instr2", "1.3b3") + span1 = tracer1.start_span("s1") + span2 = tracer2.start_span("s2") + self.assertEqual( + span1.instrumentation_info, trace.InstrumentationInfo("instr1", "") + ) + self.assertEqual( + span2.instrumentation_info, + trace.InstrumentationInfo("instr2", "1.3b3"), + ) + + self.assertEqual(span2.instrumentation_info.version, "1.3b3") + self.assertEqual(span2.instrumentation_info.name, "instr2") + + self.assertLess( + span1.instrumentation_info, span2.instrumentation_info + ) # Check sortability. + + def test_invalid_instrumentation_info(self): + tracer_source = trace.TracerSource() + tracer1 = tracer_source.get_tracer("") + tracer2 = tracer_source.get_tracer(None) + self.assertEqual( + tracer1.instrumentation_info, tracer2.instrumentation_info + ) + self.assertIsInstance( + tracer1.instrumentation_info, trace.InstrumentationInfo + ) + span1 = tracer1.start_span("foo") + self.assertTrue(span1.is_recording_events()) + self.assertEqual(tracer1.instrumentation_info.version, "") + self.assertEqual( + tracer1.instrumentation_info.name, "ERROR:MISSING MODULE NAME" + ) + + def test_span_processor_for_source(self): + tracer_source = trace.TracerSource() + tracer1 = tracer_source.get_tracer("instr1") + tracer2 = tracer_source.get_tracer("instr2", "1.3b3") + span1 = tracer1.start_span("s1") + span2 = tracer2.start_span("s2") + + # pylint:disable=protected-access + self.assertIs( + span1.span_processor, tracer_source._active_span_processor + ) + self.assertIs( + span2.span_processor, tracer_source._active_span_processor + ) + def test_start_span_implicit(self): - tracer = trace.Tracer("test_start_span_implicit") + tracer = new_tracer() self.assertIsNone(tracer.get_current_span()) @@ -185,7 +243,7 @@ def test_start_span_implicit(self): self.assertIsNotNone(root.end_time) def test_start_span_explicit(self): - tracer = trace.Tracer("test_start_span_explicit") + tracer = new_tracer() other_parent = trace_api.SpanContext( trace_id=0x000000000000000000000000DEADBEEF, @@ -233,7 +291,7 @@ def test_start_span_explicit(self): self.assertIsNotNone(child.end_time) def test_start_as_current_span_implicit(self): - tracer = trace.Tracer("test_start_as_current_span_implicit") + tracer = new_tracer() self.assertIsNone(tracer.get_current_span()) @@ -253,7 +311,7 @@ def test_start_as_current_span_implicit(self): self.assertIsNotNone(root.end_time) def test_start_as_current_span_explicit(self): - tracer = trace.Tracer("test_start_as_current_span_explicit") + tracer = new_tracer() other_parent = trace_api.SpanContext( trace_id=0x000000000000000000000000DEADBEEF, @@ -288,7 +346,7 @@ def test_start_as_current_span_explicit(self): class TestSpan(unittest.TestCase): def setUp(self): - self.tracer = trace.Tracer("test_span") + self.tracer = new_tracer() def test_basic_span(self): span = trace.Span("name", mock.Mock(spec=trace_api.SpanContext)) @@ -335,14 +393,19 @@ def test_attributes(self): self.assertEqual(root.attributes["attr-key2"], "val2") self.assertEqual(root.attributes["attr-in-both"], "span-attr") + def test_sampling_attributes(self): decision_attributes = { "sampler-attr": "sample-val", "attr-in-both": "decision-attr", } - self.tracer.sampler = sampling.StaticSampler( - sampling.Decision(sampled=True, attributes=decision_attributes) + tracer_source = trace.TracerSource( + sampling.StaticSampler( + sampling.Decision(sampled=True, attributes=decision_attributes) + ) ) + self.tracer = tracer_source.get_tracer(__name__) + with self.tracer.start_as_current_span("root2") as root: self.assertEqual(len(root.attributes), 2) self.assertEqual(root.attributes["sampler-attr"], "sample-val") @@ -515,7 +578,9 @@ def test_ended_span(self): def test_error_status(self): try: - with trace.Tracer("test_error_status").start_span("root") as root: + with trace.TracerSource().get_tracer(__name__).start_span( + "root" + ) as root: raise Exception("unknown") except Exception: # pylint: disable=broad-except pass @@ -546,7 +611,8 @@ def on_end(self, span: "trace.Span") -> None: class TestSpanProcessor(unittest.TestCase): def test_span_processor(self): - tracer = trace.Tracer() + tracer_source = trace.TracerSource() + tracer = tracer_source.get_tracer(__name__) spans_calls_list = [] # filled by MySpanProcessor expected_list = [] # filled by hand @@ -564,7 +630,7 @@ def test_span_processor(self): self.assertEqual(len(spans_calls_list), 0) # add single span processor - tracer.add_span_processor(sp1) + tracer_source.add_span_processor(sp1) with tracer.start_as_current_span("foo"): expected_list.append(span_event_start_fmt("SP1", "foo")) @@ -587,7 +653,7 @@ def test_span_processor(self): expected_list.clear() # go for multiple span processors - tracer.add_span_processor(sp2) + tracer_source.add_span_processor(sp2) with tracer.start_as_current_span("foo"): expected_list.append(span_event_start_fmt("SP1", "foo")) @@ -614,7 +680,8 @@ def test_span_processor(self): self.assertListEqual(spans_calls_list, expected_list) def test_add_span_processor_after_span_creation(self): - tracer = trace.Tracer() + tracer_source = trace.TracerSource() + tracer = tracer_source.get_tracer(__name__) spans_calls_list = [] # filled by MySpanProcessor expected_list = [] # filled by hand @@ -626,7 +693,7 @@ def test_add_span_processor_after_span_creation(self): with tracer.start_as_current_span("bar"): with tracer.start_as_current_span("baz"): # add span processor after spans have been created - tracer.add_span_processor(sp) + tracer_source.add_span_processor(sp) expected_list.append(span_event_end_fmt("SP1", "baz")) diff --git a/tests/w3c_tracecontext_validation_server.py b/tests/w3c_tracecontext_validation_server.py index a26141f14c..bea4d4fde5 100644 --- a/tests/w3c_tracecontext_validation_server.py +++ b/tests/w3c_tracecontext_validation_server.py @@ -26,7 +26,7 @@ from opentelemetry import trace from opentelemetry.ext import http_requests from opentelemetry.ext.wsgi import OpenTelemetryMiddleware -from opentelemetry.sdk.trace import Tracer +from opentelemetry.sdk.trace import TracerSource from opentelemetry.sdk.trace.export import ( ConsoleSpanExporter, SimpleExportSpanProcessor, @@ -34,16 +34,16 @@ # The preferred tracer implementation must be set, as the opentelemetry-api # defines the interface with a no-op implementation. -trace.set_preferred_tracer_implementation(lambda T: Tracer()) +trace.set_preferred_tracer_source_implementation(lambda T: TracerSource()) # Integrations are the glue that binds the OpenTelemetry API and the # frameworks and libraries that are used together, automatically creating # Spans and propagating context as appropriate. -http_requests.enable(trace.tracer()) +http_requests.enable(trace.tracer_source()) # SpanExporter receives the spans and send them to the target location. span_processor = SimpleExportSpanProcessor(ConsoleSpanExporter()) -trace.tracer().add_span_processor(span_processor) +trace.tracer_source().add_span_processor(span_processor) app = flask.Flask(__name__) app.wsgi_app = OpenTelemetryMiddleware(app.wsgi_app)