-
Notifications
You must be signed in to change notification settings - Fork 651
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add Flask integration based on WSGI ext (#206)
The flask integration has (only) two advantages over the plain WSGI middleware approach: - It can use the endpoint as span name (which is lower cardinality than the route; cf #270) - It can set the http.route attribute. In addition, it also has an easier syntax to enable (you don't have to know about Flask.wsgi_app).
- Loading branch information
Showing
18 changed files
with
603 additions
and
135 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
OpenTelemetry Flask tracing | ||
=========================== | ||
|
||
This library builds on the OpenTelemetry WSGI middleware to track web requests | ||
in Flask applications. In addition to opentelemetry-ext-wsgi, it supports | ||
flask-specific features such as: | ||
|
||
* The Flask endpoint name is used as the Span name. | ||
* The ``http.route`` Span attribute is set so that one can see which URL rule | ||
matched a request. | ||
|
||
Usage | ||
----- | ||
|
||
.. code-block:: python | ||
from flask import Flask | ||
from opentelemetry.ext.flask import instrument_app | ||
app = Flask(__name__) | ||
instrument_app(app) # This is where the magic happens. ✨ | ||
@app.route("/") | ||
def hello(): | ||
return "Hello!" | ||
if __name__ == "__main__": | ||
app.run(debug=True) | ||
References | ||
---------- | ||
|
||
* `OpenTelemetry Project <https://opentelemetry.io/>`_ | ||
* `OpenTelemetry WSGI extension <https://github.com/open-telemetry/opentelemetry-python/tree/master/ext/opentelemetry-ext-wsgi>`_ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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-ext-flask | ||
description = Flask tracing for OpenTelemetry (based on opentelemetry-ext-wsgi) | ||
long_description = file: README.rst | ||
long_description_content_type = text/x-rst | ||
author = OpenTelemetry Authors | ||
author_email = [email protected] | ||
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 = | ||
opentelemetry-ext-wsgi | ||
|
||
[options.extras_require] | ||
test = | ||
flask~=1.0 | ||
opentelemetry-ext-testutil | ||
|
||
[options.packages.find] | ||
where = src |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
# 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. | ||
import os | ||
|
||
import setuptools | ||
|
||
BASE_DIR = os.path.dirname(__file__) | ||
VERSION_FILENAME = os.path.join( | ||
BASE_DIR, "src", "opentelemetry", "ext", "flask", "version.py" | ||
) | ||
PACKAGE_INFO = {} | ||
with open(VERSION_FILENAME) as f: | ||
exec(f.read(), PACKAGE_INFO) | ||
|
||
setuptools.setup(version=PACKAGE_INFO["__version__"]) |
93 changes: 93 additions & 0 deletions
93
ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
# Note: This package is not named "flask" because of | ||
# https://github.com/PyCQA/pylint/issues/2648 | ||
|
||
import logging | ||
|
||
from flask import request as flask_request | ||
|
||
import opentelemetry.ext.wsgi as otel_wsgi | ||
from opentelemetry import propagators, trace | ||
from opentelemetry.util import time_ns | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
_ENVIRON_STARTTIME_KEY = object() | ||
_ENVIRON_SPAN_KEY = object() | ||
_ENVIRON_ACTIVATION_KEY = object() | ||
|
||
|
||
def instrument_app(flask): | ||
"""Makes the passed-in Flask object traced by OpenTelemetry. | ||
You must not call this function multiple times on the same Flask object. | ||
""" | ||
|
||
wsgi = flask.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) | ||
|
||
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 | ||
) | ||
parent_span = propagators.extract( | ||
otel_wsgi.get_header_from_environ, environ | ||
) | ||
|
||
tracer = trace.tracer() | ||
|
||
span = tracer.create_span( | ||
span_name, parent_span, kind=trace.SpanKind.SERVER | ||
) | ||
span.start(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 | ||
otel_wsgi.add_request_attributes(span, environ) | ||
if flask_request.url_rule: | ||
# For 404 that result from no route found, etc, we don't have a url_rule. | ||
span.set_attribute("http.route", flask_request.url_rule.rule) | ||
|
||
|
||
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) | ||
) |
15 changes: 15 additions & 0 deletions
15
ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/version.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
# 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. | ||
|
||
__version__ = "0.3dev0" |
Empty file.
136 changes: 136 additions & 0 deletions
136
ext/opentelemetry-ext-flask/tests/test_flask_integration.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
# 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. | ||
|
||
import unittest | ||
|
||
from flask import Flask | ||
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 | ||
|
||
|
||
class TestFlaskIntegration(WsgiTestBase): | ||
def setUp(self): | ||
super().setUp() | ||
|
||
self.span_attrs = {} | ||
|
||
def setspanattr(key, value): | ||
self.assertIsInstance(key, str) | ||
self.span_attrs[key] = value | ||
|
||
self.span.set_attribute = setspanattr | ||
|
||
self.app = Flask(__name__) | ||
|
||
def hello_endpoint(helloid): | ||
if helloid == 500: | ||
raise ValueError(":-(") | ||
return "Hello: " + str(helloid) | ||
|
||
self.app.route("/hello/<int:helloid>")(hello_endpoint) | ||
|
||
otel_flask.instrument_app(self.app) | ||
self.client = Client(self.app, BaseResponse) | ||
|
||
def test_simple(self): | ||
resp = self.client.get("/hello/123") | ||
self.assertEqual(200, resp.status_code) | ||
self.assertEqual([b"Hello: 123"], list(resp.response)) | ||
|
||
self.create_span.assert_called_with( | ||
"hello_endpoint", | ||
trace_api.INVALID_SPAN_CONTEXT, | ||
kind=trace_api.SpanKind.SERVER, | ||
) | ||
self.assertEqual(1, self.span.start.call_count) | ||
|
||
# TODO: Change this test to use the SDK, as mocking becomes painful | ||
|
||
self.assertEqual( | ||
self.span_attrs, | ||
{ | ||
"component": "http", | ||
"http.method": "GET", | ||
"http.host": "localhost", | ||
"http.url": "http://localhost/hello/123", | ||
"http.route": "/hello/<int:helloid>", | ||
"http.status_code": 200, | ||
"http.status_text": "OK", | ||
}, | ||
) | ||
|
||
def test_404(self): | ||
resp = self.client.post("/bye") | ||
self.assertEqual(404, resp.status_code) | ||
resp.close() | ||
|
||
self.create_span.assert_called_with( | ||
"/bye", | ||
trace_api.INVALID_SPAN_CONTEXT, | ||
kind=trace_api.SpanKind.SERVER, | ||
) | ||
self.assertEqual(1, self.span.start.call_count) | ||
|
||
# Nope, this uses Tracer.use_span(end_on_exit) | ||
# self.assertEqual(1, self.span.end.call_count) | ||
# TODO: Change this test to use the SDK, as mocking becomes painful | ||
|
||
self.assertEqual( | ||
self.span_attrs, | ||
{ | ||
"component": "http", | ||
"http.method": "POST", | ||
"http.host": "localhost", | ||
"http.url": "http://localhost/bye", | ||
"http.status_code": 404, | ||
"http.status_text": "NOT FOUND", | ||
}, | ||
) | ||
|
||
def test_internal_error(self): | ||
resp = self.client.get("/hello/500") | ||
self.assertEqual(500, resp.status_code) | ||
resp.close() | ||
|
||
self.create_span.assert_called_with( | ||
"hello_endpoint", | ||
trace_api.INVALID_SPAN_CONTEXT, | ||
kind=trace_api.SpanKind.SERVER, | ||
) | ||
self.assertEqual(1, self.span.start.call_count) | ||
|
||
# Nope, this uses Tracer.use_span(end_on_exit) | ||
# self.assertEqual(1, self.span.end.call_count) | ||
# TODO: Change this test to use the SDK, as mocking becomes painful | ||
|
||
self.assertEqual( | ||
self.span_attrs, | ||
{ | ||
"component": "http", | ||
"http.method": "GET", | ||
"http.host": "localhost", | ||
"http.url": "http://localhost/hello/500", | ||
"http.route": "/hello/<int:helloid>", | ||
"http.status_code": 500, | ||
"http.status_text": "INTERNAL SERVER ERROR", | ||
}, | ||
) | ||
|
||
|
||
if __name__ == "__main__": | ||
unittest.main() |
Oops, something went wrong.