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/index.rst b/docs/index.rst index bc09b5a4759..777bd0dfde4 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -61,7 +61,6 @@ install getting-started - .. toctree:: :maxdepth: 1 :caption: OpenTelemetry Python Packages @@ -83,6 +82,9 @@ install :caption: Examples :name: examples :glob: + :caption: OpenTelemetry Auto Instrumentation: + + opentelemetry.auto_instrumentation.patcher examples/** diff --git a/docs/opentelemetry.auto_instrumentation.patcher.rst b/docs/opentelemetry.auto_instrumentation.patcher.rst new file mode 100644 index 00000000000..731d03d6e07 --- /dev/null +++ b/docs/opentelemetry.auto_instrumentation.patcher.rst @@ -0,0 +1,8 @@ +opentelemetry.auto_instrumentation.patcher package +================================================== + + +Module contents +--------------- + +.. automodule:: opentelemetry.auto_instrumentation.patcher diff --git a/examples/auto_instrumentation/README.md b/examples/auto_instrumentation/README.md new file mode 100644 index 00000000000..431593e3f4c --- /dev/null +++ b/examples/auto_instrumentation/README.md @@ -0,0 +1,119 @@ +# 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. `publisher_instrumented.py` which has been instrumented manually +2. `publisher_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 an equal 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: + +## Publisher instrumented manually + +`publisher_instrumented.py` + +```python +@app.route("/publish_request") +def publish_request(): + + with tracer.start_as_current_span( + "publish_request", propagators.extract(get_as_list, request.headers) + ): + hello_str = request.args.get("helloStr") + print(hello_str) + return "published" +``` + +## Publisher not instrumented manually + +`publisher_uninstrumented.py` + +```python +@app.route("/publish_request") +def publish_request(): + hello_str = request.args.get("helloStr") + print(hello_str) + return "published" +``` + +# 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 +$ git clone git@github.com:open-telemetry/opentelemetry-python.git +$ cd opentelemetry-python +$ pip3 install -e opentelemetry-api +$ pip3 install -e opentelemetry-sdk +$ pip3 install -e opentelemetry-auto-instrumentation +$ pip3 install -e ext/opentelemetry-ext-flask +$ pip3 install flask +$ pip3 install requests +``` + +# Execution + +## Execution of the manually instrumented publisher + +This is done in 3 separate consoles, one to run each of the scripts that make up this example: + +```sh +$ source auto_instrumentation/bin/activate +$ python3 opentelemetry-python/examples/auto_instrumentation/formatter.py +``` + +```sh +$ source auto_instrumentation/bin/activate +$ python3 opentelemetry-python/examples/auto_instrumentation/publisher_instrumented.py +``` + +```sh +$ source auto_instrumentation/bin/activate +$ python3 opentelemetry-python/examples/auto_instrumentation/hello.py testing +``` + +The execution of `publisher_instrumented.py` should return an output similar to: + +```sh +Hello, testing! +Span(name="publish", context=SpanContext(trace_id=0xd18be4c644d3be57a8623bbdbdbcef76, span_id=0x6162c475bab8d365, trace_state={}), kind=SpanKind.SERVER, parent=SpanContext(trace_id=0xd18be4c644d3be57a8623bbdbdbcef76, span_id=0xdafb264c5b1b6ed0, trace_state={}), start_time=2019-12-19T01:11:12.172866Z, end_time=2019-12-19T01:11:12.173383Z) +127.0.0.1 - - [18/Dec/2019 19:11:12] "GET /publish?helloStr=Hello%2C+testing%21 HTTP/1.1" 200 - +``` + +## Execution of an automatically instrumented publisher + +Now, kill the execution of `publisher_instrumented.py` with `ctrl + c` and run this instead: + +```sh +$ opentelemetry-auto-instrumentation opentelemetry-python/examples/auto_instrumentation/publisher_uninstrumented.py +``` + +In the console where you previously executed `hello.py`, run again this again: + +```sh +$ python3 opentelemetry-python/examples/auto_instrumentation/hello.py testing +``` + +The execution of `publisher_uninstrumented.py` should return an output similar to: + +```sh +Hello, testing! +Span(name="publish", context=SpanContext(trace_id=0xd18be4c644d3be57a8623bbdbdbcef76, span_id=0x6162c475bab8d365, trace_state={}), kind=SpanKind.SERVER, parent=SpanContext(trace_id=0xd18be4c644d3be57a8623bbdbdbcef76, span_id=0xdafb264c5b1b6ed0, trace_state={}), start_time=2019-12-19T01:11:12.172866Z, end_time=2019-12-19T01:11:12.173383Z) +127.0.0.1 - - [18/Dec/2019 19:11:12] "GET /publish?helloStr=Hello%2C+testing%21 HTTP/1.1" 200 - +``` + +As you can see, both outputs are equal since the automatic instrumentation does what the manual instrumentation does too. diff --git a/examples/auto_instrumentation/formatter.py b/examples/auto_instrumentation/formatter.py new file mode 100644 index 00000000000..5e16f89938e --- /dev/null +++ b/examples/auto_instrumentation/formatter.py @@ -0,0 +1,52 @@ +# 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 flask import Flask, request + +from opentelemetry import propagators, trace +from opentelemetry.context.propagation.tracecontexthttptextformat import ( + TraceContextHTTPTextFormat, +) +from opentelemetry.propagators import set_global_httptextformat +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import ( + ConsoleSpanExporter, + SimpleExportSpanProcessor, +) +from utils import get_as_list + +app = Flask(__name__) + +trace.set_preferred_tracer_provider_implementation(lambda T: TracerProvider()) +tracer = trace.tracer_provider().get_tracer(__name__) + +trace.tracer_provider().add_span_processor( + SimpleExportSpanProcessor(ConsoleSpanExporter()) +) +set_global_httptextformat(TraceContextHTTPTextFormat) + + +@app.route("/format_request") +def format_request(): + + with tracer.start_as_current_span( + "format_request", + parent=propagators.extract(get_as_list, request.headers), + ): + hello_to = request.args.get("helloTo") + return "Hello, %s!" % hello_to + + +if __name__ == "__main__": + app.run(port=8081) diff --git a/examples/auto_instrumentation/hello.py b/examples/auto_instrumentation/hello.py new file mode 100644 index 00000000000..85108006f71 --- /dev/null +++ b/examples/auto_instrumentation/hello.py @@ -0,0 +1,67 @@ +# 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 sys import argv + +from flask import Flask +from requests import get + +from opentelemetry import propagators, trace +from opentelemetry.context.propagation.tracecontexthttptextformat import ( + TraceContextHTTPTextFormat, +) +from opentelemetry.propagators import set_global_httptextformat +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import ( + ConsoleSpanExporter, + SimpleExportSpanProcessor, +) + +app = Flask(__name__) + +trace.set_preferred_tracer_provider_implementation(lambda T: TracerProvider()) +tracer = trace.tracer_provider().get_tracer(__name__) + +trace.tracer_provider().add_span_processor( + SimpleExportSpanProcessor(ConsoleSpanExporter()) +) +set_global_httptextformat(TraceContextHTTPTextFormat) + + +def http_get(port, path, param, value): + + headers = {} + propagators.inject(tracer, dict.__setitem__, headers) + + requested = get( + "http://localhost:{}/{}".format(port, path), + params={param: value}, + headers=headers, + ) + + assert requested.status_code == 200 + return requested.text + + +assert len(argv) == 2 + +hello_to = argv[1] + +with tracer.start_as_current_span("hello") as hello_span: + + with tracer.start_as_current_span("hello-format", parent=hello_span): + hello_str = http_get(8081, "format_request", "helloTo", hello_to) + + with tracer.start_as_current_span("hello-publish", parent=hello_span): + http_get(8082, "publish_request", "helloStr", hello_str) diff --git a/examples/auto_instrumentation/publisher_instrumented.py b/examples/auto_instrumentation/publisher_instrumented.py new file mode 100644 index 00000000000..328c5cbb37c --- /dev/null +++ b/examples/auto_instrumentation/publisher_instrumented.py @@ -0,0 +1,52 @@ +# 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 flask import Flask, request + +from opentelemetry import propagators, trace +from opentelemetry.context.propagation.tracecontexthttptextformat import ( + TraceContextHTTPTextFormat, +) +from opentelemetry.propagators import set_global_httptextformat +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import ( + ConsoleSpanExporter, + SimpleExportSpanProcessor, +) +from utils import get_as_list + +app = Flask(__name__) + +trace.set_preferred_tracer_provider_implementation(lambda T: TracerProvider()) +tracer = trace.tracer_provider().get_tracer(__name__) + +trace.tracer_provider().add_span_processor( + SimpleExportSpanProcessor(ConsoleSpanExporter()) +) +set_global_httptextformat(TraceContextHTTPTextFormat) + + +@app.route("/publish_request") +def publish_request(): + + with tracer.start_as_current_span( + "publish_request", propagators.extract(get_as_list, request.headers) + ): + hello_str = request.args.get("helloStr") + print(hello_str) + return "published" + + +if __name__ == "__main__": + app.run(port=8082) diff --git a/examples/auto_instrumentation/publisher_uninstrumented.py b/examples/auto_instrumentation/publisher_uninstrumented.py new file mode 100644 index 00000000000..79d06b592e9 --- /dev/null +++ b/examples/auto_instrumentation/publisher_uninstrumented.py @@ -0,0 +1,41 @@ +# 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 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_preferred_tracer_provider_implementation(lambda T: TracerProvider()) + +trace.tracer_provider().add_span_processor( + SimpleExportSpanProcessor(ConsoleSpanExporter()) +) + + +@app.route("/publish_request") +def publish_request(): + hello_str = request.args.get("helloStr") + print(hello_str) + return "published" + + +if __name__ == "__main__": + app.run(port=8082) diff --git a/examples/auto_instrumentation/utils.py b/examples/auto_instrumentation/utils.py new file mode 100644 index 00000000000..0fa82af694b --- /dev/null +++ b/examples/auto_instrumentation/utils.py @@ -0,0 +1,21 @@ +# 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. + + +def get_as_list(dict_object, key): + value = dict_object.get(key) + return value if value is not None else [] + + +__all__ = ["get_as_list"] 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 new file mode 100644 index 00000000000..baa5a52cc50 --- /dev/null +++ b/examples/opentelemetry-example-app/src/opentelemetry_example_app/flask_example.py @@ -0,0 +1,78 @@ +# 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. +# +""" +This module serves as an example to integrate with flask, using +the requests library to perform downstream requests +""" +import flask +import pkg_resources +import requests + +import opentelemetry.ext.http_requests +from opentelemetry import trace +from opentelemetry.ext.flask import FlaskPatcher +from opentelemetry.sdk.trace import TracerProvider + + +def configure_opentelemetry(): + """Configure a flask application to use OpenTelemetry. + + This activates the specific components: + + * sets tracer to the SDK's Tracer + * enables requests integration on the Tracer + * uses a WSGI middleware to enable configuration + + TODO: + + * processors? + * exporters? + """ + # Start by configuring all objects required to ensure + # a complete end to end workflow. + # 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_provider_implementation( + lambda _: TracerProvider() + ) + + # 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). + # 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_provider()) + + +FlaskPatcher().patch() +app = flask.Flask(__name__) + + +@app.route("/") +def hello(): + # emit a trace that measures how long the + # sleep takes + version = pkg_resources.get_distribution( + "opentelemetry-example-app" + ).version + tracer = trace.get_tracer(__name__, version) + with tracer.start_as_current_span("example-request"): + requests.get("http://www.example.com") + return "hello" + + +configure_opentelemetry() diff --git a/ext/opentelemetry-ext-flask/setup.py b/ext/opentelemetry-ext-flask/setup.py index 34b27c60342..13313eca777 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_auto_instrumentation_patcher": [ + "flask = opentelemetry.ext.flask:FlaskPatcher" + ] + }, +) 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 b30b42d3fd2..0991a4a3d01 100644 --- a/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py +++ b/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py @@ -3,12 +3,12 @@ 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 import propagators, trace +from opentelemetry.auto_instrumentation.patcher import BasePatcher 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__) @@ -19,82 +19,104 @@ _ENVIRON_TOKEN = "opentelemetry-flask.token" -def instrument_app(flask): - """Makes the passed-in Flask object traced by OpenTelemetry. +class _PatchedFlask(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) + ) + parent_span = 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, + parent_span, + 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 + + @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) + ) - You must not call this function multiple times on the same Flask object. + +class FlaskPatcher(BasePatcher): + """A patcher for flask.Flask + + See `BasePatcher` """ - 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 _patch(self): + self._original_flask = flask.Flask + flask.Flask = _PatchedFlask - 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 _unpatch(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..e3c59b6dad4 --- /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 FlaskPatcher + +_FLASK_PATCHER = FlaskPatcher() + + +def pytest_sessionstart(session): # pylint: disable=unused-argument + _FLASK_PATCHER.patch() + + +def pytest_sessionfinish(session): # pylint: disable=unused-argument + _FLASK_PATCHER.unpatch() diff --git a/ext/opentelemetry-ext-flask/tests/test_flask_integration.py b/ext/opentelemetry-ext-flask/tests/test_flask_integration.py index 9d2f2560118..b81ad673cbe 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 @@ -54,7 +53,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..d7b34d1ab95 --- /dev/null +++ b/opentelemetry-auto-instrumentation/README.rst @@ -0,0 +1,32 @@ +OpenTelemetry Auto Instrumentation +============================================================================ + +|pypi| + +.. |pypi| image:: https://badge.fury.io/py/opentelemetry-api.svg + :target: https://pypi.org/project/opentelemetry-auto-instrumentation/ + +Installation +------------ + +:: + + pip install opentelemetry-auto-instrumentation + +References +---------- + +* `OpenTelemetry Project `_ + +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/setup.cfg b/opentelemetry-auto-instrumentation/setup.cfg new file mode 100644 index 00000000000..49fcd38b01a --- /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.5.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..8d6300384a4 --- /dev/null +++ b/opentelemetry-auto-instrumentation/setup.py @@ -0,0 +1,27 @@ +# 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. + +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..e69de29bb2d 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..44651e4964b --- /dev/null +++ b/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/auto_instrumentation.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 + +# 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 logging import getLogger +from runpy import run_path +from sys import argv + +from pkg_resources import iter_entry_points + +_LOG = getLogger(__file__) + + +def run() -> None: + + for entry_point in iter_entry_points( + "opentelemetry_auto_instrumentation_patcher" + ): + try: + entry_point.load()().patch() # type: ignore + _LOG.debug("Patched %s", entry_point.name) + + except Exception: # pylint: disable=broad-except + _LOG.exception("Patching 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/patcher.py b/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/patcher.py new file mode 100644 index 00000000000..ee5d3c68a15 --- /dev/null +++ b/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/patcher.py @@ -0,0 +1,65 @@ +# 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. +# type: ignore + +""" +OpenTelemetry Auto Instrumentation Patcher +""" + +from abc import ABC, abstractmethod +from logging import getLogger + +_LOG = getLogger(__name__) + + +class BasePatcher(ABC): + """An ABC for patchers""" + + def __init__(self): + self._is_patched = False + + @abstractmethod + def _patch(self) -> None: + """Patch""" + + @abstractmethod + def _unpatch(self) -> None: + """Unpatch""" + + def patch(self) -> None: + """Patch""" + + if not self._is_patched: + result = self._patch() + self._is_patched = True + return result + + _LOG.warning("Attempting to patch while already patched") + + return None + + def unpatch(self) -> None: + """Unpatch""" + + if self._is_patched: + result = self._unpatch() + self._is_patched = False + return result + + _LOG.warning("Attempting to unpatch while already unpatched") + + return None + + +__all__ = ["BasePatcher"] 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..f48cb5bee5c --- /dev/null +++ b/opentelemetry-auto-instrumentation/src/opentelemetry/auto_instrumentation/version.py @@ -0,0 +1,15 @@ +# 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. + +__version__ = "0.5.dev0" diff --git a/opentelemetry-auto-instrumentation/tests/__init__.py b/opentelemetry-auto-instrumentation/tests/__init__.py new file mode 100644 index 00000000000..6ab2e961ec4 --- /dev/null +++ b/opentelemetry-auto-instrumentation/tests/__init__.py @@ -0,0 +1,13 @@ +# 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. diff --git a/opentelemetry-auto-instrumentation/tests/test_patcher.py b/opentelemetry-auto-instrumentation/tests/test_patcher.py new file mode 100644 index 00000000000..8e9e45b5d3e --- /dev/null +++ b/opentelemetry-auto-instrumentation/tests/test_patcher.py @@ -0,0 +1,44 @@ +# 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. +# type: ignore + +from logging import WARNING +from unittest import TestCase + +from opentelemetry.auto_instrumentation.patcher import BasePatcher + + +class TestPatcher(TestCase): + def test_protect(self): + class Patcher(BasePatcher): + def _patch(self): + return "patched" + + def _unpatch(self): + return "unpatched" + + patcher = Patcher() + + with self.assertLogs(level=WARNING): + self.assertIs(patcher.unpatch(), None) + + self.assertEqual(patcher.patch(), "patched") + + with self.assertLogs(level=WARNING): + self.assertIs(patcher.patch(), None) + + self.assertEqual(patcher.unpatch(), "unpatched") + + with self.assertLogs(level=WARNING): + self.assertIs(patcher.unpatch(), None) diff --git a/tox.ini b/tox.ini index 52c82eba075..4517b74b906 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