diff --git a/docs/operate/prometheus.rst b/docs/operate/prometheus.rst new file mode 100644 index 00000000..2a912afc --- /dev/null +++ b/docs/operate/prometheus.rst @@ -0,0 +1,30 @@ +Prometheus +========== + +.. warning:: + + This feature is mostly for internal use for now, and although we'll keep a prometheus integration in the future, + configuration will change. + +Enable the middleware using ... + +.. code-block:: bash + + USE_ASGI_PROMETHEUS_MIDDLEWARE=true harp ... + +Then you can access the metrics at ``/.prometheus/metrics``. + +Here is an example scrape configuration for prometheus: + +.. code-block:: yaml + + scrape_configs: + - job_name: harp + honor_timestamps: true + scrape_interval: 10s + scrape_timeout: 10s + metrics_path: /.prometheus/metrics + scheme: http + static_configs: + - targets: + - url.or.ip.for.harp.example.com:4080 diff --git a/harp/config/adapters/hypercorn.py b/harp/config/adapters/hypercorn.py index 1354472e..9994d521 100644 --- a/harp/config/adapters/hypercorn.py +++ b/harp/config/adapters/hypercorn.py @@ -5,6 +5,7 @@ from hypercorn.utils import LifespanFailureError from harp import get_logger +from harp.utils.env import get_bool_from_env logger = get_logger(__name__) @@ -34,11 +35,19 @@ async def serve(self): from hypercorn.asyncio import serve asgi_app, binds = await self.factory.build() + + if get_bool_from_env("USE_ASGI_PROMETHEUS_MIDDLEWARE", False): + from asgi_prometheus import PrometheusMiddleware + + _metrics_url = "/.prometheus/metrics" + asgi_app = PrometheusMiddleware(asgi_app, metrics_url=_metrics_url, group_paths=["/"]) + logger.info(f"🌎 PrometheusMiddleware enabled, metrics under {_metrics_url}.") + config = self._create_config(binds) logger.debug(f"🌎 {type(self).__name__}::serve({', '.join(config.bind)})") try: - return await serve(asgi_app, config) + return await serve(asgi_app, config, mode="asgi") except LifespanFailureError as exc: logger.exception(f"Server initiliation failed: {repr(exc.__cause__)}", exc_info=exc.__cause__) if isinstance(exc.__cause__, PostgresError): diff --git a/harp/utils/env.py b/harp/utils/env.py index b59100a5..ff81ce85 100644 --- a/harp/utils/env.py +++ b/harp/utils/env.py @@ -20,6 +20,6 @@ def get_bool_from_env(key, default): return default try: - return cast_bool(key) + return cast_bool(value) except ValueError: raise ValueError(f"Invalid boolean value for {key}: {value!r}") diff --git a/harp/utils/tests/test_env.py b/harp/utils/tests/test_env.py new file mode 100644 index 00000000..2ab27d08 --- /dev/null +++ b/harp/utils/tests/test_env.py @@ -0,0 +1,28 @@ +from unittest.mock import patch + +import pytest + +from harp.utils.env import cast_bool, get_bool_from_env + +parametrize_with_string_booleans = pytest.mark.parametrize( + "value, expected", + [ + ("true", True), + ("yes", True), + ("1", True), + ("false", False), + ("no", False), + ("0", False), + ], +) + + +@parametrize_with_string_booleans +def test_cast_bool(value, expected): + assert cast_bool(value) == expected + + +@parametrize_with_string_booleans +def test_get_bool_from_env(value, expected): + with patch.dict("os.environ", {"TEST_ENV_VAR": value}): + assert get_bool_from_env("TEST_ENV_VAR", False) == expected diff --git a/poetry.lock b/poetry.lock index 9eea1da7..029151a9 100644 --- a/poetry.lock +++ b/poetry.lock @@ -224,6 +224,25 @@ doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphin test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] trio = ["trio (>=0.23)"] +[[package]] +name = "asgi-prometheus" +version = "1.1.2" +description = "Support Prometheus metrics for ASGI applications" +optional = false +python-versions = ">=3.7" +files = [ + {file = "asgi-prometheus-1.1.2.tar.gz", hash = "sha256:bcfe83b8c1391378ab7826b9ea5171e779d83e09407c7d9f84028f909361fd73"}, + {file = "asgi_prometheus-1.1.2-py3-none-any.whl", hash = "sha256:8199c6ed19b4f71065e3e5e9f12cb26d647f2c57e9f682f35eb48a212be74e7b"}, +] + +[package.dependencies] +asgi-tools = ">=0.71.0" +prometheus-client = ">=0.10.1" + +[package.extras] +dev = ["bump2version", "pre-commit", "refurb", "tox"] +tests = ["pytest", "pytest-aio[curio,trio] (>=1.1.0)", "pytest-mypy", "ruff"] + [[package]] name = "asgi-tools" version = "0.76.0" @@ -1941,6 +1960,20 @@ files = [ {file = "priority-2.0.0.tar.gz", hash = "sha256:c965d54f1b8d0d0b19479db3924c7c36cf672dbf2aec92d43fbdaf4492ba18c0"}, ] +[[package]] +name = "prometheus-client" +version = "0.20.0" +description = "Python client for the Prometheus monitoring system." +optional = false +python-versions = ">=3.8" +files = [ + {file = "prometheus_client-0.20.0-py3-none-any.whl", hash = "sha256:cde524a85bce83ca359cc837f28b8c0db5cac7aa653a588fd7e84ba061c329e7"}, + {file = "prometheus_client-0.20.0.tar.gz", hash = "sha256:287629d00b147a32dcb2be0b9df905da599b2d82f80377083ec8463309a4bb89"}, +] + +[package.extras] +twisted = ["twisted"] + [[package]] name = "psutil" version = "5.9.8" @@ -3642,4 +3675,4 @@ dev = ["honcho", "watchfiles"] [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "d5c888e9d669af3a02ed55feee4639bc2fb6b63d61cf806f176cd92f4fa92bf7" +content-hash = "8409ac0a705f380703c200f7c7b405e002e86725ce4a5b3fc6a3110e36b39ea3" diff --git a/pyproject.toml b/pyproject.toml index 741b4697..a383dcd3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,6 +47,7 @@ svix-ksuid = "^0.6.2" watchfiles = { version = "^0.22.0", optional = true } whistle = { version = "2.0.0b1", allow-prereleases = true } pyheck = "^0.1.5" +asgi-prometheus = "^1.1.2" [tool.poetry.group.dev.dependencies]