@@ -141,6 +141,7 @@ def response_hook(span: Span, status: str, response_headers: List):
141
141
"""
142
142
143
143
from logging import getLogger
144
+ from timeit import default_timer
144
145
from typing import Collection
145
146
146
147
import flask
@@ -154,6 +155,7 @@ def response_hook(span: Span, status: str, response_headers: List):
154
155
get_global_response_propagator ,
155
156
)
156
157
from opentelemetry .instrumentation .utils import _start_internal_or_server_span
158
+ from opentelemetry .metrics import get_meter
157
159
from opentelemetry .semconv .trace import SpanAttributes
158
160
from opentelemetry .util ._time import _time_ns
159
161
from opentelemetry .util .http import get_excluded_urls , parse_excluded_urls
@@ -165,7 +167,6 @@ def response_hook(span: Span, status: str, response_headers: List):
165
167
_ENVIRON_ACTIVATION_KEY = "opentelemetry-flask.activation_key"
166
168
_ENVIRON_TOKEN = "opentelemetry-flask.token"
167
169
168
-
169
170
_excluded_urls_from_env = get_excluded_urls ("FLASK" )
170
171
171
172
@@ -178,13 +179,26 @@ def get_default_span_name():
178
179
return span_name
179
180
180
181
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
+ ):
182
189
def _wrapped_app (wrapped_app_environ , start_response ):
183
190
# We want to measure the time for route matching, etc.
184
191
# In theory, we could start the span here and use
185
192
# update_name later but that API is "highly discouraged" so
186
193
# we better avoid it.
187
194
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 )
188
202
189
203
def _start_response (status , response_headers , * args , ** kwargs ):
190
204
if flask .request and (
@@ -204,6 +218,11 @@ def _start_response(status, response_headers, *args, **kwargs):
204
218
otel_wsgi .add_response_attributes (
205
219
span , status , response_headers
206
220
)
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
207
226
if (
208
227
span .is_recording ()
209
228
and span .kind == trace .SpanKind .SERVER
@@ -223,13 +242,19 @@ def _start_response(status, response_headers, *args, **kwargs):
223
242
response_hook (span , status , response_headers )
224
243
return start_response (status , response_headers , * args , ** kwargs )
225
244
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
227
250
228
251
return _wrapped_app
229
252
230
253
231
254
def _wrapped_before_request (
232
- request_hook = None , tracer = None , excluded_urls = None
255
+ request_hook = None ,
256
+ tracer = None ,
257
+ excluded_urls = None ,
233
258
):
234
259
def _before_request ():
235
260
if excluded_urls and excluded_urls .url_disabled (flask .request .url ):
@@ -278,7 +303,9 @@ def _before_request():
278
303
return _before_request
279
304
280
305
281
- def _wrapped_teardown_request (excluded_urls = None ):
306
+ def _wrapped_teardown_request (
307
+ excluded_urls = None ,
308
+ ):
282
309
def _teardown_request (exc ):
283
310
# pylint: disable=E1101
284
311
if excluded_urls and excluded_urls .url_disabled (flask .request .url ):
@@ -290,7 +317,6 @@ def _teardown_request(exc):
290
317
# a way that doesn't run `before_request`, like when it is created
291
318
# with `app.test_request_context`.
292
319
return
293
-
294
320
if exc is None :
295
321
activation .__exit__ (None , None , None )
296
322
else :
@@ -310,15 +336,32 @@ class _InstrumentedFlask(flask.Flask):
310
336
_tracer_provider = None
311
337
_request_hook = None
312
338
_response_hook = None
339
+ _meter_provider = None
313
340
314
341
def __init__ (self , * args , ** kwargs ):
315
342
super ().__init__ (* args , ** kwargs )
316
343
317
344
self ._original_wsgi_app = self .wsgi_app
318
345
self ._is_instrumented_by_opentelemetry = True
319
346
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
+
320
361
self .wsgi_app = _rewrapped_app (
321
362
self .wsgi_app ,
363
+ active_requests_counter ,
364
+ duration_histogram ,
322
365
_InstrumentedFlask ._response_hook ,
323
366
excluded_urls = _InstrumentedFlask ._excluded_urls ,
324
367
)
@@ -367,6 +410,8 @@ def _instrument(self, **kwargs):
367
410
if excluded_urls is None
368
411
else parse_excluded_urls (excluded_urls )
369
412
)
413
+ meter_provider = kwargs .get ("meter_provider" )
414
+ _InstrumentedFlask ._meter_provider = meter_provider
370
415
flask .Flask = _InstrumentedFlask
371
416
372
417
def _uninstrument (self , ** kwargs ):
@@ -379,6 +424,7 @@ def instrument_app(
379
424
response_hook = None ,
380
425
tracer_provider = None ,
381
426
excluded_urls = None ,
427
+ meter_provider = None ,
382
428
):
383
429
if not hasattr (app , "_is_instrumented_by_opentelemetry" ):
384
430
app ._is_instrumented_by_opentelemetry = False
@@ -389,9 +435,25 @@ def instrument_app(
389
435
if excluded_urls is not None
390
436
else _excluded_urls_from_env
391
437
)
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
+
392
450
app ._original_wsgi_app = app .wsgi_app
393
451
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 ,
395
457
)
396
458
397
459
tracer = trace .get_tracer (__name__ , __version__ , tracer_provider )
0 commit comments