diff --git a/docs/auto_instrumentation/auto_instrumentation.rst b/docs/auto_instrumentation/auto_instrumentation.rst new file mode 100644 index 00000000000..a45512f7f9d --- /dev/null +++ b/docs/auto_instrumentation/auto_instrumentation.rst @@ -0,0 +1,7 @@ +OpenTelemetry Python Autoinstrumentation +======================================== + +.. toctree:: + :maxdepth: 1 + + instrumentor diff --git a/docs/auto_instrumentation/instrumentor.rst b/docs/auto_instrumentation/instrumentor.rst new file mode 100644 index 00000000000..c94c0237f57 --- /dev/null +++ b/docs/auto_instrumentation/instrumentor.rst @@ -0,0 +1,7 @@ +opentelemetry.auto_instrumentation.instrumentor package +======================================================= + +.. automodule:: opentelemetry.auto_instrumentation.instrumentor + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/conf.py b/docs/conf.py index a509f14f5d6..acd44c0c7af 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -18,6 +18,7 @@ source_dirs = [ os.path.abspath("../opentelemetry-api/src/"), os.path.abspath("../opentelemetry-sdk/src/"), + os.path.abspath("../opentelemetry-auto-instrumentation/src/"), ] ext = "../ext" diff --git a/docs/examples/auto-instrumentation/README.md b/docs/examples/auto-instrumentation/README.md new file mode 100644 index 00000000000..46b0b44b2c8 --- /dev/null +++ b/docs/examples/auto-instrumentation/README.md @@ -0,0 +1,112 @@ +# Overview + +This example shows how to use auto-instrumentation in OpenTelemetry. This example is also based on a previous example +for OpenTracing that can be found [here](https://github.com/yurishkuro/opentracing-tutorial/tree/master/python). + +This example uses 2 scripts whose main difference is they being instrumented manually or not: + +1. `server_instrumented.py` which has been instrumented manually +2. `server_uninstrumented.py` which has not been instrumented manually + +The former will be run without the automatic instrumentation agent and the latter with the automatic instrumentation +agent. They should produce the same result, showing that the automatic instrumentation agent does the equivalent +of what manual instrumentation does. + +In order to understand this better, here is the relevant part of both scripts: + +## Manually instrumented server + +`server_instrumented.py` + +```python +@app.route("/server_request") +def server_request(): + with tracer.start_as_current_span( + "server_request", + parent=propagators.extract( + lambda dict_, key: dict_.get(key, []), request.headers + )["current-span"], + ): + print(request.args.get("param")) + return "served" +``` + +## Publisher not instrumented manually + +`server_uninstrumented.py` + +```python +@app.route("/server_request") +def server_request(): + print(request.args.get("param")) + return "served" +``` + +# Preparation + +This example will be executed in a separate virtual environment: + +```sh +$ mkdir auto_instrumentation +$ virtualenv auto_instrumentation +$ source auto_instrumentation/bin/activate +``` + +# Installation + +```sh +$ pip install opentelemetry-api +$ pip install opentelemetry-sdk +$ pip install opentelemetry-auto-instrumentation +$ pip install ext/opentelemetry-ext-flask +$ pip install flask +$ pip install requests +``` + +# Execution + +## Execution of the manually instrumented server + +This is done in 2 separate consoles, one to run each of the scripts that make up this example: + +```sh +$ source auto_instrumentation/bin/activate +$ python opentelemetry-python/opentelemetry-auto-instrumentation/example/server_instrumented.py +``` + +```sh +$ source auto_instrumentation/bin/activate +$ python opentelemetry-python/opentelemetry-auto-instrumentation/example/client.py testing +``` + +The execution of `server_instrumented.py` should return an output similar to: + +```sh +Hello, testing! +Span(name="serv_request", context=SpanContext(trace_id=0x9c0e0ce8f7b7dbb51d1d6e744a4dad49, span_id=0xd1ba3ec4c76a0d7f, trace_state={}), kind=SpanKind.INTERNAL, parent=None, start_time=2020-03-19T00:06:31.275719Z, end_time=2020-03-19T00:06:31.275920Z) +127.0.0.1 - - [18/Mar/2020 18:06:31] "GET /serv_request?helloStr=Hello%2C+testing%21 HTTP/1.1" 200 - +``` + +## Execution of an automatically instrumented server + +Now, kill the execution of `server_instrumented.py` with `ctrl + c` and run this instead: + +```sh +$ opentelemetry-auto-instrumentation opentelemetry-python/opentelemetry-auto-instrumentation/example/server_uninstrumented.py +``` + +In the console where you previously executed `client.py`, run again this again: + +```sh +$ python opentelemetry-python/opentelemetry-auto-instrumentation/example/client.py testing +``` + +The execution of `server_uninstrumented.py` should return an output similar to: + +```sh +Hello, testing! +Span(name="serv_request", context=SpanContext(trace_id=0xf26b28b5243e48f5f96bfc753f95f3f0, span_id=0xbeb179a095d087ed, trace_state={}), kind=SpanKind.SERVER, parent=<opentelemetry.trace.DefaultSpan object at 0x7f1a20a54908>, start_time=2020-03-19T00:24:18.828561Z, end_time=2020-03-19T00:24:18.845127Z) +127.0.0.1 - - [18/Mar/2020 18:24:18] "GET /serv_request?helloStr=Hello%2C+testing%21 HTTP/1.1" 200 - +``` + +As you can see, both outputs are equivalentsince the automatic instrumentation does what the manual instrumentation does too. diff --git a/docs/examples/auto-instrumentation/client.py b/docs/examples/auto-instrumentation/client.py new file mode 100644 index 00000000000..c8301003be7 --- /dev/null +++ b/docs/examples/auto-instrumentation/client.py @@ -0,0 +1,50 @@ +# 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. + +from sys import argv + +from flask import Flask +from requests import get + +from opentelemetry import propagators, trace +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import ( + ConsoleSpanExporter, + SimpleExportSpanProcessor, +) + +app = Flask(__name__) + +trace.set_tracer_provider(TracerProvider()) +tracer = trace.get_tracer_provider().get_tracer(__name__) + +trace.get_tracer_provider().add_span_processor( + SimpleExportSpanProcessor(ConsoleSpanExporter()) +) + + +assert len(argv) == 2 + +with tracer.start_as_current_span("client"): + + with tracer.start_as_current_span("client-server"): + headers = {} + propagators.inject(dict.__setitem__, headers) + requested = get( + "http://localhost:8082/server_request", + params={"param": argv[1]}, + headers=headers, + ) + + assert requested.status_code == 200 diff --git a/docs/examples/auto-instrumentation/server_instrumented.py b/docs/examples/auto-instrumentation/server_instrumented.py new file mode 100644 index 00000000000..1c78aab15d8 --- /dev/null +++ b/docs/examples/auto-instrumentation/server_instrumented.py @@ -0,0 +1,47 @@ +# 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. + +from flask import Flask, request + +from opentelemetry import propagators, trace +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import ( + ConsoleSpanExporter, + SimpleExportSpanProcessor, +) + +app = Flask(__name__) + +trace.set_tracer_provider(TracerProvider()) +tracer = trace.get_tracer_provider().get_tracer(__name__) + +trace.get_tracer_provider().add_span_processor( + SimpleExportSpanProcessor(ConsoleSpanExporter()) +) + + +@app.route("/server_request") +def server_request(): + with tracer.start_as_current_span( + "server_request", + parent=propagators.extract( + lambda dict_, key: dict_.get(key, []), request.headers + )["current-span"], + ): + print(request.args.get("param")) + return "served" + + +if __name__ == "__main__": + app.run(port=8082) diff --git a/docs/examples/auto-instrumentation/server_uninstrumented.py b/docs/examples/auto-instrumentation/server_uninstrumented.py new file mode 100644 index 00000000000..b8360341ab2 --- /dev/null +++ b/docs/examples/auto-instrumentation/server_uninstrumented.py @@ -0,0 +1,40 @@ +# 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. + +from flask import Flask, request + +from opentelemetry import trace +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import ( + ConsoleSpanExporter, + SimpleExportSpanProcessor, +) + +app = Flask(__name__) + +trace.set_tracer_provider(TracerProvider()) + +trace.get_tracer_provider().add_span_processor( + SimpleExportSpanProcessor(ConsoleSpanExporter()) +) + + +@app.route("/server_request") +def server_request(): + print(request.args.get("param")) + return "served" + + +if __name__ == "__main__": + app.run(port=8082) diff --git a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py index a37048b7d49..21a9962d864 100644 --- a/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py +++ b/docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py @@ -21,7 +21,7 @@ import opentelemetry.ext.http_requests from opentelemetry import trace -from opentelemetry.ext.flask import instrument_app +from opentelemetry.ext.flask import FlaskInstrumentor from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( ConsoleSpanExporter, @@ -33,9 +33,9 @@ SimpleExportSpanProcessor(ConsoleSpanExporter()) ) +FlaskInstrumentor().instrument() app = flask.Flask(__name__) opentelemetry.ext.http_requests.enable(trace.get_tracer_provider()) -instrument_app(app) @app.route("/") diff --git a/docs/index.rst b/docs/index.rst index fd60e72f037..2d26e24f839 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -61,7 +61,6 @@ install <https://pip.pypa.io/en/stable/reference/pip_install/#editable-installs> getting-started - .. toctree:: :maxdepth: 1 :caption: OpenTelemetry Python Packages @@ -69,6 +68,7 @@ install <https://pip.pypa.io/en/stable/reference/pip_install/#editable-installs> api/api sdk/sdk + auto_instrumentation/auto_instrumentation .. toctree:: :maxdepth: 2 diff --git a/ext/opentelemetry-ext-flask/setup.py b/ext/opentelemetry-ext-flask/setup.py index df9742c9006..84b33c23b22 100644 --- a/ext/opentelemetry-ext-flask/setup.py +++ b/ext/opentelemetry-ext-flask/setup.py @@ -23,4 +23,11 @@ with open(VERSION_FILENAME) as f: exec(f.read(), PACKAGE_INFO) -setuptools.setup(version=PACKAGE_INFO["__version__"]) +setuptools.setup( + version=PACKAGE_INFO["__version__"], + entry_points={ + "opentelemetry_instrumentor": [ + "flask = opentelemetry.ext.flask:FlaskInstrumentor" + ] + }, +) 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 11c027ecbcc..02b0de8652e 100644 --- a/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py +++ b/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py @@ -34,12 +34,12 @@ def hello(): import logging -from flask import request as flask_request +import flask import opentelemetry.ext.wsgi as otel_wsgi from opentelemetry import context, propagators, trace +from opentelemetry.auto_instrumentation.instrumentor import BaseInstrumentor from opentelemetry.ext.flask.version import __version__ -from opentelemetry.trace.propagation import get_span_from_context from opentelemetry.util import time_ns logger = logging.getLogger(__name__) @@ -50,82 +50,105 @@ def hello(): _ENVIRON_TOKEN = "opentelemetry-flask.token" -def instrument_app(flask): - """Makes the passed-in Flask object traced by OpenTelemetry. +class _InstrumentedFlask(flask.Flask): + def __init__(self, *args, **kwargs): + + super().__init__(*args, **kwargs) + + # Single use variable here to avoid recursion issues. + wsgi = self.wsgi_app + + def wrapped_app(environ, start_response): + # We want to measure the time for route matching, etc. + # In theory, we could start the span here and use + # update_name later but that API is "highly discouraged" so + # we better avoid it. + environ[_ENVIRON_STARTTIME_KEY] = time_ns() + + def _start_response(status, response_headers, *args, **kwargs): + span = flask.request.environ.get(_ENVIRON_SPAN_KEY) + if span: + otel_wsgi.add_response_attributes( + span, status, response_headers + ) + else: + logger.warning( + "Flask environ's OpenTelemetry span " + "missing at _start_response(%s)", + status, + ) + + return start_response( + status, response_headers, *args, **kwargs + ) + + return wsgi(environ, _start_response) + + self.wsgi_app = wrapped_app + + @self.before_request + def _before_flask_request(): + environ = flask.request.environ + span_name = ( + flask.request.endpoint + or otel_wsgi.get_default_span_name(environ) + ) + token = context.attach( + propagators.extract(otel_wsgi.get_header_from_environ, environ) + ) + + tracer = trace.get_tracer(__name__, __version__) + + attributes = otel_wsgi.collect_request_attributes(environ) + if flask.request.url_rule: + # For 404 that result from no route found, etc, we + # don't have a url_rule. + attributes["http.route"] = flask.request.url_rule.rule + span = tracer.start_span( + span_name, + kind=trace.SpanKind.SERVER, + attributes=attributes, + start_time=environ.get(_ENVIRON_STARTTIME_KEY), + ) + activation = tracer.use_span(span, end_on_exit=True) + activation.__enter__() + environ[_ENVIRON_ACTIVATION_KEY] = activation + environ[_ENVIRON_SPAN_KEY] = span + environ[_ENVIRON_TOKEN] = token + + @self.teardown_request + def _teardown_flask_request(exc): + activation = flask.request.environ.get(_ENVIRON_ACTIVATION_KEY) + if not activation: + logger.warning( + "Flask environ's OpenTelemetry activation missing" + "at _teardown_flask_request(%s)", + exc, + ) + return + + if exc is None: + activation.__exit__(None, None, None) + else: + activation.__exit__( + type(exc), exc, getattr(exc, "__traceback__", None) + ) + context.detach(flask.request.environ.get(_ENVIRON_TOKEN)) - You must not call this function multiple times on the same Flask object. + +class FlaskInstrumentor(BaseInstrumentor): + """A instrumentor for flask.Flask + + See `BaseInstrumentor` """ - wsgi = flask.wsgi_app + def __init__(self): + super().__init__() + self._original_flask = None - def wrapped_app(environ, start_response): - # We want to measure the time for route matching, etc. - # In theory, we could start the span here and use update_name later - # but that API is "highly discouraged" so we better avoid it. - environ[_ENVIRON_STARTTIME_KEY] = time_ns() + def _instrument(self): + self._original_flask = flask.Flask + flask.Flask = _InstrumentedFlask - def _start_response(status, response_headers, *args, **kwargs): - span = flask_request.environ.get(_ENVIRON_SPAN_KEY) - if span: - otel_wsgi.add_response_attributes( - span, status, response_headers - ) - else: - logger.warning( - "Flask environ's OpenTelemetry span missing at _start_response(%s)", - status, - ) - return start_response(status, response_headers, *args, **kwargs) - - return wsgi(environ, _start_response) - - flask.wsgi_app = wrapped_app - - flask.before_request(_before_flask_request) - flask.teardown_request(_teardown_flask_request) - - -def _before_flask_request(): - environ = flask_request.environ - span_name = flask_request.endpoint or otel_wsgi.get_default_span_name( - environ - ) - token = context.attach( - propagators.extract(otel_wsgi.get_header_from_environ, environ) - ) - - tracer = trace.get_tracer(__name__, __version__) - - attributes = otel_wsgi.collect_request_attributes(environ) - if flask_request.url_rule: - # For 404 that result from no route found, etc, we don't have a url_rule. - attributes["http.route"] = flask_request.url_rule.rule - span = tracer.start_span( - span_name, - kind=trace.SpanKind.SERVER, - attributes=attributes, - start_time=environ.get(_ENVIRON_STARTTIME_KEY), - ) - activation = tracer.use_span(span, end_on_exit=True) - activation.__enter__() - environ[_ENVIRON_ACTIVATION_KEY] = activation - environ[_ENVIRON_SPAN_KEY] = span - environ[_ENVIRON_TOKEN] = token - - -def _teardown_flask_request(exc): - activation = flask_request.environ.get(_ENVIRON_ACTIVATION_KEY) - if not activation: - logger.warning( - "Flask environ's OpenTelemetry activation missing at _teardown_flask_request(%s)", - exc, - ) - return - - if exc is None: - activation.__exit__(None, None, None) - else: - activation.__exit__( - type(exc), exc, getattr(exc, "__traceback__", None) - ) - context.detach(flask_request.environ.get(_ENVIRON_TOKEN)) + def _uninstrument(self): + flask.Flask = self._original_flask diff --git a/ext/opentelemetry-ext-flask/tests/conftest.py b/ext/opentelemetry-ext-flask/tests/conftest.py new file mode 100644 index 00000000000..22a587ab2e6 --- /dev/null +++ b/ext/opentelemetry-ext-flask/tests/conftest.py @@ -0,0 +1,24 @@ +# Copyright 2020, 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. +from opentelemetry.ext.flask import FlaskInstrumentor + +_FLASK_INSTRUMENTOR = FlaskInstrumentor() + + +def pytest_sessionstart(session): # pylint: disable=unused-argument + _FLASK_INSTRUMENTOR.instrument() + + +def pytest_sessionfinish(session): # pylint: disable=unused-argument + _FLASK_INSTRUMENTOR.uninstrument() diff --git a/ext/opentelemetry-ext-flask/tests/test_flask_integration.py b/ext/opentelemetry-ext-flask/tests/test_flask_integration.py index 9f61920ce94..b63424b905a 100644 --- a/ext/opentelemetry-ext-flask/tests/test_flask_integration.py +++ b/ext/opentelemetry-ext-flask/tests/test_flask_integration.py @@ -18,7 +18,6 @@ from werkzeug.test import Client from werkzeug.wrappers import BaseResponse -import opentelemetry.ext.flask as otel_flask from opentelemetry import trace as trace_api from opentelemetry.ext.testutil.wsgitestutil import WsgiTestBase @@ -43,6 +42,8 @@ def expected_attributes(override_attributes): class TestFlaskIntegration(WsgiTestBase): def setUp(self): + # No instrumentation code is here because it is present in the + # conftest.py file next to this file. super().setUp() self.app = Flask(__name__) @@ -54,7 +55,6 @@ def hello_endpoint(helloid): self.app.route("/hello/<int:helloid>")(hello_endpoint) - otel_flask.instrument_app(self.app) self.client = Client(self.app, BaseResponse) def test_only_strings_in_environ(self): diff --git a/opentelemetry-auto-instrumentation/CHANGELOG.md b/opentelemetry-auto-instrumentation/CHANGELOG.md new file mode 100644 index 00000000000..825c32f0d03 --- /dev/null +++ b/opentelemetry-auto-instrumentation/CHANGELOG.md @@ -0,0 +1 @@ +# Changelog diff --git a/opentelemetry-auto-instrumentation/MANIFEST.in b/opentelemetry-auto-instrumentation/MANIFEST.in new file mode 100644 index 00000000000..191b7d19592 --- /dev/null +++ b/opentelemetry-auto-instrumentation/MANIFEST.in @@ -0,0 +1,7 @@ +prune tests +graft src +global-exclude *.pyc +global-exclude *.pyo +global-exclude __pycache__/* +include MANIFEST.in +include README.rst diff --git a/opentelemetry-auto-instrumentation/README.rst b/opentelemetry-auto-instrumentation/README.rst new file mode 100644 index 00000000000..b153072ae5a --- /dev/null +++ b/opentelemetry-auto-instrumentation/README.rst @@ -0,0 +1,19 @@ +OpenTelemetry Auto Instrumentation +================================== + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-auto-instrumentation.svg + :target: https://pypi.org/project/opentelemetry-auto-instrumentation/ + +Installation +------------ + +:: + + pip install opentelemetry-auto-instrumentation + +References +---------- + +* `OpenTelemetry Project <https://opentelemetry.io/>`_ diff --git a/opentelemetry-auto-instrumentation/setup.cfg b/opentelemetry-auto-instrumentation/setup.cfg new file mode 100644 index 00000000000..182b15866fc --- /dev/null +++ b/opentelemetry-auto-instrumentation/setup.cfg @@ -0,0 +1,50 @@ +# Copyright 2019, 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. +# +[metadata] +name = opentelemetry-auto-instrumentation +description = Auto Instrumentation for OpenTelemetry Python +long_description = file: README.rst +long_description_content_type = text/x-rst +author = OpenTelemetry Authors +author_email = cncf-opentelemetry-contributors@lists.cncf.io +url = https://github.com/open-telemetry/opentelemetry-python/tree/master/opentelemetry-auto-instrumentation" +platforms = any +license = Apache-2.0 +classifiers = + Development Status :: 3 - Alpha + Intended Audience :: Developers + License :: OSI Approved :: Apache Software License + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.4 + Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + +[options] +python_requires = >=3.4 +package_dir= + =src +packages=find_namespace: +zip_safe = False +include_package_data = True +install_requires = opentelemetry-api==0.6.dev0 + +[options.packages.find] +where = src + +[options.entry_points] +console_scripts = + opentelemetry-auto-instrumentation = opentelemetry.auto_instrumentation.auto_instrumentation:run diff --git a/opentelemetry-auto-instrumentation/setup.py b/opentelemetry-auto-instrumentation/setup.py new file mode 100644 index 00000000000..86f8faedbcf --- /dev/null +++ b/opentelemetry-auto-instrumentation/setup.py @@ -0,0 +1,27 @@ +# 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. + +import os + +import setuptools + +BASE_DIR = os.path.dirname(__file__) +VERSION_FILENAME = os.path.join( + BASE_DIR, "src", "opentelemetry", "auto_instrumentation", "version.py" +) +PACKAGE_INFO = {} +with open(VERSION_FILENAME) as f: + exec(f.read(), PACKAGE_INFO) + +setuptools.setup(version=PACKAGE_INFO["__version__"],) diff --git a/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/__init__.py b/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/__init__.py new file mode 100644 index 00000000000..dfafb5386a9 --- /dev/null +++ b/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/__init__.py @@ -0,0 +1,28 @@ +# 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. + +""" +Usage +----- + +This package provides a command that automatically instruments a program: + +:: + + opentelemetry-auto-instrumentation program.py + +The code in ``program.py`` needs to use one of the packages for which there is +an OpenTelemetry extension. For a list of the available extensions please check +`here <https://opentelemetry-python.readthedocsio/>`_. +""" diff --git a/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/auto_instrumentation.py b/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/auto_instrumentation.py new file mode 100644 index 00000000000..00ccf6a0ea9 --- /dev/null +++ b/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/auto_instrumentation.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 + +# 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. + +from logging import getLogger +from runpy import run_path +from sys import argv + +from pkg_resources import iter_entry_points + +logger = getLogger(__file__) + + +def run() -> None: + + for entry_point in iter_entry_points("opentelemetry_instrumentor"): + try: + entry_point.load()().instrument() # type: ignore + logger.debug("Instrumented %s", entry_point.name) + + except Exception: # pylint: disable=broad-except + logger.exception("Instrumenting of %s failed", entry_point.name) + + run_path(argv[1], run_name="__main__") # type: ignore diff --git a/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/instrumentor.py b/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/instrumentor.py new file mode 100644 index 00000000000..9deb6b15238 --- /dev/null +++ b/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/instrumentor.py @@ -0,0 +1,65 @@ +# 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. +# type: ignore + +""" +OpenTelemetry Base Instrumentor +""" + +from abc import ABC, abstractmethod +from logging import getLogger + +_LOG = getLogger(__name__) + + +class BaseInstrumentor(ABC): + """An ABC for instrumentors""" + + def __init__(self): + self._is_instrumented = False + + @abstractmethod + def _instrument(self) -> None: + """Instrument""" + + @abstractmethod + def _uninstrument(self) -> None: + """Uninstrument""" + + def instrument(self) -> None: + """Instrument""" + + if not self._is_instrumented: + result = self._instrument() + self._is_instrumented = True + return result + + _LOG.warning("Attempting to instrument while already instrumented") + + return None + + def uninstrument(self) -> None: + """Uninstrument""" + + if self._is_instrumented: + result = self._uninstrument() + self._is_instrumented = False + return result + + _LOG.warning("Attempting to uninstrument while already uninstrumented") + + return None + + +__all__ = ["BaseInstrumentor"] diff --git a/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/version.py b/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/version.py new file mode 100644 index 00000000000..0941210ca3f --- /dev/null +++ b/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/version.py @@ -0,0 +1,15 @@ +# 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. + +__version__ = "0.6.dev0" diff --git a/opentelemetry-auto-instrumentation/tests/__init__.py b/opentelemetry-auto-instrumentation/tests/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/opentelemetry-auto-instrumentation/tests/test_instrumentor.py b/opentelemetry-auto-instrumentation/tests/test_instrumentor.py new file mode 100644 index 00000000000..1324213536c --- /dev/null +++ b/opentelemetry-auto-instrumentation/tests/test_instrumentor.py @@ -0,0 +1,44 @@ +# 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. +# type: ignore + +from logging import WARNING +from unittest import TestCase + +from opentelemetry.auto_instrumentation.instrumentor import BaseInstrumentor + + +class TestInstrumentor(TestCase): + def test_protect(self): + class Instrumentor(BaseInstrumentor): + def _instrument(self): + return "instrumented" + + def _uninstrument(self): + return "uninstrumented" + + instrumentor = Instrumentor() + + with self.assertLogs(level=WARNING): + self.assertIs(instrumentor.uninstrument(), None) + + self.assertEqual(instrumentor.instrument(), "instrumented") + + with self.assertLogs(level=WARNING): + self.assertIs(instrumentor.instrument(), None) + + self.assertEqual(instrumentor.uninstrument(), "uninstrumented") + + with self.assertLogs(level=WARNING): + self.assertIs(instrumentor.uninstrument(), None) diff --git a/tox.ini b/tox.ini index 3c0023bd47d..6e64bdbd441 100644 --- a/tox.ini +++ b/tox.ini @@ -12,6 +12,10 @@ envlist = py3{4,5,6,7,8}-test-sdk pypy3-test-sdk + ; opentelemetry-auto-instrumentation + py3{4,5,6,7,8}-test-auto-instrumentation + pypy3-test-auto-instrumentation + ; opentelemetry-example-app py3{4,5,6,7,8}-test-example-app pypy3-test-example-app @@ -43,6 +47,7 @@ envlist = ; opentelemetry-ext-mysql py3{4,5,6,7,8}-test-ext-mysql pypy3-test-ext-mysql + ; opentelemetry-ext-otcollector py3{4,5,6,7,8}-test-ext-otcollector ; ext-otcollector intentionally excluded from pypy3 @@ -101,6 +106,7 @@ setenv = changedir = test-api: opentelemetry-api/tests test-sdk: opentelemetry-sdk/tests + test-auto-instrumentation: opentelemetry-auto-instrumentation/tests test-ext-http-requests: ext/opentelemetry-ext-http-requests/tests test-ext-jaeger: ext/opentelemetry-ext-jaeger/tests test-ext-dbapi: ext/opentelemetry-ext-dbapi/tests @@ -122,7 +128,9 @@ commands_pre = python -m pip install -U pip setuptools wheel test: pip install {toxinidir}/opentelemetry-api test-sdk: pip install {toxinidir}/opentelemetry-sdk + test-auto-instrumentation: pip install {toxinidir}/opentelemetry-auto-instrumentation example-app: pip install {toxinidir}/opentelemetry-sdk + example-app: pip install {toxinidir}/opentelemetry-auto-instrumentation example-app: pip install {toxinidir}/ext/opentelemetry-ext-http-requests example-app: pip install {toxinidir}/ext/opentelemetry-ext-wsgi example-app: pip install {toxinidir}/ext/opentelemetry-ext-flask @@ -139,6 +147,7 @@ commands_pre = wsgi,flask: pip install {toxinidir}/ext/opentelemetry-ext-testutil wsgi,flask: pip install {toxinidir}/ext/opentelemetry-ext-wsgi wsgi,flask: pip install {toxinidir}/opentelemetry-sdk + flask: pip install {toxinidir}/opentelemetry-auto-instrumentation flask: pip install {toxinidir}/ext/opentelemetry-ext-flask[test] dbapi: pip install {toxinidir}/ext/opentelemetry-ext-dbapi mysql: pip install {toxinidir}/ext/opentelemetry-ext-dbapi