@@ -250,6 +250,51 @@ def response_hook(span: Span, status: str, response_headers: List):
250250| ``controller`` | Flask controller/endpoint name. | ``controller='home_view'`` |
251251+-------------------+----------------------------------------------------+----------------------------------------+
252252
253+ Custom Metrics Attributes using Labeler
254+ ***************************************
255+ The Flask instrumentation reads from a labeler utility that supports adding custom
256+ attributes to HTTP server metrics at emit time, including:
257+
258+ - Active requests counter (``http.server.active_requests``)
259+ - Duration histogram (``http.server.duration``)
260+ - Request duration histogram (``http.server.request.duration``)
261+
262+ The custom attributes are stored in the current OpenTelemetry context and are
263+ typically request-scoped for instrumented Flask handlers. In normal application
264+ flow, context detach at request teardown prevents these attributes from leaking
265+ to later requests. Application code typically should not call ``clear_labeler``;
266+ use it primarily for test isolation or manual context-lifecycle management. The
267+ instrumentor does not overwrite base attributes that exist at the same keys as
268+ any custom attributes.
269+
270+
271+ .. code-block:: python
272+
273+ from flask import Flask
274+
275+ from opentelemetry.instrumentation._labeler import get_labeler
276+ from opentelemetry.instrumentation.flask import FlaskInstrumentor
277+
278+ app = Flask(__name__)
279+ FlaskInstrumentor().instrument_app(app)
280+
281+ @app.route("/users/<user_id>/")
282+ def user_profile(user_id):
283+ # Get the labeler for the current request
284+ labeler = get_labeler()
285+
286+ # Add custom attributes to Flask instrumentation metrics
287+ labeler.add("user_id", user_id)
288+ labeler.add("user_type", "registered")
289+
290+ # Or, add multiple attributes at once
291+ labeler.add_attributes({
292+ "feature_flag": "new_ui",
293+ "experiment_group": "control"
294+ })
295+
296+ return f"User profile for {user_id}"
297+
253298API
254299---
255300"""
@@ -266,6 +311,10 @@ def response_hook(span: Span, status: str, response_headers: List):
266311
267312import opentelemetry .instrumentation .wsgi as otel_wsgi
268313from opentelemetry import context , trace
314+ from opentelemetry .instrumentation ._labeler import (
315+ enrich_metric_attributes ,
316+ get_labeler_attributes ,
317+ )
269318from opentelemetry .instrumentation ._semconv import (
270319 HTTP_DURATION_HISTOGRAM_BUCKETS_NEW ,
271320 _get_schema_url ,
@@ -312,6 +361,7 @@ def response_hook(span: Span, status: str, response_headers: List):
312361_ENVIRON_ACTIVATION_KEY = "opentelemetry-flask.activation_key"
313362_ENVIRON_REQCTX_REF_KEY = "opentelemetry-flask.reqctx_ref_key"
314363_ENVIRON_TOKEN = "opentelemetry-flask.token"
364+ _ENVIRON_LABELER_ATTRIBUTES_KEY = "opentelemetry-flask.labeler_attributes"
315365
316366_excluded_urls_from_env = get_excluded_urls ("FLASK" )
317367
@@ -351,6 +401,7 @@ def _rewrapped_app(
351401 duration_histogram_new = None ,
352402):
353403 # pylint: disable=too-many-statements
404+ # pylint: disable=too-many-locals
354405 def _wrapped_app (wrapped_app_environ , start_response ):
355406 # We want to measure the time for route matching, etc.
356407 # In theory, we could start the span here and use
@@ -367,6 +418,9 @@ def _wrapped_app(wrapped_app_environ, start_response):
367418 sem_conv_opt_in_mode ,
368419 )
369420 )
421+ active_requests_count_attrs = enrich_metric_attributes (
422+ active_requests_count_attrs
423+ )
370424
371425 active_requests_counter .add (1 , active_requests_count_attrs )
372426 request_route = None
@@ -437,6 +491,15 @@ def _start_response(status, response_headers, *args, **kwargs):
437491 if request_route :
438492 # http.target to be included in old semantic conventions
439493 duration_attrs_old [HTTP_TARGET ] = str (request_route )
494+ duration_attrs_old = enrich_metric_attributes (
495+ duration_attrs_old
496+ )
497+ labeler_metric_attributes = wrapped_app_environ .get (
498+ _ENVIRON_LABELER_ATTRIBUTES_KEY , {}
499+ )
500+ for key , value in labeler_metric_attributes .items ():
501+ if key not in duration_attrs_old :
502+ duration_attrs_old [key ] = value
440503 duration_histogram_old .record (
441504 max (round (duration_s * 1000 ), 0 ),
442505 duration_attrs_old ,
@@ -450,12 +513,32 @@ def _start_response(status, response_headers, *args, **kwargs):
450513 if request_route :
451514 duration_attrs_new [HTTP_ROUTE ] = str (request_route )
452515
516+ duration_attrs_new = enrich_metric_attributes (
517+ duration_attrs_new
518+ )
519+ labeler_metric_attributes = wrapped_app_environ .get (
520+ _ENVIRON_LABELER_ATTRIBUTES_KEY , {}
521+ )
522+ for key , value in labeler_metric_attributes .items ():
523+ if key not in duration_attrs_new :
524+ duration_attrs_new [key ] = value
525+
453526 duration_histogram_new .record (
454527 max (duration_s , 0 ),
455528 duration_attrs_new ,
456529 context = metrics_context ,
457530 )
458531
532+ active_requests_count_attrs = enrich_metric_attributes (
533+ active_requests_count_attrs
534+ )
535+ labeler_metric_attributes = wrapped_app_environ .get (
536+ _ENVIRON_LABELER_ATTRIBUTES_KEY , {}
537+ )
538+ for key , value in labeler_metric_attributes .items ():
539+ if key not in active_requests_count_attrs :
540+ active_requests_count_attrs [key ] = value
541+
459542 active_requests_counter .add (- 1 , active_requests_count_attrs )
460543 return result
461544
@@ -561,6 +644,9 @@ def _teardown_request(exc):
561644
562645 activation = flask .request .environ .get (_ENVIRON_ACTIVATION_KEY )
563646 token = flask .request .environ .get (_ENVIRON_TOKEN )
647+ flask .request .environ [_ENVIRON_LABELER_ATTRIBUTES_KEY ] = dict (
648+ get_labeler_attributes ()
649+ )
564650
565651 original_reqctx_ref = flask .request .environ .get (
566652 _ENVIRON_REQCTX_REF_KEY
0 commit comments