2929
3030.. code-block:: python
3131
32- from opentelemetry.ext.flask import FlaskInstrumentor
33- FlaskInstrumentor().instrument() # This needs to be executed before importing Flask
3432 from flask import Flask
33+ from opentelemetry.ext.flask import FlaskInstrumentor
3534
3635 app = Flask(__name__)
3736
37+ FlaskInstrumentor().instrument_app(app)
38+
3839 @app.route("/")
3940 def hello():
4041 return "Hello!"
@@ -46,7 +47,7 @@ def hello():
4647---
4748"""
4849
49- import logging
50+ from logging import getLogger
5051
5152import flask
5253
@@ -60,110 +61,112 @@ def hello():
6061 time_ns ,
6162)
6263
63- logger = logging . getLogger (__name__ )
64+ _logger = getLogger (__name__ )
6465
6566_ENVIRON_STARTTIME_KEY = "opentelemetry-flask.starttime_key"
6667_ENVIRON_SPAN_KEY = "opentelemetry-flask.span_key"
6768_ENVIRON_ACTIVATION_KEY = "opentelemetry-flask.activation_key"
6869_ENVIRON_TOKEN = "opentelemetry-flask.token"
6970
7071
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+
71155class _InstrumentedFlask (flask .Flask ):
72156 def __init__ (self , * args , ** kwargs ):
73-
74157 super ().__init__ (* args , ** kwargs )
75158
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 )
121161
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 )
162164
163165
164166def _disable_trace (url ):
165167 excluded_hosts = configuration .Configuration ().FLASK_EXCLUDED_HOSTS
166168 excluded_paths = configuration .Configuration ().FLASK_EXCLUDED_PATHS
169+
167170 if excluded_hosts :
168171 excluded_hosts = str .split (excluded_hosts , "," )
169172 if disable_tracing_hostname (url , excluded_hosts ):
@@ -176,18 +179,50 @@ def _disable_trace(url):
176179
177180
178181class FlaskInstrumentor (BaseInstrumentor ):
179- """A instrumentor for flask.Flask
182+ # pylint: disable=protected-access,attribute-defined-outside-init
183+ """An instrumentor for flask.Flask
180184
181185 See `BaseInstrumentor`
182186 """
183187
184- def __init__ (self ):
185- super ().__init__ ()
186- self ._original_flask = None
187-
188188 def _instrument (self , ** kwargs ):
189189 self ._original_flask = flask .Flask
190190 flask .Flask = _InstrumentedFlask
191191
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+
192208 def _uninstrument (self , ** kwargs ):
193209 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