Skip to content

Commit

Permalink
tests: added integration tests for prometheus
Browse files Browse the repository at this point in the history
[EC-299]
  • Loading branch information
nosahama committed Jun 19, 2024
1 parent d6352b7 commit 40bc4a3
Show file tree
Hide file tree
Showing 6 changed files with 63 additions and 28 deletions.
5 changes: 3 additions & 2 deletions karapace/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ async def get(
headers: Optional[Headers] = None,
auth: Optional[BasicAuth] = None,
params: Optional[Mapping[str, str]] = None,
json_response: bool = True,
) -> Result:
path = self.path_for(path)
if not headers:
Expand All @@ -105,8 +106,8 @@ async def get(
params=params,
) as res:
# required for forcing the response body conversion to json despite missing valid Accept headers
json_result = await res.json(content_type=None)
return Result(res.status, json_result, headers=res.headers)
result = await res.json(content_type=None) if json_response else await res.text()
return Result(res.status, result, headers=res.headers)

async def delete(
self,
Expand Down
41 changes: 23 additions & 18 deletions karapace/instrumentation/prometheus.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@
Copyright (c) 2024 Aiven Ltd
See LICENSE for details
"""
# mypy: disable-error-code="call-overload"

from __future__ import annotations

from aiohttp.web import middleware, Request, RequestHandler, Response
from aiohttp.web import middleware, Request, Response
from karapace.rapu import RestApp
from prometheus_client import CollectorRegistry, Counter, Gauge, generate_latest, Histogram
from typing import ClassVar
from typing import Awaitable, Callable, Final

import logging
import time
Expand All @@ -19,34 +20,34 @@


class PrometheusInstrumentation:
METRICS_ENDPOINT_PATH: ClassVar[str] = "/metrics"
START_TIME_REQUEST_KEY: ClassVar[str] = "start_time"
METRICS_ENDPOINT_PATH: Final[str] = "/metrics"
START_TIME_REQUEST_KEY: Final[str] = "start_time"

registry: ClassVar[CollectorRegistry] = CollectorRegistry()
registry: Final[CollectorRegistry] = CollectorRegistry()

karapace_http_requests_total: ClassVar[Counter] = Counter(
karapace_http_requests_total: Final[Counter] = Counter(
registry=registry,
name="karapace_http_requests_total",
documentation="Total Request Count for HTTP/TCP Protocol",
labelnames=("method", "path", "status"),
)

karapace_http_requests_latency_seconds: ClassVar[Histogram] = Histogram(
karapace_http_requests_duration_seconds: Final[Histogram] = Histogram(
registry=registry,
name="karapace_http_requests_latency_seconds",
name="karapace_http_requests_duration_seconds",
documentation="Request Duration for HTTP/TCP Protocol",
labelnames=("method", "path"),
)

karapace_http_requests_in_progress: ClassVar[Gauge] = Gauge(
karapace_http_requests_in_progress: Final[Gauge] = Gauge(
registry=registry,
name="karapace_http_requests_in_progress",
documentation="Request Duration for HTTP/TCP Protocol",
documentation="In-progress requests for HTTP/TCP Protocol",
labelnames=("method", "path"),
)

@classmethod
def setup_metrics(cls: PrometheusInstrumentation, *, app: RestApp) -> None:
def setup_metrics(cls, *, app: RestApp) -> None:
LOG.info("Setting up prometheus metrics")
app.route(
cls.METRICS_ENDPOINT_PATH,
Expand All @@ -57,22 +58,26 @@ def setup_metrics(cls: PrometheusInstrumentation, *, app: RestApp) -> None:
json_body=False,
auth=None,
)
app.app.middlewares.insert(0, cls.http_request_metrics_middleware)
app.app.middlewares.insert(0, cls.http_request_metrics_middleware) # type: ignore[arg-type]

# disable-error-code="call-overload" is used at the top of this file to allow mypy checks.
# the issue is in the type difference (Counter, Gauge, etc) of the arguments which we are
# passing to `__setitem__()`, but we need to keep these objects in the `app.app` dict.
app.app[cls.karapace_http_requests_total] = cls.karapace_http_requests_total
app.app[cls.karapace_http_requests_latency_seconds] = cls.karapace_http_requests_latency_seconds
app.app[cls.karapace_http_requests_duration_seconds] = cls.karapace_http_requests_duration_seconds
app.app[cls.karapace_http_requests_in_progress] = cls.karapace_http_requests_in_progress

@classmethod
async def serve_metrics(cls: PrometheusInstrumentation) -> bytes:
async def serve_metrics(cls) -> bytes:
return generate_latest(cls.registry)

@classmethod
@middleware
async def http_request_metrics_middleware(
cls: PrometheusInstrumentation,
cls,
request: Request,
handler: RequestHandler,
) -> None:
handler: Callable[[Request], Awaitable[Response]],
) -> Response:
request[cls.START_TIME_REQUEST_KEY] = time.time()

# Extract request labels
Expand All @@ -86,7 +91,7 @@ async def http_request_metrics_middleware(
response: Response = await handler(request)

# Instrument request latency
request.app[cls.karapace_http_requests_latency_seconds].labels(method, path).observe(
request.app[cls.karapace_http_requests_duration_seconds].labels(method, path).observe(
time.time() - request[cls.START_TIME_REQUEST_KEY]
)

Expand Down
3 changes: 1 addition & 2 deletions karapace/karapace_all.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,8 @@ def main() -> int:
logging.log(logging.DEBUG, "Config %r", config_without_secrets)

try:
# `close` will be called by the callback `close_by_app` set by `KarapaceBase`
PrometheusInstrumentation.setup_metrics(app=app)
app.run()
app.run() # `close` will be called by the callback `close_by_app` set by `KarapaceBase`
except Exception as ex: # pylint: disable-broad-except
app.stats.unexpected_exception(ex=ex, where="karapace")
raise
Expand Down
Empty file.
30 changes: 30 additions & 0 deletions tests/integration/instrumentation/test_prometheus.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"""
karapace - prometheus instrumentation tests
Copyright (c) 2024 Aiven Ltd
See LICENSE for details
"""

from http import HTTPStatus
from karapace.client import Client, Result
from karapace.instrumentation.prometheus import PrometheusInstrumentation
from prometheus_client.parser import text_string_to_metric_families


async def test_metrics_endpoint(registry_async_client: Client) -> None:
result: Result = await registry_async_client.get(
PrometheusInstrumentation.METRICS_ENDPOINT_PATH,
json_response=False,
)
assert result.status_code == HTTPStatus.OK.value


async def test_metrics_endpoint_parsed_response(registry_async_client: Client) -> None:
result: Result = await registry_async_client.get(
PrometheusInstrumentation.METRICS_ENDPOINT_PATH,
json_response=False,
)
metrics = [family.name for family in text_string_to_metric_families(result.json_result)]
assert "karapace_http_requests" in metrics
assert "karapace_http_requests_duration_seconds" in metrics
assert "karapace_http_requests_in_progress" in metrics
12 changes: 6 additions & 6 deletions tests/unit/instrumentation/test_prometheus.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,15 @@ def test_constants(self, prometheus: PrometheusInstrumentation) -> None:

def test_metric_types(self, prometheus: PrometheusInstrumentation) -> None:
assert isinstance(prometheus.karapace_http_requests_total, Counter)
assert isinstance(prometheus.karapace_http_requests_latency_seconds, Histogram)
assert isinstance(prometheus.karapace_http_requests_duration_seconds, Histogram)
assert isinstance(prometheus.karapace_http_requests_in_progress, Gauge)

def test_metric_values(self, prometheus: PrometheusInstrumentation) -> None:
# `_total` suffix is stripped off the metric name for `Counters`, but needed for clarity.
assert repr(prometheus.karapace_http_requests_total) == "prometheus_client.metrics.Counter(karapace_http_requests)"
assert (
repr(prometheus.karapace_http_requests_latency_seconds)
== "prometheus_client.metrics.Histogram(karapace_http_requests_latency_seconds)"
repr(prometheus.karapace_http_requests_duration_seconds)
== "prometheus_client.metrics.Histogram(karapace_http_requests_duration_seconds)"
)
assert (
repr(prometheus.karapace_http_requests_in_progress)
Expand All @@ -62,8 +62,8 @@ def test_setup_metrics(self, caplog: LogCaptureFixture, prometheus: PrometheusIn
[
call(prometheus.karapace_http_requests_total, prometheus.karapace_http_requests_total),
call(
prometheus.karapace_http_requests_latency_seconds,
prometheus.karapace_http_requests_latency_seconds,
prometheus.karapace_http_requests_duration_seconds,
prometheus.karapace_http_requests_duration_seconds,
),
call(prometheus.karapace_http_requests_in_progress, prometheus.karapace_http_requests_in_progress),
]
Expand Down Expand Up @@ -99,7 +99,7 @@ async def test_http_request_metrics_middleware(
call().inc(),
]
)
request.app[prometheus.karapace_http_requests_latency_seconds].labels.assert_has_calls(
request.app[prometheus.karapace_http_requests_duration_seconds].labels.assert_has_calls(
[
call("GET", "/path"),
call().observe(request.__getitem__.return_value.__rsub__.return_value),
Expand Down

0 comments on commit 40bc4a3

Please sign in to comment.