diff --git a/README.md b/README.md index 0333853eec..6e47958fe1 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,42 @@ -# OpenTelemetry Python -[![Gitter chat](https://img.shields.io/gitter/room/opentelemetry/opentelemetry-python)](https://gitter.im/open-telemetry/opentelemetry-python) -[![Build status](https://travis-ci.org/open-telemetry/opentelemetry-python.svg?branch=master)](https://travis-ci.org/open-telemetry/opentelemetry-python) +--- +
+ + Getting Started + • + API Documentation + • + Getting In Touch (Gitter) + +
+ + + ++ + Contributing + • + Examples + +
+ +--- + +## About this project The Python [OpenTelemetry](https://opentelemetry.io/) client. 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 863d6f3389..8f44273b6e 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 @@ -34,15 +34,15 @@ trace.set_tracer_provider(TracerProvider()) opentelemetry.ext.requests.RequestsInstrumentor().instrument() -FlaskInstrumentor().instrument() trace.get_tracer_provider().add_span_processor( SimpleExportSpanProcessor(ConsoleSpanExporter()) ) - app = flask.Flask(__name__) +FlaskInstrumentor().instrument_app(app) + @app.route("/") def hello(): diff --git a/docs/getting-started.rst b/docs/getting-started.rst index f25cf79b77..5d20fbe2c0 100644 --- a/docs/getting-started.rst +++ b/docs/getting-started.rst @@ -184,9 +184,6 @@ And let's write a small Flask application that sends an HTTP request, activating .. code-block:: python # flask_example.py - from opentelemetry.ext.flask import FlaskInstrumentor - FlaskInstrumentor().instrument() # This needs to be executed before importing Flask - import flask import requests @@ -195,6 +192,7 @@ And let's write a small Flask application that sends an HTTP request, activating from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ConsoleSpanExporter from opentelemetry.sdk.trace.export import SimpleExportSpanProcessor + from opentelemetry.ext.flask import FlaskInstrumentor trace.set_tracer_provider(TracerProvider()) trace.get_tracer_provider().add_span_processor( @@ -202,7 +200,8 @@ And let's write a small Flask application that sends an HTTP request, activating ) app = flask.Flask(__name__) - opentelemetry.ext.requests.RequestsInstrumentor().instrument() + FlaskInstrumentor().instrument_app(app) + opentelemetry.ext.http_requests.RequestsInstrumentor().instrument() @app.route("/") def hello(): diff --git a/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py b/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py index 1e936da115..040c8770c6 100644 --- a/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py +++ b/ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py @@ -29,12 +29,13 @@ .. code-block:: python - from opentelemetry.ext.flask import FlaskInstrumentor - FlaskInstrumentor().instrument() # This needs to be executed before importing Flask from flask import Flask + from opentelemetry.ext.flask import FlaskInstrumentor app = Flask(__name__) + FlaskInstrumentor().instrument_app(app) + @app.route("/") def hello(): return "Hello!" @@ -46,7 +47,7 @@ def hello(): --- """ -import logging +from logging import getLogger import flask @@ -60,7 +61,7 @@ def hello(): time_ns, ) -logger = logging.getLogger(__name__) +_logger = getLogger(__name__) _ENVIRON_STARTTIME_KEY = "opentelemetry-flask.starttime_key" _ENVIRON_SPAN_KEY = "opentelemetry-flask.span_key" @@ -68,102 +69,104 @@ def hello(): _ENVIRON_TOKEN = "opentelemetry-flask.token" +def _rewrapped_app(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): + + if not _disable_trace(flask.request.url): + + 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_app(environ, _start_response) + + return _wrapped_app + + +def _before_request(): + if _disable_trace(flask.request.url): + return + + 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_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)) + + 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): - if not _disable_trace(flask.request.url): - 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(): - # Do not trace if the url is excluded - if _disable_trace(flask.request.url): - return - 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) - ) + self._original_wsgi_ = self.wsgi_app + self.wsgi_app = _rewrapped_app(self.wsgi_app) - 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): - # Not traced if the url is excluded - if _disable_trace(flask.request.url): - return - 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)) + self.before_request(_before_request) + self.teardown_request(_teardown_request) def _disable_trace(url): excluded_hosts = configuration.Configuration().FLASK_EXCLUDED_HOSTS excluded_paths = configuration.Configuration().FLASK_EXCLUDED_PATHS + if excluded_hosts: excluded_hosts = str.split(excluded_hosts, ",") if disable_tracing_hostname(url, excluded_hosts): @@ -176,18 +179,50 @@ def _disable_trace(url): class FlaskInstrumentor(BaseInstrumentor): - """A instrumentor for flask.Flask + # pylint: disable=protected-access,attribute-defined-outside-init + """An instrumentor for flask.Flask See `BaseInstrumentor` """ - def __init__(self): - super().__init__() - self._original_flask = None - def _instrument(self, **kwargs): self._original_flask = flask.Flask flask.Flask = _InstrumentedFlask + def instrument_app(self, app): # pylint: disable=no-self-use + if not hasattr(app, "_is_instrumented"): + app._is_instrumented = False + + if not app._is_instrumented: + app._original_wsgi_app = app.wsgi_app + app.wsgi_app = _rewrapped_app(app.wsgi_app) + + app.before_request(_before_request) + app.teardown_request(_teardown_request) + app._is_instrumented = True + else: + _logger.warning( + "Attempting to instrument Flask app while already instrumented" + ) + def _uninstrument(self, **kwargs): flask.Flask = self._original_flask + + def uninstrument_app(self, app): # pylint: disable=no-self-use + if not hasattr(app, "_is_instrumented"): + app._is_instrumented = False + + if app._is_instrumented: + app.wsgi_app = app._original_wsgi_app + + # FIXME add support for other Flask blueprints that are not None + app.before_request_funcs[None].remove(_before_request) + app.teardown_request_funcs[None].remove(_teardown_request) + del app._original_wsgi_app + + app._is_instrumented = False + else: + _logger.warning( + "Attempting to uninstrument Flask " + "app while already uninstrumented" + ) diff --git a/ext/opentelemetry-ext-flask/tests/test_flask_integration.py b/ext/opentelemetry-ext-flask/tests/base_test.py similarity index 74% rename from ext/opentelemetry-ext-flask/tests/test_flask_integration.py rename to ext/opentelemetry-ext-flask/tests/base_test.py index 1babfff2f5..42341826df 100644 --- a/ext/opentelemetry-ext-flask/tests/test_flask_integration.py +++ b/ext/opentelemetry-ext-flask/tests/base_test.py @@ -12,16 +12,14 @@ # See the License for the specific language governing permissions and # limitations under the License. -import unittest from unittest.mock import patch -from flask import Flask, request +from flask import request from werkzeug.test import Client from werkzeug.wrappers import BaseResponse -from opentelemetry import trace as trace_api +from opentelemetry import trace from opentelemetry.configuration import Configuration -from opentelemetry.test.wsgitestutil import WsgiTestBase def expected_attributes(override_attributes): @@ -42,36 +40,34 @@ def expected_attributes(override_attributes): return default_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() - Configuration._instance = None # pylint:disable=protected-access - Configuration.__slots__ = [] - self.app = Flask(__name__) +class InstrumentationTest: + def setUp(self): # pylint: disable=invalid-name + super().setUp() # pylint: disable=no-member + Configuration._reset() # pylint: disable=protected-access - def hello_endpoint(helloid): - if helloid == 500: - raise ValueError(":-(") - return "Hello: " + str(helloid) + @staticmethod + def _hello_endpoint(helloid): + if helloid == 500: + raise ValueError(":-(") + return "Hello: " + str(helloid) + def _common_initialization(self): def excluded_endpoint(): return "excluded" def excluded2_endpoint(): return "excluded2" - self.app.route("/hello/