-
Notifications
You must be signed in to change notification settings - Fork 71
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
instrumentation: fastapi prometheus middleware #1007
Open
nosahama
wants to merge
1
commit into
jjaakola-aiven-fastapi
Choose a base branch
from
nosahama/EC-627-fastapi-prometheus-metrics
base: jjaakola-aiven-fastapi
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
""" | ||
Copyright (c) 2024 Aiven Ltd | ||
See LICENSE for details | ||
""" | ||
|
||
from collections.abc import Awaitable, Callable | ||
from dependency_injector.wiring import inject, Provide | ||
from fastapi import Depends, FastAPI, Request, Response | ||
from karapace.instrumentation.prometheus import PrometheusInstrumentation | ||
from schema_registry.container import SchemaRegistryContainer | ||
|
||
import logging | ||
import time | ||
|
||
LOG = logging.getLogger(__name__) | ||
|
||
|
||
@inject | ||
async def prometheus_middleware( | ||
request: Request, | ||
call_next: Callable[[Request], Awaitable[Response]], | ||
prometheus: PrometheusInstrumentation = Depends(Provide[SchemaRegistryContainer.karapace_container.prometheus]), | ||
) -> Response: | ||
# Set start time for request | ||
setattr(request.state, prometheus.START_TIME_REQUEST_KEY, time.monotonic()) | ||
|
||
# Extract request labels | ||
path = request.url.path | ||
method = request.method | ||
|
||
# Increment requests in progress before response handler | ||
prometheus.karapace_http_requests_in_progress.labels(method=method, path=path).inc() | ||
|
||
# Call request handler | ||
response: Response = await call_next(request) | ||
|
||
# Instrument request duration | ||
prometheus.karapace_http_requests_duration_seconds.labels(method=method, path=path).observe( | ||
time.monotonic() - getattr(request.state, prometheus.START_TIME_REQUEST_KEY) | ||
) | ||
|
||
# Instrument total requests | ||
prometheus.karapace_http_requests_total.labels(method=method, path=path, status=response.status_code).inc() | ||
|
||
# Decrement requests in progress after response handler | ||
prometheus.karapace_http_requests_in_progress.labels(method=method, path=path).dec() | ||
|
||
return response | ||
|
||
|
||
def setup_prometheus_middleware(app: FastAPI) -> None: | ||
LOG.info("Setting up prometheus middleware for metrics") | ||
app.middleware("http")(prometheus_middleware) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
""" | ||
Copyright (c) 2024 Aiven Ltd | ||
See LICENSE for details | ||
""" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
""" | ||
Copyright (c) 2024 Aiven Ltd | ||
See LICENSE for details | ||
""" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
""" | ||
schema_registry - prometheus instrumentation middleware tests | ||
|
||
Copyright (c) 2024 Aiven Ltd | ||
See LICENSE for details | ||
""" | ||
|
||
from _pytest.logging import LogCaptureFixture | ||
from fastapi import FastAPI, Request, Response | ||
from karapace.instrumentation.prometheus import PrometheusInstrumentation | ||
from schema_registry.middlewares.prometheus import prometheus_middleware, setup_prometheus_middleware | ||
from starlette.datastructures import State | ||
from unittest.mock import AsyncMock, call, MagicMock, patch | ||
|
||
import logging | ||
|
||
|
||
def test_setup_prometheus_middleware(caplog: LogCaptureFixture) -> None: | ||
app = AsyncMock(spec=FastAPI) | ||
with caplog.at_level(logging.INFO, logger="schema_registry.middlewares.prometheus"): | ||
setup_prometheus_middleware(app=app) | ||
|
||
for log in caplog.records: | ||
assert log.name == "schema_registry.middlewares.prometheus" | ||
assert log.levelname == "INFO" | ||
assert log.message == "Setting up prometheus middleware for metrics" | ||
|
||
app.middleware.assert_called_once_with("http") | ||
app.middleware.return_value.assert_called_once_with(prometheus_middleware) | ||
|
||
|
||
async def test_prometheus_middleware() -> None: | ||
response_mock = AsyncMock(spec=Response) | ||
response_mock.status_code = 200 | ||
|
||
call_next = AsyncMock() | ||
call_next.return_value = response_mock | ||
|
||
request = AsyncMock(spec=Request) | ||
request.state = MagicMock(spec=State) | ||
|
||
prometheus = MagicMock(spec=PrometheusInstrumentation, START_TIME_REQUEST_KEY="start_time") | ||
|
||
with patch("schema_registry.middlewares.prometheus.time.monotonic", return_value=1): | ||
response = await prometheus_middleware(request=request, call_next=call_next, prometheus=prometheus) | ||
|
||
# Check that the `start_time` for the request is set | ||
assert hasattr(request.state, "start_time") | ||
assert getattr(request.state, "start_time") == 1 | ||
|
||
# Check that `karapace_http_requests_in_progress` metric is incremented/decremented | ||
prometheus.karapace_http_requests_in_progress.labels.assert_has_calls( | ||
[ | ||
call(method=request.method, path=request.url.path), | ||
call().inc(), | ||
call(method=request.method, path=request.url.path), | ||
call().dec(), | ||
] | ||
) | ||
|
||
# Check that `karapace_http_requests_duration_seconds` metric is observed | ||
prometheus.karapace_http_requests_duration_seconds.labels.assert_has_calls( | ||
[ | ||
call(method=request.method, path=request.url.path), | ||
call().observe(0), | ||
] | ||
) | ||
|
||
# Check that the request handler is called | ||
call_next.assert_awaited_once_with(request) | ||
|
||
# Check that `karapace_http_requests_total` metric is incremented/decremented | ||
prometheus.karapace_http_requests_total.labels.assert_has_calls( | ||
[ | ||
call(method=request.method, path=request.url.path, status=response.status_code), | ||
call().inc(), | ||
] | ||
) | ||
|
||
assert response == response_mock |
File renamed without changes.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can cause quite high cardinality, can we align this with the path template. E.g.
/subjects/<subject>/versions
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes I can think around it, it was similar thought I had on the tracing PR #1009, where I extract the resource from the path as I didn't find a way to get the template, as this was the traces, it looked fine to trace the resource, as further spans contain relevant information and this reduces the trace cardinality.
In prometheus tho, I do not think this should be a problem, major cardinality issues arise with too many labels. To instrument, might be necessary to see exactly the path that's being checked, but I also get your point. Let me reason around it and get back.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking at this discussion - fastapi/fastapi#12975, the route template is only accessed after a successful response, I do need the path already to instrument the requests in progress.
Unless we want to fallback and extract the base resource, i.e.
resource = request.url.path.split("/")[1]
. From a template like/subjects/{subject}/versions
and a request for/subjects/test-topic-value/versions
, this will yieldsubjects
.