29
29
30
30
.. code-block:: python
31
31
32
- from opentelemetry.ext.flask import FlaskInstrumentor
33
- FlaskInstrumentor().instrument() # This needs to be executed before importing Flask
34
32
from flask import Flask
33
+ from opentelemetry.ext.flask import FlaskInstrumentor
35
34
36
35
app = Flask(__name__)
37
36
37
+ FlaskInstrumentor().instrument_app(app)
38
+
38
39
@app.route("/")
39
40
def hello():
40
41
return "Hello!"
@@ -46,7 +47,7 @@ def hello():
46
47
---
47
48
"""
48
49
49
- import logging
50
+ from logging import getLogger
50
51
51
52
import flask
52
53
@@ -60,110 +61,112 @@ def hello():
60
61
time_ns ,
61
62
)
62
63
63
- logger = logging . getLogger (__name__ )
64
+ _logger = getLogger (__name__ )
64
65
65
66
_ENVIRON_STARTTIME_KEY = "opentelemetry-flask.starttime_key"
66
67
_ENVIRON_SPAN_KEY = "opentelemetry-flask.span_key"
67
68
_ENVIRON_ACTIVATION_KEY = "opentelemetry-flask.activation_key"
68
69
_ENVIRON_TOKEN = "opentelemetry-flask.token"
69
70
70
71
72
+ def _rewrapped_app (wsgi_app ):
73
+ def _wrapped_app (environ , start_response ):
74
+ # We want to measure the time for route matching, etc.
75
+ # In theory, we could start the span here and use
76
+ # update_name later but that API is "highly discouraged" so
77
+ # we better avoid it.
78
+ environ [_ENVIRON_STARTTIME_KEY ] = time_ns ()
79
+
80
+ def _start_response (status , response_headers , * args , ** kwargs ):
81
+
82
+ if not _disable_trace (flask .request .url ):
83
+
84
+ span = flask .request .environ .get (_ENVIRON_SPAN_KEY )
85
+
86
+ if span :
87
+ otel_wsgi .add_response_attributes (
88
+ span , status , response_headers
89
+ )
90
+ else :
91
+ _logger .warning (
92
+ "Flask environ's OpenTelemetry span "
93
+ "missing at _start_response(%s)" ,
94
+ status ,
95
+ )
96
+
97
+ return start_response (status , response_headers , * args , ** kwargs )
98
+
99
+ return wsgi_app (environ , _start_response )
100
+
101
+ return _wrapped_app
102
+
103
+
104
+ def _before_request ():
105
+ if _disable_trace (flask .request .url ):
106
+ return
107
+
108
+ environ = flask .request .environ
109
+ span_name = flask .request .endpoint or otel_wsgi .get_default_span_name (
110
+ environ
111
+ )
112
+ token = context .attach (
113
+ propagators .extract (otel_wsgi .get_header_from_environ , environ )
114
+ )
115
+
116
+ tracer = trace .get_tracer (__name__ , __version__ )
117
+
118
+ attributes = otel_wsgi .collect_request_attributes (environ )
119
+ if flask .request .url_rule :
120
+ # For 404 that result from no route found, etc, we
121
+ # don't have a url_rule.
122
+ attributes ["http.route" ] = flask .request .url_rule .rule
123
+ span = tracer .start_span (
124
+ span_name ,
125
+ kind = trace .SpanKind .SERVER ,
126
+ attributes = attributes ,
127
+ start_time = environ .get (_ENVIRON_STARTTIME_KEY ),
128
+ )
129
+ activation = tracer .use_span (span , end_on_exit = True )
130
+ activation .__enter__ ()
131
+ environ [_ENVIRON_ACTIVATION_KEY ] = activation
132
+ environ [_ENVIRON_SPAN_KEY ] = span
133
+ environ [_ENVIRON_TOKEN ] = token
134
+
135
+
136
+ def _teardown_request (exc ):
137
+ activation = flask .request .environ .get (_ENVIRON_ACTIVATION_KEY )
138
+ if not activation :
139
+ _logger .warning (
140
+ "Flask environ's OpenTelemetry activation missing"
141
+ "at _teardown_flask_request(%s)" ,
142
+ exc ,
143
+ )
144
+ return
145
+
146
+ if exc is None :
147
+ activation .__exit__ (None , None , None )
148
+ else :
149
+ activation .__exit__ (
150
+ type (exc ), exc , getattr (exc , "__traceback__" , None )
151
+ )
152
+ context .detach (flask .request .environ .get (_ENVIRON_TOKEN ))
153
+
154
+
71
155
class _InstrumentedFlask (flask .Flask ):
72
156
def __init__ (self , * args , ** kwargs ):
73
-
74
157
super ().__init__ (* args , ** kwargs )
75
158
76
- # Single use variable here to avoid recursion issues.
77
- wsgi = self .wsgi_app
78
-
79
- def wrapped_app (environ , start_response ):
80
- # We want to measure the time for route matching, etc.
81
- # In theory, we could start the span here and use
82
- # update_name later but that API is "highly discouraged" so
83
- # we better avoid it.
84
- environ [_ENVIRON_STARTTIME_KEY ] = time_ns ()
85
-
86
- def _start_response (status , response_headers , * args , ** kwargs ):
87
- if not _disable_trace (flask .request .url ):
88
- span = flask .request .environ .get (_ENVIRON_SPAN_KEY )
89
- if span :
90
- otel_wsgi .add_response_attributes (
91
- span , status , response_headers
92
- )
93
- else :
94
- logger .warning (
95
- "Flask environ's OpenTelemetry span "
96
- "missing at _start_response(%s)" ,
97
- status ,
98
- )
99
-
100
- return start_response (
101
- status , response_headers , * args , ** kwargs
102
- )
103
-
104
- return wsgi (environ , _start_response )
105
-
106
- self .wsgi_app = wrapped_app
107
-
108
- @self .before_request
109
- def _before_flask_request ():
110
- # Do not trace if the url is excluded
111
- if _disable_trace (flask .request .url ):
112
- return
113
- environ = flask .request .environ
114
- span_name = (
115
- flask .request .endpoint
116
- or otel_wsgi .get_default_span_name (environ )
117
- )
118
- token = context .attach (
119
- propagators .extract (otel_wsgi .get_header_from_environ , environ )
120
- )
159
+ self ._original_wsgi_ = self .wsgi_app
160
+ self .wsgi_app = _rewrapped_app (self .wsgi_app )
121
161
122
- tracer = trace .get_tracer (__name__ , __version__ )
123
-
124
- attributes = otel_wsgi .collect_request_attributes (environ )
125
- if flask .request .url_rule :
126
- # For 404 that result from no route found, etc, we
127
- # don't have a url_rule.
128
- attributes ["http.route" ] = flask .request .url_rule .rule
129
- span = tracer .start_span (
130
- span_name ,
131
- kind = trace .SpanKind .SERVER ,
132
- attributes = attributes ,
133
- start_time = environ .get (_ENVIRON_STARTTIME_KEY ),
134
- )
135
- activation = tracer .use_span (span , end_on_exit = True )
136
- activation .__enter__ ()
137
- environ [_ENVIRON_ACTIVATION_KEY ] = activation
138
- environ [_ENVIRON_SPAN_KEY ] = span
139
- environ [_ENVIRON_TOKEN ] = token
140
-
141
- @self .teardown_request
142
- def _teardown_flask_request (exc ):
143
- # Not traced if the url is excluded
144
- if _disable_trace (flask .request .url ):
145
- return
146
- activation = flask .request .environ .get (_ENVIRON_ACTIVATION_KEY )
147
- if not activation :
148
- logger .warning (
149
- "Flask environ's OpenTelemetry activation missing"
150
- "at _teardown_flask_request(%s)" ,
151
- exc ,
152
- )
153
- return
154
-
155
- if exc is None :
156
- activation .__exit__ (None , None , None )
157
- else :
158
- activation .__exit__ (
159
- type (exc ), exc , getattr (exc , "__traceback__" , None )
160
- )
161
- context .detach (flask .request .environ .get (_ENVIRON_TOKEN ))
162
+ self .before_request (_before_request )
163
+ self .teardown_request (_teardown_request )
162
164
163
165
164
166
def _disable_trace (url ):
165
167
excluded_hosts = configuration .Configuration ().FLASK_EXCLUDED_HOSTS
166
168
excluded_paths = configuration .Configuration ().FLASK_EXCLUDED_PATHS
169
+
167
170
if excluded_hosts :
168
171
excluded_hosts = str .split (excluded_hosts , "," )
169
172
if disable_tracing_hostname (url , excluded_hosts ):
@@ -176,18 +179,50 @@ def _disable_trace(url):
176
179
177
180
178
181
class FlaskInstrumentor (BaseInstrumentor ):
179
- """A instrumentor for flask.Flask
182
+ # pylint: disable=protected-access,attribute-defined-outside-init
183
+ """An instrumentor for flask.Flask
180
184
181
185
See `BaseInstrumentor`
182
186
"""
183
187
184
- def __init__ (self ):
185
- super ().__init__ ()
186
- self ._original_flask = None
187
-
188
188
def _instrument (self , ** kwargs ):
189
189
self ._original_flask = flask .Flask
190
190
flask .Flask = _InstrumentedFlask
191
191
192
+ def instrument_app (self , app ): # pylint: disable=no-self-use
193
+ if not hasattr (app , "_is_instrumented" ):
194
+ app ._is_instrumented = False
195
+
196
+ if not app ._is_instrumented :
197
+ app ._original_wsgi_app = app .wsgi_app
198
+ app .wsgi_app = _rewrapped_app (app .wsgi_app )
199
+
200
+ app .before_request (_before_request )
201
+ app .teardown_request (_teardown_request )
202
+ app ._is_instrumented = True
203
+ else :
204
+ _logger .warning (
205
+ "Attempting to instrument Flask app while already instrumented"
206
+ )
207
+
192
208
def _uninstrument (self , ** kwargs ):
193
209
flask .Flask = self ._original_flask
210
+
211
+ def uninstrument_app (self , app ): # pylint: disable=no-self-use
212
+ if not hasattr (app , "_is_instrumented" ):
213
+ app ._is_instrumented = False
214
+
215
+ if app ._is_instrumented :
216
+ app .wsgi_app = app ._original_wsgi_app
217
+
218
+ # FIXME add support for other Flask blueprints that are not None
219
+ app .before_request_funcs [None ].remove (_before_request )
220
+ app .teardown_request_funcs [None ].remove (_teardown_request )
221
+ del app ._original_wsgi_app
222
+
223
+ app ._is_instrumented = False
224
+ else :
225
+ _logger .warning (
226
+ "Attempting to uninstrument Flask "
227
+ "app while already uninstrumented"
228
+ )
0 commit comments