Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add an autoinstrumentation mechanism and an instrumentor for Flask #327

Merged
merged 29 commits into from
Mar 30, 2020
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
b346942
Add autoinstrumentation prototype for Flask
ocelotl Dec 10, 2019
e59f4d0
Remove unused app
ocelotl Mar 17, 2020
4f79a0e
Move example
ocelotl Mar 17, 2020
9125b7e
More fixes
ocelotl Mar 17, 2020
3524cce
More fixes
ocelotl Mar 18, 2020
ef5b329
Use current-span
ocelotl Mar 18, 2020
2eaf7e4
More lint fixes
ocelotl Mar 18, 2020
9c479b0
More lint fixes
ocelotl Mar 18, 2020
fa069c3
Fix example
ocelotl Mar 19, 2020
2b97ab0
Rename patcher to instrumentor
ocelotl Mar 21, 2020
7a11b3b
More fixes
ocelotl Mar 23, 2020
31ec6c3
fix documents
mauriciovasquezbernal Mar 25, 2020
b18efe3
Add comment
ocelotl Mar 25, 2020
bc7a9a7
Update opentelemetry-auto-instrumentation/README.rst
ocelotl Mar 25, 2020
06e5a2b
Update opentelemetry-auto-instrumentation/README.rst
ocelotl Mar 25, 2020
f23d004
Move description to the package docstring
ocelotl Mar 25, 2020
3218bff
Update opentelemetry-auto-instrumentation/example/README.md
ocelotl Mar 25, 2020
6f36a33
Use python instead of python3
ocelotl Mar 25, 2020
f0f182e
Remove formatter
ocelotl Mar 25, 2020
0f65460
Change logger
ocelotl Mar 25, 2020
4907663
Update opentelemetry-auto-instrumentation/tests/__init__.py
ocelotl Mar 25, 2020
94f6167
Remove year
ocelotl Mar 25, 2020
91803f9
Remove line
ocelotl Mar 26, 2020
410817e
Move examples
ocelotl Mar 26, 2020
fdf1774
Remove blank line
ocelotl Mar 26, 2020
c8df74b
Rename directory
ocelotl Mar 26, 2020
7172275
Merge branch 'master' into issue_300
toumorokoshi Mar 29, 2020
2a56a92
Merge branch 'master' into issue_300
c24t Mar 30, 2020
5bb20d1
Shorten entry point
ocelotl Mar 30, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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("/")
Expand Down
4 changes: 3 additions & 1 deletion docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -83,6 +82,9 @@ install <https://pip.pypa.io/en/stable/reference/pip_install/#editable-installs>
:caption: Examples
:name: examples
:glob:
:caption: OpenTelemetry Auto Instrumentation:

opentelemetry.auto_instrumentation.patcher

examples/**

Expand Down
8 changes: 8 additions & 0 deletions docs/opentelemetry.auto_instrumentation.patcher.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
opentelemetry.auto_instrumentation.patcher package
==================================================


Module contents
---------------

.. automodule:: opentelemetry.auto_instrumentation.patcher
9 changes: 8 additions & 1 deletion ext/opentelemetry-ext-flask/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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_instrumentor": [
"flask = opentelemetry.ext.flask:FlaskInstrumentor"
]
},
)
175 changes: 99 additions & 76 deletions ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.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__)
Expand All @@ -19,82 +19,105 @@
_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
24 changes: 24 additions & 0 deletions ext/opentelemetry-ext-flask/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -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()
2 changes: 0 additions & 2 deletions ext/opentelemetry-ext-flask/tests/test_flask_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -54,7 +53,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):
Expand Down
1 change: 1 addition & 0 deletions opentelemetry-auto-instrumentation/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Changelog
7 changes: 7 additions & 0 deletions opentelemetry-auto-instrumentation/MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
prune tests
graft src
global-exclude *.pyc
global-exclude *.pyo
global-exclude __pycache__/*
include MANIFEST.in
include README.rst
32 changes: 32 additions & 0 deletions opentelemetry-auto-instrumentation/README.rst
Original file line number Diff line number Diff line change
@@ -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 <https://opentelemetry.io/>`_

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/>`_.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's better to use an "internal" target for this link. So if we decide to host our documentation somewhere else the generated link is not broken.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense, but what do you mean with "internal"? Can you provide an example, please?

Loading