diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 72314cc..0d6a417 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,12 +13,12 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.6", "3.7", "3.8", "3.9", "3.10"] + python-version: ["3.8", "3.9", "3.10", "3.11"] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install dependencies @@ -39,17 +39,10 @@ jobs: - name: Check code style run: | make check-style - - name: Install package on Python 3.6 - # Quart is not supported on Python 3.6 - if: ${{ matrix.python-version == '3.6' }} - run: | - echo "Python version is: ${{ matrix.python-version }}" - pip install .[aiohttp,binary,starlette] - name: Install package - if: ${{ matrix.python-version != '3.6' }} run: | echo "Python version is: ${{ matrix.python-version }}" - pip install .[aiohttp,binary,starlette,quart] + pip install .[aiohttp,starlette,quart] - name: Run unit tests run: | make test diff --git a/.github/workflows/scheduled.yml b/.github/workflows/scheduled.yml index 8e1818d..4120818 100644 --- a/.github/workflows/scheduled.yml +++ b/.github/workflows/scheduled.yml @@ -17,9 +17,9 @@ jobs: matrix: python-version: ["3.9"] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install dependencies diff --git a/.pylintrc b/.pylintrc index 0124967..bd705dd 100644 --- a/.pylintrc +++ b/.pylintrc @@ -15,12 +15,12 @@ suggestion-mode=yes disable=invalid-name, broad-except, + broad-exception-raised, duplicate-code, logging-fstring-interpolation, missing-class-docstring, missing-module-docstring, missing-function-docstring, - no-self-use, too-many-instance-attributes, too-many-arguments, too-few-public-methods, diff --git a/CHANGELOG.md b/CHANGELOG.md index de9b037..772340f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,33 @@ ## XX.Y.Z +## 23.3.0 + +- Added support for Histogram metric in timer decorator +- Update docs to demonstrate how to use basic authentication with Pusher. +- Developer updates + - Removed isort optional extra 'deprecated_imports_finder' as it isn't supported anymore. + - Minor type annotations updates to keep mypy happy + - Updated '.pylintrc' to fix warnings about options that are no longer supported. + - Updated Sphinx config to specify language to avoid warning being reported. + - Minor updates to address pylint warnings + - Silence orjson no-members warnings (See: https://github.com/ijl/orjson/issues/248) + - Added httpx as developmental dependency so that the Starlette test client can be used. + - Update ASGI middleware to obtain starlette app reference from 'http' ASGI scope when run from Starlette test client. + - Updated Pusher unit test to check basic authentication. + - Added aiohttp_basicauth to dev dependencies. + - Fix CI + - Removed support for Python3.6 as it isn't supported by Github actions anymore. + - Updated repo to indicate minimum supported Python is 3.8+ + - Removed dependency on asynctest package which is no longer maintained and causes errors in Python3.11. + - Using 'unittest.IsolatedAsyncioTestCase' instead, but this is only supported for 3.8+. + - Updated CI workflow 'uses' items to use later versions +- Prometheus 2.0 removed support for the binary protocol. Removed support for Prometheus binary protocol (fixes #57). + - Updated unit tests + - Updated CI + - Updated docs + - Updated examples + ## 22.5.0 - Fix CI package install issue related to pip (#78) diff --git a/Makefile b/Makefile index 46c8005..1495b8f 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ # This makefile has been created to help developers perform common actions. # It assumes it is operating in an environment, such as a virtual env, -# where the python command links to the Python3.7 executable. +# where the python command links to the Python3.11 executable. # Do not remove this block. It is used by the 'help' rule when @@ -9,7 +9,7 @@ # help: aioprometheus Makefile help # help: -PYTHON_VERSION := python3.9 +PYTHON_VERSION := python3.11 VENV_DIR := venv # help: help - display this makefile's help information @@ -22,9 +22,9 @@ help: .PHONY: venv venv: @rm -Rf "$(VENV_DIR)" - @$(PYTHON_VERSION) -m venv "$(VENV_DIR)" + @$(PYTHON_VERSION) -m venv "$(VENV_DIR)" --prompt aioprom @/bin/bash -c "source $(VENV_DIR)/bin/activate && pip install pip --upgrade && pip install -r requirements.dev.txt" - @/bin/bash -c "source $(VENV_DIR)/bin/activate && pip install -e .[aiohttp,binary,starlette,quart]" + @/bin/bash -c "source $(VENV_DIR)/bin/activate && pip install -e .[aiohttp,starlette,quart]" @echo "Enter virtual environment using:\n\n\t$ source $(VENV_DIR)/bin/activate\n" diff --git a/README.rst b/README.rst index cf1aff0..41a1062 100644 --- a/README.rst +++ b/README.rst @@ -6,7 +6,7 @@ aioprometheus `aioprometheus` is a Prometheus Python client library for asyncio-based applications. It provides metrics collection and serving capabilities for use with Prometheus and compatible monitoring systems. It supports exporting -metrics into text and binary formats and pushing metrics to a gateway. +metrics into text and pushing metrics to a gateway. The ASGI middleware in `aioprometheus` can be used in FastAPI/Starlette and Quart applications. `aioprometheus` can also be used in other kinds of asyncio @@ -50,20 +50,11 @@ dependencies are not installed by default. You can install them alongside $ pip install aioprometheus[aiohttp] -Prometheus 2.0 removed support for the binary protocol, so in version 20.0.0 the -dependency on `prometheus-metrics-proto`, which provides binary support, is now -optional. If you need binary response support, for use with an older Prometheus, -you will need to specify the 'binary' optional extra: - -.. code-block:: console - - $ pip install aioprometheus[binary] - Multiple optional dependencies can be listed at once, such as: .. code-block:: console - $ pip install aioprometheus[aiohttp,binary,starlette,quart] + $ pip install aioprometheus[aiohttp,starlette,quart] Usage @@ -159,9 +150,15 @@ The next example shows how to use the Service HTTP endpoint to provide a dedicated metrics endpoint for other applications such as long running distributed system processes. +The Service object requires optional extras to be installed so make sure you +install aioprometheus with the 'aiohttp' extras. + +.. code-block:: console + + $ pip install aioprometheus[aiohttp] + .. code-block:: python - #!/usr/bin/env python """ This example demonstrates how the ``aioprometheus.Service`` can be used to expose metrics on a HTTP endpoint. @@ -226,13 +223,6 @@ the Service uses the default collector registry, which is ``aioprometheus.REGISTRY``. The Service can be configured to use a different registry by passing one in as an argument to the Service constructor. -The Service object requires optional extras to be installed so make sure you -install aioprometheus with the 'aiohttp' extras. - -.. code-block:: console - - $ pip install aioprometheus[aiohttp] - License ------- diff --git a/docs/conf.py b/docs/conf.py index 54f20e6..c59e8fb 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -65,7 +65,7 @@ # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. -language = None +language = "en" # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: diff --git a/docs/dev/index.rst b/docs/dev/index.rst index 992360a..510264d 100644 --- a/docs/dev/index.rst +++ b/docs/dev/index.rst @@ -9,7 +9,7 @@ If you have found a bug or have an idea for an enhancement that would improve the library, use the `bug tracker `_. -To develop `aioprometheus` you'll need Python 3.6+, some dependencies and +To develop `aioprometheus` you'll need Python 3.8+, some dependencies and the source code. @@ -41,6 +41,7 @@ commands are pointing at the correct tools. $ source venv/bin/activate (aioprom) $ (aioprom) $ pip install pip --upgrade + (aioprom) $ pip install wheel .. note:: @@ -56,13 +57,13 @@ Rules in the convenience Makefile depend on the development dependencies being installed. The development dependencies also include various web application frameworks to assist verifying integration methods. Install the developmental dependencies using ``pip``. Then install the `aioprometheus` -package (and its optional dependencies). in a way that allows you to edit the +package (and its optional dependencies) in a way that allows you to edit the code after it is installed so that any changes take effect immediately. .. code-block:: console (aioprom) $ pip install -r requirements.dev.txt - (aioprom) $ pip install -e .[aiohttp,binary] + (aioprom) $ pip install -e .[aiohttp,starlette,quart] Code Style diff --git a/docs/index.rst b/docs/index.rst index ec2aed2..5358729 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -15,8 +15,8 @@ aioprometheus `aioprometheus` is a Prometheus Python client library for asyncio-based applications. It provides metrics collection and serving capabilities for use with `Prometheus `_ and compatible monitoring -systems. It supports exporting metrics into text and binary formats and -pushing metrics to a gateway. +systems. It supports exporting metrics into text format and pushing metrics +to a gateway. `aioprometheus` can be used in applications built with FastAPI/Starlette, Quart, aiohttp as well as networking apps built upon asyncio. @@ -41,9 +41,9 @@ Origins package. Many thanks to `slok `_ for developing `prometheus-python`. -The original work has been modified and updated it to meet the needs of asyncio -applications by adding the histogram metric, binary format support, docs, -decorators, ASGI middleware, etc. +The original work has been modified and updated to support the needs of +asyncio applications by adding the histogram metric, docs, decorators, ASGI +middleware, etc. .. |ci status| image:: https://github.com/claws/aioprometheus/workflows/CI%20Pipeline/badge.svg?branch=master diff --git a/docs/user/index.rst b/docs/user/index.rst index d48a739..4c1fdd8 100644 --- a/docs/user/index.rst +++ b/docs/user/index.rst @@ -41,20 +41,11 @@ dependencies are not installed by default. You can install them alongside $ pip install aioprometheus[aiohttp] -Prometheus 2.0 removed support for the binary protocol, so in version 20.0.0 the -dependency on `prometheus-metrics-proto`, which provides binary support, is now -optional. If you need binary response support, for use with an older Prometheus, -you will need to specify the 'binary' optional extra: - -.. code-block:: console - - $ pip install aioprometheus[binary] - Multiple optional dependencies can be listed at once, such as: .. code-block:: console - $ pip install aioprometheus[aiohttp,binary,starlette,quart] + $ pip install aioprometheus[aiohttp,starlette,quart] .. _usage-label: @@ -484,13 +475,38 @@ install aioprometheus with the 'aiohttp' extras. from aioprometheus.pusher import Pusher PUSH_GATEWAY_ADDR = "http://127.0.0.1:61423" - pusher = Pusher("my-job", PUSH_GATEWAY_ADDR, grouping_key={"instance": "127.0.0.1:1234"}) - c = Counter("total_requests", "Total requests.", {}) - c.inc({'url': "/p/user"}) + async def main(): + pusher = Pusher("my-job", PUSH_GATEWAY_ADDR, grouping_key={"instance": "127.0.0.1:1234"}) + c = Counter("total_requests", "Total requests.", {}) + c.inc({'url': "/p/user"}) + resp = await pusher.replace(REGISTRY) + + if __name__ == "__main__": + asyncio.run(main()) + +The Pusher API supports passing 'kwargs' on to the underlying client session +used to push updates to the Gateway. This feature provides the ability to +supply configuration information such as authentication parameters. The example +code below shows how you can use basic authentication with the Pusher. + +.. code-block:: python + + import aiohttp + from aioprometheus import REGISTRY, Counter + from aioprometheus.pusher import Pusher + + PUSH_GATEWAY_ADDR = "http://127.0.0.1:61423" + AUTH = aiohttp.BasicAuth("Joe", password="4321") + + async def main(): + pusher = Pusher("my-job", PUSH_GATEWAY_ADDR) + c = Counter("total_requests", "Total requests.", {}) + c.inc({'url': "/p/user"}) + resp = await pusher.replace(REGISTRY, auth=AUTH) - # Push to the push gateway - resp = await pusher.replace(REGISTRY) + if __name__ == "__main__": + asyncio.run(main()) Using Prometheus To Check Examples diff --git a/examples/decorators/decorator_count_exceptions.py b/examples/decorators/decorator_count_exceptions.py index 92b5e22..8f0f9c4 100644 --- a/examples/decorators/decorator_count_exceptions.py +++ b/examples/decorators/decorator_count_exceptions.py @@ -1,6 +1,7 @@ -#!/usr/bin/env python """ +Usage: + .. code-block:: python $ python decorator_count_exceptions.py @@ -30,6 +31,7 @@ ) REQUESTS = Counter("request_total", "Total number of requests") + # Decorate function with metric. @count_exceptions(REQUEST_EXCEPTIONS, {"route": "/"}) async def handle_request(duration): @@ -52,7 +54,6 @@ async def handle_requests(): if __name__ == "__main__": - loop = asyncio.get_event_loop() svr = Service() diff --git a/examples/decorators/decorator_inprogress.py b/examples/decorators/decorator_inprogress.py index f7f2be6..0cf70c1 100644 --- a/examples/decorators/decorator_inprogress.py +++ b/examples/decorators/decorator_inprogress.py @@ -1,6 +1,7 @@ -#!/usr/bin/env python """ +Usage: + .. code-block:: python $ python decorator_inprogress.py @@ -26,6 +27,7 @@ REQUESTS_IN_PROGRESS = Gauge("request_in_progress", "Number of requests in progress") REQUESTS = Counter("request_total", "Total number of requests") + # Decorate function with metric. @inprogress(REQUESTS_IN_PROGRESS, {"route": "/"}) async def handle_request(duration): @@ -43,7 +45,6 @@ async def handle_requests(): if __name__ == "__main__": - loop = asyncio.get_event_loop() svr = Service() diff --git a/examples/decorators/decorator_timer.py b/examples/decorators/decorator_timer.py index 20333dc..a87cca2 100644 --- a/examples/decorators/decorator_timer.py +++ b/examples/decorators/decorator_timer.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python """ Usage: @@ -31,6 +30,7 @@ REQUEST_TIME = Summary("request_processing_seconds", "Time spent processing request") REQUESTS = Counter("request_total", "Total number of requests") + # Decorate function with metric. @timer(REQUEST_TIME) async def handle_request(duration): @@ -48,7 +48,6 @@ async def handle_requests(): if __name__ == "__main__": - loop = asyncio.get_event_loop() svr = Service() diff --git a/examples/frameworks/aiohttp-example.py b/examples/frameworks/aiohttp-example.py index b1a5956..1c71ab0 100644 --- a/examples/frameworks/aiohttp-example.py +++ b/examples/frameworks/aiohttp-example.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python """ Sometimes you may not want to expose Prometheus metrics from a dedicated Prometheus metrics server but instead want to use an existing web framework. diff --git a/examples/frameworks/fastapi-example.py b/examples/frameworks/fastapi-example.py index 390cd13..cc0d0fb 100644 --- a/examples/frameworks/fastapi-example.py +++ b/examples/frameworks/fastapi-example.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python """ This example adds Prometheus metrics to a FastAPI application. In this example a counter metric is instantiated and gets updated whenever the "/" @@ -9,11 +8,20 @@ renders Prometheus metrics from the default collector registry into the appropriate format. +Setup: + + (venv) $ pip install fastapi uvicorn aioprometheus[starlette] + Run: - (venv) $ pip install fastapi uvicorn (venv) $ python fastapi-example.py +Test: + + (venv) $ curl http://127.0.0.1:8000/metrics + (venv) $ curl http://127.0.0.1:8000/ + (venv) $ curl http://127.0.0.1:8000/metrics + """ from typing import List diff --git a/examples/frameworks/fastapi-middleware.py b/examples/frameworks/fastapi-middleware.py index beda76e..9800999 100644 --- a/examples/frameworks/fastapi-middleware.py +++ b/examples/frameworks/fastapi-middleware.py @@ -1,14 +1,24 @@ -#!/usr/bin/env python """ This example shows how to use the aioprometheus ASGI middleware in a FastAPI application. FastAPI is built upon Starlette so using the middleware in Starlette would be the same. +Setup: + + (venv) $ pip install fastapi uvicorn aioprometheus[starlette] + Run: - (venv) $ pip install fastapi uvicorn (venv) $ python fastapi-middleware.py +Test: + + (venv) $ curl http://127.0.0.1:8000/metrics + (venv) $ curl http://127.0.0.1:8000/ + (venv) $ curl http://127.0.0.1:8000/metrics + (venv) $ curl http://127.0.0.1:8000/users/bob + (venv) $ curl http://127.0.0.1:8000/metrics + """ from fastapi import FastAPI, Request, Response diff --git a/examples/frameworks/quart-example.py b/examples/frameworks/quart-example.py index 02dab15..d171904 100644 --- a/examples/frameworks/quart-example.py +++ b/examples/frameworks/quart-example.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python """ Sometimes you may not want to expose Prometheus metrics from a dedicated Prometheus metrics server but instead want to use an existing web framework. @@ -10,11 +9,20 @@ web framework method. The metrics route renders Prometheus metrics into the appropriate format. +Setup: + + (venv) $ pip install quart aioprometheus[quart] + Run: - (venv) $ pip install quart (venv) $ python quart-example.py +Test: + + (venv) $ curl http://127.0.0.1:8000/metrics + (venv) $ curl http://127.0.0.1:8000/ + (venv) $ curl http://127.0.0.1:8000/metrics + """ from quart import Quart, request diff --git a/examples/frameworks/quart-middleware.py b/examples/frameworks/quart-middleware.py index 04eff91..23ee276 100644 --- a/examples/frameworks/quart-middleware.py +++ b/examples/frameworks/quart-middleware.py @@ -1,13 +1,23 @@ -#!/usr/bin/env python """ This example shows how to use the aioprometheus ASGI middleware in a Quart application. +Setup: + + (venv) $ pip install quart aioprometheus[quart] + Run: - (venv) $ pip install quart (venv) $ python quart_middleware.py +Test: + + (venv) $ curl http://127.0.0.1:8000/metrics + (venv) $ curl http://127.0.0.1:8000/ + (venv) $ curl http://127.0.0.1:8000/metrics + (venv) $ curl http://127.0.0.1:8000/users/bob + (venv) $ curl http://127.0.0.1:8000/metrics + """ from quart import Quart, request diff --git a/examples/metrics-fetcher.py b/examples/metrics-fetcher.py index 9667bde..c3ca381 100644 --- a/examples/metrics-fetcher.py +++ b/examples/metrics-fetcher.py @@ -1,93 +1,63 @@ -#!/usr/bin/env python """ This script implements a fetching function that emulates a Prometheus server -scraping a metrics service endpoint. The fetching function can randomly -requests metrics in text or binary formats or you can specify a format to -use. +scraping a metrics service endpoint. This script requires some optional extras to be installed. .. code-block:: console - $ pip install aioprometheus[aiohttp,binary] + $ pip install aioprometheus[aiohttp] Usage: .. code-block:: console - $ python metrics-fetcher.py --url http://0.0.0.0:50123/metrics --format=text --interval=2.0 + $ python metrics-fetcher.py --url http://0.0.0.0:50123/metrics --interval=2.0 """ import argparse import asyncio import logging -import random import aiohttp -import prometheus_metrics_proto from aiohttp.hdrs import ACCEPT, CONTENT_TYPE from aioprometheus import formats -TEXT = "text" -BINARY = "binary" -header_kinds = { - TEXT: formats.text.TEXT_CONTENT_TYPE, - BINARY: formats.binary.BINARY_CONTENT_TYPE, -} - async def fetch_metrics( url: str, - fmt: str = None, interval: float = 1.0, ): - """Fetch metrics from the service endpoint using different formats. + """Fetch metrics from the service endpoint. - This coroutine runs 'n' times, with a brief interval in between, before - exiting. + This coroutine runs forever, with a brief interval in between calls. """ - if fmt is None: - # Randomly choose a format to request metrics in. - choice = random.choice((TEXT, BINARY)) - else: - assert fmt in header_kinds - choice = fmt - - print(f"fetching metrics in {choice} format") async with aiohttp.ClientSession() as session: - async with session.get(url, headers={ACCEPT: header_kinds[choice]}) as resp: - assert resp.status == 200 - content = await resp.read() - content_type = resp.headers.get(CONTENT_TYPE) - print(f"Content-Type: {content_type}") - print(f"size: {len(content)}") - if choice == "text": - print(content.decode()) - else: - print(content) - # Decode the binary metrics into protobuf objects - print(prometheus_metrics_proto.decode(content)) - - # schedule another fetch - asyncio.get_event_loop().call_later(interval, fetch_task, url, fmt, interval) - - -def fetch_task(url, fmt, interval): - asyncio.ensure_future(fetch_metrics(url, fmt, interval)) + while True: + try: + print("Fetching metrics") + async with session.get( + url, headers={ACCEPT: formats.text.TEXT_CONTENT_TYPE} + ) as resp: + assert resp.status == 200 + content = await resp.read() + content_type = resp.headers.get(CONTENT_TYPE) + print(f"Content-Type: {content_type}, size: {len(content)}") + print(content.decode()) + print("") + + # Wait briefly before fetching again + await asyncio.sleep(interval) + + except asyncio.CancelledError: + return if __name__ == "__main__": - ARGS = argparse.ArgumentParser(description="Metrics Fetcher") ARGS.add_argument("--url", type=str, default=None, help="The metrics URL") - ARGS.add_argument( - "--format", - type=str, - default=None, - help="Metrics response format (i.e. 'text' or 'binary'", - ) ARGS.add_argument( "--interval", type=float, @@ -106,14 +76,7 @@ def fetch_task(url, fmt, interval): logging.getLogger("asyncio").setLevel(logging.ERROR) logging.getLogger("aiohttp").setLevel(logging.ERROR) - loop = asyncio.get_event_loop() - - # create a task to fetch metrics at a periodic interval - loop.call_later(args.interval, fetch_task, args.url, args.format, args.interval) - try: - loop.run_forever() + asyncio.run(fetch_metrics(args.url, args.interval)) except KeyboardInterrupt: pass - loop.stop() - loop.close() diff --git a/examples/service/app-service-example.py b/examples/service/app-service-example.py index 8fcd415..1762dc1 100644 --- a/examples/service/app-service-example.py +++ b/examples/service/app-service-example.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python """ This example implements an application that exposes application metrics obtained from the psutil package. @@ -35,7 +34,6 @@ def __init__( metrics_host="127.0.0.1", metrics_port: int = 8000, ): - self.metrics_host = metrics_host self.metrics_port = metrics_port self.timer = None # type: asyncio.Handle @@ -128,7 +126,6 @@ def on_timer_expiry(self): if __name__ == "__main__": - logging.basicConfig(level=logging.DEBUG) # Silence asyncio and aiohttp loggers logging.getLogger("asyncio").setLevel(logging.ERROR) diff --git a/examples/service/simple-service-example.py b/examples/service/simple-service-example.py index 62508cd..c6a2954 100644 --- a/examples/service/simple-service-example.py +++ b/examples/service/simple-service-example.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python """ This example demonstrates how the ``aioprometheus.Service`` can be used to expose metrics on a HTTP endpoint. @@ -21,7 +20,6 @@ async def main(): - service = Service() events_counter = Counter( "events", "Number of events.", const_labels={"host": socket.gethostname()} @@ -44,7 +42,6 @@ async def updater(c: Counter): if __name__ == "__main__": - try: asyncio.run(main()) except KeyboardInterrupt: diff --git a/requirements.dev.txt b/requirements.dev.txt index 0daad59..9610a24 100644 --- a/requirements.dev.txt +++ b/requirements.dev.txt @@ -1,9 +1,8 @@ -asynctest black sphinx sphinx-material coverage -isort[requirements_deprecated_finder] +isort mypy pylint twine @@ -11,6 +10,8 @@ wheel # web app frameworks are used in ASGI middleware and unit tests to verify integration aiohttp +aiohttp_basicauth +httpx # provides fastapi with test client fastapi uvicorn quart; python_version > '3.7' diff --git a/setup.py b/setup.py index 2b68992..589cc6d 100644 --- a/setup.py +++ b/setup.py @@ -35,7 +35,6 @@ def parse_requirements(filename): if __name__ == "__main__": - setup( name="aioprometheus", version=version, @@ -51,25 +50,23 @@ def parse_requirements(filename): package_data={ "aioprometheus": ["py.typed"], }, - python_requires=">=3.6.0", + python_requires=">=3.8.0", install_requires=requirements, extras_require={ "aiohttp": ["aiohttp>=3.3.2"], - "binary": ["prometheus-metrics-proto>=18.1.1"], "starlette": ["starlette>=0.14.2"], "quart": ["quart>=0.15.1"], }, classifiers=[ - "Development Status :: 4 - Beta", + "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: System :: Monitoring", "Typing :: Typed", diff --git a/src/aioprometheus/__init__.py b/src/aioprometheus/__init__.py index 8fb56cf..27b7c0f 100644 --- a/src/aioprometheus/__init__.py +++ b/src/aioprometheus/__init__.py @@ -9,4 +9,4 @@ # The 'pusher' and 'service' modules must be explicitly imported by package # users as they depend on optional extras. -__version__ = "22.5.0" +__version__ = "23.3.0" diff --git a/src/aioprometheus/asgi/middleware.py b/src/aioprometheus/asgi/middleware.py index d044d52..4527548 100644 --- a/src/aioprometheus/asgi/middleware.py +++ b/src/aioprometheus/asgi/middleware.py @@ -1,4 +1,4 @@ -from typing import Any, Awaitable, Callable, Dict, Sequence +from typing import Any, Awaitable, Callable, Dict, Optional, Sequence from aioprometheus import REGISTRY, Counter, Registry from aioprometheus.mypy_types import LabelsType @@ -45,7 +45,7 @@ class MetricsMiddleware: '/users/alice', etc. The template URLS can be more useful than the actual route url as they allow the route handler to be easily identified. This feature is only supported with Starlette / FastAPI - currently. + currently. Default value is True. :param group_status_codes: A boolean that defines whether status codes should be grouped under a value representing that code kind. For @@ -60,15 +60,15 @@ def __init__( exclude_paths: Sequence[str] = EXCLUDE_PATHS, use_template_urls: bool = True, group_status_codes: bool = False, - const_labels: LabelsType = None, + const_labels: Optional[LabelsType] = None, ) -> None: # The 'app' argument really represents an ASGI framework callable. self.asgi_callable = app - # Starlette applications add a reference to the ASGI app in the - # lifespan start scope. Save a reference to the ASGI app to assist - # later when extracting route templates. Only Starlette/FastAPI - # apps provide this feature. + # A reference to the ASGI app is used to assist when extracting + # route template patterns. Only Starlette/FastAPI apps currently + # provide this feature. In normal operations the app reference is + # obtained from the 'lifespan' scope. self.starlette_app = None self.exclude_paths = exclude_paths if exclude_paths else [] @@ -84,7 +84,7 @@ def __init__( # The creation of the middleware metrics is delayed until the first # call to update one of the metrics. This ensures that the metrics # are only created once - even in situations such as Starlette's - # occasionally middleware rebuilding that creates new instances of + # occasional middleware rebuilding that creates new instances of # middleware. This avoids exceptions being raised by the registry # when identical metrics collectors are created. self.metrics_created = False @@ -131,63 +131,68 @@ def create_metrics(self): self.metrics_created = True async def __call__(self, scope: Scope, receive: Receive, send: Send): - if not self.metrics_created: self.create_metrics() - if scope["type"] == "lifespan": - # Starlette adds a reference to the app in the lifespan start - # scope. Store a reference to the app to assist later when - # extracting route templates. - self.starlette_app = scope.get("app") + if self.starlette_app is None: + # To assist with extracting route templates later the middleware + # needs a reference to the starlette app. Fetch it from the scope. + # In normal operations this can be found in the 'lifespan' scope. + # However, in unit tests that use the starlette httpx test client + # it appears that the ASGI 'lifespan' call is not made. In this + # scenario obtain the app reference from the 'http' scope. + if scope["type"] in ("lifespan", "http"): + self.starlette_app = scope.get("app") - if scope["type"] != "http": + if scope["type"] == "lifespan": await self.asgi_callable(scope, receive, send) return - def wrapped_send(response): - """ - Wrap the ASGI send function so that metrics collection can be finished. - """ - # This function makes use of labels defined in the calling context. + if scope["type"] == "http": + + def wrapped_send(response): + """ + Wrap the ASGI send function so that metrics collection can be finished. + """ + # This function makes use of labels defined in the calling context. + + if response["type"] == "http.response.start": + status_code_labels = labels.copy() + status_code = str(response["status"]) + status_code_labels["status_code"] = ( + f"{status_code[0]}xx" + if self.group_status_codes + else status_code + ) + self.status_codes_counter.inc(status_code_labels) + self.responses_counter.inc(labels) + + return send(response) + + # Store HTTP path and method so they can be used later in the send + # method to complete metrics updates. + method = scope["method"] + path = self.get_full_or_template_path(scope) + labels = {"method": method, "path": path} + + if path in self.exclude_paths: + await self.asgi_callable(scope, receive, send) + return + + self.requests_counter.inc(labels) + try: + await self.asgi_callable(scope, receive, wrapped_send) + except Exception: + self.exceptions_counter.inc(labels) - if response["type"] == "http.response.start": status_code_labels = labels.copy() - status_code = str(response["status"]) status_code_labels["status_code"] = ( - f"{status_code[0]}xx" if self.group_status_codes else status_code + "5xx" if self.group_status_codes else "500" ) self.status_codes_counter.inc(status_code_labels) self.responses_counter.inc(labels) - return send(response) - - # Store HTTP path and method attributes in a variable that can be used - # later in the send method to complete metrics updates. - - method = scope["method"] - path = self.get_full_or_template_path(scope) - - if path in self.exclude_paths: - await self.asgi_callable(scope, receive, send) - return - - labels = dict(method=method, path=path) - - self.requests_counter.inc(labels) - try: - await self.asgi_callable(scope, receive, wrapped_send) - except Exception: - self.exceptions_counter.inc(labels) - - status_code_labels = labels.copy() - status_code_labels["status_code"] = ( - "5xx" if self.group_status_codes else "500" - ) - self.status_codes_counter.inc(status_code_labels) - self.responses_counter.inc(labels) - - raise + raise def get_full_or_template_path(self, scope) -> str: """ diff --git a/src/aioprometheus/collectors.py b/src/aioprometheus/collectors.py index d6673ae..bfb2d23 100644 --- a/src/aioprometheus/collectors.py +++ b/src/aioprometheus/collectors.py @@ -1,7 +1,7 @@ import enum import re from collections import OrderedDict -from typing import Dict, List, Sequence, Tuple, Union, cast +from typing import Dict, List, Optional, Sequence, Tuple, Union, cast import orjson import quantile @@ -79,8 +79,8 @@ def __init__( self, name: str, doc: str, - const_labels: LabelsType = None, - registry: "Registry" = None, + const_labels: Optional[LabelsType] = None, + registry: Optional["Registry"] = None, ) -> None: """ :param name: The name of the metric. @@ -164,7 +164,11 @@ def get_all(self) -> List[Tuple[LabelsType, NumericValueType]]: result = [] for k in self.values: # Check if is a single value dict (custom empty key) - key = {} if k == MetricDict.EMPTY_KEY else orjson.loads(k) + key = ( + {} + if k == MetricDict.EMPTY_KEY + else orjson.loads(k) # pylint: disable=no-member + ) result.append((key, self.get(k))) return result @@ -318,8 +322,8 @@ def __init__( self, name: str, doc: str, - const_labels: LabelsType = None, - registry: "Registry" = None, + const_labels: Optional[LabelsType] = None, + registry: Optional["Registry"] = None, invariants: Sequence[Tuple[float, float]] = DEFAULT_INVARIANTS, ) -> None: super().__init__(name, doc, const_labels=const_labels, registry=registry) @@ -414,8 +418,8 @@ def __init__( self, name: str, doc: str, - const_labels: LabelsType = None, - registry: "Registry" = None, + const_labels: Optional[LabelsType] = None, + registry: Optional["Registry"] = None, buckets: Sequence[float] = DEFAULT_BUCKETS, ) -> None: super().__init__(name, doc, const_labels=const_labels, registry=registry) diff --git a/src/aioprometheus/decorators.py b/src/aioprometheus/decorators.py index 05c3672..f4f53f6 100644 --- a/src/aioprometheus/decorators.py +++ b/src/aioprometheus/decorators.py @@ -5,13 +5,13 @@ import asyncio import time from functools import wraps -from typing import Any, Callable, Dict, Union +from typing import Any, Callable, Dict, Optional, Union from .collectors import Counter, Gauge, Histogram, Summary def timer( - metric: Union[Histogram, Summary], labels: Dict[str, str] = None + metric: Union[Histogram, Summary], labels: Optional[Dict[str, str]] = None ) -> Callable[..., Any]: """ This decorator wraps a callable with code to calculate how long the @@ -66,7 +66,9 @@ def func_wrapper(*args, **kwds): return measure -def inprogress(metric: Gauge, labels: Dict[str, str] = None) -> Callable[..., Any]: +def inprogress( + metric: Gauge, labels: Optional[Dict[str, str]] = None +) -> Callable[..., Any]: """ This decorator wraps a callables with code to track whether it is currently in progress. The metric is incremented before calling the callable and is @@ -121,7 +123,7 @@ def func_wrapper(*args, **kwds): def count_exceptions( - metric: Counter, labels: Dict[str, str] = None + metric: Counter, labels: Optional[Dict[str, str]] = None ) -> Callable[..., Any]: """ This decorator wraps a callable with code to count how many times the diff --git a/src/aioprometheus/formats/__init__.py b/src/aioprometheus/formats/__init__.py index 095a582..79a4f0e 100644 --- a/src/aioprometheus/formats/__init__.py +++ b/src/aioprometheus/formats/__init__.py @@ -1,7 +1,2 @@ """ This sub-package implements metrics formatters """ from . import text - -try: - from . import binary -except ImportError: - binary = None # type: ignore diff --git a/src/aioprometheus/formats/binary.py b/src/aioprometheus/formats/binary.py deleted file mode 100644 index 96fca77..0000000 --- a/src/aioprometheus/formats/binary.py +++ /dev/null @@ -1,201 +0,0 @@ -""" -This module implements a formatter that emits metrics in a binary (Google -Protocol Buffers) format. -""" - -# imports only used for type annotations -from typing import Callable, List, Optional, cast - -import prometheus_metrics_proto as pmp - -from aioprometheus.collectors import ( - Collector, - Counter, - Gauge, - Histogram, - Registry, - Summary, -) -from aioprometheus.mypy_types import ( - HistogramDictType, - LabelsType, - MetricTupleType, - SummaryDictType, -) - -from .base import IFormatter - -# typing aliases -FormatterFuncType = Callable[[MetricTupleType, str, LabelsType], pmp.Metric] - - -BINARY_CONTENT_TYPE = ( - "application/vnd.google.protobuf; " - "proto=io.prometheus.client.MetricFamily; " - "encoding=delimited" -) -BINARY_ACCEPTS = set(BINARY_CONTENT_TYPE.split("; ")) - - -class BinaryFormatter(IFormatter): - """This formatter encodes into the Protocol Buffers binary format""" - - def __init__(self, timestamp: bool = False) -> None: - """ - :param timestamp: a boolean flag that when True will add a timestamp - to metric. - """ - self.timestamp = timestamp - - def get_headers(self) -> LabelsType: - """Returns a dict of HTTP headers for this response format""" - return {"Content-Type": BINARY_CONTENT_TYPE} - - def _format_counter( - self, counter: MetricTupleType, name: str, const_labels: LabelsType - ) -> pmp.Metric: - """Create a Counter metric instance. - - :param counter: a 2-tuple containing labels and the counter value. - :param name: the metric name. - :param const_labels: a dict of constant labels to be associated with - the metric. - """ - counter_labels, counter_value = counter - - metric = pmp.utils.create_counter_metric( - counter_labels, - counter_value, - timestamp=self.timestamp, - const_labels=const_labels, - ordered=True, - ) - - return metric - - def _format_gauge( - self, gauge: MetricTupleType, name: str, const_labels: LabelsType - ) -> pmp.Metric: - """Create a Gauge metric instance. - - :param gauge: a 2-tuple containing labels and the gauge value. - :param name: the metric name. - :param const_labels: a dict of constant labels to be associated with - the metric. - """ - gauge_labels, gauge_value = gauge - - metric = pmp.utils.create_gauge_metric( - gauge_labels, - gauge_value, - timestamp=self.timestamp, - const_labels=const_labels, - ordered=True, - ) - - return metric - - def _format_summary( - self, summary: MetricTupleType, name: str, const_labels: LabelsType - ) -> pmp.Metric: - """Create a Summary metric instance. - - :param summary: a 2-tuple containing labels and a dict representing - the summary value. The dict contains keys for each quantile as - well as the sum and count fields. - :param name: the metric name. - :param const_labels: a dict of constant labels to be associated with - the metric. - """ - - summary_labels, summary_value_dict = summary - # typing check, no runtime behaviour. - summary_value_dict = cast(SummaryDictType, summary_value_dict) - - metric = pmp.utils.create_summary_metric( - summary_labels, - summary_value_dict, - samples_count=summary_value_dict["count"], - samples_sum=summary_value_dict["sum"], - timestamp=self.timestamp, - const_labels=const_labels, - ordered=True, - ) - - return metric - - def _format_histogram( - self, histogram: MetricTupleType, name: str, const_labels: LabelsType - ) -> pmp.Metric: - """ - :param histogram: a 2-tuple containing labels and a dict representing - the histogram value. The dict contains keys for each bucket as - well as the sum and count fields. - :param name: the metric name. - :param const_labels: a dict of constant labels to be associated with - the metric. - """ - histogram_labels, histogram_value_dict = histogram - # typing check, no runtime behaviour. - histogram_value_dict = cast(HistogramDictType, histogram_value_dict) - - metric = pmp.utils.create_histogram_metric( - histogram_labels, - histogram_value_dict, - samples_count=histogram_value_dict["count"], - samples_sum=histogram_value_dict["sum"], - timestamp=self.timestamp, - const_labels=const_labels, - ordered=True, - ) - - return metric - - def marshall_collector(self, collector: Collector) -> pmp.MetricFamily: - """ - Marshalls a collector into a :class:`MetricFamily` object representing - the metrics in the collector. - - :return: a :class:`MetricFamily` object - """ - exec_method = None # type: Optional[FormatterFuncType] - if isinstance(collector, Counter): - metric_type = pmp.COUNTER - exec_method = self._format_counter - elif isinstance(collector, Gauge): - metric_type = pmp.GAUGE - exec_method = self._format_gauge - elif isinstance(collector, Summary): - metric_type = pmp.SUMMARY - exec_method = self._format_summary - elif isinstance(collector, Histogram): - metric_type = pmp.HISTOGRAM - exec_method = self._format_histogram - else: - raise TypeError("Not a valid object format") - - metrics = [] # type: List[pmp.Metric] - for i in collector.get_all(): - i = cast(MetricTupleType, i) # typing check, no runtime behavior. - r = exec_method(i, collector.name, collector.const_labels) - metrics.append(r) - - mf = pmp.utils.create_metric_family( - collector.name, collector.doc, metric_type, metrics - ) - - return mf - - def marshall(self, registry: Registry) -> bytes: - """Marshall the collectors in the registry into binary protocol - buffer format. - - The Prometheus metrics parser expects each metric (MetricFamily) to - be prefixed with a varint containing the size of the encoded metric. - - :returns: bytes - """ - buf = bytearray() - for i in registry.get_all(): - buf.extend(pmp.encode(self.marshall_collector(i))) - return bytes(buf) diff --git a/src/aioprometheus/formats/text.py b/src/aioprometheus/formats/text.py index 90b563f..c9b3e8b 100644 --- a/src/aioprometheus/formats/text.py +++ b/src/aioprometheus/formats/text.py @@ -78,7 +78,6 @@ def _format_line( value: NumericValueType, const_labels: LabelsType, ) -> str: - labels = self._unify_labels(labels, const_labels, True) labels_str = "" # type: str diff --git a/src/aioprometheus/metricdict.py b/src/aioprometheus/metricdict.py index cacd17f..e695d30 100644 --- a/src/aioprometheus/metricdict.py +++ b/src/aioprometheus/metricdict.py @@ -37,7 +37,6 @@ def __len__(self): return len(self.store) def __keytransform__(self, key): - # Sometimes we need empty keys if not key or key == MetricDict.EMPTY_KEY: return MetricDict.EMPTY_KEY @@ -50,6 +49,10 @@ def __keytransform__(self, key): if not isinstance(key, dict): raise TypeError("Only accepts dicts as keys") - return orjson.dumps( - key, option=(orjson.OPT_NON_STR_KEYS | orjson.OPT_SORT_KEYS) + return orjson.dumps( # pylint: disable=no-member + key, + option=( + orjson.OPT_NON_STR_KEYS # pylint: disable=no-member + | orjson.OPT_SORT_KEYS # pylint: disable=no-member + ), ) diff --git a/src/aioprometheus/negotiator.py b/src/aioprometheus/negotiator.py index ce9375f..dbcb444 100644 --- a/src/aioprometheus/negotiator.py +++ b/src/aioprometheus/negotiator.py @@ -13,6 +13,9 @@ def negotiate(accepts_headers: Sequence[str]) -> FormatterType: """Negotiate a response format by scanning through a list of ACCEPTS headers and selecting the most efficient format. + Prometheus used to support text and binary format data but binary was + removed some time ago. This function now only returns the text formatter. + The formatter returned by this function is used to render a response. :param accepts_headers: a list of ACCEPT headers fields extracted from a request. @@ -24,10 +27,6 @@ def negotiate(accepts_headers: Sequence[str]) -> FormatterType: formatter = formats.text.TextFormatter # type: FormatterType - if formats.binary is not None: - if formats.binary.BINARY_ACCEPTS.issubset(accepts): - formatter = formats.binary.BinaryFormatter # type: ignore - logger.debug(f"negotiating {accepts} resulted in choosing {formatter.__name__}") return formatter diff --git a/src/aioprometheus/pusher.py b/src/aioprometheus/pusher.py index 72f8872..c4e5f96 100644 --- a/src/aioprometheus/pusher.py +++ b/src/aioprometheus/pusher.py @@ -11,6 +11,8 @@ "with the `aiohttp` extra?" ) from exc +from typing import Optional + from aioprometheus import REGISTRY, Registry from aioprometheus.formats import text @@ -28,7 +30,7 @@ def __init__( self, job_name: str, addr: str, - grouping_key: dict = None, + grouping_key: Optional[dict] = None, path: str = "/metrics", ) -> None: """ diff --git a/src/aioprometheus/service.py b/src/aioprometheus/service.py index 14f0867..f715381 100644 --- a/src/aioprometheus/service.py +++ b/src/aioprometheus/service.py @@ -47,12 +47,12 @@ def __init__(self, registry: Registry = REGISTRY) -> None: if not isinstance(registry, Registry): raise Exception(f"registry must be a Registry, got: {registry}") self.registry = registry - self._site = None # type: Optional[aiohttp.web.TCPSite] - self._app = None # type: Optional[aiohttp.web.Application] - self._runner = None # type: Optional[aiohttp.web.AppRunner] + self._site: Optional[aiohttp.web.TCPSite] = None + self._app: Optional[aiohttp.web.Application] = None + self._runner: Optional[aiohttp.web.AppRunner] = None self._https = False self._root_url = "/" - self._metrics_url = None # type: Optional[str] + self._metrics_url: Optional[str] = None @property def base_url(self) -> str: @@ -104,7 +104,7 @@ async def start( self, addr: str = "", port: int = 0, - ssl: SSLContext = None, + ssl: Optional[SSLContext] = None, metrics_url: str = DEFAULT_METRICS_PATH, ) -> None: """Start the prometheus metrics HTTP(S) server. diff --git a/tests/test_aiohttp.py b/tests/test_aiohttp.py index e9f74ea..46a0fe5 100644 --- a/tests/test_aiohttp.py +++ b/tests/test_aiohttp.py @@ -1,4 +1,4 @@ -import asynctest +import unittest from aioprometheus import REGISTRY, Counter, formats, render @@ -11,16 +11,9 @@ except ImportError: have_aiohttp = False -try: - import prometheus_metrics_proto as pmp - - have_pmp = True -except ImportError: - have_pmp = False - -@asynctest.skipUnless(have_aiohttp, "aiohttp library is not available") -class TestAiohttpRender(asynctest.TestCase): +@unittest.skipUnless(have_aiohttp, "aiohttp library is not available") +class TestAiohttpRender(unittest.IsolatedAsyncioTestCase): """ Test exposing Prometheus metrics from within a aiohttp existing web service without starting a separate Prometheus metrics server. @@ -65,7 +58,6 @@ async def handle_metrics(request): metrics_url = f"{url}/metrics" async with aiohttp.ClientSession() as session: - # Access root to increment metric counter async with session.get(root_url) as response: self.assertEqual(response.status, 200) @@ -92,58 +84,3 @@ async def handle_metrics(request): ) await runner.cleanup() - - @asynctest.skipUnless(have_pmp, "prometheus_metrics_proto library is not available") - async def test_binary_render_in_aiohttp_app(self): - """check binary render usage in aiohttp app""" - - app = aiohttp.web.Application() - app.registry = REGISTRY - app.events_counter = Counter("events", "Number of events.") - - async def index(request): - app.events_counter.inc({"path": "/"}) - return aiohttp.web.Response(text="hello") - - async def handle_metrics(request): - content, http_headers = render( - app.registry, request.headers.getall(aiohttp.hdrs.ACCEPT, []) - ) - return aiohttp.web.Response(body=content, headers=http_headers) - - app.add_routes( - [aiohttp.web.get("/", index), aiohttp.web.get("/metrics", handle_metrics)] - ) - - runner = aiohttp.web.AppRunner(app) - await runner.setup() - - site = aiohttp.web.TCPSite(runner, "127.0.0.1", 0, shutdown_timeout=1.0) - await site.start() - - # Fetch ephemeral port that was bound. - # IPv4 address returns a 2-tuple, IPv6 returns a 4-tuple - host, port, *_ = runner.addresses[0] - host = host if ":" not in host else f"[{host}]" - url = f"http://{host}:{port}" - root_url = f"{url}/" - metrics_url = f"{url}/metrics" - - async with aiohttp.ClientSession() as session: - - # Access root to increment metric counter - async with session.get(root_url) as response: - self.assertEqual(response.status, 200) - - # Get binary format - async with session.get( - metrics_url, - headers={aiohttp.hdrs.ACCEPT: formats.binary.BINARY_CONTENT_TYPE}, - ) as response: - self.assertEqual(response.status, 200) - self.assertIn( - formats.binary.BINARY_CONTENT_TYPE, - response.headers.get("content-type"), - ) - - await runner.cleanup() diff --git a/tests/test_decorators.py b/tests/test_decorators.py index c24b9f1..57714cf 100644 --- a/tests/test_decorators.py +++ b/tests/test_decorators.py @@ -1,8 +1,7 @@ import asyncio +import unittest from typing import Union -import asynctest - from aioprometheus import ( REGISTRY, Counter, @@ -15,7 +14,7 @@ ) -class TestDecorators(asynctest.TestCase): +class TestDecorators(unittest.IsolatedAsyncioTestCase): def tearDown(self): REGISTRY.clear() @@ -59,7 +58,6 @@ async def b(self, should_raise: bool, arg_1, kwarg_1=None): ) for test_msg, raises, count in SUB_TESTS: with self.subTest(test_msg, raises=raises): - if raises: # check decorator with async function with self.assertRaises(Exception) as cm: @@ -144,7 +142,6 @@ async def b(self, should_raise: bool, arg_1, kwarg_1=None): ) for test_msg, raises in SUB_TESTS: with self.subTest(test_msg, raises=raises): - if raises: # check decorator with async function with self.assertRaises(Exception) as cm: @@ -159,7 +156,6 @@ async def b(self, should_raise: bool, arg_1, kwarg_1=None): await b.b(raises, "b_arg", kwarg_1="kwarg_1") else: - # check decorator with async function. # Set a non-zero wait duration so we can check that the # metric actually increases while it is in-progress @@ -237,7 +233,6 @@ async def b(self, should_raise: bool, arg_1, kwarg_1=None): ) for test_msg, raises in SUB_TESTS: with self.subTest(test_msg, raises=raises): - if raises: # check decorator with async function with self.assertRaises(Exception) as cm: diff --git a/tests/test_dist.bash b/tests/test_dist.bash index 5effe57..8dac0f3 100755 --- a/tests/test_dist.bash +++ b/tests/test_dist.bash @@ -22,10 +22,10 @@ echo "Upgrading pip" pip install pip --upgrade echo "Install test dependencies and extras to check integrations" -pip install asynctest requests aiohttp fastapi quart +pip install asynctest requests aiohttp fastapi quart httpx echo "Installing $RELEASE_ARCHIVE" -pip install $RELEASE_ARCHIVE[aiohttp,binary,starlette,quart] +pip install $RELEASE_ARCHIVE[aiohttp,starlette,quart] echo "Running tests" cd .. diff --git a/tests/test_export.py b/tests/test_export.py index 681223b..5b9ed43 100644 --- a/tests/test_export.py +++ b/tests/test_export.py @@ -1,5 +1,5 @@ -import asynctest -import asynctest.mock +import unittest +import unittest.mock import aioprometheus from aioprometheus import REGISTRY, Counter, Gauge, Histogram, Registry, Summary @@ -16,19 +16,9 @@ have_aiohttp = False -try: - import prometheus_metrics_proto as pmp - - from aioprometheus.formats import binary - - have_pmp = True -except ImportError: - have_pmp = False - - -@asynctest.skipUnless(have_aiohttp, "aiohttp library is not available") -class TestTextExporter(asynctest.TestCase): - async def tearDown(self): +@unittest.skipUnless(have_aiohttp, "aiohttp library is not available") +class TestTextExporter(unittest.IsolatedAsyncioTestCase): + def tearDown(self): REGISTRY.clear() async def test_valid_registry(self): @@ -65,7 +55,7 @@ async def test_start_started_server(self): s = Service() await s.start(addr="127.0.0.1") - with asynctest.mock.patch.object( + with unittest.mock.patch.object( aioprometheus.service.logger, "warning" ) as mock_warn: await s.start(addr="127.0.0.1") @@ -83,7 +73,7 @@ async def test_stop_stopped_server(self): await s.start(addr="127.0.0.1") await s.stop() - with asynctest.mock.patch.object( + with unittest.mock.patch.object( aioprometheus.service.logger, "warning" ) as mock_warn: await s.stop() @@ -128,43 +118,6 @@ async def test_counter_text(self): await s.stop() - @asynctest.skipUnless(have_pmp, "prometheus_metrics_proto library is not available") - async def test_counter_binary(self): - """check counter metric binary export""" - - s = Service() - await s.start(addr="127.0.0.1") - - # Add some metrics - data = ( - ({"data": 1}, 100), - ({"data": "2"}, 200), - ({"data": 3}, 300), - ({"data": 1}, 400), - ) - c = Counter("test_counter", "Test Counter.", {"test": "test_counter"}) - - for i in data: - c.set(i[0], i[1]) - - async with aiohttp.ClientSession() as session: - async with session.get( - s.metrics_url, headers={ACCEPT: binary.BINARY_CONTENT_TYPE} - ) as resp: - self.assertEqual(resp.status, 200) - content = await resp.read() - self.assertEqual( - binary.BINARY_CONTENT_TYPE, resp.headers.get(CONTENT_TYPE) - ) - metrics = pmp.decode(content) - self.assertEqual(len(metrics), 1) - mf = metrics[0] - self.assertIsInstance(mf, pmp.MetricFamily) - self.assertEqual(mf.type, pmp.COUNTER) - self.assertEqual(len(mf.metric), 3) - - await s.stop() - async def test_gauge_text(self): """check gauge metric text export""" @@ -201,43 +154,6 @@ async def test_gauge_text(self): await s.stop() - @asynctest.skipUnless(have_pmp, "prometheus_metrics_proto library is not available") - async def test_gauge_binary(self): - """check gauge metric binary export""" - - s = Service() - await s.start(addr="127.0.0.1") - - # Add some metrics - data = ( - ({"data": 1}, 100), - ({"data": "2"}, 200), - ({"data": 3}, 300), - ({"data": 1}, 400), - ) - g = Gauge("test_gauge", "Test Gauge.", {"test": "test_gauge"}) - - for i in data: - g.set(i[0], i[1]) - - async with aiohttp.ClientSession() as session: - async with session.get( - s.metrics_url, headers={ACCEPT: binary.BINARY_CONTENT_TYPE} - ) as resp: - self.assertEqual(resp.status, 200) - content = await resp.read() - self.assertEqual( - binary.BINARY_CONTENT_TYPE, resp.headers.get(CONTENT_TYPE) - ) - metrics = pmp.decode(content) - self.assertEqual(len(metrics), 1) - mf = metrics[0] - self.assertIsInstance(mf, pmp.MetricFamily) - self.assertEqual(mf.type, pmp.GAUGE) - self.assertEqual(len(mf.metric), 3) - - await s.stop() - async def test_summary_text(self): """check summary metric text export""" @@ -263,7 +179,6 @@ async def test_summary_text(self): """ async with aiohttp.ClientSession() as session: - # Fetch as text async with session.get( s.metrics_url, headers={ACCEPT: text.TEXT_CONTENT_TYPE} @@ -273,59 +188,6 @@ async def test_summary_text(self): self.assertEqual(text.TEXT_CONTENT_TYPE, resp.headers.get(CONTENT_TYPE)) self.assertEqual(expected_data, content.decode()) - if have_pmp: - # Fetch as binary - async with session.get( - s.metrics_url, headers={ACCEPT: binary.BINARY_CONTENT_TYPE} - ) as resp: - self.assertEqual(resp.status, 200) - content = await resp.read() - self.assertEqual( - binary.BINARY_CONTENT_TYPE, resp.headers.get(CONTENT_TYPE) - ) - metrics = pmp.decode(content) - self.assertEqual(len(metrics), 1) - mf = metrics[0] - self.assertIsInstance(mf, pmp.MetricFamily) - self.assertEqual(mf.type, pmp.SUMMARY) - self.assertEqual(len(mf.metric), 1) - self.assertEqual(len(mf.metric[0].summary.quantile), 3) - - await s.stop() - - @asynctest.skipUnless(have_pmp, "prometheus_metrics_proto library is not available") - async def test_summary_binary(self): - """check summary metric binary export""" - - s = Service() - await s.start(addr="127.0.0.1") - - # Add some metrics - data = [3, 5.2, 13, 4] - label = {"data": 1} - - summary = Summary("test_summary", "Test Summary.", {"test": "test_summary"}) - - for i in data: - summary.add(label, i) - - async with aiohttp.ClientSession() as session: - async with session.get( - s.metrics_url, headers={ACCEPT: binary.BINARY_CONTENT_TYPE} - ) as resp: - self.assertEqual(resp.status, 200) - content = await resp.read() - self.assertEqual( - binary.BINARY_CONTENT_TYPE, resp.headers.get(CONTENT_TYPE) - ) - metrics = pmp.decode(content) - self.assertEqual(len(metrics), 1) - mf = metrics[0] - self.assertIsInstance(mf, pmp.MetricFamily) - self.assertEqual(mf.type, pmp.SUMMARY) - self.assertEqual(len(mf.metric), 1) - self.assertEqual(len(mf.metric[0].summary.quantile), 3) - await s.stop() async def test_histogram_text(self): @@ -359,7 +221,6 @@ async def test_histogram_text(self): """ async with aiohttp.ClientSession() as session: - # Fetch as text async with session.get( s.metrics_url, headers={ACCEPT: text.TEXT_CONTENT_TYPE} @@ -369,64 +230,6 @@ async def test_histogram_text(self): self.assertEqual(text.TEXT_CONTENT_TYPE, resp.headers.get(CONTENT_TYPE)) self.assertEqual(expected_data, content.decode()) - if have_pmp: - # Fetch as binary - async with session.get( - s.metrics_url, headers={ACCEPT: binary.BINARY_CONTENT_TYPE} - ) as resp: - self.assertEqual(resp.status, 200) - content = await resp.read() - self.assertEqual( - binary.BINARY_CONTENT_TYPE, resp.headers.get(CONTENT_TYPE) - ) - metrics = pmp.decode(content) - self.assertEqual(len(metrics), 1) - mf = metrics[0] - self.assertIsInstance(mf, pmp.MetricFamily) - self.assertEqual(mf.type, pmp.HISTOGRAM) - self.assertEqual(len(mf.metric), 1) - self.assertEqual(len(mf.metric[0].histogram.bucket), 4) - - await s.stop() - - @asynctest.skipUnless(have_pmp, "prometheus_metrics_proto library is not available") - async def test_histogram_binary(self): - """check histogram metric binary export""" - - s = Service() - await s.start(addr="127.0.0.1") - - # Add some metrics - data = [3, 5.2, 13, 4] - label = {"data": 1} - - h = Histogram( - "histogram_test", - "Test Histogram.", - {"type": "test_histogram"}, - buckets=[5.0, 10.0, 15.0], - ) - - for i in data: - h.add(label, i) - - async with aiohttp.ClientSession() as session: - async with session.get( - s.metrics_url, headers={ACCEPT: binary.BINARY_CONTENT_TYPE} - ) as resp: - self.assertEqual(resp.status, 200) - content = await resp.read() - self.assertEqual( - binary.BINARY_CONTENT_TYPE, resp.headers.get(CONTENT_TYPE) - ) - metrics = pmp.decode(content) - self.assertEqual(len(metrics), 1) - mf = metrics[0] - self.assertIsInstance(mf, pmp.MetricFamily) - self.assertEqual(mf.type, pmp.HISTOGRAM) - self.assertEqual(len(mf.metric), 1) - self.assertEqual(len(mf.metric[0].histogram.bucket), 4) - await s.stop() async def test_all_text(self): @@ -542,7 +345,6 @@ async def test_all_text(self): """ async with aiohttp.ClientSession() as session: - # Fetch as text async with session.get( s.metrics_url, headers={ACCEPT: text.TEXT_CONTENT_TYPE} @@ -552,108 +354,6 @@ async def test_all_text(self): self.assertEqual(text.TEXT_CONTENT_TYPE, resp.headers.get(CONTENT_TYPE)) self.assertEqual(expected_data, content.decode()) - if have_pmp: - # Fetch as binary - async with session.get( - s.metrics_url, headers={ACCEPT: binary.BINARY_CONTENT_TYPE} - ) as resp: - self.assertEqual(resp.status, 200) - content = await resp.read() - self.assertEqual( - binary.BINARY_CONTENT_TYPE, resp.headers.get(CONTENT_TYPE) - ) - metrics = pmp.decode(content) - self.assertEqual(len(metrics), 4) - for mf in metrics: - self.assertIsInstance(mf, pmp.MetricFamily) - if mf.type == pmp.COUNTER: - self.assertEqual(len(mf.metric), 4) - elif mf.type == pmp.GAUGE: - self.assertEqual(len(mf.metric), 4) - elif mf.type == pmp.SUMMARY: - self.assertEqual(len(mf.metric), 4) - self.assertEqual(len(mf.metric[0].summary.quantile), 3) - elif mf.type == pmp.HISTOGRAM: - self.assertEqual(len(mf.metric), 4) - self.assertEqual(len(mf.metric[0].histogram.bucket), 4) - - await s.stop() - - @asynctest.skipUnless(have_pmp, "prometheus_metrics_proto library is not available") - async def test_all_binary(self): - """check multiple metrics binary export""" - - s = Service() - await s.start(addr="127.0.0.1") - - counter_data = ( - ({"c_sample": "1"}, 100), - ({"c_sample": "2"}, 200), - ({"c_sample": "3"}, 300), - ({"c_sample": "1", "c_subsample": "b"}, 400), - ) - - gauge_data = ( - ({"g_sample": "1"}, 500), - ({"g_sample": "2"}, 600), - ({"g_sample": "3"}, 700), - ({"g_sample": "1", "g_subsample": "b"}, 800), - ) - - summary_data = ( - ({"s_sample": "1"}, range(1000, 2000, 4)), - ({"s_sample": "2"}, range(2000, 3000, 20)), - ({"s_sample": "3"}, range(3000, 4000, 13)), - ({"s_sample": "1", "s_subsample": "b"}, range(4000, 5000, 47)), - ) - - histogram_data = ( - ({"h_sample": "1"}, [3, 14]), - ({"h_sample": "2"}, range(1, 20, 2)), - ({"h_sample": "3"}, range(1, 20, 2)), - ({"h_sample": "1", "h_subsample": "b"}, range(1, 20, 2)), - ) - - counter = Counter("counter_test", "A counter.", {"type": "counter"}) - gauge = Gauge("gauge_test", "A gauge.", {"type": "gauge"}) - summary = Summary("summary_test", "A summary.", {"type": "summary"}) - histogram = Histogram( - "histogram_test", - "A histogram.", - {"type": "histogram"}, - buckets=[5.0, 10.0, 15.0], - ) - - # Add data - [counter.set(c[0], c[1]) for c in counter_data] - [gauge.set(g[0], g[1]) for g in gauge_data] - [summary.add(i[0], s) for i in summary_data for s in i[1]] - [histogram.observe(i[0], h) for i in histogram_data for h in i[1]] - - async with aiohttp.ClientSession() as session: - async with session.get( - s.metrics_url, headers={ACCEPT: binary.BINARY_CONTENT_TYPE} - ) as resp: - self.assertEqual(resp.status, 200) - content = await resp.read() - self.assertEqual( - binary.BINARY_CONTENT_TYPE, resp.headers.get(CONTENT_TYPE) - ) - metrics = pmp.decode(content) - self.assertEqual(len(metrics), 4) - for mf in metrics: - self.assertIsInstance(mf, pmp.MetricFamily) - if mf.type == pmp.COUNTER: - self.assertEqual(len(mf.metric), 4) - elif mf.type == pmp.GAUGE: - self.assertEqual(len(mf.metric), 4) - elif mf.type == pmp.SUMMARY: - self.assertEqual(len(mf.metric), 4) - self.assertEqual(len(mf.metric[0].summary.quantile), 3) - elif mf.type == pmp.HISTOGRAM: - self.assertEqual(len(mf.metric), 4) - self.assertEqual(len(mf.metric[0].histogram.bucket), 4) - await s.stop() async def test_no_accept_header(self): @@ -675,7 +375,6 @@ async def test_no_accept_header(self): """ async with aiohttp.ClientSession() as session: - # Fetch without explicit accept type async with session.get(s.metrics_url) as resp: self.assertEqual(resp.status, 200) diff --git a/tests/test_fastapi.py b/tests/test_fastapi.py index 9f337c4..8067b26 100644 --- a/tests/test_fastapi.py +++ b/tests/test_fastapi.py @@ -13,13 +13,6 @@ except ImportError: have_fastapi = False -try: - import prometheus_metrics_proto as pmp - - have_pmp = True -except ImportError: - have_pmp = False - @unittest.skipUnless(have_fastapi, "FastAPI library is not available") class TestFastAPIUsage(unittest.TestCase): @@ -70,51 +63,6 @@ async def handle_metrics(response: Response, accept: List[str] = Header(None)): # Check content self.assertIn('events{path="/"} 1', response.text) - @unittest.skipUnless(have_pmp, "prometheus_metrics_proto library is not available") - def test_render_binary(self): - """check render binary usage in FastAPI app""" - - app = FastAPI() - app.events_counter = Counter("events", "Number of events.") - - @app.get("/") - async def hello(): - app.events_counter.inc({"path": "/"}) - return "hello" - - @app.get("/metrics") - async def handle_metrics(response: Response, accept: List[str] = Header(None)): - content, http_headers = render(REGISTRY, accept) - return Response(content=content, media_type=http_headers["Content-Type"]) - - # The test client also starts the web service - test_client = TestClient(app) - - # Access root to increment metric counter - response = test_client.get("/") - self.assertEqual(response.status_code, 200) - - # Get binary format - response = test_client.get( - "/metrics", - headers={"accept": formats.binary.BINARY_CONTENT_TYPE}, - ) - self.assertEqual(response.status_code, 200) - self.assertIn( - formats.binary.BINARY_CONTENT_TYPE, - response.headers.get("content-type"), - ) - - metrics = pmp.decode(response.content) - self.assertEqual(len(metrics), 1) - mf = metrics[0] - self.assertIsInstance(mf, pmp.MetricFamily) - self.assertEqual(mf.type, pmp.COUNTER) - self.assertEqual(len(mf.metric), 1) - self.assertEqual(mf.metric[0].counter.value, 1) - self.assertEqual(mf.metric[0].label[0].name, "path") - self.assertEqual(mf.metric[0].label[0].value, "/") - def test_asgi_middleware(self): """check ASGI middleware usage in FastAPI app""" @@ -144,16 +92,6 @@ async def hello(): # The test client also starts the web service test_client = TestClient(app) - # The test client does not call the ASGI lifespan scope on the app - # so we need to manual set the starlette_app attribute on the middleware. - # The only way I can think of doing this is to walk over the middlewares. - # However the structure is difficult to walk over. - # 'sem' represents the ServerErrorMiddleware that Starlette always adds - # as the first Middlware, before any user middleware. - sem = app.middleware_stack - self.assertIsInstance(sem.app, MetricsMiddleware) - sem.app.starlette_app = app - # Access root to update default metrics and trigger custom metric update response = test_client.get("/") self.assertEqual(response.status_code, 200) @@ -364,14 +302,16 @@ async def hello(): # Check content self.assertIn( - 'requests_total_counter{method="GET",path="/users/bob"} 1', response.text + 'requests_total_counter{method="GET",path="/users/{user_id}"} 1', + response.text, ) self.assertIn( - 'status_codes_counter{method="GET",path="/users/bob",status_code="2xx"} 1', + 'status_codes_counter{method="GET",path="/users/{user_id}",status_code="2xx"} 1', response.text, ) self.assertIn( - 'responses_total_counter{method="GET",path="/users/bob"} 1', response.text + 'responses_total_counter{method="GET",path="/users/{user_id}"} 1', + response.text, ) # Access it again to confirm default metrics get incremented @@ -388,24 +328,16 @@ async def hello(): # Check content self.assertIn( - 'requests_total_counter{method="GET",path="/users/bob"} 1', response.text - ) - self.assertIn( - 'requests_total_counter{method="GET",path="/users/alice"} 1', response.text - ) - self.assertIn( - 'status_codes_counter{method="GET",path="/users/bob",status_code="2xx"} 1', + 'requests_total_counter{method="GET",path="/users/{user_id}"} 2', response.text, ) self.assertIn( - 'status_codes_counter{method="GET",path="/users/alice",status_code="2xx"} 1', + 'status_codes_counter{method="GET",path="/users/{user_id}",status_code="2xx"} 2', response.text, ) self.assertIn( - 'responses_total_counter{method="GET",path="/users/bob"} 1', response.text - ) - self.assertIn( - 'responses_total_counter{method="GET",path="/users/alice"} 1', response.text + 'responses_total_counter{method="GET",path="/users/{user_id}"} 2', + response.text, ) # Access boom route to trigger exception metric update diff --git a/tests/test_formats_binary.py b/tests/test_formats_binary.py deleted file mode 100644 index 40b5e84..0000000 --- a/tests/test_formats_binary.py +++ /dev/null @@ -1,623 +0,0 @@ -import unittest -import unittest.mock - -from aioprometheus import REGISTRY, Counter, Gauge, Histogram, Summary - -try: - import prometheus_metrics_proto as pmp - - from aioprometheus.formats import binary - - have_pmp = True -except ImportError: - have_pmp = False - -TEST_TIMESTAMP = 1515044377268 - - -@unittest.skipUnless(have_pmp, "prometheus_metrics_proto library is not available") -class TestProtobufFormat(unittest.TestCase): - def setUp(self): - - self.const_labels = {"app": "my_app"} - - # Counter test fields - self.counter_metric_name = "logged_users_total" - self.counter_metric_help = "Logged users in the application." - self.counter_metric_data = ( - ({"country": "sp", "device": "desktop"}, 520), - ({"country": "us", "device": "mobile"}, 654), - ({"country": "uk", "device": "desktop"}, 1001), - ({"country": "de", "device": "desktop"}, 995), - ({"country": "zh", "device": "desktop"}, 520), - ) - - # Gauge test fields - self.gauge_metric_name = "logged_users_total" - self.gauge_metric_help = "Logged users in the application." - self.gauge_metric_data = ( - ({"country": "sp", "device": "desktop"}, 520), - ({"country": "us", "device": "mobile"}, 654), - ({"country": "uk", "device": "desktop"}, 1001), - ({"country": "de", "device": "desktop"}, 995), - ({"country": "zh", "device": "desktop"}, 520), - ) - - # Summary test fields - self.summary_metric_name = "request_payload_size_bytes" - self.summary_metric_help = "Request payload size in bytes." - self.summary_metric_data = ( - ({"route": "/"}, {0.5: 4.0, 0.9: 5.2, 0.99: 5.2, "sum": 25.2, "count": 4}), - ) - self.summary_metric_data_values = (({"route": "/"}, (3, 5.2, 13, 4)),) - - # Histogram test fields - self.histogram_metric_name = "request_latency_seconds" - self.histogram_metric_help = "Request latency in seconds." - self.histogram_metric_buckets = [5.0, 10.0, 15.0] - # buckets typically have a POS_INF upper bound to catch values - # beyond the largest bucket bound. Simulate this behavior. - POS_INF = float("inf") - self.histogram_metric_data = ( - ( - {"route": "/"}, - {5.0: 2, 10.0: 3, 15.0: 4, POS_INF: 4, "sum": 25.2, "count": 4}, - ), - ) - self.histogram_metric_data_values = (({"route": "/"}, (3, 5.2, 13, 4)),) - - def tearDown(self): - REGISTRY.clear() - - def test_headers_binary(self): - """check binary header info is provided""" - f = binary.BinaryFormatter() - expected_result = {"Content-Type": binary.BINARY_CONTENT_TYPE} - self.assertEqual(expected_result, f.get_headers()) - - def test_no_metric_instances_present_binary(self): - """Check marshalling a collector with no metrics instances present""" - - c = Counter( - name=self.counter_metric_name, - doc=self.counter_metric_help, - const_labels=self.const_labels, - ) - - f = binary.BinaryFormatter() - - result = f.marshall_collector(c) - self.assertIsInstance(result, pmp.MetricFamily) - - # Construct the result expected to receive when the counter - # collector is marshalled. - expected_result = pmp.create_counter( - self.counter_metric_name, self.counter_metric_help, [] - ) - - self.assertEqual(result, expected_result) - - def test_counter_format_binary(self): - - # Check simple metric - c = Counter(name=self.counter_metric_name, doc=self.counter_metric_help) - - # Add data to the collector - for labels, value in self.counter_metric_data: - c.set_value(labels, value) - - f = binary.BinaryFormatter() - - result = f.marshall_collector(c) - self.assertIsInstance(result, pmp.MetricFamily) - - # Construct the result expected to receive when the counter - # collector is marshalled. - expected_result = pmp.create_counter( - self.counter_metric_name, self.counter_metric_help, self.counter_metric_data - ) - - self.assertEqual(result, expected_result) - - REGISTRY.clear() - - ###################################################################### - - # Check metric with constant labels - c = Counter( - name=self.counter_metric_name, - doc=self.counter_metric_help, - const_labels=self.const_labels, - ) - - # Add data to the collector - for labels, value in self.counter_metric_data: - c.set_value(labels, value) - - f = binary.BinaryFormatter() - - result = f.marshall_collector(c) - self.assertIsInstance(result, pmp.MetricFamily) - - # Construct the result to expected to receive when the counter - # collector is marshalled. - expected_result = pmp.create_counter( - self.counter_metric_name, - self.counter_metric_help, - self.counter_metric_data, - const_labels=self.const_labels, - ) - - self.assertEqual(result, expected_result) - - REGISTRY.clear() - - ###################################################################### - - # Check metric with timestamps - with unittest.mock.patch.object( - pmp.utils, "_timestamp_ms", return_value=TEST_TIMESTAMP - ): - - c = Counter(name=self.counter_metric_name, doc=self.counter_metric_help) - - # Add data to the collector - for labels, value in self.counter_metric_data: - c.set_value(labels, value) - - f = binary.BinaryFormatter(timestamp=True) - - result = f.marshall_collector(c) - self.assertIsInstance(result, pmp.MetricFamily) - - # Construct the result to expected to receive when the counter - # collector is marshalled. - expected_result = pmp.create_counter( - self.counter_metric_name, - self.counter_metric_help, - self.counter_metric_data, - timestamp=True, - ) - - self.assertEqual(result, expected_result) - - def test_gauge_format_binary(self): - - g = Gauge(name=self.gauge_metric_name, doc=self.gauge_metric_help) - - # Add data to the collector - for labels, values in self.gauge_metric_data: - g.set_value(labels, values) - - f = binary.BinaryFormatter() - - result = f.marshall_collector(g) - self.assertIsInstance(result, pmp.MetricFamily) - - # Construct the result to expected to receive when the gauge - # collector is marshalled. - expected_result = pmp.create_gauge( - self.gauge_metric_name, self.gauge_metric_help, self.gauge_metric_data - ) - - self.assertEqual(result, expected_result) - - REGISTRY.clear() - - ###################################################################### - - # Check metric with constant labels - g = Gauge( - name=self.gauge_metric_name, - doc=self.gauge_metric_help, - const_labels=self.const_labels, - ) - - # Add data to the collector - for labels, values in self.gauge_metric_data: - g.set_value(labels, values) - - f = binary.BinaryFormatter() - - result = f.marshall_collector(g) - self.assertIsInstance(result, pmp.MetricFamily) - - # Construct the result to expected to receive when the gauge - # collector is marshalled. - expected_result = pmp.create_gauge( - self.gauge_metric_name, - self.gauge_metric_help, - self.gauge_metric_data, - const_labels=self.const_labels, - ) - - self.assertEqual(result, expected_result) - - REGISTRY.clear() - - ###################################################################### - - # Check metric with timestamps - with unittest.mock.patch.object( - pmp.utils, "_timestamp_ms", return_value=TEST_TIMESTAMP - ): - - g = Gauge(name=self.gauge_metric_name, doc=self.gauge_metric_help) - - # Add data to the collector - for labels, values in self.gauge_metric_data: - g.set_value(labels, values) - - f = binary.BinaryFormatter(timestamp=True) - - result = f.marshall_collector(g) - self.assertIsInstance(result, pmp.MetricFamily) - - # Construct the result to expected to receive when the gauge - # collector is marshalled. - expected_result = pmp.create_gauge( - self.gauge_metric_name, - self.gauge_metric_help, - self.gauge_metric_data, - timestamp=True, - ) - - self.assertEqual(result, expected_result) - - def test_summary_format_binary(self): - - s = Summary(name=self.summary_metric_name, doc=self.summary_metric_help) - - # Add data to the collector - for labels, values in self.summary_metric_data_values: - for value in values: - s.add(labels, value) - - f = binary.BinaryFormatter() - - result = f.marshall_collector(s) - self.assertIsInstance(result, pmp.MetricFamily) - self.assertEqual(len(result.metric), 1) - - # Construct the result to expected to receive when the summary - # collector is marshalled. - expected_result = pmp.create_summary( - self.summary_metric_name, self.summary_metric_help, self.summary_metric_data - ) - - self.assertEqual(result, expected_result) - - REGISTRY.clear() - - ###################################################################### - - # Check metric with constant labels - s = Summary( - name=self.summary_metric_name, - doc=self.summary_metric_help, - const_labels=self.const_labels, - ) - - # Add data to the collector - for labels, values in self.summary_metric_data_values: - for value in values: - s.add(labels, value) - - f = binary.BinaryFormatter() - - result = f.marshall_collector(s) - self.assertIsInstance(result, pmp.MetricFamily) - self.assertEqual(len(result.metric), 1) - - # Construct the result to expected to receive when the summary - # collector is marshalled. - expected_result = pmp.create_summary( - self.summary_metric_name, - self.summary_metric_help, - self.summary_metric_data, - const_labels=self.const_labels, - ) - - self.assertEqual(result, expected_result) - - REGISTRY.clear() - - ###################################################################### - - # Check metric with timestamps - with unittest.mock.patch.object( - pmp.utils, "_timestamp_ms", return_value=TEST_TIMESTAMP - ): - - s = Summary(name=self.summary_metric_name, doc=self.summary_metric_help) - - # Add data to the collector - for labels, values in self.summary_metric_data_values: - for value in values: - s.add(labels, value) - - f = binary.BinaryFormatter(timestamp=True) - - result = f.marshall_collector(s) - self.assertIsInstance(result, pmp.MetricFamily) - self.assertEqual(len(result.metric), 1) - - # Construct the result to expected to receive when the summary - # collector is marshalled. - expected_result = pmp.create_summary( - self.summary_metric_name, - self.summary_metric_help, - self.summary_metric_data, - timestamp=True, - ) - - self.assertEqual(result, expected_result) - - REGISTRY.clear() - - ###################################################################### - - # Check metric with multiple metric instances - - input_summary_data = ( - ({"interval": "5s"}, [3, 5.2, 13, 4]), - ({"interval": "10s"}, [1.3, 1.2, 32.1, 59.2, 109.46, 70.9]), - ({"interval": "10s", "method": "fast"}, [5, 9.8, 31, 9.7, 101.4]), - ) - - managed_summary_data = ( - ( - {"interval": "5s"}, - {0.5: 4.0, 0.9: 5.2, 0.99: 5.2, "sum": 25.2, "count": 4}, - ), - ( - {"interval": "10s"}, - { - 0.5: 32.1, - 0.9: 59.2, - 0.99: 59.2, - "sum": 274.15999999999997, - "count": 6, - }, - ), - ( - {"interval": "10s", "method": "fast"}, - {0.5: 9.7, 0.9: 9.8, 0.99: 9.8, "sum": 156.9, "count": 5}, - ), - ) - - s = Summary(name=self.summary_metric_name, doc=self.summary_metric_help) - - # Add data to the collector - for labels, values in input_summary_data: - for value in values: - s.add(labels, value) - - f = binary.BinaryFormatter() - - result = f.marshall_collector(s) - self.assertIsInstance(result, pmp.MetricFamily) - self.assertEqual(len(result.metric), 3) - - # Construct the result to expected to receive when the summary - # collector is marshalled. - expected_result = pmp.create_summary( - self.summary_metric_name, self.summary_metric_help, managed_summary_data - ) - - self.assertEqual(result, expected_result) - - def test_histogram_format_binary(self): - - h = Histogram( - name=self.histogram_metric_name, - doc=self.histogram_metric_help, - buckets=self.histogram_metric_buckets, - ) - - # Add data to the collector - for labels, values in self.histogram_metric_data_values: - for value in values: - h.add(labels, value) - - f = binary.BinaryFormatter() - - result = f.marshall_collector(h) - self.assertIsInstance(result, pmp.MetricFamily) - self.assertEqual(len(result.metric), 1) - - # Construct the result to expected to receive when the histogram - # collector is marshalled. - expected_result = pmp.create_histogram( - self.histogram_metric_name, - self.histogram_metric_help, - self.histogram_metric_data, - ) - - self.assertEqual(result, expected_result) - - REGISTRY.clear() - - ###################################################################### - - # Check metric with constant labels - h = Histogram( - name=self.histogram_metric_name, - doc=self.histogram_metric_help, - const_labels=self.const_labels, - buckets=self.histogram_metric_buckets, - ) - - # Add data to the collector - for labels, values in self.histogram_metric_data_values: - for value in values: - h.add(labels, value) - - f = binary.BinaryFormatter() - - result = f.marshall_collector(h) - self.assertIsInstance(result, pmp.MetricFamily) - self.assertEqual(len(result.metric), 1) - - # Construct the result to expected to receive when the histogram - # collector is marshalled. - expected_result = pmp.create_histogram( - self.histogram_metric_name, - self.histogram_metric_help, - self.histogram_metric_data, - const_labels=self.const_labels, - ) - - self.assertEqual(result, expected_result) - - REGISTRY.clear() - - ###################################################################### - - # Check metric with timestamps - with unittest.mock.patch.object( - pmp.utils, "_timestamp_ms", return_value=TEST_TIMESTAMP - ): - - h = Histogram( - name=self.histogram_metric_name, - doc=self.histogram_metric_help, - buckets=self.histogram_metric_buckets, - ) - - # Add data to the collector - for labels, values in self.histogram_metric_data_values: - for value in values: - h.add(labels, value) - - f = binary.BinaryFormatter(timestamp=True) - - result = f.marshall_collector(h) - self.assertIsInstance(result, pmp.MetricFamily) - self.assertEqual(len(result.metric), 1) - - # Construct the result to expected to receive when the histogram - # collector is marshalled. - expected_result = pmp.create_histogram( - self.histogram_metric_name, - self.histogram_metric_help, - self.histogram_metric_data, - timestamp=True, - ) - - self.assertEqual(result, expected_result) - - def test_registry_marshall_counter(self): - - counter_data = (({"c_sample": "1", "c_subsample": "b"}, 400),) - - counter = Counter( - "counter_test", "A counter.", const_labels={"type": "counter"} - ) - - for labels, value in counter_data: - counter.set(labels, value) - - valid_result = ( - b'[\n\x0ccounter_test\x12\nA counter.\x18\x00"=\n\r' - b"\n\x08c_sample\x12\x011\n\x10\n\x0bc_subsample\x12" - b"\x01b\n\x0f\n\x04type\x12\x07counter\x1a\t\t\x00\x00" - b"\x00\x00\x00\x00y@" - ) - f = binary.BinaryFormatter() - - self.assertEqual(valid_result, f.marshall(REGISTRY)) - - def test_registry_marshall_gauge(self): - - gauge_data = (({"g_sample": "1", "g_subsample": "b"}, 800),) - - gauge = Gauge("gauge_test", "A gauge.", const_labels={"type": "gauge"}) - - for labels, value in gauge_data: - gauge.set(labels, value) - - valid_result = ( - b'U\n\ngauge_test\x12\x08A gauge.\x18\x01";' - b"\n\r\n\x08g_sample\x12\x011\n\x10\n\x0bg_subsample" - b"\x12\x01b\n\r\n\x04type\x12\x05gauge\x12\t\t\x00" - b"\x00\x00\x00\x00\x00\x89@" - ) - - f = binary.BinaryFormatter() - - self.assertEqual(valid_result, f.marshall(REGISTRY)) - - def test_registry_marshall_summary(self): - - metric_name = "summary_test" - metric_help = "A summary." - # metric_data = ( - # ({'s_sample': '1', 's_subsample': 'b'}, - # {0.5: 4235.0, 0.9: 4470.0, 0.99: 4517.0, 'count': 22, 'sum': 98857.0}), - # ) - - summary_data = (({"s_sample": "1", "s_subsample": "b"}, range(4000, 5000, 47)),) - - summary = Summary(metric_name, metric_help, const_labels={"type": "summary"}) - - for labels, values in summary_data: - for v in values: - summary.add(labels, v) - - valid_result = ( - b"\x99\x01\n\x0csummary_test\x12\nA summary." - b'\x18\x02"{\n\r\n\x08s_sample\x12\x011\n\x10\n' - b"\x0bs_subsample\x12\x01b\n\x0f\n\x04type\x12\x07" - b'summary"G\x08\x16\x11\x00\x00\x00\x00\x90"\xf8@' - b"\x1a\x12\t\x00\x00\x00\x00\x00\x00\xe0?\x11\x00" - b"\x00\x00\x00\x00\x8b\xb0@\x1a\x12\t\xcd\xcc\xcc" - b"\xcc\xcc\xcc\xec?\x11\x00\x00\x00\x00\x00v\xb1@" - b"\x1a\x12\t\xaeG\xe1z\x14\xae\xef?\x11\x00\x00\x00" - b"\x00\x00\xa5\xb1@" - ) - - f = binary.BinaryFormatter() - - self.assertEqual(valid_result, f.marshall(REGISTRY)) - - def test_registry_marshall_histogram(self): - """check encode of histogram matches expected output""" - - metric_name = "histogram_test" - metric_help = "A histogram." - metric_data = ( - ( - {"h_sample": "1", "h_subsample": "b"}, - {5.0: 3, 10.0: 2, 15.0: 1, "count": 6, "sum": 46.0}, - ), - ) - histogram_data = ( - ({"h_sample": "1", "h_subsample": "b"}, (4.5, 5.0, 4.0, 9.6, 9.0, 13.9)), - ) - - POS_INF = float("inf") - histogram = Histogram( - metric_name, - metric_help, - const_labels={"type": "histogram"}, - buckets=(5.0, 10.0, 15.0, POS_INF), - ) - for labels, values in histogram_data: - for v in values: - histogram.add(labels, v) - - valid_result = ( - b"\x97\x01\n\x0ehistogram_test\x12\x0cA histogram." - b'\x18\x04"u\n\r\n\x08h_sample\x12\x011\n\x10\n' - b"\x0bh_subsample\x12\x01b\n\x11\n\x04type\x12\t" - b"histogram:?\x08\x06\x11\x00\x00\x00\x00\x00\x00G@" - b"\x1a\x0b\x08\x03\x11\x00\x00\x00\x00\x00\x00\x14@" - b"\x1a\x0b\x08\x05\x11\x00\x00\x00\x00\x00\x00$@\x1a" - b"\x0b\x08\x06\x11\x00\x00\x00\x00\x00\x00.@\x1a\x0b" - b"\x08\x06\x11\x00\x00\x00\x00\x00\x00\xf0\x7f" - ) - - f = binary.BinaryFormatter() - - self.assertEqual(valid_result, f.marshall(REGISTRY)) diff --git a/tests/test_formats_text.py b/tests/test_formats_text.py index f5f62a1..5629a98 100644 --- a/tests/test_formats_text.py +++ b/tests/test_formats_text.py @@ -32,7 +32,6 @@ def test_wrong_format(self): self.assertEqual("Not a valid object format", str(context.exception)) def test_counter_format(self): - self.data = { "name": "logged_users_total", "doc": "Logged users in the application", @@ -86,7 +85,6 @@ def test_counter_format(self): self.assertEqual(valid_result, result) def test_counter_format_with_const_labels(self): - self.data = { "name": "logged_users_total", "doc": "Logged users in the application", @@ -140,7 +138,6 @@ def test_counter_format_with_const_labels(self): self.assertEqual(valid_result, result) def test_counter_format_text(self): - name = "container_cpu_usage_seconds_total" doc = "Total seconds of cpu time consumed." @@ -257,7 +254,6 @@ def test_counter_format_with_timestamp(self): self.assertTrue(re.match(result_regex, result)) def test_single_counter_format_text(self): - name = "prometheus_dns_sd_lookups_total" doc = "The number of DNS-SD lookups." @@ -281,7 +277,6 @@ def test_single_counter_format_text(self): self.assertEqual(valid_result, result) def test_gauge_format(self): - self.data = { "name": "logged_users_total", "doc": "Logged users in the application", @@ -335,7 +330,6 @@ def test_gauge_format(self): self.assertEqual(valid_result, result) def test_gauge_format_with_const_labels(self): - self.data = { "name": "logged_users_total", "doc": "Logged users in the application", @@ -389,7 +383,6 @@ def test_gauge_format_with_const_labels(self): self.assertEqual(valid_result, result) def test_gauge_format_text(self): - name = "container_memory_max_usage_bytes" doc = "Maximum memory usage ever recorded in bytes." @@ -482,7 +475,6 @@ def test_gauge_format_with_timestamp(self): self.assertTrue(re.match(result_regex, result)) def test_single_gauge_format_text(self): - name = "prometheus_local_storage_indexing_queue_capacity" doc = "The capacity of the indexing queue." @@ -672,7 +664,6 @@ def test_summary_format_timestamp(self): self.assertTrue(re.match(result_regex, result)) def test_registry_marshall(self): - format_times = 3 counter_data = ( diff --git a/tests/test_histogram.py b/tests/test_histogram.py index 0e1f7e9..7cfaee7 100644 --- a/tests/test_histogram.py +++ b/tests/test_histogram.py @@ -12,7 +12,6 @@ class TestHistogram(unittest.TestCase): """Test histogram module""" def test_histogram(self): - with self.assertRaises(Exception) as context: h = Histogram() self.assertEqual("Must have at least two buckets", str(context.exception)) @@ -40,7 +39,6 @@ def test_histogram(self): self.assertEqual(tuple(h.buckets.values()), expected_values) def test_linear_bucket_helper_functions(self): - buckets = linearBuckets(1, 2, 5) self.assertEqual(buckets, [1, 3, 5, 7, 9]) @@ -53,7 +51,6 @@ def test_linear_bucket_helper_functions(self): ) def test_exponential_bucket_helper_functions(self): - buckets = exponentialBuckets(1, 10, 5) self.assertEqual(buckets, [1, 10, 100, 1000, 10000]) h = Histogram(*buckets) diff --git a/tests/test_negotiate.py b/tests/test_negotiate.py index 1f6cb89..3a9892c 100644 --- a/tests/test_negotiate.py +++ b/tests/test_negotiate.py @@ -3,15 +3,6 @@ from aioprometheus.formats import text from aioprometheus.negotiator import negotiate -try: - import prometheus_metrics_proto as pmp - - from aioprometheus.formats import binary - - have_pmp = True -except ImportError: - have_pmp = False - class TestNegotiate(unittest.TestCase): def test_text_default(self): @@ -43,15 +34,3 @@ def test_no_accept_header(self): """check request with no accept header works""" self.assertEqual(text.TextFormatter, negotiate(set())) self.assertEqual(text.TextFormatter, negotiate(set([""]))) - - @unittest.skipUnless(have_pmp, "prometheus_metrics_proto library is not available") - def test_protobuffer(self): - """check that a protobuf formatter is returned""" - headers = ( - "proto=io.prometheus.client.MetricFamily;application/vnd.google.protobuf;encoding=delimited", - "application/vnd.google.protobuf;proto=io.prometheus.client.MetricFamily;encoding=delimited", - "encoding=delimited;application/vnd.google.protobuf;proto=io.prometheus.client.MetricFamily", - ) - - for accept in headers: - self.assertEqual(binary.BinaryFormatter, negotiate(set(accept.split(";")))) diff --git a/tests/test_pusher.py b/tests/test_pusher.py index 458dbcb..7cb75c8 100644 --- a/tests/test_pusher.py +++ b/tests/test_pusher.py @@ -1,7 +1,8 @@ import asyncio import sys +import unittest -import asynctest +from aiohttp_basicauth import BasicAuthMiddleware from aioprometheus import REGISTRY, Counter, Registry @@ -51,22 +52,24 @@ async def slow_handler(self, request): resp = aiohttp.web.Response(status=200) return resp - async def start(self, addr="127.0.0.1", port=None): + async def start(self, addr="127.0.0.1", port=None, middleware=None): self._app = aiohttp.web.Application() self._app.router.add_route( "*", "/metrics/job/{job}{tail:(/.+)?}", self.handler ) self._app.router.add_route("*", "/api/v1/import/prometheus", self.handler) self._app.router.add_route("*", "/slow", self.slow_handler) + if middleware: + self._app.middlewares.append(middleware) self._runner = aiohttp.web.AppRunner(self._app) await self._runner.setup() self._site = aiohttp.web.TCPSite(self._runner, addr, port) await self._site.start() # IPV4 returns a 2-Tuple, IPV6 returns a 4-Tuple _details = self._site._server.sockets[0].getsockname() - _host, _port = _details[0:2] - self.port = _port - self.url = f"http://{addr}:{_port}" + _host, port = _details[0:2] + self.port = port + self.url = f"http://{addr}:{port}" # TODO: replace the above with url = self._site.name when aiohttp # issue #3018 is resolved. @@ -77,13 +80,13 @@ async def stop(self): self._runner = None -@asynctest.skipUnless(have_aiohttp, "aiohttp library is not available") -class TestPusher(asynctest.TestCase): - async def setUp(self): +@unittest.skipUnless(have_aiohttp, "aiohttp library is not available") +class TestPusher(unittest.IsolatedAsyncioTestCase): + async def asyncSetUp(self): self.server = TestPusherServer() await self.server.start() - async def tearDown(self): + async def asyncTearDown(self): await self.server.stop() REGISTRY.clear() @@ -92,13 +95,12 @@ async def test_push_job_ping_victoriametrics(self): # Create a pusher with the path for VictoriaMetrics p = Pusher(job_name, self.server.url, path="/api/v1/import/prometheus") - registry = Registry() - c = Counter("total_requests", "Total requests.", {}, registry=registry) + c = Counter("total_requests", "Total requests.", {}) c.inc({"url": "/p/user"}) # Push to the pushgateway - resp = await p.replace(registry) + resp = await p.replace(REGISTRY) self.assertEqual(resp.status, 200) self.assertEqual("/api/v1/import/prometheus", self.server.test_results["path"]) @@ -106,13 +108,12 @@ async def test_push_job_ping_victoriametrics(self): async def test_push_job_ping(self): job_name = "my-job" p = Pusher(job_name, self.server.url) - registry = Registry() - c = Counter("total_requests", "Total requests.", {}, registry=registry) + c = Counter("total_requests", "Total requests.", {}) c.inc({"url": "/p/user"}) # Push to the pushgateway - resp = await p.replace(registry) + resp = await p.replace(REGISTRY) self.assertEqual(resp.status, 200) self.assertEqual("/metrics/job/my-job", self.server.test_results["path"]) @@ -126,13 +127,12 @@ async def test_grouping_key(self): self.server.url, grouping_key={"instance": "127.0.0.1:1234"}, ) - registry = Registry() - c = Counter("total_requests", "Total requests.", {}, registry=registry) + c = Counter("total_requests", "Total requests.", {}) c.inc({}) # Push to the pushgateway - resp = await p.replace(registry) + resp = await p.replace(REGISTRY) self.assertEqual(resp.status, 200) self.assertEqual( @@ -149,13 +149,12 @@ async def test_grouping_key_with_empty_value(self): self.server.url, grouping_key={"first": "", "second": "foo"}, ) - registry = Registry() - c = Counter("example_total", "Total examples", {}, registry=registry) + c = Counter("example_total", "Total examples", {}) c.inc({}) # Push to the pushgateway - resp = await p.replace(registry) + resp = await p.replace(REGISTRY) self.assertEqual(resp.status, 200) self.assertEqual( @@ -172,13 +171,12 @@ async def test_grouping_key_with_value_containing_slash(self): self.server.url, grouping_key={"path": "/var/tmp"}, ) - registry = Registry() - c = Counter("exec_total", "Total executions", {}, registry=registry) + c = Counter("exec_total", "Total executions", {}) c.inc({}) # Push to the pushgateway - resp = await p.replace(registry) + resp = await p.replace(REGISTRY) self.assertEqual(resp.status, 200) # Generated base64 content include '=' as padding. @@ -202,11 +200,6 @@ async def test_push_add(self): b"# TYPE counter_test counter\n" b'counter_test{c_sample="1",c_subsample="b",type="counter"} 400\n' ) - # BinaryFormatter expected result - # valid_result = (b'[\n\x0ccounter_test\x12\nA counter.\x18\x00"=\n\r' - # b'\n\x08c_sample\x12\x011\n\x10\n\x0bc_subsample\x12' - # b'\x01b\n\x0f\n\x04type\x12\x07counter\x1a\t\t\x00' - # b'\x00\x00\x00\x00\x00y@') # Push to the pushgateway resp = await p.add(REGISTRY) @@ -231,11 +224,6 @@ async def test_push_replace(self): b"# TYPE counter_test counter\n" b'counter_test{c_sample="1",c_subsample="b",type="counter"} 400\n' ) - # BinaryFormatter expected result - # valid_result = (b'[\n\x0ccounter_test\x12\nA counter.\x18\x00"=\n\r' - # b'\n\x08c_sample\x12\x011\n\x10\n\x0bc_subsample\x12' - # b'\x01b\n\x0f\n\x04type\x12\x07counter\x1a\t\t\x00' - # b'\x00\x00\x00\x00\x00y@') # Push to the pushgateway resp = await p.replace(REGISTRY) @@ -260,11 +248,6 @@ async def test_push_delete(self): b"# TYPE counter_test counter\n" b'counter_test{c_sample="1",c_subsample="b",type="counter"} 400\n' ) - # BinaryFormatter expected result - # valid_result = (b'[\n\x0ccounter_test\x12\nA counter.\x18\x00"=\n' - # b'\r\n\x08c_sample\x12\x011\n\x10\n\x0bc_subsample' - # b'\x12\x01b\n\x0f\n\x04type\x12\x07counter\x1a\t\t' - # b'\x00\x00\x00\x00\x00\x00y@') # Push to the pushgateway resp = await p.delete(REGISTRY) @@ -274,7 +257,7 @@ async def test_push_delete(self): self.assertEqual("DELETE", self.server.test_results["method"]) self.assertEqual(valid_result, self.server.test_results["body"]) - @asynctest.skipUnless(sys.version_info > (3, 8, 0), "requires 3.8+") + @unittest.skipUnless(sys.version_info > (3, 8, 0), "requires 3.8+") async def test_push_timeout(self): job_name = "my-job" p = Pusher(job_name, self.server.url, path="/slow") @@ -300,3 +283,34 @@ async def test_push_timeout(self): with self.assertRaises(asyncio.exceptions.TimeoutError): await p.replace(REGISTRY, timeout=timeout) + + +@unittest.skipUnless(have_aiohttp, "aiohttp library is not available") +class TestPusherBasicAuth(unittest.IsolatedAsyncioTestCase): + """A minimal duplicate of the test suite above that demonstrates auth used with Pusher""" + + async def asyncSetUp(self): + self.username = "Joe" + self.password = "4321" + self.server = TestPusherServer() + bam = BasicAuthMiddleware(username=self.username, password=self.password) + await self.server.start(middleware=bam) + + async def asyncTearDown(self): + await self.server.stop() + REGISTRY.clear() + + async def test_push_job_ping_with_auth(self): + job_name = "my-job" + p = Pusher(job_name, self.server.url) + c = Counter("total_requests", "Total requests.", {}) + + c.inc({"url": "/p/user"}) + + self.auth = aiohttp.BasicAuth(self.username, password=self.password) + + # Push to the pushgateway + resp = await p.replace(REGISTRY, auth=self.auth) + self.assertEqual(resp.status, 200) + + self.assertEqual("/metrics/job/my-job", self.server.test_results["path"]) diff --git a/tests/test_quart.py b/tests/test_quart.py index ee68a39..5c3c942 100644 --- a/tests/test_quart.py +++ b/tests/test_quart.py @@ -1,8 +1,6 @@ -import logging import sys import unittest - -import asynctest +import unittest.mock from aioprometheus import REGISTRY, Counter, MetricsMiddleware, formats, render @@ -15,16 +13,9 @@ except ImportError: have_quart = False -try: - import prometheus_metrics_proto as pmp - - have_pmp = True -except ImportError: - have_pmp = False - @unittest.skipUnless(have_quart, "Quart library is not available") -class TestQuartRender(asynctest.TestCase): +class TestQuartRender(unittest.IsolatedAsyncioTestCase): """Test exposing Prometheus metrics from within a Quart app""" def tearDown(self): @@ -74,52 +65,6 @@ async def handle_metrics(): # Check content self.assertIn(b'events{path="/"} 1', payload) - @unittest.skipUnless(have_pmp, "prometheus_metrics_proto library is not available") - async def test_render_binary(self): - """check render binary usage in Quart app""" - - app = Quart(__name__) - app.events_counter = Counter("events", "Number of events.") - - @app.route("/") - async def index(): - app.events_counter.inc({"path": "/"}) - return "hello" - - @app.route("/metrics") - async def handle_metrics(): - content, http_headers = render(REGISTRY, request.headers.getlist("accept")) - return content, http_headers - - # The test client also starts the web service - test_client = app.test_client() - - # Access root to increment metric counter - response = await test_client.get("/") - self.assertEqual(response.status_code, 200) - - # Get binary format - response = await test_client.get( - "/metrics", - headers={"accept": formats.binary.BINARY_CONTENT_TYPE}, - ) - self.assertEqual(response.status_code, 200) - self.assertIn( - formats.binary.BINARY_CONTENT_TYPE, - response.headers.get("content-type"), - ) - - payload = await response.get_data() - metrics = pmp.decode(payload) - self.assertEqual(len(metrics), 1) - mf = metrics[0] - self.assertIsInstance(mf, pmp.MetricFamily) - self.assertEqual(mf.type, pmp.COUNTER) - self.assertEqual(len(mf.metric), 1) - self.assertEqual(mf.metric[0].counter.value, 1) - self.assertEqual(mf.metric[0].label[0].name, "path") - self.assertEqual(mf.metric[0].label[0].value, "/") - async def test_asgi_middleware(self): """check ASGI middleware usage in Quart app""" @@ -193,10 +138,9 @@ async def hello(): # Access boom route to trigger exception metric update. # Silence the stderr output log generated by Quart when it captures # the exception. - with self.assertLogs("quart.app", logging.ERROR): - with asynctest.mock.patch.object(sys.stderr, "write") as mock_stderr: - response = await test_client.get("/boom") - self.assertEqual(response.status_code, 500) + with unittest.mock.patch.object(sys.stderr, "write") as mock_stderr: + response = await test_client.get("/boom") + self.assertEqual(response.status_code, 500) response = await test_client.get("/metrics", headers={"accept": "*/*"}) self.assertEqual(response.status_code, 200) @@ -290,10 +234,9 @@ async def hello(): # Access boom route to trigger exception metric update. # Silence the stderr output log generated by Quart when it captures # the exception. - with self.assertLogs("quart.app", logging.ERROR): - with asynctest.mock.patch.object(sys.stderr, "write") as mock_stderr: - response = await test_client.get("/boom") - self.assertEqual(response.status_code, 500) + with unittest.mock.patch.object(sys.stderr, "write") as mock_stderr: + response = await test_client.get("/boom") + self.assertEqual(response.status_code, 500) response = await test_client.get("/metrics", headers={"accept": "*/*"}) self.assertEqual(response.status_code, 200) diff --git a/tests/test_renderer.py b/tests/test_renderer.py index bedfcd3..55de0ad 100644 --- a/tests/test_renderer.py +++ b/tests/test_renderer.py @@ -1,16 +1,9 @@ -import asynctest +import unittest from aioprometheus import REGISTRY, formats, render -try: - import prometheus_metrics_proto as pmp - have_pmp = True -except ImportError: - have_pmp = False - - -class TestRenderer(asynctest.TestCase): +class TestRenderer(unittest.IsolatedAsyncioTestCase): async def test_invalid_registry(self): """check only valid registry can be provided""" for invalid_registry in ["nope", dict(), list()]: @@ -36,13 +29,3 @@ async def test_render_text(self): accepts_headers = ("text/plain;",) content, http_headers = render(REGISTRY, accepts_headers) self.assertEqual(http_headers["Content-Type"], formats.text.TEXT_CONTENT_TYPE) - - @asynctest.skipUnless(have_pmp, "prometheus_metrics_proto library is not available") - async def test_render_binary(self): - """check metrics can be rendered using binary format""" - accepts_headers = (formats.binary.BINARY_CONTENT_TYPE,) - content, http_headers = render(REGISTRY, accepts_headers) - self.assertEqual( - http_headers["Content-Type"], - formats.binary.BINARY_CONTENT_TYPE, - )