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 Flask instrumentor #8

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 9 additions & 0 deletions ext/opentelemetry-ext-testutil/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
OpenTelemetry Test Utilities
============================

Test utilities for OpenTelemetry unit tests


References
----------
* `OpenTelemetry Project <https://opentelemetry.io/>`_
48 changes: 48 additions & 0 deletions ext/opentelemetry-ext-testutil/setup.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# 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.
#

[metadata]
name = opentelemetry-ext-testutil
description = Test utilities for OpenTelemetry unit tests
author = OpenTelemetry Authors
author_email = cncf-opentelemetry-contributors@lists.cncf.io
url = https://github.com/open-telemetry/opentelemetry-python/ext/opentelemetry-ext-testutil
platforms = any
license = Apache-2.0
classifiers =
Development Status :: 4 - Beta
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
Programming Language :: Python :: 3.8

[options]
python_requires = >=3.4
package_dir=
=src
packages=find_namespace:
install_requires =
opentelemetry-api

[options.extras_require]
test = flask~=1.0

[options.packages.find]
where = src
26 changes: 26 additions & 0 deletions ext/opentelemetry-ext-testutil/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# 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", "ext", "testutil", "version.py"
)
PACKAGE_INFO = {}
with open(VERSION_FILENAME) as f:
exec(f.read(), PACKAGE_INFO)

setuptools.setup(version=PACKAGE_INFO["__version__"])
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__version__ = "0.7.dev0"
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import io
import unittest
import wsgiref.util as wsgiref_util
from importlib import reload

from opentelemetry import trace as trace_api
from opentelemetry.sdk.trace import TracerProvider, export
from opentelemetry.sdk.trace.export.in_memory_span_exporter import (
InMemorySpanExporter,
)

_MEMORY_EXPORTER = None


class WsgiTestBase(unittest.TestCase):
@classmethod
def setUpClass(cls):
global _MEMORY_EXPORTER # pylint:disable=global-statement
trace_api.set_tracer_provider(TracerProvider())
tracer_provider = trace_api.get_tracer_provider()
_MEMORY_EXPORTER = InMemorySpanExporter()
span_processor = export.SimpleExportSpanProcessor(_MEMORY_EXPORTER)
tracer_provider.add_span_processor(span_processor)

@classmethod
def tearDownClass(cls):
reload(trace_api)

def setUp(self):

self.memory_exporter = _MEMORY_EXPORTER
self.memory_exporter.clear()

self.write_buffer = io.BytesIO()
self.write = self.write_buffer.write

self.environ = {}
wsgiref_util.setup_testing_defaults(self.environ)

self.status = None
self.response_headers = None
self.exc_info = None

def start_response(self, status, response_headers, exc_info=None):
self.status = status
self.response_headers = response_headers
self.exc_info = exc_info
return self.write
15 changes: 15 additions & 0 deletions instrumentation/flask/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Changelog

## Unreleased

## 0.4a0

- Use string keys for WSGI environ values
([#366](https://github.com/open-telemetry/opentelemetry-python/pull/366))


## 0.3a0

Released 2019-12-11

- Initial release
13 changes: 13 additions & 0 deletions instrumentation/flask/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
OpenTelemetry Flask Instrumentor
================================

This instrumentor creates OpenTelemetry spans for Flask applications. It just
needs to be installed in the virtual environment where OpenTelemetry and the
Flask application are. The instrumentation of the Flask application will be
done automatically.

References
----------

* `OpenTelemetry Project <https://opentelemetry.io/>`_
* `OpenTelemetry WSGI extension <https://github.com/open-telemetry/opentelemetry-python/tree/master/ext/opentelemetry-ext-wsgi>`_
51 changes: 51 additions & 0 deletions instrumentation/flask/setup.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# 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.
#
[metadata]
name = opentelemetry-instrumentors-flask
description = OpenTelemetry Instrumentor for Flask
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/ext/opentelemetry-ext-flask
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:
install_requires =
flask~=1.0
opentelemetry-ext-wsgi
opentelemetry-auto-instrumentation

[options.extras_require]
test =
opentelemetry-ext-testutil

[options.packages.find]
where = src
39 changes: 39 additions & 0 deletions instrumentation/flask/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# 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 os.path import dirname, join

from setuptools import setup

PACKAGE_INFO = {}
with open(
join(
dirname(__file__),
"src",
"opentelemetry",
"instrumentation",
"flask",
"version.py",
)
) as f:
exec(f.read(), PACKAGE_INFO)

setup(
version=PACKAGE_INFO["__version__"],
entry_points={
"opentelemetry_auto_instrumentation_instrumentor": [
"flask = opentelemetry.ext.flask:FlaskInstrumentor"
]
},
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
# 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.

# Note: This package is not named "flask" because of
# https://github.com/PyCQA/pylint/issues/2648

import logging

import flask

from opentelemetry import context, propagators, trace
from opentelemetry.auto_instrumentation.instrumentor import BaseInstrumentor
from opentelemetry.ext.wsgi import (
add_response_attributes,
collect_request_attributes,
get_default_span_name,
get_header_from_environ,
)
from opentelemetry.instrumentation.flask.version import __version__
from opentelemetry.util import time_ns

logger = logging.getLogger(__name__)

_ENVIRON_STARTTIME_KEY = "opentelemetry-flask.starttime_key"
_ENVIRON_SPAN_KEY = "opentelemetry-flask.span_key"
_ENVIRON_ACTIVATION_KEY = "opentelemetry-flask.activation_key"
_ENVIRON_TOKEN = "opentelemetry-flask.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):
span = flask.request.environ.get(_ENVIRON_SPAN_KEY)
if span:
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 get_default_span_name(
environ
)
token = context.attach(
propagators.extract(get_header_from_environ, environ)
)

tracer = trace.get_tracer(__name__, __version__)

attributes = 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))


class FlaskInstrumentor(BaseInstrumentor):
"""A instrumentor for flask.Flask

See `BaseInstrumentor`
"""

def __init__(self):
super().__init__()
self._original_flask = None

def _instrument(self):
self._original_flask = flask.Flask
flask.Flask = _InstrumentedFlask

def _uninstrument(self):
flask.Flask = self._original_flask
Loading