Skip to content

Commit 26a12a3

Browse files
committed
Dynamically load patchers
Fixes open-telemetry#333
1 parent c385427 commit 26a12a3

File tree

8 files changed

+180
-84
lines changed

8 files changed

+180
-84
lines changed

ext/opentelemetry-ext-flask/setup.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,11 @@
2323
with open(VERSION_FILENAME) as f:
2424
exec(f.read(), PACKAGE_INFO)
2525

26-
setuptools.setup(version=PACKAGE_INFO["__version__"])
26+
setuptools.setup(
27+
version=PACKAGE_INFO["__version__"],
28+
entry_points={
29+
'opentelemetry_patcher': [
30+
'flask = opentelemetry.ext.flask:FlaskPatcher'
31+
]
32+
}
33+
)

ext/opentelemetry-ext-flask/src/opentelemetry/ext/flask/__init__.py

Lines changed: 85 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import logging
55

66
import opentelemetry.ext.wsgi as otel_wsgi
7+
from opentelemetry.patcher.base_patcher import BasePatcher
78
from opentelemetry import propagators, trace
89
from opentelemetry.util import time_ns
910
import flask
@@ -15,92 +16,100 @@
1516
_ENVIRON_ACTIVATION_KEY = "opentelemetry-flask.activation_key"
1617

1718

18-
def patch():
19+
class FlaskPatcher(BasePatcher):
1920

20-
class PatchedFlask(flask.Flask):
21+
def patch(self):
2122

22-
def __init__(self, *args, **kwargs):
23+
class PatchedFlask(flask.Flask):
2324

24-
super().__init__(*args, **kwargs)
25+
def __init__(self, *args, **kwargs):
2526

26-
# Single use variable here to avoid recursion issues.
27-
wsgi = self.wsgi_app
27+
super().__init__(*args, **kwargs)
2828

29-
def wrapped_app(environ, start_response):
30-
# We want to measure the time for route matching, etc.
31-
# In theory, we could start the span here and use
32-
# update_name later but that API is "highly discouraged" so
33-
# we better avoid it.
34-
environ[_ENVIRON_STARTTIME_KEY] = time_ns()
29+
# Single use variable here to avoid recursion issues.
30+
wsgi = self.wsgi_app
3531

36-
def _start_response(
37-
status, response_headers, *args, **kwargs
38-
):
39-
span = flask.request.environ.get(_ENVIRON_SPAN_KEY)
40-
if span:
41-
otel_wsgi.add_response_attributes(
42-
span, status, response_headers
43-
)
44-
else:
45-
logger.warning(
46-
"Flask environ's OpenTelemetry span "
47-
"missing at _start_response(%s)",
48-
status,
49-
)
32+
def wrapped_app(environ, start_response):
33+
# We want to measure the time for route matching, etc.
34+
# In theory, we could start the span here and use
35+
# update_name later but that API is "highly discouraged" so
36+
# we better avoid it.
37+
environ[_ENVIRON_STARTTIME_KEY] = time_ns()
5038

