From a137bc228ab4e82b0e99d88c4f075a19f73fc0d6 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Mon, 30 Mar 2020 13:44:43 -0600 Subject: [PATCH] Add an autoinstrumentation mechanism and an instrumentor for Flask (#327) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adding an autoinstrumentation mechanism and a Flask instrumentor (an instrumentor is a class that implements the _instrument and _uninstrument methods). It works like this: A console command is defined. This makes it possible to run a command named opentelemetry-auto-instrumentation that will execute this function. When the opentelemetry-auto-instrumentation command is executed, then the instrument method of the different instrumentors is called, which are made available via an entry-point. 2.In the case of the Flask instrumentor, the original flask.Flask gets replaced with _InstrumentedFlask (in this case, the Flask instrumentor uses monkey patching to perform the instrumentation, nevertheless, monkey patching is not always the method used to do this, so the name instrumentor is preferred over patcher). Once all instrumentation is enabled, the app is executed. Co-Authored-By: Mauricio Vásquez Co-authored-by: Chris Kleinknecht --- .../auto_instrumentation.rst | 7 + docs/auto_instrumentation/instrumentor.rst | 7 + docs/conf.py | 1 + docs/examples/auto-instrumentation/README.md | 112 +++++++++++ docs/examples/auto-instrumentation/client.py | 50 +++++ .../server_instrumented.py | 47 +++++ .../server_uninstrumented.py | 40 ++++ .../flask_example.py | 4 +- docs/index.rst | 2 +- ext/opentelemetry-ext-flask/setup.py | 9 +- .../src/opentelemetry/ext/flask/__init__.py | 175 ++++++++++-------- ext/opentelemetry-ext-flask/tests/conftest.py | 24 +++ .../tests/test_flask_integration.py | 4 +- .../CHANGELOG.md | 1 + .../MANIFEST.in | 7 + opentelemetry-auto-instrumentation/README.rst | 19 ++ opentelemetry-auto-instrumentation/setup.cfg | 50 +++++ opentelemetry-auto-instrumentation/setup.py | 27 +++ .../auto_instrumentation/__init__.py | 28 +++ .../auto_instrumentation.py | 36 ++++ .../auto_instrumentation/instrumentor.py | 65 +++++++ .../auto_instrumentation/version.py | 15 ++ .../tests/__init__.py | 0 .../tests/test_instrumentor.py | 44 +++++ tox.ini | 9 + 25 files changed, 701 insertions(+), 82 deletions(-) create mode 100644 docs/auto_instrumentation/auto_instrumentation.rst create mode 100644 docs/auto_instrumentation/instrumentor.rst create mode 100644 docs/examples/auto-instrumentation/README.md create mode 100644 docs/examples/auto-instrumentation/client.py create mode 100644 docs/examples/auto-instrumentation/server_instrumented.py create mode 100644 docs/examples/auto-instrumentation/server_uninstrumented.py create mode 100644 ext/opentelemetry-ext-flask/tests/conftest.py create mode 100644 opentelemetry-auto-instrumentation/CHANGELOG.md create mode 100644 opentelemetry-auto-instrumentation/MANIFEST.in create mode 100644 opentelemetry-auto-instrumentation/README.rst create mode 100644 opentelemetry-auto-instrumentation/setup.cfg create mode 100644 opentelemetry-auto-instrumentation/setup.py create mode 100644 opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/__init__.py create mode 100644 opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/auto_instrumentation.py create mode 100644 opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/instrumentor.py create mode 100644 opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/version.py create mode 100644 opentelemetry-auto-instrumentation/tests/__init__.py create mode 100644 opentelemetry-auto-instrumentation/tests/test_instrumentor.py 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=, 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 getting-started - .. toctree:: :maxdepth: 1 :caption: OpenTelemetry Python Packages @@ -69,6 +68,7 @@ install 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/")(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 `_ 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 `_. +""" 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