forked from open-telemetry/opentelemetry-python
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy path__init__.py
More file actions
228 lines (176 loc) · 6.98 KB
/
__init__.py
File metadata and controls
228 lines (176 loc) · 6.98 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
# 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
"""
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 FlaskInstrumentor
app = Flask(__name__)
FlaskInstrumentor().instrument_app(app)
@app.route("/")
def hello():
return "Hello!"
if __name__ == "__main__":
app.run(debug=True)
API
---
"""
from logging import getLogger
import flask
import opentelemetry.ext.wsgi as otel_wsgi
from opentelemetry import configuration, context, propagators, trace
from opentelemetry.auto_instrumentation.instrumentor import BaseInstrumentor
from opentelemetry.ext.flask.version import __version__
from opentelemetry.util import (
disable_tracing_hostname,
disable_tracing_path,
time_ns,
)
_logger = 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"
def _rewrapped_app(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):
if not _disable_trace(flask.request.url):
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_app(environ, _start_response)
return _wrapped_app
def _before_request():
if _disable_trace(flask.request.url):
return
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_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 _InstrumentedFlask(flask.Flask):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._original_wsgi_ = self.wsgi_app
self.wsgi_app = _rewrapped_app(self.wsgi_app)
self.before_request(_before_request)
self.teardown_request(_teardown_request)
def _disable_trace(url):
excluded_hosts = configuration.Configuration().FLASK_EXCLUDED_HOSTS
excluded_paths = configuration.Configuration().FLASK_EXCLUDED_PATHS
if excluded_hosts:
excluded_hosts = str.split(excluded_hosts, ",")
if disable_tracing_hostname(url, excluded_hosts):
return True
if excluded_paths:
excluded_paths = str.split(excluded_paths, ",")
if disable_tracing_path(url, excluded_paths):
return True
return False
class FlaskInstrumentor(BaseInstrumentor):
# pylint: disable=protected-access,attribute-defined-outside-init
"""An instrumentor for flask.Flask
See `BaseInstrumentor`
"""
def _instrument(self, **kwargs):
self._original_flask = flask.Flask
flask.Flask = _InstrumentedFlask
def instrument_app(self, app): # pylint: disable=no-self-use
if not hasattr(app, "_is_instrumented"):
app._is_instrumented = False
if not app._is_instrumented:
app._original_wsgi_app = app.wsgi_app
app.wsgi_app = _rewrapped_app(app.wsgi_app)
app.before_request(_before_request)
app.teardown_request(_teardown_request)
app._is_instrumented = True
else:
_logger.warning(
"Attempting to instrument Flask app while already instrumented"
)
def _uninstrument(self, **kwargs):
flask.Flask = self._original_flask
def uninstrument_app(self, app): # pylint: disable=no-self-use
if not hasattr(app, "_is_instrumented"):
app._is_instrumented = False
if app._is_instrumented:
app.wsgi_app = app._original_wsgi_app
# FIXME add support for other Flask blueprints that are not None
app.before_request_funcs[None].remove(_before_request)
app.teardown_request_funcs[None].remove(_teardown_request)
del app._original_wsgi_app
app._is_instrumented = False
else:
_logger.warning(
"Attempting to uninstrument Flask "
"app while already uninstrumented"
)