# Copyright The OpenTelemetry Authors
# SPDX-License-Identifier: Apache-2.0
# Note: This package is not named "flask" because of
# https://github.com/PyCQA/pylint/issues/2648
"""
This library builds on the OpenTelemetry WSGI middleware to track web requests
in Flask applications. In addition to opentelemetry-util-http, it
supports Flask-specific features such as:
* The Flask url rule pattern is used as the Span name.
* The ``http.route`` Span attribute is set so that one can see which URL rule
matched a request.
Usage
-----
.. code-block:: python
from flask import Flask
from opentelemetry.instrumentation.flask import FlaskInstrumentor
app = Flask(__name__)
FlaskInstrumentor().instrument_app(app)
@app.route("/")
def hello():
return "Hello!"
if __name__ == "__main__":
app.run(debug=True)
Configuration
-------------
Exclude lists
*************
To exclude certain URLs from tracking, set the environment variable ``OTEL_PYTHON_FLASK_EXCLUDED_URLS``
(or ``OTEL_PYTHON_EXCLUDED_URLS`` to cover all instrumentations) to a string of comma delimited regexes that match the
URLs.
For example,
::
export OTEL_PYTHON_FLASK_EXCLUDED_URLS="client/.*/info,healthcheck"
will exclude requests such as ``https://site/client/123/info`` and ``https://site/xyz/healthcheck``.
You can also pass comma delimited regexes directly to the ``instrument_app`` method:
.. code-block:: python
FlaskInstrumentor().instrument_app(app, excluded_urls="client/.*/info,healthcheck")
Request/Response hooks
**********************
This instrumentation supports request and response hooks. These are functions that get called
right after a span is created for a request and right before the span is finished for the response.
- The client request hook is called with the internal span and an instance of WSGIEnvironment (flask.request.environ)
when the method ``receive`` is called.
- The client response hook is called with the internal span, the status of the response and a list of key-value (tuples)
representing the response headers returned from the response when the method ``send`` is called.
For example,
.. code-block:: python
from opentelemetry.trace import Span
from wsgiref.types import WSGIEnvironment
from typing import List
from opentelemetry.instrumentation.flask import FlaskInstrumentor
def request_hook(span: Span, environ: WSGIEnvironment):
if span and span.is_recording():
span.set_attribute("custom_user_attribute_from_request_hook", "some-value")
def response_hook(span: Span, status: str, response_headers: List):
if span and span.is_recording():
span.set_attribute("custom_user_attribute_from_response_hook", "some-value")
FlaskInstrumentor().instrument(request_hook=request_hook, response_hook=response_hook)
Flask Request object reference: https://flask.palletsprojects.com/en/2.1.x/api/#flask.Request
Capture HTTP request and response headers
*****************************************
You can configure the agent to capture specified HTTP headers as span attributes, according to the
`semantic conventions <https://github.com/open-telemetry/semantic-conventions/blob/main/docs/http/http-spans.md#http-server-span>`_.
Request headers
***************
To capture HTTP request headers as span attributes, set the environment variable
``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST`` to a comma delimited list of HTTP header names.
For example,
::
export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST="content-type,custom_request_header"
will extract ``content-type`` and ``custom_request_header`` from the request headers and add them as span attributes.
Request header names in Flask are case-insensitive and ``-`` characters are replaced by ``_``. So, giving the header
name as ``CUStom_Header`` in the environment variable will capture the header named ``custom-header``.
Regular expressions may also be used to match multiple headers that correspond to the given pattern. For example:
::
export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST="Accept.*,X-.*"
Would match all request headers that start with ``Accept`` and ``X-``.
To capture all request headers, set ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST`` to ``".*"``.
::
export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST=".*"
The name of the added span attribute will follow the format ``http.request.header.<header_name>`` where ``<header_name>``
is the normalized HTTP header name (lowercase, with ``-`` replaced by ``_``). The value of the attribute will be a
single item list containing all the header values.
For example:
``http.request.header.custom_request_header = ["<value1>,<value2>"]``
Response headers
****************
To capture HTTP response headers as span attributes, set the environment variable
``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE`` to a comma delimited list of HTTP header names.
For example,
::
export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE="content-type,custom_response_header"
will extract ``content-type`` and ``custom_response_header`` from the response headers and add them as span attributes.
Response header names in Flask are case-insensitive. So, giving the header name as ``CUStom-Header`` in the environment
variable will capture the header named ``custom-header``.
Regular expressions may also be used to match multiple headers that correspond to the given pattern. For example:
::
export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE="Content.*,X-.*"
Would match all response headers that start with ``Content`` and ``X-``.
To capture all response headers, set ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE`` to ``".*"``.
::
export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE=".*"
The name of the added span attribute will follow the format ``http.response.header.<header_name>`` where ``<header_name>``
is the normalized HTTP header name (lowercase, with ``-`` replaced by ``_``). The value of the attribute will be a
single item list containing all the header values.
For example:
``http.response.header.custom_response_header = ["<value1>,<value2>"]``
Sanitizing headers
******************
In order to prevent storing sensitive data such as personally identifiable information (PII), session keys, passwords,
etc, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS``
to a comma delimited list of HTTP header names to be sanitized. Regexes may be used, and all header names will be
matched in a case-insensitive manner.
For example,
::
export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS=".*session.*,set-cookie"
will replace the value of headers such as ``session-id`` and ``set-cookie`` with ``[REDACTED]`` in the span.
Note:
The environment variable names used to capture HTTP headers are still experimental, and thus are subject to change.
SQLCommenter
************
You can optionally enable sqlcommenter which enriches the query with contextual
information. Queries made after setting up trace integration with sqlcommenter
enabled will have configurable key-value pairs appended to them, e.g.
``"select * from auth_users; /*framework=flask%%3A2.9.3*/"``. This
supports context propagation between database client and server when database log
records are enabled. For more information, see:
* `Semantic Conventions - Database Spans <https://github.com/open-telemetry/semantic-conventions/blob/main/docs/db/database-spans.md#sql-commenter>`_
* `sqlcommenter <https://google.github.io/sqlcommenter/>`_
.. code:: python
from opentelemetry.instrumentation.flask import FlaskInstrumentor
FlaskInstrumentor().instrument(enable_commenter=True)
Note:
FlaskInstrumentor sqlcommenter requires that sqlcommenter is also
enabled for an active instrumentation of a database driver or object-relational
mapper (ORM) in the same database client stack. The latter, such as
Psycopg2Instrumentor of SQLAlchemyInstrumentor, will create a base sqlcomment
that is enhanced by FlaskInstrumentor with additional values from context
before appending to the query statement.
SQLCommenter with commenter_options
***********************************
The key-value pairs appended to the query can be configured using
``commenter_options``. When sqlcommenter is enabled, all available KVs/tags
are calculated by default. ``commenter_options`` supports *opting out*
of specific KVs.
.. code:: python
from opentelemetry.instrumentation.flask import FlaskInstrumentor
# Opts into sqlcomment for Flask trace integration.
# Opts out of tags for controller.
FlaskInstrumentor().instrument(
enable_commenter=True,
commenter_options={
"controller": False,
}
)
Available commenter_options
###########################
The following sqlcomment key-values can be opted out of through ``commenter_options``:
+-------------------+----------------------------------------------------+----------------------------------------+
| Commenter Option | Description | Example |
+===================+====================================================+========================================+
| ``framework`` | Flask framework name with version (URL encoded). | ``framework='flask%%%%3A2.9.3'`` |
+-------------------+----------------------------------------------------+----------------------------------------+
| ``route`` | Flask route URI pattern. | ``route='/home'`` |
+-------------------+----------------------------------------------------+----------------------------------------+
| ``controller`` | Flask controller/endpoint name. | ``controller='home_view'`` |
+-------------------+----------------------------------------------------+----------------------------------------+
API
---
"""
import weakref
from logging import getLogger
from time import time_ns
from timeit import default_timer
from typing import Collection
import flask
from packaging import version as package_version
import opentelemetry.instrumentation.wsgi as otel_wsgi
from opentelemetry import context, trace
from opentelemetry.instrumentation._semconv import (
HTTP_DURATION_HISTOGRAM_BUCKETS_NEW,
_get_schema_url,
_OpenTelemetrySemanticConventionStability,
_OpenTelemetryStabilitySignalType,
_report_new,
_report_old,
_StabilityMode,
)
from opentelemetry.instrumentation.flask.package import _instruments
from opentelemetry.instrumentation.flask.version import __version__
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
from opentelemetry.instrumentation.propagators import (
get_global_response_propagator,
)
from opentelemetry.instrumentation.utils import _start_internal_or_server_span
from opentelemetry.metrics import get_meter
from opentelemetry.semconv._incubating.attributes.http_attributes import (
HTTP_ROUTE,
HTTP_TARGET,
)
from opentelemetry.semconv._incubating.metrics.http_metrics import (
create_http_server_active_requests,
)
from opentelemetry.semconv.metrics import MetricInstruments
from opentelemetry.semconv.metrics.http_metrics import (
HTTP_SERVER_REQUEST_DURATION,
)
from opentelemetry.util._importlib_metadata import version
from opentelemetry.util.http import (
get_excluded_urls,
parse_excluded_urls,
sanitize_method,
)
_logger = getLogger(__name__)
_ENVIRON_STARTTIME_KEY = "opentelemetry-flask.starttime_key"
_ENVIRON_SPAN_KEY = "opentelemetry-flask.span_key"
_ENVIRON_ACTIVATION_KEY = "opentelemetry-flask.activation_key"
_ENVIRON_REQCTX_REF_KEY = "opentelemetry-flask.reqctx_ref_key"
_ENVIRON_TOKEN = "opentelemetry-flask.token"
_excluded_urls_from_env = get_excluded_urls("FLASK")
flask_version = version("flask")
# Global constant for Flask 3.1+ streaming context cleanup
_IS_FLASK_31_PLUS = package_version.parse(
flask_version
) >= package_version.parse("3.1.0")
if package_version.parse(flask_version) >= package_version.parse("2.2.0"):
def _request_ctx_ref() -> weakref.ReferenceType:
return weakref.ref(flask.globals.request_ctx._get_current_object())
else:
def _request_ctx_ref() -> weakref.ReferenceType:
return weakref.ref(flask._request_ctx_stack.top)
[docs]def get_default_span_name():
method = sanitize_method(
flask.request.environ.get("REQUEST_METHOD", "").strip()
)
if method == "_OTHER":
method = "HTTP"
try:
span_name = f"{method} {flask.request.url_rule.rule}"
except AttributeError:
span_name = otel_wsgi.get_default_span_name(flask.request.environ)
return span_name
def _rewrapped_app(
wsgi_app,
active_requests_counter,
duration_histogram_old=None,
response_hook=None,
excluded_urls=None,
sem_conv_opt_in_mode=_StabilityMode.DEFAULT,
duration_histogram_new=None,
):
# pylint: disable=too-many-statements
def _wrapped_app(wrapped_app_environ, start_response):
# We want to measure the time for route matching, etc.
# In theory, we could start the span here and use
# update_name later but that API is "highly discouraged" so
# we better avoid it.
wrapped_app_environ[_ENVIRON_STARTTIME_KEY] = time_ns()
start = default_timer()
attributes = otel_wsgi.collect_request_attributes(
wrapped_app_environ, sem_conv_opt_in_mode
)
active_requests_count_attrs = (
otel_wsgi._parse_active_request_count_attrs(
attributes,
sem_conv_opt_in_mode,
)
)
active_requests_counter.add(1, active_requests_count_attrs)
request_route = None
should_trace = True
def _start_response(status, response_headers, *args, **kwargs):
nonlocal should_trace
should_trace = _should_trace(excluded_urls)
if should_trace:
nonlocal request_route
request_route = flask.request.url_rule
span = flask.request.environ.get(_ENVIRON_SPAN_KEY)
propagator = get_global_response_propagator()
if propagator:
propagator.inject(
response_headers,
setter=otel_wsgi.default_response_propagation_setter,
)
if span:
otel_wsgi.add_response_attributes(
span,
status,
response_headers,
attributes,
sem_conv_opt_in_mode,
)
if (
span.is_recording()
and span.kind == trace.SpanKind.SERVER
):
custom_attributes = otel_wsgi.collect_custom_response_headers_attributes(
response_headers
)
if len(custom_attributes) > 0:
span.set_attributes(custom_attributes)
else:
_logger.warning(
"Flask environ's OpenTelemetry span "
"missing at _start_response(%s)",
status,
)
if response_hook is not None:
response_hook(span, status, response_headers)
return start_response(status, response_headers, *args, **kwargs)
result = wsgi_app(wrapped_app_environ, _start_response)
# Note: Streaming response context cleanup is now handled in the Flask teardown function
# (_wrapped_teardown_request) to ensure proper cleanup following Logfire's recommendations
# for OpenTelemetry generator context management
if should_trace:
duration_s = default_timer() - start
# Get the span from wrapped_app_environ and re-create context manually
# to pass to histogram for exemplars generation
span = wrapped_app_environ.get(_ENVIRON_SPAN_KEY)
metrics_context = trace.set_span_in_context(span)
if duration_histogram_old:
duration_attrs_old = otel_wsgi._parse_duration_attrs(
attributes, _StabilityMode.DEFAULT
)
if request_route:
# http.target to be included in old semantic conventions
duration_attrs_old[HTTP_TARGET] = str(request_route)
duration_histogram_old.record(
max(round(duration_s * 1000), 0),
duration_attrs_old,
context=metrics_context,
)
if duration_histogram_new:
duration_attrs_new = otel_wsgi._parse_duration_attrs(
attributes, _StabilityMode.HTTP
)
if request_route:
duration_attrs_new[HTTP_ROUTE] = str(request_route)
duration_histogram_new.record(
max(duration_s, 0),
duration_attrs_new,
context=metrics_context,
)
active_requests_counter.add(-1, active_requests_count_attrs)
return result
def _should_trace(excluded_urls) -> bool:
return bool(
flask.request
and (
excluded_urls is None
or not excluded_urls.url_disabled(flask.request.url)
)
)
return _wrapped_app
def _wrapped_before_request(
request_hook=None,
tracer=None,
excluded_urls=None,
enable_commenter=True,
commenter_options=None,
sem_conv_opt_in_mode=_StabilityMode.DEFAULT,
):
def _before_request():
if excluded_urls and excluded_urls.url_disabled(flask.request.url):
return
flask_request_environ = flask.request.environ
span_name = get_default_span_name()
attributes = otel_wsgi.collect_request_attributes(
flask_request_environ,
sem_conv_opt_in_mode=sem_conv_opt_in_mode,
)
if flask.request.url_rule:
# For 404 that result from no route found, etc, we
# don't have a url_rule.
attributes[HTTP_ROUTE] = flask.request.url_rule.rule
span, token = _start_internal_or_server_span(
tracer=tracer,
span_name=span_name,
start_time=flask_request_environ.get(_ENVIRON_STARTTIME_KEY),
context_carrier=flask_request_environ,
context_getter=otel_wsgi.wsgi_getter,
attributes=attributes,
)
if request_hook:
request_hook(span, flask_request_environ)
if span.is_recording():
for key, value in attributes.items():
span.set_attribute(key, value)
if span.is_recording() and span.kind == trace.SpanKind.SERVER:
custom_attributes = (
otel_wsgi.collect_custom_request_headers_attributes(
flask_request_environ
)
)
if len(custom_attributes) > 0:
span.set_attributes(custom_attributes)
activation = trace.use_span(span, end_on_exit=True)
activation.__enter__() # pylint: disable=unnecessary-dunder-call
flask_request_environ[_ENVIRON_ACTIVATION_KEY] = activation
flask_request_environ[_ENVIRON_REQCTX_REF_KEY] = _request_ctx_ref()
flask_request_environ[_ENVIRON_SPAN_KEY] = span
flask_request_environ[_ENVIRON_TOKEN] = token
if enable_commenter:
current_context = context.get_current()
flask_info = {}
# https://flask.palletsprojects.com/en/1.1.x/api/#flask.has_request_context
if flask and flask.request:
if commenter_options.get("framework", True):
flask_info["framework"] = f"flask:{flask_version}"
if (
commenter_options.get("controller", True)
and flask.request.endpoint
):
flask_info["controller"] = flask.request.endpoint
if (
commenter_options.get("route", True)
and flask.request.url_rule
and flask.request.url_rule.rule
):
flask_info["route"] = flask.request.url_rule.rule
sqlcommenter_context = context.set_value(
"SQLCOMMENTER_ORM_TAGS_AND_VALUES", flask_info, current_context
)
context.attach(sqlcommenter_context)
return _before_request
def _wrapped_teardown_request(
excluded_urls=None,
):
def _teardown_request(exc):
# pylint: disable=unnecessary-dunder-call
if excluded_urls and excluded_urls.url_disabled(flask.request.url):
return
activation = flask.request.environ.get(_ENVIRON_ACTIVATION_KEY)
token = flask.request.environ.get(_ENVIRON_TOKEN)
original_reqctx_ref = flask.request.environ.get(
_ENVIRON_REQCTX_REF_KEY
)
current_reqctx_ref = _request_ctx_ref()
if not activation or original_reqctx_ref != current_reqctx_ref:
# This request didn't start a span, maybe because it was created in
# a way that doesn't run `before_request`, like when it is created
# with `app.test_request_context`.
#
# Similarly, check that the request_ctx that created the span
# matches the current request_ctx, and only tear down if they match.
# This situation can arise if the original request_ctx handling
# the request calls functions that push new request_ctx's,
# like any decorated with `flask.copy_current_request_context`.
return
try:
# For Flask 3.1+, check if this is a streaming response that might
# have already been cleaned up to prevent double cleanup
is_streaming = False
if _IS_FLASK_31_PLUS:
try:
# Additional safety check: verify we're in a Flask request context
if hasattr(flask, "request") and hasattr(
flask.request, "response"
):
is_streaming = (
hasattr(flask.request, "response")
and flask.request.response
and hasattr(flask.request.response, "stream")
and flask.request.response.stream
)
except (RuntimeError, AttributeError):
# Not in a proper Flask request context, don't check for streaming
is_streaming = False
if _IS_FLASK_31_PLUS and is_streaming:
# For Flask 3.1+ streaming responses, ensure OpenTelemetry contexts are cleaned up
# This addresses the generator context leak issues documented by Logfire
# (open-telemetry/opentelemetry-python#2606)
try:
context.detach(token)
if hasattr(activation, "__exit__"):
activation.__exit__(None, None, None)
# Mark as cleaned up
flask.request.environ[_ENVIRON_ACTIVATION_KEY] = None
flask.request.environ[_ENVIRON_TOKEN] = None
_logger.debug(
"Streaming response context cleanup completed in teardown function"
)
except (
RuntimeError,
ValueError,
TypeError,
AttributeError,
) as cleanup_exc:
_logger.debug(
"Teardown streaming context cleanup failed: %s",
cleanup_exc,
)
return
if exc is None:
activation.__exit__(None, None, None)
else:
activation.__exit__(
type(exc), exc, getattr(exc, "__traceback__", None)
)
if token:
context.detach(token)
flask.request.environ.pop(_ENVIRON_ACTIVATION_KEY, None)
flask.request.environ.pop(_ENVIRON_TOKEN, None)
except (RuntimeError, AttributeError, ValueError) as teardown_exc:
# Log the error but don't raise it to avoid breaking the request handling
_logger.debug(
"Error during request teardown: %s",
teardown_exc,
exc_info=True,
)
return _teardown_request
class _InstrumentedFlask(flask.Flask):
_excluded_urls = None
_tracer_provider = None
_request_hook = None
_response_hook = None
_enable_commenter = True
_commenter_options = None
_meter_provider = None
_sem_conv_opt_in_mode = _StabilityMode.DEFAULT
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._original_wsgi_app = self.wsgi_app
self._is_instrumented_by_opentelemetry = True
meter = get_meter(
__name__,
__version__,
_InstrumentedFlask._meter_provider,
schema_url=_get_schema_url(
_InstrumentedFlask._sem_conv_opt_in_mode
),
)
duration_histogram_old = None
if _report_old(_InstrumentedFlask._sem_conv_opt_in_mode):
duration_histogram_old = meter.create_histogram(
name=MetricInstruments.HTTP_SERVER_DURATION,
unit="ms",
description="Measures the duration of inbound HTTP requests.",
)
duration_histogram_new = None
if _report_new(_InstrumentedFlask._sem_conv_opt_in_mode):
duration_histogram_new = meter.create_histogram(
name=HTTP_SERVER_REQUEST_DURATION,
unit="s",
description="Duration of HTTP server requests.",
explicit_bucket_boundaries_advisory=HTTP_DURATION_HISTOGRAM_BUCKETS_NEW,
)
if _report_new(_InstrumentedFlask._sem_conv_opt_in_mode):
active_requests_counter = create_http_server_active_requests(meter)
else:
active_requests_counter = meter.create_up_down_counter(
name=MetricInstruments.HTTP_SERVER_ACTIVE_REQUESTS,
unit="requests",
description="Measures the number of concurrent HTTP requests that are currently in-flight.",
)
self.wsgi_app = _rewrapped_app(
self.wsgi_app,
active_requests_counter,
duration_histogram_old,
_InstrumentedFlask._response_hook,
excluded_urls=_InstrumentedFlask._excluded_urls,
sem_conv_opt_in_mode=_InstrumentedFlask._sem_conv_opt_in_mode,
duration_histogram_new=duration_histogram_new,
)
tracer = trace.get_tracer(
__name__,
__version__,
_InstrumentedFlask._tracer_provider,
schema_url=_get_schema_url(
_InstrumentedFlask._sem_conv_opt_in_mode
),
)
_before_request = _wrapped_before_request(
_InstrumentedFlask._request_hook,
tracer,
excluded_urls=_InstrumentedFlask._excluded_urls,
enable_commenter=_InstrumentedFlask._enable_commenter,
commenter_options=_InstrumentedFlask._commenter_options,
sem_conv_opt_in_mode=_InstrumentedFlask._sem_conv_opt_in_mode,
)
self._before_request = _before_request
self.before_request(_before_request)
_teardown_request = _wrapped_teardown_request(
excluded_urls=_InstrumentedFlask._excluded_urls,
)
self.teardown_request(_teardown_request)
[docs]class FlaskInstrumentor(BaseInstrumentor):
# pylint: disable=protected-access
"""An instrumentor for flask.Flask
See `BaseInstrumentor`
"""
[docs] def instrumentation_dependencies(self) -> Collection[str]:
return _instruments
def _instrument(self, **kwargs):
self._original_flask = flask.Flask
request_hook = kwargs.get("request_hook")
response_hook = kwargs.get("response_hook")
if callable(request_hook):
_InstrumentedFlask._request_hook = request_hook
if callable(response_hook):
_InstrumentedFlask._response_hook = response_hook
tracer_provider = kwargs.get("tracer_provider")
_InstrumentedFlask._tracer_provider = tracer_provider
excluded_urls = kwargs.get("excluded_urls")
_InstrumentedFlask._excluded_urls = (
_excluded_urls_from_env
if excluded_urls is None
else parse_excluded_urls(excluded_urls)
)
enable_commenter = kwargs.get("enable_commenter", True)
_InstrumentedFlask._enable_commenter = enable_commenter
commenter_options = kwargs.get("commenter_options", {})
_InstrumentedFlask._commenter_options = commenter_options
meter_provider = kwargs.get("meter_provider")
_InstrumentedFlask._meter_provider = meter_provider
sem_conv_opt_in_mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode(
_OpenTelemetryStabilitySignalType.HTTP,
)
_InstrumentedFlask._sem_conv_opt_in_mode = sem_conv_opt_in_mode
flask.Flask = _InstrumentedFlask
def _uninstrument(self, **kwargs):
flask.Flask = self._original_flask
# pylint: disable=too-many-locals
[docs] @staticmethod
def instrument_app(
app,
request_hook=None,
response_hook=None,
tracer_provider=None,
excluded_urls=None,
enable_commenter=True,
commenter_options=None,
meter_provider=None,
):
if not hasattr(app, "_is_instrumented_by_opentelemetry"):
app._is_instrumented_by_opentelemetry = False
if not app._is_instrumented_by_opentelemetry:
# initialize semantic conventions opt-in if needed
_OpenTelemetrySemanticConventionStability._initialize()
sem_conv_opt_in_mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode(
_OpenTelemetryStabilitySignalType.HTTP,
)
excluded_urls = (
parse_excluded_urls(excluded_urls)
if excluded_urls is not None
else _excluded_urls_from_env
)
meter = get_meter(
__name__,
__version__,
meter_provider,
schema_url=_get_schema_url(sem_conv_opt_in_mode),
)
duration_histogram_old = None
if _report_old(sem_conv_opt_in_mode):
duration_histogram_old = meter.create_histogram(
name=MetricInstruments.HTTP_SERVER_DURATION,
unit="ms",
description="Measures the duration of inbound HTTP requests.",
)
duration_histogram_new = None
if _report_new(sem_conv_opt_in_mode):
duration_histogram_new = meter.create_histogram(
name=HTTP_SERVER_REQUEST_DURATION,
unit="s",
description="Duration of HTTP server requests.",
explicit_bucket_boundaries_advisory=HTTP_DURATION_HISTOGRAM_BUCKETS_NEW,
)
if _report_new(sem_conv_opt_in_mode):
active_requests_counter = create_http_server_active_requests(
meter
)
else:
active_requests_counter = meter.create_up_down_counter(
name=MetricInstruments.HTTP_SERVER_ACTIVE_REQUESTS,
unit="requests",
description="Measures the number of concurrent HTTP requests that are currently in-flight.",
)
app._original_wsgi_app = app.wsgi_app
app.wsgi_app = _rewrapped_app(
app.wsgi_app,
active_requests_counter,
duration_histogram_old,
response_hook=response_hook,
excluded_urls=excluded_urls,
sem_conv_opt_in_mode=sem_conv_opt_in_mode,
duration_histogram_new=duration_histogram_new,
)
tracer = trace.get_tracer(
__name__,
__version__,
tracer_provider,
schema_url=_get_schema_url(sem_conv_opt_in_mode),
)
_before_request = _wrapped_before_request(
request_hook,
tracer,
excluded_urls=excluded_urls,
enable_commenter=enable_commenter,
commenter_options=(
commenter_options if commenter_options else {}
),
sem_conv_opt_in_mode=sem_conv_opt_in_mode,
)
app._before_request = _before_request
app.before_request(_before_request)
_teardown_request = _wrapped_teardown_request(
excluded_urls=excluded_urls,
)
app._teardown_request = _teardown_request
app.teardown_request(_teardown_request)
app._is_instrumented_by_opentelemetry = True
else:
_logger.warning(
"Attempting to instrument Flask app while already instrumented"
)
[docs] @staticmethod
def uninstrument_app(app):
if hasattr(app, "_original_wsgi_app"):
app.wsgi_app = app._original_wsgi_app
# FIXME add support for other Flask blueprints that are not None
app.before_request_funcs[None].remove(app._before_request)
app.teardown_request_funcs[None].remove(app._teardown_request)
del app._original_wsgi_app
app._is_instrumented_by_opentelemetry = False
else:
_logger.warning(
"Attempting to uninstrument Flask "
"app while already uninstrumented"
)