51-
return start_response(
39+
def _start_response(
5240
status, response_headers, *args, **kwargs
41+
):
42+
span = flask.request.environ.get(_ENVIRON_SPAN_KEY)
43+
if span:
44+
otel_wsgi.add_response_attributes(
45+
span, status, response_headers
46+
)
47+
else:
48+
logger.warning(
49+
"Flask environ's OpenTelemetry span "
50+
"missing at _start_response(%s)",
51+
status,
52+
)
53+
54+
return start_response(
55+
status, response_headers, *args, **kwargs
56+
)
57+
return wsgi(environ, _start_response)
58+
59+
self.wsgi_app = wrapped_app
60+
61+
@self.before_request
62+
def _before_flask_request():
63+
environ = flask.request.environ
64+
span_name = (
65+
flask.request.endpoint
66+
or otel_wsgi.get_default_span_name(environ)
5367
)
54-
return wsgi(environ, _start_response)
55-
56-
self.wsgi_app = wrapped_app
57-
58-
@self.before_request
59-
def _before_flask_request():
60-
environ = flask.request.environ
61-
span_name = (
62-
flask.request.endpoint
63-
or otel_wsgi.get_default_span_name(environ)
64-
)
65-
parent_span = propagators.extract(
66-
otel_wsgi.get_header_from_environ, environ
67-
)
68-
69-
tracer = trace.tracer(__name__, __version__)
70-
71-
attributes = otel_wsgi.collect_request_attributes(environ)
72-
if flask.request.url_rule:
73-
# For 404 that result from no route found, etc, we don't
74-
# have a url_rule.
75-
attributes["http.route"] = flask.request.url_rule.rule
76-
span = tracer.start_span(
77-
span_name,
78-
parent_span,
79-
kind=trace.SpanKind.SERVER,
80-
attributes=attributes,
81-
start_time=environ.get(_ENVIRON_STARTTIME_KEY),
82-
)
83-
activation = tracer.use_span(span, end_on_exit=True)
84-
activation.__enter__()
85-
environ[_ENVIRON_ACTIVATION_KEY] = activation
86-
environ[_ENVIRON_SPAN_KEY] = span
87-
88-
@self.teardown_request
89-
def _teardown_flask_request(exc):
90-
activation = flask.request.environ.get(_ENVIRON_ACTIVATION_KEY)
91-
if not activation:
92-
logger.warning(
93-
"Flask environ's OpenTelemetry activation missing at "
94-
"_teardown_flask_request(%s)",
95-
exc,
68+
parent_span = propagators.extract(
69+
otel_wsgi.get_header_from_environ, environ
9670
)
97-
return
9871

99-
if exc is None:
100-
activation.__exit__(None, None, None)
101-
else:
102-
activation.__exit__(
103-
type(exc), exc, getattr(exc, "__traceback__", None)
72+
tracer = trace.tracer(__name__, __version__)
73+
74+
attributes = otel_wsgi.collect_request_attributes(environ)
75+
if flask.request.url_rule:
76+
# For 404 that result from no route found, etc, we
77+
# don't have a url_rule.
78+
attributes["http.route"] = flask.request.url_rule.rule
79+
span = tracer.start_span(
80+
span_name,
81+
parent_span,
82+
kind=trace.SpanKind.SERVER,
83+
attributes=attributes,
84+
start_time=environ.get(_ENVIRON_STARTTIME_KEY),
10485
)
86+
activation = tracer.use_span(span, end_on_exit=True)
87+
activation.__enter__()
88+
environ[_ENVIRON_ACTIVATION_KEY] = activation
89+
environ[_ENVIRON_SPAN_KEY] = span
90+
91+
@self.teardown_request
92+
def _teardown_flask_request(exc):
93+
activation = flask.request.environ.get(
94+
_ENVIRON_ACTIVATION_KEY
95+
)
96+
if not activation:
97+
logger.warning(
98+
"Flask environ's OpenTelemetry activation missing"
99+
"at _teardown_flask_request(%s)",
100+
exc,
101+
)
102+
return
103+
104+
if exc is None:
105+
activation.__exit__(None, None, None)
106+
else:
107+
activation.__exit__(
108+
type(exc), exc, getattr(exc, "__traceback__", None)
109+
)
110+
111+
flask.Flask = PatchedFlask
105112

106-
flask.Flask = PatchedFlask
113+
def unpatch(self):
114+
# FIXME this needs an actual implementation
115+
pass

opentelemetry-api/setup.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@
4343
entry_points={
4444
"console_scripts": [
4545
"auto_agent = opentelemetry.commands.auto_agent:run"
46+
],
47+
"opentelemetry_patcher": [
48+
"no_op_patcher = opentelemetry.patcher.no_op_patcher:NoOpPatcher"
4649
]
4750
},
4851
description="OpenTelemetry Python API",

opentelemetry-api/src/opentelemetry/commands/auto_agent.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ def run():
1111
os.environ['PYTHONPATH'] = join(dirname(__file__), 'initialize')
1212
print(os.environ['PYTHONPATH'])
1313

14-
python3 = find_executable('python3')
15-
execl(python3, python3, argv[1])
14+
python3 = find_executable(argv[1])
15+
execl(python3, python3, *argv[2:])
1616
exit(0)
1717

1818

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
1-
from opentelemetry.ext.flask import patch
1+
from pkg_resources import iter_entry_points
2+
from logging import getLogger
23

3-
try:
4-
patch()
4+
_LOG = getLogger(__file__)
55

6-
except Exception as error:
6+
for entry_point in iter_entry_points("opentelemetry_patcher"):
7+
try:
8+
entry_point.load()().patch()
79

8-
print(error)
10+
_LOG.debug("Patched {}".format(entry_point.name))
11+
12+
except Exception as error:
13+
14+
_LOG.exception(
15+
"Patching of {} failed with: {}".format(entry_point.name, error)
16+
)
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Copyright 2019, OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Copyright 2019, OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from abc import ABC, abstractmethod
16+
17+
18+
class BasePatcher(ABC):
19+
20+
@abstractmethod
21+
def patch(self):
22+
"""Patch"""
23+
24+
@abstractmethod
25+
def unpatch(self):
26+
"""Unpatch"""
27+
28+
29+
__all__ = ["BasePatcher"]
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Copyright 2019, OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from opentelemetry.patcher.base_patcher import BasePatcher
16+
17+
18+
class NoOpPatcher(BasePatcher):
19+
20+
def patch(self):
21+
"""Patch"""
22+
23+
def unpatch(self):
24+
"""Unpatch"""
25+
26+
27+
__all__ = ["NoOpPatcher"]

0 commit comments

Comments
 (0)