Skip to content

Commit

Permalink
feat: Make instrumentator work with just Starlette (#288)
Browse files Browse the repository at this point in the history
* Replace fastapi with Starlette

* build(deps): Move fastapi to dev dependencies

* fix: Make it work

* docs: Add entry to changelog

---------

Co-authored-by: Tim Schwenke <[email protected]>
  • Loading branch information
mvanderlee and trallnag authored Mar 10, 2024
1 parent 885c80c commit c608c4e
Show file tree
Hide file tree
Showing 7 changed files with 78 additions and 17 deletions.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,16 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0).

### Added

- Instrumentator now works without FastAPI. This is possible because every
FastAPI app is also a Starlette app (but not the other way around). Or to be
more specific: FastAPI uses Starlette for things like routing and middleware
this package relies on. The change is backwards compatible, even type checkers
like mypy should continue working. Thanks to
[@mvanderlee](https://github.com/mvanderlee) for proposing this in
[#280](https://github.com/trallnag/prometheus-fastapi-instrumentator/issues/280)
and implementing it in
[#288](https://github.com/trallnag/prometheus-fastapi-instrumentator/pull/288).

- Relaxed type of `get_route_name` argument to `HTTPConnection`. This allows
developers to use the `get_route_name` function for getting the name of
websocket routes as well. Thanks to [@pajowu](https://github.com/pajowu) for
Expand Down
2 changes: 1 addition & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ keywords = ["prometheus", "instrumentation", "fastapi", "exporter", "metrics"]

[tool.poetry.dependencies]
python = ">= 3.8.1, < 4.0.0"
fastapi = ">= 0.38.1, < 1.0.0"
starlette = ">= 0.30.0, < 1.0.0"
prometheus-client = ">= 0.8.0, < 1.0.0"

[tool.poetry.group.dev.dependencies]
Expand All @@ -32,6 +32,7 @@ asgiref = "^3.7.2"
uvicorn = ">=0.28.0"
gunicorn = "^21.2.0"
pytest-asyncio = ">=0.23.5.post1"
fastapi = "^0.110.0"

[tool.black]
line-length = 90
Expand Down
38 changes: 28 additions & 10 deletions src/prometheus_fastapi_instrumentator/instrumentation.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
import asyncio
import gzip
import importlib.util
import os
import re
import warnings
from enum import Enum
from typing import Any, Awaitable, Callable, List, Optional, Sequence, Union, cast

from fastapi import FastAPI
from prometheus_client import (
CONTENT_TYPE_LATEST,
REGISTRY,
CollectorRegistry,
generate_latest,
multiprocess,
)
from starlette.applications import Starlette
from starlette.requests import Request
from starlette.responses import Response

Expand All @@ -40,7 +41,7 @@ def __init__(
inprogress_labels: bool = False,
registry: Union[CollectorRegistry, None] = None,
) -> None:
"""Create a Prometheus FastAPI Instrumentator.
"""Create a Prometheus FastAPI (and Starlette) Instrumentator.
Args:
should_group_status_codes (bool): Should status codes be grouped into
Expand Down Expand Up @@ -149,7 +150,7 @@ def __init__(

def instrument(
self,
app: FastAPI,
app: Starlette,
metric_namespace: str = "",
metric_subsystem: str = "",
should_only_respect_2xx_for_highr: bool = False,
Expand Down Expand Up @@ -183,10 +184,11 @@ def instrument(
The middleware iterates through all `instrumentations` and executes them.
Args:
app (FastAPI): FastAPI app instance.
app: Starlette app instance. Note that every FastAPI app is a
Starlette app.
Raises:
e: Only raised if FastAPI itself throws an exception.
e: Only raised if app itself throws an exception.
Returns:
self: Instrumentator. Builder Pattern.
Expand Down Expand Up @@ -222,7 +224,7 @@ def instrument(

def expose(
self,
app: FastAPI,
app: Starlette,
should_gzip: bool = False,
endpoint: str = "/metrics",
include_in_schema: bool = True,
Expand All @@ -232,7 +234,9 @@ def expose(
"""Exposes endpoint for metrics.
Args:
app: FastAPI app instance. Endpoint will be added to this app.
app: App instance. Endpoint will be added to this app. This can be
a Starlette app or a FastAPI app. If it is a Starlette app, `tags`
`kwargs` will be ignored.
should_gzip: Should the endpoint return compressed data? It will
also check for `gzip` in the `Accept-Encoding` header.
Expand All @@ -245,9 +249,9 @@ def expose(
include_in_schema: Should the endpoint show up in the documentation?
tags (List[str], optional): If you manage your routes with tags.
Defaults to None.
Defaults to None. Only passed to FastAPI app.
kwargs: Will be passed to FastAPI route annotation.
kwargs: Will be passed to app. Only passed to FastAPI app.
Returns:
self: Instrumentator. Builder Pattern.
Expand All @@ -256,7 +260,6 @@ def expose(
if self.should_respect_env_var and not self._should_instrumentate():
return self

@app.get(endpoint, include_in_schema=include_in_schema, tags=tags, **kwargs)
def metrics(request: Request) -> Response:
"""Endpoint that serves Prometheus metrics."""

Expand All @@ -277,6 +280,21 @@ def metrics(request: Request) -> Response:

return resp

route_configured = False
if importlib.util.find_spec("fastapi"):
from fastapi import FastAPI

if isinstance(app, FastAPI):
fastapi_app: FastAPI = app
fastapi_app.get(
endpoint, include_in_schema=include_in_schema, tags=tags, **kwargs
)(metrics)
route_configured = True
if not route_configured:
app.add_route(
path=endpoint, route=metrics, include_in_schema=include_in_schema
)

return self

def add(
Expand Down
2 changes: 1 addition & 1 deletion src/prometheus_fastapi_instrumentator/metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ def latency(
buckets: Sequence[Union[float, str]] = Histogram.DEFAULT_BUCKETS,
registry: CollectorRegistry = REGISTRY,
) -> Optional[Callable[[Info], None]]:
"""Default metric for the Prometheus FastAPI Instrumentator.
"""Default metric for the Prometheus Starlette Instrumentator.
Args:
metric_name (str, optional): Name of the metric to be created. Must be
Expand Down
4 changes: 2 additions & 2 deletions src/prometheus_fastapi_instrumentator/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
from timeit import default_timer
from typing import Awaitable, Callable, Optional, Sequence, Tuple, Union

from fastapi import FastAPI
from prometheus_client import REGISTRY, CollectorRegistry, Gauge
from starlette.applications import Starlette
from starlette.datastructures import Headers
from starlette.requests import Request
from starlette.responses import Response
Expand All @@ -19,7 +19,7 @@
class PrometheusInstrumentatorMiddleware:
def __init__(
self,
app: FastAPI,
app: Starlette,
*,
should_group_status_codes: bool = True,
should_ignore_untemplated: bool = False,
Expand Down
36 changes: 34 additions & 2 deletions tests/test_instrumentation_expose.py → tests/test_expose.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
from typing import Any, Dict, Optional

from fastapi import FastAPI, HTTPException
from starlette.applications import Starlette
from starlette.responses import PlainTextResponse
from starlette.routing import Route
from starlette.testclient import TestClient

from prometheus_fastapi_instrumentator import Instrumentator
Expand All @@ -10,7 +13,7 @@
# Setup


def create_app() -> FastAPI:
def create_fastapi_app() -> FastAPI:
app = FastAPI()

@app.get("/")
Expand Down Expand Up @@ -45,6 +48,13 @@ def create_item(item: Dict[Any, Any]):
return app


def create_starlette_app() -> Starlette:
async def homepage(request):
return PlainTextResponse("Homepage")

return Starlette(routes=[Route("/", endpoint=homepage)])


def reset_prometheus() -> None:
from prometheus_client import REGISTRY

Expand All @@ -70,7 +80,7 @@ def reset_prometheus() -> None:

def test_expose_default_content_type():
reset_prometheus()
app = create_app()
app = create_fastapi_app()
Instrumentator().instrument(app).expose(app)
client = TestClient(app)

Expand All @@ -80,3 +90,25 @@ def test_expose_default_content_type():
"text/plain; version=0.0.4; charset=utf-8; charset=utf-8"
not in response.headers.values()
)


def test_fastapi_app_expose():
reset_prometheus()
app = create_fastapi_app()
Instrumentator().instrument(app).expose(app)
client = TestClient(app)

response = client.get("/metrics")

assert response.status_code == 200


def test_starlette_app_expose():
reset_prometheus()
app = create_starlette_app()
Instrumentator().instrument(app).expose(app)
client = TestClient(app)

response = client.get("/metrics")

assert response.status_code == 200

0 comments on commit c608c4e

Please sign in to comment.