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

Issue 333 #336

Closed
wants to merge 3 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: 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_patcher': [
'flask = opentelemetry.ext.flask:FlaskPatcher'
]
}
)
186 changes: 105 additions & 81 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 opentelemetry.ext.wsgi as otel_wsgi
from opentelemetry.patcher.base_patcher import BasePatcher
from opentelemetry import propagators, trace
from opentelemetry.ext.flask.version import __version__
from opentelemetry.util import time_ns
from opentelemetry.ext.flask.version import __version__
import flask

logger = logging.getLogger(__name__)

Expand All @@ -17,81 +17,105 @@
_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_source().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


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)
)
class FlaskPatcher(BasePatcher):

def patch(self):

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.tracer_source().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)
)

flask.Flask = PatchedFlask

def unpatch(self):
# FIXME this needs an actual implementation
pass


__all__ = ["FlaskPatcher"]
10 changes: 6 additions & 4 deletions ext/opentelemetry-ext-flask/tests/test_flask_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@
import unittest
from unittest import mock

from flask import Flask
import flask
from werkzeug.test import Client
from werkzeug.wrappers import BaseResponse

import opentelemetry.ext.flask as otel_flask
from opentelemetry.ext.flask import patch
from opentelemetry import trace as trace_api
from opentelemetry.ext.testutil.wsgitestutil import WsgiTestBase

Expand All @@ -36,7 +36,9 @@ def setspanattr(key, value):

self.span.set_attribute = setspanattr

self.app = Flask(__name__)
patch()

self.app = flask.Flask(__name__)

def hello_endpoint(helloid):
if helloid == 500:
Expand All @@ -45,7 +47,7 @@ def hello_endpoint(helloid):

self.app.route("/hello/<int:helloid>")(hello_endpoint)

otel_flask.instrument_app(self.app)
# otel_flask.instrument_app(self.app)
self.client = Client(self.app, BaseResponse)

def test_simple(self):
Expand Down
8 changes: 8 additions & 0 deletions opentelemetry-api/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,14 @@
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
],
entry_points={
"console_scripts": [
"auto_agent = opentelemetry.commands.auto_agent:run"
],
"opentelemetry_patcher": [
"no_op_patcher = opentelemetry.patcher.no_op_patcher:NoOpPatcher"
]
},
description="OpenTelemetry Python API",
include_package_data=True,
long_description=open("README.rst").read(),
Expand Down
Empty file.
20 changes: 20 additions & 0 deletions opentelemetry-api/src/opentelemetry/commands/auto_agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#!/usr/bin/env python3

from sys import exit, argv
from os import execl
import os
from os.path import dirname, join
from distutils.spawn import find_executable


def run():
os.environ['PYTHONPATH'] = join(dirname(__file__), 'initialize')
print(os.environ['PYTHONPATH'])

python3 = find_executable(argv[1])
execl(python3, python3, *argv[2:])
exit(0)


if __name__ == "__main__":
run()
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from pkg_resources import iter_entry_points
from logging import getLogger

_LOG = getLogger(__file__)

for entry_point in iter_entry_points("opentelemetry_patcher"):
try:
entry_point.load()().patch()

_LOG.debug("Patched {}".format(entry_point.name))

except Exception as error:

_LOG.exception(
"Patching of {} failed with: {}".format(entry_point.name, error)
)
13 changes: 13 additions & 0 deletions opentelemetry-api/src/opentelemetry/patcher/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# 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.
29 changes: 29 additions & 0 deletions opentelemetry-api/src/opentelemetry/patcher/base_patcher.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# 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.

from abc import ABC, abstractmethod


class BasePatcher(ABC):

@abstractmethod
def patch(self):
"""Patch"""

@abstractmethod
def unpatch(self):
"""Unpatch"""


__all__ = ["BasePatcher"]
27 changes: 27 additions & 0 deletions opentelemetry-api/src/opentelemetry/patcher/no_op_patcher.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# 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.

from opentelemetry.patcher.base_patcher import BasePatcher


class NoOpPatcher(BasePatcher):

def patch(self):
"""Patch"""

def unpatch(self):
"""Unpatch"""


__all__ = ["NoOpPatcher"]
Loading