main
py 81 lines 3.05 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4
5 import prometheus_client
6 import regex as re
7 from fastapi import FastAPI, Response
8 from prometheus_client import make_asgi_app
9 from prometheus_fastapi_instrumentator import Instrumentator
10 from prometheus_fastapi_instrumentator import routing as _pfi_routing
11 from starlette.routing import Match, Mount
12 from starlette.types import Scope
13
14 from vllm.v1.metrics.prometheus import get_prometheus_registry
15
16
17 def _patch_instrumentator_route_walk() -> None:
18 """Make prometheus-fastapi-instrumentator's route walk tolerate routes
19 without a ``.path``.
20
21 FastAPI >= 0.137 stores lazy ``_IncludedRouter`` objects in ``app.routes``;
22 these are ``BaseRoute`` subclasses with no ``.path`` attribute. The
23 instrumentator's ``_get_route_name`` (up to 8.0.0) reads ``route.path``
24 unconditionally, so every request raises ``AttributeError`` in the metrics
25 middleware and the server returns 500 (e.g. ``/health`` never goes ready).
26 Skip path-less routes; this only affects the metric handler label, not
27 request routing. Idempotent.
28 """
29
30 def _get_route_name(scope: Scope, routes, route_name=None):
31 for route in routes:
32 if getattr(route, "path", None) is None:
33 continue
34 match, child_scope = route.matches(scope)
35 if match == Match.FULL:
36 route_name = route.path
37 child_scope = {**scope, **child_scope}
38 if isinstance(route, Mount) and route.routes:
39 child = _get_route_name(child_scope, route.routes, route_name)
40 route_name = None if child is None else route_name + child
41 return route_name
42 elif match == Match.PARTIAL and route_name is None:
43 route_name = route.path
44 return None
45
46 _pfi_routing._get_route_name = _get_route_name
47
48
49 _patch_instrumentator_route_walk()
50
51
52 class PrometheusResponse(Response):
53 media_type = prometheus_client.CONTENT_TYPE_LATEST
54
55
56 def attach_router(app: FastAPI):
57 """Mount prometheus metrics to a FastAPI app."""
58 registry = get_prometheus_registry()
59
60 # `response_class=PrometheusResponse` is needed to return an HTTP response
61 # with header "Content-Type: text/plain; version=0.0.4; charset=utf-8"
62 # instead of the default "application/json" which is incorrect.
63 # See https://github.com/trallnag/prometheus-fastapi-instrumentator/issues/163#issue-1296092364
64 Instrumentator(
65 excluded_handlers=[
66 "/metrics",
67 "/health",
68 "/load",
69 "/ping",
70 "/version",
71 "/server_info",
72 ],
73 registry=registry,
74 ).add().instrument(app).expose(app, response_class=PrometheusResponse)
75
76 # Add prometheus asgi middleware to route /metrics requests
77 metrics_route = Mount("/metrics", make_asgi_app(registry=registry))
78
79 # Workaround for 307 Redirect for /metrics
80 metrics_route.path_regex = re.compile("^/metrics(?P<path>.*)$")
81 app.routes.append(metrics_route)