@@ -141,6 +141,7 @@ def response_hook(span: Span, status: str, response_headers: List):
141141"""
142142
143143from logging import getLogger
144+ from timeit import default_timer
144145from typing import Collection
145146
146147import flask
@@ -154,6 +155,7 @@ def response_hook(span: Span, status: str, response_headers: List):
154155 get_global_response_propagator ,
155156)
156157from opentelemetry .instrumentation .utils import _start_internal_or_server_span
158+ from opentelemetry .metrics import get_meter
157159from opentelemetry .semconv .trace import SpanAttributes
158160from opentelemetry .util ._time import _time_ns
159161from opentelemetry .util .http import get_excluded_urls , parse_excluded_urls
@@ -165,7 +167,6 @@ def response_hook(span: Span, status: str, response_headers: List):
165167_ENVIRON_ACTIVATION_KEY = "opentelemetry-flask.activation_key"
166168_ENVIRON_TOKEN = "opentelemetry-flask.token"
167169
168-
169170_excluded_urls_from_env = get_excluded_urls ("FLASK" )
170171
171172
@@ -178,13 +179,26 @@ def get_default_span_name():
178179 return span_name
179180
180181
181- def _rewrapped_app (wsgi_app , response_hook = None , excluded_urls = None ):
182+ def _rewrapped_app (
183+ wsgi_app ,
184+ active_requests_counter ,
185+ duration_histogram ,
186+ response_hook = None ,
187+ excluded_urls = None ,
188+ ):
182189 def _wrapped_app (wrapped_app_environ , start_response ):
183190 # We want to measure the time for route matching, etc.
184191 # In theory, we could start the span here and use
185192 # update_name later but that API is "highly discouraged" so
186193 # we better avoid it.
187194 wrapped_app_environ [_ENVIRON_STARTTIME_KEY ] = _time_ns ()
195+ start = default_timer ()
196+ attributes = otel_wsgi .collect_request_attributes (wrapped_app_environ )
197+ active_requests_count_attrs = (
198+ otel_wsgi ._parse_active_request_count_attrs (attributes )
199+ )
200+ duration_attrs = otel_wsgi ._parse_duration_attrs (attributes )
201+ active_requests_counter .add (1 , active_requests_count_attrs )
188202
189203 def _start_response (status , response_headers , * args , ** kwargs ):
190204 if flask .request and (
@@ -204,6 +218,11 @@ def _start_response(status, response_headers, *args, **kwargs):
204218 otel_wsgi .add_response_attributes (
205219 span , status , response_headers
206220 )
221+ status_code = otel_wsgi ._parse_status_code (status )
222+ if status_code is not None :
223+ duration_attrs [
224+ SpanAttributes .HTTP_STATUS_CODE
225+ ] = status_code
207226 if (
208227 span .is_recording ()
209228 and span .kind == trace .SpanKind .SERVER
@@ -223,13 +242,19 @@ def _start_response(status, response_headers, *args, **kwargs):
223242 response_hook (span , status , response_headers )
224243 return start_response (status , response_headers , * args , ** kwargs )
225244
226- return wsgi_app (wrapped_app_environ , _start_response )
245+ result = wsgi_app (wrapped_app_environ , _start_response )
246+ duration = max (round ((default_timer () - start ) * 1000 ), 0 )
247+ duration_histogram .record (duration , duration_attrs )
248+ active_requests_counter .add (- 1 , active_requests_count_attrs )
249+ return result
227250
228251 return _wrapped_app
229252
230253
231254def _wrapped_before_request (
232- request_hook = None , tracer = None , excluded_urls = None
255+ request_hook = None ,
256+ tracer = None ,
257+ excluded_urls = None ,
233258):
234259 def _before_request ():
235260 if excluded_urls and excluded_urls .url_disabled (flask .request .url ):
@@ -278,7 +303,9 @@ def _before_request():
278303 return _before_request
279304
280305
281- def _wrapped_teardown_request (excluded_urls = None ):
306+ def _wrapped_teardown_request (
307+ excluded_urls = None ,
308+ ):
282309 def _teardown_request (exc ):
283310 # pylint: disable=E1101
284311 if excluded_urls and excluded_urls .url_disabled (flask .request .url ):
@@ -290,7 +317,6 @@ def _teardown_request(exc):
290317 # a way that doesn't run `before_request`, like when it is created
291318 # with `app.test_request_context`.
292319 return
293-
294320 if exc is None :
295321 activation .__exit__ (None , None , None )
296322 else :
@@ -310,15 +336,32 @@ class _InstrumentedFlask(flask.Flask):
310336 _tracer_provider = None
311337 _request_hook = None
312338 _response_hook = None
339+ _meter_provider = None
313340
314341 def __init__ (self , * args , ** kwargs ):
315342 super ().__init__ (* args , ** kwargs )
316343
317344 self ._original_wsgi_app = self .wsgi_app
318345 self ._is_instrumented_by_opentelemetry = True
319346
347+ meter = get_meter (
348+ __name__ , __version__ , _InstrumentedFlask ._meter_provider
349+ )
350+ duration_histogram = meter .create_histogram (
351+ name = "http.server.duration" ,
352+ unit = "ms" ,
353+ description = "measures the duration of the inbound HTTP request" ,
354+ )
355+ active_requests_counter = meter .create_up_down_counter (
356+ name = "http.server.active_requests" ,
357+ unit = "requests" ,
358+ description = "measures the number of concurrent HTTP requests that are currently in-flight" ,
359+ )
360+
320361 self .wsgi_app = _rewrapped_app (
321362 self .wsgi_app ,
363+ active_requests_counter ,
364+ duration_histogram ,
322365 _InstrumentedFlask ._response_hook ,
323366 excluded_urls = _InstrumentedFlask ._excluded_urls ,
324367 )
@@ -367,6 +410,8 @@ def _instrument(self, **kwargs):
367410 if excluded_urls is None
368411 else parse_excluded_urls (excluded_urls )
369412 )
413+ meter_provider = kwargs .get ("meter_provider" )
414+ _InstrumentedFlask ._meter_provider = meter_provider
370415 flask .Flask = _InstrumentedFlask
371416
372417 def _uninstrument (self , ** kwargs ):
@@ -379,6 +424,7 @@ def instrument_app(
379424 response_hook = None ,
380425 tracer_provider = None ,
381426 excluded_urls = None ,
427+ meter_provider = None ,
382428 ):
383429 if not hasattr (app , "_is_instrumented_by_opentelemetry" ):
384430 app ._is_instrumented_by_opentelemetry = False
@@ -389,9 +435,25 @@ def instrument_app(
389435 if excluded_urls is not None
390436 else _excluded_urls_from_env
391437 )
438+ meter = get_meter (__name__ , __version__ , meter_provider )
439+ duration_histogram = meter .create_histogram (
440+ name = "http.server.duration" ,
441+ unit = "ms" ,
442+ description = "measures the duration of the inbound HTTP request" ,
443+ )
444+ active_requests_counter = meter .create_up_down_counter (
445+ name = "http.server.active_requests" ,
446+ unit = "requests" ,
447+ description = "measures the number of concurrent HTTP requests that are currently in-flight" ,
448+ )
449+
392450 app ._original_wsgi_app = app .wsgi_app
393451 app .wsgi_app = _rewrapped_app (
394- app .wsgi_app , response_hook , excluded_urls = excluded_urls
452+ app .wsgi_app ,
453+ active_requests_counter ,
454+ duration_histogram ,
455+ response_hook ,
456+ excluded_urls = excluded_urls ,
395457 )
396458
397459 tracer = trace .get_tracer (__name__ , __version__ , tracer_provider )
0 commit comments