Script 'mail_helper' called by obssrc
Hello community,
here is the log from the commit of package
python-prometheus-fastapi-instrumentator for openSUSE:Factory checked in at
2026-07-28 17:52:45
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-prometheus-fastapi-instrumentator
(Old)
and
/work/SRC/openSUSE:Factory/.python-prometheus-fastapi-instrumentator.new.2004
(New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-prometheus-fastapi-instrumentator"
Tue Jul 28 17:52:45 2026 rev:2 rq:1367845 version:8.1.0
Changes:
--------
---
/work/SRC/openSUSE:Factory/python-prometheus-fastapi-instrumentator/python-prometheus-fastapi-instrumentator.changes
2026-07-14 13:47:05.189163258 +0200
+++
/work/SRC/openSUSE:Factory/.python-prometheus-fastapi-instrumentator.new.2004/python-prometheus-fastapi-instrumentator.changes
2026-07-28 17:53:39.103539447 +0200
@@ -1,0 +2,14 @@
+Sun Jul 26 18:13:36 UTC 2026 - Martin Pluskal <[email protected]>
+
+- Update to version 8.1.0:
+ * Add the optional should_include_root_path parameter to the
+ Instrumentator constructor; when True the handler label of the
+ exported metrics includes the FastAPI app's effective
+ root_path (defaults to False, so existing metrics are
+ unchanged)
+ * Fix resolving route names when the FastAPI app sets a
+ root_path
+ * Fix incorrect handling of nested apps and routes in corner
+ cases introduced in 8.0.1
+
+-------------------------------------------------------------------
Old:
----
prometheus_fastapi_instrumentator-8.0.2.tar.gz
New:
----
prometheus_fastapi_instrumentator-8.1.0.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-prometheus-fastapi-instrumentator.spec ++++++
--- /var/tmp/diff_new_pack.25X6gR/_old 2026-07-28 17:53:40.519588967 +0200
+++ /var/tmp/diff_new_pack.25X6gR/_new 2026-07-28 17:53:40.519588967 +0200
@@ -1,7 +1,7 @@
#
# spec file for package python-prometheus-fastapi-instrumentator
#
-# Copyright (c) 2026 SUSE LLC
+# Copyright (c) 2026 SUSE LLC and contributors
#
# All modifications and additions to the file contributed by third parties
# remain the property of their copyright owners, unless otherwise agreed
@@ -18,7 +18,7 @@
%{?sle15_python_module_pythons}
Name: python-prometheus-fastapi-instrumentator
-Version: 8.0.2
+Version: 8.1.0
Release: 0
Summary: Instrument a FastAPI app with Prometheus metrics
License: ISC
++++++ prometheus_fastapi_instrumentator-8.0.2.tar.gz ->
prometheus_fastapi_instrumentator-8.1.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/prometheus_fastapi_instrumentator-8.0.2/PKG-INFO
new/prometheus_fastapi_instrumentator-8.1.0/PKG-INFO
--- old/prometheus_fastapi_instrumentator-8.0.2/PKG-INFO 1970-01-01
01:00:00.000000000 +0100
+++ new/prometheus_fastapi_instrumentator-8.1.0/PKG-INFO 1970-01-01
01:00:00.000000000 +0100
@@ -1,6 +1,6 @@
Metadata-Version: 2.4
Name: prometheus-fastapi-instrumentator
-Version: 8.0.2
+Version: 8.1.0
Summary: Instrument your FastAPI app with Prometheus metrics
License: ISC
License-File: LICENSE
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/prometheus_fastapi_instrumentator-8.0.2/pyproject.toml
new/prometheus_fastapi_instrumentator-8.1.0/pyproject.toml
--- old/prometheus_fastapi_instrumentator-8.0.2/pyproject.toml 1970-01-01
01:00:00.000000000 +0100
+++ new/prometheus_fastapi_instrumentator-8.1.0/pyproject.toml 1970-01-01
01:00:00.000000000 +0100
@@ -4,7 +4,7 @@
[project]
name = "prometheus-fastapi-instrumentator"
-version = "8.0.2"
+version = "8.1.0"
description = "Instrument your FastAPI app with Prometheus metrics"
authors = [
{name = "Tim Schwenke", email = "[email protected]"}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/prometheus_fastapi_instrumentator-8.0.2/src/prometheus_fastapi_instrumentator/__init__.py
new/prometheus_fastapi_instrumentator-8.1.0/src/prometheus_fastapi_instrumentator/__init__.py
---
old/prometheus_fastapi_instrumentator-8.0.2/src/prometheus_fastapi_instrumentator/__init__.py
1970-01-01 01:00:00.000000000 +0100
+++
new/prometheus_fastapi_instrumentator-8.1.0/src/prometheus_fastapi_instrumentator/__init__.py
1970-01-01 01:00:00.000000000 +0100
@@ -1,5 +1,5 @@
from .instrumentation import PrometheusFastApiInstrumentator
-__version__ = "8.0.2"
+__version__ = "8.1.0"
Instrumentator = PrometheusFastApiInstrumentator
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/prometheus_fastapi_instrumentator-8.0.2/src/prometheus_fastapi_instrumentator/instrumentation.py
new/prometheus_fastapi_instrumentator-8.1.0/src/prometheus_fastapi_instrumentator/instrumentation.py
---
old/prometheus_fastapi_instrumentator-8.0.2/src/prometheus_fastapi_instrumentator/instrumentation.py
1970-01-01 01:00:00.000000000 +0100
+++
new/prometheus_fastapi_instrumentator-8.1.0/src/prometheus_fastapi_instrumentator/instrumentation.py
1970-01-01 01:00:00.000000000 +0100
@@ -39,6 +39,7 @@
should_group_status_codes: bool = True,
should_ignore_untemplated: bool = False,
should_group_untemplated: bool = True,
+ should_include_root_path: bool = False,
should_round_latency_decimals: bool = False,
should_respect_env_var: bool = False,
should_instrument_requests_inprogress: bool = False,
@@ -65,6 +66,10 @@
should_group_untemplated (bool): Should requests without a matching
template be grouped to handler `none`? Defaults to `True`.
+ should_include_root_path (bool): Should resolved handler templates
+ include the application's effective `root_path`? Defaults to
+ `False`.
+
should_round_latency_decimals: Should recorded latencies be
rounded to a certain number of decimals?
@@ -123,6 +128,7 @@
self.should_group_status_codes = should_group_status_codes
self.should_ignore_untemplated = should_ignore_untemplated
self.should_group_untemplated = should_group_untemplated
+ self.should_include_root_path = should_include_root_path
self.should_round_latency_decimals = should_round_latency_decimals
self.should_respect_env_var = should_respect_env_var
self.should_instrument_requests_inprogress =
should_instrument_requests_inprogress
@@ -217,6 +223,7 @@
should_group_status_codes=self.should_group_status_codes,
should_ignore_untemplated=self.should_ignore_untemplated,
should_group_untemplated=self.should_group_untemplated,
+ should_include_root_path=self.should_include_root_path,
should_round_latency_decimals=self.should_round_latency_decimals,
should_respect_env_var=self.should_respect_env_var,
should_instrument_requests_inprogress=self.should_instrument_requests_inprogress,
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/prometheus_fastapi_instrumentator-8.0.2/src/prometheus_fastapi_instrumentator/middleware.py
new/prometheus_fastapi_instrumentator-8.1.0/src/prometheus_fastapi_instrumentator/middleware.py
---
old/prometheus_fastapi_instrumentator-8.0.2/src/prometheus_fastapi_instrumentator/middleware.py
1970-01-01 01:00:00.000000000 +0100
+++
new/prometheus_fastapi_instrumentator-8.1.0/src/prometheus_fastapi_instrumentator/middleware.py
1970-01-01 01:00:00.000000000 +0100
@@ -16,6 +16,8 @@
class PrometheusInstrumentatorMiddleware:
+ """ASGI middleware that records Prometheus metrics for HTTP requests."""
+
def __init__(
self,
app: ASGIApp,
@@ -23,6 +25,7 @@
should_group_status_codes: bool = True,
should_ignore_untemplated: bool = False,
should_group_untemplated: bool = True,
+ should_include_root_path: bool = False,
should_round_latency_decimals: bool = False,
should_respect_env_var: bool = False,
should_instrument_requests_inprogress: bool = False,
@@ -70,6 +73,7 @@
self.should_group_status_codes = should_group_status_codes
self.should_ignore_untemplated = should_ignore_untemplated
self.should_group_untemplated = should_group_untemplated
+ self.should_include_root_path = should_include_root_path
self.should_round_latency_decimals = should_round_latency_decimals
self.should_respect_env_var = should_respect_env_var
self.should_instrument_requests_inprogress =
should_instrument_requests_inprogress
@@ -235,9 +239,12 @@
Returns:
Tuple[str, bool]: Tuple with two elements. First element is either
template or if no template the path. Second element tells you
- if the path is templated or not.
+ if the path is templated or not. When configured, the template
+ includes the application's effective `root_path`.
"""
- route_name = routing.get_route_name(request)
+ route_name = routing.get_route_name(
+ request, should_include_root_path=self.should_include_root_path
+ )
return route_name or request.url.path, True if route_name else False
def _is_handler_excluded(self, handler: str, is_templated: bool) -> bool:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/prometheus_fastapi_instrumentator-8.0.2/src/prometheus_fastapi_instrumentator/routing.py
new/prometheus_fastapi_instrumentator-8.1.0/src/prometheus_fastapi_instrumentator/routing.py
---
old/prometheus_fastapi_instrumentator-8.0.2/src/prometheus_fastapi_instrumentator/routing.py
1970-01-01 01:00:00.000000000 +0100
+++
new/prometheus_fastapi_instrumentator-8.1.0/src/prometheus_fastapi_instrumentator/routing.py
1970-01-01 01:00:00.000000000 +0100
@@ -1,179 +1,135 @@
-# BSD 3-Clause License
-#
-# Copyright (c) 2012, the Sentry Team, see AUTHORS for more details
-# Copyright (c) 2019, Elasticsearch BV
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are met:
-#
-# * Redistributions of source code must retain the above copyright notice, this
-# list of conditions and the following disclaimer.
-#
-# * Redistributions in binary form must reproduce the above copyright notice,
-# this list of conditions and the following disclaimer in the documentation
-# and/or other materials provided with the distribution.
-#
-# * Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE
-# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-
-"""Helper module for routing.
-
-The two functions in this module are licensed under the BSD 3-Clause License
-instead of the ISC License like the rest of the project. Therefore the code
-is contained in a dedicated module.
-
-Based on code from
[elastic/apm-agent-python](https://github.com/elastic/apm-agent-python/blob/527f62c0c50842f94ef90fda079853372539319a/elasticapm/contrib/starlette/__init__.py).
-"""
-
-from typing import List, Optional
+from typing import Any, List, Optional
from starlette.requests import HTTPConnection
-from starlette.routing import Match, Mount, Route
+from starlette.routing import BaseRoute, Match, Mount
from starlette.types import Scope
-def _resolve_path(route: Route) -> Optional[str]:
- """Return the request path contributed by ``route``, or ``None`` if the
- route is a router-like wrapper whose own routes must be traversed.
-
- FastAPI 0.116+ (officially 0.137) wraps routers registered via
- ``app.include_router`` in an internal ``_IncludedRouter`` class that
- does not expose a ``path`` attribute. The configured mount path is
- available on ``include_context.prefix``.
+def _effective_routes(routes: List[BaseRoute]) -> List[BaseRoute]:
+ """Flattens a list of routes into directly matchable routes.
+
+ FastAPI (>= 0.116) represents routers registered via `include_router`
+ with an internal `_IncludedRouter` object that has no `path` attribute
+ and does not expose its children via the `matches` API. Such routers do
+ expose `effective_route_contexts()` though, which yields context objects
+ whose resolved route already carries the fully prefixed `path`. Every
+ other route is passed through unchanged.
"""
- if hasattr(route, "path"):
- return route.path
- include_context = getattr(route, "include_context", None)
- if include_context is not None:
- # An empty prefix means the wrapper contributes no path segment of
- # its own (e.g. ``APIRouter(prefix=...)`` registered via
- # ``include_router`` without an extra ``prefix=`` argument). The
- # caller must still recurse into nested routes.
- prefix = getattr(include_context, "prefix", "") or ""
- return prefix
- return None
+ effective: List[BaseRoute] = []
+ for route in routes:
+ contexts: Any = getattr(route, "effective_route_contexts", None)
+ if callable(contexts):
+ resolved_contexts: Any = contexts()
+ for context in resolved_contexts:
+ # Mounts must be recursed into, so the mount object (which
+ # carries the fully prefixed path and its sub-routes) is used.
+ # Plain Starlette routes expose their fully prefixed path via
+ # `starlette_route`. FastAPI routes keep `starlette_route`
+ # unset but the context itself is matchable and carries the
+ # fully prefixed `path`.
+ starlette_route = getattr(context, "starlette_route", None)
+ if starlette_route is not None:
+ effective.append(starlette_route)
+ else:
+ effective.append(context)
+ else:
+ effective.append(route)
+ return effective
-def _child_routes(route: Route) -> Optional[List[Route]]:
- """Return nested routes for router-like route objects, else ``None``."""
+def _get_route_name(scope: Scope, routes: List[BaseRoute]) -> Optional[str]:
+ """Resolves the templated handler for a scope against a list of routes.
- if isinstance(route, Mount):
- return route.routes or None
- original_router = getattr(route, "original_router", None)
- if original_router is not None and hasattr(original_router, "routes"):
- nested = list(original_router.routes)
- return nested or None
- return None
+ Matching relies on Starlette's own `matches` implementation, which
+ resolves the path relative to `scope["root_path"]`. Mounts are recursed
+ into with the child scope so their prefix is prepended to the result. A
+ full match wins immediately, a partial match (e.g. path matches but method
+ does not) is remembered as a fallback.
+ """
+ partial: Optional[str] = None
-def _strip_prefix_from_scope(scope: Scope, prefix: str) -> Scope:
- """Return a copy of ``scope`` with the mount ``prefix`` removed from
- ``path``.
-
- ``starlette.routing.Mount.matches`` returns a child scope with the
- mount prefix already stripped. FastAPI's ``_IncludedRouter`` does
- not, so the recursion into the included router's own routes would
- never match a leaf endpoint. Stripping the prefix here restores the
- behaviour expected by the recursive call.
- """
+ for route in _effective_routes(routes):
+ match, child_scope = route.matches(scope)
+ if match == Match.NONE:
+ continue
- if not prefix:
- return scope
- path = scope.get("path", "") or ""
- if path == prefix:
- return {**scope, "path": ""}
- if path.startswith(prefix + "/"):
- return {**scope, "path": path.removeprefix(prefix)}
- return scope
+ if isinstance(route, Mount):
+ sub_routes = route.routes
+ if not sub_routes:
+ # Opaque mounted ASGI app without introspectable sub-routes.
+ # The mount path itself is the most specific handler available.
+ name = route.path
+ else:
+ child = _get_route_name({**scope, **child_scope}, sub_routes)
+ if child is None:
+ continue
+ name = route.path + child
+ else:
+ path = getattr(route, "path", None)
+ if path is None:
+ continue
+ name = path
+ if match == Match.FULL:
+ return name
+ if partial is None:
+ partial = name
-def _get_route_name(
- scope: Scope, routes: List[Route], route_name: Optional[str] = None
-) -> Optional[str]:
- """Gets route name for given scope taking mounts into account.
+ return partial
- Supports plain ``Route``/``Mount`` objects as well as FastAPI's
- internal ``_IncludedRouter`` wrapper produced by
- ``app.include_router(...)``. When a matched route is a router-like
- object, the function recurses into its nested routes so the final
- label reflects the leaf endpoint.
- """
- for route in routes:
- match, child_scope = route.matches(scope)
- if match == Match.FULL:
- resolved = _resolve_path(route)
- if resolved is None:
- # Cannot produce a stable label for this route; try the
- # next candidate. Callers fall back to ``request.url.path``
- # if no route yields a name.
- continue
- route_name = resolved
- child_scope = {**scope, **child_scope}
- children = _child_routes(route)
- if children:
- # FastAPI's ``_IncludedRouter`` does not strip the mount
- # prefix from the scope before matching nested routes,
- # unlike ``starlette.routing.Mount``. Strip it here so
- # the leaf endpoint inside the included router can match.
- if not isinstance(route, Mount):
- include_context = getattr(route, "include_context", None)
- if include_context is not None:
- prefix = getattr(include_context, "prefix", "") or ""
- if prefix:
- child_scope =
_strip_prefix_from_scope(child_scope, prefix)
- child_route_name = _get_route_name(child_scope, children)
- if child_route_name is not None:
- # Concatenate the leaf path onto the parent path so the
- # final label is e.g. ``/api/v1/items/{item_id}`` rather
- # than just ``/{item_id}``.
- route_name = route_name + child_route_name
- else:
- route_name = None
- return route_name
- elif match == Match.PARTIAL and route_name is None:
- resolved = _resolve_path(route)
- if resolved is not None:
- route_name = resolved
- return route_name
+def get_route_name(
+ request: HTTPConnection, should_include_root_path: bool
+) -> Optional[str]:
+ """Gets route name.
+ Resolves the templated route for the given request, e.g. `/api/items/123`
+ maps to `/api/items/{item_id}`. Returns `None` when no route matches.
-def get_route_name(request: HTTPConnection) -> Optional[str]:
- """Gets route name for given request taking mounts into account."""
+ Works with both plain Starlette and FastAPI applications and supports
+ router prefixes, `include_router` (including nested routers), mounted
+ sub-applications and `root_path`. When the application itself is a mounted
+ sub-application that is instrumented directly, the resolved name is
relative
+ to that sub-application.
+
+ Args:
+ request: Connection whose scope should be resolved to a route name.
+ should_include_root_path: Whether the returned route name should be
+ prefixed with the application's effective `root_path`.
+ """
- app = request.app
scope = request.scope
- routes = app.routes
- route_name = _get_route_name(scope, routes)
+ app = scope.get("app")
+ routes = getattr(app, "routes", None)
+ if not routes:
+ return None
+
+ name = _get_route_name(scope, routes)
+
+ if name is not None:
+ if not should_include_root_path:
+ return name
+
+ # Only the application's own `root_path` is prepended. A `root_path`
+ # injected into the scope by a parent mount (i.e. when a mounted
+ # sub-application is instrumented directly) is intentionally ignored so
+ # the resolved name stays relative to that sub-application.
+ root_path = getattr(app, "root_path", "") or ""
+ if root_path:
+ return root_path.rstrip("/") + name
+ return name
+
+ # No route matched. If the router redirects on trailing slashes, a request
+ # like `/items` against a route `/items/` (or vice versa) does not match
+ # directly but would be redirected. Report the requested path in that case.
+ router = getattr(app, "router", None)
+ if router is not None and getattr(router, "redirect_slashes", False):
+ path = scope.get("path", "")
+ toggled = path[:-1] if path.endswith("/") else path + "/"
+ if toggled and toggled != path:
+ if _get_route_name({**scope, "path": toggled}, routes) is not None:
+ return path
- # Starlette magically redirects requests if the path matches a route name
- # with a trailing slash appended or removed. To not spam the transaction
- # names list, we do the same here and put these redirects all in the
- # same "redirect trailing slashes" transaction name.
- if not route_name and app.router.redirect_slashes and scope["path"] != "/":
- redirect_scope = dict(scope)
- if scope["path"].endswith("/"):
- redirect_scope["path"] = scope["path"][:-1]
- trim = True
- else:
- redirect_scope["path"] = scope["path"] + "/"
- trim = False
-
- route_name = _get_route_name(redirect_scope, routes)
- if route_name is not None:
- route_name = route_name.rstrip("/")
- route_name = route_name + "/" if trim else route_name
- return route_name
+ return None