aboutsummaryrefslogtreecommitdiff
path: root/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/flask
diff options
context:
space:
mode:
Diffstat (limited to '.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/flask')
-rw-r--r--.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/flask/__init__.py776
-rw-r--r--.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/flask/package.py20
-rw-r--r--.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/flask/version.py15
3 files changed, 811 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/flask/__init__.py b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/flask/__init__.py
new file mode 100644
index 00000000..9691f884
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/flask/__init__.py
@@ -0,0 +1,776 @@
+# Copyright The OpenTelemetry Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# 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.
+
+SQLCOMMENTER
+*****************************************
+You can optionally configure Flask instrumentation to enable sqlcommenter which enriches
+the query with contextual information.
+
+Usage
+-----
+
+.. code:: python
+
+ from opentelemetry.instrumentation.flask import FlaskInstrumentor
+
+ FlaskInstrumentor().instrument(enable_commenter=True, commenter_options={})
+
+For example, FlaskInstrumentor when used with SQLAlchemyInstrumentor or Psycopg2Instrumentor,
+invoking ``cursor.execute("select * from auth_users")`` will lead to sql query
+``select * from auth_users`` but when SQLCommenter is enabled the query will get appended with
+some configurable tags like:
+
+.. code::
+
+ select * from auth_users /*metrics=value*/;"
+
+Inorder for the commenter to append flask related tags to sql queries, the commenter needs
+to enabled on the respective SQLAlchemyInstrumentor or Psycopg2Instrumentor framework too.
+
+SQLCommenter Configurations
+***************************
+We can configure the tags to be appended to the sqlquery log by adding configuration
+inside ``commenter_options={}`` dict.
+
+For example, enabling this flag will add flask and it's version which
+is ``/*flask%%3A2.9.3*/`` to the SQL query as a comment (default is True):
+
+.. code:: python
+
+ framework = True
+
+For example, enabling this flag will add route uri ``/*route='/home'*/``
+to the SQL query as a comment (default is True):
+
+.. code:: python
+
+ route = True
+
+For example, enabling this flag will add controller name ``/*controller='home_view'*/``
+to the SQL query as a comment (default is True):
+
+.. code:: python
+
+ controller = True
+
+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
+
+ 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 convention <https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#http-request-and-response-headers>`_.
+
+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.
+
+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 (
+ _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.attributes.http_attributes import HTTP_ROUTE
+from opentelemetry.semconv.metrics import MetricInstruments
+from opentelemetry.semconv.metrics.http_metrics import (
+ HTTP_SERVER_REQUEST_DURATION,
+)
+from opentelemetry.semconv.trace import SpanAttributes
+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")
+
+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)
+
+
+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,
+):
+ 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
+
+ def _start_response(status, response_headers, *args, **kwargs):
+ if flask.request and (
+ excluded_urls is None
+ or not excluded_urls.url_disabled(flask.request.url)
+ ):
+ 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)
+ duration_s = default_timer() - start
+ 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[SpanAttributes.HTTP_TARGET] = str(
+ request_route
+ )
+
+ duration_histogram_old.record(
+ max(round(duration_s * 1000), 0), duration_attrs_old
+ )
+ 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
+ )
+ active_requests_counter.add(-1, active_requests_count_attrs)
+ return result
+
+ 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[SpanAttributes.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=E1101
+ 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=E1101
+ if excluded_urls and excluded_urls.url_disabled(flask.request.url):
+ return
+
+ activation = flask.request.environ.get(_ENVIRON_ACTIVATION_KEY)
+
+ 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
+ if exc is None:
+ activation.__exit__(None, None, None)
+ else:
+ activation.__exit__(
+ type(exc), exc, getattr(exc, "__traceback__", None)
+ )
+
+ if flask.request.environ.get(_ENVIRON_TOKEN, None):
+ context.detach(flask.request.environ.get(_ENVIRON_TOKEN))
+
+ 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.",
+ )
+ 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)
+
+
+class FlaskInstrumentor(BaseInstrumentor):
+ # pylint: disable=protected-access,attribute-defined-outside-init
+ """An instrumentor for flask.Flask
+
+ See `BaseInstrumentor`
+ """
+
+ 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
+ @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.",
+ )
+ active_requests_counter = meter.create_up_down_counter(
+ name=MetricInstruments.HTTP_SERVER_ACTIVE_REQUESTS,
+ unit="{request}",
+ description="Number of active HTTP server requests.",
+ )
+
+ 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"
+ )
+
+ @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"
+ )
diff --git a/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/flask/package.py b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/flask/package.py
new file mode 100644
index 00000000..150ca0ca
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/flask/package.py
@@ -0,0 +1,20 @@
+# Copyright The OpenTelemetry Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+_instruments = ("flask >= 1.0",)
+
+_supports_metrics = True
+
+_semconv_status = "migration"
diff --git a/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/flask/version.py b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/flask/version.py
new file mode 100644
index 00000000..7fb5b98b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/flask/version.py
@@ -0,0 +1,15 @@
+# Copyright The OpenTelemetry Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+__version__ = "0.52b1"